Initial commit

This commit is contained in:
Henner M. Kruse
2026-08-04 00:14:42 +02:00
commit c774df7ad7
12 changed files with 705 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Shared helpers, sourced by the other scripts in this directory.
# Not meant to be executed directly, and not whitelisted on its own.
CONFIG_FILE="$HOME/.agent-skills/obsidian-vault-kb/config.json"
# Resolve a vault's absolute path by name from the config file.
# If no name is given and the config has exactly one vault, use that one.
# Never accepts a raw path from the caller — only a name looked up
# server-side (in the config file) — so a whitelisted script can't be
# pointed at an arbitrary directory outside the configured vault(s).
resolve_vault_path() {
local vault_name="${1:-}"
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: no configuration found at $CONFIG_FILE. Run /setup first." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: python3 is required to parse the config file." >&2
exit 1
fi
python3 - "$CONFIG_FILE" "$vault_name" << 'PYEOF'
import json, sys
config_file, vault_name = sys.argv[1], sys.argv[2]
with open(config_file) as f:
cfg = json.load(f)
vaults = cfg.get("vaults", [])
if not vaults:
sys.stderr.write("Error: no vaults defined in config.\n")
sys.exit(1)
if vault_name:
match = [v for v in vaults if v.get("name") == vault_name]
if not match:
sys.stderr.write(f"Error: no vault named '{vault_name}' in config.\n")
sys.exit(1)
print(match[0]["path"])
elif len(vaults) == 1:
print(vaults[0]["path"])
else:
names = ", ".join(v.get("name", "?") for v in vaults)
sys.stderr.write(f"Error: multiple vaults configured ({names}); specify one by name.\n")
sys.exit(1)
PYEOF
}
# Validate that a resolved path is an existing, real directory and print
# its canonical form (resolves symlinks, blocks '..' tricks).
canonicalize_and_check_dir() {
local path="$1"
local real
real=$(realpath -e "$path" 2>/dev/null) || {
echo "Error: vault path '$path' does not exist." >&2
exit 1
}
if [ ! -d "$real" ]; then
echo "Error: vault path '$real' is not a directory." >&2
exit 1
fi
echo "$real"
}
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Find backlinks (notes containing [[note-name]]) within a configured vault.
#
# Usage: vault_backlinks.sh <note-name> [vault-name]
#
# The vault path always comes from
# ~/.agent-skills/obsidian-vault-kb/config.json — never from a raw argument.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib.sh
source "$SCRIPT_DIR/_lib.sh"
NOTE_NAME="${1:?Usage: vault_backlinks.sh <note-name> [vault-name]}"
VAULT_NAME="${2:-}"
VAULT_RAW="$(resolve_vault_path "$VAULT_NAME")"
VAULT="$(canonicalize_and_check_dir "$VAULT_RAW")"
# Build a regex-safe literal for the note name (escape regex metachars).
ESCAPED_NAME=$(printf '%s' "$NOTE_NAME" | sed -e 's/[.[\*^$/]/\\&/g')
echo "== Notes linking to [[$NOTE_NAME]] in $VAULT =="
echo
if command -v rg >/dev/null 2>&1; then
rg -n "\[\[${ESCAPED_NAME}(\||\]\])" "$VAULT" \
-g '!.obsidian' -g '!.git' -g '*.md' || echo "(no backlinks found)"
else
grep -rn "\[\[${ESCAPED_NAME}" "$VAULT" --include='*.md' 2>/dev/null || echo "(no backlinks found)"
fi
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Quick structured overview of a configured Obsidian vault:
# folder structure, note count per folder, frontmatter tags in use.
#
# Usage: vault_index.sh [vault-name]
# vault-name is optional if only one vault is configured.
# The path is always resolved from ~/.agent-skills/obsidian-vault-kb/config.json —
# this script never accepts a raw filesystem path as an argument.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib.sh
source "$SCRIPT_DIR/_lib.sh"
VAULT_NAME="${1:-}"
VAULT_RAW="$(resolve_vault_path "$VAULT_NAME")"
VAULT="$(canonicalize_and_check_dir "$VAULT_RAW")"
echo "== Vault: $VAULT =="
echo
echo "== Folder structure =="
find "$VAULT" -type d \
-not -path '*/.obsidian*' -not -path '*/.git*' \
| sed "s|^$VAULT||" | sort
echo
echo "== Notes per top-level folder =="
find "$VAULT" -mindepth 1 -maxdepth 1 -type d \
-not -name '.obsidian' -not -name '.git' \
| while read -r dir; do
count=$(find "$dir" -name '*.md' | wc -l)
echo "$(basename "$dir"): $count"
done
echo
echo "== Frontmatter tags (field 'tags:') =="
if command -v rg >/dev/null 2>&1; then
rg -o --no-filename '^tags:\s*\[?([^]]*)\]?' -r '$1' "$VAULT" 2>/dev/null \
| tr ',' '\n' | sed 's/^\s*-\?\s*//; s/\s*$//' | sort -u | grep -v '^$' || true
else
grep -rho '^tags:.*' "$VAULT" --include='*.md' 2>/dev/null | sort -u || true
fi
echo
echo "== Possible index/MOC files =="
find "$VAULT" -iname '*index*.md' -o -iname '*moc*.md' -o -iname '*overview*.md' 2>/dev/null || true
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Full-text search strictly confined to a configured vault.
#
# Usage: vault_search.sh <query> [vault-name] [subfolder]
# - query: search term (plain text, treated as a fixed string, not a
# shell-interpreted pattern)
# - vault-name: optional if only one vault is configured
# - subfolder: optional, relative subfolder to narrow the search; rejected
# if it tries to escape the vault (e.g. contains '..' or is
# an absolute path)
#
# The vault path always comes from
# ~/.agent-skills/obsidian-vault-kb/config.json — never from a raw argument —
# so this script can't be used to search outside the configured vault(s).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib.sh
source "$SCRIPT_DIR/_lib.sh"
QUERY="${1:?Usage: vault_search.sh <query> [vault-name] [subfolder]}"
VAULT_NAME="${2:-}"
SUBFOLDER="${3:-}"
VAULT_RAW="$(resolve_vault_path "$VAULT_NAME")"
VAULT="$(canonicalize_and_check_dir "$VAULT_RAW")"
SEARCH_ROOT="$VAULT"
if [ -n "$SUBFOLDER" ]; then
case "$SUBFOLDER" in
/*|*..*)
echo "Error: subfolder must be a relative path within the vault (no '..' or absolute paths)." >&2
exit 1
;;
esac
CANDIDATE="$VAULT/$SUBFOLDER"
SEARCH_ROOT="$(realpath -e "$CANDIDATE" 2>/dev/null)" || {
echo "Error: subfolder '$SUBFOLDER' does not exist in the vault." >&2
exit 1
}
# Ensure the resolved path is still inside the vault after symlink resolution.
case "$SEARCH_ROOT" in
"$VAULT"/*|"$VAULT") ;;
*)
echo "Error: subfolder resolves outside the vault, refusing." >&2
exit 1
;;
esac
fi
echo "== Search root: $SEARCH_ROOT =="
echo
if command -v rg >/dev/null 2>&1; then
rg -i -n --fixed-strings -- "$QUERY" "$SEARCH_ROOT" \
-g '!.obsidian' -g '!.git' -g '*.md' || echo "(no matches)"
else
grep -ril -- "$QUERY" "$SEARCH_ROOT" --include='*.md' 2>/dev/null || echo "(no matches)"
fi