clawdex-mobile 1.3.2 → 2.0.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.
Files changed (48) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/npm-release.yml +18 -0
  3. package/AGENTS.md +3 -3
  4. package/README.md +101 -541
  5. package/apps/mobile/.env.example +1 -2
  6. package/apps/mobile/App.tsx +261 -68
  7. package/apps/mobile/app.json +31 -5
  8. package/apps/mobile/assets/brand/splash-icon-white.png +0 -0
  9. package/apps/mobile/eas.json +30 -0
  10. package/apps/mobile/package.json +22 -21
  11. package/apps/mobile/plugins/withAndroidCleartextTraffic.js +14 -0
  12. package/apps/mobile/src/api/__tests__/ws.test.ts +44 -6
  13. package/apps/mobile/src/api/chatMapping.ts +48 -8
  14. package/apps/mobile/src/api/client.ts +6 -0
  15. package/apps/mobile/src/api/types.ts +11 -0
  16. package/apps/mobile/src/api/ws.ts +52 -10
  17. package/apps/mobile/src/bridgeUrl.ts +105 -0
  18. package/apps/mobile/src/components/ActivityBar.tsx +32 -13
  19. package/apps/mobile/src/components/ChatHeader.tsx +3 -2
  20. package/apps/mobile/src/components/ChatInput.tsx +246 -91
  21. package/apps/mobile/src/components/ChatMessage.tsx +108 -4
  22. package/apps/mobile/src/config.ts +11 -29
  23. package/apps/mobile/src/hooks/useVoiceRecorder.ts +264 -0
  24. package/apps/mobile/src/navigation/DrawerContent.tsx +18 -8
  25. package/apps/mobile/src/screens/GitScreen.tsx +1 -1
  26. package/apps/mobile/src/screens/MainScreen.tsx +906 -268
  27. package/apps/mobile/src/screens/OnboardingScreen.tsx +1132 -0
  28. package/apps/mobile/src/screens/PrivacyScreen.tsx +1 -1
  29. package/apps/mobile/src/screens/SettingsScreen.tsx +65 -1
  30. package/apps/mobile/src/screens/TerminalScreen.tsx +1 -1
  31. package/apps/mobile/src/screens/TermsScreen.tsx +1 -1
  32. package/docs/app-review-notes.md +7 -2
  33. package/docs/eas-builds.md +91 -0
  34. package/docs/realtime-streaming-limitations.md +84 -0
  35. package/docs/setup-and-operations.md +239 -0
  36. package/docs/troubleshooting.md +121 -0
  37. package/docs/voice-transcription.md +87 -0
  38. package/package.json +8 -16
  39. package/scripts/setup-secure-dev.sh +122 -8
  40. package/scripts/setup-wizard.sh +342 -122
  41. package/scripts/start-bridge-secure.sh +7 -1
  42. package/scripts/sync-versions.js +63 -0
  43. package/services/rust-bridge/.env.example +1 -1
  44. package/services/rust-bridge/Cargo.lock +1104 -23
  45. package/services/rust-bridge/Cargo.toml +3 -1
  46. package/services/rust-bridge/package.json +1 -1
  47. package/services/rust-bridge/src/main.rs +587 -12
  48. package/apps/mobile/metro.config.js +0 -3
@@ -0,0 +1,121 @@
1
+ # Troubleshooting
2
+
3
+ ## Onboarding looks stuck before Expo logs appear
4
+
5
+ - Expo startup can be slow on first launch.
6
+ - You should see: `Waiting for Expo output ...`
7
+ - Increase timeout if needed:
8
+
9
+ ```bash
10
+ EXPO_OUTPUT_WAIT_SECS=180 clawdex init
11
+ ```
12
+
13
+ - If Expo never emits logs:
14
+
15
+ ```bash
16
+ tail -n 120 .expo.log
17
+ ```
18
+
19
+ ## Expo starts but QR/network is wrong
20
+
21
+ - Re-run `npm run secure:setup`
22
+ - Confirm `.env.secure` has correct `BRIDGE_HOST`
23
+ - Restart `npm run mobile`
24
+
25
+ ## Stop all running services quickly
26
+
27
+ Preferred:
28
+
29
+ ```bash
30
+ clawdex stop
31
+ ```
32
+
33
+ From repo checkout:
34
+
35
+ ```bash
36
+ npm run stop:services
37
+ ```
38
+
39
+ ## Bridge auth errors (`401`, invalid token)
40
+
41
+ - Ensure `BRIDGE_AUTH_TOKEN` in `.env.secure` matches `EXPO_PUBLIC_HOST_BRIDGE_TOKEN` in `apps/mobile/.env`
42
+ - Restart bridge + Expo after token changes
43
+
44
+ ## Tailscale issues
45
+
46
+ - Verify host and phone are on the same Tailscale network
47
+ - Check host IP (`tailscale ip -4`) and mobile `.env` URL
48
+
49
+ ## `codex` not found
50
+
51
+ - Ensure `codex` is in `PATH`
52
+ - Or set `CODEX_CLI_BIN` explicitly
53
+
54
+ ## Bridge build fails with `linker 'cc' not found`
55
+
56
+ Install C build tools:
57
+
58
+ ```bash
59
+ sudo apt-get update && sudo apt-get install -y build-essential
60
+ ```
61
+
62
+ Then retry `npm run secure:bridge`.
63
+
64
+ ## iOS bundling error: `Unable to resolve "./BoundingDimensions"`
65
+
66
+ Manual recovery:
67
+
68
+ ```bash
69
+ npm install --include=dev --force
70
+ npm install --include=dev --force -w apps/mobile
71
+ npm run -w apps/mobile start -- --clear
72
+ ```
73
+
74
+ ## Runtime errors: `[runtime not ready]` / `property is not writable`
75
+
76
+ Manual recovery:
77
+
78
+ ```bash
79
+ rm -rf node_modules apps/mobile/node_modules
80
+ npm install --include=dev --force
81
+ npm install --include=dev --force -w apps/mobile
82
+ npm run -w apps/mobile start -- --clear
83
+ ```
84
+
85
+ Also update Expo Go on your phone.
86
+
87
+ ## Git operations fail
88
+
89
+ - Verify chat workspace is a valid git repo
90
+ - Verify remote auth/access for push
91
+
92
+ ## Attachment upload issues
93
+
94
+ - Ensure mobile app has file/photo permissions
95
+ - File limit is `20 MB` per upload
96
+ - Uploads persist under `BRIDGE_WORKDIR/.clawdex-mobile-attachments`
97
+ - Ensure `BRIDGE_WORKDIR` is writable
98
+
99
+ ## Worklets/Reanimated mismatch
100
+
101
+ ```bash
102
+ cd apps/mobile
103
+ npx expo install --fix
104
+ npm run start -- --clear
105
+ ```
106
+
107
+ ## Plan mode errors (`RPC-32600` invalid `collaborationMode`)
108
+
109
+ - Restart Expo and reload app bundle
110
+ - Ensure bridge/mobile revisions match
111
+ - Run API test if needed:
112
+
113
+ ```bash
114
+ npm run -w apps/mobile test -- --runInBand src/api/__tests__/client.test.ts
115
+ ```
116
+
117
+ ## Stop button does not interrupt a run
118
+
119
+ - Ensure revision supports `turn/interrupt`
120
+ - If run already finished, stop button disappears by design
121
+ - Pull latest, restart bridge, reload Expo bundle
@@ -0,0 +1,87 @@
1
+ # Push-to-Talk Voice Transcription
2
+
3
+ Voice-to-text input for the mobile composer. Tap the mic button to record, tap again to stop — the audio is sent to the Rust bridge which calls OpenAI's transcription API and returns the text into the composer.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Phone (expo-audio) Rust Bridge OpenAI
9
+ record 16kHz mono --> base64 over WebSocket --> POST /v1/audio/transcriptions
10
+ (iOS WAV, Android M4A)
11
+ insert text in composer <-- { text: "..." } <-- gpt-4o-transcribe response
12
+ ```
13
+
14
+ The flow mirrors Codex CLI 0.105.0's TUI push-to-talk feature — same API, same model (`gpt-4o-transcribe`), same auth. The bridge acts as the HTTP proxy since the phone doesn't hold API keys directly.
15
+
16
+ ## Auth Resolution
17
+
18
+ The bridge resolves transcription credentials in order:
19
+
20
+ 1. `OPENAI_API_KEY` env var → `https://api.openai.com/v1/audio/transcriptions` (model: `gpt-4o-transcribe`)
21
+ 2. `BRIDGE_CHATGPT_ACCESS_TOKEN` env var → `https://chatgpt.com/backend-api/transcribe` (no model param)
22
+ 3. `~/.codex/auth.json` fallback:
23
+ - `OPENAI_API_KEY` field present → same as path 1
24
+ - `auth_mode: "chatgpt"` with `tokens.access_token` → same as path 2
25
+
26
+ ## Files
27
+
28
+ ### Rust Bridge
29
+
30
+ | File | What |
31
+ |------|------|
32
+ | `services/rust-bridge/Cargo.toml` | Added `reqwest` (multipart, json, rustls-tls) |
33
+ | `services/rust-bridge/src/main.rs` | `bridge/voice/transcribe` JSON-RPC method, `transcribe_voice()`, `resolve_transcription_auth()`, `resolve_codex_auth_json_path()` |
34
+
35
+ ### Mobile
36
+
37
+ | File | What |
38
+ |------|------|
39
+ | `apps/mobile/app.json` | `NSMicrophoneUsageDescription` (iOS), `RECORD_AUDIO` permission (Android), `expo-audio` plugin |
40
+ | `apps/mobile/src/api/types.ts` | `VoiceTranscribeRequest`, `VoiceTranscribeResponse` |
41
+ | `apps/mobile/src/api/client.ts` | `transcribeVoice()` method on `HostBridgeApiClient` |
42
+ | `apps/mobile/src/hooks/useVoiceRecorder.ts` | Recording state machine hook using `expo-audio` |
43
+ | `apps/mobile/src/components/ChatInput.tsx` | Mic button UI with three visual states |
44
+ | `apps/mobile/src/screens/MainScreen.tsx` | Wires hook to ChatInput |
45
+
46
+ ## Recording Config
47
+
48
+ - Sample rate: 16,000 Hz
49
+ - Channels: 1 (mono)
50
+ - Format:
51
+ - iOS: LINEARPCM 16-bit (`audio/wav`)
52
+ - Android: MPEG-4 AAC (`audio/mp4`, `.m4a`)
53
+ - Minimum duration:
54
+ - Mobile guard: 1 second
55
+ - Bridge raw payload guard: ~0.5 seconds (16KB minimum)
56
+ - Maximum payload size:
57
+ - Mobile guard: 20MB
58
+ - Bridge guard: 100MB by default (override with `BRIDGE_MAX_VOICE_TRANSCRIPTION_BYTES`)
59
+
60
+ ## UI States
61
+
62
+ The mic button occupies the send button slot when the composer is empty and no turn is running:
63
+
64
+ | State | Icon | Style |
65
+ |-------|------|-------|
66
+ | Idle | `mic-outline` | Muted color |
67
+ | Recording | `mic` | Red icon, red border |
68
+ | Transcribing | `ActivityIndicator` | Spinner |
69
+
70
+ Interaction: tap to start recording, tap again to stop and transcribe. The transcribed text is appended to the current draft.
71
+
72
+ Voice input is enabled on iOS and Android. The mic button is hidden on web.
73
+
74
+ ## Dependencies
75
+
76
+ - **`expo-audio`** — Recording via `useAudioRecorder` hook. Works in Expo Go (unlike `expo-av` which requires a dev build in SDK 55).
77
+ - **`reqwest`** (Rust) — HTTP client for the multipart POST to OpenAI. Uses `rustls-tls` to avoid native-tls linking issues on macOS.
78
+
79
+ ## Error Handling
80
+
81
+ - Mic permission denied → error message, stays idle
82
+ - Recording < 1 second → "Recording too short" error, discarded
83
+ - Audio payload < 16KB → bridge rejects with `invalid_params`
84
+ - Audio payload > 20MB → mobile rejects before upload
85
+ - Audio payload > bridge max bytes (default 100MB) → bridge rejects with `invalid_params`
86
+ - No credentials found → bridge returns error code `-32002`
87
+ - Transcription API HTTP error → bridge returns status + body in error data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawdex-mobile",
3
- "version": "1.3.2",
3
+ "version": "2.0.0",
4
4
  "homepage": "https://github.com/Mohit-Patil/clawdex-mobile#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,9 @@
25
25
  "stop:services": "./scripts/stop-services.sh",
26
26
  "secure:setup": "./scripts/setup-secure-dev.sh",
27
27
  "secure:bridge": "./scripts/start-bridge-secure.sh",
28
+ "secure:bridge:dev": "BRIDGE_RUN_MODE=dev ./scripts/start-bridge-secure.sh",
28
29
  "bridge:ts": "BRIDGE_WORKDIR=$(pwd) npm run -w @codex/mac-bridge dev",
30
+ "version:sync": "node scripts/sync-versions.js",
29
31
  "build": "npm run --workspaces build",
30
32
  "typecheck": "npm run --workspaces typecheck",
31
33
  "lint": "npm run --workspaces lint",
@@ -33,20 +35,10 @@
33
35
  "teardown": "./scripts/teardown.sh"
34
36
  },
35
37
  "dependencies": {
36
- "@types/react": "19.1.10",
37
- "react": "19.1.0",
38
- "react-native": "0.81.5",
39
- "expo": "54.0.33"
40
- },
41
- "overrides": {
42
- "minimatch": "10.2.2",
43
- "@react-navigation/drawer": {
44
- "react-native-reanimated": "4.1.1"
45
- },
46
- "react-native-drawer-layout": {
47
- "react-native-reanimated": "4.1.1"
48
- },
49
- "react-native-reanimated": "4.1.1",
50
- "react-native-worklets": "0.5.1"
38
+ "@types/react": "~19.2.10",
39
+ "react": "19.2.0",
40
+ "react-dom": "19.2.0",
41
+ "react-native": "0.83.2",
42
+ "expo": "^55.0.2"
51
43
  }
52
44
  }
@@ -77,6 +77,34 @@ ensure_tailscale_cli() {
77
77
  return 0
78
78
  }
79
79
 
80
+ is_ipv4() {
81
+ local ip="$1"
82
+ local part=""
83
+ local -a octets=()
84
+
85
+ [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
86
+ IFS='.' read -r -a octets <<<"$ip"
87
+ if [[ "${#octets[@]}" -ne 4 ]]; then
88
+ return 1
89
+ fi
90
+
91
+ for part in "${octets[@]}"; do
92
+ if (( part < 0 || part > 255 )); then
93
+ return 1
94
+ fi
95
+ done
96
+
97
+ return 0
98
+ }
99
+
100
+ is_non_loopback_ipv4() {
101
+ local ip="$1"
102
+ if ! is_ipv4 "$ip"; then
103
+ return 1
104
+ fi
105
+ [[ "$ip" != 127.* ]]
106
+ }
107
+
80
108
  resolve_tailscale_ipv4() {
81
109
  local ip
82
110
  ip="$(tailscale ip -4 2>/dev/null | head -n1 | tr -d '[:space:]' || true)"
@@ -100,6 +128,77 @@ resolve_tailscale_ipv4() {
100
128
  printf '%s' "$ip"
101
129
  }
102
130
 
131
+ current_local_ipv4() {
132
+ local candidate=""
133
+ local iface=""
134
+
135
+ if [[ "$(uname -s)" == "Darwin" ]] && command -v ipconfig >/dev/null 2>&1; then
136
+ for iface in en0 en1; do
137
+ candidate="$(ipconfig getifaddr "$iface" 2>/dev/null | tr -d '[:space:]' || true)"
138
+ if is_non_loopback_ipv4 "$candidate"; then
139
+ printf '%s' "$candidate"
140
+ return 0
141
+ fi
142
+ done
143
+ fi
144
+
145
+ if command -v hostname >/dev/null 2>&1; then
146
+ while IFS= read -r candidate; do
147
+ candidate="$(printf '%s' "$candidate" | tr -d '[:space:]')"
148
+ if is_non_loopback_ipv4 "$candidate"; then
149
+ printf '%s' "$candidate"
150
+ return 0
151
+ fi
152
+ done < <(hostname -I 2>/dev/null | tr ' ' '\n' || true)
153
+ fi
154
+
155
+ if command -v ip >/dev/null 2>&1; then
156
+ candidate="$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i=1; i<=NF; i++) if ($i=="src") { print $(i+1); exit } }')"
157
+ candidate="$(printf '%s' "$candidate" | tr -d '[:space:]')"
158
+ if is_non_loopback_ipv4 "$candidate"; then
159
+ printf '%s' "$candidate"
160
+ return 0
161
+ fi
162
+ fi
163
+
164
+ if command -v ifconfig >/dev/null 2>&1; then
165
+ while IFS= read -r candidate; do
166
+ candidate="$(printf '%s' "$candidate" | tr -d '[:space:]')"
167
+ if is_non_loopback_ipv4 "$candidate"; then
168
+ printf '%s' "$candidate"
169
+ return 0
170
+ fi
171
+ done < <(ifconfig 2>/dev/null | awk '/inet /{print $2}' || true)
172
+ fi
173
+
174
+ return 1
175
+ }
176
+
177
+ resolve_local_ipv4() {
178
+ local ip=""
179
+ ip="$(current_local_ipv4 || true)"
180
+
181
+ if [[ -n "$ip" ]]; then
182
+ printf '%s' "$ip"
183
+ return 0
184
+ fi
185
+
186
+ echo "No active local/LAN IPv4 found automatically."
187
+ if ! confirm_prompt "Enter bridge host IP manually?"; then
188
+ echo "error: unable to resolve LAN IPv4. Connect to LAN and retry." >&2
189
+ return 1
190
+ fi
191
+
192
+ read -r -p "Bridge host IP: " ip
193
+ ip="$(printf '%s' "$ip" | tr -d '[:space:]')"
194
+ if ! is_non_loopback_ipv4 "$ip"; then
195
+ echo "error: invalid IPv4 '$ip'." >&2
196
+ return 1
197
+ fi
198
+
199
+ printf '%s' "$ip"
200
+ }
201
+
103
202
  if ! command -v openssl >/dev/null 2>&1; then
104
203
  echo "error: openssl not found. Install OpenSSL first." >&2
105
204
  exit 1
@@ -117,13 +216,28 @@ fi
117
216
 
118
217
  BRIDGE_HOST="${BRIDGE_HOST_OVERRIDE:-}"
119
218
  HOST_SOURCE=""
219
+ BRIDGE_NETWORK_MODE="${BRIDGE_NETWORK_MODE:-tailscale}"
220
+
221
+ case "$BRIDGE_NETWORK_MODE" in
222
+ tailscale|local)
223
+ ;;
224
+ *)
225
+ echo "error: BRIDGE_NETWORK_MODE must be 'tailscale' or 'local'." >&2
226
+ exit 1
227
+ ;;
228
+ esac
120
229
 
121
230
  if [[ -n "$BRIDGE_HOST" ]]; then
122
231
  HOST_SOURCE="override"
123
232
  else
124
- ensure_tailscale_cli
125
- BRIDGE_HOST="$(resolve_tailscale_ipv4)"
126
- HOST_SOURCE="tailscale"
233
+ if [[ "$BRIDGE_NETWORK_MODE" == "tailscale" ]]; then
234
+ ensure_tailscale_cli
235
+ BRIDGE_HOST="$(resolve_tailscale_ipv4)"
236
+ HOST_SOURCE="tailscale"
237
+ else
238
+ BRIDGE_HOST="$(resolve_local_ipv4)"
239
+ HOST_SOURCE="local"
240
+ fi
127
241
  fi
128
242
 
129
243
  BRIDGE_PORT="${BRIDGE_PORT_OVERRIDE:-8787}"
@@ -139,26 +253,26 @@ if [[ -z "$BRIDGE_TOKEN" ]]; then
139
253
  fi
140
254
 
141
255
  cat > "$SECURE_ENV_FILE" <<EOT
256
+ BRIDGE_NETWORK_MODE=$BRIDGE_NETWORK_MODE
142
257
  BRIDGE_HOST=$BRIDGE_HOST
143
258
  BRIDGE_PORT=$BRIDGE_PORT
144
259
  BRIDGE_AUTH_TOKEN=$BRIDGE_TOKEN
145
- BRIDGE_ALLOW_QUERY_TOKEN_AUTH=false
260
+ BRIDGE_ALLOW_QUERY_TOKEN_AUTH=true
146
261
  CODEX_CLI_BIN=codex
147
262
  BRIDGE_WORKDIR=$ROOT_DIR
148
263
  EOT
149
264
 
150
265
  chmod 600 "$SECURE_ENV_FILE"
151
266
 
152
- upsert_env_key "$MOBILE_ENV_FILE" "EXPO_PUBLIC_HOST_BRIDGE_URL" "http://$BRIDGE_HOST:$BRIDGE_PORT"
153
267
  upsert_env_key "$MOBILE_ENV_FILE" "EXPO_PUBLIC_HOST_BRIDGE_TOKEN" "$BRIDGE_TOKEN"
154
- # Backward compatibility for older app builds that still read MAC_BRIDGE keys.
155
- upsert_env_key "$MOBILE_ENV_FILE" "EXPO_PUBLIC_MAC_BRIDGE_URL" "http://$BRIDGE_HOST:$BRIDGE_PORT"
268
+ # Backward compatibility for older app builds that still read MAC_BRIDGE token key.
156
269
  upsert_env_key "$MOBILE_ENV_FILE" "EXPO_PUBLIC_MAC_BRIDGE_TOKEN" "$BRIDGE_TOKEN"
157
- upsert_env_key "$MOBILE_ENV_FILE" "EXPO_PUBLIC_ALLOW_QUERY_TOKEN_AUTH" "false"
270
+ upsert_env_key "$MOBILE_ENV_FILE" "EXPO_PUBLIC_ALLOW_QUERY_TOKEN_AUTH" "true"
158
271
  upsert_env_key "$MOBILE_ENV_FILE" "EXPO_PUBLIC_ALLOW_INSECURE_REMOTE_BRIDGE" "true"
159
272
 
160
273
  echo "Secure dev setup complete."
161
274
  echo ""
275
+ echo "Bridge network mode: $BRIDGE_NETWORK_MODE"
162
276
  echo "Bridge host: $BRIDGE_HOST ($HOST_SOURCE)"
163
277
  echo "Bridge port: $BRIDGE_PORT"
164
278
  echo "Token source: $SECURE_ENV_FILE"