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
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# setup-dragonscale.sh — opt-in installer for DragonScale Memory.
#
# Provisions the runtime files that the wiki-ingest and wiki-lint skills
# feature-detect. Safe to re-run (idempotent).
#
# Does NOT install ollama or pull any embedding model. Those are
# prerequisites for Mechanism 3 (semantic tiling) and are the user's
# responsibility. Mechanism 1 (fold) and Mechanism 2 (addresses) have no
# external prerequisites.
#
# Usage:
# bash bin/setup-dragonscale.sh [optional: /path/to/vault]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VAULT="${1:-$(dirname "$SCRIPT_DIR")}"
echo "Setting up DragonScale Memory at: $VAULT"
cd "$VAULT"
# ── 1. Verify required artifacts that ship with the plugin ───────────────────
for required in "scripts/allocate-address.sh" "scripts/tiling-check.py" "skills/wiki-fold/SKILL.md"; do
if [ ! -e "$required" ]; then
echo "ERR: missing $required. Reinstall the claude-obsidian plugin." >&2
exit 1
fi
done
chmod +x scripts/allocate-address.sh scripts/tiling-check.py
# ── 2. Provision .vault-meta/ ─────────────────────────────────────────────────
mkdir -p .vault-meta
if [ ! -f .vault-meta/address-counter.txt ]; then
echo "1" > .vault-meta/address-counter.txt
echo "OK .vault-meta/address-counter.txt initialized at 1"
else
echo "-- .vault-meta/address-counter.txt already present (not overwritten)"
fi
if [ ! -f .vault-meta/tiling-thresholds.json ]; then
cat > .vault-meta/tiling-thresholds.json <<'JSON'
{
"version": 1,
"model": "nomic-embed-text",
"bands": {
"error": 0.90,
"review": 0.80
},
"calibrated": false,
"calibration_pairs_labeled": 0,
"notes": "Conservative seed thresholds, NOT calibrated against this vault. See skills/wiki-lint/SKILL.md Semantic Tiling section for the calibration procedure."
}
JSON
echo "OK .vault-meta/tiling-thresholds.json initialized with conservative seed bands"
else
echo "-- .vault-meta/tiling-thresholds.json already present (not overwritten)"
fi
# ── 3. Provision .raw/.manifest.json (if absent) ──────────────────────────────
mkdir -p .raw
if [ ! -f .raw/.manifest.json ]; then
cat > .raw/.manifest.json <<'JSON'
{
"version": 1,
"created": "DRAGONSCALE_SETUP",
"description": "Ingest delta tracker and address map for the claude-obsidian vault. Do not hand-edit; wiki-ingest maintains this.",
"sources": {},
"address_map": {}
}
JSON
# Replace placeholder with today's date
DATE=$(date +%Y-%m-%d)
sed -i.bak "s/DRAGONSCALE_SETUP/$DATE/" .raw/.manifest.json
rm -f .raw/.manifest.json.bak
echo "OK .raw/.manifest.json initialized (empty sources + address_map)"
else
echo "-- .raw/.manifest.json already present (not overwritten)"
fi
# ── 4. Rollout-baseline marker in legacy-pages.txt ────────────────────────────
if [ ! -f .vault-meta/legacy-pages.txt ]; then
cat > .vault-meta/legacy-pages.txt <<EOF
# DragonScale legacy-pages manifest
# rollout: $(date +%Y-%m-%d)
#
# List, one path per line, any pages whose frontmatter \`created:\` date is
# post-rollout but which should still be treated as legacy (i.e. not required
# to have an address). Also lines beginning with "# rollout:" set the
# per-vault rollout baseline used by wiki-lint for severity classification.
# Example:
# wiki/sources/old-page-with-wrong-metadata.md
EOF
echo "OK .vault-meta/legacy-pages.txt initialized (rollout baseline set to today)"
else
echo "-- .vault-meta/legacy-pages.txt already present (not overwritten)"
fi
# ── 5. Sanity checks ──────────────────────────────────────────────────────────
echo ""
echo "Sanity checks:"
NEXT=$(./scripts/allocate-address.sh --peek 2>&1 | tail -1)
echo " next address: c-$(printf '%06d' $NEXT)"
PYTHON=$(command -v python3 || echo "not installed")
echo " python3: $PYTHON"
if command -v curl >/dev/null 2>&1; then
if curl -sS --max-time 2 http://localhost:11434/api/version >/dev/null 2>&1; then
echo " ollama: reachable at http://localhost:11434"
if curl -sS --max-time 2 http://localhost:11434/api/tags | grep -q nomic-embed-text; then
echo " nomic-embed: installed"
else
echo " nomic-embed: NOT installed (run 'ollama pull nomic-embed-text' to enable Mechanism 3)"
fi
else
echo " ollama: not reachable (Mechanism 3 will no-op; install from https://ollama.com)"
fi
else
echo " curl: not installed (cannot check ollama)"
fi
echo ""
echo "DragonScale setup complete."
echo "See wiki/concepts/DragonScale Memory.md for the full spec."
echo "See skills/wiki-fold/ for Mechanism 1 (log folds)."
echo "wiki-ingest and wiki-lint will now feature-detect DragonScale automatically."
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# setup-mode.sh — interactive methodology mode selector (v1.8+).
#
# Sets the vault's .vault-meta/mode.json and optionally seeds template
# folders for the chosen mode. Idempotent — safe to re-run to switch modes.
# Existing files are NOT auto-migrated; the new mode only affects future
# filing operations.
#
# Usage:
# bash bin/setup-mode.sh # interactive
# bash bin/setup-mode.sh --mode lyt # non-interactive (CI / scripts)
# bash bin/setup-mode.sh --mode generic --no-seed
# bash bin/setup-mode.sh --check # diagnostics only, no write
#
# Flags:
# --mode MODE Skip the interactive prompt; pick MODE directly.
# Valid: generic | lyt | para | zettelkasten
# --no-seed Skip the optional folder-seeding step
# --check Print current mode + diagnostics; write nothing
#
# Exit codes:
# 0 — success
# 2 — usage error
# 3 — invalid mode string
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VAULT="$(dirname "$SCRIPT_DIR")"
WM="$VAULT/scripts/wiki-mode.py"
REQUESTED_MODE=""
NO_SEED=false
CHECK_ONLY=false
while [ $# -gt 0 ]; do
case "$1" in
--mode) REQUESTED_MODE="${2:-}"; shift 2 ;;
--no-seed) NO_SEED=true; shift ;;
--check) CHECK_ONLY=true; shift ;;
-h|--help)
sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*) echo "ERR: unknown flag: $1" >&2; exit 2 ;;
esac
done
say() { printf '%s\n' "$@"; }
warn() { printf 'WARN: %s\n' "$@" >&2; }
say "═══ wiki-mode setup (v1.8+) ═══"
say "Vault: $VAULT"
say ""
# ── Sanity check ────────────────────────────────────────────────────────────
if [ ! -x "$WM" ]; then
warn "scripts/wiki-mode.py not found or not executable: $WM"
exit 3
fi
# ── Diagnostics ─────────────────────────────────────────────────────────────
CURRENT=$(python3 "$WM" get 2>/dev/null || echo "generic")
say "Current mode: $CURRENT"
if $CHECK_ONLY; then
say ""
python3 "$WM" config
exit 0
fi
# ── Mode selection ──────────────────────────────────────────────────────────
if [ -z "$REQUESTED_MODE" ]; then
say ""
say "Pick a methodology mode for this vault:"
say " 1) generic — v1.7 default; wiki/sources/, entities/, concepts/"
say " 2) lyt — Linking Your Thinking (MOCs + atomic notes flat under wiki/notes/)"
say " 3) para — Projects / Areas / Resources / Archives"
say " 4) zettelkasten — timestamped IDs, flat under wiki/, dense linking"
say ""
printf "Pick [1-4, default 1]: "
read -r choice || choice="1"
case "${choice:-1}" in
1|generic) REQUESTED_MODE="generic" ;;
2|lyt) REQUESTED_MODE="lyt" ;;
3|para) REQUESTED_MODE="para" ;;
4|zettelkasten) REQUESTED_MODE="zettelkasten" ;;
*) warn "invalid choice: $choice"; exit 3 ;;
esac
fi
case "$REQUESTED_MODE" in
generic|lyt|para|zettelkasten) ;;
*) warn "invalid mode: $REQUESTED_MODE (valid: generic|lyt|para|zettelkasten)"; exit 3 ;;
esac
# ── Write the mode ──────────────────────────────────────────────────────────
python3 "$WM" set "$REQUESTED_MODE"
# ── Seed template folders (optional) ────────────────────────────────────────
if ! $NO_SEED; then
if [ -t 0 ]; then
say ""
printf "Seed template folders for %s? [y/N]: " "$REQUESTED_MODE"
read -r seed || seed="n"
else
seed="n"
fi
case "${seed:-n}" in
[yY]|[yY][eE][sS])
case "$REQUESTED_MODE" in
lyt)
mkdir -p "$VAULT/wiki/mocs" "$VAULT/wiki/notes"
say "✓ Created wiki/mocs/ and wiki/notes/"
;;
para)
mkdir -p "$VAULT/wiki/projects/inbox" "$VAULT/wiki/areas" \
"$VAULT/wiki/resources/incoming" "$VAULT/wiki/resources/people" \
"$VAULT/wiki/resources/concepts" "$VAULT/wiki/archives"
say "✓ Created PARA folder structure: projects/{inbox}/, areas/, resources/{incoming,people,concepts}/, archives/"
;;
zettelkasten)
say "✓ Zettelkasten uses no subfolders; all notes file flat under wiki/"
;;
generic)
mkdir -p "$VAULT/wiki/sources" "$VAULT/wiki/entities" \
"$VAULT/wiki/concepts" "$VAULT/wiki/sessions"
say "✓ Created generic folders: sources/, entities/, concepts/, sessions/"
;;
esac
;;
*) say "(skipped folder seeding)" ;;
esac
fi
say ""
say "═══ Done. Mode is: $REQUESTED_MODE ═══"
say ""
say "Other skills (wiki-ingest, save, autoresearch) will consult this mode automatically."
say "Existing files are NOT auto-migrated. New files will follow the new mode's conventions."
say ""
say "To switch modes later: re-run \`bash bin/setup-mode.sh\`."
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# claude-obsidian: multi-agent skill installer
# Symlinks the skills/ directory into each AI agent's expected location.
# Idempotent: safe to run multiple times.
#
# Supported agents:
# - Claude Code : auto-discovered via .claude-plugin/ (no symlink needed)
# - Codex CLI : symlink to ~/.codex/skills/claude-obsidian
# - OpenCode : symlink to ~/.opencode/skills/claude-obsidian
# - Gemini CLI : symlink to ~/.gemini/skills/claude-obsidian
# - Cursor : symlink to .cursor/skills (in repo)
# - Windsurf : symlink to .windsurf/skills (in repo)
#
# Bootstrap files (AGENTS.md, GEMINI.md, .cursor/rules/, .windsurf/rules/,
# .github/copilot-instructions.md) are already committed in the repo.
# This script just wires up the skills directory.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SKILLS_DIR="$REPO_ROOT/skills"
if [ ! -d "$SKILLS_DIR" ]; then
echo "ERROR: $SKILLS_DIR does not exist. Are you running this from the claude-obsidian repo?"
exit 1
fi
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
GRAY='\033[0;37m'
NC='\033[0m'
link_if_missing() {
local target="$1"
local dest="$2"
local agent_name="$3"
mkdir -p "$(dirname "$dest")"
if [ -L "$dest" ]; then
local existing="$(readlink "$dest")"
if [ "$existing" = "$target" ]; then
echo -e "${GRAY}[$agent_name] already linked: $dest${NC}"
return
else
echo -e "${YELLOW}[$agent_name] symlink exists but points elsewhere: $dest -> $existing (skipping, remove manually if you want to relink)${NC}"
return
fi
fi
if [ -e "$dest" ]; then
echo -e "${YELLOW}[$agent_name] path exists and is not a symlink: $dest (skipping)${NC}"
return
fi
ln -s "$target" "$dest"
echo -e "${GREEN}[$agent_name] linked: $dest -> $target${NC}"
}
echo "claude-obsidian: multi-agent skill installer"
echo "Repo: $REPO_ROOT"
echo
# Codex CLI
link_if_missing "$SKILLS_DIR" "$HOME/.codex/skills/claude-obsidian" "Codex CLI"
# OpenCode
link_if_missing "$SKILLS_DIR" "$HOME/.opencode/skills/claude-obsidian" "OpenCode"
# Gemini CLI
link_if_missing "$SKILLS_DIR" "$HOME/.gemini/skills/claude-obsidian" "Gemini CLI"
# Cursor (workspace-local)
link_if_missing "$SKILLS_DIR" "$REPO_ROOT/.cursor/skills" "Cursor"
# Windsurf (workspace-local)
link_if_missing "$SKILLS_DIR" "$REPO_ROOT/.windsurf/skills" "Windsurf"
echo
echo -e "${GREEN}Done.${NC} Bootstrap files (AGENTS.md, GEMINI.md, .cursor/rules/, .windsurf/rules/, .github/copilot-instructions.md) are already in this repo."
echo
echo "To verify each agent picks up the skills:"
echo " - Claude Code: open the project, type /wiki"
echo " - Codex CLI: codex --list-skills | grep claude-obsidian"
echo " - Cursor: open the project, ask 'what skills do you have?'"
echo " - Windsurf: open in Cascade, ask the same"
echo " - Gemini CLI: gemini --list-skills (if supported)"
+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env bash
# setup-retrieve.sh — opt-in bootstrap for wiki-retrieve (v1.7+).
#
# Provisions the contextual-prefix + BM25 + rerank pipeline. Idempotent;
# safe to re-run after schema changes or full vault re-ingest.
#
# What this does (in order):
# 1. Sanity-check that scripts/contextual-prefix.py, bm25-index.py,
# rerank.py, retrieve.py are present and executable.
# 2. Create .vault-meta/chunks/ and .vault-meta/bm25/ directories.
# 3. Check for ollama + nomic-embed-text (informational; not required for
# contextual prefix tier 2/3, but required for the rerank cosine stage).
# 4. Data-egress consent (v1.7.1+). If a non-synthetic prefix tier
# (anthropic-api or claude-cli) would otherwise be chosen, prompt
# the user for explicit y/N consent. Default is abort (synthetic-only
# remains the safe alternative). On consent, pass --allow-egress
# through to contextual-prefix.py. Pass --no-llm at the CLI to skip
# the prompt entirely and stay on tier-3 (synthetic).
# 5. Run contextual-prefix.py --all to chunk + contextualize every wiki page.
# Tier picker (synthetic by default; non-synthetic only with consent):
# tier 1: Anthropic API (--allow-egress + ANTHROPIC_API_KEY set)
# tier 2: claude CLI -p (--allow-egress + `claude` on PATH)
# tier 3: synthetic (no flag, --no-llm, or no consent)
# Stage 1 exit code is captured; non-zero aborts with a recovery hint
# (rc=5) before Stage 2.
# 6. Run bm25-index.py build to build the inverted index.
#
# After completion the wiki-retrieve skill is "feature-detected" by other
# skills (wiki-query checks for scripts/retrieve.py + .vault-meta/chunks/).
#
# This is fully opt-in. Doing nothing leaves v1.6 behavior intact.
#
# Usage:
# bash bin/setup-retrieve.sh
# bash bin/setup-retrieve.sh --no-llm # force tier-3 synthetic-only
# bash bin/setup-retrieve.sh --rebuild # rebuild all chunks
# bash bin/setup-retrieve.sh --check # diagnostics only; no provisioning
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VAULT="$(dirname "$SCRIPT_DIR")"
META="$VAULT/.vault-meta"
NO_LLM=false
REBUILD=false
CHECK_ONLY=false
while [ $# -gt 0 ]; do
case "$1" in
--no-llm) NO_LLM=true ;;
--rebuild) REBUILD=true ;;
--check) CHECK_ONLY=true ;;
-h|--help)
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "ERR: unknown flag: $1" >&2
exit 2
;;
esac
shift
done
say() { printf '%s\n' "$@"; }
warn() { printf 'WARN: %s\n' "$@" >&2; }
say "═══ wiki-retrieve setup (v1.7+) ═══"
say "Vault: $VAULT"
say ""
# ── 1. Sanity check ──────────────────────────────────────────────────────────
REQUIRED=(
"$VAULT/scripts/contextual-prefix.py"
"$VAULT/scripts/bm25-index.py"
"$VAULT/scripts/rerank.py"
"$VAULT/scripts/retrieve.py"
)
missing=0
for f in "${REQUIRED[@]}"; do
if [ ! -x "$f" ]; then
warn "missing or not executable: $f"
missing=$((missing+1))
fi
done
if [ $missing -gt 0 ]; then
say "FAIL: $missing required script(s) missing."
exit 3
fi
say "✓ All 4 retrieval scripts present and executable"
# ── 2. Provision .vault-meta state directories ───────────────────────────────
mkdir -p "$META/chunks" "$META/bm25"
say "✓ State directories: $META/chunks/, $META/bm25/"
# ── 3. Check ollama (informational) ──────────────────────────────────────────
OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11434}"
# v1.9.1 / closes audit S4: if OLLAMA_URL was overridden to point off-machine,
# refuse to probe unless the caller passes --allow-remote-ollama (mirrors the
# existing scripts/tiling-check.py:351 gate). Same allowlist of localhost
# patterns ollama itself recommends.
case "$OLLAMA_URL" in
"http://127.0.0.1:"*|"http://localhost:"*|"http://[::1]:"*) ;;
*)
if ! printf '%s ' "$@" | grep -q -- '--allow-remote-ollama'; then
warn "OLLAMA_URL points off-localhost: $OLLAMA_URL"
warn "Refusing to probe remote ollama without explicit consent."
warn "Pass --allow-remote-ollama to bin/setup-retrieve.sh to opt in, or"
warn "unset OLLAMA_URL to use the default http://127.0.0.1:11434."
OLLAMA_URL=""
fi
;;
esac
OLLAMA_ALIVE=false
MODEL_PRESENT=false
if [ -n "$OLLAMA_URL" ] && command -v curl >/dev/null 2>&1; then
if curl -fsS --max-time 3 "$OLLAMA_URL/api/tags" >/dev/null 2>&1; then
OLLAMA_ALIVE=true
if curl -fsS --max-time 3 "$OLLAMA_URL/api/tags" 2>/dev/null \
| grep -q '"nomic-embed-text'; then
MODEL_PRESENT=true
fi
fi
fi
if $OLLAMA_ALIVE && $MODEL_PRESENT; then
say "✓ ollama reachable at $OLLAMA_URL with nomic-embed-text pulled (rerank will use cosine)"
elif $OLLAMA_ALIVE; then
warn "ollama reachable but nomic-embed-text is not pulled. Run: ollama pull nomic-embed-text"
warn "rerank stage will no-op until the model is available."
else
warn "ollama not reachable at $OLLAMA_URL"
warn "rerank stage will no-op until ollama is running. BM25 retrieval still works."
warn "Install: https://ollama.com/download; then: ollama pull nomic-embed-text"
fi
# ── 4. Prefix-tier picker (informational) ────────────────────────────────────
# v1.7.1: tier reflects what WOULD run if --allow-egress were passed.
# Without consent, the actual run forces tier-3 synthetic.
if $NO_LLM; then
PREFIX_TIER="synthetic (forced via --no-llm)"
elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then
PREFIX_TIER="anthropic-api (ANTHROPIC_API_KEY detected; ~\$12/1000 docs)"
elif command -v claude >/dev/null 2>&1; then
PREFIX_TIER="claude-cli subprocess (no API key needed; uses CC subscription)"
else
PREFIX_TIER="synthetic (no API key, no claude CLI; reduced retrieval quality)"
fi
say "✓ Contextual-prefix tier (if --allow-egress): $PREFIX_TIER"
if $CHECK_ONLY; then
say ""
say "── --check passed; not provisioning."
exit 0
fi
# ── 4b. Egress consent (v1.7.1) ──────────────────────────────────────────────
# If a non-synthetic tier would otherwise be selected, require explicit consent
# before letting contextual-prefix.py send page bodies off-machine. Mirrors the
# --allow-remote-ollama precedent in scripts/tiling-check.py.
ALLOW_EGRESS=false
if ! $NO_LLM; then
case "$PREFIX_TIER" in
anthropic-api*|claude-cli*)
say ""
say "⚠️ Stage 1 will send wiki page BODIES off-machine via the '$PREFIX_TIER' tier."
say " Estimated cost: ~\$0 (claude-cli, free) to ~\$12 per 1,000 pages (Anthropic API)."
say " Per-page bodies are POSTed to the provider; review their privacy policy first."
say " Default is NO. Tier-3 (synthetic, on-machine) is the safe alternative."
printf " Continue with egress? [y/N]: "
read -r reply || reply=""
case "$reply" in
[yY]|[yY][eE][sS])
say "→ Proceeding with egress."
ALLOW_EGRESS=true
;;
*)
say "→ Aborted. Re-run with --no-llm for the synthetic-only path,"
say " or set ANTHROPIC_API_KEY and use the claude CLI deliberately."
exit 0
;;
esac
;;
esac
fi
# ── 5. Chunk + contextualize every wiki page ─────────────────────────────────
say ""
say "═══ Stage 1/2: chunking + contextual-prefix generation ═══"
ARGS=("--all")
$NO_LLM && ARGS+=("--no-llm")
$ALLOW_EGRESS && ARGS+=("--allow-egress")
$REBUILD && ARGS+=("--rebuild")
# Disable set -e for the call so we can inspect the exit code and offer a
# concrete recovery hint instead of aborting with a bare trace.
set +e
python3 "$VAULT/scripts/contextual-prefix.py" "${ARGS[@]}"
STAGE1_RC=$?
set -e
if [ "$STAGE1_RC" -ne 0 ]; then
warn "Stage 1 failed (rc=$STAGE1_RC). Partial chunks may exist at:"
warn " $META/chunks/"
warn "Recovery options:"
warn " 1. Re-run setup-retrieve.sh — body_hash skips already-processed chunks."
warn " 2. Wipe and start over: rm -rf $META/chunks/ && bash bin/setup-retrieve.sh"
warn " 3. Re-process one page: python3 scripts/contextual-prefix.py wiki/<failing-page>.md --rebuild"
exit 5
fi
# ── 6. Build BM25 index ──────────────────────────────────────────────────────
say ""
say "═══ Stage 2/2: BM25 index build ═══"
python3 "$VAULT/scripts/bm25-index.py" build
# ── 7. Smoke-test retrieve.py ────────────────────────────────────────────────
say ""
say "═══ Smoke test ═══"
SMOKE_OUT="$(python3 "$VAULT/scripts/retrieve.py" "wiki" --top 1 2>/dev/null || echo '{}')"
if echo "$SMOKE_OUT" | grep -q '"candidates":'; then
say "✓ retrieve.py returns valid JSON"
else
warn "retrieve.py smoke test produced unexpected output. Run manually for details."
fi
say ""
say "═══ wiki-retrieve is provisioned. ═══"
say ""
say "Usage from the command line:"
say " python3 scripts/retrieve.py \"your question here\" --top 5"
say ""
say "Other skills (wiki-query, autoresearch) will now automatically use the"
say "hybrid pipeline when answering questions. See skills/wiki-retrieve/SKILL.md."
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# claude-obsidian vault setup script
# Run this ONCE before opening Obsidian for the first time.
# Usage: bash bin/setup-vault.sh [optional: /path/to/vault]
# Default: uses the directory where this script lives (the vault root)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VAULT="${1:-$(dirname "$SCRIPT_DIR")}"
OBSIDIAN="$VAULT/.obsidian"
echo "Setting up claude-obsidian vault at: $VAULT"
# ── 1. Create directories ─────────────────────────────────────────────────────
mkdir -p "$OBSIDIAN/snippets"
mkdir -p "$VAULT/.raw"
mkdir -p "$VAULT/wiki/concepts" "$VAULT/wiki/entities" "$VAULT/wiki/sources" "$VAULT/wiki/meta"
mkdir -p "$VAULT/_templates"
# ── 2. Write graph.json ───────────────────────────────────────────────────────
cat > "$OBSIDIAN/graph.json" << 'EOF'
{
"collapse-filter": false,
"search": "path:wiki",
"showTags": false,
"showAttachments": false,
"hideUnresolved": true,
"showOrphans": false,
"collapse-color-groups": false,
"colorGroups": [
{ "query": "path:wiki/entities", "color": { "a": 1, "rgb": 12945088 } },
{ "query": "path:wiki/concepts", "color": { "a": 1, "rgb": 5227007 } },
{ "query": "path:wiki/sources", "color": { "a": 1, "rgb": 6986069 } },
{ "query": "path:wiki/meta", "color": { "a": 1, "rgb": 5676246 } },
{ "query": "path:wiki", "color": { "a": 1, "rgb": 5676246 } }
],
"showArrow": true,
"textFadeMultiplier": -1,
"nodeSizeMultiplier": 1.8,
"lineSizeMultiplier": 1.2,
"centerStrength": 0.5,
"repelStrength": 30,
"linkStrength": 1.5,
"linkDistance": 120,
"scale": 1.0
}
EOF
# ── 3. Write app.json (excluded files) ───────────────────────────────────────
cat > "$OBSIDIAN/app.json" << 'EOF'
{
"userIgnoreFilters": [
"agents/",
"commands/",
"hooks/",
"skills/",
"_templates/",
"README.md",
"CLAUDE.md",
"WIKI.md",
"Welcome.md"
]
}
EOF
# ── 4. Write appearance.json (enable CSS snippets) ───────────────────────────
cat > "$OBSIDIAN/appearance.json" << 'EOF'
{
"enabledCssSnippets": [
"vault-colors",
"ITS-Dataview-Cards",
"ITS-Image-Adjustments"
]
}
EOF
# ── 5. Download Excalidraw main.js (8MB, not in git) ─────────────────────────
EXCALIDRAW="$OBSIDIAN/plugins/obsidian-excalidraw-plugin"
if [ -f "$EXCALIDRAW/manifest.json" ] && [ ! -f "$EXCALIDRAW/main.js" ]; then
echo "Downloading Excalidraw main.js (~8MB)..."
curl -sS -L \
"https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/latest/download/main.js" \
-o "$EXCALIDRAW/main.js"
echo "✓ Excalidraw main.js downloaded"
elif [ -f "$EXCALIDRAW/main.js" ]; then
echo "✓ Excalidraw main.js already present"
fi
echo ""
echo "✓ Setup complete."
echo ""
echo "Next steps:"
echo " 1. Open Obsidian"
echo " 2. Manage Vaults → Open folder as vault → select: $VAULT"
echo " 3. Enable community plugins when prompted (Calendar, Thino, Excalidraw, Banners are pre-installed)"
echo " 4. Install: Dataview, Templater, Obsidian Git (Settings → Community Plugins)"
echo " 5. Type /wiki in Claude Code to scaffold your knowledge base"
echo ""
echo "Pre-installed plugins:"
echo " - Calendar (sidebar calendar with word count + task dots)"
echo " - Thino (quick memo capture)"
echo " - Excalidraw (freehand drawing + image annotation)"
echo " - Banners (add banner: to any note frontmatter for header images)"
echo ""
echo "CSS snippets enabled:"
echo " - vault-colors: color-codes wiki/ folders in file explorer"
echo " - ITS-Dataview-Cards: use \`\`\`dataviewjs with .cards for card grids"
echo " - ITS-Image-Adjustments: append |100 to image embeds for sizing"
echo ""
echo "Views available:"
echo " - Wiki Map canvas (wiki/Wiki Map.canvas) — knowledge graph"
echo " - Design Ideas canvas (projects/visual-vault/design-ideas.canvas) — visual reference board"
echo " - Graph view filtered to wiki/ only, color-coded by type"
echo ""
echo "To switch to the visual layout (Canvas + Calendar + Thino sidebar):"
echo " Quit Obsidian, then run:"
echo " cp $OBSIDIAN/workspace-visual.json $OBSIDIAN/workspace.json"
echo " Then reopen Obsidian."
echo ""
echo "Graph colors: if they reset after closing Obsidian, open Graph settings"
echo "→ Color groups and re-add them once. They persist permanently after that."