@startanaicompany/crm 2.18.0 → 2.21.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 +646 -1
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -3900,7 +3900,8 @@ tagsCmd
3900
3900
  const res = await client.get('/tags', { params: { limit: opts.limit } });
3901
3901
  const tags = res.data.data || res.data || [];
3902
3902
  if (tags.length === 0) { console.log('No tags in use.'); return; }
3903
- tags.forEach(t => console.log(`${t.tag} (${t.count} lead${t.count !== 1 ? 's' : ''})`));
3903
+ // Bug 1d18aaac: API returns lead_count not count
3904
+ tags.forEach(t => console.log(`${t.tag} (${t.lead_count} lead${t.lead_count !== 1 ? 's' : ''})`));
3904
3905
  } catch (err) { handleError(err); }
3905
3906
  });
3906
3907
 
@@ -4149,6 +4150,249 @@ analyticsCmd
4149
4150
  } catch (err) { handleError(err); }
4150
4151
  });
4151
4152
 
4153
+ // ============================================================
4154
+ // Sprint 56 T1: leads timeline subcommand
4155
+ // ============================================================
4156
+ leadsCmd
4157
+ .command('timeline <lead-id>')
4158
+ .description('View reverse-chronological activity timeline for a lead (Sprint 56 T1)')
4159
+ .option('--limit <n>', 'Max entries to return', '50')
4160
+ .option('--type <type>', 'Filter by event type: note_added|field_changed|score_changed|relationship_added|sla_breach|note|score|merge')
4161
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
4162
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
4163
+ .action(async (leadId, opts) => {
4164
+ const globalOpts = program.opts();
4165
+ const client = getClient(globalOpts);
4166
+ // Map friendly type aliases to server type names
4167
+ const typeAliases = {
4168
+ note: 'note_added',
4169
+ score: 'score_changed',
4170
+ merge: 'relationship_added',
4171
+ escalation: 'sla_breach',
4172
+ };
4173
+ const params = { limit: opts.limit };
4174
+ if (opts.type) params.type_filter = typeAliases[opts.type] || opts.type;
4175
+ if (opts.from) params.from = opts.from;
4176
+ if (opts.to) params.to = opts.to;
4177
+ try {
4178
+ const res = await client.get(`/leads/${leadId}/timeline`, { params });
4179
+ const d = res.data.data || {};
4180
+ const entries = d.timeline || [];
4181
+ if (entries.length === 0) { console.log('No timeline entries found.'); return; }
4182
+ console.log(`\nTimeline for lead ${leadId} (${d.total} total, showing ${entries.length}):`);
4183
+ entries.forEach(e => {
4184
+ const when = new Date(e.timestamp).toLocaleString();
4185
+ const actor = e.actor || 'system';
4186
+ console.log(`${when} [${e.type}] ${e.summary} — ${actor}`);
4187
+ });
4188
+ } catch (err) { handleError(err); }
4189
+ });
4190
+
4191
+ // ============================================================
4192
+ // Sprint 56 T2: email-logs + email-sync commands (admin JWT)
4193
+ // ============================================================
4194
+ const emailLogsCmd = program.command('email-logs').description('Inbound email parse logs (Sprint 56 T2) — requires admin login');
4195
+
4196
+ emailLogsCmd
4197
+ .command('list')
4198
+ .description('List inbound email parse log entries')
4199
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
4200
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
4201
+ .option('--status <status>', 'Filter: parsed | failed')
4202
+ .option('--limit <n>', 'Max results', '50')
4203
+ .option('--workspace <slug>', 'Workspace slug')
4204
+ .option('--email <email>', 'Admin email')
4205
+ .option('--password <password>', 'Admin password')
4206
+ .action(async (opts) => {
4207
+ const globalOpts = program.opts();
4208
+ const apiUrl = resolveApiUrl(globalOpts.url);
4209
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4210
+ const cfg = loadConfig();
4211
+ try {
4212
+ const base = apiUrl.replace(/\/$/, '');
4213
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4214
+ const params = { limit: opts.limit };
4215
+ if (opts.from) params.from = opts.from;
4216
+ if (opts.to) params.to = opts.to;
4217
+ if (opts.status) params.status = opts.status;
4218
+ const res = await axios.get(`${base}/api/v1/admin/inbound/email/log`, {
4219
+ params, headers: { Authorization: `Bearer ${token}` }
4220
+ });
4221
+ const d = res.data.data || {};
4222
+ const items = d.items || [];
4223
+ if (items.length === 0) { console.log('No email log entries found.'); return; }
4224
+ console.log(`Total: ${d.total || items.length}`);
4225
+ items.forEach(e => {
4226
+ const when = new Date(e.created_at).toLocaleString();
4227
+ const status = e.action === 'failed' ? '❌' : '✅';
4228
+ console.log(`[${e.id}] ${status} ${when} — from: ${e.from_raw || 'N/A'} | action: ${e.action} | lead: ${e.lead_id || 'none'}`);
4229
+ });
4230
+ } catch (err) { handleError(err); }
4231
+ });
4232
+
4233
+ emailLogsCmd
4234
+ .command('get <log-id>')
4235
+ .description('Get full details of a single inbound email log entry')
4236
+ .option('--workspace <slug>', 'Workspace slug')
4237
+ .option('--email <email>', 'Admin email')
4238
+ .option('--password <password>', 'Admin password')
4239
+ .action(async (logId, opts) => {
4240
+ const globalOpts = program.opts();
4241
+ const apiUrl = resolveApiUrl(globalOpts.url);
4242
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4243
+ const cfg = loadConfig();
4244
+ try {
4245
+ const base = apiUrl.replace(/\/$/, '');
4246
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4247
+ const res = await axios.get(`${base}/api/v1/admin/inbound/email/log/${logId}`, {
4248
+ headers: { Authorization: `Bearer ${token}` }
4249
+ });
4250
+ printJSON(res.data);
4251
+ } catch (err) { handleError(err); }
4252
+ });
4253
+
4254
+ const emailSyncCmd = program.command('email-sync').description('Manage email sync accounts (Sprint 56 T2) — requires admin login');
4255
+
4256
+ emailSyncCmd
4257
+ .command('status')
4258
+ .description('Show connected email sync accounts and their status')
4259
+ .option('--workspace <slug>', 'Workspace slug')
4260
+ .option('--email <email>', 'Admin email')
4261
+ .option('--password <password>', 'Admin password')
4262
+ .action(async (opts) => {
4263
+ const globalOpts = program.opts();
4264
+ const apiUrl = resolveApiUrl(globalOpts.url);
4265
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4266
+ const cfg = loadConfig();
4267
+ try {
4268
+ const base = apiUrl.replace(/\/$/, '');
4269
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4270
+ const res = await axios.get(`${base}/api/v1/admin/email-accounts`, {
4271
+ headers: { Authorization: `Bearer ${token}` }
4272
+ });
4273
+ const accounts = res.data.data || res.data || [];
4274
+ if (accounts.length === 0) { console.log('No email sync accounts connected.'); return; }
4275
+ accounts.forEach(a => {
4276
+ const lastSync = a.last_sync_at ? new Date(a.last_sync_at).toLocaleString() : 'never';
4277
+ const enabled = a.enabled ? '✓ enabled' : '✗ disabled';
4278
+ console.log(`[${a.id}] ${a.user_email || a.label || 'N/A'} (${a.provider || 'gmail'}) — ${enabled} | last sync: ${lastSync}`);
4279
+ });
4280
+ } catch (err) { handleError(err); }
4281
+ });
4282
+
4283
+ emailSyncCmd
4284
+ .command('disconnect <account-id>')
4285
+ .description('Disconnect an email sync account by ID')
4286
+ .option('--workspace <slug>', 'Workspace slug')
4287
+ .option('--email <email>', 'Admin email')
4288
+ .option('--password <password>', 'Admin password')
4289
+ .action(async (accountId, opts) => {
4290
+ const globalOpts = program.opts();
4291
+ const apiUrl = resolveApiUrl(globalOpts.url);
4292
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4293
+ const cfg = loadConfig();
4294
+ try {
4295
+ const base = apiUrl.replace(/\/$/, '');
4296
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4297
+ await axios.delete(`${base}/api/v1/admin/email-accounts/${accountId}`, {
4298
+ headers: { Authorization: `Bearer ${token}` }
4299
+ });
4300
+ console.log(`Email sync account ${accountId} disconnected.`);
4301
+ } catch (err) { handleError(err); }
4302
+ });
4303
+
4304
+ // ============================================================
4305
+ // Sprint 56 T3: users get + activity list commands
4306
+ // ============================================================
4307
+
4308
+ // Sprint 56 T3: users get <id> — uses admin JWT (/admin/users/:id) to avoid requireAdminScope on API key route
4309
+ usersCmd
4310
+ .command('get <id>')
4311
+ .description('Get a single user by ID including assigned lead count (Sprint 56 T3)')
4312
+ .option('--workspace <slug>', 'Workspace slug')
4313
+ .option('--email <email>', 'Admin email')
4314
+ .option('--password <password>', 'Admin password')
4315
+ .action(async (userId, opts) => {
4316
+ const globalOpts = program.opts();
4317
+ const apiUrl = resolveApiUrl(globalOpts.url);
4318
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4319
+ const cfg = loadConfig();
4320
+ try {
4321
+ const base = apiUrl.replace(/\/$/, '');
4322
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4323
+ const res = await axios.get(`${base}/api/v1/admin/users/${userId}`, {
4324
+ headers: { Authorization: `Bearer ${token}` }
4325
+ });
4326
+ const u = res.data.data || res.data;
4327
+ console.log(`[${u.id}] ${u.name || '—'} <${u.email}> — role: ${u.role} | active: ${u.is_active} | assigned leads: ${u.assigned_lead_count ?? 'N/A'}`);
4328
+ printJSON(res.data);
4329
+ } catch (err) { handleError(err); }
4330
+ });
4331
+
4332
+ // users list — also add admin JWT fallback via /admin/users for workspaces without admin-scope API keys
4333
+ usersCmd
4334
+ .command('list-admin')
4335
+ .description('List all users via admin JWT (use when API key lacks admin scope)')
4336
+ .option('--workspace <slug>', 'Workspace slug')
4337
+ .option('--email <email>', 'Admin email')
4338
+ .option('--password <password>', 'Admin password')
4339
+ .action(async (opts) => {
4340
+ const globalOpts = program.opts();
4341
+ const apiUrl = resolveApiUrl(globalOpts.url);
4342
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4343
+ const cfg = loadConfig();
4344
+ try {
4345
+ const base = apiUrl.replace(/\/$/, '');
4346
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4347
+ const res = await axios.get(`${base}/api/v1/admin/users`, {
4348
+ headers: { Authorization: `Bearer ${token}` }
4349
+ });
4350
+ const users = res.data.data || res.data || [];
4351
+ if (users.length === 0) { console.log('No users found.'); return; }
4352
+ users.forEach(u => console.log(`[${u.id}] ${u.name || '—'} <${u.email}> — role: ${u.role} | leads: ${u.assigned_lead_count}`));
4353
+ } catch (err) { handleError(err); }
4354
+ });
4355
+
4356
+ const activityCmd = program.command('activity').description('System-wide activity feed (Sprint 56 T3) — requires admin login');
4357
+
4358
+ activityCmd
4359
+ .command('list')
4360
+ .description('List recent system-wide activity across all leads')
4361
+ .option('--lead-id <id>', 'Filter to a specific lead ID')
4362
+ .option('--author-type <type>', 'Filter by actor type: api_key|human-admin|system')
4363
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
4364
+ .option('--limit <n>', 'Max results', '50')
4365
+ .option('--workspace <slug>', 'Workspace slug')
4366
+ .option('--email <email>', 'Admin email')
4367
+ .option('--password <password>', 'Admin password')
4368
+ .action(async (opts) => {
4369
+ const globalOpts = program.opts();
4370
+ const apiUrl = resolveApiUrl(globalOpts.url);
4371
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4372
+ const cfg = loadConfig();
4373
+ try {
4374
+ const base = apiUrl.replace(/\/$/, '');
4375
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4376
+ const params = { limit: opts.limit };
4377
+ if (opts.leadId) params.lead_id = opts.leadId;
4378
+ if (opts.authorType) params.author_type = opts.authorType;
4379
+ if (opts.from) params.from = opts.from;
4380
+ const res = await axios.get(`${base}/api/v1/admin/activity`, {
4381
+ params, headers: { Authorization: `Bearer ${token}` }
4382
+ });
4383
+ const d = res.data;
4384
+ const items = d.data || [];
4385
+ if (items.length === 0) { console.log('No activity found.'); return; }
4386
+ const total = d.pagination?.total || items.length;
4387
+ console.log(`Activity feed (${total} total, showing ${items.length}):`);
4388
+ items.forEach(e => {
4389
+ const when = new Date(e.changed_at).toLocaleString();
4390
+ const lead = e.lead_name ? `${e.lead_name}` : (e.lead_id || 'N/A');
4391
+ console.log(`${when} | ${e.changed_by || 'system'} | ${e.field}: ${e.old_value ?? '—'} → ${e.new_value ?? '—'} | lead: ${lead}`);
4392
+ });
4393
+ } catch (err) { handleError(err); }
4394
+ });
4395
+
4152
4396
  // ============================================================
4153
4397
  // GDPR COMMANDS — Sprint 50 T2
4154
4398
  // ============================================================
@@ -4287,4 +4531,405 @@ reportsCmd
4287
4531
  } catch (err) { handleError(err); }
4288
4532
  });
4289
4533
 
4534
+ // ============================================================
4535
+ // SCORING RULES COMMANDS (S57-T1) — admin JWT
4536
+ // ============================================================
4537
+
4538
+ const scoringRulesCmd = program.command('scoring-rules').description('Manage auto lead scoring rules (admin)');
4539
+
4540
+ scoringRulesCmd
4541
+ .command('list')
4542
+ .description('List all auto scoring rules')
4543
+ .option('--workspace <slug>', 'Workspace slug')
4544
+ .option('--email <email>', 'Admin email')
4545
+ .option('--password <password>', 'Admin password')
4546
+ .action(async (opts) => {
4547
+ const globalOpts = program.opts();
4548
+ const apiUrl = resolveApiUrl(globalOpts.url);
4549
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4550
+ const cfg = loadConfig();
4551
+ try {
4552
+ const base = apiUrl.replace(/\/$/, '');
4553
+ const token = await stagesAdminLogin(base, cfg, opts);
4554
+ const res = await axios.get(`${base}/api/v1/admin/score-rules/auto`, {
4555
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4556
+ });
4557
+ const rules = res.data.data || res.data;
4558
+ if (Array.isArray(rules) && rules.length === 0) { console.log('No scoring rules found.'); return; }
4559
+ printJSON(res.data);
4560
+ } catch (err) { handleError(err); }
4561
+ });
4562
+
4563
+ scoringRulesCmd
4564
+ .command('get <rule-id>')
4565
+ .description('Get a single auto scoring rule by ID')
4566
+ .option('--workspace <slug>', 'Workspace slug')
4567
+ .option('--email <email>', 'Admin email')
4568
+ .option('--password <password>', 'Admin password')
4569
+ .action(async (ruleId, opts) => {
4570
+ const globalOpts = program.opts();
4571
+ const apiUrl = resolveApiUrl(globalOpts.url);
4572
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4573
+ const cfg = loadConfig();
4574
+ try {
4575
+ const base = apiUrl.replace(/\/$/, '');
4576
+ const token = await stagesAdminLogin(base, cfg, opts);
4577
+ const res = await axios.get(`${base}/api/v1/admin/score-rules/auto/${ruleId}`, {
4578
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4579
+ });
4580
+ printJSON(res.data);
4581
+ } catch (err) { handleError(err); }
4582
+ });
4583
+
4584
+ scoringRulesCmd
4585
+ .command('create')
4586
+ .description('Create a new auto scoring rule')
4587
+ .requiredOption('--name <name>', 'Rule name')
4588
+ .requiredOption('--score <delta>', 'Score delta (-100 to 100, non-zero integer)')
4589
+ .option('--trigger <event>', 'Trigger event (default: lead.status_changed)', 'lead.status_changed')
4590
+ .option('--condition <expr>', 'Condition: "<field> <op> <value>" (repeatable). e.g. --condition "deal_value gte 10000" --condition "status eq qualified"', (val, acc) => { acc.push(val); return acc; }, [])
4591
+ .option('--match <mode>', 'Condition logic: all (AND) or any (OR) (default: all)', 'all')
4592
+ .option('--field <field>', 'Lead field (single-condition shorthand, used if no --condition flags)')
4593
+ .option('--operator <op>', 'Operator shorthand: eq, neq, gt, lt, gte, lte, contains')
4594
+ .option('--value <val>', 'Value shorthand')
4595
+ .option('--workspace <slug>', 'Workspace slug')
4596
+ .option('--email <email>', 'Admin email')
4597
+ .option('--password <password>', 'Admin password')
4598
+ .action(async (opts) => {
4599
+ const globalOpts = program.opts();
4600
+ const apiUrl = resolveApiUrl(globalOpts.url);
4601
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4602
+ const cfg = loadConfig();
4603
+ const delta = parseInt(opts.score, 10);
4604
+ if (isNaN(delta)) { console.error('Error: --score must be a non-zero integer'); process.exit(1); }
4605
+ // Build conditions: repeatable --condition "field op value" takes precedence over --field/--operator/--value
4606
+ const conditions = [];
4607
+ if (opts.condition && opts.condition.length > 0) {
4608
+ for (const expr of opts.condition) {
4609
+ const parts = expr.trim().split(/\s+/);
4610
+ if (parts.length < 3) { console.error(`Invalid --condition "${expr}". Format: "<field> <operator> <value>"`); process.exit(1); }
4611
+ const [field, operator, ...rest] = parts;
4612
+ conditions.push({ field, operator, value: rest.join(' ') });
4613
+ }
4614
+ } else if (opts.field && opts.operator && opts.value !== undefined) {
4615
+ conditions.push({ field: opts.field, operator: opts.operator, value: opts.value });
4616
+ }
4617
+ try {
4618
+ const base = apiUrl.replace(/\/$/, '');
4619
+ const token = await stagesAdminLogin(base, cfg, opts);
4620
+ const body = { name: opts.name, trigger_event: opts.trigger, score_delta: delta, conditions, match: opts.match };
4621
+ const res = await axios.post(`${base}/api/v1/admin/score-rules/auto`, body, {
4622
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4623
+ });
4624
+ const r = res.data.data || res.data;
4625
+ console.log(`Rule created: [${r.id}] ${r.name} (delta: ${r.score_delta}, conditions: ${conditions.length})`);
4626
+ printJSON(res.data);
4627
+ } catch (err) { handleError(err); }
4628
+ });
4629
+
4630
+ scoringRulesCmd
4631
+ .command('update <rule-id>')
4632
+ .description('Update an auto scoring rule')
4633
+ .option('--name <name>', 'New rule name')
4634
+ .option('--score <delta>', 'New score delta (-100 to 100)')
4635
+ .option('--trigger <event>', 'New trigger event')
4636
+ .option('--active <bool>', 'Enable/disable rule (true|false)')
4637
+ .option('--workspace <slug>', 'Workspace slug')
4638
+ .option('--email <email>', 'Admin email')
4639
+ .option('--password <password>', 'Admin password')
4640
+ .action(async (ruleId, opts) => {
4641
+ const globalOpts = program.opts();
4642
+ const apiUrl = resolveApiUrl(globalOpts.url);
4643
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4644
+ const cfg = loadConfig();
4645
+ const body = {};
4646
+ if (opts.name) body.name = opts.name;
4647
+ if (opts.score !== undefined) body.score_delta = parseInt(opts.score, 10);
4648
+ if (opts.trigger) body.trigger_event = opts.trigger;
4649
+ if (opts.active !== undefined) body.is_active = opts.active === 'true';
4650
+ if (Object.keys(body).length === 0) { console.error('Provide at least one of: --name, --score, --trigger, --active'); process.exit(1); }
4651
+ try {
4652
+ const base = apiUrl.replace(/\/$/, '');
4653
+ const token = await stagesAdminLogin(base, cfg, opts);
4654
+ const res = await axios.patch(`${base}/api/v1/admin/score-rules/auto/${ruleId}`, body, {
4655
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4656
+ });
4657
+ console.log(`Rule updated.`);
4658
+ printJSON(res.data);
4659
+ } catch (err) { handleError(err); }
4660
+ });
4661
+
4662
+ scoringRulesCmd
4663
+ .command('delete <rule-id>')
4664
+ .description('Delete an auto scoring rule')
4665
+ .option('--workspace <slug>', 'Workspace slug')
4666
+ .option('--email <email>', 'Admin email')
4667
+ .option('--password <password>', 'Admin password')
4668
+ .action(async (ruleId, opts) => {
4669
+ const globalOpts = program.opts();
4670
+ const apiUrl = resolveApiUrl(globalOpts.url);
4671
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4672
+ const cfg = loadConfig();
4673
+ try {
4674
+ const base = apiUrl.replace(/\/$/, '');
4675
+ const token = await stagesAdminLogin(base, cfg, opts);
4676
+ const res = await axios.delete(`${base}/api/v1/admin/score-rules/auto/${ruleId}`, {
4677
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4678
+ });
4679
+ console.log(`Rule ${ruleId} deleted.`);
4680
+ printJSON(res.data);
4681
+ } catch (err) { handleError(err); }
4682
+ });
4683
+
4684
+ scoringRulesCmd
4685
+ .command('evaluate <lead-id>')
4686
+ .description('Re-evaluate all active scoring rules against a lead and apply matching score deltas')
4687
+ .action(async (leadId) => {
4688
+ const globalOpts = program.opts();
4689
+ const client = getClient(globalOpts);
4690
+ try {
4691
+ const res = await client.post(`/leads/${leadId}/score/evaluate`);
4692
+ const r = res.data.data || res.data;
4693
+ console.log(`Score evaluated: final_score=${r.final_score}, rules_applied=${r.rules_applied}`);
4694
+ printJSON(res.data);
4695
+ } catch (err) { handleError(err); }
4696
+ });
4697
+
4698
+ // ============================================================
4699
+ // NOTIFICATIONS COMMANDS (S57-T2) — API key auth
4700
+ // ============================================================
4701
+
4702
+ const notificationsCmd = program.command('notifications').description('Manage agent notifications');
4703
+
4704
+ notificationsCmd
4705
+ .command('list')
4706
+ .description('List notifications for current API key')
4707
+ .option('--unread', 'Show only unread notifications')
4708
+ .option('--limit <n>', 'Max notifications to return', '50')
4709
+ .action(async (opts) => {
4710
+ const globalOpts = program.opts();
4711
+ const client = getClient(globalOpts);
4712
+ try {
4713
+ const params = { limit: opts.limit };
4714
+ if (opts.unread) params.unread = 'true';
4715
+ const res = await client.get('/notifications', { params });
4716
+ const notifications = res.data.data || res.data;
4717
+ if (Array.isArray(notifications) && notifications.length === 0) { console.log('No notifications.'); return; }
4718
+ printJSON(res.data);
4719
+ } catch (err) { handleError(err); }
4720
+ });
4721
+
4722
+ notificationsCmd
4723
+ .command('read <notification-id>')
4724
+ .description('Mark a notification as read')
4725
+ .action(async (notificationId) => {
4726
+ const globalOpts = program.opts();
4727
+ const client = getClient(globalOpts);
4728
+ try {
4729
+ const res = await client.patch(`/notifications/${notificationId}/read`);
4730
+ console.log('Notification marked as read.');
4731
+ printJSON(res.data);
4732
+ } catch (err) { handleError(err); }
4733
+ });
4734
+
4735
+ notificationsCmd
4736
+ .command('read-all')
4737
+ .description('Mark all notifications as read for current API key')
4738
+ .action(async () => {
4739
+ const globalOpts = program.opts();
4740
+ const client = getClient(globalOpts);
4741
+ try {
4742
+ const res = await client.post('/notifications/read-all');
4743
+ const r = res.data.data || res.data;
4744
+ console.log(`Marked ${r.marked_read} notification(s) as read.`);
4745
+ } catch (err) { handleError(err); }
4746
+ });
4747
+
4748
+ notificationsCmd
4749
+ .command('delete <notification-id>')
4750
+ .description('Delete a notification')
4751
+ .action(async (notificationId) => {
4752
+ const globalOpts = program.opts();
4753
+ const client = getClient(globalOpts);
4754
+ try {
4755
+ await client.delete(`/notifications/${notificationId}`);
4756
+ console.log(`Notification ${notificationId} deleted.`);
4757
+ } catch (err) { handleError(err); }
4758
+ });
4759
+
4760
+ // ============================================================
4761
+ // KEYS USAGE SUBCOMMAND (S57-T3) — admin JWT
4762
+ // ============================================================
4763
+
4764
+ keysCmd
4765
+ .command('usage [key-id]')
4766
+ .description('Show API key usage stats. Omit key-id to show all-keys summary.')
4767
+ .option('--all', 'Show summary across all keys')
4768
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
4769
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
4770
+ .option('--workspace <slug>', 'Workspace slug')
4771
+ .option('--email <email>', 'Admin email')
4772
+ .option('--password <password>', 'Admin password')
4773
+ .action(async (keyId, opts) => {
4774
+ const globalOpts = program.opts();
4775
+ const apiUrl = resolveApiUrl(globalOpts.url);
4776
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4777
+ const cfg = loadConfig();
4778
+ try {
4779
+ const base = apiUrl.replace(/\/$/, '');
4780
+ const token = await stagesAdminLogin(base, cfg, opts);
4781
+ const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
4782
+ if (!keyId || opts.all) {
4783
+ const res = await axios.get(`${base}/api/v1/admin/api-keys/usage/summary`, { headers });
4784
+ printJSON(res.data);
4785
+ } else {
4786
+ const params = {};
4787
+ if (opts.from) params.from = opts.from;
4788
+ if (opts.to) params.to = opts.to;
4789
+ const res = await axios.get(`${base}/api/v1/admin/api-keys/${keyId}/usage`, { headers, params });
4790
+ printJSON(res.data);
4791
+ }
4792
+ } catch (err) { handleError(err); }
4793
+ });
4794
+
4795
+ // ============================================================
4796
+ // INTEGRATIONS COMMANDS (S57-T3) — admin JWT
4797
+ // ============================================================
4798
+
4799
+ const integrationsCmd = program.command('integrations').description('View integration status (email sync, webhooks)');
4800
+
4801
+ integrationsCmd
4802
+ .command('list')
4803
+ .description('List all integrations for workspace')
4804
+ .option('--workspace <slug>', 'Workspace slug')
4805
+ .option('--email <email>', 'Admin email')
4806
+ .option('--password <password>', 'Admin password')
4807
+ .action(async (opts) => {
4808
+ const globalOpts = program.opts();
4809
+ const apiUrl = resolveApiUrl(globalOpts.url);
4810
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4811
+ const cfg = loadConfig();
4812
+ try {
4813
+ const base = apiUrl.replace(/\/$/, '');
4814
+ const token = await stagesAdminLogin(base, cfg, opts);
4815
+ const res = await axios.get(`${base}/api/v1/admin/integrations`, {
4816
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4817
+ });
4818
+ const list = res.data.data || res.data;
4819
+ if (Array.isArray(list) && list.length === 0) { console.log('No integrations configured.'); return; }
4820
+ if (Array.isArray(list)) {
4821
+ list.forEach(i => {
4822
+ const status = i.status || (i.enabled ? 'active' : 'disabled');
4823
+ console.log(`[${i.id}] ${i.type.padEnd(12)} ${(i.name || '').substring(0, 50).padEnd(52)} status: ${status}`);
4824
+ });
4825
+ } else {
4826
+ printJSON(res.data);
4827
+ }
4828
+ } catch (err) { handleError(err); }
4829
+ });
4830
+
4831
+ integrationsCmd
4832
+ .command('status <integration-id>')
4833
+ .description('Show detailed status for a specific integration')
4834
+ .option('--workspace <slug>', 'Workspace slug')
4835
+ .option('--email <email>', 'Admin email')
4836
+ .option('--password <password>', 'Admin password')
4837
+ .action(async (integrationId, opts) => {
4838
+ const globalOpts = program.opts();
4839
+ const apiUrl = resolveApiUrl(globalOpts.url);
4840
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4841
+ const cfg = loadConfig();
4842
+ try {
4843
+ const base = apiUrl.replace(/\/$/, '');
4844
+ const token = await stagesAdminLogin(base, cfg, opts);
4845
+ const res = await axios.get(`${base}/api/v1/admin/integrations/${integrationId}`, {
4846
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4847
+ });
4848
+ printJSON(res.data);
4849
+ } catch (err) { handleError(err); }
4850
+ });
4851
+
4852
+ // ============================================================
4853
+ // LEAD ARCHIVAL COMMANDS (S58-T3) — archive, restore, list archived, purge
4854
+ // ============================================================
4855
+
4856
+ leadsCmd
4857
+ .command('archive <lead-id>')
4858
+ .description('Archive a lead (removes from active pipeline)')
4859
+ .action(async (leadId) => {
4860
+ const globalOpts = program.opts();
4861
+ const client = getClient(globalOpts);
4862
+ try {
4863
+ const res = await client.post(`/leads/${leadId}/archive`);
4864
+ console.log(`Lead ${leadId} archived.`);
4865
+ printJSON(res.data);
4866
+ } catch (err) { handleError(err); }
4867
+ });
4868
+
4869
+ leadsCmd
4870
+ .command('restore <lead-id>')
4871
+ .description('Restore an archived lead back to the active pipeline')
4872
+ .action(async (leadId) => {
4873
+ const globalOpts = program.opts();
4874
+ const client = getClient(globalOpts);
4875
+ try {
4876
+ const res = await client.post(`/leads/${leadId}/restore`);
4877
+ console.log(`Lead ${leadId} restored.`);
4878
+ printJSON(res.data);
4879
+ } catch (err) { handleError(err); }
4880
+ });
4881
+
4882
+ leadsCmd
4883
+ .command('archived')
4884
+ .description('List archived leads')
4885
+ .option('--limit <n>', 'Max leads to return', '50')
4886
+ .action(async (opts) => {
4887
+ const globalOpts = program.opts();
4888
+ const client = getClient(globalOpts);
4889
+ try {
4890
+ const res = await client.get('/leads', { params: { archived_only: 'true', limit: opts.limit } });
4891
+ const leads = res.data.data || res.data;
4892
+ if (Array.isArray(leads) && leads.length === 0) { console.log('No archived leads.'); return; }
4893
+ if (Array.isArray(leads)) {
4894
+ leads.forEach(l => {
4895
+ const archivedAt = l.archived_at ? new Date(l.archived_at).toISOString().split('T')[0] : 'unknown';
4896
+ console.log(`[${l.id}] ${(l.name || l.email || 'unnamed').padEnd(35)} archived: ${archivedAt}`);
4897
+ });
4898
+ } else {
4899
+ printJSON(res.data);
4900
+ }
4901
+ } catch (err) { handleError(err); }
4902
+ });
4903
+
4904
+ leadsCmd
4905
+ .command('purge-archived')
4906
+ .description('Permanently delete archived leads older than a given date (admin only)')
4907
+ .requiredOption('--before <date>', 'Cutoff date (YYYY-MM-DD) — delete leads archived before this date')
4908
+ .option('--dry-run', 'Show count of leads that would be deleted without actually deleting')
4909
+ .option('--workspace <slug>', 'Workspace slug')
4910
+ .option('--email <email>', 'Admin email')
4911
+ .option('--password <password>', 'Admin password')
4912
+ .action(async (opts) => {
4913
+ const globalOpts = program.opts();
4914
+ const apiUrl = resolveApiUrl(globalOpts.url);
4915
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4916
+ const cfg = loadConfig();
4917
+ try {
4918
+ const base = apiUrl.replace(/\/$/, '');
4919
+ const token = await stagesAdminLogin(base, cfg, opts);
4920
+ const params = { before: opts.before };
4921
+ if (opts.dryRun) params.dry_run = 'true';
4922
+ const res = await axios.delete(`${base}/api/v1/admin/leads/purge-archived`, {
4923
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
4924
+ params
4925
+ });
4926
+ const r = res.data.data || res.data;
4927
+ if (r.dry_run) {
4928
+ console.log(`Dry run: ${r.would_delete} lead(s) would be permanently deleted (archived before ${opts.before}).`);
4929
+ } else {
4930
+ console.log(`Purged: ${r.deleted} archived lead(s) permanently deleted (before ${opts.before}).`);
4931
+ }
4932
+ } catch (err) { handleError(err); }
4933
+ });
4934
+
4290
4935
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.18.0",
3
+ "version": "2.21.0",
4
4
  "description": "AI-first CRM CLI — manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {