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,112 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import shutil
8
+ import stat
9
+ import tempfile
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ def atomic_bytes(path: Path, payload: bytes, mode: int) -> None:
15
+ fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
16
+ try:
17
+ with os.fdopen(fd, "wb") as stream:
18
+ stream.write(payload)
19
+ stream.flush()
20
+ os.fsync(stream.fileno())
21
+ os.chmod(temporary, mode)
22
+ os.replace(temporary, path)
23
+ finally:
24
+ if os.path.exists(temporary):
25
+ os.unlink(temporary)
26
+
27
+
28
+ def main() -> None:
29
+ parser = argparse.ArgumentParser()
30
+ parser.add_argument("--config", type=Path, required=True)
31
+ parser.add_argument("--preferences", type=Path)
32
+ args = parser.parse_args()
33
+
34
+ config = json.loads(args.config.read_text())
35
+ if not isinstance(config, dict):
36
+ raise RuntimeError("Invalid bridge configuration")
37
+ configured_path = config.get("bravePreferencesPath")
38
+ preferences_path = args.preferences or (
39
+ Path(configured_path)
40
+ if isinstance(configured_path, str)
41
+ else Path.home()
42
+ / "Library/Application Support/BraveSoftware/Brave-Browser/Default/Preferences"
43
+ )
44
+ if not preferences_path.is_file():
45
+ raise RuntimeError(f"Brave Preferences not found: {preferences_path}")
46
+
47
+ owned_keys = {
48
+ key for key in config.get("braveModelKeys", []) if isinstance(key, str)
49
+ }
50
+ owned_ids = (
51
+ {
52
+ profile.get("publicModelId")
53
+ for profile in config.get("profiles", [])
54
+ if isinstance(profile, dict) and isinstance(profile.get("publicModelId"), str)
55
+ }
56
+ if not owned_keys
57
+ else set()
58
+ )
59
+ raw = preferences_path.read_bytes()
60
+ preferences: dict[str, Any] = json.loads(raw)
61
+ ai_chat = preferences.get("brave", {}).get("ai_chat", {})
62
+ models = ai_chat.get("custom_models", [])
63
+ if not isinstance(models, list) or not all(isinstance(model, dict) for model in models):
64
+ raise RuntimeError("Unexpected Brave custom-model preference structure")
65
+
66
+ def managed(model: dict[str, Any]) -> bool:
67
+ # Current installations use the exact random Brave keys recorded in
68
+ # config. Model ids are only a compatibility fallback for the original
69
+ # private build, which did not record those keys.
70
+ return model.get("key") in owned_keys or (
71
+ not owned_keys and model.get("model_request_name") in owned_ids
72
+ )
73
+
74
+ removed = [model for model in models if managed(model)]
75
+ if not removed:
76
+ print("No managed Pi Leo models were present in Brave.")
77
+ return
78
+
79
+ removed_keys = {model.get("key") for model in removed}
80
+ retained = [model for model in models if not managed(model)]
81
+ ai_chat["custom_models"] = retained
82
+ if ai_chat.get("default_model_key") in removed_keys:
83
+ previous = config.get("previousDefaultModelKey")
84
+ retained_keys = {model.get("key") for model in retained}
85
+ previous_is_available = (
86
+ isinstance(previous, str)
87
+ and bool(previous)
88
+ and (not previous.startswith("custom:") or previous in retained_keys)
89
+ )
90
+ if previous_is_available:
91
+ ai_chat["default_model_key"] = previous
92
+ else:
93
+ ai_chat.pop("default_model_key", None)
94
+
95
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
96
+ backup = preferences_path.with_name(
97
+ f"Preferences.backup-{stamp}-before-pi-leo-bridge-uninstall"
98
+ )
99
+ shutil.copy2(preferences_path, backup)
100
+ if backup.read_bytes() != raw:
101
+ raise RuntimeError("Brave Preferences backup verification failed")
102
+
103
+ encoded = json.dumps(preferences, ensure_ascii=False, separators=(",", ":")).encode()
104
+ json.loads(encoded)
105
+ mode = stat.S_IMODE(preferences_path.stat().st_mode)
106
+ atomic_bytes(preferences_path, encoded, mode)
107
+ print(f"Removed {len(removed)} managed Brave model(s).")
108
+ print(f"Brave backup: {backup}")
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import shutil
8
+ import stat
9
+ import tempfile
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ def atomic_bytes(path: Path, payload: bytes, mode: int) -> None:
16
+ fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
17
+ try:
18
+ with os.fdopen(fd, "wb") as stream:
19
+ stream.write(payload)
20
+ stream.flush()
21
+ os.fsync(stream.fileno())
22
+ os.chmod(temporary, mode)
23
+ os.replace(temporary, path)
24
+ finally:
25
+ if os.path.exists(temporary):
26
+ os.unlink(temporary)
27
+
28
+
29
+ def main() -> None:
30
+ parser = argparse.ArgumentParser()
31
+ parser.add_argument("--config", type=Path, required=True)
32
+ parser.add_argument("selector", help="Thinking level, public model id, or 'restore'")
33
+ args = parser.parse_args()
34
+
35
+ config: dict[str, Any] = json.loads(args.config.read_text())
36
+ preferences_value = config.get("bravePreferencesPath")
37
+ if not isinstance(preferences_value, str):
38
+ raise RuntimeError("The installed configuration has no Brave profile path")
39
+ preferences_path = Path(preferences_value)
40
+ raw = preferences_path.read_bytes()
41
+ preferences = json.loads(raw)
42
+ ai_chat = preferences.get("brave", {}).get("ai_chat", {})
43
+ models = ai_chat.get("custom_models", [])
44
+ if not isinstance(models, list):
45
+ raise RuntimeError("Unexpected Brave custom-model preference structure")
46
+
47
+ if args.selector == "restore":
48
+ previous = config.get("previousDefaultModelKey")
49
+ available_custom_keys = {
50
+ model.get("key") for model in models if isinstance(model, dict)
51
+ }
52
+ if not isinstance(previous, str) or not previous:
53
+ raise RuntimeError("No previous Brave default was recorded")
54
+ if previous.startswith("custom:") and previous not in available_custom_keys:
55
+ raise RuntimeError("The previously selected custom model no longer exists")
56
+ ai_chat["default_model_key"] = previous
57
+ selected_label = "the recorded previous Brave model"
58
+ else:
59
+ profiles = config.get("profiles", [])
60
+ profile_index = next(
61
+ (
62
+ index
63
+ for index, candidate in enumerate(profiles)
64
+ if isinstance(candidate, dict)
65
+ and (
66
+ candidate.get("thinkingLevel") == args.selector
67
+ or candidate.get("publicModelId") == args.selector
68
+ )
69
+ ),
70
+ None,
71
+ )
72
+ profile = profiles[profile_index] if profile_index is not None else None
73
+ if profile is None:
74
+ levels = ", ".join(
75
+ str(candidate.get("thinkingLevel"))
76
+ for candidate in profiles
77
+ if isinstance(candidate, dict)
78
+ )
79
+ raise RuntimeError(f"Unknown profile. Available thinking levels: {levels}")
80
+ managed_keys = config.get("braveModelKeys", [])
81
+ expected_key = (
82
+ managed_keys[profile_index]
83
+ if isinstance(managed_keys, list)
84
+ and profile_index is not None
85
+ and profile_index < len(managed_keys)
86
+ else None
87
+ )
88
+ model = next(
89
+ (
90
+ candidate
91
+ for candidate in models
92
+ if isinstance(candidate, dict)
93
+ and candidate.get("model_request_name") == profile.get("publicModelId")
94
+ and (expected_key is None or candidate.get("key") == expected_key)
95
+ ),
96
+ None,
97
+ )
98
+ if model is None or not isinstance(model.get("key"), str):
99
+ raise RuntimeError("The selected managed model is missing from Brave")
100
+ ai_chat["default_model_key"] = model["key"]
101
+ selected_label = str(model.get("label") or profile.get("publicModelId"))
102
+
103
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
104
+ backup = preferences_path.with_name(
105
+ f"Preferences.backup-{stamp}-before-pi-leo-default-change"
106
+ )
107
+ shutil.copy2(preferences_path, backup)
108
+ if backup.read_bytes() != raw:
109
+ raise RuntimeError("Brave Preferences backup verification failed")
110
+
111
+ encoded = json.dumps(preferences, ensure_ascii=False, separators=(",", ":")).encode()
112
+ json.loads(encoded)
113
+ atomic_bytes(preferences_path, encoded, stat.S_IMODE(preferences_path.stat().st_mode))
114
+ print(f"Default for new Leo conversations: {selected_label}")
115
+ print(f"Brave backup: {backup}")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -0,0 +1,133 @@
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
+ ASSUME_YES=false
9
+ PURGE=false
10
+
11
+ usage() {
12
+ cat <<'EOF'
13
+ Usage: pi-leo uninstall [--yes] [--purge]
14
+
15
+ Stops and removes the LaunchAgent and removes models managed by this bridge
16
+ from Brave after creating a verified Preferences backup.
17
+
18
+ --yes Do not ask for confirmation
19
+ --purge Also remove bridge workspace and logs
20
+ EOF
21
+ }
22
+
23
+ while [[ $# -gt 0 ]]; do
24
+ case "$1" in
25
+ --yes) ASSUME_YES=true; shift ;;
26
+ --purge) PURGE=true; shift ;;
27
+ -h|--help) usage; exit 0 ;;
28
+ *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
29
+ esac
30
+ done
31
+
32
+ if [[ "$ASSUME_YES" != true && -t 0 ]]; then
33
+ read -r -p "Remove the Pi Leo service and its managed Brave models? [y/N] " answer
34
+ case "$answer" in y|Y|yes|YES) ;; *) echo "Cancelled."; exit 0 ;; esac
35
+ fi
36
+
37
+ WAS_RUNNING=false
38
+ RELAUNCHED=false
39
+ APP_NAME="Brave Browser"
40
+ PREFERENCES=""
41
+ if [[ -f "$CONFIG" ]]; then
42
+ TARGET="$(python3 - "$CONFIG" <<'PY'
43
+ import json,sys
44
+ c=json.load(open(sys.argv[1]))
45
+ print(c.get('bravePreferencesPath',''))
46
+ print(c.get('braveApplicationName','Brave Browser'))
47
+ PY
48
+ )"
49
+ PREFERENCES="$(printf '%s\n' "$TARGET" | sed -n '1p')"
50
+ APP_NAME="$(printf '%s\n' "$TARGET" | sed -n '2p')"
51
+ fi
52
+
53
+ if [[ -f "$CONFIG" && ( -z "$PREFERENCES" || ! -f "$PREFERENCES" ) ]]; then
54
+ echo "Configured Brave Preferences could not be found; refusing to orphan authenticated model entries." >&2
55
+ echo "Expected: ${PREFERENCES:-<missing from configuration>}" >&2
56
+ exit 1
57
+ fi
58
+
59
+ WAS_LOADED=false
60
+ UNINSTALL_SUCCEEDED=false
61
+ if launchctl print "gui/${UID}/${LABEL}" >/dev/null 2>&1; then
62
+ WAS_LOADED=true
63
+ fi
64
+ launchctl bootout "gui/${UID}/${LABEL}" >/dev/null 2>&1 || true
65
+
66
+ relaunch_if_needed() {
67
+ if [[ "$UNINSTALL_SUCCEEDED" != true && "$WAS_LOADED" == true && -f "$PLIST" ]]; then
68
+ launchctl bootstrap "gui/${UID}" "$PLIST" >/dev/null 2>&1 || true
69
+ launchctl enable "gui/${UID}/${LABEL}" >/dev/null 2>&1 || true
70
+ fi
71
+ if [[ "$WAS_RUNNING" == true && "$RELAUNCHED" == false ]]; then
72
+ open -a "$APP_NAME" >/dev/null 2>&1 || true
73
+ fi
74
+ }
75
+ trap relaunch_if_needed EXIT
76
+
77
+ if [[ -f "$CONFIG" ]]; then
78
+ if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
79
+ WAS_RUNNING=true
80
+ echo "Quitting $APP_NAME briefly to remove the managed model entries..."
81
+ for _ in 1 2 3; do
82
+ osascript -e "tell application \"$APP_NAME\" to quit" >/dev/null 2>&1 || true
83
+ for _ in $(seq 1 20); do
84
+ pgrep -x "$APP_NAME" >/dev/null 2>&1 || break
85
+ sleep 0.5
86
+ done
87
+ pgrep -x "$APP_NAME" >/dev/null 2>&1 || break
88
+ done
89
+ if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
90
+ echo "$APP_NAME did not quit; Brave Preferences were not changed." >&2
91
+ exit 1
92
+ fi
93
+ fi
94
+ python3 "$ROOT/scripts/remove-brave-models.py" \
95
+ --config "$CONFIG" \
96
+ --preferences "$PREFERENCES"
97
+ fi
98
+
99
+ rm -f "$PLIST"
100
+ rm -f "$CONFIG"
101
+ rmdir "${HOME}/.config/pi-leo-bridge" >/dev/null 2>&1 || true
102
+
103
+ BIN_LINK="${HOME}/.local/bin/pi-leo"
104
+ if [[ -L "$BIN_LINK" ]]; then
105
+ LINK_TARGET="$(python3 - "$BIN_LINK" <<'PY'
106
+ import sys
107
+ from pathlib import Path
108
+ print(Path(sys.argv[1]).resolve())
109
+ PY
110
+ )"
111
+ if [[ "$LINK_TARGET" == "$ROOT/bin/pi-leo" ]]; then
112
+ rm -f "$BIN_LINK"
113
+ fi
114
+ fi
115
+
116
+ if [[ "$PURGE" == true ]]; then
117
+ rm -rf "${HOME}/.local/share/pi-leo-bridge"
118
+ rm -f \
119
+ "${HOME}/Library/Logs/pi-leo-bridge.log" \
120
+ "${HOME}/Library/Logs/pi-leo-bridge.error.log"
121
+ fi
122
+
123
+ UNINSTALL_SUCCEEDED=true
124
+ if [[ "$WAS_RUNNING" == true ]]; then
125
+ open -a "$APP_NAME"
126
+ RELAUNCHED=true
127
+ fi
128
+ trap - EXIT
129
+
130
+ echo "Pi Leo bridge uninstalled."
131
+ if [[ "$PURGE" != true ]]; then
132
+ echo "Workspace and logs were retained; use --purge to remove them."
133
+ fi