@startanaicompany/crm 2.10.0 → 2.11.1

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 +703 -6
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -212,6 +212,36 @@ keysCmd
212
212
  }
213
213
  });
214
214
 
215
+ // S20-1: set per-key rate limit (requires admin JWT auth)
216
+ keysCmd
217
+ .command('set-limit <key-id> <limit>')
218
+ .description('Set rate limit (req/min) for an API key — requires admin credentials')
219
+ .option('--email <email>', 'Admin email (or set SAAC_CRM_ADMIN_EMAIL env)')
220
+ .option('--password <password>', 'Admin password (or set SAAC_CRM_ADMIN_PASSWORD env)')
221
+ .action(async (keyId, limitStr, opts) => {
222
+ const globalOpts = program.opts();
223
+ const apiUrl = resolveApiUrl(globalOpts.url);
224
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
225
+ const limit = parseInt(limitStr, 10);
226
+ if (isNaN(limit) || limit < 1 || limit > 10000) {
227
+ console.error('Error: limit must be an integer between 1 and 10000');
228
+ process.exit(1);
229
+ }
230
+ const cfg = loadConfig();
231
+ const email = opts.email || process.env.SAAC_CRM_ADMIN_EMAIL || cfg.admin_email;
232
+ const password = opts.password || process.env.SAAC_CRM_ADMIN_PASSWORD || await promptSecret('Admin password: ');
233
+ try {
234
+ const base = apiUrl.replace(/\/$/, '');
235
+ const loginRes = await axios.post(`${base}/api/v1/admin/login`, { email, password });
236
+ const token = loginRes.data.data?.token;
237
+ const res = await axios.patch(`${base}/api/v1/admin/keys/${keyId}`, { rate_limit: limit }, {
238
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
239
+ });
240
+ console.log(`Rate limit updated to ${limit} req/min.`);
241
+ printJSON(res.data.data);
242
+ } catch (err) { handleError(err); }
243
+ });
244
+
215
245
  // ============================================================
216
246
  // REGISTER — first-time workspace + admin key setup
217
247
  // NOTE: For agent self-service (zero-auth), use: saac_crm keys create --name <name>
@@ -578,6 +608,10 @@ leadsCmd
578
608
  .option('--status <status>', 'New status')
579
609
  .option('--source <source>', 'New source')
580
610
  .option('--deal-value <amount>', 'Deal value (numeric)')
611
+ .option('--close-probability <n>', 'Close probability 0-100')
612
+ .option('--close-reason <reason>', 'Close reason (price|timing|competition|no_budget|no_decision|fit|other)')
613
+ .option('--expected-close-date <date>', 'Expected close date (YYYY-MM-DD)')
614
+ .option('--score <n>', 'Lead score 0-100 (calls PATCH /leads/:id/score)')
581
615
  .option('--notes <notes>', 'New notes')
582
616
  .option('--assigned-to <assignedTo>', 'New assigned-to')
583
617
  .option('--tag <tag>', 'Replace ALL tags (repeatable). Use --add-tag / --remove-tag for surgical edits.', (v, prev) => prev.concat([v]), [])
@@ -585,9 +619,11 @@ leadsCmd
585
619
  .option('--remove-tag <tag>', 'Remove a single tag (repeatable)', (v, prev) => prev.concat([v]), [])
586
620
  .option('--external-id <externalId>', 'New external ID')
587
621
  .option('--version <version>', 'Optimistic lock version')
