add claude-obsidian
Tests / Hermetic test suite (push) Has been cancelled
Tests / Skill frontmatter validation (push) Has been cancelled

This commit is contained in:
김경종
2026-05-28 10:57:16 +09:00
parent 1b07531a45
commit 72dad72703
205 changed files with 41703 additions and 80 deletions
+229
View File
@@ -0,0 +1,229 @@
#!/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)"