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
@@ -2,29 +2,77 @@
2
2
  // x-caracal service of WDA, and the list of the queues. Read only. Counts, never percentages from the service: a
3
3
  // rate is computed from the counts (in the text only). A report covers one PBX: the one named by --pbx, else the
4
4
  // PBX of the first --queue (its id is <company>_<pbx>_<number>), else the PBX of the login.
5
- import {UsageError} from '../errors.mjs';
6
- import {localTime, oneLine, table} from '../format.mjs';
7
- import {apiPath, query} from '../http.mjs';
5
+ import {UsageError} from 'wldx-core/errors';
6
+ import {localTime, oneLine, table} from 'wldx-core/format';
7
+ import {apiPath, query} from 'wldx-core/http';
8
8
  import {millis} from '../history/text.mjs';
9
9
  import {callFlow, checkId, checkZone, intOption, plural, wdaClient} from '../history/wda.mjs';
10
10
 
11
- export const OUTCOMES = ['offered', 'received', 'answered', 'completed', 'transferred', 'abandoned', 'unanswered', 'lost', 'lostAbandoned', 'lostUnanswered', 'overflow', 'unknown'];
11
+ export const OUTCOMES = [
12
+ 'offered',
13
+ 'received',
14
+ 'answered',
15
+ 'completed',
16
+ 'transferred',
17
+ 'abandoned',
18
+ 'unanswered',
19
+ 'lost',
20
+ 'lostAbandoned',
21
+ 'lostUnanswered',
22
+ 'overflow',
23
+ 'unknown',
24
+ ];
12
25
 
13
26
  const FILTER_OPTIONS = {
14
- from: {type: 'string', placeholder: 'YYYY-MM-DD', description: 'the first day (today when --from and --to are left out)'},
27
+ from: {
28
+ type: 'string',
29
+ placeholder: 'YYYY-MM-DD',
30
+ description: 'the first day (today when --from and --to are left out)',
31
+ },
15
32
  to: {type: 'string', placeholder: 'YYYY-MM-DD', description: 'the last day, inclusive (today by default)'},
16
33
  days: {type: 'string', placeholder: 'N', description: 'the last N days, today included (1-90)'},
17
34
  tz: {type: 'string', placeholder: 'ZONE', description: "the zone the days are cut in (the PBX's own by default)"},
18
- pbx: {type: 'string', placeholder: 'SERIAL', description: 'the PBX of the report (else the PBX of the first --queue, else of the login)'},
19
- queue: {type: 'string', multiple: true, placeholder: 'ID', description: 'only these queues (ids from wx reports list)'},
20
- extension: {type: 'string', multiple: true, placeholder: 'EXT', description: 'only the calls these users answered, or that rang them unanswered'},
35
+ pbx: {
36
+ type: 'string',
37
+ placeholder: 'SERIAL',
38
+ description: 'the PBX of the report (else the PBX of the first --queue, else of the login)',
39
+ },
40
+ queue: {
41
+ type: 'string',
42
+ multiple: true,
43
+ placeholder: 'ID',
44
+ description: 'only these queues (ids from wx reports list)',
45
+ },
46
+ extension: {
47
+ type: 'string',
48
+ multiple: true,
49
+ placeholder: 'EXT',
50
+ description: 'only the calls these users answered, or that rang them unanswered',
51
+ },
21
52
  outcome: {type: 'string', multiple: true, choices: OUTCOMES, description: 'only calls of these outcomes'},
22
- hours: {type: 'string', placeholder: 'HH:MM-HH:MM', description: 'the time of day the call entered the queue (slow: reads every call)'},
53
+ hours: {
54
+ type: 'string',
55
+ placeholder: 'HH:MM-HH:MM',
56
+ description: 'the time of day the call entered the queue (slow: reads every call)',
57
+ },
23
58
  weekday: {type: 'string', multiple: true, placeholder: '1-7', description: '1 = Monday … 7 = Sunday (slow)'},
24
- wait: {type: 'string', placeholder: 'FROM-TO', description: 'a band of wait seconds: more than FROM, at most TO (TO may be left out) (slow)'},
59
+ wait: {
60
+ type: 'string',
61
+ placeholder: 'FROM-TO',
62
+ description: 'a band of wait seconds: more than FROM, at most TO (TO may be left out) (slow)',
63
+ },
25
64
  talk: {type: 'string', placeholder: 'FROM-TO', description: 'a band of talk seconds (slow)'},
26
- reason: {type: 'string', multiple: true, placeholder: 'R', description: 'disconnect reasons: HANGUP, TRANSFER, TIMEOUT, EMPTY, EXITWITHKEY, … (slow)'},
27
- sla: {type: 'string', placeholder: 'S', description: 'the SLA threshold in seconds, 5 to 120 in steps of 5 (20 by default)'},
65
+ reason: {
66
+ type: 'string',
67
+ multiple: true,
68
+ placeholder: 'R',
69
+ description: 'disconnect reasons: HANGUP, TRANSFER, TIMEOUT, EMPTY, EXITWITHKEY, … (slow)',
70
+ },
71
+ sla: {
72
+ type: 'string',
73
+ placeholder: 'S',
74
+ description: 'the SLA threshold in seconds, 5 to 120 in steps of 5 (20 by default)',
75
+ },
28
76
  'queue-sla': {type: 'string', multiple: true, placeholder: 'ID=S', description: 'the SLA threshold of one queue'},
29
77
  };
