callkaro-cli-unofficial 0.9.5 → 0.10.1
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 +14 -15
- package/dist/commands/agentCommits.js +102 -0
- package/dist/commands/agentVersions.js +3 -102
- package/dist/commands/agents.js +3 -5
- package/dist/commands/auditReports.js +275 -0
- package/dist/commands/chatAgents.js +41 -17
- package/dist/commands/chatAuditStrategies.js +0 -20
- package/dist/commands/contacts.js +32 -15
- package/dist/commands/liveTest.js +330 -0
- package/dist/commands/models.js +46 -88
- package/dist/commands/platformExtras.js +0 -25
- package/dist/commands/simulations.js +25 -2
- package/dist/commands/transcribers.js +36 -65
- package/dist/commands/update.js +73 -27
- package/dist/commands/whatsappStats.js +105 -0
- package/dist/commands/workflows.js +6 -18
- package/dist/index.js +171 -26
- package/dist/lib/agentUpdate.js +4 -116
- package/dist/lib/api.js +41 -0
- package/dist/lib/config.js +1 -1
- package/dist/lib/dashboardApi.js +1 -43
- package/dist/lib/dashboardAuth.js +0 -33
- package/dist/lib/liveCatalogCheck.js +99 -0
- package/dist/lib/liveTest.js +361 -0
- package/dist/lib/ui.js +15 -0
- package/dist/lib/updateCheck.js +3 -2
- package/package.json +6 -6
- package/dist/data/catalog.json +0 -1976
- package/dist/lib/catalog.js +0 -136
|
@@ -44,16 +44,14 @@ exports.chatAgentsCreateCommand = chatAgentsCreateCommand;
|
|
|
44
44
|
exports.chatAgentsUpdateCommand = chatAgentsUpdateCommand;
|
|
45
45
|
exports.chatAgentsCreateVersionCommand = chatAgentsCreateVersionCommand;
|
|
46
46
|
exports.chatAgentsPublishCommand = chatAgentsPublishCommand;
|
|
47
|
-
exports.
|
|
47
|
+
exports.chatAgentsCompareCommand = chatAgentsCompareCommand;
|
|
48
48
|
exports.chatAgentsDeleteVersionCommand = chatAgentsDeleteVersionCommand;
|
|
49
49
|
exports.chatAgentsImportVersionsCommand = chatAgentsImportVersionsCommand;
|
|
50
50
|
exports.chatAgentsImportCommand = chatAgentsImportCommand;
|
|
51
51
|
exports.chatAgentsCommitsCommand = chatAgentsCommitsCommand;
|
|
52
52
|
exports.chatAgentsCommitCommand = chatAgentsCommitCommand;
|
|
53
53
|
exports.chatAgentsRevertCommand = chatAgentsRevertCommand;
|
|
54
|
-
const dashboardApi_1 = require("../lib/dashboardApi");
|
|
55
54
|
const session_1 = require("../lib/session");
|
|
56
|
-
const dashboardAuth_1 = require("../lib/dashboardAuth");
|
|
57
55
|
const fs_1 = require("fs");
|
|
58
56
|
const ui_1 = require("../lib/ui");
|
|
59
57
|
const looseJson_1 = require("../lib/looseJson");
|
|
@@ -218,7 +216,6 @@ async function chatAgentsCreateCommand(json, opts) {
|
|
|
218
216
|
async function chatAgentsUpdateCommand(agentId, opts) {
|
|
219
217
|
const api = (0, session_1.requireApi)();
|
|
220
218
|
try {
|
|
221
|
-
await (0, dashboardAuth_1.refreshModelCatalog)();
|
|
222
219
|
const patch = await readChatAgentJson(opts.set, undefined, '--set object');
|
|
223
220
|
const res = await api.updateChatAgent(agentId, (0, versions_1.firstVersion)(opts.versions), patch);
|
|
224
221
|
if (opts.json)
|
|
@@ -261,20 +258,47 @@ async function chatAgentsPublishCommand(agentId, opts) {
|
|
|
261
258
|
}
|
|
262
259
|
}
|
|
263
260
|
/** `cku chat-agents diff <chatAgentId> --versions vA,vB | --commits cA,cB` */
|
|
264
|
-
|
|
261
|
+
const lineCount = (s) => (s ? s.split('\n').length : 0);
|
|
262
|
+
/** `cku chat-agents compare <agentId> --a <id> --b <id> [--commits] [--field name]` — versions by default (official 5.4.8). */
|
|
263
|
+
async function chatAgentsCompareCommand(agentId, opts) {
|
|
265
264
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
265
|
+
if (!opts.a || !opts.b)
|
|
266
|
+
return (0, ui_1.handleError)(new Error('--a <id> and --b <id> are required (version ids, or commit ids with --commits)'));
|
|
267
|
+
const api = (0, session_1.requireApi)();
|
|
268
|
+
const res = opts.commits
|
|
269
|
+
? await api.compareChatAgentCommits(agentId, opts.a, opts.b)
|
|
270
|
+
: await api.compareChatAgentVersions(agentId, [opts.a, opts.b]);
|
|
271
|
+
if ((0, ui_1.output)(res, opts, 'comparison'))
|
|
272
|
+
return;
|
|
273
|
+
const label = (s) => `${s.versionName || s.id} (${s.id})${s.agentMode ? ` [${s.agentMode}]` : ''}`;
|
|
274
|
+
(0, ui_1.info)(`A: ${label(res.left)}\nB: ${label(res.right)}\n`);
|
|
275
|
+
if (res.modeMismatch)
|
|
276
|
+
(0, ui_1.info)(`Different agent types (${res.modeMismatch.left} vs ${res.modeMismatch.right}): only fields both use are compared.\n`);
|
|
277
|
+
if (opts.field) {
|
|
278
|
+
const t = res.textFieldDiffs[opts.field];
|
|
279
|
+
const c = res.changedFields[opts.field];
|
|
280
|
+
if (!t && !c)
|
|
281
|
+
return (0, ui_1.handleError)(new Error(`"${opts.field}" did not change. Changed: ${Object.keys(res.changedFields).join(', ') || '(none)'}`));
|
|
282
|
+
const a = t ? t.left : JSON.stringify(c.left, null, 2);
|
|
283
|
+
const b = t ? t.right : JSON.stringify(c.right, null, 2);
|
|
284
|
+
return void console.log(`--- A\n${a}\n\n--- B\n${b}`);
|
|
285
|
+
}
|
|
286
|
+
const rows = [];
|
|
287
|
+
for (const f of Object.keys(res.changedFields)) {
|
|
288
|
+
const t = res.textFieldDiffs[f];
|
|
289
|
+
const l = res.listDiffs?.[f];
|
|
290
|
+
rows.push([f, t ? `text: ${lineCount(t.left)} → ${lineCount(t.right)} lines` : l ? `+${l.added.length} -${l.removed.length} ~${l.changed.length}` : 'value changed']);
|
|
291
|
+
}
|
|
292
|
+
if (res.nodeDiffs)
|
|
293
|
+
rows.push(['flowGraph nodes', `+${res.nodeDiffs.added.length} -${res.nodeDiffs.removed.length} ~${res.nodeDiffs.changed.length}`]);
|
|
294
|
+
if (!rows.length)
|
|
295
|
+
console.log('No differences.');
|
|
296
|
+
else {
|
|
297
|
+
(0, ui_1.table)(['FIELD', 'CHANGE'], rows);
|
|
298
|
+
(0, ui_1.info)(`\n${rows.length} difference(s). Use --field <name> to see one side by side, or --json for everything (incl. per-node and per-function diffs).`);
|
|
299
|
+
}
|
|
300
|
+
if (res.notComparable?.length)
|
|
301
|
+
(0, ui_1.info)(`Not comparable (not used by both agent types): ${res.notComparable.join(', ')}`);
|
|
278
302
|
}
|
|
279
303
|
catch (err) {
|
|
280
304
|
(0, ui_1.handleError)(err);
|
|
@@ -7,8 +7,6 @@ exports.chatAuditCreateCommand = chatAuditCreateCommand;
|
|
|
7
7
|
exports.chatAuditUpdateCommand = chatAuditUpdateCommand;
|
|
8
8
|
exports.chatAuditDeleteCommand = chatAuditDeleteCommand;
|
|
9
9
|
exports.chatAuditBatchCommand = chatAuditBatchCommand;
|
|
10
|
-
exports.chatAuditReportsCommand = chatAuditReportsCommand;
|
|
11
|
-
const dashboardApi_1 = require("../lib/dashboardApi");
|
|
12
10
|
const session_1 = require("../lib/session");
|
|
13
11
|
const ui_1 = require("../lib/ui");
|
|
14
12
|
const setJson_1 = require("./setJson");
|
|
@@ -142,21 +140,3 @@ async function chatAuditBatchCommand(agentId, strategyId, opts) {
|
|
|
142
140
|
(0, ui_1.handleError)(err);
|
|
143
141
|
}
|
|
144
142
|
}
|
|
145
|
-
/** `cku chat-audit reports [--agent <id>] [--page <n>]` — dashboard session. */
|
|
146
|
-
async function chatAuditReportsCommand(opts) {
|
|
147
|
-
try {
|
|
148
|
-
const res = await (0, dashboardApi_1.dashboardApi)().chatAuditReportBatches({
|
|
149
|
-
agentId: opts.agent,
|
|
150
|
-
page: opts.page ? Number(opts.page) : undefined,
|
|
151
|
-
});
|
|
152
|
-
if (opts.json)
|
|
153
|
-
return void console.log(JSON.stringify(res, null, 2));
|
|
154
|
-
const rows = res?.data?.batches || res?.batches || (Array.isArray(res?.data) ? res.data : []);
|
|
155
|
-
if (!rows.length)
|
|
156
|
-
return void (0, ui_1.info)('No chat audit report batches.');
|
|
157
|
-
(0, ui_1.table)(['ID', 'STATUS', 'CREATED'], rows.map((r) => [r._id || r.id || '', r.status || '', r.createdAt || '']));
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
(0, ui_1.handleError)(err);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.collect = void 0;
|
|
4
4
|
exports.contactsListCommand = contactsListCommand;
|
|
5
|
+
exports.contactsMessagesCommand = contactsMessagesCommand;
|
|
5
6
|
exports.contactsGetCommand = contactsGetCommand;
|
|
6
7
|
exports.contactsCreateCommand = contactsCreateCommand;
|
|
7
8
|
exports.contactsUpdateCommand = contactsUpdateCommand;
|
|
@@ -53,18 +54,6 @@ const attrValue = (c, name) => {
|
|
|
53
54
|
const v = a && typeof a === 'object' && 'value' in a ? a.value : a;
|
|
54
55
|
return v === undefined || v === null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
55
56
|
};
|
|
56
|
-
function output(value, opts, label) {
|
|
57
|
-
if (opts.file) {
|
|
58
|
-
(0, fs_1.writeFileSync)(opts.file, JSON.stringify(value, null, 2));
|
|
59
|
-
(0, ui_1.success)(`Saved ${label} to ${opts.file}`);
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
if (opts.json) {
|
|
63
|
-
console.log(JSON.stringify(value, null, 2));
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
57
|
// ---- contacts -------------------------------------------------------------
|
|
69
58
|
async function contactsListCommand(opts) {
|
|
70
59
|
const api = (0, session_1.requireApi)();
|
|
@@ -75,7 +64,7 @@ async function contactsListCommand(opts) {
|
|
|
75
64
|
page: opts.page,
|
|
76
65
|
pageSize: opts.pageSize,
|
|
77
66
|
});
|
|
78
|
-
if (output({ contacts, pagination }, opts, 'contacts'))
|
|
67
|
+
if ((0, ui_1.output)({ contacts, pagination }, opts, 'contacts'))
|
|
79
68
|
return;
|
|
80
69
|
if (!contacts.length)
|
|
81
70
|
return void (0, ui_1.info)('No contacts match.');
|
|
@@ -89,11 +78,39 @@ async function contactsListCommand(opts) {
|
|
|
89
78
|
(0, ui_1.handleError)(err);
|
|
90
79
|
}
|
|
91
80
|
}
|
|
81
|
+
/** `cku contacts messages <phone>` — a contact's chat messages, newest first (official 5.4.8). */
|
|
82
|
+
async function contactsMessagesCommand(phone, opts) {
|
|
83
|
+
try {
|
|
84
|
+
const result = await (0, session_1.requireApi)().getContactMessages(phone.replace(/\D/g, ''), {
|
|
85
|
+
agentPhoneNumber: opts.agentPhoneNumber?.replace(/\D/g, ''),
|
|
86
|
+
channel: opts.channel,
|
|
87
|
+
threadId: opts.threadId,
|
|
88
|
+
page: opts.page,
|
|
89
|
+
pageSize: opts.pageSize,
|
|
90
|
+
});
|
|
91
|
+
if ((0, ui_1.output)(result, opts, 'contact messages'))
|
|
92
|
+
return;
|
|
93
|
+
if (!result.messages?.length)
|
|
94
|
+
return void (0, ui_1.info)('No messages found for this contact.');
|
|
95
|
+
(0, ui_1.table)(['TIME', 'DIRECTION', 'CHANNEL', 'MESSAGE', 'STATUS'], result.messages.map((m) => [
|
|
96
|
+
String(m.createdAt || '').slice(0, 19), // drop milliseconds
|
|
97
|
+
m.direction || '',
|
|
98
|
+
m.channel || '',
|
|
99
|
+
String(m.body || m.contentLink || m.template || '').replace(/\s+/g, ' ').slice(0, 100),
|
|
100
|
+
m.status || '',
|
|
101
|
+
]));
|
|
102
|
+
const { page, totalPages, total, hasNext } = result.pagination || {};
|
|
103
|
+
(0, ui_1.info)(`Page ${page ?? 1}/${totalPages || 1} - ${total ?? result.messages.length} message(s)${hasNext ? ' (use --page to see more)' : ''}`);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
(0, ui_1.handleError)(err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
92
109
|
async function contactsGetCommand(phone, opts) {
|
|
93
110
|
const api = (0, session_1.requireApi)();
|
|
94
111
|
try {
|
|
95
112
|
const { contact } = await api.getContact(phone);
|
|
96
|
-
if (output(contact, opts, 'contact'))
|
|
113
|
+
if ((0, ui_1.output)(contact, opts, 'contact'))
|
|
97
114
|
return;
|
|
98
115
|
(0, ui_1.keyValue)({
|
|
99
116
|
Phone: contact.phone_number,
|
|
@@ -303,7 +320,7 @@ async function attributesListCommand(opts) {
|
|
|
303
320
|
const api = (0, session_1.requireApi)();
|
|
304
321
|
try {
|
|
305
322
|
const { definitions } = await api.listContactAttributes();
|
|
306
|
-
if (output(definitions, opts, 'attributes'))
|
|
323
|
+
if ((0, ui_1.output)(definitions, opts, 'attributes'))
|
|
307
324
|
return;
|
|
308
325
|
if (!definitions.length) {
|
|
309
326
|
return void (0, ui_1.info)('No attributes defined yet. Create one with `cku contacts attributes set <name>`.');
|
|
@@ -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
|
+
}
|