@startanaicompany/crm 2.3.1 → 2.4.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 +160 -6
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -96,10 +96,12 @@ function promptSecret(question) {
|
|
|
96
96
|
|
|
97
97
|
const program = new Command();
|
|
98
98
|
|
|
99
|
+
const { version: pkgVersion } = require('./package.json');
|
|
100
|
+
|
|
99
101
|
program
|
|
100
102
|
.name('saac_crm')
|
|
101
103
|
.description('AI-first CRM CLI — manage leads and API keys')
|
|
102
|
-
.version(
|
|
104
|
+
.version(pkgVersion)
|
|
103
105
|
.option('--api-key <key>', 'API key (overrides SAAC_CRM_API_KEY env and config)')
|
|
104
106
|
.option('--url <url>', 'API base URL (overrides config)');
|
|
105
107
|
|
|
@@ -298,6 +300,8 @@ leadsCmd
|
|
|
298
300
|
.option('--company <company>', 'Company name')
|
|
299
301
|
.option('--status <status>', 'Status: new|contacted|qualified|unresponsive|converted|lost', 'new')
|
|
300
302
|
.option('--source <source>', 'Source: api|import|referral', 'api')
|
|
303
|
+
.option('--deal-value <amount>', 'Deal value (numeric)')
|
|
304
|
+
.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')
|
|
301
305
|
.option('--notes <notes>', 'Notes')
|
|
302
306
|
.option('--assigned-to <assignedTo>', 'Assigned to')
|
|
303
307
|
.option('--tag <tag>', 'Tag (repeatable)', (v, prev) => prev.concat([v]), [])
|
|
@@ -315,6 +319,8 @@ leadsCmd
|
|
|
315
319
|
...(opts.company && { company: opts.company }),
|
|
316
320
|
status: opts.status,
|
|
317
321
|
source: opts.source,
|
|
322
|
+
...(opts.dealValue !== undefined && { deal_value: parseFloat(opts.dealValue) }),
|
|
323
|
+
...(opts.pipelineStage && { pipeline_stage: opts.pipelineStage }),
|
|
318
324
|
...(opts.notes && { notes: opts.notes }),
|
|
319
325
|
...(opts.assignedTo && { assigned_to: opts.assignedTo }),
|
|
320
326
|
...(opts.tag.length > 0 && { tags: opts.tag }),
|
|
@@ -394,6 +400,7 @@ leadsCmd
|
|
|
394
400
|
.option('--company <company>', 'New company')
|
|
395
401
|
.option('--status <status>', 'New status')
|
|
396
402
|
.option('--source <source>', 'New source')
|
|
403
|
+
.option('--deal-value <amount>', 'Deal value (numeric)')
|
|
397
404
|
.option('--notes <notes>', 'New notes')
|
|
398
405
|
.option('--assigned-to <assignedTo>', 'New assigned-to')
|
|
399
406
|
.option('--tag <tag>', 'Replace tags (repeatable)', (v, prev) => prev.concat([v]), [])
|
|
@@ -409,6 +416,7 @@ leadsCmd
|
|
|
409
416
|
...(opts.company !== undefined && { company: opts.company }),
|
|
410
417
|
...(opts.status && { status: opts.status }),
|
|
411
418
|
...(opts.source && { source: opts.source }),
|
|
419
|
+
...(opts.dealValue !== undefined && { deal_value: parseFloat(opts.dealValue) }),
|
|
412
420
|
...(opts.notes !== undefined && { notes: opts.notes }),
|
|
413
421
|
...(opts.assignedTo !== undefined && { assigned_to: opts.assignedTo }),
|
|
414
422
|
...(opts.tag.length > 0 && { tags: opts.tag }),
|
|
@@ -684,6 +692,7 @@ contactsCmd
|
|
|
684
692
|
.option('--phone <phone>', 'Phone number')
|
|
685
693
|
.option('--company <company>', 'Company name')
|
|
686
694
|
.option('--title <title>', 'Job title')
|
|
695
|
+
.option('--role <role>', 'Role: champion|economic_buyer|technical_buyer|gatekeeper|influencer|end_user')
|
|
687
696
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
688
697
|
.option('--notes <notes>', 'Notes')
|
|
689
698
|
.action(async (id, opts) => {
|
|
@@ -697,6 +706,7 @@ contactsCmd
|
|
|
697
706
|
if (opts.phone !== undefined) body.phone = opts.phone;
|
|
698
707
|
if (opts.company !== undefined) body.company = opts.company;
|
|
699
708
|
if (opts.title !== undefined) body.title = opts.title;
|
|
709
|
+
if (opts.role !== undefined) body.role = opts.role;
|
|
700
710
|
if (opts.tags !== undefined) body.tags = opts.tags.split(',').map(t => t.trim());
|
|
701
711
|
if (opts.notes !== undefined) body.notes = opts.notes;
|
|
702
712
|
const res = await client.patch(`/contacts/${id}`, body);
|
|
@@ -887,7 +897,7 @@ leadsStageCmd
|
|
|
887
897
|
|
|
888
898
|
leadsStageCmd
|
|
889
899
|
.command('set <lead-id> <stage>')
|
|
890
|
-
.description('Set pipeline stage: new
|
|
900
|
+
.description('Set pipeline stage: new|prospecting|discovery|qualified|demo_scheduled|demo_completed|proposal_sent|negotiation|contract_sent|closed_won|closed_lost|no_decision|dormant')
|
|
891
901
|
.option('--note <note>', 'Optional note about why stage changed')
|
|
892
902
|
.action(async (leadId, stage, opts) => {
|
|
893
903
|
const globalOpts = program.opts();
|
|
@@ -1012,6 +1022,7 @@ meetingsCmd
|
|
|
1012
1022
|
.option('--video-link <url>', 'Video link')
|
|
1013
1023
|
.option('--notes <notes>', 'Notes')
|
|
1014
1024
|
.option('--outcome <outcome>', 'scheduled | completed | cancelled | no_show')
|
|
1025
|
+
.option('--sentiment <sentiment>', 'positive | neutral | negative | unknown')
|
|
1015
1026
|
.action(async (id, opts) => {
|
|
1016
1027
|
const globalOpts = program.opts();
|
|
1017
1028
|
const client = getClient(globalOpts);
|
|
@@ -1024,6 +1035,7 @@ meetingsCmd
|
|
|
1024
1035
|
if (opts.videoLink !== undefined) body.video_link = opts.videoLink;
|
|
1025
1036
|
if (opts.notes !== undefined) body.notes = opts.notes;
|
|
1026
1037
|
if (opts.outcome !== undefined) body.outcome = opts.outcome;
|
|
1038
|
+
if (opts.sentiment !== undefined) body.sentiment = opts.sentiment;
|
|
1027
1039
|
const res = await client.patch(`/meetings/${id}`, body);
|
|
1028
1040
|
printJSON(res.data);
|
|
1029
1041
|
} catch (err) {
|
|
@@ -1496,16 +1508,158 @@ contractsCmd
|
|
|
1496
1508
|
|
|
1497
1509
|
contractsCmd
|
|
1498
1510
|
.command('sign <id>')
|
|
1499
|
-
.description('
|
|
1500
|
-
.option('--
|
|
1511
|
+
.description('Mark a specific signatory as signed (use --contact-id for multi-signatory; falls back to whole-contract sign if omitted)')
|
|
1512
|
+
.option('--contact-id <uuid>', 'Contact UUID of the signatory to mark as signed')
|
|
1513
|
+
.option('--method <method>', 'Signature method: electronic, wet_signature, docusign, hellosign, other')
|
|
1514
|
+
.option('--signed-at <datetime>', 'ISO8601 timestamp of signing (defaults to now)')
|
|
1515
|
+
.option('--signed-by <name>', 'Legacy: name of signatory (used when no --contact-id)')
|
|
1501
1516
|
.action(async (id, opts) => {
|
|
1502
1517
|
const globalOpts = program.opts();
|
|
1503
1518
|
const client = getClient(globalOpts);
|
|
1504
1519
|
try {
|
|
1520
|
+
if (opts.contactId) {
|
|
1521
|
+
// Sprint 8: per-signatory sign via PATCH /contracts/:id/signatories/:signatory_id
|
|
1522
|
+
// First find the signatory record for this contact
|
|
1523
|
+
const listRes = await client.get(`/contracts/${id}/signatories`);
|
|
1524
|
+
const signatories = listRes.data.data.signatories || [];
|
|
1525
|
+
const sig = signatories.find(s => s.contact_id === opts.contactId);
|
|
1526
|
+
if (!sig) {
|
|
1527
|
+
console.error(`No signatory found for contact ${opts.contactId} on contract ${id}`);
|
|
1528
|
+
process.exit(1);
|
|
1529
|
+
}
|
|
1530
|
+
const res = await client.patch(`/contracts/${id}/signatories/${sig.id}`, {
|
|
1531
|
+
status: 'signed',
|
|
1532
|
+
signature_method: opts.method || 'electronic',
|
|
1533
|
+
signed_at: opts.signedAt || new Date().toISOString(),
|
|
1534
|
+
});
|
|
1535
|
+
printJSON(res.data);
|
|
1536
|
+
} else {
|
|
1537
|
+
// Legacy whole-contract sign
|
|
1538
|
+
const res = await client.post(`/contracts/${id}/status`, { status: 'signed', signed_by: opts.signedBy });
|
|
1539
|
+
printJSON(res.data);
|
|
1540
|
+
}
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
handleError(err);
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1505
1545
|
|
|
1506
|
-
|
|
1546
|
+
// Sprint 8: Multi-Signatory Commands
|
|
1547
|
+
|
|
1548
|
+
contractsCmd
|
|
1549
|
+
.command('signatories <id>')
|
|
1550
|
+
.description('List all signatories for a contract with status and timestamps')
|
|
1551
|
+
.action(async (id) => {
|
|
1552
|
+
const globalOpts = program.opts();
|
|
1553
|
+
const client = getClient(globalOpts);
|
|
1554
|
+
try {
|
|
1555
|
+
const res = await client.get(`/contracts/${id}/signatories`);
|
|
1556
|
+
const { signatories, summary } = res.data.data;
|
|
1557
|
+
console.log(`\nSignatories for contract ${id}`);
|
|
1558
|
+
console.log(`Summary: ${summary.signed}/${summary.total} signed | ${summary.pending} pending | ${summary.declined} declined`);
|
|
1559
|
+
if (summary.is_fully_executed) console.log('✅ Contract fully executed');
|
|
1560
|
+
console.log('');
|
|
1561
|
+
if (signatories.length === 0) {
|
|
1562
|
+
console.log('No signatories added yet.');
|
|
1563
|
+
} else {
|
|
1564
|
+
signatories.forEach(s => {
|
|
1565
|
+
const icon = s.status === 'signed' ? '✅' : s.status === 'declined' ? '⛔' : '⏳';
|
|
1566
|
+
const signedAt = s.signed_at ? ` | Signed: ${new Date(s.signed_at).toLocaleDateString()}` : '';
|
|
1567
|
+
const reminded = s.reminder_sent_at ? ` | Last reminded: ${new Date(s.reminder_sent_at).toLocaleDateString()}` : '';
|
|
1568
|
+
console.log(` ${icon} [${s.party}] ${s.contact_name} <${s.contact_email}> — ${s.role} | ${s.status}${signedAt}${reminded}`);
|
|
1569
|
+
if (s.notes) console.log(` Notes: ${s.notes}`);
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
handleError(err);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
contractsCmd
|
|
1578
|
+
.command('add-signatory <id>')
|
|
1579
|
+
.description('Add a signatory to a contract')
|
|
1580
|
+
.requiredOption('--contact-id <uuid>', 'Contact UUID of the signatory')
|
|
1581
|
+
.requiredOption('--party <party>', 'Party: vendor or customer')
|
|
1582
|
+
.requiredOption('--role <role>', 'Role: signer, approver, or witness')
|
|
1583
|
+
.option('--method <method>', 'Signature method: electronic, wet_signature, docusign, hellosign, other')
|
|
1584
|
+
.option('--notes <notes>', 'Internal notes about this signatory')
|
|
1585
|
+
.action(async (id, opts) => {
|
|
1586
|
+
const globalOpts = program.opts();
|
|
1587
|
+
const client = getClient(globalOpts);
|
|
1588
|
+
try {
|
|
1589
|
+
const res = await client.post(`/contracts/${id}/signatories`, {
|
|
1590
|
+
contact_id: opts.contactId,
|
|
1591
|
+
party: opts.party,
|
|
1592
|
+
role: opts.role,
|
|
1593
|
+
signature_method: opts.method || undefined,
|
|
1594
|
+
notes: opts.notes || undefined,
|
|
1595
|
+
});
|
|
1596
|
+
printJSON(res.data);
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
handleError(err);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
contractsCmd
|
|
1603
|
+
.command('decline-signature <id>')
|
|
1604
|
+
.description('Mark a signatory as declined (deal blocker alert)')
|
|
1605
|
+
.requiredOption('--contact-id <uuid>', 'Contact UUID of the signatory declining')
|
|
1606
|
+
.option('--notes <notes>', 'Reason for declining')
|
|
1607
|
+
.action(async (id, opts) => {
|
|
1608
|
+
const globalOpts = program.opts();
|
|
1609
|
+
const client = getClient(globalOpts);
|
|
1610
|
+
try {
|
|
1611
|
+
const listRes = await client.get(`/contracts/${id}/signatories`);
|
|
1612
|
+
const signatories = listRes.data.data.signatories || [];
|
|
1613
|
+
const sig = signatories.find(s => s.contact_id === opts.contactId);
|
|
1614
|
+
if (!sig) {
|
|
1615
|
+
console.error(`No signatory found for contact ${opts.contactId} on contract ${id}`);
|
|
1616
|
+
process.exit(1);
|
|
1617
|
+
}
|
|
1618
|
+
const res = await client.patch(`/contracts/${id}/signatories/${sig.id}`, {
|
|
1619
|
+
status: 'declined',
|
|
1620
|
+
notes: opts.notes || undefined,
|
|
1621
|
+
});
|
|
1622
|
+
console.log('⛔ Signature declined — deal blocker raised.');
|
|
1623
|
+
printJSON(res.data);
|
|
1624
|
+
} catch (err) {
|
|
1625
|
+
handleError(err);
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
contractsCmd
|
|
1630
|
+
.command('remind-signatory <id>')
|
|
1631
|
+
.description('Send a reminder to a pending signatory')
|
|
1632
|
+
.requiredOption('--contact-id <uuid>', 'Contact UUID of the signatory to remind')
|
|
1633
|
+
.action(async (id, opts) => {
|
|
1634
|
+
const globalOpts = program.opts();
|
|
1635
|
+
const client = getClient(globalOpts);
|
|
1636
|
+
try {
|
|
1637
|
+
const listRes = await client.get(`/contracts/${id}/signatories`);
|
|
1638
|
+
const signatories = listRes.data.data.signatories || [];
|
|
1639
|
+
const sig = signatories.find(s => s.contact_id === opts.contactId);
|
|
1640
|
+
if (!sig) {
|
|
1641
|
+
console.error(`No signatory found for contact ${opts.contactId} on contract ${id}`);
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1644
|
+
const res = await client.post(`/contracts/${id}/signatories/${sig.id}/remind`);
|
|
1645
|
+
console.log(`📩 Reminder sent to ${sig.contact_name} (${sig.contact_email})`);
|
|
1646
|
+
printJSON(res.data);
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
handleError(err);
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
contractsCmd
|
|
1653
|
+
.command('remind-all-pending <id>')
|
|
1654
|
+
.description('Send reminders to all pending signatories on a contract')
|
|
1655
|
+
.action(async (id) => {
|
|
1656
|
+
const globalOpts = program.opts();
|
|
1657
|
+
const client = getClient(globalOpts);
|
|
1658
|
+
try {
|
|
1659
|
+
const res = await client.post(`/contracts/${id}/signatories/remind-all-pending`);
|
|
1660
|
+
const { reminded } = res.data.data;
|
|
1661
|
+
console.log(`📩 Reminders sent to ${reminded} pending signator${reminded === 1 ? 'y' : 'ies'}.`);
|
|
1507
1662
|
printJSON(res.data);
|
|
1508
|
-
|
|
1509
1663
|
} catch (err) {
|
|
1510
1664
|
handleError(err);
|
|
1511
1665
|
}
|