agent-relay-server 0.4.20 → 0.4.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -4,6 +4,16 @@
4
4
  const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
5
5
  const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
6
6
  const LIVE_REFRESH_MS = 5_000;
7
+ const AGENT_TYPE_ICONS = {
8
+ codex: "ti-terminal-2",
9
+ claude: "ti-sparkles",
10
+ agent: "ti-robot",
11
+ };
12
+ const AGENT_TYPE_TITLES = {
13
+ codex: "Codex agent",
14
+ claude: "Claude agent",
15
+ agent: "Agent",
16
+ };
7
17
 
8
18
  function loadPref(key, fallback) {
9
19
  try {
@@ -95,7 +105,7 @@
95
105
 
96
106
  function upsertById(list, item) {
97
107
  const idx = list.findIndex((existing) => existing.id === item.id);
98
- if (idx >= 0) list[idx] = item;
108
+ if (idx >= 0) list.splice(idx, 1, item);
99
109
  else list.push(item);
100
110
  }
101
111
 
@@ -126,11 +136,18 @@
126
136
  function createLifecycleMethods() {
127
137
  return {
128
138
  async init() {
139
+ this.startClock();
140
+ watchPersistedPrefs(this);
141
+
142
+ try {
143
+ this.stats = await this.api("GET", "/stats");
144
+ } catch {
145
+ if (this.authNeeded) return;
146
+ }
147
+
129
148
  await this.refresh();
130
149
  this.connectSSE();
131
- this.startClock();
132
150
  this.startAutoRefresh();
133
- watchPersistedPrefs(this);
134
151
  },
135
152
 
136
153
  save(key, value) {
@@ -446,6 +463,9 @@
446
463
  return {
447
464
  displayName,
448
465
  displayTarget,
466
+ agentType,
467
+ agentTypeIcon,
468
+ agentTypeTitle,
449
469
  severityClass,
450
470
  agentStatusTitle,
451
471
  timeAgo,
@@ -470,6 +490,32 @@
470
490
  return agent ? this.displayName(agent) : target.slice(-8);
471
491
  }
472
492
 
493
+ function agentType(agent) {
494
+ const values = [
495
+ ...(agent?.tags || []),
496
+ agent?.meta?.provider,
497
+ agent?.meta?.client,
498
+ agent?.meta?.runtime,
499
+ agent?.meta?.agentType,
500
+ agent?.id,
501
+ agent?.name,
502
+ ]
503
+ .filter((value) => typeof value === "string")
504
+ .map((value) => value.toLowerCase());
505
+
506
+ if (values.some((value) => value.includes("codex"))) return "codex";
507
+ if (values.some((value) => value.includes("claude"))) return "claude";
508
+ return "agent";
509
+ }
510
+
511
+ function agentTypeIcon(agent) {
512
+ return AGENT_TYPE_ICONS[agentType(agent)] || AGENT_TYPE_ICONS.agent;
513
+ }
514
+
515
+ function agentTypeTitle(agent) {
516
+ return AGENT_TYPE_TITLES[agentType(agent)] || AGENT_TYPE_TITLES.agent;
517
+ }
518
+
473
519
  function severityClass(severity) {
474
520
  if (severity === "critical") return "bg-danger-lt";
475
521
  if (severity === "warning") return "bg-warning-lt";
@@ -789,7 +835,7 @@
789
835
 
790
836
  window.AgentRelayDashboard = {
791
837
  createRelayDashboard,
792
- helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents },
838
+ helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents, agentType },
793
839
  };
794
840
  window.relay = createRelayDashboard;
795
841
 
package/public/index.html CHANGED
@@ -48,6 +48,33 @@
48
48
  .agent-card:hover .agent-actions { opacity: 1; }
49
49
 
50
50
  .agent-label { color: var(--tblr-warning); font-weight: 600; }
51
+ .agent-type-icon {
52
+ width: 20px;
53
+ height: 20px;
54
+ border-radius: 6px;
55
+ display: inline-flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ flex: 0 0 20px;
59
+ border: 1px solid transparent;
60
+ font-size: 14px;
61
+ line-height: 1;
62
+ }
63
+ .agent-type-icon.codex {
64
+ color: var(--tblr-info);
65
+ background: rgba(var(--tblr-info-rgb), 0.12);
66
+ border-color: rgba(var(--tblr-info-rgb), 0.25);
67
+ }
68
+ .agent-type-icon.claude {
69
+ color: var(--tblr-warning);
70
+ background: rgba(var(--tblr-warning-rgb), 0.12);
71
+ border-color: rgba(var(--tblr-warning-rgb), 0.25);
72
+ }
73
+ .agent-type-icon.agent {
74
+ color: var(--tblr-secondary);
75
+ background: var(--tblr-bg-surface-secondary);
76
+ border-color: var(--tblr-border-color);
77
+ }
51
78
 
52
79
  .msg-card { border-left: 3px solid transparent; }
53
80
  .msg-card.claimed { border-left-color: var(--tblr-success); }
@@ -102,7 +129,7 @@
102
129
  <div class="ar-sidebar-footer">
103
130
  <div class="d-flex align-items-center gap-2 mb-2">
104
131
  <span class="status-dot" :class="connected ? 'online' : 'offline'"></span>
105
- <span class="small" x-text="connected ? 'Live' : 'Reconnecting…'"></span>
132
+ <span class="small" x-text="authNeeded ? 'Auth required' : connected ? 'Live' : 'Reconnecting…'"></span>
106
133
  </div>
107
134
  <div class="d-flex align-items-center gap-2 mb-2">
108
135
  <label class="form-check form-switch mb-0">
@@ -238,6 +265,9 @@
238
265
  <template x-for="a in sortedAgents.slice(0, 20)" :key="a.id">
239
266
  <div class="list-group-item d-flex align-items-center gap-2" style="cursor:pointer" @click="selectedAgent = a.id; switchView('messages')">
240
267
  <span class="status-dot" :class="[a.status, a.status !== 'offline' && !a.ready ? 'not-ready' : '']"></span>
268
+ <span class="agent-type-icon" :class="agentType(a)" :title="agentTypeTitle(a)" :aria-label="agentTypeTitle(a)">
269
+ <i class="ti" :class="agentTypeIcon(a)"></i>
270
+ </span>
241
271
  <div class="flex-grow-1 min-width-0">
242
272
  <div class="text-truncate">
243
273
  <template x-if="a.label">
@@ -339,6 +369,9 @@
339
369
  <div class="card-body">
340
370
  <div class="d-flex align-items-start gap-2">
341
371
  <span class="status-dot mt-1" :class="[a.status, a.status !== 'offline' && !a.ready ? 'not-ready' : '']" :title="agentStatusTitle(a)"></span>
372
+ <span class="agent-type-icon mt-0" :class="agentType(a)" :title="agentTypeTitle(a)" :aria-label="agentTypeTitle(a)">
373
+ <i class="ti" :class="agentTypeIcon(a)"></i>
374
+ </span>
342
375
  <div class="flex-grow-1 min-width-0">
343
376
  <div class="d-flex align-items-center gap-2">
344
377
  <template x-if="a.label">
package/src/security.ts CHANGED
@@ -19,10 +19,21 @@ export function isOriginAllowed(req: Request): boolean {
19
19
  if (!origin) return true;
20
20
  const origins = corsOrigins();
21
21
  if (origins.includes("*")) return true;
22
+ if (origins.includes(origin)) return true;
22
23
 
23
24
  const url = new URL(req.url);
24
25
  const sameOrigin = `${url.protocol}//${url.host}`;
25
- return origin === sameOrigin || origins.includes(origin);
26
+ if (origin === sameOrigin) return true;
27
+
28
+ // Behind a reverse proxy, req.url is the backend address (e.g. http://127.0.0.1:4850)
29
+ // while the browser sends the public origin. Check forwarded headers.
30
+ try {
31
+ const originHost = new URL(origin).host;
32
+ const fwdHost = req.headers.get("x-forwarded-host");
33
+ if (fwdHost && originHost === fwdHost) return true;
34
+ } catch {}
35
+
36
+ return false;
26
37
  }
27
38
 
28
39
  export function applyCors(req: Request, response: Response): Response {
@@ -117,7 +128,7 @@ export function unauthorized(req: Request): Response {
117
128
  }
118
129
 
119
130
  function authToken(): string {
120
- return process.env.AGENT_RELAY_TOKEN || AUTH_TOKEN;
131
+ return process.env.AGENT_RELAY_TOKEN ?? AUTH_TOKEN;
121
132
  }
122
133
 
123
134
  function extractToken(req: Request): string | null {