622
+ .option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
588
623
  .action(async (id, opts) => {
589
624
  const globalOpts = program.opts();
590
- const client = getClient(globalOpts);
625
+ const agentName = resolveAgentName(opts.fromAgentName);
626
+ const client = getClient(globalOpts, agentName);
591
627
  try {
592
628
  // Surgical tag ops: call PATCH /leads/:id/tags first if add/remove specified
593
629
  if (opts.addTag.length > 0 || opts.removeTag.length > 0) {
@@ -598,14 +634,19 @@ leadsCmd
598
634
  // If no other fields to update, return tag result
599
635
  const hasOtherFields = opts.name || opts.email || opts.phone !== undefined ||
600
636
  opts.company !== undefined || opts.status || opts.source ||
601
- opts.dealValue !== undefined || opts.notes !== undefined ||
602
- opts.assignedTo !== undefined || opts.tag.length > 0 ||
603
- opts.externalId !== undefined;
637
+ opts.dealValue !== undefined || opts.closeProbability !== undefined ||
638
+ opts.closeReason !== undefined || opts.expectedCloseDate !== undefined ||
639
+ opts.notes !== undefined || opts.assignedTo !== undefined ||
640
+ opts.tag.length > 0 || opts.externalId !== undefined || opts.score !== undefined;
604
641
  if (!hasOtherFields) {
605
642
  printJSON(tagRes.data);
606
643
  return;
607
644
  }
608
645
  }
646
+ // Score update: call separate PATCH /leads/:id/score endpoint
647
+ if (opts.score !== undefined) {
648
+ await client.patch(`/leads/${id}/score`, { score: parseInt(opts.score) });
649
+ }
609
650
  const body = {
610
651
  ...(opts.name && { name: opts.name }),
611
652
  ...(opts.email && { email: opts.email }),
@@ -614,14 +655,22 @@ leadsCmd
614
655
  ...(opts.status && { status: opts.status }),
615
656
  ...(opts.source && { source: opts.source }),
616
657
  ...(opts.dealValue !== undefined && { deal_value: parseFloat(opts.dealValue) }),
658
+ ...(opts.closeProbability !== undefined && { close_probability: parseInt(opts.closeProbability) }),
659
+ ...(opts.closeReason !== undefined && { close_reason: opts.closeReason }),
660
+ ...(opts.expectedCloseDate !== undefined && { expected_close_date: opts.expectedCloseDate }),
617
661
  ...(opts.notes !== undefined && { notes: opts.notes }),
618
662
  ...(opts.assignedTo !== undefined && { assigned_to: opts.assignedTo }),
619
663
  ...(opts.tag.length > 0 && { tags: opts.tag }),
620
664
  ...(opts.externalId !== undefined && { external_id: opts.externalId }),
621
665
  ...(opts.version !== undefined && { version: parseInt(opts.version) })
622
666
  };
623
- const res = await client.put(`/leads/${id}`, body);
624
- printJSON(res.data);
667
+ // Only call PUT if there are fields beyond score/tags to update
668
+ if (Object.keys(body).length > 0) {
669
+ const res = await client.put(`/leads/${id}`, body);
670
+ printJSON(res.data);
671
+ } else if (opts.score !== undefined) {
672
+ console.log(JSON.stringify({ success: true, data: { scored: true, score: parseInt(opts.score) } }, null, 2));
673
+ }
625
674
  } catch (err) {
626
675
  handleError(err);
627
676
  }
@@ -642,6 +691,21 @@ leadsCmd
642
691
  }
643
692
  });
644
693
 
694
+ leadsCmd
695
+ .command('assign <id> <agent>')
696
+ .description('Assign a lead to an agent (fires lead.assigned webhook) — S18-2')
697
+ .action(async (id, agent) => {
698
+ const globalOpts = program.opts();
699
+ const client = getClient(globalOpts);
700
+ try {
701
+ const res = await client.patch(`/leads/${id}/assign`, { assigned_to: agent });
702
+ console.log(`Lead assigned to: ${res.data.data.assigned_to}`);
703
+ printJSON(res.data);
704
+ } catch (err) {
705
+ handleError(err);
706
+ }
707
+ });
708
+
645
709
  leadsCmd
646
710
  .command('history <id>')
647
711
  .description('Show status change history for a lead')
@@ -656,6 +720,146 @@ leadsCmd
656
720
  }
657
721
  });
658
722
 
