@thinkrun/cli 0.1.27

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 (183) hide show
  1. package/README.md +349 -0
  2. package/dist/bin/thinkrun.d.ts +6 -0
  3. package/dist/bin/thinkrun.d.ts.map +1 -0
  4. package/dist/bin/thinkrun.js +124 -0
  5. package/dist/bin/thinkrun.js.map +1 -0
  6. package/dist/scripts/browse.sh +1107 -0
  7. package/dist/src/adapters/cloud.d.ts +79 -0
  8. package/dist/src/adapters/cloud.d.ts.map +1 -0
  9. package/dist/src/adapters/cloud.js +637 -0
  10. package/dist/src/adapters/cloud.js.map +1 -0
  11. package/dist/src/adapters/index.d.ts +47 -0
  12. package/dist/src/adapters/index.d.ts.map +1 -0
  13. package/dist/src/adapters/index.js +211 -0
  14. package/dist/src/adapters/index.js.map +1 -0
  15. package/dist/src/adapters/local-command-retry.d.ts +12 -0
  16. package/dist/src/adapters/local-command-retry.d.ts.map +1 -0
  17. package/dist/src/adapters/local-command-retry.js +224 -0
  18. package/dist/src/adapters/local-command-retry.js.map +1 -0
  19. package/dist/src/adapters/local.d.ts +136 -0
  20. package/dist/src/adapters/local.d.ts.map +1 -0
  21. package/dist/src/adapters/local.js +1273 -0
  22. package/dist/src/adapters/local.js.map +1 -0
  23. package/dist/src/adapters/types.d.ts +45 -0
  24. package/dist/src/adapters/types.d.ts.map +1 -0
  25. package/dist/src/adapters/types.js +6 -0
  26. package/dist/src/adapters/types.js.map +1 -0
  27. package/dist/src/commands/actions.d.ts +135 -0
  28. package/dist/src/commands/actions.d.ts.map +1 -0
  29. package/dist/src/commands/actions.js +2207 -0
  30. package/dist/src/commands/actions.js.map +1 -0
  31. package/dist/src/commands/agent-init.d.ts +16 -0
  32. package/dist/src/commands/agent-init.d.ts.map +1 -0
  33. package/dist/src/commands/agent-init.js +222 -0
  34. package/dist/src/commands/agent-init.js.map +1 -0
  35. package/dist/src/commands/analyze.d.ts +11 -0
  36. package/dist/src/commands/analyze.d.ts.map +1 -0
  37. package/dist/src/commands/analyze.js +238 -0
  38. package/dist/src/commands/analyze.js.map +1 -0
  39. package/dist/src/commands/cache.d.ts +6 -0
  40. package/dist/src/commands/cache.d.ts.map +1 -0
  41. package/dist/src/commands/cache.js +147 -0
  42. package/dist/src/commands/cache.js.map +1 -0
  43. package/dist/src/commands/cloud.d.ts +6 -0
  44. package/dist/src/commands/cloud.d.ts.map +1 -0
  45. package/dist/src/commands/cloud.js +332 -0
  46. package/dist/src/commands/cloud.js.map +1 -0
  47. package/dist/src/commands/config.d.ts +7 -0
  48. package/dist/src/commands/config.d.ts.map +1 -0
  49. package/dist/src/commands/config.js +208 -0
  50. package/dist/src/commands/config.js.map +1 -0
  51. package/dist/src/commands/doctor.d.ts +127 -0
  52. package/dist/src/commands/doctor.d.ts.map +1 -0
  53. package/dist/src/commands/doctor.js +684 -0
  54. package/dist/src/commands/doctor.js.map +1 -0
  55. package/dist/src/commands/evaluate-helpers.d.ts +6 -0
  56. package/dist/src/commands/evaluate-helpers.d.ts.map +1 -0
  57. package/dist/src/commands/evaluate-helpers.js +13 -0
  58. package/dist/src/commands/evaluate-helpers.js.map +1 -0
  59. package/dist/src/commands/install.d.ts +118 -0
  60. package/dist/src/commands/install.d.ts.map +1 -0
  61. package/dist/src/commands/install.js +975 -0
  62. package/dist/src/commands/install.js.map +1 -0
  63. package/dist/src/commands/release.d.ts +7 -0
  64. package/dist/src/commands/release.d.ts.map +1 -0
  65. package/dist/src/commands/release.js +123 -0
  66. package/dist/src/commands/release.js.map +1 -0
  67. package/dist/src/commands/reset-connection.d.ts +17 -0
  68. package/dist/src/commands/reset-connection.d.ts.map +1 -0
  69. package/dist/src/commands/reset-connection.js +141 -0
  70. package/dist/src/commands/reset-connection.js.map +1 -0
  71. package/dist/src/commands/session-debug.d.ts +23 -0
  72. package/dist/src/commands/session-debug.d.ts.map +1 -0
  73. package/dist/src/commands/session-debug.js +267 -0
  74. package/dist/src/commands/session-debug.js.map +1 -0
  75. package/dist/src/commands/setup.d.ts +53 -0
  76. package/dist/src/commands/setup.d.ts.map +1 -0
  77. package/dist/src/commands/setup.js +249 -0
  78. package/dist/src/commands/setup.js.map +1 -0
  79. package/dist/src/config/store.d.ts +39 -0
  80. package/dist/src/config/store.d.ts.map +1 -0
  81. package/dist/src/config/store.js +290 -0
  82. package/dist/src/config/store.js.map +1 -0
  83. package/dist/src/daemon/access.d.ts +53 -0
  84. package/dist/src/daemon/access.d.ts.map +1 -0
  85. package/dist/src/daemon/access.js +87 -0
  86. package/dist/src/daemon/access.js.map +1 -0
  87. package/dist/src/daemon/bridge-envelope.d.ts +96 -0
  88. package/dist/src/daemon/bridge-envelope.d.ts.map +1 -0
  89. package/dist/src/daemon/bridge-envelope.js +235 -0
  90. package/dist/src/daemon/bridge-envelope.js.map +1 -0
  91. package/dist/src/daemon/utils.d.ts +43 -0
  92. package/dist/src/daemon/utils.d.ts.map +1 -0
  93. package/dist/src/daemon/utils.js +134 -0
  94. package/dist/src/daemon/utils.js.map +1 -0
  95. package/dist/src/errors.d.ts +60 -0
  96. package/dist/src/errors.d.ts.map +1 -0
  97. package/dist/src/errors.js +87 -0
  98. package/dist/src/errors.js.map +1 -0
  99. package/dist/src/local-bridge-timing.d.ts +31 -0
  100. package/dist/src/local-bridge-timing.d.ts.map +1 -0
  101. package/dist/src/local-bridge-timing.js +41 -0
  102. package/dist/src/local-bridge-timing.js.map +1 -0
  103. package/dist/src/obstacle-recovery/classify-script.d.ts +16 -0
  104. package/dist/src/obstacle-recovery/classify-script.d.ts.map +1 -0
  105. package/dist/src/obstacle-recovery/classify-script.js +53 -0
  106. package/dist/src/obstacle-recovery/classify-script.js.map +1 -0
  107. package/dist/src/obstacle-recovery/obstacle-classifier.d.ts +21 -0
  108. package/dist/src/obstacle-recovery/obstacle-classifier.d.ts.map +1 -0
  109. package/dist/src/obstacle-recovery/obstacle-classifier.js +37 -0
  110. package/dist/src/obstacle-recovery/obstacle-classifier.js.map +1 -0
  111. package/dist/src/obstacle-recovery/state-fingerprint.d.ts +26 -0
  112. package/dist/src/obstacle-recovery/state-fingerprint.d.ts.map +1 -0
  113. package/dist/src/obstacle-recovery/state-fingerprint.js +85 -0
  114. package/dist/src/obstacle-recovery/state-fingerprint.js.map +1 -0
  115. package/dist/src/obstacle-recovery/types.d.ts +44 -0
  116. package/dist/src/obstacle-recovery/types.d.ts.map +1 -0
  117. package/dist/src/obstacle-recovery/types.js +16 -0
  118. package/dist/src/obstacle-recovery/types.js.map +1 -0
  119. package/dist/src/output/formatter.d.ts +55 -0
  120. package/dist/src/output/formatter.d.ts.map +1 -0
  121. package/dist/src/output/formatter.js +55 -0
  122. package/dist/src/output/formatter.js.map +1 -0
  123. package/dist/src/output/mode.d.ts +11 -0
  124. package/dist/src/output/mode.d.ts.map +1 -0
  125. package/dist/src/output/mode.js +16 -0
  126. package/dist/src/output/mode.js.map +1 -0
  127. package/dist/src/protected-flow/detector.d.ts +26 -0
  128. package/dist/src/protected-flow/detector.d.ts.map +1 -0
  129. package/dist/src/protected-flow/detector.js +75 -0
  130. package/dist/src/protected-flow/detector.js.map +1 -0
  131. package/dist/src/protected-flow/types.d.ts +24 -0
  132. package/dist/src/protected-flow/types.d.ts.map +1 -0
  133. package/dist/src/protected-flow/types.js +28 -0
  134. package/dist/src/protected-flow/types.js.map +1 -0
  135. package/dist/src/session/agent-identity.d.ts +65 -0
  136. package/dist/src/session/agent-identity.d.ts.map +1 -0
  137. package/dist/src/session/agent-identity.js +133 -0
  138. package/dist/src/session/agent-identity.js.map +1 -0
  139. package/dist/src/session/cli-session-sync.d.ts +72 -0
  140. package/dist/src/session/cli-session-sync.d.ts.map +1 -0
  141. package/dist/src/session/cli-session-sync.js +244 -0
  142. package/dist/src/session/cli-session-sync.js.map +1 -0
  143. package/dist/src/session/context.d.ts +24 -0
  144. package/dist/src/session/context.d.ts.map +1 -0
  145. package/dist/src/session/context.js +165 -0
  146. package/dist/src/session/context.js.map +1 -0
  147. package/dist/src/session/continuity.d.ts +33 -0
  148. package/dist/src/session/continuity.d.ts.map +1 -0
  149. package/dist/src/session/continuity.js +179 -0
  150. package/dist/src/session/continuity.js.map +1 -0
  151. package/dist/src/session/errors.d.ts +9 -0
  152. package/dist/src/session/errors.d.ts.map +1 -0
  153. package/dist/src/session/errors.js +31 -0
  154. package/dist/src/session/errors.js.map +1 -0
  155. package/dist/src/session/local-continuity.d.ts +16 -0
  156. package/dist/src/session/local-continuity.d.ts.map +1 -0
  157. package/dist/src/session/local-continuity.js +146 -0
  158. package/dist/src/session/local-continuity.js.map +1 -0
  159. package/dist/src/session/signal-handler.d.ts +24 -0
  160. package/dist/src/session/signal-handler.d.ts.map +1 -0
  161. package/dist/src/session/signal-handler.js +35 -0
  162. package/dist/src/session/signal-handler.js.map +1 -0
  163. package/dist/src/shared/local-recovery-policy.d.ts +40 -0
  164. package/dist/src/shared/local-recovery-policy.d.ts.map +1 -0
  165. package/dist/src/shared/local-recovery-policy.js +59 -0
  166. package/dist/src/shared/local-recovery-policy.js.map +1 -0
  167. package/dist/src/shared/recovery-state.d.ts +3 -0
  168. package/dist/src/shared/recovery-state.d.ts.map +1 -0
  169. package/dist/src/shared/recovery-state.js +9 -0
  170. package/dist/src/shared/recovery-state.js.map +1 -0
  171. package/dist/src/types.d.ts +131 -0
  172. package/dist/src/types.d.ts.map +1 -0
  173. package/dist/src/types.js +5 -0
  174. package/dist/src/types.js.map +1 -0
  175. package/dist/src/utils.d.ts +50 -0
  176. package/dist/src/utils.d.ts.map +1 -0
  177. package/dist/src/utils.js +147 -0
  178. package/dist/src/utils.js.map +1 -0
  179. package/dist/src/working-location.d.ts +107 -0
  180. package/dist/src/working-location.d.ts.map +1 -0
  181. package/dist/src/working-location.js +651 -0
  182. package/dist/src/working-location.js.map +1 -0
  183. package/package.json +65 -0
@@ -0,0 +1,1107 @@
1
+ #!/bin/bash
2
+ # browse — Portable browser automation for AI agent brains
3
+ #
4
+ # Modes:
5
+ # LOCAL: Controls your Chrome browser via the native messaging host
6
+ # Port auto-discovered from ~/.thinkbrowse/port (written by native host on startup)
7
+ # CLOUD: Controls a cloud Playwright browser via ThinkRun REST API
8
+ #
9
+ # Mode detection (in order):
10
+ # 1. THINKRUN_LOCAL=true → local mode
11
+ # 2. Native host responding on discovered port → local mode
12
+ # 3. THINKRUN_API_KEY or config file → cloud mode
13
+ #
14
+ # Deps: curl, jq
15
+ #
16
+ # Usage:
17
+ # browse session-create Create session, print ID
18
+ # browse session-artifacts <sid> List cloud session artifacts
19
+ # browse artifact-download <id> [out] Download artifact by ID (cloud mode)
20
+ # browse goto <sid> <url> Navigate to URL
21
+ # browse click <sid> <selector> Click element
22
+ # browse extract <sid> [text|html] Extract page content
23
+ # browse session-delete <sid> Close session
24
+
25
+ set -euo pipefail
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+
29
+ # --- Config ---
30
+ CONFIG_FILE="$HOME/.config/thinkrun/config.json"
31
+ PORT_DISCOVERY_FILE="$HOME/.thinkbrowse/port"
32
+ TAB_DIR="/tmp/thinkrun-tabs"
33
+ LOCAL_HOST="${THINKRUN_LOCAL_HOST:-127.0.0.1}"
34
+ if [ "$LOCAL_HOST" = "localhost" ]; then
35
+ echo "WARN: localhost is non-deterministic (IPv4/IPv6); using 127.0.0.1 for ThinkRun bridge." >&2
36
+ LOCAL_HOST="127.0.0.1"
37
+ fi
38
+
39
+ # Port discovery: THINKRUN_LOCAL_PORT > THINKRUN_BRIDGE_PORT (legacy) > discovery file > default 3012
40
+ resolve_local_port() {
41
+ if [ -n "${THINKRUN_LOCAL_PORT:-}" ]; then
42
+ echo "$THINKRUN_LOCAL_PORT"
43
+ return
44
+ fi
45
+ if [ -n "${THINKRUN_BRIDGE_PORT:-}" ]; then
46
+ echo "$THINKRUN_BRIDGE_PORT"
47
+ return
48
+ fi
49
+ if [ -f "$PORT_DISCOVERY_FILE" ]; then
50
+ local port
51
+ port=$(cat "$PORT_DISCOVERY_FILE" 2>/dev/null | tr -d '[:space:]')
52
+ if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -gt 0 ] && [ "$port" -le 65535 ]; then
53
+ echo "$port"
54
+ return
55
+ fi
56
+ fi
57
+ echo "3012"
58
+ }
59
+
60
+ LOCAL_PORT=$(resolve_local_port)
61
+ LOCAL_URL="http://$LOCAL_HOST:$LOCAL_PORT"
62
+
63
+ # Local-bridge timing contract. Keep these values aligned with
64
+ # packages/cli/src/local-bridge-timing.ts when changing retry behavior.
65
+ LOCAL_SAFE_READ_RETRY_MAX_ATTEMPTS="${THINKRUN_LOCAL_SAFE_READ_RETRY_MAX_ATTEMPTS:-3}"
66
+ LOCAL_SAFE_READ_RETRY_INITIAL_DELAY_S="${THINKRUN_LOCAL_SAFE_READ_RETRY_INITIAL_DELAY_S:-1}"
67
+ LOCAL_SAFE_READ_RETRY_BACKOFF_MULTIPLIER="${THINKRUN_LOCAL_SAFE_READ_RETRY_BACKOFF_MULTIPLIER:-2}"
68
+
69
+ is_thinkbrowse_health_response() {
70
+ local health_json="$1"
71
+ echo "$health_json" | jq -e '
72
+ .success == true
73
+ and (.data | type == "object")
74
+ and (.data.status == "ok")
75
+ and (.data.service == "thinkbrowse-native-host")
76
+ and ((.data.extensionConnected | type) == "boolean")
77
+ ' >/dev/null 2>&1
78
+ }
79
+
80
+ probe_local_bridge_health() {
81
+ local health_json
82
+ health_json=$(curl -4 -s --max-time 2 "$LOCAL_URL/health" 2>/dev/null || true)
83
+ if [ -z "$health_json" ]; then
84
+ return 1
85
+ fi
86
+ is_thinkbrowse_health_response "$health_json"
87
+ }
88
+
89
+ # --- Parse --mode flag before mode detection ---
90
+ MODE_OVERRIDE=""
91
+ ARGS=()
92
+ while [[ $# -gt 0 ]]; do
93
+ case "$1" in
94
+ --mode)
95
+ MODE_OVERRIDE="${2:-}"
96
+ shift 2
97
+ ;;
98
+ --mode=*)
99
+ MODE_OVERRIDE="${1#--mode=}"
100
+ shift
101
+ ;;
102
+ *)
103
+ ARGS+=("$1")
104
+ shift
105
+ ;;
106
+ esac
107
+ done
108
+ # Restore positional parameters without --mode flags
109
+ set -- "${ARGS[@]+"${ARGS[@]}"}"
110
+
111
+ # --- Mode Detection ---
112
+ MODE=""
113
+
114
+ if [ -n "$MODE_OVERRIDE" ]; then
115
+ case "$MODE_OVERRIDE" in
116
+ native|local) MODE="local" ;; # native is now an alias for local
117
+ cloud) MODE="cloud" ;;
118
+ *)
119
+ echo "ERROR: Unknown --mode value '$MODE_OVERRIDE' (must be local or cloud)" >&2
120
+ exit 1
121
+ ;;
122
+ esac
123
+ elif [ "${THINKRUN_LOCAL:-}" = "true" ]; then
124
+ MODE="local"
125
+ elif probe_local_bridge_health; then
126
+ MODE="local"
127
+ else
128
+ # Try cloud mode
129
+ API_KEY=""
130
+ if [ -n "${THINKRUN_API_KEY:-}" ]; then
131
+ API_KEY="$THINKRUN_API_KEY"
132
+ elif [ -f "$CONFIG_FILE" ]; then
133
+ API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE" 2>/dev/null)
134
+ fi
135
+
136
+ if [ -n "${API_KEY:-}" ]; then
137
+ MODE="cloud"
138
+ else
139
+ echo "ERROR: No browser automation endpoint or API key found." >&2
140
+ echo " Local mode: Open Chrome/Helium (Chromium) with ThinkRun extension + native host installed" >&2
141
+ echo " Cloud mode: Set THINKRUN_API_KEY or create $CONFIG_FILE" >&2
142
+ exit 1
143
+ fi
144
+ fi
145
+
146
+ # --- Cloud config ---
147
+ if [ "$MODE" = "cloud" ]; then
148
+ # Resolve API key for explicit --mode cloud and auto-detected cloud mode.
149
+ if [ -n "${THINKRUN_API_KEY:-}" ]; then
150
+ API_KEY="$THINKRUN_API_KEY"
151
+ elif [ -f "$CONFIG_FILE" ]; then
152
+ API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE" 2>/dev/null)
153
+ else
154
+ API_KEY=""
155
+ fi
156
+ if [ -z "${API_KEY:-}" ]; then
157
+ echo "ERROR: Cloud mode requires THINKRUN_API_KEY or $CONFIG_FILE" >&2
158
+ exit 1
159
+ fi
160
+ # Read apiUrl from config file, fall back to env var or default
161
+ CONFIG_API_URL=""
162
+ if [ -f "$CONFIG_FILE" ]; then
163
+ CONFIG_API_URL=$(jq -r '.apiUrl // empty' "$CONFIG_FILE" 2>/dev/null)
164
+ fi
165
+ BASE="${THINKRUN_URL:-${CONFIG_API_URL:-https://api.thinkbrowse.io}}"
166
+ H=(-H "X-API-Key: $API_KEY" -H "Content-Type: application/json")
167
+ fi
168
+
169
+ # --- Local session sync helpers ---
170
+ LOCAL_SYNC_AUTH_INITIALIZED="false"
171
+ LOCAL_SYNC_API_KEY=""
172
+ LOCAL_SYNC_BASE=""
173
+
174
+ init_local_sync_auth() {
175
+ if [ "$LOCAL_SYNC_AUTH_INITIALIZED" = "true" ]; then
176
+ return
177
+ fi
178
+
179
+ local cfg_api_key=""
180
+ local cfg_api_url=""
181
+ if [ -f "$CONFIG_FILE" ]; then
182
+ cfg_api_key=$(jq -r '.apiKey // empty' "$CONFIG_FILE" 2>/dev/null)
183
+ cfg_api_url=$(jq -r '.apiUrl // empty' "$CONFIG_FILE" 2>/dev/null)
184
+ fi
185
+
186
+ LOCAL_SYNC_API_KEY="${THINKRUN_API_KEY:-$cfg_api_key}"
187
+ LOCAL_SYNC_BASE="${THINKRUN_URL:-${cfg_api_url:-https://api.thinkbrowse.io}}"
188
+ LOCAL_SYNC_AUTH_INITIALIZED="true"
189
+ }
190
+
191
+ can_sync_local_session() {
192
+ local sid="$1"
193
+ [ "$MODE" = "local" ] || return 1
194
+ [[ "$sid" == local-* ]] && return 1
195
+ init_local_sync_auth
196
+ [ -n "${LOCAL_SYNC_API_KEY:-}" ]
197
+ }
198
+
199
+ sync_local_action_event() {
200
+ local sid="$1"
201
+ local action_type="$2"
202
+ local success="$3"
203
+ local details_json="${4:-{}}"
204
+ local error_msg="${5:-}"
205
+ local artifact_b64="${6:-}"
206
+
207
+ can_sync_local_session "$sid" || return 0
208
+
209
+ local payload
210
+ payload=$(jq -cn \
211
+ --arg type "$action_type" \
212
+ --arg success "$success" \
213
+ --arg details_raw "$details_json" \
214
+ --arg error "$error_msg" \
215
+ --arg artifact "$artifact_b64" \
216
+ '{
217
+ type: $type,
218
+ success: ($success == "true"),
219
+ details: ($details_raw | fromjson? // {})
220
+ }
221
+ + (if ($error|length) > 0 then { error: $error } else {} end)
222
+ + (if ($artifact|length) > 0 then { artifactBase64: $artifact, artifactMimeType: "image/png" } else {} end)
223
+ ')
224
+
225
+ curl -s \
226
+ -H "X-API-Key: $LOCAL_SYNC_API_KEY" \
227
+ -H "Content-Type: application/json" \
228
+ -X POST "$LOCAL_SYNC_BASE/api/sessions/$sid/local-sync/action" \
229
+ -d "$payload" >/dev/null 2>&1 || true
230
+ }
231
+
232
+ maybe_sync_local_command() {
233
+ local sid="$1"
234
+ local http_code="$2"
235
+ local body="$3"
236
+ shift 3
237
+
238
+ can_sync_local_session "$sid" || return 0
239
+
240
+ local method="POST"
241
+ local req_data=""
242
+ local req_url=""
243
+ local prev=""
244
+ for arg in "$@"; do
245
+ if [ "$prev" = "-X" ]; then
246
+ method="$arg"
247
+ prev=""
248
+ continue
249
+ fi
250
+ if [ "$prev" = "-d" ]; then
251
+ req_data="$arg"
252
+ prev=""
253
+ continue
254
+ fi
255
+ case "$arg" in
256
+ -X|-d)
257
+ prev="$arg"
258
+ ;;
259
+ http://*|https://*)
260
+ req_url="$arg"
261
+ ;;
262
+ esac
263
+ done
264
+
265
+ [ -n "$req_url" ] || return 0
266
+ [ "$method" = "POST" ] || return 0
267
+
268
+ local endpoint="${req_url##*/api/}"
269
+ local action_type=""
270
+ case "$endpoint" in
271
+ navigate) action_type="navigate" ;;
272
+ click) action_type="click" ;;
273
+ fill) action_type="fill" ;;
274
+ type) action_type="type" ;;
275
+ press) action_type="press" ;;
276
+ hover) action_type="hover" ;;
277
+ select) action_type="select" ;;
278
+ wait) action_type="wait" ;;
279
+ wait-for-text) action_type="wait" ;;
280
+ evaluate) action_type="evaluate" ;;
281
+ extract) action_type="extract" ;;
282
+ snapshot) action_type="snapshot" ;;
283
+ screenshot) action_type="screenshot" ;;
284
+ scroll) action_type="scroll" ;;
285
+ go-back) action_type="go-back" ;;
286
+ go-forward) action_type="go-forward" ;;
287
+ *) return 0 ;;
288
+ esac
289
+
290
+ local details_json="{}"
291
+ if [ -n "$req_data" ]; then
292
+ details_json=$(printf '%s' "$req_data" | jq -c '.' 2>/dev/null || echo '{}')
293
+ fi
294
+ details_json=$(jq -cn --arg endpoint "$endpoint" --arg details_raw "$details_json" '{ endpoint: $endpoint } + ($details_raw | fromjson? // {})')
295
+
296
+ local success_bool error_msg artifact_b64
297
+ success_bool=$(echo "$body" | jq -r 'if (.success == false) then "false" else "true" end' 2>/dev/null || echo "true")
298
+ error_msg=$(echo "$body" | jq -r '.error // .message // empty' 2>/dev/null || echo "")
299
+ artifact_b64=""
300
+ if [ "$action_type" = "screenshot" ]; then
301
+ artifact_b64=$(echo "$body" | jq -r '.data.screenshot // .screenshot // empty' 2>/dev/null || echo "")
302
+ fi
303
+
304
+ sync_local_action_event "$sid" "$action_type" "$success_bool" "$details_json" "$error_msg" "$artifact_b64"
305
+ }
306
+
307
+ # --- Local helpers ---
308
+
309
+ # Check native host is reachable
310
+ ensure_local() {
311
+ local health_json
312
+ health_json=$(curl -4 -s --max-time 2 "$LOCAL_URL/health" 2>/dev/null || true)
313
+
314
+ if [ -z "$health_json" ]; then
315
+ echo "ERROR: Native host not responding on $LOCAL_URL" >&2
316
+ echo " Make sure Chrome is open with the ThinkRun extension installed." >&2
317
+ exit 1
318
+ fi
319
+
320
+ if ! is_thinkbrowse_health_response "$health_json"; then
321
+ echo "ERROR: Service on $LOCAL_URL is not the ThinkRun native host." >&2
322
+ echo " Received /health payload: $health_json" >&2
323
+ echo " This usually means another local service is using port $LOCAL_PORT" >&2
324
+ echo " or localhost/IPv6 resolved to a different process." >&2
325
+ if command -v lsof >/dev/null 2>&1; then
326
+ echo " Listening processes on :$LOCAL_PORT:" >&2
327
+ lsof -nP -iTCP:"$LOCAL_PORT" -sTCP:LISTEN >&2 || true
328
+ fi
329
+ echo " Fix: keep ThinkRun on $LOCAL_HOST:$LOCAL_PORT, move conflicting services," >&2
330
+ echo " or override host/port with THINKRUN_LOCAL_HOST / THINKRUN_LOCAL_PORT." >&2
331
+ exit 1
332
+ fi
333
+ }
334
+
335
+ # Save tab ID for a local session
336
+ save_tab_id() {
337
+ local sid="$1" tab_id="$2"
338
+ mkdir -p "$TAB_DIR"
339
+ echo "$tab_id" > "$TAB_DIR/$sid"
340
+ }
341
+
342
+ # Load tab ID for a local session
343
+ load_tab_id() {
344
+ local sid="$1"
345
+ local tab_file="$TAB_DIR/$sid"
346
+ if [ -f "$tab_file" ]; then
347
+ cat "$tab_file"
348
+ return
349
+ fi
350
+ # Fallback: if sid is a numeric Chrome tab ID, verify it exists and pin it
351
+ if [[ "$sid" =~ ^[0-9]+$ ]]; then
352
+ local tabs_json
353
+ tabs_json=$(curl -sf --max-time 5 "http://${LOCAL_HOST}:${LOCAL_PORT}/api/tabs" -H "Accept: application/json" 2>/dev/null)
354
+ if [ -n "$tabs_json" ]; then
355
+ local match
356
+ match=$(echo "$tabs_json" | jq -r --arg id "$sid" '.data.tabs[] | select(.id == ($id | tonumber)) | .id' 2>/dev/null)
357
+ if [ -n "$match" ] && [ "$match" != "null" ]; then
358
+ # Auto-pin: record the raw numeric tab ID as its own session mapping
359
+ save_tab_id "$sid" "$match"
360
+ echo "$match"
361
+ return
362
+ fi
363
+ fi
364
+ fi
365
+ echo "ERROR: No tab found for session $sid" >&2
366
+ exit 1
367
+ }
368
+
369
+ # Remove tab ID file for a local session
370
+ remove_tab_id() {
371
+ local sid="$1"
372
+ rm -f "$TAB_DIR/$sid"
373
+ }
374
+
375
+ local_retry_profile() {
376
+ local endpoint="$1"
377
+ case "$endpoint" in
378
+ snapshot|screenshot|extract|wait|wait-for-text|console|network|url|title|html)
379
+ echo "safe_same_bridge"
380
+ ;;
381
+ navigate|click|fill|type|press|hover|select|evaluate|scroll|go-back|go-forward|dialog|clear-logs)
382
+ echo "unknown_outcome"
383
+ ;;
384
+ *)
385
+ echo "unknown_outcome"
386
+ ;;
387
+ esac
388
+ }
389
+
390
+ local_retry_endpoint_from_args() {
391
+ local req_url=""
392
+ for arg in "$@"; do
393
+ case "$arg" in
394
+ http://*|https://*)
395
+ req_url="$arg"
396
+ ;;
397
+ esac
398
+ done
399
+ req_url="${req_url##*/api/}"
400
+ echo "$req_url"
401
+ }
402
+
403
+ # curl wrapper for local/native HTTP endpoints with tab pinning + retry
404
+ local_curl() {
405
+ local sid="$1"; shift
406
+ local tab_id
407
+ tab_id=$(load_tab_id "$sid")
408
+ local max_retries="$LOCAL_SAFE_READ_RETRY_MAX_ATTEMPTS"
409
+ local delay="$LOCAL_SAFE_READ_RETRY_INITIAL_DELAY_S"
410
+ local endpoint retry_profile
411
+ endpoint=$(local_retry_endpoint_from_args "$@")
412
+ retry_profile=$(local_retry_profile "$endpoint")
413
+ for attempt in $(seq 1 $max_retries); do
414
+ RESP=$(curl -s -w "\n%{http_code}" -H "Content-Type: application/json" -H "X-Tab-Id: $tab_id" "$@")
415
+ HTTP_CODE=$(echo "$RESP" | tail -1)
416
+ BODY=$(echo "$RESP" | sed '$d')
417
+
418
+ # Check for TAB_NOT_FOUND — don't retry, clean up session
419
+ CODE=$(echo "$BODY" | jq -r '.code // empty' 2>/dev/null)
420
+ if [ "$CODE" = "TAB_NOT_FOUND" ]; then
421
+ echo "ERROR: Session tab was closed" >&2
422
+ remove_tab_id "$sid"
423
+ exit 1
424
+ fi
425
+
426
+ # Check for retryable conditions (structured field or HTTP status)
427
+ RETRYABLE=$(echo "$BODY" | jq -r '.retryable // false' 2>/dev/null)
428
+ RECOVERY_STATE=$(echo "$BODY" | jq -r '.data.recoveryState // .recoveryState // empty' 2>/dev/null)
429
+ case "$HTTP_CODE" in
430
+ 503|504) RETRYABLE=true ;;
431
+ esac
432
+ if [ "$RECOVERY_STATE" = "flapping" ] && [ "$RETRYABLE" = "true" ]; then
433
+ echo "WARN: local bridge is flapping from repeated disconnects; suppressing patient retries for '$endpoint'. Reload the ThinkRun extension or stabilize the page/browser before retrying." >&2
434
+ RETRYABLE=false
435
+ fi
436
+ if [ "$RETRYABLE" = "true" ] && [ "$attempt" -lt "$max_retries" ] && [ "$retry_profile" = "safe_same_bridge" ]; then
437
+ sleep $delay
438
+ delay=$((delay * LOCAL_SAFE_READ_RETRY_BACKOFF_MULTIPLIER))
439
+ continue
440
+ fi
441
+ if [ "$RETRYABLE" = "true" ] && [ "$retry_profile" != "safe_same_bridge" ]; then
442
+ echo "WARN: local command '$endpoint' was not retried after transport failure because the outcome may be unknown; verify page state before retrying." >&2
443
+ fi
444
+ maybe_sync_local_command "$sid" "$HTTP_CODE" "$BODY" "$@"
445
+ echo "$BODY"
446
+ return 0
447
+ done
448
+ maybe_sync_local_command "$sid" "$HTTP_CODE" "$BODY" "$@"
449
+ echo "$BODY"
450
+ }
451
+
452
+ # --- Cloud helpers ---
453
+
454
+ # Retry helper for transient errors (Fly routing, timeouts, 502/503/504/524)
455
+ retry_curl() {
456
+ local max_retries=4
457
+ local delay=1
458
+ for attempt in $(seq 1 $max_retries); do
459
+ RESP=$(curl -s -w "\n%{http_code}" "${H[@]}" "$@")
460
+ HTTP_CODE=$(echo "$RESP" | tail -1)
461
+ BODY=$(echo "$RESP" | sed '$d')
462
+
463
+ # Check for retryable conditions
464
+ SHOULD_RETRY=false
465
+
466
+ # Structured retryable field from bridge/cloud API
467
+ RETRYABLE=$(echo "$BODY" | jq -r '.retryable // false' 2>/dev/null)
468
+ if [ "$RETRYABLE" = "true" ]; then
469
+ SHOULD_RETRY=true
470
+ fi
471
+
472
+ # HTTP status codes that indicate transient failures
473
+ case "$HTTP_CODE" in
474
+ 502|503|504|524) SHOULD_RETRY=true ;;
475
+ esac
476
+
477
+ # Legacy error string matching
478
+ ERROR=$(echo "$BODY" | jq -r '.error // empty' 2>/dev/null)
479
+ case "$ERROR" in
480
+ "Session not found"|"Session is still provisioning. Please wait for it to become active.")
481
+ SHOULD_RETRY=true ;;
482
+ esac
483
+
484
+ if [ "$SHOULD_RETRY" = "true" ] && [ "$attempt" -lt "$max_retries" ]; then
485
+ sleep $delay
486
+ delay=$((delay * 2))
487
+ continue
488
+ fi
489
+
490
+ echo "$BODY"
491
+ return 0
492
+ done
493
+ echo "$BODY"
494
+ }
495
+
496
+ # Validate local bridge identity once before command dispatch.
497
+ if [ "$MODE" = "local" ]; then
498
+ ensure_local
499
+ fi
500
+
501
+ # --- Commands ---
502
+ case "${1:-help}" in
503
+
504
+ # ========== Session Management ==========
505
+
506
+ session-create)
507
+ if [ "$MODE" = "local" ]; then
508
+ ensure_local
509
+ # Create a new tab via native host
510
+ RESP=$(curl -s -H "Content-Type: application/json" -X POST "$LOCAL_URL/api/tabs/new" \
511
+ -d '{"url":"about:blank"}')
512
+ SUCCESS=$(echo "$RESP" | jq -r '.success // false')
513
+ if [ "$SUCCESS" != "true" ]; then
514
+ echo "ERROR: Failed to create tab (mode: $MODE)" >&2
515
+ echo "$RESP" >&2
516
+ exit 1
517
+ fi
518
+ # Extract tab ID from response
519
+ TAB_ID=$(echo "$RESP" | jq -r '.data.tabId // .data.id // empty')
520
+ if [ -z "$TAB_ID" ]; then
521
+ echo "ERROR: No tab ID in response (mode: $MODE)" >&2
522
+ echo "$RESP" >&2
523
+ exit 1
524
+ fi
525
+ # Prefer backend-issued session IDs for local execution when API auth is available.
526
+ LOCAL_API_KEY=""
527
+ LOCAL_API_URL=""
528
+ if [ -n "${THINKRUN_API_KEY:-}" ]; then
529
+ LOCAL_API_KEY="$THINKRUN_API_KEY"
530
+ elif [ -f "$CONFIG_FILE" ]; then
531
+ LOCAL_API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE" 2>/dev/null)
532
+ fi
533
+ if [ -f "$CONFIG_FILE" ]; then
534
+ LOCAL_API_URL=$(jq -r '.apiUrl // empty' "$CONFIG_FILE" 2>/dev/null)
535
+ fi
536
+ LOCAL_BASE="${THINKRUN_URL:-${LOCAL_API_URL:-https://api.thinkbrowse.io}}"
537
+
538
+ SID=""
539
+ if [ -n "$LOCAL_API_KEY" ]; then
540
+ BACKEND_RESP=$(curl -s -H "X-API-Key: $LOCAL_API_KEY" -H "Content-Type: application/json" \
541
+ -X POST "$LOCAL_BASE/api/sessions/local" -d '{}')
542
+ SID=$(echo "$BACKEND_RESP" | jq -r '.data.sessionId // .sessionId // empty')
543
+ fi
544
+ if [ -z "$SID" ]; then
545
+ SID="local-$TAB_ID"
546
+ fi
547
+ save_tab_id "$SID" "$TAB_ID"
548
+ echo "$SID"
549
+ else
550
+ RESP=$(curl -s "${H[@]}" -X POST "$BASE/api/sessions" -d '{}')
551
+ SID=$(echo "$RESP" | jq -r '.data.sessionId // .sessionId // empty')
552
+ if [ -z "$SID" ]; then
553
+ echo "ERROR: Session creation failed" >&2
554
+ echo "$RESP" >&2
555
+ exit 1
556
+ fi
557
+ # Poll until ready (max 120s for cold machine start)
558
+ for i in $(seq 1 40); do
559
+ STATUS=$(curl -s "${H[@]}" "$BASE/api/sessions/$SID" | jq -r '.data.status // .status // "unknown"')
560
+ case "$STATUS" in
561
+ ready|active|running)
562
+ echo "$SID"
563
+ exit 0
564
+ ;;
565
+ creating|provisioning|pending)
566
+ printf "." >&2
567
+ sleep 3
568
+ ;;
569
+ failed|error|terminated)
570
+ echo "" >&2
571
+ echo "ERROR: Session $SID failed with status: $STATUS" >&2
572
+ exit 1
573
+ ;;
574
+ esac
575
+ done
576
+ echo "" >&2
577
+ echo "ERROR: Session $SID not ready after 120s" >&2
578
+ exit 1
579
+ fi
580
+ ;;
581
+
582
+ session-status)
583
+ if [ "$MODE" = "local" ]; then
584
+ ensure_local
585
+ TAB_ID=$(load_tab_id "$2")
586
+ curl -s -H "Content-Type: application/json" -H "X-Tab-Id: $TAB_ID" "$LOCAL_URL/sessions/$TAB_ID"
587
+ else
588
+ curl -s "${H[@]}" "$BASE/api/sessions/$2"
589
+ fi
590
+ ;;
591
+
592
+ session-delete)
593
+ if [ "$MODE" = "local" ]; then
594
+ TAB_ID=$(load_tab_id "$2")
595
+ curl -s -H "Content-Type: application/json" -X POST "$LOCAL_URL/api/tabs/close" \
596
+ -d "{\"tabId\":$TAB_ID}"
597
+ remove_tab_id "$2"
598
+ else
599
+ curl -s "${H[@]}" -X DELETE "$BASE/api/sessions/$2"
600
+ fi
601
+ ;;
602
+
603
+ session-details)
604
+ if [ "$MODE" = "local" ]; then
605
+ if can_sync_local_session "$2"; then
606
+ BACKEND_DETAILS=$(curl -s -H "X-API-Key: $LOCAL_SYNC_API_KEY" -H "Content-Type: application/json" \
607
+ -X GET "$LOCAL_SYNC_BASE/api/sessions/$2/details" 2>/dev/null || true)
608
+ if [ -n "$BACKEND_DETAILS" ] && [ "$(echo "$BACKEND_DETAILS" | jq -r '.success // false' 2>/dev/null)" = "true" ]; then
609
+ echo "$BACKEND_DETAILS"
610
+ exit 0
611
+ fi
612
+ fi
613
+ TAB_ID=$(load_tab_id "$2")
614
+ SESSION_JSON=$(curl -s -H "Content-Type: application/json" -H "X-Tab-Id: $TAB_ID" "$LOCAL_URL/sessions/$TAB_ID")
615
+ URL_VAL=$(echo "$SESSION_JSON" | jq -r '.data.url // ""')
616
+ TITLE_VAL=$(echo "$SESSION_JSON" | jq -r '.data.title // ""')
617
+ jq -n \
618
+ --arg sid "$2" \
619
+ --arg tabId "$TAB_ID" \
620
+ --arg url "$URL_VAL" \
621
+ --arg title "$TITLE_VAL" \
622
+ '{
623
+ success: true,
624
+ session: {
625
+ sessionId: $sid,
626
+ executionMode: "local",
627
+ executor: "native-host",
628
+ status: "active",
629
+ tabId: ($tabId|tonumber),
630
+ currentUrl: $url,
631
+ title: $title,
632
+ artifacts: [],
633
+ actions: [],
634
+ storageBacked: false
635
+ }
636
+ }'
637
+ else
638
+ retry_curl -X GET "$BASE/api/sessions/$2/details"
639
+ fi
640
+ ;;
641
+
642
+ session-actions)
643
+ if [ "$MODE" = "local" ]; then
644
+ if can_sync_local_session "$2"; then
645
+ LIMIT="${3:-50}"
646
+ OFFSET="${4:-0}"
647
+ TYPE="${5:-}"
648
+ ACTIONS_URL="$LOCAL_SYNC_BASE/api/sessions/$2/actions?limit=$LIMIT&offset=$OFFSET"
649
+ if [ -n "$TYPE" ]; then
650
+ ACTIONS_URL="${ACTIONS_URL}&type=$TYPE"
651
+ fi
652
+ BACKEND_ACTIONS=$(curl -s -H "X-API-Key: $LOCAL_SYNC_API_KEY" -H "Content-Type: application/json" \
653
+ -X GET "$ACTIONS_URL" 2>/dev/null || true)
654
+ if [ -n "$BACKEND_ACTIONS" ] && [ "$(echo "$BACKEND_ACTIONS" | jq -r '.success // false' 2>/dev/null)" = "true" ]; then
655
+ echo "$BACKEND_ACTIONS"
656
+ exit 0
657
+ fi
658
+ fi
659
+ TAB_ID=$(load_tab_id "$2")
660
+ CONSOLE_JSON=$(local_curl "$2" -X POST "$LOCAL_URL/api/console")
661
+ NETWORK_JSON=$(local_curl "$2" -X POST "$LOCAL_URL/api/network")
662
+ LIMIT="${3:-50}"
663
+ OFFSET="${4:-0}"
664
+ jq -n \
665
+ --arg sid "$2" \
666
+ --argjson limit "$LIMIT" \
667
+ --argjson offset "$OFFSET" \
668
+ --argjson console "$CONSOLE_JSON" \
669
+ --argjson network "$NETWORK_JSON" \
670
+ '{
671
+ success: true,
672
+ data: {
673
+ sessionId: $sid,
674
+ executionMode: "local",
675
+ actions: [],
676
+ console: ($console.data // []),
677
+ network: ($network.data // []),
678
+ total: 0,
679
+ successful: 0,
680
+ failed: 0,
681
+ hasMore: false,
682
+ note: "Local mode currently exposes console and network logs; action history persistence is cloud-only."
683
+ }
684
+ }'
685
+ else
686
+ LIMIT="${3:-50}"
687
+ OFFSET="${4:-0}"
688
+ TYPE="${5:-}"
689
+ ACTIONS_URL="$BASE/api/sessions/$2/actions?limit=$LIMIT&offset=$OFFSET"
690
+ if [ -n "$TYPE" ]; then
691
+ ACTIONS_URL="${ACTIONS_URL}&type=$TYPE"
692
+ fi
693
+ retry_curl -X GET "$ACTIONS_URL"
694
+ fi
695
+ ;;
696
+
697
+ session-artifacts)
698
+ if [ "$MODE" = "local" ]; then
699
+ if can_sync_local_session "$2"; then
700
+ BACKEND_ARTIFACTS=$(curl -s -H "X-API-Key: $LOCAL_SYNC_API_KEY" -H "Content-Type: application/json" \
701
+ -X GET "$LOCAL_SYNC_BASE/api/sessions/$2/artifacts" 2>/dev/null || true)
702
+ if [ -n "$BACKEND_ARTIFACTS" ] && [ "$(echo "$BACKEND_ARTIFACTS" | jq -r '.success // false' 2>/dev/null)" = "true" ]; then
703
+ echo "$BACKEND_ARTIFACTS"
704
+ exit 0
705
+ fi
706
+ fi
707
+ jq -n --arg sid "$2" '{
708
+ success: true,
709
+ sessionId: $sid,
710
+ executionMode: "local",
711
+ artifacts: [],
712
+ note: "Local mode does not persist artifacts to cloud storage. Use --mode cloud for reusable stored artifacts."
713
+ }'
714
+ else
715
+ retry_curl -X GET "$BASE/api/sessions/$2/artifacts"
716
+ fi
717
+ ;;
718
+
719
+ artifact-download)
720
+ if [ "$MODE" = "local" ]; then
721
+ echo "ERROR: artifact-download is available in cloud mode only (use --mode cloud)" >&2
722
+ exit 1
723
+ fi
724
+ ARTIFACT_ID="$2"
725
+ OUTFILE="${3:-artifact-$ARTIFACT_ID.bin}"
726
+ if ! curl -fsSL -H "X-API-Key: $API_KEY" "$BASE/api/storage/artifacts/$ARTIFACT_ID/download" -o "$OUTFILE"; then
727
+ echo "ERROR: Failed to download artifact $ARTIFACT_ID" >&2
728
+ exit 1
729
+ fi
730
+ echo "$OUTFILE"
731
+ ;;
732
+
733
+ # ========== Navigation ==========
734
+
735
+ goto)
736
+ if [ "$MODE" = "local" ]; then
737
+ local_curl "$2" -X POST "$LOCAL_URL/api/navigate" \
738
+ -d "{\"url\":$(printf '%s' "$3" | jq -Rs)}"
739
+ else
740
+ retry_curl -X POST "$BASE/api/sessions/$2/navigate" \
741
+ -d "{\"url\":$(printf '%s' "$3" | jq -Rs)}"
742
+ fi
743
+ ;;
744
+
745
+ scroll)
746
+ DIR="${3:-down}"
747
+ AMT="${4:-500}"
748
+ if ! [[ "$AMT" =~ ^[0-9]+$ ]]; then
749
+ echo "ERROR: Amount must be a positive integer" >&2
750
+ exit 1
751
+ fi
752
+ if [ "$MODE" = "local" ]; then
753
+ local_curl "$2" -X POST "$LOCAL_URL/api/scroll" \
754
+ -d "{\"direction\":\"$DIR\",\"amount\":$AMT}"
755
+ else
756
+ if [ "$DIR" = "up" ]; then
757
+ retry_curl -X POST "$BASE/api/sessions/$2/evaluate" \
758
+ -d "{\"script\":\"window.scrollBy(0, -$AMT)\"}"
759
+ else
760
+ retry_curl -X POST "$BASE/api/sessions/$2/evaluate" \
761
+ -d "{\"script\":\"window.scrollBy(0, $AMT)\"}"
762
+ fi
763
+ fi
764
+ ;;
765
+
766
+ # ========== Observation ==========
767
+
768
+ snapshot)
769
+ if [ "$MODE" = "local" ]; then
770
+ local_curl "$2" -X POST "$LOCAL_URL/api/snapshot" \
771
+ | jq -r '.data // .error // "No snapshot available"'
772
+ else
773
+ retry_curl -X POST "$BASE/api/sessions/$2/snapshot" \
774
+ | jq -r '.snapshot // .error // "No snapshot available"'
775
+ fi
776
+ ;;
777
+
778
+ screenshot)
779
+ if [ "$MODE" = "local" ]; then
780
+ local_curl "$2" -X POST "$LOCAL_URL/api/screenshot"
781
+ else
782
+ retry_curl -X POST "$BASE/api/sessions/$2/screenshot"
783
+ fi
784
+ ;;
785
+
786
+ extract)
787
+ TYPE="${3:-text}"
788
+ if [ "$MODE" = "local" ]; then
789
+ local_curl "$2" -X POST "$LOCAL_URL/api/extract" \
790
+ -d "{\"type\":$(printf '%s' "$TYPE" | jq -Rs)}"
791
+ else
792
+ retry_curl -X POST "$BASE/api/sessions/$2/extract" \
793
+ -d "{\"type\":$(printf '%s' "$TYPE" | jq -Rs)}"
794
+ fi
795
+ ;;
796
+
797
+ # ========== Interaction ==========
798
+
799
+ click)
800
+ if [ "$MODE" = "local" ]; then
801
+ local_curl "$2" -X POST "$LOCAL_URL/api/click" \
802
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs)}"
803
+ else
804
+ retry_curl -X POST "$BASE/api/sessions/$2/click" \
805
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs)}"
806
+ fi
807
+ ;;
808
+
809
+ fill)
810
+ if [ "$MODE" = "local" ]; then
811
+ local_curl "$2" -X POST "$LOCAL_URL/api/fill" \
812
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"value\":$(printf '%s' "$4" | jq -Rs)}"
813
+ else
814
+ retry_curl -X POST "$BASE/api/sessions/$2/fill" \
815
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"value\":$(printf '%s' "$4" | jq -Rs)}"
816
+ fi
817
+ ;;
818
+
819
+ type)
820
+ if [ "$MODE" = "local" ]; then
821
+ local_curl "$2" -X POST "$LOCAL_URL/api/type" \
822
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"text\":$(printf '%s' "$4" | jq -Rs)}"
823
+ else
824
+ retry_curl -X POST "$BASE/api/sessions/$2/type" \
825
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"text\":$(printf '%s' "$4" | jq -Rs)}"
826
+ fi
827
+ ;;
828
+
829
+ press)
830
+ if [ "$MODE" = "local" ]; then
831
+ local_curl "$2" -X POST "$LOCAL_URL/api/press" \
832
+ -d "{\"key\":$(printf '%s' "$3" | jq -Rs)}"
833
+ else
834
+ retry_curl -X POST "$BASE/api/sessions/$2/press" \
835
+ -d "{\"key\":$(printf '%s' "$3" | jq -Rs)}"
836
+ fi
837
+ ;;
838
+
839
+ hover)
840
+ if [ "$MODE" = "local" ]; then
841
+ local_curl "$2" -X POST "$LOCAL_URL/api/hover" \
842
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs)}"
843
+ else
844
+ retry_curl -X POST "$BASE/api/sessions/$2/hover" \
845
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs)}"
846
+ fi
847
+ ;;
848
+
849
+ select)
850
+ if [ "$MODE" = "local" ]; then
851
+ local_curl "$2" -X POST "$LOCAL_URL/api/select" \
852
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"value\":$(printf '%s' "$4" | jq -Rs)}"
853
+ else
854
+ retry_curl -X POST "$BASE/api/sessions/$2/select" \
855
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"value\":$(printf '%s' "$4" | jq -Rs)}"
856
+ fi
857
+ ;;
858
+
859
+ wait)
860
+ TIMEOUT="${4:-10000}"
861
+ if ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then
862
+ echo "ERROR: Timeout must be a positive integer (ms)" >&2
863
+ exit 1
864
+ fi
865
+ if [ "$MODE" = "local" ]; then
866
+ local_curl "$2" -X POST "$LOCAL_URL/api/wait" \
867
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"timeout\":$TIMEOUT}"
868
+ else
869
+ retry_curl -X POST "$BASE/api/sessions/$2/wait" \
870
+ -d "{\"selector\":$(printf '%s' "$3" | jq -Rs),\"timeout\":$TIMEOUT}"
871
+ fi
872
+ ;;
873
+
874
+ evaluate)
875
+ if [ "$MODE" = "local" ]; then
876
+ local_curl "$2" -X POST "$LOCAL_URL/api/evaluate" \
877
+ -d "{\"script\":$(printf '%s' "$3" | jq -Rs)}"
878
+ else
879
+ retry_curl -X POST "$BASE/api/sessions/$2/evaluate" \
880
+ -d "{\"script\":$(printf '%s' "$3" | jq -Rs)}"
881
+ fi
882
+ ;;
883
+
884
+ # ========== Navigation History ==========
885
+
886
+ back)
887
+ if [ "$MODE" = "local" ]; then
888
+ local_curl "$2" -X POST "$LOCAL_URL/api/go-back"
889
+ else
890
+ retry_curl -X POST "$BASE/api/sessions/$2/go-back"
891
+ fi
892
+ ;;
893
+
894
+ forward)
895
+ if [ "$MODE" = "local" ]; then
896
+ local_curl "$2" -X POST "$LOCAL_URL/api/go-forward"
897
+ else
898
+ retry_curl -X POST "$BASE/api/sessions/$2/go-forward"
899
+ fi
900
+ ;;
901
+
902
+ # ========== Dialog Handling ==========
903
+
904
+ dialog)
905
+ local dialog_action="${3:-status}"
906
+ case "$dialog_action" in
907
+ status)
908
+ if [ "$MODE" = "local" ]; then
909
+ local_curl "$2" -X POST "$LOCAL_URL/api/dialog"
910
+ else
911
+ retry_curl -X GET "$BASE/api/sessions/$2/dialog"
912
+ fi
913
+ ;;
914
+ accept)
915
+ if [ "$MODE" = "local" ]; then
916
+ echo '{"info":"Dialogs are auto-handled in local mode"}'
917
+ local_curl "$2" -X POST "$LOCAL_URL/api/dialog"
918
+ else
919
+ retry_curl -X POST "$BASE/api/sessions/$2/dialog" \
920
+ -d "{\"accept\":true$([ -n "${4:-}" ] && echo ",\"promptText\":$(printf '%s' "$4" | jq -Rs)" || echo "")}"
921
+ fi
922
+ ;;
923
+ dismiss)
924
+ if [ "$MODE" = "local" ]; then
925
+ echo '{"info":"Dialogs are auto-handled in local mode"}'
926
+ local_curl "$2" -X POST "$LOCAL_URL/api/dialog"
927
+ else
928
+ retry_curl -X POST "$BASE/api/sessions/$2/dialog" \
929
+ -d '{"accept":false}'
930
+ fi
931
+ ;;
932
+ *)
933
+ echo "ERROR: Unknown dialog action: $dialog_action (use status, accept, dismiss)" >&2
934
+ exit 1
935
+ ;;
936
+ esac
937
+ ;;
938
+
939
+ # ========== Wait for Text ==========
940
+
941
+ wait-for-text)
942
+ local text_val="$3"
943
+ local wft_timeout="${4:-30000}"
944
+ if [ "$MODE" = "local" ]; then
945
+ local_curl "$2" -X POST "$LOCAL_URL/api/wait-for-text" \
946
+ -d "{\"text\":$(printf '%s' "$text_val" | jq -Rs),\"timeout\":$wft_timeout}"
947
+ else
948
+ retry_curl -X POST "$BASE/api/sessions/$2/wait-for-text" \
949
+ -d "{\"text\":$(printf '%s' "$text_val" | jq -Rs),\"timeout\":$wft_timeout}"
950
+ fi
951
+ ;;
952
+
953
+ # ========== Observability ==========
954
+
955
+ console)
956
+ if [ "$MODE" = "local" ]; then
957
+ local_curl "$2" -X POST "$LOCAL_URL/api/console"
958
+ else
959
+ retry_curl -X GET "$BASE/api/sessions/$2/console"
960
+ fi
961
+ ;;
962
+
963
+ network)
964
+ if [ "$MODE" = "local" ]; then
965
+ local_curl "$2" -X POST "$LOCAL_URL/api/network"
966
+ else
967
+ retry_curl -X GET "$BASE/api/sessions/$2/network"
968
+ fi
969
+ ;;
970
+
971
+ url)
972
+ if [ "$MODE" = "local" ]; then
973
+ local_curl "$2" -X POST "$LOCAL_URL/api/url"
974
+ else
975
+ # Cloud: extract URL from snapshot
976
+ retry_curl -X POST "$BASE/api/sessions/$2/evaluate" \
977
+ -d '{"script":"window.location.href"}'
978
+ fi
979
+ ;;
980
+
981
+ title)
982
+ if [ "$MODE" = "local" ]; then
983
+ local_curl "$2" -X POST "$LOCAL_URL/api/title"
984
+ else
985
+ # Cloud: extract title from evaluate
986
+ retry_curl -X POST "$BASE/api/sessions/$2/evaluate" \
987
+ -d '{"script":"document.title"}'
988
+ fi
989
+ ;;
990
+
991
+ html)
992
+ if [ "$MODE" = "local" ]; then
993
+ local_curl "$2" -X POST "$LOCAL_URL/api/html"
994
+ else
995
+ retry_curl -X POST "$BASE/api/sessions/$2/extract" \
996
+ -d '{"type":"html"}'
997
+ fi
998
+ ;;
999
+
1000
+ clear-logs)
1001
+ if [ "$MODE" = "local" ]; then
1002
+ local_curl "$2" -X POST "$LOCAL_URL/api/clear-logs"
1003
+ else
1004
+ echo "ERROR: clear-logs only available in local mode" >&2
1005
+ exit 1
1006
+ fi
1007
+ ;;
1008
+
1009
+ # ========== Tab Management (local only) ==========
1010
+
1011
+ tabs)
1012
+ if [ "$MODE" = "local" ]; then
1013
+ curl -s -H "Content-Type: application/json" "$LOCAL_URL/api/tabs"
1014
+ else
1015
+ echo "ERROR: Tab management only available in local mode" >&2
1016
+ exit 1
1017
+ fi
1018
+ ;;
1019
+
1020
+ switch-tab)
1021
+ if [ "$MODE" = "local" ]; then
1022
+ local_curl "$2" -X POST "$LOCAL_URL/api/tabs/switch" \
1023
+ -d "{\"tabId\":$3}"
1024
+ else
1025
+ echo "ERROR: Tab management only available in local mode" >&2
1026
+ exit 1
1027
+ fi
1028
+ ;;
1029
+
1030
+ # ========== Help ==========
1031
+
1032
+ help|*)
1033
+ echo "browse — Portable browser automation for AI agents"
1034
+ echo ""
1035
+ echo "Mode: ${MODE:-not detected} (local=native host $LOCAL_HOST:$LOCAL_PORT, cloud=Playwright)"
1036
+ echo ""
1037
+ echo "Session Management:"
1038
+ echo " session-create Create session, print ID"
1039
+ echo " session-status <sid> Get session details"
1040
+ echo " session-delete <sid> Close session"
1041
+ echo " session-details <sid> Get session details (cloud/local)"
1042
+ echo " session-actions <sid> [limit] [off] Get session actions/logs (cloud/local)"
1043
+ echo " session-artifacts <sid> List session artifacts (cloud/local)"
1044
+ echo " artifact-download <id> [out] Download cloud artifact by ID"
1045
+ echo ""
1046
+ echo "Navigation:"
1047
+ echo " goto <sid> <url> Navigate to URL"
1048
+ echo " back <sid> Go back in browser history"
1049
+ echo " forward <sid> Go forward in browser history"
1050
+ echo " scroll <sid> [up|down] [pixels] Scroll page (default: down 500)"
1051
+ echo ""
1052
+ echo "Observation:"
1053
+ echo " snapshot <sid> Get DOM tree / accessibility snapshot"
1054
+ echo " screenshot <sid> Take screenshot"
1055
+ echo " extract <sid> [text|html] Extract page content"
1056
+ echo " console <sid> Get browser console logs"
1057
+ echo " network <sid> Get network requests"
1058
+ echo " url <sid> Get current page URL"
1059
+ echo " title <sid> Get current page title"
1060
+ echo " html <sid> Get full page HTML"
1061
+ echo ""
1062
+ echo "Interaction:"
1063
+ echo " click <sid> <selector> Click element"
1064
+ echo " fill <sid> <selector> <value> Fill form field (clears first)"
1065
+ echo " type <sid> <selector> <text> Type text (appends)"
1066
+ echo " press <sid> <key> Press key (Enter, Tab, Escape)"
1067
+ echo " hover <sid> <selector> Hover over element"
1068
+ echo " select <sid> <selector> <value> Select dropdown option"
1069
+ echo " wait <sid> <selector> [timeout_ms] Wait for element (default 10000ms)"
1070
+ echo " wait-for-text <sid> <text> [timeout] Wait for text to appear (default 30000ms)"
1071
+ echo " dialog <sid> [status|accept|dismiss] Handle browser dialog (alert/confirm/prompt)"
1072
+ echo " evaluate <sid> <javascript> Run JavaScript on page"
1073
+ echo ""
1074
+ echo "Local Only:"
1075
+ echo " clear-logs <sid> Clear console/network logs"
1076
+ echo " tabs List all open tabs"
1077
+ echo " switch-tab <sid> <tabId> Switch to specific tab"
1078
+ echo ""
1079
+ echo "Tab Listing Examples:"
1080
+ echo " browse tabs | jq -r '.data.tabs[] | \"\\(.id)\\t\\(.active)\\t\\(.url)\"'"
1081
+ echo " browse tabs | jq -r '.data.tabs[] | \"\\(.id)\\t\\(.active)\\t\\(.title)\\t\\(.url)\"'"
1082
+ echo ""
1083
+ echo "Flags:"
1084
+ echo " --mode local|cloud Override mode detection"
1085
+ echo ""
1086
+ echo "Port Discovery:"
1087
+ echo " The native host writes its port to ~/.thinkbrowse/port on startup."
1088
+ echo " This script reads it automatically. Override: THINKRUN_LOCAL_PORT env var."
1089
+ echo ""
1090
+ echo "Environment:"
1091
+ echo " THINKRUN_LOCAL=true Force local mode"
1092
+ echo " THINKRUN_LOCAL_PORT=<port> Override native host port"
1093
+ echo " THINKRUN_BRIDGE_PORT=<port> Legacy override alias (deprecated)"
1094
+ echo " THINKRUN_API_KEY=... Cloud API key"
1095
+ echo " THINKRUN_URL=... Cloud API base URL"
1096
+ echo " ~/.config/thinkrun/config.json Cloud config file"
1097
+ echo ""
1098
+ echo "Mode detection order:"
1099
+ echo " 1. --mode flag or THINKRUN_LOCAL=true"
1100
+ echo " 2. Native host health check ($LOCAL_HOST:$LOCAL_PORT)"
1101
+ echo " 3. THINKRUN_API_KEY or config file → cloud"
1102
+ echo ""
1103
+ echo "Docs:"
1104
+ echo " https://thinkbrowse.io/docs"
1105
+ echo " https://thinkbrowse.io/llms.txt"
1106
+ ;;
1107
+ esac