agent-relay-server 0.4.37 → 0.4.39
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 +210 -13
- package/public/index.html +261 -23
- package/src/agent-spawn.ts +137 -0
- package/src/db.ts +40 -1
- package/src/routes.ts +120 -0
package/package.json
CHANGED
package/public/dashboard.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const DEFAULT_INBOX_COMPOSE = { toMode: "agent", to: "", body: "", channel: "", subject: "", claimable: false };
|
|
7
7
|
const DEFAULT_PAIR_MESSAGE = { pairId: "", from: "", body: "", subject: "" };
|
|
8
8
|
const DEFAULT_PAIR_INVITE = { requesterId: "", targetId: "", objective: "" };
|
|
9
|
+
const DEFAULT_AGENT_SPAWN = { provider: "codex", approvalMode: "guarded", cwd: "", label: "" };
|
|
9
10
|
const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
|
|
10
11
|
const WAITING_TASK_STATUSES = new Set(["open", "blocked"]);
|
|
11
12
|
const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
claude: "claude-sol",
|
|
16
17
|
user: "ti-user",
|
|
17
18
|
system: "ti-server",
|
|
19
|
+
channel: "ti-messages",
|
|
18
20
|
agent: "ti-robot",
|
|
19
21
|
};
|
|
20
22
|
const AGENT_TYPE_TITLES = {
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
claude: "Claude agent",
|
|
23
25
|
user: "Human operator",
|
|
24
26
|
system: "System",
|
|
27
|
+
channel: "Channel",
|
|
25
28
|
agent: "Agent",
|
|
26
29
|
};
|
|
27
30
|
|
|
@@ -76,9 +79,13 @@
|
|
|
76
79
|
selectedAgent: "",
|
|
77
80
|
agentDetailOpen: false,
|
|
78
81
|
agentDetailId: "",
|
|
82
|
+
channelDetailOpen: false,
|
|
83
|
+
channelDetailId: "",
|
|
79
84
|
selectedInboxThread: "",
|
|
80
85
|
replyTo: null,
|
|
81
86
|
composeOpen: false,
|
|
87
|
+
agentSpawnOpen: false,
|
|
88
|
+
agentDirectoryBrowser: { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" },
|
|
82
89
|
pairInviteOpen: false,
|
|
83
90
|
pairMessageOpen: false,
|
|
84
91
|
threadOpen: false,
|
|
@@ -90,6 +97,7 @@
|
|
|
90
97
|
authNeeded: false,
|
|
91
98
|
|
|
92
99
|
compose: { ...DEFAULT_COMPOSE },
|
|
100
|
+
agentSpawn: { ...DEFAULT_AGENT_SPAWN },
|
|
93
101
|
pairInvite: { ...DEFAULT_PAIR_INVITE },
|
|
94
102
|
pairMessage: { ...DEFAULT_PAIR_MESSAGE },
|
|
95
103
|
inboxCompose: { ...DEFAULT_INBOX_COMPOSE },
|
|
@@ -496,11 +504,14 @@
|
|
|
496
504
|
function createComputedDescriptors() {
|
|
497
505
|
return {
|
|
498
506
|
onlineCount: { get: getOnlineCount },
|
|
507
|
+
busyAgentCount: { get: getBusyAgentCount },
|
|
499
508
|
hiddenBuiltInAgentCount: { get: getHiddenBuiltInAgentCount },
|
|
500
509
|
sortedAgents: { get: getSortedAgents },
|
|
501
510
|
pairsByAgentId: { get: getPairsByAgentId },
|
|
502
511
|
selectedAgentDetail: { get: getSelectedAgentDetail },
|
|
503
512
|
agentDetailMessages: { get: getAgentDetailMessages },
|
|
513
|
+
selectedChannelDetail: { get: getSelectedChannelDetail },
|
|
514
|
+
channelDetailMessages: { get: getChannelDetailMessages },
|
|
504
515
|
pairMessagePair: { get: getPairMessagePair },
|
|
505
516
|
allInboxThreads: { get: getAllInboxThreads },
|
|
506
517
|
inboxThreads: { get: getInboxThreads },
|
|
@@ -513,6 +524,7 @@
|
|
|
513
524
|
workQueueItems: { get: getWorkQueueItems },
|
|
514
525
|
channelCards: { get: getChannelCards },
|
|
515
526
|
integrationCards: { get: getIntegrationCards },
|
|
527
|
+
openPairCount: { get: getOpenPairCount },
|
|
516
528
|
filteredMessages: { get: getFilteredMessages },
|
|
517
529
|
groupedMessages: { get: getGroupedMessages },
|
|
518
530
|
filteredTasks: { get: getFilteredTasks },
|
|
@@ -530,6 +542,10 @@
|
|
|
530
542
|
return visibleAgents(this).filter((agent) => agent.status !== "offline").length;
|
|
531
543
|
}
|
|
532
544
|
|
|
545
|
+
function getBusyAgentCount() {
|
|
546
|
+
return this.agents.filter((agent) => agent.status === "busy").length;
|
|
547
|
+
}
|
|
548
|
+
|
|
533
549
|
function getHiddenBuiltInAgentCount() {
|
|
534
550
|
return this.showBuiltIns ? 0 : this.agents.filter(isBuiltInAgent).length;
|
|
535
551
|
}
|
|
@@ -578,6 +594,29 @@
|
|
|
578
594
|
.slice(0, 8);
|
|
579
595
|
}
|
|
580
596
|
|
|
597
|
+
function getSelectedChannelDetail() {
|
|
598
|
+
if (!this.channelDetailId) return null;
|
|
599
|
+
return (this.channels || []).find((channel) => channel.id === this.channelDetailId) || null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function getChannelDetailMessages() {
|
|
603
|
+
const channel = this.selectedChannelDetail;
|
|
604
|
+
if (!channel) return [];
|
|
605
|
+
return this.messages
|
|
606
|
+
.filter((msg) => messageMatchesChannel(msg, channel))
|
|
607
|
+
.slice()
|
|
608
|
+
.sort((a, b) => b.id - a.id)
|
|
609
|
+
.slice(0, 8);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function messageMatchesChannel(message, channel) {
|
|
613
|
+
if (!message || !channel) return false;
|
|
614
|
+
const channelKeys = [channel.id, channel.type, ...(channel.topicChannels || [])].filter(Boolean);
|
|
615
|
+
if (message.channel && channelKeys.includes(message.channel)) return true;
|
|
616
|
+
if (channel.agentId && (message.from === channel.agentId || message.to === channel.agentId)) return true;
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
|
|
581
620
|
function getPairMessagePair() {
|
|
582
621
|
return this.pairs.find((pair) => pair.id === this.pairMessage.pairId) || null;
|
|
583
622
|
}
|
|
@@ -1071,11 +1110,47 @@
|
|
|
1071
1110
|
});
|
|
1072
1111
|
}
|
|
1073
1112
|
|
|
1113
|
+
function getOpenPairCount() {
|
|
1114
|
+
return (this.pairs || []).filter(isOpenPair).length;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function isOpenPair(pair) {
|
|
1118
|
+
return pair?.status === "active" || pair?.status === "pending";
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1074
1121
|
function getComposeAgents() {
|
|
1075
1122
|
const list = visibleAgents(this);
|
|
1076
1123
|
return this.showOffline ? list : list.filter((agent) => agent.status !== "offline");
|
|
1077
1124
|
}
|
|
1078
1125
|
|
|
1126
|
+
function composeTargetAllowsClaimable() {
|
|
1127
|
+
return matchingComposeRecipientCount(this, this.compose.to) > 1;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function inboxComposeTargetAllowsClaimable() {
|
|
1131
|
+
return matchingComposeRecipientCount(this, inboxComposeTarget(this)) > 1;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function matchingComposeRecipientCount(vm, target) {
|
|
1135
|
+
if (!target) return 0;
|
|
1136
|
+
const candidates = vm.composeAgents.filter((agent) => !isBuiltInAgent(agent) && !isChannelAgent(agent));
|
|
1137
|
+
if (target === "broadcast") return candidates.length;
|
|
1138
|
+
if (vm.agentsById[target]) return 1;
|
|
1139
|
+
if (target.startsWith("tag:")) {
|
|
1140
|
+
const tag = target.slice(4);
|
|
1141
|
+
return candidates.filter((agent) => (agent.tags || []).includes(tag)).length;
|
|
1142
|
+
}
|
|
1143
|
+
if (target.startsWith("cap:")) {
|
|
1144
|
+
const cap = target.slice(4);
|
|
1145
|
+
return candidates.filter((agent) => (agent.capabilities || []).includes(cap)).length;
|
|
1146
|
+
}
|
|
1147
|
+
if (target.startsWith("label:")) {
|
|
1148
|
+
const label = target.slice(6);
|
|
1149
|
+
return candidates.filter((agent) => agent.label === label).length;
|
|
1150
|
+
}
|
|
1151
|
+
return 1;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1079
1154
|
function getUniqueLabels() {
|
|
1080
1155
|
return [...new Set(visibleAgents(this).filter((agent) => agent.label).map((agent) => agent.label))];
|
|
1081
1156
|
}
|
|
@@ -1089,13 +1164,22 @@
|
|
|
1089
1164
|
}
|
|
1090
1165
|
|
|
1091
1166
|
function visibleAgents(vm) {
|
|
1092
|
-
|
|
1167
|
+
const nonChannelAgents = vm.agents.filter((agent) => !isChannelAgent(agent));
|
|
1168
|
+
return vm.showBuiltIns ? nonChannelAgents : nonChannelAgents.filter((agent) => !isBuiltInAgent(agent));
|
|
1093
1169
|
}
|
|
1094
1170
|
|
|
1095
1171
|
function isBuiltInAgent(agent) {
|
|
1096
1172
|
return agent?.meta?.builtin === true || agent?.id === HUMAN_AGENT_ID || agent?.id === "system";
|
|
1097
1173
|
}
|
|
1098
1174
|
|
|
1175
|
+
function isChannelAgent(agent) {
|
|
1176
|
+
return agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function agentSupportsControlActions(agent) {
|
|
1180
|
+
return Boolean(agent && !isBuiltInAgent(agent) && !isChannelAgent(agent));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1099
1183
|
function applyAgentPreset(vm, list) {
|
|
1100
1184
|
switch (vm.agentPresetFilter) {
|
|
1101
1185
|
case "active":
|
|
@@ -1249,6 +1333,9 @@
|
|
|
1249
1333
|
agentChannels,
|
|
1250
1334
|
channelPresence,
|
|
1251
1335
|
integrationPresence,
|
|
1336
|
+
composeTargetAllowsClaimable,
|
|
1337
|
+
inboxComposeTargetAllowsClaimable,
|
|
1338
|
+
agentSupportsControlActions,
|
|
1252
1339
|
timeAgo,
|
|
1253
1340
|
fmtTime,
|
|
1254
1341
|
healthAlertClass,
|
|
@@ -1368,6 +1455,7 @@
|
|
|
1368
1455
|
function agentType(agent) {
|
|
1369
1456
|
if (agent?.id === HUMAN_AGENT_ID) return "user";
|
|
1370
1457
|
if (agent?.id === "system") return "system";
|
|
1458
|
+
if (isChannelAgent(agent)) return "channel";
|
|
1371
1459
|
|
|
1372
1460
|
const values = [
|
|
1373
1461
|
...(agent?.tags || []),
|
|
@@ -1573,6 +1661,12 @@
|
|
|
1573
1661
|
openPairInvite,
|
|
1574
1662
|
closePairInvite,
|
|
1575
1663
|
doCreatePair,
|
|
1664
|
+
openAgentSpawn,
|
|
1665
|
+
closeAgentSpawn,
|
|
1666
|
+
doSpawnAgent,
|
|
1667
|
+
openAgentDirectoryBrowser,
|
|
1668
|
+
browseAgentDirectory,
|
|
1669
|
+
selectAgentDirectory,
|
|
1576
1670
|
doSendPairMessage,
|
|
1577
1671
|
doAcceptPair,
|
|
1578
1672
|
doRejectPair,
|
|
@@ -1783,7 +1877,7 @@
|
|
|
1783
1877
|
}
|
|
1784
1878
|
|
|
1785
1879
|
function resetInboxComposeTarget() {
|
|
1786
|
-
this.inboxCompose = { ...this.inboxCompose, to: "" };
|
|
1880
|
+
this.inboxCompose = { ...this.inboxCompose, to: "", claimable: false };
|
|
1787
1881
|
}
|
|
1788
1882
|
|
|
1789
1883
|
function inboxComposeTarget(vm) {
|
|
@@ -1809,14 +1903,14 @@
|
|
|
1809
1903
|
};
|
|
1810
1904
|
if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
|
|
1811
1905
|
if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
|
|
1812
|
-
if (this.inboxCompose.claimable) payload.claimable = true;
|
|
1906
|
+
if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this)) payload.claimable = true;
|
|
1813
1907
|
await this.api("POST", "/messages", payload);
|
|
1814
1908
|
this.recordOperatorActivity({
|
|
1815
|
-
title:
|
|
1909
|
+
title: payload.claimable ? "Claimable task sent" : "Message sent",
|
|
1816
1910
|
body: this.inboxCompose.subject || this.inboxCompose.body,
|
|
1817
1911
|
meta: "to " + this.displayTarget(target),
|
|
1818
|
-
icon:
|
|
1819
|
-
kind:
|
|
1912
|
+
icon: payload.claimable ? "ti-hand-grab" : "ti-send",
|
|
1913
|
+
kind: payload.claimable ? "task" : "operator",
|
|
1820
1914
|
view: inboxPeer(payload) ? "inbox" : "messages",
|
|
1821
1915
|
peer: inboxPeer(payload),
|
|
1822
1916
|
});
|
|
@@ -1832,7 +1926,7 @@
|
|
|
1832
1926
|
this.replyTo = { id: msg.id, from: msg.from };
|
|
1833
1927
|
this.compose = {
|
|
1834
1928
|
...DEFAULT_COMPOSE,
|
|
1835
|
-
from:
|
|
1929
|
+
from: HUMAN_AGENT_ID,
|
|
1836
1930
|
to: replyTarget,
|
|
1837
1931
|
channel: msg.channel || "",
|
|
1838
1932
|
};
|
|
@@ -1852,7 +1946,7 @@
|
|
|
1852
1946
|
if (vm.compose.channel) payload.channel = vm.compose.channel;
|
|
1853
1947
|
if (vm.compose.subject) payload.subject = vm.compose.subject;
|
|
1854
1948
|
if (vm.replyTo) payload.replyTo = vm.replyTo.id;
|
|
1855
|
-
if (vm.compose.claimable) payload.claimable = true;
|
|
1949
|
+
if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm)) payload.claimable = true;
|
|
1856
1950
|
return payload;
|
|
1857
1951
|
}
|
|
1858
1952
|
|
|
@@ -1876,7 +1970,7 @@
|
|
|
1876
1970
|
});
|
|
1877
1971
|
this.composeOpen = false;
|
|
1878
1972
|
this.replyTo = null;
|
|
1879
|
-
this.compose = { ...DEFAULT_COMPOSE };
|
|
1973
|
+
this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID };
|
|
1880
1974
|
await this.fetchMessages();
|
|
1881
1975
|
} catch (e) {
|
|
1882
1976
|
alert("Send failed: " + e.message);
|
|
@@ -2313,6 +2407,77 @@
|
|
|
2313
2407
|
}
|
|
2314
2408
|
}
|
|
2315
2409
|
|
|
2410
|
+
function openAgentSpawn() {
|
|
2411
|
+
this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
|
|
2412
|
+
this.agentDirectoryBrowser = { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" };
|
|
2413
|
+
this.agentSpawnOpen = true;
|
|
2414
|
+
this.$nextTick(() => this.$refs?.agentSpawnCwd?.focus());
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function closeAgentSpawn() {
|
|
2418
|
+
this.agentSpawnOpen = false;
|
|
2419
|
+
this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
async function openAgentDirectoryBrowser() {
|
|
2423
|
+
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: true };
|
|
2424
|
+
await this.browseAgentDirectory(this.agentSpawn.cwd || "");
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
async function browseAgentDirectory(path) {
|
|
2428
|
+
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: true, error: "" };
|
|
2429
|
+
try {
|
|
2430
|
+
const query = path ? "?path=" + encodeURIComponent(path) : "";
|
|
2431
|
+
const listing = await this.api("GET", "/agents/spawn/directories" + query);
|
|
2432
|
+
this.agentDirectoryBrowser = {
|
|
2433
|
+
open: true,
|
|
2434
|
+
loading: false,
|
|
2435
|
+
path: listing.path || "",
|
|
2436
|
+
parent: listing.parent || "",
|
|
2437
|
+
home: listing.home || "",
|
|
2438
|
+
cwd: listing.cwd || "",
|
|
2439
|
+
entries: listing.entries || [],
|
|
2440
|
+
error: "",
|
|
2441
|
+
};
|
|
2442
|
+
this.agentSpawn = { ...this.agentSpawn, cwd: listing.path || this.agentSpawn.cwd };
|
|
2443
|
+
} catch (e) {
|
|
2444
|
+
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: false, error: e.message };
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function selectAgentDirectory(path) {
|
|
2449
|
+
this.agentSpawn = { ...this.agentSpawn, cwd: path || this.agentDirectoryBrowser.path };
|
|
2450
|
+
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: false };
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
async function doSpawnAgent() {
|
|
2454
|
+
if (this.agentSpawn.provider !== "codex") {
|
|
2455
|
+
alert("Only Codex live sessions can be spawned from the dashboard right now.");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
try {
|
|
2459
|
+
const payload = {
|
|
2460
|
+
provider: "codex",
|
|
2461
|
+
approvalMode: this.agentSpawn.approvalMode || "guarded",
|
|
2462
|
+
};
|
|
2463
|
+
if (this.agentSpawn.cwd) payload.cwd = this.agentSpawn.cwd;
|
|
2464
|
+
if (this.agentSpawn.label) payload.label = this.agentSpawn.label;
|
|
2465
|
+
const result = await this.api("POST", "/agents/spawn", payload);
|
|
2466
|
+
this.recordOperatorActivity({
|
|
2467
|
+
title: "Codex agent spawn requested",
|
|
2468
|
+
body: result?.cwd || payload.cwd || "",
|
|
2469
|
+
meta: result?.pid ? "pid " + result.pid : "starting",
|
|
2470
|
+
icon: "ti-plus",
|
|
2471
|
+
kind: "state",
|
|
2472
|
+
view: "agents",
|
|
2473
|
+
});
|
|
2474
|
+
this.closeAgentSpawn();
|
|
2475
|
+
await Promise.all([this.fetchAgents(), this.fetchActivityEvents()]);
|
|
2476
|
+
} catch (e) {
|
|
2477
|
+
alert("Spawn failed: " + e.message);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2316
2481
|
function closePairMessage() {
|
|
2317
2482
|
this.pairMessageOpen = false;
|
|
2318
2483
|
this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
|
|
@@ -2409,6 +2574,16 @@
|
|
|
2409
2574
|
this.agentDetailOpen = false;
|
|
2410
2575
|
},
|
|
2411
2576
|
|
|
2577
|
+
openChannelDetail(channel) {
|
|
2578
|
+
if (!channel) return;
|
|
2579
|
+
this.channelDetailId = channel.id;
|
|
2580
|
+
this.channelDetailOpen = true;
|
|
2581
|
+
},
|
|
2582
|
+
|
|
2583
|
+
closeChannelDetail() {
|
|
2584
|
+
this.channelDetailOpen = false;
|
|
2585
|
+
},
|
|
2586
|
+
|
|
2412
2587
|
openRename(agent) {
|
|
2413
2588
|
this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
|
|
2414
2589
|
this.$nextTick(() => this.$refs.renameInput?.focus());
|
|
@@ -2438,6 +2613,26 @@
|
|
|
2438
2613
|
alert("Delete failed: " + e.message);
|
|
2439
2614
|
}
|
|
2440
2615
|
},
|
|
2616
|
+
|
|
2617
|
+
async doAgentAction(agent, action) {
|
|
2618
|
+
if (!agent || !action) return;
|
|
2619
|
+
try {
|
|
2620
|
+
const result = await this.api("POST", "/agents/" + encodeURIComponent(agent.id) + "/actions", { action });
|
|
2621
|
+
this.recordOperatorActivity({
|
|
2622
|
+
title: action === "restart" ? "Agent restart requested" : "Agent shutdown requested",
|
|
2623
|
+
body: this.displayName(agent),
|
|
2624
|
+
meta: agent.id,
|
|
2625
|
+
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
2626
|
+
kind: "state",
|
|
2627
|
+
view: "agents",
|
|
2628
|
+
agentId: agent.id,
|
|
2629
|
+
messageId: result?.message?.id,
|
|
2630
|
+
});
|
|
2631
|
+
await Promise.all([this.fetchMessages(), this.fetchActivityEvents()]);
|
|
2632
|
+
} catch (e) {
|
|
2633
|
+
alert("Agent action failed: " + e.message);
|
|
2634
|
+
}
|
|
2635
|
+
},
|
|
2441
2636
|
};
|
|
2442
2637
|
}
|
|
2443
2638
|
|
|
@@ -2495,7 +2690,7 @@
|
|
|
2495
2690
|
|
|
2496
2691
|
function renderStatusChart() {
|
|
2497
2692
|
const { labels, series } = countAgentStatuses(this.agents);
|
|
2498
|
-
const colorMap = {
|
|
2693
|
+
const colorMap = { idle: "#4299e1", busy: "#ecc94b" };
|
|
2499
2694
|
|
|
2500
2695
|
if (this.chartInstances.status) {
|
|
2501
2696
|
this.chartInstances.status.updateOptions({
|
|
@@ -2522,10 +2717,12 @@
|
|
|
2522
2717
|
}
|
|
2523
2718
|
|
|
2524
2719
|
function countAgentStatuses(agents) {
|
|
2525
|
-
const counts = {
|
|
2526
|
-
for (const agent of agents)
|
|
2720
|
+
const counts = { idle: 0, busy: 0 };
|
|
2721
|
+
for (const agent of agents) {
|
|
2722
|
+
if (agent.status in counts) counts[agent.status] += 1;
|
|
2723
|
+
}
|
|
2527
2724
|
|
|
2528
|
-
const labels = Object.keys(counts)
|
|
2725
|
+
const labels = Object.keys(counts);
|
|
2529
2726
|
return { labels, series: labels.map((key) => counts[key]) };
|
|
2530
2727
|
}
|
|
2531
2728
|
|
package/public/index.html
CHANGED
|
@@ -205,18 +205,21 @@
|
|
|
205
205
|
</a>
|
|
206
206
|
<a href="#" class="nav-link" :class="{ active: view === 'agents' }" @click.prevent="switchView('agents')">
|
|
207
207
|
<i class="ti ti-robot"></i>Agents
|
|
208
|
-
<span class="
|
|
209
|
-
|
|
208
|
+
<span class="ms-auto d-inline-flex gap-1 align-items-center">
|
|
209
|
+
<span class="badge bg-warning text-white" x-show="attentionAgentCount > 0" x-text="attentionAgentCount"></span>
|
|
210
|
+
<span class="badge bg-success text-white" x-show="onlineCount > 0" x-text="onlineCount"></span>
|
|
211
|
+
</span>
|
|
210
212
|
</a>
|
|
211
213
|
<a href="#" class="nav-link" :class="{ active: view === 'channels' }" @click.prevent="switchView('channels')">
|
|
212
214
|
<i class="ti ti-messages"></i>Channels
|
|
213
215
|
<span class="badge bg-success text-white ms-auto" x-show="channelCards.filter((item) => item.ready).length > 0" x-text="channelCards.filter((item) => item.ready).length"></span>
|
|
214
|
-
<span class="badge bg-secondary text-white ms-1" x-text="channelCards.length"></span>
|
|
215
216
|
</a>
|
|
216
217
|
<a href="#" class="nav-link" :class="{ active: view === 'integrations' }" @click.prevent="switchView('integrations')">
|
|
217
218
|
<i class="ti ti-plug-connected"></i>Integrations
|
|
218
|
-
<span class="
|
|
219
|
-
|
|
219
|
+
<span class="ms-auto d-inline-flex gap-1 align-items-center">
|
|
220
|
+
<span class="badge bg-warning text-white" x-show="integrationCards.some((item) => (item.taskStats?.waitingTasks || 0) > 0)" x-text="integrationCards.reduce((total, item) => total + (item.taskStats?.waitingTasks || 0), 0)"></span>
|
|
221
|
+
<span class="badge bg-secondary text-white" x-show="integrationCards.length > 0" x-text="integrationCards.length"></span>
|
|
222
|
+
</span>
|
|
220
223
|
</a>
|
|
221
224
|
<a href="#" class="nav-link" :class="{ active: view === 'inbox' }" @click.prevent="switchView('inbox')">
|
|
222
225
|
<i class="ti ti-inbox"></i>Inbox
|
|
@@ -228,8 +231,10 @@
|
|
|
228
231
|
</a>
|
|
229
232
|
<a href="#" class="nav-link" :class="{ active: view === 'pairs' }" @click.prevent="switchView('pairs')">
|
|
230
233
|
<i class="ti ti-link"></i>Pairs
|
|
231
|
-
<span class="
|
|
232
|
-
|
|
234
|
+
<span class="ms-auto d-inline-flex gap-1 align-items-center">
|
|
235
|
+
<span class="badge bg-warning text-white" x-show="attentionSummary.pendingPairInvites > 0" x-text="attentionSummary.pendingPairInvites"></span>
|
|
236
|
+
<span class="badge bg-primary text-white" x-show="openPairCount > 0" x-text="openPairCount"></span>
|
|
237
|
+
</span>
|
|
233
238
|
</a>
|
|
234
239
|
<a href="#" class="nav-link" :class="{ active: view === 'messages' }" @click.prevent="switchView('messages')">
|
|
235
240
|
<i class="ti ti-messages"></i>Messages
|
|
@@ -240,8 +245,10 @@
|
|
|
240
245
|
</a>
|
|
241
246
|
<a href="#" class="nav-link" :class="{ active: view === 'tasks' }" @click.prevent="switchView('tasks')">
|
|
242
247
|
<i class="ti ti-checkup-list"></i>Tasks
|
|
243
|
-
<span class="
|
|
244
|
-
|
|
248
|
+
<span class="ms-auto d-inline-flex gap-1 align-items-center">
|
|
249
|
+
<span class="badge bg-warning text-white" x-show="attentionSummary.claimableTasks > 0" x-text="attentionSummary.claimableTasks"></span>
|
|
250
|
+
<span class="badge bg-secondary text-white" x-show="(stats.openTasks ?? 0) > 0" x-text="stats.openTasks ?? 0"></span>
|
|
251
|
+
</span>
|
|
245
252
|
</a>
|
|
246
253
|
<a href="#" class="nav-link" :class="{ active: view === 'analytics' }" @click.prevent="switchView('analytics')">
|
|
247
254
|
<i class="ti ti-chart-area-line"></i>Analytics
|
|
@@ -525,6 +532,9 @@
|
|
|
525
532
|
<div class="d-flex align-items-center mb-3 gap-3 flex-wrap">
|
|
526
533
|
<h2 class="page-title mb-0">Agents</h2>
|
|
527
534
|
<div class="ms-auto d-flex gap-2 align-items-center">
|
|
535
|
+
<button class="btn btn-sm btn-primary" @click="openAgentSpawn()" title="Create Codex live agent">
|
|
536
|
+
<i class="ti ti-plus"></i>
|
|
537
|
+
</button>
|
|
528
538
|
<select class="form-select form-select-sm" style="width:auto; min-width: 150px" x-model="agentPresetFilter">
|
|
529
539
|
<option value="">View: All</option>
|
|
530
540
|
<option value="active">View: Active agents</option>
|
|
@@ -663,10 +673,10 @@
|
|
|
663
673
|
<div class="row g-3">
|
|
664
674
|
<template x-for="channel in channelCards" :key="channel.id">
|
|
665
675
|
<div class="col-md-6 col-xl-4">
|
|
666
|
-
<div class="card">
|
|
676
|
+
<div class="card" style="cursor:pointer" @click="if (!window.getSelection().toString()) openChannelDetail(channel)">
|
|
667
677
|
<div class="card-body">
|
|
668
678
|
<div class="d-flex align-items-start gap-2">
|
|
669
|
-
<span class="agent-type-icon
|
|
679
|
+
<span class="agent-type-icon channel mt-0">
|
|
670
680
|
<i class="ti" :class="channelPresence(channel).icon"></i>
|
|
671
681
|
</span>
|
|
672
682
|
<div class="flex-grow-1 min-width-0">
|
|
@@ -696,8 +706,8 @@
|
|
|
696
706
|
<span x-text="'Last seen: ' + timeAgo(channel.lastSeen)"></span>
|
|
697
707
|
</div>
|
|
698
708
|
</div>
|
|
699
|
-
<button class="btn btn-sm btn-ghost-secondary p-1" title="Show
|
|
700
|
-
<i class="ti ti-
|
|
709
|
+
<button class="btn btn-sm btn-ghost-secondary p-1" title="Show channel" @click.stop="openChannelDetail(channel)">
|
|
710
|
+
<i class="ti ti-messages"></i>
|
|
701
711
|
</button>
|
|
702
712
|
</div>
|
|
703
713
|
</div>
|
|
@@ -844,7 +854,7 @@
|
|
|
844
854
|
</select>
|
|
845
855
|
</div>
|
|
846
856
|
<div class="col-md-3">
|
|
847
|
-
<select class="form-select form-select-sm" x-model="inboxCompose.to">
|
|
857
|
+
<select class="form-select form-select-sm" x-model="inboxCompose.to" @change="if (!inboxComposeTargetAllowsClaimable()) inboxCompose.claimable = false">
|
|
848
858
|
<option value="">Target</option>
|
|
849
859
|
<template x-for="option in inboxComposeTargetOptions" :key="option.value">
|
|
850
860
|
<option :value="option.value" x-text="option.label"></option>
|
|
@@ -857,7 +867,7 @@
|
|
|
857
867
|
<div class="col-md-2">
|
|
858
868
|
<input type="text" class="form-control form-control-sm" placeholder="Channel" x-model="inboxCompose.channel">
|
|
859
869
|
</div>
|
|
860
|
-
<div class="col-md-2 d-flex gap-2 align-items-center">
|
|
870
|
+
<div class="col-md-2 d-flex gap-2 align-items-center" x-show="inboxComposeTargetAllowsClaimable()">
|
|
861
871
|
<label class="form-check mb-0">
|
|
862
872
|
<input type="checkbox" class="form-check-input" x-model="inboxCompose.claimable">
|
|
863
873
|
<span class="form-check-label small">Claimable</span>
|
|
@@ -1471,16 +1481,16 @@
|
|
|
1471
1481
|
<div class="col-sm-6 col-lg-3">
|
|
1472
1482
|
<div class="card">
|
|
1473
1483
|
<div class="card-body text-center">
|
|
1474
|
-
<div class="text-secondary small">
|
|
1475
|
-
<div class="h2 mb-0" x-text="
|
|
1484
|
+
<div class="text-secondary small">Agents</div>
|
|
1485
|
+
<div class="h2 mb-0" x-text="onlineCount"></div>
|
|
1476
1486
|
</div>
|
|
1477
1487
|
</div>
|
|
1478
1488
|
</div>
|
|
1479
1489
|
<div class="col-sm-6 col-lg-3">
|
|
1480
1490
|
<div class="card">
|
|
1481
1491
|
<div class="card-body text-center">
|
|
1482
|
-
<div class="text-secondary small">
|
|
1483
|
-
<div class="h2 mb-0 text-
|
|
1492
|
+
<div class="text-secondary small">Busy Agents</div>
|
|
1493
|
+
<div class="h2 mb-0 text-warning" x-text="busyAgentCount"></div>
|
|
1484
1494
|
</div>
|
|
1485
1495
|
</div>
|
|
1486
1496
|
</div>
|
|
@@ -1488,7 +1498,7 @@
|
|
|
1488
1498
|
<div class="card">
|
|
1489
1499
|
<div class="card-body text-center">
|
|
1490
1500
|
<div class="text-secondary small">Messages (24h)</div>
|
|
1491
|
-
|
|
1501
|
+
<div class="h2 mb-0 text-info" x-text="stats.messagesLast24h ?? 0"></div>
|
|
1492
1502
|
</div>
|
|
1493
1503
|
</div>
|
|
1494
1504
|
</div>
|
|
@@ -1612,6 +1622,16 @@
|
|
|
1612
1622
|
<button class="btn btn-sm btn-ghost-secondary" @click="openRename(selectedAgentDetail)">
|
|
1613
1623
|
<i class="ti ti-pencil me-1"></i>Rename
|
|
1614
1624
|
</button>
|
|
1625
|
+
<template x-if="agentSupportsControlActions(selectedAgentDetail)">
|
|
1626
|
+
<button class="btn btn-sm btn-ghost-secondary" @click="openConfirm('Restart Agent', 'Restart agent ' + displayName(selectedAgentDetail) + '?', () => doAgentAction(selectedAgentDetail, 'restart'))">
|
|
1627
|
+
<i class="ti ti-refresh me-1"></i>Restart
|
|
1628
|
+
</button>
|
|
1629
|
+
</template>
|
|
1630
|
+
<template x-if="agentSupportsControlActions(selectedAgentDetail)">
|
|
1631
|
+
<button class="btn btn-sm btn-ghost-danger" @click="openConfirm('Shutdown Agent', 'Shut down agent ' + displayName(selectedAgentDetail) + '?', () => doAgentAction(selectedAgentDetail, 'shutdown'))">
|
|
1632
|
+
<i class="ti ti-power me-1"></i>Shutdown
|
|
1633
|
+
</button>
|
|
1634
|
+
</template>
|
|
1615
1635
|
</div>
|
|
1616
1636
|
|
|
1617
1637
|
<div class="p-3 border-bottom">
|
|
@@ -1766,6 +1786,141 @@
|
|
|
1766
1786
|
</div>
|
|
1767
1787
|
</template>
|
|
1768
1788
|
|
|
1789
|
+
<!-- ==================== CHANNEL DETAIL DRAWER ==================== -->
|
|
1790
|
+
<template x-if="selectedChannelDetail">
|
|
1791
|
+
<div>
|
|
1792
|
+
<div class="agent-drawer-backdrop" x-show="channelDetailOpen" x-cloak @click="closeChannelDetail()"></div>
|
|
1793
|
+
<aside class="agent-drawer" x-show="channelDetailOpen" x-cloak>
|
|
1794
|
+
<div class="p-3 border-bottom d-flex align-items-start gap-2">
|
|
1795
|
+
<span class="agent-type-icon channel mt-1" title="Channel">
|
|
1796
|
+
<i class="ti ti-messages"></i>
|
|
1797
|
+
</span>
|
|
1798
|
+
<div class="flex-grow-1 min-width-0">
|
|
1799
|
+
<div class="d-flex align-items-center gap-2">
|
|
1800
|
+
<span class="fw-bold text-truncate" x-text="selectedChannelDetail.name"></span>
|
|
1801
|
+
<span class="badge" :class="'bg-' + channelPresence(selectedChannelDetail).tone + '-lt'">
|
|
1802
|
+
<i class="ti me-1" :class="channelPresence(selectedChannelDetail).icon"></i><span x-text="channelPresence(selectedChannelDetail).label"></span>
|
|
1803
|
+
</span>
|
|
1804
|
+
</div>
|
|
1805
|
+
<div class="text-secondary small text-truncate" x-text="selectedChannelDetail.id"></div>
|
|
1806
|
+
</div>
|
|
1807
|
+
<button class="btn btn-sm btn-ghost-secondary p-1" @click="closeChannelDetail()" title="Close">
|
|
1808
|
+
<i class="ti ti-x"></i>
|
|
1809
|
+
</button>
|
|
1810
|
+
</div>
|
|
1811
|
+
|
|
1812
|
+
<div class="p-3 border-bottom d-flex gap-2 flex-wrap">
|
|
1813
|
+
<button class="btn btn-sm btn-primary" @click="compose = { ...compose, from: 'user', to: selectedChannelDetail.target || selectedChannelDetail.agentId, channel: selectedChannelDetail.id, body: '', claimable: false }; composeOpen = true">
|
|
1814
|
+
<i class="ti ti-send me-1"></i>Message
|
|
1815
|
+
</button>
|
|
1816
|
+
<button class="btn btn-sm btn-ghost-secondary" @click="channelFilter = selectedChannelDetail.id; switchView('messages'); closeChannelDetail()">
|
|
1817
|
+
<i class="ti ti-filter me-1"></i>Messages
|
|
1818
|
+
</button>
|
|
1819
|
+
<template x-if="agentsById[selectedChannelDetail.agentId]">
|
|
1820
|
+
<button class="btn btn-sm btn-ghost-secondary" @click="openAgentDetail(agentsById[selectedChannelDetail.agentId]); closeChannelDetail()">
|
|
1821
|
+
<i class="ti ti-robot me-1"></i>Backing agent
|
|
1822
|
+
</button>
|
|
1823
|
+
</template>
|
|
1824
|
+
</div>
|
|
1825
|
+
|
|
1826
|
+
<div class="p-3 border-bottom">
|
|
1827
|
+
<h3 class="card-title mb-3">Route</h3>
|
|
1828
|
+
<div class="detail-row mb-2">
|
|
1829
|
+
<div class="text-secondary small">Target</div>
|
|
1830
|
+
<div class="small text-break" x-text="displayTarget(selectedChannelDetail.target || selectedChannelDetail.agentId)"></div>
|
|
1831
|
+
</div>
|
|
1832
|
+
<div class="detail-row mb-2">
|
|
1833
|
+
<div class="text-secondary small">Backing agent</div>
|
|
1834
|
+
<div class="small text-break user-select-all" x-text="selectedChannelDetail.agentId"></div>
|
|
1835
|
+
</div>
|
|
1836
|
+
<div class="detail-row mb-2">
|
|
1837
|
+
<div class="text-secondary small">Transport</div>
|
|
1838
|
+
<div class="d-flex gap-1 flex-wrap">
|
|
1839
|
+
<span class="badge bg-info-lt" x-text="selectedChannelDetail.type"></span>
|
|
1840
|
+
<span class="badge bg-cyan-lt" x-text="selectedChannelDetail.transport || selectedChannelDetail.type"></span>
|
|
1841
|
+
<span class="badge bg-secondary-lt" x-text="selectedChannelDetail.direction"></span>
|
|
1842
|
+
</div>
|
|
1843
|
+
</div>
|
|
1844
|
+
<div class="detail-row mb-2">
|
|
1845
|
+
<div class="text-secondary small">Last seen</div>
|
|
1846
|
+
<div class="small" x-text="timeAgo(selectedChannelDetail.lastSeen)"></div>
|
|
1847
|
+
</div>
|
|
1848
|
+
</div>
|
|
1849
|
+
|
|
1850
|
+
<div class="p-3 border-bottom">
|
|
1851
|
+
<h3 class="card-title mb-3">Capabilities</h3>
|
|
1852
|
+
<div class="detail-row mb-2">
|
|
1853
|
+
<div class="text-secondary small">Capabilities</div>
|
|
1854
|
+
<div class="d-flex gap-1 flex-wrap">
|
|
1855
|
+
<template x-for="capability in (selectedChannelDetail.capabilities || [])" :key="capability">
|
|
1856
|
+
<span class="badge bg-purple-lt" x-text="capability"></span>
|
|
1857
|
+
</template>
|
|
1858
|
+
<template x-if="!(selectedChannelDetail.capabilities || []).length">
|
|
1859
|
+
<span class="text-secondary small">-</span>
|
|
1860
|
+
</template>
|
|
1861
|
+
</div>
|
|
1862
|
+
</div>
|
|
1863
|
+
<div class="detail-row mb-2">
|
|
1864
|
+
<div class="text-secondary small">Topics</div>
|
|
1865
|
+
<div class="d-flex gap-1 flex-wrap">
|
|
1866
|
+
<template x-for="topic in (selectedChannelDetail.topicChannels || [])" :key="topic">
|
|
1867
|
+
<span class="badge bg-warning-lt" x-text="'#' + topic"></span>
|
|
1868
|
+
</template>
|
|
1869
|
+
<template x-if="!(selectedChannelDetail.topicChannels || []).length">
|
|
1870
|
+
<span class="text-secondary small">-</span>
|
|
1871
|
+
</template>
|
|
1872
|
+
</div>
|
|
1873
|
+
</div>
|
|
1874
|
+
<div class="detail-row mb-2">
|
|
1875
|
+
<div class="text-secondary small">Tags</div>
|
|
1876
|
+
<div class="d-flex gap-1 flex-wrap">
|
|
1877
|
+
<template x-for="tag in (selectedChannelDetail.tags || [])" :key="tag">
|
|
1878
|
+
<span class="badge bg-cyan-lt" x-text="tag"></span>
|
|
1879
|
+
</template>
|
|
1880
|
+
<template x-if="!(selectedChannelDetail.tags || []).length">
|
|
1881
|
+
<span class="text-secondary small">-</span>
|
|
1882
|
+
</template>
|
|
1883
|
+
</div>
|
|
1884
|
+
</div>
|
|
1885
|
+
</div>
|
|
1886
|
+
|
|
1887
|
+
<div class="p-3 border-bottom">
|
|
1888
|
+
<h3 class="card-title mb-3">Metadata</h3>
|
|
1889
|
+
<template x-for="entry in Object.entries(selectedChannelDetail.meta || {})" :key="entry[0]">
|
|
1890
|
+
<div class="detail-row mb-2">
|
|
1891
|
+
<div class="text-secondary small" x-text="entry[0]"></div>
|
|
1892
|
+
<div class="small text-break" x-text="typeof entry[1] === 'object' ? JSON.stringify(entry[1]) : String(entry[1])"></div>
|
|
1893
|
+
</div>
|
|
1894
|
+
</template>
|
|
1895
|
+
<template x-if="!Object.keys(selectedChannelDetail.meta || {}).length">
|
|
1896
|
+
<div class="text-secondary small">No metadata</div>
|
|
1897
|
+
</template>
|
|
1898
|
+
</div>
|
|
1899
|
+
|
|
1900
|
+
<div class="p-3">
|
|
1901
|
+
<h3 class="card-title mb-3">Recent Messages</h3>
|
|
1902
|
+
<div class="list-group list-group-flush">
|
|
1903
|
+
<template x-for="m in channelDetailMessages" :key="m.id">
|
|
1904
|
+
<div class="list-group-item px-0">
|
|
1905
|
+
<div class="d-flex align-items-center gap-2 mb-1">
|
|
1906
|
+
<span class="fw-bold small" x-text="displayTarget(m.from)"></span>
|
|
1907
|
+
<i class="ti ti-arrow-right text-secondary" style="font-size:12px"></i>
|
|
1908
|
+
<span class="small" x-text="displayTarget(m.to)"></span>
|
|
1909
|
+
<span class="badge bg-warning-lt" x-show="m.channel" x-text="'#' + m.channel"></span>
|
|
1910
|
+
<span class="text-secondary small ms-auto" x-text="'#' + m.id"></span>
|
|
1911
|
+
</div>
|
|
1912
|
+
<div class="text-secondary small text-truncate" x-text="messagePreview(m)"></div>
|
|
1913
|
+
</div>
|
|
1914
|
+
</template>
|
|
1915
|
+
<template x-if="channelDetailMessages.length === 0">
|
|
1916
|
+
<div class="text-secondary small">No recent channel messages loaded</div>
|
|
1917
|
+
</template>
|
|
1918
|
+
</div>
|
|
1919
|
+
</div>
|
|
1920
|
+
</aside>
|
|
1921
|
+
</div>
|
|
1922
|
+
</template>
|
|
1923
|
+
|
|
1769
1924
|
<!-- ==================== COMPOSE MODAL ==================== -->
|
|
1770
1925
|
<div class="modal modal-blur" :class="{ show: composeOpen }" :style="composeOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="composeOpen = false">
|
|
1771
1926
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
@@ -1787,14 +1942,15 @@
|
|
|
1787
1942
|
<label class="form-label">From</label>
|
|
1788
1943
|
<select class="form-select" x-model="compose.from">
|
|
1789
1944
|
<option value="">Select sender…</option>
|
|
1790
|
-
<
|
|
1945
|
+
<option value="user">User [user]</option>
|
|
1946
|
+
<template x-for="a in composeAgents.filter((agent) => agent.id !== 'user')" :key="a.id">
|
|
1791
1947
|
<option :value="a.id" x-text="displayName(a) + ' [' + a.id.slice(-6) + ']'"></option>
|
|
1792
1948
|
</template>
|
|
1793
1949
|
</select>
|
|
1794
1950
|
</div>
|
|
1795
1951
|
<div class="col-md-6">
|
|
1796
1952
|
<label class="form-label">To</label>
|
|
1797
|
-
<select class="form-select" x-model="compose.to">
|
|
1953
|
+
<select class="form-select" x-model="compose.to" @change="if (!composeTargetAllowsClaimable()) compose.claimable = false">
|
|
1798
1954
|
<option value="">Select recipient…</option>
|
|
1799
1955
|
<option value="broadcast">📢 Broadcast (all)</option>
|
|
1800
1956
|
<optgroup label="Labels">
|
|
@@ -1831,7 +1987,7 @@
|
|
|
1831
1987
|
<label class="form-label">Message</label>
|
|
1832
1988
|
<textarea class="form-control" rows="5" x-model="compose.body" placeholder="Type your message…" x-ref="composeBody"></textarea>
|
|
1833
1989
|
</div>
|
|
1834
|
-
<div class="col-12">
|
|
1990
|
+
<div class="col-12" x-show="composeTargetAllowsClaimable()">
|
|
1835
1991
|
<label class="form-check">
|
|
1836
1992
|
<input type="checkbox" class="form-check-input" x-model="compose.claimable">
|
|
1837
1993
|
<span class="form-check-label">Claimable task (only one agent can claim)</span>
|
|
@@ -1850,6 +2006,88 @@
|
|
|
1850
2006
|
</div>
|
|
1851
2007
|
<div class="modal-backdrop fade show" x-show="composeOpen" x-cloak></div>
|
|
1852
2008
|
|
|
2009
|
+
<!-- ==================== AGENT SPAWN MODAL ==================== -->
|
|
2010
|
+
<div class="modal modal-blur" :class="{ show: agentSpawnOpen }" :style="agentSpawnOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="closeAgentSpawn()">
|
|
2011
|
+
<div class="modal-dialog modal-md modal-dialog-centered">
|
|
2012
|
+
<div class="modal-content">
|
|
2013
|
+
<div class="modal-header">
|
|
2014
|
+
<h5 class="modal-title">Create Agent</h5>
|
|
2015
|
+
<button class="btn-close" @click="closeAgentSpawn()"></button>
|
|
2016
|
+
</div>
|
|
2017
|
+
<div class="modal-body">
|
|
2018
|
+
<div class="row g-3">
|
|
2019
|
+
<div class="col-12">
|
|
2020
|
+
<label class="form-label">Provider</label>
|
|
2021
|
+
<select class="form-select" x-model="agentSpawn.provider">
|
|
2022
|
+
<option value="codex">Codex Live</option>
|
|
2023
|
+
</select>
|
|
2024
|
+
</div>
|
|
2025
|
+
<div class="col-12">
|
|
2026
|
+
<label class="form-label">Permission model</label>
|
|
2027
|
+
<select class="form-select" x-model="agentSpawn.approvalMode">
|
|
2028
|
+
<option value="guarded">Guarded</option>
|
|
2029
|
+
<option value="read-only">Read-only</option>
|
|
2030
|
+
<option value="open">Open</option>
|
|
2031
|
+
</select>
|
|
2032
|
+
</div>
|
|
2033
|
+
<div class="col-12">
|
|
2034
|
+
<label class="form-label">Working directory <span class="text-secondary">(optional)</span></label>
|
|
2035
|
+
<div class="input-group">
|
|
2036
|
+
<input type="text" class="form-control" x-model="agentSpawn.cwd" x-ref="agentSpawnCwd" placeholder="Defaults to the relay server working directory">
|
|
2037
|
+
<button class="btn btn-ghost-secondary" @click="openAgentDirectoryBrowser()" title="Browse host directories">
|
|
2038
|
+
<i class="ti ti-folder-open me-1"></i>Browse
|
|
2039
|
+
</button>
|
|
2040
|
+
</div>
|
|
2041
|
+
<div class="border rounded mt-2" x-show="agentDirectoryBrowser.open" x-cloak>
|
|
2042
|
+
<div class="d-flex align-items-center gap-2 p-2 border-bottom">
|
|
2043
|
+
<button class="btn btn-sm btn-ghost-secondary" :disabled="!agentDirectoryBrowser.parent || agentDirectoryBrowser.loading" @click="browseAgentDirectory(agentDirectoryBrowser.parent)" title="Parent directory">
|
|
2044
|
+
<i class="ti ti-arrow-up"></i>
|
|
2045
|
+
</button>
|
|
2046
|
+
<button class="btn btn-sm btn-ghost-secondary" :disabled="!agentDirectoryBrowser.home || agentDirectoryBrowser.loading" @click="browseAgentDirectory(agentDirectoryBrowser.home)" title="Home directory">
|
|
2047
|
+
<i class="ti ti-home"></i>
|
|
2048
|
+
</button>
|
|
2049
|
+
<button class="btn btn-sm btn-ghost-secondary" :disabled="!agentDirectoryBrowser.cwd || agentDirectoryBrowser.loading" @click="browseAgentDirectory(agentDirectoryBrowser.cwd)" title="Server directory">
|
|
2050
|
+
<i class="ti ti-server"></i>
|
|
2051
|
+
</button>
|
|
2052
|
+
<div class="small text-secondary text-truncate flex-grow-1" x-text="agentDirectoryBrowser.path || 'Loading...'"></div>
|
|
2053
|
+
<button class="btn btn-sm btn-primary" :disabled="!agentDirectoryBrowser.path" @click="selectAgentDirectory(agentDirectoryBrowser.path)">Select</button>
|
|
2054
|
+
</div>
|
|
2055
|
+
<div class="p-2" style="max-height: 240px; overflow:auto">
|
|
2056
|
+
<template x-if="agentDirectoryBrowser.loading">
|
|
2057
|
+
<div class="text-secondary small py-3 text-center">Loading directories...</div>
|
|
2058
|
+
</template>
|
|
2059
|
+
<template x-if="agentDirectoryBrowser.error">
|
|
2060
|
+
<div class="alert alert-danger py-2 mb-2" x-text="agentDirectoryBrowser.error"></div>
|
|
2061
|
+
</template>
|
|
2062
|
+
<template x-for="entry in agentDirectoryBrowser.entries" :key="entry.path">
|
|
2063
|
+
<button type="button" class="list-group-item list-group-item-action d-flex align-items-center gap-2 w-100 text-start border-0 rounded" @click="browseAgentDirectory(entry.path)">
|
|
2064
|
+
<i class="ti ti-folder text-warning"></i>
|
|
2065
|
+
<span class="text-truncate" x-text="entry.name"></span>
|
|
2066
|
+
</button>
|
|
2067
|
+
</template>
|
|
2068
|
+
<template x-if="!agentDirectoryBrowser.loading && !agentDirectoryBrowser.error && agentDirectoryBrowser.entries.length === 0">
|
|
2069
|
+
<div class="text-secondary small py-3 text-center">No child directories</div>
|
|
2070
|
+
</template>
|
|
2071
|
+
</div>
|
|
2072
|
+
</div>
|
|
2073
|
+
</div>
|
|
2074
|
+
<div class="col-12">
|
|
2075
|
+
<label class="form-label">Label <span class="text-secondary">(optional)</span></label>
|
|
2076
|
+
<input type="text" class="form-control" x-model="agentSpawn.label" placeholder="e.g. backend helper">
|
|
2077
|
+
</div>
|
|
2078
|
+
</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
<div class="modal-footer">
|
|
2081
|
+
<button class="btn btn-ghost-secondary" @click="closeAgentSpawn()">Cancel</button>
|
|
2082
|
+
<button class="btn btn-primary" @click="doSpawnAgent()">
|
|
2083
|
+
<i class="ti ti-plus me-1"></i>Create
|
|
2084
|
+
</button>
|
|
2085
|
+
</div>
|
|
2086
|
+
</div>
|
|
2087
|
+
</div>
|
|
2088
|
+
</div>
|
|
2089
|
+
<div class="modal-backdrop fade show" x-show="agentSpawnOpen" x-cloak></div>
|
|
2090
|
+
|
|
1853
2091
|
<!-- ==================== PAIR INVITE MODAL ==================== -->
|
|
1854
2092
|
<div class="modal modal-blur" :class="{ show: pairInviteOpen }" :style="pairInviteOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="closePairInvite()">
|
|
1855
2093
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export type CodexSpawnApprovalMode = "open" | "guarded" | "read-only";
|
|
6
|
+
|
|
7
|
+
interface CodexSpawnInput {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
approvalMode: CodexSpawnApprovalMode;
|
|
10
|
+
label?: string;
|
|
11
|
+
relayUrl: string;
|
|
12
|
+
token?: string;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CodexSpawnResult {
|
|
17
|
+
provider: "codex";
|
|
18
|
+
pid?: number;
|
|
19
|
+
cwd: string;
|
|
20
|
+
approvalMode: CodexSpawnApprovalMode;
|
|
21
|
+
logPath: string;
|
|
22
|
+
command: string[];
|
|
23
|
+
dryRun?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HostDirectoryEntry {
|
|
27
|
+
name: string;
|
|
28
|
+
path: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface HostDirectoryListing {
|
|
32
|
+
path: string;
|
|
33
|
+
parent?: string;
|
|
34
|
+
home: string;
|
|
35
|
+
cwd: string;
|
|
36
|
+
entries: HostDirectoryEntry[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeCodexSpawnCwd(raw: string | undefined, fallback = process.cwd()): string {
|
|
40
|
+
const cwd = resolve(raw || fallback);
|
|
41
|
+
if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
|
|
42
|
+
throw new Error(`cwd does not exist or is not a directory: ${cwd}`);
|
|
43
|
+
}
|
|
44
|
+
return cwd;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function listHostDirectories(raw: string | undefined): HostDirectoryListing {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const home = homedir();
|
|
50
|
+
const path = normalizeCodexSpawnCwd(raw || cwd, cwd);
|
|
51
|
+
const entries = readdirSync(path, { withFileTypes: true })
|
|
52
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
53
|
+
.map((entry) => ({
|
|
54
|
+
name: entry.name,
|
|
55
|
+
path: join(path, entry.name),
|
|
56
|
+
}))
|
|
57
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
const parent = dirname(path);
|
|
59
|
+
return {
|
|
60
|
+
path,
|
|
61
|
+
parent: parent !== path ? parent : undefined,
|
|
62
|
+
home,
|
|
63
|
+
cwd,
|
|
64
|
+
entries,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function codexSpawnCommand(): string[] {
|
|
69
|
+
const override = process.env.AGENT_RELAY_CODEX_RELAY_BIN;
|
|
70
|
+
if (override) return [override];
|
|
71
|
+
|
|
72
|
+
const repoLauncher = resolve(import.meta.dir, "../codex/bin/agent-relay-codex.ts");
|
|
73
|
+
if (existsSync(repoLauncher)) return ["bun", "run", repoLauncher, "start"];
|
|
74
|
+
|
|
75
|
+
const fromPath = findOnPath("codex-relay");
|
|
76
|
+
if (fromPath) return [fromPath];
|
|
77
|
+
|
|
78
|
+
return ["codex-relay"];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function codexSpawnLogPath(cwd: string, now = Date.now()): string {
|
|
82
|
+
const project = basename(cwd).replace(/[^a-zA-Z0-9._-]+/g, "-") || "project";
|
|
83
|
+
return join(homedir(), ".agent-relay", "spawns", `codex-${project}-${now}.log`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function spawnCodexAgent(input: CodexSpawnInput): CodexSpawnResult {
|
|
87
|
+
const cwd = normalizeCodexSpawnCwd(input.cwd);
|
|
88
|
+
const command = [
|
|
89
|
+
...codexSpawnCommand(),
|
|
90
|
+
"--headless",
|
|
91
|
+
"--relay-url",
|
|
92
|
+
input.relayUrl,
|
|
93
|
+
];
|
|
94
|
+
const logPath = codexSpawnLogPath(cwd);
|
|
95
|
+
const env: Record<string, string | undefined> = {
|
|
96
|
+
...process.env,
|
|
97
|
+
AGENT_RELAY_CODEX_HEADLESS: "1",
|
|
98
|
+
AGENT_RELAY_APPROVAL: input.approvalMode,
|
|
99
|
+
AGENT_RELAY_TAGS: mergeCsv(process.env.AGENT_RELAY_TAGS, ["headless", "dashboard-spawned"]),
|
|
100
|
+
AGENT_RELAY_LABEL: input.label || process.env.AGENT_RELAY_LABEL,
|
|
101
|
+
AGENT_RELAY_URL: input.relayUrl,
|
|
102
|
+
AGENT_RELAY_TOKEN: input.token || process.env.AGENT_RELAY_TOKEN,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (input.dryRun) {
|
|
106
|
+
return { provider: "codex", cwd, approvalMode: input.approvalMode, logPath, command, dryRun: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
110
|
+
const log = Bun.file(logPath);
|
|
111
|
+
const child = Bun.spawn(command, {
|
|
112
|
+
cwd,
|
|
113
|
+
env,
|
|
114
|
+
stdin: "ignore",
|
|
115
|
+
stdout: log,
|
|
116
|
+
stderr: log,
|
|
117
|
+
});
|
|
118
|
+
child.unref();
|
|
119
|
+
|
|
120
|
+
return { provider: "codex", pid: child.pid, cwd, approvalMode: input.approvalMode, logPath, command };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergeCsv(raw: string | undefined, additions: string[]): string {
|
|
124
|
+
return [...new Set([
|
|
125
|
+
...(raw || "").split(",").map((item) => item.trim()).filter(Boolean),
|
|
126
|
+
...additions,
|
|
127
|
+
])].join(",");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findOnPath(command: string): string | null {
|
|
131
|
+
for (const dir of (process.env.PATH || "").split(delimiter)) {
|
|
132
|
+
if (!dir) continue;
|
|
133
|
+
const candidate = join(dir, command);
|
|
134
|
+
if (existsSync(candidate)) return candidate;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -1328,6 +1328,44 @@ function findMessageByIdempotencyKey(from: string, key: string): Message | null
|
|
|
1328
1328
|
return row ? rowToMessage(row) : null;
|
|
1329
1329
|
}
|
|
1330
1330
|
|
|
1331
|
+
function isDeliveryAgent(agent: AgentCard): boolean {
|
|
1332
|
+
return agent.status !== "offline" &&
|
|
1333
|
+
agent.id !== "user" &&
|
|
1334
|
+
agent.id !== "system" &&
|
|
1335
|
+
agent.meta?.kind !== "channel";
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function matchingDeliveryAgents(target: string): AgentCard[] {
|
|
1339
|
+
if (!target) return [];
|
|
1340
|
+
const candidates = listAgents().filter(isDeliveryAgent);
|
|
1341
|
+
if (target === "broadcast") return candidates;
|
|
1342
|
+
const direct = getAgent(target);
|
|
1343
|
+
if (direct) return isDeliveryAgent(direct) ? [direct] : [];
|
|
1344
|
+
if (target.startsWith("tag:")) {
|
|
1345
|
+
const tag = target.slice(4);
|
|
1346
|
+
return candidates.filter((agent) => agent.tags.includes(tag));
|
|
1347
|
+
}
|
|
1348
|
+
if (target.startsWith("cap:")) {
|
|
1349
|
+
const cap = target.slice(4);
|
|
1350
|
+
return candidates.filter((agent) => agent.capabilities.includes(cap));
|
|
1351
|
+
}
|
|
1352
|
+
if (target.startsWith("label:")) {
|
|
1353
|
+
const label = target.slice(6);
|
|
1354
|
+
return candidates.filter((agent) => agent.label === label);
|
|
1355
|
+
}
|
|
1356
|
+
return [];
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function claimableAllowedForTarget(target: string): boolean {
|
|
1360
|
+
return matchingDeliveryAgents(target).length > 1;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function shouldStoreClaimable(input: SendMessageInput): boolean {
|
|
1364
|
+
if (!input.claimable) return false;
|
|
1365
|
+
if (input.type === "system" || typeof input.meta?.taskId === "number") return true;
|
|
1366
|
+
return claimableAllowedForTarget(input.to);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1331
1369
|
export function sendMessageWithResult(input: SendMessageInput): { message: Message; created: boolean } {
|
|
1332
1370
|
const now = Date.now();
|
|
1333
1371
|
|
|
@@ -1354,6 +1392,7 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
1354
1392
|
VALUES ($from, $to, $type, $channel, $subject, $body, $threadId, $replyTo, $claimable, $idempotencyKey, $meta, $now)
|
|
1355
1393
|
`);
|
|
1356
1394
|
const setSelfThread = db.prepare("UPDATE messages SET thread_id = ? WHERE id = ?");
|
|
1395
|
+
const claimable = shouldStoreClaimable(input);
|
|
1357
1396
|
|
|
1358
1397
|
const id = db.transaction(() => {
|
|
1359
1398
|
const result = insert.run({
|
|
@@ -1365,7 +1404,7 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
1365
1404
|
$body: input.body,
|
|
1366
1405
|
$threadId: threadId,
|
|
1367
1406
|
$replyTo: input.replyTo ?? null,
|
|
1368
|
-
$claimable:
|
|
1407
|
+
$claimable: claimable ? 1 : 0,
|
|
1369
1408
|
$idempotencyKey: input.idempotencyKey ?? null,
|
|
1370
1409
|
$meta: JSON.stringify(input.meta ?? {}),
|
|
1371
1410
|
$now: now,
|
package/src/routes.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
} from "./db";
|
|
53
53
|
import type { ActivityEventInput, ActivityKind, AgentCard, AgentSessionGuard, ChannelDirection, ChannelSummary, CreatePairInput, IntegrationEventInput, IntegrationSummary, PairActionInput, PairMessageInput, PairStatus, RegisterAgentInput, SendMessageInput, TaskStatus, TaskStatusInput } from "./types";
|
|
54
54
|
import { getIntegrationTokens, INTEGRATION_RATE_LIMIT_PER_MINUTE, MAX_BODY_BYTES } from "./config";
|
|
55
|
+
import { listHostDirectories, spawnCodexAgent, type CodexSpawnApprovalMode } from "./agent-spawn";
|
|
55
56
|
import {
|
|
56
57
|
getIntegrationAuth,
|
|
57
58
|
hasIntegrationScope,
|
|
@@ -150,6 +151,9 @@ function parseQueryInt(
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
const VALID_AGENT_STATUSES = ["online", "idle", "busy", "offline"] as const;
|
|
154
|
+
const VALID_AGENT_ACTIONS = ["restart", "shutdown"] as const;
|
|
155
|
+
const VALID_AGENT_SPAWN_PROVIDERS = ["codex"] as const;
|
|
156
|
+
const VALID_CODEX_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
|
|
153
157
|
const VALID_TASK_SEVERITIES = ["info", "warning", "critical"] as const;
|
|
154
158
|
const VALID_TASK_STATUSES = ["open", "claimed", "in_progress", "blocked", "done", "failed", "canceled"] as const;
|
|
155
159
|
const VALID_PAIR_STATUSES = ["pending", "active", "ended", "rejected", "expired"] as const;
|
|
@@ -738,6 +742,119 @@ const deleteAgentById: Handler = (_req, params) => {
|
|
|
738
742
|
return json({ ok: true });
|
|
739
743
|
};
|
|
740
744
|
|
|
745
|
+
function agentCanReceiveControlAction(agent: AgentCard): boolean {
|
|
746
|
+
return agent.id !== "user" &&
|
|
747
|
+
agent.id !== "system" &&
|
|
748
|
+
agent.meta?.kind !== "channel" &&
|
|
749
|
+
!agent.tags.includes("channel");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const postAgentAction: Handler = async (req, params) => {
|
|
753
|
+
const parsed = await parseBody<unknown>(req);
|
|
754
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
755
|
+
try {
|
|
756
|
+
if (!isRecord(parsed.body)) return error("action required");
|
|
757
|
+
const action = cleanEnum(parsed.body.action, "action", VALID_AGENT_ACTIONS);
|
|
758
|
+
if (!action) return error("action required");
|
|
759
|
+
const agent = getAgent(params.id!);
|
|
760
|
+
if (!agent) return error("agent not found", 404);
|
|
761
|
+
if (!agentCanReceiveControlAction(agent)) return error("agent does not support dashboard control actions", 400);
|
|
762
|
+
|
|
763
|
+
const title = action === "restart" ? "Agent restart requested" : "Agent shutdown requested";
|
|
764
|
+
const msg = sendMessage({
|
|
765
|
+
from: "system",
|
|
766
|
+
to: agent.id,
|
|
767
|
+
type: "system",
|
|
768
|
+
subject: title,
|
|
769
|
+
body: action === "restart"
|
|
770
|
+
? "Dashboard requested that this agent restart its relay-managed session now."
|
|
771
|
+
: "Dashboard requested that this agent shut down its relay-managed session now.",
|
|
772
|
+
meta: {
|
|
773
|
+
agentControl: {
|
|
774
|
+
action,
|
|
775
|
+
requestedBy: "dashboard",
|
|
776
|
+
requestedAt: Date.now(),
|
|
777
|
+
},
|
|
778
|
+
delivery: "interrupt",
|
|
779
|
+
priority: "urgent",
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
emitNewMessage(msg);
|
|
783
|
+
auditEvent({
|
|
784
|
+
clientId: "server-agent-" + agent.id + "-action-" + action + "-" + msg.id,
|
|
785
|
+
kind: "state",
|
|
786
|
+
title,
|
|
787
|
+
body: action,
|
|
788
|
+
meta: agent.id,
|
|
789
|
+
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
790
|
+
view: "agents",
|
|
791
|
+
messageId: msg.id,
|
|
792
|
+
agentId: agent.id,
|
|
793
|
+
metadata: { action },
|
|
794
|
+
});
|
|
795
|
+
return json({ ok: true, action, message: msg }, 202);
|
|
796
|
+
} catch (e) {
|
|
797
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
798
|
+
throw e;
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
const postAgentSpawn: Handler = async (req) => {
|
|
803
|
+
const parsed = await parseBody<unknown>(req);
|
|
804
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
805
|
+
try {
|
|
806
|
+
if (!isRecord(parsed.body)) return error("provider required");
|
|
807
|
+
const provider = cleanEnum(parsed.body.provider, "provider", VALID_AGENT_SPAWN_PROVIDERS);
|
|
808
|
+
if (provider !== "codex") return error("provider must be codex");
|
|
809
|
+
const approvalMode = cleanEnum(parsed.body.approvalMode, "approvalMode", VALID_CODEX_SPAWN_APPROVALS, "guarded") as CodexSpawnApprovalMode;
|
|
810
|
+
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
811
|
+
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
812
|
+
const relayUrl = process.env.AGENT_RELAY_SPAWN_RELAY_URL || process.env.AGENT_RELAY_URL || `http://127.0.0.1:${process.env.PORT || "4850"}`;
|
|
813
|
+
const token = req.headers.get("X-Agent-Relay-Token") ?? req.headers.get("Authorization")?.replace(/^Bearer\s+/i, "");
|
|
814
|
+
const result = spawnCodexAgent({
|
|
815
|
+
cwd,
|
|
816
|
+
approvalMode,
|
|
817
|
+
label,
|
|
818
|
+
relayUrl,
|
|
819
|
+
token: token || undefined,
|
|
820
|
+
dryRun: process.env.AGENT_RELAY_SPAWN_DRY_RUN === "1",
|
|
821
|
+
});
|
|
822
|
+
auditEvent({
|
|
823
|
+
clientId: "server-agent-spawn-codex-" + Date.now(),
|
|
824
|
+
kind: "state",
|
|
825
|
+
title: "Codex agent spawn requested",
|
|
826
|
+
body: result.cwd,
|
|
827
|
+
meta: result.pid ? `pid ${result.pid}` : "dry run",
|
|
828
|
+
icon: "ti-plus",
|
|
829
|
+
view: "agents",
|
|
830
|
+
metadata: {
|
|
831
|
+
provider: result.provider,
|
|
832
|
+
approvalMode: result.approvalMode,
|
|
833
|
+
pid: result.pid ?? null,
|
|
834
|
+
logPath: result.logPath,
|
|
835
|
+
dryRun: result.dryRun === true,
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
return json(result, 202);
|
|
839
|
+
} catch (e) {
|
|
840
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
841
|
+
if (e instanceof Error) return error(e.message, 400);
|
|
842
|
+
throw e;
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const getHostDirectories: Handler = (req) => {
|
|
847
|
+
try {
|
|
848
|
+
const url = new URL(req.url);
|
|
849
|
+
const path = cleanString(url.searchParams.get("path") ?? undefined, "path", { max: 500 });
|
|
850
|
+
return json(listHostDirectories(path));
|
|
851
|
+
} catch (e) {
|
|
852
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
853
|
+
if (e instanceof Error) return error(e.message, 400);
|
|
854
|
+
throw e;
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
741
858
|
// --- Message routes ---
|
|
742
859
|
|
|
743
860
|
const VALID_MSG_TYPES = ["message", "system"];
|
|
@@ -1412,14 +1529,17 @@ function route(method: string, path: string, handler: Handler): Route {
|
|
|
1412
1529
|
|
|
1413
1530
|
const routes: Route[] = [
|
|
1414
1531
|
route("POST", "/api/agents", postAgent),
|
|
1532
|
+
route("POST", "/api/agents/spawn", postAgentSpawn),
|
|
1415
1533
|
route("GET", "/api/agents", getAgents),
|
|
1416
1534
|
route("GET", "/api/agents/find", findAgents),
|
|
1535
|
+
route("GET", "/api/agents/spawn/directories", getHostDirectories),
|
|
1417
1536
|
route("GET", "/api/agents/:id", getAgentById),
|
|
1418
1537
|
route("PATCH", "/api/agents/:id/status", patchAgentStatus),
|
|
1419
1538
|
route("PATCH", "/api/agents/:id/ready", patchAgentReady),
|
|
1420
1539
|
route("PATCH", "/api/agents/:id/label", patchAgentLabel),
|
|
1421
1540
|
route("PATCH", "/api/agents/:id/tags", patchAgentTags),
|
|
1422
1541
|
route("POST", "/api/agents/:id/heartbeat", postHeartbeat),
|
|
1542
|
+
route("POST", "/api/agents/:id/actions", postAgentAction),
|
|
1423
1543
|
route("DELETE", "/api/agents/:id", deleteAgentById),
|
|
1424
1544
|
|
|
1425
1545
|
route("POST", "/api/system/broadcast", postSystemBroadcast),
|