723
+ leadsCmd
724
+ .command('bulk-update')
725
+ .description('Bulk update lead status using filter criteria (S17-1). Up to 200 leads per call.')
726
+ .option('--filter-status <status>', 'Only update leads with this current status')
727
+ .option('--filter-source <source>', 'Only update leads with this source')
728
+ .option('--filter-assigned-to <agent>', 'Only update leads assigned to this agent')
729
+ .requiredOption('--status <status>', 'New status to set (new|contacted|qualified|unresponsive|converted|lost)')
730
+ .option('--close-reason <reason>', 'Close reason (required when --status lost): price|timing|competition|no_budget|no_decision|fit|other')
731
+ .option('--dry-run', 'Preview count without applying changes', false)
732
+ .action(async (opts) => {
733
+ const globalOpts = program.opts();
734
+ const client = getClient(globalOpts);
735
+ const filter = {};
736
+ if (opts.filterStatus) filter.status = opts.filterStatus;
737
+ if (opts.filterSource) filter.source = opts.filterSource;
738
+ if (opts.filterAssignedTo) filter.assigned_to = opts.filterAssignedTo;
739
+ const update = { status: opts.status };
740
+ if (opts.closeReason) update.close_reason = opts.closeReason;
741
+ try {
742
+ const res = await client.patch('/leads/bulk', { filter, update, dry_run: opts.dryRun });
743
+ if (opts.dryRun) {
744
+ console.log(`Dry run: ${res.data.data.would_update} lead(s) would be updated.`);
745
+ } else {
746
+ console.log(`Updated: ${res.data.data.updated} lead(s).`);
747
+ if (res.data.data.failed && res.data.data.failed.length > 0) {
748
+ console.log(`Failed: ${res.data.data.failed.length}`);
749
+ }
750
+ }
751
+ printJSON(res.data);
752
+ } catch (err) {
753
+ handleError(err);
754
+ }
755
+ });
756
+
757
+ // leads search <term> — S19-1
758
+ leadsCmd
759
+ .command('search <term>')
760
+ .description('Search leads by name, email, company, or notes')
761
+ .action(async (term) => {
762
+ const globalOpts = program.opts();
763
+ const client = getClient(globalOpts);
764
+ try {
765
+ const res = await client.get('/leads', { params: { q: term, per_page: 50 } });
766
+ const leads = res.data.data || [];
767
+ if (leads.length === 0) {
768
+ console.log('No leads found matching: ' + term);
769
+ return;
770
+ }
771
+ const rows = leads.map(l => ({
772
+ id: l.id,
773
+ name: l.name,
774
+ email: l.email,
775
+ company: l.company || '—',
776
+ status: l.status
777
+ }));
778
+ console.table(rows);
779
+ } catch (err) {
780
+ handleError(err);
781
+ }
782
+ });
783
+
784
+ // leads dedupe <id> — S19-3
785
+ leadsCmd
786
+ .command('dedupe <id>')
787
+ .description('Find duplicate leads for a given lead ID')
788
+ .action(async (id) => {
789
+ const globalOpts = program.opts();
790
+ const client = getClient(globalOpts);
791
+ try {
792
+ const leadRes = await client.get(`/leads/${id}`);
793
+ const lead = leadRes.data.data;
794
+ const body = {};
795
+ if (lead.email) body.email = lead.email;
796
+ if (lead.name) body.name = lead.name;
797
+ if (lead.company) body.company = lead.company;
798
+ const dedupeRes = await client.post('/leads/dedupe/check', body);
799
+ const duplicates = (dedupeRes.data.data && dedupeRes.data.data.duplicates) || [];
800
+ // Filter out the lead itself
801
+ const matches = duplicates.filter(d => d.id !== id);
802
+ if (matches.length === 0) {
803
+ console.log('No duplicate leads found for: ' + lead.name);
804
+ return;
805
+ }
806
+ console.log(`Potential duplicates for "${lead.name}" (${id}):`);
807
+ const rows = matches.map(d => ({
808
+ id: d.id,
809
+ name: d.name,
810
+ email: d.email,
811
+ company: d.company || '—',
812
+ status: d.status,
813
+ score: d.score
814
+ }));
815
+ console.table(rows);
816
+ } catch (err) {
817
+ handleError(err);
818
+ }
819
+ });
820
+
821
+
822
+ // S21-1: leads tag / leads untag
823
+ leadsCmd
824
+ .command('tag <lead-id> <tag>')
825
+ .description('Add a tag to a lead')
826
+ .action(async (leadId, tag, opts) => {
827
+ const globalOpts = program.opts();
828
+ const client = getClient(globalOpts);
829
+ try {
830
+ const res = await client.patch(`/leads/${leadId}/tags`, { add: [tag] });
831
+ printJSON(res.data);
832
+ } catch (err) { handleError(err); }
833
+ });
834
+
835
+ leadsCmd
836
+ .command('untag <lead-id> <tag>')
837
+ .description('Remove a tag from a lead')
838
+ .action(async (leadId, tag, opts) => {
839
+ const globalOpts = program.opts();
840
+ const client = getClient(globalOpts);
841
+ try {
842
+ const res = await client.patch(`/leads/${leadId}/tags`, { remove: [tag] });
843
+ printJSON(res.data);
844
+ } catch (err) { handleError(err); }
845
+ });
846
+
847
+ leadsCmd
848
+ .command('audit <id>')
849
+ .description('Show audit trail for a lead')
850
+ .action(async (id) => {
851
+ const globalOpts = program.opts();
852
+ const client = getClient(globalOpts);
853
+ try {
854
+ const res = await client.get(`/leads/${id}/audit`);
855
+ const events = res.data.data || [];
856
+ if (events.length === 0) { console.log('No audit events found.'); return; }
857
+ for (const e of events) {
858
+ console.log(`[${new Date(e.changed_at).toLocaleString()}] ${e.field}: ${e.old_value ?? '—'} → ${e.new_value ?? '—'} (by ${e.changed_by})`);
859
+ }
860
+ } catch (err) { handleError(err); }
861
+ });
862
+
659
863
  // ============================================================
660
864
  // USERS COMMANDS (requires admin scope key)
661
865
  // ============================================================
@@ -2493,4 +2697,497 @@ const supportCmd = program.command('support').description('Submit and manage bug
2493
2697
  });
2494
2698
  });
2495
2699
 
