grok-bot-cli 0.2.1 → 0.2.3

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 CHANGED
@@ -14,7 +14,7 @@ Manage [Grok Bot](https://cursor.com/help/grok-bot/plans) agents, groups, and me
14
14
  npm install --global grok-bot-cli
15
15
  ```
16
16
 
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
+ Requires Node.js 18+ and the Grok Bot desktop app on macOS or Linux. Open Grok Bot and sign in once; `gbot` automatically uses the app's encrypted session and routing credentials. No token copying is required. On Linux the app keeps its session under `~/.config/Grok Bot` (or `$XDG_CONFIG_HOME`); when it is stored in the system keyring, `gbot` reads the key with `secret-tool` (package `libsecret-tools`).
18
18
 
19
19
  ## Use
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grok-bot-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI for Grok Bot agents and groups: create, update, message, inspect threads, and automate cleanup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,6 +43,6 @@
43
43
  "LICENSE"
44
44
  ],
45
45
  "devDependencies": {
46
- "@changesets/cli": "2.31.1"
46
+ "@changesets/cli": "3.0.1"
47
47
  }
48
48
  }
@@ -4,15 +4,71 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
 
7
- const SAFE_STORAGE_PREFIX = Buffer.from("v10");
7
+ // Chromium OSCrypt: "v10" = fixed password (macOS: the Keychain password; Linux: "peanuts"),
8
+ // "v11" = Linux Secret Service password. macOS stretches with 1003 PBKDF2 rounds, Linux with 1.
9
+ const SAFE_STORAGE_PREFIX_V10 = "v10";
10
+ const SAFE_STORAGE_PREFIX_V11 = "v11";
11
+ const LINUX_BASIC_TEXT_PASSWORD = "peanuts";
12
+ const SUPPORTED_PLATFORMS = new Set(["darwin", "linux"]);
8
13
 
9
- export function decryptSafeStorageString(encryptedBase64, password) {
14
+ export class GrokBotGatewaySessionError extends Error {
15
+ constructor(code, message) {
16
+ super(message);
17
+ this.name = "GrokBotGatewaySessionError";
18
+ this.code = code;
19
+ }
20
+ }
21
+
22
+ function encryptedPayload(wrapped) {
23
+ if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2) {
24
+ throw new GrokBotGatewaySessionError(
25
+ "UNSUPPORTED_VERSION",
26
+ `Unsupported Grok Bot gateway descriptor version ${wrapped.version}.`,
27
+ );
28
+ }
29
+ let encrypted;
30
+ if (wrapped.version === 2) {
31
+ const entries = Object.values(wrapped.entries ?? {});
32
+ if (entries.length === 0) {
33
+ throw new GrokBotGatewaySessionError(
34
+ "EMPTY_ENTRIES",
35
+ "Grok Bot gateway descriptor has no saved gateway entries.",
36
+ );
37
+ }
38
+ if (entries.length > 1) {
39
+ throw new GrokBotGatewaySessionError(
40
+ "AMBIGUOUS_ENTRIES",
41
+ "Grok Bot gateway descriptor has multiple saved gateway entries and no active entry selection.",
42
+ );
43
+ }
44
+ encrypted = entries[0]?.encrypted;
45
+ } else {
46
+ encrypted = wrapped.encrypted;
47
+ }
48
+ if (typeof encrypted !== "string" || !encrypted) {
49
+ throw new GrokBotGatewaySessionError(
50
+ "MISSING_ENCRYPTED_PAYLOAD",
51
+ "Grok Bot gateway descriptor is missing an encrypted payload.",
52
+ );
53
+ }
54
+ return encrypted;
55
+ }
56
+
57
+ function safeStorageKey(password, platform) {
58
+ const iterations = platform === "linux" ? 1 : 1003;
59
+ return crypto.pbkdf2Sync(password, "saltysalt", iterations, 16, "sha1");
60
+ }
61
+
62
+ export function decryptSafeStorageString(encryptedBase64, password, platform = "darwin") {
10
63
  const encrypted = Buffer.from(encryptedBase64, "base64");
11
- if (!encrypted.subarray(0, 3).equals(SAFE_STORAGE_PREFIX)) {
64
+ const prefix = encrypted.subarray(0, 3).toString("latin1");
65
+ const linuxBasicText = platform === "linux" && prefix === SAFE_STORAGE_PREFIX_V10;
66
+ const keyring = prefix === SAFE_STORAGE_PREFIX_V10 || (platform === "linux" && prefix === SAFE_STORAGE_PREFIX_V11);
67
+ if (!keyring) {
12
68
  throw new Error("Unsupported Grok Bot Safe Storage format.");
13
69
  }
14
70
 
15
- const key = crypto.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
71
+ const key = safeStorageKey(linuxBasicText ? LINUX_BASIC_TEXT_PASSWORD : password, platform);
16
72
  const decipher = crypto.createDecipheriv(
17
73
  "aes-128-cbc",
18
74
  key,
@@ -24,7 +80,11 @@ export function decryptSafeStorageString(encryptedBase64, password) {
24
80
  ]).toString("utf8");
25
81
  }
26
82
 
27
- export function grokBotGatewayDescriptorPath(home = homedir()) {
83
+ export function grokBotGatewayDescriptorPath(home = homedir(), platform = process.platform, env = process.env) {
84
+ if (platform === "linux") {
85
+ const configHome = env.XDG_CONFIG_HOME || join(home, ".config");
86
+ return join(configHome, "Grok Bot/gateway-descriptor.json");
87
+ }
28
88
  return join(
29
89
  home,
30
90
  "Library/Application Support/Grok Bot/gateway-descriptor.json",
@@ -34,11 +94,19 @@ export function grokBotGatewayDescriptorPath(home = homedir()) {
34
94
  export function hasGrokBotGatewaySession({
35
95
  platform = process.platform,
36
96
  home = homedir(),
97
+ env = process.env,
37
98
  } = {}) {
38
- return platform === "darwin" && existsSync(grokBotGatewayDescriptorPath(home));
99
+ return SUPPORTED_PLATFORMS.has(platform) && existsSync(grokBotGatewayDescriptorPath(home, platform, env));
39
100
  }
40
101
 
41
- function readKeychainPassword() {
102
+ function readKeychainPassword(platform = process.platform) {
103
+ if (platform === "linux") {
104
+ return execFileSync(
105
+ "secret-tool",
106
+ ["lookup", "xdg:schema", "chrome_libsecret_os_crypt_password_v2", "application", "Grok Bot"],
107
+ { encoding: "utf8" },
108
+ ).trimEnd();
109
+ }
42
110
  return execFileSync(
43
111
  "/usr/bin/security",
44
112
  ["find-generic-password", "-w", "-s", "Grok Bot Safe Storage"],
@@ -49,21 +117,30 @@ function readKeychainPassword() {
49
117
  export function loadGrokBotGatewaySession({
50
118
  platform = process.platform,
51
119
  home = homedir(),
120
+ env = process.env,
52
121
  getKeychainPassword = readKeychainPassword,
53
122
  } = {}) {
54
- if (platform !== "darwin") return null;
123
+ if (!SUPPORTED_PLATFORMS.has(platform)) return null;
55
124
 
56
- const path = grokBotGatewayDescriptorPath(home);
125
+ const path = grokBotGatewayDescriptorPath(home, platform, env);
57
126
  if (!existsSync(path)) return null;
58
127
 
59
128
  const wrapped = JSON.parse(readFileSync(path, "utf8"));
129
+ const encrypted = encryptedPayload(wrapped);
130
+ const prefix = Buffer.from(encrypted, "base64").subarray(0, 3).toString("latin1");
131
+ // Linux v10 is the keyring-less basic_text backend; no secret store to ask.
132
+ const needsKeychain = !(platform === "linux" && prefix === SAFE_STORAGE_PREFIX_V10);
60
133
  const clear = decryptSafeStorageString(
61
- wrapped.encrypted,
62
- getKeychainPassword(),
134
+ encrypted,
135
+ needsKeychain ? getKeychainPassword(platform) : LINUX_BASIC_TEXT_PASSWORD,
136
+ platform,
63
137
  );
64
138
  const descriptor = JSON.parse(clear);
65
139
  if (!descriptor.baseUrl || !descriptor.token) {
66
- throw new Error("Grok Bot gateway descriptor is incomplete.");
140
+ throw new GrokBotGatewaySessionError(
141
+ "INCOMPLETE_DESCRIPTOR",
142
+ "Decrypted Grok Bot gateway descriptor is incomplete.",
143
+ );
67
144
  }
68
145
 
69
146
  return {
@@ -72,3 +149,20 @@ export function loadGrokBotGatewaySession({
72
149
  headers: descriptor.headers ?? {},
73
150
  };
74
151
  }
152
+
153
+ export function inspectGrokBotGatewaySession(options = {}) {
154
+ if (!hasGrokBotGatewaySession(options)) {
155
+ return { present: false, usable: false };
156
+ }
157
+ try {
158
+ loadGrokBotGatewaySession(options);
159
+ return { present: true, usable: true };
160
+ } catch (error) {
161
+ return {
162
+ present: true,
163
+ usable: false,
164
+ code: error instanceof GrokBotGatewaySessionError ? error.code : "UNUSABLE_SESSION",
165
+ error: error instanceof Error ? error.message : String(error),
166
+ };
167
+ }
168
+ }
package/src/cli.js CHANGED
@@ -2,6 +2,7 @@
2
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
+ import { inspectGrokBotGatewaySession } from "./app-session.js";
5
6
 
6
7
  function print(value) {
7
8
  if (typeof value === "string") process.stdout.write(value + "\n");
@@ -247,11 +248,16 @@ async function main(argv) {
247
248
  if (!(err instanceof StoreError)) throw err;
248
249
  }
249
250
  const note = "Live roster is on the box. Prefer CURSOR_ACCESS_TOKEN then EnsureSandBox then POST gateway /api/*.";
250
- const payload = { resolved, found, candidates, gatewayAuthPresent: hasGatewayAuth(), note };
251
+ const gatewayAuthPresent = hasGatewayAuth();
252
+ const grokBotAppSession = inspectGrokBotGatewaySession();
253
+ const payload = { resolved, found, candidates, gatewayAuthPresent, grokBotAppSession, note };
251
254
  if (json) print(payload);
252
255
  else {
253
256
  print("resolved: " + (resolved ?? "(none)"));
254
- print("gateway auth: " + (hasGatewayAuth() ? "yes (env)" : "no"));
257
+ print("gateway auth: " + (gatewayAuthPresent ? "present" : "no"));
258
+ if (grokBotAppSession.usable) print("Grok Bot app session: usable");
259
+ else if (grokBotAppSession.present) print("Grok Bot app session: present but unusable: " + grokBotAppSession.error);
260
+ else print("Grok Bot app session: not found");
255
261
  print("found:");
256
262
  print(found.length ? found.map((p) => " " + p).join("\n") : " (none)");
257
263
  print("candidates:");