@startanaicompany/crm 2.17.0 → 2.18.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 +332 -1
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1241,7 +1241,9 @@ leadsCmd
1241
1241
  }
1242
1242
  console.log('\n Run without --dry-run to commit the merge.');
1243
1243
  } else {
1244
- const res = await client.post(`/leads/${primaryId}/merge`, { merge_into: secondaryId });
1244
+ // Bug 58e6d000: server treats :id as the source (soft-deleted) and merge_into as target (kept)
1245
+ // So we POST to /leads/:secondaryId/merge with { merge_into: primaryId }
1246
+ const res = await client.post(`/leads/${secondaryId}/merge`, { merge_into: primaryId });
1245
1247
  console.log(`Merged lead ${secondaryId} into ${primaryId}.`);
1246
1248
  printJSON(res.data);
1247
1249
  }
@@ -3818,6 +3820,335 @@ formsCmd
3818
3820
  } catch (err) { handleError(err); }
3819
3821
  });
3820
3822
 
3823
+ // ============================================================
3824
+ // AUDIT COMMANDS — Sprint 55 T1 (admin JWT via stagesAdminLogin)
3825
+ // ============================================================
3826
+ const auditCmd = program.command('audit').description('View workspace audit log (Sprint 55 T1) — requires admin login');
3827
+
3828
+ auditCmd
3829
+ .command('list')
3830
+ .description('List audit log entries with optional filters')
3831
+ .option('--lead-id <id>', 'Filter by lead ID')
3832
+ .option('--author-type <type>', 'Filter by actor type: api_key|human-admin|system')
3833
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
3834
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
3835
+ .option('--limit <n>', 'Max results (max 200)', '50')
3836
+ .option('--offset <n>', 'Pagination offset', '0')
3837
+ .option('--workspace <slug>', 'Workspace slug')
3838
+ .option('--email <email>', 'Admin email')
3839
+ .option('--password <password>', 'Admin password')
3840
+ .action(async (opts) => {
3841
+ const globalOpts = program.opts();
3842
+ const apiUrl = resolveApiUrl(globalOpts.url);
3843
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
3844
+ const cfg = loadConfig();
3845
+ try {
3846
+ const base = apiUrl.replace(/\/$/, '');
3847
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
3848
+ const params = { limit: opts.limit, offset: opts.offset };
3849
+ if (opts.leadId) params.lead_id = opts.leadId;
3850
+ if (opts.authorType) params.author_type = opts.authorType;
3851
+ if (opts.from) params.from = opts.from;
3852
+ if (opts.to) params.to = opts.to;
3853
+ const res = await axios.get(`${base}/api/v1/admin/audit-log`, {
3854
+ params, headers: { Authorization: `Bearer ${token}` }
3855
+ });
3856
+ const d = res.data.data || {};
3857
+ const items = d.items || [];
3858
+ console.log(`Total: ${d.total || items.length}`);
3859
+ items.forEach(e => {
3860
+ const when = new Date(e.created_at).toLocaleString();
3861
+ console.log(`[${e.id}] ${when} | ${e.actor_type} ${e.actor_name || 'system'} | ${e.action}: ${e.old_value || '—'} → ${e.new_value || '—'} | lead: ${e.lead_name || e.lead_id || 'N/A'}`);
3862
+ });
3863
+ } catch (err) { handleError(err); }
3864
+ });
3865
+
3866
+ auditCmd
3867
+ .command('get <entry-id>')
3868
+ .description('Get a single audit log entry by ID')
3869
+ .option('--workspace <slug>', 'Workspace slug')
3870
+ .option('--email <email>', 'Admin email')
3871
+ .option('--password <password>', 'Admin password')
3872
+ .action(async (entryId, opts) => {
3873
+ const globalOpts = program.opts();
3874
+ const apiUrl = resolveApiUrl(globalOpts.url);
3875
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
3876
+ const cfg = loadConfig();
3877
+ try {
3878
+ const base = apiUrl.replace(/\/$/, '');
3879
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
3880
+ const res = await axios.get(`${base}/api/v1/admin/audit-log/${entryId}`, {
3881
+ headers: { Authorization: `Bearer ${token}` }
3882
+ });
3883
+ printJSON(res.data);
3884
+ } catch (err) { handleError(err); }
3885
+ });
3886
+
3887
+ // ============================================================
3888
+ // TAGS COMMANDS — Sprint 55 T1 (API key auth)
3889
+ // ============================================================
3890
+ const tagsCmd = program.command('tags').description('Explore lead tags (Sprint 55 T1)');
3891
+
3892
+ tagsCmd
3893
+ .command('list')
3894
+ .description('List all unique tags in use with lead counts')
3895
+ .option('--limit <n>', 'Max tags to return', '100')
3896
+ .action(async (opts) => {
3897
+ const globalOpts = program.opts();
3898
+ const client = getClient(globalOpts);
3899
+ try {
3900
+ const res = await client.get('/tags', { params: { limit: opts.limit } });
3901
+ const tags = res.data.data || res.data || [];
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' : ''})`));
3904
+ } catch (err) { handleError(err); }
3905
+ });
3906
+
3907
+ tagsCmd
3908
+ .command('leads <tag-name>')
3909
+ .description('List leads with a specific tag')
3910
+ .option('--limit <n>', 'Max results', '50')
3911
+ .action(async (tagName, opts) => {
3912
+ const globalOpts = program.opts();
3913
+ const client = getClient(globalOpts);
3914
+ try {
3915
+ const res = await client.get('/leads', { params: { tag: tagName, per_page: parseInt(opts.limit) } });
3916
+ const leads = res.data.data || [];
3917
+ if (leads.length === 0) { console.log(`No leads found with tag "${tagName}".`); return; }
3918
+ console.log(`Leads tagged "${tagName}" (${leads.length}):`);
3919
+ leads.forEach(l => console.log(`[${l.id}] ${l.name} <${l.email}> — ${l.status} (${l.assigned_to || 'unassigned'})`));
3920
+ } catch (err) { handleError(err); }
3921
+ });
3922
+
3923
+ // ============================================================
3924
+ // SLA COMMANDS — Sprint 55 T2 (admin JWT via stagesAdminLogin)
3925
+ // ============================================================
3926
+ const slaCmd = program.command('sla').description('Manage SLA escalation rules and view breaches (Sprint 55 T2)');
3927
+
3928
+ slaCmd
3929
+ .command('list')
3930
+ .description('List all SLA escalation rules')
3931
+ .option('--workspace <slug>', 'Workspace slug')
3932
+ .option('--email <email>', 'Admin email')
3933
+ .option('--password <password>', 'Admin password')
3934
+ .action(async (opts) => {
3935
+ const globalOpts = program.opts();
3936
+ const apiUrl = resolveApiUrl(globalOpts.url);
3937
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
3938
+ const cfg = loadConfig();
3939
+ try {
3940
+ const base = apiUrl.replace(/\/$/, '');
3941
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
3942
+ const res = await axios.get(`${base}/api/v1/admin/sla-escalation-rules`, {
3943
+ headers: { Authorization: `Bearer ${token}` }
3944
+ });
3945
+ const rules = res.data.data || res.data || [];
3946
+ if (rules.length === 0) { console.log('No SLA rules configured.'); return; }
3947
+ rules.forEach(r => {
3948
+ const hours = (r.escalate_after_breach_minutes / 60).toFixed(1);
3949
+ const stage = r.stage_name_label ? `stage: ${r.stage_name_label}` : 'all stages';
3950
+ const active = r.is_active ? '✓' : '✗';
3951
+ console.log(`[${r.id}] ${active} "${r.name}" — ${hours}h breach → escalate to ${r.escalate_to} (${stage})`);
3952
+ });
3953
+ } catch (err) { handleError(err); }
3954
+ });
3955
+
3956
+ slaCmd
3957
+ .command('create')
3958
+ .description('Create a new SLA escalation rule')
3959
+ .requiredOption('--name <name>', 'Rule name')
3960
+ .requiredOption('--hours <n>', 'Hours after breach before escalating')
3961
+ .requiredOption('--escalate-to <agent>', 'Agent/email to escalate to')
3962
+ .option('--stage-id <id>', 'Pipeline stage UUID to scope this rule (omit for all stages)')
3963
+ .option('--workspace <slug>', 'Workspace slug')
3964
+ .option('--email <email>', 'Admin email')
3965
+ .option('--password <password>', 'Admin password')
3966
+ .action(async (opts) => {
3967
+ const globalOpts = program.opts();
3968
+ const apiUrl = resolveApiUrl(globalOpts.url);
3969
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
3970
+ const cfg = loadConfig();
3971
+ const minutes = Math.round(parseFloat(opts.hours) * 60);
3972
+ if (isNaN(minutes) || minutes < 1) { console.error('Error: --hours must be a positive number'); process.exit(1); }
3973
+ try {
3974
+ const base = apiUrl.replace(/\/$/, '');
3975
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
3976
+ const body = { name: opts.name, escalate_after_breach_minutes: minutes, escalate_to: opts.escalateTo };
3977
+ if (opts.stageId) body.stage_id = opts.stageId;
3978
+ const res = await axios.post(`${base}/api/v1/admin/sla-escalation-rules`, body, {
3979
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
3980
+ });
3981
+ console.log(`SLA rule created: ${res.data.data?.id || res.data?.id}`);
3982
+ printJSON(res.data);
3983
+ } catch (err) { handleError(err); }
3984
+ });
3985
+
3986
+ slaCmd
3987
+ .command('update <id>')
3988
+ .description('Update an SLA escalation rule')
3989
+ .option('--name <name>', 'New rule name')
3990
+ .option('--hours <n>', 'New escalation threshold in hours')
3991
+ .option('--escalate-to <agent>', 'New escalation target')
3992
+ .option('--active <bool>', 'Enable/disable rule (true|false)')
3993
+ .option('--workspace <slug>', 'Workspace slug')
3994
+ .option('--email <email>', 'Admin email')
3995
+ .option('--password <password>', 'Admin password')
3996
+ .action(async (id, opts) => {
3997
+ const globalOpts = program.opts();
3998
+ const apiUrl = resolveApiUrl(globalOpts.url);
3999
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4000
+ const cfg = loadConfig();
4001
+ const body = {};
4002
+ if (opts.name) body.name = opts.name;
4003
+ if (opts.hours !== undefined) {
4004
+ const minutes = Math.round(parseFloat(opts.hours) * 60);
4005
+ if (isNaN(minutes) || minutes < 1) { console.error('Error: --hours must be a positive number'); process.exit(1); }
4006
+ body.escalate_after_breach_minutes = minutes;
4007
+ }
4008
+ if (opts.escalateTo) body.escalate_to = opts.escalateTo;
4009
+ if (opts.active !== undefined) body.is_active = opts.active === 'true';
4010
+ if (Object.keys(body).length === 0) { console.error('Error: provide at least one field to update'); process.exit(1); }
4011
+ try {
4012
+ const base = apiUrl.replace(/\/$/, '');
4013
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4014
+ const res = await axios.patch(`${base}/api/v1/admin/sla-escalation-rules/${id}`, body, {
4015
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
4016
+ });
4017
+ console.log(`SLA rule ${id} updated.`);
4018
+ printJSON(res.data);
4019
+ } catch (err) { handleError(err); }
4020
+ });
4021
+
4022
+ slaCmd
4023
+ .command('delete <id>')
4024
+ .description('Delete an SLA escalation rule')
4025
+ .option('--workspace <slug>', 'Workspace slug')
4026
+ .option('--email <email>', 'Admin email')
4027
+ .option('--password <password>', 'Admin password')
4028
+ .action(async (id, opts) => {
4029
+ const globalOpts = program.opts();
4030
+ const apiUrl = resolveApiUrl(globalOpts.url);
4031
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4032
+ const cfg = loadConfig();
4033
+ try {
4034
+ const base = apiUrl.replace(/\/$/, '');
4035
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4036
+ await axios.delete(`${base}/api/v1/admin/sla-escalation-rules/${id}`, {
4037
+ headers: { Authorization: `Bearer ${token}` }
4038
+ });
4039
+ console.log(`SLA rule ${id} deleted.`);
4040
+ } catch (err) { handleError(err); }
4041
+ });
4042
+
4043
+ slaCmd
4044
+ .command('breaches')
4045
+ .description('List leads with active SLA breaches')
4046
+ .option('--status <status>', 'open (default) or resolved', 'open')
4047
+ .option('--limit <n>', 'Max results', '50')
4048
+ .option('--workspace <slug>', 'Workspace slug')
4049
+ .option('--email <email>', 'Admin email')
4050
+ .option('--password <password>', 'Admin password')
4051
+ .action(async (opts) => {
4052
+ const globalOpts = program.opts();
4053
+ const apiUrl = resolveApiUrl(globalOpts.url);
4054
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4055
+ const cfg = loadConfig();
4056
+ try {
4057
+ const base = apiUrl.replace(/\/$/, '');
4058
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4059
+ const res = await axios.get(`${base}/api/v1/admin/sla-breaches`, {
4060
+ params: { status: opts.status, limit: opts.limit },
4061
+ headers: { Authorization: `Bearer ${token}` }
4062
+ });
4063
+ const d = res.data.data || {};
4064
+ const items = d.items || [];
4065
+ if (items.length === 0) { console.log(`No ${opts.status} SLA breaches.`); return; }
4066
+ console.log(`${opts.status.toUpperCase()} SLA breaches (${d.total || items.length}):`);
4067
+ items.forEach(l => {
4068
+ const breached = l.breached_at ? new Date(l.breached_at).toLocaleString() : 'N/A';
4069
+ console.log(`[${l.id}] ${l.name} <${l.email}> — ${l.status} | stage: ${l.pipeline_stage || 'N/A'} | breached: ${breached} | assigned: ${l.assigned_to || 'none'}`);
4070
+ });
4071
+ } catch (err) { handleError(err); }
4072
+ });
4073
+
4074
+ // ============================================================
4075
+ // ANALYTICS COMMANDS — Sprint 55 T3 (admin JWT via stagesAdminLogin)
4076
+ // (analyticsCmd already declared above — appending pipeline + funnel subcommands)
4077
+ // ============================================================
4078
+
4079
+ analyticsCmd
4080
+ .command('pipeline')
4081
+ .description('Stage-by-stage pipeline breakdown: lead count, avg deal value, avg time-in-stage, conversion rate')
4082
+ .option('--stage <name>', 'Filter output to a single stage name')
4083
+ .option('--workspace <slug>', 'Workspace slug')
4084
+ .option('--email <email>', 'Admin email')
4085
+ .option('--password <password>', 'Admin password')
4086
+ .action(async (opts) => {
4087
+ const globalOpts = program.opts();
4088
+ const apiUrl = resolveApiUrl(globalOpts.url);
4089
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4090
+ const cfg = loadConfig();
4091
+ try {
4092
+ const base = apiUrl.replace(/\/$/, '');
4093
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4094
+ const res = await axios.get(`${base}/api/v1/admin/analytics/funnel`, {
4095
+ headers: { Authorization: `Bearer ${token}` }
4096
+ });
4097
+ let stages = (res.data.data?.stages || []);
4098
+ if (opts.stage) {
4099
+ stages = stages.filter(s => s.stage_name.toLowerCase() === opts.stage.toLowerCase());
4100
+ if (stages.length === 0) { console.log(`Stage "${opts.stage}" not found.`); return; }
4101
+ }
4102
+ console.log('\nPipeline Breakdown:');
4103
+ console.log('─'.repeat(90));
4104
+ stages.forEach(s => {
4105
+ const deal = s.avg_deal_value != null ? `$${s.avg_deal_value}` : 'N/A';
4106
+ const time = s.avg_time_in_stage_hours != null ? `${s.avg_time_in_stage_hours}h` : 'N/A';
4107
+ const conv = s.conversion_rate_to_next_stage != null ? `${s.conversion_rate_to_next_stage}%` : 'N/A';
4108
+ console.log(`${String(s.stage_name).padEnd(20)} leads: ${String(s.lead_count).padStart(4)} | avg deal: ${String(deal).padStart(10)} | avg time: ${String(time).padStart(8)} | conv: ${conv}`);
4109
+ });
4110
+ } catch (err) { handleError(err); }
4111
+ });
4112
+
4113
+ analyticsCmd
4114
+ .command('funnel')
4115
+ .description('Full top-to-bottom conversion funnel with drop-off rates')
4116
+ .option('--from <date>', 'From date (YYYY-MM-DD, default: 30 days ago)')
4117
+ .option('--to <date>', 'To date (YYYY-MM-DD, default: today)')
4118
+ .option('--workspace <slug>', 'Workspace slug')
4119
+ .option('--email <email>', 'Admin email')
4120
+ .option('--password <password>', 'Admin password')
4121
+ .action(async (opts) => {
4122
+ const globalOpts = program.opts();
4123
+ const apiUrl = resolveApiUrl(globalOpts.url);
4124
+ if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
4125
+ const cfg = loadConfig();
4126
+ try {
4127
+ const base = apiUrl.replace(/\/$/, '');
4128
+ const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
4129
+ const params = {};
4130
+ if (opts.from) params.from = opts.from;
4131
+ if (opts.to) params.to = opts.to;
4132
+ const res = await axios.get(`${base}/api/v1/admin/analytics/funnel`, {
4133
+ params, headers: { Authorization: `Bearer ${token}` }
4134
+ });
4135
+ const d = res.data.data || {};
4136
+ const stages = d.stages || [];
4137
+ const from = d.from ? new Date(d.from).toLocaleDateString() : '30d ago';
4138
+ const to = d.to ? new Date(d.to).toLocaleDateString() : 'today';
4139
+ console.log(`\nConversion Funnel (${from} → ${to}):`);
4140
+ console.log('─'.repeat(85));
4141
+ stages.forEach((s, i) => {
4142
+ const entered = s.leads_entered_window || 0;
4143
+ const exited = s.leads_exited_window || 0;
4144
+ const dropOff = s.drop_off_rate != null ? `${s.drop_off_rate}% drop-off` : 'N/A';
4145
+ const conv = s.conversion_rate_to_next_stage != null ? `${s.conversion_rate_to_next_stage}%` : 'N/A';
4146
+ const bar = '█'.repeat(Math.min(Math.round((entered / Math.max(...stages.map(x => x.leads_entered_window || 0), 1)) * 20), 20));
4147
+ console.log(`${String(s.stage_name).padEnd(20)} ${bar.padEnd(20)} entered: ${String(entered).padStart(4)} | exited: ${String(exited).padStart(4)} | conv: ${String(conv).padStart(5)} | ${dropOff}`);
4148
+ });
4149
+ } catch (err) { handleError(err); }
4150
+ });
4151
+
3821
4152
  // ============================================================
3822
4153
  // GDPR COMMANDS — Sprint 50 T2
3823
4154
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.17.0",
3
+ "version": "2.18.0",
4
4
  "description": "AI-first CRM CLI — manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {