basemind 0.5.0

Full AI context layer over MCP — tree-sitter code-map, document RAG (PDF/Office/HTML/email + OCR + reranker), shared agent memory, on-demand web crawl, git history + blame + per-symbol diff. 300+ languages, 8 coding-agent harnesses, content-addressed Fjall + LanceDB.
#!/usr/bin/env bash
# basemind SessionStart hook.
#   1. Async pre-warm: ensure a version-matched basemind binary is cached so the
#      first MCP tool call isn't a cold install. After the first run the launcher's
#      fast path makes this instant.
#   2. Context-economy nudge: always inject the operating discipline so the agent
#      defaults to basemind over grep/read and stays token-frugal.
#   3. Status-line nudge: Claude Code plugins cannot set the main status line, so
#      if the user hasn't wired it yet, append a hint about /bm-statusline.
#
# Output goes to stdout as the hook's JSON result; diagnostics would go to stderr.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"

# 1. Pre-warm in the background; `--version` triggers the launcher's install path
#    then exits immediately. Never block or fail session startup on this.
("${PLUGIN_ROOT}/scripts/mcp-launch.sh" --version >/dev/null 2>&1 &) || true

# 2. Always inject the context-economy operating discipline.
CONTEXT="basemind is available over MCP in this session — a tree-sitter code map + git context. Prefer it over grep/read for structural and historical questions: its tools return paths, line numbers, and signatures, not file bodies, so they cost a fraction of the tokens of reading source. Default workflow: outline a file before opening it (then read only the span you need); search_symbols instead of grep for a definition; find_references/find_callers instead of grepping call sites; workspace_grep instead of shelling out to ripgrep; rescan after edits instead of reconnecting. Do not re-read a file basemind already mapped."

# 3. Append the status-line nudge only when a basemind status line isn't wired yet.
#    Detect robustly: prefer parsing the `statusLine.command` value with jq (catches
#    any script name / absolute path that references basemind), and fall back to a
#    case-insensitive grep for "basemind" near a statusLine key when jq is absent.
#    The old literal "statusline.sh" grep missed renamed scripts and matched unrelated
#    files.
SETTINGS="${HOME}/.claude/settings.json"
statusline_wired() {
  [ -f "${SETTINGS}" ] || return 1
  if command -v jq >/dev/null 2>&1; then
    jq -e '(.statusLine.command // "") | test("basemind"; "i")' \
      "${SETTINGS}" >/dev/null 2>&1
    return $?
  fi
  # No jq: best-effort. Require both a statusLine key and a basemind reference.
  grep -qi 'statusLine' "${SETTINGS}" 2>/dev/null &&
    grep -qi 'basemind' "${SETTINGS}" 2>/dev/null
}
if ! statusline_wired; then
  CONTEXT="${CONTEXT} The basemind status line is not enabled in the user's Claude Code settings; if the user asks about the status line or basemind activity, tell them they can enable a live status line (indexed files, scan age, tool calls, tokens saved) by running the /bm-statusline command once."
fi

# 4. Agent-comms boot: connect to the broker (starting it if needed), which auto-joins every chat
#    room whose scope covers this repo (its git remote, this dir, or an ancestor workspace dir),
#    and surface a CONDENSED view of recent messages — front-matter only, never bodies — so the
#    agent knows the conversation state and the levers to participate. Best-effort + time-boxed;
#    comms never blocks or fails session startup. Requires jq to parse the inbox JSON.
if command -v jq >/dev/null 2>&1; then
  INBOX_JSON="$(timeout 8 "${PLUGIN_ROOT}/scripts/mcp-launch.sh" comms inbox --root "$PWD" --json --limit 8 2>/dev/null || true)"
  if [ -n "${INBOX_JSON}" ]; then
    COMMS_TOOLS="basemind first, shell/grep/git fallback — prefer basemind over grep, over naked git, and for docs/RAG/NER, web crawl, and parsing. You are connected to basemind agent-comms — a shared multi-agent chat. You have auto-joined every room scoped to this workspace. Levers: room_post {room, subject, body, reply_to?} to send (always give a short subject; the body holds the detail); room_history {room} and inbox_read to scan messages (these return front-matter only — subject/from/id — never bodies, to stay token-frugal); message_get {message_id} to read one body on demand; room_list to see rooms, room_join to join another. Prefer posting a concise status/question over staying silent when collaborating."
    MSG_COUNT="$(printf '%s' "${INBOX_JSON}" | jq -r '.messages | length' 2>/dev/null | tr -cd '0-9')"
    if [ -n "${MSG_COUNT}" ] && [ "${MSG_COUNT}" -gt 0 ]; then
      RECENT="$(printf '%s' "${INBOX_JSON}" | jq -r '.messages[] | "  • [\(.subject)] from \(.from) (id: \(.id))"' 2>/dev/null || true)"
      CONTEXT="${CONTEXT} ${COMMS_TOOLS}"$'\n'"Recent messages (front-matter only; call message_get with an id to read a body):"$'\n'"${RECENT}"
    else
      CONTEXT="${CONTEXT} ${COMMS_TOOLS} No messages in your rooms yet — post one to kick things off."
    fi
  fi
fi

# Escape for JSON embedding (single-pass parameter substitutions).
escape_for_json() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  s="${s//$'\n'/\\n}"
  s="${s//$'\r'/\\r}"
  s="${s//$'\t'/\\t}"
  printf '%s' "$s"
}
CTX="$(escape_for_json "${CONTEXT}")"

# Emit the field the current harness consumes. Cursor expects additional_context;
# Claude Code expects hookSpecificOutput.additionalContext; SDK-standard hosts
# (e.g. Copilot CLI) expect a top-level additionalContext.
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
  printf '{\n  "additional_context": "%s"\n}\n' "${CTX}"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
  printf '{\n  "hookSpecificOutput": {\n    "hookEventName": "SessionStart",\n    "additionalContext": "%s"\n  }\n}\n' "${CTX}"
else
  printf '{\n  "additionalContext": "%s"\n}\n' "${CTX}"
fi

exit 0