aimux-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +743 -0
  2. package/bin/aimux +2 -0
  3. package/dist/agent-events.d.ts +20 -0
  4. package/dist/agent-events.js +2 -0
  5. package/dist/agent-events.js.map +1 -0
  6. package/dist/agent-message-parts.d.ts +17 -0
  7. package/dist/agent-message-parts.js +31 -0
  8. package/dist/agent-message-parts.js.map +1 -0
  9. package/dist/agent-output-parser.d.ts +16 -0
  10. package/dist/agent-output-parser.js +229 -0
  11. package/dist/agent-output-parser.js.map +1 -0
  12. package/dist/agent-tracker.d.ts +9 -0
  13. package/dist/agent-tracker.js +144 -0
  14. package/dist/agent-tracker.js.map +1 -0
  15. package/dist/agent-watcher.d.ts +15 -0
  16. package/dist/agent-watcher.js +2 -0
  17. package/dist/agent-watcher.js.map +1 -0
  18. package/dist/attachment-store.d.ts +35 -0
  19. package/dist/attachment-store.js +129 -0
  20. package/dist/attachment-store.js.map +1 -0
  21. package/dist/builtin-metadata-watchers.d.ts +2 -0
  22. package/dist/builtin-metadata-watchers.js +275 -0
  23. package/dist/builtin-metadata-watchers.js.map +1 -0
  24. package/dist/claude-hooks.d.ts +29 -0
  25. package/dist/claude-hooks.js +106 -0
  26. package/dist/claude-hooks.js.map +1 -0
  27. package/dist/config.d.ts +78 -0
  28. package/dist/config.js +172 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/context/compactor.d.ts +20 -0
  31. package/dist/context/compactor.js +212 -0
  32. package/dist/context/compactor.js.map +1 -0
  33. package/dist/context/context-bridge.d.ts +67 -0
  34. package/dist/context/context-bridge.js +471 -0
  35. package/dist/context/context-bridge.js.map +1 -0
  36. package/dist/context/context-file.d.ts +11 -0
  37. package/dist/context/context-file.js +93 -0
  38. package/dist/context/context-file.js.map +1 -0
  39. package/dist/context/history.d.ts +40 -0
  40. package/dist/context/history.js +108 -0
  41. package/dist/context/history.js.map +1 -0
  42. package/dist/daemon.d.ts +39 -0
  43. package/dist/daemon.js +344 -0
  44. package/dist/daemon.js.map +1 -0
  45. package/dist/dashboard-session-registry.d.ts +47 -0
  46. package/dist/dashboard-session-registry.js +161 -0
  47. package/dist/dashboard-session-registry.js.map +1 -0
  48. package/dist/dashboard-state.d.ts +18 -0
  49. package/dist/dashboard-state.js +26 -0
  50. package/dist/dashboard-state.js.map +1 -0
  51. package/dist/dashboard.d.ts +118 -0
  52. package/dist/dashboard.js +91 -0
  53. package/dist/dashboard.js.map +1 -0
  54. package/dist/debug.d.ts +7 -0
  55. package/dist/debug.js +41 -0
  56. package/dist/debug.js.map +1 -0
  57. package/dist/fast-control.d.ts +45 -0
  58. package/dist/fast-control.js +174 -0
  59. package/dist/fast-control.js.map +1 -0
  60. package/dist/hotkeys.d.ts +44 -0
  61. package/dist/hotkeys.js +118 -0
  62. package/dist/hotkeys.js.map +1 -0
  63. package/dist/http-client.d.ts +10 -0
  64. package/dist/http-client.js +54 -0
  65. package/dist/http-client.js.map +1 -0
  66. package/dist/instance-directory.d.ts +32 -0
  67. package/dist/instance-directory.js +82 -0
  68. package/dist/instance-directory.js.map +1 -0
  69. package/dist/instance-registry.d.ts +38 -0
  70. package/dist/instance-registry.js +208 -0
  71. package/dist/instance-registry.js.map +1 -0
  72. package/dist/key-parser.d.ts +30 -0
  73. package/dist/key-parser.js +272 -0
  74. package/dist/key-parser.js.map +1 -0
  75. package/dist/last-used.d.ts +31 -0
  76. package/dist/last-used.js +93 -0
  77. package/dist/last-used.js.map +1 -0
  78. package/dist/main.d.ts +1 -0
  79. package/dist/main.js +2483 -0
  80. package/dist/main.js.map +1 -0
  81. package/dist/metadata-server.d.ts +268 -0
  82. package/dist/metadata-server.js +1379 -0
  83. package/dist/metadata-server.js.map +1 -0
  84. package/dist/metadata-store.d.ts +80 -0
  85. package/dist/metadata-store.js +87 -0
  86. package/dist/metadata-store.js.map +1 -0
  87. package/dist/multiplexer.d.ts +471 -0
  88. package/dist/multiplexer.js +5714 -0
  89. package/dist/multiplexer.js.map +1 -0
  90. package/dist/notification-context.d.ts +18 -0
  91. package/dist/notification-context.js +68 -0
  92. package/dist/notification-context.js.map +1 -0
  93. package/dist/notifications.d.ts +38 -0
  94. package/dist/notifications.js +111 -0
  95. package/dist/notifications.js.map +1 -0
  96. package/dist/notify.d.ts +10 -0
  97. package/dist/notify.js +62 -0
  98. package/dist/notify.js.map +1 -0
  99. package/dist/orchestration-actions.d.ts +76 -0
  100. package/dist/orchestration-actions.js +310 -0
  101. package/dist/orchestration-actions.js.map +1 -0
  102. package/dist/orchestration-dispatcher.d.ts +22 -0
  103. package/dist/orchestration-dispatcher.js +49 -0
  104. package/dist/orchestration-dispatcher.js.map +1 -0
  105. package/dist/orchestration-routing.d.ts +20 -0
  106. package/dist/orchestration-routing.js +78 -0
  107. package/dist/orchestration-routing.js.map +1 -0
  108. package/dist/orchestration.d.ts +26 -0
  109. package/dist/orchestration.js +110 -0
  110. package/dist/orchestration.js.map +1 -0
  111. package/dist/osc-notifications.d.ts +15 -0
  112. package/dist/osc-notifications.js +180 -0
  113. package/dist/osc-notifications.js.map +1 -0
  114. package/dist/paths.d.ts +55 -0
  115. package/dist/paths.js +259 -0
  116. package/dist/paths.js.map +1 -0
  117. package/dist/plugin-runtime.d.ts +46 -0
  118. package/dist/plugin-runtime.js +180 -0
  119. package/dist/plugin-runtime.js.map +1 -0
  120. package/dist/project-events.d.ts +36 -0
  121. package/dist/project-events.js +63 -0
  122. package/dist/project-events.js.map +1 -0
  123. package/dist/project-scanner.d.ts +38 -0
  124. package/dist/project-scanner.js +243 -0
  125. package/dist/project-scanner.js.map +1 -0
  126. package/dist/project-service-manifest.d.ts +18 -0
  127. package/dist/project-service-manifest.js +56 -0
  128. package/dist/project-service-manifest.js.map +1 -0
  129. package/dist/recency.d.ts +2 -0
  130. package/dist/recency.js +34 -0
  131. package/dist/recency.js.map +1 -0
  132. package/dist/recorder.d.ts +14 -0
  133. package/dist/recorder.js +130 -0
  134. package/dist/recorder.js.map +1 -0
  135. package/dist/session-bootstrap.d.ts +45 -0
  136. package/dist/session-bootstrap.js +436 -0
  137. package/dist/session-bootstrap.js.map +1 -0
  138. package/dist/session-message-history.d.ts +27 -0
  139. package/dist/session-message-history.js +105 -0
  140. package/dist/session-message-history.js.map +1 -0
  141. package/dist/session-runtime.d.ts +44 -0
  142. package/dist/session-runtime.js +56 -0
  143. package/dist/session-runtime.js.map +1 -0
  144. package/dist/session-semantics.d.ts +35 -0
  145. package/dist/session-semantics.js +110 -0
  146. package/dist/session-semantics.js.map +1 -0
  147. package/dist/status-detector.d.ts +17 -0
  148. package/dist/status-detector.js +67 -0
  149. package/dist/status-detector.js.map +1 -0
  150. package/dist/statusline-model.d.ts +103 -0
  151. package/dist/statusline-model.js +177 -0
  152. package/dist/statusline-model.js.map +1 -0
  153. package/dist/task-dispatcher.d.ts +63 -0
  154. package/dist/task-dispatcher.js +210 -0
  155. package/dist/task-dispatcher.js.map +1 -0
  156. package/dist/task-workflow.d.ts +13 -0
  157. package/dist/task-workflow.js +153 -0
  158. package/dist/task-workflow.js.map +1 -0
  159. package/dist/tasks.d.ts +60 -0
  160. package/dist/tasks.js +120 -0
  161. package/dist/tasks.js.map +1 -0
  162. package/dist/team.d.ts +28 -0
  163. package/dist/team.js +91 -0
  164. package/dist/team.js.map +1 -0
  165. package/dist/terminal-host.d.ts +10 -0
  166. package/dist/terminal-host.js +52 -0
  167. package/dist/terminal-host.js.map +1 -0
  168. package/dist/threads.d.ts +61 -0
  169. package/dist/threads.js +200 -0
  170. package/dist/threads.js.map +1 -0
  171. package/dist/tmux-doctor.d.ts +47 -0
  172. package/dist/tmux-doctor.js +112 -0
  173. package/dist/tmux-doctor.js.map +1 -0
  174. package/dist/tmux-runtime-manager.d.ts +164 -0
  175. package/dist/tmux-runtime-manager.js +794 -0
  176. package/dist/tmux-runtime-manager.js.map +1 -0
  177. package/dist/tmux-session-transport.d.ts +31 -0
  178. package/dist/tmux-session-transport.js +115 -0
  179. package/dist/tmux-session-transport.js.map +1 -0
  180. package/dist/tmux-statusline.d.ts +17 -0
  181. package/dist/tmux-statusline.js +166 -0
  182. package/dist/tmux-statusline.js.map +1 -0
  183. package/dist/tool-output-watchers.d.ts +10 -0
  184. package/dist/tool-output-watchers.js +190 -0
  185. package/dist/tool-output-watchers.js.map +1 -0
  186. package/dist/tui/render/box.d.ts +1 -0
  187. package/dist/tui/render/box.js +20 -0
  188. package/dist/tui/render/box.js.map +1 -0
  189. package/dist/tui/render/text.d.ts +8 -0
  190. package/dist/tui/render/text.js +92 -0
  191. package/dist/tui/render/text.js.map +1 -0
  192. package/dist/tui/screens/dashboard-renderers.d.ts +23 -0
  193. package/dist/tui/screens/dashboard-renderers.js +411 -0
  194. package/dist/tui/screens/dashboard-renderers.js.map +1 -0
  195. package/dist/tui/screens/overlay-renderers.d.ts +10 -0
  196. package/dist/tui/screens/overlay-renderers.js +274 -0
  197. package/dist/tui/screens/overlay-renderers.js.map +1 -0
  198. package/dist/tui/screens/subscreen-renderers.d.ts +9 -0
  199. package/dist/tui/screens/subscreen-renderers.js +327 -0
  200. package/dist/tui/screens/subscreen-renderers.js.map +1 -0
  201. package/dist/workflow.d.ts +19 -0
  202. package/dist/workflow.js +111 -0
  203. package/dist/workflow.js.map +1 -0
  204. package/dist/worktree.d.ts +23 -0
  205. package/dist/worktree.js +101 -0
  206. package/dist/worktree.js.map +1 -0
  207. package/package.json +70 -0
@@ -0,0 +1,1379 @@
1
+ import { createServer } from "node:http";
2
+ import { createHash } from "node:crypto";
3
+ import { statSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { getProjectId, getProjectStateDir } from "./paths.js";
7
+ import { updateSessionMetadata, clearSessionLogs, saveMetadataEndpoint, loadMetadataState, } from "./metadata-store.js";
8
+ import { notifyAlert } from "./notify.js";
9
+ import { clearNotifications, listNotifications, markNotificationsRead, unreadNotificationCount, } from "./notifications.js";
10
+ import { updateNotificationContext } from "./notification-context.js";
11
+ import { AgentTracker } from "./agent-tracker.js";
12
+ import { createThread, listThreadSummaries, markThreadSeen, readMessages, readThread, setThreadStatus, } from "./threads.js";
13
+ import { sendDirectMessage, sendThreadMessage } from "./orchestration.js";
14
+ import { acceptHandoff, approveReview, acceptTask, assignTask, blockTask, completeHandoff, completeTask, reopenTask, requestTaskChanges, sendHandoff, } from "./orchestration-actions.js";
15
+ import { buildWorkflowEntries } from "./workflow.js";
16
+ import { markLastUsed } from "./last-used.js";
17
+ import { formatRelativeRecency } from "./recency.js";
18
+ import { getAttachment, getAttachmentContent, ingestAttachmentFromBase64, ingestAttachmentFromPath, } from "./attachment-store.js";
19
+ import { ProjectEventBus } from "./project-events.js";
20
+ import { getProjectServiceManifest } from "./project-service-manifest.js";
21
+ import { listSwitchableAgentItems, resolveAttentionAgent, resolveNextAgent, resolvePrevAgent, serializeFastControlItem, } from "./fast-control.js";
22
+ import { TmuxRuntimeManager } from "./tmux-runtime-manager.js";
23
+ function resolveLiveClientTty(tmux, currentClientSession, preferredClientTty) {
24
+ const normalizedTty = preferredClientTty?.trim();
25
+ if (normalizedTty && tmux.findClientByTty(normalizedTty)) {
26
+ return normalizedTty;
27
+ }
28
+ const normalizedSession = currentClientSession?.trim();
29
+ if (!normalizedSession)
30
+ return undefined;
31
+ const liveClient = tmux.listClients().find((client) => client.sessionName === normalizedSession);
32
+ return liveClient?.tty || undefined;
33
+ }
34
+ function openTarget(tmux, target, currentClientSession, clientTty) {
35
+ const liveClientTty = resolveLiveClientTty(tmux, currentClientSession, clientTty) ?? tmux.getAttachedClientForTarget(target)?.tty;
36
+ if (liveClientTty) {
37
+ tmux.switchClientToTarget(liveClientTty, target);
38
+ tmux.refreshStatus();
39
+ if (target.windowName.startsWith("dashboard")) {
40
+ tmux.sendFocusIn(target);
41
+ }
42
+ return;
43
+ }
44
+ if (currentClientSession) {
45
+ const linkedTarget = tmux.getTargetByWindowId(currentClientSession, target.windowId);
46
+ if (linkedTarget) {
47
+ tmux.switchClient(currentClientSession, linkedTarget.windowIndex);
48
+ tmux.refreshStatus();
49
+ if (linkedTarget.windowName.startsWith("dashboard")) {
50
+ tmux.sendFocusIn(linkedTarget);
51
+ }
52
+ return;
53
+ }
54
+ }
55
+ tmux.openTarget(target, { insideTmux: Boolean(currentClientSession) });
56
+ tmux.refreshStatus();
57
+ }
58
+ function displayMenu(tmux, items, currentWindowId, currentClientSession, clientTty) {
59
+ const menuItems = items.map((item) => ({
60
+ label: item.target.windowId === currentWindowId ? `${item.label}*` : item.label,
61
+ target: item.target,
62
+ }));
63
+ const liveClientTty = resolveLiveClientTty(tmux, currentClientSession, clientTty);
64
+ if (liveClientTty) {
65
+ tmux.displayWindowMenuForClient(liveClientTty, "aimux", menuItems);
66
+ return;
67
+ }
68
+ tmux.displayWindowMenu("aimux", menuItems);
69
+ }
70
+ function markTargetUsed(tmux, projectRoot, target, currentClientSession, itemId) {
71
+ const resolvedItemId = itemId ||
72
+ tmux
73
+ .listManagedWindows(tmux.getProjectSession(projectRoot).sessionName)
74
+ .find((entry) => entry.target.windowId === target.windowId)?.metadata.sessionId;
75
+ if (!resolvedItemId)
76
+ return;
77
+ markLastUsed(projectRoot, {
78
+ itemId: resolvedItemId,
79
+ clientSession: currentClientSession,
80
+ });
81
+ }
82
+ function getDashboardCommandSpec(projectRoot) {
83
+ const currentFile = fileURLToPath(import.meta.url);
84
+ const mainScript = join(dirname(currentFile), "main.js");
85
+ return {
86
+ dashboardCommand: {
87
+ cwd: projectRoot,
88
+ command: process.execPath,
89
+ args: [mainScript, "--tmux-dashboard-internal"],
90
+ },
91
+ dashboardBuildStamp: String(statSync(mainScript).mtimeMs),
92
+ };
93
+ }
94
+ function desiredPort() {
95
+ const hash = createHash("sha1").update(getProjectId()).digest("hex").slice(0, 6);
96
+ return 43000 + (parseInt(hash, 16) % 10000);
97
+ }
98
+ async function readJson(req) {
99
+ const chunks = [];
100
+ for await (const chunk of req) {
101
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
102
+ }
103
+ const body = Buffer.concat(chunks).toString("utf8").trim();
104
+ return body ? JSON.parse(body) : {};
105
+ }
106
+ function send(res, status, body) {
107
+ const payload = JSON.stringify(body);
108
+ res.statusCode = status;
109
+ res.setHeader("content-type", "application/json");
110
+ res.setHeader("content-length", Buffer.byteLength(payload));
111
+ res.setHeader("access-control-allow-origin", "*");
112
+ res.setHeader("connection", "close");
113
+ res.end(payload);
114
+ }
115
+ function sendBytes(res, status, body, mimeType) {
116
+ res.statusCode = status;
117
+ res.setHeader("content-type", mimeType);
118
+ res.setHeader("content-length", body.byteLength);
119
+ res.setHeader("cache-control", "private, max-age=31536000, immutable");
120
+ res.setHeader("access-control-allow-origin", "*");
121
+ res.setHeader("connection", "close");
122
+ res.end(body);
123
+ }
124
+ function sendSseEvent(res, event, data) {
125
+ res.write(`event: ${event}\n`);
126
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
127
+ }
128
+ export class MetadataServer {
129
+ options;
130
+ server = null;
131
+ port = 0;
132
+ tracker = new AgentTracker();
133
+ eventBus;
134
+ unsubscribeAlertSink = null;
135
+ constructor(options = {}) {
136
+ this.options = options;
137
+ this.eventBus = options.events?.bus ?? new ProjectEventBus();
138
+ this.unsubscribeAlertSink = this.eventBus.subscribe((event) => {
139
+ if (event.type !== "alert")
140
+ return;
141
+ notifyAlert(event);
142
+ });
143
+ }
144
+ async start() {
145
+ if (this.server)
146
+ return;
147
+ this.server = createServer((req, res) => {
148
+ void this.handle(req, res);
149
+ });
150
+ await this.listen(desiredPort()).catch(async () => {
151
+ await this.listen(0);
152
+ });
153
+ saveMetadataEndpoint({
154
+ host: "127.0.0.1",
155
+ port: this.port,
156
+ pid: process.pid,
157
+ updatedAt: new Date().toISOString(),
158
+ });
159
+ }
160
+ stop() {
161
+ this.server?.close();
162
+ this.server = null;
163
+ this.unsubscribeAlertSink?.();
164
+ this.unsubscribeAlertSink = null;
165
+ }
166
+ getAddress() {
167
+ if (!this.server || this.port === 0)
168
+ return null;
169
+ return { host: "127.0.0.1", port: this.port };
170
+ }
171
+ getEventBus() {
172
+ return this.eventBus;
173
+ }
174
+ listen(port) {
175
+ return new Promise((resolve, reject) => {
176
+ if (!this.server)
177
+ return reject(new Error("server not initialized"));
178
+ this.server.once("error", reject);
179
+ this.server.listen(port, "127.0.0.1", () => {
180
+ this.server?.off("error", reject);
181
+ const address = this.server?.address();
182
+ if (!address || typeof address === "string")
183
+ return reject(new Error("invalid address"));
184
+ this.port = address.port;
185
+ resolve();
186
+ });
187
+ });
188
+ }
189
+ emitAlert(input) {
190
+ this.eventBus.publishAlert(input);
191
+ }
192
+ emitThreadWaitingAlert(input) {
193
+ for (const recipient of [...new Set((input.recipients ?? []).map((value) => value?.trim()).filter(Boolean))]) {
194
+ if (recipient === input.from?.trim())
195
+ continue;
196
+ this.emitAlert({
197
+ kind: input.kind,
198
+ sessionId: recipient,
199
+ threadId: input.threadId,
200
+ worktreePath: input.worktreePath,
201
+ title: input.title,
202
+ message: input.message,
203
+ dedupeKey: `${input.kind}:${input.threadId}:${recipient}`,
204
+ cooldownMs: input.cooldownMs ?? 15_000,
205
+ });
206
+ }
207
+ }
208
+ emitAssignedTaskAlert(input) {
209
+ const recipient = input.task.assignedTo?.trim();
210
+ if (!recipient)
211
+ return;
212
+ const kind = input.task.type === "review" ? "review_waiting" : "task_assigned";
213
+ const noun = input.task.type === "review" ? "Review" : "Task";
214
+ this.emitAlert({
215
+ kind,
216
+ sessionId: recipient,
217
+ taskId: input.task.id,
218
+ threadId: input.thread?.id,
219
+ worktreePath: input.thread?.worktreePath,
220
+ title: `${noun} assigned: ${input.task.description}`,
221
+ message: input.task.type === "review"
222
+ ? "A review is waiting for your attention."
223
+ : "A task is waiting for your attention.",
224
+ dedupeKey: `${kind}:${input.task.id}:${recipient}`,
225
+ cooldownMs: 15_000,
226
+ });
227
+ }
228
+ emitReviewOutcomeAlert(input) {
229
+ const recipient = input.task.assignedBy?.trim();
230
+ if (!recipient)
231
+ return;
232
+ const isBlocked = input.kind === "blocked";
233
+ this.emitAlert({
234
+ kind: input.kind,
235
+ sessionId: recipient,
236
+ taskId: input.task.id,
237
+ threadId: input.thread?.id,
238
+ worktreePath: input.thread?.worktreePath,
239
+ title: `${isBlocked ? "Changes requested" : "Review approved"}: ${input.task.description}`,
240
+ message: input.task.reviewFeedback?.trim() || input.fallbackMessage,
241
+ dedupeKey: `${isBlocked ? "review-blocked" : "review-approved"}:${input.task.id}:${recipient}`,
242
+ cooldownMs: 15_000,
243
+ });
244
+ }
245
+ resolveAlertRecipients(explicit, message, fallback) {
246
+ const fromExplicit = explicit?.map((value) => value?.trim()).filter(Boolean);
247
+ if (fromExplicit && fromExplicit.length > 0)
248
+ return [...new Set(fromExplicit)];
249
+ const payload = message;
250
+ const fromMessage = payload?.deliveredTo?.map((value) => value?.trim()).filter(Boolean);
251
+ if (fromMessage && fromMessage.length > 0)
252
+ return [...new Set(fromMessage)];
253
+ const fallbackRecipients = payload?.to ?? fallback ?? [];
254
+ return [...new Set(fallbackRecipients.map((value) => value?.trim()).filter(Boolean))];
255
+ }
256
+ async handle(req, res) {
257
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
258
+ if (req.method === "GET" && url.pathname === "/events") {
259
+ const sessionFilter = url.searchParams.get("sessionId")?.trim() || null;
260
+ const startLineRaw = url.searchParams.get("startLine");
261
+ const intervalMsRaw = url.searchParams.get("intervalMs");
262
+ const startLine = startLineRaw === null || startLineRaw.trim() === "" ? undefined : Number.parseInt(startLineRaw, 10);
263
+ if (startLineRaw !== null && Number.isNaN(startLine)) {
264
+ send(res, 400, { ok: false, error: "startLine must be an integer" });
265
+ return;
266
+ }
267
+ const intervalMs = intervalMsRaw === null || intervalMsRaw.trim() === "" ? 500 : Number.parseInt(intervalMsRaw, 10);
268
+ if (Number.isNaN(intervalMs) || intervalMs < 100) {
269
+ send(res, 400, { ok: false, error: "intervalMs must be an integer >= 100" });
270
+ return;
271
+ }
272
+ res.statusCode = 200;
273
+ res.setHeader("content-type", "text/event-stream");
274
+ res.setHeader("cache-control", "no-cache, no-transform");
275
+ res.setHeader("connection", "keep-alive");
276
+ res.setHeader("x-accel-buffering", "no");
277
+ res.setHeader("access-control-allow-origin", "*");
278
+ res.flushHeaders?.();
279
+ let closed = false;
280
+ let keepaliveTimer = null;
281
+ let outputPollTimer = null;
282
+ let lastOutput;
283
+ const unsubscribe = this.eventBus.subscribe((event) => {
284
+ if (closed)
285
+ return;
286
+ if (sessionFilter && event.sessionId && event.sessionId !== sessionFilter)
287
+ return;
288
+ if (sessionFilter && !event.sessionId)
289
+ return;
290
+ sendSseEvent(res, event.type, event);
291
+ });
292
+ const cleanup = () => {
293
+ if (closed)
294
+ return;
295
+ closed = true;
296
+ unsubscribe();
297
+ if (keepaliveTimer)
298
+ clearInterval(keepaliveTimer);
299
+ keepaliveTimer = null;
300
+ if (outputPollTimer)
301
+ clearInterval(outputPollTimer);
302
+ outputPollTimer = null;
303
+ res.end();
304
+ };
305
+ req.on("close", cleanup);
306
+ req.on("aborted", cleanup);
307
+ res.on("close", cleanup);
308
+ const pollSessionOutput = async () => {
309
+ if (closed || !sessionFilter || !this.options.lifecycle?.readAgentOutput)
310
+ return;
311
+ try {
312
+ const result = await this.options.lifecycle.readAgentOutput({ sessionId: sessionFilter, startLine });
313
+ if (closed)
314
+ return;
315
+ if (result.output !== lastOutput) {
316
+ lastOutput = result.output;
317
+ sendSseEvent(res, "agent_output", {
318
+ sessionId: result.sessionId,
319
+ output: result.output,
320
+ startLine: result.startLine ?? startLine ?? -120,
321
+ parsed: result.parsed,
322
+ });
323
+ }
324
+ }
325
+ catch (error) {
326
+ sendSseEvent(res, "error", {
327
+ sessionId: sessionFilter,
328
+ error: error instanceof Error ? error.message : String(error),
329
+ });
330
+ cleanup();
331
+ }
332
+ };
333
+ sendSseEvent(res, "ready", {
334
+ projectId: getProjectId(),
335
+ ts: new Date().toISOString(),
336
+ sessionId: sessionFilter,
337
+ startLine: startLine ?? -120,
338
+ intervalMs,
339
+ });
340
+ if (sessionFilter && this.options.lifecycle?.readAgentOutput) {
341
+ await pollSessionOutput();
342
+ outputPollTimer = setInterval(() => {
343
+ void pollSessionOutput();
344
+ }, intervalMs);
345
+ outputPollTimer.unref?.();
346
+ }
347
+ keepaliveTimer = setInterval(() => {
348
+ if (closed)
349
+ return;
350
+ res.write(": keepalive\n\n");
351
+ }, 15_000);
352
+ keepaliveTimer.unref?.();
353
+ return;
354
+ }
355
+ if (req.method === "GET" && url.pathname === "/notifications") {
356
+ const unreadOnly = url.searchParams.get("unread") === "1";
357
+ const sessionId = url.searchParams.get("sessionId")?.trim() || undefined;
358
+ const notifications = listNotifications({ unreadOnly, sessionId });
359
+ send(res, 200, {
360
+ ok: true,
361
+ notifications,
362
+ unreadCount: unreadNotificationCount({ sessionId }),
363
+ });
364
+ return;
365
+ }
366
+ if (req.method === "GET" && url.pathname === "/health") {
367
+ send(res, 200, {
368
+ ok: true,
369
+ projectStateDir: getProjectStateDir(),
370
+ pid: process.pid,
371
+ serviceInfo: getProjectServiceManifest(),
372
+ });
373
+ return;
374
+ }
375
+ if (req.method === "GET" && url.pathname === "/state") {
376
+ send(res, 200, loadMetadataState());
377
+ return;
378
+ }
379
+ if (req.method === "GET" && url.pathname === "/desktop-state") {
380
+ if (!this.options.desktop?.getState) {
381
+ send(res, 501, { ok: false, error: "desktop state not supported by this service" });
382
+ return;
383
+ }
384
+ send(res, 200, {
385
+ ok: true,
386
+ serviceInfo: getProjectServiceManifest(),
387
+ ...this.options.desktop.getState(),
388
+ });
389
+ return;
390
+ }
391
+ if (req.method === "GET" && url.pathname === "/worktrees") {
392
+ if (!this.options.desktop?.listWorktrees) {
393
+ send(res, 501, { ok: false, error: "worktree listing not supported by this service" });
394
+ return;
395
+ }
396
+ send(res, 200, { ok: true, worktrees: this.options.desktop.listWorktrees() });
397
+ return;
398
+ }
399
+ if (req.method === "GET" && url.pathname === "/graveyard") {
400
+ if (!this.options.desktop?.listGraveyard) {
401
+ send(res, 501, { ok: false, error: "graveyard listing not supported by this service" });
402
+ return;
403
+ }
404
+ send(res, 200, { ok: true, entries: this.options.desktop.listGraveyard() });
405
+ return;
406
+ }
407
+ if (req.method === "GET" && url.pathname === "/threads") {
408
+ send(res, 200, listThreadSummaries(url.searchParams.get("session") ?? undefined));
409
+ return;
410
+ }
411
+ if (req.method === "GET" && url.pathname === "/workflow") {
412
+ send(res, 200, buildWorkflowEntries(url.searchParams.get("participant") ?? "user"));
413
+ return;
414
+ }
415
+ if (req.method === "POST" && url.pathname === "/usage/mark") {
416
+ const body = (await readJson(req));
417
+ const itemId = body.itemId?.trim() || "";
418
+ if (!itemId) {
419
+ send(res, 400, { ok: false, error: "itemId is required" });
420
+ return;
421
+ }
422
+ const state = markLastUsed(process.cwd(), {
423
+ itemId,
424
+ clientSession: body.clientSession?.trim() || undefined,
425
+ });
426
+ send(res, 200, {
427
+ ok: true,
428
+ itemId,
429
+ lastUsedAt: state.items[itemId]?.lastUsedAt ?? null,
430
+ });
431
+ return;
432
+ }
433
+ if (req.method === "GET" && url.pathname === "/control/switchable-agents") {
434
+ const currentClientSession = url.searchParams.get("currentClientSession")?.trim() || undefined;
435
+ const currentWindow = url.searchParams.get("currentWindow")?.trim() || undefined;
436
+ const currentWindowId = url.searchParams.get("currentWindowId")?.trim() || undefined;
437
+ const currentPath = url.searchParams.get("currentPath")?.trim() || undefined;
438
+ const items = listSwitchableAgentItems({
439
+ projectRoot: process.cwd(),
440
+ currentClientSession,
441
+ currentWindow,
442
+ currentWindowId,
443
+ currentPath,
444
+ }, new TmuxRuntimeManager()).map((item) => ({
445
+ ...serializeFastControlItem(item),
446
+ label: item.lastUsedAt ? `${item.label} · ${formatRelativeRecency(item.lastUsedAt)}` : item.label,
447
+ }));
448
+ send(res, 200, { ok: true, items });
449
+ return;
450
+ }
451
+ if ((req.method === "GET" || req.method === "POST") && url.pathname === "/control/show-menu") {
452
+ const currentClientSession = url.searchParams.get("currentClientSession")?.trim() || undefined;
453
+ const clientTty = url.searchParams.get("clientTty")?.trim() || undefined;
454
+ const currentWindow = url.searchParams.get("currentWindow")?.trim() || undefined;
455
+ const currentWindowId = url.searchParams.get("currentWindowId")?.trim() || undefined;
456
+ const currentPath = url.searchParams.get("currentPath")?.trim() || undefined;
457
+ const tmux = new TmuxRuntimeManager();
458
+ const items = listSwitchableAgentItems({
459
+ projectRoot: process.cwd(),
460
+ currentClientSession,
461
+ currentWindow,
462
+ currentWindowId,
463
+ currentPath,
464
+ }, tmux).map((item) => ({
465
+ ...serializeFastControlItem(item),
466
+ label: item.lastUsedAt ? `${item.label} · ${formatRelativeRecency(item.lastUsedAt)}` : item.label,
467
+ }));
468
+ if (items.length === 0) {
469
+ send(res, 404, { ok: false, error: "no switchable agent found" });
470
+ return;
471
+ }
472
+ displayMenu(tmux, items, currentWindowId, currentClientSession, clientTty);
473
+ send(res, 200, { ok: true });
474
+ return;
475
+ }
476
+ if (req.method === "GET" && url.pathname === "/agents/output/stream") {
477
+ const sessionId = url.searchParams.get("sessionId")?.trim();
478
+ const startLineRaw = url.searchParams.get("startLine");
479
+ const intervalMsRaw = url.searchParams.get("intervalMs");
480
+ if (!sessionId) {
481
+ send(res, 400, { ok: false, error: "sessionId is required" });
482
+ return;
483
+ }
484
+ if (!this.options.lifecycle?.readAgentOutput) {
485
+ send(res, 501, { ok: false, error: "agent output stream not supported by this service" });
486
+ return;
487
+ }
488
+ const startLine = startLineRaw === null || startLineRaw.trim() === "" ? undefined : Number.parseInt(startLineRaw, 10);
489
+ if (startLineRaw !== null && Number.isNaN(startLine)) {
490
+ send(res, 400, { ok: false, error: "startLine must be an integer" });
491
+ return;
492
+ }
493
+ const intervalMs = intervalMsRaw === null || intervalMsRaw.trim() === "" ? 500 : Number.parseInt(intervalMsRaw, 10);
494
+ if (Number.isNaN(intervalMs) || intervalMs < 100) {
495
+ send(res, 400, { ok: false, error: "intervalMs must be an integer >= 100" });
496
+ return;
497
+ }
498
+ res.statusCode = 200;
499
+ res.setHeader("content-type", "text/event-stream");
500
+ res.setHeader("cache-control", "no-cache, no-transform");
501
+ res.setHeader("connection", "keep-alive");
502
+ res.setHeader("x-accel-buffering", "no");
503
+ res.setHeader("access-control-allow-origin", "*");
504
+ res.flushHeaders?.();
505
+ let closed = false;
506
+ let lastOutput;
507
+ let pollTimer = null;
508
+ const cleanup = () => {
509
+ if (closed)
510
+ return;
511
+ closed = true;
512
+ if (pollTimer)
513
+ clearInterval(pollTimer);
514
+ pollTimer = null;
515
+ res.end();
516
+ };
517
+ req.on("close", cleanup);
518
+ req.on("aborted", cleanup);
519
+ res.on("close", cleanup);
520
+ const poll = async () => {
521
+ if (closed)
522
+ return;
523
+ try {
524
+ const result = await this.options.lifecycle.readAgentOutput({ sessionId, startLine });
525
+ if (closed)
526
+ return;
527
+ if (result.output !== lastOutput) {
528
+ lastOutput = result.output;
529
+ sendSseEvent(res, "output", {
530
+ sessionId: result.sessionId,
531
+ output: result.output,
532
+ startLine: result.startLine ?? startLine ?? -120,
533
+ parsed: result.parsed,
534
+ });
535
+ }
536
+ else {
537
+ res.write(": keepalive\n\n");
538
+ }
539
+ }
540
+ catch (error) {
541
+ sendSseEvent(res, "error", {
542
+ sessionId,
543
+ error: error instanceof Error ? error.message : String(error),
544
+ });
545
+ cleanup();
546
+ }
547
+ };
548
+ sendSseEvent(res, "ready", { sessionId, startLine: startLine ?? -120, intervalMs });
549
+ await poll();
550
+ pollTimer = setInterval(() => {
551
+ void poll();
552
+ }, intervalMs);
553
+ pollTimer.unref?.();
554
+ return;
555
+ }
556
+ if (req.method === "GET" && url.pathname.startsWith("/threads/")) {
557
+ const threadId = decodeURIComponent(url.pathname.slice("/threads/".length));
558
+ const thread = readThread(threadId);
559
+ if (!thread) {
560
+ send(res, 404, { ok: false, error: "thread not found" });
561
+ return;
562
+ }
563
+ send(res, 200, { thread, messages: readMessages(threadId) });
564
+ return;
565
+ }
566
+ try {
567
+ if (req.method === "POST" && url.pathname === "/set-status") {
568
+ const body = (await readJson(req));
569
+ updateSessionMetadata(body.session, (current) => ({
570
+ ...current,
571
+ status: { text: body.text, tone: body.tone },
572
+ }));
573
+ this.options.onChange?.();
574
+ send(res, 200, { ok: true });
575
+ return;
576
+ }
577
+ if ((req.method === "GET" || req.method === "POST") && url.pathname === "/control/open-dashboard") {
578
+ const body = req.method === "POST"
579
+ ? (await readJson(req))
580
+ : {};
581
+ const currentClientSession = body.currentClientSession?.trim() || url.searchParams.get("currentClientSession")?.trim() || undefined;
582
+ const clientTty = body.clientTty?.trim() || url.searchParams.get("clientTty")?.trim() || undefined;
583
+ if (!currentClientSession) {
584
+ send(res, 400, { ok: false, error: "currentClientSession is required" });
585
+ return;
586
+ }
587
+ const tmux = new TmuxRuntimeManager();
588
+ const { dashboardCommand, dashboardBuildStamp } = getDashboardCommandSpec(process.cwd());
589
+ const dashboardSession = tmux.ensureProjectSession(process.cwd(), dashboardCommand);
590
+ const openSessionName = tmux.hasSession(currentClientSession)
591
+ ? currentClientSession
592
+ : tmux.getOpenSessionName(dashboardSession.sessionName);
593
+ const target = tmux.ensureDashboardWindow(openSessionName, process.cwd(), dashboardCommand);
594
+ const currentBuildStamp = tmux.getWindowOption(target, "@aimux-dashboard-build");
595
+ if (!tmux.isWindowAlive(target) || currentBuildStamp !== dashboardBuildStamp) {
596
+ tmux.respawnWindow(target, dashboardCommand);
597
+ tmux.setWindowOption(target, "@aimux-dashboard-build", dashboardBuildStamp);
598
+ }
599
+ openTarget(tmux, target, currentClientSession, clientTty);
600
+ send(res, 200, { ok: true });
601
+ return;
602
+ }
603
+ if ((req.method === "GET" || req.method === "POST") && url.pathname === "/control/focus-window") {
604
+ const body = req.method === "POST"
605
+ ? (await readJson(req))
606
+ : {};
607
+ const currentClientSession = body.currentClientSession?.trim() || url.searchParams.get("currentClientSession")?.trim() || undefined;
608
+ const clientTty = body.clientTty?.trim() || url.searchParams.get("clientTty")?.trim() || undefined;
609
+ const windowId = body.windowId?.trim() || url.searchParams.get("windowId")?.trim() || undefined;
610
+ if (!windowId) {
611
+ send(res, 400, { ok: false, error: "windowId is required" });
612
+ return;
613
+ }
614
+ const tmux = new TmuxRuntimeManager();
615
+ const sessionName = currentClientSession || tmux.getProjectSession(process.cwd()).sessionName;
616
+ const target = tmux.getTargetByWindowId(sessionName, windowId) ??
617
+ tmux.getTargetByWindowId(tmux.getProjectSession(process.cwd()).sessionName, windowId);
618
+ if (!target) {
619
+ send(res, 404, { ok: false, error: "window not found" });
620
+ return;
621
+ }
622
+ openTarget(tmux, target, currentClientSession, clientTty);
623
+ markTargetUsed(tmux, process.cwd(), target, currentClientSession);
624
+ send(res, 200, { ok: true });
625
+ return;
626
+ }
627
+ if ((req.method === "GET" || req.method === "POST") && url.pathname === "/control/switch-next") {
628
+ const body = req.method === "POST"
629
+ ? (await readJson(req))
630
+ : {};
631
+ const currentClientSession = body.currentClientSession?.trim() || url.searchParams.get("currentClientSession")?.trim() || undefined;
632
+ const clientTty = body.clientTty?.trim() || url.searchParams.get("clientTty")?.trim() || undefined;
633
+ const item = resolveNextAgent({
634
+ projectRoot: process.cwd(),
635
+ currentClientSession,
636
+ currentWindow: body.currentWindow?.trim() || url.searchParams.get("currentWindow")?.trim() || undefined,
637
+ currentWindowId: body.currentWindowId?.trim() || url.searchParams.get("currentWindowId")?.trim() || undefined,
638
+ currentPath: body.currentPath?.trim() || url.searchParams.get("currentPath")?.trim() || undefined,
639
+ }, new TmuxRuntimeManager());
640
+ if (!item) {
641
+ send(res, 404, { ok: false, error: "no switchable agent found" });
642
+ return;
643
+ }
644
+ const tmux = new TmuxRuntimeManager();
645
+ openTarget(tmux, item.target, currentClientSession, clientTty);
646
+ markTargetUsed(tmux, process.cwd(), item.target, currentClientSession, item.metadata.sessionId);
647
+ send(res, 200, { ok: true });
648
+ return;
649
+ }
650
+ if ((req.method === "GET" || req.method === "POST") && url.pathname === "/control/switch-prev") {
651
+ const body = req.method === "POST"
652
+ ? (await readJson(req))
653
+ : {};
654
+ const currentClientSession = body.currentClientSession?.trim() || url.searchParams.get("currentClientSession")?.trim() || undefined;
655
+ const clientTty = body.clientTty?.trim() || url.searchParams.get("clientTty")?.trim() || undefined;
656
+ const item = resolvePrevAgent({
657
+ projectRoot: process.cwd(),
658
+ currentClientSession,
659
+ currentWindow: body.currentWindow?.trim() || url.searchParams.get("currentWindow")?.trim() || undefined,
660
+ currentWindowId: body.currentWindowId?.trim() || url.searchParams.get("currentWindowId")?.trim() || undefined,
661
+ currentPath: body.currentPath?.trim() || url.searchParams.get("currentPath")?.trim() || undefined,
662
+ }, new TmuxRuntimeManager());
663
+ if (!item) {
664
+ send(res, 404, { ok: false, error: "no switchable agent found" });
665
+ return;
666
+ }
667
+ const tmux = new TmuxRuntimeManager();
668
+ openTarget(tmux, item.target, currentClientSession, clientTty);
669
+ markTargetUsed(tmux, process.cwd(), item.target, currentClientSession, item.metadata.sessionId);
670
+ send(res, 200, { ok: true });
671
+ return;
672
+ }
673
+ if ((req.method === "GET" || req.method === "POST") && url.pathname === "/control/switch-attention") {
674
+ const body = req.method === "POST"
675
+ ? (await readJson(req))
676
+ : {};
677
+ const currentClientSession = body.currentClientSession?.trim() || url.searchParams.get("currentClientSession")?.trim() || undefined;
678
+ const clientTty = body.clientTty?.trim() || url.searchParams.get("clientTty")?.trim() || undefined;
679
+ const item = resolveAttentionAgent({
680
+ projectRoot: process.cwd(),
681
+ currentClientSession,
682
+ currentWindow: body.currentWindow?.trim() || url.searchParams.get("currentWindow")?.trim() || undefined,
683
+ currentWindowId: body.currentWindowId?.trim() || url.searchParams.get("currentWindowId")?.trim() || undefined,
684
+ currentPath: body.currentPath?.trim() || url.searchParams.get("currentPath")?.trim() || undefined,
685
+ }, new TmuxRuntimeManager());
686
+ if (!item) {
687
+ send(res, 404, { ok: false, error: "no attention target found" });
688
+ return;
689
+ }
690
+ const tmux = new TmuxRuntimeManager();
691
+ openTarget(tmux, item.target, currentClientSession, clientTty);
692
+ markTargetUsed(tmux, process.cwd(), item.target, currentClientSession, item.metadata.sessionId);
693
+ send(res, 200, { ok: true });
694
+ return;
695
+ }
696
+ if (req.method === "POST" && url.pathname === "/set-progress") {
697
+ const body = (await readJson(req));
698
+ updateSessionMetadata(body.session, (current) => ({
699
+ ...current,
700
+ progress: { current: body.current, total: body.total, label: body.label },
701
+ }));
702
+ this.options.onChange?.();
703
+ send(res, 200, { ok: true });
704
+ return;
705
+ }
706
+ if (req.method === "POST" && url.pathname === "/set-context") {
707
+ const body = (await readJson(req));
708
+ updateSessionMetadata(body.session, (current) => ({
709
+ ...current,
710
+ context: {
711
+ ...(current.context ?? {}),
712
+ ...body.context,
713
+ },
714
+ }));
715
+ this.options.onChange?.();
716
+ send(res, 200, { ok: true });
717
+ return;
718
+ }
719
+ if (req.method === "POST" && url.pathname === "/set-services") {
720
+ const body = (await readJson(req));
721
+ updateSessionMetadata(body.session, (current) => ({
722
+ ...current,
723
+ derived: {
724
+ ...(current.derived ?? {}),
725
+ services: body.services,
726
+ },
727
+ }));
728
+ this.options.onChange?.();
729
+ send(res, 200, { ok: true });
730
+ return;
731
+ }
732
+ if (req.method === "POST" && url.pathname === "/log") {
733
+ const body = (await readJson(req));
734
+ const entry = {
735
+ message: body.message,
736
+ source: body.source,
737
+ tone: body.tone,
738
+ ts: new Date().toISOString(),
739
+ };
740
+ updateSessionMetadata(body.session, (current) => ({
741
+ ...current,
742
+ logs: [...(current.logs ?? []).slice(-19), entry],
743
+ }));
744
+ this.options.onChange?.();
745
+ send(res, 200, { ok: true });
746
+ return;
747
+ }
748
+ if (req.method === "POST" && url.pathname === "/event") {
749
+ const body = (await readJson(req));
750
+ this.tracker.emit(body.session, body.event);
751
+ this.options.onChange?.();
752
+ send(res, 200, { ok: true });
753
+ return;
754
+ }
755
+ if (req.method === "POST" && url.pathname === "/mark-seen") {
756
+ const body = (await readJson(req));
757
+ this.tracker.markSeen(body.session);
758
+ this.options.onChange?.();
759
+ send(res, 200, { ok: true });
760
+ return;
761
+ }
762
+ if (req.method === "POST" && url.pathname === "/set-activity") {
763
+ const body = (await readJson(req));
764
+ this.tracker.setActivity(body.session, body.activity);
765
+ this.options.onChange?.();
766
+ send(res, 200, { ok: true });
767
+ return;
768
+ }
769
+ if (req.method === "POST" && url.pathname === "/set-attention") {
770
+ const body = (await readJson(req));
771
+ this.tracker.setAttention(body.session, body.attention);
772
+ if (body.attention === "needs_input") {
773
+ this.emitAlert({
774
+ kind: "needs_input",
775
+ sessionId: body.session,
776
+ title: `${body.session} needs input`,
777
+ message: "Agent is waiting for input.",
778
+ dedupeKey: `needs_input:${body.session}`,
779
+ cooldownMs: 15_000,
780
+ });
781
+ }
782
+ else if (body.attention === "blocked") {
783
+ this.emitAlert({
784
+ kind: "blocked",
785
+ sessionId: body.session,
786
+ title: `${body.session} is blocked`,
787
+ message: "Agent reported a blocked state.",
788
+ dedupeKey: `blocked:${body.session}`,
789
+ cooldownMs: 15_000,
790
+ });
791
+ }
792
+ else if (body.attention === "error") {
793
+ this.emitAlert({
794
+ kind: "task_failed",
795
+ sessionId: body.session,
796
+ title: `${body.session} errored`,
797
+ message: "Agent reported an error state.",
798
+ dedupeKey: `error:${body.session}`,
799
+ cooldownMs: 15_000,
800
+ });
801
+ }
802
+ this.options.onChange?.();
803
+ send(res, 200, { ok: true });
804
+ return;
805
+ }
806
+ if (req.method === "POST" && url.pathname === "/clear-log") {
807
+ const body = (await readJson(req));
808
+ clearSessionLogs(body.session);
809
+ this.options.onChange?.();
810
+ send(res, 200, { ok: true });
811
+ return;
812
+ }
813
+ if (req.method === "POST" && url.pathname === "/notify") {
814
+ const body = (await readJson(req));
815
+ const requestedKind = body.kind?.trim();
816
+ const kind = requestedKind === "notification" || requestedKind === "generic"
817
+ ? "notification"
818
+ : requestedKind === "task_done" || requestedKind === "complete"
819
+ ? "task_done"
820
+ : requestedKind === "task_failed" || requestedKind === "error"
821
+ ? "task_failed"
822
+ : requestedKind === "blocked"
823
+ ? "blocked"
824
+ : requestedKind === "message_waiting"
825
+ ? "message_waiting"
826
+ : requestedKind === "handoff_waiting"
827
+ ? "handoff_waiting"
828
+ : requestedKind === "task_assigned"
829
+ ? "task_assigned"
830
+ : requestedKind === "review_waiting"
831
+ ? "review_waiting"
832
+ : "needs_input";
833
+ this.emitAlert({
834
+ kind,
835
+ sessionId: body.sessionId?.trim() || undefined,
836
+ title: body.title?.trim() || "aimux",
837
+ message: [body.subtitle?.trim(), body.message?.trim() || body.title?.trim() || "aimux"]
838
+ .filter(Boolean)
839
+ .join(" — "),
840
+ dedupeKey: kind === "task_done" ? `notify:complete:${body.title ?? body.message ?? "aimux"}` : undefined,
841
+ });
842
+ send(res, 200, { ok: true });
843
+ return;
844
+ }
845
+ if (req.method === "POST" && url.pathname === "/notification-context") {
846
+ const body = (await readJson(req));
847
+ const source = body.source === "desktop" ? "desktop" : "tui";
848
+ const context = updateNotificationContext(source, {
849
+ focused: Boolean(body.focused),
850
+ screen: body.screen?.trim() || undefined,
851
+ sessionId: body.sessionId?.trim() || undefined,
852
+ panelOpen: Boolean(body.panelOpen),
853
+ });
854
+ send(res, 200, { ok: true, context });
855
+ return;
856
+ }
857
+ if (req.method === "POST" && url.pathname === "/notifications/read") {
858
+ const body = (await readJson(req));
859
+ const updated = markNotificationsRead({
860
+ id: body.id?.trim() || undefined,
861
+ sessionId: body.sessionId?.trim() || undefined,
862
+ });
863
+ send(res, 200, { ok: true, updated });
864
+ return;
865
+ }
866
+ if (req.method === "POST" && url.pathname === "/notifications/clear") {
867
+ const body = (await readJson(req));
868
+ const cleared = clearNotifications({
869
+ id: body.id?.trim() || undefined,
870
+ sessionId: body.sessionId?.trim() || undefined,
871
+ });
872
+ send(res, 200, { ok: true, cleared });
873
+ return;
874
+ }
875
+ if (req.method === "POST" && url.pathname === "/threads/open") {
876
+ const body = (await readJson(req));
877
+ const thread = createThread({
878
+ title: body.title,
879
+ createdBy: body.from,
880
+ participants: [...new Set([body.from, ...(body.participants ?? [])])],
881
+ kind: body.kind ?? "conversation",
882
+ worktreePath: body.worktreePath,
883
+ });
884
+ this.options.onChange?.();
885
+ send(res, 200, { ok: true, thread });
886
+ return;
887
+ }
888
+ if (req.method === "POST" && url.pathname === "/threads/send") {
889
+ const body = (await readJson(req));
890
+ const result = this.options.threads?.sendMessage
891
+ ? this.options.threads.sendMessage(body)
892
+ : body.threadId
893
+ ? sendThreadMessage({
894
+ threadId: body.threadId,
895
+ from: body.from ?? "user",
896
+ to: body.to,
897
+ kind: body.kind,
898
+ body: body.body,
899
+ })
900
+ : sendDirectMessage({
901
+ from: body.from ?? "user",
902
+ to: body.to ?? [],
903
+ kind: body.kind,
904
+ body: body.body,
905
+ title: body.title,
906
+ worktreePath: body.worktreePath,
907
+ });
908
+ const messageKind = body.kind ?? "request";
909
+ if (messageKind === "handoff") {
910
+ const recipients = this.resolveAlertRecipients(body.to, result.message, body.to);
911
+ this.emitThreadWaitingAlert({
912
+ kind: "handoff_waiting",
913
+ threadId: result.thread.id,
914
+ from: body.from ?? "user",
915
+ recipients,
916
+ title: `Handoff for ${recipients.join(", ") || "agent"}`,
917
+ message: body.body.trim() || "A handoff is waiting for you.",
918
+ worktreePath: result.thread.worktreePath ?? body.worktreePath,
919
+ });
920
+ }
921
+ else if (messageKind === "request" || messageKind === "reply" || messageKind === "note") {
922
+ const recipients = this.resolveAlertRecipients(body.to, result.message, body.to);
923
+ this.emitThreadWaitingAlert({
924
+ kind: "message_waiting",
925
+ threadId: result.thread.id,
926
+ from: body.from ?? "user",
927
+ recipients,
928
+ title: `Message for ${recipients.join(", ") || "agent"}`,
929
+ message: body.body.trim() || "A new message is waiting.",
930
+ worktreePath: result.thread.worktreePath ?? body.worktreePath,
931
+ });
932
+ }
933
+ this.options.onChange?.();
934
+ send(res, 200, { ok: true, ...result });
935
+ return;
936
+ }
937
+ if (req.method === "POST" && url.pathname === "/threads/mark-seen") {
938
+ const body = (await readJson(req));
939
+ const thread = markThreadSeen(body.threadId, body.session);
940
+ if (!thread) {
941
+ send(res, 404, { ok: false, error: "thread not found" });
942
+ return;
943
+ }
944
+ this.options.onChange?.();
945
+ send(res, 200, { ok: true, thread });
946
+ return;
947
+ }
948
+ if (req.method === "POST" && url.pathname === "/threads/status") {
949
+ const body = (await readJson(req));
950
+ const thread = setThreadStatus(body.threadId, body.status, {
951
+ owner: body.owner?.trim(),
952
+ waitingOn: body.waitingOn?.map((value) => value.trim()).filter(Boolean),
953
+ });
954
+ if (!thread) {
955
+ send(res, 404, { ok: false, error: "thread not found" });
956
+ return;
957
+ }
958
+ this.options.onChange?.();
959
+ send(res, 200, { ok: true, thread });
960
+ return;
961
+ }
962
+ if (req.method === "POST" && url.pathname === "/handoff") {
963
+ const body = (await readJson(req));
964
+ const result = this.options.actions?.sendHandoff
965
+ ? this.options.actions.sendHandoff(body)
966
+ : sendHandoff({
967
+ from: body.from?.trim() || "user",
968
+ to: body.to ?? [],
969
+ body: body.body,
970
+ title: body.title,
971
+ worktreePath: body.worktreePath,
972
+ });
973
+ const recipients = this.resolveAlertRecipients(body.to, result.message, body.to);
974
+ this.emitThreadWaitingAlert({
975
+ kind: "handoff_waiting",
976
+ threadId: result.thread.id,
977
+ from: body.from?.trim() || "user",
978
+ recipients,
979
+ title: `Handoff for ${recipients.join(", ") || "agent"}`,
980
+ message: body.body.trim() || "A handoff is waiting for you.",
981
+ worktreePath: result.thread.worktreePath ?? body.worktreePath,
982
+ });
983
+ this.options.onChange?.();
984
+ send(res, 200, { ok: true, ...result });
985
+ return;
986
+ }
987
+ if (req.method === "POST" && url.pathname === "/handoff/accept") {
988
+ const body = (await readJson(req));
989
+ const result = this.options.actions?.acceptHandoff
990
+ ? this.options.actions.acceptHandoff(body)
991
+ : acceptHandoff({
992
+ threadId: body.threadId,
993
+ from: body.from?.trim() || "user",
994
+ body: body.body,
995
+ });
996
+ this.options.onChange?.();
997
+ send(res, 200, { ok: true, ...result });
998
+ return;
999
+ }
1000
+ if (req.method === "POST" && url.pathname === "/handoff/complete") {
1001
+ const body = (await readJson(req));
1002
+ const result = this.options.actions?.completeHandoff
1003
+ ? this.options.actions.completeHandoff(body)
1004
+ : completeHandoff({
1005
+ threadId: body.threadId,
1006
+ from: body.from?.trim() || "user",
1007
+ body: body.body,
1008
+ });
1009
+ this.options.onChange?.();
1010
+ send(res, 200, { ok: true, ...result });
1011
+ return;
1012
+ }
1013
+ if (req.method === "POST" && url.pathname === "/tasks/assign") {
1014
+ const body = (await readJson(req));
1015
+ const result = await assignTask({
1016
+ from: body.from?.trim() || "user",
1017
+ to: body.to?.trim(),
1018
+ assignee: body.assignee?.trim(),
1019
+ tool: body.tool?.trim(),
1020
+ description: body.description,
1021
+ prompt: body.prompt,
1022
+ type: body.type,
1023
+ diff: body.diff,
1024
+ worktreePath: body.worktreePath,
1025
+ });
1026
+ this.emitAssignedTaskAlert(result);
1027
+ this.options.onChange?.();
1028
+ send(res, 200, { ok: true, ...result });
1029
+ return;
1030
+ }
1031
+ if (req.method === "POST" && url.pathname === "/tasks/accept") {
1032
+ const body = (await readJson(req));
1033
+ const result = this.options.actions?.acceptTask
1034
+ ? await this.options.actions.acceptTask(body)
1035
+ : await acceptTask({
1036
+ taskId: body.taskId,
1037
+ from: body.from?.trim() || "user",
1038
+ body: body.body,
1039
+ });
1040
+ this.options.onChange?.();
1041
+ send(res, 200, { ok: true, ...result });
1042
+ return;
1043
+ }
1044
+ if (req.method === "POST" && url.pathname === "/tasks/block") {
1045
+ const body = (await readJson(req));
1046
+ const result = this.options.actions?.blockTask
1047
+ ? await this.options.actions.blockTask(body)
1048
+ : await blockTask({
1049
+ taskId: body.taskId,
1050
+ from: body.from?.trim() || "user",
1051
+ body: body.body,
1052
+ });
1053
+ this.emitAlert({
1054
+ kind: "blocked",
1055
+ sessionId: result.task.assignedTo,
1056
+ taskId: result.task.id,
1057
+ threadId: result.thread?.id,
1058
+ worktreePath: result.thread?.worktreePath,
1059
+ title: `Task blocked: ${result.task.description}`,
1060
+ message: result.task.error || body.body || "Task is blocked.",
1061
+ dedupeKey: `task-blocked:${result.task.id}`,
1062
+ cooldownMs: 15_000,
1063
+ });
1064
+ this.options.onChange?.();
1065
+ send(res, 200, { ok: true, ...result });
1066
+ return;
1067
+ }
1068
+ if (req.method === "POST" && url.pathname === "/tasks/complete") {
1069
+ const body = (await readJson(req));
1070
+ const result = this.options.actions?.completeTask
1071
+ ? await this.options.actions.completeTask(body)
1072
+ : await completeTask({
1073
+ taskId: body.taskId,
1074
+ from: body.from?.trim() || "user",
1075
+ body: body.body,
1076
+ });
1077
+ this.emitAlert({
1078
+ kind: "task_done",
1079
+ sessionId: result.task.assignedTo,
1080
+ taskId: result.task.id,
1081
+ threadId: result.thread?.id,
1082
+ worktreePath: result.thread?.worktreePath,
1083
+ title: `Task done: ${result.task.description}`,
1084
+ message: body.body?.trim() || result.message?.body || "Task completed.",
1085
+ dedupeKey: `task-done:${result.task.id}`,
1086
+ cooldownMs: 15_000,
1087
+ });
1088
+ this.options.onChange?.();
1089
+ send(res, 200, { ok: true, ...result });
1090
+ return;
1091
+ }
1092
+ if (req.method === "POST" && url.pathname === "/agents/spawn") {
1093
+ const body = (await readJson(req));
1094
+ if (!this.options.lifecycle?.spawnAgent) {
1095
+ send(res, 501, { ok: false, error: "agent spawn not supported by this service" });
1096
+ return;
1097
+ }
1098
+ const result = await this.options.lifecycle.spawnAgent(body);
1099
+ this.options.onChange?.();
1100
+ send(res, 200, { ok: true, ...result });
1101
+ return;
1102
+ }
1103
+ if (req.method === "POST" && url.pathname === "/agents/fork") {
1104
+ const body = (await readJson(req));
1105
+ if (!this.options.lifecycle?.forkAgent) {
1106
+ send(res, 501, { ok: false, error: "agent fork not supported by this service" });
1107
+ return;
1108
+ }
1109
+ const result = await this.options.lifecycle.forkAgent(body);
1110
+ this.options.onChange?.();
1111
+ send(res, 200, { ok: true, ...result });
1112
+ return;
1113
+ }
1114
+ if (req.method === "POST" && url.pathname === "/agents/stop") {
1115
+ const body = (await readJson(req));
1116
+ if (!this.options.lifecycle?.stopAgent) {
1117
+ send(res, 501, { ok: false, error: "agent stop not supported by this service" });
1118
+ return;
1119
+ }
1120
+ const result = await this.options.lifecycle.stopAgent(body);
1121
+ this.options.onChange?.();
1122
+ send(res, 200, { ok: true, ...result });
1123
+ return;
1124
+ }
1125
+ if (req.method === "POST" && url.pathname === "/agents/interrupt") {
1126
+ const body = (await readJson(req));
1127
+ if (!this.options.lifecycle?.interruptAgent) {
1128
+ send(res, 501, { ok: false, error: "agent interrupt not supported by this service" });
1129
+ return;
1130
+ }
1131
+ const result = await this.options.lifecycle.interruptAgent(body);
1132
+ this.options.onChange?.();
1133
+ send(res, 200, { ok: true, ...result });
1134
+ return;
1135
+ }
1136
+ if (req.method === "POST" && url.pathname === "/agents/rename") {
1137
+ const body = (await readJson(req));
1138
+ if (!this.options.lifecycle?.renameAgent) {
1139
+ send(res, 501, { ok: false, error: "agent rename not supported by this service" });
1140
+ return;
1141
+ }
1142
+ const result = await this.options.lifecycle.renameAgent(body);
1143
+ this.options.onChange?.();
1144
+ send(res, 200, { ok: true, ...result });
1145
+ return;
1146
+ }
1147
+ if (req.method === "POST" && url.pathname === "/agents/migrate") {
1148
+ const body = (await readJson(req));
1149
+ if (!this.options.lifecycle?.migrateAgent) {
1150
+ send(res, 501, { ok: false, error: "agent migrate not supported by this service" });
1151
+ return;
1152
+ }
1153
+ const result = await this.options.lifecycle.migrateAgent(body);
1154
+ this.options.onChange?.();
1155
+ send(res, 200, { ok: true, ...result });
1156
+ return;
1157
+ }
1158
+ if (req.method === "POST" && url.pathname === "/agents/kill") {
1159
+ const body = (await readJson(req));
1160
+ if (!this.options.lifecycle?.killAgent) {
1161
+ send(res, 501, { ok: false, error: "agent kill not supported by this service" });
1162
+ return;
1163
+ }
1164
+ const result = await this.options.lifecycle.killAgent(body);
1165
+ this.options.onChange?.();
1166
+ send(res, 200, { ok: true, ...result });
1167
+ return;
1168
+ }
1169
+ if (req.method === "POST" && url.pathname === "/agents/input") {
1170
+ const body = (await readJson(req));
1171
+ if (!this.options.lifecycle?.writeAgentInput) {
1172
+ send(res, 501, { ok: false, error: "agent input not supported by this service" });
1173
+ return;
1174
+ }
1175
+ const result = await this.options.lifecycle.writeAgentInput(body);
1176
+ if (this.options.lifecycle.readAgentHistory) {
1177
+ try {
1178
+ const history = await this.options.lifecycle.readAgentHistory({ sessionId: body.sessionId, lastN: 20 });
1179
+ this.eventBus.publishHistoryUpdate({
1180
+ sessionId: history.sessionId,
1181
+ messages: history.messages,
1182
+ lastN: history.lastN,
1183
+ });
1184
+ }
1185
+ catch {
1186
+ // History update is best-effort; the write result should still succeed.
1187
+ }
1188
+ }
1189
+ this.options.onChange?.();
1190
+ send(res, 200, { ok: true, ...result });
1191
+ return;
1192
+ }
1193
+ if (req.method === "POST" && url.pathname === "/attachments") {
1194
+ const body = (await readJson(req));
1195
+ const attachment = body.path?.trim()
1196
+ ? ingestAttachmentFromPath(body.path)
1197
+ : ingestAttachmentFromBase64({
1198
+ filename: body.filename,
1199
+ mimeType: body.mimeType,
1200
+ contentBase64: String(body.contentBase64 ?? ""),
1201
+ });
1202
+ send(res, 200, { ok: true, attachment });
1203
+ return;
1204
+ }
1205
+ const attachmentContentMatch = url.pathname.match(/^\/attachments\/([^/]+)\/content$/);
1206
+ if (req.method === "GET" && attachmentContentMatch) {
1207
+ const content = getAttachmentContent(decodeURIComponent(attachmentContentMatch[1] || ""));
1208
+ if (!content) {
1209
+ send(res, 404, { ok: false, error: "attachment not found" });
1210
+ return;
1211
+ }
1212
+ sendBytes(res, 200, content.buffer, content.attachment.mimeType);
1213
+ return;
1214
+ }
1215
+ const attachmentMatch = url.pathname.match(/^\/attachments\/([^/]+)$/);
1216
+ if (req.method === "GET" && attachmentMatch) {
1217
+ const attachment = getAttachment(decodeURIComponent(attachmentMatch[1] || ""));
1218
+ if (!attachment) {
1219
+ send(res, 404, { ok: false, error: "attachment not found" });
1220
+ return;
1221
+ }
1222
+ send(res, 200, { ok: true, attachment });
1223
+ return;
1224
+ }
1225
+ if (req.method === "GET" && url.pathname === "/agents/output") {
1226
+ const sessionId = url.searchParams.get("sessionId")?.trim();
1227
+ const startLineRaw = url.searchParams.get("startLine");
1228
+ if (!sessionId) {
1229
+ send(res, 400, { ok: false, error: "sessionId is required" });
1230
+ return;
1231
+ }
1232
+ if (!this.options.lifecycle?.readAgentOutput) {
1233
+ send(res, 501, { ok: false, error: "agent output not supported by this service" });
1234
+ return;
1235
+ }
1236
+ const startLine = startLineRaw === null || startLineRaw.trim() === "" ? undefined : Number.parseInt(startLineRaw, 10);
1237
+ if (startLineRaw !== null && Number.isNaN(startLine)) {
1238
+ send(res, 400, { ok: false, error: "startLine must be an integer" });
1239
+ return;
1240
+ }
1241
+ const result = await this.options.lifecycle.readAgentOutput({ sessionId, startLine });
1242
+ send(res, 200, { ok: true, ...result });
1243
+ return;
1244
+ }
1245
+ if (req.method === "GET" && url.pathname === "/agents/history") {
1246
+ const sessionId = url.searchParams.get("sessionId")?.trim();
1247
+ const lastNRaw = url.searchParams.get("lastN");
1248
+ if (!sessionId) {
1249
+ send(res, 400, { ok: false, error: "sessionId is required" });
1250
+ return;
1251
+ }
1252
+ if (!this.options.lifecycle?.readAgentHistory) {
1253
+ send(res, 501, { ok: false, error: "agent history not supported by this service" });
1254
+ return;
1255
+ }
1256
+ const lastN = lastNRaw === null || lastNRaw.trim() === "" ? undefined : Number.parseInt(lastNRaw, 10);
1257
+ if (lastNRaw !== null && Number.isNaN(lastN)) {
1258
+ send(res, 400, { ok: false, error: "lastN must be an integer" });
1259
+ return;
1260
+ }
1261
+ const result = await this.options.lifecycle.readAgentHistory({ sessionId, lastN });
1262
+ send(res, 200, { ok: true, ...result });
1263
+ return;
1264
+ }
1265
+ if (req.method === "POST" && url.pathname === "/worktrees/create") {
1266
+ const body = (await readJson(req));
1267
+ if (!this.options.desktop?.createWorktree) {
1268
+ send(res, 501, { ok: false, error: "worktree create not supported by this service" });
1269
+ return;
1270
+ }
1271
+ const result = await this.options.desktop.createWorktree(body);
1272
+ this.options.onChange?.();
1273
+ send(res, 200, { ok: true, ...result });
1274
+ return;
1275
+ }
1276
+ if (req.method === "POST" && url.pathname === "/worktrees/remove") {
1277
+ const body = (await readJson(req));
1278
+ if (!this.options.desktop?.removeWorktree) {
1279
+ send(res, 501, { ok: false, error: "worktree remove not supported by this service" });
1280
+ return;
1281
+ }
1282
+ const result = await this.options.desktop.removeWorktree(body);
1283
+ this.options.onChange?.();
1284
+ send(res, 200, { ok: true, ...result });
1285
+ return;
1286
+ }
1287
+ if (req.method === "POST" && url.pathname === "/services/create") {
1288
+ const body = (await readJson(req));
1289
+ if (!this.options.desktop?.createService) {
1290
+ send(res, 501, { ok: false, error: "service create not supported by this service" });
1291
+ return;
1292
+ }
1293
+ const result = await this.options.desktop.createService(body);
1294
+ this.options.onChange?.();
1295
+ send(res, 200, { ok: true, ...result });
1296
+ return;
1297
+ }
1298
+ if (req.method === "POST" && url.pathname === "/services/stop") {
1299
+ const body = (await readJson(req));
1300
+ if (!this.options.desktop?.stopService) {
1301
+ send(res, 501, { ok: false, error: "service stop not supported by this service" });
1302
+ return;
1303
+ }
1304
+ const result = await this.options.desktop.stopService(body);
1305
+ this.options.onChange?.();
1306
+ send(res, 200, { ok: true, ...result });
1307
+ return;
1308
+ }
1309
+ if (req.method === "POST" && url.pathname === "/graveyard/resurrect") {
1310
+ const body = (await readJson(req));
1311
+ if (!this.options.desktop?.resurrectGraveyard) {
1312
+ send(res, 501, { ok: false, error: "graveyard resurrect not supported by this service" });
1313
+ return;
1314
+ }
1315
+ const result = await this.options.desktop.resurrectGraveyard(body);
1316
+ this.options.onChange?.();
1317
+ send(res, 200, { ok: true, ...result });
1318
+ return;
1319
+ }
1320
+ if (req.method === "POST" && url.pathname === "/reviews/approve") {
1321
+ const body = (await readJson(req));
1322
+ const result = this.options.actions?.approveReview
1323
+ ? await this.options.actions.approveReview(body)
1324
+ : await approveReview({
1325
+ taskId: body.taskId,
1326
+ from: body.from?.trim() || "user",
1327
+ body: body.body,
1328
+ });
1329
+ this.emitReviewOutcomeAlert({
1330
+ kind: "task_done",
1331
+ task: result.task,
1332
+ thread: result.thread,
1333
+ fallbackMessage: body.body?.trim() || result.message?.body || "Review approved.",
1334
+ });
1335
+ this.options.onChange?.();
1336
+ send(res, 200, { ok: true, ...result });
1337
+ return;
1338
+ }
1339
+ if (req.method === "POST" && url.pathname === "/reviews/request-changes") {
1340
+ const body = (await readJson(req));
1341
+ const result = this.options.actions?.requestTaskChanges
1342
+ ? await this.options.actions.requestTaskChanges(body)
1343
+ : await requestTaskChanges({
1344
+ taskId: body.taskId,
1345
+ from: body.from?.trim() || "user",
1346
+ body: body.body,
1347
+ });
1348
+ this.emitReviewOutcomeAlert({
1349
+ kind: "blocked",
1350
+ task: result.task,
1351
+ thread: result.thread,
1352
+ fallbackMessage: body.body?.trim() || result.message?.body || "Changes requested.",
1353
+ });
1354
+ this.options.onChange?.();
1355
+ send(res, 200, { ok: true, ...result });
1356
+ return;
1357
+ }
1358
+ if (req.method === "POST" && url.pathname === "/tasks/reopen") {
1359
+ const body = (await readJson(req));
1360
+ const result = this.options.actions?.reopenTask
1361
+ ? await this.options.actions.reopenTask(body)
1362
+ : await reopenTask({
1363
+ taskId: body.taskId,
1364
+ from: body.from?.trim() || "user",
1365
+ body: body.body,
1366
+ });
1367
+ this.options.onChange?.();
1368
+ send(res, 200, { ok: true, ...result });
1369
+ return;
1370
+ }
1371
+ }
1372
+ catch (error) {
1373
+ send(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
1374
+ return;
1375
+ }
1376
+ send(res, 404, { ok: false, error: "not found" });
1377
+ }
1378
+ }
1379
+ //# sourceMappingURL=metadata-server.js.map