wldx-cli 1.0.0 → 1.2.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 (42) hide show
  1. package/README.md +19 -22
  2. package/package.json +9 -6
  3. package/src/chat/format.mjs +40 -8
  4. package/src/chat/lib.mjs +71 -23
  5. package/src/chat/watch.mjs +46 -15
  6. package/src/cli.mjs +13 -252
  7. package/src/commands/calls.mjs +135 -32
  8. package/src/commands/channel.mjs +99 -21
  9. package/src/commands/chat.mjs +77 -22
  10. package/src/commands/conversations.mjs +146 -29
  11. package/src/commands/file.mjs +13 -5
  12. package/src/commands/inbox.mjs +20 -13
  13. package/src/commands/index.mjs +5 -29
  14. package/src/commands/live.mjs +82 -24
  15. package/src/commands/meetings.mjs +114 -27
  16. package/src/commands/phone.mjs +145 -33
  17. package/src/commands/reports.mjs +219 -44
  18. package/src/commands/section.mjs +49 -8
  19. package/src/commands/user.mjs +42 -13
  20. package/src/history/text.mjs +30 -4
  21. package/src/history/wda.mjs +20 -10
  22. package/src/live/socket.mjs +25 -7
  23. package/src/live/stats.mjs +41 -9
  24. package/src/live/text.mjs +122 -23
  25. package/src/live/views.mjs +93 -23
  26. package/src/phone/control.mjs +93 -33
  27. package/src/phone/events.mjs +38 -7
  28. package/src/phone/pbx.mjs +29 -11
  29. package/src/phone/sip.mjs +87 -17
  30. package/src/phone/xml.mjs +2 -1
  31. package/src/stream.mjs +10 -5
  32. package/src/version.mjs +0 -1
  33. package/src/auth.mjs +0 -274
  34. package/src/commands/login.mjs +0 -126
  35. package/src/commands/profile.mjs +0 -51
  36. package/src/commands/status.mjs +0 -95
  37. package/src/completion.mjs +0 -132
  38. package/src/errors.mjs +0 -30
  39. package/src/files.mjs +0 -98
  40. package/src/format.mjs +0 -82
  41. package/src/http.mjs +0 -64
  42. package/src/services.mjs +0 -58
@@ -1,7 +1,23 @@
1
1
  // Text formatters shared by wx calls, meetings and conversations: the AI summary, the insights, the people of a call.
2
- import {duration, oneLine} from '../format.mjs';
2
+ import {duration, oneLine} from 'wldx-core/format';
3
3
 
4
- const SECTIONS = ['topics', 'keyPoints', 'review', 'decisions', 'progress', 'issues', 'actionItems', 'nextSteps', 'customerInsights', 'customerResponse', 'salesProposals', 'competitiveInformation', 'implementationTimeline', 'budget', 'tags'];
4
+ const SECTIONS = [
5
+ 'topics',
6
+ 'keyPoints',
7
+ 'review',
8
+ 'decisions',
9
+ 'progress',
10
+ 'issues',
11
+ 'actionItems',
12
+ 'nextSteps',
13
+ 'customerInsights',
14
+ 'customerResponse',
15
+ 'salesProposals',
16
+ 'competitiveInformation',
17
+ 'implementationTimeline',
18
+ 'budget',
19
+ 'tags',
20
+ ];
5
21
 
6
22
  // "actionItems" -> "Action items"
