wldx-cli 0.1.1 → 1.0.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 +272 -23
- package/package.json +1 -1
- package/src/auth.mjs +181 -35
- package/src/chat/format.mjs +97 -0
- package/src/chat/lib.mjs +162 -0
- package/src/chat/watch.mjs +251 -0
- package/src/cli.mjs +167 -47
- package/src/commands/calls.mjs +367 -0
- package/src/commands/channel.mjs +326 -0
- package/src/commands/chat.mjs +388 -60
- package/src/commands/conversations.mjs +251 -0
- package/src/commands/file.mjs +72 -0
- package/src/commands/inbox.mjs +101 -0
- package/src/commands/index.mjs +54 -9
- package/src/commands/live.mjs +404 -0
- package/src/commands/login.mjs +126 -0
- package/src/commands/meetings.mjs +299 -0
- package/src/commands/phone.mjs +452 -0
- package/src/commands/profile.mjs +51 -0
- package/src/commands/reports.mjs +431 -0
- package/src/commands/section.mjs +60 -0
- package/src/commands/status.mjs +95 -0
- package/src/commands/user.mjs +130 -0
- package/src/completion.mjs +47 -20
- package/src/files.mjs +98 -0
- package/src/format.mjs +82 -0
- package/src/history/text.mjs +60 -0
- package/src/history/wda.mjs +161 -0
- package/src/http.mjs +30 -13
- package/src/live/socket.mjs +121 -0
- package/src/live/stats.mjs +211 -0
- package/src/live/text.mjs +219 -0
- package/src/live/views.mjs +321 -0
- package/src/phone/control.mjs +377 -0
- package/src/phone/events.mjs +189 -0
- package/src/phone/pbx.mjs +140 -0
- package/src/phone/sip.mjs +316 -0
- package/src/phone/xml.mjs +56 -0
- package/src/services.mjs +58 -0
- package/src/stream.mjs +4 -11
- package/src/commands/auth.mjs +0 -32
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// wx calls: the user's own call history (as the PBX recorded it) and one call after the fact: its record, the
|
|
2
|
+
// transcript, the AI summary and insights, the characteristics, the recordings. Read only, except
|
|
3
|
+
// schedule-summary (asks the service to generate the summary again). Data of WDA History and WDA Insights.
|
|
4
|
+
import {UsageError} from '../errors.mjs';
|
|
5
|
+
import {localTime, oneLine, table} from '../format.mjs';
|
|
6
|
+
import {apiPath} from '../http.mjs';
|
|
7
|
+
import {insightLines, millis, party, secs, summaryLines} from '../history/text.mjs';
|
|
8
|
+
import {NoDataError, alt, callFlow, checkIso, compactInsights, intOption, isoSeconds, optional, wdaClient, writePrivate} from '../history/wda.mjs';
|
|
9
|
+
|
|
10
|
+
const CALL_ARGS = '<callId> [flow]';
|
|
11
|
+
|
|
12
|
+
const callPath = ({callId, flow}) => apiPath`/${callId}/flows/${String(flow)}`;
|
|
13
|
+
|
|
14
|
+
// The user call history gives durations in seconds; the record of one call in milliseconds. The start and the end
|
|
15
|
+
// are milliseconds in both, so the length of a call comes from them when they are there.
|
|
16
|
+
const lengthSec = (c) => (Number.isFinite(c.startTime) && Number.isFinite(c.endTime) ? Math.round((c.endTime - c.startTime) / 1000) : Math.round(c.duration ?? 0));
|
|
17
|
+
|
|
18
|
+
const hasRecording = (c) => (c.recordingsData ?? []).length > 0 || (c.attachments ?? []).some((a) => a?.recording);
|
|
19
|
+
|
|
20
|
+
// One row of `calls list --brief`.
|
|
21
|
+
export function briefHistoryCall(c) {
|
|
22
|
+
return {
|
|
23
|
+
id: c.id ?? null,
|
|
24
|
+
flowIndex: c.flowIndex ?? null,
|
|
25
|
+
start: isoSeconds(c.startTime),
|
|
26
|
+
durationSec: lengthSec(c),
|
|
27
|
+
direction: c.direction ?? null,
|
|
28
|
+
status: c.callStatus ?? null,
|
|
29
|
+
from: alt(c.caller?.name, c.caller?.phone, c.caller?.userExtension),
|
|
30
|
+
to: alt(c.callee?.name, c.callee?.phone, c.callee?.userExtension),
|
|
31
|
+
transcriptionStatus: c.transcriptionStatus ?? null,
|
|
32
|
+
hasRecording: hasRecording(c),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const person = (p) => (p ? {name: p.name ?? null, phone: p.phone ?? null, extension: p.userExtension ?? null, email: p.email ?? null} : {name: null, phone: null, extension: null, email: null});
|
|
37
|
+
|
|
38
|
+
// `calls brief`: the record, the summary and the insights as one readable object.
|
|
39
|
+
export function briefCall(id, rec, sum, ins) {
|
|
40
|
+
const c = rec?.call ?? {};
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
flowIndex: c.flowIndex ?? null,
|
|
44
|
+
start: isoSeconds(c.startTime),
|
|
45
|
+
durationSec: Math.round((c.duration ?? 0) / 1000),
|
|
46
|
+
talkSec: Math.round((c.talkTime ?? 0) / 1000),
|
|
47
|
+
direction: c.direction ?? null,
|
|
48
|
+
status: c.callStatus ?? null,
|
|
49
|
+
caller: person(c.caller),
|
|
50
|
+
callee: person(c.callee),
|
|
51
|
+
queue: c.queueName ?? null,
|
|
52
|
+
service: c.service ?? null,
|
|
53
|
+
tags: c.tags ?? [],
|
|
54
|
+
transcriptionStatus: c.transcriptionStatus ?? null,
|
|
55
|
+
hasRecording: hasRecording(c),
|
|
56
|
+
summary: sum?.status === 'SUCCEEDED' ? {title: sum.summary?.title ?? null, brief: sum.summary?.brief ?? null} : {status: sum?.status ?? 'UNKNOWN'},
|
|
57
|
+
insights: compactInsights(ins),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The recordings of a record: the files on the PBX (attachments, else the older recordingsData), each once.
|
|
62
|
+
export function recordingsOf(c) {
|
|
63
|
+
const out = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const r of [...(c.attachments ?? []).map((a) => a?.recording).filter(Boolean), ...(c.recordingsData ?? [])]) {
|
|
66
|
+
const key = r.fileName ?? r.url;
|
|
67
|
+
if (seen.has(key)) continue;
|
|
68
|
+
seen.add(key);
|
|
69
|
+
out.push({fileName: r.fileName ?? null, url: r.url ?? null, start: r.start ?? null, end: r.end ?? null});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The history filter of `list` and `missed`.
|
|
75
|
+
const LIST_OPTIONS = {
|
|
76
|
+
direction: {type: 'string', choices: ['INTERNAL', 'INBOUND', 'OUTBOUND'], description: 'only calls of this direction'},
|
|
77
|
+
status: {type: 'string', choices: ['COMPLETED', 'MISSED'], description: 'only answered (COMPLETED) or missed calls'},
|
|
78
|
+
flag: {type: 'string', multiple: true, choices: ['FAX', 'VOICEMAIL'], description: 'only calls with a fax or a voicemail'},
|
|
79
|
+
from: {type: 'string', placeholder: 'ISO', description: 'calls from this time (ISO, e.g. 2026-11-01T00:00:00Z)'},
|
|
80
|
+
to: {type: 'string', placeholder: 'ISO', description: 'calls up to this time (ISO)'},
|
|
81
|
+
search: {type: 'string', placeholder: 'TEXT', description: 'a name or a number to search for'},
|
|
82
|
+
tag: {type: 'string', multiple: true, placeholder: 'TAG', description: 'only calls with this tag'},
|
|
83
|
+
'has-attachment': {type: 'boolean', description: 'only calls with an attachment (a recording, a fax, a voicemail)'},
|
|
84
|
+
'user-direction': {type: 'string', choices: ['INCOMING', 'OUTGOING'], description: 'only calls you received or placed'},
|
|
85
|
+
limit: {type: 'string', placeholder: '1-100', description: 'calls per page (50 by default, at most 100)'},
|
|
86
|
+
offset: {type: 'string', placeholder: 'N', description: 'skip this many calls (the next page)'},
|
|
87
|
+
brief: {type: 'boolean', description: 'one short object per call: id, start, duration, direction, status, from, to'},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
async function listCalls(ctx, status) {
|
|
91
|
+
const v = ctx.values;
|
|
92
|
+
const limit = intOption('--limit', v.limit, 1, 100);
|
|
93
|
+
const offset = intOption('--offset', v.offset, 0, 1_000_000);
|
|
94
|
+
const filter = {};
|
|
95
|
+
if (v.direction) filter.direction = v.direction;
|
|
96
|
+
if (status) filter.status = status;
|
|
97
|
+
if (v.flag?.length) filter.flags = v.flag;
|
|
98
|
+
if (v.from) filter.from = checkIso('--from', v.from);
|
|
99
|
+
if (v.to) filter.to = checkIso('--to', v.to);
|
|
100
|
+
if (v.search) filter.search = v.search;
|
|
101
|
+
if (v.tag?.length) filter.tags = v.tag;
|
|
102
|
+
if (v['has-attachment']) filter.hasAttachment = true;
|
|
103
|
+
if (v['user-direction']) filter.userDirection = v['user-direction'];
|
|
104
|
+
const body = {filter};
|
|
105
|
+
if (limit !== undefined) body.limit = limit;
|
|
106
|
+
if (offset !== undefined) body.offset = offset;
|
|
107
|
+
const r = await ctx.client(wdaClient).post('/history/user/calls', body);
|
|
108
|
+
return v.brief ? {calls: (r.calls ?? []).map(briefHistoryCall)} : r;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function listText(data, ctx) {
|
|
112
|
+
const rows = (data.calls ?? []).map((c) => (ctx.values.brief ? c : briefHistoryCall(c)));
|
|
113
|
+
if (!rows.length) return 'No calls.';
|
|
114
|
+
return table(
|
|
115
|
+
rows.map((c) => [
|
|
116
|
+
localTime(c.start),
|
|
117
|
+
c.direction ?? '',
|
|
118
|
+
c.status ?? '',
|
|
119
|
+
`${oneLine(c.from ?? '?', 40)} → ${oneLine(c.to ?? '?', 40)}`,
|
|
120
|
+
secs(c.durationSec),
|
|
121
|
+
[c.transcriptionStatus === 'AVAILABLE' && 'transcript', c.hasRecording && 'recording'].filter(Boolean).join(' '),
|
|
122
|
+
`${c.id}${c.flowIndex ? ` ${c.flowIndex}` : ''}`,
|
|
123
|
+
]),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function recordText(r) {
|
|
128
|
+
const c = r.call ?? {};
|
|
129
|
+
const rows = [
|
|
130
|
+
['Call', `${c.id ?? ''} flow ${c.flowIndex ?? 0}`],
|
|
131
|
+
['Start', localTime(c.startTime, {seconds: true})],
|
|
132
|
+
['Duration', [millis(c.duration), Number.isFinite(c.talkTime) && `talk ${millis(c.talkTime)}`, c.waitTime && `wait ${millis(c.waitTime)}`, c.holdTime && `hold ${millis(c.holdTime)}`].filter(Boolean).join(' · ')],
|
|
133
|
+
['Direction', [c.direction, c.callStatus].filter(Boolean).join(' · ')],
|
|
134
|
+
['From', party(c.caller)],
|
|
135
|
+
['To', party(c.callee)],
|
|
136
|
+
];
|
|
137
|
+
if (c.queueName) rows.push(['Queue', c.queueName]);
|
|
138
|
+
if (c.service) rows.push(['Service', c.service]);
|
|
139
|
+
if (c.trunkName) rows.push(['Trunk', [c.trunkName, c.trunkDirection].filter(Boolean).join(' · ')]);
|
|
140
|
+
if (c.tags?.length) rows.push(['Tags', c.tags.join(', ')]);
|
|
141
|
+
if (c.transcriptionStatus) rows.push(['Transcript', [c.transcriptionStatus, c.transcriptionLanguage].filter(Boolean).join(' · ')]);
|
|
142
|
+
const recs = recordingsOf(c);
|
|
143
|
+
if (recs.length) rows.push(['Recordings', String(recs.length)]);
|
|
144
|
+
return table(rows);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function briefText(b) {
|
|
148
|
+
const who = (p) => [p.name, p.extension && `(${p.extension})`, p.phone && p.phone !== p.extension ? p.phone : undefined].filter(Boolean).join(' ');
|
|
149
|
+
const rows = [
|
|
150
|
+
['Call', `${b.id} flow ${b.flowIndex ?? 0}`],
|
|
151
|
+
['Start', localTime(b.start)],
|
|
152
|
+
['Duration', `${secs(b.durationSec)} · talk ${secs(b.talkSec)}`],
|
|
153
|
+
['Direction', [b.direction, b.status].filter(Boolean).join(' · ')],
|
|
154
|
+
['From', who(b.caller)],
|
|
155
|
+
['To', who(b.callee)],
|
|
156
|
+
];
|
|
157
|
+
if (b.queue) rows.push(['Queue', b.queue]);
|
|
158
|
+
if (b.tags.length) rows.push(['Tags', b.tags.join(', ')]);
|
|
159
|
+
rows.push(['Transcript', b.transcriptionStatus ?? 'none'], ['Recording', b.hasRecording ? 'yes' : 'no']);
|
|
160
|
+
if (b.summary.title || b.summary.brief) rows.push(['Summary', oneLine(b.summary.title ?? '', 200)]);
|
|
161
|
+
else rows.push(['Summary', b.summary.status]);
|
|
162
|
+
const lines = table(rows);
|
|
163
|
+
if (b.summary.brief) lines.push('', oneLine(b.summary.brief, 2000));
|
|
164
|
+
const ins = insightLines(b.insights);
|
|
165
|
+
if (ins.length) lines.push('', 'Insights:', ...insightLines(b.insights, ' '));
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const NO_TRANSCRIPT = 'This call has no transcription, or there is no such call.';
|
|
170
|
+
|
|
171
|
+
const group = {
|
|
172
|
+
summary: 'your call history and calls after the fact: record, transcript, summary, insights',
|
|
173
|
+
auth: true,
|
|
174
|
+
commands: {
|
|
175
|
+
list: {
|
|
176
|
+
summary: 'your own call history, newest first, 50 per page',
|
|
177
|
+
usage: 'wx calls list [--direction INTERNAL|INBOUND|OUTBOUND] [--status COMPLETED|MISSED] [--flag FAX|VOICEMAIL]... [--from ISO] [--to ISO] [--search TEXT] [--tag TAG]... [--has-attachment] [--user-direction INCOMING|OUTGOING] [--limit 1-100] [--offset N] [--brief]',
|
|
178
|
+
details: 'Calls only (not meetings), as the PBX recorded them. The next page: --offset.',
|
|
179
|
+
options: LIST_OPTIONS,
|
|
180
|
+
args: [0, 0],
|
|
181
|
+
run: (ctx) => listCalls(ctx, ctx.values.status),
|
|
182
|
+
text: listText,
|
|
183
|
+
},
|
|
184
|
+
missed: {
|
|
185
|
+
summary: 'your missed calls, newest first (list --status MISSED)',
|
|
186
|
+
usage: 'wx calls missed [the options of list, without --status]',
|
|
187
|
+
options: Object.fromEntries(Object.entries(LIST_OPTIONS).filter(([k]) => k !== 'status')),
|
|
188
|
+
args: [0, 0],
|
|
189
|
+
run: (ctx) => listCalls(ctx, 'MISSED'),
|
|
190
|
+
text: listText,
|
|
191
|
+
},
|
|
192
|
+
get: {
|
|
193
|
+
summary: 'the record of a call: who called whom, times, queue, trunk, recordings',
|
|
194
|
+
usage: `wx calls get ${CALL_ARGS}`,
|
|
195
|
+
details: 'The flow index is 0 for the first part of the call; a transferred call has more flows.',
|
|
196
|
+
options: {},
|
|
197
|
+
args: [1, 2],
|
|
198
|
+
run: (ctx) => ctx.client(wdaClient).get(`/history/calls${callPath(callFlow(ctx.positionals))}`),
|
|
199
|
+
text: recordText,
|
|
200
|
+
},
|
|
201
|
+
brief: {
|
|
202
|
+
summary: 'a call as one readable object: record, summary, insights',
|
|
203
|
+
usage: `wx calls brief ${CALL_ARGS}`,
|
|
204
|
+
options: {},
|
|
205
|
+
args: [1, 2],
|
|
206
|
+
async run(ctx) {
|
|
207
|
+
const call = callFlow(ctx.positionals);
|
|
208
|
+
const p = callPath(call);
|
|
209
|
+
const wda = ctx.client(wdaClient);
|
|
210
|
+
const [rec, sum, ins] = await Promise.all([wda.get(`/history/calls${p}`), optional(wda.get(`/insights/calls${p}/summary`), ctx.info), optional(wda.get(`/insights/calls${p}/insights`), ctx.info)]);
|
|
211
|
+
return briefCall(call.callId, rec, sum, ins);
|
|
212
|
+
},
|
|
213
|
+
text: briefText,
|
|
214
|
+
},
|
|
215
|
+
transcript: {
|
|
216
|
+
summary: 'the dialogue of a call (exit 3 when there is none)',
|
|
217
|
+
usage: `wx calls transcript ${CALL_ARGS} [--to FILE] [--structured]`,
|
|
218
|
+
details: 'Text: the dialogue, "[MM:SS] Name: text". --output json: the text and its chunks as the service gives them. --structured: the transcription record with the interaction data. --to writes the dialogue to a new private file and prints its path.',
|
|
219
|
+
options: {
|
|
220
|
+
to: {type: 'string', placeholder: 'FILE', description: 'write the dialogue to this new file (private) and print its path'},
|
|
221
|
+
structured: {type: 'boolean', description: 'the transcription record: chunks with timing, sentiment, the interaction'},
|
|
222
|
+
},
|
|
223
|
+
args: [1, 2],
|
|
224
|
+
async run(ctx) {
|
|
225
|
+
const p = callPath(callFlow(ctx.positionals));
|
|
226
|
+
const wda = ctx.client(wdaClient);
|
|
227
|
+
if (ctx.values.structured) {
|
|
228
|
+
if (ctx.values.to) throw new UsageError('--to writes the dialogue: leave out --structured');
|
|
229
|
+
const r = await wda.soft(`/history/calls${p}/transcription`);
|
|
230
|
+
if (!r) throw new NoDataError(NO_TRANSCRIPT);
|
|
231
|
+
return r;
|
|
232
|
+
}
|
|
233
|
+
const r = await wda.soft(`/history/calls${p}/transcription/text`);
|
|
234
|
+
if (!r) throw new NoDataError(NO_TRANSCRIPT);
|
|
235
|
+
if (ctx.output === 'json' && !ctx.values.to) return r;
|
|
236
|
+
const text = r.text ?? '';
|
|
237
|
+
if (!text.trim()) throw new NoDataError('The transcription is empty.');
|
|
238
|
+
if (ctx.values.to) return {file: writePrivate(ctx.values.to, `${text}\n`, {cwd: ctx.cwd, env: ctx.env})};
|
|
239
|
+
return r;
|
|
240
|
+
},
|
|
241
|
+
text(r, ctx) {
|
|
242
|
+
if (r.file) return r.file;
|
|
243
|
+
if (ctx.values.structured) return structuredLines(r.transcription?.chunks ?? [], r.transcription?.callStartTime ?? r.transcription?.flowStartTime);
|
|
244
|
+
return (r.text ?? '').replace(/\n+$/, '');
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
annotations: {
|
|
248
|
+
summary: 'the annotations of a call (404 when there are none)',
|
|
249
|
+
usage: `wx calls annotations ${CALL_ARGS}`,
|
|
250
|
+
options: {},
|
|
251
|
+
args: [1, 2],
|
|
252
|
+
run: (ctx) => ctx.client(wdaClient).get(`/history/calls${callPath(callFlow(ctx.positionals))}/annotations`),
|
|
253
|
+
text: (r) => annotationLines(r.annotations?.annotations ?? []),
|
|
254
|
+
},
|
|
255
|
+
insights: {
|
|
256
|
+
summary: 'the AI insights that were generated: {projection: {field: value}}',
|
|
257
|
+
usage: `wx calls insights ${CALL_ARGS} [--raw]`,
|
|
258
|
+
options: {raw: {type: 'boolean', description: 'every projection as the service gives it, with its status'}},
|
|
259
|
+
args: [1, 2],
|
|
260
|
+
async run(ctx) {
|
|
261
|
+
const r = await ctx.client(wdaClient).get(`/insights/calls${callPath(callFlow(ctx.positionals))}/insights`);
|
|
262
|
+
return ctx.values.raw ? r : {status: r.status ?? null, insights: compactInsights(r)};
|
|
263
|
+
},
|
|
264
|
+
text: (r) => insightsText(r),
|
|
265
|
+
},
|
|
266
|
+
summary: {
|
|
267
|
+
summary: 'the AI title, brief and the structured summary of a call',
|
|
268
|
+
usage: `wx calls summary ${CALL_ARGS}`,
|
|
269
|
+
options: {},
|
|
270
|
+
args: [1, 2],
|
|
271
|
+
run: (ctx) => ctx.client(wdaClient).get(`/insights/calls${callPath(callFlow(ctx.positionals))}/summary`),
|
|
272
|
+
text: summaryLines,
|
|
273
|
+
},
|
|
274
|
+
characteristics: {
|
|
275
|
+
summary: 'talk time, interruptions, sentiment and talk speed of a call',
|
|
276
|
+
usage: `wx calls characteristics ${CALL_ARGS}`,
|
|
277
|
+
options: {},
|
|
278
|
+
args: [1, 2],
|
|
279
|
+
run: (ctx) => ctx.client(wdaClient).get(`/insights/calls${callPath(callFlow(ctx.positionals))}/characteristics`),
|
|
280
|
+
text: characteristicsText,
|
|
281
|
+
},
|
|
282
|
+
state: {
|
|
283
|
+
summary: 'your own state of a call: playlists, playback position',
|
|
284
|
+
usage: `wx calls state ${CALL_ARGS}`,
|
|
285
|
+
options: {},
|
|
286
|
+
args: [1, 2],
|
|
287
|
+
run: (ctx) => ctx.client(wdaClient).get(`/insights/calls${callPath(callFlow(ctx.positionals))}/state`),
|
|
288
|
+
text: stateText,
|
|
289
|
+
},
|
|
290
|
+
recordings: {
|
|
291
|
+
summary: 'the recordings of a call: file names and their links on the PBX',
|
|
292
|
+
usage: `wx calls recordings ${CALL_ARGS}`,
|
|
293
|
+
options: {},
|
|
294
|
+
args: [1, 2],
|
|
295
|
+
async run(ctx) {
|
|
296
|
+
const r = await ctx.client(wdaClient).get(`/history/calls${callPath(callFlow(ctx.positionals))}`);
|
|
297
|
+
return {recordings: recordingsOf(r.call ?? {})};
|
|
298
|
+
},
|
|
299
|
+
text: (r) => (r.recordings.length ? table(r.recordings.map((x) => [localTime(x.start, {seconds: true}), Number.isFinite(x.start) && Number.isFinite(x.end) ? millis(x.end - x.start) : '', x.url ?? x.fileName ?? ''])) : 'No recordings.'),
|
|
300
|
+
},
|
|
301
|
+
'schedule-summary': {
|
|
302
|
+
summary: 'ask the service to generate the summary of a call again (only when asked: it does work)',
|
|
303
|
+
usage: `wx calls schedule-summary ${CALL_ARGS}`,
|
|
304
|
+
options: {},
|
|
305
|
+
args: [1, 2],
|
|
306
|
+
run: (ctx) => ctx.client(wdaClient).post(`/insights/calls${callPath(callFlow(ctx.positionals))}/schedule_summary_generation`),
|
|
307
|
+
text: () => 'The summary was scheduled: read it later with wx calls summary.',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
export const groups = {calls: group};
|
|
314
|
+
|
|
315
|
+
// Shared with wx meetings.
|
|
316
|
+
|
|
317
|
+
// Chunks of a structured transcription (the service does not keep them in order) as "[MM:SS] speaker: text", in
|
|
318
|
+
// order of their start, the time from the start of the transcription.
|
|
319
|
+
export function structuredLines(chunks, start, names = {}) {
|
|
320
|
+
const t0 = Number.isFinite(start) ? start : chunks.find((c) => Number.isFinite(c.start))?.start;
|
|
321
|
+
const ordered = [...chunks].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
|
|
322
|
+
return ordered.map((c) => {
|
|
323
|
+
const at = Number.isFinite(c.start) && Number.isFinite(t0) ? Math.max(0, Math.floor((c.start - t0) / 1000)) : undefined;
|
|
324
|
+
const mmss = at === undefined ? '--:--' : `${String(Math.floor(at / 60)).padStart(2, '0')}:${String(at % 60).padStart(2, '0')}`;
|
|
325
|
+
const who = c.jid ? names[c.jid] ?? c.jid : typeof c.speaker === 'object' ? c.speaker?.name ?? c.speaker?.type ?? '?' : c.speaker ?? '?';
|
|
326
|
+
return `[${mmss}] ${who}: ${String(c.text ?? '').replace(/\s+/g, ' ').trim()}`;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function annotationLines(list) {
|
|
331
|
+
if (!list.length) return 'No annotations.';
|
|
332
|
+
return table(list.map((a) => [localTime(a.time, {seconds: true}), a.key ?? '', a.source?.name ?? a.source?.type ?? '', oneLine(JSON.stringify(a.payload ?? {}), 160)]));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function insightsText(r) {
|
|
336
|
+
if (r.insights && !Array.isArray(r.insights)) {
|
|
337
|
+
const lines = insightLines(r.insights);
|
|
338
|
+
return lines.length ? lines : `No insights${r.status ? ` (${r.status})` : ''}.`;
|
|
339
|
+
}
|
|
340
|
+
const list = r.insights ?? [];
|
|
341
|
+
if (!list.length) return `No insights${r.status ? ` (${r.status})` : ''}.`;
|
|
342
|
+
return list.flatMap((p) => [`${p.id}: ${p.status ?? ''}${p.reason ? ` (${p.reason})` : ''}`, ...(p.fields ?? []).map((f) => ` ${f.id}: ${oneLine(typeof f.value === 'object' ? JSON.stringify(f.value) : f.value ?? '', 300)}`)]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function stateText(r) {
|
|
346
|
+
const s = r.state ?? {};
|
|
347
|
+
const rows = [];
|
|
348
|
+
if (s.firstWatchedAt) rows.push(['First watched', localTime(s.firstWatchedAt)]);
|
|
349
|
+
if (s.lastWatchedAt) rows.push(['Last watched', localTime(s.lastWatchedAt)]);
|
|
350
|
+
if (s.playback) rows.push(['Playback', `${millis(s.playback.position)} of ${millis(s.playback.duration)}`]);
|
|
351
|
+
rows.push(['Playlists', (s.playlists ?? []).length ? s.playlists.map((p) => (typeof p === 'object' ? p.name ?? p.id : p)).join(', ') : 'none']);
|
|
352
|
+
return table(rows);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function characteristicsText(r) {
|
|
356
|
+
const c = r.characteristics;
|
|
357
|
+
if (!c) return `No characteristics${r.status ? ` (${r.status})` : ''}.`;
|
|
358
|
+
const rows = [];
|
|
359
|
+
if (Number.isFinite(c.duration)) rows.push(['Duration', millis(c.duration)]);
|
|
360
|
+
for (const t of c.talkTime?.detailsByParticipant ?? []) rows.push([`Talk ${t.interrupter ?? '?'}`, millis(t.totalTime)]);
|
|
361
|
+
for (const t of c.talkSpeed?.detailsByParticipant ?? []) rows.push([`Speed ${t.interrupter ?? '?'}`, `${Math.round(t.averageWordsPerMinute ?? 0)} words/min`]);
|
|
362
|
+
if (c.interruptions) rows.push(['Interruptions', String((c.interruptions.chunks ?? []).length)]);
|
|
363
|
+
if (c.nonTalkTime) rows.push(['Silences', String((c.nonTalkTime.chunks ?? []).length)]);
|
|
364
|
+
if (c.sentiment && Number.isFinite(c.sentiment.score)) rows.push(['Sentiment', String(c.sentiment.score)]);
|
|
365
|
+
for (const s of c.sentiment?.scoreByParticipant ?? []) rows.push([`Sentiment ${s.interrupter ?? '?'}`, String(s.score)]);
|
|
366
|
+
return [`Status: ${r.status ?? ''}`, ...table(rows)];
|
|
367
|
+
}
|