@velaro/cli 1.4.100 → 1.4.102
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { messagingGet, messagingPost } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
|
+
import { yearInReviewBody } from '../year-in-review-range.js';
|
|
3
4
|
|
|
4
5
|
// -- velaro enterprise-reports ----------------------------------------------------
|
|
5
6
|
// Backend: velaro-messaging /api/enterprise-reports/* (Enterprise Reports package: enterprise-internal, default OFF;
|
|
@@ -20,6 +21,7 @@ const REPORTS = {
|
|
|
20
21
|
'promise-ledger': 'Agent commitments made in chat and whether a follow-up appeared; overdue list',
|
|
21
22
|
'promise-dismiss': 'Dismiss one promise by --id (managers only)',
|
|
22
23
|
'weekly-brief-send-test': 'Email this week\'s brief to the signed-in user',
|
|
24
|
+
'year-in-review': 'Build the 12-month Year in review document: --from/--to (default last 12 whole Pacific months) starts a build and prints its id; --id <id> checks status and the download link',
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export const enterpriseReportsCommand = {
|
|
@@ -43,12 +45,21 @@ export const enterpriseReportsCommand = {
|
|
|
43
45
|
.option('time-zone', { type: 'string', describe: 'demand-coverage: IANA time zone (default America/Los_Angeles)' })
|
|
44
46
|
.option('low-score', { type: 'number', describe: 'bot-containment: QA score below which a handed-off chat counts as regret (1-100, default 60)' })
|
|
45
47
|
.option('include-demo', { type: 'boolean', describe: 'Show only the seeded sample (demo) data' })
|
|
46
|
-
.option('id', { type: 'string', describe: 'promise-dismiss: the promise id' }),
|
|
48
|
+
.option('id', { type: 'string', describe: 'promise-dismiss: the promise id; year-in-review: the build id to check' }),
|
|
47
49
|
handler: runCommand(async (argv) => {
|
|
48
50
|
if (argv.report === 'weekly-brief-send-test') {
|
|
49
51
|
console.log(JSON.stringify(await messagingPost('/api/enterprise-reports/weekly-brief/send-test', {}), null, 2));
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
54
|
+
if (argv.report === 'year-in-review') {
|
|
55
|
+
// POST starts a build (202 {id}); with --id, GET its status {status: queued|building|completed|failed, downloadUrl, error}. 403 when the key is off.
|
|
56
|
+
if (argv.id) {
|
|
57
|
+
console.log(JSON.stringify(await messagingGet(`/api/enterprise-reports/year-in-review/${encodeURIComponent(String(argv.id))}`), null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(JSON.stringify(await messagingPost('/api/enterprise-reports/year-in-review', yearInReviewBody(argv.from, argv.to)), null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
52
63
|
if (argv.report === 'promise-dismiss') {
|
|
53
64
|
if (!argv.id) throw new Error('--id is required for promise-dismiss');
|
|
54
65
|
console.log(JSON.stringify(await messagingPost(`/api/enterprise-reports/promise-ledger/${encodeURIComponent(String(argv.id))}/dismiss`, {}), null, 2));
|
package/lib/commands/migrate.js
CHANGED
|
@@ -12,7 +12,8 @@ export const migrateCommand = {
|
|
|
12
12
|
.command(migrateTeamsCommand)
|
|
13
13
|
.command(migrateWorkflowsCommand)
|
|
14
14
|
.command(migrateRulesCommand)
|
|
15
|
-
.
|
|
15
|
+
.command(migrateAdviseCommand)
|
|
16
|
+
.demandCommand(1, 'Specify a subcommand: export, livefluence, url-rules, teams, workflows, rules, advise'),
|
|
16
17
|
handler: () => {},
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -321,3 +322,52 @@ const migrateRulesCommand = {
|
|
|
321
322
|
.demandCommand(1, 'Specify: list, set-enabled'),
|
|
322
323
|
handler: () => {},
|
|
323
324
|
};
|
|
325
|
+
|
|
326
|
+
// ── conversion advisor: consolidated v20 design from a v10 site or an intent list ──
|
|
327
|
+
|
|
328
|
+
const migrateAdviseCommand = {
|
|
329
|
+
command: 'advise',
|
|
330
|
+
describe: 'Livefluence conversion advisor: recommend (dry run, default) or build (--apply) a consolidated v20 routing design. Staff only.',
|
|
331
|
+
builder: (y) =>
|
|
332
|
+
y
|
|
333
|
+
.option('v10-site-id', { type: 'number', alias: 'v', describe: 'V10 (Livefluence) site to read (omit when using --intents)' })
|
|
334
|
+
.option('v20-site-id', { type: 'number', alias: 's', describe: 'V20 destination site ID', demandOption: true })
|
|
335
|
+
.option('intents', { type: 'string', describe: 'JSON array (or @file.json) of intents: [{"intent":"support","paths":["/support"],"device":"any","target":"Service team + troubleshooting AI node"}]' })
|
|
336
|
+
.option('apply', { type: 'boolean', default: false, describe: 'Create missing teams and DISABLED workflows (default: dry run + diff)' })
|
|
337
|
+
.option('include-non-routing', { type: 'boolean', default: false, describe: 'Also read v10 rules with IsRouting=false (group-assignment rules)' }),
|
|
338
|
+
handler: runCommand(async (argv) => {
|
|
339
|
+
let intents = null;
|
|
340
|
+
if (argv.intents) {
|
|
341
|
+
const raw = argv.intents.startsWith('@')
|
|
342
|
+
? (await import('node:fs')).readFileSync(argv.intents.slice(1), 'utf8')
|
|
343
|
+
: argv.intents;
|
|
344
|
+
intents = JSON.parse(raw);
|
|
345
|
+
}
|
|
346
|
+
if (!intents && !argv['v10-site-id']) throw new Error('Give -v <v10 site> or --intents.');
|
|
347
|
+
|
|
348
|
+
const result = await post('/DatabaseTool/ConversionAdvisor', {
|
|
349
|
+
v10SiteId: intents ? null : argv['v10-site-id'],
|
|
350
|
+
v20SiteId: argv['v20-site-id'],
|
|
351
|
+
apply: argv.apply,
|
|
352
|
+
includeNonRouting: argv['include-non-routing'],
|
|
353
|
+
intents,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
console.log(`\n${'─'.repeat(60)}`);
|
|
357
|
+
console.log(` Conversion advisor (${result.source}) -> V20 site ${result.v20SiteId} ${result.dryRun ? 'DRY RUN' : 'APPLIED (all workflows disabled)'}`);
|
|
358
|
+
if (result.v10RuleCount) console.log(` ${result.v10RuleCount} v10 rule(s) -> ${result.consolidatedWorkflows} workflow(s)`);
|
|
359
|
+
console.log(`${'─'.repeat(60)}\n`);
|
|
360
|
+
for (const t of result.teams ?? [])
|
|
361
|
+
console.log(` team ${t.status.padEnd(8)} ${t.name}${t.v10Members ? ` (${t.v10Members} v10 members, assign v20 agents by hand)` : ''}`);
|
|
362
|
+
console.log('');
|
|
363
|
+
for (const w of result.workflows ?? []) {
|
|
364
|
+
console.log(` [${w.status}] #${w.priority} ${w.name} mode=${w.mode}${w.device !== 'any' ? ` device=${w.device}` : ''}`);
|
|
365
|
+
console.log(` team: ${w.teamName ?? '-'} AI node: ${w.aiNodeName ?? '-'} condition groups (OR): ${w.conditionGroups}`);
|
|
366
|
+
if (w.statusReason) console.log(` ${w.statusReason}`);
|
|
367
|
+
for (const x of w.warnings ?? []) console.log(` ! ${x}`);
|
|
368
|
+
}
|
|
369
|
+
if (result.existingNotInPlan?.length) console.log(`\n Already on the v20 site, not in this plan: ${result.existingNotInPlan.join(', ')}`);
|
|
370
|
+
for (const n of result.notes ?? []) console.log(` note: ${n}`);
|
|
371
|
+
console.log(`\n ${result.message}\n`);
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { enterpriseReportsCommand } from './commands/enterprise-reports.js';
|
|
5
|
+
import { yearInReviewBody, lastTwelveWholeMonthsKeys } from './year-in-review-range.js';
|
|
5
6
|
|
|
6
7
|
test('describe lists the five novel reports and the promise dismiss action', () => {
|
|
7
8
|
for (const r of ['repeat-contact', 'demand-coverage', 'bot-containment', 'effort-index', 'promise-ledger', 'promise-dismiss']) {
|
|
8
9
|
assert.ok(enterpriseReportsCommand.describe.includes(r), `${r} missing from describe`);
|
|
9
10
|
}
|
|
10
11
|
});
|
|
12
|
+
|
|
13
|
+
// Year in review (item 16): the CLI sends Pacific-midnight instants, defaulting to the last 12 whole Pacific months.
|
|
14
|
+
test('describe lists year-in-review', () => {
|
|
15
|
+
assert.ok(enterpriseReportsCommand.describe.includes('year-in-review'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('year-in-review default range is the last 12 whole Pacific months', () => {
|
|
19
|
+
const now = new Date('2026-10-14T20:00:00Z');
|
|
20
|
+
assert.deepEqual(lastTwelveWholeMonthsKeys(now), { from: '2025-10-01', to: '2026-09-30' });
|
|
21
|
+
assert.deepEqual(yearInReviewBody(undefined, undefined, now), { from: '2025-10-01T07:00:00.000Z', to: '2026-10-01T07:00:00.000Z' });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('year-in-review uses Pacific, not UTC, at a month boundary and honours explicit dates (inclusive to)', () => {
|
|
25
|
+
assert.deepEqual(lastTwelveWholeMonthsKeys(new Date('2026-11-01T03:00:00Z')), { from: '2025-10-01', to: '2026-09-30' });
|
|
26
|
+
assert.deepEqual(yearInReviewBody('2026-01-01', '2026-01-31'), { from: '2026-01-01T08:00:00.000Z', to: '2026-02-01T08:00:00.000Z' });
|
|
27
|
+
assert.throws(() => yearInReviewBody('2026-02-01', '2026-01-01'), /on or before/);
|
|
28
|
+
assert.throws(() => yearInReviewBody('01/01/2026', '2026-01-31'), /yyyy-MM-dd/);
|
|
29
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Year in review (Enterprise Reports item 16) range helpers. The default is the last 12 whole calendar months in Pacific time,
|
|
2
|
+
// sent to the server as a half-open [from, to) pair of ISO instants (Pacific midnight), same rule as the Enterprise Reports tab.
|
|
3
|
+
// mcp/server.js carries the same logic inline (it is a separate package); enterprise-reports.test.js pins this one.
|
|
4
|
+
|
|
5
|
+
const ZONE = 'America/Los_Angeles';
|
|
6
|
+
|
|
7
|
+
function offsetMs(instantMs) {
|
|
8
|
+
const parts = new Intl.DateTimeFormat('en-US', { timeZone: ZONE, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).formatToParts(new Date(instantMs));
|
|
9
|
+
const g = (t) => Number(parts.find((p) => p.type === t).value);
|
|
10
|
+
return Date.UTC(g('year'), g('month') - 1, g('day'), g('hour'), g('minute'), g('second')) - Math.floor(instantMs / 1000) * 1000;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** ISO instant of Pacific midnight at the start of the yyyy-MM-dd calendar day. */
|
|
14
|
+
export function pacificMidnightIso(key) {
|
|
15
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key || '');
|
|
16
|
+
if (!m) throw new Error(`Invalid date "${key}", expected yyyy-MM-dd`);
|
|
17
|
+
const wall = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
18
|
+
let t = wall - offsetMs(wall);
|
|
19
|
+
t = wall - offsetMs(t);
|
|
20
|
+
return new Date(t).toISOString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function addDays(key, n) {
|
|
24
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
|
|
25
|
+
return new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]) + n)).toISOString().slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Last 12 whole Pacific calendar months as inclusive day keys: { from: first day, to: last day }. */
|
|
29
|
+
export function lastTwelveWholeMonthsKeys(now = new Date()) {
|
|
30
|
+
const today = new Intl.DateTimeFormat('en-CA', { timeZone: ZONE, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now); // yyyy-MM-dd
|
|
31
|
+
const y = Number(today.slice(0, 4));
|
|
32
|
+
const mo = today.slice(5, 7);
|
|
33
|
+
const firstOfThisMonth = `${today.slice(0, 4)}-${mo}-01`;
|
|
34
|
+
return { from: `${String(y - 1).padStart(4, '0')}-${mo}-01`, to: addDays(firstOfThisMonth, -1) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The request body: { from, to } instants. `from`/`to` are inclusive yyyy-MM-dd day keys; both default to the last 12 whole months. */
|
|
38
|
+
export function yearInReviewBody(from, to, now = new Date()) {
|
|
39
|
+
const def = lastTwelveWholeMonthsKeys(now);
|
|
40
|
+
const f = from || def.from;
|
|
41
|
+
const t = to || def.to;
|
|
42
|
+
if (f > t) throw new Error('--from must be on or before --to');
|
|
43
|
+
return { from: pacificMidnightIso(f), to: pacificMidnightIso(addDays(t, 1)) };
|
|
44
|
+
}
|
package/package.json
CHANGED