@minasoft/mina-ai-router 0.1.4 → 0.2.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.
@@ -7,14 +7,19 @@ 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
9
  const statePath = process.env.MINA_ROUTER_STATE ?? (0, node_path_1.join)(process.cwd(), "data", "router-state.json");
10
- const version = "0.1.4";
10
+ const version = (0, src_1.packageVersion)();
11
11
  const serverPidPath = process.env.MINA_SERVER_PID ?? (0, node_path_1.join)(process.cwd(), "data", "mair-server.json");
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 showAgent(argv.slice(3), context);
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
- showRequest(argv.slice(3), context);
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
- child.unref();
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
- sleep(500);
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
- if (shouldPromptRegister && !context.registry.get(id)) {
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 prompt = buildSelfRegistrationPrompt(agent);
195
- if (agentType === "codex") {
196
- tmux.sendCodexText(sessionId, prompt);
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
- tmux.sendText(sessionId, prompt);
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: shouldPromptRegister && !context.registry.get(id)
207
- ? "registration prompt sent to agent"
208
- : "registration prompt skipped",
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) => agent.status !== "missing"),
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: `http://${process.env.MINA_HTTP_HOST ?? "127.0.0.1"}:${process.env.MINA_HTTP_PORT ?? "3333"}/mcp`,
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, node_child_process_1.execFileSync)("npm", ["run", "verify"], {
269
- encoding: "utf8",
270
- stdio: ["ignore", "pipe", "pipe"],
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"] ?? "/Users/stevenna/WebstormProjects/minasoftai";
293
- const helperRoot = options["helper-root"] ?? "/Users/stevenna/PycharmProjects/mina-ralph-loop-bootstrap-nextjs";
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
- context.registry.register(agent);
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 showRequest(args, context) {
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
- throw new Error(`Required command "${command}" is not available on PATH.`);
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 verify
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,19 +1649,25 @@ 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를 요약해줘."
@@ -565,6 +1676,99 @@ State:
565
1676
  Set MINA_ROUTER_STATE=/path/to/router-state.json to share state between CLI and MCP.
566
1677
  `);
567
1678
  }
1679
+ function printCommandHelp(command) {
1680
+ switch (command) {
1681
+ case "doctor":
1682
+ console.log(`Usage: mair doctor [--client <codex|claude|all>] [--project <path>] [--json] [--ignore-blocked-agents]
1683
+
1684
+ Checks local server, client MCP setup, registration skill installation, and route readiness.
1685
+ `);
1686
+ return;
1687
+ case "setup":
1688
+ console.log(`Usage: mair setup <codex|claude> [--project <path>] [--mcp-url <url>] [--mcp-name mina-ai-router] [--dry-run]
1689
+
1690
+ Configures a chosen client MCP profile and installs the Mina registration skill.
1691
+ `);
1692
+ return;
1693
+ case "server":
1694
+ console.log(`Usage: mair server <start|stop|status> [--port 3333] [--host 127.0.0.1]
1695
+
1696
+ Starts, stops, or inspects the local Mina HTTP/Web UI server.
1697
+ `);
1698
+ return;
1699
+ case "codex":
1700
+ console.log(`Usage: mair codex [--id <id>] [--session <tmux-session>] [--root <path>] [--no-attach] [--no-register]
1701
+
1702
+ Starts a visible Codex tmux agent and creates a Mina registry placeholder.
1703
+ `);
1704
+ return;
1705
+ case "claude":
1706
+ console.log(`Usage: mair claude [--id <id>] [--session <tmux-session>] [--root <path>] [--no-attach] [--no-register]
1707
+
1708
+ Starts a visible Claude tmux agent and creates a Mina registry placeholder.
1709
+ `);
1710
+ return;
1711
+ case "register":
1712
+ console.log(`Usage: mair register <id> --agent <type> --transport <transport> --session <session> --root <path>
1713
+
1714
+ Registers or refreshes an agent in the local Mina router registry.
1715
+ `);
1716
+ return;
1717
+ case "agent":
1718
+ console.log(`Usage: mair agent <id> | mair agent refresh-capabilities <id> [--timeout-ms 300000]
1719
+
1720
+ Shows agent detail or asks an agent to refresh its capability profile.
1721
+ `);
1722
+ return;
1723
+ case "attach":
1724
+ console.log(`Usage: mair attach <id>
1725
+
1726
+ Prints the attach command for a tmux-backed agent.
1727
+ `);
1728
+ return;
1729
+ case "setup-codex-pair":
1730
+ console.log(`Usage: mair setup-codex-pair --main-root <path> --helper-root <path>
1731
+
1732
+ Developer/demo helper. Use mair setup codex or mair setup claude for normal setup.
1733
+ `);
1734
+ return;
1735
+ case "serve":
1736
+ console.log(`Usage: mair serve [--port 3333]
1737
+
1738
+ Runs the HTTP/Web UI server in the current process.
1739
+ `);
1740
+ return;
1741
+ case "ask":
1742
+ console.log(`Usage: mair ask <target> "question" [--timeout-ms 300000]
1743
+
1744
+ Routes a question to a route-ready registered agent.
1745
+ `);
1746
+ return;
1747
+ case "requests":
1748
+ console.log(`Usage: mair requests [--target <id>]
1749
+
1750
+ Lists routed requests from local router state.
1751
+ `);
1752
+ return;
1753
+ case "request":
1754
+ console.log(`Usage: mair request <request-id> [retry|cancel|archive|unarchive|interrupt|recover]
1755
+
1756
+ Shows or changes a request lifecycle state.
1757
+ `);
1758
+ return;
1759
+ case "health":
1760
+ console.log("Usage: mair health\n\nShows router health and readiness counts.");
1761
+ return;
1762
+ case "verify":
1763
+ console.log("Usage: mair verify\n\nRuns checkout verification in a checkout, or installed package self-checks in an installed package.");
1764
+ return;
1765
+ case "version":
1766
+ console.log("Usage: mair version\n\nPrints the installed Mina AI Router package version.");
1767
+ return;
1768
+ default:
1769
+ printHelp();
1770
+ }
1771
+ }
568
1772
  function printJson(value) {
569
1773
  console.log(JSON.stringify(value, null, 2));
570
1774
  }