2700
+ // ============================================================
2701
+ // EMAIL SYNC CONFIG COMMANDS (Sprint 16)
2702
+ // ============================================================
2703
+
2704
+ const emailCmd = program.command('email-config').description('Persistent workspace email sync settings (Sprint 16)');
2705
+
2706
+ emailCmd
2707
+ .command('status')
2708
+ .description('Show current workspace email sync configuration and defaults')
2709
+ .action(async () => {
2710
+ const globalOpts = program.opts();
2711
+ const client = getClient(globalOpts);
2712
+ try {
2713
+ const res = await client.get('/emails/sync/status');
2714
+ const d = res.data.data;
2715
+ console.log('\n=== Email Sync Config ===');
2716
+ console.log(`Gmail configured: ${d.gmail_configured}`);
2717
+ console.log(`Workspace: ${d.workspace_slug}`);
2718
+ console.log(`auto_create_inbound_leads default: ${d.auto_create_inbound_leads}`);
2719
+ } catch (err) {
2720
+ handleError(err);
2721
+ }
2722
+ });
2723
+
2724
+ emailCmd
2725
+ .command('set')
2726
+ .description('Set persistent workspace default for auto_create_inbound_leads')
2727
+ .requiredOption('--auto-create-leads <bool>', 'Set workspace default: true or false')
2728
+ .action(async (opts) => {
2729
+ const globalOpts = program.opts();
2730
+ const client = getClient(globalOpts);
2731
+ const value = opts.autoCreateLeads === 'true' || opts.autoCreateLeads === true;
2732
+ try {
2733
+ const res = await client.patch('/emails/sync/config', { auto_create_inbound_leads: value });
2734
+ console.log(`Workspace default auto_create_inbound_leads set to: ${res.data.data.auto_create_inbound_leads}`);
2735
+ } catch (err) {
2736
+ handleError(err);
2737
+ }
2738
+ });
2739
+
2740
+ // ============================================================
2741
+ // ANALYTICS COMMANDS (S17-2)
2742
+ // ============================================================
2743
+
2744
+ const analyticsCmd = program.command('analytics').description('CRM analytics and pipeline metrics (S17-2)');
2745
+
2746
+ analyticsCmd
2747
+ .command('summary')
2748
+ .description('Show aggregated lead analytics including pipeline velocity, loss reasons, and conversion rates')
2749
+ .option('--assigned-to <agent>', 'Scope metrics to a specific agent')
2750
+ .action(async (opts) => {
2751
+ const globalOpts = program.opts();
2752
+ const client = getClient(globalOpts);
2753
+ const params = {};
2754
+ if (opts.assignedTo) params.assigned_to = opts.assignedTo;
2755
+ try {
2756
+ const res = await client.get('/analytics/summary', { params });
2757
+ const d = res.data.data;
2758
+ console.log('\n=== Analytics Summary ===');
2759
+ console.log(`Total leads: ${d.total_leads}`);
2760
+ if (d.scoped_to) console.log(`Scoped to agent: ${d.scoped_to}`);
2761
+ console.log('\nBy Status:');
2762
+ Object.entries(d.by_status || {}).forEach(([k, v]) => console.log(` ${k}: ${v}`));
2763
+ console.log('\nBy Stage:');
2764
+ Object.entries(d.by_stage || {}).forEach(([k, v]) => console.log(` ${k}: ${v}`));
2765
+ if (d.avg_close_probability != null) console.log(`\nAvg close probability: ${d.avg_close_probability}%`);
2766
+ console.log(`Stale leads (30d no activity): ${d.stale_leads_count}`);
2767
+ if (Object.keys(d.loss_reason_distribution || {}).length > 0) {
2768
+ console.log('\nLoss Reasons:');
2769
+ Object.entries(d.loss_reason_distribution).forEach(([k, v]) => console.log(` ${k}: ${v}`));
2770
+ }
2771
+ if (Object.keys(d.pipeline_velocity || {}).length > 0) {
2772
+ console.log('\nPipeline Velocity (avg days per stage):');
2773
+ Object.entries(d.pipeline_velocity).forEach(([k, v]) => console.log(` ${k}: ${v}d`));
2774
+ }
2775
+ if (Object.keys(d.stage_conversion_rates || {}).length > 0) {
2776
+ console.log('\nStage Conversion Rates:');
2777
+ Object.entries(d.stage_conversion_rates).forEach(([k, v]) => console.log(` ${k}: ${v}%`));
2778
+ }
2779
+ } catch (err) {
2780
+ handleError(err);
2781
+ }
2782
+ });
2783
+
2784
+ // ============================================================
2785
+ // WEBHOOKS COMMANDS (S17-3)
2786
+ // ============================================================
2787
+
2788
+ const webhooksCmd = program.command('webhooks').description('Manage webhook endpoints (S17-3)');
2789
+
2790
+ webhooksCmd
2791
+ .command('list')
2792
+ .description('List registered webhook endpoints')
2793
+ .action(async () => {
2794
+ const globalOpts = program.opts();
2795
+ const client = getClient(globalOpts);
2796
+ try {
2797
+ const res = await client.get('/webhooks');
2798
+ printJSON(res.data);
2799
+ } catch (err) {
2800
+ handleError(err);
2801
+ }
2802
+ });
2803
+
2804
+ webhooksCmd
2805
+ .command('events')
2806
+ .description('List valid webhook event types')
2807
+ .action(async () => {
2808
+ const globalOpts = program.opts();
2809
+ const client = getClient(globalOpts);
2810
+ try {
2811
+ const res = await client.get('/webhooks/events');
2812
+ printJSON(res.data);
2813
+ } catch (err) {
2814
+ handleError(err);
2815
+ }
2816
+ });
2817
+
2818
+ webhooksCmd
2819
+ .command('create')
2820
+ .description('Register a new webhook endpoint')
2821
+ .requiredOption('--url <url>', 'Webhook destination URL')
2822
+ .requiredOption('--events <events>', 'Comma-separated event types (e.g. lead.created,lead.status_changed)')
2823
+ .option('--secret <secret>', 'Signing secret for HMAC-SHA256 verification (generated if omitted)')
2824
+ .action(async (opts) => {
2825
+ const globalOpts = program.opts();
2826
+ const client = getClient(globalOpts);
2827
+ const events = opts.events.split(',').map(e => e.trim()).filter(Boolean);
2828
+ const body = { url: opts.url, events };
2829
+ if (opts.secret) body.secret = opts.secret;
2830
+ try {
2831
+ const res = await client.post('/webhooks', body);
2832
+ console.log('Webhook registered.');
2833
+ printJSON(res.data);
2834
+ } catch (err) {
2835
+ handleError(err);
2836
+ }
2837
+ });
2838
+
2839
+ webhooksCmd
2840
+ .command('delete <id>')
2841
+ .description('Remove a webhook endpoint by ID')
2842
+ .action(async (id) => {
2843
+ const globalOpts = program.opts();
2844
+ const client = getClient(globalOpts);
2845
+ try {
2846
+ const res = await client.delete(`/webhooks/${id}`);
2847
+ console.log('Webhook deleted.');
2848
+ printJSON(res.data);
2849
+ } catch (err) {
2850
+ handleError(err);
2851
+ }
2852
+ });
2853
+
2854
+ // ── stages ──────────────────────────────────────────────────────────
2855
+ const stagesCmd = program.command('stages').description('Manage pipeline stages (admin)');
2856
+
2857
+ // Helper: login via workspace-scoped POST /auth/login and return JWT token
2858
+ async function stagesAdminLogin(base, cfg, opts) {
2859
+ const workspace = opts.workspace || process.env.SAAC_CRM_WORKSPACE || cfg.workspace;
2860
+ const email = opts.email || process.env.SAAC_CRM_ADMIN_EMAIL || cfg.admin_email;
2861
+ const password = opts.password || process.env.SAAC_CRM_ADMIN_PASSWORD || await promptSecret('Admin password: ');
2862
+ if (!workspace) { console.error('Error: workspace required. Use --workspace or run: saac_crm config set --workspace <slug>'); process.exit(1); }
2863
+ if (!email) { console.error('Error: email required. Use --email or SAAC_CRM_ADMIN_EMAIL'); process.exit(1); }
2864
+ const loginRes = await axios.post(`${base}/api/v1/auth/login`, { workspace, email, password });
2865
+ return loginRes.data.data?.token;
2866
+ }
2867
+
2868
+ stagesCmd
2869
+ .command('list')
2870
+ .description('List all pipeline stages for workspace')
2871
+ .option('--workspace <slug>', 'Workspace slug')
2872
+ .option('--email <email>', 'Admin email')
2873
+ .option('--password <password>', 'Admin password')
2874
+ .action(async (opts) => {
2875
+ const globalOpts = program.opts();
2876
+ const apiUrl = resolveApiUrl(globalOpts.url);
2877
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
2878
+ const cfg = loadConfig();
2879
+ try {
2880
+ const base = apiUrl.replace(/\/$/, '');
2881
+ const token = await stagesAdminLogin(base, cfg, opts);
2882
+ const res = await axios.get(`${base}/api/v1/admin/stages`, {
2883
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
2884
+ });
2885
+ printJSON(res.data);
2886
+ } catch (err) { handleError(err); }
2887
+ });
2888
+
2889
+ stagesCmd
2890
+ .command('create <name>')
2891
+ .description('Create a new pipeline stage')
2892
+ .option('--order <n>', 'Sort order (integer)', '999')
2893
+ .option('--workspace <slug>', 'Workspace slug')
2894
+ .option('--email <email>', 'Admin email')
2895
+ .option('--password <password>', 'Admin password')
2896
+ .action(async (name, opts) => {
2897
+ const globalOpts = program.opts();
2898
+ const apiUrl = resolveApiUrl(globalOpts.url);
2899
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
2900
+ const cfg = loadConfig();
2901
+ try {
2902
+ const base = apiUrl.replace(/\/$/, '');
2903
+ const token = await stagesAdminLogin(base, cfg, opts);
2904
+ const res = await axios.post(`${base}/api/v1/admin/stages`, { name, order: parseInt(opts.order, 10) }, {
2905
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
2906
+ });
2907
+ printJSON(res.data);
2908
+ } catch (err) { handleError(err); }
2909
+ });
2910
+
2911
+ stagesCmd
2912
+ .command('update <stage-id>')
2913
+ .description('Rename or reorder a pipeline stage')
2914
+ .option('--name <name>', 'New stage name')
2915
+ .option('--order <n>', 'New sort order (integer)')
2916
+ .option('--workspace <slug>', 'Workspace slug')
2917
+ .option('--email <email>', 'Admin email')
2918
+ .option('--password <password>', 'Admin password')
2919
+ .action(async (stageId, opts) => {
2920
+ const globalOpts = program.opts();
2921
+ const apiUrl = resolveApiUrl(globalOpts.url);
2922
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
2923
+ const cfg = loadConfig();
2924
+ const body = {};
2925
+ if (opts.name) body.name = opts.name;
2926
+ if (opts.order !== undefined) body.order = parseInt(opts.order, 10);
2927
+ if (Object.keys(body).length === 0) { console.error('Provide --name and/or --order'); process.exit(1); }
2928
+ try {
2929
+ const base = apiUrl.replace(/\/$/, '');
2930
+ const token = await stagesAdminLogin(base, cfg, opts);
2931
+ const res = await axios.patch(`${base}/api/v1/admin/stages/${stageId}`, body, {
2932
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
2933
+ });
2934
+ printJSON(res.data);
2935
+ } catch (err) { handleError(err); }
2936
+ });
2937
+
2938
+ stagesCmd
2939
+ .command('delete <stage-id>')
2940
+ .description('Delete a pipeline stage (fails if leads are in that stage)')
2941
+ .option('--workspace <slug>', 'Workspace slug')
2942
+ .option('--email <email>', 'Admin email')
2943
+ .option('--password <password>', 'Admin password')
2944
+ .action(async (stageId, opts) => {
2945
+ const globalOpts = program.opts();
2946
+ const apiUrl = resolveApiUrl(globalOpts.url);
2947
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
2948
+ const cfg = loadConfig();
2949
+ try {
2950
+ const base = apiUrl.replace(/\/$/, '');
2951
+ const token = await stagesAdminLogin(base, cfg, opts);
2952
+ const res = await axios.delete(`${base}/api/v1/admin/stages/${stageId}`, {
2953
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
2954
+ });
2955
+ printJSON(res.data);
2956
+ } catch (err) { handleError(err); }
2957
+ });
2958
+
2959
+ // ============================================================
2960
+ // Sprint 49 T1-B: companies command group
2961
+ // ============================================================
2962
+ const companiesCmd = program.command('companies').description('Manage companies/accounts (S49-T1-B)');
2963
+
2964
+ companiesCmd
2965
+ .command('list')
2966
+ .description('List companies')
2967
+ .option('--q <query>', 'Search by name or domain')
2968
+ .option('--limit <n>', 'Max results', '20')
2969
+ .option('--offset <n>', 'Offset for pagination', '0')
2970
+ .action(async (opts) => {
2971
+ const globalOpts = program.opts();
2972
+ const client = getClient(globalOpts);
2973
+ try {
2974
+ const params = {};
2975
+ if (opts.q) params.q = opts.q;
2976
+ if (opts.limit) params.limit = opts.limit;
2977
+ if (opts.offset) params.offset = opts.offset;
2978
+ const res = await client.get('/companies', { params });
2979
+ printJSON(res.data);
2980
+ } catch (err) { handleError(err); }
2981
+ });
2982
+
2983
+ companiesCmd
2984
+ .command('get <id>')
2985
+ .description('Get a company by ID (includes linked leads)')
2986
+ .action(async (id) => {
2987
+ const globalOpts = program.opts();
2988
+ const client = getClient(globalOpts);
2989
+ try {
2990
+ const res = await client.get(`/companies/${id}`);
2991
+ printJSON(res.data);
2992
+ } catch (err) { handleError(err); }
2993
+ });
2994
+
2995
+ companiesCmd
2996
+ .command('create')
2997
+ .description('Create a new company')
2998
+ .requiredOption('--name <name>', 'Company name')
2999
+ .option('--domain <domain>', 'Company domain (e.g. acme.com)')
3000
+ .option('--industry <industry>', 'Industry')
3001
+ .option('--employee-count <n>', 'Number of employees')
3002
+ .option('--website <url>', 'Website URL')
3003
+ .option('--from-agent-name <name>', 'Agent name for attribution')
3004
+ .action(async (opts) => {
3005
+ const globalOpts = program.opts();
3006
+ const client = getClient(globalOpts, resolveAgentName(opts.fromAgentName));
3007
+ try {
3008
+ const body = {
3009
+ name: opts.name,
3010
+ ...(opts.domain && { domain: opts.domain }),
3011
+ ...(opts.industry && { industry: opts.industry }),
3012
+ ...(opts.employeeCount !== undefined && { size: String(parseInt(opts.employeeCount)) }),
3013
+ ...(opts.website && { website: opts.website }),
3014
+ };
3015
+ const res = await client.post('/companies', body);
3016
+ printJSON(res.data);
3017
+ } catch (err) { handleError(err); }
3018
+ });
3019
+
3020
+ companiesCmd
3021
+ .command('update <id>')
3022
+ .description('Update a company by ID')
3023
+ .option('--name <name>', 'New name')
3024
+ .option('--domain <domain>', 'New domain')
3025
+ .option('--industry <industry>', 'New industry')
3026
+ .option('--employee-count <n>', 'New employee count')
3027
+ .option('--website <url>', 'New website URL')
3028
+ .option('--from-agent-name <name>', 'Agent name for attribution')
3029
+ .action(async (id, opts) => {
3030
+ const globalOpts = program.opts();
3031
+ const client = getClient(globalOpts, resolveAgentName(opts.fromAgentName));
3032
+ try {
3033
+ const body = {
3034
+ ...(opts.name && { name: opts.name }),
3035
+ ...(opts.domain !== undefined && { domain: opts.domain }),
3036
+ ...(opts.industry !== undefined && { industry: opts.industry }),
3037
+ ...(opts.employeeCount !== undefined && { size: String(parseInt(opts.employeeCount)) }),
3038
+ ...(opts.website !== undefined && { website: opts.website }),
3039
+ };
3040
+ const res = await client.put(`/companies/${id}`, body);
3041
+ printJSON(res.data);
3042
+ } catch (err) { handleError(err); }
3043
+ });
3044
+
3045
+ companiesCmd
3046
+ .command('delete <id>')
3047
+ .description('Delete a company (soft delete, unlinks leads)')
3048
+ .action(async (id) => {
3049
+ const globalOpts = program.opts();
3050
+ const client = getClient(globalOpts);
3051
+ try {
3052
+ const res = await client.delete(`/companies/${id}`);
3053
+ printJSON(res.data);
3054
+ } catch (err) { handleError(err); }
3055
+ });
3056
+
3057
+ companiesCmd
3058
+ .command('leads <id>')
3059
+ .description('List leads linked to a company')
3060
+ .action(async (id) => {
3061
+ const globalOpts = program.opts();
3062
+ const client = getClient(globalOpts);
3063
+ try {
3064
+ const res = await client.get(`/companies/${id}`);
3065
+ // Return only the leads array from the company response
3066
+ const data = res.data?.data;
3067
+ printJSON({ success: true, data: data?.leads || [] });
3068
+ } catch (err) { handleError(err); }
3069
+ });
3070
+
3071
+ // ============================================================
3072
+ // Sprint 49 T1-B: sequences command group (enrollment only — definitions are admin-only)
3073
+ // ============================================================
3074
+ const sequencesCmd = program.command('sequences').description('Manage sequence enrollments (S49-T1-B). Sequence definitions are admin-only.');
3075
+
3076
+ sequencesCmd
3077
+ .command('list')
3078
+ .description('List all sequences (read-only)')
3079
+ .action(async () => {
3080
+ const globalOpts = program.opts();
3081
+ const client = getClient(globalOpts);
3082
+ try {
3083
+ const res = await client.get('/sequences');
3084
+ printJSON(res.data);
3085
+ } catch (err) { handleError(err); }
3086
+ });
3087
+
3088
+ sequencesCmd
3089
+ .command('get <id>')
3090
+ .description('Get a sequence with its steps')
3091
+ .action(async (id) => {
3092
+ const globalOpts = program.opts();
3093
+ const client = getClient(globalOpts);
3094
+ try {
3095
+ const res = await client.get(`/sequences/${id}`);
3096
+ printJSON(res.data);
3097
+ } catch (err) { handleError(err); }
3098
+ });
3099
+
3100
+ sequencesCmd
3101
+ .command('enroll <sequenceId>')
3102
+ .description('Enroll one or more leads into a sequence')
3103
+ .requiredOption('--lead-ids <ids>', 'Comma-separated lead UUIDs')
3104
+ .option('--from-agent-name <name>', 'Agent name for attribution')
3105
+ .action(async (sequenceId, opts) => {
3106
+ const globalOpts = program.opts();
3107
+ const client = getClient(globalOpts, resolveAgentName(opts.fromAgentName));
3108
+ try {
3109
+ const lead_ids = opts.leadIds.split(',').map(s => s.trim()).filter(Boolean);
3110
+ const res = await client.post(`/sequences/${sequenceId}/enroll`, { lead_ids });
3111
+ printJSON(res.data);
3112
+ } catch (err) { handleError(err); }
3113
+ });
3114
+
3115
+ sequencesCmd
3116
+ .command('unenroll <sequenceId> <leadId>')
3117
+ .description('Unenroll a single lead from a sequence')
3118
+ .action(async (sequenceId, leadId) => {
3119
+ const globalOpts = program.opts();
3120
+ const client = getClient(globalOpts);
3121
+ try {
3122
+ const res = await client.delete(`/sequences/${sequenceId}/enroll/${leadId}`);
3123
+ printJSON(res.data);
3124
+ } catch (err) { handleError(err); }
3125
+ });
3126
+
3127
+ sequencesCmd
3128
+ .command('enrollments <sequenceId>')
3129
+ .description('List enrollments for a sequence')
3130
+ .option('--status <status>', 'Filter by status (active|paused|completed|unenrolled)')
3131
+ .option('--q <query>', 'Search by lead name or email')
3132
+ .option('--limit <n>', 'Max results', '50')
3133
+ .option('--offset <n>', 'Offset for pagination', '0')
3134
+ .action(async (sequenceId, opts) => {
3135
+ const globalOpts = program.opts();
3136
+ const client = getClient(globalOpts);
3137
+ try {
3138
+ const params = { limit: opts.limit, offset: opts.offset };
3139
+ if (opts.status) params.status = opts.status;
3140
+ if (opts.q) params.q = opts.q;
3141
+ const res = await client.get(`/sequences/${sequenceId}/enrollments`, { params });
3142
+ printJSON(res.data);
3143
+ } catch (err) { handleError(err); }
3144
+ });
3145
+
3146
+ sequencesCmd
3147
+ .command('bulk-enroll <sequenceId>')
3148
+ .description('Bulk enroll leads into a sequence (≤50 sync, >50 returns job ID)')
3149
+ .option('--lead-ids <ids>', 'Comma-separated lead UUIDs (max 500)')
3150
+ .option('--segment-id <id>', 'Enroll all leads in a segment')
3151
+ .option('--search-status <status>', 'Enroll leads matching status filter')
3152
+ .option('--from-agent-name <name>', 'Agent name for attribution')
3153
+ .action(async (sequenceId, opts) => {
3154
+ const globalOpts = program.opts();
3155
+ const client = getClient(globalOpts, resolveAgentName(opts.fromAgentName));
3156
+ try {
3157
+ const body = {};
3158
+ if (opts.leadIds) body.lead_ids = opts.leadIds.split(',').map(s => s.trim()).filter(Boolean);
3159
+ else if (opts.segmentId) body.segment_id = opts.segmentId;
3160
+ else if (opts.searchStatus) body.search = { status: opts.searchStatus };
3161
+ else { console.error('One of --lead-ids, --segment-id, or --search-status required'); process.exit(1); }
3162
+ const res = await client.post(`/sequences/${sequenceId}/enroll/bulk`, body);
3163
+ printJSON(res.data);
3164
+ } catch (err) { handleError(err); }
3165
+ });
3166
+
3167
+ sequencesCmd
3168
+ .command('bulk-unenroll <sequenceId>')
3169
+ .description('Bulk unenroll leads from a sequence')
3170
+ .requiredOption('--lead-ids <ids>', 'Comma-separated lead UUIDs')
3171
+ .action(async (sequenceId, opts) => {
3172
+ const globalOpts = program.opts();
3173
+ const client = getClient(globalOpts);
3174
+ try {
3175
+ const lead_ids = opts.leadIds.split(',').map(s => s.trim()).filter(Boolean);
3176
+ const res = await client.post(`/sequences/${sequenceId}/unenroll/bulk`, { lead_ids });
3177
+ printJSON(res.data);
3178
+ } catch (err) { handleError(err); }
3179
+ });
3180
+
3181
+ sequencesCmd
3182
+ .command('job <jobId>')
3183
+ .description('Poll status of an async bulk-enroll job')
3184
+ .action(async (jobId) => {
3185
+ const globalOpts = program.opts();
3186
+ const client = getClient(globalOpts);
3187
+ try {
3188
+ const res = await client.get(`/jobs/${jobId}`);
3189
+ printJSON(res.data);
3190
+ } catch (err) { handleError(err); }
3191
+ });
3192
+
2496
3193
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.10.0",
3
+ "version": "2.11.1",
4
4
  "description": "AI-first CRM CLI — manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {