callkaro-cli-unofficial 0.9.4 → 0.10.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
@@ -200,8 +200,21 @@ npm run catalog:check # fail if catalog.json disagrees with the snapshot
200
200
 
201
201
  `catalog:check` runs offline against a committed snapshot, so it works in CI with no credentials. On 1 October 2026 it caught shipped tables that listed withdrawn models *and* missed live ones in both directions. A wrong voice model otherwise persists silently and only surfaces as a broken call.
202
202
 
203
+ ### Live test calls, with you or an LLM as the caller
204
+
205
+ ```bash
206
+ cku test start <agentId> --yes # a coding agent (or you) plays the caller, one turn per command
207
+ cku test say <sessionId> "Kitne ka hai?"
208
+ cku test end <sessionId> # transcript with function calls and node switches
209
+
210
+ cku test run <agentId> --test <testCaseId> --llm anthropic --yes # the CLI plays the caller and grades the call
211
+ ```
212
+
213
+ This is the agent editor's **Test Chat** tab from a terminal: a real test call on the real call runtime, over the same LiveKit room the dashboard joins, with the caller typing instead of speaking. Unlike a simulation, nothing is mocked, so node switching, function calls and post-call analysis are what production does, and every function runs for real. `run` uses any OpenAI-compatible LLM (OpenAI, Anthropic, Gemini, DeepSeek, Groq, OpenRouter, Ollama, or `--llm-url`), with the key read from the environment, and exits 1 when the judge fails the call. It needs `@livekit/rtc-node`, an optional dependency with prebuilt binaries for Linux, macOS and Windows.
214
+
203
215
  ### Smaller things
204
216
 
