@startanaicompany/crm 2.15.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 +420 -29
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -580,21 +580,40 @@ leadsCmd
580
580
  });
581
581
 
582
582
  leadsCmd
583
- .command('score')
584
- .description('Set lead score (0-100). Conventions: 0-19 Cold, 20-39 Warm, 40-59 Developing, 60-79 Qualified, 80-100 Hot')
585
- .requiredOption('--id <uuid>', 'Lead UUID')
586
- .requiredOption('--score <n>', 'Score value (0-100 integer)')
587
- .option('--reason <text>', 'Score reason/explanation')
588
- .action(async (opts) => {
583
+ .command('score <lead-id>')
584
+ .description('Get or set lead score. Without --set shows current score + history. Score conventions: 0-19 Cold, 20-39 Warm, 40-59 Developing, 60-79 Qualified, 80-100 Hot (Sprint 53 T1)')
585
+ .option('--set <n>', 'Set score to this value (0-100 integer)')
586
+ .option('--reason <text>', 'Score reason/explanation (used with --set)')
587
+ .action(async (leadId, opts) => {
589
588
  const globalOpts = program.opts();
590
589
  const client = getClient(globalOpts);
591
- const body = {
592
- score: parseInt(opts.score),
593
- ...(opts.reason && { score_reason: opts.reason })
594
- };
595
590
  try {
596
- const res = await client.patch(`/leads/${opts.id}/score`, body);
597
- printJSON(res.data);
591
+ if (opts.set !== undefined) {
592
+ // SET mode: PATCH /leads/:id/score
593
+ const body = { score: parseInt(opts.set) };
594
+ if (opts.reason) body.score_reason = opts.reason;
595
+ const res = await client.patch(`/leads/${leadId}/score`, body);
596
+ const d = res.data.data;
597
+ console.log(`Score set to ${d.score}${d.score_reason ? ' — ' + d.score_reason : ''}`);
598
+ printJSON(res.data);
599
+ } else {
600
+ // GET mode: show current score + history
601
+ const [leadRes, histRes] = await Promise.all([
602
+ client.get(`/leads/${leadId}`),
603
+ client.get(`/leads/${leadId}/score-history`),
604
+ ]);
605
+ const lead = leadRes.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 || [];
608
+ const label = lead.score >= 80 ? 'Hot' : lead.score >= 60 ? 'Qualified' : lead.score >= 40 ? 'Developing' : lead.score >= 20 ? 'Warm' : 'Cold';
609
+ console.log(`\nLead: ${lead.name} <${lead.email}>`);
610
+ console.log(`Current Score: ${lead.score ?? 'N/A'} (${label})`);
611
+ if (lead.score_reason) console.log(`Reason: ${lead.score_reason}`);
612
+ if (history.length > 0) {
613
+ console.log(`\nScore History (${history.length} entries):`);
614
+ history.slice(0, 10).forEach(h => console.log(` ${new Date(h.changed_at).toLocaleString()} — ${h.previous_score ?? '?'} → ${h.new_score} (${h.changed_by || 'system'})`));
615
+ }
616
+ }
598
617
  } catch (err) {
599
618
  handleError(err);
600
619
  }
@@ -708,13 +727,16 @@ leadsCmd
708
727
  });
709
728
 
710
729
  leadsCmd
711
- .command('assign <id> <agent>')
712
- .description('Assign a lead to an agent (fires lead.assigned webhook) — S18-2')
713
- .action(async (id, agent) => {
730
+ .command('assign <id> [agent]')
731
+ .description('Assign a lead to an agent (fires lead.assigned webhook) — S18-2. Use positional [agent] or --to <agent>.')
732
+ .option('--to <agent>', 'Agent/user to assign to (alias for positional agent arg)')
733
+ .action(async (id, agent, opts) => {
714
734
  const globalOpts = program.opts();
715
735
  const client = getClient(globalOpts);
736
+ const assignTo = agent || opts.to;
737
+ if (!assignTo) { console.error('Error: specify agent as positional arg or --to <agent>'); process.exit(1); }
716
738
  try {
717
- const res = await client.patch(`/leads/${id}/assign`, { assigned_to: agent });
739
+ const res = await client.patch(`/leads/${id}/assign`, { assigned_to: assignTo });
718
740
  console.log(`Lead assigned to: ${res.data.data.assigned_to}`);
719
741
  printJSON(res.data);
720
742
  } catch (err) {
@@ -770,27 +792,67 @@ leadsCmd
770
792
  }
771
793
  });
772
794
 
773
- // leads search <term> S19-1
795
+ // Sprint 53 T1: leads stageget or set pipeline stage
774
796
  leadsCmd
775
- .command('search <term>')
776
- .description('Search leads by name, email, company, or notes')
777
- .action(async (term) => {
797
+ .command('stage <lead-id>')
798
+ .description('Get current pipeline stage or move lead to a new stage (Sprint 53 T1)')
799
+ .option('--to <stage>', 'Move lead to this pipeline stage name')
800
+ .action(async (leadId, opts) => {
778
801
  const globalOpts = program.opts();
779
802
  const client = getClient(globalOpts);
780
803
  try {
781
- const res = await client.get('/leads', { params: { q: term, per_page: 50 } });
804
+ if (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 });
807
+ const d = res.data.data;
808
+ console.log(`Stage set to: ${d.pipeline_stage}`);
809
+ printJSON(res.data);
810
+ } else {
811
+ // GET mode: show current stage
812
+ const leadRes = await client.get(`/leads/${leadId}`);
813
+ const lead = leadRes.data.data;
814
+ console.log(`\nLead: ${lead.name} <${lead.email}>`);
815
+ console.log(`Current Stage: ${lead.pipeline_stage || 'N/A'}`);
816
+ if (lead.pipeline_id) {
817
+ try {
818
+ const pipeRes = await client.get(`/pipelines/${lead.pipeline_id}/stages`);
819
+ const stages = pipeRes.data.data || [];
820
+ if (stages.length > 0) {
821
+ console.log(`Available stages: ${stages.map(s => s.name).join(' → ')}`);
822
+ }
823
+ } catch (_) { /* pipeline stages not critical */ }
824
+ }
825
+ }
826
+ } catch (err) {
827
+ handleError(err);
828
+ }
829
+ });
830
+
831
+ // leads search <term> — S19-1 / Sprint 53 T3: added --q flag alias
832
+ leadsCmd
833
+ .command('search [term]')
834
+ .description('Search leads by name, email, company, or notes. Use positional term or --q flag.')
835
+ .option('--q <keyword>', 'Search keyword (alias for positional term)')
836
+ .option('--fields <fields>', 'Comma-separated fields to display (e.g. name,email,company)')
837
+ .option('--limit <n>', 'Max results', '50')
838
+ .action(async (term, opts) => {
839
+ const globalOpts = program.opts();
840
+ const client = getClient(globalOpts);
841
+ const query = term || opts.q;
842
+ if (!query) { console.error('Error: provide a search term (positional or --q)'); process.exit(1); }
843
+ try {
844
+ const res = await client.get('/leads', { params: { q: query, per_page: parseInt(opts.limit) } });
782
845
  const leads = res.data.data || [];
783
846
  if (leads.length === 0) {
784
- console.log('No leads found matching: ' + term);
847
+ console.log('No leads found matching: ' + query);
785
848
  return;
786
849
  }
787
- const rows = leads.map(l => ({
788
- id: l.id,
789
- name: l.name,
790
- email: l.email,
791
- company: l.company || '—',
792
- status: l.status
793
- }));
850
+ const fields = opts.fields ? opts.fields.split(',').map(f => f.trim()) : ['id', 'name', 'email', 'company', 'status'];
851
+ const rows = leads.map(l => {
852
+ const row = {};
853
+ fields.forEach(f => { row[f] = l[f] ?? '—'; });
854
+ return row;
855
+ });
794
856
  console.table(rows);
795
857
  } catch (err) {
796
858
  handleError(err);
@@ -1120,6 +1182,149 @@ leadsCmd
1120
1182
  } catch (err) { handleError(err); }
1121
1183
  });
1122
1184
 
1185
+ // Sprint 53 T3: leads due — follow-up due/overdue list
1186
+ leadsCmd
1187
+ .command('due')
1188
+ .description('List leads with upcoming or overdue follow-ups (Sprint 53 T3)')
1189
+ .option('--today', 'Leads with follow-up scheduled for today')
1190
+ .option('--overdue', 'Leads with overdue follow-ups (follow_up_at < now)')
1191
+ .option('--assigned-to-me', 'Filter to leads assigned to the current API key name')
1192
+ .option('--limit <n>', 'Max results', '50')
1193
+ .action(async (opts) => {
1194
+ const globalOpts = program.opts();
1195
+ const client = getClient(globalOpts);
1196
+ try {
1197
+ const params = { per_page: parseInt(opts.limit) };
1198
+ if (opts.overdue) params.follow_up_overdue = 'true';
1199
+ if (opts.today) params.follow_up_today = 'true';
1200
+ if (!opts.overdue && !opts.today) {
1201
+ // default: both overdue + today
1202
+ params.follow_up_overdue = 'true';
1203
+ }
1204
+ const res = await client.get('/leads', { params });
1205
+ const leads = res.data.data || [];
1206
+ if (leads.length === 0) { console.log('No leads with due/overdue follow-ups.'); return; }
1207
+ leads.forEach(l => {
1208
+ const fu = l.follow_up_at ? new Date(l.follow_up_at).toLocaleString() : 'N/A';
1209
+ const overdue = l.follow_up_at && new Date(l.follow_up_at) < new Date() ? ' ⚠️ OVERDUE' : '';
1210
+ console.log(`[${l.id}] ${l.name} <${l.email}> — follow_up: ${fu}${overdue} (${l.assigned_to || 'unassigned'})`);
1211
+ });
1212
+ } catch (err) { handleError(err); }
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
+
1123
1328
  // Sprint 52 T2: leads escalate + leads unassign
1124
1329
  leadsCmd
1125
1330
  .command('escalate <lead-id>')
@@ -3088,6 +3293,59 @@ webhooksCmd
3088
3293
  }
3089
3294
  });
3090
3295
 
3296
+ // Sprint 53 T2: webhooks test / logs / retry
3297
+ webhooksCmd
3298
+ .command('test <webhook-id>')
3299
+ .description('Send a test payload to a webhook and check delivery (Sprint 53 T2)')
3300
+ .action(async (webhookId) => {
3301
+ const globalOpts = program.opts();
3302
+ const client = getClient(globalOpts);
3303
+ try {
3304
+ const res = await client.post(`/webhooks/${webhookId}/test`);
3305
+ const d = res.data.data;
3306
+ console.log(`Test queued — event: ${d.event}, webhook_id: ${d.webhook_id}`);
3307
+ printJSON(res.data);
3308
+ } catch (err) { handleError(err); }
3309
+ });
3310
+
3311
+ webhooksCmd
3312
+ .command('logs <webhook-id>')
3313
+ .description('View webhook delivery history (Sprint 53 T2)')
3314
+ .option('--limit <n>', 'Max results (default 20)', '20')
3315
+ .option('--status <status>', 'Filter by status: success or failed')
3316
+ .action(async (webhookId, opts) => {
3317
+ const globalOpts = program.opts();
3318
+ const client = getClient(globalOpts);
3319
+ try {
3320
+ const params = { limit: parseInt(opts.limit) };
3321
+ if (opts.status) params.status = opts.status;
3322
+ const res = await client.get(`/webhooks/${webhookId}/deliveries`, { params });
3323
+ const deliveries = res.data.data || [];
3324
+ if (deliveries.length === 0) { console.log('No delivery logs found.'); return; }
3325
+ deliveries.forEach(d => {
3326
+ const icon = d.success ? '✅' : '❌';
3327
+ const retry = d.next_retry_at ? ` | retry: ${new Date(d.next_retry_at).toLocaleString()}` : '';
3328
+ console.log(`${icon} [${d.id}] ${d.event} — HTTP ${d.status_code || '?'} at ${new Date(d.attempted_at).toLocaleString()}${retry}`);
3329
+ if (!d.success && d.error_message) console.log(` Error: ${d.error_message}`);
3330
+ });
3331
+ const p = res.data.pagination;
3332
+ if (p) console.log(`\nShowing ${deliveries.length} of ${p.total} total deliveries.`);
3333
+ } catch (err) { handleError(err); }
3334
+ });
3335
+
3336
+ webhooksCmd
3337
+ .command('retry <webhook-id> <delivery-id>')
3338
+ .description('Retry a failed webhook delivery (Sprint 53 T2)')
3339
+ .action(async (webhookId, deliveryId) => {
3340
+ const globalOpts = program.opts();
3341
+ const client = getClient(globalOpts);
3342
+ try {
3343
+ const res = await client.post(`/webhooks/${webhookId}/deliveries/${deliveryId}/retry`);
3344
+ console.log(`Retry queued for delivery ${deliveryId}`);
3345
+ printJSON(res.data);
3346
+ } catch (err) { handleError(err); }
3347
+ });
3348
+
3091
3349
  // ── stages ──────────────────────────────────────────────────────────
3092
3350
  const stagesCmd = program.command('stages').description('Manage pipeline stages (admin)');
3093
3351
 
@@ -3427,6 +3685,139 @@ sequencesCmd
3427
3685
  } catch (err) { handleError(err); }
3428
3686
  });
3429
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
+
3430
3821
  // ============================================================
3431
3822
  // GDPR COMMANDS — Sprint 50 T2
3432
3823
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.15.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": {