@startanaicompany/crm 2.16.0 → 2.17.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 +250 -3
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -603,7 +603,8 @@ leadsCmd
603
603
  client.get(`/leads/${leadId}/score-history`),
604
604
  ]);
605
605
  const lead = leadRes.data.data;
606
- const history = histRes.data.data || [];
606
+ // Bug 00d0c981: score-history returns { history: [], total: N } not a bare array
607
+ const history = histRes.data.data?.history || histRes.data.data || [];
607
608
  const label = lead.score >= 80 ? 'Hot' : lead.score >= 60 ? 'Qualified' : lead.score >= 40 ? 'Developing' : lead.score >= 20 ? 'Warm' : 'Cold';
608
609
  console.log(`\nLead: ${lead.name} <${lead.email}>`);
609
610
  console.log(`Current Score: ${lead.score ?? 'N/A'} (${label})`);
@@ -801,8 +802,8 @@ leadsCmd
801
802
  const client = getClient(globalOpts);
802
803
  try {
803
804
  if (opts.to) {
804
- // SET mode: PATCH /leads/:id with pipeline_stage
805
- const res = await client.patch(`/leads/${leadId}`, { pipeline_stage: opts.to });
805
+ // Bug 6d850c25: use PATCH /leads/:id/stage with { stage } not /leads/:id with { pipeline_stage }
806
+ const res = await client.patch(`/leads/${leadId}/stage`, { stage: opts.to });
806
807
  const d = res.data.data;
807
808
  console.log(`Stage set to: ${d.pipeline_stage}`);
808
809
  printJSON(res.data);
@@ -1211,6 +1212,119 @@ leadsCmd
1211
1212
  } catch (err) { handleError(err); }
1212
1213
  });
1213
1214
 