30
78
 
@@ -44,7 +92,8 @@ const localDay = (now, back) => {
44
92
  function checkDay(name, value) {
45
93
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
46
94
  const d = m && new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
47
- if (!m || d.getUTCMonth() !== +m[2] - 1 || d.getUTCDate() !== +m[3]) throw new UsageError(`${name} must be a day as YYYY-MM-DD: ${value}`);
95
+ if (!m || d.getUTCMonth() !== +m[2] - 1 || d.getUTCDate() !== +m[3])
96
+ throw new UsageError(`${name} must be a day as YYYY-MM-DD: ${value}`);
48
97
  return value;
49
98
  }
50
99
 
@@ -100,7 +149,8 @@ export function reportFilter(v, now) {
100
149
  f.disconnectReasons = v.reason;
101
150
  }
102
151
  if (v.weekday?.length) {
103
- for (const d of v.weekday) if (!/^[1-7]$/.test(d)) throw new UsageError(`--weekday is 1 (Monday) to 7 (Sunday): ${d}`);
152
+ for (const d of v.weekday)
153
+ if (!/^[1-7]$/.test(d)) throw new UsageError(`--weekday is 1 (Monday) to 7 (Sunday): ${d}`);
104
154
  f.daysOfWeek = v.weekday.map(Number);
105
155
  }
106
156
  if (v.hours !== undefined) {
@@ -142,16 +192,30 @@ function ladder(v, widthKey, stepsKey) {
142
192
  // The queues: {total, pbxs: {pbx: n}, queues: [{id, name, pbx, aclGroup?}]}, by PBX and name.
143
193
  export function shapeQueues(r, {pbx, name} = {}) {
144
194
  const needle = (name ?? '').toLowerCase();
145
- const list = (r.services ?? []).filter((s) => (!pbx || s.pbx === pbx) && (!needle || String(s.name ?? '').toLowerCase().includes(needle)));
195
+ const list = (r.services ?? []).filter(
196
+ (s) =>
197
+ (!pbx || s.pbx === pbx) &&
198
+ (!needle ||
199
+ String(s.name ?? '')
200
+ .toLowerCase()
201
+ .includes(needle)),
202
+ );
146
203
  const pbxs = {};
147
204
  for (const s of [...list].sort((a, b) => cmp(a.pbx, b.pbx))) pbxs[s.pbx] = (pbxs[s.pbx] ?? 0) + 1;
148
205
  const queues = [...list]
149
206
  .sort((a, b) => cmp(a.pbx, b.pbx) || cmp(a.name ?? '', b.name ?? ''))
150
- .map((s) => Object.fromEntries(Object.entries({id: s.id, name: s.name, pbx: s.pbx, aclGroup: s.aclGroup ? s.aclGroup : null}).filter(([, x]) => x !== null && x !== undefined)));
207
+ .map((s) =>
208
+ Object.fromEntries(
209
+ Object.entries({id: s.id, name: s.name, pbx: s.pbx, aclGroup: s.aclGroup ? s.aclGroup : null}).filter(
210
+ ([, x]) => x !== null && x !== undefined,
211
+ ),
212
+ ),
213
+ );
151
214
  return {total: list.length, pbxs, queues};
152
215
  }
153
216
 
154
- const cmp = (a, b) => (a === b ? 0 : a === undefined || a === null ? -1 : b === undefined || b === null ? 1 : a < b ? -1 : 1);
217
+ const cmp = (a, b) =>
218
+ a === b ? 0 : a === undefined || a === null ? -1 : b === undefined || b === null ? 1 : a < b ? -1 : 1;
155
219
 
156
220
  // Customers' numbers and names are shown only with --full.
157
221
  const CALLER = ['callerPhone', 'callerName'];
@@ -160,8 +224,10 @@ const dropCaller = (row) => (row ? Object.fromEntries(Object.entries(row).filter
160
224
  // Text.
161
225
 
162
226
  const n = (x) => (Number.isFinite(x) ? String(x) : '');
163
- const rate = (within, eligible) => (eligible ? `${within}/${eligible} ${Math.round((100 * within) / eligible)}%` : 'no calls');
164
- const slaText = (s) => (s ? `${rate(s.answeredWithin ?? s.abandonedWithin ?? 0, s.eligible ?? 0)} within ${s.thresholdSeconds ?? 20} s` : '');
227
+ const rate = (within, eligible) =>
228
+ eligible ? `${within}/${eligible} ${Math.round((100 * within) / eligible)}%` : 'no calls';
229
+ const slaText = (s) =>
230
+ s ? `${rate(s.answeredWithin ?? s.abandonedWithin ?? 0, s.eligible ?? 0)} within ${s.thresholdSeconds ?? 20} s` : '';
165
231
 
166
232
  function heading(r) {
167
233
  const w = r.window;
@@ -174,7 +240,10 @@ function metricsRows(m = {}) {
174
240
  return [
175
241
  ['Offered', n(c.offered)],
176
242
  ['Received', n(c.received)],
177
- ['Answered', `${(c.completed ?? 0) + (c.transferred ?? 0)} (completed ${c.completed ?? 0}, transferred ${c.transferred ?? 0})`],
243
+ [
244
+ 'Answered',
245
+ `${(c.completed ?? 0) + (c.transferred ?? 0)} (completed ${c.completed ?? 0}, transferred ${c.transferred ?? 0})`,
246
+ ],
178
247
  ['Abandoned', `${c.abandoned ?? 0}${c.shortAbandoned ? ` (within 5 s: ${c.shortAbandoned})` : ''}`],
179
248
  ['Unanswered', n(c.unanswered ?? 0)],
180
249
  ['Lost', `${c.lost ?? 0} (abandoned ${c.lostAbandoned ?? 0}, unanswered ${c.lostUnanswered ?? 0})`],
@@ -191,12 +260,25 @@ function metricsRows(m = {}) {
191
260
  const METRIC_HEAD = ['Offered', 'Answered', 'Abandoned', 'Lost', 'Overflow', 'SLA', 'Avg wait', 'Max wait', 'Avg talk'];
192
261
  const metricCells = (m = {}) => {
193
262
  const c = m.counts ?? {};
194
- return [n(c.offered ?? 0), n((c.completed ?? 0) + (c.transferred ?? 0)), n(c.abandoned ?? 0), n(c.lost ?? 0), n(c.overflow ?? 0), m.sla ? rate(m.sla.answeredWithin ?? 0, m.sla.eligible ?? 0) : '', millis(m.avgWaitMs), millis(m.maxWaitMs), millis(m.avgTalkMs)];
263
+ return [
264
+ n(c.offered ?? 0),
265
+ n((c.completed ?? 0) + (c.transferred ?? 0)),
266
+ n(c.abandoned ?? 0),
267
+ n(c.lost ?? 0),
268
+ n(c.overflow ?? 0),
269
+ m.sla ? rate(m.sla.answeredWithin ?? 0, m.sla.eligible ?? 0) : '',
270
+ millis(m.avgWaitMs),
271
+ millis(m.maxWaitMs),
272
+ millis(m.avgTalkMs),
273
+ ];
195
274
  };
196
275
 
197
276
  // 1 = Monday … 7 = Sunday, as in the --weekday filter.
198
277
  const ISO_WEEKDAYS = [undefined, 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
199
- const periodLabel = (b, granularity) => (granularity === 'dayOfWeek' ? ISO_WEEKDAYS[Number(b.bucketKey)] ?? b.bucketKey : b.bucketKey ?? b.bucketStart ?? '');
278
+ const periodLabel = (b, granularity) =>
279
+ granularity === 'dayOfWeek'
280
+ ? (ISO_WEEKDAYS[Number(b.bucketKey)] ?? b.bucketKey)
281
+ : (b.bucketKey ?? b.bucketStart ?? '');
200
282
 
201
283
  const who = (name, ext) => [name, ext && `(${ext})`].filter(Boolean).join(' ');
202
284
 
@@ -247,7 +329,13 @@ const group = {
247
329
  return shapeQueues(await ctx.client(wdaClient).post('/stream/services', {}), {pbx, name});
248
330
  },
249
331
  text(r) {
250
- const lines = [`${plural(r.total, 'queue')} · ${Object.entries(r.pbxs).map(([p, k]) => `PBX ${p}: ${k}`).join(', ') || 'no PBX'}`];
332
+ const lines = [
333
+ `${plural(r.total, 'queue')} · ${
334
+ Object.entries(r.pbxs)
335
+ .map(([p, k]) => `PBX ${p}: ${k}`)
336
+ .join(', ') || 'no PBX'
337
+ }`,
338
+ ];
251
339
  if (r.queues.length) lines.push(...table(r.queues.map((q) => [q.pbx ?? '', oneLine(q.name ?? '', 50), q.id])));
252
340
  return lines;
253
341
  },
@@ -255,7 +343,8 @@ const group = {
255
343
  summary: {
256
344
  summary: 'every queue call of the filter: offered, answered, abandoned, lost, waits, talk, ring, the SLA counts',
257
345
  usage: 'wx reports summary [filter]',
258
- details: 'Filter: --from/--to days or --days N (today by default; at most 90 days), --tz, --pbx, --queue, --extension, --outcome, --sla, --queue-sla; slow ones: --hours, --weekday, --wait, --talk, --reason.',
346
+ details:
347
+ 'Filter: --from/--to days or --days N (today by default; at most 90 days), --tz, --pbx, --queue, --extension, --outcome, --sla, --queue-sla; slow ones: --hours, --weekday, --wait, --talk, --reason.',
259
348
  options: FILTER_OPTIONS,
260
349
  args: [0, 0],
261
350
  run: (ctx) => report(ctx, 'distribution/summary/query'),
@@ -267,38 +356,81 @@ const group = {
267
356
  options: FILTER_OPTIONS,
268
357
  args: [0, 0],
269
358
  run: (ctx) => report(ctx, 'distribution/by-service/query'),
270
- text: (r) => [heading(r), ...table([['Queue', ...METRIC_HEAD], ...(r.groups ?? []).map((g) => [oneLine(g.serviceName ?? g.serviceId ?? '', 40), ...metricCells(g.metrics)])])],
359
+ text: (r) => [
360
+ heading(r),
361
+ ...table([
362
+ ['Queue', ...METRIC_HEAD],
363
+ ...(r.groups ?? []).map((g) => [oneLine(g.serviceName ?? g.serviceId ?? '', 40), ...metricCells(g.metrics)]),
364
+ ]),
365
+ ],
271
366
  },
272
367
  'by-period': {
273
368
  summary: 'the same figures as a series (hour: the 24 hours of the day over the window)',
274
369
  usage: 'wx reports by-period --by hour|day|week|month|weekday [filter]',
275
- options: {...FILTER_OPTIONS, by: {type: 'string', choices: ['hour', 'day', 'week', 'month', 'weekday'], description: 'the period of a bucket'}},
370
+ options: {
371
+ ...FILTER_OPTIONS,
372
+ by: {
373
+ type: 'string',
374
+ choices: ['hour', 'day', 'week', 'month', 'weekday'],
375
+ description: 'the period of a bucket',
376
+ },
377
+ },
276
378
  args: [0, 0],
277
379
  run(ctx) {
278
380
  if (!ctx.values.by) throw new UsageError('by-period needs --by hour|day|week|month|weekday', ctx.usage());
279
- return report(ctx, 'distribution/by-period/query', {granularity: ctx.values.by === 'weekday' ? 'dayOfWeek' : ctx.values.by});
381
+ return report(ctx, 'distribution/by-period/query', {
382
+ granularity: ctx.values.by === 'weekday' ? 'dayOfWeek' : ctx.values.by,
383
+ });
280
384
  },
281
- text: (r) => [heading(r), ...table([['Period', ...METRIC_HEAD], ...(r.buckets ?? []).map((b) => [periodLabel(b, r.granularity), ...metricCells(b.metrics)])])],
385
+ text: (r) => [
386
+ heading(r),
387
+ ...table([
388
+ ['Period', ...METRIC_HEAD],
389
+ ...(r.buckets ?? []).map((b) => [periodLabel(b, r.granularity), ...metricCells(b.metrics)]),
390
+ ]),
391
+ ],
282
392
  },
283
393
  answered: {
284
394
  summary: 'the answered calls: summary, or grouped by queue, agent, user, reason or talk time',
285
395
  usage: 'wx reports answered [--by queue|agent|user|reason|talk-time] [--band S --steps N] [filter]',
286
- details: 'agent: a person in one queue; user: a person over every queue; talk-time: bands of --band seconds, --steps of them. The breakdowns read every call of the window (slow).',
287
- options: {...FILTER_OPTIONS, by: {type: 'string', choices: ['queue', 'agent', 'user', 'reason', 'talk-time'], description: 'group the calls'}, ...LADDER},
396
+ details:
397
+ 'agent: a person in one queue; user: a person over every queue; talk-time: bands of --band seconds, --steps of them. The breakdowns read every call of the window (slow).',
398
+ options: {
399
+ ...FILTER_OPTIONS,
400
+ by: {
401
+ type: 'string',
402
+ choices: ['queue', 'agent', 'user', 'reason', 'talk-time'],
403
+ description: 'group the calls',
404
+ },
405
+ ...LADDER,
406
+ },
288
407
  args: [0, 0],
289
408
  run(ctx) {
290
409
  const v = ctx.values;
291
- if (v.by !== 'talk-time' && (v.band !== undefined || v.steps !== undefined)) throw new UsageError('--band and --steps go with --by talk-time');
410
+ if (v.by !== 'talk-time' && (v.band !== undefined || v.steps !== undefined))
411
+ throw new UsageError('--band and --steps go with --by talk-time');
292
412
  if (!v.by) return report(ctx, 'answered/summary/query');
293
- const dimension = {queue: 'byService', agent: 'byAgent', user: 'byUser', reason: 'byDisconnectReason', 'talk-time': 'byTalkTime'}[v.by];
294
- return report(ctx, 'answered/breakdown/query', {...(dimension === 'byTalkTime' ? ladder(v, 'talkBandSeconds', 'talkBandSteps') : {}), dimension});
413
+ const dimension = {
414
+ queue: 'byService',
415
+ agent: 'byAgent',
416
+ user: 'byUser',
417
+ reason: 'byDisconnectReason',
418
+ 'talk-time': 'byTalkTime',
419
+ }[v.by];
420
+ return report(ctx, 'answered/breakdown/query', {
421
+ ...(dimension === 'byTalkTime' ? ladder(v, 'talkBandSeconds', 'talkBandSteps') : {}),
422
+ dimension,
423
+ });
295
424
  },
296
425
  text(r) {
297
426
  if (!r.groups) {
298
427
  return [
299
428
  heading(r),
300
429
  ...table([
301
- ['Answered', `${(r.completed ?? 0) + (r.transferred ?? 0)} (completed ${r.completed ?? 0}, transferred ${r.transferred ?? 0})`],
430
+ [
431
+ 'Answered',
432
+ `${(r.completed ?? 0) + (r.transferred ?? 0)} (completed ${r.completed ?? 0}, transferred ${r.transferred ?? 0})`,
433
+ ],
302
434
  ...(r.unknown ? [['Unknown', n(r.unknown)]] : []),
303
435
  ['Service level', slaText(r.sla)],
304
436
  ['Wait', `avg ${millis(r.avgWaitMs)}, max ${millis(r.maxWaitMs)}`],
@@ -308,7 +440,18 @@ const group = {
308
440
  }
309
441
  return [
310
442
  `${heading(r)} · ${r.total ?? 0} answered`,
311
- ...table([['', 'Calls', 'Completed', 'Transferred', 'Avg wait', 'Avg talk', 'Total talk'], ...r.groups.map((g) => [oneLine(groupKey(g, r.dimension), 60), n(g.calls ?? 0), n(g.completed ?? 0), n(g.transferred ?? 0), millis(g.avgWaitMs), millis(g.avgTalkMs), millis(g.totalTalkMs)])]),
443
+ ...table([
444
+ ['', 'Calls', 'Completed', 'Transferred', 'Avg wait', 'Avg talk', 'Total talk'],
445
+ ...r.groups.map((g) => [
446
+ oneLine(groupKey(g, r.dimension), 60),
447
+ n(g.calls ?? 0),
448
+ n(g.completed ?? 0),
449
+ n(g.transferred ?? 0),
450
+ millis(g.avgWaitMs),
451
+ millis(g.avgTalkMs),
452
+ millis(g.totalTalkMs),
453
+ ]),
454
+ ]),
312
455
  ];
313
456
  },
314
457
  },
@@ -319,7 +462,9 @@ const group = {
319
462
  args: [0, 0],
320
463
  run(ctx) {
321
464
  if (!ctx.values.by) return report(ctx, 'unanswered/summary/query');
322
- return report(ctx, 'unanswered/breakdown/query', {dimension: ctx.values.by === 'queue' ? 'byService' : 'byDisconnectReason'});
465
+ return report(ctx, 'unanswered/breakdown/query', {
466
+ dimension: ctx.values.by === 'queue' ? 'byService' : 'byDisconnectReason',
467
+ });
323
468
  },
324
469
  text(r) {
325
470
  if (!r.groups) {
@@ -332,14 +477,28 @@ const group = {
332
477
  ['Overflow', n(r.overflow ?? 0)],
333
478
  ...(r.unknown ? [['Unknown', n(r.unknown)]] : []),
334
479
  ['Given up', slaText(r.sla)],
335
- ['Wait', `avg ${millis(r.avgWaitMs)}, max ${millis(r.maxWaitMs)}, abandoned avg ${millis(r.abandonedAvgWaitMs)}`],
480
+ [
481
+ 'Wait',
482
+ `avg ${millis(r.avgWaitMs)}, max ${millis(r.maxWaitMs)}, abandoned avg ${millis(r.abandonedAvgWaitMs)}`,
483
+ ],
336
484
  ['Place in line', `avg start ${n(r.avgStartPosition ?? 0)}, avg final ${n(r.avgFinalPosition ?? 0)}`],
337
485
  ]),
338
486
  ];
339
487
  }
340
488
  return [
341
489
  `${heading(r)} · ${r.total ?? 0} not answered`,
342
- ...table([['', 'Abandoned', 'Unanswered', 'Lost', 'Overflow', 'Avg wait', 'Max wait'], ...r.groups.map((g) => [oneLine(groupKey(g, r.dimension), 60), n(g.abandoned ?? 0), n(g.unanswered ?? 0), n(g.lost ?? 0), n(g.overflow ?? 0), millis(g.avgWaitMs), millis(g.maxWaitMs)])]),
490
+ ...table([
491
+ ['', 'Abandoned', 'Unanswered', 'Lost', 'Overflow', 'Avg wait', 'Max wait'],
492
+ ...r.groups.map((g) => [
493
+ oneLine(groupKey(g, r.dimension), 60),
494
+ n(g.abandoned ?? 0),
495
+ n(g.unanswered ?? 0),
496
+ n(g.lost ?? 0),
497
+ n(g.overflow ?? 0),
498
+ millis(g.avgWaitMs),
499
+ millis(g.maxWaitMs),
500
+ ]),
501
+ ]),
343
502
  ];
344
503
  },
345
504
  },
@@ -350,20 +509,27 @@ const group = {
350
509
  args: [1, 1],
351
510
  run(ctx) {
352
511
  const kind = ctx.positionals[0];
353
- if (kind !== 'answered' && kind !== 'unanswered') throw new UsageError('service-level answered|unanswered', ctx.usage());
512
+ if (kind !== 'answered' && kind !== 'unanswered')
513
+ throw new UsageError('service-level answered|unanswered', ctx.usage());
354
514
  return report(ctx, `${kind}/service-level/query`, ladder(ctx.values, 'waitBandSeconds', 'waitBandSteps'));
355
515
  },
356
516
  text(r, ctx) {
357
517
  const unanswered = ctx.positionals[0] === 'unanswered';
358
518
  const head = `${heading(r)} · ${r.total ?? 0} ${unanswered ? 'not answered' : 'answered'}${unanswered ? ` (lost ${r.lost ?? 0}, overflow ${r.overflow ?? 0})` : ''}`;
359
- const rows = (r.bands ?? []).map((b) => [`within ${b.withinSeconds} s`, n(b.count), `+${b.delta ?? 0}`, ...(unanswered ? [`lost ${b.lost ?? 0}`, `overflow ${b.overflow ?? 0}`] : [])]);
519
+ const rows = (r.bands ?? []).map((b) => [
520
+ `within ${b.withinSeconds} s`,
521
+ n(b.count),
522
+ `+${b.delta ?? 0}`,
523
+ ...(unanswered ? [`lost ${b.lost ?? 0}`, `overflow ${b.overflow ?? 0}`] : []),
524
+ ]);
360
525
  return [head, ...table(rows)];
361
526
  },
362
527
  },
363
528
  calls: {
364
529
  summary: 'the queue calls one by one, newest first: queue, times, wait, talk, outcome, who answered',
365
530
  usage: 'wx reports calls [--limit N] [--pages N] [--cursor C] [--call CALLID] [--full] [filter]',
366
- details: '50 a page (--limit up to 200); --pages follows the next pages (up to 20); --call: every queue a phone call went through. The caller (number and name) only with --full: show it when the user asks for the callers.',
531
+ details:
532
+ '50 a page (--limit up to 200); --pages follows the next pages (up to 20); --call: every queue a phone call went through. The caller (number and name) only with --full: show it when the user asks for the callers.',
367
533
  options: {
368
534
  ...FILTER_OPTIONS,
369
535
  limit: {type: 'string', placeholder: 'N', description: 'calls per page (1-200, 50 by default)'},
@@ -377,7 +543,8 @@ const group = {
377
543
  const v = ctx.values;
378
544
  const limit = intOption('--limit', v.limit, 1, 200);
379
545
  const pages = intOption('--pages', v.pages, 1, 20) ?? 1;
380
- if (v.cursor !== undefined && (v.cursor.length > 2048 || !/^[A-Za-z0-9_=+/.:-]+$/.test(v.cursor))) throw new UsageError('not a cursor');
546
+ if (v.cursor !== undefined && (v.cursor.length > 2048 || !/^[A-Za-z0-9_=+/.:-]+$/.test(v.cursor)))
547
+ throw new UsageError('not a cursor');
381
548
  const filter = reportFilter(v, ctx.now);
382
549
  const x = {};
383
550
  if (limit !== undefined) x.limit = limit;
@@ -416,12 +583,20 @@ const group = {
416
583
  async run(ctx) {
417
584
  const {callId, flow} = callFlow(ctx.positionals);
418
585
  const q = ctx.values.queue === undefined ? undefined : checkId('the queue id', ctx.values.queue);
419
- const r = await ctx.client(wdaClient).get(apiPath`/xcaracal/services/calls/${callId}/flows/${String(flow)}/events` + query({serviceId: q}));
586
+ const r = await ctx
587
+ .client(wdaClient)
588
+ .get(apiPath`/xcaracal/services/calls/${callId}/flows/${String(flow)}/events` + query({serviceId: q}));
420
589
  return ctx.values.full ? r : {...r, call: dropCaller(r.call)};
421
590
  },
422
591
  text(r, ctx) {
423
592
  const lines = r.call ? table([callLine(r.call, ctx.values.full)]) : [];
424
- const rows = (r.events ?? []).map((e) => [localTime(e.time, {seconds: true}), e.action ?? '', e.reason ?? '', who(e.userName, e.userExtension), e.queuePosition === undefined ? '' : `place ${e.queuePosition}`]);
593
+ const rows = (r.events ?? []).map((e) => [
594
+ localTime(e.time, {seconds: true}),
595
+ e.action ?? '',
596
+ e.reason ?? '',
597
+ who(e.userName, e.userExtension),
598
+ e.queuePosition === undefined ? '' : `place ${e.queuePosition}`,
599
+ ]);
425
600
  return [...lines, ...(rows.length ? table(rows).map((l) => ` ${l}`) : [' No events.'])];
426
601
  },
427
602
  },
@@ -1,14 +1,15 @@
1
1
  // wx section: the user's sections of the chat inbox (the agent skill's sections.sh). The list and its counters come
2
2
  // from the inbox; creating, renaming, deleting and moving channels are not implemented by the service yet.
3
- import {UsageError} from '../errors.mjs';
4
- import {apiPath} from '../http.mjs';
3
+ import {UsageError} from 'wldx-core/errors';
4
+ import {apiPath} from 'wldx-core/http';
5
5
  import {channelId} from '../stream.mjs';
6
6
  import {done} from '../chat/format.mjs';
7
7
  import {NOT_IMPLEMENTED, api, notImplemented} from '../chat/lib.mjs';
8
8
  import {sectionLines} from './inbox.mjs';
9
9
 
10
10
  function sectionId(value) {
11
- if (!/^[a-z0-9_-]{1,32}$/.test(String(value))) throw new UsageError(`A section id must match [a-z0-9_-] (up to 32): ${value}`);
11
+ if (!/^[a-z0-9_-]{1,32}$/.test(String(value)))
12
+ throw new UsageError(`A section id must match [a-z0-9_-] (up to 32): ${value}`);
12
13
  return value;
13
14
  }
14
15
 
@@ -49,11 +50,51 @@ const section = {
49
50
  auth: true,
50
51
  commands: {
51
52
  list,
52
- create: make('create', 'create a section', '<sectionId> <name>', 'CreateSection', 2, ([id, name]) => ['POST', '/sections', {sectionId: sectionId(id), name: sectionName(name)}], 'Created'),
53
- rename: make('rename', 'rename a section', '<sectionId> <name>', 'RenameSection', 2, ([id, name]) => ['PATCH', apiPath`/sections/${sectionId(id)}`, {name: sectionName(name)}], 'Renamed'),
54
- delete: make('delete', 'delete a section', '<sectionId>', 'DeleteSection', 1, ([id]) => ['DELETE', apiPath`/sections/${sectionId(id)}`], 'Deleted'),
55
- add: make('add', 'put a channel into a section', '<channel> <sectionId>', 'MoveChannelToSection', 2, ([ch, id]) => ['PUT', apiPath`/channels/${channelId(ch)}/sections/${sectionId(id)}`], 'Added'),
56
- remove: make('remove', 'take a channel out of a section', '<channel> <sectionId>', 'RemoveChannelFromSection', 2, ([ch, id]) => ['DELETE', apiPath`/channels/${channelId(ch)}/sections/${sectionId(id)}`], 'Removed'),
53
+ create: make(
54
+ 'create',
55
+ 'create a section',
56
+ '<sectionId> <name>',
57
+ 'CreateSection',
58
+ 2,
59
+ ([id, name]) => ['POST', '/sections', {sectionId: sectionId(id), name: sectionName(name)}],
60
+ 'Created',
61
+ ),
62
+ rename: make(
63
+ 'rename',
64
+ 'rename a section',
65
+ '<sectionId> <name>',
66
+ 'RenameSection',
67
+ 2,
68
+ ([id, name]) => ['PATCH', apiPath`/sections/${sectionId(id)}`, {name: sectionName(name)}],
69
+ 'Renamed',
70
+ ),
71
+ delete: make(
72
+ 'delete',
73
+ 'delete a section',
74
+ '<sectionId>',
75
+ 'DeleteSection',
76
+ 1,
77
+ ([id]) => ['DELETE', apiPath`/sections/${sectionId(id)}`],
78
+ 'Deleted',
79
+ ),
80
+ add: make(
81
+ 'add',
82
+ 'put a channel into a section',
83
+ '<channel> <sectionId>',
84
+ 'MoveChannelToSection',
85
+ 2,
86
+ ([ch, id]) => ['PUT', apiPath`/channels/${channelId(ch)}/sections/${sectionId(id)}`],
87
+ 'Added',
88
+ ),
89
+ remove: make(
90
+ 'remove',
91
+ 'take a channel out of a section',
92
+ '<channel> <sectionId>',
93
+ 'RemoveChannelFromSection',
94
+ 2,
95
+ ([ch, id]) => ['DELETE', apiPath`/channels/${channelId(ch)}/sections/${sectionId(id)}`],
96
+ 'Removed',
97
+ ),
57
98
  },
58
99
  };
59
100
 
@@ -1,9 +1,9 @@
1
1
  // wx user: people of the corporate directory on the stream service (the agent skill's users.sh and search.sh
2
2
  // users): who you are, a colleague by name (any script, typos, as it sounds), e-mail or phone.
3
- import {jwtClaims} from '../auth.mjs';
4
- import {UsageError} from '../errors.mjs';
5
- import {paint, table} from '../format.mjs';
6
- import {query} from '../http.mjs';
3
+ import {jwtClaims} from 'wldx-core/auth';
4
+ import {UsageError} from 'wldx-core/errors';
5
+ import {paint, table} from 'wldx-core/format';
6
+ import {query} from 'wldx-core/http';
7
7
  import {nextPage, userLine} from '../chat/format.mjs';
8
8
  import {api, checkId, checkInt, displayName} from '../chat/lib.mjs';
9
9
 
@@ -20,7 +20,11 @@ const me = {
20
20
  },
21
21
  text(u, ctx) {
22
22
  if (!u) return 'Not found in the chat directory.';
23
- const rows = [['Name', paint(ctx).bold(displayName(u) ?? '')], ['Id', u.id], ['E-mail', u.email ?? '']];
23
+ const rows = [
24
+ ['Name', paint(ctx).bold(displayName(u) ?? '')],
25
+ ['Id', u.id],
26
+ ['E-mail', u.email ?? ''],
27
+ ];
24
28
  if (u.pbxExtension) rows.push(['Extension', u.pbxExtension]);
25
29
  if (u.timeZone) rows.push(['Time zone', u.timeZone]);
26
30
  if (u.locale) rows.push(['Locale', u.locale]);
@@ -31,12 +35,21 @@ const me = {
31
35
  const brief = (r) => ({
32
36
  margin: r?.margin,
33
37
  nextCursor: r?.nextCursor,
34
- results: (r?.results ?? []).map((x) => ({id: x.user?.id, name: x.user?.displayName, email: x.user?.email, extension: x.user?.pbxExtension, bot: x.user?.bot, confidence: x.confidence, matchedOn: x.matchedOn})),
38
+ results: (r?.results ?? []).map((x) => ({
39
+ id: x.user?.id,
40
+ name: x.user?.displayName,
41
+ email: x.user?.email,
42
+ extension: x.user?.pbxExtension,
43
+ bot: x.user?.bot,
44
+ confidence: x.confidence,
45
+ matchedOn: x.matchedOn,
46
+ })),
35
47
  });
36
48
 
37
49
  const find = {
38
50
  summary: 'find a colleague by name (any script, typos, as it sounds), e-mail or phone',
39
- usage: 'wx user find <query> [--mode auto|name|email|phone] [--kind human|bot|all] [--with-extension] [--min-confidence 0..1] [--brief] [--size 1-50] [--cursor C]',
51
+ usage:
52
+ 'wx user find <query> [--mode auto|name|email|phone] [--kind human|bot|all] [--with-extension] [--min-confidence 0..1] [--brief] [--size 1-50] [--cursor C]',
40
53
  details: [
41
54
  'An e-mail or a phone number (with its country code, +…) is looked up exactly (confidence 1). confidence (0..1)',
42
55
  'is how much the query looks like the name, not the probability that it is the right person (default cut 0.5;',
@@ -45,10 +58,18 @@ const find = {
45
58
  'matchedOn (JSON).',
46
59
  ].join('\n'),
47
60
  options: {
48
- mode: {type: 'string', choices: ['auto', 'name', 'email', 'phone'], description: 'what the query is (auto by default)'},
61
+ mode: {
62
+ type: 'string',
63
+ choices: ['auto', 'name', 'email', 'phone'],
64
+ description: 'what the query is (auto by default)',
65
+ },
49
66
  kind: {type: 'string', choices: ['human', 'bot', 'all'], description: 'people (default), bots or both'},
50
67
  'with-extension': {type: 'boolean', description: 'only people with a PBX extension'},
51
- 'min-confidence': {type: 'string', placeholder: '0..1', description: 'drop hits below this confidence (default 0.5)'},
68
+ 'min-confidence': {
69
+ type: 'string',
70
+ placeholder: '0..1',
71
+ description: 'drop hits below this confidence (default 0.5)',
72
+ },
52
73
  brief: {type: 'boolean', description: 'only the fields needed to pick a person'},
53
74
  size: {type: 'string', placeholder: '1-50', description: 'results per page (default 20)'},
54
75
  cursor: {type: 'string', description: 'the next page (nextCursor of the previous one)'},
@@ -57,7 +78,8 @@ const find = {
57
78
  async run(ctx) {
58
79
  const {values} = ctx;
59
80
  const minc = values['min-confidence'];
60
- if (minc !== undefined && !/^(0|1|0?\.[0-9]+|1\.0+)$/.test(minc)) throw new UsageError(`--min-confidence is a number from 0 to 1: ${minc}`);
81
+ if (minc !== undefined && !/^(0|1|0?\.[0-9]+|1\.0+)$/.test(minc))
82
+ throw new UsageError(`--min-confidence is a number from 0 to 1: ${minc}`);
61
83
  const q = query({
62
84
  q: ctx.positionals[0],
63
85
  mode: values.mode,
@@ -79,7 +101,12 @@ const find = {
79
101
  return [...userLine(u, c), `${Number(x.confidence ?? 0).toFixed(2)} ${x.matchedOn ?? ''}`.trim()];
80
102
  });
81
103
  const lines = table(rows);
82
- if (hits.length > 1 && typeof r.margin === 'number') lines.push(r.margin < 0.1 ? c.yellow(`Ambiguous (margin ${r.margin.toFixed(2)}): several people fit, ask which one`) : c.dim(`margin ${r.margin.toFixed(2)}`));
104
+ if (hits.length > 1 && typeof r.margin === 'number')
105
+ lines.push(
106
+ r.margin < 0.1
107
+ ? c.yellow(`Ambiguous (margin ${r.margin.toFixed(2)}): several people fit, ask which one`)
108
+ : c.dim(`margin ${r.margin.toFixed(2)}`),
109
+ );
83
110
  return [...lines, ...nextPage(r.nextCursor, c)];
84
111
  },
85
112
  };
@@ -90,7 +117,8 @@ const PHONE = /^\+?[0-9]{4,17}$/;
90
117
  const get = {
91
118
  summary: 'people by user id, e-mail or phone (exact lookup)',
92
119
  usage: 'wx user get [<id|email|phone>...] [--id USERID]... [--email EMAIL]... [--phone PHONE]...',
93
- details: 'A value given without an option is an e-mail when it has "@", a phone when it is digits (+ allowed), else a user id. For a search by name: wx user find.',
120
+ details:
121
+ 'A value given without an option is an e-mail when it has "@", a phone when it is digits (+ allowed), else a user id. For a search by name: wx user find.',
94
122
  options: {
95
123
  id: {type: 'string', multiple: true, placeholder: 'USERID', description: 'a user id'},
96
124
  email: {type: 'string', multiple: true, placeholder: 'EMAIL', description: 'an e-mail address'},
@@ -111,7 +139,8 @@ const get = {
111
139
  if (ids.length) q.ids = ids.map((id) => checkId('The user id', id));
112
140
  if (emails.length) q.emails = emails;
113
141
  if (phones.length) q.phones = phones;
114
- if (!Object.keys(q).length) throw new UsageError('Give a user id, an e-mail or a phone (or --id, --email, --phone)', ctx.usage());
142
+ if (!Object.keys(q).length)
143
+ throw new UsageError('Give a user id, an e-mail or a phone (or --id, --email, --phone)', ctx.usage());
115
144
  return api(ctx).call('POST', '/users/query', {query: q});
116
145
  },
117
146
  text(r, ctx) {