@startanaicompany/crm 2.14.0 → 2.16.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.
Files changed (2) hide show
  1. package/index.js +302 -35
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -580,21 +580,39 @@ leadsCmd
580
580
  });
581
581
 
582
582
  leadsCmd
583
- .command('score')
584
- .description('Set lead score (0-100). Conventions: 0-19 Cold, 20-39 Warm, 40-59 Developing, 60-79 Qualified, 80-100 Hot')
585
- .requiredOption('--id <uuid>', 'Lead UUID')
586
- .requiredOption('--score <n>', 'Score value (0-100 integer)')
587
- .option('--reason <text>', 'Score reason/explanation')
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
- const res = await client.patch(`/leads/${opts.id}/score`, body);
597
- printJSON(res.data);
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
+ const history = histRes.data.data || [];
607
+ const label = lead.score >= 80 ? 'Hot' : lead.score >= 60 ? 'Qualified' : lead.score >= 40 ? 'Developing' : lead.score >= 20 ? 'Warm' : 'Cold';
608
+ console.log(`\nLead: ${lead.name} <${lead.email}>`);
609
+ console.log(`Current Score: ${lead.score ?? 'N/A'} (${label})`);
610
+ if (lead.score_reason) console.log(`Reason: ${lead.score_reason}`);
611
+ if (history.length > 0) {
612
+ console.log(`\nScore History (${history.length} entries):`);
613
+ history.slice(0, 10).forEach(h => console.log(` ${new Date(h.changed_at).toLocaleString()} — ${h.previous_score ?? '?'} → ${h.new_score} (${h.changed_by || 'system'})`));
614
+ }
615
+ }
598
616
  } catch (err) {
599
617
  handleError(err);
600
618
  }
@@ -708,13 +726,16 @@ leadsCmd
708
726
  });
709
727
 
710
728
  leadsCmd
