230 lines
8.5 KiB
Bash
230 lines
8.5 KiB
Bash
#!/usr/bin/env bash
|
|
# detect-transport.sh — discover which vault-mutation transports are available
|
|
# on this machine, write a normalized JSON snapshot to .vault-meta/transport.json,
|
|
# and pick a preferred transport per the v1.7 fallback chain.
|
|
#
|
|
# Fallback chain (highest to lowest precedence):
|
|
# 1. cli — Obsidian CLI binary (Obsidian 1.12+). No MCP server, no TLS, no plugin.
|
|
# 2. mcp-obsidian — REST-API-backed MCP server (Local REST API plugin required).
|
|
# 3. mcpvault — Filesystem-backed MCP server (BM25 search; no Obsidian plugin).
|
|
# 4. filesystem — Direct Read/Write/Edit tools. Always available (ultimate floor).
|
|
#
|
|
# MCP auto-detection is deferred to a v1.7.x patch (calling `claude mcp list` from
|
|
# inside a running claude session has reentrancy concerns). For v1.7, we detect
|
|
# CLI + filesystem and leave MCP fields as `{"present": null, "detection": "deferred"}`.
|
|
# Users with MCP transports configured can either edit transport.json manually or
|
|
# follow the legacy guidance in wiki/references/mcp-setup.md.
|
|
#
|
|
# Usage:
|
|
# ./scripts/detect-transport.sh # detect and write .vault-meta/transport.json
|
|
# ./scripts/detect-transport.sh --peek # print result to stdout without writing
|
|
# ./scripts/detect-transport.sh --force # refresh even if existing snapshot is fresh (<7d)
|
|
# ./scripts/detect-transport.sh --quiet # suppress informational stderr output
|
|
#
|
|
# Exit codes:
|
|
# 0 — success (transport.json written or peeked)
|
|
# 2 — vault-meta/ missing and cannot be created
|
|
# 3 — unrecognized flag
|
|
|
|
set -euo pipefail
|
|
|
|
VAULT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
META_DIR="${VAULT_ROOT}/.vault-meta"
|
|
OUTPUT_FILE="${META_DIR}/transport.json"
|
|
STALE_AFTER_DAYS=7
|
|
|
|
MODE="write"
|
|
QUIET=false
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--peek) MODE="peek" ;;
|
|
--force) MODE="force" ;;
|
|
--quiet) QUIET=true ;;
|
|
-h|--help)
|
|
sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "ERR: unknown flag: $1" >&2
|
|
exit 3
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
log() { $QUIET || echo "$@" >&2; }
|
|
|
|
# json_escape: read stdin and emit a JSON-encoded string (including the
|
|
# surrounding double quotes). Used for any untrusted value that lands in the
|
|
# transport.json heredoc — newlines, backslashes, control chars in upstream
|
|
# binaries (obsidian-cli --version) would otherwise break the JSON.
|
|
json_escape() {
|
|
python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()), end="")'
|
|
}
|
|
|
|
mkdir -p "$META_DIR" || {
|
|
echo "ERR: cannot create .vault-meta/ at $META_DIR" >&2
|
|
exit 2
|
|
}
|
|
|
|
# ── 0. Honor manual_override from existing transport.json ────────────────────
|
|
# Users can pin a non-detected transport (mcp-obsidian, mcpvault, or any custom
|
|
# value) by editing transport.json to set:
|
|
# "manual_override": true
|
|
# "preferred": "<their-choice>"
|
|
# "fallback_chain": [...]
|
|
# Auto-detection still runs (to refresh CLI/Obsidian-running flags for visibility),
|
|
# but PREFERRED and CHAIN are preserved from the existing file across both the
|
|
# normal write path AND --force runs. Documented at
|
|
# wiki/references/transport-fallback.md §Manual override.
|
|
MANUAL_OVERRIDE_FLAG=false
|
|
MANUAL_OVERRIDE_PREFERRED=""
|
|
MANUAL_OVERRIDE_CHAIN=""
|
|
if [ -f "$OUTPUT_FILE" ]; then
|
|
MANUAL_PARSE="$(python3 - "$OUTPUT_FILE" 2>/dev/null <<'PYEOF'
|
|
import json, sys
|
|
try:
|
|
with open(sys.argv[1]) as fh:
|
|
data = json.load(fh)
|
|
if data.get("manual_override") is True:
|
|
pref = data.get("preferred", "")
|
|
chain = data.get("fallback_chain", [])
|
|
# Output: line 1 = preferred; line 2 = comma-separated quoted chain entries.
|
|
print(pref)
|
|
print(",".join('"' + str(c) + '"' for c in chain))
|
|
except Exception:
|
|
pass
|
|
PYEOF
|
|
)" || MANUAL_PARSE=""
|
|
if [ -n "${MANUAL_PARSE:-}" ]; then
|
|
MANUAL_OVERRIDE_FLAG=true
|
|
MANUAL_OVERRIDE_PREFERRED="$(printf '%s\n' "$MANUAL_PARSE" | sed -n '1p')"
|
|
MANUAL_OVERRIDE_CHAIN="$(printf '%s\n' "$MANUAL_PARSE" | sed -n '2p')"
|
|
log "manual_override=true; preserving preferred=${MANUAL_OVERRIDE_PREFERRED}"
|
|
fi
|
|
fi
|
|
|
|
# ── Freshness check: skip detection if snapshot is recent ────────────────────
|
|
if [ "$MODE" = "write" ] && [ -f "$OUTPUT_FILE" ]; then
|
|
if find "$OUTPUT_FILE" -mtime -${STALE_AFTER_DAYS} -print 2>/dev/null | grep -q .; then
|
|
log "transport.json is fresh (<${STALE_AFTER_DAYS}d). Use --force to refresh."
|
|
cat "$OUTPUT_FILE"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# ── 1. CLI detection ─────────────────────────────────────────────────────────
|
|
CLI_PRESENT=false
|
|
CLI_BINARY=""
|
|
CLI_VERSION=""
|
|
CLI_VERSION_RAW=""
|
|
if command -v obsidian-cli >/dev/null 2>&1; then
|
|
CLI_PRESENT=true
|
|
CLI_BINARY="obsidian-cli"
|
|
# Keep two views of the version: RAW for the human log line, JSON-escaped
|
|
# for the transport.json heredoc. CLI_VERSION below is pre-quoted (includes
|
|
# the surrounding double quotes), so the heredoc emits ${CLI_VERSION}
|
|
# without wrapping quotes.
|
|
CLI_VERSION_RAW="$(obsidian-cli --version 2>/dev/null | head -1 || echo unknown)"
|
|
CLI_VERSION="$(printf '%s' "$CLI_VERSION_RAW" | json_escape || echo '"unknown"')"
|
|
elif command -v obsidian >/dev/null 2>&1; then
|
|
# Obsidian 1.12+ ships `obsidian` as the CLI binary on some platforms.
|
|
# We treat it as cli-capable if it accepts a --cli or --version flag without launching the GUI.
|
|
if obsidian --version >/dev/null 2>&1; then
|
|
CLI_PRESENT=true
|
|
CLI_BINARY="obsidian"
|
|
CLI_VERSION_RAW="$(obsidian --version 2>/dev/null | head -1 || echo unknown)"
|
|
CLI_VERSION="$(printf '%s' "$CLI_VERSION_RAW" | json_escape || echo '"unknown"')"
|
|
fi
|
|
fi
|
|
# Fallback default when neither binary was found: must still be a valid JSON literal.
|
|
if [ -z "$CLI_VERSION" ]; then
|
|
CLI_VERSION='""'
|
|
CLI_VERSION_RAW=""
|
|
fi
|
|
|
|
# ── 2. Obsidian app running? (informational only; CLI works either way) ──────
|
|
OBSIDIAN_RUNNING=false
|
|
if command -v pgrep >/dev/null 2>&1; then
|
|
if pgrep -if 'obsidian' >/dev/null 2>&1; then
|
|
OBSIDIAN_RUNNING=true
|
|
fi
|
|
fi
|
|
|
|
# ── 3. Compute preferred + fallback chain ────────────────────────────────────
|
|
if $CLI_PRESENT; then
|
|
PREFERRED="cli"
|
|
CHAIN='"cli", "filesystem"'
|
|
else
|
|
PREFERRED="filesystem"
|
|
CHAIN='"filesystem"'
|
|
fi
|
|
|
|
# ── 3b. Apply manual_override if it was parsed from the existing snapshot ────
|
|
# Auto-detected PREFERRED/CHAIN above are overridden so the user's pinned
|
|
# transport survives every refresh cycle including --force.
|
|
if $MANUAL_OVERRIDE_FLAG; then
|
|
PREFERRED="$MANUAL_OVERRIDE_PREFERRED"
|
|
CHAIN="$MANUAL_OVERRIDE_CHAIN"
|
|
fi
|
|
|
|
# ── 4. Build JSON snapshot ───────────────────────────────────────────────────
|
|
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
HOSTNAME="$(hostname 2>/dev/null || echo unknown)"
|
|
|
|
snapshot() {
|
|
cat <<JSON
|
|
{
|
|
"schema_version": 1,
|
|
"detected_at": "${TIMESTAMP}",
|
|
"host": "${HOSTNAME}",
|
|
"vault_root": "${VAULT_ROOT}",
|
|
"manual_override": ${MANUAL_OVERRIDE_FLAG},
|
|
"preferred": "${PREFERRED}",
|
|
"fallback_chain": [${CHAIN}],
|
|
"available": {
|
|
"cli": {
|
|
"present": ${CLI_PRESENT},
|
|
"binary": "${CLI_BINARY}",
|
|
"version_string": ${CLI_VERSION},
|
|
"obsidian_app_running": ${OBSIDIAN_RUNNING}
|
|
},
|
|
"filesystem": {
|
|
"present": true,
|
|
"vault_root": "${VAULT_ROOT}",
|
|
"note": "ultimate fallback; uses Claude's Read/Write/Edit tools directly"
|
|
},
|
|
"mcp_obsidian": {
|
|
"present": null,
|
|
"detection": "deferred",
|
|
"note": "v1.7 does not auto-detect MCP servers. Configure manually per wiki/references/mcp-setup.md and edit this file by hand if needed."
|
|
},
|
|
"mcpvault": {
|
|
"present": null,
|
|
"detection": "deferred",
|
|
"note": "v1.7 does not auto-detect MCP servers. Configure manually per wiki/references/mcp-setup.md and edit this file by hand if needed."
|
|
}
|
|
}
|
|
}
|
|
JSON
|
|
}
|
|
|
|
if [ "$MODE" = "peek" ]; then
|
|
snapshot
|
|
exit 0
|
|
fi
|
|
|
|
# Atomic write: stage to .tmp then rename. Avoids partial files if killed mid-write.
|
|
TMP="${OUTPUT_FILE}.$$.tmp"
|
|
trap 'rm -f "$TMP"' EXIT
|
|
snapshot > "$TMP"
|
|
mv "$TMP" "$OUTPUT_FILE"
|
|
trap - EXIT
|
|
|
|
log "Wrote: ${OUTPUT_FILE}"
|
|
log "Preferred transport: ${PREFERRED}"
|
|
$CLI_PRESENT && log " CLI: ${CLI_BINARY} (${CLI_VERSION_RAW})"
|
|
log " Filesystem: always available (Read/Write/Edit tools)"
|
|
log " MCP: not auto-detected (see wiki/references/mcp-setup.md to configure)"
|