@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
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMinaMcpProvider = createMinaMcpProvider;
4
+ const tools = [
5
+ {
6
+ name: "list_agents",
7
+ description: "List registered Mina helper agents.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {},
11
+ additionalProperties: false,
12
+ },
13
+ },
14
+ {
15
+ name: "register_agent",
16
+ description: "Register or update an agent in Mina AI Router. Use this from a visible project Codex session to connect itself to the router.",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ id: { type: "string" },
21
+ name: { type: "string" },
22
+ agentType: { type: "string" },
23
+ transport: { type: "string" },
24
+ sessionId: { type: "string" },
25
+ projectRoot: { type: "string" },
26
+ tmuxTarget: { type: "string" },
27
+ startupCommand: { type: "string" },
28
+ capabilitySummary: { type: "string" },
29
+ capabilitySources: { type: "string" },
30
+ },
31
+ required: ["id", "agentType", "transport", "sessionId", "projectRoot"],
32
+ additionalProperties: false,
33
+ },
34
+ },
35
+ {
36
+ name: "call_agent",
37
+ description: "Send a task to a registered helper agent and wait for a marker-wrapped response.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ target: { type: "string" },
42
+ task: { type: "string" },
43
+ timeoutMs: { type: "number" },
44
+ },
45
+ required: ["target", "task"],
46
+ additionalProperties: false,
47
+ },
48
+ },
49
+ {
50
+ name: "get_request_status",
51
+ description: "Return status for a Mina AI Router request.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ requestId: { type: "string" },
56
+ },
57
+ required: ["requestId"],
58
+ additionalProperties: false,
59
+ },
60
+ },
61
+ ];
62
+ function createMinaMcpProvider(context) {
63
+ return {
64
+ serverInfo: {
65
+ name: "mina-ai-router",
66
+ version: "0.1.0",
67
+ },
68
+ tools: {
69
+ async list() {
70
+ return { items: tools };
71
+ },
72
+ async call({ name, arguments: args }) {
73
+ return callTool(context, name, args ?? {});
74
+ },
75
+ },
76
+ };
77
+ }
78
+ async function callTool(context, name, args) {
79
+ switch (name) {
80
+ case "list_agents": {
81
+ const agents = await context.router.listAgentStatuses();
82
+ return jsonToolResult({ agents });
83
+ }
84
+ case "register_agent": {
85
+ try {
86
+ const agent = agentFromArgs(args);
87
+ context.registry.register(agent);
88
+ context.save();
89
+ const agents = await context.router.listAgentStatuses();
90
+ return jsonToolResult({
91
+ agent,
92
+ agents,
93
+ });
94
+ }
95
+ catch (error) {
96
+ return {
97
+ kind: "invalid_params",
98
+ message: error instanceof Error ? error.message : String(error),
99
+ };
100
+ }
101
+ }
102
+ case "call_agent": {
103
+ const target = typeof args.target === "string" ? args.target : "";
104
+ const task = typeof args.task === "string" ? args.task : "";
105
+ const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : undefined;
106
+ if (!target || !task) {
107
+ return { kind: "invalid_params", message: "call_agent requires string target and task." };
108
+ }
109
+ try {
110
+ const response = await context.router.callAgent({ target, task, timeoutMs });
111
+ return jsonToolResult(response);
112
+ }
113
+ catch (error) {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ return {
116
+ kind: "tool_error",
117
+ content: [{ type: "text", text: message }],
118
+ structuredContent: { error: message },
119
+ };
120
+ }
121
+ finally {
122
+ context.save();
123
+ }
124
+ }
125
+ case "get_request_status": {
126
+ const requestId = typeof args.requestId === "string" ? args.requestId : "";
127
+ if (!requestId) {
128
+ return { kind: "invalid_params", message: "get_request_status requires string requestId." };
129
+ }
130
+ try {
131
+ const found = context.router.getRequest(requestId);
132
+ return jsonToolResult({
133
+ requestId: found.id,
134
+ status: found.status,
135
+ error: found.error,
136
+ });
137
+ }
138
+ catch {
139
+ return { kind: "not_found", message: `Request "${requestId}" was not found.` };
140
+ }
141
+ }
142
+ default:
143
+ return { kind: "not_found", message: `Unknown tool "${name}".` };
144
+ }
145
+ }
146
+ function agentFromArgs(args) {
147
+ const id = requiredString(args.id, "id");
148
+ return {
149
+ id,
150
+ name: stringValue(args.name) ?? id,
151
+ agentType: requiredString(args.agentType, "agentType"),
152
+ transport: requiredString(args.transport, "transport"),
153
+ sessionId: requiredString(args.sessionId, "sessionId"),
154
+ projectRoot: requiredString(args.projectRoot, "projectRoot"),
155
+ tmuxTarget: stringValue(args.tmuxTarget),
156
+ startupCommand: stringValue(args.startupCommand),
157
+ capabilitySummary: stringValue(args.capabilitySummary),
158
+ capabilitySources: stringValue(args.capabilitySources),
159
+ };
160
+ }
161
+ function requiredString(value, field) {
162
+ if (typeof value !== "string" || !value.trim()) {
163
+ throw new Error(`register_agent requires string ${field}.`);
164
+ }
165
+ return value.trim();
166
+ }
167
+ function stringValue(value) {
168
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
169
+ }
170
+ function jsonToolResult(value) {
171
+ const text = JSON.stringify(value, null, 2);
172
+ return {
173
+ kind: "success",
174
+ content: [{ type: "text", text }],
175
+ structuredContent: value,
176
+ };
177
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HeadlessTransport = void 0;
4
+ class HeadlessTransport {
5
+ constructor() {
6
+ this.buffers = new Map();
7
+ }
8
+ async send(agent, input) {
9
+ this.buffers.set(agent.id, input);
10
+ }
11
+ async capture(agent) {
12
+ return this.buffers.get(agent.id) ?? "";
13
+ }
14
+ async waitForResponse(agent, requestId) {
15
+ const prompt = this.buffers.get(agent.id) ?? "";
16
+ const answer = [
17
+ `Headless response from ${agent.id}.`,
18
+ "",
19
+ "This transport is for local testing only.",
20
+ "It proves the router envelope, request lifecycle, and response parser without a live CLI session.",
21
+ "",
22
+ `Received ${prompt.length} prompt characters for request ${requestId}.`,
23
+ ].join("\n");
24
+ return [
25
+ `<<<MINA_AGENT_RESPONSE_START ${requestId}>>>`,
26
+ answer,
27
+ `<<<MINA_AGENT_RESPONSE_END ${requestId}>>>`,
28
+ ].join("\n");
29
+ }
30
+ }
31
+ exports.HeadlessTransport = HeadlessTransport;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./headless/headless-transport"), exports);
18
+ __exportStar(require("./tmux/tmux-client"), exports);
19
+ __exportStar(require("./tmux/tmux-transport"), exports);
20
+ __exportStar(require("./transport-registry"), exports);
21
+ __exportStar(require("./zmux/zmux-client"), exports);
22
+ __exportStar(require("./zmux/zmux-transport"), exports);
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TmuxClient = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ class TmuxClient {
6
+ constructor(options = {}) {
7
+ this.binary = options.binary ?? process.env.MINA_TMUX_BIN ?? "tmux";
8
+ this.captureLines = options.captureLines ?? 2000;
9
+ this.submitDelayMs = options.submitDelayMs ?? Number(process.env.MINA_TMUX_SUBMIT_DELAY_MS ?? "350");
10
+ this.submitKey = options.submitKey ?? process.env.MINA_TMUX_SUBMIT_KEY ?? "C-m";
11
+ this.inputChunkSize = options.inputChunkSize ?? Number(process.env.MINA_TMUX_INPUT_CHUNK_SIZE ?? "800");
12
+ this.clearInputKeys = options.clearInputKeys ?? splitKeys(process.env.MINA_TMUX_CLEAR_INPUT_KEYS ?? "C-u");
13
+ this.chunkDelayMs = options.chunkDelayMs ?? Number(process.env.MINA_TMUX_CHUNK_DELAY_MS ?? "50");
14
+ this.maxSubmitDelayMs = options.maxSubmitDelayMs ?? Number(process.env.MINA_TMUX_MAX_SUBMIT_DELAY_MS ?? "5000");
15
+ }
16
+ isAvailable() {
17
+ try {
18
+ this.run(["-V"]);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ hasSession(sessionId) {
26
+ try {
27
+ this.run(["has-session", "-t", sessionId]);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ killSession(sessionId) {
35
+ if (!this.hasSession(sessionId)) {
36
+ return;
37
+ }
38
+ this.run(["kill-session", "-t", sessionId]);
39
+ }
40
+ ensureSession(agent) {
41
+ if (this.hasSession(agent.sessionId)) {
42
+ return;
43
+ }
44
+ const args = ["new-session", "-d", "-s", agent.sessionId, "-x", "200", "-y", "60", "-c", agent.projectRoot];
45
+ if (agent.startupCommand) {
46
+ args.push(agent.startupCommand);
47
+ }
48
+ this.run(args);
49
+ }
50
+ sendText(target, text) {
51
+ this.run(["load-buffer", "-"], text);
52
+ this.run(["paste-buffer", "-t", target, "-d"]);
53
+ this.sleep(this.submitDelayMs);
54
+ this.run(["send-keys", "-t", target, this.submitKey]);
55
+ }
56
+ sendEnter(target) {
57
+ this.run(["send-keys", "-t", target, this.submitKey]);
58
+ }
59
+ sendCodexText(target, text) {
60
+ const prompt = asSingleLinePrompt(text);
61
+ for (const key of this.clearInputKeys) {
62
+ this.run(["send-keys", "-t", target, key]);
63
+ this.sleep(this.submitDelayMs);
64
+ }
65
+ this.sleep(this.submitDelayMs);
66
+ for (const chunk of chunks(prompt, this.inputChunkSize)) {
67
+ this.run(["send-keys", "-t", target, "-l", chunk]);
68
+ this.sleep(this.chunkDelayMs);
69
+ }
70
+ this.sleep(submitDelayFor(prompt, this.submitDelayMs, this.maxSubmitDelayMs));
71
+ this.run(["send-keys", "-t", target, this.submitKey]);
72
+ }
73
+ capture(target) {
74
+ return this.run(["capture-pane", "-t", target, "-p", "-J", "-S", `-${this.captureLines}`]);
75
+ }
76
+ attachCommand(agent) {
77
+ return `${this.binary} attach -t ${agent.sessionId}`;
78
+ }
79
+ run(args, input) {
80
+ try {
81
+ return (0, node_child_process_1.execFileSync)(this.binary, args, {
82
+ encoding: "utf8",
83
+ input,
84
+ stdio: input === undefined ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
85
+ });
86
+ }
87
+ catch (error) {
88
+ const detail = error instanceof Error ? error.message : String(error);
89
+ throw new Error(`tmux ${args.join(" ")} failed: ${detail}`);
90
+ }
91
+ }
92
+ sleep(milliseconds) {
93
+ if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
94
+ return;
95
+ }
96
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
97
+ }
98
+ }
99
+ exports.TmuxClient = TmuxClient;
100
+ function asSingleLinePrompt(text) {
101
+ return text
102
+ .split(/\r?\n/)
103
+ .map((line) => line.trim())
104
+ .filter(Boolean)
105
+ .join(" | ");
106
+ }
107
+ function chunks(text, chunkSize) {
108
+ const size = Number.isFinite(chunkSize) && chunkSize > 0 ? Math.floor(chunkSize) : 800;
109
+ const result = [];
110
+ for (let index = 0; index < text.length; index += size) {
111
+ result.push(text.slice(index, index + size));
112
+ }
113
+ return result;
114
+ }
115
+ function splitKeys(value) {
116
+ return value
117
+ .split(",")
118
+ .map((key) => key.trim())
119
+ .filter(Boolean);
120
+ }
121
+ function submitDelayFor(text, minimumDelayMs, maximumDelayMs) {
122
+ const minimum = Number.isFinite(minimumDelayMs) && minimumDelayMs > 0 ? minimumDelayMs : 350;
123
+ const maximum = Number.isFinite(maximumDelayMs) && maximumDelayMs > 0 ? maximumDelayMs : 5000;
124
+ const proportional = 1000 + text.length * 3;
125
+ return Math.min(Math.max(minimum, proportional), maximum);
126
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TmuxTransport = void 0;
4
+ const src_1 = require("../../../core/src");
5
+ const tmux_client_1 = require("./tmux-client");
6
+ const pollIntervalMs = 500;
7
+ class TmuxTransport {
8
+ constructor(client = new tmux_client_1.TmuxClient()) {
9
+ this.client = client;
10
+ }
11
+ async send(agent, input) {
12
+ this.client.ensureSession(agent);
13
+ if (agent.agentType === "codex") {
14
+ this.client.sendCodexText(targetFor(agent), input);
15
+ return;
16
+ }
17
+ this.client.sendText(targetFor(agent), input);
18
+ }
19
+ async capture(agent) {
20
+ this.client.ensureSession(agent);
21
+ return this.client.capture(targetFor(agent));
22
+ }
23
+ async waitForResponse(agent, requestId, timeoutMs) {
24
+ const deadline = Date.now() + timeoutMs;
25
+ let lastOutput = "";
26
+ while (Date.now() < deadline) {
27
+ lastOutput = await this.capture(agent);
28
+ try {
29
+ (0, src_1.parseMarkedResponse)(lastOutput, requestId);
30
+ return lastOutput;
31
+ }
32
+ catch {
33
+ await sleep(pollIntervalMs);
34
+ }
35
+ }
36
+ throw new Error(`Timed out waiting for response markers for ${requestId}. Last capture:\n${lastOutput}`);
37
+ }
38
+ async status(agent) {
39
+ if (!this.client.isAvailable()) {
40
+ return { status: "missing", detail: "tmux binary is not available" };
41
+ }
42
+ if (!this.client.hasSession(agent.sessionId)) {
43
+ return { status: "missing", detail: `tmux session "${agent.sessionId}" does not exist` };
44
+ }
45
+ return { status: "available", detail: this.client.attachCommand(agent) };
46
+ }
47
+ }
48
+ exports.TmuxTransport = TmuxTransport;
49
+ function targetFor(agent) {
50
+ return agent.tmuxTarget ?? agent.sessionId;
51
+ }
52
+ function sleep(ms) {
53
+ return new Promise((resolve) => setTimeout(resolve, ms));
54
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DefaultTransportRegistry = void 0;
4
+ class DefaultTransportRegistry {
5
+ constructor() {
6
+ this.transports = new Map();
7
+ }
8
+ register(type, transport) {
9
+ this.transports.set(type, transport);
10
+ return this;
11
+ }
12
+ get(type) {
13
+ const transport = this.transports.get(type);
14
+ if (!transport) {
15
+ throw new Error(`Transport "${type}" is not configured.`);
16
+ }
17
+ return transport;
18
+ }
19
+ }
20
+ exports.DefaultTransportRegistry = DefaultTransportRegistry;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZmuxJsonRpcClient = void 0;
4
+ class ZmuxJsonRpcClient {
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ }
8
+ async sendPrompt(sessionId, prompt) {
9
+ void sessionId;
10
+ void prompt;
11
+ throw new Error(`Zmux JSON-RPC client is not wired yet${this.options.endpoint ? ` for ${this.options.endpoint}` : ""}.`);
12
+ }
13
+ async capture(sessionId) {
14
+ void sessionId;
15
+ throw new Error(`Zmux JSON-RPC client is not wired yet${this.options.endpoint ? ` for ${this.options.endpoint}` : ""}.`);
16
+ }
17
+ }
18
+ exports.ZmuxJsonRpcClient = ZmuxJsonRpcClient;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZmuxTransport = void 0;
4
+ const src_1 = require("../../../core/src");
5
+ const zmux_client_1 = require("./zmux-client");
6
+ const pollIntervalMs = 500;
7
+ class ZmuxTransport {
8
+ constructor(client = new zmux_client_1.ZmuxJsonRpcClient()) {
9
+ this.client = client;
10
+ }
11
+ async send(agent, input) {
12
+ await this.client.sendPrompt(agent.sessionId, input);
13
+ }
14
+ async capture(agent) {
15
+ return this.client.capture(agent.sessionId);
16
+ }
17
+ async waitForResponse(agent, requestId, timeoutMs) {
18
+ const deadline = Date.now() + timeoutMs;
19
+ let lastOutput = "";
20
+ while (Date.now() < deadline) {
21
+ lastOutput = await this.capture(agent);
22
+ try {
23
+ (0, src_1.parseMarkedResponse)(lastOutput, requestId);
24
+ return lastOutput;
25
+ }
26
+ catch {
27
+ await sleep(pollIntervalMs);
28
+ }
29
+ }
30
+ throw new Error(`Timed out waiting for response markers for ${requestId}. Last capture:\n${lastOutput}`);
31
+ }
32
+ }
33
+ exports.ZmuxTransport = ZmuxTransport;
34
+ function sleep(ms) {
35
+ return new Promise((resolve) => setTimeout(resolve, ms));
36
+ }
@@ -0,0 +1,111 @@
1
+ # Developer Start Guide
2
+
3
+ This guide is for developing Mina AI Router.
4
+
5
+ ## 1. Install Dependencies
6
+
7
+ ```sh
8
+ cd mina-ai-router
9
+ npm install
10
+ ```
11
+
12
+ ## 2. Build
13
+
14
+ ```sh
15
+ npm run build
16
+ ```
17
+
18
+ ## 3. Register the Local CLI
19
+
20
+ Use `npm link` so your shell can run `mair` directly.
21
+
22
+ ```sh
23
+ npm link
24
+ mair version
25
+ ```
26
+
27
+ This links the package bin entries:
28
+
29
+ - `mair`
30
+ - `mair-mcp`
31
+ - `mair-http`
32
+
33
+ ## 4. Run the Server During Development
34
+
35
+ Preferred:
36
+
37
+ ```sh
38
+ mair server start --port 3333
39
+ mair server status
40
+ ```
41
+
42
+ Direct Node fallback:
43
+
44
+ ```sh
45
+ node dist/apps/cli/src/index.js server start --port 3333
46
+ ```
47
+
48
+ Open:
49
+
50
+ ```text
51
+ http://127.0.0.1:3333/
52
+ ```
53
+
54
+ ## 5. Run Verification
55
+
56
+ ```sh
57
+ npm run verify
58
+ ```
59
+
60
+ This runs:
61
+
62
+ - TypeScript build
63
+ - core tests
64
+ - HTTP UI smoke test
65
+ - CLI control smoke test
66
+ - tmux smoke test
67
+ - MCP smoke test
68
+ - multi-agent smoke test
69
+
70
+ ## 6. Useful Development Commands
71
+
72
+ ```sh
73
+ npm run build
74
+ npm run smoke:http
75
+ npm run smoke:mcp
76
+ npm run smoke:tmux
77
+ mair health
78
+ mair verify
79
+ ```
80
+
81
+ ## 7. Local State
82
+
83
+ By default, MAIR stores local state in:
84
+
85
+ ```text
86
+ data/router-state.json
87
+ ```
88
+
89
+ Override it with:
90
+
91
+ ```sh
92
+ export MINA_ROUTER_STATE=/path/to/router-state.json
93
+ ```
94
+
95
+ ## 8. Important Paths
96
+
97
+ - CLI: `apps/cli/src/index.ts`
98
+ - HTTP UI/server: `apps/http-server/src/index.ts`
99
+ - Browser UI HTML: `apps/http-server/src/ui.html`
100
+ - MCP provider: `packages/mcp/src/provider.ts`
101
+ - tmux transport: `packages/transports/src/tmux`
102
+ - agent registration skill: `skills/mina-ai-router-agent/SKILL.md`
103
+
104
+ ## 9. Before Committing
105
+
106
+ Run:
107
+
108
+ ```sh
109
+ npm run verify
110
+ git status --short
111
+ ```
@@ -0,0 +1,32 @@
1
+ # Getting Started
2
+
3
+ Choose the guide that matches what you are trying to do.
4
+
5
+ ## For Users
6
+
7
+ Start here if you want to run Mina AI Router, create agents, open their terminals in the browser, and make agents talk to each other.
8
+
9
+ [User Start Guide](./USER-START-GUIDE.md)
10
+
11
+ ## For Developers
12
+
13
+ Start here if you want to build, test, modify, or package this repository.
14
+
15
+ [Developer Start Guide](./DEVELOPER-START-GUIDE.md)
16
+
17
+ ## Required Setup Guides
18
+
19
+ Use these once per machine or per AI CLI profile:
20
+
21
+ - [MCP Client Setup](./MCP-CLIENT-SETUP.md): connect Codex or Claude to the local MAIR MCP server.
22
+ - [Skill Install Guide](./SKILL-INSTALL-GUIDE.md): install the MAIR registration skill for Codex or Claude.
23
+
24
+ ## Recommended First Path
25
+
26
+ 1. Install the local `mair` command.
27
+ 2. Start the MAIR server.
28
+ 3. Connect your AI CLI to MAIR MCP.
29
+ 4. Install the MAIR agent registration skill.
30
+ 5. Create an agent from the Web UI or with `mair codex` / `mair claude`.
31
+
32
+ The user guide walks through that path with screenshots.