callkaro-cli-unofficial 0.7.1 → 0.8.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/dist/commands/agentVersions.js +153 -0
- package/dist/commands/aiTools.js +64 -0
- package/dist/commands/analytics.js +66 -0
- package/dist/commands/analyzers.js +99 -0
- package/dist/commands/dnd.js +105 -0
- package/dist/commands/feedback.js +84 -0
- package/dist/commands/knowledge.js +33 -0
- package/dist/commands/kpi.js +631 -12
- package/dist/commands/simulations.js +68 -0
- package/dist/index.js +310 -1
- package/dist/lib/dashboardApi.js +262 -0
- package/dist/lib/kpiReport.js +363 -0
- package/package.json +1 -1
package/dist/commands/kpi.js
CHANGED
|
@@ -12,7 +12,32 @@ exports.kpiAssignCommand = kpiAssignCommand;
|
|
|
12
12
|
exports.kpiAssigneesCommand = kpiAssigneesCommand;
|
|
13
13
|
exports.kpiTargetCommand = kpiTargetCommand;
|
|
14
14
|
exports.kpiRcaCommand = kpiRcaCommand;
|
|
15
|
+
exports.kpiConfigCommand = kpiConfigCommand;
|
|
16
|
+
exports.kpiReportSchemaCommand = kpiReportSchemaCommand;
|
|
17
|
+
exports.kpiReportGetCommand = kpiReportGetCommand;
|
|
18
|
+
exports.kpiReportSetCommand = kpiReportSetCommand;
|
|
19
|
+
exports.kpiReportPreviewCommand = kpiReportPreviewCommand;
|
|
20
|
+
exports.kpiReportExportCommand = kpiReportExportCommand;
|
|
21
|
+
exports.kpiReportAddVarCommand = kpiReportAddVarCommand;
|
|
22
|
+
exports.kpiReportRemoveVarCommand = kpiReportRemoveVarCommand;
|
|
23
|
+
exports.kpiReportAddElementCommand = kpiReportAddElementCommand;
|
|
24
|
+
exports.kpiReportRemoveElementCommand = kpiReportRemoveElementCommand;
|
|
25
|
+
exports.kpiImportCommand = kpiImportCommand;
|
|
26
|
+
exports.kpiDetachCommand = kpiDetachCommand;
|
|
27
|
+
exports.kpiRcaReportsCommand = kpiRcaReportsCommand;
|
|
28
|
+
exports.kpiRcaTimelineCommand = kpiRcaTimelineCommand;
|
|
29
|
+
exports.kpiRcaPdfCommand = kpiRcaPdfCommand;
|
|
30
|
+
exports.kpiRcaDraftPromptCommand = kpiRcaDraftPromptCommand;
|
|
31
|
+
exports.kpiSourceAddApiCommand = kpiSourceAddApiCommand;
|
|
32
|
+
exports.kpiSourceAddPythonCommand = kpiSourceAddPythonCommand;
|
|
33
|
+
exports.kpiSourceAddExprCommand = kpiSourceAddExprCommand;
|
|
34
|
+
exports.kpiSourceRemoveCommand = kpiSourceRemoveCommand;
|
|
35
|
+
exports.kpiMetricSetCommand = kpiMetricSetCommand;
|
|
36
|
+
exports.kpiMetricRemoveCommand = kpiMetricRemoveCommand;
|
|
37
|
+
const fs_1 = require("fs");
|
|
15
38
|
const dashboardApi_1 = require("../lib/dashboardApi");
|
|
39
|
+
const setJson_1 = require("./setJson");
|
|
40
|
+
const kpiReport_1 = require("../lib/kpiReport");
|
|
16
41
|
const ui_1 = require("../lib/ui");
|
|
17
42
|
/** Table cells are one line each; these keep a long prompt from wrapping a row. */
|
|
18
43
|
function clip(value, max = 60) {
|
|
@@ -335,7 +360,11 @@ async function kpiHistoryCommand(opts) {
|
|
|
335
360
|
if (!agentIds.length) {
|
|
336
361
|
return (0, ui_1.handleError)(new Error(`No agent ids in --agents "${opts.agents}".\n Use a comma-separated list, e.g. --agents abc123,def456`));
|
|
337
362
|
}
|
|
338
|
-
const res = await (0, dashboardApi_1.dashboardApi)().kpiHistory({
|
|
363
|
+
const res = await (0, dashboardApi_1.dashboardApi)().kpiHistory({
|
|
364
|
+
agentIds: agentIds.join(','),
|
|
365
|
+
startDate: opts.since ? dateBound(opts.since, false) : undefined,
|
|
366
|
+
endDate: opts.until ? dateBound(opts.until, true) : undefined,
|
|
367
|
+
});
|
|
339
368
|
if (opts.json)
|
|
340
369
|
return void console.log(JSON.stringify(res, null, 2));
|
|
341
370
|
// Deliberately generic: this response's shape was not confirmed field by field,
|
|
@@ -481,6 +510,18 @@ async function kpiCommentCommand(taskId, text, opts = {}) {
|
|
|
481
510
|
*/
|
|
482
511
|
async function kpiAssignCommand(taskId, opts) {
|
|
483
512
|
try {
|
|
513
|
+
if (opts.unassign) {
|
|
514
|
+
const res = await (0, dashboardApi_1.dashboardApi)().savePendingAssignments([
|
|
515
|
+
{ taskId, unassign: true, versionIds: splitList(opts.versions), versionNames: splitList(opts.versionNames) },
|
|
516
|
+
]);
|
|
517
|
+
const result = Array.isArray(res?.results) ? res.results[0] : undefined;
|
|
518
|
+
if (result && result.success === false) {
|
|
519
|
+
return (0, ui_1.handleError)(new Error(result.error || 'The server refused the unassignment.'));
|
|
520
|
+
}
|
|
521
|
+
if (opts.json)
|
|
522
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
523
|
+
return void (0, ui_1.success)(`Unassigned ${taskId}.`);
|
|
524
|
+
}
|
|
484
525
|
let to = String(opts.to || '').trim();
|
|
485
526
|
if (!to) {
|
|
486
527
|
return (0, ui_1.handleError)(new Error('--to <who> is required.\n' +
|
|
@@ -606,17 +647,25 @@ async function kpiTargetCommand(opts) {
|
|
|
606
647
|
throw err;
|
|
607
648
|
}
|
|
608
649
|
const metricName = 'connected_to_conversion_percentage';
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
650
|
+
const target = { name: metricName, source: metricName, value: String(opts.value).trim() };
|
|
651
|
+
let body;
|
|
652
|
+
if (Object.keys(current).length) {
|
|
653
|
+
// Existing config: send all 10 fields so data sources, report and extra
|
|
654
|
+
// metrics survive the wholesale PUT.
|
|
655
|
+
const rest = (Array.isArray(current.kpi_metrics) ? current.kpi_metrics : []).filter((m) => m?.name !== metricName && m?.name !== 'connected_to_conversion');
|
|
656
|
+
body = (0, kpiReport_1.buildFullKpiPayload)(current, {
|
|
657
|
+
kpi_metrics: [target, ...rest],
|
|
658
|
+
...(opts.rcaModel !== undefined ? { rca_model: String(opts.rcaModel), rca_llms: (0, kpiReport_1.rcaLlmsFor)(String(opts.rcaModel)) } : {}),
|
|
659
|
+
...(opts.rcaPrompt !== undefined ? { rca_prompt: String(opts.rcaPrompt) } : {}),
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
body = { kpi_metrics: [target] };
|
|
664
|
+
if (opts.rcaModel)
|
|
665
|
+
body.rca_model = String(opts.rcaModel);
|
|
666
|
+
if (opts.rcaPrompt !== undefined)
|
|
667
|
+
body.rca_prompt = String(opts.rcaPrompt);
|
|
668
|
+
}
|
|
620
669
|
const res = await api.setKpiAgentConfig(agentId, body);
|
|
621
670
|
if (opts.json)
|
|
622
671
|
return void console.log(JSON.stringify(res ?? body, null, 2));
|
|
@@ -684,3 +733,573 @@ function splitList(raw) {
|
|
|
684
733
|
.map((s) => s.trim())
|
|
685
734
|
.filter(Boolean);
|
|
686
735
|
}
|
|
736
|
+
// --- KPI config & report ------------------------------------------------------
|
|
737
|
+
/** Read the agent's config; 404 is "none yet", anything else must stop the write. */
|
|
738
|
+
async function loadKpiConfig(agentId) {
|
|
739
|
+
try {
|
|
740
|
+
return (await (0, dashboardApi_1.dashboardApi)().kpiAgentConfig(agentId)) || {};
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
if (!(0, dashboardApi_1.isNotFound)(err))
|
|
744
|
+
throw err;
|
|
745
|
+
return {};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/** PUT the full 10-field payload and report. */
|
|
749
|
+
async function saveKpiConfig(agentId, current, patch, json, msg) {
|
|
750
|
+
const payload = (0, kpiReport_1.buildFullKpiPayload)(current, patch);
|
|
751
|
+
const res = await (0, dashboardApi_1.dashboardApi)().setKpiAgentConfig(agentId, payload);
|
|
752
|
+
if (json)
|
|
753
|
+
return void console.log(JSON.stringify(res ?? payload, null, 2));
|
|
754
|
+
(0, ui_1.success)(msg);
|
|
755
|
+
}
|
|
756
|
+
/** `cku kpi config <agentId>` — show or edit the whole KPI Manager document. */
|
|
757
|
+
async function kpiConfigCommand(agentId, opts) {
|
|
758
|
+
try {
|
|
759
|
+
const current = await loadKpiConfig(agentId);
|
|
760
|
+
const mutate = opts.set || opts.enableRca || opts.disableRca || opts.enableReport || opts.disableReport ||
|
|
761
|
+
opts.rcaModel !== undefined || opts.rcaPrompt !== undefined;
|
|
762
|
+
if (!mutate) {
|
|
763
|
+
if (opts.json)
|
|
764
|
+
return void console.log(JSON.stringify(current, null, 2));
|
|
765
|
+
(0, ui_1.keyValue)({
|
|
766
|
+
agentId,
|
|
767
|
+
rca_model: current.rca_model ?? '',
|
|
768
|
+
nightly_rca_enabled: String(current.nightly_rca_enabled !== false),
|
|
769
|
+
report_enabled: String(current.report_enabled === true),
|
|
770
|
+
metrics: (current.kpi_metrics || []).map((m) => `${m.name}=${m.value}`).join(', ') || '(none)',
|
|
771
|
+
api_endpoints: (current.api_endpoints || []).length,
|
|
772
|
+
python_data_sources: (current.python_data_sources || []).length,
|
|
773
|
+
expression_data_sources: (current.expression_data_sources || []).length,
|
|
774
|
+
report_elements: (current.report_config?.elements || []).length,
|
|
775
|
+
rca_prompt: clip(current.rca_prompt, 80),
|
|
776
|
+
});
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const setPatch = opts.set ? (0, setJson_1.readSetJson)(opts.set) : {};
|
|
780
|
+
await saveKpiConfig(agentId, current, {
|
|
781
|
+
...setPatch,
|
|
782
|
+
...(opts.rcaModel !== undefined ? { rca_model: opts.rcaModel } : {}),
|
|
783
|
+
...(opts.rcaPrompt !== undefined ? { rca_prompt: opts.rcaPrompt } : {}),
|
|
784
|
+
...(opts.enableRca ? { nightly_rca_enabled: true } : opts.disableRca ? { nightly_rca_enabled: false } : {}),
|
|
785
|
+
...(opts.enableReport ? { report_enabled: true } : opts.disableReport ? { report_enabled: false } : {}),
|
|
786
|
+
}, opts.json, `KPI config for ${agentId} saved.`);
|
|
787
|
+
}
|
|
788
|
+
catch (err) {
|
|
789
|
+
(0, ui_1.handleError)(err);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/** `cku kpi report schema` */
|
|
793
|
+
async function kpiReportSchemaCommand() {
|
|
794
|
+
console.log(kpiReport_1.REPORT_SCHEMA_REFERENCE);
|
|
795
|
+
}
|
|
796
|
+
/** `cku kpi report get <agentId>` */
|
|
797
|
+
async function kpiReportGetCommand(agentId, opts) {
|
|
798
|
+
try {
|
|
799
|
+
const current = await loadKpiConfig(agentId);
|
|
800
|
+
const cfg = (0, kpiReport_1.normalizeReportConfig)(current.report_config);
|
|
801
|
+
const enabled = Boolean(current.report_enabled);
|
|
802
|
+
if (opts.json) {
|
|
803
|
+
return void console.log(JSON.stringify({ agentId, report_enabled: enabled, report_config: cfg }, null, 2));
|
|
804
|
+
}
|
|
805
|
+
(0, ui_1.keyValue)({
|
|
806
|
+
agentId,
|
|
807
|
+
report_enabled: enabled,
|
|
808
|
+
recipients: cfg.recipients.join(', ') || '(none)',
|
|
809
|
+
schedule: `${cfg.min_trigger_time} – ${cfg.max_trigger_time} IST, every ${cfg.interval_hours}h`,
|
|
810
|
+
custom_variables: cfg.custom_variables.length,
|
|
811
|
+
elements: cfg.elements.length,
|
|
812
|
+
});
|
|
813
|
+
if (cfg.custom_variables.length) {
|
|
814
|
+
(0, ui_1.table)(['NAME', 'MODE', 'CONSIDER FOR', 'RULE'], cfg.custom_variables.map((v) => [
|
|
815
|
+
v.name || '', v.mode || '', (v.consider_for || []).join(','), clip(v.mode === 'conditions' ? JSON.stringify(v.conditions ?? []) : (v.expression || ''), 50),
|
|
816
|
+
]));
|
|
817
|
+
}
|
|
818
|
+
if (cfg.elements.length) {
|
|
819
|
+
(0, ui_1.table)(['ID', 'TYPE', 'POS (x,y,w,h)', 'SUMMARY'], cfg.elements.map((e) => [
|
|
820
|
+
e.id || '',
|
|
821
|
+
e.type === 'chart' ? `chart/${e.chart_type}` : e.type || '',
|
|
822
|
+
`${e.x ?? ''},${e.y ?? ''},${e.w ?? ''},${e.h ?? ''}`,
|
|
823
|
+
clip(e.type === 'chart' ? `${e.title || ''} ${(e.y_axis || []).join(' ')}` : e.type === 'table' ? `${(e.cells || []).length} rows` : e.content, 50),
|
|
824
|
+
]));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
(0, ui_1.handleError)(err);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const HHMM = /^\d{2}:\d{2}$/;
|
|
832
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
833
|
+
/** `cku kpi report set <agentId>` */
|
|
834
|
+
async function kpiReportSetCommand(agentId, opts) {
|
|
835
|
+
try {
|
|
836
|
+
const current = await loadKpiConfig(agentId);
|
|
837
|
+
let next = (0, kpiReport_1.normalizeReportConfig)(current.report_config);
|
|
838
|
+
let enabled = Boolean(current.report_enabled);
|
|
839
|
+
if (opts.set) {
|
|
840
|
+
let patch = (0, setJson_1.readSetJson)(opts.set);
|
|
841
|
+
if (patch.report_config && typeof patch.report_config === 'object') {
|
|
842
|
+
if (patch.report_enabled !== undefined)
|
|
843
|
+
enabled = Boolean(patch.report_enabled);
|
|
844
|
+
patch = patch.report_config;
|
|
845
|
+
}
|
|
846
|
+
next = (0, kpiReport_1.normalizeReportConfig)({ ...next, ...patch });
|
|
847
|
+
}
|
|
848
|
+
if (opts.recipients !== undefined) {
|
|
849
|
+
const list = opts.recipients.split(/[,;\s]+/).map((r) => r.trim().toLowerCase()).filter(Boolean);
|
|
850
|
+
const bad = list.filter((r) => !EMAIL.test(r));
|
|
851
|
+
if (bad.length)
|
|
852
|
+
throw new Error(`Invalid recipient email(s): ${bad.join(', ')}`);
|
|
853
|
+
if (list.length > 10)
|
|
854
|
+
throw new Error(`At most 10 recipients (got ${list.length}).`);
|
|
855
|
+
next.recipients = list;
|
|
856
|
+
}
|
|
857
|
+
if (opts.interval !== undefined) {
|
|
858
|
+
const n = Number(opts.interval);
|
|
859
|
+
if (!kpiReport_1.INTERVAL_HOURS.includes(n))
|
|
860
|
+
throw new Error(`--interval must be one of: ${kpiReport_1.INTERVAL_HOURS.join(', ')} (got "${opts.interval}").`);
|
|
861
|
+
next.interval_hours = n;
|
|
862
|
+
}
|
|
863
|
+
for (const [flag, key, v] of [['--min-trigger', 'min_trigger_time', opts.minTrigger], ['--max-trigger', 'max_trigger_time', opts.maxTrigger]]) {
|
|
864
|
+
if (v === undefined)
|
|
865
|
+
continue;
|
|
866
|
+
if (!HHMM.test(v))
|
|
867
|
+
throw new Error(`${flag} must be HH:mm (got "${v}").`);
|
|
868
|
+
next[key] = v;
|
|
869
|
+
}
|
|
870
|
+
if (Number(next.interval_hours) >= 24)
|
|
871
|
+
next.max_trigger_time = next.min_trigger_time;
|
|
872
|
+
if (opts.enable)
|
|
873
|
+
enabled = true;
|
|
874
|
+
else if (opts.disable)
|
|
875
|
+
enabled = false;
|
|
876
|
+
await saveKpiConfig(agentId, current, { report_enabled: enabled, report_config: next }, opts.json, `KPI report for ${agentId} saved.`);
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
(0, ui_1.handleError)(err);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/** Saved report config with `--set` / `--interval` overlaid, for preview and export. */
|
|
883
|
+
function overlayReport(current, opts) {
|
|
884
|
+
const cfg = (0, kpiReport_1.normalizeReportConfig)({
|
|
885
|
+
...(0, kpiReport_1.normalizeReportConfig)(current.report_config),
|
|
886
|
+
...(opts.set ? (0, setJson_1.readSetJson)(opts.set) : {}),
|
|
887
|
+
...(opts.interval !== undefined ? { interval_hours: Number(opts.interval) } : {}),
|
|
888
|
+
});
|
|
889
|
+
return cfg;
|
|
890
|
+
}
|
|
891
|
+
/** `cku kpi report preview <agentId>` */
|
|
892
|
+
async function kpiReportPreviewCommand(agentId, opts) {
|
|
893
|
+
try {
|
|
894
|
+
const cfg = overlayReport(await loadKpiConfig(agentId), opts);
|
|
895
|
+
const res = await (0, dashboardApi_1.dashboardApi)().previewKpiReport(agentId, cfg);
|
|
896
|
+
if (opts.json)
|
|
897
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
898
|
+
const values = res?.values || {};
|
|
899
|
+
const labels = res?.labels || {};
|
|
900
|
+
(0, ui_1.info)(`Window: ${res?.window?.label ?? '?'} Generated: ${res?.generated_at ?? '?'}`);
|
|
901
|
+
const rows = Object.entries(values)
|
|
902
|
+
.filter(([ref]) => !ref.startsWith('chart:') && !ref.endsWith('@trend'))
|
|
903
|
+
.map(([ref, v]) => [ref, labels[ref] || v?.label || '', (0, kpiReport_1.formatPreviewValue)(v)]);
|
|
904
|
+
if (rows.length)
|
|
905
|
+
(0, ui_1.table)(['REF', 'LABEL', 'VALUE'], rows);
|
|
906
|
+
for (const el of cfg.elements) {
|
|
907
|
+
const series = values[`chart:${el.id}`] ?? res?.charts?.[el.id] ?? res?.series?.[el.id];
|
|
908
|
+
if (el.type === 'chart' && series)
|
|
909
|
+
(0, ui_1.info)(`\nchart:${el.id} (${el.title || el.chart_type})\n${JSON.stringify(series)}`);
|
|
910
|
+
else if (el.type === 'text')
|
|
911
|
+
(0, ui_1.info)(`\n${(0, kpiReport_1.interpolateReportText)(el.content, values)}`);
|
|
912
|
+
else if (el.type === 'table') {
|
|
913
|
+
const cells = (el.cells || []).map((r) => r.map((c) => (0, kpiReport_1.interpolateReportText)(c, values)));
|
|
914
|
+
if (cells.length)
|
|
915
|
+
(0, ui_1.table)(cells[0], cells.slice(1));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
catch (err) {
|
|
920
|
+
(0, ui_1.handleError)(err);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/** `cku kpi report export <agentId>` — render to PDF. */
|
|
924
|
+
async function kpiReportExportCommand(agentId, opts) {
|
|
925
|
+
try {
|
|
926
|
+
const cfg = overlayReport(await loadKpiConfig(agentId), opts);
|
|
927
|
+
const { bytes, filename } = await (0, dashboardApi_1.dashboardApi)().exportKpiReport(agentId, cfg);
|
|
928
|
+
const file = opts.file || filename || `kpi_report_${agentId}.pdf`;
|
|
929
|
+
(0, fs_1.writeFileSync)(file, bytes);
|
|
930
|
+
(0, ui_1.success)(`Wrote ${file} (${bytes.length} bytes).`);
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
(0, ui_1.handleError)(err);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/** Load config + its report, apply `edit`, save. Shared by the var/element commands. */
|
|
937
|
+
async function editReport(agentId, json, msg, edit) {
|
|
938
|
+
const current = await loadKpiConfig(agentId);
|
|
939
|
+
const cfg = (0, kpiReport_1.normalizeReportConfig)(current.report_config);
|
|
940
|
+
edit(cfg);
|
|
941
|
+
await saveKpiConfig(agentId, current, { report_config: cfg }, json, msg);
|
|
942
|
+
}
|
|
943
|
+
/** `cku kpi report add-var <agentId>` */
|
|
944
|
+
async function kpiReportAddVarCommand(agentId, opts) {
|
|
945
|
+
try {
|
|
946
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(opts.name || '')) {
|
|
947
|
+
throw new Error('--name must be lowercase letters, digits, underscores, start with a letter, max 64 chars.');
|
|
948
|
+
}
|
|
949
|
+
const mode = opts.conditions ? 'conditions' : 'expression';
|
|
950
|
+
if (mode === 'expression' && !opts.expression?.trim()) {
|
|
951
|
+
throw new Error('--expression <rule> is required (or --conditions <json>). See: cku kpi report schema');
|
|
952
|
+
}
|
|
953
|
+
const consider = (opts.considerFor || 'all').split(',').map((s) => s.trim()).filter(Boolean);
|
|
954
|
+
const badConsider = consider.filter((c) => c !== 'all' && c !== 'unique');
|
|
955
|
+
if (badConsider.length)
|
|
956
|
+
throw new Error(`--consider-for takes all and/or unique (got ${badConsider.join(', ')}).`);
|
|
957
|
+
const variable = { name: opts.name, mode, conditions: [], expression: '', equals: true, consider_for: consider };
|
|
958
|
+
if (mode === 'conditions')
|
|
959
|
+
variable.conditions = (0, setJson_1.readSetJson)(opts.conditions);
|
|
960
|
+
else
|
|
961
|
+
variable.expression = opts.expression.trim();
|
|
962
|
+
await editReport(agentId, opts.json, `Custom variable ${opts.name} saved.`, (cfg) => {
|
|
963
|
+
const i = cfg.custom_variables.findIndex((v) => v.name === opts.name);
|
|
964
|
+
if (i >= 0)
|
|
965
|
+
cfg.custom_variables[i] = variable;
|
|
966
|
+
else
|
|
967
|
+
cfg.custom_variables.push(variable);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
(0, ui_1.handleError)(err);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/** `cku kpi report remove-var <agentId> <name>` */
|
|
975
|
+
async function kpiReportRemoveVarCommand(agentId, name, opts) {
|
|
976
|
+
try {
|
|
977
|
+
await editReport(agentId, opts.json, `Custom variable ${name} removed.`, (cfg) => {
|
|
978
|
+
cfg.custom_variables = cfg.custom_variables.filter((v) => v.name !== name);
|
|
979
|
+
const strip = (list) => (list || []).filter((r) => parseCustom(r) !== name);
|
|
980
|
+
cfg.elements = cfg.elements.map((el) => el.type === 'chart' ? { ...el, refs: strip(el.refs), y_axis: strip(el.y_axis) } : el);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
catch (err) {
|
|
984
|
+
(0, ui_1.handleError)(err);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/** `custom:foo#unique` -> `foo`; anything else -> undefined. */
|
|
988
|
+
function parseCustom(ref) {
|
|
989
|
+
const m = /^custom:([^#]+)/.exec(String(ref));
|
|
990
|
+
return m ? m[1] : undefined;
|
|
991
|
+
}
|
|
992
|
+
/** `cku kpi report add-element <agentId>` */
|
|
993
|
+
async function kpiReportAddElementCommand(agentId, opts) {
|
|
994
|
+
try {
|
|
995
|
+
const shorthand = kpiReport_1.CHART_TYPES.includes(opts.type) && opts.type !== 'table';
|
|
996
|
+
const kind = shorthand ? 'chart' : opts.type;
|
|
997
|
+
if (!['text', 'table', 'chart'].includes(kind)) {
|
|
998
|
+
throw new Error(`--type must be text | table | chart (or a chart type: tiles | bar | line | pie | funnel). Got "${opts.type}".`);
|
|
999
|
+
}
|
|
1000
|
+
const chartType = (shorthand ? opts.type : opts.chartType || 'tiles');
|
|
1001
|
+
if (kind === 'chart' && !kpiReport_1.CHART_TYPES.includes(chartType)) {
|
|
1002
|
+
throw new Error(`--chart-type must be one of: ${kpiReport_1.CHART_TYPES.join(', ')}.`);
|
|
1003
|
+
}
|
|
1004
|
+
const num = (v, flag) => numeric(v, flag);
|
|
1005
|
+
await editReport(agentId, opts.json, `Added ${kind} element.`, (cfg) => {
|
|
1006
|
+
const w = num(opts.w, '--w') ?? (kind === 'chart' ? kpiReport_1.PAGE_W - kpiReport_1.MARGIN * 2 : kpiReport_1.DEFAULT_WIDTHS[kind]);
|
|
1007
|
+
if (w < kpiReport_1.MIN_W)
|
|
1008
|
+
throw new Error(`--w must be at least ${kpiReport_1.MIN_W}.`);
|
|
1009
|
+
const el = {
|
|
1010
|
+
id: (0, kpiReport_1.newElementId)(),
|
|
1011
|
+
type: kind,
|
|
1012
|
+
x: num(opts.x, '--x') ?? kpiReport_1.MARGIN,
|
|
1013
|
+
y: num(opts.y, '--y') ?? (0, kpiReport_1.nextElementY)(cfg.elements),
|
|
1014
|
+
w,
|
|
1015
|
+
};
|
|
1016
|
+
if (kind === 'text') {
|
|
1017
|
+
el.content = opts.content ?? '';
|
|
1018
|
+
el.style = { bold: false, italic: false, underline: false, size: 14, color: '#111827', align: 'left' };
|
|
1019
|
+
}
|
|
1020
|
+
else if (kind === 'table') {
|
|
1021
|
+
el.header = true;
|
|
1022
|
+
if (opts.cells) {
|
|
1023
|
+
const cells = (0, setJson_1.readSetJson)(opts.cells);
|
|
1024
|
+
if (!Array.isArray(cells) || !cells.length || !cells.every((r) => Array.isArray(r))) {
|
|
1025
|
+
throw new Error('--cells must be a JSON array of rows, e.g. [["Metric","Value"],["Calls","${total_calls}"]].');
|
|
1026
|
+
}
|
|
1027
|
+
el.cells = cells;
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
const r = num(opts.rows, '--rows') ?? 2;
|
|
1031
|
+
const c = num(opts.cols, '--cols') ?? 2;
|
|
1032
|
+
if (r < 1 || r > 8 || c < 1 || c > 8)
|
|
1033
|
+
throw new Error('--rows and --cols must be 1..8.');
|
|
1034
|
+
el.cells = Array.from({ length: r }, () => Array.from({ length: c }, () => ''));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
const refs = (opts.refs || '').split(',').map(kpiReport_1.canonicalRef).filter(Boolean);
|
|
1039
|
+
const axes = (0, kpiReport_1.defaultChartAxes)(chartType, refs);
|
|
1040
|
+
el.chart_type = chartType;
|
|
1041
|
+
el.title = opts.title ?? '';
|
|
1042
|
+
el.x_axis = opts.xAxis !== undefined ? opts.xAxis : axes.x_axis;
|
|
1043
|
+
el.y_axis = axes.y_axis;
|
|
1044
|
+
el.refs = axes.y_axis;
|
|
1045
|
+
el.stack_by = opts.stackBy !== undefined ? opts.stackBy : axes.stack_by;
|
|
1046
|
+
el.options = {};
|
|
1047
|
+
el.h = kpiReport_1.CHART_HEIGHTS[chartType] || 280;
|
|
1048
|
+
}
|
|
1049
|
+
if (opts.h !== undefined)
|
|
1050
|
+
el.h = num(opts.h, '--h');
|
|
1051
|
+
cfg.elements.push(el);
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
catch (err) {
|
|
1055
|
+
(0, ui_1.handleError)(err);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/** `cku kpi report remove-element <agentId> <elementId>` */
|
|
1059
|
+
async function kpiReportRemoveElementCommand(agentId, elementId, opts) {
|
|
1060
|
+
try {
|
|
1061
|
+
await editReport(agentId, opts.json, `Element ${elementId} removed.`, (cfg) => {
|
|
1062
|
+
const n = cfg.elements.length;
|
|
1063
|
+
cfg.elements = cfg.elements.filter((e) => e.id !== elementId);
|
|
1064
|
+
if (cfg.elements.length === n) {
|
|
1065
|
+
throw new Error(`No element with id "${elementId}". List ids with: cku kpi report get ${agentId}`);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
catch (err) {
|
|
1070
|
+
(0, ui_1.handleError)(err);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// --- import / detach / RCA reports / data sources -----------------------------
|
|
1074
|
+
/** `cku kpi import --file <rows.json>` — bulk-create cards. */
|
|
1075
|
+
async function kpiImportCommand(opts) {
|
|
1076
|
+
try {
|
|
1077
|
+
const raw = (0, setJson_1.readSetJson)(opts.file.startsWith('@') ? opts.file : '@' + opts.file);
|
|
1078
|
+
const list = Array.isArray(raw) ? raw : raw?.rows;
|
|
1079
|
+
if (!Array.isArray(list) || !list.length) {
|
|
1080
|
+
throw new Error('File must hold a non-empty JSON array of task rows, or {"rows": [...]}.');
|
|
1081
|
+
}
|
|
1082
|
+
const rows = list.map((r) => (r && r.columnId ? { ...r, columnId: columnId(r.columnId) } : r));
|
|
1083
|
+
const res = await (0, dashboardApi_1.dashboardApi)().importPendingTasks(rows);
|
|
1084
|
+
if (opts.json)
|
|
1085
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
1086
|
+
(0, ui_1.success)(`Imported ${res?.created ?? 0} card(s)${res?.skipped?.length ? ` (${res.skipped.length} skipped)` : ''}.`);
|
|
1087
|
+
}
|
|
1088
|
+
catch (err) {
|
|
1089
|
+
(0, ui_1.handleError)(err);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/** `cku kpi detach <taskId> <url>` — remove one attachment. */
|
|
1093
|
+
async function kpiDetachCommand(taskId, url, opts) {
|
|
1094
|
+
try {
|
|
1095
|
+
const res = await (0, dashboardApi_1.dashboardApi)().deletePendingTaskAttachment(taskId, url);
|
|
1096
|
+
if (opts.json)
|
|
1097
|
+
return void console.log(JSON.stringify(res ?? { taskId, url }, null, 2));
|
|
1098
|
+
(0, ui_1.success)(`Detached attachment from ${taskId}.`);
|
|
1099
|
+
}
|
|
1100
|
+
catch (err) {
|
|
1101
|
+
(0, ui_1.handleError)(err);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
/** `cku kpi rca-reports` — daywise RCA reports. */
|
|
1105
|
+
async function kpiRcaReportsCommand(opts) {
|
|
1106
|
+
try {
|
|
1107
|
+
const res = await (0, dashboardApi_1.dashboardApi)().daywiseRcaReports({
|
|
1108
|
+
agentId: opts.agent,
|
|
1109
|
+
startDate: opts.since ? dateBound(opts.since, false) : undefined,
|
|
1110
|
+
endDate: opts.until ? dateBound(opts.until, true) : undefined,
|
|
1111
|
+
source: opts.source,
|
|
1112
|
+
page: Number(opts.page) || 1,
|
|
1113
|
+
pageSize: pageSize(opts.limit, 20),
|
|
1114
|
+
});
|
|
1115
|
+
if (opts.json)
|
|
1116
|
+
return void console.log(JSON.stringify(res?.data || res, null, 2));
|
|
1117
|
+
const rows = Array.isArray(res?.data) ? res.data : [];
|
|
1118
|
+
if (!rows.length)
|
|
1119
|
+
return void (0, ui_1.info)('No RCA reports.');
|
|
1120
|
+
(0, ui_1.table)(['DATE', 'AGENT', 'SOURCE', 'AUDITED', 'ROOT CAUSES', 'CONFIDENCE', 'GAPS'], rows.map((r) => [
|
|
1121
|
+
r.rcaDate || r.date || '',
|
|
1122
|
+
r.agentName || r.agentId || '',
|
|
1123
|
+
r.source || r.reportSource || '',
|
|
1124
|
+
r.auditedCalls ?? r.audited ?? '',
|
|
1125
|
+
r.rootCauseCount ?? '',
|
|
1126
|
+
r.confidence ?? '',
|
|
1127
|
+
Array.isArray(r.metricGaps) && r.metricGaps.length
|
|
1128
|
+
? r.metricGaps.map((g) => `${g.metricName}: ${g.actualPct}% vs ${g.targetPct}% (${g.gapPct > 0 ? '+' : ''}${g.gapPct}pp)`).join('; ')
|
|
1129
|
+
: '',
|
|
1130
|
+
]));
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
(0, ui_1.handleError)(err);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/** `cku kpi rca-timeline <agentId>` — day-by-day RCA execution events. */
|
|
1137
|
+
async function kpiRcaTimelineCommand(agentId, opts) {
|
|
1138
|
+
try {
|
|
1139
|
+
const res = await (0, dashboardApi_1.dashboardApi)().daywiseRcaTimeline(agentId, {
|
|
1140
|
+
startDate: opts.since ? dateBound(opts.since, false) : undefined,
|
|
1141
|
+
endDate: opts.until ? dateBound(opts.until, true) : undefined,
|
|
1142
|
+
});
|
|
1143
|
+
if (opts.json)
|
|
1144
|
+
return void console.log(JSON.stringify(res?.data || res, null, 2));
|
|
1145
|
+
const days = res?.data?.days || [];
|
|
1146
|
+
if (!days.length)
|
|
1147
|
+
return void (0, ui_1.info)('No timeline events.');
|
|
1148
|
+
for (const day of days) {
|
|
1149
|
+
(0, ui_1.info)(`\n${day.date || day.rcaDate || ''}`);
|
|
1150
|
+
(0, ui_1.table)(['TIME', 'STAGE', 'STEP', 'STATUS', 'TITLE', 'DETAIL'], (day.events || []).map((e) => [e.at || '', e.stage || '', e.step || '', e.status || '', clip(e.title, 40), clip(e.detail, 60)]));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
catch (err) {
|
|
1154
|
+
(0, ui_1.handleError)(err);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
/** `cku kpi rca-pdf <agentId> <date>` — download one RCA report PDF. */
|
|
1158
|
+
async function kpiRcaPdfCommand(agentId, rcaDate, opts) {
|
|
1159
|
+
try {
|
|
1160
|
+
const { bytes, filename } = await (0, dashboardApi_1.dashboardApi)().daywiseRcaPdf(agentId, rcaDate, opts.manual ? 'manual' : undefined);
|
|
1161
|
+
const file = opts.file || filename || `rca_${agentId}_${rcaDate}.pdf`;
|
|
1162
|
+
(0, fs_1.writeFileSync)(file, bytes);
|
|
1163
|
+
(0, ui_1.success)(`Wrote ${file} (${bytes.length} bytes).`);
|
|
1164
|
+
}
|
|
1165
|
+
catch (err) {
|
|
1166
|
+
(0, ui_1.handleError)(err);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/** `cku kpi rca-draft-prompt <agentId> --prompt <text>` — AI-draft the RCA prompt. */
|
|
1170
|
+
async function kpiRcaDraftPromptCommand(agentId, opts) {
|
|
1171
|
+
try {
|
|
1172
|
+
const current = await loadKpiConfig(agentId);
|
|
1173
|
+
const res = await (0, dashboardApi_1.dashboardApi)().generateRcaPromptDraft({
|
|
1174
|
+
agentId,
|
|
1175
|
+
text_input: opts.prompt,
|
|
1176
|
+
...(current.rca_prompt ? { existingRcaPrompt: current.rca_prompt } : {}),
|
|
1177
|
+
...(current.kpi_metrics ? { kpi_metrics: current.kpi_metrics } : {}),
|
|
1178
|
+
});
|
|
1179
|
+
const draft = res?.response || res?.data || '';
|
|
1180
|
+
if (opts.save) {
|
|
1181
|
+
return void (await saveKpiConfig(agentId, current, { rca_prompt: String(draft) }, opts.json, 'Saved generated RCA prompt.'));
|
|
1182
|
+
}
|
|
1183
|
+
if (opts.json)
|
|
1184
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
1185
|
+
console.log(typeof draft === 'string' ? draft : JSON.stringify(draft, null, 2));
|
|
1186
|
+
}
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
(0, ui_1.handleError)(err);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const PHONE_PARAM = { key_name: 'phone_number', value: 'user_phone_number' };
|
|
1192
|
+
/** Parameters with the mandatory phone_number entry first (bundle's `PXe`). */
|
|
1193
|
+
function withPhoneParam(raw) {
|
|
1194
|
+
const extra = raw ? (0, setJson_1.readSetJson)(raw) : [];
|
|
1195
|
+
if (!Array.isArray(extra))
|
|
1196
|
+
throw new Error('--params must be a JSON array of {key_name, value}.');
|
|
1197
|
+
return [PHONE_PARAM, ...extra.filter((p) => p?.key_name !== 'phone_number')];
|
|
1198
|
+
}
|
|
1199
|
+
/** Replace the entry with the same name, or append. */
|
|
1200
|
+
function upsertByName(list, item) {
|
|
1201
|
+
const out = [...(list || [])];
|
|
1202
|
+
const i = out.findIndex((x) => x?.name === item.name);
|
|
1203
|
+
if (i >= 0)
|
|
1204
|
+
out[i] = item;
|
|
1205
|
+
else
|
|
1206
|
+
out.push(item);
|
|
1207
|
+
return out;
|
|
1208
|
+
}
|
|
1209
|
+
/** `cku kpi source add-api <agentId>` */
|
|
1210
|
+
async function kpiSourceAddApiCommand(agentId, opts) {
|
|
1211
|
+
try {
|
|
1212
|
+
if (!opts.name?.trim())
|
|
1213
|
+
throw new Error('--name is required.');
|
|
1214
|
+
if (!opts.url?.trim())
|
|
1215
|
+
throw new Error('--url is required.');
|
|
1216
|
+
const headers = opts.headers ? (0, setJson_1.readSetJson)(opts.headers) : {};
|
|
1217
|
+
const current = await loadKpiConfig(agentId);
|
|
1218
|
+
const item = { name: opts.name.trim(), url: opts.url.trim(), method: 'POST', headers, parameters: withPhoneParam(opts.params) };
|
|
1219
|
+
await saveKpiConfig(agentId, current, { api_endpoints: upsertByName(current.api_endpoints, item) }, opts.json, `API data source ${item.name} saved.`);
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
(0, ui_1.handleError)(err);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
/** `cku kpi source add-python <agentId>` */
|
|
1226
|
+
async function kpiSourceAddPythonCommand(agentId, opts) {
|
|
1227
|
+
try {
|
|
1228
|
+
if (!opts.name?.trim())
|
|
1229
|
+
throw new Error('--name is required.');
|
|
1230
|
+
if (!opts.code)
|
|
1231
|
+
throw new Error('--code is required (inline, or @file.py).');
|
|
1232
|
+
const code = opts.code.startsWith('@') ? (0, fs_1.readFileSync)(opts.code.slice(1), 'utf8') : opts.code;
|
|
1233
|
+
if (!opts.skipVerify)
|
|
1234
|
+
await (0, dashboardApi_1.dashboardApi)().verifyKpiPythonCode(code);
|
|
1235
|
+
const current = await loadKpiConfig(agentId);
|
|
1236
|
+
const item = { name: opts.name.trim(), source_code: code, parameters: withPhoneParam(opts.params) };
|
|
1237
|
+
await saveKpiConfig(agentId, current, { python_data_sources: upsertByName(current.python_data_sources, item) }, opts.json, `Python data source ${item.name} saved.`);
|
|
1238
|
+
}
|
|
1239
|
+
catch (err) {
|
|
1240
|
+
(0, ui_1.handleError)(err);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/** `cku kpi source add-expr <agentId>` */
|
|
1244
|
+
async function kpiSourceAddExprCommand(agentId, opts) {
|
|
1245
|
+
try {
|
|
1246
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(opts.name || '')) {
|
|
1247
|
+
throw new Error('--name must start with a letter, then letters, digits, _ or -, max 64 chars.');
|
|
1248
|
+
}
|
|
1249
|
+
const item = {
|
|
1250
|
+
name: opts.name,
|
|
1251
|
+
mode: opts.conditions ? 'conditions' : 'expression',
|
|
1252
|
+
conditions: opts.conditions ? (0, setJson_1.readSetJson)(opts.conditions) : [],
|
|
1253
|
+
expression: opts.expression || '',
|
|
1254
|
+
equals: true,
|
|
1255
|
+
consider_for: opts.considerFor === 'unique' ? 'unique' : 'all',
|
|
1256
|
+
};
|
|
1257
|
+
const current = await loadKpiConfig(agentId);
|
|
1258
|
+
await saveKpiConfig(agentId, current, { expression_data_sources: upsertByName(current.expression_data_sources, item) }, opts.json, `Expression data source ${item.name} saved.`);
|
|
1259
|
+
}
|
|
1260
|
+
catch (err) {
|
|
1261
|
+
(0, ui_1.handleError)(err);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/** `cku kpi source remove <agentId> <name>` */
|
|
1265
|
+
async function kpiSourceRemoveCommand(agentId, name, opts) {
|
|
1266
|
+
try {
|
|
1267
|
+
const current = await loadKpiConfig(agentId);
|
|
1268
|
+
const drop = (l) => (l || []).filter((x) => x?.name !== name);
|
|
1269
|
+
await saveKpiConfig(agentId, current, {
|
|
1270
|
+
api_endpoints: drop(current.api_endpoints),
|
|
1271
|
+
python_data_sources: drop(current.python_data_sources),
|
|
1272
|
+
expression_data_sources: drop(current.expression_data_sources),
|
|
1273
|
+
}, opts.json, `Data source ${name} removed.`);
|
|
1274
|
+
}
|
|
1275
|
+
catch (err) {
|
|
1276
|
+
(0, ui_1.handleError)(err);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
/** `cku kpi metric set <agentId>` */
|
|
1280
|
+
async function kpiMetricSetCommand(agentId, opts) {
|
|
1281
|
+
try {
|
|
1282
|
+
if (!opts.name?.trim() || !opts.source?.trim() || opts.value === undefined || String(opts.value).trim() === '') {
|
|
1283
|
+
throw new Error('--name, --source and --value are all required.');
|
|
1284
|
+
}
|
|
1285
|
+
const current = await loadKpiConfig(agentId);
|
|
1286
|
+
const item = { name: opts.name.trim(), source: opts.source.trim(), value: String(opts.value) };
|
|
1287
|
+
await saveKpiConfig(agentId, current, { kpi_metrics: upsertByName(current.kpi_metrics, item) }, opts.json, `Metric ${item.name} saved.`);
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
(0, ui_1.handleError)(err);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
/** `cku kpi metric remove <agentId> <name>` */
|
|
1294
|
+
async function kpiMetricRemoveCommand(agentId, name, opts) {
|
|
1295
|
+
try {
|
|
1296
|
+
if (name === 'connected_to_conversion_percentage') {
|
|
1297
|
+
throw new Error('connected_to_conversion_percentage is locked by the server and cannot be removed. Change it with: cku kpi target');
|
|
1298
|
+
}
|
|
1299
|
+
const current = await loadKpiConfig(agentId);
|
|
1300
|
+
await saveKpiConfig(agentId, current, { kpi_metrics: (current.kpi_metrics || []).filter((m) => m?.name !== name) }, opts.json, `Metric ${name} removed.`);
|
|
1301
|
+
}
|
|
1302
|
+
catch (err) {
|
|
1303
|
+
(0, ui_1.handleError)(err);
|
|
1304
|
+
}
|
|
1305
|
+
}
|