pi-leo-bridge 0.1.1

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.
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import plistlib
9
+ import re
10
+ import socket
11
+ import stat
12
+ import subprocess
13
+ import sys
14
+ import urllib.error
15
+ import urllib.request
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ LABEL = "com.ojm.pi-leo-bridge"
20
+ ENDPOINT_RE = re.compile(
21
+ r"^http://127\.0\.0\.1:(\d+)/auth/([A-Za-z0-9_-]{32,})/v1/chat/completions$"
22
+ )
23
+
24
+
25
+ def file_mode(path: Path) -> int | None:
26
+ return stat.S_IMODE(path.stat().st_mode) if path.exists() else None
27
+
28
+
29
+ def main() -> None:
30
+ parser = argparse.ArgumentParser()
31
+ parser.add_argument(
32
+ "--config",
33
+ type=Path,
34
+ default=Path.home() / ".config" / "pi-leo-bridge" / "config.json",
35
+ )
36
+ args = parser.parse_args()
37
+
38
+ checks: list[tuple[bool, str]] = []
39
+
40
+ def check(condition: bool, message: str) -> None:
41
+ checks.append((condition, message))
42
+ print(f"{'PASS' if condition else 'FAIL'} {message}")
43
+
44
+ config_path = args.config
45
+ check(config_path.is_file(), f"Configuration exists: {config_path}")
46
+ if not config_path.is_file():
47
+ raise SystemExit(1)
48
+
49
+ try:
50
+ config: dict[str, Any] = json.loads(config_path.read_text())
51
+ check(isinstance(config, dict), "Configuration is valid JSON")
52
+ except Exception:
53
+ check(False, "Configuration is valid JSON")
54
+ raise SystemExit(1)
55
+
56
+ check(file_mode(config_path) == 0o600, "Configuration permissions are 0600")
57
+ check(config.get("host") == "127.0.0.1", "Bridge is configured for IPv4 loopback only")
58
+ port = config.get("port")
59
+ check(isinstance(port, int) and 1024 <= port <= 65535, "Bridge port is valid")
60
+
61
+ plist_path = Path.home() / "Library" / "LaunchAgents" / f"{LABEL}.plist"
62
+ check(plist_path.is_file(), f"LaunchAgent exists: {plist_path}")
63
+ check(file_mode(plist_path) == 0o600, "LaunchAgent permissions are 0600")
64
+ if plist_path.is_file():
65
+ try:
66
+ plist = plistlib.loads(plist_path.read_bytes())
67
+ arguments = plist.get("ProgramArguments", [])
68
+ check(
69
+ isinstance(arguments, list)
70
+ and len(arguments) >= 2
71
+ and all(Path(value).exists() for value in arguments[:2]),
72
+ "LaunchAgent runtime paths exist",
73
+ )
74
+ except Exception:
75
+ check(False, "LaunchAgent is valid")
76
+
77
+ domain = f"gui/{os.getuid()}/{LABEL}"
78
+ loaded = subprocess.run(
79
+ ["launchctl", "print", domain],
80
+ stdout=subprocess.DEVNULL,
81
+ stderr=subprocess.DEVNULL,
82
+ check=False,
83
+ ).returncode == 0
84
+ check(loaded, "LaunchAgent is loaded")
85
+
86
+ configured_profiles = config.get("profiles", [])
87
+ check(
88
+ isinstance(configured_profiles, list) and bool(configured_profiles),
89
+ "At least one thinking profile is configured",
90
+ )
91
+
92
+ preferences_value = config.get("bravePreferencesPath")
93
+ preferences_path = (
94
+ Path(preferences_value)
95
+ if isinstance(preferences_value, str)
96
+ else Path.home()
97
+ / "Library/Application Support/BraveSoftware/Brave-Browser/Default/Preferences"
98
+ )
99
+ check(preferences_path.is_file(), f"Brave Preferences exists: {preferences_path}")
100
+ health_url: str | None = None
101
+ if preferences_path.is_file():
102
+ try:
103
+ preferences = json.loads(preferences_path.read_text())
104
+ models = preferences.get("brave", {}).get("ai_chat", {}).get("custom_models", [])
105
+ managed_keys = config.get("braveModelKeys", [])
106
+ expected_hash = config.get("tokenSha256")
107
+ profile_ok = (
108
+ isinstance(configured_profiles, list)
109
+ and isinstance(managed_keys, list)
110
+ and len(managed_keys) == len(configured_profiles)
111
+ )
112
+ for index, profile in enumerate(configured_profiles):
113
+ if not isinstance(profile, dict) or index >= len(managed_keys):
114
+ profile_ok = False
115
+ continue
116
+ model = next(
117
+ (
118
+ candidate
119
+ for candidate in models
120
+ if isinstance(candidate, dict)
121
+ and candidate.get("key") == managed_keys[index]
122
+ and candidate.get("model_request_name") == profile.get("publicModelId")
123
+ ),
124
+ None,
125
+ )
126
+ match = ENDPOINT_RE.match(str(model.get("endpoint_url", ""))) if model else None
127
+ current_ok = bool(
128
+ model
129
+ and match
130
+ and int(match.group(1)) == port
131
+ and hashlib.sha256(match.group(2).encode()).hexdigest() == expected_hash
132
+ and model.get("api_key") == ""
133
+ and model.get("supports_tools") is False
134
+ )
135
+ profile_ok = profile_ok and current_ok
136
+ if current_ok and health_url is None and match is not None:
137
+ health_url = (
138
+ f"http://127.0.0.1:{match.group(1)}/auth/{match.group(2)}/healthz"
139
+ )
140
+ check(profile_ok, "Brave models match the authenticated no-tools configuration")
141
+ except Exception:
142
+ check(False, "Brave model configuration is valid")
143
+
144
+ health: dict[str, Any] = {}
145
+ if health_url is not None:
146
+ try:
147
+ with urllib.request.urlopen(health_url, timeout=3) as response:
148
+ health = json.load(response)
149
+ check(
150
+ health.get("status") == "ok"
151
+ and health.get("service") == "pi-leo-bridge",
152
+ "Authenticated bridge health endpoint identifies the expected service",
153
+ )
154
+ except (OSError, urllib.error.URLError, json.JSONDecodeError):
155
+ check(False, "Authenticated bridge health endpoint is reachable")
156
+ else:
157
+ check(False, "Authenticated bridge health endpoint can be derived")
158
+
159
+ if health:
160
+ check(health.get("profiles") == configured_profiles, "Running profiles match configuration")
161
+ check(health.get("tools") == "disabled", "Running bridge reports tools disabled")
162
+
163
+ if isinstance(port, int):
164
+ listener_is_local = False
165
+ try:
166
+ output = subprocess.run(
167
+ ["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN"],
168
+ capture_output=True,
169
+ text=True,
170
+ check=False,
171
+ timeout=5,
172
+ ).stdout
173
+ lines = [line for line in output.splitlines()[1:] if line.strip()]
174
+ listener_is_local = bool(lines) and all(
175
+ "127.0.0.1:" in line and "*:" not in line for line in lines
176
+ )
177
+ except (OSError, subprocess.SubprocessError):
178
+ try:
179
+ with socket.create_connection(("127.0.0.1", port), timeout=2):
180
+ listener_is_local = config.get("host") == "127.0.0.1"
181
+ except OSError:
182
+ listener_is_local = False
183
+ check(listener_is_local, "Listening socket is restricted to IPv4 loopback")
184
+
185
+ passed = sum(1 for ok, _ in checks if ok)
186
+ print(f"\n{passed}/{len(checks)} checks passed.")
187
+ if passed != len(checks):
188
+ raise SystemExit(1)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
@@ -0,0 +1,406 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5
+ LABEL="com.ojm.pi-leo-bridge"
6
+ CONFIG="${HOME}/.config/pi-leo-bridge/config.json"
7
+ PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
8
+ CHANNEL="stable"
9
+ PROFILE=""
10
+ PROVIDER=""
11
+ MODEL=""
12
+ DISPLAY_NAME=""
13
+ LEVELS=""
14
+ PRIMARY_LEVEL=""
15
+ PORT=""
16
+ CONTEXT_SIZE=""
17
+ ASSUME_YES=false
18
+ SKIP_VERIFY=false
19
+ ROTATE_TOKEN=false
20
+ CHANNEL_SET=false
21
+ PROFILE_SET=false
22
+
23
+ usage() {
24
+ cat <<'EOF'
25
+ Usage: pi-leo install [options]
26
+
27
+ Options:
28
+ --provider ID Pi provider (default: openai-codex)
29
+ --model ID Pi model (default: gpt-5.6-sol)
30
+ --name NAME Display name used in Brave
31
+ --levels LIST Comma-separated levels (default: low,medium,high)
32
+ --primary-level LEVEL Unsuffixed/default profile (default: medium)
33
+ --context-size TOKENS Context cap advertised to Leo (default: 100000)
34
+ --port PORT Loopback port (default: 43127)
35
+ --channel CHANNEL stable, beta, or nightly
36
+ --profile DIRECTORY Brave profile directory, e.g. Default or "Profile 1"
37
+ --yes Accept defaults without interactive questions
38
+ --rotate-token Replace the local capability token
39
+ --skip-verify Skip source-checkout dependency and test step
40
+ -h, --help Show this help
41
+ EOF
42
+ }
43
+
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ --provider) PROVIDER="${2:?Missing provider}"; shift 2 ;;
47
+ --model) MODEL="${2:?Missing model}"; shift 2 ;;
48
+ --name) DISPLAY_NAME="${2:?Missing display name}"; shift 2 ;;
49
+ --levels) LEVELS="${2:?Missing levels}"; shift 2 ;;
50
+ --primary-level) PRIMARY_LEVEL="${2:?Missing primary level}"; shift 2 ;;
51
+ --context-size) CONTEXT_SIZE="${2:?Missing context size}"; shift 2 ;;
52
+ --port) PORT="${2:?Missing port}"; shift 2 ;;
53
+ --channel) CHANNEL="${2:?Missing channel}"; CHANNEL_SET=true; shift 2 ;;
54
+ --profile) PROFILE="${2:?Missing profile}"; PROFILE_SET=true; shift 2 ;;
55
+ --yes) ASSUME_YES=true; shift ;;
56
+ --rotate-token) ROTATE_TOKEN=true; shift ;;
57
+ --skip-verify) SKIP_VERIFY=true; shift ;;
58
+ -h|--help) usage; exit 0 ;;
59
+ *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
60
+ esac
61
+ done
62
+
63
+ if [[ "$(uname -s)" != "Darwin" ]]; then
64
+ echo "This release currently supports macOS only." >&2
65
+ exit 1
66
+ fi
67
+ for command in node npm python3 curl launchctl osascript lsof; do
68
+ if ! command -v "$command" >/dev/null 2>&1; then
69
+ echo "Required command not found: $command" >&2
70
+ exit 1
71
+ fi
72
+ done
73
+ NODE_BIN="$(command -v node)"
74
+ node -e 'const [major,minor]=process.versions.node.split(".").map(Number); if(major<22||(major===22&&minor<19)){console.error("Node.js 22.19 or newer is required"); process.exit(1)}'
75
+ python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else "Python 3.10 or newer is required")'
76
+
77
+ if [[ -f "$ROOT/src/index.ts" && "$SKIP_VERIFY" != true ]]; then
78
+ echo "Verifying source checkout..."
79
+ (cd "$ROOT" && npm ci && npm run typecheck && npm test)
80
+ elif [[ ! -f "$ROOT/dist/src/index.js" ]]; then
81
+ echo "Packaged runtime is missing: $ROOT/dist/src/index.js" >&2
82
+ exit 1
83
+ fi
84
+
85
+ # Reuse the browser target chosen by an existing installation unless overridden.
86
+ PREFERENCES=""
87
+ APP_NAME=""
88
+ if [[ -f "$CONFIG" && "$CHANNEL_SET" != true && "$PROFILE_SET" != true ]]; then
89
+ EXISTING_TARGET="$(python3 - "$CONFIG" <<'PY'
90
+ import json,sys
91
+ c=json.load(open(sys.argv[1]))
92
+ print(c.get('bravePreferencesPath',''))
93
+ print(c.get('braveApplicationName',''))
94
+ PY
95
+ )"
96
+ PREFERENCES="$(printf '%s\n' "$EXISTING_TARGET" | sed -n '1p')"
97
+ APP_NAME="$(printf '%s\n' "$EXISTING_TARGET" | sed -n '2p')"
98
+ fi
99
+ if [[ -z "$PREFERENCES" || -z "$APP_NAME" ]]; then
100
+ case "$CHANNEL" in
101
+ stable)
102
+ USER_DATA="${HOME}/Library/Application Support/BraveSoftware/Brave-Browser"
103
+ APP_NAME="Brave Browser"
104
+ ;;
105
+ beta)
106
+ USER_DATA="${HOME}/Library/Application Support/BraveSoftware/Brave-Browser-Beta"
107
+ APP_NAME="Brave Browser Beta"
108
+ ;;
109
+ nightly)
110
+ USER_DATA="${HOME}/Library/Application Support/BraveSoftware/Brave-Browser-Nightly"
111
+ APP_NAME="Brave Browser Nightly"
112
+ ;;
113
+ *) echo "Unsupported Brave channel: $CHANNEL" >&2; exit 2 ;;
114
+ esac
115
+ if [[ -z "$PROFILE" ]]; then
116
+ PROFILE="$(python3 - "$USER_DATA" <<'PY'
117
+ import json,sys
118
+ from pathlib import Path
119
+ root=Path(sys.argv[1])
120
+ try:
121
+ state=json.loads((root/'Local State').read_text())
122
+ value=state.get('profile',{}).get('last_used','Default')
123
+ print(value if isinstance(value,str) and value else 'Default')
124
+ except Exception:
125
+ print('Default')
126
+ PY
127
+ )"
128
+ fi
129
+ if [[ "$PROFILE" == *"/"* || "$PROFILE" == "." || "$PROFILE" == ".." ]]; then
130
+ echo "Invalid Brave profile directory: $PROFILE" >&2
131
+ exit 2
132
+ fi
133
+ PREFERENCES="$USER_DATA/$PROFILE/Preferences"
134
+ fi
135
+
136
+ if [[ -f "$CONFIG" ]]; then
137
+ INSTALLED_PREFERENCES="$(python3 - "$CONFIG" <<'PY'
138
+ import json,sys
139
+ print(json.load(open(sys.argv[1])).get('bravePreferencesPath',''))
140
+ PY
141
+ )"
142
+ if [[ -n "$INSTALLED_PREFERENCES" && "$INSTALLED_PREFERENCES" != "$PREFERENCES" ]]; then
143
+ echo "Changing Brave channels or profiles in place could leave an authenticated model in the old profile." >&2
144
+ echo "Run 'pi-leo uninstall' first, then install again with the new --channel/--profile." >&2
145
+ exit 1
146
+ fi
147
+ fi
148
+
149
+ case "$APP_NAME" in
150
+ "Brave Browser"|"Brave Browser Beta"|"Brave Browser Nightly") ;;
151
+ *) echo "Unsupported Brave application name in configuration: $APP_NAME" >&2; exit 1 ;;
152
+ esac
153
+ if [[ -L "$(dirname "$PREFERENCES")" || -L "$PREFERENCES" ]]; then
154
+ echo "Refusing to modify symbolic-linked Brave profile data." >&2
155
+ exit 1
156
+ fi
157
+ if [[ ! -f "$PREFERENCES" ]]; then
158
+ echo "Brave Preferences not found: $PREFERENCES" >&2
159
+ echo "Use --channel and --profile to select the correct Brave profile." >&2
160
+ exit 1
161
+ fi
162
+
163
+ if [[ ! -f "$CONFIG" && "$ASSUME_YES" != true && -t 0 && -t 1 ]]; then
164
+ echo "Configure the Pi model used by Leo. Run 'pi-leo models' in another terminal to list available models."
165
+ read -r -p "Pi provider [openai-codex]: " answer
166
+ PROVIDER="${PROVIDER:-${answer:-openai-codex}}"
167
+ read -r -p "Pi model [gpt-5.6-sol]: " answer
168
+ MODEL="${MODEL:-${answer:-gpt-5.6-sol}}"
169
+ read -r -p "Brave display name [automatic]: " answer
170
+ DISPLAY_NAME="${DISPLAY_NAME:-$answer}"
171
+ read -r -p "Thinking levels [low,medium,high]: " answer
172
+ LEVELS="${LEVELS:-${answer:-low,medium,high}}"
173
+ fi
174
+
175
+ EXISTING_MODEL_INFO=""
176
+ if [[ -f "$CONFIG" ]]; then
177
+ EXISTING_MODEL_INFO="$(python3 - "$CONFIG" <<'PY'
178
+ import json,sys
179
+ c=json.load(open(sys.argv[1]))
180
+ print(c.get('provider',''))
181
+ print(c.get('modelId',''))
182
+ print(c.get('displayName',''))
183
+ print(c.get('contextSize',''))
184
+ print(c.get('port',''))
185
+ PY
186
+ )"
187
+ fi
188
+ EXISTING_PROVIDER="$(printf '%s\n' "$EXISTING_MODEL_INFO" | sed -n '1p')"
189
+ EXISTING_MODEL="$(printf '%s\n' "$EXISTING_MODEL_INFO" | sed -n '2p')"
190
+ EXISTING_DISPLAY_NAME="$(printf '%s\n' "$EXISTING_MODEL_INFO" | sed -n '3p')"
191
+ EXISTING_CONTEXT_SIZE="$(printf '%s\n' "$EXISTING_MODEL_INFO" | sed -n '4p')"
192
+ EXISTING_PORT="$(printf '%s\n' "$EXISTING_MODEL_INFO" | sed -n '5p')"
193
+ if [[ -n "$PROVIDER" && -z "$MODEL" && -n "$EXISTING_PROVIDER" && "$PROVIDER" != "$EXISTING_PROVIDER" ]]; then
194
+ echo "--model is required when changing --provider." >&2
195
+ exit 2
196
+ fi
197
+ EFFECTIVE_PROVIDER="${PROVIDER:-${EXISTING_PROVIDER:-openai-codex}}"
198
+ EFFECTIVE_MODEL="${MODEL:-${EXISTING_MODEL:-gpt-5.6-sol}}"
199
+ EFFECTIVE_PORT="${PORT:-${EXISTING_PORT:-43127}}"
200
+ if [[ ! "$EFFECTIVE_PORT" =~ ^[0-9]+$ ]] || (( EFFECTIVE_PORT < 1024 || EFFECTIVE_PORT > 65535 )); then
201
+ echo "Port must be an integer between 1024 and 65535." >&2
202
+ exit 2
203
+ fi
204
+ MODEL_INFO="$(node "$ROOT/dist/src/check-model.js" "$EFFECTIVE_PROVIDER" "$EFFECTIVE_MODEL")"
205
+ MODEL_FIELDS="$(printf '%s' "$MODEL_INFO" | python3 -c 'import json,sys; m=json.load(sys.stdin); print(m["name"]); print(m["contextWindow"]); print(str(m["reasoning"]).lower()); print(str(m["vision"]).lower())')"
206
+ MODEL_DISPLAY_NAME="$(printf '%s\n' "$MODEL_FIELDS" | sed -n '1p')"
207
+ MODEL_CONTEXT_WINDOW="$(printf '%s\n' "$MODEL_FIELDS" | sed -n '2p')"
208
+ MODEL_REASONING="$(printf '%s\n' "$MODEL_FIELDS" | sed -n '3p')"
209
+ MODEL_VISION="$(printf '%s\n' "$MODEL_FIELDS" | sed -n '4p')"
210
+ echo "Validated Pi model: $EFFECTIVE_PROVIDER/$EFFECTIVE_MODEL (context $MODEL_CONTEXT_WINDOW)"
211
+ PROVIDER="$EFFECTIVE_PROVIDER"
212
+ MODEL="$EFFECTIVE_MODEL"
213
+ PORT="$EFFECTIVE_PORT"
214
+ if [[ -z "$DISPLAY_NAME" && ( -z "$EXISTING_MODEL" || "$MODEL" != "$EXISTING_MODEL" || -z "$EXISTING_DISPLAY_NAME" ) ]]; then
215
+ DISPLAY_NAME="$MODEL_DISPLAY_NAME"
216
+ fi
217
+ if [[ -z "$CONTEXT_SIZE" ]]; then
218
+ CONTEXT_SIZE="${EXISTING_CONTEXT_SIZE:-100000}"
219
+ fi
220
+ if [[ ! "$CONTEXT_SIZE" =~ ^[0-9]+$ || ! "$MODEL_CONTEXT_WINDOW" =~ ^[0-9]+$ ]]; then
221
+ echo "Context sizes must be integers." >&2
222
+ exit 2
223
+ fi
224
+ if (( CONTEXT_SIZE > MODEL_CONTEXT_WINDOW )); then
225
+ CONTEXT_SIZE="$MODEL_CONTEXT_WINDOW"
226
+ fi
227
+ if [[ "$MODEL_REASONING" != true && -z "$LEVELS" ]]; then
228
+ LEVELS="off"
229
+ PRIMARY_LEVEL="off"
230
+ fi
231
+
232
+ CONFIGURE_ARGS=(
233
+ --project "$ROOT"
234
+ --node "$NODE_BIN"
235
+ --preferences "$PREFERENCES"
236
+ --app-name "$APP_NAME"
237
+ --vision-support "$MODEL_VISION"
238
+ )
239
+ [[ -n "$PROVIDER" ]] && CONFIGURE_ARGS+=(--provider "$PROVIDER")
240
+ [[ -n "$MODEL" ]] && CONFIGURE_ARGS+=(--model "$MODEL")
241
+ [[ -n "$DISPLAY_NAME" ]] && CONFIGURE_ARGS+=(--display-name "$DISPLAY_NAME")
242
+ [[ -n "$LEVELS" ]] && CONFIGURE_ARGS+=(--levels "$LEVELS")
243
+ [[ -n "$PRIMARY_LEVEL" ]] && CONFIGURE_ARGS+=(--primary-level "$PRIMARY_LEVEL")
244
+ [[ -n "$PORT" ]] && CONFIGURE_ARGS+=(--port "$PORT")
245
+ [[ -n "$CONTEXT_SIZE" ]] && CONFIGURE_ARGS+=(--context-size "$CONTEXT_SIZE")
246
+ [[ "$ROTATE_TOKEN" == true ]] && CONFIGURE_ARGS+=(--rotate-token)
247
+
248
+ WAS_RUNNING=false
249
+ if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
250
+ WAS_RUNNING=true
251
+ fi
252
+ ROLLBACK_DIR=""
253
+ CHANGES_STARTED=false
254
+ INSTALL_SUCCEEDED=false
255
+ HAD_CONFIG=false
256
+ HAD_PLIST=false
257
+ HAD_BIN_LINK=false
258
+ HAD_LOADED=false
259
+ BIN_LINK="${HOME}/.local/bin/pi-leo"
260
+ if [[ -e "$BIN_LINK" && ! -L "$BIN_LINK" ]]; then
261
+ echo "Refusing to replace non-symlink command: $BIN_LINK" >&2
262
+ exit 1
263
+ fi
264
+ if launchctl print "gui/${UID}/${LABEL}" >/dev/null 2>&1; then
265
+ HAD_LOADED=true
266
+ fi
267
+
268
+ finish_install() {
269
+ local status=$?
270
+ trap - EXIT
271
+ if [[ "$CHANGES_STARTED" == true && "$INSTALL_SUCCEEDED" != true && -n "$ROLLBACK_DIR" ]]; then
272
+ echo "Installation failed; restoring the previous bridge and Brave configuration." >&2
273
+ launchctl bootout "gui/${UID}/${LABEL}" >/dev/null 2>&1 || true
274
+ cp -p "$ROLLBACK_DIR/Preferences" "$PREFERENCES" || true
275
+ if [[ "$HAD_CONFIG" == true ]]; then
276
+ mkdir -p "$(dirname "$CONFIG")"
277
+ cp -p "$ROLLBACK_DIR/config.json" "$CONFIG" || true
278
+ else
279
+ rm -f "$CONFIG"
280
+ fi
281
+ if [[ "$HAD_PLIST" == true ]]; then
282
+ mkdir -p "$(dirname "$PLIST")"
283
+ cp -p "$ROLLBACK_DIR/launch-agent.plist" "$PLIST" || true
284
+ else
285
+ rm -f "$PLIST"
286
+ fi
287
+ rm -f "$BIN_LINK"
288
+ if [[ "$HAD_BIN_LINK" == true ]]; then
289
+ cp -P "$ROLLBACK_DIR/pi-leo-link" "$BIN_LINK" || true
290
+ fi
291
+ if [[ "$HAD_LOADED" == true && "$HAD_PLIST" == true ]]; then
292
+ launchctl bootstrap "gui/${UID}" "$PLIST" >/dev/null 2>&1 || true
293
+ launchctl enable "gui/${UID}/${LABEL}" >/dev/null 2>&1 || true
294
+ fi
295
+ fi
296
+ [[ -z "$ROLLBACK_DIR" ]] || rm -rf "$ROLLBACK_DIR"
297
+ if [[ "$WAS_RUNNING" == true ]]; then
298
+ open -a "$APP_NAME" >/dev/null 2>&1 || true
299
+ fi
300
+ exit "$status"
301
+ }
302
+ trap finish_install EXIT
303
+
304
+ if [[ "$WAS_RUNNING" == true ]]; then
305
+ echo "Quitting $APP_NAME briefly to update its model list..."
306
+ for _ in 1 2 3; do
307
+ osascript -e "tell application \"$APP_NAME\" to quit" >/dev/null 2>&1 || true
308
+ for _ in $(seq 1 20); do
309
+ pgrep -x "$APP_NAME" >/dev/null 2>&1 || break
310
+ sleep 0.5
311
+ done
312
+ pgrep -x "$APP_NAME" >/dev/null 2>&1 || break
313
+ done
314
+ if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
315
+ echo "$APP_NAME did not quit within 30 seconds; no preferences were changed." >&2
316
+ exit 1
317
+ fi
318
+ fi
319
+
320
+ ROLLBACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pi-leo-install.XXXXXX")"
321
+ cp -p "$PREFERENCES" "$ROLLBACK_DIR/Preferences"
322
+ cmp -s "$PREFERENCES" "$ROLLBACK_DIR/Preferences"
323
+ if [[ -f "$CONFIG" ]]; then
324
+ HAD_CONFIG=true
325
+ cp -p "$CONFIG" "$ROLLBACK_DIR/config.json"
326
+ fi
327
+ if [[ -f "$PLIST" ]]; then
328
+ HAD_PLIST=true
329
+ cp -p "$PLIST" "$ROLLBACK_DIR/launch-agent.plist"
330
+ fi
331
+ if [[ -L "$BIN_LINK" ]]; then
332
+ HAD_BIN_LINK=true
333
+ cp -P "$BIN_LINK" "$ROLLBACK_DIR/pi-leo-link"
334
+ fi
335
+ CHANGES_STARTED=true
336
+
337
+ # Stop the prior managed process before trusting the port. A listener that
338
+ # remains cannot masquerade as this installation's public health endpoint.
339
+ launchctl bootout "gui/${UID}/${LABEL}" >/dev/null 2>&1 || true
340
+ for _ in $(seq 1 50); do
341
+ if ! lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
342
+ break
343
+ fi
344
+ sleep 0.1
345
+ done
346
+ if lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
347
+ echo "Port $PORT is already used by another service." >&2
348
+ exit 1
349
+ fi
350
+
351
+ python3 "$ROOT/scripts/configure-install.py" "${CONFIGURE_ARGS[@]}"
352
+
353
+ mkdir -p "${HOME}/.local/bin"
354
+ ln -sfn "$ROOT/bin/pi-leo" "$BIN_LINK"
355
+
356
+ launchctl bootstrap "gui/${UID}" "$PLIST"
357
+ launchctl enable "gui/${UID}/${LABEL}"
358
+
359
+ HEALTH_URL="$(python3 - "$CONFIG" "$PREFERENCES" <<'PY'
360
+ import hashlib,json,re,sys
361
+ config=json.load(open(sys.argv[1]))
362
+ preferences=json.load(open(sys.argv[2]))
363
+ keys=set(config.get('braveModelKeys',[]))
364
+ models=preferences.get('brave',{}).get('ai_chat',{}).get('custom_models',[])
365
+ pattern=re.compile(r'^(http://127\.0\.0\.1:\d+/auth/([A-Za-z0-9_-]{32,}))/v1/chat/completions$')
366
+ for model in models:
367
+ if not isinstance(model,dict) or model.get('key') not in keys:
368
+ continue
369
+ match=pattern.match(str(model.get('endpoint_url','')))
370
+ if match and hashlib.sha256(match.group(2).encode()).hexdigest()==config.get('tokenSha256'):
371
+ print(f"{match.group(1)}/healthz")
372
+ break
373
+ else:
374
+ raise SystemExit('Could not recover the managed health capability')
375
+ PY
376
+ )"
377
+ managed_listener_matches() {
378
+ local service_pid
379
+ service_pid="$(launchctl print "gui/${UID}/${LABEL}" 2>/dev/null \
380
+ | awk '$1 == "pid" && $2 == "=" { print $3; exit }')"
381
+ [[ "$service_pid" =~ ^[0-9]+$ ]] || return 1
382
+ lsof -t -nP -iTCP:"$PORT" -sTCP:LISTEN 2>/dev/null | grep -qx "$service_pid"
383
+ }
384
+
385
+ healthy=false
386
+ for _ in $(seq 1 90); do
387
+ if managed_listener_matches \
388
+ && payload="$(curl --fail --silent --max-time 2 "$HEALTH_URL" 2>/dev/null)" \
389
+ && printf '%s' "$payload" | python3 -c 'import json,sys; p=json.load(sys.stdin); assert p.get("service")=="pi-leo-bridge" and p.get("status")=="ok"' 2>/dev/null; then
390
+ healthy=true
391
+ break
392
+ fi
393
+ sleep 1
394
+ done
395
+ if [[ "$healthy" != true ]]; then
396
+ echo "The bridge did not become healthy on its LaunchAgent-owned socket. Recent errors:" >&2
397
+ tail -n 30 "${HOME}/Library/Logs/pi-leo-bridge.error.log" 2>/dev/null || true
398
+ exit 1
399
+ fi
400
+
401
+ INSTALL_SUCCEEDED=true
402
+
403
+ echo
404
+ echo "Pi Leo bridge installed and healthy."
405
+ echo "Choose one of the new Pi profiles in Leo's model picker."
406
+ echo "Commands: pi-leo status | restart | doctor | logs | smoke-test | uninstall"