#!/usr/bin/env bash # goldbrain.sh — the single validated client for the goldbrain Obsidian Local REST API. # # Every read/write to the vault should go through this script instead of hand-built # curl. It centralizes: auth injection, HTTP-status checking (non-zero exit on >=400 # so a failed write can never look like success), one bounded retry on 5xx/connection # errors, idempotent append (read-before-POST), correct `::` heading-target handling, # frontmatter field patches, and an advisory multi-writer lock. # # Config (env overrides; defaults match the rest of the plugin): # GB_BASE default https://goldbrainapi.mpm.to # GB_KEY default the plugin bearer token # GB_VERIFY default 1 — read-back verify after a PUT # GB_LOCK_TTL default 900 — seconds before an advisory lock is considered stale # # Usage: # goldbrain.sh get # print file contents (404 -> exit 44) # goldbrain.sh map # document-map JSON (headings/blocks/frontmatter) # goldbrain.sh ls # directory listing JSON # goldbrain.sh search # /search/simple # goldbrain.sh put [file] # create/overwrite (body from file or stdin) # goldbrain.sh post [file] # raw append (NON-idempotent; prefer `append`) # goldbrain.sh append # idempotent append: skips if the exact line exists # goldbrain.sh patch [file] # goldbrain.sh fm # PATCH a frontmatter scalar (e.g. fm p.md updated '"2026-06-19"') # goldbrain.sh bump [YYYY-MM-DD] # set frontmatter updated: to today (or given date) # goldbrain.sh delete # DELETE (destructive; explicit use only) # goldbrain.sh lock # acquire advisory lock (exit 75 if held by someone else & fresh) # goldbrain.sh unlock # release advisory lock if owned by # goldbrain.sh scope show # print active scope, its freshness, and sessions-since # goldbrain.sh scope set "" # switch scope atomically (history + replace + stamp scope_updated) # # Exit codes: 0 ok · 44 not-found(404) · 75 lock-held · 2 usage · 1 other HTTP/transport error. set -euo pipefail GB_BASE="${GB_BASE:-https://goldbrainapi.mpm.to}" GB_BASE="${GB_BASE%/}" GB_KEY="${GB_KEY:-fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8}" GB_VERIFY="${GB_VERIFY:-1}" GB_LOCK_TTL="${GB_LOCK_TTL:-900}" AUTH="Authorization: Bearer ${GB_KEY}" # response-body scratch file (filled by every _curl; read by callers via $RESP) RESP="$(mktemp)"; BODY="" cleanup() { rm -f "$RESP" "${BODY:-}"; } trap cleanup EXIT die() { echo "goldbrain.sh: $*" >&2; exit 1; } usage() { sed -n '2,40p' "$0" >&2; exit 2; } _today() { echo "${GB_TODAY:-$(date +%Y-%m-%d)}"; } _now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; } _vault_url() { echo "${GB_BASE}/vault/$1"; } # _curl METHOD URL [extra curl args...] # Writes the response body to $RESP and the status code to $HTTP — BOTH IN THE PARENT # SHELL (never call this in $(...) or on the right of a pipe, or those globals are lost). # One bounded retry on transport failure (000) or 5xx. HTTP="" _curl() { local method="$1" url="$2"; shift 2 local code attempt=0 while :; do code="$(curl -sS -X "$method" -H "$AUTH" -o "$RESP" -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000)" if { [ "$code" = "000" ] || [ "${code:0:1}" = "5" ]; } && [ "$attempt" -lt 1 ]; then attempt=$((attempt+1)); sleep 1; continue fi break done HTTP="$code" } # assert $HTTP is acceptable; 404 -> exit 44, other >=400 -> exit 1 _check() { local ctx="$1" [ "$HTTP" = "404" ] && exit 44 [ "$HTTP" = "000" ] && die "$ctx: vault unreachable (connection failed) [$GB_BASE]" [ "${HTTP:-000}" -ge 400 ] && die "$ctx: HTTP $HTTP — $(cat "$RESP")" return 0 } # capture a body argument (file path or '-'/empty for stdin) into $BODY (a temp file) _capture_body() { BODY="$(mktemp)" if [ "${1:-}" = "" ] || [ "${1:-}" = "-" ]; then cat > "$BODY"; else cat "$1" > "$BODY"; fi } cmd="${1:-}"; shift || true case "$cmd" in get) [ $# -ge 1 ] || usage _curl GET "$(_vault_url "$1")"; _check "get $1"; cat "$RESP" ;; map) [ $# -ge 1 ] || usage _curl GET "$(_vault_url "$1")" -H 'Accept: application/vnd.olrapi.document-map+json' _check "map $1"; cat "$RESP" ;; ls) [ $# -ge 1 ] || usage p="$1"; [ "${p%/}" = "$p" ] && p="$p/" _curl GET "$(_vault_url "$p")"; _check "ls $1"; cat "$RESP" ;; search) [ $# -ge 1 ] || usage q="$*"; q="${q// /+}" _curl POST "${GB_BASE}/search/simple/?query=${q}"; _check "search"; cat "$RESP" ;; put) [ $# -ge 1 ] || usage path="$1"; _capture_body "${2:-}" _curl PUT "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY" _check "put $path" if [ "$GB_VERIFY" = "1" ]; then _curl GET "$(_vault_url "$path")" [ "$HTTP" = "200" ] || die "put $path: write did not verify (GET returned $HTTP)" fi echo "ok: PUT $path" ;; post) [ $# -ge 1 ] || usage path="$1"; _capture_body "${2:-}" _curl POST "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY" _check "post $path"; echo "ok: POST $path" ;; append) # idempotent: GET the file, skip the POST if the exact line is already present. [ $# -ge 2 ] || usage path="$1"; line="$2" _curl GET "$(_vault_url "$path")" if [ "$HTTP" = "200" ] && grep -qF -- "$line" "$RESP"; then echo "skip: line already present in $path"; exit 0 fi [ "$HTTP" = "200" ] || [ "$HTTP" = "404" ] || _check "append(read) $path" BODY="$(mktemp)"; printf '%s\n' "$line" > "$BODY" _curl POST "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY" _check "append $path"; echo "ok: APPEND $path" ;; patch) [ $# -ge 4 ] || usage path="$1"; op="$2"; ttype="$3"; target="$4"; _capture_body "${5:-}" case "$op" in append|prepend|replace) ;; *) die "patch: op must be append|prepend|replace";; esac case "$ttype" in heading|frontmatter|block) ;; *) die "patch: target-type must be heading|frontmatter|block";; esac _curl PATCH "$(_vault_url "$path")" \ -H "Operation: $op" -H "Target-Type: $ttype" -H "Target: $target" \ -H 'Content-Type: text/markdown' --data-binary @"$BODY" _check "patch $path ($target)" echo "ok: PATCH $op $ttype '$target' -> $path" ;; fm) [ $# -ge 3 ] || usage path="$1"; field="$2"; value="$3" BODY="$(mktemp)"; printf '%s' "$value" > "$BODY" _curl PATCH "$(_vault_url "$path")" \ -H 'Operation: replace' -H 'Target-Type: frontmatter' -H "Target: $field" \ -H 'Content-Type: application/json' --data-binary @"$BODY" _check "fm $path.$field"; echo "ok: frontmatter $field -> $path" ;; bump) [ $# -ge 1 ] || usage path="$1"; d="${2:-$(_today)}" exec "$0" fm "$path" updated "\"$d\"" ;; delete) [ $# -ge 1 ] || usage _curl DELETE "$(_vault_url "$1")"; _check "delete $1"; echo "ok: DELETE $1" ;; lock) # advisory lock: _agent/locks/vault.lock holds " @ ". Honored cooperatively. [ $# -ge 1 ] || usage owner="$1"; lockpath="_agent/locks/vault.lock" _curl GET "$(_vault_url "$lockpath")" if [ "$HTTP" = "200" ] && [ -s "$RESP" ]; then cur="$(cat "$RESP")"; held_owner="${cur%% @ *}"; held_iso="${cur##* @ }"; held_iso="${held_iso%$'\n'}" held_epoch="$(date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$held_iso" +%s 2>/dev/null \ || date -u -d "$held_iso" +%s 2>/dev/null || echo 0)" now_epoch="$(date -u +%s)" if [ "$held_owner" != "$owner" ] && [ $((now_epoch - held_epoch)) -lt "$GB_LOCK_TTL" ]; then echo "lock held by '$held_owner' since $held_iso (fresh)" >&2; exit 75 fi fi BODY="$(mktemp)"; printf '%s @ %s\n' "$owner" "$(_now_iso)" > "$BODY" _curl PUT "$(_vault_url "$lockpath")" -H 'Content-Type: text/markdown' --data-binary @"$BODY" _check "lock"; echo "ok: locked by $owner" ;; unlock) [ $# -ge 1 ] || usage owner="$1"; lockpath="_agent/locks/vault.lock" _curl GET "$(_vault_url "$lockpath")" if [ "$HTTP" = "200" ]; then cur="$(cat "$RESP")"; held_owner="${cur%% @ *}" [ "$held_owner" = "$owner" ] || { echo "lock owned by '$held_owner', not '$owner' — not releasing" >&2; exit 75; } fi _curl DELETE "$(_vault_url "$lockpath")" [ "$HTTP" = "404" ] || _check "unlock" echo "ok: unlocked" ;; scope) # scope show | scope set "" # 'set' archives the prior scope to ## Scope History, replaces ## Scope, and stamps # the scope_updated freshness timestamp — one command instead of three error-prone PATCHes. sub="${1:-show}"; shift || true ccpath="_agent/context/current-context.md" case "$sub" in show) _curl GET "$(_vault_url "$ccpath")"; _check "scope show" cur="$RESP" echo "── Active scope ──" awk '/^## Scope[[:space:]]*$/{f=1;next} /^## /{if(f)exit} f' "$cur" su="$(sed -n 's/^scope_updated:[[:space:]]*//p' "$cur" | head -1)" su="${su//\"/}" echo "scope_updated: ${su:-}" _curl GET "$(_vault_url "_agent/sessions/")" if [ "$HTTP" = "200" ] && [ -n "$su" ]; then n="$(python3 -c "import json,sys;f=json.load(open('$RESP'))['files'];print(sum(1 for x in f if x.endswith('.md') and x[:10]>'$su'))" 2>/dev/null || echo '?')" echo "sessions logged since: ${n}" fi ;; set) [ $# -ge 1 ] || die "scope set needs the new scope text" new="$1" _curl GET "$(_vault_url "$ccpath")"; _check "scope set(read)" prior="$(awk '/^## Scope[[:space:]]*$/{f=1;next} /^## /{if(f)exit} f' "$RESP" \ | tr '\n' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | cut -c1-140)" BODY="$(mktemp)"; printf -- '- %s: %s\n' "$(_today)" "${prior:-(prior scope)}" > "$BODY" _curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: prepend' -H 'Target-Type: heading' \ -H 'Target: Current Context::Scope History' -H 'Content-Type: text/markdown' --data-binary @"$BODY" _check "scope set(history)" BODY="$(mktemp)"; printf '%s\n' "$new" > "$BODY" _curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: replace' -H 'Target-Type: heading' \ -H 'Target: Current Context::Scope' -H 'Content-Type: text/markdown' --data-binary @"$BODY" _check "scope set(replace)" BODY="$(mktemp)"; printf '"%s"' "$(_today)" > "$BODY" _curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: replace' -H 'Target-Type: frontmatter' \ -H 'Target: scope_updated' -H 'Content-Type: application/json' --data-binary @"$BODY" if [ "${HTTP:-000}" -ge 400 ]; then die "scope set: body switched, but scope_updated frontmatter is missing (run bootstrap.sh repair to add it) [HTTP $HTTP]" fi echo "ok: scope switched (prior archived to Scope History; scope_updated=$(_today))" ;; *) die "scope: use 'show' or 'set \"\"'" ;; esac ;; ""|-h|--help|help) usage ;; *) die "unknown command '$cmd' (try: get map ls search put post append patch fm bump delete lock unlock scope)" ;; esac