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.
- package/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +16 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/SECURITY.md +23 -0
- package/bin/pi-leo +229 -0
- package/dist/src/check-model.d.ts +1 -0
- package/dist/src/check-model.js +47 -0
- package/dist/src/check-model.js.map +1 -0
- package/dist/src/config.d.ts +23 -0
- package/dist/src/config.js +81 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +60 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/list-models.d.ts +1 -0
- package/dist/src/list-models.js +54 -0
- package/dist/src/list-models.js.map +1 -0
- package/dist/src/openai.d.ts +47 -0
- package/dist/src/openai.js +210 -0
- package/dist/src/openai.js.map +1 -0
- package/dist/src/output-filter.d.ts +21 -0
- package/dist/src/output-filter.js +129 -0
- package/dist/src/output-filter.js.map +1 -0
- package/dist/src/pi-runner.d.ts +16 -0
- package/dist/src/pi-runner.js +220 -0
- package/dist/src/pi-runner.js.map +1 -0
- package/dist/src/server.d.ts +13 -0
- package/dist/src/server.js +342 -0
- package/dist/src/server.js.map +1 -0
- package/docs/RELEASING.md +28 -0
- package/package.json +63 -0
- package/scripts/configure-install.py +530 -0
- package/scripts/doctor.py +192 -0
- package/scripts/install.sh +406 -0
- package/scripts/remove-brave-models.py +112 -0
- package/scripts/set-brave-default.py +119 -0
- package/scripts/uninstall.sh +133 -0
|
@@ -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
|