#!/usr/bin/env sh
# agents.sh — Synapse context-vault CLI (sourced shell library for @eborja/synapse)
#
# Source from your shell rc (one-time setup via `synapse install --write`). Since the engine
# ships as an npm package, this file lives at the package root (or node_modules/@eborja/synapse/):
#     export SYNAPSE_VAULT="/path/to/vault"; source ".../node_modules/@eborja/synapse/agents.sh"
#
# One-time setup:
#     npx synapse install --write
# That sources this file and bakes SYNAPSE_VAULT as a safety-net override. Per-call vault
# resolution still prefers $PWD (flat or nested layout).
#
# Vault resolution (per command, from $PWD):
#   • dir with agents/ + _meta/tools/context.manifest.json  → that vault (flat)
#   • dir/context-vault/ with the same                       → nested vault
#   • else $SYNAPSE_VAULT override / monorepo synapse-vault default
#
# ── What you get ──────────────────────────────────────────────────────────────
#
#   curator                              # launch default CLI as agent-curator
#   curator hub-finances                 # agent + a target hub (standard — auto-upgrades)
#   reconciler hub-contacts              # agent + a single domain hub
#   curator hub-finances --profile fat   # override the profile (context dial)
#   curator hub-finances "regenerate the Q2 summary view"   # seed a task (+semantic)
#
#   synapse                # unified front door — `synapse help` lists everything
#   synapse agents         # list all agent commands + their purpose  (vault-agents)
#   synapse hubs           # list all hub targets (valid second arg)   (vault-hubs)
#   synapse profiles       # explain the three profiles (context dials)(vault-profiles)
#   synapse models         # list models for a CLI (--cli opencode|claude|cursor) (vault-models)
#   synapse reload         # force re-source agents.sh (also auto-reloads each prompt)(vault-reload)
#
# ── Syntax ────────────────────────────────────────────────────────────────────
#   <agent-name> [<target-id>] [--profile lean|standard|fat]
#     [--cli opencode|claude|cursor|clip|print]
#     [--model <id>] [--auto|--bypass|--manual] [--no-semantic] [--clipboard] ["task text"]
#
# Engine tools resolve via: `synapse` bin on PATH → package lib/ → node_modules/@eborja/synapse
# → legacy _meta/tools/*.mjs shims.
#
# Must be SOURCED, not executed. zsh + bash (+ POSIX sh) supported.

# ── defaults (override in your env) ───────────────────────────────────────────
: "${SYNAPSE_MODEL:=ollama/qwen3.6-256k}"
: "${SYNAPSE_CLI:=opencode}"
: "${SYNAPSE_CURSOR_MODEL:=auto}"
# Bedrock is opt-in only (subscription-dependent). Set on or run: vault-bedrock on
: "${SYNAPSE_CURSOR_BEDROCK:=off}"
# Semantic-recall Ollama endpoint (augment.mjs/gen-embeddings.mjs only; NOT opencode's runtime).
: "${SYNAPSE_OLLAMA_URL:=http://localhost:11434}"

# ── locate THIS script (package-aware — may live in node_modules/@eborja/synapse/) ──
if [ -n "${ZSH_VERSION:-}" ]; then
  _mx_self="${(%):-%x}"
elif [ -n "${BASH_VERSION:-}" ]; then
  _mx_self="${BASH_SOURCE[0]}"
else
  _mx_self="$0"
fi
_SYN_SELF_FILE="$(cd "$(dirname "$_mx_self")" 2>/dev/null && pwd)/$(basename "$_mx_self")"
export _SYN_SELF_FILE
# Package root = dirname of agents.sh (when shipped at package root).
_MX_PKG_ROOT="$(cd "$(dirname "$_mx_self")" 2>/dev/null && pwd)"
unset _mx_self

# Monorepo convenience: if this package sits next to synapse-vault/, default there outside any vault.
_MX_DEFAULT_VAULT="$(cd "$_MX_PKG_ROOT/../synapse-vault" 2>/dev/null && pwd)"
if [ ! -d "${_MX_DEFAULT_VAULT:-}/agents" ] || [ ! -f "${_MX_DEFAULT_VAULT:-}/_meta/tools/context.manifest.json" ]; then
  # Standalone: default is whichever vault $PWD / $SYNAPSE_VAULT resolves to.
  _MX_DEFAULT_VAULT=""
fi

export SYNAPSE_MODEL SYNAPSE_CLI SYNAPSE_OLLAMA_URL SYNAPSE_CURSOR_MODEL SYNAPSE_CURSOR_BEDROCK

# ── resolve the EFFECTIVE vault for a single invocation ───────────────────────
# A dir D is a vault when it has agents/ + _meta/tools/context.manifest.json (flat),
# or D/context-vault/ with the same (nested). Engine scripts may live in the npm package,
# so we no longer require _meta/tools/render.mjs to be present.
__mx_is_vault() { [ -d "$1/agents" ] && [ -f "$1/_meta/tools/context.manifest.json" ]; }

__mx_vault() {
  _mx_d="$PWD"
  while [ -n "$_mx_d" ] && [ "$_mx_d" != "/" ]; do
    if   __mx_is_vault "$_mx_d";               then printf '%s\n' "$_mx_d"; return 0
    elif __mx_is_vault "$_mx_d/context-vault"; then printf '%s\n' "$_mx_d/context-vault"; return 0
    fi
    _mx_d="$(dirname "$_mx_d")"
  done
  if [ -n "${SYNAPSE_VAULT:-}" ]; then
    if   __mx_is_vault "$SYNAPSE_VAULT";               then printf '%s\n' "$SYNAPSE_VAULT"; return 0
    elif __mx_is_vault "$SYNAPSE_VAULT/context-vault"; then printf '%s\n' "$SYNAPSE_VAULT/context-vault"; return 0
    fi
  fi
  if [ -n "${_MX_DEFAULT_VAULT:-}" ] && __mx_is_vault "$_MX_DEFAULT_VAULT"; then
    printf '%s\n' "$_MX_DEFAULT_VAULT"; return 0
  fi
  return 1
}

