agent-relay-server 0.4.22 → 0.4.24
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/package.json +1 -1
- package/public/dashboard.js +1661 -17
- package/public/index.html +1026 -32
- package/src/db.ts +252 -1
- package/src/routes.ts +336 -3
- package/src/security.ts +3 -1
- package/src/types.ts +61 -0
package/src/db.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { VERSION } from "./config.ts";
|
|
4
4
|
import type {
|
|
5
5
|
AgentCard,
|
|
6
|
+
ActivityEvent,
|
|
7
|
+
ActivityEventInput,
|
|
6
8
|
AgentSessionGuard,
|
|
7
9
|
CreatePairInput,
|
|
8
10
|
HealthCheck,
|
|
@@ -21,6 +23,9 @@ import type {
|
|
|
21
23
|
TaskSeverity,
|
|
22
24
|
TaskStatus,
|
|
23
25
|
IntegrationEventInput,
|
|
26
|
+
InboxDraft,
|
|
27
|
+
InboxState,
|
|
28
|
+
InboxThreadState,
|
|
24
29
|
TaskStatusInput,
|
|
25
30
|
} from "./types";
|
|
26
31
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS } from "./config";
|
|
@@ -144,6 +149,48 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
144
149
|
CREATE INDEX IF NOT EXISTS idx_pairs_requester ON pairs(requester_id);
|
|
145
150
|
CREATE INDEX IF NOT EXISTS idx_pairs_target ON pairs(target_id);
|
|
146
151
|
CREATE INDEX IF NOT EXISTS idx_pairs_status ON pairs(status);
|
|
152
|
+
|
|
153
|
+
CREATE TABLE IF NOT EXISTS inbox_thread_state (
|
|
154
|
+
operator_id TEXT NOT NULL,
|
|
155
|
+
peer_id TEXT NOT NULL,
|
|
156
|
+
read_cursor_message_id INTEGER,
|
|
157
|
+
archived_at_message_id INTEGER,
|
|
158
|
+
updated_at INTEGER NOT NULL,
|
|
159
|
+
PRIMARY KEY (operator_id, peer_id)
|
|
160
|
+
);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_thread_operator ON inbox_thread_state(operator_id);
|
|
162
|
+
|
|
163
|
+
CREATE TABLE IF NOT EXISTS inbox_drafts (
|
|
164
|
+
operator_id TEXT NOT NULL,
|
|
165
|
+
peer_id TEXT NOT NULL,
|
|
166
|
+
body TEXT NOT NULL,
|
|
167
|
+
subject TEXT,
|
|
168
|
+
channel TEXT,
|
|
169
|
+
updated_at INTEGER NOT NULL,
|
|
170
|
+
PRIMARY KEY (operator_id, peer_id)
|
|
171
|
+
);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_drafts_operator ON inbox_drafts(operator_id);
|
|
173
|
+
|
|
174
|
+
CREATE TABLE IF NOT EXISTS activity_events (
|
|
175
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
176
|
+
operator_id TEXT,
|
|
177
|
+
client_id TEXT UNIQUE,
|
|
178
|
+
kind TEXT NOT NULL,
|
|
179
|
+
title TEXT NOT NULL,
|
|
180
|
+
body TEXT,
|
|
181
|
+
meta_text TEXT,
|
|
182
|
+
icon TEXT,
|
|
183
|
+
view TEXT,
|
|
184
|
+
peer_id TEXT,
|
|
185
|
+
message_id INTEGER,
|
|
186
|
+
pair_id TEXT,
|
|
187
|
+
task_id INTEGER,
|
|
188
|
+
agent_id TEXT,
|
|
189
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
190
|
+
created_at INTEGER NOT NULL
|
|
191
|
+
);
|
|
192
|
+
CREATE INDEX IF NOT EXISTS idx_activity_operator ON activity_events(operator_id, created_at);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_events(created_at);
|
|
147
194
|
`);
|
|
148
195
|
|
|
149
196
|
// Migrations
|
|
@@ -257,6 +304,44 @@ function parseStringArray(raw: string): string[] {
|
|
|
257
304
|
return parsed.filter((value): value is string => typeof value === "string");
|
|
258
305
|
}
|
|
259
306
|
|
|
307
|
+
function normalizeTags(tags: string[] | undefined): string[] {
|
|
308
|
+
return [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function stringValue(value: unknown): string | undefined {
|
|
312
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function inferProviderTag(input: RegisterAgentInput): "claude" | "codex" | undefined {
|
|
316
|
+
const meta = input.meta ?? {};
|
|
317
|
+
const values = [
|
|
318
|
+
input.id,
|
|
319
|
+
input.name,
|
|
320
|
+
input.rig,
|
|
321
|
+
...(input.tags ?? []),
|
|
322
|
+
stringValue(meta.provider),
|
|
323
|
+
stringValue(meta.client),
|
|
324
|
+
stringValue(meta.runtime),
|
|
325
|
+
stringValue(meta.agentType),
|
|
326
|
+
].filter((value): value is string => typeof value === "string");
|
|
327
|
+
|
|
328
|
+
if (values.some((value) => value.toLowerCase().includes("codex"))) return "codex";
|
|
329
|
+
if (values.some((value) => value.toLowerCase().includes("claude"))) return "claude";
|
|
330
|
+
|
|
331
|
+
// Older Claude hooks did not always send an explicit provider tag, but did
|
|
332
|
+
// send Claude-style approval metadata. Codex also sends approvalMode, so this
|
|
333
|
+
// fallback only runs after Codex signals have been ruled out.
|
|
334
|
+
if (Object.prototype.hasOwnProperty.call(meta, "approvalMode")) return "claude";
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function tagsWithProvider(input: RegisterAgentInput): string[] {
|
|
339
|
+
const tags = normalizeTags(input.tags);
|
|
340
|
+
const provider = inferProviderTag(input);
|
|
341
|
+
if (!provider || tags.includes(provider)) return tags;
|
|
342
|
+
return [provider, ...tags];
|
|
343
|
+
}
|
|
344
|
+
|
|
260
345
|
function rowToAgent(row: any): AgentCard {
|
|
261
346
|
return {
|
|
262
347
|
id: row.id,
|
|
@@ -355,6 +440,48 @@ function rowToPair(row: any): PairSession {
|
|
|
355
440
|
};
|
|
356
441
|
}
|
|
357
442
|
|
|
443
|
+
function rowToInboxThreadState(row: any): InboxThreadState {
|
|
444
|
+
return {
|
|
445
|
+
operatorId: row.operator_id,
|
|
446
|
+
peerId: row.peer_id,
|
|
447
|
+
readCursorMessageId: row.read_cursor_message_id ?? undefined,
|
|
448
|
+
archivedAtMessageId: row.archived_at_message_id ?? undefined,
|
|
449
|
+
updatedAt: row.updated_at,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function rowToInboxDraft(row: any): InboxDraft {
|
|
454
|
+
return {
|
|
455
|
+
operatorId: row.operator_id,
|
|
456
|
+
peerId: row.peer_id,
|
|
457
|
+
body: row.body,
|
|
458
|
+
subject: row.subject ?? undefined,
|
|
459
|
+
channel: row.channel ?? undefined,
|
|
460
|
+
updatedAt: row.updated_at,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function rowToActivityEvent(row: any): ActivityEvent {
|
|
465
|
+
return {
|
|
466
|
+
id: row.id,
|
|
467
|
+
operatorId: row.operator_id ?? undefined,
|
|
468
|
+
clientId: row.client_id ?? undefined,
|
|
469
|
+
kind: row.kind,
|
|
470
|
+
title: row.title,
|
|
471
|
+
body: row.body ?? undefined,
|
|
472
|
+
meta: row.meta_text ?? undefined,
|
|
473
|
+
icon: row.icon ?? undefined,
|
|
474
|
+
view: row.view ?? undefined,
|
|
475
|
+
peer: row.peer_id ?? undefined,
|
|
476
|
+
messageId: row.message_id ?? undefined,
|
|
477
|
+
pairId: row.pair_id ?? undefined,
|
|
478
|
+
taskId: row.task_id ?? undefined,
|
|
479
|
+
agentId: row.agent_id ?? undefined,
|
|
480
|
+
metadata: parseJson(row.metadata, {}),
|
|
481
|
+
createdAt: row.created_at,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
358
485
|
const MSG_SELECT = `SELECT m.*, (
|
|
359
486
|
SELECT json_group_array(agent_id) FROM message_reads WHERE message_id = m.id
|
|
360
487
|
) AS read_by_agents FROM messages m`;
|
|
@@ -363,6 +490,7 @@ const MSG_SELECT = `SELECT m.*, (
|
|
|
363
490
|
|
|
364
491
|
export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
365
492
|
const now = Date.now();
|
|
493
|
+
const tags = tagsWithProvider(input);
|
|
366
494
|
// Preserve the existing label across re-registrations unless the caller
|
|
367
495
|
// explicitly sends one (including null to clear).
|
|
368
496
|
const labelProvided = Object.prototype.hasOwnProperty.call(input, "label");
|
|
@@ -394,7 +522,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
|
394
522
|
$name: input.name,
|
|
395
523
|
$label: input.label ?? null,
|
|
396
524
|
$labelProvided: labelProvided ? 1 : 0,
|
|
397
|
-
$tags: JSON.stringify(
|
|
525
|
+
$tags: JSON.stringify(tags),
|
|
398
526
|
$machine: input.machine ?? null,
|
|
399
527
|
$rig: input.rig ?? null,
|
|
400
528
|
$capabilities: JSON.stringify(input.capabilities ?? []),
|
|
@@ -1423,6 +1551,129 @@ export function markRead(messageId: number, agentId: string): boolean {
|
|
|
1423
1551
|
return true;
|
|
1424
1552
|
}
|
|
1425
1553
|
|
|
1554
|
+
export function getInboxState(operatorId: string): InboxState {
|
|
1555
|
+
const threads = (db.prepare(
|
|
1556
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? ORDER BY updated_at DESC",
|
|
1557
|
+
).all(operatorId) as any[]).map(rowToInboxThreadState);
|
|
1558
|
+
const drafts = (db.prepare(
|
|
1559
|
+
"SELECT * FROM inbox_drafts WHERE operator_id = ? ORDER BY updated_at DESC",
|
|
1560
|
+
).all(operatorId) as any[]).map(rowToInboxDraft);
|
|
1561
|
+
return { operatorId, threads, drafts };
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
export function setInboxThreadState(input: {
|
|
1565
|
+
operatorId: string;
|
|
1566
|
+
peerId: string;
|
|
1567
|
+
readCursorMessageId?: number | null;
|
|
1568
|
+
archivedAtMessageId?: number | null;
|
|
1569
|
+
}): InboxThreadState {
|
|
1570
|
+
const now = Date.now();
|
|
1571
|
+
const current = db.prepare(
|
|
1572
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
|
|
1573
|
+
).get(input.operatorId, input.peerId) as any | undefined;
|
|
1574
|
+
|
|
1575
|
+
const readCursorMessageId = Object.prototype.hasOwnProperty.call(input, "readCursorMessageId")
|
|
1576
|
+
? input.readCursorMessageId ?? null
|
|
1577
|
+
: current?.read_cursor_message_id ?? null;
|
|
1578
|
+
const archivedAtMessageId = Object.prototype.hasOwnProperty.call(input, "archivedAtMessageId")
|
|
1579
|
+
? input.archivedAtMessageId ?? null
|
|
1580
|
+
: current?.archived_at_message_id ?? null;
|
|
1581
|
+
|
|
1582
|
+
db.prepare(`
|
|
1583
|
+
INSERT INTO inbox_thread_state (operator_id, peer_id, read_cursor_message_id, archived_at_message_id, updated_at)
|
|
1584
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1585
|
+
ON CONFLICT(operator_id, peer_id) DO UPDATE SET
|
|
1586
|
+
read_cursor_message_id = excluded.read_cursor_message_id,
|
|
1587
|
+
archived_at_message_id = excluded.archived_at_message_id,
|
|
1588
|
+
updated_at = excluded.updated_at
|
|
1589
|
+
`).run(input.operatorId, input.peerId, readCursorMessageId, archivedAtMessageId, now);
|
|
1590
|
+
|
|
1591
|
+
return rowToInboxThreadState(db.prepare(
|
|
1592
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
|
|
1593
|
+
).get(input.operatorId, input.peerId));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
export function setInboxDraft(input: {
|
|
1597
|
+
operatorId: string;
|
|
1598
|
+
peerId: string;
|
|
1599
|
+
body: string;
|
|
1600
|
+
subject?: string | null;
|
|
1601
|
+
channel?: string | null;
|
|
1602
|
+
}): InboxDraft {
|
|
1603
|
+
const now = Date.now();
|
|
1604
|
+
db.prepare(`
|
|
1605
|
+
INSERT INTO inbox_drafts (operator_id, peer_id, body, subject, channel, updated_at)
|
|
1606
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1607
|
+
ON CONFLICT(operator_id, peer_id) DO UPDATE SET
|
|
1608
|
+
body = excluded.body,
|
|
1609
|
+
subject = excluded.subject,
|
|
1610
|
+
channel = excluded.channel,
|
|
1611
|
+
updated_at = excluded.updated_at
|
|
1612
|
+
`).run(input.operatorId, input.peerId, input.body, input.subject ?? null, input.channel ?? null, now);
|
|
1613
|
+
|
|
1614
|
+
return rowToInboxDraft(db.prepare(
|
|
1615
|
+
"SELECT * FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?",
|
|
1616
|
+
).get(input.operatorId, input.peerId));
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
export function deleteInboxDraft(operatorId: string, peerId: string): boolean {
|
|
1620
|
+
return db.prepare("DELETE FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?").run(operatorId, peerId).changes > 0;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
export function listActivityEvents(input: {
|
|
1624
|
+
operatorId?: string;
|
|
1625
|
+
limit?: number;
|
|
1626
|
+
since?: number;
|
|
1627
|
+
} = {}): ActivityEvent[] {
|
|
1628
|
+
const conditions: string[] = [];
|
|
1629
|
+
const params: any[] = [];
|
|
1630
|
+
if (input.operatorId) {
|
|
1631
|
+
conditions.push("operator_id = ?");
|
|
1632
|
+
params.push(input.operatorId);
|
|
1633
|
+
}
|
|
1634
|
+
if (input.since !== undefined) {
|
|
1635
|
+
conditions.push("created_at >= ?");
|
|
1636
|
+
params.push(input.since);
|
|
1637
|
+
}
|
|
1638
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1639
|
+
params.push(input.limit ?? 200);
|
|
1640
|
+
return (db.prepare(
|
|
1641
|
+
`SELECT * FROM activity_events ${where} ORDER BY created_at DESC, id DESC LIMIT ?`,
|
|
1642
|
+
).all(...params) as any[]).map(rowToActivityEvent);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
export function createActivityEvent(input: ActivityEventInput): ActivityEvent {
|
|
1646
|
+
if (input.clientId) {
|
|
1647
|
+
const existing = db.prepare("SELECT * FROM activity_events WHERE client_id = ?").get(input.clientId) as any | undefined;
|
|
1648
|
+
if (existing) return rowToActivityEvent(existing);
|
|
1649
|
+
}
|
|
1650
|
+
const now = Date.now();
|
|
1651
|
+
const result = db.prepare(`
|
|
1652
|
+
INSERT INTO activity_events (
|
|
1653
|
+
operator_id, client_id, kind, title, body, meta_text, icon, view, peer_id,
|
|
1654
|
+
message_id, pair_id, task_id, agent_id, metadata, created_at
|
|
1655
|
+
)
|
|
1656
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1657
|
+
`).run(
|
|
1658
|
+
input.operatorId ?? null,
|
|
1659
|
+
input.clientId ?? null,
|
|
1660
|
+
input.kind,
|
|
1661
|
+
input.title,
|
|
1662
|
+
input.body ?? null,
|
|
1663
|
+
input.meta ?? null,
|
|
1664
|
+
input.icon ?? null,
|
|
1665
|
+
input.view ?? null,
|
|
1666
|
+
input.peer ?? null,
|
|
1667
|
+
input.messageId ?? null,
|
|
1668
|
+
input.pairId ?? null,
|
|
1669
|
+
input.taskId ?? null,
|
|
1670
|
+
input.agentId ?? null,
|
|
1671
|
+
JSON.stringify(input.metadata ?? {}),
|
|
1672
|
+
now,
|
|
1673
|
+
);
|
|
1674
|
+
return rowToActivityEvent(db.prepare("SELECT * FROM activity_events WHERE id = ?").get(Number(result.lastInsertRowid)));
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1426
1677
|
export function deleteMessage(id: number): boolean {
|
|
1427
1678
|
return db.transaction(() => {
|
|
1428
1679
|
// Break reply_to references from children so the FK doesn't block delete.
|