@startanaicompany/crm 2.16.0 → 2.18.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.
- package/index.js +581 -3
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -603,7 +603,8 @@ leadsCmd
|
|
|
603
603
|
client.get(`/leads/${leadId}/score-history`),
|
|
604
604
|
]);
|
|
605
605
|
const lead = leadRes.data.data;
|
|
606
|
-
|
|
606
|
+
// Bug 00d0c981: score-history returns { history: [], total: N } not a bare array
|
|
607
|
+
const history = histRes.data.data?.history || histRes.data.data || [];
|
|
607
608
|
const label = lead.score >= 80 ? 'Hot' : lead.score >= 60 ? 'Qualified' : lead.score >= 40 ? 'Developing' : lead.score >= 20 ? 'Warm' : 'Cold';
|
|
608
609
|
console.log(`\nLead: ${lead.name} <${lead.email}>`);
|
|
609
610
|
console.log(`Current Score: ${lead.score ?? 'N/A'} (${label})`);
|
|
@@ -801,8 +802,8 @@ leadsCmd
|
|
|
801
802
|
const client = getClient(globalOpts);
|
|
802
803
|
try {
|
|
803
804
|
if (opts.to) {
|
|
804
|
-
//
|
|
805
|
-
const res = await client.patch(`/leads/${leadId}`, {
|
|
805
|
+
// Bug 6d850c25: use PATCH /leads/:id/stage with { stage } not /leads/:id with { pipeline_stage }
|
|
806
|
+
const res = await client.patch(`/leads/${leadId}/stage`, { stage: opts.to });
|
|
806
807
|
const d = res.data.data;
|
|
807
808
|
console.log(`Stage set to: ${d.pipeline_stage}`);
|
|
808
809
|
printJSON(res.data);
|
|
@@ -1211,6 +1212,121 @@ leadsCmd
|
|
|
1211
1212
|
} catch (err) { handleError(err); }
|
|
1212
1213
|
});
|
|
1213
1214
|
|
|
1215
|
+
// Sprint 54 T1: leads merge — merge secondary lead into primary
|
|
1216
|
+
leadsCmd
|
|
1217
|
+
.command('merge <primary-id> <secondary-id>')
|
|
1218
|
+
.description('Merge secondary lead into primary lead (Sprint 54 T1). Use --dry-run to preview without committing.')
|
|
1219
|
+
.option('--dry-run', 'Preview what would be merged without committing changes', false)
|
|
1220
|
+
.action(async (primaryId, secondaryId, opts) => {
|
|
1221
|
+
const globalOpts = program.opts();
|
|
1222
|
+
const client = getClient(globalOpts);
|
|
1223
|
+
try {
|
|
1224
|
+
if (opts.dryRun) {
|
|
1225
|
+
// Dry-run: fetch both leads and show diff without calling merge
|
|
1226
|
+
const [primaryRes, secondaryRes] = await Promise.all([
|
|
1227
|
+
client.get(`/leads/${primaryId}`),
|
|
1228
|
+
client.get(`/leads/${secondaryId}`),
|
|
1229
|
+
]);
|
|
1230
|
+
const primary = primaryRes.data.data;
|
|
1231
|
+
const secondary = secondaryRes.data.data;
|
|
1232
|
+
console.log('\n[DRY RUN] Merge preview:');
|
|
1233
|
+
console.log(` Primary : [${primary.id}] ${primary.name} <${primary.email}> — ${primary.status}`);
|
|
1234
|
+
console.log(` Secondary: [${secondary.id}] ${secondary.name} <${secondary.email}> — ${secondary.status}`);
|
|
1235
|
+
console.log('\n What would happen:');
|
|
1236
|
+
console.log(` - Notes, communications, and audit history from secondary copied to primary`);
|
|
1237
|
+
console.log(` - Secondary lead soft-deleted (status → merged)`);
|
|
1238
|
+
console.log(` - Primary lead retained with merged data`);
|
|
1239
|
+
if (secondary.tags && secondary.tags.length > 0) {
|
|
1240
|
+
console.log(` - Tags from secondary that will be merged: ${secondary.tags.join(', ')}`);
|
|
1241
|
+
}
|
|
1242
|
+
console.log('\n Run without --dry-run to commit the merge.');
|
|
1243
|
+
} else {
|
|
1244
|
+
// Bug 58e6d000: server treats :id as the source (soft-deleted) and merge_into as target (kept)
|
|
1245
|
+
// So we POST to /leads/:secondaryId/merge with { merge_into: primaryId }
|
|
1246
|
+
const res = await client.post(`/leads/${secondaryId}/merge`, { merge_into: primaryId });
|
|
1247
|
+
console.log(`Merged lead ${secondaryId} into ${primaryId}.`);
|
|
1248
|
+
printJSON(res.data);
|
|
1249
|
+
}
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
handleError(err);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
// Sprint 54 T1: leads bulk-update --ids — ID-based bulk status update (distinct from filter-based bulk-update)
|
|
1256
|
+
leadsCmd
|
|
1257
|
+
.command('bulk-update-ids')
|
|
1258
|
+
.description('Bulk update status for specific leads by ID list (Sprint 54 T1). Up to 100 leads per call.')
|
|
1259
|
+
.requiredOption('--ids <ids>', 'Comma-separated lead IDs to update')
|
|
1260
|
+
.requiredOption('--status <status>', 'New status: new|contacted|qualified|unresponsive|converted|lost')
|
|
1261
|
+
.option('--close-reason <reason>', 'Close reason (required when --status lost)')
|
|
1262
|
+
.action(async (opts) => {
|
|
1263
|
+
const globalOpts = program.opts();
|
|
1264
|
+
const client = getClient(globalOpts);
|
|
1265
|
+
const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
1266
|
+
if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
|
|
1267
|
+
const updates = { status: opts.status };
|
|
1268
|
+
if (opts.closeReason) updates.close_reason = opts.closeReason;
|
|
1269
|
+
try {
|
|
1270
|
+
const res = await client.patch('/leads/bulk', { ids, updates });
|
|
1271
|
+
const d = res.data.data || {};
|
|
1272
|
+
console.log(`Updated: ${d.updated !== undefined ? d.updated : ids.length} lead(s).`);
|
|
1273
|
+
if (d.failed && d.failed.length > 0) {
|
|
1274
|
+
console.log(`Failed IDs: ${d.failed.join(', ')}`);
|
|
1275
|
+
}
|
|
1276
|
+
printJSON(res.data);
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
handleError(err);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// Sprint 54 T1: leads bulk-tag — add a tag to multiple leads by ID
|
|
1283
|
+
leadsCmd
|
|
1284
|
+
.command('bulk-tag')
|
|
1285
|
+
.description('Add a tag to multiple leads by ID list (Sprint 54 T1). Up to 100 leads per call.')
|
|
1286
|
+
.requiredOption('--tag <tag>', 'Tag to add to each lead')
|
|
1287
|
+
.requiredOption('--ids <ids>', 'Comma-separated lead IDs')
|
|
1288
|
+
.action(async (opts) => {
|
|
1289
|
+
const globalOpts = program.opts();
|
|
1290
|
+
const client = getClient(globalOpts);
|
|
1291
|
+
const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
1292
|
+
if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
|
|
1293
|
+
try {
|
|
1294
|
+
const res = await client.patch('/leads/bulk', { ids, updates: { tags: [opts.tag] } });
|
|
1295
|
+
const d = res.data.data || {};
|
|
1296
|
+
console.log(`Tagged ${d.updated !== undefined ? d.updated : ids.length} lead(s) with "${opts.tag}".`);
|
|
1297
|
+
if (d.failed && d.failed.length > 0) {
|
|
1298
|
+
console.log(`Failed IDs: ${d.failed.join(', ')}`);
|
|
1299
|
+
}
|
|
1300
|
+
printJSON(res.data);
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
handleError(err);
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
// Sprint 54 T1: leads bulk-assign — reassign multiple leads to an agent by ID
|
|
1307
|
+
leadsCmd
|
|
1308
|
+
.command('bulk-assign')
|
|
1309
|
+
.description('Bulk reassign leads to an agent by ID list (Sprint 54 T1). Up to 100 leads per call.')
|
|
1310
|
+
.requiredOption('--to <agent>', 'Agent/user to assign leads to')
|
|
1311
|
+
.requiredOption('--ids <ids>', 'Comma-separated lead IDs')
|
|
1312
|
+
.action(async (opts) => {
|
|
1313
|
+
const globalOpts = program.opts();
|
|
1314
|
+
const client = getClient(globalOpts);
|
|
1315
|
+
const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
1316
|
+
if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
|
|
1317
|
+
try {
|
|
1318
|
+
const res = await client.patch('/leads/bulk', { ids, updates: { assigned_to: opts.to } });
|
|
1319
|
+
const d = res.data.data || {};
|
|
1320
|
+
console.log(`Assigned ${d.updated !== undefined ? d.updated : ids.length} lead(s) to "${opts.to}".`);
|
|
1321
|
+
if (d.failed && d.failed.length > 0) {
|
|
1322
|
+
console.log(`Failed IDs: ${d.failed.join(', ')}`);
|
|
1323
|
+
}
|
|
1324
|
+
printJSON(res.data);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
handleError(err);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1214
1330
|
// Sprint 52 T2: leads escalate + leads unassign
|
|
1215
1331
|
leadsCmd
|
|
1216
1332
|
.command('escalate <lead-id>')
|
|
@@ -3571,6 +3687,468 @@ sequencesCmd
|
|
|
3571
3687
|
} catch (err) { handleError(err); }
|
|
3572
3688
|
});
|
|
3573
3689
|
|
|
3690
|
+
// Sprint 54 T3: sequences stats <sequenceId>
|
|
3691
|
+
sequencesCmd
|
|
3692
|
+
.command('stats <sequenceId>')
|
|
3693
|
+
.description('Get enrollment stats for a sequence (Sprint 54 T3)')
|
|
3694
|
+
.action(async (sequenceId) => {
|
|
3695
|
+
const globalOpts = program.opts();
|
|
3696
|
+
const client = getClient(globalOpts);
|
|
3697
|
+
try {
|
|
3698
|
+
const res = await client.get(`/sequences/${sequenceId}/stats`);
|
|
3699
|
+
const d = res.data.data || {};
|
|
3700
|
+
console.log(`\nSequence: ${d.sequence_name} [${d.sequence_id}]`);
|
|
3701
|
+
console.log(`Active: ${d.active ? 'yes' : 'no'} Steps: ${d.step_count}`);
|
|
3702
|
+
console.log(`Total enrolled: ${d.total_enrolled}`);
|
|
3703
|
+
if (d.by_status) {
|
|
3704
|
+
console.log(` active: ${d.by_status.active}`);
|
|
3705
|
+
console.log(` completed: ${d.by_status.completed}`);
|
|
3706
|
+
console.log(` unsubscribed: ${d.by_status.unsubscribed}`);
|
|
3707
|
+
console.log(` paused: ${d.by_status.paused}`);
|
|
3708
|
+
console.log(` failed: ${d.by_status.failed}`);
|
|
3709
|
+
}
|
|
3710
|
+
if (d.first_enrollment) console.log(`First enrollment: ${new Date(d.first_enrollment).toLocaleString()}`);
|
|
3711
|
+
if (d.last_enrollment) console.log(`Last enrollment: ${new Date(d.last_enrollment).toLocaleString()}`);
|
|
3712
|
+
} catch (err) { handleError(err); }
|
|
3713
|
+
});
|
|
3714
|
+
|
|
3715
|
+
// Sprint 54 T3: leads sequences <lead-id>
|
|
3716
|
+
leadsCmd
|
|
3717
|
+
.command('sequences <lead-id>')
|
|
3718
|
+
.description('List sequences a lead is enrolled in (Sprint 54 T3)')
|
|
3719
|
+
.action(async (leadId) => {
|
|
3720
|
+
const globalOpts = program.opts();
|
|
3721
|
+
const client = getClient(globalOpts);
|
|
3722
|
+
try {
|
|
3723
|
+
const res = await client.get(`/leads/${leadId}/sequences`);
|
|
3724
|
+
const enrollments = res.data.data || res.data || [];
|
|
3725
|
+
if (enrollments.length === 0) { console.log('Lead is not enrolled in any sequences.'); return; }
|
|
3726
|
+
enrollments.forEach(e => {
|
|
3727
|
+
const step = e.current_step || 'N/A';
|
|
3728
|
+
const next = e.next_send_at ? new Date(e.next_send_at).toLocaleString() : 'N/A';
|
|
3729
|
+
console.log(`[${e.sequence_id}] ${e.sequence_name} — status: ${e.status}, step: ${step}, next: ${next}`);
|
|
3730
|
+
});
|
|
3731
|
+
} catch (err) { handleError(err); }
|
|
3732
|
+
});
|
|
3733
|
+
|
|
3734
|
+
// ============================================================
|
|
3735
|
+
// FORMS COMMANDS — Sprint 54 T2
|
|
3736
|
+
// ============================================================
|
|
3737
|
+
const formsCmd = program.command('forms').description('Manage lead capture forms (Sprint 54 T2)');
|
|
3738
|
+
|
|
3739
|
+
formsCmd
|
|
3740
|
+
.command('list')
|
|
3741
|
+
.description('List all forms in the workspace')
|
|
3742
|
+
.action(async () => {
|
|
3743
|
+
const globalOpts = program.opts();
|
|
3744
|
+
const client = getClient(globalOpts);
|
|
3745
|
+
try {
|
|
3746
|
+
const res = await client.get('/apikey/forms');
|
|
3747
|
+
const forms = res.data.data || res.data || [];
|
|
3748
|
+
if (forms.length === 0) { console.log('No forms found.'); return; }
|
|
3749
|
+
forms.forEach(f => {
|
|
3750
|
+
console.log(`[${f.id}] ${f.name} — submissions: ${f.submission_count || 0} — created: ${new Date(f.created_at).toLocaleDateString()}`);
|
|
3751
|
+
});
|
|
3752
|
+
} catch (err) { handleError(err); }
|
|
3753
|
+
});
|
|
3754
|
+
|
|
3755
|
+
formsCmd
|
|
3756
|
+
.command('get <form-id>')
|
|
3757
|
+
.description('Get form details by ID')
|
|
3758
|
+
.action(async (formId) => {
|
|
3759
|
+
const globalOpts = program.opts();
|
|
3760
|
+
const client = getClient(globalOpts);
|
|
3761
|
+
try {
|
|
3762
|
+
const res = await client.get(`/apikey/forms/${formId}`);
|
|
3763
|
+
printJSON(res.data);
|
|
3764
|
+
} catch (err) { handleError(err); }
|
|
3765
|
+
});
|
|
3766
|
+
|
|
3767
|
+
formsCmd
|
|
3768
|
+
.command('create')
|
|
3769
|
+
.description('Create a new lead capture form')
|
|
3770
|
+
.requiredOption('--name <name>', 'Form name')
|
|
3771
|
+
.option('--success-message <msg>', 'Message shown after submission', 'Thank you! We will be in touch.')
|
|
3772
|
+
.option('--redirect-url <url>', 'Redirect URL after submission')
|
|
3773
|
+
.option('--fields <json>', 'JSON array of field definitions (e.g. \'[{"key":"email","label":"Email","type":"email","required":true}]\')')
|
|
3774
|
+
.action(async (opts) => {
|
|
3775
|
+
const globalOpts = program.opts();
|
|
3776
|
+
const client = getClient(globalOpts);
|
|
3777
|
+
let fields = [];
|
|
3778
|
+
if (opts.fields) {
|
|
3779
|
+
try { fields = JSON.parse(opts.fields); } catch (e) { console.error('Error: --fields must be valid JSON array'); process.exit(1); }
|
|
3780
|
+
}
|
|
3781
|
+
try {
|
|
3782
|
+
const body = { name: opts.name, fields, success_message: opts.successMessage };
|
|
3783
|
+
if (opts.redirectUrl) body.redirect_url = opts.redirectUrl;
|
|
3784
|
+
const res = await client.post('/apikey/forms', body);
|
|
3785
|
+
console.log(`Form created: ${res.data.data?.id || res.data?.id}`);
|
|
3786
|
+
printJSON(res.data);
|
|
3787
|
+
} catch (err) { handleError(err); }
|
|
3788
|
+
});
|
|
3789
|
+
|
|
3790
|
+
formsCmd
|
|
3791
|
+
.command('submissions <form-id>')
|
|
3792
|
+
.description('List submissions for a form')
|
|
3793
|
+
.option('--limit <n>', 'Max results', '50')
|
|
3794
|
+
.option('--offset <n>', 'Pagination offset', '0')
|
|
3795
|
+
.action(async (formId, opts) => {
|
|
3796
|
+
const globalOpts = program.opts();
|
|
3797
|
+
const client = getClient(globalOpts);
|
|
3798
|
+
try {
|
|
3799
|
+
const res = await client.get(`/apikey/forms/${formId}/submissions`, { params: { limit: opts.limit, offset: opts.offset } });
|
|
3800
|
+
const d = res.data.data || {};
|
|
3801
|
+
const subs = d.data || [];
|
|
3802
|
+
if (subs.length === 0) { console.log('No submissions found.'); return; }
|
|
3803
|
+
console.log(`Total: ${d.total || subs.length}`);
|
|
3804
|
+
subs.forEach(s => {
|
|
3805
|
+
const when = s.submitted_at ? new Date(s.submitted_at).toLocaleString() : 'N/A';
|
|
3806
|
+
console.log(`[${s.id}] ${s.lead_name || 'Unknown'} <${s.lead_email || ''}> — ${when}`);
|
|
3807
|
+
});
|
|
3808
|
+
} catch (err) { handleError(err); }
|
|
3809
|
+
});
|
|
3810
|
+
|
|
3811
|
+
formsCmd
|
|
3812
|
+
.command('delete <form-id>')
|
|
3813
|
+
.description('Delete a form by ID')
|
|
3814
|
+
.action(async (formId) => {
|
|
3815
|
+
const globalOpts = program.opts();
|
|
3816
|
+
const client = getClient(globalOpts);
|
|
3817
|
+
try {
|
|
3818
|
+
await client.delete(`/apikey/forms/${formId}`);
|
|
3819
|
+
console.log(`Form ${formId} deleted.`);
|
|
3820
|
+
} catch (err) { handleError(err); }
|
|
3821
|
+
});
|
|
3822
|
+
|
|
3823
|
+
// ============================================================
|
|
3824
|
+
// AUDIT COMMANDS — Sprint 55 T1 (admin JWT via stagesAdminLogin)
|
|
3825
|
+
// ============================================================
|
|
3826
|
+
const auditCmd = program.command('audit').description('View workspace audit log (Sprint 55 T1) — requires admin login');
|
|
3827
|
+
|
|
3828
|
+
auditCmd
|
|
3829
|
+
.command('list')
|
|
3830
|
+
.description('List audit log entries with optional filters')
|
|
3831
|
+
.option('--lead-id <id>', 'Filter by lead ID')
|
|
3832
|
+
.option('--author-type <type>', 'Filter by actor type: api_key|human-admin|system')
|
|
3833
|
+
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
3834
|
+
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
3835
|
+
.option('--limit <n>', 'Max results (max 200)', '50')
|
|
3836
|
+
.option('--offset <n>', 'Pagination offset', '0')
|
|
3837
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3838
|
+
.option('--email <email>', 'Admin email')
|
|
3839
|
+
.option('--password <password>', 'Admin password')
|
|
3840
|
+
.action(async (opts) => {
|
|
3841
|
+
const globalOpts = program.opts();
|
|
3842
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
3843
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
3844
|
+
const cfg = loadConfig();
|
|
3845
|
+
try {
|
|
3846
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
3847
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
3848
|
+
const params = { limit: opts.limit, offset: opts.offset };
|
|
3849
|
+
if (opts.leadId) params.lead_id = opts.leadId;
|
|
3850
|
+
if (opts.authorType) params.author_type = opts.authorType;
|
|
3851
|
+
if (opts.from) params.from = opts.from;
|
|
3852
|
+
if (opts.to) params.to = opts.to;
|
|
3853
|
+
const res = await axios.get(`${base}/api/v1/admin/audit-log`, {
|
|
3854
|
+
params, headers: { Authorization: `Bearer ${token}` }
|
|
3855
|
+
});
|
|
3856
|
+
const d = res.data.data || {};
|
|
3857
|
+
const items = d.items || [];
|
|
3858
|
+
console.log(`Total: ${d.total || items.length}`);
|
|
3859
|
+
items.forEach(e => {
|
|
3860
|
+
const when = new Date(e.created_at).toLocaleString();
|
|
3861
|
+
console.log(`[${e.id}] ${when} | ${e.actor_type} ${e.actor_name || 'system'} | ${e.action}: ${e.old_value || '—'} → ${e.new_value || '—'} | lead: ${e.lead_name || e.lead_id || 'N/A'}`);
|
|
3862
|
+
});
|
|
3863
|
+
} catch (err) { handleError(err); }
|
|
3864
|
+
});
|
|
3865
|
+
|
|
3866
|
+
auditCmd
|
|
3867
|
+
.command('get <entry-id>')
|
|
3868
|
+
.description('Get a single audit log entry by ID')
|
|
3869
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3870
|
+
.option('--email <email>', 'Admin email')
|
|
3871
|
+
.option('--password <password>', 'Admin password')
|
|
3872
|
+
.action(async (entryId, opts) => {
|
|
3873
|
+
const globalOpts = program.opts();
|
|
3874
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
3875
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
3876
|
+
const cfg = loadConfig();
|
|
3877
|
+
try {
|
|
3878
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
3879
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
3880
|
+
const res = await axios.get(`${base}/api/v1/admin/audit-log/${entryId}`, {
|
|
3881
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3882
|
+
});
|
|
3883
|
+
printJSON(res.data);
|
|
3884
|
+
} catch (err) { handleError(err); }
|
|
3885
|
+
});
|
|
3886
|
+
|
|
3887
|
+
// ============================================================
|
|
3888
|
+
// TAGS COMMANDS — Sprint 55 T1 (API key auth)
|
|
3889
|
+
// ============================================================
|
|
3890
|
+
const tagsCmd = program.command('tags').description('Explore lead tags (Sprint 55 T1)');
|
|
3891
|
+
|
|
3892
|
+
tagsCmd
|
|
3893
|
+
.command('list')
|
|
3894
|
+
.description('List all unique tags in use with lead counts')
|
|
3895
|
+
.option('--limit <n>', 'Max tags to return', '100')
|
|
3896
|
+
.action(async (opts) => {
|
|
3897
|
+
const globalOpts = program.opts();
|
|
3898
|
+
const client = getClient(globalOpts);
|
|
3899
|
+
try {
|
|
3900
|
+
const res = await client.get('/tags', { params: { limit: opts.limit } });
|
|
3901
|
+
const tags = res.data.data || res.data || [];
|
|
3902
|
+
if (tags.length === 0) { console.log('No tags in use.'); return; }
|
|
3903
|
+
tags.forEach(t => console.log(`${t.tag} (${t.count} lead${t.count !== 1 ? 's' : ''})`));
|
|
3904
|
+
} catch (err) { handleError(err); }
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3907
|
+
tagsCmd
|
|
3908
|
+
.command('leads <tag-name>')
|
|
3909
|
+
.description('List leads with a specific tag')
|
|
3910
|
+
.option('--limit <n>', 'Max results', '50')
|
|
3911
|
+
.action(async (tagName, opts) => {
|
|
3912
|
+
const globalOpts = program.opts();
|
|
3913
|
+
const client = getClient(globalOpts);
|
|
3914
|
+
try {
|
|
3915
|
+
const res = await client.get('/leads', { params: { tag: tagName, per_page: parseInt(opts.limit) } });
|
|
3916
|
+
const leads = res.data.data || [];
|
|
3917
|
+
if (leads.length === 0) { console.log(`No leads found with tag "${tagName}".`); return; }
|
|
3918
|
+
console.log(`Leads tagged "${tagName}" (${leads.length}):`);
|
|
3919
|
+
leads.forEach(l => console.log(`[${l.id}] ${l.name} <${l.email}> — ${l.status} (${l.assigned_to || 'unassigned'})`));
|
|
3920
|
+
} catch (err) { handleError(err); }
|
|
3921
|
+
});
|
|
3922
|
+
|
|
3923
|
+
// ============================================================
|
|
3924
|
+
// SLA COMMANDS — Sprint 55 T2 (admin JWT via stagesAdminLogin)
|
|
3925
|
+
// ============================================================
|
|
3926
|
+
const slaCmd = program.command('sla').description('Manage SLA escalation rules and view breaches (Sprint 55 T2)');
|
|
3927
|
+
|
|
3928
|
+
slaCmd
|
|
3929
|
+
.command('list')
|
|
3930
|
+
.description('List all SLA escalation rules')
|
|
3931
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3932
|
+
.option('--email <email>', 'Admin email')
|
|
3933
|
+
.option('--password <password>', 'Admin password')
|
|
3934
|
+
.action(async (opts) => {
|
|
3935
|
+
const globalOpts = program.opts();
|
|
3936
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
3937
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
3938
|
+
const cfg = loadConfig();
|
|
3939
|
+
try {
|
|
3940
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
3941
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
3942
|
+
const res = await axios.get(`${base}/api/v1/admin/sla-escalation-rules`, {
|
|
3943
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3944
|
+
});
|
|
3945
|
+
const rules = res.data.data || res.data || [];
|
|
3946
|
+
if (rules.length === 0) { console.log('No SLA rules configured.'); return; }
|
|
3947
|
+
rules.forEach(r => {
|
|
3948
|
+
const hours = (r.escalate_after_breach_minutes / 60).toFixed(1);
|
|
3949
|
+
const stage = r.stage_name_label ? `stage: ${r.stage_name_label}` : 'all stages';
|
|
3950
|
+
const active = r.is_active ? '✓' : '✗';
|
|
3951
|
+
console.log(`[${r.id}] ${active} "${r.name}" — ${hours}h breach → escalate to ${r.escalate_to} (${stage})`);
|
|
3952
|
+
});
|
|
3953
|
+
} catch (err) { handleError(err); }
|
|
3954
|
+
});
|
|
3955
|
+
|
|
3956
|
+
slaCmd
|
|
3957
|
+
.command('create')
|
|
3958
|
+
.description('Create a new SLA escalation rule')
|
|
3959
|
+
.requiredOption('--name <name>', 'Rule name')
|
|
3960
|
+
.requiredOption('--hours <n>', 'Hours after breach before escalating')
|
|
3961
|
+
.requiredOption('--escalate-to <agent>', 'Agent/email to escalate to')
|
|
3962
|
+
.option('--stage-id <id>', 'Pipeline stage UUID to scope this rule (omit for all stages)')
|
|
3963
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3964
|
+
.option('--email <email>', 'Admin email')
|
|
3965
|
+
.option('--password <password>', 'Admin password')
|
|
3966
|
+
.action(async (opts) => {
|
|
3967
|
+
const globalOpts = program.opts();
|
|
3968
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
3969
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
3970
|
+
const cfg = loadConfig();
|
|
3971
|
+
const minutes = Math.round(parseFloat(opts.hours) * 60);
|
|
3972
|
+
if (isNaN(minutes) || minutes < 1) { console.error('Error: --hours must be a positive number'); process.exit(1); }
|
|
3973
|
+
try {
|
|
3974
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
3975
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
3976
|
+
const body = { name: opts.name, escalate_after_breach_minutes: minutes, escalate_to: opts.escalateTo };
|
|
3977
|
+
if (opts.stageId) body.stage_id = opts.stageId;
|
|
3978
|
+
const res = await axios.post(`${base}/api/v1/admin/sla-escalation-rules`, body, {
|
|
3979
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
|
|
3980
|
+
});
|
|
3981
|
+
console.log(`SLA rule created: ${res.data.data?.id || res.data?.id}`);
|
|
3982
|
+
printJSON(res.data);
|
|
3983
|
+
} catch (err) { handleError(err); }
|
|
3984
|
+
});
|
|
3985
|
+
|
|
3986
|
+
slaCmd
|
|
3987
|
+
.command('update <id>')
|
|
3988
|
+
.description('Update an SLA escalation rule')
|
|
3989
|
+
.option('--name <name>', 'New rule name')
|
|
3990
|
+
.option('--hours <n>', 'New escalation threshold in hours')
|
|
3991
|
+
.option('--escalate-to <agent>', 'New escalation target')
|
|
3992
|
+
.option('--active <bool>', 'Enable/disable rule (true|false)')
|
|
3993
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3994
|
+
.option('--email <email>', 'Admin email')
|
|
3995
|
+
.option('--password <password>', 'Admin password')
|
|
3996
|
+
.action(async (id, opts) => {
|
|
3997
|
+
const globalOpts = program.opts();
|
|
3998
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
3999
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4000
|
+
const cfg = loadConfig();
|
|
4001
|
+
const body = {};
|
|
4002
|
+
if (opts.name) body.name = opts.name;
|
|
4003
|
+
if (opts.hours !== undefined) {
|
|
4004
|
+
const minutes = Math.round(parseFloat(opts.hours) * 60);
|
|
4005
|
+
if (isNaN(minutes) || minutes < 1) { console.error('Error: --hours must be a positive number'); process.exit(1); }
|
|
4006
|
+
body.escalate_after_breach_minutes = minutes;
|
|
4007
|
+
}
|
|
4008
|
+
if (opts.escalateTo) body.escalate_to = opts.escalateTo;
|
|
4009
|
+
if (opts.active !== undefined) body.is_active = opts.active === 'true';
|
|
4010
|
+
if (Object.keys(body).length === 0) { console.error('Error: provide at least one field to update'); process.exit(1); }
|
|
4011
|
+
try {
|
|
4012
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4013
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4014
|
+
const res = await axios.patch(`${base}/api/v1/admin/sla-escalation-rules/${id}`, body, {
|
|
4015
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
|
|
4016
|
+
});
|
|
4017
|
+
console.log(`SLA rule ${id} updated.`);
|
|
4018
|
+
printJSON(res.data);
|
|
4019
|
+
} catch (err) { handleError(err); }
|
|
4020
|
+
});
|
|
4021
|
+
|
|
4022
|
+
slaCmd
|
|
4023
|
+
.command('delete <id>')
|
|
4024
|
+
.description('Delete an SLA escalation rule')
|
|
4025
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4026
|
+
.option('--email <email>', 'Admin email')
|
|
4027
|
+
.option('--password <password>', 'Admin password')
|
|
4028
|
+
.action(async (id, opts) => {
|
|
4029
|
+
const globalOpts = program.opts();
|
|
4030
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4031
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4032
|
+
const cfg = loadConfig();
|
|
4033
|
+
try {
|
|
4034
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4035
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4036
|
+
await axios.delete(`${base}/api/v1/admin/sla-escalation-rules/${id}`, {
|
|
4037
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
4038
|
+
});
|
|
4039
|
+
console.log(`SLA rule ${id} deleted.`);
|
|
4040
|
+
} catch (err) { handleError(err); }
|
|
4041
|
+
});
|
|
4042
|
+
|
|
4043
|
+
slaCmd
|
|
4044
|
+
.command('breaches')
|
|
4045
|
+
.description('List leads with active SLA breaches')
|
|
4046
|
+
.option('--status <status>', 'open (default) or resolved', 'open')
|
|
4047
|
+
.option('--limit <n>', 'Max results', '50')
|
|
4048
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4049
|
+
.option('--email <email>', 'Admin email')
|
|
4050
|
+
.option('--password <password>', 'Admin password')
|
|
4051
|
+
.action(async (opts) => {
|
|
4052
|
+
const globalOpts = program.opts();
|
|
4053
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4054
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4055
|
+
const cfg = loadConfig();
|
|
4056
|
+
try {
|
|
4057
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4058
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4059
|
+
const res = await axios.get(`${base}/api/v1/admin/sla-breaches`, {
|
|
4060
|
+
params: { status: opts.status, limit: opts.limit },
|
|
4061
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
4062
|
+
});
|
|
4063
|
+
const d = res.data.data || {};
|
|
4064
|
+
const items = d.items || [];
|
|
4065
|
+
if (items.length === 0) { console.log(`No ${opts.status} SLA breaches.`); return; }
|
|
4066
|
+
console.log(`${opts.status.toUpperCase()} SLA breaches (${d.total || items.length}):`);
|
|
4067
|
+
items.forEach(l => {
|
|
4068
|
+
const breached = l.breached_at ? new Date(l.breached_at).toLocaleString() : 'N/A';
|
|
4069
|
+
console.log(`[${l.id}] ${l.name} <${l.email}> — ${l.status} | stage: ${l.pipeline_stage || 'N/A'} | breached: ${breached} | assigned: ${l.assigned_to || 'none'}`);
|
|
4070
|
+
});
|
|
4071
|
+
} catch (err) { handleError(err); }
|
|
4072
|
+
});
|
|
4073
|
+
|
|
4074
|
+
// ============================================================
|
|
4075
|
+
// ANALYTICS COMMANDS — Sprint 55 T3 (admin JWT via stagesAdminLogin)
|
|
4076
|
+
// (analyticsCmd already declared above — appending pipeline + funnel subcommands)
|
|
4077
|
+
// ============================================================
|
|
4078
|
+
|
|
4079
|
+
analyticsCmd
|
|
4080
|
+
.command('pipeline')
|
|
4081
|
+
.description('Stage-by-stage pipeline breakdown: lead count, avg deal value, avg time-in-stage, conversion rate')
|
|
4082
|
+
.option('--stage <name>', 'Filter output to a single stage name')
|
|
4083
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4084
|
+
.option('--email <email>', 'Admin email')
|
|
4085
|
+
.option('--password <password>', 'Admin password')
|
|
4086
|
+
.action(async (opts) => {
|
|
4087
|
+
const globalOpts = program.opts();
|
|
4088
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4089
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4090
|
+
const cfg = loadConfig();
|
|
4091
|
+
try {
|
|
4092
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4093
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4094
|
+
const res = await axios.get(`${base}/api/v1/admin/analytics/funnel`, {
|
|
4095
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
4096
|
+
});
|
|
4097
|
+
let stages = (res.data.data?.stages || []);
|
|
4098
|
+
if (opts.stage) {
|
|
4099
|
+
stages = stages.filter(s => s.stage_name.toLowerCase() === opts.stage.toLowerCase());
|
|
4100
|
+
if (stages.length === 0) { console.log(`Stage "${opts.stage}" not found.`); return; }
|
|
4101
|
+
}
|
|
4102
|
+
console.log('\nPipeline Breakdown:');
|
|
4103
|
+
console.log('─'.repeat(90));
|
|
4104
|
+
stages.forEach(s => {
|
|
4105
|
+
const deal = s.avg_deal_value != null ? `$${s.avg_deal_value}` : 'N/A';
|
|
4106
|
+
const time = s.avg_time_in_stage_hours != null ? `${s.avg_time_in_stage_hours}h` : 'N/A';
|
|
4107
|
+
const conv = s.conversion_rate_to_next_stage != null ? `${s.conversion_rate_to_next_stage}%` : 'N/A';
|
|
4108
|
+
console.log(`${String(s.stage_name).padEnd(20)} leads: ${String(s.lead_count).padStart(4)} | avg deal: ${String(deal).padStart(10)} | avg time: ${String(time).padStart(8)} | conv: ${conv}`);
|
|
4109
|
+
});
|
|
4110
|
+
} catch (err) { handleError(err); }
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
analyticsCmd
|
|
4114
|
+
.command('funnel')
|
|
4115
|
+
.description('Full top-to-bottom conversion funnel with drop-off rates')
|
|
4116
|
+
.option('--from <date>', 'From date (YYYY-MM-DD, default: 30 days ago)')
|
|
4117
|
+
.option('--to <date>', 'To date (YYYY-MM-DD, default: today)')
|
|
4118
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4119
|
+
.option('--email <email>', 'Admin email')
|
|
4120
|
+
.option('--password <password>', 'Admin password')
|
|
4121
|
+
.action(async (opts) => {
|
|
4122
|
+
const globalOpts = program.opts();
|
|
4123
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4124
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4125
|
+
const cfg = loadConfig();
|
|
4126
|
+
try {
|
|
4127
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4128
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4129
|
+
const params = {};
|
|
4130
|
+
if (opts.from) params.from = opts.from;
|
|
4131
|
+
if (opts.to) params.to = opts.to;
|
|
4132
|
+
const res = await axios.get(`${base}/api/v1/admin/analytics/funnel`, {
|
|
4133
|
+
params, headers: { Authorization: `Bearer ${token}` }
|
|
4134
|
+
});
|
|
4135
|
+
const d = res.data.data || {};
|
|
4136
|
+
const stages = d.stages || [];
|
|
4137
|
+
const from = d.from ? new Date(d.from).toLocaleDateString() : '30d ago';
|
|
4138
|
+
const to = d.to ? new Date(d.to).toLocaleDateString() : 'today';
|
|
4139
|
+
console.log(`\nConversion Funnel (${from} → ${to}):`);
|
|
4140
|
+
console.log('─'.repeat(85));
|
|
4141
|
+
stages.forEach((s, i) => {
|
|
4142
|
+
const entered = s.leads_entered_window || 0;
|
|
4143
|
+
const exited = s.leads_exited_window || 0;
|
|
4144
|
+
const dropOff = s.drop_off_rate != null ? `${s.drop_off_rate}% drop-off` : 'N/A';
|
|
4145
|
+
const conv = s.conversion_rate_to_next_stage != null ? `${s.conversion_rate_to_next_stage}%` : 'N/A';
|
|
4146
|
+
const bar = '█'.repeat(Math.min(Math.round((entered / Math.max(...stages.map(x => x.leads_entered_window || 0), 1)) * 20), 20));
|
|
4147
|
+
console.log(`${String(s.stage_name).padEnd(20)} ${bar.padEnd(20)} entered: ${String(entered).padStart(4)} | exited: ${String(exited).padStart(4)} | conv: ${String(conv).padStart(5)} | ${dropOff}`);
|
|
4148
|
+
});
|
|
4149
|
+
} catch (err) { handleError(err); }
|
|
4150
|
+
});
|
|
4151
|
+
|
|
3574
4152
|
// ============================================================
|
|
3575
4153
|
// GDPR COMMANDS — Sprint 50 T2
|
|
3576
4154
|
// ============================================================
|