callkaro-cli-unofficial 0.6.1 → 0.7.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 +1 -0
- package/dist/commands/chatAuditStrategies.js +139 -0
- package/dist/commands/chatSimulations.js +217 -0
- package/dist/commands/contacts.js +0 -2
- package/dist/commands/simulations.js +14 -5
- package/dist/commands/workflows.js +31 -9
- package/dist/endpoints.json +0 -0
- package/dist/index.js +91 -24
- package/dist/lib/agentUpdate.js +4 -4
- package/dist/lib/api.js +27 -1
- package/dist/lib/workflowEdit.js +36 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,6 +205,7 @@ npm run catalog:check # fail if catalog.json disagrees with the snapshot
|
|
|
205
205
|
- `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
206
|
- `cku scheduled list|queued|get` — scheduled calls and the queue of what will actually dial (dashboard login)
|
|
207
207
|
- `cku sim update` — edit a test case in place
|
|
208
|
+
- `cku chat-sim` — simulations and test cases for chat agents (official 5.4.0); `cku chat-audit-strategies` — AI Auditor for WhatsApp agents, including `batch` to audit conversations now
|
|
208
209
|
- `cku calls list --postcall dnc_requested=true --provider-type plivo --sid <sid>` — filters the official CLI doesn't expose
|
|
209
210
|
- `cku calls logs` — grep/awk/sed over the raw call log, in-process, no subprocess and no shell
|
|
210
211
|
- `cku accounts status` — reports which account is in effect **and where that choice came from**
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.chatAuditListCommand = chatAuditListCommand;
|
|
4
|
+
exports.chatAuditGetCommand = chatAuditGetCommand;
|
|
5
|
+
exports.chatAuditModelsCommand = chatAuditModelsCommand;
|
|
6
|
+
exports.chatAuditCreateCommand = chatAuditCreateCommand;
|
|
7
|
+
exports.chatAuditUpdateCommand = chatAuditUpdateCommand;
|
|
8
|
+
exports.chatAuditDeleteCommand = chatAuditDeleteCommand;
|
|
9
|
+
exports.chatAuditBatchCommand = chatAuditBatchCommand;
|
|
10
|
+
const session_1 = require("../lib/session");
|
|
11
|
+
const ui_1 = require("../lib/ui");
|
|
12
|
+
const setJson_1 = require("./setJson");
|
|
13
|
+
// Mirrors the backend's /cli/chat-audit-strategies. It keeps every field not sent on update,
|
|
14
|
+
// so no client-side merge is needed (unlike the voice audit strategies).
|
|
15
|
+
/** A numeric flag, or undefined when absent. NaN would serialise to null and be silently accepted. */
|
|
16
|
+
function numericFlag(raw, flag) {
|
|
17
|
+
if (raw === undefined || raw === null || raw === '')
|
|
18
|
+
return undefined;
|
|
19
|
+
const n = Number(raw);
|
|
20
|
+
if (!Number.isFinite(n))
|
|
21
|
+
throw new Error(`${flag} must be a number (got "${raw}").`);
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
function buildFields(opts) {
|
|
25
|
+
return (0, setJson_1.pruneUndefined)({
|
|
26
|
+
...(opts.set ? (0, setJson_1.readSetJson)(opts.set) : {}),
|
|
27
|
+
name: opts.name,
|
|
28
|
+
audit_strategy: opts.strategy,
|
|
29
|
+
active: opts.disable ? false : opts.active,
|
|
30
|
+
llms: opts.model ? { name: opts.model } : undefined,
|
|
31
|
+
percentage_per_day: numericFlag(opts.pctPerDay, '--pct-per-day'),
|
|
32
|
+
max_conversations_per_day: numericFlag(opts.maxPerDay, '--max-per-day'),
|
|
33
|
+
min_user_messages: numericFlag(opts.minUserMessages, '--min-user-messages'),
|
|
34
|
+
min_messages: numericFlag(opts.minMessages, '--min-messages'),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function chatAuditListCommand(agentId, opts) {
|
|
38
|
+
const api = (0, session_1.requireApi)();
|
|
39
|
+
try {
|
|
40
|
+
const rows = (await api.listChatAuditStrategies(agentId)).strategies || [];
|
|
41
|
+
if (opts.json)
|
|
42
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
43
|
+
if (!rows.length)
|
|
44
|
+
return void (0, ui_1.info)('No chat audit strategies yet.');
|
|
45
|
+
(0, ui_1.table)(['NAME', 'ID', 'ACTIVE'], rows.map((s) => [s.name || '', s._id, s.active ? 'yes' : 'no']));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
(0, ui_1.handleError)(err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function chatAuditGetCommand(agentId, strategyId, opts) {
|
|
52
|
+
const api = (0, session_1.requireApi)();
|
|
53
|
+
try {
|
|
54
|
+
const strategy = ((await api.listChatAuditStrategies(agentId)).strategies || []).find((s) => s._id === strategyId);
|
|
55
|
+
if (!strategy)
|
|
56
|
+
return (0, ui_1.handleError)(new Error(`Chat audit strategy ${strategyId} not found for agent ${agentId}.`));
|
|
57
|
+
if (opts.json)
|
|
58
|
+
return void console.log(JSON.stringify(strategy, null, 2));
|
|
59
|
+
(0, ui_1.keyValue)({
|
|
60
|
+
name: strategy.name,
|
|
61
|
+
id: strategy._id,
|
|
62
|
+
active: strategy.active,
|
|
63
|
+
audit_strategy: strategy.audit_strategy,
|
|
64
|
+
llms: JSON.stringify(strategy.llms),
|
|
65
|
+
audit_filters: JSON.stringify(strategy.audit_filters),
|
|
66
|
+
percentage_per_day: strategy.percentage_per_day,
|
|
67
|
+
max_conversations_per_day: strategy.max_conversations_per_day,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
(0, ui_1.handleError)(err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function chatAuditModelsCommand(agentId, opts) {
|
|
75
|
+
const api = (0, session_1.requireApi)();
|
|
76
|
+
try {
|
|
77
|
+
const rows = (await api.listChatAuditStrategies(agentId)).models || [];
|
|
78
|
+
if (opts.json)
|
|
79
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
80
|
+
(0, ui_1.table)(['MODEL'], rows.map((m) => [m]));
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
(0, ui_1.handleError)(err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function chatAuditCreateCommand(agentId, opts) {
|
|
87
|
+
const api = (0, session_1.requireApi)();
|
|
88
|
+
try {
|
|
89
|
+
const fields = buildFields(opts);
|
|
90
|
+
if (!fields.audit_strategy) {
|
|
91
|
+
throw new Error('audit_strategy is required: pass --strategy <text> or --set \'{"audit_strategy":"..."}\'');
|
|
92
|
+
}
|
|
93
|
+
const res = await api.createChatAuditStrategy({ agentId, ...fields });
|
|
94
|
+
(0, ui_1.success)(`Created chat audit strategy ${res.strategy?._id || ''}.`);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
(0, ui_1.handleError)(err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function chatAuditUpdateCommand(agentId, strategyId, opts) {
|
|
101
|
+
const api = (0, session_1.requireApi)();
|
|
102
|
+
try {
|
|
103
|
+
const res = await api.updateChatAuditStrategy(strategyId, { agentId, ...buildFields(opts) });
|
|
104
|
+
(0, ui_1.success)(res.message || `Updated chat audit strategy ${strategyId}.`);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
(0, ui_1.handleError)(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function chatAuditDeleteCommand(strategyId) {
|
|
111
|
+
const api = (0, session_1.requireApi)();
|
|
112
|
+
try {
|
|
113
|
+
const res = await api.deleteChatAuditStrategy(strategyId);
|
|
114
|
+
(0, ui_1.success)(res.message || `Deleted chat audit strategy ${strategyId}.`);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
(0, ui_1.handleError)(err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function chatAuditBatchCommand(agentId, strategyId, opts) {
|
|
121
|
+
const api = (0, session_1.requireApi)();
|
|
122
|
+
try {
|
|
123
|
+
const max = numericFlag(opts.max, '--max');
|
|
124
|
+
if (max === undefined || !Number.isInteger(max) || max < 1 || max > 500) {
|
|
125
|
+
throw new Error('--max must be a whole number from 1 to 500');
|
|
126
|
+
}
|
|
127
|
+
const res = await api.createChatAuditBatch({
|
|
128
|
+
...(opts.set ? (0, setJson_1.readSetJson)(opts.set) : {}),
|
|
129
|
+
agentId,
|
|
130
|
+
strategyId,
|
|
131
|
+
maxConversations: max,
|
|
132
|
+
...(0, setJson_1.pruneUndefined)({ phone: opts.phone, llm: opts.model ? { name: opts.model } : undefined }),
|
|
133
|
+
});
|
|
134
|
+
(0, ui_1.success)(`${res.message || 'Batch created'}${res.data?.batchId ? ` (batch ${res.data.batchId})` : ''}.`);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
(0, ui_1.handleError)(err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
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.chatTestCaseBody = chatTestCaseBody;
|
|
7
|
+
exports.evaluationOf = evaluationOf;
|
|
8
|
+
exports.chatSimTestsListCommand = chatSimTestsListCommand;
|
|
9
|
+
exports.chatSimCreateCommand = chatSimCreateCommand;
|
|
10
|
+
exports.chatSimUpdateCommand = chatSimUpdateCommand;
|
|
11
|
+
exports.chatSimDeleteCommand = chatSimDeleteCommand;
|
|
12
|
+
exports.chatSimRunCommand = chatSimRunCommand;
|
|
13
|
+
exports.chatSimRunsListCommand = chatSimRunsListCommand;
|
|
14
|
+
exports.chatSimResultsCommand = chatSimResultsCommand;
|
|
15
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
16
|
+
const session_1 = require("../lib/session");
|
|
17
|
+
const ui_1 = require("../lib/ui");
|
|
18
|
+
const jsonInput_1 = require("./jsonInput");
|
|
19
|
+
// The dashboard's pass mark: successRating out of 10.
|
|
20
|
+
const PASS_RATING = 7;
|
|
21
|
+
/** Only the fields given. `enabled: false` on a mock runs the agent's real function. */
|
|
22
|
+
async function chatTestCaseBody(opts) {
|
|
23
|
+
const body = {
|
|
24
|
+
test_case_name: opts.name,
|
|
25
|
+
user_persona: opts.persona,
|
|
26
|
+
user_prompt: opts.prompt,
|
|
27
|
+
success_criteria: opts.criteria,
|
|
28
|
+
primary_user_model: opts.model,
|
|
29
|
+
secondary_user_model: opts.secondaryModel,
|
|
30
|
+
};
|
|
31
|
+
if (opts.mocks !== undefined) {
|
|
32
|
+
const mocks = await (0, jsonInput_1.readJsonFlag)(opts.mocks, '--mocks');
|
|
33
|
+
if (!Array.isArray(mocks))
|
|
34
|
+
throw new Error('--mocks must be a JSON array of {"name", "response"?, "enabled"?}.');
|
|
35
|
+
body.function_mocks = mocks.map((m, i) => {
|
|
36
|
+
if (!m || typeof m.name !== 'string' || !m.name)
|
|
37
|
+
throw new Error(`--mocks[${i}] needs a "name" (string).`);
|
|
38
|
+
return {
|
|
39
|
+
name: m.name,
|
|
40
|
+
...(m.response === undefined ? {} : { response: m.response }),
|
|
41
|
+
...(m.enabled === undefined ? {} : { enabled: m.enabled }),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return Object.fromEntries(Object.entries(body).filter(([, v]) => v !== undefined));
|
|
46
|
+
}
|
|
47
|
+
async function defaultChatModel() {
|
|
48
|
+
const res = await (0, session_1.requireApi)().listChatAgentModels();
|
|
49
|
+
return res.categories?.[0]?.defaultModel || res.models?.[0]?.id;
|
|
50
|
+
}
|
|
51
|
+
function evaluationOf(sim) {
|
|
52
|
+
const evaluation = Array.isArray(sim.post_simulation) ? sim.post_simulation[0] : null;
|
|
53
|
+
const raw = evaluation?.successRating;
|
|
54
|
+
const rating = raw === null || raw === undefined || raw === '' ? NaN : Number(raw);
|
|
55
|
+
if (!evaluation || !Number.isFinite(rating))
|
|
56
|
+
return { rating: null, result: 'GRADING FAILED', flaws: '' };
|
|
57
|
+
return { rating, result: rating >= PASS_RATING ? 'PASS' : 'FAIL', flaws: evaluation.successFlaws || '' };
|
|
58
|
+
}
|
|
59
|
+
async function chatSimTestsListCommand(chatAgentId, opts) {
|
|
60
|
+
const api = (0, session_1.requireApi)();
|
|
61
|
+
try {
|
|
62
|
+
const rows = (await api.listChatTestCases(chatAgentId)).testCases || [];
|
|
63
|
+
if (opts.json)
|
|
64
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
65
|
+
if (!rows.length)
|
|
66
|
+
return void (0, ui_1.info)('No chat test cases yet.');
|
|
67
|
+
(0, ui_1.table)(['NAME', 'ID', 'USER MODEL'], rows.map((t) => [t.test_case_name || '', t._id, t.primary_user_model || '']));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
(0, ui_1.handleError)(err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function chatSimCreateCommand(chatAgentId, opts) {
|
|
74
|
+
const api = (0, session_1.requireApi)();
|
|
75
|
+
try {
|
|
76
|
+
// Parse --mocks before prompting, so a typo fails before any typing.
|
|
77
|
+
const body = await chatTestCaseBody(opts);
|
|
78
|
+
let { name, persona, prompt, criteria } = opts;
|
|
79
|
+
if (!name || !prompt || !criteria) {
|
|
80
|
+
(0, ui_1.needTty)('Pass --name, --prompt and --criteria.');
|
|
81
|
+
const answers = await (0, prompts_1.default)([
|
|
82
|
+
{ type: name ? null : 'text', name: 'name', message: 'Test case name' },
|
|
83
|
+
{ type: persona ? null : 'text', name: 'persona', message: 'User persona (who they are, tone, what they know)' },
|
|
84
|
+
{ type: prompt ? null : 'text', name: 'prompt', message: 'Simulated user prompt (goal, beats, rules)' },
|
|
85
|
+
{ type: criteria ? null : 'text', name: 'criteria', message: 'Success criteria' },
|
|
86
|
+
], { onCancel: () => process.exit(1) });
|
|
87
|
+
name = name || answers.name;
|
|
88
|
+
persona = persona || answers.persona;
|
|
89
|
+
prompt = prompt || answers.prompt;
|
|
90
|
+
criteria = criteria || answers.criteria;
|
|
91
|
+
}
|
|
92
|
+
if (!name || !prompt || !criteria) {
|
|
93
|
+
return (0, ui_1.handleError)(new Error('--name, --prompt and --criteria are all required (or answer the prompts)'));
|
|
94
|
+
}
|
|
95
|
+
const res = await api.createChatTestCase({
|
|
96
|
+
...body,
|
|
97
|
+
chat_agent_id: chatAgentId,
|
|
98
|
+
test_case_name: name,
|
|
99
|
+
...(persona ? { user_persona: persona } : {}),
|
|
100
|
+
user_prompt: prompt,
|
|
101
|
+
success_criteria: criteria,
|
|
102
|
+
primary_user_model: opts.model || (await defaultChatModel()),
|
|
103
|
+
});
|
|
104
|
+
(0, ui_1.success)(`Created chat test case ${res.testCase?._id || ''} (user model: ${res.testCase?.primary_user_model || ''}).`);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
(0, ui_1.handleError)(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Sends only the passed fields; --mocks replaces the whole list. */
|
|
111
|
+
async function chatSimUpdateCommand(testCaseId, opts) {
|
|
112
|
+
const api = (0, session_1.requireApi)();
|
|
113
|
+
try {
|
|
114
|
+
const body = await chatTestCaseBody(opts);
|
|
115
|
+
if (!Object.keys(body).length) {
|
|
116
|
+
throw new Error('Nothing to update: pass at least one of --name --persona --prompt --criteria --model --secondary-model --mocks');
|
|
117
|
+
}
|
|
118
|
+
await api.updateChatTestCase(testCaseId, body);
|
|
119
|
+
(0, ui_1.success)(`Updated chat test case ${testCaseId}.`);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
(0, ui_1.handleError)(err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function chatSimDeleteCommand(testCaseId, opts) {
|
|
126
|
+
const api = (0, session_1.requireApi)();
|
|
127
|
+
try {
|
|
128
|
+
if (!(await (0, ui_1.confirm)(`Delete chat test case ${testCaseId}?`, opts.yes)))
|
|
129
|
+
return;
|
|
130
|
+
const res = await api.deleteChatTestCase(testCaseId);
|
|
131
|
+
(0, ui_1.success)(res.msg || 'Deleted.');
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
(0, ui_1.handleError)(err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const ids = (raw) => (raw || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
138
|
+
async function chatSimRunCommand(chatAgentId, opts) {
|
|
139
|
+
const api = (0, session_1.requireApi)();
|
|
140
|
+
try {
|
|
141
|
+
const testIds = ids(opts.tests);
|
|
142
|
+
const versionIds = ids(opts.versions);
|
|
143
|
+
if (!testIds.length)
|
|
144
|
+
throw new Error('--tests <testCaseIds> is required');
|
|
145
|
+
if (!versionIds.length)
|
|
146
|
+
throw new Error('--versions <versionIds> is required');
|
|
147
|
+
const epochs = (0, ui_1.intFlag)(opts.epochs, '--epochs', 1);
|
|
148
|
+
if (epochs < 1 || epochs > 100)
|
|
149
|
+
throw new Error('--epochs must be between 1 and 100');
|
|
150
|
+
const res = await api.runChatSimulation({
|
|
151
|
+
chat_agent_id: chatAgentId,
|
|
152
|
+
test_case_ids: testIds,
|
|
153
|
+
chat_version_ids: versionIds,
|
|
154
|
+
batch_name: opts.name || `cku-run-${Date.now()}`,
|
|
155
|
+
epochs,
|
|
156
|
+
});
|
|
157
|
+
(0, ui_1.success)(`Chat simulation run started (${res.batch?._id || ''}, ${res.batch?.number_of_runs ?? '?'} runs). Results arrive asynchronously.`);
|
|
158
|
+
(0, ui_1.info)(`Check with: cku chat-sim results ${res.batch?._id || '<runId>'}`);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
(0, ui_1.handleError)(err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function chatSimRunsListCommand(chatAgentId, opts) {
|
|
165
|
+
const api = (0, session_1.requireApi)();
|
|
166
|
+
try {
|
|
167
|
+
const res = await api.listChatSimulationRuns(chatAgentId);
|
|
168
|
+
const rows = res.batches || [];
|
|
169
|
+
if (opts.json)
|
|
170
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
171
|
+
if (!rows.length)
|
|
172
|
+
return void (0, ui_1.info)('No chat simulation runs yet.');
|
|
173
|
+
(0, ui_1.table)(['NAME', 'ID', 'RUNS', 'CREATED'], rows.map((b) => [b.batch_name || '', b._id, String(b.number_of_runs ?? ''), b.createdAt || '']));
|
|
174
|
+
if (typeof res.totalCost === 'number')
|
|
175
|
+
console.log(`Total cost, all runs: ${res.totalCost > 0 ? res.totalCost.toFixed(3) : 'N/A'}`);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
(0, ui_1.handleError)(err);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function chatSimResultsCommand(runId, opts) {
|
|
182
|
+
const api = (0, session_1.requireApi)();
|
|
183
|
+
try {
|
|
184
|
+
const res = await api.chatSimulationResults(runId);
|
|
185
|
+
const rows = res.simulations || [];
|
|
186
|
+
if (opts.json)
|
|
187
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
188
|
+
if (!rows.length)
|
|
189
|
+
return void (0, ui_1.info)('No results yet (the run may still be in progress).');
|
|
190
|
+
// Names are best-effort; a deleted test case still shows by id.
|
|
191
|
+
let names = new Map();
|
|
192
|
+
try {
|
|
193
|
+
const tc = await api.listChatTestCases(res.batch?.chat_agent_id);
|
|
194
|
+
names = new Map((tc.testCases || []).map((t) => [t._id, t.test_case_name]));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// ids are enough
|
|
198
|
+
}
|
|
199
|
+
(0, ui_1.table)(['TEST CASE', 'VERSION', 'RESULT', 'RATING', 'FLAWS', 'ID'], rows.map((s) => {
|
|
200
|
+
const e = evaluationOf(s);
|
|
201
|
+
return [
|
|
202
|
+
names.get(s.test_case_id) || s.test_case_id || '',
|
|
203
|
+
s.chat_agent_version_id || '',
|
|
204
|
+
e.result,
|
|
205
|
+
e.rating === null ? '' : `${e.rating}/10`,
|
|
206
|
+
e.flaws.slice(0, 60),
|
|
207
|
+
s._id,
|
|
208
|
+
];
|
|
209
|
+
}));
|
|
210
|
+
const expected = res.batch?.number_of_runs;
|
|
211
|
+
if (expected && rows.length < expected)
|
|
212
|
+
(0, ui_1.info)(`${rows.length} of ${expected} runs finished; the rest are still running.`);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
(0, ui_1.handleError)(err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -205,8 +205,6 @@ async function assignDryRun(api, opts, phones, q) {
|
|
|
205
205
|
if (opts.workflow) {
|
|
206
206
|
const wf = await api.getWorkflow(opts.workflow);
|
|
207
207
|
const steps = wf.graph?.nodes?.length ?? 0;
|
|
208
|
-
if (wf.type && wf.type !== 'normal')
|
|
209
|
-
throw new Error('This workflow is deprecated and cannot take contacts.');
|
|
210
208
|
if (!steps)
|
|
211
209
|
throw new Error(`Workflow "${wf.workFlowName}" has no steps yet; the server would refuse this assign.`);
|
|
212
210
|
target = `workflow "${wf.workFlowName}" (${steps} nodes)`;
|
|
@@ -34,16 +34,25 @@ function toTestVariables(input) {
|
|
|
34
34
|
}
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* `testFunctions` is a list of `{name, mock?, response?}`. `mock: false` runs the agent's REAL
|
|
39
|
+
* function instead of the canned response (official 5.4.0); never for WhatsApp functions.
|
|
40
|
+
*/
|
|
38
41
|
function toTestFunctions(input) {
|
|
39
42
|
if (!Array.isArray(input)) {
|
|
40
|
-
throw new Error('--functions must be a JSON array of {"name": ..., "response"
|
|
43
|
+
throw new Error('--functions must be a JSON array of {"name": ..., "mock"?: bool, "response"?: ...}.');
|
|
41
44
|
}
|
|
42
45
|
return input.map((f, i) => {
|
|
43
|
-
if (!f || typeof f !== 'object' || typeof f.name !== 'string' || !f.name
|
|
44
|
-
throw new Error(`--functions[${i}] needs a "name" (string)
|
|
46
|
+
if (!f || typeof f !== 'object' || typeof f.name !== 'string' || !f.name) {
|
|
47
|
+
throw new Error(`--functions[${i}] needs a "name" (string).`);
|
|
48
|
+
}
|
|
49
|
+
if (f.mock !== undefined && typeof f.mock !== 'boolean') {
|
|
50
|
+
throw new Error(`--functions[${i}].mock must be true or false.`);
|
|
51
|
+
}
|
|
52
|
+
if (f.mock !== false && !('response' in f)) {
|
|
53
|
+
throw new Error(`--functions[${i}] needs a "response" (object or string), or "mock": false to run the real function.`);
|
|
45
54
|
}
|
|
46
|
-
return { name: f.name, response: f.response };
|
|
55
|
+
return { name: f.name, ...(f.mock === undefined ? {} : { mock: f.mock }), ...('response' in f ? { response: f.response } : {}) };
|
|
47
56
|
});
|
|
48
57
|
}
|
|
49
58
|
async function simTestsListCommand(agentId, opts) {
|
|
@@ -66,15 +66,39 @@ const HANGUP_REASONS = [
|
|
|
66
66
|
];
|
|
67
67
|
const NODE_REFERENCE = `Workflow graph: { "nodes": [...], "edges": [...] }
|
|
68
68
|
|
|
69
|
-
Every node: { id (unique), type, name, x, y }. Exactly one "start";
|
|
70
|
-
"
|
|
69
|
+
Every node: { id (unique), type, name, x, y }. Exactly one "start"; an "end" or
|
|
70
|
+
"cancel" must be reachable from it. At most one "inbound", the second entry
|
|
71
|
+
point. Nothing may lead into start or inbound, and nothing may follow end,
|
|
72
|
+
cancel or resume.
|
|
71
73
|
|
|
72
74
|
Node types — required fields, then a sample (x/y optional)
|
|
73
75
|
These are the only types; the dashboard's Add Node menu offers exactly these:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
Inbound Call = inbound · End = end · Cancel Workflow = cancel ·
|
|
77
|
+
Resume Where They Were = resume · Send Call = send-call ·
|
|
78
|
+
Send WhatsApp Template = wa-template · Send WhatsApp Message = wa-message ·
|
|
79
|
+
Wait = wait · Conditions = conditions · API Call — Basic = api-basic ·
|
|
80
|
+
API Call — Advanced = api-advanced · Update Contact = update-lead
|
|
81
|
+
(Start is created with the workflow, never added)
|
|
82
|
+
|
|
83
|
+
Inbound calls — the second entry point
|
|
84
|
+
When a lead calls in on a number this workflow has called them from, their
|
|
85
|
+
live run JUMPS to the inbound node from wherever it had reached, and carries
|
|
86
|
+
on down that branch. Conditions placed after it read THAT call.
|
|
87
|
+
What the branch leads to decides the journey they were on:
|
|
88
|
+
reaches cancel -> cancelled, and any scheduled call is dropped
|
|
89
|
+
reaches end -> completed
|
|
90
|
+
reaches resume -> back to the step the call interrupted (a wait keeps the
|
|
91
|
+
time it had left)
|
|
92
|
+
leads nowhere -> the same as resume. An inbound node with no outgoing
|
|
93
|
+
edge is valid: the call is recorded against the run and
|
|
94
|
+
the lead left exactly where they were.
|
|
95
|
+
|
|
96
|
+
inbound no fields; at most one per workflow; nothing may lead into it
|
|
97
|
+
{"id":"inbound-1","type":"inbound","name":"Inbound Call","x":320,"y":600}
|
|
98
|
+
cancel no fields; ends the run as "cancelled" and drops its queued calls
|
|
99
|
+
{"id":"cancel-1","type":"cancel","name":"Cancel Workflow","x":1600,"y":600}
|
|
100
|
+
resume no fields; needs an inbound node in the same workflow
|
|
101
|
+
{"id":"resume-1","type":"resume","name":"Resume Where They Were","x":1600,"y":760}
|
|
78
102
|
|
|
79
103
|
start {"id":"start-1","type":"start","name":"Start","x":320,"y":180}
|
|
80
104
|
end {"id":"end-1","type":"end","name":"End","x":2400,"y":180}
|
|
@@ -290,7 +314,7 @@ async function workflowsExportCommand(id, opts) {
|
|
|
290
314
|
type: 'callkaro_workflow',
|
|
291
315
|
version: 1,
|
|
292
316
|
exportedAt: new Date().toISOString(),
|
|
293
|
-
data: { workFlowName: wf.workFlowName,
|
|
317
|
+
data: { workFlowName: wf.workFlowName, graph: wf.graph ?? { nodes: [], edges: [] } },
|
|
294
318
|
};
|
|
295
319
|
if (opts.file) {
|
|
296
320
|
(0, fs_1.writeFileSync)(opts.file, JSON.stringify(payload, null, 2));
|
|
@@ -402,8 +426,6 @@ async function editGraph(id, apply) {
|
|
|
402
426
|
const api = (0, session_1.requireApi)();
|
|
403
427
|
try {
|
|
404
428
|
const wf = await api.getWorkflow(id);
|
|
405
|
-
if (wf.type && wf.type !== 'normal')
|
|
406
|
-
throw new Error('This workflow is deprecated and cannot be edited.');
|
|
407
429
|
const graph = {
|
|
408
430
|
nodes: JSON.parse(JSON.stringify(wf.graph?.nodes ?? [])),
|
|
409
431
|
edges: JSON.parse(JSON.stringify(wf.graph?.edges ?? [])),
|
package/dist/endpoints.json
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const chatSimulations_1 = require("./commands/chatSimulations");
|
|
8
|
+
const chatAuditStrategies_1 = require("./commands/chatAuditStrategies");
|
|
7
9
|
const commander_1 = require("commander");
|
|
8
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
11
|
const ui_1 = require("./lib/ui");
|
|
@@ -491,7 +493,7 @@ workflows.command('leads <id>').description('leads enrolled in a workflow and wh
|
|
|
491
493
|
workflows.command('lead <id> <phone>').description("one lead's call totals and enrolment time in a workflow").option('--json', 'output raw JSON').option('--file <path>', 'save JSON to a file').action(workflows_1.workflowsLeadCommand);
|
|
492
494
|
workflows.command('run <id> <phone>').description("a lead's latest run, step by step").option('--json', 'full detail incl. step results and variables').option('--file <path>', 'save JSON to a file').action(workflows_1.workflowsRunCommand);
|
|
493
495
|
workflows.command('journey <id> <phone>').description('every call to or from a number inside this workflow').option('--json', 'output raw JSON').option('--file <path>', 'save JSON to a file').action(workflows_1.workflowsJourneyCommand);
|
|
494
|
-
workflows.command('add-node <id>').description('add a node; with --after it is spliced into the chain after that node').requiredOption('--type <type>', 'send-call | wa-template | wa-message | wait | conditions | api-basic | api-advanced | update-lead | end').option('--name <name>', 'node name').option('--set <json>', 'extra fields as JSON or @file.json, e.g. \'{"minutes":5}\'').option('--after <nodeId>', 'insert after this node').option('--handle <handle>', 'which branch of --after (conditions exit id)').option('--x <n>', 'canvas x').option('--y <n>', 'canvas y').action(workflows_1.workflowsAddNodeCommand);
|
|
496
|
+
workflows.command('add-node <id>').description('add a node; with --after it is spliced into the chain after that node').requiredOption('--type <type>', 'inbound | send-call | wa-template | wa-message | wait | conditions | api-basic | api-advanced | update-lead | end | cancel | resume').option('--name <name>', 'node name').option('--set <json>', 'extra fields as JSON or @file.json, e.g. \'{"minutes":5}\'').option('--after <nodeId>', 'insert after this node').option('--handle <handle>', 'which branch of --after (conditions exit id)').option('--x <n>', 'canvas x').option('--y <n>', 'canvas y').action(workflows_1.workflowsAddNodeCommand);
|
|
495
497
|
workflows.command('set-node <id> <nodeId>').description("change a node's fields (merged; id and type are fixed)").option('--set <json>', 'fields as JSON or @file.json').option('--name <name>', 'rename').action(workflows_1.workflowsSetNodeCommand);
|
|
496
498
|
workflows.command('remove-node <id> <nodeId>').description('remove a node and its edges').option('--reconnect', 'link what led into it to what it led to').option('--yes', 'skip confirmation').action(workflows_1.workflowsRemoveNodeCommand);
|
|
497
499
|
workflows.command('connect <id>').description('link a node to another (replaces any existing link from that handle)').requiredOption('--from <nodeId>', 'source node').requiredOption('--to <nodeId>', 'target node').option('--handle <handle>', 'source branch (conditions exit id); default "out"').action(workflows_1.workflowsConnectCommand);
|
|
@@ -590,15 +592,15 @@ agents.command('chat <agentId>')
|
|
|
590
592
|
.option('--save <file>', 'write the conversation to a file at the end (readable by --history)')
|
|
591
593
|
.option('--show-prompt', 'print the rendered system prompt once (checks metadata substitution)')
|
|
592
594
|
.option('--json', 'print only the final conversation as JSON')
|
|
593
|
-
.addHelpText('after', `
|
|
594
|
-
Not mocked: any custom in-call function. It really runs (CRM write, webhook, WhatsApp).
|
|
595
|
-
Mocked: end_call, transfer_call, warm_transfer, keep_call_on_hold, use_knowledge_base,
|
|
596
|
-
change_conversation_language, send_booking_link_to_whatsapp, collect_digits_via_dtmf.
|
|
597
|
-
|
|
598
|
-
Examples:
|
|
599
|
-
cku agents chat <agentId>
|
|
600
|
-
cku agents chat <agentId> --var customer_name=Ravi --say "Hello?" --say "Not interested"
|
|
601
|
-
cku agents chat <agentId> --say "hi" --json > convo.json
|
|
595
|
+
.addHelpText('after', `
|
|
596
|
+
Not mocked: any custom in-call function. It really runs (CRM write, webhook, WhatsApp).
|
|
597
|
+
Mocked: end_call, transfer_call, warm_transfer, keep_call_on_hold, use_knowledge_base,
|
|
598
|
+
change_conversation_language, send_booking_link_to_whatsapp, collect_digits_via_dtmf.
|
|
599
|
+
|
|
600
|
+
Examples:
|
|
601
|
+
cku agents chat <agentId>
|
|
602
|
+
cku agents chat <agentId> --var customer_name=Ravi --say "Hello?" --say "Not interested"
|
|
603
|
+
cku agents chat <agentId> --say "hi" --json > convo.json
|
|
602
604
|
cku agents chat <agentId> --history @convo.json --say "What was that again?"`)
|
|
603
605
|
.action(agentChat_1.agentChatCommand);
|
|
604
606
|
agents.command('versions <agentId>').description("list an agent's versions (name, id, language, prompt type, active)")
|
|
@@ -938,7 +940,7 @@ sim.command('create <agentId>').description('create a test case')
|
|
|
938
940
|
.option('--criteria <text>', 'success criteria')
|
|
939
941
|
.option('--model <model>', 'test LLM model (default: platform default; see `cku sim models`)')
|
|
940
942
|
.option('--variables <json|@file>', 'testVariables: a JSON object of name -> value (non-strings are stringified)')
|
|
941
|
-
.option('--functions <json|@file>', '
|
|
943
|
+
.option('--functions <json|@file>', 'a JSON array of {"name":"fn","mock":true,"response":{}}; "mock":false runs the REAL function (never for WhatsApp functions)')
|
|
942
944
|
.action(simulations_1.simCreateCommand);
|
|
943
945
|
sim.command('update <testCaseId>').description('edit a test case (only the fields you pass are sent)')
|
|
944
946
|
.option('--name <name>', 'test case name')
|
|
@@ -947,7 +949,7 @@ sim.command('update <testCaseId>').description('edit a test case (only the field
|
|
|
947
949
|
.option('--criteria <text>', 'success criteria')
|
|
948
950
|
.option('--model <model>', 'test LLM model')
|
|
949
951
|
.option('--variables <json|@file>', 'testVariables: replaces the whole map')
|
|
950
|
-
.option('--functions <json|@file>', 'function
|
|
952
|
+
.option('--functions <json|@file>', 'function list, replaces the whole list; entries are {name, mock?, response?}, "mock":false runs the REAL function')
|
|
951
953
|
.action(simulations_1.simUpdateCommand);
|
|
952
954
|
sim.command('models').description('models that can play the simulated caller').option('--json', 'raw JSON').action(simulations_1.simModelsCommand);
|
|
953
955
|
sim.command('delete <testCaseId>').description('delete a test case').option('--yes', 'skip confirmation')
|
|
@@ -979,6 +981,71 @@ simVars.command('create <agentId> <name>').description('save a variable set')
|
|
|
979
981
|
simVars.command('delete <agentId> <nameOrId>').description('delete a saved set').option('--yes', 'skip confirmation')
|
|
980
982
|
.action(simVariables_1.simVarsDeleteCommand);
|
|
981
983
|
// --- audit strategies ------------------------------------------------------
|
|
984
|
+
const chatSim = program.command('chat-sim').description('chat agent simulations & test cases');
|
|
985
|
+
chatSim.command('tests <chatAgentId>').description('list chat test cases').option('--json', 'raw JSON').action(chatSimulations_1.chatSimTestsListCommand);
|
|
986
|
+
chatSim.command('create <chatAgentId>').description('create a chat test case')
|
|
987
|
+
.option('--name <name>', 'test case name')
|
|
988
|
+
.option('--persona <text>', 'who the simulated user is')
|
|
989
|
+
.option('--prompt <text>', 'simulated user prompt')
|
|
990
|
+
.option('--criteria <text>', 'success criteria')
|
|
991
|
+
.option('--model <model>', 'model that plays the user (default: chat default; see `cku chat-agents models`)')
|
|
992
|
+
.option('--secondary-model <model>', 'fallback model if the primary fails to reply')
|
|
993
|
+
.option('--mocks <json|@file>', 'custom-function mocks: [{"name":"fn","response":{},"enabled":true}]; "enabled":false runs the real function')
|
|
994
|
+
.action(chatSimulations_1.chatSimCreateCommand);
|
|
995
|
+
chatSim.command('update <testCaseId>').description('update a chat test case (only the flags you pass change)')
|
|
996
|
+
.option('--name <name>', 'test case name')
|
|
997
|
+
.option('--persona <text>', 'who the simulated user is')
|
|
998
|
+
.option('--prompt <text>', 'simulated user prompt')
|
|
999
|
+
.option('--criteria <text>', 'success criteria')
|
|
1000
|
+
.option('--model <model>', 'model that plays the user')
|
|
1001
|
+
.option('--secondary-model <model>', 'fallback model')
|
|
1002
|
+
.option('--mocks <json|@file>', 'custom-function mocks, replaces the whole list; "enabled":false runs the real function')
|
|
1003
|
+
.action(chatSimulations_1.chatSimUpdateCommand);
|
|
1004
|
+
chatSim.command('delete <testCaseId>').description('delete a chat test case').option('--yes', 'skip confirmation').action(chatSimulations_1.chatSimDeleteCommand);
|
|
1005
|
+
chatSim.command('run <chatAgentId>').description('run a chat simulation batch (async)')
|
|
1006
|
+
.option('--tests <ids>', 'comma-separated test case ids')
|
|
1007
|
+
.option('--versions <ids>', 'comma-separated chat agent version ids')
|
|
1008
|
+
.option('--name <name>', 'run name')
|
|
1009
|
+
.option('--epochs <n>', 'times to run each test/version pair (default 1, max 100)')
|
|
1010
|
+
.action(chatSimulations_1.chatSimRunCommand);
|
|
1011
|
+
chatSim.command('runs <chatAgentId>').description('list chat simulation runs').option('--json', 'raw JSON').action(chatSimulations_1.chatSimRunsListCommand);
|
|
1012
|
+
chatSim.command('results <runId>').description('results of a chat simulation run').option('--json', 'raw JSON').action(chatSimulations_1.chatSimResultsCommand);
|
|
1013
|
+
const chatAudit = program.command('chat-audit-strategies').alias('chat-audit')
|
|
1014
|
+
.description('AI Auditor strategies for WhatsApp chat agents');
|
|
1015
|
+
chatAudit.command('list <agentId>').description('list chat audit strategies for a WhatsApp agent').option('--json', 'raw JSON').action(chatAuditStrategies_1.chatAuditListCommand);
|
|
1016
|
+
chatAudit.command('get <agentId> <strategyId>').description('show one chat audit strategy').option('--json', 'raw JSON').action(chatAuditStrategies_1.chatAuditGetCommand);
|
|
1017
|
+
chatAudit.command('models <agentId>').description('list models chat audits may run on').option('--json', 'raw JSON').action(chatAuditStrategies_1.chatAuditModelsCommand);
|
|
1018
|
+
chatAudit.command('create <agentId>').description('create a chat audit strategy')
|
|
1019
|
+
.option('--name <name>', 'strategy name')
|
|
1020
|
+
.option('--strategy <text>', 'free-text audit instructions for the LLM auditor (required)')
|
|
1021
|
+
.option('--active', 'activate immediately (default: inactive)')
|
|
1022
|
+
.option('--model <name>', 'audit model, from `cku chat-audit models <agentId>`')
|
|
1023
|
+
.option('--pct-per-day <n>', 'percent of eligible conversations audited each day')
|
|
1024
|
+
.option('--max-per-day <n>', 'max conversations audited per day, 0 = no cap (set this or --pct-per-day)')
|
|
1025
|
+
.option('--min-user-messages <n>', 'skip conversations with fewer customer messages (default 1)')
|
|
1026
|
+
.option('--min-messages <n>', 'skip conversations with fewer total messages (default 2)')
|
|
1027
|
+
.option('--set <json|@file>', 'JSON for audit_filters/extra_variables/llms; attribute names/values: `cku contacts attributes list`')
|
|
1028
|
+
.action(chatAuditStrategies_1.chatAuditCreateCommand);
|
|
1029
|
+
chatAudit.command('update <agentId> <strategyId>').description('update a chat audit strategy (only named fields change)')
|
|
1030
|
+
.option('--name <name>', 'strategy name')
|
|
1031
|
+
.option('--strategy <text>', 'free-text audit instructions for the LLM auditor')
|
|
1032
|
+
.option('--active', 'activate the strategy')
|
|
1033
|
+
.option('--disable', 'deactivate the strategy')
|
|
1034
|
+
.option('--model <name>', 'audit model, from `cku chat-audit models <agentId>`')
|
|
1035
|
+
.option('--pct-per-day <n>', 'percent of eligible conversations audited each day')
|
|
1036
|
+
.option('--max-per-day <n>', 'max conversations audited per day, 0 = no cap')
|
|
1037
|
+
.option('--min-user-messages <n>', 'skip conversations with fewer customer messages')
|
|
1038
|
+
.option('--min-messages <n>', 'skip conversations with fewer total messages')
|
|
1039
|
+
.option('--set <json|@file>', 'JSON for audit_filters/extra_variables/llms; each replaces the stored value')
|
|
1040
|
+
.action(chatAuditStrategies_1.chatAuditUpdateCommand);
|
|
1041
|
+
chatAudit.command('delete <strategyId>').description('delete a chat audit strategy').action(chatAuditStrategies_1.chatAuditDeleteCommand);
|
|
1042
|
+
chatAudit.command('batch <agentId> <strategyId>')
|
|
1043
|
+
.description('audit up to N conversations now (uses credits; only messages since each last audit)')
|
|
1044
|
+
.requiredOption('--max <n>', 'number of conversations to audit (1-500)')
|
|
1045
|
+
.option('--phone <number>', "audit only this customer's number, with country code")
|
|
1046
|
+
.option('--model <name>', 'audit model for this batch only')
|
|
1047
|
+
.option('--set <json|@file>', 'JSON with filters/minUserMessages/minMessages')
|
|
1048
|
+
.action(chatAuditStrategies_1.chatAuditBatchCommand);
|
|
982
1049
|
const audit = program.command('audit-strategies').alias('audit').description('AI Auditor audit strategies');
|
|
983
1050
|
audit.command('list <agentId>').description('list audit strategies for an agent').option('--json', 'raw JSON')
|
|
984
1051
|
.action(auditStrategies_1.auditListCommand);
|
|
@@ -1011,18 +1078,18 @@ audit.command('update <agentId> <strategyId>').description('update an audit stra
|
|
|
1011
1078
|
.option('--retranscribe', 'retranscribe audio before auditing')
|
|
1012
1079
|
.option('--set <json>', 'JSON (or @file.json) for audit_filters/postcall_filters/extra_variables/llms — merged onto the existing strategy')
|
|
1013
1080
|
.action(auditStrategies_1.auditUpdateCommand);
|
|
1014
|
-
program.addHelpText('after', `
|
|
1015
|
-
${chalk_1.default.bold('Examples:')}
|
|
1016
|
-
${chalk_1.default.cyan(`${name} register`)} sign up and log in
|
|
1017
|
-
${chalk_1.default.cyan(`${name} login`)} log into an existing account
|
|
1018
|
-
${chalk_1.default.cyan(`${name} accounts`)} list saved logins (* = active)
|
|
1019
|
-
${chalk_1.default.cyan(`${name} accounts use <email>`)} switch account
|
|
1020
|
-
${chalk_1.default.cyan(`${name} whoami`)} see who you're logged in as
|
|
1021
|
-
${chalk_1.default.cyan(`${name} config`)} show settings and session status
|
|
1022
|
-
${chalk_1.default.cyan(`${name} agents list`)} list your agents in a table
|
|
1023
|
-
${chalk_1.default.cyan(`${name} agents list --json`)} output raw JSON
|
|
1024
|
-
${chalk_1.default.cyan(`${name} agents update <agentId> --set '{"name":"New"}' --commit "rename"`)}
|
|
1025
|
-
${chalk_1.default.cyan(`${name} calls make --agent <id> --to <number> --test`)} place a test call
|
|
1081
|
+
program.addHelpText('after', `
|
|
1082
|
+
${chalk_1.default.bold('Examples:')}
|
|
1083
|
+
${chalk_1.default.cyan(`${name} register`)} sign up and log in
|
|
1084
|
+
${chalk_1.default.cyan(`${name} login`)} log into an existing account
|
|
1085
|
+
${chalk_1.default.cyan(`${name} accounts`)} list saved logins (* = active)
|
|
1086
|
+
${chalk_1.default.cyan(`${name} accounts use <email>`)} switch account
|
|
1087
|
+
${chalk_1.default.cyan(`${name} whoami`)} see who you're logged in as
|
|
1088
|
+
${chalk_1.default.cyan(`${name} config`)} show settings and session status
|
|
1089
|
+
${chalk_1.default.cyan(`${name} agents list`)} list your agents in a table
|
|
1090
|
+
${chalk_1.default.cyan(`${name} agents list --json`)} output raw JSON
|
|
1091
|
+
${chalk_1.default.cyan(`${name} agents update <agentId> --set '{"name":"New"}' --commit "rename"`)}
|
|
1092
|
+
${chalk_1.default.cyan(`${name} calls make --agent <id> --to <number> --test`)} place a test call
|
|
1026
1093
|
`);
|
|
1027
1094
|
// Bare invocation → show help instead of an error.
|
|
1028
1095
|
if (process.argv.length <= 2) {
|
package/dist/lib/agentUpdate.js
CHANGED
|
@@ -33,7 +33,7 @@ exports.REPLACE_WHOLESALE_KEYS = new Set([
|
|
|
33
33
|
/** String-array fields the web trims + drops empties on; we match that. */
|
|
34
34
|
exports.STRING_ARRAY_KEYS = new Set([
|
|
35
35
|
'end_call_msg', 'instructions', 'guardrails', 'rebuttals', 'silence_prompts',
|
|
36
|
-
'
|
|
36
|
+
'substrings_to_remove',
|
|
37
37
|
]);
|
|
38
38
|
/**
|
|
39
39
|
* Which live catalogue list governs which field.
|
|
@@ -125,10 +125,10 @@ function validateCallSettings(obj) {
|
|
|
125
125
|
throw new Error('Invalid value for "noise_cancellation_strength": expected a number between 0.05 and 1');
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
if ('
|
|
129
|
-
const punctuations = obj.
|
|
128
|
+
if ('substrings_to_remove' in obj) {
|
|
129
|
+
const punctuations = obj.substrings_to_remove;
|
|
130
130
|
if (!Array.isArray(punctuations) || punctuations.some((v) => typeof v !== 'string')) {
|
|
131
|
-
throw new Error('Invalid value for "
|
|
131
|
+
throw new Error('Invalid value for "substrings_to_remove": expected an array of strings');
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
if ('webhook_headers' in obj) {
|
package/dist/lib/api.js
CHANGED
|
@@ -99,7 +99,7 @@ class Api {
|
|
|
99
99
|
return (await this.request(method, `/cli/workflows${path}`, body)).data;
|
|
100
100
|
}
|
|
101
101
|
listWorkflows(search) {
|
|
102
|
-
const q = new URLSearchParams({ limit: '100'
|
|
102
|
+
const q = new URLSearchParams({ limit: '100' });
|
|
103
103
|
if (search)
|
|
104
104
|
q.set('search', search);
|
|
105
105
|
return this.workflow('GET', `?${q}`);
|
|
@@ -396,6 +396,32 @@ class Api {
|
|
|
396
396
|
updateAuditStrategy(strategyId, body) {
|
|
397
397
|
return this.request('PUT', `/cli/audit-strategies/${strategyId}`, body);
|
|
398
398
|
}
|
|
399
|
+
// --- chat simulations -------------------------------------------------------
|
|
400
|
+
createChatTestCase(body) { return this.request('POST', '/cli/chat-simulations/test-cases', body); }
|
|
401
|
+
listChatTestCases(chatAgentId) {
|
|
402
|
+
return this.request('GET', `/cli/chat-simulations/test-cases${qs({ chat_agent_id: chatAgentId })}`);
|
|
403
|
+
}
|
|
404
|
+
updateChatTestCase(id, body) {
|
|
405
|
+
return this.request('PUT', `/cli/chat-simulations/test-cases/${id}`, body);
|
|
406
|
+
}
|
|
407
|
+
deleteChatTestCase(id) { return this.request('DELETE', `/cli/chat-simulations/test-cases/${id}`); }
|
|
408
|
+
runChatSimulation(body) { return this.request('POST', '/cli/chat-simulations/runs', body); }
|
|
409
|
+
listChatSimulationRuns(chatAgentId) {
|
|
410
|
+
return this.request('GET', `/cli/chat-simulations/runs${qs({ chat_agent_id: chatAgentId })}`);
|
|
411
|
+
}
|
|
412
|
+
chatSimulationResults(batchId) { return this.request('GET', `/cli/chat-simulations/runs/${batchId}`); }
|
|
413
|
+
// --- chat audit strategies (WhatsApp AI Auditor) ------------------------------
|
|
414
|
+
listChatAuditStrategies(agentId) {
|
|
415
|
+
return this.request('GET', `/cli/chat-audit-strategies${qs({ agentId })}`);
|
|
416
|
+
}
|
|
417
|
+
createChatAuditStrategy(body) { return this.request('POST', '/cli/chat-audit-strategies', body); }
|
|
418
|
+
updateChatAuditStrategy(strategyId, body) {
|
|
419
|
+
return this.request('PUT', `/cli/chat-audit-strategies/${strategyId}`, body);
|
|
420
|
+
}
|
|
421
|
+
deleteChatAuditStrategy(strategyId) {
|
|
422
|
+
return this.request('DELETE', `/cli/chat-audit-strategies/${strategyId}`);
|
|
423
|
+
}
|
|
424
|
+
createChatAuditBatch(body) { return this.request('POST', '/cli/chat-audit-strategies/batches', body); }
|
|
399
425
|
}
|
|
400
426
|
exports.Api = Api;
|
|
401
427
|
/** Build a `?a=1&b=2` query string, skipping undefined/empty values. */
|
package/dist/lib/workflowEdit.js
CHANGED
|
@@ -15,9 +15,20 @@ exports.disconnect = disconnect;
|
|
|
15
15
|
*/
|
|
16
16
|
const DEFAULT_HANDLE = 'out';
|
|
17
17
|
const X_STEP = 320;
|
|
18
|
+
// Mirrors ENTRY_TYPES / BRANCH_END_TYPES in the server's utils/workflowGraph.js.
|
|
19
|
+
// Nothing may lead into an entry point, and nothing may follow a branch end
|
|
20
|
+
// (resume included: it does not stop the run but does finish the branch).
|
|
21
|
+
const ENTRY_TYPES = ['start', 'inbound'];
|
|
22
|
+
const BRANCH_END_TYPES = ['end', 'cancel', 'resume'];
|
|
23
|
+
// One per workflow. Start is seeded and so is never added.
|
|
24
|
+
const SINGLETON_TYPES = ['start', 'inbound'];
|
|
25
|
+
const stopsBecause = (type) => type === 'resume' ? 'the lead goes back to where they were' : 'a run stops there';
|
|
18
26
|
// Mirrors NODE_META in the dashboard's WorkflowCanvas.jsx.
|
|
19
27
|
const DEFAULT_NAMES = {
|
|
28
|
+
inbound: 'Inbound Call',
|
|
20
29
|
end: 'End',
|
|
30
|
+
cancel: 'Cancel Workflow',
|
|
31
|
+
resume: 'Resume Where They Were',
|
|
21
32
|
'send-call': 'Send Call',
|
|
22
33
|
'wa-template': 'WhatsApp Template',
|
|
23
34
|
'wa-message': 'WhatsApp Message',
|
|
@@ -68,7 +79,11 @@ const nodeOf = (g, id) => {
|
|
|
68
79
|
throw new Error(`No node with id "${id}". Ids: ${g.nodes.map((n) => n.id).join(', ')}`);
|
|
69
80
|
return node;
|
|
70
81
|
};
|
|
71
|
-
const handlesOf = (node) =>
|
|
82
|
+
const handlesOf = (node) => {
|
|
83
|
+
if (BRANCH_END_TYPES.includes(node.type))
|
|
84
|
+
return [];
|
|
85
|
+
return node.type === 'conditions' ? (node.exits || []).map((e) => e.id) : [DEFAULT_HANDLE];
|
|
86
|
+
};
|
|
72
87
|
exports.handlesOf = handlesOf;
|
|
73
88
|
/** The handle to use when the caller named none: the only one, or an error listing the choices. */
|
|
74
89
|
function resolveHandle(node, handle) {
|
|
@@ -94,9 +109,19 @@ function addNode(g, opts) {
|
|
|
94
109
|
if (!exports.ADDABLE_TYPES.includes(opts.type)) {
|
|
95
110
|
throw new Error(`--type must be one of: ${exports.ADDABLE_TYPES.join(', ')} (a workflow has exactly one start, which already exists)`);
|
|
96
111
|
}
|
|
112
|
+
if (SINGLETON_TYPES.includes(opts.type) && g.nodes.some((n) => n.type === opts.type)) {
|
|
113
|
+
throw new Error(`A workflow can only have one ${DEFAULT_NAMES[opts.type]} node.`);
|
|
114
|
+
}
|
|
115
|
+
if (opts.type === 'resume' && !g.nodes.some((n) => n.type === 'inbound')) {
|
|
116
|
+
throw new Error('A resume node has nothing to resume: add an inbound node first.');
|
|
117
|
+
}
|
|
97
118
|
const anchor = opts.after ? nodeOf(g, opts.after) : undefined;
|
|
98
|
-
if (anchor
|
|
99
|
-
throw new Error(`Nothing can follow "${anchor.id}":
|
|
119
|
+
if (anchor && BRANCH_END_TYPES.includes(anchor.type)) {
|
|
120
|
+
throw new Error(`Nothing can follow "${anchor.id}": ${stopsBecause(anchor.type)}.`);
|
|
121
|
+
}
|
|
122
|
+
if (ENTRY_TYPES.includes(opts.type) && opts.after) {
|
|
123
|
+
throw new Error(`Nothing can lead into a ${DEFAULT_NAMES[opts.type]} node: a run begins there. Add it without --after.`);
|
|
124
|
+
}
|
|
100
125
|
const afterHandle = anchor ? resolveHandle(anchor, opts.handle) : undefined;
|
|
101
126
|
const id = `${opts.type}-${stamp()}`;
|
|
102
127
|
const node = {
|
|
@@ -121,8 +146,8 @@ function addNode(g, opts) {
|
|
|
121
146
|
const previous = edgeFrom(g, anchor.id, afterHandle)?.to;
|
|
122
147
|
setEdge(g, anchor.id, afterHandle, id);
|
|
123
148
|
if (previous) {
|
|
124
|
-
if (opts.type
|
|
125
|
-
note = ` "${previous}" is no longer connected (
|
|
149
|
+
if (BRANCH_END_TYPES.includes(opts.type)) {
|
|
150
|
+
note = ` "${previous}" is no longer connected (${stopsBecause(opts.type)}).`;
|
|
126
151
|
}
|
|
127
152
|
else {
|
|
128
153
|
const exits = node.exits || [];
|
|
@@ -164,10 +189,12 @@ function removeNode(g, nodeId, reconnect = false) {
|
|
|
164
189
|
function connect(g, from, to, handle) {
|
|
165
190
|
const source = nodeOf(g, from);
|
|
166
191
|
const target = nodeOf(g, to);
|
|
167
|
-
if (source.type
|
|
168
|
-
throw new Error(`Nothing can follow "${from}":
|
|
169
|
-
|
|
170
|
-
|
|
192
|
+
if (BRANCH_END_TYPES.includes(source.type)) {
|
|
193
|
+
throw new Error(`Nothing can follow "${from}": ${stopsBecause(source.type)}.`);
|
|
194
|
+
}
|
|
195
|
+
if (ENTRY_TYPES.includes(target.type)) {
|
|
196
|
+
throw new Error(`Nothing can lead into "${to}": a run begins there.`);
|
|
197
|
+
}
|
|
171
198
|
const h = resolveHandle(source, handle);
|
|
172
199
|
const replaced = edgeFrom(g, from, h)?.to;
|
|
173
200
|
setEdge(g, from, h, to);
|