@velaro/cli 1.4.69 → 1.4.70
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 +2 -0
- package/lib/commands/agent.js +43 -1
- package/lib/commands/ai-models.js +45 -1
- package/lib/commands/article.js +47 -1
- package/lib/commands/booking.js +63 -10
- package/lib/commands/contact-list.js +46 -0
- package/lib/commands/klaviyo-migration.js +84 -0
- package/lib/commands/report.js +52 -0
- package/package.json +1 -1
package/bin/velaro.js
CHANGED
|
@@ -51,6 +51,7 @@ import { notionCommand } from '../lib/commands/notion.js';
|
|
|
51
51
|
import { callrailCommand } from '../lib/commands/callrail.js';
|
|
52
52
|
import { emailCampaignCommand } from '../lib/commands/email-campaign.js';
|
|
53
53
|
import { outboundActivityCommand } from '../lib/commands/outbound-activity.js';
|
|
54
|
+
import { klaviyoMigrationCommand } from '../lib/commands/klaviyo-migration.js';
|
|
54
55
|
import { telephonyCommand } from '../lib/commands/telephony.js';
|
|
55
56
|
import { teamsPhoneCommand } from '../lib/commands/teams-phone.js';
|
|
56
57
|
import { teamsCommand } from '../lib/commands/teams.js';
|
|
@@ -152,6 +153,7 @@ await yargs(hideBin(process.argv))
|
|
|
152
153
|
.command(callrailCommand)
|
|
153
154
|
.command(emailCampaignCommand)
|
|
154
155
|
.command(outboundActivityCommand)
|
|
156
|
+
.command(klaviyoMigrationCommand)
|
|
155
157
|
.command(telephonyCommand)
|
|
156
158
|
.command(teamsPhoneCommand)
|
|
157
159
|
.command(teamsCommand)
|
package/lib/commands/agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { get, post } from '../api.js';
|
|
1
|
+
import { get, post, messagingPost, getCredentials } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
3
|
|
|
4
4
|
const VALID_STATUSES = ['Available', 'Away', 'Offline'];
|
|
@@ -55,6 +55,48 @@ export const agentCommand = {
|
|
|
55
55
|
console.log(`\n${rows.length} agent(s)`);
|
|
56
56
|
}),
|
|
57
57
|
})
|
|
58
|
+
.command({
|
|
59
|
+
command: 'create',
|
|
60
|
+
describe: 'Provision a new licensed agent/admin on an EXISTING site (Velaro staff only) -- ' +
|
|
61
|
+
'closes the gap where site-provision only creates brand-new sites and Users/List ' +
|
|
62
|
+
'requires an existing site admin, which a zero-agent site can never have',
|
|
63
|
+
builder: (y) => y
|
|
64
|
+
.option('site-id', { type: 'number', demandOption: true, describe: 'Target site ID (must already exist)' })
|
|
65
|
+
.option('email', { type: 'string', demandOption: true, describe: 'New agent email' })
|
|
66
|
+
.option('first-name', { type: 'string', describe: 'First name' })
|
|
67
|
+
.option('last-name', { type: 'string', describe: 'Last name' })
|
|
68
|
+
.option('roles', { type: 'array', describe: 'Roles to grant, e.g. --roles Administrator (default: Administrator). "Velaro Admin"/"Velaro Support" are always stripped server-side.' })
|
|
69
|
+
.option('team-ids', { type: 'array', describe: 'Team IDs on the target site (default: the site\'s first team)' })
|
|
70
|
+
.option('max-chats', { type: 'number', describe: 'Concurrent chat capacity (default: site ceiling, usually 4)' }),
|
|
71
|
+
handler: runCommand(async (argv) => {
|
|
72
|
+
const creds = await getCredentials();
|
|
73
|
+
if (creds.siteId !== 1032) {
|
|
74
|
+
console.error('This command is only available to Velaro staff.');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const body = {
|
|
79
|
+
email: argv.email,
|
|
80
|
+
firstName: argv['first-name'],
|
|
81
|
+
lastName: argv['last-name'],
|
|
82
|
+
roles: argv.roles,
|
|
83
|
+
teamIds: argv['team-ids']?.map(Number),
|
|
84
|
+
maxChats: argv['max-chats'] ?? 0,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
console.log(`\nCreating agent ${argv.email} on site ${argv['site-id']}...`);
|
|
88
|
+
// Non-2xx (existing email, seat limit reached, site not found) throws via
|
|
89
|
+
// messagingRequest and is reported by runCommand's own catch -- no separate
|
|
90
|
+
// success/error branch needed here, unlike site-provision's payload-level flag.
|
|
91
|
+
const result = await messagingPost(`/SupportTools/sites/${argv['site-id']}/agents`, body);
|
|
92
|
+
|
|
93
|
+
console.log(`\x1b[32mOK: agent created on site ${argv['site-id']}\x1b[0m`);
|
|
94
|
+
console.log(` Email: ${result.user?.email ?? argv.email}`);
|
|
95
|
+
console.log(` Roles: ${(result.user?.roles ?? []).join(', ')}`);
|
|
96
|
+
if (result.temporaryPassword) console.log(` Temp password: ${result.temporaryPassword}`);
|
|
97
|
+
console.log(` ${result.message ?? ''}`);
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
58
100
|
.command({
|
|
59
101
|
command: 'set-status',
|
|
60
102
|
describe: 'Set an agent\'s availability status (admin only)',
|
|
@@ -114,6 +114,29 @@ async function clearTierDefault(argv) {
|
|
|
114
114
|
console.log(`\n${GREEN}OK${RESET} Cleared tier '${argv.tier}' role '${argv.role}'. Reverts to global default.\n`);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// -- per-site thinking + version pin -------------------------------------------
|
|
118
|
+
|
|
119
|
+
async function setSiteThinking(argv) {
|
|
120
|
+
const data = await callResolver(argv, `site/${argv.site}/thinking`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: { effort: argv.effort, ...(argv.role ? { role: argv.role } : {}) },
|
|
123
|
+
});
|
|
124
|
+
console.log(`
|
|
125
|
+
${GREEN}OK${RESET} Site ${data.siteId}: ${data.key} = ${data.effort}`);
|
|
126
|
+
console.log(`${DIM}Thinking bills as output tokens; only active if the site holds EnableAiThinking.${RESET}
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function setSitePin(argv) {
|
|
131
|
+
const data = await callResolver(argv, `site/${argv.site}`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
body: { role: argv.role, model: argv.model, pinVersion: argv.pin === true, updatedBy: 'velaro-cli' },
|
|
134
|
+
});
|
|
135
|
+
console.log(`
|
|
136
|
+
${GREEN}OK${RESET} Site ${data.siteId} role '${data.role}' -> ${data.model} (pinVersion=${data.pinVersion})
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
// -- command export -------------------------------------------------------------
|
|
118
141
|
|
|
119
142
|
export const aiModelsCommand = {
|
|
@@ -155,5 +178,26 @@ export const aiModelsCommand = {
|
|
|
155
178
|
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
156
179
|
handler: clearTierDefault,
|
|
157
180
|
})
|
|
158
|
-
.
|
|
181
|
+
.command({
|
|
182
|
+
command: 'set-thinking',
|
|
183
|
+
describe: 'Set the extended-thinking level for a site (off default; needs EnableAiThinking entitlement)',
|
|
184
|
+
builder: y => y
|
|
185
|
+
.option('site', { describe: 'Site ID', type: 'number', demandOption: true })
|
|
186
|
+
.option('effort', { describe: 'off, low, medium, or high', type: 'string', choices: ['off', 'low', 'medium', 'high'], demandOption: true })
|
|
187
|
+
.option('role', { describe: 'Scope to one role (omit for site-wide)', type: 'string' })
|
|
188
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
189
|
+
handler: setSiteThinking,
|
|
190
|
+
})
|
|
191
|
+
.command({
|
|
192
|
+
command: 'set-site-model',
|
|
193
|
+
describe: 'Set a site role override, optionally pinned to its exact version (--pin), e.g. velaro ai-models set-site-model --site 1032 --role chatbot --model claude-opus-5 --pin',
|
|
194
|
+
builder: y => y
|
|
195
|
+
.option('site', { describe: 'Site ID', type: 'number', demandOption: true })
|
|
196
|
+
.option('role', { describe: 'e.g. chatbot, ivr, skill_synthesis', type: 'string', demandOption: true })
|
|
197
|
+
.option('model', { describe: 'Model alias/ID', type: 'string', demandOption: true })
|
|
198
|
+
.option('pin', { describe: 'Keep this exact model, do not auto-upgrade', type: 'boolean', default: false })
|
|
199
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
200
|
+
handler: setSitePin,
|
|
201
|
+
})
|
|
202
|
+
.demandCommand(1, 'Specify a subcommand: resolver, effective, set-tier, clear-tier, set-thinking, or set-site-model'),
|
|
159
203
|
};
|
package/lib/commands/article.js
CHANGED
|
@@ -198,7 +198,7 @@ const pushCommand = {
|
|
|
198
198
|
|
|
199
199
|
const deleteCommand = {
|
|
200
200
|
command: 'delete <id>',
|
|
201
|
-
describe: 'Delete a KB article',
|
|
201
|
+
describe: 'Delete a KB article (restorable for 30 days)',
|
|
202
202
|
builder: (y) => y.positional('id', { type: 'number', describe: 'Article ID' }),
|
|
203
203
|
handler: runCommand(async (argv) => {
|
|
204
204
|
await adminRequest('DELETE', `/api/kb/articles?id=${argv.id}`);
|
|
@@ -206,6 +206,48 @@ const deleteCommand = {
|
|
|
206
206
|
}),
|
|
207
207
|
};
|
|
208
208
|
|
|
209
|
+
// ── recycle bin (soft delete) ────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
const deletedCommand = {
|
|
212
|
+
command: 'deleted',
|
|
213
|
+
describe: 'List recently deleted KB articles (restorable for 30 days)',
|
|
214
|
+
handler: runCommand(async () => {
|
|
215
|
+
const rows = await adminRequest('GET', '/api/kb/articles/deleted');
|
|
216
|
+
(rows || []).forEach(r => console.log(`#${r.id} ${r.name} (purges in ${r.daysUntilPurge} day(s))`));
|
|
217
|
+
if (!rows || !rows.length) console.log('Nothing in Recently deleted.');
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const restoreCommand = {
|
|
222
|
+
command: 'restore <id>',
|
|
223
|
+
describe: 'Restore a deleted KB article (re-indexes it)',
|
|
224
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Article ID' }),
|
|
225
|
+
handler: runCommand(async (argv) => {
|
|
226
|
+
await adminRequest('POST', `/api/kb/articles/${argv.id}/restore`);
|
|
227
|
+
console.log(`Article ${argv.id} restored.`);
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const deletedTopicsCommand = {
|
|
232
|
+
command: 'deleted-topics',
|
|
233
|
+
describe: 'List recently deleted KB topics',
|
|
234
|
+
handler: runCommand(async () => {
|
|
235
|
+
const rows = await adminRequest('GET', '/api/kb/topics/deleted');
|
|
236
|
+
(rows || []).forEach(r => console.log(`#${r.id} ${r.name} (purges in ${r.daysUntilPurge} day(s))`));
|
|
237
|
+
if (!rows || !rows.length) console.log('Nothing in Recently deleted.');
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const restoreTopicCommand = {
|
|
242
|
+
command: 'restore-topic <id>',
|
|
243
|
+
describe: 'Restore a deleted KB topic',
|
|
244
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Topic ID' }),
|
|
245
|
+
handler: runCommand(async (argv) => {
|
|
246
|
+
await adminRequest('POST', `/api/kb/topics/${argv.id}/restore`);
|
|
247
|
+
console.log(`Topic ${argv.id} restored.`);
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
250
|
+
|
|
209
251
|
// ── seed-views ────────────────────────────────────────────────────────────────
|
|
210
252
|
|
|
211
253
|
const seedViewsCommand = {
|
|
@@ -461,6 +503,10 @@ export const articleCommand = {
|
|
|
461
503
|
.command(pushCommand)
|
|
462
504
|
.command(previewCommand)
|
|
463
505
|
.command(deleteCommand)
|
|
506
|
+
.command(deletedCommand)
|
|
507
|
+
.command(restoreCommand)
|
|
508
|
+
.command(deletedTopicsCommand)
|
|
509
|
+
.command(restoreTopicCommand)
|
|
464
510
|
.command(seedViewsCommand)
|
|
465
511
|
.command(improveCommand)
|
|
466
512
|
.command(screenshotCommand)
|
package/lib/commands/booking.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { messagingGet, messagingPost, messagingPut, messagingDel } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
// CLI surface for the generic booking engine (Phase 1b
|
|
6
|
-
//
|
|
4
|
+
// -- velaro booking --------------------------------------------------------------
|
|
5
|
+
// CLI surface for the generic booking engine (Phase 1b config CRUD + Phase 1c
|
|
6
|
+
// booking write path) -- see velaro-admin memory
|
|
7
|
+
// project_2026_09_06_generic_booking_engine_schema_plan.md. Backend:
|
|
7
8
|
// server/Velaro.Messaging/Controllers/{BookableResourcesController,
|
|
8
9
|
// BookableServicesController,ResourceAvailabilityRulesController}.cs, routed
|
|
9
|
-
// at /BookableResources, /BookableServices, /ResourceAvailabilityRules
|
|
10
|
+
// at /BookableResources, /BookableServices, /ResourceAvailabilityRules
|
|
11
|
+
// (Phase 1b, config); server/Velaro.Messaging/Controllers/Bookings/BookingsController.cs,
|
|
12
|
+
// routed at /Bookings (Phase 1c, staff-facing list/get/cancel of actual bookings).
|
|
10
13
|
// Mirrors the booking_* MCP tools. SiteId always derives from the caller's
|
|
11
14
|
// JWT server-side -- never accepted as a CLI argument.
|
|
12
15
|
|
|
@@ -17,9 +20,9 @@ function fmtTime(t) { return t == null ? '-' : String(t); }
|
|
|
17
20
|
|
|
18
21
|
export const bookingCommand = {
|
|
19
22
|
command: 'booking <subcommand>',
|
|
20
|
-
describe: 'Generic booking engine -- bookable resources, services,
|
|
23
|
+
describe: 'Generic booking engine -- bookable resources, services, availability rules, and bookings',
|
|
21
24
|
builder: yargs => yargs
|
|
22
|
-
//
|
|
25
|
+
// -- resource --------------------------------------------------------------
|
|
23
26
|
.command({
|
|
24
27
|
command: 'resource <action>',
|
|
25
28
|
describe: 'Manage bookable resources (staff / room / equipment / virtual)',
|
|
@@ -93,7 +96,7 @@ export const bookingCommand = {
|
|
|
93
96
|
})
|
|
94
97
|
.demandCommand(1, 'Specify a resource action: list | get | create | update | delete'),
|
|
95
98
|
})
|
|
96
|
-
//
|
|
99
|
+
// -- service -----------------------------------------------------------------
|
|
97
100
|
.command({
|
|
98
101
|
command: 'service <action>',
|
|
99
102
|
describe: 'Manage bookable services (the "thing" a customer books)',
|
|
@@ -179,7 +182,7 @@ export const bookingCommand = {
|
|
|
179
182
|
})
|
|
180
183
|
.demandCommand(1, 'Specify a service action: list | get | create | update | delete'),
|
|
181
184
|
})
|
|
182
|
-
//
|
|
185
|
+
// -- availability --------------------------------------------------------------
|
|
183
186
|
.command({
|
|
184
187
|
command: 'availability <action>',
|
|
185
188
|
describe: 'Manage weekly-recurring resource availability rules',
|
|
@@ -275,5 +278,55 @@ export const bookingCommand = {
|
|
|
275
278
|
})
|
|
276
279
|
.demandCommand(1, 'Specify an availability action: list | get | create | update | delete'),
|
|
277
280
|
})
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
// -- appointment (Phase 1c: real bookings, staff-facing) ------------------------
|
|
282
|
+
.command({
|
|
283
|
+
command: 'appointment <action>',
|
|
284
|
+
describe: 'View and cancel actual bookings made against the booking engine (staff-facing)',
|
|
285
|
+
builder: y => y
|
|
286
|
+
.command({
|
|
287
|
+
command: 'list',
|
|
288
|
+
describe: 'List bookings for this site, optionally filtered by date range or service',
|
|
289
|
+
builder: y2 => y2
|
|
290
|
+
.option('from', { type: 'string', describe: 'Only bookings starting at or after this ISO 8601 timestamp' })
|
|
291
|
+
.option('to', { type: 'string', describe: 'Only bookings starting at or before this ISO 8601 timestamp' })
|
|
292
|
+
.option('service-id', { type: 'number', describe: 'Only bookings for this bookable service id' }),
|
|
293
|
+
handler: runCommand(async argv => {
|
|
294
|
+
const params = new URLSearchParams();
|
|
295
|
+
if (argv.from != null) params.set('fromUtc', argv.from);
|
|
296
|
+
if (argv.to != null) params.set('toUtc', argv.to);
|
|
297
|
+
if (argv.serviceId != null) params.set('bookableServiceId', String(argv.serviceId));
|
|
298
|
+
const qs = params.toString();
|
|
299
|
+
const bookings = await messagingGet(`/Bookings${qs ? `?${qs}` : ''}`);
|
|
300
|
+
if (!Array.isArray(bookings) || !bookings.length) {
|
|
301
|
+
console.log('\n No bookings found.\n');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
console.log('\nBookings\n');
|
|
305
|
+
for (const b of bookings) {
|
|
306
|
+
console.log(` id:${b.id} service:${b.bookableServiceId} resource:${b.resourceId ?? '-'} ${fmtDate(b.startUtc)}-${fmtDate(b.endUtc)} party:${b.partySize} status:${b.status} customer:${b.customerName} (${b.customerPhone})`);
|
|
307
|
+
}
|
|
308
|
+
console.log('');
|
|
309
|
+
}),
|
|
310
|
+
})
|
|
311
|
+
.command({
|
|
312
|
+
command: 'get <id>',
|
|
313
|
+
describe: 'Get a single booking by id',
|
|
314
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Booking id' }),
|
|
315
|
+
handler: runCommand(async argv => {
|
|
316
|
+
const booking = await messagingGet(`/Bookings/${argv.id}`);
|
|
317
|
+
console.log(JSON.stringify(booking, null, 2));
|
|
318
|
+
}),
|
|
319
|
+
})
|
|
320
|
+
.command({
|
|
321
|
+
command: 'cancel <id>',
|
|
322
|
+
describe: 'Cancel a booking (staff-initiated)',
|
|
323
|
+
builder: y2 => y2.positional('id', { type: 'number', describe: 'Booking id' }),
|
|
324
|
+
handler: runCommand(async argv => {
|
|
325
|
+
const result = await messagingPost(`/Bookings/${argv.id}/cancel`, {});
|
|
326
|
+
console.log(`\n✓ Cancelled booking id:${result.bookingId} (status: ${result.status})\n`);
|
|
327
|
+
}),
|
|
328
|
+
})
|
|
329
|
+
.demandCommand(1, 'Specify an appointment action: list | get | cancel'),
|
|
330
|
+
})
|
|
331
|
+
.demandCommand(1, 'Specify a subcommand: resource | service | availability | appointment'),
|
|
332
|
+
};
|
|
@@ -277,6 +277,52 @@ export const contactListCommand = {
|
|
|
277
277
|
describe: 'Marketing contact lists: CRUD, members, export, resync, quota, segments, CRM import',
|
|
278
278
|
builder: (y) => y
|
|
279
279
|
.command({ command: 'list', describe: 'List all contact lists for this site', handler: runCommand(listsHandler) })
|
|
280
|
+
.command({
|
|
281
|
+
command: 'deleted',
|
|
282
|
+
describe: 'List recently deleted contact lists (restorable for 30 days)',
|
|
283
|
+
handler: runCommand(async () => {
|
|
284
|
+
const rows = await request('GET', 'ContactLists/deleted');
|
|
285
|
+
(rows || []).forEach(r => console.log(`#${r.id} ${r.name} (purges in ${r.daysUntilPurge} day(s))`));
|
|
286
|
+
if (!rows || !rows.length) console.log('Nothing in Recently deleted.');
|
|
287
|
+
}),
|
|
288
|
+
})
|
|
289
|
+
.command({
|
|
290
|
+
command: 'restore <id>',
|
|
291
|
+
describe: 'Restore a deleted contact list',
|
|
292
|
+
builder: (y2) => y2.positional('id', { type: 'number' }),
|
|
293
|
+
handler: runCommand(async (argv) => { await request('POST', `ContactLists/${argv.id}/restore`); console.log(`List #${argv.id} restored.`); }),
|
|
294
|
+
})
|
|
295
|
+
.command({
|
|
296
|
+
command: 'deleted-members <listId>',
|
|
297
|
+
describe: 'List members recently removed from a list',
|
|
298
|
+
builder: (y2) => y2.positional('listId', { type: 'number' }),
|
|
299
|
+
handler: runCommand(async (argv) => {
|
|
300
|
+
const rows = await request('GET', `ContactLists/${argv.listId}/Members/deleted`);
|
|
301
|
+
(rows || []).forEach(r => console.log(`#${r.id} ${r.name} (purges in ${r.daysUntilPurge} day(s))`));
|
|
302
|
+
if (!rows || !rows.length) console.log('Nothing in Recently deleted.');
|
|
303
|
+
}),
|
|
304
|
+
})
|
|
305
|
+
.command({
|
|
306
|
+
command: 'restore-member <listId> <memberId>',
|
|
307
|
+
describe: 'Restore a member removed from a list',
|
|
308
|
+
builder: (y2) => y2.positional('listId', { type: 'number' }).positional('memberId', { type: 'number' }),
|
|
309
|
+
handler: runCommand(async (argv) => { await request('POST', `ContactLists/${argv.listId}/Members/${argv.memberId}/restore`); console.log(`Member #${argv.memberId} restored.`); }),
|
|
310
|
+
})
|
|
311
|
+
.command({
|
|
312
|
+
command: 'deleted-segments',
|
|
313
|
+
describe: 'List recently deleted contact segments',
|
|
314
|
+
handler: runCommand(async () => {
|
|
315
|
+
const rows = await request('GET', 'ContactSegments/deleted');
|
|
316
|
+
(rows || []).forEach(r => console.log(`#${r.id} ${r.name} (purges in ${r.daysUntilPurge} day(s))`));
|
|
317
|
+
if (!rows || !rows.length) console.log('Nothing in Recently deleted.');
|
|
318
|
+
}),
|
|
319
|
+
})
|
|
320
|
+
.command({
|
|
321
|
+
command: 'restore-segment <id>',
|
|
322
|
+
describe: 'Restore a deleted contact segment',
|
|
323
|
+
builder: (y2) => y2.positional('id', { type: 'number' }),
|
|
324
|
+
handler: runCommand(async (argv) => { await request('POST', `ContactSegments/${argv.id}/restore`); console.log(`Segment #${argv.id} restored.`); }),
|
|
325
|
+
})
|
|
280
326
|
.command({
|
|
281
327
|
command: 'create',
|
|
282
328
|
describe: 'Create a new contact list',
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro klaviyo-migration - Klaviyo -> Velaro importer + savings calculator
|
|
3
|
+
*
|
|
4
|
+
* velaro klaviyo-migration preview Dry run: what would be imported (writes nothing)
|
|
5
|
+
* velaro klaviyo-migration apply Import lists, segments, flows (idempotent)
|
|
6
|
+
* velaro klaviyo-migration savings --profiles N --bill USD [--plan plan.professional]
|
|
7
|
+
*
|
|
8
|
+
* Uses the API key stored by the Klaviyo integration. Site scoping is server-side.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { request } from '../api.js';
|
|
12
|
+
|
|
13
|
+
const BOLD = '\x1b[1m';
|
|
14
|
+
const YEL = '\x1b[33m';
|
|
15
|
+
const RED = '\x1b[31m';
|
|
16
|
+
const DIM = '\x1b[2m';
|
|
17
|
+
const RST = '\x1b[0m';
|
|
18
|
+
|
|
19
|
+
function body(argv) {
|
|
20
|
+
const split = v => (v ? String(v).split(',').map(s => s.trim()).filter(Boolean) : undefined);
|
|
21
|
+
return {
|
|
22
|
+
listIds: split(argv.lists),
|
|
23
|
+
segmentIds: split(argv.segments),
|
|
24
|
+
flowIds: split(argv.flows),
|
|
25
|
+
includeFlows: argv['no-flows'] ? false : true,
|
|
26
|
+
maxProfilesPerGroup: argv.max,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function render(r) {
|
|
31
|
+
console.log(`\n${BOLD}${r.dryRun ? 'Klaviyo import preview (nothing written)' : 'Klaviyo import applied'}${RST}\n`);
|
|
32
|
+
for (const e of r.errors ?? []) console.log(`${RED} ! ${e}${RST}`);
|
|
33
|
+
for (const i of r.items ?? []) {
|
|
34
|
+
const n = i.kind === 'flow' ? `${i.stepCount} steps` : `${i.profileCount ?? 0} contacts`;
|
|
35
|
+
console.log(` ${i.kind.padEnd(8)} ${i.name} ${DIM}${i.action}, ${n}, skipped not-subscribed ${i.skippedNotSubscribed}${i.truncated ? ', capped' : ''}${RST}`);
|
|
36
|
+
for (const w of i.warnings ?? []) console.log(`${YEL} ${w}${RST}`);
|
|
37
|
+
}
|
|
38
|
+
if (!r.dryRun) {
|
|
39
|
+
console.log(`\n Created: ${r.listsCreated} lists, ${r.segmentsCreated} segments, ${r.flowsCreated} flows; ${r.contactsUpserted} contacts upserted.`);
|
|
40
|
+
}
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sharedOptions = yy => yy
|
|
45
|
+
.option('lists', { describe: 'Comma-separated Klaviyo list ids (default all)', type: 'string' })
|
|
46
|
+
.option('segments', { describe: 'Comma-separated Klaviyo segment ids (default all)', type: 'string' })
|
|
47
|
+
.option('flows', { describe: 'Comma-separated Klaviyo flow ids (default all)', type: 'string' })
|
|
48
|
+
.option('no-flows', { describe: 'Skip flows', type: 'boolean' })
|
|
49
|
+
.option('max', { describe: 'Max profiles per list/segment', type: 'number', default: 5000 });
|
|
50
|
+
|
|
51
|
+
export const klaviyoMigrationCommand = {
|
|
52
|
+
command: 'klaviyo-migration <subcommand>',
|
|
53
|
+
describe: 'Import lists, segments and flows from Klaviyo; compare cost',
|
|
54
|
+
builder: y => y
|
|
55
|
+
.command({
|
|
56
|
+
command: 'preview',
|
|
57
|
+
describe: 'Dry run, writes nothing',
|
|
58
|
+
builder: sharedOptions,
|
|
59
|
+
handler: async argv => render(await request('POST', '/KlaviyoMigration/Preview', body(argv))),
|
|
60
|
+
})
|
|
61
|
+
.command({
|
|
62
|
+
command: 'apply',
|
|
63
|
+
describe: 'Run the import (idempotent)',
|
|
64
|
+
builder: sharedOptions,
|
|
65
|
+
handler: async argv => render(await request('POST', '/KlaviyoMigration/Apply', body(argv))),
|
|
66
|
+
})
|
|
67
|
+
.command({
|
|
68
|
+
command: 'savings',
|
|
69
|
+
describe: 'Compare your Klaviyo bill to a Velaro plan catalog price',
|
|
70
|
+
builder: yy => yy
|
|
71
|
+
.option('profiles', { describe: 'Klaviyo profile count', type: 'number', demandOption: true })
|
|
72
|
+
.option('bill', { describe: 'Klaviyo monthly bill in USD', type: 'number', demandOption: true })
|
|
73
|
+
.option('plan', { describe: 'Velaro plan key (default plan.professional)', type: 'string' }),
|
|
74
|
+
handler: async argv => {
|
|
75
|
+
const qs = new URLSearchParams({ profiles: String(argv.profiles), klaviyoMonthlyBill: String(argv.bill) });
|
|
76
|
+
if (argv.plan) qs.set('planKey', argv.plan);
|
|
77
|
+
const r = (await request('GET', `/KlaviyoMigration/SavingsCalculator?${qs}`)).result;
|
|
78
|
+
console.log(`\nVelaro ${r.planName}: $${r.velaroMonthly}/mo vs Klaviyo $${r.klaviyoMonthly}/mo`);
|
|
79
|
+
console.log(`Difference: $${r.monthlySavings}/mo, $${r.annualSavings}/yr\n${DIM}${r.note}${RST}\n`);
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
.demandCommand(1, 'Specify a subcommand. Try: velaro klaviyo-migration --help'),
|
|
83
|
+
handler: () => {},
|
|
84
|
+
};
|
package/lib/commands/report.js
CHANGED
|
@@ -448,6 +448,57 @@ const workedHoursCommand = {
|
|
|
448
448
|
},
|
|
449
449
|
};
|
|
450
450
|
|
|
451
|
+
// -- visitor-traffic command ------------------------------------------------
|
|
452
|
+
// Visitor Traffic report (velaro-messaging): daily visit trend, top countries,
|
|
453
|
+
// chat-channel breakdown. Backend: server/Velaro.Messaging/Controllers/ReportsController.cs
|
|
454
|
+
// POST reports/visitor-traffic-overview { start, end } -> { kpis, dailySeries, countryBreakdown, channelBreakdown }
|
|
455
|
+
const visitorTrafficCommand = {
|
|
456
|
+
command: 'visitor-traffic',
|
|
457
|
+
describe: 'Visitor Traffic report: daily visit trend, top countries and chat-channel breakdown for a date range',
|
|
458
|
+
builder: (y) => y
|
|
459
|
+
.option('start', { type: 'string', describe: 'Start date (ISO 8601 or YYYY-MM-DD). Default: 30 days ago' })
|
|
460
|
+
.option('end', { type: 'string', describe: 'End date (ISO 8601 or YYYY-MM-DD). Default: now' }),
|
|
461
|
+
handler: async (argv) => {
|
|
462
|
+
const end = argv.end ? new Date(argv.end) : new Date();
|
|
463
|
+
const start = argv.start ? new Date(argv.start) : new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
464
|
+
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
|
465
|
+
console.error('--start and --end must be valid dates (ISO 8601 or YYYY-MM-DD).');
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const data = await messagingRequest('POST', '/reports/visitor-traffic-overview', {
|
|
470
|
+
start: start.toISOString(),
|
|
471
|
+
end: end.toISOString(),
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
section('Visitor Traffic');
|
|
475
|
+
console.log(` Period: ${data?.start ?? start.toISOString().slice(0, 10)} -> ${data?.end ?? end.toISOString().slice(0, 10)}`);
|
|
476
|
+
|
|
477
|
+
const k = data?.kpis ?? {};
|
|
478
|
+
fmtRow('Total visits', fmtNum(k.totalVisits));
|
|
479
|
+
fmtRow('Avg daily visits', fmtNum(k.avgDailyVisits));
|
|
480
|
+
fmtRow('Countries reached', fmtNum(k.countriesReached));
|
|
481
|
+
fmtRow('Chats started', fmtNum(k.chatsStarted));
|
|
482
|
+
|
|
483
|
+
const countries = Array.isArray(data?.countryBreakdown) ? data.countryBreakdown : [];
|
|
484
|
+
section('Top Countries');
|
|
485
|
+
if (!countries.length) {
|
|
486
|
+
console.log(' (no data)');
|
|
487
|
+
} else {
|
|
488
|
+
for (const c of countries) console.log(` ${(c.country ?? '?').padEnd(6)} ${fmtNum(c.count)}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const channels = Array.isArray(data?.channelBreakdown) ? data.channelBreakdown : [];
|
|
492
|
+
section('By Channel');
|
|
493
|
+
if (!channels.length) {
|
|
494
|
+
console.log(' (no data)');
|
|
495
|
+
} else {
|
|
496
|
+
for (const c of channels) console.log(` ${(c.name ?? '?').padEnd(16)} ${fmtNum(c.count)} (${fmtNum(c.pct)}%)`);
|
|
497
|
+
}
|
|
498
|
+
console.log('');
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
451
502
|
// ── scheduled reports (Reports/Schedules) ──────────────────────────────────────
|
|
452
503
|
// Backend: server/Velaro.Admin/Controllers/ReportsController.cs
|
|
453
504
|
// GET Reports/Schedules -> scheduledReport[]
|
|
@@ -638,6 +689,7 @@ export const reportCommand = {
|
|
|
638
689
|
.command(conversationsCommand)
|
|
639
690
|
.command(callactivityCommand)
|
|
640
691
|
.command(workedHoursCommand)
|
|
692
|
+
.command(visitorTrafficCommand)
|
|
641
693
|
.command(schedulesCommand)
|
|
642
694
|
.command(exportCommand)
|
|
643
695
|
.demandCommand(1, 'Specify a report type. Run "velaro report --help" for options.'),
|
package/package.json
CHANGED