@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.
- package/dist/agents/profiles.js +11 -8
- package/dist/runtime/cli-parsers.js +23 -3
- package/dist/runtime/grok-auth.js +85 -14
- package/package.json +1 -1
package/dist/agents/profiles.js
CHANGED
|
@@ -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
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
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)
|
|
388
|
-
//
|
|
389
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
51
|
-
* `grok login` or a prior materialization)
|
|
52
|
-
* self-refreshes it
|
|
53
|
-
*
|
|
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
|
-
|
|
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`;
|