@bubblebrain-ai/bubble 0.0.34 → 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/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -2
- package/dist/agent/profiles.js +25 -14
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +21 -0
- package/dist/agent/workflow/control.js +50 -8
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +82 -48
- package/dist/agent.js +360 -232
- 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 +5 -3
- package/dist/config.js +10 -5
- 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 +114 -28
- 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 +92 -52
- 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 +23 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +41 -8
- 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 +20 -0
- package/dist/prompt/task-reminders.js +73 -1
- 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 +35 -17
- 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.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +42 -272
- 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 +672 -243
- package/dist/tui-ink/approval/approval-dialog.js +37 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- 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 +36 -7
- package/dist/tui-ink/input-box.js +229 -157
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +464 -128
- 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/plan-confirm.js +1 -1
- 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 +34 -1
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +8 -31
- 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 +22 -0
- package/dist/variant/variant-resolver.js +43 -0
- package/package.json +6 -2
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
|
@@ -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;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class GrokRuntimeError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
diagnostic;
|
|
4
|
+
constructor(code, message, diagnostic) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "GrokRuntimeError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.diagnostic = diagnostic;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function safeErrorMessage(error) {
|
|
12
|
+
if (error instanceof GrokRuntimeError)
|
|
13
|
+
return error.message;
|
|
14
|
+
return "Grok runtime is unavailable.";
|
|
15
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Commands that remain local while a Grok subscription session is bound. */
|
|
2
|
+
export declare const GROK_LOCAL_COMMAND_HELP: readonly [{
|
|
3
|
+
readonly name: "help";
|
|
4
|
+
readonly usage: "/help";
|
|
5
|
+
readonly description: "Show commands";
|
|
6
|
+
}, {
|
|
7
|
+
readonly name: "model";
|
|
8
|
+
readonly usage: "/model";
|
|
9
|
+
readonly description: "Choose the Grok subscription model and reasoning effort";
|
|
10
|
+
}, {
|
|
11
|
+
readonly name: "provider";
|
|
12
|
+
readonly usage: "/provider";
|
|
13
|
+
readonly description: "Start a fresh native session and manage providers";
|
|
14
|
+
}, {
|
|
15
|
+
readonly name: "login";
|
|
16
|
+
readonly usage: "/login [openai|grok]";
|
|
17
|
+
readonly description: "Start a fresh session with that login";
|
|
18
|
+
}, {
|
|
19
|
+
readonly name: "logout";
|
|
20
|
+
readonly usage: "/logout grok";
|
|
21
|
+
readonly description: "Remove this device's local Grok login";
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "session";
|
|
24
|
+
readonly usage: "/session";
|
|
25
|
+
readonly description: "Browse sessions";
|
|
26
|
+
}, {
|
|
27
|
+
readonly name: "theme";
|
|
28
|
+
readonly usage: "/theme";
|
|
29
|
+
readonly description: "Pick the color theme";
|
|
30
|
+
}, {
|
|
31
|
+
readonly name: "feedback";
|
|
32
|
+
readonly usage: "/feedback";
|
|
33
|
+
readonly description: "Send product feedback";
|
|
34
|
+
}, {
|
|
35
|
+
readonly name: "quit";
|
|
36
|
+
readonly usage: "/quit";
|
|
37
|
+
readonly description: "Exit Bubble";
|
|
38
|
+
}, {
|
|
39
|
+
readonly name: "exit";
|
|
40
|
+
readonly usage: "/exit";
|
|
41
|
+
readonly description: "Exit Bubble";
|
|
42
|
+
}];
|
|
43
|
+
export declare const GROK_LOCAL_SLASH_COMMANDS: ReadonlySet<string>;
|
|
44
|
+
export type GrokInputDecision = {
|
|
45
|
+
kind: "prompt";
|
|
46
|
+
} | {
|
|
47
|
+
kind: "local_command";
|
|
48
|
+
command: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "blocked";
|
|
51
|
+
message: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function grokSlashCommandName(input: string): string | undefined;
|
|
54
|
+
export declare function isGrokLocalSlashCommand(command: string): boolean;
|
|
55
|
+
export declare function classifyGrokInput(input: {
|
|
56
|
+
text: string;
|
|
57
|
+
imageCount: number;
|
|
58
|
+
}): GrokInputDecision;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Commands that remain local while a Grok subscription session is bound. */
|
|
2
|
+
export const GROK_LOCAL_COMMAND_HELP = [
|
|
3
|
+
{ name: "help", usage: "/help", description: "Show commands" },
|
|
4
|
+
{ name: "model", usage: "/model", description: "Choose the Grok subscription model and reasoning effort" },
|
|
5
|
+
{ name: "provider", usage: "/provider", description: "Start a fresh native session and manage providers" },
|
|
6
|
+
{ name: "login", usage: "/login [openai|grok]", description: "Start a fresh session with that login" },
|
|
7
|
+
{ name: "logout", usage: "/logout grok", description: "Remove this device's local Grok login" },
|
|
8
|
+
{ name: "session", usage: "/session", description: "Browse sessions" },
|
|
9
|
+
{ name: "theme", usage: "/theme", description: "Pick the color theme" },
|
|
10
|
+
{ name: "feedback", usage: "/feedback", description: "Send product feedback" },
|
|
11
|
+
{ name: "quit", usage: "/quit", description: "Exit Bubble" },
|
|
12
|
+
{ name: "exit", usage: "/exit", description: "Exit Bubble" },
|
|
13
|
+
];
|
|
14
|
+
export const GROK_LOCAL_SLASH_COMMANDS = new Set(GROK_LOCAL_COMMAND_HELP.map((command) => command.name));
|
|
15
|
+
export function grokSlashCommandName(input) {
|
|
16
|
+
const match = /^\s*\/([^\s/]+)/.exec(input);
|
|
17
|
+
return match?.[1]?.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
export function isGrokLocalSlashCommand(command) {
|
|
20
|
+
return GROK_LOCAL_SLASH_COMMANDS.has(command.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
export function classifyGrokInput(input) {
|
|
23
|
+
if (input.imageCount > 0) {
|
|
24
|
+
return {
|
|
25
|
+
kind: "blocked",
|
|
26
|
+
message: "This Grok runtime does not advertise image or attachment input support.",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const command = grokSlashCommandName(input.text);
|
|
30
|
+
if (command) {
|
|
31
|
+
if (!isGrokLocalSlashCommand(command)) {
|
|
32
|
+
return {
|
|
33
|
+
kind: "blocked",
|
|
34
|
+
message: `/${command} is not available in this Grok workspace session.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (command === "logout") {
|
|
38
|
+
const argument = input.text.trim().split(/\s+/)[1]?.toLowerCase();
|
|
39
|
+
if (argument !== "grok") {
|
|
40
|
+
return {
|
|
41
|
+
kind: "blocked",
|
|
42
|
+
message: "Use /logout grok while Grok subscription chat is active.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { kind: "local_command", command };
|
|
47
|
+
}
|
|
48
|
+
if (input.text.trimStart().startsWith("/")) {
|
|
49
|
+
return { kind: "blocked", message: "Unknown slash command." };
|
|
50
|
+
}
|
|
51
|
+
return { kind: "prompt" };
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
3
|
+
export declare const GROK_ACP_MAX_LINE_BYTES: number;
|
|
4
|
+
export interface BoundedNdjsonOptions {
|
|
5
|
+
maxLineBytes?: number;
|
|
6
|
+
onProtocolError?: (error: GrokRuntimeError) => void;
|
|
7
|
+
/** Return true to consume a validated envelope before SDK async dispatch. */
|
|
8
|
+
interceptIncoming?: (message: AnyMessage) => boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A credential-safe NDJSON transport. Unlike the SDK convenience transport,
|
|
12
|
+
* this implementation bounds each line and never prints rejected input.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createBoundedGrokNdjsonStream(output: WritableStream<Uint8Array>, input: ReadableStream<Uint8Array>, options?: BoundedNdjsonOptions): Stream;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
2
|
+
export const GROK_ACP_MAX_LINE_BYTES = 1024 * 1024;
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function isJsonRpcId(value) {
|
|
7
|
+
return value === null || typeof value === "string" || (typeof value === "number" && Number.isFinite(value));
|
|
8
|
+
}
|
|
9
|
+
function isJsonRpcMessage(value) {
|
|
10
|
+
if (!isRecord(value) || value.jsonrpc !== "2.0")
|
|
11
|
+
return false;
|
|
12
|
+
if (typeof value.method === "string" && value.method.length > 0) {
|
|
13
|
+
return !("id" in value) || isJsonRpcId(value.id);
|
|
14
|
+
}
|
|
15
|
+
if (!("id" in value) || !isJsonRpcId(value.id))
|
|
16
|
+
return false;
|
|
17
|
+
const hasResult = Object.prototype.hasOwnProperty.call(value, "result");
|
|
18
|
+
const hasError = Object.prototype.hasOwnProperty.call(value, "error");
|
|
19
|
+
if (hasResult === hasError)
|
|
20
|
+
return false;
|
|
21
|
+
if (hasError) {
|
|
22
|
+
return isRecord(value.error)
|
|
23
|
+
&& typeof value.error.code === "number"
|
|
24
|
+
&& typeof value.error.message === "string";
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function malformedMessage() {
|
|
29
|
+
return new GrokRuntimeError("protocol_error", "Grok ACP sent a malformed JSON-RPC message.");
|
|
30
|
+
}
|
|
31
|
+
function oversizedMessage() {
|
|
32
|
+
return new GrokRuntimeError("protocol_error", "Grok ACP message exceeded the safe line limit.");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A credential-safe NDJSON transport. Unlike the SDK convenience transport,
|
|
36
|
+
* this implementation bounds each line and never prints rejected input.
|
|
37
|
+
*/
|
|
38
|
+
export function createBoundedGrokNdjsonStream(output, input, options = {}) {
|
|
39
|
+
const maxLineBytes = options.maxLineBytes ?? GROK_ACP_MAX_LINE_BYTES;
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
42
|
+
let inputReader;
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
let reported = false;
|
|
45
|
+
const pendingRequestIds = new Set();
|
|
46
|
+
const requestKey = (id) => `${typeof id}:${String(id)}`;
|
|
47
|
+
const report = (error) => {
|
|
48
|
+
if (reported)
|
|
49
|
+
return;
|
|
50
|
+
reported = true;
|
|
51
|
+
options.onProtocolError?.(error);
|
|
52
|
+
};
|
|
53
|
+
const readable = new ReadableStream({
|
|
54
|
+
async start(controller) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
let lineBytes = 0;
|
|
57
|
+
const append = (segment) => {
|
|
58
|
+
if (lineBytes + segment.byteLength > maxLineBytes)
|
|
59
|
+
throw oversizedMessage();
|
|
60
|
+
if (segment.byteLength > 0)
|
|
61
|
+
parts.push(new Uint8Array(segment));
|
|
62
|
+
lineBytes += segment.byteLength;
|
|
63
|
+
};
|
|
64
|
+
const consumeLine = () => {
|
|
65
|
+
if (lineBytes === 0)
|
|
66
|
+
return;
|
|
67
|
+
const bytes = new Uint8Array(lineBytes);
|
|
68
|
+
let offset = 0;
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
bytes.set(part, offset);
|
|
71
|
+
offset += part.byteLength;
|
|
72
|
+
}
|
|
73
|
+
parts.length = 0;
|
|
74
|
+
lineBytes = 0;
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = decoder.decode(bytes).trim();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw malformedMessage();
|
|
81
|
+
}
|
|
82
|
+
if (!text)
|
|
83
|
+
return;
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw malformedMessage();
|
|
90
|
+
}
|
|
91
|
+
if (!isJsonRpcMessage(parsed))
|
|
92
|
+
throw malformedMessage();
|
|
93
|
+
if (!("method" in parsed)) {
|
|
94
|
+
// The pinned CLI emits a small number of internal responses (for
|
|
95
|
+
// example `skills-reload`) that were never requested by Bubble. The
|
|
96
|
+
// SDK logs unknown response IDs verbatim, so consume them here unless
|
|
97
|
+
// the ID belongs to an outgoing Bubble request.
|
|
98
|
+
if (!pendingRequestIds.delete(requestKey(parsed.id)))
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (options.interceptIncoming?.(parsed))
|
|
102
|
+
return;
|
|
103
|
+
controller.enqueue(parsed);
|
|
104
|
+
};
|
|
105
|
+
const reader = input.getReader();
|
|
106
|
+
inputReader = reader;
|
|
107
|
+
try {
|
|
108
|
+
while (!cancelled) {
|
|
109
|
+
const { value, done } = await reader.read();
|
|
110
|
+
if (done)
|
|
111
|
+
break;
|
|
112
|
+
if (!value)
|
|
113
|
+
continue;
|
|
114
|
+
let start = 0;
|
|
115
|
+
for (let index = 0; index < value.byteLength; index += 1) {
|
|
116
|
+
if (value[index] !== 0x0a)
|
|
117
|
+
continue;
|
|
118
|
+
append(value.subarray(start, index));
|
|
119
|
+
consumeLine();
|
|
120
|
+
start = index + 1;
|
|
121
|
+
}
|
|
122
|
+
if (start < value.byteLength)
|
|
123
|
+
append(value.subarray(start));
|
|
124
|
+
}
|
|
125
|
+
if (!cancelled && lineBytes > 0)
|
|
126
|
+
consumeLine();
|
|
127
|
+
if (!cancelled)
|
|
128
|
+
controller.close();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!cancelled) {
|
|
132
|
+
const safe = error instanceof GrokRuntimeError ? error : malformedMessage();
|
|
133
|
+
report(safe);
|
|
134
|
+
await reader.cancel(safe).catch(() => undefined);
|
|
135
|
+
// The typed failure is delivered through onProtocolError. Close the
|
|
136
|
+
// SDK-facing stream cleanly so its router never logs the rejected raw
|
|
137
|
+
// JSON-RPC line as part of an exception path.
|
|
138
|
+
try {
|
|
139
|
+
controller.close();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// The connection may have been concurrently closed by the
|
|
143
|
+
// protocol-error callback. Either state is safely terminal.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (inputReader === reader)
|
|
149
|
+
inputReader = undefined;
|
|
150
|
+
reader.releaseLock();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
async cancel(reason) {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
await inputReader?.cancel(reason);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const writable = new WritableStream({
|
|
159
|
+
async write(message) {
|
|
160
|
+
let bytes;
|
|
161
|
+
try {
|
|
162
|
+
bytes = encoder.encode(`${JSON.stringify(message)}\n`);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
const error = malformedMessage();
|
|
166
|
+
report(error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
if (bytes.byteLength > maxLineBytes) {
|
|
170
|
+
const error = oversizedMessage();
|
|
171
|
+
report(error);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if ("method" in message && "id" in message) {
|
|
175
|
+
pendingRequestIds.add(requestKey(message.id));
|
|
176
|
+
}
|
|
177
|
+
const writer = output.getWriter();
|
|
178
|
+
try {
|
|
179
|
+
await writer.write(bytes);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
writer.releaseLock();
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
async abort(reason) {
|
|
186
|
+
await output.abort(reason);
|
|
187
|
+
},
|
|
188
|
+
async close() {
|
|
189
|
+
await output.close();
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return { readable, writable };
|
|
193
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type GrokNetworkRouteSource = "direct" | "clash-verge" | "clashx";
|
|
2
|
+
export interface GrokNetworkRoute {
|
|
3
|
+
source: GrokNetworkRouteSource;
|
|
4
|
+
proxy?: string;
|
|
5
|
+
}
|
|
6
|
+
export type GrokNetworkProbe = (proxy?: string) => Promise<boolean>;
|
|
7
|
+
export interface GrokNetworkResolverDependencies {
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
userHome?: string;
|
|
10
|
+
uid?: number;
|
|
11
|
+
probe?: GrokNetworkProbe;
|
|
12
|
+
}
|
|
13
|
+
export declare function probeXaiOidc(proxy?: string, timeoutMs?: number): Promise<boolean>;
|
|
14
|
+
export declare function parseClashHttpProxyPort(contents: string): number | undefined;
|
|
15
|
+
export declare function resolveGrokNetworkRoute(deps?: GrokNetworkResolverDependencies): Promise<GrokNetworkRoute>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
6
|
+
const XAI_OIDC_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
7
|
+
const MAX_PROXY_CONFIG_BYTES = 64 * 1024;
|
|
8
|
+
export async function probeXaiOidc(proxy, timeoutMs = 5_000) {
|
|
9
|
+
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1_000)).toString();
|
|
10
|
+
const args = [
|
|
11
|
+
"--fail",
|
|
12
|
+
"--silent",
|
|
13
|
+
"--show-error",
|
|
14
|
+
"--output", "/dev/null",
|
|
15
|
+
"--connect-timeout", timeoutSeconds,
|
|
16
|
+
"--max-time", timeoutSeconds,
|
|
17
|
+
"--proto", "=https",
|
|
18
|
+
];
|
|
19
|
+
if (proxy)
|
|
20
|
+
args.push("--proxy", proxy);
|
|
21
|
+
else
|
|
22
|
+
args.push("--noproxy", "*");
|
|
23
|
+
args.push(XAI_OIDC_DISCOVERY_URL);
|
|
24
|
+
return await new Promise((resolve) => {
|
|
25
|
+
execFile("/usr/bin/curl", args, { timeout: timeoutMs + 1_000, maxBuffer: 4 * 1024 }, (error) => resolve(error === null));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function parseClashHttpProxyPort(contents) {
|
|
29
|
+
for (const key of ["mixed-port", "port"]) {
|
|
30
|
+
const match = new RegExp(`^${key}:\\s*([0-9]{1,5})\\s*$`, "m").exec(contents);
|
|
31
|
+
const port = match ? Number(match[1]) : 0;
|
|
32
|
+
if (port >= 1 && port <= 65_535)
|
|
33
|
+
return port;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
async function readOwnedProxyPort(path, uid) {
|
|
38
|
+
try {
|
|
39
|
+
const metadata = await lstat(path);
|
|
40
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid
|
|
41
|
+
|| (metadata.mode & 0o022) !== 0 || metadata.size > MAX_PROXY_CONFIG_BYTES)
|
|
42
|
+
return undefined;
|
|
43
|
+
return parseClashHttpProxyPort(await readFile(path, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function discoverConfiguredLoopbackProxies(deps) {
|
|
50
|
+
if ((deps.platform ?? process.platform) !== "darwin")
|
|
51
|
+
return [];
|
|
52
|
+
const uid = deps.uid ?? process.getuid?.();
|
|
53
|
+
if (uid === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
const home = deps.userHome ?? homedir();
|
|
56
|
+
const configs = [
|
|
57
|
+
{ source: "clash-verge", path: join(home, "Library", "Application Support", "io.github.clash-verge-rev.clash-verge-rev", "config.yaml") },
|
|
58
|
+
{ source: "clashx", path: join(home, "Library", "Application Support", "com.west2online.ClashX", "config.yaml") },
|
|
59
|
+
];
|
|
60
|
+
const candidates = [];
|
|
61
|
+
for (const config of configs) {
|
|
62
|
+
const port = await readOwnedProxyPort(config.path, uid);
|
|
63
|
+
if (port)
|
|
64
|
+
candidates.push({ source: config.source, proxy: `http://127.0.0.1:${port}` });
|
|
65
|
+
}
|
|
66
|
+
return candidates;
|
|
67
|
+
}
|
|
68
|
+
export async function resolveGrokNetworkRoute(deps = {}) {
|
|
69
|
+
const probe = deps.probe ?? probeXaiOidc;
|
|
70
|
+
if (await probe())
|
|
71
|
+
return { source: "direct" };
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const candidate of await discoverConfiguredLoopbackProxies(deps)) {
|
|
74
|
+
if (seen.has(candidate.proxy))
|
|
75
|
+
continue;
|
|
76
|
+
seen.add(candidate.proxy);
|
|
77
|
+
if (await probe(candidate.proxy))
|
|
78
|
+
return candidate;
|
|
79
|
+
}
|
|
80
|
+
throw new GrokRuntimeError("not_authenticated", "Bubble could not reach xAI through this Mac's current network configuration.");
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GrokProfile } from "./grok-profile.js";
|
|
2
|
+
import type { GrokSpawn } from "./grok-process.js";
|
|
3
|
+
/**
|
|
4
|
+
* How a workspace-scoped preflight may relax the empty-surface rule. The
|
|
5
|
+
* pinned Grok CLI always *lists* project-level extensions in `inspect`, even
|
|
6
|
+
* when their loading is disabled, so a workspace containing its own
|
|
7
|
+
* `.grok/skills` or `.agents/skills` directories can never produce an empty
|
|
8
|
+
* report. Loading is prevented instead: every discovered project skill must be
|
|
9
|
+
* written into the profile's `[skills] disabled` list before the sidecar
|
|
10
|
+
* starts, and MCP entries are acceptable only when they come from the
|
|
11
|
+
* cursor/claude compatibility scanners that the profile verifiably disables
|
|
12
|
+
* (agent-stdio sessions additionally use only the MCP servers passed by
|
|
13
|
+
* Bubble, which passes none).
|
|
14
|
+
*/
|
|
15
|
+
export interface GrokWorkspaceExtensionPolicy {
|
|
16
|
+
allowedProjectSkills: ReadonlySet<string>;
|
|
17
|
+
allowCompatMcpServers: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Collect the names of every skill Grok discovered for this workspace so they
|
|
21
|
+
* can be written into the profile's `[skills] disabled` list. Fails closed on
|
|
22
|
+
* any skill that is not a plain project-level discovery inside the workspace
|
|
23
|
+
* scope, or whose name cannot be disabled safely.
|
|
24
|
+
*/
|
|
25
|
+
export declare function collectGrokProjectSkills(payload: unknown, workspace: string): Promise<string[]>;
|
|
26
|
+
export declare function validateGrokInspect(payload: unknown, profile: GrokProfile, workspace?: string, policy?: GrokWorkspaceExtensionPolicy): Promise<void>;
|
|
27
|
+
export declare function runGrokInspectJson(spawn: GrokSpawn, binary: string, profile: GrokProfile, env: NodeJS.ProcessEnv, workspace?: string, signal?: AbortSignal): Promise<unknown>;
|
|
28
|
+
export declare function runGrokPreflight(spawn: GrokSpawn, binary: string, profile: GrokProfile, env: NodeJS.ProcessEnv, workspace?: string, signal?: AbortSignal, policy?: GrokWorkspaceExtensionPolicy): Promise<void>;
|