callkaro-cli-unofficial 0.1.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 +196 -0
- package/dist/commands/accounts.js +275 -0
- package/dist/commands/agentPhone.js +50 -0
- package/dist/commands/agentVersions.js +228 -0
- package/dist/commands/agents.js +455 -0
- package/dist/commands/analytics.js +77 -0
- package/dist/commands/analyzers.js +130 -0
- package/dist/commands/auditStrategies.js +178 -0
- package/dist/commands/batches.js +189 -0
- package/dist/commands/calls.js +193 -0
- package/dist/commands/chatAgents.js +251 -0
- package/dist/commands/jsonInput.js +56 -0
- package/dist/commands/kpi.js +681 -0
- package/dist/commands/models.js +99 -0
- package/dist/commands/numbers.js +154 -0
- package/dist/commands/ongoing.js +39 -0
- package/dist/commands/secrets.js +124 -0
- package/dist/commands/setJson.js +42 -0
- package/dist/commands/simulations.js +139 -0
- package/dist/commands/skills.js +318 -0
- package/dist/commands/transcribers.js +95 -0
- package/dist/commands/update.js +86 -0
- package/dist/commands/voices.js +211 -0
- package/dist/data/catalog.json +1978 -0
- package/dist/endpoints.json +8 -0
- package/dist/index.js +684 -0
- package/dist/lib/agentImport.js +60 -0
- package/dist/lib/agentUpdate.js +262 -0
- package/dist/lib/api.js +287 -0
- package/dist/lib/browserAuth.js +93 -0
- package/dist/lib/catalog.js +136 -0
- package/dist/lib/config.js +354 -0
- package/dist/lib/dashboardApi.js +197 -0
- package/dist/lib/dashboardAuth.js +339 -0
- package/dist/lib/dbFields.js +30 -0
- package/dist/lib/logQuery.js +140 -0
- package/dist/lib/looseJson.js +198 -0
- package/dist/lib/session.js +17 -0
- package/dist/lib/types.js +15 -0
- package/dist/lib/ui.js +91 -0
- package/dist/lib/versions.js +19 -0
- package/package.json +52 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DashboardApi = void 0;
|
|
4
|
+
exports.dashboardApi = dashboardApi;
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const dashboardAuth_1 = require("./dashboardAuth");
|
|
7
|
+
/**
|
|
8
|
+
* Client for the CallKaro *dashboard* API — a different surface from `/cli/*`.
|
|
9
|
+
*
|
|
10
|
+
* ## Two tokens, two halves of the API
|
|
11
|
+
*
|
|
12
|
+
* `POST /auth/login` returns two JWTs and they are not interchangeable. Probed
|
|
13
|
+
* every route below with both on 1 October 2026:
|
|
14
|
+
*
|
|
15
|
+
* | token | claims | serves |
|
|
16
|
+
* |------------------------------|---------------------|------------------------------------------------------------|
|
|
17
|
+
* | `auth-token` (response body) | `{user:{id,email}}` | `GET /v2/agents/` only, as `Authorization: Bearer` |
|
|
18
|
+
* | `access_token` (Set-Cookie) | `{id,isAdmin,…}` | `/auth/*`, `/v1/*`, `/kpi-manager`, `/pending-tasks`, |
|
|
19
|
+
* | | | `/phone-number/`, as `Cookie: access_token=…` |
|
|
20
|
+
*
|
|
21
|
+
* Each is rejected by the other's routes with `401`. The comment on each method
|
|
22
|
+
* says which token it uses, because getting this wrong produces an unexplained
|
|
23
|
+
* `Unauthorised!` rather than anything diagnostic.
|
|
24
|
+
*
|
|
25
|
+
* The `ck_` token from `/cli/auth/login` is rejected by all of these, so nothing
|
|
26
|
+
* here may be called through `requireApi()`.
|
|
27
|
+
*
|
|
28
|
+
* Everything on this surface is undocumented and can change without notice.
|
|
29
|
+
*/
|
|
30
|
+
class DashboardApi {
|
|
31
|
+
baseUrl;
|
|
32
|
+
constructor(baseUrl) {
|
|
33
|
+
this.baseUrl = baseUrl;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One place that adds auth, so no caller can forget it. `requireDashboard()`
|
|
37
|
+
* runs per request rather than once at construction: it is what proves the
|
|
38
|
+
* stored 24h token is still valid, and a long-lived client must not outlive it.
|
|
39
|
+
*/
|
|
40
|
+
async request(method, path, body) {
|
|
41
|
+
const { token, sessionToken } = (0, dashboardAuth_1.requireDashboard)();
|
|
42
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
43
|
+
if (usesCookieAuth(path)) {
|
|
44
|
+
headers.Cookie = `access_token=${sessionToken}`;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
headers.Authorization = `Bearer ${token}`;
|
|
48
|
+
}
|
|
49
|
+
let res;
|
|
50
|
+
try {
|
|
51
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
52
|
+
method,
|
|
53
|
+
headers,
|
|
54
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new Error(`Cannot reach ${this.baseUrl} (${err.message})`);
|
|
59
|
+
}
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
let data;
|
|
62
|
+
try {
|
|
63
|
+
data = text ? JSON.parse(text) : undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
data = { msg: text.slice(0, 200) };
|
|
67
|
+
}
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw new Error(data?.msg || data?.message || `Request failed (${res.status})`);
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `GET /v2/agents/` — the one route that wants the body token as a bearer.
|
|
75
|
+
* The only place `agentFolders` is exposed, which `/cli/agents` does not return.
|
|
76
|
+
*/
|
|
77
|
+
agentsWithFolders() { return this.request('GET', '/v2/agents/'); }
|
|
78
|
+
/**
|
|
79
|
+
* Bootstrap payload from `/auth/get-user`.
|
|
80
|
+
*
|
|
81
|
+
* Worth its own method because it is the one route carrying the server's own
|
|
82
|
+
* model and provider catalogues, which `ENUMS` validation mirrors. Also returns
|
|
83
|
+
* `agents` and `phoneNumbers`, which is why it is worth calling rather than
|
|
84
|
+
* just `/auth/get-user` for the identity.
|
|
85
|
+
*/
|
|
86
|
+
getUser() { return this.request('GET', '/auth/get-user'); }
|
|
87
|
+
// --- KPI manager ----------------------------------------------------------
|
|
88
|
+
/** Agent ids the account is scored on. */
|
|
89
|
+
kpiAgents() { return this.request('GET', '/kpi-manager'); }
|
|
90
|
+
/** Metric history for a set of agents. Params: agentIds (csv), startDate, endDate. */
|
|
91
|
+
kpiHistory(query = {}) {
|
|
92
|
+
return this.request('GET', `/kpi-manager/metrics-history${qs(query)}`);
|
|
93
|
+
}
|
|
94
|
+
/** The pending-task board. Large — always page it client-side or with --limit. */
|
|
95
|
+
pendingTasks(query = {}) {
|
|
96
|
+
return this.request('GET', `/pending-tasks${qs(query)}`);
|
|
97
|
+
}
|
|
98
|
+
teamMembers() { return this.request('GET', '/feedback/users/team-members'); }
|
|
99
|
+
// --- KPI board writes -----------------------------------------------------
|
|
100
|
+
//
|
|
101
|
+
// Routes, bodies and the column set below were read out of the dashboard's own
|
|
102
|
+
// bundle rather than guessed — this surface is undocumented, and a wrong guess
|
|
103
|
+
// is a 404 that looks like a broken CLI. Two things that reading settled, and
|
|
104
|
+
// which are easy to get wrong:
|
|
105
|
+
//
|
|
106
|
+
// * There is no delete for a card. The board retires one by moving it to the
|
|
107
|
+
// `rejected` column. The only DELETE is an attachment, by URL.
|
|
108
|
+
// * PATCH /pending-tasks/:taskId writes `description` and nothing else.
|
|
109
|
+
// cause / recommendedFix / severity are settable at create and are not
|
|
110
|
+
// editable afterwards through this surface.
|
|
111
|
+
/** Create a card. `cause` is the only field the server treats as required. */
|
|
112
|
+
createPendingTask(body) {
|
|
113
|
+
return this.request('POST', '/pending-tasks', body);
|
|
114
|
+
}
|
|
115
|
+
/** Edit a card's description. The dashboard PATCHes this field alone. */
|
|
116
|
+
updatePendingTask(taskId, body) {
|
|
117
|
+
return this.request('PATCH', `/pending-tasks/${encodeURIComponent(taskId)}`, body);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Move a card to a column, optionally positioning it between two siblings.
|
|
121
|
+
* `beforeTaskId` / `afterTaskId` may be null to append. Omit both to let the
|
|
122
|
+
* server place it, which is what the dashboard does for a plain column change.
|
|
123
|
+
*/
|
|
124
|
+
movePendingTask(taskId, columnId, order) {
|
|
125
|
+
return this.request('POST', `/pending-tasks/${encodeURIComponent(taskId)}/move`, {
|
|
126
|
+
columnId,
|
|
127
|
+
beforeTaskId: order?.beforeTaskId ?? null,
|
|
128
|
+
afterTaskId: order?.afterTaskId ?? null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Append a comment. Returns the updated task. */
|
|
132
|
+
commentOnPendingTask(taskId, text) {
|
|
133
|
+
return this.request('POST', `/pending-tasks/${encodeURIComponent(taskId)}/comments`, { text });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Assign one or more cards. The response is `{results:[{success,error,queued}]}`
|
|
137
|
+
* per assignment, so a partial failure is normal — callers must check each.
|
|
138
|
+
*/
|
|
139
|
+
savePendingAssignments(assignments) {
|
|
140
|
+
return this.request('POST', '/pending-tasks/save', { assignments });
|
|
141
|
+
}
|
|
142
|
+
/** Detach one attachment, identified by its URL. */
|
|
143
|
+
deletePendingTaskAttachment(taskId, url) {
|
|
144
|
+
return this.request('DELETE', `/pending-tasks/${encodeURIComponent(taskId)}/attachments`, { url });
|
|
145
|
+
}
|
|
146
|
+
// --- KPI manager writes ---------------------------------------------------
|
|
147
|
+
/** One agent's KPI Manager configuration, or 404 when it has none. */
|
|
148
|
+
kpiAgentConfig(agentId) {
|
|
149
|
+
return this.request('GET', `/kpi-manager/${encodeURIComponent(agentId)}`);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Set the target and the RCA config. `rca_model` and `rca_prompt` are sent
|
|
153
|
+
* together with the metric, and the dashboard sends whatever is in its form —
|
|
154
|
+
* so read the current config first and send it back, or an omitted field is
|
|
155
|
+
* cleared rather than left alone.
|
|
156
|
+
*/
|
|
157
|
+
setKpiAgentConfig(agentId, body) {
|
|
158
|
+
return this.request('PUT', `/kpi-manager/${encodeURIComponent(agentId)}`, body);
|
|
159
|
+
}
|
|
160
|
+
/** Kick off an RCA run for one agent. `filters` scopes the date range. */
|
|
161
|
+
runKpiRca(agentId, filters) {
|
|
162
|
+
return this.request('POST', `/kpi-manager/${encodeURIComponent(agentId)}/rca/run-now`, { filters });
|
|
163
|
+
}
|
|
164
|
+
// --- Call analyzers -------------------------------------------------------
|
|
165
|
+
listAnalyzers() { return this.request('GET', '/v1/call-analyzers'); }
|
|
166
|
+
// --- Audit reports (dashboard side) ---------------------------------------
|
|
167
|
+
/** Distinct from /cli/audit-strategies: these are completed run reports. */
|
|
168
|
+
listAuditReports(query = {}) {
|
|
169
|
+
return this.request('GET', `/v1/audit-reports${qs(query)}`);
|
|
170
|
+
}
|
|
171
|
+
listAuditStrategies(query = {}) {
|
|
172
|
+
return this.request('GET', `/v1/audit-reports/strategies${qs(query)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.DashboardApi = DashboardApi;
|
|
176
|
+
/** Build a client bound to the configured backend. */
|
|
177
|
+
function dashboardApi() {
|
|
178
|
+
return new DashboardApi((0, config_1.backendUrl)());
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Which of the two tokens a route wants. Cookie-authenticated by default because
|
|
182
|
+
* that is the overwhelming majority; only `/v2/agents/` opted out of it.
|
|
183
|
+
*
|
|
184
|
+
* Kept as one function so the rule is auditable in one place. Adding a `/v2/*`
|
|
185
|
+
* route means checking this against a live probe first — the backend does not
|
|
186
|
+
* accept either token everywhere, and guessing produces an unexplained 401.
|
|
187
|
+
*/
|
|
188
|
+
function usesCookieAuth(path) {
|
|
189
|
+
return !path.startsWith('/v2/agents');
|
|
190
|
+
}
|
|
191
|
+
/** `?a=1&b=2`, skipping undefined and empty. Mirrors the one in api.ts. */
|
|
192
|
+
function qs(params) {
|
|
193
|
+
const parts = Object.entries(params)
|
|
194
|
+
.filter(([, v]) => v !== undefined && v !== '')
|
|
195
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
196
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
197
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
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.decodeJwt = decodeJwt;
|
|
7
|
+
exports.msUntilExpiry = msUntilExpiry;
|
|
8
|
+
exports.dashboardLogin = dashboardLogin;
|
|
9
|
+
exports.saveDashboard = saveDashboard;
|
|
10
|
+
exports.dashboardSession = dashboardSession;
|
|
11
|
+
exports.hasDashboard = hasDashboard;
|
|
12
|
+
exports.forgetDashboard = forgetDashboard;
|
|
13
|
+
exports.requireDashboard = requireDashboard;
|
|
14
|
+
exports.expiryLabel = expiryLabel;
|
|
15
|
+
exports.dashboardLoginCommand = dashboardLoginCommand;
|
|
16
|
+
exports.dashboardWhoamiCommand = dashboardWhoamiCommand;
|
|
17
|
+
exports.dashboardProbeCommand = dashboardProbeCommand;
|
|
18
|
+
exports.refreshModelCatalog = refreshModelCatalog;
|
|
19
|
+
exports.dashboardLogoutCommand = dashboardLogoutCommand;
|
|
20
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
21
|
+
const ui_1 = require("./ui");
|
|
22
|
+
const config_1 = require("./config");
|
|
23
|
+
/**
|
|
24
|
+
* Pull the `access_token` JWT out of the Set-Cookie headers.
|
|
25
|
+
*
|
|
26
|
+
* The header is a comma-joined list of several cookies and each is repeated for
|
|
27
|
+
* three domains, with three leading entries that clear the cookie (empty value,
|
|
28
|
+
* epoch expiry). Only the populated one carries a JWT.
|
|
29
|
+
*/
|
|
30
|
+
function sessionTokenFrom(response) {
|
|
31
|
+
const raw = typeof response.headers.getSetCookie === 'function'
|
|
32
|
+
? response.headers.getSetCookie()
|
|
33
|
+
: [response.headers.get('set-cookie') || ''];
|
|
34
|
+
const jwt = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/;
|
|
35
|
+
// Iterate the headers, not the joined string: `for...of` over a string yields
|
|
36
|
+
// characters, which silently never matches.
|
|
37
|
+
for (const header of raw) {
|
|
38
|
+
const match = header.match(/access_token=([^;,\s]*)/);
|
|
39
|
+
if (match && jwt.test(match[1]))
|
|
40
|
+
return match[1];
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Decode a JWT payload without verifying it. Safe: we only read `exp`/`email`. */
|
|
45
|
+
function decodeJwt(token) {
|
|
46
|
+
const parts = token.split('.');
|
|
47
|
+
if (parts.length !== 3)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** ms until expiry, or undefined if the token has no readable `exp`. */
|
|
57
|
+
function msUntilExpiry(token) {
|
|
58
|
+
const exp = decodeJwt(token)?.exp;
|
|
59
|
+
return typeof exp === 'number' ? exp * 1000 - Date.now() : undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Password sign-in against the dashboard, exactly as the web form does it.
|
|
63
|
+
* The password is used here and then discarded — it is never stored.
|
|
64
|
+
*
|
|
65
|
+
* Two JWTs come back and both are kept, because they authorise different routes:
|
|
66
|
+
* `auth-token` in the body serves `GET /v2/agents/` as a bearer, while
|
|
67
|
+
* `access_token` in the Set-Cookie serves `/auth/*`, `/v1/*`, `/kpi-manager`,
|
|
68
|
+
* `/pending-tasks` and `/phone-number/` as a cookie. Verified by probing every
|
|
69
|
+
* route with each token: each is rejected by the other's routes.
|
|
70
|
+
*/
|
|
71
|
+
async function dashboardLogin(email, password) {
|
|
72
|
+
const res = await fetch(`${(0, config_1.backendUrl)()}/auth/login`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({ email, password }),
|
|
76
|
+
});
|
|
77
|
+
const text = await res.text();
|
|
78
|
+
let data;
|
|
79
|
+
try {
|
|
80
|
+
data = text ? JSON.parse(text) : {};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new Error(`Login returned a non-JSON response (HTTP ${res.status}).`);
|
|
84
|
+
}
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
throw new Error(data.msg || `Login failed (HTTP ${res.status})`);
|
|
87
|
+
}
|
|
88
|
+
const token = data['auth-token'];
|
|
89
|
+
if (!token)
|
|
90
|
+
throw new Error('Login succeeded but returned no auth-token.');
|
|
91
|
+
const sessionToken = sessionTokenFrom(res);
|
|
92
|
+
if (!sessionToken) {
|
|
93
|
+
// Almost always a proxy stripping Set-Cookie. Worth naming: without it, half
|
|
94
|
+
// the dashboard (`/kpi-manager`, `/v1/*`, `/pending-tasks`) will 401 later.
|
|
95
|
+
throw new Error('Login succeeded but no access_token cookie came back, so the dashboard session is incomplete.\n' +
|
|
96
|
+
' Without it, `cku kpi` and `cku analyzers` will fail with "Unauthorised!".\n' +
|
|
97
|
+
' Usually a proxy or browser extension stripping Set-Cookie on /auth/login.');
|
|
98
|
+
}
|
|
99
|
+
const payload = decodeJwt(token) || {};
|
|
100
|
+
return {
|
|
101
|
+
email: payload.user?.email || data.user?.email || email,
|
|
102
|
+
token,
|
|
103
|
+
sessionToken,
|
|
104
|
+
userId: data.id || data.user?.id || payload.user?.id || '',
|
|
105
|
+
/** Precomputed so the UI can show "expires in 23h" without decoding each time. */
|
|
106
|
+
expiresAt: typeof payload.exp === 'number' ? new Date(payload.exp * 1000).toISOString() : undefined,
|
|
107
|
+
savedAt: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// --- account binding -------------------------------------------------------
|
|
111
|
+
/** Store on the account, keeping whatever else it holds (notably the `ck_` token). */
|
|
112
|
+
function saveDashboard(email, session) {
|
|
113
|
+
return (0, config_1.saveDashboardSession)(email, session);
|
|
114
|
+
}
|
|
115
|
+
function dashboardSession(email) {
|
|
116
|
+
if (!email)
|
|
117
|
+
return undefined;
|
|
118
|
+
return (0, config_1.loadAccount)((0, config_1.slugFor)(email)).dashboard;
|
|
119
|
+
}
|
|
120
|
+
/** Does this account hold a dashboard session locally? A hint, not a guarantee. */
|
|
121
|
+
function hasDashboard(email) {
|
|
122
|
+
return Boolean(email && (0, config_1.loadAccount)((0, config_1.slugFor)(email)).dashboard?.token);
|
|
123
|
+
}
|
|
124
|
+
function forgetDashboard(email) {
|
|
125
|
+
return (0, config_1.clearDashboardSession)(email);
|
|
126
|
+
}
|
|
127
|
+
// --- the auth gate ---------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* The restricted-endpoint gate, the counterpart to session.ts's `requireApi()`.
|
|
130
|
+
* Deliberately separate: no `/cli/*` command ever depends on this credential,
|
|
131
|
+
* and no dashboard command depends on the `ck_` token.
|
|
132
|
+
*
|
|
133
|
+
* Returns BOTH tokens, because the two halves of the dashboard need different
|
|
134
|
+
* ones. `dashboardApi.ts` picks per route; callers should not choose.
|
|
135
|
+
*
|
|
136
|
+
* There is nothing to refresh — the tokens last 24h and the backend has no
|
|
137
|
+
* refresh endpoint (confirmed by capture). So this checks expiry locally and
|
|
138
|
+
* fails with the concrete next command, rather than pretending it can renew.
|
|
139
|
+
*/
|
|
140
|
+
function requireDashboard() {
|
|
141
|
+
const slug = (0, config_1.activeAccount)();
|
|
142
|
+
const email = slug ? (0, config_1.loadAccount)(slug).email : undefined;
|
|
143
|
+
const session = dashboardSession(email);
|
|
144
|
+
if (!session?.token) {
|
|
145
|
+
throw new Error(`No dashboard session for ${email || 'the account in effect'}.\n` +
|
|
146
|
+
' Dashboard endpoints (/auth/*, /v1/*, /kpi-manager, /pending-tasks) need a dashboard login, not a ck_ token.\n' +
|
|
147
|
+
' Run: cku dashboard login');
|
|
148
|
+
}
|
|
149
|
+
if (!session.sessionToken) {
|
|
150
|
+
// A session stored before the two-token split. Re-issuing is the only fix;
|
|
151
|
+
// the cookie token cannot be derived from the body token.
|
|
152
|
+
throw new Error(`The stored dashboard session for ${email} is incomplete — it predates the two-token fix.\n` +
|
|
153
|
+
' /auth/login returns a separate access_token cookie that the dashboard routes require.\n' +
|
|
154
|
+
' Run: cku dashboard login');
|
|
155
|
+
}
|
|
156
|
+
// Expiry is read off the cookie token: it is the one that guards every route
|
|
157
|
+
// except /v2/agents/, so it is the one that matters for a session's lifetime.
|
|
158
|
+
const left = msUntilExpiry(session.sessionToken);
|
|
159
|
+
if (left !== undefined && left <= 0) {
|
|
160
|
+
const when = new Date(Date.now() + left).toLocaleString('en-US');
|
|
161
|
+
throw new Error(`Dashboard session for ${email} expired ${when}.\n` +
|
|
162
|
+
' There is no refresh endpoint — the token lives 24h and must be re-issued.\n' +
|
|
163
|
+
' Run: cku dashboard login');
|
|
164
|
+
}
|
|
165
|
+
return { token: session.token, sessionToken: session.sessionToken, email: session.email };
|
|
166
|
+
}
|
|
167
|
+
/** Human phrasing for the expiry, used by `accounts` and `dashboard whoami`. */
|
|
168
|
+
function expiryLabel(token) {
|
|
169
|
+
const left = msUntilExpiry(token);
|
|
170
|
+
if (left === undefined)
|
|
171
|
+
return 'unknown';
|
|
172
|
+
if (left <= 0)
|
|
173
|
+
return 'expired';
|
|
174
|
+
const hours = left / 3_600_000;
|
|
175
|
+
if (hours < 1)
|
|
176
|
+
return `${Math.round(left / 60_000)}m`;
|
|
177
|
+
if (hours < 48)
|
|
178
|
+
return `${Math.round(hours)}h`;
|
|
179
|
+
return `${Math.round(hours / 24)}d`;
|
|
180
|
+
}
|
|
181
|
+
// --- commands --------------------------------------------------------------
|
|
182
|
+
/** `cku dashboard login` — the browser's email+password flow, minus the browser. */
|
|
183
|
+
async function dashboardLoginCommand(opts) {
|
|
184
|
+
try {
|
|
185
|
+
const answers = await (0, prompts_1.default)([
|
|
186
|
+
{ type: opts.email ? null : 'text', name: 'email', message: 'Email' },
|
|
187
|
+
{ type: 'password', name: 'password', message: 'Password' },
|
|
188
|
+
], { onCancel: () => process.exit(1) });
|
|
189
|
+
const email = opts.email || answers.email;
|
|
190
|
+
const password = answers.password;
|
|
191
|
+
if (!email || !password)
|
|
192
|
+
return (0, ui_1.handleError)(new Error('Email and password are required'));
|
|
193
|
+
const session = await dashboardLogin(email, password);
|
|
194
|
+
saveDashboard(email, session);
|
|
195
|
+
(0, ui_1.success)(`Dashboard session stored for ${session.email} (expires in ${expiryLabel(session.token)}).`);
|
|
196
|
+
(0, ui_1.info)(' Your password was not stored. The token cannot be refreshed, so you re-run');
|
|
197
|
+
(0, ui_1.info)(' this command once it expires.');
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
(0, ui_1.handleError)(err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** `cku dashboard whoami` — prove the stored session still works. */
|
|
204
|
+
async function dashboardWhoamiCommand(opts) {
|
|
205
|
+
try {
|
|
206
|
+
const { sessionToken, email } = requireDashboard();
|
|
207
|
+
const res = await fetch(`${(0, config_1.backendUrl)()}/auth/get-user`, {
|
|
208
|
+
headers: { Cookie: `access_token=${sessionToken}` },
|
|
209
|
+
});
|
|
210
|
+
const text = await res.text();
|
|
211
|
+
if (!res.ok)
|
|
212
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 160)}`);
|
|
213
|
+
const data = text ? JSON.parse(text) : {};
|
|
214
|
+
if (opts.json)
|
|
215
|
+
return void console.log(JSON.stringify(data, null, 2));
|
|
216
|
+
(0, ui_1.success)(`Dashboard session valid for ${data.email || email} (expires in ${expiryLabel(sessionToken)}).`);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
(0, ui_1.handleError)(err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* `cku dashboard probe` — GET each dashboard route and print status + shape.
|
|
224
|
+
* Read-only. Each row is tried with BOTH stored tokens, because the backend does
|
|
225
|
+
* not accept either one everywhere and the only way to know which is which is to
|
|
226
|
+
* see which one answers.
|
|
227
|
+
*/
|
|
228
|
+
async function dashboardProbeCommand() {
|
|
229
|
+
const PATHS = [
|
|
230
|
+
'/auth/get-user',
|
|
231
|
+
'/v2/agents/',
|
|
232
|
+
'/v1/call-analyzers',
|
|
233
|
+
'/v1/audit-reports',
|
|
234
|
+
'/v1/audit-reports/strategies',
|
|
235
|
+
'/kpi-manager',
|
|
236
|
+
'/pending-tasks',
|
|
237
|
+
'/feedback/users/team-members',
|
|
238
|
+
'/phone-number/',
|
|
239
|
+
];
|
|
240
|
+
try {
|
|
241
|
+
const { token, sessionToken, email } = requireDashboard();
|
|
242
|
+
const base = (0, config_1.backendUrl)();
|
|
243
|
+
const send = (path, which) => fetch(`${base}${path}`, {
|
|
244
|
+
headers: which === 'bearer' ? { Authorization: `Bearer ${token}` } : { Cookie: `access_token=${sessionToken}` },
|
|
245
|
+
});
|
|
246
|
+
(0, ui_1.info)(`${email} · expires in ${expiryLabel(sessionToken)} · ${base}\n`);
|
|
247
|
+
for (const path of PATHS) {
|
|
248
|
+
const results = [];
|
|
249
|
+
let shape = '';
|
|
250
|
+
for (const which of ['bearer', 'cookie']) {
|
|
251
|
+
let status = 'ERR';
|
|
252
|
+
try {
|
|
253
|
+
const res = await send(path, which);
|
|
254
|
+
status = res.status;
|
|
255
|
+
const text = await res.text();
|
|
256
|
+
try {
|
|
257
|
+
const d = JSON.parse(text);
|
|
258
|
+
const kind = Array.isArray(d) ? `array[${d.length}]` : typeof d;
|
|
259
|
+
const keys = d && typeof d === 'object' && !Array.isArray(d) ? Object.keys(d).slice(0, 6).join(',') : '';
|
|
260
|
+
shape = `${kind}${keys ? ` {${keys}}` : ''}`;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
shape = text.slice(0, 40).replace(/\s+/g, ' ');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
shape = e.message;
|
|
268
|
+
}
|
|
269
|
+
results.push(`${which}=${String(status).padEnd(4)}`);
|
|
270
|
+
}
|
|
271
|
+
// Only print the body shape when one of the two actually worked, so a
|
|
272
|
+
// rejected 401 doesn't masquerade as the route's shape.
|
|
273
|
+
const worked = results.some((r) => r.includes('200'));
|
|
274
|
+
(0, ui_1.info)(` ${path.padEnd(34)} ${results.join(' ')} ${worked ? shape : ''}`);
|
|
275
|
+
}
|
|
276
|
+
(0, ui_1.info)('\nAll calls above are GETs. Nothing was modified.');
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
(0, ui_1.handleError)(err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Load the server's own catalogues into `ENUMS` and the voice/transcriber
|
|
284
|
+
* provider tables, so validation and `cku voices` reflect what the backend
|
|
285
|
+
* actually serves instead of the built-in copies.
|
|
286
|
+
*
|
|
287
|
+
* Covers three tables, all of which had drifted from the server in both
|
|
288
|
+
* directions: LLM models per slot, TTS models per provider, and STT models per
|
|
289
|
+
* provider (including one provider, Groq, the server no longer lists at all).
|
|
290
|
+
*
|
|
291
|
+
* Best-effort by design: without a dashboard session, or if the call fails, the
|
|
292
|
+
* built-in lists stay in place and the command proceeds. A validation list is
|
|
293
|
+
* not worth failing an update over.
|
|
294
|
+
*
|
|
295
|
+
* Exported from here rather than from a command because `agents update`,
|
|
296
|
+
* `chat-agents update`, `voices` and `transcribers` all need it, and none should
|
|
297
|
+
* re-implement the fallback.
|
|
298
|
+
*/
|
|
299
|
+
async function refreshModelCatalog() {
|
|
300
|
+
const { dashboardApi } = require('./dashboardApi');
|
|
301
|
+
const { applyLiveModels } = require('./agentUpdate');
|
|
302
|
+
const { applyLiveCatalog } = require('./catalog');
|
|
303
|
+
try {
|
|
304
|
+
const res = await dashboardApi().getUser();
|
|
305
|
+
if (!res?.catalogs)
|
|
306
|
+
return false;
|
|
307
|
+
applyLiveCatalog(res.catalogs);
|
|
308
|
+
return applyLiveModels(res.catalogs);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false; // no dashboard session, or the route moved
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** `cku dashboard logout` — forget the dashboard session, keep the ck_ token. */
|
|
315
|
+
async function dashboardLogoutCommand(opts) {
|
|
316
|
+
try {
|
|
317
|
+
const slug = (0, config_1.activeAccount)();
|
|
318
|
+
const email = slug ? (0, config_1.loadAccount)(slug).email : undefined;
|
|
319
|
+
if (!hasDashboard(email))
|
|
320
|
+
return void (0, ui_1.info)('No dashboard session stored for this account.');
|
|
321
|
+
if (!opts.yes) {
|
|
322
|
+
const { ok } = await (0, prompts_1.default)({
|
|
323
|
+
type: 'confirm',
|
|
324
|
+
name: 'ok',
|
|
325
|
+
message: `Forget the dashboard session for ${email}? (your ck_ token is kept)`,
|
|
326
|
+
initial: false,
|
|
327
|
+
});
|
|
328
|
+
if (!ok)
|
|
329
|
+
return void (0, ui_1.info)('Cancelled.');
|
|
330
|
+
}
|
|
331
|
+
if (email && forgetDashboard(email))
|
|
332
|
+
(0, ui_1.success)(`Dashboard session removed for ${email}.`);
|
|
333
|
+
else
|
|
334
|
+
(0, ui_1.fail)('Nothing to remove.');
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
(0, ui_1.handleError)(err);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dbManagedFields = dbManagedFields;
|
|
4
|
+
exports.dbManagedFieldsError = dbManagedFieldsError;
|
|
5
|
+
/**
|
|
6
|
+
* Fields the database manages itself. A payload carrying them either 500s on
|
|
7
|
+
* the server (CastError on a bad _id) or silently lies about identity — so we
|
|
8
|
+
* reject them client-side with an explanation an LLM or human can act on.
|
|
9
|
+
*/
|
|
10
|
+
const DB_MANAGED = ['_id', '__v', 'createdAt', 'updatedAt', 'userId', 'agentId'];
|
|
11
|
+
/** Returns the offending keys found at the top level of a payload. */
|
|
12
|
+
function dbManagedFields(obj) {
|
|
13
|
+
if (!obj || typeof obj !== 'object')
|
|
14
|
+
return [];
|
|
15
|
+
return DB_MANAGED.filter((k) => k in obj);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Human/LLM-actionable error message for rejected payloads.
|
|
19
|
+
*
|
|
20
|
+
* Error text is written to be read by an LLM as well as a human — the CLI is
|
|
21
|
+
* driven by the installed skill pack, and an agent has to act on the message.
|
|
22
|
+
* Name the offending fields, explain why, give the concrete next command.
|
|
23
|
+
*/
|
|
24
|
+
function dbManagedFieldsError(found, where = 'payload') {
|
|
25
|
+
return (`The ${where} contains database-managed field(s): ${found.join(', ')}. ` +
|
|
26
|
+
`These are created by MongoDB/the server itself (_id, createdAt, updatedAt, __v) ` +
|
|
27
|
+
`or derived from your login (userId, agentId) — remove them and retry. ` +
|
|
28
|
+
`Tip: JSON from \`cku agents get --json\` includes them; strip before reuse, ` +
|
|
29
|
+
`or use \`cku agents export\` which is already sanitized.`);
|
|
30
|
+
}
|