@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.
Files changed (2) hide show
  1. package/index.js +160 -6
  2. 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('2.3.0')
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 | qualified | proposal | negotiation | closed_won | closed_lost')
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('Sign a contract (auto-moves linked lead to closed_won)')
1500
- .option('--signed-by <name>', 'Name of signatory')
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
- const res = await client.post(`/contracts/${id}/status`, { status: 'signed', signed_by: opts.signedBy });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "AI-first CRM CLI \u2014 manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {