clawdex-mobile 5.0.6 → 5.0.7

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.7",
4
4
  "description": "Private-network mobile bridge and CLI for Codex and OpenCode",
5
5
  "keywords": [
6
6
  "codex",
@@ -59,6 +59,11 @@ function sleep(ms) {
59
59
  return new Promise((resolve) => setTimeout(resolve, ms));
60
60
  }
61
61
 
62
+ function readNonEmptyEnv(env, key) {
63
+ const value = env[key];
64
+ return typeof value === "string" && value.trim() ? value.trim() : "";
65
+ }
66
+
62
67
  function formatHostForUrl(host) {
63
68
  if (host.includes(":") && !host.startsWith("[")) {
64
69
  return `[${host}]`;
@@ -66,6 +71,27 @@ function formatHostForUrl(host) {
66
71
  return host;
67
72
  }
68
73
 
74
+ function buildBridgeUrl(host, port) {
75
+ return `http://${formatHostForUrl(host)}:${port}`;
76
+ }
77
+
78
+ function shouldShowPairingQr(env) {
79
+ const raw = readNonEmptyEnv(env, "BRIDGE_SHOW_PAIRING_QR");
80
+ return raw ? raw.toLowerCase() !== "false" : true;
81
+ }
82
+
83
+ function printBridgeAccessDetails(env, endpoint) {
84
+ const bridgeUrl = buildBridgeUrl(endpoint.host, endpoint.port);
85
+ console.log(`Bridge URL: ${bridgeUrl}`);
86
+
87
+ const token = readNonEmptyEnv(env, "BRIDGE_AUTH_TOKEN");
88
+ if (token) {
89
+ console.log(`Bridge token: ${token}`);
90
+ }
91
+
92
+ return bridgeUrl;
93
+ }
94
+
69
95
  function bridgePidFile(rootDir) {
70
96
  return path.join(rootDir, ".bridge.pid");
71
97
  }
@@ -317,8 +343,11 @@ async function spawnDetachedAndWait(command, args, options) {
317
343
  if (await probeHealth(healthUrl)) {
318
344
  console.log(`Bridge already running (pid ${existingPid}).`);
319
345
  console.log(`Logs: ${logPath}`);
320
- console.log(`Bridge is healthy at http://${formatHostForUrl(host)}:${port}`);
321
- printLatestPairingQr(logPath);
346
+ console.log("Bridge is healthy.");
347
+ printBridgeAccessDetails(env, { host, port });
348
+ if (shouldShowPairingQr(env)) {
349
+ printLatestPairingQr(logPath);
350
+ }
322
351
  return;
323
352
  }
324
353
  } else if (existingPid) {
@@ -362,9 +391,13 @@ async function spawnDetachedAndWait(command, args, options) {
362
391
 
363
392
  try {
364
393
  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.");
394
+ console.log("Bridge is healthy.");
395
+ printBridgeAccessDetails(env, endpoint);
396
+
397
+ if (shouldShowPairingQr(env) && !(await waitForLatestPairingQr(logPath, logStartOffset, 8000))) {
398
+ console.log(
399
+ "Pairing QR not found in the new bridge startup log. Bridge URL/token are above. Open logs if you need to inspect startup output."
400
+ );
368
401
  }
369
402
  } catch (error) {
370
403
  removePidFile(rootDir);
@@ -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.7"
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.7"
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 {