@openbat/cli 0.2.3 → 0.3.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 CHANGED
@@ -1,8 +1,10 @@
1
1
  # @openbat/cli
2
2
 
3
3
  Command-line tool for managing OpenBat chatbots end-to-end — read
4
- analytics + conversations, manage settings + keys + webhooks + workflows
4
+ analytics + conversations, pull daily eval digests (`review`), manage the
5
+ live system prompt (`prompts`), manage settings + keys + webhooks + workflows
5
6
  + reports, run experiments, and help install the SDK in a target app.
7
+ All commands operate on one **pinned chatbot** (`openbat use <id>`).
6
8
 
7
9
  > **Companion docs**: [`@openbat/mcp`](../mcp/README.md) (same surface
8
10
  > over MCP), [`lib/openbat-tools/`](../../lib/openbat-tools/README.md)
@@ -63,6 +65,25 @@ rejects it with a helpful error.
63
65
 
64
66
  ---
65
67
 
68
+ ## Pin to one chatbot (active-chatbot scope)
69
+
70
+ `ob_read_*` / `ob_admin_*` keys are already scoped to one chatbot server-side.
71
+ A `ob_pat_*` reaches many — pin one so every command stays on it:
72
+
73
+ ```bash
74
+ openbat use <id|name> # persists ~/.openbatrc.activeChatbotId
75
+ openbat use # no arg → shows the current pin + reachable options
76
+ ```
77
+
78
+ Once pinned, every data command (`conversations`, `analytics`, `review`,
79
+ `prompts`, …) targets that chatbot and prints a `→ chatbot: <name> (<id>)`
80
+ banner to **stderr** so scope is never in doubt (and `--json | jq` stays
81
+ clean). Active-chatbot resolution order: `--chatbot <id|name>` flag →
82
+ `$OPENBAT_CHATBOT_ID` → `~/.openbatrc`. The MCP server reads the same pin and
83
+ **hard-locks** to it (see [`@openbat/mcp`](../mcp/README.md)).
84
+
85
+ ---
86
+
66
87
  ## Command tree
67
88
 
68
89
  ```
@@ -70,7 +91,13 @@ openbat
70
91
  ├── config
71
92
  │ ├── set-key Store / replace the key in ~/.openbatrc
72
93
  │ ├── set-url <baseUrl> Override the API base URL
73
- └── show Print the resolved config (key prefix only)
94
+ ├── show Print the resolved config (key prefix + active chatbot)
95
+ │ ├── use-chatbot <id|name> Pin the active chatbot (persists to ~/.openbatrc)
96
+ │ └── clear-chatbot Forget the pinned chatbot
97
+
98
+ ├── use [id|name] [any kind]
99
+ │ └── Pin the active chatbot (shortcut for `config use-chatbot`).
100
+ │ Omit the arg to print the current pin + the reachable list.
74
101
 
75
102
  ├── auth [any kind]
76
103
  │ ├── whoami Show kind, chatbots in scope, orgs (PAT only)
@@ -96,7 +123,11 @@ openbat
96
123
 
97
124
  ├── conversations [any kind]
98
125
  │ ├── list [--days N] [--from ISO] [--to ISO] [--limit N]
99
- │ └── show <id>
126
+ │ └── show <id> Messages + ALL analyses (issues/outcomes/flags/intents + reasoning)
127
+
128
+ ├── review [--since 45m|6h|7d] [any kind]
129
+ │ └── Daily eval digest: outcome/sentiment deltas + top issues/flags/intents
130
+ │ with representative conversation pointers. Default window 24h.
100
131
 
101
132
  ├── users [any kind]
102
133
  │ └── list --chatbot ID [--days] [--search] External users + health
@@ -124,6 +155,12 @@ openbat
124
155
  │ ├── list --chatbot ID
125
156
  │ └── create --chatbot ID [--name "..."] Returns org-private dashboard URL
126
157
 
158
+ ├── prompts manage the LIVE published prompt
159
+ │ ├── list [any kind] versions + active + kill-switch
160
+ │ ├── publish --file PATH | --text "..." [admin/pat] create a version + set it LIVE
161
+ │ ├── activate <versionId> [admin/pat] roll back/forward to a version
162
+ │ └── kill-switch --on | --off [admin/pat] emergency fallback toggle
163
+
127
164
  ├── analysis [admin or pat]
128
165
  │ ├── list --chatbot ID [--type] [--pending]
129
166
  │ └── add --chatbot ID --type intent|flag|assistant_outcome|assistant_issue
@@ -272,6 +309,9 @@ Per-tool buckets keyed by credential. The strict ones to know about:
272
309
  | Create backtest | 10 per hour per PAT |
273
310
  | Invite org member | 20 per hour per PAT |
274
311
  | Chat with an AI report | 30 per minute per credential |
312
+ | Daily review digest (`review`) | 30 per minute per credential |
313
+ | Publish prompt | 20 per hour per credential |
314
+ | Activate prompt / toggle kill switch | 30 per hour per credential |
275
315
  | Generic write | 60 per minute per credential |
276
316
  | Generic read | 600 per minute per credential |
277
317
  | Export | 30 per hour per credential |
@@ -290,11 +330,12 @@ install the OpenBat agent skill bundle:
290
330
  npx skills add openbat-dev/agent-skills
291
331
  ```
292
332
 
293
- Drops 8 `SKILL.md` files into your project's `.claude/skills/` (or the
333
+ Drops the `SKILL.md` bundle into your project's `.claude/skills/` (or the
294
334
  equivalent for Cursor / Copilot / Gemini CLI / Codex / OpenCode / Amp).
295
335
  The bundle covers chatbot onboarding, key management, webhooks,
296
- workflows, AI reports, SDK install, and safety patterns — written
297
- specifically for the four-kind auth ladder this CLI exposes.
336
+ workflows, AI reports, SDK install, the daily eval→fix loop
337
+ (`openbat-optimize`), and safety patterns written specifically for the
338
+ four-kind auth ladder this CLI exposes.
298
339
 
299
340
  Source + docs: https://github.com/openbat-dev/agent-skills.
300
341
 
@@ -32,7 +32,7 @@ __export(api_client_exports, {
32
32
  });
33
33
  module.exports = __toCommonJS(api_client_exports);
34
34
  var import_node_url = require("url");
35
- var CLI_VERSION = "0.2.3";
35
+ var CLI_VERSION = "0.3.0";
36
36
  var KEY_REGEX = /ob_(?:live|read|admin|pat)_[0-9a-f]{32}/g;
37
37
  function redact(s) {
38
38
  return s.replace(KEY_REGEX, (k) => `${k.slice(0, 16)}\u2026<hidden>`);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ApiClient
3
- } from "./chunk-HBSCN3HV.mjs";
3
+ } from "./chunk-KIY62R3O.mjs";
4
4
  export {
5
5
  ApiClient
6
6
  };
@@ -9,7 +9,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
9
9
 
10
10
  // src/api-client.ts
11
11
  import { URL } from "url";
12
- var CLI_VERSION = "0.2.3";
12
+ var CLI_VERSION = "0.3.0";
13
13
  var KEY_REGEX = /ob_(?:live|read|admin|pat)_[0-9a-f]{32}/g;
14
14
  function redact(s) {
15
15
  return s.replace(KEY_REGEX, (k) => `${k.slice(0, 16)}\u2026<hidden>`);
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 import_commander8 = require("commander");
35
+ var import_commander10 = require("commander");
36
36
 
37
37
  // src/commands/config.ts
38
38
  var import_commander = require("commander");
@@ -163,7 +163,7 @@ function configPath() {
163
163
 
164
164
  // src/api-client.ts
165
165
  var import_node_url = require("url");
166
- var CLI_VERSION = "0.2.3";
166
+ var CLI_VERSION = "0.3.0";
167
167
  var KEY_REGEX = /ob_(?:live|read|admin|pat)_[0-9a-f]{32}/g;
168
168
  function redact(s) {
169
169
  return s.replace(KEY_REGEX, (k) => `${k.slice(0, 16)}\u2026<hidden>`);
@@ -433,12 +433,24 @@ function configCommand() {
433
433
  cmd.command("show").description("Show the current resolved config (key prefix only)").action(async () => {
434
434
  const cfg = await resolveConfig({});
435
435
  const keyDisplay = cfg.apiKey ? `${cfg.apiKey.slice(0, 16)}\u2026<hidden>` : "(not set)";
436
+ let activeChatbot = cfg.activeChatbotId ?? "(not set)";
437
+ if (cfg.apiKey && cfg.activeChatbotId) {
438
+ try {
439
+ const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
440
+ const list = await api.get("/api/v1/chatbots");
441
+ const hit = (list.chatbots ?? []).find(
442
+ (c) => c.id === cfg.activeChatbotId
443
+ );
444
+ if (hit) activeChatbot = `${hit.name} (${hit.id})`;
445
+ } catch {
446
+ }
447
+ }
436
448
  emit({
437
449
  apiKey: keyDisplay,
438
450
  apiKeySource: cfg.apiKeySource,
439
451
  baseUrl: cfg.baseUrl,
440
452
  baseUrlSource: cfg.baseUrlSource,
441
- activeChatbotId: cfg.activeChatbotId ?? "(not set)",
453
+ activeChatbot,
442
454
  activeChatbotIdSource: cfg.activeChatbotIdSource,
443
455
  configFile: configPath()
444
456
  });
@@ -493,9 +505,46 @@ async function resolveTargetChatbotId(target) {
493
505
  }
494
506
  return matches[0].id;
495
507
  }
508
+ async function showCurrentAndOptions() {
509
+ const cfg = await resolveConfig({});
510
+ if (!cfg.apiKey) {
511
+ fatal("No API key configured. Run `openbat config set-key` first.");
512
+ }
513
+ const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
514
+ const list = await api.get(
515
+ "/api/v1/chatbots"
516
+ );
517
+ const chatbots = list.chatbots ?? [];
518
+ if (cfg.activeChatbotId) {
519
+ const hit = chatbots.find((c) => c.id === cfg.activeChatbotId);
520
+ process.stdout.write(
521
+ hit ? `Active chatbot: ${hit.name} (${hit.id})
522
+
523
+ ` : `Active chatbot: ${cfg.activeChatbotId} (not in reachable set)
524
+
525
+ `
526
+ );
527
+ } else {
528
+ process.stdout.write("No active chatbot set.\n\n");
529
+ }
530
+ if (chatbots.length === 0) {
531
+ process.stdout.write("No chatbots reachable by this key.\n");
532
+ return;
533
+ }
534
+ process.stdout.write("Reachable chatbots \u2014 pin one with `openbat use <id>`:\n");
535
+ for (const c of chatbots) {
536
+ const marker = c.id === cfg.activeChatbotId ? "\u25CF" : "\u25CB";
537
+ process.stdout.write(` ${marker} ${c.name} ${c.id}
538
+ `);
539
+ }
540
+ }
496
541
  function useCommand() {
497
- 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) => {
542
+ return new import_commander.Command("use").argument("[id-or-name]", "Chatbot UUID or exact name (omit to show current + options)").description("Set the active chatbot for this CLI (omit the arg to see current + options)").action(async (target) => {
498
543
  try {
544
+ if (!target) {
545
+ await showCurrentAndOptions();
546
+ return;
547
+ }
499
548
  const id = await resolveTargetChatbotId(target);
500
549
  await setActiveChatbotId(id);
501
550
  process.stdout.write(`Active chatbot saved: ${id}
@@ -525,6 +574,10 @@ async function client(globals) {
525
574
  };
526
575
  }
527
576
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
577
+ function announceScope(name, id) {
578
+ process.stderr.write(`\u2192 chatbot: ${name} (${id})
579
+ `);
580
+ }
528
581
  async function resolveChatbotId(api, chatbotFlag) {
529
582
  const list = await api.get(
530
583
  "/api/v1/chatbots"
@@ -543,6 +596,7 @@ async function resolveChatbotId(api, chatbotFlag) {
543
596
  `Chatbot ${chatbotFlag} is not reachable by this API key. Run \`openbat chatbot list\` to see what is.`
544
597
  );
545
598
  }
599
+ announceScope(hit.name, hit.id);
546
600
  return hit.id;
547
601
  }
548
602
  const lower = chatbotFlag.toLowerCase();
@@ -557,9 +611,13 @@ async function resolveChatbotId(api, chatbotFlag) {
557
611
  `Multiple chatbots named "${chatbotFlag}". Use the UUID instead \u2014 list them with \`openbat chatbot list\`.`
558
612
  );
559
613
  }
614
+ announceScope(matches[0].name, matches[0].id);
560
615
  return matches[0].id;
561
616
  }
562
- if (chatbots.length === 1) return chatbots[0].id;
617
+ if (chatbots.length === 1) {
618
+ announceScope(chatbots[0].name, chatbots[0].id);
619
+ return chatbots[0].id;
620
+ }
563
621
  const lines = chatbots.map((c) => ` \u2022 ${c.name} ${c.id}`).join("\n");
564
622
  fatal(
565
623
  `This API key targets multiple chatbots. Pick one with \`openbat use <id>\` (persistent) or \`--chatbot <id>\` (one-off):
@@ -687,9 +745,10 @@ function exportCommand() {
687
745
  `/api/v1/export/${id}?format=${format}`
688
746
  );
689
747
  if (opts.out) {
690
- const { createWriteStream } = await import("fs");
691
- const { Writable } = await import("stream");
692
- const { Readable } = await import("stream");
748
+ const [{ createWriteStream }, { Writable, Readable }] = await Promise.all([
749
+ import("fs"),
750
+ import("stream")
751
+ ]);
693
752
  const file = createWriteStream(opts.out);
694
753
  const nodeReadable = Readable.fromWeb(
695
754
  body
@@ -724,6 +783,13 @@ function detectKind(apiKey) {
724
783
  if (apiKey.startsWith("ob_pat_")) return "pat";
725
784
  return "unknown";
726
785
  }
786
+ function activeChatbotDisplay(activeId, chatbots) {
787
+ if (!activeId) {
788
+ return chatbots.length > 1 ? `(not pinned \u2014 ${chatbots.length} reachable; run \`openbat use <id>\`)` : "(not pinned)";
789
+ }
790
+ const hit = chatbots.find((c) => c.id === activeId);
791
+ return hit ? `${hit.name} (${hit.id})` : `${activeId} (not in reachable set)`;
792
+ }
727
793
  function authCommand() {
728
794
  const cmd = new import_commander3.Command("auth").description(
729
795
  "Inspect the current credential's scope and audit history"
@@ -747,6 +813,10 @@ function authCommand() {
747
813
  {
748
814
  kind,
749
815
  keyPrefix: `${cfg.apiKey.slice(0, 16)}\u2026<hidden>`,
816
+ activeChatbot: activeChatbotDisplay(
817
+ cfg.activeChatbotId,
818
+ chatbots.chatbots
819
+ ),
750
820
  orgs: orgs.orgs,
751
821
  chatbots: chatbots.chatbots
752
822
  },
@@ -760,6 +830,10 @@ function authCommand() {
760
830
  {
761
831
  kind,
762
832
  keyPrefix: `${cfg.apiKey.slice(0, 16)}\u2026<hidden>`,
833
+ activeChatbot: activeChatbotDisplay(
834
+ cfg.activeChatbotId,
835
+ chatbots.chatbots
836
+ ),
763
837
  chatbots: chatbots.chatbots
764
838
  },
765
839
  { json: !!globals.json }
@@ -772,8 +846,178 @@ function authCommand() {
772
846
  return cmd;
773
847
  }
774
848
 
775
- // src/commands/login.ts
849
+ // src/commands/review.ts
776
850
  var import_commander4 = require("commander");
851
+ function parseSinceToMinutes(s) {
852
+ const match = /^(\d+)\s*(m|h|d)?$/.exec(s.trim().toLowerCase());
853
+ if (!match) {
854
+ fatal(`Invalid --since "${s}". Use e.g. 45m, 6h, or 7d.`);
855
+ }
856
+ const n = Number(match[1]);
857
+ const unit = match[2] ?? "m";
858
+ const minutes = unit === "d" ? n * 1440 : unit === "h" ? n * 60 : n;
859
+ if (minutes < 1) fatal("--since must be at least 1 minute.");
860
+ if (minutes > 43200) fatal("--since cannot exceed 30 days (43200m / 720h / 30d).");
861
+ return minutes;
862
+ }
863
+ var arrow = (t) => t === "up" ? "\u25B2" : t === "down" ? "\u25BC" : "=";
864
+ var pctDelta = (d) => d === null ? "(new)" : `${arrow(d >= 0 ? "up" : "down")} ${d >= 0 ? "+" : ""}${d}%`;
865
+ var ptsDelta = (d) => `${arrow(d >= 0 ? "up" : "down")} ${d >= 0 ? "+" : ""}${d} pts`;
866
+ function renderReview(r) {
867
+ const L = [];
868
+ const h = r.headline;
869
+ L.push(`OpenBat review \xB7 last ${r.window.since} \xB7 vs prior ${r.window.since}`);
870
+ L.push("");
871
+ L.push(` Conversations ${h.conversations.value} ${pctDelta(h.conversations.delta_pct)}`);
872
+ L.push(` Messages ${h.messages.value}`);
873
+ L.push(` Resolved ${h.outcomes.resolved.pct}% ${ptsDelta(h.outcomes.resolved.delta_pts)}`);
874
+ L.push(` Partial ${h.outcomes.partially_resolved.pct}% ${ptsDelta(h.outcomes.partially_resolved.delta_pts)}`);
875
+ L.push(` Failed ${h.outcomes.failed.pct}% ${ptsDelta(h.outcomes.failed.delta_pts)}`);
876
+ const sent = h.avg_sentiment.value ?? "\u2014";
877
+ const sentDelta = h.avg_sentiment.delta === null ? "" : ` ${arrow(h.avg_sentiment.delta >= 0 ? "up" : "down")} ${h.avg_sentiment.delta >= 0 ? "+" : ""}${h.avg_sentiment.delta}`;
878
+ L.push(` Avg sentiment ${sent}${sentDelta}`);
879
+ L.push(` Flagged ${h.flagged}`);
880
+ if (r.clusters.issues.length) {
881
+ L.push("");
882
+ L.push("Top issues");
883
+ for (const i of r.clusters.issues) {
884
+ const ans = i.answer_available.true + i.answer_available.false;
885
+ const ansStr = ans > 0 ? ` answer_available ${i.answer_available.true}/${ans}` : "";
886
+ L.push(
887
+ ` ${i.type} ${i.count} ${arrow(i.trend)} high ${i.severity.high} \xB7 med ${i.severity.medium} \xB7 low ${i.severity.low}${ansStr}`
888
+ );
889
+ }
890
+ }
891
+ if (r.clusters.flags.length) {
892
+ L.push("");
893
+ L.push(`Top flags ${r.clusters.flags.map((f) => `${f.value} ${f.count} ${arrow(f.trend)}`).join(" \xB7 ")}`);
894
+ }
895
+ if (r.clusters.intents.length) {
896
+ L.push(`Top intents ${r.clusters.intents.map((i) => `${i.value} ${i.count} ${arrow(i.trend)}`).join(" \xB7 ")}`);
897
+ }
898
+ const reps = [
899
+ ...r.clusters.issues.flatMap((i) => i.representatives),
900
+ ...r.clusters.failedOutcomes
901
+ ];
902
+ if (reps.length) {
903
+ L.push("");
904
+ L.push("Representative failures");
905
+ for (const rep of reps.slice(0, 8)) {
906
+ const tag = rep.type ? `[${rep.type} \xB7 ${rep.severity ?? "?"} \xB7 answer_available=${rep.answer_available ?? "?"} \xB7 source=${rep.verification_source ?? "?"}]` : `[failed outcome]`;
907
+ L.push(` ${tag}`);
908
+ L.push(` conv ${rep.conversationId ?? "?"} msg ${rep.messageId}`);
909
+ if (rep.reasoning) L.push(` why: ${rep.reasoning}`);
910
+ }
911
+ }
912
+ L.push("");
913
+ L.push("Drill in: openbat conversations show <id> \xB7 Fix: openbat-optimize skill");
914
+ return L.join("\n");
915
+ }
916
+ function reviewCommand() {
917
+ return new import_commander4.Command("review").description(
918
+ "Daily eval digest for the active chatbot \u2014 flags, issues, outcomes + reasonings"
919
+ ).option(
920
+ "--since <duration>",
921
+ "Look-back window: 45m, 6h, 7d (max 30d)",
922
+ "24h"
923
+ ).action(async function(opts) {
924
+ try {
925
+ const windowMinutes = parseSinceToMinutes(opts.since);
926
+ const globals = this.optsWithGlobals();
927
+ const { api, chatbotFlag } = await client(globals);
928
+ const id = await resolveChatbotId(api, chatbotFlag);
929
+ const params = new URLSearchParams({
930
+ windowMinutes: String(windowMinutes)
931
+ });
932
+ const result = await api.get(
933
+ `/api/v1/chatbots/${id}/review?${params}`
934
+ );
935
+ if (this.optsWithGlobals().json) {
936
+ emit(result, { json: true });
937
+ } else {
938
+ process.stdout.write(renderReview(result) + "\n");
939
+ }
940
+ } catch (err) {
941
+ fatal(err instanceof Error ? err.message : String(err));
942
+ }
943
+ });
944
+ }
945
+
946
+ // src/commands/prompts.ts
947
+ var import_commander5 = require("commander");
948
+ var import_node_fs2 = require("fs");
949
+ function promptsCommand() {
950
+ const cmd = new import_commander5.Command("prompts").description(
951
+ "Manage the live published system prompt (list / publish / activate / kill-switch)"
952
+ );
953
+ cmd.command("list").description("List system-prompt versions + live controls (active version, kill switch)").action(async function() {
954
+ try {
955
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
956
+ const id = await resolveChatbotId(api, chatbotFlag);
957
+ const result = await api.get(
958
+ `/api/v1/chatbots/${id}/prompts`
959
+ );
960
+ emit(result, { json: !!this.optsWithGlobals().json });
961
+ } catch (err) {
962
+ fatal(err instanceof Error ? err.message : String(err));
963
+ }
964
+ });
965
+ cmd.command("publish").description("Publish a template as the LIVE prompt (from --file or --text)").option("--file <path>", "Read the template text from a file").option("--text <text>", "Inline template text (prefer --file for long prompts)").action(async function(opts) {
966
+ try {
967
+ let templateText = opts.text;
968
+ if (opts.file) templateText = await import_node_fs2.promises.readFile(opts.file, "utf8");
969
+ if (!templateText || !templateText.trim()) {
970
+ fatal("Provide the prompt text via --file <path> or --text <text>.");
971
+ }
972
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
973
+ const id = await resolveChatbotId(api, chatbotFlag);
974
+ const result = await api.post(
975
+ `/api/v1/chatbots/${id}/prompts`,
976
+ { templateText }
977
+ );
978
+ emit(result, { json: !!this.optsWithGlobals().json });
979
+ process.stderr.write(
980
+ "\nPublished. Live within ~60s for chatbots that fetch their prompt from OpenBat.\n"
981
+ );
982
+ } catch (err) {
983
+ fatal(err instanceof Error ? err.message : String(err));
984
+ }
985
+ });
986
+ cmd.command("activate <versionId>").description("Point the live prompt at an existing version id (roll back/forward)").action(async function(versionId) {
987
+ try {
988
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
989
+ const id = await resolveChatbotId(api, chatbotFlag);
990
+ const result = await api.post(
991
+ `/api/v1/chatbots/${id}/prompts/activate`,
992
+ { versionId }
993
+ );
994
+ emit(result, { json: !!this.optsWithGlobals().json });
995
+ } catch (err) {
996
+ fatal(err instanceof Error ? err.message : String(err));
997
+ }
998
+ });
999
+ cmd.command("kill-switch").description("Toggle the remote prompt kill switch (--on / --off)").option("--on", "Enable: SDK falls back to its hardcoded prompt").option("--off", "Disable: SDK serves the active published prompt again").action(async function(opts) {
1000
+ try {
1001
+ if (!!opts.on === !!opts.off) {
1002
+ fatal("Pass exactly one of --on or --off.");
1003
+ }
1004
+ const enabled = !!opts.on;
1005
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
1006
+ const id = await resolveChatbotId(api, chatbotFlag);
1007
+ const result = await api.post(
1008
+ `/api/v1/chatbots/${id}/prompts/kill-switch`,
1009
+ { enabled }
1010
+ );
1011
+ emit(result, { json: !!this.optsWithGlobals().json });
1012
+ } catch (err) {
1013
+ fatal(err instanceof Error ? err.message : String(err));
1014
+ }
1015
+ });
1016
+ return cmd;
1017
+ }
1018
+
1019
+ // src/commands/login.ts
1020
+ var import_commander6 = require("commander");
777
1021
  var import_node_os2 = __toESM(require("os"));
778
1022
  var import_node_readline = __toESM(require("readline"));
779
1023
 
@@ -916,7 +1160,7 @@ async function runLoopbackServer(opts) {
916
1160
  // src/commands/login.ts
917
1161
  var HOSTNAME = import_node_os2.default.hostname();
918
1162
  function loginCommand() {
919
- 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(
1163
+ return new import_commander6.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(
920
1164
  "--use <key>",
921
1165
  "Skip the browser; install a PAT plaintext you already have"
922
1166
  ).action(async function(opts) {
@@ -1106,9 +1350,9 @@ You have ${chatbots.length} chatbots. Pick one later with \`openbat use <id>\`.
1106
1350
  }
1107
1351
 
1108
1352
  // src/commands/logout.ts
1109
- var import_commander5 = require("commander");
1353
+ var import_commander7 = require("commander");
1110
1354
  function logoutCommand() {
1111
- return new import_commander5.Command("logout").description("Revoke the stored API key and clear ~/.openbatrc").action(async function() {
1355
+ return new import_commander7.Command("logout").description("Revoke the stored API key and clear ~/.openbatrc").action(async function() {
1112
1356
  try {
1113
1357
  const globals = this.optsWithGlobals();
1114
1358
  const cfg = await resolveConfig({
@@ -1143,7 +1387,7 @@ function logoutCommand() {
1143
1387
  }
1144
1388
 
1145
1389
  // src/commands/org.ts
1146
- var import_commander6 = require("commander");
1390
+ var import_commander8 = require("commander");
1147
1391
  async function client2(globals) {
1148
1392
  const cfg = await resolveConfig({
1149
1393
  apiKeyFlag: globals.apiKey ?? null,
@@ -1158,7 +1402,7 @@ async function client2(globals) {
1158
1402
  return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
1159
1403
  }
1160
1404
  function orgCommand() {
1161
- const cmd = new import_commander6.Command("org").description(
1405
+ const cmd = new import_commander8.Command("org").description(
1162
1406
  "Manage the OpenBat tenant org (rename, members, invitations). Requires PAT."
1163
1407
  );
1164
1408
  cmd.command("list").description("List orgs the current PAT's user belongs to").action(async function() {
@@ -1266,7 +1510,7 @@ function orgCommand() {
1266
1510
  }
1267
1511
 
1268
1512
  // src/commands/write.ts
1269
- var import_commander7 = require("commander");
1513
+ var import_commander9 = require("commander");
1270
1514
  async function client3(globals) {
1271
1515
  const cfg = await resolveConfig({
1272
1516
  apiKeyFlag: globals.apiKey ?? null,
@@ -1299,7 +1543,7 @@ function surfacePlaintext(plaintext, label) {
1299
1543
  );
1300
1544
  }
1301
1545
  function chatbotsCommand() {
1302
- const cmd = new import_commander7.Command("chatbots").description(
1546
+ const cmd = new import_commander9.Command("chatbots").description(
1303
1547
  "List, create, delete chatbots in the current scope"
1304
1548
  );
1305
1549
  cmd.command("list").description("List every chatbot the credential can reach").action(async function() {
@@ -1347,7 +1591,7 @@ function chatbotsCommand() {
1347
1591
  return cmd;
1348
1592
  }
1349
1593
  function webhooksCommand() {
1350
- const cmd = new import_commander7.Command("webhooks").description("Manage webhooks for a chatbot");
1594
+ const cmd = new import_commander9.Command("webhooks").description("Manage webhooks for a chatbot");
1351
1595
  cmd.command("list").action(async function() {
1352
1596
  try {
1353
1597
  const chatbotId = requireChatbotId(this);
@@ -1391,7 +1635,7 @@ function webhooksCommand() {
1391
1635
  return cmd;
1392
1636
  }
1393
1637
  function settingsCommand() {
1394
- const cmd = new import_commander7.Command("settings").description(
1638
+ const cmd = new import_commander9.Command("settings").description(
1395
1639
  "Manage chatbot settings + per-chatbot keys"
1396
1640
  );
1397
1641
  const keys = cmd.command("keys").description("Manage API keys for a chatbot");
@@ -1485,7 +1729,7 @@ function settingsCommand() {
1485
1729
  return cmd;
1486
1730
  }
1487
1731
  function workflowsCommand() {
1488
- const cmd = new import_commander7.Command("workflows").description("Manage workflows");
1732
+ const cmd = new import_commander9.Command("workflows").description("Manage workflows");
1489
1733
  cmd.command("list").action(async function() {
1490
1734
  try {
1491
1735
  const chatbotId = requireChatbotId(this);
@@ -1525,7 +1769,7 @@ function workflowsCommand() {
1525
1769
  return cmd;
1526
1770
  }
1527
1771
  function reportsCommand() {
1528
- const cmd = new import_commander7.Command("reports").description("Manage AI reports");
1772
+ const cmd = new import_commander9.Command("reports").description("Manage AI reports");
1529
1773
  cmd.command("list").action(async function() {
1530
1774
  try {
1531
1775
  const chatbotId = requireChatbotId(this);
@@ -1563,7 +1807,7 @@ Created report. View it (org members only):
1563
1807
  return cmd;
1564
1808
  }
1565
1809
  function analysisCommand() {
1566
- const cmd = new import_commander7.Command("analysis").description("Manage analysis definitions");
1810
+ const cmd = new import_commander9.Command("analysis").description("Manage analysis definitions");
1567
1811
  cmd.command("list").option("--type <t>", "intent | flag | assistant_outcome | assistant_issue").option("--pending", "Include pending suggestions").action(async function(opts) {
1568
1812
  try {
1569
1813
  const chatbotId = requireChatbotId(this);
@@ -1605,7 +1849,7 @@ function analysisCommand() {
1605
1849
  return cmd;
1606
1850
  }
1607
1851
  function usersCommand() {
1608
- const cmd = new import_commander7.Command("users").description(
1852
+ const cmd = new import_commander9.Command("users").description(
1609
1853
  "List external users (chatbot customers) with health metrics"
1610
1854
  );
1611
1855
  cmd.command("list").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) {
@@ -1638,7 +1882,7 @@ Total: ${result.total}
1638
1882
  return cmd;
1639
1883
  }
1640
1884
  function sdkCommand() {
1641
- const cmd = new import_commander7.Command("sdk").description(
1885
+ const cmd = new import_commander9.Command("sdk").description(
1642
1886
  "Help install and verify the OpenBat SDK in a target project"
1643
1887
  );
1644
1888
  cmd.command("install-instructions").description("Print markdown the calling agent can follow").option(
@@ -1748,10 +1992,10 @@ Timed out after ${timeoutSec}s \u2014 no events yet. Confirm OPENBAT_API_KEY and
1748
1992
  }
1749
1993
 
1750
1994
  // src/index.ts
1751
- var program = new import_commander8.Command();
1995
+ var program = new import_commander10.Command();
1752
1996
  program.name("openbat").description(
1753
1997
  "Query OpenBat chatbot data \u2014 conversations, sentiment, analytics, exports."
1754
- ).version("0.2.3").option("--api-key <key>", "Override the stored Read API key (footgun \u2014 leaks into shell history)").option(
1998
+ ).version("0.3.0").option("--api-key <key>", "Override the stored Read API key (footgun \u2014 leaks into shell history)").option(
1755
1999
  "--base-url <url>",
1756
2000
  "Override the OpenBat API base URL (defaults to ~/.openbatrc or https://openbat.dev)"
1757
2001
  ).option(
@@ -1770,6 +2014,8 @@ program.addCommand(orgCommand());
1770
2014
  program.addCommand(chatbotCommand());
1771
2015
  program.addCommand(chatbotsCommand());
1772
2016
  program.addCommand(conversationsCommand());
2017
+ program.addCommand(reviewCommand());
2018
+ program.addCommand(promptsCommand());
1773
2019
  program.addCommand(usersCommand());
1774
2020
  program.addCommand(settingsCommand());
1775
2021
  program.addCommand(webhooksCommand());
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ApiClient
4
- } from "./chunk-HBSCN3HV.mjs";
4
+ } from "./chunk-KIY62R3O.mjs";
5
5
 
6
6
  // src/index.ts
7
- import { Command as Command8 } from "commander";
7
+ import { Command as Command10 } from "commander";
8
8
 
9
9
  // src/commands/config.ts
10
10
  import { Command } from "commander";
@@ -255,12 +255,24 @@ function configCommand() {
255
255
  cmd.command("show").description("Show the current resolved config (key prefix only)").action(async () => {
256
256
  const cfg = await resolveConfig({});
257
257
  const keyDisplay = cfg.apiKey ? `${cfg.apiKey.slice(0, 16)}\u2026<hidden>` : "(not set)";
258
+ let activeChatbot = cfg.activeChatbotId ?? "(not set)";
259
+ if (cfg.apiKey && cfg.activeChatbotId) {
260
+ try {
261
+ const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
262
+ const list = await api.get("/api/v1/chatbots");
263
+ const hit = (list.chatbots ?? []).find(
264
+ (c) => c.id === cfg.activeChatbotId
265
+ );
266
+ if (hit) activeChatbot = `${hit.name} (${hit.id})`;
267
+ } catch {
268
+ }
269
+ }
258
270
  emit({
259
271
  apiKey: keyDisplay,
260
272
  apiKeySource: cfg.apiKeySource,
261
273
  baseUrl: cfg.baseUrl,
262
274
  baseUrlSource: cfg.baseUrlSource,
263
- activeChatbotId: cfg.activeChatbotId ?? "(not set)",
275
+ activeChatbot,
264
276
  activeChatbotIdSource: cfg.activeChatbotIdSource,
265
277
  configFile: configPath()
266
278
  });
@@ -315,9 +327,46 @@ async function resolveTargetChatbotId(target) {
315
327
  }
316
328
  return matches[0].id;
317
329
  }
330
+ async function showCurrentAndOptions() {
331
+ const cfg = await resolveConfig({});
332
+ if (!cfg.apiKey) {
333
+ fatal("No API key configured. Run `openbat config set-key` first.");
334
+ }
335
+ const api = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
336
+ const list = await api.get(
337
+ "/api/v1/chatbots"
338
+ );
339
+ const chatbots = list.chatbots ?? [];
340
+ if (cfg.activeChatbotId) {
341
+ const hit = chatbots.find((c) => c.id === cfg.activeChatbotId);
342
+ process.stdout.write(
343
+ hit ? `Active chatbot: ${hit.name} (${hit.id})
344
+
345
+ ` : `Active chatbot: ${cfg.activeChatbotId} (not in reachable set)
346
+
347
+ `
348
+ );
349
+ } else {
350
+ process.stdout.write("No active chatbot set.\n\n");
351
+ }
352
+ if (chatbots.length === 0) {
353
+ process.stdout.write("No chatbots reachable by this key.\n");
354
+ return;
355
+ }
356
+ process.stdout.write("Reachable chatbots \u2014 pin one with `openbat use <id>`:\n");
357
+ for (const c of chatbots) {
358
+ const marker = c.id === cfg.activeChatbotId ? "\u25CF" : "\u25CB";
359
+ process.stdout.write(` ${marker} ${c.name} ${c.id}
360
+ `);
361
+ }
362
+ }
318
363
  function useCommand() {
319
- 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) => {
364
+ return new Command("use").argument("[id-or-name]", "Chatbot UUID or exact name (omit to show current + options)").description("Set the active chatbot for this CLI (omit the arg to see current + options)").action(async (target) => {
320
365
  try {
366
+ if (!target) {
367
+ await showCurrentAndOptions();
368
+ return;
369
+ }
321
370
  const id = await resolveTargetChatbotId(target);
322
371
  await setActiveChatbotId(id);
323
372
  process.stdout.write(`Active chatbot saved: ${id}
@@ -347,6 +396,10 @@ async function client(globals) {
347
396
  };
348
397
  }
349
398
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
399
+ function announceScope(name, id) {
400
+ process.stderr.write(`\u2192 chatbot: ${name} (${id})
401
+ `);
402
+ }
350
403
  async function resolveChatbotId(api, chatbotFlag) {
351
404
  const list = await api.get(
352
405
  "/api/v1/chatbots"
@@ -365,6 +418,7 @@ async function resolveChatbotId(api, chatbotFlag) {
365
418
  `Chatbot ${chatbotFlag} is not reachable by this API key. Run \`openbat chatbot list\` to see what is.`
366
419
  );
367
420
  }
421
+ announceScope(hit.name, hit.id);
368
422
  return hit.id;
369
423
  }
370
424
  const lower = chatbotFlag.toLowerCase();
@@ -379,9 +433,13 @@ async function resolveChatbotId(api, chatbotFlag) {
379
433
  `Multiple chatbots named "${chatbotFlag}". Use the UUID instead \u2014 list them with \`openbat chatbot list\`.`
380
434
  );
381
435
  }
436
+ announceScope(matches[0].name, matches[0].id);
382
437
  return matches[0].id;
383
438
  }
384
- if (chatbots.length === 1) return chatbots[0].id;
439
+ if (chatbots.length === 1) {
440
+ announceScope(chatbots[0].name, chatbots[0].id);
441
+ return chatbots[0].id;
442
+ }
385
443
  const lines = chatbots.map((c) => ` \u2022 ${c.name} ${c.id}`).join("\n");
386
444
  fatal(
387
445
  `This API key targets multiple chatbots. Pick one with \`openbat use <id>\` (persistent) or \`--chatbot <id>\` (one-off):
@@ -509,9 +567,10 @@ function exportCommand() {
509
567
  `/api/v1/export/${id}?format=${format}`
510
568
  );
511
569
  if (opts.out) {
512
- const { createWriteStream } = await import("fs");
513
- const { Writable } = await import("stream");
514
- const { Readable } = await import("stream");
570
+ const [{ createWriteStream }, { Writable, Readable }] = await Promise.all([
571
+ import("fs"),
572
+ import("stream")
573
+ ]);
515
574
  const file = createWriteStream(opts.out);
516
575
  const nodeReadable = Readable.fromWeb(
517
576
  body
@@ -546,6 +605,13 @@ function detectKind(apiKey) {
546
605
  if (apiKey.startsWith("ob_pat_")) return "pat";
547
606
  return "unknown";
548
607
  }
608
+ function activeChatbotDisplay(activeId, chatbots) {
609
+ if (!activeId) {
610
+ return chatbots.length > 1 ? `(not pinned \u2014 ${chatbots.length} reachable; run \`openbat use <id>\`)` : "(not pinned)";
611
+ }
612
+ const hit = chatbots.find((c) => c.id === activeId);
613
+ return hit ? `${hit.name} (${hit.id})` : `${activeId} (not in reachable set)`;
614
+ }
549
615
  function authCommand() {
550
616
  const cmd = new Command3("auth").description(
551
617
  "Inspect the current credential's scope and audit history"
@@ -569,6 +635,10 @@ function authCommand() {
569
635
  {
570
636
  kind,
571
637
  keyPrefix: `${cfg.apiKey.slice(0, 16)}\u2026<hidden>`,
638
+ activeChatbot: activeChatbotDisplay(
639
+ cfg.activeChatbotId,
640
+ chatbots.chatbots
641
+ ),
572
642
  orgs: orgs.orgs,
573
643
  chatbots: chatbots.chatbots
574
644
  },
@@ -582,6 +652,10 @@ function authCommand() {
582
652
  {
583
653
  kind,
584
654
  keyPrefix: `${cfg.apiKey.slice(0, 16)}\u2026<hidden>`,
655
+ activeChatbot: activeChatbotDisplay(
656
+ cfg.activeChatbotId,
657
+ chatbots.chatbots
658
+ ),
585
659
  chatbots: chatbots.chatbots
586
660
  },
587
661
  { json: !!globals.json }
@@ -594,8 +668,178 @@ function authCommand() {
594
668
  return cmd;
595
669
  }
596
670
 
597
- // src/commands/login.ts
671
+ // src/commands/review.ts
598
672
  import { Command as Command4 } from "commander";
673
+ function parseSinceToMinutes(s) {
674
+ const match = /^(\d+)\s*(m|h|d)?$/.exec(s.trim().toLowerCase());
675
+ if (!match) {
676
+ fatal(`Invalid --since "${s}". Use e.g. 45m, 6h, or 7d.`);
677
+ }
678
+ const n = Number(match[1]);
679
+ const unit = match[2] ?? "m";
680
+ const minutes = unit === "d" ? n * 1440 : unit === "h" ? n * 60 : n;
681
+ if (minutes < 1) fatal("--since must be at least 1 minute.");
682
+ if (minutes > 43200) fatal("--since cannot exceed 30 days (43200m / 720h / 30d).");
683
+ return minutes;
684
+ }
685
+ var arrow = (t) => t === "up" ? "\u25B2" : t === "down" ? "\u25BC" : "=";
686
+ var pctDelta = (d) => d === null ? "(new)" : `${arrow(d >= 0 ? "up" : "down")} ${d >= 0 ? "+" : ""}${d}%`;
687
+ var ptsDelta = (d) => `${arrow(d >= 0 ? "up" : "down")} ${d >= 0 ? "+" : ""}${d} pts`;
688
+ function renderReview(r) {
689
+ const L = [];
690
+ const h = r.headline;
691
+ L.push(`OpenBat review \xB7 last ${r.window.since} \xB7 vs prior ${r.window.since}`);
692
+ L.push("");
693
+ L.push(` Conversations ${h.conversations.value} ${pctDelta(h.conversations.delta_pct)}`);
694
+ L.push(` Messages ${h.messages.value}`);
695
+ L.push(` Resolved ${h.outcomes.resolved.pct}% ${ptsDelta(h.outcomes.resolved.delta_pts)}`);
696
+ L.push(` Partial ${h.outcomes.partially_resolved.pct}% ${ptsDelta(h.outcomes.partially_resolved.delta_pts)}`);
697
+ L.push(` Failed ${h.outcomes.failed.pct}% ${ptsDelta(h.outcomes.failed.delta_pts)}`);
698
+ const sent = h.avg_sentiment.value ?? "\u2014";
699
+ const sentDelta = h.avg_sentiment.delta === null ? "" : ` ${arrow(h.avg_sentiment.delta >= 0 ? "up" : "down")} ${h.avg_sentiment.delta >= 0 ? "+" : ""}${h.avg_sentiment.delta}`;
700
+ L.push(` Avg sentiment ${sent}${sentDelta}`);
701
+ L.push(` Flagged ${h.flagged}`);
702
+ if (r.clusters.issues.length) {
703
+ L.push("");
704
+ L.push("Top issues");
705
+ for (const i of r.clusters.issues) {
706
+ const ans = i.answer_available.true + i.answer_available.false;
707
+ const ansStr = ans > 0 ? ` answer_available ${i.answer_available.true}/${ans}` : "";
708
+ L.push(
709
+ ` ${i.type} ${i.count} ${arrow(i.trend)} high ${i.severity.high} \xB7 med ${i.severity.medium} \xB7 low ${i.severity.low}${ansStr}`
710
+ );
711
+ }
712
+ }
713
+ if (r.clusters.flags.length) {
714
+ L.push("");
715
+ L.push(`Top flags ${r.clusters.flags.map((f) => `${f.value} ${f.count} ${arrow(f.trend)}`).join(" \xB7 ")}`);
716
+ }
717
+ if (r.clusters.intents.length) {
718
+ L.push(`Top intents ${r.clusters.intents.map((i) => `${i.value} ${i.count} ${arrow(i.trend)}`).join(" \xB7 ")}`);
719
+ }
720
+ const reps = [
721
+ ...r.clusters.issues.flatMap((i) => i.representatives),
722
+ ...r.clusters.failedOutcomes
723
+ ];
724
+ if (reps.length) {
725
+ L.push("");
726
+ L.push("Representative failures");
727
+ for (const rep of reps.slice(0, 8)) {
728
+ const tag = rep.type ? `[${rep.type} \xB7 ${rep.severity ?? "?"} \xB7 answer_available=${rep.answer_available ?? "?"} \xB7 source=${rep.verification_source ?? "?"}]` : `[failed outcome]`;
729
+ L.push(` ${tag}`);
730
+ L.push(` conv ${rep.conversationId ?? "?"} msg ${rep.messageId}`);
731
+ if (rep.reasoning) L.push(` why: ${rep.reasoning}`);
732
+ }
733
+ }
734
+ L.push("");
735
+ L.push("Drill in: openbat conversations show <id> \xB7 Fix: openbat-optimize skill");
736
+ return L.join("\n");
737
+ }
738
+ function reviewCommand() {
739
+ return new Command4("review").description(
740
+ "Daily eval digest for the active chatbot \u2014 flags, issues, outcomes + reasonings"
741
+ ).option(
742
+ "--since <duration>",
743
+ "Look-back window: 45m, 6h, 7d (max 30d)",
744
+ "24h"
745
+ ).action(async function(opts) {
746
+ try {
747
+ const windowMinutes = parseSinceToMinutes(opts.since);
748
+ const globals = this.optsWithGlobals();
749
+ const { api, chatbotFlag } = await client(globals);
750
+ const id = await resolveChatbotId(api, chatbotFlag);
751
+ const params = new URLSearchParams({
752
+ windowMinutes: String(windowMinutes)
753
+ });
754
+ const result = await api.get(
755
+ `/api/v1/chatbots/${id}/review?${params}`
756
+ );
757
+ if (this.optsWithGlobals().json) {
758
+ emit(result, { json: true });
759
+ } else {
760
+ process.stdout.write(renderReview(result) + "\n");
761
+ }
762
+ } catch (err) {
763
+ fatal(err instanceof Error ? err.message : String(err));
764
+ }
765
+ });
766
+ }
767
+
768
+ // src/commands/prompts.ts
769
+ import { Command as Command5 } from "commander";
770
+ import { promises as fs2 } from "fs";
771
+ function promptsCommand() {
772
+ const cmd = new Command5("prompts").description(
773
+ "Manage the live published system prompt (list / publish / activate / kill-switch)"
774
+ );
775
+ cmd.command("list").description("List system-prompt versions + live controls (active version, kill switch)").action(async function() {
776
+ try {
777
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
778
+ const id = await resolveChatbotId(api, chatbotFlag);
779
+ const result = await api.get(
780
+ `/api/v1/chatbots/${id}/prompts`
781
+ );
782
+ emit(result, { json: !!this.optsWithGlobals().json });
783
+ } catch (err) {
784
+ fatal(err instanceof Error ? err.message : String(err));
785
+ }
786
+ });
787
+ cmd.command("publish").description("Publish a template as the LIVE prompt (from --file or --text)").option("--file <path>", "Read the template text from a file").option("--text <text>", "Inline template text (prefer --file for long prompts)").action(async function(opts) {
788
+ try {
789
+ let templateText = opts.text;
790
+ if (opts.file) templateText = await fs2.readFile(opts.file, "utf8");
791
+ if (!templateText || !templateText.trim()) {
792
+ fatal("Provide the prompt text via --file <path> or --text <text>.");
793
+ }
794
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
795
+ const id = await resolveChatbotId(api, chatbotFlag);
796
+ const result = await api.post(
797
+ `/api/v1/chatbots/${id}/prompts`,
798
+ { templateText }
799
+ );
800
+ emit(result, { json: !!this.optsWithGlobals().json });
801
+ process.stderr.write(
802
+ "\nPublished. Live within ~60s for chatbots that fetch their prompt from OpenBat.\n"
803
+ );
804
+ } catch (err) {
805
+ fatal(err instanceof Error ? err.message : String(err));
806
+ }
807
+ });
808
+ cmd.command("activate <versionId>").description("Point the live prompt at an existing version id (roll back/forward)").action(async function(versionId) {
809
+ try {
810
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
811
+ const id = await resolveChatbotId(api, chatbotFlag);
812
+ const result = await api.post(
813
+ `/api/v1/chatbots/${id}/prompts/activate`,
814
+ { versionId }
815
+ );
816
+ emit(result, { json: !!this.optsWithGlobals().json });
817
+ } catch (err) {
818
+ fatal(err instanceof Error ? err.message : String(err));
819
+ }
820
+ });
821
+ cmd.command("kill-switch").description("Toggle the remote prompt kill switch (--on / --off)").option("--on", "Enable: SDK falls back to its hardcoded prompt").option("--off", "Disable: SDK serves the active published prompt again").action(async function(opts) {
822
+ try {
823
+ if (!!opts.on === !!opts.off) {
824
+ fatal("Pass exactly one of --on or --off.");
825
+ }
826
+ const enabled = !!opts.on;
827
+ const { api, chatbotFlag } = await client(this.optsWithGlobals());
828
+ const id = await resolveChatbotId(api, chatbotFlag);
829
+ const result = await api.post(
830
+ `/api/v1/chatbots/${id}/prompts/kill-switch`,
831
+ { enabled }
832
+ );
833
+ emit(result, { json: !!this.optsWithGlobals().json });
834
+ } catch (err) {
835
+ fatal(err instanceof Error ? err.message : String(err));
836
+ }
837
+ });
838
+ return cmd;
839
+ }
840
+
841
+ // src/commands/login.ts
842
+ import { Command as Command6 } from "commander";
599
843
  import os from "os";
600
844
  import readline from "readline";
601
845
 
@@ -738,7 +982,7 @@ async function runLoopbackServer(opts) {
738
982
  // src/commands/login.ts
739
983
  var HOSTNAME = os.hostname();
740
984
  function loginCommand() {
741
- 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(
985
+ return new Command6("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(
742
986
  "--use <key>",
743
987
  "Skip the browser; install a PAT plaintext you already have"
744
988
  ).action(async function(opts) {
@@ -928,9 +1172,9 @@ You have ${chatbots.length} chatbots. Pick one later with \`openbat use <id>\`.
928
1172
  }
929
1173
 
930
1174
  // src/commands/logout.ts
931
- import { Command as Command5 } from "commander";
1175
+ import { Command as Command7 } from "commander";
932
1176
  function logoutCommand() {
933
- return new Command5("logout").description("Revoke the stored API key and clear ~/.openbatrc").action(async function() {
1177
+ return new Command7("logout").description("Revoke the stored API key and clear ~/.openbatrc").action(async function() {
934
1178
  try {
935
1179
  const globals = this.optsWithGlobals();
936
1180
  const cfg = await resolveConfig({
@@ -965,7 +1209,7 @@ function logoutCommand() {
965
1209
  }
966
1210
 
967
1211
  // src/commands/org.ts
968
- import { Command as Command6 } from "commander";
1212
+ import { Command as Command8 } from "commander";
969
1213
  async function client2(globals) {
970
1214
  const cfg = await resolveConfig({
971
1215
  apiKeyFlag: globals.apiKey ?? null,
@@ -980,7 +1224,7 @@ async function client2(globals) {
980
1224
  return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
981
1225
  }
982
1226
  function orgCommand() {
983
- const cmd = new Command6("org").description(
1227
+ const cmd = new Command8("org").description(
984
1228
  "Manage the OpenBat tenant org (rename, members, invitations). Requires PAT."
985
1229
  );
986
1230
  cmd.command("list").description("List orgs the current PAT's user belongs to").action(async function() {
@@ -1088,7 +1332,7 @@ function orgCommand() {
1088
1332
  }
1089
1333
 
1090
1334
  // src/commands/write.ts
1091
- import { Command as Command7 } from "commander";
1335
+ import { Command as Command9 } from "commander";
1092
1336
  async function client3(globals) {
1093
1337
  const cfg = await resolveConfig({
1094
1338
  apiKeyFlag: globals.apiKey ?? null,
@@ -1121,7 +1365,7 @@ function surfacePlaintext(plaintext, label) {
1121
1365
  );
1122
1366
  }
1123
1367
  function chatbotsCommand() {
1124
- const cmd = new Command7("chatbots").description(
1368
+ const cmd = new Command9("chatbots").description(
1125
1369
  "List, create, delete chatbots in the current scope"
1126
1370
  );
1127
1371
  cmd.command("list").description("List every chatbot the credential can reach").action(async function() {
@@ -1169,7 +1413,7 @@ function chatbotsCommand() {
1169
1413
  return cmd;
1170
1414
  }
1171
1415
  function webhooksCommand() {
1172
- const cmd = new Command7("webhooks").description("Manage webhooks for a chatbot");
1416
+ const cmd = new Command9("webhooks").description("Manage webhooks for a chatbot");
1173
1417
  cmd.command("list").action(async function() {
1174
1418
  try {
1175
1419
  const chatbotId = requireChatbotId(this);
@@ -1213,7 +1457,7 @@ function webhooksCommand() {
1213
1457
  return cmd;
1214
1458
  }
1215
1459
  function settingsCommand() {
1216
- const cmd = new Command7("settings").description(
1460
+ const cmd = new Command9("settings").description(
1217
1461
  "Manage chatbot settings + per-chatbot keys"
1218
1462
  );
1219
1463
  const keys = cmd.command("keys").description("Manage API keys for a chatbot");
@@ -1307,7 +1551,7 @@ function settingsCommand() {
1307
1551
  return cmd;
1308
1552
  }
1309
1553
  function workflowsCommand() {
1310
- const cmd = new Command7("workflows").description("Manage workflows");
1554
+ const cmd = new Command9("workflows").description("Manage workflows");
1311
1555
  cmd.command("list").action(async function() {
1312
1556
  try {
1313
1557
  const chatbotId = requireChatbotId(this);
@@ -1347,7 +1591,7 @@ function workflowsCommand() {
1347
1591
  return cmd;
1348
1592
  }
1349
1593
  function reportsCommand() {
1350
- const cmd = new Command7("reports").description("Manage AI reports");
1594
+ const cmd = new Command9("reports").description("Manage AI reports");
1351
1595
  cmd.command("list").action(async function() {
1352
1596
  try {
1353
1597
  const chatbotId = requireChatbotId(this);
@@ -1385,7 +1629,7 @@ Created report. View it (org members only):
1385
1629
  return cmd;
1386
1630
  }
1387
1631
  function analysisCommand() {
1388
- const cmd = new Command7("analysis").description("Manage analysis definitions");
1632
+ const cmd = new Command9("analysis").description("Manage analysis definitions");
1389
1633
  cmd.command("list").option("--type <t>", "intent | flag | assistant_outcome | assistant_issue").option("--pending", "Include pending suggestions").action(async function(opts) {
1390
1634
  try {
1391
1635
  const chatbotId = requireChatbotId(this);
@@ -1427,7 +1671,7 @@ function analysisCommand() {
1427
1671
  return cmd;
1428
1672
  }
1429
1673
  function usersCommand() {
1430
- const cmd = new Command7("users").description(
1674
+ const cmd = new Command9("users").description(
1431
1675
  "List external users (chatbot customers) with health metrics"
1432
1676
  );
1433
1677
  cmd.command("list").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) {
@@ -1460,7 +1704,7 @@ Total: ${result.total}
1460
1704
  return cmd;
1461
1705
  }
1462
1706
  function sdkCommand() {
1463
- const cmd = new Command7("sdk").description(
1707
+ const cmd = new Command9("sdk").description(
1464
1708
  "Help install and verify the OpenBat SDK in a target project"
1465
1709
  );
1466
1710
  cmd.command("install-instructions").description("Print markdown the calling agent can follow").option(
@@ -1570,10 +1814,10 @@ Timed out after ${timeoutSec}s \u2014 no events yet. Confirm OPENBAT_API_KEY and
1570
1814
  }
1571
1815
 
1572
1816
  // src/index.ts
1573
- var program = new Command8();
1817
+ var program = new Command10();
1574
1818
  program.name("openbat").description(
1575
1819
  "Query OpenBat chatbot data \u2014 conversations, sentiment, analytics, exports."
1576
- ).version("0.2.3").option("--api-key <key>", "Override the stored Read API key (footgun \u2014 leaks into shell history)").option(
1820
+ ).version("0.3.0").option("--api-key <key>", "Override the stored Read API key (footgun \u2014 leaks into shell history)").option(
1577
1821
  "--base-url <url>",
1578
1822
  "Override the OpenBat API base URL (defaults to ~/.openbatrc or https://openbat.dev)"
1579
1823
  ).option(
@@ -1592,6 +1836,8 @@ program.addCommand(orgCommand());
1592
1836
  program.addCommand(chatbotCommand());
1593
1837
  program.addCommand(chatbotsCommand());
1594
1838
  program.addCommand(conversationsCommand());
1839
+ program.addCommand(reviewCommand());
1840
+ program.addCommand(promptsCommand());
1595
1841
  program.addCommand(usersCommand());
1596
1842
  program.addCommand(settingsCommand());
1597
1843
  program.addCommand(webhooksCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openbat/cli",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line tool for querying OpenBat chatbot data — conversations, sentiment, analytics, exports",
5
5
  "bin": {
6
6
  "openbat": "bin/openbat"