grok-bot-cli 0.1.0 → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ScriptedAlchemy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,84 +1,42 @@
1
1
  # grok-bot-cli
2
2
 
3
- CLI for [Grok Bot](https://cursor.com/help/grok-bot/plans) agents and groups: create, delete, membership, send, and read threads.
3
+ [![npm version](https://img.shields.io/npm/v/grok-bot-cli.svg)](https://www.npmjs.com/package/grok-bot-cli)
4
4
 
5
- gbot bots list
6
- gbot send Oncall status
5
+ Manage [Grok Bot](https://cursor.com/help/grok-bot/plans) agents, groups, and messages from your terminal.
7
6
 
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.
7
+ ![Live create, group, send, and delete smoke test](https://raw.githubusercontent.com/ScriptedAlchemy/grok-bot-cli/main/demo/grok-bot-cli-demo.gif)
9
8
 
10
- ## Setup
9
+ [Watch the MP4](https://github.com/ScriptedAlchemy/grok-bot-cli/blob/main/demo/grok-bot-cli-demo.mp4)
11
10
 
12
- Package: grok-bot-cli. Node 18+. Bins: gbot and grok-bot.
11
+ ## Install
13
12
 
14
- ## Auth
13
+ ```sh
14
+ npm install --global grok-bot-cli
15
+ ```
15
16
 
16
- Preferred: a Cursor session JWT in CURSOR_ACCESS_TOKEN.
17
+ Requires Node.js 18+ and the Grok Bot macOS app. Open Grok Bot and sign in once; `gbot` automatically uses the app's encrypted session and routing credentials. No token copying is required.
17
18
 
18
- gbot bots list
19
+ ## Use
19
20
 
20
- Or set GROK_BOT_GATEWAY_URL plus GROK_BOT_GATEWAY_TOKEN and pass --gateway.
21
+ ```sh
22
+ gbot bots list
23
+ gbot bots create --name Researcher
24
+ gbot bots update Researcher --description "Research the launch" --notify on
25
+ gbot bots create --name Writer
26
+ gbot groups create --name Launch --member Researcher --member Writer --description "Ship together"
27
+ gbot groups update Launch --title "Launch room" --hidden off
28
+ gbot send Researcher "Summarize the launch status."
29
+ gbot send Launch "Share your updates."
30
+ gbot thread Researcher
31
+ gbot groups delete Launch
32
+ gbot bots delete Researcher
33
+ gbot bots delete Writer
34
+ ```
21
35
 
22
- On a Grok Bot computer, SAND_GATEWAY_TOKEN and SAND_HOST_PORT are picked up automatically.
36
+ `update` fields: `--name` `--description`/`--instructions` `--title` `--avatar-shape` `--avatar-color` `--notify` `--hidden`. `--description` is the UI Instructions field.
23
37
 
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.
38
+ Run `gbot --help` for every command.
80
39
 
81
40
  ## License
82
41
 
83
42
  MIT
84
-
package/package.json CHANGED
@@ -1,28 +1,48 @@
1
1
  {
2
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",
3
+ "version": "0.2.1",
4
+ "description": "CLI for Grok Bot agents and groups: create, update, message, inspect threads, and automate cleanup",
5
5
  "type": "module",
6
6
  "bin": {
7
- "gbot": "./src/cli.js",
8
- "grok-bot": "./src/cli.js"
7
+ "gbot": "src/cli.js",
8
+ "grok-bot": "src/cli.js"
9
9
  },
10
10
  "engines": {
11
11
  "node": ">=18"
12
12
  },
13
13
  "scripts": {
14
+ "changeset": "changeset",
14
15
  "gbot": "node src/cli.js",
16
+ "release": "changeset publish",
15
17
  "test": "node --test"
16
18
  },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ScriptedAlchemy/grok-bot-cli.git"
22
+ },
23
+ "homepage": "https://github.com/ScriptedAlchemy/grok-bot-cli#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/ScriptedAlchemy/grok-bot-cli/issues"
26
+ },
27
+ "author": "Zack Jackson (https://github.com/ScriptedAlchemy)",
17
28
  "license": "MIT",
18
29
  "keywords": [
30
+ "grok",
19
31
  "grok-bot",
32
+ "grokbot",
20
33
  "gbot",
21
34
  "cursor",
35
+ "ai-agents",
36
+ "automation",
37
+ "terminal",
22
38
  "cli"
23
39
  ],
24
40
  "files": [
25
41
  "src",
26
- "README.md"
27
- ]
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "devDependencies": {
46
+ "@changesets/cli": "2.31.1"
47
+ }
28
48
  }
@@ -0,0 +1,74 @@
1
+ import crypto from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const SAFE_STORAGE_PREFIX = Buffer.from("v10");
8
+
9
+ export function decryptSafeStorageString(encryptedBase64, password) {
10
+ const encrypted = Buffer.from(encryptedBase64, "base64");
11
+ if (!encrypted.subarray(0, 3).equals(SAFE_STORAGE_PREFIX)) {
12
+ throw new Error("Unsupported Grok Bot Safe Storage format.");
13
+ }
14
+
15
+ const key = crypto.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
16
+ const decipher = crypto.createDecipheriv(
17
+ "aes-128-cbc",
18
+ key,
19
+ Buffer.alloc(16, 32),
20
+ );
21
+ return Buffer.concat([
22
+ decipher.update(encrypted.subarray(3)),
23
+ decipher.final(),
24
+ ]).toString("utf8");
25
+ }
26
+
27
+ export function grokBotGatewayDescriptorPath(home = homedir()) {
28
+ return join(
29
+ home,
30
+ "Library/Application Support/Grok Bot/gateway-descriptor.json",
31
+ );
32
+ }
33
+
34
+ export function hasGrokBotGatewaySession({
35
+ platform = process.platform,
36
+ home = homedir(),
37
+ } = {}) {
38
+ return platform === "darwin" && existsSync(grokBotGatewayDescriptorPath(home));
39
+ }
40
+
41
+ function readKeychainPassword() {
42
+ return execFileSync(
43
+ "/usr/bin/security",
44
+ ["find-generic-password", "-w", "-s", "Grok Bot Safe Storage"],
45
+ { encoding: "utf8" },
46
+ ).trimEnd();
47
+ }
48
+
49
+ export function loadGrokBotGatewaySession({
50
+ platform = process.platform,
51
+ home = homedir(),
52
+ getKeychainPassword = readKeychainPassword,
53
+ } = {}) {
54
+ if (platform !== "darwin") return null;
55
+
56
+ const path = grokBotGatewayDescriptorPath(home);
57
+ if (!existsSync(path)) return null;
58
+
59
+ const wrapped = JSON.parse(readFileSync(path, "utf8"));
60
+ const clear = decryptSafeStorageString(
61
+ wrapped.encrypted,
62
+ getKeychainPassword(),
63
+ );
64
+ const descriptor = JSON.parse(clear);
65
+ if (!descriptor.baseUrl || !descriptor.token) {
66
+ throw new Error("Grok Bot gateway descriptor is incomplete.");
67
+ }
68
+
69
+ return {
70
+ gatewayUrl: String(descriptor.baseUrl).replace(/\/$/, ""),
71
+ gatewayToken: String(descriptor.token),
72
+ headers: descriptor.headers ?? {},
73
+ };
74
+ }
package/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { MAX_GROUP_MEMBERS, StoreError, defaultCandidateRoots, looksLikeAgentsRoot, resolveAgentsRoot } from "./store.js";
2
+ import { AVATAR_COLORS, AVATAR_SHAPES, MAX_GROUP_MEMBERS, StoreError, defaultCandidateRoots, looksLikeAgentsRoot, resolveAgentsRoot } from "./store.js";
3
3
  import { hasGatewayAuth } from "./gateway.js";
4
4
  import { openBackend } from "./commands.js";
5
5
 
@@ -25,11 +25,18 @@ function usage() {
25
25
  "Commands:",
26
26
  " doctor",
27
27
  " bots list",
28
- " bots create --name NAME [--description TEXT] [--title TEXT]",
28
+ " bots create --name NAME [--description TEXT] [--instructions TEXT] [--title TEXT]",
29
+ " [--avatar-shape SHAPE] [--avatar-color COLOR]",
30
+ " bots update <id-or-name> [--name NAME] [--description TEXT] [--instructions TEXT]",
31
+ " [--title TEXT] [--avatar-shape SHAPE] [--avatar-color COLOR]",
32
+ " [--notify on|off] [--hidden on|off]",
29
33
  " bots get <id-or-name>",
30
34
  " bots delete <id-or-name>",
31
35
  " groups list",
32
36
  " groups create --name NAME --member ID_OR_NAME [--member ...]",
37
+ " [--description TEXT] [--instructions TEXT] [--title TEXT]",
38
+ " [--avatar-shape SHAPE] [--avatar-color COLOR]",
39
+ " groups update <id-or-name> (same flags as bots update; members stay on set/add/remove)",
33
40
  " groups get <id-or-name>",
34
41
  " groups members <id-or-name>",
35
42
  " groups add <group> <bot>",
@@ -41,8 +48,11 @@ function usage() {
41
48
  " chat <bot-or-group> alias for thread",
42
49
  "",
43
50
  "Max group members: " + MAX_GROUP_MEMBERS,
51
+ "--description / --instructions is the UI Instructions field (same key).",
52
+ "Avatar shapes: " + AVATAR_SHAPES.join(" "),
53
+ "Avatar colors: " + AVATAR_COLORS.join(" "),
44
54
  "Flags: --gateway --files --dir DIR --json",
45
- "Env: CURSOR_ACCESS_TOKEN or GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN",
55
+ "Auth: GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN, or the Grok Bot app session, or CURSOR_ACCESS_TOKEN",
46
56
  "File fallback: GROK_BOT_AGENTS_DIR",
47
57
  ].join("\n");
48
58
  }
@@ -56,6 +66,27 @@ function takeFlag(args, name) {
56
66
  return value;
57
67
  }
58
68
 
69
+ function takeLastFlag(args, ...names) {
70
+ let value;
71
+ let seen = false;
72
+ for (;;) {
73
+ let hit = false;
74
+ for (const name of names) {
75
+ const i = args.indexOf(name);
76
+ if (i === -1) continue;
77
+ const next = args[i + 1];
78
+ if (next == null || next.startsWith("-")) throw new StoreError(name + " needs a value");
79
+ args.splice(i, 2);
80
+ value = next;
81
+ seen = true;
82
+ hit = true;
83
+ break;
84
+ }
85
+ if (!hit) break;
86
+ }
87
+ return seen ? value : undefined;
88
+ }
89
+
59
90
  function takeRepeating(args, name) {
60
91
  const out = [];
61
92
  for (;;) {
@@ -73,12 +104,55 @@ function hasFlag(args, name) {
73
104
  return true;
74
105
  }
75
106
 
107
+ function parseOnOff(value, flag) {
108
+ const v = String(value).trim().toLowerCase();
109
+ if (v === "on" || v === "true" || v === "1" || v === "yes") return true;
110
+ if (v === "off" || v === "false" || v === "0" || v === "no") return false;
111
+ throw new StoreError(flag + " must be on|off (or true|false|1|0|yes|no)");
112
+ }
113
+
114
+ function takeCreateFields(args) {
115
+ return {
116
+ name: takeFlag(args, "--name"),
117
+ description: takeLastFlag(args, "--description", "--instructions") ?? "",
118
+ title: takeFlag(args, "--title") ?? "",
119
+ avatarShape: takeFlag(args, "--avatar-shape") ?? "",
120
+ avatarColor: takeFlag(args, "--avatar-color") ?? "",
121
+ };
122
+ }
123
+
124
+ function takeUpdatePatch(args) {
125
+ const name = takeFlag(args, "--name");
126
+ const description = takeLastFlag(args, "--description", "--instructions");
127
+ const title = takeFlag(args, "--title");
128
+ const avatarShape = takeFlag(args, "--avatar-shape");
129
+ const avatarColor = takeFlag(args, "--avatar-color");
130
+ const notifyRaw = takeFlag(args, "--notify");
131
+ const hiddenRaw = takeFlag(args, "--hidden");
132
+ const patch = {};
133
+ if (name !== undefined) patch.name = name;
134
+ if (description !== undefined) patch.description = description;
135
+ if (title !== undefined) patch.title = title;
136
+ if (avatarShape !== undefined) patch.avatarShape = avatarShape;
137
+ if (avatarColor !== undefined) patch.avatarColor = avatarColor;
138
+ if (notifyRaw !== undefined) patch.notifyOnAgentUpdates = parseOnOff(notifyRaw, "--notify");
139
+ if (hiddenRaw !== undefined) patch.hiddenFromSidebar = parseOnOff(hiddenRaw, "--hidden");
140
+ if (Object.keys(patch).length === 0) {
141
+ throw new StoreError("update needs at least one of --name --description --instructions --title --avatar-shape --avatar-color --notify --hidden");
142
+ }
143
+ return patch;
144
+ }
145
+
76
146
  function summarize(rec) {
77
147
  return {
78
148
  id: rec.id,
79
149
  name: rec.name,
80
150
  title: rec.title || undefined,
81
151
  description: rec.description || undefined,
152
+ avatarShape: rec.avatarShape || undefined,
153
+ avatarColor: rec.avatarColor || undefined,
154
+ ...(rec.notifyOnAgentUpdates !== undefined ? { notifyOnAgentUpdates: rec.notifyOnAgentUpdates } : {}),
155
+ ...(rec.hiddenFromSidebar !== undefined ? { hiddenFromSidebar: rec.hiddenFromSidebar } : {}),
82
156
  kind: rec.isGroup ? "group" : "bot",
83
157
  members: rec.isGroup ? rec.memberIds : undefined,
84
158
  };
@@ -98,8 +172,15 @@ function formatRecord(rec, all) {
98
172
  : "";
99
173
  const title = rec.title ? " - " + rec.title : "";
100
174
  const desc = rec.description ? "\n " + rec.description : "";
175
+ const avatar = rec.avatarShape || rec.avatarColor
176
+ ? "\n avatar: " + [rec.avatarShape, rec.avatarColor].filter(Boolean).join(" ")
177
+ : "";
178
+ const settings = [];
179
+ if (rec.notifyOnAgentUpdates !== undefined) settings.push("notify " + (rec.notifyOnAgentUpdates ? "on" : "off"));
180
+ if (rec.hiddenFromSidebar !== undefined) settings.push("hidden " + (rec.hiddenFromSidebar ? "on" : "off"));
181
+ const settingsLine = settings.length ? "\n " + settings.join(", ") : "";
101
182
  const extra = rec.isGroup ? "\n members (" + rec.memberIds.length + "): " + (members || "(none)") : "";
102
- return kind + " " + rec.name + title + "\n " + rec.id + desc + extra;
183
+ return kind + " " + rec.name + title + "\n " + rec.id + desc + avatar + settingsLine + extra;
103
184
  }
104
185
 
105
186
  function entryText(e) {
@@ -191,14 +272,20 @@ async function main(argv) {
191
272
  }
192
273
 
193
274
  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 });
275
+ const fields = takeCreateFields(rest);
276
+ const rec = await backend.createAgent(fields);
198
277
  done(json, rec, "Created bot " + rec.name + " (" + rec.id + ")");
199
278
  return;
200
279
  }
201
280
 
281
+ if (cmd === "bots" && sub === "update") {
282
+ const ref = rest.shift();
283
+ if (!ref || ref.startsWith("-")) throw new StoreError("gbot bots update <id-or-name> [--name NAME] ...");
284
+ const rec = await backend.updateAgent(ref, takeUpdatePatch(rest));
285
+ done(json, rec, "Updated " + (rec.isGroup ? "group" : "bot") + " " + rec.name + " (" + rec.id + ")");
286
+ return;
287
+ }
288
+
202
289
  if (cmd === "bots" && (sub === "get" || sub === "delete")) {
203
290
  const ref = rest[0];
204
291
  if (!ref) throw new StoreError("gbot bots " + sub + " <id-or-name>");
@@ -233,14 +320,23 @@ async function main(argv) {
233
320
  }
234
321
 
235
322
  if (cmd === "groups" && sub === "create") {
236
- const name = takeFlag(rest, "--name");
237
- const description = takeFlag(rest, "--description") ?? "";
323
+ const fields = takeCreateFields(rest);
238
324
  const members = takeRepeating(rest, "--member");
239
- const rec = await backend.createGroup({ name, description, memberIds: members });
325
+ const rec = await backend.createGroup({ ...fields, memberIds: members });
240
326
  done(json, rec, "Created group " + rec.name + " (" + rec.id + ") with " + rec.memberIds.length + " members");
241
327
  return;
242
328
  }
243
329
 
330
+ if (cmd === "groups" && sub === "update") {
331
+ const ref = rest.shift();
332
+ if (!ref || ref.startsWith("-")) throw new StoreError("gbot groups update <id-or-name> [--name NAME] ...");
333
+ const current = await backend.resolve(ref);
334
+ if (!current.isGroup) throw new StoreError('"' + current.name + '" is a bot, not a group. Use bots update.');
335
+ const rec = await backend.updateAgent(ref, takeUpdatePatch(rest));
336
+ done(json, rec, "Updated group " + rec.name + " (" + rec.id + ")");
337
+ return;
338
+ }
339
+
244
340
  if (cmd === "groups" && (sub === "get" || sub === "members")) {
245
341
  const ref = rest[0];
246
342
  if (!ref) throw new StoreError("gbot groups " + sub + " <id-or-name>");
package/src/commands.js CHANGED
@@ -21,6 +21,7 @@ export async function openBackend(opts) {
21
21
  list: () => gw.listAgents(session),
22
22
  resolve: (ref) => gw.resolveRef(session, ref),
23
23
  createAgent: (input) => gw.createAgent(session, input),
24
+ updateAgent: (ref, patch) => gw.updateAgent(session, ref, patch),
24
25
  deleteAgent: (ref) => gw.deleteAgent(session, ref),
25
26
  createGroup: (input) => gw.createGroup(session, input),
26
27
  setGroupMembers: (group, members) => gw.setGroupMembers(session, group, members),
@@ -38,6 +39,7 @@ export async function openBackend(opts) {
38
39
  list: async () => files.listRecords(root),
39
40
  resolve: async (ref) => files.resolveRef(root, ref),
40
41
  createAgent: async (input) => files.createAgent(root, input),
42
+ updateAgent: async (ref, patch) => files.updateAgent(root, ref, patch),
41
43
  deleteAgent: async (ref) => files.deleteAgent(root, ref),
42
44
  createGroup: async (input) => files.createGroup(root, input),
43
45
  setGroupMembers: async (group, members) => files.setGroupMembers(root, group, members),
package/src/gateway.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { ensureSandboxHeaders, headersFromEnsureSandbox, headersFromEnv, mergeGatewayHeaders, normalizeHeaderMap, requestHeaders } from "./headers.js";
3
+ import { hasGrokBotGatewaySession, loadGrokBotGatewaySession } from "./app-session.js";
4
+ import { AVATAR_COLORS, AVATAR_SHAPES } from "./store.js";
2
5
 
3
6
  export class GatewayError extends Error {
4
7
  constructor(message, { status, method } = {}) {
@@ -42,12 +45,22 @@ function gatewayOverride() {
42
45
  ? "http://127.0.0.1:" + (process.env.SAND_HOST_PORT || "1340")
43
46
  : "";
44
47
  const url = explicitUrl || localUrl;
45
- if (url && token) return { gatewayUrl: url.replace(/\/$/, ""), gatewayToken: token };
48
+ if (url && token) return { gatewayUrl: url.replace(/\/$/, ""), gatewayToken: token, gatewayHeaders: headersFromEnv() };
46
49
  return null;
47
50
  }
48
51
 
52
+ function sessionFromApp() {
53
+ const loaded = loadGrokBotGatewaySession();
54
+ if (!loaded) return null;
55
+ return {
56
+ gatewayUrl: loaded.gatewayUrl,
57
+ gatewayToken: loaded.gatewayToken,
58
+ gatewayHeaders: mergeGatewayHeaders(normalizeHeaderMap(loaded.headers), headersFromEnv()),
59
+ };
60
+ }
61
+
49
62
  export function hasGatewayAuth() {
50
- return Boolean(gatewayOverride() || accessTokenFromEnv());
63
+ return Boolean(gatewayOverride() || accessTokenFromEnv() || hasGrokBotGatewaySession());
51
64
  }
52
65
 
53
66
  async function readJson(res) {
@@ -72,14 +85,7 @@ export async function ensureSandbox(accessToken) {
72
85
  const url = backendBase() + "/aiserver.v1.GrokBotService/EnsureSandBox";
73
86
  const res = await fetch(url, {
74
87
  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
- },
88
+ headers: ensureSandboxHeaders(accessToken),
83
89
  body: "{}",
84
90
  });
85
91
  const body = await readJson(res);
@@ -92,12 +98,14 @@ export async function ensureSandbox(accessToken) {
92
98
  if (!gatewayUrl || !gatewayToken) {
93
99
  throw new GatewayError("EnsureSandBox returned no gatewayUrl/gatewayToken. Auth may be a dashboard API key (those do not work).");
94
100
  }
95
- return { gatewayUrl: String(gatewayUrl).replace(/\/$/, ""), gatewayToken: String(gatewayToken) };
101
+ return { gatewayUrl: String(gatewayUrl).replace(/\/$/, ""), gatewayToken: String(gatewayToken), gatewayHeaders: mergeGatewayHeaders(headersFromEnsureSandbox(body), headersFromEnv()) };
96
102
  }
97
103
 
98
104
  export async function connectGateway() {
99
105
  const override = gatewayOverride();
100
106
  if (override) return override;
107
+ const fromApp = sessionFromApp();
108
+ if (fromApp) return fromApp;
101
109
  const token = accessTokenFromEnv();
102
110
  if (!token) {
103
111
  throw new GatewayError("Set CURSOR_ACCESS_TOKEN, or GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN. Do not use a Cursor dashboard API key.");
@@ -109,10 +117,7 @@ export async function gatewayCall(session, method, body = {}) {
109
117
  const url = session.gatewayUrl + "/api/" + method;
110
118
  const res = await fetch(url, {
111
119
  method: "POST",
112
- headers: {
113
- "content-type": "application/json",
114
- authorization: "Bearer " + session.gatewayToken,
115
- },
120
+ headers: requestHeaders(session),
116
121
  body: JSON.stringify(body),
117
122
  });
118
123
  const data = await readJson(res);
@@ -127,16 +132,31 @@ function asRecord(agent) {
127
132
  if (!agent) return null;
128
133
  const id = agent.id || agent.agentId;
129
134
  const memberIds = agent.memberIds || agent.memberAgentIds || [];
135
+ const notify = agent.notifyOnUpdatesEnabled ?? agent.notifyOnAgentUpdates;
136
+ const hidden = agent.isHiddenFromSidebar ?? agent.hiddenFromSidebar;
130
137
  return {
131
138
  id,
132
139
  name: agent.name || "",
133
140
  title: agent.title || "",
134
141
  description: agent.description || "",
142
+ avatarShape: agent.avatarShape || "",
143
+ avatarColor: agent.avatarColor || "",
144
+ ...(notify !== undefined ? { notifyOnAgentUpdates: Boolean(notify) } : {}),
145
+ ...(hidden !== undefined ? { hiddenFromSidebar: Boolean(hidden) } : {}),
135
146
  isGroup: agent.isGroup === true || (agent.isGroup == null && Array.isArray(memberIds) && memberIds.length > 0),
136
147
  memberIds: Array.isArray(memberIds) ? memberIds : [],
137
148
  };
138
149
  }
139
150
 
151
+ function assertAvatar(kind, value, allowed) {
152
+ const trimmed = (value ?? "").trim();
153
+ if (!trimmed) return "";
154
+ if (!allowed.includes(trimmed)) {
155
+ throw new GatewayError("Unknown avatar " + kind + " \"" + value + "\". Use: " + allowed.join(" "));
156
+ }
157
+ return trimmed;
158
+ }
159
+
140
160
  function unwrapList(data) {
141
161
  if (Array.isArray(data)) return data;
142
162
  if (Array.isArray(data.agents)) return data.agents;
@@ -172,13 +192,36 @@ export async function createAgent(session, input) {
172
192
  name: input.name,
173
193
  description: input.description || "",
174
194
  title: input.title || "",
175
- avatarShape: input.avatarShape || "",
176
- avatarColor: input.avatarColor || "",
195
+ avatarShape: assertAvatar("shape", input.avatarShape, AVATAR_SHAPES),
196
+ avatarColor: assertAvatar("color", input.avatarColor, AVATAR_COLORS),
177
197
  origin: "user",
178
198
  });
179
199
  return unwrapOne(data);
180
200
  }
181
201
 
202
+ export async function updateAgent(session, ref, patch = {}) {
203
+ const rec = await resolveRef(session, ref);
204
+ if (patch.name !== undefined && !String(patch.name).trim()) {
205
+ throw new GatewayError("Name cannot be blank.");
206
+ }
207
+ const profile = {
208
+ name: patch.name !== undefined ? String(patch.name).trim() : rec.name,
209
+ description: patch.description !== undefined ? String(patch.description) : rec.description,
210
+ };
211
+ if (patch.title !== undefined) profile.title = String(patch.title);
212
+ if (patch.avatarShape !== undefined) profile.avatarShape = assertAvatar("shape", patch.avatarShape, AVATAR_SHAPES);
213
+ if (patch.avatarColor !== undefined) profile.avatarColor = assertAvatar("color", patch.avatarColor, AVATAR_COLORS);
214
+ await gatewayCall(session, "updateAgent", { id: rec.id, profile });
215
+ if (patch.notifyOnAgentUpdates !== undefined) {
216
+ await gatewayCall(session, "setAgentNotifyOnUpdates", { id: rec.id, isEnabled: Boolean(patch.notifyOnAgentUpdates) });
217
+ }
218
+ if (patch.hiddenFromSidebar !== undefined) {
219
+ await gatewayCall(session, "setAgentHiddenFromSidebar", { id: rec.id, isHidden: Boolean(patch.hiddenFromSidebar) });
220
+ }
221
+ const fresh = (await listAgents(session)).find((r) => r.id === rec.id);
222
+ return fresh || rec;
223
+ }
224
+
182
225
  export async function deleteAgent(session, ref) {
183
226
  const rec = await resolveRef(session, ref);
184
227
  await gatewayCall(session, "deleteAgent", { id: rec.id });
@@ -193,7 +236,13 @@ export async function createGroup(session, input) {
193
236
  description: input.description || "",
194
237
  memberAgentIds,
195
238
  });
196
- return unwrapOne(data);
239
+ const rec = unwrapOne(data);
240
+ const extras = {};
241
+ if (input.title) extras.title = input.title;
242
+ if (input.avatarShape) extras.avatarShape = input.avatarShape;
243
+ if (input.avatarColor) extras.avatarColor = input.avatarColor;
244
+ if (Object.keys(extras).length && rec?.id) return updateAgent(session, rec.id, extras);
245
+ return rec;
197
246
  }
198
247
 
199
248
  export async function setGroupMembers(session, groupRef, memberRefs) {
package/src/headers.js ADDED
@@ -0,0 +1,53 @@
1
+ export function normalizeHeaderMap(obj) {
2
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return {};
3
+ const out = {};
4
+ for (const [key, value] of Object.entries(obj)) {
5
+ if (value == null || value === "") continue;
6
+ out[String(key).toLowerCase()] = String(value);
7
+ }
8
+ return out;
9
+ }
10
+
11
+ export function parseGatewayHeaders(raw) {
12
+ if (raw == null || String(raw).trim() === "") return {};
13
+ const parsed = JSON.parse(String(raw));
14
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15
+ throw new Error("GROK_BOT_GATEWAY_HEADERS must be a JSON object");
16
+ }
17
+ return normalizeHeaderMap(parsed);
18
+ }
19
+
20
+ export function headersFromEnsureSandbox(body) {
21
+ if (!body || typeof body !== "object") return {};
22
+ const mapped = normalizeHeaderMap(body.gatewayHeaders || body.gateway_headers);
23
+ if (Object.keys(mapped).length) return mapped;
24
+ const token = body.anyrunNetworkToken || body.anyrun_network_token || body.networkToken;
25
+ return token ? { "x-anyrun-network-token": String(token) } : {};
26
+ }
27
+
28
+ export function headersFromEnv() {
29
+ return parseGatewayHeaders(process.env.GROK_BOT_GATEWAY_HEADERS);
30
+ }
31
+
32
+ export function mergeGatewayHeaders(sessionHeaders, envHeaders) {
33
+ return { ...(sessionHeaders || {}), ...(envHeaders || {}) };
34
+ }
35
+
36
+ export function requestHeaders(session) {
37
+ return {
38
+ "content-type": "application/json",
39
+ authorization: "Bearer " + session.gatewayToken,
40
+ ...(session.gatewayHeaders || {}),
41
+ };
42
+ }
43
+
44
+ export function ensureSandboxHeaders(accessToken) {
45
+ return {
46
+ "content-type": "application/json",
47
+ "connect-protocol-version": "1",
48
+ authorization: "Bearer " + accessToken,
49
+ "x-cursor-client-type": "sand",
50
+ "x-cursor-client-version": process.env.SAND_CLIENT_VERSION || "0.20.0",
51
+ "x-sand-box-namespace": process.env.SAND_BOX_NAMESPACE || "prod",
52
+ };
53
+ }
package/src/store.js CHANGED
@@ -17,6 +17,41 @@ export const PROFILE_FILE = "profile.json";
17
17
  export const GROUP_FILE = "group.json";
18
18
  export const SETTINGS_FILE = "settings.json";
19
19
 
20
+ export const AVATAR_SHAPES = [
21
+ "blob",
22
+ "pebble",
23
+ "bean",
24
+ "egg",
25
+ "squircle",
26
+ "tablet",
27
+ "capsule",
28
+ "cylinder",
29
+ "hex",
30
+ "gem",
31
+ "crystal",
32
+ "wedge",
33
+ "shield",
34
+ "dome",
35
+ "arch",
36
+ "cloud",
37
+ "teardrop",
38
+ "leaf",
39
+ ];
40
+
41
+ export const AVATAR_COLORS = [
42
+ "black",
43
+ "brown",
44
+ "red",
45
+ "orange",
46
+ "yellow",
47
+ "green",
48
+ "cyan",
49
+ "blue",
50
+ "violet",
51
+ "magenta",
52
+ "gray",
53
+ ];
54
+
20
55
  const UUID_RE =
21
56
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22
57
 
@@ -104,6 +139,25 @@ function readGroup(dir) {
104
139
  };
105
140
  }
106
141
 
142
+ function readRawSettings(dir) {
143
+ const path = join(dir, SETTINGS_FILE);
144
+ if (!existsSync(path)) return {};
145
+ try {
146
+ const raw = readJson(path);
147
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
148
+ } catch {
149
+ return {};
150
+ }
151
+ }
152
+
153
+ function readSettings(dir) {
154
+ const raw = readRawSettings(dir);
155
+ return {
156
+ notifyOnAgentUpdates: typeof raw.notifyOnAgentUpdates === "boolean" ? raw.notifyOnAgentUpdates : true,
157
+ hiddenFromSidebar: typeof raw.hiddenFromSidebar === "boolean" ? raw.hiddenFromSidebar : false,
158
+ };
159
+ }
160
+
107
161
  export function listRecords(root) {
108
162
  if (!existsSync(root)) return [];
109
163
  const out = [];
@@ -117,6 +171,7 @@ export function listRecords(root) {
117
171
  id: entry.name,
118
172
  path: dir,
119
173
  ...profile,
174
+ ...readSettings(dir),
120
175
  isGroup: group != null,
121
176
  memberIds: group?.memberIds ?? [],
122
177
  });
@@ -142,13 +197,22 @@ export function resolveRef(root, ref) {
142
197
  );
143
198
  }
144
199
 
200
+ function assertAvatar(kind, value, allowed) {
201
+ const trimmed = (value ?? "").trim();
202
+ if (!trimmed) return "";
203
+ if (!allowed.includes(trimmed)) {
204
+ throw new StoreError(`Unknown avatar ${kind} "${value}". Use: ${allowed.join(" ")}`);
205
+ }
206
+ return trimmed;
207
+ }
208
+
145
209
  function writeProfile(dir, profile) {
146
210
  writeJson(join(dir, PROFILE_FILE), {
147
211
  name: profile.name.trim(),
148
212
  description: (profile.description ?? "").trim(),
149
213
  title: (profile.title ?? "").trim(),
150
- avatarShape: (profile.avatarShape ?? "").trim(),
151
- avatarColor: (profile.avatarColor ?? "").trim(),
214
+ avatarShape: assertAvatar("shape", profile.avatarShape, AVATAR_SHAPES),
215
+ avatarColor: assertAvatar("color", profile.avatarColor, AVATAR_COLORS),
152
216
  });
153
217
  }
154
218
 
@@ -156,6 +220,14 @@ function writeSettings(dir) {
156
220
  writeJson(join(dir, SETTINGS_FILE), { notifyOnAgentUpdates: true });
157
221
  }
158
222
 
223
+ function mergeSettings(dir, patch) {
224
+ const update = {};
225
+ if (patch.notifyOnAgentUpdates !== undefined) update.notifyOnAgentUpdates = Boolean(patch.notifyOnAgentUpdates);
226
+ if (patch.hiddenFromSidebar !== undefined) update.hiddenFromSidebar = Boolean(patch.hiddenFromSidebar);
227
+ if (Object.keys(update).length === 0) return;
228
+ writeJson(join(dir, SETTINGS_FILE), { ...readRawSettings(dir), ...update });
229
+ }
230
+
159
231
  function writeGroup(dir, memberIds) {
160
232
  writeJson(join(dir, GROUP_FILE), {
161
233
  version: GROUP_JSON_VERSION,
@@ -163,9 +235,8 @@ function writeGroup(dir, memberIds) {
163
235
  });
164
236
  }
165
237
 
166
- function normalizeMemberIds(root, memberRefs, { groupId } = {}) {
238
+ function normalizeMemberIds(root, memberRefs) {
167
239
  const records = listRecords(root);
168
- const byId = new Map(records.map((r) => [r.id, r]));
169
240
  const groups = new Set(records.filter((r) => r.isGroup).map((r) => r.id));
170
241
  const resolved = [];
171
242
  const seen = new Set();
@@ -174,8 +245,6 @@ function normalizeMemberIds(root, memberRefs, { groupId } = {}) {
174
245
  if (groups.has(rec.id)) {
175
246
  throw new StoreError(`Cannot add group "${rec.name}" as a member. Nested groups are not allowed.`);
176
247
  }
177
- if (groupId && rec.id === groupId) continue;
178
- if (!byId.has(rec.id)) continue;
179
248
  if (seen.has(rec.id)) continue;
180
249
  seen.add(rec.id);
181
250
  resolved.push(rec.id);
@@ -205,6 +274,31 @@ export function createAgent(root, input) {
205
274
  return resolveRef(root, id);
206
275
  }
207
276
 
277
+ export function updateAgent(root, ref, patch = {}) {
278
+ const rec = resolveRef(root, ref);
279
+ if (patch.name !== undefined) {
280
+ const name = String(patch.name).trim();
281
+ if (!name) throw new StoreError("Name cannot be blank.");
282
+ }
283
+ const hasProfile =
284
+ patch.name !== undefined ||
285
+ patch.description !== undefined ||
286
+ patch.title !== undefined ||
287
+ patch.avatarShape !== undefined ||
288
+ patch.avatarColor !== undefined;
289
+ if (hasProfile) {
290
+ writeProfile(rec.path, {
291
+ name: patch.name !== undefined ? String(patch.name) : rec.name,
292
+ description: patch.description !== undefined ? String(patch.description) : rec.description,
293
+ title: patch.title !== undefined ? String(patch.title) : rec.title,
294
+ avatarShape: patch.avatarShape !== undefined ? String(patch.avatarShape) : rec.avatarShape,
295
+ avatarColor: patch.avatarColor !== undefined ? String(patch.avatarColor) : rec.avatarColor,
296
+ });
297
+ }
298
+ mergeSettings(rec.path, patch);
299
+ return resolveRef(root, rec.id);
300
+ }
301
+
208
302
  export function deleteAgent(root, ref) {
209
303
  const rec = resolveRef(root, ref);
210
304
  rmSync(rec.path, { recursive: true, force: true });
@@ -234,6 +328,8 @@ export function createGroup(root, input) {
234
328
  name: input.name,
235
329
  description: input.description ?? "",
236
330
  title: input.title ?? "",
331
+ avatarShape: input.avatarShape ?? "",
332
+ avatarColor: input.avatarColor ?? "",
237
333
  });
238
334
  writeGroup(join(root, group.id), memberIds);
239
335
  return resolveRef(root, group.id);
@@ -242,7 +338,7 @@ export function createGroup(root, input) {
242
338
  export function setGroupMembers(root, groupRef, memberRefs) {
243
339
  const group = resolveRef(root, groupRef);
244
340
  if (!group.isGroup) throw new StoreError(`"${group.name}" is a bot, not a group.`);
245
- const memberIds = normalizeMemberIds(root, memberRefs, { groupId: group.id });
341
+ const memberIds = normalizeMemberIds(root, memberRefs);
246
342
  if (memberIds.length === 0) {
247
343
  throw new StoreError("A group needs at least one existing member agent.");
248
344
  }