callkaro-cli-unofficial 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/commands/accounts.js +16 -2
- package/dist/commands/admin.js +519 -0
- package/dist/commands/dashboardApiCmd.js +5 -1
- package/dist/index.js +146 -0
- package/dist/lib/adminApi.js +323 -0
- package/dist/lib/adminAuth.js +395 -0
- package/dist/lib/config.js +87 -8
- package/dist/lib/dashboardApi.js +5 -1
- package/dist/lib/dashboardAuth.js +18 -1
- package/dist/lib/looseJson.js +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,6 +166,20 @@ cku dashboard probe # which dashboard routes this token reaches
|
|
|
166
166
|
|
|
167
167
|
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.
|
|
168
168
|
|
|
169
|
+
### The admin console credential
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
cku admin login # a third credential: the internal admin console
|
|
173
|
+
cku admin clients # the client accounts you are assigned to
|
|
174
|
+
cku admin impersonate <accountId> # mint a real ck_ token for one of them
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The internal admin console (`intadmin.callkaro.ai`) is a *third* credential, separate from both the `ck_` token and the dashboard session — each rejects the other two with an opaque 401. It is the only place the console's write routes live, and the only way to act on a client account when no login of yours is assigned to it.
|
|
178
|
+
|
|
179
|
+
`cku admin impersonate` is the console's "Login as this user" in the terminal: it mints a real `ck_` token through the admin session, stores it under the client's own email, and scopes it to that client account — so `cku accounts use` picks it up and it renews itself by impersonating again. The client-facing `/cli/*` surface has no equivalent, and hand-rolling the cookie chain is four hops of undocumented routes.
|
|
180
|
+
|
|
181
|
+
The console's writes — recharge, concurrency, pause/resume, phone numbers — are all confirmed, and need `--yes` without a terminal. `cku admin recharge` moves real credits and the console has no undo, so it checks the account's own minimum first. The full route map, role gates and request bodies are in [`docs/admin-api.md`](docs/admin-api.md).
|
|
182
|
+
|
|
169
183
|
### Knowing which models are actually valid
|
|
170
184
|
|
|
171
185
|
```bash
|
|
@@ -175,7 +175,7 @@ async function accountsCommand(opts) {
|
|
|
175
175
|
// Never print tokens or passwords: this output ends up in agent transcripts
|
|
176
176
|
// and CI logs. `signedIn` says whether a credential is held, not what it is.
|
|
177
177
|
if (opts.json) {
|
|
178
|
-
return void console.log(JSON.stringify(accounts.map(({ token, password, dashboard, ...a }) => ({
|
|
178
|
+
return void console.log(JSON.stringify(accounts.map(({ token, password, dashboard, admin, ...a }) => ({
|
|
179
179
|
...a,
|
|
180
180
|
signedIn: Boolean(token),
|
|
181
181
|
...(dashboard
|
|
@@ -189,22 +189,36 @@ async function accountsCommand(opts) {
|
|
|
189
189
|
},
|
|
190
190
|
}
|
|
191
191
|
: {}),
|
|
192
|
+
...(admin
|
|
193
|
+
? {
|
|
194
|
+
admin: {
|
|
195
|
+
email: admin.email,
|
|
196
|
+
userId: admin.userId,
|
|
197
|
+
role: admin.role,
|
|
198
|
+
expiresAt: admin.expiresAt,
|
|
199
|
+
savedAt: admin.savedAt,
|
|
200
|
+
signedIn: Boolean(admin.token),
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
192
204
|
})), null, 2));
|
|
193
205
|
}
|
|
194
206
|
if (!accounts.length) {
|
|
195
207
|
return void (0, ui_1.info)('No accounts saved. Run `cku login` to add one.');
|
|
196
208
|
}
|
|
197
|
-
(0, ui_1.table)(['', 'EMAIL', 'CK TOKEN', 'DASHBOARD', 'SAVED'], accounts.map((a) => [
|
|
209
|
+
(0, ui_1.table)(['', 'EMAIL', 'CK TOKEN', 'DASHBOARD', 'ADMIN', 'SAVED'], accounts.map((a) => [
|
|
198
210
|
active && (0, config_1.slugFor)(a.email || '') === active ? '*' : '',
|
|
199
211
|
a.email || '(unknown)',
|
|
200
212
|
a.token ? 'yes' : 'no',
|
|
201
213
|
a.dashboard?.token ? (0, dashboardAuth_1.expiryLabel)(a.dashboard.token) : '—',
|
|
214
|
+
a.admin?.token ? (0, dashboardAuth_1.expiryLabel)(a.admin.token) : '—',
|
|
202
215
|
a.savedAt ? new Date(a.savedAt).toLocaleString('en-US') : '',
|
|
203
216
|
]));
|
|
204
217
|
const source = (0, config_1.accountSource)();
|
|
205
218
|
(0, ui_1.info)(`\n* = active account (${source === 'env' ? 'CK_ACCOUNT' : source === 'project' ? 'this folder\'s .cku.json' : 'global default'}).`);
|
|
206
219
|
(0, ui_1.info)('CK TOKEN gates /cli/* commands. DASHBOARD gates /auth/* and /v2/*, and is a separate');
|
|
207
220
|
(0, ui_1.info)('login that expires in 24h — add one with `cku dashboard login`.');
|
|
221
|
+
(0, ui_1.info)('ADMIN gates /monitor/*, the internal admin console — add one with `cku admin login`.');
|
|
208
222
|
if (source === 'global') {
|
|
209
223
|
(0, ui_1.info)(' Tip: `cku accounts bind <email>` pins an account to this folder, so parallel');
|
|
210
224
|
(0, ui_1.info)(' agents in different folders cannot switch each other out from under themselves.');
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.activeAccount = exports.getClient = void 0;
|
|
4
|
+
exports.adminClientsCommand = adminClientsCommand;
|
|
5
|
+
exports.adminClientCommand = adminClientCommand;
|
|
6
|
+
exports.adminCoworkersCommand = adminCoworkersCommand;
|
|
7
|
+
exports.adminImpersonateCommand = adminImpersonateCommand;
|
|
8
|
+
exports.adminAnalyticsCommand = adminAnalyticsCommand;
|
|
9
|
+
exports.adminCostsCommand = adminCostsCommand;
|
|
10
|
+
exports.adminRechargesCommand = adminRechargesCommand;
|
|
11
|
+
exports.adminCallsCommand = adminCallsCommand;
|
|
12
|
+
exports.adminBotsCommand = adminBotsCommand;
|
|
13
|
+
exports.adminSentryCommand = adminSentryCommand;
|
|
14
|
+
exports.adminRechargeCommand = adminRechargeCommand;
|
|
15
|
+
exports.adminClientSetCommand = adminClientSetCommand;
|
|
16
|
+
exports.adminClientActionCommand = adminClientActionCommand;
|
|
17
|
+
exports.adminAgentActionCommand = adminAgentActionCommand;
|
|
18
|
+
exports.adminNumbersCommand = adminNumbersCommand;
|
|
19
|
+
exports.adminVoiceCacheCommand = adminVoiceCacheCommand;
|
|
20
|
+
exports.adminApiCommand = adminApiCommand;
|
|
21
|
+
const adminApi_1 = require("../lib/adminApi");
|
|
22
|
+
const adminAuth_1 = require("../lib/adminAuth");
|
|
23
|
+
const config_1 = require("../lib/config");
|
|
24
|
+
Object.defineProperty(exports, "getClient", { enumerable: true, get: function () { return config_1.getClient; } });
|
|
25
|
+
Object.defineProperty(exports, "activeAccount", { enumerable: true, get: function () { return config_1.activeAccount; } });
|
|
26
|
+
const ui_1 = require("../lib/ui");
|
|
27
|
+
const jsonInput_1 = require("./jsonInput");
|
|
28
|
+
const looseJson_1 = require("../lib/looseJson");
|
|
29
|
+
/** The route segments the path may contain — see `repairPosixPath`. */
|
|
30
|
+
const PATH_SEGMENTS = ['monitor'];
|
|
31
|
+
/**
|
|
32
|
+
* `cku admin` — the internal admin console (intadmin.callkaro.ai).
|
|
33
|
+
*
|
|
34
|
+
* A third surface, on a third credential: the `/monitor/*` routes answer to the
|
|
35
|
+
* `access_token_monitor` cookie and to nothing else. It is the only place the
|
|
36
|
+
* console's write routes exist — recharge, concurrency, pause/resume, phone
|
|
37
|
+
* numbers — and the only way to act on a client account when no login of yours
|
|
38
|
+
* is assigned to it.
|
|
39
|
+
*/
|
|
40
|
+
/** The window analytics and costs default to when the user gives none. */
|
|
41
|
+
function defaultWindow() {
|
|
42
|
+
const end = new Date();
|
|
43
|
+
const start = new Date(end);
|
|
44
|
+
start.setMonth(start.getMonth() - 1);
|
|
45
|
+
const iso = (d) => d.toISOString().slice(0, 10);
|
|
46
|
+
return { startDate: iso(start), endDate: iso(end) };
|
|
47
|
+
}
|
|
48
|
+
/** Reject a date that is not YYYY-MM-DD, naming the format. */
|
|
49
|
+
function dateArg(raw, flag, fallback) {
|
|
50
|
+
if (!raw)
|
|
51
|
+
return fallback;
|
|
52
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
53
|
+
throw new Error(`${flag} must be YYYY-MM-DD (got "${raw}").`);
|
|
54
|
+
}
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
/** The admin's own id — the route that lists the clients is keyed by it. */
|
|
58
|
+
async function adminId() {
|
|
59
|
+
const res = await (0, adminApi_1.adminApi)().getAdmin();
|
|
60
|
+
const id = res?.user?._id || res?._id;
|
|
61
|
+
if (!id)
|
|
62
|
+
throw new Error('The admin console did not return an id for this user.');
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One client account's record.
|
|
67
|
+
*
|
|
68
|
+
* It comes out of the admin's *own* user record, not out of
|
|
69
|
+
* `GET /monitor/users/:clientId` — that route 403s, because a client id is not
|
|
70
|
+
* an admin you are allowed to see. The console's own clients page reads the
|
|
71
|
+
* same way.
|
|
72
|
+
*/
|
|
73
|
+
async function lookupClient(clientId) {
|
|
74
|
+
const id = await adminId();
|
|
75
|
+
const res = await (0, adminApi_1.adminApi)().myClients(id);
|
|
76
|
+
const client = (res?.users || []).find((c) => (c._id || c.id) === clientId);
|
|
77
|
+
if (!client) {
|
|
78
|
+
throw new Error(`No client account with id ${clientId} is assigned to this admin user.\n` +
|
|
79
|
+
' Run `cku admin clients` to see the ones you have.');
|
|
80
|
+
}
|
|
81
|
+
return client;
|
|
82
|
+
}
|
|
83
|
+
// --- identity --------------------------------------------------------------
|
|
84
|
+
async function adminClientsCommand(opts) {
|
|
85
|
+
try {
|
|
86
|
+
const id = await adminId();
|
|
87
|
+
const res = await (0, adminApi_1.adminApi)().myClients(id);
|
|
88
|
+
const clients = res?.users || [];
|
|
89
|
+
if (opts.json)
|
|
90
|
+
return void console.log(JSON.stringify(clients, null, 2));
|
|
91
|
+
if (!clients.length)
|
|
92
|
+
return void (0, ui_1.info)('No client accounts are assigned to this admin user.');
|
|
93
|
+
(0, ui_1.table)(['NAME', 'EMAIL', 'ACCOUNT ID', 'PLAN', 'CREDITS', 'CONCURRENCY', 'PAUSED'], clients.map((c) => [
|
|
94
|
+
c.name || '',
|
|
95
|
+
c.email || '',
|
|
96
|
+
c._id || c.id || '',
|
|
97
|
+
c.plan_type || '—',
|
|
98
|
+
c.currentCredits !== undefined ? Math.round(c.currentCredits) : '—',
|
|
99
|
+
c.concurrency ?? '—',
|
|
100
|
+
c.isPaused ? 'yes' : 'no',
|
|
101
|
+
]));
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
(0, ui_1.handleError)(err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function adminClientCommand(clientId, opts) {
|
|
108
|
+
try {
|
|
109
|
+
const c = await lookupClient(clientId);
|
|
110
|
+
if (opts.json)
|
|
111
|
+
return void console.log(JSON.stringify(c, null, 2));
|
|
112
|
+
(0, ui_1.keyValue)({
|
|
113
|
+
name: c?.name,
|
|
114
|
+
email: c?.email,
|
|
115
|
+
id: c?._id || c?.id,
|
|
116
|
+
plan: c?.plan_type,
|
|
117
|
+
credits: c?.currentCredits !== undefined ? Math.round(c.currentCredits) : undefined,
|
|
118
|
+
minRechargeAmount: c?.minRechargeAmount,
|
|
119
|
+
concurrency: c?.concurrency,
|
|
120
|
+
phoneNumbers: (c?.phoneNumbers || []).join(', ') || undefined,
|
|
121
|
+
queueSize: c?.queueSize,
|
|
122
|
+
paused: c?.isPaused,
|
|
123
|
+
triggerWindow: c?.min_trigger_time && c?.max_trigger_time ? `${c.min_trigger_time}–${c.max_trigger_time}` : undefined,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
(0, ui_1.handleError)(err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function adminCoworkersCommand(clientId, opts) {
|
|
131
|
+
try {
|
|
132
|
+
const res = await (0, adminApi_1.adminApi)().coworkers(clientId);
|
|
133
|
+
const people = res?.coworkers || [];
|
|
134
|
+
if (opts.json)
|
|
135
|
+
return void console.log(JSON.stringify(people, null, 2));
|
|
136
|
+
if (!people.length)
|
|
137
|
+
return void (0, ui_1.info)('No coworkers on that client account.');
|
|
138
|
+
(0, ui_1.table)(['NAME', 'EMAIL', 'EMPLOYEE ID', 'ROLE', 'PHONE'], people.map((p) => [p.name || '', p.email || '', p.employee_id || '', p.role || '', p.phone_number || '']));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
(0, ui_1.handleError)(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// --- impersonation ---------------------------------------------------------
|
|
145
|
+
/**
|
|
146
|
+
* `cku admin impersonate <clientId>` — mint a real `ck_` token for a client
|
|
147
|
+
* account and store it, scoped to that client.
|
|
148
|
+
*
|
|
149
|
+
* This is the "Login as this user" button, in the terminal. The token is stored
|
|
150
|
+
* under the *client's* own email so `cku accounts use` and `cku accounts bind`
|
|
151
|
+
* find it, and the global default is left alone — minting a client token must
|
|
152
|
+
* not silently redirect the admin's own commands.
|
|
153
|
+
*/
|
|
154
|
+
async function adminImpersonateCommand(clientId, opts) {
|
|
155
|
+
try {
|
|
156
|
+
// Find the client so the token is filed under its own email.
|
|
157
|
+
const client = await lookupClient(clientId);
|
|
158
|
+
const clientEmail = client.email;
|
|
159
|
+
const slug = (0, config_1.slugFor)(clientEmail);
|
|
160
|
+
const existing = (0, config_1.loadAccount)(slug);
|
|
161
|
+
if (existing.token && !(await (0, ui_1.confirm)(`${clientEmail} already has a stored login. Replace it with the impersonated token?`, opts.yes))) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const { token, email: adminEmail, dashboard } = await (0, adminAuth_1.impersonateClient)(clientId, { adminEmail: (await (0, adminAuth_1.requireAdmin)()).email });
|
|
165
|
+
// activate: false — the admin's own account stays the default.
|
|
166
|
+
(0, config_1.saveAccount)(clientEmail, {
|
|
167
|
+
token,
|
|
168
|
+
user: { email: clientEmail },
|
|
169
|
+
monitoring: { id: clientId, name: client.name, email: clientEmail },
|
|
170
|
+
impersonated: { adminEmail, clientId },
|
|
171
|
+
}, { activate: false });
|
|
172
|
+
(0, config_1.saveDashboardSession)(clientEmail, { email: clientEmail, ...dashboard, savedAt: new Date().toISOString() });
|
|
173
|
+
(0, ui_1.success)(`Impersonating ${client.name || clientEmail} (${clientEmail}).`);
|
|
174
|
+
(0, ui_1.info)(' minted a ck_ token through the admin console and stored it, scoped to that client account.');
|
|
175
|
+
(0, ui_1.info)(` Use it: cku accounts use ${clientEmail}`);
|
|
176
|
+
(0, ui_1.info)(` Or pin a folder: cku accounts bind ${clientEmail}`);
|
|
177
|
+
(0, ui_1.info)(' Also stored a dashboard session for it, so `cku dashboard api` reaches /v2/agents/.');
|
|
178
|
+
(0, ui_1.info)(' Both renew themselves by impersonating again.');
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
(0, ui_1.handleError)(err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// --- analytics and costs ---------------------------------------------------
|
|
185
|
+
async function adminAnalyticsCommand(opts) {
|
|
186
|
+
try {
|
|
187
|
+
const win = defaultWindow();
|
|
188
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
189
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
190
|
+
// The server's filter is `user_ids` (a csv). `userIds` is accepted and
|
|
191
|
+
// answered with "At least one user must be selected", which reads like a
|
|
192
|
+
// permissions problem rather than a wrong parameter name.
|
|
193
|
+
const user_ids = opts.client;
|
|
194
|
+
const res = await (0, adminApi_1.adminApi)().analytics({ startDate, endDate, user_ids });
|
|
195
|
+
const rows = res?.data?.user_wise || [];
|
|
196
|
+
if (opts.json)
|
|
197
|
+
return void console.log(JSON.stringify(res?.data ?? res, null, 2));
|
|
198
|
+
if (!rows.length)
|
|
199
|
+
return void (0, ui_1.info)('No analytics for that window.');
|
|
200
|
+
(0, ui_1.table)(['CLIENT ID', 'CALLS', 'MINUTES', 'CONNECTED', 'CONNECTED MIN', 'PULSE MIN', 'COST'], rows.map((r) => [
|
|
201
|
+
r.userId,
|
|
202
|
+
r.total_calls ?? '',
|
|
203
|
+
Math.round(r.minutes ?? 0),
|
|
204
|
+
r.connected_calls ?? '',
|
|
205
|
+
Math.round(r.connected_minutes ?? 0),
|
|
206
|
+
Math.round(r.pulse_minutes ?? 0),
|
|
207
|
+
r.total_cost !== undefined ? Math.round(r.total_cost) : '',
|
|
208
|
+
]));
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
(0, ui_1.handleError)(err);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function adminCostsCommand(opts) {
|
|
215
|
+
try {
|
|
216
|
+
const win = defaultWindow();
|
|
217
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
218
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
219
|
+
const user_ids = opts.client;
|
|
220
|
+
const res = await (0, adminApi_1.adminApi)().costs({ startDate, endDate, user_ids });
|
|
221
|
+
const data = res?.data || res;
|
|
222
|
+
if (opts.json)
|
|
223
|
+
return void console.log(JSON.stringify(data, null, 2));
|
|
224
|
+
(0, ui_1.keyValue)({ totalCost: data?.totalCost, days: (data?.daily || []).length });
|
|
225
|
+
const daily = (data?.daily || []).slice(-30);
|
|
226
|
+
if (!daily.length)
|
|
227
|
+
return;
|
|
228
|
+
(0, ui_1.table)(['DATE', 'TOTAL'], daily.map((d) => [d.date, Math.round(d.total ?? 0)]));
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
(0, ui_1.handleError)(err);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function adminRechargesCommand(clientId, opts) {
|
|
235
|
+
try {
|
|
236
|
+
const win = defaultWindow();
|
|
237
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
238
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
239
|
+
const res = await (0, adminApi_1.adminApi)().recharges(clientId, { startDate, endDate });
|
|
240
|
+
const rows = res?.recharges || [];
|
|
241
|
+
if (opts.json)
|
|
242
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
243
|
+
if (!rows.length)
|
|
244
|
+
return void (0, ui_1.info)('No recharges in that window.');
|
|
245
|
+
(0, ui_1.table)(['DATE', 'AMOUNT', 'CREDITS', 'STATUS', 'METHOD', 'INVOICE'], rows.map((r) => [
|
|
246
|
+
r.createdAt || r.date || '',
|
|
247
|
+
r.amount ?? '',
|
|
248
|
+
r.credits ?? '',
|
|
249
|
+
r.status || '',
|
|
250
|
+
r.paymentMethod || '',
|
|
251
|
+
r.invoiceNumber || '',
|
|
252
|
+
]));
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
(0, ui_1.handleError)(err);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// --- calls -----------------------------------------------------------------
|
|
259
|
+
async function adminCallsCommand(opts) {
|
|
260
|
+
try {
|
|
261
|
+
// The server needs exactly one of these; filling more than one narrows the
|
|
262
|
+
// search rather than widening it.
|
|
263
|
+
const query = {};
|
|
264
|
+
if (opts.phone)
|
|
265
|
+
query.to = opts.phone;
|
|
266
|
+
if (opts.leadId)
|
|
267
|
+
query.lead_id = opts.leadId;
|
|
268
|
+
if (opts.buylead)
|
|
269
|
+
query.buylead = opts.buylead;
|
|
270
|
+
if (opts.callSid)
|
|
271
|
+
query.call_sid = opts.callSid;
|
|
272
|
+
if (opts.callId)
|
|
273
|
+
query.call_id = opts.callId;
|
|
274
|
+
if (!Object.keys(query).length) {
|
|
275
|
+
throw new Error('Give one of --phone, --lead-id, --buylead, --call-sid or --call-id.\n' +
|
|
276
|
+
' The search needs one of them; filling more than one narrows it.');
|
|
277
|
+
}
|
|
278
|
+
const res = await (0, adminApi_1.adminApi)().calls(query);
|
|
279
|
+
const calls = res?.calls || [];
|
|
280
|
+
if (opts.json)
|
|
281
|
+
return void console.log(JSON.stringify(calls, null, 2));
|
|
282
|
+
if (!calls.length)
|
|
283
|
+
return void (0, ui_1.info)('No calls matched.');
|
|
284
|
+
(0, ui_1.table)(['CALL ID', 'AGENT', 'VERSION', 'TO', 'DURATION', 'STATUS', 'DATE'], calls.map((c) => [
|
|
285
|
+
c._id || '',
|
|
286
|
+
c.agent?.name || '',
|
|
287
|
+
c.versionId?.versionName || '',
|
|
288
|
+
c.to_number || c.to || '',
|
|
289
|
+
c.duration ?? '',
|
|
290
|
+
c.status || c.disposition || '',
|
|
291
|
+
c.createdAt || c.date || '',
|
|
292
|
+
]));
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
(0, ui_1.handleError)(err);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function adminBotsCommand(opts) {
|
|
299
|
+
try {
|
|
300
|
+
const win = defaultWindow();
|
|
301
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
302
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
303
|
+
const res = await (0, adminApi_1.adminApi)().bots({ startDate, endDate });
|
|
304
|
+
const bots = res?.bots || [];
|
|
305
|
+
if (opts.json)
|
|
306
|
+
return void console.log(JSON.stringify(bots, null, 2));
|
|
307
|
+
if (!bots.length)
|
|
308
|
+
return void (0, ui_1.info)('No bots in that window.');
|
|
309
|
+
(0, ui_1.table)(['AGENT', 'CLIENT', 'AGENT ID', 'LEAD STAGE', 'PRIORITY'], bots.map((b) => [
|
|
310
|
+
b.agentName || b.agent?.name || '',
|
|
311
|
+
b.clientName || b.client?.name || '',
|
|
312
|
+
b.agentId || '',
|
|
313
|
+
b.leadStage || b.lead_stage || '',
|
|
314
|
+
b.priority ?? '',
|
|
315
|
+
]));
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
(0, ui_1.handleError)(err);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function adminSentryCommand(opts) {
|
|
322
|
+
try {
|
|
323
|
+
const res = await (0, adminApi_1.adminApi)().sentryTransactions();
|
|
324
|
+
if (opts.json)
|
|
325
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
326
|
+
const rows = res?.rows || [];
|
|
327
|
+
if (!rows.length)
|
|
328
|
+
return void (0, ui_1.info)('No Sentry transactions in the last 24h.');
|
|
329
|
+
(0, ui_1.table)(['TRANSACTION', 'COUNT', 'ERRORS', 'P95'], rows.map((r) => [r.transaction || r.name || '', r.count ?? '', r.errors ?? '', r.p95 ?? '']));
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
(0, ui_1.handleError)(err);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// --- writes ----------------------------------------------------------------
|
|
336
|
+
/**
|
|
337
|
+
* `cku admin recharge <clientId> --amount <n>` — add credits to a client account.
|
|
338
|
+
*
|
|
339
|
+
* This moves real money into a live account and the console has no undo, so it
|
|
340
|
+
* is confirmed, and the client's own `minRechargeAmount` is checked first.
|
|
341
|
+
*/
|
|
342
|
+
async function adminRechargeCommand(clientId, opts) {
|
|
343
|
+
try {
|
|
344
|
+
const amount = Number(opts.amount);
|
|
345
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
346
|
+
throw new Error(`--amount must be a positive number (got "${opts.amount}").`);
|
|
347
|
+
}
|
|
348
|
+
const client = await lookupClient(clientId);
|
|
349
|
+
const minimum = client?.minRechargeAmount;
|
|
350
|
+
if (minimum && amount < minimum) {
|
|
351
|
+
throw new Error(`The client account's minimum recharge is ${minimum}; ${amount} is below it.\n` +
|
|
352
|
+
' The server would reject it.');
|
|
353
|
+
}
|
|
354
|
+
const name = client?.name || clientId;
|
|
355
|
+
if (!(await (0, ui_1.confirm)(`Recharge ${amount} credits to ${name} (${clientId})? This adds real credits and the console has no undo.`, opts.yes))) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const out = await (0, adminApi_1.adminApi)().recharge(clientId, amount);
|
|
359
|
+
(0, ui_1.success)(`Recharged ${amount} to ${name}.`);
|
|
360
|
+
if (out?.user?.currentCredits !== undefined) {
|
|
361
|
+
(0, ui_1.info)(` Balance is now ${Math.round(out.user.currentCredits)}.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
(0, ui_1.handleError)(err);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** `cku admin client set <clientId> --concurrency <n>` */
|
|
369
|
+
async function adminClientSetCommand(clientId, opts) {
|
|
370
|
+
try {
|
|
371
|
+
if (opts.concurrency === undefined) {
|
|
372
|
+
throw new Error('Nothing to set. Pass --concurrency <n>.');
|
|
373
|
+
}
|
|
374
|
+
const concurrency = Number(opts.concurrency);
|
|
375
|
+
if (!Number.isInteger(concurrency) || concurrency < 0) {
|
|
376
|
+
throw new Error(`--concurrency must be a non-negative whole number (got "${opts.concurrency}").`);
|
|
377
|
+
}
|
|
378
|
+
const client = await lookupClient(clientId);
|
|
379
|
+
const name = client?.name || clientId;
|
|
380
|
+
if (!(await (0, ui_1.confirm)(`Set concurrency of ${name} to ${concurrency}?`, opts.yes)))
|
|
381
|
+
return;
|
|
382
|
+
const out = await (0, adminApi_1.adminApi)().updateClient(clientId, { concurrency });
|
|
383
|
+
(0, ui_1.success)(`Concurrency set to ${concurrency} on ${name}.`);
|
|
384
|
+
if (out?.user?.concurrency !== undefined)
|
|
385
|
+
(0, ui_1.info)(` It reads back as ${out.user.concurrency}.`);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
(0, ui_1.handleError)(err);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** `cku admin client pause|resume|clear <clientId>` */
|
|
392
|
+
async function adminClientActionCommand(action, clientId, opts) {
|
|
393
|
+
try {
|
|
394
|
+
const what = action === 'pause' ? 'Pause this client account — no more calls dial out for it.'
|
|
395
|
+
: action === 'resume' ? 'Resume this client account.'
|
|
396
|
+
: "Clear this client account's whole call queue — queued calls are dropped.";
|
|
397
|
+
if (!(await (0, ui_1.confirm)(`${what}\n Client: ${clientId}`, opts.yes)))
|
|
398
|
+
return;
|
|
399
|
+
const api = (0, adminApi_1.adminApi)();
|
|
400
|
+
if (action === 'pause')
|
|
401
|
+
await api.pauseClient(clientId);
|
|
402
|
+
else if (action === 'resume')
|
|
403
|
+
await api.resumeClient(clientId);
|
|
404
|
+
else
|
|
405
|
+
await api.clearClientQueue(clientId);
|
|
406
|
+
(0, ui_1.success)(`Client ${clientId}: ${action} done.`);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
(0, ui_1.handleError)(err);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** `cku admin agent pause|resume|clear <clientId> <agentId>` */
|
|
413
|
+
async function adminAgentActionCommand(action, clientId, agentId, opts) {
|
|
414
|
+
try {
|
|
415
|
+
if (!['pause', 'resume', 'clear'].includes(action)) {
|
|
416
|
+
throw new Error(`Action must be pause, resume or clear (got "${action}").`);
|
|
417
|
+
}
|
|
418
|
+
const what = action === 'pause' ? 'Pause this agent — no more calls dial out for it.'
|
|
419
|
+
: action === 'resume' ? 'Resume this agent.'
|
|
420
|
+
: "Clear this agent's call queue — queued calls are dropped.";
|
|
421
|
+
if (!(await (0, ui_1.confirm)(`${what}\n Client: ${clientId} Agent: ${agentId}`, opts.yes)))
|
|
422
|
+
return;
|
|
423
|
+
const api = (0, adminApi_1.adminApi)();
|
|
424
|
+
if (action === 'pause')
|
|
425
|
+
await api.pauseAgent(clientId, agentId);
|
|
426
|
+
else if (action === 'resume')
|
|
427
|
+
await api.resumeAgent(clientId, agentId);
|
|
428
|
+
else
|
|
429
|
+
await api.clearAgentQueue(clientId, agentId);
|
|
430
|
+
(0, ui_1.success)(`Agent ${agentId}: ${action} done.`);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
(0, ui_1.handleError)(err);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* `cku admin numbers bulk-update <clientIds...> --phone <n,n> [--mode add|replace]`
|
|
438
|
+
*
|
|
439
|
+
* The console normalises every number to `+` plus 12 digits and rejects
|
|
440
|
+
* anything else before it sends, so this does the same and says why.
|
|
441
|
+
*/
|
|
442
|
+
async function adminNumbersCommand(clientIds, opts) {
|
|
443
|
+
try {
|
|
444
|
+
const mode = opts.mode || 'replace';
|
|
445
|
+
if (mode !== 'add' && mode !== 'replace') {
|
|
446
|
+
throw new Error(`--mode must be "add" or "replace" (got "${mode}").`);
|
|
447
|
+
}
|
|
448
|
+
const phones = opts.phone.split(',').map((p) => p.trim()).filter(Boolean);
|
|
449
|
+
if (!phones.length)
|
|
450
|
+
throw new Error('--phone needs at least one number, comma separated.');
|
|
451
|
+
const normalised = phones.map(adminApi_1.normalisePhone);
|
|
452
|
+
const bad = normalised.filter((p) => p.length !== 13);
|
|
453
|
+
if (bad.length) {
|
|
454
|
+
throw new Error(`Each phone number must be 12 digits (e.g. +911234567890). Invalid: ${bad.join(', ')}\n` +
|
|
455
|
+
' The server rejects anything else.');
|
|
456
|
+
}
|
|
457
|
+
const verb = mode === 'add' ? 'Add' : 'Replace';
|
|
458
|
+
if (!(await (0, ui_1.confirm)(`${verb} the phone numbers ${normalised.join(', ')} on ${clientIds.length} client account(s)?`, opts.yes))) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
await (0, adminApi_1.adminApi)().bulkUpdatePhoneNumbers(clientIds, normalised, mode);
|
|
462
|
+
(0, ui_1.success)(`Phone numbers updated for ${clientIds.length} client account(s).`);
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
(0, ui_1.handleError)(err);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/** `cku admin voice-cache clear` */
|
|
469
|
+
async function adminVoiceCacheCommand(opts) {
|
|
470
|
+
try {
|
|
471
|
+
if (!(await (0, ui_1.confirm)('Clear the console-wide voice cache? Every client is affected.', opts.yes)))
|
|
472
|
+
return;
|
|
473
|
+
const res = await (0, adminApi_1.adminApi)().clearVoiceCache();
|
|
474
|
+
(0, ui_1.success)(`Voice cache cleared (${res?.clearedEntries ?? 0} entries).`);
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
(0, ui_1.handleError)(err);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// --- escape hatch ----------------------------------------------------------
|
|
481
|
+
/**
|
|
482
|
+
* `cku admin api <METHOD> <path> [--body] [--query]` — the escape hatch for a
|
|
483
|
+
* `/monitor/*` route the CLI has no command for. Mirrors `cku dashboard api`:
|
|
484
|
+
* the session cookie stays inside the CLI, the path must start with "/",
|
|
485
|
+
* redirects are refused, and a non-2xx prints the status and the whole body to
|
|
486
|
+
* stderr and exits 1. It never prints a header, so a token cannot leak.
|
|
487
|
+
*/
|
|
488
|
+
async function adminApiCommand(method, path, opts) {
|
|
489
|
+
try {
|
|
490
|
+
const verb = method.toUpperCase();
|
|
491
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(verb)) {
|
|
492
|
+
throw new Error(`Method must be one of GET, POST, PUT, PATCH, DELETE (got "${method}").`);
|
|
493
|
+
}
|
|
494
|
+
// Git Bash rewrites a leading "/" into a Windows path; put it back.
|
|
495
|
+
let target = (0, looseJson_1.repairPosixPath)(path, PATH_SEGMENTS);
|
|
496
|
+
if (opts.query?.length) {
|
|
497
|
+
const params = new URLSearchParams();
|
|
498
|
+
for (const kv of opts.query) {
|
|
499
|
+
const i = kv.indexOf('=');
|
|
500
|
+
if (i < 1)
|
|
501
|
+
throw new Error(`--query expects key=value (got "${kv}").`);
|
|
502
|
+
params.append(kv.slice(0, i), kv.slice(i + 1));
|
|
503
|
+
}
|
|
504
|
+
target += (path.includes('?') ? '&' : '?') + params.toString();
|
|
505
|
+
}
|
|
506
|
+
const body = opts.body ? await (0, jsonInput_1.readJsonFlag)(opts.body, '--body') : undefined;
|
|
507
|
+
const res = await (0, adminApi_1.adminApi)().request(verb, target, body);
|
|
508
|
+
console.log(JSON.stringify(res ?? null, null, 2));
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
if (err instanceof adminApi_1.AdminError) {
|
|
512
|
+
console.error(`HTTP ${err.status}`);
|
|
513
|
+
console.error(JSON.stringify(err.data ?? { message: err.message }, null, 2));
|
|
514
|
+
process.exitCode = 1;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
(0, ui_1.handleError)(err);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
@@ -4,7 +4,10 @@ exports.dashboardApiCommand = dashboardApiCommand;
|
|
|
4
4
|
const dashboardApi_1 = require("../lib/dashboardApi");
|
|
5
5
|
const ui_1 = require("../lib/ui");
|
|
6
6
|
const jsonInput_1 = require("./jsonInput");
|
|
7
|
+
const looseJson_1 = require("../lib/looseJson");
|
|
7
8
|
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
9
|
+
/** The route segments the path may contain — see `repairPosixPath`. */
|
|
10
|
+
const PATH_SEGMENTS = ['auth', 'v1', 'v2', 'v3', 'kpi-manager', 'pending-tasks', 'phone-number'];
|
|
8
11
|
/**
|
|
9
12
|
* `cku dashboard api <METHOD> <path> [--body <json|@file>] [--query k=v ...]`
|
|
10
13
|
*
|
|
@@ -21,7 +24,8 @@ async function dashboardApiCommand(method, path, opts) {
|
|
|
21
24
|
const verb = method.toUpperCase();
|
|
22
25
|
if (!METHODS.includes(verb))
|
|
23
26
|
throw new Error(`Method must be one of ${METHODS.join(', ')} (got "${method}").`);
|
|
24
|
-
|
|
27
|
+
// Git Bash rewrites a leading "/" into a Windows path; put it back.
|
|
28
|
+
let target = (0, looseJson_1.repairPosixPath)(path, PATH_SEGMENTS);
|
|
25
29
|
if (opts.query?.length) {
|
|
26
30
|
const params = new URLSearchParams();
|
|
27
31
|
for (const kv of opts.query) {
|