callkaro-cli-unofficial 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/commands/accounts.js +4 -10
- package/dist/commands/agentPhone.js +38 -0
- package/dist/commands/agents.js +7 -1
- package/dist/commands/calls.js +6 -3
- package/dist/commands/dashboardApiCmd.js +48 -0
- package/dist/commands/jsonInput.js +16 -0
- package/dist/commands/kpi.js +8 -3
- package/dist/commands/numbers.js +4 -24
- package/dist/commands/secrets.js +3 -10
- package/dist/commands/simVariables.js +86 -0
- package/dist/commands/simulations.js +123 -25
- package/dist/commands/whatsapp.js +35 -0
- package/dist/index.js +50 -1
- package/dist/lib/agentUpdate.js +30 -0
- package/dist/lib/api.js +8 -0
- package/dist/lib/config.js +3 -3
- package/dist/lib/dashboardApi.js +51 -3
- package/dist/lib/dashboardAuth.js +45 -34
- package/dist/lib/ui.js +30 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Every arrow above is a real import or a real call, checked against the source. `
|
|
|
69
69
|
/cli/* · 61 endpoints · Bearer ck_5b02…
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
One thing worth knowing up front: the official CLI ships **compiled output only**. Its npm package has `dist/` and nothing else: no TypeScript, no tests, no build config, and it isn't in any public repo. So this is a reconstruction. The `/cli/*` contract was rebuilt from the published bundle, and the command surface matches v4.0.
|
|
72
|
+
One thing worth knowing up front: the official CLI ships **compiled output only**. Its npm package has `dist/` and nothing else: no TypeScript, no tests, no build config, and it isn't in any public repo. So this is a reconstruction. The `/cli/*` contract was rebuilt from the published bundle, and the command surface matches v4.0.1 (the official CLI's commands plus the `accounts` group and the dashboard-side `dashboard`, `kpi`, `analyzers` and `models` groups).
|
|
73
73
|
|
|
74
74
|
The public API at `api.callkaro.ai` is a different thing entirely, with an `X-API-KEY` header and three documented endpoints. This CLI doesn't use it. [`docs/architecture.md`](docs/architecture.md) covers the boundaries, and [`docs/api.md`](docs/api.md) has the full endpoint inventory.
|
|
75
75
|
|
|
@@ -147,12 +147,12 @@ There is **no delete** for a card — the board retires one by moving it to `rej
|
|
|
147
147
|
### The dashboard credential
|
|
148
148
|
|
|
149
149
|
```bash
|
|
150
|
-
cku dashboard login # separate from `cku login
|
|
150
|
+
cku dashboard login # separate from `cku login`; the token lasts 24h, renewed silently
|
|
151
151
|
cku dashboard whoami
|
|
152
152
|
cku dashboard probe # which dashboard routes this token reaches
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
The dashboard API needs a *different* token from `/cli/*` — they're not interchangeable, and each rejects the other with an opaque 401. This also unlocks `cku analyzers` and `cku models`.
|
|
155
|
+
The dashboard API needs a *different* token from `/cli/*` — they're not interchangeable, and each rejects the other with an opaque 401. This also unlocks `cku analyzers` and `cku models`. The dashboard token expires every 24h with no refresh endpoint, so `cku dashboard login` saves your password (plain text, mode 0600, alongside the tokens) and re-logs in for you when it lapses. Pass `--no-save-password` to keep it off disk and re-enter it daily; `cku dashboard logout` deletes it.
|
|
156
156
|
|
|
157
157
|
### Knowing which models are actually valid
|
|
158
158
|
|
|
@@ -34,6 +34,7 @@ async function loginCommand(opts) {
|
|
|
34
34
|
}
|
|
35
35
|
try {
|
|
36
36
|
const { api } = (0, config_1.getClient)();
|
|
37
|
+
(0, ui_1.needTty)('Use `cku login` (browser sign-in) or set CK_TOKEN.');
|
|
37
38
|
const answers = await (0, prompts_1.default)([
|
|
38
39
|
{ type: opts.email ? null : 'text', name: 'email', message: 'Email' },
|
|
39
40
|
{ type: 'password', name: 'password', message: 'Password' },
|
|
@@ -58,6 +59,7 @@ async function registerCommand(opts) {
|
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
const { api } = (0, config_1.getClient)();
|
|
62
|
+
(0, ui_1.needTty)('Registration is interactive (it sends a verification code); run it in a terminal.');
|
|
61
63
|
const answers = await (0, prompts_1.default)([
|
|
62
64
|
{ type: opts.email ? null : 'text', name: 'email', message: 'Email' },
|
|
63
65
|
{ type: opts.name ? null : 'text', name: 'name', message: 'Your name' },
|
|
@@ -254,16 +256,8 @@ async function accountsRemoveCommand(emailOrSlug, opts) {
|
|
|
254
256
|
accounts.find((a) => (0, config_1.slugFor)(a.email || '').startsWith(wanted));
|
|
255
257
|
if (!match)
|
|
256
258
|
return (0, ui_1.handleError)(new Error(`No saved account matches "${emailOrSlug}".`));
|
|
257
|
-
if (!opts.yes)
|
|
258
|
-
|
|
259
|
-
type: 'confirm',
|
|
260
|
-
name: 'ok',
|
|
261
|
-
message: `Remove the saved login for ${match.email}?`,
|
|
262
|
-
initial: false,
|
|
263
|
-
});
|
|
264
|
-
if (!ok)
|
|
265
|
-
return void (0, ui_1.info)('Cancelled.');
|
|
266
|
-
}
|
|
259
|
+
if (!(await (0, ui_1.confirm)(`Remove the saved login for ${match.email}?`, opts.yes)))
|
|
260
|
+
return;
|
|
267
261
|
const slug = (0, config_1.slugFor)(match.email || '');
|
|
268
262
|
(0, config_1.removeAccount)(slug);
|
|
269
263
|
(0, ui_1.success)(`Removed ${match.email}.`);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.setInboundCommand = setInboundCommand;
|
|
4
|
+
exports.setWhatsappOutboundCommand = setWhatsappOutboundCommand;
|
|
5
|
+
exports.setWhatsappInboundCommand = setWhatsappInboundCommand;
|
|
4
6
|
exports.setOutboundCommand = setOutboundCommand;
|
|
5
7
|
const session_1 = require("../lib/session");
|
|
6
8
|
const ui_1 = require("../lib/ui");
|
|
@@ -33,6 +35,42 @@ async function setInboundCommand(agentId, opts) {
|
|
|
33
35
|
(0, ui_1.handleError)(err);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* `cku agents set-whatsapp-outbound <agentId> --number <id>` (or `--clear`)
|
|
40
|
+
*
|
|
41
|
+
* Reuses PUT /cli/agents/:id, like voice outbound. The id is the WhatsApp phone
|
|
42
|
+
* number id from `cku whatsapp list`, not the display number.
|
|
43
|
+
*/
|
|
44
|
+
async function setWhatsappOutboundCommand(agentId, opts) {
|
|
45
|
+
const api = (0, session_1.requireApi)();
|
|
46
|
+
if (!opts.clear && !opts.number)
|
|
47
|
+
return (0, ui_1.handleError)(new Error('--number <phoneNumberId> or --clear is required'));
|
|
48
|
+
try {
|
|
49
|
+
await api.updateAgent(agentId, undefined, { whatsappPhoneNumber: opts.clear ? null : opts.number });
|
|
50
|
+
(0, ui_1.success)(opts.clear ? `Cleared WhatsApp outbound number on agent ${agentId}.` : `Set WhatsApp outbound number on agent ${agentId}.`);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
(0, ui_1.handleError)(err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `cku agents set-whatsapp-inbound <agentId> --number <id>` (or `--clear`)
|
|
58
|
+
*
|
|
59
|
+
* Own endpoint, and a single number rather than an array: the backend clears the
|
|
60
|
+
* number off any other agent holding it first (one inbound agent per number).
|
|
61
|
+
*/
|
|
62
|
+
async function setWhatsappInboundCommand(agentId, opts) {
|
|
63
|
+
const api = (0, session_1.requireApi)();
|
|
64
|
+
if (!opts.clear && !opts.number)
|
|
65
|
+
return (0, ui_1.handleError)(new Error('--number <phoneNumberId> or --clear is required'));
|
|
66
|
+
try {
|
|
67
|
+
await api.setWhatsappInbound(agentId, opts.clear ? null : opts.number);
|
|
68
|
+
(0, ui_1.success)(opts.clear ? `Cleared WhatsApp inbound number on agent ${agentId}.` : `Set WhatsApp inbound number on agent ${agentId}.`);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
(0, ui_1.handleError)(err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
36
74
|
/** `cku agents set-outbound <agentId> --number <id>` (or `--clear`) */
|
|
37
75
|
async function setOutboundCommand(agentId, opts) {
|
|
38
76
|
const api = (0, session_1.requireApi)();
|
package/dist/commands/agents.js
CHANGED
|
@@ -234,6 +234,7 @@ async function agentsExportCommand(agentId, opts) {
|
|
|
234
234
|
const versions = res.versions || [];
|
|
235
235
|
if (!versions.length)
|
|
236
236
|
return (0, ui_1.handleError)(new Error('This agent has no versions'));
|
|
237
|
+
(0, ui_1.needTty)('Pass --versions <ids> to choose which versions to export.');
|
|
237
238
|
const picked = await (0, prompts_1.default)({
|
|
238
239
|
type: 'multiselect',
|
|
239
240
|
name: 'ids',
|
|
@@ -426,9 +427,14 @@ async function agentsUpdateCommand(agentId, opts) {
|
|
|
426
427
|
// *target* version: without one, getAgent returns the default version, and
|
|
427
428
|
// its siblings (provider, voice id, …) would be written onto a different
|
|
428
429
|
// version. Wholesale keys are version-level, so --versions is set here.
|
|
429
|
-
|
|
430
|
+
// `functions` is a whole array on the backend: merge by name unless the
|
|
431
|
+
// caller asked to replace the list.
|
|
432
|
+
const mergeFns = Array.isArray(patch.functions) && !opts.replaceFunctions;
|
|
433
|
+
if (wholesaleKeys.length || mergeFns) {
|
|
430
434
|
const { agent } = await api.getAgent(agentId, (0, versions_1.firstVersion)(opts.versions));
|
|
431
435
|
patch = (0, agentUpdate_1.mergeWholesaleKeys)(patch, agent);
|
|
436
|
+
if (mergeFns)
|
|
437
|
+
patch.functions = (0, agentUpdate_1.mergeFunctions)(patch.functions, agent.functions);
|
|
432
438
|
}
|
|
433
439
|
// A commit message is recorded against a version. Version-level updates
|
|
434
440
|
// already name one; for an agent-level-only update with --commit, attach it
|
package/dist/commands/calls.js
CHANGED
|
@@ -39,11 +39,14 @@ async function callsListCommand(opts) {
|
|
|
39
39
|
const res = await api.listCalls({
|
|
40
40
|
...buildCallQuery(opts),
|
|
41
41
|
pageSize: (0, ui_1.intFlag)(opts.limit, '--limit', 50),
|
|
42
|
-
page: 1,
|
|
42
|
+
page: (0, ui_1.intFlag)(opts.page, '--page', 1),
|
|
43
43
|
});
|
|
44
44
|
const calls = res.calls || [];
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// --json stays a bare array so existing parsers keep working; --meta is the
|
|
46
|
+
// opt-in that also exposes pagination (totalDocs, next page) for scripts.
|
|
47
|
+
if (opts.json) {
|
|
48
|
+
return void console.log(JSON.stringify(opts.meta ? { calls, pagination: res.pagination ?? null } : calls, null, 2));
|
|
49
|
+
}
|
|
47
50
|
if (!calls.length)
|
|
48
51
|
return void (0, ui_1.info)('No calls found.');
|
|
49
52
|
(0, ui_1.table)(['TYPE', 'FROM', 'TO', 'AGENT', 'VERSION', 'DURATION', 'HANGUP', 'TIME'], calls.map((c) => [
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dashboardApiCommand = dashboardApiCommand;
|
|
4
|
+
const dashboardApi_1 = require("../lib/dashboardApi");
|
|
5
|
+
const ui_1 = require("../lib/ui");
|
|
6
|
+
const jsonInput_1 = require("./jsonInput");
|
|
7
|
+
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
8
|
+
/**
|
|
9
|
+
* `cku dashboard api <METHOD> <path> [--body <json|@file>] [--query k=v ...]`
|
|
10
|
+
*
|
|
11
|
+
* The escape hatch for a dashboard route the CLI has no command for yet. It goes
|
|
12
|
+
* through `dashboardApi().request`, so the session cookie stays inside the CLI
|
|
13
|
+
* (a script otherwise has to read it out of the account file) and the path rules
|
|
14
|
+
* there apply: it must start with "/", redirects are refused, and the host is
|
|
15
|
+
* always the configured backend. Prints the response body as JSON. On a non-2xx
|
|
16
|
+
* it prints the status and the whole error body to stderr and exits 1; it never
|
|
17
|
+
* prints a header, so a token cannot leak through it.
|
|
18
|
+
*/
|
|
19
|
+
async function dashboardApiCommand(method, path, opts) {
|
|
20
|
+
try {
|
|
21
|
+
const verb = method.toUpperCase();
|
|
22
|
+
if (!METHODS.includes(verb))
|
|
23
|
+
throw new Error(`Method must be one of ${METHODS.join(', ')} (got "${method}").`);
|
|
24
|
+
let target = path;
|
|
25
|
+
if (opts.query?.length) {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
for (const kv of opts.query) {
|
|
28
|
+
const i = kv.indexOf('=');
|
|
29
|
+
if (i < 1)
|
|
30
|
+
throw new Error(`--query expects key=value (got "${kv}").`);
|
|
31
|
+
params.append(kv.slice(0, i), kv.slice(i + 1));
|
|
32
|
+
}
|
|
33
|
+
target += (path.includes('?') ? '&' : '?') + params.toString();
|
|
34
|
+
}
|
|
35
|
+
const body = opts.body ? await (0, jsonInput_1.readJsonFlag)(opts.body, '--body') : undefined;
|
|
36
|
+
const res = await (0, dashboardApi_1.dashboardApi)().request(verb, target, body);
|
|
37
|
+
console.log(JSON.stringify(res ?? null, null, 2));
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err instanceof dashboardApi_1.DashboardError) {
|
|
41
|
+
console.error(`HTTP ${err.status}`);
|
|
42
|
+
console.error(JSON.stringify(err.data ?? { message: err.message }, null, 2));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
(0, ui_1.handleError)(err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.jsonHelpTip = jsonHelpTip;
|
|
4
4
|
exports.resolveJsonInput = resolveJsonInput;
|
|
5
5
|
exports.parseJsonInput = parseJsonInput;
|
|
6
|
+
exports.readJsonFlag = readJsonFlag;
|
|
6
7
|
const fs_1 = require("fs");
|
|
7
8
|
const looseJson_1 = require("../lib/looseJson");
|
|
8
9
|
async function readStdin() {
|
|
@@ -54,3 +55,18 @@ async function resolveJsonInput(arg, file) {
|
|
|
54
55
|
function parseJsonInput({ raw, fromShell }) {
|
|
55
56
|
return fromShell ? (0, looseJson_1.parseLooseJson)(raw) : JSON.parse(raw);
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Read one JSON-valued flag (`--variables`, `--body`) given inline or as `@file`.
|
|
60
|
+
* Errors name the flag, so the message says which input was bad.
|
|
61
|
+
*/
|
|
62
|
+
async function readJsonFlag(raw, flag) {
|
|
63
|
+
if (!raw.trim())
|
|
64
|
+
throw new Error(`${flag} needs JSON or @file.json.`);
|
|
65
|
+
const input = await resolveJsonInput(raw);
|
|
66
|
+
try {
|
|
67
|
+
return parseJsonInput(input);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new Error(`${flag} is not valid JSON: ${err.message}${jsonHelpTip(input.raw, input.fromShell)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/commands/kpi.js
CHANGED
|
@@ -596,9 +596,14 @@ async function kpiTargetCommand(opts) {
|
|
|
596
596
|
try {
|
|
597
597
|
current = (await api.kpiAgentConfig(agentId)) || {};
|
|
598
598
|
}
|
|
599
|
-
catch {
|
|
600
|
-
//
|
|
601
|
-
|
|
599
|
+
catch (err) {
|
|
600
|
+
// 404 is the only "no config yet" case, and it is a normal state — the PUT
|
|
601
|
+
// creates it. Anything else (401, 500, a network drop) is a real failure,
|
|
602
|
+
// and treating it as "no config" would send a PUT that clears the RCA
|
|
603
|
+
// prompt this read was supposed to preserve. That is silent data loss, so
|
|
604
|
+
// it has to stop here.
|
|
605
|
+
if (!(0, dashboardApi_1.isNotFound)(err))
|
|
606
|
+
throw err;
|
|
602
607
|
}
|
|
603
608
|
const metricName = 'connected_to_conversion_percentage';
|
|
604
609
|
const body = {
|
package/dist/commands/numbers.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.numbersListCommand = numbersListCommand;
|
|
7
4
|
exports.numbersCatalogCommand = numbersCatalogCommand;
|
|
@@ -9,7 +6,6 @@ exports.numbersBuyCommand = numbersBuyCommand;
|
|
|
9
6
|
exports.numbersSpamCommand = numbersSpamCommand;
|
|
10
7
|
exports.numbersUnspamCommand = numbersUnspamCommand;
|
|
11
8
|
exports.numbersReleaseCommand = numbersReleaseCommand;
|
|
12
|
-
const prompts_1 = __importDefault(require("prompts"));
|
|
13
9
|
const session_1 = require("../lib/session");
|
|
14
10
|
const ui_1 = require("../lib/ui");
|
|
15
11
|
function fmtDate(v) {
|
|
@@ -92,16 +88,8 @@ async function numbersBuyCommand(numberOrId, opts) {
|
|
|
92
88
|
if (!number) {
|
|
93
89
|
return (0, ui_1.handleError)(new Error('That number is not in the buyable pool (already taken or invalid id).'));
|
|
94
90
|
}
|
|
95
|
-
if (!opts.yes)
|
|
96
|
-
|
|
97
|
-
type: 'confirm',
|
|
98
|
-
name: 'ok',
|
|
99
|
-
message: `Buy +${number.phoneNumber} for ₹${pricepernumber}? This deducts credits.`,
|
|
100
|
-
initial: false,
|
|
101
|
-
});
|
|
102
|
-
if (!ok)
|
|
103
|
-
return void (0, ui_1.info)('Cancelled.');
|
|
104
|
-
}
|
|
91
|
+
if (!(await (0, ui_1.confirm)(`Buy +${number.phoneNumber} for ₹${pricepernumber}? This deducts credits.`, opts.yes)))
|
|
92
|
+
return;
|
|
105
93
|
const res = await api.buyNumber(numberId, pricepernumber);
|
|
106
94
|
return void (0, ui_1.success)(res.message || `Bought +${number.phoneNumber}.`);
|
|
107
95
|
}
|
|
@@ -135,16 +123,8 @@ async function numbersReleaseCommand(numberOrId, opts) {
|
|
|
135
123
|
const api = (0, session_1.requireApi)();
|
|
136
124
|
try {
|
|
137
125
|
const numberId = await resolveNumberId(api, numberOrId);
|
|
138
|
-
if (!opts.yes)
|
|
139
|
-
|
|
140
|
-
type: 'confirm',
|
|
141
|
-
name: 'ok',
|
|
142
|
-
message: 'Release (unrent) this number? It will be unassigned from all agents.',
|
|
143
|
-
initial: false,
|
|
144
|
-
});
|
|
145
|
-
if (!ok)
|
|
146
|
-
return void (0, ui_1.info)('Cancelled.');
|
|
147
|
-
}
|
|
126
|
+
if (!(await (0, ui_1.confirm)('Release (unrent) this number? It will be unassigned from all agents.', opts.yes)))
|
|
127
|
+
return;
|
|
148
128
|
const res = await api.releaseNumber(numberId);
|
|
149
129
|
(0, ui_1.success)(res.message || 'Released.');
|
|
150
130
|
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -35,6 +35,7 @@ async function readSecretValue(name, fromStdin) {
|
|
|
35
35
|
throw new Error('--stdin requires piped or redirected input');
|
|
36
36
|
return (await readStdin()).trim();
|
|
37
37
|
}
|
|
38
|
+
(0, ui_1.needTty)('Pass --stdin and pipe the value in.');
|
|
38
39
|
const answer = await (0, prompts_1.default)({ type: 'password', name: 'value', message: `Value for ${name}` });
|
|
39
40
|
return typeof answer.value === 'string' ? answer.value.trim() : undefined;
|
|
40
41
|
}
|
|
@@ -84,16 +85,8 @@ async function secretsRemoveCommand(inputName, opts) {
|
|
|
84
85
|
const list = secrets || [];
|
|
85
86
|
if (!list.some((secret) => secret.name === name))
|
|
86
87
|
throw new Error(`Secret "${name}" not found`);
|
|
87
|
-
if (!opts.yes)
|
|
88
|
-
|
|
89
|
-
type: 'confirm',
|
|
90
|
-
name: 'ok',
|
|
91
|
-
message: `Remove secret "${name}"?`,
|
|
92
|
-
initial: false,
|
|
93
|
-
});
|
|
94
|
-
if (!answer.ok)
|
|
95
|
-
return void (0, ui_1.info)('Cancelled.');
|
|
96
|
-
}
|
|
88
|
+
if (!(await (0, ui_1.confirm)(`Remove secret "${name}"?`, opts.yes)))
|
|
89
|
+
return;
|
|
97
90
|
await api.saveSecrets(list.filter((secret) => secret.name !== name).map(preserve));
|
|
98
91
|
(0, ui_1.success)(`Secret "${name}" removed.`);
|
|
99
92
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.simVarsListCommand = simVarsListCommand;
|
|
4
|
+
exports.simVarsShowCommand = simVarsShowCommand;
|
|
5
|
+
exports.simVarsCreateCommand = simVarsCreateCommand;
|
|
6
|
+
exports.simVarsDeleteCommand = simVarsDeleteCommand;
|
|
7
|
+
const dashboardApi_1 = require("../lib/dashboardApi");
|
|
8
|
+
const ui_1 = require("../lib/ui");
|
|
9
|
+
const jsonInput_1 = require("./jsonInput");
|
|
10
|
+
const simulations_1 = require("./simulations");
|
|
11
|
+
/**
|
|
12
|
+
* Saved variable sets: named bundles of `testVariables` the dashboard's "Import
|
|
13
|
+
* Variables" button loads into a test case. Dashboard-only (cookie-authed), so
|
|
14
|
+
* these commands need `cku dashboard login`; `cku sim create --variables` needs
|
|
15
|
+
* no session and takes the same JSON.
|
|
16
|
+
*/
|
|
17
|
+
async function simVarsListCommand(agentId, opts) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await (0, dashboardApi_1.dashboardApi)().testVariableSets(agentId);
|
|
20
|
+
const sets = res?.data || [];
|
|
21
|
+
if (opts.json)
|
|
22
|
+
return void console.log(JSON.stringify(sets, null, 2));
|
|
23
|
+
if (!sets.length)
|
|
24
|
+
return void (0, ui_1.info)('No saved variable sets for this agent.');
|
|
25
|
+
(0, ui_1.table)(['NAME', 'ID', 'VARIABLES', 'CREATED'], sets.map((s) => [
|
|
26
|
+
s.name,
|
|
27
|
+
s._id,
|
|
28
|
+
Object.keys(s.callMetadata || {}).length,
|
|
29
|
+
s.createdAt ? new Date(s.createdAt).toLocaleString('en-US') : '',
|
|
30
|
+
]));
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
(0, ui_1.handleError)(err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function simVarsShowCommand(agentId, nameOrId, opts) {
|
|
37
|
+
try {
|
|
38
|
+
const set = await findSet(agentId, nameOrId);
|
|
39
|
+
if (opts.json)
|
|
40
|
+
return void console.log(JSON.stringify(set.callMetadata || {}, null, 2));
|
|
41
|
+
(0, ui_1.table)(['VARIABLE', 'VALUE'], Object.entries(set.callMetadata || {}).map(([k, v]) => [k, String(v)]));
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
(0, ui_1.handleError)(err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function simVarsCreateCommand(agentId, name, opts) {
|
|
48
|
+
try {
|
|
49
|
+
if (!opts.variables) {
|
|
50
|
+
return (0, ui_1.handleError)(new Error('--variables <json|@file.json> is required, e.g. --variables @lead.json'));
|
|
51
|
+
}
|
|
52
|
+
const callMetadata = (0, simulations_1.toTestVariables)(await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables'));
|
|
53
|
+
const res = await (0, dashboardApi_1.dashboardApi)().createTestVariableSet({ agentId, name, callMetadata });
|
|
54
|
+
(0, ui_1.success)(`Saved variable set "${name}" (${res?.data?._id || ''}) with ${Object.keys(callMetadata).length} variables.`);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
(0, ui_1.handleError)(err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function simVarsDeleteCommand(agentId, nameOrId, opts) {
|
|
61
|
+
try {
|
|
62
|
+
const set = await findSet(agentId, nameOrId);
|
|
63
|
+
if (!(await (0, ui_1.confirm)(`Delete saved variable set "${set.name}"?`, opts.yes)))
|
|
64
|
+
return;
|
|
65
|
+
await (0, dashboardApi_1.dashboardApi)().deleteTestVariableSet(set._id);
|
|
66
|
+
(0, ui_1.success)(`Deleted "${set.name}".`);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
(0, ui_1.handleError)(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Match by exact id, then by exact name; an ambiguous name is an error, not a guess. */
|
|
73
|
+
async function findSet(agentId, nameOrId) {
|
|
74
|
+
const res = await (0, dashboardApi_1.dashboardApi)().testVariableSets(agentId);
|
|
75
|
+
const sets = res?.data || [];
|
|
76
|
+
const byId = sets.find((s) => s._id === nameOrId);
|
|
77
|
+
if (byId)
|
|
78
|
+
return byId;
|
|
79
|
+
const byName = sets.filter((s) => s.name === nameOrId);
|
|
80
|
+
if (byName.length === 1)
|
|
81
|
+
return byName[0];
|
|
82
|
+
if (byName.length > 1) {
|
|
83
|
+
throw new Error(`${byName.length} saved sets are named "${nameOrId}". Use an id: ${byName.map((s) => s._id).join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`No saved variable set "${nameOrId}" for this agent. List them: cku sim variables list ${agentId}`);
|
|
86
|
+
}
|
|
@@ -3,15 +3,47 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sleep = void 0;
|
|
7
|
+
exports.toTestVariables = toTestVariables;
|
|
8
|
+
exports.toTestFunctions = toTestFunctions;
|
|
6
9
|
exports.simTestsListCommand = simTestsListCommand;
|
|
7
10
|
exports.simCreateCommand = simCreateCommand;
|
|
8
11
|
exports.simDeleteCommand = simDeleteCommand;
|
|
12
|
+
exports.waitForBatch = waitForBatch;
|
|
9
13
|
exports.simRunCommand = simRunCommand;
|
|
10
14
|
exports.simRunsListCommand = simRunsListCommand;
|
|
11
15
|
exports.simResultsCommand = simResultsCommand;
|
|
12
16
|
const prompts_1 = __importDefault(require("prompts"));
|
|
13
17
|
const session_1 = require("../lib/session");
|
|
14
18
|
const ui_1 = require("../lib/ui");
|
|
19
|
+
const jsonInput_1 = require("./jsonInput");
|
|
20
|
+
/**
|
|
21
|
+
* `testVariables` is a flat name -> string map on the backend (ids, "True", and
|
|
22
|
+
* JSON-encoded arrays are all strings). Accept natural JSON and stringify what is
|
|
23
|
+
* not already a string, so `{"lead_id": 900029, "pointers": ["a"]}` works.
|
|
24
|
+
*/
|
|
25
|
+
function toTestVariables(input) {
|
|
26
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
27
|
+
throw new Error('--variables must be a JSON object of name -> value, e.g. {"lead_id":"900029"}.');
|
|
28
|
+
}
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [k, v] of Object.entries(input)) {
|
|
31
|
+
out[k] = typeof v === 'string' ? v : v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** `testFunctions` is a list of `{name, response}` mocks; `response` is an object or a string. */
|
|
36
|
+
function toTestFunctions(input) {
|
|
37
|
+
if (!Array.isArray(input)) {
|
|
38
|
+
throw new Error('--functions must be a JSON array of {"name": ..., "response": ...} mocks.');
|
|
39
|
+
}
|
|
40
|
+
return input.map((f, i) => {
|
|
41
|
+
if (!f || typeof f !== 'object' || typeof f.name !== 'string' || !f.name || !('response' in f)) {
|
|
42
|
+
throw new Error(`--functions[${i}] needs a "name" (string) and a "response" (object or string).`);
|
|
43
|
+
}
|
|
44
|
+
return { name: f.name, response: f.response };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
15
47
|
async function simTestsListCommand(agentId, opts) {
|
|
16
48
|
const api = (0, session_1.requireApi)();
|
|
17
49
|
try {
|
|
@@ -30,8 +62,12 @@ async function simTestsListCommand(agentId, opts) {
|
|
|
30
62
|
async function simCreateCommand(agentId, opts) {
|
|
31
63
|
const api = (0, session_1.requireApi)();
|
|
32
64
|
try {
|
|
65
|
+
// Parse the JSON flags before prompting, so a typo fails before any typing.
|
|
66
|
+
const testVariables = opts.variables ? toTestVariables(await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables')) : {};
|
|
67
|
+
const testFunctions = opts.functions ? toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions')) : [];
|
|
33
68
|
let { name, prompt, criteria } = opts;
|
|
34
69
|
if (!name || !prompt || !criteria) {
|
|
70
|
+
(0, ui_1.needTty)('Pass --name, --prompt and --criteria.');
|
|
35
71
|
const answers = await (0, prompts_1.default)([
|
|
36
72
|
{ type: name ? null : 'text', name: 'name', message: 'Test case name' },
|
|
37
73
|
{ type: prompt ? null : 'text', name: 'prompt', message: 'Simulated user prompt' },
|
|
@@ -50,8 +86,8 @@ async function simCreateCommand(agentId, opts) {
|
|
|
50
86
|
userPrompt: prompt,
|
|
51
87
|
successCriteria: criteria,
|
|
52
88
|
testLLMModel: opts.model || 'gpt-4.1-mini',
|
|
53
|
-
testVariables
|
|
54
|
-
testFunctions
|
|
89
|
+
testVariables,
|
|
90
|
+
testFunctions,
|
|
55
91
|
});
|
|
56
92
|
(0, ui_1.success)(`Created test case ${res.testCase?._id || ''}.`);
|
|
57
93
|
}
|
|
@@ -62,13 +98,8 @@ async function simCreateCommand(agentId, opts) {
|
|
|
62
98
|
async function simDeleteCommand(testCaseId, opts) {
|
|
63
99
|
const api = (0, session_1.requireApi)();
|
|
64
100
|
try {
|
|
65
|
-
if (!opts.yes)
|
|
66
|
-
|
|
67
|
-
type: 'confirm', name: 'ok', message: `Delete test case ${testCaseId}?`, initial: false,
|
|
68
|
-
});
|
|
69
|
-
if (!ok)
|
|
70
|
-
return void (0, ui_1.info)('Cancelled.');
|
|
71
|
-
}
|
|
101
|
+
if (!(await (0, ui_1.confirm)(`Delete test case ${testCaseId}?`, opts.yes)))
|
|
102
|
+
return;
|
|
72
103
|
const res = await api.deleteTestCase(testCaseId);
|
|
73
104
|
(0, ui_1.success)(res.msg || 'Deleted.');
|
|
74
105
|
}
|
|
@@ -76,6 +107,48 @@ async function simDeleteCommand(testCaseId, opts) {
|
|
|
76
107
|
(0, ui_1.handleError)(err);
|
|
77
108
|
}
|
|
78
109
|
}
|
|
110
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
111
|
+
exports.sleep = sleep;
|
|
112
|
+
/**
|
|
113
|
+
* Poll a batch until it has produced every row it will produce.
|
|
114
|
+
*
|
|
115
|
+
* The backend has no "finished" flag. A batch carries `totalRuns` and the
|
|
116
|
+
* dashboard counts rows in the results list against it, so that is the done
|
|
117
|
+
* signal here too. Progress goes to stderr, so `--json` output stays parseable.
|
|
118
|
+
* Returns the last rows seen and whether the batch completed before the timeout.
|
|
119
|
+
*/
|
|
120
|
+
async function waitForBatch(api, batchId, agentId, opts) {
|
|
121
|
+
const timeoutMs = (0, ui_1.intFlag)(opts.timeout, '--timeout', 600) * 1000;
|
|
122
|
+
const every = opts.intervalMs ?? 5000;
|
|
123
|
+
const deadline = Date.now() + timeoutMs;
|
|
124
|
+
let rows = [];
|
|
125
|
+
let total;
|
|
126
|
+
for (;;) {
|
|
127
|
+
const res = await api.simulationResults(batchId);
|
|
128
|
+
rows = res.simulations || res.results || res.data || [];
|
|
129
|
+
if (total === undefined && agentId) {
|
|
130
|
+
const runs = (await api.listSimulationRuns(agentId)).batches || [];
|
|
131
|
+
total = runs.find((b) => b._id === batchId)?.totalRuns;
|
|
132
|
+
}
|
|
133
|
+
console.error(` ${rows.length}${total ? `/${total}` : ''} results`);
|
|
134
|
+
if (total && rows.length >= total)
|
|
135
|
+
return { rows, done: true, total };
|
|
136
|
+
if (Date.now() + every > deadline)
|
|
137
|
+
return { rows, done: false, total };
|
|
138
|
+
await (0, exports.sleep)(every);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function resultTable(rows) {
|
|
142
|
+
(0, ui_1.table)(['TEST CASE', 'VERSION', 'RESULT', 'TURNS', 'SUMMARY'], rows.map((r) => [
|
|
143
|
+
r.testcaseId?.testcaseName || r.testcaseName || r.testcaseId || '',
|
|
144
|
+
r.versionId?.versionName || r.versionId || '',
|
|
145
|
+
r.isTestSucceed === undefined ? '' : r.isTestSucceed ? 'PASS' : 'FAIL',
|
|
146
|
+
r.total_turns ?? '',
|
|
147
|
+
String(r.testResultSummary || '').replace(/\s+/g, ' ').slice(0, 70),
|
|
148
|
+
]));
|
|
149
|
+
const passed = rows.filter((r) => r.isTestSucceed).length;
|
|
150
|
+
(0, ui_1.info)(`\n${passed}/${rows.length} passed.`);
|
|
151
|
+
}
|
|
79
152
|
async function simRunCommand(agentId, opts) {
|
|
80
153
|
const api = (0, session_1.requireApi)();
|
|
81
154
|
const testIds = (opts.tests || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
@@ -85,13 +158,31 @@ async function simRunCommand(agentId, opts) {
|
|
|
85
158
|
if (!versionIds.length)
|
|
86
159
|
return (0, ui_1.handleError)(new Error('--versions <versionIds> is required'));
|
|
87
160
|
try {
|
|
161
|
+
const epoch = (0, ui_1.intFlag)(opts.epoch, '--epoch', 1);
|
|
162
|
+
(0, ui_1.intFlag)(opts.timeout, '--timeout', 600); // reject a bad value before starting a run
|
|
88
163
|
const res = await api.runSimulation({
|
|
89
164
|
agentId, testIds, versionIds,
|
|
90
165
|
batchName: opts.name || `cli-run-${Date.now()}`,
|
|
91
|
-
epoch
|
|
166
|
+
epoch,
|
|
92
167
|
});
|
|
93
|
-
|
|
94
|
-
(
|
|
168
|
+
const batchId = res.batch?._id || '';
|
|
169
|
+
if (!opts.wait) {
|
|
170
|
+
(0, ui_1.success)(`Simulation run started (${batchId}). Results arrive asynchronously.`);
|
|
171
|
+
return void (0, ui_1.info)(`Check with: cku sim results ${batchId || '<batchId>'} (or re-run with --wait)`);
|
|
172
|
+
}
|
|
173
|
+
if (!batchId)
|
|
174
|
+
return (0, ui_1.handleError)(new Error('The backend did not return a batch id, so there is nothing to wait on.'));
|
|
175
|
+
console.error(`Run started (${batchId}); waiting for ${testIds.length * versionIds.length * epoch} results …`);
|
|
176
|
+
const { rows, done, total } = await waitForBatch(api, batchId, agentId, opts);
|
|
177
|
+
if (opts.json)
|
|
178
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
179
|
+
else
|
|
180
|
+
resultTable(rows);
|
|
181
|
+
if (!done) {
|
|
182
|
+
return (0, ui_1.handleError)(new Error(`Timed out with ${rows.length}${total ? `/${total}` : ''} results. The run is still going: cku sim results ${batchId} --wait`));
|
|
183
|
+
}
|
|
184
|
+
if (rows.some((r) => !r.isTestSucceed))
|
|
185
|
+
process.exitCode = 1;
|
|
95
186
|
}
|
|
96
187
|
catch (err) {
|
|
97
188
|
(0, ui_1.handleError)(err);
|
|
@@ -101,15 +192,16 @@ async function simRunsListCommand(agentId, opts) {
|
|
|
101
192
|
const api = (0, session_1.requireApi)();
|
|
102
193
|
try {
|
|
103
194
|
const res = await api.listSimulationRuns(agentId);
|
|
104
|
-
const rows = res.runs || res.data || [];
|
|
195
|
+
const rows = res.batches || res.runs || res.data || [];
|
|
105
196
|
if (opts.json)
|
|
106
197
|
return void console.log(JSON.stringify(rows, null, 2));
|
|
107
198
|
if (!rows.length)
|
|
108
199
|
return void (0, ui_1.info)('No simulation runs yet.');
|
|
109
|
-
(0, ui_1.table)(['NAME', 'BATCH ID', '
|
|
200
|
+
(0, ui_1.table)(['NAME', 'BATCH ID', 'RUNS', 'EPOCH', 'CREATED'], rows.map((r) => [
|
|
110
201
|
r.batchName || r.name || '',
|
|
111
|
-
r.
|
|
112
|
-
r.
|
|
202
|
+
r._id || r.batch?._id || '',
|
|
203
|
+
r.totalRuns ?? '',
|
|
204
|
+
r.epoch ?? '',
|
|
113
205
|
r.createdAt ? new Date(r.createdAt).toLocaleString('en-US') : '',
|
|
114
206
|
]));
|
|
115
207
|
}
|
|
@@ -120,18 +212,24 @@ async function simRunsListCommand(agentId, opts) {
|
|
|
120
212
|
async function simResultsCommand(batchId, opts) {
|
|
121
213
|
const api = (0, session_1.requireApi)();
|
|
122
214
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
215
|
+
let rows;
|
|
216
|
+
if (opts.wait) {
|
|
217
|
+
if (!opts.agent)
|
|
218
|
+
return (0, ui_1.handleError)(new Error('--wait needs --agent <agentId> to know how many results to expect.'));
|
|
219
|
+
const out = await waitForBatch(api, batchId, opts.agent, opts);
|
|
220
|
+
rows = out.rows;
|
|
221
|
+
if (!out.done)
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const res = await api.simulationResults(batchId);
|
|
226
|
+
rows = res.simulations || res.results || res.data || [];
|
|
227
|
+
}
|
|
125
228
|
if (opts.json)
|
|
126
229
|
return void console.log(JSON.stringify(rows, null, 2));
|
|
127
230
|
if (!rows.length)
|
|
128
|
-
return void (0, ui_1.info)('No results yet — runs are asynchronous.');
|
|
129
|
-
(
|
|
130
|
-
r.testcaseName || r.testCaseId || '',
|
|
131
|
-
r.versionId?.versionName || r.versionId || '',
|
|
132
|
-
r.passed === undefined ? '' : r.passed ? 'yes' : 'no',
|
|
133
|
-
r.score ?? '',
|
|
134
|
-
]));
|
|
231
|
+
return void (0, ui_1.info)('No results yet — runs are asynchronous. Use --wait --agent <agentId> to wait.');
|
|
232
|
+
resultTable(rows);
|
|
135
233
|
}
|
|
136
234
|
catch (err) {
|
|
137
235
|
(0, ui_1.handleError)(err);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.whatsappListCommand = whatsappListCommand;
|
|
4
|
+
const session_1 = require("../lib/session");
|
|
5
|
+
const ui_1 = require("../lib/ui");
|
|
6
|
+
/**
|
|
7
|
+
* `cku whatsapp list`
|
|
8
|
+
*
|
|
9
|
+
* Numbers come from Meta (the linked WhatsApp Business Account), not a platform
|
|
10
|
+
* pool, so there is no catalog/buy/register here. Adding one is Meta's embedded
|
|
11
|
+
* signup (Facebook login) behind the dashboard's "+ Add Phone Number" button, an
|
|
12
|
+
* interactive OAuth flow the CLI cannot drive.
|
|
13
|
+
*/
|
|
14
|
+
async function whatsappListCommand(opts) {
|
|
15
|
+
const api = (0, session_1.requireApi)();
|
|
16
|
+
try {
|
|
17
|
+
const { phoneNumbers } = await api.listWhatsappNumbers();
|
|
18
|
+
const list = phoneNumbers || [];
|
|
19
|
+
if (opts.json)
|
|
20
|
+
return void console.log(JSON.stringify(list, null, 2));
|
|
21
|
+
if (!list.length) {
|
|
22
|
+
return void console.log('No WhatsApp numbers linked yet — add one on the dashboard first ("+ Add Phone Number").');
|
|
23
|
+
}
|
|
24
|
+
(0, ui_1.table)(['NUMBER', 'ID', 'VERIFIED NAME', 'STATUS', 'QUALITY'], list.map((n) => [
|
|
25
|
+
n.display_phone_number,
|
|
26
|
+
n.id,
|
|
27
|
+
n.verified_name || '',
|
|
28
|
+
n.code_verification_status || '',
|
|
29
|
+
n.quality_rating || '',
|
|
30
|
+
]));
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
(0, ui_1.handleError)(err);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const models_1 = require("./commands/models");
|
|
|
17
17
|
const agents_1 = require("./commands/agents");
|
|
18
18
|
const agentVersions_1 = require("./commands/agentVersions");
|
|
19
19
|
const agentPhone_1 = require("./commands/agentPhone");
|
|
20
|
+
const whatsapp_1 = require("./commands/whatsapp");
|
|
20
21
|
const numbers_1 = require("./commands/numbers");
|
|
21
22
|
const calls_1 = require("./commands/calls");
|
|
22
23
|
const ongoing_1 = require("./commands/ongoing");
|
|
@@ -28,6 +29,8 @@ const update_1 = require("./commands/update");
|
|
|
28
29
|
const analytics_1 = require("./commands/analytics");
|
|
29
30
|
const batches_1 = require("./commands/batches");
|
|
30
31
|
const simulations_1 = require("./commands/simulations");
|
|
32
|
+
const simVariables_1 = require("./commands/simVariables");
|
|
33
|
+
const dashboardApiCmd_1 = require("./commands/dashboardApiCmd");
|
|
31
34
|
const auditStrategies_1 = require("./commands/auditStrategies");
|
|
32
35
|
const chatAgents_1 = require("./commands/chatAgents");
|
|
33
36
|
// The command this fork installs as. Deliberately NOT `ck` or `callkaro`:
|
|
@@ -78,6 +81,7 @@ dashboard
|
|
|
78
81
|
.command('login')
|
|
79
82
|
.description('sign in with email + password, exactly as the login form does')
|
|
80
83
|
.option('--email <email>', 'skip the email prompt')
|
|
84
|
+
.option('--no-save-password', "don't keep the password (the session then lapses after 24h)")
|
|
81
85
|
.action(dashboardAuth_1.dashboardLoginCommand);
|
|
82
86
|
dashboard
|
|
83
87
|
.command('whoami')
|
|
@@ -93,6 +97,13 @@ dashboard
|
|
|
93
97
|
.description('forget the dashboard session (the ck_ token is kept)')
|
|
94
98
|
.option('--yes', 'skip the confirmation')
|
|
95
99
|
.action(dashboardAuth_1.dashboardLogoutCommand);
|
|
100
|
+
dashboard
|
|
101
|
+
.command('api <method> <path>')
|
|
102
|
+
.description('call any dashboard route with the stored session (the token never leaves the CLI)')
|
|
103
|
+
.option('--body <json|@file>', 'JSON request body')
|
|
104
|
+
.option('--query <k=v...>', 'query parameter, repeatable', (v, prev = []) => [...prev, v])
|
|
105
|
+
.addHelpText('after', '\n Example: cku dashboard api GET /v1/test-simulations --query agentId=<id>\n Errors print the status and full body to stderr and exit 1.')
|
|
106
|
+
.action(dashboardApiCmd_1.dashboardApiCommand);
|
|
96
107
|
// Which models the server accepts. Same source the validator uses, so it cannot drift.
|
|
97
108
|
program
|
|
98
109
|
.command('models')
|
|
@@ -314,6 +325,7 @@ agents.command('update <agentId>').description('update fields via --set \'{json}
|
|
|
314
325
|
.option('--set <json>', "JSON object of fields (or @file.json), e.g. '{\"name\":\"x\",\"temperature\":5}'")
|
|
315
326
|
.option('--versions <id>', 'target a specific version (needed for version-level fields)')
|
|
316
327
|
.option('--commit <msg>', 'commit message for the version change')
|
|
328
|
+
.option('--replace-functions', 'replace the whole functions list instead of merging by name (default merges; omitted functions are kept)')
|
|
317
329
|
.option('--json', 'output the raw updated-agent response')
|
|
318
330
|
.action(agents_1.agentsUpdateCommand);
|
|
319
331
|
// Phone assignment (agent-centric). The one-inbound-agent rule is enforced server-side.
|
|
@@ -325,6 +337,14 @@ agents.command('set-outbound <agentId>').description("set/clear an agent's outbo
|
|
|
325
337
|
.option('--number <id>', 'phone number id')
|
|
326
338
|
.option('--clear', 'clear the outbound number')
|
|
327
339
|
.action(agentPhone_1.setOutboundCommand);
|
|
340
|
+
agents.command('set-whatsapp-inbound <agentId>').description("set/clear an agent's WhatsApp inbound number")
|
|
341
|
+
.option('--number <id>', 'WhatsApp phone number id (see `cku whatsapp list`)')
|
|
342
|
+
.option('--clear', 'clear the inbound number')
|
|
343
|
+
.action(agentPhone_1.setWhatsappInboundCommand);
|
|
344
|
+
agents.command('set-whatsapp-outbound <agentId>').description("set/clear an agent's WhatsApp outbound number")
|
|
345
|
+
.option('--number <id>', 'WhatsApp phone number id (see `cku whatsapp list`)')
|
|
346
|
+
.option('--clear', 'clear the outbound number')
|
|
347
|
+
.action(agentPhone_1.setWhatsappOutboundCommand);
|
|
328
348
|
// Version management: clone, publish, activate, A/B testing
|
|
329
349
|
agents.command('clone-version <agentId>').description('clone a version with a required target prompt mode and language')
|
|
330
350
|
.option('--versions <id>', 'source version id')
|
|
@@ -419,6 +439,13 @@ addSkillsProviderOptions(skills.command('dir'))
|
|
|
419
439
|
.description('print the selected install location(s)')
|
|
420
440
|
.option('--project', 'print project location(s)')
|
|
421
441
|
.action(skills_1.skillsDirCommand);
|
|
442
|
+
// --- whatsapp --------------------------------------------------------------
|
|
443
|
+
// Numbers come from your linked Meta WhatsApp Business Account, not a platform
|
|
444
|
+
// pool: add one on the dashboard ("+ Add Phone Number", a Facebook-login flow the
|
|
445
|
+
// CLI cannot run), then list/assign it here. No catalog/buy/register.
|
|
446
|
+
const whatsapp = program.command('whatsapp').description('manage WhatsApp numbers (from your linked Meta WhatsApp Business Account)');
|
|
447
|
+
whatsapp.command('list').description('list WhatsApp numbers on your linked WhatsApp Business Account')
|
|
448
|
+
.option('--json', 'raw JSON').action(whatsapp_1.whatsappListCommand);
|
|
422
449
|
// --- numbers ---------------------------------------------------------------
|
|
423
450
|
const numbers = program.command('numbers').description('manage phone numbers');
|
|
424
451
|
numbers.command('list').description('list your phone numbers').option('--json', 'raw JSON').action(numbers_1.numbersListCommand);
|
|
@@ -506,7 +533,9 @@ calls.command('make').description('place a single outbound call (real, or --test
|
|
|
506
533
|
.action(calls_1.callsMakeCommand);
|
|
507
534
|
addCallFilters(calls.command('list').description('list call history'))
|
|
508
535
|
.option('--limit <n>', 'page size (default 50)')
|
|
509
|
-
.option('--
|
|
536
|
+
.option('--page <n>', 'page number (default 1)')
|
|
537
|
+
.option('--json', 'raw JSON (a bare array of calls)')
|
|
538
|
+
.option('--meta', 'with --json, wrap as {calls, pagination} so scripts can see totalDocs')
|
|
510
539
|
.action(calls_1.callsListCommand);
|
|
511
540
|
calls.command('get <callId>').description('show one call (with recording URL)')
|
|
512
541
|
.option('--json', 'raw JSON')
|
|
@@ -601,6 +630,8 @@ sim.command('create <agentId>').description('create a test case')
|
|
|
601
630
|
.option('--prompt <text>', 'simulated user prompt')
|
|
602
631
|
.option('--criteria <text>', 'success criteria')
|
|
603
632
|
.option('--model <model>', 'test LLM model')
|
|
633
|
+
.option('--variables <json|@file>', 'testVariables: a JSON object of name -> value (non-strings are stringified)')
|
|
634
|
+
.option('--functions <json|@file>', 'function mocks: a JSON array of {"name": ..., "response": ...}')
|
|
604
635
|
.action(simulations_1.simCreateCommand);
|
|
605
636
|
sim.command('delete <testCaseId>').description('delete a test case').option('--yes', 'skip confirmation')
|
|
606
637
|
.action(simulations_1.simDeleteCommand);
|
|
@@ -608,10 +639,28 @@ sim.command('run <agentId>').description('run a batch simulation (async)')
|
|
|
608
639
|
.option('--tests <ids>', 'comma-separated test case ids')
|
|
609
640
|
.option('--versions <ids>', 'comma-separated version ids')
|
|
610
641
|
.option('--name <name>', 'run name')
|
|
642
|
+
.option('--epoch <n>', 'times to run each test case (default 1)')
|
|
643
|
+
.option('--wait', 'block until every result is in; exit 1 if any test fails')
|
|
644
|
+
.option('--timeout <seconds>', 'with --wait, give up after this long (default 600)')
|
|
645
|
+
.option('--json', 'with --wait, print the results as a bare JSON array')
|
|
611
646
|
.action(simulations_1.simRunCommand);
|
|
612
647
|
sim.command('runs <agentId>').description('list simulation runs').option('--json', 'raw JSON').action(simulations_1.simRunsListCommand);
|
|
613
648
|
sim.command('results <batchId>').description('results of a simulation run').option('--json', 'raw JSON')
|
|
649
|
+
.option('--wait', 'block until the batch has all its results (needs --agent)')
|
|
650
|
+
.option('--agent <agentId>', 'with --wait, the agent the batch belongs to')
|
|
651
|
+
.option('--timeout <seconds>', 'with --wait, give up after this long (default 600)')
|
|
614
652
|
.action(simulations_1.simResultsCommand);
|
|
653
|
+
// Saved variable sets are dashboard-only (cookie-authed): they need `cku dashboard login`.
|
|
654
|
+
const simVars = sim.command('variables').description('saved variable sets for test cases (needs `cku dashboard login`)');
|
|
655
|
+
simVars.command('list <agentId>').description('list saved variable sets').option('--json', 'raw JSON')
|
|
656
|
+
.action(simVariables_1.simVarsListCommand);
|
|
657
|
+
simVars.command('show <agentId> <nameOrId>').description('show one saved set').option('--json', 'bare variables object')
|
|
658
|
+
.action(simVariables_1.simVarsShowCommand);
|
|
659
|
+
simVars.command('create <agentId> <name>').description('save a variable set')
|
|
660
|
+
.option('--variables <json|@file>', 'a JSON object of name -> value')
|
|
661
|
+
.action(simVariables_1.simVarsCreateCommand);
|
|
662
|
+
simVars.command('delete <agentId> <nameOrId>').description('delete a saved set').option('--yes', 'skip confirmation')
|
|
663
|
+
.action(simVariables_1.simVarsDeleteCommand);
|
|
615
664
|
// --- audit strategies ------------------------------------------------------
|
|
616
665
|
const audit = program.command('audit-strategies').alias('audit').description('AI Auditor audit strategies');
|
|
617
666
|
audit.command('list <agentId>').description('list audit strategies for an agent').option('--json', 'raw JSON')
|
package/dist/lib/agentUpdate.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.preparePatch = preparePatch;
|
|
|
9
9
|
exports.coerce = coerce;
|
|
10
10
|
exports.deepMerge = deepMerge;
|
|
11
11
|
exports.mergeWholesaleKeys = mergeWholesaleKeys;
|
|
12
|
+
exports.mergeFunctions = mergeFunctions;
|
|
12
13
|
exports.isAgentLevelKey = isAgentLevelKey;
|
|
13
14
|
exports.normalizeStringArray = normalizeStringArray;
|
|
14
15
|
const agentImport_1 = require("./agentImport");
|
|
@@ -251,6 +252,35 @@ function mergeWholesaleKeys(patch, current) {
|
|
|
251
252
|
}
|
|
252
253
|
return out;
|
|
253
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Merge a partial `functions` array into the version's current one, by `name`:
|
|
257
|
+
* a matching name is replaced, a new name is appended, the rest are kept.
|
|
258
|
+
*
|
|
259
|
+
* The backend replaces the whole array, so sending one function would delete
|
|
260
|
+
* every other function on the version while reporting success.
|
|
261
|
+
*/
|
|
262
|
+
function mergeFunctions(incoming, current) {
|
|
263
|
+
if (!Array.isArray(incoming))
|
|
264
|
+
throw new Error('"functions" must be an array of function objects.');
|
|
265
|
+
const out = Array.isArray(current) ? [...current] : [];
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
incoming.forEach((fn, i) => {
|
|
268
|
+
const name = fn && typeof fn === 'object' ? fn.name : undefined;
|
|
269
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
270
|
+
throw new Error(`functions[${i}] has no "name". Functions are merged by name, so each needs one.\n` +
|
|
271
|
+
' To replace the whole list on purpose, pass --replace-functions.');
|
|
272
|
+
}
|
|
273
|
+
if (seen.has(name))
|
|
274
|
+
throw new Error(`functions lists "${name}" twice.`);
|
|
275
|
+
seen.add(name);
|
|
276
|
+
const at = out.findIndex((c) => c && c.name === name);
|
|
277
|
+
if (at >= 0)
|
|
278
|
+
out[at] = fn;
|
|
279
|
+
else
|
|
280
|
+
out.push(fn);
|
|
281
|
+
});
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
254
284
|
function isAgentLevelKey(key) {
|
|
255
285
|
return agentImport_1.AGENT_LEVEL_KEYS.has(key);
|
|
256
286
|
}
|
package/dist/lib/api.js
CHANGED
|
@@ -144,6 +144,14 @@ class Api {
|
|
|
144
144
|
publishChatAgentVersion(agentId, versionId) {
|
|
145
145
|
return this.request('POST', `/cli/chat-agents/${agentId}/publish`, { versionId });
|
|
146
146
|
}
|
|
147
|
+
// --- whatsapp numbers -----------------------------------------------------
|
|
148
|
+
// Numbers come from Meta (the linked WhatsApp Business Account), not a
|
|
149
|
+
// platform pool, so there is no buy/catalog counterpart to /cli/numbers.
|
|
150
|
+
listWhatsappNumbers() { return this.request('GET', '/cli/whatsapp'); }
|
|
151
|
+
/** Pass null to clear. The backend first unassigns the number from any other agent. */
|
|
152
|
+
setWhatsappInbound(agentId, whatsappInboundPhoneNumber) {
|
|
153
|
+
return this.request('POST', '/cli/agents/whatsapp-inbound', { agentId, whatsappInboundPhoneNumber });
|
|
154
|
+
}
|
|
147
155
|
// --- phone numbers --------------------------------------------------------
|
|
148
156
|
listNumbers() { return this.request('GET', '/cli/numbers'); }
|
|
149
157
|
numbersCatalog() { return this.request('GET', '/cli/numbers/catalog'); }
|
package/dist/lib/config.js
CHANGED
|
@@ -249,9 +249,8 @@ function saveAccount(email, session) {
|
|
|
249
249
|
function saveDashboardSession(email, dashboard) {
|
|
250
250
|
const slug = slugFor(email);
|
|
251
251
|
const current = loadAccount(slug);
|
|
252
|
-
// Built field by field rather than spread
|
|
253
|
-
//
|
|
254
|
-
// that no password is ever persisted has to hold here, not at the call site.
|
|
252
|
+
// Built field by field rather than spread, so stray fields a caller passes are
|
|
253
|
+
// not written to disk. `password` is the one deliberate exception.
|
|
255
254
|
const stored = {
|
|
256
255
|
email: dashboard.email || email,
|
|
257
256
|
token: dashboard.token,
|
|
@@ -259,6 +258,7 @@ function saveDashboardSession(email, dashboard) {
|
|
|
259
258
|
...(dashboard.sessionToken ? { sessionToken: dashboard.sessionToken } : {}),
|
|
260
259
|
...(dashboard.userId ? { userId: dashboard.userId } : {}),
|
|
261
260
|
...(dashboard.expiresAt ? { expiresAt: dashboard.expiresAt } : {}),
|
|
261
|
+
...(dashboard.password ? { password: dashboard.password } : {}),
|
|
262
262
|
};
|
|
263
263
|
writeJson(accountPath(slug), { ...current, email, dashboard: stored, savedAt: new Date().toISOString() });
|
|
264
264
|
return slug;
|
package/dist/lib/dashboardApi.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DashboardApi = void 0;
|
|
3
|
+
exports.DashboardApi = exports.DashboardError = void 0;
|
|
4
|
+
exports.isNotFound = isNotFound;
|
|
4
5
|
exports.dashboardApi = dashboardApi;
|
|
5
6
|
const config_1 = require("./config");
|
|
6
7
|
const dashboardAuth_1 = require("./dashboardAuth");
|
|
@@ -27,6 +28,29 @@ const dashboardAuth_1 = require("./dashboardAuth");
|
|
|
27
28
|
*
|
|
28
29
|
* Everything on this surface is undocumented and can change without notice.
|
|
29
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* A non-2xx response from the dashboard API.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately shaped like `ApiError` on the `/cli/*` side, so a caller reads
|
|
35
|
+
* `err.status` the same way on both surfaces. Extends Error so existing
|
|
36
|
+
* `handleError` paths and message-based assertions keep working unchanged.
|
|
37
|
+
*/
|
|
38
|
+
class DashboardError extends Error {
|
|
39
|
+
status;
|
|
40
|
+
data;
|
|
41
|
+
/** `data` is the parsed error body, kept whole for callers (`dashboard api`) that print it. */
|
|
42
|
+
constructor(status, message, data) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.data = data;
|
|
46
|
+
this.name = 'DashboardError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.DashboardError = DashboardError;
|
|
50
|
+
/** True when the failure was "no such record", as opposed to a real error. */
|
|
51
|
+
function isNotFound(err) {
|
|
52
|
+
return err instanceof DashboardError && err.status === 404;
|
|
53
|
+
}
|
|
30
54
|
class DashboardApi {
|
|
31
55
|
baseUrl;
|
|
32
56
|
constructor(baseUrl) {
|
|
@@ -38,7 +62,12 @@ class DashboardApi {
|
|
|
38
62
|
* stored 24h token is still valid, and a long-lived client must not outlive it.
|
|
39
63
|
*/
|
|
40
64
|
async request(method, path, body) {
|
|
41
|
-
|
|
65
|
+
// baseUrl + path is the whole URL, so a path that does not start with "/" can
|
|
66
|
+
// rewrite the host ("https://backend.callkaro.ai" + "@evil.com/x") and carry
|
|
67
|
+
// the session cookie with it.
|
|
68
|
+
if (!path.startsWith('/'))
|
|
69
|
+
throw new Error(`Dashboard path must start with "/" (got "${path}").`);
|
|
70
|
+
const { token, sessionToken } = await (0, dashboardAuth_1.requireDashboard)();
|
|
42
71
|
const headers = { 'Content-Type': 'application/json' };
|
|
43
72
|
if (usesCookieAuth(path)) {
|
|
44
73
|
headers.Cookie = `access_token=${sessionToken}`;
|
|
@@ -52,6 +81,9 @@ class DashboardApi {
|
|
|
52
81
|
method,
|
|
53
82
|
headers,
|
|
54
83
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
84
|
+
// A redirect would re-send the cookie to wherever it points.
|
|
85
|
+
redirect: 'error',
|
|
86
|
+
signal: AbortSignal.timeout(30_000),
|
|
55
87
|
});
|
|
56
88
|
}
|
|
57
89
|
catch (err) {
|
|
@@ -66,7 +98,12 @@ class DashboardApi {
|
|
|
66
98
|
data = { msg: text.slice(0, 200) };
|
|
67
99
|
}
|
|
68
100
|
if (!res.ok) {
|
|
69
|
-
|
|
101
|
+
// Carry the status, not just the message. A caller that cannot tell 404
|
|
102
|
+
// from 500 has to treat every failure the same, and for a read-modify-write
|
|
103
|
+
// that is the difference between "no config yet" and "the read failed" —
|
|
104
|
+
// collapsing them sends a write that discards what it failed to read.
|
|
105
|
+
// Mirrors ApiError on the /cli/* side.
|
|
106
|
+
throw new DashboardError(res.status, data?.msg || data?.message || `Request failed (${res.status})`, data);
|
|
70
107
|
}
|
|
71
108
|
return data;
|
|
72
109
|
}
|
|
@@ -96,6 +133,17 @@ class DashboardApi {
|
|
|
96
133
|
return this.request('GET', `/pending-tasks${qs(query)}`);
|
|
97
134
|
}
|
|
98
135
|
teamMembers() { return this.request('GET', '/feedback/users/team-members'); }
|
|
136
|
+
// --- saved simulation variable sets ----------------------------------------
|
|
137
|
+
/** `agentId` is required; without it the backend answers 400 "Invalid agentId". */
|
|
138
|
+
testVariableSets(agentId) {
|
|
139
|
+
return this.request('GET', `/v1/call-metadata-test-variables${qs({ agentId })}`);
|
|
140
|
+
}
|
|
141
|
+
createTestVariableSet(body) {
|
|
142
|
+
return this.request('POST', '/v1/call-metadata-test-variables', body);
|
|
143
|
+
}
|
|
144
|
+
deleteTestVariableSet(id) {
|
|
145
|
+
return this.request('DELETE', `/v1/call-metadata-test-variables/${encodeURIComponent(id)}`);
|
|
146
|
+
}
|
|
99
147
|
// --- KPI board writes -----------------------------------------------------
|
|
100
148
|
//
|
|
101
149
|
// Routes, bodies and the column set below were read out of the dashboard's own
|
|
@@ -60,7 +60,7 @@ function msUntilExpiry(token) {
|
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* Password sign-in against the dashboard, exactly as the web form does it.
|
|
63
|
-
*
|
|
63
|
+
* Returns the session without the password; the caller decides whether to keep it.
|
|
64
64
|
*
|
|
65
65
|
* Two JWTs come back and both are kept, because they authorise different routes:
|
|
66
66
|
* `auth-token` in the body serves `GET /v2/agents/` as a bearer, while
|
|
@@ -133,34 +133,44 @@ function forgetDashboard(email) {
|
|
|
133
133
|
* Returns BOTH tokens, because the two halves of the dashboard need different
|
|
134
134
|
* ones. `dashboardApi.ts` picks per route; callers should not choose.
|
|
135
135
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* fails with the concrete next command
|
|
136
|
+
* The backend has no refresh endpoint (confirmed by capture), so a lapsed
|
|
137
|
+
* session is renewed by logging in again. With a stored password that happens
|
|
138
|
+
* here, silently; without one this fails with the concrete next command.
|
|
139
139
|
*/
|
|
140
|
-
function requireDashboard() {
|
|
140
|
+
async function requireDashboard() {
|
|
141
141
|
const slug = (0, config_1.activeAccount)();
|
|
142
142
|
const email = slug ? (0, config_1.loadAccount)(slug).email : undefined;
|
|
143
|
-
|
|
143
|
+
let session = dashboardSession(email);
|
|
144
144
|
if (!session?.token) {
|
|
145
145
|
throw new Error(`No dashboard session for ${email || 'the account in effect'}.\n` +
|
|
146
146
|
' Dashboard endpoints (/auth/*, /v1/*, /kpi-manager, /pending-tasks) need a dashboard login, not a ck_ token.\n' +
|
|
147
147
|
' Run: cku dashboard login');
|
|
148
148
|
}
|
|
149
|
-
if (!session.sessionToken) {
|
|
150
|
-
// A session stored before the two-token split. Re-issuing is the only fix;
|
|
151
|
-
// the cookie token cannot be derived from the body token.
|
|
152
|
-
throw new Error(`The stored dashboard session for ${email} is incomplete — it predates the two-token fix.\n` +
|
|
153
|
-
' /auth/login returns a separate access_token cookie that the dashboard routes require.\n' +
|
|
154
|
-
' Run: cku dashboard login');
|
|
155
|
-
}
|
|
156
149
|
// Expiry is read off the cookie token: it is the one that guards every route
|
|
157
150
|
// except /v2/agents/, so it is the one that matters for a session's lifetime.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
151
|
+
// A session stored before the two-token split has no cookie token, and that
|
|
152
|
+
// cannot be derived from the body token, so it needs a fresh login too.
|
|
153
|
+
// With a stored password, renew a minute early so the token cannot lapse
|
|
154
|
+
// mid-request; without one a token that still has time left is still valid.
|
|
155
|
+
const left = session.sessionToken ? msUntilExpiry(session.sessionToken) : 0;
|
|
156
|
+
if (left !== undefined && left <= (session.password ? 60_000 : 0)) {
|
|
157
|
+
if (!session.password) {
|
|
158
|
+
const why = session.sessionToken
|
|
159
|
+
? `Dashboard session for ${email} expired ${new Date(Date.now() + left).toLocaleString('en-US')}.\n` +
|
|
160
|
+
' There is no refresh endpoint — the token lives 24h and must be re-issued.'
|
|
161
|
+
: `The stored dashboard session for ${email} is incomplete — it predates the two-token fix.\n` +
|
|
162
|
+
' /auth/login returns a separate access_token cookie that the dashboard routes require.';
|
|
163
|
+
throw new Error(`${why}\n Run: cku dashboard login`);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const renewed = await dashboardLogin(session.email || email, session.password);
|
|
167
|
+
saveDashboard(email, { ...renewed, password: session.password });
|
|
168
|
+
session = renewed;
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
throw new Error(`Dashboard session for ${email} expired and re-login with the stored password failed: ${err.message}\n` +
|
|
172
|
+
' If the password changed, run: cku dashboard login');
|
|
173
|
+
}
|
|
164
174
|
}
|
|
165
175
|
return { token: session.token, sessionToken: session.sessionToken, email: session.email };
|
|
166
176
|
}
|
|
@@ -182,6 +192,7 @@ function expiryLabel(token) {
|
|
|
182
192
|
/** `cku dashboard login` — the browser's email+password flow, minus the browser. */
|
|
183
193
|
async function dashboardLoginCommand(opts) {
|
|
184
194
|
try {
|
|
195
|
+
(0, ui_1.needTty)('Dashboard login is interactive; sign in from a terminal.');
|
|
185
196
|
const answers = await (0, prompts_1.default)([
|
|
186
197
|
{ type: opts.email ? null : 'text', name: 'email', message: 'Email' },
|
|
187
198
|
{ type: 'password', name: 'password', message: 'Password' },
|
|
@@ -191,10 +202,18 @@ async function dashboardLoginCommand(opts) {
|
|
|
191
202
|
if (!email || !password)
|
|
192
203
|
return (0, ui_1.handleError)(new Error('Email and password are required'));
|
|
193
204
|
const session = await dashboardLogin(email, password);
|
|
194
|
-
|
|
205
|
+
// Kept by default so the session renews itself every 24h; --no-save-password
|
|
206
|
+
// opts out and brings back a daily re-login.
|
|
207
|
+
const keep = opts.savePassword !== false;
|
|
208
|
+
saveDashboard(email, keep ? { ...session, password } : session);
|
|
195
209
|
(0, ui_1.success)(`Dashboard session stored for ${session.email} (expires in ${expiryLabel(session.token)}).`);
|
|
196
|
-
(
|
|
197
|
-
|
|
210
|
+
if (keep) {
|
|
211
|
+
(0, ui_1.info)(' Password saved (plain text, mode 0600) so the session renews itself when it lapses.');
|
|
212
|
+
(0, ui_1.info)(' Remove it with `cku dashboard logout`, or sign in with --no-save-password.');
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
(0, ui_1.info)(' Password not saved. The token cannot be refreshed, so re-run this once it expires.');
|
|
216
|
+
}
|
|
198
217
|
}
|
|
199
218
|
catch (err) {
|
|
200
219
|
(0, ui_1.handleError)(err);
|
|
@@ -203,7 +222,7 @@ async function dashboardLoginCommand(opts) {
|
|
|
203
222
|
/** `cku dashboard whoami` — prove the stored session still works. */
|
|
204
223
|
async function dashboardWhoamiCommand(opts) {
|
|
205
224
|
try {
|
|
206
|
-
const { sessionToken, email } = requireDashboard();
|
|
225
|
+
const { sessionToken, email } = await requireDashboard();
|
|
207
226
|
const res = await fetch(`${(0, config_1.backendUrl)()}/auth/get-user`, {
|
|
208
227
|
headers: { Cookie: `access_token=${sessionToken}` },
|
|
209
228
|
});
|
|
@@ -238,7 +257,7 @@ async function dashboardProbeCommand() {
|
|
|
238
257
|
'/phone-number/',
|
|
239
258
|
];
|
|
240
259
|
try {
|
|
241
|
-
const { token, sessionToken, email } = requireDashboard();
|
|
260
|
+
const { token, sessionToken, email } = await requireDashboard();
|
|
242
261
|
const base = (0, config_1.backendUrl)();
|
|
243
262
|
const send = (path, which) => fetch(`${base}${path}`, {
|
|
244
263
|
headers: which === 'bearer' ? { Authorization: `Bearer ${token}` } : { Cookie: `access_token=${sessionToken}` },
|
|
@@ -318,16 +337,8 @@ async function dashboardLogoutCommand(opts) {
|
|
|
318
337
|
const email = slug ? (0, config_1.loadAccount)(slug).email : undefined;
|
|
319
338
|
if (!hasDashboard(email))
|
|
320
339
|
return void (0, ui_1.info)('No dashboard session stored for this account.');
|
|
321
|
-
if (!opts.yes)
|
|
322
|
-
|
|
323
|
-
type: 'confirm',
|
|
324
|
-
name: 'ok',
|
|
325
|
-
message: `Forget the dashboard session for ${email}? (your ck_ token is kept)`,
|
|
326
|
-
initial: false,
|
|
327
|
-
});
|
|
328
|
-
if (!ok)
|
|
329
|
-
return void (0, ui_1.info)('Cancelled.');
|
|
330
|
-
}
|
|
340
|
+
if (!(await (0, ui_1.confirm)(`Forget the dashboard session for ${email}? (your ck_ token is kept)`, opts.yes)))
|
|
341
|
+
return;
|
|
331
342
|
if (email && forgetDashboard(email))
|
|
332
343
|
(0, ui_1.success)(`Dashboard session removed for ${email}.`);
|
|
333
344
|
else
|
package/dist/lib/ui.js
CHANGED
|
@@ -6,11 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.fail = exports.info = exports.success = void 0;
|
|
7
7
|
exports.banner = banner;
|
|
8
8
|
exports.keyValue = keyValue;
|
|
9
|
+
exports.needTty = needTty;
|
|
10
|
+
exports.confirm = confirm;
|
|
9
11
|
exports.table = table;
|
|
10
12
|
exports.handleError = handleError;
|
|
11
13
|
exports.intFlag = intFlag;
|
|
12
14
|
const chalk_1 = __importDefault(require("chalk"));
|
|
13
15
|
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
16
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
14
17
|
const types_1 = require("./types");
|
|
15
18
|
const success = (msg) => console.log(chalk_1.default.green('✔') + ' ' + msg);
|
|
16
19
|
exports.success = success;
|
|
@@ -46,9 +49,35 @@ function keyValue(obj) {
|
|
|
46
49
|
console.log(`${chalk_1.default.dim(k.padEnd(width))} ${v}`);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Throw when a prompt is about to run with nobody to answer it. `prompts` waits
|
|
54
|
+
* on stdin forever in a non-interactive shell, so a script or agent hangs
|
|
55
|
+
* instead of failing. `hint` names the flag that skips the prompt.
|
|
56
|
+
*/
|
|
57
|
+
function needTty(hint) {
|
|
58
|
+
if (!process.stdin.isTTY) {
|
|
59
|
+
throw new Error(`This needs input, but there is no terminal to ask on. ${hint}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Ask y/N, defaulting to no. `--yes` skips it; no terminal and no `--yes` is an error, not a hang. */
|
|
63
|
+
async function confirm(message, yes) {
|
|
64
|
+
if (yes)
|
|
65
|
+
return true;
|
|
66
|
+
needTty('Re-run with --yes to confirm.');
|
|
67
|
+
const { ok } = await (0, prompts_1.default)({ type: 'confirm', name: 'ok', message, initial: false });
|
|
68
|
+
if (!ok)
|
|
69
|
+
(0, exports.info)('Cancelled.');
|
|
70
|
+
return Boolean(ok);
|
|
71
|
+
}
|
|
49
72
|
/** Render rows as a bordered table with a highlighted header. */
|
|
50
73
|
function table(headers, rows) {
|
|
51
|
-
|
|
74
|
+
// cli-table3 colours borders and header red/grey by default, independent of
|
|
75
|
+
// chalk, so piped output carried escape codes. Tie its style to chalk's level.
|
|
76
|
+
const colour = chalk_1.default.level > 0;
|
|
77
|
+
const t = new cli_table3_1.default({
|
|
78
|
+
head: headers.map((h) => chalk_1.default.cyan(h)),
|
|
79
|
+
style: { head: [], border: colour ? ['grey'] : [] },
|
|
80
|
+
});
|
|
52
81
|
for (const row of rows) {
|
|
53
82
|
t.push(row.map((cell) => (cell === undefined || cell === null ? '' : String(cell))));
|
|
54
83
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "callkaro-cli-unofficial",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"description": "Unofficial fork of the CallKaro CLI
|
|
5
|
+
"description": "Unofficial fork of the CallKaro CLI — manage your voice AI agents from the terminal",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cku": "dist/index.js"
|
|
8
8
|
},
|