callkaro-cli-unofficial 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +196 -0
  2. package/dist/commands/accounts.js +275 -0
  3. package/dist/commands/agentPhone.js +50 -0
  4. package/dist/commands/agentVersions.js +228 -0
  5. package/dist/commands/agents.js +455 -0
  6. package/dist/commands/analytics.js +77 -0
  7. package/dist/commands/analyzers.js +130 -0
  8. package/dist/commands/auditStrategies.js +178 -0
  9. package/dist/commands/batches.js +189 -0
  10. package/dist/commands/calls.js +193 -0
  11. package/dist/commands/chatAgents.js +251 -0
  12. package/dist/commands/jsonInput.js +56 -0
  13. package/dist/commands/kpi.js +681 -0
  14. package/dist/commands/models.js +99 -0
  15. package/dist/commands/numbers.js +154 -0
  16. package/dist/commands/ongoing.js +39 -0
  17. package/dist/commands/secrets.js +124 -0
  18. package/dist/commands/setJson.js +42 -0
  19. package/dist/commands/simulations.js +139 -0
  20. package/dist/commands/skills.js +318 -0
  21. package/dist/commands/transcribers.js +95 -0
  22. package/dist/commands/update.js +86 -0
  23. package/dist/commands/voices.js +211 -0
  24. package/dist/data/catalog.json +1978 -0
  25. package/dist/endpoints.json +8 -0
  26. package/dist/index.js +684 -0
  27. package/dist/lib/agentImport.js +60 -0
  28. package/dist/lib/agentUpdate.js +262 -0
  29. package/dist/lib/api.js +287 -0
  30. package/dist/lib/browserAuth.js +93 -0
  31. package/dist/lib/catalog.js +136 -0
  32. package/dist/lib/config.js +354 -0
  33. package/dist/lib/dashboardApi.js +197 -0
  34. package/dist/lib/dashboardAuth.js +339 -0
  35. package/dist/lib/dbFields.js +30 -0
  36. package/dist/lib/logQuery.js +140 -0
  37. package/dist/lib/looseJson.js +198 -0
  38. package/dist/lib/session.js +17 -0
  39. package/dist/lib/types.js +15 -0
  40. package/dist/lib/ui.js +91 -0
  41. package/dist/lib/versions.js +19 -0
  42. package/package.json +52 -0
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Client-side import validation, mirrored from the web importer
4
+ * (Frontend/src/components/agent/ImportAgentV2.jsx) so a bad file is rejected
5
+ * with a clear message before it ever reaches the backend. Keep this list in
6
+ * sync with that component.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.AGENT_LEVEL_KEYS = exports.REQUIRED_KEYS = void 0;
10
+ exports.missingKeys = missingKeys;
11
+ exports.agentLevelMismatches = agentLevelMismatches;
12
+ /** Fields every agent JSON must contain (same set the web importer checks). */
13
+ exports.REQUIRED_KEYS = [
14
+ 'postcallmodel', 'useOthersDropOffReason', 'name', 'systemprompt',
15
+ 'auto_reschedule', 'conversion_reason', 'model', 'transcriber',
16
+ 'voice_configuration', 'inboundPhoneNumbers', 'outboundPhoneNumber',
17
+ 'secondaryOutboundPhoneNumber', 'bgNoise', 'bgNoiseVolume', 'voicemail_msg',
18
+ 'voicemail_custom_msg', 'noise_cancellation', 'speakfirst', 'speakfirst_inbound',
19
+ 'end_call_msg', 'functions', 'knowledges', 'temperature', 'vad_configuration',
20
+ 'time_limit', 'postcall', 'webhook', 'initial_pause_outbound',
21
+ 'initial_pause_inbound', 'silence_count', 'silence_language', 'default_language',
22
+ 'silence_wait', 'silence_mode', 'silence_instructions', 'silence_prompts',
23
+ 'systempromptType', 'role', 'goal', 'instructions', 'callFlow', 'guardrails',
24
+ 'rebuttals', 'language_switching', 'language_switching_instructions',
25
+ 'language_lockin_time', 'allowed_languages', 'language_switch_min_words',
26
+ ];
27
+ /**
28
+ * Agent-level (as opposed to version-level) fields. In a multi-version import
29
+ * these must be identical across every entry, since they all become one agent.
30
+ */
31
+ exports.AGENT_LEVEL_KEYS = new Set([
32
+ 'folderId', 'name', 'phoneNumber', 'inboundPhoneNumbers', 'outboundPhoneNumber',
33
+ 'secondaryOutboundPhoneNumber', 'whatsappPhoneNumber', 'whatsappInboundPhoneNumber',
34
+ 'publishedVersionId', 'lastpublisedDate', 'abTestVersions', 'abTestEnabled', '_id',
35
+ 'default_agent_language', 'useMultiTranscribers', 'publishedVersionsByLanguage',
36
+ 'agentStatus',
37
+ ]);
38
+ /** Required keys absent from a single entry. */
39
+ function missingKeys(entry) {
40
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
41
+ return [...exports.REQUIRED_KEYS];
42
+ return exports.REQUIRED_KEYS.filter((k) => !(k in entry));
43
+ }
44
+ /** Agent-level fields that differ from the first entry (multi-version import). */
45
+ function agentLevelMismatches(entries) {
46
+ if (entries.length < 2)
47
+ return [];
48
+ const ref = entries[0];
49
+ const out = [];
50
+ for (let i = 1; i < entries.length; i++) {
51
+ for (const key of exports.AGENT_LEVEL_KEYS) {
52
+ if (!(key in ref) && !(key in entries[i]))
53
+ continue;
54
+ if (JSON.stringify(ref[key]) !== JSON.stringify(entries[i][key])) {
55
+ out.push({ key, entry: i + 1, expected: ref[key], got: entries[i][key] });
56
+ }
57
+ }
58
+ }
59
+ return out;
60
+ }
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENUMS = exports.STRING_ARRAY_KEYS = exports.REPLACE_WHOLESALE_KEYS = void 0;
4
+ exports.applyLiveModels = applyLiveModels;
5
+ exports.validateEnums = validateEnums;
6
+ exports.validateCallSettings = validateCallSettings;
7
+ exports.validateProviderConfigs = validateProviderConfigs;
8
+ exports.preparePatch = preparePatch;
9
+ exports.coerce = coerce;
10
+ exports.deepMerge = deepMerge;
11
+ exports.mergeWholesaleKeys = mergeWholesaleKeys;
12
+ exports.isAgentLevelKey = isAgentLevelKey;
13
+ exports.normalizeStringArray = normalizeStringArray;
14
+ const agentImport_1 = require("./agentImport");
15
+ const catalog_1 = require("./catalog");
16
+ /**
17
+ * Helpers for `ck agents update` — turning `--set '{json}'` / `--file` input
18
+ * into a clean update payload that matches how the backend `updateAgent`
19
+ * controller treats fields.
20
+ */
21
+ /**
22
+ * Version-level object fields the backend replaces *wholesale* rather than
23
+ * shallow-merging (`full_keys` in updateAgent). A partial update to one of these
24
+ * would wipe the others, so for dotted paths into them we deep-merge locally
25
+ * against the current value and send the complete object.
26
+ */
27
+ exports.REPLACE_WHOLESALE_KEYS = new Set([
28
+ 'preFormatVariables', 'customPronunciations', 'transcriber', 'secondary_transcriber',
29
+ 'voice_configuration', 'secondary_voice_configuration', 'callFlow_json',
30
+ 'variableSource', 'callPropertyMapping',
31
+ ]);
32
+ /** String-array fields the web trims + drops empties on; we match that. */
33
+ exports.STRING_ARRAY_KEYS = new Set([
34
+ 'end_call_msg', 'instructions', 'guardrails', 'rebuttals', 'silence_prompts',
35
+ 'punctuations_to_remove',
36
+ ]);
37
+ /**
38
+ * Which live catalogue list governs which field.
39
+ *
40
+ * The server scopes models per slot — `post-call` has 9 entries and does NOT
41
+ * include `gpt-4o` or `o4-mini`, while `agent` has 44 and does. Validating every
42
+ * model field against one flat list is therefore wrong in both directions.
43
+ */
44
+ const MODEL_SLOTS = {
45
+ model: 'agent',
46
+ secondary_model: 'agent',
47
+ postcallmodel: 'post-call',
48
+ };
49
+ /**
50
+ * The shipped per-slot lists from `catalog.json`, used when no dashboard session
51
+ * is present. This was a hand-written list of 7 ids while the server accepted 44
52
+ * for agent models, so it rejected ~37 working models. Reading the same file the
53
+ * voice and transcriber tables come from means `npm run catalog:check` covers
54
+ * models too, and there is one place to refresh.
55
+ *
56
+ * The empty list is a real possibility (a `--no-server` sync, a truncated file),
57
+ * and an empty allow-set would reject everything, so it falls back to the union:
58
+ * strictly more permissive, and the drift check flags the file.
59
+ */
60
+ function shippedModels(slot) {
61
+ const list = catalog_1.LLM_MODELS[slot];
62
+ if (Array.isArray(list) && list.length)
63
+ return list;
64
+ return [...new Set(Object.values(catalog_1.LLM_MODELS).flat())];
65
+ }
66
+ /** Fields with a fixed set of valid values — a wrong value is a clear error, not a 500. */
67
+ exports.ENUMS = {
68
+ default_language: ['en', 'hi', 'kn', 'ta', 'te', 'mr', 'gu', 'bn', 'ml'],
69
+ default_agent_language: ['en', 'hi', 'kn', 'ta', 'te', 'mr', 'gu', 'bn', 'ml'],
70
+ silence_language: ['en', 'hi', 'kn', 'ta', 'te', 'mr', 'gu', 'bn', 'ml', 'multi'],
71
+ silence_mode: ['default', 'custom', 'dynamic', 'ignore'],
72
+ caching_strategy: ['response', 'none', 'sentence'],
73
+ noise_cancellation_strategy: ['aicoustics', 'dtln', 'deepfilternet', 'noisereduce'],
74
+ agentStatus: ['live', 'in-progress'],
75
+ model: shippedModels('agent'),
76
+ secondary_model: shippedModels('agent'),
77
+ postcallmodel: shippedModels('post-call'),
78
+ };
79
+ /**
80
+ * Replace the model fallbacks with the server's own per-slot lists.
81
+ *
82
+ * Called by the commands when a dashboard session is available. Mutating ENUMS
83
+ * rather than threading a parameter keeps `preparePatch` and `validateEnums`
84
+ * synchronous and unchanged for every existing caller and test.
85
+ *
86
+ * A field whose slot is missing from the response keeps its fallback, so a
87
+ * partial response can never silently widen or narrow validation.
88
+ */
89
+ function applyLiveModels(catalogs) {
90
+ const slots = catalogs?.llmModels;
91
+ if (!slots)
92
+ return false;
93
+ let applied = 0;
94
+ for (const [field, slot] of Object.entries(MODEL_SLOTS)) {
95
+ const list = slots[slot];
96
+ if (Array.isArray(list) && list.length) {
97
+ exports.ENUMS[field] = list;
98
+ applied += 1;
99
+ }
100
+ }
101
+ return applied > 0;
102
+ }
103
+ /** Throw if any field in `obj` carries a value outside its allowed set. */
104
+ function validateEnums(obj) {
105
+ for (const [key, val] of Object.entries(obj)) {
106
+ if (exports.ENUMS[key] && typeof val === 'string' && !exports.ENUMS[key].includes(val)) {
107
+ // Point at the right list rather than dumping 44 ids into the message.
108
+ const slot = MODEL_SLOTS[key];
109
+ throw new Error(`Invalid value for "${key}": "${val}"\n` +
110
+ (slot
111
+ ? ` The server lists ${exports.ENUMS[key].length} model(s) for "${slot}". Run \`cku models --slot ${slot}\` to see them.`
112
+ : ` allowed: ${exports.ENUMS[key].join(', ')}`));
113
+ }
114
+ }
115
+ }
116
+ /** Validate call-setting fields whose shape cannot be expressed as a simple enum. */
117
+ function validateCallSettings(obj) {
118
+ if ('noise_cancellation' in obj && typeof obj.noise_cancellation !== 'boolean') {
119
+ throw new Error('Invalid value for "noise_cancellation": expected a boolean');
120
+ }
121
+ if ('noise_cancellation_strength' in obj) {
122
+ const strength = obj.noise_cancellation_strength;
123
+ if (typeof strength !== 'number' || !Number.isFinite(strength) || strength < 0.05 || strength > 1) {
124
+ throw new Error('Invalid value for "noise_cancellation_strength": expected a number between 0.05 and 1');
125
+ }
126
+ }
127
+ if ('punctuations_to_remove' in obj) {
128
+ const punctuations = obj.punctuations_to_remove;
129
+ if (!Array.isArray(punctuations) || punctuations.some((v) => typeof v !== 'string')) {
130
+ throw new Error('Invalid value for "punctuations_to_remove": expected an array of strings');
131
+ }
132
+ }
133
+ if ('webhook_headers' in obj) {
134
+ const headers = obj.webhook_headers;
135
+ const valid = Array.isArray(headers) &&
136
+ headers.every((header) => {
137
+ if (!header || typeof header !== 'object' || Array.isArray(header))
138
+ return false;
139
+ const row = header;
140
+ return typeof row.key_name === 'string' && typeof row.value === 'string';
141
+ });
142
+ if (!valid) {
143
+ throw new Error('Invalid value for "webhook_headers": expected an array of { key_name, value } strings');
144
+ }
145
+ }
146
+ }
147
+ /** The four voice/transcriber objects, and which catalogue validates each. */
148
+ const PROVIDER_CONFIGS = [
149
+ { key: 'voice_configuration', kind: 'voice' },
150
+ { key: 'secondary_voice_configuration', kind: 'voice' },
151
+ { key: 'transcriber', kind: 'transcriber' },
152
+ { key: 'secondary_transcriber', kind: 'transcriber' },
153
+ ];
154
+ /**
155
+ * Catch a bad provider or a model that belongs to a *different* provider before
156
+ * it reaches the database. Both mistakes persist silently and only surface as a
157
+ * broken call, so failing here is much cheaper.
158
+ *
159
+ * Patches may be partial (a dotted `--set` touching only `voice_speed`), so each
160
+ * check runs only when the keys it needs are actually present.
161
+ */
162
+ function validateProviderConfigs(obj) {
163
+ for (const { key, kind } of PROVIDER_CONFIGS) {
164
+ const cfg = obj[key];
165
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
166
+ continue;
167
+ const c = cfg;
168
+ const providerKey = kind === 'voice' ? 'voice_provider' : 'transcriber_provider';
169
+ const modelKey = kind === 'voice' ? 'voice_model' : 'transcriber_model';
170
+ const providerName = c[providerKey];
171
+ if (typeof providerName !== 'string')
172
+ continue;
173
+ const provider = kind === 'voice' ? (0, catalog_1.findVoiceProvider)(providerName) : (0, catalog_1.findTranscriberProvider)(providerName);
174
+ if (!provider) {
175
+ const known = (kind === 'voice' ? catalog_1.VOICE_PROVIDERS : catalog_1.TRANSCRIBER_PROVIDERS).map((p) => p.name);
176
+ throw new Error(`Unknown ${providerKey} "${providerName}" in ${key}\n allowed: ${known.join(', ')}`);
177
+ }
178
+ // Providers store the canonical spelling; "elevenlabs" must land as "Eleven Labs".
179
+ if (providerName !== provider.name)
180
+ c[providerKey] = provider.name;
181
+ const model = c[modelKey];
182
+ if (typeof model !== 'string' || !model)
183
+ continue;
184
+ if (!provider.models.length) {
185
+ throw new Error(`${provider.name} takes no ${modelKey} — remove it from ${key}.`);
186
+ }
187
+ if (!provider.models.some((m) => m.name === model)) {
188
+ throw new Error(`"${model}" is not a ${provider.name} ${modelKey} (in ${key})\n` +
189
+ ` allowed: ${provider.models.map((m) => m.name).join(', ')}`);
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Validate + split a JSON object of fields for `ck agents update --set '{json}'`.
195
+ * Enums are checked, string-arrays normalized, and keys partitioned into
196
+ * agent-level vs version-level (the latter need a --versions target).
197
+ *
198
+ * `wholesaleKeys` lists the replace-wholesale objects present in the patch, so
199
+ * the caller can merge them against the agent's current value before sending.
200
+ */
201
+ function preparePatch(obj) {
202
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
203
+ throw new Error('--set must be a JSON object of fields, e.g. \'{"name":"x","temperature":5}\'');
204
+ }
205
+ const source = obj;
206
+ validateEnums(source);
207
+ validateCallSettings(source);
208
+ const patch = { ...source };
209
+ validateProviderConfigs(patch);
210
+ for (const key of Object.keys(patch)) {
211
+ if (exports.STRING_ARRAY_KEYS.has(key))
212
+ patch[key] = normalizeStringArray(patch[key]);
213
+ }
214
+ const versionKeys = Object.keys(patch).filter((k) => !isAgentLevelKey(k));
215
+ const wholesaleKeys = Object.keys(patch).filter((k) => exports.REPLACE_WHOLESALE_KEYS.has(k) && patch[k] && typeof patch[k] === 'object' && !Array.isArray(patch[k]));
216
+ return { patch, versionKeys, wholesaleKeys };
217
+ }
218
+ /** JSON-first value coercion: `true`→bool, `5`→num, `[...]`→array, else the raw string. */
219
+ function coerce(raw) {
220
+ try {
221
+ return JSON.parse(raw);
222
+ }
223
+ catch {
224
+ return raw;
225
+ }
226
+ }
227
+ /** Recursively merge `src` into `dst` (objects only; arrays/scalars replace). */
228
+ function deepMerge(dst, src) {
229
+ if (typeof dst !== 'object' || dst === null || Array.isArray(dst))
230
+ return src;
231
+ const out = { ...dst };
232
+ for (const [k, v] of Object.entries(src)) {
233
+ out[k] = k in out ? deepMerge(out[k], v) : v;
234
+ }
235
+ return out;
236
+ }
237
+ /**
238
+ * Merge every replace-wholesale object in `patch` against the agent's current
239
+ * value, so a partial `--set voice_configuration:{voice_speed:1.3}` keeps the
240
+ * provider, id and language that the backend would otherwise drop.
241
+ *
242
+ * Safe when the object is complete too: merging a full object over itself is a
243
+ * no-op, so callers need not guess whether the user meant replace or merge.
244
+ */
245
+ function mergeWholesaleKeys(patch, current) {
246
+ const out = { ...patch };
247
+ for (const key of exports.REPLACE_WHOLESALE_KEYS) {
248
+ if (out[key] && typeof out[key] === 'object' && !Array.isArray(out[key])) {
249
+ out[key] = deepMerge(current[key], out[key]);
250
+ }
251
+ }
252
+ return out;
253
+ }
254
+ function isAgentLevelKey(key) {
255
+ return agentImport_1.AGENT_LEVEL_KEYS.has(key);
256
+ }
257
+ /** Normalize a string-array field the way the web does: strings, trimmed, no empties. */
258
+ function normalizeStringArray(value) {
259
+ if (!Array.isArray(value))
260
+ return [];
261
+ return value.map((v) => (typeof v === 'string' ? v.trim() : '')).filter((s) => s !== '');
262
+ }
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Api = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const types_1 = require("./types");
7
+ /**
8
+ * The single place that talks to the CallKaro backend.
9
+ *
10
+ * `request()` owns the base URL, bearer token, JSON encoding, and error
11
+ * mapping — every command goes through it, so there is exactly one copy of that
12
+ * logic. Public methods map 1:1 to the `/cli/*` endpoints.
13
+ */
14
+ class Api {
15
+ token;
16
+ baseUrl;
17
+ callBaseUrl;
18
+ constructor(baseUrl, callBaseUrl, token) {
19
+ this.token = token;
20
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
21
+ this.callBaseUrl = (callBaseUrl || baseUrl).replace(/\/+$/, '');
22
+ }
23
+ request(method, path, body) {
24
+ return this.requestTo(this.baseUrl, method, path, body);
25
+ }
26
+ /** Same as request(), but against a specific base URL (e.g. the call cloud). */
27
+ async requestTo(base, method, path, body) {
28
+ const headers = { 'Content-Type': 'application/json' };
29
+ if (this.token)
30
+ headers.Authorization = `Bearer ${this.token}`;
31
+ let res;
32
+ try {
33
+ res = await fetch(`${base}${path}`, {
34
+ method,
35
+ headers,
36
+ body: body === undefined ? undefined : JSON.stringify(body),
37
+ });
38
+ }
39
+ catch (err) {
40
+ throw new types_1.ApiError(0, `Cannot reach ${base} (${err.message})`);
41
+ }
42
+ const text = await res.text();
43
+ const data = text ? safeJson(text) : undefined;
44
+ if (!res.ok) {
45
+ throw new types_1.ApiError(res.status, data?.msg || data?.message || (typeof data?.error === 'string' ? data.error : undefined) ||
46
+ `Request failed (${res.status})`, parseErrorDetails(data?.errors));
47
+ }
48
+ return data;
49
+ }
50
+ // --- auth endpoints -------------------------------------------------------
51
+ registerStart(email) { return this.request('POST', '/cli/auth/register/start', { email }); }
52
+ registerVerify(input) { return this.request('POST', '/cli/auth/register/verify', input); }
53
+ login(email, password) { return this.request('POST', '/cli/auth/login', { email, password }); }
54
+ logout() { return this.request('POST', '/cli/auth/logout'); }
55
+ /** Begin browser sign-in: returns the code for the URL + a secret only we hold. */
56
+ browserStart() { return this.request('POST', '/cli/auth/browser/start'); }
57
+ /** Poll until the browser approves; returns the token exactly once. */
58
+ browserPoll(code, secret) {
59
+ return this.request('GET', `/cli/auth/browser/poll?code=${encodeURIComponent(code)}&secret=${encodeURIComponent(secret)}`);
60
+ }
61
+ me() { return this.request('GET', '/cli/auth/me'); }
62
+ // --- secrets --------------------------------------------------------------
63
+ listSecrets() { return this.request('GET', '/cli/secrets'); }
64
+ saveSecrets(secrets) { return this.request('PUT', '/cli/secrets', { secrets }); }
65
+ // --- agents ---------------------------------------------------------------
66
+ listAgents() { return this.request('GET', '/cli/agents'); }
67
+ getAgent(agentId, version) {
68
+ const qs = version ? `?version=${encodeURIComponent(version)}` : '';
69
+ return this.request('GET', `/cli/agents/${agentId}${qs}`);
70
+ }
71
+ listAgentVersions(agentId) { return this.request('GET', `/cli/agents/${agentId}/versions`); }
72
+ /** Clone a source version, optionally migrating its prompt mode and runtime config. */
73
+ cloneAgentVersion(agentId, sourceVersionId, body) {
74
+ return this.request('POST', `/cli/agents/${agentId}/versions?version=${encodeURIComponent(sourceVersionId)}`, body);
75
+ }
76
+ exportAgent(agentId, versionIds) {
77
+ return this.request('POST', `/cli/agents/${agentId}/export`, { versionIds });
78
+ }
79
+ createAgent(payload) { return this.request('POST', '/cli/agents', payload); }
80
+ /** Import one agent with several versions from an array of exported objects. */
81
+ importMultiple(payloads) { return this.request('POST', '/cli/agents/import-multiple', payloads); }
82
+ /** Update agent-level fields and/or one version's fields. */
83
+ updateAgent(agentId, version, body) {
84
+ const qs = version ? `?version=${encodeURIComponent(version)}` : '';
85
+ return this.request('PUT', `/cli/agents/${agentId}${qs}`, body);
86
+ }
87
+ publishVersion(agentId, versionId) {
88
+ return this.request('PUT', `/cli/agents/${agentId}/publish?version=${encodeURIComponent(versionId)}`);
89
+ }
90
+ toggleVersionActive(agentId, versionId) {
91
+ return this.request('PUT', `/cli/agents/${agentId}/toggle-active?version=${encodeURIComponent(versionId)}`);
92
+ }
93
+ /** Enable A/B testing with weighted versions (reuses updateAgent). */
94
+ enableAbTest(agentId, abTestVersions) {
95
+ return this.updateAgent(agentId, undefined, { abTestEnabled: true, abTestVersions });
96
+ }
97
+ disableAbTest(agentId) {
98
+ return this.updateAgent(agentId, undefined, { abTestEnabled: false });
99
+ }
100
+ // --- chat agents ----------------------------------------------------------
101
+ listChatAgentModels() { return this.request('GET', '/cli/chat-agents/models'); }
102
+ async listChatAgentTemplates(provider = 'meta') {
103
+ const response = await this.request('GET', `/cli/chat-agents/templates/${provider}`);
104
+ const records = provider === 'aisensy' ? response.campaigns || [] : response.templates || [];
105
+ const templates = records.map((record) => {
106
+ const source = record;
107
+ const nested = provider === 'aisensy' && source.template && typeof source.template === 'object' ? source.template : source;
108
+ const id = String(source.id ?? nested.id ?? source.name ?? nested.name ?? '');
109
+ const name = String(source.name ?? nested.name ?? id);
110
+ return {
111
+ ...source,
112
+ id,
113
+ name,
114
+ provider,
115
+ language: nested.language,
116
+ category: nested.category,
117
+ status: source.status ?? nested.status,
118
+ ...(provider === 'aisensy' ? { campaignId: id, campaignName: name } : {}),
119
+ };
120
+ });
121
+ return { ...response, templates };
122
+ }
123
+ async getChatAgentTemplate(templateId, provider = 'meta') {
124
+ const { templates } = await this.listChatAgentTemplates(provider);
125
+ const template = templates.find((c) => String(c.id) === templateId);
126
+ if (!template)
127
+ throw new Error(`${provider} template "${templateId}" was not found for this account.`);
128
+ return { template };
129
+ }
130
+ listChatAgents() { return this.request('GET', '/cli/chat-agents'); }
131
+ getChatAgent(agentId, version) {
132
+ const query = version ? `?version=${encodeURIComponent(version)}` : '';
133
+ return this.request('GET', `/cli/chat-agents/${agentId}${query}`);
134
+ }
135
+ createChatAgent(payload) { return this.request('POST', '/cli/chat-agents', payload); }
136
+ updateChatAgent(agentId, version, payload) {
137
+ const query = version ? `?version=${encodeURIComponent(version)}` : '';
138
+ return this.request('PUT', `/cli/chat-agents/${agentId}${query}`, payload);
139
+ }
140
+ listChatAgentVersions(agentId) { return this.request('GET', `/cli/chat-agents/${agentId}/versions`); }
141
+ createChatAgentVersion(agentId, payload) {
142
+ return this.request('POST', `/cli/chat-agents/${agentId}/versions`, payload);
143
+ }
144
+ publishChatAgentVersion(agentId, versionId) {
145
+ return this.request('POST', `/cli/chat-agents/${agentId}/publish`, { versionId });
146
+ }
147
+ // --- phone numbers --------------------------------------------------------
148
+ listNumbers() { return this.request('GET', '/cli/numbers'); }
149
+ numbersCatalog() { return this.request('GET', '/cli/numbers/catalog'); }
150
+ buyNumber(phoneNumberId, pricepernumber) {
151
+ return this.request('POST', '/cli/numbers/buy', { phoneNumberId, pricepernumber });
152
+ }
153
+ setSpam(phoneNumberId, spam) {
154
+ return this.request('PUT', `/cli/numbers/${phoneNumberId}/spam`, { spam });
155
+ }
156
+ releaseNumber(phoneNumberId) { return this.request('DELETE', `/cli/numbers/${phoneNumberId}`); }
157
+ // --- calls ----------------------------------------------------------------
158
+ listCalls(query = {}) { return this.request('GET', `/cli/calls${qs(query)}`); }
159
+ getCall(callId) { return this.request('GET', `/cli/calls/${callId}`); }
160
+ /** Place a single outbound call (real or test) — uses the call-cloud base URL. */
161
+ makeCall(body) { return this.requestTo(this.callBaseUrl, 'POST', '/cli/calls/make', body); }
162
+ /** Export call history. Returns raw bytes (CSV/xlsx), or JSON on the email/too-large path. */
163
+ async exportCalls(query = {}) {
164
+ const headers = {};
165
+ if (this.token)
166
+ headers.Authorization = `Bearer ${this.token}`;
167
+ const res = await fetch(`${this.baseUrl}/cli/calls/export${qs(query)}`, { headers });
168
+ const contentType = res.headers.get('content-type') || '';
169
+ if (contentType.includes('application/json')) {
170
+ const json = await res.text().then((t) => (t ? JSON.parse(t) : {}));
171
+ return { ok: res.ok, status: res.status, json, buffer: null };
172
+ }
173
+ const buffer = Buffer.from(await res.arrayBuffer());
174
+ return { ok: res.ok, status: res.status, json: null, buffer };
175
+ }
176
+ // --- ongoing calls --------------------------------------------------------
177
+ ongoingStatus() { return this.request('GET', '/cli/ongoing/status'); }
178
+ // `action` is a literal supplied by the caller (pause|resume|clear), never user
179
+ // input; the agent id travels in the body, so neither reaches the path unescaped.
180
+ ongoingAgent(action, agentId) {
181
+ return this.request('POST', `/cli/ongoing/agent/${action}`, { agent_id: agentId });
182
+ }
183
+ ongoingUser(action) { return this.request('POST', `/cli/ongoing/${action}`); }
184
+ // --- analytics ------------------------------------------------------------
185
+ analytics(query = {}) { return this.request('GET', `/cli/analytics${qs(query)}`); }
186
+ performance(query = {}) {
187
+ return this.request('GET', `/cli/analytics/performance${qs(query)}`);
188
+ }
189
+ versionAnalytics(agentId, query = {}) {
190
+ return this.request('GET', `/cli/analytics/performance/${agentId}${qs(query)}`);
191
+ }
192
+ // --- batch calls ----------------------------------------------------------
193
+ listBatches(query = {}) { return this.request('GET', `/cli/batches${qs(query)}`); }
194
+ getBatch(batchId) { return this.request('GET', `/cli/batches/${batchId}`); }
195
+ batchStatus(batchId) { return this.request('GET', `/cli/batches/${batchId}/status`); }
196
+ /** Create a batch from a local CSV (multipart upload → Azure → Python trigger). */
197
+ createBatch(filePath, fields) {
198
+ return this.uploadCsv(`${this.baseUrl}/cli/batches/create`, filePath, fields);
199
+ }
200
+ /** Schedule a batch from a local CSV for a future time (multipart upload). */
201
+ scheduleBatch(filePath, fields) {
202
+ return this.uploadCsv(`${this.baseUrl}/cli/batches/schedule`, filePath, fields);
203
+ }
204
+ sendNextTry(batchId, maxTryCount) {
205
+ return this.request('POST', `/cli/batches/${batchId}/send-next-try`, { max_try_count: maxTryCount });
206
+ }
207
+ sendUntriggered(batchId) { return this.request('POST', `/cli/batches/${batchId}/send-untriggered`, {}); }
208
+ batchDownload(batchId, kind) { return this.request('GET', `/cli/batches/${batchId}/${kind}`); }
209
+ async uploadCsv(url, filePath, fields) {
210
+ const form = new FormData();
211
+ form.append('file', new Blob([(0, fs_1.readFileSync)(filePath)], { type: 'text/csv' }), (0, path_1.basename)(filePath));
212
+ for (const [k, v] of Object.entries(fields))
213
+ if (v !== undefined && v !== '')
214
+ form.append(k, v);
215
+ const headers = {};
216
+ // Let fetch set the multipart boundary — do not set Content-Type here.
217
+ if (this.token)
218
+ headers.Authorization = `Bearer ${this.token}`;
219
+ const res = await fetch(url, { method: 'POST', headers, body: form });
220
+ const text = await res.text();
221
+ const data = text ? safeJson(text) : {};
222
+ if (!res.ok)
223
+ throw new types_1.ApiError(res.status, data?.msg || `Request failed (${res.status})`);
224
+ return data;
225
+ }
226
+ // --- voices ---------------------------------------------------------------
227
+ /** Providers, models, fields, and controls available to the owning account. */
228
+ getVoiceCatalog() { return this.request('GET', '/cli/voices/catalog'); }
229
+ /** Voices for one account-authorized provider. */
230
+ getProviderVoices(provider, params = {}) {
231
+ return this.request('GET', `/cli/voices/provider/${encodeURIComponent(provider)}${qs(params)}`);
232
+ }
233
+ // --- simulations ----------------------------------------------------------
234
+ createTestCase(body) { return this.request('POST', '/cli/simulations/test-cases', body); }
235
+ listTestCases(agentId) {
236
+ return this.request('GET', `/cli/simulations/test-cases${qs({ agentId })}`);
237
+ }
238
+ updateTestCase(id, body) { return this.request('PUT', `/cli/simulations/test-cases/${id}`, body); }
239
+ deleteTestCase(id) { return this.request('DELETE', `/cli/simulations/test-cases/${id}`); }
240
+ runSimulation(body) { return this.request('POST', '/cli/simulations/runs', body); }
241
+ listSimulationRuns(agentId) { return this.request('GET', `/cli/simulations/runs${qs({ agentId })}`); }
242
+ simulationResults(batchId) { return this.request('GET', `/cli/simulations/results${qs({ batchId })}`); }
243
+ // --- audit strategies (AI Auditor) -----------------------------------------
244
+ listAuditStrategies(agentId) {
245
+ return this.request('GET', `/cli/audit-strategies${qs({ agentId })}`);
246
+ }
247
+ getAuditFilterOptions(agentId) {
248
+ return this.request('GET', `/cli/audit-strategies/filter-options${qs({ agentId })}`);
249
+ }
250
+ listAuditModels() { return this.request('GET', '/cli/audit-strategies/models'); }
251
+ createAuditStrategy(body) { return this.request('POST', '/cli/audit-strategies', body); }
252
+ updateAuditStrategy(strategyId, body) {
253
+ return this.request('PUT', `/cli/audit-strategies/${strategyId}`, body);
254
+ }
255
+ }
256
+ exports.Api = Api;
257
+ /** Build a `?a=1&b=2` query string, skipping undefined/empty values. */
258
+ function qs(params) {
259
+ const parts = Object.entries(params)
260
+ .filter(([, v]) => v !== undefined && v !== '')
261
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
262
+ return parts.length ? `?${parts.join('&')}` : '';
263
+ }
264
+ function safeJson(text) {
265
+ try {
266
+ return JSON.parse(text);
267
+ }
268
+ catch {
269
+ return { msg: text };
270
+ }
271
+ }
272
+ function parseErrorDetails(value) {
273
+ if (!Array.isArray(value))
274
+ return [];
275
+ return value.flatMap((detail) => {
276
+ if (!detail || typeof detail !== 'object')
277
+ return [];
278
+ const candidate = detail;
279
+ if (typeof candidate.message !== 'string')
280
+ return [];
281
+ return [{
282
+ message: candidate.message,
283
+ path: typeof candidate.path === 'string' ? candidate.path : undefined,
284
+ code: typeof candidate.code === 'string' ? candidate.code : undefined,
285
+ }];
286
+ });
287
+ }