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