# ── resolve engine tools from the npm package (or legacy shims / package lib/) ──
__mx_have_bin() { command -v synapse >/dev/null 2>&1; }

__mx_tool() {
  # $1 = render | augment | gen-embeddings | …
  _mx_name="$1"
  _mx_file="${_mx_name}.mjs"

  # Prefer the package's own lib/ when agents.sh is sourced from the package root (dev / linked).
  if [ -f "$_MX_PKG_ROOT/lib/$_mx_file" ]; then
    printf '%s\n' "$_MX_PKG_ROOT/lib/$_mx_file"
    unset _mx_name _mx_file
    return 0
  fi

  _mx_start="$(__mx_vault 2>/dev/null || true)"
  [ -n "$_mx_start" ] || _mx_start="$PWD"
  for _mx_base in "$_mx_start" "$PWD"; do
    _mx_d="$_mx_base"
    while [ -n "$_mx_d" ] && [ "$_mx_d" != "/" ]; do
      if [ -f "$_mx_d/node_modules/@eborja/synapse/lib/$_mx_file" ]; then
        printf '%s\n' "$_mx_d/node_modules/@eborja/synapse/lib/$_mx_file"
        unset _mx_name _mx_file _mx_start _mx_base _mx_d
        return 0
      fi
      # Legacy shim / in-vault engine (pre-package).
      if [ -f "$_mx_d/_meta/tools/$_mx_file" ]; then
        printf '%s\n' "$_mx_d/_meta/tools/$_mx_file"
        unset _mx_name _mx_file _mx_start _mx_base _mx_d
        return 0
      fi
      _mx_d="$(dirname "$_mx_d")"
    done
  done

  case "$_mx_name" in
    render|augment) _mx_sub="@eborja/synapse/$_mx_name" ;;
    *)              _mx_sub="" ;;
  esac
  if [ -n "$_mx_sub" ]; then
    _mx_p="$(node --input-type=module -e \
      "import{fileURLToPath}from 'node:url';process.stdout.write(fileURLToPath(import.meta.resolve('$_mx_sub')))" \
      2>/dev/null)"
    if [ -n "$_mx_p" ] && [ -f "$_mx_p" ]; then
      printf '%s\n' "$_mx_p"
      unset _mx_name _mx_file _mx_start _mx_base _mx_d _mx_sub _mx_p
      return 0
    fi
  fi
  unset _mx_name _mx_file _mx_start _mx_base _mx_d _mx_sub _mx_p
  return 1
}

__mx_run() {
  # $1 = synapse subcommand (render|augment|…) OR legacy tool basename without .mjs
  _mx_cmd="$1"; shift
  if __mx_have_bin; then
    synapse "$_mx_cmd" "$@"
    _mx_rc=$?
  else
    _mx_toolpath="$(__mx_tool "$_mx_cmd")"
    if [ -z "$_mx_toolpath" ]; then
      echo "agents.sh: could not resolve synapse '$_mx_cmd' (no 'synapse' bin and no @eborja/synapse install)." >&2
      unset _mx_cmd _mx_toolpath
      return 127
    fi
    node "$_mx_toolpath" "$@"
    _mx_rc=$?
    unset _mx_toolpath
  fi
  unset _mx_cmd
  return $_mx_rc
}

if ! __mx_vault >/dev/null 2>&1 && [ -z "${SYNAPSE_VAULT:-}" ]; then
  echo "agents.sh: no vault found yet (cd into a vault, or set SYNAPSE_VAULT). Commands will resolve per-call." >&2
fi

# Launch an agent by short name; profile read from the effective vault at call time.
__mx_agent_cmd() {
  _name="$1"; shift
  _vault="$(__mx_vault)"
  _prof="$(_mx_field "$_vault/agents/agent-${_name}.md" profile)"
  _prof="${_prof:-lean}"
  __mx_launch "agent-${_name}" "$_prof" "$@"
}

# ── clipboard helper (same tool matrix as render --copy) ──────────────────────
__mx_clip() {
  if   command -v pbcopy >/dev/null 2>&1; then pbcopy
  elif command -v clip   >/dev/null 2>&1; then clip
  elif command -v xclip  >/dev/null 2>&1; then xclip -selection clipboard
  else cat; fi
}

# ── per-CLI model memory (last explicit --model wins until overridden) ────────
__mx_last_model_path() { printf '%s/.synapse-last-model-%s' "${HOME}" "$1"; }

__mx_last_model_save() {
  [ -n "${2:-}" ] || return 0
  printf '%s\n' "$2" > "$(__mx_last_model_path "$1")" 2>/dev/null || true
}

__mx_last_model_load() {
  cat "$(__mx_last_model_path "$1")" 2>/dev/null | head -1
}

# Resolve the model for a CLI: explicit --model > env/config default > last-used > fallback.
__mx_resolve_model() {
  _mx_cli="$1"
  _mx_explicit="$2"
  if [ -n "$_mx_explicit" ]; then
    __mx_last_model_save "$_mx_cli" "$_mx_explicit"
    printf '%s\n' "$_mx_explicit"
    return 0
  fi

  case "$_mx_cli" in
    cursor)
      if [ -n "${SYNAPSE_CURSOR_MODEL:-}" ]; then
        printf '%s\n' "$SYNAPSE_CURSOR_MODEL"; return 0
      fi
      _mx_cfg="${HOME}/.cursor/cli-config.json"
      if [ -f "$_mx_cfg" ] && command -v node >/dev/null 2>&1; then
        _mx_from_cfg="$(node -e '
          try {
            const c = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
            const sel = c.selectedModel?.modelId || c.model?.modelId || "";
            const aliases = c.model?.aliases;
            if (sel === "default" && aliases?.[0]) process.stdout.write(aliases[0]);
            else if (sel) process.stdout.write(sel);
          } catch {}
        ' "$_mx_cfg" 2>/dev/null)"
        if [ -n "$_mx_from_cfg" ]; then
          printf '%s\n' "$_mx_from_cfg"; return 0
        fi
      fi
      _mx_last="$(__mx_last_model_load cursor)"
      if [ -n "$_mx_last" ]; then printf '%s\n' "$_mx_last"; return 0; fi
      printf '%s\n' "auto"
      ;;
    opencode)
      if [ -n "${SYNAPSE_MODEL:-}" ]; then
        printf '%s\n' "$SYNAPSE_MODEL"; return 0
      fi
      _mx_last="$(__mx_last_model_load opencode)"
      if [ -n "$_mx_last" ]; then printf '%s\n' "$_mx_last"; return 0; fi
      _mx_oc_cfg="${HOME}/.config/opencode/opencode.json"
      if [ -f "$_mx_oc_cfg" ] && command -v node >/dev/null 2>&1; then
        _mx_from_cfg="$(node -e '
          try {
            const c = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
            if (c.model) process.stdout.write(c.model);
          } catch {}
        ' "$_mx_oc_cfg" 2>/dev/null)"
        if [ -n "$_mx_from_cfg" ]; then
          printf '%s\n' "$_mx_from_cfg"; return 0
        fi
      fi
      printf '%s\n' "ollama/qwen3.6-256k"
      ;;
    claude)
      if [ -n "${ANTHROPIC_MODEL:-}" ]; then
        printf '%s\n' "$ANTHROPIC_MODEL"; return 0
      fi
      _mx_last="$(__mx_last_model_load claude)"
      if [ -n "$_mx_last" ]; then printf '%s\n' "$_mx_last"; return 0; fi
      printf '%s\n' "sonnet"
      ;;
    *)
      printf '%s\n' ""
      ;;
  esac
}

# ── Cursor / AWS Bedrock (opt-in — subscription-dependent) ───────────────────
# Read-only: is Bedrock configured/enabled? Fast path reads cli-config; no network.
__mx_cursor_bedrock_is_enabled() {
  _mx_cfg="${HOME}/.cursor/cli-config.json"
  if [ -f "$_mx_cfg" ] && command -v node >/dev/null 2>&1; then
    node -e '
      try {
        const c = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
        process.exit(c.bedrock?.enabled ? 0 : 1);
      } catch { process.exit(1); }
    ' "$_mx_cfg" 2>/dev/null && return 0
  fi
  return 1
}

# Enable org Bedrock models (us.anthropic.*, …). Only called by vault-bedrock on
# or when SYNAPSE_CURSOR_BEDROCK=on explicitly requests it.
__mx_cursor_bedrock_ensure() {
  command -v cursor-agent >/dev/null 2>&1 || return 1
  __mx_cursor_bedrock_is_enabled && return 0
  _mx_br_team="$(cursor-agent bedrock status 2>/dev/null | awk '/^Team role available:/{print $4}')"
  if [ "$_mx_br_team" = "yes" ]; then
    if cursor-agent bedrock use-team-role >/dev/null 2>&1; then
      echo "[synapse] Bedrock team-role enabled (org subscription models now available)" >&2
      return 0
    fi
  fi
  return 1
}

__mx_cursor_bedrock_wanted() {
  case "${SYNAPSE_CURSOR_BEDROCK:-off}" in
    1|on|true|yes) return 0 ;;
    *) return 1 ;;
  esac
}

# ── model list cache (per CLI) ────────────────────────────────────────────────
__mx_models_cache() { printf '%s/.synapse-models-%s.cache' "${TMPDIR:-/tmp}" "$1"; }

# Emit one model id per line for the given CLI. Cached (TTL SYNAPSE_MODELS_TTL, default 3600).
__mx_cli_model_ids() {
  cli="${1:-cursor}"
  cache="$(__mx_models_cache "$cli")"
  ttl="${SYNAPSE_MODELS_TTL:-3600}"
  if [ "${2:-}" = "--refresh" ]; then rm -f "$cache"; fi
  if [ -f "$cache" ]; then
    now="$(date +%s)"
    mtime="$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null || echo 0)"
    if [ "$((now - mtime))" -lt "$ttl" ] && [ -s "$cache" ]; then
      cat "$cache"; return 0
    fi
  fi

  ids=""
  case "$cli" in
    cursor)
      command -v cursor-agent >/dev/null 2>&1 || { cat "$cache" 2>/dev/null; return 0; }
      # Fast catalog probe (subscription-agnostic — no Bedrock required).
      catalog="$(cursor-agent --list-models 2>/dev/null \
        | sed -n 's/^\([^ ][^ ]*\) - .*/\1/p')"
      ids="$catalog"
      # Optional Bedrock tenant IDs — only when explicitly opted in (slow probe).
      if __mx_cursor_bedrock_wanted; then
        __mx_cursor_bedrock_ensure 2>/dev/null || true
        _mx_probe_dir="${SYNAPSE_VAULT:-$_MX_DEFAULT_VAULT}"
        bedrock="$(cd "$_mx_probe_dir" 2>/dev/null && cursor-agent -p --model __invalid_probe__ "x" 2>&1 \
          | tr ',' '\n' | sed 's/^ *//;s/ *$//' \
          | grep -E '^(us|eu|ap|sa)\.(anthropic|amazon|meta|cohere|mistral)\.' \
          | sort -u)"
        if [ -n "$bedrock" ]; then
          ids="$(printf '%s\n%s\n' "$bedrock" "$catalog" | grep -v '^$' | sort -u)"
        fi
      fi
      ;;
    opencode)
      command -v opencode >/dev/null 2>&1 || { cat "$cache" 2>/dev/null; return 0; }
      ids="$(opencode models 2>/dev/null | sed '/^$/d')"
      ;;
    claude)
      # Claude Code accepts aliases (sonnet, opus, …) and full catalog names.
      # No reliable offline probe — ship common aliases; user can always type more.
      ids="$(printf '%s\n' \
        sonnet opus haiku fable \
        claude-sonnet-4-6 claude-opus-4-6 claude-opus-4-8 \
        claude-sonnet-5 claude-opus-4-8-thinking-high \
        claude-fable-5 claude-4.6-sonnet-medium)"
      ;;
    *)
      ids=""
      ;;
  esac

  if [ -n "$ids" ]; then
    printf '%s\n' "$ids" > "$cache" 2>/dev/null
    printf '%s\n' "$ids"
  else
    cat "$cache" 2>/dev/null
  fi
}