1215
+ // Sprint 54 T1: leads merge — merge secondary lead into primary
1216
+ leadsCmd
1217
+ .command('merge <primary-id> <secondary-id>')
1218
+ .description('Merge secondary lead into primary lead (Sprint 54 T1). Use --dry-run to preview without committing.')
1219
+ .option('--dry-run', 'Preview what would be merged without committing changes', false)
1220
+ .action(async (primaryId, secondaryId, opts) => {
1221
+ const globalOpts = program.opts();
1222
+ const client = getClient(globalOpts);
1223
+ try {
1224
+ if (opts.dryRun) {
1225
+ // Dry-run: fetch both leads and show diff without calling merge
1226
+ const [primaryRes, secondaryRes] = await Promise.all([
1227
+ client.get(`/leads/${primaryId}`),
1228
+ client.get(`/leads/${secondaryId}`),
1229
+ ]);
1230
+ const primary = primaryRes.data.data;
1231
+ const secondary = secondaryRes.data.data;
1232
+ console.log('\n[DRY RUN] Merge preview:');
1233
+ console.log(` Primary : [${primary.id}] ${primary.name} <${primary.email}> — ${primary.status}`);
1234
+ console.log(` Secondary: [${secondary.id}] ${secondary.name} <${secondary.email}> — ${secondary.status}`);
1235
+ console.log('\n What would happen:');
1236
+ console.log(` - Notes, communications, and audit history from secondary copied to primary`);
1237
+ console.log(` - Secondary lead soft-deleted (status → merged)`);
1238
+ console.log(` - Primary lead retained with merged data`);
1239
+ if (secondary.tags && secondary.tags.length > 0) {
1240
+ console.log(` - Tags from secondary that will be merged: ${secondary.tags.join(', ')}`);
1241
+ }
1242
+ console.log('\n Run without --dry-run to commit the merge.');
1243
+ } else {
1244
+ const res = await client.post(`/leads/${primaryId}/merge`, { merge_into: secondaryId });
1245
+ console.log(`Merged lead ${secondaryId} into ${primaryId}.`);
1246
+ printJSON(res.data);
1247
+ }
1248
+ } catch (err) {
1249
+ handleError(err);
1250
+ }
1251
+ });
1252
+
1253
+ // Sprint 54 T1: leads bulk-update --ids — ID-based bulk status update (distinct from filter-based bulk-update)
1254
+ leadsCmd
1255
+ .command('bulk-update-ids')
1256
+ .description('Bulk update status for specific leads by ID list (Sprint 54 T1). Up to 100 leads per call.')
1257
+ .requiredOption('--ids <ids>', 'Comma-separated lead IDs to update')
1258
+ .requiredOption('--status <status>', 'New status: new|contacted|qualified|unresponsive|converted|lost')
1259
+ .option('--close-reason <reason>', 'Close reason (required when --status lost)')
1260
+ .action(async (opts) => {
1261
+ const globalOpts = program.opts();
1262
+ const client = getClient(globalOpts);
1263
+ const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
1264
+ if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
1265
+ const updates = { status: opts.status };
1266
+ if (opts.closeReason) updates.close_reason = opts.closeReason;
1267
+ try {
1268
+ const res = await client.patch('/leads/bulk', { ids, updates });
1269
+ const d = res.data.data || {};
1270
+ console.log(`Updated: ${d.updated !== undefined ? d.updated : ids.length} lead(s).`);
1271
+ if (d.failed && d.failed.length > 0) {
1272
+ console.log(`Failed IDs: ${d.failed.join(', ')}`);
1273
+ }
1274
+ printJSON(res.data);
1275
+ } catch (err) {
1276
+ handleError(err);
1277
+ }
1278
+ });
1279
+
1280
+ // Sprint 54 T1: leads bulk-tag — add a tag to multiple leads by ID
1281
+ leadsCmd
1282
+ .command('bulk-tag')
1283
+ .description('Add a tag to multiple leads by ID list (Sprint 54 T1). Up to 100 leads per call.')
1284
+ .requiredOption('--tag <tag>', 'Tag to add to each lead')
1285
+ .requiredOption('--ids <ids>', 'Comma-separated lead IDs')
1286
+ .action(async (opts) => {
1287
+ const globalOpts = program.opts();
1288
+ const client = getClient(globalOpts);
1289
+ const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
1290
+ if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
1291
+ try {
1292
+ const res = await client.patch('/leads/bulk', { ids, updates: { tags: [opts.tag] } });
1293
+ const d = res.data.data || {};
1294
+ console.log(`Tagged ${d.updated !== undefined ? d.updated : ids.length} lead(s) with "${opts.tag}".`);
1295
+ if (d.failed && d.failed.length > 0) {
1296
+ console.log(`Failed IDs: ${d.failed.join(', ')}`);
1297
+ }
1298
+ printJSON(res.data);
1299
+ } catch (err) {
1300
+ handleError(err);
1301
+ }
1302
+ });
1303
+
1304
+ // Sprint 54 T1: leads bulk-assign — reassign multiple leads to an agent by ID
1305
+ leadsCmd
1306
+ .command('bulk-assign')
1307
+ .description('Bulk reassign leads to an agent by ID list (Sprint 54 T1). Up to 100 leads per call.')
1308
+ .requiredOption('--to <agent>', 'Agent/user to assign leads to')
1309
+ .requiredOption('--ids <ids>', 'Comma-separated lead IDs')
1310
+ .action(async (opts) => {
1311
+ const globalOpts = program.opts();
1312
+ const client = getClient(globalOpts);
1313
+ const ids = opts.ids.split(',').map(s => s.trim()).filter(Boolean);
1314
+ if (ids.length === 0) { console.error('Error: --ids requires at least one ID'); process.exit(1); }
1315
+ try {
1316
+ const res = await client.patch('/leads/bulk', { ids, updates: { assigned_to: opts.to } });
1317
+ const d = res.data.data || {};
1318
+ console.log(`Assigned ${d.updated !== undefined ? d.updated : ids.length} lead(s) to "${opts.to}".`);
1319
+ if (d.failed && d.failed.length > 0) {
1320
+ console.log(`Failed IDs: ${d.failed.join(', ')}`);
1321
+ }
1322
+ printJSON(res.data);
1323
+ } catch (err) {
1324
+ handleError(err);
1325
+ }
1326
+ });
1327
+
1214
1328
  // Sprint 52 T2: leads escalate + leads unassign
