v0.7: one-click install — pyproject, console scripts, user units, hermes wiring
A clone + ./install.sh + ./launch-brave.sh + sign in once. Nothing else. pyproject.toml declares the one real dependency (websockets) and maps the repo-root-is-the-package layout, which retires the PYTHONPATH=<parent> line every unit used to carry. cli.py backs three console scripts. They exist because `python3 -m socialmag` run from INSIDE a clone dies at `import asyncio`: the repo root is the package dir, so it ships selectors.py and queue.py, and python puts the current directory first on sys.path. `cd socialmag` is the first thing anyone does after a clone, so the documented workaround failed at exactly the moment it would be tried. A generated bin/ script never puts cwd on sys.path. The same trap bit `python3 -m venv` inside install.sh — the installer was broken by the thing it exists to fix. Every python3 call now runs from /. deploy/user/*.in replaces root system units with systemd --user templates: no User=, WantedBy=default.target, venv paths, @REPO@/@VENV@/@CDP_ENDPOINT@ substituted at install time. deploy/home/ is untouched and still live here. launch-brave.sh carries --disable-background-timer-throttling. That is load-bearing, not tidiness: five of six platforms are read in a hidden tab, and a throttled background tab stops running the timers those pages use to fetch their own data — the reads come back empty with no error. install.sh checks the mcp package in hermes's own interpreter and refuses to continue without it. Hermes treats mcp as optional and silently no-ops the whole subsystem when it is missing, so a server that never loaded looks exactly like one that loaded and did nothing. It will not merge into an existing mcp_servers: key — it backs up config.yaml and appends only when the key is absent, otherwise it prints the block. Blind YAML merging is how someone's other servers disappear. Verified end-to-end in a sandbox with a fake HOME and stubbed systemctl: real MCP initialize handshake from inside the repo dir returns all 7 tools; hermes's own yaml parser re-reads the written config with every pre-existing key intact; a second run leaves the mcp_servers key count at 1; launch-brave.sh exits 1 against a busy port; no unsubstituted @PLACEHOLDER@ in any rendered unit. Not fixed, pre-existing: server.py reports SERVER_VERSION 0.1.0 while the repo is at v0.7. The handshake shows 0.1.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,5 @@ state.json
|
|||||||
Archived/
|
Archived/
|
||||||
socialmag.db
|
socialmag.db
|
||||||
deploy/kdmirah/
|
deploy/kdmirah/
|
||||||
|
.venv/
|
||||||
|
*.egg-info/
|
||||||
|
|||||||
@@ -4,17 +4,57 @@ MCP server that posts, reads, and replies across 6 platforms — Threads, Facebo
|
|||||||
Instagram, YouTube, TikTok, and Twitter/X — by driving abang's already-logged-in
|
Instagram, YouTube, TikTok, and Twitter/X — by driving abang's already-logged-in
|
||||||
Brave over CDP. No API keys, no OAuth.
|
Brave over CDP. No API keys, no OAuth.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/juxtapo9090/socialmag.git
|
||||||
|
cd socialmag
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
That builds a venv, installs the package, writes four systemd **user** units,
|
||||||
|
and registers socialmag with Hermes as an MCP server. Then, once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./launch-brave.sh # opens Brave on the debugging port, own profile
|
||||||
|
```
|
||||||
|
|
||||||
|
Sign in to your platforms in that window. socialmag stores no passwords — it
|
||||||
|
drives the session that now belongs to that browser profile, which is why this
|
||||||
|
step is yours and cannot be scripted.
|
||||||
|
|
||||||
|
Afterwards: dashboard at `http://127.0.0.1:8321/dashboard.html`, a refresh pass
|
||||||
|
every 30 minutes, and `socialmag` / `socialmag-refresh` / `socialmag-openbridge`
|
||||||
|
on the venv's `PATH`.
|
||||||
|
|
||||||
|
⚠️ **Hermes treats the `mcp` Python package as optional.** Without it, every
|
||||||
|
`mcp_servers` entry is ignored — no error, one debug log line. `install.sh`
|
||||||
|
checks for it in Hermes's own interpreter and refuses to continue rather than
|
||||||
|
leave you with a server that looks registered and never loads.
|
||||||
|
|
||||||
|
⚠️ **`install.sh` will not merge into an existing `mcp_servers:` key.** It backs
|
||||||
|
up `config.yaml` and appends only when the key is absent; otherwise it prints the
|
||||||
|
block for you to paste. Blind YAML merging is how someone's other servers vanish.
|
||||||
|
|
||||||
|
⚠️ **User units stop at logout** unless `sudo loginctl enable-linger $USER`.
|
||||||
|
The installer says so when linger is off.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /mnt/playground/Playground_V2
|
socialmag # stdio MCP server, logs to stderr
|
||||||
python3 -m socialmag # stdio MCP server, logs to stderr
|
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ **Must run as a package.** `python3 socialmag/server.py` puts the package
|
⚠️ **Use the console script, not `python3 -m socialmag`, from inside the repo.**
|
||||||
dir first on `sys.path`, where `selectors.py` and `queue.py` shadow the stdlib
|
The repo root is the package dir, so it ships `selectors.py` and `queue.py` —
|
||||||
modules `asyncio` imports — asyncio then dies at import with
|
names the stdlib also uses. Python puts the current directory first on
|
||||||
`module 'selectors' has no attribute 'SelectSelector'`. Run `-m`.
|
`sys.path`, so from inside a clone, asyncio's own `import selectors` finds ours
|
||||||
|
and dies with `module 'selectors' has no attribute 'SelectSelector'`. The
|
||||||
|
generated `bin/socialmag` never puts the current directory on `sys.path`, so it
|
||||||
|
works from anywhere. `-m` still works from outside the repo.
|
||||||
|
|
||||||
|
The same trap bites `python3 -m venv` during install, which is why `install.sh`
|
||||||
|
runs every `python3` from `/`.
|
||||||
|
|
||||||
Brave must be listening on `http://127.0.0.1:9336` (`cdp.py:29`,
|
Brave must be listening on `http://127.0.0.1:9336` (`cdp.py:29`,
|
||||||
`DEFAULT_HTTP_ENDPOINT`). Override with `SOCIALMAG_CDP_ENDPOINT`.
|
`DEFAULT_HTTP_ENDPOINT`). Override with `SOCIALMAG_CDP_ENDPOINT`.
|
||||||
@@ -25,7 +65,7 @@ for this server, but no code looks at it — grep says the only env var consulte
|
|||||||
the default endpoint is already `:9336`. Change the port there and nothing happens,
|
the default endpoint is already `:9336`. Change the port there and nothing happens,
|
||||||
silently. Fix the name in the config, or teach `server.py` to read the port.
|
silently. Fix the name in the config, or teach `server.py` to read the port.
|
||||||
|
|
||||||
Requires `websockets` (17.1 present on this box). Everything else is stdlib.
|
Requires `websockets`. Everything else is stdlib.
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Console-script entry points.
|
||||||
|
|
||||||
|
These exist for one reason: `python3 -m socialmag` from inside the repo
|
||||||
|
directory dies at `import asyncio`.
|
||||||
|
|
||||||
|
The repo root is the package dir, so it holds `selectors.py` and `queue.py` —
|
||||||
|
names the stdlib also uses. `-m` puts the current directory first on
|
||||||
|
`sys.path`, so asyncio's own `import selectors` finds ours and fails with
|
||||||
|
`module 'selectors' has no attribute 'SelectSelector'`. Running from one
|
||||||
|
directory up avoids it, which is what the old PYTHONPATH units did.
|
||||||
|
|
||||||
|
A console script is generated in the venv's `bin/` and run by absolute path, so
|
||||||
|
the current directory never reaches `sys.path` at all. `cd socialmag` — the
|
||||||
|
first thing anyone does after a clone — stops being a trap.
|
||||||
|
|
||||||
|
The `-m` forms still work from outside the repo directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import openbridge as _openbridge
|
||||||
|
from . import refresher as _refresher
|
||||||
|
from . import server as _server
|
||||||
|
|
||||||
|
|
||||||
|
def serve() -> None:
|
||||||
|
"""socialmag — the stdio MCP server."""
|
||||||
|
try:
|
||||||
|
asyncio.run(_server.main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def refresh() -> int:
|
||||||
|
"""socialmag-refresh — one refresher pass, then exit."""
|
||||||
|
return asyncio.run(_refresher.main())
|
||||||
|
|
||||||
|
|
||||||
|
def openbridge() -> None:
|
||||||
|
"""socialmag-openbridge — the localhost open_link HTTP shim."""
|
||||||
|
_openbridge.main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(serve())
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=socialmag dashboard file server (:8321)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# Serves the repo directory itself, so the page is at
|
||||||
|
# http://127.0.0.1:8321/dashboard.html — refresher writes dashboard.json in
|
||||||
|
# here too, both resolved off the package's own __file__.
|
||||||
|
WorkingDirectory=@REPO@
|
||||||
|
# No --bind: the page is reachable from the LAN, not only from this box.
|
||||||
|
# openbridge stays loopback-only, so a LAN viewer just falls back to opening a
|
||||||
|
# new tab — progressive enhancement, and the page is never worse than it was.
|
||||||
|
ExecStart=@VENV@/bin/python -m http.server 8321
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=socialmag openbridge (:8322) — open_link over localhost HTTP
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=@REPO@
|
||||||
|
Environment=SOCIALMAG_CDP_ENDPOINT=@CDP_ENDPOINT@
|
||||||
|
ExecStart=@VENV@/bin/socialmag-openbridge
|
||||||
|
# openbridge refuses to move to another port if :8322 is taken — no silent
|
||||||
|
# fallback — so a clash fails loudly here instead of listening somewhere nobody
|
||||||
|
# is looking.
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=socialmag refresh — login states, activity, timeline capture
|
||||||
|
# The browser is not a service. There is nothing to order against, so a pass
|
||||||
|
# that finds no CDP records the third state and exits 0.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=@REPO@
|
||||||
|
Environment=SOCIALMAG_CDP_ENDPOINT=@CDP_ENDPOINT@
|
||||||
|
# --skip-foreground is the whole reason this can run on a desktop someone is
|
||||||
|
# using. Every other platform reads a JS-state signal in a hidden tab; tiktok
|
||||||
|
# still activates, so it is left out and keeps its last stored result with its
|
||||||
|
# own older checkedAt. Stale is honest. Drop the flag for a full pass by hand.
|
||||||
|
ExecStart=@VENV@/bin/socialmag-refresh --skip-foreground
|
||||||
|
# No Restart=: a down browser is a waiting state the refresher records and exits
|
||||||
|
# 0 for. The timer brings the next attempt, not a crash loop.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=socialmag refresh every 30 minutes
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# 2 min after login so the browser has a chance to be up; a pass that finds no
|
||||||
|
# browser still records the unknown state rather than skipping.
|
||||||
|
OnStartupSec=2min
|
||||||
|
OnUnitActiveSec=30min
|
||||||
|
# A pass takes ~30s with --skip-foreground. 30min is the desktop number: there
|
||||||
|
# is a human at this box, and the metrics table only needs enough resolution to
|
||||||
|
# watch engagement move, not to watch it live.
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Executable
+164
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# socialmag installer — clone, run this, sign in, done.
|
||||||
|
#
|
||||||
|
# What it does not do: log you in. socialmag holds no passwords, which is the
|
||||||
|
# whole design. The last step is yours and it only happens once.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
VENV="${SOCIALMAG_VENV:-$REPO/.venv}"
|
||||||
|
CDP_ENDPOINT="${SOCIALMAG_CDP_ENDPOINT:-http://127.0.0.1:9336}"
|
||||||
|
UNIT_DIR="$HOME/.config/systemd/user"
|
||||||
|
HERMES_CONFIG="${HERMES_CONFIG:-$HOME/.hermes/config.yaml}"
|
||||||
|
|
||||||
|
say() { printf '\n\033[1;36m==>\033[0m %s\n' "$*"; }
|
||||||
|
ok() { printf ' \033[32mok\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf ' \033[33m!!\033[0m %s\n' "$*"; }
|
||||||
|
die() { printf '\n\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- python
|
||||||
|
|
||||||
|
say "Python"
|
||||||
|
command -v python3 >/dev/null 2>&1 || die "python3 not found on PATH"
|
||||||
|
# Every python3 call below runs from / rather than from here. The repo root is
|
||||||
|
# the package dir, so it ships selectors.py and queue.py — names the stdlib also
|
||||||
|
# uses. Python puts the current directory first on sys.path, so `python3 -m venv`
|
||||||
|
# run from inside a fresh clone dies at `import subprocess` with
|
||||||
|
# "module 'selectors' has no attribute 'SelectSelector'". Running from / is the
|
||||||
|
# whole fix. (This is the same trap cli.py exists to keep off users.)
|
||||||
|
(cd / && python3 - <<'PY') || die "socialmag needs Python 3.10 or newer"
|
||||||
|
import sys
|
||||||
|
sys.exit(0 if sys.version_info >= (3, 10) else 1)
|
||||||
|
PY
|
||||||
|
ok "$(cd / && python3 -V)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- venv
|
||||||
|
|
||||||
|
say "Virtualenv + package"
|
||||||
|
if [[ ! -x "$VENV/bin/python" ]]; then
|
||||||
|
(cd / && python3 -m venv "$VENV") || die "could not create a venv at $VENV
|
||||||
|
Two things cause this. Read the traceback above to tell them apart:
|
||||||
|
- 'No module named venv' -> the venv module is not installed.
|
||||||
|
Debian/Ubuntu: sudo apt install python3-venv
|
||||||
|
- anything else -> report it; the installer already runs python
|
||||||
|
from / to keep this repo off sys.path."
|
||||||
|
ok "created $VENV"
|
||||||
|
else
|
||||||
|
ok "reusing $VENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Editable on purpose. socialmag resolves socialmag.db, state.json and
|
||||||
|
# dashboard.json relative to its own __file__; a non-editable install would put
|
||||||
|
# all three inside site-packages, where nothing else looks for them.
|
||||||
|
(cd / && "$VENV/bin/pip" install --quiet --upgrade pip) >/dev/null
|
||||||
|
(cd / && "$VENV/bin/pip" install --quiet -e "$REPO") || die "pip install failed"
|
||||||
|
ok "socialmag installed (editable), websockets pulled in"
|
||||||
|
|
||||||
|
for script in socialmag socialmag-refresh socialmag-openbridge; do
|
||||||
|
[[ -x "$VENV/bin/$script" ]] || die "console script $script was not generated — pyproject.toml [project.scripts] did not take"
|
||||||
|
done
|
||||||
|
ok "console scripts: socialmag, socialmag-refresh, socialmag-openbridge"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- systemd
|
||||||
|
|
||||||
|
say "systemd user units"
|
||||||
|
if ! command -v systemctl >/dev/null 2>&1; then
|
||||||
|
warn "systemctl not found — skipping units"
|
||||||
|
warn "run the three processes by hand: socialmag, socialmag-openbridge, and 'python -m http.server 8321' in $REPO"
|
||||||
|
else
|
||||||
|
mkdir -p "$UNIT_DIR"
|
||||||
|
for src in "$REPO"/deploy/user/*.in; do
|
||||||
|
dest="$UNIT_DIR/$(basename "${src%.in}")"
|
||||||
|
sed -e "s|@REPO@|$REPO|g" \
|
||||||
|
-e "s|@VENV@|$VENV|g" \
|
||||||
|
-e "s|@CDP_ENDPOINT@|$CDP_ENDPOINT|g" \
|
||||||
|
"$src" > "$dest"
|
||||||
|
ok "$(basename "$dest")"
|
||||||
|
done
|
||||||
|
cp "$REPO/deploy/user/socialmag-refresh.timer" "$UNIT_DIR/socialmag-refresh.timer"
|
||||||
|
ok "socialmag-refresh.timer"
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now socialmag-dash.service socialmag-openbridge.service >/dev/null
|
||||||
|
systemctl --user enable --now socialmag-refresh.timer >/dev/null
|
||||||
|
ok "enabled: dash (:8321), openbridge (:8322), refresh timer (30min)"
|
||||||
|
|
||||||
|
# These die at logout unless lingering is on. Say so rather than let someone
|
||||||
|
# discover it when the dashboard is empty tomorrow.
|
||||||
|
if ! loginctl show-user "$USER" -p Linger 2>/dev/null | grep -q 'Linger=yes'; then
|
||||||
|
warn "user services stop when you log out. To keep them running:"
|
||||||
|
warn " sudo loginctl enable-linger $USER"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- hermes
|
||||||
|
|
||||||
|
say "Hermes MCP registration"
|
||||||
|
HERMES_PY="/usr/local/lib/hermes-agent/venv/bin/python"
|
||||||
|
|
||||||
|
if ! command -v hermes >/dev/null 2>&1; then
|
||||||
|
warn "hermes not on PATH — skipping. Register manually later:"
|
||||||
|
printf '\n mcp_servers:\n socialmag:\n command: "%s/bin/socialmag"\n args: []\n env:\n SOCIALMAG_CDP_ENDPOINT: "%s"\n\n' "$VENV" "$CDP_ENDPOINT"
|
||||||
|
elif [[ ! -f "$HERMES_CONFIG" ]]; then
|
||||||
|
die "hermes is installed but $HERMES_CONFIG does not exist.
|
||||||
|
Start hermes once so it writes a config, then re-run this installer."
|
||||||
|
else
|
||||||
|
# Hermes treats the mcp python package as optional: without it the entire MCP
|
||||||
|
# subsystem is a no-op that logs at debug level. A server that never loaded
|
||||||
|
# looks exactly like one that loaded and did nothing, so check it here rather
|
||||||
|
# than let that be discovered later.
|
||||||
|
if [[ -x "$HERMES_PY" ]]; then
|
||||||
|
if "$HERMES_PY" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('mcp') else 1)"; then
|
||||||
|
ok "hermes has the mcp package"
|
||||||
|
else
|
||||||
|
die "hermes cannot load MCP servers: the 'mcp' package is missing from
|
||||||
|
$HERMES_PY
|
||||||
|
Without it hermes silently ignores every mcp_servers entry — no error, only a
|
||||||
|
debug log line. Install it into that interpreter, then re-run:
|
||||||
|
$HERMES_PY -m pip install mcp"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "could not find hermes's interpreter at $HERMES_PY — cannot check for the mcp package"
|
||||||
|
warn "if socialmag's tools never appear in hermes, that is the first thing to check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -qE '^\s*mcp_servers\s*:' "$HERMES_CONFIG"; then
|
||||||
|
warn "$HERMES_CONFIG already has an mcp_servers: key — not touching it."
|
||||||
|
warn "Merging YAML blind is how someone's other servers disappear. Add this by hand:"
|
||||||
|
printf '\n socialmag:\n command: "%s/bin/socialmag"\n args: []\n env:\n SOCIALMAG_CDP_ENDPOINT: "%s"\n\n' "$VENV" "$CDP_ENDPOINT"
|
||||||
|
else
|
||||||
|
backup="$HERMES_CONFIG.bak-pre-socialmag-$(date +%Y%m%d%H%M%S)"
|
||||||
|
cp "$HERMES_CONFIG" "$backup"
|
||||||
|
cat >> "$HERMES_CONFIG" <<EOF
|
||||||
|
mcp_servers:
|
||||||
|
socialmag:
|
||||||
|
command: "$VENV/bin/socialmag"
|
||||||
|
args: []
|
||||||
|
env:
|
||||||
|
SOCIALMAG_CDP_ENDPOINT: "$CDP_ENDPOINT"
|
||||||
|
EOF
|
||||||
|
ok "registered in $HERMES_CONFIG (backup: $(basename "$backup"))"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
$(printf '\033[1;32mInstalled.\033[0m') Two things left, both yours:
|
||||||
|
|
||||||
|
1. Start the browser socialmag drives:
|
||||||
|
|
||||||
|
$REPO/launch-brave.sh
|
||||||
|
|
||||||
|
2. Sign in to your platforms in that window — Threads, Facebook, Instagram,
|
||||||
|
YouTube, TikTok, X. Once. socialmag stores no passwords; it uses the
|
||||||
|
session that now belongs to that browser profile.
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
dashboard http://127.0.0.1:8321/dashboard.html
|
||||||
|
check $VENV/bin/socialmag-refresh
|
||||||
|
logs journalctl --user -u socialmag-refresh -f
|
||||||
|
|
||||||
|
EOF
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Launch Brave with the debugging port socialmag talks to.
|
||||||
|
#
|
||||||
|
# socialmag never holds a password. It drives a browser that is already signed
|
||||||
|
# in as you, which means there has to be a browser, listening, with your
|
||||||
|
# sessions in it. This script is that browser.
|
||||||
|
#
|
||||||
|
# Sign in to your platforms once in the window this opens. The profile is
|
||||||
|
# separate from your everyday Brave on purpose: socialmag navigates tabs and
|
||||||
|
# clicks things, and it should not be doing that in the window you are reading
|
||||||
|
# in.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT="${SOCIALMAG_CDP_PORT:-9336}"
|
||||||
|
PROFILE="${SOCIALMAG_BRAVE_PROFILE:-$HOME/.local/share/brave-socialmag}"
|
||||||
|
|
||||||
|
BRAVE=""
|
||||||
|
for candidate in brave brave-browser brave-browser-stable; do
|
||||||
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
|
BRAVE="$(command -v "$candidate")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$BRAVE" ]]; then
|
||||||
|
echo "error: no brave binary found on PATH" >&2
|
||||||
|
echo " looked for: brave, brave-browser, brave-browser-stable" >&2
|
||||||
|
echo " install Brave, or set BRAVE=/path/to/brave and re-run" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -sf --max-time 2 "http://127.0.0.1:${PORT}/json/version" >/dev/null 2>&1; then
|
||||||
|
echo "error: something is already listening on 127.0.0.1:${PORT}" >&2
|
||||||
|
echo " socialmag would drive that browser, not the one this script opens." >&2
|
||||||
|
echo " close it first, or set SOCIALMAG_CDP_PORT to a free port." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$PROFILE"
|
||||||
|
|
||||||
|
echo "brave : $BRAVE"
|
||||||
|
echo "profile : $PROFILE"
|
||||||
|
echo "cdp : http://127.0.0.1:${PORT}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --disable-background-timer-throttling is load-bearing, not tidiness. socialmag
|
||||||
|
# reads five of six platforms in a HIDDEN tab so it never steals your focus. A
|
||||||
|
# throttled background tab stops running the timers those pages use to fetch
|
||||||
|
# their own data, and the reads come back empty with no error.
|
||||||
|
exec "$BRAVE" \
|
||||||
|
--remote-debugging-port="$PORT" \
|
||||||
|
--user-data-dir="$PROFILE" \
|
||||||
|
--disable-background-timer-throttling \
|
||||||
|
"$@"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=64"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "socialmag"
|
||||||
|
version = "0.6.0"
|
||||||
|
description = "MCP server that posts, reads, and replies across 6 platforms by driving an already-logged-in Brave over CDP"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = ["websockets>=12"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/juxtapo9090/socialmag"
|
||||||
|
|
||||||
|
# Console scripts, not `-m`. See cli.py for why: `-m socialmag` run from inside
|
||||||
|
# the repo directory dies at `import asyncio`, because the repo root is the
|
||||||
|
# package dir and ships `selectors.py`/`queue.py`. A generated bin/ script never
|
||||||
|
# puts the current directory on sys.path.
|
||||||
|
[project.scripts]
|
||||||
|
socialmag = "socialmag.cli:serve"
|
||||||
|
socialmag-refresh = "socialmag.cli:refresh"
|
||||||
|
socialmag-openbridge = "socialmag.cli:openbridge"
|
||||||
|
|
||||||
|
# The repo root IS the package dir — `socialmag/__init__.py` sits beside this
|
||||||
|
# file. That is why every unit used to carry PYTHONPATH=<parent>: the import
|
||||||
|
# name only resolved from one directory up. The mapping below tells setuptools
|
||||||
|
# the same thing once, so an installed package imports from anywhere and the
|
||||||
|
# PYTHONPATH line disappears from the units.
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = { "socialmag" = "." }
|
||||||
|
packages = ["socialmag", "socialmag.platforms"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
socialmag = ["dashboard.html", "dashboard.json"]
|
||||||
Reference in New Issue
Block a user