# ── core launcher ─────────────────────────────────────────────────────────────
__mx_launch() {
  agent="$1"; profile="$2"; shift 2

  SYNAPSE_VAULT="$(__mx_vault 2>/dev/null)" || true
  if [ -z "$SYNAPSE_VAULT" ]; then
    echo "agents.sh: could not locate a vault (cd into one, or set SYNAPSE_VAULT)." >&2
    return 2
  fi
  export SYNAPSE_VAULT

  __mx_is_profile() { case "$1" in lean|standard|fat) return 0;; *) return 1;; esac; }

  target=""
  if [ -n "${1:-}" ] && ! __mx_is_profile "${1:-}" && [ "${1#--}" = "$1" ] && [ "${1#[a-z]}" != "$1" ]; then
    target="$1"; shift
    case "$target" in hub-*) [ "$profile" = "lean" ] && profile="standard" ;; esac
  fi

  if __mx_is_profile "${1:-}"; then
    profile="$1"; shift
  elif [ "${1:-}" = "--profile" ] || [ "${1:-}" = "-P" ]; then
    case "${2:-}" in
      lean|standard|fat) profile="$2"; shift 2 ;;
      *) echo "profile must be lean|standard|fat (got '${2:-}')" >&2; return 2 ;;
    esac
  fi

  # Extract flags anywhere in remaining args (before task/target parsing leaks them).
  no_semantic=0
  cli="${SYNAPSE_CLI:-opencode}"
  perm_mode="${SYNAPSE_PERM_MODE:-auto}"
  clipboard=0
  model=""
  case "${SYNAPSE_AUTO:-}" in
    0) perm_mode="manual" ;;
    1) perm_mode="auto"   ;;
  esac

  __mx_kept=""
  __mx_want_cli=0
  __mx_want_model=0
  for __mx_a in "$@"; do
    if [ "$__mx_want_cli" = "1" ]; then
      cli="$__mx_a"; __mx_want_cli=0; continue
    fi
    if [ "$__mx_want_model" = "1" ]; then
      model="$__mx_a"; __mx_want_model=0; continue
    fi
    case "$__mx_a" in
      --no-semantic)     no_semantic=1; continue ;;
      --cli)             __mx_want_cli=1; continue ;;
      --cli=*)           cli="${__mx_a#--cli=}"; continue ;;
      --model|-m)        __mx_want_model=1; continue ;;
      --model=*)         model="${__mx_a#--model=}"; continue ;;
      --auto|-y)         perm_mode="auto"; continue ;;
      --bypass|--yolo|--dangerously-skip-permissions) perm_mode="bypass"; continue ;;
      --no-auto|--safe|--confirm|--manual) perm_mode="manual"; continue ;;
      --clipboard|--copy|-c) clipboard=1; continue ;;
    esac
    __mx_kept="${__mx_kept:+$__mx_kept }$__mx_a"
  done

  task="$__mx_kept"

  case "$cli" in
    opencode|claude|cursor|clip|clipboard|print|-) ;;
    *) echo "[synapse] unknown --cli '$cli' — using opencode" >&2; cli=opencode ;;
  esac
  case "$perm_mode" in
    manual|auto|bypass) ;;
    *) echo "SYNAPSE_PERM_MODE must be manual|auto|bypass (got '$perm_mode')" >&2; return 2 ;;
  esac

  if ! command -v node >/dev/null 2>&1; then
    echo "node not found — install Node.js to render briefings." >&2; return 127
  fi

  # Semantic augment: on when task present (unless --no-semantic / SYNAPSE_SEMANTIC=off).
  __mx_semantic=0
  if [ -n "$task" ] && [ "$no_semantic" = "0" ]; then
    case "${SYNAPSE_SEMANTIC:-auto}" in
      1|on|true|yes) __mx_semantic=1 ;;
      0|off|false|no) __mx_semantic=0 ;;
      *)
        if node -e '
          import("node:sqlite").then(({DatabaseSync})=>{
            try{
              const db=new DatabaseSync(process.argv[1],{readOnly:true});
              const t=db.prepare("SELECT name FROM sqlite_master WHERE type='"'"'table'"'"' AND name='"'"'note_vectors'"'"'").get();
              const n=t?db.prepare("SELECT COUNT(*) c FROM note_vectors").get().c:0;
              process.exit(n>0?0:1);
            }catch{process.exit(1);}
          }).catch(()=>process.exit(1));
        ' "$SYNAPSE_VAULT/db/synapse.db" 2>/dev/null; then __mx_semantic=1; fi
        ;;
    esac
  fi

  engine="render"
  if [ "$__mx_semantic" = "1" ]; then
    # Prefer package augment when resolvable (bin or lib); else fall back silently to render.
    if __mx_have_bin || __mx_tool augment >/dev/null 2>&1; then
      engine="augment"
    fi
  fi

  # Emit briefing to stdout or file. Briefing ONLY — task never mixed in here.
  _mx_emit() {
    _out="$1"; shift
    if [ "$engine" = "augment" ]; then
      if [ -n "$target" ]; then
        if [ -n "$_out" ]; then __mx_run augment "$agent" "$target" --profile "$profile" --task "$task" "$@" > "$_out"
        else                       __mx_run augment "$agent" "$target" --profile "$profile" --task "$task" "$@"; fi
      else
        if [ -n "$_out" ]; then __mx_run augment "$agent" --profile "$profile" --task "$task" "$@" > "$_out"
        else                       __mx_run augment "$agent" --profile "$profile" --task "$task" "$@"; fi
      fi
    else
      if [ -n "$target" ]; then
        if [ -n "$_out" ]; then __mx_run render "$agent" "$target" --profile "$profile" "$@" > "$_out"
        else                       __mx_run render "$agent" "$target" --profile "$profile" "$@"; fi
      else
        if [ -n "$_out" ]; then __mx_run render "$agent" --profile "$profile" "$@" > "$_out"
        else                       __mx_run render "$agent" --profile "$profile" "$@"; fi
      fi
    fi
  }

  case "$cli" in
    clip|clipboard)
      echo "[synapse] ${agent#agent-}${target:+ + $target} (${profile}) → clipboard" >&2
      if [ -n "$task" ]; then
        { _mx_emit "" | cat; printf '\n\n---\n\n%s\n' "$task"; } | __mx_clip
      else
        _mx_emit "" --copy 2>/dev/null || _mx_emit "" | __mx_clip
      fi
      return $?
      ;;
    print|-)
      if [ -n "$task" ]; then
        _mx_emit ""
        printf '\n\n---\n\n%s\n' "$task"
      else
        _mx_emit ""
      fi
      return $?
      ;;
  esac

  _bin=""
  case "$cli" in
    opencode) _bin=opencode ;;
    claude)   _bin=claude ;;
    cursor)   _bin=cursor-agent ;;
  esac

  if ! command -v "$_bin" >/dev/null 2>&1; then
    echo "[synapse] '$_bin' not found — copying briefing to clipboard. (install $_bin, or use --cli print)" >&2
    _mx_emit "" --copy 2>/dev/null || _mx_emit "" | __mx_clip
    return $?
  fi

  tmp="$(mktemp "${TMPDIR:-/tmp}/synapse.XXXXXX")" || return 1
  if ! _mx_emit "$tmp"; then
    rm -f "$tmp"; return 1
  fi
  tok="$(( $(wc -c < "$tmp" | tr -d ' ') / 4 ))"
  _eng_tag=""; [ "$engine" = "augment" ] && _eng_tag=" +semantic"
  echo "[synapse] ${agent#agent-}${target:+ + $target} (${profile}, ~${tok} tok${_eng_tag}, ${perm_mode}) → ${cli}" >&2
  if [ "$tok" -gt 60000 ]; then
    echo "[synapse] ⚠ briefing is large (~${tok} tok) — try 'standard' or a single note at 'lean'." >&2
  fi

  if [ "$clipboard" = "1" ]; then
    if [ -n "$task" ]; then
      { cat "$tmp"; printf '\n\n---\n\n%s\n' "$task"; } | __mx_clip
    else
      cat "$tmp" | __mx_clip
    fi
    rm -f "$tmp"
    return 0
  fi

  # Permission flags per CLI (loaded into $@ via set --)
  set --
  case "$cli" in
    claude)
      case "$perm_mode" in
        auto)   set -- --permission-mode auto ;;
        bypass) set -- --permission-mode bypassPermissions ;;
        manual) ;;
      esac
      ;;
    opencode)
      case "$perm_mode" in
        auto)
          set -- --dangerously-skip-permissions
          echo "[synapse] note: opencode has no separate 'auto' mode; using its bypass flag." >&2
          ;;
        bypass) set -- --dangerously-skip-permissions ;;
        manual) ;;
      esac
      ;;
    cursor)
      case "$perm_mode" in
        auto)   set -- --auto-review ;;
        bypass) set -- --force ;;
        manual) ;;
      esac
      ;;
  esac

  rc=0
  case "$cli" in
    claude)
      _mx_claude_model="$(__mx_resolve_model claude "$model")"
      if [ -n "$task" ]; then
        claude "$@" --append-system-prompt-file "$tmp" --add-dir "$SYNAPSE_VAULT" --model "$_mx_claude_model" -- "$task"
      else
        claude "$@" --append-system-prompt-file "$tmp" --add-dir "$SYNAPSE_VAULT" --model "$_mx_claude_model"
      fi
      rc=$?
      ;;
    cursor)
      _rules_dir="$PWD/.cursor/rules"
      _rule_file="$_rules_dir/.synapse-vault-briefing.mdc"
      mkdir -p "$_rules_dir"
      {
        printf '%s\n' '---'
        printf '%s\n' 'description: Synapse vault briefing (temporary — auto-deleted on exit)'
        printf '%s\n' 'alwaysApply: true'
        printf '%s\n' '---'
        printf '\n'
        cat "$tmp"
      } > "$_rule_file"
      trap "rm -f '$_rule_file' '$tmp'; trap - EXIT INT TERM" EXIT INT TERM

      if __mx_cursor_bedrock_wanted; then
        __mx_cursor_bedrock_ensure 2>/dev/null || \
          echo "[synapse] note: SYNAPSE_CURSOR_BEDROCK=on but Bedrock unavailable — using Cursor catalog" >&2
      fi
      _cursor_model="$(__mx_resolve_model cursor "$model")"
      if [ -n "$task" ]; then
        cursor-agent --model "$_cursor_model" "$@" --add-dir "$SYNAPSE_VAULT" "$task"
      else
        cursor-agent --model "$_cursor_model" "$@" --add-dir "$SYNAPSE_VAULT"
      fi
      rc=$?
      return $rc
      ;;
    opencode)
      _mx_model="$(__mx_resolve_model opencode "$model")"
      _mx_tui="${SYNAPSE_TUI:-auto}"
      case "$_mx_tui" in
        auto) { [ -t 0 ] && [ -t 1 ]; } && _mx_tui=1 || _mx_tui=0 ;;
        1|on|true|yes) _mx_tui=1 ;;
        *) _mx_tui=0 ;;
      esac
      if [ "$_mx_tui" = "1" ] && [ -z "$task" ]; then
        opencode "$SYNAPSE_VAULT" -m "$_mx_model" --prompt "$(cat "$tmp")"
        rc=$?
      elif [ -n "$task" ]; then
        # Briefing as context file; task as the user message (augment-aware).
        opencode run --interactive "$@" -m "$_mx_model" --dir "$SYNAPSE_VAULT" --file "$tmp" "$task"
        rc=$?
      else
        _mx_oc=(run -m "$_mx_model" --dir "$SYNAPSE_VAULT")
        case "${SYNAPSE_THINKING:-1}" in
          0|off|false|no) ;;
          *) _mx_oc+=(--thinking) ;;
        esac
        opencode "${_mx_oc[@]}" "$@" "$(cat "$tmp")"
        rc=$?
      fi
      ;;
  esac
  rm -f "$tmp"
  return $rc
}

# ── auto-generate one function per agent-*.md ─────────────────────────────────
_mx_field() { sed -n "s/^$2:[[:space:]]*//p" "$1" | tr -d '"' | head -1; }

_mx_wrap() {
  indent="$1"; text="$2"; width="${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}"
  case "$width" in ''|*[!0-9]*) width=80 ;; esac
  awk -v ind="$indent" -v width="$width" -v text="$text" '
    BEGIN {
      avail = width - ind; if (avail < 24) avail = 24
      pad = sprintf("%*s", ind, "")
      n = split(text, w, /[ \t]+/); line = ""; first = 1
      for (i = 1; i <= n; i++) {
        cand = (line == "" ? w[i] : line " " w[i])
        if (length(cand) > avail && line != "") {
          print (first ? "" : pad) line; first = 0; line = w[i]
        } else line = cand
      }
      if (line != "") print (first ? "" : pad) line
    }'
}

_MX_AGENT_NAMES=""
for _mx_f in "$SYNAPSE_VAULT"/agents/agent-*.md; do
  [ -e "$_mx_f" ] || continue
  _mx_name="$(basename "$_mx_f" .md)"
  _mx_name="${_mx_name#agent-}"
  # shellcheck disable=SC2086,SC2090
  eval "${_mx_name}() { __mx_agent_cmd '${_mx_name}' \"\$@\"; }"
  _MX_AGENT_NAMES="$_MX_AGENT_NAMES $_mx_name"
