callkaro-cli-unofficial 0.5.0 → 0.6.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 +14 -0
- package/dist/commands/accounts.js +45 -4
- package/dist/commands/admin.js +517 -0
- package/dist/commands/dashboardApiCmd.js +5 -1
- package/dist/index.js +147 -0
- package/dist/lib/adminApi.js +323 -0
- package/dist/lib/adminAuth.js +388 -0
- package/dist/lib/api.js +41 -14
- package/dist/lib/config.js +143 -9
- 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
|
|
@@ -44,7 +44,19 @@ async function loginCommand(opts) {
|
|
|
44
44
|
if (!email || !password)
|
|
45
45
|
return (0, ui_1.handleError)(new Error('Email and password are required'));
|
|
46
46
|
const res = await api.login(email, password);
|
|
47
|
-
|
|
47
|
+
// The password is kept by default so a token the server stops accepting can
|
|
48
|
+
// be re-issued on its own — the `ck_` token is opaque and has no refresh
|
|
49
|
+
// route, so a dead token is otherwise a dead end. `--no-save-password` opts
|
|
50
|
+
// out and brings back a manual re-login.
|
|
51
|
+
const keep = opts.savePassword !== false;
|
|
52
|
+
(0, ui_1.success)((0, config_1.saveLogin)(res.user?.email || email, { token: res.token, user: res.user, ...(keep ? { password } : {}) }));
|
|
53
|
+
if (keep) {
|
|
54
|
+
(0, ui_1.info)(' Password saved (plain text, mode 0600) so the CLI can renew this token by itself.');
|
|
55
|
+
(0, ui_1.info)(' Drop it with `cku accounts remove <email>`, or sign in with --no-save-password.');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
(0, ui_1.info)(' Password not saved. The token cannot be renewed, so re-run this if it stops working.');
|
|
59
|
+
}
|
|
48
60
|
}
|
|
49
61
|
catch (err) {
|
|
50
62
|
(0, ui_1.handleError)(err);
|
|
@@ -160,24 +172,53 @@ async function configCommand() {
|
|
|
160
172
|
async function accountsCommand(opts) {
|
|
161
173
|
const accounts = (0, config_1.listAccounts)();
|
|
162
174
|
const active = (0, config_1.activeAccount)();
|
|
163
|
-
// Never print tokens: this output ends up in agent transcripts
|
|
175
|
+
// Never print tokens or passwords: this output ends up in agent transcripts
|
|
176
|
+
// and CI logs. `signedIn` says whether a credential is held, not what it is.
|
|
164
177
|
if (opts.json) {
|
|
165
|
-
return void console.log(JSON.stringify(accounts.map(({ token, ...a }) => ({
|
|
178
|
+
return void console.log(JSON.stringify(accounts.map(({ token, password, dashboard, admin, ...a }) => ({
|
|
179
|
+
...a,
|
|
180
|
+
signedIn: Boolean(token),
|
|
181
|
+
...(dashboard
|
|
182
|
+
? {
|
|
183
|
+
dashboard: {
|
|
184
|
+
email: dashboard.email,
|
|
185
|
+
userId: dashboard.userId,
|
|
186
|
+
expiresAt: dashboard.expiresAt,
|
|
187
|
+
savedAt: dashboard.savedAt,
|
|
188
|
+
signedIn: Boolean(dashboard.token),
|
|
189
|
+
},
|
|
190
|
+
}
|
|
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
|
+
: {}),
|
|
204
|
+
})), null, 2));
|
|
166
205
|
}
|
|
167
206
|
if (!accounts.length) {
|
|
168
207
|
return void (0, ui_1.info)('No accounts saved. Run `cku login` to add one.');
|
|
169
208
|
}
|
|
170
|
-
(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) => [
|
|
171
210
|
active && (0, config_1.slugFor)(a.email || '') === active ? '*' : '',
|
|
172
211
|
a.email || '(unknown)',
|
|
173
212
|
a.token ? 'yes' : 'no',
|
|
174
213
|
a.dashboard?.token ? (0, dashboardAuth_1.expiryLabel)(a.dashboard.token) : '—',
|
|
214
|
+
a.admin?.token ? (0, dashboardAuth_1.expiryLabel)(a.admin.token) : '—',
|
|
175
215
|
a.savedAt ? new Date(a.savedAt).toLocaleString('en-US') : '',
|
|
176
216
|
]));
|
|
177
217
|
const source = (0, config_1.accountSource)();
|
|
178
218
|
(0, ui_1.info)(`\n* = active account (${source === 'env' ? 'CK_ACCOUNT' : source === 'project' ? 'this folder\'s .cku.json' : 'global default'}).`);
|
|
179
219
|
(0, ui_1.info)('CK TOKEN gates /cli/* commands. DASHBOARD gates /auth/* and /v2/*, and is a separate');
|
|
180
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`.');
|
|
181
222
|
if (source === 'global') {
|
|
182
223
|
(0, ui_1.info)(' Tip: `cku accounts bind <email>` pins an account to this folder, so parallel');
|
|
183
224
|
(0, ui_1.info)(' agents in different folders cannot switch each other out from under themselves.');
|
|
@@ -0,0 +1,517 @@
|
|
|
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 } = 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, ui_1.success)(`Impersonating ${client.name || clientEmail} (${clientEmail}).`);
|
|
173
|
+
(0, ui_1.info)(' minted a ck_ token through the admin console and stored it, scoped to that client account.');
|
|
174
|
+
(0, ui_1.info)(` Use it: cku accounts use ${clientEmail}`);
|
|
175
|
+
(0, ui_1.info)(` Or pin a folder: cku accounts bind ${clientEmail}`);
|
|
176
|
+
(0, ui_1.info)(' The token renews itself by impersonating again.');
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
(0, ui_1.handleError)(err);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// --- analytics and costs ---------------------------------------------------
|
|
183
|
+
async function adminAnalyticsCommand(opts) {
|
|
184
|
+
try {
|
|
185
|
+
const win = defaultWindow();
|
|
186
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
187
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
188
|
+
// The server's filter is `user_ids` (a csv). `userIds` is accepted and
|
|
189
|
+
// answered with "At least one user must be selected", which reads like a
|
|
190
|
+
// permissions problem rather than a wrong parameter name.
|
|
191
|
+
const user_ids = opts.client;
|
|
192
|
+
const res = await (0, adminApi_1.adminApi)().analytics({ startDate, endDate, user_ids });
|
|
193
|
+
const rows = res?.data?.user_wise || [];
|
|
194
|
+
if (opts.json)
|
|
195
|
+
return void console.log(JSON.stringify(res?.data ?? res, null, 2));
|
|
196
|
+
if (!rows.length)
|
|
197
|
+
return void (0, ui_1.info)('No analytics for that window.');
|
|
198
|
+
(0, ui_1.table)(['CLIENT ID', 'CALLS', 'MINUTES', 'CONNECTED', 'CONNECTED MIN', 'PULSE MIN', 'COST'], rows.map((r) => [
|
|
199
|
+
r.userId,
|
|
200
|
+
r.total_calls ?? '',
|
|
201
|
+
Math.round(r.minutes ?? 0),
|
|
202
|
+
r.connected_calls ?? '',
|
|
203
|
+
Math.round(r.connected_minutes ?? 0),
|
|
204
|
+
Math.round(r.pulse_minutes ?? 0),
|
|
205
|
+
r.total_cost !== undefined ? Math.round(r.total_cost) : '',
|
|
206
|
+
]));
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
(0, ui_1.handleError)(err);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function adminCostsCommand(opts) {
|
|
213
|
+
try {
|
|
214
|
+
const win = defaultWindow();
|
|
215
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
216
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
217
|
+
const user_ids = opts.client;
|
|
218
|
+
const res = await (0, adminApi_1.adminApi)().costs({ startDate, endDate, user_ids });
|
|
219
|
+
const data = res?.data || res;
|
|
220
|
+
if (opts.json)
|
|
221
|
+
return void console.log(JSON.stringify(data, null, 2));
|
|
222
|
+
(0, ui_1.keyValue)({ totalCost: data?.totalCost, days: (data?.daily || []).length });
|
|
223
|
+
const daily = (data?.daily || []).slice(-30);
|
|
224
|
+
if (!daily.length)
|
|
225
|
+
return;
|
|
226
|
+
(0, ui_1.table)(['DATE', 'TOTAL'], daily.map((d) => [d.date, Math.round(d.total ?? 0)]));
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
(0, ui_1.handleError)(err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function adminRechargesCommand(clientId, opts) {
|
|
233
|
+
try {
|
|
234
|
+
const win = defaultWindow();
|
|
235
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
236
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
237
|
+
const res = await (0, adminApi_1.adminApi)().recharges(clientId, { startDate, endDate });
|
|
238
|
+
const rows = res?.recharges || [];
|
|
239
|
+
if (opts.json)
|
|
240
|
+
return void console.log(JSON.stringify(rows, null, 2));
|
|
241
|
+
if (!rows.length)
|
|
242
|
+
return void (0, ui_1.info)('No recharges in that window.');
|
|
243
|
+
(0, ui_1.table)(['DATE', 'AMOUNT', 'CREDITS', 'STATUS', 'METHOD', 'INVOICE'], rows.map((r) => [
|
|
244
|
+
r.createdAt || r.date || '',
|
|
245
|
+
r.amount ?? '',
|
|
246
|
+
r.credits ?? '',
|
|
247
|
+
r.status || '',
|
|
248
|
+
r.paymentMethod || '',
|
|
249
|
+
r.invoiceNumber || '',
|
|
250
|
+
]));
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
(0, ui_1.handleError)(err);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// --- calls -----------------------------------------------------------------
|
|
257
|
+
async function adminCallsCommand(opts) {
|
|
258
|
+
try {
|
|
259
|
+
// The server needs exactly one of these; filling more than one narrows the
|
|
260
|
+
// search rather than widening it.
|
|
261
|
+
const query = {};
|
|
262
|
+
if (opts.phone)
|
|
263
|
+
query.to = opts.phone;
|
|
264
|
+
if (opts.leadId)
|
|
265
|
+
query.lead_id = opts.leadId;
|
|
266
|
+
if (opts.buylead)
|
|
267
|
+
query.buylead = opts.buylead;
|
|
268
|
+
if (opts.callSid)
|
|
269
|
+
query.call_sid = opts.callSid;
|
|
270
|
+
if (opts.callId)
|
|
271
|
+
query.call_id = opts.callId;
|
|
272
|
+
if (!Object.keys(query).length) {
|
|
273
|
+
throw new Error('Give one of --phone, --lead-id, --buylead, --call-sid or --call-id.\n' +
|
|
274
|
+
' The search needs one of them; filling more than one narrows it.');
|
|
275
|
+
}
|
|
276
|
+
const res = await (0, adminApi_1.adminApi)().calls(query);
|
|
277
|
+
const calls = res?.calls || [];
|
|
278
|
+
if (opts.json)
|
|
279
|
+
return void console.log(JSON.stringify(calls, null, 2));
|
|
280
|
+
if (!calls.length)
|
|
281
|
+
return void (0, ui_1.info)('No calls matched.');
|
|
282
|
+
(0, ui_1.table)(['CALL ID', 'AGENT', 'VERSION', 'TO', 'DURATION', 'STATUS', 'DATE'], calls.map((c) => [
|
|
283
|
+
c._id || '',
|
|
284
|
+
c.agent?.name || '',
|
|
285
|
+
c.versionId?.versionName || '',
|
|
286
|
+
c.to_number || c.to || '',
|
|
287
|
+
c.duration ?? '',
|
|
288
|
+
c.status || c.disposition || '',
|
|
289
|
+
c.createdAt || c.date || '',
|
|
290
|
+
]));
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
(0, ui_1.handleError)(err);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function adminBotsCommand(opts) {
|
|
297
|
+
try {
|
|
298
|
+
const win = defaultWindow();
|
|
299
|
+
const startDate = dateArg(opts.start, '--start', win.startDate);
|
|
300
|
+
const endDate = dateArg(opts.end, '--end', win.endDate);
|
|
301
|
+
const res = await (0, adminApi_1.adminApi)().bots({ startDate, endDate });
|
|
302
|
+
const bots = res?.bots || [];
|
|
303
|
+
if (opts.json)
|
|
304
|
+
return void console.log(JSON.stringify(bots, null, 2));
|
|
305
|
+
if (!bots.length)
|
|
306
|
+
return void (0, ui_1.info)('No bots in that window.');
|
|
307
|
+
(0, ui_1.table)(['AGENT', 'CLIENT', 'AGENT ID', 'LEAD STAGE', 'PRIORITY'], bots.map((b) => [
|
|
308
|
+
b.agentName || b.agent?.name || '',
|
|
309
|
+
b.clientName || b.client?.name || '',
|
|
310
|
+
b.agentId || '',
|
|
311
|
+
b.leadStage || b.lead_stage || '',
|
|
312
|
+
b.priority ?? '',
|
|
313
|
+
]));
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
(0, ui_1.handleError)(err);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function adminSentryCommand(opts) {
|
|
320
|
+
try {
|
|
321
|
+
const res = await (0, adminApi_1.adminApi)().sentryTransactions();
|
|
322
|
+
if (opts.json)
|
|
323
|
+
return void console.log(JSON.stringify(res, null, 2));
|
|
324
|
+
const rows = res?.rows || [];
|
|
325
|
+
if (!rows.length)
|
|
326
|
+
return void (0, ui_1.info)('No Sentry transactions in the last 24h.');
|
|
327
|
+
(0, ui_1.table)(['TRANSACTION', 'COUNT', 'ERRORS', 'P95'], rows.map((r) => [r.transaction || r.name || '', r.count ?? '', r.errors ?? '', r.p95 ?? '']));
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
(0, ui_1.handleError)(err);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// --- writes ----------------------------------------------------------------
|
|
334
|
+
/**
|
|
335
|
+
* `cku admin recharge <clientId> --amount <n>` — add credits to a client account.
|
|
336
|
+
*
|
|
337
|
+
* This moves real money into a live account and the console has no undo, so it
|
|
338
|
+
* is confirmed, and the client's own `minRechargeAmount` is checked first.
|
|
339
|
+
*/
|
|
340
|
+
async function adminRechargeCommand(clientId, opts) {
|
|
341
|
+
try {
|
|
342
|
+
const amount = Number(opts.amount);
|
|
343
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
344
|
+
throw new Error(`--amount must be a positive number (got "${opts.amount}").`);
|
|
345
|
+
}
|
|
346
|
+
const client = await lookupClient(clientId);
|
|
347
|
+
const minimum = client?.minRechargeAmount;
|
|
348
|
+
if (minimum && amount < minimum) {
|
|
349
|
+
throw new Error(`The client account's minimum recharge is ${minimum}; ${amount} is below it.\n` +
|
|
350
|
+
' The server would reject it.');
|
|
351
|
+
}
|
|
352
|
+
const name = client?.name || clientId;
|
|
353
|
+
if (!(await (0, ui_1.confirm)(`Recharge ${amount} credits to ${name} (${clientId})? This adds real credits and the console has no undo.`, opts.yes))) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const out = await (0, adminApi_1.adminApi)().recharge(clientId, amount);
|
|
357
|
+
(0, ui_1.success)(`Recharged ${amount} to ${name}.`);
|
|
358
|
+
if (out?.user?.currentCredits !== undefined) {
|
|
359
|
+
(0, ui_1.info)(` Balance is now ${Math.round(out.user.currentCredits)}.`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
(0, ui_1.handleError)(err);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/** `cku admin client set <clientId> --concurrency <n>` */
|
|
367
|
+
async function adminClientSetCommand(clientId, opts) {
|
|
368
|
+
try {
|
|
369
|
+
if (opts.concurrency === undefined) {
|
|
370
|
+
throw new Error('Nothing to set. Pass --concurrency <n>.');
|
|
371
|
+
}
|
|
372
|
+
const concurrency = Number(opts.concurrency);
|
|
373
|
+
if (!Number.isInteger(concurrency) || concurrency < 0) {
|
|
374
|
+
throw new Error(`--concurrency must be a non-negative whole number (got "${opts.concurrency}").`);
|
|
375
|
+
}
|
|
376
|
+
const client = await lookupClient(clientId);
|
|
377
|
+
const name = client?.name || clientId;
|
|
378
|
+
if (!(await (0, ui_1.confirm)(`Set concurrency of ${name} to ${concurrency}?`, opts.yes)))
|
|
379
|
+
return;
|
|
380
|
+
const out = await (0, adminApi_1.adminApi)().updateClient(clientId, { concurrency });
|
|
381
|
+
(0, ui_1.success)(`Concurrency set to ${concurrency} on ${name}.`);
|
|
382
|
+
if (out?.user?.concurrency !== undefined)
|
|
383
|
+
(0, ui_1.info)(` It reads back as ${out.user.concurrency}.`);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
(0, ui_1.handleError)(err);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** `cku admin client pause|resume|clear <clientId>` */
|
|
390
|
+
async function adminClientActionCommand(action, clientId, opts) {
|
|
391
|
+
try {
|
|
392
|
+
const what = action === 'pause' ? 'Pause this client account — no more calls dial out for it.'
|
|
393
|
+
: action === 'resume' ? 'Resume this client account.'
|
|
394
|
+
: "Clear this client account's whole call queue — queued calls are dropped.";
|
|
395
|
+
if (!(await (0, ui_1.confirm)(`${what}\n Client: ${clientId}`, opts.yes)))
|
|
396
|
+
return;
|
|
397
|
+
const api = (0, adminApi_1.adminApi)();
|
|
398
|
+
if (action === 'pause')
|
|
399
|
+
await api.pauseClient(clientId);
|
|
400
|
+
else if (action === 'resume')
|
|
401
|
+
await api.resumeClient(clientId);
|
|
402
|
+
else
|
|
403
|
+
await api.clearClientQueue(clientId);
|
|
404
|
+
(0, ui_1.success)(`Client ${clientId}: ${action} done.`);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
(0, ui_1.handleError)(err);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/** `cku admin agent pause|resume|clear <clientId> <agentId>` */
|
|
411
|
+
async function adminAgentActionCommand(action, clientId, agentId, opts) {
|
|
412
|
+
try {
|
|
413
|
+
if (!['pause', 'resume', 'clear'].includes(action)) {
|
|
414
|
+
throw new Error(`Action must be pause, resume or clear (got "${action}").`);
|
|
415
|
+
}
|
|
416
|
+
const what = action === 'pause' ? 'Pause this agent — no more calls dial out for it.'
|
|
417
|
+
: action === 'resume' ? 'Resume this agent.'
|
|
418
|
+
: "Clear this agent's call queue — queued calls are dropped.";
|
|
419
|
+
if (!(await (0, ui_1.confirm)(`${what}\n Client: ${clientId} Agent: ${agentId}`, opts.yes)))
|
|
420
|
+
return;
|
|
421
|
+
const api = (0, adminApi_1.adminApi)();
|
|
422
|
+
if (action === 'pause')
|
|
423
|
+
await api.pauseAgent(clientId, agentId);
|
|
424
|
+
else if (action === 'resume')
|
|
425
|
+
await api.resumeAgent(clientId, agentId);
|
|
426
|
+
else
|
|
427
|
+
await api.clearAgentQueue(clientId, agentId);
|
|
428
|
+
(0, ui_1.success)(`Agent ${agentId}: ${action} done.`);
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
(0, ui_1.handleError)(err);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* `cku admin numbers bulk-update <clientIds...> --phone <n,n> [--mode add|replace]`
|
|
436
|
+
*
|
|
437
|
+
* The console normalises every number to `+` plus 12 digits and rejects
|
|
438
|
+
* anything else before it sends, so this does the same and says why.
|
|
439
|
+
*/
|
|
440
|
+
async function adminNumbersCommand(clientIds, opts) {
|
|
441
|
+
try {
|
|
442
|
+
const mode = opts.mode || 'replace';
|
|
443
|
+
if (mode !== 'add' && mode !== 'replace') {
|
|
444
|
+
throw new Error(`--mode must be "add" or "replace" (got "${mode}").`);
|
|
445
|
+
}
|
|
446
|
+
const phones = opts.phone.split(',').map((p) => p.trim()).filter(Boolean);
|
|
447
|
+
if (!phones.length)
|
|
448
|
+
throw new Error('--phone needs at least one number, comma separated.');
|
|
449
|
+
const normalised = phones.map(adminApi_1.normalisePhone);
|
|
450
|
+
const bad = normalised.filter((p) => p.length !== 13);
|
|
451
|
+
if (bad.length) {
|
|
452
|
+
throw new Error(`Each phone number must be 12 digits (e.g. +911234567890). Invalid: ${bad.join(', ')}\n` +
|
|
453
|
+
' The server rejects anything else.');
|
|
454
|
+
}
|
|
455
|
+
const verb = mode === 'add' ? 'Add' : 'Replace';
|
|
456
|
+
if (!(await (0, ui_1.confirm)(`${verb} the phone numbers ${normalised.join(', ')} on ${clientIds.length} client account(s)?`, opts.yes))) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
await (0, adminApi_1.adminApi)().bulkUpdatePhoneNumbers(clientIds, normalised, mode);
|
|
460
|
+
(0, ui_1.success)(`Phone numbers updated for ${clientIds.length} client account(s).`);
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
(0, ui_1.handleError)(err);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/** `cku admin voice-cache clear` */
|
|
467
|
+
async function adminVoiceCacheCommand(opts) {
|
|
468
|
+
try {
|
|
469
|
+
if (!(await (0, ui_1.confirm)('Clear the console-wide voice cache? Every client is affected.', opts.yes)))
|
|
470
|
+
return;
|
|
471
|
+
const res = await (0, adminApi_1.adminApi)().clearVoiceCache();
|
|
472
|
+
(0, ui_1.success)(`Voice cache cleared (${res?.clearedEntries ?? 0} entries).`);
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
(0, ui_1.handleError)(err);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// --- escape hatch ----------------------------------------------------------
|
|
479
|
+
/**
|
|
480
|
+
* `cku admin api <METHOD> <path> [--body] [--query]` — the escape hatch for a
|
|
481
|
+
* `/monitor/*` route the CLI has no command for. Mirrors `cku dashboard api`:
|
|
482
|
+
* the session cookie stays inside the CLI, the path must start with "/",
|
|
483
|
+
* redirects are refused, and a non-2xx prints the status and the whole body to
|
|
484
|
+
* stderr and exits 1. It never prints a header, so a token cannot leak.
|
|
485
|
+
*/
|
|
486
|
+
async function adminApiCommand(method, path, opts) {
|
|
487
|
+
try {
|
|
488
|
+
const verb = method.toUpperCase();
|
|
489
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(verb)) {
|
|
490
|
+
throw new Error(`Method must be one of GET, POST, PUT, PATCH, DELETE (got "${method}").`);
|
|
491
|
+
}
|
|
492
|
+
// Git Bash rewrites a leading "/" into a Windows path; put it back.
|
|
493
|
+
let target = (0, looseJson_1.repairPosixPath)(path, PATH_SEGMENTS);
|
|
494
|
+
if (opts.query?.length) {
|
|
495
|
+
const params = new URLSearchParams();
|
|
496
|
+
for (const kv of opts.query) {
|
|
497
|
+
const i = kv.indexOf('=');
|
|
498
|
+
if (i < 1)
|
|
499
|
+
throw new Error(`--query expects key=value (got "${kv}").`);
|
|
500
|
+
params.append(kv.slice(0, i), kv.slice(i + 1));
|
|
501
|
+
}
|
|
502
|
+
target += (path.includes('?') ? '&' : '?') + params.toString();
|
|
503
|
+
}
|
|
504
|
+
const body = opts.body ? await (0, jsonInput_1.readJsonFlag)(opts.body, '--body') : undefined;
|
|
505
|
+
const res = await (0, adminApi_1.adminApi)().request(verb, target, body);
|
|
506
|
+
console.log(JSON.stringify(res ?? null, null, 2));
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
if (err instanceof adminApi_1.AdminError) {
|
|
510
|
+
console.error(`HTTP ${err.status}`);
|
|
511
|
+
console.error(JSON.stringify(err.data ?? { message: err.message }, null, 2));
|
|
512
|
+
process.exitCode = 1;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
(0, ui_1.handleError)(err);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
@@ -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) {
|