@startanaicompany/crm 2.13.1 → 2.15.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 +285 -6
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1020,6 +1020,194 @@ program
1020
1020
  }
1021
1021
  });
1022
1022
 
1023
+ // Sprint 51 T1: leads notes subcommands
1024
+ const leadsNotesCmd = leadsCmd.command('notes').description('Manage notes on a lead (Sprint 51 T1)');
1025
+
1026
+ leadsNotesCmd
1027
+ .command('add <lead-id>')
1028
+ .description('Add a note to a lead')
1029
+ .requiredOption('--body <text>', 'Note body text')
1030
+ .option('--mention <agent>', 'Agent name to @mention (repeatable)', (v, prev) => prev.concat([v]), [])
1031
+ .option('--type <type>', 'Note type: general|call_summary|email_summary|qualification_note|action_taken|handoff', 'general')
1032
+ .option('--pin', 'Pin note to top', false)
1033
+ .action(async (leadId, opts) => {
1034
+ const globalOpts = program.opts();
1035
+ const client = getClient(globalOpts);
1036
+ try {
1037
+ const res = await client.post(`/leads/${leadId}/notes`, {
1038
+ content: opts.body,
1039
+ mentions: opts.mention.length > 0 ? opts.mention : undefined,
1040
+ note_type: opts.type,
1041
+ is_pinned: opts.pin || undefined,
1042
+ });
1043
+ printJSON(res.data);
1044
+ } catch (err) { handleError(err); }
1045
+ });
1046
+
1047
+ leadsNotesCmd
1048
+ .command('list <lead-id>')
1049
+ .description('List all notes for a lead (pinned first, then reverse-chron)')
1050
+ .option('--limit <n>', 'Max results (server returns all by default)')
1051
+ .action(async (leadId, opts) => {
1052
+ const globalOpts = program.opts();
1053
+ const client = getClient(globalOpts);
1054
+ try {
1055
+ const params = {};
1056
+ if (opts.limit) params.limit = parseInt(opts.limit);
1057
+ const res = await client.get(`/leads/${leadId}/notes`, { params });
1058
+ const notes = res.data.data || [];
1059
+ if (notes.length === 0) { console.log('No notes found.'); return; }
1060
+ notes.forEach(n => {
1061
+ const pin = n.is_pinned ? ' 📌' : '';
1062
+ const mentions = n.mentions && n.mentions.length > 0 ? ` [@${n.mentions.join(', @')}]` : '';
1063
+ console.log(`[${n.id}]${pin} ${n.note_type} — ${new Date(n.created_at).toLocaleString()} by ${n.created_by || 'unknown'}`);
1064
+ console.log(` ${n.content}${mentions}`);
1065
+ });
1066
+ } catch (err) { handleError(err); }
1067
+ });
1068
+
1069
+ leadsNotesCmd
1070
+ .command('get <lead-id> <note-id>')
1071
+ .description('Get a single note by ID')
1072
+ .action(async (leadId, noteId) => {
1073
+ const globalOpts = program.opts();
1074
+ const client = getClient(globalOpts);
1075
+ try {
1076
+ const res = await client.get(`/leads/${leadId}/notes/${noteId}`);
1077
+ printJSON(res.data);
1078
+ } catch (err) { handleError(err); }
1079
+ });
1080
+
1081
+ // Sprint 51 T2: leads watch / unwatch / watched
1082
+ leadsCmd
1083
+ .command('watch <lead-id>')
1084
+ .description('Watch a lead — receive lead.watched_change webhook events (Sprint 51 T2)')
1085
+ .action(async (leadId) => {
1086
+ const globalOpts = program.opts();
1087
+ const client = getClient(globalOpts);
1088
+ try {
1089
+ const res = await client.post(`/leads/${leadId}/watch`);
1090
+ console.log(`Watching lead ${leadId}`);
1091
+ printJSON(res.data);
1092
+ } catch (err) { handleError(err); }
1093
+ });
1094
+
1095
+ leadsCmd
1096
+ .command('unwatch <lead-id>')
1097
+ .description('Stop watching a lead (Sprint 51 T2)')
1098
+ .action(async (leadId) => {
1099
+ const globalOpts = program.opts();
1100
+ const client = getClient(globalOpts);
1101
+ try {
1102
+ const res = await client.delete(`/leads/${leadId}/watch`);
1103
+ console.log(`Unwatched lead ${leadId}`);
1104
+ printJSON(res.data);
1105
+ } catch (err) { handleError(err); }
1106
+ });
1107
+
1108
+ leadsCmd
1109
+ .command('watched')
1110
+ .description('List all leads currently watched by this API key (Sprint 51 T2)')
1111
+ .option('--limit <n>', 'Max results (default 50)', '50')
1112
+ .action(async (opts) => {
1113
+ const globalOpts = program.opts();
1114
+ const client = getClient(globalOpts);
1115
+ try {
1116
+ const res = await client.get('/leads/watched', { params: { limit: parseInt(opts.limit) } });
1117
+ const leads = res.data.data || res.data;
1118
+ if (!leads || leads.length === 0) { console.log('No watched leads.'); return; }
1119
+ leads.forEach(l => console.log(`[${l.id}] ${l.name} <${l.email}> — ${l.status}`));
1120
+ } catch (err) { handleError(err); }
1121
+ });
1122
+
1123
+ // Sprint 52 T2: leads escalate + leads unassign
1124
+ leadsCmd
1125
+ .command('escalate <lead-id>')
1126
+ .description('Manually escalate a lead — sets escalated=true and fires lead.escalated webhook (Sprint 52 T2)')
1127
+ .option('--reason <text>', 'Reason for escalation')
1128
+ .option('--to <agent>', 'Reassign to this agent/user on escalation')
1129
+ .action(async (leadId, opts) => {
1130
+ const globalOpts = program.opts();
1131
+ const client = getClient(globalOpts);
1132
+ try {
1133
+ const body = {};
1134
+ if (opts.reason) body.reason = opts.reason;
1135
+ if (opts.to) body.to = opts.to;
1136
+ const res = await client.post(`/leads/${leadId}/escalate`, body);
1137
+ console.log(`Lead ${leadId} escalated.`);
1138
+ printJSON(res.data);
1139
+ } catch (err) { handleError(err); }
1140
+ });
1141
+
1142
+ leadsCmd
1143
+ .command('unassign <lead-id>')
1144
+ .description('Remove assignment from a lead (sets assigned_to to null) (Sprint 52 T2)')
1145
+ .action(async (leadId) => {
1146
+ const globalOpts = program.opts();
1147
+ const client = getClient(globalOpts);
1148
+ try {
1149
+ const res = await client.patch(`/leads/${leadId}`, { assigned_to: null });
1150
+ console.log(`Lead ${leadId} unassigned.`);
1151
+ printJSON(res.data);
1152
+ } catch (err) { handleError(err); }
1153
+ });
1154
+
1155
+ // Sprint 51 T3: leads export / leads import
1156
+ leadsCmd
1157
+ .command('export')
1158
+ .description('Export leads to CSV or JSON (Sprint 51 T3)')
1159
+ .option('--format <format>', 'Output format: csv or json', 'csv')
1160
+ .option('--output <file>', 'Save to this file (default: leads-export.<ext>)')
1161
+ .option('--status <status>', 'Filter by status')
1162
+ .option('--pipeline-stage <stage>', 'Filter by pipeline stage')
1163
+ .option('--assigned-to <agent>', 'Filter by assigned agent')
1164
+ .option('--q <term>', 'Search term')
1165
+ .action(async (opts) => {
1166
+ const globalOpts = program.opts();
1167
+ const client = getClient(globalOpts);
1168
+ try {
1169
+ const fmt = opts.format === 'json' ? 'json' : 'csv';
1170
+ const params = { format: fmt };
1171
+ if (opts.status) params.status = opts.status;
1172
+ if (opts.pipelineStage) params.pipeline_stage = opts.pipelineStage;
1173
+ if (opts.assignedTo) params.assigned_to = opts.assignedTo;
1174
+ if (opts.q) params.q = opts.q;
1175
+ const res = await client.get('/leads/export', { params, responseType: fmt === 'json' ? 'text' : 'text' });
1176
+ const outFile = opts.output || `leads-export.${fmt}`;
1177
+ fs.writeFileSync(outFile, res.data);
1178
+ console.log(`Exported ${outFile}`);
1179
+ } catch (err) { handleError(err); }
1180
+ });
1181
+
1182
+ leadsCmd
1183
+ .command('import')
1184
+ .description('Import leads from CSV file (Sprint 51 T3). Required columns: name, email. Optional: phone, company, status, deal_value, tags (semicolon-separated).')
1185
+ .requiredOption('--file <path>', 'Path to CSV file')
1186
+ .option('--dry-run', 'Validate without writing to database', false)
1187
+ .action(async (opts) => {
1188
+ const globalOpts = program.opts();
1189
+ const client = getClient(globalOpts);
1190
+ try {
1191
+ const csvData = fs.readFileSync(opts.file, 'utf8');
1192
+ const params = {};
1193
+ if (opts.dryRun) params.dry_run = 'true';
1194
+ const res = await client.post('/leads/import', csvData, {
1195
+ params,
1196
+ headers: { 'Content-Type': 'text/csv' },
1197
+ });
1198
+ const d = res.data.data;
1199
+ if (opts.dryRun) {
1200
+ console.log(`Dry-run: ${d.imported} rows would import, ${d.failed.length} errors.`);
1201
+ } else {
1202
+ console.log(`Imported: ${d.imported} leads. Errors: ${d.failed.length}`);
1203
+ }
1204
+ if (d.failed.length > 0) {
1205
+ console.log('Errors:');
1206
+ d.failed.forEach(f => console.log(` Row ${f.row}: ${f.reason}`));
1207
+ }
1208
+ } catch (err) { handleError(err); }
1209
+ });
1210
+
1023
1211
  // ============================================================
1024
1212
  // CONTACTS
1025
1213
  // ============================================================
@@ -1031,11 +1219,14 @@ const contactsCmd = program
1031
1219
  contactsCmd
1032
1220
  .command('create')
1033
1221
  .description('Create a new contact')
1034
- .requiredOption('--first-name <name>', 'First name')
1222
+ .option('--name <name>', 'Full name (alias for --first-name)')
1223
+ .option('--first-name <name>', 'First name')
1035
1224
  .option('--last-name <name>', 'Last name')
1036
- .option('--email <email>', 'Email address (unique)')
1225
+ .requiredOption('--email <email>', 'Email address (unique)')
1037
1226
  .option('--phone <phone>', 'Phone number')
1038
- .option('--company <company>', 'Company name')
1227
+ .option('--company <company>', 'Company name (text)')
1228
+ .option('--company-id <uuid>', 'Company UUID (links to companies table)')
1229
+ .option('--lead-id <uuid>', 'Lead UUID to link to this contact on creation')
1039
1230
  .option('--title <title>', 'Job title')
1040
1231
  .option('--tags <tags>', 'Comma-separated tags')
1041
1232
  .option('--do-not-contact', 'Mark as do not contact')
@@ -1047,11 +1238,14 @@ contactsCmd
1047
1238
  const client = getClient(globalOpts, agentName);
1048
1239
  try {
1049
1240
  const body = {
1050
- first_name: opts.firstName,
1241
+ name: opts.name || opts.firstName,
1242
+ first_name: opts.firstName || opts.name,
1051
1243
  last_name: opts.lastName,
1052
1244
  email: opts.email,
1053
1245
  phone: opts.phone,
1054
1246
  company: opts.company,
1247
+ company_id: opts.companyId,
1248
+ lead_id: opts.leadId,
1055
1249
  title: opts.title,
1056
1250
  notes: opts.notes,
1057
1251
  do_not_contact: opts.doNotContact === true,
@@ -1068,8 +1262,10 @@ contactsCmd
1068
1262
  .command('list')
1069
1263
  .description('List contacts')
1070
1264
  .option('--email <email>', 'Filter by email (partial match)')
1071
- .option('--company <company>', 'Filter by company (partial match)')
1265
+ .option('--company <company>', 'Filter by company name (partial match)')
1266
+ .option('--company-id <uuid>', 'Filter by company UUID')
1072
1267
  .option('--tag <tag>', 'Filter by tag')
1268
+ .option('--limit <n>', 'Max results per page (default 20)')
1073
1269
  .option('--page <n>', 'Page number', '1')
1074
1270
  .option('--per-page <n>', 'Results per page', '20')
1075
1271
  .action(async (opts) => {
@@ -1077,8 +1273,10 @@ contactsCmd
1077
1273
  const client = getClient(globalOpts);
1078
1274
  try {
1079
1275
  const params = { page: opts.page, per_page: opts.perPage };
1276
+ if (opts.limit) params.limit = opts.limit;
1080
1277
  if (opts.email) params.email = opts.email;
1081
1278
  if (opts.company) params.company = opts.company;
1279
+ if (opts.companyId) params.company_id = opts.companyId;
1082
1280
  if (opts.tag) params.tag = opts.tag;
1083
1281
  const res = await client.get('/contacts', { params });
1084
1282
  printJSON(res.data);
@@ -1104,11 +1302,13 @@ contactsCmd
1104
1302
  contactsCmd
1105
1303
  .command('update <id>')
1106
1304
  .description('Update a contact')
1305
+ .option('--name <name>', 'Full name (alias for --first-name)')
1107
1306
  .option('--first-name <name>', 'First name')
1108
1307
  .option('--last-name <name>', 'Last name')
1109
1308
  .option('--email <email>', 'Email address')
1110
1309
  .option('--phone <phone>', 'Phone number')
1111
- .option('--company <company>', 'Company name')
1310
+ .option('--company <company>', 'Company name (text)')
1311
+ .option('--company-id <uuid>', 'Company UUID (links to companies table)')
1112
1312
  .option('--title <title>', 'Job title')
1113
1313
  .option('--role <role>', 'Role: champion|economic_buyer|technical_buyer|gatekeeper|influencer|end_user')
1114
1314
  .option('--tags <tags>', 'Comma-separated tags')
@@ -1118,11 +1318,13 @@ contactsCmd
1118
1318
  const client = getClient(globalOpts);
1119
1319
  try {
1120
1320
  const body = {};
1321
+ if (opts.name !== undefined) body.name = opts.name;
1121
1322
  if (opts.firstName !== undefined) body.first_name = opts.firstName;
1122
1323
  if (opts.lastName !== undefined) body.last_name = opts.lastName;
1123
1324
  if (opts.email !== undefined) body.email = opts.email;
1124
1325
  if (opts.phone !== undefined) body.phone = opts.phone;
1125
1326
  if (opts.company !== undefined) body.company = opts.company;
1327
+ if (opts.companyId !== undefined) body.company_id = opts.companyId;
1126
1328
  if (opts.title !== undefined) body.title = opts.title;
1127
1329
  if (opts.role !== undefined) body.role = opts.role;
1128
1330
  if (opts.tags !== undefined) body.tags = opts.tags.split(',').map(t => t.trim());
@@ -3286,4 +3488,81 @@ gdprCmd
3286
3488
  }
3287
3489
  });
3288
3490
 
3491
+ // ============================================================
3492
+ // REPORTS COMMANDS — Sprint 52 T3
3493
+ // ============================================================
3494
+ const reportsCmd = program.command('reports').description('Manage and run saved CRM reports (Sprint 52 T3)');
3495
+
3496
+ reportsCmd
3497
+ .command('list')
3498
+ .description('List all saved reports')
3499
+ .action(async () => {
3500
+ const globalOpts = program.opts();
3501
+ const client = getClient(globalOpts);
3502
+ try {
3503
+ const res = await client.get('/reports');
3504
+ const reports = res.data.data || res.data;
3505
+ if (!reports || reports.length === 0) { console.log('No reports found.'); return; }
3506
+ reports.forEach(r => console.log(`[${r.id}] ${r.name} — type: ${r.dimension}/${r.metric}, period: ${r.period}`));
3507
+ } catch (err) { handleError(err); }
3508
+ });
3509
+
3510
+ reportsCmd
3511
+ .command('create')
3512
+ .description('Create and save a report definition')
3513
+ .requiredOption('--name <name>', 'Report name')
3514
+ .option('--type <type>', 'Report type: leads (count by stage) | pipeline (deal value by pipeline) | revenue (deal value by stage)', 'leads')
3515
+ .option('--period <period>', 'Time period: 7d | 30d | 90d | 365d | all', '30d')
3516
+ .option('--filter-status <status>', 'Filter leads by status')
3517
+ .option('--filter-stage <stage>', 'Filter leads by pipeline stage')
3518
+ .option('--chart-type <type>', 'Chart type: bar | line | pie | table', 'bar')
3519
+ .action(async (opts) => {
3520
+ const globalOpts = program.opts();
3521
+ const client = getClient(globalOpts);
3522
+ try {
3523
+ const body = {
3524
+ name: opts.name,
3525
+ type: opts.type,
3526
+ period: opts.period,
3527
+ chart_type: opts.chartType,
3528
+ };
3529
+ if (opts.filterStatus) body.filter_status = opts.filterStatus;
3530
+ if (opts.filterStage) body.filter_stage = opts.filterStage;
3531
+ const res = await client.post('/reports', body);
3532
+ const r = res.data.data || res.data;
3533
+ console.log(`Report created: [${r.id}] ${r.name}`);
3534
+ printJSON(res.data);
3535
+ } catch (err) { handleError(err); }
3536
+ });
3537
+
3538
+ reportsCmd
3539
+ .command('run <report-id>')
3540
+ .description('Run a saved report and display or save results')
3541
+ .option('--output <file>', 'Save results to this JSON file')
3542
+ .action(async (reportId, opts) => {
3543
+ const globalOpts = program.opts();
3544
+ const client = getClient(globalOpts);
3545
+ try {
3546
+ const res = await client.get(`/reports/${reportId}/run`);
3547
+ if (opts.output) {
3548
+ fs.writeFileSync(opts.output, JSON.stringify(res.data, null, 2));
3549
+ console.log(`Report results saved to ${opts.output}`);
3550
+ } else {
3551
+ printJSON(res.data);
3552
+ }
3553
+ } catch (err) { handleError(err); }
3554
+ });
3555
+
3556
+ reportsCmd
3557
+ .command('delete <report-id>')
3558
+ .description('Delete a saved report')
3559
+ .action(async (reportId) => {
3560
+ const globalOpts = program.opts();
3561
+ const client = getClient(globalOpts);
3562
+ try {
3563
+ await client.delete(`/reports/${reportId}`);
3564
+ console.log(`Report ${reportId} deleted.`);
3565
+ } catch (err) { handleError(err); }
3566
+ });
3567
+
3289
3568
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.13.1",
3
+ "version": "2.15.0",
4
4
  "description": "AI-first CRM CLI — manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {