@openbat/cli 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +715 -178
- package/dist/index.mjs +575 -46
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ApiClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NYKJTHHK.mjs";
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
|
-
import { Command as
|
|
7
|
+
import { Command as Command8 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/config.ts
|
|
10
10
|
import { Command } from "commander";
|
|
@@ -14,7 +14,7 @@ import { promises as fs, statSync, chmodSync } from "fs";
|
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { join } from "path";
|
|
16
16
|
var CONFIG_PATH = join(homedir(), ".openbatrc");
|
|
17
|
-
var DEFAULT_BASE_URL = "https://
|
|
17
|
+
var DEFAULT_BASE_URL = "https://openbat.dev";
|
|
18
18
|
async function readConfig() {
|
|
19
19
|
try {
|
|
20
20
|
const st = statSync(CONFIG_PATH);
|
|
@@ -64,7 +64,26 @@ async function resolveConfig(opts) {
|
|
|
64
64
|
baseUrl = file.baseUrl;
|
|
65
65
|
baseUrlSource = "file";
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
let activeChatbotId = null;
|
|
68
|
+
let activeChatbotIdSource = "missing";
|
|
69
|
+
if (opts.chatbotFlag) {
|
|
70
|
+
activeChatbotId = opts.chatbotFlag;
|
|
71
|
+
activeChatbotIdSource = "flag";
|
|
72
|
+
} else if (process.env.OPENBAT_CHATBOT_ID) {
|
|
73
|
+
activeChatbotId = process.env.OPENBAT_CHATBOT_ID;
|
|
74
|
+
activeChatbotIdSource = "env";
|
|
75
|
+
} else if (file.activeChatbotId) {
|
|
76
|
+
activeChatbotId = file.activeChatbotId;
|
|
77
|
+
activeChatbotIdSource = "file";
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
apiKey,
|
|
81
|
+
baseUrl,
|
|
82
|
+
activeChatbotId,
|
|
83
|
+
apiKeySource,
|
|
84
|
+
baseUrlSource,
|
|
85
|
+
activeChatbotIdSource
|
|
86
|
+
};
|
|
68
87
|
}
|
|
69
88
|
async function setApiKey(apiKey) {
|
|
70
89
|
if (apiKey.startsWith("ob_live_")) {
|
|
@@ -81,6 +100,25 @@ async function setApiKey(apiKey) {
|
|
|
81
100
|
file.apiKey = apiKey;
|
|
82
101
|
await writeConfig(file);
|
|
83
102
|
}
|
|
103
|
+
async function clearApiKey() {
|
|
104
|
+
const file = await readConfig();
|
|
105
|
+
delete file.apiKey;
|
|
106
|
+
delete file.activeChatbotId;
|
|
107
|
+
await writeConfig(file);
|
|
108
|
+
}
|
|
109
|
+
async function setActiveChatbotId(chatbotId) {
|
|
110
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chatbotId)) {
|
|
111
|
+
throw new Error(`Not a UUID: ${chatbotId}`);
|
|
112
|
+
}
|
|
113
|
+
const file = await readConfig();
|
|
114
|
+
file.activeChatbotId = chatbotId;
|
|
115
|
+
await writeConfig(file);
|
|
116
|
+
}
|
|
117
|
+
async function clearActiveChatbotId() {
|
|
118
|
+
const file = await readConfig();
|
|
119
|
+
delete file.activeChatbotId;
|
|
120
|
+
await writeConfig(file);
|
|
121
|
+
}
|
|
84
122
|
async function setBaseUrl(baseUrl) {
|
|
85
123
|
try {
|
|
86
124
|
new URL(baseUrl);
|
|
@@ -202,7 +240,7 @@ function configCommand() {
|
|
|
202
240
|
}
|
|
203
241
|
});
|
|
204
242
|
cmd.command("set-url <baseUrl>").description(
|
|
205
|
-
"Override the OpenBat API base URL (default: https://
|
|
243
|
+
"Override the OpenBat API base URL (default: https://openbat.dev)"
|
|
206
244
|
).action(async (baseUrl) => {
|
|
207
245
|
try {
|
|
208
246
|
await setBaseUrl(baseUrl);
|
|
@@ -220,25 +258,131 @@ function configCommand() {
|
|
|
220
258
|
apiKeySource: cfg.apiKeySource,
|
|
221
259
|
baseUrl: cfg.baseUrl,
|
|
222
260
|
baseUrlSource: cfg.baseUrlSource,
|
|
261
|
+
activeChatbotId: cfg.activeChatbotId ?? "(not set)",
|
|
262
|
+
activeChatbotIdSource: cfg.activeChatbotIdSource,
|
|
223
263
|
configFile: configPath()
|
|
224
264
|
});
|
|
225
265
|
});
|
|
266
|
+
cmd.command("use-chatbot <id-or-name>").description(
|
|
267
|
+
"Persist a default chatbot for commands that need one (e.g. with a PAT spanning many chatbots)"
|
|
268
|
+
).action(async function(target) {
|
|
269
|
+
try {
|
|
270
|
+
const id = await resolveTargetChatbotId(target);
|
|
271
|
+
await setActiveChatbotId(id);
|
|
272
|
+
process.stdout.write(`Active chatbot saved: ${id}
|
|
273
|
+
`);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
cmd.command("clear-chatbot").description("Forget the persisted default chatbot").action(async () => {
|
|
279
|
+
await clearActiveChatbotId();
|
|
280
|
+
process.stdout.write("Cleared active chatbot.\n");
|
|
281
|
+
});
|
|
226
282
|
return cmd;
|
|
227
283
|
}
|
|
284
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
285
|
+
async function resolveTargetChatbotId(target) {
|
|
286
|
+
const cfg = await resolveConfig({});
|
|
287
|
+
if (!cfg.apiKey) {
|
|
288
|
+
fatal(
|
|
289
|
+
"No API key configured. Run `openbat config set-key` first so I can list your chatbots."
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
293
|
+
const list = await api.get(
|
|
294
|
+
"/api/v1/chatbots"
|
|
295
|
+
);
|
|
296
|
+
const chatbots = list.chatbots ?? [];
|
|
297
|
+
if (UUID_RE.test(target)) {
|
|
298
|
+
const hit = chatbots.find((c) => c.id === target);
|
|
299
|
+
if (!hit) {
|
|
300
|
+
fatal(`Chatbot ${target} is not reachable by this API key.`);
|
|
301
|
+
}
|
|
302
|
+
return hit.id;
|
|
303
|
+
}
|
|
304
|
+
const lower = target.toLowerCase();
|
|
305
|
+
const matches = chatbots.filter((c) => c.name.toLowerCase() === lower);
|
|
306
|
+
if (matches.length === 0) {
|
|
307
|
+
fatal(`No chatbot named "${target}". Run \`openbat chatbot list\`.`);
|
|
308
|
+
}
|
|
309
|
+
if (matches.length > 1) {
|
|
310
|
+
fatal(
|
|
311
|
+
`Multiple chatbots named "${target}". Use the UUID \u2014 list them with \`openbat chatbot list\`.`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return matches[0].id;
|
|
315
|
+
}
|
|
316
|
+
function useCommand() {
|
|
317
|
+
return new 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) => {
|
|
318
|
+
try {
|
|
319
|
+
const id = await resolveTargetChatbotId(target);
|
|
320
|
+
await setActiveChatbotId(id);
|
|
321
|
+
process.stdout.write(`Active chatbot saved: ${id}
|
|
322
|
+
`);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
228
328
|
|
|
229
329
|
// src/commands/data.ts
|
|
230
330
|
import { Command as Command2 } from "commander";
|
|
231
331
|
async function client(globals) {
|
|
232
332
|
const cfg = await resolveConfig({
|
|
233
333
|
apiKeyFlag: globals.apiKey ?? null,
|
|
234
|
-
baseUrlFlag: globals.baseUrl ?? null
|
|
334
|
+
baseUrlFlag: globals.baseUrl ?? null,
|
|
335
|
+
chatbotFlag: globals.chatbot ?? null
|
|
235
336
|
});
|
|
236
337
|
if (!cfg.apiKey) {
|
|
237
338
|
fatal(
|
|
238
339
|
"No API key configured. Run `openbat config set-key`, or pass --api-key, or set OPENBAT_API_KEY."
|
|
239
340
|
);
|
|
240
341
|
}
|
|
241
|
-
return
|
|
342
|
+
return {
|
|
343
|
+
api: new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl }),
|
|
344
|
+
chatbotFlag: cfg.activeChatbotId
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
348
|
+
async function resolveChatbotId(api, chatbotFlag) {
|
|
349
|
+
const list = await api.get(
|
|
350
|
+
"/api/v1/chatbots"
|
|
351
|
+
);
|
|
352
|
+
const chatbots = list.chatbots ?? [];
|
|
353
|
+
if (chatbots.length === 0) {
|
|
354
|
+
fatal("This API key authorizes no chatbots.");
|
|
355
|
+
}
|
|
356
|
+
if (chatbotFlag) {
|
|
357
|
+
if (UUID_RE2.test(chatbotFlag)) {
|
|
358
|
+
const hit = chatbots.find((c) => c.id === chatbotFlag);
|
|
359
|
+
if (!hit) {
|
|
360
|
+
fatal(
|
|
361
|
+
`Chatbot ${chatbotFlag} is not reachable by this API key. Run \`openbat chatbot list\` to see what is.`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return hit.id;
|
|
365
|
+
}
|
|
366
|
+
const lower = chatbotFlag.toLowerCase();
|
|
367
|
+
const matches = chatbots.filter((c) => c.name.toLowerCase() === lower);
|
|
368
|
+
if (matches.length === 0) {
|
|
369
|
+
fatal(
|
|
370
|
+
`No chatbot named "${chatbotFlag}". Run \`openbat chatbot list\` to see what's reachable.`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (matches.length > 1) {
|
|
374
|
+
fatal(
|
|
375
|
+
`Multiple chatbots named "${chatbotFlag}". Use the UUID instead \u2014 list them with \`openbat chatbot list\`.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return matches[0].id;
|
|
379
|
+
}
|
|
380
|
+
if (chatbots.length === 1) return chatbots[0].id;
|
|
381
|
+
const lines = chatbots.map((c) => ` \u2022 ${c.name} ${c.id}`).join("\n");
|
|
382
|
+
fatal(
|
|
383
|
+
`This API key targets multiple chatbots. Pick one with \`openbat use <id>\` (persistent) or \`--chatbot <id>\` (one-off):
|
|
384
|
+
${lines}`
|
|
385
|
+
);
|
|
242
386
|
}
|
|
243
387
|
function formatOpts(cmd) {
|
|
244
388
|
const opts = cmd.optsWithGlobals();
|
|
@@ -250,12 +394,26 @@ function chatbotCommand() {
|
|
|
250
394
|
);
|
|
251
395
|
cmd.command("info").description("Show the chatbot the current API key authorizes").action(async function() {
|
|
252
396
|
try {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
397
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
398
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
399
|
+
const result = await api.get(
|
|
255
400
|
"/api/v1/chatbots"
|
|
256
401
|
);
|
|
257
|
-
const
|
|
258
|
-
|
|
402
|
+
const row = (result.chatbots ?? []).find(
|
|
403
|
+
(c) => c.id === id
|
|
404
|
+
) ?? null;
|
|
405
|
+
emit(row, formatOpts(this));
|
|
406
|
+
} catch (err) {
|
|
407
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
cmd.command("list").description("List every chatbot this API key authorizes").action(async function() {
|
|
411
|
+
try {
|
|
412
|
+
const { api } = await client(this.optsWithGlobals());
|
|
413
|
+
const result = await api.get(
|
|
414
|
+
"/api/v1/chatbots"
|
|
415
|
+
);
|
|
416
|
+
emit(result.chatbots ?? [], formatOpts(this));
|
|
259
417
|
} catch (err) {
|
|
260
418
|
fatal(err instanceof Error ? err.message : String(err));
|
|
261
419
|
}
|
|
@@ -268,12 +426,14 @@ function conversationsCommand() {
|
|
|
268
426
|
);
|
|
269
427
|
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) {
|
|
270
428
|
try {
|
|
271
|
-
const
|
|
429
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
430
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
272
431
|
const params = new URLSearchParams({
|
|
432
|
+
chatbotId: id,
|
|
273
433
|
page: opts.page,
|
|
274
434
|
limit: opts.limit
|
|
275
435
|
});
|
|
276
|
-
const result = await
|
|
436
|
+
const result = await api.get(`/api/v1/conversations?${params}`);
|
|
277
437
|
if (this.optsWithGlobals().json) {
|
|
278
438
|
emit(result, { json: true });
|
|
279
439
|
} else {
|
|
@@ -288,11 +448,13 @@ Total: ${result.total}, page ${opts.page}, limit ${opts.limit}
|
|
|
288
448
|
fatal(err instanceof Error ? err.message : String(err));
|
|
289
449
|
}
|
|
290
450
|
});
|
|
291
|
-
cmd.command("show <id>").description("Show one conversation by id, with messages").action(async function(
|
|
451
|
+
cmd.command("show <id>").description("Show one conversation by id, with messages").action(async function(convId) {
|
|
292
452
|
try {
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
453
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
454
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
455
|
+
const params = new URLSearchParams({ chatbotId: id });
|
|
456
|
+
const result = await api.get(
|
|
457
|
+
`/api/v1/conversations/${encodeURIComponent(convId)}?${params}`
|
|
296
458
|
);
|
|
297
459
|
emit(result.conversation, formatOpts(this));
|
|
298
460
|
} catch (err) {
|
|
@@ -303,13 +465,15 @@ Total: ${result.total}, page ${opts.page}, limit ${opts.limit}
|
|
|
303
465
|
}
|
|
304
466
|
function analyticsCommand() {
|
|
305
467
|
const cmd = new Command2("analytics").description(
|
|
306
|
-
"Aggregated analytics for the
|
|
468
|
+
"Aggregated analytics for the active chatbot"
|
|
307
469
|
);
|
|
308
470
|
cmd.command("overview").description("Total conversations, messages, sentiment distribution").action(async function() {
|
|
309
471
|
try {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
472
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
473
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
474
|
+
const params = new URLSearchParams({ chatbotId: id });
|
|
475
|
+
const result = await api.get(
|
|
476
|
+
`/api/v1/analytics/overview?${params}`
|
|
313
477
|
);
|
|
314
478
|
emit(result, formatOpts(this));
|
|
315
479
|
} catch (err) {
|
|
@@ -318,9 +482,10 @@ function analyticsCommand() {
|
|
|
318
482
|
});
|
|
319
483
|
cmd.command("sentiment").description("Sentiment over time (default last 30 days)").option("--days <n>", "Look-back window in days", "30").action(async function(opts) {
|
|
320
484
|
try {
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
485
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
486
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
487
|
+
const params = new URLSearchParams({ chatbotId: id, days: opts.days });
|
|
488
|
+
const result = await api.get(
|
|
324
489
|
`/api/v1/analytics/sentiment?${params}`
|
|
325
490
|
);
|
|
326
491
|
emit(result, formatOpts(this));
|
|
@@ -331,16 +496,12 @@ function analyticsCommand() {
|
|
|
331
496
|
return cmd;
|
|
332
497
|
}
|
|
333
498
|
function exportCommand() {
|
|
334
|
-
const cmd = new Command2("export").description("Dump all conversation data for the
|
|
499
|
+
const cmd = new Command2("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) {
|
|
335
500
|
try {
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
"/api/v1/chatbots"
|
|
339
|
-
);
|
|
340
|
-
const id = info.chatbots[0]?.id;
|
|
341
|
-
if (!id) fatal("Could not resolve chatbot from API key.");
|
|
501
|
+
const { api, chatbotFlag } = await client(this.optsWithGlobals());
|
|
502
|
+
const id = await resolveChatbotId(api, chatbotFlag);
|
|
342
503
|
const format = opts.format === "csv" ? "csv" : "json";
|
|
343
|
-
const { body } = await
|
|
504
|
+
const { body } = await api.getRaw(
|
|
344
505
|
`/api/v1/export/${id}?format=${format}`
|
|
345
506
|
);
|
|
346
507
|
if (opts.out) {
|
|
@@ -452,8 +613,370 @@ function authCommand() {
|
|
|
452
613
|
return cmd;
|
|
453
614
|
}
|
|
454
615
|
|
|
455
|
-
// src/commands/
|
|
616
|
+
// src/commands/login.ts
|
|
456
617
|
import { Command as Command4 } from "commander";
|
|
618
|
+
import os from "os";
|
|
619
|
+
import readline from "readline";
|
|
620
|
+
|
|
621
|
+
// src/browser.ts
|
|
622
|
+
import { spawn } from "child_process";
|
|
623
|
+
function openBrowser(url) {
|
|
624
|
+
try {
|
|
625
|
+
if (process.platform === "darwin") {
|
|
626
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
if (process.platform === "win32") {
|
|
630
|
+
spawn("cmd", ["/c", "start", "", url], {
|
|
631
|
+
detached: true,
|
|
632
|
+
stdio: "ignore",
|
|
633
|
+
windowsVerbatimArguments: true
|
|
634
|
+
}).unref();
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
638
|
+
return true;
|
|
639
|
+
} catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/pkce.ts
|
|
645
|
+
import crypto from "crypto";
|
|
646
|
+
function generateVerifier() {
|
|
647
|
+
return crypto.randomBytes(32).toString("hex");
|
|
648
|
+
}
|
|
649
|
+
function challengeFor(verifier) {
|
|
650
|
+
return crypto.createHash("sha256").update(verifier).digest("hex");
|
|
651
|
+
}
|
|
652
|
+
function randomState() {
|
|
653
|
+
return crypto.randomBytes(32).toString("hex");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/loopback.ts
|
|
657
|
+
import http from "http";
|
|
658
|
+
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
659
|
+
var SUCCESS_HTML = `<!doctype html>
|
|
660
|
+
<html lang="en">
|
|
661
|
+
<head>
|
|
662
|
+
<meta charset="utf-8" />
|
|
663
|
+
<title>OpenBat CLI \u2014 signed in</title>
|
|
664
|
+
<style>
|
|
665
|
+
body {
|
|
666
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
667
|
+
background: #0a0a0a;
|
|
668
|
+
color: #fafafa;
|
|
669
|
+
margin: 0;
|
|
670
|
+
min-height: 100vh;
|
|
671
|
+
display: flex;
|
|
672
|
+
align-items: center;
|
|
673
|
+
justify-content: center;
|
|
674
|
+
}
|
|
675
|
+
.card {
|
|
676
|
+
max-width: 420px;
|
|
677
|
+
padding: 32px;
|
|
678
|
+
border-radius: 14px;
|
|
679
|
+
background: #141414;
|
|
680
|
+
border: 1px solid #262626;
|
|
681
|
+
text-align: center;
|
|
682
|
+
}
|
|
683
|
+
h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; }
|
|
684
|
+
p { margin: 0; color: #a3a3a3; font-size: 14px; line-height: 1.5; }
|
|
685
|
+
</style>
|
|
686
|
+
</head>
|
|
687
|
+
<body>
|
|
688
|
+
<div class="card">
|
|
689
|
+
<h1>Signed in</h1>
|
|
690
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
691
|
+
</div>
|
|
692
|
+
</body>
|
|
693
|
+
</html>`;
|
|
694
|
+
var ERROR_HTML = (msg) => `<!doctype html>
|
|
695
|
+
<html><body style="font-family:system-ui;background:#0a0a0a;color:#fafafa;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center">
|
|
696
|
+
<div style="max-width:420px;padding:32px;text-align:center">
|
|
697
|
+
<h1 style="font-size:18px;margin:0 0 8px">Login failed</h1>
|
|
698
|
+
<p style="margin:0;color:#a3a3a3;font-size:14px">${escapeHtml(msg)}</p>
|
|
699
|
+
</div></body></html>`;
|
|
700
|
+
function escapeHtml(s) {
|
|
701
|
+
return s.replace(
|
|
702
|
+
/[&<>"]/g,
|
|
703
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
async function runLoopbackServer(opts) {
|
|
707
|
+
let resolve = null;
|
|
708
|
+
let reject = null;
|
|
709
|
+
const callback = new Promise((res, rej) => {
|
|
710
|
+
resolve = res;
|
|
711
|
+
reject = rej;
|
|
712
|
+
});
|
|
713
|
+
const server = http.createServer((req, res) => {
|
|
714
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
715
|
+
if (url.pathname !== "/cb") {
|
|
716
|
+
res.writeHead(404).end();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const code = url.searchParams.get("code");
|
|
720
|
+
const state = url.searchParams.get("state");
|
|
721
|
+
if (!code || !state) {
|
|
722
|
+
res.writeHead(400, { "content-type": "text/html" }).end(ERROR_HTML("Missing code or state"));
|
|
723
|
+
reject?.(new Error("Missing code or state in callback"));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (state !== opts.expectedState) {
|
|
727
|
+
res.writeHead(400, { "content-type": "text/html" }).end(ERROR_HTML("State mismatch \u2014 aborting for safety"));
|
|
728
|
+
reject?.(new Error("State mismatch \u2014 aborting"));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
|
|
732
|
+
resolve?.({ code });
|
|
733
|
+
setTimeout(() => server.close(), 100);
|
|
734
|
+
});
|
|
735
|
+
await new Promise((res) => server.listen(0, "127.0.0.1", () => res()));
|
|
736
|
+
const port = server.address().port;
|
|
737
|
+
const timer = setTimeout(() => {
|
|
738
|
+
server.close();
|
|
739
|
+
reject?.(new Error("Login timed out after 5 minutes."));
|
|
740
|
+
}, TIMEOUT_MS);
|
|
741
|
+
return {
|
|
742
|
+
port,
|
|
743
|
+
waitForCallback: async () => {
|
|
744
|
+
try {
|
|
745
|
+
const result = await callback;
|
|
746
|
+
clearTimeout(timer);
|
|
747
|
+
return result;
|
|
748
|
+
} catch (err) {
|
|
749
|
+
clearTimeout(timer);
|
|
750
|
+
server.close();
|
|
751
|
+
throw err;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/commands/login.ts
|
|
758
|
+
var HOSTNAME = os.hostname();
|
|
759
|
+
function loginCommand() {
|
|
760
|
+
return new Command4("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(
|
|
761
|
+
"--use <key>",
|
|
762
|
+
"Skip the browser; install a PAT plaintext you already have"
|
|
763
|
+
).action(async function(opts) {
|
|
764
|
+
try {
|
|
765
|
+
const globals = this.optsWithGlobals();
|
|
766
|
+
const cfg = await resolveConfig({
|
|
767
|
+
apiKeyFlag: null,
|
|
768
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
769
|
+
});
|
|
770
|
+
let token;
|
|
771
|
+
if (opts.use) {
|
|
772
|
+
token = opts.use.trim();
|
|
773
|
+
} else if (opts.device) {
|
|
774
|
+
token = await deviceFlow(cfg.baseUrl);
|
|
775
|
+
} else {
|
|
776
|
+
token = await loopbackFlow(cfg.baseUrl);
|
|
777
|
+
}
|
|
778
|
+
await setApiKey(token);
|
|
779
|
+
process.stdout.write(`Signed in. API key saved to ~/.openbatrc.
|
|
780
|
+
`);
|
|
781
|
+
await pickDefaultChatbot(token, cfg.baseUrl);
|
|
782
|
+
} catch (err) {
|
|
783
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async function loopbackFlow(baseUrl) {
|
|
788
|
+
const verifier = generateVerifier();
|
|
789
|
+
const challenge = challengeFor(verifier);
|
|
790
|
+
const state = randomState();
|
|
791
|
+
const { port, waitForCallback } = await runLoopbackServer({
|
|
792
|
+
expectedState: state
|
|
793
|
+
});
|
|
794
|
+
const redirectUri = `http://127.0.0.1:${port}/cb`;
|
|
795
|
+
const authorizeUrl = new URL("/platform/cli/authorize", baseUrl);
|
|
796
|
+
authorizeUrl.searchParams.set("state", state);
|
|
797
|
+
authorizeUrl.searchParams.set("challenge", challenge);
|
|
798
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
799
|
+
authorizeUrl.searchParams.set("hostname", HOSTNAME);
|
|
800
|
+
const opened = openBrowser(authorizeUrl.toString());
|
|
801
|
+
if (!opened) {
|
|
802
|
+
process.stderr.write(`
|
|
803
|
+
Couldn't open a browser. Visit this URL to continue:
|
|
804
|
+
${authorizeUrl}
|
|
805
|
+
|
|
806
|
+
`);
|
|
807
|
+
} else {
|
|
808
|
+
process.stderr.write(`
|
|
809
|
+
Opening your browser\u2026 (waiting on http://127.0.0.1:${port})
|
|
810
|
+
`);
|
|
811
|
+
process.stderr.write(`If it didn't open, visit: ${authorizeUrl}
|
|
812
|
+
|
|
813
|
+
`);
|
|
814
|
+
}
|
|
815
|
+
const { code } = await waitForCallback();
|
|
816
|
+
const exchangeRes = await fetch(new URL("/api/cli/exchange", baseUrl), {
|
|
817
|
+
method: "POST",
|
|
818
|
+
headers: { "content-type": "application/json" },
|
|
819
|
+
body: JSON.stringify({ code, verifier })
|
|
820
|
+
});
|
|
821
|
+
if (!exchangeRes.ok) {
|
|
822
|
+
const text = await exchangeRes.text();
|
|
823
|
+
throw new Error(`Exchange failed (${exchangeRes.status}): ${text}`);
|
|
824
|
+
}
|
|
825
|
+
const exchange = await exchangeRes.json();
|
|
826
|
+
if (!exchange.ok || !exchange.token) {
|
|
827
|
+
throw new Error("Exchange response missing token.");
|
|
828
|
+
}
|
|
829
|
+
return exchange.token;
|
|
830
|
+
}
|
|
831
|
+
async function deviceFlow(baseUrl) {
|
|
832
|
+
const startRes = await fetch(new URL("/api/cli/device/start", baseUrl), {
|
|
833
|
+
method: "POST",
|
|
834
|
+
headers: { "content-type": "application/json" },
|
|
835
|
+
body: JSON.stringify({ hostname: HOSTNAME })
|
|
836
|
+
});
|
|
837
|
+
if (!startRes.ok) {
|
|
838
|
+
const text = await startRes.text();
|
|
839
|
+
throw new Error(`Device flow start failed (${startRes.status}): ${text}`);
|
|
840
|
+
}
|
|
841
|
+
const start = await startRes.json();
|
|
842
|
+
process.stdout.write(`
|
|
843
|
+
On any device, visit:
|
|
844
|
+
${start.verification_uri}
|
|
845
|
+
|
|
846
|
+
`);
|
|
847
|
+
process.stdout.write(`and enter this code:
|
|
848
|
+
${start.user_code}
|
|
849
|
+
|
|
850
|
+
`);
|
|
851
|
+
process.stdout.write(`Or open directly:
|
|
852
|
+
${start.verification_uri_complete}
|
|
853
|
+
|
|
854
|
+
`);
|
|
855
|
+
process.stderr.write("Waiting for authorization\u2026\n");
|
|
856
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
857
|
+
const intervalMs = Math.max(start.interval, 5) * 1e3;
|
|
858
|
+
while (Date.now() < deadline) {
|
|
859
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
860
|
+
const pollRes = await fetch(new URL("/api/cli/device/poll", baseUrl), {
|
|
861
|
+
method: "POST",
|
|
862
|
+
headers: { "content-type": "application/json" },
|
|
863
|
+
body: JSON.stringify({ device_code: start.device_code })
|
|
864
|
+
});
|
|
865
|
+
if (pollRes.status === 429) continue;
|
|
866
|
+
if (!pollRes.ok) {
|
|
867
|
+
const text = await pollRes.text();
|
|
868
|
+
throw new Error(`Poll failed (${pollRes.status}): ${text}`);
|
|
869
|
+
}
|
|
870
|
+
const poll = await pollRes.json();
|
|
871
|
+
if (poll.status === "pending") continue;
|
|
872
|
+
if (poll.status === "expired") throw new Error("Code expired. Run `openbat login --device` again.");
|
|
873
|
+
if (poll.status === "denied") throw new Error("Authorization denied.");
|
|
874
|
+
if (poll.status === "unknown") throw new Error("Server lost the request. Try again.");
|
|
875
|
+
if (poll.status === "authorized") {
|
|
876
|
+
if (!poll.token) throw new Error("Server authorized but didn't return a token.");
|
|
877
|
+
return poll.token;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
throw new Error("Timed out waiting for authorization.");
|
|
881
|
+
}
|
|
882
|
+
async function pickDefaultChatbot(token, baseUrl) {
|
|
883
|
+
const api = new ApiClient({ apiKey: token, baseUrl });
|
|
884
|
+
let chatbots;
|
|
885
|
+
try {
|
|
886
|
+
const result = await api.get(
|
|
887
|
+
"/api/v1/chatbots"
|
|
888
|
+
);
|
|
889
|
+
chatbots = result.chatbots ?? [];
|
|
890
|
+
} catch (err) {
|
|
891
|
+
process.stderr.write(
|
|
892
|
+
`
|
|
893
|
+
(Couldn't list chatbots: ${err instanceof Error ? err.message : String(err)})
|
|
894
|
+
`
|
|
895
|
+
);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (chatbots.length === 0) {
|
|
899
|
+
process.stdout.write("\nNo chatbots reachable yet. Create one from the dashboard.\n");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (chatbots.length === 1) {
|
|
903
|
+
await setActiveChatbotId(chatbots[0].id);
|
|
904
|
+
process.stdout.write(`Default chatbot: ${chatbots[0].name}
|
|
905
|
+
`);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (!process.stdin.isTTY) {
|
|
909
|
+
process.stdout.write(
|
|
910
|
+
`
|
|
911
|
+
You have ${chatbots.length} chatbots. Pick one later with \`openbat use <id>\`.
|
|
912
|
+
`
|
|
913
|
+
);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
process.stdout.write("\nYou have access to multiple chatbots. Pick a default:\n");
|
|
917
|
+
chatbots.forEach((c, i) => {
|
|
918
|
+
process.stdout.write(` ${i + 1}. ${c.name} ${c.id}
|
|
919
|
+
`);
|
|
920
|
+
});
|
|
921
|
+
process.stdout.write(` ${chatbots.length + 1}. Skip \u2014 pick later with \`openbat use\`
|
|
922
|
+
`);
|
|
923
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
924
|
+
const answer = await new Promise(
|
|
925
|
+
(res) => rl.question("Choice: ", (input) => {
|
|
926
|
+
rl.close();
|
|
927
|
+
res(input.trim());
|
|
928
|
+
})
|
|
929
|
+
);
|
|
930
|
+
const idx = Number(answer);
|
|
931
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > chatbots.length + 1) {
|
|
932
|
+
process.stdout.write("(Invalid choice \u2014 skipping.)\n");
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (idx === chatbots.length + 1) return;
|
|
936
|
+
await setActiveChatbotId(chatbots[idx - 1].id);
|
|
937
|
+
process.stdout.write(`Default chatbot: ${chatbots[idx - 1].name}
|
|
938
|
+
`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/commands/logout.ts
|
|
942
|
+
import { Command as Command5 } from "commander";
|
|
943
|
+
function logoutCommand() {
|
|
944
|
+
return new Command5("logout").description("Revoke the stored API key and clear ~/.openbatrc").action(async function() {
|
|
945
|
+
try {
|
|
946
|
+
const globals = this.optsWithGlobals();
|
|
947
|
+
const cfg = await resolveConfig({
|
|
948
|
+
apiKeyFlag: null,
|
|
949
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
950
|
+
});
|
|
951
|
+
if (!cfg.apiKey) {
|
|
952
|
+
process.stdout.write("Not signed in.\n");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (cfg.apiKey.startsWith("ob_pat_")) {
|
|
956
|
+
const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
957
|
+
try {
|
|
958
|
+
await api.post("/api/v1/me/pats/self/revoke", {});
|
|
959
|
+
} catch (err) {
|
|
960
|
+
process.stderr.write(
|
|
961
|
+
`Warning: server-side revoke failed (${err instanceof Error ? err.message : err}). Clearing local config anyway.
|
|
962
|
+
`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
} else {
|
|
966
|
+
process.stderr.write(
|
|
967
|
+
"Note: read/admin keys can't self-revoke. Visit the dashboard to revoke; this command will just clear the local config.\n"
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
await clearApiKey();
|
|
971
|
+
process.stdout.write("Signed out.\n");
|
|
972
|
+
} catch (err) {
|
|
973
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/commands/org.ts
|
|
979
|
+
import { Command as Command6 } from "commander";
|
|
457
980
|
async function client3(globals) {
|
|
458
981
|
const cfg = await resolveConfig({
|
|
459
982
|
apiKeyFlag: globals.apiKey ?? null,
|
|
@@ -468,7 +991,7 @@ async function client3(globals) {
|
|
|
468
991
|
return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
469
992
|
}
|
|
470
993
|
function orgCommand() {
|
|
471
|
-
const cmd = new
|
|
994
|
+
const cmd = new Command6("org").description(
|
|
472
995
|
"Manage the OpenBat tenant org (rename, members, invitations). Requires PAT."
|
|
473
996
|
);
|
|
474
997
|
cmd.command("list").description("List orgs the current PAT's user belongs to").action(async function() {
|
|
@@ -576,7 +1099,7 @@ function orgCommand() {
|
|
|
576
1099
|
}
|
|
577
1100
|
|
|
578
1101
|
// src/commands/write.ts
|
|
579
|
-
import { Command as
|
|
1102
|
+
import { Command as Command7 } from "commander";
|
|
580
1103
|
async function client4(globals) {
|
|
581
1104
|
const cfg = await resolveConfig({
|
|
582
1105
|
apiKeyFlag: globals.apiKey ?? null,
|
|
@@ -600,7 +1123,7 @@ function surfacePlaintext(plaintext, label) {
|
|
|
600
1123
|
);
|
|
601
1124
|
}
|
|
602
1125
|
function chatbotsCommand() {
|
|
603
|
-
const cmd = new
|
|
1126
|
+
const cmd = new Command7("chatbots").description(
|
|
604
1127
|
"List, create, delete chatbots in the current scope"
|
|
605
1128
|
);
|
|
606
1129
|
cmd.command("list").description("List every chatbot the credential can reach").action(async function() {
|
|
@@ -648,7 +1171,7 @@ function chatbotsCommand() {
|
|
|
648
1171
|
return cmd;
|
|
649
1172
|
}
|
|
650
1173
|
function webhooksCommand() {
|
|
651
|
-
const cmd = new
|
|
1174
|
+
const cmd = new Command7("webhooks").description("Manage webhooks for a chatbot");
|
|
652
1175
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
653
1176
|
try {
|
|
654
1177
|
const globals = this.optsWithGlobals();
|
|
@@ -689,7 +1212,7 @@ function webhooksCommand() {
|
|
|
689
1212
|
return cmd;
|
|
690
1213
|
}
|
|
691
1214
|
function settingsCommand() {
|
|
692
|
-
const cmd = new
|
|
1215
|
+
const cmd = new Command7("settings").description(
|
|
693
1216
|
"Manage chatbot settings + per-chatbot keys"
|
|
694
1217
|
);
|
|
695
1218
|
const keys = cmd.command("keys").description("Manage API keys for a chatbot");
|
|
@@ -777,7 +1300,7 @@ function settingsCommand() {
|
|
|
777
1300
|
return cmd;
|
|
778
1301
|
}
|
|
779
1302
|
function workflowsCommand() {
|
|
780
|
-
const cmd = new
|
|
1303
|
+
const cmd = new Command7("workflows").description("Manage workflows");
|
|
781
1304
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
782
1305
|
try {
|
|
783
1306
|
const globals = this.optsWithGlobals();
|
|
@@ -815,7 +1338,7 @@ function workflowsCommand() {
|
|
|
815
1338
|
return cmd;
|
|
816
1339
|
}
|
|
817
1340
|
function reportsCommand() {
|
|
818
|
-
const cmd = new
|
|
1341
|
+
const cmd = new Command7("reports").description("Manage AI reports");
|
|
819
1342
|
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
820
1343
|
try {
|
|
821
1344
|
const globals = this.optsWithGlobals();
|
|
@@ -851,7 +1374,7 @@ Created report. View it (org members only):
|
|
|
851
1374
|
return cmd;
|
|
852
1375
|
}
|
|
853
1376
|
function analysisCommand() {
|
|
854
|
-
const cmd = new
|
|
1377
|
+
const cmd = new Command7("analysis").description("Manage analysis definitions");
|
|
855
1378
|
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) {
|
|
856
1379
|
try {
|
|
857
1380
|
const globals = this.optsWithGlobals();
|
|
@@ -891,7 +1414,7 @@ function analysisCommand() {
|
|
|
891
1414
|
return cmd;
|
|
892
1415
|
}
|
|
893
1416
|
function usersCommand() {
|
|
894
|
-
const cmd = new
|
|
1417
|
+
const cmd = new Command7("users").description(
|
|
895
1418
|
"List external users (chatbot customers) with health metrics"
|
|
896
1419
|
);
|
|
897
1420
|
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) {
|
|
@@ -923,7 +1446,7 @@ Total: ${result.total}
|
|
|
923
1446
|
return cmd;
|
|
924
1447
|
}
|
|
925
1448
|
function sdkCommand() {
|
|
926
|
-
const cmd = new
|
|
1449
|
+
const cmd = new Command7("sdk").description(
|
|
927
1450
|
"Help install and verify the OpenBat SDK in a target project"
|
|
928
1451
|
);
|
|
929
1452
|
cmd.command("install-instructions").description("Print markdown the calling agent can follow").option(
|
|
@@ -1017,17 +1540,23 @@ function sdkCommand() {
|
|
|
1017
1540
|
}
|
|
1018
1541
|
|
|
1019
1542
|
// src/index.ts
|
|
1020
|
-
var program = new
|
|
1543
|
+
var program = new Command8();
|
|
1021
1544
|
program.name("openbat").description(
|
|
1022
1545
|
"Query OpenBat chatbot data \u2014 conversations, sentiment, analytics, exports."
|
|
1023
|
-
).version("0.1
|
|
1546
|
+
).version("0.2.1").option("--api-key <key>", "Override the stored Read API key (footgun \u2014 leaks into shell history)").option(
|
|
1024
1547
|
"--base-url <url>",
|
|
1025
|
-
"Override the OpenBat API base URL (defaults to ~/.openbatrc or https://
|
|
1548
|
+
"Override the OpenBat API base URL (defaults to ~/.openbatrc or https://openbat.dev)"
|
|
1549
|
+
).option(
|
|
1550
|
+
"--chatbot <id|name>",
|
|
1551
|
+
"Override the active chatbot for this invocation (UUID or chatbot name)"
|
|
1026
1552
|
).option(
|
|
1027
1553
|
"--json",
|
|
1028
1554
|
"Emit raw JSON instead of the TTY-friendly pretty-print"
|
|
1029
1555
|
);
|
|
1556
|
+
program.addCommand(loginCommand());
|
|
1557
|
+
program.addCommand(logoutCommand());
|
|
1030
1558
|
program.addCommand(configCommand());
|
|
1559
|
+
program.addCommand(useCommand());
|
|
1031
1560
|
program.addCommand(authCommand());
|
|
1032
1561
|
program.addCommand(orgCommand());
|
|
1033
1562
|
program.addCommand(chatbotCommand());
|