@openbat/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -0
- package/bin/openbat +3 -0
- package/dist/api-client.d.mts +41 -0
- package/dist/api-client.d.ts +41 -0
- package/dist/api-client.js +175 -0
- package/dist/api-client.mjs +6 -0
- package/dist/chunk-CRJZM45P.mjs +152 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1221 -0
- package/dist/index.mjs +1051 -0
- package/package.json +65 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApiClient
|
|
4
|
+
} from "./chunk-CRJZM45P.mjs";
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
import { Command as Command6 } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/commands/config.ts
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import { promises as fs, statSync, chmodSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
var CONFIG_PATH = join(homedir(), ".openbatrc");
|
|
17
|
+
var DEFAULT_BASE_URL = "https://app.openbat.com";
|
|
18
|
+
async function readConfig() {
|
|
19
|
+
try {
|
|
20
|
+
const st = statSync(CONFIG_PATH);
|
|
21
|
+
if ((st.mode & 63) !== 0) {
|
|
22
|
+
try {
|
|
23
|
+
chmodSync(CONFIG_PATH, 384);
|
|
24
|
+
} catch {
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
"warning: ~/.openbatrc has loose permissions (not 0600). Run `chmod 600 ~/.openbatrc`.\n"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const raw = await fs.readFile(CONFIG_PATH, "utf8");
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.code === "ENOENT") return {};
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function writeConfig(cfg) {
|
|
38
|
+
const json = JSON.stringify(cfg, null, 2);
|
|
39
|
+
await fs.writeFile(CONFIG_PATH, json + "\n", { mode: 384 });
|
|
40
|
+
}
|
|
41
|
+
async function resolveConfig(opts) {
|
|
42
|
+
const file = await readConfig();
|
|
43
|
+
let apiKey = null;
|
|
44
|
+
let apiKeySource = "missing";
|
|
45
|
+
if (opts.apiKeyFlag) {
|
|
46
|
+
apiKey = opts.apiKeyFlag;
|
|
47
|
+
apiKeySource = "flag";
|
|
48
|
+
} else if (process.env.OPENBAT_API_KEY) {
|
|
49
|
+
apiKey = process.env.OPENBAT_API_KEY;
|
|
50
|
+
apiKeySource = "env";
|
|
51
|
+
} else if (file.apiKey) {
|
|
52
|
+
apiKey = file.apiKey;
|
|
53
|
+
apiKeySource = "file";
|
|
54
|
+
}
|
|
55
|
+
let baseUrl = DEFAULT_BASE_URL;
|
|
56
|
+
let baseUrlSource = "default";
|
|
57
|
+
if (opts.baseUrlFlag) {
|
|
58
|
+
baseUrl = opts.baseUrlFlag;
|
|
59
|
+
baseUrlSource = "flag";
|
|
60
|
+
} else if (process.env.OPENBAT_BASE_URL) {
|
|
61
|
+
baseUrl = process.env.OPENBAT_BASE_URL;
|
|
62
|
+
baseUrlSource = "env";
|
|
63
|
+
} else if (file.baseUrl) {
|
|
64
|
+
baseUrl = file.baseUrl;
|
|
65
|
+
baseUrlSource = "file";
|
|
66
|
+
}
|
|
67
|
+
return { apiKey, baseUrl, apiKeySource, baseUrlSource };
|
|
68
|
+
}
|
|
69
|
+
async function setApiKey(apiKey) {
|
|
70
|
+
if (apiKey.startsWith("ob_live_")) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"This looks like an ingest key (ob_live_\u2026). The CLI uses Read / Admin / PAT keys \u2014 generate one in Settings \u2192 API Keys."
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!/^ob_(?:read|admin|pat)_[0-9a-f]{32}$/.test(apiKey)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"That doesn't look like a valid OpenBat key. Expected format: ob_(read|admin|pat)_<32 hex chars>."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const file = await readConfig();
|
|
81
|
+
file.apiKey = apiKey;
|
|
82
|
+
await writeConfig(file);
|
|
83
|
+
}
|
|
84
|
+
async function setBaseUrl(baseUrl) {
|
|
85
|
+
try {
|
|
86
|
+
new URL(baseUrl);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error(`Invalid URL: ${baseUrl}`);
|
|
89
|
+
}
|
|
90
|
+
const file = await readConfig();
|
|
91
|
+
file.baseUrl = baseUrl;
|
|
92
|
+
await writeConfig(file);
|
|
93
|
+
}
|
|
94
|
+
function configPath() {
|
|
95
|
+
return CONFIG_PATH;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/format.ts
|
|
99
|
+
var ANSI = {
|
|
100
|
+
dim: "\x1B[2m",
|
|
101
|
+
bold: "\x1B[1m",
|
|
102
|
+
reset: "\x1B[0m"
|
|
103
|
+
};
|
|
104
|
+
function emit(value, opts = {}) {
|
|
105
|
+
if (opts.json || !process.stdout.isTTY) {
|
|
106
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(prettyPrint(value) + "\n");
|
|
110
|
+
}
|
|
111
|
+
function prettyPrint(value) {
|
|
112
|
+
if (value === null || value === void 0) return "(empty)";
|
|
113
|
+
if (Array.isArray(value)) return prettyArray(value);
|
|
114
|
+
if (typeof value === "object") return prettyObject(value);
|
|
115
|
+
return String(value);
|
|
116
|
+
}
|
|
117
|
+
function prettyObject(obj) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
const keyWidth = Math.max(...Object.keys(obj).map((k) => k.length), 0);
|
|
120
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
121
|
+
const padded = k.padEnd(keyWidth);
|
|
122
|
+
const rendered = v === null ? `${ANSI.dim}null${ANSI.reset}` : typeof v === "object" ? indent(prettyPrint(v), keyWidth + 2) : String(v);
|
|
123
|
+
lines.push(`${ANSI.bold}${padded}${ANSI.reset} ${rendered}`);
|
|
124
|
+
}
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
function prettyArray(arr) {
|
|
128
|
+
if (arr.length === 0) return `${ANSI.dim}(no rows)${ANSI.reset}`;
|
|
129
|
+
const first = arr[0];
|
|
130
|
+
if (first && typeof first === "object" && !Array.isArray(first)) {
|
|
131
|
+
const keys = Object.keys(first);
|
|
132
|
+
const isFlat = arr.every(
|
|
133
|
+
(row) => row && typeof row === "object" && !Array.isArray(row) && keys.every((k) => {
|
|
134
|
+
const v = row[k];
|
|
135
|
+
return v === null || ["string", "number", "boolean"].includes(typeof v);
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
if (isFlat) return renderTable(arr, keys);
|
|
139
|
+
}
|
|
140
|
+
return arr.map((row) => prettyPrint(row)).join("\n---\n");
|
|
141
|
+
}
|
|
142
|
+
function renderTable(rows, keys) {
|
|
143
|
+
const cellAt = (row, k) => {
|
|
144
|
+
const v = row[k];
|
|
145
|
+
if (v === null || v === void 0) return "\u2014";
|
|
146
|
+
return String(v);
|
|
147
|
+
};
|
|
148
|
+
const widths = keys.map(
|
|
149
|
+
(k) => Math.max(k.length, ...rows.map((r) => cellAt(r, k).length))
|
|
150
|
+
);
|
|
151
|
+
const header = keys.map((k, i) => `${ANSI.bold}${k.padEnd(widths[i])}${ANSI.reset}`).join(" ");
|
|
152
|
+
const sep = widths.map((w) => "\u2500".repeat(w)).join(" ");
|
|
153
|
+
const body = rows.map(
|
|
154
|
+
(row) => keys.map((k, i) => cellAt(row, k).padEnd(widths[i])).join(" ")
|
|
155
|
+
).join("\n");
|
|
156
|
+
return `${header}
|
|
157
|
+
${ANSI.dim}${sep}${ANSI.reset}
|
|
158
|
+
${body}`;
|
|
159
|
+
}
|
|
160
|
+
function indent(s, columns) {
|
|
161
|
+
const pad = " ".repeat(columns);
|
|
162
|
+
return s.split("\n").map((line, i) => i === 0 ? line : pad + line).join("\n");
|
|
163
|
+
}
|
|
164
|
+
function fatal(msg) {
|
|
165
|
+
process.stderr.write(`error: ${msg}
|
|
166
|
+
`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/commands/config.ts
|
|
171
|
+
function configCommand() {
|
|
172
|
+
const cmd = new Command("config").description(
|
|
173
|
+
"Manage the CLI's ~/.openbatrc settings"
|
|
174
|
+
);
|
|
175
|
+
cmd.command("set-key").description("Store your Read API key in ~/.openbatrc").option("--from-stdin", "Read the key from stdin (default and recommended)", true).option(
|
|
176
|
+
"--value <key>",
|
|
177
|
+
"Pass the key inline. Discouraged \u2014 leaks into shell history."
|
|
178
|
+
).action(async (opts) => {
|
|
179
|
+
try {
|
|
180
|
+
let key;
|
|
181
|
+
if (opts.value) {
|
|
182
|
+
key = opts.value.trim();
|
|
183
|
+
} else {
|
|
184
|
+
const chunks = [];
|
|
185
|
+
for await (const chunk of process.stdin) {
|
|
186
|
+
chunks.push(chunk);
|
|
187
|
+
}
|
|
188
|
+
key = Buffer.concat(chunks).toString("utf8").trim();
|
|
189
|
+
if (!key) {
|
|
190
|
+
fatal(
|
|
191
|
+
"No key on stdin. Try: echo 'ob_read_...' | openbat config set-key --from-stdin"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await setApiKey(key);
|
|
196
|
+
process.stdout.write(
|
|
197
|
+
`Saved Read key to ${configPath()} (mode 0600).
|
|
198
|
+
`
|
|
199
|
+
);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
cmd.command("set-url <baseUrl>").description(
|
|
205
|
+
"Override the OpenBat API base URL (default: https://app.openbat.com)"
|
|
206
|
+
).action(async (baseUrl) => {
|
|
207
|
+
try {
|
|
208
|
+
await setBaseUrl(baseUrl);
|
|
209
|
+
process.stdout.write(`Saved base URL: ${baseUrl}
|
|
210
|
+
`);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
cmd.command("show").description("Show the current resolved config (key prefix only)").action(async () => {
|
|
216
|
+
const cfg = await resolveConfig({});
|
|
217
|
+
const keyDisplay = cfg.apiKey ? `${cfg.apiKey.slice(0, 16)}\u2026<hidden>` : "(not set)";
|
|
218
|
+
emit({
|
|
219
|
+
apiKey: keyDisplay,
|
|
220
|
+
apiKeySource: cfg.apiKeySource,
|
|
221
|
+
baseUrl: cfg.baseUrl,
|
|
222
|
+
baseUrlSource: cfg.baseUrlSource,
|
|
223
|
+
configFile: configPath()
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
return cmd;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/commands/data.ts
|
|
230
|
+
import { Command as Command2 } from "commander";
|
|
231
|
+
async function client(globals) {
|
|
232
|
+
const cfg = await resolveConfig({
|
|
233
|
+
apiKeyFlag: globals.apiKey ?? null,
|
|
234
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
235
|
+
});
|
|
236
|
+
if (!cfg.apiKey) {
|
|
237
|
+
fatal(
|
|
238
|
+
"No API key configured. Run `openbat config set-key`, or pass --api-key, or set OPENBAT_API_KEY."
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
242
|
+
}
|
|
243
|
+
function formatOpts(cmd) {
|
|
244
|
+
const opts = cmd.optsWithGlobals();
|
|
245
|
+
return { json: !!opts.json };
|
|
246
|
+
}
|
|
247
|
+
function chatbotCommand() {
|
|
248
|
+
const cmd = new Command2("chatbot").description(
|
|
249
|
+
"Inspect the chatbot bound to this API key"
|
|
250
|
+
);
|
|
251
|
+
cmd.command("info").description("Show the chatbot the current API key authorizes").action(async function() {
|
|
252
|
+
try {
|
|
253
|
+
const c = await client(this.optsWithGlobals());
|
|
254
|
+
const result = await c.get(
|
|
255
|
+
"/api/v1/chatbots"
|
|
256
|
+
);
|
|
257
|
+
const list = result.chatbots ?? [];
|
|
258
|
+
emit(list[0] ?? null, formatOpts(this));
|
|
259
|
+
} catch (err) {
|
|
260
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
return cmd;
|
|
264
|
+
}
|
|
265
|
+
function conversationsCommand() {
|
|
266
|
+
const cmd = new Command2("conversations").description(
|
|
267
|
+
"Browse conversations"
|
|
268
|
+
);
|
|
269
|
+
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
|
+
try {
|
|
271
|
+
const c = await client(this.optsWithGlobals());
|
|
272
|
+
const params = new URLSearchParams({
|
|
273
|
+
page: opts.page,
|
|
274
|
+
limit: opts.limit
|
|
275
|
+
});
|
|
276
|
+
const result = await c.get(`/api/v1/conversations?${params}`);
|
|
277
|
+
if (this.optsWithGlobals().json) {
|
|
278
|
+
emit(result, { json: true });
|
|
279
|
+
} else {
|
|
280
|
+
emit(result.conversations, formatOpts(this));
|
|
281
|
+
process.stderr.write(
|
|
282
|
+
`
|
|
283
|
+
Total: ${result.total}, page ${opts.page}, limit ${opts.limit}
|
|
284
|
+
`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
cmd.command("show <id>").description("Show one conversation by id, with messages").action(async function(id) {
|
|
292
|
+
try {
|
|
293
|
+
const c = await client(this.optsWithGlobals());
|
|
294
|
+
const result = await c.get(
|
|
295
|
+
`/api/v1/conversations/${encodeURIComponent(id)}`
|
|
296
|
+
);
|
|
297
|
+
emit(result.conversation, formatOpts(this));
|
|
298
|
+
} catch (err) {
|
|
299
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
return cmd;
|
|
303
|
+
}
|
|
304
|
+
function analyticsCommand() {
|
|
305
|
+
const cmd = new Command2("analytics").description(
|
|
306
|
+
"Aggregated analytics for the bound chatbot"
|
|
307
|
+
);
|
|
308
|
+
cmd.command("overview").description("Total conversations, messages, sentiment distribution").action(async function() {
|
|
309
|
+
try {
|
|
310
|
+
const c = await client(this.optsWithGlobals());
|
|
311
|
+
const result = await c.get(
|
|
312
|
+
"/api/v1/analytics/overview"
|
|
313
|
+
);
|
|
314
|
+
emit(result, formatOpts(this));
|
|
315
|
+
} catch (err) {
|
|
316
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
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
|
+
try {
|
|
321
|
+
const c = await client(this.optsWithGlobals());
|
|
322
|
+
const params = new URLSearchParams({ days: opts.days });
|
|
323
|
+
const result = await c.get(
|
|
324
|
+
`/api/v1/analytics/sentiment?${params}`
|
|
325
|
+
);
|
|
326
|
+
emit(result, formatOpts(this));
|
|
327
|
+
} catch (err) {
|
|
328
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
return cmd;
|
|
332
|
+
}
|
|
333
|
+
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) {
|
|
335
|
+
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.");
|
|
342
|
+
const format = opts.format === "csv" ? "csv" : "json";
|
|
343
|
+
const { body } = await c.getRaw(
|
|
344
|
+
`/api/v1/export/${id}?format=${format}`
|
|
345
|
+
);
|
|
346
|
+
if (opts.out) {
|
|
347
|
+
const { createWriteStream } = await import("fs");
|
|
348
|
+
const { Writable } = await import("stream");
|
|
349
|
+
const { Readable } = await import("stream");
|
|
350
|
+
const file = createWriteStream(opts.out);
|
|
351
|
+
const nodeReadable = Readable.fromWeb(
|
|
352
|
+
body
|
|
353
|
+
);
|
|
354
|
+
await new Promise((resolve, reject) => {
|
|
355
|
+
nodeReadable.pipe(file);
|
|
356
|
+
file.on("finish", () => resolve());
|
|
357
|
+
file.on("error", (e) => reject(e));
|
|
358
|
+
});
|
|
359
|
+
process.stderr.write(`Wrote ${opts.out}
|
|
360
|
+
`);
|
|
361
|
+
} else {
|
|
362
|
+
const reader = body.getReader();
|
|
363
|
+
while (true) {
|
|
364
|
+
const { done, value } = await reader.read();
|
|
365
|
+
if (done) break;
|
|
366
|
+
process.stdout.write(value);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} catch (err) {
|
|
370
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
return cmd;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/commands/auth.ts
|
|
377
|
+
import { Command as Command3 } from "commander";
|
|
378
|
+
async function client2(globals) {
|
|
379
|
+
const cfg = await resolveConfig({
|
|
380
|
+
apiKeyFlag: globals.apiKey ?? null,
|
|
381
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
382
|
+
});
|
|
383
|
+
if (!cfg.apiKey) {
|
|
384
|
+
fatal(
|
|
385
|
+
"No API key configured. Run `openbat config set-key`, pass --api-key, or set OPENBAT_API_KEY."
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
389
|
+
}
|
|
390
|
+
function detectKind(apiKey) {
|
|
391
|
+
if (apiKey.startsWith("ob_read_")) return "read";
|
|
392
|
+
if (apiKey.startsWith("ob_admin_")) return "admin";
|
|
393
|
+
if (apiKey.startsWith("ob_pat_")) return "pat";
|
|
394
|
+
return "unknown";
|
|
395
|
+
}
|
|
396
|
+
function authCommand() {
|
|
397
|
+
const cmd = new Command3("auth").description(
|
|
398
|
+
"Inspect the current credential's scope and audit history"
|
|
399
|
+
);
|
|
400
|
+
cmd.command("whoami").description("Print the credential kind, target chatbots / orgs").action(async function() {
|
|
401
|
+
try {
|
|
402
|
+
const globals = this.optsWithGlobals();
|
|
403
|
+
const cfg = await resolveConfig({
|
|
404
|
+
apiKeyFlag: globals.apiKey ?? null,
|
|
405
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
406
|
+
});
|
|
407
|
+
if (!cfg.apiKey) fatal("No API key configured.");
|
|
408
|
+
const kind = detectKind(cfg.apiKey);
|
|
409
|
+
const c = new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
410
|
+
if (kind === "pat") {
|
|
411
|
+
const orgs = await c.get("/api/v1/orgs");
|
|
412
|
+
const chatbots = await c.get(
|
|
413
|
+
"/api/v1/chatbots"
|
|
414
|
+
);
|
|
415
|
+
emit(
|
|
416
|
+
{
|
|
417
|
+
kind,
|
|
418
|
+
keyPrefix: `${cfg.apiKey.slice(0, 16)}\u2026<hidden>`,
|
|
419
|
+
orgs: orgs.orgs,
|
|
420
|
+
chatbots: chatbots.chatbots
|
|
421
|
+
},
|
|
422
|
+
{ json: !!globals.json }
|
|
423
|
+
);
|
|
424
|
+
} else {
|
|
425
|
+
const chatbots = await c.get(
|
|
426
|
+
"/api/v1/chatbots"
|
|
427
|
+
);
|
|
428
|
+
emit(
|
|
429
|
+
{
|
|
430
|
+
kind,
|
|
431
|
+
keyPrefix: `${cfg.apiKey.slice(0, 16)}\u2026<hidden>`,
|
|
432
|
+
chatbots: chatbots.chatbots
|
|
433
|
+
},
|
|
434
|
+
{ json: !!globals.json }
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
} catch (err) {
|
|
438
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
cmd.command("audit-log").description("Show recent audit-log entries for the current user").option("--days <n>", "Look-back window in days", "7").action(async function() {
|
|
442
|
+
try {
|
|
443
|
+
process.stderr.write(
|
|
444
|
+
"audit-log: endpoint not yet exposed via HTTP \u2014 query `select * from api_audit_log` in Supabase Studio for now.\n"
|
|
445
|
+
);
|
|
446
|
+
const _c = await client2(this.optsWithGlobals());
|
|
447
|
+
void _c;
|
|
448
|
+
} catch (err) {
|
|
449
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
return cmd;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/commands/org.ts
|
|
456
|
+
import { Command as Command4 } from "commander";
|
|
457
|
+
async function client3(globals) {
|
|
458
|
+
const cfg = await resolveConfig({
|
|
459
|
+
apiKeyFlag: globals.apiKey ?? null,
|
|
460
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
461
|
+
});
|
|
462
|
+
if (!cfg.apiKey) {
|
|
463
|
+
fatal("No API key configured. Run `openbat config set-key`.");
|
|
464
|
+
}
|
|
465
|
+
if (!cfg.apiKey.startsWith("ob_pat_")) {
|
|
466
|
+
fatal("Org commands require a PAT key (ob_pat_\u2026). Set one with `openbat config set-key`.");
|
|
467
|
+
}
|
|
468
|
+
return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
469
|
+
}
|
|
470
|
+
function orgCommand() {
|
|
471
|
+
const cmd = new Command4("org").description(
|
|
472
|
+
"Manage the OpenBat tenant org (rename, members, invitations). Requires PAT."
|
|
473
|
+
);
|
|
474
|
+
cmd.command("list").description("List orgs the current PAT's user belongs to").action(async function() {
|
|
475
|
+
try {
|
|
476
|
+
const globals = this.optsWithGlobals();
|
|
477
|
+
const c = await client3(globals);
|
|
478
|
+
const result = await c.get("/api/v1/orgs");
|
|
479
|
+
emit(result.orgs, { json: !!globals.json });
|
|
480
|
+
} catch (err) {
|
|
481
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
cmd.command("show").description("Show the active org (id, name, members)").action(async function() {
|
|
485
|
+
try {
|
|
486
|
+
const globals = this.optsWithGlobals();
|
|
487
|
+
const c = await client3(globals);
|
|
488
|
+
const result = await c.get(
|
|
489
|
+
"/api/v1/orgs/active"
|
|
490
|
+
);
|
|
491
|
+
emit(
|
|
492
|
+
{ org: result.org, members: result.members },
|
|
493
|
+
{ json: !!globals.json }
|
|
494
|
+
);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
cmd.command("rename").description("Rename an org (owner only)").requiredOption("--id <orgId>", "Org id").requiredOption("--name <name>", "New name").action(async function(opts) {
|
|
500
|
+
try {
|
|
501
|
+
const globals = this.optsWithGlobals();
|
|
502
|
+
const c = await client3(globals);
|
|
503
|
+
await c.patch(`/api/v1/orgs/${opts.id}`, { name: opts.name });
|
|
504
|
+
emit({ ok: true, renamed: opts.id }, { json: !!globals.json });
|
|
505
|
+
} catch (err) {
|
|
506
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
const members = cmd.command("members").description("Manage org members");
|
|
510
|
+
members.command("list").requiredOption("--id <orgId>", "Org id").action(async function(opts) {
|
|
511
|
+
try {
|
|
512
|
+
const globals = this.optsWithGlobals();
|
|
513
|
+
const c = await client3(globals);
|
|
514
|
+
const result = await c.get(
|
|
515
|
+
`/api/v1/orgs/${opts.id}/members`
|
|
516
|
+
);
|
|
517
|
+
emit(result.members, { json: !!globals.json });
|
|
518
|
+
} catch (err) {
|
|
519
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
members.command("invite").requiredOption("--id <orgId>", "Org id").requiredOption("--email <email>", "Invitee email").option("--role <role>", "member | admin", "member").action(async function(opts) {
|
|
523
|
+
try {
|
|
524
|
+
const globals = this.optsWithGlobals();
|
|
525
|
+
const c = await client3(globals);
|
|
526
|
+
const result = await c.post(
|
|
527
|
+
`/api/v1/orgs/${opts.id}/members`,
|
|
528
|
+
{ email: opts.email, role: opts.role }
|
|
529
|
+
);
|
|
530
|
+
if (!result.emailSent) {
|
|
531
|
+
process.stderr.write(
|
|
532
|
+
"[note] invitation row created \u2014 but the invitation email is currently sent only via the dashboard flow. Wire-up tracked as a follow-up.\n"
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
emit(result.invitation, { json: !!globals.json });
|
|
536
|
+
} catch (err) {
|
|
537
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
members.command("set-role").requiredOption("--id <orgId>", "Org id").requiredOption("--member <memberId>", "Member id").requiredOption("--role <role>", "admin | member").action(async function(opts) {
|
|
541
|
+
try {
|
|
542
|
+
const globals = this.optsWithGlobals();
|
|
543
|
+
const c = await client3(globals);
|
|
544
|
+
await c.patch(`/api/v1/orgs/${opts.id}/members/${opts.member}`, {
|
|
545
|
+
role: opts.role
|
|
546
|
+
});
|
|
547
|
+
emit({ ok: true }, { json: !!globals.json });
|
|
548
|
+
} catch (err) {
|
|
549
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
members.command("remove").requiredOption("--id <orgId>", "Org id").requiredOption("--member <memberId>", "Member id").action(async function(opts) {
|
|
553
|
+
try {
|
|
554
|
+
const globals = this.optsWithGlobals();
|
|
555
|
+
const c = await client3(globals);
|
|
556
|
+
await c.delete(`/api/v1/orgs/${opts.id}/members/${opts.member}`);
|
|
557
|
+
emit({ ok: true }, { json: !!globals.json });
|
|
558
|
+
} catch (err) {
|
|
559
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
const invitations = cmd.command("invitations").description("Manage pending invitations");
|
|
563
|
+
invitations.command("list").requiredOption("--id <orgId>", "Org id").action(async function(opts) {
|
|
564
|
+
try {
|
|
565
|
+
const globals = this.optsWithGlobals();
|
|
566
|
+
const c = await client3(globals);
|
|
567
|
+
const result = await c.get(
|
|
568
|
+
`/api/v1/orgs/${opts.id}/invitations`
|
|
569
|
+
);
|
|
570
|
+
emit(result.invitations, { json: !!globals.json });
|
|
571
|
+
} catch (err) {
|
|
572
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
return cmd;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/commands/write.ts
|
|
579
|
+
import { Command as Command5 } from "commander";
|
|
580
|
+
async function client4(globals) {
|
|
581
|
+
const cfg = await resolveConfig({
|
|
582
|
+
apiKeyFlag: globals.apiKey ?? null,
|
|
583
|
+
baseUrlFlag: globals.baseUrl ?? null
|
|
584
|
+
});
|
|
585
|
+
if (!cfg.apiKey) {
|
|
586
|
+
fatal("No API key configured. Run `openbat config set-key`.");
|
|
587
|
+
}
|
|
588
|
+
return new ApiClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
|
|
589
|
+
}
|
|
590
|
+
function surfacePlaintext(plaintext, label) {
|
|
591
|
+
process.stderr.write(
|
|
592
|
+
`
|
|
593
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
594
|
+
${label} (shown ONCE \u2014 store this now)
|
|
595
|
+
|
|
596
|
+
${plaintext}
|
|
597
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
598
|
+
|
|
599
|
+
`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
function chatbotsCommand() {
|
|
603
|
+
const cmd = new Command5("chatbots").description(
|
|
604
|
+
"List, create, delete chatbots in the current scope"
|
|
605
|
+
);
|
|
606
|
+
cmd.command("list").description("List every chatbot the credential can reach").action(async function() {
|
|
607
|
+
try {
|
|
608
|
+
const globals = this.optsWithGlobals();
|
|
609
|
+
const c = await client4(globals);
|
|
610
|
+
const result = await c.get(
|
|
611
|
+
"/api/v1/chatbots"
|
|
612
|
+
);
|
|
613
|
+
emit(result.chatbots, { json: !!globals.json });
|
|
614
|
+
} catch (err) {
|
|
615
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
cmd.command("create").description("Create a new chatbot (PAT scope required)").requiredOption("--name <name>", "Chatbot name").option("--website <url>").option("--docs-url <url>").option("--mcp-url <url>").option("--language <code>", "Primary language code (default: en)", "en").action(async function(opts) {
|
|
619
|
+
try {
|
|
620
|
+
const globals = this.optsWithGlobals();
|
|
621
|
+
const c = await client4(globals);
|
|
622
|
+
const result = await c.post("/api/v1/chatbots", {
|
|
623
|
+
name: opts.name,
|
|
624
|
+
websiteUrl: opts.website,
|
|
625
|
+
docsUrl: opts.docsUrl,
|
|
626
|
+
mcpUrl: opts.mcpUrl,
|
|
627
|
+
primaryLanguage: opts.language
|
|
628
|
+
});
|
|
629
|
+
surfacePlaintext(result.ingestApiKey, "Ingest API key (ob_live_*)");
|
|
630
|
+
emit(
|
|
631
|
+
{ chatbot: result.chatbot, dashboardUrl: result.dashboardUrl },
|
|
632
|
+
{ json: !!globals.json }
|
|
633
|
+
);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
cmd.command("delete <chatbotId>").description("Delete a chatbot (irreversible \u2014 cascade-deletes everything)").action(async function(chatbotId) {
|
|
639
|
+
try {
|
|
640
|
+
const globals = this.optsWithGlobals();
|
|
641
|
+
const c = await client4(globals);
|
|
642
|
+
await c.delete(`/api/v1/chatbots/${chatbotId}`);
|
|
643
|
+
emit({ ok: true, deleted: chatbotId }, { json: !!globals.json });
|
|
644
|
+
} catch (err) {
|
|
645
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
return cmd;
|
|
649
|
+
}
|
|
650
|
+
function webhooksCommand() {
|
|
651
|
+
const cmd = new Command5("webhooks").description("Manage webhooks for a chatbot");
|
|
652
|
+
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
653
|
+
try {
|
|
654
|
+
const globals = this.optsWithGlobals();
|
|
655
|
+
const c = await client4(globals);
|
|
656
|
+
const result = await c.get(
|
|
657
|
+
`/api/v1/chatbots/${opts.chatbot}/webhooks`
|
|
658
|
+
);
|
|
659
|
+
emit(result.webhooks, { json: !!globals.json });
|
|
660
|
+
} catch (err) {
|
|
661
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
cmd.command("create").requiredOption("--chatbot <id>", "Chatbot id").requiredOption("--name <name>").requiredOption("--url <url>").option("--type <type>", "discord | slack | custom", "custom").action(async function(opts) {
|
|
665
|
+
try {
|
|
666
|
+
const globals = this.optsWithGlobals();
|
|
667
|
+
const c = await client4(globals);
|
|
668
|
+
const result = await c.post(`/api/v1/chatbots/${opts.chatbot}/webhooks`, {
|
|
669
|
+
name: opts.name,
|
|
670
|
+
url: opts.url,
|
|
671
|
+
type: opts.type
|
|
672
|
+
});
|
|
673
|
+
surfacePlaintext(result.webhook.signing_secret, "Webhook signing secret");
|
|
674
|
+
emit(result.webhook, { json: !!globals.json });
|
|
675
|
+
} catch (err) {
|
|
676
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
cmd.command("delete").requiredOption("--chatbot <id>", "Chatbot id").requiredOption("--webhook <id>", "Webhook id").action(async function(opts) {
|
|
680
|
+
try {
|
|
681
|
+
const globals = this.optsWithGlobals();
|
|
682
|
+
const c = await client4(globals);
|
|
683
|
+
await c.delete(`/api/v1/chatbots/${opts.chatbot}/webhooks/${opts.webhook}`);
|
|
684
|
+
emit({ ok: true, deleted: opts.webhook }, { json: !!globals.json });
|
|
685
|
+
} catch (err) {
|
|
686
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
return cmd;
|
|
690
|
+
}
|
|
691
|
+
function settingsCommand() {
|
|
692
|
+
const cmd = new Command5("settings").description(
|
|
693
|
+
"Manage chatbot settings + per-chatbot keys"
|
|
694
|
+
);
|
|
695
|
+
const keys = cmd.command("keys").description("Manage API keys for a chatbot");
|
|
696
|
+
keys.command("rotate-ingest").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
697
|
+
try {
|
|
698
|
+
const globals = this.optsWithGlobals();
|
|
699
|
+
const c = await client4(globals);
|
|
700
|
+
const result = await c.post(
|
|
701
|
+
`/api/v1/chatbots/${opts.chatbot}/keys/ingest/rotate`,
|
|
702
|
+
{}
|
|
703
|
+
);
|
|
704
|
+
surfacePlaintext(result.plaintext, "New ingest key (ob_live_*)");
|
|
705
|
+
emit({ prefix: result.prefix }, { json: !!globals.json });
|
|
706
|
+
} catch (err) {
|
|
707
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
keys.command("generate-read").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
711
|
+
try {
|
|
712
|
+
const globals = this.optsWithGlobals();
|
|
713
|
+
const c = await client4(globals);
|
|
714
|
+
const result = await c.post(
|
|
715
|
+
`/api/v1/chatbots/${opts.chatbot}/keys/read`,
|
|
716
|
+
{}
|
|
717
|
+
);
|
|
718
|
+
surfacePlaintext(result.plaintext, "New read key (ob_read_*)");
|
|
719
|
+
emit({ prefix: result.prefix }, { json: !!globals.json });
|
|
720
|
+
} catch (err) {
|
|
721
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
keys.command("generate-admin").requiredOption("--chatbot <id>", "Chatbot id").requiredOption("--name <name>", "Human-friendly name (e.g. 'CI key')").option("--expires-in-days <n>", "Auto-expire after N days").action(async function(opts) {
|
|
725
|
+
try {
|
|
726
|
+
const globals = this.optsWithGlobals();
|
|
727
|
+
const c = await client4(globals);
|
|
728
|
+
const result = await c.post(`/api/v1/chatbots/${opts.chatbot}/admin-keys`, {
|
|
729
|
+
name: opts.name,
|
|
730
|
+
expiresInDays: opts.expiresInDays ? Number(opts.expiresInDays) : void 0
|
|
731
|
+
});
|
|
732
|
+
surfacePlaintext(result.plaintext, "New admin key (ob_admin_*)");
|
|
733
|
+
emit(result.key, { json: !!globals.json });
|
|
734
|
+
} catch (err) {
|
|
735
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
keys.command("list-admin").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
739
|
+
try {
|
|
740
|
+
const globals = this.optsWithGlobals();
|
|
741
|
+
const c = await client4(globals);
|
|
742
|
+
const result = await c.get(
|
|
743
|
+
`/api/v1/chatbots/${opts.chatbot}/admin-keys`
|
|
744
|
+
);
|
|
745
|
+
emit(result.keys, { json: !!globals.json });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
keys.command("revoke-admin").requiredOption("--chatbot <id>", "Chatbot id").requiredOption("--key <keyId>", "Admin key id").action(async function(opts) {
|
|
751
|
+
try {
|
|
752
|
+
const globals = this.optsWithGlobals();
|
|
753
|
+
const c = await client4(globals);
|
|
754
|
+
await c.delete(`/api/v1/chatbots/${opts.chatbot}/admin-keys/${opts.key}`);
|
|
755
|
+
emit({ ok: true, revoked: opts.key }, { json: !!globals.json });
|
|
756
|
+
} catch (err) {
|
|
757
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
cmd.command("update").description("Patch a chatbot's settings JSONB").requiredOption("--chatbot <id>", "Chatbot id").option("--description <text>").option("--website-url <url>").option("--language <code>").action(async function(opts) {
|
|
761
|
+
try {
|
|
762
|
+
const settings = {};
|
|
763
|
+
if (opts.description) settings.description = opts.description;
|
|
764
|
+
if (opts.websiteUrl) settings.website_url = opts.websiteUrl;
|
|
765
|
+
if (opts.language) settings.primaryLanguage = opts.language;
|
|
766
|
+
if (Object.keys(settings).length === 0) {
|
|
767
|
+
fatal("Provide at least one setting to update.");
|
|
768
|
+
}
|
|
769
|
+
const globals = this.optsWithGlobals();
|
|
770
|
+
const c = await client4(globals);
|
|
771
|
+
await c.patch(`/api/v1/chatbots/${opts.chatbot}/settings`, { settings });
|
|
772
|
+
emit({ ok: true }, { json: !!globals.json });
|
|
773
|
+
} catch (err) {
|
|
774
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
return cmd;
|
|
778
|
+
}
|
|
779
|
+
function workflowsCommand() {
|
|
780
|
+
const cmd = new Command5("workflows").description("Manage workflows");
|
|
781
|
+
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
782
|
+
try {
|
|
783
|
+
const globals = this.optsWithGlobals();
|
|
784
|
+
const c = await client4(globals);
|
|
785
|
+
const result = await c.get(
|
|
786
|
+
`/api/v1/chatbots/${opts.chatbot}/workflows`
|
|
787
|
+
);
|
|
788
|
+
emit(result.workflows, { json: !!globals.json });
|
|
789
|
+
} catch (err) {
|
|
790
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
cmd.command("create").description("Create a workflow from a built-in template").requiredOption("--chatbot <id>", "Chatbot id").requiredOption("--name <name>").requiredOption(
|
|
794
|
+
"--template <name>",
|
|
795
|
+
"flag-to-webhook | outcome-to-webhook | sentiment-drop-to-webhook"
|
|
796
|
+
).requiredOption(
|
|
797
|
+
"--trigger-value <v>",
|
|
798
|
+
"Flag value / outcome value / sentiment threshold"
|
|
799
|
+
).requiredOption("--webhook <id>", "Webhook id to fire").option("--message <tpl>", "Message template (supports {{user.id}}, etc.)").action(async function(opts) {
|
|
800
|
+
try {
|
|
801
|
+
const globals = this.optsWithGlobals();
|
|
802
|
+
const c = await client4(globals);
|
|
803
|
+
const result = await c.post(`/api/v1/chatbots/${opts.chatbot}/workflows`, {
|
|
804
|
+
name: opts.name,
|
|
805
|
+
template: opts.template,
|
|
806
|
+
triggerValue: opts.triggerValue,
|
|
807
|
+
webhookId: opts.webhook,
|
|
808
|
+
messageTemplate: opts.message
|
|
809
|
+
});
|
|
810
|
+
emit(result, { json: !!globals.json });
|
|
811
|
+
} catch (err) {
|
|
812
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
return cmd;
|
|
816
|
+
}
|
|
817
|
+
function reportsCommand() {
|
|
818
|
+
const cmd = new Command5("reports").description("Manage AI reports");
|
|
819
|
+
cmd.command("list").requiredOption("--chatbot <id>", "Chatbot id").action(async function(opts) {
|
|
820
|
+
try {
|
|
821
|
+
const globals = this.optsWithGlobals();
|
|
822
|
+
const c = await client4(globals);
|
|
823
|
+
const result = await c.get(
|
|
824
|
+
`/api/v1/chatbots/${opts.chatbot}/reports`
|
|
825
|
+
);
|
|
826
|
+
emit(result.reports, { json: !!globals.json });
|
|
827
|
+
} catch (err) {
|
|
828
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
cmd.command("create").description("Create a new AI report; returns the org-private dashboard URL").requiredOption("--chatbot <id>", "Chatbot id").option("--name <name>", "Report name", "Untitled Report").action(async function(opts) {
|
|
832
|
+
try {
|
|
833
|
+
const globals = this.optsWithGlobals();
|
|
834
|
+
const c = await client4(globals);
|
|
835
|
+
const result = await c.post(`/api/v1/chatbots/${opts.chatbot}/reports`, { name: opts.name });
|
|
836
|
+
process.stderr.write(
|
|
837
|
+
`
|
|
838
|
+
Created report. View it (org members only):
|
|
839
|
+
${result.dashboardUrl}
|
|
840
|
+
|
|
841
|
+
`
|
|
842
|
+
);
|
|
843
|
+
emit(
|
|
844
|
+
{ report: result.report, dashboardUrl: result.dashboardUrl },
|
|
845
|
+
{ json: !!globals.json }
|
|
846
|
+
);
|
|
847
|
+
} catch (err) {
|
|
848
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
return cmd;
|
|
852
|
+
}
|
|
853
|
+
function analysisCommand() {
|
|
854
|
+
const cmd = new Command5("analysis").description("Manage analysis definitions");
|
|
855
|
+
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
|
+
try {
|
|
857
|
+
const globals = this.optsWithGlobals();
|
|
858
|
+
const c = await client4(globals);
|
|
859
|
+
const qs = new URLSearchParams();
|
|
860
|
+
if (opts.type) qs.set("type", opts.type);
|
|
861
|
+
if (opts.pending) qs.set("pending", "true");
|
|
862
|
+
const result = await c.get(
|
|
863
|
+
`/api/v1/chatbots/${opts.chatbot}/analysis-definitions${qs.size ? `?${qs}` : ""}`
|
|
864
|
+
);
|
|
865
|
+
emit(result.definitions, { json: !!globals.json });
|
|
866
|
+
} catch (err) {
|
|
867
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
cmd.command("add").requiredOption("--chatbot <id>", "Chatbot id").requiredOption(
|
|
871
|
+
"--type <t>",
|
|
872
|
+
"intent | flag | assistant_outcome | assistant_issue"
|
|
873
|
+
).requiredOption("--name <slug>", "snake_case slug").requiredOption("--display-name <text>").requiredOption("--description <text>").action(async function(opts) {
|
|
874
|
+
try {
|
|
875
|
+
const globals = this.optsWithGlobals();
|
|
876
|
+
const c = await client4(globals);
|
|
877
|
+
const result = await c.post(
|
|
878
|
+
`/api/v1/chatbots/${opts.chatbot}/analysis-definitions`,
|
|
879
|
+
{
|
|
880
|
+
analysisType: opts.type,
|
|
881
|
+
name: opts.name,
|
|
882
|
+
displayName: opts.displayName,
|
|
883
|
+
description: opts.description
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
emit(result, { json: !!globals.json });
|
|
887
|
+
} catch (err) {
|
|
888
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
return cmd;
|
|
892
|
+
}
|
|
893
|
+
function usersCommand() {
|
|
894
|
+
const cmd = new Command5("users").description(
|
|
895
|
+
"List external users (chatbot customers) with health metrics"
|
|
896
|
+
);
|
|
897
|
+
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) {
|
|
898
|
+
try {
|
|
899
|
+
const globals = this.optsWithGlobals();
|
|
900
|
+
const c = await client4(globals);
|
|
901
|
+
const qs = new URLSearchParams();
|
|
902
|
+
let from = opts.from;
|
|
903
|
+
if (!from && opts.days) {
|
|
904
|
+
from = new Date(
|
|
905
|
+
Date.now() - Number(opts.days) * 864e5
|
|
906
|
+
).toISOString();
|
|
907
|
+
}
|
|
908
|
+
if (from) qs.set("from", from);
|
|
909
|
+
if (opts.to) qs.set("to", opts.to);
|
|
910
|
+
if (opts.search) qs.set("search", opts.search);
|
|
911
|
+
qs.set("limit", opts.limit);
|
|
912
|
+
const result = await c.get(
|
|
913
|
+
`/api/v1/chatbots/${opts.chatbot}/external-users?${qs}`
|
|
914
|
+
);
|
|
915
|
+
emit(result.users, { json: !!globals.json });
|
|
916
|
+
process.stderr.write(`
|
|
917
|
+
Total: ${result.total}
|
|
918
|
+
`);
|
|
919
|
+
} catch (err) {
|
|
920
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
return cmd;
|
|
924
|
+
}
|
|
925
|
+
function sdkCommand() {
|
|
926
|
+
const cmd = new Command5("sdk").description(
|
|
927
|
+
"Help install and verify the OpenBat SDK in a target project"
|
|
928
|
+
);
|
|
929
|
+
cmd.command("install-instructions").description("Print markdown the calling agent can follow").option(
|
|
930
|
+
"--framework <name>",
|
|
931
|
+
"next | node | vercel-ai-sdk (default: next)",
|
|
932
|
+
"next"
|
|
933
|
+
).option("--chatbot <id>", "Chatbot id (for the example snippet)").action(async function(opts) {
|
|
934
|
+
const chatbotId = opts.chatbot ?? "<chatbotId>";
|
|
935
|
+
const snippetNext = `
|
|
936
|
+
1. Install the SDK:
|
|
937
|
+
|
|
938
|
+
\`\`\`bash
|
|
939
|
+
npm install @openbat/sdk
|
|
940
|
+
\`\`\`
|
|
941
|
+
|
|
942
|
+
2. Add the ingest key to \`.env.local\` (mode 0600):
|
|
943
|
+
|
|
944
|
+
\`\`\`
|
|
945
|
+
OPENBAT_API_KEY=ob_live_\u2026
|
|
946
|
+
\`\`\`
|
|
947
|
+
|
|
948
|
+
3. Capture conversations after each LLM turn (e.g. in your Next.js
|
|
949
|
+
API route or server action):
|
|
950
|
+
|
|
951
|
+
\`\`\`ts
|
|
952
|
+
import { OpenBat } from "@openbat/sdk";
|
|
953
|
+
|
|
954
|
+
const openbat = new OpenBat({
|
|
955
|
+
apiKey: process.env.OPENBAT_API_KEY!,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
await openbat.recordMessages({
|
|
959
|
+
conversationId, // your own stable id per conversation
|
|
960
|
+
user: { id: userId },
|
|
961
|
+
messages: [
|
|
962
|
+
{ role: "user", content: userText },
|
|
963
|
+
{ role: "assistant", content: assistantText },
|
|
964
|
+
],
|
|
965
|
+
});
|
|
966
|
+
\`\`\`
|
|
967
|
+
|
|
968
|
+
4. Verify with \`openbat sdk verify --chatbot ${chatbotId} --timeout 60\`.
|
|
969
|
+
`;
|
|
970
|
+
const snippetWrapper = `
|
|
971
|
+
1. \`npm install @openbat/sdk ai\`
|
|
972
|
+
|
|
973
|
+
2. Wrap your AI SDK chat call:
|
|
974
|
+
|
|
975
|
+
\`\`\`ts
|
|
976
|
+
import { OpenBat, withOpenBat } from "@openbat/sdk";
|
|
977
|
+
|
|
978
|
+
const openbat = new OpenBat({ apiKey: process.env.OPENBAT_API_KEY! });
|
|
979
|
+
|
|
980
|
+
const result = await withOpenBat(openbat, { conversationId }, () =>
|
|
981
|
+
streamText({ model, messages, system }),
|
|
982
|
+
);
|
|
983
|
+
\`\`\`
|
|
984
|
+
`;
|
|
985
|
+
const out = opts.framework === "vercel-ai-sdk" ? snippetWrapper : snippetNext;
|
|
986
|
+
process.stdout.write(out);
|
|
987
|
+
});
|
|
988
|
+
cmd.command("verify").description("Check whether any event has arrived for the chatbot yet").requiredOption("--chatbot <id>", "Chatbot id").option("--timeout <n>", "Seconds to wait (default: 60)", "60").action(async function(opts) {
|
|
989
|
+
try {
|
|
990
|
+
const globals = this.optsWithGlobals();
|
|
991
|
+
const c = await client4(globals);
|
|
992
|
+
const deadline = Date.now() + Number(opts.timeout) * 1e3;
|
|
993
|
+
while (Date.now() < deadline) {
|
|
994
|
+
try {
|
|
995
|
+
const result = await c.get(`/api/v1/conversations?limit=1`);
|
|
996
|
+
if (result.total > 0) {
|
|
997
|
+
process.stderr.write(
|
|
998
|
+
`\u2713 First event detected. ${result.total} conversation(s) ingested.
|
|
999
|
+
`
|
|
1000
|
+
);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1006
|
+
}
|
|
1007
|
+
process.stderr.write(
|
|
1008
|
+
`Timed out after ${opts.timeout}s \u2014 no events yet. Confirm OPENBAT_API_KEY and that recordMessages is being called.
|
|
1009
|
+
`
|
|
1010
|
+
);
|
|
1011
|
+
process.exit(2);
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
return cmd;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/index.ts
|
|
1020
|
+
var program = new Command6();
|
|
1021
|
+
program.name("openbat").description(
|
|
1022
|
+
"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(
|
|
1024
|
+
"--base-url <url>",
|
|
1025
|
+
"Override the OpenBat API base URL (defaults to ~/.openbatrc or https://app.openbat.com)"
|
|
1026
|
+
).option(
|
|
1027
|
+
"--json",
|
|
1028
|
+
"Emit raw JSON instead of the TTY-friendly pretty-print"
|
|
1029
|
+
);
|
|
1030
|
+
program.addCommand(configCommand());
|
|
1031
|
+
program.addCommand(authCommand());
|
|
1032
|
+
program.addCommand(orgCommand());
|
|
1033
|
+
program.addCommand(chatbotCommand());
|
|
1034
|
+
program.addCommand(chatbotsCommand());
|
|
1035
|
+
program.addCommand(conversationsCommand());
|
|
1036
|
+
program.addCommand(usersCommand());
|
|
1037
|
+
program.addCommand(settingsCommand());
|
|
1038
|
+
program.addCommand(webhooksCommand());
|
|
1039
|
+
program.addCommand(workflowsCommand());
|
|
1040
|
+
program.addCommand(reportsCommand());
|
|
1041
|
+
program.addCommand(analysisCommand());
|
|
1042
|
+
program.addCommand(analyticsCommand());
|
|
1043
|
+
program.addCommand(exportCommand());
|
|
1044
|
+
program.addCommand(sdkCommand());
|
|
1045
|
+
program.parseAsync().catch((err) => {
|
|
1046
|
+
process.stderr.write(
|
|
1047
|
+
`error: ${err instanceof Error ? err.message : String(err)}
|
|
1048
|
+
`
|
|
1049
|
+
);
|
|
1050
|
+
process.exit(1);
|
|
1051
|
+
});
|