callkaro-cli-unofficial 0.7.1 → 0.7.2

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.
@@ -12,7 +12,20 @@ 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
+ const fs_1 = require("fs");
15
26
  const dashboardApi_1 = require("../lib/dashboardApi");
27
+ const setJson_1 = require("./setJson");
28
+ const kpiReport_1 = require("../lib/kpiReport");
16
29
  const ui_1 = require("../lib/ui");
17
30
  /** Table cells are one line each; these keep a long prompt from wrapping a row. */
18
31
  function clip(value, max = 60) {
@@ -606,17 +619,25 @@ async function kpiTargetCommand(opts) {
606
619
  throw err;
607
620
  }
608
621
  const metricName = 'connected_to_conversion_percentage';
609
- const body = {
610
- kpi_metrics: [{ name: metricName, source: metricName, value: String(opts.value).trim() }],
611
- };
612
- // Only send the RCA fields when the user names them, or when the account
613
- // already has them and would otherwise be wiped.
614
- const rcaModel = opts.rcaModel ?? current.rca_model;
615
- const rcaPrompt = opts.rcaPrompt ?? current.rca_prompt;
616
- if (rcaModel)
617
- body.rca_model = String(rcaModel);
618
- if (rcaPrompt !== undefined)
619
- body.rca_prompt = String(rcaPrompt);
622
+ const target = { name: metricName, source: metricName, value: String(opts.value).trim() };
623
+ let body;
624
+ if (Object.keys(current).length) {
625
+ // Existing config: send all 10 fields so data sources, report and extra
626
+ // metrics survive the wholesale PUT.
627
+ const rest = (Array.isArray(current.kpi_metrics) ? current.kpi_metrics : []).filter((m) => m?.name !== metricName && m?.name !== 'connected_to_conversion');
628
+ body = (0, kpiReport_1.buildFullKpiPayload)(current, {
629
+ kpi_metrics: [target, ...rest],
630
+ ...(opts.rcaModel !== undefined ? { rca_model: String(opts.rcaModel), rca_llms: (0, kpiReport_1.rcaLlmsFor)(String(opts.rcaModel)) } : {}),
631
+ ...(opts.rcaPrompt !== undefined ? { rca_prompt: String(opts.rcaPrompt) } : {}),
632
+ });
633
+ }
634
+ else {
635
+ body = { kpi_metrics: [target] };
636
+ if (opts.rcaModel)
637
+ body.rca_model = String(opts.rcaModel);
638
+ if (opts.rcaPrompt !== undefined)
639
+ body.rca_prompt = String(opts.rcaPrompt);
640
+ }
620
641
  const res = await api.setKpiAgentConfig(agentId, body);
621
642
  if (opts.json)
622
643
  return void console.log(JSON.stringify(res ?? body, null, 2));
@@ -684,3 +705,340 @@ function splitList(raw) {
684
705
  .map((s) => s.trim())
685
706
  .filter(Boolean);
686
707
  }
708
+ // --- KPI config & report ------------------------------------------------------
709
+ /** Read the agent's config; 404 is "none yet", anything else must stop the write. */
710
+ async function loadKpiConfig(agentId) {
711
+ try {
712
+ return (await (0, dashboardApi_1.dashboardApi)().kpiAgentConfig(agentId)) || {};
713
+ }
714
+ catch (err) {
715
+ if (!(0, dashboardApi_1.isNotFound)(err))
716
+ throw err;
717
+ return {};
718
+ }
719
+ }
720
+ /** PUT the full 10-field payload and report. */
721
+ async function saveKpiConfig(agentId, current, patch, json, msg) {
722
+ const payload = (0, kpiReport_1.buildFullKpiPayload)(current, patch);
723
+ const res = await (0, dashboardApi_1.dashboardApi)().setKpiAgentConfig(agentId, payload);
724
+ if (json)
725
+ return void console.log(JSON.stringify(res ?? payload, null, 2));
726
+ (0, ui_1.success)(msg);
727
+ }
728
+ /** `cku kpi config <agentId>` — show or edit the whole KPI Manager document. */
729
+ async function kpiConfigCommand(agentId, opts) {
730
+ try {
731
+ const current = await loadKpiConfig(agentId);
732
+ const mutate = opts.set || opts.enableRca || opts.disableRca || opts.enableReport || opts.disableReport ||
733
+ opts.rcaModel !== undefined || opts.rcaPrompt !== undefined;
734
+ if (!mutate) {
735
+ if (opts.json)
736
+ return void console.log(JSON.stringify(current, null, 2));
737
+ (0, ui_1.keyValue)({
738
+ agentId,
739
+ rca_model: current.rca_model ?? '',
740
+ nightly_rca_enabled: String(current.nightly_rca_enabled !== false),
741
+ report_enabled: String(current.report_enabled === true),
742
+ metrics: (current.kpi_metrics || []).map((m) => `${m.name}=${m.value}`).join(', ') || '(none)',
743
+ api_endpoints: (current.api_endpoints || []).length,
744
+ python_data_sources: (current.python_data_sources || []).length,
745
+ expression_data_sources: (current.expression_data_sources || []).length,
746
+ report_elements: (current.report_config?.elements || []).length,
747
+ rca_prompt: clip(current.rca_prompt, 80),
748
+ });
749
+ return;
750
+ }
751
+ const setPatch = opts.set ? (0, setJson_1.readSetJson)(opts.set) : {};
752
+ await saveKpiConfig(agentId, current, {
753
+ ...setPatch,
754
+ ...(opts.rcaModel !== undefined ? { rca_model: opts.rcaModel } : {}),
755
+ ...(opts.rcaPrompt !== undefined ? { rca_prompt: opts.rcaPrompt } : {}),
756
+ ...(opts.enableRca ? { nightly_rca_enabled: true } : opts.disableRca ? { nightly_rca_enabled: false } : {}),
757
+ ...(opts.enableReport ? { report_enabled: true } : opts.disableReport ? { report_enabled: false } : {}),
758
+ }, opts.json, `KPI config for ${agentId} saved.`);
759
+ }
760
+ catch (err) {
761
+ (0, ui_1.handleError)(err);
762
+ }
763
+ }
764
+ /** `cku kpi report schema` */
765
+ async function kpiReportSchemaCommand() {
766
+ console.log(kpiReport_1.REPORT_SCHEMA_REFERENCE);
767
+ }
768
+ /** `cku kpi report get <agentId>` */
769
+ async function kpiReportGetCommand(agentId, opts) {
770
+ try {
771
+ const current = await loadKpiConfig(agentId);
772
+ const cfg = (0, kpiReport_1.normalizeReportConfig)(current.report_config);
773
+ const enabled = Boolean(current.report_enabled);
774
+ if (opts.json) {
775
+ return void console.log(JSON.stringify({ agentId, report_enabled: enabled, report_config: cfg }, null, 2));
776
+ }
777
+ (0, ui_1.keyValue)({
778
+ agentId,
779
+ report_enabled: enabled,
780
+ recipients: cfg.recipients.join(', ') || '(none)',
781
+ schedule: `${cfg.min_trigger_time} – ${cfg.max_trigger_time} IST, every ${cfg.interval_hours}h`,
782
+ custom_variables: cfg.custom_variables.length,
783
+ elements: cfg.elements.length,
784
+ });
785
+ if (cfg.custom_variables.length) {
786
+ (0, ui_1.table)(['NAME', 'MODE', 'CONSIDER FOR', 'RULE'], cfg.custom_variables.map((v) => [
787
+ v.name || '', v.mode || '', (v.consider_for || []).join(','), clip(v.mode === 'conditions' ? JSON.stringify(v.conditions ?? []) : (v.expression || ''), 50),
788
+ ]));
789
+ }
790
+ if (cfg.elements.length) {
791
+ (0, ui_1.table)(['ID', 'TYPE', 'POS (x,y,w,h)', 'SUMMARY'], cfg.elements.map((e) => [
792
+ e.id || '',
793
+ e.type === 'chart' ? `chart/${e.chart_type}` : e.type || '',
794
+ `${e.x ?? ''},${e.y ?? ''},${e.w ?? ''},${e.h ?? ''}`,
795
+ clip(e.type === 'chart' ? `${e.title || ''} ${(e.y_axis || []).join(' ')}` : e.type === 'table' ? `${(e.cells || []).length} rows` : e.content, 50),
796
+ ]));
797
+ }
798
+ }
799
+ catch (err) {
800
+ (0, ui_1.handleError)(err);
801
+ }
802
+ }
803
+ const HHMM = /^\d{2}:\d{2}$/;
804
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
805
+ /** `cku kpi report set <agentId>` */
806
+ async function kpiReportSetCommand(agentId, opts) {
807
+ try {
808
+ const current = await loadKpiConfig(agentId);
809
+ let next = (0, kpiReport_1.normalizeReportConfig)(current.report_config);
810
+ let enabled = Boolean(current.report_enabled);
811
+ if (opts.set) {
812
+ let patch = (0, setJson_1.readSetJson)(opts.set);
813
+ if (patch.report_config && typeof patch.report_config === 'object') {
814
+ if (patch.report_enabled !== undefined)
815
+ enabled = Boolean(patch.report_enabled);
816
+ patch = patch.report_config;
817
+ }
818
+ next = (0, kpiReport_1.normalizeReportConfig)({ ...next, ...patch });
819
+ }
820
+ if (opts.recipients !== undefined) {
821
+ const list = opts.recipients.split(/[,;\s]+/).map((r) => r.trim().toLowerCase()).filter(Boolean);
822
+ const bad = list.filter((r) => !EMAIL.test(r));
823
+ if (bad.length)
824
+ throw new Error(`Invalid recipient email(s): ${bad.join(', ')}`);
825
+ if (list.length > 10)
826
+ throw new Error(`At most 10 recipients (got ${list.length}).`);
827
+ next.recipients = list;
828
+ }
829
+ if (opts.interval !== undefined) {
830
+ const n = Number(opts.interval);
831
+ if (!kpiReport_1.INTERVAL_HOURS.includes(n))
832
+ throw new Error(`--interval must be one of: ${kpiReport_1.INTERVAL_HOURS.join(', ')} (got "${opts.interval}").`);
833
+ next.interval_hours = n;
834
+ }
835
+ for (const [flag, key, v] of [['--min-trigger', 'min_trigger_time', opts.minTrigger], ['--max-trigger', 'max_trigger_time', opts.maxTrigger]]) {
836
+ if (v === undefined)
837
+ continue;
838
+ if (!HHMM.test(v))
839
+ throw new Error(`${flag} must be HH:mm (got "${v}").`);
840
+ next[key] = v;
841
+ }
842
+ if (Number(next.interval_hours) >= 24)
843
+ next.max_trigger_time = next.min_trigger_time;
844
+ if (opts.enable)
845
+ enabled = true;
846
+ else if (opts.disable)
847
+ enabled = false;
848
+ await saveKpiConfig(agentId, current, { report_enabled: enabled, report_config: next }, opts.json, `KPI report for ${agentId} saved.`);
849
+ }
850
+ catch (err) {
851
+ (0, ui_1.handleError)(err);
852
+ }
853
+ }
854
+ /** Saved report config with `--set` / `--interval` overlaid, for preview and export. */
855
+ function overlayReport(current, opts) {
856
+ const cfg = (0, kpiReport_1.normalizeReportConfig)({
857
+ ...(0, kpiReport_1.normalizeReportConfig)(current.report_config),
858
+ ...(opts.set ? (0, setJson_1.readSetJson)(opts.set) : {}),
859
+ ...(opts.interval !== undefined ? { interval_hours: Number(opts.interval) } : {}),
860
+ });
861
+ return cfg;
862
+ }
863
+ /** `cku kpi report preview <agentId>` */
864
+ async function kpiReportPreviewCommand(agentId, opts) {
865
+ try {
866
+ const cfg = overlayReport(await loadKpiConfig(agentId), opts);
867
+ const res = await (0, dashboardApi_1.dashboardApi)().previewKpiReport(agentId, cfg);
868
+ if (opts.json)
869
+ return void console.log(JSON.stringify(res, null, 2));
870
+ const values = res?.values || {};
871
+ const labels = res?.labels || {};
872
+ (0, ui_1.info)(`Window: ${res?.window?.label ?? '?'} Generated: ${res?.generated_at ?? '?'}`);
873
+ const rows = Object.entries(values)
874
+ .filter(([ref]) => !ref.startsWith('chart:') && !ref.endsWith('@trend'))
875
+ .map(([ref, v]) => [ref, labels[ref] || v?.label || '', (0, kpiReport_1.formatPreviewValue)(v)]);
876
+ if (rows.length)
877
+ (0, ui_1.table)(['REF', 'LABEL', 'VALUE'], rows);
878
+ for (const el of cfg.elements) {
879
+ const series = values[`chart:${el.id}`] ?? res?.charts?.[el.id] ?? res?.series?.[el.id];
880
+ if (el.type === 'chart' && series)
881
+ (0, ui_1.info)(`\nchart:${el.id} (${el.title || el.chart_type})\n${JSON.stringify(series)}`);
882
+ else if (el.type === 'text')
883
+ (0, ui_1.info)(`\n${(0, kpiReport_1.interpolateReportText)(el.content, values)}`);
884
+ else if (el.type === 'table') {
885
+ const cells = (el.cells || []).map((r) => r.map((c) => (0, kpiReport_1.interpolateReportText)(c, values)));
886
+ if (cells.length)
887
+ (0, ui_1.table)(cells[0], cells.slice(1));
888
+ }
889
+ }
890
+ }
891
+ catch (err) {
892
+ (0, ui_1.handleError)(err);
893
+ }
894
+ }
895
+ /** `cku kpi report export <agentId>` — render to PDF. */
896
+ async function kpiReportExportCommand(agentId, opts) {
897
+ try {
898
+ const cfg = overlayReport(await loadKpiConfig(agentId), opts);
899
+ const { bytes, filename } = await (0, dashboardApi_1.dashboardApi)().exportKpiReport(agentId, cfg);
900
+ const file = opts.file || filename || `kpi_report_${agentId}.pdf`;
901
+ (0, fs_1.writeFileSync)(file, bytes);
902
+ (0, ui_1.success)(`Wrote ${file} (${bytes.length} bytes).`);
903
+ }
904
+ catch (err) {
905
+ (0, ui_1.handleError)(err);
906
+ }
907
+ }
908
+ /** Load config + its report, apply `edit`, save. Shared by the var/element commands. */
909
+ async function editReport(agentId, json, msg, edit) {
910
+ const current = await loadKpiConfig(agentId);
911
+ const cfg = (0, kpiReport_1.normalizeReportConfig)(current.report_config);
912
+ edit(cfg);
913
+ await saveKpiConfig(agentId, current, { report_config: cfg }, json, msg);
914
+ }
915
+ /** `cku kpi report add-var <agentId>` */
916
+ async function kpiReportAddVarCommand(agentId, opts) {
917
+ try {
918
+ if (!/^[a-z][a-z0-9_]{0,63}$/.test(opts.name || '')) {
919
+ throw new Error('--name must be lowercase letters, digits, underscores, start with a letter, max 64 chars.');
920
+ }
921
+ const mode = opts.conditions ? 'conditions' : 'expression';
922
+ if (mode === 'expression' && !opts.expression?.trim()) {
923
+ throw new Error('--expression <rule> is required (or --conditions <json>). See: cku kpi report schema');
924
+ }
925
+ const consider = (opts.considerFor || 'all').split(',').map((s) => s.trim()).filter(Boolean);
926
+ const badConsider = consider.filter((c) => c !== 'all' && c !== 'unique');
927
+ if (badConsider.length)
928
+ throw new Error(`--consider-for takes all and/or unique (got ${badConsider.join(', ')}).`);
929
+ const variable = { name: opts.name, mode, conditions: [], expression: '', equals: true, consider_for: consider };
930
+ if (mode === 'conditions')
931
+ variable.conditions = (0, setJson_1.readSetJson)(opts.conditions);
932
+ else
933
+ variable.expression = opts.expression.trim();
934
+ await editReport(agentId, opts.json, `Custom variable ${opts.name} saved.`, (cfg) => {
935
+ const i = cfg.custom_variables.findIndex((v) => v.name === opts.name);
936
+ if (i >= 0)
937
+ cfg.custom_variables[i] = variable;
938
+ else
939
+ cfg.custom_variables.push(variable);
940
+ });
941
+ }
942
+ catch (err) {
943
+ (0, ui_1.handleError)(err);
944
+ }
945
+ }
946
+ /** `cku kpi report remove-var <agentId> <name>` */
947
+ async function kpiReportRemoveVarCommand(agentId, name, opts) {
948
+ try {
949
+ await editReport(agentId, opts.json, `Custom variable ${name} removed.`, (cfg) => {
950
+ cfg.custom_variables = cfg.custom_variables.filter((v) => v.name !== name);
951
+ const strip = (list) => (list || []).filter((r) => parseCustom(r) !== name);
952
+ cfg.elements = cfg.elements.map((el) => el.type === 'chart' ? { ...el, refs: strip(el.refs), y_axis: strip(el.y_axis) } : el);
953
+ });
954
+ }
955
+ catch (err) {
956
+ (0, ui_1.handleError)(err);
957
+ }
958
+ }
959
+ /** `custom:foo#unique` -> `foo`; anything else -> undefined. */
960
+ function parseCustom(ref) {
961
+ const m = /^custom:([^#]+)/.exec(String(ref));
962
+ return m ? m[1] : undefined;
963
+ }
964
+ /** `cku kpi report add-element <agentId>` */
965
+ async function kpiReportAddElementCommand(agentId, opts) {
966
+ try {
967
+ const shorthand = kpiReport_1.CHART_TYPES.includes(opts.type) && opts.type !== 'table';
968
+ const kind = shorthand ? 'chart' : opts.type;
969
+ if (!['text', 'table', 'chart'].includes(kind)) {
970
+ throw new Error(`--type must be text | table | chart (or a chart type: tiles | bar | line | pie | funnel). Got "${opts.type}".`);
971
+ }
972
+ const chartType = (shorthand ? opts.type : opts.chartType || 'tiles');
973
+ if (kind === 'chart' && !kpiReport_1.CHART_TYPES.includes(chartType)) {
974
+ throw new Error(`--chart-type must be one of: ${kpiReport_1.CHART_TYPES.join(', ')}.`);
975
+ }
976
+ const num = (v, flag) => numeric(v, flag);
977
+ await editReport(agentId, opts.json, `Added ${kind} element.`, (cfg) => {
978
+ const w = num(opts.w, '--w') ?? (kind === 'chart' ? kpiReport_1.PAGE_W - kpiReport_1.MARGIN * 2 : kpiReport_1.DEFAULT_WIDTHS[kind]);
979
+ if (w < kpiReport_1.MIN_W)
980
+ throw new Error(`--w must be at least ${kpiReport_1.MIN_W}.`);
981
+ const el = {
982
+ id: (0, kpiReport_1.newElementId)(),
983
+ type: kind,
984
+ x: num(opts.x, '--x') ?? kpiReport_1.MARGIN,
985
+ y: num(opts.y, '--y') ?? (0, kpiReport_1.nextElementY)(cfg.elements),
986
+ w,
987
+ };
988
+ if (kind === 'text') {
989
+ el.content = opts.content ?? '';
990
+ el.style = { bold: false, italic: false, underline: false, size: 14, color: '#111827', align: 'left' };
991
+ }
992
+ else if (kind === 'table') {
993
+ el.header = true;
994
+ if (opts.cells) {
995
+ const cells = (0, setJson_1.readSetJson)(opts.cells);
996
+ if (!Array.isArray(cells) || !cells.length || !cells.every((r) => Array.isArray(r))) {
997
+ throw new Error('--cells must be a JSON array of rows, e.g. [["Metric","Value"],["Calls","${total_calls}"]].');
998
+ }
999
+ el.cells = cells;
1000
+ }
1001
+ else {
1002
+ const r = num(opts.rows, '--rows') ?? 2;
1003
+ const c = num(opts.cols, '--cols') ?? 2;
1004
+ if (r < 1 || r > 8 || c < 1 || c > 8)
1005
+ throw new Error('--rows and --cols must be 1..8.');
1006
+ el.cells = Array.from({ length: r }, () => Array.from({ length: c }, () => ''));
1007
+ }
1008
+ }
1009
+ else {
1010
+ const refs = (opts.refs || '').split(',').map(kpiReport_1.canonicalRef).filter(Boolean);
1011
+ const axes = (0, kpiReport_1.defaultChartAxes)(chartType, refs);
1012
+ el.chart_type = chartType;
1013
+ el.title = opts.title ?? '';
1014
+ el.x_axis = opts.xAxis !== undefined ? opts.xAxis : axes.x_axis;
1015
+ el.y_axis = axes.y_axis;
1016
+ el.refs = axes.y_axis;
1017
+ el.stack_by = opts.stackBy !== undefined ? opts.stackBy : axes.stack_by;
1018
+ el.options = {};
1019
+ el.h = kpiReport_1.CHART_HEIGHTS[chartType] || 280;
1020
+ }
1021
+ if (opts.h !== undefined)
1022
+ el.h = num(opts.h, '--h');
1023
+ cfg.elements.push(el);
1024
+ });
1025
+ }
1026
+ catch (err) {
1027
+ (0, ui_1.handleError)(err);
1028
+ }
1029
+ }
1030
+ /** `cku kpi report remove-element <agentId> <elementId>` */
1031
+ async function kpiReportRemoveElementCommand(agentId, elementId, opts) {
1032
+ try {
1033
+ await editReport(agentId, opts.json, `Element ${elementId} removed.`, (cfg) => {
1034
+ const n = cfg.elements.length;
1035
+ cfg.elements = cfg.elements.filter((e) => e.id !== elementId);
1036
+ if (cfg.elements.length === n) {
1037
+ throw new Error(`No element with id "${elementId}". List ids with: cku kpi report get ${agentId}`);
1038
+ }
1039
+ });
1040
+ }
1041
+ catch (err) {
1042
+ (0, ui_1.handleError)(err);
1043
+ }
1044
+ }
package/dist/index.js CHANGED
@@ -361,6 +361,89 @@ kpi
361
361
  .addHelpText('after', '\n The server replaces the whole config at once, so an RCA prompt already set on\n' +
362
362
  ' the account is read back and re-sent rather than cleared.\n')
363
363
  .action(kpi_1.kpiTargetCommand);
364
+ kpi
365
+ .command('config <agentId>')
366
+ .description("show or edit an agent's full KPI Manager config (all fields preserved on write)")
367
+ .option('--set <json>', 'JSON patch of top-level fields, or @file.json')
368
+ .option('--enable-rca', 'turn nightly RCA on')
369
+ .option('--disable-rca', 'turn nightly RCA off')
370
+ .option('--enable-report', 'turn the auto report on')
371
+ .option('--disable-report', 'turn the auto report off')
372
+ .option('--rca-model <id>', 'RCA model')
373
+ .option('--rca-prompt <text>', 'RCA prompt')
374
+ .option('--json', 'output raw JSON')
375
+ .action(kpi_1.kpiConfigCommand);
376
+ const kpiReport = kpi.command('report').description('KPI Report builder, live preview, and PDF export');
377
+ kpiReport.command('schema').description('print the report schema and variable reference').action(kpi_1.kpiReportSchemaCommand);
378
+ kpiReport
379
+ .command('get <agentId>')
380
+ .description("show an agent's report config")
381
+ .option('--json', 'output raw JSON')
382
+ .action(kpi_1.kpiReportGetCommand);
383
+ kpiReport
384
+ .command('set <agentId>')
385
+ .description('edit schedule, recipients, enable flag, or merge a JSON patch')
386
+ .option('--set <json>', 'JSON merged into report_config (or @file.json)')
387
+ .option('--enable', 'enable the auto report')
388
+ .option('--disable', 'disable the auto report')
389
+ .option('--recipients <emails>', 'comma-separated emails (max 10); "" clears')
390
+ .option('--min-trigger <HH:mm>', 'earliest send time, IST')
391
+ .option('--max-trigger <HH:mm>', 'latest send time, IST (forced equal to min when interval >= 24)')
392
+ .option('--interval <hours>', 'one of 1,2,3,4,6,8,12,24,48,72,168')
393
+ .option('--json', 'output raw JSON')
394
+ .action(kpi_1.kpiReportSetCommand);
395
+ kpiReport
396
+ .command('preview <agentId>')
397
+ .description('live-evaluate the report (nothing is sent)')
398
+ .option('--set <json>', 'overlay JSON onto report_config')
399
+ .option('--interval <hours>', 'override interval_hours')
400
+ .option('--json', 'output raw JSON')
401
+ .action(kpi_1.kpiReportPreviewCommand);
402
+ kpiReport
403
+ .command('export <agentId>')
404
+ .description('render the report to a PDF file')
405
+ .option('--file <path>', 'output path')
406
+ .option('--set <json>', 'overlay JSON onto report_config')
407
+ .option('--interval <hours>', 'override interval_hours')
408
+ .action(kpi_1.kpiReportExportCommand);
409
+ kpiReport
410
+ .command('add-var <agentId>')
411
+ .description('add or replace a custom variable')
412
+ .requiredOption('--name <name>', 'lowercase identifier')
413
+ .option('--expression <rule>', 'rule, e.g. "is_connected && post_call.x == \'YES\'"')
414
+ .option('--conditions <json>', 'conditions JSON (or @file.json); makes mode "conditions"')
415
+ .option('--consider-for <list>', 'all and/or unique, comma-separated (default all)')
416
+ .option('--json', 'output raw JSON')
417
+ .action(kpi_1.kpiReportAddVarCommand);
418
+ kpiReport
419
+ .command('remove-var <agentId> <name>')
420
+ .description('remove a custom variable and its chart refs')
421
+ .option('--json', 'output raw JSON')
422
+ .action(kpi_1.kpiReportRemoveVarCommand);
423
+ kpiReport
424
+ .command('add-element <agentId>')
425
+ .description('add a text, table or chart element to the canvas')
426
+ .requiredOption('--type <type>', 'text | table | chart (or tiles | bar | line | pie | funnel)')
427
+ .option('--chart-type <type>', 'tiles | bar | line | pie | funnel | table (default tiles)')
428
+ .option('--title <text>', 'chart title')
429
+ .option('--refs <refs>', 'comma-separated variable refs, e.g. total_calls,custom:foo')
430
+ .option('--x-axis <dim>', 'override x axis')
431
+ .option('--stack-by <dim>', 'override stack/split dimension')
432
+ .option('--content <text>', 'text content; ${total_calls} interpolates')
433
+ .option('--cells <json>', 'table cells as JSON rows (or @file.json)')
434
+ .option('--rows <n>', 'empty table rows (default 2)')
435
+ .option('--cols <n>', 'empty table cols (default 2)')
436
+ .option('--x <px>', 'left (default 40)')
437
+ .option('--y <px>', 'top (default: below existing elements)')
438
+ .option('--w <px>', 'width')
439
+ .option('--h <px>', 'height')
440
+ .option('--json', 'output raw JSON')
441
+ .action(kpi_1.kpiReportAddElementCommand);
442
+ kpiReport
443
+ .command('remove-element <agentId> <elementId>')
444
+ .description('remove a canvas element by id')
445
+ .option('--json', 'output raw JSON')
446
+ .action(kpi_1.kpiReportRemoveElementCommand);
364
447
  kpi
365
448
  .command('rca')
366
449
  .description('start an RCA run for one agent over a date range')
@@ -107,6 +107,56 @@ class DashboardApi {
107
107
  }
108
108
  return data;
109
109
  }
110
+ /**
111
+ * Like `request()`, but returns the raw bytes and suggested filename when the
112
+ * server answers with a binary download (PDF exports). Non-2xx still parses JSON
113
+ * and throws `DashboardError`.
114
+ */
115
+ async requestBinary(method, path, body) {
116
+ if (!path.startsWith('/'))
117
+ throw new Error(`Dashboard path must start with "/" (got "${path}").`);
118
+ const { token, sessionToken } = await (0, dashboardAuth_1.requireDashboard)();
119
+ const headers = {};
120
+ if (body !== undefined)
121
+ headers['Content-Type'] = 'application/json';
122
+ if (usesCookieAuth(path)) {
123
+ headers.Cookie = `access_token=${sessionToken}`;
124
+ }
125
+ else {
126
+ headers.Authorization = `Bearer ${token}`;
127
+ }
128
+ let res;
129
+ try {
130
+ res = await fetch(`${this.baseUrl}${path}`, {
131
+ method,
132
+ headers,
133
+ body: body === undefined ? undefined : JSON.stringify(body),
134
+ redirect: 'error',
135
+ signal: AbortSignal.timeout(60_000),
136
+ });
137
+ }
138
+ catch (err) {
139
+ throw new Error(`Cannot reach ${this.baseUrl} (${err.message})`);
140
+ }
141
+ if (!res.ok) {
142
+ const text = await res.text();
143
+ let data;
144
+ try {
145
+ data = text ? JSON.parse(text) : undefined;
146
+ }
147
+ catch {
148
+ data = { msg: text.slice(0, 200) };
149
+ }
150
+ throw new DashboardError(res.status, data?.msg || data?.message || `Request failed (${res.status})`, data);
151
+ }
152
+ const disp = typeof res.headers?.get === 'function' ? res.headers.get('content-disposition') || '' : '';
153
+ const m = /filename="?([^";]+)"?/i.exec(disp);
154
+ const contentType = typeof res.headers?.get === 'function' ? res.headers.get('content-type') || '' : '';
155
+ const ab = typeof res.arrayBuffer === 'function'
156
+ ? await res.arrayBuffer()
157
+ : Buffer.from(await res.text());
158
+ return { bytes: Buffer.from(ab), filename: m?.[1], contentType };
159
+ }
110
160
  /**
111
161
  * `GET /v2/agents/` — the one route that wants the body token as a bearer.
112
162
  * The only place `agentFolders` is exposed, which `/cli/agents` does not return.
@@ -225,6 +275,47 @@ class DashboardApi {
225
275
  runKpiRca(agentId, filters) {
226
276
  return this.request('POST', `/kpi-manager/${encodeURIComponent(agentId)}/rca/run-now`, { filters });
227
277
  }
278
+ /** Live-evaluate a KPI Report configuration (values, labels, chart series, source_fields). */
279
+ previewKpiReport(agentId, reportConfig) {
280
+ return this.request('POST', `/kpi-manager/${encodeURIComponent(agentId)}/report/preview`, {
281
+ report_config: { ...reportConfig, recipients: [] },
282
+ });
283
+ }
284
+ /** Render a KPI Report configuration to a PDF binary. */
285
+ exportKpiReport(agentId, reportConfig) {
286
+ return this.requestBinary('POST', `/kpi-manager/${encodeURIComponent(agentId)}/report/export`, {
287
+ report_config: reportConfig,
288
+ });
289
+ }
290
+ /** Draft or refine an RCA prompt with AI (`POST /openai-tools/generate-rca-prompt-draft`). */
291
+ generateRcaPromptDraft(body) {
292
+ return this.request('POST', '/openai-tools/generate-rca-prompt-draft', body);
293
+ }
294
+ /** Verify Python data source code before saving (`POST /utils/verify-code`). */
295
+ verifyKpiPythonCode(sourceCode, name = 'generate_kpi_data') {
296
+ return this.request('POST', '/utils/verify-code', {
297
+ name,
298
+ source_code: sourceCode,
299
+ type: 'kpi_data_source',
300
+ });
301
+ }
302
+ /** Bulk-import pending task cards (`POST /pending-tasks/import`). */
303
+ importPendingTasks(rows) {
304
+ return this.request('POST', '/pending-tasks/import', { rows });
305
+ }
306
+ /** Daywise RCA reports (`GET /v1/audit-reports/daywise`). */
307
+ daywiseRcaReports(query = {}) {
308
+ return this.request('GET', `/v1/audit-reports/daywise${qs(query)}`);
309
+ }
310
+ /** Day-by-day RCA execution timeline (`GET /v1/audit-reports/daywise/:agentId/timeline`). */
311
+ daywiseRcaTimeline(agentId, query = {}) {
312
+ return this.request('GET', `/v1/audit-reports/daywise/${encodeURIComponent(agentId)}/timeline${qs(query)}`);
313
+ }
314
+ /** Download a daywise RCA report PDF (`GET /v1/audit-reports/daywise/:agentId/:rcaDate/pdf`). */
315
+ daywiseRcaPdf(agentId, rcaDate, source) {
316
+ const q = source === 'manual' ? { source: 'manual' } : {};
317
+ return this.requestBinary('GET', `/v1/audit-reports/daywise/${encodeURIComponent(agentId)}/${encodeURIComponent(rcaDate)}/pdf${qs(q)}`);
318
+ }
228
319
  // --- Call analyzers -------------------------------------------------------
