@botschat/botschat 0.1.21 → 0.1.22

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.
@@ -0,0 +1,1680 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command15 } from "commander";
5
+
6
+ // src/lib/config.ts
7
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ var CONFIG_DIR = join(homedir(), ".botschat");
11
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
12
+ var DEFAULT_CONFIG = {
13
+ url: "https://console.botschat.app",
14
+ token: null,
15
+ refreshToken: null,
16
+ userId: null,
17
+ e2ePassword: null,
18
+ e2eKeyBase64: null,
19
+ defaultChannel: null,
20
+ defaultSession: null
21
+ };
22
+ var _configPath = CONFIG_FILE;
23
+ function setConfigPath(path) {
24
+ _configPath = path;
25
+ }
26
+ function loadConfig() {
27
+ try {
28
+ if (existsSync(_configPath)) {
29
+ const raw = readFileSync(_configPath, "utf-8");
30
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
31
+ }
32
+ } catch {
33
+ }
34
+ return { ...DEFAULT_CONFIG };
35
+ }
36
+ function saveConfig(config) {
37
+ const dir = _configPath === CONFIG_FILE ? CONFIG_DIR : join(_configPath, "..");
38
+ if (!existsSync(dir)) {
39
+ mkdirSync(dir, { recursive: true, mode: 448 });
40
+ }
41
+ writeFileSync(_configPath, JSON.stringify(config, null, 2) + "\n", {
42
+ mode: 384
43
+ });
44
+ }
45
+ function updateConfig(partial) {
46
+ const config = loadConfig();
47
+ Object.assign(config, partial);
48
+ saveConfig(config);
49
+ return config;
50
+ }
51
+
52
+ // src/lib/output.ts
53
+ var _jsonMode = false;
54
+ function setJsonMode(on) {
55
+ _jsonMode = on;
56
+ }
57
+ function isJsonMode() {
58
+ return _jsonMode;
59
+ }
60
+ function printJson(data) {
61
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
62
+ }
63
+ function printError(msg) {
64
+ process.stderr.write(`Error: ${msg}
65
+ `);
66
+ }
67
+ function printInfo(msg) {
68
+ process.stderr.write(`${msg}
69
+ `);
70
+ }
71
+ function printTable(rows, columns) {
72
+ if (_jsonMode) {
73
+ printJson(rows);
74
+ return;
75
+ }
76
+ if (rows.length === 0) {
77
+ console.log("(no results)");
78
+ return;
79
+ }
80
+ const widths = columns.map((col) => {
81
+ const maxData = rows.reduce(
82
+ (max, row) => Math.max(max, String(row[col.key] ?? "").length),
83
+ 0
84
+ );
85
+ return col.width ?? Math.max(col.label.length, Math.min(maxData, 60));
86
+ });
87
+ const header = columns.map((col, i) => col.label.padEnd(widths[i])).join(" ");
88
+ console.log(header);
89
+ console.log(columns.map((_, i) => "\u2500".repeat(widths[i])).join(" "));
90
+ for (const row of rows) {
91
+ const line = columns.map((col, i) => {
92
+ const val = String(row[col.key] ?? "");
93
+ return val.length > widths[i] ? val.slice(0, widths[i] - 1) + "\u2026" : val.padEnd(widths[i]);
94
+ }).join(" ");
95
+ console.log(line);
96
+ }
97
+ }
98
+ function printResult(data) {
99
+ if (_jsonMode) {
100
+ printJson(data);
101
+ return;
102
+ }
103
+ const maxKey = Math.max(...Object.keys(data).map((k) => k.length));
104
+ for (const [key, val] of Object.entries(data)) {
105
+ console.log(`${key.padEnd(maxKey)} ${val}`);
106
+ }
107
+ }
108
+
109
+ // src/lib/api-client.ts
110
+ var _token = null;
111
+ var _refreshToken = null;
112
+ var _apiBase = "";
113
+ function initApiClient() {
114
+ const cfg = loadConfig();
115
+ _token = cfg.token;
116
+ _refreshToken = cfg.refreshToken;
117
+ _apiBase = `${cfg.url}/api`;
118
+ }
119
+ function setToken(token) {
120
+ _token = token;
121
+ updateConfig({ token });
122
+ }
123
+ function setRefreshToken(token) {
124
+ _refreshToken = token;
125
+ updateConfig({ refreshToken: token });
126
+ }
127
+ function getToken() {
128
+ return _token;
129
+ }
130
+ function setApiBase(url) {
131
+ _apiBase = `${url.replace(/\/+$/, "")}/api`;
132
+ }
133
+ async function tryRefreshAccessToken() {
134
+ if (!_refreshToken) return false;
135
+ try {
136
+ const res = await fetch(`${_apiBase}/auth/refresh`, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify({ refreshToken: _refreshToken })
140
+ });
141
+ if (!res.ok) return false;
142
+ const data = await res.json();
143
+ _token = data.token;
144
+ updateConfig({ token: data.token });
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ async function request(method, path, body) {
151
+ const headers = {
152
+ "Content-Type": "application/json"
153
+ };
154
+ if (_token) headers["Authorization"] = `Bearer ${_token}`;
155
+ let res;
156
+ try {
157
+ res = await fetch(`${_apiBase}${path}`, {
158
+ method,
159
+ headers,
160
+ body: body ? JSON.stringify(body) : void 0
161
+ });
162
+ } catch (err) {
163
+ throw new Error(`Network error: ${err}`);
164
+ }
165
+ if (res.status === 401 && _refreshToken && !path.includes("/auth/refresh")) {
166
+ const refreshed = await tryRefreshAccessToken();
167
+ if (refreshed) {
168
+ headers["Authorization"] = `Bearer ${_token}`;
169
+ try {
170
+ res = await fetch(`${_apiBase}${path}`, {
171
+ method,
172
+ headers,
173
+ body: body ? JSON.stringify(body) : void 0
174
+ });
175
+ } catch (err) {
176
+ throw new Error(`Network error on retry: ${err}`);
177
+ }
178
+ }
179
+ }
180
+ if (!res.ok) {
181
+ const err = await res.json().catch(() => ({ error: res.statusText }));
182
+ const message = err.error ?? `HTTP ${res.status}`;
183
+ throw new Error(message);
184
+ }
185
+ return await res.json();
186
+ }
187
+ var authApi = {
188
+ register: (email, password, displayName) => request("POST", "/auth/register", {
189
+ email,
190
+ password,
191
+ displayName
192
+ }),
193
+ login: (email, password) => request("POST", "/auth/login", { email, password }),
194
+ me: () => request("GET", "/me")
195
+ };
196
+ var devAuthApi = {
197
+ login: (secret, userId) => request("POST", "/dev-auth/login", {
198
+ secret,
199
+ userId: userId ?? "dev-test-user"
200
+ })
201
+ };
202
+ var meApi = {
203
+ updateSettings: (data) => request("PATCH", "/me", data)
204
+ };
205
+ var modelsApi = {
206
+ list: () => request("GET", "/models")
207
+ };
208
+ var channelsApi = {
209
+ list: () => request("GET", "/channels"),
210
+ get: (id) => request("GET", `/channels/${id}`),
211
+ create: (data) => request("POST", "/channels", data),
212
+ update: (id, data) => request("PATCH", `/channels/${id}`, data),
213
+ delete: (id) => request("DELETE", `/channels/${id}`)
214
+ };
215
+ var sessionsApi = {
216
+ list: (channelId) => request(
217
+ "GET",
218
+ `/channels/${channelId}/sessions`
219
+ ),
220
+ create: (channelId, name) => request("POST", `/channels/${channelId}/sessions`, { name }),
221
+ rename: (channelId, sessionId, name) => request(
222
+ "PATCH",
223
+ `/channels/${channelId}/sessions/${sessionId}`,
224
+ { name }
225
+ ),
226
+ delete: (channelId, sessionId) => request(
227
+ "DELETE",
228
+ `/channels/${channelId}/sessions/${sessionId}`
229
+ )
230
+ };
231
+ var tasksApi = {
232
+ list: (channelId) => request("GET", `/channels/${channelId}/tasks`),
233
+ listAll: (kind = "background") => request("GET", `/tasks?kind=${kind}`),
234
+ scanData: () => request("GET", "/task-scan"),
235
+ create: (channelId, data) => request("POST", `/channels/${channelId}/tasks`, data),
236
+ update: (channelId, taskId, data) => request(
237
+ "PATCH",
238
+ `/channels/${channelId}/tasks/${taskId}`,
239
+ data
240
+ ),
241
+ delete: (channelId, taskId) => request(
242
+ "DELETE",
243
+ `/channels/${channelId}/tasks/${taskId}`
244
+ ),
245
+ run: (channelId, taskId) => request(
246
+ "POST",
247
+ `/channels/${channelId}/tasks/${taskId}/run`
248
+ )
249
+ };
250
+ var jobsApi = {
251
+ list: (channelId, taskId) => request(
252
+ "GET",
253
+ `/channels/${channelId}/tasks/${taskId}/jobs`
254
+ ),
255
+ listByTask: (taskId) => request("GET", `/tasks/${taskId}/jobs`)
256
+ };
257
+ var messagesApi = {
258
+ list: (userId, sessionKey, threadId) => request(
259
+ "GET",
260
+ `/messages/${userId}?sessionKey=${encodeURIComponent(sessionKey)}${threadId ? `&threadId=${encodeURIComponent(threadId)}` : ""}`
261
+ )
262
+ };
263
+ var pairingApi = {
264
+ list: () => request("GET", "/pairing-tokens"),
265
+ create: (label) => request(
266
+ "POST",
267
+ "/pairing-tokens",
268
+ { label }
269
+ ),
270
+ delete: (id) => request("DELETE", `/pairing-tokens/${id}`)
271
+ };
272
+ var setupApi = {
273
+ init: (data) => request("POST", "/setup/init", data),
274
+ cloudUrl: () => request(
275
+ "GET",
276
+ "/setup/cloud-url"
277
+ )
278
+ };
279
+ var connectionApi = {
280
+ status: (userId) => request(
281
+ "GET",
282
+ `/connection/${userId}/status`
283
+ )
284
+ };
285
+
286
+ // ../e2e-crypto/e2e-crypto.ts
287
+ var isNode = typeof globalThis.process !== "undefined" && typeof globalThis.process.versions?.node === "string";
288
+ var PBKDF2_ITERATIONS = 31e4;
289
+ var KEY_LENGTH = 32;
290
+ var NONCE_LENGTH = 16;
291
+ var SALT_PREFIX = "botschat-e2e:";
292
+ function utf8Encode(str) {
293
+ return new TextEncoder().encode(str);
294
+ }
295
+ function utf8Decode(buf) {
296
+ return new TextDecoder().decode(buf);
297
+ }
298
+ async function deriveKeyWeb(password, userId) {
299
+ const enc = utf8Encode(password);
300
+ const salt = utf8Encode(SALT_PREFIX + userId);
301
+ const baseKey = await crypto.subtle.importKey("raw", enc.buffer, "PBKDF2", false, [
302
+ "deriveBits"
303
+ ]);
304
+ const saltArr = new ArrayBuffer(salt.byteLength);
305
+ new Uint8Array(saltArr).set(salt);
306
+ const bits = await crypto.subtle.deriveBits(
307
+ { name: "PBKDF2", salt: saltArr, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
308
+ baseKey,
309
+ KEY_LENGTH * 8
310
+ );
311
+ return new Uint8Array(bits);
312
+ }
313
+ async function hkdfNonceWeb(key, contextId) {
314
+ const hmacKey = await crypto.subtle.importKey(
315
+ "raw",
316
+ key.buffer,
317
+ { name: "HMAC", hash: "SHA-256" },
318
+ false,
319
+ ["sign"]
320
+ );
321
+ const info = utf8Encode("nonce-" + contextId);
322
+ const input = new Uint8Array(info.length + 1);
323
+ input.set(info);
324
+ input[info.length] = 1;
325
+ const full = await crypto.subtle.sign("HMAC", hmacKey, input.buffer);
326
+ return new Uint8Array(full).slice(0, NONCE_LENGTH);
327
+ }
328
+ async function encryptWeb(key, plaintext, contextId) {
329
+ const counter = await hkdfNonceWeb(key, contextId);
330
+ const aesKey = await crypto.subtle.importKey(
331
+ "raw",
332
+ key.buffer,
333
+ { name: "AES-CTR" },
334
+ false,
335
+ ["encrypt"]
336
+ );
337
+ const ciphertext = await crypto.subtle.encrypt(
338
+ { name: "AES-CTR", counter: new Uint8Array(counter).buffer, length: 128 },
339
+ aesKey,
340
+ plaintext.buffer
341
+ );
342
+ return new Uint8Array(ciphertext);
343
+ }
344
+ async function decryptWeb(key, ciphertext, contextId) {
345
+ const counter = await hkdfNonceWeb(key, contextId);
346
+ const aesKey = await crypto.subtle.importKey(
347
+ "raw",
348
+ key.buffer,
349
+ { name: "AES-CTR" },
350
+ false,
351
+ ["decrypt"]
352
+ );
353
+ const plaintext = await crypto.subtle.decrypt(
354
+ { name: "AES-CTR", counter: new Uint8Array(counter).buffer, length: 128 },
355
+ aesKey,
356
+ ciphertext.buffer
357
+ );
358
+ return new Uint8Array(plaintext);
359
+ }
360
+ var _nodeCrypto = null;
361
+ var _nodeUtil = null;
362
+ var _g = globalThis;
363
+ if (isNode && _g.__e2e_nodeCrypto) {
364
+ _nodeCrypto = _g.__e2e_nodeCrypto;
365
+ _nodeUtil = _g.__e2e_nodeUtil;
366
+ }
367
+ async function ensureNodeModules() {
368
+ if (_nodeCrypto && _nodeUtil) return;
369
+ _nodeCrypto = await import("crypto");
370
+ _nodeUtil = await import("util");
371
+ _g.__e2e_nodeCrypto = _nodeCrypto;
372
+ _g.__e2e_nodeUtil = _nodeUtil;
373
+ }
374
+ async function deriveKeyNode(password, userId) {
375
+ await ensureNodeModules();
376
+ const pbkdf2Async = _nodeUtil.promisify(_nodeCrypto.pbkdf2);
377
+ const salt = SALT_PREFIX + userId;
378
+ const buf = await pbkdf2Async(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
379
+ return new Uint8Array(buf);
380
+ }
381
+ async function hkdfNonceNode(key, contextId) {
382
+ await ensureNodeModules();
383
+ const info = utf8Encode("nonce-" + contextId);
384
+ const input = new Uint8Array(info.length + 1);
385
+ input.set(info);
386
+ input[info.length] = 1;
387
+ const hmac = _nodeCrypto.createHmac("sha256", Buffer.from(key));
388
+ hmac.update(Buffer.from(input));
389
+ const full = hmac.digest();
390
+ return new Uint8Array(full.buffer, full.byteOffset, NONCE_LENGTH);
391
+ }
392
+ async function encryptNode(key, plaintext, contextId) {
393
+ await ensureNodeModules();
394
+ const iv = await hkdfNonceNode(key, contextId);
395
+ const cipher = _nodeCrypto.createCipheriv(
396
+ "aes-256-ctr",
397
+ Buffer.from(key),
398
+ Buffer.from(iv)
399
+ );
400
+ const encrypted = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
401
+ return new Uint8Array(encrypted);
402
+ }
403
+ async function decryptNode(key, ciphertext, contextId) {
404
+ await ensureNodeModules();
405
+ const iv = await hkdfNonceNode(key, contextId);
406
+ const decipher = _nodeCrypto.createDecipheriv(
407
+ "aes-256-ctr",
408
+ Buffer.from(key),
409
+ Buffer.from(iv)
410
+ );
411
+ const decrypted = Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]);
412
+ return new Uint8Array(decrypted);
413
+ }
414
+ async function deriveKey(password, userId) {
415
+ return isNode ? deriveKeyNode(password, userId) : deriveKeyWeb(password, userId);
416
+ }
417
+ async function encryptText(key, plaintext, contextId) {
418
+ const data = utf8Encode(plaintext);
419
+ return isNode ? encryptNode(key, data, contextId) : encryptWeb(key, data, contextId);
420
+ }
421
+ async function decryptText(key, ciphertext, contextId) {
422
+ const data = isNode ? await decryptNode(key, ciphertext, contextId) : await decryptWeb(key, ciphertext, contextId);
423
+ return utf8Decode(data);
424
+ }
425
+ function toBase64(data) {
426
+ if (typeof Buffer !== "undefined") {
427
+ return Buffer.from(data).toString("base64");
428
+ }
429
+ let binary = "";
430
+ for (let i = 0; i < data.length; i++) {
431
+ binary += String.fromCharCode(data[i]);
432
+ }
433
+ return btoa(binary);
434
+ }
435
+ function fromBase64(b64) {
436
+ if (typeof Buffer !== "undefined") {
437
+ const buf = Buffer.from(b64, "base64");
438
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
439
+ }
440
+ const binary = atob(b64);
441
+ const bytes = new Uint8Array(binary.length);
442
+ for (let i = 0; i < binary.length; i++) {
443
+ bytes[i] = binary.charCodeAt(i);
444
+ }
445
+ return bytes;
446
+ }
447
+
448
+ // src/lib/e2e.ts
449
+ import { randomUUID } from "crypto";
450
+ var currentKey = null;
451
+ var currentPassword = null;
452
+ function initE2eFromConfig() {
453
+ try {
454
+ const cfg = loadConfig();
455
+ if (cfg.e2eKeyBase64) {
456
+ currentKey = fromBase64(cfg.e2eKeyBase64);
457
+ currentPassword = cfg.e2ePassword;
458
+ }
459
+ } catch {
460
+ }
461
+ }
462
+ var E2eService = {
463
+ async setPassword(password, userId, remember = true) {
464
+ if (!password) {
465
+ currentKey = null;
466
+ currentPassword = null;
467
+ updateConfig({ e2ePassword: null, e2eKeyBase64: null });
468
+ return;
469
+ }
470
+ currentKey = await deriveKey(password, userId);
471
+ currentPassword = password;
472
+ if (remember) {
473
+ updateConfig({
474
+ e2ePassword: password,
475
+ e2eKeyBase64: toBase64(currentKey)
476
+ });
477
+ }
478
+ },
479
+ clear() {
480
+ currentKey = null;
481
+ currentPassword = null;
482
+ updateConfig({ e2ePassword: null, e2eKeyBase64: null });
483
+ },
484
+ hasKey() {
485
+ return !!currentKey;
486
+ },
487
+ getPassword() {
488
+ return currentPassword;
489
+ },
490
+ async loadSavedPassword(userId) {
491
+ if (currentKey) return true;
492
+ const cfg = loadConfig();
493
+ if (!cfg.e2ePassword) return false;
494
+ try {
495
+ await this.setPassword(cfg.e2ePassword, userId, true);
496
+ return true;
497
+ } catch {
498
+ return false;
499
+ }
500
+ },
501
+ async encrypt(text, contextId) {
502
+ if (!currentKey) throw new Error("E2E key not set");
503
+ const messageId = contextId || randomUUID();
504
+ const encrypted = await encryptText(currentKey, text, messageId);
505
+ return { ciphertext: toBase64(encrypted), messageId };
506
+ },
507
+ async decrypt(ciphertextBase64, messageId) {
508
+ if (!currentKey) throw new Error("E2E key not set");
509
+ const ciphertext = fromBase64(ciphertextBase64);
510
+ return decryptText(currentKey, ciphertext, messageId);
511
+ }
512
+ };
513
+
514
+ // src/commands/login.ts
515
+ import { Command } from "commander";
516
+ import { createServer } from "http";
517
+ import { randomUUID as randomUUID2 } from "crypto";
518
+ import { exec } from "child_process";
519
+ var loginCmd = new Command("login").description("Authenticate with BotsChat server").option("--dev", "Use dev auth (local development)").option("--secret <secret>", "Dev auth secret").option("--user <userId>", "Dev auth user ID", "dev-test-user").option("--email <email>", "Email for login").option("--password <password>", "Password for login").option("--no-open", "Print login URL instead of opening browser").action(async (opts) => {
520
+ try {
521
+ if (opts.dev) {
522
+ const secret = opts.secret;
523
+ if (!secret) {
524
+ printError("--secret is required for dev auth");
525
+ process.exit(1);
526
+ }
527
+ const result = await devAuthApi.login(secret, opts.user);
528
+ setToken(result.token);
529
+ updateConfig({ userId: result.userId });
530
+ if (isJsonMode()) {
531
+ printResult({ userId: result.userId });
532
+ } else {
533
+ console.log(`Logged in as ${result.userId}`);
534
+ }
535
+ } else if (opts.email && opts.password) {
536
+ const result = await authApi.login(opts.email, opts.password);
537
+ setToken(result.token);
538
+ if (result.refreshToken) setRefreshToken(result.refreshToken);
539
+ updateConfig({ userId: result.id });
540
+ if (isJsonMode()) {
541
+ printResult({ id: result.id, email: result.email });
542
+ } else {
543
+ console.log(`Logged in as ${result.email} (${result.id})`);
544
+ }
545
+ } else {
546
+ await browserLogin(opts.open !== false ? true : false);
547
+ }
548
+ } catch (err) {
549
+ printError(String(err.message));
550
+ process.exit(1);
551
+ }
552
+ });
553
+ async function browserLogin(autoOpen = true) {
554
+ const cfg = loadConfig();
555
+ const state = randomUUID2();
556
+ return new Promise((resolve, reject) => {
557
+ const server = createServer((req, res) => {
558
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
559
+ if (url.pathname === "/start") {
560
+ const addr = server.address();
561
+ const port = typeof addr === "object" && addr ? addr.port : 0;
562
+ const loginUrl = `${cfg.url}/?cli_port=${port}&cli_state=${encodeURIComponent(state)}`;
563
+ res.writeHead(302, { Location: loginUrl });
564
+ res.end();
565
+ return;
566
+ }
567
+ if (url.pathname === "/callback") {
568
+ const token = url.searchParams.get("token");
569
+ const refreshToken = url.searchParams.get("refreshToken");
570
+ const userId = url.searchParams.get("userId");
571
+ const email = url.searchParams.get("email");
572
+ const displayName = url.searchParams.get("displayName");
573
+ const reqState = url.searchParams.get("state");
574
+ if (reqState !== state) {
575
+ res.writeHead(403, { "Content-Type": "text/html" });
576
+ res.end(successPage(false, "Invalid state. Please try again."));
577
+ return;
578
+ }
579
+ if (!token || !userId) {
580
+ res.writeHead(400, { "Content-Type": "text/html" });
581
+ res.end(successPage(false, "Missing credentials. Please try again."));
582
+ return;
583
+ }
584
+ setToken(token);
585
+ if (refreshToken) setRefreshToken(refreshToken);
586
+ updateConfig({ userId });
587
+ res.writeHead(200, { "Content-Type": "text/html" });
588
+ res.end(successPage(true, `Logged in as ${email}`));
589
+ if (isJsonMode()) {
590
+ printResult({
591
+ userId,
592
+ email: email ?? "(unknown)",
593
+ displayName: displayName ?? "(not set)"
594
+ });
595
+ } else {
596
+ console.log(`
597
+ Logged in as ${email} (${userId})`);
598
+ }
599
+ setTimeout(() => {
600
+ server.close();
601
+ resolve();
602
+ }, 2e3);
603
+ return;
604
+ }
605
+ res.writeHead(404);
606
+ res.end();
607
+ });
608
+ server.listen(0, "127.0.0.1", () => {
609
+ const addr = server.address();
610
+ if (!addr || typeof addr === "string") {
611
+ reject(new Error("Failed to start callback server"));
612
+ return;
613
+ }
614
+ const port = addr.port;
615
+ const startUrl = `http://127.0.0.1:${port}/start`;
616
+ if (autoOpen) {
617
+ console.log("Opening browser for login...");
618
+ const cmd = process.platform === "darwin" ? `open "${startUrl}"` : process.platform === "win32" ? `start "" "${startUrl}"` : `xdg-open "${startUrl}"`;
619
+ exec(cmd, (err) => {
620
+ if (err) {
621
+ console.log(
622
+ `Couldn't open browser automatically.`
623
+ );
624
+ }
625
+ });
626
+ }
627
+ console.log(`Login URL: ${startUrl}`);
628
+ console.log("Waiting for login...");
629
+ const timeout = setTimeout(() => {
630
+ server.close();
631
+ reject(new Error("Login timed out (5 minutes). Please try again."));
632
+ }, 5 * 60 * 1e3);
633
+ server.on("close", () => clearTimeout(timeout));
634
+ });
635
+ });
636
+ }
637
+ function successPage(ok, message) {
638
+ const icon = ok ? "&#10003;" : "&#10007;";
639
+ const color = ok ? "#22c55e" : "#ef4444";
640
+ return `<!DOCTYPE html>
641
+ <html><head><title>BotsChat CLI</title></head>
642
+ <body style="display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:system-ui;background:#1a1a2e;color:#e0e0e0">
643
+ <div style="text-align:center">
644
+ <div style="font-size:48px;margin-bottom:16px;color:${color}">${icon}</div>
645
+ <h2>${ok ? "CLI Login Successful" : "Login Failed"}</h2>
646
+ <p style="color:#888;margin-top:8px">${message}</p>
647
+ <p style="color:#666;margin-top:16px;font-size:14px">You can close this tab and return to the terminal.</p>
648
+ </div>
649
+ </body></html>`;
650
+ }
651
+
652
+ // src/commands/logout.ts
653
+ import { Command as Command2 } from "commander";
654
+ var logoutCmd = new Command2("logout").description("Clear authentication tokens").action(() => {
655
+ updateConfig({
656
+ token: null,
657
+ refreshToken: null,
658
+ userId: null
659
+ });
660
+ console.log("Logged out.");
661
+ });
662
+
663
+ // src/commands/whoami.ts
664
+ import { Command as Command3 } from "commander";
665
+ var whoamiCmd = new Command3("whoami").description("Show current user info").action(async () => {
666
+ try {
667
+ const me = await authApi.me();
668
+ printResult({
669
+ id: me.id,
670
+ email: me.email,
671
+ displayName: me.displayName ?? "(not set)",
672
+ defaultModel: me.settings.defaultModel ?? "(not set)"
673
+ });
674
+ } catch (err) {
675
+ printError(String(err.message));
676
+ process.exit(1);
677
+ }
678
+ });
679
+
680
+ // src/commands/channels.ts
681
+ import { Command as Command4 } from "commander";
682
+ var channelsCmd = new Command4("channels").description("Manage channels").action(async () => {
683
+ try {
684
+ const { channels } = await channelsApi.list();
685
+ printTable(
686
+ channels.map((c) => ({
687
+ id: c.id,
688
+ name: c.name,
689
+ agent: c.openclawAgentId,
690
+ updated: new Date(c.updatedAt).toLocaleString()
691
+ })),
692
+ [
693
+ { key: "id", label: "ID", width: 20 },
694
+ { key: "name", label: "Name", width: 20 },
695
+ { key: "agent", label: "Agent", width: 15 },
696
+ { key: "updated", label: "Updated", width: 20 }
697
+ ]
698
+ );
699
+ } catch (err) {
700
+ printError(String(err.message));
701
+ process.exit(1);
702
+ }
703
+ });
704
+ channelsCmd.command("show <id>").description("Show channel details").action(async (id) => {
705
+ try {
706
+ const ch = await channelsApi.get(id);
707
+ printResult({
708
+ id: ch.id,
709
+ name: ch.name,
710
+ description: ch.description || "(none)",
711
+ openclawAgentId: ch.openclawAgentId,
712
+ systemPrompt: ch.systemPrompt || "(none)",
713
+ createdAt: new Date(ch.createdAt).toLocaleString(),
714
+ updatedAt: new Date(ch.updatedAt).toLocaleString()
715
+ });
716
+ } catch (err) {
717
+ printError(String(err.message));
718
+ process.exit(1);
719
+ }
720
+ });
721
+ channelsCmd.command("create <name>").description("Create a new channel").option("--description <desc>", "Channel description").option("--agent-id <agentId>", "OpenClaw agent ID").option("--system-prompt <prompt>", "System prompt").action(async (name, opts) => {
722
+ try {
723
+ const ch = await channelsApi.create({
724
+ name,
725
+ description: opts.description,
726
+ openclawAgentId: opts.agentId,
727
+ systemPrompt: opts.systemPrompt
728
+ });
729
+ if (isJsonMode()) {
730
+ printJson(ch);
731
+ } else {
732
+ console.log(`Channel created: ${ch.id} (${ch.name})`);
733
+ }
734
+ } catch (err) {
735
+ printError(String(err.message));
736
+ process.exit(1);
737
+ }
738
+ });
739
+ channelsCmd.command("update <id>").description("Update a channel").option("--name <name>", "New name").option("--description <desc>", "New description").option("--system-prompt <prompt>", "New system prompt").action(async (id, opts) => {
740
+ try {
741
+ const data = {};
742
+ if (opts.name) data.name = opts.name;
743
+ if (opts.description) data.description = opts.description;
744
+ if (opts.systemPrompt) data.systemPrompt = opts.systemPrompt;
745
+ await channelsApi.update(id, data);
746
+ console.log("Channel updated.");
747
+ } catch (err) {
748
+ printError(String(err.message));
749
+ process.exit(1);
750
+ }
751
+ });
752
+ channelsCmd.command("delete <id>").description("Delete a channel").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
753
+ try {
754
+ if (!opts.yes) {
755
+ const { createInterface: createInterface2 } = await import("readline/promises");
756
+ const rl = createInterface2({
757
+ input: process.stdin,
758
+ output: process.stderr
759
+ });
760
+ const answer = await rl.question(
761
+ `Delete channel ${id}? [y/N] `
762
+ );
763
+ rl.close();
764
+ if (answer.toLowerCase() !== "y") {
765
+ console.log("Cancelled.");
766
+ return;
767
+ }
768
+ }
769
+ await channelsApi.delete(id);
770
+ console.log("Channel deleted.");
771
+ } catch (err) {
772
+ printError(String(err.message));
773
+ process.exit(1);
774
+ }
775
+ });
776
+
777
+ // src/commands/sessions.ts
778
+ import { Command as Command5 } from "commander";
779
+ var sessionsCmd = new Command5("sessions").description("Manage sessions").argument("<channelId>", "Channel ID").action(async (channelId) => {
780
+ try {
781
+ const { sessions } = await sessionsApi.list(channelId);
782
+ printTable(
783
+ sessions.map((s) => ({
784
+ id: s.id,
785
+ name: s.name,
786
+ sessionKey: s.sessionKey,
787
+ updated: new Date(s.updatedAt).toLocaleString()
788
+ })),
789
+ [
790
+ { key: "id", label: "ID", width: 20 },
791
+ { key: "name", label: "Name", width: 25 },
792
+ { key: "sessionKey", label: "Session Key", width: 40 },
793
+ { key: "updated", label: "Updated", width: 20 }
794
+ ]
795
+ );
796
+ } catch (err) {
797
+ printError(String(err.message));
798
+ process.exit(1);
799
+ }
800
+ });
801
+ sessionsCmd.command("create <channelId>").description("Create a new session").option("--name <name>", "Session name").action(async (channelId, opts) => {
802
+ try {
803
+ const session = await sessionsApi.create(channelId, opts.name);
804
+ if (isJsonMode()) {
805
+ printJson(session);
806
+ } else {
807
+ console.log(
808
+ `Session created: ${session.id} (key: ${session.sessionKey})`
809
+ );
810
+ }
811
+ } catch (err) {
812
+ printError(String(err.message));
813
+ process.exit(1);
814
+ }
815
+ });
816
+ sessionsCmd.command("rename <channelId> <sessionId> <name>").description("Rename a session").action(async (channelId, sessionId, name) => {
817
+ try {
818
+ await sessionsApi.rename(channelId, sessionId, name);
819
+ console.log("Session renamed.");
820
+ } catch (err) {
821
+ printError(String(err.message));
822
+ process.exit(1);
823
+ }
824
+ });
825
+ sessionsCmd.command("delete <channelId> <sessionId>").description("Delete a session").option("-y, --yes", "Skip confirmation").action(async (channelId, sessionId, opts) => {
826
+ try {
827
+ if (!opts.yes) {
828
+ const { createInterface: createInterface2 } = await import("readline/promises");
829
+ const rl = createInterface2({
830
+ input: process.stdin,
831
+ output: process.stderr
832
+ });
833
+ const answer = await rl.question(
834
+ `Delete session ${sessionId}? [y/N] `
835
+ );
836
+ rl.close();
837
+ if (answer.toLowerCase() !== "y") {
838
+ console.log("Cancelled.");
839
+ return;
840
+ }
841
+ }
842
+ await sessionsApi.delete(channelId, sessionId);
843
+ console.log("Session deleted.");
844
+ } catch (err) {
845
+ printError(String(err.message));
846
+ process.exit(1);
847
+ }
848
+ });
849
+
850
+ // src/commands/tokens.ts
851
+ import { Command as Command6 } from "commander";
852
+ var tokensCmd = new Command6("tokens").description("Manage pairing tokens").action(async () => {
853
+ try {
854
+ const { tokens } = await pairingApi.list();
855
+ printTable(
856
+ tokens.map((t) => ({
857
+ id: t.id,
858
+ preview: t.tokenPreview,
859
+ label: t.label ?? "",
860
+ lastConnected: t.lastConnectedAt ? new Date(t.lastConnectedAt).toLocaleString() : "never",
861
+ created: new Date(t.createdAt).toLocaleString()
862
+ })),
863
+ [
864
+ { key: "id", label: "ID", width: 20 },
865
+ { key: "preview", label: "Token", width: 20 },
866
+ { key: "label", label: "Label", width: 15 },
867
+ { key: "lastConnected", label: "Last Connected", width: 20 },
868
+ { key: "created", label: "Created", width: 20 }
869
+ ]
870
+ );
871
+ } catch (err) {
872
+ printError(String(err.message));
873
+ process.exit(1);
874
+ }
875
+ });
876
+ tokensCmd.command("create").description("Create a new pairing token").option("--label <label>", "Token label").action(async (opts) => {
877
+ try {
878
+ const result = await pairingApi.create(opts.label);
879
+ if (isJsonMode()) {
880
+ printJson(result);
881
+ } else {
882
+ console.log(`Token created: ${result.token}`);
883
+ console.log(`ID: ${result.id}`);
884
+ }
885
+ } catch (err) {
886
+ printError(String(err.message));
887
+ process.exit(1);
888
+ }
889
+ });
890
+ tokensCmd.command("delete <id>").description("Revoke a pairing token").action(async (id) => {
891
+ try {
892
+ await pairingApi.delete(id);
893
+ console.log("Token revoked.");
894
+ } catch (err) {
895
+ printError(String(err.message));
896
+ process.exit(1);
897
+ }
898
+ });
899
+
900
+ // src/commands/models.ts
901
+ import { Command as Command7 } from "commander";
902
+ var modelsCmd = new Command7("models").description("Manage models").action(async () => {
903
+ try {
904
+ const { models } = await modelsApi.list();
905
+ printTable(
906
+ models.map((m) => ({
907
+ id: m.id,
908
+ name: m.name,
909
+ provider: m.provider
910
+ })),
911
+ [
912
+ { key: "id", label: "ID", width: 35 },
913
+ { key: "name", label: "Name", width: 25 },
914
+ { key: "provider", label: "Provider", width: 20 }
915
+ ]
916
+ );
917
+ } catch (err) {
918
+ printError(String(err.message));
919
+ process.exit(1);
920
+ }
921
+ });
922
+ modelsCmd.command("set <modelId>").description("Set default model").action(async (modelId) => {
923
+ try {
924
+ await meApi.updateSettings({ defaultModel: modelId });
925
+ console.log(`Default model set to: ${modelId}`);
926
+ } catch (err) {
927
+ printError(String(err.message));
928
+ process.exit(1);
929
+ }
930
+ });
931
+
932
+ // src/commands/status.ts
933
+ import { Command as Command8 } from "commander";
934
+ var statusCmd = new Command8("status").description("Show OpenClaw connection status").action(async () => {
935
+ try {
936
+ const cfg = loadConfig();
937
+ if (!cfg.userId) {
938
+ printError("Not logged in. Run 'botschat login' first.");
939
+ process.exit(1);
940
+ }
941
+ const result = await connectionApi.status(cfg.userId);
942
+ printResult({
943
+ connected: result.connected ? "yes" : "no",
944
+ agents: result.agents?.join(", ") || "(none)",
945
+ model: result.model ?? "(not set)"
946
+ });
947
+ } catch (err) {
948
+ printError(String(err.message));
949
+ process.exit(1);
950
+ }
951
+ });
952
+
953
+ // src/commands/chat.ts
954
+ import { Command as Command9 } from "commander";
955
+ import { createInterface } from "readline/promises";
956
+ import { randomUUID as randomUUID3 } from "crypto";
957
+
958
+ // src/lib/ws-client.ts
959
+ import WebSocket from "ws";
960
+ var BotsChatWSClient = class {
961
+ constructor(opts) {
962
+ this.opts = opts;
963
+ }
964
+ ws = null;
965
+ reconnectTimer = null;
966
+ backoffMs = 1e3;
967
+ intentionalClose = false;
968
+ _connected = false;
969
+ get connected() {
970
+ return this._connected;
971
+ }
972
+ connect() {
973
+ this.intentionalClose = false;
974
+ this.ws = new WebSocket(this.opts.url);
975
+ this.ws.on("open", () => {
976
+ const token = this.opts.getToken();
977
+ if (!token) {
978
+ this.ws?.close();
979
+ return;
980
+ }
981
+ this.ws.send(JSON.stringify({ type: "auth", token }));
982
+ });
983
+ this.ws.on("message", async (data) => {
984
+ try {
985
+ const raw = typeof data === "string" ? data : data.toString("utf-8");
986
+ const msg = JSON.parse(raw);
987
+ if (msg.encrypted && E2eService.hasKey()) {
988
+ try {
989
+ if (msg.type === "agent.text" || msg.type === "agent.media") {
990
+ const text = msg.text;
991
+ const messageId = msg.messageId;
992
+ if (text && messageId) {
993
+ msg.text = await E2eService.decrypt(text, messageId);
994
+ msg.encrypted = false;
995
+ }
996
+ } else if (msg.type === "job.update") {
997
+ const summary = msg.summary;
998
+ const jobId = msg.jobId;
999
+ if (summary && jobId) {
1000
+ msg.summary = await E2eService.decrypt(summary, jobId);
1001
+ msg.encrypted = false;
1002
+ }
1003
+ }
1004
+ } catch {
1005
+ msg.decryptionError = true;
1006
+ }
1007
+ }
1008
+ if (msg.type === "agent.stream.chunk" && msg.encrypted && msg.chunkId && E2eService.hasKey()) {
1009
+ try {
1010
+ msg.text = await E2eService.decrypt(
1011
+ msg.text,
1012
+ msg.chunkId
1013
+ );
1014
+ msg.encrypted = false;
1015
+ } catch {
1016
+ }
1017
+ }
1018
+ if (msg.type === "agent.activity" && msg.encrypted && msg.activityId && E2eService.hasKey()) {
1019
+ try {
1020
+ msg.text = await E2eService.decrypt(
1021
+ msg.text,
1022
+ msg.activityId
1023
+ );
1024
+ msg.encrypted = false;
1025
+ } catch {
1026
+ }
1027
+ }
1028
+ if (msg.type === "task.scan.result" && Array.isArray(msg.tasks) && E2eService.hasKey()) {
1029
+ for (const t of msg.tasks) {
1030
+ if (t.encrypted && t.iv) {
1031
+ try {
1032
+ if (t.schedule)
1033
+ t.schedule = await E2eService.decrypt(
1034
+ t.schedule,
1035
+ t.iv
1036
+ );
1037
+ if (t.instructions)
1038
+ t.instructions = await E2eService.decrypt(
1039
+ t.instructions,
1040
+ t.iv
1041
+ );
1042
+ t.encrypted = false;
1043
+ } catch {
1044
+ t.decryptionError = true;
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+ if (msg.type === "auth.ok") {
1050
+ this.backoffMs = 1e3;
1051
+ this._connected = true;
1052
+ this.opts.onStatusChange(true);
1053
+ } else if (msg.type === "auth.fail") {
1054
+ this.intentionalClose = true;
1055
+ this.ws?.close();
1056
+ this.opts.onMessage(msg);
1057
+ } else {
1058
+ this.opts.onMessage(msg);
1059
+ }
1060
+ } catch {
1061
+ }
1062
+ });
1063
+ this.ws.on("close", (code) => {
1064
+ this._connected = false;
1065
+ this.opts.onStatusChange(false);
1066
+ if (!this.intentionalClose && !this.opts.noReconnect) {
1067
+ const isAuthFail = code === 4001;
1068
+ this.reconnectTimer = setTimeout(async () => {
1069
+ this.backoffMs = Math.min(this.backoffMs * 2, 3e4);
1070
+ if (isAuthFail) {
1071
+ const ok = await tryRefreshAccessToken();
1072
+ if (ok) this.backoffMs = 1e3;
1073
+ }
1074
+ this.connect();
1075
+ }, this.backoffMs);
1076
+ }
1077
+ });
1078
+ this.ws.on("error", () => {
1079
+ });
1080
+ }
1081
+ async send(msg) {
1082
+ if (this.ws?.readyState === WebSocket.OPEN) {
1083
+ if (msg.type === "user.message" && E2eService.hasKey() && typeof msg.text === "string") {
1084
+ try {
1085
+ const existingId = msg.messageId || void 0;
1086
+ const { ciphertext, messageId } = await E2eService.encrypt(
1087
+ msg.text,
1088
+ existingId
1089
+ );
1090
+ msg.text = ciphertext;
1091
+ if (!existingId) msg.messageId = messageId;
1092
+ msg.encrypted = true;
1093
+ } catch {
1094
+ return;
1095
+ }
1096
+ }
1097
+ this.ws.send(JSON.stringify(msg));
1098
+ }
1099
+ }
1100
+ disconnect() {
1101
+ this.intentionalClose = true;
1102
+ if (this.reconnectTimer) {
1103
+ clearTimeout(this.reconnectTimer);
1104
+ this.reconnectTimer = null;
1105
+ }
1106
+ this.ws?.close();
1107
+ this.ws = null;
1108
+ this._connected = false;
1109
+ }
1110
+ };
1111
+
1112
+ // src/commands/chat.ts
1113
+ var chatCmd = new Command9("chat").description("Chat with an AI agent").argument("[message]", "Message to send (omit for interactive mode)").option("-i, --interactive", "Interactive REPL mode").option("-s, --session <sessionId>", "Session ID").option("-c, --channel <channelId>", "Channel ID").option("-a, --agent <agentId>", "Agent ID").option("--no-stream", "Wait for full response instead of streaming").option("--pipe", "Read message from stdin").option("--timeout <ms>", "Timeout in ms for single-shot mode", "120000").action(async (message, opts) => {
1114
+ try {
1115
+ const cfg = loadConfig();
1116
+ if (!cfg.userId || !cfg.token) {
1117
+ printError("Not logged in. Run 'botschat login' first.");
1118
+ process.exit(1);
1119
+ }
1120
+ if (opts.pipe) {
1121
+ const chunks = [];
1122
+ for await (const chunk of process.stdin) {
1123
+ chunks.push(chunk);
1124
+ }
1125
+ message = Buffer.concat(chunks).toString("utf-8").trim();
1126
+ }
1127
+ const interactive = opts.interactive || !message;
1128
+ let channelId = opts.channel || cfg.defaultChannel;
1129
+ let sessionId = opts.session || cfg.defaultSession;
1130
+ if (!channelId || !sessionId) {
1131
+ const { channels } = await channelsApi.list();
1132
+ if (channels.length === 0) {
1133
+ printError("No channels found. Create one first.");
1134
+ process.exit(1);
1135
+ }
1136
+ if (!channelId) {
1137
+ channelId = channels[0].id;
1138
+ updateConfig({ defaultChannel: channelId });
1139
+ }
1140
+ if (!sessionId) {
1141
+ const { sessions } = await sessionsApi.list(channelId);
1142
+ if (sessions.length === 0) {
1143
+ const session = await sessionsApi.create(channelId);
1144
+ sessionId = session.sessionKey;
1145
+ } else {
1146
+ sessionId = sessions[0].sessionKey;
1147
+ }
1148
+ updateConfig({ defaultSession: sessionId });
1149
+ }
1150
+ }
1151
+ const wsProtocol = cfg.url.startsWith("https") ? "wss" : "ws";
1152
+ const wsHost = cfg.url.replace(/^https?:\/\//, "");
1153
+ const wsUrl = `${wsProtocol}://${wsHost}/api/ws/${cfg.userId}/${encodeURIComponent(sessionId)}`;
1154
+ if (interactive) {
1155
+ await runInteractive(wsUrl, sessionId, opts.agent, cfg.userId);
1156
+ } else {
1157
+ await runSingleShot(
1158
+ wsUrl,
1159
+ sessionId,
1160
+ message,
1161
+ opts.agent,
1162
+ parseInt(opts.timeout),
1163
+ opts.stream !== false
1164
+ );
1165
+ }
1166
+ } catch (err) {
1167
+ printError(String(err.message));
1168
+ process.exit(1);
1169
+ }
1170
+ });
1171
+ async function runSingleShot(wsUrl, sessionKey, message, agentId, timeout = 12e4, stream = true) {
1172
+ return new Promise((resolve, reject) => {
1173
+ let fullText = "";
1174
+ let streaming = false;
1175
+ let timer;
1176
+ const ws = new BotsChatWSClient({
1177
+ url: wsUrl,
1178
+ getToken,
1179
+ noReconnect: true,
1180
+ onStatusChange: async (connected) => {
1181
+ if (connected) {
1182
+ const msg = {
1183
+ type: "user.message",
1184
+ sessionKey,
1185
+ text: message,
1186
+ messageId: randomUUID3()
1187
+ };
1188
+ if (agentId) msg.targetAgentId = agentId;
1189
+ await ws.send(msg);
1190
+ timer = setTimeout(() => {
1191
+ ws.disconnect();
1192
+ reject(new Error("Timeout waiting for response"));
1193
+ }, timeout);
1194
+ }
1195
+ },
1196
+ onMessage: (msg) => {
1197
+ if (msg.type === "auth.fail") {
1198
+ ws.disconnect();
1199
+ reject(new Error(`Auth failed: ${msg.reason}`));
1200
+ return;
1201
+ }
1202
+ if (stream) {
1203
+ if (msg.type === "agent.stream.start") {
1204
+ streaming = true;
1205
+ } else if (msg.type === "agent.stream.chunk" && streaming) {
1206
+ const text = msg.text;
1207
+ if (text) {
1208
+ process.stdout.write(text);
1209
+ fullText += text;
1210
+ }
1211
+ } else if (msg.type === "agent.stream.end" && streaming) {
1212
+ process.stdout.write("\n");
1213
+ clearTimeout(timer);
1214
+ ws.disconnect();
1215
+ resolve();
1216
+ return;
1217
+ } else if (msg.type === "agent.activity") {
1218
+ const text = msg.text;
1219
+ if (text) {
1220
+ printInfo(`[${msg.kind}] ${text}`);
1221
+ }
1222
+ }
1223
+ }
1224
+ if (msg.type === "agent.text") {
1225
+ clearTimeout(timer);
1226
+ if (!streaming) {
1227
+ const text = msg.text;
1228
+ if (isJsonMode()) {
1229
+ printJson(msg);
1230
+ } else {
1231
+ console.log(text);
1232
+ }
1233
+ }
1234
+ ws.disconnect();
1235
+ resolve();
1236
+ }
1237
+ }
1238
+ });
1239
+ ws.connect();
1240
+ });
1241
+ }
1242
+ async function runInteractive(wsUrl, sessionKey, agentId, userId) {
1243
+ return new Promise((resolve) => {
1244
+ let streaming = false;
1245
+ const ws = new BotsChatWSClient({
1246
+ url: wsUrl,
1247
+ getToken,
1248
+ onStatusChange: (connected) => {
1249
+ if (connected) {
1250
+ printInfo("Connected. Type your message (Ctrl+C to exit).\n");
1251
+ startRepl();
1252
+ } else {
1253
+ printInfo("Disconnected. Reconnecting...");
1254
+ }
1255
+ },
1256
+ onMessage: (msg) => {
1257
+ if (msg.type === "agent.stream.start") {
1258
+ streaming = true;
1259
+ } else if (msg.type === "agent.stream.chunk" && streaming) {
1260
+ const text = msg.text;
1261
+ if (text) process.stdout.write(text);
1262
+ } else if (msg.type === "agent.stream.end" && streaming) {
1263
+ streaming = false;
1264
+ process.stdout.write("\n\n");
1265
+ } else if (msg.type === "agent.activity") {
1266
+ const text = msg.text;
1267
+ if (text) printInfo(`[${msg.kind}] ${text}`);
1268
+ } else if (msg.type === "agent.text") {
1269
+ if (!streaming) {
1270
+ if (isJsonMode()) {
1271
+ printJson(msg);
1272
+ } else {
1273
+ console.log(`${msg.text}
1274
+ `);
1275
+ }
1276
+ }
1277
+ }
1278
+ }
1279
+ });
1280
+ ws.connect();
1281
+ function startRepl() {
1282
+ const rl = createInterface({
1283
+ input: process.stdin,
1284
+ output: process.stdout,
1285
+ prompt: "> "
1286
+ });
1287
+ rl.prompt();
1288
+ rl.on("line", async (line) => {
1289
+ const text = line.trim();
1290
+ if (!text) {
1291
+ rl.prompt();
1292
+ return;
1293
+ }
1294
+ if (text === "/quit" || text === "/exit") {
1295
+ ws.disconnect();
1296
+ rl.close();
1297
+ resolve();
1298
+ return;
1299
+ }
1300
+ const msg = {
1301
+ type: "user.message",
1302
+ sessionKey,
1303
+ text,
1304
+ messageId: randomUUID3()
1305
+ };
1306
+ if (agentId) msg.targetAgentId = agentId;
1307
+ await ws.send(msg);
1308
+ const waitForResponse = () => {
1309
+ const origOnMsg = ws["opts"].onMessage;
1310
+ ws["opts"].onMessage = (m) => {
1311
+ origOnMsg(m);
1312
+ if (m.type === "agent.text" || m.type === "agent.stream.end") {
1313
+ ws["opts"].onMessage = origOnMsg;
1314
+ rl.prompt();
1315
+ }
1316
+ };
1317
+ };
1318
+ waitForResponse();
1319
+ });
1320
+ rl.on("close", () => {
1321
+ ws.disconnect();
1322
+ resolve();
1323
+ });
1324
+ }
1325
+ });
1326
+ }
1327
+
1328
+ // src/commands/tasks.ts
1329
+ import { Command as Command10 } from "commander";
1330
+ var tasksCmd = new Command10("tasks").description("Manage background tasks").action(async () => {
1331
+ try {
1332
+ const { tasks } = await tasksApi.listAll("background");
1333
+ printTable(
1334
+ tasks.map((t) => ({
1335
+ id: t.id,
1336
+ name: t.name,
1337
+ channel: t.channelId,
1338
+ enabled: t.enabled ? "yes" : "no",
1339
+ schedule: t.schedule ?? "(none)",
1340
+ cronJobId: t.openclawCronJobId ?? "(none)"
1341
+ })),
1342
+ [
1343
+ { key: "id", label: "ID", width: 20 },
1344
+ { key: "name", label: "Name", width: 20 },
1345
+ { key: "channel", label: "Channel", width: 20 },
1346
+ { key: "enabled", label: "Enabled", width: 8 },
1347
+ { key: "schedule", label: "Schedule", width: 15 }
1348
+ ]
1349
+ );
1350
+ } catch (err) {
1351
+ printError(String(err.message));
1352
+ process.exit(1);
1353
+ }
1354
+ });
1355
+ tasksCmd.command("list <channelId>").description("List tasks for a channel").action(async (channelId) => {
1356
+ try {
1357
+ const { tasks } = await tasksApi.list(channelId);
1358
+ printTable(
1359
+ tasks.map((t) => ({
1360
+ id: t.id,
1361
+ name: t.name,
1362
+ kind: t.kind,
1363
+ enabled: t.enabled ? "yes" : "no",
1364
+ schedule: t.schedule ?? "(none)"
1365
+ })),
1366
+ [
1367
+ { key: "id", label: "ID", width: 20 },
1368
+ { key: "name", label: "Name", width: 20 },
1369
+ { key: "kind", label: "Kind", width: 12 },
1370
+ { key: "enabled", label: "Enabled", width: 8 },
1371
+ { key: "schedule", label: "Schedule", width: 15 }
1372
+ ]
1373
+ );
1374
+ } catch (err) {
1375
+ printError(String(err.message));
1376
+ process.exit(1);
1377
+ }
1378
+ });
1379
+ tasksCmd.command("create <channelId> <name>").description("Create a task").option("--kind <kind>", "Task kind", "background").option("--schedule <schedule>", "Cron schedule").option("--instructions <text>", "Task instructions").action(async (channelId, name, opts) => {
1380
+ try {
1381
+ const task = await tasksApi.create(channelId, {
1382
+ name,
1383
+ kind: opts.kind,
1384
+ schedule: opts.schedule,
1385
+ instructions: opts.instructions
1386
+ });
1387
+ if (isJsonMode()) {
1388
+ printJson(task);
1389
+ } else {
1390
+ console.log(`Task created: ${task.id} (${task.name})`);
1391
+ }
1392
+ } catch (err) {
1393
+ printError(String(err.message));
1394
+ process.exit(1);
1395
+ }
1396
+ });
1397
+ tasksCmd.command("update <channelId> <taskId>").description("Update a task").option("--name <name>", "New name").option("--schedule <schedule>", "New schedule").option("--instructions <text>", "New instructions").option("--model <model>", "New model").option("--enabled <bool>", "Enable/disable").action(async (channelId, taskId, opts) => {
1398
+ try {
1399
+ const data = {};
1400
+ if (opts.name) data.name = opts.name;
1401
+ if (opts.schedule) data.schedule = opts.schedule;
1402
+ if (opts.instructions) data.instructions = opts.instructions;
1403
+ if (opts.model) data.model = opts.model;
1404
+ if (opts.enabled !== void 0)
1405
+ data.enabled = opts.enabled === "true";
1406
+ await tasksApi.update(channelId, taskId, data);
1407
+ console.log("Task updated.");
1408
+ } catch (err) {
1409
+ printError(String(err.message));
1410
+ process.exit(1);
1411
+ }
1412
+ });
1413
+ tasksCmd.command("delete <channelId> <taskId>").description("Delete a task").option("-y, --yes", "Skip confirmation").action(async (channelId, taskId, opts) => {
1414
+ try {
1415
+ if (!opts.yes) {
1416
+ const { createInterface: createInterface2 } = await import("readline/promises");
1417
+ const rl = createInterface2({
1418
+ input: process.stdin,
1419
+ output: process.stderr
1420
+ });
1421
+ const answer = await rl.question(`Delete task ${taskId}? [y/N] `);
1422
+ rl.close();
1423
+ if (answer.toLowerCase() !== "y") {
1424
+ console.log("Cancelled.");
1425
+ return;
1426
+ }
1427
+ }
1428
+ await tasksApi.delete(channelId, taskId);
1429
+ console.log("Task deleted.");
1430
+ } catch (err) {
1431
+ printError(String(err.message));
1432
+ process.exit(1);
1433
+ }
1434
+ });
1435
+ tasksCmd.command("run <channelId> <taskId>").description("Run a task immediately").action(async (channelId, taskId) => {
1436
+ try {
1437
+ const result = await tasksApi.run(channelId, taskId);
1438
+ console.log(result.message || "Task execution triggered.");
1439
+ } catch (err) {
1440
+ printError(String(err.message));
1441
+ process.exit(1);
1442
+ }
1443
+ });
1444
+ tasksCmd.command("scan").description("Show live OpenClaw task data").action(async () => {
1445
+ try {
1446
+ const { tasks } = await tasksApi.scanData();
1447
+ if (isJsonMode()) {
1448
+ printJson(tasks);
1449
+ } else {
1450
+ printTable(
1451
+ tasks.map((t) => ({
1452
+ cronJobId: t.cronJobId,
1453
+ schedule: t.schedule,
1454
+ model: t.model || "(default)",
1455
+ enabled: t.enabled ? "yes" : "no",
1456
+ encrypted: t.encrypted ? "yes" : "no"
1457
+ })),
1458
+ [
1459
+ { key: "cronJobId", label: "Cron Job ID", width: 38 },
1460
+ { key: "schedule", label: "Schedule", width: 15 },
1461
+ { key: "model", label: "Model", width: 20 },
1462
+ { key: "enabled", label: "Enabled", width: 8 }
1463
+ ]
1464
+ );
1465
+ }
1466
+ } catch (err) {
1467
+ printError(String(err.message));
1468
+ process.exit(1);
1469
+ }
1470
+ });
1471
+
1472
+ // src/commands/jobs.ts
1473
+ import { Command as Command11 } from "commander";
1474
+ var jobsCmd = new Command11("jobs").description("View job history").argument("<taskId>", "Task ID").option("--limit <n>", "Max results", "50").action(async (taskId) => {
1475
+ try {
1476
+ const { jobs } = await jobsApi.listByTask(taskId);
1477
+ for (const job of jobs) {
1478
+ if (job.encrypted && job.summary && E2eService.hasKey()) {
1479
+ try {
1480
+ job.summary = await E2eService.decrypt(job.summary, job.id);
1481
+ job.encrypted = false;
1482
+ } catch {
1483
+ job.summary = "[decryption failed]";
1484
+ }
1485
+ }
1486
+ }
1487
+ printTable(
1488
+ jobs.map((j) => ({
1489
+ id: j.id,
1490
+ number: j.number,
1491
+ status: j.status,
1492
+ started: new Date(j.startedAt).toLocaleString(),
1493
+ duration: j.durationMs ? `${(j.durationMs / 1e3).toFixed(1)}s` : "-",
1494
+ summary: j.summary || ""
1495
+ })),
1496
+ [
1497
+ { key: "number", label: "#", width: 5 },
1498
+ { key: "status", label: "Status", width: 10 },
1499
+ { key: "started", label: "Started", width: 20 },
1500
+ { key: "duration", label: "Duration", width: 10 },
1501
+ { key: "summary", label: "Summary", width: 40 }
1502
+ ]
1503
+ );
1504
+ } catch (err) {
1505
+ printError(String(err.message));
1506
+ process.exit(1);
1507
+ }
1508
+ });
1509
+
1510
+ // src/commands/messages.ts
1511
+ import { Command as Command12 } from "commander";
1512
+ var messagesCmd = new Command12("messages").description("View message history").argument("<sessionKey>", "Session key").option("--limit <n>", "Max results", "50").option("--thread <threadId>", "Thread ID").action(async (sessionKey, opts) => {
1513
+ try {
1514
+ const cfg = loadConfig();
1515
+ if (!cfg.userId) {
1516
+ printError("Not logged in.");
1517
+ process.exit(1);
1518
+ }
1519
+ const { messages } = await messagesApi.list(
1520
+ cfg.userId,
1521
+ sessionKey,
1522
+ opts.thread
1523
+ );
1524
+ for (const msg of messages) {
1525
+ if (msg.encrypted && msg.text && E2eService.hasKey()) {
1526
+ try {
1527
+ msg.text = await E2eService.decrypt(msg.text, msg.id);
1528
+ msg.encrypted = false;
1529
+ } catch {
1530
+ msg.text = "[decryption failed]";
1531
+ }
1532
+ }
1533
+ }
1534
+ if (isJsonMode()) {
1535
+ printJson(messages);
1536
+ } else {
1537
+ for (const msg of messages) {
1538
+ const time = new Date(msg.timestamp).toLocaleString();
1539
+ const prefix = msg.sender === "user" ? "You" : "AI";
1540
+ const enc = msg.encrypted ? " [encrypted]" : "";
1541
+ console.log(`[${time}] ${prefix}: ${msg.text}${enc}`);
1542
+ }
1543
+ }
1544
+ } catch (err) {
1545
+ printError(String(err.message));
1546
+ process.exit(1);
1547
+ }
1548
+ });
1549
+
1550
+ // src/commands/config-cmd.ts
1551
+ import { Command as Command13 } from "commander";
1552
+ var configCmd = new Command13("config").description("View/edit CLI configuration").action(() => {
1553
+ const cfg = loadConfig();
1554
+ const masked = {
1555
+ url: cfg.url,
1556
+ userId: cfg.userId ?? "(not set)",
1557
+ token: cfg.token ? cfg.token.slice(0, 10) + "..." : "(not set)",
1558
+ refreshToken: cfg.refreshToken ? "***" : "(not set)",
1559
+ e2ePassword: cfg.e2ePassword ? "***" : "(not set)",
1560
+ defaultChannel: cfg.defaultChannel ?? "(not set)",
1561
+ defaultSession: cfg.defaultSession ?? "(not set)"
1562
+ };
1563
+ if (isJsonMode()) {
1564
+ printJson(masked);
1565
+ } else {
1566
+ printResult(masked);
1567
+ }
1568
+ });
1569
+ configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
1570
+ const validKeys = [
1571
+ "url",
1572
+ "defaultChannel",
1573
+ "defaultSession"
1574
+ ];
1575
+ if (!validKeys.includes(key)) {
1576
+ printError(
1577
+ `Invalid key. Valid keys: ${validKeys.join(", ")}`
1578
+ );
1579
+ process.exit(1);
1580
+ }
1581
+ updateConfig({ [key]: value });
1582
+ console.log(`${key} = ${value}`);
1583
+ });
1584
+ configCmd.command("e2e").description("Manage E2E encryption password").option("--password <password>", "Set E2E password").option("--clear", "Remove E2E password").action(async (opts) => {
1585
+ try {
1586
+ const cfg = loadConfig();
1587
+ if (!cfg.userId) {
1588
+ printError("Not logged in. Run 'botschat login' first.");
1589
+ process.exit(1);
1590
+ }
1591
+ if (opts.clear) {
1592
+ E2eService.clear();
1593
+ console.log("E2E password cleared.");
1594
+ return;
1595
+ }
1596
+ let password = opts.password;
1597
+ if (!password) {
1598
+ const { createInterface: createInterface2 } = await import("readline/promises");
1599
+ const rl = createInterface2({
1600
+ input: process.stdin,
1601
+ output: process.stderr
1602
+ });
1603
+ password = await rl.question("E2E Password: ");
1604
+ rl.close();
1605
+ }
1606
+ if (!password) {
1607
+ printError("Password is required.");
1608
+ process.exit(1);
1609
+ }
1610
+ await E2eService.setPassword(password, cfg.userId, true);
1611
+ console.log("E2E password set and key derived.");
1612
+ } catch (err) {
1613
+ printError(String(err.message));
1614
+ process.exit(1);
1615
+ }
1616
+ });
1617
+
1618
+ // src/commands/setup.ts
1619
+ import { Command as Command14 } from "commander";
1620
+ var setupCmd = new Command14("setup").description("One-shot onboarding (setup/init)").option("--secret <secret>", "Dev auth secret").option("--email <email>", "Email").option("--password <password>", "Password").option("--user <userId>", "User ID for dev auth").action(async (opts) => {
1621
+ try {
1622
+ const data = {};
1623
+ if (opts.secret) data.secret = opts.secret;
1624
+ if (opts.email) data.email = opts.email;
1625
+ if (opts.password) data.password = opts.password;
1626
+ if (opts.user) data.userId = opts.user;
1627
+ const result = await setupApi.init(data);
1628
+ setToken(result.token);
1629
+ if (result.refreshToken) setRefreshToken(result.refreshToken);
1630
+ updateConfig({ userId: result.userId });
1631
+ if (isJsonMode()) {
1632
+ printJson(result);
1633
+ } else {
1634
+ console.log(`User ID: ${result.userId}`);
1635
+ console.log(`Pairing Token: ${result.pairingToken}`);
1636
+ console.log(`Cloud URL: ${result.cloudUrl}`);
1637
+ if (result.channels.length > 0) {
1638
+ console.log(
1639
+ `Channel: ${result.channels[0].name} (${result.channels[0].id})`
1640
+ );
1641
+ }
1642
+ if (result.setupCommands.length > 0) {
1643
+ console.log("\nSetup commands for OpenClaw plugin:");
1644
+ for (const cmd of result.setupCommands) {
1645
+ console.log(` ${cmd}`);
1646
+ }
1647
+ }
1648
+ }
1649
+ } catch (err) {
1650
+ printError(String(err.message));
1651
+ process.exit(1);
1652
+ }
1653
+ });
1654
+
1655
+ // src/index.ts
1656
+ var program = new Command15();
1657
+ program.name("botschat").description("BotsChat CLI \u2014 headless client for BotsChat").version("0.1.0").option("--json", "Output JSON (machine-readable)").option("--url <url>", "Override server URL").option("--config <path>", "Config file path").hook("preAction", (thisCommand) => {
1658
+ const opts = thisCommand.opts();
1659
+ if (opts.config) setConfigPath(opts.config);
1660
+ if (opts.json) setJsonMode(true);
1661
+ if (opts.url) updateConfig({ url: opts.url });
1662
+ initApiClient();
1663
+ if (opts.url) setApiBase(opts.url);
1664
+ initE2eFromConfig();
1665
+ });
1666
+ program.addCommand(loginCmd);
1667
+ program.addCommand(logoutCmd);
1668
+ program.addCommand(whoamiCmd);
1669
+ program.addCommand(setupCmd);
1670
+ program.addCommand(channelsCmd);
1671
+ program.addCommand(sessionsCmd);
1672
+ program.addCommand(tokensCmd);
1673
+ program.addCommand(modelsCmd);
1674
+ program.addCommand(statusCmd);
1675
+ program.addCommand(chatCmd);
1676
+ program.addCommand(tasksCmd);
1677
+ program.addCommand(jobsCmd);
1678
+ program.addCommand(messagesCmd);
1679
+ program.addCommand(configCmd);
1680
+ program.parse();