@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.
- package/README.md +53 -0
- package/dist/apps/cli/src/index.js +574 -0
- package/dist/apps/http-server/src/index.js +755 -0
- package/dist/apps/mcp-server/src/index.js +308 -0
- package/dist/packages/core/src/file-state.js +35 -0
- package/dist/packages/core/src/ids.js +8 -0
- package/dist/packages/core/src/index.js +24 -0
- package/dist/packages/core/src/prompt-envelope.js +27 -0
- package/dist/packages/core/src/registry.js +34 -0
- package/dist/packages/core/src/request-store.js +50 -0
- package/dist/packages/core/src/response-parser.js +33 -0
- package/dist/packages/core/src/router.js +100 -0
- package/dist/packages/core/src/types.js +2 -0
- package/dist/packages/mcp/src/provider.js +177 -0
- package/dist/packages/transports/src/headless/headless-transport.js +31 -0
- package/dist/packages/transports/src/index.js +22 -0
- package/dist/packages/transports/src/tmux/tmux-client.js +126 -0
- package/dist/packages/transports/src/tmux/tmux-transport.js +54 -0
- package/dist/packages/transports/src/transport-registry.js +20 -0
- package/dist/packages/transports/src/zmux/zmux-client.js +18 -0
- package/dist/packages/transports/src/zmux/zmux-transport.js +36 -0
- package/docs/DEVELOPER-START-GUIDE.md +111 -0
- package/docs/GETTING-STARTED.md +32 -0
- package/docs/HTTP-UI-MCP.md +142 -0
- package/docs/MCP-CLIENT-SETUP.md +71 -0
- package/docs/SKILL-INSTALL-GUIDE.md +96 -0
- package/docs/TROUBLESHOOTING.md +75 -0
- package/docs/USER-START-GUIDE.md +187 -0
- package/docs/assets/mair-agent-details.jpg +0 -0
- package/docs/assets/mair-live-flow.jpg +0 -0
- package/docs/assets/mair-terminal-preview.jpg +0 -0
- package/package.json +47 -0
- package/skills/mina-ai-router-agent/SKILL.md +64 -0
- 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.
|