grok-bot-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 ADDED
@@ -0,0 +1,84 @@
1
+ # grok-bot-cli
2
+
3
+ CLI for [Grok Bot](https://cursor.com/help/grok-bot/plans) agents and groups: create, delete, membership, send, and read threads.
4
+
5
+ gbot bots list
6
+ gbot send Oncall status
7
+
8
+ There is no public HTTP API. This talks to the same box gateway the macOS app uses after EnsureSandBox. Use a Cursor session JWT, not a dashboard API key.
9
+
10
+ ## Setup
11
+
12
+ Package: grok-bot-cli. Node 18+. Bins: gbot and grok-bot.
13
+
14
+ ## Auth
15
+
16
+ Preferred: a Cursor session JWT in CURSOR_ACCESS_TOKEN.
17
+
18
+ gbot bots list
19
+
20
+ Or set GROK_BOT_GATEWAY_URL plus GROK_BOT_GATEWAY_TOKEN and pass --gateway.
21
+
22
+ On a Grok Bot computer, SAND_GATEWAY_TOKEN and SAND_HOST_PORT are picked up automatically.
23
+
24
+ File fallback (local profile.json folders only; no send or thread):
25
+
26
+ gbot --files --dir ./tmp-agents bots create --name Oncall
27
+
28
+ `--dir` is the local agents folder. `--root` is a thread message id.
29
+
30
+ ## Commands
31
+
32
+ gbot doctor
33
+ gbot bots list
34
+ gbot bots create --name NAME [--description TEXT] [--title TEXT]
35
+ gbot bots get <id-or-name>
36
+ gbot bots delete <id-or-name>
37
+
38
+ gbot groups list
39
+ gbot groups create --name NAME --member ID_OR_NAME [--member ...]
40
+ gbot groups get <id-or-name>
41
+ gbot groups members <id-or-name>
42
+ gbot groups add <group> <bot>
43
+ gbot groups remove <group> <bot>
44
+ gbot groups set <group> --member ID [--member ...]
45
+ gbot groups delete <id-or-name>
46
+
47
+ gbot send <bot-or-group> <message...>
48
+ gbot thread <bot-or-group> [--limit N] [--root MESSAGE_ID]
49
+ gbot chat <bot-or-group>
50
+
51
+ Global flags: `--json` `--gateway` `--files` `--dir DIR`.
52
+
53
+ Groups follow the app: at least one member, no nesting, max 6.
54
+
55
+ ## Examples
56
+
57
+ gbot bots create --name Oncall --description pages
58
+ gbot bots create --name Docs
59
+ gbot groups create --name Launch --member Oncall --member Docs
60
+ gbot send Oncall status?
61
+ gbot send Launch standup
62
+ gbot thread Oncall --limit 20
63
+ gbot groups delete Launch
64
+ gbot bots delete Oncall
65
+
66
+ ## Gateway
67
+
68
+ The macOS app (com.anysphere.sand 0.20.0) calls:
69
+
70
+ POST {gateway}/api/listAgents
71
+ POST {gateway}/api/createAgent
72
+ POST {gateway}/api/deleteAgent
73
+ POST {gateway}/api/createGroup
74
+ POST {gateway}/api/setGroupMembers
75
+ POST {gateway}/api/sendPrompt
76
+ POST {gateway}/api/getAgentTranscriptTail
77
+ POST {gateway}/api/getAgentThread
78
+
79
+ A group is an agent plus group.json with version 1 and memberIds.
80
+
81
+ ## License
82
+
83
+ MIT
84
+
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "grok-bot-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for Grok Bot agents and groups: create, delete, send, and read threads",
5
+ "type": "module",
6
+ "bin": {
7
+ "gbot": "./src/cli.js",
8
+ "grok-bot": "./src/cli.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "scripts": {
14
+ "gbot": "node src/cli.js",
15
+ "test": "node --test"
16
+ },
17
+ "license": "MIT",
18
+ "keywords": [
19
+ "grok-bot",
20
+ "gbot",
21
+ "cursor",
22
+ "cli"
23
+ ],
24
+ "files": [
25
+ "src",
26
+ "README.md"
27
+ ]
28
+ }
package/src/cli.js ADDED
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ import { MAX_GROUP_MEMBERS, StoreError, defaultCandidateRoots, looksLikeAgentsRoot, resolveAgentsRoot } from "./store.js";
3
+ import { hasGatewayAuth } from "./gateway.js";
4
+ import { openBackend } from "./commands.js";
5
+
6
+ function print(value) {
7
+ if (typeof value === "string") process.stdout.write(value + "\n");
8
+ else process.stdout.write(JSON.stringify(value, null, 2) + "\n");
9
+ }
10
+
11
+ function fail(err) {
12
+ let message = err instanceof Error ? err.message : String(err);
13
+ message = message.replace(/Bearer\s+[A-Za-z0-9._\-]+/g, "Bearer <redacted>");
14
+ process.stderr.write(message + "\n");
15
+ process.exit(1);
16
+ }
17
+
18
+ function usage() {
19
+ return [
20
+ "gbot - manage Grok Bot agents and groups",
21
+ "",
22
+ "Usage:",
23
+ " gbot [--dir DIR] [--json] <command>",
24
+ "",
25
+ "Commands:",
26
+ " doctor",
27
+ " bots list",
28
+ " bots create --name NAME [--description TEXT] [--title TEXT]",
29
+ " bots get <id-or-name>",
30
+ " bots delete <id-or-name>",
31
+ " groups list",
32
+ " groups create --name NAME --member ID_OR_NAME [--member ...]",
33
+ " groups get <id-or-name>",
34
+ " groups members <id-or-name>",
35
+ " groups add <group> <bot>",
36
+ " groups remove <group> <bot>",
37
+ " groups set <group> --member ID [--member ...]",
38
+ " groups delete <id-or-name>",
39
+ " send <bot-or-group> <message...>",
40
+ " thread <bot-or-group> [--limit N] [--root MESSAGE_ID]",
41
+ " chat <bot-or-group> alias for thread",
42
+ "",
43
+ "Max group members: " + MAX_GROUP_MEMBERS,
44
+ "Flags: --gateway --files --dir DIR --json",
45
+ "Env: CURSOR_ACCESS_TOKEN or GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN",
46
+ "File fallback: GROK_BOT_AGENTS_DIR",
47
+ ].join("\n");
48
+ }
49
+
50
+ function takeFlag(args, name) {
51
+ const i = args.indexOf(name);
52
+ if (i === -1) return undefined;
53
+ const value = args[i + 1];
54
+ if (value == null || value.startsWith("-")) throw new StoreError(name + " needs a value");
55
+ args.splice(i, 2);
56
+ return value;
57
+ }
58
+
59
+ function takeRepeating(args, name) {
60
+ const out = [];
61
+ for (;;) {
62
+ const value = takeFlag(args, name);
63
+ if (value == null) break;
64
+ out.push(value);
65
+ }
66
+ return out;
67
+ }
68
+
69
+ function hasFlag(args, name) {
70
+ const i = args.indexOf(name);
71
+ if (i === -1) return false;
72
+ args.splice(i, 1);
73
+ return true;
74
+ }
75
+
76
+ function summarize(rec) {
77
+ return {
78
+ id: rec.id,
79
+ name: rec.name,
80
+ title: rec.title || undefined,
81
+ description: rec.description || undefined,
82
+ kind: rec.isGroup ? "group" : "bot",
83
+ members: rec.isGroup ? rec.memberIds : undefined,
84
+ };
85
+ }
86
+
87
+ function done(json, rec, text) {
88
+ print(json ? summarize(rec) : text);
89
+ }
90
+
91
+ function formatRecord(rec, all) {
92
+ const kind = rec.isGroup ? "group" : "bot";
93
+ const members = rec.isGroup
94
+ ? rec.memberIds.map((id) => {
95
+ const m = all.find((r) => r.id === id);
96
+ return m ? m.name + " (" + id + ")" : id;
97
+ }).join(", ")
98
+ : "";
99
+ const title = rec.title ? " - " + rec.title : "";
100
+ const desc = rec.description ? "\n " + rec.description : "";
101
+ const extra = rec.isGroup ? "\n members (" + rec.memberIds.length + "): " + (members || "(none)") : "";
102
+ return kind + " " + rec.name + title + "\n " + rec.id + desc + extra;
103
+ }
104
+
105
+ function entryText(e) {
106
+ if (!e || typeof e !== "object") return "";
107
+ const direct = e.text || e.prompt || e.message || e.preview;
108
+ if (typeof direct === "string" && direct) return direct;
109
+ const content = e.content;
110
+ if (typeof content === "string") return content;
111
+ if (Array.isArray(content)) {
112
+ return content.map((part) => {
113
+ if (typeof part === "string") return part;
114
+ if (part && typeof part === "object") return part.text || part.content || "";
115
+ return "";
116
+ }).filter(Boolean).join("\n");
117
+ }
118
+ if (content && typeof content === "object") return content.text || JSON.stringify(content);
119
+ return "";
120
+ }
121
+
122
+ function formatTranscript(out) {
123
+ const rec = out.target;
124
+ const payload = out.transcript || out.thread || {};
125
+ const entries = payload.entries || payload.messages || payload.items || (Array.isArray(payload) ? payload : []);
126
+ const header = (rec.isGroup ? "group" : "bot") + " " + rec.name + "\n " + rec.id;
127
+ if (!Array.isArray(entries) || entries.length === 0) {
128
+ return header + "\n (no messages)";
129
+ }
130
+ const lines = [header, ""];
131
+ for (const e of entries) {
132
+ const role = e.role || e.kind || e.sender || e.type || "msg";
133
+ const text = entryText(e);
134
+ const id = e.id || e.messageId || "";
135
+ lines.push("[" + role + (id ? " " + id : "") + "] " + String(text).slice(0, 400));
136
+ }
137
+ return lines.join("\n");
138
+ }
139
+
140
+ async function main(argv) {
141
+ const args = argv.slice(2);
142
+ if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
143
+ print(usage());
144
+ return;
145
+ }
146
+
147
+ const json = hasFlag(args, "--json");
148
+ const gateway = hasFlag(args, "--gateway");
149
+ const filesMode = hasFlag(args, "--files");
150
+ const rootFlag = takeFlag(args, "--dir");
151
+ const cmd = args[0];
152
+ const sub = args[1];
153
+ const rest = args.slice(2);
154
+ if (!cmd) {
155
+ print(usage());
156
+ return;
157
+ }
158
+
159
+ if (cmd === "doctor") {
160
+ const candidates = defaultCandidateRoots();
161
+ const found = candidates.filter(looksLikeAgentsRoot);
162
+ let resolved = null;
163
+ try {
164
+ resolved = resolveAgentsRoot(rootFlag);
165
+ } catch (err) {
166
+ if (!(err instanceof StoreError)) throw err;
167
+ }
168
+ const note = "Live roster is on the box. Prefer CURSOR_ACCESS_TOKEN then EnsureSandBox then POST gateway /api/*.";
169
+ const payload = { resolved, found, candidates, gatewayAuthPresent: hasGatewayAuth(), note };
170
+ if (json) print(payload);
171
+ else {
172
+ print("resolved: " + (resolved ?? "(none)"));
173
+ print("gateway auth: " + (hasGatewayAuth() ? "yes (env)" : "no"));
174
+ print("found:");
175
+ print(found.length ? found.map((p) => " " + p).join("\n") : " (none)");
176
+ print("candidates:");
177
+ for (const c of candidates) print(" " + c);
178
+ print(note);
179
+ }
180
+ return;
181
+ }
182
+
183
+ const backend = await openBackend({ root: rootFlag, gateway, files: filesMode });
184
+
185
+ if (cmd === "bots" && sub === "list") {
186
+ const rows = (await backend.list()).filter((r) => !r.isGroup);
187
+ if (json) print(rows.map(summarize));
188
+ else if (rows.length === 0) print("No bots.");
189
+ else print(rows.map((r) => formatRecord(r, rows)).join("\n\n"));
190
+ return;
191
+ }
192
+
193
+ if (cmd === "bots" && sub === "create") {
194
+ const name = takeFlag(rest, "--name");
195
+ const description = takeFlag(rest, "--description") ?? "";
196
+ const title = takeFlag(rest, "--title") ?? "";
197
+ const rec = await backend.createAgent({ name, description, title });
198
+ done(json, rec, "Created bot " + rec.name + " (" + rec.id + ")");
199
+ return;
200
+ }
201
+
202
+ if (cmd === "bots" && (sub === "get" || sub === "delete")) {
203
+ const ref = rest[0];
204
+ if (!ref) throw new StoreError("gbot bots " + sub + " <id-or-name>");
205
+ if (sub === "get") {
206
+ const rec = await backend.resolve(ref);
207
+ if (json) print(summarize(rec));
208
+ else print(formatRecord(rec, await backend.list()));
209
+ return;
210
+ }
211
+ const rec = await backend.deleteAgent(ref);
212
+ done(json, rec, "Deleted " + (rec.isGroup ? "group" : "bot") + " " + rec.name + " (" + rec.id + ")");
213
+ return;
214
+ }
215
+
216
+ if (cmd === "groups" && sub === "list") {
217
+ const all = await backend.list();
218
+ const rows = all.filter((r) => r.isGroup);
219
+ if (json) print(rows.map(summarize));
220
+ else if (rows.length === 0) print("No groups.");
221
+ else print(rows.map((r) => formatRecord(r, all)).join("\n\n"));
222
+ return;
223
+ }
224
+
225
+ if (cmd === "groups" && sub === "delete") {
226
+ const ref = rest[0];
227
+ if (!ref) throw new StoreError("gbot groups delete <id-or-name>");
228
+ const rec = await backend.resolve(ref);
229
+ if (!rec.isGroup) throw new StoreError('"' + rec.name + '" is a bot, not a group. Use bots delete.');
230
+ const deleted = await backend.deleteAgent(ref);
231
+ done(json, deleted, "Deleted group " + deleted.name + " (" + deleted.id + ")");
232
+ return;
233
+ }
234
+
235
+ if (cmd === "groups" && sub === "create") {
236
+ const name = takeFlag(rest, "--name");
237
+ const description = takeFlag(rest, "--description") ?? "";
238
+ const members = takeRepeating(rest, "--member");
239
+ const rec = await backend.createGroup({ name, description, memberIds: members });
240
+ done(json, rec, "Created group " + rec.name + " (" + rec.id + ") with " + rec.memberIds.length + " members");
241
+ return;
242
+ }
243
+
244
+ if (cmd === "groups" && (sub === "get" || sub === "members")) {
245
+ const ref = rest[0];
246
+ if (!ref) throw new StoreError("gbot groups " + sub + " <id-or-name>");
247
+ const rec = await backend.resolve(ref);
248
+ if (!rec.isGroup) throw new StoreError('"' + rec.name + '" is a bot, not a group.');
249
+ if (json) print(summarize(rec));
250
+ else print(formatRecord(rec, await backend.list()));
251
+ return;
252
+ }
253
+
254
+ if (cmd === "groups" && (sub === "add" || sub === "remove")) {
255
+ const group = rest[0];
256
+ const bot = rest[1];
257
+ if (!group || !bot) throw new StoreError("gbot groups " + sub + " <group> <bot>");
258
+ const rec = sub === "add"
259
+ ? await backend.addGroupMember(group, bot)
260
+ : await backend.removeGroupMember(group, bot);
261
+ const verb = sub === "add" ? "Added to " : "Removed from ";
262
+ done(json, rec, verb + rec.name + ". Members: " + rec.memberIds.length);
263
+ return;
264
+ }
265
+
266
+ if (cmd === "groups" && sub === "set") {
267
+ const group = rest.shift();
268
+ const members = takeRepeating(rest, "--member");
269
+ if (!group) throw new StoreError("gbot groups set <group> --member ID [--member ...]");
270
+ const rec = await backend.setGroupMembers(group, members);
271
+ done(json, rec, "Updated " + rec.name + ". Members: " + rec.memberIds.length);
272
+ return;
273
+ }
274
+
275
+ if (cmd === "send") {
276
+ const ref = sub;
277
+ const message = rest.join(" ").trim();
278
+ if (!ref || !message) throw new StoreError("gbot send <bot-or-group> <message...>");
279
+ const out = await backend.send(ref, message);
280
+ if (json) print({ id: out.target.id, name: out.target.name, kind: out.target.isGroup ? "group" : "bot", result: out.result });
281
+ else print("Sent to " + (out.target.isGroup ? "group" : "bot") + " " + out.target.name + " (" + out.target.id + ")");
282
+ return;
283
+ }
284
+
285
+ if (cmd === "thread" || cmd === "chat") {
286
+ const ref = sub;
287
+ if (!ref) throw new StoreError("gbot thread <bot-or-group> [--limit N] [--root MESSAGE_ID]");
288
+ const limitRaw = takeFlag(rest, "--limit");
289
+ const rootId = takeFlag(rest, "--root");
290
+ const limit = limitRaw ? Number(limitRaw) : 40;
291
+ const out = rootId ? await backend.thread(ref, rootId) : await backend.transcript(ref, limit);
292
+ if (json) print(out);
293
+ else print(formatTranscript(out));
294
+ return;
295
+ }
296
+
297
+ throw new StoreError(usage());
298
+ }
299
+
300
+ main(process.argv).catch(fail);
@@ -0,0 +1,50 @@
1
+ import * as files from "./store.js";
2
+ import * as gw from "./gateway.js";
3
+
4
+ export function chooseBackend(opts) {
5
+ const forceFiles = Boolean(opts.root) || opts.files;
6
+ const forceGw = Boolean(opts.gateway);
7
+ if (forceGw && forceFiles) {
8
+ throw new files.StoreError("Use --dir/--files or --gateway, not both.");
9
+ }
10
+ if (forceGw || (!forceFiles && gw.hasGatewayAuth())) return "gateway";
11
+ return "files";
12
+ }
13
+
14
+ export async function openBackend(opts) {
15
+ const kind = chooseBackend(opts);
16
+ if (kind === "gateway") {
17
+ const session = await gw.connectGateway();
18
+ return {
19
+ kind,
20
+ session,
21
+ list: () => gw.listAgents(session),
22
+ resolve: (ref) => gw.resolveRef(session, ref),
23
+ createAgent: (input) => gw.createAgent(session, input),
24
+ deleteAgent: (ref) => gw.deleteAgent(session, ref),
25
+ createGroup: (input) => gw.createGroup(session, input),
26
+ setGroupMembers: (group, members) => gw.setGroupMembers(session, group, members),
27
+ addGroupMember: (group, bot) => gw.addGroupMember(session, group, bot),
28
+ removeGroupMember: (group, bot) => gw.removeGroupMember(session, group, bot),
29
+ send: (ref, prompt, extra) => gw.sendPrompt(session, ref, prompt, extra),
30
+ transcript: (ref, limit) => gw.getTranscriptTail(session, ref, limit),
31
+ thread: (ref, rootId) => gw.getThread(session, ref, rootId),
32
+ };
33
+ }
34
+ const root = files.resolveAgentsRoot(opts.root);
35
+ return {
36
+ kind,
37
+ root,
38
+ list: async () => files.listRecords(root),
39
+ resolve: async (ref) => files.resolveRef(root, ref),
40
+ createAgent: async (input) => files.createAgent(root, input),
41
+ deleteAgent: async (ref) => files.deleteAgent(root, ref),
42
+ createGroup: async (input) => files.createGroup(root, input),
43
+ setGroupMembers: async (group, members) => files.setGroupMembers(root, group, members),
44
+ addGroupMember: async (group, bot) => files.addGroupMember(root, group, bot),
45
+ removeGroupMember: async (group, bot) => files.removeGroupMember(root, group, bot),
46
+ send: async () => { throw new files.StoreError("send requires the live gateway"); },
47
+ transcript: async () => { throw new files.StoreError("thread requires the live gateway"); },
48
+ thread: async () => { throw new files.StoreError("thread requires the live gateway"); },
49
+ };
50
+ }
package/src/gateway.js ADDED
@@ -0,0 +1,248 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export class GatewayError extends Error {
4
+ constructor(message, { status, method } = {}) {
5
+ super(message);
6
+ this.name = "GatewayError";
7
+ this.status = status;
8
+ this.method = method;
9
+ }
10
+ }
11
+
12
+ function backendBase() {
13
+ return (
14
+ process.env.SAND_BACKEND_URL ||
15
+ process.env.CURSOR_API_BASE_URL ||
16
+ "https://api2.cursor.sh"
17
+ ).replace(/\/$/, "");
18
+ }
19
+
20
+ function accessTokenFromEnv() {
21
+ return (
22
+ process.env.CURSOR_ACCESS_TOKEN ||
23
+ process.env.GROK_BOT_ACCESS_TOKEN ||
24
+ process.env.SAND_ACCESS_TOKEN ||
25
+ ""
26
+ ).trim();
27
+ }
28
+
29
+ function gatewayTokenFromEnv() {
30
+ return (
31
+ process.env.GROK_BOT_GATEWAY_TOKEN ||
32
+ process.env.SAND_HOST_GATEWAY_TOKEN ||
33
+ process.env.SAND_GATEWAY_TOKEN ||
34
+ ""
35
+ ).trim();
36
+ }
37
+
38
+ function gatewayOverride() {
39
+ const token = gatewayTokenFromEnv();
40
+ const explicitUrl = (process.env.GROK_BOT_GATEWAY_URL || process.env.SAND_HOST_GATEWAY_URL || "").trim();
41
+ const localUrl = token
42
+ ? "http://127.0.0.1:" + (process.env.SAND_HOST_PORT || "1340")
43
+ : "";
44
+ const url = explicitUrl || localUrl;
45
+ if (url && token) return { gatewayUrl: url.replace(/\/$/, ""), gatewayToken: token };
46
+ return null;
47
+ }
48
+
49
+ export function hasGatewayAuth() {
50
+ return Boolean(gatewayOverride() || accessTokenFromEnv());
51
+ }
52
+
53
+ async function readJson(res) {
54
+ const text = await res.text();
55
+ if (!text) return {};
56
+ try {
57
+ return JSON.parse(text);
58
+ } catch {
59
+ return { raw: text };
60
+ }
61
+ }
62
+
63
+ function pick(obj, ...keys) {
64
+ if (!obj || typeof obj !== "object") return undefined;
65
+ for (const key of keys) {
66
+ if (obj[key] != null && obj[key] !== "") return obj[key];
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ export async function ensureSandbox(accessToken) {
72
+ const url = backendBase() + "/aiserver.v1.GrokBotService/EnsureSandBox";
73
+ const res = await fetch(url, {
74
+ method: "POST",
75
+ headers: {
76
+ "content-type": "application/json",
77
+ "connect-protocol-version": "1",
78
+ authorization: "Bearer " + accessToken,
79
+ "x-cursor-client-type": "sand",
80
+ "x-cursor-client-version": process.env.SAND_CLIENT_VERSION || "0.20.0",
81
+ "x-sand-box-namespace": process.env.SAND_BOX_NAMESPACE || "prod",
82
+ },
83
+ body: "{}",
84
+ });
85
+ const body = await readJson(res);
86
+ if (!res.ok) {
87
+ const detail = body.message || body.error || body.raw || res.statusText;
88
+ throw new GatewayError("EnsureSandBox failed: " + res.status + " " + detail, { status: res.status, method: "EnsureSandBox" });
89
+ }
90
+ const gatewayUrl = pick(body, "gatewayUrl", "gateway_url");
91
+ const gatewayToken = pick(body, "gatewayToken", "gateway_token");
92
+ if (!gatewayUrl || !gatewayToken) {
93
+ throw new GatewayError("EnsureSandBox returned no gatewayUrl/gatewayToken. Auth may be a dashboard API key (those do not work).");
94
+ }
95
+ return { gatewayUrl: String(gatewayUrl).replace(/\/$/, ""), gatewayToken: String(gatewayToken) };
96
+ }
97
+
98
+ export async function connectGateway() {
99
+ const override = gatewayOverride();
100
+ if (override) return override;
101
+ const token = accessTokenFromEnv();
102
+ if (!token) {
103
+ throw new GatewayError("Set CURSOR_ACCESS_TOKEN, or GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN. Do not use a Cursor dashboard API key.");
104
+ }
105
+ return ensureSandbox(token);
106
+ }
107
+
108
+ export async function gatewayCall(session, method, body = {}) {
109
+ const url = session.gatewayUrl + "/api/" + method;
110
+ const res = await fetch(url, {
111
+ method: "POST",
112
+ headers: {
113
+ "content-type": "application/json",
114
+ authorization: "Bearer " + session.gatewayToken,
115
+ },
116
+ body: JSON.stringify(body),
117
+ });
118
+ const data = await readJson(res);
119
+ if (!res.ok) {
120
+ const detail = data.message || data.error || data.raw || res.statusText;
121
+ throw new GatewayError(method + " failed: " + res.status + " " + String(detail).slice(0, 300), { status: res.status, method });
122
+ }
123
+ return data;
124
+ }
125
+
126
+ function asRecord(agent) {
127
+ if (!agent) return null;
128
+ const id = agent.id || agent.agentId;
129
+ const memberIds = agent.memberIds || agent.memberAgentIds || [];
130
+ return {
131
+ id,
132
+ name: agent.name || "",
133
+ title: agent.title || "",
134
+ description: agent.description || "",
135
+ isGroup: agent.isGroup === true || (agent.isGroup == null && Array.isArray(memberIds) && memberIds.length > 0),
136
+ memberIds: Array.isArray(memberIds) ? memberIds : [],
137
+ };
138
+ }
139
+
140
+ function unwrapList(data) {
141
+ if (Array.isArray(data)) return data;
142
+ if (Array.isArray(data.agents)) return data.agents;
143
+ if (data.agent) return [data.agent];
144
+ return [];
145
+ }
146
+
147
+ function unwrapOne(data) {
148
+ return asRecord(data.agent || data);
149
+ }
150
+
151
+ export async function listAgents(session) {
152
+ const data = await gatewayCall(session, "listAgents", {});
153
+ return unwrapList(data).map(asRecord).filter((r) => r && r.id);
154
+ }
155
+
156
+ function resolveFromList(records, ref) {
157
+ const needle = String(ref).trim().toLowerCase();
158
+ const byId = records.find((r) => r.id.toLowerCase() === needle);
159
+ if (byId) return byId;
160
+ const matches = records.filter((r) => r.name.toLowerCase() === needle || r.title.toLowerCase() === needle);
161
+ if (matches.length === 1) return matches[0];
162
+ if (matches.length === 0) throw new GatewayError("No bot or group named \"" + ref + "\"");
163
+ throw new GatewayError("Ambiguous name \"" + ref + "\"");
164
+ }
165
+
166
+ export async function resolveRef(session, ref) {
167
+ return resolveFromList(await listAgents(session), ref);
168
+ }
169
+
170
+ export async function createAgent(session, input) {
171
+ const data = await gatewayCall(session, "createAgent", {
172
+ name: input.name,
173
+ description: input.description || "",
174
+ title: input.title || "",
175
+ avatarShape: input.avatarShape || "",
176
+ avatarColor: input.avatarColor || "",
177
+ origin: "user",
178
+ });
179
+ return unwrapOne(data);
180
+ }
181
+
182
+ export async function deleteAgent(session, ref) {
183
+ const rec = await resolveRef(session, ref);
184
+ await gatewayCall(session, "deleteAgent", { id: rec.id });
185
+ return rec;
186
+ }
187
+
188
+ export async function createGroup(session, input) {
189
+ const records = await listAgents(session);
190
+ const memberAgentIds = (input.memberIds || []).map((ref) => resolveFromList(records, ref).id);
191
+ const data = await gatewayCall(session, "createGroup", {
192
+ name: input.name,
193
+ description: input.description || "",
194
+ memberAgentIds,
195
+ });
196
+ return unwrapOne(data);
197
+ }
198
+
199
+ export async function setGroupMembers(session, groupRef, memberRefs) {
200
+ const records = await listAgents(session);
201
+ const group = resolveFromList(records, groupRef);
202
+ const memberAgentIds = memberRefs.map((ref) => resolveFromList(records, ref).id);
203
+ const data = await gatewayCall(session, "setGroupMembers", {
204
+ id: group.id,
205
+ memberAgentIds,
206
+ });
207
+ return unwrapOne(data) || { ...group, memberIds: memberAgentIds, isGroup: true };
208
+ }
209
+
210
+ export async function addGroupMember(session, groupRef, memberRef) {
211
+ const records = await listAgents(session);
212
+ const group = resolveFromList(records, groupRef);
213
+ const member = resolveFromList(records, memberRef);
214
+ const next = [...new Set([...group.memberIds, member.id])];
215
+ return setGroupMembers(session, group.id, next);
216
+ }
217
+
218
+ export async function removeGroupMember(session, groupRef, memberRef) {
219
+ const records = await listAgents(session);
220
+ const group = resolveFromList(records, groupRef);
221
+ const member = resolveFromList(records, memberRef);
222
+ const next = group.memberIds.filter((id) => id !== member.id);
223
+ return setGroupMembers(session, group.id, next);
224
+ }
225
+
226
+ export async function sendPrompt(session, ref, prompt, extra = {}) {
227
+ const rec = await resolveRef(session, ref);
228
+ const body = {
229
+ agentId: rec.id,
230
+ prompt,
231
+ clientNonce: extra.clientNonce || randomUUID(),
232
+ };
233
+ if (extra.replyToId) body.replyToId = extra.replyToId;
234
+ const data = await gatewayCall(session, "sendPrompt", body);
235
+ return { target: rec, result: data };
236
+ }
237
+
238
+ export async function getTranscriptTail(session, ref, limit = 50) {
239
+ const rec = await resolveRef(session, ref);
240
+ const data = await gatewayCall(session, "getAgentTranscriptTail", { id: rec.id, limit });
241
+ return { target: rec, transcript: data };
242
+ }
243
+
244
+ export async function getThread(session, ref, rootId) {
245
+ const rec = await resolveRef(session, ref);
246
+ const data = await gatewayCall(session, "getAgentThread", { id: rec.id, rootId });
247
+ return { target: rec, thread: data };
248
+ }
package/src/store.js ADDED
@@ -0,0 +1,262 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ statSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ export const GROUP_JSON_VERSION = 1;
15
+ export const MAX_GROUP_MEMBERS = 6;
16
+ export const PROFILE_FILE = "profile.json";
17
+ export const GROUP_FILE = "group.json";
18
+ export const SETTINGS_FILE = "settings.json";
19
+
20
+ const UUID_RE =
21
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22
+
23
+ export class StoreError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "StoreError";
27
+ }
28
+ }
29
+
30
+ function readJson(path) {
31
+ return JSON.parse(readFileSync(path, "utf8"));
32
+ }
33
+
34
+ function writeJson(path, value) {
35
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
36
+ }
37
+
38
+ function isUuid(value) {
39
+ return UUID_RE.test(value);
40
+ }
41
+
42
+ export function defaultCandidateRoots() {
43
+ const home = homedir();
44
+ const env = [
45
+ process.env.GROK_BOT_AGENTS_DIR,
46
+ process.env.SAND_AGENTS_DIR,
47
+ process.env.SAND_DATA_ROOT && join(process.env.SAND_DATA_ROOT, "agents"),
48
+ process.env.SAND_DATA_ROOT && join(process.env.SAND_DATA_ROOT, "agent-data", "agents"),
49
+ ].filter(Boolean);
50
+
51
+ return [
52
+ ...env,
53
+ "/home/box/agent-data/agents",
54
+ "/home/box/sand-data/agents",
55
+ join(home, ".grokbot", "agent-data", "agents"),
56
+ join(home, ".grokbot", "sand-data", "agents"),
57
+ join(home, "Library", "Application Support", "Grok Bot", "agent-data", "agents"),
58
+ join(home, "Library", "Application Support", "Grok Bot", "sand-data", "agents"),
59
+ ];
60
+ }
61
+
62
+ export function looksLikeAgentsRoot(dir) {
63
+ if (!dir || !existsSync(dir) || !statSync(dir).isDirectory()) return false;
64
+ const entries = readdirSync(dir, { withFileTypes: true });
65
+ return entries.some(
66
+ (e) => e.isDirectory() && isUuid(e.name) && existsSync(join(dir, e.name, PROFILE_FILE)),
67
+ );
68
+ }
69
+
70
+ export function resolveAgentsRoot(explicit) {
71
+ if (explicit) {
72
+ mkdirSync(explicit, { recursive: true });
73
+ return explicit;
74
+ }
75
+ for (const candidate of defaultCandidateRoots()) {
76
+ if (looksLikeAgentsRoot(candidate)) return candidate;
77
+ }
78
+ throw new StoreError(
79
+ "Could not find a Grok Bot agents directory. Pass --dir or set GROK_BOT_AGENTS_DIR.\nThe macOS app keeps the live roster on its cloud computer (agent-data/agents), not in ~/Library/Application Support/Grok Bot.",
80
+ );
81
+ }
82
+
83
+ function readProfile(dir) {
84
+ const path = join(dir, PROFILE_FILE);
85
+ if (!existsSync(path)) return null;
86
+ const raw = readJson(path);
87
+ return {
88
+ name: String(raw.name ?? ""),
89
+ description: String(raw.description ?? ""),
90
+ title: String(raw.title ?? ""),
91
+ avatarShape: String(raw.avatarShape ?? ""),
92
+ avatarColor: String(raw.avatarColor ?? ""),
93
+ };
94
+ }
95
+
96
+ function readGroup(dir) {
97
+ const path = join(dir, GROUP_FILE);
98
+ if (!existsSync(path)) return null;
99
+ const raw = readJson(path);
100
+ const memberIds = Array.isArray(raw.memberIds) ? raw.memberIds.map(String) : [];
101
+ return {
102
+ version: Number(raw.version ?? GROUP_JSON_VERSION),
103
+ memberIds,
104
+ };
105
+ }
106
+
107
+ export function listRecords(root) {
108
+ if (!existsSync(root)) return [];
109
+ const out = [];
110
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
111
+ if (!entry.isDirectory() || !isUuid(entry.name)) continue;
112
+ const dir = join(root, entry.name);
113
+ const profile = readProfile(dir);
114
+ if (!profile) continue;
115
+ const group = readGroup(dir);
116
+ out.push({
117
+ id: entry.name,
118
+ path: dir,
119
+ ...profile,
120
+ isGroup: group != null,
121
+ memberIds: group?.memberIds ?? [],
122
+ });
123
+ }
124
+ return out.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
125
+ }
126
+
127
+ export function resolveRef(root, ref) {
128
+ const records = listRecords(root);
129
+ if (isUuid(ref)) {
130
+ const exact = records.find((r) => r.id.toLowerCase() === ref.toLowerCase());
131
+ if (!exact) throw new StoreError(`No bot or group with id ${ref}`);
132
+ return exact;
133
+ }
134
+ const needle = ref.trim().toLowerCase();
135
+ const matches = records.filter(
136
+ (r) => r.name.toLowerCase() === needle || r.title.toLowerCase() === needle,
137
+ );
138
+ if (matches.length === 1) return matches[0];
139
+ if (matches.length === 0) throw new StoreError(`No bot or group named "${ref}"`);
140
+ throw new StoreError(
141
+ `Ambiguous name "${ref}": ${matches.map((m) => `${m.name} (${m.id})`).join(", ")}`,
142
+ );
143
+ }
144
+
145
+ function writeProfile(dir, profile) {
146
+ writeJson(join(dir, PROFILE_FILE), {
147
+ name: profile.name.trim(),
148
+ description: (profile.description ?? "").trim(),
149
+ title: (profile.title ?? "").trim(),
150
+ avatarShape: (profile.avatarShape ?? "").trim(),
151
+ avatarColor: (profile.avatarColor ?? "").trim(),
152
+ });
153
+ }
154
+
155
+ function writeSettings(dir) {
156
+ writeJson(join(dir, SETTINGS_FILE), { notifyOnAgentUpdates: true });
157
+ }
158
+
159
+ function writeGroup(dir, memberIds) {
160
+ writeJson(join(dir, GROUP_FILE), {
161
+ version: GROUP_JSON_VERSION,
162
+ memberIds,
163
+ });
164
+ }
165
+
166
+ function normalizeMemberIds(root, memberRefs, { groupId } = {}) {
167
+ const records = listRecords(root);
168
+ const byId = new Map(records.map((r) => [r.id, r]));
169
+ const groups = new Set(records.filter((r) => r.isGroup).map((r) => r.id));
170
+ const resolved = [];
171
+ const seen = new Set();
172
+ for (const ref of memberRefs) {
173
+ const rec = resolveRef(root, ref);
174
+ if (groups.has(rec.id)) {
175
+ throw new StoreError(`Cannot add group "${rec.name}" as a member. Nested groups are not allowed.`);
176
+ }
177
+ if (groupId && rec.id === groupId) continue;
178
+ if (!byId.has(rec.id)) continue;
179
+ if (seen.has(rec.id)) continue;
180
+ seen.add(rec.id);
181
+ resolved.push(rec.id);
182
+ }
183
+ if (resolved.length > MAX_GROUP_MEMBERS) {
184
+ throw new StoreError(`A group can have at most ${MAX_GROUP_MEMBERS} members.`);
185
+ }
186
+ return resolved;
187
+ }
188
+
189
+ export function createAgent(root, input) {
190
+ const name = (input.name ?? "").trim();
191
+ if (!name) throw new StoreError("Name is required.");
192
+ const id = randomUUID();
193
+ const dir = join(root, id);
194
+ mkdirSync(dir, { recursive: true });
195
+ mkdirSync(join(dir, "memory"), { recursive: true });
196
+ mkdirSync(join(dir, "automations"), { recursive: true });
197
+ writeProfile(dir, {
198
+ name,
199
+ description: input.description ?? "",
200
+ title: input.title ?? "",
201
+ avatarShape: input.avatarShape ?? "",
202
+ avatarColor: input.avatarColor ?? "",
203
+ });
204
+ writeSettings(dir);
205
+ return resolveRef(root, id);
206
+ }
207
+
208
+ export function deleteAgent(root, ref) {
209
+ const rec = resolveRef(root, ref);
210
+ rmSync(rec.path, { recursive: true, force: true });
211
+ for (const other of listRecords(root).filter((r) => r.isGroup)) {
212
+ if (!other.memberIds.includes(rec.id)) continue;
213
+ writeGroup(
214
+ other.path,
215
+ other.memberIds.filter((id) => id !== rec.id),
216
+ );
217
+ }
218
+ return rec;
219
+ }
220
+
221
+ export function createGroup(root, input) {
222
+ const memberIds = normalizeMemberIds(root, input.memberIds ?? []);
223
+ if (memberIds.length === 0) {
224
+ throw new StoreError("A group needs at least one existing member agent.");
225
+ }
226
+ const existing = listRecords(root).find(
227
+ (r) =>
228
+ r.isGroup &&
229
+ r.memberIds.length === memberIds.length &&
230
+ r.memberIds.every((id) => memberIds.includes(id)),
231
+ );
232
+ if (existing) return existing;
233
+ const group = createAgent(root, {
234
+ name: input.name,
235
+ description: input.description ?? "",
236
+ title: input.title ?? "",
237
+ });
238
+ writeGroup(join(root, group.id), memberIds);
239
+ return resolveRef(root, group.id);
240
+ }
241
+
242
+ export function setGroupMembers(root, groupRef, memberRefs) {
243
+ const group = resolveRef(root, groupRef);
244
+ if (!group.isGroup) throw new StoreError(`"${group.name}" is a bot, not a group.`);
245
+ const memberIds = normalizeMemberIds(root, memberRefs, { groupId: group.id });
246
+ if (memberIds.length === 0) {
247
+ throw new StoreError("A group needs at least one existing member agent.");
248
+ }
249
+ writeGroup(group.path, memberIds);
250
+ return resolveRef(root, group.id);
251
+ }
252
+
253
+ export function addGroupMember(root, groupRef, memberRef) {
254
+ const group = resolveRef(root, groupRef);
255
+ return setGroupMembers(root, group.id, [...group.memberIds, memberRef]);
256
+ }
257
+
258
+ export function removeGroupMember(root, groupRef, memberRef) {
259
+ const group = resolveRef(root, groupRef);
260
+ const member = resolveRef(root, memberRef);
261
+ return setGroupMembers(root, group.id, group.memberIds.filter((id) => id !== member.id));
262
+ }