@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/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ApiClient
4
- } from "./chunk-CRJZM45P.mjs";
4
+ } from "./chunk-NYKJTHHK.mjs";
5
5
 
6
6
  // src/index.ts
7
- import { Command as Command6 } from "commander";
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://app.openbat.com";
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
- return { apiKey, baseUrl, apiKeySource, baseUrlSource };
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://app.openbat.com)"
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 new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
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 c = await client(this.optsWithGlobals());
254
- const result = await c.get(
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 list = result.chatbots ?? [];
258
- emit(list[0] ?? null, formatOpts(this));
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 c = await client(this.optsWithGlobals());
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 c.get(`/api/v1/conversations?${params}`);
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(id) {
451
+ cmd.command("show <id>").description("Show one conversation by id, with messages").action(async function(convId) {
292
452
  try {
293
- const c = await client(this.optsWithGlobals());
294
- const result = await c.get(
295
- `/api/v1/conversations/${encodeURIComponent(id)}`
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 bound chatbot"
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 c = await client(this.optsWithGlobals());
311
- const result = await c.get(
312
- "/api/v1/analytics/overview"
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 c = await client(this.optsWithGlobals());
322
- const params = new URLSearchParams({ days: opts.days });
323
- const result = await c.get(
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 bound chatbot").option("--format <fmt>", "json or csv", "json").option("--out <path>", "Write to file (defaults to stdout)").action(async function(opts) {
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 c = await client(this.optsWithGlobals());
337
- const info = await c.get(
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 c.getRaw(
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/org.ts
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 === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;"
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 Command4("org").description(
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 Command5 } from "commander";
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 Command5("chatbots").description(
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 Command5("webhooks").description("Manage webhooks for a chatbot");
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 Command5("settings").description(
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 Command5("workflows").description("Manage workflows");
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 Command5("reports").description("Manage AI reports");
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 Command5("analysis").description("Manage analysis definitions");
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 Command5("users").description(
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 Command5("sdk").description(
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 Command6();
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.0").option("--api-key <key>", "Override the stored Read API key (footgun \u2014 leaks into shell history)").option(
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://app.openbat.com)"
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());