done
unset _mx_f _mx_name
_MX_AGENT_NAMES="${_MX_AGENT_NAMES# }"

# ── TAB-completion (--model values depend on --cli in the same command) ───────
_MX_FLAGS="--cli --model --profile --auto --bypass --yolo --no-auto --safe --confirm --manual --no-semantic --clipboard --copy"

__mx_cli_from_words() {
  # $1 = space-separated words (no leading agent name needed for model list)
  _w_cli="${SYNAPSE_CLI:-opencode}"
  for _w in $1; do
    case "$_w" in
      --cli=*) _w_cli="${_w#--cli=}" ;;
      --cli)   _mx_next_cli=1; continue ;;
    esac
    if [ "${_mx_next_cli:-0}" = "1" ]; then _w_cli="$_w"; _mx_next_cli=0; fi
  done
  unset _mx_next_cli
  printf '%s\n' "$_w_cli"
}

if [ -n "${ZSH_VERSION:-}" ]; then
  __mx_complete_zsh() {
    local -a models
    local cur="${words[CURRENT]}" prev="${words[CURRENT-1]}"
    local cli; cli="$(__mx_cli_from_words "${words[2,-1]}")"
    case "$prev" in
      --model|-m)
        models=(${(f)"$(__mx_cli_model_ids "$cli" 2>/dev/null)"})
        compadd -- $models; return ;;
      --cli)
        compadd -- opencode claude cursor clip print; return ;;
      --profile|-P)
        compadd -- lean standard fat; return ;;
    esac
    case "$cur" in
      --model=*)
        models=(${(f)"$(__mx_cli_model_ids "$cli" 2>/dev/null)"})
        compadd -P '--model=' -- $models; return ;;
      --cli=*)
        compadd -P '--cli=' -- opencode claude cursor clip print; return ;;
    esac
  }
  __mx_complete_synapse_zsh() {
    if [ "${CURRENT:-0}" -eq 2 ]; then
      compadd -- render augment lint index views migrate embeddings setup install journal \
                 agents hubs profiles models bedrock reload gate help
      return
    fi
    __mx_complete_zsh
  }
  if whence compdef >/dev/null 2>&1 && (( ${+_comps} )); then
    # shellcheck disable=SC2086
    compdef __mx_complete_zsh ${_MX_AGENT_NAMES} vault-models vault-cursor-models
    compdef __mx_complete_synapse_zsh synapse
  fi
elif [ -n "${BASH_VERSION:-}" ]; then
  __mx_complete_bash() {
    local cur prev cli
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    cli="$(__mx_cli_from_words "${COMP_WORDS[*]}")"
    case "$prev" in
      --model|-m)
        # shellcheck disable=SC2207
        COMPREPLY=($(compgen -W "$(__mx_cli_model_ids "$cli" 2>/dev/null)" -- "$cur")); return ;;
      --cli)
        # shellcheck disable=SC2207
        COMPREPLY=($(compgen -W "opencode claude cursor clip print" -- "$cur")); return ;;
      --profile|-P)
        # shellcheck disable=SC2207
        COMPREPLY=($(compgen -W "lean standard fat" -- "$cur")); return ;;
    esac
    # shellcheck disable=SC2207
    COMPREPLY=($(compgen -W "$_MX_FLAGS" -- "$cur"))
  }
  __mx_complete_synapse_bash() {
    local cur; cur="${COMP_WORDS[COMP_CWORD]}"
    if [ "${COMP_CWORD:-0}" -eq 1 ]; then
      # shellcheck disable=SC2207
      COMPREPLY=($(compgen -W "render augment lint index views migrate embeddings setup install journal agents hubs profiles models bedrock reload gate help" -- "$cur")); return
    fi
    __mx_complete_bash
  }
  # shellcheck disable=SC2086
  complete -F __mx_complete_bash ${_MX_AGENT_NAMES} vault-models vault-cursor-models 2>/dev/null
  complete -F __mx_complete_synapse_bash synapse 2>/dev/null
fi

# ── discovery commands ────────────────────────────────────────────────────────

vault-agents() {
  SYNAPSE_VAULT="$(__mx_vault)"
  echo "Synapse vault agent commands:"
  echo "  Usage: <name> [<target-id>] [--profile lean|standard|fat] [--cli opencode|claude|cursor|clip|print]"
  echo "         [--model <id>] [--auto|--bypass|--manual] [--no-semantic] [--clipboard] [\"task\"]"
  echo "  Permission default: auto. Global: export SYNAPSE_PERM_MODE=manual|auto|bypass"
  echo "  --model TAB-completes per --cli (see: vault-models --cli <name>)"
  echo ""
  for f in "$SYNAPSE_VAULT"/agents/agent-*.md; do
    [ -e "$f" ] || continue
    id="$(basename "$f" .md)"
    name="${id#agent-}"
    prof="$(_mx_field "$f" profile)"; prof="${prof:-lean}"
    purpose="$(_mx_field "$f" purpose)"
    printf '  %-14s [%-8s] ' "$name" "$prof"
    _mx_wrap 28 "$purpose"
  done
  echo ""
  echo "  Also: synapse hubs | profiles | models | bedrock | reload | gate   (or the vault-* aliases)"
  echo "  Runtime (--cli, default ${SYNAPSE_CLI:-opencode}): opencode | claude | cursor | clip | print"
  echo "  Cursor default model: auto (override: --model <id> or SYNAPSE_CURSOR_MODEL=...)"
  echo "  Bedrock (opt-in): vault-bedrock on  or  SYNAPSE_CURSOR_BEDROCK=on"
}

vault-hubs() {
  SYNAPSE_VAULT="$(__mx_vault)"
  echo "Synapse vault hub targets (pass as the second arg to any agent):"
  echo "  Usage: <agent> <hub-id> [--profile standard]"
  echo ""
  for f in "$SYNAPSE_VAULT"/hub/hub-*.md "$SYNAPSE_VAULT"/hub-synapse.md; do
    [ -e "$f" ] || continue
    id="$(basename "$f" .md)"
    title="$(grep -m1 '^title:' "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"')"
    printf '  %-24s %s\n' "$id" "$title"
  done
  echo ""
  echo "  Also valid: project-* / plan-* / note-* / contact-* / account-* / summary-*"
}

vault-profiles() {
  echo "Synapse vault profiles (context dial — presets of relationship ROLES, not hop counts):"
  echo ""
  printf '  %-10s %-30s %-12s %s\n' "Profile" "Roles pulled" "~Budget" "Best for"
  printf '  %-10s %-30s %-12s %s\n' "lean" "self + rules/skills/tools/deleg" "~4K tok" "an agent + its rules/skills/tools"
  printf '  %-10s %-30s %-12s %s\n' "standard" "+ members/attach/navigate/refs" "~15K tok" "a domain hub"
  printf '  %-10s %-30s %-12s %s\n' "fat" "+ transitive closure" "~30K tok" "deep dives / maximum context"
  echo ""
  echo "  Rule of thumb: agents → lean; hubs → standard (auto when target is hub-*)."
}

vault-models() {
  cli="${SYNAPSE_CLI:-opencode}"
  refresh=""
  while [ $# -gt 0 ]; do
    case "$1" in
      --cli) cli="${2:-opencode}"; shift 2 ;;
      --refresh) refresh="--refresh"; shift ;;
      cursor|opencode|claude) cli="$1"; shift ;;
      *) shift ;;
    esac
  done
  if [ "$cli" = "cursor" ]; then
    if __mx_cursor_bedrock_wanted; then
      echo "── Bedrock status (SYNAPSE_CURSOR_BEDROCK=on) ──"
      command -v cursor-agent >/dev/null 2>&1 && cursor-agent bedrock status 2>&1 | sed 's/^/  /' || echo "  cursor-agent not on PATH"
      echo ""
      echo "── Raw Bedrock model IDs ──"
      __mx_cli_model_ids cursor $refresh 2>/dev/null | grep -E '^(us|eu|ap|sa)\.' | sed 's/^/  /' || echo "  (none)"
      echo ""
    elif __mx_cursor_bedrock_is_enabled; then
      echo "── Bedrock ──"
      echo "  configured in ~/.cursor/cli-config.json (off by default for Synapse — set SYNAPSE_CURSOR_BEDROCK=on to include tenant IDs)"
      echo ""
    fi
    echo "── Cursor catalog ──"
    __mx_cli_model_ids cursor $refresh 2>/dev/null | grep -v -E '^(us|eu|ap|sa)\.' | sed 's/^/  /'
  else
    echo "── Models for --cli $cli ──"
    __mx_cli_model_ids "$cli" $refresh | sed 's/^/  /'
  fi
  echo ""
  echo "Use:  <agent> --cli $cli --model <id> ..."
  echo "Cache: $(__mx_models_cache "$cli")  (TTL ${SYNAPSE_MODELS_TTL:-3600}s; vault-models --cli $cli --refresh)"
}

# Alias for genesis-parity / cursor-focused docs
vault-cursor-models() {
  refresh=""; [ "${1:-}" = "--refresh" ] && refresh="--refresh"
  vault-models --cli cursor $refresh
}

# Toggle / inspect AWS Bedrock via Cursor team-role (org subscription models).
vault-bedrock() {
  if ! command -v cursor-agent >/dev/null 2>&1; then
    echo "cursor-agent not on PATH" >&2; return 127
  fi
  case "${1:-status}" in
    on|enable|use-team-role)
      if __mx_cursor_bedrock_ensure; then
        cursor-agent bedrock status 2>&1 | sed 's/^/  /'
      else
        echo "Could not enable Bedrock team-role. Check: cursor-agent bedrock status" >&2
        cursor-agent bedrock status 2>&1 | sed 's/^/  /' >&2
        return 1
      fi
      ;;
    off|disable)
      cursor-agent bedrock disable 2>&1 | sed 's/^/  /'
      rm -f "$(__mx_models_cache cursor)" 2>/dev/null
      ;;
    status)
      if command -v cursor-agent >/dev/null 2>&1; then
        cursor-agent bedrock status 2>&1 | sed 's/^/  /'
      else
        echo "  cursor-agent not on PATH" >&2
        return 127
      fi
      echo ""
      if __mx_cursor_bedrock_is_enabled; then
        echo "  Raw Bedrock model IDs:"
        __mx_cli_model_ids cursor --refresh 2>/dev/null \
          | grep -E '^(us|eu|ap|sa)\.' | sed 's/^/    /' || echo "    (none)"
      else
        echo "  Bedrock off (default). Enable: vault-bedrock on  or  SYNAPSE_CURSOR_BEDROCK=on"
      fi
      ;;
    *)
      echo "usage: vault-bedrock on|off|status" >&2; return 2 ;;
  esac
}

vault-reload() {
  _mx_env="${_MX_REPO:-}/bin/synapse-env.sh"
  if [ -f "$_mx_env" ]; then
    # shellcheck disable=SC1090
    . "$_mx_env"
    echo "[synapse] re-sourced via $_mx_env"
    return 0
  fi
  if [ -n "${ZSH_VERSION:-}" ]; then
    _mx_self="${(%):-%x}"
  elif [ -n "${BASH_VERSION:-}" ]; then
    _mx_self="${BASH_SOURCE[0]}"
  else
    echo "vault-reload: re-source bin/synapse-env.sh manually" >&2; return 1
  fi
  # shellcheck disable=SC1090
  . "$_mx_self"
  echo "[synapse] agents.sh re-sourced from $_mx_self"
}

vault-gate() {
  case "${1:-status}" in
    off)    : > "$HOME/.claude/vault-gate-off" && echo "🔓 vault gate OFF — external agent may access the vault" ;;
    on)     rm -f "$HOME/.claude/vault-gate-off" && echo "🔒 vault gate ON — vault sealed (default)" ;;
    status) [ -f "$HOME/.claude/vault-gate-off" ] && echo "🔓 vault gate: OFF" || echo "🔒 vault gate: ON" ;;
    *)      echo "usage: synapse gate on|off|status" >&2; return 2 ;;
  esac
}

# ── unified front door: `synapse <sub>` ───────────────────────────────────────
# One namespace over two families: ENGINE subcommands delegate to the Node binary
# (`command synapse …`, which this function shadows on PATH); SHELL subcommands run
# in-process here because they must touch the live shell (re-source, env, host config).
# The vault-* functions above remain first-class synonyms.
__syn_help() {
  cat <<'EOF'
synapse — Synapse context-vault CLI (@eborja/synapse)

Engine (delegates to the packaged binary):
  synapse render <id> … [--profile lean|standard|fat] [--dry-run] [--copy]
  synapse augment <id> … --task "…"      render + semantic recall
  synapse lint [--strict]                mechanical vault health-check
  synapse index | views | migrate        SQL records tooling
  synapse embeddings [--all|--selftest]  (re)build the embeddings cache
  synapse setup [--write]                probe/provision Ollama + embedding model
  synapse install [--write]              wire this shell CLI + editor dirs
  synapse journal "slug"                 scaffold journal/<date>-<slug>.md

Shell (need this sourced CLI; vault-* aliases in parentheses):
  synapse agents        list agent commands            (vault-agents)
  synapse hubs          list hub targets               (vault-hubs)
  synapse profiles      explain the profiles           (vault-profiles)
  synapse models        list models per --cli          (vault-models)
  synapse bedrock …     toggle Cursor Bedrock (on|off|status)   (vault-bedrock)
  synapse reload        re-source this CLI now         (vault-reload)
  synapse gate …        host privacy gate (on|off|status)       (vault-gate)

Agents (launch a coding CLI seeded with a briefing):
  curator | oracle | reconciler | ingester  [<hub-id>] [--profile …] ["task"]
  e.g.  curator hub-finances "regenerate the Q2 summary view"
EOF
}

synapse() {
  case "${1:-}" in
    ""|-h|--help|help) __syn_help ;;
    agents)   shift; vault-agents   "$@" ;;
    hubs)     shift; vault-hubs     "$@" ;;
    profiles) shift; vault-profiles "$@" ;;
    models)   shift; vault-models   "$@" ;;
    bedrock)  shift; vault-bedrock  "$@" ;;
    reload)   shift; vault-reload   "$@" ;;
    gate)     shift; vault-gate     "$@" ;;
    # Engine subcommands (and anything else) go to the real binary, which owns its
    # own arg parsing, --help, and unknown-command errors.
    *)        command synapse "$@" ;;
  esac
}

# ── auto-reload agents.sh when the file changes (edit it, next prompt picks it up) ──
_MX_AGENTS_SH=""
if [ -n "${ZSH_VERSION:-}" ]; then
  _MX_AGENTS_SH="${(%):-%x}"
elif [ -n "${BASH_VERSION:-}" ]; then
  _MX_AGENTS_SH="${BASH_SOURCE[0]}"
fi
if [ -n "$_MX_AGENTS_SH" ] && [ -f "$_MX_AGENTS_SH" ]; then
  _MX_AGENTS_MTIME="$(stat -c %Y "$_MX_AGENTS_SH" 2>/dev/null || stat -f %m "$_MX_AGENTS_SH" 2>/dev/null || echo 0)"
  if [ -n "${ZSH_VERSION:-}" ]; then
  __mx_autoreload() {
    _mt="$(stat -c %Y "$_MX_AGENTS_SH" 2>/dev/null || stat -f %m "$_MX_AGENTS_SH" 2>/dev/null || echo 0)"
    if [ "$_mt" != "${_MX_AGENTS_MTIME:-0}" ]; then
      _MX_AGENTS_MTIME="$_mt"
      # shellcheck disable=SC1090
      . "$_MX_AGENTS_SH" 2>/dev/null
    fi
  }
  if typeset -f add-zsh-hook >/dev/null 2>&1; then
    add-zsh-hook precmd __mx_autoreload
  fi
  elif [ -n "${BASH_VERSION:-}" ]; then
    __mx_autoreload() {
      _mt="$(stat -c %Y "$_MX_AGENTS_SH" 2>/dev/null || stat -f %m "$_MX_AGENTS_SH" 2>/dev/null || echo 0)"
      if [ "$_mt" != "${_MX_AGENTS_MTIME:-0}" ]; then
        _MX_AGENTS_MTIME="$_mt"
        # shellcheck disable=SC1090
        . "$_MX_AGENTS_SH" 2>/dev/null
      fi
    }
    case "${PROMPT_COMMAND:-}" in
      *__mx_autoreload*) ;;
      *) PROMPT_COMMAND="__mx_autoreload${PROMPT_COMMAND:+; $PROMPT_COMMAND}" ;;
    esac
  fi
fi
