@bivy/bivy 0.16.15 → 0.16.16-staging.2

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.
@@ -370,10 +370,13 @@ export const AGENT_PROFILES = {
370
370
  // TUI uses the same store via `grok --resume <id>`. Model ids match
371
371
  // `grok models` for the official CLI (override with BIVY_GROK_MODELS).
372
372
  args: ["-p"],
373
- // The current CLI's streaming-json mode is ACP's standard NDJSON envelope.
374
- // The shared tolerant parser unwraps session/update notifications, including
375
- // tool calls/results and sub-agent activity, so Grok gets faithful transcripts
376
- // without a Grok-specific adapter. Keep the plain args as the explicit
373
+ // The current CLI's `--output-format streaming-json` emits newline-delimited
374
+ // JSON keyed off `type`: {type:"text",data} for the answer, {type:"thought",
375
+ // data} for reasoning, {type:"end"} to close the turn (plus tool frames). The
376
+ // shared tolerant generic-stream-json parser understands this shape (and the
377
+ // ACP session/update envelope other CLIs use), so Grok gets faithful
378
+ // transcripts — answer prose, a thinking sidecar, and tool cards — without a
379
+ // Grok-specific adapter. Keep the plain args as the explicit
377
380
  // BIVY_AGENT_STRUCTURED=0 fallback.
378
381
  jsonArgs: ["--output-format", "streaming-json", "-p"],
379
382
  parserId: "generic-stream-json",
@@ -384,12 +387,12 @@ export const AGENT_PROFILES = {
384
387
  model: {
385
388
  flag: "-m",
386
389
  models: [
387
- // Official Grok CLI (1.x) currently advertises grok-4.5 as the default
388
- // subscription model. Older curated ids (grok-4-latest, grok-code-fast-1,
389
- // …) return "unknown model id" against current CLIs — keep the list
390
+ // Official Grok CLI (1.x) advertises grok-4.6 as the default subscription
391
+ // model (verified against `grok models`); the older grok-4.5 / grok-4-latest
392
+ // / grok-code-fast-1 ids now return "unknown model id". Keep the list
390
393
  // honest; operators can override with BIVY_GROK_MODELS if their install
391
394
  // exposes more.
392
- { id: "grok-4.5", name: "Grok 4.5", provider: "xai" },
395
+ { id: "grok-4.6", name: "Grok 4.6", provider: "xai" },
393
396
  ],
394
397
  },
395
398
  promptMode: "argv",
@@ -607,7 +607,12 @@ function acpToolUpdate(msg) {
607
607
  return undefined;
608
608
  }
609
609
  // Event `type` values that mean "the turn is finished" across the various CLIs.
610
- const STREAM_TERMINALS = new Set(["result", "done", "complete", "completed", "turn.completed", "session.done", "session_end", "session/ended", "message_stop", "response.completed", "final"]);
610
+ const STREAM_TERMINALS = new Set(["result", "done", "complete", "completed", "turn.completed", "session.done", "session_end", "session/ended", "message_stop", "response.completed", "final", "end"]);
611
+ // Event `type` values whose chunk (in a `data` field) is assistant answer prose
612
+ // vs. reasoning/thinking. Used by the generic streaming parser to support CLIs
613
+ // (e.g. Grok) that key the content off `type` with the text under `data`.
614
+ const STREAM_TEXT_TYPES = new Set(["text", "assistant", "answer", "agent_message", "assistant_message", "output_text", "response_text"]);
615
+ const STREAM_REASONING_TYPES = new Set(["thought", "thinking", "reasoning"]);
611
616
  /**
612
617
  * A TOLERANT line-delimited JSON parser for CLIs whose `--stream-json` /
613
618
  * `--format json` streaming vocabularies we haven't pinned exactly (Amp, Cursor,
@@ -645,7 +650,8 @@ export function genericStreamJsonParser() {
645
650
  // (a suffix match, no per-agent branch) so the message is surfaced as a
646
651
  // real turn error instead of leaking into the transcript as assistant
647
652
  // prose via the broad `message` text fallback in textFromStreamEvent.
648
- const isErrorFrame = /(^|[._:/])error$/.test(type.toLowerCase());
653
+ const lowerType = type.toLowerCase();
654
+ const isErrorFrame = /(^|[._:/])error$/.test(lowerType);
649
655
  const tool = acpToolUpdate(msg);
650
656
  if (tool?.kind === "call")
651
657
  acc.addToolUse(tool.id, tool.name ?? "tool", tool.input, events);
@@ -660,7 +666,21 @@ export function genericStreamJsonParser() {
660
666
  if (typeof m === "string" && m.trim())
661
667
  events.push({ type: "session.error", error: m.trim() });
662
668
  }
663
- const text = isErrorFrame ? "" : textFromStreamEvent(msg);
669
+ // Reasoning/thinking stream carried as a typed chunk keyed by `type` with
670
+ // the content in a `data` field (Grok's streaming-json: {type:"thought",
671
+ // data}). Surface it as the same display-only thinking sidecar every agent
672
+ // uses, never as answer prose — a generic shape, not a per-agent branch.
673
+ if (!isErrorFrame && STREAM_REASONING_TYPES.has(lowerType) && typeof msg.data === "string") {
674
+ acc.appendReasoning(msg.data, events);
675
+ return events;
676
+ }
677
+ // Assistant answer text. Most CLIs expose it via one of the fields
678
+ // textFromStreamEvent covers; some (Grok) put it in `data` keyed by an
679
+ // assistant-text `type`. Fall back to `data` only for those types so an
680
+ // unrelated control frame's `data` never leaks into the transcript.
681
+ let text = isErrorFrame ? "" : textFromStreamEvent(msg);
682
+ if (!text && !isErrorFrame && typeof msg.data === "string" && STREAM_TEXT_TYPES.has(lowerType))
683
+ text = msg.data;
664
684
  if (text && !STREAM_TERMINALS.has(type)) {
665
685
  acc.appendText(text, events);
666
686
  sawText = true;
@@ -40,6 +40,60 @@ export function grokAuthEntryKey(clientId) {
40
40
  const id = clientId?.trim() || getModelOAuthProvider("xai")?.clientId || "";
41
41
  return `${GROK_OIDC_ISSUER}::${id}`;
42
42
  }
43
+ /** Decode a JWT payload without verifying its signature (best-effort). */
44
+ function decodeJwtClaims(token) {
45
+ const parts = token.split(".");
46
+ if (parts.length < 2 || !parts[1])
47
+ return undefined;
48
+ try {
49
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
50
+ const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
51
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ /**
58
+ * The xAI user id the Grok CLI records as `user_id` in each auth.json entry —
59
+ * the OIDC subject (`sub`) of the access token (falling back to `principal_id`).
60
+ * The current Grok CLI (1.x) rejects an auth.json entry that is *missing* this
61
+ * field (serde: "missing field `user_id`"), so a minted file without it cannot
62
+ * be parsed — Grok then can't authenticate or self-refresh and silently
63
+ * produces empty turns. Bivy's `xai` vault record only stores {access, refresh,
64
+ * expires}, so we recover the id from the access token's own claims.
65
+ */
66
+ export function grokUserIdFromAccessToken(access) {
67
+ const claims = decodeJwtClaims(access);
68
+ if (!claims)
69
+ return undefined;
70
+ const sub = typeof claims.sub === "string" ? claims.sub.trim() : "";
71
+ if (sub)
72
+ return sub;
73
+ const principal = typeof claims.principal_id === "string" ? claims.principal_id.trim() : "";
74
+ return principal || undefined;
75
+ }
76
+ /**
77
+ * Whether an existing auth.json entry for our scope is parseable by the current
78
+ * Grok CLI. An entry written by an older Bivy (or hand-rolled) that lacks a
79
+ * non-empty `user_id` cannot be loaded — leaving it in place guarantees a
80
+ * broken, unauthenticated session, so those are re-minted rather than kept.
81
+ */
82
+ function grokEntryIsHealthy(entry) {
83
+ if (!entry || typeof entry !== "object")
84
+ return false;
85
+ const userId = entry.user_id;
86
+ return typeof userId === "string" && userId.trim().length > 0;
87
+ }
88
+ function readGrokAuthJson(authFile) {
89
+ try {
90
+ const parsed = JSON.parse(fs.readFileSync(authFile, "utf8"));
91
+ return parsed && typeof parsed === "object" ? parsed : undefined;
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ }
43
97
  /**
44
98
  * Ensure the Grok CLI has a usable credential file, minting one from Bivy's
45
99
  * vault when needed. Returns the resolved `GROK_HOME` (so the caller can pin it
@@ -47,24 +101,32 @@ export function grokAuthEntryKey(clientId) {
47
101
  * credential — in which case the caller's preflight surfaces the actionable
48
102
  * "no credential" error unchanged.
49
103
  *
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.
104
+ * Idempotent and low-churn: a *parseable* existing entry for our scope (a native
105
+ * `grok login` or a prior materialization) is left untouched — Grok owns and
106
+ * self-refreshes it. The one exception is a legacy entry that the current Grok
107
+ * CLI can no longer parse (missing `user_id`): leaving that in place guarantees
108
+ * a silently-unauthenticated session, so it is re-minted from the vault (other
109
+ * scopes in the file are preserved). We write to the *default* Grok home (never
110
+ * a throwaway dir) so sessions stay where the CLI already looks.
54
111
  */
55
112
  export async function ensureGrokAuth(credsDir) {
56
113
  const grokHome = resolveGrokHome();
57
114
  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
115
  const provider = getModelOAuthProvider("xai");
66
116
  if (!provider)
67
- return undefined;
117
+ return fs.existsSync(authFile) ? grokHome : undefined;
118
+ const entryKey = grokAuthEntryKey(provider.clientId);
119
+ // A parseable entry for our scope is Grok's to own and refresh — never clobber
120
+ // it. Only an absent file or a legacy entry the current CLI can't load (no
121
+ // `user_id`) falls through to (re)minting below.
122
+ const existingJson = fs.existsSync(authFile) ? readGrokAuthJson(authFile) : undefined;
123
+ if (existingJson && grokEntryIsHealthy(existingJson[entryKey]))
124
+ return grokHome;
125
+ // An API key authenticates Grok directly (preflight / env projection) — no
126
+ // auth.json needed. Only skip minting when we have no OAuth entry to heal.
127
+ if (process.env.XAI_API_KEY?.trim() || process.env.GROK_API_KEY?.trim()) {
128
+ return fs.existsSync(authFile) ? grokHome : undefined;
129
+ }
68
130
  // Ensure the vault access token is still live before we project it. No-op when
69
131
  // fresh; refreshes under the store lock when expired. Failure leaves the vault
70
132
  // alone and we fall through to the (likely still-stale) read below.
@@ -77,10 +139,17 @@ export async function ensureGrokAuth(credsDir) {
77
139
  const refresh = typeof cred.refresh === "string" ? cred.refresh : "";
78
140
  if (!access || !refresh)
79
141
  return undefined;
142
+ // The current Grok CLI requires `user_id` on every entry; recover it from the
143
+ // access token's OIDC claims. Without it the file is unparseable and useless,
144
+ // so surface the honest "no credential" state (undefined) rather than writing
145
+ // a file Grok will silently reject.
146
+ const userId = grokUserIdFromAccessToken(access);
147
+ if (!userId)
148
+ return fs.existsSync(authFile) ? grokHome : undefined;
80
149
  const expiresMs = Number(cred.expires) || 0;
81
- const entryKey = grokAuthEntryKey(provider.clientId);
82
150
  const entry = {
83
151
  key: access,
152
+ user_id: userId,
84
153
  auth_mode: "oidc",
85
154
  create_time: new Date().toISOString(),
86
155
  refresh_token: refresh,
@@ -89,7 +158,9 @@ export async function ensureGrokAuth(credsDir) {
89
158
  };
90
159
  if (expiresMs > 0)
91
160
  entry.expires_at = new Date(expiresMs).toISOString();
92
- const authJson = { [entryKey]: entry };
161
+ // Preserve any other scopes already present (e.g. a native login for a
162
+ // different client id) — replace only our own entry.
163
+ const authJson = { ...(existingJson ?? {}), [entryKey]: entry };
93
164
  try {
94
165
  fs.mkdirSync(grokHome, { recursive: true, mode: 0o700 });
95
166
  const tmp = `${authFile}.tmp`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.16.15",
3
+ "version": "0.16.16-staging.2",
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.",