@minasoft/mina-ai-router 0.1.5 → 0.2.1
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 +69 -16
- package/dist/apps/cli/src/index.js +1251 -46
- package/dist/apps/http-server/src/index.js +559 -43
- 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 +2 -2
- package/dist/apps/mcp-server/src/index.js +55 -9
- package/dist/packages/core/src/capability-profile.js +145 -0
- package/dist/packages/core/src/index.js +4 -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/runtime-paths.js +16 -0
- 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 +51 -2
- package/docs/USER-START-GUIDE.md +157 -26
- package/docs/assets/mina-ai-router-overview.svg +109 -0
- package/package.json +8 -2
- package/dist/apps/http-server/src/public/assets/index-Be0tne90.js +0 -9
- package/dist/apps/http-server/src/public/assets/index-CEhd8YGG.css +0 -1
|
@@ -13,7 +13,8 @@ const importEsm = new Function("specifier", "return import(specifier)");
|
|
|
13
13
|
const runtimeModuleUrl = (0, node_url_1.pathToFileURL)((0, node_path_1.join)(__dirname, "../../../../node_modules/@minasoft/mcp-runtime/dist/index.js")).href;
|
|
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
|
-
const statePath = process.env.MINA_ROUTER_STATE ?? (0,
|
|
16
|
+
const statePath = process.env.MINA_ROUTER_STATE ?? (0, src_1.defaultRouterStatePath)();
|
|
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}/`);
|
|
@@ -102,6 +108,13 @@ async function handleRequest(request, response) {
|
|
|
102
108
|
sendJson(response, 200, { agent, state: await getUiState() });
|
|
103
109
|
return;
|
|
104
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
|
+
}
|
|
105
118
|
const agentDeleteMatch = url.pathname.match(/^\/api\/agents\/([^/]+)$/);
|
|
106
119
|
if (agentDeleteMatch && request.method === "PATCH") {
|
|
107
120
|
const body = await readJsonBody(request);
|
|
@@ -154,19 +167,27 @@ async function handleRequest(request, response) {
|
|
|
154
167
|
}
|
|
155
168
|
catch (error) {
|
|
156
169
|
context.save();
|
|
157
|
-
sendJson(response, 500, {
|
|
170
|
+
sendJson(response, error instanceof src_1.AgentNotRouteReadyError ? 409 : 500, {
|
|
158
171
|
error: error instanceof Error ? error.message : String(error),
|
|
159
172
|
state: await getUiState(),
|
|
160
173
|
});
|
|
161
174
|
}
|
|
162
175
|
return;
|
|
163
176
|
}
|
|
164
|
-
const requestActionMatch = url.pathname.match(/^\/api\/requests\/([^/]+)\/(retry|cancel|archive)$/);
|
|
177
|
+
const requestActionMatch = url.pathname.match(/^\/api\/requests\/([^/]+)\/(retry|cancel|archive|unarchive|interrupt|recover)$/);
|
|
165
178
|
if (requestActionMatch && request.method === "POST") {
|
|
166
179
|
const requestId = decodeURIComponent(requestActionMatch[1]);
|
|
167
180
|
const action = requestActionMatch[2];
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|
|
170
191
|
return;
|
|
171
192
|
}
|
|
172
193
|
if (url.pathname === "/api/requests/archive-stale" && request.method === "POST") {
|
|
@@ -178,8 +199,13 @@ async function handleRequest(request, response) {
|
|
|
178
199
|
}
|
|
179
200
|
if (url.pathname === "/api/setup-codex-pair" && request.method === "POST") {
|
|
180
201
|
const body = await readJsonBody(request);
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
209
|
return;
|
|
184
210
|
}
|
|
185
211
|
sendJson(response, 404, { error: "Not found" });
|
|
@@ -196,9 +222,15 @@ function archiveStaleRequests(olderThanMs) {
|
|
|
196
222
|
if (Number.isFinite(updatedAt) && updatedAt > cutoff) {
|
|
197
223
|
continue;
|
|
198
224
|
}
|
|
199
|
-
|
|
225
|
+
const archivedRequest = context.requestStore.updateStatus(request.id, "archived", {
|
|
226
|
+
archivedAt: new Date().toISOString(),
|
|
227
|
+
archivedFromStatus: request.status,
|
|
200
228
|
error: request.error ?? "Archived as stale by operator.",
|
|
201
|
-
|
|
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);
|
|
202
234
|
}
|
|
203
235
|
if (archived.length > 0) {
|
|
204
236
|
context.save();
|
|
@@ -208,26 +240,73 @@ function archiveStaleRequests(olderThanMs) {
|
|
|
208
240
|
async function handleRequestAction(requestId, action) {
|
|
209
241
|
const request = context.requestStore.require(requestId);
|
|
210
242
|
if (action === "retry") {
|
|
211
|
-
|
|
243
|
+
context.requestStore.assertActionAllowed(request, "retry");
|
|
244
|
+
const result = await context.router.callAgent({
|
|
212
245
|
sourceAgent: request.sourceAgent,
|
|
213
246
|
target: request.targetAgent,
|
|
214
247
|
task: request.task,
|
|
248
|
+
retryOfRequestId: request.id,
|
|
215
249
|
});
|
|
250
|
+
context.requestStore.recordRetry(request.id, result.requestId);
|
|
251
|
+
context.save();
|
|
252
|
+
return result;
|
|
216
253
|
}
|
|
217
254
|
if (action === "cancel") {
|
|
218
|
-
const updated = context.requestStore.
|
|
219
|
-
|
|
220
|
-
});
|
|
255
|
+
const updated = context.requestStore.cancel(requestId, "Cancelled by operator from Mina AI Router UI.", "ui");
|
|
256
|
+
clearAgentLeaseForRequest(updated);
|
|
221
257
|
context.save();
|
|
222
258
|
return updated;
|
|
223
259
|
}
|
|
260
|
+
if (action === "interrupt") {
|
|
261
|
+
const updated = interruptRequest(requestId, "ui");
|
|
262
|
+
context.save();
|
|
263
|
+
return updated;
|
|
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
|
+
}
|
|
224
269
|
if (action === "archive") {
|
|
225
|
-
|
|
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);
|
|
226
274
|
context.save();
|
|
227
275
|
return updated;
|
|
228
276
|
}
|
|
229
277
|
throw new Error(`Unsupported request action "${action}".`);
|
|
230
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
|
+
}
|
|
231
310
|
async function handleMcp(request, response, url) {
|
|
232
311
|
const handler = await getMcpHandler();
|
|
233
312
|
const body = await readRawBody(request);
|
|
@@ -285,14 +364,16 @@ async function getHealth() {
|
|
|
285
364
|
const requests = context.router.listRequests();
|
|
286
365
|
const openRequests = requests.filter((request) => ["created", "sent", "waiting"].includes(request.status));
|
|
287
366
|
return {
|
|
288
|
-
ok: agents.every((agent) =>
|
|
367
|
+
ok: agents.every((agent) => !["missing", "stale", "needs-attention"].includes(agent.status)),
|
|
289
368
|
statePath,
|
|
290
369
|
mcpUrl: `http://${host}:${port}/mcp`,
|
|
291
370
|
agents: {
|
|
292
371
|
total: agents.length,
|
|
293
372
|
available: agents.filter((agent) => agent.status === "available").length,
|
|
294
373
|
busy: agents.filter((agent) => agent.status === "busy").length,
|
|
374
|
+
stale: agents.filter((agent) => agent.status === "stale").length,
|
|
295
375
|
missing: agents.filter((agent) => agent.status === "missing").length,
|
|
376
|
+
needsAttention: agents.filter((agent) => agent.status === "needs-attention").length,
|
|
296
377
|
unknown: agents.filter((agent) => agent.status === "unknown").length,
|
|
297
378
|
},
|
|
298
379
|
requests: {
|
|
@@ -309,6 +390,7 @@ function registerAgent(body) {
|
|
|
309
390
|
const id = requiredString(body.id, "id");
|
|
310
391
|
const projectRoot = stringValue(body.projectRoot) ?? process.cwd();
|
|
311
392
|
const capabilityNotice = inferCapabilityNotice(projectRoot);
|
|
393
|
+
const now = new Date().toISOString();
|
|
312
394
|
const agent = {
|
|
313
395
|
id,
|
|
314
396
|
name: stringValue(body.name) ?? id,
|
|
@@ -320,22 +402,149 @@ function registerAgent(body) {
|
|
|
320
402
|
startupCommand: stringValue(body.startupCommand),
|
|
321
403
|
capabilitySummary: stringValue(body.capabilitySummary) ?? capabilityNotice.summary,
|
|
322
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),
|
|
323
421
|
};
|
|
324
|
-
context.registry.register(agent
|
|
422
|
+
const registered = context.registry.register(agent, {
|
|
423
|
+
capabilitySource: agent.capabilitySummary || agent.capabilitySources ? "generated" : undefined,
|
|
424
|
+
});
|
|
325
425
|
context.save();
|
|
326
|
-
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;
|
|
327
450
|
}
|
|
328
451
|
function updateAgent(id, body) {
|
|
329
452
|
const current = context.registry.require(id);
|
|
453
|
+
const capabilitySummary = stringFieldValue(body, "capabilitySummary");
|
|
454
|
+
const capabilitySources = stringFieldValue(body, "capabilitySources");
|
|
330
455
|
const next = {
|
|
331
456
|
...current,
|
|
332
457
|
name: stringValue(body.name) ?? current.name,
|
|
333
|
-
capabilitySummary: stringFieldValue(body, "capabilitySummary") ?? current.capabilitySummary,
|
|
334
|
-
capabilitySources: stringFieldValue(body, "capabilitySources") ?? current.capabilitySources,
|
|
335
458
|
};
|
|
336
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);
|
|
337
467
|
context.save();
|
|
338
|
-
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
|
+
};
|
|
339
548
|
}
|
|
340
549
|
function listDirectories(body) {
|
|
341
550
|
const home = process.env.HOME ?? process.cwd();
|
|
@@ -381,13 +590,39 @@ function captureAgentTerminal(id) {
|
|
|
381
590
|
}
|
|
382
591
|
const tmux = new src_2.TmuxClient({ captureLines: 120 });
|
|
383
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
|
+
}
|
|
384
617
|
return {
|
|
385
|
-
agent,
|
|
618
|
+
agent: nextAgent,
|
|
386
619
|
terminal: {
|
|
387
620
|
text,
|
|
388
621
|
capturedAt: new Date().toISOString(),
|
|
389
|
-
trustPrompt:
|
|
390
|
-
|
|
622
|
+
trustPrompt: Boolean(bootstrapPrompt && bootstrapPrompt.kind !== "client-update"),
|
|
623
|
+
permissionPrompt: bootstrapPrompt,
|
|
624
|
+
actions: terminalActions(nextAgent, bootstrapPrompt),
|
|
625
|
+
pendingRegistration: needsSelfRegistration(nextAgent),
|
|
391
626
|
},
|
|
392
627
|
};
|
|
393
628
|
}
|
|
@@ -399,8 +634,16 @@ function sendAgentTerminalInput(id, body) {
|
|
|
399
634
|
const target = agent.tmuxTarget ?? agent.sessionId;
|
|
400
635
|
const text = stringValue(body.text);
|
|
401
636
|
const enter = body.enter === true;
|
|
637
|
+
const actionId = stringValue(body.actionId);
|
|
402
638
|
const tmux = new src_2.TmuxClient({ captureLines: 120 });
|
|
403
|
-
|
|
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) {
|
|
404
647
|
if (agent.agentType === "codex") {
|
|
405
648
|
tmux.sendCodexText(target, text);
|
|
406
649
|
}
|
|
@@ -412,10 +655,23 @@ function sendAgentTerminalInput(id, body) {
|
|
|
412
655
|
tmux.sendEnter(target);
|
|
413
656
|
}
|
|
414
657
|
let registration = "unchanged";
|
|
415
|
-
|
|
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)) {
|
|
416
665
|
sleep(1200);
|
|
417
666
|
const capture = tmux.capture(target);
|
|
418
|
-
|
|
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) {
|
|
419
675
|
if (agent.agentType === "codex") {
|
|
420
676
|
tmux.sendCodexText(target, buildSelfRegistrationPrompt(agent));
|
|
421
677
|
}
|
|
@@ -423,6 +679,12 @@ function sendAgentTerminalInput(id, body) {
|
|
|
423
679
|
tmux.sendText(target, buildSelfRegistrationPrompt(agent));
|
|
424
680
|
}
|
|
425
681
|
registration = "registration prompt sent to agent";
|
|
682
|
+
context.registry.register({
|
|
683
|
+
...agent,
|
|
684
|
+
bootstrapStatus: "registration-pending",
|
|
685
|
+
registrationStatus: "pending",
|
|
686
|
+
});
|
|
687
|
+
context.save();
|
|
426
688
|
}
|
|
427
689
|
}
|
|
428
690
|
return {
|
|
@@ -445,6 +707,20 @@ function createTmuxAgent(body) {
|
|
|
445
707
|
const sessionId = stringValue(body.sessionId) ?? `${agentType}-${sanitizeName(projectName)}`;
|
|
446
708
|
const startupCommand = stringValue(body.startupCommand)
|
|
447
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
|
+
});
|
|
448
724
|
const command = startupCommand.split(/\s+/)[0];
|
|
449
725
|
assertCommandAvailable("tmux");
|
|
450
726
|
assertCommandAvailable(command);
|
|
@@ -456,12 +732,26 @@ function createTmuxAgent(body) {
|
|
|
456
732
|
sessionId,
|
|
457
733
|
projectRoot,
|
|
458
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,
|
|
459
749
|
...uiCreatedCapabilityNotice(projectRoot),
|
|
460
750
|
};
|
|
461
751
|
const tmux = new src_2.TmuxClient();
|
|
462
752
|
const existed = tmux.hasSession(sessionId);
|
|
463
753
|
tmux.ensureSession(agent);
|
|
464
|
-
context.registry.register(agent);
|
|
754
|
+
let registeredAgent = context.registry.register(agent);
|
|
465
755
|
context.save();
|
|
466
756
|
const sendRegistrationPrompt = body.sendRegistrationPrompt !== false;
|
|
467
757
|
let registration = "registration prompt skipped";
|
|
@@ -470,33 +760,102 @@ function createTmuxAgent(body) {
|
|
|
470
760
|
const delayMs = typeof body.registerDelayMs === "number" ? body.registerDelayMs : 4000;
|
|
471
761
|
sleep(delayMs);
|
|
472
762
|
const capture = tmux.capture(sessionId);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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();
|
|
476
783
|
}
|
|
477
784
|
else {
|
|
478
785
|
const prompt = buildSelfRegistrationPrompt(agent);
|
|
479
|
-
|
|
480
|
-
|
|
786
|
+
try {
|
|
787
|
+
if (agentType === "codex") {
|
|
788
|
+
tmux.sendCodexText(sessionId, prompt);
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
tmux.sendText(sessionId, prompt);
|
|
792
|
+
}
|
|
481
793
|
}
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
};
|
|
484
818
|
}
|
|
485
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();
|
|
486
826
|
}
|
|
487
827
|
}
|
|
488
828
|
return {
|
|
489
|
-
agent,
|
|
829
|
+
agent: registeredAgent,
|
|
490
830
|
existed,
|
|
491
831
|
registration,
|
|
492
832
|
nextAction,
|
|
833
|
+
permissionProfile,
|
|
834
|
+
mcpPreflight,
|
|
493
835
|
attachCommand: `tmux attach -t ${sessionId}`,
|
|
494
836
|
mairAttachCommand: `mair attach ${id}`,
|
|
495
837
|
};
|
|
496
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
|
+
}
|
|
497
853
|
function setupCodexPair(body) {
|
|
498
|
-
const mainRoot = stringValue(body.mainRoot)
|
|
499
|
-
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
|
+
}
|
|
500
859
|
const helperId = stringValue(body.helperId) ?? "ralph";
|
|
501
860
|
const sessionId = stringValue(body.sessionId) ?? "mina-ralph-codex";
|
|
502
861
|
const agent = {
|
|
@@ -530,14 +889,18 @@ function buildSelfRegistrationPrompt(agent) {
|
|
|
530
889
|
`- agentType: ${agent.agentType}`,
|
|
531
890
|
`- transport: ${agent.transport}`,
|
|
532
891
|
`- sessionId: ${agent.sessionId}`,
|
|
892
|
+
`- sessionFingerprint: ${agent.sessionFingerprint ?? agent.sessionId}`,
|
|
533
893
|
`- projectRoot: ${agent.projectRoot}`,
|
|
534
894
|
`- startupCommand: ${agent.startupCommand ?? ""}`,
|
|
535
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
|
+
"",
|
|
536
898
|
"Before registering, create a concise capability notice for this session:",
|
|
537
899
|
"- Prefer capability docs in this order when present: CLAUDE.md/claude.md, AGENTS.md/agents.md, agent.md, README.md.",
|
|
538
900
|
"- If those files are missing, inspect package metadata and the project file tree to infer what this project/agent can help with.",
|
|
539
901
|
"- Set register_agent capabilitySummary to 2-5 short bullets or one short paragraph under 800 characters.",
|
|
540
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.",
|
|
541
904
|
"After registering, call list_agents and confirm this agent is available.",
|
|
542
905
|
].join("\n");
|
|
543
906
|
}
|
|
@@ -672,24 +1035,177 @@ function truncateText(value, maxLength) {
|
|
|
672
1035
|
const normalized = value.replace(/\s+/g, " ").trim();
|
|
673
1036
|
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1).trim()}...` : normalized;
|
|
674
1037
|
}
|
|
675
|
-
function isCodexTrustPrompt(capture) {
|
|
676
|
-
return capture.includes("Do you trust the contents of this directory?")
|
|
677
|
-
|| capture.includes("Press enter to continue")
|
|
678
|
-
|| capture.includes("Yes, continue");
|
|
679
|
-
}
|
|
680
1038
|
function isPendingUiRegistration(agent) {
|
|
681
1039
|
return agent.capabilitySources?.startsWith("created from Mina UI") ?? false;
|
|
682
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
|
+
}
|
|
683
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) {
|
|
684
1168
|
try {
|
|
685
1169
|
(0, node_child_process_1.execFileSync)("which", [command], {
|
|
686
1170
|
encoding: "utf8",
|
|
687
1171
|
stdio: ["ignore", "pipe", "pipe"],
|
|
688
1172
|
});
|
|
1173
|
+
return true;
|
|
689
1174
|
}
|
|
690
1175
|
catch {
|
|
691
|
-
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
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;
|
|
692
1206
|
}
|
|
1207
|
+
const match = output.match(/https?:\/\/[^\s"',)]+/);
|
|
1208
|
+
return match?.[0];
|
|
693
1209
|
}
|
|
694
1210
|
function sanitizeName(value) {
|
|
695
1211
|
return value
|