@velaro/cli 1.4.106 → 1.4.108
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/bin/velaro.js +53 -6
- package/lib/commands/agent.js +11 -1
- package/lib/commands/avatar.js +3 -0
- package/lib/commands/billing-agents.js +51 -0
- package/lib/commands/billing-automation.js +1 -1
- package/lib/commands/diag-groups.js +108 -0
- package/lib/commands/endpoint-parity.js +58 -0
- package/lib/commands/entitlement.js +27 -1
- package/lib/commands/feature-discovery.js +3 -0
- package/lib/commands/magento.js +52 -0
- package/lib/commands/ops-groups.js +188 -0
- package/lib/commands/order-form.js +6 -1
- package/lib/commands/package-builder.js +35 -0
- package/lib/commands/parity-staff-tools.js +192 -0
- package/lib/commands/price-increase.js +93 -0
- package/lib/commands/safety.js +78 -2
- package/lib/commands/teams-phone.js +63 -0
- package/lib/log-search.js +43 -0
- package/lib/log-search.test.js +1 -1
- package/lib/parity-util.js +27 -0
- package/lib/sign-versions.test.js +1 -1
- package/package.json +1 -1
package/lib/commands/safety.js
CHANGED
|
@@ -40,9 +40,56 @@ function printSettings(s) {
|
|
|
40
40
|
console.log(`Count alert: ${s.threshold?.count ?? 0} events in ${s.threshold?.windowMinutes ?? 60} minutes`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
|
|
44
|
+
// ── Safety policy (Compliance > Safety Controls) ─────────────────────────────────────────────
|
|
45
|
+
// Wraps SafetyPolicyController. Customer routes take the site from login; Velaro staff pass --site to use the staff routes.
|
|
46
|
+
// Turning a control OFF needs the recorded liability acknowledgement: the CLI prints the exact text and only sends it when you pass --accept.
|
|
47
|
+
const policyPath = (a) => (a.site ? `/SafetyPolicy/staff/${Number(a.site)}` : '/SafetyPolicy');
|
|
48
|
+
const AUDIENCES = ['ai_bot', 'human_agent'];
|
|
49
|
+
|
|
50
|
+
function printPolicy(v) {
|
|
51
|
+
console.log('Safety controls (on by default):');
|
|
52
|
+
for (const c of v.controls ?? []) {
|
|
53
|
+
console.log(` ${c.control}: AI bot ${c.aiBot ? 'on' : 'OFF'}, human agent ${c.humanAgent ? 'on' : 'OFF'}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(`Always on (floor): ${(v.floor ?? []).join(', ')}`);
|
|
56
|
+
if (v.anyOff) console.log(`\nWARNING: ${v.warning}`);
|
|
57
|
+
const feats = Object.keys(v.overrides?.features ?? {});
|
|
58
|
+
if (feats.length) console.log(`Per-feature overrides: ${feats.join(', ')}`);
|
|
59
|
+
console.log(`Acknowledgement version: ${v.acknowledgement?.version}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function applyChange(a, enabled) {
|
|
63
|
+
const audiences = a.audience === 'both' ? AUDIENCES : [a.audience];
|
|
64
|
+
let last;
|
|
65
|
+
if (enabled === false) {
|
|
66
|
+
if (!a.accept) {
|
|
67
|
+
const cur = await messagingGet(policyPath(a));
|
|
68
|
+
console.error(`Turning off a safety control needs the acknowledgement. Read it, then re-run with --accept:\n\n${cur.acknowledgement?.text}\n`);
|
|
69
|
+
throw new Error('Acknowledgement not accepted (pass --accept after reading the text above).');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const ack = enabled === false ? (await messagingGet(policyPath(a))).acknowledgement : null;
|
|
73
|
+
for (const audience of audiences) {
|
|
74
|
+
last = await messagingPut(policyPath(a), {
|
|
75
|
+
control: a.control,
|
|
76
|
+
audience,
|
|
77
|
+
featureKey: a.feature ?? null,
|
|
78
|
+
enabled,
|
|
79
|
+
...(enabled === false ? { acknowledged: true, ackTextVersion: ack.version } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return last;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const siteOpt = { type: 'number', describe: 'Velaro staff only: target site ID (uses the staff route)' };
|
|
86
|
+
const controlOpt = { type: 'string', demandOption: true, describe: 'emergency_self_harm | abuse_ladder | threat_detection | input_jailbreak | output_filter' };
|
|
87
|
+
const audienceOpt = { type: 'string', choices: [...AUDIENCES, 'both'], default: 'both' };
|
|
88
|
+
const featureOpt = { type: 'string', describe: 'Per-feature/channel scope (chat, voice, sms, whatsapp, email). Omit for the whole account.' };
|
|
89
|
+
|
|
43
90
|
export const safetyCommand = {
|
|
44
91
|
command: 'safety',
|
|
45
|
-
describe: 'Safety events and
|
|
92
|
+
describe: 'Safety events, alerts and safety controls: list events, acknowledge, alert settings, policy (on/off controls), test alert, ask Velaro for help',
|
|
46
93
|
builder: (yargs) => yargs
|
|
47
94
|
.command('events', 'List safety events', (y) => y
|
|
48
95
|
.option('from', { type: 'string', describe: 'ISO start' })
|
|
@@ -106,6 +153,35 @@ export const safetyCommand = {
|
|
|
106
153
|
await messagingPost('/SafetyEvents/need-help', { message: a.message });
|
|
107
154
|
console.log('Sent. The Velaro team has been alerted.');
|
|
108
155
|
}))
|
|
109
|
-
.
|
|
156
|
+
.command('policy <action>', 'Safety controls on/off policy (get | set)', (y) => y
|
|
157
|
+
.positional('action', { choices: ['get', 'set'] })
|
|
158
|
+
.option('site', siteOpt)
|
|
159
|
+
.option('control', { ...controlOpt, demandOption: false })
|
|
160
|
+
.option('audience', audienceOpt)
|
|
161
|
+
.option('feature', featureOpt)
|
|
162
|
+
.option('enabled', { type: 'boolean', describe: 'true = on, false = off (off needs --accept)' })
|
|
163
|
+
.option('reset', { type: 'boolean', describe: 'Clear the override and use the default (on)' })
|
|
164
|
+
.option('accept', { type: 'boolean', describe: 'Accept the liability acknowledgement (required to turn a control off)' })
|
|
165
|
+
.option('json', { type: 'boolean' }),
|
|
166
|
+
runCommand(async (a) => {
|
|
167
|
+
if (a.action === 'get') return out(a, await messagingGet(policyPath(a)), printPolicy);
|
|
168
|
+
if (!a.control) throw new Error('--control is required for set.');
|
|
169
|
+
if (a.reset && a.enabled !== undefined) throw new Error('Use either --enabled or --reset, not both.');
|
|
170
|
+
if (!a.reset && a.enabled === undefined) throw new Error('Pass --enabled true|false or --reset.');
|
|
171
|
+
const v = await applyChange(a, a.reset ? null : a.enabled);
|
|
172
|
+
out(a, v, (x) => { console.log('Saved.\n'); printPolicy(x); });
|
|
173
|
+
}))
|
|
174
|
+
.command('acknowledge-off', 'Turn a safety control OFF after accepting the liability acknowledgement', (y) => y
|
|
175
|
+
.option('site', siteOpt)
|
|
176
|
+
.option('control', controlOpt)
|
|
177
|
+
.option('audience', audienceOpt)
|
|
178
|
+
.option('feature', featureOpt)
|
|
179
|
+
.option('accept', { type: 'boolean', describe: 'I have read the acknowledgement text and accept it' })
|
|
180
|
+
.option('json', { type: 'boolean' }),
|
|
181
|
+
runCommand(async (a) => {
|
|
182
|
+
const v = await applyChange(a, false);
|
|
183
|
+
out(a, v, (x) => { console.log(`${a.control} turned off. The acceptance was recorded.\n`); printPolicy(x); });
|
|
184
|
+
}))
|
|
185
|
+
.demandCommand(1, 'Specify a subcommand: events, summary, ack, settings, policy, acknowledge-off, test, need-help'),
|
|
110
186
|
handler: () => {},
|
|
111
187
|
};
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* velaro teams-phone set --tenant-id ... Create or update the configuration
|
|
6
6
|
* velaro teams-phone remove Delete the configuration
|
|
7
7
|
* velaro teams-phone test Run the inbound-calling readiness checks
|
|
8
|
+
* velaro teams-phone agents list List agents and their Teams user object IDs
|
|
9
|
+
* velaro teams-phone agents set <agentId> <teamsUserId> Map an agent to an Entra user Object ID
|
|
10
|
+
* velaro teams-phone agents delete <agentId> Clear an agent's mapping
|
|
8
11
|
*
|
|
9
12
|
* Backend contract: server/Velaro.Messaging/Controllers/Voice/TeamsPhoneConfigController.cs
|
|
10
13
|
* (velaro-messaging). Site is resolved server-side from the auth context — never
|
|
@@ -98,6 +101,39 @@ async function testHandler() {
|
|
|
98
101
|
if (!result?.success) process.exitCode = 1;
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
// -- agents (agent -> Entra user object id) ---------------------------------------
|
|
105
|
+
|
|
106
|
+
async function agentsListHandler() {
|
|
107
|
+
const agents = await messagingGet('/TeamsPhone/Agents');
|
|
108
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
109
|
+
console.log(`
|
|
110
|
+
${DIM}No agents found.${RST}
|
|
111
|
+
`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(`
|
|
115
|
+
${BOLD}Teams Phone agent mapping${RST}`);
|
|
116
|
+
for (const a of agents) {
|
|
117
|
+
const name = `${a.firstName ?? ''} ${a.lastName ?? ''}`.trim();
|
|
118
|
+
console.log(` ${String(a.id).padEnd(8)} ${name.padEnd(28)} ${String(a.email ?? '').padEnd(34)} ${a.teamsUserId ? GRN + a.teamsUserId + RST : DIM + '(unmapped)' + RST}`);
|
|
119
|
+
}
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function agentsSetHandler(argv) {
|
|
124
|
+
const r = await messagingPut(`/TeamsPhone/Agents/${encodeURIComponent(argv.agentId)}`, { teamsUserId: argv.teamsUserId });
|
|
125
|
+
console.log(`
|
|
126
|
+
${GRN}✓${RST} Agent ${argv.agentId} mapped to Teams user ${r?.teamsUserId ?? argv.teamsUserId}.
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function agentsDeleteHandler(argv) {
|
|
131
|
+
await messagingDel(`/TeamsPhone/Agents/${encodeURIComponent(argv.agentId)}`);
|
|
132
|
+
console.log(`
|
|
133
|
+
${GRN}✓${RST} Agent ${argv.agentId} mapping cleared.
|
|
134
|
+
`);
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
// -- command export ------------------------------------------------------------------
|
|
102
138
|
|
|
103
139
|
export const teamsPhoneCommand = {
|
|
@@ -137,6 +173,33 @@ export const teamsPhoneCommand = {
|
|
|
137
173
|
builder: yy => yy,
|
|
138
174
|
handler: testHandler,
|
|
139
175
|
})
|
|
176
|
+
.command({
|
|
177
|
+
command: 'agents <action>',
|
|
178
|
+
describe: 'Agent mapping: which Entra user Object ID is which Velaro agent (list | set | delete)',
|
|
179
|
+
builder: yy => yy
|
|
180
|
+
.command({
|
|
181
|
+
command: 'list',
|
|
182
|
+
describe: 'List agents with their Teams user Object ID',
|
|
183
|
+
builder: z => z,
|
|
184
|
+
handler: agentsListHandler,
|
|
185
|
+
})
|
|
186
|
+
.command({
|
|
187
|
+
command: 'set <agentId> <teamsUserId>',
|
|
188
|
+
describe: 'Map a Velaro agent (workspace user id) to a Microsoft Entra user Object ID (GUID)',
|
|
189
|
+
builder: z => z
|
|
190
|
+
.positional('agentId', { type: 'number', describe: 'Velaro agent (workspace user) id' })
|
|
191
|
+
.positional('teamsUserId', { type: 'string', describe: 'Entra user Object ID (GUID); "" clears' }),
|
|
192
|
+
handler: agentsSetHandler,
|
|
193
|
+
})
|
|
194
|
+
.command({
|
|
195
|
+
command: 'delete <agentId>',
|
|
196
|
+
describe: 'Clear the Teams user mapping for a Velaro agent',
|
|
197
|
+
builder: z => z.positional('agentId', { type: 'number', describe: 'Velaro agent (workspace user) id' }),
|
|
198
|
+
handler: agentsDeleteHandler,
|
|
199
|
+
})
|
|
200
|
+
.demandCommand(1, 'Specify list, set or delete.'),
|
|
201
|
+
handler: () => {},
|
|
202
|
+
})
|
|
140
203
|
.demandCommand(1, 'Specify a teams-phone subcommand.')
|
|
141
204
|
.strict(),
|
|
142
205
|
handler: () => {},
|
package/lib/log-search.js
CHANGED
|
@@ -207,3 +207,46 @@ export function describeRawMeta(resp) {
|
|
|
207
207
|
const bestEffort = resp.exact === false ? ' exact=false (answered from the best-effort search index: it can lag and misses text that starts mid-word, so treat counts as a floor, not exact)' : '';
|
|
208
208
|
return `searchPath=${resp.searchPath} scannedRows=${resp.scannedRows} truncated=${t}${t ? ' (NOT proof of absence; continue with nextCursor or narrow the window)' : ''}${bestEffort}${resp.nextCursor ? ' nextCursor=present' : ''}`;
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
// ── Time window + raw query building (shared by server.js and staff-server.js velaro_logs_search) ──
|
|
212
|
+
// Bug fixed 2026-10-21: the tool only honored `last` and silently ignored from/to, so an explicit
|
|
213
|
+
// historical range returned the newest entries instead. from/to accept an ISO timestamp or a
|
|
214
|
+
// relative duration (30m, 2h, 1d = that long ago). `to` defaults to now, `from` to to - last (2h).
|
|
215
|
+
const DUR_RE = /^(\d+)(m|h|d)$/i;
|
|
216
|
+
const durMs = (s) => {
|
|
217
|
+
const m = DUR_RE.exec(String(s).trim());
|
|
218
|
+
const n = parseInt(m[1], 10);
|
|
219
|
+
const u = m[2].toLowerCase();
|
|
220
|
+
return u === 'm' ? n * 60000 : u === 'h' ? n * 3600000 : n * 86400000;
|
|
221
|
+
};
|
|
222
|
+
function parseWhen(label, v, nowMs) {
|
|
223
|
+
const s = String(v).trim();
|
|
224
|
+
if (DUR_RE.test(s)) return new Date(nowMs - durMs(s));
|
|
225
|
+
const d = new Date(s);
|
|
226
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid ${label} "${v}": use an ISO timestamp (2026-10-20T00:00:00Z) or a relative duration like 2h.`);
|
|
227
|
+
return d;
|
|
228
|
+
}
|
|
229
|
+
export function resolveLogWindow(args = {}, nowMs = Date.now()) {
|
|
230
|
+
const given = (v) => v !== undefined && v !== null && String(v).trim() !== '';
|
|
231
|
+
const to = given(args.to) ? parseWhen('to', args.to, nowMs) : new Date(nowMs);
|
|
232
|
+
const lastMs = DUR_RE.test(String(args.last ?? '').trim()) ? durMs(args.last) : 2 * 3600000;
|
|
233
|
+
const from = given(args.from) ? parseWhen('from', args.from, nowMs) : new Date(to.getTime() - lastMs);
|
|
234
|
+
if (from.getTime() >= to.getTime()) throw new Error(`Invalid window: from (${from.toISOString()}) must be before to (${to.toISOString()}).`);
|
|
235
|
+
return { from, to };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Query string for GET /superadmin/logs/search (same params as /internal/logs/search).
|
|
239
|
+
export function buildRawLogQuery(args, from, to) {
|
|
240
|
+
const params = new URLSearchParams({ from: from.toISOString(), to: to.toISOString(), take: String(args.take ?? 50) });
|
|
241
|
+
if (args.q) params.set('q', args.q);
|
|
242
|
+
if (args.level) params.set('level', String(args.level).toUpperCase());
|
|
243
|
+
if (args.logger) params.set('logger', args.logger);
|
|
244
|
+
if (args.integration) params.set('integration', args.integration);
|
|
245
|
+
if (args.siteId) params.set('siteId', String(args.siteId));
|
|
246
|
+
if (args.source) params.set('source', args.source);
|
|
247
|
+
if (args.platform) params.set('platform', args.platform);
|
|
248
|
+
if (args.environment) params.set('environment', args.environment);
|
|
249
|
+
if (args.correlationId) params.set('correlationId', args.correlationId);
|
|
250
|
+
if (args.regex) params.set('regex', 'true');
|
|
251
|
+
return params.toString();
|
|
252
|
+
}
|
package/lib/log-search.test.js
CHANGED
|
@@ -321,7 +321,7 @@ test('fetchRetry429 retries only 429 and returns the final response after the re
|
|
|
321
321
|
});
|
|
322
322
|
|
|
323
323
|
test('bot_full_diagnostic: errors use the server-side level=ERROR filter, patterns are a separate read; neither is the old text regex', () => {
|
|
324
|
-
for (const rel of ['../../mcp/staff-server.js'
|
|
324
|
+
for (const rel of ['../../mcp/staff-server.js']) { // bot_full_diagnostic is staff-only since MCP 2.0.0
|
|
325
325
|
const src = fs.readFileSync(path.join(here, rel), 'utf8');
|
|
326
326
|
const a = src.indexOf('const velaroLoggerP = safeJson');
|
|
327
327
|
const body = src.slice(a, a + 2600);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Shared helpers for the MCP-parity command groups (diag-groups.js, ops-groups.js).
|
|
2
|
+
export const show = (r) => console.log(typeof r === 'string' ? r : JSON.stringify(r, null, 2));
|
|
3
|
+
export const qs = (o) => {
|
|
4
|
+
const p = new URLSearchParams();
|
|
5
|
+
for (const [k, v] of Object.entries(o)) if (v !== undefined && v !== null && v !== '') p.set(k, String(v));
|
|
6
|
+
const s = p.toString();
|
|
7
|
+
return s ? `?${s}` : '';
|
|
8
|
+
};
|
|
9
|
+
export const run = (fn) => async (argv) => {
|
|
10
|
+
try { await fn(argv); } catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
|
|
11
|
+
};
|
|
12
|
+
export const needYes = (argv, what) => {
|
|
13
|
+
if (!argv.yes) { console.error(`Refusing to ${what} without --yes.`); process.exit(1); }
|
|
14
|
+
};
|
|
15
|
+
// Build a yargs group body (spread into `{ command: 'name', ...group(...) }` so the parity validator sees the command name): subs = [[command, describe, builderFn|null, handler]]
|
|
16
|
+
export function group(describe, subs) {
|
|
17
|
+
return {
|
|
18
|
+
describe,
|
|
19
|
+
builder: (y) => {
|
|
20
|
+
for (const [command, desc, b, handler] of subs) {
|
|
21
|
+
y.command({ command, describe: desc, builder: b || ((x) => x), handler: run(handler) });
|
|
22
|
+
}
|
|
23
|
+
return y.demandCommand(1, `Specify a subcommand: ${subs.map((s) => s[0].split(' ')[0]).join(' | ')}`);
|
|
24
|
+
},
|
|
25
|
+
handler: () => {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -42,7 +42,7 @@ import { readFileSync } from 'node:fs';
|
|
|
42
42
|
import { fileURLToPath } from 'node:url';
|
|
43
43
|
import { dirname, join as pjoin } from 'node:path';
|
|
44
44
|
const root = pjoin(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
45
|
-
for (const f of ['mcp/
|
|
45
|
+
for (const f of ['mcp/staff-server.js', 'server/Velaro.Admin/Controllers/StaffMcpController.cs']) {
|
|
46
46
|
test(`${f} registers sign_versions tools`, () => {
|
|
47
47
|
const s = readFileSync(pjoin(root, f), 'utf8');
|
|
48
48
|
for (const t of ['sign_versions_list', 'sign_versions_diff', 'sign_versions_upload']) assert.ok(s.includes(t), `${t} missing in ${f}`);
|
package/package.json
CHANGED