agent-relay-server 0.101.1 → 0.102.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.
Files changed (89) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -2
  3. package/public/assets/{MessageBubble-BEjK1-bi.js → MessageBubble-Dppo7pil.js} +2 -2
  4. package/public/assets/{MessageBubble-BEjK1-bi.js.map → MessageBubble-Dppo7pil.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-AqFEg7Yg.js → PlanGraphPanel-DO5OY2qa.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-AqFEg7Yg.js.map → PlanGraphPanel-DO5OY2qa.js.map} +1 -1
  7. package/public/assets/{activity-D5EZw4bj.js → activity-BhuOs5ha.js} +2 -2
  8. package/public/assets/{activity-D5EZw4bj.js.map → activity-BhuOs5ha.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CSISRm_L.js → agent-profiles-CT-vbfzd.js} +2 -2
  10. package/public/assets/{agent-profiles-CSISRm_L.js.map → agent-profiles-CT-vbfzd.js.map} +1 -1
  11. package/public/assets/{agents-szyYnkgD.js → agents-CLcgi8AD.js} +2 -2
  12. package/public/assets/{agents-szyYnkgD.js.map → agents-CLcgi8AD.js.map} +1 -1
  13. package/public/assets/{analytics-KzLQfGMO.js → analytics-BxGRljQc.js} +2 -2
  14. package/public/assets/{analytics-KzLQfGMO.js.map → analytics-BxGRljQc.js.map} +1 -1
  15. package/public/assets/{automation-DVSq4A1b.js → automation-CE1i1zpw.js} +2 -2
  16. package/public/assets/{automation-DVSq4A1b.js.map → automation-CE1i1zpw.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-DgwkJ2rN.js → branch-state-badge-olhanssg.js} +2 -2
  18. package/public/assets/{branch-state-badge-DgwkJ2rN.js.map → branch-state-badge-olhanssg.js.map} +1 -1
  19. package/public/assets/channels-yHkm1dux.js +2 -0
  20. package/public/assets/channels-yHkm1dux.js.map +1 -0
  21. package/public/assets/{chat-patMKjsi.js → chat-DHdFZvAg.js} +2 -2
  22. package/public/assets/chat-DHdFZvAg.js.map +1 -0
  23. package/public/assets/{connectors-DVKBHNRV.js → connectors-DpR0V2hy.js} +2 -2
  24. package/public/assets/{connectors-DVKBHNRV.js.map → connectors-DpR0V2hy.js.map} +1 -1
  25. package/public/assets/{formatted-body-impl-Csr2DuEk.js → formatted-body-impl-BTeCfK4I.js} +2 -2
  26. package/public/assets/{formatted-body-impl-Csr2DuEk.js.map → formatted-body-impl-BTeCfK4I.js.map} +1 -1
  27. package/public/assets/index-CKhh9XRZ.css +2 -0
  28. package/public/assets/{index-D3woTUwp.js → index-DeDosfVE.js} +6 -6
  29. package/public/assets/{index-D3woTUwp.js.map → index-DeDosfVE.js.map} +1 -1
  30. package/public/assets/{insights-D2_Qry06.js → insights-C4aYlgsY.js} +2 -2
  31. package/public/assets/{insights-D2_Qry06.js.map → insights-C4aYlgsY.js.map} +1 -1
  32. package/public/assets/integrations-CEmYa5mW.js +2 -0
  33. package/public/assets/integrations-CEmYa5mW.js.map +1 -0
  34. package/public/assets/{maintenance-Dm56u8F9.js → maintenance-DVx3SiC_.js} +2 -2
  35. package/public/assets/{maintenance-Dm56u8F9.js.map → maintenance-DVx3SiC_.js.map} +1 -1
  36. package/public/assets/{managed-agents-DpX-cHsm.js → managed-agents-DUyOTdz1.js} +2 -2
  37. package/public/assets/{managed-agents-DpX-cHsm.js.map → managed-agents-DUyOTdz1.js.map} +1 -1
  38. package/public/assets/{markdown-preview-impl-Df6EWwbg.js → markdown-preview-impl-D3hYfzGD.js} +2 -2
  39. package/public/assets/{markdown-preview-impl-Df6EWwbg.js.map → markdown-preview-impl-D3hYfzGD.js.map} +1 -1
  40. package/public/assets/{memory-b2AgvV7H.js → memory-2IfPgcnN.js} +2 -2
  41. package/public/assets/{memory-b2AgvV7H.js.map → memory-2IfPgcnN.js.map} +1 -1
  42. package/public/assets/{messages-C5yDD_oq.js → messages-CkOwABhw.js} +2 -2
  43. package/public/assets/{messages-C5yDD_oq.js.map → messages-CkOwABhw.js.map} +1 -1
  44. package/public/assets/{orchestrators-D3LecmEw.js → orchestrators-CEKO3eQ1.js} +2 -2
  45. package/public/assets/{orchestrators-D3LecmEw.js.map → orchestrators-CEKO3eQ1.js.map} +1 -1
  46. package/public/assets/{overview-CnGkrBNe.js → overview-D9BxevAq.js} +2 -2
  47. package/public/assets/{overview-CnGkrBNe.js.map → overview-D9BxevAq.js.map} +1 -1
  48. package/public/assets/{pairs-DXUdZOqn.js → pairs-CqT4D1uL.js} +2 -2
  49. package/public/assets/{pairs-DXUdZOqn.js.map → pairs-CqT4D1uL.js.map} +1 -1
  50. package/public/assets/{projects-1h41atqJ.js → projects-C_zwR-Fn.js} +2 -2
  51. package/public/assets/{projects-1h41atqJ.js.map → projects-C_zwR-Fn.js.map} +1 -1
  52. package/public/assets/{registry-BeGP37ot.js → registry-BLjwVXCW.js} +2 -2
  53. package/public/assets/{registry-BeGP37ot.js.map → registry-BLjwVXCW.js.map} +1 -1
  54. package/public/assets/{security-BMh_y43L.js → security-8a5_4U3S.js} +2 -2
  55. package/public/assets/{security-BMh_y43L.js.map → security-8a5_4U3S.js.map} +1 -1
  56. package/public/assets/{select-Cvi5OHz0.js → select-s4BE-T62.js} +2 -2
  57. package/public/assets/{select-Cvi5OHz0.js.map → select-s4BE-T62.js.map} +1 -1
  58. package/public/assets/{settings-DKSEwIVG.js → settings-BR30UpbJ.js} +2 -2
  59. package/public/assets/{settings-DKSEwIVG.js.map → settings-BR30UpbJ.js.map} +1 -1
  60. package/public/assets/{store-Ch-QzxPq.js → store-DNbkrqbU.js} +2 -2
  61. package/public/assets/store-DNbkrqbU.js.map +1 -0
  62. package/public/assets/{tasks-BybIAX-y.js → tasks-Bcr17zR3.js} +2 -2
  63. package/public/assets/{tasks-BybIAX-y.js.map → tasks-Bcr17zR3.js.map} +1 -1
  64. package/public/assets/{teams-CMteOToR.js → teams-B_HSkU9T.js} +2 -2
  65. package/public/assets/{teams-CMteOToR.js.map → teams-B_HSkU9T.js.map} +1 -1
  66. package/public/assets/{terminal-viewer-impl-DanOi7lh.js → terminal-viewer-impl-Ctp4csJ3.js} +2 -2
  67. package/public/assets/{terminal-viewer-impl-DanOi7lh.js.map → terminal-viewer-impl-Ctp4csJ3.js.map} +1 -1
  68. package/public/assets/{user-avatar-DG629icI.js → user-avatar-RliaJcaG.js} +2 -2
  69. package/public/assets/{user-avatar-DG629icI.js.map → user-avatar-RliaJcaG.js.map} +1 -1
  70. package/public/assets/{work-queue-DqW80DIy.js → work-queue-C0hpmlv8.js} +2 -2
  71. package/public/assets/{work-queue-DqW80DIy.js.map → work-queue-C0hpmlv8.js.map} +1 -1
  72. package/public/assets/{workspaces-CccVI6lA.js → workspaces-QXbQx_AG.js} +2 -2
  73. package/public/assets/{workspaces-CccVI6lA.js.map → workspaces-QXbQx_AG.js.map} +1 -1
  74. package/public/index.html +3 -3
  75. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  76. package/runner/src/adapter.ts +5 -0
  77. package/src/db/channels.ts +18 -2
  78. package/src/mcp/index.ts +5 -3
  79. package/src/mcp-egress-tools.ts +101 -0
  80. package/src/routes/integrations.ts +47 -13
  81. package/public/assets/channels-Dh_r7SnM.js +0 -2
  82. package/public/assets/channels-Dh_r7SnM.js.map +0 -1
  83. package/public/assets/chat-patMKjsi.js.map +0 -1
  84. package/public/assets/index-B95vV-dd.css +0 -2
  85. package/public/assets/integrations-AW8b4h7T.js +0 -2
  86. package/public/assets/integrations-AW8b4h7T.js.map +0 -1
  87. package/public/assets/store-Ch-QzxPq.js.map +0 -1
  88. package/src/mcp-ntfy-tools.ts +0 -35
  89. package/src/ntfy.ts +0 -222
