clawdex-mobile 5.0.6 → 5.0.8-internal.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/README.md CHANGED
@@ -38,7 +38,7 @@ npm install -g clawdex-mobile@latest
38
38
  clawdex init
39
39
  ```
40
40
 
41
- Then open the mobile app and connect using the printed bridge URL/token.
41
+ Then open the mobile app and connect using the printed bridge URL/token or pairing QR.
42
42
  `clawdex init` now writes config, starts the bridge in the background, and returns you to the shell. Bridge logs go to `.bridge.log`.
43
43
 
44
44
  The npm package is bridge-only. It does not install Expo or the mobile source tree. On supported macOS, Linux, and Windows hosts it uses bundled bridge binaries, so normal startup does not compile Rust.
@@ -28,7 +28,7 @@ After `clawdex init`, expected sequence:
28
28
 
29
29
  1. Secure config is written or reused
30
30
  2. The bridge starts in the background
31
- 3. The wizard prints the bridge URL/token for manual mobile pairing
31
+ 3. The wizard prints the bridge URL, token, and pairing QR for mobile onboarding
32
32
  4. Bridge logs are written to `.bridge.log`
33
33
 
34
34
  Published npm releases bundle prebuilt bridge binaries for `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `linux-armv7l`, and `win32-x64`. On those hosts, normal bridge startup does not require a Rust compile.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawdex-mobile",
3
- "version": "5.0.6",
3
+ "version": "5.0.8-internal.0",
4
4
  "description": "Private-network mobile bridge and CLI for Codex and OpenCode",
5
5
  "keywords": [
6
6
  "codex",
@@ -19,6 +19,9 @@
19
19
  "publishConfig": {
20
20
  "access": "public"
21
21
  },
22
+ "dependencies": {
23
+ "qrcode-terminal": "^0.12.0"
24
+ },
22
25
  "bin": {
23
26
  "clawdex": "./bin/clawdex.js"
24
27
  },
@@ -194,7 +194,7 @@ sync_active_engine_from_selection() {
194
194
  return
195
195
  fi
196
196
 
197
- if engine_list_contains "$ACTIVE_ENGINE" "${SELECTED_ENGINES[@]}"; then
197
+ if selected_engines_contains "$ACTIVE_ENGINE"; then
198
198
  return
199
199
  fi
200
200
 
@@ -215,6 +215,30 @@ format_engine_list() {
215
215
  printf '%s' "$result"
216
216
  }
217
217
 
218
+ selected_engines_contains() {
219
+ local needle="$1"
220
+
221
+ if (( ${#SELECTED_ENGINES[@]} == 0 )); then
222
+ return 1
223
+ fi
224
+
225
+ engine_list_contains "$needle" "${SELECTED_ENGINES[@]}"
226
+ }
227
+
228
+ format_selected_engines() {
229
+ if (( ${#SELECTED_ENGINES[@]} == 0 )); then
230
+ printf ''
231
+ return 0
232
+ fi
233
+
234
+ format_engine_list "${SELECTED_ENGINES[@]}"
235
+ }
236
+
237
+ selected_engines_csv() {
238
+ local IFS=','
239
+ printf '%s' "${SELECTED_ENGINES[*]-}"
240
+ }
241
+
218
242
  engine_from_menu_label() {
219
243
  case "$1" in
220
244
  "Codex")
@@ -957,7 +981,11 @@ print_existing_setup_summary() {
957
981
  local harnesses=""
958
982
  local source_path=""
959
983
  local saved_active_engine="$ACTIVE_ENGINE"
960
- local -a saved_selected_engines=("${SELECTED_ENGINES[@]}")
984
+ local -a saved_selected_engines=()
985
+
986
+ if (( ${#SELECTED_ENGINES[@]} > 0 )); then
987
+ saved_selected_engines=("${SELECTED_ENGINES[@]}")
988
+ fi
961
989
 
962
990
  if [[ ! -f "$SECURE_ENV_FILE" ]]; then
963
991
  return 1
@@ -971,7 +999,7 @@ print_existing_setup_summary() {
971
999
  if [[ -n "$harnesses" ]] && ! parse_existing_engine_list_csv "$harnesses"; then
972
1000
  harnesses=""
973
1001
  elif [[ -n "$harnesses" ]]; then
974
- harnesses="$(IFS=,; printf '%s' "${SELECTED_ENGINES[*]}")"
1002
+ harnesses="$(selected_engines_csv)"
975
1003
  fi
976
1004
  if [[ -z "$harnesses" ]]; then
977
1005
  harnesses="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ACTIVE_ENGINE")"
@@ -999,7 +1027,10 @@ print_existing_setup_summary() {
999
1027
  fi
1000
1028
  echo "source: $source_path"
1001
1029
 
1002
- SELECTED_ENGINES=("${saved_selected_engines[@]}")
1030
+ SELECTED_ENGINES=()
1031
+ if (( ${#saved_selected_engines[@]} > 0 )); then
1032
+ SELECTED_ENGINES=("${saved_selected_engines[@]}")
1033
+ fi
1003
1034
  ACTIVE_ENGINE="$saved_active_engine"
1004
1035
  }
1005
1036
 
@@ -1082,12 +1113,12 @@ choose_bridge_network_mode() {
1082
1113
  choose_runtime_engine() {
1083
1114
  local label=""
1084
1115
  MENU_MULTI_PRESELECTED="Codex"
1085
- if engine_list_contains "codex" "${SELECTED_ENGINES[@]}"; then
1116
+ if selected_engines_contains "codex"; then
1086
1117
  MENU_MULTI_PRESELECTED="Codex"
1087
1118
  else
1088
1119
  MENU_MULTI_PRESELECTED=""
1089
1120
  fi
1090
- if engine_list_contains "opencode" "${SELECTED_ENGINES[@]}"; then
1121
+ if selected_engines_contains "opencode"; then
1091
1122
  if [[ -n "$MENU_MULTI_PRESELECTED" ]]; then
1092
1123
  MENU_MULTI_PRESELECTED+=","
1093
1124
  fi
@@ -1101,7 +1132,7 @@ choose_runtime_engine() {
1101
1132
  SELECTED_ENGINES+=("$(engine_from_menu_label "$label")")
1102
1133
  done
1103
1134
  sync_active_engine_from_selection
1104
- info "Selected harnesses: $(format_engine_list "${SELECTED_ENGINES[@]}")."
1135
+ info "Selected harnesses: $(format_selected_engines)."
1105
1136
  }
1106
1137
 
1107
1138
  infer_network_mode_from_host() {
@@ -1217,6 +1248,11 @@ ensure_opencode_cli() {
1217
1248
 
1218
1249
  ensure_selected_engine_clis() {
1219
1250
  local engine=""
1251
+
1252
+ if (( ${#SELECTED_ENGINES[@]} == 0 )); then
1253
+ abort_wizard "Select at least one harness before continuing."
1254
+ fi
1255
+
1220
1256
  for engine in "${SELECTED_ENGINES[@]}"; do
1221
1257
  case "$engine" in
1222
1258
  codex)
@@ -1613,16 +1649,16 @@ fi
1613
1649
  load_existing_engine_selection
1614
1650
  if [[ "$CONFIG_ACTION" == "keep" ]]; then
1615
1651
  if [[ "$ENGINE_SELECTION_PRESET" == "false" ]]; then
1616
- info "Keeping existing harnesses: $(format_engine_list "${SELECTED_ENGINES[@]}")."
1652
+ info "Keeping existing harnesses: $(format_selected_engines)."
1617
1653
  else
1618
- info "Harness selection preset via flag: $(format_engine_list "${SELECTED_ENGINES[@]}")."
1654
+ info "Harness selection preset via flag: $(format_selected_engines)."
1619
1655
  fi
1620
1656
  else
1621
1657
  section "Harnesses"
1622
1658
  if [[ "$ENGINE_SELECTION_PRESET" == "false" ]]; then
1623
1659
  choose_runtime_engine
1624
1660
  else
1625
- info "Harness selection preset via flag: $(format_engine_list "${SELECTED_ENGINES[@]}")."
1661
+ info "Harness selection preset via flag: $(format_selected_engines)."
1626
1662
  fi
1627
1663
  fi
1628
1664
 
@@ -1652,7 +1688,7 @@ if [[ "$CONFIG_ACTION" != "keep" ]]; then
1652
1688
  esac
1653
1689
 
1654
1690
  section "Write secure config"
1655
- BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(IFS=,; printf '%s' "${SELECTED_ENGINES[*]}")" "$SCRIPT_DIR/setup-secure-dev.sh"
1691
+ BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(selected_engines_csv)" "$SCRIPT_DIR/setup-secure-dev.sh"
1656
1692
  else
1657
1693
  ok "Keeping existing secure config."
1658
1694
  NETWORK_MODE="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_NETWORK_MODE")"
@@ -1677,10 +1713,10 @@ else
1677
1713
  fi
1678
1714
 
1679
1715
  section "Write secure config"
1680
- BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(IFS=,; printf '%s' "${SELECTED_ENGINES[*]}")" "$SCRIPT_DIR/setup-secure-dev.sh"
1681
- elif [[ "$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ENABLED_ENGINES")" != "$(IFS=,; printf '%s' "${SELECTED_ENGINES[*]}")" ]]; then
1716
+ BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(selected_engines_csv)" "$SCRIPT_DIR/setup-secure-dev.sh"
1717
+ elif [[ "$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ENABLED_ENGINES")" != "$(selected_engines_csv)" ]]; then
1682
1718
  section "Write secure config"
1683
- BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(IFS=,; printf '%s' "${SELECTED_ENGINES[*]}")" "$SCRIPT_DIR/setup-secure-dev.sh"
1719
+ BRIDGE_NETWORK_MODE="$NETWORK_MODE" BRIDGE_HOST_OVERRIDE="$BRIDGE_HOST" BRIDGE_ACTIVE_ENGINE="$ACTIVE_ENGINE" BRIDGE_ENABLED_ENGINES="$(selected_engines_csv)" "$SCRIPT_DIR/setup-secure-dev.sh"
1684
1720
  fi
1685
1721
  fi
1686
1722
 
@@ -1711,7 +1747,7 @@ BRIDGE_PORT="${BRIDGE_PORT:-8787}"
1711
1747
  section "Summary"
1712
1748
  rail_echo "Bridge mode: $NETWORK_MODE"
1713
1749
  rail_echo "Bridge endpoint: http://$BRIDGE_HOST:$BRIDGE_PORT"
1714
- rail_echo "Harnesses: $(format_engine_list "${SELECTED_ENGINES[@]}")"
1750
+ rail_echo "Harnesses: $(format_selected_engines)"
1715
1751
  rail_echo "Secure env: $SECURE_ENV_FILE"
1716
1752
  if [[ "$FLOW" == "quickstart" ]]; then
1717
1753
  rail_echo "${DIM}Tip: re-run with Manual mode for full control at each step.${RESET}"
@@ -17,6 +17,10 @@ const {
17
17
 
18
18
  const DEFAULT_HEALTH_TIMEOUT_MS = 15000;
19
19
  const DEV_HEALTH_TIMEOUT_MS = 60000;
20
+ let qrcodeTerminal = null;
21
+ let qrcodeTerminalLoaded = false;
22
+ let qrcodeTerminalLoadError = null;
23
+ let pairingQrRenderError = null;
20
24
 
21
25
  function resolveRootDir() {
22
26
  let rootDir = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : path.resolve(__dirname, "..");
@@ -59,6 +63,11 @@ function sleep(ms) {
59
63
  return new Promise((resolve) => setTimeout(resolve, ms));
60
64
  }
61
65
 
66
+ function readNonEmptyEnv(env, key) {
67
+ const value = env[key];
68
+ return typeof value === "string" && value.trim() ? value.trim() : "";
69
+ }
70
+
62
71
  function formatHostForUrl(host) {
63
72
  if (host.includes(":") && !host.startsWith("[")) {
64
73
  return `[${host}]`;
@@ -66,71 +75,148 @@ function formatHostForUrl(host) {
66
75
  return host;
67
76
  }
68
77
 
69
- function bridgePidFile(rootDir) {
70
- return path.join(rootDir, ".bridge.pid");
78
+ function buildBridgeUrl(host, port) {
79
+ return `http://${formatHostForUrl(host)}:${port}`;
71
80
  }
72
81
 
73
- function bridgeLogFile(rootDir) {
74
- return path.join(rootDir, ".bridge.log");
82
+ function isUnspecifiedBindHost(host) {
83
+ const normalized = String(host || "").trim().toLowerCase();
84
+ return normalized === "0.0.0.0" || normalized === "::" || normalized === "[::]";
75
85
  }
76
86
 
77
- function extractLatestPairingQrBlock(logContents) {
78
- const lines = logContents.split(/\r?\n/);
79
- for (let index = lines.length - 1; index >= 0; index -= 1) {
80
- const line = lines[index];
81
- if (line.includes("Bridge pairing QR (scan from mobile onboarding):")) {
82
- const endIndex = lines.findIndex(
83
- (entry, offset) =>
84
- offset > index && entry.includes("QR contains bridge URL + token for one-tap onboarding.")
85
- );
86
- if (endIndex !== -1) {
87
- return lines.slice(index, endIndex + 1).join("\n").trimEnd();
88
- }
89
- }
87
+ function buildPairingPayload(env, endpoint) {
88
+ const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
89
+ if (!token || isUnspecifiedBindHost(endpoint.host)) {
90
+ return null;
91
+ }
90
92
 
91
- if (line.includes("Bridge token QR fallback (scan from mobile onboarding):")) {
92
- const endIndex = lines.findIndex(
93
- (entry, offset) =>
94
- offset > index &&
95
- entry.includes("Full pairing QR unavailable because BRIDGE_HOST=")
96
- );
97
- if (endIndex !== -1) {
98
- return lines.slice(index, endIndex + 1).join("\n").trimEnd();
99
- }
100
- }
93
+ return JSON.stringify({
94
+ type: "clawdex-bridge-pair",
95
+ bridgeUrl: buildBridgeUrl(endpoint.host, endpoint.port),
96
+ bridgeToken: token,
97
+ });
98
+ }
99
+
100
+ function buildTokenOnlyPairingPayload(env) {
101
+ const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
102
+ if (!token) {
103
+ return null;
101
104
  }
102
105
 
103
- return null;
106
+ return JSON.stringify({
107
+ type: "clawdex-bridge-token",
108
+ bridgeToken: token,
109
+ });
104
110
  }
105
111
 
106
- function printLatestPairingQr(logPath, startOffset = 0) {
112
+ function loadQrcodeTerminal() {
113
+ if (qrcodeTerminalLoaded) {
114
+ return qrcodeTerminal;
115
+ }
116
+
117
+ qrcodeTerminalLoaded = true;
118
+ try {
119
+ qrcodeTerminal = require("qrcode-terminal");
120
+ } catch (error) {
121
+ qrcodeTerminal = null;
122
+ qrcodeTerminalLoadError = error;
123
+ }
124
+
125
+ return qrcodeTerminal;
126
+ }
127
+
128
+ function printPairingQr(env, endpoint) {
129
+ pairingQrRenderError = null;
130
+ const qr = loadQrcodeTerminal();
131
+ if (!qr) {
132
+ return false;
133
+ }
134
+
107
135
  try {
108
- const raw = fs.readFileSync(logPath, "utf8");
109
- const contents = startOffset > 0 ? raw.slice(startOffset) : raw;
110
- const qrBlock = extractLatestPairingQrBlock(contents);
111
- if (!qrBlock) {
136
+ const payload = buildPairingPayload(env, endpoint);
137
+ if (payload) {
138
+ console.log("");
139
+ console.log("Bridge pairing QR (scan from mobile onboarding):");
140
+ qr.generate(payload, { small: true });
141
+ console.log("QR contains bridge URL + token for one-tap onboarding.");
142
+ console.log("");
143
+ return true;
144
+ }
145
+
146
+ const tokenPayload = buildTokenOnlyPairingPayload(env);
147
+ if (!tokenPayload) {
112
148
  return false;
113
149
  }
150
+
114
151
  console.log("");
115
- console.log(qrBlock);
152
+ console.log("Bridge token QR fallback (scan from mobile onboarding):");
153
+ qr.generate(tokenPayload, { small: true });
154
+ console.log(
155
+ `Full pairing QR unavailable because BRIDGE_HOST=${endpoint.host} is a bind address. Enter URL manually in onboarding.`
156
+ );
116
157
  console.log("");
117
158
  return true;
118
- } catch {
159
+ } catch (error) {
160
+ pairingQrRenderError = error;
119
161
  return false;
120
162
  }
121
163
  }
122
164
 
123
- async function waitForLatestPairingQr(logPath, startOffset, timeoutMs = 4000) {
124
- const startedAt = Date.now();
165
+ function printPairingQrUnavailableMessage(env) {
166
+ const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
167
+ if (!token) {
168
+ console.log(
169
+ "Pairing QR unavailable because BRIDGE_AUTH_TOKEN is not set. Bridge URL is above for manual onboarding."
170
+ );
171
+ return;
172
+ }
125
173
 
126
- while (Date.now() - startedAt < timeoutMs) {
127
- if (printLatestPairingQr(logPath, startOffset)) {
128
- return true;
129
- }
130
- await sleep(250);
174
+ if (pairingQrRenderError) {
175
+ console.log(
176
+ `Pairing QR unavailable because terminal rendering failed: ${pairingQrRenderError.message}. Bridge URL/token are above for manual onboarding.`
177
+ );
178
+ return;
179
+ }
180
+
181
+ if (qrcodeTerminalLoaded && !qrcodeTerminal) {
182
+ const detail =
183
+ qrcodeTerminalLoadError && qrcodeTerminalLoadError.message
184
+ ? ` (${qrcodeTerminalLoadError.message})`
185
+ : "";
186
+ console.log(
187
+ `Pairing QR unavailable because the terminal QR renderer could not be loaded${detail}. Bridge URL/token are above for manual onboarding.`
188
+ );
189
+ return;
190
+ }
191
+
192
+ console.log(
193
+ "Pairing QR unavailable due to an unexpected startup condition. Bridge URL/token are above for manual onboarding."
194
+ );
195
+ }
196
+
197
+ function shouldShowPairingQr(env) {
198
+ const raw = readNonEmptyEnv(env, "BRIDGE_SHOW_PAIRING_QR");
199
+ return raw ? raw.toLowerCase() !== "false" : true;
200
+ }
201
+
202
+ function printBridgeAccessDetails(env, endpoint) {
203
+ const bridgeUrl = buildBridgeUrl(endpoint.host, endpoint.port);
204
+ console.log(`Bridge URL: ${bridgeUrl}`);
205
+
206
+ const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
207
+ if (token) {
208
+ console.log(`Bridge token: ${token}`);
131
209
  }
132
210
 
133
- return false;
211
+ return bridgeUrl;
212
+ }
213
+
214
+ function bridgePidFile(rootDir) {
215
+ return path.join(rootDir, ".bridge.pid");
216
+ }
217
+
218
+ function bridgeLogFile(rootDir) {
219
+ return path.join(rootDir, ".bridge.log");
134
220
  }
135
221
 
136
222
  function readPidFile(rootDir) {
@@ -317,8 +403,12 @@ async function spawnDetachedAndWait(command, args, options) {
317
403
  if (await probeHealth(healthUrl)) {
318
404
  console.log(`Bridge already running (pid ${existingPid}).`);
319
405
  console.log(`Logs: ${logPath}`);
320
- console.log(`Bridge is healthy at http://${formatHostForUrl(host)}:${port}`);
321
- printLatestPairingQr(logPath);
406
+ console.log("Bridge is healthy.");
407
+ const endpoint = { host, port };
408
+ printBridgeAccessDetails(env, endpoint);
409
+ if (shouldShowPairingQr(env) && !printPairingQr(env, endpoint)) {
410
+ printPairingQrUnavailableMessage(env);
411
+ }
322
412
  return;
323
413
  }
324
414
  } else if (existingPid) {
@@ -334,7 +424,6 @@ async function spawnDetachedAndWait(command, args, options) {
334
424
 
335
425
  const output = fs.openSync(logPath, "a");
336
426
  const error = fs.openSync(logPath, "a");
337
- const logStartOffset = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
338
427
 
339
428
  const child = spawn(command, args, {
340
429
  cwd,
@@ -362,9 +451,11 @@ async function spawnDetachedAndWait(command, args, options) {
362
451
 
363
452
  try {
364
453
  const endpoint = await waitForHealth(env, child.pid, healthTimeoutMs);
365
- console.log(`Bridge is healthy at http://${formatHostForUrl(endpoint.host)}:${endpoint.port}`);
366
- if (!(await waitForLatestPairingQr(logPath, logStartOffset))) {
367
- console.log("Pairing QR not found in the new bridge startup log. Open logs if you need to inspect startup output.");
454
+ console.log("Bridge is healthy.");
455
+ printBridgeAccessDetails(env, endpoint);
456
+
457
+ if (shouldShowPairingQr(env) && !printPairingQr(env, endpoint)) {
458
+ printPairingQrUnavailableMessage(env);
368
459
  }
369
460
  } catch (error) {
370
461
  removePidFile(rootDir);
@@ -73,6 +73,23 @@ extract_env_value() {
73
73
  ' "$file"
74
74
  }
75
75
 
76
+ stop_launchctl_job() {
77
+ local label="$1"
78
+
79
+ if [[ -z "$label" ]] || ! command -v launchctl >/dev/null 2>&1; then
80
+ return 1
81
+ fi
82
+
83
+ local domain="gui/$(id -u)/$label"
84
+ if ! launchctl print "$domain" >/dev/null 2>&1; then
85
+ return 1
86
+ fi
87
+
88
+ echo "Stopping launchd job: $label"
89
+ launchctl bootout "$domain" >/dev/null 2>&1 || launchctl remove "$label" >/dev/null 2>&1 || true
90
+ return 0
91
+ }
92
+
76
93
  stop_process_group() {
77
94
  local label="$1"
78
95
  local pattern="$2"
@@ -111,7 +128,12 @@ stop_process_group() {
111
128
 
112
129
  echo "Stopping Clawdex services for project: $ROOT_DIR"
113
130
 
131
+ BRIDGE_PORT="${BRIDGE_PORT:-$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_PORT" || true)}"
132
+ BRIDGE_PORT="${BRIDGE_PORT:-8787}"
133
+
114
134
  stop_process_group "Expo" "$ROOT_DIR/.*/expo start|$ROOT_DIR/node_modules/.bin/expo start"
135
+ stop_launchctl_job "clawdex.bridge.$BRIDGE_PORT" || true
136
+ stop_process_group "Bridge launcher" "$ROOT_DIR/scripts/start-bridge-secure\\.js|node .*start-bridge-secure\\.js"
115
137
  stop_pid_file_process "Rust bridge" "$BRIDGE_PID_FILE" || true
116
138
  stop_process_group "Rust bridge" "$ROOT_DIR/services/rust-bridge|codex-rust-bridge|@codex/rust-bridge"
117
139
  stop_process_group "Legacy TS bridge" "$ROOT_DIR/services/mac-bridge|@codex/mac-bridge"
@@ -149,7 +149,7 @@ dependencies = [
149
149
 
150
150
  [[package]]
151
151
  name = "codex-rust-bridge"
152
- version = "5.0.6"
152
+ version = "5.0.8-internal.0"
153
153
  dependencies = [
154
154
  "axum",
155
155
  "base64",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "codex-rust-bridge"
3
- version = "5.0.6"
3
+ version = "5.0.8-internal.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -2,7 +2,7 @@ use std::{
2
2
  collections::{HashMap, HashSet, VecDeque},
3
3
  env,
4
4
  hash::{Hash, Hasher},
5
- io::SeekFrom,
5
+ io::{SeekFrom, Write},
6
6
  path::{Component, Path, PathBuf},
7
7
  process::Stdio,
8
8
  sync::{
@@ -5404,6 +5404,11 @@ fn build_token_only_pairing_payload(config: &BridgeConfig) -> Option<String> {
5404
5404
  )
5405
5405
  }
5406
5406
 
5407
+ fn flush_pairing_output() {
5408
+ let _ = std::io::stdout().flush();
5409
+ let _ = std::io::stderr().flush();
5410
+ }
5411
+
5407
5412
  fn maybe_print_pairing_qr(config: &BridgeConfig) {
5408
5413
  if !config.show_pairing_qr {
5409
5414
  return;
@@ -5414,15 +5419,18 @@ fn maybe_print_pairing_qr(config: &BridgeConfig) {
5414
5419
  println!("Bridge pairing QR (scan from mobile onboarding):");
5415
5420
  if let Err(error) = qr2term::print_qr(payload.as_bytes()) {
5416
5421
  eprintln!("failed to render pairing QR: {error}");
5422
+ flush_pairing_output();
5417
5423
  return;
5418
5424
  }
5419
5425
  println!("QR contains bridge URL + token for one-tap onboarding.");
5420
5426
  println!();
5427
+ flush_pairing_output();
5421
5428
  return;
5422
5429
  }
5423
5430
 
5424
5431
  let Some(payload) = build_token_only_pairing_payload(config) else {
5425
5432
  eprintln!("bridge token QR skipped because BRIDGE_AUTH_TOKEN is not set");
5433
+ flush_pairing_output();
5426
5434
  return;
5427
5435
  };
5428
5436
 
@@ -5430,6 +5438,7 @@ fn maybe_print_pairing_qr(config: &BridgeConfig) {
5430
5438
  println!("Bridge token QR fallback (scan from mobile onboarding):");
5431
5439
  if let Err(error) = qr2term::print_qr(payload.as_bytes()) {
5432
5440
  eprintln!("failed to render pairing QR: {error}");
5441
+ flush_pairing_output();
5433
5442
  return;
5434
5443
  }
5435
5444
  println!(
@@ -5437,6 +5446,7 @@ fn maybe_print_pairing_qr(config: &BridgeConfig) {
5437
5446
  config.host
5438
5447
  );
5439
5448
  println!();
5449
+ flush_pairing_output();
5440
5450
  }
5441
5451
 
5442
5452
  fn parse_bool_env(name: &str) -> bool {
@@ -9009,6 +9019,71 @@ mod tests {
9009
9019
  assert!(!constant_time_eq("secret-token", "secret-token-extra"));
9010
9020
  }
9011
9021
 
9022
+ #[test]
9023
+ fn build_pairing_payload_includes_url_and_token_for_connectable_host() {
9024
+ let config = BridgeConfig {
9025
+ host: "127.0.0.1".to_string(),
9026
+ port: 8787,
9027
+ workdir: PathBuf::from("/tmp/workdir"),
9028
+ cli_bin: "codex".to_string(),
9029
+ opencode_cli_bin: "opencode".to_string(),
9030
+ active_engine: BridgeRuntimeEngine::Codex,
9031
+ enabled_engines: vec![BridgeRuntimeEngine::Codex],
9032
+ opencode_host: "127.0.0.1".to_string(),
9033
+ opencode_port: 4090,
9034
+ opencode_server_username: "opencode".to_string(),
9035
+ opencode_server_password: Some("secret-token".to_string()),
9036
+ auth_token: Some("secret-token".to_string()),
9037
+ auth_enabled: true,
9038
+ allow_insecure_no_auth: false,
9039
+ allow_query_token_auth: false,
9040
+ allow_outside_root_cwd: false,
9041
+ disable_terminal_exec: false,
9042
+ terminal_allowed_commands: HashSet::new(),
9043
+ show_pairing_qr: true,
9044
+ };
9045
+
9046
+ let payload = build_pairing_payload(&config).expect("pairing payload");
9047
+ let parsed: Value = serde_json::from_str(&payload).expect("valid json");
9048
+
9049
+ assert_eq!(parsed["type"], "clawdex-bridge-pair");
9050
+ assert_eq!(parsed["bridgeUrl"], "http://127.0.0.1:8787");
9051
+ assert_eq!(parsed["bridgeToken"], "secret-token");
9052
+ }
9053
+
9054
+ #[test]
9055
+ fn build_pairing_payload_uses_token_only_fallback_for_unspecified_bind_host() {
9056
+ let config = BridgeConfig {
9057
+ host: "0.0.0.0".to_string(),
9058
+ port: 8787,
9059
+ workdir: PathBuf::from("/tmp/workdir"),
9060
+ cli_bin: "codex".to_string(),
9061
+ opencode_cli_bin: "opencode".to_string(),
9062
+ active_engine: BridgeRuntimeEngine::Codex,
9063
+ enabled_engines: vec![BridgeRuntimeEngine::Codex],
9064
+ opencode_host: "127.0.0.1".to_string(),
9065
+ opencode_port: 4090,
9066
+ opencode_server_username: "opencode".to_string(),
9067
+ opencode_server_password: Some("secret-token".to_string()),
9068
+ auth_token: Some("secret-token".to_string()),
9069
+ auth_enabled: true,
9070
+ allow_insecure_no_auth: false,
9071
+ allow_query_token_auth: false,
9072
+ allow_outside_root_cwd: false,
9073
+ disable_terminal_exec: false,
9074
+ terminal_allowed_commands: HashSet::new(),
9075
+ show_pairing_qr: true,
9076
+ };
9077
+
9078
+ assert!(build_pairing_payload(&config).is_none());
9079
+
9080
+ let fallback = build_token_only_pairing_payload(&config).expect("token-only payload");
9081
+ let parsed: Value = serde_json::from_str(&fallback).expect("valid json");
9082
+
9083
+ assert_eq!(parsed["type"], "clawdex-bridge-token");
9084
+ assert_eq!(parsed["bridgeToken"], "secret-token");
9085
+ }
9086
+
9012
9087
  #[test]
9013
9088
  fn bridge_config_authorization_validates_header_and_query_token_paths() {
9014
9089
  let base = BridgeConfig {