agent-relay-server 0.4.16 → 0.4.18
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 +84 -2
- package/package.json +2 -2
- package/public/dashboard.js +32 -2
- package/public/index.html +19 -0
- package/src/cli.ts +441 -0
- package/src/config.ts +1 -0
- package/src/db.ts +597 -28
- package/src/index.ts +13 -3
- package/src/routes.ts +330 -13
- package/src/security.ts +31 -1
- package/src/sse.ts +8 -2
- package/src/types.ts +65 -0
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
3
5
|
import {
|
|
4
6
|
createDaemonPlan,
|
|
5
7
|
detectDaemonEnvironment,
|
|
@@ -23,8 +25,30 @@ Usage:
|
|
|
23
25
|
agent-relay [start]
|
|
24
26
|
agent-relay setup [--yes] [--dry-run] [--force] [--env-file PATH] [--host HOST] [--port PORT] [--db-path PATH] [--token TOKEN|--no-token]
|
|
25
27
|
agent-relay daemon <install|uninstall|start|stop|restart|enable|disable|status|logs> [options]
|
|
28
|
+
agent-relay pair <target|create|status|accept|reject|hangup|send> [options]
|
|
29
|
+
agent-relay message <target> <body> [options]
|
|
30
|
+
agent-relay /pair <target|accept|reject|send|status> [...]
|
|
31
|
+
agent-relay /message <target> <body>
|
|
32
|
+
agent-relay /send-claimable <target> <body>
|
|
33
|
+
agent-relay /disconnect [PAIR_ID]
|
|
34
|
+
agent-relay /status
|
|
35
|
+
agent-relay /label [LABEL]
|
|
36
|
+
agent-relay /tags [TAG ...]
|
|
26
37
|
agent-relay --help
|
|
27
38
|
|
|
39
|
+
Pair examples:
|
|
40
|
+
agent-relay pair codex --objective "Debug flaky tests"
|
|
41
|
+
agent-relay /pair codex "Debug flaky tests"
|
|
42
|
+
agent-relay pair status
|
|
43
|
+
agent-relay pair accept PAIR_ID --agent AGENT_ID
|
|
44
|
+
agent-relay pair send PAIR_ID --from AGENT_ID --body "What do you see?"
|
|
45
|
+
agent-relay /message codex "Can you look at that failing action?"
|
|
46
|
+
agent-relay /send-claimable tag:backend "Please claim and fix the failing API test"
|
|
47
|
+
agent-relay /disconnect
|
|
48
|
+
agent-relay /status
|
|
49
|
+
agent-relay /label backend-fixer
|
|
50
|
+
agent-relay /tags backend tests urgent
|
|
51
|
+
|
|
28
52
|
Daemon options:
|
|
29
53
|
--env-file PATH Env file sourced by the daemon (default: platform user config dir)
|
|
30
54
|
--binary PATH Stable agent-relay binary/script path for the service
|
|
@@ -71,9 +95,45 @@ export async function handleCli(args: string[]): Promise<"start" | "handled"> {
|
|
|
71
95
|
await handleDaemonCommand(args.slice(1));
|
|
72
96
|
return "handled";
|
|
73
97
|
}
|
|
98
|
+
if (command === "pair" || command === "/pair" || command === "/disconnect") {
|
|
99
|
+
await handleSlashOrPairCommand(command, args.slice(1));
|
|
100
|
+
return "handled";
|
|
101
|
+
}
|
|
102
|
+
if (command === "message" || command === "send" || command === "/message" || command === "/send" || command === "/send-claimable") {
|
|
103
|
+
await handleMessageCommand(args.slice(1), { claimable: command === "/send-claimable" });
|
|
104
|
+
return "handled";
|
|
105
|
+
}
|
|
106
|
+
if (command === "/status" || command === "status") {
|
|
107
|
+
await handleStatusCommand(args.slice(1));
|
|
108
|
+
return "handled";
|
|
109
|
+
}
|
|
110
|
+
if (command === "/label" || command === "label") {
|
|
111
|
+
await handleLabelCommand(args.slice(1));
|
|
112
|
+
return "handled";
|
|
113
|
+
}
|
|
114
|
+
if (command === "/tags" || command === "tags") {
|
|
115
|
+
await handleTagsCommand(args.slice(1));
|
|
116
|
+
return "handled";
|
|
117
|
+
}
|
|
118
|
+
if (command === "/reconnect") {
|
|
119
|
+
console.log("Reconnect is handled automatically by provider sidecars; use `agent-relay pair status` to inspect current pair state.");
|
|
120
|
+
return "handled";
|
|
121
|
+
}
|
|
74
122
|
throw new Error(`Unknown command "${command}". Run agent-relay --help.`);
|
|
75
123
|
}
|
|
76
124
|
|
|
125
|
+
async function handleSlashOrPairCommand(command: string, args: string[]): Promise<void> {
|
|
126
|
+
if (command === "/disconnect") {
|
|
127
|
+
await handlePairCommand(["hangup", ...args]);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (command === "/pair") {
|
|
131
|
+
await handlePairCommand(args);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
await handlePairCommand(args);
|
|
135
|
+
}
|
|
136
|
+
|
|
77
137
|
async function handleSetupCommand(args: string[]): Promise<void> {
|
|
78
138
|
let envFile: string | undefined;
|
|
79
139
|
let host: string | undefined;
|
|
@@ -205,6 +265,387 @@ function parseDaemonAction(value: string | undefined): DaemonAction {
|
|
|
205
265
|
return value as DaemonAction;
|
|
206
266
|
}
|
|
207
267
|
|
|
268
|
+
async function handlePairCommand(args: string[]): Promise<void> {
|
|
269
|
+
if (!args.length) throw new Error("Usage: agent-relay pair <target|create|status|accept|reject|hangup|send> [options]");
|
|
270
|
+
const knownActions = new Set(["create", "status", "list", "accept", "reject", "hangup", "disconnect", "send"]);
|
|
271
|
+
const action = knownActions.has(args[0]!) ? args[0]! : "create";
|
|
272
|
+
const rest = action === "create" && args[0] !== "create" ? args : args.slice(1);
|
|
273
|
+
|
|
274
|
+
if (action === "status" || action === "list") {
|
|
275
|
+
let agent: string | undefined = await detectAgentId();
|
|
276
|
+
let status: string | undefined;
|
|
277
|
+
let json = false;
|
|
278
|
+
for (let i = 0; i < rest.length; i++) {
|
|
279
|
+
const arg = rest[i];
|
|
280
|
+
if (arg === "--agent" && i + 1 < rest.length) agent = rest[++i];
|
|
281
|
+
else if (arg === "--status" && i + 1 < rest.length) status = rest[++i];
|
|
282
|
+
else if (arg === "--json") json = true;
|
|
283
|
+
else throw new Error(`Unknown pair status option "${arg}"`);
|
|
284
|
+
}
|
|
285
|
+
const query = new URLSearchParams();
|
|
286
|
+
if (agent) query.set("agent", agent);
|
|
287
|
+
if (status) query.set("status", status);
|
|
288
|
+
const pairs = await apiRequest("GET", `/api/pairs${query.size ? `?${query}` : ""}`);
|
|
289
|
+
if (json) console.log(JSON.stringify(pairs, null, 2));
|
|
290
|
+
else console.log(formatPairs(pairs as any[]));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (action === "create") {
|
|
295
|
+
const target = rest[0];
|
|
296
|
+
if (!target || target.startsWith("--")) throw new Error("Usage: agent-relay pair <target> [--from AGENT_ID] [--objective TEXT]");
|
|
297
|
+
let from = await detectAgentId();
|
|
298
|
+
let objective: string | undefined;
|
|
299
|
+
let ttlMs: number | undefined;
|
|
300
|
+
let json = false;
|
|
301
|
+
const objectiveParts: string[] = [];
|
|
302
|
+
for (let i = 1; i < rest.length; i++) {
|
|
303
|
+
const arg = rest[i];
|
|
304
|
+
if (arg === "--from" && i + 1 < rest.length) from = rest[++i];
|
|
305
|
+
else if (arg === "--objective" && i + 1 < rest.length) objective = rest[++i];
|
|
306
|
+
else if (arg === "--ttl-ms" && i + 1 < rest.length) ttlMs = parseInt(rest[++i]!, 10);
|
|
307
|
+
else if (arg === "--json") json = true;
|
|
308
|
+
else objectiveParts.push(arg!);
|
|
309
|
+
}
|
|
310
|
+
objective ??= objectiveParts.length ? objectiveParts.join(" ") : undefined;
|
|
311
|
+
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
312
|
+
const result = await apiRequest("POST", "/api/pairs", { from, target, objective, ttlMs });
|
|
313
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
314
|
+
else {
|
|
315
|
+
const pair = (result as any).pair;
|
|
316
|
+
console.log(`Pair invite ${pair.id} sent: ${pair.requesterId} -> ${pair.targetId}`);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (action === "accept" || action === "reject" || action === "hangup" || action === "disconnect") {
|
|
322
|
+
const pairId = rest[0];
|
|
323
|
+
let agentId = await detectAgentId();
|
|
324
|
+
let reason: string | undefined;
|
|
325
|
+
let json = false;
|
|
326
|
+
let startIndex = 0;
|
|
327
|
+
if (pairId && !pairId.startsWith("--")) startIndex = 1;
|
|
328
|
+
for (let i = startIndex; i < rest.length; i++) {
|
|
329
|
+
const arg = rest[i];
|
|
330
|
+
if (arg === "--agent" && i + 1 < rest.length) agentId = rest[++i];
|
|
331
|
+
else if (arg === "--reason" && i + 1 < rest.length) reason = rest[++i];
|
|
332
|
+
else if (arg === "--json") json = true;
|
|
333
|
+
else throw new Error(`Unknown pair ${action} option "${arg}"`);
|
|
334
|
+
}
|
|
335
|
+
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
336
|
+
const resolvedPairId = pairId && !pairId.startsWith("--") ? pairId : await detectActivePairId(agentId);
|
|
337
|
+
if (!resolvedPairId) throw new Error(`Usage: agent-relay pair ${action} PAIR_ID --agent AGENT_ID`);
|
|
338
|
+
const endpoint = action === "disconnect" ? "hangup" : action;
|
|
339
|
+
const pair = await apiRequest("POST", `/api/pairs/${encodeURIComponent(resolvedPairId)}/${endpoint}`, { agentId, reason });
|
|
340
|
+
if (json) console.log(JSON.stringify(pair, null, 2));
|
|
341
|
+
else console.log(`Pair ${resolvedPairId}: ${(pair as any).status}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (action === "send") {
|
|
346
|
+
const pairId = rest[0];
|
|
347
|
+
if (!pairId || pairId.startsWith("--")) throw new Error("Usage: agent-relay pair send PAIR_ID --from AGENT_ID --body TEXT");
|
|
348
|
+
let from = await detectAgentId();
|
|
349
|
+
let body: string | undefined;
|
|
350
|
+
let subject: string | undefined;
|
|
351
|
+
let json = false;
|
|
352
|
+
for (let i = 1; i < rest.length; i++) {
|
|
353
|
+
const arg = rest[i];
|
|
354
|
+
if (arg === "--from" && i + 1 < rest.length) from = rest[++i];
|
|
355
|
+
else if (arg === "--body" && i + 1 < rest.length) body = rest[++i];
|
|
356
|
+
else if (arg === "--subject" && i + 1 < rest.length) subject = rest[++i];
|
|
357
|
+
else if (arg === "--json") json = true;
|
|
358
|
+
else throw new Error(`Unknown pair send option "${arg}"`);
|
|
359
|
+
}
|
|
360
|
+
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
361
|
+
if (!body) throw new Error("--body TEXT required");
|
|
362
|
+
const result = await apiRequest("POST", `/api/pairs/${encodeURIComponent(pairId)}/messages`, { from, body, subject });
|
|
363
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
364
|
+
else console.log(`Pair message sent: ${(result as any).message.id}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function handleMessageCommand(args: string[], defaults: { claimable?: boolean } = {}): Promise<void> {
|
|
370
|
+
const target = args[0];
|
|
371
|
+
if (!target || target.startsWith("--")) {
|
|
372
|
+
throw new Error("Usage: agent-relay message <target> <body> [--from AGENT_ID] [--subject TEXT] [--channel NAME] [--reply-to ID] [--claimable]");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let from = await detectAgentId();
|
|
376
|
+
let subject: string | undefined;
|
|
377
|
+
let channel: string | undefined;
|
|
378
|
+
let replyTo: number | undefined;
|
|
379
|
+
let idempotencyKey: string | undefined;
|
|
380
|
+
let json = false;
|
|
381
|
+
let claimable = defaults.claimable ?? false;
|
|
382
|
+
const bodyParts: string[] = [];
|
|
383
|
+
|
|
384
|
+
for (let i = 1; i < args.length; i++) {
|
|
385
|
+
const arg = args[i];
|
|
386
|
+
if (arg === "--from" && i + 1 < args.length) from = args[++i];
|
|
387
|
+
else if (arg === "--subject" && i + 1 < args.length) subject = args[++i];
|
|
388
|
+
else if (arg === "--channel" && i + 1 < args.length) channel = args[++i];
|
|
389
|
+
else if (arg === "--reply-to" && i + 1 < args.length) {
|
|
390
|
+
const parsed = Number.parseInt(args[++i]!, 10);
|
|
391
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error("--reply-to must be a positive message id");
|
|
392
|
+
replyTo = parsed;
|
|
393
|
+
} else if (arg === "--idempotency-key" && i + 1 < args.length) idempotencyKey = args[++i];
|
|
394
|
+
else if (arg === "--claimable") claimable = true;
|
|
395
|
+
else if (arg === "--json") json = true;
|
|
396
|
+
else bodyParts.push(arg!);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const body = bodyParts.join(" ").trim();
|
|
400
|
+
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
401
|
+
if (!body) throw new Error("Message body required.");
|
|
402
|
+
|
|
403
|
+
const message = await apiRequest("POST", "/api/messages", {
|
|
404
|
+
from,
|
|
405
|
+
to: target,
|
|
406
|
+
subject,
|
|
407
|
+
channel,
|
|
408
|
+
body,
|
|
409
|
+
replyTo,
|
|
410
|
+
claimable,
|
|
411
|
+
idempotencyKey,
|
|
412
|
+
});
|
|
413
|
+
if (json) console.log(JSON.stringify(message, null, 2));
|
|
414
|
+
else console.log(`${claimable ? "Claimable message" : "Message"} sent: ${(message as any).id} -> ${target}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function handleStatusCommand(args: string[]): Promise<void> {
|
|
418
|
+
let agentId = await detectAgentId();
|
|
419
|
+
let json = false;
|
|
420
|
+
for (let i = 0; i < args.length; i++) {
|
|
421
|
+
const arg = args[i];
|
|
422
|
+
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
423
|
+
else if (arg === "--json") json = true;
|
|
424
|
+
else throw new Error(`Unknown status option "${arg}"`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const [stats, health, pairs, agent] = await Promise.all([
|
|
428
|
+
apiRequest("GET", "/api/stats"),
|
|
429
|
+
apiRequest("GET", "/api/health"),
|
|
430
|
+
agentId ? apiRequest("GET", `/api/pairs?agent=${encodeURIComponent(agentId)}`) : Promise.resolve([]),
|
|
431
|
+
agentId ? apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`).catch(() => null) : Promise.resolve(null),
|
|
432
|
+
]);
|
|
433
|
+
const payload = { agent, stats, health, pairs };
|
|
434
|
+
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
435
|
+
else console.log(formatStatus(payload));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function handleLabelCommand(args: string[]): Promise<void> {
|
|
439
|
+
let agentId = await detectAgentId();
|
|
440
|
+
let label: string | null | undefined;
|
|
441
|
+
let json = false;
|
|
442
|
+
for (let i = 0; i < args.length; i++) {
|
|
443
|
+
const arg = args[i];
|
|
444
|
+
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
445
|
+
else if (arg === "--clear") label = null;
|
|
446
|
+
else if (arg === "--json") json = true;
|
|
447
|
+
else if (label === undefined) label = args.slice(i).join(" ");
|
|
448
|
+
}
|
|
449
|
+
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
450
|
+
if (label === undefined) {
|
|
451
|
+
const agent = await apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`) as { label?: string };
|
|
452
|
+
console.log(agent.label ?? "(no label)");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const result = await apiRequest("PATCH", `/api/agents/${encodeURIComponent(agentId)}/label`, { label });
|
|
456
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
457
|
+
else console.log(label ? `Label set: ${label}` : "Label cleared.");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function handleTagsCommand(args: string[]): Promise<void> {
|
|
461
|
+
let agentId = await detectAgentId();
|
|
462
|
+
let json = false;
|
|
463
|
+
let listOnly = false;
|
|
464
|
+
let add: string[] = [];
|
|
465
|
+
let remove: string[] = [];
|
|
466
|
+
const positional: string[] = [];
|
|
467
|
+
for (let i = 0; i < args.length; i++) {
|
|
468
|
+
const arg = args[i];
|
|
469
|
+
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
470
|
+
else if (arg === "--json") json = true;
|
|
471
|
+
else if (arg === "--list") listOnly = true;
|
|
472
|
+
else if (arg === "--add" && i + 1 < args.length) add = add.concat(splitTagArgs(args[++i]!));
|
|
473
|
+
else if (arg === "--remove" && i + 1 < args.length) remove = remove.concat(splitTagArgs(args[++i]!));
|
|
474
|
+
else positional.push(...splitTagArgs(arg!));
|
|
475
|
+
}
|
|
476
|
+
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
477
|
+
const current = await apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`) as { tags?: string[] };
|
|
478
|
+
if (listOnly || (positional.length === 0 && add.length === 0 && remove.length === 0)) {
|
|
479
|
+
console.log((current.tags ?? []).join(", ") || "(no tags)");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const next = positional.length > 0
|
|
483
|
+
? uniqueStrings(positional)
|
|
484
|
+
: uniqueStrings([...(current.tags ?? []), ...add]).filter((tag) => !remove.includes(tag));
|
|
485
|
+
const updated = await apiRequest("PATCH", `/api/agents/${encodeURIComponent(agentId)}/tags`, { tags: next });
|
|
486
|
+
if (json) console.log(JSON.stringify(updated, null, 2));
|
|
487
|
+
else console.log(`Tags: ${next.join(", ") || "(none)"}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function apiRequest(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
491
|
+
const baseUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
492
|
+
const headers: Record<string, string> = {};
|
|
493
|
+
const token = process.env.AGENT_RELAY_TOKEN;
|
|
494
|
+
if (token) headers["X-Agent-Relay-Token"] = token;
|
|
495
|
+
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
496
|
+
const response = await fetch(new URL(path, baseUrl), {
|
|
497
|
+
method,
|
|
498
|
+
headers,
|
|
499
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
500
|
+
});
|
|
501
|
+
const text = await response.text();
|
|
502
|
+
const payload = text ? JSON.parse(text) : null;
|
|
503
|
+
if (!response.ok) {
|
|
504
|
+
const message = payload && typeof payload === "object" && "error" in payload ? String((payload as any).error) : text;
|
|
505
|
+
throw new Error(`agent-relay ${method} ${path} failed (${response.status}): ${message}`);
|
|
506
|
+
}
|
|
507
|
+
return payload;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function splitTagArgs(raw: string): string[] {
|
|
511
|
+
return raw.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function uniqueStrings(values: string[]): string[] {
|
|
515
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function detectActivePairId(agentId: string): Promise<string | undefined> {
|
|
519
|
+
const pairs = await apiRequest("GET", `/api/pairs?agent=${encodeURIComponent(agentId)}&status=active`) as Array<{ id?: string }>;
|
|
520
|
+
return Array.isArray(pairs) && typeof pairs[0]?.id === "string" ? pairs[0].id : undefined;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function detectAgentId(): Promise<string | undefined> {
|
|
524
|
+
const explicit = process.env.AGENT_RELAY_ID;
|
|
525
|
+
if (explicit) return explicit;
|
|
526
|
+
|
|
527
|
+
const cwd = process.cwd();
|
|
528
|
+
const stateCandidates = [
|
|
529
|
+
process.env.AGENT_RELAY_CODEX_STATE_PATH,
|
|
530
|
+
resolve(cwd, "codex/runtime/live-state.json"),
|
|
531
|
+
...collectCodexStateFiles(),
|
|
532
|
+
].filter((path): path is string => Boolean(path));
|
|
533
|
+
|
|
534
|
+
const codexMatch = newestCodexAgentId(stateCandidates, cwd);
|
|
535
|
+
if (codexMatch) return codexMatch;
|
|
536
|
+
|
|
537
|
+
const claudeMatch = newestClaudeAgentId();
|
|
538
|
+
if (claudeMatch) return claudeMatch;
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const agents = await apiRequest("GET", "/api/agents") as Array<{ id?: string; status?: string; ready?: boolean; meta?: { cwd?: unknown }; lastSeen?: number }>;
|
|
542
|
+
const cwdAgents = agents
|
|
543
|
+
.filter((agent) => agent.status !== "offline" && agent.ready !== false && agent.meta?.cwd === cwd && typeof agent.id === "string")
|
|
544
|
+
.sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0));
|
|
545
|
+
return cwdAgents[0]?.id;
|
|
546
|
+
} catch {
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function newestCodexAgentId(paths: string[], cwd: string): string | undefined {
|
|
552
|
+
const states = paths
|
|
553
|
+
.map((path) => readCodexState(path))
|
|
554
|
+
.filter((state): state is { agentId: string; cwd?: string; updatedAtMs: number } => Boolean(state))
|
|
555
|
+
.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
556
|
+
return states.find((state) => state.cwd === cwd)?.agentId ?? states[0]?.agentId;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function readCodexState(path: string): { agentId: string; cwd?: string; updatedAtMs: number } | null {
|
|
560
|
+
if (!existsSync(path)) return null;
|
|
561
|
+
try {
|
|
562
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as { agentId?: unknown; cwd?: unknown; updatedAt?: unknown };
|
|
563
|
+
if (typeof parsed.agentId !== "string" || !parsed.agentId) return null;
|
|
564
|
+
const stat = statSync(path);
|
|
565
|
+
const updatedAt = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
|
|
566
|
+
return {
|
|
567
|
+
agentId: parsed.agentId,
|
|
568
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined,
|
|
569
|
+
updatedAtMs: Number.isFinite(updatedAt) ? updatedAt : stat.mtimeMs,
|
|
570
|
+
};
|
|
571
|
+
} catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function collectCodexStateFiles(): string[] {
|
|
577
|
+
const roots = [
|
|
578
|
+
join(process.env.HOME || "", ".agent-relay", "codex", "runtime"),
|
|
579
|
+
resolve(process.cwd(), "codex", "runtime"),
|
|
580
|
+
].filter((root) => root && existsSync(root));
|
|
581
|
+
const files: string[] = [];
|
|
582
|
+
for (const root of roots) collectFiles(root, "live-state.json", files, 4);
|
|
583
|
+
return files;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function collectFiles(dir: string, name: string, output: string[], depth: number): void {
|
|
587
|
+
if (depth < 0) return;
|
|
588
|
+
let entries: string[];
|
|
589
|
+
try {
|
|
590
|
+
entries = readdirSync(dir);
|
|
591
|
+
} catch {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
for (const entry of entries) {
|
|
595
|
+
const path = join(dir, entry);
|
|
596
|
+
try {
|
|
597
|
+
const stat = statSync(path);
|
|
598
|
+
if (stat.isDirectory()) collectFiles(path, name, output, depth - 1);
|
|
599
|
+
else if (entry === name) output.push(path);
|
|
600
|
+
} catch {
|
|
601
|
+
// Ignore state files that disappear while scanning.
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function newestClaudeAgentId(): string | undefined {
|
|
607
|
+
if (!existsSync("/tmp")) return undefined;
|
|
608
|
+
try {
|
|
609
|
+
const candidates = readdirSync("/tmp")
|
|
610
|
+
.filter((entry) => entry.startsWith("agent-relay-instance-") && entry.endsWith(".state"))
|
|
611
|
+
.map((entry) => join("/tmp", entry))
|
|
612
|
+
.map((path) => ({ path, mtimeMs: statSync(path).mtimeMs }))
|
|
613
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
614
|
+
for (const candidate of candidates) {
|
|
615
|
+
const id = readFileSync(candidate.path, "utf8").split(/\r?\n/)[0]?.trim();
|
|
616
|
+
if (id) return id;
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function formatPairs(pairs: any[]): string {
|
|
625
|
+
if (!pairs.length) return "No pair sessions.";
|
|
626
|
+
return pairs
|
|
627
|
+
.map((pair) => `${pair.id} ${pair.status} ${pair.requesterId} <-> ${pair.targetId}${pair.objective ? ` ${pair.objective}` : ""}`)
|
|
628
|
+
.join("\n");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function formatStatus(payload: any): string {
|
|
632
|
+
const agent = payload.agent;
|
|
633
|
+
const stats = payload.stats ?? {};
|
|
634
|
+
const health = payload.health ?? {};
|
|
635
|
+
const pairs = Array.isArray(payload.pairs) ? payload.pairs : [];
|
|
636
|
+
const activePair = pairs.find((pair: any) => pair.status === "active") ?? pairs.find((pair: any) => pair.status === "pending");
|
|
637
|
+
return [
|
|
638
|
+
`Relay: ${health.status ?? "unknown"} version=${stats.version ?? "unknown"}`,
|
|
639
|
+
`Agents: ${stats.online ?? "?"}/${stats.agents ?? "?"} online Messages: ${stats.messages ?? "?"} Tasks: ${stats.openTasks ?? "?"}/${stats.tasks ?? "?"} open`,
|
|
640
|
+
agent
|
|
641
|
+
? `Current: ${agent.id} status=${agent.status} ready=${agent.ready ? "yes" : "no"} label=${agent.label ?? "(none)"} tags=${(agent.tags ?? []).join(", ") || "(none)"}`
|
|
642
|
+
: "Current: unknown",
|
|
643
|
+
activePair
|
|
644
|
+
? `Pair: ${activePair.id} ${activePair.status} ${activePair.requesterId} <-> ${activePair.targetId}`
|
|
645
|
+
: "Pair: none active",
|
|
646
|
+
].join("\n");
|
|
647
|
+
}
|
|
648
|
+
|
|
208
649
|
async function confirm(message: string): Promise<boolean> {
|
|
209
650
|
if (!input.isTTY) return false;
|
|
210
651
|
const rl = createInterface({ input, output });
|
package/src/config.ts
CHANGED
|
@@ -20,6 +20,7 @@ function envPositiveInt(name: string, fallback: number): number {
|
|
|
20
20
|
export const STALE_TTL_MS = envPositiveInt("STALE_TTL_MS", 120_000); // 2min without heartbeat → offline
|
|
21
21
|
export const OFFLINE_PRUNE_MS = envPositiveInt("OFFLINE_PRUNE_MS", DAY_MS); // 24h offline → delete
|
|
22
22
|
export const REAP_INTERVAL_MS = envPositiveInt("REAP_INTERVAL_MS", 60_000); // reaper cadence
|
|
23
|
+
export const CLAIM_LEASE_MS = envPositiveInt("AGENT_RELAY_CLAIM_LEASE_MS", 1_800_000); // 30min claim lease
|
|
23
24
|
|
|
24
25
|
// Max body size for any POST/PATCH request (64 KiB).
|
|
25
26
|
export const MAX_BODY_BYTES = 64 * 1024;
|