7
23
  export const label = (key) => {
@@ -9,7 +25,14 @@ export const label = (key) => {
9
25
  return words.charAt(0).toUpperCase() + words.slice(1);
10
26
  };
11
27
 
12
- const valueText = (v) => (v === null || v === undefined ? '' : typeof v === 'object' ? (Array.isArray(v) && v.every((x) => typeof x !== 'object') ? v.join('; ') : JSON.stringify(v)) : String(v));
28
+ const valueText = (v) =>
29
+ v === null || v === undefined
30
+ ? ''
31
+ : typeof v === 'object'
32
+ ? Array.isArray(v) && v.every((x) => typeof x !== 'object')
33
+ ? v.join('; ')
34
+ : JSON.stringify(v)
35
+ : String(v);
13
36
 
14
37
  function bullets(title, items, indent = '') {
15
38
  const list = (items ?? []).filter((x) => x !== null && x !== undefined && x !== '');
@@ -21,7 +44,10 @@ function bullets(title, items, indent = '') {
21
44
  export function sectionLines(json, indent = '') {
22
45
  const lines = [];
23
46
  for (const key of SECTIONS) if (Array.isArray(json?.[key])) lines.push(...bullets(label(key), json[key], indent));
24
- if (json?.csat && (json.csat.score || json.csat.reason)) lines.push(`${indent}CSAT: ${[json.csat.score, json.csat.reason && oneLine(json.csat.reason, 300)].filter(Boolean).join(' · ')}`);
47
+ if (json?.csat && (json.csat.score || json.csat.reason))
48
+ lines.push(
49
+ `${indent}CSAT: ${[json.csat.score, json.csat.reason && oneLine(json.csat.reason, 300)].filter(Boolean).join(' · ')}`,
50
+ );
25
51
  return lines;
26
52
  }
27
53
 
@@ -3,12 +3,13 @@
3
3
  // commands of wx calls, meetings, conversations and reports share (ids, numbers, dates, the shapes of the skill).
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- import {HttpError, UsageError} from '../errors.mjs';
7
- import {refuseUnsafeWrite} from '../files.mjs';
8
- import {request} from '../http.mjs';
9
- import {wdaUrl} from '../services.mjs';
6
+ import {HttpError, UsageError} from 'wldx-core/errors';
7
+ import {refuseUnsafeWrite} from 'wldx-core/files';
8
+ import {request} from 'wldx-core/http';
9
+ import {wdaUrl} from 'wldx-core/services';
10
10
 
11
- const FORBIDDEN_HINT = 'The login has no access to this record (a meeting or call of other people, or a role without the right).';
11
+ const FORBIDDEN_HINT =
12
+ 'The login has no access to this record (a meeting or call of other people, or a role without the right).';
12
13
 
13
14
  // wdaClient({token, env, fetchImpl}) (for ctx.client): get(path), post(path, body), soft(path) (null on a 404).
14
15
  // The path is relative to /v2 and built with apiPath`` by the caller.
@@ -68,7 +69,8 @@ export function checkId(what, value) {
68
69
  // A conference id from an id or a link (the last path segment of .../insights/conferences/<id>).
69
70
  export function conferenceId(input) {
70
71
  const v = String(input).split(/[?#]/)[0].replace(/\/$/, '').split('/').pop();
71
- if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(v) || v.includes('..') || v === 'conferences') throw new UsageError(`not a conference id: ${input}`);
72
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(v) || v.includes('..') || v === 'conferences')
73
+ throw new UsageError(`not a conference id: ${input}`);
72
74
  return v;
73
75
  }
74
76
 
@@ -82,7 +84,8 @@ export function callFlow(positionals) {
82
84
  // A whole number from min to max (the services refuse anything else with a 400 that tells little).
83
85
  export function intOption(name, value, min, max) {
84
86
  if (value === undefined) return undefined;
85
- if (!/^\d{1,12}$/.test(value) || Number(value) < min || Number(value) > max) throw new UsageError(`${name} must be a whole number from ${min} to ${max}: ${value}`);
87
+ if (!/^\d{1,12}$/.test(value) || Number(value) < min || Number(value) > max)
88
+ throw new UsageError(`${name} must be a whole number from ${min} to ${max}: ${value}`);
86
89
  return Number(value);
87
90
  }
88
91
 
@@ -96,7 +99,8 @@ export function checkZone(value) {
96
99
  // is a time at all.
97
100
  export function checkIso(name, value) {
98
101
  if (value === undefined) return undefined;
99
- if (!/^\d{4}-\d{2}-\d{2}/.test(value) || Number.isNaN(Date.parse(value))) throw new UsageError(`${name} must be an ISO time (2026-11-07 or 2026-11-07T09:00:00Z): ${value}`);
102
+ if (!/^\d{4}-\d{2}-\d{2}/.test(value) || Number.isNaN(Date.parse(value)))
103
+ throw new UsageError(`${name} must be an ISO time (2026-11-07 or 2026-11-07T09:00:00Z): ${value}`);
100
104
  return value;
101
105
  }
102
106
 
@@ -127,7 +131,12 @@ export function compactInsights(r) {
127
131
 
128
132
  // The participants of a conference by name (or e-mail), each once.
129
133
  export const participantNames = (conference) =>
130
- unique((conference?.participants ?? []).map((p) => alt(p.info?.name, p.info?.email)).filter((n) => n !== null).map((n) => String(n).replace(/\s+/g, ' ').trim()));
134
+ unique(
135
+ (conference?.participants ?? [])
136
+ .map((p) => alt(p.info?.name, p.info?.email))
137
+ .filter((n) => n !== null)
138
+ .map((n) => String(n).replace(/\s+/g, ' ').trim()),
139
+ );
131
140
 
132
141
  // Writes a transcript to a private file (folders 0700, file 0600) and returns its absolute path. An existing path
133
142
  // is replaced only when it is a transcript of the same conference written earlier (its second line is
@@ -150,7 +159,8 @@ export function writePrivate(file, content, {cwd, conferenceId: id, env = proces
150
159
  } catch {
151
160
  second = undefined;
152
161
  }
153
- if (!id || second !== `# conferenceId: ${id}`) throw new Error(`${file} exists: choose another path (nothing was overwritten)`);
162
+ if (!id || second !== `# conferenceId: ${id}`)
163
+ throw new Error(`${file} exists: choose another path (nothing was overwritten)`);
154
164
  }
155
165
  fs.mkdirSync(path.dirname(target), {recursive: true, mode: 0o700});
156
166
  fs.writeFileSync(target, content, {mode: 0o600, flag: exists ? 'w' : 'wx'});
@@ -5,7 +5,7 @@
5
5
  // "data":[…]} frames, then {"event":"subscribed","topic"}; afterwards one broadcast per change.
6
6
  // An error is {"event":"error","message","code","source"}: `source` echoes the request, which for authorize holds
7
7
  // the token, so it is never printed (and the token is scrubbed from every message shown).
8
- import {AuthError} from '../errors.mjs';
8
+ import {AuthError} from 'wldx-core/errors';
9
9
 
10
10
  export const AUTH_TIMEOUT_MS = 20_000;
11
11
  export const SNAPSHOT_TIMEOUT_MS = 30_000;
@@ -27,8 +27,20 @@ export function explain(topic, d, license) {
27
27
  // snapshot(topic, extra) subscribes with the state and resolves with every item broadcast until "subscribed"
28
28
  // close() closes it (a _closed frame follows)
29
29
  // Rejects with AuthError when the service refuses the token, with Error when it cannot connect.
30
- export function connect({url, token, WebSocket = globalThis.WebSocket, license, timeoutMs = AUTH_TIMEOUT_MS, snapshotTimeoutMs = SNAPSHOT_TIMEOUT_MS}) {
31
- const scrub = (s) => (token ? String(s ?? '').split(token).join('[token]') : String(s ?? ''));
30
+ export function connect({
31
+ url,
32
+ token,
33
+ WebSocket = globalThis.WebSocket,
34
+ license,
35
+ timeoutMs = AUTH_TIMEOUT_MS,
36
+ snapshotTimeoutMs = SNAPSHOT_TIMEOUT_MS,
37
+ }) {
38
+ const scrub = (s) =>
39
+ token
40
+ ? String(s ?? '')
41
+ .split(token)
42
+ .join('[token]')
43
+ : String(s ?? '');
32
44
  return new Promise((resolve, reject) => {
33
45
  const listeners = new Set();
34
46
  let ws;
@@ -59,12 +71,18 @@ export function connect({url, token, WebSocket = globalThis.WebSocket, license,
59
71
  snapshot(topic, extra = {}) {
60
72
  return new Promise((ok, fail) => {
61
73
  const items = [];
62
- const timer = setTimeout(() => finish(new Error(`no answer for ${topic} in ${Math.round(snapshotTimeoutMs / 1000)} seconds`)), snapshotTimeoutMs);
74
+ const timer = setTimeout(
75
+ () => finish(new Error(`no answer for ${topic} in ${Math.round(snapshotTimeoutMs / 1000)} seconds`)),
76
+ snapshotTimeoutMs,
77
+ );
63
78
  const listener = (d) => {
64
- if (d.event === 'broadcast' && d.topic === topic) items.push(...(Array.isArray(d.data) ? d.data : [d.data]));
79
+ if (d.event === 'broadcast' && d.topic === topic)
80
+ items.push(...(Array.isArray(d.data) ? d.data : [d.data]));
65
81
  else if (d.event === 'subscribed' && d.topic === topic) finish();
66
- else if (d.event === 'error') finish(Object.assign(new Error(scrub(explain(topic, d, license))), {code: d.code}));
67
- else if (d.event === '_closed') finish(new Error(`the live service closed the connection (code ${d.code})`));
82
+ else if (d.event === 'error')
83
+ finish(Object.assign(new Error(scrub(explain(topic, d, license))), {code: d.code}));
84
+ else if (d.event === '_closed')
85
+ finish(new Error(`the live service closed the connection (code ${d.code})`));
68
86
  };
69
87
  function finish(err) {
70
88
  clearTimeout(timer);
@@ -10,7 +10,8 @@
10
10
  export const SLA_SECONDS = [5, 10, 15, 20, 25, 30];
11
11
  export const WINDOWS = ['today', '15', '30', '45', '60'];
12
12
 
13
- const round = (n, d = 0) => (n === null || n === undefined || !Number.isFinite(n) ? null : Math.round(n * 10 ** d) / 10 ** d);
13
+ const round = (n, d = 0) =>
14
+ n === null || n === undefined || !Number.isFinite(n) ? null : Math.round(n * 10 ** d) / 10 ** d;
14
15
  const ratio = (a, b) => (b ? a / b : null);
15
16
  const pct = (a, b) => round(b ? (a / b) * 100 : null, 1);
16
17
  const sec = (ms) => round(ms === null || ms === undefined ? null : ms / 1000);
@@ -72,10 +73,32 @@ export function queueFigures(s, now = Date.now()) {
72
73
  }
73
74
 
74
75
  const SUMMED = [
75
- 'offered', 'outbound', 'answered', 'unanswered', 'unknown', 'abandoned', 'shortAbandoned', 'transferred',
76
- 'answeredWaitSumMs', 'abandonedWaitSumMs', 'unansweredWaitSumMs', 'waitCount', 'talkSumMs', 'talkCount',
77
- 'acwSumMs', 'ringSumMs', 'ringCount', 'answeredWithinSl', 'callsWaiting', 'callsActive', 'agentsTotal',
78
- 'agentsAvailable', 'agentsTalking', 'agentsPaused', 'agentsWrapUp', 'agentsLoggedOff',
76
+ 'offered',
77
+ 'outbound',
78
+ 'answered',
79
+ 'unanswered',
80
+ 'unknown',
81
+ 'abandoned',
82
+ 'shortAbandoned',
83
+ 'transferred',
84
+ 'answeredWaitSumMs',
85
+ 'abandonedWaitSumMs',
86
+ 'unansweredWaitSumMs',
87
+ 'waitCount',
88
+ 'talkSumMs',
89
+ 'talkCount',
90
+ 'acwSumMs',
91
+ 'ringSumMs',
92
+ 'ringCount',
93
+ 'answeredWithinSl',
94
+ 'callsWaiting',
95
+ 'callsActive',
96
+ 'agentsTotal',
97
+ 'agentsAvailable',
98
+ 'agentsTalking',
99
+ 'agentsPaused',
100
+ 'agentsWrapUp',
101
+ 'agentsLoggedOff',
79
102
  ];
80
103
 
81
104
  // The counters of several queues added up (counters first, rates after: never an average of rates). An agent in
@@ -121,7 +144,9 @@ export function agentStatsByExtension(services) {
121
144
  const out = new Map();
122
145
  for (const s of services) {
123
146
  for (const [id, a] of Object.entries(s.stats?.agentStats ?? {})) {
124
- const ext = s.agents?.find((x) => x.id === id)?.userExtension ?? (id.startsWith(`${s.id}_`) ? id.slice(s.id.length + 1) : id);
147
+ const ext =
148
+ s.agents?.find((x) => x.id === id)?.userExtension ??
149
+ (id.startsWith(`${s.id}_`) ? id.slice(s.id.length + 1) : id);
125
150
  const prev = out.get(ext);
126
151
  if (!prev) out.set(ext, {...a});
127
152
  else for (const [k, v] of Object.entries(a)) if (typeof v === 'number') prev[k] = (prev[k] ?? 0) + v;
@@ -152,7 +177,8 @@ export function applyServiceEvent(queues, raw, now = Date.now()) {
152
177
  return;
153
178
  }
154
179
  const next = q ?? {id, agents: [], calls: new Map()};
155
- for (const k of ['pbx', 'name', 'strategy', 'maxCalls', 'aclGroup', 'timeZone']) if (raw[k] !== undefined) next[k] = raw[k];
180
+ for (const k of ['pbx', 'name', 'strategy', 'maxCalls', 'aclGroup', 'timeZone'])
181
+ if (raw[k] !== undefined) next[k] = raw[k];
156
182
  const agents = Array.isArray(raw.agents) ? raw.agents : [];
157
183
  if (trigger === 'service.agent_leave') {
158
184
  const gone = new Set(agents.map((a) => a.id));
@@ -163,7 +189,12 @@ export function applyServiceEvent(queues, raw, now = Date.now()) {
163
189
  if (i < 0) continue;
164
190
  const prev = next.agents[i];
165
191
  const merged = {...prev, ...Object.fromEntries(Object.entries(a).filter(([, v]) => v !== undefined))};
166
- if (merged.status !== prev.status || String(merged.paused) !== String(prev.paused) || merged.reason !== prev.reason) merged.since = raw.time ?? now;
192
+ if (
193
+ merged.status !== prev.status ||
194
+ String(merged.paused) !== String(prev.paused) ||
195
+ merged.reason !== prev.reason
196
+ )
197
+ merged.since = raw.time ?? now;
167
198
  next.agents[i] = merged;
168
199
  }
169
200
  } else if (trigger === 'service.agent_join' && q) {
@@ -181,7 +212,8 @@ export function applyServiceEvent(queues, raw, now = Date.now()) {
181
212
  // has left it; answered calls are not listed and stay until their call_end.
182
213
  if (Array.isArray(raw.calls) && (trigger === 'service.init' || trigger === 'service.update')) {
183
214
  const listed = new Set(raw.calls.map(String));
184
- for (const [cid, c] of next.calls) if (!c.answeredBy && c.direction !== 'outgoing' && !listed.has(cid)) next.calls.delete(cid);
215
+ for (const [cid, c] of next.calls)
216
+ if (!c.answeredBy && c.direction !== 'outgoing' && !listed.has(cid)) next.calls.delete(cid);
185
217
  for (const cid of listed) if (!next.calls.has(cid)) next.calls.set(cid, {callId: cid, direction: 'incoming'});
186
218
  }
187
219
  if (raw.stats) next.stats = raw.stats;
package/src/live/text.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // Text output of `wx live`: queues, agents, conversations and presence as aligned tables, the events of `watch` and
2
2
  // `transcript --follow` as one readable line each. The JSON output is the data itself (src/live/views.mjs).
3
- import {duration, localTime, oneLine, paint, table} from '../format.mjs';
3
+ import {duration, localTime, oneLine, paint, table} from 'wldx-core/format';
4
4
 
5
5
  // Agents are listed under their queue when this many queues or fewer are shown (else the table of queues only).
6
6
  export const AGENT_LIST_MAX = 10;
@@ -42,12 +42,23 @@ export function figuresLine(s) {
42
42
  `ASA ${secs(s.asaSeconds)}`,
43
43
  `talk ${secs(s.avgTalkSeconds)}`,
44
44
  ];
45
- if (s.now) parts.push(`waiting ${num(s.now.waiting)}${s.now.longestWaitSeconds ? ` (longest ${duration(s.now.longestWaitSeconds)})` : ''}`, `talking ${num(s.now.talking)}`);
45
+ if (s.now)
46
+ parts.push(
47
+ `waiting ${num(s.now.waiting)}${s.now.longestWaitSeconds ? ` (longest ${duration(s.now.longestWaitSeconds)})` : ''}`,
48
+ `talking ${num(s.now.talking)}`,
49
+ );
46
50
  return parts.join(' · ');
47
51
  }
48
52
 
49
53
  function agentsTable(agents, c, withStats) {
50
- const head = ['AGENT', 'EXT', 'STATUS', 'REASON', 'MESSAGE', ...(withStats ? ['ANSWERED', 'MISSED', 'OCCUPANCY'] : [])];
54
+ const head = [
55
+ 'AGENT',
56
+ 'EXT',
57
+ 'STATUS',
58
+ 'REASON',
59
+ 'MESSAGE',
60
+ ...(withStats ? ['ANSWERED', 'MISSED', 'OCCUPANCY'] : []),
61
+ ];
51
62
  const rows = agents.map((a) => [
52
63
  a.name ?? '',
53
64
  a.extension ?? a.userExtension ?? '',
@@ -64,11 +75,21 @@ export function queuesText(r, ctx) {
64
75
  const withStats = Boolean(r.stats);
65
76
  const lines = [];
66
77
  const pbxCount = r.pbxs ? Object.keys(r.pbxs).length : 0;
67
- lines.push(`${r.total} queue${r.total === 1 ? '' : 's'}${pbxCount ? ` on ${pbxCount} PBXs` : ''}, ${r.callsWaitingOrTalking} call${r.callsWaitingOrTalking === 1 ? '' : 's'} waiting or talking`);
78
+ lines.push(
79
+ `${r.total} queue${r.total === 1 ? '' : 's'}${pbxCount ? ` on ${pbxCount} PBXs` : ''}, ${r.callsWaitingOrTalking} call${r.callsWaitingOrTalking === 1 ? '' : 's'} waiting or talking`,
80
+ );
68
81
  if (withStats) lines.push(`All shown ${windowLabel(r.stats.window)}: ${figuresLine(r.stats)}`);
69
82
  if (!r.queues?.length) return lines;
70
83
  lines.push('');
71
- const head = ['QUEUE', 'CALLS', 'AVAILABLE', 'PAUSED', 'STRATEGY', ...(pbxCount ? ['PBX'] : []), ...(withStats ? ['OFFERED', 'ANSWERED', 'ABANDONED', 'SL', 'ASA', 'WAITING'] : [])];
84
+ const head = [
85
+ 'QUEUE',
86
+ 'CALLS',
87
+ 'AVAILABLE',
88
+ 'PAUSED',
89
+ 'STRATEGY',
90
+ ...(pbxCount ? ['PBX'] : []),
91
+ ...(withStats ? ['OFFERED', 'ANSWERED', 'ABANDONED', 'SL', 'ASA', 'WAITING'] : []),
92
+ ];
72
93
  const rows = r.queues.map((q) => {
73
94
  const agents = q.agents ?? [];
74
95
  const available = agents.filter((a) => a.status === 'available' && !isPaused(a)).length;
@@ -81,16 +102,31 @@ export function queuesText(r, ctx) {
81
102
  paused ? c.yellow(String(paused)) : '0',
82
103
  q.strategy ?? '',
83
104
  ...(pbxCount ? [q.pbx ?? ''] : []),
84
- ...(withStats ? [num(s?.offered), num(s?.answered), num(s?.abandoned), percent(s?.serviceLevel), secs(s?.asaSeconds), num(s?.now?.waiting)] : []),
105
+ ...(withStats
106
+ ? [
107
+ num(s?.offered),
108
+ num(s?.answered),
109
+ num(s?.abandoned),
110
+ percent(s?.serviceLevel),
111
+ secs(s?.asaSeconds),
112
+ num(s?.now?.waiting),
113
+ ]
114
+ : []),
85
115
  ];
86
116
  });
87
117
  lines.push(...table([head.map((h) => c.dim(h)), ...rows]));
88
118
  if (r.queues.length > AGENT_LIST_MAX) {
89
- ctx?.info?.(`The agents are listed for ${AGENT_LIST_MAX} queues or fewer: narrow with --name, --busy, --paused or --active, or use --output json.`);
119
+ ctx?.info?.(
120
+ `The agents are listed for ${AGENT_LIST_MAX} queues or fewer: narrow with --name, --busy, --paused or --active, or use --output json.`,
121
+ );
90
122
  return lines;
91
123
  }
92
124
  for (const q of r.queues) {
93
- lines.push('', c.bold(`${q.name ?? q.id}`) + c.dim(` · ${q.strategy ?? ''}${q.maxCalls ? ` · max ${q.maxCalls} calls` : ''} · ${q.id}`));
125
+ lines.push(
126
+ '',
127
+ c.bold(`${q.name ?? q.id}`) +
128
+ c.dim(` · ${q.strategy ?? ''}${q.maxCalls ? ` · max ${q.maxCalls} calls` : ''} · ${q.id}`),
129
+ );
94
130
  if (Array.isArray(q.callIds) && q.callIds.length) lines.push(` calls: ${q.callIds.join(', ')}`);
95
131
  if (q.figures ?? q.stats) lines.push(` ${figuresLine(q.figures ?? q.stats)}`);
96
132
  if (q.agents?.length) lines.push(...agentsTable(q.agents, c, withStats));
@@ -106,26 +142,54 @@ function conversationCells(item) {
106
142
  if (item.type === 'call' || item.event === 'call') {
107
143
  const f = lastFlow(item) ?? {};
108
144
  const legs = Array.isArray(item.flows) && item.flows.length > 1 ? ` · ${item.flows.length} legs` : '';
109
- return ['call', f.status ?? '', `${who(f.caller)} → ${who(f.callee)}`, [f.direction, f.queue && `queue ${f.queue}`, f.transcription && `transcription ${f.transcription}`].filter(Boolean).join(' · ') + legs];
145
+ return [
146
+ 'call',
147
+ f.status ?? '',
148
+ `${who(f.caller)} → ${who(f.callee)}`,
149
+ [f.direction, f.queue && `queue ${f.queue}`, f.transcription && `transcription ${f.transcription}`]
150
+ .filter(Boolean)
151
+ .join(' · ') + legs,
152
+ ];
110
153
  }
111
154
  const sentiment = item.insights?.sentiment !== undefined ? `sentiment ${item.insights.sentiment}` : '';
112
155
  if (item.type === 'chat' || item.event === 'chat') {
113
- const people = (item.customers ?? []).map((p) => p.name).filter(Boolean).join(', ');
114
- return ['chat', item.status ?? '', people, [item.subject && oneLine(item.subject, 60), sentiment].filter(Boolean).join(' · ')];
156
+ const people = (item.customers ?? [])
157
+ .map((p) => p.name)
158
+ .filter(Boolean)
159
+ .join(', ');
160
+ return [
161
+ 'chat',
162
+ item.status ?? '',
163
+ people,
164
+ [item.subject && oneLine(item.subject, 60), sentiment].filter(Boolean).join(' · '),
165
+ ];
115
166
  }
116
167
  if (item.type === 'conference' || item.event === 'conference') {
117
- return ['conference', '', `${item.participants?.length ?? 0} participants`, [item.subject && oneLine(item.subject, 60), sentiment].filter(Boolean).join(' · ')];
168
+ return [
169
+ 'conference',
170
+ '',
171
+ `${item.participants?.length ?? 0} participants`,
172
+ [item.subject && oneLine(item.subject, 60), sentiment].filter(Boolean).join(' · '),
173
+ ];
118
174
  }
119
175
  return [String(item.type ?? item.event ?? ''), '', '', item.key ?? ''];
120
176
  }
121
177
 
122
178
  export function conversationsText(r, ctx) {
123
179
  const c = paint(ctx);
124
- const counts = Object.entries(r.byType ?? {}).map(([k, n]) => `${k}s ${n}`).join(', ');
125
- const lines = [`${r.total} in progress${counts ? ` (all: ${counts})` : ''}${r.shown < r.total ? `, newest ${r.shown} shown` : ''}`];
180
+ const counts = Object.entries(r.byType ?? {})
181
+ .map(([k, n]) => `${k}s ${n}`)
182
+ .join(', ');
183
+ const lines = [
184
+ `${r.total} in progress${counts ? ` (all: ${counts})` : ''}${r.shown < r.total ? `, newest ${r.shown} shown` : ''}`,
185
+ ];
126
186
  if (!r.items?.length) return lines;
127
187
  lines.push('');
128
- const rows = r.items.map((item) => [localTime(item.start), ...conversationCells(item).map((v, i) => (i === 1 && v ? c.cyan(v) : oneLine(v, 80))), c.dim(item.id ?? '')]);
188
+ const rows = r.items.map((item) => [
189
+ localTime(item.start),
190
+ ...conversationCells(item).map((v, i) => (i === 1 && v ? c.cyan(v) : oneLine(v, 80))),
191
+ c.dim(item.id ?? ''),
192
+ ]);
129
193
  lines.push(...table([['STARTED', 'TYPE', 'STATUS', 'WHO', 'DETAIL', 'ID'].map((h) => c.dim(h)), ...rows]));
130
194
  return lines;
131
195
  }
@@ -140,11 +204,19 @@ function presenceStatus(status, c) {
140
204
 
141
205
  export function presenceText(r, ctx) {
142
206
  const c = paint(ctx);
143
- const rows = (r.presence ?? []).map((p) => [p.extension ?? '', presenceStatus(p.status, c), oneLine(p.message ?? '', 50), p.until ? localTime(p.until) : '', p.phone ?? p.telephony ?? '', p.company ? `company ${p.company}` : '']);
207
+ const rows = (r.presence ?? []).map((p) => [
208
+ p.extension ?? '',
209
+ presenceStatus(p.status, c),
210
+ oneLine(p.message ?? '', 50),
211
+ p.until ? localTime(p.until) : '',
212
+ p.phone ?? p.telephony ?? '',
213
+ p.company ? `company ${p.company}` : '',
214
+ ]);
144
215
  return table([['EXT', 'STATUS', 'MESSAGE', 'UNTIL', 'PHONE', ''].map((h) => c.dim(h)), ...rows]);
145
216
  }
146
217
 
147
- const textLine = (t) => `${t.at !== undefined ? `[${clockTime(t.at)}] ` : ''}${t.name ?? t.speaker ?? '?'}: ${t.text ?? ''}`;
218
+ const textLine = (t) =>
219
+ `${t.at !== undefined ? `[${clockTime(t.at)}] ` : ''}${t.name ?? t.speaker ?? '?'}: ${t.text ?? ''}`;
148
220
 
149
221
  export function transcriptText(r, ctx) {
150
222
  const c = paint(ctx);
@@ -171,7 +243,10 @@ export function eventText(e, ctx) {
171
243
  what = `watching ${(d.topics ?? []).join(', ') || 'nothing'}${d.calls ? ' (calls only)' : ''}${d.mine ? ' (my calls)' : ''}${d.extension ? ` · extension ${d.extension}` : ''}${d.license ? ` · ${d.license}` : ''}`;
172
244
  break;
173
245
  case 'RECONNECTING':
174
- what = d.reason === 'token' ? 'renewing the login' : `connection lost (code ${d.code}), attempt ${d.attempt} in ${d.inSeconds} s`;
246
+ what =
247
+ d.reason === 'token'
248
+ ? 'renewing the login'
249
+ : `connection lost (code ${d.code}), attempt ${d.attempt} in ${d.inSeconds} s`;
175
250
  break;
176
251
  case 'ERROR':
177
252
  what = c.red(`${d.message}${d.code ? ` (${d.code})` : ''}`);
@@ -180,18 +255,39 @@ export function eventText(e, ctx) {
180
255
  what = `${d.queues} queues known${d.stats ? ` · stats ${d.stats.periodMinutes ? `last ${d.stats.periodMinutes} min` : 'today'}, SLA ${d.stats.serviceLevelSeconds} s` : ''}`;
181
256
  break;
182
257
  case 'QUEUE': {
183
- const agents = (d.agents ?? []).map((a) => `${a.name ?? a.extension ?? a.userExtension ?? '?'} ${isPaused(a) ? 'paused' : a.status ?? ''}${a.reason ? ` ${a.reason}` : ''}${a.message ? ` "${oneLine(a.message, 30)}"` : ''}`.trim());
184
- what = [d.trigger ?? d.eventTrigger, d.name ?? d.id, d.calls ? `${d.calls} calls` : '', agents.join('; ')].filter(Boolean).join(' · ');
258
+ const agents = (d.agents ?? []).map((a) =>
259
+ `${a.name ?? a.extension ?? a.userExtension ?? '?'} ${isPaused(a) ? 'paused' : (a.status ?? '')}${a.reason ? ` ${a.reason}` : ''}${a.message ? ` "${oneLine(a.message, 30)}"` : ''}`.trim(),
260
+ );
261
+ what = [d.trigger ?? d.eventTrigger, d.name ?? d.id, d.calls ? `${d.calls} calls` : '', agents.join('; ')]
262
+ .filter(Boolean)
263
+ .join(' · ');
185
264
  break;
186
265
  }
187
266
  case 'QUEUE_CALL':
188
- what = [d.trigger ?? d.eventTrigger, `queue ${d.queueId ?? d.id}`, `call ${d.callId}`, d.direction, d.customer && (typeof d.customer === 'object' ? d.customer.phone ?? d.customer.name : d.customer), d.agents !== undefined && `${d.agents} agents`].filter(Boolean).join(' · ');
267
+ what = [
268
+ d.trigger ?? d.eventTrigger,
269
+ `queue ${d.queueId ?? d.id}`,
270
+ `call ${d.callId}`,
271
+ d.direction,
272
+ d.customer && (typeof d.customer === 'object' ? (d.customer.phone ?? d.customer.name) : d.customer),
273
+ d.agents !== undefined && `${d.agents} agents`,
274
+ ]
275
+ .filter(Boolean)
276
+ .join(' · ');
189
277
  break;
190
278
  case 'QUEUE_STATS':
191
279
  what = `${d.name ?? d.id}: ${figuresLine(d)}`;
192
280
  break;
193
281
  case 'PRESENCE':
194
- what = [d.extension, d.status, d.message && `"${oneLine(d.message, 40)}"`, d.until && `until ${localTime(d.until)}`, d.phone ?? d.telephony].filter(Boolean).join(' · ');
282
+ what = [
283
+ d.extension,
284
+ d.status,
285
+ d.message && `"${oneLine(d.message, 40)}"`,
286
+ d.until && `until ${localTime(d.until)}`,
287
+ d.phone ?? d.telephony,
288
+ ]
289
+ .filter(Boolean)
290
+ .join(' · ');
195
291
  break;
196
292
  case 'SNAPSHOT':
197
293
  what = `${d.type} ${d.id}: ${d.transcript?.length ?? 0} lines so far${(d.transcript ?? []).map((t) => `\n ${textLine(t)}`).join('')}`;
@@ -209,7 +305,10 @@ export function eventText(e, ctx) {
209
305
  if (/^(CALL|CHAT|CONFERENCE)(_|$)/.test(e.event)) {
210
306
  const [, status, people, detail] = conversationCells(d);
211
307
  const extra = d.key ? `${d.key}${d.insights ? ` ${oneLine(JSON.stringify(d.insights), 80)}` : ''}` : '';
212
- what = [d.trigger, status, people, detail, extra, c.dim(d.id ?? '')].filter(Boolean).map((v) => oneLine(v, 100)).join(' · ');
308
+ what = [d.trigger, status, people, detail, extra, c.dim(d.id ?? '')]
309
+ .filter(Boolean)
310
+ .map((v) => oneLine(v, 100))
311
+ .join(' · ');
213
312
  } else {
214
313
  what = oneLine(JSON.stringify(d), 140);
215
314
  }