agent-relay-server 0.5.0 → 0.6.0
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 +106 -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 +91 -4
- 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 +413 -25
- 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 +92 -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) {
|
|
@@ -1141,6 +1155,17 @@
|
|
|
1141
1155
|
});
|
|
1142
1156
|
}
|
|
1143
1157
|
|
|
1158
|
+
function getConnectorCards() {
|
|
1159
|
+
return [...(this.connectors || [])].sort((a, b) => {
|
|
1160
|
+
const statusRank = { error: 0, warn: 1, unknown: 2, ok: 3 };
|
|
1161
|
+
const aStatus = a.runtime?.status || "unknown";
|
|
1162
|
+
const bStatus = b.runtime?.status || "unknown";
|
|
1163
|
+
const statusDiff = (statusRank[aStatus] ?? 2) - (statusRank[bStatus] ?? 2);
|
|
1164
|
+
if (statusDiff !== 0) return statusDiff;
|
|
1165
|
+
return String(a.displayName || a.id || "").localeCompare(String(b.displayName || b.id || ""));
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1144
1169
|
function getOpenPairCount() {
|
|
1145
1170
|
return (this.pairs || []).filter(isOpenPair).length;
|
|
1146
1171
|
}
|
|
@@ -1204,7 +1229,7 @@
|
|
|
1204
1229
|
}
|
|
1205
1230
|
|
|
1206
1231
|
function isChannelAgent(agent) {
|
|
1207
|
-
return agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
|
|
1232
|
+
return agent?.kind === "channel" || agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
|
|
1208
1233
|
}
|
|
1209
1234
|
|
|
1210
1235
|
function agentSupportsControlActions(agent) {
|
|
@@ -1343,6 +1368,7 @@
|
|
|
1343
1368
|
displayName,
|
|
1344
1369
|
displayTarget,
|
|
1345
1370
|
conversationTitle,
|
|
1371
|
+
messageBody,
|
|
1346
1372
|
messagePreview,
|
|
1347
1373
|
agentPair,
|
|
1348
1374
|
pairPeerId,
|
|
@@ -1363,6 +1389,7 @@
|
|
|
1363
1389
|
isBuiltInAgent,
|
|
1364
1390
|
agentChannels,
|
|
1365
1391
|
channelPresence,
|
|
1392
|
+
connectorPresence,
|
|
1366
1393
|
integrationPresence,
|
|
1367
1394
|
composeTargetAllowsClaimable,
|
|
1368
1395
|
inboxComposeTargetAllowsClaimable,
|
|
@@ -1396,10 +1423,43 @@
|
|
|
1396
1423
|
}
|
|
1397
1424
|
|
|
1398
1425
|
function messagePreview(msg) {
|
|
1399
|
-
const text = msg?.subject || msg
|
|
1426
|
+
const text = msg?.subject || messageBody(msg) || "";
|
|
1400
1427
|
return text.length > 90 ? text.slice(0, 90) + "..." : text;
|
|
1401
1428
|
}
|
|
1402
1429
|
|
|
1430
|
+
function messageBody(msg) {
|
|
1431
|
+
if (!msg) return "";
|
|
1432
|
+
const payload = msg.payload || {};
|
|
1433
|
+
const channelMessage = payload.message;
|
|
1434
|
+
if (channelMessage && typeof channelMessage === "object" && typeof channelMessage.text === "string" && channelMessage.text.trim()) {
|
|
1435
|
+
return channelMessage.text;
|
|
1436
|
+
}
|
|
1437
|
+
const interaction = payload.interaction;
|
|
1438
|
+
if (interaction && typeof interaction === "object") {
|
|
1439
|
+
const title = typeof interaction.title === "string" ? interaction.title.trim() : "";
|
|
1440
|
+
const description = typeof interaction.description === "string" ? interaction.description.trim() : "";
|
|
1441
|
+
if (title && description) return title + "\n" + description;
|
|
1442
|
+
if (title) return title;
|
|
1443
|
+
if (description) return description;
|
|
1444
|
+
}
|
|
1445
|
+
const reaction = payload.reaction;
|
|
1446
|
+
if (reaction && typeof reaction === "object") {
|
|
1447
|
+
const name = typeof reaction.name === "string" ? reaction.name : "";
|
|
1448
|
+
const emoji = typeof reaction.emoji === "string" ? reaction.emoji : "";
|
|
1449
|
+
const value = typeof reaction.value === "string" ? reaction.value : "";
|
|
1450
|
+
return ["Reaction", emoji || name || value].filter(Boolean).join(": ");
|
|
1451
|
+
}
|
|
1452
|
+
const activity = payload.activity;
|
|
1453
|
+
if (activity && typeof activity === "object") {
|
|
1454
|
+
const kind = typeof activity.kind === "string" ? activity.kind : "activity";
|
|
1455
|
+
const state = typeof activity.state === "string" ? activity.state : "";
|
|
1456
|
+
return [kind, state].filter(Boolean).join(" ");
|
|
1457
|
+
}
|
|
1458
|
+
if (typeof payload.text === "string" && payload.text.trim()) return payload.text;
|
|
1459
|
+
if (typeof payload.message === "string" && payload.message.trim()) return payload.message;
|
|
1460
|
+
return msg.body || "";
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1403
1463
|
function agentPair(agent) {
|
|
1404
1464
|
return agent ? this.pairsByAgentId[agent.id] : null;
|
|
1405
1465
|
}
|
|
@@ -1583,6 +1643,16 @@
|
|
|
1583
1643
|
return { label: "configured", tone: "primary", icon: "ti-plug-connected" };
|
|
1584
1644
|
}
|
|
1585
1645
|
|
|
1646
|
+
function connectorPresence(connector) {
|
|
1647
|
+
const runtime = connector?.runtime || {};
|
|
1648
|
+
if (runtime.status === "error") return { label: "error", tone: "danger", icon: "ti-alert-triangle" };
|
|
1649
|
+
if (runtime.status === "warn") return { label: "warning", tone: "warning", icon: "ti-alert-circle" };
|
|
1650
|
+
if (runtime.running) return { label: "running", tone: "success", icon: "ti-circle-check" };
|
|
1651
|
+
if (runtime.enabled === false) return { label: "disabled", tone: "secondary", icon: "ti-player-pause" };
|
|
1652
|
+
if (runtime.status === "ok") return { label: "ok", tone: "success", icon: "ti-circle-check" };
|
|
1653
|
+
return { label: "unknown", tone: "secondary", icon: "ti-help-circle" };
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1586
1656
|
function agentChannels(agent) {
|
|
1587
1657
|
if (!agent) return [];
|
|
1588
1658
|
return (this.channels || []).filter((ch) => ch.agentId === agent.id);
|
|
@@ -1675,6 +1745,7 @@
|
|
|
1675
1745
|
recordOperatorActivity,
|
|
1676
1746
|
openActivityItem,
|
|
1677
1747
|
runHealthAction,
|
|
1748
|
+
runConnectorAction,
|
|
1678
1749
|
openCommandPalette,
|
|
1679
1750
|
closeCommandPalette,
|
|
1680
1751
|
runCommand,
|
|
@@ -1940,7 +2011,11 @@
|
|
|
1940
2011
|
};
|
|
1941
2012
|
if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
|
|
1942
2013
|
if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
|
|
1943
|
-
if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this))
|
|
2014
|
+
if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this)) {
|
|
2015
|
+
payload.claimable = true;
|
|
2016
|
+
payload.kind = "task";
|
|
2017
|
+
payload.payload = { title: this.inboxCompose.subject || "Claimable task" };
|
|
2018
|
+
}
|
|
1944
2019
|
await this.api("POST", "/messages", payload);
|
|
1945
2020
|
this.recordOperatorActivity({
|
|
1946
2021
|
title: payload.claimable ? "Claimable task sent" : "Message sent",
|
|
@@ -1983,7 +2058,11 @@
|
|
|
1983
2058
|
if (vm.compose.channel) payload.channel = vm.compose.channel;
|
|
1984
2059
|
if (vm.compose.subject) payload.subject = vm.compose.subject;
|
|
1985
2060
|
if (vm.replyTo) payload.replyTo = vm.replyTo.id;
|
|
1986
|
-
if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm))
|
|
2061
|
+
if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm)) {
|
|
2062
|
+
payload.claimable = true;
|
|
2063
|
+
payload.kind = "task";
|
|
2064
|
+
payload.payload = { title: vm.compose.subject || "Claimable task" };
|
|
2065
|
+
}
|
|
1987
2066
|
return payload;
|
|
1988
2067
|
}
|
|
1989
2068
|
|
|
@@ -2200,6 +2279,16 @@
|
|
|
2200
2279
|
if (action.copy) await copyText(action.copy);
|
|
2201
2280
|
}
|
|
2202
2281
|
|
|
2282
|
+
async function runConnectorAction(connector, action) {
|
|
2283
|
+
if (!connector || !action) return;
|
|
2284
|
+
try {
|
|
2285
|
+
await this.api("POST", "/connectors/" + encodeURIComponent(connector.id) + "/actions", { action });
|
|
2286
|
+
await this.fetchConnectors();
|
|
2287
|
+
} catch (e) {
|
|
2288
|
+
alert("Connector action failed: " + e.message);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2203
2292
|
async function copyText(value) {
|
|
2204
2293
|
if (typeof navigator === "undefined") return;
|
|
2205
2294
|
try {
|
|
@@ -2289,8 +2378,8 @@
|
|
|
2289
2378
|
|
|
2290
2379
|
function pairMessages(vm, pair) {
|
|
2291
2380
|
return (vm.messages || []).filter((msg) =>
|
|
2292
|
-
msg.
|
|
2293
|
-
(msg.
|
|
2381
|
+
msg.payload?.pairId === pair.id ||
|
|
2382
|
+
(msg.payload?.pairEvent && [pair.requesterId, pair.targetId].includes(msg.from) && [pair.requesterId, pair.targetId].includes(msg.to))
|
|
2294
2383
|
);
|
|
2295
2384
|
}
|
|
2296
2385
|
|
|
@@ -2343,7 +2432,7 @@
|
|
|
2343
2432
|
if (msg.channel) lines.push("- Channel: " + msg.channel);
|
|
2344
2433
|
if (msg.subject) lines.push("- Subject: " + msg.subject);
|
|
2345
2434
|
if (msg.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(msg.claimedBy));
|
|
2346
|
-
lines.push("",
|
|
2435
|
+
lines.push("", messageBody.call(vm, msg) || "", "");
|
|
2347
2436
|
}
|
|
2348
2437
|
}
|
|
2349
2438
|
|
|
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>
|
|
@@ -911,6 +922,75 @@ agent-relay-orchestrator</pre>
|
|
|
911
922
|
</template>
|
|
912
923
|
</div>
|
|
913
924
|
|
|
925
|
+
<!-- ==================== CONNECTORS ==================== -->
|
|
926
|
+
<div x-show="view === 'connectors'" x-cloak class="fade-in">
|
|
927
|
+
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
928
|
+
<h2 class="page-title mb-0">Connectors</h2>
|
|
929
|
+
<span class="badge bg-secondary-lt" x-show="connectorCards.length > 0" x-text="connectorCards.length + ' registered'"></span>
|
|
930
|
+
<div class="ms-auto">
|
|
931
|
+
<button class="btn btn-sm btn-ghost-secondary" @click="fetchConnectors()" title="Refresh">
|
|
932
|
+
<i class="ti ti-refresh"></i>
|
|
933
|
+
</button>
|
|
934
|
+
</div>
|
|
935
|
+
</div>
|
|
936
|
+
|
|
937
|
+
<div class="row g-3">
|
|
938
|
+
<template x-for="connector in connectorCards" :key="connector.id">
|
|
939
|
+
<div class="col-md-6 col-xl-4">
|
|
940
|
+
<div class="card">
|
|
941
|
+
<div class="card-body">
|
|
942
|
+
<div class="d-flex align-items-start gap-2">
|
|
943
|
+
<span class="agent-type-icon agent mt-0">
|
|
944
|
+
<i class="ti" :class="connectorPresence(connector).icon"></i>
|
|
945
|
+
</span>
|
|
946
|
+
<div class="flex-grow-1 min-width-0">
|
|
947
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
948
|
+
<span class="fw-bold text-truncate" x-text="connector.displayName || connector.id"></span>
|
|
949
|
+
<span class="badge" :class="'bg-' + connectorPresence(connector).tone + '-lt'">
|
|
950
|
+
<i class="ti me-1" :class="connectorPresence(connector).icon"></i><span x-text="connectorPresence(connector).label"></span>
|
|
951
|
+
</span>
|
|
952
|
+
<span class="badge bg-secondary-lt" x-text="connector.kind"></span>
|
|
953
|
+
</div>
|
|
954
|
+
<div class="text-secondary small mt-1" x-text="connector.description || connector.packageName || connector.binary"></div>
|
|
955
|
+
<div class="d-flex gap-1 mt-2 flex-wrap">
|
|
956
|
+
<template x-for="capability in (connector.capabilities || [])" :key="capability">
|
|
957
|
+
<span class="badge bg-cyan-lt" x-text="capability"></span>
|
|
958
|
+
</template>
|
|
959
|
+
</div>
|
|
960
|
+
<div class="text-secondary small mt-2 d-flex gap-2 flex-wrap">
|
|
961
|
+
<span x-text="'v' + connector.version"></span>
|
|
962
|
+
<span x-show="connector.runtime?.detail" x-text="connector.runtime.detail"></span>
|
|
963
|
+
<span x-show="connector.runtime?.updatedAt" x-text="'Updated ' + timeAgo(connector.runtime.updatedAt)"></span>
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
<div class="d-flex gap-1 mt-3 flex-wrap">
|
|
968
|
+
<template x-for="action in ['status','doctor','start','stop','restart','enable','disable']" :key="connector.id + action">
|
|
969
|
+
<button
|
|
970
|
+
class="btn btn-sm btn-ghost-secondary"
|
|
971
|
+
x-show="connector.manifest?.commands?.[action]"
|
|
972
|
+
@click="runConnectorAction(connector, action)"
|
|
973
|
+
:title="action.charAt(0).toUpperCase() + action.slice(1)">
|
|
974
|
+
<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>
|
|
975
|
+
</button>
|
|
976
|
+
</template>
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
</template>
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
<template x-if="connectorCards.length === 0">
|
|
985
|
+
<div class="card">
|
|
986
|
+
<div class="card-body text-center text-secondary py-5">
|
|
987
|
+
<i class="ti ti-plug-off" style="font-size:48px; opacity:0.3"></i>
|
|
988
|
+
<p class="mt-2">No connectors registered</p>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
</template>
|
|
992
|
+
</div>
|
|
993
|
+
|
|
914
994
|
<!-- ==================== INTEGRATIONS ==================== -->
|
|
915
995
|
<div x-show="view === 'integrations'" x-cloak class="fade-in">
|
|
916
996
|
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
|
@@ -1164,7 +1244,7 @@ agent-relay-orchestrator</pre>
|
|
|
1164
1244
|
<template x-if="m.subject">
|
|
1165
1245
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
1166
1246
|
</template>
|
|
1167
|
-
<div class="msg-body" x-text="m
|
|
1247
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
1168
1248
|
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap">
|
|
1169
1249
|
<button class="btn btn-sm btn-ghost-primary py-0 px-1" @click="startReply(m)">
|
|
1170
1250
|
<i class="ti ti-corner-up-left" style="font-size:14px"></i> Reply
|
|
@@ -1419,7 +1499,7 @@ agent-relay-orchestrator</pre>
|
|
|
1419
1499
|
<template x-if="m.subject">
|
|
1420
1500
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
1421
1501
|
</template>
|
|
1422
|
-
<div class="msg-body" x-text="m
|
|
1502
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
1423
1503
|
<div class="d-flex align-items-center gap-2 mt-2 flex-wrap">
|
|
1424
1504
|
<button class="btn btn-sm btn-ghost-primary py-0 px-1" @click="startReply(m)">
|
|
1425
1505
|
<i class="ti ti-corner-up-left" style="font-size:14px"></i> Reply
|
|
@@ -2388,7 +2468,7 @@ agent-relay-orchestrator</pre>
|
|
|
2388
2468
|
<template x-if="m.subject">
|
|
2389
2469
|
<div class="fw-bold small mb-1" x-text="m.subject"></div>
|
|
2390
2470
|
</template>
|
|
2391
|
-
<div class="msg-body" x-text="m
|
|
2471
|
+
<div class="msg-body" x-text="messageBody(m)"></div>
|
|
2392
2472
|
</div>
|
|
2393
2473
|
</template>
|
|
2394
2474
|
<template x-if="threadMessages.length === 0">
|
|
@@ -2481,6 +2561,13 @@ agent-relay-orchestrator</pre>
|
|
|
2481
2561
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@latest/dist/apexcharts.min.js"></script>
|
|
2482
2562
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
|
2483
2563
|
<script src="dashboard.js"></script>
|
|
2564
|
+
<script>
|
|
2565
|
+
if ("serviceWorker" in navigator) {
|
|
2566
|
+
window.addEventListener("load", () => {
|
|
2567
|
+
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
</script>
|
|
2484
2571
|
|
|
2485
2572
|
</body>
|
|
2486
2573
|
</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
|
+
}
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const CACHE_NAME = "agent-relay-dashboard-v1";
|
|
2
|
+
const APP_SHELL = [
|
|
3
|
+
"/",
|
|
4
|
+
"/index.html",
|
|
5
|
+
"/dashboard.js",
|
|
6
|
+
"/manifest.webmanifest",
|
|
7
|
+
"/icons/agent-relay.svg",
|
|
8
|
+
"/icons/agent-relay-192.png",
|
|
9
|
+
"/icons/agent-relay-512.png",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
self.addEventListener("install", (event) => {
|
|
13
|
+
event.waitUntil(
|
|
14
|
+
caches.open(CACHE_NAME)
|
|
15
|
+
.then((cache) => cache.addAll(APP_SHELL))
|
|
16
|
+
.then(() => self.skipWaiting()),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
self.addEventListener("activate", (event) => {
|
|
21
|
+
event.waitUntil(
|
|
22
|
+
caches.keys()
|
|
23
|
+
.then((names) => Promise.all(names
|
|
24
|
+
.filter((name) => name !== CACHE_NAME)
|
|
25
|
+
.map((name) => caches.delete(name))))
|
|
26
|
+
.then(() => self.clients.claim()),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
self.addEventListener("fetch", (event) => {
|
|
31
|
+
const request = event.request;
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
|
|
34
|
+
if (request.method !== "GET" || url.origin !== self.location.origin || url.pathname.startsWith("/api/")) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (request.headers.get("accept")?.includes("text/event-stream")) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
event.respondWith(
|
|
43
|
+
fetch(request)
|
|
44
|
+
.then((response) => {
|
|
45
|
+
if (response.ok && APP_SHELL.includes(url.pathname === "/" ? "/" : url.pathname)) {
|
|
46
|
+
const copy = response.clone();
|
|
47
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
|
|
48
|
+
}
|
|
49
|
+
return response;
|
|
50
|
+
})
|
|
51
|
+
.catch(async () => {
|
|
52
|
+
const cached = await caches.match(request);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
if (request.mode === "navigate") return caches.match("/index.html");
|
|
55
|
+
throw new Error("offline");
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
});
|