@minasoft/mina-ai-router 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -16
- package/dist/apps/cli/src/index.js +1251 -46
- package/dist/apps/http-server/src/index.js +559 -43
- package/dist/apps/http-server/src/public/assets/index-Bl059Jd0.js +9 -0
- package/dist/apps/http-server/src/public/assets/index-CaPxN_Ez.css +1 -0
- package/dist/apps/http-server/src/public/index.html +2 -2
- package/dist/apps/mcp-server/src/index.js +55 -9
- package/dist/packages/core/src/capability-profile.js +145 -0
- package/dist/packages/core/src/index.js +4 -0
- package/dist/packages/core/src/mcp-preflight.js +80 -0
- package/dist/packages/core/src/registry.js +128 -3
- package/dist/packages/core/src/request-store.js +158 -0
- package/dist/packages/core/src/response-parser.js +76 -8
- package/dist/packages/core/src/router.js +408 -13
- package/dist/packages/core/src/runtime-paths.js +16 -0
- package/dist/packages/core/src/version.js +57 -0
- package/dist/packages/mcp/src/provider.js +57 -6
- package/dist/packages/transports/src/headless/headless-transport.js +13 -8
- package/dist/packages/transports/src/tmux/tmux-client.js +334 -0
- package/dist/packages/transports/src/tmux/tmux-transport.js +10 -0
- package/docs/DEVELOPER-START-GUIDE.md +9 -1
- package/docs/GETTING-STARTED.md +10 -5
- package/docs/HTTP-UI-MCP.md +39 -13
- package/docs/MCP-CLIENT-SETUP.md +56 -3
- package/docs/SKILL-INSTALL-GUIDE.md +21 -3
- package/docs/TROUBLESHOOTING.md +51 -2
- package/docs/USER-START-GUIDE.md +157 -26
- package/docs/assets/mina-ai-router-overview.svg +109 -0
- package/package.json +8 -2
- package/dist/apps/http-server/src/public/assets/index-Be0tne90.js +0 -9
- package/dist/apps/http-server/src/public/assets/index-CEhd8YGG.css +0 -1
|
@@ -6,15 +6,20 @@ const node_fs_1 = require("node:fs");
|
|
|
6
6
|
const node_child_process_1 = require("node:child_process");
|
|
7
7
|
const src_1 = require("../../../packages/core/src");
|
|
8
8
|
const src_2 = require("../../../packages/transports/src");
|
|
9
|
-
const statePath = process.env.MINA_ROUTER_STATE ?? (0,
|
|
10
|
-
const version =
|
|
11
|
-
const serverPidPath = process.env.MINA_SERVER_PID ?? (0,
|
|
9
|
+
const statePath = process.env.MINA_ROUTER_STATE ?? (0, src_1.defaultRouterStatePath)();
|
|
10
|
+
const version = (0, src_1.packageVersion)();
|
|
11
|
+
const serverPidPath = process.env.MINA_SERVER_PID ?? (0, src_1.defaultServerPidPath)();
|
|
12
|
+
const agentStaleAfterMs = Number(process.env.MINA_AGENT_STALE_AFTER_MS ?? 15 * 60 * 1000);
|
|
12
13
|
async function main(argv) {
|
|
13
14
|
const command = argv[2];
|
|
14
15
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
15
16
|
printHelp();
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
19
|
+
if (hasHelpFlag(argv.slice(3))) {
|
|
20
|
+
printCommandHelp(command);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
18
23
|
const context = createContext();
|
|
19
24
|
switch (command) {
|
|
20
25
|
case "version":
|
|
@@ -28,23 +33,29 @@ async function main(argv) {
|
|
|
28
33
|
case "verify":
|
|
29
34
|
runVerify();
|
|
30
35
|
break;
|
|
36
|
+
case "doctor":
|
|
37
|
+
await runDoctor(argv.slice(3), context);
|
|
38
|
+
break;
|
|
39
|
+
case "setup":
|
|
40
|
+
runSetup(argv.slice(3));
|
|
41
|
+
break;
|
|
31
42
|
case "server":
|
|
32
|
-
handleServerCommand(argv.slice(3));
|
|
43
|
+
await handleServerCommand(argv.slice(3));
|
|
33
44
|
break;
|
|
34
45
|
case "codex":
|
|
35
|
-
startVisibleAgent("codex", "codex --no-alt-screen", argv.slice(3), context);
|
|
46
|
+
await startVisibleAgent("codex", "codex --no-alt-screen", argv.slice(3), context);
|
|
36
47
|
break;
|
|
37
48
|
case "claude":
|
|
38
|
-
startVisibleAgent("claude", "claude", argv.slice(3), context);
|
|
49
|
+
await startVisibleAgent("claude", "claude", argv.slice(3), context);
|
|
39
50
|
break;
|
|
40
51
|
case "register":
|
|
41
|
-
registerAgent(argv.slice(3), context);
|
|
52
|
+
await registerAgent(argv.slice(3), context);
|
|
42
53
|
break;
|
|
43
54
|
case "agents":
|
|
44
55
|
await listAgents(context);
|
|
45
56
|
break;
|
|
46
57
|
case "agent":
|
|
47
|
-
await
|
|
58
|
+
await handleAgent(argv.slice(3), context);
|
|
48
59
|
break;
|
|
49
60
|
case "attach":
|
|
50
61
|
showAttach(argv.slice(3), context);
|
|
@@ -62,18 +73,22 @@ async function main(argv) {
|
|
|
62
73
|
listRequests(argv.slice(3), context);
|
|
63
74
|
break;
|
|
64
75
|
case "request":
|
|
65
|
-
|
|
76
|
+
await handleRequest(argv.slice(3), context);
|
|
66
77
|
break;
|
|
67
78
|
default:
|
|
68
79
|
throw new Error(`Unknown command "${command}". Run "mair help".`);
|
|
69
80
|
}
|
|
70
81
|
}
|
|
71
|
-
function handleServerCommand(args) {
|
|
82
|
+
async function handleServerCommand(args) {
|
|
83
|
+
if (hasHelpFlag(args)) {
|
|
84
|
+
printCommandHelp("server");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
72
87
|
const action = args[0] ?? "status";
|
|
73
88
|
const flags = parseFlags(args.slice(1));
|
|
74
89
|
switch (action) {
|
|
75
90
|
case "start":
|
|
76
|
-
startServer(flags);
|
|
91
|
+
await startServer(flags);
|
|
77
92
|
break;
|
|
78
93
|
case "stop":
|
|
79
94
|
stopServer();
|
|
@@ -85,7 +100,7 @@ function handleServerCommand(args) {
|
|
|
85
100
|
throw new Error("Usage: mair server <start|stop|status> [--port 3333] [--host 127.0.0.1]");
|
|
86
101
|
}
|
|
87
102
|
}
|
|
88
|
-
function startServer(flags) {
|
|
103
|
+
async function startServer(flags) {
|
|
89
104
|
const current = serverStatus();
|
|
90
105
|
if (current.running) {
|
|
91
106
|
printJson(current);
|
|
@@ -95,10 +110,17 @@ function startServer(flags) {
|
|
|
95
110
|
const host = flags.host ?? process.env.MINA_HTTP_HOST ?? "127.0.0.1";
|
|
96
111
|
const serverPath = (0, node_path_1.join)(__dirname, "../../http-server/src/index.js");
|
|
97
112
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(serverPidPath), { recursive: true });
|
|
113
|
+
try {
|
|
114
|
+
(0, node_fs_1.unlinkSync)(serverPidPath);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// stale pid file may not exist
|
|
118
|
+
}
|
|
98
119
|
const logPath = flags.log ?? (0, node_path_1.join)((0, node_path_1.dirname)(serverPidPath), "mair-server.log");
|
|
120
|
+
const logFd = (0, node_fs_1.openSync)(logPath, "a");
|
|
99
121
|
const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
|
|
100
122
|
detached: true,
|
|
101
|
-
stdio: "ignore",
|
|
123
|
+
stdio: ["ignore", logFd, logFd],
|
|
102
124
|
env: {
|
|
103
125
|
...process.env,
|
|
104
126
|
PORT: port,
|
|
@@ -106,12 +128,29 @@ function startServer(flags) {
|
|
|
106
128
|
MINA_ROUTER_STATE: process.env.MINA_ROUTER_STATE ?? statePath,
|
|
107
129
|
},
|
|
108
130
|
});
|
|
109
|
-
|
|
131
|
+
(0, node_fs_1.closeSync)(logFd);
|
|
110
132
|
const pid = child.pid;
|
|
111
133
|
if (!pid) {
|
|
112
134
|
throw new Error("Failed to start Mina HTTP server.");
|
|
113
135
|
}
|
|
114
|
-
|
|
136
|
+
let childExit;
|
|
137
|
+
child.on("exit", (code) => {
|
|
138
|
+
childExit = { code, signal: null };
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
await waitForServerReadiness({ host, port, expectedStatePath: process.env.MINA_ROUTER_STATE ?? statePath, logPath, pid, getChildExit: () => childExit });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (isProcessRunning(pid)) {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, "SIGTERM");
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// child may already be gone
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
115
154
|
(0, node_fs_1.writeFileSync)(serverPidPath, `${JSON.stringify({
|
|
116
155
|
pid,
|
|
117
156
|
port,
|
|
@@ -122,8 +161,67 @@ function startServer(flags) {
|
|
|
122
161
|
mcpUrl: `http://${host}:${port}/mcp`,
|
|
123
162
|
startedAt: new Date().toISOString(),
|
|
124
163
|
}, null, 2)}\n`);
|
|
164
|
+
child.unref();
|
|
125
165
|
printJson(serverStatus());
|
|
126
166
|
}
|
|
167
|
+
async function waitForServerReadiness(options) {
|
|
168
|
+
const url = `http://${options.host}:${options.port}/api/health`;
|
|
169
|
+
const deadline = Date.now() + 5000;
|
|
170
|
+
let lastError = "";
|
|
171
|
+
while (Date.now() < deadline) {
|
|
172
|
+
const childExit = options.getChildExit();
|
|
173
|
+
if (childExit || !isProcessRunning(options.pid)) {
|
|
174
|
+
throw new Error(serverStartFailureMessage({
|
|
175
|
+
url,
|
|
176
|
+
logPath: options.logPath,
|
|
177
|
+
cause: childExit
|
|
178
|
+
? `server process exited before readiness (code=${childExit.code ?? "null"}, signal=${childExit.signal ?? "null"})`
|
|
179
|
+
: "server process exited before readiness",
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(url);
|
|
184
|
+
const text = await response.text();
|
|
185
|
+
const parsed = safeJson(text);
|
|
186
|
+
if (response.ok && parsed && typeof parsed.statePath === "string") {
|
|
187
|
+
if (resolvePath(parsed.statePath) !== resolvePath(options.expectedStatePath)) {
|
|
188
|
+
lastError = `readiness endpoint responded from a Mina server for a different state file: ${parsed.statePath}`;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
lastError = parsed
|
|
196
|
+
? `readiness endpoint returned HTTP ${response.status} without Mina health state`
|
|
197
|
+
: `readiness endpoint did not return Mina JSON: ${truncateText(text, 160)}`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
202
|
+
}
|
|
203
|
+
await delay(100);
|
|
204
|
+
}
|
|
205
|
+
throw new Error(serverStartFailureMessage({
|
|
206
|
+
url,
|
|
207
|
+
logPath: options.logPath,
|
|
208
|
+
cause: `timed out waiting for Mina readiness${lastError ? `: ${lastError}` : ""}`,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
function serverStartFailureMessage(input) {
|
|
212
|
+
const logExcerpt = readLogExcerpt(input.logPath);
|
|
213
|
+
return [
|
|
214
|
+
`Mina HTTP server failed to become ready at ${input.url}: ${input.cause}.`,
|
|
215
|
+
`Log: ${input.logPath}`,
|
|
216
|
+
logExcerpt ? `Recent log:\n${logExcerpt}` : "Recent log: <empty>",
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
219
|
+
function readLogExcerpt(logPath) {
|
|
220
|
+
if (!(0, node_fs_1.existsSync)(logPath)) {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
return (0, node_fs_1.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean).slice(-12).join("\n");
|
|
224
|
+
}
|
|
127
225
|
function stopServer() {
|
|
128
226
|
const status = serverStatus();
|
|
129
227
|
if (!status.pid) {
|
|
@@ -165,7 +263,7 @@ function isProcessRunning(pid) {
|
|
|
165
263
|
return false;
|
|
166
264
|
}
|
|
167
265
|
}
|
|
168
|
-
function startVisibleAgent(agentType, defaultCommand, args, context) {
|
|
266
|
+
async function startVisibleAgent(agentType, defaultCommand, args, context) {
|
|
169
267
|
assertCommandAvailable("tmux");
|
|
170
268
|
const flags = parseFlags(args);
|
|
171
269
|
const root = flags.root ?? process.cwd();
|
|
@@ -173,39 +271,123 @@ function startVisibleAgent(agentType, defaultCommand, args, context) {
|
|
|
173
271
|
const id = flags.id ?? sanitizeName(projectName);
|
|
174
272
|
const sessionId = flags.session ?? `${agentType}-${sanitizeName(projectName)}`;
|
|
175
273
|
const startupCommand = flags.command ?? defaultCommand;
|
|
274
|
+
const permissionProfile = resolvePermissionProfile(agentType, flags["permission-profile"] ?? "default", root);
|
|
275
|
+
const mcpUrl = matchingLiveServerStatus()?.mcpUrl
|
|
276
|
+
?? `http://${process.env.MINA_HTTP_HOST ?? "127.0.0.1"}:${process.env.MINA_HTTP_PORT ?? "3333"}/mcp`;
|
|
277
|
+
const mcpName = flags["mcp-name"] ?? "mina-ai-router";
|
|
278
|
+
const explicitConfiguredUrl = flags["mcp-configured-url"];
|
|
279
|
+
const detectedConfiguredUrl = explicitConfiguredUrl
|
|
280
|
+
?? (flags["mcp-configured"] === "true" ? undefined : detectClientMcpConfiguredUrl(agentType, mcpName, mcpUrl, root));
|
|
281
|
+
const mcpPreflight = (0, src_1.buildMcpPreflight)({
|
|
282
|
+
agentType,
|
|
283
|
+
mcpUrl,
|
|
284
|
+
mcpName,
|
|
285
|
+
configured: flags["mcp-configured"] === "true",
|
|
286
|
+
configuredUrl: detectedConfiguredUrl,
|
|
287
|
+
});
|
|
176
288
|
assertCommandAvailable(startupCommand.split(/\s+/)[0]);
|
|
177
289
|
const shouldAttach = flags.attach !== "false" && flags["no-attach"] !== "true";
|
|
178
290
|
const shouldPromptRegister = flags.register !== "false" && flags["no-register"] !== "true";
|
|
179
291
|
const registerDelayMs = flags["register-delay-ms"] ? Number(flags["register-delay-ms"]) : 4000;
|
|
292
|
+
const existing = context.registry.get(id) ?? context.registry.findBySessionFingerprint(sessionId);
|
|
293
|
+
const registrationAttemptAt = new Date().toISOString();
|
|
180
294
|
const agent = {
|
|
181
295
|
id,
|
|
182
296
|
name: id,
|
|
183
297
|
agentType,
|
|
184
298
|
transport: "tmux",
|
|
185
299
|
sessionId,
|
|
300
|
+
sessionFingerprint: sessionId,
|
|
186
301
|
projectRoot: root,
|
|
187
302
|
startupCommand,
|
|
303
|
+
bootstrapStatus: mcpPreflight.canSendSelfRegistrationPrompt ? "created" : "mcp-configuring",
|
|
304
|
+
registrationSource: "cli",
|
|
305
|
+
registrationStatus: existing?.registrationStatus === "confirmed" ? "confirmed" : "pending",
|
|
306
|
+
lastRegistrationAttemptAt: registrationAttemptAt,
|
|
307
|
+
permissionProfile: permissionProfile.permissionProfile,
|
|
308
|
+
permissionProfileStatus: permissionProfile.permissionProfileStatus,
|
|
309
|
+
permissionProfileDetail: permissionProfile.permissionProfileDetail,
|
|
310
|
+
mcpPreflightStatus: mcpPreflight.mcpPreflightStatus,
|
|
311
|
+
mcpPreflightDetail: mcpPreflight.mcpPreflightDetail,
|
|
312
|
+
mcpSetupCommand: mcpPreflight.mcpSetupCommand,
|
|
313
|
+
mcpVerifyCommand: mcpPreflight.mcpVerifyCommand,
|
|
314
|
+
mcpRemoveCommand: mcpPreflight.mcpRemoveCommand,
|
|
315
|
+
mcpUrl: mcpPreflight.mcpUrl,
|
|
188
316
|
};
|
|
189
317
|
const tmux = new src_2.TmuxClient();
|
|
190
318
|
const existed = tmux.hasSession(sessionId);
|
|
191
319
|
tmux.ensureSession(agent);
|
|
192
|
-
|
|
320
|
+
let registeredAgent = await registerThroughLiveOwner(agent, context);
|
|
321
|
+
let registration = "registration prompt skipped";
|
|
322
|
+
let nextAction;
|
|
323
|
+
if (shouldPromptRegister && existing?.registrationStatus !== "confirmed") {
|
|
193
324
|
sleep(registerDelayMs);
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
|
|
325
|
+
const bootstrapPrompt = (0, src_2.detectAgentBootstrapPrompt)(agent, tmux.capture(sessionId));
|
|
326
|
+
if (bootstrapPrompt) {
|
|
327
|
+
registration = bootstrapPrompt.kind === "client-update"
|
|
328
|
+
? "waiting for client update choice"
|
|
329
|
+
: "waiting for permission approval";
|
|
330
|
+
nextAction = bootstrapPrompt.action;
|
|
331
|
+
registeredAgent = await registerThroughLiveOwner({
|
|
332
|
+
...registeredAgent,
|
|
333
|
+
bootstrapStatus: bootstrapPrompt.kind === "client-update" ? "client-update-required" : "permission-required",
|
|
334
|
+
}, context);
|
|
335
|
+
}
|
|
336
|
+
else if (!mcpPreflight.canSendSelfRegistrationPrompt) {
|
|
337
|
+
registration = "waiting for MCP setup";
|
|
338
|
+
nextAction = mcpPreflight.nextAction;
|
|
339
|
+
registeredAgent = await registerThroughLiveOwner({
|
|
340
|
+
...registeredAgent,
|
|
341
|
+
bootstrapStatus: "mcp-configuring",
|
|
342
|
+
}, context);
|
|
197
343
|
}
|
|
198
344
|
else {
|
|
199
|
-
|
|
345
|
+
const prompt = buildSelfRegistrationPrompt(agent);
|
|
346
|
+
try {
|
|
347
|
+
if (agentType === "codex") {
|
|
348
|
+
tmux.sendCodexText(sessionId, prompt);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
tmux.sendText(sessionId, prompt);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
356
|
+
registration = "registration prompt failed";
|
|
357
|
+
nextAction = `Resolve the terminal/session blocker for ${sessionId}, then retry agent registration. ${detail}`;
|
|
358
|
+
registeredAgent = await registerThroughLiveOwner({
|
|
359
|
+
...registeredAgent,
|
|
360
|
+
bootstrapStatus: "failed",
|
|
361
|
+
registrationStatus: "failed",
|
|
362
|
+
mcpPreflightDetail: detail,
|
|
363
|
+
}, context);
|
|
364
|
+
printJson({
|
|
365
|
+
agent: registeredAgent,
|
|
366
|
+
existed,
|
|
367
|
+
attach: `tmux attach -t ${sessionId}`,
|
|
368
|
+
registration,
|
|
369
|
+
nextAction,
|
|
370
|
+
mcpPreflight,
|
|
371
|
+
permissionProfile,
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
registration = "registration prompt sent to agent";
|
|
376
|
+
registeredAgent = await registerThroughLiveOwner({
|
|
377
|
+
...registeredAgent,
|
|
378
|
+
bootstrapStatus: "registration-pending",
|
|
379
|
+
registrationStatus: "pending",
|
|
380
|
+
}, context);
|
|
200
381
|
}
|
|
201
382
|
}
|
|
202
383
|
const summary = {
|
|
203
|
-
agent,
|
|
384
|
+
agent: registeredAgent,
|
|
204
385
|
existed,
|
|
205
386
|
attach: `tmux attach -t ${sessionId}`,
|
|
206
|
-
registration
|
|
207
|
-
|
|
208
|
-
|
|
387
|
+
registration,
|
|
388
|
+
nextAction,
|
|
389
|
+
permissionProfile,
|
|
390
|
+
mcpPreflight,
|
|
209
391
|
};
|
|
210
392
|
if (!shouldAttach) {
|
|
211
393
|
printJson(summary);
|
|
@@ -216,6 +398,20 @@ function startVisibleAgent(agentType, defaultCommand, args, context) {
|
|
|
216
398
|
stdio: "inherit",
|
|
217
399
|
});
|
|
218
400
|
}
|
|
401
|
+
function resolvePermissionProfile(agentType, requestedProfile, projectRoot) {
|
|
402
|
+
if (requestedProfile !== "direct-workspace-read") {
|
|
403
|
+
return {
|
|
404
|
+
permissionProfile: "default",
|
|
405
|
+
permissionProfileStatus: "not-requested",
|
|
406
|
+
permissionProfileDetail: "Default CLI startup. Mina will surface permission prompts instead of hiding them.",
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
permissionProfile: "direct-workspace-read",
|
|
411
|
+
permissionProfileStatus: "unsupported",
|
|
412
|
+
permissionProfileDetail: `No known ${agentType} startup flag in Mina is both direct-read and scoped only to ${projectRoot}. Mina will start the default command and surface permission prompts.`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
219
415
|
function buildSelfRegistrationPrompt(agent) {
|
|
220
416
|
return [
|
|
221
417
|
"Use Mina AI Router MCP register_agent to register this visible CLI session.",
|
|
@@ -225,23 +421,43 @@ function buildSelfRegistrationPrompt(agent) {
|
|
|
225
421
|
`- agentType: ${agent.agentType}`,
|
|
226
422
|
`- transport: ${agent.transport}`,
|
|
227
423
|
`- sessionId: ${agent.sessionId}`,
|
|
424
|
+
`- sessionFingerprint: ${agent.sessionFingerprint ?? agent.sessionId}`,
|
|
228
425
|
`- projectRoot: ${agent.projectRoot}`,
|
|
229
426
|
`- startupCommand: ${agent.startupCommand ?? ""}`,
|
|
230
427
|
"",
|
|
428
|
+
"If Mina already created this agent record, confirm and update that existing id. Do not create a new id for the same session.",
|
|
429
|
+
"",
|
|
231
430
|
"Before registering, create a concise capability notice for this session:",
|
|
232
431
|
"- Prefer capability docs in this order when present: CLAUDE.md/claude.md, AGENTS.md/agents.md, agent.md, README.md.",
|
|
233
432
|
"- If those files are missing, inspect package metadata and the project file tree to infer what this project/agent can help with.",
|
|
234
433
|
"- Set register_agent capabilitySummary to 2-5 short bullets or one short paragraph under 800 characters.",
|
|
235
434
|
"- Set register_agent capabilitySources to a comma-separated list of the files or project signals you used.",
|
|
435
|
+
"- Set register_agent sessionFingerprint to the value above.",
|
|
236
436
|
"After registering, call list_agents and confirm this agent is available.",
|
|
237
437
|
].join("\n");
|
|
238
438
|
}
|
|
239
439
|
async function showHealth(context) {
|
|
440
|
+
const liveHealth = await getFromMatchingLiveServer("/api/health");
|
|
441
|
+
if (liveHealth) {
|
|
442
|
+
printJson({
|
|
443
|
+
ok: liveHealth.ok,
|
|
444
|
+
version,
|
|
445
|
+
statePath: liveHealth.statePath,
|
|
446
|
+
tmuxAvailable: new src_2.TmuxClient().isAvailable(),
|
|
447
|
+
agents: liveHealth.agents,
|
|
448
|
+
requests: liveHealth.requests,
|
|
449
|
+
mcp: {
|
|
450
|
+
httpUrl: liveHealth.mcpUrl,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
240
455
|
const agents = await context.router.listAgentStatuses();
|
|
241
456
|
const requests = context.router.listRequests();
|
|
242
457
|
const openRequests = requests.filter((request) => ["created", "sent", "waiting"].includes(request.status));
|
|
458
|
+
const matchingServer = matchingLiveServerStatus();
|
|
243
459
|
printJson({
|
|
244
|
-
ok: agents.every((agent) =>
|
|
460
|
+
ok: agents.every((agent) => !["missing", "stale", "needs-attention"].includes(agent.status)),
|
|
245
461
|
version,
|
|
246
462
|
statePath,
|
|
247
463
|
tmuxAvailable: new src_2.TmuxClient().isAvailable(),
|
|
@@ -249,7 +465,9 @@ async function showHealth(context) {
|
|
|
249
465
|
total: agents.length,
|
|
250
466
|
available: agents.filter((agent) => agent.status === "available").length,
|
|
251
467
|
busy: agents.filter((agent) => agent.status === "busy").length,
|
|
468
|
+
stale: agents.filter((agent) => agent.status === "stale").length,
|
|
252
469
|
missing: agents.filter((agent) => agent.status === "missing").length,
|
|
470
|
+
needsAttention: agents.filter((agent) => agent.status === "needs-attention").length,
|
|
253
471
|
unknown: agents.filter((agent) => agent.status === "unknown").length,
|
|
254
472
|
},
|
|
255
473
|
requests: {
|
|
@@ -260,16 +478,256 @@ async function showHealth(context) {
|
|
|
260
478
|
archived: requests.filter((request) => request.status === "archived").length,
|
|
261
479
|
},
|
|
262
480
|
mcp: {
|
|
263
|
-
httpUrl:
|
|
481
|
+
httpUrl: matchingServer?.mcpUrl
|
|
482
|
+
?? `http://${process.env.MINA_HTTP_HOST ?? "127.0.0.1"}:${process.env.MINA_HTTP_PORT ?? "3333"}/mcp`,
|
|
264
483
|
},
|
|
265
484
|
});
|
|
266
485
|
}
|
|
267
486
|
function runVerify() {
|
|
268
|
-
(0,
|
|
269
|
-
|
|
270
|
-
|
|
487
|
+
const root = (0, src_1.packageRoot)();
|
|
488
|
+
if (!root) {
|
|
489
|
+
printJson({
|
|
490
|
+
ok: false,
|
|
491
|
+
error: "Could not locate @minasoft/mina-ai-router package root.",
|
|
492
|
+
});
|
|
493
|
+
process.exitCode = 1;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const checkoutVerifyScript = (0, node_path_1.join)(root, "scripts", "core-tests.js");
|
|
497
|
+
if ((0, node_fs_1.existsSync)(checkoutVerifyScript)) {
|
|
498
|
+
(0, node_child_process_1.execFileSync)("npm", ["run", "verify"], {
|
|
499
|
+
cwd: root,
|
|
500
|
+
encoding: "utf8",
|
|
501
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
502
|
+
});
|
|
503
|
+
printJson({ ok: true, command: "npm run verify", cwd: root });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const checks = [
|
|
507
|
+
verifyInstallCheck("package root", true, "Mina package root found.", `Could not locate ${root}.`),
|
|
508
|
+
verifyInstallCheck("cli dist", (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "dist", "apps", "cli", "src", "index.js")), "CLI dist found.", "Package is missing CLI dist. Run npm run build before packaging."),
|
|
509
|
+
verifyInstallCheck("mcp dist", (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "dist", "apps", "mcp-server", "src", "index.js")), "MCP server dist found.", "Package is missing MCP server dist. Run npm run build before packaging."),
|
|
510
|
+
verifyInstallCheck("http server dist", (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "dist", "apps", "http-server", "src", "index.js")), "HTTP server dist found.", "Package is missing HTTP server dist. Run npm run build before packaging."),
|
|
511
|
+
verifyInstallCheck("web ui index", (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "dist", "apps", "http-server", "src", "public", "index.html")), "Web UI index found.", "Package is missing built Web UI index. Run npm run build before packaging."),
|
|
512
|
+
verifyInstallCheck("web ui js asset", hasWebUiAsset(root, ".js"), "Web UI JavaScript asset found.", "Package is missing built Web UI JavaScript assets. Run npm run build before packaging."),
|
|
513
|
+
verifyInstallCheck("web ui css asset", hasWebUiAsset(root, ".css"), "Web UI CSS asset found.", "Package is missing built Web UI CSS assets. Run npm run build before packaging."),
|
|
514
|
+
verifyInstallCheck("user guide", (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "docs", "USER-START-GUIDE.md")), "Packaged user documentation found.", "Packaged user documentation is missing."),
|
|
515
|
+
verifyInstallCheck("registration skill", (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "skills", "mina-ai-router-agent", "SKILL.md")), "Packaged registration skill found.", "Packaged registration skill is missing."),
|
|
516
|
+
];
|
|
517
|
+
const ok = checks.every((check) => check.ok);
|
|
518
|
+
printJson({
|
|
519
|
+
ok,
|
|
520
|
+
command: "mair verify",
|
|
521
|
+
packageRoot: root,
|
|
522
|
+
version,
|
|
523
|
+
checks,
|
|
524
|
+
});
|
|
525
|
+
if (!ok) {
|
|
526
|
+
process.exitCode = 1;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function verifyInstallCheck(name, ok, okDetail, failDetail) {
|
|
530
|
+
return { name, ok, detail: ok ? okDetail : failDetail };
|
|
531
|
+
}
|
|
532
|
+
function hasWebUiAsset(root, extension) {
|
|
533
|
+
const assetRoot = (0, node_path_1.join)(root, "dist", "apps", "http-server", "src", "public", "assets");
|
|
534
|
+
if (!(0, node_fs_1.existsSync)(assetRoot)) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
return (0, node_fs_1.readdirSync)(assetRoot).some((file) => {
|
|
539
|
+
const filePath = (0, node_path_1.join)(assetRoot, file);
|
|
540
|
+
return (0, node_fs_1.statSync)(filePath).isFile() && file.endsWith(extension);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function runDoctor(args, context) {
|
|
548
|
+
const flags = parseFlags(args);
|
|
549
|
+
const projectRoot = (0, node_path_1.resolve)(flags.project ?? process.cwd());
|
|
550
|
+
const clientFilter = resolveDoctorClientFilter(args, flags);
|
|
551
|
+
const scopedDoctor = clientFilter.length !== 2;
|
|
552
|
+
const ignoreBlockedAgents = flags["ignore-blocked-agents"] === "true";
|
|
553
|
+
const liveServer = matchingLiveServerStatus();
|
|
554
|
+
const status = serverStatus();
|
|
555
|
+
const mcpUrl = resolveMcpUrl(flags);
|
|
556
|
+
const environmentChecks = [
|
|
557
|
+
doctorCheck("node", true, process.execPath),
|
|
558
|
+
doctorCheck("npm", commandAvailable("npm"), "Required for build, verify, and local package workflows."),
|
|
559
|
+
doctorCheck("tmux", commandAvailable("tmux"), "Required for visible CLI sessions."),
|
|
560
|
+
doctorCheck("built dist", (0, node_fs_1.existsSync)((0, node_path_1.join)(process.cwd(), "dist", "apps", "cli", "src", "index.js")), "Run npm run build if this is missing."),
|
|
561
|
+
doctorCheck("matching server", Boolean(liveServer), liveServer ? `MCP ${liveServer.mcpUrl ?? mcpUrl}` : `No running Mina server owns ${statePath}. Run mair server start.`),
|
|
562
|
+
];
|
|
563
|
+
const clients = clientFilter.map((client) => doctorClient(client, projectRoot, mcpUrl));
|
|
564
|
+
const liveState = await getLiveUiState();
|
|
565
|
+
const agents = liveState?.agents ?? await context.router.listAgentStatuses();
|
|
566
|
+
const blockers = agents
|
|
567
|
+
.filter((agent) => agent.routeReady === false || ["mcp-configuring", "permission-required", "registration-pending"].includes(agent.bootstrapStatus ?? ""))
|
|
568
|
+
.filter((agent) => !scopedDoctor || agentMatchesDoctorScope(agent, clientFilter, projectRoot))
|
|
569
|
+
.map((agent) => ({
|
|
570
|
+
id: agent.id,
|
|
571
|
+
status: agent.status,
|
|
572
|
+
routeReady: agent.routeReady,
|
|
573
|
+
routeBlockedReason: agent.routeBlockedReason,
|
|
574
|
+
bootstrapStatus: agent.bootstrapStatus,
|
|
575
|
+
mcpPreflightStatus: agent.mcpPreflightStatus,
|
|
576
|
+
registrationStatus: agent.registrationStatus,
|
|
577
|
+
repairAction: doctorRepairAction(agent),
|
|
578
|
+
}));
|
|
579
|
+
const checks = [
|
|
580
|
+
...environmentChecks,
|
|
581
|
+
doctorCheck("route-ready agents", ignoreBlockedAgents || blockers.length === 0, blockers.length
|
|
582
|
+
? `${blockers.length} agent(s) are blocked. Resolve the listed repairAction values or rerun with --ignore-blocked-agents for environment-only checks.`
|
|
583
|
+
: scopedDoctor
|
|
584
|
+
? "No selected project/client agents are blocked from receiving routed work."
|
|
585
|
+
: "No known agents are blocked from receiving routed work."),
|
|
586
|
+
];
|
|
587
|
+
const ok = checks.every((check) => check.ok) && clients.every((client) => client.ok);
|
|
588
|
+
printJson({
|
|
589
|
+
ok,
|
|
590
|
+
statePath,
|
|
591
|
+
projectRoot,
|
|
592
|
+
mcpUrl,
|
|
593
|
+
server: status,
|
|
594
|
+
checks,
|
|
595
|
+
clients,
|
|
596
|
+
blockedAgents: blockers,
|
|
597
|
+
});
|
|
598
|
+
if (!ok) {
|
|
599
|
+
process.exitCode = 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function resolveDoctorClientFilter(args, flags) {
|
|
603
|
+
const explicit = flags.client ?? flags.clients;
|
|
604
|
+
if (explicit) {
|
|
605
|
+
return normalizeSetupClientFilter(explicit);
|
|
606
|
+
}
|
|
607
|
+
const positional = args.find((arg) => !arg.startsWith("--") && ["codex", "claude", "all"].includes(arg));
|
|
608
|
+
return normalizeSetupClientFilter(positional ?? "all");
|
|
609
|
+
}
|
|
610
|
+
function agentMatchesDoctorScope(agent, clientFilter, projectRoot) {
|
|
611
|
+
const agentClient = agent.agentType === "claude" ? "claude" : agent.agentType === "codex" ? "codex" : undefined;
|
|
612
|
+
if (!agentClient) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
return clientFilter.includes(agentClient)
|
|
616
|
+
&& projectRootAliases(projectRoot).includes(agent.projectRoot ?? "");
|
|
617
|
+
}
|
|
618
|
+
function projectRootAliases(projectRoot) {
|
|
619
|
+
const aliases = new Set([projectRoot]);
|
|
620
|
+
if (projectRoot.startsWith("/tmp/")) {
|
|
621
|
+
aliases.add(`/private${projectRoot}`);
|
|
622
|
+
}
|
|
623
|
+
if (projectRoot.startsWith("/var/")) {
|
|
624
|
+
aliases.add(`/private${projectRoot}`);
|
|
625
|
+
}
|
|
626
|
+
if (projectRoot.startsWith("/private/tmp/")) {
|
|
627
|
+
aliases.add(projectRoot.replace(/^\/private/, ""));
|
|
628
|
+
}
|
|
629
|
+
if (projectRoot.startsWith("/private/var/")) {
|
|
630
|
+
aliases.add(projectRoot.replace(/^\/private/, ""));
|
|
631
|
+
}
|
|
632
|
+
return [...aliases].filter(Boolean);
|
|
633
|
+
}
|
|
634
|
+
function doctorRepairAction(agent) {
|
|
635
|
+
if (agent.bootstrapStatus === "mcp-configuring" || agent.mcpPreflightStatus === "missing" || agent.mcpPreflightStatus === "stale") {
|
|
636
|
+
const client = agent.agentType === "claude" ? "claude" : "codex";
|
|
637
|
+
const projectRoot = shellQuote(agent.projectRoot || process.cwd());
|
|
638
|
+
return `Run mair setup ${client} --project ${projectRoot}, then rerun mair doctor --client ${client} --project ${projectRoot}.`;
|
|
639
|
+
}
|
|
640
|
+
if (agent.bootstrapStatus === "permission-required") {
|
|
641
|
+
return `Open the terminal for ${agent.id}, approve the CLI trust or permission prompt, then rerun mair doctor.`;
|
|
642
|
+
}
|
|
643
|
+
if (agent.bootstrapStatus === "registration-pending" || agent.registrationStatus === "pending") {
|
|
644
|
+
return `Ask ${agent.id} to register this session with Mina AI Router, then rerun mair doctor.`;
|
|
645
|
+
}
|
|
646
|
+
if (agent.routeBlockedReason) {
|
|
647
|
+
return agent.routeBlockedReason;
|
|
648
|
+
}
|
|
649
|
+
return `Open agent ${agent.id} in the Web UI inspector and resolve its readiness blocker.`;
|
|
650
|
+
}
|
|
651
|
+
function runSetup(args) {
|
|
652
|
+
const client = args[0];
|
|
653
|
+
if (client !== "codex" && client !== "claude") {
|
|
654
|
+
throw new Error("Usage: mair setup <codex|claude> [--project <path>] [--mcp-url <url>] [--mcp-name mina-ai-router] [--dry-run]");
|
|
655
|
+
}
|
|
656
|
+
setupClient(client, parseFlags(args.slice(1)));
|
|
657
|
+
}
|
|
658
|
+
function setupClient(client, flags) {
|
|
659
|
+
const projectRoot = (0, node_path_1.resolve)(flags.project ?? process.cwd());
|
|
660
|
+
const mcpName = flags["mcp-name"] ?? "mina-ai-router";
|
|
661
|
+
const mcpUrl = resolveMcpUrl(flags);
|
|
662
|
+
const dryRun = flags["dry-run"] === "true";
|
|
663
|
+
const source = resolveSkillSourcePath();
|
|
664
|
+
const target = skillTargetFor(client, projectRoot);
|
|
665
|
+
const commands = mcpCommandsFor(client, mcpName, mcpUrl);
|
|
666
|
+
const actions = [];
|
|
667
|
+
if (!commandAvailable(client)) {
|
|
668
|
+
throw new Error(`Required command "${client}" is not available on PATH. Install ${client} before running mair setup ${client}.`);
|
|
669
|
+
}
|
|
670
|
+
if (!(0, node_fs_1.existsSync)(projectRoot)) {
|
|
671
|
+
throw new Error(`Project path does not exist: ${projectRoot}`);
|
|
672
|
+
}
|
|
673
|
+
if (dryRun) {
|
|
674
|
+
printJson({
|
|
675
|
+
ok: true,
|
|
676
|
+
dryRun,
|
|
677
|
+
client,
|
|
678
|
+
projectRoot,
|
|
679
|
+
mcpUrl,
|
|
680
|
+
commands: {
|
|
681
|
+
remove: shellCommand(commands.remove),
|
|
682
|
+
add: shellCommand(commands.add),
|
|
683
|
+
verify: shellCommand(commands.verify),
|
|
684
|
+
list: shellCommand(commands.list),
|
|
685
|
+
},
|
|
686
|
+
skill: {
|
|
687
|
+
source,
|
|
688
|
+
target,
|
|
689
|
+
planned: true,
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
runOptionalMcpCommand(commands.remove, actions, "remove existing MCP entry", projectRoot);
|
|
695
|
+
const addOutput = runRequiredMcpCommand(commands.add, actions, "add MCP entry", projectRoot);
|
|
696
|
+
const verifyOutput = runRequiredMcpCommand(commands.verify, actions, "verify MCP entry", projectRoot);
|
|
697
|
+
const listOutput = runRequiredMcpCommand(commands.list, actions, "verify MCP visibility", projectRoot);
|
|
698
|
+
const verified = `${addOutput}\n${verifyOutput}`.includes(mcpUrl) && listOutput.includes(mcpName);
|
|
699
|
+
actions.push({
|
|
700
|
+
name: "verify MCP URL",
|
|
701
|
+
ok: verified,
|
|
702
|
+
detail: verified ? `MCP URL verified and visible in ${client} mcp list: ${mcpUrl}` : `MCP command output did not prove ${mcpName} is visible for ${mcpUrl}.`,
|
|
703
|
+
});
|
|
704
|
+
if (!verified) {
|
|
705
|
+
throw new Error(`MCP setup for ${client} did not verify ${mcpUrl}. Run ${shellCommand(commands.verify)} to inspect the client config.`);
|
|
706
|
+
}
|
|
707
|
+
linkSkill(source, target);
|
|
708
|
+
actions.push({ name: "install registration skill", ok: true, detail: `${target} -> ${source}` });
|
|
709
|
+
printJson({
|
|
710
|
+
ok: true,
|
|
711
|
+
client,
|
|
712
|
+
projectRoot,
|
|
713
|
+
mcpUrl,
|
|
714
|
+
actions,
|
|
715
|
+
commands: {
|
|
716
|
+
remove: shellCommand(commands.remove),
|
|
717
|
+
add: shellCommand(commands.add),
|
|
718
|
+
verify: shellCommand(commands.verify),
|
|
719
|
+
list: shellCommand(commands.list),
|
|
720
|
+
},
|
|
721
|
+
skill: {
|
|
722
|
+
source,
|
|
723
|
+
target,
|
|
724
|
+
installed: (0, node_fs_1.existsSync)((0, node_path_1.join)(target, "SKILL.md")),
|
|
725
|
+
},
|
|
726
|
+
nextSteps: [
|
|
727
|
+
`Start a visible ${client} session with mair ${client}.`,
|
|
728
|
+
"If the UI shows mcp-configuring, rerun mair doctor --client " + client,
|
|
729
|
+
],
|
|
271
730
|
});
|
|
272
|
-
printJson({ ok: true, command: "npm run verify" });
|
|
273
731
|
}
|
|
274
732
|
function serveHttp(args) {
|
|
275
733
|
const flags = parseFlags(args);
|
|
@@ -289,8 +747,11 @@ function serveHttp(args) {
|
|
|
289
747
|
}
|
|
290
748
|
function setupCodexPair(args, context) {
|
|
291
749
|
const options = parseFlags(args);
|
|
292
|
-
const mainRoot = options["main-root"]
|
|
293
|
-
const helperRoot = options["helper-root"]
|
|
750
|
+
const mainRoot = options["main-root"];
|
|
751
|
+
const helperRoot = options["helper-root"];
|
|
752
|
+
if (!mainRoot || !helperRoot) {
|
|
753
|
+
throw new Error("setup-codex-pair is a developer/demo helper. Provide explicit --main-root and --helper-root, or use mair setup codex/claude for normal first-run setup.");
|
|
754
|
+
}
|
|
294
755
|
const helperId = options["helper-id"] ?? "ralph";
|
|
295
756
|
const sessionId = options.session ?? "mina-ralph-codex";
|
|
296
757
|
const mcpName = options["mcp-name"] ?? "mina-ai-router";
|
|
@@ -349,6 +810,204 @@ function setupCodexPair(args, context) {
|
|
|
349
810
|
],
|
|
350
811
|
});
|
|
351
812
|
}
|
|
813
|
+
function normalizeSetupClientFilter(value) {
|
|
814
|
+
if (value === "all") {
|
|
815
|
+
return ["codex", "claude"];
|
|
816
|
+
}
|
|
817
|
+
if (value === "codex" || value === "claude") {
|
|
818
|
+
return [value];
|
|
819
|
+
}
|
|
820
|
+
throw new Error("--client must be codex, claude, or all.");
|
|
821
|
+
}
|
|
822
|
+
function doctorClient(client, projectRoot, mcpUrl) {
|
|
823
|
+
const binaryOk = commandAvailable(client);
|
|
824
|
+
const target = skillTargetFor(client, projectRoot);
|
|
825
|
+
const skillInstalled = (0, node_fs_1.existsSync)((0, node_path_1.join)(target, "SKILL.md"));
|
|
826
|
+
const mcp = binaryOk ? inspectMcpConfig(client, "mina-ai-router", mcpUrl, projectRoot) : {
|
|
827
|
+
ok: false,
|
|
828
|
+
detail: `${client} is not available on PATH.`,
|
|
829
|
+
};
|
|
830
|
+
return {
|
|
831
|
+
client,
|
|
832
|
+
ok: binaryOk && skillInstalled && mcp.ok,
|
|
833
|
+
binary: doctorCheck(`${client} binary`, binaryOk, binaryOk ? "available on PATH" : `Install ${client} before setup.`),
|
|
834
|
+
mcp,
|
|
835
|
+
skill: {
|
|
836
|
+
ok: skillInstalled,
|
|
837
|
+
target,
|
|
838
|
+
detail: skillInstalled ? "registration skill installed or linked" : `Run mair setup ${client} to install the registration skill.`,
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function doctorCheck(name, ok, detail) {
|
|
843
|
+
return { name, ok, detail };
|
|
844
|
+
}
|
|
845
|
+
function inspectMcpConfig(client, mcpName, mcpUrl, projectRoot) {
|
|
846
|
+
const commands = mcpCommandsFor(client, mcpName, mcpUrl);
|
|
847
|
+
try {
|
|
848
|
+
const getOutput = (0, node_child_process_1.execFileSync)(commands.verify.command, commands.verify.args, {
|
|
849
|
+
cwd: projectRoot,
|
|
850
|
+
encoding: "utf8",
|
|
851
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
852
|
+
});
|
|
853
|
+
const listOutput = (0, node_child_process_1.execFileSync)(commands.list.command, commands.list.args, {
|
|
854
|
+
cwd: projectRoot,
|
|
855
|
+
encoding: "utf8",
|
|
856
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
857
|
+
});
|
|
858
|
+
const output = `${getOutput}\n${listOutput}`;
|
|
859
|
+
return {
|
|
860
|
+
ok: getOutput.includes(mcpUrl) && listOutput.includes(mcpName),
|
|
861
|
+
detail: getOutput.includes(mcpUrl) && listOutput.includes(mcpName)
|
|
862
|
+
? `configured for ${mcpUrl}; ${mcpName} is visible in ${client} mcp list`
|
|
863
|
+
: `MCP entry check did not prove ${mcpName} is visible to ${client}; run mair setup ${client}.`,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
return {
|
|
868
|
+
ok: false,
|
|
869
|
+
detail: commandFailureMessage(error) || `Run mair setup ${client}.`,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function detectClientMcpConfiguredUrl(client, mcpName, mcpUrl, projectRoot) {
|
|
874
|
+
if (!commandAvailable(client)) {
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
const output = (0, node_child_process_1.execFileSync)(client, ["mcp", "get", mcpName], {
|
|
879
|
+
cwd: projectRoot,
|
|
880
|
+
encoding: "utf8",
|
|
881
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
882
|
+
});
|
|
883
|
+
const list = (0, node_child_process_1.execFileSync)(client, ["mcp", "list"], {
|
|
884
|
+
cwd: projectRoot,
|
|
885
|
+
encoding: "utf8",
|
|
886
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
887
|
+
});
|
|
888
|
+
if (!list.includes(mcpName)) {
|
|
889
|
+
return undefined;
|
|
890
|
+
}
|
|
891
|
+
return extractMcpUrl(output, mcpUrl);
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function extractMcpUrl(output, expectedUrl) {
|
|
898
|
+
if (output.includes(expectedUrl)) {
|
|
899
|
+
return expectedUrl;
|
|
900
|
+
}
|
|
901
|
+
const match = output.match(/https?:\/\/[^\s"',)]+/);
|
|
902
|
+
return match?.[0];
|
|
903
|
+
}
|
|
904
|
+
function resolveMcpUrl(flags) {
|
|
905
|
+
if (flags["mcp-url"]) {
|
|
906
|
+
return flags["mcp-url"];
|
|
907
|
+
}
|
|
908
|
+
const live = matchingLiveServerStatus();
|
|
909
|
+
if (live?.mcpUrl) {
|
|
910
|
+
return live.mcpUrl;
|
|
911
|
+
}
|
|
912
|
+
return `http://${process.env.MINA_HTTP_HOST ?? "127.0.0.1"}:${process.env.MINA_HTTP_PORT ?? "3333"}/mcp`;
|
|
913
|
+
}
|
|
914
|
+
function mcpCommandsFor(client, mcpName, mcpUrl) {
|
|
915
|
+
if (client === "codex") {
|
|
916
|
+
return {
|
|
917
|
+
remove: { command: "codex", args: ["mcp", "remove", mcpName] },
|
|
918
|
+
add: { command: "codex", args: ["mcp", "add", mcpName, "--url", mcpUrl] },
|
|
919
|
+
verify: { command: "codex", args: ["mcp", "get", mcpName] },
|
|
920
|
+
list: { command: "codex", args: ["mcp", "list"] },
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
remove: { command: "claude", args: ["mcp", "remove", mcpName] },
|
|
925
|
+
add: { command: "claude", args: ["mcp", "add", "--transport", "http", mcpName, mcpUrl] },
|
|
926
|
+
verify: { command: "claude", args: ["mcp", "get", mcpName] },
|
|
927
|
+
list: { command: "claude", args: ["mcp", "list"] },
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function runOptionalMcpCommand(command, actions, name, cwd) {
|
|
931
|
+
try {
|
|
932
|
+
(0, node_child_process_1.execFileSync)(command.command, command.args, {
|
|
933
|
+
cwd,
|
|
934
|
+
encoding: "utf8",
|
|
935
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
936
|
+
});
|
|
937
|
+
actions.push({ name, ok: true, detail: shellCommand(command) });
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
actions.push({ name, ok: true, detail: `No existing entry removed or removal was ignored. ${commandFailureMessage(error)}`.trim() });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function runRequiredMcpCommand(command, actions, name, cwd) {
|
|
944
|
+
try {
|
|
945
|
+
const output = (0, node_child_process_1.execFileSync)(command.command, command.args, {
|
|
946
|
+
cwd,
|
|
947
|
+
encoding: "utf8",
|
|
948
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
949
|
+
});
|
|
950
|
+
actions.push({ name, ok: true, detail: shellCommand(command) });
|
|
951
|
+
return output;
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
const detail = commandFailureMessage(error);
|
|
955
|
+
actions.push({ name, ok: false, detail });
|
|
956
|
+
throw new Error(`${name} failed: ${detail}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function commandFailureMessage(error) {
|
|
960
|
+
if (error && typeof error === "object") {
|
|
961
|
+
const record = error;
|
|
962
|
+
const text = `${record.stderr ? String(record.stderr) : ""}${record.stdout ? String(record.stdout) : ""}`.trim();
|
|
963
|
+
return text || record.message || "";
|
|
964
|
+
}
|
|
965
|
+
return String(error);
|
|
966
|
+
}
|
|
967
|
+
function resolveSkillSourcePath() {
|
|
968
|
+
const candidates = [
|
|
969
|
+
(0, node_path_1.join)(process.cwd(), "skills", "mina-ai-router-agent"),
|
|
970
|
+
(0, node_path_1.join)(__dirname, "../../../../skills/mina-ai-router-agent"),
|
|
971
|
+
];
|
|
972
|
+
const source = candidates.find((candidate) => (0, node_fs_1.existsSync)((0, node_path_1.join)(candidate, "SKILL.md")));
|
|
973
|
+
if (!source) {
|
|
974
|
+
throw new Error("Could not locate skills/mina-ai-router-agent/SKILL.md in this checkout or package.");
|
|
975
|
+
}
|
|
976
|
+
return source;
|
|
977
|
+
}
|
|
978
|
+
function skillTargetFor(client, projectRoot) {
|
|
979
|
+
if (client === "codex") {
|
|
980
|
+
const home = process.env.HOME;
|
|
981
|
+
if (!home) {
|
|
982
|
+
throw new Error("HOME is required to install the Codex registration skill.");
|
|
983
|
+
}
|
|
984
|
+
return (0, node_path_1.join)(home, ".codex", "skills", "mina-ai-router-agent");
|
|
985
|
+
}
|
|
986
|
+
return (0, node_path_1.join)(projectRoot, ".claude", "skills", "mina-ai-router-agent");
|
|
987
|
+
}
|
|
988
|
+
function linkSkill(source, target) {
|
|
989
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
990
|
+
(0, node_child_process_1.execFileSync)("rm", ["-rf", target], {
|
|
991
|
+
encoding: "utf8",
|
|
992
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
993
|
+
});
|
|
994
|
+
try {
|
|
995
|
+
(0, node_child_process_1.execFileSync)("ln", ["-sfn", source, target], {
|
|
996
|
+
encoding: "utf8",
|
|
997
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
1002
|
+
(0, node_child_process_1.execFileSync)("cp", ["-R", `${source}/.`, target], {
|
|
1003
|
+
encoding: "utf8",
|
|
1004
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function shellCommand(command) {
|
|
1009
|
+
return [command.command, ...command.args].map(shellQuote).join(" ");
|
|
1010
|
+
}
|
|
352
1011
|
function createContext() {
|
|
353
1012
|
const fileState = new src_1.FileState(statePath);
|
|
354
1013
|
const state = fileState.load();
|
|
@@ -372,6 +1031,7 @@ function createContext() {
|
|
|
372
1031
|
registry,
|
|
373
1032
|
requestStore,
|
|
374
1033
|
transports,
|
|
1034
|
+
agentStaleAfterMs,
|
|
375
1035
|
onStateChanged: context.save,
|
|
376
1036
|
});
|
|
377
1037
|
return {
|
|
@@ -380,38 +1040,79 @@ function createContext() {
|
|
|
380
1040
|
router,
|
|
381
1041
|
};
|
|
382
1042
|
}
|
|
383
|
-
function registerAgent(args, context) {
|
|
1043
|
+
async function registerAgent(args, context) {
|
|
384
1044
|
const id = args[0];
|
|
385
1045
|
if (!id) {
|
|
386
1046
|
throw new Error("Usage: mair register <id> --agent <type> --transport <transport> --session <session> --root <path>");
|
|
387
1047
|
}
|
|
388
1048
|
const options = parseFlags(args.slice(1));
|
|
1049
|
+
const now = new Date().toISOString();
|
|
389
1050
|
const agent = {
|
|
390
1051
|
id,
|
|
391
1052
|
name: options.name ?? id,
|
|
392
1053
|
agentType: options.agent ?? "unknown",
|
|
393
1054
|
transport: (options.transport ?? "headless"),
|
|
394
1055
|
sessionId: options.session ?? id,
|
|
1056
|
+
sessionFingerprint: options["session-fingerprint"] ?? options.session ?? id,
|
|
395
1057
|
projectRoot: options.root ?? process.cwd(),
|
|
396
1058
|
tmuxTarget: options.target,
|
|
397
1059
|
startupCommand: options.command,
|
|
398
1060
|
capabilitySummary: options.summary ?? options["capability-summary"],
|
|
399
1061
|
capabilitySources: options.sources ?? options["capability-sources"],
|
|
1062
|
+
bootstrapStatus: "ready",
|
|
1063
|
+
registrationSource: "cli",
|
|
1064
|
+
registrationStatus: "confirmed",
|
|
1065
|
+
lastRegistrationAttemptAt: now,
|
|
1066
|
+
confirmedByAgentAt: now,
|
|
400
1067
|
};
|
|
401
|
-
|
|
1068
|
+
const proxied = await postToMatchingLiveServer("/api/register", agent);
|
|
1069
|
+
if (proxied) {
|
|
1070
|
+
printJson({ agent: proxied.agent });
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const registered = context.registry.register(agent, {
|
|
1074
|
+
capabilitySource: agent.capabilitySummary || agent.capabilitySources ? "manual" : undefined,
|
|
1075
|
+
});
|
|
402
1076
|
context.save();
|
|
403
|
-
printJson({ agent });
|
|
1077
|
+
printJson({ agent: registered });
|
|
404
1078
|
}
|
|
405
1079
|
async function listAgents(context) {
|
|
1080
|
+
const liveState = await getLiveUiState();
|
|
1081
|
+
if (liveState) {
|
|
1082
|
+
printJson({
|
|
1083
|
+
agents: liveState.agents,
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
406
1087
|
const agents = await context.router.listAgentStatuses();
|
|
407
1088
|
printJson({
|
|
408
1089
|
agents,
|
|
409
1090
|
});
|
|
410
1091
|
}
|
|
1092
|
+
async function handleAgent(args, context) {
|
|
1093
|
+
if (args[0] === "refresh-capabilities") {
|
|
1094
|
+
await refreshAgentCapabilities(args.slice(1), context);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
await showAgent(args, context);
|
|
1098
|
+
}
|
|
411
1099
|
async function showAgent(args, context) {
|
|
412
1100
|
const id = args[0];
|
|
413
1101
|
if (!id) {
|
|
414
|
-
throw new Error("Usage: mair agent <id>");
|
|
1102
|
+
throw new Error("Usage: mair agent <id> | mair agent refresh-capabilities <id> [--timeout-ms 300000]");
|
|
1103
|
+
}
|
|
1104
|
+
const liveState = await getLiveUiState();
|
|
1105
|
+
if (liveState) {
|
|
1106
|
+
const status = liveState.agents.find((candidate) => candidate.id === id);
|
|
1107
|
+
if (!status) {
|
|
1108
|
+
throw new Error(`Agent "${id}" is not registered.`);
|
|
1109
|
+
}
|
|
1110
|
+
printJson({
|
|
1111
|
+
agent: status,
|
|
1112
|
+
status,
|
|
1113
|
+
attach: status.transport === "tmux" ? `tmux attach -t ${status.sessionId}` : undefined,
|
|
1114
|
+
});
|
|
1115
|
+
return;
|
|
415
1116
|
}
|
|
416
1117
|
const agent = context.registry.require(id);
|
|
417
1118
|
const statuses = await context.router.listAgentStatuses();
|
|
@@ -421,6 +1122,125 @@ async function showAgent(args, context) {
|
|
|
421
1122
|
attach: agent.transport === "tmux" ? `tmux attach -t ${agent.sessionId}` : undefined,
|
|
422
1123
|
});
|
|
423
1124
|
}
|
|
1125
|
+
async function refreshAgentCapabilities(args, context) {
|
|
1126
|
+
const id = args[0];
|
|
1127
|
+
if (!id) {
|
|
1128
|
+
throw new Error("Usage: mair agent refresh-capabilities <id> [--timeout-ms 300000]");
|
|
1129
|
+
}
|
|
1130
|
+
const flags = parseFlags(args.slice(1));
|
|
1131
|
+
const proxied = await postToMatchingLiveServer(`/api/agents/${encodeURIComponent(id)}/refresh-capabilities`, {
|
|
1132
|
+
timeoutMs: flags["timeout-ms"] ? Number(flags["timeout-ms"]) : undefined,
|
|
1133
|
+
});
|
|
1134
|
+
if (proxied) {
|
|
1135
|
+
printJson({
|
|
1136
|
+
agent: proxied.agent,
|
|
1137
|
+
refresh: proxied.refresh,
|
|
1138
|
+
});
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const agent = context.registry.require(id);
|
|
1142
|
+
const response = await context.router.callAgent({
|
|
1143
|
+
target: id,
|
|
1144
|
+
task: buildCapabilityRefreshTask(agent),
|
|
1145
|
+
timeoutMs: flags["timeout-ms"] ? Number(flags["timeout-ms"]) : undefined,
|
|
1146
|
+
});
|
|
1147
|
+
const notice = parseCapabilityRefreshAnswer(response.answer);
|
|
1148
|
+
const refreshedAt = new Date().toISOString();
|
|
1149
|
+
const updated = context.registry.updateCapabilities(id, {
|
|
1150
|
+
summary: notice.capabilitySummary,
|
|
1151
|
+
sources: notice.capabilitySources,
|
|
1152
|
+
source: "generated",
|
|
1153
|
+
refreshedAt,
|
|
1154
|
+
profile: notice.capabilityProfile,
|
|
1155
|
+
});
|
|
1156
|
+
context.save();
|
|
1157
|
+
printJson({
|
|
1158
|
+
agent: updated,
|
|
1159
|
+
refresh: {
|
|
1160
|
+
requestId: response.requestId,
|
|
1161
|
+
refreshedAt,
|
|
1162
|
+
capabilitySource: updated.capabilitySource,
|
|
1163
|
+
},
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
function buildCapabilityRefreshTask(agent) {
|
|
1167
|
+
return [
|
|
1168
|
+
"Refresh your Mina AI Router capability registration for this visible local agent.",
|
|
1169
|
+
"",
|
|
1170
|
+
"Inspect local project docs and metadata before answering.",
|
|
1171
|
+
"Prefer capability docs in this order when present: CLAUDE.md/claude.md, AGENTS.md/agents.md, agent.md, README.md.",
|
|
1172
|
+
"If those files are missing, inspect package metadata and the project file tree.",
|
|
1173
|
+
"",
|
|
1174
|
+
"Return only JSON with these string fields:",
|
|
1175
|
+
"{",
|
|
1176
|
+
' "capabilitySummary": "2-5 short bullets or one short paragraph under 800 characters",',
|
|
1177
|
+
' "capabilitySources": "comma-separated file paths or project signals used",',
|
|
1178
|
+
' "capabilityProfile": {',
|
|
1179
|
+
' "projectPurpose": "what this project implements",',
|
|
1180
|
+
' "primaryLanguages": ["TypeScript"],',
|
|
1181
|
+
' "keyAreas": ["router", "MCP endpoint"],',
|
|
1182
|
+
' "canAnswer": ["specific question domains this agent can answer"],',
|
|
1183
|
+
' "cannotAnswerYet": ["known limits"],',
|
|
1184
|
+
' "evidence": ["README.md", "package.json", "src/..."]',
|
|
1185
|
+
" }",
|
|
1186
|
+
"}",
|
|
1187
|
+
"",
|
|
1188
|
+
"Do not include markdown fences or extra commentary in the JSON body.",
|
|
1189
|
+
"",
|
|
1190
|
+
"Registration context:",
|
|
1191
|
+
`- id: ${agent.id}`,
|
|
1192
|
+
`- agentType: ${agent.agentType}`,
|
|
1193
|
+
`- transport: ${agent.transport}`,
|
|
1194
|
+
`- sessionId: ${agent.sessionId}`,
|
|
1195
|
+
`- projectRoot: ${agent.projectRoot}`,
|
|
1196
|
+
].join("\n");
|
|
1197
|
+
}
|
|
1198
|
+
function parseCapabilityRefreshAnswer(answer) {
|
|
1199
|
+
const trimmed = answer.trim();
|
|
1200
|
+
const jsonText = trimmed.startsWith("{") ? trimmed : trimmed.slice(trimmed.indexOf("{"), trimmed.lastIndexOf("}") + 1);
|
|
1201
|
+
if (!jsonText || !jsonText.startsWith("{") || !jsonText.endsWith("}")) {
|
|
1202
|
+
throw new Error("Capability refresh response did not contain a JSON object.");
|
|
1203
|
+
}
|
|
1204
|
+
const parsed = JSON.parse(jsonText);
|
|
1205
|
+
const capabilitySummary = typeof parsed.capabilitySummary === "string" ? parsed.capabilitySummary.trim() : "";
|
|
1206
|
+
const capabilitySources = typeof parsed.capabilitySources === "string" ? parsed.capabilitySources.trim() : "";
|
|
1207
|
+
if (!capabilitySummary || !capabilitySources) {
|
|
1208
|
+
throw new Error("Capability refresh JSON requires non-empty capabilitySummary and capabilitySources strings.");
|
|
1209
|
+
}
|
|
1210
|
+
if (capabilitySummary.length > 800) {
|
|
1211
|
+
throw new Error("Capability refresh JSON capabilitySummary must be under 800 characters.");
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
capabilitySummary,
|
|
1215
|
+
capabilitySources,
|
|
1216
|
+
capabilityProfile: capabilityProfileValue(parsed.capabilityProfile),
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function capabilityProfileValue(value) {
|
|
1220
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1221
|
+
return undefined;
|
|
1222
|
+
}
|
|
1223
|
+
const record = value;
|
|
1224
|
+
return {
|
|
1225
|
+
projectPurpose: stringObjectValue(record.projectPurpose),
|
|
1226
|
+
primaryLanguages: stringArrayValue(record.primaryLanguages),
|
|
1227
|
+
keyAreas: stringArrayValue(record.keyAreas),
|
|
1228
|
+
canAnswer: stringArrayValue(record.canAnswer),
|
|
1229
|
+
cannotAnswerYet: stringArrayValue(record.cannotAnswerYet),
|
|
1230
|
+
evidence: stringArrayValue(record.evidence),
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function stringObjectValue(value) {
|
|
1234
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1235
|
+
}
|
|
1236
|
+
function stringArrayValue(value) {
|
|
1237
|
+
if (!Array.isArray(value)) {
|
|
1238
|
+
return undefined;
|
|
1239
|
+
}
|
|
1240
|
+
const values = value.filter((item) => typeof item === "string" && Boolean(item.trim()))
|
|
1241
|
+
.map((item) => item.trim());
|
|
1242
|
+
return values.length ? values : undefined;
|
|
1243
|
+
}
|
|
424
1244
|
function showAttach(args, context) {
|
|
425
1245
|
const id = args[0];
|
|
426
1246
|
if (!id) {
|
|
@@ -442,6 +1262,15 @@ async function askAgent(args, context) {
|
|
|
442
1262
|
throw new Error('Usage: mair ask <target> "question" [--timeout-ms 300000]');
|
|
443
1263
|
}
|
|
444
1264
|
const flags = parseFlags(args);
|
|
1265
|
+
const proxied = await postToMatchingLiveServer("/api/ask", {
|
|
1266
|
+
target,
|
|
1267
|
+
task: taskFromArgs(args.slice(1)),
|
|
1268
|
+
timeoutMs: flags["timeout-ms"] ? Number(flags["timeout-ms"]) : undefined,
|
|
1269
|
+
});
|
|
1270
|
+
if (proxied) {
|
|
1271
|
+
printJson(proxied.result);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
445
1274
|
try {
|
|
446
1275
|
const response = await context.router.callAgent({
|
|
447
1276
|
target,
|
|
@@ -454,6 +1283,17 @@ async function askAgent(args, context) {
|
|
|
454
1283
|
context.save();
|
|
455
1284
|
}
|
|
456
1285
|
}
|
|
1286
|
+
async function registerThroughLiveOwner(agent, context) {
|
|
1287
|
+
const proxied = await postToMatchingLiveServer("/api/register", agent);
|
|
1288
|
+
if (proxied) {
|
|
1289
|
+
return proxied.agent;
|
|
1290
|
+
}
|
|
1291
|
+
const registered = context.registry.register(agent, {
|
|
1292
|
+
capabilitySource: agent.capabilitySummary || agent.capabilitySources ? "manual" : undefined,
|
|
1293
|
+
});
|
|
1294
|
+
context.save();
|
|
1295
|
+
return registered;
|
|
1296
|
+
}
|
|
457
1297
|
function listRequests(args, context) {
|
|
458
1298
|
const flags = parseFlags(args);
|
|
459
1299
|
const target = flags.target;
|
|
@@ -462,13 +1302,256 @@ function listRequests(args, context) {
|
|
|
462
1302
|
: context.router.listRequests();
|
|
463
1303
|
printJson({ requests });
|
|
464
1304
|
}
|
|
465
|
-
function
|
|
1305
|
+
async function handleRequest(args, context) {
|
|
466
1306
|
const requestId = args[0];
|
|
467
1307
|
if (!requestId) {
|
|
468
|
-
throw new Error("Usage: mair request <request-id>");
|
|
1308
|
+
throw new Error("Usage: mair request <request-id> [retry|cancel|archive|unarchive|interrupt|recover]");
|
|
1309
|
+
}
|
|
1310
|
+
const action = args[1];
|
|
1311
|
+
if (action) {
|
|
1312
|
+
await runRequestAction(requestId, action, context);
|
|
1313
|
+
return;
|
|
469
1314
|
}
|
|
470
1315
|
printJson(context.router.getRequest(requestId));
|
|
471
1316
|
}
|
|
1317
|
+
async function runRequestAction(requestId, action, context) {
|
|
1318
|
+
if (isRequestAction(action) && await runServerRequestAction(requestId, action)) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const request = context.router.getRequest(requestId);
|
|
1322
|
+
switch (action) {
|
|
1323
|
+
case "retry": {
|
|
1324
|
+
context.requestStore.assertActionAllowed(request, "retry");
|
|
1325
|
+
try {
|
|
1326
|
+
const result = await context.router.callAgent({
|
|
1327
|
+
sourceAgent: request.sourceAgent,
|
|
1328
|
+
target: request.targetAgent,
|
|
1329
|
+
task: request.task,
|
|
1330
|
+
retryOfRequestId: request.id,
|
|
1331
|
+
});
|
|
1332
|
+
context.requestStore.recordRetry(request.id, result.requestId);
|
|
1333
|
+
printJson(result);
|
|
1334
|
+
}
|
|
1335
|
+
finally {
|
|
1336
|
+
context.save();
|
|
1337
|
+
}
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
case "cancel": {
|
|
1341
|
+
const updated = context.requestStore.cancel(requestId, "Cancelled by operator from Mina AI Router CLI.", "cli");
|
|
1342
|
+
clearAgentLeaseForRequest(updated, context);
|
|
1343
|
+
context.save();
|
|
1344
|
+
printJson(updated);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
case "interrupt": {
|
|
1348
|
+
const updated = interruptRequest(requestId, context);
|
|
1349
|
+
context.save();
|
|
1350
|
+
printJson(updated);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
case "recover": {
|
|
1354
|
+
const updated = context.router.recoverRequestLease(requestId, "cli", "Marked recovered by operator from Mina AI Router CLI.");
|
|
1355
|
+
printJson(updated);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
case "archive": {
|
|
1359
|
+
const updated = context.router.archiveRequest(requestId, "cli", "Archived by operator from Mina AI Router CLI.");
|
|
1360
|
+
printJson(updated);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
case "unarchive": {
|
|
1364
|
+
const updated = context.requestStore.unarchive(requestId);
|
|
1365
|
+
context.save();
|
|
1366
|
+
printJson(updated);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
default:
|
|
1370
|
+
throw new Error(`Unsupported request action "${action}". Use retry, cancel, archive, unarchive, interrupt, or recover.`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function interruptRequest(requestId, context) {
|
|
1374
|
+
const request = context.requestStore.require(requestId);
|
|
1375
|
+
context.requestStore.assertActionAllowed(request, "interrupt");
|
|
1376
|
+
const agentId = request.leaseOwnerAgentId ?? request.targetAgent;
|
|
1377
|
+
const agent = context.registry.require(agentId);
|
|
1378
|
+
if (agent.transport !== "tmux") {
|
|
1379
|
+
throw new Error(`Request "${requestId}" targets ${agent.transport}, not tmux; open the session manually.`);
|
|
1380
|
+
}
|
|
1381
|
+
const target = agent.tmuxTarget ?? agent.sessionId;
|
|
1382
|
+
new src_2.TmuxClient().sendInterrupt(target);
|
|
1383
|
+
return context.requestStore.recordInterrupt(requestId, {
|
|
1384
|
+
source: "cli",
|
|
1385
|
+
terminalTarget: target,
|
|
1386
|
+
message: `Terminal interrupt sent to ${target}.`,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
function clearAgentLeaseForRequest(request, context) {
|
|
1390
|
+
if (!request.leaseOwnerAgentId) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
const agent = context.registry.get(request.leaseOwnerAgentId);
|
|
1394
|
+
if (!agent || agent.activeRequestId !== request.id) {
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
context.registry.register({
|
|
1398
|
+
...agent,
|
|
1399
|
+
activeRequestId: undefined,
|
|
1400
|
+
leaseStatus: "released",
|
|
1401
|
+
leaseReleasedAt: new Date().toISOString(),
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
function isRequestAction(action) {
|
|
1405
|
+
return action === "retry"
|
|
1406
|
+
|| action === "cancel"
|
|
1407
|
+
|| action === "archive"
|
|
1408
|
+
|| action === "unarchive"
|
|
1409
|
+
|| action === "interrupt"
|
|
1410
|
+
|| action === "recover";
|
|
1411
|
+
}
|
|
1412
|
+
async function runServerRequestAction(requestId, action) {
|
|
1413
|
+
const status = matchingLiveServerStatus();
|
|
1414
|
+
if (!status) {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
const url = `http://${status.host}:${status.port}/api/requests/${encodeURIComponent(requestId)}/${encodeURIComponent(action)}`;
|
|
1418
|
+
let response;
|
|
1419
|
+
try {
|
|
1420
|
+
response = await fetch(url, {
|
|
1421
|
+
method: "POST",
|
|
1422
|
+
headers: { "content-type": "application/json" },
|
|
1423
|
+
body: "{}",
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
throw new Error(`Mina server is running for this state file, but ${action} could not reach ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1428
|
+
}
|
|
1429
|
+
const body = await parseLiveServerJsonResponse(response, url, `/api/requests/${requestId}/${action}`);
|
|
1430
|
+
if (!response.ok) {
|
|
1431
|
+
throw new Error(body.error ?? `Request action ${action} failed with HTTP ${response.status}.`);
|
|
1432
|
+
}
|
|
1433
|
+
printJson(body.result);
|
|
1434
|
+
return true;
|
|
1435
|
+
}
|
|
1436
|
+
async function getLiveUiState() {
|
|
1437
|
+
return getFromMatchingLiveServer("/api/state");
|
|
1438
|
+
}
|
|
1439
|
+
function matchingLiveServerStatus() {
|
|
1440
|
+
const status = serverStatus();
|
|
1441
|
+
if (!status.running || !status.host || !status.port || !status.statePath) {
|
|
1442
|
+
return undefined;
|
|
1443
|
+
}
|
|
1444
|
+
if (resolvePath(status.statePath) !== resolvePath(statePath)) {
|
|
1445
|
+
return undefined;
|
|
1446
|
+
}
|
|
1447
|
+
return status;
|
|
1448
|
+
}
|
|
1449
|
+
async function getFromMatchingLiveServer(path) {
|
|
1450
|
+
const status = matchingLiveServerStatus();
|
|
1451
|
+
if (!status) {
|
|
1452
|
+
return undefined;
|
|
1453
|
+
}
|
|
1454
|
+
const url = `http://${status.host}:${status.port}${path}`;
|
|
1455
|
+
let response;
|
|
1456
|
+
try {
|
|
1457
|
+
response = await fetch(url);
|
|
1458
|
+
}
|
|
1459
|
+
catch (error) {
|
|
1460
|
+
throw new Error(`Mina server is running for this state file, but CLI live read could not reach ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1461
|
+
}
|
|
1462
|
+
const responseBody = await parseLiveServerJsonResponse(response, url, path);
|
|
1463
|
+
if (!response.ok) {
|
|
1464
|
+
throw new Error(responseBody.error ?? `CLI live read ${path} failed with HTTP ${response.status}.`);
|
|
1465
|
+
}
|
|
1466
|
+
return responseBody;
|
|
1467
|
+
}
|
|
1468
|
+
async function postToMatchingLiveServer(path, body) {
|
|
1469
|
+
const status = matchingLiveServerStatus();
|
|
1470
|
+
if (!status) {
|
|
1471
|
+
return undefined;
|
|
1472
|
+
}
|
|
1473
|
+
const url = `http://${status.host}:${status.port}${path}`;
|
|
1474
|
+
let response;
|
|
1475
|
+
try {
|
|
1476
|
+
response = await fetch(url, {
|
|
1477
|
+
method: "POST",
|
|
1478
|
+
headers: { "content-type": "application/json" },
|
|
1479
|
+
body: JSON.stringify(body ?? {}),
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
throw new Error(`Mina server is running for this state file, but CLI proxy could not reach ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1484
|
+
}
|
|
1485
|
+
const responseBody = await parseLiveServerJsonResponse(response, url, path);
|
|
1486
|
+
if (!response.ok) {
|
|
1487
|
+
throw new Error(responseBody.error ?? `CLI proxy ${path} failed with HTTP ${response.status}.`);
|
|
1488
|
+
}
|
|
1489
|
+
return responseBody;
|
|
1490
|
+
}
|
|
1491
|
+
async function parseLiveServerJsonResponse(response, url, path) {
|
|
1492
|
+
const text = await response.text();
|
|
1493
|
+
const parsed = safeJson(text);
|
|
1494
|
+
if (parsed === undefined) {
|
|
1495
|
+
throw new Error(nonMinaPidMessage(url, `response was not Mina JSON: ${truncateText(text, 160)}`));
|
|
1496
|
+
}
|
|
1497
|
+
if (response.ok && !looksLikeMinaResponse(path, parsed)) {
|
|
1498
|
+
throw new Error(nonMinaPidMessage(url, "response JSON did not match Mina server shape"));
|
|
1499
|
+
}
|
|
1500
|
+
return parsed;
|
|
1501
|
+
}
|
|
1502
|
+
function looksLikeMinaResponse(path, value) {
|
|
1503
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
const record = value;
|
|
1507
|
+
if (path === "/api/health") {
|
|
1508
|
+
return typeof record.statePath === "string"
|
|
1509
|
+
&& typeof record.mcpUrl === "string"
|
|
1510
|
+
&& Boolean(record.agents && typeof record.agents === "object")
|
|
1511
|
+
&& Boolean(record.requests && typeof record.requests === "object");
|
|
1512
|
+
}
|
|
1513
|
+
if (path === "/api/state") {
|
|
1514
|
+
return typeof record.statePath === "string"
|
|
1515
|
+
&& typeof record.mcpUrl === "string"
|
|
1516
|
+
&& Array.isArray(record.agents)
|
|
1517
|
+
&& Array.isArray(record.requests);
|
|
1518
|
+
}
|
|
1519
|
+
if (path === "/api/register") {
|
|
1520
|
+
return Boolean(record.agent && typeof record.agent === "object");
|
|
1521
|
+
}
|
|
1522
|
+
if (path === "/api/ask") {
|
|
1523
|
+
return Boolean(record.result && typeof record.result === "object");
|
|
1524
|
+
}
|
|
1525
|
+
if (/^\/api\/agents\/[^/]+\/refresh-capabilities$/.test(path)) {
|
|
1526
|
+
return Boolean(record.agent && typeof record.agent === "object")
|
|
1527
|
+
&& Boolean(record.refresh && typeof record.refresh === "object");
|
|
1528
|
+
}
|
|
1529
|
+
if (/^\/api\/requests\/[^/]+\/[^/]+$/.test(path)) {
|
|
1530
|
+
return "result" in record;
|
|
1531
|
+
}
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
function nonMinaPidMessage(url, detail) {
|
|
1535
|
+
return [
|
|
1536
|
+
`Mina server pid file points at ${url}, but ${detail}.`,
|
|
1537
|
+
`Remove stale pid file ${serverPidPath} or restart mair server.`,
|
|
1538
|
+
].join(" ");
|
|
1539
|
+
}
|
|
1540
|
+
function resolvePath(value) {
|
|
1541
|
+
return (0, node_path_1.resolve)(value);
|
|
1542
|
+
}
|
|
1543
|
+
function safeJson(value) {
|
|
1544
|
+
try {
|
|
1545
|
+
return JSON.parse(value);
|
|
1546
|
+
}
|
|
1547
|
+
catch {
|
|
1548
|
+
return undefined;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
function truncateText(value, maxLength) {
|
|
1552
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
1553
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1).trim()}...` : normalized;
|
|
1554
|
+
}
|
|
472
1555
|
function parseFlags(args) {
|
|
473
1556
|
const flags = {};
|
|
474
1557
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -476,6 +1559,12 @@ function parseFlags(args) {
|
|
|
476
1559
|
if (!token?.startsWith("--")) {
|
|
477
1560
|
continue;
|
|
478
1561
|
}
|
|
1562
|
+
const inlineValueIndex = token.indexOf("=");
|
|
1563
|
+
if (inlineValueIndex > 2) {
|
|
1564
|
+
const key = token.slice(2, inlineValueIndex);
|
|
1565
|
+
flags[key] = token.slice(inlineValueIndex + 1);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
479
1568
|
const key = token.slice(2);
|
|
480
1569
|
const value = args[index + 1];
|
|
481
1570
|
if (!value || value.startsWith("--")) {
|
|
@@ -487,15 +1576,25 @@ function parseFlags(args) {
|
|
|
487
1576
|
}
|
|
488
1577
|
return flags;
|
|
489
1578
|
}
|
|
1579
|
+
function hasHelpFlag(args) {
|
|
1580
|
+
return args.includes("--help") || args.includes("-h");
|
|
1581
|
+
}
|
|
490
1582
|
function assertCommandAvailable(command) {
|
|
1583
|
+
if (commandAvailable(command)) {
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
throw new Error(`Required command "${command}" is not available on PATH.`);
|
|
1587
|
+
}
|
|
1588
|
+
function commandAvailable(command) {
|
|
491
1589
|
try {
|
|
492
1590
|
(0, node_child_process_1.execFileSync)("which", [command], {
|
|
493
1591
|
encoding: "utf8",
|
|
494
1592
|
stdio: ["ignore", "pipe", "pipe"],
|
|
495
1593
|
});
|
|
1594
|
+
return true;
|
|
496
1595
|
}
|
|
497
1596
|
catch {
|
|
498
|
-
|
|
1597
|
+
return false;
|
|
499
1598
|
}
|
|
500
1599
|
}
|
|
501
1600
|
function shellQuote(value) {
|
|
@@ -517,6 +1616,9 @@ function sleep(milliseconds) {
|
|
|
517
1616
|
}
|
|
518
1617
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
519
1618
|
}
|
|
1619
|
+
function delay(milliseconds) {
|
|
1620
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
|
|
1621
|
+
}
|
|
520
1622
|
function taskFromArgs(args) {
|
|
521
1623
|
const taskTokens = [];
|
|
522
1624
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -535,7 +1637,10 @@ function printHelp() {
|
|
|
535
1637
|
Commands:
|
|
536
1638
|
mair version
|
|
537
1639
|
mair health
|
|
538
|
-
mair
|
|
1640
|
+
mair doctor [--client <codex|claude|all>] [--project <path>] [--json]
|
|
1641
|
+
mair verify # installed package self-check; use npm run verify in a checkout
|
|
1642
|
+
mair setup codex [--project <path>] [--mcp-url <url>] [--mcp-name mina-ai-router]
|
|
1643
|
+
mair setup claude [--project <path>] [--mcp-url <url>] [--mcp-name mina-ai-router]
|
|
539
1644
|
mair server start [--port 3333] [--host 127.0.0.1]
|
|
540
1645
|
mair server stop
|
|
541
1646
|
mair server status
|
|
@@ -544,26 +1649,126 @@ Commands:
|
|
|
544
1649
|
mair register <id> --agent <type> --transport <headless|mock|tmux|zmux> --session <session> --root <path>
|
|
545
1650
|
mair agents
|
|
546
1651
|
mair agent <id>
|
|
1652
|
+
mair agent refresh-capabilities <id> [--timeout-ms 300000]
|
|
547
1653
|
mair attach <id>
|
|
548
|
-
mair setup-codex-pair [--main-root <path>] [--helper-root <path>] [--helper-id <id>] [--session <tmux-session>]
|
|
549
1654
|
mair serve [--port 3333]
|
|
550
1655
|
mair ask <target> "question"
|
|
551
1656
|
mair requests [--target <id>]
|
|
552
|
-
mair request <request-id>
|
|
1657
|
+
mair request <request-id> [retry|cancel|archive|unarchive|interrupt|recover]
|
|
1658
|
+
|
|
1659
|
+
Developer/demo helper:
|
|
1660
|
+
mair setup-codex-pair --main-root <path> --helper-root <path> [--helper-id <id>] [--session <tmux-session>]
|
|
553
1661
|
|
|
554
1662
|
Example:
|
|
1663
|
+
mair server start --port 3333
|
|
1664
|
+
mair setup codex --project ~/work/payment
|
|
1665
|
+
mair doctor --client codex --project ~/work/payment
|
|
1666
|
+
mair setup claude --project ~/work/payment
|
|
1667
|
+
mair doctor --client claude --project ~/work/payment
|
|
555
1668
|
mair register payment --agent gemini --transport headless --session payment --root ./payment
|
|
556
1669
|
mair register payment --agent gemini --transport tmux --session payment --root ~/work/payment
|
|
557
|
-
mair setup-codex-pair
|
|
558
1670
|
mair serve
|
|
559
|
-
mair server start --port 3333
|
|
560
1671
|
mair codex
|
|
561
1672
|
mair claude
|
|
562
1673
|
mair ask payment "현재 payment flow를 요약해줘."
|
|
563
1674
|
|
|
564
1675
|
State:
|
|
565
|
-
|
|
1676
|
+
By default, Mina uses ~/.mair for router state, pid, and logs across directories.
|
|
1677
|
+
Set MINA_RUNTIME_DIR, MINA_ROUTER_STATE, or MINA_SERVER_PID only for an isolated runtime.
|
|
1678
|
+
`);
|
|
1679
|
+
}
|
|
1680
|
+
function printCommandHelp(command) {
|
|
1681
|
+
switch (command) {
|
|
1682
|
+
case "doctor":
|
|
1683
|
+
console.log(`Usage: mair doctor [--client <codex|claude|all>] [--project <path>] [--json] [--ignore-blocked-agents]
|
|
1684
|
+
|
|
1685
|
+
Checks local server, client MCP setup, registration skill installation, and route readiness.
|
|
1686
|
+
`);
|
|
1687
|
+
return;
|
|
1688
|
+
case "setup":
|
|
1689
|
+
console.log(`Usage: mair setup <codex|claude> [--project <path>] [--mcp-url <url>] [--mcp-name mina-ai-router] [--dry-run]
|
|
1690
|
+
|
|
1691
|
+
Configures a chosen client MCP profile and installs the Mina registration skill.
|
|
1692
|
+
`);
|
|
1693
|
+
return;
|
|
1694
|
+
case "server":
|
|
1695
|
+
console.log(`Usage: mair server <start|stop|status> [--port 3333] [--host 127.0.0.1]
|
|
1696
|
+
|
|
1697
|
+
Starts, stops, or inspects the local Mina HTTP/Web UI server.
|
|
1698
|
+
`);
|
|
1699
|
+
return;
|
|
1700
|
+
case "codex":
|
|
1701
|
+
console.log(`Usage: mair codex [--id <id>] [--session <tmux-session>] [--root <path>] [--no-attach] [--no-register]
|
|
1702
|
+
|
|
1703
|
+
Starts a visible Codex tmux agent and creates a Mina registry placeholder.
|
|
1704
|
+
`);
|
|
1705
|
+
return;
|
|
1706
|
+
case "claude":
|
|
1707
|
+
console.log(`Usage: mair claude [--id <id>] [--session <tmux-session>] [--root <path>] [--no-attach] [--no-register]
|
|
1708
|
+
|
|
1709
|
+
Starts a visible Claude tmux agent and creates a Mina registry placeholder.
|
|
1710
|
+
`);
|
|
1711
|
+
return;
|
|
1712
|
+
case "register":
|
|
1713
|
+
console.log(`Usage: mair register <id> --agent <type> --transport <transport> --session <session> --root <path>
|
|
1714
|
+
|
|
1715
|
+
Registers or refreshes an agent in the local Mina router registry.
|
|
1716
|
+
`);
|
|
1717
|
+
return;
|
|
1718
|
+
case "agent":
|
|
1719
|
+
console.log(`Usage: mair agent <id> | mair agent refresh-capabilities <id> [--timeout-ms 300000]
|
|
1720
|
+
|
|
1721
|
+
Shows agent detail or asks an agent to refresh its capability profile.
|
|
1722
|
+
`);
|
|
1723
|
+
return;
|
|
1724
|
+
case "attach":
|
|
1725
|
+
console.log(`Usage: mair attach <id>
|
|
1726
|
+
|
|
1727
|
+
Prints the attach command for a tmux-backed agent.
|
|
1728
|
+
`);
|
|
1729
|
+
return;
|
|
1730
|
+
case "setup-codex-pair":
|
|
1731
|
+
console.log(`Usage: mair setup-codex-pair --main-root <path> --helper-root <path>
|
|
1732
|
+
|
|
1733
|
+
Developer/demo helper. Use mair setup codex or mair setup claude for normal setup.
|
|
1734
|
+
`);
|
|
1735
|
+
return;
|
|
1736
|
+
case "serve":
|
|
1737
|
+
console.log(`Usage: mair serve [--port 3333]
|
|
1738
|
+
|
|
1739
|
+
Runs the HTTP/Web UI server in the current process.
|
|
1740
|
+
`);
|
|
1741
|
+
return;
|
|
1742
|
+
case "ask":
|
|
1743
|
+
console.log(`Usage: mair ask <target> "question" [--timeout-ms 300000]
|
|
1744
|
+
|
|
1745
|
+
Routes a question to a route-ready registered agent.
|
|
1746
|
+
`);
|
|
1747
|
+
return;
|
|
1748
|
+
case "requests":
|
|
1749
|
+
console.log(`Usage: mair requests [--target <id>]
|
|
1750
|
+
|
|
1751
|
+
Lists routed requests from local router state.
|
|
1752
|
+
`);
|
|
1753
|
+
return;
|
|
1754
|
+
case "request":
|
|
1755
|
+
console.log(`Usage: mair request <request-id> [retry|cancel|archive|unarchive|interrupt|recover]
|
|
1756
|
+
|
|
1757
|
+
Shows or changes a request lifecycle state.
|
|
566
1758
|
`);
|
|
1759
|
+
return;
|
|
1760
|
+
case "health":
|
|
1761
|
+
console.log("Usage: mair health\n\nShows router health and readiness counts.");
|
|
1762
|
+
return;
|
|
1763
|
+
case "verify":
|
|
1764
|
+
console.log("Usage: mair verify\n\nRuns checkout verification in a checkout, or installed package self-checks in an installed package.");
|
|
1765
|
+
return;
|
|
1766
|
+
case "version":
|
|
1767
|
+
console.log("Usage: mair version\n\nPrints the installed Mina AI Router package version.");
|
|
1768
|
+
return;
|
|
1769
|
+
default:
|
|
1770
|
+
printHelp();
|
|
1771
|
+
}
|
|
567
1772
|
}
|
|
568
1773
|
function printJson(value) {
|
|
569
1774
|
console.log(JSON.stringify(value, null, 2));
|