@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.
@@ -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: "0.1.0",
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 agents = await context.router.listAgentStatuses();
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
- context.registry.register(agent);
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({ target, task, timeoutMs });
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
- `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");
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
- - Browser UI HTML: `apps/http-server/src/ui.html`
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`
@@ -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
+ ![Mina AI Router overview](./assets/mina-ai-router-overview.svg)
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
- ## Required Setup Guides
21
+ ## Setup Reference Guides
18
22
 
19
- Use these once per machine or per AI CLI profile:
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. 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`.
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.