agent-relay-server 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/package.json +1 -1
- package/public/dashboard.js +119 -17
- package/public/icons/agent-relay-192.png +0 -0
- package/public/icons/agent-relay-512.png +0 -0
- package/public/icons/agent-relay.svg +14 -0
- package/public/index.html +104 -9
- package/public/manifest.webmanifest +33 -0
- package/public/sw.js +58 -0
- package/src/cli.ts +80 -17
- package/src/connectors.ts +256 -0
- package/src/db.ts +548 -26
- package/src/index.ts +25 -1
- package/src/routes.ts +380 -32
- package/src/security.ts +2 -1
- package/src/sse.ts +6 -0
- package/src/types.ts +109 -3
package/README.md
CHANGED
|
@@ -40,20 +40,20 @@ Agent Relay has two parts:
|
|
|
40
40
|
|
|
41
41
|
Pick your provider: install the Claude Code plugin, the Codex connector, or both. The shared config is the same four env vars either way. See the [full mental model](docs/mental-model.md) for routing, identity, and task lifecycle details, or the [provider spec](docs/provider-spec.md) if you want to build your own integration.
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Connector Model
|
|
44
44
|
|
|
45
|
-
Agent Relay
|
|
45
|
+
Agent Relay extensions run as external **Connectors**. The relay discovers them from `~/.agent-relay/connectors/<id>/manifest.json`, then can show status and run lifecycle actions without loading connector code in-process.
|
|
46
46
|
|
|
47
47
|
> **Agents do work. Providers host agents. Integrations create work. Channels carry conversations.**
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Connector kinds:
|
|
50
50
|
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
51
|
+
- **Provider connectors** host AI runtimes. The Claude plugin and Codex connector register coding-agent sessions as Relay agents.
|
|
52
|
+
- **Event connectors** connect external systems that create or update work. CI, monitoring, support desks, and deployment tools fit here.
|
|
53
|
+
- **Channel connectors** connect communication surfaces where humans and systems already talk. Telegram is the first channel; Slack, email, SMS, Matrix, Discord, and web chat are natural next ones.
|
|
54
|
+
- **Orchestrator connectors** supervise hosts and can spawn or control provider sessions.
|
|
55
55
|
|
|
56
|
-
If you want to contribute, pick the shape that matches your system. Building a new AI runtime adapter? Build a provider. Sending alerts or tickets into Agent Relay? Build an
|
|
56
|
+
If you want to contribute, pick the shape that matches your system. Building a new AI runtime adapter? Build a provider connector. Sending alerts or tickets into Agent Relay? Build an event connector. Opening Agent Relay to another messaging app? Build a channel connector.
|
|
57
57
|
|
|
58
58
|
## Quick Start
|
|
59
59
|
|
package/package.json
CHANGED
package/public/dashboard.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
tasks: [],
|
|
61
61
|
integrations: [],
|
|
62
62
|
channels: [],
|
|
63
|
+
connectors: [],
|
|
63
64
|
taskEvents: [],
|
|
64
65
|
taskEventCache: {},
|
|
65
66
|
stats: {},
|
|
@@ -240,6 +241,7 @@
|
|
|
240
241
|
if (view === "work") await Promise.all([this.fetchMessages(), this.fetchTasks()]);
|
|
241
242
|
if (view === "pairs") this.fetchPairs();
|
|
242
243
|
if (view === "channels") await this.fetchChannels();
|
|
244
|
+
if (view === "connectors") await this.fetchConnectors();
|
|
243
245
|
if (view === "integrations") await this.fetchIntegrations();
|
|
244
246
|
if (view === "tasks") this.fetchTasks();
|
|
245
247
|
},
|
|
@@ -393,7 +395,7 @@
|
|
|
393
395
|
},
|
|
394
396
|
|
|
395
397
|
async refresh() {
|
|
396
|
-
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchOrchestrators(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchChannels(), this.fetchIntegrations(), this.fetchInboxState(), this.fetchActivityEvents()]);
|
|
398
|
+
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchOrchestrators(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchChannels(), this.fetchConnectors(), this.fetchIntegrations(), this.fetchInboxState(), this.fetchActivityEvents()]);
|
|
397
399
|
},
|
|
398
400
|
|
|
399
401
|
async refreshLiveData() {
|
|
@@ -476,6 +478,12 @@
|
|
|
476
478
|
} catch {}
|
|
477
479
|
},
|
|
478
480
|
|
|
481
|
+
async fetchConnectors() {
|
|
482
|
+
try {
|
|
483
|
+
this.connectors = await this.api("GET", "/connectors");
|
|
484
|
+
} catch {}
|
|
485
|
+
},
|
|
486
|
+
|
|
479
487
|
async fetchChannels() {
|
|
480
488
|
try {
|
|
481
489
|
this.channels = await this.api("GET", "/channels");
|
|
@@ -548,6 +556,7 @@
|
|
|
548
556
|
activityItems: { get: getActivityItems },
|
|
549
557
|
workQueueItems: { get: getWorkQueueItems },
|
|
550
558
|
channelCards: { get: getChannelCards },
|
|
559
|
+
connectorCards: { get: getConnectorCards },
|
|
551
560
|
integrationCards: { get: getIntegrationCards },
|
|
552
561
|
openPairCount: { get: getOpenPairCount },
|
|
553
562
|
filteredMessages: { get: getFilteredMessages },
|
|
@@ -644,6 +653,11 @@
|
|
|
644
653
|
if (!message || !channel) return false;
|
|
645
654
|
const channelKeys = [channel.id, channel.type, ...(channel.topicChannels || [])].filter(Boolean);
|
|
646
655
|
if (message.channel && channelKeys.includes(message.channel)) return true;
|
|
656
|
+
const payloadChannel = message.payload?.channel;
|
|
657
|
+
if (payloadChannel && typeof payloadChannel === "object") {
|
|
658
|
+
const payloadKeys = [payloadChannel.agentId, payloadChannel.provider, payloadChannel.accountId].filter(Boolean);
|
|
659
|
+
if (payloadKeys.includes(channel.id) || payloadKeys.includes(channel.type) || payloadKeys.includes(channel.accountId)) return true;
|
|
660
|
+
}
|
|
647
661
|
if (channel.agentId && (message.from === channel.agentId || message.to === channel.agentId)) return true;
|
|
648
662
|
return false;
|
|
649
663
|
}
|
|
@@ -710,7 +724,7 @@
|
|
|
710
724
|
const haystack = [
|
|
711
725
|
vm.displayTarget(thread.peer),
|
|
712
726
|
thread.peer,
|
|
713
|
-
...thread.messages.flatMap((msg) => [msg.subject || "",
|
|
727
|
+
...thread.messages.flatMap((msg) => [msg.subject || "", messageBody.call(vm, msg) || "", msg.channel || "", vm.displayTarget(msg.from), vm.displayTarget(msg.to)]),
|
|
714
728
|
].join("\n").toLowerCase();
|
|
715
729
|
return haystack.includes(search);
|
|
716
730
|
}
|
|
@@ -799,12 +813,12 @@
|
|
|
799
813
|
}));
|
|
800
814
|
|
|
801
815
|
const messageItems = (this.messages || [])
|
|
802
|
-
.filter(
|
|
816
|
+
.filter(isClaimableMessageWaiting)
|
|
803
817
|
.map((msg) => ({
|
|
804
818
|
id: "message-" + msg.id,
|
|
805
819
|
sourceType: "message",
|
|
806
820
|
title: msg.subject || "Claimable message #" + msg.id,
|
|
807
|
-
body: msg
|
|
821
|
+
body: messageBody(msg),
|
|
808
822
|
severity: "warning",
|
|
809
823
|
status: msg.claimedBy ? "claimed" : "open",
|
|
810
824
|
owner: msg.claimedBy || "",
|
|
@@ -852,7 +866,7 @@
|
|
|
852
866
|
return (vm.messages || []).flatMap((msg) => {
|
|
853
867
|
const ts = toTimestamp(msg.createdAt);
|
|
854
868
|
const items = [];
|
|
855
|
-
const pairEvent = msg.
|
|
869
|
+
const pairEvent = msg.payload?.pairEvent;
|
|
856
870
|
if (pairEvent) {
|
|
857
871
|
items.push(activityItem({
|
|
858
872
|
id: "pair-message-" + msg.id,
|
|
@@ -868,10 +882,10 @@
|
|
|
868
882
|
} else if (msg.from === HUMAN_AGENT_ID) {
|
|
869
883
|
items.push(activityItem({
|
|
870
884
|
id: "human-send-" + msg.id,
|
|
871
|
-
kind: msg.
|
|
885
|
+
kind: msg.kind === "task" ? "task" : "message",
|
|
872
886
|
ts,
|
|
873
|
-
icon: msg.
|
|
874
|
-
title: msg.
|
|
887
|
+
icon: msg.kind === "task" ? "ti-hand-grab" : "ti-send",
|
|
888
|
+
title: msg.kind === "task" ? "Claimable task sent" : "Message sent",
|
|
875
889
|
body: vm.messagePreview(msg),
|
|
876
890
|
meta: "to " + vm.displayTarget(msg.to),
|
|
877
891
|
view: inboxPeer(msg) ? "inbox" : "messages",
|
|
@@ -1049,7 +1063,7 @@
|
|
|
1049
1063
|
}
|
|
1050
1064
|
|
|
1051
1065
|
function messageLooksLikeQuestion(msg) {
|
|
1052
|
-
return /\?/.test(`${msg.subject || ""}\n${msg
|
|
1066
|
+
return /\?/.test(`${msg.subject || ""}\n${messageBody(msg)}`);
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
1055
1069
|
function countClaimableWaiting(vm) {
|
|
@@ -1069,7 +1083,7 @@
|
|
|
1069
1083
|
}
|
|
1070
1084
|
|
|
1071
1085
|
function isClaimableMessageWaiting(msg) {
|
|
1072
|
-
return Boolean(msg.claimable && !msg.claimedBy);
|
|
1086
|
+
return Boolean(msg.claimable && !msg.claimedBy && !(msg.kind === "task" && Number.isSafeInteger(msg.payload?.taskId)));
|
|
1073
1087
|
}
|
|
1074
1088
|
|
|
1075
1089
|
function targetMatchesAgent(target, agent) {
|
|
@@ -1133,6 +1147,11 @@
|
|
|
1133
1147
|
|
|
1134
1148
|
function getChannelCards() {
|
|
1135
1149
|
return [...(this.channels || [])].sort((a, b) => {
|
|
1150
|
+
const healthRank = { error: 0, warning: 1, ok: 2 };
|
|
1151
|
+
const aHealth = a.targetHealth?.status || "ok";
|
|
1152
|
+
const bHealth = b.targetHealth?.status || "ok";
|
|
1153
|
+
const healthDiff = (healthRank[aHealth] ?? 2) - (healthRank[bHealth] ?? 2);
|
|
1154
|
+
if (healthDiff !== 0) return healthDiff;
|
|
1136
1155
|
const readyDiff = Number(Boolean(b.ready)) - Number(Boolean(a.ready));
|
|
1137
1156
|
if (readyDiff !== 0) return readyDiff;
|
|
1138
1157
|
const statusDiff = String(a.status || "").localeCompare(String(b.status || ""));
|
|
@@ -1141,6 +1160,17 @@
|
|
|
1141
1160
|
});
|
|
1142
1161
|
}
|
|
1143
1162
|
|
|
1163
|
+
function getConnectorCards() {
|
|
1164
|
+
return [...(this.connectors || [])].sort((a, b) => {
|
|
1165
|
+
const statusRank = { error: 0, warn: 1, unknown: 2, ok: 3 };
|
|
1166
|
+
const aStatus = a.runtime?.status || "unknown";
|
|
1167
|
+
const bStatus = b.runtime?.status || "unknown";
|
|
1168
|
+
const statusDiff = (statusRank[aStatus] ?? 2) - (statusRank[bStatus] ?? 2);
|
|
1169
|
+
if (statusDiff !== 0) return statusDiff;
|
|
1170
|
+
return String(a.displayName || a.id || "").localeCompare(String(b.displayName || b.id || ""));
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1144
1174
|
function getOpenPairCount() {
|
|
1145
1175
|
return (this.pairs || []).filter(isOpenPair).length;
|
|
1146
1176
|
}
|
|
@@ -1204,7 +1234,7 @@
|
|
|
1204
1234
|
}
|
|
1205
1235
|
|
|
1206
1236
|
function isChannelAgent(agent) {
|
|
1207
|
-
return agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
|
|
1237
|
+
return agent?.kind === "channel" || agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
|
|
1208
1238
|
}
|
|
1209
1239
|
|
|
1210
1240
|
function agentSupportsControlActions(agent) {
|
|
@@ -1265,6 +1295,11 @@
|
|
|
1265
1295
|
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" },
|
|
1266
1296
|
{ label: "Show stale", icon: "ti-filter", view: "agents", preset: "offline_stale" }
|
|
1267
1297
|
);
|
|
1298
|
+
} else if (check.name === "channel-delivery-targets") {
|
|
1299
|
+
base.actions.unshift(
|
|
1300
|
+
{ label: "Open channels", icon: "ti-messages", view: "channels" },
|
|
1301
|
+
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" }
|
|
1302
|
+
);
|
|
1268
1303
|
} else if (check.name === "expired-message-claims" || check.name === "expired-task-claims" || check.name === "offline-claimed-tasks") {
|
|
1269
1304
|
base.actions.unshift(
|
|
1270
1305
|
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" },
|
|
@@ -1281,6 +1316,7 @@
|
|
|
1281
1316
|
if (check.name === "expired-message-claims") return "Claimable messages may be stuck until the reaper releases expired claims.";
|
|
1282
1317
|
if (check.name === "expired-task-claims") return "Tasks can appear owned by agents that no longer hold a live lease.";
|
|
1283
1318
|
if (check.name === "offline-claimed-tasks") return "Offline agents are still shown as owners for active work.";
|
|
1319
|
+
if (check.name === "channel-delivery-targets") return "Inbound channel messages may be accepted but routed to no live delivery agent.";
|
|
1284
1320
|
return "Relay health is degraded for this check.";
|
|
1285
1321
|
}
|
|
1286
1322
|
|
|
@@ -1343,6 +1379,7 @@
|
|
|
1343
1379
|
displayName,
|
|
1344
1380
|
displayTarget,
|
|
1345
1381
|
conversationTitle,
|
|
1382
|
+
messageBody,
|
|
1346
1383
|
messagePreview,
|
|
1347
1384
|
agentPair,
|
|
1348
1385
|
pairPeerId,
|
|
@@ -1363,6 +1400,7 @@
|
|
|
1363
1400
|
isBuiltInAgent,
|
|
1364
1401
|
agentChannels,
|
|
1365
1402
|
channelPresence,
|
|
1403
|
+
connectorPresence,
|
|
1366
1404
|
integrationPresence,
|
|
1367
1405
|
composeTargetAllowsClaimable,
|
|
1368
1406
|
inboxComposeTargetAllowsClaimable,
|
|
@@ -1396,10 +1434,43 @@
|
|
|
1396
1434
|
}
|
|
1397
1435
|
|
|
1398
1436
|
function messagePreview(msg) {
|
|
1399
|
-
const text = msg?.subject || msg
|
|
1437
|
+
const text = msg?.subject || messageBody(msg) || "";
|
|
1400
1438
|
return text.length > 90 ? text.slice(0, 90) + "..." : text;
|
|
1401
1439
|
}
|
|
1402
1440
|
|
|
1441
|
+
function messageBody(msg) {
|
|
1442
|
+
if (!msg) return "";
|
|
1443
|
+
const payload = msg.payload || {};
|
|
1444
|
+
const channelMessage = payload.message;
|
|
1445
|
+
if (channelMessage && typeof channelMessage === "object" && typeof channelMessage.text === "string" && channelMessage.text.trim()) {
|
|
1446
|
+
return channelMessage.text;
|
|
1447
|
+
}
|
|
1448
|
+
const interaction = payload.interaction;
|
|
1449
|
+
if (interaction && typeof interaction === "object") {
|
|
1450
|
+
const title = typeof interaction.title === "string" ? interaction.title.trim() : "";
|
|
1451
|
+
const description = typeof interaction.description === "string" ? interaction.description.trim() : "";
|
|
1452
|
+
if (title && description) return title + "\n" + description;
|
|
1453
|
+
if (title) return title;
|
|
1454
|
+
if (description) return description;
|
|
1455
|
+
}
|
|
1456
|
+
const reaction = payload.reaction;
|
|
1457
|
+
if (reaction && typeof reaction === "object") {
|
|
1458
|
+
const name = typeof reaction.name === "string" ? reaction.name : "";
|
|
1459
|
+
const emoji = typeof reaction.emoji === "string" ? reaction.emoji : "";
|
|
1460
|
+
const value = typeof reaction.value === "string" ? reaction.value : "";
|
|
1461
|
+
return ["Reaction", emoji || name || value].filter(Boolean).join(": ");
|
|
1462
|
+
}
|
|
1463
|
+
const activity = payload.activity;
|
|
1464
|
+
if (activity && typeof activity === "object") {
|
|
1465
|
+
const kind = typeof activity.kind === "string" ? activity.kind : "activity";
|
|
1466
|
+
const state = typeof activity.state === "string" ? activity.state : "";
|
|
1467
|
+
return [kind, state].filter(Boolean).join(" ");
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof payload.text === "string" && payload.text.trim()) return payload.text;
|
|
1470
|
+
if (typeof payload.message === "string" && payload.message.trim()) return payload.message;
|
|
1471
|
+
return msg.body || "";
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1403
1474
|
function agentPair(agent) {
|
|
1404
1475
|
return agent ? this.pairsByAgentId[agent.id] : null;
|
|
1405
1476
|
}
|
|
@@ -1583,6 +1654,16 @@
|
|
|
1583
1654
|
return { label: "configured", tone: "primary", icon: "ti-plug-connected" };
|
|
1584
1655
|
}
|
|
1585
1656
|
|
|
1657
|
+
function connectorPresence(connector) {
|
|
1658
|
+
const runtime = connector?.runtime || {};
|
|
1659
|
+
if (runtime.status === "error") return { label: "error", tone: "danger", icon: "ti-alert-triangle" };
|
|
1660
|
+
if (runtime.status === "warn") return { label: "warning", tone: "warning", icon: "ti-alert-circle" };
|
|
1661
|
+
if (runtime.running) return { label: "running", tone: "success", icon: "ti-circle-check" };
|
|
1662
|
+
if (runtime.enabled === false) return { label: "disabled", tone: "secondary", icon: "ti-player-pause" };
|
|
1663
|
+
if (runtime.status === "ok") return { label: "ok", tone: "success", icon: "ti-circle-check" };
|
|
1664
|
+
return { label: "unknown", tone: "secondary", icon: "ti-help-circle" };
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1586
1667
|
function agentChannels(agent) {
|
|
1587
1668
|
if (!agent) return [];
|
|
1588
1669
|
return (this.channels || []).filter((ch) => ch.agentId === agent.id);
|
|
@@ -1590,6 +1671,8 @@
|
|
|
1590
1671
|
|
|
1591
1672
|
function channelPresence(channel) {
|
|
1592
1673
|
if (!channel) return { label: "unknown", tone: "secondary", icon: "ti-plug-off" };
|
|
1674
|
+
if (channel.targetHealth?.status === "error") return { label: "target broken", tone: "danger", icon: "ti-alert-triangle" };
|
|
1675
|
+
if (channel.targetHealth?.status === "warning") return { label: "target warning", tone: "warning", icon: "ti-alert-circle" };
|
|
1593
1676
|
if (channel.status === "offline") return { label: "offline", tone: "secondary", icon: "ti-plug-off" };
|
|
1594
1677
|
if (!channel.ready) return { label: "not ready", tone: "warning", icon: "ti-loader" };
|
|
1595
1678
|
if (channel.status === "busy") return { label: "busy", tone: "warning", icon: "ti-activity" };
|
|
@@ -1675,6 +1758,7 @@
|
|
|
1675
1758
|
recordOperatorActivity,
|
|
1676
1759
|
openActivityItem,
|
|
1677
1760
|
runHealthAction,
|
|
1761
|
+
runConnectorAction,
|
|
1678
1762
|
openCommandPalette,
|
|
1679
1763
|
closeCommandPalette,
|
|
1680
1764
|
runCommand,
|
|
@@ -1940,7 +2024,11 @@
|
|
|
1940
2024
|
};
|
|
1941
2025
|
if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
|
|
1942
2026
|
if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
|
|
1943
|
-
if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this))
|
|
2027
|
+
if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this)) {
|
|
2028
|
+
payload.claimable = true;
|
|
2029
|
+
payload.kind = "task";
|
|
2030
|
+
payload.payload = { title: this.inboxCompose.subject || "Claimable task" };
|
|
2031
|
+
}
|
|
1944
2032
|
await this.api("POST", "/messages", payload);
|
|
1945
2033
|
this.recordOperatorActivity({
|
|
1946
2034
|
title: payload.claimable ? "Claimable task sent" : "Message sent",
|
|
@@ -1983,7 +2071,11 @@
|
|
|
1983
2071
|
if (vm.compose.channel) payload.channel = vm.compose.channel;
|
|
1984
2072
|
if (vm.compose.subject) payload.subject = vm.compose.subject;
|
|
1985
2073
|
if (vm.replyTo) payload.replyTo = vm.replyTo.id;
|
|
1986
|
-
if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm))
|
|
2074
|
+
if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm)) {
|
|
2075
|
+
payload.claimable = true;
|
|
2076
|
+
payload.kind = "task";
|
|
2077
|
+
payload.payload = { title: vm.compose.subject || "Claimable task" };
|
|
2078
|
+
}
|
|
1987
2079
|
return payload;
|
|
1988
2080
|
}
|
|
1989
2081
|
|
|
@@ -2200,6 +2292,16 @@
|
|
|
2200
2292
|
if (action.copy) await copyText(action.copy);
|
|
2201
2293
|
}
|
|
2202
2294
|
|
|
2295
|
+
async function runConnectorAction(connector, action) {
|
|
2296
|
+
if (!connector || !action) return;
|
|
2297
|
+
try {
|
|
2298
|
+
await this.api("POST", "/connectors/" + encodeURIComponent(connector.id) + "/actions", { action });
|
|
2299
|
+
await this.fetchConnectors();
|
|
2300
|
+
} catch (e) {
|
|
2301
|
+
alert("Connector action failed: " + e.message);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2203
2305
|
async function copyText(value) {
|
|
2204
2306
|
if (typeof navigator === "undefined") return;
|
|
2205
2307
|
try {
|
|
@@ -2289,8 +2391,8 @@
|
|
|
2289
2391
|
|
|
2290
2392
|
function pairMessages(vm, pair) {
|
|
2291
2393
|
return (vm.messages || []).filter((msg) =>
|
|
2292
|
-
msg.
|
|
2293
|
-
(msg.
|
|
2394
|
+
msg.payload?.pairId === pair.id ||
|
|
2395
|
+
(msg.payload?.pairEvent && [pair.requesterId, pair.targetId].includes(msg.from) && [pair.requesterId, pair.targetId].includes(msg.to))
|
|
2294
2396
|
);
|
|
2295
2397
|
}
|
|
2296
2398
|
|
|
@@ -2343,7 +2445,7 @@
|
|
|
2343
2445
|
if (msg.channel) lines.push("- Channel: " + msg.channel);
|
|
2344
2446
|
if (msg.subject) lines.push("- Subject: " + msg.subject);
|
|
2345
2447
|
if (msg.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(msg.claimedBy));
|
|
2346
|
-
lines.push("",
|
|
2448
|
+
lines.push("", messageBody.call(vm, msg) || "", "");
|
|
2347
2449
|
}
|
|
2348
2450
|
}
|
|
2349
2451
|
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
2
|
+
<rect width="512" height="512" rx="96" fill="#0d1117"/>
|
|
3
|
+
<g stroke="#30363d" stroke-width="22" stroke-linecap="round">
|
|
4
|
+
<line x1="137" y1="151" x2="221" y2="229"/>
|
|
5
|
+
<line x1="375" y1="151" x2="291" y2="229"/>
|
|
6
|
+
<line x1="137" y1="361" x2="221" y2="283"/>
|
|
7
|
+
<line x1="375" y1="361" x2="291" y2="283"/>
|
|
8
|
+
</g>
|
|
9
|
+
<circle cx="256" cy="256" r="72" fill="#58a6ff"/>
|
|
10
|
+
<circle cx="96" cy="116" r="42" fill="#3fb950"/>
|
|
11
|
+
<circle cx="416" cy="116" r="42" fill="#3fb950"/>
|
|
12
|
+
<circle cx="96" cy="396" r="42" fill="#3fb950"/>
|
|
13
|
+
<circle cx="416" cy="396" r="42" fill="#3fb950"/>
|
|
14
|
+
</svg>
|
package/public/index.html
CHANGED
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="theme-color" content="#0d1117">
|
|
7
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Agent Relay">
|
|
10
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
6
11
|
<title>Agent Relay</title>
|
|
7
12
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230d1117'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
|
|
13
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
14
|
+
<link rel="apple-touch-icon" href="/icons/agent-relay-192.png">
|
|
8
15
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css">
|
|
9
16
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
17
|
<style>
|
|
@@ -220,6 +227,10 @@
|
|
|
220
227
|
<i class="ti ti-messages"></i>Channels
|
|
221
228
|
<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>
|
|
222
229
|
</a>
|
|
230
|
+
<a href="#" class="nav-link" :class="{ active: view === 'connectors' }" @click.prevent="switchView('connectors')">
|
|
231
|
+
<i class="ti ti-plug"></i>Connectors
|
|
232
|
+
<span class="badge bg-secondary text-white ms-auto" x-show="connectorCards.length > 0" x-text="connectorCards.length"></span>
|
|
233
|
+
</a>
|
|
223
234
|
<a href="#" class="nav-link" :class="{ active: view === 'integrations' }" @click.prevent="switchView('integrations')">
|
|
224
235
|
<i class="ti ti-plug-connected"></i>Integrations
|
|
225
236
|
<span class="ms-auto d-inline-flex gap-1 align-items-center">
|
|
@@ -292,7 +303,7 @@
|
|
|
292
303
|
|
|
293
304
|
<!-- Mobile nav -->
|
|
294
305
|
<div class="mobile-nav d-none border-bottom p-2 gap-1 position-fixed top-0 w-100 bg-dark" style="z-index:50">
|
|
295
|
-
<template x-for="v in ['overview','agents','channels','integrations','inbox','activity','pairs','messages','work','tasks','analytics']">
|
|
306
|
+
<template x-for="v in ['overview','agents','channels','connectors','integrations','inbox','activity','pairs','messages','work','tasks','analytics']">
|
|
296
307
|
<button class="btn btn-sm" :class="view === v ? 'btn-primary' : 'btn-ghost-secondary'" @click="switchView(v)" x-text="v.charAt(0).toUpperCase() + v.slice(1)"></button>
|
|
297
308
|
</template>
|
|
298
309
|
</div>
|
|
@@ -328,8 +339,8 @@
|
|
|
328
339
|
<div class="card-body">
|
|
329
340
|
<div class="d-flex align-items-center">
|
|
330
341
|
<div>
|
|
331
|
-
<div class="text-secondary small">
|
|
332
|
-
<div class="h1 mb-0" x-text="
|
|
342
|
+
<div class="text-secondary small">Agents</div>
|
|
343
|
+
<div class="h1 mb-0" x-text="onlineCount"></div>
|
|
333
344
|
</div>
|
|
334
345
|
<i class="ti ti-robot ms-auto stat-card"></i>
|
|
335
346
|
</div>
|
|
@@ -341,10 +352,10 @@
|
|
|
341
352
|
<div class="card-body">
|
|
342
353
|
<div class="d-flex align-items-center">
|
|
343
354
|
<div>
|
|
344
|
-
<div class="text-secondary small">
|
|
345
|
-
<div class="h1 mb-0 text-
|
|
355
|
+
<div class="text-secondary small">Busy</div>
|
|
356
|
+
<div class="h1 mb-0 text-warning" x-text="busyAgentCount"></div>
|
|
346
357
|
</div>
|
|
347
|
-
<i class="ti ti-
|
|
358
|
+
<i class="ti ti-activity ms-auto stat-card"></i>
|
|
348
359
|
</div>
|
|
349
360
|
</div>
|
|
350
361
|
</div>
|
|
@@ -848,6 +859,8 @@ agent-relay-orchestrator</pre>
|
|
|
848
859
|
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
849
860
|
<h2 class="page-title mb-0">Channels</h2>
|
|
850
861
|
<span class="badge bg-success-lt" x-show="channelCards.filter((item) => item.ready).length > 0" x-text="channelCards.filter((item) => item.ready).length + ' ready'"></span>
|
|
862
|
+
<span class="badge bg-danger-lt" x-show="channelCards.filter((item) => item.targetHealth?.status === 'error').length > 0" x-text="channelCards.filter((item) => item.targetHealth?.status === 'error').length + ' target issue' + (channelCards.filter((item) => item.targetHealth?.status === 'error').length === 1 ? '' : 's')"></span>
|
|
863
|
+
<span class="badge bg-warning-lt" x-show="channelCards.filter((item) => item.targetHealth?.status === 'warning').length > 0" x-text="channelCards.filter((item) => item.targetHealth?.status === 'warning').length + ' target warning' + (channelCards.filter((item) => item.targetHealth?.status === 'warning').length === 1 ? '' : 's')"></span>
|
|
851
864
|
<div class="ms-auto d-flex gap-2 align-items-center">
|
|
852
865
|
<button class="btn btn-sm btn-ghost-secondary" @click="fetchChannels()">
|
|
853
866
|
<i class="ti ti-refresh"></i>
|
|
@@ -876,6 +889,12 @@ agent-relay-orchestrator</pre>
|
|
|
876
889
|
<span class="badge bg-cyan-lt" x-text="channel.direction"></span>
|
|
877
890
|
<span class="badge bg-secondary-lt" x-text="displayTarget(channel.target || channel.agentId)"></span>
|
|
878
891
|
</div>
|
|
892
|
+
<div class="alert py-2 px-2 mt-2 mb-0"
|
|
893
|
+
:class="channel.targetHealth?.status === 'error' ? 'alert-danger' : 'alert-warning'"
|
|
894
|
+
x-show="channel.targetHealth && channel.targetHealth.status !== 'ok'">
|
|
895
|
+
<i class="ti me-1" :class="channel.targetHealth?.status === 'error' ? 'ti-alert-triangle' : 'ti-alert-circle'"></i>
|
|
896
|
+
<span x-text="channel.targetHealth?.detail"></span>
|
|
897
|
+
</div>
|
|
879
898
|
<div class="d-flex gap-1 mt-2 flex-wrap" x-show="channel.capabilities?.length">
|
|
880
899
|
<template x-for="capability in (channel.capabilities || [])" :key="capability">
|
|
881
900
|
<span class="badge bg-secondary-lt" x-text="capability"></span>
|
|
@@ -911,6 +930,75 @@ agent-relay-orchestrator</pre>
|
|
|
911
930
|
</template>
|
|
912
931
|
</div>
|
|
913
932
|
|
|
933
|
+
<!-- ==================== CONNECTORS ==================== -->
|
|
934
|
+
<div x-show="view === 'connectors'" x-cloak class="fade-in">
|
|
935
|
+
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
936
|
+
<h2 class="page-title mb-0">Connectors</h2>
|
|
937
|
+
<span class="badge bg-secondary-lt" x-show="connectorCards.length > 0" x-text="connectorCards.length + ' registered'"></span>
|
|
938
|
+
<div class="ms-auto">
|
|
939
|
+
<button class="btn btn-sm btn-ghost-secondary" @click="fetchConnectors()" title="Refresh">
|
|
940
|
+
<i class="ti ti-refresh"></i>
|
|
941
|
+
</button>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<div class="row g-3">
|
|
946
|
+
<template x-for="connector in connectorCards" :key="connector.id">
|
|
947
|
+
<div class="col-md-6 col-xl-4">
|
|
948
|
+
<div class="card">
|
|
949
|
+
<div class="card-body">
|
|
950
|
+
<div class="d-flex align-items-start gap-2">
|
|
951
|
+
<span class="agent-type-icon agent mt-0">
|
|
952
|
+
<i class="ti" :class="connectorPresence(connector).icon"></i>
|
|
953
|
+
</span>
|
|
954
|
+
<div class="flex-grow-1 min-width-0">
|
|
955
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
956
|
+
<span class="fw-bold text-truncate" x-text="connector.displayName || connector.id"></span>
|
|
957
|
+
<span class="badge" :class="'bg-' + connectorPresence(connector).tone + '-lt'">
|
|
958
|
+
<i class="ti me-1" :class="connectorPresence(connector).icon"></i><span x-text="connectorPresence(connector).label"></span>
|
|
959
|
+
</span>
|
|
960
|
+
<span class="badge bg-secondary-lt" x-text="connector.kind"></span>
|
|
961
|
+
</div>
|
|
962
|
+
<div class="text-secondary small mt-1" x-text="connector.description || connector.packageName || connector.binary"></div>
|
|
963
|
+
<div class="d-flex gap-1 mt-2 flex-wrap">
|
|
964
|
+
<template x-for="capability in (connector.capabilities || [])" :key="capability">
|
|
965
|
+
<span class="badge bg-cyan-lt" x-text="capability"></span>
|
|
966
|
+
</template>
|
|
967
|
+
</div>
|
|
968
|
+
<div class="text-secondary small mt-2 d-flex gap-2 flex-wrap">
|
|
969
|
+
<span x-text="'v' + connector.version"></span>
|
|
970
|
+
<span x-show="connector.runtime?.detail" x-text="connector.runtime.detail"></span>
|
|
971
|
+
<span x-show="connector.runtime?.updatedAt" x-text="'Updated ' + timeAgo(connector.runtime.updatedAt)"></span>
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
<div class="d-flex gap-1 mt-3 flex-wrap">
|
|
976
|
+
<template x-for="action in ['status','doctor','start','stop','restart','enable','disable']" :key="connector.id + action">
|
|
977
|
+
<button
|
|
978
|
+
class="btn btn-sm btn-ghost-secondary"
|
|
979
|
+
x-show="connector.manifest?.commands?.[action]"
|
|
980
|
+
@click="runConnectorAction(connector, action)"
|
|
981
|
+
:title="action.charAt(0).toUpperCase() + action.slice(1)">
|
|
982
|
+
<i class="ti" :class="action === 'doctor' ? 'ti-stethoscope' : action === 'status' ? 'ti-activity' : action === 'start' ? 'ti-player-play' : action === 'stop' ? 'ti-player-stop' : action === 'restart' ? 'ti-refresh' : action === 'enable' ? 'ti-toggle-right' : 'ti-toggle-left'"></i>
|
|
983
|
+
</button>
|
|
984
|
+
</template>
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
</div>
|
|
988
|
+
</div>
|
|
989
|
+
</template>
|
|
990
|
+
</div>
|
|
991
|
+
|
|
992
|
+
<template x-if="connectorCards.length === 0">
|
|
993
|
+
<div class="card">
|
|
994
|
+
<div class="card-body text-center text-secondary py-5">
|
|
995
|
+
<i class="ti ti-plug-off" style="font-size:48px; opacity:0.3"></i>
|
|
996
|
+
<p class="mt-2">No connectors registered</p>
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
</template>
|
|
1000
|
+
</div>
|
|
1001
|
+
|
|
914
1002
|
<!-- ==================== INTEGRATIONS ==================== -->
|
|
915
1003
|
<div x-show="view === 'integrations'" x-cloak class="fade-in">
|
|
916
1004
|
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
@@ -1164,7 +1252,7 @@ agent-relay-orchestrator</pre>
|
|
|
1164
1252
|
<template x-if="m.subject">
|
|
1165
1253
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
1166
1254
|
</template>
|
|
1167
|
-
<div class="msg-body" x-text="m
|
|
1255
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
1168
1256
|
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap">
|
|
1169
1257
|
<button class="btn btn-sm btn-ghost-primary py-0 px-1" @click="startReply(m)">
|
|
1170
1258
|
<i class="ti ti-corner-up-left" style="font-size:14px"></i> Reply
|
|
@@ -1419,7 +1507,7 @@ agent-relay-orchestrator</pre>
|
|
|
1419
1507
|
<template x-if="m.subject">
|
|
1420
1508
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
1421
1509
|
</template>
|
|
1422
|
-
<div class="msg-body" x-text="m
|
|
1510
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
1423
1511
|
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap">
|
|
1424
1512
|
<button class="btn btn-sm btn-ghost-primary py-0 px-1" @click="startReply(m)">
|
|
1425
1513
|
<i class="ti ti-corner-up-left" style="font-size:14px"></i> Reply
|
|
@@ -2388,7 +2476,7 @@ agent-relay-orchestrator</pre>
|
|
|
2388
2476
|
<template x-if="m.subject">
|
|
2389
2477
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
2390
2478
|
</template>
|
|
2391
|
-
<div class="msg-body" x-text="m
|
|
2479
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
2392
2480
|
</div>
|
|
2393
2481
|
</template>
|
|
2394
2482
|
<template x-if="threadMessages.length === 0">
|
|
@@ -2481,6 +2569,13 @@ agent-relay-orchestrator</pre>
|
|
|
2481
2569
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@latest/dist/apexcharts.min.js"></script>
|
|
2482
2570
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
|
2483
2571
|
<script src="dashboard.js"></script>
|
|
2572
|
+
<script>
|
|
2573
|
+
if ("serviceWorker" in navigator) {
|
|
2574
|
+
window.addEventListener("load", () => {
|
|
2575
|
+
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
</script>
|
|
2484
2579
|
|
|
2485
2580
|
</body>
|
|
2486
2581
|
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Agent Relay",
|
|
3
|
+
"short_name": "Relay",
|
|
4
|
+
"description": "Local control panel for Agent Relay agents, channels, tasks, and messages.",
|
|
5
|
+
"id": "/",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#0d1117",
|
|
10
|
+
"theme_color": "#0d1117",
|
|
11
|
+
"orientation": "any",
|
|
12
|
+
"categories": ["developer", "productivity", "utilities"],
|
|
13
|
+
"icons": [
|
|
14
|
+
{
|
|
15
|
+
"src": "/icons/agent-relay.svg",
|
|
16
|
+
"sizes": "any",
|
|
17
|
+
"type": "image/svg+xml",
|
|
18
|
+
"purpose": "any maskable"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"src": "/icons/agent-relay-192.png",
|
|
22
|
+
"sizes": "192x192",
|
|
23
|
+
"type": "image/png",
|
|
24
|
+
"purpose": "any maskable"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"src": "/icons/agent-relay-512.png",
|
|
28
|
+
"sizes": "512x512",
|
|
29
|
+
"type": "image/png",
|
|
30
|
+
"purpose": "any maskable"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|