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.
- package/README.md +19 -22
- 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/cli.mjs
CHANGED
|
@@ -1,256 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
// The context of run():
|
|
8
|
-
// values, positionals parsed options and arguments
|
|
9
|
-
// token a valid IdToken when the command needs auth (undefined otherwise)
|
|
10
|
-
// freshToken() a valid IdToken again (renewed when it expired): for long-running commands
|
|
11
|
-
// profile the --profile value (or undefined)
|
|
12
|
-
// client(factory) factory({token, env, fetchImpl}): a service client bound to the token
|
|
13
|
-
// emit(data) print one JSON line on stdout now (streaming commands)
|
|
14
|
-
// info(text) print a line for the person on stderr (progress, prompts); never data
|
|
15
|
-
// signal aborted on Ctrl-C / SIGTERM: long-running commands stop and return
|
|
16
|
-
// output ('text'|'json'), color (colors allowed)
|
|
17
|
-
// env, stdin, stdout, stderr, fetch, WebSocket, now (ms at the start), clock() (ms now), cwd
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import {parseArgs} from 'node:util';
|
|
20
|
-
import {getToken} from './auth.mjs';
|
|
21
|
-
import {COMMON_OPTIONS, GLOBAL_OPTIONS, REGISTRY, commandOf, groupOf, singleOf} from './commands/index.mjs';
|
|
22
|
-
import {SHELLS, TOP_LEVEL, complete, completionScript, completionUsage} from './completion.mjs';
|
|
23
|
-
import {AuthError, UsageError} from './errors.mjs';
|
|
24
|
-
import {clean, toText} from './format.mjs';
|
|
1
|
+
// wx: the spec of the command line (its commands, its lines in the help) on the router of wldx-core.
|
|
2
|
+
import {createMain} from 'wldx-core/cli';
|
|
3
|
+
import {complete as completeOf} from 'wldx-core/completion';
|
|
4
|
+
import {setProgram} from 'wldx-core/program';
|
|
5
|
+
import {COMMANDS, GROUPS} from './commands/index.mjs';
|
|
25
6
|
import {VERSION} from './version.mjs';
|
|
26
7
|
|
|
27
|
-
|
|
8
|
+
setProgram({name: 'wx', pkg: 'wldx-cli', version: VERSION});
|
|
28
9
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (a.startsWith(`--${name}=`)) return [a.slice(name.length + 3), i, name];
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
10
|
+
export const SPEC = {
|
|
11
|
+
groups: GROUPS,
|
|
12
|
+
commands: COMMANDS,
|
|
13
|
+
help: {exitCodes: 'Exit codes: 0 done, 1 error, 2 not logged in, 3 nothing to show (no transcript), 64 wrong usage.'},
|
|
14
|
+
};
|
|
38
15
|
|
|
39
|
-
|
|
40
|
-
export
|
|
41
|
-
const g = {output: 'text', pretty: false, color: true, profile: undefined};
|
|
42
|
-
const rest = [];
|
|
43
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
44
|
-
const a = argv[i];
|
|
45
|
-
if (a === '--') {
|
|
46
|
-
rest.push(...argv.slice(i));
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
const profile = valueOf(argv, i, ['profile']);
|
|
50
|
-
if (profile) {
|
|
51
|
-
const [value, next, name] = profile;
|
|
52
|
-
if (!value || value.startsWith('--')) throw new UsageError(`--${name} needs a profile: the e-mail of a login`);
|
|
53
|
-
if (g.profile !== undefined && g.profile.toLowerCase() !== value.toLowerCase()) throw new UsageError(`Two profiles given: ${g.profile} and ${value}`);
|
|
54
|
-
g.profile = value;
|
|
55
|
-
i = next;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (a === '--output' || a.startsWith('--output=')) {
|
|
59
|
-
g.output = a === '--output' ? argv[(i += 1)] : a.slice('--output='.length);
|
|
60
|
-
if (!OUTPUTS.includes(g.output)) throw new UsageError(`--output must be one of ${OUTPUTS.join(', ')}${g.output === undefined ? '' : `: ${g.output}`}`);
|
|
61
|
-
} else if (a === '--pretty') g.pretty = true;
|
|
62
|
-
else if (a === '--no-color') g.color = false;
|
|
63
|
-
else rest.push(a);
|
|
64
|
-
}
|
|
65
|
-
return {globals: g, rest};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function render(data, formatter, ctx, pretty) {
|
|
69
|
-
if (ctx.output === 'json') return JSON.stringify(data, null, pretty ? 2 : 0);
|
|
70
|
-
const t = formatter ? formatter(data, ctx) : toText(data);
|
|
71
|
-
return clean(Array.isArray(t) ? t.join('\n') : String(t ?? ''));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const pad = (s, n) => s + ' '.repeat(Math.max(1, n - s.length));
|
|
75
|
-
|
|
76
|
-
function mainUsage(registry) {
|
|
77
|
-
return [
|
|
78
|
-
'Usage: wx <command> [options]',
|
|
79
|
-
'',
|
|
80
|
-
'Commands:',
|
|
81
|
-
...Object.entries(registry.groups).map(([name, g]) => ` ${pad(name, 14)}${g.summary}`),
|
|
82
|
-
...Object.entries(registry.commands).map(([name, c]) => ` ${pad(name, 14)}${c.summary}`),
|
|
83
|
-
...Object.entries(TOP_LEVEL).map(([name, summary]) => ` ${pad(name, 14)}${summary}`),
|
|
84
|
-
'',
|
|
85
|
-
'Options:',
|
|
86
|
-
' --output text|json text for people (default), JSON for scripts and agents',
|
|
87
|
-
' --pretty indent the JSON',
|
|
88
|
-
' --no-color no colors in the text',
|
|
89
|
-
' --profile EMAIL the profile (a logged-in e-mail) to use; else WILDIX_EMAIL, else the default',
|
|
90
|
-
' -h, --help help of wx, a command or a subcommand',
|
|
91
|
-
' -v, --version the version of wx',
|
|
92
|
-
'',
|
|
93
|
-
'Exit codes: 0 done, 1 error, 2 not logged in, 3 nothing to show (no transcript), 64 wrong usage.',
|
|
94
|
-
'Without installing: npx wldx-cli <command>',
|
|
95
|
-
].join('\n');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function groupUsage(name, g) {
|
|
99
|
-
return [`Usage: wx ${name} <command> [options]`, '', ...Object.values(g.commands).flatMap((cmd) => [` ${cmd.usage}`, ` ${cmd.summary}`])].join('\n');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function commandUsage(cmd) {
|
|
103
|
-
const options = Object.entries({...cmd.options, ...COMMON_OPTIONS, ...GLOBAL_OPTIONS}).map(([name, o]) => {
|
|
104
|
-
const value = o.choices && o.choices.length <= 8 ? o.choices.join('|') : o.placeholder ?? 'VALUE';
|
|
105
|
-
const flag = `${o.short ? `-${o.short}, ` : ''}--${name}${o.type === 'string' ? ` ${value}` : ''}`;
|
|
106
|
-
const values = o.choices && o.choices.length > 8 ? ` (one of: ${o.choices.join(', ')})` : '';
|
|
107
|
-
return ` ${pad(flag, 34)}${o.description ?? ''}${o.multiple ? ' (repeatable)' : ''}${values}`;
|
|
108
|
-
});
|
|
109
|
-
return [`Usage: ${cmd.usage}`, '', cmd.summary, ...(cmd.details ? ['', cmd.details] : []), '', 'Options:', ...options].join('\n');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function line(stream, text) {
|
|
113
|
-
stream.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function completion(args, io) {
|
|
117
|
-
if (args.includes('-h') || args.includes('--help')) return line(io.stdout, completionUsage());
|
|
118
|
-
if (args.length > 1) throw new UsageError('wx completion takes one shell', completionUsage());
|
|
119
|
-
const shell = args[0] ?? path.basename(io.env.SHELL ?? '');
|
|
120
|
-
const script = completionScript(shell);
|
|
121
|
-
if (!script) throw new UsageError(`No completion for the shell "${shell}": give one of ${SHELLS.join(', ')}`, completionUsage());
|
|
122
|
-
line(io.stdout, script);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Ctrl-C and SIGTERM abort ctx.signal once; a second Ctrl-C leaves it to Node (exit).
|
|
126
|
-
function stopSignal(io) {
|
|
127
|
-
if (io.signal) return {signal: io.signal, dispose() {}};
|
|
128
|
-
const controller = new AbortController();
|
|
129
|
-
const onSignal = () => {
|
|
130
|
-
process.off('SIGINT', onSignal);
|
|
131
|
-
process.off('SIGTERM', onSignal);
|
|
132
|
-
controller.abort();
|
|
133
|
-
};
|
|
134
|
-
process.on('SIGINT', onSignal);
|
|
135
|
-
process.on('SIGTERM', onSignal);
|
|
136
|
-
return {signal: controller.signal, dispose: onSignal};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function runCommand(cmd, args, needsAuth, io) {
|
|
140
|
-
const options = {...COMMON_OPTIONS, ...cmd.options};
|
|
141
|
-
let parsed;
|
|
142
|
-
try {
|
|
143
|
-
parsed = parseArgs({args, options, allowPositionals: true, strict: true});
|
|
144
|
-
} catch (e) {
|
|
145
|
-
if (String(e.code).startsWith('ERR_PARSE_ARGS')) throw new UsageError(e.message, commandUsage(cmd));
|
|
146
|
-
throw e;
|
|
147
|
-
}
|
|
148
|
-
const {values, positionals} = parsed;
|
|
149
|
-
if (values.help) return line(io.stdout, commandUsage(cmd));
|
|
150
|
-
if (io.globals.profile !== undefined) values.profile = io.globals.profile;
|
|
151
|
-
for (const [name, o] of Object.entries(options)) {
|
|
152
|
-
const given = values[name] === undefined ? [] : [values[name]].flat();
|
|
153
|
-
const bad = o.choices && given.find((v) => !o.choices.includes(v));
|
|
154
|
-
if (bad !== undefined) throw new UsageError(`--${name} must be one of ${o.choices.join(', ')}: ${bad}`, commandUsage(cmd));
|
|
155
|
-
}
|
|
156
|
-
const [min, max] = cmd.args;
|
|
157
|
-
if (positionals.length < min || positionals.length > max) {
|
|
158
|
-
throw new UsageError(positionals.length < min ? 'Missing arguments' : `Unexpected argument: ${positionals[max]}`, commandUsage(cmd));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const freshToken = () => getToken({profile: values.profile, env: io.env, fetchImpl: io.fetch, now: io.now()});
|
|
162
|
-
const token = needsAuth ? await freshToken() : undefined;
|
|
163
|
-
const stop = stopSignal(io);
|
|
164
|
-
const ctx = {
|
|
165
|
-
values,
|
|
166
|
-
positionals,
|
|
167
|
-
token,
|
|
168
|
-
freshToken,
|
|
169
|
-
profile: values.profile,
|
|
170
|
-
client: (factory) => factory({token, env: io.env, fetchImpl: io.fetch}),
|
|
171
|
-
output: io.globals.output,
|
|
172
|
-
color: io.globals.color && !io.env.NO_COLOR && Boolean(io.stdout.isTTY),
|
|
173
|
-
emit: (data) => {
|
|
174
|
-
const text = io.globals.output === 'json' ? JSON.stringify(data) : render(data, cmd.textEvent ?? cmd.text, ctx);
|
|
175
|
-
if (text) line(io.stdout, text);
|
|
176
|
-
},
|
|
177
|
-
info: (text) => line(io.stderr, clean(text)),
|
|
178
|
-
signal: stop.signal,
|
|
179
|
-
usage: () => commandUsage(cmd),
|
|
180
|
-
env: io.env,
|
|
181
|
-
stdin: io.stdin,
|
|
182
|
-
stdout: io.stdout,
|
|
183
|
-
stderr: io.stderr,
|
|
184
|
-
fetch: io.fetch,
|
|
185
|
-
WebSocket: io.WebSocket, // only when injected (tests); a command falls back to globalThis.WebSocket
|
|
186
|
-
now: io.now(),
|
|
187
|
-
clock: io.now,
|
|
188
|
-
cwd: io.cwd,
|
|
189
|
-
};
|
|
190
|
-
try {
|
|
191
|
-
const result = await cmd.run(ctx);
|
|
192
|
-
if (result !== undefined) {
|
|
193
|
-
const text = render(result, cmd.text, ctx, io.globals.pretty);
|
|
194
|
-
if (text) line(io.stdout, text);
|
|
195
|
-
}
|
|
196
|
-
} finally {
|
|
197
|
-
stop.dispose();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function dispatch(all, io) {
|
|
202
|
-
const registry = io.registry;
|
|
203
|
-
const {globals, rest: argv} = takeGlobals(all);
|
|
204
|
-
io.globals = globals;
|
|
205
|
-
const [first, sub, ...rest] = argv;
|
|
206
|
-
if (!first || first === '-h' || first === '--help' || first === 'help') return line(io.stdout, mainUsage(registry));
|
|
207
|
-
if (first === '-v' || first === '--version') return line(io.stdout, VERSION);
|
|
208
|
-
if (first === '__complete') {
|
|
209
|
-
const out = complete(argv.slice(sub === '--' ? 2 : 1), io.env, registry);
|
|
210
|
-
if (out.length) line(io.stdout, out.join('\n'));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if (first === 'completion') return completion(argv.slice(1), io);
|
|
214
|
-
|
|
215
|
-
const single = singleOf(first, registry);
|
|
216
|
-
if (single) return runCommand(single, argv.slice(1), Boolean(single.auth), io);
|
|
217
|
-
|
|
218
|
-
const group = groupOf(first, registry);
|
|
219
|
-
if (!group) throw new UsageError(`Unknown command: ${first}`, mainUsage(registry));
|
|
220
|
-
if (sub === '-h' || sub === '--help') return line(io.stdout, groupUsage(first, group));
|
|
221
|
-
if (!sub) throw new UsageError(`wx ${first} needs a command`, groupUsage(first, group));
|
|
222
|
-
const cmd = commandOf(group, sub);
|
|
223
|
-
if (!cmd) throw new UsageError(`Unknown command: wx ${first} ${sub}`, groupUsage(first, group));
|
|
224
|
-
return runCommand(cmd, rest, Boolean(cmd.auth ?? group.auth), io);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Runs wx with the arguments and returns the exit code: 0 done, 1 error, 2 not logged in, 3 nothing to show (an
|
|
228
|
-
// error with exitCode 3, e.g. no transcript), 64 wrong usage.
|
|
229
|
-
// io replaces the process streams, environment, fetch, clock and (in tests) the commands themselves.
|
|
230
|
-
export async function main(argv, io = {}) {
|
|
231
|
-
io = {
|
|
232
|
-
stdout: process.stdout,
|
|
233
|
-
stderr: process.stderr,
|
|
234
|
-
stdin: process.stdin,
|
|
235
|
-
env: process.env,
|
|
236
|
-
fetch: globalThis.fetch,
|
|
237
|
-
now: Date.now,
|
|
238
|
-
cwd: process.cwd(),
|
|
239
|
-
registry: REGISTRY,
|
|
240
|
-
...io,
|
|
241
|
-
};
|
|
242
|
-
try {
|
|
243
|
-
await dispatch(argv, io);
|
|
244
|
-
return 0;
|
|
245
|
-
} catch (e) {
|
|
246
|
-
line(io.stderr, `wx: ${clean(e.message)}`);
|
|
247
|
-
if (e instanceof UsageError) {
|
|
248
|
-
if (e.usage) line(io.stderr, `\n${e.usage}`);
|
|
249
|
-
return 64;
|
|
250
|
-
}
|
|
251
|
-
if (e instanceof AuthError) return 2;
|
|
252
|
-
if (Number.isInteger(e.exitCode)) return e.exitCode;
|
|
253
|
-
if (io.env.WX_DEBUG === '1' && e.stack) line(io.stderr, e.stack);
|
|
254
|
-
return 1;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
16
|
+
export const main = createMain(SPEC);
|
|
17
|
+
export const complete = (words, env = process.env) => completeOf(words, env, SPEC);
|
package/src/commands/calls.mjs
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
// wx calls: the user's own call history (as the PBX recorded it) and one call after the fact: its record, the
|
|
2
2
|
// transcript, the AI summary and insights, the characteristics, the recordings. Read only, except
|
|
3
3
|
// schedule-summary (asks the service to generate the summary again). Data of WDA History and WDA Insights.
|
|
4
|
-
import {UsageError} from '
|
|
5
|
-
import {localTime, oneLine, table} from '
|
|
6
|
-
import {apiPath} from '
|
|
4
|
+
import {UsageError} from 'wldx-core/errors';
|
|
5
|
+
import {localTime, oneLine, table} from 'wldx-core/format';
|
|
6
|
+
import {apiPath} from 'wldx-core/http';
|
|
7
7
|
import {insightLines, millis, party, secs, summaryLines} from '../history/text.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
NoDataError,
|
|
10
|
+
alt,
|
|
11
|
+
callFlow,
|
|
12
|
+
checkIso,
|
|
13
|
+
compactInsights,
|
|
14
|
+
intOption,
|
|
15
|
+
isoSeconds,
|
|
16
|
+
optional,
|
|
17
|
+
wdaClient,
|
|
18
|
+
writePrivate,
|
|
19
|
+
} from '../history/wda.mjs';
|
|
9
20
|
|
|
10
21
|
const CALL_ARGS = '<callId> [flow]';
|
|
11
22
|
|
|
@@ -13,7 +24,10 @@ const callPath = ({callId, flow}) => apiPath`/${callId}/flows/${String(flow)}`;
|
|
|
13
24
|
|
|
14
25
|
// The user call history gives durations in seconds; the record of one call in milliseconds. The start and the end
|
|
15
26
|
// are milliseconds in both, so the length of a call comes from them when they are there.
|
|
16
|
-
const lengthSec = (c) =>
|
|
27
|
+
const lengthSec = (c) =>
|
|
28
|
+
Number.isFinite(c.startTime) && Number.isFinite(c.endTime)
|
|
29
|
+
? Math.round((c.endTime - c.startTime) / 1000)
|
|
30
|
+
: Math.round(c.duration ?? 0);
|
|
17
31
|
|
|
18
32
|
const hasRecording = (c) => (c.recordingsData ?? []).length > 0 || (c.attachments ?? []).some((a) => a?.recording);
|
|
19
33
|
|
|
@@ -33,7 +47,10 @@ export function briefHistoryCall(c) {
|
|
|
33
47
|
};
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
const person = (p) =>
|
|
50
|
+
const person = (p) =>
|
|
51
|
+
p
|
|
52
|
+
? {name: p.name ?? null, phone: p.phone ?? null, extension: p.userExtension ?? null, email: p.email ?? null}
|
|
53
|
+
: {name: null, phone: null, extension: null, email: null};
|
|
37
54
|
|
|
38
55
|
// `calls brief`: the record, the summary and the insights as one readable object.
|
|
39
56
|
export function briefCall(id, rec, sum, ins) {
|
|
@@ -53,7 +70,10 @@ export function briefCall(id, rec, sum, ins) {
|
|
|
53
70
|
tags: c.tags ?? [],
|
|
54
71
|
transcriptionStatus: c.transcriptionStatus ?? null,
|
|
55
72
|
hasRecording: hasRecording(c),
|
|
56
|
-
summary:
|
|
73
|
+
summary:
|
|
74
|
+
sum?.status === 'SUCCEEDED'
|
|
75
|
+
? {title: sum.summary?.title ?? null, brief: sum.summary?.brief ?? null}
|
|
76
|
+
: {status: sum?.status ?? 'UNKNOWN'},
|
|
57
77
|
insights: compactInsights(ins),
|
|
58
78
|
};
|
|
59
79
|
}
|
|
@@ -73,15 +93,28 @@ export function recordingsOf(c) {
|
|
|
73
93
|
|
|
74
94
|
// The history filter of `list` and `missed`.
|
|
75
95
|
const LIST_OPTIONS = {
|
|
76
|
-
direction: {
|
|
96
|
+
direction: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
choices: ['INTERNAL', 'INBOUND', 'OUTBOUND'],
|
|
99
|
+
description: 'only calls of this direction',
|
|
100
|
+
},
|
|
77
101
|
status: {type: 'string', choices: ['COMPLETED', 'MISSED'], description: 'only answered (COMPLETED) or missed calls'},
|
|
78
|
-
flag: {
|
|
102
|
+
flag: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
multiple: true,
|
|
105
|
+
choices: ['FAX', 'VOICEMAIL'],
|
|
106
|
+
description: 'only calls with a fax or a voicemail',
|
|
107
|
+
},
|
|
79
108
|
from: {type: 'string', placeholder: 'ISO', description: 'calls from this time (ISO, e.g. 2026-11-01T00:00:00Z)'},
|
|
80
109
|
to: {type: 'string', placeholder: 'ISO', description: 'calls up to this time (ISO)'},
|
|
81
110
|
search: {type: 'string', placeholder: 'TEXT', description: 'a name or a number to search for'},
|
|
82
111
|
tag: {type: 'string', multiple: true, placeholder: 'TAG', description: 'only calls with this tag'},
|
|
83
112
|
'has-attachment': {type: 'boolean', description: 'only calls with an attachment (a recording, a fax, a voicemail)'},
|
|
84
|
-
'user-direction': {
|
|
113
|
+
'user-direction': {
|
|
114
|
+
type: 'string',
|
|
115
|
+
choices: ['INCOMING', 'OUTGOING'],
|
|
116
|
+
description: 'only calls you received or placed',
|
|
117
|
+
},
|
|
85
118
|
limit: {type: 'string', placeholder: '1-100', description: 'calls per page (50 by default, at most 100)'},
|
|
86
119
|
offset: {type: 'string', placeholder: 'N', description: 'skip this many calls (the next page)'},
|
|
87
120
|
brief: {type: 'boolean', description: 'one short object per call: id, start, duration, direction, status, from, to'},
|
|
@@ -129,7 +162,17 @@ function recordText(r) {
|
|
|
129
162
|
const rows = [
|
|
130
163
|
['Call', `${c.id ?? ''} flow ${c.flowIndex ?? 0}`],
|
|
131
164
|
['Start', localTime(c.startTime, {seconds: true})],
|
|
132
|
-
[
|
|
165
|
+
[
|
|
166
|
+
'Duration',
|
|
167
|
+
[
|
|
168
|
+
millis(c.duration),
|
|
169
|
+
Number.isFinite(c.talkTime) && `talk ${millis(c.talkTime)}`,
|
|
170
|
+
c.waitTime && `wait ${millis(c.waitTime)}`,
|
|
171
|
+
c.holdTime && `hold ${millis(c.holdTime)}`,
|
|
172
|
+
]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(' · '),
|
|
175
|
+
],
|
|
133
176
|
['Direction', [c.direction, c.callStatus].filter(Boolean).join(' · ')],
|
|
134
177
|
['From', party(c.caller)],
|
|
135
178
|
['To', party(c.callee)],
|
|
@@ -138,14 +181,18 @@ function recordText(r) {
|
|
|
138
181
|
if (c.service) rows.push(['Service', c.service]);
|
|
139
182
|
if (c.trunkName) rows.push(['Trunk', [c.trunkName, c.trunkDirection].filter(Boolean).join(' · ')]);
|
|
140
183
|
if (c.tags?.length) rows.push(['Tags', c.tags.join(', ')]);
|
|
141
|
-
if (c.transcriptionStatus)
|
|
184
|
+
if (c.transcriptionStatus)
|
|
185
|
+
rows.push(['Transcript', [c.transcriptionStatus, c.transcriptionLanguage].filter(Boolean).join(' · ')]);
|
|
142
186
|
const recs = recordingsOf(c);
|
|
143
187
|
if (recs.length) rows.push(['Recordings', String(recs.length)]);
|
|
144
188
|
return table(rows);
|
|
145
189
|
}
|
|
146
190
|
|
|
147
191
|
function briefText(b) {
|
|
148
|
-
const who = (p) =>
|
|
192
|
+
const who = (p) =>
|
|
193
|
+
[p.name, p.extension && `(${p.extension})`, p.phone && p.phone !== p.extension ? p.phone : undefined]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join(' ');
|
|
149
196
|
const rows = [
|
|
150
197
|
['Call', `${b.id} flow ${b.flowIndex ?? 0}`],
|
|
151
198
|
['Start', localTime(b.start)],
|
|
@@ -174,7 +221,8 @@ const group = {
|
|
|
174
221
|
commands: {
|
|
175
222
|
list: {
|
|
176
223
|
summary: 'your own call history, newest first, 50 per page',
|
|
177
|
-
usage:
|
|
224
|
+
usage:
|
|
225
|
+
'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
226
|
details: 'Calls only (not meetings), as the PBX recorded them. The next page: --offset.',
|
|
179
227
|
options: LIST_OPTIONS,
|
|
180
228
|
args: [0, 0],
|
|
@@ -207,7 +255,11 @@ const group = {
|
|
|
207
255
|
const call = callFlow(ctx.positionals);
|
|
208
256
|
const p = callPath(call);
|
|
209
257
|
const wda = ctx.client(wdaClient);
|
|
210
|
-
const [rec, sum, ins] = await Promise.all([
|
|
258
|
+
const [rec, sum, ins] = await Promise.all([
|
|
259
|
+
wda.get(`/history/calls${p}`),
|
|
260
|
+
optional(wda.get(`/insights/calls${p}/summary`), ctx.info),
|
|
261
|
+
optional(wda.get(`/insights/calls${p}/insights`), ctx.info),
|
|
262
|
+
]);
|
|
211
263
|
return briefCall(call.callId, rec, sum, ins);
|
|
212
264
|
},
|
|
213
265
|
text: briefText,
|
|
@@ -215,10 +267,18 @@ const group = {
|
|
|
215
267
|
transcript: {
|
|
216
268
|
summary: 'the dialogue of a call (exit 3 when there is none)',
|
|
217
269
|
usage: `wx calls transcript ${CALL_ARGS} [--to FILE] [--structured]`,
|
|
218
|
-
details:
|
|
270
|
+
details:
|
|
271
|
+
'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
272
|
options: {
|
|
220
|
-
to: {
|
|
221
|
-
|
|
273
|
+
to: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
placeholder: 'FILE',
|
|
276
|
+
description: 'write the dialogue to this new file (private) and print its path',
|
|
277
|
+
},
|
|
278
|
+
structured: {
|
|
279
|
+
type: 'boolean',
|
|
280
|
+
description: 'the transcription record: chunks with timing, sentiment, the interaction',
|
|
281
|
+
},
|
|
222
282
|
},
|
|
223
283
|
args: [1, 2],
|
|
224
284
|
async run(ctx) {
|
|
@@ -240,7 +300,11 @@ const group = {
|
|
|
240
300
|
},
|
|
241
301
|
text(r, ctx) {
|
|
242
302
|
if (r.file) return r.file;
|
|
243
|
-
if (ctx.values.structured)
|
|
303
|
+
if (ctx.values.structured)
|
|
304
|
+
return structuredLines(
|
|
305
|
+
r.transcription?.chunks ?? [],
|
|
306
|
+
r.transcription?.callStartTime ?? r.transcription?.flowStartTime,
|
|
307
|
+
);
|
|
244
308
|
return (r.text ?? '').replace(/\n+$/, '');
|
|
245
309
|
},
|
|
246
310
|
},
|
|
@@ -296,20 +360,29 @@ const group = {
|
|
|
296
360
|
const r = await ctx.client(wdaClient).get(`/history/calls${callPath(callFlow(ctx.positionals))}`);
|
|
297
361
|
return {recordings: recordingsOf(r.call ?? {})};
|
|
298
362
|
},
|
|
299
|
-
text: (r) =>
|
|
363
|
+
text: (r) =>
|
|
364
|
+
r.recordings.length
|
|
365
|
+
? table(
|
|
366
|
+
r.recordings.map((x) => [
|
|
367
|
+
localTime(x.start, {seconds: true}),
|
|
368
|
+
Number.isFinite(x.start) && Number.isFinite(x.end) ? millis(x.end - x.start) : '',
|
|
369
|
+
x.url ?? x.fileName ?? '',
|
|
370
|
+
]),
|
|
371
|
+
)
|
|
372
|
+
: 'No recordings.',
|
|
300
373
|
},
|
|
301
374
|
'schedule-summary': {
|
|
302
375
|
summary: 'ask the service to generate the summary of a call again (only when asked: it does work)',
|
|
303
376
|
usage: `wx calls schedule-summary ${CALL_ARGS}`,
|
|
304
377
|
options: {},
|
|
305
378
|
args: [1, 2],
|
|
306
|
-
run: (ctx) =>
|
|
379
|
+
run: (ctx) =>
|
|
380
|
+
ctx.client(wdaClient).post(`/insights/calls${callPath(callFlow(ctx.positionals))}/schedule_summary_generation`),
|
|
307
381
|
text: () => 'The summary was scheduled: read it later with wx calls summary.',
|
|
308
382
|
},
|
|
309
383
|
},
|
|
310
384
|
};
|
|
311
385
|
|
|
312
|
-
|
|
313
386
|
export const groups = {calls: group};
|
|
314
387
|
|
|
315
388
|
// Shared with wx meetings.
|
|
@@ -320,16 +393,33 @@ export function structuredLines(chunks, start, names = {}) {
|
|
|
320
393
|
const t0 = Number.isFinite(start) ? start : chunks.find((c) => Number.isFinite(c.start))?.start;
|
|
321
394
|
const ordered = [...chunks].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
|
|
322
395
|
return ordered.map((c) => {
|
|
323
|
-
const at =
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
396
|
+
const at =
|
|
397
|
+
Number.isFinite(c.start) && Number.isFinite(t0) ? Math.max(0, Math.floor((c.start - t0) / 1000)) : undefined;
|
|
398
|
+
const mmss =
|
|
399
|
+
at === undefined
|
|
400
|
+
? '--:--'
|
|
401
|
+
: `${String(Math.floor(at / 60)).padStart(2, '0')}:${String(at % 60).padStart(2, '0')}`;
|
|
402
|
+
const who = c.jid
|
|
403
|
+
? (names[c.jid] ?? c.jid)
|
|
404
|
+
: typeof c.speaker === 'object'
|
|
405
|
+
? (c.speaker?.name ?? c.speaker?.type ?? '?')
|
|
406
|
+
: (c.speaker ?? '?');
|
|
407
|
+
return `[${mmss}] ${who}: ${String(c.text ?? '')
|
|
408
|
+
.replace(/\s+/g, ' ')
|
|
409
|
+
.trim()}`;
|
|
327
410
|
});
|
|
328
411
|
}
|
|
329
412
|
|
|
330
413
|
export function annotationLines(list) {
|
|
331
414
|
if (!list.length) return 'No annotations.';
|
|
332
|
-
return table(
|
|
415
|
+
return table(
|
|
416
|
+
list.map((a) => [
|
|
417
|
+
localTime(a.time, {seconds: true}),
|
|
418
|
+
a.key ?? '',
|
|
419
|
+
a.source?.name ?? a.source?.type ?? '',
|
|
420
|
+
oneLine(JSON.stringify(a.payload ?? {}), 160),
|
|
421
|
+
]),
|
|
422
|
+
);
|
|
333
423
|
}
|
|
334
424
|
|
|
335
425
|
export function insightsText(r) {
|
|
@@ -339,7 +429,12 @@ export function insightsText(r) {
|
|
|
339
429
|
}
|
|
340
430
|
const list = r.insights ?? [];
|
|
341
431
|
if (!list.length) return `No insights${r.status ? ` (${r.status})` : ''}.`;
|
|
342
|
-
return list.flatMap((p) => [
|
|
432
|
+
return list.flatMap((p) => [
|
|
433
|
+
`${p.id}: ${p.status ?? ''}${p.reason ? ` (${p.reason})` : ''}`,
|
|
434
|
+
...(p.fields ?? []).map(
|
|
435
|
+
(f) => ` ${f.id}: ${oneLine(typeof f.value === 'object' ? JSON.stringify(f.value) : (f.value ?? ''), 300)}`,
|
|
436
|
+
),
|
|
437
|
+
]);
|
|
343
438
|
}
|
|
344
439
|
|
|
345
440
|
export function stateText(r) {
|
|
@@ -348,7 +443,12 @@ export function stateText(r) {
|
|
|
348
443
|
if (s.firstWatchedAt) rows.push(['First watched', localTime(s.firstWatchedAt)]);
|
|
349
444
|
if (s.lastWatchedAt) rows.push(['Last watched', localTime(s.lastWatchedAt)]);
|
|
350
445
|
if (s.playback) rows.push(['Playback', `${millis(s.playback.position)} of ${millis(s.playback.duration)}`]);
|
|
351
|
-
rows.push([
|
|
446
|
+
rows.push([
|
|
447
|
+
'Playlists',
|
|
448
|
+
(s.playlists ?? []).length
|
|
449
|
+
? s.playlists.map((p) => (typeof p === 'object' ? (p.name ?? p.id) : p)).join(', ')
|
|
450
|
+
: 'none',
|
|
451
|
+
]);
|
|
352
452
|
return table(rows);
|
|
353
453
|
}
|
|
354
454
|
|
|
@@ -357,11 +457,14 @@ function characteristicsText(r) {
|
|
|
357
457
|
if (!c) return `No characteristics${r.status ? ` (${r.status})` : ''}.`;
|
|
358
458
|
const rows = [];
|
|
359
459
|
if (Number.isFinite(c.duration)) rows.push(['Duration', millis(c.duration)]);
|
|
360
|
-
for (const t of c.talkTime?.detailsByParticipant ?? [])
|
|
361
|
-
|
|
460
|
+
for (const t of c.talkTime?.detailsByParticipant ?? [])
|
|
461
|
+
rows.push([`Talk ${t.interrupter ?? '?'}`, millis(t.totalTime)]);
|
|
462
|
+
for (const t of c.talkSpeed?.detailsByParticipant ?? [])
|
|
463
|
+
rows.push([`Speed ${t.interrupter ?? '?'}`, `${Math.round(t.averageWordsPerMinute ?? 0)} words/min`]);
|
|
362
464
|
if (c.interruptions) rows.push(['Interruptions', String((c.interruptions.chunks ?? []).length)]);
|
|
363
465
|
if (c.nonTalkTime) rows.push(['Silences', String((c.nonTalkTime.chunks ?? []).length)]);
|
|
364
466
|
if (c.sentiment && Number.isFinite(c.sentiment.score)) rows.push(['Sentiment', String(c.sentiment.score)]);
|
|
365
|
-
for (const s of c.sentiment?.scoreByParticipant ?? [])
|
|
467
|
+
for (const s of c.sentiment?.scoreByParticipant ?? [])
|
|
468
|
+
rows.push([`Sentiment ${s.interrupter ?? '?'}`, String(s.score)]);
|
|
366
469
|
return [`Status: ${r.status ?? ''}`, ...table(rows)];
|
|
367
470
|
}
|