#!/bin/sh
# Briefkin installer: connects this computer's coding agents to your Briefkin wall.
#
#   curl -fsSL https://briefkin.dev/install | sh                  # approve in the browser
#   curl -fsSL https://briefkin.dev/install | sh -s -- bkc_...    # code from your dashboard
#
# It finds the agents installed here (Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Copilot CLI,
# Windsurf, Amp),
# gets a token for this computer, and for each one installs:
#   - the Briefkin skill, so the agent reports its tasks, milestones and questions;
#   - a hook, so every session shows up on the wall (working, waiting on you, done)
#     even when the agent says nothing;
# and, with node present, the usage collector: a script run every 15 minutes (launchd, a systemd user timer or cron)
# that reads the token counts Claude Code, Codex and OpenCode keep locally and posts daily totals, so the wall shows
# what your agents cost. Only counts leave the computer, never a prompt or a reply. Also with node, the routines
# runner: a background service that runs the routines in ~/.config/briefkin/routines/ when you press Run on the board.
# Credentials go in ~/.config/briefkin/env (mode 600). Re-running it is safe.
# T3 Code runs Codex or Claude Code underneath, so it is covered by them.
# Options (environment): BRIEFKIN_ONLY="claude codex" limits the agents set up;
# BRIEFKIN_NO_HOOKS=1 skips hooks; BRIEFKIN_NO_USAGE=1 skips the usage collector; BRIEFKIN_NO_ROUTINES=1 skips the
# routines runner; BRIEFKIN_NO_LIMITS=1 leaves the
# Claude Code status line alone (no AI limits tile); BRIEFKIN_URL points at another server; BRIEFKIN_TOKEN uses that token
# with no approval step (for cloud machines, such as a Claude Code cloud environment's setup script).
set -eu

URL=${BRIEFKIN_URL:-https://briefkin.dev}
DIR="$HOME/.config/briefkin"
ENV_FILE="$DIR/env"
HOOK="$DIR/briefkin-hook.sh"
SECRET=${1:-}

if [ -t 1 ]; then B=$(printf '\033[1m') D=$(printf '\033[2m') A=$(printf '\033[33m') G=$(printf '\033[32m') R=$(printf '\033[0m'); else B= D= A= G= R=; fi
say() { printf '%s\n' "$*"; }
ok() { printf '  %s✓%s %s\n' "$G" "$R" "$*"; }
note() { printf '  %s%s%s\n' "$D" "$*" "$R"; }
die() { printf '\n%sBriefkin setup stopped:%s %s\n' "$A" "$R" "$*" >&2; exit 1; }

command -v curl >/dev/null 2>&1 || die "curl is required."

# JSON and settings files are edited with node or python3, whichever is here.
if [ -n "${BRIEFKIN_JS:-}" ]; then JS=$BRIEFKIN_JS; elif command -v node >/dev/null 2>&1; then JS=node; elif command -v python3 >/dev/null 2>&1; then JS=python3; else JS=; fi

# json_get <key> <json>: a top-level string field.
json_get() {
  printf '%s' "$2" | tr -d '\n' | sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p"
}

# ---- 1. What is installed here ----

wanted() { [ -z "${BRIEFKIN_ONLY:-}" ] || case " $BRIEFKIN_ONLY " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
has() { command -v "$1" >/dev/null 2>&1; }

FOUND=
NAMES=
QUOTED=
add() { FOUND="$FOUND $1"; NAMES="${NAMES:+$NAMES, }$2"; QUOTED="${QUOTED:+$QUOTED,}\"$2\""; }
wanted claude && { [ -d "$HOME/.claude" ] || has claude; } && add claude "Claude Code"
wanted codex && { [ -d "${CODEX_HOME:-$HOME/.codex}" ] || has codex; } && add codex "Codex"
wanted cursor && { [ -d "$HOME/.cursor" ] || has cursor-agent || has cursor; } && add cursor "Cursor"
wanted opencode && { [ -d "$HOME/.config/opencode" ] || has opencode; } && add opencode "OpenCode"
wanted gemini && { [ -d "$HOME/.gemini" ] || has gemini; } && add gemini "Gemini CLI"
wanted copilot && { [ -d "$HOME/.copilot" ] || has copilot; } && add copilot "Copilot CLI"
wanted windsurf && { [ -d "$HOME/.codeium/windsurf" ] || has windsurf; } && add windsurf "Windsurf"
wanted amp && { [ -d "${XDG_CONFIG_HOME:-$HOME/.config}/amp" ] || has amp; } && add amp "Amp"

HOST=$(hostname 2>/dev/null | sed 's/\.local$//' | cut -c1-60)
[ -n "$HOST" ] || HOST="My computer"

say ""
say "${B}Briefkin${R} · connecting ${B}$HOST${R} to your wall"
if [ -n "$FOUND" ]; then note "Found: $NAMES"; else note "No coding agents found; setting up credentials for scripts and cron."; fi
say ""

MACHINE="\"host\":\"$(printf '%s' "$HOST" | tr -d '"\\')\",\"harnesses\":[$QUOTED]"

# ---- 2. A token for this computer ----

open_url() {
  if has open; then open "$1" >/dev/null 2>&1 || true
  elif has xdg-open; then xdg-open "$1" >/dev/null 2>&1 || true
  elif has wslview; then wslview "$1" >/dev/null 2>&1 || true
  fi
}

# Re-running on a connected computer keeps its token, so updating the hooks needs no approval.
# BRIEFKIN_NEW_TOKEN=1 asks for a new one.
TOKEN= GIVEN_TOKEN=
# This computer's own token, as an earlier run saved it: an agent session started here inherits it as BRIEFKIN_TOKEN
# (from ~/.claude/settings.json), which does not make this a cloud machine.
SAVED_TOKEN=$(sed -n 's/^BRIEFKIN_TOKEN=//p' "$ENV_FILE" 2>/dev/null | tail -n1)
# A token from the environment (a cloud machine's secret) is used as it is: there is nobody to approve it.
if [ -z "$SECRET" ] && [ -n "${BRIEFKIN_TOKEN:-}" ]; then
  GIVEN_TOKEN=1
  check=$(curl -sS -o /dev/null -w '%{http_code}' -H "authorization: Bearer $BRIEFKIN_TOKEN" "$URL/v1/cards/briefkin-install-check" 2>/dev/null) || check=000
  case "$check" in
    200|404) TOKEN=$BRIEFKIN_TOKEN ;;
    *) die "the BRIEFKIN_TOKEN in the environment was refused by $URL (HTTP $check)." ;;
  esac
fi
if [ -z "$TOKEN" ] && [ -z "$SECRET" ] && [ -z "${BRIEFKIN_NEW_TOKEN:-}" ] && [ -r "$ENV_FILE" ]; then
  old_url=$(sed -n 's/^BRIEFKIN_URL=//p' "$ENV_FILE" | tail -n1)
  old_token=$(sed -n 's/^BRIEFKIN_TOKEN=//p' "$ENV_FILE" | tail -n1)
  # The token also works when the saved URL is another address of the same server
  # (briefkin.petarjs.workers.dev before briefkin.dev); the check below decides.
  if [ -n "$old_token" ]; then
    check=$(curl -sS -o /dev/null -w '%{http_code}' -H "authorization: Bearer $old_token" "$URL/v1/cards/briefkin-install-check" 2>/dev/null) || check=000
    case "$check" in
      200|404) TOKEN=$old_token ;;
    esac
  fi
fi

if [ -n "$TOKEN" ]; then
  ok "Already connected: keeping this computer's token"
elif [ -z "$SECRET" ]; then
  start=$(curl -fsS -X POST "$URL/v1/connect/start" -H 'content-type: application/json' -d "{$MACHINE}") ||
    die "could not reach $URL."
  SECRET=$(json_get secret "$start")
  CODE=$(json_get user_code "$start")
  VERIFY=$(json_get verify_url "$start")
  [ -n "$SECRET" ] && [ -n "$VERIFY" ] || die "unexpected answer from $URL."
  say "Approve this computer in your browser (sign in with GitHub if asked):"
  say ""
  say "    ${B}$VERIFY${R}"
  say ""
  say "    Code: ${B}$CODE${R}"
  say ""
  open_url "$VERIFY"
fi

waited=0
[ -z "$TOKEN" ] && [ -n "${CODE:-}" ] && printf '  Waiting for approval'
while [ -z "$TOKEN" ]; do
  res=$(curl -sS -w '\n%{http_code}' -X POST "$URL/v1/connect/token" -H 'content-type: application/json' \
    -d "{\"secret\":\"$SECRET\",$MACHINE}" 2>/dev/null) || res="
000"
  status=$(printf '%s' "$res" | tail -n1)
  body=$(printf '%s' "$res" | sed '$d')
  case "$status" in
    200) TOKEN=$(json_get token "$body"); break ;;
    202) ;;
    410) printf '\n'; die "this setup command was already used or has expired. Run: curl -fsSL $URL/install | sh" ;;
    404) printf '\n'; die "this setup code is not valid. Run: curl -fsSL $URL/install | sh" ;;
    *) [ "$waited" -gt 30 ] && { printf '\n'; die "could not reach $URL (HTTP $status)."; } ;;
  esac
  waited=$((waited + 2))
  [ "$waited" -lt 900 ] || { printf '\n'; die "approval timed out. Run the command again."; }
  [ -n "${CODE:-}" ] && printf '.'
  sleep 2
done
if [ "$waited" -gt 0 ] || [ -n "$SECRET" ]; then
  [ -n "${CODE:-}" ] && printf '\n'
  [ -n "$TOKEN" ] || die "no token in the answer from $URL."
  ok "Approved: token created for $HOST"
fi

umask 077
mkdir -p "$DIR"
cat >"$ENV_FILE" <<EOF
# Briefkin credentials for this computer. Revoke the token in $URL/app/settings.
BRIEFKIN_URL=$URL
BRIEFKIN_TOKEN=$TOKEN
export BRIEFKIN_URL BRIEFKIN_TOKEN
EOF
chmod 600 "$ENV_FILE"
umask 022
ok "Saved credentials to ~/.config/briefkin/env"

# ---- 3. Skill and hooks per agent ----

SKILL_TMP=$(mktemp)
trap 'rm -f "$SKILL_TMP"' EXIT
curl -fsSL "$URL/skill.md" -o "$SKILL_TMP" || die "could not download the skill."
curl -fsSL "$URL/hooks.sh" -o "$HOOK" || die "could not download the hook."
chmod +x "$HOOK"
HOOK_VERSION=$(sed -n 's/^BRIEFKIN_HOOK_VERSION=//p' "$HOOK" | head -n1)
ok "Hook version ${HOOK_VERSION:-?} saved to ~/.config/briefkin/briefkin-hook.sh"

# Earlier setups saved the hook as ~/.claude/briefkin-hook.sh, and project settings
# files may still run that copy. Point it at the current hook so they stay up to date.
OLD_HOOK="$HOME/.claude/briefkin-hook.sh"
if [ -f "$OLD_HOOK" ] && ! grep -q '^# Briefkin: moved' "$OLD_HOOK"; then
  printf '#!/bin/sh\n# Briefkin: moved to ~/.config/briefkin/briefkin-hook.sh; this runs that one.\nexec "%s" "$@"\n' "$HOOK" >"$OLD_HOOK"
  chmod +x "$OLD_HOOK"
  ok "Old hook copy in ~/.claude now runs the current hook"
fi

install_skill() { # <skills dir>
  mkdir -p "$1/briefkin" && cp "$SKILL_TMP" "$1/briefkin/SKILL.md"
}

# merge_hooks <settings file> <harness> <style> <events...>
# Adds the Briefkin hook to each event in a JSON settings file, replacing an
# earlier Briefkin entry and keeping everything else. Styles:
#   claude: {"hooks": {"Event": [{"hooks": [{"type": "command", "command": ...}]}]}}  (Claude Code, Codex)
#           An earlier Briefkin entry (including hooks 12-16's background `claude wait` on Stop) is replaced.
#   gemini: the same, with "matcher": "*"
#   cursor: {"version": 1, "hooks": {"event": [{"command": ...}]}}
#   windsurf: {"hooks": {"event": [{"command": "... <event>", "show_output": false}]}}  (the event is passed on)
# With SET_ENV=1 it also stores the credentials under "env" (Claude Code).
# Needs node or python3; returns 1 without them. A backup of the file is kept.
MERGE_JS='
const fs = require("fs"), [f, harness, style, ...events] = process.argv.slice(1);
const cmd = `"${process.env.HOOK}" ${harness}`, mine = (h) => String(h.command || "").includes("briefkin-hook");
const text = fs.existsSync(f) ? fs.readFileSync(f, "utf8") : "";
const s = text.trim() ? JSON.parse(text) : {};
if (process.env.BRIEFKIN_SET_ENV) s.env = { ...s.env, BRIEFKIN_URL: process.env.BRIEFKIN_URL, BRIEFKIN_TOKEN: process.env.BRIEFKIN_TOKEN };
if (style === "cursor") s.version = s.version || 1;
s.hooks = s.hooks || {};
for (const ev of events) {
  const list = s.hooks[ev] || [];
  s.hooks[ev] = style === "cursor" ? [...list.filter((h) => !mine(h)), { command: cmd }]
    : style === "windsurf" ? [...list.filter((h) => !mine(h)), { command: `${cmd} ${ev}`, show_output: false }]
    : [...list.filter((g) => !(g.hooks || []).some(mine)),
       { ...(style === "gemini" && { matcher: "*" }), hooks: [{ type: "command", command: cmd, timeout: style === "gemini" ? 10000 : 10 }] }];
}
fs.writeFileSync(f, JSON.stringify(s, null, 2) + "\n");'
MERGE_PY='
import json, os, sys
f, harness, style, events = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4:]
cmd = "\"%s\" %s" % (os.environ["HOOK"], harness)
mine = lambda h: "briefkin-hook" in str(h.get("command", ""))
text = open(f).read() if os.path.exists(f) else ""
s = json.loads(text) if text.strip() else {}
if os.environ.get("BRIEFKIN_SET_ENV"):
    s.setdefault("env", {}).update(BRIEFKIN_URL=os.environ["BRIEFKIN_URL"], BRIEFKIN_TOKEN=os.environ["BRIEFKIN_TOKEN"])
if style == "cursor":
    s.setdefault("version", 1)
hooks = s.setdefault("hooks", {})
for ev in events:
    lst = hooks.get(ev, [])
    if style == "cursor":
        hooks[ev] = [h for h in lst if not mine(h)] + [{"command": cmd}]
    elif style == "windsurf":
        hooks[ev] = [h for h in lst if not mine(h)] + [{"command": "%s %s" % (cmd, ev), "show_output": False}]
    else:
        entry = {"hooks": [{"type": "command", "command": cmd, "timeout": 10000 if style == "gemini" else 10}]}
        if style == "gemini":
            entry = {"matcher": "*", **entry}
        hooks[ev] = [g for g in lst if not any(mine(h) for h in g.get("hooks", []))] + [entry]
open(f, "w").write(json.dumps(s, indent=2) + "\n")'

merge_hooks() {
  [ -n "$JS" ] || return 1
  mkdir -p "$(dirname "$1")"
  [ -f "$1" ] && cp "$1" "$1.briefkin-backup"
  if [ "$JS" = node ]; then HOOK="$HOOK" BRIEFKIN_URL="$URL" BRIEFKIN_TOKEN="$TOKEN" BRIEFKIN_SET_ENV="${SET_ENV:-}" node -e "$MERGE_JS" "$@"
  else HOOK="$HOOK" BRIEFKIN_URL="$URL" BRIEFKIN_TOKEN="$TOKEN" BRIEFKIN_SET_ENV="${SET_ENV:-}" python3 -c "$MERGE_PY" "$@"; fi
  rc=$?
  SET_ENV=
  return $rc
}

SKIPPED=

setup_claude() {
  install_skill "$HOME/.claude/skills"
  ok "Claude Code: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  # PostToolUse says the todo list changed or a permission prompt was answered (the hook exits at once otherwise);
  # SessionEnd closes the card.
  if SET_ENV=1 && merge_hooks "$HOME/.claude/settings.json" claude claude UserPromptSubmit Notification PostToolUse Stop SessionEnd; then
    ok "Claude Code: hooks and credentials added to ~/.claude/settings.json"
  else
    SKIPPED="$SKIPPED claude"
  fi
}

setup_codex() {
  home=${CODEX_HOME:-$HOME/.codex}
  # Codex reads user skills from ~/.agents/skills, which every run installs (below).
  ok "Codex: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  # Codex hooks: prompts, approval requests, turn ends (Stop) and session ends. PostToolUse says the plan changed
  # (update_plan) or an approval request was answered (the hook exits at once otherwise). The user trusts them
  # once with /hooks.
  if merge_hooks "$home/hooks.json" codex claude UserPromptSubmit PermissionRequest PostToolUse Stop SessionEnd; then
    ok "Codex: hooks added to hooks.json"
    CODEX_TRUST=1
    # Earlier installs reported turn ends through notify; the Stop hook does that now.
    cfg="$home/config.toml"
    if [ -f "$cfg" ] && grep -q '^notify = \[.*briefkin-hook' "$cfg"; then
      cp "$cfg" "$cfg.briefkin-backup"
      grep -v '^notify = \[.*briefkin-hook' "$cfg.briefkin-backup" | grep -v '^# Briefkin: report finished turns to your wall\.$' >"$cfg" || true
    fi
  else
    SKIPPED="$SKIPPED codex"
  fi
}

setup_cursor() {
  install_skill "$HOME/.cursor/skills"
  ok "Cursor: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  # postToolUse brings the todo list (the hook exits at once for every other tool).
  if merge_hooks "$HOME/.cursor/hooks.json" cursor cursor beforeSubmitPrompt afterAgentResponse postToolUse stop; then
    ok "Cursor: hooks added to ~/.cursor/hooks.json"
  else
    SKIPPED="$SKIPPED cursor"
  fi
}

setup_opencode() {
  base="$HOME/.config/opencode"
  install_skill "$base/skills"
  ok "OpenCode: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  mkdir -p "$base/plugins"
  if curl -fsSL "$URL/opencode-plugin.js" -o "$base/plugins/briefkin.js"; then
    ok "OpenCode: plugin installed"
  else
    SKIPPED="$SKIPPED opencode"
  fi
}

setup_gemini() {
  install_skill "$HOME/.gemini/skills"
  ok "Gemini CLI: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  if merge_hooks "$HOME/.gemini/settings.json" gemini gemini BeforeAgent Notification AfterAgent; then
    ok "Gemini CLI: hooks added to ~/.gemini/settings.json"
  else
    SKIPPED="$SKIPPED gemini"
  fi
}

setup_windsurf() {
  base="$HOME/.codeium/windsurf"
  install_skill "$base/skills"
  ok "Windsurf: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  if merge_hooks "$base/hooks.json" windsurf windsurf pre_user_prompt post_cascade_response; then
    ok "Windsurf: hooks added to ~/.codeium/windsurf/hooks.json"
  else
    SKIPPED="$SKIPPED windsurf"
  fi
}

setup_amp() {
  base="${XDG_CONFIG_HOME:-$HOME/.config}/amp"
  # Amp reads user skills from ~/.agents/skills.
  ok "Amp: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  mkdir -p "$base/plugins"
  if curl -fsSL "$URL/amp-plugin.ts" -o "$base/plugins/briefkin.ts"; then
    ok "Amp: plugin installed"
  else
    SKIPPED="$SKIPPED amp"
  fi
}

setup_copilot() {
  # Copilot CLI reads user skills from ~/.agents/skills; its hooks live in their own file.
  ok "Copilot CLI: skill installed"
  [ -n "${BRIEFKIN_NO_HOOKS:-}" ] && return
  mkdir -p "$HOME/.copilot/hooks"
  q=$(printf '%s' "$HOOK" | sed 's/"/\\"/g')
  cat >"$HOME/.copilot/hooks/briefkin.json" <<EOF
{
  "version": 1,
  "hooks": {
    "userPromptSubmitted": [{ "type": "command", "bash": "\\"$q\\" copilot prompt", "timeoutSec": 10 }],
    "permissionRequest": [{ "type": "command", "bash": "\\"$q\\" copilot permission", "timeoutSec": 10 }],
    "agentStop": [{ "type": "command", "bash": "\\"$q\\" copilot stop", "timeoutSec": 10 }]
  }
}
EOF
  ok "Copilot CLI: hooks added to ~/.copilot/hooks/briefkin.json"
}

for h in $FOUND; do "setup_$h"; done
# The shared skills folder: Codex, Copilot CLI, Cursor, OpenCode, Gemini CLI, Windsurf and Amp read it.
install_skill "$HOME/.agents/skills"

# launchd_load <label> <plist>: (re)loads a launchd job and only succeeds once launchd really has it. bootout
# returns before the old job is gone, so a bootstrap right after it can fail; wait for it to go, retry, and check
# with launchctl print rather than trusting the exit code of the legacy load.
launchd_load() {
  uid=$(id -u)
  launchctl bootout "gui/$uid/$1" >/dev/null 2>&1 || true
  n=0
  while launchctl print "gui/$uid/$1" >/dev/null 2>&1 && [ $n -lt 20 ]; do sleep 0.25; n=$((n + 1)); done
  n=0
  while [ $n -lt 5 ]; do
    launchctl bootstrap "gui/$uid" "$2" >/dev/null 2>&1 || launchctl load -w "$2" >/dev/null 2>&1 || true
    launchctl print "gui/$uid/$1" >/dev/null 2>&1 && return 0
    sleep 1
    n=$((n + 1))
  done
  return 1
}

# ---- 3b. AI usage collector ----

USAGE="$DIR/briefkin-usage.mjs"
USAGE_LOG="$DIR/usage.log"
USAGE_NOTE=

# schedule_usage: runs the collector every 15 minutes, with whatever this system has: launchd on macOS, a systemd
# user timer on Linux, else a crontab line. Each is replaced on re-run. Prints how it was scheduled.
schedule_usage() {
  node_bin=$(command -v node)
  if [ "$(uname -s)" = Darwin ] && has launchctl; then
    plist="$HOME/Library/LaunchAgents/dev.briefkin.usage.plist"
    mkdir -p "$HOME/Library/LaunchAgents"
    cat >"$plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>dev.briefkin.usage</string>
  <key>ProgramArguments</key><array><string>$node_bin</string><string>$USAGE</string></array>
  <key>StartInterval</key><integer>900</integer>
  <key>RunAtLoad</key><true/>
  <key>ProcessType</key><string>Background</string>
  <key>StandardOutPath</key><string>$USAGE_LOG</string>
  <key>StandardErrorPath</key><string>$USAGE_LOG</string>
  <key>EnvironmentVariables</key><dict><key>PATH</key><string>$PATH</string></dict>
</dict>
</plist>
EOF
    if launchd_load dev.briefkin.usage "$plist"; then
      echo "launchd, every 15 minutes (~/Library/LaunchAgents/dev.briefkin.usage.plist)"
      return 0
    fi
    rm -f "$plist"
  fi
  if has systemctl && systemctl --user show-environment >/dev/null 2>&1; then
    unit="$HOME/.config/systemd/user"
    mkdir -p "$unit"
    cat >"$unit/briefkin-usage.service" <<EOF
[Unit]
Description=Briefkin usage collector

[Service]
Type=oneshot
ExecStart=$node_bin $USAGE
StandardOutput=append:$USAGE_LOG
StandardError=append:$USAGE_LOG
EOF
    cat >"$unit/briefkin-usage.timer" <<EOF
[Unit]
Description=Briefkin usage collector, every 15 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=15min

[Install]
WantedBy=timers.target
EOF
    if systemctl --user daemon-reload >/dev/null 2>&1 && systemctl --user enable --now briefkin-usage.timer >/dev/null 2>&1; then
      echo "systemd user timer, every 15 minutes (briefkin-usage.timer)"
      return 0
    fi
  fi
  if has crontab; then
    line="*/15 * * * * $node_bin $USAGE >>$USAGE_LOG 2>&1 # briefkin-usage"
    { crontab -l 2>/dev/null | grep -v '# briefkin-usage$'; echo "$line"; } | crontab - 2>/dev/null && {
      echo "cron, every 15 minutes"
      return 0
    }
  fi
  return 1
}

# setup_statusline: points Claude Code's statusLine at the shim; a status line that was there before keeps running,
# from ~/.config/briefkin/statusline-command. Re-runs leave a shim that is already in place alone.
setup_statusline() {
  node_bin=$(command -v node)
  shim="$DIR/briefkin-statusline.sh"
  cat >"$shim" <<EOF
#!/bin/sh
# Briefkin: saves the limits Claude Code gives the status line (for the AI limits tile), then shows your status line.
D="\$HOME/.config/briefkin"
p=\$(cat)
printf '%s\\n' "\$p" >"\$D/claude-status.json.tmp" 2>/dev/null && mv -f "\$D/claude-status.json.tmp" "\$D/claude-status.json"
if [ -s "\$D/statusline-command" ]; then printf '%s' "\$p" | sh -c "\$(cat "\$D/statusline-command")"
else printf '%s' "\$p" | "$node_bin" "$USAGE" --status-line; fi
EOF
  chmod +x "$shim"
  SHIM="$shim" PREV="$DIR/statusline-command" node -e '
const fs = require("fs"), f = process.argv[1];
const text = fs.existsSync(f) ? fs.readFileSync(f, "utf8") : "";
const s = text.trim() ? JSON.parse(text) : {};
const cur = s.statusLine;
if (cur && typeof cur === "object" && cur.type === "command" && cur.command && !String(cur.command).includes("briefkin-statusline")) fs.writeFileSync(process.env.PREV, cur.command + "\n");
s.statusLine = { ...(cur && typeof cur === "object" ? cur : {}), type: "command", command: process.env.SHIM };
fs.writeFileSync(f, JSON.stringify(s, null, 2) + "\n");
' "$HOME/.claude/settings.json" 2>/dev/null
}

if [ -n "${BRIEFKIN_NO_USAGE:-}" ]; then
  :
elif ! has node; then
  USAGE_NOTE="AI usage tiles need node; install it and run the installer again to add the usage collector."
elif curl -fsSL "$URL/usage.mjs" -o "$USAGE"; then
  if how=$(schedule_usage); then
    ok "Usage collector: $how"
  else
    USAGE_NOTE="Usage collector saved, but no scheduler was found (launchd, systemd or cron); run it yourself: node $USAGE"
  fi
  # Claude Code tells its status line how much of the 5-hour and weekly limits is used. A two-line shim saves that
  # for the collector (the AI limits tile), then runs the status line you had, or prints a plain one.
  if [ -z "${BRIEFKIN_NO_LIMITS:-}" ] && [ -d "$HOME/.claude" ] && setup_statusline; then
    ok "Claude Code: status line saves your limits for the AI limits tile (~/.config/briefkin/briefkin-statusline.sh)"
  fi
  # The first totals now, in the background, so the tiles are on the wall by the time the browser opens.
  (node "$USAGE" >>"$USAGE_LOG" 2>&1 &)
else
  USAGE_NOTE="Could not download the usage collector; run the installer again later."
fi

# ---- 3c. Routines runner ----

ROUTINES="$DIR/briefkin-routines.mjs"
ROUTINES_LOG="$DIR/routines.log"
ROUTINES_NOTE=

# serve_routines: keeps the runner going in the background (launchd on macOS, a systemd user service on Linux), so a
# Run button on the board reaches this computer. Replaced on re-run. Prints how it runs.
serve_routines() {
  node_bin=$(command -v node)
  if [ "$(uname -s)" = Darwin ] && has launchctl; then
    plist="$HOME/Library/LaunchAgents/dev.briefkin.routines.plist"
    mkdir -p "$HOME/Library/LaunchAgents"
    cat >"$plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>dev.briefkin.routines</string>
  <key>ProgramArguments</key><array><string>$node_bin</string><string>$ROUTINES</string></array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>ThrottleInterval</key><integer>30</integer>
  <key>ProcessType</key><string>Background</string>
  <key>StandardOutPath</key><string>$ROUTINES_LOG</string>
  <key>StandardErrorPath</key><string>$ROUTINES_LOG</string>
  <key>EnvironmentVariables</key><dict><key>PATH</key><string>$PATH</string></dict>
</dict>
</plist>
EOF
    if launchd_load dev.briefkin.routines "$plist"; then
      echo "launchd (~/Library/LaunchAgents/dev.briefkin.routines.plist)"
      return 0
    fi
    rm -f "$plist"
  fi
  if has systemctl && systemctl --user show-environment >/dev/null 2>&1; then
    unit="$HOME/.config/systemd/user"
    mkdir -p "$unit"
    cat >"$unit/briefkin-routines.service" <<EOF
[Unit]
Description=Briefkin routines runner

[Service]
ExecStart=$node_bin $ROUTINES
Environment=PATH=$PATH
Restart=on-failure
RestartSec=30
StandardOutput=append:$ROUTINES_LOG
StandardError=append:$ROUTINES_LOG

[Install]
WantedBy=default.target
EOF
    if systemctl --user daemon-reload >/dev/null 2>&1 && systemctl --user enable briefkin-routines.service >/dev/null 2>&1 \
      && systemctl --user restart briefkin-routines.service >/dev/null 2>&1; then
      echo "systemd user service (briefkin-routines.service)"
      return 0
    fi
  fi
  return 1
}

# A routine to copy: files starting with _ are not run.
routine_example() {
  mkdir -p "$DIR/routines"
  cat >"$DIR/routines/_example.md" <<'EOF'
---
name: Summarize my open PRs
description: What is waiting on review, failing CI or ready to merge, across my repos.
agent: claude
inputs: repo? (Only this repo)
allowed_tools: Bash(gh pr list:*), Bash(gh pr view:*), Bash(gh pr checks:*)
timeout: 10
---
List my open pull requests with the gh CLI (only {{repo}} if this names a repository, else all of mine) and say, for
each, whether it waits on review, has failing checks or is ready to merge. Lead with the ones that need me.
EOF
}

# Cloud machines (a token given up front that this computer never saved) have no one to press a button for: no runner
# there. Re-running from an agent session on a computer that has the runner keeps it.
if [ -n "${BRIEFKIN_NO_ROUTINES:-}" ] || { [ -n "$GIVEN_TOKEN" ] && [ "$TOKEN" != "$SAVED_TOKEN" ]; }; then
  :
elif ! has node; then
  ROUTINES_NOTE="Routines (Run buttons on your board) need node; install it and run the installer again."
elif curl -fsSL "$URL/routines.mjs" -o "$ROUTINES"; then
  routine_example
  if how=$(serve_routines); then
    ok "Routines: listening for Run buttons, $how; your routines go in ~/.config/briefkin/routines/"
  else
    ROUTINES_NOTE="Routines runner saved, but no service manager was found (launchd or systemd); run it yourself: node $ROUTINES"
  fi
else
  ROUTINES_NOTE="Could not download the routines runner; run the installer again later."
fi

# ---- 4. First card ----

now=$(date +%s)
name=$(printf '%s' "$HOST" | tr -d '"\\')
key=$(printf '%s' "$HOST" | tr -c 'A-Za-z0-9._-' '-')
badges=$(printf '%s' "${QUOTED:-\"curl\"}" | sed 's/"\([^"]*\)"/{"label":"\1","tone":"ok"}/g')
first="{\"v\":1,\"event_id\":\"connect-$now-$$\",\"item_key\":\"briefkin/computer/$key\",\"project\":\"Briefkin\",
\"source\":\"$name\",\"kind\":\"update\",\"work_state\":\"done\",\"title\":\"$name is connected\",
\"summary\":\"${NAMES:-Scripts and cron jobs} will report here.\",
\"widget\":[{\"type\":\"Badges\",\"props\":{\"items\":[$badges]}},
{\"type\":\"Text\",\"props\":{\"text\":\"Each new session shows up as its own card: working, waiting on you, done.\",\"variant\":\"muted\"}}]}"
if curl -fsS -m 10 -o /dev/null -X POST "$URL/v1/events" -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' --data-binary "$first"; then
  ok "First card sent: it is on your wall now"
else
  note "Could not send the first card; check your connection and run the installer again."
fi

say ""
[ -n "$USAGE_NOTE" ] && note "$USAGE_NOTE"
[ -n "$ROUTINES_NOTE" ] && note "$ROUTINES_NOTE"
for h in $SKIPPED; do
  note "$h: hooks need node or python3 to edit its settings file; see $URL/docs#setup for the manual steps."
done
case " $FOUND " in *" codex "*)
  [ -n "${CODEX_TRUST:-}" ] && note "Codex: run /hooks once in Codex and trust the Briefkin hooks; until then Codex sessions do not report."
  note "Codex: the default sandbox gives shell commands no network, so the agent cannot post from it; hooks still"
  note "  report. To let it post too, add network_access = true under [sandbox_workspace_write] in ~/.codex/config.toml." ;;
esac
say "${B}Done.${R} Restart any running agent sessions so they load the skill and hooks."
say "Open your wall: ${B}$URL/app${R}"
say ""
