@agent-live/cli 0.0.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 @@
1
+ import{a as n,u as l,m as o,j as e,f as d}from"./index-jfHKGDWM.js";import{P as c,D as p,a as h,c as m,b as u,E as j}from"./index-49jCgePm.js";import{A as x}from"./components-0p8SghrX.js";function A(){const{agentId:r,search:i}=n(),a=l("approvals",r),t=a.data?.approvals.filter(s=>o(i,s.label,s.agentName,s.customerAlias));return e.jsxs(e.Fragment,{children:[e.jsx(c,{title:"Approvals",description:"Observed approval requests and decisions. Decisions are made in the originating application.",children:e.jsx(x,{})}),e.jsx(p,{pending:a.isPending,error:a.error,retry:a.refetch,children:t&&e.jsx(h,{children:t.length?e.jsx(m,{label:"Approval events",headers:["Request","Agent","Customer","When","Status"],children:t.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[s.label,e.jsx("div",{className:"row-sub",children:s.channel})]}),e.jsx("td",{children:s.agentName}),e.jsx("td",{children:s.customerAlias}),e.jsx("td",{children:d(s.at)}),e.jsxs("td",{children:[e.jsx(u,{status:s.status}),s.open&&e.jsx("div",{className:"row-sub",children:"Needs a decision"})]})]},s.id))}):e.jsx(j,{children:"No approvals match this view. Adjust filters or wait for an approval request."})})})]})}export{A as default};
@@ -0,0 +1 @@
1
+ import{a as n,u as l,m as c,j as e,f as o}from"./index-jfHKGDWM.js";import{P as m,D as d,a as h,c as u,b as j,E as x}from"./index-49jCgePm.js";import{A as v}from"./components-0p8SghrX.js";function y(){const{agentId:a,search:i}=n(),r=l("deliveries",a),t=r.data?.deliveries.filter(s=>c(i,s.summary,s.purpose,s.agentName,s.customerAlias));return e.jsxs(e.Fragment,{children:[e.jsx(m,{title:"Deliveries",description:"Delivery events and their recorded outcomes.",children:e.jsx(v,{})}),e.jsx(d,{pending:r.isPending,error:r.error,retry:r.refetch,children:t&&e.jsx(h,{children:t.length?e.jsx(u,{label:"Delivery events",headers:["Delivery","Agent","Customer","When","Outcome"],children:t.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[s.purpose,e.jsx("div",{className:"row-sub",children:s.summary})]}),e.jsx("td",{children:s.agentName}),e.jsx("td",{children:s.customerAlias}),e.jsx("td",{children:o(s.at)}),e.jsx("td",{children:e.jsx(j,{status:s.outcome})})]},s.id))}):e.jsx(x,{children:"No deliveries match this view. Adjust filters or wait for a delivery event."})})})]})}export{y as default};
@@ -0,0 +1 @@
1
+ import{a as i,u as o,m as c,j as e}from"./index-jfHKGDWM.js";import{P as u,D as m,a as d}from"./index-49jCgePm.js";import{A as l,R as g}from"./components-0p8SghrX.js";function f(){const{agentId:t,search:n}=i(),a=o("runs",t),r=a.data?.runs.filter(s=>c(n,s.summary,s.agentName,s.customerAlias,s.channel,s.id));return e.jsxs(e.Fragment,{children:[e.jsx(u,{title:"Activity",description:"Runs and outcomes across your agents.",children:e.jsx(l,{})}),e.jsx(m,{pending:a.isPending,error:a.error,retry:a.refetch,children:r&&e.jsx(d,{children:e.jsx(g,{runs:r})})})]})}export{f as default};
@@ -0,0 +1 @@
1
+ import{u as t,j as e,f as n}from"./index-jfHKGDWM.js";import{P as i,D as c,a as r,E as l}from"./index-49jCgePm.js";import{C as o}from"./components-0p8SghrX.js";function p(){const s=t("touchpoints");return e.jsxs(e.Fragment,{children:[e.jsx(i,{title:"Touchpoints",description:"Where customers interact with your agents."}),e.jsx(c,{pending:s.isPending,error:s.error,retry:s.refetch,children:s.data&&e.jsxs("div",{className:"panels",children:[e.jsx(r,{title:"Channel share",children:e.jsx(o,{items:s.data.breakdown})}),e.jsx(r,{title:"Recent touches",children:e.jsx("div",{className:"card-b",children:s.data.recent.length?s.data.recent.map(a=>e.jsxs("div",{className:"row",children:[e.jsxs("div",{className:"row-title",children:[a.action,e.jsxs("div",{className:"row-sub",children:[a.customerAlias," · ",a.agentName," · ",n(a.at)]})]}),e.jsx("span",{className:"pill pill-info",children:a.channel})]},a.id)):e.jsx(l,{children:"Customer interactions will appear here when received."})})})]})})]})}export{p as default};
@@ -0,0 +1 @@
1
+ import{u as a,j as e,L as n,f as r}from"./index-jfHKGDWM.js";import{P as i,D as d,b as c,E as l}from"./index-49jCgePm.js";function g(){const t=a("agents");return e.jsxs(e.Fragment,{children:[e.jsx(i,{title:"Agents",description:"Fleet health. Open an agent to see its activity."}),e.jsx(d,{pending:t.isPending,error:t.error,retry:t.refetch,children:t.data&&(t.data.agents.length?e.jsx("div",{className:"agent-grid",children:t.data.agents.map(s=>e.jsxs(n,{className:"agent-card",to:`/activity?${new URLSearchParams({agent:s.id})}`,children:[e.jsxs("div",{className:"agent-title",children:[e.jsx("h2",{children:s.name}),e.jsx(c,{status:s.status})]}),e.jsx("p",{children:s.description}),e.jsxs("div",{className:"agent-meta",children:[e.jsxs("span",{children:[s.runsToday," runs today"]}),e.jsxs("span",{children:[Math.round(s.failureRate*100),"% failed"]}),e.jsx("span",{children:s.channels.join(", ")}),e.jsxs("span",{children:["Last ",r(s.lastActivityAt)]})]})]},s.id))}):e.jsx(l,{children:"Connected agents will appear here when the server receives activity."}))})]})}export{g as default};
@@ -0,0 +1 @@
1
+ import{u as l,j as e,f as r}from"./index-jfHKGDWM.js";import{P as d,D as o,S as i,a as n,b as c,E as u}from"./index-49jCgePm.js";import{C as v,R as x}from"./components-0p8SghrX.js";function m(){const t=l("overview"),a=t.data;return e.jsxs(e.Fragment,{children:[e.jsx(d,{title:"Overview",description:"Customer touchpoints, agent activity, and what needs attention."}),e.jsx(o,{pending:t.isPending,error:t.error,retry:t.refetch,children:a&&e.jsxs("div",{className:"stack",children:[e.jsxs("div",{className:"grid-stats",children:[e.jsx(i,{label:"Active agents",value:a.stats.activeAgents,detail:`${a.stats.agentCount} in fleet`}),e.jsx(i,{label:"Open approvals",value:a.stats.openApprovals,detail:"Waiting on humans"}),e.jsx(i,{label:"Failed deliveries",value:a.stats.failedDeliveries,detail:"Need follow-up"}),e.jsx(i,{label:"Touches today",value:a.stats.touchesToday,detail:`${a.stats.runningRuns} runs live`})]}),e.jsxs("div",{className:"panels",children:[e.jsx(n,{title:"Needs attention",children:e.jsx("div",{className:"card-b",children:a.attention.length?a.attention.map(s=>e.jsxs("div",{className:"row",children:[e.jsxs("div",{className:"row-title",children:[s.title,e.jsxs("div",{className:"row-sub",children:[s.agentName," · ",s.customerAlias," · ",r(s.at)]})]}),e.jsx(c,{status:s.kind==="approval"?"waiting":"failed"})]},`${s.kind}:${s.runId}`)):e.jsx(u,{children:"No outstanding approvals or failed deliveries."})})}),e.jsx(n,{title:"Touch mix",children:e.jsx(v,{items:a.channelBreakdown})})]}),e.jsx(n,{title:"Recent activity",children:e.jsx(x,{runs:a.recentRuns})})]})})]})}export{m as default};
@@ -0,0 +1,4 @@
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><meta name="description" content="Agent Live — agent activity, approvals, and delivery outcomes."/><title>Agent Live</title> <script type="module" crossorigin src="/assets/index-jfHKGDWM.js"></script>
3
+ <link rel="stylesheet" crossorigin href="/assets/index-BnLKN4xZ.css">
4
+ </head><body><div id="root"></div></body></html>
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@agent-live/cli",
3
+ "version": "0.0.1",
4
+ "description": "Run a local dashboard for agents instrumented with @agent-live/sdk — one command, no build tooling required.",
5
+ "license": "Apache-2.0",
6
+ "keywords": ["ai-agents", "observability", "telemetry", "dashboard", "cli"],
7
+ "type": "module",
8
+ "bin": {
9
+ "agent-live": "./bin.mjs"
10
+ },
11
+ "files": ["bin.mjs", "console-dist", "server"],
12
+ "publishConfig": { "access": "public" },
13
+ "scripts": {
14
+ "prepack": "node ./scripts/build-assets.mjs"
15
+ },
16
+ "dependencies": {
17
+ "@agent-live/sdk": "^0.0.2"
18
+ }
19
+ }
@@ -0,0 +1,524 @@
1
+ export const ORG = {
2
+ id: 'org_northline',
3
+ name: 'Northline Ops',
4
+ mode: 'demo',
5
+ }
6
+
7
+ function ago(ms) {
8
+ return new Date(Date.now() - ms).toISOString()
9
+ }
10
+
11
+ function seed() {
12
+ const agents = [
13
+ {
14
+ id: 'agt_invoice',
15
+ name: 'Invoice Concierge',
16
+ description: 'Collects, reviews, and delivers invoice packs over WhatsApp',
17
+ status: 'busy',
18
+ channels: ['whatsapp'],
19
+ runsToday: 48,
20
+ failureRate: 0.04,
21
+ lastActivityAt: ago(40_000),
22
+ },
23
+ {
24
+ id: 'agt_support',
25
+ name: 'Support Desk',
26
+ description: 'Triages inbound email and drafts grounded replies',
27
+ status: 'busy',
28
+ channels: ['email', 'web'],
29
+ runsToday: 112,
30
+ failureRate: 0.07,
31
+ lastActivityAt: ago(18_000),
32
+ },
33
+ {
34
+ id: 'agt_ops',
35
+ name: 'Ops Runner',
36
+ description: 'Schedules field jobs and confirms with customers',
37
+ status: 'idle',
38
+ channels: ['whatsapp', 'api'],
39
+ runsToday: 31,
40
+ failureRate: 0.02,
41
+ lastActivityAt: ago(14 * 60_000),
42
+ },
43
+ {
44
+ id: 'agt_billing',
45
+ name: 'Billing Follow-up',
46
+ description: 'Sends payment reminders and escalates disputes',
47
+ status: 'degraded',
48
+ channels: ['email', 'whatsapp'],
49
+ runsToday: 67,
50
+ failureRate: 0.14,
51
+ lastActivityAt: ago(3 * 60_000),
52
+ },
53
+ {
54
+ id: 'agt_onboard',
55
+ name: 'Onboarding Guide',
56
+ description: 'Walks new accounts through setup checklists',
57
+ status: 'idle',
58
+ channels: ['web', 'email'],
59
+ runsToday: 19,
60
+ failureRate: 0.01,
61
+ lastActivityAt: ago(42 * 60_000),
62
+ },
63
+ ]
64
+
65
+ const customers = [
66
+ { id: 'cus_mira', alias: 'Mira Chen', hint: 'WhatsApp · +1 ···· 1212' },
67
+ { id: 'cus_jonas', alias: 'Jonas Reed', hint: 'Email · jonas@…' },
68
+ { id: 'cus_nova', alias: 'Nova Freight', hint: 'API · tenant nova' },
69
+ { id: 'cus_hala', alias: 'Hala Ortiz', hint: 'WhatsApp · +1 ···· 9981' },
70
+ { id: 'cus_orbit', alias: 'Orbit Labs', hint: 'Web · portal' },
71
+ { id: 'cus_eli', alias: 'Eli Park', hint: 'Email · eli@…' },
72
+ ]
73
+
74
+ const runs = []
75
+ const eventsByRun = new Map()
76
+
77
+ function addRun(partial, events) {
78
+ const run = {
79
+ model: null,
80
+ inputTokens: null,
81
+ outputTokens: null,
82
+ errorCode: null,
83
+ externalTraceId: null,
84
+ inputPreview: null,
85
+ outputPreview: null,
86
+ ...partial,
87
+ }
88
+ runs.push(run)
89
+ eventsByRun.set(
90
+ run.id,
91
+ events.map((e, i) => ({
92
+ id: `${run.id}:${i + 1}`,
93
+ runId: run.id,
94
+ agentId: run.agentId,
95
+ tenantId: run.customerId,
96
+ sequence: i + 1,
97
+ status: null,
98
+ completedAt: null,
99
+ durationMs: null,
100
+ ...e,
101
+ })),
102
+ )
103
+ return run
104
+ }
105
+
106
+ addRun(
107
+ {
108
+ id: 'run_live_invoice',
109
+ agentId: 'agt_invoice',
110
+ customerId: 'cus_mira',
111
+ channel: 'whatsapp',
112
+ status: 'running',
113
+ startedAt: ago(2 * 60_000),
114
+ completedAt: null,
115
+ durationMs: null,
116
+ summary: 'Reviewing invoice pack #4421 — waiting on approval',
117
+ },
118
+ [
119
+ { type: 'run.received', label: 'Inbound WhatsApp', startedAt: ago(120_000) },
120
+ { type: 'context.loaded', label: 'Customer context ready', startedAt: ago(110_000) },
121
+ { type: 'tool.completed', label: 'lookup_invoice', startedAt: ago(90_000) },
122
+ {
123
+ type: 'approval.waiting',
124
+ label: 'Send invoice summary to customer',
125
+ startedAt: ago(45_000),
126
+ metadata: { action: 'Send invoice-summary.pdf to Mira Chen' },
127
+ },
128
+ ],
129
+ )
130
+
131
+ addRun(
132
+ {
133
+ id: 'run_done_invoice',
134
+ agentId: 'agt_invoice',
135
+ customerId: 'cus_hala',
136
+ channel: 'whatsapp',
137
+ status: 'completed',
138
+ startedAt: ago(28 * 60_000),
139
+ completedAt: ago(24 * 60_000),
140
+ durationMs: 210_000,
141
+ summary: 'Delivered weekly invoice summary',
142
+ },
143
+ [
144
+ { type: 'run.received', label: 'Inbound WhatsApp', startedAt: ago(28 * 60_000) },
145
+ { type: 'approval.approved', label: 'Customer approved send', startedAt: ago(26 * 60_000) },
146
+ {
147
+ type: 'output.published',
148
+ label: 'File ready',
149
+ startedAt: ago(25.5 * 60_000),
150
+ metadata: { fileName: 'invoice-summary.pdf' },
151
+ },
152
+ {
153
+ type: 'receipt.sent',
154
+ label: 'Work receipt',
155
+ startedAt: ago(25 * 60_000),
156
+ metadata: {
157
+ checked: ['Invoice #4418'],
158
+ created: ['PDF summary'],
159
+ approval: 'approved',
160
+ },
161
+ },
162
+ {
163
+ type: 'delivery.delivered',
164
+ label: 'Delivered on WhatsApp',
165
+ startedAt: ago(24.2 * 60_000),
166
+ metadata: { purpose: 'send_result' },
167
+ },
168
+ { type: 'run.completed', label: 'Completed', startedAt: ago(24 * 60_000) },
169
+ ],
170
+ )
171
+
172
+ addRun(
173
+ {
174
+ id: 'run_fail_billing',
175
+ agentId: 'agt_billing',
176
+ customerId: 'cus_eli',
177
+ channel: 'email',
178
+ status: 'failed',
179
+ startedAt: ago(55 * 60_000),
180
+ completedAt: ago(52 * 60_000),
181
+ durationMs: 180_000,
182
+ summary: 'Reminder prepared but delivery failed',
183
+ },
184
+ [
185
+ { type: 'run.received', label: 'Scheduled reminder', startedAt: ago(55 * 60_000) },
186
+ {
187
+ type: 'output.published',
188
+ label: 'Reminder draft',
189
+ startedAt: ago(53.5 * 60_000),
190
+ metadata: { fileName: 'payment-reminder.eml' },
191
+ },
192
+ {
193
+ type: 'delivery.started',
194
+ label: 'Sending email',
195
+ startedAt: ago(53 * 60_000),
196
+ metadata: { purpose: 'payment_reminder' },
197
+ },
198
+ {
199
+ type: 'delivery.failed',
200
+ label: 'Mailbox rejected',
201
+ startedAt: ago(52.4 * 60_000),
202
+ metadata: { purpose: 'payment_reminder' },
203
+ },
204
+ { type: 'run.failed', label: 'Stopped', startedAt: ago(52 * 60_000) },
205
+ ],
206
+ )
207
+
208
+ addRun(
209
+ {
210
+ id: 'run_support_live',
211
+ agentId: 'agt_support',
212
+ customerId: 'cus_jonas',
213
+ channel: 'email',
214
+ status: 'running',
215
+ startedAt: ago(90_000),
216
+ completedAt: null,
217
+ durationMs: null,
218
+ summary: 'Drafting reply for shipping delay ticket',
219
+ },
220
+ [
221
+ { type: 'run.received', label: 'Inbound email', startedAt: ago(90_000) },
222
+ { type: 'context.loading', label: 'Loading ticket thread', startedAt: ago(80_000) },
223
+ { type: 'tool.started', label: 'search_orders', startedAt: ago(40_000) },
224
+ ],
225
+ )
226
+
227
+ addRun(
228
+ {
229
+ id: 'run_ops_done',
230
+ agentId: 'agt_ops',
231
+ customerId: 'cus_nova',
232
+ channel: 'api',
233
+ status: 'completed',
234
+ startedAt: ago(3 * 60 * 60_000),
235
+ completedAt: ago(3 * 60 * 60_000 - 90_000),
236
+ durationMs: 90_000,
237
+ summary: 'Confirmed two field jobs via API',
238
+ },
239
+ [
240
+ { type: 'run.received', label: 'API job.confirm', startedAt: ago(3 * 60 * 60_000) },
241
+ { type: 'tool.completed', label: 'schedule_jobs', startedAt: ago(3 * 60 * 60_000 - 40_000) },
242
+ { type: 'run.completed', label: 'Completed', startedAt: ago(3 * 60 * 60_000 - 90_000) },
243
+ ],
244
+ )
245
+
246
+ addRun(
247
+ {
248
+ id: 'run_onboard_web',
249
+ agentId: 'agt_onboard',
250
+ customerId: 'cus_orbit',
251
+ channel: 'web',
252
+ status: 'completed',
253
+ startedAt: ago(5 * 60 * 60_000),
254
+ completedAt: ago(5 * 60 * 60_000 - 4 * 60_000),
255
+ durationMs: 240_000,
256
+ summary: 'Finished onboarding checklist',
257
+ },
258
+ [
259
+ { type: 'run.received', label: 'Portal session', startedAt: ago(5 * 60 * 60_000) },
260
+ { type: 'progress.sent', label: 'Step 3 of 5', startedAt: ago(5 * 60 * 60_000 - 90_000) },
261
+ { type: 'run.completed', label: 'Completed', startedAt: ago(5 * 60 * 60_000 - 4 * 60_000) },
262
+ ],
263
+ )
264
+
265
+ const touchpoints = [
266
+ { id: 'tp1', channel: 'whatsapp', customerId: 'cus_mira', agentId: 'agt_invoice', at: ago(120_000), action: 'Message received' },
267
+ { id: 'tp2', channel: 'email', customerId: 'cus_jonas', agentId: 'agt_support', at: ago(90_000), action: 'Ticket opened' },
268
+ { id: 'tp3', channel: 'whatsapp', customerId: 'cus_hala', agentId: 'agt_invoice', at: ago(24 * 60_000), action: 'Delivery read' },
269
+ { id: 'tp4', channel: 'email', customerId: 'cus_eli', agentId: 'agt_billing', at: ago(52 * 60_000), action: 'Bounce received' },
270
+ { id: 'tp5', channel: 'api', customerId: 'cus_nova', agentId: 'agt_ops', at: ago(3 * 60 * 60_000), action: 'job.confirm' },
271
+ { id: 'tp6', channel: 'web', customerId: 'cus_orbit', agentId: 'agt_onboard', at: ago(5 * 60 * 60_000), action: 'Checklist started' },
272
+ { id: 'tp7', channel: 'whatsapp', customerId: 'cus_mira', agentId: 'agt_billing', at: ago(6 * 60 * 60_000), action: 'Reminder opened' },
273
+ { id: 'tp8', channel: 'email', customerId: 'cus_jonas', agentId: 'agt_support', at: ago(7 * 60 * 60_000), action: 'Follow-up reply' },
274
+ { id: 'tp9', channel: 'web', customerId: 'cus_orbit', agentId: 'agt_support', at: ago(8 * 60 * 60_000), action: 'Help center chat' },
275
+ { id: 'tp10', channel: 'api', customerId: 'cus_nova', agentId: 'agt_ops', at: ago(9 * 60 * 60_000), action: 'status.poll' },
276
+ { id: 'tp11', channel: 'whatsapp', customerId: 'cus_hala', agentId: 'agt_invoice', at: ago(10 * 60 * 60_000), action: 'Approval granted' },
277
+ { id: 'tp12', channel: 'email', customerId: 'cus_eli', agentId: 'agt_billing', at: ago(11 * 60 * 60_000), action: 'Reminder queued' },
278
+ ]
279
+
280
+ return { agents, customers, runs, eventsByRun, touchpoints }
281
+ }
282
+
283
+ export const db = seed()
284
+
285
+
286
+ export function agentById(id) {
287
+ return db.agents.find((a) => a.id === id)
288
+ }
289
+
290
+ export function customerById(id) {
291
+ return db.customers.find((c) => c.id === id)
292
+ }
293
+
294
+ export function enrichRun(run) {
295
+ const agent = agentById(run.agentId)
296
+ const customer = customerById(run.customerId)
297
+ return {
298
+ ...run,
299
+ agentName: agent?.name ?? run.agentId,
300
+ customerAlias: customer?.alias ?? run.customerId,
301
+ customerHint: customer?.hint ?? '',
302
+ }
303
+ }
304
+
305
+ export function channelBreakdown() {
306
+ const counts = {}
307
+ for (const t of db.touchpoints) {
308
+ counts[t.channel] = (counts[t.channel] || 0) + 1
309
+ }
310
+ const total = db.touchpoints.length || 1
311
+ return Object.entries(counts)
312
+ .map(([channel, count]) => ({
313
+ channel,
314
+ count,
315
+ pct: Math.round((count / total) * 100),
316
+ }))
317
+ .sort((a, b) => b.count - a.count)
318
+ }
319
+
320
+ function runHasOpenApproval(run) {
321
+ if (run.status !== 'running') return false
322
+ const events = db.eventsByRun.get(run.id) || []
323
+ const waiting = events.filter((e) => e.type === 'approval.waiting')
324
+ if (!waiting.length) return false
325
+ const resolved = events.some((e) => e.type === 'approval.approved' || e.type === 'approval.cancelled')
326
+ return !resolved
327
+ }
328
+
329
+ function openApprovalEvent(run) {
330
+ if (!runHasOpenApproval(run)) return null
331
+ const events = db.eventsByRun.get(run.id) || []
332
+ return events.findLast?.((e) => e.type === 'approval.waiting') || events.filter((e) => e.type === 'approval.waiting').at(-1)
333
+ }
334
+
335
+ export function buildOverview() {
336
+ const openApprovals = db.runs.filter((r) => runHasOpenApproval(r)).length
337
+ const failedDeliveries = db.runs.filter((r) =>
338
+ (db.eventsByRun.get(r.id) || []).some((e) => e.type === 'delivery.failed'),
339
+ ).length
340
+ const activeAgents = db.agents.filter((a) => a.status === 'busy' || a.status === 'degraded').length
341
+ const touchesToday = db.touchpoints.length
342
+ const running = db.runs.filter((r) => r.status === 'running').length
343
+
344
+ return {
345
+ org: ORG,
346
+ stats: {
347
+ activeAgents,
348
+ agentCount: db.agents.length,
349
+ openApprovals,
350
+ failedDeliveries,
351
+ touchesToday,
352
+ runningRuns: running,
353
+ },
354
+ recentRuns: db.runs
355
+ .slice()
356
+ .sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt))
357
+ .slice(0, 6)
358
+ .map(enrichRun),
359
+ channelBreakdown: channelBreakdown(),
360
+ attention: [
361
+ ...db.runs.flatMap((r) => {
362
+ const waiting = openApprovalEvent(r)
363
+ if (!waiting) return []
364
+ return [
365
+ {
366
+ kind: 'approval',
367
+ runId: r.id,
368
+ title: waiting.metadata?.action || waiting.label,
369
+ agentName: agentById(r.agentId)?.name,
370
+ customerAlias: customerById(r.customerId)?.alias,
371
+ at: waiting.startedAt,
372
+ },
373
+ ]
374
+ }),
375
+ ...db.runs
376
+ .filter((r) => (db.eventsByRun.get(r.id) || []).some((e) => e.type === 'delivery.failed'))
377
+ .map((r) => ({
378
+ kind: 'delivery_failed',
379
+ runId: r.id,
380
+ title: r.summary,
381
+ agentName: agentById(r.agentId)?.name,
382
+ customerAlias: customerById(r.customerId)?.alias,
383
+ at: r.completedAt || r.startedAt,
384
+ })),
385
+ ],
386
+ }
387
+ }
388
+
389
+ export function listApprovals(agentId) {
390
+ const items = []
391
+ for (const run of db.runs) {
392
+ if (agentId && run.agentId !== agentId) continue
393
+ for (const e of db.eventsByRun.get(run.id) || []) {
394
+ if (!e.type.startsWith('approval.')) continue
395
+ items.push({
396
+ id: e.id,
397
+ runId: run.id,
398
+ type: e.type,
399
+ label: e.metadata?.action || e.label,
400
+ status: e.type === 'approval.waiting' ? 'waiting' : e.type === 'approval.approved' ? 'approved' : 'cancelled',
401
+ at: e.startedAt,
402
+ agentId: run.agentId,
403
+ agentName: agentById(run.agentId)?.name,
404
+ customerAlias: customerById(run.customerId)?.alias,
405
+ customerHint: customerById(run.customerId)?.hint,
406
+ channel: run.channel,
407
+ open: e.type === 'approval.waiting' && runHasOpenApproval(run),
408
+ })
409
+ }
410
+ }
411
+ return items.sort((a, b) => Date.parse(b.at) - Date.parse(a.at))
412
+ }
413
+
414
+ export function listDeliveries(agentId) {
415
+ const items = []
416
+ for (const run of db.runs) {
417
+ if (agentId && run.agentId !== agentId) continue
418
+ for (const e of db.eventsByRun.get(run.id) || []) {
419
+ if (!e.type.startsWith('delivery.')) continue
420
+ let outcome = 'in_progress'
421
+ if (e.type === 'delivery.delivered' || e.type === 'delivery.completed' || e.type === 'delivery.read') {
422
+ outcome = 'delivered'
423
+ } else if (e.type === 'delivery.failed') {
424
+ outcome = 'failed'
425
+ } else if (e.type === 'delivery.accepted' || e.type === 'delivery.sent') {
426
+ outcome = 'in_progress'
427
+ }
428
+ items.push({
429
+ id: e.id,
430
+ runId: run.id,
431
+ type: e.type,
432
+ outcome,
433
+ purpose: e.metadata?.purpose || e.label,
434
+ at: e.startedAt,
435
+ agentId: run.agentId,
436
+ agentName: agentById(run.agentId)?.name,
437
+ customerAlias: customerById(run.customerId)?.alias,
438
+ channel: run.channel,
439
+ summary: run.summary,
440
+ })
441
+ }
442
+ }
443
+ return items.sort((a, b) => Date.parse(b.at) - Date.parse(a.at))
444
+ }
445
+
446
+
447
+ export function startSimulation(broadcast) {
448
+ // Simulate live fleet activity
449
+ let tick = 0
450
+ return setInterval(() => {
451
+ tick += 1
452
+ const support = db.runs.find((r) => r.id === 'run_support_live')
453
+ if (support && tick === 1) {
454
+ const list = db.eventsByRun.get(support.id) || []
455
+ const next = {
456
+ id: `${support.id}:${list.length + 1}`,
457
+ runId: support.id,
458
+ agentId: support.agentId,
459
+ tenantId: support.customerId,
460
+ sequence: list.length + 1,
461
+ type: 'tool.completed',
462
+ status: null,
463
+ label: 'search_orders finished',
464
+ metadata: null,
465
+ startedAt: new Date().toISOString(),
466
+ completedAt: null,
467
+ durationMs: null,
468
+ }
469
+ list.push(next)
470
+ support.lastEventAt = next.startedAt
471
+ const agent = agentById(support.agentId)
472
+ if (agent) agent.lastActivityAt = next.startedAt
473
+ broadcast('run-event', {
474
+ ...next,
475
+ agentName: agent?.name,
476
+ customerAlias: customerById(support.customerId)?.alias,
477
+ })
478
+ }
479
+
480
+ if (tick === 2) {
481
+ const touch = {
482
+ id: `tp_live_${Date.now()}`,
483
+ channel: 'whatsapp',
484
+ customerId: 'cus_mira',
485
+ agentId: 'agt_invoice',
486
+ at: new Date().toISOString(),
487
+ action: 'Customer opened approval prompt',
488
+ agentName: agentById('agt_invoice')?.name,
489
+ customerAlias: customerById('cus_mira')?.alias,
490
+ }
491
+ db.touchpoints.unshift(touch)
492
+ broadcast('touch', touch)
493
+ }
494
+
495
+ if (tick === 3) {
496
+ const support = db.runs.find((r) => r.id === 'run_support_live')
497
+ if (!support) return
498
+ const list = db.eventsByRun.get(support.id) || []
499
+ const next = {
500
+ id: `${support.id}:${list.length + 1}`,
501
+ runId: support.id,
502
+ agentId: support.agentId,
503
+ tenantId: support.customerId,
504
+ sequence: list.length + 1,
505
+ type: 'model.started',
506
+ status: null,
507
+ label: 'Drafting grounded reply',
508
+ metadata: null,
509
+ startedAt: new Date().toISOString(),
510
+ completedAt: null,
511
+ durationMs: null,
512
+ }
513
+ list.push(next)
514
+ const agent = agentById(support.agentId)
515
+ if (agent) agent.lastActivityAt = next.startedAt
516
+ broadcast('run-event', {
517
+ ...next,
518
+ agentName: agent?.name,
519
+ customerAlias: customerById(support.customerId)?.alias,
520
+ })
521
+ }
522
+ }, 5000)
523
+
524
+ }
@@ -0,0 +1,102 @@
1
+ import { createServer as createHttpServer } from 'node:http'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { resolve, sep, extname } from 'node:path'
5
+ import { db, buildOverview as buildDemoOverview, channelBreakdown as demoChannelBreakdown, enrichRun, listApprovals as listDemoApprovals, listDeliveries as listDemoDeliveries, agentById, customerById, startSimulation } from './demo/data.mjs'
6
+ import { createRealData } from './real/data.mjs'
7
+
8
+ const defaultAssets = fileURLToPath(new URL('../../console/dist/', import.meta.url))
9
+ const mimeTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.png': 'image/png', '.ico': 'image/x-icon' }
10
+ export function sendJson(res, status, body) {
11
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }).end(JSON.stringify(body))
12
+ }
13
+
14
+ function demoData() {
15
+ return {
16
+ buildOverview: buildDemoOverview,
17
+ listAgents: () => ({ agents: db.agents }),
18
+ listTouchpoints: () => ({
19
+ breakdown: demoChannelBreakdown(),
20
+ recent: db.touchpoints.slice().sort((a, b) => Date.parse(b.at) - Date.parse(a.at)).map(t => ({ ...t, agentName: agentById(t.agentId)?.name, customerAlias: customerById(t.customerId)?.alias })),
21
+ }),
22
+ listRuns: (agentId) => ({ runs: db.runs.filter(r => !agentId || r.agentId === agentId).map(enrichRun).sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt)) }),
23
+ listApprovals: (agentId) => ({ approvals: listDemoApprovals(agentId) }),
24
+ listDeliveries: (agentId) => ({ deliveries: listDemoDeliveries(agentId) }),
25
+ subscribe: (broadcast) => { const sim = startSimulation(broadcast); return () => clearInterval(sim) },
26
+ dispose() {},
27
+ }
28
+ }
29
+
30
+ /** `mode: 'demo'` serves fabricated sample data; `mode: 'real'` reads the SDK's SQLite store at `dbPath`. */
31
+ export function createServer({ mode = 'demo', dbPath, assetsDir = defaultAssets, simulate = true } = {}) {
32
+ const data = mode === 'real' ? createRealData({ dbPath }) : demoData()
33
+ const clients = new Set()
34
+ const heartbeats = new Set()
35
+ function broadcast(event, payload) {
36
+ for (const client of clients) client.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`)
37
+ }
38
+ const unsubscribe = (mode === 'demo' ? simulate : true) ? data.subscribe(broadcast) : null
39
+ const server = createHttpServer((req, res) => {
40
+ handle(req, res).catch(() => {
41
+ if (!res.headersSent) sendJson(res, 500, { error: 'Request failed' })
42
+ else res.end()
43
+ })
44
+ })
45
+ async function handle(req, res) {
46
+ const url = new URL(req.url || '/', 'http://localhost')
47
+ const agentId = url.searchParams.get('agentId') || undefined
48
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
49
+ res.setHeader('Allow', 'GET, HEAD')
50
+ return sendJson(res, 405, { error: 'Method not allowed' })
51
+ }
52
+ if (url.pathname === '/api/overview') return sendJson(res, 200, data.buildOverview())
53
+ if (url.pathname === '/api/agents') return sendJson(res, 200, data.listAgents())
54
+ if (url.pathname === '/api/touchpoints') return sendJson(res, 200, data.listTouchpoints())
55
+ if (url.pathname === '/api/runs') return sendJson(res, 200, data.listRuns(agentId))
56
+ if (url.pathname === '/api/approvals') return sendJson(res, 200, data.listApprovals(agentId))
57
+ if (url.pathname === '/api/deliveries') return sendJson(res, 200, data.listDeliveries(agentId))
58
+ if (url.pathname === '/api/stream') {
59
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' })
60
+ if (req.method === 'HEAD') return res.end()
61
+ res.write(': connected\n\n')
62
+ clients.add(res)
63
+ const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000)
64
+ heartbeats.add(heartbeat)
65
+ res.on('close', () => { clearInterval(heartbeat); heartbeats.delete(heartbeat); clients.delete(res) })
66
+ return
67
+ }
68
+ if (url.pathname.startsWith('/api/') || url.pathname === '/api') return sendJson(res, 404, { error: 'Not found' })
69
+ let pathname
70
+ try { pathname = decodeURIComponent(url.pathname) } catch { return sendJson(res, 400, { error: 'Invalid path' }) }
71
+ const root = resolve(assetsDir)
72
+ const target = resolve(root, '.' + pathname)
73
+ if (target !== root && !target.startsWith(root + sep)) return sendJson(res, 403, { error: 'Forbidden' })
74
+ const isAsset = pathname.startsWith('/assets/') || extname(pathname) !== ''
75
+ const file = isAsset ? target : resolve(root, 'index.html')
76
+ try {
77
+ const body = await readFile(file)
78
+ res.writeHead(200, {
79
+ 'Content-Type': mimeTypes[extname(file)] || 'application/octet-stream',
80
+ 'Cache-Control': pathname.startsWith('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache',
81
+ 'X-Content-Type-Options': 'nosniff',
82
+ }).end(req.method === 'HEAD' ? undefined : body)
83
+ } catch {
84
+ sendJson(res, isAsset ? 404 : 503, { error: isAsset ? 'Asset not found' : 'Console is not built. Run npm run build first, or use npm run dev.' })
85
+ }
86
+ }
87
+ function dispose() {
88
+ if (unsubscribe) unsubscribe()
89
+ for (const timer of heartbeats) clearInterval(timer)
90
+ for (const client of clients) client.end()
91
+ heartbeats.clear()
92
+ clients.clear()
93
+ data.dispose()
94
+ }
95
+ server.on('close', dispose)
96
+ return { server, dispose }
97
+ }
98
+
99
+ /** Back-compat wrapper — `npm run demo` and existing tests import this directly. */
100
+ export function createDemoServer({ assetsDir = defaultAssets, simulate = true } = {}) {
101
+ return createServer({ mode: 'demo', assetsDir, simulate })
102
+ }