711
- .command('assign <id> <agent>')
712
- .description('Assign a lead to an agent (fires lead.assigned webhook) — S18-2')
713
- .action(async (id, agent) => {
729
+ .command('assign <id> [agent]')
730
+ .description('Assign a lead to an agent (fires lead.assigned webhook) — S18-2. Use positional [agent] or --to <agent>.')
731
+ .option('--to <agent>', 'Agent/user to assign to (alias for positional agent arg)')
732
+ .action(async (id, agent, opts) => {
714
733
  const globalOpts = program.opts();
715
734
  const client = getClient(globalOpts);
735
+ const assignTo = agent || opts.to;
736
+ if (!assignTo) { console.error('Error: specify agent as positional arg or --to <agent>'); process.exit(1); }
716
737
  try {
717
- const res = await client.patch(`/leads/${id}/assign`, { assigned_to: agent });
738
+ const res = await client.patch(`/leads/${id}/assign`, { assigned_to: assignTo });
718
739
  console.log(`Lead assigned to: ${res.data.data.assigned_to}`);
719
740
  printJSON(res.data);
720
741
  } catch (err) {
@@ -770,27 +791,67 @@ leadsCmd
770
791
  }
771
792
  });
772
793
 
773
- // leads search <term> S19-1
794
+ // Sprint 53 T1: leads stageget or set pipeline stage
774
795
  leadsCmd
775
- .command('search <term>')
776
- .description('Search leads by name, email, company, or notes')
777
- .action(async (term) => {
796
+ .command('stage <lead-id>')
797
+ .description('Get current pipeline stage or move lead to a new stage (Sprint 53 T1)')
798
+ .option('--to <stage>', 'Move lead to this pipeline stage name')
799
+ .action(async (leadId, opts) => {
778
800
  const globalOpts = program.opts();
779
801
  const client = getClient(globalOpts);
780
802
  try {
781
- const res = await client.get('/leads', { params: { q: term, per_page: 50 } });
803
+ if (opts.to) {
804
+ // SET mode: PATCH /leads/:id with pipeline_stage
805
+ const res = await client.patch(`/leads/${leadId}`, { pipeline_stage: opts.to });
806
+ const d = res.data.data;
807
+ console.log(`Stage set to: ${d.pipeline_stage}`);
808
+ printJSON(res.data);
809
+ } else {
810
+ // GET mode: show current stage
811
+ const leadRes = await client.get(`/leads/${leadId}`);
812
+ const lead = leadRes.data.data;
813
+ console.log(`\nLead: ${lead.name} <${lead.email}>`);
814
+ console.log(`Current Stage: ${lead.pipeline_stage || 'N/A'}`);
815
+ if (lead.pipeline_id) {
816
+ try {
817
+ const pipeRes = await client.get(`/pipelines/${lead.pipeline_id}/stages`);
818
+ const stages = pipeRes.data.data || [];
819
+ if (stages.length > 0) {
820
+ console.log(`Available stages: ${stages.map(s => s.name).join(' → ')}`);
821
+ }
822
+ } catch (_) { /* pipeline stages not critical */ }
823
+ }
824
+ }
825
+ } catch (err) {
826
+ handleError(err);
827
+ }
828
+ });
829
+
830
+ // leads search <term> — S19-1 / Sprint 53 T3: added --q flag alias
831
+ leadsCmd
832
+ .command('search [term]')
833
+ .description('Search leads by name, email, company, or notes. Use positional term or --q flag.')
834
+ .option('--q <keyword>', 'Search keyword (alias for positional term)')
835
+ .option('--fields <fields>', 'Comma-separated fields to display (e.g. name,email,company)')
836
+ .option('--limit <n>', 'Max results', '50')
837
+ .action(async (term, opts) => {
838
+ const globalOpts = program.opts();
839
+ const client = getClient(globalOpts);
840
+ const query = term || opts.q;
841
+ if (!query) { console.error('Error: provide a search term (positional or --q)'); process.exit(1); }
842
+ try {
843
+ const res = await client.get('/leads', { params: { q: query, per_page: parseInt(opts.limit) } });
782
844
  const leads = res.data.data || [];
783
845
  if (leads.length === 0) {
784
- console.log('No leads found matching: ' + term);
846
+ console.log('No leads found matching: ' + query);
785
847
  return;
786
848
  }
787
- const rows = leads.map(l => ({
788
- id: l.id,
789
- name: l.name,
790
- email: l.email,
791
- company: l.company || '—',
792
- status: l.status
793
- }));
849
+ const fields = opts.fields ? opts.fields.split(',').map(f => f.trim()) : ['id', 'name', 'email', 'company', 'status'];
850
+ const rows = leads.map(l => {
851
+ const row = {};
852
+ fields.forEach(f => { row[f] = l[f] ?? '—'; });
853
+ return row;
854
+ });
794
855
  console.table(rows);
795
856
  } catch (err) {
796
857
  handleError(err);
@@ -1120,6 +1181,68 @@ leadsCmd
1120
1181
  } catch (err) { handleError(err); }
1121
1182
  });
1122
1183
 
1184
+ // Sprint 53 T3: leads due — follow-up due/overdue list
1185
+ leadsCmd
1186
+ .command('due')
1187
+ .description('List leads with upcoming or overdue follow-ups (Sprint 53 T3)')
1188
+ .option('--today', 'Leads with follow-up scheduled for today')
1189
+ .option('--overdue', 'Leads with overdue follow-ups (follow_up_at < now)')
1190
+ .option('--assigned-to-me', 'Filter to leads assigned to the current API key name')
1191
+ .option('--limit <n>', 'Max results', '50')
1192
+ .action(async (opts) => {
1193
+ const globalOpts = program.opts();
1194
+ const client = getClient(globalOpts);
1195
+ try {
1196
+ const params = { per_page: parseInt(opts.limit) };
1197
+ if (opts.overdue) params.follow_up_overdue = 'true';
1198
+ if (opts.today) params.follow_up_today = 'true';
1199
+ if (!opts.overdue && !opts.today) {
1200
+ // default: both overdue + today
1201
+ params.follow_up_overdue = 'true';
1202
+ }
1203
+ const res = await client.get('/leads', { params });
1204
+ const leads = res.data.data || [];
1205
+ if (leads.length === 0) { console.log('No leads with due/overdue follow-ups.'); return; }
1206
+ leads.forEach(l => {
1207
+ const fu = l.follow_up_at ? new Date(l.follow_up_at).toLocaleString() : 'N/A';
1208
+ const overdue = l.follow_up_at && new Date(l.follow_up_at) < new Date() ? ' ⚠️ OVERDUE' : '';
1209
+ console.log(`[${l.id}] ${l.name} <${l.email}> — follow_up: ${fu}${overdue} (${l.assigned_to || 'unassigned'})`);
1210
+ });
1211
+ } catch (err) { handleError(err); }
1212
+ });
1213
+
1214
+ // Sprint 52 T2: leads escalate + leads unassign
1215
+ leadsCmd
1216
+ .command('escalate <lead-id>')
1217
+ .description('Manually escalate a lead — sets escalated=true and fires lead.escalated webhook (Sprint 52 T2)')
1218
+ .option('--reason <text>', 'Reason for escalation')
1219
+ .option('--to <agent>', 'Reassign to this agent/user on escalation')
1220
+ .action(async (leadId, opts) => {
1221
+ const globalOpts = program.opts();
1222
+ const client = getClient(globalOpts);
1223
+ try {
1224
+ const body = {};
1225
+ if (opts.reason) body.reason = opts.reason;
1226
+ if (opts.to) body.to = opts.to;
1227
+ const res = await client.post(`/leads/${leadId}/escalate`, body);
1228
+ console.log(`Lead ${leadId} escalated.`);
1229
+ printJSON(res.data);
1230
+ } catch (err) { handleError(err); }
1231
+ });
1232
+
1233
+ leadsCmd
1234
+ .command('unassign <lead-id>')
1235
+ .description('Remove assignment from a lead (sets assigned_to to null) (Sprint 52 T2)')
1236
+ .action(async (leadId) => {
1237
+ const globalOpts = program.opts();
1238
+ const client = getClient(globalOpts);
1239
+ try {
1240
+ const res = await client.patch(`/leads/${leadId}`, { assigned_to: null });
1241
+ console.log(`Lead ${leadId} unassigned.`);
1242
+ printJSON(res.data);
1243
+ } catch (err) { handleError(err); }
1244
+ });
1245
+
1123
1246
  // Sprint 51 T3: leads export / leads import
1124
1247
  leadsCmd
1125
1248
  .command('export')
@@ -1187,11 +1310,14 @@ const contactsCmd = program
1187
1310
  contactsCmd
1188
1311
  .command('create')
1189
1312
  .description('Create a new contact')
1190
- .requiredOption('--first-name <name>', 'First name')
1313
+ .option('--name <name>', 'Full name (alias for --first-name)')
1314
+ .option('--first-name <name>', 'First name')
1191
1315
  .option('--last-name <name>', 'Last name')
1192
- .option('--email <email>', 'Email address (unique)')
1316
+ .requiredOption('--email <email>', 'Email address (unique)')
1193
1317
  .option('--phone <phone>', 'Phone number')
1194
- .option('--company <company>', 'Company name')
1318
+ .option('--company <company>', 'Company name (text)')
1319
+ .option('--company-id <uuid>', 'Company UUID (links to companies table)')
1320
+ .option('--lead-id <uuid>', 'Lead UUID to link to this contact on creation')
1195
1321
  .option('--title <title>', 'Job title')
1196
1322
  .option('--tags <tags>', 'Comma-separated tags')
1197
1323
  .option('--do-not-contact', 'Mark as do not contact')
@@ -1203,11 +1329,14 @@ contactsCmd
1203
1329
  const client = getClient(globalOpts, agentName);
1204
1330
  try {
1205
1331
  const body = {
1206
- first_name: opts.firstName,
1332
+ name: opts.name || opts.firstName,
1333
+ first_name: opts.firstName || opts.name,
1207
1334
  last_name: opts.lastName,
1208
1335
  email: opts.email,
1209
1336
  phone: opts.phone,
1210
1337
  company: opts.company,
1338
+ company_id: opts.companyId,
1339
+ lead_id: opts.leadId,
1211
1340
  title: opts.title,
1212
1341
  notes: opts.notes,
1213
1342
  do_not_contact: opts.doNotContact === true,
@@ -1224,8 +1353,10 @@ contactsCmd
1224
1353
  .command('list')
1225
1354
  .description('List contacts')
1226
1355
  .option('--email <email>', 'Filter by email (partial match)')
1227
- .option('--company <company>', 'Filter by company (partial match)')
1356
+ .option('--company <company>', 'Filter by company name (partial match)')
1357
+ .option('--company-id <uuid>', 'Filter by company UUID')
1228
1358
  .option('--tag <tag>', 'Filter by tag')
1359
+ .option('--limit <n>', 'Max results per page (default 20)')
1229
1360
  .option('--page <n>', 'Page number', '1')
1230
1361
  .option('--per-page <n>', 'Results per page', '20')
1231
1362
  .action(async (opts) => {
@@ -1233,8 +1364,10 @@ contactsCmd
1233
1364
  const client = getClient(globalOpts);
1234
1365
  try {
1235
1366
  const params = { page: opts.page, per_page: opts.perPage };
1367
+ if (opts.limit) params.limit = opts.limit;
1236
1368
  if (opts.email) params.email = opts.email;
1237
1369
  if (opts.company) params.company = opts.company;
1370
+ if (opts.companyId) params.company_id = opts.companyId;
1238
1371
  if (opts.tag) params.tag = opts.tag;
1239
1372
  const res = await client.get('/contacts', { params });
1240
1373
  printJSON(res.data);
@@ -1260,11 +1393,13 @@ contactsCmd
1260
1393
  contactsCmd
1261
1394
  .command('update <id>')
1262
1395
  .description('Update a contact')
1396
+ .option('--name <name>', 'Full name (alias for --first-name)')
1263
1397
  .option('--first-name <name>', 'First name')
1264
1398
  .option('--last-name <name>', 'Last name')
1265
1399
  .option('--email <email>', 'Email address')
1266
1400
  .option('--phone <phone>', 'Phone number')
1267
- .option('--company <company>', 'Company name')
1401
+ .option('--company <company>', 'Company name (text)')
1402
+ .option('--company-id <uuid>', 'Company UUID (links to companies table)')
1268
1403
  .option('--title <title>', 'Job title')
1269
1404
  .option('--role <role>', 'Role: champion|economic_buyer|technical_buyer|gatekeeper|influencer|end_user')
1270
1405
  .option('--tags <tags>', 'Comma-separated tags')
@@ -1274,11 +1409,13 @@ contactsCmd
1274
1409
  const client = getClient(globalOpts);
1275
1410
  try {
1276
1411
  const body = {};
1412
+ if (opts.name !== undefined) body.name = opts.name;
1277
1413
  if (opts.firstName !== undefined) body.first_name = opts.firstName;
1278
1414
  if (opts.lastName !== undefined) body.last_name = opts.lastName;
1279
1415
  if (opts.email !== undefined) body.email = opts.email;
1280
1416
  if (opts.phone !== undefined) body.phone = opts.phone;
1281
1417
  if (opts.company !== undefined) body.company = opts.company;
1418
+ if (opts.companyId !== undefined) body.company_id = opts.companyId;
1282
1419
  if (opts.title !== undefined) body.title = opts.title;
1283
1420
  if (opts.role !== undefined) body.role = opts.role;
1284
1421
  if (opts.tags !== undefined) body.tags = opts.tags.split(',').map(t => t.trim());
@@ -3042,6 +3179,59 @@ webhooksCmd
3042
3179
  }
3043
3180
  });
3044
3181
 
3182
+ // Sprint 53 T2: webhooks test / logs / retry
3183
+ webhooksCmd
3184
+ .command('test <webhook-id>')
3185
+ .description('Send a test payload to a webhook and check delivery (Sprint 53 T2)')
3186
+ .action(async (webhookId) => {
3187
+ const globalOpts = program.opts();
3188
+ const client = getClient(globalOpts);
3189
+ try {
3190
+ const res = await client.post(`/webhooks/${webhookId}/test`);
3191
+ const d = res.data.data;
3192
+ console.log(`Test queued — event: ${d.event}, webhook_id: ${d.webhook_id}`);
3193
+ printJSON(res.data);
3194
+ } catch (err) { handleError(err); }
3195
+ });
3196
+
3197
+ webhooksCmd
3198
+ .command('logs <webhook-id>')
3199
+ .description('View webhook delivery history (Sprint 53 T2)')
3200
+ .option('--limit <n>', 'Max results (default 20)', '20')
3201
+ .option('--status <status>', 'Filter by status: success or failed')
3202
+ .action(async (webhookId, opts) => {
3203
+ const globalOpts = program.opts();
3204
+ const client = getClient(globalOpts);
3205
+ try {
3206
+ const params = { limit: parseInt(opts.limit) };
3207
+ if (opts.status) params.status = opts.status;
3208
+ const res = await client.get(`/webhooks/${webhookId}/deliveries`, { params });
3209
+ const deliveries = res.data.data || [];
3210
+ if (deliveries.length === 0) { console.log('No delivery logs found.'); return; }
3211
+ deliveries.forEach(d => {
3212
+ const icon = d.success ? '✅' : '❌';
3213
+ const retry = d.next_retry_at ? ` | retry: ${new Date(d.next_retry_at).toLocaleString()}` : '';
3214
+ console.log(`${icon} [${d.id}] ${d.event} — HTTP ${d.status_code || '?'} at ${new Date(d.attempted_at).toLocaleString()}${retry}`);
3215
+ if (!d.success && d.error_message) console.log(` Error: ${d.error_message}`);
3216
+ });
3217
+ const p = res.data.pagination;
3218
+ if (p) console.log(`\nShowing ${deliveries.length} of ${p.total} total deliveries.`);
3219
+ } catch (err) { handleError(err); }
3220
+ });
3221
+
3222
+ webhooksCmd
3223
+ .command('retry <webhook-id> <delivery-id>')
3224
+ .description('Retry a failed webhook delivery (Sprint 53 T2)')
3225
+ .action(async (webhookId, deliveryId) => {
3226
+ const globalOpts = program.opts();
3227
+ const client = getClient(globalOpts);
3228
+ try {
3229
+ const res = await client.post(`/webhooks/${webhookId}/deliveries/${deliveryId}/retry`);
3230
+ console.log(`Retry queued for delivery ${deliveryId}`);
3231
+ printJSON(res.data);
3232
+ } catch (err) { handleError(err); }
3233
+ });
3234
+
3045
3235
  // ── stages ──────────────────────────────────────────────────────────
3046
3236
  const stagesCmd = program.command('stages').description('Manage pipeline stages (admin)');
3047
3237
 
@@ -3442,4 +3632,81 @@ gdprCmd
3442
3632
  }
3443
3633
  });
3444
3634
 
3635
+ // ============================================================
3636
+ // REPORTS COMMANDS — Sprint 52 T3
3637
+ // ============================================================
3638
+ const reportsCmd = program.command('reports').description('Manage and run saved CRM reports (Sprint 52 T3)');
3639
+
3640
+ reportsCmd
3641
+ .command('list')
3642
+ .description('List all saved reports')
3643
+ .action(async () => {
3644
+ const globalOpts = program.opts();
3645
+ const client = getClient(globalOpts);
3646
+ try {
3647
+ const res = await client.get('/reports');
3648
+ const reports = res.data.data || res.data;
3649
+ if (!reports || reports.length === 0) { console.log('No reports found.'); return; }
3650
+ reports.forEach(r => console.log(`[${r.id}] ${r.name} — type: ${r.dimension}/${r.metric}, period: ${r.period}`));
3651
+ } catch (err) { handleError(err); }
3652
+ });
3653
+
3654
+ reportsCmd
3655
+ .command('create')
3656
+ .description('Create and save a report definition')
3657
+ .requiredOption('--name <name>', 'Report name')
3658
+ .option('--type <type>', 'Report type: leads (count by stage) | pipeline (deal value by pipeline) | revenue (deal value by stage)', 'leads')
3659
+ .option('--period <period>', 'Time period: 7d | 30d | 90d | 365d | all', '30d')
3660
+ .option('--filter-status <status>', 'Filter leads by status')
3661
+ .option('--filter-stage <stage>', 'Filter leads by pipeline stage')
3662
+ .option('--chart-type <type>', 'Chart type: bar | line | pie | table', 'bar')
3663
+ .action(async (opts) => {
3664
+ const globalOpts = program.opts();
3665
+ const client = getClient(globalOpts);
3666
+ try {
3667
+ const body = {
3668
+ name: opts.name,
3669
+ type: opts.type,
3670
+ period: opts.period,
3671
+ chart_type: opts.chartType,
3672
+ };
3673
+ if (opts.filterStatus) body.filter_status = opts.filterStatus;
3674
+ if (opts.filterStage) body.filter_stage = opts.filterStage;
3675
+ const res = await client.post('/reports', body);
3676
+ const r = res.data.data || res.data;
3677
+ console.log(`Report created: [${r.id}] ${r.name}`);
3678
+ printJSON(res.data);
3679
+ } catch (err) { handleError(err); }
3680
+ });
3681
+
3682
+ reportsCmd
3683
+ .command('run <report-id>')
3684
+ .description('Run a saved report and display or save results')
3685
+ .option('--output <file>', 'Save results to this JSON file')
3686
+ .action(async (reportId, opts) => {
3687
+ const globalOpts = program.opts();
3688
+ const client = getClient(globalOpts);
3689
+ try {
3690
+ const res = await client.get(`/reports/${reportId}/run`);
3691
+ if (opts.output) {
3692
+ fs.writeFileSync(opts.output, JSON.stringify(res.data, null, 2));
3693
+ console.log(`Report results saved to ${opts.output}`);
3694
+ } else {
3695
+ printJSON(res.data);
3696
+ }
3697
+ } catch (err) { handleError(err); }
3698
+ });
3699
+
3700
+ reportsCmd
3701
+ .command('delete <report-id>')
3702
+ .description('Delete a saved report')
3703
+ .action(async (reportId) => {
3704
+ const globalOpts = program.opts();
3705
+ const client = getClient(globalOpts);
3706
+ try {
3707
+ await client.delete(`/reports/${reportId}`);
3708
+ console.log(`Report ${reportId} deleted.`);
3709
+ } catch (err) { handleError(err); }
3710
+ });
3711
+
3445
3712
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
4
4
  "description": "AI-first CRM CLI — manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {