#!/usr/bin/env bash # VibeMon installer — curl one-liner setup # Usage: curl -fsSL https://vibemon.dev/install.sh | sh -s -- API_KEY set -euo pipefail # ─── Pre-flight checks ─────────────────────────────────────────────── VIBEMON_VERSION="8" API_KEY="${1:-}" IS_UPDATE=false if [ -z "$API_KEY" ]; then if [ -f "$HOME/.vibemon/api-key" ]; then API_KEY=$(cat "$HOME/.vibemon/api-key") IS_UPDATE=true else echo "❌ API key is required." echo "Usage: curl -fsSL https://…/install | sh -s -- YOUR_API_KEY" exit 1 fi fi for cmd in curl python3; do if ! command -v "$cmd" &>/dev/null; then echo "❌ '$cmd' is not installed. Please install it first." exit 1 fi done API_URL="https://sirpdtcwawcidhgtltps.supabase.co/functions/v1" VIBEMON_DIR="$HOME/.vibemon" CLAUDE_SETTINGS="$HOME/.claude/settings.json" GEMINI_SETTINGS="$HOME/.gemini/settings.json" if [ "$IS_UPDATE" = true ]; then echo "🐾 Updating VibeMon… (v$VIBEMON_VERSION)" else echo "🐾 Installing VibeMon… (v$VIBEMON_VERSION)" fi # ─── 1. Create directory ───────────────────────────────────────────── mkdir -p "$VIBEMON_DIR" # ─── 2. Save API key ───────────────────────────────────────────────── printf '%s' "$API_KEY" > "$VIBEMON_DIR/api-key" chmod 0600 "$VIBEMON_DIR/api-key" echo " ✓ API key saved" # ─── 2.5. Save version ──────────────────────────────────────────────── printf '%s' "$VIBEMON_VERSION" > "$VIBEMON_DIR/version" echo " ✓ Version v$VIBEMON_VERSION recorded" # ─── 3. Write notify.sh (v2 thin client) ─────────────────────────── cat > "$VIBEMON_DIR/notify.sh" << 'NOTIFY_SCRIPT' #!/usr/bin/env bash # VibeMon v2 thin client — all events route through /hook set -euo pipefail VIBEMON_DIR="$HOME/.vibemon" API_KEY_FILE="$VIBEMON_DIR/api-key" API_URL="https://sirpdtcwawcidhgtltps.supabase.co/functions/v1" if [ ! -f "$API_KEY_FILE" ]; then echo "[vibemon] API key not found at $API_KEY_FILE" >&2 exit 1 fi API_KEY=$(cat "$API_KEY_FILE") VIBEMON_VER=$(cat "$VIBEMON_DIR/version" 2>/dev/null || echo "0") EVENT_TYPE="${1:-unknown}" AGENT="${2:-claude_code}" # Save stdin to temp file (handles large tool_input payloads safely) STDIN_FILE=$(mktemp) trap "rm -f $STDIN_FILE" EXIT if [ ! -t 0 ]; then cat > "$STDIN_FILE" fi # ─── Auto-update check (only on session_start, non-blocking) ────── if [ "$EVENT_TYPE" = "session_start" ]; then _vibemon_update_check() { local LAST_CHECK="$VIBEMON_DIR/last-update-check" local NOW=$(date +%s) if [ -f "$LAST_CHECK" ]; then local LAST=$(cat "$LAST_CHECK") if [ $(( NOW - LAST )) -lt 86400 ]; then return fi fi printf '%s' "$NOW" > "$LAST_CHECK" local LATEST=$(curl -sf "$API_URL/install?v" 2>/dev/null || true) local CURRENT="" [ -f "$VIBEMON_DIR/version" ] && CURRENT=$(cat "$VIBEMON_DIR/version") if [ -n "$LATEST" ] && [ "$LATEST" != "$CURRENT" ]; then curl -fsSL "$API_URL/install" 2>/dev/null | bash -s 2>/dev/null fi } (_vibemon_update_check) & fi # ─── Detect project identifier (owner/repo from git remote, or dir name) ─ PROJECT_ROOT="" _url=$(git -C "$(pwd)" remote get-url origin 2>/dev/null || true) if [ -n "$_url" ]; then _url="${_url%.git}" case "$_url" in *://*) PROJECT_ROOT="$(basename "$(dirname "$_url")")/$(basename "$_url")" ;; *) PROJECT_ROOT="${_url#*:}" ;; esac elif _root=$(git -C "$(pwd)" rev-parse --show-toplevel 2>/dev/null) && [ -n "$_root" ]; then PROJECT_ROOT=$(basename "$_root") fi # ─── Build envelope and send to /hook ───────────────────────────── # Count lines locally and strip source code before sending (privacy) HOOK_BODY=$(VIBEMON_EVT="$EVENT_TYPE" \ VIBEMON_AGENT="$AGENT" \ VIBEMON_CWD="$(pwd)" \ VIBEMON_ROOT="${PROJECT_ROOT:-}" \ VIBEMON_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ VIBEMON_FILE="$STDIN_FILE" \ python3 -c " import json, os def count_nl(s): if not s: return 0 return sum(1 for l in s.split(chr(10)) if l.strip()) try: with open(os.environ['VIBEMON_FILE']) as f: raw = f.read() p = json.loads(raw) if raw.strip() else {} except Exception: p = {} ti = p.get('tool_input') or {} if isinstance(ti, dict) and ti: tn = (p.get('tool_name') or p.get('tool') or '').lower() if tn in ('write', 'write_file'): p['lines_added'] = count_nl(ti.get('content')) p['lines_removed'] = 0 elif tn in ('edit', 'notebookedit', 'replace'): nw = count_nl(ti.get('new_string') or ti.get('new_source')) ol = count_nl(ti.get('old_string') or ti.get('old_source')) p['lines_added'] = max(0, nw - ol) p['lines_removed'] = max(0, ol - nw) fp = ti.get('file_path') p['tool_input'] = {'file_path': fp} if fp else {} root = os.environ.get('VIBEMON_ROOT', '') if root: p['project_root'] = root env = {'event': os.environ['VIBEMON_EVT'], 'payload': p, 'cwd': os.environ['VIBEMON_CWD'], 'timestamp': os.environ['VIBEMON_TS'], 'agent': os.environ.get('VIBEMON_AGENT', 'claude_code')} if root: env['project_root'] = root print(json.dumps(env)) " 2>/dev/null) if [ -z "$HOOK_BODY" ]; then HOOK_BODY="{\"event\":\"$EVENT_TYPE\",\"payload\":{},\"cwd\":\"$(pwd)\",\"agent\":\"$AGENT\"}" fi if [ "$EVENT_TYPE" = "test" ]; then # test is synchronous — verify connection HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$API_URL/hook" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $API_KEY" \ -H "X-Vibemon-Version: $VIBEMON_VER" \ -d "$HOOK_BODY") if [ "$HTTP_CODE" = "200" ]; then echo "[vibemon] ✓ Connection successful" else echo "[vibemon] ✗ Connection failed (HTTP $HTTP_CODE)" >&2 exit 1 fi else # All other events are fire-and-forget (curl -s -X POST "$API_URL/hook" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $API_KEY" \ -H "X-Vibemon-Version: $VIBEMON_VER" \ -d "$HOOK_BODY" \ > /dev/null 2>&1) & fi # Gemini CLI requires JSON stdout response if [ "$AGENT" = "gemini_cli" ]; then echo '{"decision":"allow"}' fi NOTIFY_SCRIPT # Replace placeholder URL in notify.sh sed -i.bak "s|https://sirpdtcwawcidhgtltps.supabase.co|${API_URL%/functions/v1}|g" "$VIBEMON_DIR/notify.sh" rm -f "$VIBEMON_DIR/notify.sh.bak" chmod 0755 "$VIBEMON_DIR/notify.sh" echo " ✓ notify.sh created" # ─── 4a. Merge Claude Code hooks into settings.json ────────────────── mkdir -p "$(dirname "$CLAUDE_SETTINGS")" python3 - "$CLAUDE_SETTINGS" << 'PYMERGE' import json, sys, os settings_path = sys.argv[1] # Load existing settings settings = {} if os.path.exists(settings_path): with open(settings_path, "r") as f: try: settings = json.load(f) except json.JSONDecodeError: settings = {} hooks = settings.setdefault("hooks", {}) vibemon_hooks = { "PostToolUse": [ { "matcher": "Edit|Write|NotebookEdit", "hooks": [{"type": "command", "command": "bash ~/.vibemon/notify.sh activity claude_code"}] } ], "UserPromptSubmit": [ { "hooks": [{"type": "command", "command": "bash ~/.vibemon/notify.sh prompt claude_code"}] } ], "Stop": [ { "hooks": [{"type": "command", "command": "bash ~/.vibemon/notify.sh stop claude_code"}] } ], "Notification": [ { "matcher": "permission_prompt", "hooks": [{"type": "command", "command": "bash ~/.vibemon/notify.sh permission claude_code"}] } ], "SessionStart": [ { "hooks": [{"type": "command", "command": "bash ~/.vibemon/notify.sh session_start claude_code"}] } ], "SessionEnd": [ { "hooks": [{"type": "command", "command": "bash ~/.vibemon/notify.sh session_end claude_code"}] } ], "PostToolUseFailure": [ { "matcher": "Edit|Write|NotebookEdit", "hooks": [{"type": "command", "command": "bash ~/.vibemon/notify.sh tool_failure claude_code"}] } ] } for event_name, new_entries in vibemon_hooks.items(): existing = hooks.get(event_name, []) # Remove any previous vibemon entries existing = [e for e in existing if not any( "vibemon" in (h.get("command", "") if isinstance(h, dict) else h) for h in e.get("hooks", []) )] existing.extend(new_entries) hooks[event_name] = existing settings["hooks"] = hooks with open(settings_path, "w") as f: json.dump(settings, f, indent=2, ensure_ascii=False) f.write("\n") PYMERGE echo " ✓ Claude Code hooks configured ($CLAUDE_SETTINGS)" # ─── 4b. Merge Gemini CLI hooks into settings.json ───────────────── mkdir -p "$(dirname "$GEMINI_SETTINGS")" python3 - "$GEMINI_SETTINGS" << 'PYMERGE_GEMINI' import json, sys, os settings_path = sys.argv[1] settings = {} if os.path.exists(settings_path): with open(settings_path, "r") as f: try: settings = json.load(f) except json.JSONDecodeError: settings = {} hooks = settings.setdefault("hooks", {}) vibemon_hooks = { "AfterTool": [ { "matcher": "write_file|replace", "hooks": [{"name": "vibemon-exp", "type": "command", "command": "bash ~/.vibemon/notify.sh activity gemini_cli", "timeout": 5000}] } ], "SessionStart": [ { "hooks": [{"name": "vibemon-session-start", "type": "command", "command": "bash ~/.vibemon/notify.sh session_start gemini_cli", "timeout": 5000}] } ], "SessionEnd": [ { "hooks": [{"name": "vibemon-session-end", "type": "command", "command": "bash ~/.vibemon/notify.sh session_end gemini_cli", "timeout": 5000}] } ], "BeforeAgent": [ { "hooks": [{"name": "vibemon-prompt", "type": "command", "command": "bash ~/.vibemon/notify.sh prompt gemini_cli", "timeout": 5000}] } ], "AfterAgent": [ { "hooks": [{"name": "vibemon-stop", "type": "command", "command": "bash ~/.vibemon/notify.sh stop gemini_cli", "timeout": 5000}] } ] } for event_name, new_entries in vibemon_hooks.items(): existing = hooks.get(event_name, []) existing = [e for e in existing if not any( "vibemon" in (h.get("command", "") if isinstance(h, dict) else h) for h in e.get("hooks", []) )] existing.extend(new_entries) hooks[event_name] = existing settings["hooks"] = hooks with open(settings_path, "w") as f: json.dump(settings, f, indent=2, ensure_ascii=False) f.write("\n") PYMERGE_GEMINI echo " ✓ Gemini CLI hooks configured ($GEMINI_SETTINGS)" # ─── 4c. Cursor hooks (.cursor/hooks.json) ───────────────────────── CURSOR_HOOKS="$HOME/.cursor/hooks.json" if command -v cursor &>/dev/null || [ -d "$HOME/.cursor" ]; then mkdir -p "$(dirname "$CURSOR_HOOKS")" python3 - "$CURSOR_HOOKS" << 'PYMERGE_CURSOR' import json, sys, os hooks_path = sys.argv[1] config = {} if os.path.exists(hooks_path): with open(hooks_path, "r") as f: try: config = json.load(f) except json.JSONDecodeError: config = {} hooks = config.setdefault("hooks", {}) vibemon_hooks = { "afterFileEdit": [ {"command": "bash ~/.vibemon/notify.sh activity cursor", "timeout": 5000} ], "afterFileCreate": [ {"command": "bash ~/.vibemon/notify.sh activity cursor", "timeout": 5000} ] } for event_name, new_entries in vibemon_hooks.items(): existing = hooks.get(event_name, []) existing = [e for e in existing if "vibemon" not in e.get("command", "")] existing.extend(new_entries) hooks[event_name] = existing config["hooks"] = hooks with open(hooks_path, "w") as f: json.dump(config, f, indent=2, ensure_ascii=False) f.write("\n") PYMERGE_CURSOR echo " ✓ Cursor hooks configured ($CURSOR_HOOKS)" fi # ─── 4d. Codex CLI (session-level only) ──────────────────────────── CODEX_SETTINGS="$HOME/.codex/settings.json" if command -v codex &>/dev/null || [ -d "$HOME/.codex" ]; then mkdir -p "$(dirname "$CODEX_SETTINGS")" python3 - "$CODEX_SETTINGS" << 'PYMERGE_CODEX' import json, sys, os settings_path = sys.argv[1] settings = {} if os.path.exists(settings_path): with open(settings_path, "r") as f: try: settings = json.load(f) except json.JSONDecodeError: settings = {} hooks = settings.setdefault("hooks", {}) vibemon_hooks = { "SessionStart": [ {"command": "bash ~/.vibemon/notify.sh session_start codex_cli", "timeout": 5000} ], "SessionEnd": [ {"command": "bash ~/.vibemon/notify.sh session_end codex_cli", "timeout": 5000} ] } for event_name, new_entries in vibemon_hooks.items(): existing = hooks.get(event_name, []) existing = [e for e in existing if "vibemon" not in e.get("command", "")] existing.extend(new_entries) hooks[event_name] = existing settings["hooks"] = hooks with open(settings_path, "w") as f: json.dump(settings, f, indent=2, ensure_ascii=False) f.write("\n") PYMERGE_CODEX echo " ✓ Codex CLI hooks configured ($CODEX_SETTINGS)" fi # ─── 5. Test connection ────────────────────────────────────────────── echo "" echo "🔗 Testing connection…" bash "$VIBEMON_DIR/notify.sh" test echo "" if [ "$IS_UPDATE" = true ]; then echo "🎉 VibeMon updated successfully! (v$VIBEMON_VERSION)" else echo "🎉 VibeMon installed successfully!" echo " Your slime will grow as you code with Claude Code, Gemini CLI, Cursor, or Codex." fi