@startanaicompany/crm 2.17.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.
- package/index.js +977 -1
- 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
|
-
|
|
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,579 @@ 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
|
+
// 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' : ''})`));
|
|
3905
|
+
} catch (err) { handleError(err); }
|
|
3906
|
+
});
|
|
3907
|
+
|
|
3908
|
+
tagsCmd
|
|
3909
|
+
.command('leads <tag-name>')
|
|
3910
|
+
.description('List leads with a specific tag')
|
|
3911
|
+
.option('--limit <n>', 'Max results', '50')
|
|
3912
|
+
.action(async (tagName, opts) => {
|
|
3913
|
+
const globalOpts = program.opts();
|
|
3914
|
+
const client = getClient(globalOpts);
|
|
3915
|
+
try {
|
|
3916
|
+
const res = await client.get('/leads', { params: { tag: tagName, per_page: parseInt(opts.limit) } });
|
|
3917
|
+
const leads = res.data.data || [];
|
|
3918
|
+
if (leads.length === 0) { console.log(`No leads found with tag "${tagName}".`); return; }
|
|
3919
|
+
console.log(`Leads tagged "${tagName}" (${leads.length}):`);
|
|
3920
|
+
leads.forEach(l => console.log(`[${l.id}] ${l.name} <${l.email}> — ${l.status} (${l.assigned_to || 'unassigned'})`));
|
|
3921
|
+
} catch (err) { handleError(err); }
|
|
3922
|
+
});
|
|
3923
|
+
|
|
3924
|
+
// ============================================================
|
|
3925
|
+
// SLA COMMANDS — Sprint 55 T2 (admin JWT via stagesAdminLogin)
|
|
3926
|
+
// ============================================================
|
|
3927
|
+
const slaCmd = program.command('sla').description('Manage SLA escalation rules and view breaches (Sprint 55 T2)');
|
|
3928
|
+
|
|
3929
|
+
slaCmd
|
|
3930
|
+
.command('list')
|
|
3931
|
+
.description('List all SLA escalation rules')
|
|
3932
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3933
|
+
.option('--email <email>', 'Admin email')
|
|
3934
|
+
.option('--password <password>', 'Admin password')
|
|
3935
|
+
.action(async (opts) => {
|
|
3936
|
+
const globalOpts = program.opts();
|
|
3937
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
3938
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
3939
|
+
const cfg = loadConfig();
|
|
3940
|
+
try {
|
|
3941
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
3942
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
3943
|
+
const res = await axios.get(`${base}/api/v1/admin/sla-escalation-rules`, {
|
|
3944
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3945
|
+
});
|
|
3946
|
+
const rules = res.data.data || res.data || [];
|
|
3947
|
+
if (rules.length === 0) { console.log('No SLA rules configured.'); return; }
|
|
3948
|
+
rules.forEach(r => {
|
|
3949
|
+
const hours = (r.escalate_after_breach_minutes / 60).toFixed(1);
|
|
3950
|
+
const stage = r.stage_name_label ? `stage: ${r.stage_name_label}` : 'all stages';
|
|
3951
|
+
const active = r.is_active ? '✓' : '✗';
|
|
3952
|
+
console.log(`[${r.id}] ${active} "${r.name}" — ${hours}h breach → escalate to ${r.escalate_to} (${stage})`);
|
|
3953
|
+
});
|
|
3954
|
+
} catch (err) { handleError(err); }
|
|
3955
|
+
});
|
|
3956
|
+
|
|
3957
|
+
slaCmd
|
|
3958
|
+
.command('create')
|
|
3959
|
+
.description('Create a new SLA escalation rule')
|
|
3960
|
+
.requiredOption('--name <name>', 'Rule name')
|
|
3961
|
+
.requiredOption('--hours <n>', 'Hours after breach before escalating')
|
|
3962
|
+
.requiredOption('--escalate-to <agent>', 'Agent/email to escalate to')
|
|
3963
|
+
.option('--stage-id <id>', 'Pipeline stage UUID to scope this rule (omit for all stages)')
|
|
3964
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3965
|
+
.option('--email <email>', 'Admin email')
|
|
3966
|
+
.option('--password <password>', 'Admin password')
|
|
3967
|
+
.action(async (opts) => {
|
|
3968
|
+
const globalOpts = program.opts();
|
|
3969
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
3970
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
3971
|
+
const cfg = loadConfig();
|
|
3972
|
+
const minutes = Math.round(parseFloat(opts.hours) * 60);
|
|
3973
|
+
if (isNaN(minutes) || minutes < 1) { console.error('Error: --hours must be a positive number'); process.exit(1); }
|
|
3974
|
+
try {
|
|
3975
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
3976
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
3977
|
+
const body = { name: opts.name, escalate_after_breach_minutes: minutes, escalate_to: opts.escalateTo };
|
|
3978
|
+
if (opts.stageId) body.stage_id = opts.stageId;
|
|
3979
|
+
const res = await axios.post(`${base}/api/v1/admin/sla-escalation-rules`, body, {
|
|
3980
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
|
|
3981
|
+
});
|
|
3982
|
+
console.log(`SLA rule created: ${res.data.data?.id || res.data?.id}`);
|
|
3983
|
+
printJSON(res.data);
|
|
3984
|
+
} catch (err) { handleError(err); }
|
|
3985
|
+
});
|
|
3986
|
+
|
|
3987
|
+
slaCmd
|
|
3988
|
+
.command('update <id>')
|
|
3989
|
+
.description('Update an SLA escalation rule')
|
|
3990
|
+
.option('--name <name>', 'New rule name')
|
|
3991
|
+
.option('--hours <n>', 'New escalation threshold in hours')
|
|
3992
|
+
.option('--escalate-to <agent>', 'New escalation target')
|
|
3993
|
+
.option('--active <bool>', 'Enable/disable rule (true|false)')
|
|
3994
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
3995
|
+
.option('--email <email>', 'Admin email')
|
|
3996
|
+
.option('--password <password>', 'Admin password')
|
|
3997
|
+
.action(async (id, opts) => {
|
|
3998
|
+
const globalOpts = program.opts();
|
|
3999
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4000
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4001
|
+
const cfg = loadConfig();
|
|
4002
|
+
const body = {};
|
|
4003
|
+
if (opts.name) body.name = opts.name;
|
|
4004
|
+
if (opts.hours !== undefined) {
|
|
4005
|
+
const minutes = Math.round(parseFloat(opts.hours) * 60);
|
|
4006
|
+
if (isNaN(minutes) || minutes < 1) { console.error('Error: --hours must be a positive number'); process.exit(1); }
|
|
4007
|
+
body.escalate_after_breach_minutes = minutes;
|
|
4008
|
+
}
|
|
4009
|
+
if (opts.escalateTo) body.escalate_to = opts.escalateTo;
|
|
4010
|
+
if (opts.active !== undefined) body.is_active = opts.active === 'true';
|
|
4011
|
+
if (Object.keys(body).length === 0) { console.error('Error: provide at least one field to update'); process.exit(1); }
|
|
4012
|
+
try {
|
|
4013
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4014
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4015
|
+
const res = await axios.patch(`${base}/api/v1/admin/sla-escalation-rules/${id}`, body, {
|
|
4016
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
|
|
4017
|
+
});
|
|
4018
|
+
console.log(`SLA rule ${id} updated.`);
|
|
4019
|
+
printJSON(res.data);
|
|
4020
|
+
} catch (err) { handleError(err); }
|
|
4021
|
+
});
|
|
4022
|
+
|
|
4023
|
+
slaCmd
|
|
4024
|
+
.command('delete <id>')
|
|
4025
|
+
.description('Delete an SLA escalation rule')
|
|
4026
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4027
|
+
.option('--email <email>', 'Admin email')
|
|
4028
|
+
.option('--password <password>', 'Admin password')
|
|
4029
|
+
.action(async (id, opts) => {
|
|
4030
|
+
const globalOpts = program.opts();
|
|
4031
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4032
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4033
|
+
const cfg = loadConfig();
|
|
4034
|
+
try {
|
|
4035
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4036
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4037
|
+
await axios.delete(`${base}/api/v1/admin/sla-escalation-rules/${id}`, {
|
|
4038
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
4039
|
+
});
|
|
4040
|
+
console.log(`SLA rule ${id} deleted.`);
|
|
4041
|
+
} catch (err) { handleError(err); }
|
|
4042
|
+
});
|
|
4043
|
+
|
|
4044
|
+
slaCmd
|
|
4045
|
+
.command('breaches')
|
|
4046
|
+
.description('List leads with active SLA breaches')
|
|
4047
|
+
.option('--status <status>', 'open (default) or resolved', 'open')
|
|
4048
|
+
.option('--limit <n>', 'Max results', '50')
|
|
4049
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4050
|
+
.option('--email <email>', 'Admin email')
|
|
4051
|
+
.option('--password <password>', 'Admin password')
|
|
4052
|
+
.action(async (opts) => {
|
|
4053
|
+
const globalOpts = program.opts();
|
|
4054
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4055
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4056
|
+
const cfg = loadConfig();
|
|
4057
|
+
try {
|
|
4058
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4059
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4060
|
+
const res = await axios.get(`${base}/api/v1/admin/sla-breaches`, {
|
|
4061
|
+
params: { status: opts.status, limit: opts.limit },
|
|
4062
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
4063
|
+
});
|
|
4064
|
+
const d = res.data.data || {};
|
|
4065
|
+
const items = d.items || [];
|
|
4066
|
+
if (items.length === 0) { console.log(`No ${opts.status} SLA breaches.`); return; }
|
|
4067
|
+
console.log(`${opts.status.toUpperCase()} SLA breaches (${d.total || items.length}):`);
|
|
4068
|
+
items.forEach(l => {
|
|
4069
|
+
const breached = l.breached_at ? new Date(l.breached_at).toLocaleString() : 'N/A';
|
|
4070
|
+
console.log(`[${l.id}] ${l.name} <${l.email}> — ${l.status} | stage: ${l.pipeline_stage || 'N/A'} | breached: ${breached} | assigned: ${l.assigned_to || 'none'}`);
|
|
4071
|
+
});
|
|
4072
|
+
} catch (err) { handleError(err); }
|
|
4073
|
+
});
|
|
4074
|
+
|
|
4075
|
+
// ============================================================
|
|
4076
|
+
// ANALYTICS COMMANDS — Sprint 55 T3 (admin JWT via stagesAdminLogin)
|
|
4077
|
+
// (analyticsCmd already declared above — appending pipeline + funnel subcommands)
|
|
4078
|
+
// ============================================================
|
|
4079
|
+
|
|
4080
|
+
analyticsCmd
|
|
4081
|
+
.command('pipeline')
|
|
4082
|
+
.description('Stage-by-stage pipeline breakdown: lead count, avg deal value, avg time-in-stage, conversion rate')
|
|
4083
|
+
.option('--stage <name>', 'Filter output to a single stage name')
|
|
4084
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4085
|
+
.option('--email <email>', 'Admin email')
|
|
4086
|
+
.option('--password <password>', 'Admin password')
|
|
4087
|
+
.action(async (opts) => {
|
|
4088
|
+
const globalOpts = program.opts();
|
|
4089
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4090
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4091
|
+
const cfg = loadConfig();
|
|
4092
|
+
try {
|
|
4093
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4094
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4095
|
+
const res = await axios.get(`${base}/api/v1/admin/analytics/funnel`, {
|
|
4096
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
4097
|
+
});
|
|
4098
|
+
let stages = (res.data.data?.stages || []);
|
|
4099
|
+
if (opts.stage) {
|
|
4100
|
+
stages = stages.filter(s => s.stage_name.toLowerCase() === opts.stage.toLowerCase());
|
|
4101
|
+
if (stages.length === 0) { console.log(`Stage "${opts.stage}" not found.`); return; }
|
|
4102
|
+
}
|
|
4103
|
+
console.log('\nPipeline Breakdown:');
|
|
4104
|
+
console.log('─'.repeat(90));
|
|
4105
|
+
stages.forEach(s => {
|
|
4106
|
+
const deal = s.avg_deal_value != null ? `$${s.avg_deal_value}` : 'N/A';
|
|
4107
|
+
const time = s.avg_time_in_stage_hours != null ? `${s.avg_time_in_stage_hours}h` : 'N/A';
|
|
4108
|
+
const conv = s.conversion_rate_to_next_stage != null ? `${s.conversion_rate_to_next_stage}%` : 'N/A';
|
|
4109
|
+
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}`);
|
|
4110
|
+
});
|
|
4111
|
+
} catch (err) { handleError(err); }
|
|
4112
|
+
});
|
|
4113
|
+
|
|
4114
|
+
analyticsCmd
|
|
4115
|
+
.command('funnel')
|
|
4116
|
+
.description('Full top-to-bottom conversion funnel with drop-off rates')
|
|
4117
|
+
.option('--from <date>', 'From date (YYYY-MM-DD, default: 30 days ago)')
|
|
4118
|
+
.option('--to <date>', 'To date (YYYY-MM-DD, default: today)')
|
|
4119
|
+
.option('--workspace <slug>', 'Workspace slug')
|
|
4120
|
+
.option('--email <email>', 'Admin email')
|
|
4121
|
+
.option('--password <password>', 'Admin password')
|
|
4122
|
+
.action(async (opts) => {
|
|
4123
|
+
const globalOpts = program.opts();
|
|
4124
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
4125
|
+
if (!apiUrl) { console.error('Error: API URL not configured.'); process.exit(1); }
|
|
4126
|
+
const cfg = loadConfig();
|
|
4127
|
+
try {
|
|
4128
|
+
const base = apiUrl.replace(/\/$/, '');
|
|
4129
|
+
const token = cfg.token || await stagesAdminLogin(base, cfg, opts);
|
|
4130
|
+
const params = {};
|
|
4131
|
+
if (opts.from) params.from = opts.from;
|
|
4132
|
+
if (opts.to) params.to = opts.to;
|
|
4133
|
+
const res = await axios.get(`${base}/api/v1/admin/analytics/funnel`, {
|
|
4134
|
+
params, headers: { Authorization: `Bearer ${token}` }
|
|
4135
|
+
});
|
|
4136
|
+
const d = res.data.data || {};
|
|
4137
|
+
const stages = d.stages || [];
|
|
4138
|
+
const from = d.from ? new Date(d.from).toLocaleDateString() : '30d ago';
|
|
4139
|
+
const to = d.to ? new Date(d.to).toLocaleDateString() : 'today';
|
|
4140
|
+
console.log(`\nConversion Funnel (${from} → ${to}):`);
|
|
4141
|
+
console.log('─'.repeat(85));
|
|
4142
|
+
stages.forEach((s, i) => {
|
|
4143
|
+
const entered = s.leads_entered_window || 0;
|
|
4144
|
+
const exited = s.leads_exited_window || 0;
|
|
4145
|
+
const dropOff = s.drop_off_rate != null ? `${s.drop_off_rate}% drop-off` : 'N/A';
|
|
4146
|
+
const conv = s.conversion_rate_to_next_stage != null ? `${s.conversion_rate_to_next_stage}%` : 'N/A';
|
|
4147
|
+
const bar = '█'.repeat(Math.min(Math.round((entered / Math.max(...stages.map(x => x.leads_entered_window || 0), 1)) * 20), 20));
|
|
4148
|
+
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}`);
|
|
4149
|
+
});
|
|
4150
|
+
} catch (err) { handleError(err); }
|
|
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
|
+
|
|
3821
4396
|
// ============================================================
|
|
3822
4397
|
// GDPR COMMANDS — Sprint 50 T2
|
|
3823
4398
|
// ============================================================
|
|
@@ -3956,4 +4531,405 @@ reportsCmd
|
|
|
3956
4531
|
} catch (err) { handleError(err); }
|
|
3957
4532
|
});
|
|
3958
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
|
+
|
|
3959
4935
|
program.parse(process.argv);
|