codex-grok-bridge 1.0.4 → 1.5.0
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/CHANGELOG.md +45 -0
- package/README.md +73 -50
- package/package.json +6 -2
- package/scripts/codex-grok.mjs +2 -1
- package/scripts/codex-wrapper.mjs +2 -1
- package/scripts/install-codex-grok-app.ps1 +67 -0
- package/scripts/install-codex-grok-app.sh +95 -15
- package/scripts/launch-desktop.mjs +97 -22
- package/src/bridge.mjs +4 -30
- package/src/cli-inference.mjs +4 -2
- package/src/models.mjs +121 -0
- package/src/paths.mjs +163 -0
- package/src/proxy.mjs +2 -2
- package/src/router.mjs +10 -28
- package/src/runtime.mjs +3 -2
- package/src/tools.mjs +145 -22
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
desktopLaunchArgs,
|
|
7
|
+
desktopLaunchEnv,
|
|
8
|
+
desktopUserDataDir,
|
|
9
|
+
isGrokDesktopProcess,
|
|
10
|
+
resolveDesktopApp,
|
|
11
|
+
resolvePlatform,
|
|
12
|
+
} from "../src/paths.mjs";
|
|
6
13
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
14
|
+
const platform = resolvePlatform();
|
|
15
|
+
const userData = desktopUserDataDir();
|
|
16
|
+
const logFile = path.join(userData, "..", "launch.log");
|
|
9
17
|
mkdirSync(userData, { recursive: true, mode: 0o700 });
|
|
10
18
|
mkdirSync(path.dirname(logFile), { recursive: true, mode: 0o700 });
|
|
11
19
|
|
|
@@ -13,17 +21,33 @@ function log(message) {
|
|
|
13
21
|
appendFileSync(logFile, `${new Date().toISOString()} ${message}\n`);
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
function grokPidWin32() {
|
|
25
|
+
const systemRoot = process.env.SystemRoot || "C:\\Windows";
|
|
26
|
+
const powershell = `${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
27
|
+
try {
|
|
28
|
+
const out = execFileSync(
|
|
29
|
+
powershell,
|
|
30
|
+
[
|
|
31
|
+
"-NoProfile",
|
|
32
|
+
"-Command",
|
|
33
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like ${JSON.stringify(`*--user-data-dir=${userData}*`)} } | Select-Object -First 1 -ExpandProperty ProcessId`,
|
|
34
|
+
],
|
|
35
|
+
{ encoding: "utf8", windowsHide: true, timeout: 15000 },
|
|
36
|
+
);
|
|
37
|
+
const pid = Number(String(out).trim());
|
|
38
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
function grokPid() {
|
|
45
|
+
if (platform === "win32") return grokPidWin32();
|
|
17
46
|
const out = execFileSync("/bin/ps", ["-axo", "pid=,command="], {
|
|
18
47
|
encoding: "utf8",
|
|
19
48
|
});
|
|
20
49
|
for (const line of out.split("\n")) {
|
|
21
|
-
if (
|
|
22
|
-
line.includes(`--user-data-dir=${userData}`) &&
|
|
23
|
-
/MacOS\/(ChatGPT|Codex)(\s|$)/.test(line) &&
|
|
24
|
-
!line.includes("Helper") &&
|
|
25
|
-
!line.includes("crashpad")
|
|
26
|
-
)
|
|
50
|
+
if (isGrokDesktopProcess(line, userData))
|
|
27
51
|
return Number(line.trim().split(/\s+/)[0]);
|
|
28
52
|
}
|
|
29
53
|
return null;
|
|
@@ -41,6 +65,7 @@ function activate(pid) {
|
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
function notify(text) {
|
|
68
|
+
if (platform !== "darwin") return;
|
|
44
69
|
try {
|
|
45
70
|
execFileSync("/usr/bin/osascript", [
|
|
46
71
|
"-e",
|
|
@@ -49,19 +74,48 @@ function notify(text) {
|
|
|
49
74
|
} catch {}
|
|
50
75
|
}
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
77
|
+
function startWin32(wrapper) {
|
|
78
|
+
const app = resolveDesktopApp();
|
|
79
|
+
log(`start win32 app=${app} wrapper=${wrapper}`);
|
|
80
|
+
const child = spawn(app, desktopLaunchArgs(userData), {
|
|
81
|
+
stdio: "inherit",
|
|
82
|
+
env: {
|
|
83
|
+
...process.env,
|
|
84
|
+
...desktopLaunchEnv({ wrapper, userData }),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
child.on("error", (error) => {
|
|
88
|
+
log(`spawn failed: ${error.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
91
|
+
child.on("exit", (code) => {
|
|
92
|
+
log(`chatgpt exit=${code}`);
|
|
93
|
+
process.exit(code ?? 1);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function startLinux(wrapper) {
|
|
98
|
+
const app = resolveDesktopApp();
|
|
99
|
+
const pathPrefix = `${path.dirname(process.execPath)}:/usr/local/bin:/usr/bin:/bin`;
|
|
100
|
+
log(`start linux app=${app} wrapper=${wrapper}`);
|
|
101
|
+
const child = spawn(app, desktopLaunchArgs(userData), {
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
...desktopLaunchEnv({ wrapper, userData, pathPrefix }),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
child.on("error", (error) => {
|
|
109
|
+
log(`spawn failed: ${error.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
112
|
+
child.on("exit", (code) => {
|
|
113
|
+
log(`chatgpt exit=${code}`);
|
|
114
|
+
process.exit(code ?? 1);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function startDarwin(wrapper) {
|
|
65
119
|
log(`start wrapper=${wrapper}`);
|
|
66
120
|
const child = spawn(
|
|
67
121
|
"/usr/bin/open",
|
|
@@ -95,6 +149,27 @@ try {
|
|
|
95
149
|
}
|
|
96
150
|
process.exit(code ?? 1);
|
|
97
151
|
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const wrapper = fileURLToPath(new URL("./codex-wrapper.mjs", import.meta.url));
|
|
155
|
+
try {
|
|
156
|
+
const running = grokPid();
|
|
157
|
+
if (running) {
|
|
158
|
+
log(`activate pid=${running}`);
|
|
159
|
+
if (platform !== "darwin") {
|
|
160
|
+
process.exit(0);
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
activate(running);
|
|
164
|
+
notify("Codex Grok 창을 앞으로 가져왔습니다.");
|
|
165
|
+
process.exit(0);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
log(`activate failed: ${error.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (platform === "linux") startLinux(wrapper);
|
|
171
|
+
else if (platform === "win32") startWin32(wrapper);
|
|
172
|
+
else startDarwin(wrapper);
|
|
98
173
|
} catch (error) {
|
|
99
174
|
log(`fatal: ${error.message}`);
|
|
100
175
|
notify(error.message);
|
package/src/bridge.mjs
CHANGED
|
@@ -13,35 +13,9 @@ import { toProxyRequest } from "./tools.mjs";
|
|
|
13
13
|
import { classifyBridgeError, errorSignature, bridgeErrorMessage } from "./errors.mjs";
|
|
14
14
|
import { createDiagnostics } from "./diagnostics.mjs";
|
|
15
15
|
import { createSlots } from "./slots.mjs";
|
|
16
|
+
import { catalogModelInfos, isGrokModel, MODEL_INFO } from "./models.mjs";
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
-
slug: "grok-4.6",
|
|
19
|
-
display_name: "Grok 4.6 / xAI",
|
|
20
|
-
description: "Grok 4.6 via grok login; Codex executes tools",
|
|
21
|
-
default_reasoning_level: "high",
|
|
22
|
-
supported_reasoning_levels: ["low", "medium", "high", "xhigh"].map(
|
|
23
|
-
(effort) => ({ effort, description: effort }),
|
|
24
|
-
),
|
|
25
|
-
shell_type: "unified_exec",
|
|
26
|
-
visibility: "list",
|
|
27
|
-
supported_in_api: true,
|
|
28
|
-
priority: 50,
|
|
29
|
-
availability_nux: null,
|
|
30
|
-
upgrade: null,
|
|
31
|
-
support_verbosity: false,
|
|
32
|
-
default_verbosity: null,
|
|
33
|
-
apply_patch_tool_type: "freeform",
|
|
34
|
-
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
35
|
-
experimental_supported_tools: [],
|
|
36
|
-
context_window: 500000,
|
|
37
|
-
input_modalities: ["text", "image"],
|
|
38
|
-
tool_mode: "direct",
|
|
39
|
-
node_repl_disabled: true,
|
|
40
|
-
model_messages: {
|
|
41
|
-
instructions_template:
|
|
42
|
-
"You are Grok 4.6 by xAI, running as the Codex model. Use the tools provided by Codex and respect its permissions. Complete the user request accurately.",
|
|
43
|
-
},
|
|
44
|
-
};
|
|
18
|
+
export { MODEL_INFO };
|
|
45
19
|
|
|
46
20
|
export function publicBridgeError(error, context = {}) {
|
|
47
21
|
if (error instanceof GrokAuthError) return error.message;
|
|
@@ -69,7 +43,7 @@ export function createBridgeServer(options = {}) {
|
|
|
69
43
|
};
|
|
70
44
|
const route = new URL(req.url, "http://127.0.0.1").pathname;
|
|
71
45
|
if (req.method === "GET" && route === "/v1/models")
|
|
72
|
-
return json(200, { models:
|
|
46
|
+
return json(200, { models: catalogModelInfos() });
|
|
73
47
|
if (req.method !== "POST" || route !== "/v1/responses")
|
|
74
48
|
return json(404, { error: "Not found" });
|
|
75
49
|
const auth = Buffer.from(req.headers.authorization ?? "");
|
|
@@ -95,7 +69,7 @@ export function createBridgeServer(options = {}) {
|
|
|
95
69
|
}
|
|
96
70
|
requestBytes = size;
|
|
97
71
|
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
98
|
-
if (body?.model
|
|
72
|
+
if (!isGrokModel(body?.model) || !Array.isArray(body.input))
|
|
99
73
|
throw new Error();
|
|
100
74
|
} catch {
|
|
101
75
|
return json(400, { error: "Invalid request" });
|
package/src/cli-inference.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
|
5
5
|
import { tmpdir, homedir } from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { toImageBlocks } from "./images.mjs";
|
|
8
|
+
import { resolveGrokBinary } from "./paths.mjs";
|
|
9
|
+
import { resolveGrokModel } from "./models.mjs";
|
|
8
10
|
|
|
9
11
|
// The GROK_BRIDGE_INFERENCE=cli fallback. It drives the Grok CLI once per turn
|
|
10
12
|
// with the whole Codex request as a prompt and parses a JSON envelope back out.
|
|
@@ -59,7 +61,7 @@ export function buildGrokInvocation(body, options = {}) {
|
|
|
59
61
|
"--single",
|
|
60
62
|
extractLatestUserPrompt(body.input),
|
|
61
63
|
"--model",
|
|
62
|
-
|
|
64
|
+
resolveGrokModel(body.model),
|
|
63
65
|
"--output-format",
|
|
64
66
|
"json",
|
|
65
67
|
"--json-schema",
|
|
@@ -117,7 +119,7 @@ export function buildGrokInvocation(body, options = {}) {
|
|
|
117
119
|
"You are a model backend for Codex. Return exactly the JSON envelope requested in the input. Do not execute your own tools. Tool calls in the envelope are executed by Codex.",
|
|
118
120
|
];
|
|
119
121
|
return {
|
|
120
|
-
binary: options.grokBinary ??
|
|
122
|
+
binary: options.grokBinary ?? resolveGrokBinary(homedir()),
|
|
121
123
|
args,
|
|
122
124
|
// Run the CLI where Codex is working, not where the bridge happens to live.
|
|
123
125
|
cwd:
|
package/src/models.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const DEFAULT_GROK_MODEL = "grok-4.6";
|
|
2
|
+
export const GROK_PROVIDER = "grok_build_cli";
|
|
3
|
+
|
|
4
|
+
export function isGrokModel(model) {
|
|
5
|
+
return typeof model === "string" && model.startsWith("grok-");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveGrokModel(model) {
|
|
9
|
+
return isGrokModel(model) ? model : DEFAULT_GROK_MODEL;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function grokDisplayName(id) {
|
|
13
|
+
return `Grok ${String(id).slice("grok-".length)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function extraGrokModels(env = process.env) {
|
|
17
|
+
const raw = env.GROK_BRIDGE_MODELS ?? "";
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const ids = [];
|
|
20
|
+
for (const part of raw.split(/[,\s]+/)) {
|
|
21
|
+
const id = part.trim();
|
|
22
|
+
if (!isGrokModel(id) || seen.has(id)) continue;
|
|
23
|
+
seen.add(id);
|
|
24
|
+
ids.push(id);
|
|
25
|
+
}
|
|
26
|
+
return ids;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function catalogModelIds(env = process.env) {
|
|
30
|
+
const ids = [DEFAULT_GROK_MODEL];
|
|
31
|
+
const seen = new Set(ids);
|
|
32
|
+
for (const id of extraGrokModels(env)) {
|
|
33
|
+
if (seen.has(id)) continue;
|
|
34
|
+
seen.add(id);
|
|
35
|
+
ids.push(id);
|
|
36
|
+
}
|
|
37
|
+
return ids;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseGrokCliModels(text) {
|
|
41
|
+
const ids = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
44
|
+
const match = line.match(/^\s*[*+-]\s+(grok-[^\s(]+)/);
|
|
45
|
+
if (!match || seen.has(match[1])) continue;
|
|
46
|
+
seen.add(match[1]);
|
|
47
|
+
ids.push(match[1]);
|
|
48
|
+
}
|
|
49
|
+
return ids;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function modelEntry(id = DEFAULT_GROK_MODEL) {
|
|
53
|
+
const label = grokDisplayName(id);
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
model: id,
|
|
57
|
+
displayName: `${label} / xAI`,
|
|
58
|
+
description: `${label} · Codex tools`,
|
|
59
|
+
hidden: false,
|
|
60
|
+
isDefault: false,
|
|
61
|
+
upgrade: null,
|
|
62
|
+
upgradeInfo: null,
|
|
63
|
+
availabilityNux: null,
|
|
64
|
+
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"].map(
|
|
65
|
+
(reasoningEffort) => ({ reasoningEffort, description: reasoningEffort }),
|
|
66
|
+
),
|
|
67
|
+
defaultReasoningEffort: "high",
|
|
68
|
+
inputModalities: ["text", "image"],
|
|
69
|
+
supportsPersonality: false,
|
|
70
|
+
multiAgentVersion: null,
|
|
71
|
+
additionalSpeedTiers: [],
|
|
72
|
+
serviceTiers: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function modelInfo(id = DEFAULT_GROK_MODEL) {
|
|
77
|
+
const label = grokDisplayName(id);
|
|
78
|
+
return {
|
|
79
|
+
slug: id,
|
|
80
|
+
display_name: `${label} / xAI`,
|
|
81
|
+
description: `${label} via grok login; Codex executes tools`,
|
|
82
|
+
default_reasoning_level: "high",
|
|
83
|
+
supported_reasoning_levels: ["low", "medium", "high", "xhigh"].map(
|
|
84
|
+
(effort) => ({ effort, description: effort }),
|
|
85
|
+
),
|
|
86
|
+
shell_type: "unified_exec",
|
|
87
|
+
visibility: "list",
|
|
88
|
+
supported_in_api: true,
|
|
89
|
+
priority: 50,
|
|
90
|
+
availability_nux: null,
|
|
91
|
+
upgrade: null,
|
|
92
|
+
support_verbosity: false,
|
|
93
|
+
default_verbosity: null,
|
|
94
|
+
apply_patch_tool_type: "freeform",
|
|
95
|
+
truncation_policy: { mode: "tokens", limit: 10000 },
|
|
96
|
+
experimental_supported_tools: [],
|
|
97
|
+
context_window: 500000,
|
|
98
|
+
input_modalities: ["text", "image"],
|
|
99
|
+
tool_mode: "direct",
|
|
100
|
+
node_repl_disabled: true,
|
|
101
|
+
model_messages: {
|
|
102
|
+
instructions_template:
|
|
103
|
+
`You are ${label} by xAI, running as the Codex model. Use the tools provided by Codex and respect its permissions. Complete the user request accurately.`,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const MODEL_ENTRY = modelEntry();
|
|
109
|
+
export const MODEL_INFO = modelInfo();
|
|
110
|
+
|
|
111
|
+
export function catalogModelEntries(env = process.env) {
|
|
112
|
+
return catalogModelIds(env).map((id) =>
|
|
113
|
+
id === DEFAULT_GROK_MODEL ? MODEL_ENTRY : modelEntry(id),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function catalogModelInfos(env = process.env) {
|
|
118
|
+
return catalogModelIds(env).map((id) =>
|
|
119
|
+
id === DEFAULT_GROK_MODEL ? MODEL_INFO : modelInfo(id),
|
|
120
|
+
);
|
|
121
|
+
}
|
package/src/paths.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export const LINUX_CODEX_BINARY = "/usr/lib/chatgpt/resources/codex";
|
|
6
|
+
export const LINUX_CHATGPT_BIN = "/usr/lib/chatgpt/ChatGPT";
|
|
7
|
+
export const DARWIN_CODEX_BINARY = "/Applications/Codex.app/Contents/Resources/codex";
|
|
8
|
+
export const DARWIN_CODEX_APP = "/Applications/Codex.app";
|
|
9
|
+
export const LINUX_STOCK_PREFIX = "/usr/lib/chatgpt";
|
|
10
|
+
export const WIN32_WINDOWSAPPS = "WindowsApps";
|
|
11
|
+
export const WIN32_BRIDGE_APP = "codex-grok-bridge";
|
|
12
|
+
|
|
13
|
+
export function resolvePlatform(options = {}) {
|
|
14
|
+
const env = options.env ?? process.env;
|
|
15
|
+
return options.platform ?? env.CODEX_GROK_PLATFORM ?? process.platform;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function localAppData(home = homedir(), options = {}) {
|
|
19
|
+
const env = options.env ?? process.env;
|
|
20
|
+
if (env.LOCALAPPDATA) return env.LOCALAPPDATA;
|
|
21
|
+
return path.join(home, "AppData", "Local");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function win32AppDir(home = homedir(), options = {}) {
|
|
25
|
+
return path.join(localAppData(home, options), WIN32_BRIDGE_APP, "app");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function win32StorePointer(home = homedir(), options = {}, name = "store-app.txt") {
|
|
29
|
+
return path.join(localAppData(home, options), WIN32_BRIDGE_APP, name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readPointer(file, options = {}) {
|
|
33
|
+
const exists = options.existsSync ?? existsSync;
|
|
34
|
+
const read = options.readFileSync ?? ((p) => readFileSync(p, "utf8"));
|
|
35
|
+
if (!exists(file)) return "";
|
|
36
|
+
return String(read(file)).trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveWin32Binary(fileName, pointerName, options = {}) {
|
|
40
|
+
const env = options.env ?? process.env;
|
|
41
|
+
const home = options.home ?? homedir();
|
|
42
|
+
const exists = options.existsSync ?? existsSync;
|
|
43
|
+
const isolated = path.join(win32AppDir(home, { env }), fileName);
|
|
44
|
+
if (exists(isolated)) return isolated;
|
|
45
|
+
const pointed = readPointer(win32StorePointer(home, { env }, pointerName), options);
|
|
46
|
+
return pointed || isolated;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveCodexBinary(options = {}) {
|
|
50
|
+
const env = options.env ?? process.env;
|
|
51
|
+
if (env.CODEX_BINARY) return env.CODEX_BINARY;
|
|
52
|
+
const platform = resolvePlatform({ ...options, env });
|
|
53
|
+
if (platform === "linux") return LINUX_CODEX_BINARY;
|
|
54
|
+
if (platform === "win32") {
|
|
55
|
+
return resolveWin32Binary("codex.exe", "store-codex.txt", { ...options, env });
|
|
56
|
+
}
|
|
57
|
+
return DARWIN_CODEX_BINARY;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveDesktopApp(options = {}) {
|
|
61
|
+
const env = options.env ?? process.env;
|
|
62
|
+
if (env.CODEX_DESKTOP_APP) return env.CODEX_DESKTOP_APP;
|
|
63
|
+
const platform = resolvePlatform({ ...options, env });
|
|
64
|
+
if (platform === "linux") return LINUX_CHATGPT_BIN;
|
|
65
|
+
if (platform === "win32") {
|
|
66
|
+
return resolveWin32Binary("ChatGPT.exe", "store-app.txt", { ...options, env });
|
|
67
|
+
}
|
|
68
|
+
return DARWIN_CODEX_APP;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveGrokBinary(home = homedir(), options = {}) {
|
|
72
|
+
const env = options.env ?? process.env;
|
|
73
|
+
if (env.GROK_BINARY) return env.GROK_BINARY;
|
|
74
|
+
const platform = resolvePlatform({ ...options, env });
|
|
75
|
+
const name = platform === "win32" ? "grok.exe" : "grok";
|
|
76
|
+
return path.join(home, ".grok", "bin", name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function desktopUserDataDir(home = homedir(), options = {}) {
|
|
80
|
+
const env = options.env ?? process.env;
|
|
81
|
+
if (resolvePlatform({ ...options, env }) === "win32") {
|
|
82
|
+
return path.join(localAppData(home, { env }), WIN32_BRIDGE_APP, "desktop");
|
|
83
|
+
}
|
|
84
|
+
return path.join(home, ".local/share/codex-grok-bridge/desktop");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function linuxAppDir(home = homedir()) {
|
|
88
|
+
return path.join(home, ".local/share/codex-grok-bridge/app");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function linuxDesktopEntryPath(home = homedir(), xdgDataHome) {
|
|
92
|
+
const dataHome = xdgDataHome || path.join(home, ".local/share");
|
|
93
|
+
return path.join(dataHome, "applications", "codex-grok.desktop");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function desktopLaunchEnv({ wrapper, userData, pathPrefix }) {
|
|
97
|
+
const env = {
|
|
98
|
+
CODEX_CLI_PATH: wrapper,
|
|
99
|
+
CODEX_APP_SERVER_FORCE_CLI: "1",
|
|
100
|
+
CODEX_ELECTRON_USER_DATA_PATH: userData,
|
|
101
|
+
};
|
|
102
|
+
if (pathPrefix) env.PATH = pathPrefix;
|
|
103
|
+
return env;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function desktopLaunchArgs(userData) {
|
|
107
|
+
return [`--user-data-dir=${userData}`];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isGrokDesktopProcess(line, userData, options = {}) {
|
|
111
|
+
if (!line.includes(`--user-data-dir=${userData}`)) return false;
|
|
112
|
+
if (line.includes("Helper") || line.includes("crashpad")) return false;
|
|
113
|
+
const app = resolveDesktopApp(options);
|
|
114
|
+
if (line.includes(app)) return true;
|
|
115
|
+
const platform = resolvePlatform(options);
|
|
116
|
+
if (platform === "linux") return line.includes(LINUX_CHATGPT_BIN);
|
|
117
|
+
if (platform === "win32") {
|
|
118
|
+
return /ChatGPT\.exe|Codex\.exe/i.test(line);
|
|
119
|
+
}
|
|
120
|
+
return /MacOS\/(ChatGPT|Codex)(\s|$)/.test(line);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isForbiddenInstallDir(app) {
|
|
124
|
+
const resolved = path.resolve(app);
|
|
125
|
+
const norm = resolved.replace(/\\/g, "/").toLowerCase();
|
|
126
|
+
const asPosix = String(app).replace(/\\/g, "/");
|
|
127
|
+
const forbidden = [
|
|
128
|
+
LINUX_STOCK_PREFIX,
|
|
129
|
+
"/usr/bin/chatgpt",
|
|
130
|
+
"/usr/share/applications/chatgpt.desktop",
|
|
131
|
+
DARWIN_CODEX_APP,
|
|
132
|
+
];
|
|
133
|
+
if (
|
|
134
|
+
forbidden.some((prefix) => {
|
|
135
|
+
const p = prefix.toLowerCase();
|
|
136
|
+
return (
|
|
137
|
+
asPosix === prefix ||
|
|
138
|
+
asPosix.startsWith(`${prefix}/`) ||
|
|
139
|
+
norm === p ||
|
|
140
|
+
norm.endsWith(p) ||
|
|
141
|
+
norm.includes(`${p}/`)
|
|
142
|
+
);
|
|
143
|
+
})
|
|
144
|
+
) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return norm.includes("/windowsapps/") || norm.endsWith("/windowsapps");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function linuxDesktopEntry({ exec, icon = "chatgpt" }) {
|
|
151
|
+
return [
|
|
152
|
+
"[Desktop Entry]",
|
|
153
|
+
"Type=Application",
|
|
154
|
+
"Name=Codex Grok",
|
|
155
|
+
"Comment=Codex desktop with Grok via the local bridge",
|
|
156
|
+
`Exec=${exec}`,
|
|
157
|
+
`Icon=${icon}`,
|
|
158
|
+
"Terminal=false",
|
|
159
|
+
"Categories=Development;",
|
|
160
|
+
"StartupWMClass=ChatGPT",
|
|
161
|
+
"",
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
package/src/proxy.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveGrokBinary } from "./paths.mjs";
|
|
4
4
|
import { createSseRewriter } from "./tools.mjs";
|
|
5
5
|
import { requestStream } from "./transport.mjs";
|
|
6
6
|
import { BRIDGE_ERROR, classifyBridgeError } from "./errors.mjs";
|
|
@@ -15,7 +15,7 @@ export function detectGrokClientVersion(home = homedir()) {
|
|
|
15
15
|
try {
|
|
16
16
|
// Synchronous, and on the first request's critical path: a grok binary that
|
|
17
17
|
// hangs would freeze the whole bridge event loop without a timeout.
|
|
18
|
-
const output = execFileSync(
|
|
18
|
+
const output = execFileSync(resolveGrokBinary(home), ["--version"], {
|
|
19
19
|
encoding: "utf8",
|
|
20
20
|
timeout: 5000,
|
|
21
21
|
});
|
package/src/router.mjs
CHANGED
|
@@ -1,25 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { catalogModelEntries, isGrokModel, MODEL_ENTRY } from "./models.mjs";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
id: "grok-4.6",
|
|
5
|
-
model: "grok-4.6",
|
|
6
|
-
displayName: "Grok 4.6 / xAI",
|
|
7
|
-
description: "Grok 4.6 · Codex tools",
|
|
8
|
-
hidden: false,
|
|
9
|
-
isDefault: false,
|
|
10
|
-
upgrade: null,
|
|
11
|
-
upgradeInfo: null,
|
|
12
|
-
availabilityNux: null,
|
|
13
|
-
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"].map(
|
|
14
|
-
(reasoningEffort) => ({ reasoningEffort, description: reasoningEffort }),
|
|
15
|
-
),
|
|
16
|
-
defaultReasoningEffort: "high",
|
|
17
|
-
inputModalities: ["text", "image"],
|
|
18
|
-
supportsPersonality: false,
|
|
19
|
-
multiAgentVersion: null,
|
|
20
|
-
additionalSpeedTiers: [],
|
|
21
|
-
serviceTiers: [],
|
|
22
|
-
};
|
|
4
|
+
export { MODEL_ENTRY };
|
|
23
5
|
|
|
24
6
|
export class Router {
|
|
25
7
|
constructor(catalogPath) {
|
|
@@ -45,7 +27,7 @@ export class Router {
|
|
|
45
27
|
const fresh = this.unstarted.get(p.threadId);
|
|
46
28
|
const snapshot = fresh ?? await rpc("thread/resume", { threadId: p.threadId, excludeTurns: true });
|
|
47
29
|
const model = selected ?? snapshot.model;
|
|
48
|
-
const provider = model
|
|
30
|
+
const provider = isGrokModel(model)
|
|
49
31
|
? "grok_build_cli"
|
|
50
32
|
: model?.startsWith("gpt-") && snapshot.modelProvider === "grok_build_cli"
|
|
51
33
|
? "openai" : snapshot.modelProvider;
|
|
@@ -73,7 +55,7 @@ export class Router {
|
|
|
73
55
|
const msg = structuredClone(message),
|
|
74
56
|
p = msg.params ?? {};
|
|
75
57
|
const model = p.collaborationMode?.settings?.model ?? p.model;
|
|
76
|
-
const grok = model
|
|
58
|
+
const grok = isGrokModel(model);
|
|
77
59
|
if (
|
|
78
60
|
["thread/start", "thread/resume", "thread/fork"].includes(
|
|
79
61
|
msg.method,
|
|
@@ -95,12 +77,12 @@ export class Router {
|
|
|
95
77
|
this.pending.delete(msg.id);
|
|
96
78
|
if (request.method === "thread/start" && msg.result?.thread?.id)
|
|
97
79
|
this.unstarted.set(msg.result.thread.id, structuredClone(msg.result));
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
if (request.method === "model/list" && Array.isArray(msg.result?.data)) {
|
|
81
|
+
for (const entry of catalogModelEntries()) {
|
|
82
|
+
if (!msg.result.data.some((m) => m.id === entry.id))
|
|
83
|
+
msg.result.data.push(entry);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
104
86
|
if (
|
|
105
87
|
["thread/start", "thread/resume", "thread/fork"].includes(
|
|
106
88
|
request.method,
|
package/src/runtime.mjs
CHANGED
|
@@ -3,13 +3,14 @@ import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { once } from "node:events";
|
|
6
|
-
import { createBridgeServer
|
|
6
|
+
import { createBridgeServer } from "./bridge.mjs";
|
|
7
|
+
import { catalogModelInfos } from "./models.mjs";
|
|
7
8
|
|
|
8
9
|
export async function startRuntime(options = {}) {
|
|
9
10
|
const token = randomBytes(32).toString("hex");
|
|
10
11
|
const dir = await mkdtemp(path.join(tmpdir(), "codex-grok-runtime-"));
|
|
11
12
|
const catalogPath = path.join(dir, "models.json");
|
|
12
|
-
await writeFile(catalogPath, JSON.stringify({ models:
|
|
13
|
+
await writeFile(catalogPath, JSON.stringify({ models: catalogModelInfos() }), {
|
|
13
14
|
mode: 0o600,
|
|
14
15
|
});
|
|
15
16
|
const server = createBridgeServer({
|