@startanaicompany/crm 2.15.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 +173 -29
  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
795
+ leadsCmd
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) => {
800
+ const globalOpts = program.opts();
801
+ const client = getClient(globalOpts);
802
+ try {
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
774
831
  leadsCmd
775
- .command('search <term>')
776
- .description('Search leads by name, email, company, or notes')
777
- .action(async (term) => {
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) => {
778
838
  const globalOpts = program.opts();
779
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); }
780
842
  try {
781
- const res = await client.get('/leads', { params: { q: term, per_page: 50 } });
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,36 @@ 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
+
1123
1214
  // Sprint 52 T2: leads escalate + leads unassign
1124
1215
  leadsCmd
1125
1216
  .command('escalate <lead-id>')
@@ -3088,6 +3179,59 @@ webhooksCmd
3088
3179
  }
3089
3180
  });
3090
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
+
3091
3235
  // ── stages ──────────────────────────────────────────────────────────
3092
3236
  const stagesCmd = program.command('stages').description('Manage pipeline stages (admin)');
3093
3237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.15.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": {