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
package/dist/main.js ADDED
@@ -0,0 +1,2483 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, readFileSync, writeFileSync, readdirSync, copyFileSync, mkdirSync, chmodSync, statSync, } from "node:fs";
3
+ import { join as pathJoin, resolve as pathResolve, dirname as pathDirname } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+ import { Multiplexer } from "./multiplexer.js";
7
+ import { llmCompact } from "./context/compactor.js";
8
+ import { initProject } from "./config.js";
9
+ import { initPaths, getHistoryDir, getGraveyardPath, getStatePath, getContextDir } from "./paths.js";
10
+ import { loadTeamConfig, saveTeamConfig, getDefaultTeamConfig } from "./team.js";
11
+ import { createWorktree, findMainRepo, listWorktrees } from "./worktree.js";
12
+ import { TmuxRuntimeManager } from "./tmux-runtime-manager.js";
13
+ import { buildTmuxDoctorReport, renderTmuxDoctorReport } from "./tmux-doctor.js";
14
+ import { loadMetadataEndpoint, resolveProjectServiceEndpoint as resolveStoredProjectServiceEndpoint, updateSessionMetadata, clearSessionLogs, removeMetadataEndpoint, } from "./metadata-store.js";
15
+ import { AgentTracker } from "./agent-tracker.js";
16
+ import { AimuxDaemon, ensureDaemonRunning, ensureProjectService, loadDaemonInfo, loadDaemonState, projectServiceStatus, requestDaemonJson, stopDaemon, stopProjectService, } from "./daemon.js";
17
+ import { getProjectServiceManifest, manifestsMatch } from "./project-service-manifest.js";
18
+ import { createThread, listThreadSummaries, markThreadSeen, readMessages, readThread, setThreadStatus, } from "./threads.js";
19
+ import { sendDirectMessage, sendThreadMessage } from "./orchestration.js";
20
+ import { acceptHandoff, approveReview, acceptTask, assignTask, blockTask, completeHandoff, completeTask, reopenTask, requestTaskChanges, sendHandoff, } from "./orchestration-actions.js";
21
+ import { addNotification, clearNotifications, listNotifications, markNotificationsRead, unreadNotificationCount, } from "./notifications.js";
22
+ import { parseClaudeHookPayload, summarizeClaudeNotification, summarizeClaudeStop } from "./claude-hooks.js";
23
+ import { requestJson } from "./http-client.js";
24
+ const program = new Command();
25
+ class ProjectServiceVersionError extends Error {
26
+ projectRoot;
27
+ expected;
28
+ actual;
29
+ constructor(message, projectRoot, expected, actual) {
30
+ super(message);
31
+ this.projectRoot = projectRoot;
32
+ this.expected = expected;
33
+ this.actual = actual;
34
+ this.name = "ProjectServiceVersionError";
35
+ }
36
+ }
37
+ function renderProjectServiceVersionHelp(error) {
38
+ const quotedProject = JSON.stringify(error.projectRoot);
39
+ const lines = [
40
+ "aimux: the running project service is from a different local build.",
41
+ "",
42
+ `Project: ${error.projectRoot}`,
43
+ `Expected build: ${error.expected.buildStamp}`,
44
+ `Running build: ${error.actual?.buildStamp ?? "unknown"}`,
45
+ "",
46
+ "Restart the daemon-managed control plane, then retry:",
47
+ ` aimux daemon restart`,
48
+ ` aimux daemon project-ensure --project ${quotedProject}`,
49
+ "",
50
+ "Or just restart the daemon and rerun `aimux` if you only changed this local checkout.",
51
+ ];
52
+ return lines.join("\n");
53
+ }
54
+ async function restartStaleControlPlane(projectRoot) {
55
+ console.error(`aimux: restarting stale daemon-managed control plane for ${projectRoot}...`);
56
+ await stopDaemon();
57
+ removeMetadataEndpoint(projectRoot);
58
+ await ensureDaemonRunning();
59
+ await ensureProjectService(projectRoot);
60
+ const { dashboardBuildStamp } = getDashboardCommandSpec(projectRoot);
61
+ pruneDashboardArtifacts(projectRoot, dashboardBuildStamp);
62
+ }
63
+ async function fetchProjectServiceHealth(endpoint) {
64
+ const { status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}/health`);
65
+ if (status < 200 || status >= 300 || json?.ok === false) {
66
+ throw new Error(json?.error || `health request failed: ${status}`);
67
+ }
68
+ return json;
69
+ }
70
+ async function waitForVerifiedProjectService(projectRoot, opts) {
71
+ const expected = getProjectServiceManifest();
72
+ const deadline = Date.now() + (opts?.timeoutMs ?? 8000);
73
+ let lastError = "project service did not become reachable";
74
+ let lastServiceInfo = null;
75
+ let respawnAttempted = false;
76
+ let missingEndpointSince = 0;
77
+ while (Date.now() < deadline) {
78
+ const endpoint = await resolveProjectServiceEndpoint(projectRoot);
79
+ if (endpoint) {
80
+ missingEndpointSince = 0;
81
+ try {
82
+ const health = await fetchProjectServiceHealth(endpoint);
83
+ lastServiceInfo = health.serviceInfo ?? null;
84
+ if (manifestsMatch(expected, health.serviceInfo)) {
85
+ return { endpoint, health };
86
+ }
87
+ lastError = `project service manifest mismatch: expected ${JSON.stringify(expected)} actual ${JSON.stringify(health.serviceInfo ?? null)}`;
88
+ }
89
+ catch (error) {
90
+ lastError = error instanceof Error ? error.message : String(error);
91
+ if (!respawnAttempted &&
92
+ typeof lastError === "string" &&
93
+ (lastError.includes("ECONNREFUSED") ||
94
+ lastError.includes("ECONNRESET") ||
95
+ lastError.includes("socket hang up"))) {
96
+ respawnAttempted = true;
97
+ removeMetadataEndpoint(projectRoot);
98
+ await ensureProjectService(projectRoot);
99
+ }
100
+ }
101
+ }
102
+ else {
103
+ lastError = "no live project service metadata endpoint";
104
+ if (!missingEndpointSince) {
105
+ missingEndpointSince = Date.now();
106
+ }
107
+ else if (!respawnAttempted && Date.now() - missingEndpointSince >= 1000) {
108
+ respawnAttempted = true;
109
+ await stopProjectService(projectRoot);
110
+ removeMetadataEndpoint(projectRoot);
111
+ await ensureProjectService(projectRoot);
112
+ }
113
+ }
114
+ await new Promise((resolve) => setTimeout(resolve, 150));
115
+ }
116
+ if (lastError.startsWith("project service manifest mismatch") &&
117
+ lastServiceInfo &&
118
+ typeof lastServiceInfo === "object") {
119
+ throw new ProjectServiceVersionError(lastError, projectRoot, expected, lastServiceInfo);
120
+ }
121
+ throw new Error(`${lastError}${lastServiceInfo ? `; last serviceInfo=${JSON.stringify(lastServiceInfo)}` : ""}`);
122
+ }
123
+ async function postProjectServiceJson(path, body) {
124
+ let endpoint = await resolveProjectServiceEndpoint();
125
+ if (!endpoint) {
126
+ await ensureProjectService(resolveProjectRoot(process.cwd()));
127
+ endpoint = await resolveProjectServiceEndpoint();
128
+ }
129
+ if (!endpoint) {
130
+ throw new Error("no live project service metadata endpoint");
131
+ }
132
+ const { status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}${path}`, {
133
+ method: "POST",
134
+ headers: { "content-type": "application/json" },
135
+ body,
136
+ });
137
+ if (status < 200 || status >= 300 || json?.ok === false) {
138
+ throw new Error(json?.error || `request failed: ${status}`);
139
+ }
140
+ return json;
141
+ }
142
+ async function getProjectServiceJson(path) {
143
+ let endpoint = await resolveProjectServiceEndpoint();
144
+ if (!endpoint) {
145
+ await ensureProjectService(resolveProjectRoot(process.cwd()));
146
+ endpoint = await resolveProjectServiceEndpoint();
147
+ }
148
+ if (!endpoint) {
149
+ throw new Error("no live project service metadata endpoint");
150
+ }
151
+ const { status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}${path}`);
152
+ if (status < 200 || status >= 300 || json?.ok === false) {
153
+ throw new Error(json?.error || `request failed: ${status}`);
154
+ }
155
+ return json;
156
+ }
157
+ async function postProjectServiceJsonOrLocal(path, body, fallback) {
158
+ try {
159
+ return await postProjectServiceJson(path, body);
160
+ }
161
+ catch {
162
+ return fallback();
163
+ }
164
+ }
165
+ async function postLiveProjectServiceJsonOrLocal(projectRoot, path, body, fallback) {
166
+ try {
167
+ const endpoint = await resolveProjectServiceEndpoint(projectRoot);
168
+ if (!endpoint) {
169
+ return fallback();
170
+ }
171
+ const { status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}${path}`, {
172
+ method: "POST",
173
+ headers: { "content-type": "application/json" },
174
+ body,
175
+ });
176
+ if (status < 200 || status >= 300 || json?.ok === false) {
177
+ throw new Error(json?.error || `request failed: ${status}`);
178
+ }
179
+ return json;
180
+ }
181
+ catch {
182
+ return fallback();
183
+ }
184
+ }
185
+ async function resolveClaudeHookSessionId(explicitSessionId, payloadSessionId) {
186
+ if (!payloadSessionId)
187
+ return explicitSessionId;
188
+ const state = Multiplexer.loadState();
189
+ const match = state?.sessions.find((session) => session.backendSessionId === payloadSessionId);
190
+ return match?.id ?? explicitSessionId;
191
+ }
192
+ async function resolveProjectServiceEndpoint(projectRoot = resolveProjectRoot(process.cwd())) {
193
+ return resolveStoredProjectServiceEndpoint(projectRoot);
194
+ }
195
+ async function getProjectServiceEndpoint(projectRoot = resolveProjectRoot(process.cwd())) {
196
+ let endpoint = await resolveProjectServiceEndpoint(projectRoot);
197
+ if (!endpoint) {
198
+ await ensureProjectService(projectRoot);
199
+ endpoint = await resolveProjectServiceEndpoint(projectRoot);
200
+ }
201
+ if (!endpoint) {
202
+ throw new Error("no live project service metadata endpoint");
203
+ }
204
+ return endpoint;
205
+ }
206
+ async function readAllStdin() {
207
+ const chunks = [];
208
+ for await (const chunk of process.stdin) {
209
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
210
+ }
211
+ return Buffer.concat(chunks).toString("utf8");
212
+ }
213
+ async function ensureDaemonProjectReady(projectRoot, opts) {
214
+ await ensureDaemonRunning();
215
+ await ensureProjectService(projectRoot);
216
+ try {
217
+ await waitForVerifiedProjectService(projectRoot);
218
+ }
219
+ catch (error) {
220
+ if (!(error instanceof ProjectServiceVersionError) || opts?.repairVersionDrift === false) {
221
+ throw error;
222
+ }
223
+ await restartStaleControlPlane(projectRoot);
224
+ await waitForVerifiedProjectService(projectRoot);
225
+ }
226
+ }
227
+ async function ensureDaemonProjectSpawned(projectRoot) {
228
+ await ensureDaemonRunning();
229
+ await ensureProjectService(projectRoot);
230
+ }
231
+ function resolveProjectRoot(cwd) {
232
+ try {
233
+ return findMainRepo(cwd);
234
+ }
235
+ catch {
236
+ return cwd;
237
+ }
238
+ }
239
+ function ensureTmuxAvailable(tmux) {
240
+ if (!tmux.isAvailable()) {
241
+ console.error("aimux: tmux is not installed or not available in PATH");
242
+ process.exit(1);
243
+ }
244
+ }
245
+ function shellQuote(value) {
246
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
247
+ }
248
+ function getDashboardCommandSpec(projectRoot) {
249
+ const scriptPath = fileURLToPath(import.meta.url);
250
+ const wrappedDashboardCommand = [
251
+ "set -o pipefail",
252
+ ";",
253
+ shellQuote(process.execPath),
254
+ shellQuote(scriptPath),
255
+ "--tmux-dashboard-internal",
256
+ "2>&1",
257
+ "|",
258
+ "tee",
259
+ "-a",
260
+ shellQuote("/tmp/aimux-debug.log"),
261
+ ";",
262
+ "code=$?",
263
+ ";",
264
+ "if",
265
+ "[",
266
+ "$code",
267
+ "-ne",
268
+ "0",
269
+ "]",
270
+ ";",
271
+ "then",
272
+ "printf",
273
+ "'\\033[?1049l\\033[H\\033[2J'",
274
+ ";",
275
+ "printf",
276
+ "%s\\n%s\\n%s\\n%s\\n",
277
+ shellQuote(""),
278
+ shellQuote("aimux dashboard failed to start."),
279
+ shellQuote("The error above was captured from the dashboard process."),
280
+ shellQuote("Press q, Enter, or Ctrl+C to close this pane."),
281
+ ";",
282
+ "while",
283
+ "IFS= read -rsn1 key",
284
+ ";",
285
+ "do",
286
+ "if",
287
+ "[",
288
+ "-z",
289
+ '"$key"',
290
+ "]",
291
+ "||",
292
+ "[",
293
+ '"$key"',
294
+ "=",
295
+ shellQuote("q"),
296
+ "]",
297
+ ";",
298
+ "then",
299
+ "exit 0",
300
+ ";",
301
+ "fi",
302
+ ";",
303
+ "done",
304
+ ";",
305
+ "fi",
306
+ ].join(" ");
307
+ return {
308
+ scriptPath,
309
+ dashboardBuildStamp: String(statSync(scriptPath).mtimeMs),
310
+ dashboardCommand: {
311
+ cwd: projectRoot,
312
+ command: "bash",
313
+ args: ["-lc", wrappedDashboardCommand],
314
+ },
315
+ };
316
+ }
317
+ function ensureDashboardTarget(projectRoot, tmux = new TmuxRuntimeManager()) {
318
+ const { dashboardBuildStamp, dashboardCommand } = getDashboardCommandSpec(projectRoot);
319
+ pruneDashboardArtifacts(projectRoot, dashboardBuildStamp, tmux);
320
+ const dashboardSession = tmux.ensureProjectSession(projectRoot, {
321
+ cwd: dashboardCommand.cwd,
322
+ command: dashboardCommand.command,
323
+ args: dashboardCommand.args,
324
+ });
325
+ const openSessionName = tmux.getOpenSessionName(dashboardSession.sessionName, tmux.isInsideTmux());
326
+ const dashboardTarget = tmux.ensureDashboardWindow(openSessionName, projectRoot, dashboardCommand);
327
+ const currentBuildStamp = tmux.getWindowOption(dashboardTarget, "@aimux-dashboard-build");
328
+ const shouldRespawnDashboard = !tmux.isWindowAlive(dashboardTarget) || currentBuildStamp !== dashboardBuildStamp;
329
+ if (shouldRespawnDashboard) {
330
+ tmux.respawnWindow(dashboardTarget, dashboardCommand);
331
+ tmux.setWindowOption(dashboardTarget, "@aimux-dashboard-build", dashboardBuildStamp);
332
+ }
333
+ return { dashboardSession, dashboardTarget };
334
+ }
335
+ function pruneDashboardArtifacts(projectRoot, dashboardBuildStamp, tmux = new TmuxRuntimeManager()) {
336
+ const hostSession = tmux.getProjectSession(projectRoot).sessionName;
337
+ const sessions = tmux
338
+ .listSessionNames()
339
+ .filter((sessionName) => sessionName === hostSession || sessionName.startsWith(`${hostSession}-client-`));
340
+ for (const sessionName of sessions) {
341
+ const windows = tmux.listWindows(sessionName);
342
+ const dashboardWindows = windows.filter((window) => window.name.startsWith("dashboard"));
343
+ for (const window of dashboardWindows) {
344
+ const target = {
345
+ sessionName,
346
+ windowId: window.id,
347
+ windowIndex: window.index,
348
+ windowName: window.name,
349
+ };
350
+ const paneCommand = tmux.displayMessage("#{pane_current_command}", window.id);
351
+ const currentBuildStamp = tmux.getWindowOption(target, "@aimux-dashboard-build");
352
+ const invalid = !tmux.isWindowAlive(target) ||
353
+ paneCommand === "cat" ||
354
+ paneCommand === "tail" ||
355
+ !currentBuildStamp ||
356
+ currentBuildStamp !== dashboardBuildStamp;
357
+ if (!invalid)
358
+ continue;
359
+ try {
360
+ tmux.killWindow(target);
361
+ }
362
+ catch { }
363
+ }
364
+ if (sessionName === hostSession || !tmux.hasSession(sessionName))
365
+ continue;
366
+ const remaining = tmux.listWindows(sessionName);
367
+ const hasValidDashboard = remaining.some((window) => window.name.startsWith("dashboard"));
368
+ if (hasValidDashboard)
369
+ continue;
370
+ const hasNonDashboardWindows = remaining.some((window) => !window.name.startsWith("dashboard"));
371
+ if (hasNonDashboardWindows)
372
+ continue;
373
+ try {
374
+ tmux.killSession(sessionName);
375
+ }
376
+ catch { }
377
+ }
378
+ }
379
+ function getLiveDashboardTarget(projectRoot, tmux = new TmuxRuntimeManager()) {
380
+ const { dashboardBuildStamp } = getDashboardCommandSpec(projectRoot);
381
+ pruneDashboardArtifacts(projectRoot, dashboardBuildStamp, tmux);
382
+ const dashboardSession = tmux.getProjectSession(projectRoot);
383
+ const isUsableDashboardTarget = (dashboardTarget) => {
384
+ const currentBuildStamp = tmux.getWindowOption(dashboardTarget, "@aimux-dashboard-build");
385
+ const paneCommand = tmux.displayMessage("#{pane_current_command}", dashboardTarget.windowId);
386
+ return tmux.isWindowAlive(dashboardTarget) && currentBuildStamp === dashboardBuildStamp && paneCommand !== "cat";
387
+ };
388
+ if (!tmux.hasSession(dashboardSession.sessionName)) {
389
+ return null;
390
+ }
391
+ const openSessionName = tmux.peekOpenSessionName(dashboardSession.sessionName, tmux.isInsideTmux());
392
+ if (!tmux.hasSession(openSessionName)) {
393
+ const candidateSessions = tmux
394
+ .listSessionNames()
395
+ .filter((sessionName) => sessionName === dashboardSession.sessionName ||
396
+ sessionName.startsWith(`${dashboardSession.sessionName}-client-`));
397
+ const candidates = candidateSessions
398
+ .flatMap((sessionName) => tmux
399
+ .listWindows(sessionName)
400
+ .filter((window) => window.name.startsWith("dashboard"))
401
+ .map((window) => ({
402
+ sessionName,
403
+ windowId: window.id,
404
+ windowIndex: window.index,
405
+ windowName: window.name,
406
+ })))
407
+ .filter(isUsableDashboardTarget);
408
+ if (candidates.length === 1) {
409
+ return { dashboardSession, dashboardTarget: candidates[0] };
410
+ }
411
+ return null;
412
+ }
413
+ const dashboardWindow = tmux.listWindows(openSessionName).find((window) => window.name.startsWith("dashboard"));
414
+ if (!dashboardWindow) {
415
+ return null;
416
+ }
417
+ const dashboardTarget = {
418
+ sessionName: openSessionName,
419
+ windowId: dashboardWindow.id,
420
+ windowIndex: dashboardWindow.index,
421
+ windowName: dashboardWindow.name,
422
+ };
423
+ if (!isUsableDashboardTarget(dashboardTarget)) {
424
+ return null;
425
+ }
426
+ return { dashboardSession, dashboardTarget };
427
+ }
428
+ function forceReloadDashboardTarget(projectRoot, tmux = new TmuxRuntimeManager()) {
429
+ const { dashboardBuildStamp, dashboardCommand } = getDashboardCommandSpec(projectRoot);
430
+ pruneDashboardArtifacts(projectRoot, dashboardBuildStamp, tmux);
431
+ const dashboardSession = tmux.ensureProjectSession(projectRoot, {
432
+ cwd: dashboardCommand.cwd,
433
+ command: dashboardCommand.command,
434
+ args: dashboardCommand.args,
435
+ });
436
+ const openSessionName = tmux.getOpenSessionName(dashboardSession.sessionName, tmux.isInsideTmux());
437
+ const dashboardTarget = tmux.ensureDashboardWindow(openSessionName, projectRoot, dashboardCommand);
438
+ tmux.respawnWindow(dashboardTarget, dashboardCommand);
439
+ tmux.setWindowOption(dashboardTarget, "@aimux-dashboard-build", dashboardBuildStamp);
440
+ return { dashboardSession, dashboardTarget };
441
+ }
442
+ program
443
+ .name("aimux")
444
+ .description("Native CLI agent multiplexer")
445
+ .version("0.1.0")
446
+ .argument("[tool]", "Tool to run (e.g. claude, codex, aider)")
447
+ .argument("[args...]", "Arguments to pass to the tool")
448
+ .option("--resume", "Resume previous sessions using native tool resume")
449
+ .option("--restore", "Start fresh sessions with injected history context")
450
+ .option("--tmux-dashboard-internal", "Internal tmux dashboard entrypoint")
451
+ .hook("preAction", async (_thisCommand, actionCommand) => {
452
+ const opts = typeof actionCommand?.opts === "function" ? actionCommand.opts() : {};
453
+ const requestedProject = typeof opts.project === "string" ? opts.project : undefined;
454
+ const projectRoot = requestedProject ? resolveProjectRoot(pathResolve(requestedProject)) : undefined;
455
+ await initPaths(projectRoot);
456
+ })
457
+ .action(async (tool, args, opts) => {
458
+ const originalCwd = process.cwd();
459
+ const dashboardMode = !tool && !opts.resume && !opts.restore;
460
+ const shouldAnchorToMainRepo = opts.tmuxDashboardInternal || dashboardMode;
461
+ let projectRoot = originalCwd;
462
+ if (shouldAnchorToMainRepo) {
463
+ try {
464
+ projectRoot = findMainRepo(originalCwd);
465
+ }
466
+ catch {
467
+ projectRoot = originalCwd;
468
+ }
469
+ if (projectRoot !== originalCwd) {
470
+ process.chdir(projectRoot);
471
+ }
472
+ }
473
+ await initPaths(projectRoot);
474
+ if (opts.tmuxDashboardInternal) {
475
+ await ensureDaemonProjectReady(projectRoot);
476
+ }
477
+ else {
478
+ initProject();
479
+ const tmux = new TmuxRuntimeManager();
480
+ ensureTmuxAvailable(tmux);
481
+ if (!tool && !opts.resume && !opts.restore) {
482
+ const liveDashboard = getLiveDashboardTarget(projectRoot, tmux);
483
+ if (liveDashboard) {
484
+ tmux.openTarget(liveDashboard.dashboardTarget, {
485
+ insideTmux: tmux.isInsideTmux(),
486
+ alreadyResolved: true,
487
+ });
488
+ return;
489
+ }
490
+ }
491
+ await ensureDaemonProjectSpawned(projectRoot);
492
+ const { dashboardTarget } = ensureDashboardTarget(projectRoot, tmux);
493
+ if (!tool && !opts.resume && !opts.restore) {
494
+ tmux.openTarget(dashboardTarget, { insideTmux: tmux.isInsideTmux(), alreadyResolved: true });
495
+ return;
496
+ }
497
+ }
498
+ const mux = new Multiplexer();
499
+ let cleanedUp = false;
500
+ const ensureTerminalRestored = () => mux.cleanupTerminalOnly();
501
+ const cleanupAll = () => {
502
+ if (cleanedUp)
503
+ return;
504
+ cleanedUp = true;
505
+ mux.cleanup();
506
+ };
507
+ // Graceful shutdown on signals
508
+ const shutdown = () => {
509
+ cleanupAll();
510
+ process.exit(0);
511
+ };
512
+ process.on("exit", ensureTerminalRestored);
513
+ process.on("SIGINT", shutdown);
514
+ process.on("SIGTERM", shutdown);
515
+ process.on("uncaughtException", (err) => {
516
+ cleanupAll();
517
+ console.error(err);
518
+ process.exit(1);
519
+ });
520
+ process.on("unhandledRejection", (reason) => {
521
+ cleanupAll();
522
+ console.error(reason);
523
+ process.exit(1);
524
+ });
525
+ try {
526
+ let exitCode;
527
+ if (opts.resume) {
528
+ exitCode = await mux.resumeSessions(tool);
529
+ }
530
+ else if (opts.restore) {
531
+ exitCode = await mux.restoreSessions(tool);
532
+ }
533
+ else if (tool) {
534
+ exitCode = await mux.run({ command: tool, args });
535
+ }
536
+ else {
537
+ exitCode = await mux.runDashboard();
538
+ }
539
+ cleanupAll();
540
+ process.exit(exitCode);
541
+ }
542
+ catch (err) {
543
+ cleanupAll();
544
+ if (err instanceof ProjectServiceVersionError) {
545
+ console.error(renderProjectServiceVersionHelp(err));
546
+ process.exit(1);
547
+ }
548
+ const msg = err instanceof Error ? err.message : String(err);
549
+ console.error(`aimux: failed to spawn "${tool}": ${msg}`);
550
+ process.exit(1);
551
+ }
552
+ });
553
+ program
554
+ .command("init")
555
+ .description("Initialize .aimux directory with default config and gitignore")
556
+ .action(() => {
557
+ initProject();
558
+ console.log("Initialized .aimux/ with config.json and .gitignore");
559
+ });
560
+ program
561
+ .command("dashboard-reload")
562
+ .description("Force reload the managed tmux dashboard for this project")
563
+ .option("--open", "Open the dashboard after reloading")
564
+ .action(async (opts) => {
565
+ try {
566
+ const originalCwd = process.cwd();
567
+ const projectRoot = resolveProjectRoot(originalCwd);
568
+ await ensureDaemonProjectSpawned(projectRoot);
569
+ const tmux = new TmuxRuntimeManager();
570
+ ensureTmuxAvailable(tmux);
571
+ const { dashboardSession, dashboardTarget } = forceReloadDashboardTarget(projectRoot, tmux);
572
+ if (opts.open) {
573
+ tmux.openTarget(dashboardTarget, { insideTmux: tmux.isInsideTmux(), alreadyResolved: true });
574
+ return;
575
+ }
576
+ console.log(`Reloaded dashboard for ${dashboardSession.sessionName}`);
577
+ }
578
+ catch (err) {
579
+ if (err instanceof ProjectServiceVersionError) {
580
+ console.error(renderProjectServiceVersionHelp(err));
581
+ process.exit(1);
582
+ }
583
+ const msg = err instanceof Error ? err.message : String(err);
584
+ console.error(`Error: ${msg}`);
585
+ process.exit(1);
586
+ }
587
+ });
588
+ const hostCmd = program.command("host").description("Compatibility wrappers for daemon-managed project services");
589
+ program
590
+ .command("serve")
591
+ .description("Ensure the daemon-backed project control service is running")
592
+ .action(async () => {
593
+ const projectRoot = resolveProjectRoot(process.cwd());
594
+ if (projectRoot !== process.cwd()) {
595
+ process.chdir(projectRoot);
596
+ }
597
+ await initPaths(projectRoot);
598
+ await ensureDaemonProjectReady(projectRoot);
599
+ const status = await projectServiceStatus(projectRoot);
600
+ console.log(`aimux serve: daemon managing ${projectRoot}${status ? ` (service pid ${status.pid})` : ""}`);
601
+ });
602
+ hostCmd
603
+ .command("status")
604
+ .description("Show current project control-service status")
605
+ .option("--json", "Emit JSON")
606
+ .action(async (opts) => {
607
+ await initPaths();
608
+ await ensureDaemonRunning();
609
+ const projectRoot = resolveProjectRoot(process.cwd());
610
+ const project = await projectServiceStatus(projectRoot);
611
+ const endpoint = await resolveProjectServiceEndpoint(projectRoot);
612
+ const expectedServiceManifest = getProjectServiceManifest();
613
+ let liveServiceHealth = null;
614
+ if (endpoint) {
615
+ try {
616
+ liveServiceHealth = await fetchProjectServiceHealth(endpoint);
617
+ }
618
+ catch { }
619
+ }
620
+ const tmux = new TmuxRuntimeManager();
621
+ const session = tmux.getProjectSession(projectRoot);
622
+ const payload = {
623
+ projectRoot,
624
+ sessionName: session.sessionName,
625
+ daemon: loadDaemonInfo(),
626
+ projectService: project,
627
+ metadataEndpoint: endpoint,
628
+ expectedServiceManifest,
629
+ liveServiceHealth,
630
+ };
631
+ if (opts.json) {
632
+ console.log(JSON.stringify(payload, null, 2));
633
+ return;
634
+ }
635
+ if (!project) {
636
+ console.log(`No live control service for ${session.sessionName}`);
637
+ return;
638
+ }
639
+ console.log(`Service pid=${project.pid}`);
640
+ console.log(`Started: ${project.startedAt}`);
641
+ console.log(`Metadata: ${endpoint ? `http://${endpoint.host}:${endpoint.port}` : "not running"}`);
642
+ console.log(`Expected manifest: ${JSON.stringify(expectedServiceManifest)}`);
643
+ if (liveServiceHealth?.serviceInfo) {
644
+ console.log(`Live manifest: ${JSON.stringify(liveServiceHealth.serviceInfo)}`);
645
+ }
646
+ console.log(`Tmux session: ${session.sessionName}`);
647
+ });
648
+ hostCmd
649
+ .command("stop")
650
+ .description("Stop the current project's daemon-managed control service")
651
+ .action(async () => {
652
+ await initPaths();
653
+ const projectRoot = resolveProjectRoot(process.cwd());
654
+ const result = await stopProjectService(projectRoot);
655
+ if (!result) {
656
+ console.log("No live project service to stop.");
657
+ return;
658
+ }
659
+ removeMetadataEndpoint();
660
+ console.log(`Stopped project service pid ${result.pid}`);
661
+ });
662
+ hostCmd
663
+ .command("kill")
664
+ .description("Force kill the current project's daemon-managed control service")
665
+ .action(async () => {
666
+ await initPaths();
667
+ const projectRoot = resolveProjectRoot(process.cwd());
668
+ const result = await stopProjectService(projectRoot);
669
+ if (!result) {
670
+ console.log("No live project service to kill.");
671
+ return;
672
+ }
673
+ removeMetadataEndpoint();
674
+ console.log(`Killed project service pid ${result.pid}`);
675
+ });
676
+ hostCmd
677
+ .command("restart")
678
+ .description("Restart the current project's daemon-managed control service")
679
+ .option("--open", "Open the dashboard after restarting")
680
+ .option("--serve", "Restart the project service without reopening the dashboard")
681
+ .action(async (opts) => {
682
+ await initPaths();
683
+ const projectRoot = resolveProjectRoot(process.cwd());
684
+ await stopProjectService(projectRoot);
685
+ removeMetadataEndpoint();
686
+ await ensureDaemonProjectReady(projectRoot);
687
+ if (opts.serve) {
688
+ console.log(`Restarted project service for ${projectRoot}`);
689
+ return;
690
+ }
691
+ const tmux = new TmuxRuntimeManager();
692
+ ensureTmuxAvailable(tmux);
693
+ const { dashboardSession, dashboardTarget } = forceReloadDashboardTarget(projectRoot, tmux);
694
+ if (opts.open) {
695
+ tmux.openTarget(dashboardTarget, { insideTmux: tmux.isInsideTmux(), alreadyResolved: true });
696
+ return;
697
+ }
698
+ console.log(`Restarted project service for ${dashboardSession.sessionName}`);
699
+ });
700
+ hostCmd
701
+ .command("agent-send")
702
+ .description("Send raw input to a running agent session over the project HTTP service")
703
+ .argument("<sessionId>", "Agent session ID")
704
+ .argument("[data...]", "Input to send")
705
+ .option("--stdin", "Read the full input payload from stdin")
706
+ .option("--submit", "Submit after writing the input")
707
+ .action(async (sessionId, data, opts) => {
708
+ await initPaths();
709
+ const payload = opts.stdin === true ? await readAllStdin() : data.join(" ");
710
+ if (!payload) {
711
+ throw new Error("input data is required");
712
+ }
713
+ const result = await postProjectServiceJson("/agents/input", {
714
+ sessionId,
715
+ data: payload,
716
+ submit: opts.submit === true,
717
+ });
718
+ console.log(`sent input to ${result.sessionId}`);
719
+ });
720
+ hostCmd
721
+ .command("agent-read")
722
+ .description("Read captured output from a running agent session over the project HTTP service")
723
+ .argument("<sessionId>", "Agent session ID")
724
+ .option("--start-line <number>", "tmux capture-pane start line", "-120")
725
+ .action(async (sessionId, opts) => {
726
+ await initPaths();
727
+ const startLine = Number.parseInt(opts.startLine ?? "-120", 10);
728
+ if (Number.isNaN(startLine)) {
729
+ throw new Error("--start-line must be an integer");
730
+ }
731
+ const result = await getProjectServiceJson(`/agents/output?sessionId=${encodeURIComponent(sessionId)}&startLine=${encodeURIComponent(String(startLine))}`);
732
+ process.stdout.write(result.output ?? "");
733
+ if ((result.output ?? "").length > 0 && !String(result.output).endsWith("\n")) {
734
+ process.stdout.write("\n");
735
+ }
736
+ });
737
+ hostCmd
738
+ .command("agent-stream")
739
+ .description("Stream live captured output from a running agent session over SSE")
740
+ .argument("<sessionId>", "Agent session ID")
741
+ .option("--start-line <number>", "tmux capture-pane start line", "-120")
742
+ .option("--interval-ms <number>", "Polling interval in milliseconds", "500")
743
+ .action(async (sessionId, opts) => {
744
+ await initPaths();
745
+ const startLine = Number.parseInt(opts.startLine ?? "-120", 10);
746
+ const intervalMs = Number.parseInt(opts.intervalMs ?? "500", 10);
747
+ if (Number.isNaN(startLine)) {
748
+ throw new Error("--start-line must be an integer");
749
+ }
750
+ if (Number.isNaN(intervalMs) || intervalMs < 100) {
751
+ throw new Error("--interval-ms must be an integer >= 100");
752
+ }
753
+ const endpoint = await getProjectServiceEndpoint();
754
+ const controller = new AbortController();
755
+ const shutdown = () => controller.abort();
756
+ process.on("SIGINT", shutdown);
757
+ process.on("SIGTERM", shutdown);
758
+ try {
759
+ const res = await fetch(`http://${endpoint.host}:${endpoint.port}/agents/output/stream?sessionId=${encodeURIComponent(sessionId)}&startLine=${encodeURIComponent(String(startLine))}&intervalMs=${encodeURIComponent(String(intervalMs))}`, {
760
+ signal: controller.signal,
761
+ headers: {
762
+ accept: "text/event-stream",
763
+ },
764
+ });
765
+ if (!res.ok || !res.body) {
766
+ const json = await res.json().catch(() => ({}));
767
+ throw new Error(json?.error || `request failed: ${res.status}`);
768
+ }
769
+ const decoder = new TextDecoder();
770
+ let buffer = "";
771
+ let lastOutput = "";
772
+ const flushEventBlock = (block) => {
773
+ const lines = block.split("\n");
774
+ let eventName = "message";
775
+ const dataLines = [];
776
+ for (const line of lines) {
777
+ if (line.startsWith("event:")) {
778
+ eventName = line.slice("event:".length).trim();
779
+ continue;
780
+ }
781
+ if (line.startsWith("data:")) {
782
+ dataLines.push(line.slice("data:".length).trim());
783
+ }
784
+ }
785
+ if (eventName === "ready")
786
+ return;
787
+ if (eventName === "error") {
788
+ const payload = dataLines.length > 0 ? JSON.parse(dataLines.join("\n")) : {};
789
+ throw new Error(payload?.error || `stream error for ${sessionId}`);
790
+ }
791
+ if (eventName !== "output" || dataLines.length === 0)
792
+ return;
793
+ const payload = JSON.parse(dataLines.join("\n"));
794
+ if (typeof payload.output === "string") {
795
+ const nextOutput = payload.output;
796
+ const renderText = nextOutput.startsWith(lastOutput)
797
+ ? nextOutput.slice(lastOutput.length)
798
+ : `${lastOutput ? "\n[aimux stream resync]\n" : ""}${nextOutput}`;
799
+ lastOutput = nextOutput;
800
+ if (!renderText)
801
+ return;
802
+ process.stdout.write(renderText);
803
+ if (renderText.length > 0 && !renderText.endsWith("\n")) {
804
+ process.stdout.write("\n");
805
+ }
806
+ }
807
+ };
808
+ for await (const chunk of res.body) {
809
+ buffer += decoder.decode(chunk, { stream: true });
810
+ let boundary = buffer.indexOf("\n\n");
811
+ while (boundary !== -1) {
812
+ const block = buffer.slice(0, boundary).replace(/\r/g, "");
813
+ buffer = buffer.slice(boundary + 2);
814
+ if (block && !block.startsWith(":")) {
815
+ flushEventBlock(block);
816
+ }
817
+ boundary = buffer.indexOf("\n\n");
818
+ }
819
+ }
820
+ }
821
+ catch (error) {
822
+ if (error.name === "AbortError") {
823
+ return;
824
+ }
825
+ throw error;
826
+ }
827
+ finally {
828
+ process.off("SIGINT", shutdown);
829
+ process.off("SIGTERM", shutdown);
830
+ }
831
+ });
832
+ hostCmd.action(() => {
833
+ console.log("`aimux host` is a compatibility alias for daemon-managed project services.");
834
+ });
835
+ const daemonCmd = program.command("daemon").description("Manage the global aimux control-plane daemon");
836
+ daemonCmd
837
+ .command("run")
838
+ .description("Internal daemon entrypoint")
839
+ .action(async () => {
840
+ const daemon = new AimuxDaemon();
841
+ await daemon.start();
842
+ const shutdown = () => {
843
+ daemon.stop();
844
+ process.exit(0);
845
+ };
846
+ process.on("SIGINT", shutdown);
847
+ process.on("SIGTERM", shutdown);
848
+ await new Promise(() => { });
849
+ });
850
+ daemonCmd
851
+ .command("ensure")
852
+ .description("Ensure the global aimux daemon is running")
853
+ .option("--json", "Emit JSON")
854
+ .action(async (opts) => {
855
+ const info = await ensureDaemonRunning();
856
+ if (opts.json) {
857
+ console.log(JSON.stringify({ daemon: info }, null, 2));
858
+ return;
859
+ }
860
+ console.log(`aimux daemon: pid ${info.pid} on http://127.0.0.1:${info.port}`);
861
+ });
862
+ daemonCmd
863
+ .command("stop")
864
+ .description("Stop the global aimux daemon")
865
+ .action(async () => {
866
+ const info = await stopDaemon("SIGTERM");
867
+ if (!info) {
868
+ console.log("aimux daemon is not running.");
869
+ return;
870
+ }
871
+ console.log(`Stopped daemon pid ${info.pid}`);
872
+ });
873
+ daemonCmd
874
+ .command("kill")
875
+ .description("Force kill the global aimux daemon")
876
+ .action(async () => {
877
+ const info = await stopDaemon("SIGKILL");
878
+ if (!info) {
879
+ console.log("aimux daemon is not running.");
880
+ return;
881
+ }
882
+ console.log(`Killed daemon pid ${info.pid}`);
883
+ });
884
+ daemonCmd
885
+ .command("restart")
886
+ .description("Restart the global aimux daemon")
887
+ .action(async () => {
888
+ const priorProjects = Object.values(loadDaemonState().projects)
889
+ .map((project) => project.projectRoot)
890
+ .filter((projectRoot, index, items) => items.indexOf(projectRoot) === index);
891
+ await stopDaemon("SIGTERM");
892
+ const info = await ensureDaemonRunning();
893
+ for (const projectRoot of priorProjects) {
894
+ await ensureProjectService(projectRoot);
895
+ }
896
+ const restoredSuffix = priorProjects.length > 0
897
+ ? ` and restored ${priorProjects.length} project service${priorProjects.length === 1 ? "" : "s"}`
898
+ : "";
899
+ console.log(`Restarted daemon pid ${info.pid} on http://127.0.0.1:${info.port}${restoredSuffix}`);
900
+ });
901
+ daemonCmd
902
+ .command("status")
903
+ .description("Show daemon status")
904
+ .option("--json", "Emit JSON")
905
+ .action(async (opts) => {
906
+ const info = loadDaemonInfo();
907
+ const state = loadDaemonState();
908
+ const payload = {
909
+ daemon: info,
910
+ projects: Object.values(state.projects),
911
+ };
912
+ if (opts.json) {
913
+ console.log(JSON.stringify(payload, null, 2));
914
+ return;
915
+ }
916
+ if (!info) {
917
+ console.log("aimux daemon is not running.");
918
+ return;
919
+ }
920
+ console.log(`Daemon pid=${info.pid} port=${info.port}`);
921
+ console.log(`Managed projects: ${Object.keys(state.projects).length}`);
922
+ });
923
+ daemonCmd
924
+ .command("projects")
925
+ .description("List projects through the daemon")
926
+ .option("--json", "Emit JSON")
927
+ .action(async (opts) => {
928
+ await ensureDaemonRunning();
929
+ const result = await requestDaemonJson("/projects");
930
+ if (opts.json) {
931
+ console.log(JSON.stringify({ projects: result.projects }, null, 2));
932
+ return;
933
+ }
934
+ for (const project of result.projects) {
935
+ const badge = project.serviceAlive ? "service" : "idle";
936
+ console.log(`${project.name} ${badge} ${project.path}`);
937
+ }
938
+ });
939
+ daemonCmd
940
+ .command("project-ensure")
941
+ .description("Ensure a project's control service is running")
942
+ .requiredOption("--project <path>", "Project path")
943
+ .option("--json", "Emit JSON")
944
+ .action(async (opts) => {
945
+ const projectRoot = resolveProjectRoot(pathResolve(opts.project));
946
+ const project = await ensureProjectService(projectRoot);
947
+ if (opts.json) {
948
+ console.log(JSON.stringify({ project }, null, 2));
949
+ return;
950
+ }
951
+ console.log(`Ensured project service for ${projectRoot} (pid ${project.pid})`);
952
+ });
953
+ program
954
+ .command("__project-service-internal")
955
+ .description("Internal daemon-managed project service entrypoint")
956
+ .action(async () => {
957
+ const projectRoot = resolveProjectRoot(process.cwd());
958
+ if (projectRoot !== process.cwd()) {
959
+ process.chdir(projectRoot);
960
+ }
961
+ await initPaths(projectRoot);
962
+ initProject();
963
+ const mux = new Multiplexer();
964
+ let cleanedUp = false;
965
+ const ensureTerminalRestored = () => mux.cleanupTerminalOnly();
966
+ const cleanupAll = () => {
967
+ if (cleanedUp)
968
+ return;
969
+ cleanedUp = true;
970
+ mux.cleanup();
971
+ };
972
+ const shutdown = () => {
973
+ cleanupAll();
974
+ process.exit(0);
975
+ };
976
+ process.on("exit", ensureTerminalRestored);
977
+ process.on("SIGINT", shutdown);
978
+ process.on("SIGTERM", shutdown);
979
+ process.on("uncaughtException", (err) => {
980
+ cleanupAll();
981
+ console.error(err);
982
+ process.exit(1);
983
+ });
984
+ process.on("unhandledRejection", (reason) => {
985
+ cleanupAll();
986
+ console.error(reason);
987
+ process.exit(1);
988
+ });
989
+ try {
990
+ const exitCode = await mux.runProjectService();
991
+ cleanupAll();
992
+ process.exit(exitCode);
993
+ }
994
+ catch (err) {
995
+ cleanupAll();
996
+ const msg = err instanceof Error ? err.message : String(err);
997
+ console.error(`aimux project service: ${msg}`);
998
+ process.exit(1);
999
+ }
1000
+ });
1001
+ const projectsCmd = program.command("projects").description("Inspect known aimux projects");
1002
+ projectsCmd
1003
+ .command("list")
1004
+ .description("List known aimux projects")
1005
+ .option("--json", "Emit JSON")
1006
+ .action(async (opts) => {
1007
+ await ensureDaemonRunning();
1008
+ const result = await requestDaemonJson("/projects");
1009
+ const projects = result.projects;
1010
+ if (opts.json) {
1011
+ console.log(JSON.stringify({ projects }, null, 2));
1012
+ return;
1013
+ }
1014
+ if (projects.length === 0) {
1015
+ console.log("No aimux projects found.");
1016
+ return;
1017
+ }
1018
+ for (const project of projects) {
1019
+ const liveBadge = project.sessions.some((session) => session.status !== "offline") ? "live" : "idle";
1020
+ console.log(`${project.name} ${liveBadge} ${project.path}`);
1021
+ if (project.sessions.length === 0)
1022
+ continue;
1023
+ for (const session of project.sessions) {
1024
+ const label = session.label ? ` ${session.label}` : "";
1025
+ const headline = session.headline ? ` - ${session.headline}` : "";
1026
+ console.log(` ${session.id} ${session.tool} ${session.status}${label}${headline}`);
1027
+ }
1028
+ }
1029
+ });
1030
+ program
1031
+ .command("compact")
1032
+ .description("Compact session history using LLM summarization")
1033
+ .action(() => {
1034
+ const historyDir = getHistoryDir();
1035
+ let sessionIds = [];
1036
+ try {
1037
+ sessionIds = readdirSync(historyDir)
1038
+ .filter((f) => f.endsWith(".jsonl"))
1039
+ .map((f) => f.replace(/\.jsonl$/, ""));
1040
+ }
1041
+ catch {
1042
+ console.error("No history found at " + historyDir);
1043
+ process.exit(1);
1044
+ }
1045
+ if (sessionIds.length === 0) {
1046
+ console.error("No session history files found.");
1047
+ process.exit(1);
1048
+ }
1049
+ console.log(`Compacting history for ${sessionIds.length} session(s)...`);
1050
+ llmCompact(sessionIds);
1051
+ console.log(`Done. Summary written to ${getContextDir()}/summary.md`);
1052
+ });
1053
+ async function prepareProjectContext(requestedProject) {
1054
+ const requestedPath = pathResolve(requestedProject ?? process.cwd());
1055
+ const projectRoot = resolveProjectRoot(requestedPath);
1056
+ await initPaths(projectRoot);
1057
+ process.chdir(projectRoot);
1058
+ return projectRoot;
1059
+ }
1060
+ function printWorktrees(projectRoot) {
1061
+ try {
1062
+ const worktrees = listWorktrees(projectRoot);
1063
+ if (worktrees.length === 0) {
1064
+ console.log("No worktrees found.");
1065
+ return;
1066
+ }
1067
+ console.log("Name".padEnd(30) + "Branch".padEnd(35) + "Path");
1068
+ console.log("-".repeat(95));
1069
+ for (const wt of worktrees) {
1070
+ console.log(wt.name.padEnd(30) + wt.branch.padEnd(35) + wt.path);
1071
+ }
1072
+ }
1073
+ catch (err) {
1074
+ const msg = err instanceof Error ? err.message : String(err);
1075
+ console.error(`Error: ${msg}`);
1076
+ process.exit(1);
1077
+ }
1078
+ }
1079
+ const worktreeCmd = program.command("worktree").description("Manage git worktrees");
1080
+ worktreeCmd.action(() => {
1081
+ printWorktrees();
1082
+ });
1083
+ const threadCmd = program.command("thread").description("Inspect and manage orchestration threads");
1084
+ threadCmd
1085
+ .command("list")
1086
+ .description("List orchestration threads")
1087
+ .option("--session <sessionId>", "Filter to threads involving a session")
1088
+ .option("--json", "Emit JSON")
1089
+ .action((opts) => {
1090
+ const summaries = listThreadSummaries(opts.session);
1091
+ if (opts.json) {
1092
+ console.log(JSON.stringify(summaries, null, 2));
1093
+ return;
1094
+ }
1095
+ if (summaries.length === 0) {
1096
+ console.log("No threads found.");
1097
+ return;
1098
+ }
1099
+ for (const summary of summaries) {
1100
+ const unread = summary.thread.unreadBy?.length ? ` unread=${summary.thread.unreadBy.length}` : "";
1101
+ const waiting = summary.thread.waitingOn?.length ? ` waiting=${summary.thread.waitingOn.join(",")}` : "";
1102
+ console.log(`${summary.thread.id} ${summary.thread.kind} ${summary.thread.status}${unread}${waiting}`);
1103
+ console.log(` ${summary.thread.title}`);
1104
+ if (summary.latestMessage) {
1105
+ console.log(` latest: ${summary.latestMessage.from} [${summary.latestMessage.kind}] ${summary.latestMessage.body}`);
1106
+ }
1107
+ }
1108
+ });
1109
+ threadCmd
1110
+ .command("show")
1111
+ .description("Show a thread and its messages")
1112
+ .argument("<threadId>")
1113
+ .option("--json", "Emit JSON")
1114
+ .action((threadId, opts) => {
1115
+ const thread = readThread(threadId);
1116
+ if (!thread) {
1117
+ console.error(`aimux: thread not found: ${threadId}`);
1118
+ process.exit(1);
1119
+ }
1120
+ const messages = readMessages(threadId);
1121
+ if (opts.json) {
1122
+ console.log(JSON.stringify({ thread, messages }, null, 2));
1123
+ return;
1124
+ }
1125
+ console.log(`${thread.title} (${thread.kind})`);
1126
+ console.log(`id: ${thread.id}`);
1127
+ console.log(`status: ${thread.status}`);
1128
+ console.log(`participants: ${thread.participants.join(", ")}`);
1129
+ if (thread.owner)
1130
+ console.log(`owner: ${thread.owner}`);
1131
+ if (thread.waitingOn?.length)
1132
+ console.log(`waitingOn: ${thread.waitingOn.join(", ")}`);
1133
+ console.log("");
1134
+ for (const message of messages) {
1135
+ console.log(`${message.ts} ${message.from} [${message.kind}]`);
1136
+ console.log(` ${message.body}`);
1137
+ }
1138
+ });
1139
+ threadCmd
1140
+ .command("open")
1141
+ .description("Open a new orchestration thread")
1142
+ .requiredOption("--title <title>", "Thread title")
1143
+ .requiredOption("--from <sessionId>", "Creating session")
1144
+ .requiredOption("--participants <ids>", "Comma-separated participant session ids")
1145
+ .option("--kind <kind>", "conversation|task|review|handoff|user", "conversation")
1146
+ .action((opts) => {
1147
+ const participants = opts.participants
1148
+ .split(",")
1149
+ .map((value) => value.trim())
1150
+ .filter(Boolean);
1151
+ const thread = createThread({
1152
+ title: opts.title,
1153
+ kind: opts.kind ?? "conversation",
1154
+ createdBy: opts.from,
1155
+ participants: [...new Set([opts.from, ...participants])],
1156
+ });
1157
+ console.log(thread.id);
1158
+ });
1159
+ threadCmd
1160
+ .command("send")
1161
+ .description("Append a message to an orchestration thread")
1162
+ .argument("<threadId>")
1163
+ .argument("<body>")
1164
+ .requiredOption("--from <sessionId>", "Sending session")
1165
+ .option("--to <ids>", "Comma-separated recipient session ids")
1166
+ .option("--kind <kind>", "request|reply|status|decision|handoff|note", "note")
1167
+ .action((threadId, body, opts) => {
1168
+ const thread = readThread(threadId);
1169
+ if (!thread) {
1170
+ console.error(`aimux: thread not found: ${threadId}`);
1171
+ process.exit(1);
1172
+ }
1173
+ const to = opts.to
1174
+ ?.split(",")
1175
+ .map((value) => value.trim())
1176
+ .filter(Boolean);
1177
+ const message = sendThreadMessage({
1178
+ threadId,
1179
+ from: opts.from,
1180
+ to,
1181
+ kind: opts.kind ?? "note",
1182
+ body,
1183
+ }).message;
1184
+ console.log(message.id);
1185
+ });
1186
+ threadCmd
1187
+ .command("mark-seen")
1188
+ .description("Mark a thread as seen for a participant")
1189
+ .argument("<threadId>")
1190
+ .requiredOption("--session <sessionId>", "Participant session id")
1191
+ .action((threadId, opts) => {
1192
+ const thread = markThreadSeen(threadId, opts.session);
1193
+ if (!thread) {
1194
+ console.error(`aimux: thread not found: ${threadId}`);
1195
+ process.exit(1);
1196
+ }
1197
+ console.log("ok");
1198
+ });
1199
+ threadCmd
1200
+ .command("status")
1201
+ .description("Update a thread status")
1202
+ .argument("<threadId>")
1203
+ .requiredOption("--status <status>", "open|waiting|blocked|done|abandoned")
1204
+ .option("--owner <sessionId>", "Override thread owner")
1205
+ .option("--waiting-on <ids>", "Comma-separated waitingOn participants")
1206
+ .action(async (threadId, opts) => {
1207
+ const waitingOn = opts.waitingOn
1208
+ ?.split(",")
1209
+ .map((value) => value.trim())
1210
+ .filter(Boolean);
1211
+ try {
1212
+ const result = await postProjectServiceJson("/threads/status", {
1213
+ threadId,
1214
+ status: opts.status,
1215
+ owner: opts.owner,
1216
+ waitingOn,
1217
+ });
1218
+ console.log(`thread ${result.thread.id}`);
1219
+ console.log(`status ${result.thread.status}`);
1220
+ return;
1221
+ }
1222
+ catch {
1223
+ const thread = setThreadStatus(threadId, opts.status, {
1224
+ owner: opts.owner?.trim(),
1225
+ waitingOn,
1226
+ });
1227
+ if (!thread) {
1228
+ console.error(`aimux: thread not found: ${threadId}`);
1229
+ process.exit(1);
1230
+ }
1231
+ console.log(`thread ${thread.id}`);
1232
+ console.log(`status ${thread.status}`);
1233
+ }
1234
+ });
1235
+ const messageCmd = program.command("message").description("Send directed orchestration messages");
1236
+ messageCmd
1237
+ .command("send")
1238
+ .description("Send a direct message and open or reuse a conversation thread")
1239
+ .argument("<body>")
1240
+ .option("--to <ids>", "Comma-separated recipient session ids")
1241
+ .option("--assignee <role>", "Route to a role if no explicit session id is provided")
1242
+ .option("--tool <tool>", "Route to a tool if no explicit session id is provided")
1243
+ .option("--worktree <path>", "Prefer a target in this worktree")
1244
+ .option("--from <sessionId>", "Sender session id", "user")
1245
+ .option("--title <title>", "Conversation title if a new thread is opened")
1246
+ .option("--kind <kind>", "request|reply|status|decision|handoff|note", "request")
1247
+ .option("--thread <threadId>", "Append to an existing thread instead of opening/reusing a conversation")
1248
+ .action(async (body, opts) => {
1249
+ const to = opts.to
1250
+ ?.split(",")
1251
+ .map((value) => value.trim())
1252
+ .filter(Boolean);
1253
+ if ((!to || to.length === 0) && !opts.thread && !opts.assignee && !opts.tool) {
1254
+ console.error("aimux: message send requires --to, --assignee, or --tool");
1255
+ process.exit(1);
1256
+ }
1257
+ try {
1258
+ const result = await postProjectServiceJson("/threads/send", {
1259
+ threadId: opts.thread,
1260
+ from: opts.from ?? "user",
1261
+ to,
1262
+ assignee: opts.assignee,
1263
+ tool: opts.tool,
1264
+ worktreePath: opts.worktree,
1265
+ kind: opts.kind ?? "request",
1266
+ body,
1267
+ title: opts.title,
1268
+ });
1269
+ console.log(`thread ${result.thread.id}`);
1270
+ console.log(`message ${result.message.id}`);
1271
+ if (Array.isArray(result.deliveredTo) && result.deliveredTo.length > 0) {
1272
+ console.log(`delivered ${result.deliveredTo.join(",")}`);
1273
+ }
1274
+ return;
1275
+ }
1276
+ catch {
1277
+ const result = opts.thread
1278
+ ? sendThreadMessage({
1279
+ threadId: opts.thread,
1280
+ from: opts.from ?? "user",
1281
+ to,
1282
+ kind: opts.kind ?? "request",
1283
+ body,
1284
+ })
1285
+ : sendDirectMessage({
1286
+ from: opts.from ?? "user",
1287
+ to: to ?? [],
1288
+ body,
1289
+ title: opts.title,
1290
+ kind: opts.kind ?? "request",
1291
+ });
1292
+ console.log(`thread ${result.thread.id}`);
1293
+ console.log(`message ${result.message.id}`);
1294
+ }
1295
+ });
1296
+ const handoffCmd = program.command("handoff").description("Send an explicit orchestration handoff");
1297
+ handoffCmd
1298
+ .command("send")
1299
+ .description("Open a handoff thread and transfer ownership/context to another agent")
1300
+ .argument("<body>")
1301
+ .option("--to <ids>", "Comma-separated recipient session ids")
1302
+ .option("--assignee <role>", "Route to a role if no explicit session id is provided")
1303
+ .option("--tool <tool>", "Route to a tool if no explicit session id is provided")
1304
+ .option("--worktree <path>", "Prefer a target in this worktree")
1305
+ .option("--from <sessionId>", "Sender session id", "user")
1306
+ .option("--title <title>", "Handoff thread title")
1307
+ .action(async (body, opts) => {
1308
+ const to = opts.to
1309
+ ?.split(",")
1310
+ .map((value) => value.trim())
1311
+ .filter(Boolean);
1312
+ if ((!to || to.length === 0) && !opts.assignee && !opts.tool) {
1313
+ console.error("aimux: handoff send requires --to, --assignee, or --tool");
1314
+ process.exit(1);
1315
+ }
1316
+ try {
1317
+ const result = await postProjectServiceJson("/handoff", {
1318
+ from: opts.from ?? "user",
1319
+ to,
1320
+ assignee: opts.assignee,
1321
+ tool: opts.tool,
1322
+ body,
1323
+ title: opts.title,
1324
+ worktreePath: opts.worktree,
1325
+ });
1326
+ console.log(`thread ${result.thread.id}`);
1327
+ console.log(`message ${result.message.id}`);
1328
+ if (Array.isArray(result.deliveredTo) && result.deliveredTo.length > 0) {
1329
+ console.log(`delivered ${result.deliveredTo.join(",")}`);
1330
+ }
1331
+ return;
1332
+ }
1333
+ catch {
1334
+ const result = sendHandoff({
1335
+ from: opts.from ?? "user",
1336
+ to: to ?? [],
1337
+ body,
1338
+ title: opts.title,
1339
+ worktreePath: opts.worktree,
1340
+ });
1341
+ console.log(`thread ${result.thread.id}`);
1342
+ console.log(`message ${result.message.id}`);
1343
+ }
1344
+ });
1345
+ handoffCmd
1346
+ .command("accept")
1347
+ .description("Accept an existing handoff thread")
1348
+ .argument("<threadId>")
1349
+ .option("--from <sessionId>", "Accepting session id", "user")
1350
+ .option("--body <text>", "Optional acceptance note")
1351
+ .action(async (threadId, opts) => {
1352
+ try {
1353
+ const result = await postProjectServiceJson("/handoff/accept", {
1354
+ threadId,
1355
+ from: opts.from ?? "user",
1356
+ body: opts.body,
1357
+ });
1358
+ console.log(`thread ${result.thread.id}`);
1359
+ console.log(`message ${result.message.id}`);
1360
+ return;
1361
+ }
1362
+ catch {
1363
+ const result = acceptHandoff({
1364
+ threadId,
1365
+ from: opts.from ?? "user",
1366
+ body: opts.body,
1367
+ });
1368
+ console.log(`thread ${result.thread.id}`);
1369
+ console.log(`message ${result.message.id}`);
1370
+ }
1371
+ });
1372
+ handoffCmd
1373
+ .command("complete")
1374
+ .description("Complete an existing handoff thread")
1375
+ .argument("<threadId>")
1376
+ .option("--from <sessionId>", "Completing session id", "user")
1377
+ .option("--body <text>", "Optional completion note")
1378
+ .action(async (threadId, opts) => {
1379
+ try {
1380
+ const result = await postProjectServiceJson("/handoff/complete", {
1381
+ threadId,
1382
+ from: opts.from ?? "user",
1383
+ body: opts.body,
1384
+ });
1385
+ console.log(`thread ${result.thread.id}`);
1386
+ console.log(`message ${result.message.id}`);
1387
+ return;
1388
+ }
1389
+ catch {
1390
+ const result = completeHandoff({
1391
+ threadId,
1392
+ from: opts.from ?? "user",
1393
+ body: opts.body,
1394
+ });
1395
+ console.log(`thread ${result.thread.id}`);
1396
+ console.log(`message ${result.message.id}`);
1397
+ }
1398
+ });
1399
+ const taskCmd = program.command("task").description("Create and manage orchestrated tasks");
1400
+ taskCmd
1401
+ .command("assign")
1402
+ .description("Create a durable task assignment")
1403
+ .argument("<description>")
1404
+ .option("--from <sessionId>", "Assigning session id", "user")
1405
+ .option("--to <sessionId>", "Specific assignee session id")
1406
+ .option("--assignee <role>", "Role name to route to")
1407
+ .option("--tool <tool>", "Tool key to route to")
1408
+ .option("--prompt <text>", "Full task prompt")
1409
+ .option("--type <type>", "task|review", "task")
1410
+ .option("--diff <text>", "Optional diff snippet or review payload")
1411
+ .option("--worktree <path>", "Associated worktree path")
1412
+ .action(async (description, opts) => {
1413
+ try {
1414
+ const result = await postProjectServiceJson("/tasks/assign", {
1415
+ from: opts.from ?? "user",
1416
+ to: opts.to,
1417
+ assignee: opts.assignee,
1418
+ tool: opts.tool,
1419
+ description,
1420
+ prompt: opts.prompt,
1421
+ type: opts.type,
1422
+ diff: opts.diff,
1423
+ worktreePath: opts.worktree,
1424
+ });
1425
+ console.log(`task ${result.task.id}`);
1426
+ if (result.thread?.id)
1427
+ console.log(`thread ${result.thread.id}`);
1428
+ return;
1429
+ }
1430
+ catch {
1431
+ const result = await assignTask({
1432
+ from: opts.from ?? "user",
1433
+ to: opts.to,
1434
+ assignee: opts.assignee,
1435
+ tool: opts.tool,
1436
+ description,
1437
+ prompt: opts.prompt,
1438
+ type: opts.type,
1439
+ diff: opts.diff,
1440
+ worktreePath: opts.worktree,
1441
+ });
1442
+ console.log(`task ${result.task.id}`);
1443
+ if (result.thread?.id)
1444
+ console.log(`thread ${result.thread.id}`);
1445
+ }
1446
+ });
1447
+ taskCmd
1448
+ .command("accept")
1449
+ .description("Accept an assigned task and mark it in progress")
1450
+ .argument("<taskId>")
1451
+ .option("--from <sessionId>", "Accepting session id", "user")
1452
+ .option("--body <text>", "Optional acceptance note")
1453
+ .action(async (taskId, opts) => {
1454
+ try {
1455
+ const result = await postProjectServiceJson("/tasks/accept", {
1456
+ taskId,
1457
+ from: opts.from ?? "user",
1458
+ body: opts.body,
1459
+ });
1460
+ console.log(`task ${result.task.id}`);
1461
+ if (result.thread?.id)
1462
+ console.log(`thread ${result.thread.id}`);
1463
+ return;
1464
+ }
1465
+ catch {
1466
+ const result = await acceptTask({
1467
+ taskId,
1468
+ from: opts.from ?? "user",
1469
+ body: opts.body,
1470
+ });
1471
+ console.log(`task ${result.task.id}`);
1472
+ if (result.thread?.id)
1473
+ console.log(`thread ${result.thread.id}`);
1474
+ }
1475
+ });
1476
+ taskCmd
1477
+ .command("block")
1478
+ .description("Mark a task blocked and route it back for attention")
1479
+ .argument("<taskId>")
1480
+ .option("--from <sessionId>", "Blocking session id", "user")
1481
+ .option("--body <text>", "Blocking reason")
1482
+ .action(async (taskId, opts) => {
1483
+ try {
1484
+ const result = await postProjectServiceJson("/tasks/block", {
1485
+ taskId,
1486
+ from: opts.from ?? "user",
1487
+ body: opts.body,
1488
+ });
1489
+ console.log(`task ${result.task.id}`);
1490
+ if (result.thread?.id)
1491
+ console.log(`thread ${result.thread.id}`);
1492
+ return;
1493
+ }
1494
+ catch {
1495
+ const result = await blockTask({
1496
+ taskId,
1497
+ from: opts.from ?? "user",
1498
+ body: opts.body,
1499
+ });
1500
+ console.log(`task ${result.task.id}`);
1501
+ if (result.thread?.id)
1502
+ console.log(`thread ${result.thread.id}`);
1503
+ }
1504
+ });
1505
+ taskCmd
1506
+ .command("complete")
1507
+ .description("Complete a task explicitly and publish the result")
1508
+ .argument("<taskId>")
1509
+ .option("--from <sessionId>", "Completing session id", "user")
1510
+ .option("--body <text>", "Completion summary/result")
1511
+ .action(async (taskId, opts) => {
1512
+ try {
1513
+ const result = await postProjectServiceJson("/tasks/complete", {
1514
+ taskId,
1515
+ from: opts.from ?? "user",
1516
+ body: opts.body,
1517
+ });
1518
+ console.log(`task ${result.task.id}`);
1519
+ if (result.thread?.id)
1520
+ console.log(`thread ${result.thread.id}`);
1521
+ return;
1522
+ }
1523
+ catch {
1524
+ const result = await completeTask({
1525
+ taskId,
1526
+ from: opts.from ?? "user",
1527
+ body: opts.body,
1528
+ });
1529
+ console.log(`task ${result.task.id}`);
1530
+ if (result.thread?.id)
1531
+ console.log(`thread ${result.thread.id}`);
1532
+ }
1533
+ });
1534
+ taskCmd
1535
+ .command("reopen")
1536
+ .description("Reopen a completed or blocked task chain")
1537
+ .argument("<taskId>")
1538
+ .option("--from <sessionId>", "Reopening session id", "user")
1539
+ .option("--body <text>", "Optional reopening note")
1540
+ .action(async (taskId, opts) => {
1541
+ try {
1542
+ const result = await postProjectServiceJson("/tasks/reopen", {
1543
+ taskId,
1544
+ from: opts.from ?? "user",
1545
+ body: opts.body,
1546
+ });
1547
+ console.log(`task ${result.task.id}`);
1548
+ if (result.thread?.id)
1549
+ console.log(`thread ${result.thread.id}`);
1550
+ return;
1551
+ }
1552
+ catch {
1553
+ const result = await reopenTask({
1554
+ taskId,
1555
+ from: opts.from ?? "user",
1556
+ body: opts.body,
1557
+ });
1558
+ console.log(`task ${result.task.id}`);
1559
+ if (result.thread?.id)
1560
+ console.log(`thread ${result.thread.id}`);
1561
+ }
1562
+ });
1563
+ const reviewCmd = program.command("review").description("Manage review workflow tasks");
1564
+ reviewCmd
1565
+ .command("approve")
1566
+ .description("Approve a review task")
1567
+ .argument("<taskId>")
1568
+ .option("--from <sessionId>", "Reviewer session id", "user")
1569
+ .option("--body <text>", "Optional approval note")
1570
+ .action(async (taskId, opts) => {
1571
+ try {
1572
+ const result = await postProjectServiceJson("/reviews/approve", {
1573
+ taskId,
1574
+ from: opts.from ?? "user",
1575
+ body: opts.body,
1576
+ });
1577
+ console.log(`task ${result.task.id}`);
1578
+ if (result.thread?.id)
1579
+ console.log(`thread ${result.thread.id}`);
1580
+ return;
1581
+ }
1582
+ catch {
1583
+ const result = await approveReview({
1584
+ taskId,
1585
+ from: opts.from ?? "user",
1586
+ body: opts.body,
1587
+ });
1588
+ console.log(`task ${result.task.id}`);
1589
+ if (result.thread?.id)
1590
+ console.log(`thread ${result.thread.id}`);
1591
+ }
1592
+ });
1593
+ reviewCmd
1594
+ .command("request-changes")
1595
+ .description("Request changes on a review task")
1596
+ .argument("<taskId>")
1597
+ .option("--from <sessionId>", "Reviewer session id", "user")
1598
+ .option("--body <text>", "Requested changes")
1599
+ .action(async (taskId, opts) => {
1600
+ try {
1601
+ const result = await postProjectServiceJson("/reviews/request-changes", {
1602
+ taskId,
1603
+ from: opts.from ?? "user",
1604
+ body: opts.body,
1605
+ });
1606
+ console.log(`task ${result.task.id}`);
1607
+ if (result.followUpTask?.id)
1608
+ console.log(`follow-up ${result.followUpTask.id}`);
1609
+ if (result.thread?.id)
1610
+ console.log(`thread ${result.thread.id}`);
1611
+ return;
1612
+ }
1613
+ catch {
1614
+ const result = await requestTaskChanges({
1615
+ taskId,
1616
+ from: opts.from ?? "user",
1617
+ body: opts.body,
1618
+ });
1619
+ console.log(`task ${result.task.id}`);
1620
+ if (result.followUpTask?.id)
1621
+ console.log(`follow-up ${result.followUpTask.id}`);
1622
+ if (result.thread?.id)
1623
+ console.log(`thread ${result.thread.id}`);
1624
+ }
1625
+ });
1626
+ worktreeCmd
1627
+ .command("list")
1628
+ .description("List all git worktrees")
1629
+ .option("--project <path>", "Project path")
1630
+ .option("--json", "Emit JSON")
1631
+ .action(async (opts) => {
1632
+ const projectRoot = await prepareProjectContext(opts.project);
1633
+ const worktrees = listWorktrees(projectRoot);
1634
+ if (opts.json) {
1635
+ console.log(JSON.stringify(worktrees, null, 2));
1636
+ return;
1637
+ }
1638
+ printWorktrees(projectRoot);
1639
+ });
1640
+ worktreeCmd
1641
+ .command("create <name>")
1642
+ .description("Create a git worktree")
1643
+ .option("--project <path>", "Project path")
1644
+ .option("--json", "Emit JSON")
1645
+ .action(async (name, opts) => {
1646
+ try {
1647
+ const projectRoot = await prepareProjectContext(opts.project);
1648
+ const createdPath = createWorktree(name, projectRoot);
1649
+ if (opts.json) {
1650
+ console.log(JSON.stringify({
1651
+ ok: true,
1652
+ name,
1653
+ path: createdPath,
1654
+ projectRoot,
1655
+ }, null, 2));
1656
+ return;
1657
+ }
1658
+ console.log(`Created worktree "${name}" at ${createdPath}`);
1659
+ }
1660
+ catch (err) {
1661
+ const msg = err instanceof Error ? err.message : String(err);
1662
+ console.error(`Error: ${msg}`);
1663
+ process.exit(1);
1664
+ }
1665
+ });
1666
+ program
1667
+ .command("spawn")
1668
+ .description("Spawn a fresh agent session using the same flow as the dashboard")
1669
+ .requiredOption("--tool <toolKey>", "Configured target tool key, e.g. claude or codex")
1670
+ .option("--project <path>", "Project path")
1671
+ .option("--worktree <path>", "Target worktree path")
1672
+ .option("--no-open", "Do not switch into the spawned agent window")
1673
+ .option("--json", "Emit JSON")
1674
+ .action(async (opts) => {
1675
+ try {
1676
+ const projectRoot = await prepareProjectContext(opts.project);
1677
+ await ensureDaemonProjectReady(projectRoot);
1678
+ initProject();
1679
+ const mux = new Multiplexer();
1680
+ const targetWorktreePath = opts.worktree ? pathResolve(opts.worktree) : undefined;
1681
+ const result = await mux.spawnAgent({
1682
+ toolConfigKey: opts.tool,
1683
+ targetWorktreePath,
1684
+ open: opts.open,
1685
+ });
1686
+ if (opts.json) {
1687
+ console.log(JSON.stringify({
1688
+ ok: true,
1689
+ projectRoot,
1690
+ sessionId: result.sessionId,
1691
+ tool: opts.tool,
1692
+ worktreePath: targetWorktreePath ?? projectRoot,
1693
+ opened: opts.open !== false,
1694
+ }, null, 2));
1695
+ return;
1696
+ }
1697
+ console.log(`spawned ${result.sessionId}`);
1698
+ }
1699
+ catch (err) {
1700
+ const msg = err instanceof Error ? err.message : String(err);
1701
+ console.error(`Error: ${msg}`);
1702
+ process.exit(1);
1703
+ }
1704
+ });
1705
+ program
1706
+ .command("fork")
1707
+ .description("Fork an existing agent into a new agent with handed-off context")
1708
+ .argument("<sourceSessionId>", "Source session id to fork from")
1709
+ .requiredOption("--tool <toolKey>", "Configured target tool key, e.g. claude or codex")
1710
+ .option("--project <path>", "Project path")
1711
+ .option("--instruction <text>", "Extra instruction for the forked agent")
1712
+ .option("--worktree <path>", "Target worktree path")
1713
+ .option("--no-open", "Do not switch into the forked agent window")
1714
+ .option("--json", "Emit JSON")
1715
+ .action(async (sourceSessionId, opts) => {
1716
+ try {
1717
+ const projectRoot = await prepareProjectContext(opts.project);
1718
+ await ensureDaemonProjectReady(projectRoot);
1719
+ initProject();
1720
+ const mux = new Multiplexer();
1721
+ const targetWorktreePath = opts.worktree ? pathResolve(opts.worktree) : undefined;
1722
+ const result = await mux.forkAgent({
1723
+ sourceSessionId,
1724
+ targetToolConfigKey: opts.tool,
1725
+ instruction: opts.instruction,
1726
+ targetWorktreePath,
1727
+ open: opts.open,
1728
+ });
1729
+ if (opts.json) {
1730
+ console.log(JSON.stringify({
1731
+ ok: true,
1732
+ projectRoot,
1733
+ sourceSessionId,
1734
+ sessionId: result.sessionId,
1735
+ threadId: result.threadId,
1736
+ tool: opts.tool,
1737
+ worktreePath: targetWorktreePath ?? projectRoot,
1738
+ opened: opts.open !== false,
1739
+ }, null, 2));
1740
+ return;
1741
+ }
1742
+ console.log(`forked ${result.sessionId}`);
1743
+ console.log(`thread ${result.threadId}`);
1744
+ }
1745
+ catch (err) {
1746
+ const msg = err instanceof Error ? err.message : String(err);
1747
+ console.error(`Error: ${msg}`);
1748
+ process.exit(1);
1749
+ }
1750
+ });
1751
+ const graveyardCmd = program.command("graveyard").description("Manage killed agents (recoverable)");
1752
+ graveyardCmd
1753
+ .command("list")
1754
+ .description("List agents in the graveyard")
1755
+ .option("--project <path>", "Project path")
1756
+ .option("--json", "Emit JSON")
1757
+ .action(async (opts) => {
1758
+ await prepareProjectContext(opts.project);
1759
+ const graveyardPath = getGraveyardPath();
1760
+ try {
1761
+ const graveyard = JSON.parse(readFileSync(graveyardPath, "utf-8"));
1762
+ if (opts.json) {
1763
+ console.log(JSON.stringify(Array.isArray(graveyard) ? graveyard : [], null, 2));
1764
+ return;
1765
+ }
1766
+ if (!Array.isArray(graveyard) || graveyard.length === 0) {
1767
+ console.log("Graveyard is empty.");
1768
+ return;
1769
+ }
1770
+ console.log("ID".padEnd(25) + "Tool".padEnd(15) + "Backend Session ID");
1771
+ console.log("-".repeat(70));
1772
+ for (const s of graveyard) {
1773
+ console.log((s.id ?? "?").padEnd(25) + (s.command ?? s.tool ?? "?").padEnd(15) + (s.backendSessionId ?? "(none)"));
1774
+ }
1775
+ }
1776
+ catch {
1777
+ if (opts.json) {
1778
+ console.log("[]");
1779
+ return;
1780
+ }
1781
+ console.log("Graveyard is empty.");
1782
+ }
1783
+ });
1784
+ graveyardCmd
1785
+ .command("send <id>")
1786
+ .description("Send an agent to the graveyard from running or offline state")
1787
+ .option("--project <path>", "Project path")
1788
+ .option("--json", "Emit JSON")
1789
+ .action(async (id, opts) => {
1790
+ try {
1791
+ const projectRoot = await prepareProjectContext(opts.project);
1792
+ const mux = new Multiplexer();
1793
+ const result = await mux.sendAgentToGraveyard(id);
1794
+ if (opts.json) {
1795
+ console.log(JSON.stringify({
1796
+ ok: true,
1797
+ projectRoot,
1798
+ sessionId: result.sessionId,
1799
+ status: result.status,
1800
+ previousStatus: result.previousStatus,
1801
+ }, null, 2));
1802
+ return;
1803
+ }
1804
+ console.log(`graveyarded ${result.sessionId}`);
1805
+ }
1806
+ catch (err) {
1807
+ const msg = err instanceof Error ? err.message : String(err);
1808
+ console.error(`Error: ${msg}`);
1809
+ process.exit(1);
1810
+ }
1811
+ });
1812
+ graveyardCmd
1813
+ .command("resurrect <id>")
1814
+ .description("Resurrect an agent from the graveyard back to offline state")
1815
+ .option("--project <path>", "Project path")
1816
+ .option("--json", "Emit JSON")
1817
+ .action(async (id, opts) => {
1818
+ await prepareProjectContext(opts.project);
1819
+ const graveyardPath = getGraveyardPath();
1820
+ if (!existsSync(graveyardPath)) {
1821
+ console.error("Graveyard is empty.");
1822
+ process.exit(1);
1823
+ }
1824
+ try {
1825
+ const graveyard = JSON.parse(readFileSync(graveyardPath, "utf-8"));
1826
+ const idx = graveyard.findIndex((s) => s.id === id);
1827
+ if (idx === -1) {
1828
+ console.error(`Agent "${id}" not found in graveyard.`);
1829
+ process.exit(1);
1830
+ }
1831
+ const restored = graveyard.splice(idx, 1)[0];
1832
+ writeFileSync(graveyardPath, JSON.stringify(graveyard, null, 2) + "\n");
1833
+ const statePath = getStatePath();
1834
+ let state = {
1835
+ savedAt: new Date().toISOString(),
1836
+ cwd: process.cwd(),
1837
+ sessions: [],
1838
+ };
1839
+ if (existsSync(statePath)) {
1840
+ try {
1841
+ state = JSON.parse(readFileSync(statePath, "utf-8"));
1842
+ }
1843
+ catch { }
1844
+ }
1845
+ state.sessions.push(restored);
1846
+ writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
1847
+ if (opts.json) {
1848
+ console.log(JSON.stringify({
1849
+ ok: true,
1850
+ sessionId: id,
1851
+ status: "offline",
1852
+ }, null, 2));
1853
+ return;
1854
+ }
1855
+ console.log(`Resurrected "${id}". It will appear as offline next time you start aimux.`);
1856
+ }
1857
+ catch (err) {
1858
+ const msg = err instanceof Error ? err.message : String(err);
1859
+ console.error(`Error: ${msg}`);
1860
+ process.exit(1);
1861
+ }
1862
+ });
1863
+ program
1864
+ .command("stop <sessionId>")
1865
+ .description("Stop a running agent and move it to offline state")
1866
+ .option("--project <path>", "Project path")
1867
+ .option("--json", "Emit JSON")
1868
+ .action(async (sessionId, opts) => {
1869
+ try {
1870
+ const projectRoot = await prepareProjectContext(opts.project);
1871
+ const mux = new Multiplexer();
1872
+ const result = await mux.stopAgent(sessionId);
1873
+ if (opts.json) {
1874
+ console.log(JSON.stringify({
1875
+ ok: true,
1876
+ projectRoot,
1877
+ sessionId: result.sessionId,
1878
+ status: result.status,
1879
+ }, null, 2));
1880
+ return;
1881
+ }
1882
+ console.log(`stopped ${result.sessionId}`);
1883
+ }
1884
+ catch (err) {
1885
+ const msg = err instanceof Error ? err.message : String(err);
1886
+ console.error(`Error: ${msg}`);
1887
+ process.exit(1);
1888
+ }
1889
+ });
1890
+ program
1891
+ .command("rename <sessionId>")
1892
+ .description("Rename an agent label in running or offline state")
1893
+ .requiredOption("--label <label>", "New agent label")
1894
+ .option("--project <path>", "Project path")
1895
+ .option("--json", "Emit JSON")
1896
+ .action(async (sessionId, opts) => {
1897
+ try {
1898
+ const projectRoot = await prepareProjectContext(opts.project);
1899
+ const mux = new Multiplexer();
1900
+ const result = await mux.renameAgent(sessionId, opts.label);
1901
+ if (opts.json) {
1902
+ console.log(JSON.stringify({
1903
+ ok: true,
1904
+ projectRoot,
1905
+ sessionId: result.sessionId,
1906
+ label: result.label,
1907
+ }, null, 2));
1908
+ return;
1909
+ }
1910
+ console.log(`renamed ${result.sessionId} -> ${result.label ?? ""}`.trim());
1911
+ }
1912
+ catch (err) {
1913
+ const msg = err instanceof Error ? err.message : String(err);
1914
+ console.error(`Error: ${msg}`);
1915
+ process.exit(1);
1916
+ }
1917
+ });
1918
+ program
1919
+ .command("kill <sessionId>")
1920
+ .description("Send an agent to the graveyard from running or offline state")
1921
+ .option("--project <path>", "Project path")
1922
+ .option("--json", "Emit JSON")
1923
+ .action(async (sessionId, opts) => {
1924
+ try {
1925
+ const projectRoot = await prepareProjectContext(opts.project);
1926
+ const mux = new Multiplexer();
1927
+ const result = await mux.sendAgentToGraveyard(sessionId);
1928
+ if (opts.json) {
1929
+ console.log(JSON.stringify({
1930
+ ok: true,
1931
+ projectRoot,
1932
+ sessionId: result.sessionId,
1933
+ status: result.status,
1934
+ previousStatus: result.previousStatus,
1935
+ }, null, 2));
1936
+ return;
1937
+ }
1938
+ console.log(`graveyarded ${result.sessionId}`);
1939
+ }
1940
+ catch (err) {
1941
+ const msg = err instanceof Error ? err.message : String(err);
1942
+ console.error(`Error: ${msg}`);
1943
+ process.exit(1);
1944
+ }
1945
+ });
1946
+ program
1947
+ .command("migrate <sessionId>")
1948
+ .description("Migrate a running agent into another worktree")
1949
+ .requiredOption("--worktree <path>", "Target worktree path")
1950
+ .option("--project <path>", "Project path")
1951
+ .option("--json", "Emit JSON")
1952
+ .action(async (sessionId, opts) => {
1953
+ try {
1954
+ const projectRoot = await prepareProjectContext(opts.project);
1955
+ const mux = new Multiplexer();
1956
+ const targetWorktreePath = pathResolve(opts.worktree);
1957
+ const result = await mux.migrateAgentSession(sessionId, targetWorktreePath);
1958
+ if (opts.json) {
1959
+ console.log(JSON.stringify({
1960
+ ok: true,
1961
+ projectRoot,
1962
+ sessionId: result.sessionId,
1963
+ worktreePath: result.worktreePath ?? projectRoot,
1964
+ }, null, 2));
1965
+ return;
1966
+ }
1967
+ console.log(`migrated ${result.sessionId} -> ${result.worktreePath ?? projectRoot}`);
1968
+ }
1969
+ catch (err) {
1970
+ const msg = err instanceof Error ? err.message : String(err);
1971
+ console.error(`Error: ${msg}`);
1972
+ process.exit(1);
1973
+ }
1974
+ });
1975
+ // ── Statusline commands ────────────────────────────────────────────
1976
+ const statuslineCmd = program.command("statusline").description("Manage Claude Code statusline integration");
1977
+ const doctorCmd = program.command("doctor").description("Inspect aimux runtime compatibility");
1978
+ doctorCmd
1979
+ .command("tmux")
1980
+ .description("Inspect managed tmux session compatibility state")
1981
+ .option("--project-root <path>", "Project root", process.cwd())
1982
+ .option("--session <name>", "Managed tmux session name override")
1983
+ .option("--window-id <id>", "Specific tmux window id to inspect")
1984
+ .option("--json", "Emit JSON")
1985
+ .action(async (opts) => {
1986
+ await initPaths(opts.projectRoot);
1987
+ const tmux = new TmuxRuntimeManager();
1988
+ const report = buildTmuxDoctorReport(tmux, {
1989
+ projectRoot: opts.projectRoot,
1990
+ sessionName: opts.session,
1991
+ windowId: opts.windowId,
1992
+ });
1993
+ if (opts.json) {
1994
+ console.log(JSON.stringify(report, null, 2));
1995
+ return;
1996
+ }
1997
+ console.log(renderTmuxDoctorReport(report));
1998
+ });
1999
+ const metadataCmd = program.command("metadata").description("Push metadata into aimux tmux status integration");
2000
+ const metadataTracker = new AgentTracker();
2001
+ metadataCmd
2002
+ .command("endpoint")
2003
+ .description("Print the local metadata API endpoint")
2004
+ .action(async () => {
2005
+ await initPaths();
2006
+ const endpoint = loadMetadataEndpoint();
2007
+ if (!endpoint) {
2008
+ console.error("aimux metadata API is not running for this project");
2009
+ process.exit(1);
2010
+ }
2011
+ console.log(`http://${endpoint.host}:${endpoint.port}`);
2012
+ });
2013
+ metadataCmd
2014
+ .command("event <session> <kind>")
2015
+ .option("--message <message>", "Event message")
2016
+ .option("--source <source>", "Event source")
2017
+ .option("--tone <tone>", "Event tone")
2018
+ .option("--thread-id <threadId>", "Thread identifier")
2019
+ .option("--thread-name <threadName>", "Thread name")
2020
+ .description("Emit a normalized agent event")
2021
+ .action(async (session, kind, opts) => {
2022
+ await initPaths();
2023
+ metadataTracker.emit(session, {
2024
+ kind,
2025
+ message: opts.message,
2026
+ source: opts.source,
2027
+ tone: opts.tone,
2028
+ threadId: opts.threadId,
2029
+ threadName: opts.threadName,
2030
+ });
2031
+ });
2032
+ metadataCmd
2033
+ .command("mark-seen <session>")
2034
+ .description("Mark a session's unseen activity as seen")
2035
+ .action(async (session) => {
2036
+ await initPaths();
2037
+ metadataTracker.markSeen(session);
2038
+ });
2039
+ metadataCmd
2040
+ .command("set-activity <session> <activity>")
2041
+ .description("Set derived activity state for a session")
2042
+ .action(async (session, activity) => {
2043
+ await initPaths();
2044
+ metadataTracker.setActivity(session, activity);
2045
+ });
2046
+ metadataCmd
2047
+ .command("set-attention <session> <attention>")
2048
+ .description("Set derived attention state for a session")
2049
+ .action(async (session, attention) => {
2050
+ await initPaths();
2051
+ metadataTracker.setAttention(session, attention);
2052
+ });
2053
+ program
2054
+ .command("notify")
2055
+ .description("Send a project notification")
2056
+ .requiredOption("--title <title>", "Notification title")
2057
+ .option("--subtitle <subtitle>", "Notification subtitle")
2058
+ .option("--body <body>", "Notification body")
2059
+ .option("--session <sessionId>", "Related session id")
2060
+ .option("--kind <kind>", "Notification kind", "notification")
2061
+ .option("--json", "Emit JSON output")
2062
+ .action(async (opts) => {
2063
+ await initPaths();
2064
+ const title = opts.title.trim();
2065
+ const body = opts.body?.trim() || title;
2066
+ const result = await postProjectServiceJsonOrLocal("/notify", {
2067
+ title,
2068
+ subtitle: opts.subtitle?.trim() || undefined,
2069
+ message: body,
2070
+ sessionId: opts.session?.trim() || undefined,
2071
+ kind: opts.kind?.trim() || "notification",
2072
+ }, () => ({
2073
+ ok: true,
2074
+ notification: addNotification({
2075
+ title,
2076
+ subtitle: opts.subtitle?.trim() || undefined,
2077
+ body,
2078
+ sessionId: opts.session?.trim() || undefined,
2079
+ kind: opts.kind?.trim() || "notification",
2080
+ }),
2081
+ }));
2082
+ if (opts.json) {
2083
+ console.log(JSON.stringify(result));
2084
+ return;
2085
+ }
2086
+ const count = unreadNotificationCount();
2087
+ console.log(`Queued notification "${title}" (${count} unread).`);
2088
+ });
2089
+ program
2090
+ .command("claude-hook <action>")
2091
+ .description("Internal Claude hook adapter modeled after cmux")
2092
+ .requiredOption("--session <sessionId>", "Aimux session id")
2093
+ .requiredOption("--project <path>", "Project path")
2094
+ .option("--json", "Emit JSON output")
2095
+ .action(async (action, opts) => {
2096
+ const projectRoot = resolveProjectRoot(pathResolve(opts.project));
2097
+ await initPaths(projectRoot);
2098
+ const rawInput = await readAllStdin();
2099
+ const payload = parseClaudeHookPayload(rawInput);
2100
+ const sessionId = await resolveClaudeHookSessionId(opts.session, payload.session_id);
2101
+ const result = { ok: true, action, sessionId };
2102
+ const setActivity = async (activity) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-activity", { session: sessionId, activity }, () => metadataTracker.setActivity(sessionId, activity, projectRoot));
2103
+ const setAttention = async (attention) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-attention", { session: sessionId, attention }, () => metadataTracker.setAttention(sessionId, attention, projectRoot));
2104
+ const emitEvent = async (kind, message, tone) => postLiveProjectServiceJsonOrLocal(projectRoot, "/event", { session: sessionId, event: { kind, message, tone } }, () => metadataTracker.emit(sessionId, { kind, message, tone }, projectRoot));
2105
+ const clearSessionNotifications = async () => postLiveProjectServiceJsonOrLocal(projectRoot, "/notifications/clear", { sessionId }, () => ({
2106
+ ok: true,
2107
+ cleared: clearNotifications({ sessionId }),
2108
+ }));
2109
+ switch (action) {
2110
+ case "session-start":
2111
+ case "active":
2112
+ break;
2113
+ case "prompt-submit":
2114
+ case "pre-tool-use":
2115
+ await clearSessionNotifications();
2116
+ await setActivity("running");
2117
+ await setAttention("normal");
2118
+ await postLiveProjectServiceJsonOrLocal(projectRoot, "/mark-seen", { session: sessionId }, () => metadataTracker.markSeen(sessionId, projectRoot));
2119
+ break;
2120
+ case "notification":
2121
+ case "notify": {
2122
+ const summary = summarizeClaudeNotification(payload);
2123
+ await postLiveProjectServiceJsonOrLocal(projectRoot, "/notify", {
2124
+ title: "Claude Code",
2125
+ subtitle: summary.subtitle,
2126
+ message: summary.body,
2127
+ sessionId,
2128
+ kind: "needs_input",
2129
+ }, () => ({
2130
+ ok: true,
2131
+ notification: addNotification({
2132
+ title: "Claude Code",
2133
+ subtitle: summary.subtitle,
2134
+ body: summary.body,
2135
+ sessionId,
2136
+ kind: "needs_input",
2137
+ }),
2138
+ }));
2139
+ await emitEvent("needs_input", summary.body, "warn");
2140
+ break;
2141
+ }
2142
+ case "stop":
2143
+ case "idle": {
2144
+ const summary = summarizeClaudeStop(payload);
2145
+ await postLiveProjectServiceJsonOrLocal(projectRoot, "/notify", {
2146
+ title: "Claude Code",
2147
+ subtitle: summary.subtitle,
2148
+ message: summary.body,
2149
+ sessionId,
2150
+ kind: "task_done",
2151
+ }, () => ({
2152
+ ok: true,
2153
+ notification: addNotification({
2154
+ title: "Claude Code",
2155
+ subtitle: summary.subtitle,
2156
+ body: summary.body,
2157
+ sessionId,
2158
+ kind: "task_done",
2159
+ }),
2160
+ }));
2161
+ await emitEvent("task_done", summary.body, "success");
2162
+ break;
2163
+ }
2164
+ case "session-end":
2165
+ break;
2166
+ default:
2167
+ throw new Error(`Unsupported claude hook action: ${action}`);
2168
+ }
2169
+ if (opts.json) {
2170
+ console.log(JSON.stringify(result));
2171
+ return;
2172
+ }
2173
+ console.log("OK");
2174
+ });
2175
+ program
2176
+ .command("list-notifications")
2177
+ .description("List project notifications")
2178
+ .option("--unread", "Show only unread notifications")
2179
+ .option("--session <sessionId>", "Filter by session id")
2180
+ .option("--json", "Emit JSON output")
2181
+ .action(async (opts) => {
2182
+ await initPaths();
2183
+ const notifications = listNotifications({
2184
+ unreadOnly: Boolean(opts.unread),
2185
+ sessionId: opts.session?.trim() || undefined,
2186
+ });
2187
+ const unreadCount = unreadNotificationCount({ sessionId: opts.session?.trim() || undefined });
2188
+ if (opts.json) {
2189
+ console.log(JSON.stringify({ notifications, unreadCount }));
2190
+ return;
2191
+ }
2192
+ if (notifications.length === 0) {
2193
+ console.log("No notifications.");
2194
+ return;
2195
+ }
2196
+ for (const notification of notifications) {
2197
+ const state = notification.unread ? "unread" : "read";
2198
+ const session = notification.sessionId ? ` [${notification.sessionId}]` : "";
2199
+ console.log(`${notification.id} ${state}${session} ${notification.title}: ${notification.body}`);
2200
+ }
2201
+ });
2202
+ program
2203
+ .command("clear-notifications")
2204
+ .description("Clear project notifications")
2205
+ .option("--session <sessionId>", "Clear only notifications for a session")
2206
+ .option("--json", "Emit JSON output")
2207
+ .action(async (opts) => {
2208
+ await initPaths();
2209
+ const cleared = clearNotifications({ sessionId: opts.session?.trim() || undefined });
2210
+ if (opts.json) {
2211
+ console.log(JSON.stringify({ ok: true, cleared }));
2212
+ return;
2213
+ }
2214
+ console.log(`Cleared ${cleared} notification${cleared === 1 ? "" : "s"}.`);
2215
+ });
2216
+ program
2217
+ .command("read-notifications")
2218
+ .description("Mark project notifications as read")
2219
+ .option("--session <sessionId>", "Mark only notifications for a session as read")
2220
+ .option("--json", "Emit JSON output")
2221
+ .action(async (opts) => {
2222
+ await initPaths();
2223
+ const updated = markNotificationsRead({ sessionId: opts.session?.trim() || undefined });
2224
+ if (opts.json) {
2225
+ console.log(JSON.stringify({ ok: true, updated }));
2226
+ return;
2227
+ }
2228
+ console.log(`Marked ${updated} notification${updated === 1 ? "" : "s"} as read.`);
2229
+ });
2230
+ metadataCmd
2231
+ .command("set-status <session> <text>")
2232
+ .option("--tone <tone>", "Status tone", "info")
2233
+ .description("Set a session status pill")
2234
+ .action(async (session, text, opts) => {
2235
+ await initPaths();
2236
+ updateSessionMetadata(session, (current) => ({
2237
+ ...current,
2238
+ status: { text, tone: opts.tone },
2239
+ }));
2240
+ });
2241
+ metadataCmd
2242
+ .command("set-progress <session> <current> <total>")
2243
+ .option("--label <label>", "Progress label")
2244
+ .description("Set per-session progress")
2245
+ .action(async (session, current, total, opts) => {
2246
+ await initPaths();
2247
+ updateSessionMetadata(session, (existing) => ({
2248
+ ...existing,
2249
+ progress: { current: Number(current), total: Number(total), label: opts.label },
2250
+ }));
2251
+ });
2252
+ metadataCmd
2253
+ .command("set-context <session>")
2254
+ .option("--cwd <cwd>", "Working directory")
2255
+ .option("--worktree-path <path>", "Worktree path")
2256
+ .option("--worktree-name <name>", "Worktree name")
2257
+ .option("--branch <branch>", "Git branch")
2258
+ .option("--pr-number <number>", "PR number")
2259
+ .option("--pr-title <title>", "PR title")
2260
+ .option("--pr-url <url>", "PR URL")
2261
+ .description("Set rich session context metadata")
2262
+ .action(async (session, opts) => {
2263
+ await initPaths();
2264
+ const context = {
2265
+ cwd: opts.cwd,
2266
+ worktreePath: opts.worktreePath,
2267
+ worktreeName: opts.worktreeName,
2268
+ branch: opts.branch,
2269
+ pr: opts.prNumber || opts.prTitle || opts.prUrl
2270
+ ? {
2271
+ number: opts.prNumber ? Number(opts.prNumber) : undefined,
2272
+ title: opts.prTitle,
2273
+ url: opts.prUrl,
2274
+ }
2275
+ : undefined,
2276
+ };
2277
+ updateSessionMetadata(session, (existing) => ({
2278
+ ...existing,
2279
+ context: {
2280
+ ...(existing.context ?? {}),
2281
+ ...context,
2282
+ pr: {
2283
+ ...(existing.context?.pr ?? {}),
2284
+ ...(context.pr ?? {}),
2285
+ },
2286
+ },
2287
+ }));
2288
+ });
2289
+ metadataCmd
2290
+ .command("set-services <session>")
2291
+ .requiredOption("--url <url...>", "One or more service URLs")
2292
+ .option("--label <label>", "Shared label for the services")
2293
+ .description("Set detected session services/ports")
2294
+ .action(async (session, opts) => {
2295
+ await initPaths();
2296
+ const services = (opts.url ?? []).map((url) => {
2297
+ const match = url.match(/:(\d+)(?:\/|$)/);
2298
+ return {
2299
+ label: opts.label,
2300
+ url,
2301
+ port: match ? Number(match[1]) : undefined,
2302
+ };
2303
+ });
2304
+ updateSessionMetadata(session, (existing) => ({
2305
+ ...existing,
2306
+ derived: {
2307
+ ...(existing.derived ?? {}),
2308
+ services,
2309
+ },
2310
+ }));
2311
+ });
2312
+ metadataCmd
2313
+ .command("log <session> <message>")
2314
+ .option("--source <source>", "Log source")
2315
+ .option("--tone <tone>", "Log tone")
2316
+ .description("Append a session log line")
2317
+ .action(async (session, message, opts) => {
2318
+ await initPaths();
2319
+ updateSessionMetadata(session, (existing) => ({
2320
+ ...existing,
2321
+ logs: [
2322
+ ...(existing.logs ?? []).slice(-19),
2323
+ { message, source: opts.source, tone: opts.tone, ts: new Date().toISOString() },
2324
+ ],
2325
+ }));
2326
+ });
2327
+ metadataCmd
2328
+ .command("clear-log <session>")
2329
+ .description("Clear session logs")
2330
+ .action(async (session) => {
2331
+ await initPaths();
2332
+ clearSessionLogs(session);
2333
+ });
2334
+ statuslineCmd
2335
+ .command("install")
2336
+ .description("Install aimux statusline into Claude Code")
2337
+ .action(() => {
2338
+ const home = homedir();
2339
+ const aimuxDir = pathJoin(home, ".aimux");
2340
+ const targetScript = pathJoin(aimuxDir, "statusline.sh");
2341
+ // Resolve source script relative to compiled JS location
2342
+ const thisFile = fileURLToPath(import.meta.url);
2343
+ const sourceScript = pathResolve(pathDirname(thisFile), "..", "scripts", "statusline.sh");
2344
+ if (!existsSync(sourceScript)) {
2345
+ console.error(`Source script not found: ${sourceScript}`);
2346
+ process.exit(1);
2347
+ }
2348
+ mkdirSync(aimuxDir, { recursive: true });
2349
+ copyFileSync(sourceScript, targetScript);
2350
+ chmodSync(targetScript, 0o755);
2351
+ console.log(`Copied statusline script to ${targetScript}`);
2352
+ // Update Claude Code settings
2353
+ const claudeDir = pathJoin(home, ".claude");
2354
+ const settingsPath = pathJoin(claudeDir, "settings.json");
2355
+ let settings = {};
2356
+ if (existsSync(settingsPath)) {
2357
+ try {
2358
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
2359
+ }
2360
+ catch { }
2361
+ }
2362
+ const newCommand = `bash ${targetScript}`;
2363
+ const oldCommand = settings.statusLine?.command;
2364
+ if (oldCommand && oldCommand !== newCommand) {
2365
+ const backupPath = pathJoin(aimuxDir, "statusline-previous.txt");
2366
+ writeFileSync(backupPath, oldCommand + "\n");
2367
+ console.log(`Backed up previous statusline command to ${backupPath}`);
2368
+ }
2369
+ settings.statusLine = { type: "command", command: newCommand };
2370
+ mkdirSync(claudeDir, { recursive: true });
2371
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2372
+ console.log(`Updated ${settingsPath} → statusLine points to aimux script`);
2373
+ console.log("Restart Claude Code to see aimux agent status in the toolbar.");
2374
+ });
2375
+ statuslineCmd
2376
+ .command("uninstall")
2377
+ .description("Restore previous Claude Code statusline")
2378
+ .action(() => {
2379
+ const home = homedir();
2380
+ const aimuxDir = pathJoin(home, ".aimux");
2381
+ const settingsPath = pathJoin(home, ".claude", "settings.json");
2382
+ const backupPath = pathJoin(aimuxDir, "statusline-previous.txt");
2383
+ if (!existsSync(settingsPath)) {
2384
+ console.error("No Claude Code settings found.");
2385
+ process.exit(1);
2386
+ }
2387
+ let settings = {};
2388
+ try {
2389
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
2390
+ }
2391
+ catch {
2392
+ console.error("Could not parse settings.json");
2393
+ process.exit(1);
2394
+ }
2395
+ if (existsSync(backupPath)) {
2396
+ const prev = readFileSync(backupPath, "utf-8").trim();
2397
+ settings.statusLine = { type: "command", command: prev };
2398
+ console.log(`Restored previous statusline: ${prev}`);
2399
+ }
2400
+ else {
2401
+ delete settings.statusLine;
2402
+ console.log("Removed aimux statusline (no previous config to restore).");
2403
+ }
2404
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2405
+ console.log("Restart Claude Code for changes to take effect.");
2406
+ });
2407
+ // ── Team commands ──────────────────────────────────────────────────
2408
+ const teamCmd = program.command("team").description("Manage agent team roles");
2409
+ teamCmd
2410
+ .command("show")
2411
+ .description("Show current team config")
2412
+ .action(() => {
2413
+ const config = loadTeamConfig();
2414
+ console.log("Team Roles:");
2415
+ for (const [name, role] of Object.entries(config.roles)) {
2416
+ const flags = [];
2417
+ if (role.reviewedBy)
2418
+ flags.push(`reviewed by: ${role.reviewedBy}`);
2419
+ if (role.canEdit)
2420
+ flags.push("can edit");
2421
+ const flagStr = flags.length > 0 ? ` (${flags.join(", ")})` : "";
2422
+ console.log(` ${name}: ${role.description}${flagStr}`);
2423
+ }
2424
+ console.log(`\nDefault role: ${config.defaultRole}`);
2425
+ });
2426
+ teamCmd
2427
+ .command("add <role>")
2428
+ .description("Add or update a role")
2429
+ .option("-d, --description <desc>", "Role description")
2430
+ .option("--reviewed-by <role>", "Role that reviews this role's work")
2431
+ .option("--can-edit", "Whether this role can edit code directly")
2432
+ .action((role, options) => {
2433
+ const config = loadTeamConfig();
2434
+ config.roles[role] = {
2435
+ description: options.description ?? config.roles[role]?.description ?? `${role} agent`,
2436
+ ...(options.reviewedBy && { reviewedBy: options.reviewedBy }),
2437
+ ...(options.canEdit && { canEdit: true }),
2438
+ };
2439
+ saveTeamConfig(config);
2440
+ console.log(`Role "${role}" saved.`);
2441
+ });
2442
+ teamCmd
2443
+ .command("remove <role>")
2444
+ .description("Remove a role")
2445
+ .action((role) => {
2446
+ const config = loadTeamConfig();
2447
+ if (!config.roles[role]) {
2448
+ console.error(`Role "${role}" not found.`);
2449
+ process.exit(1);
2450
+ }
2451
+ delete config.roles[role];
2452
+ if (config.defaultRole === role) {
2453
+ config.defaultRole = Object.keys(config.roles)[0] ?? "coder";
2454
+ }
2455
+ saveTeamConfig(config);
2456
+ console.log(`Role "${role}" removed.`);
2457
+ });
2458
+ teamCmd
2459
+ .command("default <role>")
2460
+ .description("Set the default role for new agents")
2461
+ .action((role) => {
2462
+ const config = loadTeamConfig();
2463
+ if (!config.roles[role]) {
2464
+ console.error(`Role "${role}" not found. Add it first with: aimux team add ${role}`);
2465
+ process.exit(1);
2466
+ }
2467
+ config.defaultRole = role;
2468
+ saveTeamConfig(config);
2469
+ console.log(`Default role set to "${role}".`);
2470
+ });
2471
+ teamCmd
2472
+ .command("init")
2473
+ .description("Initialize project with default team structure")
2474
+ .action(() => {
2475
+ const config = getDefaultTeamConfig();
2476
+ saveTeamConfig(config);
2477
+ console.log("Team config initialized with default roles:");
2478
+ for (const [name, role] of Object.entries(config.roles)) {
2479
+ console.log(` ${name}: ${role.description}`);
2480
+ }
2481
+ });
2482
+ program.parse();
2483
+ //# sourceMappingURL=main.js.map