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