@pa1nd/horse-browser 0.4.0
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/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +96 -0
- package/agent-helpers.py +215 -0
- package/bin/horse-browser +419 -0
- package/claude-md.sh +103 -0
- package/extension/background.js +139 -0
- package/extension/hello.html +233 -0
- package/extension/hello.js +12 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +14 -0
- package/extension/monitor.css +234 -0
- package/extension/monitor.html +53 -0
- package/extension/monitor.js +541 -0
- package/install.sh +178 -0
- package/package.json +53 -0
- package/scripts/postinstall.sh +34 -0
package/install.sh
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One-time setup for horse-browser. Safe to re-run.
|
|
3
|
+
#
|
|
4
|
+
# 1. Fetches Chrome for Testing (a dedicated, automation-purposed browser that
|
|
5
|
+
# coexists with your daily browser) via @puppeteer/browsers — you install
|
|
6
|
+
# nothing by hand. Override with HORSE_BROWSER_BIN=/path/to/chromium to use
|
|
7
|
+
# your own Chromium instead.
|
|
8
|
+
# 2. Writes config + symlinks the `horse-browser` launcher onto your PATH.
|
|
9
|
+
# 3. Launches the browser for the first time (sign into your apps — logins
|
|
10
|
+
# persist), then smoke-tests the whole chain via browser-harness if present.
|
|
11
|
+
#
|
|
12
|
+
# Env overrides: HORSE_BROWSER_BIN, HORSE_BROWSER_PORT (9223),
|
|
13
|
+
# HORSE_BROWSER_PROFILE (~/.config/horse-browser/profile).
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
PORT="${HORSE_BROWSER_PORT:-9223}"
|
|
18
|
+
PROFILE="${HORSE_BROWSER_PROFILE:-$HOME/.config/horse-browser/profile}"
|
|
19
|
+
CONFIG_DIR="$HOME/.config/horse-browser"
|
|
20
|
+
CONFIG="$CONFIG_DIR/config"
|
|
21
|
+
CACHE="$HOME/.cache/horse-browser"
|
|
22
|
+
BINDIR="$HOME/.local/bin"
|
|
23
|
+
EXT="$HERE/extension"
|
|
24
|
+
|
|
25
|
+
mkdir -p "$CONFIG_DIR" "$BINDIR" "$CACHE"
|
|
26
|
+
|
|
27
|
+
# 1. browser ──────────────────────────────────────────────────────────────────
|
|
28
|
+
BIN="${HORSE_BROWSER_BIN:-}"
|
|
29
|
+
if [ -n "$BIN" ]; then
|
|
30
|
+
echo "Using your nominated browser: $BIN"
|
|
31
|
+
else
|
|
32
|
+
if ! command -v npx >/dev/null 2>&1; then
|
|
33
|
+
echo "ERROR: npx (Node) not found — needed to fetch Chrome for Testing." >&2
|
|
34
|
+
echo " Install Node, or set HORSE_BROWSER_BIN to a Chromium binary, then re-run." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
echo "Fetching Chrome for Testing via @puppeteer/browsers (one-time, ~170MB)…"
|
|
38
|
+
out="$(npx -y @puppeteer/browsers install chrome@stable --path "$CACHE")"
|
|
39
|
+
# output line: "chrome@<version> <path-to-executable>" (path may contain spaces)
|
|
40
|
+
BIN="$(printf '%s\n' "$out" | grep '^chrome@' | tail -1 | sed 's/^[^ ]* //')"
|
|
41
|
+
fi
|
|
42
|
+
if [ ! -x "$BIN" ]; then
|
|
43
|
+
echo "ERROR: browser binary not found / not executable: $BIN" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
echo "✓ browser: $BIN"
|
|
47
|
+
|
|
48
|
+
# 2. config + launcher on PATH ─────────────────────────────────────────────────
|
|
49
|
+
cat > "$CONFIG" <<EOF
|
|
50
|
+
# horse-browser config — written by install.sh
|
|
51
|
+
BROWSER_BIN="$BIN"
|
|
52
|
+
EXTENSION_DIR="$EXT"
|
|
53
|
+
PORT="$PORT"
|
|
54
|
+
PROFILE="$PROFILE"
|
|
55
|
+
EOF
|
|
56
|
+
# When installed via npm (`npm i -g @pa1nd/horse-browser`), npm owns the launcher
|
|
57
|
+
# symlink (package.json "bin") — don't lay down a second, competing one in ~/.local/bin.
|
|
58
|
+
if [ -n "${HORSE_FROM_NPM:-}" ]; then
|
|
59
|
+
echo "✓ launcher: managed by npm (bin: horse-browser) (config: $CONFIG)"
|
|
60
|
+
else
|
|
61
|
+
ln -sf "$HERE/bin/horse-browser" "$BINDIR/horse-browser"
|
|
62
|
+
echo "✓ launcher: $BINDIR/horse-browser (config: $CONFIG)"
|
|
63
|
+
case ":$PATH:" in *":$BINDIR:"*) ;; *) echo " note: $BINDIR isn't on your PATH — add it so 'horse-browser' resolves";; esac
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# 3. bh_open helpers into browser-harness ───────────────────────────────────────
|
|
67
|
+
# browser-harness auto-loads <workspace>/agent_helpers.py on every call; we append our
|
|
68
|
+
# bh_open/bh_list/bh_switch_tab helpers there so they exist on the first run — no agent
|
|
69
|
+
# installs the recipe by hand. The workspace location varies by version/install (≤0.1.0:
|
|
70
|
+
# <repo>/agent-workspace; 0.1.1+: ~/.config/browser-harness/agent-workspace; or whatever
|
|
71
|
+
# BH_AGENT_WORKSPACE points at) — so instead of guessing, we ASK browser-harness itself
|
|
72
|
+
# where it loads from via its own python (helpers.AGENT_WORKSPACE). Append-once.
|
|
73
|
+
if ! command -v browser-harness >/dev/null 2>&1; then
|
|
74
|
+
# browser-harness is a separate (Python) prerequisite. In a hand/curl install it's
|
|
75
|
+
# required up front. Under npm it's a *declared* prereq the user may not have yet — so
|
|
76
|
+
# warn and finish cleanly (config + Chrome are already in place; re-run setup once
|
|
77
|
+
# browser-harness is installed to get the bh_open helpers) rather than failing the install.
|
|
78
|
+
echo "! browser-harness not found on PATH — skipping the bh_open helper sync." >&2
|
|
79
|
+
echo " Install it (e.g. 'uv tool install browser-harness'), then re-run setup." >&2
|
|
80
|
+
[ -n "${HORSE_FROM_NPM:-}" ] && exit 0
|
|
81
|
+
echo " (https://github.com/browser-use/browser-harness)" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
HELPERS_SRC="$HERE/agent-helpers.py"
|
|
85
|
+
HELPERS_MARKER="# ── horse-browser helpers (installed by horse-browser/install.sh) ──"
|
|
86
|
+
install_helpers_into() { # $1 = workspace dir; (re)syncs our helpers block — idempotent, so
|
|
87
|
+
local dst="$1/agent_helpers.py" # re-running install.sh deploys helper UPDATES, not just the first time.
|
|
88
|
+
mkdir -p "$1" 2>/dev/null || return 0
|
|
89
|
+
# drop any previously-installed block (marker → EOF), trim trailing blanks, then re-append
|
|
90
|
+
if [ -f "$dst" ] && grep -qF "$HELPERS_MARKER" "$dst"; then
|
|
91
|
+
awk -v m="$HELPERS_MARKER" 'index($0,m){exit} {print}' "$dst" \
|
|
92
|
+
| awk 'NF{last=NR} {b[NR]=$0} END{for(i=1;i<=last;i++)print b[i]}' > "$dst.tmp" && mv "$dst.tmp" "$dst"
|
|
93
|
+
fi
|
|
94
|
+
{ [ -s "$dst" ] && printf '\n\n'; printf '%s\n' "$HELPERS_MARKER"; cat "$HELPERS_SRC"; } >> "$dst"
|
|
95
|
+
echo "✓ synced bh_open helpers → $dst"
|
|
96
|
+
}
|
|
97
|
+
# (a) the workspace browser-harness ACTUALLY loads from — ask it via its own python
|
|
98
|
+
# (the CLI's shebang points at it); helpers.AGENT_WORKSPACE honours BH_AGENT_WORKSPACE and
|
|
99
|
+
# resolves the right default on every version. Fall back to known defaults if the query fails.
|
|
100
|
+
WS_NEW=""
|
|
101
|
+
BHPY="$(head -1 "$(command -v browser-harness)" 2>/dev/null | sed 's/^#!//;s/ .*//')"
|
|
102
|
+
[ -n "$BHPY" ] && [ -x "$BHPY" ] && WS_NEW="$("$BHPY" -c 'from browser_harness.helpers import AGENT_WORKSPACE; print(AGENT_WORKSPACE)' 2>/dev/null)"
|
|
103
|
+
[ -z "$WS_NEW" ] && WS_NEW="${BH_AGENT_WORKSPACE:-$HOME/.config/browser-harness/agent-workspace}"
|
|
104
|
+
install_helpers_into "$WS_NEW"
|
|
105
|
+
# (b) a source checkout's agent-workspace, if present (editable/dev installs)
|
|
106
|
+
BH_DIR="${BROWSER_HARNESS_DIR:-}"
|
|
107
|
+
if [ -z "$BH_DIR" ]; then
|
|
108
|
+
for c in "$HOME/Developer/browser-harness" "$HOME/browser-harness"; do
|
|
109
|
+
[ -f "$c/agent-workspace/agent_helpers.py" ] && { BH_DIR="$c"; break; }
|
|
110
|
+
done
|
|
111
|
+
fi
|
|
112
|
+
[ -n "$BH_DIR" ] && [ "$BH_DIR/agent-workspace" != "$WS_NEW" ] && install_helpers_into "$BH_DIR/agent-workspace"
|
|
113
|
+
|
|
114
|
+
# Keep the stable symlink that ~/.claude/CLAUDE.md's @-import points at aimed at the current
|
|
115
|
+
# (Python-version-specific) packaged SKILL, so the import never rots across reinstalls. We
|
|
116
|
+
# only refresh the symlink — registering the block in CLAUDE.md is opt-in (./claude-md.sh apply).
|
|
117
|
+
# Under npm we don't reach into ~/.claude from a silent postinstall — the next-steps
|
|
118
|
+
# note tells the user how to wire SKILL.md into their CLAUDE.md themselves.
|
|
119
|
+
if [ -z "${HORSE_FROM_NPM:-}" ]; then
|
|
120
|
+
"$HERE/claude-md.sh" symlink || echo " (skipped CLAUDE.md SKILL symlink — see ./claude-md.sh)" >&2
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# 4. first launch + smoke test ─────────────────────────────────────────────────
|
|
124
|
+
# HORSE_SKIP_LAUNCH=1 skips this whole step — used by the "update" path (re-running
|
|
125
|
+
# install for a fresh pull) where relaunching the browser + a 40s smoke test would
|
|
126
|
+
# be noise. Steps 1–3 (browser fetch, config, launcher, helpers) still run.
|
|
127
|
+
if [ -n "${HORSE_SKIP_LAUNCH:-}" ]; then
|
|
128
|
+
echo "✓ setup refreshed (skipped launch/smoke-test: HORSE_SKIP_LAUNCH set)"
|
|
129
|
+
echo " Restart to pick up changes: horse-browser"
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
echo "Launching for the first time…"
|
|
134
|
+
"$BINDIR/horse-browser" || true
|
|
135
|
+
|
|
136
|
+
if command -v browser-harness >/dev/null 2>&1; then
|
|
137
|
+
export BU_CDP_URL="http://127.0.0.1:$PORT"
|
|
138
|
+
read -r -d '' check <<'PY' || true
|
|
139
|
+
import browser_harness.helpers as _h
|
|
140
|
+
from browser_harness.helpers import cdp
|
|
141
|
+
# our helpers must have loaded AND overridden cdp (so auto-home is active), not just added bh_open
|
|
142
|
+
if not hasattr(_h, "bh_open") or getattr(_h.cdp, "__module__", "") != "browser_harness_agent_helpers":
|
|
143
|
+
print("NOHELPERS"); raise SystemExit(0)
|
|
144
|
+
sw = next((t["targetId"] for t in cdp("Target.getTargets")["targetInfos"]
|
|
145
|
+
if t.get("type") == "service_worker"
|
|
146
|
+
and t.get("url", "").startswith("chrome-extension://")), None)
|
|
147
|
+
if not sw:
|
|
148
|
+
print("PENDING"); raise SystemExit(0)
|
|
149
|
+
s = cdp("Target.attachToTarget", targetId=sw, flatten=True)["sessionId"]
|
|
150
|
+
r = cdp("Runtime.evaluate", session_id=s,
|
|
151
|
+
expression="self.listTabs ? self.listTabs('__install_check__') : 'NOFN'",
|
|
152
|
+
awaitPromise=True, returnByValue=True)
|
|
153
|
+
cdp("Target.detachFromTarget", sessionId=s)
|
|
154
|
+
print("READY" if isinstance(r.get("result", {}).get("value"), list) else "PENDING")
|
|
155
|
+
PY
|
|
156
|
+
echo "Verifying through browser-harness…"
|
|
157
|
+
verified=""; result=""
|
|
158
|
+
for _ in $(seq 1 20); do # ~40s — must clear the SW's first 30s keepalive tick
|
|
159
|
+
result="$(printf '%s' "$check" | browser-harness 2>/dev/null | tail -1)"
|
|
160
|
+
[ "$result" = "READY" ] && { verified=1; break; }
|
|
161
|
+
[ "$result" = "NOHELPERS" ] && break # helpers not loading — no point retrying
|
|
162
|
+
sleep 2
|
|
163
|
+
done
|
|
164
|
+
if [ -n "$verified" ]; then
|
|
165
|
+
echo "✓ verified — bh_open loaded + listTabs() answered over CDP; the extension is live"
|
|
166
|
+
elif [ "$result" = "NOHELPERS" ]; then
|
|
167
|
+
echo "! bh_open did NOT load — browser-harness isn't reading the workspace we appended to." >&2
|
|
168
|
+
echo " Set BH_AGENT_WORKSPACE to its agent-workspace dir and re-run ./install.sh." >&2
|
|
169
|
+
else
|
|
170
|
+
echo "! couldn't confirm the extension within ~40s — check the browser window."
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
echo
|
|
175
|
+
echo "Next:"
|
|
176
|
+
echo " • Sign into the apps you want your agents to use — logins persist in $PROFILE"
|
|
177
|
+
echo " • Point CDP clients at it: export BU_CDP_URL=http://127.0.0.1:$PORT"
|
|
178
|
+
echo " • (Re)launch anytime — agents too — with: horse-browser"
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pa1nd/horse-browser",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "A dedicated, focus-safe browser for AI agents: per-session colored tab groups, a live monitor wall, and a browser-harness drop-in launcher. Log in once — every agent inherits the session, and none of them steals your focus.",
|
|
8
|
+
"bin": {
|
|
9
|
+
"horse-browser": "bin/horse-browser"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"extension",
|
|
14
|
+
"agent-helpers.py",
|
|
15
|
+
"scripts",
|
|
16
|
+
"install.sh",
|
|
17
|
+
"claude-md.sh",
|
|
18
|
+
"SKILL.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"postinstall": "bash scripts/postinstall.sh"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@puppeteer/browsers": "^3.0.6"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"os": [
|
|
30
|
+
"darwin"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"author": "pA1nD",
|
|
34
|
+
"homepage": "https://github.com/pA1nD/horse-browser#readme",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/pA1nD/horse-browser.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/pA1nD/horse-browser/issues"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"browser",
|
|
44
|
+
"cdp",
|
|
45
|
+
"chrome-devtools",
|
|
46
|
+
"ai-agents",
|
|
47
|
+
"browser-automation",
|
|
48
|
+
"browser-harness",
|
|
49
|
+
"puppeteer",
|
|
50
|
+
"tab-groups",
|
|
51
|
+
"macos"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# npm postinstall for @pa1nd/horse-browser.
|
|
3
|
+
#
|
|
4
|
+
# Runs the package's own install.sh in "npm mode" (HORSE_FROM_NPM) so all the
|
|
5
|
+
# battle-tested setup — fetch Chrome for Testing, write ~/.config/horse-browser/config
|
|
6
|
+
# pointing at THIS install's extension/, sync the bh_open helpers if browser-harness is
|
|
7
|
+
# present — lives in exactly one place. npm mode differs only in what npm already owns
|
|
8
|
+
# or what a silent install shouldn't touch: it skips the ~/.local/bin launcher symlink
|
|
9
|
+
# (npm's "bin" field handles it), skips the ~/.claude SKILL symlink, and doesn't launch
|
|
10
|
+
# the browser / smoke-test on install (HORSE_SKIP_LAUNCH).
|
|
11
|
+
#
|
|
12
|
+
# This must NEVER fail `npm install`: a missing browser-harness, an offline Chrome fetch,
|
|
13
|
+
# or any hiccup degrades to a printed next-step, not a broken install. `horse-browser
|
|
14
|
+
# update` re-fetches Chrome later; re-running setup syncs helpers once browser-harness is in.
|
|
15
|
+
set -u
|
|
16
|
+
|
|
17
|
+
PKG="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
18
|
+
cd "$PKG" || exit 0 # cwd = package root so `npx @puppeteer/browsers` resolves the bundled fetcher
|
|
19
|
+
|
|
20
|
+
echo "horse-browser · setting up (Chrome for Testing + config)…"
|
|
21
|
+
HORSE_FROM_NPM=1 HORSE_SKIP_LAUNCH=1 bash "$PKG/install.sh" || \
|
|
22
|
+
echo "horse-browser · setup didn't fully complete — run 'horse-browser update' to fetch Chrome." >&2
|
|
23
|
+
|
|
24
|
+
cat <<EOF
|
|
25
|
+
|
|
26
|
+
horse-browser installed. Next:
|
|
27
|
+
• Start it (launches the browser, no focus steal): horse-browser
|
|
28
|
+
• Sign into your apps once — logins persist for every agent.
|
|
29
|
+
• Prereq for driving it: browser-harness (a Python tool) —
|
|
30
|
+
uv tool install browser-harness # or: pipx install browser-harness
|
|
31
|
+
• Teach agents the bh_open discipline — add this to your ~/.claude/CLAUDE.md:
|
|
32
|
+
@$PKG/SKILL.md
|
|
33
|
+
EOF
|
|
34
|
+
exit 0
|