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
|
@@ -1,31 +1,84 @@
|
|
|
1
1
|
// wx conversations: the search over calls, conferences and chats with the AI summary of each (WDA Insights), the
|
|
2
2
|
// counts, the statistics of a dimension (the data of a chart) and the tags. By default only the conversations the
|
|
3
3
|
// logged-in user took part in; --everyone drops that (the service still decides what the login may see).
|
|
4
|
-
import {jwtClaims} from '
|
|
5
|
-
import {UsageError} from '
|
|
6
|
-
import {localTime, oneLine, table} from '
|
|
7
|
-
import {query} from '
|
|
4
|
+
import {jwtClaims} from 'wldx-core/auth';
|
|
5
|
+
import {UsageError} from 'wldx-core/errors';
|
|
6
|
+
import {localTime, oneLine, table} from 'wldx-core/format';
|
|
7
|
+
import {query} from 'wldx-core/http';
|
|
8
8
|
import {secs} from '../history/text.mjs';
|
|
9
9
|
import {alt, checkIso, checkZone, intOption, isoSeconds, localZone, pick, unique, wdaClient} from '../history/wda.mjs';
|
|
10
10
|
|
|
11
|
-
export const WHEN = [
|
|
12
|
-
|
|
11
|
+
export const WHEN = [
|
|
12
|
+
'TODAY',
|
|
13
|
+
'YESTERDAY',
|
|
14
|
+
'THIS_WEEK',
|
|
15
|
+
'PREVIOUS_WEEK',
|
|
16
|
+
'THIS_MONTH',
|
|
17
|
+
'PREVIOUS_MONTH',
|
|
18
|
+
'THIS_QUARTER',
|
|
19
|
+
'LAST_30_DAYS',
|
|
20
|
+
];
|
|
21
|
+
export const DIMENSIONS = [
|
|
22
|
+
'direction',
|
|
23
|
+
'entity',
|
|
24
|
+
'channel',
|
|
25
|
+
'recorded',
|
|
26
|
+
'language',
|
|
27
|
+
'country',
|
|
28
|
+
'location',
|
|
29
|
+
'pbx',
|
|
30
|
+
'trunk',
|
|
31
|
+
'queue',
|
|
32
|
+
'service',
|
|
33
|
+
'extension',
|
|
34
|
+
'group',
|
|
35
|
+
'department',
|
|
36
|
+
'tag',
|
|
37
|
+
'chatbot',
|
|
38
|
+
'voicebot',
|
|
39
|
+
'duration',
|
|
40
|
+
'connectTime',
|
|
41
|
+
'talkTime',
|
|
42
|
+
'waitTime',
|
|
43
|
+
'queueTime',
|
|
44
|
+
'holdTime',
|
|
45
|
+
'date',
|
|
46
|
+
'dayOfWeek',
|
|
47
|
+
'hourOfDay',
|
|
48
|
+
];
|
|
13
49
|
const ZONED = new Set(['date', 'dayOfWeek', 'hourOfDay']);
|
|
14
50
|
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
15
51
|
|
|
16
52
|
// The options of the filter (find, count, aggregate).
|
|
17
53
|
const FILTER_OPTIONS = {
|
|
18
|
-
entity: {
|
|
19
|
-
|
|
54
|
+
entity: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
multiple: true,
|
|
57
|
+
choices: ['call', 'conference', 'chat'],
|
|
58
|
+
description: 'only calls, conferences or chats',
|
|
59
|
+
},
|
|
60
|
+
email: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
multiple: true,
|
|
63
|
+
placeholder: 'E',
|
|
64
|
+
description: 'a participant by e-mail (all named must have taken part)',
|
|
65
|
+
},
|
|
20
66
|
phone: {type: 'string', multiple: true, placeholder: 'P', description: 'a participant by phone number'},
|
|
21
67
|
extension: {type: 'string', multiple: true, placeholder: 'X', description: 'a participant by extension'},
|
|
22
|
-
everyone: {
|
|
68
|
+
everyone: {
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
description: 'not only your own conversations: everything the login may see (only when asked)',
|
|
71
|
+
},
|
|
23
72
|
transcribed: {type: 'boolean', description: 'only conversations with a transcript'},
|
|
24
73
|
recorded: {type: 'boolean', description: 'only recorded conversations'},
|
|
25
74
|
direction: {type: 'string', choices: ['inbound', 'outbound', 'internal'], description: 'only this direction'},
|
|
26
75
|
from: {type: 'string', placeholder: 'ISO', description: 'from this time (ISO)'},
|
|
27
76
|
to: {type: 'string', placeholder: 'ISO', description: 'up to this time (ISO)'},
|
|
28
|
-
tz: {
|
|
77
|
+
tz: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
placeholder: 'ZONE',
|
|
80
|
+
description: 'the time zone of --from/--to/--when (and of the time dimensions of aggregate)',
|
|
81
|
+
},
|
|
29
82
|
when: {type: 'string', choices: WHEN, description: 'a ready period'},
|
|
30
83
|
tag: {type: 'string', multiple: true, placeholder: 'T', description: 'only conversations with this tag'},
|
|
31
84
|
language: {type: 'string', multiple: true, placeholder: 'L', description: 'only conversations in this language'},
|
|
@@ -36,7 +89,11 @@ const FILTER_OPTIONS = {
|
|
|
36
89
|
export function buildFilter(values, token) {
|
|
37
90
|
const f = {};
|
|
38
91
|
if (values.entity?.length) f.entity = Object.fromEntries(values.entity.map((e) => [e, true]));
|
|
39
|
-
const parts = [
|
|
92
|
+
const parts = [
|
|
93
|
+
...(values.email ?? []).map((email) => ({email})),
|
|
94
|
+
...(values.phone ?? []).map((phone) => ({phone})),
|
|
95
|
+
...(values.extension ?? []).map((extension) => ({extension})),
|
|
96
|
+
];
|
|
40
97
|
if (!parts.length && !values.everyone) {
|
|
41
98
|
const me = String(jwtClaims(token).email ?? '').toLowerCase();
|
|
42
99
|
if (!me) throw new Error('the token carries no e-mail');
|
|
@@ -49,7 +106,13 @@ export function buildFilter(values, token) {
|
|
|
49
106
|
const tz = checkZone(values.tz);
|
|
50
107
|
if (values.when) f.date = {predefined: {type: values.when, ...(tz ? {timeZone: tz} : {})}};
|
|
51
108
|
else if (values.from || values.to) {
|
|
52
|
-
f.date = {
|
|
109
|
+
f.date = {
|
|
110
|
+
absolute: {
|
|
111
|
+
...(values.from ? {from: checkIso('--from', values.from)} : {}),
|
|
112
|
+
...(values.to ? {to: checkIso('--to', values.to)} : {}),
|
|
113
|
+
...(tz ? {timeZone: tz} : {}),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
53
116
|
}
|
|
54
117
|
if (values.tag?.length) f.tag = values.tag;
|
|
55
118
|
if (values.language?.length) f.language = values.language;
|
|
@@ -68,7 +131,10 @@ export function briefItem(it) {
|
|
|
68
131
|
title: v.subject ?? null,
|
|
69
132
|
direction: v.direction ?? null,
|
|
70
133
|
status: alt(v.status, v.callStatus),
|
|
71
|
-
participants:
|
|
134
|
+
participants:
|
|
135
|
+
type === 'conference'
|
|
136
|
+
? unique((v.participants ?? []).map((p) => alt(p.info?.name, p.info?.email)).filter((n) => n !== null))
|
|
137
|
+
: null,
|
|
72
138
|
summary: pick(it.summary, ['status', 'title', 'brief']),
|
|
73
139
|
};
|
|
74
140
|
}
|
|
@@ -80,7 +146,8 @@ export function aggregationOf(dim, tz) {
|
|
|
80
146
|
if (!m) throw new UsageError(`a projection is projection:<id>:<field>: ${dim}`);
|
|
81
147
|
return {projection: {id: m[1], fieldId: m[2]}};
|
|
82
148
|
}
|
|
83
|
-
if (!DIMENSIONS.includes(dim))
|
|
149
|
+
if (!DIMENSIONS.includes(dim))
|
|
150
|
+
throw new UsageError(`not a dimension: ${dim} (one of ${DIMENSIONS.join(', ')}, or projection:<id>:<field>)`);
|
|
84
151
|
return {[dim]: ZONED.has(dim) ? {timeZone: tz} : {}};
|
|
85
152
|
}
|
|
86
153
|
|
|
@@ -116,7 +183,10 @@ export function projectionsOf(r) {
|
|
|
116
183
|
.map((x) => ({
|
|
117
184
|
by: `projection:${x.aggregation.projection.id}:${x.aggregation.projection.fieldId}`,
|
|
118
185
|
values: x.buckets.reduce((n, b) => n + (b.count ?? 0), 0),
|
|
119
|
-
top: [...x.buckets]
|
|
186
|
+
top: [...x.buckets]
|
|
187
|
+
.sort((a, b) => b.count - a.count)
|
|
188
|
+
.slice(0, 5)
|
|
189
|
+
.map((b) => ({key: b.key ?? null, count: b.count ?? null})),
|
|
120
190
|
})),
|
|
121
191
|
};
|
|
122
192
|
}
|
|
@@ -125,7 +195,10 @@ const bucketLabel = (b) => (b.day ? `${b.day}` : b.key === null || b.key === ''
|
|
|
125
195
|
|
|
126
196
|
// The buckets of a level (and of the levels under it, indented) as rows of label and count.
|
|
127
197
|
function levelRows(l, indent = '') {
|
|
128
|
-
return l.buckets.flatMap((b) => [
|
|
198
|
+
return l.buckets.flatMap((b) => [
|
|
199
|
+
[`${indent}${oneLine(bucketLabel(b), 60)}`, String(b.count)],
|
|
200
|
+
...(b.then ? levelRows(b.then, `${indent} `) : []),
|
|
201
|
+
]);
|
|
129
202
|
}
|
|
130
203
|
|
|
131
204
|
const chain = (l) => (l.buckets.find((b) => b.then) ? [l.by, ...chain(l.buckets.find((b) => b.then).then)] : [l.by]);
|
|
@@ -136,15 +209,37 @@ function aggregateText(r, ctx) {
|
|
|
136
209
|
const whose = v.everyone ? 'visible to the login' : 'your own';
|
|
137
210
|
if (v.projections) {
|
|
138
211
|
if (!r.projections.length) return `${r.count} conversations (${whose}); no AI fields with values.`;
|
|
139
|
-
return [
|
|
212
|
+
return [
|
|
213
|
+
`${r.count} conversations (${whose}); AI fields with values:`,
|
|
214
|
+
...table(
|
|
215
|
+
r.projections.map((p) => [
|
|
216
|
+
p.by,
|
|
217
|
+
String(p.values),
|
|
218
|
+
p.top.map((t) => `${oneLine(t.key, 30)} ${t.count}`).join(', '),
|
|
219
|
+
]),
|
|
220
|
+
),
|
|
221
|
+
];
|
|
140
222
|
}
|
|
141
223
|
const head = `${r.count} conversations (${whose}), times in ${r.timeZone}`;
|
|
142
|
-
if (r.results)
|
|
143
|
-
|
|
224
|
+
if (r.results)
|
|
225
|
+
return [
|
|
226
|
+
head,
|
|
227
|
+
...r.results.flatMap((l) => [
|
|
228
|
+
'',
|
|
229
|
+
`by ${l.by}:`,
|
|
230
|
+
...(l.buckets.length ? table(levelRows(l, ' ')) : [' (none)']),
|
|
231
|
+
]),
|
|
232
|
+
];
|
|
233
|
+
return [
|
|
234
|
+
head,
|
|
235
|
+
'',
|
|
236
|
+
`by ${chain(r).join(', then ')}:`,
|
|
237
|
+
...(r.buckets.length ? table(levelRows(r, ' ')) : [' (none)']),
|
|
238
|
+
];
|
|
144
239
|
}
|
|
145
240
|
|
|
146
241
|
function findText(r, ctx) {
|
|
147
|
-
const items = ctx.values.brief ? r.items ?? [] : (r.items ?? []).map(briefItem);
|
|
242
|
+
const items = ctx.values.brief ? (r.items ?? []) : (r.items ?? []).map(briefItem);
|
|
148
243
|
const lines = items.length
|
|
149
244
|
? table(
|
|
150
245
|
items.map((i) => [
|
|
@@ -168,12 +263,18 @@ const group = {
|
|
|
168
263
|
commands: {
|
|
169
264
|
find: {
|
|
170
265
|
summary: 'the search over conversations with the summary of each, 10 per page',
|
|
171
|
-
usage:
|
|
172
|
-
|
|
266
|
+
usage:
|
|
267
|
+
'wx conversations find [--entity call|conference|chat]... [--email E]... [--phone P]... [--extension X]... [--everyone] [--transcribed] [--recorded] [--direction inbound|outbound|internal] [--from ISO --to ISO [--tz ZONE]] [--when PERIOD] [--tag T]... [--language L]... [--cursor KEY] [--brief]',
|
|
268
|
+
details:
|
|
269
|
+
'Only the conversations you took part in, unless --everyone (only when asked: it may return the conversations of colleagues). The next page: --cursor with nextCursor (lastEvaluatedKey) of the answer.',
|
|
173
270
|
options: {
|
|
174
271
|
...FILTER_OPTIONS,
|
|
175
272
|
cursor: {type: 'string', placeholder: 'KEY', description: 'the next page (nextCursor of the previous answer)'},
|
|
176
|
-
brief: {
|
|
273
|
+
brief: {
|
|
274
|
+
type: 'boolean',
|
|
275
|
+
description:
|
|
276
|
+
'one short object per conversation: type, id, start, duration, title, direction, status, participants, summary',
|
|
277
|
+
},
|
|
177
278
|
},
|
|
178
279
|
args: [0, 0],
|
|
179
280
|
async run(ctx) {
|
|
@@ -189,12 +290,15 @@ const group = {
|
|
|
189
290
|
usage: 'wx conversations count [the filter of find, without --cursor and --brief]',
|
|
190
291
|
options: FILTER_OPTIONS,
|
|
191
292
|
args: [0, 0],
|
|
192
|
-
run: (ctx) =>
|
|
193
|
-
|
|
293
|
+
run: (ctx) =>
|
|
294
|
+
ctx.client(wdaClient).post('/insights/conversations/count', {filter: buildFilter(ctx.values, ctx.token)}),
|
|
295
|
+
text: (r, ctx) =>
|
|
296
|
+
`${r.count ?? JSON.stringify(r)} conversations (${ctx.values.everyone ? 'visible to the login' : 'your own'})`,
|
|
194
297
|
},
|
|
195
298
|
aggregate: {
|
|
196
299
|
summary: 'how many conversations fall into each value of a dimension (the data of a chart)',
|
|
197
|
-
usage:
|
|
300
|
+
usage:
|
|
301
|
+
'wx conversations aggregate (--by DIM[,DIM...] | --by DIM --then DIM [--then DIM] | --projections) [the filter of find] [--tz ZONE] [--top N] [--raw]',
|
|
198
302
|
details: [
|
|
199
303
|
'Several dimensions side by side (--by a,b: at most 10), or one broken down by others (--by a --then b [--then c]).',
|
|
200
304
|
`Dimensions: ${DIMENSIONS.join(' ')}, and projection:<id>:<field> (an AI insight field; --projections lists those with values).`,
|
|
@@ -204,10 +308,18 @@ const group = {
|
|
|
204
308
|
options: {
|
|
205
309
|
...FILTER_OPTIONS,
|
|
206
310
|
by: {type: 'string', placeholder: 'DIM[,DIM]', description: 'the dimensions (comma-separated)'},
|
|
207
|
-
then: {
|
|
311
|
+
then: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
multiple: true,
|
|
314
|
+
placeholder: 'DIM',
|
|
315
|
+
description: 'break each value of --by down by this dimension (at most two)',
|
|
316
|
+
},
|
|
208
317
|
top: {type: 'string', placeholder: 'N', description: 'keep the N biggest values of each level (1-1000)'},
|
|
209
318
|
raw: {type: 'boolean', description: 'the answer of the service as it is'},
|
|
210
|
-
projections: {
|
|
319
|
+
projections: {
|
|
320
|
+
type: 'boolean',
|
|
321
|
+
description: 'the AI insight fields that have values in the filter, with their biggest values',
|
|
322
|
+
},
|
|
211
323
|
},
|
|
212
324
|
args: [0, 0],
|
|
213
325
|
async run(ctx) {
|
|
@@ -242,7 +354,12 @@ const group = {
|
|
|
242
354
|
usage: 'wx conversations tags <prefix> [--limit 1-20]',
|
|
243
355
|
options: {limit: {type: 'string', placeholder: '1-20', description: 'how many tags'}},
|
|
244
356
|
args: [1, 1],
|
|
245
|
-
run: (ctx) =>
|
|
357
|
+
run: (ctx) =>
|
|
358
|
+
ctx
|
|
359
|
+
.client(wdaClient)
|
|
360
|
+
.get(
|
|
361
|
+
`/insights/conversations/tags${query({tagPrefix: ctx.positionals[0], limit: intOption('--limit', ctx.values.limit, 1, 20)})}`,
|
|
362
|
+
),
|
|
246
363
|
text: (r) => ((r.tags ?? []).length ? r.tags.map(String) : 'No tags.'),
|
|
247
364
|
},
|
|
248
365
|
},
|
package/src/commands/file.mjs
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
// wx file: files in chat channels (the agent skill's files.sh). Uploads and downloads go through presigned links
|
|
2
2
|
// of the storage, which get no token.
|
|
3
|
-
import {UsageError} from '
|
|
4
|
-
import {table} from '
|
|
5
|
-
import {apiPath} from '
|
|
3
|
+
import {UsageError} from 'wldx-core/errors';
|
|
4
|
+
import {table} from 'wldx-core/format';
|
|
5
|
+
import {apiPath} from 'wldx-core/http';
|
|
6
6
|
import {channelId, messageId} from '../stream.mjs';
|
|
7
7
|
import {api, checkId, downloadFile, uploadFile} from '../chat/lib.mjs';
|
|
8
8
|
|
|
9
|
-
const fileRows = (f) =>
|
|
9
|
+
const fileRows = (f) =>
|
|
10
|
+
table([
|
|
11
|
+
['File', f.name ?? ''],
|
|
12
|
+
['fsId', f.fsId ?? ''],
|
|
13
|
+
['Type', f.mime ?? ''],
|
|
14
|
+
['Size', f.size === undefined ? '' : `${f.size} bytes`],
|
|
15
|
+
...(f.width ? [['Image', `${f.width}x${f.height}`]] : []),
|
|
16
|
+
]);
|
|
10
17
|
|
|
11
18
|
const upload = {
|
|
12
19
|
summary: 'upload a file to a channel and print the attachment object (to put into a message)',
|
|
13
20
|
usage: 'wx file upload <channel> <path>',
|
|
14
|
-
details:
|
|
21
|
+
details:
|
|
22
|
+
'Sending a file in one step: wx chat send <channel> <text> --file PATH. Files in credential folders and files that look like secrets are refused.',
|
|
15
23
|
options: {},
|
|
16
24
|
args: [2, 2],
|
|
17
25
|
async run(ctx) {
|
package/src/commands/inbox.mjs
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
// wx inbox: the logged-in user's inbox on the stream service (the agent skill's inbox.sh): what is unread, and the
|
|
2
2
|
// user's own state of a channel (read, muted, pinned, hidden). Changes here are seen only by the user.
|
|
3
|
-
import {UsageError} from '
|
|
4
|
-
import {paint, table} from '
|
|
5
|
-
import {apiPath, query} from '
|
|
3
|
+
import {UsageError} from 'wldx-core/errors';
|
|
4
|
+
import {paint, table} from 'wldx-core/format';
|
|
5
|
+
import {apiPath, query} from 'wldx-core/http';
|
|
6
6
|
import {channelId, messageId} from '../stream.mjs';
|
|
7
7
|
import {channelLines, channelUserIds, done} from '../chat/format.mjs';
|
|
8
8
|
import {api, withNames} from '../chat/lib.mjs';
|
|
9
9
|
|
|
10
10
|
export function sectionLines(sections, ctx) {
|
|
11
11
|
const c = paint(ctx);
|
|
12
|
-
return table(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
return table(
|
|
13
|
+
(sections ?? []).map((s) => [
|
|
14
|
+
c.bold(s.name ?? s.sectionId),
|
|
15
|
+
s.sectionId,
|
|
16
|
+
s.type ?? '',
|
|
17
|
+
`${s.channelCount ?? 0} channels`,
|
|
18
|
+
s.unmutedUnreadMessageCount
|
|
19
|
+
? c.yellow(`${s.unmutedUnreadMessageCount} new in ${s.unmutedUnreadChannelCount ?? '?'}`)
|
|
20
|
+
: '',
|
|
21
|
+
s.unreadMentionCount ? c.yellow(`@${s.unreadMentionCount}`) : '',
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
const get = {
|
|
@@ -37,7 +41,9 @@ const get = {
|
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
const mark = (read) => ({
|
|
40
|
-
summary: read
|
|
44
|
+
summary: read
|
|
45
|
+
? 'mark a channel read, up to the latest message (or up to a message)'
|
|
46
|
+
: 'mark a channel unread from a message on',
|
|
41
47
|
usage: `wx inbox ${read ? 'mark-read' : 'mark-unread'} <channel> [--message MESSAGE]${read ? '' : ' [--force]'}`,
|
|
42
48
|
options: {
|
|
43
49
|
message: {type: 'string', description: 'the message (id or inbox link)'},
|
|
@@ -63,7 +69,8 @@ const mute = {
|
|
|
63
69
|
const ch = channelId(ctx.positionals[0]);
|
|
64
70
|
const body = {};
|
|
65
71
|
if (ctx.values.until !== undefined) {
|
|
66
|
-
if (!/^\d{4}-\d{2}-\d{2}T/.test(ctx.values.until) || Number.isNaN(Date.parse(ctx.values.until)))
|
|
72
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(ctx.values.until) || Number.isNaN(Date.parse(ctx.values.until)))
|
|
73
|
+
throw new UsageError(`--until must be an ISO 8601 time: ${ctx.values.until}`);
|
|
67
74
|
body.mutedUntil = ctx.values.until;
|
|
68
75
|
}
|
|
69
76
|
return api(ctx).call('POST', apiPath`/channels/${ch}/mute`, body);
|
package/src/commands/index.mjs
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
// The commands of wx. GROUPS: `wx <group> <command>`; COMMANDS: single commands, `wx <command>`.
|
|
2
|
-
//
|
|
3
|
-
// A group is {summary, auth?, commands: {name: Command}}; a Command is
|
|
4
|
-
// {summary, usage, options, args: [min, max], auth?, run(ctx)}
|
|
5
|
-
// `auth` (on the command, else on its group) loads a valid token before run(). An option is a node:util parseArgs
|
|
6
|
-
// option plus `description` (help and completion), `choices` (the router refuses other values, completion offers
|
|
7
|
-
// them) and `complete: 'profiles'` (completion offers the logged-in profiles).
|
|
1
|
+
// The commands of wx. GROUPS: `wx <group> <command>`; COMMANDS: single commands, `wx <command>`. What a group and
|
|
2
|
+
// a command are: wldx-core/registry. login, logout, status and profile come from the core, shared with wx-admin.
|
|
8
3
|
import {groups as calls} from './calls.mjs';
|
|
9
4
|
import {groups as channel} from './channel.mjs';
|
|
10
5
|
import {groups as chat} from './chat.mjs';
|
|
@@ -12,13 +7,13 @@ import {groups as conversations} from './conversations.mjs';
|
|
|
12
7
|
import {groups as file} from './file.mjs';
|
|
13
8
|
import {groups as inbox} from './inbox.mjs';
|
|
14
9
|
import {groups as live} from './live.mjs';
|
|
15
|
-
import {login, logout} from '
|
|
10
|
+
import {login, logout} from 'wldx-core/commands/login';
|
|
16
11
|
import {groups as meetings} from './meetings.mjs';
|
|
17
12
|
import {groups as phone} from './phone.mjs';
|
|
18
|
-
import {groups as profile} from '
|
|
13
|
+
import {groups as profile} from 'wldx-core/commands/profile';
|
|
19
14
|
import {groups as reports} from './reports.mjs';
|
|
20
15
|
import {groups as section} from './section.mjs';
|
|
21
|
-
import status from '
|
|
16
|
+
import status from 'wldx-core/commands/status';
|
|
22
17
|
import {groups as user} from './user.mjs';
|
|
23
18
|
|
|
24
19
|
// In the order of the help: chats, then calls and meetings on record, then now, then the phone.
|
|
@@ -39,22 +34,3 @@ export const GROUPS = {
|
|
|
39
34
|
};
|
|
40
35
|
|
|
41
36
|
export const COMMANDS = {login, logout, status};
|
|
42
|
-
|
|
43
|
-
export const COMMON_OPTIONS = {
|
|
44
|
-
help: {type: 'boolean', short: 'h', description: 'help of the command'},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Taken by the router wherever they stand (as in the internal Wildix CLI); a command never defines these names.
|
|
48
|
-
export const GLOBAL_OPTIONS = {
|
|
49
|
-
profile: {type: 'string', placeholder: 'EMAIL', description: 'the profile (a logged-in e-mail) to use; else WILDIX_EMAIL, else the default', complete: 'profiles'},
|
|
50
|
-
output: {type: 'string', choices: ['text', 'json'], description: 'text for people (default), JSON for scripts and agents'},
|
|
51
|
-
pretty: {type: 'boolean', description: 'indent the JSON'},
|
|
52
|
-
'no-color': {type: 'boolean', description: 'no colors in the text'},
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const REGISTRY = {groups: GROUPS, commands: COMMANDS};
|
|
56
|
-
|
|
57
|
-
const own = (o, k) => (o && Object.hasOwn(o, k) ? o[k] : undefined);
|
|
58
|
-
export const groupOf = (name, registry = REGISTRY) => own(registry.groups, name);
|
|
59
|
-
export const commandOf = (group, name) => own(group?.commands, name);
|
|
60
|
-
export const singleOf = (name, registry = REGISTRY) => own(registry.commands, name);
|
package/src/commands/live.mjs
CHANGED
|
@@ -5,13 +5,23 @@
|
|
|
5
5
|
// skill (live.sh, live.mjs, stats.mjs): the same commands, options, limits and JSON; `watch` and
|
|
6
6
|
// `transcript --follow` print one JSON object per event, {"event": "<EVENT>", "data": {…}} (the skill printed
|
|
7
7
|
// `<EVENT> {…}`).
|
|
8
|
-
import {jwtClaims} from '
|
|
9
|
-
import {AuthError, UsageError} from '
|
|
8
|
+
import {jwtClaims} from 'wldx-core/auth';
|
|
9
|
+
import {AuthError, UsageError} from 'wldx-core/errors';
|
|
10
10
|
import {connect, explain, subscription} from '../live/socket.mjs';
|
|
11
11
|
import {SLA_SECONDS, WINDOWS, statsOptions} from '../live/stats.mjs';
|
|
12
12
|
import {conversationsText, eventText, presenceText, queuesText, transcriptText} from '../live/text.mjs';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
callIdBySip,
|
|
15
|
+
conversationsResult,
|
|
16
|
+
drop,
|
|
17
|
+
makeViews,
|
|
18
|
+
presenceResult,
|
|
19
|
+
queuesResult,
|
|
20
|
+
transcriptEvents,
|
|
21
|
+
transcriptSnapshot,
|
|
22
|
+
watchFrames,
|
|
23
|
+
} from '../live/views.mjs';
|
|
24
|
+
import {wdaWsUrl} from 'wldx-core/services';
|
|
15
25
|
|
|
16
26
|
// Settings of the connection; tests replace them (the WebSocket constructor, shorter waits).
|
|
17
27
|
export const liveOptions = {
|
|
@@ -33,7 +43,12 @@ const retryDelay = (attempt) => liveOptions.retryDelays[attempt - 1] ?? liveOpti
|
|
|
33
43
|
function loginOf(token) {
|
|
34
44
|
const claims = jwtClaims(token);
|
|
35
45
|
const ext = claims['custom:pbx_user_extension'];
|
|
36
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
company: claims.company,
|
|
48
|
+
myExtension: ext ? String(ext) : undefined,
|
|
49
|
+
license: claims['custom:pbx_license'],
|
|
50
|
+
exp: claims.exp,
|
|
51
|
+
};
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
function open(ctx, token, login) {
|
|
@@ -48,7 +63,12 @@ function open(ctx, token, login) {
|
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
function viewsOf(ctx, login) {
|
|
51
|
-
return makeViews({
|
|
66
|
+
return makeViews({
|
|
67
|
+
full: Boolean(ctx.values.full),
|
|
68
|
+
company: login.company,
|
|
69
|
+
myExtension: login.myExtension,
|
|
70
|
+
now: () => ctx.clock(),
|
|
71
|
+
});
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
// A one-shot command: connect, run fn(connection, login), close. Ctrl-C closes the connection and prints nothing.
|
|
@@ -72,7 +92,8 @@ async function oneShot(ctx, fn) {
|
|
|
72
92
|
// ── option checks (the limits of live.sh) ─────────────────────────────────────────────────────────────────────
|
|
73
93
|
function wholeNumber(name, value, min, max) {
|
|
74
94
|
if (value === undefined) return undefined;
|
|
75
|
-
if (!/^[0-9]{1,12}$/.test(value) || Number(value) < min || Number(value) > max)
|
|
95
|
+
if (!/^[0-9]{1,12}$/.test(value) || Number(value) < min || Number(value) > max)
|
|
96
|
+
throw new UsageError(`--${name} must be a whole number from ${min} to ${max}: ${value}`);
|
|
76
97
|
return Number(value);
|
|
77
98
|
}
|
|
78
99
|
|
|
@@ -85,20 +106,33 @@ const EXTENSION = /^[0-9]{1,15}$/;
|
|
|
85
106
|
const wantsStats = (v) => Boolean(v.stats || v.window !== undefined || v.sla !== undefined);
|
|
86
107
|
const statsOptsOf = (ctx) => statsOptions(ctx.values.window ?? 'today', Number(ctx.values.sla ?? 20), ctx.clock());
|
|
87
108
|
|
|
88
|
-
const full = {
|
|
109
|
+
const full = {
|
|
110
|
+
type: 'boolean',
|
|
111
|
+
description: 'everything the service sends, also e-mail, IP address, location and user agent of people',
|
|
112
|
+
};
|
|
89
113
|
const statsOptionsDef = {
|
|
90
114
|
stats: {type: 'boolean', description: 'the figures of each queue, of each agent in it and of all of them together'},
|
|
91
|
-
window: {
|
|
92
|
-
|
|
115
|
+
window: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
choices: WINDOWS,
|
|
118
|
+
description:
|
|
119
|
+
'the window of the figures: today (default, from the local midnight) or the last minutes (implies --stats)',
|
|
120
|
+
},
|
|
121
|
+
sla: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
choices: SLA_SECONDS.map(String),
|
|
124
|
+
description: 'the answer-within target of the service level, seconds (20 by default; implies --stats)',
|
|
125
|
+
},
|
|
93
126
|
};
|
|
94
127
|
|
|
95
128
|
// ── commands ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
96
129
|
const queues = {
|
|
97
130
|
summary: 'the call queues of the company: strategy, calls in them now, agents with their status (Premium)',
|
|
98
|
-
usage:
|
|
131
|
+
usage:
|
|
132
|
+
'wx live queues [--name TEXT] [--pbx SERIAL] [--busy] [--paused] [--active] [--stats] [--window W] [--sla S] [--full]',
|
|
99
133
|
details: [
|
|
100
|
-
|
|
101
|
-
|
|
134
|
+
"The queues with calls first. An agent's status is the queue's view (available, pause…); reason and message are",
|
|
135
|
+
"the user's own status (DND, AWAY and its text). --stats counts since the local midnight (--window today) or over",
|
|
102
136
|
'the last 15/30/45/60 minutes (WDA counts in 15-minute buckets: window.minutes says what was covered). In text the',
|
|
103
137
|
'agents are listed under their queue when 10 queues or fewer are shown.',
|
|
104
138
|
].join('\n'),
|
|
@@ -115,11 +149,16 @@ const queues = {
|
|
|
115
149
|
async run(ctx) {
|
|
116
150
|
const v = ctx.values;
|
|
117
151
|
check('pbx', v.pbx, /^[A-Za-z0-9]{4,32}$/, 'Not a PBX serial');
|
|
118
|
-
if (v.name !== undefined && v.name.length > 100)
|
|
152
|
+
if (v.name !== undefined && v.name.length > 100)
|
|
153
|
+
throw new UsageError('--name is too long (100 characters at most)');
|
|
119
154
|
const stats = wantsStats(v);
|
|
120
155
|
return oneShot(ctx, async (conn, login) => {
|
|
121
156
|
const items = await conn.snapshot('services', stats ? statsOptsOf(ctx) : {});
|
|
122
|
-
return queuesResult(
|
|
157
|
+
return queuesResult(
|
|
158
|
+
items,
|
|
159
|
+
{pbx: v.pbx, name: v.name, busy: v.busy, paused: v.paused, active: v.active, stats},
|
|
160
|
+
viewsOf(ctx, login),
|
|
161
|
+
);
|
|
123
162
|
});
|
|
124
163
|
},
|
|
125
164
|
text: queuesText,
|
|
@@ -128,13 +167,19 @@ const queues = {
|
|
|
128
167
|
const conversations = {
|
|
129
168
|
summary: 'the calls, customer chats and conferences going on now, newest first, with their live insights (Premium)',
|
|
130
169
|
usage: 'wx live conversations [--calls] [--chats] [--conferences] [--mine] [--status S] [--limit N] [--full]',
|
|
131
|
-
details:
|
|
170
|
+
details:
|
|
171
|
+
'byType counts all of them before the filters. --status keeps the chats in that status (or the calls whose last flow is in it).',
|
|
132
172
|
options: {
|
|
133
173
|
calls: {type: 'boolean', description: 'only calls'},
|
|
134
174
|
chats: {type: 'boolean', description: 'only customer chats'},
|
|
135
175
|
conferences: {type: 'boolean', description: 'only conferences'},
|
|
136
176
|
mine: {type: 'boolean', description: 'only your own calls'},
|
|
137
|
-
status: {
|
|
177
|
+
status: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
placeholder: 'S',
|
|
180
|
+
description:
|
|
181
|
+
'only this status: TALKING, HOLD, CONNECTING, WAITFIRSTREPLY, WAITREPLYFROMAGENT, WAITREPLYFROMGUEST…',
|
|
182
|
+
},
|
|
138
183
|
limit: {type: 'string', placeholder: 'N', description: 'at most N items, 1 to 1000 (50 by default)'},
|
|
139
184
|
full,
|
|
140
185
|
},
|
|
@@ -155,7 +200,8 @@ const conversations = {
|
|
|
155
200
|
const presence = {
|
|
156
201
|
summary: 'status, status message, "until" and phone registration of colleagues (is someone available)',
|
|
157
202
|
usage: 'wx live presence <extension>... [--full]',
|
|
158
|
-
details:
|
|
203
|
+
details:
|
|
204
|
+
'Up to 50 extensions of your company. phone is REGISTERED when a phone of theirs is online; an extension nobody has gives status unknown.',
|
|
159
205
|
options: {full},
|
|
160
206
|
args: [1, 50],
|
|
161
207
|
async run(ctx) {
|
|
@@ -199,7 +245,8 @@ const transcript = {
|
|
|
199
245
|
args: [1, 1],
|
|
200
246
|
async run(ctx) {
|
|
201
247
|
const arg = ctx.positionals[0];
|
|
202
|
-
if (!/^(conference:)?[A-Za-z0-9._@:+-]{1,200}$/.test(arg))
|
|
248
|
+
if (!/^(conference:)?[A-Za-z0-9._@:+-]{1,200}$/.test(arg))
|
|
249
|
+
throw new UsageError(`Name the call: its WDA id or its SIP call id (or conference:<id>): ${arg}`);
|
|
203
250
|
const secs = wholeNumber('for', ctx.values.for, 1, MAX_SECONDS) ?? 0;
|
|
204
251
|
return oneShot(ctx, async (conn, login) => {
|
|
205
252
|
const views = viewsOf(ctx, login);
|
|
@@ -209,7 +256,11 @@ const transcript = {
|
|
|
209
256
|
try {
|
|
210
257
|
items = await conn.snapshot('transcriptions', {state: true, filter: [[kind, id]]});
|
|
211
258
|
} catch (e) {
|
|
212
|
-
throw new Error(
|
|
259
|
+
throw new Error(
|
|
260
|
+
e.message.includes('not found')
|
|
261
|
+
? `${kind} ${id} is not going on now (a live transcript exists only while it lasts)`
|
|
262
|
+
: e.message,
|
|
263
|
+
);
|
|
213
264
|
}
|
|
214
265
|
const {call, snapshot} = transcriptSnapshot(items, id, kind, views);
|
|
215
266
|
if (!ctx.values.follow) return snapshot;
|
|
@@ -248,7 +299,8 @@ const transcript = {
|
|
|
248
299
|
|
|
249
300
|
const watch = {
|
|
250
301
|
summary: 'a live feed: one line per change of the conversations, queues or presence until stopped',
|
|
251
|
-
usage:
|
|
302
|
+
usage:
|
|
303
|
+
'wx live watch [--conversations] [--calls] [--mine] [--queues] [--stats] [--window W] [--sla S] [--presence EXT]... [--for SECONDS] [--full]',
|
|
252
304
|
details: [
|
|
253
305
|
'Without a choice: --conversations. --stats implies --queues and adds QUEUES and QUEUE_STATS lines. Events: READY,',
|
|
254
306
|
'RECONNECTING, RECONNECTED, ERROR, CALL, CALL_COMPLETE, CALL_INTERRUPTED, CALL_ANNOTATION, CHAT…, CONFERENCE…,',
|
|
@@ -295,7 +347,9 @@ const watch = {
|
|
|
295
347
|
const subscriptions = (first = false) => [
|
|
296
348
|
...(wantConv ? [subscription('conversations', {state: false})] : []),
|
|
297
349
|
...(wantQueues ? [queueSub(first && wantStats)] : []),
|
|
298
|
-
...(presenceExt.length
|
|
350
|
+
...(presenceExt.length
|
|
351
|
+
? [subscription('presence', {state: false, filter: presenceExt.map((e) => [null, login.company, e])})]
|
|
352
|
+
: []),
|
|
299
353
|
];
|
|
300
354
|
|
|
301
355
|
return new Promise((resolve, reject) => {
|
|
@@ -369,13 +423,17 @@ const watch = {
|
|
|
369
423
|
for (const s of subscriptions(true)) c.send(s);
|
|
370
424
|
attempts = 0;
|
|
371
425
|
clearInterval(keepalive);
|
|
372
|
-
keepalive = setInterval(() =>
|
|
426
|
+
keepalive = setInterval(() => {
|
|
427
|
+
for (const s of subscriptions()) conn?.send(s);
|
|
428
|
+
}, liveOptions.keepaliveMs);
|
|
373
429
|
ctx.emit({
|
|
374
430
|
event: ready ? 'RECONNECTED' : 'READY',
|
|
375
431
|
data: drop({
|
|
376
432
|
extension: login.myExtension,
|
|
377
433
|
license: login.license,
|
|
378
|
-
topics: [wantConv && 'conversations', wantQueues && 'queues', presenceExt.length && 'presence'].filter(
|
|
434
|
+
topics: [wantConv && 'conversations', wantQueues && 'queues', presenceExt.length && 'presence'].filter(
|
|
435
|
+
Boolean,
|
|
436
|
+
),
|
|
379
437
|
calls: v.calls || undefined,
|
|
380
438
|
mine: v.mine || undefined,
|
|
381
439
|
stats: wantQueues && wantStats ? statsOpts() : undefined,
|