wldx-cli 1.0.0 → 1.1.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.
- package/README.md +15 -20
- package/package.json +9 -6
- package/src/chat/format.mjs +40 -8
- package/src/chat/lib.mjs +71 -23
- package/src/chat/watch.mjs +46 -15
- package/src/cli.mjs +13 -252
- package/src/commands/calls.mjs +135 -32
- package/src/commands/channel.mjs +99 -21
- package/src/commands/chat.mjs +77 -22
- package/src/commands/conversations.mjs +146 -29
- package/src/commands/file.mjs +13 -5
- package/src/commands/inbox.mjs +20 -13
- package/src/commands/index.mjs +5 -29
- package/src/commands/live.mjs +82 -24
- package/src/commands/meetings.mjs +114 -27
- package/src/commands/phone.mjs +145 -33
- package/src/commands/reports.mjs +219 -44
- package/src/commands/section.mjs +49 -8
- package/src/commands/user.mjs +42 -13
- package/src/history/text.mjs +30 -4
- package/src/history/wda.mjs +20 -10
- package/src/live/socket.mjs +25 -7
- package/src/live/stats.mjs +41 -9
- package/src/live/text.mjs +122 -23
- package/src/live/views.mjs +93 -23
- package/src/phone/control.mjs +93 -33
- package/src/phone/events.mjs +38 -7
- package/src/phone/pbx.mjs +29 -11
- package/src/phone/sip.mjs +87 -17
- package/src/phone/xml.mjs +2 -1
- package/src/stream.mjs +10 -5
- package/src/version.mjs +0 -1
- package/src/auth.mjs +0 -274
- package/src/commands/login.mjs +0 -126
- package/src/commands/profile.mjs +0 -51
- package/src/commands/status.mjs +0 -95
- package/src/completion.mjs +0 -132
- package/src/errors.mjs +0 -30
- package/src/files.mjs +0 -98
- package/src/format.mjs +0 -82
- package/src/http.mjs +0 -64
- package/src/services.mjs +0 -58
package/src/history/text.mjs
CHANGED
|
@@ -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 '
|
|
2
|
+
import {duration, oneLine} from 'wldx-core/format';
|
|
3
3
|
|
|
4
|
-
const SECTIONS = [
|
|
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) =>
|
|
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))
|
|
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
|
|
package/src/history/wda.mjs
CHANGED
|
@@ -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 '
|
|
7
|
-
import {refuseUnsafeWrite} from '
|
|
8
|
-
import {request} from '
|
|
9
|
-
import {wdaUrl} from '
|
|
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 =
|
|
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')
|
|
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)
|
|
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)))
|
|
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(
|
|
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}`)
|
|
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'});
|
package/src/live/socket.mjs
CHANGED
|
@@ -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 '
|
|
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({
|
|
31
|
-
|
|
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(
|
|
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)
|
|
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')
|
|
67
|
-
|
|
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);
|
package/src/live/stats.mjs
CHANGED
|
@@ -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) =>
|
|
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',
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
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 =
|
|
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'])
|
|
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 (
|
|
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)
|
|
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 '
|
|
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)
|
|
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 = [
|
|
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(
|
|
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 = [
|
|
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
|
|
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?.(
|
|
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(
|
|
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 [
|
|
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 ?? [])
|
|
114
|
-
|
|
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 [
|
|
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 ?? {})
|
|
125
|
-
|
|
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) => [
|
|
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) => [
|
|
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) =>
|
|
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 =
|
|
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) =>
|
|
184
|
-
|
|
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 = [
|
|
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 = [
|
|
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 ?? '')]
|
|
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
|
}
|