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.
Files changed (42) hide show
  1. package/README.md +15 -20
  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
@@ -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 '../auth.mjs';
5
- import {UsageError} from '../errors.mjs';
6
- import {localTime, oneLine, table} from '../format.mjs';
7
- import {query} from '../http.mjs';
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 = ['TODAY', 'YESTERDAY', 'THIS_WEEK', 'PREVIOUS_WEEK', 'THIS_MONTH', 'PREVIOUS_MONTH', 'THIS_QUARTER', 'LAST_30_DAYS'];
12
- export const DIMENSIONS = ['direction', 'entity', 'channel', 'recorded', 'language', 'country', 'location', 'pbx', 'trunk', 'queue', 'service', 'extension', 'group', 'department', 'tag', 'chatbot', 'voicebot', 'duration', 'connectTime', 'talkTime', 'waitTime', 'queueTime', 'holdTime', 'date', 'dayOfWeek', 'hourOfDay'];
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: {type: 'string', multiple: true, choices: ['call', 'conference', 'chat'], description: 'only calls, conferences or chats'},
19
- email: {type: 'string', multiple: true, placeholder: 'E', description: 'a participant by e-mail (all named must have taken part)'},
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: {type: 'boolean', description: 'not only your own conversations: everything the login may see (only when asked)'},
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: {type: 'string', placeholder: 'ZONE', description: 'the time zone of --from/--to/--when (and of the time dimensions of aggregate)'},
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 = [...(values.email ?? []).map((email) => ({email})), ...(values.phone ?? []).map((phone) => ({phone})), ...(values.extension ?? []).map((extension) => ({extension}))];
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 = {absolute: {...(values.from ? {from: checkIso('--from', values.from)} : {}), ...(values.to ? {to: checkIso('--to', values.to)} : {}), ...(tz ? {timeZone: tz} : {})}};
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: type === 'conference' ? unique((v.participants ?? []).map((p) => alt(p.info?.name, p.info?.email)).filter((n) => n !== null)) : null,
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)) throw new UsageError(`not a dimension: ${dim} (one of ${DIMENSIONS.join(', ')}, or projection:<id>:<field>)`);
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].sort((a, b) => b.count - a.count).slice(0, 5).map((b) => ({key: b.key ?? null, count: b.count ?? null})),
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) => [[`${indent}${oneLine(bucketLabel(b), 60)}`, String(b.count)], ...(b.then ? levelRows(b.then, `${indent} `) : [])]);
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 [`${r.count} conversations (${whose}); AI fields with values:`, ...table(r.projections.map((p) => [p.by, String(p.values), p.top.map((t) => `${oneLine(t.key, 30)} ${t.count}`).join(', ')]))];
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) return [head, ...r.results.flatMap((l) => ['', `by ${l.by}:`, ...(l.buckets.length ? table(levelRows(l, ' ')) : [' (none)'])])];
143
- return [head, '', `by ${chain(r).join(', then ')}:`, ...(r.buckets.length ? table(levelRows(r, ' ')) : [' (none)'])];
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: '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]',
172
- details: '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.',
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: {type: 'boolean', description: 'one short object per conversation: type, id, start, duration, title, direction, status, participants, summary'},
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) => ctx.client(wdaClient).post('/insights/conversations/count', {filter: buildFilter(ctx.values, ctx.token)}),
193
- text: (r, ctx) => `${r.count ?? JSON.stringify(r)} conversations (${ctx.values.everyone ? 'visible to the login' : 'your own'})`,
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: 'wx conversations aggregate (--by DIM[,DIM...] | --by DIM --then DIM [--then DIM] | --projections) [the filter of find] [--tz ZONE] [--top N] [--raw]',
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: {type: 'string', multiple: true, placeholder: 'DIM', description: 'break each value of --by down by this dimension (at most two)'},
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: {type: 'boolean', description: 'the AI insight fields that have values in the filter, with their biggest values'},
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) => ctx.client(wdaClient).get(`/insights/conversations/tags${query({tagPrefix: ctx.positionals[0], limit: intOption('--limit', ctx.values.limit, 1, 20)})}`),
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
  },
@@ -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 '../errors.mjs';
4
- import {table} from '../format.mjs';
5
- import {apiPath} from '../http.mjs';
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) => table([['File', f.name ?? ''], ['fsId', f.fsId ?? ''], ['Type', f.mime ?? ''], ['Size', f.size === undefined ? '' : `${f.size} bytes`], ...(f.width ? [['Image', `${f.width}x${f.height}`]] : [])]);
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: '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.',
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) {
@@ -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 '../errors.mjs';
4
- import {paint, table} from '../format.mjs';
5
- import {apiPath, query} from '../http.mjs';
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((sections ?? []).map((s) => [
13
- c.bold(s.name ?? s.sectionId),
14
- s.sectionId,
15
- s.type ?? '',
16
- `${s.channelCount ?? 0} channels`,
17
- s.unmutedUnreadMessageCount ? c.yellow(`${s.unmutedUnreadMessageCount} new in ${s.unmutedUnreadChannelCount ?? '?'}`) : '',
18
- s.unreadMentionCount ? c.yellow(`@${s.unreadMentionCount}`) : '',
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 ? 'mark a channel read, up to the latest message (or up to a message)' : 'mark a channel unread from a message on',
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))) throw new UsageError(`--until must be an ISO 8601 time: ${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);
@@ -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 './login.mjs';
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 './profile.mjs';
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 './status.mjs';
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);
@@ -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 '../auth.mjs';
9
- import {AuthError, UsageError} from '../errors.mjs';
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 {callIdBySip, conversationsResult, drop, makeViews, presenceResult, queuesResult, transcriptEvents, transcriptSnapshot, watchFrames} from '../live/views.mjs';
14
- import {wdaWsUrl} from '../services.mjs';
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 {company: claims.company, myExtension: ext ? String(ext) : undefined, license: claims['custom:pbx_license'], exp: claims.exp};
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({full: Boolean(ctx.values.full), company: login.company, myExtension: login.myExtension, now: () => ctx.clock()});
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) throw new UsageError(`--${name} must be a whole number from ${min} to ${max}: ${value}`);
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 = {type: 'boolean', description: 'everything the service sends, also e-mail, IP address, location and user agent of people'};
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: {type: 'string', choices: WINDOWS, description: 'the window of the figures: today (default, from the local midnight) or the last minutes (implies --stats)'},
92
- sla: {type: 'string', choices: SLA_SECONDS.map(String), description: 'the answer-within target of the service level, seconds (20 by default; implies --stats)'},
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: 'wx live queues [--name TEXT] [--pbx SERIAL] [--busy] [--paused] [--active] [--stats] [--window W] [--sla S] [--full]',
131
+ usage:
132
+ 'wx live queues [--name TEXT] [--pbx SERIAL] [--busy] [--paused] [--active] [--stats] [--window W] [--sla S] [--full]',
99
133
  details: [
100
- 'The queues with calls first. An agent\'s status is the queue\'s view (available, pause…); reason and message are',
101
- 'the user\'s own status (DND, AWAY and its text). --stats counts since the local midnight (--window today) or over',
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) throw new UsageError('--name is too long (100 characters at most)');
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(items, {pbx: v.pbx, name: v.name, busy: v.busy, paused: v.paused, active: v.active, stats}, viewsOf(ctx, login));
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: 'byType counts all of them before the filters. --status keeps the chats in that status (or the calls whose last flow is in it).',
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: {type: 'string', placeholder: 'S', description: 'only this status: TALKING, HOLD, CONNECTING, WAITFIRSTREPLY, WAITREPLYFROMAGENT, WAITREPLYFROMGUEST…'},
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: 'Up to 50 extensions of your company. phone is REGISTERED when a phone of theirs is online; an extension nobody has gives status unknown.',
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)) throw new UsageError(`Name the call: its WDA id or its SIP call id (or conference:<id>): ${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(e.message.includes('not found') ? `${kind} ${id} is not going on now (a live transcript exists only while it lasts)` : e.message);
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: 'wx live watch [--conversations] [--calls] [--mine] [--queues] [--stats] [--window W] [--sla S] [--presence EXT]... [--for SECONDS] [--full]',
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 ? [subscription('presence', {state: false, filter: presenceExt.map((e) => [null, login.company, e])})] : []),
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(() => subscriptions().forEach((s) => conn?.send(s)), liveOptions.keepaliveMs);
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(Boolean),
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,