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 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
- /** `testFunctions` is a list of `{name, response}` mocks; `response` is an object or a string. */
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": ...} mocks.');
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 || !('response' in f)) {
44
- throw new Error(`--functions[${i}] needs a "name" (string) and a "response" (object or 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"; at least one
70
- "end" must be reachable from it. Nothing may lead into start or out of an end.
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
- End = end · Send Call = send-call · Send WhatsApp Template = wa-template ·
75
- Send WhatsApp Message = wa-message · Wait = wait · Conditions = conditions ·
76
- API Call — Basic = api-basic · API Call — Advanced = api-advanced ·
77
- Update Contact = update-lead (Start is created with the workflow, never added)
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, type: 'normal', graph: wf.graph ?? { nodes: [], edges: [] } },
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 ?? [])),
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>', 'function mocks: a JSON array of {"name": ..., "response": ...}')
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 mocks: replaces the whole list')
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) {
@@ -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
- 'punctuations_to_remove',
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 ('punctuations_to_remove' in obj) {
129
- const punctuations = obj.punctuations_to_remove;
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 "punctuations_to_remove": expected an array of strings');
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', type: 'normal' });
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. */
@@ -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) => node.type === 'conditions' ? (node.exits || []).map((e) => e.id) : [DEFAULT_HANDLE];
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?.type === 'end')
99
- throw new Error(`Nothing can follow "${anchor.id}": a run stops at an End node.`);
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 === 'end') {
125
- note = ` "${previous}" is no longer connected (an End has nothing after it).`;
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 === 'end')
168
- throw new Error(`Nothing can follow "${from}": a run stops at an End node.`);
169
- if (target.type === 'start')
170
- throw new Error(`Nothing can lead into "${to}": a run begins at Start.`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callkaro-cli-unofficial",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Unofficial fork of the CallKaro CLI — manage your voice AI agents from the terminal",
6
6
  "bin": {