1215
1329
  leadsCmd
1216
1330
  .command('escalate <lead-id>')
@@ -3571,6 +3685,139 @@ sequencesCmd
3571
3685
  } catch (err) { handleError(err); }
3572
3686
  });
3573
3687
 
3688
+ // Sprint 54 T3: sequences stats <sequenceId>
3689
+ sequencesCmd
3690
+ .command('stats <sequenceId>')
3691
+ .description('Get enrollment stats for a sequence (Sprint 54 T3)')
3692
+ .action(async (sequenceId) => {
3693
+ const globalOpts = program.opts();
3694
+ const client = getClient(globalOpts);
3695
+ try {
3696
+ const res = await client.get(`/sequences/${sequenceId}/stats`);
3697
+ const d = res.data.data || {};
3698
+ console.log(`\nSequence: ${d.sequence_name} [${d.sequence_id}]`);
3699
+ console.log(`Active: ${d.active ? 'yes' : 'no'} Steps: ${d.step_count}`);
3700
+ console.log(`Total enrolled: ${d.total_enrolled}`);
3701
+ if (d.by_status) {
3702
+ console.log(` active: ${d.by_status.active}`);
3703
+ console.log(` completed: ${d.by_status.completed}`);
3704
+ console.log(` unsubscribed: ${d.by_status.unsubscribed}`);
3705
+ console.log(` paused: ${d.by_status.paused}`);
3706
+ console.log(` failed: ${d.by_status.failed}`);
3707
+ }
3708
+ if (d.first_enrollment) console.log(`First enrollment: ${new Date(d.first_enrollment).toLocaleString()}`);
3709
+ if (d.last_enrollment) console.log(`Last enrollment: ${new Date(d.last_enrollment).toLocaleString()}`);
3710
+ } catch (err) { handleError(err); }
3711
+ });
3712
+
3713
+ // Sprint 54 T3: leads sequences <lead-id>
3714
+ leadsCmd
3715
+ .command('sequences <lead-id>')
3716
+ .description('List sequences a lead is enrolled in (Sprint 54 T3)')
3717
+ .action(async (leadId) => {
3718
+ const globalOpts = program.opts();
3719
+ const client = getClient(globalOpts);
3720
+ try {
3721
+ const res = await client.get(`/leads/${leadId}/sequences`);
3722
+ const enrollments = res.data.data || res.data || [];
3723
+ if (enrollments.length === 0) { console.log('Lead is not enrolled in any sequences.'); return; }
3724
+ enrollments.forEach(e => {
3725
+ const step = e.current_step || 'N/A';
3726
+ const next = e.next_send_at ? new Date(e.next_send_at).toLocaleString() : 'N/A';
3727
+ console.log(`[${e.sequence_id}] ${e.sequence_name} — status: ${e.status}, step: ${step}, next: ${next}`);
3728
+ });
3729
+ } catch (err) { handleError(err); }
3730
+ });
3731
+
3732
+ // ============================================================
3733
+ // FORMS COMMANDS — Sprint 54 T2
3734
+ // ============================================================
3735
+ const formsCmd = program.command('forms').description('Manage lead capture forms (Sprint 54 T2)');
3736
+
3737
+ formsCmd
3738
+ .command('list')
3739
+ .description('List all forms in the workspace')
3740
+ .action(async () => {
3741
+ const globalOpts = program.opts();
3742
+ const client = getClient(globalOpts);
3743
+ try {
3744
+ const res = await client.get('/apikey/forms');
3745
+ const forms = res.data.data || res.data || [];
3746
+ if (forms.length === 0) { console.log('No forms found.'); return; }
3747
+ forms.forEach(f => {
3748
+ console.log(`[${f.id}] ${f.name} — submissions: ${f.submission_count || 0} — created: ${new Date(f.created_at).toLocaleDateString()}`);
3749
+ });
3750
+ } catch (err) { handleError(err); }
3751
+ });
3752
+
3753
+ formsCmd
3754
+ .command('get <form-id>')
3755
+ .description('Get form details by ID')
3756
+ .action(async (formId) => {
3757
+ const globalOpts = program.opts();
3758
+ const client = getClient(globalOpts);
3759
+ try {
3760
+ const res = await client.get(`/apikey/forms/${formId}`);
3761
+ printJSON(res.data);
3762
+ } catch (err) { handleError(err); }
3763
+ });
3764
+
3765
+ formsCmd
3766
+ .command('create')
3767
+ .description('Create a new lead capture form')
3768
+ .requiredOption('--name <name>', 'Form name')
3769
+ .option('--success-message <msg>', 'Message shown after submission', 'Thank you! We will be in touch.')
3770
+ .option('--redirect-url <url>', 'Redirect URL after submission')
3771
+ .option('--fields <json>', 'JSON array of field definitions (e.g. \'[{"key":"email","label":"Email","type":"email","required":true}]\')')
3772
+ .action(async (opts) => {
3773
+ const globalOpts = program.opts();
3774
+ const client = getClient(globalOpts);
3775
+ let fields = [];
3776
+ if (opts.fields) {
3777
+ try { fields = JSON.parse(opts.fields); } catch (e) { console.error('Error: --fields must be valid JSON array'); process.exit(1); }
3778
+ }
3779
+ try {
3780
+ const body = { name: opts.name, fields, success_message: opts.successMessage };
3781
+ if (opts.redirectUrl) body.redirect_url = opts.redirectUrl;
3782
+ const res = await client.post('/apikey/forms', body);
3783
+ console.log(`Form created: ${res.data.data?.id || res.data?.id}`);
3784
+ printJSON(res.data);
3785
+ } catch (err) { handleError(err); }
3786
+ });
3787
+
3788
+ formsCmd
3789
+ .command('submissions <form-id>')
3790
+ .description('List submissions for a form')
3791
+ .option('--limit <n>', 'Max results', '50')
3792
+ .option('--offset <n>', 'Pagination offset', '0')
3793
+ .action(async (formId, opts) => {
3794
+ const globalOpts = program.opts();
3795
+ const client = getClient(globalOpts);
3796
+ try {
3797
+ const res = await client.get(`/apikey/forms/${formId}/submissions`, { params: { limit: opts.limit, offset: opts.offset } });
3798
+ const d = res.data.data || {};
3799
+ const subs = d.data || [];
3800
+ if (subs.length === 0) { console.log('No submissions found.'); return; }
3801
+ console.log(`Total: ${d.total || subs.length}`);
3802
+ subs.forEach(s => {
3803
+ const when = s.submitted_at ? new Date(s.submitted_at).toLocaleString() : 'N/A';
3804
+ console.log(`[${s.id}] ${s.lead_name || 'Unknown'} <${s.lead_email || ''}> — ${when}`);
3805
+ });
3806
+ } catch (err) { handleError(err); }
3807
+ });
3808
+
3809
+ formsCmd
3810
+ .command('delete <form-id>')
3811
+ .description('Delete a form by ID')
3812
+ .action(async (formId) => {
3813
+ const globalOpts = program.opts();
3814
+ const client = getClient(globalOpts);
3815
+ try {
3816
+ await client.delete(`/apikey/forms/${formId}`);
3817
+ console.log(`Form ${formId} deleted.`);
3818
+ } catch (err) { handleError(err); }
3819
+ });
3820
+
3574
3821
  // ============================================================
3575
3822
  // GDPR COMMANDS — Sprint 50 T2
3576
3823
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "AI-first CRM CLI — manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {