@bivy/bivy 0.10.1-staging.150 → 0.10.1-staging.152

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.
@@ -191,11 +191,13 @@
191
191
  "supportTier": "beta",
192
192
  "certification": "adapter-tested",
193
193
  "headlessFlags": [
194
- "-p"
194
+ "-p",
195
+ "--resume"
195
196
  ],
196
197
  "install": {
197
- "kind": "npm",
198
- "pkg": "@vibe-kit/grok-cli"
198
+ "kind": "curl",
199
+ "display": "curl -fsSL https://x.ai/cli/install.sh | bash",
200
+ "shell": "curl -fsSL https://x.ai/cli/install.sh | bash"
199
201
  }
200
202
  },
201
203
  {
package/bin/bivy.mjs CHANGED
@@ -987,6 +987,9 @@ const AGENT_HEADLESS_FLAGS = {
987
987
  // appear here; others simply run without a pinned id.
988
988
  const AGENT_SESSION_ID_FLAG = {
989
989
  claude: "--session-id", // `claude --session-id <uuid>` (must be a valid UUID)
990
+ // Official Grok CLI: `grok --session-id <uuid>` pins a new session UUID under
991
+ // ~/.grok/sessions/<cwd>/<uuid>/ so takeover / resume has a known target.
992
+ grok: "--session-id",
990
993
  };
991
994
 
992
995
  // Args that mean the caller already chose a session (pin or resume), so we must
@@ -1001,6 +1004,7 @@ const SESSION_ID_CONFLICTS = ["--session-id", "--resume", "-r", "-c", "--continu
1001
1004
  const AGENT_RESUME_ARGS = {
1002
1005
  claude: (id) => ["--resume", id],
1003
1006
  codex: (id) => ["resume", id],
1007
+ grok: (id) => ["--resume", id],
1004
1008
  };
1005
1009
  function agentResumeArgs(agentId, sessionRef) {
1006
1010
  const fn = AGENT_RESUME_ARGS[(agentId || "").toLowerCase()];
@@ -14,6 +14,7 @@ import fs from "node:fs";
14
14
  import path from "node:path";
15
15
  import { createCredentialVault } from "./credential-store.js";
16
16
  import { resolveCodexHome } from "./codex-auth.js";
17
+ import { grokAuthEntryKey, resolveGrokHome, GROK_OIDC_ISSUER } from "./grok-auth.js";
17
18
  import { claudeCredentialFiles } from "./anthropic-preflight.js";
18
19
  /** Read the `exp` (seconds) claim from a JWT access token → absolute epoch ms. */
19
20
  function jwtExpiryMs(token) {
@@ -94,6 +95,52 @@ export async function ingestClaudeCredentials(credsDir) {
94
95
  }
95
96
  return 0;
96
97
  }
98
+ /** Map the official Grok CLI's `~/.grok/auth.json` OIDC entry into a Bivy credential. */
99
+ export function grokAuthToCredential(raw) {
100
+ if (!raw || typeof raw !== "object")
101
+ return undefined;
102
+ const record = raw;
103
+ // Prefer the known xAI OIDC entry key; fall back to the first oidc entry whose
104
+ // issuer is auth.x.ai (covers a client-id change without breaking ingest).
105
+ const preferredKey = grokAuthEntryKey();
106
+ let entry = record[preferredKey];
107
+ if (!entry || typeof entry !== "object") {
108
+ for (const value of Object.values(record)) {
109
+ if (!value || typeof value !== "object")
110
+ continue;
111
+ const candidate = value;
112
+ if (candidate.oidc_issuer === GROK_OIDC_ISSUER || candidate.auth_mode === "oidc") {
113
+ entry = candidate;
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ if (!entry)
119
+ return undefined;
120
+ const access = typeof entry.key === "string" ? entry.key : "";
121
+ const refresh = typeof entry.refresh_token === "string" ? entry.refresh_token : "";
122
+ if (!access || !refresh)
123
+ return undefined;
124
+ const expiresFromField = typeof entry.expires_at === "string" ? Date.parse(entry.expires_at) : NaN;
125
+ const expires = Number.isFinite(expiresFromField) && expiresFromField > 0
126
+ ? expiresFromField
127
+ : (jwtExpiryMs(access) ?? Date.now() + 60 * 60 * 1000);
128
+ return { providerId: "xai", credential: { type: "oauth", access, refresh, expires } };
129
+ }
130
+ /** Fold a login/refresh done in the Grok CLI back into Bivy's vault. */
131
+ export async function ingestGrokCredentials(credsDir) {
132
+ let raw;
133
+ try {
134
+ raw = JSON.parse(fs.readFileSync(path.join(resolveGrokHome(), "auth.json"), "utf8"));
135
+ }
136
+ catch {
137
+ return 0; // no Grok login on disk
138
+ }
139
+ const mapped = grokAuthToCredential(raw);
140
+ if (!mapped)
141
+ return 0;
142
+ return createCredentialVault(credsDir).importAll({ [mapped.providerId]: mapped.credential });
143
+ }
97
144
  /**
98
145
  * Ingest an agent's native credential store into Bivy's vault. Called when a
99
146
  * `bivy run <agent>` terminal exits, so a login done inside that agent's CLI is
@@ -112,6 +159,8 @@ export async function ingestAgentCredentials(agentId, credsDir, piDir) {
112
159
  case "claude-code":
113
160
  case "claude-code-sdk":
114
161
  return ingestClaudeCredentials(credsDir);
162
+ case "grok":
163
+ return ingestGrokCredentials(credsDir);
115
164
  case "pi":
116
165
  // Pi's TUI writes a plaintext auth.json in its own dir; fold it back into
117
166
  // the shared vault.
@@ -19,6 +19,7 @@
19
19
  import { createCredentialVault } from "./credential-store.js";
20
20
  import { buildAgentCredentialEnv, createCredentialStore } from "./credentials.js";
21
21
  import { ensureCodexAuth, ensureCodexTrusted } from "./codex-auth.js";
22
+ import { ensureGrokAuth } from "./grok-auth.js";
22
23
  import { refreshModelOAuth } from "./oauth/model-oauth.js";
23
24
  import { isNativeOAuthProvider } from "./oauth/model-oauth-providers.js";
24
25
  /**
@@ -75,5 +76,13 @@ export async function provisionAgentRun(credsDir, piDir, agentId, workspace) {
75
76
  if (workspace)
76
77
  ensureCodexTrusted(workspace);
77
78
  }
79
+ else if (agentId === "grok") {
80
+ // Official Grok CLI reads XAI_API_KEY/GROK_API_KEY (already in env for api
81
+ // keys) or its own auth.json; mint the latter from a connected SuperGrok /
82
+ // X subscription when present.
83
+ const home = await ensureGrokAuth(credsDir).catch(() => undefined);
84
+ if (home)
85
+ env.GROK_HOME = home;
86
+ }
78
87
  return env;
79
88
  }
@@ -128,8 +128,9 @@ export async function buildAgentCredentialEnv(store, providers, activeProvider)
128
128
  if (cred.kind === "oauth") {
129
129
  // OAuth *subscription* tokens are provider-specific and are not accepted
130
130
  // as a plain API key. Only Anthropic has a documented env var an external
131
- // agent reads (Claude Code's OAuth token); other providers' subscription
132
- // logins can't be handed off this way, so don't emit a misleading key.
131
+ // agent reads (Claude Code's OAuth token). Codex and Grok get their
132
+ // subscription via native auth.json materialization (codex-auth.ts /
133
+ // grok-auth.ts), not as env keys — so don't emit a misleading key here.
133
134
  if (cred.provider !== "anthropic")
134
135
  continue;
135
136
  Object.assign(env, cred.env ?? {});
@@ -137,6 +138,11 @@ export async function buildAgentCredentialEnv(store, providers, activeProvider)
137
138
  }
138
139
  else {
139
140
  env[apiKeyEnvVar(cred.provider)] = cred.token;
141
+ // vibe-kit/grok-cli (and some forks) read GROK_API_KEY instead of the
142
+ // official XAI_API_KEY. Project both so either Grok binary works off one
143
+ // Bivy API-key login.
144
+ if (cred.provider === "xai")
145
+ env.GROK_API_KEY = cred.token;
140
146
  // A custom endpoint's base URL (and standard-key alias) is injected only
141
147
  // when it's the active provider, so it can't hijack the real provider.
142
148
  if (isActive && cred.env) {
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // Grok (xAI CLI) credential materialization.
4
+ //
5
+ // An xAI SuperGrok / X subscription a user connects *inside Bivy* lands in the
6
+ // shared node vault as an `xai` OAuth record. The official Grok CLI
7
+ // (`curl -fsSL https://x.ai/cli/install.sh | bash`) cannot read that vault — it
8
+ // authenticates from `XAI_API_KEY` / `GROK_API_KEY` or its own
9
+ // `$GROK_HOME/auth.json` (the file `grok login` writes). This module bridges the
10
+ // gap: it mints the file Grok expects from the vault record, so a subscription
11
+ // connected in Bivy "just works" for the Grok agent with no separate
12
+ // `grok login`.
13
+ //
14
+ // Why it's sound: Bivy's `xai` OAuth app IS the Grok CLI's own OAuth app
15
+ // (identical client_id `b1a00492-…`, token endpoint, and scopes including
16
+ // `grok-cli:access` — verified against @earendil-works/pi-ai and the shipped
17
+ // grok binary), so tokens minted through Bivy are accepted by Grok's backend.
18
+ // The vault already stores {access, refresh, expires}; we write them in the
19
+ // shape Grok's auth.json uses for OIDC sessions.
20
+ //
21
+ // Rotation note: xAI does *not* rotate the refresh token on every grant
22
+ // (`refreshRotates: false` in model-oauth-providers), so minting is safe even
23
+ // if Grok later self-refreshes — the vault's refresh token stays valid. We mint
24
+ // only when no auth.json exists yet (Grok then owns and self-refreshes it),
25
+ // matching the Codex materialization policy.
26
+ import fs from "node:fs";
27
+ import os from "node:os";
28
+ import path from "node:path";
29
+ import { createCredentialVault } from "./credential-store.js";
30
+ import { getModelOAuthProvider } from "./oauth/model-oauth-providers.js";
31
+ import { refreshModelOAuth } from "./oauth/model-oauth.js";
32
+ /** xAI OIDC issuer the official Grok CLI keys its auth.json entry on. */
33
+ export const GROK_OIDC_ISSUER = "https://auth.x.ai";
34
+ /** Resolve Grok's home dir exactly as the CLI does (`GROK_HOME` or `~/.grok`). */
35
+ export function resolveGrokHome() {
36
+ return process.env.GROK_HOME?.trim() || path.join(os.homedir(), ".grok");
37
+ }
38
+ /** Map key Grok uses for the xAI OIDC session inside auth.json. */
39
+ export function grokAuthEntryKey(clientId) {
40
+ const id = clientId?.trim() || getModelOAuthProvider("xai")?.clientId || "";
41
+ return `${GROK_OIDC_ISSUER}::${id}`;
42
+ }
43
+ /**
44
+ * Ensure the Grok CLI has a usable credential file, minting one from Bivy's
45
+ * vault when needed. Returns the resolved `GROK_HOME` (so the caller can pin it
46
+ * on the subprocess env) or `undefined` when there's nothing to do / no vault
47
+ * credential — in which case the caller's preflight surfaces the actionable
48
+ * "no credential" error unchanged.
49
+ *
50
+ * Idempotent and low-churn: if an `auth.json` already exists (a native
51
+ * `grok login` or a prior materialization) it is left untouched — Grok owns and
52
+ * self-refreshes it — so we mint at most once. We write to the *default* Grok
53
+ * home (never a throwaway dir) so sessions stay where the CLI already looks.
54
+ */
55
+ export async function ensureGrokAuth(credsDir) {
56
+ const grokHome = resolveGrokHome();
57
+ const authFile = path.join(grokHome, "auth.json");
58
+ // Never clobber an existing login (native or previously materialized); Grok
59
+ // owns and refreshes it. An API key, if present, is handled by preflight /
60
+ // env projection — no auth.json needed.
61
+ if (fs.existsSync(authFile))
62
+ return grokHome;
63
+ if (process.env.XAI_API_KEY?.trim() || process.env.GROK_API_KEY?.trim())
64
+ return undefined;
65
+ const provider = getModelOAuthProvider("xai");
66
+ if (!provider)
67
+ return undefined;
68
+ // Ensure the vault access token is still live before we project it. No-op when
69
+ // fresh; refreshes under the store lock when expired. Failure leaves the vault
70
+ // alone and we fall through to the (likely still-stale) read below.
71
+ await refreshModelOAuth(credsDir, "xai").catch(() => undefined);
72
+ const store = createCredentialVault(credsDir);
73
+ const cred = await store.read("xai").catch(() => undefined);
74
+ if (!cred || cred.type !== "oauth")
75
+ return undefined;
76
+ const access = typeof cred.access === "string" ? cred.access : "";
77
+ const refresh = typeof cred.refresh === "string" ? cred.refresh : "";
78
+ if (!access || !refresh)
79
+ return undefined;
80
+ const expiresMs = Number(cred.expires) || 0;
81
+ const entryKey = grokAuthEntryKey(provider.clientId);
82
+ const entry = {
83
+ key: access,
84
+ auth_mode: "oidc",
85
+ create_time: new Date().toISOString(),
86
+ refresh_token: refresh,
87
+ oidc_issuer: GROK_OIDC_ISSUER,
88
+ oidc_client_id: provider.clientId,
89
+ };
90
+ if (expiresMs > 0)
91
+ entry.expires_at = new Date(expiresMs).toISOString();
92
+ const authJson = { [entryKey]: entry };
93
+ try {
94
+ fs.mkdirSync(grokHome, { recursive: true, mode: 0o700 });
95
+ const tmp = `${authFile}.tmp`;
96
+ fs.writeFileSync(tmp, `${JSON.stringify(authJson, null, 2)}\n`, { mode: 0o600 });
97
+ fs.renameSync(tmp, authFile);
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ return grokHome;
103
+ }
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // Grok (xAI CLI) credential preflight.
4
+ //
5
+ // The official Grok CLI authenticates one of two ways: an xAI API key read from
6
+ // `XAI_API_KEY` / `GROK_API_KEY`, or OIDC tokens in `$GROK_HOME/auth.json`
7
+ // (default `~/.grok/auth.json`). Bivy's shared credential vault forwards *API
8
+ // keys* to every agent (see credentials.ts), and `grok-auth.ts` mints the
9
+ // auth.json from a connected xAI *subscription* — so both sign-in kinds now
10
+ // reach Grok.
11
+ //
12
+ // This preflight is the backstop: if neither an API key nor an auth.json is
13
+ // present after that handoff, the CLI fails with an opaque "API key required"
14
+ // (vibe-kit) or "Not signed in" (official) message. We detect that up front and
15
+ // return a clear, actionable message instead.
16
+ import fs from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ /** Path to the Grok CLI's own auth file (`GROK_HOME/auth.json`, default `~/.grok`). */
20
+ export function grokAuthFile(deps = {}) {
21
+ const grokHome = (deps.grokHome ?? process.env.GROK_HOME)?.trim();
22
+ const base = grokHome ? grokHome : path.join(deps.home ?? os.homedir(), ".grok");
23
+ return path.join(base, "auth.json");
24
+ }
25
+ /** The user-facing message shown when Grok has no xAI credential. */
26
+ export const GROK_NO_CREDENTIAL_MESSAGE = [
27
+ "Grok has no xAI credential on this node, so it can't start a turn.",
28
+ "",
29
+ "Fix it any of these ways:",
30
+ "• Connect SuperGrok / X Premium under Keys & OAuth — Bivy mints Grok's auth file from it automatically, or",
31
+ "• Add an xAI API key under Keys & OAuth — Bivy passes it as XAI_API_KEY (and GROK_API_KEY), or",
32
+ "• Run `grok login` on this node to sign in directly (official Grok CLI).",
33
+ "",
34
+ "Install the official Grok CLI if you haven't: `curl -fsSL https://x.ai/cli/install.sh | bash`",
35
+ "(The older @vibe-kit/grok-cli package only accepts API keys and cannot use a subscription login.)",
36
+ "",
37
+ "If you already connected the xAI subscription in Bivy and still see this, the saved login could not be projected — reconnect it under Keys & OAuth, or delete a stale ~/.grok/auth.json and try again.",
38
+ ].join("\n");
39
+ /**
40
+ * Returns a human-readable error when the Grok CLI has no usable xAI
41
+ * credential, or undefined when a credential is present so the turn should
42
+ * proceed. `env` is the environment the Grok subprocess will run with (ambient
43
+ * process env + Bivy's vault handoff).
44
+ */
45
+ export function grokCredentialPreflight(env, deps = {}) {
46
+ if (env.XAI_API_KEY?.trim() || env.GROK_API_KEY?.trim())
47
+ return undefined;
48
+ const exists = deps.fileExists ?? ((p) => {
49
+ try {
50
+ return fs.existsSync(p);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ });
56
+ // Prefer the GROK_HOME the prepare step may have just pinned on env.
57
+ const homeFromEnv = env.GROK_HOME?.trim();
58
+ if (exists(grokAuthFile({ ...deps, grokHome: homeFromEnv || deps.grokHome })))
59
+ return undefined;
60
+ return GROK_NO_CREDENTIAL_MESSAGE;
61
+ }
@@ -0,0 +1,283 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ //
4
+ // Grok CLI on-disk session reader — the read side of adopting a Grok session
5
+ // started outside governed chat (e.g. `bivy run grok` or a bare `grok` TUI).
6
+ //
7
+ // Layout (official Grok CLI 1.x):
8
+ // $GROK_HOME/sessions/<url-encoded-cwd>/<session-uuid>/
9
+ // summary.json — id, cwd, title, timestamps, model
10
+ // chat_history.jsonl — user / assistant / system / tool_result lines
11
+ //
12
+ // GROK_HOME defaults to ~/.grok (see grok-auth.ts's resolveGrokHome). Every read
13
+ // is best-effort: a missing/malformed store never throws out of the list path.
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { resolveGrokHome } from "./grok-auth.js";
17
+ import { hasLiveProcessForCwd } from "./native-process-scan.js";
18
+ const GROK_BIN_NAMES = ["grok"];
19
+ export function grokSessionsRoot() {
20
+ return path.join(resolveGrokHome(), "sessions");
21
+ }
22
+ function toEpoch(value) {
23
+ if (value == null)
24
+ return undefined;
25
+ if (typeof value === "number" && Number.isFinite(value))
26
+ return value;
27
+ const ms = new Date(value).getTime();
28
+ return Number.isFinite(ms) ? ms : undefined;
29
+ }
30
+ function readSummary(dir) {
31
+ try {
32
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, "summary.json"), "utf8"));
33
+ const info = (raw.info && typeof raw.info === "object" ? raw.info : {});
34
+ const id = typeof info.id === "string" ? info.id : path.basename(dir);
35
+ const cwd = typeof info.cwd === "string" ? info.cwd : undefined;
36
+ const name = (typeof raw.generated_title === "string" && raw.generated_title.trim()) ||
37
+ (typeof raw.session_summary === "string" && raw.session_summary.trim()) ||
38
+ undefined;
39
+ return {
40
+ id,
41
+ dir,
42
+ cwd,
43
+ createdAt: toEpoch(raw.created_at),
44
+ updatedAt: toEpoch(raw.last_active_at ?? raw.updated_at),
45
+ name,
46
+ };
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ /** Pull plain text out of a Grok content field (string or content-block array). */
53
+ function contentText(content) {
54
+ if (typeof content === "string")
55
+ return content;
56
+ if (!Array.isArray(content))
57
+ return "";
58
+ const parts = [];
59
+ for (const block of content) {
60
+ if (!block || typeof block !== "object")
61
+ continue;
62
+ const b = block;
63
+ if (b.type === "text" && typeof b.text === "string")
64
+ parts.push(b.text);
65
+ else if (typeof b.text === "string")
66
+ parts.push(b.text);
67
+ }
68
+ return parts.join("\n");
69
+ }
70
+ /** First user-visible prompt that isn't a system/meta wrapper. */
71
+ function firstUserMessage(dir) {
72
+ const file = path.join(dir, "chat_history.jsonl");
73
+ let fh;
74
+ try {
75
+ fh = fs.openSync(file, "r");
76
+ const buf = Buffer.alloc(64 * 1024);
77
+ const n = fs.readSync(fh, buf, 0, buf.length, 0);
78
+ const chunk = buf.subarray(0, n).toString("utf8");
79
+ for (const line of chunk.split("\n")) {
80
+ if (!line.trim())
81
+ continue;
82
+ let rec;
83
+ try {
84
+ rec = JSON.parse(line);
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ if (rec.type !== "user")
90
+ continue;
91
+ const text = contentText(rec.content).trim();
92
+ if (!text)
93
+ continue;
94
+ // Skip the CLI's environment / system-reminder scaffolding — the first
95
+ // real user_query (or a short free-form prompt) is the list label.
96
+ const query = text.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i)?.[1]?.trim();
97
+ const label = (query || text).replace(/\s+/g, " ").trim();
98
+ if (!label)
99
+ continue;
100
+ // Skip pure meta blobs that never carry a user question.
101
+ if (label.startsWith("<user_info>") || label.startsWith("<system-reminder>") || label.startsWith("<git_status>")) {
102
+ continue;
103
+ }
104
+ return label.length > 120 ? `${label.slice(0, 119)}…` : label;
105
+ }
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ finally {
111
+ if (fh !== undefined)
112
+ try {
113
+ fs.closeSync(fh);
114
+ }
115
+ catch { /* ignore */ }
116
+ }
117
+ return undefined;
118
+ }
119
+ /**
120
+ * Enumerate Grok sessions under $GROK_HOME/sessions. Best-effort: a missing or
121
+ * unreadable store returns []. Sorted newest-activity-first.
122
+ */
123
+ export function listGrokSessions() {
124
+ const root = grokSessionsRoot();
125
+ let cwdEntries;
126
+ try {
127
+ cwdEntries = fs.readdirSync(root, { withFileTypes: true });
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ const out = [];
133
+ for (const cwdEntry of cwdEntries) {
134
+ if (!cwdEntry.isDirectory())
135
+ continue;
136
+ // Skip the search index / non-session dirs.
137
+ if (cwdEntry.name === "session_search.sqlite" || cwdEntry.name.endsWith(".sqlite"))
138
+ continue;
139
+ const cwdDir = path.join(root, cwdEntry.name);
140
+ let sessionEntries;
141
+ try {
142
+ sessionEntries = fs.readdirSync(cwdDir, { withFileTypes: true });
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ for (const sessionEntry of sessionEntries) {
148
+ if (!sessionEntry.isDirectory())
149
+ continue;
150
+ const dir = path.join(cwdDir, sessionEntry.name);
151
+ const summary = readSummary(dir);
152
+ if (!summary?.id)
153
+ continue;
154
+ // Prefer the cwd encoded in the parent dir name when summary lacks one.
155
+ let cwd = summary.cwd;
156
+ if (!cwd) {
157
+ try {
158
+ cwd = decodeURIComponent(cwdEntry.name);
159
+ }
160
+ catch {
161
+ cwd = undefined;
162
+ }
163
+ }
164
+ let createdAt = summary.createdAt;
165
+ let updatedAt = summary.updatedAt;
166
+ if (createdAt == null || updatedAt == null) {
167
+ try {
168
+ const st = fs.statSync(dir);
169
+ createdAt = createdAt ?? (st.birthtimeMs || st.mtimeMs);
170
+ updatedAt = updatedAt ?? st.mtimeMs;
171
+ }
172
+ catch {
173
+ /* ignore */
174
+ }
175
+ }
176
+ out.push({
177
+ id: summary.id,
178
+ dir,
179
+ cwd,
180
+ createdAt,
181
+ updatedAt,
182
+ name: summary.name,
183
+ firstMessage: firstUserMessage(dir),
184
+ });
185
+ }
186
+ }
187
+ return out.sort((a, b) => (b.updatedAt ?? b.createdAt ?? 0) - (a.updatedAt ?? a.createdAt ?? 0));
188
+ }
189
+ /**
190
+ * Load a Grok session's chat_history.jsonl as RuntimeMessage[] for history
191
+ * preload when a governed chat resumes that session. System scaffolding and
192
+ * tool_result rows are dropped — the chat surface only needs the user/assistant
193
+ * turns. Best-effort; a missing/malformed file returns [].
194
+ */
195
+ export function loadGrokTranscript(sessionId) {
196
+ const match = listGrokSessions().find((s) => s.id === sessionId);
197
+ if (!match)
198
+ return [];
199
+ const file = path.join(match.dir, "chat_history.jsonl");
200
+ let raw;
201
+ try {
202
+ raw = fs.readFileSync(file, "utf8");
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ const out = [];
208
+ for (const line of raw.split("\n")) {
209
+ if (!line.trim())
210
+ continue;
211
+ let rec;
212
+ try {
213
+ rec = JSON.parse(line);
214
+ }
215
+ catch {
216
+ continue;
217
+ }
218
+ const type = rec.type;
219
+ if (type !== "user" && type !== "assistant")
220
+ continue;
221
+ let text = contentText(rec.content).trim();
222
+ if (!text)
223
+ continue;
224
+ // Prefer the inner <user_query> when the CLI wrapped the prompt.
225
+ if (type === "user") {
226
+ const query = text.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i)?.[1]?.trim();
227
+ if (query)
228
+ text = query;
229
+ // Skip pure meta system blobs that aren't real user turns.
230
+ if (text.startsWith("<user_info>") ||
231
+ text.startsWith("<system-reminder>") ||
232
+ text.startsWith("<git_status>") ||
233
+ text.startsWith("<image_files>") ||
234
+ text.startsWith("<action_safety>")) {
235
+ continue;
236
+ }
237
+ }
238
+ out.push({
239
+ role: type === "user" ? "user" : "assistant",
240
+ content: text,
241
+ timestamp: typeof rec.timestamp === "string" || typeof rec.timestamp === "number" ? toEpoch(rec.timestamp) : undefined,
242
+ });
243
+ }
244
+ return out;
245
+ }
246
+ /**
247
+ * Locate the Grok session a run produced (by cwd + start time) when there is no
248
+ * launch-time pinned id. Mirrors discoverCodexSessionForCwd / discoverPiSessionForCwd.
249
+ */
250
+ export function discoverGrokSessionForCwd(cwd, since = 0) {
251
+ const target = path.resolve(cwd);
252
+ const skewMs = 10_000;
253
+ const earliest = since - skewMs;
254
+ const wanted = process.platform === "win32" ? target.toLowerCase() : target;
255
+ return listGrokSessions()
256
+ .filter((s) => {
257
+ if (!s.cwd)
258
+ return false;
259
+ const resolved = process.platform === "win32" ? path.resolve(s.cwd).toLowerCase() : path.resolve(s.cwd);
260
+ return resolved === wanted;
261
+ })
262
+ .filter((s) => (s.createdAt ?? 0) >= earliest)
263
+ .sort((a, b) => {
264
+ const da = Math.abs((a.createdAt ?? 0) - since);
265
+ const db = Math.abs((b.createdAt ?? 0) - since);
266
+ if (da !== db)
267
+ return da - db;
268
+ return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
269
+ })[0];
270
+ }
271
+ /** Issue #156 discovery surface for Grok's on-disk store. */
272
+ export function discoverNativeGrokSessions(hasLiveProcess = (cwd) => hasLiveProcessForCwd(cwd, GROK_BIN_NAMES)) {
273
+ return listGrokSessions().map((s) => ({
274
+ runtimeId: "grok",
275
+ ref: s.id,
276
+ file: s.dir,
277
+ cwd: s.cwd,
278
+ updatedAt: s.updatedAt ?? s.createdAt,
279
+ title: s.name || s.firstMessage,
280
+ active: Boolean(s.cwd) && hasLiveProcess(s.cwd),
281
+ resumable: true,
282
+ }));
283
+ }
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
9
9
  import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled } from "./claude-code.js";
10
10
  import { deleteCodexSession, discoverNativeCodexSessions, loadCodexTranscript, writeCodexRollout } from "./codex-sessions.js";
11
11
  import { deleteOpenCodeSession, loadOpenCodeTranscript, writeOpenCodeHistory } from "./opencode-sessions.js";
12
+ import { discoverNativeGrokSessions, listGrokSessions, loadGrokTranscript } from "./grok-sessions.js";
12
13
  import { createCredentialStore } from "./credentials.js";
13
14
  // Args that continue an existing Codex session each prompt. Codex assigns its own
14
15
  // session id (no launch-time pin), so a resumed run threads it via `exec resume`.
@@ -37,7 +38,9 @@ import { PiRuntime } from "./pi.js";
37
38
  import { ProcessRuntime, processRuntimeFromEnv } from "./process.js";
38
39
  import { codexCredentialPreflight } from "./codex-preflight.js";
39
40
  import { opencodeCredentialPreflight } from "./opencode-preflight.js";
41
+ import { grokCredentialPreflight } from "./grok-preflight.js";
40
42
  import { ensureCodexAuth } from "./codex-auth.js";
43
+ import { ensureGrokAuth } from "./grok-auth.js";
41
44
  import { parserFactoryFor } from "./cli-parsers.js";
42
45
  import { sandboxTier, sandboxArgsFor, codexSandboxPolicy } from "../harness/sandbox.js";
43
46
  import { ProtocolRuntime, protocolRuntimeFromEnv, protocolCommandsFromEnv } from "./protocol.js";
@@ -487,24 +490,41 @@ const CLI_AGENT_SPECS = {
487
490
  grok: {
488
491
  displayName: "Grok",
489
492
  command: "grok",
490
- packageName: "@vibe-kit/grok-cli",
493
+ packageName: "grok (curl -fsSL https://x.ai/cli/install.sh | bash)",
491
494
  supportTier: "beta",
492
- blurb: "Open-source terminal agent for xAI's Grok models (Grok CLI).",
493
- // `grok -p "<prompt>"` runs one prompt and exits (headless); `-m <id>` picks
494
- // the model. (The widely-installed @vibe-kit/grok-cli has no by-id resume or
495
- // JSON flag; the superagent `grok-dev` fork does — override via env if you run
496
- // that one.)
495
+ blurb: "xAI's official Grok coding agent (Grok CLI) — SuperGrok/X subscription or API key.",
496
+ // Official CLI: `grok -p "<prompt>"` (alias `--single`) runs one headless
497
+ // turn; `-m <id>` picks the model. Auth is projected from Bivy's vault:
498
+ // subscription → ~/.grok/auth.json (grok-auth.ts), API key → XAI_API_KEY /
499
+ // GROK_API_KEY. The older @vibe-kit/grok-cli only accepts API keys — install
500
+ // the official binary for OAuth to work.
501
+ //
502
+ // Resume: `grok --resume <id> -p "<prompt>"` continues a prior session by
503
+ // UUID (sessions live under ~/.grok/sessions/<cwd>/<id>/). The interactive
504
+ // TUI uses the same store via `grok --resume <id>`. Model ids match
505
+ // `grok models` for the official CLI (override with BIVY_GROK_MODELS).
497
506
  args: ["-p"],
507
+ resume: {
508
+ template: ["--resume", "{id}", "-p"],
509
+ loadHistory: (sessionId) => loadGrokTranscript(sessionId),
510
+ },
498
511
  model: {
499
512
  flag: "-m",
500
513
  models: [
501
- { id: "grok-code-fast-1", name: "Grok Code Fast 1", provider: "xai" },
502
- { id: "grok-4-latest", name: "Grok 4", provider: "xai" },
503
- { id: "grok-3-fast", name: "Grok 3 Fast", provider: "xai" },
514
+ // Official Grok CLI (1.x) currently advertises grok-4.5 as the default
515
+ // subscription model. Older curated ids (grok-4-latest, grok-code-fast-1,
516
+ // …) return "unknown model id" against current CLIs — keep the list
517
+ // honest; operators can override with BIVY_GROK_MODELS if their install
518
+ // exposes more.
519
+ { id: "grok-4.5", name: "Grok 4.5", provider: "xai" },
504
520
  ],
505
521
  },
506
522
  promptMode: "argv",
507
- install: { kind: "npm", pkg: "@vibe-kit/grok-cli" },
523
+ install: {
524
+ kind: "curl",
525
+ display: "curl -fsSL https://x.ai/cli/install.sh | bash",
526
+ shell: "curl -fsSL https://x.ai/cli/install.sh | bash",
527
+ },
508
528
  },
509
529
  amp: {
510
530
  displayName: "Amp",
@@ -1076,7 +1096,23 @@ function cliAgentInfo(id) {
1076
1096
  // (BIVY_MCP_PROXY) — an honest, narrower capability than full toolInterception
1077
1097
  // (it governs MCP tools, not the agent's built-in shell/edits). See
1078
1098
  // src/harness/mcp-inject.ts + governMcpCall in src/server.ts.
1079
- capabilities: withExactCapabilitySurface({ toolInterception: acpActive, mcpToolApprovals: acpActive || Boolean(process.env.BIVY_MCP_PROXY), modelSelection, resume, packages: false, fork: false, usageReporting, sessionDiscovery: id === "codex" }),
1099
+ capabilities: withExactCapabilitySurface({
1100
+ toolInterception: acpActive,
1101
+ mcpToolApprovals: acpActive || Boolean(process.env.BIVY_MCP_PROXY),
1102
+ modelSelection,
1103
+ resume,
1104
+ packages: false,
1105
+ fork: false,
1106
+ usageReporting,
1107
+ // Codex (and Grok) can locate an on-disk session by cwd + start time so a
1108
+ // `bivy run` terminal without a launch-time pin is still takeable as chat.
1109
+ sessionDiscovery: id === "codex" || id === "grok",
1110
+ // Grok also exposes its interactive TUI (`grok --resume <id>`) for the
1111
+ // chat↔terminal hand-off; Codex gets this via the approvals runtime.
1112
+ interactiveTui: id === "grok" && commandAvailable(spec.command),
1113
+ nativeSessionDiscovery: id === "grok" && commandAvailable(spec.command),
1114
+ nativeSessionAdoption: id === "grok" && resume,
1115
+ }),
1080
1116
  supportTier: spec.supportTier ?? (id === "codex" ? "supported" : "experimental"),
1081
1117
  testedVersion: spec.testedVersion,
1082
1118
  authOwner: spec.authOwner ?? "agent",
@@ -1835,22 +1871,29 @@ function makeCliRuntime(id, options) {
1835
1871
  : structured && spec.jsonArgs
1836
1872
  ? spec.jsonArgs
1837
1873
  : spec.args);
1838
- // Codex reads OPENAI_API_KEY or its own `$CODEX_HOME/auth.json`. When the
1839
- // user connected a ChatGPT/Codex subscription in Bivy (but hasn't run
1840
- // `codex login`), `prepare` mints that auth file from the shared vault so
1874
+ // Codex / Grok read an API key *or* their own `$…_HOME/auth.json`. When the
1875
+ // user connected a subscription in Bivy (but hasn't run `codex login` /
1876
+ // `grok login`), `prepare` mints that auth file from the shared vault so
1841
1877
  // the run just works; the preflight still catches the genuinely
1842
1878
  // uncredentialed case with an actionable message instead of an opaque 401.
1843
1879
  const preflight = id === "codex"
1844
1880
  ? (env) => codexCredentialPreflight(env)
1845
1881
  : id === "opencode"
1846
1882
  ? (env, ctx) => opencodeCredentialPreflight(env, ctx)
1847
- : undefined;
1883
+ : id === "grok"
1884
+ ? (env) => grokCredentialPreflight(env)
1885
+ : undefined;
1848
1886
  const prepare = id === "codex"
1849
1887
  ? async () => {
1850
1888
  const home = await ensureCodexAuth(options.credsDir);
1851
1889
  return home ? { CODEX_HOME: home } : {};
1852
1890
  }
1853
- : undefined;
1891
+ : id === "grok"
1892
+ ? async () => {
1893
+ const home = await ensureGrokAuth(options.credsDir);
1894
+ return home ? { GROK_HOME: home } : {};
1895
+ }
1896
+ : undefined;
1854
1897
  // Resume, the generic way. Codex keeps its verified path (rollout history +
1855
1898
  // tier-aware `codex exec resume <id> --json`). Every other CLI agent becomes
1856
1899
  // resumable purely as data: a spec.resume template (or a BIVY_<ID>_RESUME_
@@ -1879,5 +1922,47 @@ function makeCliRuntime(id, options) {
1879
1922
  : [a.replace(/\{id\}/g, sessionId).replace(/\{tier\}/g, tier)]),
1880
1923
  }
1881
1924
  : {};
1882
- return new ProcessRuntime({ id, displayName: spec.displayName, command: spec.command, args: runArgs, promptMode: spec.promptMode, credentials: createCredentialStore(options.credsDir), parserFactory: parserFactoryFor(parserId), preflight, prepare, model: cliModelConfig(id), thinking: cliThinkingConfig(id), usageReporting: cliUsageReporting(id), slashCommands: cliSlashCommands(id), ...resumeOpts });
1925
+ // Grok-specific: on-disk session enumeration + interactive TUI hand-off so
1926
+ // `bivy run grok` sessions persist after the PTY exits and can be taken
1927
+ // over as chat or reopened in the native TUI.
1928
+ const grokOpts = id === "grok"
1929
+ ? {
1930
+ sessionDiscovery: true,
1931
+ listDiskSessions: () => listGrokSessions().map((s) => ({
1932
+ id: s.id,
1933
+ path: s.dir,
1934
+ cwd: s.cwd,
1935
+ name: s.name || s.firstMessage,
1936
+ created: s.createdAt ? new Date(s.createdAt).toISOString() : undefined,
1937
+ modified: s.updatedAt ? new Date(s.updatedAt).toISOString() : undefined,
1938
+ firstMessage: s.firstMessage,
1939
+ })),
1940
+ interactiveTui: ({ sessionRef, env }) => sessionRef ? { command: "grok", args: ["--resume", sessionRef], env } : null,
1941
+ // discoverNativeSessions is on AgentRuntime; ProcessRuntime doesn't
1942
+ // expose it as an option — wire via a thin subclass below when needed.
1943
+ }
1944
+ : {};
1945
+ const runtime = new ProcessRuntime({
1946
+ id,
1947
+ displayName: spec.displayName,
1948
+ command: spec.command,
1949
+ args: runArgs,
1950
+ promptMode: spec.promptMode,
1951
+ credentials: createCredentialStore(options.credsDir),
1952
+ parserFactory: parserFactoryFor(parserId),
1953
+ preflight,
1954
+ prepare,
1955
+ model: cliModelConfig(id),
1956
+ thinking: cliThinkingConfig(id),
1957
+ usageReporting: cliUsageReporting(id),
1958
+ slashCommands: cliSlashCommands(id),
1959
+ ...resumeOpts,
1960
+ ...grokOpts,
1961
+ });
1962
+ if (id === "grok") {
1963
+ // Issue #156 discovery surface — ProcessRuntime has no options hook for
1964
+ // this; attach it so collectDiscoveredSessions picks Grok sessions up.
1965
+ runtime.discoverNativeSessions = () => discoverNativeGrokSessions();
1966
+ }
1967
+ return runtime;
1883
1968
  }
@@ -194,6 +194,33 @@ class ProcessSession {
194
194
  setName(name) {
195
195
  this.name = name;
196
196
  }
197
+ /**
198
+ * "Continue in terminal": hand this session to the agent's interactive TUI
199
+ * (see ProcessRuntimeOptions.interactiveTui). Runs prepare() first so auth
200
+ * env (e.g. GROK_HOME) matches the chat path.
201
+ */
202
+ async interactiveTuiCommand() {
203
+ const hook = this.runtimeOptions.interactiveTui;
204
+ if (!hook)
205
+ return null;
206
+ const sessionRef = this.resumeId ?? this.id;
207
+ let env = {};
208
+ if (this.runtimeOptions.prepare) {
209
+ try {
210
+ const patch = await this.runtimeOptions.prepare({});
211
+ if (patch && typeof patch === "object") {
212
+ for (const [k, v] of Object.entries(patch)) {
213
+ if (typeof v === "string")
214
+ env[k] = v;
215
+ }
216
+ }
217
+ }
218
+ catch {
219
+ env = {};
220
+ }
221
+ }
222
+ return hook({ sessionRef, cwd: this.cwd, env });
223
+ }
197
224
  /** The agent's on-disk slash commands for this workspace (Codex prompts,
198
225
  * opencode commands). Best-effort and display-only: any read failure yields an
199
226
  * empty menu, never a throw. */
@@ -460,6 +487,10 @@ export class ProcessRuntime {
460
487
  fork: false,
461
488
  // Usage reporting when a usage-emitting parser is wired (see getUsage).
462
489
  usageReporting: Boolean(options.usageReporting),
490
+ interactiveTui: Boolean(options.interactiveTui),
491
+ sessionDiscovery: Boolean(options.sessionDiscovery),
492
+ nativeSessionDiscovery: Boolean(options.listDiskSessions),
493
+ nativeSessionAdoption: Boolean(options.resumable && options.listDiskSessions),
463
494
  });
464
495
  }
465
496
  /** Session-less catalog contribution: this CLI agent's configured models, grouped by provider. */
@@ -494,6 +525,17 @@ export class ProcessRuntime {
494
525
  return this.createSession(options);
495
526
  }
496
527
  async listSessions() {
528
+ // Prefer the agent's own on-disk store when wired (Grok, …) so closed
529
+ // `bivy run` / native TUI sessions still appear in the durable list after
530
+ // the PTY exits — the in-memory set alone evaporates with the process.
531
+ if (this.options.listDiskSessions) {
532
+ try {
533
+ return await this.options.listDiskSessions();
534
+ }
535
+ catch {
536
+ // fall through to the live handles
537
+ }
538
+ }
497
539
  return this.sessions.map((session) => ({
498
540
  id: session.id,
499
541
  cwd: session.cwd,
package/dist/server.js CHANGED
@@ -31,6 +31,7 @@ import { suggestNameFromSelectedModel } from "./runtime/model-namer.js";
31
31
  import { isNativeOAuthProvider, loginModelOAuth } from "./runtime/oauth/model-oauth.js";
32
32
  import { decideOAuthLoginSweep } from "./runtime/oauth/oauth-login-sweep.js";
33
33
  import { listCodexSessions, loadCodexTranscript, discoverCodexSessionForCwd } from "./runtime/codex-sessions.js";
34
+ import { discoverGrokSessionForCwd } from "./runtime/grok-sessions.js";
34
35
  import { dedupeSessionSummaries } from "./session-identity.js";
35
36
  import { discoverPiSessionForCwd } from "./runtime/pi-session-discovery.js";
36
37
  import { exportProviderAuth, exportProviderAuthTombstones, importProviderAuth, listProviders, removeProvider, setProviderApiKey, setProviderCredential } from "./runtime/pi-auth.js";
@@ -1783,6 +1784,9 @@ async function openRunTerminal(spec, emit) {
1783
1784
  // Only genuine `bivy run` agents get quiet-agent push hints; an adopted
1784
1785
  // tmux/zellij/screen attach isn't "your agent waiting".
1785
1786
  const notifiable = !spec.mux;
1787
+ // Captured before open so the onExit discovery path (agents that assign their
1788
+ // session id lazily) can correlate by start time.
1789
+ const createdAt = Date.now();
1786
1790
  try {
1787
1791
  const id = terminals.open({
1788
1792
  workspace,
@@ -1820,6 +1824,40 @@ async function openRunTerminal(spec, emit) {
1820
1824
  clearRunIdleNotify(id);
1821
1825
  broadcast({ type: "terminal.exit", termId: id, code });
1822
1826
  broadcast({ type: "terminal.closed", termId: id });
1827
+ // Mark the durable row idle and, for agents that assign their id lazily
1828
+ // (no launch-time pin), discover the on-disk session now so it still
1829
+ // appears in the sidebar after the PTY is gone.
1830
+ if (!spec.mux && spec.agent) {
1831
+ const agentId = spec.agent;
1832
+ void (async () => {
1833
+ let sessionRef = spec.sessionId;
1834
+ if (!sessionRef) {
1835
+ try {
1836
+ sessionRef = await SESSION_DISCOVERY_BY_AGENT[agentId]?.(workspace, createdAt);
1837
+ }
1838
+ catch {
1839
+ /* agent may never have written a session */
1840
+ }
1841
+ }
1842
+ if (sessionRef) {
1843
+ const runtimeId = TAKEOVER_RUNTIME_BY_AGENT[agentId] ?? agentId;
1844
+ try {
1845
+ metadata.upsertSession({
1846
+ id: sessionRef,
1847
+ runtimeId,
1848
+ agentName: agentId,
1849
+ workspace,
1850
+ name: name || undefined,
1851
+ source: "cli",
1852
+ status: "saved",
1853
+ });
1854
+ }
1855
+ catch {
1856
+ /* best-effort */
1857
+ }
1858
+ }
1859
+ })();
1860
+ }
1823
1861
  // Unify keys, reverse direction: a login done inside the agent's own CLI
1824
1862
  // (e.g. `codex login`) is folded back into Bivy's vault, then synced.
1825
1863
  if (!spec.mux) {
@@ -1835,9 +1873,37 @@ async function openRunTerminal(spec, emit) {
1835
1873
  },
1836
1874
  });
1837
1875
  runTerminals.add(id);
1838
- const createdAt = Date.now();
1876
+ // Persist a durable metadata row when the run has a known session id (pinned
1877
+ // at launch, e.g. `claude --session-id` / `grok --session-id`). Without this,
1878
+ // a `bivy run` session vanishes from the sidebar the moment the PTY exits —
1879
+ // only live run-terminals were listed. The metadata backfill in
1880
+ // listAllSessions then keeps it resumable after close.
1881
+ if (spec.sessionId && spec.agent && !spec.mux) {
1882
+ const runtimeId = TAKEOVER_RUNTIME_BY_AGENT[spec.agent] ?? spec.agent;
1883
+ try {
1884
+ metadata.upsertSession({
1885
+ id: spec.sessionId,
1886
+ runtimeId,
1887
+ agentName: spec.agent,
1888
+ workspace,
1889
+ name: name || undefined,
1890
+ source: "cli",
1891
+ status: "working",
1892
+ });
1893
+ }
1894
+ catch {
1895
+ /* best-effort: a metadata write must never block the run launch */
1896
+ }
1897
+ }
1839
1898
  emit({ type: "terminal.opened", termId: id, workspace, mode: "run", agent: spec.agent, label, name });
1840
1899
  broadcast({ type: "terminal.created", terminal: { termId: id, workspace, createdAt, lastActivityAt: createdAt, kind: "run", agent: spec.agent, model: spec.model, label, name, command: commandLine, mux: spec.mux, sessionId: spec.sessionId, pid: terminals.pid(id) } });
1900
+ // Surface the durable row so the sidebar keeps the session after the PTY
1901
+ // exits (listAllSessions backfills from metadata). Best-effort broadcast.
1902
+ if (spec.sessionId && !spec.mux) {
1903
+ void listAllSessions()
1904
+ .then((sessions) => broadcast({ type: "sessions.list", sessions: sessions.map((s) => ({ ...s, sessionId: s.id })) }))
1905
+ .catch(() => { });
1906
+ }
1841
1907
  return id;
1842
1908
  }
1843
1909
  catch (error) {
@@ -1856,11 +1922,15 @@ const TAKEOVER_RUNTIME_BY_AGENT = {
1856
1922
  // exec-created session resumes through the app-server with full context), so
1857
1923
  // the adopted chat gets per-tool approvals, not just the exec jail.
1858
1924
  codex: "codex-approvals",
1925
+ // Official Grok CLI: resume by UUID via `grok --resume <id> -p …` (process
1926
+ // runtime) after the interactive TUI is stopped.
1927
+ grok: "grok",
1859
1928
  };
1860
1929
  // The CLI command to continue an adopted session back in a terminal.
1861
1930
  const RESUME_CLI_BY_AGENT = {
1862
1931
  claude: (id) => `claude --resume ${id}`,
1863
1932
  codex: (id) => `codex resume ${id}`,
1933
+ grok: (id) => `grok --resume ${id}`,
1864
1934
  };
1865
1935
  // Same commands, keyed by runtime id instead of the short takeover agent alias
1866
1936
  // above — for native session discovery (issue #156), where a session with a
@@ -1871,6 +1941,7 @@ const RESUME_CLI_BY_AGENT = {
1871
1941
  const NATIVE_RESUME_CLI_BY_RUNTIME = {
1872
1942
  "claude-code-sdk": (id) => `claude --resume ${id}`,
1873
1943
  "codex-approvals": (id) => `codex resume ${id}`,
1944
+ grok: (id) => `grok --resume ${id}`,
1874
1945
  };
1875
1946
  // When no resumable session id can be found, why — and what to do about it.
1876
1947
  // These agents assign their id lazily (Codex/Pi write a session only once a turn
@@ -1878,6 +1949,7 @@ const NATIVE_RESUME_CLI_BY_RUNTIME = {
1878
1949
  const TAKEOVER_EMPTY_HINT_BY_AGENT = {
1879
1950
  codex: "Codex writes its session only after the first message. Send one message in the terminal, then continue as chat.",
1880
1951
  pi: "Pi assigns its session once the conversation starts. Send one message in the terminal, then continue as chat.",
1952
+ grok: "Grok writes its session once the conversation starts. Send one message in the terminal, then continue as chat.",
1881
1953
  };
1882
1954
  // For agents with no launch-time session-id pin, locate the on-disk session a run
1883
1955
  // produced (by cwd + start time) so a takeover can still resume the right one.
@@ -1890,6 +1962,10 @@ const SESSION_DISCOVERY_BY_AGENT = {
1890
1962
  const match = discoverPiSessionForCwd(await runtimeHost.listSessions(getRuntime("pi")), cwd, since);
1891
1963
  return match?.path || match?.id;
1892
1964
  },
1965
+ // Grok can be pinned at launch (`--session-id`) but also assigns ids lazily
1966
+ // when the pin flag is absent; correlate by cwd + start time against
1967
+ // ~/.grok/sessions.
1968
+ grok: (cwd, since) => discoverGrokSessionForCwd(cwd, since)?.id,
1893
1969
  };
1894
1970
  // "Continue as chat": stop the native TUI running in a pinned run-terminal and
1895
1971
  // reopen its session as a governed, structured chat on the owning runtime. The
package/dist/terminal.js CHANGED
@@ -16,7 +16,11 @@ import { depCacheEnv } from "./harness/dep-cache.js";
16
16
  * that by an order of magnitude while adding latency well below the threshold of
17
17
  * perception for interactive echo.
18
18
  */
19
- const OUTPUT_FLUSH_MS = 8;
19
+ // 16ms keeps a single chatty TUI under the relay's default node message budget
20
+ // (6000/min ≈ 10ms/frame). At 8ms a fullscreen redraw stream could exceed that
21
+ // and the relay closed the socket with "Rate limit exceeded" the moment a
22
+ // phone/app attached to a live `bivy run` terminal.
23
+ const OUTPUT_FLUSH_MS = 16;
20
24
  /**
21
25
  * How much recent output to retain per terminal for scrollback replay on
22
26
  * reconnect. A phone that backgrounds the PWA (or drops its network) reattaches
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.150",
3
+ "version": "0.10.1-staging.152",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",