callkaro-cli-unofficial 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -176,6 +176,10 @@ npm run catalog:check # fail if catalog.json disagrees with the snapshot
176
176
 
177
177
  ### Smaller things
178
178
 
179
+ - `cku agents chat` — text chat with an agent's real prompt and tools, no phone call. Scriptable with `--say`, replayable with `--history`. Custom in-call functions run for real
180
+ - `cku scheduled list|queued|get` — scheduled calls and the queue of what will actually dial (dashboard login)
181
+ - `cku sim update` — edit a test case in place
182
+ - `cku calls list --postcall dnc_requested=true --provider-type plivo --sid <sid>` — filters the official CLI doesn't expose
179
183
  - `cku calls logs` — grep/awk/sed over the raw call log, in-process, no subprocess and no shell
180
184
  - `cku accounts status` — reports which account is in effect **and where that choice came from**
181
185
  - Per-project binding, so parallel agents can't redirect each other
@@ -219,7 +223,7 @@ Single test file:
219
223
  node --test tests/agentUpdate.test.js
220
224
  ```
221
225
 
222
- 210 tests. No linter or formatter is configured.
226
+ 269 tests. No linter or formatter is configured.
223
227
 
224
228
  ### Layout
225
229
 
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.agentChatCommand = agentChatCommand;
7
+ const fs_1 = require("fs");
8
+ const prompts_1 = __importDefault(require("prompts"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const session_1 = require("../lib/session");
11
+ const ui_1 = require("../lib/ui");
12
+ const versions_1 = require("../lib/versions");
13
+ const types_1 = require("../lib/types");
14
+ const jsonInput_1 = require("./jsonInput");
15
+ /**
16
+ * Text-only conversation with an agent over POST /cli/simulations/generate-reply.
17
+ * One request per user turn: the server replays the whole history through the
18
+ * agent's real prompt and tools and returns the next agent turn. No call, no
19
+ * telephony spend.
20
+ *
21
+ * Tools: the Python side mocks end_call, transfer_call, warm_transfer,
22
+ * keep_call_on_hold, change_conversation_language, send_booking_link_to_whatsapp,
23
+ * use_knowledge_base and collect_digits_via_dtmf. Every other function RUNS FOR
24
+ * REAL (and falls back to a mock if it throws) — a CRM write in an in-call
25
+ * function really happens.
26
+ */
27
+ const MOCKED_TOOLS = new Set([
28
+ 'end_call', 'transfer_call', 'warm_transfer', 'keep_call_on_hold',
29
+ 'change_conversation_language', 'send_booking_link_to_whatsapp',
30
+ 'use_knowledge_base', 'collect_digits_via_dtmf',
31
+ ]);
32
+ /** Agent text of a message item; the server returns `content` as an array of strings. */
33
+ const text = (item) => Array.isArray(item.content) ? item.content.join(' ') : String(item.content ?? '');
34
+ const isEnd = (items) => items.some((i) => i.type === 'function_call' && i.name === 'end_call');
35
+ /** Turn the server's error for a bad version or history into one that names the fix. */
36
+ function explain(err, versionId) {
37
+ if (!(err instanceof types_1.ApiError))
38
+ return err;
39
+ if (err.status === 502 && /Invalid version id|Agent version not found/i.test(err.message)) {
40
+ return new Error(`Version ${versionId} is not a version of this agent. List them with: cku agents versions <agentId>`);
41
+ }
42
+ if (err.status === 400 && /must end with a user message/i.test(err.message)) {
43
+ return new Error('The chat history must end with a user turn. Add one with --say "<text>".');
44
+ }
45
+ return err;
46
+ }
47
+ function printItems(items, warned) {
48
+ for (const item of items) {
49
+ if (item.type === 'message' && item.role === 'assistant') {
50
+ (0, ui_1.info)(`${chalk_1.default.cyan('agent>')} ${text(item)}`);
51
+ }
52
+ else if (item.type === 'function_call') {
53
+ const result = JSON.stringify(item.extra?.result ?? null);
54
+ (0, ui_1.info)(chalk_1.default.dim(` → ${item.name} ${item.arguments || '{}'} = ${result}`));
55
+ if (!MOCKED_TOOLS.has(item.name) && !warned.has(item.name)) {
56
+ warned.add(item.name);
57
+ console.error(chalk_1.default.yellow(` ! ${item.name} is not mocked: it ran for real.`));
58
+ }
59
+ }
60
+ }
61
+ }
62
+ async function buildMetadata(opts) {
63
+ const metadata = {};
64
+ if (opts.metadata) {
65
+ const parsed = await (0, jsonInput_1.readJsonFlag)(opts.metadata, '--metadata');
66
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
67
+ throw new Error('--metadata must be a JSON object, e.g. {"customer_name":"Ravi"}');
68
+ }
69
+ Object.assign(metadata, parsed);
70
+ }
71
+ for (const pair of opts.var || []) {
72
+ const idx = pair.indexOf('=');
73
+ if (idx < 1)
74
+ throw new Error(`Bad --var "${pair}". Use key=value, e.g. --var customer_name=Ravi`);
75
+ metadata[pair.slice(0, idx)] = pair.slice(idx + 1);
76
+ }
77
+ return metadata;
78
+ }
79
+ async function agentChatCommand(agentId, opts) {
80
+ const api = (0, session_1.requireApi)();
81
+ try {
82
+ const versionId = (0, versions_1.firstVersion)(opts.versions);
83
+ const metadata = await buildMetadata(opts);
84
+ let history = [];
85
+ if (opts.history) {
86
+ const parsed = await (0, jsonInput_1.readJsonFlag)(opts.history, '--history');
87
+ if (!Array.isArray(parsed))
88
+ throw new Error('--history must be a JSON array of chat items (the output of --json).');
89
+ history = parsed;
90
+ }
91
+ const scripted = (opts.say?.length ?? 0) > 0 || Boolean(opts.history);
92
+ if (!scripted)
93
+ (0, ui_1.needTty)('Pass --say "<text>" (repeatable) or --history @file.json to run without a terminal.');
94
+ // Python requires userId in the body; take it from the token's own account.
95
+ const me = await api.me();
96
+ const userId = me.user?.id;
97
+ if (!userId)
98
+ throw new Error('Could not read your account id from /cli/auth/me.');
99
+ const warned = new Set();
100
+ let promptShown = false;
101
+ let ended = false;
102
+ const turn = async () => {
103
+ const res = await api
104
+ .generateReply({ userId, agent_id: agentId, chat_history: history, configuration: {}, epoch: 1, metadata }, versionId)
105
+ .catch((e) => { throw explain(e, versionId); });
106
+ if (opts.showPrompt && !promptShown) {
107
+ promptShown = true;
108
+ const sys = (res.messages || []).find((m) => m.role === 'system');
109
+ if (sys)
110
+ (0, ui_1.info)(chalk_1.default.dim(`--- system prompt ---\n${sys.content}\n---------------------`));
111
+ }
112
+ const items = res.replies?.[0] || [];
113
+ history.push(...items);
114
+ if (!opts.json)
115
+ printItems(items, warned);
116
+ ended = isEnd(items);
117
+ };
118
+ const userSays = async (line) => {
119
+ history.push({ type: 'message', role: 'user', content: line });
120
+ if (!opts.json)
121
+ (0, ui_1.info)(`${chalk_1.default.green('you>')} ${line}`);
122
+ await turn();
123
+ };
124
+ // A replayed history may already end on a user turn waiting for its reply.
125
+ if (history.length && history[history.length - 1].role === 'user')
126
+ await turn();
127
+ if (scripted) {
128
+ for (const line of opts.say || []) {
129
+ if (ended)
130
+ break;
131
+ await userSays(line);
132
+ }
133
+ }
134
+ else {
135
+ (0, ui_1.info)(chalk_1.default.dim('Text chat with the agent. /reset, /save <file>, /exit. Ends after end_call.'));
136
+ while (!ended) {
137
+ const { line } = await (0, prompts_1.default)({ type: 'text', name: 'line', message: 'you' });
138
+ if (line === undefined || line === '/exit')
139
+ break;
140
+ const cmd = String(line).trim();
141
+ if (!cmd)
142
+ continue;
143
+ if (cmd === '/reset') {
144
+ history = [];
145
+ (0, ui_1.info)(chalk_1.default.dim('History cleared.'));
146
+ continue;
147
+ }
148
+ if (cmd.startsWith('/save ')) {
149
+ (0, fs_1.writeFileSync)(cmd.slice(6).trim(), JSON.stringify(history, null, 2));
150
+ (0, ui_1.info)(chalk_1.default.dim(`Saved ${history.length} items.`));
151
+ continue;
152
+ }
153
+ await userSays(cmd);
154
+ }
155
+ }
156
+ if (ended && !opts.json)
157
+ (0, ui_1.info)(chalk_1.default.dim('(agent ended the call)'));
158
+ if (opts.save)
159
+ (0, fs_1.writeFileSync)(opts.save, JSON.stringify(history, null, 2));
160
+ if (opts.json)
161
+ console.log(JSON.stringify(history, null, 2));
162
+ }
163
+ catch (err) {
164
+ (0, ui_1.handleError)(err);
165
+ }
166
+ }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.postcallQuery = postcallQuery;
3
4
  exports.buildCallQuery = buildCallQuery;
4
5
  exports.callsListCommand = callsListCommand;
5
6
  exports.callsGetCommand = callsGetCommand;
@@ -17,6 +18,24 @@ function fmtTime(v) {
17
18
  const d = new Date(v);
18
19
  return isNaN(d.getTime()) ? '' : d.toLocaleString('en-US');
19
20
  }
21
+ /**
22
+ * `--postcall name=true|false` → `postcall_name=true|false`. The server only matches
23
+ * the literal strings `true`/`false` and silently drops any other value, which would
24
+ * return every call — so anything else is rejected here.
25
+ */
26
+ function postcallQuery(pairs = []) {
27
+ const out = {};
28
+ for (const pair of pairs) {
29
+ const m = /^([^=\s]+)=(true|false)$/.exec(pair.trim());
30
+ if (!m) {
31
+ throw new Error(`Invalid --postcall "${pair}". Use --postcall <variable>=true or <variable>=false ` +
32
+ `(boolean post-call variables only; the server ignores any other value). ` +
33
+ `Example: --postcall dnc_requested=true`);
34
+ }
35
+ out[`postcall_${m[1]}`] = m[2];
36
+ }
37
+ return out;
38
+ }
20
39
  function buildCallQuery(o) {
21
40
  return {
22
41
  name: o.type,
@@ -31,6 +50,9 @@ function buildCallQuery(o) {
31
50
  conversion_status: o.converted,
32
51
  lead_id: o.lead,
33
52
  buylead: o.buylead,
53
+ provider_type: o.providerType,
54
+ call_sid: o.sid,
55
+ ...postcallQuery(o.postcall),
34
56
  };
35
57
  }
36
58
  async function callsListCommand(opts) {
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scheduledListCommand = scheduledListCommand;
4
+ exports.scheduledQueuedCommand = scheduledQueuedCommand;
5
+ exports.scheduledGetCommand = scheduledGetCommand;
6
+ const dashboardApi_1 = require("../lib/dashboardApi");
7
+ const ui_1 = require("../lib/ui");
8
+ /**
9
+ * Scheduled calls, read-only. `/schedulecalls/v1/*` takes the dashboard cookie,
10
+ * so these need `cku dashboard login`; the `/cli` tree has no scheduled-call
11
+ * routes and the `/mcp` ones reject the ck_ token.
12
+ *
13
+ * No cancel: the backend's single-call delete removes the job and books a new
14
+ * attempt instead, and neither delete checks ownership of the job id.
15
+ */
16
+ const when = (v) => {
17
+ if (v === undefined || v === null || v === '')
18
+ return '';
19
+ const d = new Date(v);
20
+ return isNaN(d.getTime()) ? String(v) : d.toLocaleString('en-US');
21
+ };
22
+ const agentName = (a) => (a && typeof a === 'object' ? a.name || a._id : a) || '';
23
+ async function scheduledListCommand(opts) {
24
+ try {
25
+ const page = (0, ui_1.intFlag)(opts.page, '--page', 1);
26
+ const pageSize = (0, ui_1.intFlag)(opts.limit, '--limit', 50);
27
+ const res = await (0, dashboardApi_1.dashboardApi)().scheduledCalls({ page, pageSize });
28
+ const rows = res?.scheduleCalls || [];
29
+ if (opts.json)
30
+ return void console.log(JSON.stringify(rows, null, 2));
31
+ if (!rows.length)
32
+ return void (0, ui_1.info)('No scheduled calls.');
33
+ (0, ui_1.table)(['ID', 'TO', 'AGENT', 'SCHEDULED FOR', 'RETRIES', 'BATCH'], rows.map((r) => [r._id, r.to_number, agentName(r.agent_id), r.schedule_at, r.number_of_retries ?? '', r.batch_id || '']));
34
+ const p = res.pagination || {};
35
+ (0, ui_1.info)(`Page ${p.page ?? page} of ${p.totalPages ?? '?'} (${p.totalDocs ?? rows.length} total). Created today: ${res.todaysCalls ?? 0}.`);
36
+ if (p.hasNext)
37
+ (0, ui_1.info)(`Next: cku scheduled list --page ${(p.page ?? page) + 1}`);
38
+ }
39
+ catch (err) {
40
+ (0, ui_1.handleError)(err);
41
+ }
42
+ }
43
+ async function scheduledQueuedCommand(opts) {
44
+ try {
45
+ const res = await (0, dashboardApi_1.dashboardApi)().queuedScheduledCalls();
46
+ const jobs = res?.scheduleCalls || [];
47
+ if (opts.json)
48
+ return void console.log(JSON.stringify(jobs, null, 2));
49
+ if (!jobs.length)
50
+ return void (0, ui_1.info)('Nothing queued: no scheduled call is waiting to dial.');
51
+ // The queue returns jobs in no useful order; soonest first.
52
+ const due = (j) => (j.timestamp || 0) + (j.delay || 0);
53
+ jobs.sort((a, b) => due(a) - due(b));
54
+ (0, ui_1.table)(['JOB ID', 'TO', 'AGENT ID', 'DIALS AT', 'TRY', 'FOLLOW-UP'], jobs.map((j) => [
55
+ j.id, j.data?.to_number, j.data?.agent_id, when(due(j)),
56
+ j.data?.try_count ?? '', j.data?.follow_up_call ? 'yes' : '',
57
+ ]));
58
+ (0, ui_1.info)(`${jobs.length} queued.`);
59
+ }
60
+ catch (err) {
61
+ (0, ui_1.handleError)(err);
62
+ }
63
+ }
64
+ async function scheduledGetCommand(id, opts) {
65
+ try {
66
+ const res = await (0, dashboardApi_1.dashboardApi)().scheduledCall(id);
67
+ if (opts.json)
68
+ return void console.log(JSON.stringify(res, null, 2));
69
+ const s = res.scheduleCall || {};
70
+ (0, ui_1.keyValue)({
71
+ ID: s._id,
72
+ To: s.to_number,
73
+ Agent: agentName(s.agent_id),
74
+ 'Scheduled for': s.schedule_at,
75
+ Language: s.call_language,
76
+ Retries: s.number_of_retries,
77
+ 'Retry gaps (min)': (s.gap_between_retries || []).join(', '),
78
+ Window: s.min_trigger_time && `${s.min_trigger_time}–${s.max_trigger_time}`,
79
+ Batch: s.batch_id || '',
80
+ Metadata: s.metadata ? JSON.stringify(s.metadata) : '',
81
+ 'Still queued': (res.futureCalls || []).length ? 'yes' : 'no',
82
+ });
83
+ const done = res.completedCalls || [];
84
+ if (done.length) {
85
+ (0, ui_1.info)('');
86
+ (0, ui_1.table)(['CALL ID', 'TRY', 'HANGUP', 'DURATION', 'WHEN'], done.map((c) => [c._id, c.try_count ?? '', c.hangup_reason || '', c.call_duration ?? '', when(c.createdAt)]));
87
+ }
88
+ }
89
+ catch (err) {
90
+ (0, ui_1.handleError)(err);
91
+ }
92
+ }
@@ -8,6 +8,7 @@ exports.toTestVariables = toTestVariables;
8
8
  exports.toTestFunctions = toTestFunctions;
