@openbat/cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/api-client.d.mts +9 -0
- package/dist/api-client.d.ts +9 -0
- package/dist/api-client.js +21 -11
- package/dist/api-client.mjs +1 -1
- package/dist/{chunk-CRJZM45P.mjs → chunk-NYKJTHHK.mjs} +21 -11
- package/dist/index.js +712 -175
- package/dist/index.mjs +572 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
32
32
|
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
33
33
|
|
|
34
34
|
// src/index.ts
|
|
35
|
-
var
|
|
35
|
+
var import_commander8 = require("commander");
|
|
36
36
|
|
|
37
37
|
// src/commands/config.ts
|
|
38
38
|
var import_commander = require("commander");
|
|
@@ -92,7 +92,26 @@ async function resolveConfig(opts) {
|
|
|
92
92
|
baseUrl = file.baseUrl;
|
|
93
93
|
baseUrlSource = "file";
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
let activeChatbotId = null;
|
|
96
|
+
let activeChatbotIdSource = "missing";
|
|
97
|
+
if (opts.chatbotFlag) {
|
|
98
|
+
activeChatbotId = opts.chatbotFlag;
|
|
99
|
+
activeChatbotIdSource = "flag";
|
|
100
|
+
} else if (process.env.OPENBAT_CHATBOT_ID) {
|
|
101
|
+
activeChatbotId = process.env.OPENBAT_CHATBOT_ID;
|
|
102
|
+
activeChatbotIdSource = "env";
|
|
103
|
+
} else if (file.activeChatbotId) {
|
|
104
|
+
activeChatbotId = file.activeChatbotId;
|
|
105
|
+
activeChatbotIdSource = "file";
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
apiKey,
|
|
109
|
+
baseUrl,
|
|
110
|
+
activeChatbotId,
|
|
111
|
+
apiKeySource,
|
|
112
|
+
baseUrlSource,
|
|
113
|
+
activeChatbotIdSource
|
|
114
|
+
};
|
|
96
115
|
}
|
|
97
116
|
async function setApiKey(apiKey) {
|
|
98
117
|
if (apiKey.startsWith("ob_live_")) {
|
|
@@ -109,6 +128,25 @@ async function setApiKey(apiKey) {
|
|
|
109
128
|
file.apiKey = apiKey;
|
|
110
129
|
await writeConfig(file);
|
|
111
130
|
}
|
|
131
|
+
async function clearApiKey() {
|
|
132
|
+
const file = await readConfig();
|
|
133
|
+
delete file.apiKey;
|
|
134
|
+
delete file.activeChatbotId;
|
|
135
|
+
await writeConfig(file);
|
|
136
|
+
}
|
|
137
|
+
async function setActiveChatbotId(chatbotId) {
|
|
138
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatbotId)) {
|
|
139
|
+
throw new Error(`Not a UUID: ${chatbotId}`);
|
|
140
|
+
}
|
|
141
|
+
const file = await readConfig();
|
|
142
|
+
file.activeChatbotId = chatbotId;
|
|
143
|
+
await writeConfig(file);
|
|
144
|
+
}
|
|
145
|
+
async function clearActiveChatbotId() {
|
|
146
|
+
const file = await readConfig();
|
|
147
|
+
delete file.activeChatbotId;
|
|
148
|
+
await writeConfig(file);
|
|
149
|
+
}
|
|
112
150
|
async function setBaseUrl(baseUrl) {
|
|
113
151
|
try {
|
|
114
152
|
new URL(baseUrl);
|
|
@@ -123,6 +161,156 @@ function configPath() {
|
|
|
123
161
|
return CONFIG_PATH;
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
// src/api-client.ts
|
|
165
|
+
var import_node_url = require("url");
|
|
166
|
+
var CLI_VERSION = "0.1.1";
|
|
167
|
+
var KEY_REGEX = /ob_(?:live|read|admin|pat)_[0-9a-f]{32}/g;
|
|
168
|
+
function redact(s) {
|
|
169
|
+
return s.replace(KEY_REGEX, (k) => `${k.slice(0, 16)}\u2026<hidden>`);
|
|
170
|
+
}
|
|
171
|
+
function assertHttpsOrLocalhost(baseUrl) {
|
|
172
|
+
let url;
|
|
173
|
+
try {
|
|
174
|
+
url = new import_node_url.URL(baseUrl);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error(`Invalid base URL: ${redact(baseUrl)}`);
|
|
177
|
+
}
|
|
178
|
+
if (url.protocol === "https:") return;
|
|
179
|
+
if (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
throw new Error(
|
|
183
|
+
"Refusing to use a non-HTTPS base URL. localhost / 127.0.0.1 are allowed for dev."
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
var _apiKey, _clientLabel, _clientVersion, _ApiClient_instances, commonHeaders_fn, mutate_fn;
|
|
187
|
+
var ApiClient = class {
|
|
188
|
+
constructor(opts) {
|
|
189
|
+
__privateAdd(this, _ApiClient_instances);
|
|
190
|
+
__privateAdd(this, _apiKey);
|
|
191
|
+
__privateAdd(this, _clientLabel);
|
|
192
|
+
__privateAdd(this, _clientVersion);
|
|
193
|
+
assertHttpsOrLocalhost(opts.baseUrl);
|
|
194
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
195
|
+
__privateSet(this, _apiKey, opts.apiKey);
|
|
196
|
+
__privateSet(this, _clientLabel, opts.clientLabel ?? "cli");
|
|
197
|
+
__privateSet(this, _clientVersion, opts.clientVersion ?? CLI_VERSION);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Issue a GET against `path` (must begin with `/`). Returns the parsed
|
|
201
|
+
* JSON on 200. Throws an Error whose message is safe to print (key
|
|
202
|
+
* redacted, status included).
|
|
203
|
+
*/
|
|
204
|
+
async get(path) {
|
|
205
|
+
const url = `${this.baseUrl}${path}`;
|
|
206
|
+
let res;
|
|
207
|
+
try {
|
|
208
|
+
res = await fetch(url, {
|
|
209
|
+
method: "GET",
|
|
210
|
+
headers: __privateMethod(this, _ApiClient_instances, commonHeaders_fn).call(this, { accept: "application/json" })
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
214
|
+
throw new Error(`Request to ${redact(url)} failed: ${redact(msg)}`);
|
|
215
|
+
}
|
|
216
|
+
return parseResponse(res, url);
|
|
217
|
+
}
|
|
218
|
+
/** Issue a POST with a JSON body. */
|
|
219
|
+
async post(path, body) {
|
|
220
|
+
return __privateMethod(this, _ApiClient_instances, mutate_fn).call(this, "POST", path, body);
|
|
221
|
+
}
|
|
222
|
+
/** Issue a PATCH with a JSON body. */
|
|
223
|
+
async patch(path, body) {
|
|
224
|
+
return __privateMethod(this, _ApiClient_instances, mutate_fn).call(this, "PATCH", path, body);
|
|
225
|
+
}
|
|
226
|
+
/** Issue a DELETE. Body is rarely used; we still allow it. */
|
|
227
|
+
async delete(path, body) {
|
|
228
|
+
return __privateMethod(this, _ApiClient_instances, mutate_fn).call(this, "DELETE", path, body);
|
|
229
|
+
}
|
|
230
|
+
/** Pass-through for streaming endpoints (export). Returns the raw body. */
|
|
231
|
+
async getRaw(path) {
|
|
232
|
+
const url = `${this.baseUrl}${path}`;
|
|
233
|
+
const res = await fetch(url, {
|
|
234
|
+
method: "GET",
|
|
235
|
+
headers: __privateMethod(this, _ApiClient_instances, commonHeaders_fn).call(this)
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok || !res.body) {
|
|
238
|
+
const errText = await res.text().catch(() => "");
|
|
239
|
+
throw new Error(
|
|
240
|
+
`GET ${redact(url)} \u2192 ${res.status} ${res.statusText}${errText ? `: ${redact(errText.slice(0, 200))}` : ""}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
body: res.body,
|
|
245
|
+
contentType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
_apiKey = new WeakMap();
|
|
250
|
+
_clientLabel = new WeakMap();
|
|
251
|
+
_clientVersion = new WeakMap();
|
|
252
|
+
_ApiClient_instances = new WeakSet();
|
|
253
|
+
commonHeaders_fn = function(extra) {
|
|
254
|
+
const h = {
|
|
255
|
+
"x-openbat-key": __privateGet(this, _apiKey),
|
|
256
|
+
"x-openbat-client": __privateGet(this, _clientLabel),
|
|
257
|
+
...extra ?? {}
|
|
258
|
+
};
|
|
259
|
+
if (__privateGet(this, _clientVersion)) h["x-openbat-client-version"] = __privateGet(this, _clientVersion);
|
|
260
|
+
return h;
|
|
261
|
+
};
|
|
262
|
+
mutate_fn = async function(method, path, body) {
|
|
263
|
+
const url = `${this.baseUrl}${path}`;
|
|
264
|
+
let res;
|
|
265
|
+
try {
|
|
266
|
+
res = await fetch(url, {
|
|
267
|
+
method,
|
|
268
|
+
headers: __privateMethod(this, _ApiClient_instances, commonHeaders_fn).call(this, {
|
|
269
|
+
"content-type": "application/json",
|
|
270
|
+
accept: "application/json"
|
|
271
|
+
}),
|
|
272
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
276
|
+
throw new Error(`Request to ${redact(url)} failed: ${redact(msg)}`);
|
|
277
|
+
}
|
|
278
|
+
return parseResponse(res, url);
|
|
279
|
+
};
|
|
280
|
+
async function parseResponse(res, url) {
|
|
281
|
+
if (res.status === 401) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
"Unauthorized. The API key was rejected (invalid, wrong kind for this endpoint, expired, or revoked)."
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (res.status === 403) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
"Forbidden. The credential is valid but lacks permission for this operation (e.g. read-scope PAT can't mutate)."
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (res.status === 429) {
|
|
292
|
+
const retry = res.headers.get("retry-after");
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Rate limited.${retry ? ` Retry after ${retry}s.` : ""}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
let errBody = null;
|
|
299
|
+
try {
|
|
300
|
+
errBody = await res.json();
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
throw new Error(
|
|
304
|
+
`GET ${redact(url)} \u2192 ${res.status} ${res.statusText}${errBody?.error ? `: ${redact(errBody.error)}` : ""}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
return await res.json();
|
|
309
|
+
} catch {
|
|
310
|
+
throw new Error(`Response from ${redact(url)} was not valid JSON`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
126
314
|
// src/format.ts
|
|
127
315
|
var ANSI = {
|
|
128
316
|
dim: "\x1B[2m",
|
|
@@ -248,167 +436,131 @@ function configCommand() {
|
|
|
248
436
|
apiKeySource: cfg.apiKeySource,
|
|
249
437
|
baseUrl: cfg.baseUrl,
|
|
250
438
|
baseUrlSource: cfg.baseUrlSource,
|
|
439
|
+
activeChatbotId: cfg.activeChatbotId ?? "(not set)",
|
|
440
|
+
activeChatbotIdSource: cfg.activeChatbotIdSource,
|
|
251
441
|
configFile: configPath()
|
|
252
442
|
});
|
|
253
443
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// src/commands/data.ts
|
|
258
|
-
var import_commander2 = require("commander");
|
|
259
|
-
|
|
260
|
-
// src/api-client.ts
|
|
261
|
-
var import_node_url = require("url");
|
|
262
|
-
var KEY_REGEX = /ob_(?:live|read|admin|pat)_[0-9a-f]{32}/g;
|
|
263
|
-
function redact(s) {
|
|
264
|
-
return s.replace(KEY_REGEX, (k) => `${k.slice(0, 16)}\u2026<hidden>`);
|
|
265
|
-
}
|
|
266
|
-
function assertHttpsOrLocalhost(baseUrl) {
|
|
267
|
-
let url;
|
|
268
|
-
try {
|
|
269
|
-
url = new import_node_url.URL(baseUrl);
|
|
270
|
-
} catch {
|
|
271
|
-
throw new Error(`Invalid base URL: ${redact(baseUrl)}`);
|
|
272
|
-
}
|
|
273
|
-
if (url.protocol === "https:") return;
|
|
274
|
-
if (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
throw new Error(
|
|
278
|
-
"Refusing to use a non-HTTPS base URL. localhost / 127.0.0.1 are allowed for dev."
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
var _apiKey, _ApiClient_instances, mutate_fn;
|
|
282
|
-
var ApiClient = class {
|
|
283
|
-
constructor(opts) {
|
|
284
|
-
__privateAdd(this, _ApiClient_instances);
|
|
285
|
-
__privateAdd(this, _apiKey);
|
|
286
|
-
assertHttpsOrLocalhost(opts.baseUrl);
|
|
287
|
-
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
288
|
-
__privateSet(this, _apiKey, opts.apiKey);
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Issue a GET against `path` (must begin with `/`). Returns the parsed
|
|
292
|
-
* JSON on 200. Throws an Error whose message is safe to print (key
|
|
293
|
-
* redacted, status included).
|
|
294
|
-
*/
|
|
295
|
-
async get(path) {
|
|
296
|
-
const url = `${this.baseUrl}${path}`;
|
|
297
|
-
let res;
|
|
444
|
+
cmd.command("use-chatbot <id-or-name>").description(
|
|
445
|
+
"Persist a default chatbot for commands that need one (e.g. with a PAT spanning many chatbots)"
|
|
446
|
+
).action(async function(target) {
|
|
298
447
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
accept: "application/json"
|
|
304
|
-
}
|
|
305
|
-
});
|
|
448
|
+
const id = await resolveTargetChatbotId(target);
|
|
449
|
+
await setActiveChatbotId(id);
|
|
450
|
+
process.stdout.write(`Active chatbot saved: ${id}
|
|
451
|
+
`);
|
|
306
452
|
} catch (err) {
|
|
307
|
-
|
|
308
|
-
throw new Error(`Request to ${redact(url)} failed: ${redact(msg)}`);
|
|
453
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
309
454
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
455
|
+
});
|
|
456
|
+
cmd.command("clear-chatbot").description("Forget the persisted default chatbot").action(async () => {
|
|
457
|
+
await clearActiveChatbotId();
|
|
458
|
+
process.stdout.write("Cleared active chatbot.\n");
|
|
459
|
+
});
|
|
460
|
+
return cmd;
|
|
461
|
+
}
|
|
462
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
463
|
+
async function resolveTargetChatbotId(target) {
|
|
464
|
+
const cfg = await resolveConfig({});
|
|
465
|
+
if (!cfg.apiKey) {
|
|
466
|
+
fatal(
|
|
467
|
+
"No API key configured. Run `openbat config set-key` first so I can list your chatbots."
|
|
468
|
+
);
|
|
323
469
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (!res.ok || !res.body) {
|
|
334
|
-
const errText = await res.text().catch(() => "");
|
|
335
|
-
throw new Error(
|
|
336
|
-
`GET ${redact(url)} \u2192 ${res.status} ${res.statusText}${errText ? `: ${redact(errText.slice(0, 200))}` : ""}`
|
|
337
|
-
);
|
|
470
|
+
const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
471
|
+
const list = await api.get(
|
|
472
|
+
"/api/v1/chatbots"
|
|
473
|
+
);
|
|
474
|
+
const chatbots = list.chatbots ?? [];
|
|
475
|
+
if (UUID_RE.test(target)) {
|
|
476
|
+
const hit = chatbots.find((c) => c.id === target);
|
|
477
|
+
if (!hit) {
|
|
478
|
+
fatal(`Chatbot ${target} is not reachable by this API key.`);
|
|
338
479
|
}
|
|
339
|
-
return
|
|
340
|
-
body: res.body,
|
|
341
|
-
contentType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
_apiKey = new WeakMap();
|
|
346
|
-
_ApiClient_instances = new WeakSet();
|
|
347
|
-
mutate_fn = async function(method, path, body) {
|
|
348
|
-
const url = `${this.baseUrl}${path}`;
|
|
349
|
-
let res;
|
|
350
|
-
try {
|
|
351
|
-
res = await fetch(url, {
|
|
352
|
-
method,
|
|
353
|
-
headers: {
|
|
354
|
-
"x-openbat-key": __privateGet(this, _apiKey),
|
|
355
|
-
"content-type": "application/json",
|
|
356
|
-
accept: "application/json"
|
|
357
|
-
},
|
|
358
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
359
|
-
});
|
|
360
|
-
} catch (err) {
|
|
361
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
362
|
-
throw new Error(`Request to ${redact(url)} failed: ${redact(msg)}`);
|
|
480
|
+
return hit.id;
|
|
363
481
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
throw new Error(
|
|
369
|
-
"Unauthorized. The API key was rejected (invalid, wrong kind for this endpoint, expired, or revoked)."
|
|
370
|
-
);
|
|
482
|
+
const lower = target.toLowerCase();
|
|
483
|
+
const matches = chatbots.filter((c) => c.name.toLowerCase() === lower);
|
|
484
|
+
if (matches.length === 0) {
|
|
485
|
+
fatal(`No chatbot named "${target}". Run \`openbat chatbot list\`.`);
|
|
371
486
|
}
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
if (res.status === 429) {
|
|
378
|
-
const retry = res.headers.get("retry-after");
|
|
379
|
-
throw new Error(
|
|
380
|
-
`Rate limited.${retry ? ` Retry after ${retry}s.` : ""}`
|
|
487
|
+
if (matches.length > 1) {
|
|
488
|
+
fatal(
|
|
489
|
+
`Multiple chatbots named "${target}". Use the UUID \u2014 list them with \`openbat chatbot list\`.`
|
|
381
490
|
);
|
|
382
491
|
}
|
|
383
|
-
|
|
384
|
-
|
|
492
|
+
return matches[0].id;
|
|
493
|
+
}
|
|
494
|
+
function useCommand() {
|
|
495
|
+
return new import_commander.Command("use").argument("<id-or-name>", "Chatbot UUID or exact name").description("Set the default chatbot for this CLI (shortcut for `config use-chatbot`)").action(async (target) => {
|
|
385
496
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
497
|
+
const id = await resolveTargetChatbotId(target);
|
|
498
|
+
await setActiveChatbotId(id);
|
|
499
|
+
process.stdout.write(`Active chatbot saved: ${id}
|
|
500
|
+
`);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
388
503
|
}
|
|
389
|
-
|
|
390
|
-
`GET ${redact(url)} \u2192 ${res.status} ${res.statusText}${errBody?.error ? `: ${redact(errBody.error)}` : ""}`
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
try {
|
|
394
|
-
return await res.json();
|
|
395
|
-
} catch {
|
|
396
|
-
throw new Error(`Response from ${redact(url)} was not valid JSON`);
|
|
397
|
-
}
|
|
504
|
+
});
|
|
398
505
|
}
|
|
399
506
|
|
|
400
507
|
// src/commands/data.ts
|
|
508
|
+
var import_commander2 = require("commander");
|
|
401
509
|
async function client(globals) {
|
|
402
510
|
const cfg = await resolveConfig({
|
|
403
511
|
apiKeyFlag: globals.apiKey ?? null,
|
|
404
|
-
baseUrlFlag: globals.baseUrl ?? null
|
|
512
|
+
baseUrlFlag: globals.baseUrl ?? null,
|
|
513
|
+
chatbotFlag: globals.chatbot ?? null
|
|
405
514
|
});
|
|
406
515
|
if (!cfg.apiKey) {
|
|
407
516
|
fatal(
|
|
408
517
|
"No API key configured. Run `openbat config set-key`, or pass --api-key, or set OPENBAT_API_KEY."
|
|
409
518
|
);
|
|
410
519
|
}
|
|
411
|
-
return
|
|
520
|
+
return {
|
|
521
|
+
api: new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl }),
|
|
522
|
+
chatbotFlag: cfg.activeChatbotId
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
526
|
+
async function resolveChatbotId(api, chatbotFlag) {
|
|
527
|
+
const list = await api.get(
|
|
528
|
+
"/api/v1/chatbots"
|
|
529
|
+
);
|
|
530
|
+
const chatbots = list.chatbots ?? [];
|
|
531
|
+
if (chatbots.length === 0) {
|
|
532
|
+
fatal("This API key authorizes no chatbots.");
|
|
533
|
+
}
|
|
534
|
+
if (chatbotFlag) {
|
|
535
|
+
if (UUID_RE2.test(chatbotFlag)) {
|
|
536
|
+
const hit = chatbots.find((c) => c.id === chatbotFlag);
|
|
537
|
+
if (!hit) {
|
|
538
|
+
fatal(
|
|
539
|
+
`Chatbot ${chatbotFlag} is not reachable by this API key. Run \`openbat chatbot list\` to see what is.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return hit.id;
|
|
543
|
+
}
|
|
544
|
+
const lower = chatbotFlag.toLowerCase();
|
|
545
|
+
const matches = chatbots.filter((c) => c.name.toLowerCase() === lower);
|
|
546
|
+
if (matches.length === 0) {
|
|
547
|
+
fatal(
|
|
548
|
+
`No chatbot named "${chatbotFlag}". Run \`openbat chatbot list\` to see what's reachable.`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
if (matches.length > 1) {
|
|
552
|
+
fatal(
|
|
553
|
+
`Multiple chatbots named "${chatbotFlag}". Use the UUID instead \u2014 list them with \`openbat chatbot list\`.`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return matches[0].id;
|
|
557
|
+
}
|
|
558
|
+
if (chatbots.length === 1) return chatbots[0].id;
|
|
559
|
+
const lines = chatbots.map((c) => ` \u2022 ${c.name} ${c.id}`).join("\n");
|
|
560
|
+
fatal(
|
|
561
|
+
`This API key targets multiple chatbots. Pick one with \`openbat use <id>\` (persistent) or \`--chatbot <id>\` (one-off):
|
|
562
|
+
${lines}`
|
|
563
|
+
);
|
|
412
564
|
}
|
|
413
565
|
function formatOpts(cmd) {
|
|
414
566
|
const opts = cmd.optsWithGlobals();
|
|
@@ -420,12 +572,26 @@ function chatbotCommand() {
|
|
|
420
572
|
);
|
|
421
573
|
cmd.command("info").description("Show the chatbot the current API key authorizes").action(async function() {
|
|
422
574
|
try {
|
|
423
|
-
const
|
|
424
|
-
const
|
|
575
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
576
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
577
|
+
const result = await api.get(
|
|
578
|
+
"/api/v1/chatbots"
|
|
579
|
+
);
|
|
580
|
+
const row = (result.chatbots ?? []).find(
|
|
581
|
+
(c) => c.id === id
|
|
582
|
+
) ?? null;
|
|
583
|
+
emit(row, formatOpts(this));
|
|
584
|
+
} catch (err) {
|
|
585
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
cmd.command("list").description("List every chatbot this API key authorizes").action(async function() {
|
|
589
|
+
try {
|
|
590
|
+
const { api } = await client(this.optsWithGlobals());
|
|
591
|
+
const result = await api.get(
|
|
425
592
|
"/api/v1/chatbots"
|
|
426
593
|
);
|
|
427
|
-
|
|
428
|
-
emit(list[0] ?? null, formatOpts(this));
|
|
594
|
+
emit(result.chatbots ?? [], formatOpts(this));
|
|
429
595
|
} catch (err) {
|
|
430
596
|
fatal(err instanceof Error ? err.message : String(err));
|
|
431
597
|
}
|
|
@@ -438,12 +604,14 @@ function conversationsCommand() {
|
|
|
438
604
|
);
|
|
439
605
|
cmd.command("list").description("Page through recent conversations").option("--page <n>", "Page number", "1").option("--limit <n>", "Page size (max 100)", "20").action(async function(opts) {
|
|
440
606
|
try {
|
|
441
|
-
const
|
|
607
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
608
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
442
609
|
const params = new URLSearchParams({
|
|
610
|
+
chatbotId: id,
|
|
443
611
|
page: opts.page,
|
|
444
612
|
limit: opts.limit
|
|
445
613
|
});
|
|
446
|
-
const result = await
|
|
614
|
+
const result = await api.get(`/api/v1/conversations?${params}`);
|
|
447
615
|
if (this.optsWithGlobals().json) {
|
|
448
616
|
emit(result, { json: true });
|
|
449
617
|
} else {
|
|
@@ -458,11 +626,13 @@ Total: ${result.total}, page ${opts.page}, limit ${opts.limit}
|
|
|
458
626
|
fatal(err instanceof Error ? err.message : String(err));
|
|
459
627
|
}
|
|
460
628
|
});
|
|
461
|
-
cmd.command("show <id>").description("Show one conversation by id, with messages").action(async function(
|
|
629
|
+
cmd.command("show <id>").description("Show one conversation by id, with messages").action(async function(convId) {
|
|
462
630
|
try {
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
631
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
632
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
633
|
+
const params = new URLSearchParams({ chatbotId: id });
|
|
634
|
+
const result = await api.get(
|
|
635
|
+
`/api/v1/conversations/${encodeURIComponent(convId)}?${params}`
|
|
466
636
|
);
|
|
467
637
|
emit(result.conversation, formatOpts(this));
|
|
468
638
|
} catch (err) {
|
|
@@ -473,13 +643,15 @@ Total: ${result.total}, page ${opts.page}, limit ${opts.limit}
|
|
|
473
643
|
}
|
|
474
644
|
function analyticsCommand() {
|
|
475
645
|
const cmd = new import_commander2.Command("analytics").description(
|
|
476
|
-
"Aggregated analytics for the
|
|
646
|
+
"Aggregated analytics for the active chatbot"
|
|
477
647
|
);
|
|
478
648
|
cmd.command("overview").description("Total conversations, messages, sentiment distribution").action(async function() {
|
|
479
649
|
try {
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
|
|
650
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
651
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
652
|
+
const params = new URLSearchParams({ chatbotId: id });
|
|
653
|
+
const result = await api.get(
|
|
654
|
+
`/api/v1/analytics/overview?${params}`
|
|
483
655
|
);
|
|
484
656
|
emit(result, formatOpts(this));
|
|
485
657
|
} catch (err) {
|
|
@@ -488,9 +660,10 @@ function analyticsCommand() {
|
|
|
488
660
|
});
|
|
489
661
|
cmd.command("sentiment").description("Sentiment over time (default last 30 days)").option("--days <n>", "Look-back window in days", "30").action(async function(opts) {
|
|
490
662
|
try {
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
const
|
|
663
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
664
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
665
|
+
const params = new URLSearchParams({ chatbotId: id, days: opts.days });
|
|
666
|
+
const result = await api.get(
|
|
494
667
|
`/api/v1/analytics/sentiment?${params}`
|
|
495
668
|
);
|
|
496
669
|
emit(result, formatOpts(this));
|
|
@@ -501,16 +674,12 @@ function analyticsCommand() {
|
|
|
501
674
|
return cmd;
|
|
502
675
|
}
|
|
503
676
|
function exportCommand() {
|
|
504
|
-
const cmd = new import_commander2.Command("export").description("Dump all conversation data for the
|
|
677
|
+
const cmd = new import_commander2.Command("export").description("Dump all conversation data for the active chatbot").option("--format <fmt>", "json or csv", "json").option("--out <path>", "Write to file (defaults to stdout)").action(async function(opts) {
|
|
505
678
|
try {
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
"/api/v1/chatbots"
|
|
509
|
-
);
|
|
510
|
-
const id = info.chatbots[0]?.id;
|
|
511
|
-
if (!id) fatal("Could not resolve chatbot from API key.");
|
|
679
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
680
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
512
681
|
const format = opts.format === "csv" ? "csv" : "json";
|
|
513
|
-
const { body } = await
|
|
682
|
+
const { body } = await api.getRaw(
|
|
514
683
|
`/api/v1/export/${id}?format=${format}`
|
|
515
684
|
);
|
|
516
685
|
if (opts.out) {
|
|
@@ -622,8 +791,370 @@ function authCommand() {
|
|
|
622
791
|
return cmd;
|
|
623
792
|
}
|
|
624
793
|
|
|
625
|
-
// src/commands/
|
|
794
|
+
// src/commands/login.ts
|
|
626
795
|
var import_commander4 = require("commander");
|
|
796
|
+
var import_node_os2 = __toESM(require("os"));
|
|
797
|
+
var import_node_readline = __toESM(require("readline"));
|
|
798
|
+
|
|
799
|
+
// src/browser.ts
|
|
800
|
+
var import_node_child_process = require("child_process");
|
|
801
|
+
function openBrowser(url) {
|
|
802
|
+
try {
|
|
803
|
+
if (process.platform === "darwin") {
|
|
804
|
+
(0, import_node_child_process.spawn)("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
if (process.platform === "win32") {
|
|
808
|
+
(0, import_node_child_process.spawn)("cmd", ["/c", "start", "", url], {
|
|
809
|
+
detached: true,
|
|
810
|
+
stdio: "ignore",
|
|
811
|
+
windowsVerbatimArguments: true
|
|
812
|
+
}).unref();
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
(0, import_node_child_process.spawn)("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
816
|
+
return true;
|
|
817
|
+
} catch {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/pkce.ts
|
|
823
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
824
|
+
function generateVerifier() {
|
|
825
|
+
return import_node_crypto.default.randomBytes(32).toString("hex");
|
|
826
|
+
}
|
|
827
|
+
function challengeFor(verifier) {
|
|
828
|
+
return import_node_crypto.default.createHash("sha256").update(verifier).digest("hex");
|
|
829
|
+
}
|
|
830
|
+
function randomState() {
|
|
831
|
+
return import_node_crypto.default.randomBytes(32).toString("hex");
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/loopback.ts
|
|
835
|
+
var import_node_http = __toESM(require("http"));
|
|
836
|
+
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
837
|
+
var SUCCESS_HTML = `<!doctype html>
|
|
838
|
+
<html lang="en">
|
|
839
|
+
<head>
|
|
840
|
+
<meta charset="utf-8" />
|
|
841
|
+
<title>OpenBat CLI \u2014 signed in</title>
|
|
842
|
+
<style>
|
|
843
|
+
body {
|
|
844
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
845
|
+
background: #0a0a0a;
|
|
846
|
+
color: #fafafa;
|
|
847
|
+
margin: 0;
|
|
848
|
+
min-height: 100vh;
|
|
849
|
+
display: flex;
|
|
850
|
+
align-items: center;
|
|
851
|
+
justify-content: center;
|
|
852
|
+
}
|
|
853
|
+
.card {
|
|
854
|
+
max-width: 420px;
|
|
855
|
+
padding: 32px;
|
|
856
|
+
border-radius: 14px;
|
|
857
|
+
background: #141414;
|
|
858
|
+
border: 1px solid #262626;
|
|
859
|
+
text-align: center;
|
|
860
|
+
}
|
|
861
|
+
h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; }
|
|
862
|
+
p { margin: 0; color: #a3a3a3; font-size: 14px; line-height: 1.5; }
|
|
863
|
+
</style>
|
|
864
|
+
</head>
|
|
865
|
+
<body>
|
|
866
|
+
<div class="card">
|
|
867
|
+
<h1>Signed in</h1>
|
|
868
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
869
|
+
</div>
|
|
870
|
+
</body>
|
|
871
|
+
</html>`;
|
|
872
|
+
var ERROR_HTML = (msg) => `<!doctype html>
|
|
873
|
+
<html><body style="font-family:system-ui;background:#0a0a0a;color:#fafafa;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center">
|
|
874
|
+
<div style="max-width:420px;padding:32px;text-align:center">
|
|
875
|
+
<h1 style="font-size:18px;margin:0 0 8px">Login failed</h1>
|
|
876
|
+
<p style="margin:0;color:#a3a3a3;font-size:14px">${escapeHtml(msg)}</p>
|
|
877
|
+
</div></body></html>`;
|
|
878
|
+
function escapeHtml(s) {
|
|
879
|
+
return s.replace(
|
|
880
|
+
/[&<>"]/g,
|
|
881
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
async function runLoopbackServer(opts) {
|
|
885
|
+
let resolve = null;
|
|
886
|
+
let reject = null;
|
|
887
|
+
const callback = new Promise((res, rej) => {
|
|
888
|
+
resolve = res;
|
|
889
|
+
reject = rej;
|
|
890
|
+
});
|
|
891
|
+
const server = import_node_http.default.createServer((req, res) => {
|
|
892
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
893
|
+
if (url.pathname !== "/cb") {
|
|
894
|
+
res.writeHead(404).end();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const code = url.searchParams.get("code");
|
|
898
|
+
const state = url.searchParams.get("state");
|
|
899
|
+
if (!code || !state) {
|
|
900
|
+
res.writeHead(400, { "content-type": "text/html" }).end(ERROR_HTML("Missing code or state"));
|
|
901
|
+
reject?.(new Error("Missing code or state in callback"));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (state !== opts.expectedState) {
|
|
905
|
+
res.writeHead(400, { "content-type": "text/html" }).end(ERROR_HTML("State mismatch \u2014 aborting for safety"));
|
|
906
|
+
reject?.(new Error("State mismatch \u2014 aborting"));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
|
|
910
|
+
resolve?.({ code });
|
|
911
|
+
setTimeout(() => server.close(), 100);
|
|
912
|
+
});
|
|
913
|
+
await new Promise((res) => server.listen(0, "127.0.0.1", () => res()));
|
|
914
|
+
const port = server.address().port;
|
|
915
|
+
const timer = setTimeout(() => {
|
|
916
|
+
server.close();
|
|
917
|
+
reject?.(new Error("Login timed out after 5 minutes."));
|
|
918
|
+
}, TIMEOUT_MS);
|
|
919
|
+
return {
|
|
920
|
+
port,
|
|
921
|
+
waitForCallback: async () => {
|
|
922
|
+
try {
|
|
923
|
+
const result = await callback;
|
|
924
|
+
clearTimeout(timer);
|
|
925
|
+
return result;
|
|
926
|
+
} catch (err) {
|
|
927
|
+
clearTimeout(timer);
|
|
928
|
+
server.close();
|
|
929
|
+
throw err;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/commands/login.ts
|
|
936
|
+
var HOSTNAME = import_node_os2.default.hostname();
|
|
937
|
+
function loginCommand() {
|
|
938
|
+
return new import_commander4.Command("login").description("Sign in via browser and install an API key on this device").option("--device", "Use the device-code flow (for SSH / headless machines)").option(
|
|
939
|
+
"--use <key>",
|
|
940
|
+
"Skip the browser; install a PAT plaintext you already have"
|
|
941
|
+
).action(async function(opts) {
|
|
942
|
+
try {
|
|
943
|
+
const globals = this.optsWithGlobals();
|
|
944
|
+
const cfg = await resolveConfig({
|
|
945
|
+
apiKeyFlag: null,
|
|
946
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
947
|
+
});
|
|
948
|
+
let token;
|
|
949
|
+
if (opts.use) {
|
|
950
|
+
token = opts.use.trim();
|
|
951
|
+
} else if (opts.device) {
|
|
952
|
+
token = await deviceFlow(cfg.baseUrl);
|
|
953
|
+
} else {
|
|
954
|
+
token = await loopbackFlow(cfg.baseUrl);
|
|
955
|
+
}
|
|
956
|
+
await setApiKey(token);
|
|
957
|
+
process.stdout.write(`Signed in. API key saved to ~/.openbatrc.
|
|
958
|
+
`);
|
|
959
|
+
await pickDefaultChatbot(token, cfg.baseUrl);
|
|
960
|
+
} catch (err) {
|
|
961
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
async function loopbackFlow(baseUrl) {
|
|
966
|
+
const verifier = generateVerifier();
|
|
967
|
+
const challenge = challengeFor(verifier);
|
|
968
|
+
const state = randomState();
|
|
969
|
+
const { port, waitForCallback } = await runLoopbackServer({
|
|
970
|
+
expectedState: state
|
|
971
|
+
});
|
|
972
|
+
const redirectUri = `http://127.0.0.1:${port}/cb`;
|
|
973
|
+
const authorizeUrl = new URL("/platform/cli/authorize", baseUrl);
|
|
974
|
+
authorizeUrl.searchParams.set("state", state);
|
|
975
|
+
authorizeUrl.searchParams.set("challenge", challenge);
|
|
976
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
977
|
+
authorizeUrl.searchParams.set("hostname", HOSTNAME);
|
|
978
|
+
const opened = openBrowser(authorizeUrl.toString());
|
|
979
|
+
if (!opened) {
|
|
980
|
+
process.stderr.write(`
|
|
981
|
+
Couldn't open a browser. Visit this URL to continue:
|
|
982
|
+
${authorizeUrl}
|
|
983
|
+
|
|
984
|
+
`);
|
|
985
|
+
} else {
|
|
986
|
+
process.stderr.write(`
|
|
987
|
+
Opening your browser\u2026 (waiting on http://127.0.0.1:${port})
|
|
988
|
+
`);
|
|
989
|
+
process.stderr.write(`If it didn't open, visit: ${authorizeUrl}
|
|
990
|
+
|
|
991
|
+
`);
|
|
992
|
+
}
|
|
993
|
+
const { code } = await waitForCallback();
|
|
994
|
+
const exchangeRes = await fetch(new URL("/api/cli/exchange", baseUrl), {
|
|
995
|
+
method: "POST",
|
|
996
|
+
headers: { "content-type": "application/json" },
|
|
997
|
+
body: JSON.stringify({ code, verifier })
|
|
998
|
+
});
|
|
999
|
+
if (!exchangeRes.ok) {
|
|
1000
|
+
const text = await exchangeRes.text();
|
|
1001
|
+
throw new Error(`Exchange failed (${exchangeRes.status}): ${text}`);
|
|
1002
|
+
}
|
|
1003
|
+
const exchange = await exchangeRes.json();
|
|
1004
|
+
if (!exchange.ok || !exchange.token) {
|
|
1005
|
+
throw new Error("Exchange response missing token.");
|
|
1006
|
+
}
|
|
1007
|
+
return exchange.token;
|
|
1008
|
+
}
|
|
1009
|
+
async function deviceFlow(baseUrl) {
|
|
1010
|
+
const startRes = await fetch(new URL("/api/cli/device/start", baseUrl), {
|
|
1011
|
+
method: "POST",
|
|
1012
|
+
headers: { "content-type": "application/json" },
|
|
1013
|
+
body: JSON.stringify({ hostname: HOSTNAME })
|
|
1014
|
+
});
|
|
1015
|
+
if (!startRes.ok) {
|
|
1016
|
+
const text = await startRes.text();
|
|
1017
|
+
throw new Error(`Device flow start failed (${startRes.status}): ${text}`);
|
|
1018
|
+
}
|
|
1019
|
+
const start = await startRes.json();
|
|
1020
|
+
process.stdout.write(`
|
|
1021
|
+
On any device, visit:
|
|
1022
|
+
${start.verification_uri}
|
|
1023
|
+
|
|
1024
|
+
`);
|
|
1025
|
+
process.stdout.write(`and enter this code:
|
|
1026
|
+
${start.user_code}
|
|
1027
|
+
|
|
1028
|
+
`);
|
|
1029
|
+
process.stdout.write(`Or open directly:
|
|
1030
|
+
${start.verification_uri_complete}
|
|
1031
|
+
|
|
1032
|
+
`);
|
|
1033
|
+
process.stderr.write("Waiting for authorization\u2026\n");
|
|
1034
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
1035
|
+
const intervalMs = Math.max(start.interval, 5) * 1e3;
|
|
1036
|
+
while (Date.now() < deadline) {
|
|
1037
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1038
|
+
const pollRes = await fetch(new URL("/api/cli/device/poll", baseUrl), {
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
headers: { "content-type": "application/json" },
|
|
1041
|
+
body: JSON.stringify({ device_code: start.device_code })
|
|
1042
|
+
});
|
|
1043
|
+
if (pollRes.status === 429) continue;
|
|
1044
|
+
if (!pollRes.ok) {
|
|
1045
|
+
const text = await pollRes.text();
|
|
1046
|
+
throw new Error(`Poll failed (${pollRes.status}): ${text}`);
|
|
1047
|
+
}
|
|
1048
|
+
const poll = await pollRes.json();
|
|
1049
|
+
if (poll.status === "pending") continue;
|
|
1050
|
+
if (poll.status === "expired") throw new Error("Code expired. Run `openbat login --device` again.");
|
|
1051
|
+
if (poll.status === "denied") throw new Error("Authorization denied.");
|
|
1052
|
+
if (poll.status === "unknown") throw new Error("Server lost the request. Try again.");
|
|
1053
|
+
if (poll.status === "authorized") {
|
|
1054
|
+
if (!poll.token) throw new Error("Server authorized but didn't return a token.");
|
|
1055
|
+
return poll.token;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
throw new Error("Timed out waiting for authorization.");
|
|
1059
|
+
}
|
|
1060
|
+
async function pickDefaultChatbot(token, baseUrl) {
|
|
1061
|
+
const api = new ApiClient({ apiKey: token, baseUrl });
|
|
1062
|
+
let chatbots;
|
|
1063
|
+
try {
|
|
1064
|
+
const result = await api.get(
|
|
1065
|
+
"/api/v1/chatbots"
|
|
1066
|
+
);
|
|
1067
|
+
chatbots = result.chatbots ?? [];
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
process.stderr.write(
|
|
1070
|
+
`
|
|
1071
|
+
(Couldn't list chatbots: ${err instanceof Error ? err.message : String(err)})
|
|
1072
|
+
`
|
|
1073
|
+
);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (chatbots.length === 0) {
|
|
1077
|
+
process.stdout.write("\nNo chatbots reachable yet. Create one from the dashboard.\n");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (chatbots.length === 1) {
|
|
1081
|
+
await setActiveChatbotId(chatbots[0].id);
|
|
1082
|
+
process.stdout.write(`Default chatbot: ${chatbots[0].name}
|
|
1083
|
+
`);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (!process.stdin.isTTY) {
|
|
1087
|
+
process.stdout.write(
|
|
1088
|
+
`
|
|
1089
|
+
You have ${chatbots.length} chatbots. Pick one later with \`openbat use <id>\`.
|
|
1090
|
+
`
|
|
1091
|
+
);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
process.stdout.write("\nYou have access to multiple chatbots. Pick a default:\n");
|
|
1095
|
+
chatbots.forEach((c, i) => {
|
|
1096
|
+
process.stdout.write(` ${i + 1}. ${c.name} ${c.id}
|
|
1097
|
+
`);
|
|
1098
|
+
});
|
|
1099
|
+
process.stdout.write(` ${chatbots.length + 1}. Skip \u2014 pick later with \`openbat use\`
|
|
1100
|
+
`);
|
|
1101
|
+
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
1102
|
+
const answer = await new Promise(
|
|
1103
|
+
(res) => rl.question("Choice: ", (input) => {
|
|
1104
|
+
rl.close();
|
|
1105
|
+
res(input.trim());
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
const idx = Number(answer);
|
|
1109
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > chatbots.length + 1) {
|
|
1110
|
+
process.stdout.write("(Invalid choice \u2014 skipping.)\n");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (idx === chatbots.length + 1) return;
|
|
1114
|
+
await setActiveChatbotId(chatbots[idx - 1].id);
|
|
1115
|
+
process.stdout.write(`Default chatbot: ${chatbots[idx - 1].name}
|
|
1116
|
+
`);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/commands/logout.ts
|
|
1120
|
+
var import_commander5 = require("commander");
|
|
1121
|
+
function logoutCommand() {
|
|
1122
|
+
return new import_commander5.Command("logout").description("Revoke the stored API key and clear ~/.openbatrc").action(async function() {
|
|
1123
|
+
try {
|
|
1124
|
+
const globals = this.optsWithGlobals();
|
|
1125
|
+
const cfg = await resolveConfig({
|
|
1126
|
+
apiKeyFlag: null,
|
|
1127
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
1128
|
+
});
|
|
1129
|
+
if (!cfg.apiKey) {
|
|
1130
|
+
process.stdout.write("Not signed in.\n");
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (cfg.apiKey.startsWith("ob_pat_")) {
|
|
1134
|
+
const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
1135
|
+
try {
|
|
1136
|
+
await api.post("/api/v1/me/pats/self/revoke", {});
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
process.stderr.write(
|
|
1139
|
+
`Warning: server-side revoke failed (${err instanceof Error ? err.message : err}). Clearing local config anyway.
|
|
1140
|
+
`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
} else {
|
|
1144
|
+
process.stderr.write(
|
|
1145
|
+
"Note: read/admin keys can't self-revoke. Visit the dashboard to revoke; this command will just clear the local config.\n"
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
await clearApiKey();
|
|
1149
|
+
process.stdout.write("Signed out.\n");
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/commands/org.ts
|
|
1157
|
+
var import_commander6 = require("commander");
|
|
627
1158
|
async function client3(globals) {
|
|
628
1159
|
const cfg = await resolveConfig({
|
|
629
1160
|
apiKeyFlag: globals.apiKey ?? null,
|
|
@@ -638,7 +1169,7 @@ async function client3(globals) {
|
|
|
638
1169
|
return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
639
1170
|
}
|
|
640
1171
|
function orgCommand() {
|
|
641
|
-
const cmd = new
|
|
1172
|
+
const cmd = new import_commander6.Command("org").description(
|
|
642
1173
|
"Manage the OpenBat tenant org (rename, members, invitations). Requires PAT."
|
|
643
1174
|
);
|
|
644
1175
|
cmd.command("list").description("List orgs the current PAT's user belongs to").action(async function() {
|
|
@@ -746,7 +1277,7 @@ function orgCommand() {
|
|
|
746
1277
|
}
|
|
747
1278
|
|
|
748
1279
|
// src/commands/write.ts
|
|
749
|
-
var
|
|
1280
|
+
var import_commander7 = require("commander");
|
|
750
1281
|
async function client4(globals) {
|
|
751
1282
|
const cfg = await resolveConfig({
|
|
752
1283
|
apiKeyFlag: globals.apiKey ?? null,
|
|
@@ -770,7 +1301,7 @@ function surfacePlaintext(plaintext, label) {
|
|
|
770
1301
|
);
|
|
771
1302
|
}
|
|
772
1303
|
function chatbotsCommand() {
|
|
773
|
-
const cmd = new
|
|
1304
|
+
const cmd = new import_commander7.Command("chatbots").description(
|
|
774
1305
|
"List, create, delete chatbots in the current scope"
|
|
775
1306
|
);
|
|
776
1307
|
cmd.command("list").description("List every chatbot the credential can reach").action(async function() {
|
|
@@ -818,7 +1349,7 @@ function chatbotsCommand() {
|
|
|
818
1349
|
return cmd;
|
|
819
1350
|
}
|
|
820
1351
|
function webhooksCommand() {
|
|
821
|
-
const cmd = new
|
|
1352
|
+
const cmd = new import_commander7.Command("webhooks").description("Manage webhooks for a chatbot");
|
|
822
1353
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
823
1354
|
try {
|
|
824
1355
|
const globals = this.optsWithGlobals();
|
|
@@ -859,7 +1390,7 @@ function webhooksCommand() {
|
|
|
859
1390
|
return cmd;
|
|
860
1391
|
}
|
|
861
1392
|
function settingsCommand() {
|
|
862
|
-
const cmd = new
|
|
1393
|
+
const cmd = new import_commander7.Command("settings").description(
|
|
863
1394
|
"Manage chatbot settings + per-chatbot keys"
|
|
864
1395
|
);
|
|
865
1396
|
const keys = cmd.command("keys").description("Manage API keys for a chatbot");
|
|
@@ -947,7 +1478,7 @@ function settingsCommand() {
|
|
|
947
1478
|
return cmd;
|
|
948
1479
|
}
|
|
949
1480
|
function workflowsCommand() {
|
|
950
|
-
const cmd = new
|
|
1481
|
+
const cmd = new import_commander7.Command("workflows").description("Manage workflows");
|
|
951
1482
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
952
1483
|
try {
|
|
953
1484
|
const globals = this.optsWithGlobals();
|
|
@@ -985,7 +1516,7 @@ function workflowsCommand() {
|
|
|
985
1516
|
return cmd;
|
|
986
1517
|
}
|
|
987
1518
|
function reportsCommand() {
|
|
988
|
-
const cmd = new
|
|
1519
|
+
const cmd = new import_commander7.Command("reports").description("Manage AI reports");
|
|
989
1520
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
990
1521
|
try {
|
|
991
1522
|
const globals = this.optsWithGlobals();
|
|
@@ -1021,7 +1552,7 @@ Created report. View it (org members only):
|
|
|
1021
1552
|
return cmd;
|
|
1022
1553
|
}
|
|
1023
1554
|
function analysisCommand() {
|
|
1024
|
-
const cmd = new
|
|
1555
|
+
const cmd = new import_commander7.Command("analysis").description("Manage analysis definitions");
|
|
1025
1556
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").option("--type <t>", "intent | flag | assistant_outcome | assistant_issue").option("--pending", "Include pending suggestions").action(async function(opts) {
|
|
1026
1557
|
try {
|
|
1027
1558
|
const globals = this.optsWithGlobals();
|
|
@@ -1061,7 +1592,7 @@ function analysisCommand() {
|
|
|
1061
1592
|
return cmd;
|
|
1062
1593
|
}
|
|
1063
1594
|
function usersCommand() {
|
|
1064
|
-
const cmd = new
|
|
1595
|
+
const cmd = new import_commander7.Command("users").description(
|
|
1065
1596
|
"List external users (chatbot customers) with health metrics"
|
|
1066
1597
|
);
|
|
1067
1598
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").option("--from <iso>").option("--to <iso>").option("--days <n>", "Convenience: last N days").option("--search <q>").option("--limit <n>", "Page size", "20").action(async function(opts) {
|
|
@@ -1093,7 +1624,7 @@ Total: ${result.total}
|
|
|
1093
1624
|
return cmd;
|
|
1094
1625
|
}
|
|
1095
1626
|
function sdkCommand() {
|
|
1096
|
-
const cmd = new
|
|
1627
|
+
const cmd = new import_commander7.Command("sdk").description(
|
|
1097
1628
|
"Help install and verify the OpenBat SDK in a target project"
|
|
1098
1629
|
);
|
|
1099
1630
|
cmd.command("install-instructions").description("Print markdown the calling agent can follow").option(
|
|
@@ -1187,17 +1718,23 @@ function sdkCommand() {
|
|
|
1187
1718
|
}
|
|
1188
1719
|
|
|
1189
1720
|
// src/index.ts
|
|
1190
|
-
var program = new
|
|
1721
|
+
var program = new import_commander8.Command();
|
|
1191
1722
|
program.name("openbat").description(
|
|
1192
1723
|
"Query OpenBat chatbot data \u2014 conversations, sentiment, analytics, exports."
|
|
1193
|
-
).version("0.
|
|
1724
|
+
).version("0.2.0").option("--api-key <key>", "Override the stored Read API key (footgun \u2014 leaks into shell history)").option(
|
|
1194
1725
|
"--base-url <url>",
|
|
1195
1726
|
"Override the OpenBat API base URL (defaults to ~/.openbatrc or https://app.openbat.com)"
|
|
1727
|
+
).option(
|
|
1728
|
+
"--chatbot <id|name>",
|
|
1729
|
+
"Override the active chatbot for this invocation (UUID or chatbot name)"
|
|
1196
1730
|
).option(
|
|
1197
1731
|
"--json",
|
|
1198
1732
|
"Emit raw JSON instead of the TTY-friendly pretty-print"
|
|
1199
1733
|
);
|
|
1734
|
+
program.addCommand(loginCommand());
|
|
1735
|
+
program.addCommand(logoutCommand());
|
|
1200
1736
|
program.addCommand(configCommand());
|
|
1737
|
+
program.addCommand(useCommand());
|
|
1201
1738
|
program.addCommand(authCommand());
|
|
1202
1739
|
program.addCommand(orgCommand());
|
|
1203
1740
|
program.addCommand(chatbotCommand());
|