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
package/src/cli.mjs CHANGED
@@ -1,256 +1,17 @@
1
- // The router: `wx <group> <command> [options]` and `wx <command> [options]`. The commands are listed in
2
- // src/commands/index.mjs. What run() returns is printed on stdout: as text by default (the command's text()
3
- // formatter, else toText()), as JSON with --output json (--pretty indents it). A streaming command prints one
4
- // line per event with ctx.emit() (text, or one JSON object per line) and returns nothing. Like the internal Wildix
5
- // CLI, --output, --pretty and --no-color may stand anywhere on the command line.
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
- const OUTPUTS = GLOBAL_OPTIONS.output.choices;
8
+ setProgram({name: 'wx', pkg: 'wldx-cli', version: VERSION});
28
9
 
29
- // The value of an option given as `--name value` or `--name=value` at argv[i]; [value, next index] or undefined.
30
- function valueOf(argv, i, names) {
31
- const a = argv[i];
32
- for (const name of names) {
33
- if (a === `--${name}`) return [argv[i + 1], i + 1, name];
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
- // Takes --output, --pretty, --no-color and --profile out of the arguments, wherever they stand (up to a "--").
40
- export function takeGlobals(argv) {
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);
@@ -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 '../errors.mjs';
5
- import {localTime, oneLine, table} from '../format.mjs';
6
- import {apiPath} from '../http.mjs';
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 {NoDataError, alt, callFlow, checkIso, compactInsights, intOption, isoSeconds, optional, wdaClient, writePrivate} from '../history/wda.mjs';
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) => (Number.isFinite(c.startTime) && Number.isFinite(c.endTime) ? Math.round((c.endTime - c.startTime) / 1000) : Math.round(c.duration ?? 0));
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) => (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});
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: sum?.status === 'SUCCEEDED' ? {title: sum.summary?.title ?? null, brief: sum.summary?.brief ?? null} : {status: sum?.status ?? 'UNKNOWN'},
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: {type: 'string', choices: ['INTERNAL', 'INBOUND', 'OUTBOUND'], description: 'only calls of this 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: {type: 'string', multiple: true, choices: ['FAX', 'VOICEMAIL'], description: 'only calls with a fax or a voicemail'},
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': {type: 'string', choices: ['INCOMING', 'OUTGOING'], description: 'only calls you received or placed'},
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
- ['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(' · ')],
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) rows.push(['Transcript', [c.transcriptionStatus, c.transcriptionLanguage].filter(Boolean).join(' · ')]);
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) => [p.name, p.extension && `(${p.extension})`, p.phone && p.phone !== p.extension ? p.phone : undefined].filter(Boolean).join(' ');
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: '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]',
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([wda.get(`/history/calls${p}`), optional(wda.get(`/insights/calls${p}/summary`), ctx.info), optional(wda.get(`/insights/calls${p}/insights`), ctx.info)]);
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: '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.',
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: {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'},
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) return structuredLines(r.transcription?.chunks ?? [], r.transcription?.callStartTime ?? r.transcription?.flowStartTime);
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) => (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.'),
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) => ctx.client(wdaClient).post(`/insights/calls${callPath(callFlow(ctx.positionals))}/schedule_summary_generation`),
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 = 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()}`;
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(list.map((a) => [localTime(a.time, {seconds: true}), a.key ?? '', a.source?.name ?? a.source?.type ?? '', oneLine(JSON.stringify(a.payload ?? {}), 160)]));
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) => [`${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)}`)]);
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(['Playlists', (s.playlists ?? []).length ? s.playlists.map((p) => (typeof p === 'object' ? p.name ?? p.id : p)).join(', ') : 'none']);
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 ?? []) 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`]);
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 ?? []) rows.push([`Sentiment ${s.interrupter ?? '?'}`, String(s.score)]);
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
  }