9
9
  exports.simTestsListCommand = simTestsListCommand;
10
10
  exports.simCreateCommand = simCreateCommand;
11
+ exports.simUpdateCommand = simUpdateCommand;
11
12
  exports.simDeleteCommand = simDeleteCommand;
12
13
  exports.waitForBatch = waitForBatch;
13
14
  exports.simRunCommand = simRunCommand;
@@ -95,6 +96,37 @@ async function simCreateCommand(agentId, opts) {
95
96
  (0, ui_1.handleError)(err);
96
97
  }
97
98
  }
99
+ /**
100
+ * Edit a test case. Sends only the fields given: the server passes the body to
101
+ * findByIdAndUpdate as-is, so anything else in it (userId, agentId) would be written.
102
+ * --variables / --functions replace the whole map / list, they do not merge.
103
+ */
104
+ async function simUpdateCommand(testCaseId, opts) {
105
+ const api = (0, session_1.requireApi)();
106
+ try {
107
+ const body = {};
108
+ if (opts.name !== undefined)
109
+ body.testcaseName = opts.name;
110
+ if (opts.prompt !== undefined)
111
+ body.userPrompt = opts.prompt;
112
+ if (opts.criteria !== undefined)
113
+ body.successCriteria = opts.criteria;
114
+ if (opts.model !== undefined)
115
+ body.testLLMModel = opts.model;
116
+ if (opts.variables !== undefined)
117
+ body.testVariables = toTestVariables(await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables'));
118
+ if (opts.functions !== undefined)
119
+ body.testFunctions = toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions'));
120
+ if (!Object.keys(body).length) {
121
+ return (0, ui_1.handleError)(new Error('Nothing to update. Pass at least one of --name, --prompt, --criteria, --model, --variables, --functions.'));
122
+ }
123
+ const res = await api.updateTestCase(testCaseId, body);
124
+ (0, ui_1.success)(`Updated test case ${res.testCase?._id || testCaseId}: ${Object.keys(body).join(', ')}.`);
125
+ }
126
+ catch (err) {
127
+ (0, ui_1.handleError)(err);
128
+ }
129
+ }
98
130
  async function simDeleteCommand(testCaseId, opts) {
99
131
  const api = (0, session_1.requireApi)();
100
132
  try {
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ const KPI_COLUMNS = 'action_items | in_progress | testing_ab | live_prod | done
16
16
  const analyzers_1 = require("./commands/analyzers");
17
17
  const models_1 = require("./commands/models");
18
18
  const agents_1 = require("./commands/agents");
19
+ const agentChat_1 = require("./commands/agentChat");
20
+ const scheduled_1 = require("./commands/scheduled");
19
21
  const agentVersions_1 = require("./commands/agentVersions");
20
22
  const agentPhone_1 = require("./commands/agentPhone");
21
23
  const whatsapp_1 = require("./commands/whatsapp");
@@ -40,6 +42,11 @@ const chatAgents_1 = require("./commands/chatAgents");
40
42
  // last silently wins. `cku` cannot collide.
41
43
  const name = 'cku';
42
44
  const { version: VERSION } = require('../package.json');
45
+ /** Collect repeatable flag values (--var, --say, --postcall) into an array. */
46
+ const collectVar = (val, acc) => {
47
+ acc.push(val);
48
+ return acc;
49
+ };
43
50
  const program = new commander_1.Command();
44
51
  program
45
52
  .name(name)
@@ -330,6 +337,27 @@ agents.command('get <agentId>').description('show one agent and list its version
330
337
  .option('--json', 'output raw JSON')
331
338
  .option('--file <path>', 'save the agent detail as JSON to a file')
332
339
  .action(agents_1.agentsGetCommand);
340
+ agents.command('chat <agentId>')
341
+ .description('text-chat with an agent, no phone call (custom in-call functions RUN FOR REAL)')
342
+ .option('--versions <id>', 'version to talk to (default: the published one)')
343
+ .option('--metadata <json|@file>', 'call metadata as a JSON object')
344
+ .option('--var <k=v>', 'call metadata variable (repeatable)', collectVar, [])
345
+ .option('--say <text>', 'user turn, repeatable, run in order; no terminal needed', collectVar, [])
346
+ .option('--history <json|@file>', 'replay a saved conversation (the output of --json) before --say')
347
+ .option('--save <file>', 'write the conversation to a file at the end (readable by --history)')
348
+ .option('--show-prompt', 'print the rendered system prompt once (checks metadata substitution)')
349
+ .option('--json', 'print only the final conversation as JSON')
350
+ .addHelpText('after', `
351
+ Not mocked: any custom in-call function. It really runs (CRM write, webhook, WhatsApp).
352
+ Mocked: end_call, transfer_call, warm_transfer, keep_call_on_hold, use_knowledge_base,
353
+ change_conversation_language, send_booking_link_to_whatsapp, collect_digits_via_dtmf.
354
+
355
+ Examples:
356
+ cku agents chat <agentId>
357
+ cku agents chat <agentId> --var customer_name=Ravi --say "Hello?" --say "Not interested"
358
+ cku agents chat <agentId> --say "hi" --json > convo.json
359
+ cku agents chat <agentId> --history @convo.json --say "What was that again?"`)
360
+ .action(agentChat_1.agentChatCommand);
333
361
  agents.command('versions <agentId>').description("list an agent's versions (name, id, language, prompt type, active)")
334
362
  .option('--json', 'output raw JSON')
335
363
  .option('--file <path>', 'save the version list as JSON to a file')
@@ -494,13 +522,11 @@ function addCallFilters(cmd) {
494
522
  .option('--to <number>', 'destination number')
495
523
  .option('--converted <bool>', 'conversion status: true|false')
496
524
  .option('--lead <id>', 'lead id')
497
- .option('--buylead <value>', 'buylead value');
525
+ .option('--buylead <value>', 'buylead value')
526
+ .option('--postcall <name=bool>', 'boolean post-call variable, repeatable: dnc_requested=true', collectVar, [])
527
+ .option('--provider-type <list>', 'telephony provider(s), comma-separated: plivo,vobiz,...')
528
+ .option('--sid <callSid>', 'provider call SID (ignores the date window)');
498
529
  }
499
- /** Collect repeatable --var key=value into an array. */
500
- const collectVar = (val, acc) => {
501
- acc.push(val);
502
- return acc;
503
- };
504
530
  // --- voices / transcribers -------------------------------------------------
505
531
  program
506
532
  .command('voices')
@@ -543,6 +569,20 @@ program
543
569
  .option('--all', 'include admin-gated providers')
544
570
  .option('--json', 'raw JSON')
545
571
  .action(transcribers_1.transcribersCommand);
572
+ // --- scheduled calls (dashboard cookie) -------------------------------------
573
+ const scheduled = program.command('scheduled')
574
+ .description('scheduled calls, read-only (needs `cku dashboard login`)');
575
+ scheduled.command('list').description('schedule records, newest first')
576
+ .option('--page <n>', 'page number (default 1)')
577
+ .option('--limit <n>', 'page size (default 50)')
578
+ .option('--json', 'raw JSON (a bare array)')
579
+ .action(scheduled_1.scheduledListCommand);
580
+ scheduled.command('queued').description('jobs waiting in the queue: what will actually dial, soonest first')
581
+ .option('--json', 'raw JSON (a bare array of queue jobs)')
582
+ .action(scheduled_1.scheduledQueuedCommand);
583
+ scheduled.command('get <id>').description('one schedule record, its completed tries, and whether it is still queued')
584
+ .option('--json', 'raw JSON')
585
+ .action(scheduled_1.scheduledGetCommand);
546
586
  // --- calls -----------------------------------------------------------------
547
587
  const calls = program.command('calls').description('calls: place & history');
548
588
  calls.command('make').description('place a single outbound call (real, or --test)')
@@ -656,6 +696,14 @@ sim.command('create <agentId>').description('create a test case')
656
696
  .option('--variables <json|@file>', 'testVariables: a JSON object of name -> value (non-strings are stringified)')
657
697
  .option('--functions <json|@file>', 'function mocks: a JSON array of {"name": ..., "response": ...}')
658
698
  .action(simulations_1.simCreateCommand);
699
+ sim.command('update <testCaseId>').description('edit a test case (only the fields you pass are sent)')
700
+ .option('--name <name>', 'test case name')
701
+ .option('--prompt <text>', 'simulated user prompt')
702
+ .option('--criteria <text>', 'success criteria')
703
+ .option('--model <model>', 'test LLM model')
704
+ .option('--variables <json|@file>', 'testVariables: replaces the whole map')
705
+ .option('--functions <json|@file>', 'function mocks: replaces the whole list')
706
+ .action(simulations_1.simUpdateCommand);
659
707
  sim.command('delete <testCaseId>').description('delete a test case').option('--yes', 'skip confirmation')
660
708
  .action(simulations_1.simDeleteCommand);
661
709
  sim.command('run <agentId>').description('run a batch simulation (async)')
package/dist/lib/api.js CHANGED
@@ -250,6 +250,11 @@ class Api {
250
250
  listTestCases(agentId) {
251
251
  return this.request('GET', `/cli/simulations/test-cases${qs({ agentId })}`);
252
252
  }
253
+ /** One agent turn from a chat history, no telephony (`agents chat`). */
254
+ generateReply(body, versionId) {
255
+ const q = versionId ? `?version=${encodeURIComponent(versionId)}` : '';
256
+ return this.request('POST', `/cli/simulations/generate-reply${q}`, body);
257
+ }
253
258
  updateTestCase(id, body) { return this.request('PUT', `/cli/simulations/test-cases/${id}`, body); }
254
259
  deleteTestCase(id) { return this.request('DELETE', `/cli/simulations/test-cases/${id}`); }
255
260
  runSimulation(body) { return this.request('POST', '/cli/simulations/runs', body); }
@@ -144,6 +144,22 @@ class DashboardApi {
144
144
  deleteTestVariableSet(id) {
145
145
  return this.request('DELETE', `/v1/call-metadata-test-variables/${encodeURIComponent(id)}`);
146
146
  }
147
+ // --- scheduled calls (read-only) -------------------------------------------
148
+ // The delete routes under /schedulecalls/v1 are deliberately not wrapped: the
149
+ // single-call one re-schedules a new attempt rather than cancelling, and neither
150
+ // checks that the job belongs to the caller. See docs/plan-unused-routes.md.
151
+ /** Schedule records, newest first. 44k rows on a busy account: always paged. */
152
+ scheduledCalls(query = {}) {
153
+ return this.request('GET', `/schedulecalls/v1/all${qs(query)}`);
154
+ }
155
+ /** Pending jobs in the queue: what will actually dial, with `data.schedule_at`. */
156
+ queuedScheduledCalls() {
157
+ return this.request('GET', '/schedulecalls/v1/queued-scheduled-calls/all');
158
+ }
159
+ /** One schedule record plus the calls it produced and any pending job. */
160
+ scheduledCall(id) {
161
+ return this.request('GET', `/schedulecalls/v1/${encodeURIComponent(id)}`);
162
+ }
147
163
  // --- KPI board writes -----------------------------------------------------
148
164
  //
149
165
  // Routes, bodies and the column set below were read out of the dashboard's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callkaro-cli-unofficial",
3
- "version": "0.3.0",
3
+ "version": "0.4.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": {