callkaro-cli-unofficial 0.4.0 → 0.5.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 +276 -264
- package/dist/commands/contacts.js +378 -0
- package/dist/commands/simulations.js +45 -9
- package/dist/commands/update.js +13 -6
- package/dist/commands/workflows.js +458 -0
- package/dist/index.js +100 -1
- package/dist/lib/api.js +109 -1
- package/dist/lib/workflowEdit.js +183 -0
- package/package.json +1 -1
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.workflowsSchemaCommand = workflowsSchemaCommand;
|
|
37
|
+
exports.workflowsListCommand = workflowsListCommand;
|
|
38
|
+
exports.workflowsGetCommand = workflowsGetCommand;
|
|
39
|
+
exports.workflowsCreateCommand = workflowsCreateCommand;
|
|
40
|
+
exports.workflowsImportCommand = workflowsImportCommand;
|
|
41
|
+
exports.workflowsUpdateCommand = workflowsUpdateCommand;
|
|
42
|
+
exports.workflowsValidateCommand = workflowsValidateCommand;
|
|
43
|
+
exports.workflowsExportCommand = workflowsExportCommand;
|
|
44
|
+
exports.workflowsDuplicateCommand = workflowsDuplicateCommand;
|
|
45
|
+
exports.workflowsDeleteCommand = workflowsDeleteCommand;
|
|
46
|
+
exports.workflowsLeadsCommand = workflowsLeadsCommand;
|
|
47
|
+
exports.workflowsLeadCommand = workflowsLeadCommand;
|
|
48
|
+
exports.workflowsRunCommand = workflowsRunCommand;
|
|
49
|
+
exports.workflowsJourneyCommand = workflowsJourneyCommand;
|
|
50
|
+
exports.workflowsAddNodeCommand = workflowsAddNodeCommand;
|
|
51
|
+
exports.workflowsSetNodeCommand = workflowsSetNodeCommand;
|
|
52
|
+
exports.workflowsRemoveNodeCommand = workflowsRemoveNodeCommand;
|
|
53
|
+
exports.workflowsConnectCommand = workflowsConnectCommand;
|
|
54
|
+
exports.workflowsDisconnectCommand = workflowsDisconnectCommand;
|
|
55
|
+
const fs_1 = require("fs");
|
|
56
|
+
const session_1 = require("../lib/session");
|
|
57
|
+
const ui_1 = require("../lib/ui");
|
|
58
|
+
const edit = __importStar(require("../lib/workflowEdit"));
|
|
59
|
+
const jsonInput_1 = require("./jsonInput");
|
|
60
|
+
const HANGUP_REASONS = [
|
|
61
|
+
'CLIENT_INITIATED', 'PARTICIPANT_REMOVED', 'SESSION_TIMEOUT', 'VOICEMAIL_DETECTED', 'USER_REJECTED',
|
|
62
|
+
'USER_UNAVAILABLE', 'USER_UNRESPONSIVE', 'INVALID_PHONE_NUMBER', 'UNKNOWN', 'OUTSIDE_TRIGGER_WINDOW',
|
|
63
|
+
'DELETED_SCH_CALL_ALL', 'DELETED_SCH_CALL_ONE', 'NO_SLOTS_AVAILABLE', 'PRE_CALL_FN_FAILED', 'DUPLICATE_IDENTITY',
|
|
64
|
+
'SERVER_SHUTDOWN', 'ROOM_DELETED', 'STATE_MISMATCH', 'JOIN_FAILURE', 'MIGRATION', 'SIGNAL_CLOSE', 'ROOM_CLOSED',
|
|
65
|
+
'SIP_TRUNK_FAILURE',
|
|
66
|
+
];
|
|
67
|
+
const NODE_REFERENCE = `Workflow graph: { "nodes": [...], "edges": [...] }
|
|
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.
|
|
71
|
+
|
|
72
|
+
Node types — required fields, then a sample (x/y optional)
|
|
73
|
+
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)
|
|
78
|
+
|
|
79
|
+
start {"id":"start-1","type":"start","name":"Start","x":320,"y":180}
|
|
80
|
+
end {"id":"end-1","type":"end","name":"End","x":2400,"y":180}
|
|
81
|
+
|
|
82
|
+
send-call agentId; optional retries, retryGapMinutes, retryGaps[]
|
|
83
|
+
{"id":"call-1","type":"send-call","name":"Send Call","agentId":"6ab2c0aa04d91efaa62cebba","retries":0,"retryGapMinutes":30,"retryGaps":[]}
|
|
84
|
+
|
|
85
|
+
wait minutes (>= 1)
|
|
86
|
+
{"id":"wait-1","type":"wait","name":"Wait","minutes":2}
|
|
87
|
+
|
|
88
|
+
wa-template templateId, templateName, languageCode, templateBody, parameters[],
|
|
89
|
+
phoneNumberId, displayPhoneNumber, provider ("whatsapp")
|
|
90
|
+
{"id":"wat-1","type":"wa-template","name":"WhatsApp Template","provider":"whatsapp","templateId":"2167373513730912","templateName":"hello_world","languageCode":"en_US","templateBody":"Welcome!","parameters":[],"phoneNumberId":"1251245231415392","displayPhoneNumber":"917943495456"}
|
|
91
|
+
|
|
92
|
+
wa-message message, phoneNumberId, displayPhoneNumber, parameters[], provider
|
|
93
|
+
{"id":"wam-1","type":"wa-message","name":"WhatsApp Message","provider":"whatsapp","phoneNumberId":"1251245231415392","displayPhoneNumber":"917943495456","message":"Hello, how are you?","parameters":[]}
|
|
94
|
+
|
|
95
|
+
api-basic url (+ optional method, headers, parameters, bodyFields)
|
|
96
|
+
{"id":"api-1","type":"api-basic","name":"API Call","method":"POST","url":"https://example.com/hook"}
|
|
97
|
+
|
|
98
|
+
api-advanced code (one function; everything is on "context")
|
|
99
|
+
{"id":"fn-1","type":"api-advanced","name":"Custom Function","code":"async function handler(context) {\\n // context.call_metadata.lead_id\\n}"}
|
|
100
|
+
|
|
101
|
+
update-lead updates: [{ attribute, source, value }] (source as for conditions;
|
|
102
|
+
static_value takes the literal in "value")
|
|
103
|
+
{"id":"upd-1","type":"update-lead","name":"Update Lead","updates":[{"attribute":"age","source":"static_value","value":"20"}]}
|
|
104
|
+
|
|
105
|
+
conditions exits: [{ id, label, next:"", condition?: { source, field, op, value } }]
|
|
106
|
+
the exit WITHOUT a condition is the fallback (label "otherwise");
|
|
107
|
+
each exit id is the fromHandle of its edge
|
|
108
|
+
{"id":"cond-1","type":"conditions","name":"Conditions","exits":[
|
|
109
|
+
{"id":"o_1","label":"long call","next":"","condition":{"source":"call_duration","field":"","op":"gt","value":"5"}},
|
|
110
|
+
{"id":"o_2","label":"otherwise","next":""}]}
|
|
111
|
+
|
|
112
|
+
Edges: { id, from, fromHandle, to }
|
|
113
|
+
fromHandle is "out" for every node except conditions, where it is the exit's id.
|
|
114
|
+
A handle with no edge is legal (the run ends there).
|
|
115
|
+
One edge per (from, fromHandle); "to" is a node id.
|
|
116
|
+
{"id":"e1","from":"start-1","fromHandle":"out","to":"wait-1"}
|
|
117
|
+
{"id":"e2","from":"cond-1","fromHandle":"o_1","to":"wam-1"} (conditions exit "o_1")
|
|
118
|
+
{"id":"e3","from":"cond-1","fromHandle":"o_2","to":"end-1"} (conditions exit "o_2")
|
|
119
|
+
|
|
120
|
+
Unique ids: every node id must be unique across the whole graph (duplicates are
|
|
121
|
+
rejected). Keep edge ids and conditions exit ids unique too — a type-timestamp
|
|
122
|
+
pattern works: "wait-1791233332977", "edge-1791233336426", "o_muvq4qid1".
|
|
123
|
+
Edges refer to nodes by these ids, so never rename an id without updating its edges.
|
|
124
|
+
|
|
125
|
+
Condition ops: eq ne contains gt lt empty not_empty
|
|
126
|
+
Condition sources: contact post_call_variables call_metadata fn_response
|
|
127
|
+
static_value (need "field")
|
|
128
|
+
phone_number connected_call voicemail hangup_reason
|
|
129
|
+
call_duration conversion_status (no field)
|
|
130
|
+
|
|
131
|
+
wa-template "parameters": one row per template variable, { key, source, value }.
|
|
132
|
+
key is ORIGIN::token, e.g. "BODY::{{1}}", "HEADER::{{1}}", "HEADER::MEDIA",
|
|
133
|
+
"BUTTON_URL[0]::{{1}}". source is as for conditions (contact -> value is a CRM
|
|
134
|
+
attribute name; static_value -> value is the literal). Media rows use static_value.
|
|
135
|
+
{"key":"BODY::{{1}}","source":"contact","value":"first_name"}
|
|
136
|
+
|
|
137
|
+
hangup_reason values: ${HANGUP_REASONS.join(' ')}
|
|
138
|
+
conversion_status: compare with "true" (converted) or "false" (not converted).
|
|
139
|
+
post_call_variables: field = the agent's post-call variable name: the "name" of each entry in
|
|
140
|
+
postcall[] (skip type call_drop_off_reason). postcall is on the VERSION: take publishedVersionId
|
|
141
|
+
from cku agents get <agentId> --json then cku agents get <agentId> --versions <versionId> --json
|
|
142
|
+
fn_response: field = "<apiNodeId>.returned.<key>", e.g. "api-basic-1791233000000.returned.data.id";
|
|
143
|
+
the keys under "returned" are whatever that API/function returns.
|
|
144
|
+
A node's own result, "<apiNodeId>.success", says whether it ran without error.
|
|
145
|
+
|
|
146
|
+
Real ids -- never invent them:
|
|
147
|
+
agentId cku agents list
|
|
148
|
+
phoneNumberId / display cku whatsapp list
|
|
149
|
+
templateId, name, language cku chat-agents templates (cku chat-agents template <id> prints
|
|
150
|
+
the exact variables[].id to use as parameters[].key)
|
|
151
|
+
CRM attribute names cku contacts attributes list
|
|
152
|
+
Check a file with: cku workflows validate --file graph.json
|
|
153
|
+
Real example: cku workflows get <id> --json`;
|
|
154
|
+
/** Accepts a bare graph, { graph }, or an exported { data: { graph } } file. */
|
|
155
|
+
function graphFrom(parsed) {
|
|
156
|
+
const graph = parsed?.data?.graph ?? parsed?.graph ?? parsed;
|
|
157
|
+
if (!graph || !Array.isArray(graph.nodes)) {
|
|
158
|
+
throw new Error('Expected a graph with a "nodes" array (and "edges"). See `cku workflows schema`.');
|
|
159
|
+
}
|
|
160
|
+
return { nodes: graph.nodes, edges: Array.isArray(graph.edges) ? graph.edges : [] };
|
|
161
|
+
}
|
|
162
|
+
async function readJson(arg, file) {
|
|
163
|
+
const input = await (0, jsonInput_1.resolveJsonInput)(arg, file);
|
|
164
|
+
try {
|
|
165
|
+
return (0, jsonInput_1.parseJsonInput)(input);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw new Error(`Input is not valid JSON: ${error.message}${(0, jsonInput_1.jsonHelpTip)(input.raw, input.fromShell)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function output(value, opts, label) {
|
|
172
|
+
if (opts.file) {
|
|
173
|
+
(0, fs_1.writeFileSync)(opts.file, JSON.stringify(value, null, 2));
|
|
174
|
+
(0, ui_1.success)(`Saved ${label} to ${opts.file}`);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (opts.json) {
|
|
178
|
+
console.log(JSON.stringify(value, null, 2));
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const date = (value) => (value ? new Date(String(value)).toLocaleDateString('en-US') : '');
|
|
184
|
+
function printErrors(errors) {
|
|
185
|
+
for (const e of errors)
|
|
186
|
+
console.error(` ${e.nodeId ? `[${e.nodeId}] ` : ''}${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
function workflowsSchemaCommand() {
|
|
189
|
+
console.log(NODE_REFERENCE);
|
|
190
|
+
}
|
|
191
|
+
async function workflowsListCommand(opts) {
|
|
192
|
+
const api = (0, session_1.requireApi)();
|
|
193
|
+
try {
|
|
194
|
+
const { workflows } = await api.listWorkflows(opts.search);
|
|
195
|
+
if (output(workflows, opts, 'workflows'))
|
|
196
|
+
return;
|
|
197
|
+
if (!workflows.length)
|
|
198
|
+
return void (0, ui_1.info)('No workflows yet.');
|
|
199
|
+
(0, ui_1.table)(['ID', 'NAME', 'NODES', 'UPDATED'], workflows.map((w) => [w._id, w.workFlowName, w.graph?.nodes?.length ?? 0, date(w.updatedAt)]));
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
(0, ui_1.handleError)(err);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function workflowsGetCommand(id, opts) {
|
|
206
|
+
const api = (0, session_1.requireApi)();
|
|
207
|
+
try {
|
|
208
|
+
const wf = await api.getWorkflow(id);
|
|
209
|
+
if (output(wf, opts, 'workflow'))
|
|
210
|
+
return;
|
|
211
|
+
(0, ui_1.keyValue)({ ID: wf._id, Name: wf.workFlowName, Updated: date(wf.updatedAt) });
|
|
212
|
+
const nodes = wf.graph?.nodes ?? [];
|
|
213
|
+
const edges = wf.graph?.edges ?? [];
|
|
214
|
+
console.log();
|
|
215
|
+
(0, ui_1.table)(['NODE ID', 'TYPE', 'NAME', 'NEXT'], nodes.map((n) => [
|
|
216
|
+
n.id,
|
|
217
|
+
n.type,
|
|
218
|
+
n.name ?? '',
|
|
219
|
+
edges
|
|
220
|
+
.filter((e) => e.from === n.id)
|
|
221
|
+
.map((e) => (e.fromHandle === 'out' ? e.to : `${e.fromHandle}→${e.to}`))
|
|
222
|
+
.join(', '),
|
|
223
|
+
]));
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
(0, ui_1.handleError)(err);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function workflowsCreateCommand(name, json, opts) {
|
|
230
|
+
const api = (0, session_1.requireApi)();
|
|
231
|
+
try {
|
|
232
|
+
const graph = json || opts.file ? graphFrom(await readJson(json, opts.file)) : undefined;
|
|
233
|
+
const wf = await api.createWorkflow({ workFlowName: name, graph });
|
|
234
|
+
(0, ui_1.success)(`Workflow "${wf.workFlowName}" created (${wf._id}).`);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
(0, ui_1.handleError)(err);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function workflowsImportCommand(json, opts) {
|
|
241
|
+
const api = (0, session_1.requireApi)();
|
|
242
|
+
try {
|
|
243
|
+
const parsed = await readJson(json, opts.file);
|
|
244
|
+
const name = opts.name || parsed?.data?.workFlowName || parsed?.workFlowName;
|
|
245
|
+
if (!name)
|
|
246
|
+
throw new Error('The file has no workFlowName; pass --name.');
|
|
247
|
+
const wf = await api.createWorkflow({ workFlowName: name, graph: graphFrom(parsed) });
|
|
248
|
+
(0, ui_1.success)(`Workflow "${wf.workFlowName}" imported (${wf._id}).`);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
(0, ui_1.handleError)(err);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function workflowsUpdateCommand(id, json, opts) {
|
|
255
|
+
const api = (0, session_1.requireApi)();
|
|
256
|
+
try {
|
|
257
|
+
if (!opts.name && !json && !opts.file) {
|
|
258
|
+
throw new Error('Nothing to update: pass --name and/or a graph via --file or @file.');
|
|
259
|
+
}
|
|
260
|
+
const graph = json || opts.file ? graphFrom(await readJson(json, opts.file)) : undefined;
|
|
261
|
+
const wf = await api.updateWorkflow(id, { workFlowName: opts.name, graph });
|
|
262
|
+
(0, ui_1.success)(`Workflow "${wf.workFlowName}" updated.`);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
(0, ui_1.handleError)(err);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Validate a local graph file, or a saved workflow by id. */
|
|
269
|
+
async function workflowsValidateCommand(id, opts) {
|
|
270
|
+
const api = (0, session_1.requireApi)();
|
|
271
|
+
try {
|
|
272
|
+
const graph = id ? (await api.getWorkflow(id)).graph : graphFrom(await readJson(undefined, opts.file));
|
|
273
|
+
const { valid, errors } = await api.validateWorkflowGraph(graph);
|
|
274
|
+
if (valid)
|
|
275
|
+
return void (0, ui_1.success)('Graph is valid.');
|
|
276
|
+
(0, ui_1.fail)(`${errors.length} problem(s):`);
|
|
277
|
+
printErrors(errors);
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
(0, ui_1.handleError)(err);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** Same payload as the dashboard's Export, so it round-trips through `import`. */
|
|
285
|
+
async function workflowsExportCommand(id, opts) {
|
|
286
|
+
const api = (0, session_1.requireApi)();
|
|
287
|
+
try {
|
|
288
|
+
const wf = await api.getWorkflow(id);
|
|
289
|
+
const payload = {
|
|
290
|
+
type: 'callkaro_workflow',
|
|
291
|
+
version: 1,
|
|
292
|
+
exportedAt: new Date().toISOString(),
|
|
293
|
+
data: { workFlowName: wf.workFlowName, type: 'normal', graph: wf.graph ?? { nodes: [], edges: [] } },
|
|
294
|
+
};
|
|
295
|
+
if (opts.file) {
|
|
296
|
+
(0, fs_1.writeFileSync)(opts.file, JSON.stringify(payload, null, 2));
|
|
297
|
+
return void (0, ui_1.success)(`Saved workflow to ${opts.file}`);
|
|
298
|
+
}
|
|
299
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
(0, ui_1.handleError)(err);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function workflowsDuplicateCommand(id, opts) {
|
|
306
|
+
const api = (0, session_1.requireApi)();
|
|
307
|
+
try {
|
|
308
|
+
const wf = await api.duplicateWorkflow(id, opts.name);
|
|
309
|
+
(0, ui_1.success)(`Duplicated as "${wf.workFlowName}" (${wf._id}).`);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
(0, ui_1.handleError)(err);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function workflowsDeleteCommand(id, opts) {
|
|
316
|
+
const api = (0, session_1.requireApi)();
|
|
317
|
+
try {
|
|
318
|
+
if (!opts.yes) {
|
|
319
|
+
const wf = await api.getWorkflow(id);
|
|
320
|
+
if (!(await (0, ui_1.confirm)(`Delete workflow "${wf.workFlowName}"? This cannot be undone.`, opts.yes)))
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
await api.deleteWorkflow(id);
|
|
324
|
+
(0, ui_1.success)('Workflow deleted.');
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
(0, ui_1.handleError)(err);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function workflowsLeadsCommand(id, opts) {
|
|
331
|
+
const api = (0, session_1.requireApi)();
|
|
332
|
+
try {
|
|
333
|
+
const res = await api.listWorkflowLeads(id, { search: opts.search, page: opts.page, pageSize: opts.pageSize });
|
|
334
|
+
if (output(res, opts, 'leads'))
|
|
335
|
+
return;
|
|
336
|
+
(0, ui_1.info)(Object.entries(res.counts || {})
|
|
337
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
338
|
+
.join(' ') || 'No leads yet.');
|
|
339
|
+
if (!res.leads.length)
|
|
340
|
+
return;
|
|
341
|
+
(0, ui_1.table)(['PHONE', 'LEAD ID', 'STATUS', 'AT STEP', 'CALLS', 'RUN ID'], res.leads.map((l) => [l.phone, l.lead_id ?? '', l.status, l.current_step ?? '', l.total_calls ?? 0, l.run_id]));
|
|
342
|
+
const p = res.pagination;
|
|
343
|
+
(0, ui_1.info)(`Page ${p.page}/${p.totalPages || 1} — ${p.totalDocs} lead(s)${p.hasNext ? ' (use --page to see more)' : ''}`);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
(0, ui_1.handleError)(err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function workflowsLeadCommand(id, phone, opts) {
|
|
350
|
+
const api = (0, session_1.requireApi)();
|
|
351
|
+
try {
|
|
352
|
+
const summary = await api.getWorkflowLead(id, phone);
|
|
353
|
+
if (output(summary, opts, 'lead'))
|
|
354
|
+
return;
|
|
355
|
+
(0, ui_1.keyValue)(Object.fromEntries(Object.entries(summary).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : v])));
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
(0, ui_1.handleError)(err);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** The lead's latest run, step by step: why it went the way it did. */
|
|
362
|
+
async function workflowsRunCommand(id, phone, opts) {
|
|
363
|
+
const api = (0, session_1.requireApi)();
|
|
364
|
+
try {
|
|
365
|
+
const detail = await api.getWorkflowRunByPhone(id, phone);
|
|
366
|
+
if (output(detail, opts, 'run'))
|
|
367
|
+
return;
|
|
368
|
+
const { run, steps } = detail;
|
|
369
|
+
(0, ui_1.keyValue)({
|
|
370
|
+
Run: run.run_id,
|
|
371
|
+
Phone: run.phone_number,
|
|
372
|
+
Status: run.status,
|
|
373
|
+
'Current node': run.current_node_id,
|
|
374
|
+
'Next node': run.next_node_id || undefined,
|
|
375
|
+
'Resumes at': run.resume_at && !String(run.resume_at).startsWith('1970') ? run.resume_at : undefined,
|
|
376
|
+
Started: run.started_at,
|
|
377
|
+
});
|
|
378
|
+
console.log();
|
|
379
|
+
(0, ui_1.table)(['#', 'AT', 'NODE', 'ACTION', 'STATUS', 'HANDLE', 'ERROR'], steps.map((s) => [s.seq, s.createdAt, s.node_name || s.node_id, s.action ?? '', s.status, s.handle ?? '', s.error ?? '']));
|
|
380
|
+
(0, ui_1.info)("Use --json for each step's action_results and the run variables.");
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
(0, ui_1.handleError)(err);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function workflowsJourneyCommand(id, phone, opts) {
|
|
387
|
+
const api = (0, session_1.requireApi)();
|
|
388
|
+
try {
|
|
389
|
+
const { calls } = await api.workflowJourney(id, phone);
|
|
390
|
+
if (output(calls, opts, 'calls'))
|
|
391
|
+
return;
|
|
392
|
+
if (!calls.length)
|
|
393
|
+
return void (0, ui_1.info)('No calls to or from this number in this workflow.');
|
|
394
|
+
(0, ui_1.table)(['CALL ID', 'AT', 'STATUS', 'DURATION', 'HANGUP'], calls.map((c) => [c._id, c.createdAt ?? c.time ?? '', c.status ?? '', c.call_duration ?? '', c.hangup_reason ?? '']));
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
(0, ui_1.handleError)(err);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Read the saved graph, apply one edit, save. The server's validation is the final word. */
|
|
401
|
+
async function editGraph(id, apply) {
|
|
402
|
+
const api = (0, session_1.requireApi)();
|
|
403
|
+
try {
|
|
404
|
+
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
|
+
const graph = {
|
|
408
|
+
nodes: JSON.parse(JSON.stringify(wf.graph?.nodes ?? [])),
|
|
409
|
+
edges: JSON.parse(JSON.stringify(wf.graph?.edges ?? [])),
|
|
410
|
+
};
|
|
411
|
+
const summary = apply(graph);
|
|
412
|
+
await api.updateWorkflow(id, { graph });
|
|
413
|
+
(0, ui_1.success)(summary);
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
(0, ui_1.handleError)(err);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const num = (v) => (v === undefined ? undefined : Number(v));
|
|
420
|
+
async function workflowsAddNodeCommand(id, opts) {
|
|
421
|
+
let fields;
|
|
422
|
+
try {
|
|
423
|
+
fields = opts.set ? await readJson(opts.set, undefined) : undefined;
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
return (0, ui_1.handleError)(err);
|
|
427
|
+
}
|
|
428
|
+
await editGraph(id, (g) => edit.addNode(g, { ...opts, set: fields, x: num(opts.x), y: num(opts.y) }));
|
|
429
|
+
}
|
|
430
|
+
async function workflowsSetNodeCommand(id, nodeId, opts) {
|
|
431
|
+
try {
|
|
432
|
+
const fields = opts.set ? await readJson(opts.set, undefined) : {};
|
|
433
|
+
if (opts.name)
|
|
434
|
+
fields.name = opts.name;
|
|
435
|
+
if (!Object.keys(fields).length)
|
|
436
|
+
throw new Error('Nothing to change: pass --set <json> and/or --name.');
|
|
437
|
+
await editGraph(id, (g) => edit.setNode(g, nodeId, fields));
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
(0, ui_1.handleError)(err);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function workflowsRemoveNodeCommand(id, nodeId, opts) {
|
|
444
|
+
try {
|
|
445
|
+
if (!(await (0, ui_1.confirm)(`Remove node "${nodeId}" and its edges?`, opts.yes)))
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
return (0, ui_1.handleError)(err);
|
|
450
|
+
}
|
|
451
|
+
await editGraph(id, (g) => edit.removeNode(g, nodeId, opts.reconnect));
|
|
452
|
+
}
|
|
453
|
+
async function workflowsConnectCommand(id, opts) {
|
|
454
|
+
await editGraph(id, (g) => edit.connect(g, opts.from, opts.to, opts.handle));
|
|
455
|
+
}
|
|
456
|
+
async function workflowsDisconnectCommand(id, opts) {
|
|
457
|
+
await editGraph(id, (g) => edit.disconnect(g, opts.from, opts.handle));
|
|
458
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,8 @@ const ongoing_1 = require("./commands/ongoing");
|
|
|
27
27
|
const transcribers_1 = require("./commands/transcribers");
|
|
28
28
|
const voices_1 = require("./commands/voices");
|
|
29
29
|
const secrets_1 = require("./commands/secrets");
|
|
30
|
+
const workflows_1 = require("./commands/workflows");
|
|
31
|
+
const contacts_1 = require("./commands/contacts");
|
|
30
32
|
const skills_1 = require("./commands/skills");
|
|
31
33
|
const update_1 = require("./commands/update");
|
|
32
34
|
const analytics_1 = require("./commands/analytics");
|
|
@@ -326,6 +328,100 @@ secrets.command('remove <name>').description('remove a secret')
|
|
|
326
328
|
.option('--yes', 'skip confirmation').action(secrets_1.secretsRemoveCommand);
|
|
327
329
|
secrets.command('rename <oldName> <newName>').description('rename a secret without changing its value')
|
|
328
330
|
.action(secrets_1.secretsRenameCommand);
|
|
331
|
+
// --- workflows -------------------------------------------------------------
|
|
332
|
+
const workflows = program.command('workflows').description('manage workflows (graphs of calls, WhatsApp, waits, conditions)');
|
|
333
|
+
workflows.command('schema').description('print the workflow graph JSON reference (node types, fields, edges)').action(workflows_1.workflowsSchemaCommand);
|
|
334
|
+
workflows.command('list').description('list workflows').option('--search <text>', 'filter by name').option('--json', 'output raw JSON').option('--file <path>', 'save JSON to a file').action(workflows_1.workflowsListCommand);
|
|
335
|
+
workflows.command('get <id>').description('show a workflow and its nodes').option('--json', 'output the full workflow as JSON').option('--file <path>', 'save JSON to a file').action(workflows_1.workflowsGetCommand);
|
|
336
|
+
workflows.command('create <name> [json]').description('create a workflow, optionally from a graph JSON').option('--file <path>', 'read the graph from a file').action(workflows_1.workflowsCreateCommand);
|
|
337
|
+
workflows.command('import [json]').description('create a workflow from an exported workflow file').option('--file <path>', 'read JSON from a file').option('--name <name>', 'override the workflow name').action(workflows_1.workflowsImportCommand);
|
|
338
|
+
workflows.command('update <id> [json]').description('rename a workflow and/or replace its graph').option('--name <name>', 'new name').option('--file <path>', 'read the new graph from a file').action(workflows_1.workflowsUpdateCommand);
|
|
339
|
+
workflows.command('validate [id]').description('check a graph (saved workflow id, or --file) without saving').option('--file <path>', 'read the graph from a file').action(workflows_1.workflowsValidateCommand);
|
|
340
|
+
workflows.command('export <id>').description('export a workflow as a re-importable JSON file').option('--file <path>', 'write to a file instead of stdout').action(workflows_1.workflowsExportCommand);
|
|
341
|
+
workflows.command('duplicate <id>').description('copy a workflow').option('--name <name>', 'name for the copy').action(workflows_1.workflowsDuplicateCommand);
|
|
342
|
+
workflows.command('delete <id>').description('delete a workflow').option('--yes', 'skip confirmation').action(workflows_1.workflowsDeleteCommand);
|
|
343
|
+
workflows.command('leads <id>').description('leads enrolled in a workflow and where each one is').option('--search <text>', 'phone number or lead id').option('--page <n>', 'page number').option('--page-size <n>', 'page size (max 200)').option('--json', 'output raw JSON').option('--file <path>', 'save JSON to a file').action(workflows_1.workflowsLeadsCommand);
|
|
344
|
+
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);
|
|
345
|
+
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);
|
|
346
|
+
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);
|
|
347
|
+
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);
|
|
348
|
+
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);
|
|
349
|
+
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);
|
|
350
|
+
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);
|
|
351
|
+
workflows.command('disconnect <id>').description('remove the link leaving a node (or one of its branches)').requiredOption('--from <nodeId>', 'source node').option('--handle <handle>', 'source branch (conditions exit id)').action(workflows_1.workflowsDisconnectCommand);
|
|
352
|
+
// --- contacts (CRM) --------------------------------------------------------
|
|
353
|
+
const contacts = program.command('contacts').description('manage CRM contacts, attributes, imports, exports and assignment');
|
|
354
|
+
contacts.command('list').description('list contacts (filters match the dashboard)')
|
|
355
|
+
.option('--search <text>', 'match phone number or text')
|
|
356
|
+
.option('--channel <channel>', 'filter by channel')
|
|
357
|
+
.option('--attr <name=value>', 'attribute filter (repeatable)', contacts_1.collect, [])
|
|
358
|
+
.option('--last-message-state <state>', 'read | delivered | sent | failed | clicked | not_clicked')
|
|
359
|
+
.option('--created-from <YYYY-MM-DD>', 'created on/after (IST)')
|
|
360
|
+
.option('--created-to <YYYY-MM-DD>', 'created on/before (IST)')
|
|
361
|
+
.option('--updated-from <YYYY-MM-DD>', 'updated on/after (IST)')
|
|
362
|
+
.option('--updated-to <YYYY-MM-DD>', 'updated on/before (IST)')
|
|
363
|
+
.option('--page <n>', 'page number')
|
|
364
|
+
.option('--page-size <n>', 'page size (max 50)')
|
|
365
|
+
.option('--columns <a,b>', 'attribute columns to show')
|
|
366
|
+
.option('--assigned-to-me', 'only contacts whose conversations are assigned to you')
|
|
367
|
+
.option('--json', 'output raw JSON').option('--file <path>', 'save JSON to a file')
|
|
368
|
+
.action(contacts_1.contactsListCommand);
|
|
369
|
+
contacts.command('get <phone>').description('show one contact and its attributes').option('--json', 'output raw JSON').option('--file <path>', 'save JSON to a file').action(contacts_1.contactsGetCommand);
|
|
370
|
+
contacts.command('create <phone>').description('create a contact (or merge into an existing one)').option('--attr <name=value>', 'attribute value (repeatable)', contacts_1.collect, []).option('--channel <channel>', 'channel (repeatable)', contacts_1.collect, []).action(contacts_1.contactsCreateCommand);
|
|
371
|
+
contacts.command('update <phone>').description('set or clear attribute values on a contact').option('--attr <name=value>', 'attribute value (repeatable)', contacts_1.collect, []).option('--clear <name>', 'clear an attribute (repeatable)', contacts_1.collect, []).action(contacts_1.contactsUpdateCommand);
|
|
372
|
+
contacts.command('delete <phone>').description('delete a contact').option('--yes', 'skip confirmation').action(contacts_1.contactsDeleteCommand);
|
|
373
|
+
contacts.command('upload <csv>').description('import contacts from a CSV (needs a phone_number column; other columns must be defined attributes)').option('--ignore-unknown', 'drop columns that are not defined attributes instead of refusing').action(contacts_1.contactsUploadCommand);
|
|
374
|
+
contacts.command('export').description('export contacts to CSV (same filters as list)')
|
|
375
|
+
.option('--search <text>', 'match phone number or text')
|
|
376
|
+
.option('--channel <channel>', 'filter by channel')
|
|
377
|
+
.option('--attr <name=value>', 'attribute filter (repeatable)', contacts_1.collect, [])
|
|
378
|
+
.option('--last-message-state <state>', 'read | delivered | sent | failed | clicked | not_clicked')
|
|
379
|
+
.option('--created-from <YYYY-MM-DD>', 'created on/after (IST)')
|
|
380
|
+
.option('--created-to <YYYY-MM-DD>', 'created on/before (IST)')
|
|
381
|
+
.option('--updated-from <YYYY-MM-DD>', 'updated on/after (IST)')
|
|
382
|
+
.option('--updated-to <YYYY-MM-DD>', 'updated on/before (IST)')
|
|
383
|
+
.option('--fields <a,b>', 'columns to include (base fields and/or attribute names)')
|
|
384
|
+
.option('--email <address>', 'email the export instead of downloading (required above 5000 rows)')
|
|
385
|
+
.option('--file <path>', 'write CSV to a file instead of stdout')
|
|
386
|
+
.action(contacts_1.contactsExportCommand);
|
|
387
|
+
contacts.command('assign').description('add contacts to a workflow (--workflow) or call them with a voice agent (--agent)')
|
|
388
|
+
.option('--phones <a,b>', 'specific phone numbers; otherwise the filters below select the set')
|
|
389
|
+
.option('--search <text>', 'match phone number or text')
|
|
390
|
+
.option('--channel <channel>', 'filter by channel')
|
|
391
|
+
.option('--attr <name=value>', 'attribute filter (repeatable)', contacts_1.collect, [])
|
|
392
|
+
.option('--last-message-state <state>', 'read | delivered | sent | failed | clicked | not_clicked')
|
|
393
|
+
.option('--created-from <YYYY-MM-DD>', 'created on/after (IST)')
|
|
394
|
+
.option('--created-to <YYYY-MM-DD>', 'created on/before (IST)')
|
|
395
|
+
.option('--updated-from <YYYY-MM-DD>', 'updated on/after (IST)')
|
|
396
|
+
.option('--updated-to <YYYY-MM-DD>', 'updated on/before (IST)')
|
|
397
|
+
.option('--workflow <id>', 'workflow to enroll into')
|
|
398
|
+
.option('--agent <id>', 'voice agent to call with')
|
|
399
|
+
.option('--name <name>', 'batch / enrollment name')
|
|
400
|
+
.option('--language <code>', 'language (default en)')
|
|
401
|
+
.option('--at <ISO>', 'start later at this time')
|
|
402
|
+
.option('--window <HH:MM-HH:MM>', 'daily trigger window')
|
|
403
|
+
.option('--retries <n>', 'retries (voice agent only)')
|
|
404
|
+
.option('--gaps <m,m>', 'minutes between retries (voice agent only)')
|
|
405
|
+
.option('--carry-over', 'carry unfinished calls over to the next day')
|
|
406
|
+
.option('--priority <n>', 'batch priority')
|
|
407
|
+
.option('--force', 'workflow: restart contacts that are already running')
|
|
408
|
+
.option('--dry-run', 'show who would be assigned and where; start nothing')
|
|
409
|
+
.option('--yes', 'skip confirmation when assigning all contacts')
|
|
410
|
+
.action(contacts_1.contactsAssignCommand);
|
|
411
|
+
const attributes = contacts.command('attributes').description('manage the CRM attribute definitions contacts can hold');
|
|
412
|
+
attributes.command('list').description('list attribute definitions').option('--json', 'output raw JSON').option('--file <path>', 'save JSON to a file').action(contacts_1.attributesListCommand);
|
|
413
|
+
attributes.command('set <name>').description('create an attribute, or change only the given fields of an existing one')
|
|
414
|
+
.option('--type <type>', 'string | description | enum | number | date | boolean | datetime | time | array | object')
|
|
415
|
+
.option('--label <text>', 'display name')
|
|
416
|
+
.option('--description <text>', 'description')
|
|
417
|
+
.option('--options <a,b>', 'allowed values (enum)')
|
|
418
|
+
.option('--manual-only', 'AI/functions may not update it').option('--ai-updatable', 'AI/functions may update it')
|
|
419
|
+
.option('--required', 'make required').option('--optional', 'make optional')
|
|
420
|
+
.option('--hidden', 'hide on screen').option('--visible', 'show on screen')
|
|
421
|
+
.option('--default-mode <mode>', 'drop | type_default | custom (value for imports that omit it)')
|
|
422
|
+
.option('--default-value <value>', 'default when mode is custom')
|
|
423
|
+
.action(contacts_1.attributesSetCommand);
|
|
424
|
+
attributes.command('delete <name>').description('delete an attribute definition (values on contacts are kept, hidden)').option('--yes', 'skip confirmation').action(contacts_1.attributesDeleteCommand);
|
|
329
425
|
// --- agents ----------------------------------------------------------------
|
|
330
426
|
const agents = program.command('agents').description('manage voice agents');
|
|
331
427
|
agents.command('list').description('list all your agents')
|
|
@@ -690,20 +786,23 @@ const sim = program.command('sim').description('agent simulations & test cases')
|
|
|
690
786
|
sim.command('tests <agentId>').description('list test cases').option('--json', 'raw JSON').action(simulations_1.simTestsListCommand);
|
|
691
787
|
sim.command('create <agentId>').description('create a test case')
|
|
692
788
|
.option('--name <name>', 'test case name')
|
|
789
|
+
.option('--persona <text>', 'who the simulated caller is')
|
|
693
790
|
.option('--prompt <text>', 'simulated user prompt')
|
|
694
791
|
.option('--criteria <text>', 'success criteria')
|
|
695
|
-
.option('--model <model>', 'test LLM model')
|
|
792
|
+
.option('--model <model>', 'test LLM model (default: platform default; see `cku sim models`)')
|
|
696
793
|
.option('--variables <json|@file>', 'testVariables: a JSON object of name -> value (non-strings are stringified)')
|
|
697
794
|
.option('--functions <json|@file>', 'function mocks: a JSON array of {"name": ..., "response": ...}')
|
|
698
795
|
.action(simulations_1.simCreateCommand);
|
|
699
796
|
sim.command('update <testCaseId>').description('edit a test case (only the fields you pass are sent)')
|
|
700
797
|
.option('--name <name>', 'test case name')
|
|
798
|
+
.option('--persona <text>', 'who the simulated caller is')
|
|
701
799
|
.option('--prompt <text>', 'simulated user prompt')
|
|
702
800
|
.option('--criteria <text>', 'success criteria')
|
|
703
801
|
.option('--model <model>', 'test LLM model')
|
|
704
802
|
.option('--variables <json|@file>', 'testVariables: replaces the whole map')
|
|
705
803
|
.option('--functions <json|@file>', 'function mocks: replaces the whole list')
|
|
706
804
|
.action(simulations_1.simUpdateCommand);
|
|
805
|
+
sim.command('models').description('models that can play the simulated caller').option('--json', 'raw JSON').action(simulations_1.simModelsCommand);
|
|
707
806
|
sim.command('delete <testCaseId>').description('delete a test case').option('--yes', 'skip confirmation')
|
|
708
807
|
.action(simulations_1.simDeleteCommand);
|
|
709
808
|
sim.command('run <agentId>').description('run a batch simulation (async)')
|