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.
@@ -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,218 @@ 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
+ }
319
+ // --- Agent version commits, diff, revert & v2 save -------------------------
320
+ /** List commits for an agent (`GET /v2/commits/:agentId?version=&page=&pageSize=`). */
321
+ agentCommits(agentId, query = {}) {
322
+ return this.request('GET', `/v2/commits/${encodeURIComponent(agentId)}${qs(query)}`);
323
+ }
324
+ /** Fetch one commit in full (`GET /v2/commits/single/:commitId`). */
325
+ agentCommit(commitId) {
326
+ return this.request('GET', `/v2/commits/single/${encodeURIComponent(commitId)}`);
327
+ }
328
+ /** Compare two commits of an agent (`GET /v2/commits/:agentId/compare?commitA=&commitB=`). */
329
+ compareAgentCommits(agentId, commitA, commitB) {
330
+ return this.request('GET', `/v2/commits/${encodeURIComponent(agentId)}/compare${qs({ commitA, commitB })}`);
331
+ }
332
+ /** Revert an agent version to a previous commit (`POST /v2/commits/revert/:agentId/:commitId`). */
333
+ revertAgentCommit(agentId, commitId) {
334
+ return this.request('POST', `/v2/commits/revert/${encodeURIComponent(agentId)}/${encodeURIComponent(commitId)}`, {});
335
+ }
336
+ /** Compare two agent versions (`POST /v2/agents/compareVersions` with `{ versionIds: [a, b] }`). */
337
+ compareAgentVersions(versionIds) {
338
+ return this.request('POST', '/v2/agents/compareVersions', { versionIds });
339
+ }
340
+ /**
341
+ * Dashboard `/v2/agents/:agentId?version=:versionId` save — accepts function
342
+ * `source_code` and `commitMessage` that `/cli/agents` rejects.
343
+ */
344
+ updateAgentV2(agentId, versionId, body) {
345
+ const q = versionId ? qs({ version: versionId }) : '';
346
+ return this.request('PUT', `/v2/agents/${encodeURIComponent(agentId)}${q}`, body);
347
+ }
348
+ /** Delete an agent version (`DELETE /v2/agents/deleteversion/:agentId?version=:versionId`). */
349
+ deleteAgentVersionV2(agentId, versionId) {
350
+ return this.request('DELETE', `/v2/agents/deleteversion/${encodeURIComponent(agentId)}${qs({ version: versionId })}`);
351
+ }
352
+ /** Duplicate an agent (`PUT /v2/agents/duplicate/:agentId` with `{ name, versions }`). */
353
+ duplicateAgentV2(agentId, body) {
354
+ return this.request('PUT', `/v2/agents/duplicate/${encodeURIComponent(agentId)}`, body);
355
+ }
356
+ // --- Audit summary drilldowns (`/v1/audit-summary/*`) ----------------------
357
+ /** Summary rollup for a strategy or batch (`GET /v1/audit-summary/:scope/:id/summary`). */
358
+ auditSummary(scope, id, query = {}) {
359
+ return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/summary${qs(query)}`);
360
+ }
361
+ /** Audited calls in a dimension/category (`GET /v1/audit-summary/:scope/:id/categories/:dimension/:category/calls`). */
362
+ auditCategoryCalls(scope, id, dimension, category, query = {}) {
363
+ return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/categories/${encodeURIComponent(dimension)}/${encodeURIComponent(category)}/calls${qs(query)}`);
364
+ }
365
+ /** Audited calls at a severity level (`GET /v1/audit-summary/:scope/:id/severities/:severity/calls`). */
366
+ auditSeverityCalls(scope, id, severity, query = {}) {
367
+ return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/severities/${encodeURIComponent(severity)}/calls${qs(query)}`);
368
+ }
369
+ /** Audited calls for an extra-variable value (`GET /v1/audit-summary/:scope/:id/extra-variables/:name/:value/calls`). */
370
+ auditExtraVariableCalls(scope, id, name, value, query = {}) {
371
+ return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/extra-variables/${encodeURIComponent(name)}/${encodeURIComponent(value)}/calls${qs(query)}`);
372
+ }
373
+ /** Audited calls scoring below 80 (`GET /v1/audit-summary/:scope/:id/scores/below-80/calls`). */
374
+ auditBelow80Calls(scope, id, query = {}) {
375
+ return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/scores/below-80/calls${qs(query)}`);
376
+ }
377
+ /** Single audited call report (`GET /v1/audit-summary/:scope/:id/calls/:callId`). */
378
+ auditCallDetail(scope, id, callId, query = {}) {
379
+ return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/calls/${encodeURIComponent(callId)}${qs(query)}`);
380
+ }
381
+ // --- Diagnostic analytics (`/analytics/*`) ---------------------------------
382
+ /** Failed LLM/TTS response buckets (`GET /analytics/failed-responses`). */
383
+ failedResponses(query = {}) {
384
+ return this.request('GET', `/analytics/failed-responses${qs(query)}`);
385
+ }
386
+ /** Calls matching a failed response bucket (`GET /analytics/failed-response-calls`). */
387
+ failedResponseCalls(query = {}) {
388
+ return this.request('GET', `/analytics/failed-response-calls${qs(query)}`);
389
+ }
390
+ /** Per-function execution analytics (`GET /analytics/functions`). */
391
+ functionAnalytics(query = {}) {
392
+ return this.request('GET', `/analytics/functions${qs(query)}`);
393
+ }
394
+ /** Chat agent analytics (`GET /analytics/chat`). */
395
+ chatAnalytics(query = {}) {
396
+ return this.request('GET', `/analytics/chat${qs(query)}`);
397
+ }
398
+ // --- Do-Not-Disturb numbers (`/dnd-phone-numbers/*`) -----------------------
399
+ /** List DND numbers (`GET /dnd-phone-numbers`). */
400
+ listDndNumbers(query = {}) {
401
+ return this.request('GET', `/dnd-phone-numbers${qs(query)}`);
402
+ }
403
+ /** Add a single DND phone number (`POST /dnd-phone-numbers/add`). */
404
+ addDndNumber(phoneNumber) {
405
+ return this.request('POST', '/dnd-phone-numbers/add', { phoneNumber });
406
+ }
407
+ /** Update a DND record (`PUT /dnd-phone-numbers/:id`). */
408
+ updateDndNumber(id, phoneNumber) {
409
+ return this.request('PUT', `/dnd-phone-numbers/${encodeURIComponent(id)}`, { phoneNumber });
410
+ }
411
+ /** Remove a DND record by id (`DELETE /dnd-phone-numbers/:id`). */
412
+ removeDndNumber(id) {
413
+ return this.request('DELETE', `/dnd-phone-numbers/${encodeURIComponent(id)}`);
414
+ }
415
+ /** Remove multiple DND records by id (`POST /dnd-phone-numbers/delete-multiple`). */
416
+ removeDndNumbers(ids) {
417
+ return this.request('POST', '/dnd-phone-numbers/delete-multiple', { ids });
418
+ }
419
+ /** Bulk import DND phone numbers (`POST /dnd-phone-numbers/import`). */
420
+ importDndNumbers(phoneNumbers) {
421
+ return this.request('POST', '/dnd-phone-numbers/import', { phoneNumbers });
422
+ }
423
+ // --- Call QA Feedback tickets (`/feedback/*`) ------------------------------
424
+ /** List feedback tickets (`GET /feedback?status=processing|in-review|resolved&page=1`). */
425
+ listFeedback(query = {}) {
426
+ return this.request('GET', `/feedback${qs({ status: query.status || 'processing', page: query.page || 1 })}`);
427
+ }
428
+ /** Create a feedback ticket (`POST /feedback`). */
429
+ createFeedback(body) {
430
+ return this.request('POST', '/feedback', body);
431
+ }
432
+ /** Comment on a feedback ticket (`PUT /feedback/:id/comment`). */
433
+ commentFeedback(id, text) {
434
+ return this.request('PUT', `/feedback/${encodeURIComponent(id)}/comment`, { text });
435
+ }
436
+ /** Submit a feedback ticket for review (`PUT /feedback/:id/submit-review`). */
437
+ submitFeedbackReview(id, body) {
438
+ return this.request('PUT', `/feedback/${encodeURIComponent(id)}/submit-review`, body);
439
+ }
440
+ /** Send a feedback ticket back to processing (`PUT /feedback/:id/back-to-processing`). */
441
+ reopenFeedback(id, comment) {
442
+ return this.request('PUT', `/feedback/${encodeURIComponent(id)}/back-to-processing`, { comment });
443
+ }
444
+ /** Resolve a feedback ticket (`PUT /feedback/:id/resolve`). */
445
+ resolveFeedback(id, comment) {
446
+ return this.request('PUT', `/feedback/${encodeURIComponent(id)}/resolve`, { comment });
447
+ }
448
+ // --- Knowledge Base (`/knowledge/*`) ---------------------------------------
449
+ /** List knowledge base items for the user (`GET /knowledge/user`). */
450
+ listKnowledge() {
451
+ return this.request('GET', '/knowledge/user');
452
+ }
453
+ /** Delete a knowledge base item (`DELETE /knowledge/:id`). */
454
+ deleteKnowledge(id) {
455
+ return this.request('DELETE', `/knowledge/${encodeURIComponent(id)}`);
456
+ }
457
+ // --- OpenAI / AI Copilot generators (`/openai-tools/*`) --------------------
458
+ /** Draft a simulation test case from a real call (`POST /openai-tools/generate-simulation-testcase-draft`). */
459
+ generateSimFromCall(body) {
460
+ return this.request('POST', '/openai-tools/generate-simulation-testcase-draft', {
461
+ includeMetadata: true,
462
+ includeFunctions: true,
463
+ ...body,
464
+ });
465
+ }
466
+ /** Generate or refine a simulation test case from a hint (`POST /openai-tools/generate-simulation-testcase-from-prompt`). */
467
+ generateSimFromPrompt(body) {
468
+ return this.request('POST', '/openai-tools/generate-simulation-testcase-from-prompt', body);
469
+ }
470
+ /** Generate an audit strategy draft (`POST /openai-tools/generate-audit-strategy-draft`). */
471
+ generateAuditStrategyDraft(body) {
472
+ return this.request('POST', '/openai-tools/generate-audit-strategy-draft', body);
473
+ }
474
+ /** Generate a callflow summary draft (`POST /openai-tools/generate-callflow-summary-draft`). */
475
+ generateCallflowSummaryDraft(body) {
476
+ return this.request('POST', '/openai-tools/generate-callflow-summary-draft', body);
477
+ }
478
+ /** Generate or edit a voice agent function (`POST /openai-tools/generate-function`). */
479
+ generateFunctionCode(body) {
480
+ return this.request('POST', '/openai-tools/generate-function', body);
481
+ }
482
+ /** Generate post-call analysis config (`POST /openai-tools/generate-postcall-config`). */
483
+ generatePostcallConfig(body) {
484
+ return this.request('POST', '/openai-tools/generate-postcall-config', body);
485
+ }
486
+ /** Generate workflow code step (`POST /openai-tools/generate-workflow-code`). */
487
+ generateWorkflowCode(body) {
488
+ return this.request('POST', '/openai-tools/generate-workflow-code', body);
489
+ }
228
490
  // --- Call analyzers -------------------------------------------------------
229
491
  listAnalyzers() { return this.request('GET', '/v1/call-analyzers'); }
230
492
  // --- 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.8.0",
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": {