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.
- package/README.md +743 -0
- package/bin/aimux +2 -0
- package/dist/agent-events.d.ts +20 -0
- package/dist/agent-events.js +2 -0
- package/dist/agent-events.js.map +1 -0
- package/dist/agent-message-parts.d.ts +17 -0
- package/dist/agent-message-parts.js +31 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-output-parser.d.ts +16 -0
- package/dist/agent-output-parser.js +229 -0
- package/dist/agent-output-parser.js.map +1 -0
- package/dist/agent-tracker.d.ts +9 -0
- package/dist/agent-tracker.js +144 -0
- package/dist/agent-tracker.js.map +1 -0
- package/dist/agent-watcher.d.ts +15 -0
- package/dist/agent-watcher.js +2 -0
- package/dist/agent-watcher.js.map +1 -0
- package/dist/attachment-store.d.ts +35 -0
- package/dist/attachment-store.js +129 -0
- package/dist/attachment-store.js.map +1 -0
- package/dist/builtin-metadata-watchers.d.ts +2 -0
- package/dist/builtin-metadata-watchers.js +275 -0
- package/dist/builtin-metadata-watchers.js.map +1 -0
- package/dist/claude-hooks.d.ts +29 -0
- package/dist/claude-hooks.js +106 -0
- package/dist/claude-hooks.js.map +1 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +172 -0
- package/dist/config.js.map +1 -0
- package/dist/context/compactor.d.ts +20 -0
- package/dist/context/compactor.js +212 -0
- package/dist/context/compactor.js.map +1 -0
- package/dist/context/context-bridge.d.ts +67 -0
- package/dist/context/context-bridge.js +471 -0
- package/dist/context/context-bridge.js.map +1 -0
- package/dist/context/context-file.d.ts +11 -0
- package/dist/context/context-file.js +93 -0
- package/dist/context/context-file.js.map +1 -0
- package/dist/context/history.d.ts +40 -0
- package/dist/context/history.js +108 -0
- package/dist/context/history.js.map +1 -0
- package/dist/daemon.d.ts +39 -0
- package/dist/daemon.js +344 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dashboard-session-registry.d.ts +47 -0
- package/dist/dashboard-session-registry.js +161 -0
- package/dist/dashboard-session-registry.js.map +1 -0
- package/dist/dashboard-state.d.ts +18 -0
- package/dist/dashboard-state.js +26 -0
- package/dist/dashboard-state.js.map +1 -0
- package/dist/dashboard.d.ts +118 -0
- package/dist/dashboard.js +91 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/debug.d.ts +7 -0
- package/dist/debug.js +41 -0
- package/dist/debug.js.map +1 -0
- package/dist/fast-control.d.ts +45 -0
- package/dist/fast-control.js +174 -0
- package/dist/fast-control.js.map +1 -0
- package/dist/hotkeys.d.ts +44 -0
- package/dist/hotkeys.js +118 -0
- package/dist/hotkeys.js.map +1 -0
- package/dist/http-client.d.ts +10 -0
- package/dist/http-client.js +54 -0
- package/dist/http-client.js.map +1 -0
- package/dist/instance-directory.d.ts +32 -0
- package/dist/instance-directory.js +82 -0
- package/dist/instance-directory.js.map +1 -0
- package/dist/instance-registry.d.ts +38 -0
- package/dist/instance-registry.js +208 -0
- package/dist/instance-registry.js.map +1 -0
- package/dist/key-parser.d.ts +30 -0
- package/dist/key-parser.js +272 -0
- package/dist/key-parser.js.map +1 -0
- package/dist/last-used.d.ts +31 -0
- package/dist/last-used.js +93 -0
- package/dist/last-used.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +2483 -0
- package/dist/main.js.map +1 -0
- package/dist/metadata-server.d.ts +268 -0
- package/dist/metadata-server.js +1379 -0
- package/dist/metadata-server.js.map +1 -0
- package/dist/metadata-store.d.ts +80 -0
- package/dist/metadata-store.js +87 -0
- package/dist/metadata-store.js.map +1 -0
- package/dist/multiplexer.d.ts +471 -0
- package/dist/multiplexer.js +5714 -0
- package/dist/multiplexer.js.map +1 -0
- package/dist/notification-context.d.ts +18 -0
- package/dist/notification-context.js +68 -0
- package/dist/notification-context.js.map +1 -0
- package/dist/notifications.d.ts +38 -0
- package/dist/notifications.js +111 -0
- package/dist/notifications.js.map +1 -0
- package/dist/notify.d.ts +10 -0
- package/dist/notify.js +62 -0
- package/dist/notify.js.map +1 -0
- package/dist/orchestration-actions.d.ts +76 -0
- package/dist/orchestration-actions.js +310 -0
- package/dist/orchestration-actions.js.map +1 -0
- package/dist/orchestration-dispatcher.d.ts +22 -0
- package/dist/orchestration-dispatcher.js +49 -0
- package/dist/orchestration-dispatcher.js.map +1 -0
- package/dist/orchestration-routing.d.ts +20 -0
- package/dist/orchestration-routing.js +78 -0
- package/dist/orchestration-routing.js.map +1 -0
- package/dist/orchestration.d.ts +26 -0
- package/dist/orchestration.js +110 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/osc-notifications.d.ts +15 -0
- package/dist/osc-notifications.js +180 -0
- package/dist/osc-notifications.js.map +1 -0
- package/dist/paths.d.ts +55 -0
- package/dist/paths.js +259 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin-runtime.d.ts +46 -0
- package/dist/plugin-runtime.js +180 -0
- package/dist/plugin-runtime.js.map +1 -0
- package/dist/project-events.d.ts +36 -0
- package/dist/project-events.js +63 -0
- package/dist/project-events.js.map +1 -0
- package/dist/project-scanner.d.ts +38 -0
- package/dist/project-scanner.js +243 -0
- package/dist/project-scanner.js.map +1 -0
- package/dist/project-service-manifest.d.ts +18 -0
- package/dist/project-service-manifest.js +56 -0
- package/dist/project-service-manifest.js.map +1 -0
- package/dist/recency.d.ts +2 -0
- package/dist/recency.js +34 -0
- package/dist/recency.js.map +1 -0
- package/dist/recorder.d.ts +14 -0
- package/dist/recorder.js +130 -0
- package/dist/recorder.js.map +1 -0
- package/dist/session-bootstrap.d.ts +45 -0
- package/dist/session-bootstrap.js +436 -0
- package/dist/session-bootstrap.js.map +1 -0
- package/dist/session-message-history.d.ts +27 -0
- package/dist/session-message-history.js +105 -0
- package/dist/session-message-history.js.map +1 -0
- package/dist/session-runtime.d.ts +44 -0
- package/dist/session-runtime.js +56 -0
- package/dist/session-runtime.js.map +1 -0
- package/dist/session-semantics.d.ts +35 -0
- package/dist/session-semantics.js +110 -0
- package/dist/session-semantics.js.map +1 -0
- package/dist/status-detector.d.ts +17 -0
- package/dist/status-detector.js +67 -0
- package/dist/status-detector.js.map +1 -0
- package/dist/statusline-model.d.ts +103 -0
- package/dist/statusline-model.js +177 -0
- package/dist/statusline-model.js.map +1 -0
- package/dist/task-dispatcher.d.ts +63 -0
- package/dist/task-dispatcher.js +210 -0
- package/dist/task-dispatcher.js.map +1 -0
- package/dist/task-workflow.d.ts +13 -0
- package/dist/task-workflow.js +153 -0
- package/dist/task-workflow.js.map +1 -0
- package/dist/tasks.d.ts +60 -0
- package/dist/tasks.js +120 -0
- package/dist/tasks.js.map +1 -0
- package/dist/team.d.ts +28 -0
- package/dist/team.js +91 -0
- package/dist/team.js.map +1 -0
- package/dist/terminal-host.d.ts +10 -0
- package/dist/terminal-host.js +52 -0
- package/dist/terminal-host.js.map +1 -0
- package/dist/threads.d.ts +61 -0
- package/dist/threads.js +200 -0
- package/dist/threads.js.map +1 -0
- package/dist/tmux-doctor.d.ts +47 -0
- package/dist/tmux-doctor.js +112 -0
- package/dist/tmux-doctor.js.map +1 -0
- package/dist/tmux-runtime-manager.d.ts +164 -0
- package/dist/tmux-runtime-manager.js +794 -0
- package/dist/tmux-runtime-manager.js.map +1 -0
- package/dist/tmux-session-transport.d.ts +31 -0
- package/dist/tmux-session-transport.js +115 -0
- package/dist/tmux-session-transport.js.map +1 -0
- package/dist/tmux-statusline.d.ts +17 -0
- package/dist/tmux-statusline.js +166 -0
- package/dist/tmux-statusline.js.map +1 -0
- package/dist/tool-output-watchers.d.ts +10 -0
- package/dist/tool-output-watchers.js +190 -0
- package/dist/tool-output-watchers.js.map +1 -0
- package/dist/tui/render/box.d.ts +1 -0
- package/dist/tui/render/box.js +20 -0
- package/dist/tui/render/box.js.map +1 -0
- package/dist/tui/render/text.d.ts +8 -0
- package/dist/tui/render/text.js +92 -0
- package/dist/tui/render/text.js.map +1 -0
- package/dist/tui/screens/dashboard-renderers.d.ts +23 -0
- package/dist/tui/screens/dashboard-renderers.js +411 -0
- package/dist/tui/screens/dashboard-renderers.js.map +1 -0
- package/dist/tui/screens/overlay-renderers.d.ts +10 -0
- package/dist/tui/screens/overlay-renderers.js +274 -0
- package/dist/tui/screens/overlay-renderers.js.map +1 -0
- package/dist/tui/screens/subscreen-renderers.d.ts +9 -0
- package/dist/tui/screens/subscreen-renderers.js +327 -0
- package/dist/tui/screens/subscreen-renderers.js.map +1 -0
- package/dist/workflow.d.ts +19 -0
- package/dist/workflow.js +111 -0
- package/dist/workflow.js.map +1 -0
- package/dist/worktree.d.ts +23 -0
- package/dist/worktree.js +101 -0
- package/dist/worktree.js.map +1 -0
- 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
|