@bubblebrain-ai/bubble 0.0.35 → 0.0.36
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 +27 -7
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -0
- package/dist/agent/profiles.js +25 -11
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/workflow/control.d.ts +14 -0
- package/dist/agent/workflow/control.js +30 -9
- package/dist/agent.d.ts +75 -5
- package/dist/agent.js +313 -61
- package/dist/approval/controller.js +28 -0
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +21 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/context/compact-llm.d.ts +2 -2
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.js +21 -0
- package/dist/feishu/agent-host/run-driver.js +19 -3
- package/dist/feishu/serve.js +1 -0
- package/dist/goal/tools.d.ts +6 -3
- package/dist/goal/tools.js +7 -19
- package/dist/main.js +105 -27
- package/dist/memory/db.d.ts +2 -0
- package/dist/memory/db.js +10 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/phase1.js +14 -2
- package/dist/memory/phase2.js +21 -2
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/storage.d.ts +1 -0
- package/dist/memory/storage.js +13 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +87 -41
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +11 -0
- package/dist/oauth/grok.d.ts +25 -0
- package/dist/oauth/grok.js +218 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/storage.d.ts +9 -0
- package/dist/oauth/storage.js +25 -0
- package/dist/orchestrator/default-hooks.js +17 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.js +22 -3
- package/dist/prompt/environment.js +0 -4
- package/dist/prompt/reminders.d.ts +0 -1
- package/dist/prompt/reminders.js +1 -11
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.js +1 -1
- package/dist/prompt/task-reminders.d.ts +19 -0
- package/dist/prompt/task-reminders.js +49 -0
- package/dist/provider-ai-sdk.js +2 -2
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +59 -0
- package/dist/provider-openai-codex.d.ts +17 -1
- package/dist/provider-openai-codex.js +101 -29
- package/dist/provider-registry.d.ts +72 -0
- package/dist/provider-registry.js +336 -59
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +28 -5
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +14 -0
- package/dist/session-types.d.ts +19 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +4 -0
- package/dist/slash-commands/commands.js +224 -14
- package/dist/slash-commands/types.d.ts +14 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.js +9 -11
- package/dist/tools/bash.js +1 -1
- package/dist/tools/child-tools.js +2 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +7 -5
- package/dist/tools/lsp.js +1 -0
- package/dist/tools/memory.d.ts +7 -2
- package/dist/tools/memory.js +28 -43
- package/dist/tools/server.js +4 -0
- package/dist/tools/tool-search.d.ts +1 -1
- package/dist/tools/tool-search.js +23 -3
- package/dist/tui/model-switch.d.ts +7 -0
- package/dist/tui/model-switch.js +15 -5
- package/dist/tui-ink/app.d.ts +3 -1
- package/dist/tui-ink/app.js +648 -232
- package/dist/tui-ink/approval/approval-dialog.js +36 -0
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +7 -2
- package/dist/tui-ink/input-box.d.ts +35 -1
- package/dist/tui-ink/input-box.js +215 -122
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +453 -123
- package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
- package/dist/tui-ink/paste-operation-tracker.js +25 -0
- package/dist/tui-ink/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +31 -0
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/types.d.ts +5 -5
- package/dist/types.js +1 -1
- package/dist/variant/thinking-level.d.ts +2 -2
- package/dist/variant/thinking-level.js +2 -1
- package/dist/variant/variant-resolver.d.ts +9 -0
- package/dist/variant/variant-resolver.js +17 -0
- package/package.json +2 -1
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI argument parsing.
|
|
3
3
|
*/
|
|
4
|
+
import { THINKING_LEVELS } from "./types.js";
|
|
4
5
|
import { isThinkingLevel } from "./variant/thinking-level.js";
|
|
5
6
|
export function parseArgs(argv) {
|
|
6
7
|
const args = {
|
|
@@ -102,7 +103,7 @@ Options (default):
|
|
|
102
103
|
-r, --resume Resume a previous session (latest by default)
|
|
103
104
|
--session <name> Session name to create or resume
|
|
104
105
|
--reasoning Enable reasoning mode at medium effort
|
|
105
|
-
--reasoning-effort <l> Set reasoning effort:
|
|
106
|
+
--reasoning-effort <l> Set reasoning effort: ${THINKING_LEVELS.join("|")}
|
|
106
107
|
--plan Start in plan mode (read-only investigation; propose before executing)
|
|
107
108
|
--dangerously-skip-permissions
|
|
108
109
|
Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Uses a single JSON file in Bubble home, normally ~/.bubble/config.json.
|
|
5
5
|
*/
|
|
6
6
|
import { type AgentCategoriesConfig } from "./agent/categories.js";
|
|
7
|
+
import { type AgentRoutingConfig } from "./agent/routing-catalog.js";
|
|
7
8
|
import type { ProviderProfile } from "./provider-registry.js";
|
|
8
9
|
import type { ThinkingLevel } from "./types.js";
|
|
9
10
|
export type ThemeMode = "auto" | "light" | "dark";
|
|
@@ -31,6 +32,8 @@ export interface UserConfigData {
|
|
|
31
32
|
providers?: ProviderProfile[];
|
|
32
33
|
defaultProvider?: string;
|
|
33
34
|
agentCategories?: AgentCategoriesConfig;
|
|
35
|
+
/** Subagent model-routing knobs: autoTier (default true), allowCrossProvider (default true). */
|
|
36
|
+
agentRouting?: Partial<AgentRoutingConfig>;
|
|
34
37
|
subagents?: SubagentsUserConfig;
|
|
35
38
|
}
|
|
36
39
|
export interface SubagentsUserConfig {
|
|
@@ -62,6 +65,7 @@ export declare class UserConfig {
|
|
|
62
65
|
setThemeMode(mode: ThemeMode): void;
|
|
63
66
|
setThemeOverrides(overrides: Record<string, string>): void;
|
|
64
67
|
getAgentCategories(): AgentCategoriesConfig;
|
|
68
|
+
getAgentRouting(): AgentRoutingConfig;
|
|
65
69
|
getSubagents(): SubagentsUserConfig;
|
|
66
70
|
}
|
|
67
71
|
export declare function shouldProbeTerminalTheme(_config: ThemeConfig): boolean;
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { getBubbleHome } from "./bubble-home.js";
|
|
9
9
|
import { sanitizeAgentCategories } from "./agent/categories.js";
|
|
10
|
+
import { sanitizeAgentRouting } from "./agent/routing-catalog.js";
|
|
10
11
|
const HIDDEN_PROVIDER_IDS = new Set(["openrouter", "openai-codex"]);
|
|
11
12
|
function getConfigPath() {
|
|
12
13
|
return join(getBubbleHome(), "config.json");
|
|
@@ -204,6 +205,9 @@ export class UserConfig {
|
|
|
204
205
|
getAgentCategories() {
|
|
205
206
|
return sanitizeAgentCategories(this.data.agentCategories);
|
|
206
207
|
}
|
|
208
|
+
getAgentRouting() {
|
|
209
|
+
return sanitizeAgentRouting(this.data.agentRouting);
|
|
210
|
+
}
|
|
207
211
|
getSubagents() {
|
|
208
212
|
return sanitizeSubagentsConfig(this.data.subagents) ?? {};
|
|
209
213
|
}
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* fails.
|
|
8
8
|
*/
|
|
9
9
|
import type { CompactOptions, CompactResult } from "./compact.js";
|
|
10
|
-
import type { Message, Provider, ProviderMessage } from "../types.js";
|
|
10
|
+
import type { Message, Provider, ProviderMessage, ThinkingLevel } from "../types.js";
|
|
11
11
|
export interface LLMCompactOptions extends CompactOptions {
|
|
12
12
|
provider: Provider;
|
|
13
13
|
model: string;
|
|
14
|
-
thinkingLevel?:
|
|
14
|
+
thinkingLevel?: ThinkingLevel;
|
|
15
15
|
}
|
|
16
16
|
export declare const COMPACT_SYSTEM_PROMPT = "You are a conversation summarizer. Your job is to produce a structured\nsummary of an earlier portion of a software-engineering assistant's\nconversation so that the assistant can continue working without the full\nhistory. Preserve fidelity over brevity where the user's intent, file\npaths, or decisions are concerned. Output ONLY the summary, no preamble.";
|
|
17
17
|
export declare const COMPACT_INSTRUCTIONS = "Summarize the conversation above using exactly these 9 sections, each\npreceded by the literal heading on its own line. If a section has no\ncontent, write \"None\".\n\n1. Primary Request and Intent\n - What the user ultimately wants, in their own framing.\n\n2. Key Technical Concepts\n - Libraries, frameworks, architectural patterns referenced.\n\n3. Files and Code Sections\n - Files read, written, or discussed. Include full paths and a one-line note.\n\n4. Errors and Fixes\n - Bugs encountered and how they were resolved.\n\n5. Problem Solving\n - Non-trivial debugging or design decisions.\n\n6. All User Messages\n - Every user message, verbatim, in order. Do not summarize here.\n\n7. Pending Tasks\n - Work that was planned but not yet completed.\n\n8. Current Work\n - What was being actively worked on when the summary was taken.\n\n9. Optional Next Step\n - The single most natural next action, if obvious.";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { GrokProfile } from "./grok-profile.js";
|
|
3
|
+
import type { GrokSpawn } from "./grok-process.js";
|
|
4
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
5
|
+
export interface GrokAcpHandlers {
|
|
6
|
+
onSessionUpdate(notification: acp.SessionNotification): void | Promise<void>;
|
|
7
|
+
onPermissionRequest(params: acp.RequestPermissionRequest): Promise<string | undefined>;
|
|
8
|
+
onPermissionViolation(sessionId?: string): void | Promise<void>;
|
|
9
|
+
onCrash(error: GrokRuntimeError): void;
|
|
10
|
+
}
|
|
11
|
+
export interface GrokAcpClient {
|
|
12
|
+
initialize(): Promise<void>;
|
|
13
|
+
newSession(): Promise<GrokAcpSessionState>;
|
|
14
|
+
loadSession(sessionId: string): Promise<GrokAcpSessionState>;
|
|
15
|
+
setModel(sessionId: string, modelId: string): Promise<GrokAcpModelState | undefined>;
|
|
16
|
+
prompt(sessionId: string, prompt: string, generation: number, signal?: AbortSignal): Promise<acp.PromptResponse>;
|
|
17
|
+
cancel(sessionId: string): Promise<void>;
|
|
18
|
+
dispose(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface GrokAcpModelInfo {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GrokAcpModelState {
|
|
26
|
+
currentModelId: string;
|
|
27
|
+
availableModels: GrokAcpModelInfo[];
|
|
28
|
+
}
|
|
29
|
+
export interface GrokAcpSessionState {
|
|
30
|
+
id: string;
|
|
31
|
+
models?: GrokAcpModelState;
|
|
32
|
+
}
|
|
33
|
+
export type GrokAcpFactory = (options: {
|
|
34
|
+
binary: string;
|
|
35
|
+
profile: GrokProfile;
|
|
36
|
+
workspace: string;
|
|
37
|
+
modelId?: string;
|
|
38
|
+
reasoningEffort?: string;
|
|
39
|
+
permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions";
|
|
40
|
+
env: NodeJS.ProcessEnv;
|
|
41
|
+
spawn: GrokSpawn;
|
|
42
|
+
handlers: GrokAcpHandlers;
|
|
43
|
+
}) => GrokAcpClient;
|
|
44
|
+
export declare const GROK_ACP_ARGS: (workspace: string, modelId?: string, reasoningEffort?: string, permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions") => readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* Reassemble stderr before redaction so secrets split across arbitrary child
|
|
47
|
+
* process chunks cannot bypass token patterns. If the private in-memory raw
|
|
48
|
+
* buffer exceeds its cap, discard it wholesale and expose only a fixed notice.
|
|
49
|
+
*/
|
|
50
|
+
export declare class GrokDiagnosticBuffer {
|
|
51
|
+
private raw;
|
|
52
|
+
private overflowed;
|
|
53
|
+
append(chunk: Buffer | string): void;
|
|
54
|
+
sanitized(maxLength?: number): string;
|
|
55
|
+
}
|
|
56
|
+
export declare const createDefaultGrokAcpClient: GrokAcpFactory;
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
3
|
+
import { sanitizeGrokDiagnostic } from "./grok-process.js";
|
|
4
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
5
|
+
import { createBoundedGrokNdjsonStream } from "./grok-ndjson.js";
|
|
6
|
+
export const GROK_ACP_ARGS = (workspace, modelId, reasoningEffort, permissionMode = "default") => [
|
|
7
|
+
"--no-auto-update",
|
|
8
|
+
"--cwd",
|
|
9
|
+
workspace,
|
|
10
|
+
"--no-memory",
|
|
11
|
+
"--no-subagents",
|
|
12
|
+
"--no-plan",
|
|
13
|
+
"--disable-web-search",
|
|
14
|
+
"--tools",
|
|
15
|
+
"Read,Edit,Grep,Bash",
|
|
16
|
+
"--permission-mode",
|
|
17
|
+
permissionMode,
|
|
18
|
+
"--sandbox",
|
|
19
|
+
"strict",
|
|
20
|
+
...(modelId ? ["--model", modelId] : []),
|
|
21
|
+
...(modelId && reasoningEffort && reasoningEffort !== "off" ? ["--reasoning-effort", reasoningEffort] : []),
|
|
22
|
+
"agent",
|
|
23
|
+
"--no-leader",
|
|
24
|
+
"stdio",
|
|
25
|
+
];
|
|
26
|
+
function processCrash(code, signal, diagnostic) {
|
|
27
|
+
const reason = code === null ? `signal ${signal ?? "unknown"}` : `exit code ${code}`;
|
|
28
|
+
return new GrokRuntimeError("process_crashed", `Grok runtime stopped unexpectedly (${reason}).`, diagnostic || undefined);
|
|
29
|
+
}
|
|
30
|
+
const MAX_GROK_STDERR_RAW_BYTES = 64 * 1024;
|
|
31
|
+
/**
|
|
32
|
+
* Reassemble stderr before redaction so secrets split across arbitrary child
|
|
33
|
+
* process chunks cannot bypass token patterns. If the private in-memory raw
|
|
34
|
+
* buffer exceeds its cap, discard it wholesale and expose only a fixed notice.
|
|
35
|
+
*/
|
|
36
|
+
export class GrokDiagnosticBuffer {
|
|
37
|
+
raw = "";
|
|
38
|
+
overflowed = false;
|
|
39
|
+
append(chunk) {
|
|
40
|
+
if (this.overflowed)
|
|
41
|
+
return;
|
|
42
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
|
|
43
|
+
if (Buffer.byteLength(this.raw, "utf8") + Buffer.byteLength(text, "utf8") > MAX_GROK_STDERR_RAW_BYTES) {
|
|
44
|
+
this.raw = "";
|
|
45
|
+
this.overflowed = true;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.raw += text;
|
|
49
|
+
}
|
|
50
|
+
sanitized(maxLength = 2048) {
|
|
51
|
+
if (this.overflowed)
|
|
52
|
+
return "Grok ACP stderr exceeded the safe diagnostic limit.";
|
|
53
|
+
return sanitizeGrokDiagnostic(this.raw, maxLength);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
function parseModelState(value) {
|
|
60
|
+
if (!isRecord(value) || typeof value.currentModelId !== "string" || !Array.isArray(value.availableModels))
|
|
61
|
+
return undefined;
|
|
62
|
+
const availableModels = [];
|
|
63
|
+
for (const item of value.availableModels) {
|
|
64
|
+
if (!isRecord(item))
|
|
65
|
+
return undefined;
|
|
66
|
+
const id = typeof item.modelId === "string" ? item.modelId : typeof item.id === "string" ? item.id : undefined;
|
|
67
|
+
if (!id)
|
|
68
|
+
return undefined;
|
|
69
|
+
availableModels.push({
|
|
70
|
+
id,
|
|
71
|
+
name: typeof item.name === "string" ? item.name : id,
|
|
72
|
+
...(typeof item.description === "string" ? { description: item.description } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (availableModels.length === 0 || !availableModels.some((model) => model.id === value.currentModelId))
|
|
76
|
+
return undefined;
|
|
77
|
+
return { currentModelId: value.currentModelId, availableModels };
|
|
78
|
+
}
|
|
79
|
+
function parseSessionResponse(value, fallbackId) {
|
|
80
|
+
if (!isRecord(value))
|
|
81
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP returned an invalid session response.");
|
|
82
|
+
const id = typeof value.sessionId === "string" ? value.sessionId : fallbackId;
|
|
83
|
+
if (!id)
|
|
84
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP session response omitted its session ID.");
|
|
85
|
+
const models = value.models === undefined ? undefined : parseModelState(value.models);
|
|
86
|
+
if (value.models !== undefined && !models) {
|
|
87
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP returned an invalid model catalog.");
|
|
88
|
+
}
|
|
89
|
+
return { id, ...(models ? { models } : {}) };
|
|
90
|
+
}
|
|
91
|
+
function parseInterceptedSessionUpdate(message) {
|
|
92
|
+
if (!("method" in message) || message.method !== acp.methods.client.session.update)
|
|
93
|
+
return undefined;
|
|
94
|
+
if ("id" in message) {
|
|
95
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP sent session/update as a request.");
|
|
96
|
+
}
|
|
97
|
+
if (!isRecord(message.params)
|
|
98
|
+
|| typeof message.params.sessionId !== "string"
|
|
99
|
+
|| !isRecord(message.params.update)
|
|
100
|
+
|| typeof message.params.update.sessionUpdate !== "string") {
|
|
101
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP sent an invalid session update.");
|
|
102
|
+
}
|
|
103
|
+
if (message.params._meta !== undefined && message.params._meta !== null && !isRecord(message.params._meta)) {
|
|
104
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP sent invalid session metadata.");
|
|
105
|
+
}
|
|
106
|
+
return message.params;
|
|
107
|
+
}
|
|
108
|
+
async function settlesWithin(promise, timeoutMs) {
|
|
109
|
+
return await new Promise((resolve) => {
|
|
110
|
+
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
111
|
+
timer.unref?.();
|
|
112
|
+
void promise.then(() => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
resolve(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
class DefaultGrokAcpClient {
|
|
119
|
+
options;
|
|
120
|
+
child;
|
|
121
|
+
connection;
|
|
122
|
+
stderr = new GrokDiagnosticBuffer();
|
|
123
|
+
disposed = false;
|
|
124
|
+
initialized = false;
|
|
125
|
+
crashReported = false;
|
|
126
|
+
crashError;
|
|
127
|
+
exited;
|
|
128
|
+
notificationTail = Promise.resolve();
|
|
129
|
+
constructor(options) {
|
|
130
|
+
this.options = options;
|
|
131
|
+
const child = options.spawn(options.binary, GROK_ACP_ARGS(options.workspace, options.modelId, options.reasoningEffort, options.permissionMode), {
|
|
132
|
+
cwd: options.workspace,
|
|
133
|
+
env: options.env,
|
|
134
|
+
shell: false,
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
136
|
+
});
|
|
137
|
+
this.child = child;
|
|
138
|
+
this.exited = new Promise((resolve) => {
|
|
139
|
+
child.once("exit", () => resolve());
|
|
140
|
+
child.once("error", () => resolve());
|
|
141
|
+
});
|
|
142
|
+
if (!child.stdin || !child.stdout) {
|
|
143
|
+
child.kill();
|
|
144
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP did not provide stdio pipes.");
|
|
145
|
+
}
|
|
146
|
+
child.stderr?.on("data", (chunk) => {
|
|
147
|
+
this.stderr.append(chunk);
|
|
148
|
+
});
|
|
149
|
+
child.once("error", () => this.reportCrash(processCrash(null, null, this.stderr.sanitized())));
|
|
150
|
+
child.once("exit", (code, signal) => {
|
|
151
|
+
if (!this.disposed)
|
|
152
|
+
this.reportCrash(processCrash(code, signal, this.stderr.sanitized()));
|
|
153
|
+
});
|
|
154
|
+
const rejectHostCapability = async (sessionId) => {
|
|
155
|
+
await this.enqueuePermissionViolation(sessionId);
|
|
156
|
+
throw acp.RequestError.requestCancelled(undefined, "This host capability is not exposed by Bubble's Grok runtime.");
|
|
157
|
+
};
|
|
158
|
+
const app = acp
|
|
159
|
+
.client({ name: "bubble-grok-runtime" })
|
|
160
|
+
.onRequest(acp.methods.client.session.requestPermission, async ({ params }) => {
|
|
161
|
+
const optionId = await options.handlers.onPermissionRequest(params);
|
|
162
|
+
if (!optionId || !params.options.some((option) => option.optionId === optionId)) {
|
|
163
|
+
return { outcome: { outcome: "cancelled" } };
|
|
164
|
+
}
|
|
165
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
166
|
+
})
|
|
167
|
+
.onRequest(acp.methods.client.fs.readTextFile, async ({ params }) => await rejectHostCapability(params.sessionId))
|
|
168
|
+
.onRequest(acp.methods.client.fs.writeTextFile, async ({ params }) => await rejectHostCapability(params.sessionId))
|
|
169
|
+
.onRequest(acp.methods.client.terminal.create, async ({ params }) => await rejectHostCapability(params.sessionId))
|
|
170
|
+
.onRequest(acp.methods.client.terminal.output, async ({ params }) => await rejectHostCapability(params.sessionId))
|
|
171
|
+
.onRequest(acp.methods.client.terminal.release, async ({ params }) => await rejectHostCapability(params.sessionId))
|
|
172
|
+
.onRequest(acp.methods.client.terminal.waitForExit, async ({ params }) => await rejectHostCapability(params.sessionId))
|
|
173
|
+
.onRequest(acp.methods.client.terminal.kill, async ({ params }) => await rejectHostCapability(params.sessionId))
|
|
174
|
+
.onRequest(acp.methods.client.elicitation.create, async ({ params }) => await rejectHostCapability("sessionId" in params && typeof params.sessionId === "string" ? params.sessionId : undefined))
|
|
175
|
+
.onNotification(acp.methods.client.elicitation.complete, () => this.enqueuePermissionViolation(undefined));
|
|
176
|
+
const input = Writable.toWeb(child.stdin);
|
|
177
|
+
const output = Readable.toWeb(child.stdout);
|
|
178
|
+
const stream = createBoundedGrokNdjsonStream(input, output, {
|
|
179
|
+
interceptIncoming: (message) => {
|
|
180
|
+
const notification = parseInterceptedSessionUpdate(message);
|
|
181
|
+
if (!notification)
|
|
182
|
+
return false;
|
|
183
|
+
void this.enqueueSessionUpdate(notification);
|
|
184
|
+
return true;
|
|
185
|
+
},
|
|
186
|
+
onProtocolError: (error) => {
|
|
187
|
+
this.reportCrash(error);
|
|
188
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
189
|
+
child.kill("SIGTERM");
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
this.connection = app.connect(stream);
|
|
193
|
+
}
|
|
194
|
+
reportCrash(error) {
|
|
195
|
+
if (this.crashReported || this.disposed)
|
|
196
|
+
return;
|
|
197
|
+
this.crashReported = true;
|
|
198
|
+
this.crashError = error;
|
|
199
|
+
this.options.handlers.onCrash(error);
|
|
200
|
+
}
|
|
201
|
+
enqueueSessionUpdate(notification) {
|
|
202
|
+
const work = this.notificationTail.then(async () => {
|
|
203
|
+
try {
|
|
204
|
+
await this.options.handlers.onSessionUpdate(notification);
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
const error = cause instanceof GrokRuntimeError
|
|
208
|
+
? cause
|
|
209
|
+
: new GrokRuntimeError("protocol_error", "Grok ACP notification handling failed.");
|
|
210
|
+
this.reportCrash(error);
|
|
211
|
+
if (this.child.exitCode === null && this.child.signalCode === null)
|
|
212
|
+
this.child.kill("SIGTERM");
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
this.notificationTail = work;
|
|
216
|
+
return work;
|
|
217
|
+
}
|
|
218
|
+
enqueuePermissionViolation(sessionId) {
|
|
219
|
+
const work = this.notificationTail.then(async () => {
|
|
220
|
+
try {
|
|
221
|
+
await this.options.handlers.onPermissionViolation(sessionId);
|
|
222
|
+
}
|
|
223
|
+
catch (cause) {
|
|
224
|
+
const error = cause instanceof GrokRuntimeError
|
|
225
|
+
? cause
|
|
226
|
+
: new GrokRuntimeError("protocol_error", "Grok ACP policy handling failed.");
|
|
227
|
+
this.reportCrash(error);
|
|
228
|
+
if (this.child.exitCode === null && this.child.signalCode === null)
|
|
229
|
+
this.child.kill("SIGTERM");
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.notificationTail = work;
|
|
233
|
+
return work;
|
|
234
|
+
}
|
|
235
|
+
async drainNotifications() {
|
|
236
|
+
// Incoming requests/notifications are dispatched independently from the
|
|
237
|
+
// response promise. Let already-parsed work join the ordered tail before
|
|
238
|
+
// deciding that initialize/new/load completed safely.
|
|
239
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
240
|
+
await this.notificationTail;
|
|
241
|
+
if (this.crashError)
|
|
242
|
+
throw this.crashError;
|
|
243
|
+
}
|
|
244
|
+
async initialize() {
|
|
245
|
+
if (this.initialized)
|
|
246
|
+
return;
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = await this.connection.agent.request(acp.methods.agent.initialize, {
|
|
250
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
251
|
+
clientCapabilities: { session: { configOptions: { boolean: {} } } },
|
|
252
|
+
clientInfo: { name: "bubble", title: "Bubble", version: "1" },
|
|
253
|
+
});
|
|
254
|
+
await this.drainNotifications();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
if (this.crashError)
|
|
258
|
+
throw this.crashError;
|
|
259
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP initialization failed.", this.stderr.sanitized() || undefined);
|
|
260
|
+
}
|
|
261
|
+
if (response.protocolVersion !== acp.PROTOCOL_VERSION) {
|
|
262
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP returned an unsupported protocol version.");
|
|
263
|
+
}
|
|
264
|
+
if (response.agentCapabilities?.loadSession !== true) {
|
|
265
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP must support session loading for isolated turn rotation.");
|
|
266
|
+
}
|
|
267
|
+
const authMethods = response.authMethods ?? [];
|
|
268
|
+
const cached = authMethods.find((method) => method.id === "cached_token");
|
|
269
|
+
if (!cached) {
|
|
270
|
+
throw new GrokRuntimeError("not_authenticated", "Sign in to Grok before starting a subscription session.");
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
await this.connection.agent.request(acp.methods.agent.authenticate, { methodId: "cached_token" });
|
|
274
|
+
await this.drainNotifications();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
if (this.crashError)
|
|
278
|
+
throw this.crashError;
|
|
279
|
+
throw new GrokRuntimeError("not_authenticated", "The cached Grok login is unavailable or expired.");
|
|
280
|
+
}
|
|
281
|
+
this.initialized = true;
|
|
282
|
+
}
|
|
283
|
+
async newSession() {
|
|
284
|
+
await this.initialize();
|
|
285
|
+
try {
|
|
286
|
+
const response = await this.connection.agent.request("session/new", {
|
|
287
|
+
cwd: this.options.workspace,
|
|
288
|
+
additionalDirectories: [],
|
|
289
|
+
mcpServers: [],
|
|
290
|
+
});
|
|
291
|
+
await this.drainNotifications();
|
|
292
|
+
return parseSessionResponse(response);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (this.crashError)
|
|
296
|
+
throw this.crashError;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async loadSession(sessionId) {
|
|
301
|
+
await this.initialize();
|
|
302
|
+
try {
|
|
303
|
+
const response = await this.connection.agent.request("session/load", {
|
|
304
|
+
sessionId,
|
|
305
|
+
cwd: this.options.workspace,
|
|
306
|
+
additionalDirectories: [],
|
|
307
|
+
mcpServers: [],
|
|
308
|
+
});
|
|
309
|
+
await this.drainNotifications();
|
|
310
|
+
return parseSessionResponse(response, sessionId);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (this.crashError)
|
|
314
|
+
throw this.crashError;
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async setModel(sessionId, modelId) {
|
|
319
|
+
await this.initialize();
|
|
320
|
+
try {
|
|
321
|
+
const response = await this.connection.agent.request("session/set_model", { sessionId, modelId });
|
|
322
|
+
await this.drainNotifications();
|
|
323
|
+
if (!isRecord(response))
|
|
324
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP returned an invalid model switch response.");
|
|
325
|
+
if (response.models === undefined)
|
|
326
|
+
return undefined;
|
|
327
|
+
const models = parseModelState(response.models);
|
|
328
|
+
if (!models)
|
|
329
|
+
throw new GrokRuntimeError("protocol_error", "Grok ACP returned an invalid model catalog.");
|
|
330
|
+
return models;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (this.crashError)
|
|
334
|
+
throw this.crashError;
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async prompt(sessionId, prompt, generation, signal) {
|
|
339
|
+
await this.initialize();
|
|
340
|
+
try {
|
|
341
|
+
const response = await this.connection.agent.request(acp.methods.agent.session.prompt, {
|
|
342
|
+
sessionId,
|
|
343
|
+
prompt: [{ type: "text", text: prompt }],
|
|
344
|
+
_meta: { bubbleGeneration: generation },
|
|
345
|
+
}, { cancellationSignal: signal });
|
|
346
|
+
// The SDK dispatches notification handlers independently from response
|
|
347
|
+
// settlement. Yield once so all notifications parsed before the prompt
|
|
348
|
+
// response can join our ordered tail, then drain it before rotating the
|
|
349
|
+
// sidecar at the turn boundary.
|
|
350
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
351
|
+
await this.notificationTail;
|
|
352
|
+
return response;
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
// Stream closure can reject the request one microtask before ChildProcess
|
|
356
|
+
// emits exit. Give the exit handler a chance to preserve the safe crash
|
|
357
|
+
// diagnostic instead of collapsing it into a generic protocol error.
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
359
|
+
if (this.crashError)
|
|
360
|
+
throw this.crashError;
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async cancel(sessionId) {
|
|
365
|
+
if (this.disposed)
|
|
366
|
+
return;
|
|
367
|
+
await this.connection.agent.notify(acp.methods.agent.session.cancel, { sessionId });
|
|
368
|
+
}
|
|
369
|
+
async dispose() {
|
|
370
|
+
if (this.disposed)
|
|
371
|
+
return;
|
|
372
|
+
this.disposed = true;
|
|
373
|
+
this.connection.close();
|
|
374
|
+
this.child.stdin?.end();
|
|
375
|
+
let stopped = await settlesWithin(this.exited, 250);
|
|
376
|
+
if (!stopped && this.child.exitCode === null && this.child.signalCode === null) {
|
|
377
|
+
this.child.kill("SIGTERM");
|
|
378
|
+
stopped = await settlesWithin(this.exited, 1_000);
|
|
379
|
+
}
|
|
380
|
+
if (!stopped && this.child.exitCode === null && this.child.signalCode === null) {
|
|
381
|
+
this.child.kill("SIGKILL");
|
|
382
|
+
await settlesWithin(this.exited, 1_000);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
export const createDefaultGrokAcpClient = (options) => new DefaultGrokAcpClient(options);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExternalRuntimeBinaryInfo } from "./types.js";
|
|
2
|
+
export declare const GROK_PINNED_VERSION = "0.2.93";
|
|
3
|
+
export declare const GROK_PINNED_SHA256 = "2a97ba675bd992aa9b981e2e83776460d94f469b510c0b8efe28b50d236d767c";
|
|
4
|
+
export interface GrokBinaryDependencies {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
arch?: string;
|
|
7
|
+
uid?: number;
|
|
8
|
+
binaryPath?: string;
|
|
9
|
+
pathEnv?: string;
|
|
10
|
+
userHome?: string;
|
|
11
|
+
expectedVersion?: string;
|
|
12
|
+
expectedSha256?: string;
|
|
13
|
+
hashFile?: (path: string) => Promise<string>;
|
|
14
|
+
readVersion?: (path: string) => Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
export declare function resolveGrokBinary(deps?: GrokBinaryDependencies): Promise<string>;
|
|
17
|
+
export declare function sha256File(path: string): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* The executable's directory chain is part of the trust boundary. Even a
|
|
20
|
+
* read-only, user-owned file can be swapped between verification and spawn if
|
|
21
|
+
* another local user can rename a writable ancestor directory entry.
|
|
22
|
+
*/
|
|
23
|
+
export declare function verifyGrokBinaryAncestors(path: string, uid?: number | undefined): Promise<void>;
|
|
24
|
+
export declare function parseGrokVersion(output: string): string | undefined;
|
|
25
|
+
export declare function verifyGrokBinary(deps?: GrokBinaryDependencies): Promise<ExternalRuntimeBinaryInfo>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
7
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
8
|
+
export const GROK_PINNED_VERSION = "0.2.93";
|
|
9
|
+
export const GROK_PINNED_SHA256 = "2a97ba675bd992aa9b981e2e83776460d94f469b510c0b8efe28b50d236d767c";
|
|
10
|
+
async function isExecutable(path) {
|
|
11
|
+
try {
|
|
12
|
+
await access(path, fsConstants.X_OK);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function resolveGrokBinary(deps = {}) {
|
|
20
|
+
const candidates = [];
|
|
21
|
+
if (deps.binaryPath)
|
|
22
|
+
candidates.push(deps.binaryPath);
|
|
23
|
+
if (!deps.binaryPath) {
|
|
24
|
+
const home = deps.userHome ?? homedir();
|
|
25
|
+
candidates.push(join(home, ".local", "bin", "grok"), join(home, ".grok", "bin", "grok"));
|
|
26
|
+
for (const entry of (deps.pathEnv ?? process.env.PATH ?? "").split(delimiter)) {
|
|
27
|
+
if (entry && isAbsolute(entry))
|
|
28
|
+
candidates.push(join(entry, "grok"));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
32
|
+
if (!isAbsolute(candidate) || !(await isExecutable(candidate)))
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
return await realpath(candidate);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Continue to the next independently verified candidate.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw new GrokRuntimeError("binary_not_found", "Install the pinned Grok Build CLI (0.2.93) to use Grok subscription login.");
|
|
42
|
+
}
|
|
43
|
+
export async function sha256File(path) {
|
|
44
|
+
return await new Promise((resolve, reject) => {
|
|
45
|
+
const hash = createHash("sha256");
|
|
46
|
+
const input = createReadStream(path);
|
|
47
|
+
input.on("error", reject);
|
|
48
|
+
input.on("data", (chunk) => hash.update(chunk));
|
|
49
|
+
input.on("end", () => resolve(hash.digest("hex")));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The executable's directory chain is part of the trust boundary. Even a
|
|
54
|
+
* read-only, user-owned file can be swapped between verification and spawn if
|
|
55
|
+
* another local user can rename a writable ancestor directory entry.
|
|
56
|
+
*/
|
|
57
|
+
export async function verifyGrokBinaryAncestors(path, uid = process.getuid?.()) {
|
|
58
|
+
if (uid === undefined) {
|
|
59
|
+
throw new GrokRuntimeError("binary_untrusted", "The Grok CLI path owner could not be verified.");
|
|
60
|
+
}
|
|
61
|
+
let current = dirname(path);
|
|
62
|
+
while (true) {
|
|
63
|
+
const metadata = await stat(current);
|
|
64
|
+
if (!metadata.isDirectory() || (metadata.uid !== uid && metadata.uid !== 0) || (metadata.mode & 0o022) !== 0) {
|
|
65
|
+
throw new GrokRuntimeError("binary_untrusted", "The Grok CLI must be located under directories writable only by the current user or root.");
|
|
66
|
+
}
|
|
67
|
+
const parent = dirname(current);
|
|
68
|
+
if (parent === current)
|
|
69
|
+
return;
|
|
70
|
+
current = parent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function parseGrokVersion(output) {
|
|
74
|
+
return /^grok\s+(\d+\.\d+\.\d+)(?:\s|$)/m.exec(output.trim())?.[1];
|
|
75
|
+
}
|
|
76
|
+
export async function verifyGrokBinary(deps = {}) {
|
|
77
|
+
const platform = deps.platform ?? process.platform;
|
|
78
|
+
const arch = deps.arch ?? process.arch;
|
|
79
|
+
if (platform !== "darwin" || arch !== "arm64") {
|
|
80
|
+
throw new GrokRuntimeError("unsupported_platform", "Grok Subscription is available on macOS Apple Silicon only.");
|
|
81
|
+
}
|
|
82
|
+
const path = await resolveGrokBinary(deps);
|
|
83
|
+
const metadata = await stat(path);
|
|
84
|
+
const uid = deps.uid ?? process.getuid?.();
|
|
85
|
+
if (!metadata.isFile() || uid === undefined || metadata.uid !== uid) {
|
|
86
|
+
throw new GrokRuntimeError("binary_untrusted", "The Grok CLI must be a regular file owned by the current user.");
|
|
87
|
+
}
|
|
88
|
+
if ((metadata.mode & 0o111) === 0 || (metadata.mode & 0o022) !== 0) {
|
|
89
|
+
throw new GrokRuntimeError("binary_untrusted", "The Grok CLI must be executable and not writable by group or other users.");
|
|
90
|
+
}
|
|
91
|
+
await verifyGrokBinaryAncestors(path, uid);
|
|
92
|
+
// Never execute an untrusted candidate merely to ask it for its version.
|
|
93
|
+
const sha256 = await (deps.hashFile ?? sha256File)(path);
|
|
94
|
+
const expectedSha256 = deps.expectedSha256 ?? GROK_PINNED_SHA256;
|
|
95
|
+
if (sha256.toLowerCase() !== expectedSha256.toLowerCase()) {
|
|
96
|
+
throw new GrokRuntimeError("binary_hash_mismatch", "The Grok CLI checksum does not match Bubble's pinned build.");
|
|
97
|
+
}
|
|
98
|
+
const expectedVersion = deps.expectedVersion ?? GROK_PINNED_VERSION;
|
|
99
|
+
const output = await deps.readVersion?.(path);
|
|
100
|
+
if (output === undefined) {
|
|
101
|
+
throw new GrokRuntimeError("protocol_error", "A version reader is required to verify the Grok CLI.");
|
|
102
|
+
}
|
|
103
|
+
const version = parseGrokVersion(output);
|
|
104
|
+
if (version !== expectedVersion) {
|
|
105
|
+
throw new GrokRuntimeError("binary_version_mismatch", `Grok CLI ${expectedVersion} is required.`);
|
|
106
|
+
}
|
|
107
|
+
return { path, version, sha256: sha256.toLowerCase() };
|
|
108
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type GrokRuntimeErrorCode = "unsupported_platform" | "binary_not_found" | "binary_untrusted" | "binary_version_mismatch" | "binary_hash_mismatch" | "profile_unsafe" | "profile_locked" | "preflight_failed" | "not_authenticated" | "protocol_error" | "policy_violation" | "process_crashed" | "cancelled" | "disposed";
|
|
2
|
+
export declare class GrokRuntimeError extends Error {
|
|
3
|
+
readonly code: GrokRuntimeErrorCode;
|
|
4
|
+
readonly diagnostic?: string;
|
|
5
|
+
constructor(code: GrokRuntimeErrorCode, message: string, diagnostic?: string);
|
|
6
|
+
}
|
|
7
|
+
export declare function safeErrorMessage(error: unknown): string;
|