@startanaicompany/crm 2.15.0 → 2.17.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 +420 -29
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -580,21 +580,40 @@ leadsCmd
|
|
|
580
580
|
});
|
|
581
581
|
|
|
582
582
|
leadsCmd
|
|
583
|
-
.command('score')
|
|
584
|
-
.description('
|
|
585
|
-
.
|
|
586
|
-
.
|
|
587
|
-
.
|
|
588
|
-
.action(async (opts) => {
|
|
583
|
+
.command('score <lead-id>')
|
|
584
|
+
.description('Get or set lead score. Without --set shows current score + history. Score conventions: 0-19 Cold, 20-39 Warm, 40-59 Developing, 60-79 Qualified, 80-100 Hot (Sprint 53 T1)')
|
|
585
|
+
.option('--set <n>', 'Set score to this value (0-100 integer)')
|
|
586
|
+
.option('--reason <text>', 'Score reason/explanation (used with --set)')
|
|
587
|
+
.action(async (leadId, opts) => {
|
|
589
588
|
const globalOpts = program.opts();
|
|
590
589
|
const client = getClient(globalOpts);
|
|
591
|
-
const body = {
|
|
592
|
-
score: parseInt(opts.score),
|
|
593
|
-
...(opts.reason && { score_reason: opts.reason })
|
|
594
|
-
};
|
|
595
590
|
try {
|
|
596
|
-
|
|
597
|
-
|
|
591
|
+
if (opts.set !== undefined) {
|
|
592
|
+
// SET mode: PATCH /leads/:id/score
|
|
593
|
+
const body = { score: parseInt(opts.set) };
|
|
594
|
+
if (opts.reason) body.score_reason = opts.reason;
|
|
595
|
+
const res = await client.patch(`/leads/${leadId}/score`, body);
|
|
596
|
+
const d = res.data.data;
|
|
597
|
+
console.log(`Score set to ${d.score}${d.score_reason ? ' — ' + d.score_reason : ''}`);
|
|
598
|
+
printJSON(res.data);
|
|
599
|
+
} else {
|
|
600
|
+
// GET mode: show current score + history
|
|
601
|
+
const [leadRes, histRes] = await Promise.all([
|
|
602
|
+
client.get(`/leads/${leadId}`),
|
|
603
|
+
client.get(`/leads/${leadId}/score-history`),
|
|
604
|
+
]);
|
|
605
|
+
const lead = leadRes.data.data;
|
|
606
|
+
// Bug 00d0c981: score-history returns { history: [], total: N } not a bare array
|
|
607
|
+
const history = histRes.data.data?.history || histRes.data.data || [];
|
|
608
|
+
const label = lead.score >= 80 ? 'Hot' : lead.score >= 60 ? 'Qualified' : lead.score >= 40 ? 'Developing' : lead.score >= 20 ? 'Warm' : 'Cold';
|
|
609
|
+
console.log(`\nLead: ${lead.name} <${lead.email}>`);
|
|
610
|
+
console.log(`Current Score: ${lead.score ?? 'N/A'} (${label})`);
|
|
611
|
+
if (lead.score_reason) console.log(`Reason: ${lead.score_reason}`);
|
|
612
|
+
if (history.length > 0) {
|
|
613
|
+
console.log(`\nScore History (${history.length} entries):`);
|
|
614
|
+
history.slice(0, 10).forEach(h => console.log(` ${new Date(h.changed_at).toLocaleString()} — ${h.previous_score ?? '?'} → ${h.new_score} (${h.changed_by || 'system'})`));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
598
617
|
} catch (err) {
|
|
599
618
|
handleError(err);
|
|
600
619
|
}
|
|
@@ -708,13 +727,16 @@ leadsCmd
|
|
|
708
727
|
});
|
|
709
728
|
|
|
710
729
|
leadsCmd
|
|
711
|
-
.command('assign <id>
|
|
712
|
-
.description('Assign a lead to an agent (fires lead.assigned webhook) — S18-2')
|
|
713
|
-
.
|
|
730
|
+
.command('assign <id> [agent]')
|
|
731
|
+
.description('Assign a lead to an agent (fires lead.assigned webhook) — S18-2. Use positional [agent] or --to <agent>.')
|
|
732
|
+
.option('--to <agent>', 'Agent/user to assign to (alias for positional agent arg)')
|
|
733
|
+
.action(async (id, agent, opts) => {
|
|
714
734
|
const globalOpts = program.opts();
|
|
715
735
|
const client = getClient(globalOpts);
|
|
736
|
+
const assignTo = agent || opts.to;
|
|
737
|
+
if (!assignTo) { console.error('Error: specify agent as positional arg or --to <agent>'); process.exit(1); }
|
|
716
738
|
try {
|
|
717
|
-
const res = await client.patch(`/leads/${id}/assign`, { assigned_to:
|
|
739
|
+
const res = await client.patch(`/leads/${id}/assign`, { assigned_to: assignTo });
|
|
718
740
|
console.log(`Lead assigned to: ${res.data.data.assigned_to}`);
|
|
719
741
|
printJSON(res.data);
|
|
720
742
|
} catch (err) {
|
|
@@ -770,27 +792,67 @@ leadsCmd
|
|
|
770
792
|
}
|
|
771
793
|
});
|
|
772
794
|
|
|
773
|
-
// leads
|
|
795
|
+
// Sprint 53 T1: leads stage — get or set pipeline stage
|
|
774
796
|
leadsCmd
|
|
775
|
-
.command('
|
|
776
|
-
.description('
|
|
777
|
-
.
|
|
797
|
+
.command('stage <lead-id>')
|
|
798
|
+
.description('Get current pipeline stage or move lead to a new stage (Sprint 53 T1)')
|
|
799
|
+
.option('--to <stage>', 'Move lead to this pipeline stage name')
|
|
800
|
+
.action(async (leadId, opts) => {
|
|
778
801
|
const globalOpts = program.opts();
|
|
779
802
|
const client = getClient(globalOpts);
|
|
780
803
|
try {
|
|
781
|
-
|
|
804
|
+
if (opts.to) {
|
|
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 });
|
|
807
|
+
const d = res.data.data;
|
|
808
|
+
console.log(`Stage set to: ${d.pipeline_stage}`);
|
|
809
|
+
printJSON(res.data);
|
|
810
|
+
} else {
|
|
811
|
+
// GET mode: show current stage
|
|
812
|
+
const leadRes = await client.get(`/leads/${leadId}`);
|
|
813
|
+
const lead = leadRes.data.data;
|
|
814
|
+
console.log(`\nLead: ${lead.name} <${lead.email}>`);
|
|
815
|
+
console.log(`Current Stage: ${lead.pipeline_stage || 'N/A'}`);
|
|
816
|
+
if (lead.pipeline_id) {
|
|
817
|
+
try {
|
|
818
|
+
const pipeRes = await client.get(`/pipelines/${lead.pipeline_id}/stages`);
|
|
819
|
+
const stages = pipeRes.data.data || [];
|
|
820
|
+
if (stages.length > 0) {
|
|
821
|
+
console.log(`Available stages: ${stages.map(s => s.name).join(' → ')}`);
|
|
822
|
+
}
|
|
823
|
+
} catch (_) { /* pipeline stages not critical */ }
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
} catch (err) {
|
|
827
|
+
handleError(err);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// leads search <term> — S19-1 / Sprint 53 T3: added --q flag alias
|
|
832
|
+
leadsCmd
|
|
833
|
+
.command('search [term]')
|
|
834
|
+
.description('Search leads by name, email, company, or notes. Use positional term or --q flag.')
|
|
835
|
+
.option('--q <keyword>', 'Search keyword (alias for positional term)')
|
|
836
|
+
.option('--fields <fields>', 'Comma-separated fields to display (e.g. name,email,company)')
|
|
837
|
+
.option('--limit <n>', 'Max results', '50')
|
|
838
|
+
.action(async (term, opts) => {
|
|
839
|
+
const globalOpts = program.opts();
|
|
840
|
+
const client = getClient(globalOpts);
|
|
841
|
+
const query = term || opts.q;
|
|
842
|
+
if (!query) { console.error('Error: provide a search term (positional or --q)'); process.exit(1); }
|
|
843
|
+
try {
|
|
844
|
+
const res = await client.get('/leads', { params: { q: query, per_page: parseInt(opts.limit) } });
|
|
782
845
|
const leads = res.data.data || [];
|
|
783
846
|
if (leads.length === 0) {
|
|
784
|
-
console.log('No leads found matching: ' +
|
|
847
|
+
console.log('No leads found matching: ' + query);
|
|
785
848
|
return;
|
|
786
849
|
}
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}));
|
|
850
|
+
const fields = opts.fields ? opts.fields.split(',').map(f => f.trim()) : ['id', 'name', 'email', 'company', 'status'];
|
|
851
|
+
const rows = leads.map(l => {
|
|
852
|
+
const row = {};
|
|
853
|
+
fields.forEach(f => { row[f] = l[f] ?? '—'; });
|
|
854
|
+
return row;
|
|
855
|
+
});
|
|
794
856
|
console.table(rows);
|
|
795
857
|
} catch (err) {
|
|
796
858
|
handleError(err);
|
|
@@ -1120,6 +1182,149 @@ leadsCmd
|
|
|
1120
1182
|
} catch (err) { handleError(err); }
|
|
1121
1183
|
});
|
|
1122
1184
|
|
|
1185
|
+
// Sprint 53 T3: leads due — follow-up due/overdue list
|
|
1186
|
+
leadsCmd
|
|
1187
|
+
.command('due')
|
|
1188
|
+
.description('List leads with upcoming or overdue follow-ups (Sprint 53 T3)')
|
|
1189
|
+
.option('--today', 'Leads with follow-up scheduled for today')
|
|
1190
|
+
.option('--overdue', 'Leads with overdue follow-ups (follow_up_at < now)')
|
|
1191
|
+
.option('--assigned-to-me', 'Filter to leads assigned to the current API key name')
|
|
1192
|
+
.option('--limit <n>', 'Max results', '50')
|
|
1193
|
+
.action(async (opts) => {
|
|
1194
|
+
const globalOpts = program.opts();
|
|
1195
|
+
const client = getClient(globalOpts);
|
|
1196
|
+
try {
|
|
1197
|
+
const params = { per_page: parseInt(opts.limit) };
|
|
1198
|
+
if (opts.overdue) params.follow_up_overdue = 'true';
|
|
1199
|
+
if (opts.today) params.follow_up_today = 'true';
|
|
1200
|
+
if (!opts.overdue && !opts.today) {
|
|
1201
|
+
// default: both overdue + today
|
|
1202
|
+
params.follow_up_overdue = 'true';
|
|
1203
|
+
}
|
|
1204
|
+
const res = await client.get('/leads', { params });
|
|
1205
|
+
const leads = res.data.data || [];
|
|
1206
|
+
if (leads.length === 0) { console.log('No leads with due/overdue follow-ups.'); return; }
|
|
1207
|
+
leads.forEach(l => {
|
|
1208
|
+
const fu = l.follow_up_at ? new Date(l.follow_up_at).toLocaleString() : 'N/A';
|
|
1209
|
+
const overdue = l.follow_up_at && new Date(l.follow_up_at) < new Date() ? ' ⚠️ OVERDUE' : '';
|
|
1210
|
+
console.log(`[${l.id}] ${l.name} <${l.email}> — follow_up: ${fu}${overdue} (${l.assigned_to || 'unassigned'})`);
|
|
1211
|
+
});
|
|
1212
|
+
} catch (err) { handleError(err); }
|
|
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
|
+
const res = await client.post(`/leads/${primaryId}/merge`, { merge_into: secondaryId });
|
|
1245
|
+
console.log(`Merged lead ${secondaryId} into ${primaryId}.`);
|
|
1246
|
+
printJSON(res.data);
|
|
1247
|
+
}
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
handleError(err);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
// Sprint 54 T1: leads bulk-update --ids — ID-based bulk status update (distinct from filter-based bulk-update)
|
|
1254
|
+
leadsCmd
|
|
1255
|
+
.command('bulk-update-ids')
|
|
1256
|
+
.description('Bulk update status for specific leads by ID list (Sprint 54 T1). Up to 100 leads per call.')
|
|
1257
|
+
.requiredOption('--ids <ids>', 'Comma-separated lead IDs to update')
|
|
1258
|
+
.requiredOption('--status <status>', 'New status: new|contacted|qualified|unresponsive|converted|lost')
|
|
1259
|
+
.option('--close-reason <reason>', 'Close reason (required when --status lost)')
|
|
1260
|
+
.action(async (opts) => {
|
|
1261
|
+
const globalOpts = program.opts();
|
|
1262
|
+
const client = getClient(globalOpts);
|
|
1263
|
+
const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
1264
|
+
if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
|
|
1265
|
+
const updates = { status: opts.status };
|
|
1266
|
+
if (opts.closeReason) updates.close_reason = opts.closeReason;
|
|
1267
|
+
try {
|
|
1268
|
+
const res = await client.patch('/leads/bulk', { ids, updates });
|
|
1269
|
+
const d = res.data.data || {};
|
|
1270
|
+
console.log(`Updated: ${d.updated !== undefined ? d.updated : ids.length} lead(s).`);
|
|
1271
|
+
if (d.failed && d.failed.length > 0) {
|
|
1272
|
+
console.log(`Failed IDs: ${d.failed.join(', ')}`);
|
|
1273
|
+
}
|
|
1274
|
+
printJSON(res.data);
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
handleError(err);
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// Sprint 54 T1: leads bulk-tag — add a tag to multiple leads by ID
|
|
1281
|
+
leadsCmd
|
|
1282
|
+
.command('bulk-tag')
|
|
1283
|
+
.description('Add a tag to multiple leads by ID list (Sprint 54 T1). Up to 100 leads per call.')
|
|
1284
|
+
.requiredOption('--tag <tag>', 'Tag to add to each lead')
|
|
1285
|
+
.requiredOption('--ids <ids>', 'Comma-separated lead IDs')
|
|
1286
|
+
.action(async (opts) => {
|
|
1287
|
+
const globalOpts = program.opts();
|
|
1288
|
+
const client = getClient(globalOpts);
|
|
1289
|
+
const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
1290
|
+
if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
|
|
1291
|
+
try {
|
|
1292
|
+
const res = await client.patch('/leads/bulk', { ids, updates: { tags: [opts.tag] } });
|
|
1293
|
+
const d = res.data.data || {};
|
|
1294
|
+
console.log(`Tagged ${d.updated !== undefined ? d.updated : ids.length} lead(s) with "${opts.tag}".`);
|
|
1295
|
+
if (d.failed && d.failed.length > 0) {
|
|
1296
|
+
console.log(`Failed IDs: ${d.failed.join(', ')}`);
|
|
1297
|
+
}
|
|
1298
|
+
printJSON(res.data);
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
handleError(err);
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
// Sprint 54 T1: leads bulk-assign — reassign multiple leads to an agent by ID
|
|
1305
|
+
leadsCmd
|
|
1306
|
+
.command('bulk-assign')
|
|
1307
|
+
.description('Bulk reassign leads to an agent by ID list (Sprint 54 T1). Up to 100 leads per call.')
|
|
1308
|
+
.requiredOption('--to <agent>', 'Agent/user to assign leads to')
|
|
1309
|
+
.requiredOption('--ids <ids>', 'Comma-separated lead IDs')
|
|
1310
|
+
.action(async (opts) => {
|
|
1311
|
+
const globalOpts = program.opts();
|
|
1312
|
+
const client = getClient(globalOpts);
|
|
1313
|
+
const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
1314
|
+
if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
|
|
1315
|
+
try {
|
|
1316
|
+
const res = await client.patch('/leads/bulk', { ids, updates: { assigned_to: opts.to } });
|
|
1317
|
+
const d = res.data.data || {};
|
|
1318
|
+
console.log(`Assigned ${d.updated !== undefined ? d.updated : ids.length} lead(s) to "${opts.to}".`);
|
|
1319
|
+
if (d.failed && d.failed.length > 0) {
|
|
1320
|
+
console.log(`Failed IDs: ${d.failed.join(', ')}`);
|
|
1321
|
+
}
|
|
1322
|
+
printJSON(res.data);
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
handleError(err);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1123
1328
|
// Sprint 52 T2: leads escalate + leads unassign
|
|
1124
1329
|
leadsCmd
|
|
1125
1330
|
.command('escalate <lead-id>')
|
|
@@ -3088,6 +3293,59 @@ webhooksCmd
|
|
|
3088
3293
|
}
|
|
3089
3294
|
});
|
|
3090
3295
|
|
|
3296
|
+
// Sprint 53 T2: webhooks test / logs / retry
|
|
3297
|
+
webhooksCmd
|
|
3298
|
+
.command('test <webhook-id>')
|
|
3299
|
+
.description('Send a test payload to a webhook and check delivery (Sprint 53 T2)')
|
|
3300
|
+
.action(async (webhookId) => {
|
|
3301
|
+
const globalOpts = program.opts();
|
|
3302
|
+
const client = getClient(globalOpts);
|
|
3303
|
+
try {
|
|
3304
|
+
const res = await client.post(`/webhooks/${webhookId}/test`);
|
|
3305
|
+
const d = res.data.data;
|
|
3306
|
+
console.log(`Test queued — event: ${d.event}, webhook_id: ${d.webhook_id}`);
|
|
3307
|
+
printJSON(res.data);
|
|
3308
|
+
} catch (err) { handleError(err); }
|
|
3309
|
+
});
|
|
3310
|
+
|
|
3311
|
+
webhooksCmd
|
|
3312
|
+
.command('logs <webhook-id>')
|
|
3313
|
+
.description('View webhook delivery history (Sprint 53 T2)')
|
|
3314
|
+
.option('--limit <n>', 'Max results (default 20)', '20')
|
|
3315
|
+
.option('--status <status>', 'Filter by status: success or failed')
|
|
3316
|
+
.action(async (webhookId, opts) => {
|
|
3317
|
+
const globalOpts = program.opts();
|
|
3318
|
+
const client = getClient(globalOpts);
|
|
3319
|
+
try {
|
|
3320
|
+
const params = { limit: parseInt(opts.limit) };
|
|
3321
|
+
if (opts.status) params.status = opts.status;
|
|
3322
|
+
const res = await client.get(`/webhooks/${webhookId}/deliveries`, { params });
|
|
3323
|
+
const deliveries = res.data.data || [];
|
|
3324
|
+
if (deliveries.length === 0) { console.log('No delivery logs found.'); return; }
|
|
3325
|
+
deliveries.forEach(d => {
|
|
3326
|
+
const icon = d.success ? '✅' : '❌';
|
|
3327
|
+
const retry = d.next_retry_at ? ` | retry: ${new Date(d.next_retry_at).toLocaleString()}` : '';
|
|
3328
|
+
console.log(`${icon} [${d.id}] ${d.event} — HTTP ${d.status_code || '?'} at ${new Date(d.attempted_at).toLocaleString()}${retry}`);
|
|
3329
|
+
if (!d.success && d.error_message) console.log(` Error: ${d.error_message}`);
|
|
3330
|
+
});
|
|
3331
|
+
const p = res.data.pagination;
|
|
3332
|
+
if (p) console.log(`\nShowing ${deliveries.length} of ${p.total} total deliveries.`);
|
|
3333
|
+
} catch (err) { handleError(err); }
|
|
3334
|
+
});
|
|
3335
|
+
|
|
3336
|
+
webhooksCmd
|
|
3337
|
+
.command('retry <webhook-id> <delivery-id>')
|
|
3338
|
+
.description('Retry a failed webhook delivery (Sprint 53 T2)')
|
|
3339
|
+
.action(async (webhookId, deliveryId) => {
|
|
3340
|
+
const globalOpts = program.opts();
|
|
3341
|
+
const client = getClient(globalOpts);
|
|
3342
|
+
try {
|
|
3343
|
+
const res = await client.post(`/webhooks/${webhookId}/deliveries/${deliveryId}/retry`);
|
|
3344
|
+
console.log(`Retry queued for delivery ${deliveryId}`);
|
|
3345
|
+
printJSON(res.data);
|
|
3346
|
+
} catch (err) { handleError(err); }
|
|
3347
|
+
});
|
|
3348
|
+
|
|
3091
3349
|
// ── stages ──────────────────────────────────────────────────────────
|
|
3092
3350
|
const stagesCmd = program.command('stages').description('Manage pipeline stages (admin)');
|
|
3093
3351
|
|
|
@@ -3427,6 +3685,139 @@ sequencesCmd
|
|
|
3427
3685
|
} catch (err) { handleError(err); }
|
|
3428
3686
|
});
|
|
3429
3687
|
|
|
3688
|
+
// Sprint 54 T3: sequences stats <sequenceId>
|
|
3689
|
+
sequencesCmd
|
|
3690
|
+
.command('stats <sequenceId>')
|
|
3691
|
+
.description('Get enrollment stats for a sequence (Sprint 54 T3)')
|
|
3692
|
+
.action(async (sequenceId) => {
|
|
3693
|
+
const globalOpts = program.opts();
|
|
3694
|
+
const client = getClient(globalOpts);
|
|
3695
|
+
try {
|
|
3696
|
+
const res = await client.get(`/sequences/${sequenceId}/stats`);
|
|
3697
|
+
const d = res.data.data || {};
|
|
3698
|
+
console.log(`\nSequence: ${d.sequence_name} [${d.sequence_id}]`);
|
|
3699
|
+
console.log(`Active: ${d.active ? 'yes' : 'no'} Steps: ${d.step_count}`);
|
|
3700
|
+
console.log(`Total enrolled: ${d.total_enrolled}`);
|
|
3701
|
+
if (d.by_status) {
|
|
3702
|
+
console.log(` active: ${d.by_status.active}`);
|
|
3703
|
+
console.log(` completed: ${d.by_status.completed}`);
|
|
3704
|
+
console.log(` unsubscribed: ${d.by_status.unsubscribed}`);
|
|
3705
|
+
console.log(` paused: ${d.by_status.paused}`);
|
|
3706
|
+
console.log(` failed: ${d.by_status.failed}`);
|
|
3707
|
+
}
|
|
3708
|
+
if (d.first_enrollment) console.log(`First enrollment: ${new Date(d.first_enrollment).toLocaleString()}`);
|
|
3709
|
+
if (d.last_enrollment) console.log(`Last enrollment: ${new Date(d.last_enrollment).toLocaleString()}`);
|
|
3710
|
+
} catch (err) { handleError(err); }
|
|
3711
|
+
});
|
|
3712
|
+
|
|
3713
|
+
// Sprint 54 T3: leads sequences <lead-id>
|
|
3714
|
+
leadsCmd
|
|
3715
|
+
.command('sequences <lead-id>')
|
|
3716
|
+
.description('List sequences a lead is enrolled in (Sprint 54 T3)')
|
|
3717
|
+
.action(async (leadId) => {
|
|
3718
|
+
const globalOpts = program.opts();
|
|
3719
|
+
const client = getClient(globalOpts);
|
|
3720
|
+
try {
|
|
3721
|
+
const res = await client.get(`/leads/${leadId}/sequences`);
|
|
3722
|
+
const enrollments = res.data.data || res.data || [];
|
|
3723
|
+
if (enrollments.length === 0) { console.log('Lead is not enrolled in any sequences.'); return; }
|
|
3724
|
+
enrollments.forEach(e => {
|
|
3725
|
+
const step = e.current_step || 'N/A';
|
|
3726
|
+
const next = e.next_send_at ? new Date(e.next_send_at).toLocaleString() : 'N/A';
|
|
3727
|
+
console.log(`[${e.sequence_id}] ${e.sequence_name} — status: ${e.status}, step: ${step}, next: ${next}`);
|
|
3728
|
+
});
|
|
3729
|
+
} catch (err) { handleError(err); }
|
|
3730
|
+
});
|
|
3731
|
+
|
|
3732
|
+
// ============================================================
|
|
3733
|
+
// FORMS COMMANDS — Sprint 54 T2
|
|
3734
|
+
// ============================================================
|
|
3735
|
+
const formsCmd = program.command('forms').description('Manage lead capture forms (Sprint 54 T2)');
|
|
3736
|
+
|
|
3737
|
+
formsCmd
|
|
3738
|
+
.command('list')
|
|
3739
|
+
.description('List all forms in the workspace')
|
|
3740
|
+
.action(async () => {
|
|
3741
|
+
const globalOpts = program.opts();
|
|
3742
|
+
const client = getClient(globalOpts);
|
|
3743
|
+
try {
|
|
3744
|
+
const res = await client.get('/apikey/forms');
|
|
3745
|
+
const forms = res.data.data || res.data || [];
|
|
3746
|
+
if (forms.length === 0) { console.log('No forms found.'); return; }
|
|
3747
|
+
forms.forEach(f => {
|
|
3748
|
+
console.log(`[${f.id}] ${f.name} — submissions: ${f.submission_count || 0} — created: ${new Date(f.created_at).toLocaleDateString()}`);
|
|
3749
|
+
});
|
|
3750
|
+
} catch (err) { handleError(err); }
|
|
3751
|
+
});
|
|
3752
|
+
|
|
3753
|
+
formsCmd
|
|
3754
|
+
.command('get <form-id>')
|
|
3755
|
+
.description('Get form details by ID')
|
|
3756
|
+
.action(async (formId) => {
|
|
3757
|
+
const globalOpts = program.opts();
|
|
3758
|
+
const client = getClient(globalOpts);
|
|
3759
|
+
try {
|
|
3760
|
+
const res = await client.get(`/apikey/forms/${formId}`);
|
|
3761
|
+
printJSON(res.data);
|
|
3762
|
+
} catch (err) { handleError(err); }
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
formsCmd
|
|
3766
|
+
.command('create')
|
|
3767
|
+
.description('Create a new lead capture form')
|
|
3768
|
+
.requiredOption('--name <name>', 'Form name')
|
|
3769
|
+
.option('--success-message <msg>', 'Message shown after submission', 'Thank you! We will be in touch.')
|
|
3770
|
+
.option('--redirect-url <url>', 'Redirect URL after submission')
|
|
3771
|
+
.option('--fields <json>', 'JSON array of field definitions (e.g. \'[{"key":"email","label":"Email","type":"email","required":true}]\')')
|
|
3772
|
+
.action(async (opts) => {
|
|
3773
|
+
const globalOpts = program.opts();
|
|
3774
|
+
const client = getClient(globalOpts);
|
|
3775
|
+
let fields = [];
|
|
3776
|
+
if (opts.fields) {
|
|
3777
|
+
try { fields = JSON.parse(opts.fields); } catch (e) { console.error('Error: --fields must be valid JSON array'); process.exit(1); }
|
|
3778
|
+
}
|
|
3779
|
+
try {
|
|
3780
|
+
const body = { name: opts.name, fields, success_message: opts.successMessage };
|
|
3781
|
+
if (opts.redirectUrl) body.redirect_url = opts.redirectUrl;
|
|
3782
|
+
const res = await client.post('/apikey/forms', body);
|
|
3783
|
+
console.log(`Form created: ${res.data.data?.id || res.data?.id}`);
|
|
3784
|
+
printJSON(res.data);
|
|
3785
|
+
} catch (err) { handleError(err); }
|
|
3786
|
+
});
|
|
3787
|
+
|
|
3788
|
+
formsCmd
|
|
3789
|
+
.command('submissions <form-id>')
|
|
3790
|
+
.description('List submissions for a form')
|
|
3791
|
+
.option('--limit <n>', 'Max results', '50')
|
|
3792
|
+
.option('--offset <n>', 'Pagination offset', '0')
|
|
3793
|
+
.action(async (formId, opts) => {
|
|
3794
|
+
const globalOpts = program.opts();
|
|
3795
|
+
const client = getClient(globalOpts);
|
|
3796
|
+
try {
|
|
3797
|
+
const res = await client.get(`/apikey/forms/${formId}/submissions`, { params: { limit: opts.limit, offset: opts.offset } });
|
|
3798
|
+
const d = res.data.data || {};
|
|
3799
|
+
const subs = d.data || [];
|
|
3800
|
+
if (subs.length === 0) { console.log('No submissions found.'); return; }
|
|
3801
|
+
console.log(`Total: ${d.total || subs.length}`);
|
|
3802
|
+
subs.forEach(s => {
|
|
3803
|
+
const when = s.submitted_at ? new Date(s.submitted_at).toLocaleString() : 'N/A';
|
|
3804
|
+
console.log(`[${s.id}] ${s.lead_name || 'Unknown'} <${s.lead_email || ''}> — ${when}`);
|
|
3805
|
+
});
|
|
3806
|
+
} catch (err) { handleError(err); }
|
|
3807
|
+
});
|
|
3808
|
+
|
|
3809
|
+
formsCmd
|
|
3810
|
+
.command('delete <form-id>')
|
|
3811
|
+
.description('Delete a form by ID')
|
|
3812
|
+
.action(async (formId) => {
|
|
3813
|
+
const globalOpts = program.opts();
|
|
3814
|
+
const client = getClient(globalOpts);
|
|
3815
|
+
try {
|
|
3816
|
+
await client.delete(`/apikey/forms/${formId}`);
|
|
3817
|
+
console.log(`Form ${formId} deleted.`);
|
|
3818
|
+
} catch (err) { handleError(err); }
|
|
3819
|
+
});
|
|
3820
|
+
|
|
3430
3821
|
// ============================================================
|
|
3431
3822
|
// GDPR COMMANDS — Sprint 50 T2
|
|
3432
3823
|
// ============================================================
|