Initial commit
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "skill-repo",
|
||||||
|
"owner": {
|
||||||
|
"name": "haemka"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Private collection of tool-neutral Agent Skills, with Claude Code-specific setup/permissions adapters.",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "obsidian-vault-kb",
|
||||||
|
"source": "./vendor/claude-code/plugins/obsidian-vault-kb",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Obsidian vault as a searchable knowledge base for lookup and note-taking.",
|
||||||
|
"keywords": ["obsidian", "knowledge-base", "notes", "vault", "markdown"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# skill-repo
|
||||||
|
|
||||||
|
Private collection of tool-neutral Agent Skills, plus optional vendor-specific
|
||||||
|
setup/integration adapters (currently: Claude Code).
|
||||||
|
|
||||||
|
## Why this structure
|
||||||
|
|
||||||
|
The `SKILL.md` format (YAML frontmatter + Markdown body + `scripts/` /
|
||||||
|
`references/` / `assets/`) is an open standard published at
|
||||||
|
[agentskills.io](https://agentskills.io), adopted by Claude Code, OpenAI
|
||||||
|
Codex, GitHub Copilot, Cursor, Gemini CLI, and others — the same skill files
|
||||||
|
work unmodified across these tools. What's *not* portable is host-specific
|
||||||
|
plumbing: slash commands, permission/allow-list syntax, plugin/marketplace
|
||||||
|
manifests. This repo keeps those two concerns physically separate.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
skill-repo/
|
||||||
|
├── skills/ ← tool-neutral, single source of truth
|
||||||
|
│ └── obsidian-vault-kb/
|
||||||
|
│ ├── SKILL.md
|
||||||
|
│ ├── references/
|
||||||
|
│ └── scripts/ ← config lives at ~/.agent-skills/<skill>/config.json
|
||||||
|
├── vendor/
|
||||||
|
│ └── claude-code/
|
||||||
|
│ ├── .claude-plugin/ ← (unused here; marketplace.json lives at repo root, see below)
|
||||||
|
│ └── plugins/
|
||||||
|
│ └── obsidian-vault-kb/
|
||||||
|
│ ├── .claude-plugin/
|
||||||
|
│ │ └── plugin.json
|
||||||
|
│ ├── skills/
|
||||||
|
│ │ └── obsidian-vault-kb -> ../../../../../skills/obsidian-vault-kb (symlink)
|
||||||
|
│ ├── commands/
|
||||||
|
│ │ └── setup.md ← -> /obsidian-vault-kb:setup
|
||||||
|
│ └── permissions-whitelist.template.json
|
||||||
|
└── .claude-plugin/
|
||||||
|
└── marketplace.json ← must live at repo root per Claude Code's convention
|
||||||
|
```
|
||||||
|
|
||||||
|
**`skills/<name>/`** is the only place skill content actually lives —
|
||||||
|
`SKILL.md`, `scripts/`, `references/`. It assumes nothing about the host
|
||||||
|
tool: configuration is read from `~/.agent-skills/<skill-name>/config.json`,
|
||||||
|
a tool-neutral location, not `~/.claude/...`.
|
||||||
|
|
||||||
|
**`vendor/<tool>/`** holds everything specific to one host tool: for Claude
|
||||||
|
Code, that's the plugin manifest, the permissions whitelist (Claude Code's
|
||||||
|
`Bash(...)` allow-list syntax), and the `/setup` slash command. The plugin's
|
||||||
|
`skills/<name>` is a **symlink** into the top-level `skills/` directory —
|
||||||
|
there is exactly one copy of the skill content on disk, never a duplicate
|
||||||
|
that can drift out of sync.
|
||||||
|
|
||||||
|
Adding support for another tool (e.g. a future OpenAI/Codex-specific
|
||||||
|
adapter) means adding `vendor/<other-tool>/` with that tool's own
|
||||||
|
conventions, symlinked back to the same `skills/<name>/`, without touching
|
||||||
|
the skill content itself.
|
||||||
|
|
||||||
|
Symlinks assume a POSIX filesystem; this repo doesn't attempt to be
|
||||||
|
Windows-git-checkout-friendly.
|
||||||
|
|
||||||
|
## Adding this marketplace locally (Claude Code)
|
||||||
|
|
||||||
|
```
|
||||||
|
/plugin marketplace add /absolute/path/to/skill-repo
|
||||||
|
/plugin install obsidian-vault-kb
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `/obsidian-vault-kb:setup` to configure the vault path/mode and the
|
||||||
|
permissions whitelist.
|
||||||
|
|
||||||
|
## Adding a new skill to this repo
|
||||||
|
|
||||||
|
1. Create `skills/<new-skill-name>/` with `SKILL.md`, and `scripts/`
|
||||||
|
/`references/` as needed. Keep configuration at
|
||||||
|
`~/.agent-skills/<new-skill-name>/config.json` if the skill needs
|
||||||
|
persistent config — don't hardcode a Claude-specific path here.
|
||||||
|
2. For Claude Code support, create
|
||||||
|
`vendor/claude-code/plugins/<new-skill-name>/` with `.claude-plugin/plugin.json`,
|
||||||
|
a `skills/<new-skill-name>` symlink back to the top-level `skills/`
|
||||||
|
directory, and (if the skill needs restricted shell access) a
|
||||||
|
`commands/setup.md` plus `permissions-whitelist.template.json` following
|
||||||
|
the `obsidian-vault-kb` pattern — wrapper scripts that resolve their own
|
||||||
|
scope from the config file rather than accepting raw paths as arguments,
|
||||||
|
whitelisted only by exact absolute path, never generic tool wildcards
|
||||||
|
like `Bash(find:*)`.
|
||||||
|
3. Add an entry for the new plugin to `.claude-plugin/marketplace.json`.
|
||||||
|
4. Bump `version` in the plugin's `plugin.json` and its `marketplace.json`
|
||||||
|
entry on updates.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
name: obsidian-vault-kb
|
||||||
|
description: Reads an Obsidian vault as a knowledge base and uses its notes (Markdown files, frontmatter, tags, [[wikilinks]], folder structure) to answer questions and help solve problems, regardless of the vault's subject matter. Always use this skill when the user refers to their "vault", "notes", "docs", "wiki", "Obsidian", or a personal/topic-specific knowledge base — even if "Obsidian" isn't mentioned explicitly, as long as the context suggests a personal note collection as the information source (e.g. "check my notes", "what did I write about this", "according to my docs"). Covers both plain lookup and creating/updating notes, depending on the mode configured on first contact.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Obsidian Vault as Knowledge Base
|
||||||
|
|
||||||
|
This skill makes an Obsidian vault (a directory of Markdown files with
|
||||||
|
Obsidian-typical structure: YAML frontmatter, `[[wikilinks]]`, `#tags`,
|
||||||
|
folder conventions) usable as a searchable knowledge base, regardless of what
|
||||||
|
topic the vault covers (e.g. technical documentation, project notes,
|
||||||
|
research or study notes).
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Check configuration (always first)
|
||||||
|
|
||||||
|
Configuration lives in `~/.agent-skills/obsidian-vault-kb/config.json`. Before any
|
||||||
|
substantive action, check whether this file exists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat ~/.agent-skills/obsidian-vault-kb/config.json 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
**If the file exists:** Use the configuration from it (vault path(s), mode)
|
||||||
|
and go straight to step 2 (search). Do not ask again.
|
||||||
|
|
||||||
|
**If the file is missing (first call):** Briefly ask the user:
|
||||||
|
|
||||||
|
1. Path to the vault (absolute path, e.g. `/home/user/vaults/notes`).
|
||||||
|
Multiple vaults are possible — assign a short name to each.
|
||||||
|
2. Mode:
|
||||||
|
- `read-only` — read only, no write access whatsoever
|
||||||
|
- `append` — read + append new findings as a new, dated note (existing
|
||||||
|
notes are never modified)
|
||||||
|
- `maintain` — read + actively update/extend existing notes (e.g. status
|
||||||
|
fields, ongoing logs)
|
||||||
|
|
||||||
|
Then create the configuration file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.agent-skills/obsidian-vault-kb
|
||||||
|
cat > ~/.agent-skills/obsidian-vault-kb/config.json << 'EOF'
|
||||||
|
{
|
||||||
|
"vaults": [
|
||||||
|
{"name": "notes", "path": "/absolute/path/to/vault"}
|
||||||
|
],
|
||||||
|
"mode": "append"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Briefly confirm what was saved, then proceed. The user can change the
|
||||||
|
configuration at any time by explicitly saying so (e.g. "use a different
|
||||||
|
vault from now on", "switch to read-only mode") — then overwrite the file
|
||||||
|
with the new values.
|
||||||
|
|
||||||
|
### 2. Search the vault
|
||||||
|
|
||||||
|
For questions/problems, first use the folder structure as a topical
|
||||||
|
categorization, then dig into content. Details and example commands are in
|
||||||
|
`references/search-strategy.md` — consult it for any non-trivial search,
|
||||||
|
especially when using tags/frontmatter/wikilinks.
|
||||||
|
|
||||||
|
Short version of the approach:
|
||||||
|
|
||||||
|
1. Get an overview of the vault structure:
|
||||||
|
`<skill-dir>/scripts/vault_index.sh [vault-name]` — returns folders as
|
||||||
|
rough categories, note counts, existing frontmatter tags, and possible
|
||||||
|
index/MOC files.
|
||||||
|
2. Targeted full-text search with
|
||||||
|
`<skill-dir>/scripts/vault_search.sh "<query>" [vault-name] [subfolder]`
|
||||||
|
— scoped to the most plausible subfolder from step 1 where useful.
|
||||||
|
3. For a specific note, follow backlinks with
|
||||||
|
`<skill-dir>/scripts/vault_backlinks.sh "<note-name>" [vault-name]` to
|
||||||
|
find related notes that link to it.
|
||||||
|
4. Open matching files (via the `view`/read tool, not a shell command) to
|
||||||
|
read their frontmatter (tags, `status`, `aliases`) and content in full
|
||||||
|
once a promising candidate has been found.
|
||||||
|
|
||||||
|
These three scripts always resolve the vault path from
|
||||||
|
`~/.agent-skills/obsidian-vault-kb/config.json` themselves and refuse to operate
|
||||||
|
outside the configured vault — they never take a raw filesystem path as an
|
||||||
|
argument. This is intentional: it's what allows them (and only them) to be
|
||||||
|
safely whitelisted in Claude Code's permissions without granting broader
|
||||||
|
shell/filesystem access. See "Setup and permissions" below and
|
||||||
|
`references/search-strategy.md` for manual fallback commands (`find`, `rg`,
|
||||||
|
`grep`) for cases the scripts don't cover — those are not whitelisted and
|
||||||
|
will prompt for confirmation, by design.
|
||||||
|
|
||||||
|
Always start with a narrow, targeted search and only widen it if needed —
|
||||||
|
don't read the whole vault unstructured.
|
||||||
|
|
||||||
|
### 3. Formulate the answer
|
||||||
|
|
||||||
|
- Base answers explicitly on the notes found, briefly citing the source
|
||||||
|
(file name/note title); don't invent anything the vault doesn't support.
|
||||||
|
- If the vault doesn't contain relevant information, say so openly instead
|
||||||
|
of guessing, and clearly separate that from general domain knowledge if
|
||||||
|
used.
|
||||||
|
- For topics with exact detail values (e.g. configuration data, numbers,
|
||||||
|
names, version info), carry these over verbatim from the notes instead of
|
||||||
|
paraphrasing, since accuracy matters here.
|
||||||
|
|
||||||
|
### 4. Write notes (only in `append` or `maintain` mode)
|
||||||
|
|
||||||
|
- **`append`**: File the new finding/solution as a new file, e.g.
|
||||||
|
`<vault>/Notes/YYYY-MM-DD-<short-topic>.md` with frontmatter (`tags`,
|
||||||
|
`date`, possibly `related`) and wikilinks to affected topics/systems.
|
||||||
|
Never modify existing files.
|
||||||
|
- **`maintain`**: Extend an existing relevant note directly (e.g. append a
|
||||||
|
new "Update YYYY-MM-DD" section or update a status field in the
|
||||||
|
frontmatter) instead of creating a new file, when an existing note is the
|
||||||
|
right place for the content.
|
||||||
|
- **`read-only`**: Never create or modify files in the vault. If the user
|
||||||
|
asks anyway, briefly point out the current mode and ask whether the
|
||||||
|
configuration should be changed.
|
||||||
|
- Before writing, always briefly summarize what will be written; for more
|
||||||
|
substantial changes (`maintain` mode), explicitly confirm before modifying
|
||||||
|
an existing note.
|
||||||
|
|
||||||
|
## Setup and permissions
|
||||||
|
|
||||||
|
This SKILL.md and the `scripts/`/`references/` next to it are the
|
||||||
|
tool-neutral core of this skill (per the open Agent Skills spec at
|
||||||
|
agentskills.io) and don't assume any particular host tool. Configuration
|
||||||
|
always lives at the tool-neutral path
|
||||||
|
`~/.agent-skills/obsidian-vault-kb/config.json`, regardless of which agent
|
||||||
|
is running this skill.
|
||||||
|
|
||||||
|
Host-specific setup helpers live under `vendor/<tool>/` in this repo, not
|
||||||
|
here. For Claude Code specifically, `vendor/claude-code/plugins/obsidian-vault-kb/`
|
||||||
|
provides a `commands/setup.md` slash command — automatically namespaced by
|
||||||
|
Claude Code's plugin system and invoked as `/obsidian-vault-kb:setup` — plus
|
||||||
|
a `permissions-whitelist.template.json` for Claude Code's Bash permission
|
||||||
|
system. Only three exact, absolute-path commands are whitelisted there — the
|
||||||
|
three wrapper scripts in `scripts/` — never a generic `Bash(find:*)`,
|
||||||
|
`Bash(rg:*)`, `Bash(grep:*)`, or `Bash(cat:*)`. Those would allow reading
|
||||||
|
anywhere on the filesystem; the wrapper scripts instead resolve the vault
|
||||||
|
path only from `~/.agent-skills/obsidian-vault-kb/config.json` and refuse to
|
||||||
|
operate outside it, so whitelisting them by exact path doesn't widen access
|
||||||
|
beyond the configured vault.
|
||||||
|
|
||||||
|
If no host-specific setup helper exists for the tool currently running this
|
||||||
|
skill, or configuration is simply missing, run through the first-call flow
|
||||||
|
in step 1 above directly instead.
|
||||||
|
|
||||||
|
Nothing that writes into the vault or into the config file is whitelisted —
|
||||||
|
those keep prompting for confirmation every time, in `append`/`maintain`
|
||||||
|
mode, by design.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This skill doesn't replace the user's own folder-structure conventions —
|
||||||
|
the structure detected in step 1 determines how content is categorized,
|
||||||
|
not a fixed predefined schema.
|
||||||
|
- Don't automatically read binary files/attachments (`.pdf`, images) in the
|
||||||
|
vault, just reference them, unless the user explicitly asks for it.
|
||||||
|
- Ignore the vault's `.obsidian/` configuration directory, it contains no
|
||||||
|
knowledge content.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Search Strategy for the Obsidian Vault (manual fallback)
|
||||||
|
|
||||||
|
The three wrapper scripts (`scripts/vault_index.sh`, `scripts/vault_search.sh`,
|
||||||
|
`scripts/vault_backlinks.sh`) cover the common cases and are the only
|
||||||
|
commands whitelisted by `/setup`. Use the raw commands below only when the
|
||||||
|
scripts genuinely don't cover what's needed (e.g. a one-off query shape they
|
||||||
|
don't support). These are **not** whitelisted, so each one will prompt the
|
||||||
|
user for confirmation — that's intentional, since they aren't confined to
|
||||||
|
the configured vault the way the scripts are.
|
||||||
|
|
||||||
|
## 1. Get an overview of the structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Folder structure (= topical categories per the user's own convention)
|
||||||
|
find <vault> -type d -not -path '*/.obsidian*' -not -path '*/.git*' | sort
|
||||||
|
|
||||||
|
# Do "index"/"MOC" (Map of Content) files exist? These often bundle links
|
||||||
|
# on a topic and are a good entry point.
|
||||||
|
find <vault> -iname '*index*.md' -o -iname '*moc*.md' -o -iname '*overview*.md'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Full-text search
|
||||||
|
|
||||||
|
Prefer `rg` (ripgrep), it's much faster and respects `.gitignore`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic search, case-insensitive, with file names and line numbers
|
||||||
|
rg -i -n "<search-term>" <vault> -g '!.obsidian' -g '!.git'
|
||||||
|
|
||||||
|
# Only file names of matches (for a quick overview)
|
||||||
|
rg -i -l "<search-term>" <vault>
|
||||||
|
|
||||||
|
# Combine multiple terms (AND via chained greps)
|
||||||
|
rg -i -l "term-a" <vault> | xargs rg -i -l "term-b"
|
||||||
|
```
|
||||||
|
|
||||||
|
Fallback without ripgrep:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -ril "<search-term>" <vault> --include='*.md'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Use frontmatter and tags
|
||||||
|
|
||||||
|
Obsidian frontmatter is a YAML block at the top of a file between `---`.
|
||||||
|
Tags can live there (`tags: [topic-a, topic-b]`) or inline in the text
|
||||||
|
(`#topic-a`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All notes with a specific tag in the frontmatter
|
||||||
|
rg -l '^tags:.*topic-a' <vault>
|
||||||
|
|
||||||
|
# All notes with a specific inline tag
|
||||||
|
rg -l '#topic-a\b' <vault>
|
||||||
|
|
||||||
|
# View the frontmatter of a single file (the first 15 lines are usually enough)
|
||||||
|
head -n 15 "<path-to-file>.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the vault uses a consistent tag scheme (e.g. `status/open`,
|
||||||
|
`system/hostname`), use it as an additional filter dimension, not just
|
||||||
|
full text.
|
||||||
|
|
||||||
|
## 4. Wikilinks and backlinks
|
||||||
|
|
||||||
|
`[[NoteName]]` points to another note. For relationships between
|
||||||
|
topics/entities, backlinks are often more informative than full-text
|
||||||
|
search:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Which notes link to "note-name"?
|
||||||
|
rg -n '\[\[note-name(\||\]\])' <vault>
|
||||||
|
|
||||||
|
# List all outgoing links of a specific note
|
||||||
|
rg -o '\[\[[^]]+\]\]' "<path-to-file>.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
For troubleshooting-style questions, it's often worth following 1–2 link
|
||||||
|
hops from the topically closest note (e.g. hardware note → linked
|
||||||
|
network note → linked incident note) instead of only matching full text
|
||||||
|
in isolation.
|
||||||
|
|
||||||
|
## 5. Large vaults
|
||||||
|
|
||||||
|
If there are too many matches: first narrow down to the most plausible
|
||||||
|
subfolder from the structure overview (step 1), then search in detail.
|
||||||
|
Don't read every matching file in full by default — check grep context
|
||||||
|
first (`rg -C 3 ...`) and only open whole files when genuinely needed.
|
||||||
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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "obsidian-vault-kb",
|
||||||
|
"description": "Reads an Obsidian vault as a knowledge base and uses its notes (Markdown files, frontmatter, tags, wikilinks, folder structure) to answer questions and help solve problems, regardless of the vault's subject matter.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Henner"
|
||||||
|
},
|
||||||
|
"keywords": ["obsidian", "knowledge-base", "notes", "vault", "markdown"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
description: Set up the obsidian-vault-kb skill (vault path, mode, minimal permissions whitelist)
|
||||||
|
argument-hint: [vault-path] [mode]
|
||||||
|
---
|
||||||
|
|
||||||
|
Set up the `obsidian-vault-kb` skill end to end. As part of the
|
||||||
|
`obsidian-vault-kb` plugin, this command is automatically namespaced by
|
||||||
|
Claude Code and invoked as `/obsidian-vault-kb:setup` — no manual filename
|
||||||
|
prefixing needed. Follow these steps in order.
|
||||||
|
|
||||||
|
## 1. Determine the skill's install directory
|
||||||
|
|
||||||
|
This plugin's `skills/obsidian-vault-kb/` is a symlink into the repo's
|
||||||
|
tool-neutral `skills/obsidian-vault-kb/` directory (the single source of
|
||||||
|
truth for `SKILL.md`, `scripts/`, and `references/`, shared across all
|
||||||
|
vendor adapters in this repo). Resolve it to its real, absolute,
|
||||||
|
symlink-free path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
realpath <plugin-dir>/skills/obsidian-vault-kb
|
||||||
|
```
|
||||||
|
|
||||||
|
This resolved path is `<SKILL_DIR>` for the rest of this setup — you'll
|
||||||
|
need it exactly for step 4. Use the real path (not the symlink path) so the
|
||||||
|
permissions whitelisted in step 4 keep working even if this plugin
|
||||||
|
directory is relocated relative to the shared `skills/` directory.
|
||||||
|
|
||||||
|
## 2. Gather vault path and mode
|
||||||
|
|
||||||
|
If `$ARGUMENTS` contains a vault path and/or mode, use those directly.
|
||||||
|
Otherwise ask the user:
|
||||||
|
|
||||||
|
1. Absolute path to the Obsidian vault (if there's more than one vault, ask
|
||||||
|
for a short name per vault too).
|
||||||
|
2. Mode: `read-only`, `append`, or `maintain` (as defined in SKILL.md —
|
||||||
|
explain briefly if the user is unsure which to pick).
|
||||||
|
|
||||||
|
Validate the path exists and is a directory before continuing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test -d "<vault-path>" && echo OK || echo "MISSING"
|
||||||
|
```
|
||||||
|
|
||||||
|
If missing, ask the user to correct it before proceeding.
|
||||||
|
|
||||||
|
## 3. Write the skill configuration
|
||||||
|
|
||||||
|
The config path is tool-neutral (not Claude-specific), so this same file
|
||||||
|
would be reused if another Agent-Skills-compatible tool ran this skill:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.agent-skills/obsidian-vault-kb
|
||||||
|
cat > ~/.agent-skills/obsidian-vault-kb/config.json << 'EOF'
|
||||||
|
{
|
||||||
|
"vaults": [
|
||||||
|
{"name": "<short-name>", "path": "<absolute-vault-path>"}
|
||||||
|
],
|
||||||
|
"mode": "<read-only|append|maintain>"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
cat ~/.agent-skills/obsidian-vault-kb/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm the written content with the user.
|
||||||
|
|
||||||
|
## 4. Generate and merge the minimal permissions whitelist
|
||||||
|
|
||||||
|
Read `permissions-whitelist.template.json` (next to this plugin's
|
||||||
|
`plugin.json`, i.e. `<plugin-dir>/permissions-whitelist.template.json`). It
|
||||||
|
whitelists exactly three commands, each pinned to its full absolute path —
|
||||||
|
no generic `Bash(find:*)`, `Bash(rg:*)`, `Bash(cat:*)`, etc., since those
|
||||||
|
would allow reading anywhere on the filesystem rather than just the vault:
|
||||||
|
|
||||||
|
- `Bash(<SKILL_DIR>/scripts/vault_index.sh:*)`
|
||||||
|
- `Bash(<SKILL_DIR>/scripts/vault_search.sh:*)`
|
||||||
|
- `Bash(<SKILL_DIR>/scripts/vault_backlinks.sh:*)`
|
||||||
|
- `Read(~/.agent-skills/obsidian-vault-kb/config.json)`
|
||||||
|
|
||||||
|
These three scripts resolve the vault path only from
|
||||||
|
`~/.agent-skills/obsidian-vault-kb/config.json` and refuse to operate on any
|
||||||
|
path outside it (see their source), so whitelisting them by exact path does
|
||||||
|
not grant broader filesystem access — not even to other files in the same
|
||||||
|
`scripts/` directory, since the wildcard is per-script-file, not per-folder.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Replace every `<SKILL_DIR>` placeholder in the template with the real,
|
||||||
|
symlink-resolved absolute path from step 1.
|
||||||
|
2. Locate the target settings file: prefer the project-level
|
||||||
|
`.claude/settings.json` if a project is open, otherwise the user-level
|
||||||
|
`~/.claude/settings.json`. Ask the user which one they want if unclear.
|
||||||
|
3. If the target file doesn't exist yet, create it containing just the
|
||||||
|
substituted `permissions.allow` array.
|
||||||
|
4. If it exists, read it first and merge: add only entries from the
|
||||||
|
substituted array that aren't already present in the target file's
|
||||||
|
`permissions.allow` array. Don't duplicate entries, don't remove or
|
||||||
|
overwrite unrelated existing permissions in the file.
|
||||||
|
5. Show the user exactly what was added (the four entries above with the
|
||||||
|
real path filled in) before writing, so they can see precisely what
|
||||||
|
they're approving.
|
||||||
|
|
||||||
|
Do **not** add anything beyond these four entries. In particular, do not
|
||||||
|
whitelist writing to the vault or to the config file — those keep prompting
|
||||||
|
for confirmation every time, by design, in `append`/`maintain` mode.
|
||||||
|
|
||||||
|
## 5. Make the helper scripts executable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x <SKILL_DIR>/scripts/vault_index.sh <SKILL_DIR>/scripts/vault_search.sh <SKILL_DIR>/scripts/vault_backlinks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Note this modifies the shared `skills/obsidian-vault-kb/scripts/` files in
|
||||||
|
the repo (via the resolved real path), not a plugin-local copy — that's
|
||||||
|
expected, since the symlink means there is only one copy on disk.
|
||||||
|
|
||||||
|
## 6. Confirm and offer a test run
|
||||||
|
|
||||||
|
Summarize to the user:
|
||||||
|
- Vault path(s) and mode that were configured
|
||||||
|
- The exact four permission entries that were added, and where (project vs.
|
||||||
|
user settings)
|
||||||
|
- That nothing else was whitelisted — every other command, and any write
|
||||||
|
into the vault, still prompts for confirmation
|
||||||
|
|
||||||
|
Then offer to run `<SKILL_DIR>/scripts/vault_index.sh` as a quick sanity
|
||||||
|
check, if the user wants to see it working right away.
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Template for this skill's Claude Code permissions. <SKILL_DIR> must be replaced with the absolute path where this plugin's skills/obsidian-vault-kb directory resolves to (i.e. the real, symlink-resolved path of skills/obsidian-vault-kb inside vendor/claude-code/plugins/obsidian-vault-kb/) before merging into settings.json. The /obsidian-vault-kb:setup command does this substitution automatically. Only the three read-only wrapper scripts are whitelisted, each pinned to its exact absolute path — no generic Bash(find:*), Bash(rg:*), Bash(cat:*), etc. The wrapper scripts themselves resolve the vault path only from ~/.agent-skills/obsidian-vault-kb/config.json and refuse to operate outside it, so whitelisting them does not grant filesystem access beyond the configured vault.",
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(<SKILL_DIR>/scripts/vault_index.sh:*)",
|
||||||
|
"Bash(<SKILL_DIR>/scripts/vault_search.sh:*)",
|
||||||
|
"Bash(<SKILL_DIR>/scripts/vault_backlinks.sh:*)",
|
||||||
|
"Read(~/.agent-skills/obsidian-vault-kb/config.json)"
|
||||||
|
],
|
||||||
|
"_deliberately_not_whitelisted": [
|
||||||
|
"No generic Bash(find:*), Bash(rg:*), Bash(grep:*), Bash(cat:*), Bash(sed:*), etc. — these would allow reading anywhere on the filesystem, not just the vault.",
|
||||||
|
"No write/append command targeting the vault or the config file — creating or editing config.json, and any 'append'/'maintain' mode writes into vault notes, keep prompting for confirmation each time by design.",
|
||||||
|
"No wildcard on the whole scripts/ directory — only the three specific script files are whitelisted, not the whole folder, so no other script could be dropped in and silently gain the same trust."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../../../skills/obsidian-vault-kb
|
||||||
Reference in New Issue
Block a user