callkaro-cli-unofficial 0.2.1 → 0.4.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 +264 -260
- package/dist/commands/accounts.js +6 -0
- package/dist/commands/agentChat.js +166 -0
- package/dist/commands/calls.js +22 -0
- package/dist/commands/monitor.js +72 -0
- package/dist/commands/scheduled.js +92 -0
- package/dist/commands/simulations.js +32 -0
- package/dist/index.js +77 -6
- package/dist/lib/api.js +16 -4
- package/dist/lib/config.js +16 -1
- package/dist/lib/dashboardApi.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,260 +1,264 @@
|
|
|
1
|
-
# cku
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
```
|
|
8
|
-
cku agents list
|
|
9
|
-
cku calls make --agent <id> --to <number> --test
|
|
10
|
-
cku kpi tasks --severity "Very High" --column action_items
|
|
11
|
-
cku accounts use tejas@acme.com
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## Why this exists
|
|
17
|
-
|
|
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.
|
|
19
|
-
|
|
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.
|
|
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
|
-
|
|
24
|
-
**It fixes three upstream bugs.** Details and evidence in [`docs/BUG-REPORT.md`](docs/BUG-REPORT.md).
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Install
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm install --global callkaro-cli-unofficial
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Or, to work on it directly:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
git clone https://github.com/TejasLamba2006/callkaro-cli-unofficial
|
|
38
|
-
cd callkaro-cli-unofficial
|
|
39
|
-
npm install && npm run build && npm link
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Node 18 or newer.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## How it fits together
|
|
47
|
-
|
|
48
|
-

|
|
49
|
-
|
|
50
|
-
Every arrow above is a real import or a real call, checked against the source. `api.ts` is the only thing that touches the network. `config.ts` is the only thing that reads a token, which is why every route to the backend goes through it. The ASCII version below says the same thing for the terminal:
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
┌─ cku ─────────────────────────────────────────────┐
|
|
54
|
-
│ index.ts commander wiring, 44 subcommands │
|
|
55
|
-
│ ↓ │
|
|
56
|
-
│ commands/ agents, calls, numbers, batches, │
|
|
57
|
-
│ sim, analytics, audits, skills │
|
|
58
|
-
│ ↓ │
|
|
59
|
-
│ lib/ api.ts, config.ts, agentUpdate.ts, │
|
|
60
|
-
│ looseJson.ts, ui.ts, catalog.ts │
|
|
61
|
-
└──────┬────────────────────────────────────────────┘
|
|
62
|
-
↓
|
|
63
|
-
account resolution, highest precedence first
|
|
64
|
-
│
|
|
65
|
-
CK_TOKEN → CK_ACCOUNT → ./.cku.json → global → none
|
|
66
|
-
│
|
|
67
|
-
↓
|
|
68
|
-
backend.callkaro.ai
|
|
69
|
-
/cli/* · 61 endpoints · Bearer ck_5b02…
|
|
70
|
-
```
|
|
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
|
|
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.
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Several accounts
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
cku login # add an account
|
|
82
|
-
cku accounts # list them, * marks the active one
|
|
83
|
-
cku accounts use <email> # change the global default
|
|
84
|
-
cku accounts remove <email> # forget one
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Pin an account to a folder
|
|
88
|
-
|
|
89
|
-
If you're running more than one agent at a time, this is the part that matters.
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
cd ~/work/bot-alpha && cku accounts bind alpha@acme.com
|
|
93
|
-
cd ~/work/bot-beta && cku accounts bind beta@other.com
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`bind` writes a `.cku.json` into that directory containing just the account slug and no token, so it's safe to commit. Every `cku` command in that folder (and anything nested under it) uses that account, regardless of what the global default says.
|
|
97
|
-
|
|
98
|
-
Why this matters: `cku accounts use` changes **shared** state. Two agents in two folders both calling it would overwrite each other, and whichever wrote last would silently redirect the other bot's commands to the wrong CallKaro account. No error, just wrong data. A bound folder never reads shared state at all, so that can't happen.
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
cku accounts status
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
tells you which account is in effect *and where the choice came from*: env var, this folder, or the global default. Worth running first when something looks off, since acting on the wrong account is the most damaging mistake available here.
|
|
105
|
-
|
|
106
|
-
Override for a single command with `CK_ACCOUNT=<email> cku whoami`.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Fixes over the official CLI
|
|
111
|
-
|
|
112
|
-
Full write-ups, with line numbers and reproduction steps, in [`docs/BUG-REPORT.md`](docs/BUG-REPORT.md).
|
|
113
|
-
|
|
114
|
-
**`agents update --set` silently deleted agent configuration.** Send a partial `voice_configuration` and the backend replaces the whole object, so nine keys, including the voice itself, were dropped, and the command reported success. The agent kept working; it just couldn't make calls. The fix already exists upstream as `deepMerge`, with a comment describing exactly this problem. Nothing ever called it.
|
|
115
|
-
|
|
116
|
-
**`skills install` failed on every run on Windows.** `tar` was handed an absolute path, and `C:/Users/...` gets read as a `<host>:<path>` remote spec, so tar tried to connect to a machine named "C". Since the temp path always sits under a drive root, this never worked there. It's also the only way to install the skill pack, so an agent couldn't bootstrap itself on Windows. Linux and macOS were never affected; no drive letters, no colon in a path.
|
|
117
|
-
|
|
118
|
-
**`agents export` can't always be re-imported.** For some agents the export response omits a field the CLI's own validator requires, so the round trip fails. That part needs a backend fix; a workaround is in the `callkaro-cli` skill.
|
|
119
|
-
|
|
120
|
-
My own first port of this fix had a worse version of the same bug: I merged against the agent's default version, so updating any other version could swap its voice for the default's. That's fixed and covered by a test, and the details are in [`docs/official-fixes.md`](docs/official-fixes.md).
|
|
121
|
-
|
|
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
|
|
180
|
-
- `cku
|
|
181
|
-
-
|
|
182
|
-
- `
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
npm run
|
|
212
|
-
npm
|
|
213
|
-
npm run
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1
|
+
# cku
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
|
|
7
|
+
```
|
|
8
|
+
cku agents list
|
|
9
|
+
cku calls make --agent <id> --to <number> --test
|
|
10
|
+
cku kpi tasks --severity "Very High" --column action_items
|
|
11
|
+
cku accounts use tejas@acme.com
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why this exists
|
|
17
|
+
|
|
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.
|
|
19
|
+
|
|
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.
|
|
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
|
+
|
|
24
|
+
**It fixes three upstream bugs.** Details and evidence in [`docs/BUG-REPORT.md`](docs/BUG-REPORT.md).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install --global callkaro-cli-unofficial
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or, to work on it directly:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/TejasLamba2006/callkaro-cli-unofficial
|
|
38
|
+
cd callkaro-cli-unofficial
|
|
39
|
+
npm install && npm run build && npm link
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Node 18 or newer.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## How it fits together
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
Every arrow above is a real import or a real call, checked against the source. `api.ts` is the only thing that touches the network. `config.ts` is the only thing that reads a token, which is why every route to the backend goes through it. The ASCII version below says the same thing for the terminal:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
┌─ cku ─────────────────────────────────────────────┐
|
|
54
|
+
│ index.ts commander wiring, 44 subcommands │
|
|
55
|
+
│ ↓ │
|
|
56
|
+
│ commands/ agents, calls, numbers, batches, │
|
|
57
|
+
│ sim, analytics, audits, skills │
|
|
58
|
+
│ ↓ │
|
|
59
|
+
│ lib/ api.ts, config.ts, agentUpdate.ts, │
|
|
60
|
+
│ looseJson.ts, ui.ts, catalog.ts │
|
|
61
|
+
└──────┬────────────────────────────────────────────┘
|
|
62
|
+
↓
|
|
63
|
+
account resolution, highest precedence first
|
|
64
|
+
│
|
|
65
|
+
CK_TOKEN → CK_ACCOUNT → ./.cku.json → global → none
|
|
66
|
+
│
|
|
67
|
+
↓
|
|
68
|
+
backend.callkaro.ai
|
|
69
|
+
/cli/* · 61 endpoints · Bearer ck_5b02…
|
|
70
|
+
```
|
|
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 v5.0.0 (the official CLI's commands, its `accounts` group renamed `monitor`, plus the `accounts` group and the dashboard-side `dashboard`, `kpi`, `analyzers` and `models` groups).
|
|
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.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Several accounts
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cku login # add an account
|
|
82
|
+
cku accounts # list them, * marks the active one
|
|
83
|
+
cku accounts use <email> # change the global default
|
|
84
|
+
cku accounts remove <email> # forget one
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Pin an account to a folder
|
|
88
|
+
|
|
89
|
+
If you're running more than one agent at a time, this is the part that matters.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd ~/work/bot-alpha && cku accounts bind alpha@acme.com
|
|
93
|
+
cd ~/work/bot-beta && cku accounts bind beta@other.com
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`bind` writes a `.cku.json` into that directory containing just the account slug and no token, so it's safe to commit. Every `cku` command in that folder (and anything nested under it) uses that account, regardless of what the global default says.
|
|
97
|
+
|
|
98
|
+
Why this matters: `cku accounts use` changes **shared** state. Two agents in two folders both calling it would overwrite each other, and whichever wrote last would silently redirect the other bot's commands to the wrong CallKaro account. No error, just wrong data. A bound folder never reads shared state at all, so that can't happen.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
cku accounts status
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
tells you which account is in effect *and where the choice came from*: env var, this folder, or the global default. Worth running first when something looks off, since acting on the wrong account is the most damaging mistake available here.
|
|
105
|
+
|
|
106
|
+
Override for a single command with `CK_ACCOUNT=<email> cku whoami`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Fixes over the official CLI
|
|
111
|
+
|
|
112
|
+
Full write-ups, with line numbers and reproduction steps, in [`docs/BUG-REPORT.md`](docs/BUG-REPORT.md).
|
|
113
|
+
|
|
114
|
+
**`agents update --set` silently deleted agent configuration.** Send a partial `voice_configuration` and the backend replaces the whole object, so nine keys, including the voice itself, were dropped, and the command reported success. The agent kept working; it just couldn't make calls. The fix already exists upstream as `deepMerge`, with a comment describing exactly this problem. Nothing ever called it.
|
|
115
|
+
|
|
116
|
+
**`skills install` failed on every run on Windows.** `tar` was handed an absolute path, and `C:/Users/...` gets read as a `<host>:<path>` remote spec, so tar tried to connect to a machine named "C". Since the temp path always sits under a drive root, this never worked there. It's also the only way to install the skill pack, so an agent couldn't bootstrap itself on Windows. Linux and macOS were never affected; no drive letters, no colon in a path.
|
|
117
|
+
|
|
118
|
+
**`agents export` can't always be re-imported.** For some agents the export response omits a field the CLI's own validator requires, so the round trip fails. That part needs a backend fix; a workaround is in the `callkaro-cli` skill.
|
|
119
|
+
|
|
120
|
+
My own first port of this fix had a worse version of the same bug: I merged against the agent's default version, so updating any other version could swap its voice for the default's. That's fixed and covered by a test, and the details are in [`docs/official-fixes.md`](docs/official-fixes.md).
|
|
121
|
+
|
|
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 agents chat` — text chat with an agent's real prompt and tools, no phone call. Scriptable with `--say`, replayable with `--history`. Custom in-call functions run for real
|
|
180
|
+
- `cku scheduled list|queued|get` — scheduled calls and the queue of what will actually dial (dashboard login)
|
|
181
|
+
- `cku sim update` — edit a test case in place
|
|
182
|
+
- `cku calls list --postcall dnc_requested=true --provider-type plivo --sid <sid>` — filters the official CLI doesn't expose
|
|
183
|
+
- `cku calls logs` — grep/awk/sed over the raw call log, in-process, no subprocess and no shell
|
|
184
|
+
- `cku accounts status` — reports which account is in effect **and where that choice came from**
|
|
185
|
+
- Per-project binding, so parallel agents can't redirect each other
|
|
186
|
+
- `secrets set` without the value on the command line
|
|
187
|
+
- `--json` that stays unpaged for scripting, with human tables paged
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Staying current
|
|
192
|
+
|
|
193
|
+
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.
|
|
194
|
+
|
|
195
|
+
| Branch | What's on it |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `official` | Clean base. Changes only arrive through sync PRs. |
|
|
198
|
+
| `main` | `official` plus your customizations. Your working branch. |
|
|
199
|
+
|
|
200
|
+
To take an update: review the PR, merge into `official`, then merge `official` into `main`.
|
|
201
|
+
|
|
202
|
+
Worth knowing what the sync does and doesn't do. It regenerates `src/data/catalog.json` — the provider tables and the per-slot LLM model lists — because those change when the dashboard changes. It does **not** port command code, because the official source isn't available, so that's maintained by hand. The PR lists which commands were added or removed so you know what to look at.
|
|
203
|
+
|
|
204
|
+
Keep your customizations in as few files as you can. That diff is the one you have to resolve on every update.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run build # tsc -> dist/, copy endpoints.json, chmod +x
|
|
212
|
+
npm test # build, then node --test
|
|
213
|
+
npm run test:only # tests without rebuilding
|
|
214
|
+
npm run dev # build and run
|
|
215
|
+
npm run catalog:sync # regenerate the catalogue: provider tables + per-slot LLM lists, from the server
|
|
216
|
+
npm run catalog:check # fail if catalog.json disagrees with the server snapshot (offline)
|
|
217
|
+
npm run catalog:check:live # same, against the live server (needs `cku dashboard login`)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Single test file:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
node --test tests/agentUpdate.test.js
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
269 tests. No linter or formatter is configured.
|
|
227
|
+
|
|
228
|
+
### Layout
|
|
229
|
+
|
|
230
|
+
`src/index.ts` is commander wiring and almost nothing else. Every command, flag and description is declared there. `src/commands/` holds one module per command group. `src/lib/` holds the shared layer, and that's where the rules worth knowing live:
|
|
231
|
+
|
|
232
|
+
- **`api.ts`** is the only place that talks to the backend. Adding an endpoint means adding a method here, not a raw `fetch` in a command.
|
|
233
|
+
- **`looseJson.ts`** has two parsers, chosen by where the text came from. Values from argv get a repair pass, because PowerShell strips quotes out of single-quoted arguments and `--set '{"name":"x"}'` arrives as `{name:x}`. Values from files or stdin are parsed strictly, since the shell never touched them and a real typo should be reported rather than guessed at.
|
|
234
|
+
- **`agentUpdate.ts`** merges updates into objects the backend replaces wholesale, so a partial update doesn't drop its siblings.
|
|
235
|
+
- **`ui.ts`** sets `process.exitCode` rather than calling `process.exit()`. On Windows, exiting while a fetch socket is still closing trips a libuv assertion that aborts with code 127 and a pile of C-level noise.
|
|
236
|
+
|
|
237
|
+
### Regenerating docs
|
|
238
|
+
|
|
239
|
+
Both of these read from the code, so they can't drift:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
node scripts/gen-api-inventory.js # endpoint tables in docs/api.md
|
|
243
|
+
node scripts/gen-cli-reference.js ../callkaro-skills/skills/callkaro-cli/REFERENCE.md
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Skills
|
|
249
|
+
|
|
250
|
+
The CLI installs a skill pack for Claude Code and other coding agents, so an agent can drive `cku` without being told how.
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
cku skills install --claude # this fork's pack
|
|
254
|
+
cku skills install --claude --official # the upstream pack
|
|
255
|
+
cku skills update # refresh every install
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Targets include `--cursor --copilot --gemini --windsurf --roo --opencode --cline --codex --all`, plus `--project` to install into a repo rather than your user account. Skills ship from GitHub, so a new one reaches users without a CLI release.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
Apache-2.0, matching the upstream CLI.
|
|
@@ -116,12 +116,15 @@ async function whoamiCommand(opts) {
|
|
|
116
116
|
if (opts.json)
|
|
117
117
|
return void console.log(JSON.stringify(res, null, 2));
|
|
118
118
|
const user = res.user || res;
|
|
119
|
+
const monitoring = res.monitoring?.active ? res.monitoring.account : undefined;
|
|
119
120
|
if (!user?.email) {
|
|
120
121
|
(0, ui_1.fail)('Could not read your account details. Run `cku login`.');
|
|
121
122
|
process.exitCode = 1;
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
(0, ui_1.success)(`Logged in as ${user.email}`);
|
|
126
|
+
if (monitoring)
|
|
127
|
+
(0, ui_1.info)(` MONITORING ${monitoring.name || monitoring.email} (${monitoring.id})`);
|
|
125
128
|
}
|
|
126
129
|
catch (err) {
|
|
127
130
|
(0, ui_1.handleError)(err);
|
|
@@ -148,6 +151,9 @@ async function configCommand() {
|
|
|
148
151
|
else {
|
|
149
152
|
(0, ui_1.info)(' session not signed in — run `cku login`');
|
|
150
153
|
}
|
|
154
|
+
const monitored = process.env.CK_ACCOUNT_ID || cfg.monitoring?.id;
|
|
155
|
+
if (monitored)
|
|
156
|
+
(0, ui_1.info)(` monitoring ${monitored} (acting on a client account — \`cku monitor clear\` to leave)`);
|
|
151
157
|
(0, ui_1.info)(` accounts ${accounts.length} saved${accounts.length ? ` — \`cku accounts list\`` : ''}`);
|
|
152
158
|
}
|
|
153
159
|
/** `cku accounts` — list / switch / add / remove stored logins. */
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.agentChatCommand = agentChatCommand;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const session_1 = require("../lib/session");
|
|
11
|
+
const ui_1 = require("../lib/ui");
|
|
12
|
+
const versions_1 = require("../lib/versions");
|
|
13
|
+
const types_1 = require("../lib/types");
|
|
14
|
+
const jsonInput_1 = require("./jsonInput");
|
|
15
|
+
/**
|
|
16
|
+
* Text-only conversation with an agent over POST /cli/simulations/generate-reply.
|
|
17
|
+
* One request per user turn: the server replays the whole history through the
|
|
18
|
+
* agent's real prompt and tools and returns the next agent turn. No call, no
|
|
19
|
+
* telephony spend.
|
|
20
|
+
*
|
|
21
|
+
* Tools: the Python side mocks end_call, transfer_call, warm_transfer,
|
|
22
|
+
* keep_call_on_hold, change_conversation_language, send_booking_link_to_whatsapp,
|
|
23
|
+
* use_knowledge_base and collect_digits_via_dtmf. Every other function RUNS FOR
|
|
24
|
+
* REAL (and falls back to a mock if it throws) — a CRM write in an in-call
|
|
25
|
+
* function really happens.
|
|
26
|
+
*/
|
|
27
|
+
const MOCKED_TOOLS = new Set([
|
|
28
|
+
'end_call', 'transfer_call', 'warm_transfer', 'keep_call_on_hold',
|
|
29
|
+
'change_conversation_language', 'send_booking_link_to_whatsapp',
|
|
30
|
+
'use_knowledge_base', 'collect_digits_via_dtmf',
|
|
31
|
+
]);
|
|
32
|
+
/** Agent text of a message item; the server returns `content` as an array of strings. */
|
|
33
|
+
const text = (item) => Array.isArray(item.content) ? item.content.join(' ') : String(item.content ?? '');
|
|
34
|
+
const isEnd = (items) => items.some((i) => i.type === 'function_call' && i.name === 'end_call');
|
|
35
|
+
/** Turn the server's error for a bad version or history into one that names the fix. */
|
|
36
|
+
function explain(err, versionId) {
|
|
37
|
+
if (!(err instanceof types_1.ApiError))
|
|
38
|
+
return err;
|
|
39
|
+
if (err.status === 502 && /Invalid version id|Agent version not found/i.test(err.message)) {
|
|
40
|
+
return new Error(`Version ${versionId} is not a version of this agent. List them with: cku agents versions <agentId>`);
|
|
41
|
+
}
|
|
42
|
+
if (err.status === 400 && /must end with a user message/i.test(err.message)) {
|
|
43
|
+
return new Error('The chat history must end with a user turn. Add one with --say "<text>".');
|
|
44
|
+
}
|
|
45
|
+
return err;
|
|
46
|
+
}
|
|
47
|
+
function printItems(items, warned) {
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
if (item.type === 'message' && item.role === 'assistant') {
|
|
50
|
+
(0, ui_1.info)(`${chalk_1.default.cyan('agent>')} ${text(item)}`);
|
|
51
|
+
}
|
|
52
|
+
else if (item.type === 'function_call') {
|
|
53
|
+
const result = JSON.stringify(item.extra?.result ?? null);
|
|
54
|
+
(0, ui_1.info)(chalk_1.default.dim(` → ${item.name} ${item.arguments || '{}'} = ${result}`));
|
|
55
|
+
if (!MOCKED_TOOLS.has(item.name) && !warned.has(item.name)) {
|
|
56
|
+
warned.add(item.name);
|
|
57
|
+
console.error(chalk_1.default.yellow(` ! ${item.name} is not mocked: it ran for real.`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function buildMetadata(opts) {
|
|
63
|
+
const metadata = {};
|
|
64
|
+
if (opts.metadata) {
|
|
65
|
+
const parsed = await (0, jsonInput_1.readJsonFlag)(opts.metadata, '--metadata');
|
|
66
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
67
|
+
throw new Error('--metadata must be a JSON object, e.g. {"customer_name":"Ravi"}');
|
|
68
|
+
}
|
|
69
|
+
Object.assign(metadata, parsed);
|
|
70
|
+
}
|
|
71
|
+
for (const pair of opts.var || []) {
|
|
72
|
+
const idx = pair.indexOf('=');
|
|
73
|
+
if (idx < 1)
|
|
74
|
+
throw new Error(`Bad --var "${pair}". Use key=value, e.g. --var customer_name=Ravi`);
|
|
75
|
+
metadata[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
76
|
+
}
|
|
77
|
+
return metadata;
|
|
78
|
+
}
|
|
79
|
+
async function agentChatCommand(agentId, opts) {
|
|
80
|
+
const api = (0, session_1.requireApi)();
|
|
81
|
+
try {
|
|
82
|
+
const versionId = (0, versions_1.firstVersion)(opts.versions);
|
|
83
|
+
const metadata = await buildMetadata(opts);
|
|
84
|
+
let history = [];
|
|
85
|
+
if (opts.history) {
|
|
86
|
+
const parsed = await (0, jsonInput_1.readJsonFlag)(opts.history, '--history');
|
|
87
|
+
if (!Array.isArray(parsed))
|
|
88
|
+
throw new Error('--history must be a JSON array of chat items (the output of --json).');
|
|
89
|
+
history = parsed;
|
|
90
|
+
}
|
|
91
|
+
const scripted = (opts.say?.length ?? 0) > 0 || Boolean(opts.history);
|
|
92
|
+
if (!scripted)
|
|
93
|
+
(0, ui_1.needTty)('Pass --say "<text>" (repeatable) or --history @file.json to run without a terminal.');
|
|
94
|
+
// Python requires userId in the body; take it from the token's own account.
|
|
95
|
+
const me = await api.me();
|
|
96
|
+
const userId = me.user?.id;
|
|
97
|
+
if (!userId)
|
|
98
|
+
throw new Error('Could not read your account id from /cli/auth/me.');
|
|
99
|
+
const warned = new Set();
|
|
100
|
+
let promptShown = false;
|
|
101
|
+
let ended = false;
|
|
102
|
+
const turn = async () => {
|
|
103
|
+
const res = await api
|
|
104
|
+
.generateReply({ userId, agent_id: agentId, chat_history: history, configuration: {}, epoch: 1, metadata }, versionId)
|
|
105
|
+
.catch((e) => { throw explain(e, versionId); });
|
|
106
|
+
if (opts.showPrompt && !promptShown) {
|
|
107
|
+
promptShown = true;
|
|
108
|
+
const sys = (res.messages || []).find((m) => m.role === 'system');
|
|
109
|
+
if (sys)
|
|
110
|
+
(0, ui_1.info)(chalk_1.default.dim(`--- system prompt ---\n${sys.content}\n---------------------`));
|
|
111
|
+
}
|
|
112
|
+
const items = res.replies?.[0] || [];
|
|
113
|
+
history.push(...items);
|
|
114
|
+
if (!opts.json)
|
|
115
|
+
printItems(items, warned);
|
|
116
|
+
ended = isEnd(items);
|
|
117
|
+
};
|
|
118
|
+
const userSays = async (line) => {
|
|
119
|
+
history.push({ type: 'message', role: 'user', content: line });
|
|
120
|
+
if (!opts.json)
|
|
121
|
+
(0, ui_1.info)(`${chalk_1.default.green('you>')} ${line}`);
|
|
122
|
+
await turn();
|
|
123
|
+
};
|
|
124
|
+
// A replayed history may already end on a user turn waiting for its reply.
|
|
125
|
+
if (history.length && history[history.length - 1].role === 'user')
|
|
126
|
+
await turn();
|
|
127
|
+
if (scripted) {
|
|
128
|
+
for (const line of opts.say || []) {
|
|
129
|
+
if (ended)
|
|
130
|
+
break;
|
|
131
|
+
await userSays(line);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
(0, ui_1.info)(chalk_1.default.dim('Text chat with the agent. /reset, /save <file>, /exit. Ends after end_call.'));
|
|
136
|
+
while (!ended) {
|
|
137
|
+
const { line } = await (0, prompts_1.default)({ type: 'text', name: 'line', message: 'you' });
|
|
138
|
+
if (line === undefined || line === '/exit')
|
|
139
|
+
break;
|
|
140
|
+
const cmd = String(line).trim();
|
|
141
|
+
if (!cmd)
|
|
142
|
+
continue;
|
|
143
|
+
if (cmd === '/reset') {
|
|
144
|
+
history = [];
|
|
145
|
+
(0, ui_1.info)(chalk_1.default.dim('History cleared.'));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (cmd.startsWith('/save ')) {
|
|
149
|
+
(0, fs_1.writeFileSync)(cmd.slice(6).trim(), JSON.stringify(history, null, 2));
|
|
150
|
+
(0, ui_1.info)(chalk_1.default.dim(`Saved ${history.length} items.`));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
await userSays(cmd);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (ended && !opts.json)
|
|
157
|
+
(0, ui_1.info)(chalk_1.default.dim('(agent ended the call)'));
|
|
158
|
+
if (opts.save)
|
|
159
|
+
(0, fs_1.writeFileSync)(opts.save, JSON.stringify(history, null, 2));
|
|
160
|
+
if (opts.json)
|
|
161
|
+
console.log(JSON.stringify(history, null, 2));
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
(0, ui_1.handleError)(err);
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/commands/calls.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postcallQuery = postcallQuery;
|
|
3
4
|
exports.buildCallQuery = buildCallQuery;
|
|
4
5
|
exports.callsListCommand = callsListCommand;
|
|
5
6
|
exports.callsGetCommand = callsGetCommand;
|
|
@@ -17,6 +18,24 @@ function fmtTime(v) {
|
|
|
17
18
|
const d = new Date(v);
|
|
18
19
|
return isNaN(d.getTime()) ? '' : d.toLocaleString('en-US');
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* `--postcall name=true|false` → `postcall_name=true|false`. The server only matches
|
|
23
|
+
* the literal strings `true`/`false` and silently drops any other value, which would
|
|
24
|
+
* return every call — so anything else is rejected here.
|
|
25
|
+
*/
|
|
26
|
+
function postcallQuery(pairs = []) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const pair of pairs) {
|
|
29
|
+
const m = /^([^=\s]+)=(true|false)$/.exec(pair.trim());
|
|
30
|
+
if (!m) {
|
|
31
|
+
throw new Error(`Invalid --postcall "${pair}". Use --postcall <variable>=true or <variable>=false ` +
|
|
32
|
+
`(boolean post-call variables only; the server ignores any other value). ` +
|
|
33
|
+
`Example: --postcall dnc_requested=true`);
|
|
34
|
+
}
|
|
35
|
+
out[`postcall_${m[1]}`] = m[2];
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
20
39
|
function buildCallQuery(o) {
|
|
21
40
|
return {
|
|
22
41
|
name: o.type,
|
|
@@ -31,6 +50,9 @@ function buildCallQuery(o) {
|
|
|
31
50
|
conversion_status: o.converted,
|
|
32
51
|
lead_id: o.lead,
|
|
33
52
|
buylead: o.buylead,
|
|
53
|
+
provider_type: o.providerType,
|
|
54
|
+
call_sid: o.sid,
|
|
55
|
+
...postcallQuery(o.postcall),
|
|
34
56
|
};
|
|
35
57
|
}
|
|
36
58
|
async function callsListCommand(opts) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.monitorListCommand = monitorListCommand;
|
|
4
|
+
exports.monitorUseCommand = monitorUseCommand;
|
|
5
|
+
exports.monitorCurrentCommand = monitorCurrentCommand;
|
|
6
|
+
exports.monitorClearCommand = monitorClearCommand;
|
|
7
|
+
const config_1 = require("../lib/config");
|
|
8
|
+
const session_1 = require("../lib/session");
|
|
9
|
+
const ui_1 = require("../lib/ui");
|
|
10
|
+
// Official v5 `accounts` group, renamed: `cku accounts` already means local saved logins.
|
|
11
|
+
async function monitorListCommand(opts) {
|
|
12
|
+
try {
|
|
13
|
+
const api = (0, session_1.requireApi)();
|
|
14
|
+
const { accounts } = await api.listClientAccounts();
|
|
15
|
+
if (opts.json)
|
|
16
|
+
return void console.log(JSON.stringify(accounts, null, 2));
|
|
17
|
+
if (!accounts?.length)
|
|
18
|
+
return void (0, ui_1.info)('No client accounts are assigned to you.');
|
|
19
|
+
const current = (0, config_1.monitoringAccountId)();
|
|
20
|
+
(0, ui_1.table)(['Active', 'Name', 'Email', 'Account ID'], accounts.map((a) => [current === a.id ? '*' : '', a.name || '', a.email || '', a.id]));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
(0, ui_1.handleError)(err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function monitorUseCommand(accountId) {
|
|
27
|
+
try {
|
|
28
|
+
const api = (0, session_1.requireApi)();
|
|
29
|
+
const { accounts } = await api.listClientAccounts();
|
|
30
|
+
const account = (accounts || []).find((a) => a.id === accountId);
|
|
31
|
+
if (!account) {
|
|
32
|
+
(0, ui_1.fail)('That client account is not assigned to you. Run `cku monitor list`.');
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!(0, config_1.setMonitoring)({ id: account.id, name: account.name, email: account.email })) {
|
|
37
|
+
(0, ui_1.fail)('No stored account to save this on (CK_TOKEN only?). Set CK_ACCOUNT_ID for this shell instead.');
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
(0, ui_1.success)(`Monitoring ${account.name || account.email || account.id}. Every command now acts on that account.`);
|
|
42
|
+
if (process.env.CK_ACCOUNT_ID)
|
|
43
|
+
(0, ui_1.info)(' CK_ACCOUNT_ID is set and overrides this; unset it to use the stored choice.');
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
(0, ui_1.handleError)(err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function monitorCurrentCommand(opts) {
|
|
50
|
+
const envId = process.env.CK_ACCOUNT_ID;
|
|
51
|
+
const stored = (0, config_1.loadConfig)().monitoring;
|
|
52
|
+
if (!envId && !stored)
|
|
53
|
+
return void (0, ui_1.info)('Monitoring mode is off.');
|
|
54
|
+
const current = envId ? { id: envId, source: 'CK_ACCOUNT_ID' } : { ...stored, source: 'config' };
|
|
55
|
+
if (opts.json)
|
|
56
|
+
return void console.log(JSON.stringify(current, null, 2));
|
|
57
|
+
(0, ui_1.keyValue)({
|
|
58
|
+
mode: 'MONITORING',
|
|
59
|
+
account: envId || stored?.name || stored?.email || stored?.id,
|
|
60
|
+
accountId: current.id,
|
|
61
|
+
source: current.source,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function monitorClearCommand() {
|
|
65
|
+
if (process.env.CK_ACCOUNT_ID) {
|
|
66
|
+
(0, ui_1.fail)('CK_ACCOUNT_ID is set for this process. Unset it to leave monitoring mode.');
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
(0, config_1.setMonitoring)(undefined);
|
|
71
|
+
(0, ui_1.success)('Monitoring mode is off.');
|
|
72
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scheduledListCommand = scheduledListCommand;
|
|
4
|
+
exports.scheduledQueuedCommand = scheduledQueuedCommand;
|
|
5
|
+
exports.scheduledGetCommand = scheduledGetCommand;
|
|
6
|
+
const dashboardApi_1 = require("../lib/dashboardApi");
|
|
7
|
+
const ui_1 = require("../lib/ui");
|
|
8
|
+
/**
|
|
9
|
+
* Scheduled calls, read-only. `/schedulecalls/v1/*` takes the dashboard cookie,
|
|
10
|
+
* so these need `cku dashboard login`; the `/cli` tree has no scheduled-call
|
|
11
|
+
* routes and the `/mcp` ones reject the ck_ token.
|
|
12
|
+
*
|
|
13
|
+
* No cancel: the backend's single-call delete removes the job and books a new
|
|
14
|
+
* attempt instead, and neither delete checks ownership of the job id.
|
|
15
|
+
*/
|
|
16
|
+
const when = (v) => {
|
|
17
|
+
if (v === undefined || v === null || v === '')
|
|
18
|
+
return '';
|
|
19
|
+
const d = new Date(v);
|
|
20
|
+
return isNaN(d.getTime()) ? String(v) : d.toLocaleString('en-US');
|
|
21
|
+
};
|
|
22
|
+
const agentName = (a) => (a && typeof a === 'object' ? a.name || a._id : a) || '';
|
|
23
|
+
async function scheduledListCommand(opts) {
|
|
24
|
+
try {
|
|
25
|
+
const page = (0, ui_1.intFlag)(opts.page, '--page', 1);
|
|
26
|
+
const pageSize = (0, ui_1.intFlag)(opts.limit, '--limit', 50);
|
|
27
|
+
const res = await (0, dashboardApi_1.dashboardApi)().scheduledCalls({ page, pageSize });
|
|
28
|
+
const rows = res?.scheduleCalls || [];
|
|
29
|
+
if (opts.json)
|
|
30
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
31
|
+
if (!rows.length)
|
|
32
|
+
return void (0, ui_1.info)('No scheduled calls.');
|
|
33
|
+
(0, ui_1.table)(['ID', 'TO', 'AGENT', 'SCHEDULED FOR', 'RETRIES', 'BATCH'], rows.map((r) => [r._id, r.to_number, agentName(r.agent_id), r.schedule_at, r.number_of_retries ?? '', r.batch_id || '']));
|
|
34
|
+
const p = res.pagination || {};
|
|
35
|
+
(0, ui_1.info)(`Page ${p.page ?? page} of ${p.totalPages ?? '?'} (${p.totalDocs ?? rows.length} total). Created today: ${res.todaysCalls ?? 0}.`);
|
|
36
|
+
if (p.hasNext)
|
|
37
|
+
(0, ui_1.info)(`Next: cku scheduled list --page ${(p.page ?? page) + 1}`);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
(0, ui_1.handleError)(err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function scheduledQueuedCommand(opts) {
|
|
44
|
+
try {
|
|
45
|
+
const res = await (0, dashboardApi_1.dashboardApi)().queuedScheduledCalls();
|
|
46
|
+
const jobs = res?.scheduleCalls || [];
|
|
47
|
+
if (opts.json)
|
|
48
|
+
return void console.log(JSON.stringify(jobs, null, 2));
|
|
49
|
+
if (!jobs.length)
|
|
50
|
+
return void (0, ui_1.info)('Nothing queued: no scheduled call is waiting to dial.');
|
|
51
|
+
// The queue returns jobs in no useful order; soonest first.
|
|
52
|
+
const due = (j) => (j.timestamp || 0) + (j.delay || 0);
|
|
53
|
+
jobs.sort((a, b) => due(a) - due(b));
|
|
54
|
+
(0, ui_1.table)(['JOB ID', 'TO', 'AGENT ID', 'DIALS AT', 'TRY', 'FOLLOW-UP'], jobs.map((j) => [
|
|
55
|
+
j.id, j.data?.to_number, j.data?.agent_id, when(due(j)),
|
|
56
|
+
j.data?.try_count ?? '', j.data?.follow_up_call ? 'yes' : '',
|
|
57
|
+
]));
|
|
58
|
+
(0, ui_1.info)(`${jobs.length} queued.`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
(0, ui_1.handleError)(err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function scheduledGetCommand(id, opts) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await (0, dashboardApi_1.dashboardApi)().scheduledCall(id);
|
|
67
|
+
if (opts.json)
|
|
68
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
69
|
+
const s = res.scheduleCall || {};
|
|
70
|
+
(0, ui_1.keyValue)({
|
|
71
|
+
ID: s._id,
|
|
72
|
+
To: s.to_number,
|
|
73
|
+
Agent: agentName(s.agent_id),
|
|
74
|
+
'Scheduled for': s.schedule_at,
|
|
75
|
+
Language: s.call_language,
|
|
76
|
+
Retries: s.number_of_retries,
|
|
77
|
+
'Retry gaps (min)': (s.gap_between_retries || []).join(', '),
|
|
78
|
+
Window: s.min_trigger_time && `${s.min_trigger_time}–${s.max_trigger_time}`,
|
|
79
|
+
Batch: s.batch_id || '',
|
|
80
|
+
Metadata: s.metadata ? JSON.stringify(s.metadata) : '',
|
|
81
|
+
'Still queued': (res.futureCalls || []).length ? 'yes' : 'no',
|
|
82
|
+
});
|
|
83
|
+
const done = res.completedCalls || [];
|
|
84
|
+
if (done.length) {
|
|
85
|
+
(0, ui_1.info)('');
|
|
86
|
+
(0, ui_1.table)(['CALL ID', 'TRY', 'HANGUP', 'DURATION', 'WHEN'], done.map((c) => [c._id, c.try_count ?? '', c.hangup_reason || '', c.call_duration ?? '', when(c.createdAt)]));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
(0, ui_1.handleError)(err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.toTestVariables = toTestVariables;
|
|
|
8
8
|
exports.toTestFunctions = toTestFunctions;
|
|
9
9
|
exports.simTestsListCommand = simTestsListCommand;
|
|
10
10
|
exports.simCreateCommand = simCreateCommand;
|
|
11
|
+
exports.simUpdateCommand = simUpdateCommand;
|
|
11
12
|
exports.simDeleteCommand = simDeleteCommand;
|
|
12
13
|
exports.waitForBatch = waitForBatch;
|
|
13
14
|
exports.simRunCommand = simRunCommand;
|
|
@@ -95,6 +96,37 @@ async function simCreateCommand(agentId, opts) {
|
|
|
95
96
|
(0, ui_1.handleError)(err);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Edit a test case. Sends only the fields given: the server passes the body to
|
|
101
|
+
* findByIdAndUpdate as-is, so anything else in it (userId, agentId) would be written.
|
|
102
|
+
* --variables / --functions replace the whole map / list, they do not merge.
|
|
103
|
+
*/
|
|
104
|
+
async function simUpdateCommand(testCaseId, opts) {
|
|
105
|
+
const api = (0, session_1.requireApi)();
|
|
106
|
+
try {
|
|
107
|
+
const body = {};
|
|
108
|
+
if (opts.name !== undefined)
|
|
109
|
+
body.testcaseName = opts.name;
|
|
110
|
+
if (opts.prompt !== undefined)
|
|
111
|
+
body.userPrompt = opts.prompt;
|
|
112
|
+
if (opts.criteria !== undefined)
|
|
113
|
+
body.successCriteria = opts.criteria;
|
|
114
|
+
if (opts.model !== undefined)
|
|
115
|
+
body.testLLMModel = opts.model;
|
|
116
|
+
if (opts.variables !== undefined)
|
|
117
|
+
body.testVariables = toTestVariables(await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables'));
|
|
118
|
+
if (opts.functions !== undefined)
|
|
119
|
+
body.testFunctions = toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions'));
|
|
120
|
+
if (!Object.keys(body).length) {
|
|
121
|
+
return (0, ui_1.handleError)(new Error('Nothing to update. Pass at least one of --name, --prompt, --criteria, --model, --variables, --functions.'));
|
|
122
|
+
}
|
|
123
|
+
const res = await api.updateTestCase(testCaseId, body);
|
|
124
|
+
(0, ui_1.success)(`Updated test case ${res.testCase?._id || testCaseId}: ${Object.keys(body).join(', ')}.`);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
(0, ui_1.handleError)(err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
98
130
|
async function simDeleteCommand(testCaseId, opts) {
|
|
99
131
|
const api = (0, session_1.requireApi)();
|
|
100
132
|
try {
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const commander_1 = require("commander");
|
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const ui_1 = require("./lib/ui");
|
|
10
10
|
const accounts_1 = require("./commands/accounts");
|
|
11
|
+
const monitor_1 = require("./commands/monitor");
|
|
11
12
|
const dashboardAuth_1 = require("./lib/dashboardAuth");
|
|
12
13
|
const kpi_1 = require("./commands/kpi");
|
|
13
14
|
/** Board columns, for `--help`. Ids are what the server keys on, not labels. */
|
|
@@ -15,6 +16,8 @@ const KPI_COLUMNS = 'action_items | in_progress | testing_ab | live_prod | done
|
|
|
15
16
|
const analyzers_1 = require("./commands/analyzers");
|
|
16
17
|
const models_1 = require("./commands/models");
|
|
17
18
|
const agents_1 = require("./commands/agents");
|
|
19
|
+
const agentChat_1 = require("./commands/agentChat");
|
|
20
|
+
const scheduled_1 = require("./commands/scheduled");
|
|
18
21
|
const agentVersions_1 = require("./commands/agentVersions");
|
|
19
22
|
const agentPhone_1 = require("./commands/agentPhone");
|
|
20
23
|
const whatsapp_1 = require("./commands/whatsapp");
|
|
@@ -39,6 +42,11 @@ const chatAgents_1 = require("./commands/chatAgents");
|
|
|
39
42
|
// last silently wins. `cku` cannot collide.
|
|
40
43
|
const name = 'cku';
|
|
41
44
|
const { version: VERSION } = require('../package.json');
|
|
45
|
+
/** Collect repeatable flag values (--var, --say, --postcall) into an array. */
|
|
46
|
+
const collectVar = (val, acc) => {
|
|
47
|
+
acc.push(val);
|
|
48
|
+
return acc;
|
|
49
|
+
};
|
|
42
50
|
const program = new commander_1.Command();
|
|
43
51
|
program
|
|
44
52
|
.name(name)
|
|
@@ -281,6 +289,28 @@ accounts
|
|
|
281
289
|
.description('forget a saved login')
|
|
282
290
|
.option('--yes', 'skip confirmation')
|
|
283
291
|
.action(accounts_1.accountsRemoveCommand);
|
|
292
|
+
// --- monitoring mode (official 5.0: client accounts assigned to you) -------
|
|
293
|
+
const monitor = program
|
|
294
|
+
.command('monitor')
|
|
295
|
+
.description('act on a client account assigned to you (official 5.0 `accounts`, renamed)');
|
|
296
|
+
monitor
|
|
297
|
+
.command('list', { isDefault: true })
|
|
298
|
+
.description('list client accounts assigned to you (* = monitored)')
|
|
299
|
+
.option('--json', 'output raw JSON')
|
|
300
|
+
.action(monitor_1.monitorListCommand);
|
|
301
|
+
monitor
|
|
302
|
+
.command('use <accountId>')
|
|
303
|
+
.description('enter monitoring mode: every command acts on that client account')
|
|
304
|
+
.action(monitor_1.monitorUseCommand);
|
|
305
|
+
monitor
|
|
306
|
+
.command('current')
|
|
307
|
+
.description('show the monitored account, if any')
|
|
308
|
+
.option('--json', 'output raw JSON')
|
|
309
|
+
.action(monitor_1.monitorCurrentCommand);
|
|
310
|
+
monitor
|
|
311
|
+
.command('clear')
|
|
312
|
+
.description('leave monitoring mode')
|
|
313
|
+
.action(monitor_1.monitorClearCommand);
|
|
284
314
|
program
|
|
285
315
|
.command('update')
|
|
286
316
|
.description('upgrade the CLI to the latest release, then refresh the skill pack')
|
|
@@ -307,6 +337,27 @@ agents.command('get <agentId>').description('show one agent and list its version
|
|
|
307
337
|
.option('--json', 'output raw JSON')
|
|
308
338
|
.option('--file <path>', 'save the agent detail as JSON to a file')
|
|
309
339
|
.action(agents_1.agentsGetCommand);
|
|
340
|
+
agents.command('chat <agentId>')
|
|
341
|
+
.description('text-chat with an agent, no phone call (custom in-call functions RUN FOR REAL)')
|
|
342
|
+
.option('--versions <id>', 'version to talk to (default: the published one)')
|
|
343
|
+
.option('--metadata <json|@file>', 'call metadata as a JSON object')
|
|
344
|
+
.option('--var <k=v>', 'call metadata variable (repeatable)', collectVar, [])
|
|
345
|
+
.option('--say <text>', 'user turn, repeatable, run in order; no terminal needed', collectVar, [])
|
|
346
|
+
.option('--history <json|@file>', 'replay a saved conversation (the output of --json) before --say')
|
|
347
|
+
.option('--save <file>', 'write the conversation to a file at the end (readable by --history)')
|
|
348
|
+
.option('--show-prompt', 'print the rendered system prompt once (checks metadata substitution)')
|
|
349
|
+
.option('--json', 'print only the final conversation as JSON')
|
|
350
|
+
.addHelpText('after', `
|
|
351
|
+
Not mocked: any custom in-call function. It really runs (CRM write, webhook, WhatsApp).
|
|
352
|
+
Mocked: end_call, transfer_call, warm_transfer, keep_call_on_hold, use_knowledge_base,
|
|
353
|
+
change_conversation_language, send_booking_link_to_whatsapp, collect_digits_via_dtmf.
|
|
354
|
+
|
|
355
|
+
Examples:
|
|
356
|
+
cku agents chat <agentId>
|
|
357
|
+
cku agents chat <agentId> --var customer_name=Ravi --say "Hello?" --say "Not interested"
|
|
358
|
+
cku agents chat <agentId> --say "hi" --json > convo.json
|
|
359
|
+
cku agents chat <agentId> --history @convo.json --say "What was that again?"`)
|
|
360
|
+
.action(agentChat_1.agentChatCommand);
|
|
310
361
|
agents.command('versions <agentId>').description("list an agent's versions (name, id, language, prompt type, active)")
|
|
311
362
|
.option('--json', 'output raw JSON')
|
|
312
363
|
.option('--file <path>', 'save the version list as JSON to a file')
|
|
@@ -471,13 +522,11 @@ function addCallFilters(cmd) {
|
|
|
471
522
|
.option('--to <number>', 'destination number')
|
|
472
523
|
.option('--converted <bool>', 'conversion status: true|false')
|
|
473
524
|
.option('--lead <id>', 'lead id')
|
|
474
|
-
.option('--buylead <value>', 'buylead value')
|
|
525
|
+
.option('--buylead <value>', 'buylead value')
|
|
526
|
+
.option('--postcall <name=bool>', 'boolean post-call variable, repeatable: dnc_requested=true', collectVar, [])
|
|
527
|
+
.option('--provider-type <list>', 'telephony provider(s), comma-separated: plivo,vobiz,...')
|
|
528
|
+
.option('--sid <callSid>', 'provider call SID (ignores the date window)');
|
|
475
529
|
}
|
|
476
|
-
/** Collect repeatable --var key=value into an array. */
|
|
477
|
-
const collectVar = (val, acc) => {
|
|
478
|
-
acc.push(val);
|
|
479
|
-
return acc;
|
|
480
|
-
};
|
|
481
530
|
// --- voices / transcribers -------------------------------------------------
|
|
482
531
|
program
|
|
483
532
|
.command('voices')
|
|
@@ -520,6 +569,20 @@ program
|
|
|
520
569
|
.option('--all', 'include admin-gated providers')
|
|
521
570
|
.option('--json', 'raw JSON')
|
|
522
571
|
.action(transcribers_1.transcribersCommand);
|
|
572
|
+
// --- scheduled calls (dashboard cookie) -------------------------------------
|
|
573
|
+
const scheduled = program.command('scheduled')
|
|
574
|
+
.description('scheduled calls, read-only (needs `cku dashboard login`)');
|
|
575
|
+
scheduled.command('list').description('schedule records, newest first')
|
|
576
|
+
.option('--page <n>', 'page number (default 1)')
|
|
577
|
+
.option('--limit <n>', 'page size (default 50)')
|
|
578
|
+
.option('--json', 'raw JSON (a bare array)')
|
|
579
|
+
.action(scheduled_1.scheduledListCommand);
|
|
580
|
+
scheduled.command('queued').description('jobs waiting in the queue: what will actually dial, soonest first')
|
|
581
|
+
.option('--json', 'raw JSON (a bare array of queue jobs)')
|
|
582
|
+
.action(scheduled_1.scheduledQueuedCommand);
|
|
583
|
+
scheduled.command('get <id>').description('one schedule record, its completed tries, and whether it is still queued')
|
|
584
|
+
.option('--json', 'raw JSON')
|
|
585
|
+
.action(scheduled_1.scheduledGetCommand);
|
|
523
586
|
// --- calls -----------------------------------------------------------------
|
|
524
587
|
const calls = program.command('calls').description('calls: place & history');
|
|
525
588
|
calls.command('make').description('place a single outbound call (real, or --test)')
|
|
@@ -633,6 +696,14 @@ sim.command('create <agentId>').description('create a test case')
|
|
|
633
696
|
.option('--variables <json|@file>', 'testVariables: a JSON object of name -> value (non-strings are stringified)')
|
|
634
697
|
.option('--functions <json|@file>', 'function mocks: a JSON array of {"name": ..., "response": ...}')
|
|
635
698
|
.action(simulations_1.simCreateCommand);
|
|
699
|
+
sim.command('update <testCaseId>').description('edit a test case (only the fields you pass are sent)')
|
|
700
|
+
.option('--name <name>', 'test case name')
|
|
701
|
+
.option('--prompt <text>', 'simulated user prompt')
|
|
702
|
+
.option('--criteria <text>', 'success criteria')
|
|
703
|
+
.option('--model <model>', 'test LLM model')
|
|
704
|
+
.option('--variables <json|@file>', 'testVariables: replaces the whole map')
|
|
705
|
+
.option('--functions <json|@file>', 'function mocks: replaces the whole list')
|
|
706
|
+
.action(simulations_1.simUpdateCommand);
|
|
636
707
|
sim.command('delete <testCaseId>').description('delete a test case').option('--yes', 'skip confirmation')
|
|
637
708
|
.action(simulations_1.simDeleteCommand);
|
|
638
709
|
sim.command('run <agentId>').description('run a batch simulation (async)')
|
package/dist/lib/api.js
CHANGED
|
@@ -13,21 +13,26 @@ const types_1 = require("./types");
|
|
|
13
13
|
*/
|
|
14
14
|
class Api {
|
|
15
15
|
token;
|
|
16
|
+
monitoringAccountId;
|
|
16
17
|
baseUrl;
|
|
17
18
|
callBaseUrl;
|
|
18
|
-
constructor(baseUrl, callBaseUrl, token) {
|
|
19
|
+
constructor(baseUrl, callBaseUrl, token, monitoringAccountId) {
|
|
19
20
|
this.token = token;
|
|
21
|
+
this.monitoringAccountId = monitoringAccountId;
|
|
20
22
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
21
23
|
this.callBaseUrl = (callBaseUrl || baseUrl).replace(/\/+$/, '');
|
|
22
24
|
}
|
|
23
|
-
request(method, path, body) {
|
|
24
|
-
return this.requestTo(this.baseUrl, method, path, body);
|
|
25
|
+
request(method, path, body, includeAccount = true) {
|
|
26
|
+
return this.requestTo(this.baseUrl, method, path, body, includeAccount);
|
|
25
27
|
}
|
|
26
28
|
/** Same as request(), but against a specific base URL (e.g. the call cloud). */
|
|
27
|
-
async requestTo(base, method, path, body) {
|
|
29
|
+
async requestTo(base, method, path, body, includeAccount = true) {
|
|
28
30
|
const headers = { 'Content-Type': 'application/json' };
|
|
29
31
|
if (this.token)
|
|
30
32
|
headers.Authorization = `Bearer ${this.token}`;
|
|
33
|
+
// Monitoring mode: every request, writes included, acts on the client account.
|
|
34
|
+
if (includeAccount && this.monitoringAccountId)
|
|
35
|
+
headers['X-CallKaro-Account-Id'] = this.monitoringAccountId;
|
|
31
36
|
let res;
|
|
32
37
|
try {
|
|
33
38
|
res = await fetch(`${base}${path}`, {
|
|
@@ -59,6 +64,8 @@ class Api {
|
|
|
59
64
|
return this.request('GET', `/cli/auth/browser/poll?code=${encodeURIComponent(code)}&secret=${encodeURIComponent(secret)}`);
|
|
60
65
|
}
|
|
61
66
|
me() { return this.request('GET', '/cli/auth/me'); }
|
|
67
|
+
/** Client accounts assigned to this user. Sent without the monitoring header: it lists, not acts. */
|
|
68
|
+
listClientAccounts() { return this.request('GET', '/cli/auth/accounts', undefined, false); }
|
|
62
69
|
// --- secrets --------------------------------------------------------------
|
|
63
70
|
listSecrets() { return this.request('GET', '/cli/secrets'); }
|
|
64
71
|
saveSecrets(secrets) { return this.request('PUT', '/cli/secrets', { secrets }); }
|
|
@@ -243,6 +250,11 @@ class Api {
|
|
|
243
250
|
listTestCases(agentId) {
|
|
244
251
|
return this.request('GET', `/cli/simulations/test-cases${qs({ agentId })}`);
|
|
245
252
|
}
|
|
253
|
+
/** One agent turn from a chat history, no telephony (`agents chat`). */
|
|
254
|
+
generateReply(body, versionId) {
|
|
255
|
+
const q = versionId ? `?version=${encodeURIComponent(versionId)}` : '';
|
|
256
|
+
return this.request('POST', `/cli/simulations/generate-reply${q}`, body);
|
|
257
|
+
}
|
|
246
258
|
updateTestCase(id, body) { return this.request('PUT', `/cli/simulations/test-cases/${id}`, body); }
|
|
247
259
|
deleteTestCase(id) { return this.request('DELETE', `/cli/simulations/test-cases/${id}`); }
|
|
248
260
|
runSimulation(body) { return this.request('POST', '/cli/simulations/runs', body); }
|
package/dist/lib/config.js
CHANGED
|
@@ -27,6 +27,8 @@ exports.removeAccount = removeAccount;
|
|
|
27
27
|
exports.loadConfig = loadConfig;
|
|
28
28
|
exports.clearSession = clearSession;
|
|
29
29
|
exports.saveLogin = saveLogin;
|
|
30
|
+
exports.monitoringAccountId = monitoringAccountId;
|
|
31
|
+
exports.setMonitoring = setMonitoring;
|
|
30
32
|
exports.getClient = getClient;
|
|
31
33
|
const fs_1 = require("fs");
|
|
32
34
|
const os_1 = require("os");
|
|
@@ -347,8 +349,21 @@ function saveLogin(email, session) {
|
|
|
347
349
|
? saved + ` This shell still uses ${current} via CK_ACCOUNT. Unset it, or set CK_ACCOUNT=${email}, to switch.`
|
|
348
350
|
: saved + ` This folder still uses ${current} via .cku.json. Run \`cku accounts bind ${email}\` to switch.`;
|
|
349
351
|
}
|
|
352
|
+
/** The client account in effect, if any: CK_ACCOUNT_ID wins over the stored choice. */
|
|
353
|
+
function monitoringAccountId(cfg = loadConfig()) {
|
|
354
|
+
return process.env.CK_ACCOUNT_ID || cfg.monitoring?.id || undefined;
|
|
355
|
+
}
|
|
356
|
+
/** Set or clear (undefined) the monitored account on the effective stored account. */
|
|
357
|
+
function setMonitoring(monitoring) {
|
|
358
|
+
const slug = activeAccount();
|
|
359
|
+
if (!slug || !(0, fs_1.existsSync)(accountPath(slug)))
|
|
360
|
+
return false;
|
|
361
|
+
const { monitoring: _old, ...rest } = loadAccount(slug);
|
|
362
|
+
writeJson(accountPath(slug), monitoring ? { ...rest, monitoring } : rest);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
350
365
|
/** Build the API client from the endpoint URLs + resolved token. */
|
|
351
366
|
function getClient() {
|
|
352
367
|
const cfg = loadConfig();
|
|
353
|
-
return { api: new api_1.Api(backendUrl(), callUrl(), cfg.token), cfg };
|
|
368
|
+
return { api: new api_1.Api(backendUrl(), callUrl(), cfg.token, monitoringAccountId(cfg)), cfg };
|
|
354
369
|
}
|
package/dist/lib/dashboardApi.js
CHANGED
|
@@ -144,6 +144,22 @@ class DashboardApi {
|
|
|
144
144
|
deleteTestVariableSet(id) {
|
|
145
145
|
return this.request('DELETE', `/v1/call-metadata-test-variables/${encodeURIComponent(id)}`);
|
|
146
146
|
}
|
|
147
|
+
// --- scheduled calls (read-only) -------------------------------------------
|
|
148
|
+
// The delete routes under /schedulecalls/v1 are deliberately not wrapped: the
|
|
149
|
+
// single-call one re-schedules a new attempt rather than cancelling, and neither
|
|
150
|
+
// checks that the job belongs to the caller. See docs/plan-unused-routes.md.
|
|
151
|
+
/** Schedule records, newest first. 44k rows on a busy account: always paged. */
|
|
152
|
+
scheduledCalls(query = {}) {
|
|
153
|
+
return this.request('GET', `/schedulecalls/v1/all${qs(query)}`);
|
|
154
|
+
}
|
|
155
|
+
/** Pending jobs in the queue: what will actually dial, with `data.schedule_at`. */
|
|
156
|
+
queuedScheduledCalls() {
|
|
157
|
+
return this.request('GET', '/schedulecalls/v1/queued-scheduled-calls/all');
|
|
158
|
+
}
|
|
159
|
+
/** One schedule record plus the calls it produced and any pending job. */
|
|
160
|
+
scheduledCall(id) {
|
|
161
|
+
return this.request('GET', `/schedulecalls/v1/${encodeURIComponent(id)}`);
|
|
162
|
+
}
|
|
147
163
|
// --- KPI board writes -----------------------------------------------------
|
|
148
164
|
//
|
|
149
165
|
// Routes, bodies and the column set below were read out of the dashboard's own
|