229
320
  listAnalyzers() { return this.request('GET', '/v1/call-analyzers'); }
230
321
  // --- Audit reports (dashboard side) ---------------------------------------
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ /**
3
+ * KPI Report canvas & config helpers, mirrored from the dashboard bundle (`XIr` / `sVe`).
4
+ *
5
+ * The backend replaces the whole `/kpi-manager/:agentId` document on `PUT`, so
6
+ * every edit here works on a normalized `KpiAgentConfig` and preserves all 10
7
+ * top-level fields (`api_endpoints`, `python_data_sources`, `expression_data_sources`,
8
+ * `kpi_metrics`, `rca_model`, `rca_llms`, `rca_prompt`, `nightly_rca_enabled`,
9
+ * `report_enabled`, `report_config`).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.REPORT_SCHEMA_REFERENCE = exports.newElementId = exports.LOCKED_METRIC = exports.CHART_TYPES = exports.BUILTIN_DIMENSIONS = exports.BUILTIN_VARIABLES = exports.INTERVAL_HOURS = exports.CHART_HEIGHTS = exports.DEFAULT_WIDTHS = exports.MIN_H = exports.MIN_W = exports.MARGIN = exports.PAGE_H = exports.PAGE_W = void 0;
13
+ exports.rcaLlmsFor = rcaLlmsFor;
14
+ exports.normalizeMetrics = normalizeMetrics;
15
+ exports.canonicalRef = canonicalRef;
16
+ exports.shortToken = shortToken;
17
+ exports.parseRef = parseRef;
18
+ exports.defaultChartAxes = defaultChartAxes;
19
+ exports.defaultReportConfig = defaultReportConfig;
20
+ exports.nextElementY = nextElementY;
21
+ exports.normalizeElement = normalizeElement;
22
+ exports.normalizeReportConfig = normalizeReportConfig;
23
+ exports.buildFullKpiPayload = buildFullKpiPayload;
24
+ exports.formatPreviewValue = formatPreviewValue;
25
+ exports.interpolateReportText = interpolateReportText;
26
+ exports.PAGE_W = 794;
27
+ exports.PAGE_H = 1123;
28
+ exports.MARGIN = 40;
29
+ exports.MIN_W = 80;
30
+ exports.MIN_H = 40;
31
+ exports.DEFAULT_WIDTHS = { chart: 520, table: 480, text: 360 };
32
+ exports.CHART_HEIGHTS = {
33
+ tiles: 130, bar: 320, line: 320, pie: 260, funnel: 300, table: 260,
34
+ };
35
+ exports.INTERVAL_HOURS = [1, 2, 3, 4, 6, 8, 12, 24, 48, 72, 168];
36
+ exports.BUILTIN_VARIABLES = [
37
+ { key: 'total_calls', label: 'Total triggered calls', valueKind: 'count' },
38
+ { key: 'connected_calls', label: 'Total connected calls', valueKind: 'count' },
39
+ { key: 'connected_pct', label: 'Connected %', valueKind: 'pct' },
40
+ { key: 'unique_receipts', label: 'Unique receipts', valueKind: 'count' },
41
+ { key: 'unique_connected', label: 'Unique connected receipts', valueKind: 'count' },
42
+ { key: 'unique_connected_pct', label: 'Receipt connectivity %', valueKind: 'pct' },
43
+ { key: 'converted_calls', label: 'Converted calls', valueKind: 'count' },
44
+ { key: 'converted_pct', label: 'Converted % (of connected)', valueKind: 'pct' },
45
+ { key: 'unique_converted', label: 'Unique converted receipts', valueKind: 'count' },
46
+ { key: 'avg_connected_duration', label: 'Avg connected duration (s)', valueKind: 'number' },
47
+ { key: 'drop_off_reasons', label: 'Drop-off reasons', valueKind: 'breakdown' },
48
+ { key: 'hangup_reasons', label: 'Hangup reasons', valueKind: 'breakdown' },
49
+ ];
50
+ exports.BUILTIN_DIMENSIONS = [
51
+ { value: 'dim:time_hour', label: 'Hour' },
52
+ { value: 'dim:time_day', label: 'Day' },
53
+ { value: 'dim:hangup_reason', label: 'Hangup reason' },
54
+ { value: 'dim:drop_off_reason', label: 'Drop-off reason' },
55
+ { value: 'dim:is_connected', label: 'Connected / not connected' },
56
+ { value: 'dim:converted', label: 'Converted / not converted' },
57
+ { value: 'dim:direction', label: 'Direction' },
58
+ ];
59
+ exports.CHART_TYPES = ['tiles', 'bar', 'line', 'pie', 'funnel', 'table'];
60
+ const CHART_META = {
61
+ bar: { x: 'X axis', y: 'Y axis', stack: 'Stack by', multiY: true, metricsX: true },
62
+ line: { x: 'X axis', y: 'Y axis', stack: 'Split by', multiY: true },
63
+ pie: { x: 'Slices', y: 'Value', multiY: false, metricsX: true },
64
+ funnel: { x: 'Split by', y: 'Steps', multiY: true, optionalX: true, direction: true },
65
+ tiles: { y: 'Metrics', multiY: true },
66
+ table: { x: 'Rows', y: 'Columns', multiY: true, metricsX: true },
67
+ };
68
+ const RCA_MODELS = [
69
+ { provider: 'callkaro', name: 'callkaro/krishna-2.5' },
70
+ { provider: 'azure-openai', name: 'gpt-5.6-luna' },
71
+ { provider: 'bedrock', name: 'arn:aws:bedrock:ap-south-2:525856616822:inference-profile/global.anthropic.claude-sonnet-4-6' },
72
+ { provider: 'bedrock', name: 'arn:aws:bedrock:ap-south-2:525856616822:inference-profile/global.anthropic.claude-opus-4-6-v1' },
73
+ { provider: 'azure-openai', name: 'gpt-5.6-sol' },
74
+ ];
75
+ const DEFAULT_RCA_MODEL = { provider: 'azure-openai', name: 'gpt-5.6-luna' };
76
+ exports.LOCKED_METRIC = 'connected_to_conversion_percentage';
77
+ function rcaLlmsFor(modelName) {
78
+ const name = (modelName || '').trim() || DEFAULT_RCA_MODEL.name;
79
+ const hit = RCA_MODELS.find((m) => m.name === name);
80
+ if (hit)
81
+ return { provider: hit.provider, name };
82
+ if (name.startsWith('callkaro/'))
83
+ return { provider: 'callkaro', name };
84
+ if (name.startsWith('arn:aws:bedrock'))
85
+ return { provider: 'bedrock', name };
86
+ return { provider: DEFAULT_RCA_MODEL.provider, name };
87
+ }
88
+ function normalizeMetrics(metrics) {
89
+ const list = Array.isArray(metrics) ? metrics : [];
90
+ const locked = list.find((m) => m?.name === exports.LOCKED_METRIC || m?.name === 'connected_to_conversion');
91
+ const rest = list
92
+ .filter((m) => m && m.name !== exports.LOCKED_METRIC && m.name !== 'connected_to_conversion')
93
+ .map((m) => ({ name: String(m.name || ''), source: String(m.source || ''), value: String(m.value ?? '') }));
94
+ return [
95
+ { name: exports.LOCKED_METRIC, source: exports.LOCKED_METRIC, value: String(locked?.value ?? '6') },
96
+ ...rest,
97
+ ];
98
+ }
99
+ const newElementId = () => `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
100
+ exports.newElementId = newElementId;
101
+ /** Bare token (`total_calls`) becomes `builtin:total_calls`; scoped refs stay as-is. */
102
+ function canonicalRef(raw) {
103
+ const trimmed = String(raw || '').trim();
104
+ if (!trimmed)
105
+ return '';
106
+ return trimmed.includes(':') ? trimmed : `builtin:${trimmed}`;
107
+ }
108
+ /** Short token inside `${...}`: strips `builtin:`, keeps `custom:...`, `postcall:...`, etc. */
109
+ function shortToken(ref) {
110
+ const s = String(ref || '').trim();
111
+ return s.startsWith('builtin:') ? s.slice(8) : s;
112
+ }
113
+ function parseRef(ref) {
114
+ const [left, base = 'all'] = String(ref || '').split('#');
115
+ const idx = left.indexOf(':');
116
+ return idx >= 0
117
+ ? { kind: left.slice(0, idx), id: left.slice(idx + 1), base }
118
+ : { kind: 'builtin', id: left, base };
119
+ }
120
+ const BREAKDOWN_TO_DIM = {
121
+ 'builtin:hangup_reasons': 'dim:hangup_reason',
122
+ 'builtin:drop_off_reasons': 'dim:drop_off_reason',
123
+ };
124
+ const BASE_TO_MEASURE = {
125
+ all: 'builtin:total_calls',
126
+ connected: 'builtin:connected_calls',
127
+ converted: 'builtin:converted_calls',
128
+ unique: 'builtin:unique_receipts',
129
+ unique_connected: 'builtin:unique_connected',
130
+ };
131
+ function autoBreakdownAxes(ref) {
132
+ if (BREAKDOWN_TO_DIM[ref])
133
+ return { axis: BREAKDOWN_TO_DIM[ref], measure: 'builtin:total_calls' };
134
+ const { kind, id, base } = parseRef(ref);
135
+ if (kind === 'postcall') {
136
+ return { axis: `dim:postcall:${id}`, measure: BASE_TO_MEASURE[base] || 'builtin:total_calls' };
137
+ }
138
+ return null;
139
+ }
140
+ function defaultChartAxes(chartType, refs = []) {
141
+ const meta = CHART_META[chartType] || CHART_META.table;
142
+ const clean = refs.map(canonicalRef).filter(Boolean);
143
+ if (!clean.length)
144
+ return { x_axis: null, y_axis: [], stack_by: null };
145
+ const single = clean.length === 1 ? autoBreakdownAxes(clean[0]) : null;
146
+ if (single && meta.x)
147
+ return { x_axis: single.axis, y_axis: [single.measure], stack_by: null };
148
+ if (!meta.x || meta.optionalX)
149
+ return { x_axis: null, y_axis: clean, stack_by: null };
150
+ if (chartType === 'line')
151
+ return { x_axis: 'dim:time_hour', y_axis: clean, stack_by: null };
152
+ return {
153
+ x_axis: 'metrics',
154
+ y_axis: meta.multiY ? clean : clean.slice(0, 1),
155
+ stack_by: null,
156
+ };
157
+ }
158
+ function defaultReportConfig() {
159
+ return {
160
+ recipients: [],
161
+ min_trigger_time: '08:00',
162
+ max_trigger_time: '08:00',
163
+ interval_hours: 24,
164
+ anchor_at: '',
165
+ custom_variables: [],
166
+ elements: [],
167
+ };
168
+ }
169
+ function estimatedHeight(el) {
170
+ if (el?.h && Number.isFinite(Number(el.h)))
171
+ return Number(el.h);
172
+ if (el?.type === 'table')
173
+ return ((el.cells?.length || 1) * 36);
174
+ if (el?.type === 'text')
175
+ return 64;
176
+ return exports.CHART_HEIGHTS[el?.chart_type] || 240;
177
+ }
178
+ function nextElementY(elements) {
179
+ if (!Array.isArray(elements) || !elements.length)
180
+ return exports.MARGIN;
181
+ return elements.reduce((max, el) => Math.max(max, (Number(el.y) || 0) + estimatedHeight(el) + 24), exports.MARGIN);
182
+ }
183
+ function normalizeElement(el) {
184
+ if (!el || typeof el !== 'object')
185
+ return el;
186
+ if (el.type !== 'chart')
187
+ return el;
188
+ const chartType = (exports.CHART_TYPES.includes(el.chart_type) ? el.chart_type : 'tiles');
189
+ const h = el.h ?? (exports.CHART_HEIGHTS[chartType] || 280);
190
+ if (el.x_axis !== undefined) {
191
+ const yAxis = Array.isArray(el.y_axis) ? el.y_axis.map(canonicalRef) : (el.refs || []).map(canonicalRef);
192
+ return { ...el, chart_type: chartType, h, y_axis: yAxis, refs: yAxis };
193
+ }
194
+ const axes = defaultChartAxes(chartType, el.refs || el.y_axis || []);
195
+ return { ...el, chart_type: chartType, h, ...axes, refs: axes.y_axis };
196
+ }
197
+ function normalizeReportConfig(raw) {
198
+ const src = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
199
+ const { widgets, ...rest } = src;
200
+ const rawElements = Array.isArray(src.elements)
201
+ ? src.elements
202
+ : Array.isArray(widgets)
203
+ ? widgets.map((w) => ({
204
+ id: w.id || (0, exports.newElementId)(),
205
+ type: 'chart',
206
+ chart_type: w.type === 'stats' ? 'tiles' : w.type,
207
+ title: w.title || '',
208
+ refs: w.refs || [],
209
+ x: exports.MARGIN,
210
+ y: exports.MARGIN,
211
+ w: exports.PAGE_W - exports.MARGIN * 2,
212
+ options: w.options || {},
213
+ }))
214
+ : [];
215
+ return {
216
+ ...defaultReportConfig(),
217
+ ...rest,
218
+ recipients: Array.isArray(src.recipients) ? src.recipients : [],
219
+ custom_variables: Array.isArray(src.custom_variables) ? src.custom_variables : [],
220
+ elements: rawElements.map(normalizeElement),
221
+ };
222
+ }
223
+ /**
224
+ * Turn a `GET /kpi-manager/:agentId` response (or `{}` when 404) into a complete
225
+ * 10-field `PUT /kpi-manager/:agentId` body so partial CLI edits never wipe
226
+ * sibling fields (`api_endpoints`, `report_config`, `rca_prompt`, etc.).
227
+ */
228
+ function buildFullKpiPayload(current = {}, patch = {}) {
229
+ const rcaModel = String(patch.rca_model ?? current?.rca_model ?? DEFAULT_RCA_MODEL.name);
230
+ return {
231
+ api_endpoints: patch.api_endpoints ?? (Array.isArray(current?.api_endpoints) ? current.api_endpoints : []),
232
+ python_data_sources: patch.python_data_sources ?? (Array.isArray(current?.python_data_sources) ? current.python_data_sources : []),
233
+ expression_data_sources: patch.expression_data_sources ??
234
+ (Array.isArray(current?.expression_data_sources) ? current.expression_data_sources : []),
235
+ kpi_metrics: normalizeMetrics(patch.kpi_metrics ?? current?.kpi_metrics),
236
+ rca_model: rcaModel,
237
+ rca_llms: patch.rca_llms ?? rcaLlmsFor(rcaModel),
238
+ rca_prompt: String(patch.rca_prompt ?? current?.rca_prompt ?? ''),
239
+ nightly_rca_enabled: patch.nightly_rca_enabled !== undefined
240
+ ? Boolean(patch.nightly_rca_enabled)
241
+ : current?.nightly_rca_enabled !== false,
242
+ report_enabled: patch.report_enabled !== undefined ? Boolean(patch.report_enabled) : current?.report_enabled === true,
243
+ report_config: normalizeReportConfig(patch.report_config ?? current?.report_config),
244
+ };
245
+ }
246
+ /** Format a preview value object (`{kind, value, actual, rows, ...}`) for terminal output. */
247
+ function formatPreviewValue(val) {
248
+ if (!val)
249
+ return '—';
250
+ const fmt = (n) => n === null || n === undefined || n === '' || Number.isNaN(Number(n))
251
+ ? '—'
252
+ : Number(n).toLocaleString('en-IN', { maximumFractionDigits: 2 });
253
+ switch (val.kind) {
254
+ case 'pct':
255
+ return val.value === null || val.value === undefined ? '—' : `${fmt(val.value)}%`;
256
+ case 'count':
257
+ return val.pct !== null && val.pct !== undefined
258
+ ? `${fmt(val.value)} (${fmt(val.pct)}% of ${fmt(val.base_value)})`
259
+ : fmt(val.value);
260
+ case 'metric':
261
+ return val.actual === null || val.actual === undefined
262
+ ? `— (target ${fmt(val.target)}%)`
263
+ : `${fmt(val.actual)}% (target ${fmt(val.target)}%)`;
264
+ case 'text':
265
+ return val.value === null || val.value === undefined ? val.note || '—' : String(val.value);
266
+ case 'breakdown':
267
+ return Array.isArray(val.rows) && val.rows.length
268
+ ? val.rows.map((r) => `${r.label}: ${fmt(r.value)} (${fmt(r.pct)}%)`).join(', ')
269
+ : '—';
270
+ case 'error':
271
+ return `[ERROR: ${val.error}]`;
272
+ default:
273
+ return fmt(val.value);
274
+ }
275
+ }
276
+ /** Interpolate `${token}` inside a text box or table cell using `preview.values`. */
277
+ function interpolateReportText(text, values = {}) {
278
+ return String(text || '').replace(/\$\{([^}]+)\}/g, (_m, raw) => {
279
+ const val = values[canonicalRef(raw)] ?? values[String(raw).trim()];
280
+ if (!val)
281
+ return `\${${raw}}`;
282
+ if (val.kind === 'count') {
283
+ return val.value === null || val.value === undefined
284
+ ? '—'
285
+ : Number(val.value).toLocaleString('en-IN', { maximumFractionDigits: 2 });
286
+ }
287
+ return formatPreviewValue(val);
288
+ });
289
+ }
290
+ exports.REPORT_SCHEMA_REFERENCE = `KPI Report schema & reference (PUT /kpi-manager/<agentId>)
291
+
292
+ Top-level report fields on /kpi-manager/<agentId>:
293
+ "report_enabled": boolean Auto Report toggle (emails report on schedule)
294
+ "report_config": {
295
+ "recipients": ["a@b.com"], Up to 10 valid email addresses
296
+ "min_trigger_time": "08:00", HH:mm IST (15-min steps)
297
+ "max_trigger_time": "20:00", HH:mm IST (if interval_hours >= 24, equals min_trigger_time)
298
+ "interval_hours": 24, One of: 1, 2, 3, 4, 6, 8, 12, 24, 48, 72, 168
299
+ "anchor_at": "", Set by server on save
300
+ "page_count": 1, Number of A4 pages (794 x 1123 px per page, margin 40)
301
+ "custom_variables": [...], Custom call counters (conditions or expression)
302
+ "elements": [...] Canvas items: text, table, chart
303
+ }
304
+
305
+ Variable references (use in chart y_axis/refs, or as \${...} inside text & table cells):
306
+ Built-in (in \${...} you may omit "builtin:", e.g. \${total_calls}):
307
+ builtin:total_calls Total triggered calls (count)
308
+ builtin:connected_calls Total connected calls (count)
309
+ builtin:connected_pct Connected % (pct)
310
+ builtin:unique_receipts Unique receipts (count)
311
+ builtin:unique_connected Unique connected receipts (count)
312
+ builtin:unique_connected_pct Receipt connectivity % (pct)
313
+ builtin:converted_calls Converted calls (count)
314
+ builtin:converted_pct Converted % of connected (pct)
315
+ builtin:unique_converted Unique converted receipts (count)
316
+ builtin:avg_connected_duration Avg connected duration in seconds (number)
317
+ builtin:drop_off_reasons Drop-off reasons (breakdown)
318
+ builtin:hangup_reasons Hangup reasons (breakdown)
319
+
320
+ Post-call variables (<base> is: all | connected | unique | unique_connected; #all may be omitted):
321
+ postcall:<varName> e.g. postcall:car_name, postcall:test_drive_booked#connected
322
+
323
+ Custom variables (<base> is: all | unique; #all may be omitted):
324
+ custom:<varName> e.g. custom:booked_test_drives, custom:booked_test_drives#unique
325
+
326
+ Data sources (from api_endpoints, python_data_sources, expression_data_sources):
327
+ source:<apiName>.data.<field> API Endpoint response field
328
+ source:<pyName>.<field> Python Code return dict key
329
+ source:<exprName>.count Expression data source (.count, .total, or .pct)
330
+
331
+ KPI metrics:
332
+ metric:<metricName> e.g. metric:connected_to_conversion_percentage
333
+
334
+ Chart dimensions (for x_axis and stack_by on bar, line, pie, funnel, table charts):
335
+ "metrics" Each metric (compare y_axis metrics side by side)
336
+ "dim:time_hour" Hour
337
+ "dim:time_day" Day
338
+ "dim:hangup_reason" Hangup reason
339
+ "dim:drop_off_reason" Drop-off reason
340
+ "dim:is_connected" Connected / not connected
341
+ "dim:converted" Converted / not converted
342
+ "dim:direction" Direction
343
+ "dim:postcall:<varName>" Any post-call variable on the agent
344
+ "dim:metadata:<key>" Any {{variable}} in the agent's prompt
345
+
346
+ Element types (A4 sheet is 794px wide x 1123px tall per page; page 2 starts at y = 1123):
347
+ 1. text:
348
+ {"id":"t1","type":"text","content":"Calls: \${total_calls}","x":40,"y":40,"w":360,
349
+ "style":{"bold":false,"italic":false,"underline":false,"size":14,"color":"#111827","align":"left"}}
350
+ 2. table (1..8 rows x 1..8 cols; row 0 is bold header when header !== false):
351
+ {"id":"tb1","type":"table","header":true,"x":40,"y":120,"w":480,
352
+ "cells":[["Metric","Value"],["Triggered","\${total_calls}"],["Connected","\${connected_pct}"]]}
353
+ 3. chart (chart_type: tiles | bar | line | pie | funnel | table):
354
+ {"id":"c1","type":"chart","chart_type":"tiles","title":"Headline","x_axis":null,
355
+ "y_axis":["builtin:total_calls","builtin:connected_calls","builtin:connected_pct"],
356
+ "refs":["builtin:total_calls","builtin:connected_calls","builtin:connected_pct"],
357
+ "stack_by":null,"options":{},"x":40,"y":240,"w":714,"h":130}
358
+
359
+ Custom variable & Expression data source syntax:
360
+ Fields: post_call.<name>, metadata.<key>, is_connected, is_converted, hangup_reason, call_duration
361
+ Operators: && || ! == != > >= < <= and in ['a', 'b'] (case-insensitive strings; bare field = is true)
362
+ Example: (is_converted==true) && post_call.test_drive_booked == 'YES' && is_connected
363
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callkaro-cli-unofficial",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Unofficial fork of the CallKaro CLI — manage your voice AI agents from the terminal",
6
6
  "bin": {