217
+ - `cku sim create --real-functions` — every function on the version runs for real in the simulation, like switching every Mock off in the dashboard
205
218
  - `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
206
219
  - `cku scheduled list|queued|get` — scheduled calls and the queue of what will actually dial (dashboard login)
207
220
  - `cku sim update` — edit a test case in place
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentsMemoryCommand = agentsMemoryCommand;
4
+ exports.agentsVoiceRotationCommand = agentsVoiceRotationCommand;
5
+ const fs_1 = require("fs");
6
+ const session_1 = require("../lib/session");
7
+ const ui_1 = require("../lib/ui");
8
+ const versions_1 = require("../lib/versions");
9
+ const looseJson_1 = require("../lib/looseJson");
10
+ const liveVoiceCheck_1 = require("../lib/liveVoiceCheck");
11
+ const agentMemory_1 = require("../lib/agentMemory");
12
+ async function run(agentId, opts, flags, change, describe, what, verify) {
13
+ const api = (0, session_1.requireApi)();
14
+ try {
15
+ const versionId = (0, versions_1.firstVersion)(opts.versions);
16
+ if (!versionId)
17
+ throw new Error('--versions <id> is required (find it with `cku agents versions <agentId>`)');
18
+ const { agent } = await api.getAgent(agentId, versionId);
19
+ const current = agent.agentMemory;
20
+ if (!(0, agentMemory_1.hasChanges)(flags)) {
21
+ if (opts.json)
22
+ console.log(JSON.stringify(current ?? {}, null, 2));
23
+ else
24
+ (0, ui_1.keyValue)(describe(current));
25
+ return;
26
+ }
27
+ const next = change(current);
28
+ if (verify)
29
+ await verify(api, next, agent);
30
+ const body = { agentMemory: next };
31
+ if (opts.commit)
32
+ body.commitMessage = opts.commit;
33
+ const result = await api.updateAgent(agentId, versionId, body);
34
+ if (opts.json)
35
+ console.log(JSON.stringify(next, null, 2));
36
+ else {
37
+ (0, ui_1.success)(result.msg || `${what} updated.`);
38
+ (0, ui_1.keyValue)(describe(next));
39
+ (0, ui_1.info)(' Takes effect on the next call. Publish the version if it is not live yet.');
40
+ }
41
+ }
42
+ catch (error) {
43
+ (0, ui_1.handleError)(error);
44
+ }
45
+ }
46
+ function agentsMemoryCommand(agentId, opts) {
47
+ const { versions, commit, json, ...flags } = opts;
48
+ return run(agentId, opts, flags, (memory) => (0, agentMemory_1.applyMemoryFlags)(memory, flags), agentMemory_1.describeMemory, 'Call memory');
49
+ }
50
+ function agentsVoiceRotationCommand(agentId, opts) {
51
+ const { versions, commit, json, voices, ...rest } = opts;
52
+ const flags = rest;
53
+ if (voices !== undefined) {
54
+ const file = String(voices).startsWith('@') ? String(voices).slice(1) : String(voices);
55
+ if (!(0, fs_1.existsSync)(file)) {
56
+ (0, ui_1.handleError)(new Error(`File not found: ${file}`));
57
+ return Promise.resolve();
58
+ }
59
+ try {
60
+ flags.voices = JSON.parse((0, looseJson_1.readJsonFile)(file));
61
+ }
62
+ catch (error) {
63
+ (0, ui_1.handleError)(new Error(`--voices is not valid JSON: ${error.message}`));
64
+ return Promise.resolve();
65
+ }
66
+ }
67
+ const verify = flags.voices === undefined ? undefined : async (api, memory, version) => {
68
+ const entries = Object.fromEntries((memory.voice_rotation_config?.voices || []).map((voice, index) => [`voices[${index}]`, voice]));
69
+ await (0, liveVoiceCheck_1.checkVoiceConfigs)(api, entries, async () => (0, liveVoiceCheck_1.savedVoices)(version));
70
+ };
71
+ return run(agentId, opts, flags, (memory) => (0, agentMemory_1.applyRotationFlags)(memory, flags), agentMemory_1.describeRotation, 'Voice rotation', verify);
72
+ }
@@ -19,6 +19,7 @@ const dashboardApi_1 = require("../lib/dashboardApi");
19
19
  const ui_1 = require("../lib/ui");
20
20
  const versions_1 = require("../lib/versions");
21
21
  const agentUpdate_1 = require("../lib/agentUpdate");
22
+ const liveVoiceCheck_1 = require("../lib/liveVoiceCheck");
22
23
  const setJson_1 = require("./setJson");
23
24
  /** Keys a clone may override. Anything else is rejected before the request. */
24
25
  const CLONE_OVERRIDE_KEYS = new Set([
@@ -60,7 +61,8 @@ async function agentsCloneVersionCommand(agentId, opts) {
60
61
  if (unsupported.length) {
61
62
  return (0, ui_1.handleError)(new Error(`Unsupported clone override(s): ${unsupported.join(', ')}\n allowed: ${[...CLONE_OVERRIDE_KEYS].join(', ')}`));
62
63
  }
63
- overrides = (0, agentUpdate_1.preparePatch)(parsed).patch;
64
+ overrides = (0, agentUpdate_1.preparePatch)(parsed, { skipVoices: true }).patch;
65
+ await (0, liveVoiceCheck_1.checkVoiceConfigs)(api, { voice_configuration: overrides.voice_configuration, secondary_voice_configuration: overrides.secondary_voice_configuration }, sourceVersionId ? async () => (0, liveVoiceCheck_1.savedVoices)((await api.getAgent(agentId, sourceVersionId)).agent) : undefined);
64
66
  }
65
67
  const res = await api.cloneAgentVersion(agentId, sourceVersionId, {
66
68
  ...overrides,
@@ -53,6 +53,7 @@ const ui_1 = require("../lib/ui");
53
53
  const versions_1 = require("../lib/versions");
54
54
  const agentUpdate_1 = require("../lib/agentUpdate");
55
55
  Object.defineProperty(exports, "preparePatch", { enumerable: true, get: function () { return agentUpdate_1.preparePatch; } });
56
+ const liveVoiceCheck_1 = require("../lib/liveVoiceCheck");
56
57
  const dashboardAuth_1 = require("../lib/dashboardAuth");
57
58
  const agentImport_1 = require("../lib/agentImport");
58
59
  const dbFields_1 = require("../lib/dbFields");
@@ -400,7 +401,7 @@ async function agentsUpdateCommand(agentId, opts) {
400
401
  const dbf = (0, dbFields_1.dbManagedFields)(obj);
401
402
  if (dbf.length)
402
403
  throw new Error((0, dbFields_1.dbManagedFieldsError)(dbf, '--set object'));
403
- const prepared = (0, agentUpdate_1.preparePatch)(obj); // throws on invalid enum/model values
404
+ const prepared = (0, agentUpdate_1.preparePatch)(obj, { skipVoices: true });
404
405
  patch = prepared.patch;
405
406
  versionKeys = prepared.versionKeys;
406
407
  wholesaleKeys = prepared.wholesaleKeys;
@@ -421,37 +422,60 @@ async function agentsUpdateCommand(agentId, opts) {
421
422
  ' Run the command once per version id.'));
422
423
  }
423
424
  try {
424
- // The backend replaces these objects wholesale, so a partial --set would drop
425
- // every sibling key. Merge against the agent's current value first. This is
426
- // a no-op when the object sent is already complete. Merge against the
427
- // *target* version: without one, getAgent returns the default version, and
428
- // its siblings (provider, voice id, …) would be written onto a different
429
- // version. Wholesale keys are version-level, so --versions is set here.
430
- // `functions` is a whole array on the backend: merge by name unless the
431
- // caller asked to replace the list.
425
+ const vid = (0, versions_1.firstVersion)(opts.versions);
426
+ await (0, liveVoiceCheck_1.checkVoiceConfigs)(api, { voice_configuration: patch.voice_configuration, secondary_voice_configuration: patch.secondary_voice_configuration }, vid ? async () => (0, liveVoiceCheck_1.savedVoices)((await api.getAgent(agentId, vid)).agent) : undefined);
427
+ let versionTarget = vid;
428
+ if (opts.commit && !versionTarget)
429
+ versionTarget = (await api.getAgent(agentId)).agent.versionId;
432
430
  const mergeFns = Array.isArray(patch.functions) && !opts.replaceFunctions;
433
- if (wholesaleKeys.length || mergeFns) {
434
- const { agent } = await api.getAgent(agentId, (0, versions_1.firstVersion)(opts.versions));
435
- patch = (0, agentUpdate_1.mergeWholesaleKeys)(patch, agent);
431
+ let current;
432
+ try {
433
+ current = (await api.getAgent(agentId, versionTarget)).agent;
434
+ }
435
+ catch (error) {
436
+ if (mergeFns)
437
+ throw error;
438
+ (0, ui_1.info)(' (could not read the current values; sending the patch as given)');
439
+ }
440
+ let effectivePatch = patch;
441
+ if (current) {
442
+ if (wholesaleKeys.length)
443
+ effectivePatch = (0, agentUpdate_1.mergeWholesaleKeys)(effectivePatch, current);
436
444
  if (mergeFns)
437
- patch.functions = (0, agentUpdate_1.mergeFunctions)(patch.functions, agent.functions);
445
+ effectivePatch.functions = (0, agentUpdate_1.mergeFunctions)(effectivePatch.functions, current.functions);
446
+ }
447
+ let { send, changed, unchanged } = current
448
+ ? (0, agentUpdate_1.planUpdate)(current, effectivePatch)
449
+ : { send: effectivePatch, changed: Object.keys(effectivePatch), unchanged: [] };
450
+ if (opts.dryRun) {
451
+ if (opts.json)
452
+ console.log(JSON.stringify({ dryRun: true, changing: changed, unchanged }, null, 2));
453
+ else {
454
+ (0, ui_1.info)(` would change: ${changed.join(', ') || '(nothing)'}`);
455
+ if (unchanged.length)
456
+ (0, ui_1.info)(` already set, skipped: ${unchanged.join(', ')}`);
457
+ (0, ui_1.info)(' Nothing saved (--dry-run).');
458
+ }
459
+ return;
460
+ }
461
+ if (!changed.length) {
462
+ (0, ui_1.info)(`Nothing to change: ${unchanged.join(', ')} already hold the requested value(s).`);
463
+ return;
438
464
  }
439
- // A commit message is recorded against a version. Version-level updates
440
- // already name one; for an agent-level-only update with --commit, attach it
441
- // to the agent's current version so the change (e.g. a rename) is logged.
442
- let versionTarget = (0, versions_1.firstVersion)(opts.versions);
443
- if (opts.commit && !versionTarget) {
444
- const { agent } = await api.getAgent(agentId);
445
- versionTarget = agent.versionId;
465
+ if (changed.length > agentUpdate_1.MAX_FIELDS_WITHOUT_YES && !opts.yes) {
466
+ return (0, ui_1.handleError)(new Error(`This would change ${changed.length} fields (${changed.join(', ')}).\n` +
467
+ ' Send only the fields you mean to change. If this is intended, re-run with --yes (or preview with --dry-run).'));
446
468
  }
447
- const body = { ...patch };
469
+ const body = { ...send };
448
470
  if (opts.commit)
449
471
  body.commitMessage = opts.commit;
450
472
  const res = await api.updateAgent(agentId, versionTarget, body);
451
473
  if (opts.json)
452
474
  return void console.log(JSON.stringify(res, null, 2));
453
475
  (0, ui_1.success)(res.msg || 'Agent updated.');
454
- (0, ui_1.info)(` updated: ${Object.keys(patch).join(', ')}`);
476
+ (0, ui_1.info)(` updated: ${changed.join(', ')}`);
477
+ if (unchanged.length)
478
+ (0, ui_1.info)(` already set, skipped: ${unchanged.join(', ')}`);
455
479
  if (opts.commit)
456
480
  (0, ui_1.info)(` commit: ${opts.commit}`);
457
481
  }
@@ -0,0 +1,330 @@
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.testStartCommand = testStartCommand;
7
+ exports.testSayCommand = testSayCommand;
8
+ exports.testEndCommand = testEndCommand;
9
+ exports.testServeCommand = testServeCommand;
10
+ exports.testRunCommand = testRunCommand;
11
+ const child_process_1 = require("child_process");
12
+ const crypto_1 = require("crypto");
13
+ const http_1 = require("http");
14
+ const fs_1 = require("fs");
15
+ const path_1 = require("path");
16
+ const chalk_1 = __importDefault(require("chalk"));
17
+ const session_1 = require("../lib/session");
18
+ const config_1 = require("../lib/config");
19
+ const ui_1 = require("../lib/ui");
20
+ const versions_1 = require("../lib/versions");
21
+ const liveTest_1 = require("../lib/liveTest");
22
+ const jsonInput_1 = require("./jsonInput");
23
+ /**
24
+ * `cku test`: live test calls over the dashboard's Test Chat path (lib/liveTest.ts).
25
+ *
26
+ * Two ways to play the caller:
27
+ * - start / say / end: a coding agent (or a person) plays the caller one turn per command.
28
+ * A detached background process holds the LiveKit room between commands and serves them on
29
+ * 127.0.0.1, guarded by a per-session secret.
30
+ * - run: the CLI plays the caller with an LLM and grades the call against criteria.
31
+ */
32
+ const SESSIONS_DIR = (0, path_1.join)(config_1.CONFIG_DIR, 'test-sessions');
33
+ const IDLE_MS = 10 * 60_000;
34
+ const WARNING = 'This places a real test call: every agent function runs for real (CRM writes, WhatsApp, webhooks), '
35
+ + 'credits are used, and the call appears in call history with post-call analysis.';
36
+ const sessionPath = (id) => (0, path_1.join)(SESSIONS_DIR, `${id}.json`);
37
+ function printEvents(events) {
38
+ for (const e of events) {
39
+ if (e.type === 'agent')
40
+ (0, ui_1.info)(`${chalk_1.default.cyan('agent>')} ${e.text}`);
41
+ else if (e.type === 'user')
42
+ (0, ui_1.info)(`${chalk_1.default.green('caller>')} ${e.text}`);
43
+ else if (e.type === 'function')
44
+ (0, ui_1.info)(chalk_1.default.dim(` → ${e.name} ${JSON.stringify(e.arguments ?? {})}`));
45
+ else if (e.type === 'capability')
46
+ (0, ui_1.info)(chalk_1.default.dim(` [node: ${e.capability}]`));
47
+ else if (e.type === 'ended')
48
+ (0, ui_1.info)(chalk_1.default.dim(' (call ended)'));
49
+ }
50
+ }
51
+ async function readMetadata(opts) {
52
+ if (!opts.variables)
53
+ return {};
54
+ const v = await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables');
55
+ if (!v || typeof v !== 'object' || Array.isArray(v))
56
+ throw new Error('--variables must be a JSON object, e.g. {"customer_name":"Ravi"}');
57
+ return v;
58
+ }
59
+ /** The test call is created under the agent's owner, which is also what the dashboard sends. */
60
+ async function startToken(agentId, version, metadata) {
61
+ const res = await (0, session_1.requireApi)().getAgent(agentId, version);
62
+ const agent = res.agent || res;
63
+ const userId = agent?.userId;
64
+ if (!userId)
65
+ throw new Error(`Agent ${agentId} came back without a userId, so the test-call host cannot be chosen.`);
66
+ return (0, liveTest_1.requestTestToken)({ userId: String(userId), agentId, version, metadata });
67
+ }
68
+ // --- start / say / end ----------------------------------------------------------------------
69
+ async function testStartCommand(agentId, opts) {
70
+ try {
71
+ if (!(await (0, ui_1.confirm)(`${WARNING} Start it?`, opts.yes)))
72
+ return;
73
+ const version = (0, versions_1.firstVersion)(opts.versions);
74
+ const { token, callSid } = await startToken(agentId, version, await readMetadata(opts));
75
+ (0, fs_1.mkdirSync)(SESSIONS_DIR, { recursive: true, mode: 0o700 });
76
+ const id = (0, crypto_1.randomBytes)(4).toString('hex');
77
+ (0, fs_1.writeFileSync)((0, path_1.join)(SESSIONS_DIR, `${id}.req.json`), JSON.stringify({ token, callSid, agentId }), { mode: 0o600 });
78
+ (0, child_process_1.spawn)(process.execPath, [process.argv[1], 'test', '_serve', id], {
79
+ detached: true, stdio: 'ignore', windowsHide: true, env: { ...process.env, CK_NO_UPDATE_CHECK: '1' },
80
+ }).unref();
81
+ const deadline = Date.now() + 60_000;
82
+ while (!(0, fs_1.existsSync)(sessionPath(id))) {
83
+ if (Date.now() > deadline)
84
+ throw new Error('The test session did not come up within 60s. If the call started, find it with: cku calls list --sid ${callSid}');
85
+ await new Promise((r) => setTimeout(r, 250));
86
+ }
87
+ const s = JSON.parse((0, fs_1.readFileSync)(sessionPath(id), 'utf8'));
88
+ if (s.error) {
89
+ (0, fs_1.unlinkSync)(sessionPath(id));
90
+ throw new Error(s.error);
91
+ }
92
+ if (opts.json)
93
+ return void console.log(JSON.stringify({ sessionId: id, callSid, events: s.greeting }, null, 2));
94
+ (0, ui_1.info)(chalk_1.default.dim(`session ${id} call ${callSid}`));
95
+ if (s.greeting.length)
96
+ printEvents(s.greeting);
97
+ else
98
+ (0, ui_1.info)(chalk_1.default.dim('(the agent is waiting for the caller to speak first)'));
99
+ (0, ui_1.info)(chalk_1.default.dim(`Next: cku test say ${id} "<what the caller says>" · cku test end ${id}`));
100
+ }
101
+ catch (err) {
102
+ (0, ui_1.handleError)(err);
103
+ }
104
+ }
105
+ async function callSession(id, path, body) {
106
+ if (!/^[0-9a-f]{8}$/.test(id) || !(0, fs_1.existsSync)(sessionPath(id))) {
107
+ throw new Error(`No test session "${id}". Start one with: cku test start <agentId>`);
108
+ }
109
+ const s = JSON.parse((0, fs_1.readFileSync)(sessionPath(id), 'utf8'));
110
+ try {
111
+ const res = await fetch(`http://127.0.0.1:${s.port}${path}`, {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json', 'x-session-secret': s.secret },
114
+ body: JSON.stringify(body),
115
+ });
116
+ const data = await res.json();
117
+ if (!res.ok)
118
+ throw new Error(data.error || `Session answered HTTP ${res.status}`);
119
+ return data;
120
+ }
121
+ catch (err) {
122
+ if (err?.cause?.code === 'ECONNREFUSED') {
123
+ try {
124
+ (0, fs_1.unlinkSync)(sessionPath(id));
125
+ }
126
+ catch { /* gone */ }
127
+ throw new Error(`Test session ${id} is no longer running (it ends after 10 idle minutes). Find the call with: cku calls list --sid ${s.callSid}`);
128
+ }
129
+ throw err;
130
+ }
131
+ }
132
+ async function testSayCommand(id, words, opts) {
133
+ try {
134
+ const text = words.join(' ').trim();
135
+ if (!text)
136
+ throw new Error('Nothing to say. Usage: cku test say <sessionId> "<what the caller says>"');
137
+ const timeoutMs = (0, ui_1.intFlag)(opts.timeout, '--timeout', 60) * 1000;
138
+ const data = await callSession(id, '/say', { text, timeoutMs });
139
+ if (opts.json)
140
+ return void console.log(JSON.stringify(data, null, 2));
141
+ printEvents(data.events.filter((e) => e.type !== 'user'));
142
+ if (!data.events.some((e) => e.type === 'agent') && !data.ended)
143
+ (0, ui_1.info)(chalk_1.default.dim('(no agent reply before the timeout)'));
144
+ if (data.ended)
145
+ (0, ui_1.info)(chalk_1.default.dim(`The call is over. Collect it with: cku test end ${id}`));
146
+ }
147
+ catch (err) {
148
+ (0, ui_1.handleError)(err);
149
+ }
150
+ }
151
+ async function testEndCommand(id, opts) {
152
+ try {
153
+ const data = await callSession(id, '/end', {});
154
+ if (opts.json)
155
+ return void console.log(JSON.stringify(data, null, 2));
156
+ (0, ui_1.info)((0, liveTest_1.formatTranscript)(data.events));
157
+ (0, ui_1.info)(chalk_1.default.dim(`call ${data.callSid}: post-call results: cku calls list --sid ${data.callSid}`));
158
+ }
159
+ catch (err) {
160
+ (0, ui_1.handleError)(err);
161
+ }
162
+ }
163
+ /** The background half of `start`: owns the room, answers say/end on localhost, exits when done or idle. */
164
+ async function testServeCommand(id) {
165
+ const reqPath = (0, path_1.join)(SESSIONS_DIR, `${id}.req.json`);
166
+ const { token, callSid, agentId } = JSON.parse((0, fs_1.readFileSync)(reqPath, 'utf8'));
167
+ (0, fs_1.unlinkSync)(reqPath);
168
+ const write = (s) => (0, fs_1.writeFileSync)(sessionPath(id), JSON.stringify(s), { mode: 0o600 });
169
+ let session;
170
+ let greeting;
171
+ try {
172
+ ({ session, greeting } = await liveTest_1.LiveSession.open(token, callSid));
173
+ }
174
+ catch (err) {
175
+ write({ error: `Could not join the test call room: ${err.message}` });
176
+ return;
177
+ }
178
+ const secret = (0, crypto_1.randomBytes)(16).toString('hex');
179
+ let idle;
180
+ let busy = false;
181
+ const shutdown = async () => {
182
+ server.close();
183
+ try {
184
+ (0, fs_1.unlinkSync)(sessionPath(id));
185
+ }
186
+ catch { /* gone */ }
187
+ await session.close();
188
+ process.exit(0);
189
+ };
190
+ const touch = () => { clearTimeout(idle); idle = setTimeout(shutdown, IDLE_MS); };
191
+ const server = (0, http_1.createServer)((req, res) => {
192
+ const reply = (status, body) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); };
193
+ if (req.headers['x-session-secret'] !== secret || req.method !== 'POST')
194
+ return reply(403, { error: 'forbidden' });
195
+ let raw = '';
196
+ req.on('data', (c) => { raw += c; if (raw.length > 100_000)
197
+ req.destroy(); });
198
+ req.on('end', async () => {
199
+ touch();
200
+ try {
201
+ const body = raw ? JSON.parse(raw) : {};
202
+ if (req.url === '/say') {
203
+ if (busy)
204
+ return reply(409, { error: 'The agent is still answering the previous turn. Wait for that command to finish.' });
205
+ busy = true;
206
+ try {
207
+ const events = await session.say(String(body.text || ''), Number(body.timeoutMs) || 60_000);
208
+ return reply(200, { sessionId: id, callSid, events, ended: session.ended });
209
+ }
210
+ finally {
211
+ busy = false;
212
+ }
213
+ }
214
+ if (req.url === '/end') {
215
+ reply(200, { sessionId: id, callSid, agentId, events: session.events, ended: session.ended });
216
+ return void setTimeout(shutdown, 100);
217
+ }
218
+ reply(404, { error: 'unknown route' });
219
+ }
220
+ catch (err) {
221
+ reply(500, { error: err.message });
222
+ }
223
+ });
224
+ });
225
+ server.listen(0, '127.0.0.1', () => {
226
+ const port = server.address().port;
227
+ write({ pid: process.pid, port, secret, callSid, agentId, greeting });
228
+ touch();
229
+ });
230
+ }
231
+ async function scenario(agentId, opts) {
232
+ let s = { name: '', persona: opts.persona, prompt: opts.prompt, criteria: opts.criteria, variables: {} };
233
+ if (opts.test) {
234
+ const res = await (0, session_1.requireApi)().listTestCases(agentId);
235
+ const t = (res.testCases || res.data || []).find((x) => x._id === opts.test);
236
+ if (!t)
237
+ throw new Error(`Test case ${opts.test} is not one of this agent's. List them with: cku sim tests ${agentId}`);
238
+ s = { name: t.testcaseName || '', persona: t.userPersona, prompt: t.userPrompt, criteria: t.successCriteria, variables: t.testVariables || {} };
239
+ if (opts.persona)
240
+ s.persona = opts.persona;
241
+ if (opts.prompt)
242
+ s.prompt = opts.prompt;
243
+ if (opts.criteria)
244
+ s.criteria = opts.criteria;
245
+ }
246
+ s.variables = { ...s.variables, ...(await readMetadata(opts)) };
247
+ if (!s.prompt)
248
+ throw new Error('Say what the caller wants: --prompt "<goal and beats>" (or --test <testCaseId> to reuse a saved simulation test case).');
249
+ return { ...s, prompt: s.prompt };
250
+ }
251
+ async function testRunCommand(agentId, opts) {
252
+ let session;
253
+ try {
254
+ const s = await scenario(agentId, opts);
255
+ const llm = (0, liveTest_1.resolveLlm)(opts);
256
+ const judge = { ...llm, model: opts.judgeModel || llm.model };
257
+ const maxTurns = (0, ui_1.intFlag)(opts.maxTurns, '--max-turns', 20);
258
+ const turnMs = (0, ui_1.intFlag)(opts.timeout, '--timeout', 60) * 1000;
259
+ if (!(await (0, ui_1.confirm)(`${WARNING} Run it?`, opts.yes)))
260
+ return;
261
+ const log = (events) => { if (!opts.json)
262
+ printEvents(events); };
263
+ const { token, callSid } = await startToken(agentId, (0, versions_1.firstVersion)(opts.versions), s.variables);
264
+ if (!opts.json)
265
+ (0, ui_1.info)(chalk_1.default.dim(`call ${callSid} caller: ${llm.name}/${llm.model}`));
266
+ const opened = await liveTest_1.LiveSession.open(token, callSid);
267
+ session = opened.session;
268
+ log(opened.greeting);
269
+ const history = [{ role: 'system', content: (0, liveTest_1.callerSystemPrompt)(s) }];
270
+ const agentSaid = (events) => events.filter((e) => e.type === 'agent').map((e) => e.text).join(' ');
271
+ let heard = agentSaid(opened.greeting);
272
+ let endedBy = 'max-turns';
273
+ for (let turn = 0; turn < maxTurns; turn++) {
274
+ if (session.ended) {
275
+ endedBy = 'agent';
276
+ break;
277
+ }
278
+ history.push({ role: 'user', content: heard || '(The call is connected. The agent has not said anything yet.)' });
279
+ const line = (0, liveTest_1.parseCallerLine)(await (0, liveTest_1.chatComplete)(llm, history));
280
+ history.push({ role: 'assistant', content: line.text || '(hangs up)' });
281
+ if (line.text) {
282
+ if (session.ended) {
283
+ endedBy = 'agent';
284
+ break;
285
+ }
286
+ const events = await session.say(line.text, turnMs);
287
+ log([{ type: 'user', text: line.text, t: Date.now() }, ...events]);
288
+ heard = agentSaid(events);
289
+ if (session.ended) {
290
+ endedBy = 'agent';
291
+ break;
292
+ }
293
+ if (!events.length) {
294
+ endedBy = 'timeout';
295
+ break;
296
+ }
297
+ }
298
+ if (line.end) {
299
+ endedBy = 'caller';
300
+ break;
301
+ }
302
+ }
303
+ const events = session.events;
304
+ await session.close();
305
+ session = undefined;
306
+ let verdict;
307
+ if (s.criteria)
308
+ verdict = (0, liveTest_1.parseVerdict)(await (0, liveTest_1.chatComplete)(judge, (0, liveTest_1.judgeMessages)(s.criteria, events), 0));
309
+ const out = { callSid, agentId, test: opts.test, endedBy, caller: `${llm.name}/${llm.model}`, events, verdict };
310
+ if (opts.json)
311
+ console.log(JSON.stringify(out, null, 2));
312
+ else {
313
+ (0, ui_1.info)(chalk_1.default.dim(`ended by: ${endedBy}`));
314
+ if (verdict) {
315
+ const label = verdict.invalid ? chalk_1.default.yellow('GRADING FAILED') : verdict.pass ? chalk_1.default.green('PASS') : chalk_1.default.red('FAIL');
316
+ (0, ui_1.info)(`${label} ${verdict.summary}`);
317
+ for (const c of verdict.criteria)
318
+ (0, ui_1.info)(` ${c.pass ? chalk_1.default.green('✔') : chalk_1.default.red('✖')} ${c.criterion}${c.evidence ? chalk_1.default.dim(` (${c.evidence})`) : ''}${c.reason && !c.pass ? ` — ${c.reason}` : ''}`);
319
+ }
320
+ (0, ui_1.info)(chalk_1.default.dim(`post-call results: cku calls list --sid ${callSid}`));
321
+ }
322
+ if (verdict && !verdict.pass)
323
+ process.exitCode = 1;
324
+ }
325
+ catch (err) {
326
+ if (session)
327
+ await session.close();
328
+ (0, ui_1.handleError)(err);
329
+ }
330
+ }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.sleep = void 0;
7
7
  exports.toTestVariables = toTestVariables;
8
8
  exports.toTestFunctions = toTestFunctions;
9
+ exports.realFunctionList = realFunctionList;
9
10
  exports.simTestsListCommand = simTestsListCommand;
10
11
  exports.simCreateCommand = simCreateCommand;
11
12
  exports.simUpdateCommand = simUpdateCommand;
@@ -58,6 +59,21 @@ function toTestFunctions(input) {
58
59
  return { name: f.name, ...(f.mock === undefined ? {} : { mock: f.mock }), ...('response' in f ? { response: f.response } : {}) };
59
60
  });
60
61
  }
62
+ /**
63
+ * `--real-functions`: every function on the agent version (version-level + node/capability), as
64
+ * `{name, mock:false}`, the CLI twin of switching every Mock toggle off in the dashboard. Entries
65
+ * from `--functions` win by name. WhatsApp functions stay out (they must never run for real).
66
+ */
67
+ function realFunctionList(version, explicit = []) {
68
+ const all = [...(version.functions || []), ...(version.capabilities || []).flatMap((c) => c.functions || [])];
69
+ const names = [...new Set(all.filter((f) => f?.name && !/whatsapp/i.test(String(f.type || ''))).map((f) => f.name))];
70
+ const byName = new Map(explicit.map((f) => [f.name, f]));
71
+ return [...names.map((n) => byName.get(n) || { name: n, mock: false }), ...explicit.filter((f) => !names.includes(f.name))];
72
+ }
73
+ async function withRealFunctions(api, agentId, version, explicit) {
74
+ const res = await api.getAgent(agentId, version);
75
+ return realFunctionList(res.agent || res, explicit);
76
+ }
61
77
  async function simTestsListCommand(agentId, opts) {
62
78
  const api = (0, session_1.requireApi)();
63
79
  try {
@@ -78,7 +94,9 @@ async function simCreateCommand(agentId, opts) {
78
94
  try {
79
95
  // Parse the JSON flags before prompting, so a typo fails before any typing.
80
96
  const testVariables = opts.variables ? toTestVariables(await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables')) : {};
81
- const testFunctions = opts.functions ? toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions')) : [];
97
+ let testFunctions = opts.functions ? toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions')) : [];
98
+ if (opts.realFunctions)
99
+ testFunctions = await withRealFunctions(api, agentId, opts.versions, testFunctions);
82
100
  let { name, persona, prompt, criteria } = opts;
83
101
  if (!name || !prompt || !criteria) {
84
102
  (0, ui_1.needTty)('Pass --name, --prompt and --criteria.');
@@ -143,8 +161,13 @@ async function simUpdateCommand(testCaseId, opts) {
143
161
  body.testVariables = toTestVariables(await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables'));
144
162
  if (opts.functions !== undefined)
145
163
  body.testFunctions = toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions'));
164
+ if (opts.realFunctions) {
165
+ if (!opts.agent)
166
+ return (0, ui_1.handleError)(new Error('--real-functions on update needs --agent <agentId> (the test case does not carry it).'));
167
+ body.testFunctions = await withRealFunctions(api, opts.agent, opts.versions, body.testFunctions || []);
168
+ }
146
169
  if (!Object.keys(body).length) {
147
- return (0, ui_1.handleError)(new Error('Nothing to update. Pass at least one of --name, --persona, --prompt, --criteria, --model, --variables, --functions.'));
170
+ return (0, ui_1.handleError)(new Error('Nothing to update. Pass at least one of --name, --persona, --prompt, --criteria, --model, --variables, --functions, --real-functions.'));
148
171
  }
149
172
  const res = await api.updateTestCase(testCaseId, body);
150
173
  (0, ui_1.success)(`Updated test case ${res.testCase?._id || testCaseId}: ${Object.keys(body).join(', ')}.`);
File without changes