grok-bot-cli 0.3.0 → 0.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +54 -22
  3. package/dist/.agents/plugins/marketplace.json +1 -0
  4. package/dist/.claude-plugin/marketplace.json +1 -0
  5. package/dist/.claude-plugin/plugin.json +1 -0
  6. package/dist/.codex-plugin/mcp.json +1 -0
  7. package/dist/.codex-plugin/plugin.json +1 -0
  8. package/dist/.cursor-plugin/marketplace.json +1 -0
  9. package/dist/.cursor-plugin/mcp.json +1 -0
  10. package/dist/.cursor-plugin/plugin.json +1 -0
  11. package/dist/.mcp.json +1 -0
  12. package/dist/INSTALL.md +269 -0
  13. package/dist/LICENSE +21 -0
  14. package/dist/README.md +173 -0
  15. package/dist/agent-bundle.compile-evidence.json +1 -0
  16. package/dist/agent-bundle.manifest.json +1 -0
  17. package/dist/agent-bundle.package-compile-evidence.json +1 -0
  18. package/dist/bin/gbot-flight.mjs +29162 -0
  19. package/dist/bin/gbot-install.js +135699 -0
  20. package/dist/bin/gbot.mjs +87242 -0
  21. package/dist/install.mjs +1322 -0
  22. package/dist/mcp/mcp-grok-bot-b8c2461e-flight.mjs +26413 -0
  23. package/dist/mcp/mcp-grok-bot-b8c2461e.mjs +107760 -0
  24. package/dist/mcp.json +1 -0
  25. package/dist/package.json +60 -0
  26. package/dist/plugin.json +1 -0
  27. package/dist/skills/talk-to-grok-bot/SKILL.md +37 -0
  28. package/package.json +30 -10
  29. package/src/app-session.js +0 -249
  30. package/src/cli.js +0 -548
  31. package/src/codex-bridge.js +0 -480
  32. package/src/commands.js +0 -52
  33. package/src/gateway.js +0 -361
  34. package/src/headers.js +0 -53
  35. package/src/history.js +0 -83
  36. package/src/store.js +0 -358
  37. package/src/transcript.js +0 -50
  38. package/src/url-policy.js +0 -161
package/src/cli.js DELETED
@@ -1,548 +0,0 @@
1
- #!/usr/bin/env node
2
- import { AVATAR_COLORS, AVATAR_SHAPES, MAX_GROUP_MEMBERS, StoreError, defaultCandidateRoots, looksLikeAgentsRoot, resolveAgentsRoot } from "./store.js";
3
- import { hasGatewayAuth } from "./gateway.js";
4
- import { openBackend } from "./commands.js";
5
- import { inspectGrokBotGatewaySession } from "./app-session.js";
6
- import { entryText, transcriptEntries } from "./transcript.js";
7
- import { historyPath, readHistory, saveHistory } from "./history.js";
8
- import { redactSecrets } from "./url-policy.js";
9
- import { codexStatus, listCodexThreads, sendToCodexThread } from "./codex-bridge.js";
10
-
11
- function print(value) {
12
- if (typeof value === "string") process.stdout.write(value + "\n");
13
- else process.stdout.write(JSON.stringify(value, null, 2) + "\n");
14
- }
15
-
16
- function fail(err) {
17
- let message = err instanceof Error ? err.message : String(err);
18
- message = redactSecrets(message);
19
- process.stderr.write(message + "\n");
20
- process.exit(1);
21
- }
22
-
23
- function usage() {
24
- return [
25
- "gbot - manage Grok Bot agents and groups",
26
- "",
27
- "Usage:",
28
- " gbot [--dir DIR] [--json] <command>",
29
- "",
30
- "Commands:",
31
- " doctor",
32
- " bots list",
33
- " bots create --name NAME [--description TEXT] [--instructions TEXT] [--title TEXT]",
34
- " [--avatar-shape SHAPE] [--avatar-color COLOR]",
35
- " bots update <id-or-name> [--name NAME] [--description TEXT] [--instructions TEXT]",
36
- " [--title TEXT] [--avatar-shape SHAPE] [--avatar-color COLOR]",
37
- " [--notify on|off] [--hidden on|off]",
38
- " bots get <id-or-name>",
39
- " bots delete <id-or-name>",
40
- " groups list",
41
- " groups create --name NAME --member ID_OR_NAME [--member ...]",
42
- " [--description TEXT] [--instructions TEXT] [--title TEXT]",
43
- " [--avatar-shape SHAPE] [--avatar-color COLOR]",
44
- " groups update <id-or-name> (same flags as bots update; members stay on set/add/remove)",
45
- " groups get <id-or-name>",
46
- " groups members <id-or-name>",
47
- " groups add <group> <bot>",
48
- " groups remove <group> <bot>",
49
- " groups set <group> --member ID [--member ...]",
50
- " groups delete <id-or-name>",
51
- " send <bot-or-group> <message...>",
52
- " thread <bot-or-group> [--limit N] [--root MESSAGE_ID] [--full]",
53
- " chat <bot-or-group> alias for thread",
54
- " history [bot-or-group] [--search TEXT] [--limit N] (offline)",
55
- " history --path print the local JSONL file path",
56
- " codex status",
57
- " codex list-threads [--limit N]",
58
- " codex send <threadId> <message...>",
59
- "",
60
- "Max group members: " + MAX_GROUP_MEMBERS,
61
- "--description / --instructions is the UI Instructions field (same key).",
62
- "Avatar shapes: " + AVATAR_SHAPES.join(" "),
63
- "Avatar colors: " + AVATAR_COLORS.join(" "),
64
- "Flags: --gateway --files --dir DIR --json",
65
- "Auth: GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN, or the Grok Bot app session, or CURSOR_ACCESS_TOKEN",
66
- "File fallback: GROK_BOT_AGENTS_DIR",
67
- "Codex: talks to the local app-server daemon socket under CODEX_HOME (default ~/.codex)",
68
- "History: opt-in plaintext JSONL at ~/.grok-bot-cli/history.jsonl",
69
- " GROK_BOT_HISTORY=on to record; --history-dir / GROK_BOT_HISTORY_DIR to relocate",
70
- " --no-history to skip one command",
71
- ].join("\n");
72
- }
73
-
74
- function takeFlag(args, name) {
75
- const i = args.indexOf(name);
76
- if (i === -1) return undefined;
77
- const value = args[i + 1];
78
- if (value == null || value.startsWith("-")) throw new StoreError(name + " needs a value");
79
- args.splice(i, 2);
80
- return value;
81
- }
82
-
83
- function takeLastFlag(args, ...names) {
84
- let value;
85
- let seen = false;
86
- for (;;) {
87
- let hit = false;
88
- for (const name of names) {
89
- const i = args.indexOf(name);
90
- if (i === -1) continue;
91
- const next = args[i + 1];
92
- if (next == null || next.startsWith("-")) throw new StoreError(name + " needs a value");
93
- args.splice(i, 2);
94
- value = next;
95
- seen = true;
96
- hit = true;
97
- break;
98
- }
99
- if (!hit) break;
100
- }
101
- return seen ? value : undefined;
102
- }
103
-
104
- function takeRepeating(args, name) {
105
- const out = [];
106
- for (;;) {
107
- const value = takeFlag(args, name);
108
- if (value == null) break;
109
- out.push(value);
110
- }
111
- return out;
112
- }
113
-
114
- function hasFlag(args, name) {
115
- const i = args.indexOf(name);
116
- if (i === -1) return false;
117
- args.splice(i, 1);
118
- return true;
119
- }
120
-
121
- /** Peel global CLI options only from the leading argv (before the command). */
122
- function takeLeadingGlobals(args) {
123
- let json = false;
124
- let gateway = false;
125
- let files = false;
126
- let noHistory = false;
127
- let historyDir;
128
- let dir;
129
- while (args.length) {
130
- const a = args[0];
131
- if (a === "--") {
132
- args.shift();
133
- break;
134
- }
135
- if (a === "--json") {
136
- json = true;
137
- args.shift();
138
- continue;
139
- }
140
- if (a === "--gateway") {
141
- gateway = true;
142
- args.shift();
143
- continue;
144
- }
145
- if (a === "--files") {
146
- files = true;
147
- args.shift();
148
- continue;
149
- }
150
- if (a === "--no-history") {
151
- noHistory = true;
152
- args.shift();
153
- continue;
154
- }
155
- if (a === "--history-dir") {
156
- args.shift();
157
- const value = args.shift();
158
- if (value == null || value.startsWith("-")) throw new StoreError("--history-dir needs a value");
159
- historyDir = value;
160
- continue;
161
- }
162
- if (a === "--dir") {
163
- args.shift();
164
- const value = args.shift();
165
- if (value == null || value.startsWith("-")) throw new StoreError("--dir needs a value");
166
- dir = value;
167
- continue;
168
- }
169
- break;
170
- }
171
- return { json, gateway, files, dir, noHistory, historyDir };
172
- }
173
-
174
- /** Strip CSI/OSC and other C0/C1 controls so thread fields cannot drive the terminal. */
175
- function stripTerminalControls(text) {
176
- return String(text)
177
- .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "")
178
- .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "")
179
- .replace(/\u001b./g, "")
180
- .replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
181
- }
182
-
183
- function parseOnOff(value, flag) {
184
- const v = String(value).trim().toLowerCase();
185
- if (v === "on" || v === "true" || v === "1" || v === "yes") return true;
186
- if (v === "off" || v === "false" || v === "0" || v === "no") return false;
187
- throw new StoreError(flag + " must be on|off (or true|false|1|0|yes|no)");
188
- }
189
-
190
- function takeCreateFields(args) {
191
- return {
192
- name: takeFlag(args, "--name"),
193
- description: takeLastFlag(args, "--description", "--instructions") ?? "",
194
- title: takeFlag(args, "--title") ?? "",
195
- avatarShape: takeFlag(args, "--avatar-shape") ?? "",
196
- avatarColor: takeFlag(args, "--avatar-color") ?? "",
197
- };
198
- }
199
-
200
- function takeUpdatePatch(args) {
201
- const name = takeFlag(args, "--name");
202
- const description = takeLastFlag(args, "--description", "--instructions");
203
- const title = takeFlag(args, "--title");
204
- const avatarShape = takeFlag(args, "--avatar-shape");
205
- const avatarColor = takeFlag(args, "--avatar-color");
206
- const notifyRaw = takeFlag(args, "--notify");
207
- const hiddenRaw = takeFlag(args, "--hidden");
208
- const patch = {};
209
- if (name !== undefined) patch.name = name;
210
- if (description !== undefined) patch.description = description;
211
- if (title !== undefined) patch.title = title;
212
- if (avatarShape !== undefined) patch.avatarShape = avatarShape;
213
- if (avatarColor !== undefined) patch.avatarColor = avatarColor;
214
- if (notifyRaw !== undefined) patch.notifyOnAgentUpdates = parseOnOff(notifyRaw, "--notify");
215
- if (hiddenRaw !== undefined) patch.hiddenFromSidebar = parseOnOff(hiddenRaw, "--hidden");
216
- if (Object.keys(patch).length === 0) {
217
- throw new StoreError("update needs at least one of --name --description --instructions --title --avatar-shape --avatar-color --notify --hidden");
218
- }
219
- return patch;
220
- }
221
-
222
- function summarize(rec) {
223
- return {
224
- id: rec.id,
225
- name: rec.name,
226
- title: rec.title || undefined,
227
- description: rec.description || undefined,
228
- avatarShape: rec.avatarShape || undefined,
229
- avatarColor: rec.avatarColor || undefined,
230
- ...(rec.notifyOnAgentUpdates !== undefined ? { notifyOnAgentUpdates: rec.notifyOnAgentUpdates } : {}),
231
- ...(rec.hiddenFromSidebar !== undefined ? { hiddenFromSidebar: rec.hiddenFromSidebar } : {}),
232
- kind: rec.isGroup ? "group" : "bot",
233
- members: rec.isGroup ? rec.memberIds : undefined,
234
- };
235
- }
236
-
237
- function done(json, rec, text) {
238
- print(json ? summarize(rec) : text);
239
- }
240
-
241
- function formatRecord(rec, all) {
242
- const kind = rec.isGroup ? "group" : "bot";
243
- const members = rec.isGroup
244
- ? rec.memberIds.map((id) => {
245
- const m = all.find((r) => r.id === id);
246
- return m ? m.name + " (" + id + ")" : id;
247
- }).join(", ")
248
- : "";
249
- const title = rec.title ? " - " + rec.title : "";
250
- const desc = rec.description ? "\n " + rec.description : "";
251
- const avatar = rec.avatarShape || rec.avatarColor
252
- ? "\n avatar: " + [rec.avatarShape, rec.avatarColor].filter(Boolean).join(" ")
253
- : "";
254
- const settings = [];
255
- if (rec.notifyOnAgentUpdates !== undefined) settings.push("notify " + (rec.notifyOnAgentUpdates ? "on" : "off"));
256
- if (rec.hiddenFromSidebar !== undefined) settings.push("hidden " + (rec.hiddenFromSidebar ? "on" : "off"));
257
- const settingsLine = settings.length ? "\n " + settings.join(", ") : "";
258
- const extra = rec.isGroup ? "\n members (" + rec.memberIds.length + "): " + (members || "(none)") : "";
259
- return kind + " " + rec.name + title + "\n " + rec.id + desc + avatar + settingsLine + extra;
260
- }
261
-
262
- function formatTranscript(out, { full = false } = {}) {
263
- const rec = out.target;
264
- const payload = out.transcript || out.thread || {};
265
- const entries = transcriptEntries(payload);
266
- const header = (rec.isGroup ? "group" : "bot") + " " + rec.name + "\n " + rec.id;
267
- if (!Array.isArray(entries) || entries.length === 0) {
268
- return header + "\n (no messages)";
269
- }
270
- const lines = [header, ""];
271
- for (const e of entries) {
272
- const role = e.role || e.kind || e.sender || e.type || "msg";
273
- const text = entryText(e);
274
- const id = e.id || e.messageId || "";
275
- lines.push("[" + role + (id ? " " + id : "") + "] " + (full ? text : truncateCliText(text)));
276
- }
277
- return lines.join("\n");
278
- }
279
-
280
- /** Bounded preview; --json/--full still retrieve the complete text. */
281
- function truncateCliText(text, max = 400) {
282
- if (text.length <= max) return text;
283
- return text.slice(0, max) + "… [+" + (text.length - max) + " chars; --full or --json for complete text]";
284
- }
285
-
286
- function formatCodexStatus(s) {
287
- const lines = ["socket: " + s.socketPath];
288
- if (!s.reachable) return lines.concat("reachable: no", s.message).join("\n");
289
- lines.push("reachable: yes (daemon)");
290
- lines.push("daemon version: " + (s.daemonVersion ?? "unknown") + " cli version: " + (s.cliVersion ?? "unknown") + " pinned schema: " + s.pinnedVersion);
291
- if (s.versionMismatch) lines.push("warning: daemon and CLI versions differ; `codex app-server daemon restart` picks up the installed CLI");
292
- return lines.join("\n");
293
- }
294
-
295
- function formatCodexThread(t) {
296
- const title = t.name ? " - " + stripTerminalControls(t.name) : "";
297
- const preview = t.preview
298
- ? "\n " + stripTerminalControls(String(t.preview).replace(/\s+/g, " ")).slice(0, 200)
299
- : "";
300
- return stripTerminalControls(t.id) + " " + stripTerminalControls(t.status) + title + "\n " + stripTerminalControls(t.cwd ?? "") + preview;
301
- }
302
-
303
- async function runCodex(sub, rest, json) {
304
- if (sub === "status") {
305
- const status = await codexStatus();
306
- print(json ? status : formatCodexStatus(status));
307
- if (!status.reachable) process.exitCode = 1;
308
- return;
309
- }
310
- if (sub === "list-threads") {
311
- const limitRaw = takeFlag(rest, "--limit");
312
- const limit = limitRaw ? Number(limitRaw) : 20;
313
- if (!Number.isInteger(limit) || limit < 1) throw new StoreError("--limit must be a positive integer");
314
- const out = await listCodexThreads({ limit });
315
- if (json) print(out);
316
- else if (out.threads.length === 0) print("No Codex threads.");
317
- else print(out.threads.map(formatCodexThread).join("\n\n"));
318
- return;
319
- }
320
- if (sub === "send") {
321
- const threadId = rest.shift();
322
- if (rest[0] === "--") rest.shift();
323
- const message = rest.join(" ").trim();
324
- if (!threadId || threadId.startsWith("-") || !message) throw new StoreError("gbot codex send <threadId> <message...>");
325
- const out = await sendToCodexThread(threadId, message);
326
- if (json) print(out);
327
- else print("Started turn " + out.turnId + " (" + out.turnStatus + ") on Codex thread " + out.threadId);
328
- return;
329
- }
330
- throw new StoreError("gbot codex status | list-threads [--limit N] | send <threadId> <message...>");
331
- }
332
-
333
- async function main(argv) {
334
- const args = argv.slice(2);
335
- if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
336
- print(usage());
337
- return;
338
- }
339
-
340
- // `json` may also be peeled later from command-local argv (trailing `--json`).
341
- let { json, gateway, files: filesMode, dir: rootFlag, noHistory, historyDir } = takeLeadingGlobals(args);
342
- const cmd = args[0];
343
- const sub = args[1];
344
- const rest = args.slice(2);
345
- if (!cmd) {
346
- print(usage());
347
- return;
348
- }
349
-
350
- if (cmd === "doctor") {
351
- const candidates = defaultCandidateRoots();
352
- const found = candidates.filter(looksLikeAgentsRoot);
353
- let resolved = null;
354
- try {
355
- resolved = resolveAgentsRoot(rootFlag);
356
- } catch (err) {
357
- if (!(err instanceof StoreError)) throw err;
358
- }
359
- const note = "Live roster is on the box. Prefer CURSOR_ACCESS_TOKEN then EnsureSandBox then POST gateway /api/*.";
360
- const gatewayAuthPresent = hasGatewayAuth();
361
- const grokBotAppSession = inspectGrokBotGatewaySession();
362
- const payload = { resolved, found, candidates, gatewayAuthPresent, grokBotAppSession, note };
363
- if (json) print(payload);
364
- else {
365
- print("resolved: " + (resolved ?? "(none)"));
366
- print("gateway auth: " + (gatewayAuthPresent ? "present" : "no"));
367
- if (grokBotAppSession.usable) print("Grok Bot app session: usable");
368
- else if (grokBotAppSession.present) print("Grok Bot app session: present but unusable: " + grokBotAppSession.error);
369
- else print("Grok Bot app session: not found");
370
- print("found:");
371
- print(found.length ? found.map((p) => " " + p).join("\n") : " (none)");
372
- print("candidates:");
373
- for (const c of candidates) print(" " + c);
374
- print(note);
375
- }
376
- return;
377
- }
378
-
379
- if (cmd === "history") {
380
- const options = args.slice(1);
381
- // Command-local flags (globals only peel from argv before the command).
382
- if (hasFlag(options, "--json")) json = true;
383
- const showPath = hasFlag(options, "--path");
384
- const search = takeFlag(options, "--search");
385
- const limitRaw = takeFlag(options, "--limit");
386
- const limit = limitRaw === undefined ? 40 : Number(limitRaw);
387
- if (!Number.isSafeInteger(limit) || limit < 1) throw new StoreError("--limit must be a positive integer");
388
- if (options.length > 1 || options[0]?.startsWith("-") || (showPath && (options.length || search !== undefined || limitRaw !== undefined))) {
389
- throw new StoreError("gbot history [bot-or-group] [--search TEXT] [--limit N], or history --path");
390
- }
391
- const path = historyPath(historyDir);
392
- if (showPath) print(json ? { path } : path);
393
- else {
394
- const rows = await readHistory(path, { ref: options[0], search, limit });
395
- if (json) print(rows);
396
- else print(rows.length ? rows.map((row) =>
397
- "[" + row.recordedAt + "] " + row.target.name + " (" + row.target.id + ") [" + row.role + "] " + row.text
398
- ).join("\n") : "No local history.");
399
- }
400
- return;
401
- }
402
-
403
- if (cmd === "codex") {
404
- await runCodex(sub, rest, json);
405
- return;
406
- }
407
-
408
- const backend = await openBackend({ root: rootFlag, gateway, files: filesMode });
409
-
410
- if (cmd === "bots" && sub === "list") {
411
- const rows = (await backend.list()).filter((r) => !r.isGroup);
412
- if (json) print(rows.map(summarize));
413
- else if (rows.length === 0) print("No bots.");
414
- else print(rows.map((r) => formatRecord(r, rows)).join("\n\n"));
415
- return;
416
- }
417
-
418
- if (cmd === "bots" && sub === "create") {
419
- const fields = takeCreateFields(rest);
420
- const rec = await backend.createAgent(fields);
421
- done(json, rec, "Created bot " + rec.name + " (" + rec.id + ")");
422
- return;
423
- }
424
-
425
- if (cmd === "bots" && sub === "update") {
426
- const ref = rest.shift();
427
- if (!ref || ref.startsWith("-")) throw new StoreError("gbot bots update <id-or-name> [--name NAME] ...");
428
- const rec = await backend.updateAgent(ref, takeUpdatePatch(rest));
429
- done(json, rec, "Updated " + (rec.isGroup ? "group" : "bot") + " " + rec.name + " (" + rec.id + ")");
430
- return;
431
- }
432
-
433
- if (cmd === "bots" && (sub === "get" || sub === "delete")) {
434
- const ref = rest[0];
435
- if (!ref) throw new StoreError("gbot bots " + sub + " <id-or-name>");
436
- if (sub === "get") {
437
- const rec = await backend.resolve(ref);
438
- if (json) print(summarize(rec));
439
- else print(formatRecord(rec, await backend.list()));
440
- return;
441
- }
442
- const rec = await backend.deleteAgent(ref);
443
- done(json, rec, "Deleted " + (rec.isGroup ? "group" : "bot") + " " + rec.name + " (" + rec.id + ")");
444
- return;
445
- }
446
-
447
- if (cmd === "groups" && sub === "list") {
448
- const all = await backend.list();
449
- const rows = all.filter((r) => r.isGroup);
450
- if (json) print(rows.map(summarize));
451
- else if (rows.length === 0) print("No groups.");
452
- else print(rows.map((r) => formatRecord(r, all)).join("\n\n"));
453
- return;
454
- }
455
-
456
- if (cmd === "groups" && sub === "delete") {
457
- const ref = rest[0];
458
- if (!ref) throw new StoreError("gbot groups delete <id-or-name>");
459
- const rec = await backend.resolve(ref);
460
- if (!rec.isGroup) throw new StoreError('"' + rec.name + '" is a bot, not a group. Use bots delete.');
461
- const deleted = await backend.deleteAgent(ref);
462
- done(json, deleted, "Deleted group " + deleted.name + " (" + deleted.id + ")");
463
- return;
464
- }
465
-
466
- if (cmd === "groups" && sub === "create") {
467
- const fields = takeCreateFields(rest);
468
- const members = takeRepeating(rest, "--member");
469
- const rec = await backend.createGroup({ ...fields, memberIds: members });
470
- done(json, rec, "Created group " + rec.name + " (" + rec.id + ") with " + rec.memberIds.length + " members");
471
- return;
472
- }
473
-
474
- if (cmd === "groups" && sub === "update") {
475
- const ref = rest.shift();
476
- if (!ref || ref.startsWith("-")) throw new StoreError("gbot groups update <id-or-name> [--name NAME] ...");
477
- const current = await backend.resolve(ref);
478
- if (!current.isGroup) throw new StoreError('"' + current.name + '" is a bot, not a group. Use bots update.');
479
- const rec = await backend.updateAgent(ref, takeUpdatePatch(rest));
480
- done(json, rec, "Updated group " + rec.name + " (" + rec.id + ")");
481
- return;
482
- }
483
-
484
- if (cmd === "groups" && (sub === "get" || sub === "members")) {
485
- const ref = rest[0];
486
- if (!ref) throw new StoreError("gbot groups " + sub + " <id-or-name>");
487
- const rec = await backend.resolve(ref);
488
- if (!rec.isGroup) throw new StoreError('"' + rec.name + '" is a bot, not a group.');
489
- if (json) print(summarize(rec));
490
- else print(formatRecord(rec, await backend.list()));
491
- return;
492
- }
493
-
494
- if (cmd === "groups" && (sub === "add" || sub === "remove")) {
495
- const group = rest[0];
496
- const bot = rest[1];
497
- if (!group || !bot) throw new StoreError("gbot groups " + sub + " <group> <bot>");
498
- const rec = sub === "add"
499
- ? await backend.addGroupMember(group, bot)
500
- : await backend.removeGroupMember(group, bot);
501
- const verb = sub === "add" ? "Added to " : "Removed from ";
502
- done(json, rec, verb + rec.name + ". Members: " + rec.memberIds.length);
503
- return;
504
- }
505
-
506
- if (cmd === "groups" && sub === "set") {
507
- const group = rest.shift();
508
- const members = takeRepeating(rest, "--member");
509
- if (!group) throw new StoreError("gbot groups set <group> --member ID [--member ...]");
510
- const rec = await backend.setGroupMembers(group, members);
511
- done(json, rec, "Updated " + rec.name + ". Members: " + rec.memberIds.length);
512
- return;
513
- }
514
-
515
- if (cmd === "send") {
516
- const ref = sub;
517
- if (hasFlag(rest, "--json")) json = true;
518
- const message = rest.join(" ").trim();
519
- if (!ref || !message) throw new StoreError("gbot send <bot-or-group> <message...>");
520
- const out = await backend.send(ref, message);
521
- saveHistory(out, { dir: historyDir, disabled: noHistory, event: "send", prompt: message });
522
- const receipt = out.messageId ? " message " + out.messageId : "";
523
- if (json) print({ id: out.target.id, name: out.target.name, kind: out.target.isGroup ? "group" : "bot", result: out.result, delivery: out.delivery || "accepted", ...(out.messageId ? { messageId: out.messageId } : {}) });
524
- else print("Sent to " + (out.target.isGroup ? "group" : "bot") + " " + out.target.name + " (" + out.target.id + ")" + receipt);
525
- return;
526
- }
527
-
528
- if (cmd === "thread" || cmd === "chat") {
529
- const ref = sub;
530
- if (!ref) throw new StoreError("gbot thread <bot-or-group> [--limit N] [--root MESSAGE_ID] [--full]");
531
- if (hasFlag(rest, "--json")) json = true;
532
- const full = rest.includes("--full");
533
- if (full) rest.splice(rest.indexOf("--full"), 1);
534
- const limitRaw = takeFlag(rest, "--limit");
535
- const rootId = takeFlag(rest, "--root");
536
- const limit = limitRaw ? Number(limitRaw) : 40;
537
- if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new StoreError("--limit must be an integer 1-200");
538
- const out = rootId ? await backend.thread(ref, rootId) : await backend.transcript(ref, limit);
539
- saveHistory(out, { dir: historyDir, disabled: noHistory, event: cmd, rootId });
540
- if (json) print(out);
541
- else print(formatTranscript(out, { full }));
542
- return;
543
- }
544
-
545
- throw new StoreError(usage());
546
- }
547
-
548
- main(process.argv).catch(fail);