codex-grok-bridge 1.0.4 → 1.0.5

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 ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 1.0.5 — 2026-09-12
4
+
5
+ First npm cut that accepts **Linux** (`"os": ["darwin", "linux"]`). Windows stays rejected.
6
+
7
+ - Input field allowlist for Grok Responses (`#10`) — drops `internal_*` and other fields Grok rejects.
8
+ - Collapse `anyOf` / `oneOf` tool-parameter roots to a plain object (`#12`) — unblocks Linux GUI `invalid_client_tool_schema` 400s.
9
+ - Linux desktop wrapper (`#11`) — `~/.local/share/codex-grok-bridge/app` plus a user `.desktop` file. Does not patch `/usr/lib/chatgpt`.
10
+ - Docs: test gate **150/150**, coverage 97.30 / 87.70 / 90.57, Grok CLI verified **1.0.25**.
11
+
12
+ Install: `npm install -g codex-grok-bridge@1.0.5`
13
+
14
+ ## 1.0.4 — 2026-09-09
15
+
16
+ English and Korean README aligned. macOS-only npm package.
17
+
18
+ ## 1.0.3 — 2026-09-09
19
+
20
+ See GitHub release `v1.0.3`.
21
+
22
+ ## 1.0.2 — 2026-09-09
23
+
24
+ See GitHub release `v1.0.2`.
25
+
26
+ ## 1.0.1 — 2026-09-09
27
+
28
+ See GitHub release `v1.0.1`.
29
+
30
+ ## 1.0.0 — 2026-09-09
31
+
32
+ Initial publish. macOS only.
package/README.md CHANGED
@@ -4,10 +4,6 @@ Run **Grok 4.6 as the model inside Codex**. Codex still owns tools, permissions,
4
4
  history and MCP. Inference uses the installed `grok` CLI login session — not an
5
5
  xAI API key.
6
6
 
7
- Codex 안에서 **Grok 4.6**을 모델로 씁니다. 도구·권한·히스토리·MCP는 Codex가
8
- 그대로 가집니다. 추론은 설치된 `grok` CLI의 로그인 세션으로 하며 `XAI_API_KEY`는
9
- 쓰지 않습니다.
10
-
11
7
  ```
12
8
  Codex UI/CLI → app-server → scripts/codex-wrapper.mjs (adds grok-4.6 to the model list)
13
9
  → localhost /v1/responses (the bridge)
@@ -15,16 +11,13 @@ Codex UI/CLI → app-server → scripts/codex-wrapper.mjs (adds grok-4.6 to the
15
11
  → Codex executes every tool call; results return as the next input
16
12
  ```
17
13
 
18
- The published package is **macOS-only** (`"os": ["darwin"]`). Linux and Windows
19
- installs are rejected by npm.
14
+ The published npm package installs on **macOS and Linux** (`"os": ["darwin",
15
+ "linux"]`). Windows installs are rejected by npm. The official ChatGPT/Codex
16
+ `.deb` stays untouched; `scripts/install-codex-grok-app.sh` writes a separate
17
+ wrapper.
20
18
 
21
- 게시된 패키지는 **macOS 전용**입니다(`"os": ["darwin"]`). Linux·Windows는 npm이
22
- 설치를 거절합니다.
23
19
 
24
20
  ---
25
-
26
- # English
27
-
28
21
  ## What this is
29
22
 
30
23
  A local bridge that puts Grok 4.6 on Codex’s model list and sends Codex
@@ -44,9 +37,9 @@ that tool the same way it calls any other Codex tool.
44
37
 
45
38
  ## Requirements
46
39
 
47
- - macOS
40
+ - macOS, or Linux with the official ChatGPT/Codex desktop package
48
41
  - Node.js ≥ 22
49
- - `/Applications/Codex.app`
42
+ - `/Applications/Codex.app` (macOS) or `/usr/lib/chatgpt/ChatGPT` (Linux)
50
43
  - `grok` CLI at `~/.grok/bin/grok`
51
44
  - a completed `grok login`
52
45
 
@@ -54,7 +47,7 @@ The `.command` launcher resolves paths from its own location, so moving the
54
47
  folder does not require edits. Set `NODE=/path/to/node` if `node` is not on
55
48
  `PATH`.
56
49
 
57
- Verified against: Codex 0.153.4 / app 26.901.51231, Grok CLI 1.0.24, Node 22.23.0.
50
+ Verified against: Codex 0.153.4 / app 26.901.51231, Grok CLI 1.0.25, Node 22.23.0.
58
51
  An app update that changes `CODEX_CLI_PATH` or the app-server protocol needs
59
52
  re-verification.
60
53
 
@@ -63,7 +56,7 @@ re-verification.
63
56
  ### Terminal (npm)
64
57
 
65
58
  ```sh
66
- npm install -g codex-grok-bridge # macOS, Node ≥ 22
59
+ npm install -g codex-grok-bridge # macOS or Linux, Node ≥ 22
67
60
  codex-grok # launches Codex with Grok 4.6 available
68
61
  codex-grok exec --skip-git-repo-check --sandbox workspace-write 'your task'
69
62
  ```
@@ -76,15 +69,17 @@ starts, and tears the provider down with that process.
76
69
  ```sh
77
70
  git clone https://github.com/deximple/codex-grok-bridge.git
78
71
  cd codex-grok-bridge
79
- npm test # 132 tests, no network, no inference
72
+ npm test # 150 tests, no network, no inference
80
73
  node scripts/codex-grok.mjs
81
74
  ```
