@startanaicompany/crm 2.8.0 → 2.11.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 +753 -6
- package/package.json +2 -2
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>
|
|
@@ -398,6 +428,10 @@ leadsCmd
|
|
|
398
428
|
.option('--source <source>', 'Source: api|import|referral|linkedin|cold_email|website|inbound|partner|event|other', 'api')
|
|
399
429
|
.option('--deal-value <amount>', 'Deal value (numeric)')
|
|
400
430
|
.option('--pipeline-stage <stage>', 'Pipeline stage: new|prospecting|discovery|qualified|demo_scheduled|demo_completed|proposal_sent|negotiation|contract_sent|closed_won|closed_lost|no_decision|dormant')
|
|
431
|
+
.option('--score <n>', 'Lead score (0-100 integer). Conventions: 0-19 Cold, 20-39 Warm, 40-59 Developing, 60-79 Qualified, 80-100 Hot')
|
|
432
|
+
.option('--score-reason <text>', 'Score reason/explanation')
|
|
433
|
+
.option('--close-probability <n>', 'Close probability 0-100 integer')
|
|
434
|
+
.option('--close-reason <reason>', `Close reason (${['price','timing','competition','no_budget','no_decision','fit','other'].join('|')})`)
|
|
401
435
|
.option('--notes <notes>', 'Notes')
|
|
402
436
|
.option('--assigned-to <assignedTo>', 'Assigned to')
|
|
403
437
|
.option('--tag <tag>', 'Tag (repeatable)', (v, prev) => prev.concat([v]), [])
|
|
@@ -419,6 +453,11 @@ leadsCmd
|
|
|
419
453
|
source: opts.source,
|
|
420
454
|
...(opts.dealValue !== undefined && { deal_value: parseFloat(opts.dealValue) }),
|
|
421
455
|
...(opts.pipelineStage && { pipeline_stage: opts.pipelineStage }),
|
|
456
|
+
// --score 0 is valid — use !== undefined not falsy check
|
|
457
|
+
...(opts.score !== undefined && { score: parseInt(opts.score) }),
|
|
458
|
+
...(opts.scoreReason && { score_reason: opts.scoreReason }),
|
|
459
|
+
...(opts.closeProbability !== undefined && { close_probability: parseInt(opts.closeProbability) }),
|
|
460
|
+
...(opts.closeReason && { close_reason: opts.closeReason }),
|
|
422
461
|
...(opts.notes && { notes: opts.notes }),
|
|
423
462
|
...(opts.assignedTo && { assigned_to: opts.assignedTo }),
|
|
424
463
|
...(opts.tag.length > 0 && { tags: opts.tag }),
|
|
@@ -448,6 +487,9 @@ leadsCmd
|
|
|
448
487
|
.option('--order <dir>', 'Sort direction: asc|desc', 'desc')
|
|
449
488
|
.option('--score-min <n>', 'Filter leads with score >= n (0-100)')
|
|
450
489
|
.option('--score-max <n>', 'Filter leads with score <= n (0-100)')
|
|
490
|
+
.option('--close-probability-min <n>', 'Filter leads with close_probability >= n')
|
|
491
|
+
.option('--close-probability-max <n>', 'Filter leads with close_probability <= n')
|
|
492
|
+
.option('--not-contacted-since-days <n>', 'Filter stale leads not contacted in N days (excludes converted/lost)')
|
|
451
493
|
.option('--created-after <date>', 'Filter leads created after this ISO8601 date (e.g. 2026-01-01T00:00:00Z)')
|
|
452
494
|
.option('--created-before <date>', 'Filter leads created before this ISO8601 date')
|
|
453
495
|
.option('--updated-after <date>', 'Filter leads updated after this ISO8601 date')
|
|
@@ -471,6 +513,9 @@ leadsCmd
|
|
|
471
513
|
...(opts.order && { order: opts.order }),
|
|
472
514
|
...(opts.scoreMin !== undefined && { score_min: opts.scoreMin }),
|
|
473
515
|
...(opts.scoreMax !== undefined && { score_max: opts.scoreMax }),
|
|
516
|
+
...(opts.closeProbabilityMin !== undefined && { close_probability_min: opts.closeProbabilityMin }),
|
|
517
|
+
...(opts.closeProbabilityMax !== undefined && { close_probability_max: opts.closeProbabilityMax }),
|
|
518
|
+
...(opts.notContactedSinceDays !== undefined && { not_contacted_since_days: opts.notContactedSinceDays }),
|
|
474
519
|
...(opts.createdAfter && { created_after: opts.createdAfter }),
|
|
475
520
|
...(opts.createdBefore && { created_before: opts.createdBefore }),
|
|
476
521
|
...(opts.updatedAfter && { updated_after: opts.updatedAfter }),
|
|
@@ -563,6 +608,10 @@ leadsCmd
|
|
|
563
608
|
.option('--status <status>', 'New status')
|
|
564
609
|
.option('--source <source>', 'New source')
|
|
565
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)')
|
|
566
615
|
.option('--notes <notes>', 'New notes')
|
|
567
616
|
.option('--assigned-to <assignedTo>', 'New assigned-to')
|
|
568
617
|
.option('--tag <tag>', 'Replace ALL tags (repeatable). Use --add-tag / --remove-tag for surgical edits.', (v, prev) => prev.concat([v]), [])
|
|
@@ -570,9 +619,11 @@ leadsCmd
|
|
|
570
619
|
.option('--remove-tag <tag>', 'Remove a single tag (repeatable)', (v, prev) => prev.concat([v]), [])
|
|
571
620
|
.option('--external-id <externalId>', 'New external ID')
|
|
572
621
|
.option('--version <version>', 'Optimistic lock version')
|
|
622
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
573
623
|
.action(async (id, opts) => {
|
|
574
624
|
const globalOpts = program.opts();
|
|
575
|
-
const
|
|
625
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
626
|
+
const client = getClient(globalOpts, agentName);
|
|
576
627
|
try {
|
|
577
628
|
// Surgical tag ops: call PATCH /leads/:id/tags first if add/remove specified
|
|
578
629
|
if (opts.addTag.length > 0 || opts.removeTag.length > 0) {
|
|
@@ -583,14 +634,19 @@ leadsCmd
|
|
|
583
634
|
// If no other fields to update, return tag result
|
|
584
635
|
const hasOtherFields = opts.name || opts.email || opts.phone !== undefined ||
|
|
585
636
|
opts.company !== undefined || opts.status || opts.source ||
|
|
586
|
-
opts.dealValue !== undefined || opts.
|
|
587
|
-
opts.
|
|
588
|
-
opts.
|
|
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;
|
|
589
641
|
if (!hasOtherFields) {
|
|
590
642
|
printJSON(tagRes.data);
|
|
591
643
|
return;
|
|
592
644
|
}
|
|
593
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
|
+
}
|
|
594
650
|
const body = {
|
|
595
651
|
...(opts.name && { name: opts.name }),
|
|
596
652
|
...(opts.email && { email: opts.email }),
|
|
@@ -599,14 +655,22 @@ leadsCmd
|
|
|
599
655
|
...(opts.status && { status: opts.status }),
|
|
600
656
|
...(opts.source && { source: opts.source }),
|
|
601
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 }),
|
|
602
661
|
...(opts.notes !== undefined && { notes: opts.notes }),
|
|
603
662
|
...(opts.assignedTo !== undefined && { assigned_to: opts.assignedTo }),
|
|
604
663
|
...(opts.tag.length > 0 && { tags: opts.tag }),
|
|
605
664
|
...(opts.externalId !== undefined && { external_id: opts.externalId }),
|
|
606
665
|
...(opts.version !== undefined && { version: parseInt(opts.version) })
|
|
607
666
|
};
|
|
608
|
-
|
|
609
|
-
|
|
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
|
+
}
|
|
610
674
|
} catch (err) {
|
|
611
675
|
handleError(err);
|
|
612
676
|
}
|
|
@@ -627,6 +691,21 @@ leadsCmd
|
|
|
627
691
|
}
|
|
628
692
|
});
|
|
629
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
|
+
|
|
630
709
|
leadsCmd
|
|
631
710
|
.command('history <id>')
|
|
632
711
|
.description('Show status change history for a lead')
|
|
@@ -641,6 +720,146 @@ leadsCmd
|
|
|
641
720
|
}
|
|
642
721
|
});
|
|
643
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
|
+
|
|
644
863
|
// ============================================================
|
|
645
864
|
// USERS COMMANDS (requires admin scope key)
|
|
646
865
|
// ============================================================
|
|
@@ -1344,7 +1563,42 @@ emailsCmd
|
|
|
1344
1563
|
}
|
|
1345
1564
|
});
|
|
1346
1565
|
|
|
1566
|
+
emailsCmd
|
|
1567
|
+
.command('sync')
|
|
1568
|
+
.description('Sync Gmail inbox into CRM (deduped by Gmail message ID)')
|
|
1569
|
+
.option('--max-results <n>', 'Max number of messages to fetch (default 100, max 500)', '100')
|
|
1570
|
+
.action(async (opts) => {
|
|
1571
|
+
const globalOpts = program.opts();
|
|
1572
|
+
const client = getClient(globalOpts);
|
|
1573
|
+
try {
|
|
1574
|
+
const res = await client.post('/emails/sync', { max_results: parseInt(opts.maxResults) || 100 });
|
|
1575
|
+
printJSON(res.data);
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
handleError(err);
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1347
1580
|
|
|
1581
|
+
emailsCmd
|
|
1582
|
+
.command('send')
|
|
1583
|
+
.description('Send an email via Gmail (rate-limited: 1/30min, 30/day per workspace)')
|
|
1584
|
+
.requiredOption('--to <email>', 'Recipient email address')
|
|
1585
|
+
.requiredOption('--subject <subject>', 'Email subject')
|
|
1586
|
+
.requiredOption('--body <text>', 'Email body text')
|
|
1587
|
+
.option('--lead-id <id>', 'Link sent email to a lead ID')
|
|
1588
|
+
.option('--dry-run', 'Validate only — do not send or consume rate limit quota')
|
|
1589
|
+
.action(async (opts) => {
|
|
1590
|
+
const globalOpts = program.opts();
|
|
1591
|
+
const client = getClient(globalOpts);
|
|
1592
|
+
try {
|
|
1593
|
+
const body = { to: opts.to, subject: opts.subject, body: opts.body };
|
|
1594
|
+
if (opts.leadId) body.lead_id = opts.leadId;
|
|
1595
|
+
if (opts.dryRun) body.dry_run = true;
|
|
1596
|
+
const res = await client.post('/emails/send', body);
|
|
1597
|
+
printJSON(res.data);
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
handleError(err);
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1348
1602
|
|
|
1349
1603
|
// ============================================================
|
|
1350
1604
|
// QUOTES commands
|
|
@@ -2443,4 +2697,497 @@ const supportCmd = program.command('support').description('Submit and manage bug
|
|
|
2443
2697
|
});
|
|
2444
2698
|
});
|
|
2445
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 && { employee_count: 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 && { employee_count: 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
|
+
|
|
2446
3193
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startanaicompany/crm",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "AI-first CRM CLI
|
|
3
|
+
"version": "2.11.0",
|
|
4
|
+
"description": "AI-first CRM CLI — manage leads and API keys from the terminal",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"saac_crm": "./index.js"
|