callkaro-cli-unofficial 0.1.0 → 0.2.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 +68 -4
- package/dist/commands/accounts.js +4 -10
- package/dist/commands/agentPhone.js +38 -0
- package/dist/commands/agents.js +1 -0
- 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 +49 -1
- 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
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
An unofficial fork of the [CallKaro CLI](https://www.npmjs.com/package/@callkaro-official/cli). It installs as `cku`, so it sits alongside the official `ck` without either one overwriting the other.
|
|
4
4
|
|
|
5
|
-
Manage voice agents, calls, phone numbers, batch campaigns, simulations and analytics from the terminal.
|
|
5
|
+
Manage voice agents, calls, phone numbers, batch campaigns, simulations and analytics from the terminal — plus the dashboard side, which the official CLI doesn't reach at all.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
cku agents list
|
|
9
9
|
cku calls make --agent <id> --to <number> --test
|
|
10
|
+
cku kpi tasks --severity "Very High" --column action_items
|
|
10
11
|
cku accounts use tejas@acme.com
|
|
11
12
|
```
|
|
12
13
|
|
|
@@ -14,12 +15,12 @@ cku accounts use tejas@acme.com
|
|
|
14
15
|
|
|
15
16
|
## Why this exists
|
|
16
17
|
|
|
17
|
-
Three reasons, all of them things the official CLI can't do.
|
|
18
|
-
|
|
19
18
|
**It doesn't fight the official CLI for a name.** Both packages write to the same global `node_modules/.bin`. If both were called `ck`, whichever npm install ran last would silently take over. You'd type `ck`, get official behaviour, and get no signal anything had changed. `cku` cannot collide.
|
|
20
19
|
|
|
21
20
|
**It holds several accounts at once.** The official CLI keeps one session. This one keeps a file per account, and can pin an account to a folder, so two agents working in two directories can't switch each other out from under themselves.
|
|
22
21
|
|
|
22
|
+
**It reaches the dashboard API.** The official CLI only talks to `/cli/*`. That leaves the KPI board, the call analyzers, and the server's own model catalogue unreachable from a terminal — [`cku kpi`](#only-in-cku) is the whole of that surface.
|
|
23
|
+
|
|
23
24
|
**It fixes three upstream bugs.** Details and evidence in [`docs/BUG-REPORT.md`](docs/BUG-REPORT.md).
|
|
24
25
|
|
|
25
26
|
---
|
|
@@ -68,7 +69,7 @@ Every arrow above is a real import or a real call, checked against the source. `
|
|
|
68
69
|
/cli/* · 61 endpoints · Bearer ck_5b02…
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
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).
|
|
72
73
|
|
|
73
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.
|
|
74
75
|
|
|
@@ -120,6 +121,69 @@ My own first port of this fix had a worse version of the same bug: I merged agai
|
|
|
120
121
|
|
|
121
122
|
---
|
|
122
123
|
|
|
124
|
+
## Only in `cku`
|
|
125
|
+
|
|
126
|
+
Commands and capabilities the official CLI does not have at all. Not bug fixes — surface area.
|
|
127
|
+
|
|
128
|
+
### The KPI manager and the pending-task board
|
|
129
|
+
|
|
130
|
+
The official `ck` has **no KPI commands whatsoever**. This is the only way to reach the board from a terminal.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
cku kpi tasks --severity "Very High" --column action_items --since 7d
|
|
134
|
+
cku kpi show "<taskId>" # cause, recommended fix, metric impact
|
|
135
|
+
cku kpi create --cause "…" --fix "…" --severity High
|
|
136
|
+
cku kpi move "<taskId>" in_progress
|
|
137
|
+
cku kpi comment "<taskId>" "Fixed in v3"
|
|
138
|
+
cku kpi assign "<taskId>" --to "Name" # or --to ai_fde
|
|
139
|
+
cku kpi target --agent <id> --value 25
|
|
140
|
+
cku kpi rca --agent <id> --since 2026-09-01 --until 2026-09-30
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
There is **no delete** for a card — the board retires one by moving it to `rejected`, which is reversible. Only `description` is editable after creation; `cause`, fix and severity are fixed at create, so correcting one means creating a replacement.
|
|
144
|
+
|
|
145
|
+
`GET /pending-tasks` returns every column including `done` and `rejected`, so a closed card is not a gone card. The list prints a **COLUMN** column for exactly that reason, and `--column` filters on it.
|
|
146
|
+
|
|
147
|
+
### The dashboard credential
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
cku dashboard login # separate from `cku login`; the token lasts 24h, renewed silently
|
|
151
|
+
cku dashboard whoami
|
|
152
|
+
cku dashboard probe # which dashboard routes this token reaches
|
|
153
|
+
```
|
|
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`. 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
|
+
|
|
157
|
+
### Knowing which models are actually valid
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
cku models # per slot, and which field each governs
|
|
161
|
+
cku models --slot agent
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Models are scoped per slot and **the slots disagree**: `agent` has 44, `post-call` has 9 and excludes `gpt-4o` and `o4-mini`. So the same value is valid in `model` and refused in `postcallmodel`. Works with or without a dashboard session, and says which source it read.
|
|
165
|
+
|
|
166
|
+
### A catalogue that can't silently rot
|
|
167
|
+
|
|
168
|
+
The provider tables, and now the model lists, are regenerated from the server rather than hand-copied:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm run catalog:sync # refresh from the bundle + the live server
|
|
172
|
+
npm run catalog:check # fail if catalog.json disagrees with the snapshot
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`catalog:check` runs offline against a committed snapshot, so it works in CI with no credentials. On 1 October 2026 it caught shipped tables that listed withdrawn models *and* missed live ones in both directions. A wrong voice model otherwise persists silently and only surfaces as a broken call.
|
|
176
|
+
|
|
177
|
+
### Smaller things
|
|
178
|
+
|
|
179
|
+
- `cku calls logs` — grep/awk/sed over the raw call log, in-process, no subprocess and no shell
|
|
180
|
+
- `cku accounts status` — reports which account is in effect **and where that choice came from**
|
|
181
|
+
- Per-project binding, so parallel agents can't redirect each other
|
|
182
|
+
- `secrets set` without the value on the command line
|
|
183
|
+
- `--json` that stays unpaged for scripting, with human tables paged
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
123
187
|
## Staying current
|
|
124
188
|
|
|
125
189
|
A daily workflow checks npm for a newer `@callkaro-official/cli` and, when it finds one, regenerates the provider catalogue and opens a PR onto the `official` branch. It never merges anything itself.
|
|
@@ -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',
|
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')
|
|
@@ -325,6 +336,14 @@ agents.command('set-outbound <agentId>').description("set/clear an agent's outbo
|
|
|
325
336
|
.option('--number <id>', 'phone number id')
|
|
326
337
|
.option('--clear', 'clear the outbound number')
|
|
327
338
|
.action(agentPhone_1.setOutboundCommand);
|
|
339
|
+
agents.command('set-whatsapp-inbound <agentId>').description("set/clear an agent's WhatsApp inbound number")
|
|
340
|
+
.option('--number <id>', 'WhatsApp phone number id (see `cku whatsapp list`)')
|
|
341
|
+
.option('--clear', 'clear the inbound number')
|
|
342
|
+
.action(agentPhone_1.setWhatsappInboundCommand);
|
|
343
|
+
agents.command('set-whatsapp-outbound <agentId>').description("set/clear an agent's WhatsApp outbound number")
|
|
344
|
+
.option('--number <id>', 'WhatsApp phone number id (see `cku whatsapp list`)')
|
|
345
|
+
.option('--clear', 'clear the outbound number')
|
|
346
|
+
.action(agentPhone_1.setWhatsappOutboundCommand);
|
|
328
347
|
// Version management: clone, publish, activate, A/B testing
|
|
329
348
|
agents.command('clone-version <agentId>').description('clone a version with a required target prompt mode and language')
|
|
330
349
|
.option('--versions <id>', 'source version id')
|
|
@@ -419,6 +438,13 @@ addSkillsProviderOptions(skills.command('dir'))
|
|
|
419
438
|
.description('print the selected install location(s)')
|
|
420
439
|
.option('--project', 'print project location(s)')
|
|
421
440
|
.action(skills_1.skillsDirCommand);
|
|
441
|
+
// --- whatsapp --------------------------------------------------------------
|
|
442
|
+
// Numbers come from your linked Meta WhatsApp Business Account, not a platform
|
|
443
|
+
// pool: add one on the dashboard ("+ Add Phone Number", a Facebook-login flow the
|
|
444
|
+
// CLI cannot run), then list/assign it here. No catalog/buy/register.
|
|
445
|
+
const whatsapp = program.command('whatsapp').description('manage WhatsApp numbers (from your linked Meta WhatsApp Business Account)');
|
|
446
|
+
whatsapp.command('list').description('list WhatsApp numbers on your linked WhatsApp Business Account')
|
|
447
|
+
.option('--json', 'raw JSON').action(whatsapp_1.whatsappListCommand);
|
|
422
448
|
// --- numbers ---------------------------------------------------------------
|
|
423
449
|
const numbers = program.command('numbers').description('manage phone numbers');
|
|
424
450
|
numbers.command('list').description('list your phone numbers').option('--json', 'raw JSON').action(numbers_1.numbersListCommand);
|
|
@@ -506,7 +532,9 @@ calls.command('make').description('place a single outbound call (real, or --test
|
|
|
506
532
|
.action(calls_1.callsMakeCommand);
|
|
507
533
|
addCallFilters(calls.command('list').description('list call history'))
|
|
508
534
|
.option('--limit <n>', 'page size (default 50)')
|
|
509
|
-
.option('--
|
|
535
|
+
.option('--page <n>', 'page number (default 1)')
|
|
536
|
+
.option('--json', 'raw JSON (a bare array of calls)')
|
|
537
|
+
.option('--meta', 'with --json, wrap as {calls, pagination} so scripts can see totalDocs')
|
|
510
538
|
.action(calls_1.callsListCommand);
|
|
511
539
|
calls.command('get <callId>').description('show one call (with recording URL)')
|
|
512
540
|
.option('--json', 'raw JSON')
|
|
@@ -601,6 +629,8 @@ sim.command('create <agentId>').description('create a test case')
|
|
|
601
629
|
.option('--prompt <text>', 'simulated user prompt')
|
|
602
630
|
.option('--criteria <text>', 'success criteria')
|
|
603
631
|
.option('--model <model>', 'test LLM model')
|
|
632
|
+
.option('--variables <json|@file>', 'testVariables: a JSON object of name -> value (non-strings are stringified)')
|
|
633
|
+
.option('--functions <json|@file>', 'function mocks: a JSON array of {"name": ..., "response": ...}')
|
|
604
634
|
.action(simulations_1.simCreateCommand);
|
|
605
635
|
sim.command('delete <testCaseId>').description('delete a test case').option('--yes', 'skip confirmation')
|
|
606
636
|
.action(simulations_1.simDeleteCommand);
|
|
@@ -608,10 +638,28 @@ sim.command('run <agentId>').description('run a batch simulation (async)')
|
|
|
608
638
|
.option('--tests <ids>', 'comma-separated test case ids')
|
|
609
639
|
.option('--versions <ids>', 'comma-separated version ids')
|
|
610
640
|
.option('--name <name>', 'run name')
|
|
641
|
+
.option('--epoch <n>', 'times to run each test case (default 1)')
|
|
642
|
+
.option('--wait', 'block until every result is in; exit 1 if any test fails')
|
|
643
|
+
.option('--timeout <seconds>', 'with --wait, give up after this long (default 600)')
|
|
644
|
+
.option('--json', 'with --wait, print the results as a bare JSON array')
|
|
611
645
|
.action(simulations_1.simRunCommand);
|
|
612
646
|
sim.command('runs <agentId>').description('list simulation runs').option('--json', 'raw JSON').action(simulations_1.simRunsListCommand);
|
|
613
647
|
sim.command('results <batchId>').description('results of a simulation run').option('--json', 'raw JSON')
|
|
648
|
+
.option('--wait', 'block until the batch has all its results (needs --agent)')
|
|
649
|
+
.option('--agent <agentId>', 'with --wait, the agent the batch belongs to')
|
|
650
|
+
.option('--timeout <seconds>', 'with --wait, give up after this long (default 600)')
|
|
614
651
|
.action(simulations_1.simResultsCommand);
|
|
652
|
+
// Saved variable sets are dashboard-only (cookie-authed): they need `cku dashboard login`.
|
|
653
|
+
const simVars = sim.command('variables').description('saved variable sets for test cases (needs `cku dashboard login`)');
|
|
654
|
+
simVars.command('list <agentId>').description('list saved variable sets').option('--json', 'raw JSON')
|
|
655
|
+
.action(simVariables_1.simVarsListCommand);
|
|
656
|
+
simVars.command('show <agentId> <nameOrId>').description('show one saved set').option('--json', 'bare variables object')
|
|
657
|
+
.action(simVariables_1.simVarsShowCommand);
|
|
658
|
+
simVars.command('create <agentId> <name>').description('save a variable set')
|
|
659
|
+
.option('--variables <json|@file>', 'a JSON object of name -> value')
|
|
660
|
+
.action(simVariables_1.simVarsCreateCommand);
|
|
661
|
+
simVars.command('delete <agentId> <nameOrId>').description('delete a saved set').option('--yes', 'skip confirmation')
|
|
662
|
+
.action(simVariables_1.simVarsDeleteCommand);
|
|
615
663
|
// --- audit strategies ------------------------------------------------------
|
|
616
664
|
const audit = program.command('audit-strategies').alias('audit').description('AI Auditor audit strategies');
|
|
617
665
|
audit.command('list <agentId>').description('list audit strategies for an agent').option('--json', 'raw JSON')
|
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.0",
|
|
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
|
},
|