82
75
 
83
76
  ### Desktop
84
77
 
85
78
  Double-click **Open Codex with Grok.command** in this folder, or keep a
86
- **separate** `Codex Grok.app` in sync with
79
+ **separate** desktop wrapper in sync with
87
80
  `scripts/install-codex-grok-app.sh` (see [Desktop install and update](#desktop-install-and-update)).
81
+ On Linux that wrapper is `~/.local/share/codex-grok-bridge/app` plus a user
82
+ `.desktop` entry; the stock `/usr/lib/chatgpt` tree is not patched.
88
83
 
89
84
  In the new Codex window, **select Grok 4.6 / xAI before starting a new
90
85
  thread**. Existing GPT models stay on the list. A Codex window that was already
@@ -95,9 +90,9 @@ The dedicated window stores UI data under
95
90
  for account, threads and settings. Work and setting changes can show up in other
96
91
  Codex windows.
97
92
 
98
- The installer never writes the stock Codex.app bundle, its signature,
99
- `~/.codex/config.toml`, or Grok auth files. It does not register a launch
100
- agent or a global environment variable.
93
+ The installer never writes the stock Codex.app bundle, `/usr/lib/chatgpt`,
94
+ their signatures, `~/.codex/config.toml`, or Grok auth files. It does not
95
+ register a launch agent or a global environment variable.
101
96
 
102
97
  Stop by closing the Codex window this extension opened. Ordinary Codex still
103
98
  launches from its usual icon.
@@ -163,7 +158,10 @@ not read Codex’s `imagegen` skill and do not send the picture to OpenAI.
163
158
  Plain-text summaries on Codex `reasoning` items are forwarded. Encrypted
164
159
  `encrypted_content` and Codex’s own item ids are stripped. This is so a
165
160
  multi-call turn can continue its own reasoning. The upstream has been observed
166
- to accept this shape.
161
+ to accept this shape. The same pass keeps only the fields Grok’s Responses
162
+ input accepts on each item and content part — `status`, unknown Codex keys,
163
+ and every `internal_*` field are dropped so a new client field cannot 422 the
164
+ upstream.
167
165
 
168
166
  ### Concurrency
169
167
 
@@ -280,12 +278,12 @@ Start here when something breaks. Do not open `~/.grok/auth.json` or
280
278
  | `GROK_BRIDGE_INFERENCE=cli` | Fall back to the CLI envelope path |
281
279
  | `GROK_BRIDGE_DIAGNOSTICS=off` | Do not write the JSONL log |
282
280
  | `NODE` | Absolute `node` binary for the `.command` launcher / desktop scripts |
283
- | `CODEX_GROK_APP` | Alternate app bundle path for `install-codex-grok-app.sh` (default `/Applications/Codex Grok.app`) |
281
+ | `CODEX_GROK_APP` | Alternate app path for `install-codex-grok-app.sh` (macOS: `/Applications/Codex Grok.app`; Linux: `~/.local/share/codex-grok-bridge/app`) |
284
282
 
285
283
  ## Verify
286
284
 
287
285
  ```sh
288
- npm test # 132 tests, no remote inference
286
+ npm test # 150 tests, no remote inference
289
287
  npm run test:coverage # 80% line / branch / function gate
290
288
  npm run verify:app-server # real app-server routing; also runs against an installed bundle
291
289
  npm audit --omit=dev
@@ -299,7 +297,7 @@ inference. A live CLI check spends the user’s quota.
299
297
 
300
298
  ```sh
301
299
  sh scripts/install-codex-grok-app.sh # sync bridge JS only (default)
302
- sh scripts/install-codex-grok-app.sh --full # also rebuild and sign the launcher applet
300
+ sh scripts/install-codex-grok-app.sh --full # macOS: rebuild and sign the launcher applet
303
301
  ```
304
302
 
305
303
  The default copies this checkout’s `src/` and `scripts/*.mjs` into the bundle,
@@ -308,8 +306,11 @@ never `rm -rf`s the live bundle. It refuses while a Codex Grok window is open
308
306
  (`--force` to override). ESM is not hot-reloaded: **reopen the window** after a
309
307
  sync.
310
308
 
311
- The script updates an existing `Codex Grok.app`. It does not create one, and it
312
- does not touch `/Applications/Codex.app`.
309
+ On macOS the script updates an existing `Codex Grok.app`. It does not create
310
+ one, and it does not touch `/Applications/Codex.app`. On Linux it creates
311
+ `~/.local/share/codex-grok-bridge/app` and
312
+ `~/.local/share/applications/codex-grok.desktop`, and it does not write
313
+ `/usr/lib/chatgpt` or `/usr/share/applications/chatgpt.desktop`.
313
314
 
314
315
  ## Security notes
315
316
 
@@ -355,16 +356,16 @@ calling으로 옮기고, 상류 Responses 스트림을 전달한 뒤, Codex가
355
356
 
356
357
  ## 필요한 것
357
358
 
358
- - macOS
359
+ - macOS, 또는 공식 ChatGPT/Codex 데스크톱 패키지가 있는 Linux
359
360
  - Node.js 22 이상
360
- - `/Applications/Codex.app`
361
+ - `/Applications/Codex.app` (macOS) 또는 `/usr/lib/chatgpt/ChatGPT` (Linux)
361
362
  - `~/.grok/bin/grok`
362
363
  - 완료된 `grok login`
363
364
 
364
365
  `.command` 런처는 자기 위치를 기준으로 경로를 잡으므로 폴더를 옮겨도 수정할
365
366
  필요가 없습니다. `PATH`에 `node`가 없으면 `NODE=/path/to/node`로 지정합니다.
366
367
 
367
- 검증 버전: Codex 0.153.4 / 앱 26.901.51231, Grok CLI 1.0.24, Node 22.23.0.
368
+ 검증 버전: Codex 0.153.4 / 앱 26.901.51231, Grok CLI 1.0.25, Node 22.23.0.
368
369
  앱 업데이트가 `CODEX_CLI_PATH`나 app-server 프로토콜을 바꾸면 재검증이
369
370
  필요합니다.
370
371
 
@@ -373,7 +374,7 @@ calling으로 옮기고, 상류 Responses 스트림을 전달한 뒤, Codex가
373
374
  ### 터미널 (npm)
374
375
 
375
376
  ```sh
376
- npm install -g codex-grok-bridge # macOS, Node ≥ 22
377
+ npm install -g codex-grok-bridge # macOS or Linux, Node ≥ 22
377
378
  codex-grok # Grok 4.6이 있는 Codex를 띄움
378
379
  codex-grok exec --skip-git-repo-check --sandbox workspace-write '작업 내용'
379
380
  ```
@@ -386,15 +387,17 @@ codex-grok exec --skip-git-repo-check --sandbox workspace-write '작업 내용'
386
387
  ```sh
387
388
  git clone https://github.com/deximple/codex-grok-bridge.git
388
389
  cd codex-grok-bridge
389
- npm test # 132건, 네트워크·추론 없음
390
+ npm test # 150건, 네트워크·추론 없음
390
391
  node scripts/codex-grok.mjs
391
392
  ```
392
393
 
393
394
  ### 데스크톱
394
395
 
395
396
  이 폴더의 **Open Codex with Grok.command**를 더블 클릭하거나,
396
- `scripts/install-codex-grok-app.sh`로 **별도의** `Codex Grok.app`을 체크아웃과
397
+ `scripts/install-codex-grok-app.sh`로 **별도의** 데스크톱 래퍼를 체크아웃과
397
398
  맞춥니다([데스크톱 설치와 갱신](#데스크톱-설치와-갱신)).
399
+ Linux에서는 `~/.local/share/codex-grok-bridge/app`과 사용자 `.desktop`이고,
400
+ 정품 `/usr/lib/chatgpt`는 패치하지 않습니다.
398
401
 
399
402
  새로 열린 Codex 창에서 **새 작업을 시작하기 전에 Grok 4.6 / xAI를 선택**하세요.
400
403
  기존 GPT 모델도 목록에 남습니다. 이미 열려 있던 일반 Codex 창에는 이 확장이
@@ -404,9 +407,9 @@ node scripts/codex-grok.mjs
404
407
  계정·작업·설정은 기존 Codex 홈을 **공유**합니다. 작업 내용과 설정 변경은 다른
405
408
  Codex 창에도 보일 수 있습니다.
406
409
 
407
- 설치 스크립트는 정품 Codex.app 번들, 코드 서명, `~/.codex/config.toml`, Grok
408
- 인증 파일을 수정하지 않습니다. 자동 시작 서비스나 전역 환경변수도 등록하지
409
- 않습니다.
410
+ 설치 스크립트는 정품 Codex.app 번들, `/usr/lib/chatgpt`, 코드 서명,
411
+ `~/.codex/config.toml`, Grok 인증 파일을 수정하지 않습니다. 자동 시작
412
+ 서비스나 전역 환경변수도 등록하지 않습니다.
410
413
 
411
414
  중지하려면 이 확장으로 연 Codex 창을 닫으면 됩니다. 일반 Codex는 기존 아이콘으로
412
415
  실행합니다.
@@ -468,7 +471,9 @@ OpenAI로 보내지 말라는 뜻입니다. `GROK_BRIDGE_IMAGE_GEN=off`면 도
468
471
  Codex `reasoning` 항목의 평문 요약은 상류로 전달합니다. 암호화된
469
472
  `encrypted_content`와 Codex 자체 아이템 id는 제거합니다. 여러 번 호출이 이어지는
470
473
  턴에서 모델이 자기 추론을 이어받게 하기 위한 것이며, 상류가 이 형태를 수락하는
471
- 것을 확인했습니다.
474
+ 것을 확인했습니다. 같은 과정에서 아이템과 content part는 Grok Responses가
475
+ 받는 필드만 남깁니다. `status`, 알 수 없는 Codex 키, `internal_*` 필드는
476
+ 버려서 새 클라이언트 필드가 상류 422를 내지 않게 합니다.
472
477
 
473
478
  ### 동시성
474
479
 
@@ -580,12 +585,12 @@ provider에 Codex `request_max_retries` / `stream_max_retries`를 2로 두어
580
585
  | `GROK_BRIDGE_INFERENCE=cli` | CLI 봉투 경로로 폴백 |
581
586
  | `GROK_BRIDGE_DIAGNOSTICS=off` | JSONL 로그를 쓰지 않음 |
582
587
  | `NODE` | `.command` / 데스크톱 스크립트가 쓸 `node` 절대 경로 |
583
- | `CODEX_GROK_APP` | `install-codex-grok-app.sh`가 쓸 앱 번들 경로 (기본 `/Applications/Codex Grok.app`) |
588
+ | `CODEX_GROK_APP` | `install-codex-grok-app.sh`가 쓸 앱 경로 (macOS: `/Applications/Codex Grok.app`; Linux: `~/.local/share/codex-grok-bridge/app`) |
584
589
 
585
590
  ## 검증
586
591
 
587
592
  ```sh
588
- npm test # 132건, 외부 추론 없음
593
+ npm test # 150건, 외부 추론 없음
589
594
  npm run test:coverage # line/branch/function 80% 게이트
590
595
  npm run verify:app-server # 실제 app-server 라우팅. 설치된 앱 번들에서도 실행
591
596
  npm audit --omit=dev
@@ -599,7 +604,7 @@ CLI 검증은 사용자 계정 사용량을 씁니다.
599
604
 
600
605
  ```sh
601
606
  sh scripts/install-codex-grok-app.sh # 브리지 JS만 동기화 (기본)
602
- sh scripts/install-codex-grok-app.sh --full # 런처 applet 재빌드 + 서명
607
+ sh scripts/install-codex-grok-app.sh --full # macOS: 런처 applet 재빌드 + 서명
603
608
  ```
604
609
 
605
610
  기본 동작은 이 체크아웃의 `src/`와 `scripts/*.mjs`를 번들에 복사하고, 저장소에
@@ -607,8 +612,10 @@ sh scripts/install-codex-grok-app.sh --full # 런처 applet 재빌드 + 서명
607
612
  `rm -rf`하지 않습니다. Codex Grok 창이 열려 있으면 거부합니다(`--force`로
608
613
  무시). ESM은 핫리로드되지 않으므로 **동기화 후 창을 다시 열어야** 합니다.
609
614
 
610
- 이 스크립트는 이미 있는 `Codex Grok.app`을 갱신합니다. 번들을 새로 만들지
611
- 않고, `/Applications/Codex.app`은 건드리지 않습니다.
615
+ macOS에서는 이미 있는 `Codex Grok.app`을 갱신하며 `/Applications/Codex.app`은
616
+ 건드리지 않습니다. Linux에서는 `~/.local/share/codex-grok-bridge/app`과
617
+ `~/.local/share/applications/codex-grok.desktop`을 만들고 `/usr/lib/chatgpt`와
618
+ 정품 `chatgpt.desktop`은 쓰지 않습니다.
612
619
 
613
620
  ## 보안
614
621
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-grok-bridge",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Run Grok 4.6 as the model inside Codex, with Codex still owning tools, permissions, history and MCP. Uses the grok login session, not an API key.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,8 @@
9
9
  "node": ">=22"
10
10
  },
11
11
  "os": [
12
- "darwin"
12
+ "darwin",
13
+ "linux"
13
14
  ],
14
15
  "bin": {
15
16
  "codex-grok": "scripts/codex-grok.mjs"
@@ -38,6 +39,7 @@
38
39
  "scripts/launch-desktop.mjs",
39
40
  "Open Codex with Grok.command",
40
41
  "README.md",
42
+ "CHANGELOG.md",
41
43
  "LICENSE"
42
44
  ],
43
45
  "scripts": {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import { startRuntime } from "../src/runtime.mjs";
4
+ import { resolveCodexBinary } from "../src/paths.mjs";
4
5
  const runtime = await startRuntime();
5
6
  const userArgs = process.argv.slice(2);
6
7
  const overrides = [
@@ -16,7 +17,7 @@ const args =
16
17
  userArgs[0] === "exec"
17
18
  ? [userArgs[0], ...overrides, ...userArgs.slice(1)]
18
19
  : [...overrides, ...userArgs];
19
- const child = spawn("/Applications/Codex.app/Contents/Resources/codex", args, {
20
+ const child = spawn(resolveCodexBinary(), args, {
20
21
  stdio: "inherit",
21
22
  env: { ...process.env, CODEX_GROK_BRIDGE_TOKEN: runtime.token },
22
23
  });
@@ -4,8 +4,9 @@ import { createInterface } from "node:readline";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { startRuntime } from "../src/runtime.mjs";
6
6
  import { Router } from "../src/router.mjs";
7
+ import { resolveCodexBinary } from "../src/paths.mjs";
7
8
 
8
- const binary = "/Applications/Codex.app/Contents/Resources/codex";
9
+ const binary = resolveCodexBinary();
9
10
  const args = process.argv.slice(2);
10
11
  if (!args.includes("app-server")) {
11
12
  const child = spawn(binary, args, { stdio: "inherit" });
@@ -1,14 +1,12 @@
1
1
  #!/bin/sh
2
- # Install or update the bridge inside /Applications/Codex Grok.app.
2
+ # Install or update the dedicated Codex Grok desktop wrapper.
3
3
  #
4
- # Default is a JS-only sync: the bundle's scripts/ and src/ are brought in line
5
- # with this checkout, in place. The previous version began with `rm -rf` on the
6
- # directory that CODEX_CLI_PATH points into, which destroys a running app's
7
- # entrypoint; nothing here removes a directory, and stale files are pruned one
8
- # by one only when the repo no longer has them.
4
+ # macOS: sync JS into /Applications/Codex Grok.app (must already exist).
5
+ # Linux: create ~/.local/share/codex-grok-bridge/app and a user .desktop.
6
+ # Never writes /Applications/Codex.app or /usr/lib/chatgpt.
9
7
  #
10
8
  # install-codex-grok-app.sh sync bridge JS (safe, fast)
11
- # install-codex-grok-app.sh --full also rebuild the launcher applet and sign
9
+ # install-codex-grok-app.sh --full macOS: rebuild applet; Linux: rewrite launcher
12
10
  # install-codex-grok-app.sh --force proceed even if a Codex Grok window is open
13
11
  #
14
12
  # ESM is not hot-reloaded: an open Codex Grok window keeps running the code it
@@ -16,11 +14,20 @@
16
14
  set -eu
17
15
 
18
16
  ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
19
- # CODEX_GROK_APP exists so this script can be exercised against a scratch copy
20
- # instead of the installed app.
21
- APP="${CODEX_GROK_APP:-/Applications/Codex Grok.app}"
22
- BRIDGE="$APP/Contents/Resources/bridge"
23
17
  USER_DATA="$HOME/.local/share/codex-grok-bridge/desktop"
18
+ PLATFORM="${CODEX_GROK_PLATFORM:-$(uname -s)}"
19
+ case "$PLATFORM" in
20
+ Linux|linux) PLATFORM=linux ;;
21
+ Darwin|darwin) PLATFORM=darwin ;;
22
+ esac
23
+
24
+ if [ "$PLATFORM" = linux ]; then
25
+ APP="${CODEX_GROK_APP:-$HOME/.local/share/codex-grok-bridge/app}"
26
+ BRIDGE="$APP"
27
+ else
28
+ APP="${CODEX_GROK_APP:-/Applications/Codex Grok.app}"
29
+ BRIDGE="$APP/Contents/Resources/bridge"
30
+ fi
24
31
 
25
32
  FULL=0
26
33
  FORCE=0
@@ -33,7 +40,14 @@ for arg in "$@"; do
33
40
  esac
34
41
  done
35
42
 
36
- if [ ! -d "$APP/Contents" ]; then
43
+ case "$APP" in
44
+ /usr/lib/chatgpt|/usr/lib/chatgpt/*|/usr/bin/chatgpt|/Applications/Codex.app|/Applications/Codex.app/*)
45
+ echo "refusing to install into the stock ChatGPT/Codex prefix: $APP" >&2
46
+ exit 1
47
+ ;;
48
+ esac
49
+
50
+ if [ "$PLATFORM" != linux ] && [ ! -d "$APP/Contents" ]; then
37
51
  echo "missing $APP" >&2
38
52
  exit 1
39
53
  fi
@@ -71,7 +85,44 @@ for dir in scripts src; do
71
85
  done
72
86
  [ "$status" -eq 0 ] || { echo "bundle did not match the checkout" >&2; exit 1; }
73
87
 
74
- if [ "$FULL" -eq 1 ]; then
88
+ write_linux_wrapper() {
89
+ NODE="${NODE:-$(command -v node || true)}"
90
+ [ -n "$NODE" ] || { echo "no node on PATH; set NODE=/path/to/node" >&2; exit 1; }
91
+ if [ -L "$NODE" ]; then
92
+ NODE="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$NODE")"
93
+ fi
94
+ mkdir -p "$APP/bin"
95
+ LAUNCHER="$APP/bin/codex-grok-desktop"
96
+ NODE_Q=$(printf "%s" "$NODE" | sed "s/'/'\\\\''/g")
97
+ cat > "$LAUNCHER" <<EOF
98
+ #!/bin/sh
99
+ set -eu
100
+ ROOT="\$(CDPATH= cd -- "\$(dirname "\$0")/.." && pwd)"
101
+ exec '$NODE_Q' "\$ROOT/scripts/launch-desktop.mjs" "\$@"
102
+ EOF
103
+ chmod +x "$LAUNCHER"
104
+ APPS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
105
+ mkdir -p "$APPS_DIR"
106
+ DESKTOP="$APPS_DIR/codex-grok.desktop"
107
+ cat > "$DESKTOP" <<EOF
108
+ [Desktop Entry]
109
+ Type=Application
110
+ Name=Codex Grok
111
+ Comment=Codex desktop with Grok via the local bridge
112
+ Exec="$LAUNCHER" %U
113
+ Icon=chatgpt
114
+ Terminal=false
115
+ Categories=Development;
116
+ StartupWMClass=ChatGPT
117
+ EOF
118
+ update-desktop-database "$APPS_DIR" >/dev/null 2>&1 || true
119
+ echo "linux wrapper $LAUNCHER"
120
+ echo "desktop entry $DESKTOP"
121
+ }
122
+
123
+ if [ "$PLATFORM" = linux ]; then
124
+ write_linux_wrapper
125
+ elif [ "$FULL" -eq 1 ]; then
75
126
  NODE="${NODE:-$(command -v node || true)}"
76
127
  [ -n "$NODE" ] || { echo "no node on PATH; set NODE=/path/to/node" >&2; exit 1; }
77
128
  if [ -L "$NODE" ]; then
@@ -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 userData = path.join(homedir(), ".local/share/codex-grok-bridge/desktop");
8
- const logFile = path.join(homedir(), ".local/share/codex-grok-bridge/launch.log");
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
 
@@ -18,12 +26,7 @@ function grokPid() {
18
26
  encoding: "utf8",
19
27
  });
20
28
  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
- )
29
+ if (isGrokDesktopProcess(line, userData))
27
30
  return Number(line.trim().split(/\s+/)[0]);
28
31
  }
29
32
  return null;
@@ -41,6 +44,7 @@ function activate(pid) {
41
44
  }
42
45
 
43
46
  function notify(text) {
47
+ if (platform !== "darwin") return;
44
48
  try {
45
49
  execFileSync("/usr/bin/osascript", [
46
50
  "-e",
@@ -49,19 +53,28 @@ function notify(text) {
49
53
  } catch {}
50
54
  }
51
55
 
52
- const wrapper = fileURLToPath(new URL("./codex-wrapper.mjs", import.meta.url));
53
- try {
54
- const running = grokPid();
55
- if (running) {
56
- log(`activate pid=${running}`);
57
- try {
58
- activate(running);
59
- notify("Codex Grok 창을 앞으로 가져왔습니다.");
60
- process.exit(0);
61
- } catch (error) {
62
- log(`activate failed: ${error.message}`);
63
- }
64
- }
56
+ function startLinux(wrapper) {
57
+ const app = resolveDesktopApp();
58
+ const pathPrefix = `${path.dirname(process.execPath)}:/usr/local/bin:/usr/bin:/bin`;
59
+ log(`start linux app=${app} wrapper=${wrapper}`);
60
+ const child = spawn(app, desktopLaunchArgs(userData), {
61
+ stdio: "inherit",
62
+ env: {
63
+ ...process.env,
64
+ ...desktopLaunchEnv({ wrapper, userData, pathPrefix }),
65
+ },
66
+ });
67
+ child.on("error", (error) => {
68
+ log(`spawn failed: ${error.message}`);
69
+ process.exit(1);
70
+ });
71
+ child.on("exit", (code) => {
72
+ log(`chatgpt exit=${code}`);
73
+ process.exit(code ?? 1);
74
+ });
75
+ }
76
+
77
+ function startDarwin(wrapper) {
65
78
  log(`start wrapper=${wrapper}`);
66
79
  const child = spawn(
67
80
  "/usr/bin/open",
@@ -95,6 +108,26 @@ try {
95
108
  }
96
109
  process.exit(code ?? 1);
97
110
  });
111
+ }
112
+
113
+ const wrapper = fileURLToPath(new URL("./codex-wrapper.mjs", import.meta.url));
114
+ try {
115
+ const running = grokPid();
116
+ if (running) {
117
+ log(`activate pid=${running}`);
118
+ if (platform !== "darwin") {
119
+ process.exit(0);
120
+ }
121
+ try {
122
+ activate(running);
123
+ notify("Codex Grok 창을 앞으로 가져왔습니다.");
124
+ process.exit(0);
125
+ } catch (error) {
126
+ log(`activate failed: ${error.message}`);
127
+ }
128
+ }
129
+ if (platform === "linux") startLinux(wrapper);
130
+ else startDarwin(wrapper);
98
131
  } catch (error) {
99
132
  log(`fatal: ${error.message}`);
100
133
  notify(error.message);
package/src/paths.mjs ADDED
@@ -0,0 +1,94 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+
4
+ export const LINUX_CODEX_BINARY = "/usr/lib/chatgpt/resources/codex";
5
+ export const LINUX_CHATGPT_BIN = "/usr/lib/chatgpt/ChatGPT";
6
+ export const DARWIN_CODEX_BINARY = "/Applications/Codex.app/Contents/Resources/codex";
7
+ export const DARWIN_CODEX_APP = "/Applications/Codex.app";
8
+ export const LINUX_STOCK_PREFIX = "/usr/lib/chatgpt";
9
+
10
+ export function resolvePlatform(options = {}) {
11
+ const env = options.env ?? process.env;
12
+ return options.platform ?? env.CODEX_GROK_PLATFORM ?? process.platform;
13
+ }
14
+
15
+ export function resolveCodexBinary(options = {}) {
16
+ const env = options.env ?? process.env;
17
+ if (env.CODEX_BINARY) return env.CODEX_BINARY;
18
+ return resolvePlatform({ ...options, env }) === "linux"
19
+ ? LINUX_CODEX_BINARY
20
+ : DARWIN_CODEX_BINARY;
21
+ }
22
+
23
+ export function resolveDesktopApp(options = {}) {
24
+ const env = options.env ?? process.env;
25
+ if (env.CODEX_DESKTOP_APP) return env.CODEX_DESKTOP_APP;
26
+ return resolvePlatform({ ...options, env }) === "linux"
27
+ ? LINUX_CHATGPT_BIN
28
+ : DARWIN_CODEX_APP;
29
+ }
30
+
31
+ export function desktopUserDataDir(home = homedir()) {
32
+ return path.join(home, ".local/share/codex-grok-bridge/desktop");
33
+ }
34
+
35
+ export function linuxAppDir(home = homedir()) {
36
+ return path.join(home, ".local/share/codex-grok-bridge/app");
37
+ }
38
+
39
+ export function linuxDesktopEntryPath(home = homedir(), xdgDataHome) {
40
+ const dataHome = xdgDataHome || path.join(home, ".local/share");
41
+ return path.join(dataHome, "applications", "codex-grok.desktop");
42
+ }
43
+
44
+ export function desktopLaunchEnv({ wrapper, userData, pathPrefix }) {
45
+ const env = {
46
+ CODEX_CLI_PATH: wrapper,
47
+ CODEX_APP_SERVER_FORCE_CLI: "1",
48
+ CODEX_ELECTRON_USER_DATA_PATH: userData,
49
+ };
50
+ if (pathPrefix) env.PATH = pathPrefix;
51
+ return env;
52
+ }
53
+
54
+ export function desktopLaunchArgs(userData) {
55
+ return [`--user-data-dir=${userData}`];
56
+ }
57
+
58
+ export function isGrokDesktopProcess(line, userData, options = {}) {
59
+ if (!line.includes(`--user-data-dir=${userData}`)) return false;
60
+ if (line.includes("Helper") || line.includes("crashpad")) return false;
61
+ const app = resolveDesktopApp(options);
62
+ if (line.includes(app)) return true;
63
+ return resolvePlatform(options) === "linux"
64
+ ? line.includes(LINUX_CHATGPT_BIN)
65
+ : /MacOS\/(ChatGPT|Codex)(\s|$)/.test(line);
66
+ }
67
+
68
+ export function isForbiddenInstallDir(app) {
69
+ const resolved = path.resolve(app);
70
+ const forbidden = [
71
+ LINUX_STOCK_PREFIX,
72
+ "/usr/bin/chatgpt",
73
+ "/usr/share/applications/chatgpt.desktop",
74
+ DARWIN_CODEX_APP,
75
+ ];
76
+ return forbidden.some(
77
+ (prefix) => resolved === prefix || resolved.startsWith(`${prefix}/`),
78
+ );
79
+ }
80
+
81
+ export function linuxDesktopEntry({ exec, icon = "chatgpt" }) {
82
+ return [
83
+ "[Desktop Entry]",
84
+ "Type=Application",
85
+ "Name=Codex Grok",
86
+ "Comment=Codex desktop with Grok via the local bridge",
87
+ `Exec=${exec}`,
88
+ `Icon=${icon}`,
89
+ "Terminal=false",
90
+ "Categories=Development;",
91
+ "StartupWMClass=ChatGPT",
92
+ "",
93
+ ].join("\n");
94
+ }
package/src/tools.mjs CHANGED
@@ -56,16 +56,145 @@ const GROK_INPUT_ITEM_TYPES = new Set([
56
56
  "function_call_output",
57
57
  "shell_call",
58
58
  ]);
59
+ // Codex item ids, status, and new client metadata have 422'd the upstream.
60
+ // Keep only the fields Grok's Responses input is known to accept.
61
+ const INPUT_ITEM_FIELDS = {
62
+ message: ["type", "role", "content"],
63
+ reasoning: ["type", "summary"],
64
+ function_call: ["type", "name", "call_id", "arguments"],
65
+ function_call_output: ["type", "name", "call_id", "output"],
66
+ shell_call: ["type", "name", "call_id", "action"],
67
+ };
68
+ const CONTENT_PART_FIELDS = {
69
+ input_text: ["type", "text"],
70
+ output_text: ["type", "text"],
71
+ summary_text: ["type", "text"],
72
+ input_image: ["type", "image_url", "detail"],
73
+ };
74
+
75
+ function pickKeys(node, keys) {
76
+ const next = {};
77
+ for (const key of keys) {
78
+ if (node[key] !== undefined) next[key] = node[key];
79
+ }
80
+ return next;
81
+ }
82
+
83
+ function whitelistContentPart(part) {
84
+ if (!part || typeof part !== "object" || Array.isArray(part)) return part;
85
+ const keys = CONTENT_PART_FIELDS[part.type];
86
+ if (!keys) return part;
87
+ const next = pickKeys(part, keys);
88
+ if (next.image_url && typeof next.image_url === "object")
89
+ next.image_url = pickKeys(next.image_url, ["url", "detail"]);
90
+ return next;
91
+ }
92
+
93
+ function whitelistInputNode(node) {
94
+ if (!node || typeof node !== "object" || Array.isArray(node)) return node;
95
+ const itemKeys = INPUT_ITEM_FIELDS[node.type];
96
+ if (itemKeys) {
97
+ const next = pickKeys(node, itemKeys);
98
+ if (Array.isArray(next.content))
99
+ next.content = next.content.map(whitelistContentPart);
100
+ return next;
101
+ }
102
+ if (node.type == null && node.role != null && node.content !== undefined) {
103
+ const next = pickKeys(node, ["role", "content"]);
104
+ if (Array.isArray(next.content))
105
+ next.content = next.content.map(whitelistContentPart);
106
+ return next;
107
+ }
108
+ return whitelistContentPart(node);
109
+ }
110
+
111
+ function isObjectFragment(schema) {
112
+ return (
113
+ schema &&
114
+ typeof schema === "object" &&
115
+ !Array.isArray(schema) &&
116
+ (schema.type === "object" || schema.type == null)
117
+ );
118
+ }
119
+
120
+ function mergeObjectFragments(fragments) {
121
+ const properties = {};
122
+ let required;
123
+ for (const fragment of fragments) {
124
+ if (fragment.properties && typeof fragment.properties === "object")
125
+ Object.assign(properties, fragment.properties);
126
+ if (Array.isArray(fragment.required)) {
127
+ required =
128
+ required === undefined
129
+ ? fragment.required.slice()
130
+ : required.filter((key) => fragment.required.includes(key));
131
+ }
132
+ }
133
+ return { properties, required };
134
+ }
135
+
136
+ function emitObjectParameters(source, overrides = {}) {
137
+ const properties =
138
+ overrides.properties ??
139
+ (source.properties && typeof source.properties === "object"
140
+ ? source.properties
141
+ : {});
142
+ const required =
143
+ overrides.required !== undefined
144
+ ? overrides.required
145
+ : Array.isArray(source.required)
146
+ ? source.required
147
+ : undefined;
148
+ const next = { type: "object", properties: { ...properties } };
149
+ if (required?.length) next.required = required.slice();
150
+ if (source.additionalProperties !== undefined)
151
+ next.additionalProperties = source.additionalProperties;
152
+ if (source.description !== undefined) next.description = source.description;
153
+ return next;
154
+ }
59
155
 
60
156
  function usableParameters(parameters) {
61
- if (!parameters || typeof parameters !== "object") return OBJECT_SCHEMA;
62
- if (
63
- parameters.type === "object" ||
64
- Array.isArray(parameters.oneOf) ||
65
- Array.isArray(parameters.anyOf)
66
- )
67
- return parameters;
68
- return OBJECT_SCHEMA;
157
+ if (!parameters || typeof parameters !== "object" || Array.isArray(parameters))
158
+ return OBJECT_SCHEMA;
159
+ const variants = Array.isArray(parameters.oneOf)
160
+ ? parameters.oneOf
161
+ : Array.isArray(parameters.anyOf)
162
+ ? parameters.anyOf
163
+ : null;
164
+ if (!variants) {
165
+ return parameters.type === "object"
166
+ ? emitObjectParameters(parameters)
167
+ : OBJECT_SCHEMA;
168
+ }
169
+ const fragments = variants.filter(isObjectFragment);
170
+ if (fragments.length === 0) {
171
+ return parameters.type === "object"
172
+ ? emitObjectParameters(parameters)
173
+ : OBJECT_SCHEMA;
174
+ }
175
+ const merged = mergeObjectFragments(fragments);
176
+ const properties = {
177
+ ...(parameters.properties && typeof parameters.properties === "object"
178
+ ? parameters.properties
179
+ : {}),
180
+ ...merged.properties,
181
+ };
182
+ let required = merged.required;
183
+ if (Array.isArray(parameters.required)) {
184
+ required =
185
+ required === undefined
186
+ ? parameters.required.slice()
187
+ : required.filter((key) => parameters.required.includes(key));
188
+ }
189
+ const first = fragments[0];
190
+ return emitObjectParameters(
191
+ {
192
+ additionalProperties:
193
+ parameters.additionalProperties ?? first.additionalProperties,
194
+ description: parameters.description ?? first.description,
195
+ },
196
+ { properties, required },
197
+ );
69
198
  }
70
199
 
71
200
  function sanitize(name) {
@@ -189,7 +318,7 @@ function toProxyInputNode(node, map, state) {
189
318
  if (
190
319
  key === "encrypted_content" ||
191
320
  key === "encrypted_function_args" ||
192
- key === "internal_chat_message_metadata_passthrough"
321
+ key.startsWith("internal_")
193
322
  )
194
323
  continue;
195
324
  const converted = toProxyInputNode(value, map, state);
@@ -204,7 +333,7 @@ function toProxyInputNode(node, map, state) {
204
333
  (part) => part && typeof part.text === "string" && part.text.trim(),
205
334
  );
206
335
  if (!summary.length) return DROP;
207
- return { type: "reasoning", summary };
336
+ return whitelistInputNode({ type: "reasoning", summary });
208
337
  }
209
338
 
210
339
  if (next.type === "agent_message") {
@@ -214,7 +343,7 @@ function toProxyInputNode(node, map, state) {
214
343
  ? [{ type: "input_text", text: next.text }]
215
344
  : [];
216
345
  if (!content.length) return DROP;
217
- return { type: "message", role: "assistant", content };
346
+ return whitelistInputNode({ type: "message", role: "assistant", content });
218
347
  }
219
348
 
220
349
  if (
@@ -227,10 +356,7 @@ function toProxyInputNode(node, map, state) {
227
356
  delete next.namespace;
228
357
  if (typeof next.call_id === "string") state.callIds.set(next.call_id, proxyName);
229
358
  }
230
- return next;
231
- }
232
-
233
- if (
359
+ } else if (
234
360
  next.type === "custom_tool_call" &&
235
361
  typeof next.name === "string"
236
362
  ) {
@@ -244,10 +370,7 @@ function toProxyInputNode(node, map, state) {
244
370
  delete next.input;
245
371
  if (typeof next.call_id === "string") state.callIds.set(next.call_id, proxyName);
246
372
  }
247
- return next;
248
- }
249
-
250
- if (
373
+ } else if (
251
374
  next.type === "function_call_output" ||
252
375
  next.type === "custom_tool_call_output"
253
376
  ) {
@@ -262,10 +385,9 @@ function toProxyInputNode(node, map, state) {
262
385
  next.name = proxyName;
263
386
  delete next.namespace;
264
387
  }
265
- return next;
266
388
  }
267
389
 
268
- return next;
390
+ return whitelistInputNode(next);
269
391
  }
270
392
 
271
393
  function toProxyInput(input, map) {