Initial commit
This commit is contained in:
Executable
+63
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user