@@ -0,0 +1,101 @@
1
+ // Generic egress MCP tool + relay_notify_ntfy shim (#777).
2
+ //
3
+ // relay_send_to_channel — resolves a registered channel agent by ID, builds a
4
+ // channel.v1 OUTBOUND envelope, and sends a relay message to the channel agent
5
+ // (which the external adapter picks up over SSE and delivers).
6
+ //
7
+ // relay_notify_ntfy — thin shim that emits to the ntfy:default channel agent with
8
+ // the ntfy event payload { title, body, priority?, tags?, click? }. Same tool name
9
+ // and args as before; async ("accepted for delivery"). Zero ntfy HTTP transport in core.
10
+ //
11
+ // Both tools use notifications:write scope (aliased; DECISION LOCKED — #773).
12
+
13
+ import { MAX_BODY_BYTES } from "./config";
14
+ import { getChannel, sendMessage, ValidationError } from "./db";
15
+ import { validateChannelEnvelope } from "./routes/integrations";
16
+ import { optionalEnum } from "./validation";
17
+ import type { ToolDefinition } from "./mcp/types";
18
+ import { optionalString, optionalStringArray, recordField, stringField } from "./mcp/validation";
19
+
20
+ export const NTFY_PRIORITIES = ["min", "low", "default", "high", "urgent"] as const;
21
+ export type NtfyPriority = (typeof NTFY_PRIORITIES)[number];
22
+
23
+ const NTFY_CHANNEL_ID = "ntfy:default";
24
+
25
+ export const EGRESS_TOOLS: ToolDefinition[] = [
26
+ {
27
+ name: "relay_send_to_channel",
28
+ description: "Send an outbound message to a registered channel agent (e.g. ntfy, webhook). channelId is the channel agent ID; event is an opaque payload the adapter interprets.",
29
+ requiredScopes: ["notifications:write"],
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ channelId: { type: "string", description: "Registered channel agent ID (e.g. 'ntfy:default')." },
34
+ event: { type: "object", description: "Adapter-specific event payload.", additionalProperties: true },
35
+ },
36
+ required: ["channelId", "event"],
37
+ additionalProperties: false,
38
+ },
39
+ },
40
+ {
41
+ name: "relay_notify_ntfy",
42
+ description: "Send an egress-only ntfy notification to the configured operator topic. Supports title, body, priority min→urgent, optional tags, and click-through link.",
43
+ requiredScopes: ["notifications:write"],
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ title: { type: "string" },
48
+ body: { type: "string" },
49
+ priority: { type: "string", enum: [...NTFY_PRIORITIES] },
50
+ tags: { type: "array", items: { type: "string" } },
51
+ click: { type: "string" },
52
+ },
53
+ required: ["title", "body"],
54
+ additionalProperties: false,
55
+ },
56
+ },
57
+ ];
58
+
59
+ function emitChannelEgress(channelId: string, event: Record<string, unknown>): { accepted: boolean; channelId: string; to: string } {
60
+ const channel = getChannel(channelId);
61
+ if (!channel) throw new ValidationError(`channel ${channelId} not registered`);
62
+ if (channel.direction === "inbound") {
63
+ throw new ValidationError(`channel ${channelId} is inbound-only; egress requires outbound or bidirectional`);
64
+ }
65
+ const payload: Record<string, unknown> = {
66
+ schema: "agent-relay.channel.v1",
67
+ direction: "outbound",
68
+ channel: { provider: channel.type, accountId: channel.accountId },
69
+ event,
70
+ };
71
+ validateChannelEnvelope(payload, channel);
72
+ sendMessage({
73
+ from: "system",
74
+ to: channel.agentId,
75
+ kind: "channel.event",
76
+ body: "channel egress",
77
+ payload,
78
+ replyExpected: false,
79
+ });
80
+ return { accepted: true, channelId, to: channel.agentId };
81
+ }
82
+
83
+ export function relayEgressToChannel(args: Record<string, unknown>): { accepted: boolean; channelId: string; to: string } {
84
+ const channelId = stringField(args.channelId, "channelId", { required: true, max: 200 });
85
+ const event = recordField(args.event, "event");
86
+ return emitChannelEgress(channelId, event);
87
+ }
88
+
89
+ export function relayNotifyNtfy(args: Record<string, unknown>): { accepted: boolean; channelId: string } {
90
+ const title = stringField(args.title, "title", { required: true, max: 200 });
91
+ const body = stringField(args.body, "body", { required: true, maxBytes: MAX_BODY_BYTES });
92
+ const priority = optionalEnum(args.priority, "priority", NTFY_PRIORITIES) as NtfyPriority | undefined;
93
+ const tags = optionalStringArray(args.tags, "tags");
94
+ const click = optionalString(args.click, "click", 1000);
95
+ const event: Record<string, unknown> = { title, body };
96
+ if (priority !== undefined) event.priority = priority;
97
+ if (tags?.length) event.tags = tags;
98
+ if (click !== undefined) event.click = click;
99
+ const result = emitChannelEgress(NTFY_CHANNEL_ID, event);
100
+ return { accepted: result.accepted, channelId: result.channelId };
101
+ }
@@ -93,33 +93,67 @@ function channelPayloadEventType(payload: Record<string, unknown>): string {
93
93
  return cleanString(event.type, "payload.event.type", { required: true, max: 80 })!;
94
94
  }
