@minasoft/mina-ai-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +53 -0
  2. package/dist/apps/cli/src/index.js +574 -0
  3. package/dist/apps/http-server/src/index.js +755 -0
  4. package/dist/apps/mcp-server/src/index.js +308 -0
  5. package/dist/packages/core/src/file-state.js +35 -0
  6. package/dist/packages/core/src/ids.js +8 -0
  7. package/dist/packages/core/src/index.js +24 -0
  8. package/dist/packages/core/src/prompt-envelope.js +27 -0
  9. package/dist/packages/core/src/registry.js +34 -0
  10. package/dist/packages/core/src/request-store.js +50 -0
  11. package/dist/packages/core/src/response-parser.js +33 -0
  12. package/dist/packages/core/src/router.js +100 -0
  13. package/dist/packages/core/src/types.js +2 -0
  14. package/dist/packages/mcp/src/provider.js +177 -0
  15. package/dist/packages/transports/src/headless/headless-transport.js +31 -0
  16. package/dist/packages/transports/src/index.js +22 -0
  17. package/dist/packages/transports/src/tmux/tmux-client.js +126 -0
  18. package/dist/packages/transports/src/tmux/tmux-transport.js +54 -0
  19. package/dist/packages/transports/src/transport-registry.js +20 -0
  20. package/dist/packages/transports/src/zmux/zmux-client.js +18 -0
  21. package/dist/packages/transports/src/zmux/zmux-transport.js +36 -0
  22. package/docs/DEVELOPER-START-GUIDE.md +111 -0
  23. package/docs/GETTING-STARTED.md +32 -0
  24. package/docs/HTTP-UI-MCP.md +142 -0
  25. package/docs/MCP-CLIENT-SETUP.md +71 -0
  26. package/docs/SKILL-INSTALL-GUIDE.md +96 -0
  27. package/docs/TROUBLESHOOTING.md +75 -0
  28. package/docs/USER-START-GUIDE.md +187 -0
  29. package/docs/assets/mair-agent-details.jpg +0 -0
  30. package/docs/assets/mair-live-flow.jpg +0 -0
  31. package/docs/assets/mair-terminal-preview.jpg +0 -0
  32. package/package.json +47 -0
  33. package/skills/mina-ai-router-agent/SKILL.md +64 -0
  34. package/skills/mina-ai-router-agent/agents/openai.yaml +4 -0
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Mina AI Router
2
+
3
+ Mina AI Router is a local control plane for visible CLI AI agents.
4
+
5
+ It lets a developer run project-scoped Codex or Claude sessions in `tmux`, register them as agents, inspect their capabilities, and route questions between agents through a local MCP server.
6
+
7
+ ## What It Does
8
+
9
+ - Runs a local HTTP UI and MCP endpoint.
10
+ - Shows live agents in a router-centered flow diagram.
11
+ - Starts Codex or Claude agents in `tmux` from a project directory.
12
+ - Includes a repo-local agent registration skill for self-registration.
13
+ - Captures and controls each agent terminal from the browser.
14
+ - Stores agent metadata, capability notices, and routed request history locally.
15
+ - Lets one registered agent ask another registered agent for project-specific context.
16
+
17
+ ## Install the `mair` Command
18
+
19
+ ```sh
20
+ npm install -g @minasoft/mina-ai-router
21
+ mair version
22
+ ```
23
+
24
+ Then start the local router:
25
+
26
+ ```sh
27
+ mair server start --port 3333
28
+ ```
29
+
30
+ Open:
31
+
32
+ ```text
33
+ http://127.0.0.1:3333/
34
+ ```
35
+
36
+ ## Documentation
37
+
38
+ - [Getting Started](./docs/GETTING-STARTED.md): choose the right start guide.
39
+ - [User Start Guide](./docs/USER-START-GUIDE.md): UI-first usage with screenshots.
40
+ - [Developer Start Guide](./docs/DEVELOPER-START-GUIDE.md): build, test, and development workflow.
41
+ - [MCP Client Setup](./docs/MCP-CLIENT-SETUP.md): Codex and Claude MCP registration.
42
+ - [Skill Install Guide](./docs/SKILL-INSTALL-GUIDE.md): Codex and Claude skill installation.
43
+ - [HTTP UI and MCP Server](./docs/HTTP-UI-MCP.md): server, MCP, and command reference.
44
+ - [Agent Registration Skill](./skills/mina-ai-router-agent/SKILL.md): instructions used by Codex or Claude to register the current visible CLI session.
45
+ - [Troubleshooting](./docs/TROUBLESHOOTING.md): common routing and tmux issues.
46
+
47
+ ## Verification
48
+
49
+ ```sh
50
+ npm run verify
51
+ ```
52
+
53
+ This runs core tests plus HTTP, CLI control, tmux, MCP, and multi-agent smoke tests.
@@ -0,0 +1,574 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const node_path_1 = require("node:path");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_child_process_1 = require("node:child_process");
7
+ const src_1 = require("../../../packages/core/src");
8
+ const src_2 = require("../../../packages/transports/src");
9
+ const statePath = process.env.MINA_ROUTER_STATE ?? (0, node_path_1.join)(process.cwd(), "data", "router-state.json");
10
+ const version = "0.1.0";
11
+ const serverPidPath = process.env.MINA_SERVER_PID ?? (0, node_path_1.join)(process.cwd(), "data", "mair-server.json");
12
+ async function main(argv) {
13
+ const command = argv[2];
14
+ if (!command || command === "help" || command === "--help" || command === "-h") {
15
+ printHelp();
16
+ return;
17
+ }
18
+ const context = createContext();
19
+ switch (command) {
20
+ case "version":
21
+ case "--version":
22
+ case "-v":
23
+ printJson({ name: "@minasoft/mina-ai-router", version });
24
+ break;
25
+ case "health":
26
+ await showHealth(context);
27
+ break;
28
+ case "verify":
29
+ runVerify();
30
+ break;
31
+ case "server":
32
+ handleServerCommand(argv.slice(3));
33
+ break;
34
+ case "codex":
35
+ startVisibleAgent("codex", "codex --no-alt-screen", argv.slice(3), context);
36
+ break;
37
+ case "claude":
38
+ startVisibleAgent("claude", "claude", argv.slice(3), context);
39
+ break;
40
+ case "register":
41
+ registerAgent(argv.slice(3), context);
42
+ break;
43
+ case "agents":
44
+ await listAgents(context);
45
+ break;
46
+ case "agent":
47
+ await showAgent(argv.slice(3), context);
48
+ break;
49
+ case "attach":
50
+ showAttach(argv.slice(3), context);
51
+ break;
52
+ case "setup-codex-pair":
53
+ setupCodexPair(argv.slice(3), context);
54
+ break;
55
+ case "serve":
56
+ serveHttp(argv.slice(3));
57
+ break;
58
+ case "ask":
59
+ await askAgent(argv.slice(3), context);
60
+ break;
61
+ case "requests":
62
+ listRequests(argv.slice(3), context);
63
+ break;
64
+ case "request":
65
+ showRequest(argv.slice(3), context);
66
+ break;
67
+ default:
68
+ throw new Error(`Unknown command "${command}". Run "mair help".`);
69
+ }
70
+ }
71
+ function handleServerCommand(args) {
72
+ const action = args[0] ?? "status";
73
+ const flags = parseFlags(args.slice(1));
74
+ switch (action) {
75
+ case "start":
76
+ startServer(flags);
77
+ break;
78
+ case "stop":
79
+ stopServer();
80
+ break;
81
+ case "status":
82
+ printJson(serverStatus());
83
+ break;
84
+ default:
85
+ throw new Error("Usage: mair server <start|stop|status> [--port 3333] [--host 127.0.0.1]");
86
+ }
87
+ }
88
+ function startServer(flags) {
89
+ const current = serverStatus();
90
+ if (current.running) {
91
+ printJson(current);
92
+ return;
93
+ }
94
+ const port = flags.port ?? process.env.MINA_HTTP_PORT ?? "3333";
95
+ const host = flags.host ?? process.env.MINA_HTTP_HOST ?? "127.0.0.1";
96
+ const serverPath = (0, node_path_1.join)(__dirname, "../../http-server/src/index.js");
97
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(serverPidPath), { recursive: true });
98
+ const logPath = flags.log ?? (0, node_path_1.join)((0, node_path_1.dirname)(serverPidPath), "mair-server.log");
99
+ const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
100
+ detached: true,
101
+ stdio: "ignore",
102
+ env: {
103
+ ...process.env,
104
+ PORT: port,
105
+ HOST: host,
106
+ MINA_ROUTER_STATE: process.env.MINA_ROUTER_STATE ?? statePath,
107
+ },
108
+ });
109
+ child.unref();
110
+ const pid = child.pid;
111
+ if (!pid) {
112
+ throw new Error("Failed to start Mina HTTP server.");
113
+ }
114
+ sleep(500);
115
+ (0, node_fs_1.writeFileSync)(serverPidPath, `${JSON.stringify({
116
+ pid,
117
+ port,
118
+ host,
119
+ statePath: process.env.MINA_ROUTER_STATE ?? statePath,
120
+ logPath,
121
+ uiUrl: `http://${host}:${port}/`,
122
+ mcpUrl: `http://${host}:${port}/mcp`,
123
+ startedAt: new Date().toISOString(),
124
+ }, null, 2)}\n`);
125
+ printJson(serverStatus());
126
+ }
127
+ function stopServer() {
128
+ const status = serverStatus();
129
+ if (!status.pid) {
130
+ printJson({ ok: true, running: false, message: "No Mina server pid file found." });
131
+ return;
132
+ }
133
+ if (status.running) {
134
+ process.kill(status.pid, "SIGTERM");
135
+ }
136
+ try {
137
+ (0, node_fs_1.unlinkSync)(serverPidPath);
138
+ }
139
+ catch {
140
+ // pid file may already be gone
141
+ }
142
+ printJson({ ok: true, running: false, stoppedPid: status.pid });
143
+ }
144
+ function serverStatus() {
145
+ if (!(0, node_fs_1.existsSync)(serverPidPath)) {
146
+ return {
147
+ running: false,
148
+ pidPath: serverPidPath,
149
+ };
150
+ }
151
+ const saved = JSON.parse((0, node_fs_1.readFileSync)(serverPidPath, "utf8"));
152
+ const running = saved.pid ? isProcessRunning(saved.pid) : false;
153
+ return {
154
+ ...saved,
155
+ running,
156
+ pidPath: serverPidPath,
157
+ };
158
+ }
159
+ function isProcessRunning(pid) {
160
+ try {
161
+ process.kill(pid, 0);
162
+ return true;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ function startVisibleAgent(agentType, defaultCommand, args, context) {
169
+ assertCommandAvailable("tmux");
170
+ const flags = parseFlags(args);
171
+ const root = flags.root ?? process.cwd();
172
+ const projectName = flags.name ?? (0, node_path_1.basename)(root);
173
+ const id = flags.id ?? sanitizeName(projectName);
174
+ const sessionId = flags.session ?? `${agentType}-${sanitizeName(projectName)}`;
175
+ const startupCommand = flags.command ?? defaultCommand;
176
+ assertCommandAvailable(startupCommand.split(/\s+/)[0]);
177
+ const shouldAttach = flags.attach !== "false" && flags["no-attach"] !== "true";
178
+ const shouldPromptRegister = flags.register !== "false" && flags["no-register"] !== "true";
179
+ const registerDelayMs = flags["register-delay-ms"] ? Number(flags["register-delay-ms"]) : 4000;
180
+ const agent = {
181
+ id,
182
+ name: id,
183
+ agentType,
184
+ transport: "tmux",
185
+ sessionId,
186
+ projectRoot: root,
187
+ startupCommand,
188
+ };
189
+ const tmux = new src_2.TmuxClient();
190
+ const existed = tmux.hasSession(sessionId);
191
+ tmux.ensureSession(agent);
192
+ if (shouldPromptRegister && !context.registry.get(id)) {
193
+ sleep(registerDelayMs);
194
+ const prompt = buildSelfRegistrationPrompt(agent);
195
+ if (agentType === "codex") {
196
+ tmux.sendCodexText(sessionId, prompt);
197
+ }
198
+ else {
199
+ tmux.sendText(sessionId, prompt);
200
+ }
201
+ }
202
+ const summary = {
203
+ agent,
204
+ existed,
205
+ attach: `tmux attach -t ${sessionId}`,
206
+ registration: shouldPromptRegister && !context.registry.get(id)
207
+ ? "registration prompt sent to agent"
208
+ : "registration prompt skipped",
209
+ };
210
+ if (!shouldAttach) {
211
+ printJson(summary);
212
+ return;
213
+ }
214
+ console.log(JSON.stringify(summary, null, 2));
215
+ (0, node_child_process_1.execFileSync)("tmux", ["attach", "-t", sessionId], {
216
+ stdio: "inherit",
217
+ });
218
+ }
219
+ function buildSelfRegistrationPrompt(agent) {
220
+ return [
221
+ "Use Mina AI Router MCP register_agent to register this visible CLI session.",
222
+ "Collect any missing context yourself when possible, but use these values as authoritative:",
223
+ `- id: ${agent.id}`,
224
+ `- name: ${agent.name}`,
225
+ `- agentType: ${agent.agentType}`,
226
+ `- transport: ${agent.transport}`,
227
+ `- sessionId: ${agent.sessionId}`,
228
+ `- projectRoot: ${agent.projectRoot}`,
229
+ `- startupCommand: ${agent.startupCommand ?? ""}`,
230
+ "",
231
+ "Before registering, create a concise capability notice for this session:",
232
+ "- Prefer capability docs in this order when present: CLAUDE.md/claude.md, AGENTS.md/agents.md, agent.md, README.md.",
233
+ "- If those files are missing, inspect package metadata and the project file tree to infer what this project/agent can help with.",
234
+ "- Set register_agent capabilitySummary to 2-5 short bullets or one short paragraph under 800 characters.",
235
+ "- Set register_agent capabilitySources to a comma-separated list of the files or project signals you used.",
236
+ "After registering, call list_agents and confirm this agent is available.",
237
+ ].join("\n");
238
+ }
239
+ async function showHealth(context) {
240
+ const agents = await context.router.listAgentStatuses();
241
+ const requests = context.router.listRequests();
242
+ const openRequests = requests.filter((request) => ["created", "sent", "waiting"].includes(request.status));
243
+ printJson({
244
+ ok: agents.every((agent) => agent.status !== "missing"),
245
+ version,
246
+ statePath,
247
+ tmuxAvailable: new src_2.TmuxClient().isAvailable(),
248
+ agents: {
249
+ total: agents.length,
250
+ available: agents.filter((agent) => agent.status === "available").length,
251
+ busy: agents.filter((agent) => agent.status === "busy").length,
252
+ missing: agents.filter((agent) => agent.status === "missing").length,
253
+ unknown: agents.filter((agent) => agent.status === "unknown").length,
254
+ },
255
+ requests: {
256
+ total: requests.length,
257
+ open: openRequests.length,
258
+ answered: requests.filter((request) => request.status === "answered").length,
259
+ failed: requests.filter((request) => ["failed", "timeout"].includes(request.status)).length,
260
+ archived: requests.filter((request) => request.status === "archived").length,
261
+ },
262
+ mcp: {
263
+ httpUrl: `http://${process.env.MINA_HTTP_HOST ?? "127.0.0.1"}:${process.env.MINA_HTTP_PORT ?? "3333"}/mcp`,
264
+ },
265
+ });
266
+ }
267
+ function runVerify() {
268
+ (0, node_child_process_1.execFileSync)("npm", ["run", "verify"], {
269
+ encoding: "utf8",
270
+ stdio: ["ignore", "pipe", "pipe"],
271
+ });
272
+ printJson({ ok: true, command: "npm run verify" });
273
+ }
274
+ function serveHttp(args) {
275
+ const flags = parseFlags(args);
276
+ const port = flags.port ?? process.env.MINA_HTTP_PORT ?? "3333";
277
+ const serverPath = (0, node_path_1.join)(__dirname, "../../http-server/src/index.js");
278
+ const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
279
+ stdio: "inherit",
280
+ env: {
281
+ ...process.env,
282
+ PORT: port,
283
+ MINA_ROUTER_STATE: process.env.MINA_ROUTER_STATE ?? statePath,
284
+ },
285
+ });
286
+ child.on("exit", (code) => {
287
+ process.exitCode = code ?? 0;
288
+ });
289
+ }
290
+ function setupCodexPair(args, context) {
291
+ 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";
294
+ const helperId = options["helper-id"] ?? "ralph";
295
+ const sessionId = options.session ?? "mina-ralph-codex";
296
+ const mcpName = options["mcp-name"] ?? "mina-ai-router";
297
+ const codexCommand = options["codex-command"] ?? "codex --no-alt-screen";
298
+ assertCommandAvailable("codex");
299
+ assertCommandAvailable("tmux");
300
+ const agent = {
301
+ id: helperId,
302
+ name: helperId,
303
+ agentType: "codex",
304
+ transport: "tmux",
305
+ sessionId,
306
+ projectRoot: helperRoot,
307
+ startupCommand: codexCommand,
308
+ };
309
+ const tmux = new src_2.TmuxClient();
310
+ tmux.ensureSession(agent);
311
+ context.registry.register(agent);
312
+ context.save();
313
+ const mcpServerPath = (0, node_path_1.join)(process.cwd(), "dist", "apps", "mcp-server", "src", "index.js");
314
+ const mcpAddCommand = [
315
+ "codex",
316
+ "mcp",
317
+ "add",
318
+ mcpName,
319
+ "--env",
320
+ `MINA_ROUTER_STATE=${statePath}`,
321
+ "--",
322
+ "node",
323
+ mcpServerPath,
324
+ ];
325
+ printJson({
326
+ ok: true,
327
+ statePath,
328
+ helper: {
329
+ id: helperId,
330
+ root: helperRoot,
331
+ session: sessionId,
332
+ attach: `tmux attach -t ${sessionId}`,
333
+ },
334
+ main: {
335
+ root: mainRoot,
336
+ startCodex: `cd ${shellQuote(mainRoot)} && codex --no-alt-screen`,
337
+ },
338
+ mcp: {
339
+ addCommand: mcpAddCommand.map(shellQuote).join(" "),
340
+ listCommand: "codex mcp list",
341
+ removeCommand: `codex mcp remove ${shellQuote(mcpName)}`,
342
+ },
343
+ nextSteps: [
344
+ "Run npm run build before installing the MCP command if you changed source files.",
345
+ "Run the mcp.addCommand once so Codex CLI can see Mina AI Router.",
346
+ "Attach to the helper session and make sure helper Codex is ready.",
347
+ "Start Codex in the main.root project.",
348
+ `Ask main Codex to call Mina AI Router target '${helperId}' for questions about the helper project.`,
349
+ ],
350
+ });
351
+ }
352
+ function createContext() {
353
+ const fileState = new src_1.FileState(statePath);
354
+ const state = fileState.load();
355
+ const registry = new src_1.AgentRegistry(state.agents);
356
+ const requestStore = new src_1.RequestStore(state.requests);
357
+ const transports = new src_2.DefaultTransportRegistry()
358
+ .register("mock", new src_2.HeadlessTransport())
359
+ .register("headless", new src_2.HeadlessTransport())
360
+ .register("tmux", new src_2.TmuxTransport())
361
+ .register("zmux", new src_2.ZmuxTransport());
362
+ const context = {
363
+ fileState,
364
+ registry,
365
+ requestStore,
366
+ save: () => fileState.save({
367
+ agents: registry.list(),
368
+ requests: requestStore.list(),
369
+ }),
370
+ };
371
+ const router = new src_1.AgentRouter({
372
+ registry,
373
+ requestStore,
374
+ transports,
375
+ onStateChanged: context.save,
376
+ });
377
+ return {
378
+ ...context,
379
+ transports,
380
+ router,
381
+ };
382
+ }
383
+ function registerAgent(args, context) {
384
+ const id = args[0];
385
+ if (!id) {
386
+ throw new Error("Usage: mair register <id> --agent <type> --transport <transport> --session <session> --root <path>");
387
+ }
388
+ const options = parseFlags(args.slice(1));
389
+ const agent = {
390
+ id,
391
+ name: options.name ?? id,
392
+ agentType: options.agent ?? "unknown",
393
+ transport: (options.transport ?? "headless"),
394
+ sessionId: options.session ?? id,
395
+ projectRoot: options.root ?? process.cwd(),
396
+ tmuxTarget: options.target,
397
+ startupCommand: options.command,
398
+ capabilitySummary: options.summary ?? options["capability-summary"],
399
+ capabilitySources: options.sources ?? options["capability-sources"],
400
+ };
401
+ context.registry.register(agent);
402
+ context.save();
403
+ printJson({ agent });
404
+ }
405
+ async function listAgents(context) {
406
+ const agents = await context.router.listAgentStatuses();
407
+ printJson({
408
+ agents,
409
+ });
410
+ }
411
+ async function showAgent(args, context) {
412
+ const id = args[0];
413
+ if (!id) {
414
+ throw new Error("Usage: mair agent <id>");
415
+ }
416
+ const agent = context.registry.require(id);
417
+ const statuses = await context.router.listAgentStatuses();
418
+ printJson({
419
+ agent,
420
+ status: statuses.find((candidate) => candidate.id === id),
421
+ attach: agent.transport === "tmux" ? `tmux attach -t ${agent.sessionId}` : undefined,
422
+ });
423
+ }
424
+ function showAttach(args, context) {
425
+ const id = args[0];
426
+ if (!id) {
427
+ throw new Error("Usage: mair attach <id>");
428
+ }
429
+ const agent = context.registry.require(id);
430
+ if (agent.transport !== "tmux") {
431
+ throw new Error(`Agent "${id}" uses transport "${agent.transport}", not tmux.`);
432
+ }
433
+ printJson({
434
+ agent: agent.id,
435
+ command: `tmux attach -t ${agent.sessionId}`,
436
+ });
437
+ }
438
+ async function askAgent(args, context) {
439
+ const target = args[0];
440
+ const task = args.slice(1).join(" ").trim();
441
+ if (!target || !task) {
442
+ throw new Error('Usage: mair ask <target> "question" [--timeout-ms 300000]');
443
+ }
444
+ const flags = parseFlags(args);
445
+ try {
446
+ const response = await context.router.callAgent({
447
+ target,
448
+ task: taskFromArgs(args.slice(1)),
449
+ timeoutMs: flags["timeout-ms"] ? Number(flags["timeout-ms"]) : undefined,
450
+ });
451
+ printJson(response);
452
+ }
453
+ finally {
454
+ context.save();
455
+ }
456
+ }
457
+ function listRequests(args, context) {
458
+ const flags = parseFlags(args);
459
+ const target = flags.target;
460
+ const requests = target
461
+ ? context.router.listRequests().filter((request) => request.targetAgent === target)
462
+ : context.router.listRequests();
463
+ printJson({ requests });
464
+ }
465
+ function showRequest(args, context) {
466
+ const requestId = args[0];
467
+ if (!requestId) {
468
+ throw new Error("Usage: mair request <request-id>");
469
+ }
470
+ printJson(context.router.getRequest(requestId));
471
+ }
472
+ function parseFlags(args) {
473
+ const flags = {};
474
+ for (let index = 0; index < args.length; index += 1) {
475
+ const token = args[index];
476
+ if (!token?.startsWith("--")) {
477
+ continue;
478
+ }
479
+ const key = token.slice(2);
480
+ const value = args[index + 1];
481
+ if (!value || value.startsWith("--")) {
482
+ flags[key] = "true";
483
+ continue;
484
+ }
485
+ flags[key] = value;
486
+ index += 1;
487
+ }
488
+ return flags;
489
+ }
490
+ function assertCommandAvailable(command) {
491
+ try {
492
+ (0, node_child_process_1.execFileSync)("which", [command], {
493
+ encoding: "utf8",
494
+ stdio: ["ignore", "pipe", "pipe"],
495
+ });
496
+ }
497
+ catch {
498
+ throw new Error(`Required command "${command}" is not available on PATH.`);
499
+ }
500
+ }
501
+ function shellQuote(value) {
502
+ if (/^[A-Za-z0-9_/:=.,@+-]+$/.test(value)) {
503
+ return value;
504
+ }
505
+ return `'${value.replace(/'/g, "'\\''")}'`;
506
+ }
507
+ function sanitizeName(value) {
508
+ return value
509
+ .trim()
510
+ .toLowerCase()
511
+ .replace(/[^a-z0-9_-]+/g, "-")
512
+ .replace(/^-+|-+$/g, "") || "agent";
513
+ }
514
+ function sleep(milliseconds) {
515
+ if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
516
+ return;
517
+ }
518
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
519
+ }
520
+ function taskFromArgs(args) {
521
+ const taskTokens = [];
522
+ for (let index = 0; index < args.length; index += 1) {
523
+ const token = args[index];
524
+ if (token === "--timeout-ms") {
525
+ index += 1;
526
+ continue;
527
+ }
528
+ taskTokens.push(token);
529
+ }
530
+ return taskTokens.join(" ").trim();
531
+ }
532
+ function printHelp() {
533
+ console.log(`Mina AI Router
534
+
535
+ Commands:
536
+ mair version
537
+ mair health
538
+ mair verify
539
+ mair server start [--port 3333] [--host 127.0.0.1]
540
+ mair server stop
541
+ mair server status
542
+ mair codex [--id <id>] [--session <tmux-session>] [--root <path>] [--no-attach] [--no-register]
543
+ mair claude [--id <id>] [--session <tmux-session>] [--root <path>] [--no-attach] [--no-register]
544
+ mair register <id> --agent <type> --transport <headless|mock|tmux|zmux> --session <session> --root <path>
545
+ mair agents
546
+ mair agent <id>
547
+ mair attach <id>
548
+ mair setup-codex-pair [--main-root <path>] [--helper-root <path>] [--helper-id <id>] [--session <tmux-session>]
549
+ mair serve [--port 3333]
550
+ mair ask <target> "question"
551
+ mair requests [--target <id>]
552
+ mair request <request-id>
553
+
554
+ Example:
555
+ mair register payment --agent gemini --transport headless --session payment --root ./payment
556
+ mair register payment --agent gemini --transport tmux --session payment --root ~/work/payment
557
+ mair setup-codex-pair
558
+ mair serve
559
+ mair server start --port 3333
560
+ mair codex
561
+ mair claude
562
+ mair ask payment "현재 payment flow를 요약해줘."
563
+
564
+ State:
565
+ Set MINA_ROUTER_STATE=/path/to/router-state.json to share state between CLI and MCP.
566
+ `);
567
+ }
568
+ function printJson(value) {
569
+ console.log(JSON.stringify(value, null, 2));
570
+ }
571
+ main(process.argv).catch((error) => {
572
+ console.error(error instanceof Error ? error.message : String(error));
573
+ process.exitCode = 1;
574
+ });