@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
|
@@ -14,6 +14,7 @@ const runtimeModuleUrl = (0, node_url_1.pathToFileURL)((0, node_path_1.join)(__d
|
|
|
14
14
|
const port = Number(process.env.PORT ?? process.env.MINA_HTTP_PORT ?? 3333);
|
|
15
15
|
const host = process.env.HOST ?? process.env.MINA_HTTP_HOST ?? "127.0.0.1";
|
|
16
16
|
const statePath = process.env.MINA_ROUTER_STATE ?? (0, node_path_1.join)(process.cwd(), "data", "router-state.json");
|
|
17
|
+
const agentStaleAfterMs = Number(process.env.MINA_AGENT_STALE_AFTER_MS ?? 15 * 60 * 1000);
|
|
17
18
|
const context = createContext();
|
|
18
19
|
let mcpHandlerPromise;
|
|
19
20
|
function createContext() {
|
|
@@ -39,6 +40,7 @@ function createContext() {
|
|
|
39
40
|
registry,
|
|
40
41
|
requestStore,
|
|
41
42
|
transports,
|
|
43
|
+
agentStaleAfterMs,
|
|
42
44
|
onStateChanged: baseContext.save,
|
|
43
45
|
});
|
|
44
46
|
return {
|
|
@@ -54,6 +56,10 @@ const server = (0, node_http_1.createServer)((request, response) => {
|
|
|
54
56
|
});
|
|
55
57
|
});
|
|
56
58
|
});
|
|
59
|
+
server.on("error", (error) => {
|
|
60
|
+
console.error(`Mina AI Router HTTP server failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
62
|
+
});
|
|
57
63
|
server.listen(port, host, () => {
|
|
58
64
|
console.log(`Mina AI Router HTTP server`);
|
|
59
65
|
console.log(`UI: http://${host}:${port}/`);
|
|
@@ -70,6 +76,15 @@ async function handleRequest(request, response) {
|
|
|
70
76
|
sendHtml(response, renderAppHtml());
|
|
71
77
|
return;
|
|
72
78
|
}
|
|
79
|
+
if (url.pathname.startsWith("/assets/") && request.method === "GET") {
|
|
80
|
+
sendStaticAsset(response, url.pathname);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (url.pathname === "/favicon.ico" && request.method === "GET") {
|
|
84
|
+
response.statusCode = 204;
|
|
85
|
+
response.end();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
73
88
|
if (url.pathname === "/mcp" && request.method === "POST") {
|
|
74
89
|
await handleMcp(request, response, url);
|
|
75
90
|
return;
|
|
@@ -93,6 +108,13 @@ async function handleRequest(request, response) {
|
|
|
93
108
|
sendJson(response, 200, { agent, state: await getUiState() });
|
|
94
109
|
return;
|
|
95
110
|
}
|
|
111
|
+
const agentCapabilityRefreshMatch = url.pathname.match(/^\/api\/agents\/([^/]+)\/refresh-capabilities$/);
|
|
112
|
+
if (agentCapabilityRefreshMatch && request.method === "POST") {
|
|
113
|
+
const body = await readJsonBody(request);
|
|
114
|
+
const result = await refreshAgentCapabilities(decodeURIComponent(agentCapabilityRefreshMatch[1]), body);
|
|
115
|
+
sendJson(response, 200, { ...result, state: await getUiState() });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
96
118
|
const agentDeleteMatch = url.pathname.match(/^\/api\/agents\/([^/]+)$/);
|
|
97
119
|
if (agentDeleteMatch && request.method === "PATCH") {
|
|
98
120
|
const body = await readJsonBody(request);
|
|
@@ -145,19 +167,27 @@ async function handleRequest(request, response) {
|
|
|
145
167
|
}
|
|
146
168
|
catch (error) {
|
|
147
169
|
context.save();
|
|
148
|
-
sendJson(response, 500, {
|
|
170
|
+
sendJson(response, error instanceof src_1.AgentNotRouteReadyError ? 409 : 500, {
|
|
149
171
|
error: error instanceof Error ? error.message : String(error),
|
|
150
172
|
state: await getUiState(),
|
|
151
173
|
});
|
|
152
174
|
}
|
|
153
175
|
return;
|
|
154
176
|
}
|
|
155
|
-
const requestActionMatch = url.pathname.match(/^\/api\/requests\/([^/]+)\/(retry|cancel|archive)$/);
|
|
177
|
+
const requestActionMatch = url.pathname.match(/^\/api\/requests\/([^/]+)\/(retry|cancel|archive|unarchive|interrupt|recover)$/);
|
|
156
178
|
if (requestActionMatch && request.method === "POST") {
|
|
157
179
|
const requestId = decodeURIComponent(requestActionMatch[1]);
|
|
158
180
|
const action = requestActionMatch[2];
|
|
159
|
-
|
|
160
|
-
|
|
181
|
+
try {
|
|
182
|
+
const result = await handleRequestAction(requestId, action);
|
|
183
|
+
sendJson(response, 200, { result, state: await getUiState() });
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
sendJson(response, 400, {
|
|
187
|
+
error: error instanceof Error ? error.message : String(error),
|
|
188
|
+
state: await getUiState(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
161
191
|
return;
|
|
162
192
|
}
|
|
163
193
|
if (url.pathname === "/api/requests/archive-stale" && request.method === "POST") {
|
|
@@ -169,8 +199,13 @@ async function handleRequest(request, response) {
|
|
|
169
199
|
}
|
|
170
200
|
if (url.pathname === "/api/setup-codex-pair" && request.method === "POST") {
|
|
171
201
|
const body = await readJsonBody(request);
|
|
172
|
-
|
|
173
|
-
|
|
202
|
+
try {
|
|
203
|
+
const result = setupCodexPair(body);
|
|
204
|
+
sendJson(response, 200, result);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
sendJson(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
208
|
+
}
|
|
174
209
|
return;
|
|
175
210
|
}
|
|
176
211
|
sendJson(response, 404, { error: "Not found" });
|
|
@@ -187,9 +222,15 @@ function archiveStaleRequests(olderThanMs) {
|
|
|
187
222
|
if (Number.isFinite(updatedAt) && updatedAt > cutoff) {
|
|
188
223
|
continue;
|
|
189
224
|
}
|
|
190
|
-
|
|
225
|
+
const archivedRequest = context.requestStore.updateStatus(request.id, "archived", {
|
|
226
|
+
archivedAt: new Date().toISOString(),
|
|
227
|
+
archivedFromStatus: request.status,
|
|
191
228
|
error: request.error ?? "Archived as stale by operator.",
|
|
192
|
-
|
|
229
|
+
leaseStatus: request.leaseStatus === "active" ? "released" : request.leaseStatus,
|
|
230
|
+
leaseReleasedAt: request.leaseStatus === "active" ? new Date().toISOString() : request.leaseReleasedAt,
|
|
231
|
+
});
|
|
232
|
+
clearAgentLeaseForRequest(archivedRequest);
|
|
233
|
+
archived.push(archivedRequest);
|
|
193
234
|
}
|
|
194
235
|
if (archived.length > 0) {
|
|
195
236
|
context.save();
|
|
@@ -199,26 +240,73 @@ function archiveStaleRequests(olderThanMs) {
|
|
|
199
240
|
async function handleRequestAction(requestId, action) {
|
|
200
241
|
const request = context.requestStore.require(requestId);
|
|
201
242
|
if (action === "retry") {
|
|
202
|
-
|
|
243
|
+
context.requestStore.assertActionAllowed(request, "retry");
|
|
244
|
+
const result = await context.router.callAgent({
|
|
203
245
|
sourceAgent: request.sourceAgent,
|
|
204
246
|
target: request.targetAgent,
|
|
205
247
|
task: request.task,
|
|
248
|
+
retryOfRequestId: request.id,
|
|
206
249
|
});
|
|
250
|
+
context.requestStore.recordRetry(request.id, result.requestId);
|
|
251
|
+
context.save();
|
|
252
|
+
return result;
|
|
207
253
|
}
|
|
208
254
|
if (action === "cancel") {
|
|
209
|
-
const updated = context.requestStore.
|
|
210
|
-
|
|
211
|
-
|
|
255
|
+
const updated = context.requestStore.cancel(requestId, "Cancelled by operator from Mina AI Router UI.", "ui");
|
|
256
|
+
clearAgentLeaseForRequest(updated);
|
|
257
|
+
context.save();
|
|
258
|
+
return updated;
|
|
259
|
+
}
|
|
260
|
+
if (action === "interrupt") {
|
|
261
|
+
const updated = interruptRequest(requestId, "ui");
|
|
212
262
|
context.save();
|
|
213
263
|
return updated;
|
|
214
264
|
}
|
|
265
|
+
if (action === "recover") {
|
|
266
|
+
const updated = context.router.recoverRequestLease(requestId, "ui", "Marked recovered by operator from Mina AI Router UI.");
|
|
267
|
+
return updated;
|
|
268
|
+
}
|
|
215
269
|
if (action === "archive") {
|
|
216
|
-
|
|
270
|
+
return context.router.archiveRequest(requestId, "ui", "Archived by operator from Mina AI Router UI.");
|
|
271
|
+
}
|
|
272
|
+
if (action === "unarchive") {
|
|
273
|
+
const updated = context.requestStore.unarchive(requestId);
|
|
217
274
|
context.save();
|
|
218
275
|
return updated;
|
|
219
276
|
}
|
|
220
277
|
throw new Error(`Unsupported request action "${action}".`);
|
|
221
278
|
}
|
|
279
|
+
function interruptRequest(requestId, source) {
|
|
280
|
+
const request = context.requestStore.require(requestId);
|
|
281
|
+
context.requestStore.assertActionAllowed(request, "interrupt");
|
|
282
|
+
const agentId = request.leaseOwnerAgentId ?? request.targetAgent;
|
|
283
|
+
const agent = context.registry.require(agentId);
|
|
284
|
+
if (agent.transport !== "tmux") {
|
|
285
|
+
throw new Error(`Request "${requestId}" targets ${agent.transport}, not tmux; open the session manually.`);
|
|
286
|
+
}
|
|
287
|
+
const target = agent.tmuxTarget ?? agent.sessionId;
|
|
288
|
+
new src_2.TmuxClient().sendInterrupt(target);
|
|
289
|
+
return context.requestStore.recordInterrupt(requestId, {
|
|
290
|
+
source,
|
|
291
|
+
terminalTarget: target,
|
|
292
|
+
message: `Terminal interrupt sent to ${target}.`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function clearAgentLeaseForRequest(request) {
|
|
296
|
+
if (!request.leaseOwnerAgentId) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const agent = context.registry.get(request.leaseOwnerAgentId);
|
|
300
|
+
if (!agent || agent.activeRequestId !== request.id) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
context.registry.register({
|
|
304
|
+
...agent,
|
|
305
|
+
activeRequestId: undefined,
|
|
306
|
+
leaseStatus: "released",
|
|
307
|
+
leaseReleasedAt: new Date().toISOString(),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
222
310
|
async function handleMcp(request, response, url) {
|
|
223
311
|
const handler = await getMcpHandler();
|
|
224
312
|
const body = await readRawBody(request);
|
|
@@ -276,14 +364,16 @@ async function getHealth() {
|
|
|
276
364
|
const requests = context.router.listRequests();
|
|
277
365
|
const openRequests = requests.filter((request) => ["created", "sent", "waiting"].includes(request.status));
|
|
278
366
|
return {
|
|
279
|
-
ok: agents.every((agent) =>
|
|
367
|
+
ok: agents.every((agent) => !["missing", "stale", "needs-attention"].includes(agent.status)),
|
|
280
368
|
statePath,
|
|
281
369
|
mcpUrl: `http://${host}:${port}/mcp`,
|
|
282
370
|
agents: {
|
|
283
371
|
total: agents.length,
|
|
284
372
|
available: agents.filter((agent) => agent.status === "available").length,
|
|
285
373
|
busy: agents.filter((agent) => agent.status === "busy").length,
|
|
374
|
+
stale: agents.filter((agent) => agent.status === "stale").length,
|
|
286
375
|
missing: agents.filter((agent) => agent.status === "missing").length,
|
|
376
|
+
needsAttention: agents.filter((agent) => agent.status === "needs-attention").length,
|
|
287
377
|
unknown: agents.filter((agent) => agent.status === "unknown").length,
|
|
288
378
|
},
|
|
289
379
|
requests: {
|
|
@@ -300,6 +390,7 @@ function registerAgent(body) {
|
|
|
300
390
|
const id = requiredString(body.id, "id");
|
|
301
391
|
const projectRoot = stringValue(body.projectRoot) ?? process.cwd();
|
|
302
392
|
const capabilityNotice = inferCapabilityNotice(projectRoot);
|
|
393
|
+
const now = new Date().toISOString();
|
|
303
394
|
const agent = {
|
|
304
395
|
id,
|
|
305
396
|
name: stringValue(body.name) ?? id,
|
|
@@ -311,22 +402,149 @@ function registerAgent(body) {
|
|
|
311
402
|
startupCommand: stringValue(body.startupCommand),
|
|
312
403
|
capabilitySummary: stringValue(body.capabilitySummary) ?? capabilityNotice.summary,
|
|
313
404
|
capabilitySources: stringValue(body.capabilitySources) ?? capabilityNotice.sources,
|
|
405
|
+
capabilityProfile: capabilityProfileValue(body.capabilityProfile),
|
|
406
|
+
bootstrapStatus: stringValue(body.bootstrapStatus) ?? "ready",
|
|
407
|
+
registrationSource: stringValue(body.registrationSource) ?? "manual",
|
|
408
|
+
registrationStatus: stringValue(body.registrationStatus) ?? "confirmed",
|
|
409
|
+
lastRegistrationAttemptAt: stringValue(body.lastRegistrationAttemptAt) ?? now,
|
|
410
|
+
confirmedByAgentAt: stringValue(body.confirmedByAgentAt) ?? now,
|
|
411
|
+
sessionFingerprint: stringValue(body.sessionFingerprint) ?? stringValue(body.sessionId) ?? id,
|
|
412
|
+
permissionProfile: stringValue(body.permissionProfile) ?? "default",
|
|
413
|
+
permissionProfileStatus: stringValue(body.permissionProfileStatus) ?? "not-requested",
|
|
414
|
+
permissionProfileDetail: stringValue(body.permissionProfileDetail),
|
|
415
|
+
mcpPreflightStatus: stringValue(body.mcpPreflightStatus),
|
|
416
|
+
mcpPreflightDetail: stringValue(body.mcpPreflightDetail),
|
|
417
|
+
mcpSetupCommand: stringValue(body.mcpSetupCommand),
|
|
418
|
+
mcpVerifyCommand: stringValue(body.mcpVerifyCommand),
|
|
419
|
+
mcpRemoveCommand: stringValue(body.mcpRemoveCommand),
|
|
420
|
+
mcpUrl: stringValue(body.mcpUrl),
|
|
314
421
|
};
|
|
315
|
-
context.registry.register(agent
|
|
422
|
+
const registered = context.registry.register(agent, {
|
|
423
|
+
capabilitySource: agent.capabilitySummary || agent.capabilitySources ? "generated" : undefined,
|
|
424
|
+
});
|
|
316
425
|
context.save();
|
|
317
|
-
return
|
|
426
|
+
return registered;
|
|
427
|
+
}
|
|
428
|
+
function capabilityProfileValue(value) {
|
|
429
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
const record = value;
|
|
433
|
+
return {
|
|
434
|
+
projectPurpose: stringValue(record.projectPurpose),
|
|
435
|
+
primaryLanguages: stringArrayValue(record.primaryLanguages),
|
|
436
|
+
keyAreas: stringArrayValue(record.keyAreas),
|
|
437
|
+
canAnswer: stringArrayValue(record.canAnswer),
|
|
438
|
+
cannotAnswerYet: stringArrayValue(record.cannotAnswerYet),
|
|
439
|
+
evidence: stringArrayValue(record.evidence),
|
|
440
|
+
quality: "missing",
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function stringArrayValue(value) {
|
|
444
|
+
if (!Array.isArray(value)) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const values = value.filter((item) => typeof item === "string" && Boolean(item.trim()))
|
|
448
|
+
.map((item) => item.trim());
|
|
449
|
+
return values.length ? values : undefined;
|
|
318
450
|
}
|
|
319
451
|
function updateAgent(id, body) {
|
|
320
452
|
const current = context.registry.require(id);
|
|
453
|
+
const capabilitySummary = stringFieldValue(body, "capabilitySummary");
|
|
454
|
+
const capabilitySources = stringFieldValue(body, "capabilitySources");
|
|
321
455
|
const next = {
|
|
322
456
|
...current,
|
|
323
457
|
name: stringValue(body.name) ?? current.name,
|
|
324
|
-
capabilitySummary: stringFieldValue(body, "capabilitySummary") ?? current.capabilitySummary,
|
|
325
|
-
capabilitySources: stringFieldValue(body, "capabilitySources") ?? current.capabilitySources,
|
|
326
458
|
};
|
|
327
459
|
context.registry.register(next);
|
|
460
|
+
const updated = capabilitySummary !== undefined || capabilitySources !== undefined
|
|
461
|
+
? context.registry.updateCapabilities(id, {
|
|
462
|
+
summary: capabilitySummary,
|
|
463
|
+
sources: capabilitySources,
|
|
464
|
+
source: "manual",
|
|
465
|
+
})
|
|
466
|
+
: context.registry.require(id);
|
|
328
467
|
context.save();
|
|
329
|
-
return
|
|
468
|
+
return updated;
|
|
469
|
+
}
|
|
470
|
+
async function refreshAgentCapabilities(id, body) {
|
|
471
|
+
const agent = context.registry.require(id);
|
|
472
|
+
const response = await context.router.callAgent({
|
|
473
|
+
target: id,
|
|
474
|
+
task: buildCapabilityRefreshTask(agent),
|
|
475
|
+
timeoutMs: typeof body.timeoutMs === "number" ? body.timeoutMs : undefined,
|
|
476
|
+
});
|
|
477
|
+
const notice = parseCapabilityRefreshAnswer(response.answer);
|
|
478
|
+
const refreshedAt = new Date().toISOString();
|
|
479
|
+
const updated = context.registry.updateCapabilities(id, {
|
|
480
|
+
summary: notice.capabilitySummary,
|
|
481
|
+
sources: notice.capabilitySources,
|
|
482
|
+
source: "generated",
|
|
483
|
+
refreshedAt,
|
|
484
|
+
profile: notice.capabilityProfile,
|
|
485
|
+
});
|
|
486
|
+
context.save();
|
|
487
|
+
return {
|
|
488
|
+
agent: updated,
|
|
489
|
+
refresh: {
|
|
490
|
+
requestId: response.requestId,
|
|
491
|
+
refreshedAt,
|
|
492
|
+
capabilitySource: updated.capabilitySource,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function buildCapabilityRefreshTask(agent) {
|
|
497
|
+
return [
|
|
498
|
+
"Refresh your Mina AI Router capability registration for this visible local agent.",
|
|
499
|
+
"",
|
|
500
|
+
"Inspect local project docs and metadata before answering.",
|
|
501
|
+
"Prefer capability docs in this order when present: CLAUDE.md/claude.md, AGENTS.md/agents.md, agent.md, README.md.",
|
|
502
|
+
"If those files are missing, inspect package metadata and the project file tree.",
|
|
503
|
+
"",
|
|
504
|
+
"Return only JSON with these string fields:",
|
|
505
|
+
"{",
|
|
506
|
+
' "capabilitySummary": "2-5 short bullets or one short paragraph under 800 characters",',
|
|
507
|
+
' "capabilitySources": "comma-separated file paths or project signals used",',
|
|
508
|
+
' "capabilityProfile": {',
|
|
509
|
+
' "projectPurpose": "what this project implements",',
|
|
510
|
+
' "primaryLanguages": ["TypeScript"],',
|
|
511
|
+
' "keyAreas": ["router", "MCP endpoint"],',
|
|
512
|
+
' "canAnswer": ["specific question domains this agent can answer"],',
|
|
513
|
+
' "cannotAnswerYet": ["known limits"],',
|
|
514
|
+
' "evidence": ["README.md", "package.json", "src/..."]',
|
|
515
|
+
" }",
|
|
516
|
+
"}",
|
|
517
|
+
"",
|
|
518
|
+
"Do not include markdown fences or extra commentary in the JSON body.",
|
|
519
|
+
"",
|
|
520
|
+
"Registration context:",
|
|
521
|
+
`- id: ${agent.id}`,
|
|
522
|
+
`- agentType: ${agent.agentType}`,
|
|
523
|
+
`- transport: ${agent.transport}`,
|
|
524
|
+
`- sessionId: ${agent.sessionId}`,
|
|
525
|
+
`- projectRoot: ${agent.projectRoot}`,
|
|
526
|
+
].join("\n");
|
|
527
|
+
}
|
|
528
|
+
function parseCapabilityRefreshAnswer(answer) {
|
|
529
|
+
const trimmed = answer.trim();
|
|
530
|
+
const jsonText = trimmed.startsWith("{") ? trimmed : trimmed.slice(trimmed.indexOf("{"), trimmed.lastIndexOf("}") + 1);
|
|
531
|
+
if (!jsonText || !jsonText.startsWith("{") || !jsonText.endsWith("}")) {
|
|
532
|
+
throw new Error("Capability refresh response did not contain a JSON object.");
|
|
533
|
+
}
|
|
534
|
+
const parsed = JSON.parse(jsonText);
|
|
535
|
+
const capabilitySummary = typeof parsed.capabilitySummary === "string" ? parsed.capabilitySummary.trim() : "";
|
|
536
|
+
const capabilitySources = typeof parsed.capabilitySources === "string" ? parsed.capabilitySources.trim() : "";
|
|
537
|
+
if (!capabilitySummary || !capabilitySources) {
|
|
538
|
+
throw new Error("Capability refresh JSON requires non-empty capabilitySummary and capabilitySources strings.");
|
|
539
|
+
}
|
|
540
|
+
if (capabilitySummary.length > 800) {
|
|
541
|
+
throw new Error("Capability refresh JSON capabilitySummary must be under 800 characters.");
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
capabilitySummary,
|
|
545
|
+
capabilitySources,
|
|
546
|
+
capabilityProfile: capabilityProfileValue(parsed.capabilityProfile),
|
|
547
|
+
};
|
|
330
548
|
}
|
|
331
549
|
function listDirectories(body) {
|
|
332
550
|
const home = process.env.HOME ?? process.cwd();
|
|
@@ -372,13 +590,39 @@ function captureAgentTerminal(id) {
|
|
|
372
590
|
}
|
|
373
591
|
const tmux = new src_2.TmuxClient({ captureLines: 120 });
|
|
374
592
|
const text = tmux.capture(agent.tmuxTarget ?? agent.sessionId);
|
|
593
|
+
const detectedBootstrapPrompt = (0, src_2.detectAgentBootstrapPrompt)(agent, text);
|
|
594
|
+
const bootstrapPrompt = agent.bootstrapStatus === "registration-pending"
|
|
595
|
+
&& detectedBootstrapPrompt
|
|
596
|
+
&& !["codex-mcp-registration-approval", "mcp-registration-approval", "scoped-command-approval"].includes(detectedBootstrapPrompt.kind)
|
|
597
|
+
? undefined
|
|
598
|
+
: detectedBootstrapPrompt;
|
|
599
|
+
const resolvedPermission = !bootstrapPrompt && agent.bootstrapStatus === "permission-required";
|
|
600
|
+
const nextAgent = bootstrapPrompt
|
|
601
|
+
? context.registry.register({
|
|
602
|
+
...agent,
|
|
603
|
+
bootstrapStatus: bootstrapPrompt.kind === "client-update" ? "client-update-required" : "permission-required",
|
|
604
|
+
})
|
|
605
|
+
: resolvedPermission
|
|
606
|
+
? context.registry.register({
|
|
607
|
+
...agent,
|
|
608
|
+
bootstrapStatus: needsSelfRegistration(agent) ? "created" : "created",
|
|
609
|
+
registrationStatus: needsSelfRegistration(agent) ? "pending" : agent.registrationStatus,
|
|
610
|
+
permissionProfileStatus: agent.permissionProfileStatus === "unsupported" ? agent.permissionProfileStatus : "supported",
|
|
611
|
+
permissionProfileDetail: "Previous permission prompt no longer appears in the terminal capture.",
|
|
612
|
+
})
|
|
613
|
+
: agent;
|
|
614
|
+
if (bootstrapPrompt || resolvedPermission) {
|
|
615
|
+
context.save();
|
|
616
|
+
}
|
|
375
617
|
return {
|
|
376
|
-
agent,
|
|
618
|
+
agent: nextAgent,
|
|
377
619
|
terminal: {
|
|
378
620
|
text,
|
|
379
621
|
capturedAt: new Date().toISOString(),
|
|
380
|
-
trustPrompt:
|
|
381
|
-
|
|
622
|
+
trustPrompt: Boolean(bootstrapPrompt && bootstrapPrompt.kind !== "client-update"),
|
|
623
|
+
permissionPrompt: bootstrapPrompt,
|
|
624
|
+
actions: terminalActions(nextAgent, bootstrapPrompt),
|
|
625
|
+
pendingRegistration: needsSelfRegistration(nextAgent),
|
|
382
626
|
},
|
|
383
627
|
};
|
|
384
628
|
}
|
|
@@ -390,8 +634,16 @@ function sendAgentTerminalInput(id, body) {
|
|
|
390
634
|
const target = agent.tmuxTarget ?? agent.sessionId;
|
|
391
635
|
const text = stringValue(body.text);
|
|
392
636
|
const enter = body.enter === true;
|
|
637
|
+
const actionId = stringValue(body.actionId);
|
|
393
638
|
const tmux = new src_2.TmuxClient({ captureLines: 120 });
|
|
394
|
-
|
|
639
|
+
const action = actionId ? resolveTerminalAction(agent, actionId) : undefined;
|
|
640
|
+
if (action?.input?.text) {
|
|
641
|
+
tmux.sendText(target, action.input.text);
|
|
642
|
+
}
|
|
643
|
+
else if (action?.input?.enter) {
|
|
644
|
+
tmux.sendEnter(target);
|
|
645
|
+
}
|
|
646
|
+
else if (text) {
|
|
395
647
|
if (agent.agentType === "codex") {
|
|
396
648
|
tmux.sendCodexText(target, text);
|
|
397
649
|
}
|
|
@@ -403,10 +655,23 @@ function sendAgentTerminalInput(id, body) {
|
|
|
403
655
|
tmux.sendEnter(target);
|
|
404
656
|
}
|
|
405
657
|
let registration = "unchanged";
|
|
406
|
-
|
|
658
|
+
const shouldRetryRegistration = (enter && !text && needsSelfRegistration(agent))
|
|
659
|
+
|| action?.id === "approve-project-trust"
|
|
660
|
+
|| action?.id === "approve-claude-project-trust"
|
|
661
|
+
|| action?.id === "skip-codex-update"
|
|
662
|
+
|| action?.id === "approve-scoped-registration-command"
|
|
663
|
+
|| action?.id === "retry-self-registration";
|
|
664
|
+
if (shouldRetryRegistration && needsSelfRegistration(agent)) {
|
|
407
665
|
sleep(1200);
|
|
408
666
|
const capture = tmux.capture(target);
|
|
409
|
-
|
|
667
|
+
const bootstrapPrompt = (0, src_2.detectAgentBootstrapPrompt)(agent, capture);
|
|
668
|
+
const permissionApprovalAttempt = agent.bootstrapStatus === "permission-required"
|
|
669
|
+
&& bootstrapPrompt?.kind !== "client-update";
|
|
670
|
+
const updateSkipAttempt = action?.id === "skip-codex-update";
|
|
671
|
+
if (updateSkipAttempt && bootstrapPrompt?.kind === "client-update") {
|
|
672
|
+
registration = "update skip sent";
|
|
673
|
+
}
|
|
674
|
+
else if (!bootstrapPrompt || permissionApprovalAttempt) {
|
|
410
675
|
if (agent.agentType === "codex") {
|
|
411
676
|
tmux.sendCodexText(target, buildSelfRegistrationPrompt(agent));
|
|
412
677
|
}
|
|
@@ -414,6 +679,12 @@ function sendAgentTerminalInput(id, body) {
|
|
|
414
679
|
tmux.sendText(target, buildSelfRegistrationPrompt(agent));
|
|
415
680
|
}
|
|
416
681
|
registration = "registration prompt sent to agent";
|
|
682
|
+
context.registry.register({
|
|
683
|
+
...agent,
|
|
684
|
+
bootstrapStatus: "registration-pending",
|
|
685
|
+
registrationStatus: "pending",
|
|
686
|
+
});
|
|
687
|
+
context.save();
|
|
417
688
|
}
|
|
418
689
|
}
|
|
419
690
|
return {
|
|
@@ -436,6 +707,20 @@ function createTmuxAgent(body) {
|
|
|
436
707
|
const sessionId = stringValue(body.sessionId) ?? `${agentType}-${sanitizeName(projectName)}`;
|
|
437
708
|
const startupCommand = stringValue(body.startupCommand)
|
|
438
709
|
?? (agentType === "codex" ? "codex --no-alt-screen" : "claude");
|
|
710
|
+
const now = new Date().toISOString();
|
|
711
|
+
const permissionProfile = resolvePermissionProfile(agentType, stringValue(body.permissionProfile) ?? "default", projectRoot);
|
|
712
|
+
const mcpUrl = `http://${host}:${port}/mcp`;
|
|
713
|
+
const mcpName = stringValue(body.mcpName) ?? "mina-ai-router";
|
|
714
|
+
const explicitConfiguredUrl = stringValue(body.mcpConfiguredUrl);
|
|
715
|
+
const detectedConfiguredUrl = explicitConfiguredUrl
|
|
716
|
+
?? (body.mcpConfigured === true ? undefined : detectClientMcpConfiguredUrl(agentType, mcpName, mcpUrl, projectRoot));
|
|
717
|
+
const mcpPreflight = (0, src_1.buildMcpPreflight)({
|
|
718
|
+
agentType,
|
|
719
|
+
mcpUrl,
|
|
720
|
+
mcpName,
|
|
721
|
+
configured: body.mcpConfigured === true,
|
|
722
|
+
configuredUrl: detectedConfiguredUrl,
|
|
723
|
+
});
|
|
439
724
|
const command = startupCommand.split(/\s+/)[0];
|
|
440
725
|
assertCommandAvailable("tmux");
|
|
441
726
|
assertCommandAvailable(command);
|
|
@@ -447,12 +732,26 @@ function createTmuxAgent(body) {
|
|
|
447
732
|
sessionId,
|
|
448
733
|
projectRoot,
|
|
449
734
|
startupCommand,
|
|
735
|
+
bootstrapStatus: "created",
|
|
736
|
+
registrationSource: "web-ui",
|
|
737
|
+
registrationStatus: "pending",
|
|
738
|
+
lastRegistrationAttemptAt: now,
|
|
739
|
+
sessionFingerprint: sessionId,
|
|
740
|
+
permissionProfile: permissionProfile.permissionProfile,
|
|
741
|
+
permissionProfileStatus: permissionProfile.permissionProfileStatus,
|
|
742
|
+
permissionProfileDetail: permissionProfile.permissionProfileDetail,
|
|
743
|
+
mcpPreflightStatus: mcpPreflight.mcpPreflightStatus,
|
|
744
|
+
mcpPreflightDetail: mcpPreflight.mcpPreflightDetail,
|
|
745
|
+
mcpSetupCommand: mcpPreflight.mcpSetupCommand,
|
|
746
|
+
mcpVerifyCommand: mcpPreflight.mcpVerifyCommand,
|
|
747
|
+
mcpRemoveCommand: mcpPreflight.mcpRemoveCommand,
|
|
748
|
+
mcpUrl: mcpPreflight.mcpUrl,
|
|
450
749
|
...uiCreatedCapabilityNotice(projectRoot),
|
|
451
750
|
};
|
|
452
751
|
const tmux = new src_2.TmuxClient();
|
|
453
752
|
const existed = tmux.hasSession(sessionId);
|
|
454
753
|
tmux.ensureSession(agent);
|
|
455
|
-
context.registry.register(agent);
|
|
754
|
+
let registeredAgent = context.registry.register(agent);
|
|
456
755
|
context.save();
|
|
457
756
|
const sendRegistrationPrompt = body.sendRegistrationPrompt !== false;
|
|
458
757
|
let registration = "registration prompt skipped";
|
|
@@ -461,33 +760,102 @@ function createTmuxAgent(body) {
|
|
|
461
760
|
const delayMs = typeof body.registerDelayMs === "number" ? body.registerDelayMs : 4000;
|
|
462
761
|
sleep(delayMs);
|
|
463
762
|
const capture = tmux.capture(sessionId);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
763
|
+
const bootstrapPrompt = (0, src_2.detectAgentBootstrapPrompt)(agent, capture);
|
|
764
|
+
if (bootstrapPrompt) {
|
|
765
|
+
registration = bootstrapPrompt.kind === "client-update"
|
|
766
|
+
? "waiting for client update choice"
|
|
767
|
+
: "waiting for permission approval";
|
|
768
|
+
nextAction = bootstrapPrompt.action;
|
|
769
|
+
registeredAgent = context.registry.register({
|
|
770
|
+
...agent,
|
|
771
|
+
bootstrapStatus: bootstrapPrompt.kind === "client-update" ? "client-update-required" : "permission-required",
|
|
772
|
+
});
|
|
773
|
+
context.save();
|
|
774
|
+
}
|
|
775
|
+
else if (!mcpPreflight.canSendSelfRegistrationPrompt) {
|
|
776
|
+
registration = "waiting for MCP setup";
|
|
777
|
+
nextAction = mcpPreflight.nextAction;
|
|
778
|
+
registeredAgent = context.registry.register({
|
|
779
|
+
...agent,
|
|
780
|
+
bootstrapStatus: "mcp-configuring",
|
|
781
|
+
});
|
|
782
|
+
context.save();
|
|
467
783
|
}
|
|
468
784
|
else {
|
|
469
785
|
const prompt = buildSelfRegistrationPrompt(agent);
|
|
470
|
-
|
|
471
|
-
|
|
786
|
+
try {
|
|
787
|
+
if (agentType === "codex") {
|
|
788
|
+
tmux.sendCodexText(sessionId, prompt);
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
tmux.sendText(sessionId, prompt);
|
|
792
|
+
}
|
|
472
793
|
}
|
|
473
|
-
|
|
474
|
-
|
|
794
|
+
catch (error) {
|
|
795
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
796
|
+
registration = "registration prompt failed";
|
|
797
|
+
nextAction = `Resolve the terminal/session blocker for ${sessionId}, then retry agent registration. ${detail}`;
|
|
798
|
+
registeredAgent = context.registry.register({
|
|
799
|
+
...registeredAgent,
|
|
800
|
+
bootstrapStatus: "failed",
|
|
801
|
+
registrationStatus: "failed",
|
|
802
|
+
mcpPreflightDetail: detail,
|
|
803
|
+
});
|
|
804
|
+
context.save();
|
|
805
|
+
return {
|
|
806
|
+
agent: registeredAgent,
|
|
807
|
+
existed,
|
|
808
|
+
attach: `tmux attach -t ${sessionId}`,
|
|
809
|
+
registration,
|
|
810
|
+
nextAction,
|
|
811
|
+
mcpPreflight,
|
|
812
|
+
permissionProfile,
|
|
813
|
+
state: {
|
|
814
|
+
agents: context.registry.list(),
|
|
815
|
+
requests: context.requestStore.list(),
|
|
816
|
+
},
|
|
817
|
+
};
|
|
475
818
|
}
|
|
476
819
|
registration = "registration prompt sent to agent";
|
|
820
|
+
registeredAgent = context.registry.register({
|
|
821
|
+
...registeredAgent,
|
|
822
|
+
bootstrapStatus: "registration-pending",
|
|
823
|
+
registrationStatus: "pending",
|
|
824
|
+
});
|
|
825
|
+
context.save();
|
|
477
826
|
}
|
|
478
827
|
}
|
|
479
828
|
return {
|
|
480
|
-
agent,
|
|
829
|
+
agent: registeredAgent,
|
|
481
830
|
existed,
|
|
482
831
|
registration,
|
|
483
832
|
nextAction,
|
|
833
|
+
permissionProfile,
|
|
834
|
+
mcpPreflight,
|
|
484
835
|
attachCommand: `tmux attach -t ${sessionId}`,
|
|
485
836
|
mairAttachCommand: `mair attach ${id}`,
|
|
486
837
|
};
|
|
487
838
|
}
|
|
839
|
+
function resolvePermissionProfile(agentType, requestedProfile, projectRoot) {
|
|
840
|
+
if (requestedProfile !== "direct-workspace-read") {
|
|
841
|
+
return {
|
|
842
|
+
permissionProfile: "default",
|
|
843
|
+
permissionProfileStatus: "not-requested",
|
|
844
|
+
permissionProfileDetail: "Default CLI startup. Mina will surface permission prompts instead of hiding them.",
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
permissionProfile: "direct-workspace-read",
|
|
849
|
+
permissionProfileStatus: "unsupported",
|
|
850
|
+
permissionProfileDetail: `No known ${agentType} startup flag in Mina is both direct-read and scoped only to ${projectRoot}. Mina will start the default command and surface permission prompts.`,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
488
853
|
function setupCodexPair(body) {
|
|
489
|
-
const mainRoot = stringValue(body.mainRoot)
|
|
490
|
-
const helperRoot = stringValue(body.helperRoot)
|
|
854
|
+
const mainRoot = stringValue(body.mainRoot);
|
|
855
|
+
const helperRoot = stringValue(body.helperRoot);
|
|
856
|
+
if (!mainRoot || !helperRoot) {
|
|
857
|
+
throw new Error("setup-codex-pair is a developer/demo helper. Provide explicit mainRoot and helperRoot. Use mair setup codex or mair setup claude for normal first-run setup.");
|
|
858
|
+
}
|
|
491
859
|
const helperId = stringValue(body.helperId) ?? "ralph";
|
|
492
860
|
const sessionId = stringValue(body.sessionId) ?? "mina-ralph-codex";
|
|
493
861
|
const agent = {
|
|
@@ -521,14 +889,18 @@ function buildSelfRegistrationPrompt(agent) {
|
|
|
521
889
|
`- agentType: ${agent.agentType}`,
|
|
522
890
|
`- transport: ${agent.transport}`,
|
|
523
891
|
`- sessionId: ${agent.sessionId}`,
|
|
892
|
+
`- sessionFingerprint: ${agent.sessionFingerprint ?? agent.sessionId}`,
|
|
524
893
|
`- projectRoot: ${agent.projectRoot}`,
|
|
525
894
|
`- startupCommand: ${agent.startupCommand ?? ""}`,
|
|
526
895
|
"",
|
|
896
|
+
"If Mina already created this agent record, confirm and update that existing id. Do not create a new id for the same session.",
|
|
897
|
+
"",
|
|
527
898
|
"Before registering, create a concise capability notice for this session:",
|
|
528
899
|
"- Prefer capability docs in this order when present: CLAUDE.md/claude.md, AGENTS.md/agents.md, agent.md, README.md.",
|
|
529
900
|
"- If those files are missing, inspect package metadata and the project file tree to infer what this project/agent can help with.",
|
|
530
901
|
"- Set register_agent capabilitySummary to 2-5 short bullets or one short paragraph under 800 characters.",
|
|
531
902
|
"- Set register_agent capabilitySources to a comma-separated list of the files or project signals you used.",
|
|
903
|
+
"- Set register_agent sessionFingerprint to the value above.",
|
|
532
904
|
"After registering, call list_agents and confirm this agent is available.",
|
|
533
905
|
].join("\n");
|
|
534
906
|
}
|
|
@@ -663,25 +1035,178 @@ function truncateText(value, maxLength) {
|
|
|
663
1035
|
const normalized = value.replace(/\s+/g, " ").trim();
|
|
664
1036
|
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1).trim()}...` : normalized;
|
|
665
1037
|
}
|
|
666
|
-
function isCodexTrustPrompt(capture) {
|
|
667
|
-
return capture.includes("Do you trust the contents of this directory?")
|
|
668
|
-
|| capture.includes("Press enter to continue")
|
|
669
|
-
|| capture.includes("Yes, continue");
|
|
670
|
-
}
|
|
671
1038
|
function isPendingUiRegistration(agent) {
|
|
672
1039
|
return agent.capabilitySources?.startsWith("created from Mina UI") ?? false;
|
|
673
1040
|
}
|
|
1041
|
+
function needsSelfRegistration(agent) {
|
|
1042
|
+
if (agent.transport !== "tmux") {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
if (agent.registrationStatus === "confirmed") {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
if (agent.mcpPreflightStatus === "missing" || agent.mcpPreflightStatus === "stale") {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
if (agent.bootstrapStatus === "mcp-configuring" || agent.bootstrapStatus === "failed") {
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
return agent.registrationStatus === "pending" || isPendingUiRegistration(agent);
|
|
1055
|
+
}
|
|
1056
|
+
function terminalActions(agent, prompt) {
|
|
1057
|
+
if (prompt?.kind === "client-update") {
|
|
1058
|
+
return [
|
|
1059
|
+
{
|
|
1060
|
+
id: "skip-codex-update",
|
|
1061
|
+
label: "Skip Codex Update",
|
|
1062
|
+
description: "Sends the explicit skip choice for the known Codex update prompt. Registration resumes after the prompt clears.",
|
|
1063
|
+
policy: "guided",
|
|
1064
|
+
input: { text: "2" },
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
id: "manual-update-choice",
|
|
1068
|
+
label: "Choose Manually",
|
|
1069
|
+
description: "Attach to tmux and choose the update option yourself.",
|
|
1070
|
+
policy: "manual",
|
|
1071
|
+
},
|
|
1072
|
+
];
|
|
1073
|
+
}
|
|
1074
|
+
if (prompt?.kind === "directory-trust") {
|
|
1075
|
+
return [
|
|
1076
|
+
{
|
|
1077
|
+
id: "approve-project-trust",
|
|
1078
|
+
label: "Approve Project Trust",
|
|
1079
|
+
description: "Sends Enter for the visible project trust prompt after you have reviewed the directory.",
|
|
1080
|
+
policy: "guided",
|
|
1081
|
+
input: { enter: true },
|
|
1082
|
+
},
|
|
1083
|
+
];
|
|
1084
|
+
}
|
|
1085
|
+
if (prompt?.kind === "claude-folder-trust") {
|
|
1086
|
+
return [
|
|
1087
|
+
{
|
|
1088
|
+
id: "approve-claude-project-trust",
|
|
1089
|
+
label: "Approve Claude Trust",
|
|
1090
|
+
description: "Sends Enter for the visible Claude folder trust prompt after you have reviewed the project path.",
|
|
1091
|
+
policy: "guided",
|
|
1092
|
+
input: { enter: true },
|
|
1093
|
+
},
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
if (prompt?.kind === "mcp-registration-approval") {
|
|
1097
|
+
return [
|
|
1098
|
+
{
|
|
1099
|
+
id: "approve-mcp-registration",
|
|
1100
|
+
label: "Approve Mina MCP Call",
|
|
1101
|
+
description: "Approves option 1 for this scoped Mina register_agent, list_agents, or call_agent MCP call only.",
|
|
1102
|
+
policy: "guided",
|
|
1103
|
+
input: { enter: true },
|
|
1104
|
+
},
|
|
1105
|
+
];
|
|
1106
|
+
}
|
|
1107
|
+
if (prompt?.kind === "codex-mcp-registration-approval") {
|
|
1108
|
+
return [
|
|
1109
|
+
{
|
|
1110
|
+
id: "approve-codex-mcp-registration",
|
|
1111
|
+
label: "Approve Codex MCP Call",
|
|
1112
|
+
description: "Approves option 1 for this scoped Codex Mina register_agent or list_agents MCP call only.",
|
|
1113
|
+
policy: "guided",
|
|
1114
|
+
input: { enter: true },
|
|
1115
|
+
},
|
|
1116
|
+
];
|
|
1117
|
+
}
|
|
1118
|
+
if (prompt?.kind === "permission-approval") {
|
|
1119
|
+
return [
|
|
1120
|
+
{
|
|
1121
|
+
id: "manual-permission-approval",
|
|
1122
|
+
label: "Approve In Terminal",
|
|
1123
|
+
description: "Attach to tmux and approve the scoped Claude permission prompt manually.",
|
|
1124
|
+
policy: "manual",
|
|
1125
|
+
},
|
|
1126
|
+
];
|
|
1127
|
+
}
|
|
1128
|
+
if (prompt?.kind === "scoped-command-approval") {
|
|
1129
|
+
return [
|
|
1130
|
+
{
|
|
1131
|
+
id: "approve-scoped-registration-command",
|
|
1132
|
+
label: "Approve Scoped Command",
|
|
1133
|
+
description: "Approves the visible Mina registration command because it is scoped to this project root.",
|
|
1134
|
+
policy: "guided",
|
|
1135
|
+
input: { enter: true },
|
|
1136
|
+
},
|
|
1137
|
+
];
|
|
1138
|
+
}
|
|
1139
|
+
if (needsSelfRegistration(agent)) {
|
|
1140
|
+
return [
|
|
1141
|
+
{
|
|
1142
|
+
id: "retry-self-registration",
|
|
1143
|
+
label: "Retry Self Registration",
|
|
1144
|
+
description: "Sends the Mina self-registration prompt to this session again.",
|
|
1145
|
+
policy: "guided",
|
|
1146
|
+
input: {},
|
|
1147
|
+
},
|
|
1148
|
+
];
|
|
1149
|
+
}
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
1152
|
+
function resolveTerminalAction(agent, actionId) {
|
|
1153
|
+
const capture = new src_2.TmuxClient({ captureLines: 120 }).capture(agent.tmuxTarget ?? agent.sessionId);
|
|
1154
|
+
const prompt = (0, src_2.detectAgentBootstrapPrompt)(agent, capture);
|
|
1155
|
+
const action = terminalActions(agent, prompt).find((candidate) => candidate.id === actionId);
|
|
1156
|
+
if (!action) {
|
|
1157
|
+
throw new Error(`Terminal action "${actionId}" is not available for agent "${agent.id}".`);
|
|
1158
|
+
}
|
|
1159
|
+
return action;
|
|
1160
|
+
}
|
|
674
1161
|
function assertCommandAvailable(command) {
|
|
1162
|
+
if (commandAvailable(command)) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
throw new Error(`Required command "${command}" is not available on PATH.`);
|
|
1166
|
+
}
|
|
1167
|
+
function commandAvailable(command) {
|
|
675
1168
|
try {
|
|
676
1169
|
(0, node_child_process_1.execFileSync)("which", [command], {
|
|
677
1170
|
encoding: "utf8",
|
|
678
1171
|
stdio: ["ignore", "pipe", "pipe"],
|
|
679
1172
|
});
|
|
1173
|
+
return true;
|
|
680
1174
|
}
|
|
681
1175
|
catch {
|
|
682
|
-
|
|
1176
|
+
return false;
|
|
683
1177
|
}
|
|
684
1178
|
}
|
|
1179
|
+
function detectClientMcpConfiguredUrl(client, mcpName, mcpUrl, projectRoot) {
|
|
1180
|
+
if (!commandAvailable(client)) {
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
const output = (0, node_child_process_1.execFileSync)(client, ["mcp", "get", mcpName], {
|
|
1185
|
+
cwd: projectRoot,
|
|
1186
|
+
encoding: "utf8",
|
|
1187
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1188
|
+
});
|
|
1189
|
+
const list = (0, node_child_process_1.execFileSync)(client, ["mcp", "list"], {
|
|
1190
|
+
cwd: projectRoot,
|
|
1191
|
+
encoding: "utf8",
|
|
1192
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1193
|
+
});
|
|
1194
|
+
if (!list.includes(mcpName)) {
|
|
1195
|
+
return undefined;
|
|
1196
|
+
}
|
|
1197
|
+
return extractMcpUrl(output, mcpUrl);
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
return undefined;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function extractMcpUrl(output, expectedUrl) {
|
|
1204
|
+
if (output.includes(expectedUrl)) {
|
|
1205
|
+
return expectedUrl;
|
|
1206
|
+
}
|
|
1207
|
+
const match = output.match(/https?:\/\/[^\s"',)]+/);
|
|
1208
|
+
return match?.[0];
|
|
1209
|
+
}
|
|
685
1210
|
function sanitizeName(value) {
|
|
686
1211
|
return value
|
|
687
1212
|
.trim()
|
|
@@ -747,9 +1272,36 @@ function setCors(response) {
|
|
|
747
1272
|
response.setHeader("access-control-allow-headers", "content-type,mcp-protocol-version,x-request-id");
|
|
748
1273
|
}
|
|
749
1274
|
function renderAppHtml() {
|
|
750
|
-
const builtPath = (0, node_path_1.join)(__dirname, "
|
|
751
|
-
if ((0, node_fs_1.existsSync)(builtPath)) {
|
|
752
|
-
|
|
1275
|
+
const builtPath = (0, node_path_1.join)(__dirname, "public", "index.html");
|
|
1276
|
+
if (!(0, node_fs_1.existsSync)(builtPath)) {
|
|
1277
|
+
throw new Error("React UI build is missing. Run `npm run build:ui` before starting the HTTP server.");
|
|
753
1278
|
}
|
|
754
|
-
return (0, node_fs_1.readFileSync)(
|
|
1279
|
+
return (0, node_fs_1.readFileSync)(builtPath, "utf8");
|
|
1280
|
+
}
|
|
1281
|
+
function sendStaticAsset(response, pathname) {
|
|
1282
|
+
const publicRoot = (0, node_path_1.join)(__dirname, "public");
|
|
1283
|
+
const filePath = (0, node_path_1.resolve)(publicRoot, `.${pathname}`);
|
|
1284
|
+
if (!filePath.startsWith(publicRoot) || !(0, node_fs_1.existsSync)(filePath) || !(0, node_fs_1.statSync)(filePath).isFile()) {
|
|
1285
|
+
sendJson(response, 404, { error: "Asset not found" });
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
response.statusCode = 200;
|
|
1289
|
+
response.setHeader("content-type", contentTypeFor(filePath));
|
|
1290
|
+
response.end((0, node_fs_1.readFileSync)(filePath));
|
|
1291
|
+
}
|
|
1292
|
+
function contentTypeFor(filePath) {
|
|
1293
|
+
const ext = (0, node_path_1.extname)(filePath);
|
|
1294
|
+
if (ext === ".js")
|
|
1295
|
+
return "text/javascript; charset=utf-8";
|
|
1296
|
+
if (ext === ".css")
|
|
1297
|
+
return "text/css; charset=utf-8";
|
|
1298
|
+
if (ext === ".svg")
|
|
1299
|
+
return "image/svg+xml";
|
|
1300
|
+
if (ext === ".png")
|
|
1301
|
+
return "image/png";
|
|
1302
|
+
if (ext === ".jpg" || ext === ".jpeg")
|
|
1303
|
+
return "image/jpeg";
|
|
1304
|
+
if (ext === ".woff2")
|
|
1305
|
+
return "font/woff2";
|
|
1306
|
+
return "application/octet-stream";
|
|
755
1307
|
}
|