95
95
 
96
- function validateChannelEnvelope(payload: Record<string, unknown>, channel: ChannelSummary): void {
96
+ // Single validator for the `agent-relay.channel.v1` envelope in BOTH directions
97
+ // (issue #776). Direction decides how strict the shape is:
98
+ //
99
+ // INBOUND (adapter → relay; e.g. a received Telegram message) — fully addressed.
100
+ // Requires payload.channel.agentId, a `conversation` { id, type }, an `actor`
101
+ // { id, kind }, and a typed `event`, because the message is about to be routed to
102
+ // a backend agent and threaded into a conversation.
103
+ //
104
+ // OUTBOUND (relay → adapter egress; e.g. an ntfy notification — issues #774/#777) —
105
+ // a fire-and-forget push with no inbound conversation/actor. The canonical egress
106
+ // envelope is:
107
+ // { schema: "agent-relay.channel.v1", direction: "outbound",
108
+ // channel: { provider, accountId }, event: { … } }
109
+ // For ntfy the channel id is `ntfy:default` and the event carries
110
+ // { title, body, priority?, tags?, click? } (mirrors the old relay_notify_ntfy args).
111
+ // So for outbound: channel.agentId / conversation / actor are OPTIONAL (validated
112
+ // only when present), and the event need NOT carry a `type`. The ntfy adapter (#774)
113
+ // and the egress shim (#777) produce/consume this exact shape — keep it STABLE.
114
+ export function validateChannelEnvelope(payload: Record<string, unknown>, channel: ChannelSummary): void {
97
115
  const schema = cleanString(payload.schema, "payload.schema", { required: true, max: 80 });
98
116
  if (schema !== "agent-relay.channel.v1") throw new ValidationError("payload.schema must be agent-relay.channel.v1");
99
117
 
118
+ const direction = cleanString(payload.direction, "payload.direction", { required: true, max: 20 });
119
+ if (direction !== "inbound" && direction !== "outbound") throw new ValidationError("payload.direction must be inbound or outbound");
120
+ const outbound = direction === "outbound";
121
+
100
122
  const payloadChannel = payload.channel;
101
123
  if (!isRecord(payloadChannel)) throw new ValidationError("payload.channel must be an object");
102
124
  const provider = cleanString(payloadChannel.provider, "payload.channel.provider", { required: true, max: 80 });
103
125
  const accountId = cleanString(payloadChannel.accountId, "payload.channel.accountId", { required: true, max: 200 });
104
- const agentId = cleanString(payloadChannel.agentId, "payload.channel.agentId", { required: true, max: 200 });
105
- if (provider !== channel.type || accountId !== channel.accountId || agentId !== channel.agentId) {
126
+ // agentId is required inbound (the routing target); the outbound egress envelope
127
+ // addresses the channel by (provider, accountId) only but if present it must match.
128
+ const agentId = cleanString(payloadChannel.agentId, "payload.channel.agentId", { required: !outbound, max: 200 });
129
+ if (provider !== channel.type || accountId !== channel.accountId || (agentId !== undefined && agentId !== channel.agentId)) {
106
130
  throw new ValidationError("payload.channel does not match channel identity");
107
131
  }
108
132
 
109
- const direction = cleanString(payload.direction, "payload.direction", { required: true, max: 20 });
110
- if (direction !== "inbound" && direction !== "outbound") throw new ValidationError("payload.direction must be inbound or outbound");
111
-
133
+ // Inbound carries a conversation + actor for threading/attribution; outbound egress
134
+ // omits them. Validate whenever present so a malformed object is still rejected.
112
135
  const conversation = payload.conversation;
113
- if (!isRecord(conversation)) throw new ValidationError("payload.conversation must be an object");
114
- cleanString(conversation.id, "payload.conversation.id", { required: true, max: 300 });
115
- cleanString(conversation.type, "payload.conversation.type", { required: true, max: 80 });
136
+ if (!outbound || conversation !== undefined) {
137
+ if (!isRecord(conversation)) throw new ValidationError("payload.conversation must be an object");
138
+ cleanString(conversation.id, "payload.conversation.id", { required: true, max: 300 });
139
+ cleanString(conversation.type, "payload.conversation.type", { required: true, max: 80 });
140
+ }
116
141
 
117
142
  const actor = payload.actor;
118
- if (!isRecord(actor)) throw new ValidationError("payload.actor must be an object");
119
- cleanString(actor.id, "payload.actor.id", { required: true, max: 240 });
120
- cleanString(actor.kind, "payload.actor.kind", { required: true, max: 80 });
143
+ if (!outbound || actor !== undefined) {
144
+ if (!isRecord(actor)) throw new ValidationError("payload.actor must be an object");
145
+ cleanString(actor.id, "payload.actor.id", { required: true, max: 240 });
146
+ cleanString(actor.kind, "payload.actor.kind", { required: true, max: 80 });
147
+ }
121
148
 
122
- channelPayloadEventType(payload);
149
+ if (outbound) {
150
+ // The egress event (e.g. ntfy { title, body, priority?, tags?, click? }) is an
151
+ // opaque object the adapter renders; a `type`, if present, is validated only for size.
152
+ if (!isRecord(payload.event)) throw new ValidationError("payload.event must be an object");
153
+ if (payload.event.type !== undefined) cleanString(payload.event.type, "payload.event.type", { max: 80 });
154
+ } else {
155
+ channelPayloadEventType(payload);
156
+ }
123
157
  validateChannelAttachmentRefs(payload);
124
158
  }
125
159
 
@@ -1,2 +0,0 @@
1
- import{Hn as e,Jt as t,Qn as n,Un as r,Wn as i,et as a}from"./lucide-react-BYZuAhQs.js";import{d as o,t as s}from"./store-Ch-QzxPq.js";import{J as c,K as l,Y as u,c as d,s as f}from"./display-DRiGi1zj.js";import{t as p}from"./badge-BLar6FjP.js";import{t as m}from"./button-BrIB69qd.js";import{a as h,o as g,r as _,t as v}from"./card-lhnFnBoz.js";var y=n();function b({direction:t}){return t===`inbound`?(0,y.jsx)(r,{className:`w-3 h-3`}):t===`outbound`?(0,y.jsx)(e,{className:`w-3 h-3`}):(0,y.jsx)(i,{className:`w-3 h-3`})}function x({channel:e}){let n=o(),r=s(e=>e.openChannelDetail),i=d(e),f=e.status!==`offline`;return(0,y.jsxs)(v,{className:`${f?``:`opacity-60`} hover:border-zinc-600 transition-colors`,children:[(0,y.jsx)(h,{className:`pb-2 pt-4 px-4`,children:(0,y.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,y.jsx)(`div`,{className:`p-1.5 rounded-md ${f?`bg-blue-500/10`:`bg-zinc-800`}`,children:(0,y.jsx)(a,{className:`w-4 h-4 ${f?`text-blue-400`:`text-zinc-500`}`})}),(0,y.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,y.jsx)(g,{className:`text-sm font-medium truncate`,children:e.name||e.id}),(0,y.jsxs)(`div`,{className:`text-xs text-muted-foreground mt-0.5`,children:[e.type,` · `,e.transport]})]}),(0,y.jsx)(p,{className:`text-[10px] shrink-0 ${c(i.tone)}`,children:i.label})]})}),(0,y.jsxs)(_,{className:`px-4 pb-4 space-y-2.5`,children:[(0,y.jsxs)(`div`,{className:`flex items-center gap-1.5 text-xs text-muted-foreground`,children:[(0,y.jsx)(b,{direction:e.direction}),(0,y.jsx)(`span`,{className:`capitalize`,children:e.direction}),e.target&&(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(`span`,{children:`·`}),(0,y.jsx)(`span`,{className:`truncate text-foreground`,children:e.target})]})]}),e.targetHealth&&e.targetHealth.status!==`ok`&&(0,y.jsx)(`div`,{className:`text-xs px-2 py-1 rounded ${e.targetHealth.status===`error`?`bg-red-500/10 text-red-400`:`bg-yellow-500/10 text-yellow-400`}`,children:e.targetHealth.detail}),u(e.capabilities).length>0&&(0,y.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:u(e.capabilities).map(e=>(0,y.jsx)(p,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))}),e.tags.length>0&&(0,y.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:e.tags.map(e=>(0,y.jsx)(p,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))}),(0,y.jsxs)(`div`,{className:`flex items-center justify-between pt-0.5`,children:[(0,y.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:l(n,e.lastSeen)}),(0,y.jsxs)(m,{size:`sm`,variant:`outline`,className:`h-7 text-xs gap-1`,onClick:()=>r(e),children:[(0,y.jsx)(t,{className:`w-3 h-3`}),`Details`]})]})]})]})}function S(){let e=s(e=>e.channels),t=e.filter(f).length,n=e.filter(e=>e.targetHealth?.status===`error`).length,r=e.filter(e=>e.targetHealth?.status===`warning`).length;return(0,y.jsxs)(`div`,{className:`space-y-4`,children:[(0,y.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`h2`,{className:`text-base font-semibold`,children:`Channels`}),(0,y.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Inbound / outbound message integrations`})]}),(0,y.jsxs)(`div`,{className:`flex items-center gap-2 ml-auto`,children:[t>0&&(0,y.jsxs)(p,{className:`text-xs bg-emerald-500/15 text-emerald-400 border-emerald-500/30`,children:[t,` ready`]}),r>0&&(0,y.jsxs)(p,{className:`text-xs bg-yellow-500/15 text-yellow-400 border-yellow-500/30`,children:[r,` warning`]}),n>0&&(0,y.jsxs)(p,{className:`text-xs bg-red-500/15 text-red-400 border-red-500/30`,children:[n,` error`]})]})]}),e.length===0?(0,y.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No channels registered.`}):(0,y.jsx)(`div`,{className:`grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4`,children:e.map(e=>(0,y.jsx)(x,{channel:e},e.id))})]})}export{S as ChannelsView};
2
- //# sourceMappingURL=channels-Dh_r7SnM.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"channels-Dh_r7SnM.js","names":[],"sources":["../../dashboard/src/components/views/channels.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { channelIsReady, channelPresence, timeAgo, toneToColor, userFacingCapabilities } from '@/lib/display'\nimport { MessagesSquare, ArrowRight, ArrowLeft, ArrowLeftRight, ExternalLink } from 'lucide-react'\nimport type { ChannelSummary, ChannelDirection } from '@/types'\n\nfunction DirectionIcon({ direction }: { direction: ChannelDirection }) {\n if (direction === 'inbound') return <ArrowLeft className=\"w-3 h-3\" />\n if (direction === 'outbound') return <ArrowRight className=\"w-3 h-3\" />\n return <ArrowLeftRight className=\"w-3 h-3\" />\n}\n\nfunction ChannelCard({ channel }: { channel: ChannelSummary }) {\n const now = useNow()\n const openChannelDetail = useRelayStore((s) => s.openChannelDetail)\n\n const presence = channelPresence(channel)\n const online = channel.status !== 'offline'\n\n return (\n <Card className={`${!online ? 'opacity-60' : ''} hover:border-zinc-600 transition-colors`}>\n <CardHeader className=\"pb-2 pt-4 px-4\">\n <div className=\"flex items-start gap-2\">\n <div className={`p-1.5 rounded-md ${online ? 'bg-blue-500/10' : 'bg-zinc-800'}`}>\n <MessagesSquare className={`w-4 h-4 ${online ? 'text-blue-400' : 'text-zinc-500'}`} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"text-sm font-medium truncate\">{channel.name || channel.id}</CardTitle>\n <div className=\"text-xs text-muted-foreground mt-0.5\">{channel.type} · {channel.transport}</div>\n </div>\n <Badge className={`text-[10px] shrink-0 ${toneToColor(presence.tone)}`}>\n {presence.label}\n </Badge>\n </div>\n </CardHeader>\n\n <CardContent className=\"px-4 pb-4 space-y-2.5\">\n {/* Direction + target */}\n <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n <DirectionIcon direction={channel.direction} />\n <span className=\"capitalize\">{channel.direction}</span>\n {channel.target && (\n <>\n <span>·</span>\n <span className=\"truncate text-foreground\">{channel.target}</span>\n </>\n )}\n </div>\n\n {/* Target health */}\n {channel.targetHealth && channel.targetHealth.status !== 'ok' && (\n <div className={`text-xs px-2 py-1 rounded ${channel.targetHealth.status === 'error' ? 'bg-red-500/10 text-red-400' : 'bg-yellow-500/10 text-yellow-400'}`}>\n {channel.targetHealth.detail}\n </div>\n )}\n\n {/* Capabilities */}\n {userFacingCapabilities(channel.capabilities).length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {userFacingCapabilities(channel.capabilities).map((c) => (\n <Badge key={c} variant=\"secondary\" className=\"text-[9px] px-1 py-0 h-4\">{c}</Badge>\n ))}\n </div>\n )}\n\n {/* Tags */}\n {channel.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {channel.tags.map((t) => (\n <Badge key={t} variant=\"outline\" className=\"text-[9px] px-1 py-0 h-4\">{t}</Badge>\n ))}\n </div>\n )}\n\n {/* Last seen + action */}\n <div className=\"flex items-center justify-between pt-0.5\">\n <span className=\"text-xs text-muted-foreground\">{timeAgo(now, channel.lastSeen)}</span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs gap-1\"\n onClick={() => openChannelDetail(channel)}\n >\n <ExternalLink className=\"w-3 h-3\" />\n Details\n </Button>\n </div>\n </CardContent>\n </Card>\n )\n}\n\nexport function ChannelsView() {\n const channels = useRelayStore((s) => s.channels)\n\n const readyCount = channels.filter(channelIsReady).length\n const errorCount = channels.filter((c) => c.targetHealth?.status === 'error').length\n const warningCount = channels.filter((c) => c.targetHealth?.status === 'warning').length\n\n return (\n <div className=\"space-y-4\">\n {/* Header */}\n <div className=\"flex items-center gap-3\">\n <div>\n <h2 className=\"text-base font-semibold\">Channels</h2>\n <p className=\"text-xs text-muted-foreground\">Inbound / outbound message integrations</p>\n </div>\n <div className=\"flex items-center gap-2 ml-auto\">\n {readyCount > 0 && (\n <Badge className=\"text-xs bg-emerald-500/15 text-emerald-400 border-emerald-500/30\">\n {readyCount} ready\n </Badge>\n )}\n {warningCount > 0 && (\n <Badge className=\"text-xs bg-yellow-500/15 text-yellow-400 border-yellow-500/30\">\n {warningCount} warning\n </Badge>\n )}\n {errorCount > 0 && (\n <Badge className=\"text-xs bg-red-500/15 text-red-400 border-red-500/30\">\n {errorCount} error\n </Badge>\n )}\n </div>\n </div>\n\n {/* Cards */}\n {channels.length === 0 ? (\n <div className=\"text-center py-16 text-muted-foreground text-sm\">\n No channels registered.\n </div>\n ) : (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {channels.map((channel) => (\n <ChannelCard key={channel.id} channel={channel} />\n ))}\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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"}