agent-relay-server 0.6.1 → 0.7.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.
@@ -0,0 +1,336 @@
1
+ import { LIVE_REFRESH_MS } from "./constants.js";
2
+ import { savePref, watchPersistedPrefs } from "./state.js";
3
+ import { indexAgents, upsertById } from "./utils.js";
4
+
5
+ function baseUrl() {
6
+ const href = window.location.href.split("?")[0].split("#")[0];
7
+ return href.endsWith("/") ? href : href + "/";
8
+ }
9
+
10
+ function buildEventsUrl(authToken) {
11
+ const eventUrl = new URL("api/events", baseUrl());
12
+ if (authToken) eventUrl.searchParams.set("token", authToken);
13
+ return eventUrl.toString();
14
+ }
15
+
16
+ function parseEventData(event) {
17
+ return JSON.parse(event.data);
18
+ }
19
+
20
+ function upsertTask(vm, task) {
21
+ const idx = vm.tasks.findIndex((existing) => existing.id === task.id);
22
+ if (idx >= 0) vm.tasks[idx] = task;
23
+ else vm.tasks.unshift(task);
24
+ }
25
+
26
+ function syncAgentStats(vm) {
27
+ vm.stats.agents = vm.agents.length;
28
+ vm.stats.online = vm.agents.filter((agent) => agent.status !== "offline").length;
29
+ }
30
+
31
+ function syncMessageStats(vm, msg) {
32
+ vm.stats.messages = (vm.stats.messages ?? 0) + 1;
33
+ const createdAt = new Date(msg.createdAt || Date.now()).getTime();
34
+ if (Number.isFinite(createdAt) && Date.now() - createdAt <= 86_400_000) {
35
+ vm.stats.messagesLast24h = (vm.stats.messagesLast24h ?? 0) + 1;
36
+ }
37
+ }
38
+
39
+ function refreshChartsIfVisible(vm) {
40
+ if (vm.view !== "analytics") return;
41
+ vm.$nextTick(() => vm.renderCharts());
42
+ }
43
+
44
+ function registerKeyboardShortcuts(vm) {
45
+ if (typeof window === "undefined" || !window.addEventListener || vm._keyboardShortcutsRegistered) return;
46
+ window.addEventListener("keydown", (event) => {
47
+ const key = event.key?.toLowerCase();
48
+ if ((event.metaKey || event.ctrlKey) && key === "k") {
49
+ event.preventDefault();
50
+ vm.openCommandPalette();
51
+ } else if (key === "escape" && vm.commandPaletteOpen) {
52
+ event.preventDefault();
53
+ vm.closeCommandPalette();
54
+ }
55
+ });
56
+ vm._keyboardShortcutsRegistered = true;
57
+ }
58
+
59
+ function handleNewMessage(vm, msg) {
60
+ if (vm.messages.some((existing) => existing.id === msg.id)) return;
61
+ if (vm.view === "messages" && vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
62
+ if (vm.view === "messages" && vm.channelFilter && msg.channel !== vm.channelFilter) return;
63
+ vm.messages.push(msg);
64
+ if (vm.messages.length > 200) vm.messages.shift();
65
+ syncMessageStats(vm, msg);
66
+ refreshChartsIfVisible(vm);
67
+ }
68
+
69
+ function handleAgentStatus(vm, agent) {
70
+ upsertById(vm.agents, agent);
71
+ vm.agentsById[agent.id] = agent;
72
+ syncAgentStats(vm);
73
+ refreshChartsIfVisible(vm);
74
+ }
75
+
76
+ function handleAgentRemoved(vm, data) {
77
+ vm.agents = vm.agents.filter((agent) => agent.id !== data.id);
78
+ delete vm.agentsById[data.id];
79
+ syncAgentStats(vm);
80
+ refreshChartsIfVisible(vm);
81
+ }
82
+
83
+ function handleOrchestratorStatus(vm, orch) {
84
+ upsertById(vm.orchestrators, orch);
85
+ }
86
+
87
+ function handleOrchestratorRemoved(vm, data) {
88
+ vm.orchestrators = vm.orchestrators.filter((o) => o.id !== data.id);
89
+ }
90
+
91
+ function handleMessageClaimed(vm, data) {
92
+ const msg = vm.messages.find((item) => item.id === data.messageId);
93
+ if (!msg) return;
94
+ msg.claimedBy = data.claimedBy;
95
+ msg.claimExpiresAt = data.claimExpiresAt;
96
+ }
97
+
98
+ function handleMessageClaimReleased(vm, data) {
99
+ const msg = vm.messages.find((item) => item.id === data.messageId);
100
+ if (!msg) return;
101
+ delete msg.claimedBy;
102
+ delete msg.claimedAt;
103
+ delete msg.claimExpiresAt;
104
+ }
105
+
106
+ function handleMessageDeleted(vm, data) {
107
+ vm.messages = vm.messages.filter((msg) => msg.id !== data.messageId);
108
+ }
109
+
110
+ function registerTaskEvents(vm, es) {
111
+ for (const eventName of ["task.created", "task.updated", "task.claimed", "task.status"]) {
112
+ es.addEventListener(eventName, (event) => upsertTask(vm, parseEventData(event)));
113
+ }
114
+ }
115
+
116
+ function connectSSE() {
117
+ if (this._es) this._es.close();
118
+ const es = new EventSource(buildEventsUrl(this.authToken));
119
+ this._es = es;
120
+
121
+ es.addEventListener("connected", () => { this.connected = true; });
122
+ es.onerror = () => { this.connected = false; };
123
+
124
+ es.addEventListener("message.new", (event) => handleNewMessage(this, parseEventData(event)));
125
+ es.addEventListener("agent.status", (event) => handleAgentStatus(this, parseEventData(event)));
126
+ es.addEventListener("agent.removed", (event) => handleAgentRemoved(this, parseEventData(event)));
127
+ es.addEventListener("message.claimed", (event) => handleMessageClaimed(this, parseEventData(event)));
128
+ es.addEventListener("message.claim_released", (event) => handleMessageClaimReleased(this, parseEventData(event)));
129
+ es.addEventListener("message.deleted", (event) => handleMessageDeleted(this, parseEventData(event)));
130
+ es.addEventListener("orchestrator.status", (event) => handleOrchestratorStatus(this, parseEventData(event)));
131
+ es.addEventListener("orchestrator.removed", (event) => handleOrchestratorRemoved(this, parseEventData(event)));
132
+ registerTaskEvents(this, es);
133
+ }
134
+
135
+ function applyInboxState(vm, state) {
136
+ const readCursors = {};
137
+ const archivedThreads = {};
138
+ const drafts = {};
139
+
140
+ for (const thread of state?.threads || []) {
141
+ if (thread.readCursorMessageId) readCursors[thread.peerId] = thread.readCursorMessageId;
142
+ if (thread.archivedAtMessageId) archivedThreads[thread.peerId] = thread.archivedAtMessageId;
143
+ }
144
+ for (const draft of state?.drafts || []) {
145
+ if (draft.body) drafts[draft.peerId] = draft.body;
146
+ }
147
+
148
+ vm.inboxReadCursors = readCursors;
149
+ vm.inboxArchivedThreads = archivedThreads;
150
+ vm.inboxDrafts = drafts;
151
+ savePref("inboxReadCursors", readCursors);
152
+ savePref("inboxArchivedThreads", archivedThreads);
153
+ savePref("inboxDrafts", drafts);
154
+ }
155
+
156
+ function pruneSyncedOperatorActivity(vm) {
157
+ const serverClientIds = new Set((vm.activityEvents || []).map((event) => event.clientId).filter(Boolean));
158
+ if (!serverClientIds.size || !vm.operatorActivity?.length) return;
159
+ vm.operatorActivity = vm.operatorActivity.filter((item) => !serverClientIds.has(item.clientId || item.id));
160
+ savePref("operatorActivity", vm.operatorActivity);
161
+ }
162
+
163
+ function comparePairs(a, b) {
164
+ return new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0);
165
+ }
166
+
167
+ export { baseUrl, pruneSyncedOperatorActivity };
168
+
169
+ export function createLifecycleMethods() {
170
+ return {
171
+ async init() {
172
+ this.startClock();
173
+ watchPersistedPrefs(this);
174
+ registerKeyboardShortcuts(this);
175
+ try {
176
+ this.stats = await this.api("GET", "/stats");
177
+ } catch {
178
+ if (this.authNeeded) return;
179
+ }
180
+ await this.refresh();
181
+ this.connectSSE();
182
+ this.startAutoRefresh();
183
+ },
184
+
185
+ save(key, value) {
186
+ savePref(key, value);
187
+ },
188
+
189
+ async switchView(view) {
190
+ this.view = view;
191
+ if (view === "inbox" || view === "messages") await this.fetchMessages();
192
+ if (view === "inbox") this.markInboxThreadRead(this.selectedInboxThreadData);
193
+ if (view === "activity") await Promise.all([this.fetchMessages(), this.fetchPairs(), this.fetchTasks(), this.fetchActivityEvents()]);
194
+ if (view === "work") await Promise.all([this.fetchMessages(), this.fetchTasks()]);
195
+ if (view === "pairs") this.fetchPairs();
196
+ if (view === "channels") await this.fetchChannels();
197
+ if (view === "connectors") await this.fetchConnectors();
198
+ if (view === "integrations") await this.fetchIntegrations();
199
+ if (view === "tasks") this.fetchTasks();
200
+ },
201
+
202
+ startClock() {
203
+ if (this._clockTimer) return;
204
+ this._clockTimer = setInterval(() => { this.now = Date.now(); }, 1_000);
205
+ },
206
+
207
+ startAutoRefresh() {
208
+ this.stopAutoRefresh();
209
+ if (!this.autoRefresh) return;
210
+ this._refreshTimer = setInterval(() => this.refreshLiveData(), LIVE_REFRESH_MS);
211
+ },
212
+
213
+ stopAutoRefresh() {
214
+ if (!this._refreshTimer) return;
215
+ clearInterval(this._refreshTimer);
216
+ this._refreshTimer = null;
217
+ },
218
+ };
219
+ }
220
+
221
+ export function createSseMethods() {
222
+ return { connectSSE };
223
+ }
224
+
225
+ export function createApiMethods() {
226
+ return {
227
+ async api(method, path, body) {
228
+ const opts = { method, headers: {} };
229
+ if (this.authToken) opts.headers["X-Agent-Relay-Token"] = this.authToken;
230
+ if (body) {
231
+ opts.headers["Content-Type"] = "application/json";
232
+ opts.body = JSON.stringify(body);
233
+ }
234
+ const response = await fetch(new URL("api" + path, baseUrl()), opts);
235
+ if (!response.ok) {
236
+ if (response.status === 401) this.authNeeded = true;
237
+ const text = await response.text();
238
+ throw new Error(text || response.statusText);
239
+ }
240
+ this.authNeeded = false;
241
+ return response.json();
242
+ },
243
+
244
+ saveTokenAndRefresh() {
245
+ this.save("authToken", this.authToken);
246
+ this.authNeeded = false;
247
+ this.connectSSE();
248
+ this.refresh();
249
+ },
250
+
251
+ async refresh() {
252
+ await Promise.all([
253
+ this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchOrchestrators(),
254
+ this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchChannels(),
255
+ this.fetchConnectors(), this.fetchIntegrations(), this.fetchInboxState(), this.fetchActivityEvents(),
256
+ ]);
257
+ },
258
+
259
+ async refreshLiveData() {
260
+ if (this._refreshInFlight || this.authNeeded) return;
261
+ this._refreshInFlight = true;
262
+ try {
263
+ await this.refresh();
264
+ refreshChartsIfVisible(this);
265
+ } finally {
266
+ this._refreshInFlight = false;
267
+ }
268
+ },
269
+
270
+ async fetchStats() { try { this.stats = await this.api("GET", "/stats"); } catch {} },
271
+ async fetchHealth() { try { this.health = await this.api("GET", "/health"); } catch {} },
272
+ async fetchAgents() {
273
+ try {
274
+ this.agents = await this.api("GET", "/agents");
275
+ this.agentsById = indexAgents(this.agents);
276
+ } catch {}
277
+ },
278
+ async fetchOrchestrators() { try { this.orchestrators = await this.api("GET", "/orchestrators"); } catch {} },
279
+
280
+ async fetchPairs() {
281
+ try {
282
+ let pairs;
283
+ if (this.view === "activity") {
284
+ pairs = await this.api("GET", "/pairs");
285
+ } else if (this.pairStatusFilter === "open") {
286
+ const [active, pending] = await Promise.all([
287
+ this.api("GET", "/pairs?status=active"),
288
+ this.api("GET", "/pairs?status=pending"),
289
+ ]);
290
+ pairs = [...active, ...pending];
291
+ } else if (this.pairStatusFilter) {
292
+ pairs = await this.api("GET", "/pairs?status=" + encodeURIComponent(this.pairStatusFilter));
293
+ } else {
294
+ pairs = await this.api("GET", "/pairs");
295
+ }
296
+ this.pairs = pairs.sort(comparePairs);
297
+ } catch {}
298
+ },
299
+
300
+ async fetchMessages() {
301
+ try {
302
+ let path = "/messages?limit=100";
303
+ if (this.view === "messages" && this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
304
+ if (this.view === "messages" && this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
305
+ this.messages = await this.api("GET", path);
306
+ } catch {}
307
+ },
308
+
309
+ async fetchTasks() {
310
+ try {
311
+ const params = new URLSearchParams({ limit: "100" });
312
+ if (this.taskStatusFilter) params.set("status", this.taskStatusFilter);
313
+ if (this.taskSourceFilter) params.set("source", this.taskSourceFilter);
314
+ this.tasks = await this.api("GET", "/tasks?" + params.toString());
315
+ } catch {}
316
+ },
317
+
318
+ async fetchIntegrations() { try { this.integrations = await this.api("GET", "/integrations"); } catch {} },
319
+ async fetchConnectors() { try { this.connectors = await this.api("GET", "/connectors"); } catch {} },
320
+ async fetchChannels() { try { this.channels = await this.api("GET", "/channels"); } catch {} },
321
+
322
+ async fetchInboxState() {
323
+ try {
324
+ const state = await this.api("GET", "/inbox/state?operatorId=" + encodeURIComponent("user"));
325
+ applyInboxState(this, state);
326
+ } catch {}
327
+ },
328
+
329
+ async fetchActivityEvents() {
330
+ try {
331
+ this.activityEvents = await this.api("GET", "/activity?limit=200");
332
+ pruneSyncedOperatorActivity(this);
333
+ } catch {}
334
+ },
335
+ };
336
+ }
@@ -0,0 +1,34 @@
1
+ import { initialState } from "./state.js";
2
+ import { createLifecycleMethods, createSseMethods, createApiMethods } from "./api.js";
3
+ import { createComputedDescriptors } from "./computed.js";
4
+ import { createDisplayMethods } from "./display.js";
5
+ import { createMessageActions, createPairActions, createAgentActions } from "./actions.js";
6
+ import { createChartMethods } from "./charts.js";
7
+ import { loadPref, savePref } from "./state.js";
8
+ import { indexAgents, upsertById, agentType, isBuiltInAgent } from "./utils.js";
9
+
10
+ function createRelayDashboard() {
11
+ const dashboard = {
12
+ ...initialState(),
13
+ ...createLifecycleMethods(),
14
+ ...createSseMethods(),
15
+ ...createApiMethods(),
16
+ ...createDisplayMethods(),
17
+ ...createMessageActions(),
18
+ ...createPairActions(),
19
+ ...createAgentActions(),
20
+ ...createChartMethods(),
21
+ };
22
+ Object.defineProperties(dashboard, createComputedDescriptors());
23
+ return dashboard;
24
+ }
25
+
26
+ window.AgentRelayDashboard = {
27
+ createRelayDashboard,
28
+ helpers: { loadPref, savePref, indexAgents, upsertById, agentType, isBuiltInAgent },
29
+ };
30
+ window.relay = createRelayDashboard;
31
+
32
+ document.addEventListener("alpine:init", () => {
33
+ Alpine.data("relay", createRelayDashboard);
34
+ });
@@ -0,0 +1,128 @@
1
+ export function createChartMethods() {
2
+ return {
3
+ renderCharts,
4
+ renderVolumeChart,
5
+ renderStatusChart,
6
+ renderAgentChart,
7
+ destroyAllCharts,
8
+ };
9
+ }
10
+
11
+ function renderCharts() {
12
+ this.renderVolumeChart();
13
+ this.renderStatusChart();
14
+ this.renderAgentChart();
15
+ }
16
+
17
+ function renderVolumeChart() {
18
+ const data = buildVolumeSeries(this.messages);
19
+ if (this.chartInstances.volume) {
20
+ this.chartInstances.volume.updateSeries([{ name: "Messages", data }]);
21
+ return;
22
+ }
23
+ const el = document.querySelector("#chart-volume");
24
+ if (!el) return;
25
+ this.chartInstances.volume = new ApexCharts(el, {
26
+ chart: { type: "area", height: 280, background: "transparent", toolbar: { show: false }, animations: { dynamicAnimation: { speed: 350 } } },
27
+ theme: { mode: "dark" },
28
+ series: [{ name: "Messages", data }],
29
+ xaxis: { type: "datetime" },
30
+ stroke: { curve: "smooth", width: 2 },
31
+ fill: { type: "gradient", gradient: { opacityFrom: 0.4, opacityTo: 0 } },
32
+ dataLabels: { enabled: false },
33
+ colors: ["#4299e1"],
34
+ grid: { borderColor: "rgba(255,255,255,0.06)" },
35
+ });
36
+ this.chartInstances.volume.render();
37
+ }
38
+
39
+ function buildVolumeSeries(messages) {
40
+ const buckets = {};
41
+ for (const msg of messages) {
42
+ const day = msg.createdAt ? new Date(msg.createdAt).toISOString().split("T")[0] : null;
43
+ if (day) buckets[day] = (buckets[day] || 0) + 1;
44
+ }
45
+ return Object.entries(buckets)
46
+ .sort((a, b) => a[0].localeCompare(b[0]))
47
+ .map(([day, count]) => ({ x: day, y: count }));
48
+ }
49
+
50
+ function renderStatusChart() {
51
+ const { labels, series } = countAgentStatuses(this.agents);
52
+ const colorMap = { idle: "#4299e1", busy: "#ecc94b" };
53
+ if (this.chartInstances.status) {
54
+ this.chartInstances.status.updateOptions({
55
+ series,
56
+ labels,
57
+ colors: labels.map((label) => colorMap[label] || "#718096"),
58
+ });
59
+ return;
60
+ }
61
+ const el = document.querySelector("#chart-status");
62
+ if (!el) return;
63
+ this.chartInstances.status = new ApexCharts(el, {
64
+ chart: { type: "donut", height: 280, background: "transparent", animations: { dynamicAnimation: { speed: 350 } } },
65
+ theme: { mode: "dark" },
66
+ series,
67
+ labels,
68
+ colors: labels.map((label) => colorMap[label] || "#718096"),
69
+ legend: { position: "bottom" },
70
+ dataLabels: { enabled: true },
71
+ });
72
+ this.chartInstances.status.render();
73
+ }
74
+
75
+ function countAgentStatuses(agents) {
76
+ const counts = { idle: 0, busy: 0 };
77
+ for (const agent of agents) {
78
+ if (agent.status in counts) counts[agent.status] += 1;
79
+ }
80
+ const labels = Object.keys(counts);
81
+ return { labels, series: labels.map((key) => counts[key]) };
82
+ }
83
+
84
+ function renderAgentChart() {
85
+ const sorted = countMessagesByAgent(this);
86
+ if (this.chartInstances.agents) {
87
+ this.chartInstances.agents.updateOptions({
88
+ series: [{ name: "Messages", data: sorted.map(([, count]) => count) }],
89
+ xaxis: { categories: sorted.map(([name]) => name) },
90
+ });
91
+ return;
92
+ }
93
+ const el = document.querySelector("#chart-agents");
94
+ if (!el) return;
95
+ this.chartInstances.agents = new ApexCharts(el, {
96
+ chart: { type: "bar", height: 280, background: "transparent", toolbar: { show: false }, animations: { dynamicAnimation: { speed: 350 } } },
97
+ theme: { mode: "dark" },
98
+ series: [{ name: "Messages", data: sorted.map(([, count]) => count) }],
99
+ xaxis: { categories: sorted.map(([name]) => name) },
100
+ colors: ["#4299e1"],
101
+ plotOptions: { bar: { borderRadius: 4, distributed: true } },
102
+ legend: { show: false },
103
+ grid: { borderColor: "rgba(255,255,255,0.06)" },
104
+ });
105
+ this.chartInstances.agents.render();
106
+ }
107
+
108
+ function countMessagesByAgent(vm) {
109
+ const counts = {};
110
+ for (const msg of vm.messages) {
111
+ const name = vm.displayTarget(msg.from);
112
+ counts[name] = (counts[name] || 0) + 1;
113
+ }
114
+ return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 10);
115
+ }
116
+
117
+ function destroyChart(vm, name) {
118
+ if (vm.chartInstances[name]) {
119
+ vm.chartInstances[name].destroy();
120
+ vm.chartInstances[name] = null;
121
+ }
122
+ }
123
+
124
+ function destroyAllCharts() {
125
+ destroyChart(this, "volume");
126
+ destroyChart(this, "status");
127
+ destroyChart(this, "agents");
128
+ }