@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.
- package/README.md +67 -16
- package/dist/apps/cli/src/index.js +1247 -43
- package/dist/apps/http-server/src/index.js +598 -46
- package/dist/apps/http-server/src/public/assets/index-Bl059Jd0.js +9 -0
- package/dist/apps/http-server/src/public/assets/index-CaPxN_Ez.css +1 -0
- package/dist/apps/http-server/src/public/index.html +16 -0
- package/dist/apps/mcp-server/src/index.js +54 -7
- package/dist/packages/core/src/capability-profile.js +145 -0
- package/dist/packages/core/src/index.js +3 -0
- package/dist/packages/core/src/mcp-preflight.js +80 -0
- package/dist/packages/core/src/registry.js +128 -3
- package/dist/packages/core/src/request-store.js +158 -0
- package/dist/packages/core/src/response-parser.js +76 -8
- package/dist/packages/core/src/router.js +408 -13
- package/dist/packages/core/src/version.js +57 -0
- package/dist/packages/mcp/src/provider.js +57 -6
- package/dist/packages/transports/src/headless/headless-transport.js +13 -8
- package/dist/packages/transports/src/tmux/tmux-client.js +334 -0
- package/dist/packages/transports/src/tmux/tmux-transport.js +10 -0
- package/docs/DEVELOPER-START-GUIDE.md +9 -1
- package/docs/GETTING-STARTED.md +10 -5
- package/docs/HTTP-UI-MCP.md +39 -13
- package/docs/MCP-CLIENT-SETUP.md +56 -3
- package/docs/SKILL-INSTALL-GUIDE.md +21 -3
- package/docs/TROUBLESHOOTING.md +47 -0
- package/docs/USER-START-GUIDE.md +155 -26
- package/docs/assets/mina-ai-router-overview.svg +109 -0
- package/package.json +19 -5
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createMinaMcpProvider = createMinaMcpProvider;
|
|
4
|
+
const src_1 = require("../../core/src");
|
|
4
5
|
const tools = [
|
|
5
6
|
{
|
|
6
7
|
name: "list_agents",
|
|
7
8
|
description: "List registered Mina helper agents.",
|
|
8
9
|
inputSchema: {
|
|
9
10
|
type: "object",
|
|
10
|
-
properties: {
|
|
11
|
+
properties: {
|
|
12
|
+
callerAgentId: { type: "string" },
|
|
13
|
+
sourceAgent: { type: "string" },
|
|
14
|
+
callerSessionFingerprint: { type: "string" },
|
|
15
|
+
},
|
|
11
16
|
additionalProperties: false,
|
|
12
17
|
},
|
|
13
18
|
},
|
|
@@ -22,11 +27,15 @@ const tools = [
|
|
|
22
27
|
agentType: { type: "string" },
|
|
23
28
|
transport: { type: "string" },
|
|
24
29
|
sessionId: { type: "string" },
|
|
30
|
+
sessionFingerprint: { type: "string" },
|
|
25
31
|
projectRoot: { type: "string" },
|
|
26
32
|
tmuxTarget: { type: "string" },
|
|
27
33
|
startupCommand: { type: "string" },
|
|
28
34
|
capabilitySummary: { type: "string" },
|
|
29
35
|
capabilitySources: { type: "string" },
|
|
36
|
+
capabilitySource: { type: "string" },
|
|
37
|
+
capabilityUpdatedAt: { type: "string" },
|
|
38
|
+
lastCapabilityRefreshAt: { type: "string" },
|
|
30
39
|
},
|
|
31
40
|
required: ["id", "agentType", "transport", "sessionId", "projectRoot"],
|
|
32
41
|
additionalProperties: false,
|
|
@@ -40,6 +49,10 @@ const tools = [
|
|
|
40
49
|
properties: {
|
|
41
50
|
target: { type: "string" },
|
|
42
51
|
task: { type: "string" },
|
|
52
|
+
sourceAgent: { type: "string" },
|
|
53
|
+
callerAgentId: { type: "string" },
|
|
54
|
+
callerSessionFingerprint: { type: "string" },
|
|
55
|
+
allowSelfCall: { type: "boolean" },
|
|
43
56
|
timeoutMs: { type: "number" },
|
|
44
57
|
},
|
|
45
58
|
required: ["target", "task"],
|
|
@@ -63,7 +76,7 @@ function createMinaMcpProvider(context) {
|
|
|
63
76
|
return {
|
|
64
77
|
serverInfo: {
|
|
65
78
|
name: "mina-ai-router",
|
|
66
|
-
version:
|
|
79
|
+
version: (0, src_1.packageVersion)(),
|
|
67
80
|
},
|
|
68
81
|
tools: {
|
|
69
82
|
async list() {
|
|
@@ -78,17 +91,29 @@ function createMinaMcpProvider(context) {
|
|
|
78
91
|
async function callTool(context, name, args) {
|
|
79
92
|
switch (name) {
|
|
80
93
|
case "list_agents": {
|
|
81
|
-
const
|
|
94
|
+
const caller = resolveCallerAgent(context.registry, args);
|
|
95
|
+
const agents = await context.router.listAgentStatuses({ callerAgentId: caller?.id });
|
|
82
96
|
return jsonToolResult({ agents });
|
|
83
97
|
}
|
|
84
98
|
case "register_agent": {
|
|
85
99
|
try {
|
|
86
100
|
const agent = agentFromArgs(args);
|
|
87
|
-
|
|
101
|
+
const now = new Date().toISOString();
|
|
102
|
+
const registered = context.registry.register({
|
|
103
|
+
...agent,
|
|
104
|
+
bootstrapStatus: "ready",
|
|
105
|
+
registrationSource: "mcp",
|
|
106
|
+
registrationStatus: "confirmed",
|
|
107
|
+
lastRegistrationAttemptAt: now,
|
|
108
|
+
confirmedByAgentAt: now,
|
|
109
|
+
sessionFingerprint: agent.sessionFingerprint ?? agent.sessionId,
|
|
110
|
+
}, {
|
|
111
|
+
capabilitySource: agent.capabilitySummary || agent.capabilitySources ? "generated" : undefined,
|
|
112
|
+
});
|
|
88
113
|
context.save();
|
|
89
114
|
const agents = await context.router.listAgentStatuses();
|
|
90
115
|
return jsonToolResult({
|
|
91
|
-
agent,
|
|
116
|
+
agent: registered,
|
|
92
117
|
agents,
|
|
93
118
|
});
|
|
94
119
|
}
|
|
@@ -103,11 +128,19 @@ async function callTool(context, name, args) {
|
|
|
103
128
|
const target = typeof args.target === "string" ? args.target : "";
|
|
104
129
|
const task = typeof args.task === "string" ? args.task : "";
|
|
105
130
|
const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : undefined;
|
|
131
|
+
const caller = resolveCallerAgent(context.registry, args);
|
|
132
|
+
const allowSelfCall = args.allowSelfCall === true;
|
|
106
133
|
if (!target || !task) {
|
|
107
134
|
return { kind: "invalid_params", message: "call_agent requires string target and task." };
|
|
108
135
|
}
|
|
109
136
|
try {
|
|
110
|
-
const response = await context.router.callAgent({
|
|
137
|
+
const response = await context.router.callAgent({
|
|
138
|
+
sourceAgent: caller?.id,
|
|
139
|
+
target,
|
|
140
|
+
task,
|
|
141
|
+
timeoutMs,
|
|
142
|
+
allowSelfCall,
|
|
143
|
+
});
|
|
111
144
|
return jsonToolResult(response);
|
|
112
145
|
}
|
|
113
146
|
catch (error) {
|
|
@@ -133,6 +166,9 @@ async function callTool(context, name, args) {
|
|
|
133
166
|
requestId: found.id,
|
|
134
167
|
status: found.status,
|
|
135
168
|
error: found.error,
|
|
169
|
+
diagnosticStatus: found.diagnosticStatus,
|
|
170
|
+
parserDiagnostics: found.parserDiagnostics,
|
|
171
|
+
rawEvidence: found.rawEvidence,
|
|
136
172
|
});
|
|
137
173
|
}
|
|
138
174
|
catch {
|
|
@@ -143,6 +179,14 @@ async function callTool(context, name, args) {
|
|
|
143
179
|
return { kind: "not_found", message: `Unknown tool "${name}".` };
|
|
144
180
|
}
|
|
145
181
|
}
|
|
182
|
+
function resolveCallerAgent(registry, args) {
|
|
183
|
+
const callerAgentId = stringValue(args.callerAgentId) ?? stringValue(args.sourceAgent);
|
|
184
|
+
if (callerAgentId) {
|
|
185
|
+
return registry.get(callerAgentId);
|
|
186
|
+
}
|
|
187
|
+
const fingerprint = stringValue(args.callerSessionFingerprint);
|
|
188
|
+
return fingerprint ? registry.findBySessionFingerprint(fingerprint) : undefined;
|
|
189
|
+
}
|
|
146
190
|
function agentFromArgs(args) {
|
|
147
191
|
const id = requiredString(args.id, "id");
|
|
148
192
|
return {
|
|
@@ -151,11 +195,15 @@ function agentFromArgs(args) {
|
|
|
151
195
|
agentType: requiredString(args.agentType, "agentType"),
|
|
152
196
|
transport: requiredString(args.transport, "transport"),
|
|
153
197
|
sessionId: requiredString(args.sessionId, "sessionId"),
|
|
198
|
+
sessionFingerprint: stringValue(args.sessionFingerprint) ?? stringValue(args.sessionId),
|
|
154
199
|
projectRoot: requiredString(args.projectRoot, "projectRoot"),
|
|
155
200
|
tmuxTarget: stringValue(args.tmuxTarget),
|
|
156
201
|
startupCommand: stringValue(args.startupCommand),
|
|
157
202
|
capabilitySummary: stringValue(args.capabilitySummary),
|
|
158
203
|
capabilitySources: stringValue(args.capabilitySources),
|
|
204
|
+
capabilitySource: capabilitySourceValue(args.capabilitySource),
|
|
205
|
+
capabilityUpdatedAt: stringValue(args.capabilityUpdatedAt),
|
|
206
|
+
lastCapabilityRefreshAt: stringValue(args.lastCapabilityRefreshAt),
|
|
159
207
|
};
|
|
160
208
|
}
|
|
161
209
|
function requiredString(value, field) {
|
|
@@ -167,6 +215,9 @@ function requiredString(value, field) {
|
|
|
167
215
|
function stringValue(value) {
|
|
168
216
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
169
217
|
}
|
|
218
|
+
function capabilitySourceValue(value) {
|
|
219
|
+
return value === "manual" || value === "generated" ? value : undefined;
|
|
220
|
+
}
|
|
170
221
|
function jsonToolResult(value) {
|
|
171
222
|
const text = JSON.stringify(value, null, 2);
|
|
172
223
|
return {
|
|
@@ -13,14 +13,19 @@ class HeadlessTransport {
|
|
|
13
13
|
}
|
|
14
14
|
async waitForResponse(agent, requestId) {
|
|
15
15
|
const prompt = this.buffers.get(agent.id) ?? "";
|
|
16
|
-
const answer =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const answer = prompt.includes("capabilitySummary") && prompt.includes("capabilitySources")
|
|
17
|
+
? JSON.stringify({
|
|
18
|
+
capabilitySummary: `Headless capability refresh for ${agent.id}.`,
|
|
19
|
+
capabilitySources: "headless transport prompt",
|
|
20
|
+
})
|
|
21
|
+
: [
|
|
22
|
+
`Headless response from ${agent.id}.`,
|
|
23
|
+
"",
|
|
24
|
+
"This transport is for local testing only.",
|
|
25
|
+
"It proves the router envelope, request lifecycle, and response parser without a live CLI session.",
|
|
26
|
+
"",
|
|
27
|
+
`Received ${prompt.length} prompt characters for request ${requestId}.`,
|
|
28
|
+
].join("\n");
|
|
24
29
|
return [
|
|
25
30
|
`<<<MINA_AGENT_RESPONSE_START ${requestId}>>>`,
|
|
26
31
|
answer,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TmuxClient = void 0;
|
|
4
|
+
exports.detectAgentPermissionPrompt = detectAgentPermissionPrompt;
|
|
5
|
+
exports.detectAgentBootstrapPrompt = detectAgentBootstrapPrompt;
|
|
4
6
|
const node_child_process_1 = require("node:child_process");
|
|
5
7
|
class TmuxClient {
|
|
6
8
|
constructor(options = {}) {
|
|
@@ -56,6 +58,9 @@ class TmuxClient {
|
|
|
56
58
|
sendEnter(target) {
|
|
57
59
|
this.run(["send-keys", "-t", target, this.submitKey]);
|
|
58
60
|
}
|
|
61
|
+
sendInterrupt(target) {
|
|
62
|
+
this.run(["send-keys", "-t", target, "C-c"]);
|
|
63
|
+
}
|
|
59
64
|
sendCodexText(target, text) {
|
|
60
65
|
const prompt = asSingleLinePrompt(text);
|
|
61
66
|
for (const key of this.clearInputKeys) {
|
|
@@ -97,6 +102,335 @@ class TmuxClient {
|
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
exports.TmuxClient = TmuxClient;
|
|
105
|
+
function detectAgentPermissionPrompt(agent, capture) {
|
|
106
|
+
const prompt = detectAgentBootstrapPrompt(agent, capture);
|
|
107
|
+
return prompt?.kind === "client-update"
|
|
108
|
+
? undefined
|
|
109
|
+
: prompt;
|
|
110
|
+
}
|
|
111
|
+
function detectAgentBootstrapPrompt(agent, capture) {
|
|
112
|
+
const promptWindow = recentPromptWindow(capture);
|
|
113
|
+
const latestSegment = latestInteractiveSegment(promptWindow);
|
|
114
|
+
const normalized = latestSegment.replace(/\s+/g, " ").trim();
|
|
115
|
+
if (!normalized) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (agent.agentType === "codex" && hasActiveCodexUpdatePrompt(latestSegment)) {
|
|
119
|
+
return {
|
|
120
|
+
client: "codex",
|
|
121
|
+
kind: "client-update",
|
|
122
|
+
message: "Codex is waiting at an update prompt before Mina can continue registration.",
|
|
123
|
+
action: `Attach with "tmux attach -t ${agent.sessionId}" and choose a safe update option, or use Mina's Skip Codex Update action when available.`,
|
|
124
|
+
evidence: promptEvidence(latestSegment, [codexUpdateBannerPattern, ...codexUpdateChoicePatterns]),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (agent.agentType === "codex" && hasCodexMcpRegistrationApproval(normalized, agent)) {
|
|
128
|
+
return {
|
|
129
|
+
client: "codex",
|
|
130
|
+
kind: "codex-mcp-registration-approval",
|
|
131
|
+
message: "Codex is waiting for approval of a Mina MCP registration or verification call.",
|
|
132
|
+
action: `Review the Mina MCP call, then approve option 1 in Mina or attach with "tmux attach -t ${agent.sessionId}".`,
|
|
133
|
+
evidence: promptEvidence(latestSegment, codexMcpApprovalPatterns),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (agent.agentType === "codex" && hasCodexTrustPrompt(normalized)) {
|
|
137
|
+
return {
|
|
138
|
+
client: "codex",
|
|
139
|
+
kind: "directory-trust",
|
|
140
|
+
message: "Codex is waiting for directory trust approval before it can work in this project.",
|
|
141
|
+
action: `Review the project directory, then attach with "tmux attach -t ${agent.sessionId}" or press Send Enter in Mina to approve the prompt.`,
|
|
142
|
+
evidence: promptEvidence(promptWindow, codexTrustPatterns),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (agent.agentType === "claude" && hasClaudeMcpRegistrationApproval(normalized, agent)) {
|
|
146
|
+
return {
|
|
147
|
+
client: "claude",
|
|
148
|
+
kind: "mcp-registration-approval",
|
|
149
|
+
message: "Claude is waiting for approval of a scoped Mina MCP call.",
|
|
150
|
+
action: `Review the Mina MCP call, then approve option 1 in Mina or attach with "tmux attach -t ${agent.sessionId}".`,
|
|
151
|
+
evidence: promptEvidence(latestSegment, claudeMcpApprovalPatterns),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (agent.agentType === "claude" && hasClaudeFolderTrustPrompt(normalized, agent.projectRoot)) {
|
|
155
|
+
return {
|
|
156
|
+
client: "claude",
|
|
157
|
+
kind: "claude-folder-trust",
|
|
158
|
+
message: "Claude is waiting for trust approval of this project folder.",
|
|
159
|
+
action: `Review the folder path, then approve option 1 in Mina or attach with "tmux attach -t ${agent.sessionId}".`,
|
|
160
|
+
evidence: promptEvidence(latestSegment, claudeFolderTrustPatterns),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (agent.agentType === "claude" && hasClaudeScopedRegistrationApproval(normalized, agent.projectRoot)) {
|
|
164
|
+
return {
|
|
165
|
+
client: "claude",
|
|
166
|
+
kind: "scoped-command-approval",
|
|
167
|
+
message: "Claude is waiting for approval of a Mina registration command scoped to this project.",
|
|
168
|
+
action: `Review the scoped command, then approve it in Mina or attach with "tmux attach -t ${agent.sessionId}".`,
|
|
169
|
+
evidence: promptEvidence(promptWindow, claudeScopedRegistrationApprovalPatterns),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (agent.agentType === "claude" && hasClaudePermissionPrompt(normalized)) {
|
|
173
|
+
return {
|
|
174
|
+
client: "claude",
|
|
175
|
+
kind: "permission-approval",
|
|
176
|
+
message: "Claude is waiting for a permission or trust approval before it can work in this project.",
|
|
177
|
+
action: `Review the project directory, then attach with "tmux attach -t ${agent.sessionId}" and approve the Claude prompt.`,
|
|
178
|
+
evidence: promptEvidence(promptWindow, claudePermissionPatterns),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
function hasCodexTrustPrompt(value) {
|
|
184
|
+
return codexTrustPatterns.some((pattern) => pattern.test(value));
|
|
185
|
+
}
|
|
186
|
+
function hasActiveCodexUpdatePrompt(value) {
|
|
187
|
+
const lines = promptLines(value);
|
|
188
|
+
const latestNormalPromptIndex = lastIndexWhere(lines, (line) => codexNormalPromptPattern.test(line));
|
|
189
|
+
const latestUpdateChoiceIndex = lastIndexWhere(lines, (line) => codexUpdateChoiceLinePatterns.some((pattern) => pattern.test(line)));
|
|
190
|
+
if (latestNormalPromptIndex > latestUpdateChoiceIndex) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return codexUpdateBannerPattern.test(value)
|
|
194
|
+
&& codexUpdateChoicePatterns.every((pattern) => pattern.test(value));
|
|
195
|
+
}
|
|
196
|
+
function hasClaudePermissionPrompt(value) {
|
|
197
|
+
return hasClaudeInteractiveApprovalUi(value)
|
|
198
|
+
&& claudePermissionPatterns.some((pattern) => pattern.test(value));
|
|
199
|
+
}
|
|
200
|
+
function hasClaudeScopedRegistrationApproval(value, projectRoot) {
|
|
201
|
+
if (!projectRoot || !claudeScopedRegistrationApprovalPatterns.some((pattern) => pattern.test(value))) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const tmuxContextProbe = /tmux\s+display-message\s+-p/i.test(value)
|
|
205
|
+
&& /\bpwd\b/i.test(value);
|
|
206
|
+
if (tmuxContextProbe) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const safetyValue = value.replace(safeDevNullRedirectionPattern, "");
|
|
210
|
+
if (unsafeShellCommandPatterns.some((pattern) => pattern.test(safetyValue))) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const projectScopedRead = includesProjectRoot(value, projectRoot)
|
|
214
|
+
&& claudeReadOnlyRegistrationCommandPatterns.some((pattern) => pattern.test(value));
|
|
215
|
+
const cwdScopedRead = !absolutePathPattern.test(value)
|
|
216
|
+
&& claudeReadOnlyRegistrationCommandPatterns.some((pattern) => pattern.test(value));
|
|
217
|
+
return projectScopedRead || cwdScopedRead;
|
|
218
|
+
}
|
|
219
|
+
function hasCodexMcpRegistrationApproval(value, agent) {
|
|
220
|
+
if (!codexMcpApprovalPatterns.every((pattern) => pattern.test(value))) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
return hasCodexRegisterAgentApproval(value, agent)
|
|
224
|
+
|| hasCodexListAgentsApproval(value, agent);
|
|
225
|
+
}
|
|
226
|
+
function hasClaudeMcpRegistrationApproval(value, agent) {
|
|
227
|
+
if (!claudeMcpApprovalPatterns.every((pattern) => pattern.test(value))) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return hasClaudeRegisterAgentApproval(value, agent)
|
|
231
|
+
|| hasClaudeListAgentsApproval(value, agent)
|
|
232
|
+
|| hasClaudeCallAgentApproval(value, agent);
|
|
233
|
+
}
|
|
234
|
+
function hasCodexRegisterAgentApproval(value, agent) {
|
|
235
|
+
return /mina-ai-router\.register_agent/i.test(value)
|
|
236
|
+
&& /run tool ["“]register_agent["”]\?/i.test(value)
|
|
237
|
+
&& value.includes(agent.id)
|
|
238
|
+
&& value.includes(agent.sessionId)
|
|
239
|
+
&& includesProjectRoot(value, agent.projectRoot);
|
|
240
|
+
}
|
|
241
|
+
function hasCodexListAgentsApproval(value, agent) {
|
|
242
|
+
return /mina-ai-router\.list_agents/i.test(value)
|
|
243
|
+
&& /run tool ["“]list_agents["”]\?/i.test(value)
|
|
244
|
+
&& value.includes(agent.id)
|
|
245
|
+
&& value.includes(agent.sessionFingerprint ?? agent.sessionId);
|
|
246
|
+
}
|
|
247
|
+
function hasClaudeRegisterAgentApproval(value, agent) {
|
|
248
|
+
return /mina-ai-router\s+-\s+register_agent/i.test(value)
|
|
249
|
+
&& value.includes(agent.id)
|
|
250
|
+
&& value.includes(agent.sessionId)
|
|
251
|
+
&& includesProjectRoot(value, agent.projectRoot);
|
|
252
|
+
}
|
|
253
|
+
function hasClaudeListAgentsApproval(value, agent) {
|
|
254
|
+
return /mina-ai-router\s+-\s+list_agents/i.test(value)
|
|
255
|
+
&& (includesProjectRoot(value, agent.projectRoot)
|
|
256
|
+
|| value.includes(agent.id)
|
|
257
|
+
|| value.includes(agent.sessionFingerprint ?? agent.sessionId));
|
|
258
|
+
}
|
|
259
|
+
function hasClaudeCallAgentApproval(value, agent) {
|
|
260
|
+
return /mina-ai-router\s+-\s+call_agent/i.test(value)
|
|
261
|
+
&& (includesProjectRoot(value, agent.projectRoot)
|
|
262
|
+
|| value.includes(agent.id)
|
|
263
|
+
|| value.includes(agent.sessionFingerprint ?? agent.sessionId));
|
|
264
|
+
}
|
|
265
|
+
function hasClaudeFolderTrustPrompt(value, projectRoot) {
|
|
266
|
+
return claudeFolderTrustPatterns.every((pattern) => pattern.test(value))
|
|
267
|
+
&& includesProjectRoot(value, projectRoot);
|
|
268
|
+
}
|
|
269
|
+
const codexUpdateBannerPattern = /update available!\s+\S+\s*->\s*\S+/i;
|
|
270
|
+
const codexUpdateChoicePatterns = [
|
|
271
|
+
/(?:^|\s)1[.)]?\s*update now/i,
|
|
272
|
+
/(?:^|\s)2[.)]?\s*skip(?:\s|$)/i,
|
|
273
|
+
];
|
|
274
|
+
const codexUpdateChoiceLinePatterns = [
|
|
275
|
+
/^[›>\s]*1[.)]?\s*update now/i,
|
|
276
|
+
/^[›>\s]*2[.)]?\s*skip(?:\s|$)/i,
|
|
277
|
+
/^[›>\s]*3[.)]?\s*skip until next version/i,
|
|
278
|
+
];
|
|
279
|
+
const codexNormalPromptPattern = /^›\s+(?!1[.)]?\s*update now|2[.)]?\s*skip(?:\s|$)|3[.)]?\s*skip until next version).+/i;
|
|
280
|
+
const codexTrustPatterns = [
|
|
281
|
+
/do you trust the contents of this directory\?/i,
|
|
282
|
+
/yes,\s*continue/i,
|
|
283
|
+
/codex.+(?:trust|permission|approval)/i,
|
|
284
|
+
];
|
|
285
|
+
const claudePermissionPatterns = [
|
|
286
|
+
/claude.+(?:permission|approval|trust)/i,
|
|
287
|
+
/(?:allow|approve).+claude/i,
|
|
288
|
+
/do you trust.+(?:folder|directory|workspace|project)/i,
|
|
289
|
+
/do you want to proceed\?/i,
|
|
290
|
+
/permission.+(?:press enter|continue|approve|allow)/i,
|
|
291
|
+
];
|
|
292
|
+
const claudeFolderTrustPatterns = [
|
|
293
|
+
/accessing workspace:/i,
|
|
294
|
+
/yes,\s*i trust this folder/i,
|
|
295
|
+
/enter to confirm/i,
|
|
296
|
+
];
|
|
297
|
+
const claudeMcpApprovalPatterns = [
|
|
298
|
+
/mina-ai-router\s+-\s+(?:register_agent|list_agents|call_agent)/i,
|
|
299
|
+
/do you want to proceed\?/i,
|
|
300
|
+
/1\.\s*yes/i,
|
|
301
|
+
/\(mcp\)/i,
|
|
302
|
+
];
|
|
303
|
+
const codexMcpApprovalPatterns = [
|
|
304
|
+
/mina-ai-router\.(?:register_agent|list_agents)/i,
|
|
305
|
+
/allow the mina-ai-router mcp server to run tool ["“](?:register_agent|list_agents)["”]\?/i,
|
|
306
|
+
/1[.)]\s*allow/i,
|
|
307
|
+
/enter to submit/i,
|
|
308
|
+
];
|
|
309
|
+
const claudeScopedRegistrationApprovalPatterns = [
|
|
310
|
+
/bash command/i,
|
|
311
|
+
/(?:manual approval required|compound command contains cd|contains simple_expansion|this command requires approval|uses shell operators that require approval)/i,
|
|
312
|
+
];
|
|
313
|
+
const claudeReadOnlyRegistrationCommandPatterns = [
|
|
314
|
+
/mina-ai-router-agent/i,
|
|
315
|
+
/mina ai router/i,
|
|
316
|
+
/register_agent/i,
|
|
317
|
+
/list_agents/i,
|
|
318
|
+
/\bls\s+-la\b/i,
|
|
319
|
+
/\bpwd\b/i,
|
|
320
|
+
/tmux\s+display-message\s+-p/i,
|
|
321
|
+
/\b(?:cat|head|find|rg)\b/i,
|
|
322
|
+
];
|
|
323
|
+
const unsafeShellCommandPatterns = [
|
|
324
|
+
/\brm\s+-/i,
|
|
325
|
+
/\bsudo\b/i,
|
|
326
|
+
/\bchmod\b/i,
|
|
327
|
+
/\bchown\b/i,
|
|
328
|
+
/\b(?:cp|mv|touch|mkdir|rmdir)\b/i,
|
|
329
|
+
/\bcurl\b/i,
|
|
330
|
+
/\bwget\b/i,
|
|
331
|
+
/\bnpm\s+(?:install|i)\b/i,
|
|
332
|
+
/\bpnpm\s+(?:install|add)\b/i,
|
|
333
|
+
/\byarn\s+add\b/i,
|
|
334
|
+
/(?:^|\s)\.\.(?:\/|\s|$)/i,
|
|
335
|
+
/>\s*(?:\/|~|\.)/i,
|
|
336
|
+
];
|
|
337
|
+
const safeDevNullRedirectionPattern = /\b[12]?>\s*\/dev\/null\b/g;
|
|
338
|
+
const absolutePathPattern = /(?:^|[\s"'])(?:\/|~\/)/;
|
|
339
|
+
function hasClaudeInteractiveApprovalUi(value) {
|
|
340
|
+
return /do you want to proceed\?/i.test(value) && /(?:❯\s*)?1\.\s*(?:yes|allow)/i.test(value)
|
|
341
|
+
|| /enter to confirm/i.test(value)
|
|
342
|
+
|| /press enter to continue/i.test(value)
|
|
343
|
+
|| /yes,\s*i trust this folder/i.test(value);
|
|
344
|
+
}
|
|
345
|
+
function recentPromptWindow(capture) {
|
|
346
|
+
return capture
|
|
347
|
+
.split(/\r?\n/)
|
|
348
|
+
.slice(-80)
|
|
349
|
+
.join("\n");
|
|
350
|
+
}
|
|
351
|
+
function latestInteractiveSegment(capture) {
|
|
352
|
+
const lines = capture.split(/\r?\n/);
|
|
353
|
+
const trimmed = lines.map((line) => line.trim());
|
|
354
|
+
const latestCompletionIndex = lastIndexWhere(trimmed, (line) => /^(ready|trusted-|approved-|codex prompt ready|selected:)/i.test(line));
|
|
355
|
+
const latestPromptishIndex = lastIndexWhere(trimmed, (line) => /^›\s+/.test(line)
|
|
356
|
+
|| /do you want to proceed\?/i.test(line)
|
|
357
|
+
|| /quick safety check:/i.test(line)
|
|
358
|
+
|| /bash command/i.test(line)
|
|
359
|
+
|| /update available!/i.test(line)
|
|
360
|
+
|| /do you trust the contents of this directory\?/i.test(line));
|
|
361
|
+
if (latestCompletionIndex > latestPromptishIndex) {
|
|
362
|
+
return lines.slice(latestCompletionIndex).join("\n");
|
|
363
|
+
}
|
|
364
|
+
const latestNormalPromptIndex = lastIndexWhere(trimmed, (line) => codexNormalPromptPattern.test(line));
|
|
365
|
+
const latestUpdateChoiceIndex = lastIndexWhere(trimmed, (line) => codexUpdateChoiceLinePatterns.some((pattern) => pattern.test(line)));
|
|
366
|
+
const latestUpdateBannerIndex = lastIndexWhere(trimmed, (line) => codexUpdateBannerPattern.test(line));
|
|
367
|
+
const latestCodexMcpToolIndex = lastIndexWhere(trimmed, (line) => /mina-ai-router\.(?:register_agent|list_agents)/i.test(line));
|
|
368
|
+
if (latestCodexMcpToolIndex >= 0) {
|
|
369
|
+
const callingIndex = lastIndexWhere(trimmed.slice(0, latestCodexMcpToolIndex + 1), (line) => /calling/i.test(line));
|
|
370
|
+
return lines.slice(Math.max(0, callingIndex >= 0 ? callingIndex : latestCodexMcpToolIndex)).join("\n");
|
|
371
|
+
}
|
|
372
|
+
if (latestNormalPromptIndex > latestUpdateChoiceIndex && latestNormalPromptIndex > latestUpdateBannerIndex) {
|
|
373
|
+
return lines.slice(latestNormalPromptIndex).join("\n");
|
|
374
|
+
}
|
|
375
|
+
const latestMcpToolIndex = lastIndexWhere(trimmed, (line) => /mina-ai-router\s+-\s+(?:register_agent|list_agents|call_agent)/i.test(line));
|
|
376
|
+
if (latestMcpToolIndex >= 0) {
|
|
377
|
+
const toolUseIndex = lastIndexWhere(trimmed.slice(0, latestMcpToolIndex + 1), (line) => /tool use/i.test(line));
|
|
378
|
+
return lines.slice(Math.max(0, toolUseIndex >= 0 ? toolUseIndex : latestMcpToolIndex)).join("\n");
|
|
379
|
+
}
|
|
380
|
+
const latestWorkspaceIndex = lastIndexWhere(trimmed, (line) => /accessing workspace:/i.test(line));
|
|
381
|
+
if (latestWorkspaceIndex >= 0) {
|
|
382
|
+
return lines.slice(latestWorkspaceIndex).join("\n");
|
|
383
|
+
}
|
|
384
|
+
const latestBashIndex = lastIndexWhere(trimmed, (line) => /bash command/i.test(line));
|
|
385
|
+
if (latestBashIndex >= 0) {
|
|
386
|
+
return lines.slice(latestBashIndex).join("\n");
|
|
387
|
+
}
|
|
388
|
+
if (latestUpdateBannerIndex >= 0) {
|
|
389
|
+
return lines.slice(latestUpdateBannerIndex).join("\n");
|
|
390
|
+
}
|
|
391
|
+
return capture;
|
|
392
|
+
}
|
|
393
|
+
function promptLines(value) {
|
|
394
|
+
return value
|
|
395
|
+
.split(/\r?\n/)
|
|
396
|
+
.map((line) => line.trim())
|
|
397
|
+
.filter(Boolean);
|
|
398
|
+
}
|
|
399
|
+
function lastIndexWhere(values, predicate) {
|
|
400
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
401
|
+
if (predicate(values[index])) {
|
|
402
|
+
return index;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return -1;
|
|
406
|
+
}
|
|
407
|
+
function includesProjectRoot(value, projectRoot) {
|
|
408
|
+
return projectRootAliases(projectRoot).some((alias) => value.includes(alias));
|
|
409
|
+
}
|
|
410
|
+
function projectRootAliases(projectRoot) {
|
|
411
|
+
const aliases = new Set([projectRoot]);
|
|
412
|
+
if (projectRoot.startsWith("/tmp/")) {
|
|
413
|
+
aliases.add(`/private${projectRoot}`);
|
|
414
|
+
}
|
|
415
|
+
if (projectRoot.startsWith("/var/")) {
|
|
416
|
+
aliases.add(`/private${projectRoot}`);
|
|
417
|
+
}
|
|
418
|
+
if (projectRoot.startsWith("/private/tmp/")) {
|
|
419
|
+
aliases.add(projectRoot.replace(/^\/private/, ""));
|
|
420
|
+
}
|
|
421
|
+
if (projectRoot.startsWith("/private/var/")) {
|
|
422
|
+
aliases.add(projectRoot.replace(/^\/private/, ""));
|
|
423
|
+
}
|
|
424
|
+
return [...aliases].filter(Boolean);
|
|
425
|
+
}
|
|
426
|
+
function promptEvidence(capture, patterns) {
|
|
427
|
+
const lines = capture
|
|
428
|
+
.split(/\r?\n/)
|
|
429
|
+
.map((line) => line.trim())
|
|
430
|
+
.filter(Boolean);
|
|
431
|
+
const matched = lines.find((line) => patterns.some((pattern) => pattern.test(line)));
|
|
432
|
+
return (matched ?? lines.slice(-1)[0] ?? "").slice(0, 240);
|
|
433
|
+
}
|
|
100
434
|
function asSingleLinePrompt(text) {
|
|
101
435
|
return text
|
|
102
436
|
.split(/\r?\n/)
|
|
@@ -42,6 +42,16 @@ class TmuxTransport {
|
|
|
42
42
|
if (!this.client.hasSession(agent.sessionId)) {
|
|
43
43
|
return { status: "missing", detail: `tmux session "${agent.sessionId}" does not exist` };
|
|
44
44
|
}
|
|
45
|
+
const capture = this.client.capture(targetFor(agent));
|
|
46
|
+
const bootstrapPrompt = (0, tmux_client_1.detectAgentBootstrapPrompt)(agent, capture);
|
|
47
|
+
if (bootstrapPrompt) {
|
|
48
|
+
return {
|
|
49
|
+
status: "needs-attention",
|
|
50
|
+
detail: `${bootstrapPrompt.message} ${bootstrapPrompt.action}`,
|
|
51
|
+
bootstrapStatus: bootstrapPrompt.kind === "client-update" ? "client-update-required" : "permission-required",
|
|
52
|
+
permissionPrompt: bootstrapPrompt,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
45
55
|
return { status: "available", detail: this.client.attachCommand(agent) };
|
|
46
56
|
}
|
|
47
57
|
}
|
|
@@ -75,9 +75,15 @@ npm run smoke:http
|
|
|
75
75
|
npm run smoke:mcp
|
|
76
76
|
npm run smoke:tmux
|
|
77
77
|
mair health
|
|
78
|
+
npm run verify
|
|
78
79
|
mair verify
|
|
79
80
|
```
|
|
80
81
|
|
|
82
|
+
`npm run verify` is the checkout test suite. A linked checkout may also make
|
|
83
|
+
`mair verify` run that same suite from the Mina package root. In an installed
|
|
84
|
+
package, `mair verify` is the CLI self-check and must not run the current
|
|
85
|
+
directory's npm scripts.
|
|
86
|
+
|
|
81
87
|
## 7. Local State
|
|
82
88
|
|
|
83
89
|
By default, MAIR stores local state in:
|
|
@@ -96,7 +102,9 @@ export MINA_ROUTER_STATE=/path/to/router-state.json
|
|
|
96
102
|
|
|
97
103
|
- CLI: `apps/cli/src/index.ts`
|
|
98
104
|
- HTTP UI/server: `apps/http-server/src/index.ts`
|
|
99
|
-
-
|
|
105
|
+
- React browser UI: `apps/http-server/ui/src`
|
|
106
|
+
- Vite UI entry: `apps/http-server/ui/index.html`
|
|
107
|
+
- Built UI assets served from: `dist/apps/http-server/src/public`
|
|
100
108
|
- MCP provider: `packages/mcp/src/provider.ts`
|
|
101
109
|
- tmux transport: `packages/transports/src/tmux`
|
|
102
110
|
- agent registration skill: `skills/mina-ai-router-agent/SKILL.md`
|
package/docs/GETTING-STARTED.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
+
Mina AI Router lets multiple local Codex and Claude CLI agents collaborate through one local MCP router and browser console.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
3
7
|
Choose the guide that matches what you are trying to do.
|
|
4
8
|
|
|
5
9
|
## For Users
|
|
@@ -14,9 +18,9 @@ Start here if you want to build, test, modify, or package this repository.
|
|
|
14
18
|
|
|
15
19
|
[Developer Start Guide](./DEVELOPER-START-GUIDE.md)
|
|
16
20
|
|
|
17
|
-
##
|
|
21
|
+
## Setup Reference Guides
|
|
18
22
|
|
|
19
|
-
Use these
|
|
23
|
+
Use these when you need manual repair details or custom client profiles:
|
|
20
24
|
|
|
21
25
|
- [MCP Client Setup](./MCP-CLIENT-SETUP.md): connect Codex or Claude to the local MAIR MCP server.
|
|
22
26
|
- [Skill Install Guide](./SKILL-INSTALL-GUIDE.md): install the MAIR registration skill for Codex or Claude.
|
|
@@ -25,8 +29,9 @@ Use these once per machine or per AI CLI profile:
|
|
|
25
29
|
|
|
26
30
|
1. Install the local `mair` command.
|
|
27
31
|
2. Start the MAIR server.
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
30
|
-
5. Create
|
|
32
|
+
3. Run `mair setup codex --project /path/to/project` and `mair doctor --client codex --project /path/to/project` if you use Codex, or the matching `claude` commands if you use Claude.
|
|
33
|
+
4. Use `mair doctor --client all --project /path/to/project` only when both clients are installed and configured.
|
|
34
|
+
5. Create two or more agents from the Web UI or with `mair codex` / `mair claude`.
|
|
35
|
+
6. Ask one registered agent to use Mina AI Router to call another agent.
|
|
31
36
|
|
|
32
37
|
The user guide walks through that path with screenshots.
|