From academic-wiki
Academic Wiki — persistent, compounding knowledge base for academic papers inside an Obsidian vault. Use when the user says "/academic-wiki:wiki", "wiki init", "wiki ingest", "wiki compile", "wiki query", "wiki lint", "wiki export-bibtex", "wiki snapshot", or asks about managing an academic knowledge base wiki.
How this skill is triggered — by the user, by Claude, or both
Slash command
/academic-wiki:wiki init <name> | ingest <path|id|url> | compile [<paper-id>] [--paper-only] | query <question> | lint | export-bibtex <selectors> | snapshot <label> | remove <name>init <name> | ingest <path|id|url> | compile [<paper-id>] [--paper-only] | query <question> | lint | export-bibtex <selectors> | snapshot <label> | remove <name>The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Persistent, compounding knowledge base for academic papers inside an Obsidian vault. Design spec: `docs/superpowers/specs/2026-04-16-academic-wiki-design.md`.
Persistent, compounding knowledge base for academic papers inside an Obsidian vault. Design spec: docs/superpowers/specs/2026-04-16-academic-wiki-design.md.
Walk up from cwd looking for a directory with both CLAUDE.md and a wiki/ subfolder (via academic_wiki_lib.wiki_paths.find_active_wiki). If none found, list ~/Documents/Obsidian Vault/03-Resources/*/wiki via academic_wiki_lib.wiki_paths.list_wikis and prompt. Default to academic/ if present.
All Python helpers live at ${CLAUDE_PLUGIN_ROOT}/scripts/academic_wiki_lib/. Invoke them through the cross-shell CLI shim — the same command line works in bash, zsh, PowerShell, and cmd:
# POSIX (bash/zsh)
export PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
PY="${HOME}/.venv/bin/python" # fall back to python3 if not present
"$PY" -m academic_wiki_lib.cli <subcommand> [args]
# Windows PowerShell
$env:PYTHONPATH = "$env:CLAUDE_PLUGIN_ROOT\scripts"
$PY = "$env:CLAUDE_PLUGIN_ROOT\.venv\Scripts\python.exe"
& $PY -m academic_wiki_lib.cli <subcommand> [args]
Available subcommands: acquire, release, source-sha, find-paper, paper-id. See scripts/academic_wiki_lib/cli.py for the full list and arguments. For helpers not yet covered by a subcommand, use inline Python on POSIX: invoke $PY with -c and from academic_wiki_lib import <module>; ... (Windows users open an issue or extend cli.py).
CLI entry points (used in Wave 3+):
${CLAUDE_PLUGIN_ROOT}/scripts/lint-wiki.py${CLAUDE_PLUGIN_ROOT}/scripts/bibtex-export.pyinit [<name>]Default name: academic.
Before running the steps below, set these shell variables and use them consistently (always quoted):
NAME="<name>" # the wiki name (default "academic")
WIKI_ROOT="$HOME/Documents/Obsidian Vault/03-Resources/$NAME"
Use "$NAME" and "$WIKI_ROOT" in all subsequent shell commands to handle names with spaces or special characters safely.
"$WIKI_ROOT". Abort if it already exists; suggest /academic-wiki:wiki remove <name> first.PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts" python3 -c \
"from academic_wiki_lib.templates import all_subdirs; import os, sys; \
base=sys.argv[1]; \
[os.makedirs(os.path.join(base, d), exist_ok=True) for d in all_subdirs()]" \
-- "$WIKI_ROOT"
git -C "$WIKI_ROOT" initCLAUDE.md:
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts" python3 -c \
"from academic_wiki_lib.templates import claude_md; import sys; \
sys.stdout.write(claude_md(sys.argv[1]))" \
-- "$NAME" > "$WIKI_ROOT/CLAUDE.md"
wiki/index.md, log.md, .gitignore, qmd.yml:
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts" python3 -c \
"from academic_wiki_lib.templates import INDEX_MD; import sys; \
sys.stdout.write(INDEX_MD.format(name=sys.argv[1]))" \
-- "$NAME" > "$WIKI_ROOT/wiki/index.md"
# Similarly for LOG_MD, GITIGNORE, qmd_yml(NAME)
.gitignore (if ~/Documents/Obsidian Vault/.git exists) to exclude 03-Resources/"$NAME"/:
VAULT="$HOME/Documents/Obsidian Vault"
if [[ -d "$VAULT/.git" ]]; then
LINE="03-Resources/$NAME/"
GITIGNORE="$VAULT/.gitignore"
touch "$GITIGNORE"
grep -Fxq "$LINE" "$GITIGNORE" || echo "$LINE" >> "$GITIGNORE"
else
echo "Note: ~/Documents/Obsidian Vault is not a git repo; skipped vault .gitignore update."
fi
git -C "$WIKI_ROOT" add .
git -C "$WIKI_ROOT" -c [email protected] -c user.name="academic-wiki" commit -m "init: $NAME wiki"
(Actual user.name/user.email should come from the user's global git config; use -c only as fallback if they're not configured.)${CLAUDE_PLUGIN_DATA}/node_modules/.bin/qmd:
QMD="${CLAUDE_PLUGIN_DATA}/node_modules/.bin/qmd"
if [[ -x "$QMD" ]]; then
env -u BUN_INSTALL "$QMD" collection add "$WIKI_ROOT/wiki" --name "$NAME"
env -u BUN_INSTALL "$QMD" embed --collection "$NAME"
fi
Obsidian Web Clipper setup:
1. Install: https://obsidian.md/clipper
2. Destination folder: 03-Resources/$NAME/raw/papers
3. Filename: {{date:YYYY-MM-DD}}-{{title}}
4. After clipping, run: /academic-wiki:wiki ingest <path>
Zotero BibTeX export (optional): File → Export → BibTeX → save to raw/bib/
ingest [<path|id|url>]Save a source to raw/ and assign a canonical paper-id. Does NOT create wiki pages — use compile for that.
ingest <path|id|url> — ingest a single source (file path, directory, arXiv ID, DOI, URL).ingest with no argument — scan raw/papers/*/ for unprocessed clipper directories and ingest each.When called with no argument:
raw/papers/*/ for directories containing ≥1 .md file.paper-id in the .md frontmatter, OR paper-id present but extract-status absent/not complete (crash recovery).paper-id is already written).Ingested N papers from raw/papers/.When the input is a directory (explicit path or from batch scan) containing .md + optional images/:
.md file inside the directory (the clipper writes exactly one).read_frontmatter() — do NOT overwrite user/clipper fields (title, doi, date, venue, etc.).paper-id (new no-hyphen format).raw/extracts/ and raw/papers/*/.write_frontmatter():
paper-idsource-shasource-type: clipper-mdsource-url — https://doi.org/<doi> if DOI present; else URL from clipper frontmatter if present; else nullextracted-at (ISO-8601 UTC)extract-status: completeextractor — set to obsidian-clipper UNLESS the existing frontmatter already has extractor: s2-stub (in which case preserve s2-stub so future audits can identify entries that were first cached by an S2 query rather than clipped by the user).ocr-used: falseextract-warnings: []images/ subdirectory exists, create a relative symlink: ln -sr "$WIKI_ROOT/raw/papers/<clipper-dir>/images" "$WIKI_ROOT/raw/figures/<paperid>" (relative so it survives vault relocation).![[fig1.png]]) in the body untouched — Obsidian resolves them vault-wide.raw/bib/<paperid>.bib as usual.Clipper differences from standard ingest:
raw/extracts/<paperid>.md is created — the clipper .md IS the extract.raw/papers/<paperid>.* copy — the clipper directory is the canonical source.Detect the active wiki (via academic_wiki_lib.wiki_paths.find_active_wiki from cwd, or default to ~/Documents/Obsidian Vault/03-Resources/academic):
PY=~/.venv/bin/python # fall back to python3 if not present
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
WIKI_ROOT="<active-wiki-path>"
Acquire lockfile:
"$PY" -m academic_wiki_lib.cli acquire "$WIKI_ROOT" --op ingest
If this fails with LockHeld, another operation is in progress — print the error and exit.
Route input: see references/ingestion-routing.md. Given the user's input string:
Compute source-sha:
"$PY" -m academic_wiki_lib.cli source-sha "<raw-source-path>"
Dedup pass 1 (byte-level): scan BOTH raw/extracts/*.md AND raw/papers/*/ clipper .md files for a matching source-sha field in frontmatter. If found, release lock, print This exact source was already ingested as <existing-paper-id>. Skipping. and exit.
Extract metadata from handler output + source content:
García → garcia)a/an/the and pure numerals){doi, arxiv, arxiv-version, url} — populate whichever are knownDedup pass 2 (identifier-level):
"$PY" -m academic_wiki_lib.cli find-paper "$WIKI_ROOT" --doi "<doi-or-empty>" --arxiv "<arxiv-or-empty>" --url "<url-or-empty>"
Compare extracted identifiers against every existing paper page's identifiers:. If any non-empty identifier matches (doi, arxiv ignoring version, url), the paper already exists:
paper-id.identifiers: frontmatter; add any identifiers from the incoming source that weren't already set. Write back the updated identifiers: to the paper page. Example: existing has {arxiv: "1706.03762"}, incoming provides {doi: "10.xxx/yyy"} — after merge, existing page has {arxiv: "1706.03762", doi: "10.xxx/yyy"}.source-version against the existing one. If different → proceed to step 8 (version handling). If same or unknown → skip saving a new version (the paper's already here with this source), release lock, log ingest: deduped <paper-id> (merged identifiers), and exit.If no identifier matches, the paper does not yet exist — proceed to step 7.
Generate paper-id (new paper):
"$PY" -m academic_wiki_lib.cli paper-id "$WIKI_ROOT" --lastname "Lastname" --year 2017 --title "Title Words"
Format: <lastname><year><firstword> (no separators). If already taken by a different paper (different identifiers), append 2, 3, ... directly (no separator) until unique. resolve_collision() scans both wiki/papers/*.md filenames AND paper-id values in clipper .md frontmatter under raw/papers/*/.
Handle versions (if dedup pass 2 matched an existing paper): if the incoming source-version differs from the existing paper's source-version, this is a new version of the same paper:
paper-id basename, with source-version: arxiv-v5 (or appropriate version) in the extract frontmatter.raw/extracts/<paper-id>.versions.yml listing every version ingested with its source-sha, extracted-at, and source-path.wiki/papers/<paper-id>.md identifiers.arxiv-version: to the most recent version.Metadata-extraction failure fallback — trigger if ANY of:
If the failure is partial (e.g., year known but author unknown), still use the fallback — do NOT synthesize a partial paper-id.
Fallback action:
paper-id = unknown<currentyear><filenameslug> (no separators)metadata-incomplete: true in the extract frontmatterSave files with <paper-id> as the consistent basename:
raw/papers/<paper-id>.pdf (or .html, .md, .tex depending on source type)raw/extracts/<paper-id>.md — the LLM-readable extract with full frontmatter (§3.7)raw/bib/<paper-id>.bib — either real or stubbedraw/figures/<paper-id>/ — if figures were extracted; else create empty directoryWrite extract frontmatter per spec §3.7. Required fields: paper-id, source-path, source-sha, source-version, source-type, source-url, extractor, extractor-version, extracted-at (ISO-8601 UTC), ocr-used, extract-status, extract-warnings.
BibTeX: if the handler provided BibTeX (arXiv/DOI/publisher handlers usually can), save it verbatim to raw/bib/<paper-id>.bib. Otherwise stub a minimal @misc entry from the extracted metadata, with a comment % bib-incomplete: true at the top (lint will flag it). The BibTeX @key field uses paper-id directly (e.g., vaswani2017attention).
Append to log.md:
## [YYYY-MM-DD] ingest | <paper-id>
New paper ingested from <source>.
Variants:
New version <source-version> of existing paper <paper-id>.Commit inside the wiki's own git repo:
git -C "$WIKI_ROOT" add .
git -C "$WIKI_ROOT" commit -m "ingest: <paper-id>"
Commit message variants:
ingest: <paper-id> (new version v5) — if step 8 was reachedingest: deduped <paper-id> — if step 4 matched (though execution exits there)Release lockfile (call explicitly after commit):
"$PY" -m academic_wiki_lib.cli release "$WIKI_ROOT"
Print confirmation:
Source saved with paper-id <paper-id>. Run /academic-wiki:wiki compile <paper-id> to integrate into the wiki.
If any step 2–14 fails after the lock is acquired, the skill MUST still release the lock. Call release explicitly on every error-exit path inside this command's step list:
"$PY" -m academic_wiki_lib.cli release "$WIKI_ROOT"
If the agent process itself crashes between acquire and release, the next mutating operation invokes the lockfile's stale-PID recovery (_is_alive(holder_pid) returns False on POSIX and Windows), warns once, and takes the lock cleanly. No bash trap is required for this — recovery is platform-agnostic.
compile [<paper-id>] [--paper-only]Default compile runs the full pipeline: paper pages + entity extraction + cites resolution + backlink audit + cross-paper candidate detection + index rebuild. --paper-only is an escape hatch that skips entity extraction through cross-paper detection (useful for a fast first pass on a new source). Venue pages under wiki/venues/<slug>.md are auto-created or updated for every paper whose extract frontmatter carries a venue: field — this happens in both modes.
For detailed behavior see references/compilation-guide.md.
<paper-id> is given: use the sequential path below (unchanged).<paper-id>:
a. Check for existing checkpoint via read_checkpoint().
status: in-progress: enter resume flow (see references/compilation-guide.md "Resume flow").find_all_extracts() + compare against wiki/papers/.references/compilation-guide.md "Batch compile mode").PY=~/.venv/bin/python
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
WIKI_ROOT="<active-wiki-path>"
Acquire lockfile (op=compile):
"$PY" -m academic_wiki_lib.cli acquire "$WIKI_ROOT" --op compile
Identify sources to compile:
find_all_extracts(wiki_root) from academic_wiki_lib.wiki_paths — it returns (paper_id, md_path) tuples from BOTH raw/extracts/*.md and raw/papers/*/ clipper directories, sorted alphabetically by paper_id.<paper-id> given: filter the list to that one.wiki/papers/<paper-id>.md exists AND its updated: frontmatter is ≥ the extract's extracted-at.All sources already compiled. Nothing to do. Release lock, exit.Per-source (all modes): for each (paper_id, md_path) tuple:
a. Read md_path via read_frontmatter — use the md_path from the tuple, do NOT reconstruct from paper-id (clipper extracts live under raw/papers/<dir>/, not raw/extracts/).
b. Read raw/notes/<paper-id>.md if it exists.
c. Check if wiki/papers/<paper-id>.md already exists (update case) — if yes, apply the update conflict policy (§3.6) during merge.
d. LLM generates body content: Metadata / Summary / Key Contributions / Methods / Results / Claims / User Notes / See Also sections.
e. LLM extracts references-raw: [...] from the bibliography section of the extract.
f. LLM infers field/*, subfield/*, method/* tags from the extract body.
ALWAYS add the deterministic tags from the extract frontmatter:
- year/<YYYY> — from the extract's year: or date: field (first 4-digit year found).
- venue/<slug> — where <slug> = academic_wiki_lib.slug.make_slug(<raw-venue-string>) from the extract's venue: field.
Record the slug (not the raw string) in the paper page's venue: frontmatter field.
g. Populate authors: as list of {slug: <author-slug>, name: <human-name>} objects. Generate each slug via academic_wiki_lib.slug.make_slug.
h. Write wiki/papers/<paper-id>.md via academic_wiki_lib.frontmatter.write_frontmatter.
i. Venue page upsert — after writing the paper page, create or update wiki/venues/<venue-slug>.md:
- If the extract has no venue: field (missing or empty/whitespace), skip this step entirely.
- Compute venue-type = academic_wiki_lib.templates.guess_venue_type(<raw-venue-string>).
- If wiki/venues/<venue-slug>.md does not exist: render via academic_wiki_lib.templates.venue_md_stub(slug=<venue-slug>, name=<raw-venue-string>, venue_type=<venue-type>, paper_ids=[<paper-id>], field_tags=<paper's field/* tags>, today=<YYYY-MM-DD>) and write the returned string to the file path (plain open(path, "w").write(...)).
- If it exists: read with academic_wiki_lib.frontmatter.read_frontmatter, append <paper-id> to papers: (dedup, preserve order), union field/* tags into tags: (dedup, preserve order), bump updated: to today. Do not change created:, name:, venue-type:, or slug: (the user may have corrected them). Write back with academic_wiki_lib.frontmatter.write_frontmatter.
- Runs in ALL modes (default AND --paper-only) — venue pages are cheap and belong with the paper write.
Entity extraction (skipped with --paper-only): scan the extract body for mentions of concepts, methods, and open problems. For each identified entity:
a. Generate a slug via academic_wiki_lib.slug.make_slug(<entity-name>).
b. Check if wiki/<entity-type>s/<slug>.md exists (where entity-type is concept, method, or open-problem).
c. If yes: apply the update conflict policy (§3.6).
d. If no: create using the appropriate §3 template. Populate sources: [<paper-id>], tags: inherited from the paper's tags, and status::
- concept: active
- method: active
- open-problem: open (override to resolved only if the paper explicitly resolves)
- result: preliminary
- claim: established
e. Add [[wikilinks]] to the new entity pages in the paper's Methods/Claims/Summary sections.
cites: resolution (skipped with --paper-only): for each entry in the paper's references-raw: [...]:
a. Fuzzy-match against existing paper pages by title + first-author + year.
b. If matched, append its paper-id to cites: [...].
c. Unmatched entries stay in references-raw: only and surface in lint's "candidate new ingests" list.
Backlink audit with ≥2-word slug allowlist (skipped with --paper-only) — prevents over-linking of common words:
a. For each newly-created entity-page slug:
bash rg -l -n --fixed-strings "<slug-with-hyphens-replaced-by-space>" "$WIKI_ROOT/wiki/"
b. Only insert [[<slug>]] if EITHER:
- The slug is ≥2 hyphen-separated words (e.g., attention-mechanism), OR
- The match appears in a noun phrase recognized as a proper named entity.
c. Skip insertion for single-word slugs like attention, method, training unless rule 6b.2 applies.
Cross-paper candidate detection (skipped with --paper-only, per references/promotion-rules.md): for each claim/result drafted in the paper page, search for semantically equivalent claims/results in other paper pages. When ≥1 equivalent found, append a candidate entry to outputs/reports/YYYY-MM-DD-promotion-candidates.md. Do NOT silently promote.
Update wiki/index.md:
field/* tag. Each paper gets listed under its primary field(s).--paper-only: append under a ## Uncategorized heading. Format: - [[<paper-id>]] — <title> (YYYY-MM-DD). Avoid duplicates.Example (full mode):
# academic Wiki Index
Last updated: YYYY-MM-DD
## field/wireless-comms
- [[vaswani2017attention]] — Attention Is All You Need (2026-04-16)
- [[chen20235g]] — 5G Networks Survey (2026-04-17)
## field/nlp
- [[vaswani2017attention]] — Attention Is All You Need (2026-04-16)
## concepts
- [[attention-mechanism]] — The attention mechanism (2026-04-16)
## methods
- [[rsma]] — Rate-Splitting Multiple Access (2026-04-17)
Append to log.md: ## [YYYY-MM-DD] compile | N paper pages created/updated with a body line listing the paper-ids.
Commit inside the wiki's own repo:
git -C "$WIKI_ROOT" add .
git -C "$WIKI_ROOT" commit -m "compile: <N> papers: <first-paper-id>, ..."
Use compile: paper-only <N> papers: ... when --paper-only was set.
Release lock (trap handles this on exit).
Batch mode replaces the per-paper loop with wave-based parallel subagents. Full orchestration logic is in references/compilation-guide.md "Batch compile mode". Summary:
outputs/.compile-checkpoint.yml.references/compilation-guide.md).run_in_background: true, model: "haiku", mode: "auto"). Each subagent receives a batch of {paper-id, extract-path} tuples. For full-tier batches, use references/batch-compile-full-prompt.md (interpolating {{PRE_BATCH_PAPERS}}, {{PRE_BATCH_SNAPSHOT_PATH}}, {{TODAY}}, plus the existing {{WIKI_ROOT}}, {{PAPER_LIST}}, {{PYTHONPATH}}); for paper-only, use references/batch-compile-prompt.md. Subagents write wiki/papers/, wiki/venues/, and — for full-tier — wiki/concepts/, wiki/methods/, wiki/open-problems/, and candidate entries under outputs/reports/.final-pass-status from pending to in-progress to ok.Batch mode honors the --paper-only flag the same way the sequential path does:
--paper-only: full-tier batch (paper pages + entity extraction + cites + backlinks + cross-paper candidates). See references/compilation-guide.md "Batch compile mode" for full orchestration.--paper-only: paper-only batch (paper + venue pages only).The tier: field on the checkpoint drives template selection on resume.
Per spec §3.6. Compile reaches this flow whenever a newly-ingested paper produces content that overlaps an existing wiki page — whether a paper page (re-compile scenario), a concept, method, open-problem, or cross-paper claim/result page.
academic_wiki_lib.frontmatter.read_frontmatter to get frontmatter + body.sources: (or to cites: for paper pages). Incorporate new content in a clearly attributed paragraph or section (e.g., "In (), the authors report...").> [!WARNING] Contradiction with [[other-paper-id]]
> <Paper A> claims X, but <Paper B> (cited above) claims Y. Needs resolution.
Never silently overwrite either side.paper-id in sources:. If an LLM cannot attribute a claim to a source, the claim is dropped OR marked status: stale.updated: frontmatter to today's date.created: — it reflects first creation, never re-bumps.aliases: [] list. Lint resolves [[old-slug]] via alias lookup.compile: merged <new-paper-id> into <N> existing pages (replaces the default compile: <N> papers: ... subject when merging into existing pages).query <question>Answer a question against the wiki's paper pages and entity pages (concepts, methods, etc.). Files the answer for future reuse.
PY=~/.venv/bin/python
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
WIKI_ROOT="<active-wiki-path>"
QMD="${CLAUDE_PLUGIN_DATA}/node_modules/.bin/qmd"
Query is mostly read-only. It only takes the lock if the user accepts the promotion prompt at the end.
Determine search backend:
qmd exists and is executable AND a qmd collection for this wiki exists → Phase 2 (qmd).Phase 1 search (index.md + ripgrep):
wiki/index.md in full. Identify candidate paper-ids by matching query keywords against entry titles/descriptions (LLM judgment — loose word match on meaningful nouns).wiki/papers/*.md:
rg -l -i -e "<noun1>" -e "<noun2>" "$WIKI_ROOT/wiki/papers/"
Collect the matching paper-ids.{path, score: 1.0, snippet: ripgrep_first_match_line, backend: "index+ripgrep"}.Phase 2 search (qmd, only when available):
env -u BUN_INSTALL "$QMD" query "<question>" --collection <wiki-name> --json
Parse JSON output for path/score/snippet per hit.
Read all candidate paper pages. Follow one level of wikilinks if the linked target is any existing wiki page (paper, concept, method, open-problem, result, claim).
Synthesize answer:
[[paper-id]] wikilinks as citations. Every factual claim must be attributed.marp: true frontmatter; save as .md that can be rendered via ${CLAUDE_PLUGIN_DATA}/node_modules/.bin/marp.File the answer to wiki/queries/<slug>.md (mandatory, no prompt). Use this frontmatter:
---
type: query-output
question: "<original question>"
status: filed
created: YYYY-MM-DD
updated: YYYY-MM-DD
sources: [<paper-id-1>, <paper-id-2>, ...] # all cited paper-ids
tags: [field/..., ...] # union of tags from sources
---
Slug: academic_wiki_lib.slug.make_slug(<question>) (truncated at 60 chars automatically).
Acquire the lockfile JUST BEFORE writing this file (query is mostly read-only; only the write needs a lock).
Prompt for promotion:
Promote this answer to a first-class page? Type one of:
- "concept", "method", "open-problem", "claim", "result" — promote as that type
- "no" — keep only in queries/
If the user accepts:
wiki/queries/<slug>.md to wiki/<type>s/<slug>.md (e.g., wiki/concepts/<slug>.md).type: <chosen-type>, status: promoted (for query-output, then convert to the target entity's schema), sources: stays.concept: Definition / Details / See Also / Counter-Arguments and Gaps).Append to log.md: ## [YYYY-MM-DD] query | <slug>. If promoted: also ## [YYYY-MM-DD] promote | <slug> to <type>.
Commit inside the wiki's own repo if anything was written:
git -C "$WIKI_ROOT" add .
git -C "$WIKI_ROOT" commit -m "query: <slug>"
Use promote: <slug> to <type> if promoted instead.
Release lockfile (if acquired in step 6).
score: 1.0 — no real ranking, LLM uses order of reading.lint [--fix-dead-links] [--suggest-backlinks] [--with-suggestions]Audit wiki integrity via a deterministic Python script plus opt-in LLM-assisted passes.
PY=~/.venv/bin/python
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
WIKI_ROOT="<active-wiki-path>"
SCRIPT="${CLAUDE_PLUGIN_ROOT}/scripts/lint-wiki.py"
Lint is read-only by default. Only the opt-in --fix-* passes acquire the lockfile.
Run deterministic checks:
"$PY" "$SCRIPT" "$WIKI_ROOT" \
> "$WIKI_ROOT/outputs/reports/$(date +%Y-%m-%d)-lint.md" \
2>&1
REPORT="$WIKI_ROOT/outputs/reports/$(date +%Y-%m-%d)-lint.md"
The script emits tagged lines like:
DEAD_LINK: [[foo]] in wiki/concepts/bar.md:12ALIAS_LINK: [[old]] in wiki/... resolves to [[new]] — consider rewritingORPHAN: wiki/concepts/foo.md has no inbound linksMISSING_FIELD: wiki/... lacks required field 'sources' for type 'concept'MISSING_FIELD_TAG: wiki/...MISSING_SECTION: ... lacks 'Counter-Arguments and Gaps'STALE: ...INVALID_CITES: ... cites unknown paper-id [...]MISSING_BIBTEX: ...INDEX_DRIFT: ...VERSION_DRIFT: ...EXTRACT_MISSING: ..., EXTRACT_FAILED: ...CONTRADICTION: ... has [!WARNING] calloutPARSE_ERROR: ... / EXTRACT_PARSE_ERROR: ...Opt-in pass --fix-dead-links: LLM creates stubs for each DEAD_LINK issue. Before running, acquire the lockfile:
"$PY" -m academic_wiki_lib.cli acquire "$WIKI_ROOT" --op lint
For each DEAD_LINK: [[foo]] in <file>:<line>:
<file> at <line> to get the surrounding context.method.concept.open-problem.concept.wiki/<entity-type>s/foo.md with frontmatter per §3 template and body placeholder text.sources: with whatever paper-ids appear in the same line or nearby paragraph.field/* inherited from the referring page's tags.lint: created <N> dead-link stubs.Opt-in pass --suggest-backlinks: LLM identifies pages that SHOULD link to existing entity pages but currently don't (based on textual mentions that didn't get wikilinked during compile).
outputs/reports/YYYY-MM-DD-backlink-suggestions.md.lint: suggested N backlinks (review required).Opt-in pass --with-suggestions: LLM reads the full lint report + a sampling of wiki content and adds a "Suggested Next Steps" section:
Append to log.md: ## [YYYY-MM-DD] lint | <N> issues found (if deterministic only), or ## [YYYY-MM-DD] lint | <N> issues, <M> fixed (if --fix-dead-links was used).
Commit the report file (deterministic checks always produce this):
git -C "$WIKI_ROOT" add outputs/reports/
git -C "$WIKI_ROOT" commit -m "lint: <YYYY-MM-DD> (<N> issues)"
Release lockfile (if acquired for a --fix-* pass).
The report at outputs/reports/YYYY-MM-DD-lint.md is plain text with one issue per line. LLM should summarize key findings to the user:
export-bibtex <selectors>Generate a consolidated .bib file for a subset of papers (e.g., all papers tagged with a research-project slug, ready to paste into LaTeX). See references/bibtex-handling.md for detail.
PY=~/.venv/bin/python
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
WIKI_ROOT="<active-wiki-path>"
SCRIPT="${CLAUDE_PLUGIN_ROOT}/scripts/bibtex-export.py"
Export acquires the lockfile during the output write step; commits the result.
At least one of --project, --field, --tag, --query, --keys, --since must be provided. Multiple selectors combine with AND semantics. --label <string> is optional; used verbatim (only filesystem-unsafe chars stripped).
Resolve --query (if provided) via search backend: the --query flag isn't handled by the CLI directly — SKILL layer resolves it first. Invoke query-style Phase 1/Phase 2 search against paper pages, collect the matching paper-ids, and pass them as --keys to the CLI instead. This makes --query transparently work through the same pipeline.
# Pseudo-code:
if [[ -n "$QUERY_TEXT" ]]; then
matched_pids=$(run_search_against_paper_pages "$QUERY_TEXT")
export_args="--keys ${matched_pids//$'\n'/,} $OTHER_ARGS"
else
export_args="$ORIGINAL_ARGS"
fi
Acquire lockfile:
"$PY" -m academic_wiki_lib.cli acquire "$WIKI_ROOT" --op export
Invoke the CLI:
"$PY" "$SCRIPT" "$WIKI_ROOT" $export_args
CLI writes outputs/bib/YYYY-MM-DD-<label>.bib and prints a summary:
Exported N papers to <path>M papers have bib-incomplete issues: (if any)No papers match the selector(s). or No usable BibTeX entries found for N selected papers.Append to log.md:
## [YYYY-MM-DD] export | <label> (<N> papers)
Commit inside the wiki's own repo:
git -C "$WIKI_ROOT" add outputs/bib/ log.md
git -C "$WIKI_ROOT" commit -m "export: <label> (<N> papers)"
Release lockfile (trap handles it).
outputs/bib/YYYY-MM-DD-<label>.bib is a plain BibTeX file. Paste into LaTeX or pass to a bib manager. Each entry has a % <paper-id> comment line immediately before the @type{key,...} — so you can map back to the wiki page if needed.
For a paper draft: /academic-wiki:wiki export-bibtex --project rsma-survey-2025
Tag every relevant wiki page with #project/rsma-survey-2025 as you write, then export at submission time.
For a field-scoped review: /academic-wiki:wiki export-bibtex --field nlp --since 2024-01-01
Only papers tagged field/nlp ingested since 2024-01-01.
For an ad-hoc set: /academic-wiki:wiki export-bibtex --keys vaswani2017attention,bahdanau2014neural --label icml-rebuttal
Explicit paper-id list + custom label.
--query vs --keys--query: LLM/search finds papers; you describe the topic in prose.--keys: you explicitly list paper-ids.Use --keys when you know exactly what to export; use --query when you want the wiki to help find relevant papers.
snapshot <label>Tag the wiki's state for reproducibility — e.g., the state when you submitted a paper. Operates on the wiki's own nested git repo so the tag captures ONLY wiki state, not unrelated Obsidian vault changes.
PY=~/.venv/bin/python
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
WIKI_ROOT="<active-wiki-path>"
LABEL="<user-supplied-label>"
Acquire lockfile:
"$PY" -m academic_wiki_lib.cli acquire "$WIKI_ROOT" --op snapshot
Validate label. Require a non-empty label. Git tag names cannot contain spaces, ~, ^, :, ?, *, [, \, or end in .lock. Sanitize by replacing whitespace with - and rejecting invalid characters:
if [[ -z "$LABEL" ]]; then
echo "Error: snapshot requires a <label> argument." >&2
exit 2
fi
# Sanitize: replace whitespace with hyphens
LABEL_SANITIZED=$(echo "$LABEL" | tr '[:space:]' '-')
# Reject labels with invalid git-tag characters
if [[ "$LABEL_SANITIZED" =~ [[:space:]~^:?*\[\\] ]] || [[ "$LABEL_SANITIZED" =~ \.lock$ ]]; then
echo "Error: label '$LABEL_SANITIZED' contains characters invalid in git tag names." >&2
exit 2
fi
Check for a tag collision. If snapshot/$LABEL_SANITIZED already exists, abort:
if git -C "$WIKI_ROOT" rev-parse --verify "snapshot/$LABEL_SANITIZED" >/dev/null 2>&1; then
echo "Error: tag snapshot/$LABEL_SANITIZED already exists." >&2
echo "Use a different label or delete the existing tag with: git -C $WIKI_ROOT tag -d snapshot/$LABEL_SANITIZED" >&2
exit 3
fi
Verify working tree is clean. Uncommitted changes would not be captured by the tag, so require a clean state:
status=$(git -C "$WIKI_ROOT" status --porcelain)
if [[ -n "$status" ]]; then
echo "Error: uncommitted changes in the wiki — commit or stash before snapshot." >&2
echo "$status" >&2
exit 4
fi
Append to log.md BEFORE tagging (so the log entry is included in the snapshot):
SHA=$(git -C "$WIKI_ROOT" rev-parse HEAD)
# Current HEAD sha before the log update
DATE=$(date +%Y-%m-%d)
cat >> "$WIKI_ROOT/log.md" <<EOF
## [$DATE] snapshot | $LABEL_SANITIZED
Tagged at $SHA
EOF
Commit the log update:
git -C "$WIKI_ROOT" add log.md
git -C "$WIKI_ROOT" commit -m "snapshot: $LABEL_SANITIZED"
NEW_SHA=$(git -C "$WIKI_ROOT" rev-parse HEAD)
Create the git tag (annotated, so the tag carries a message):
git -C "$WIKI_ROOT" tag -a "snapshot/$LABEL_SANITIZED" -m "snapshot: $LABEL_SANITIZED" "$NEW_SHA"
Release lockfile (trap handles it).
Print confirmation:
Tagged snapshot/$LABEL_SANITIZED at $NEW_SHA.
Revisit with: git -C "$WIKI_ROOT" checkout snapshot/$LABEL_SANITIZED
List all snapshots: git -C "$WIKI_ROOT" tag --list 'snapshot/*'
snapshot/*: keeps the tag space organized. If you later want other tag conventions (e.g., paper/* for paper-submission tags), they won't collide.-a creates an annotated tag with author/date/message metadata, which is richer than a lightweight tag and survives git push --follow-tags.log.md update is committed BEFORE tagging, so the snapshot includes its own log entry. Reading the wiki from the snapshot shows the snapshot itself as an event.# List all snapshots
git -C "$WIKI_ROOT" tag --list 'snapshot/*'
# Check out a snapshot (detached HEAD)
git -C "$WIKI_ROOT" checkout snapshot/icc-2026-submission
# Return to the current state
git -C "$WIKI_ROOT" checkout - # or `git checkout main/master/whatever-branch`
Rarely needed, but:
git -C "$WIKI_ROOT" tag -d snapshot/<label>
This removes only the tag, not the underlying commits. A tag deletion is NOT logged in log.md by the snapshot command — manually append a note if you care.
remove <name>Delete a wiki and its nested git repo after explicit confirmation. Destructive — all wiki content, commits, and tags are lost.
PY=~/.venv/bin/python
PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/scripts"
NAME="<name>"
WIKI_ROOT="$HOME/Documents/Obsidian Vault/03-Resources/$NAME"
QMD="${CLAUDE_PLUGIN_DATA}/node_modules/.bin/qmd"
Validate argument. Require a non-empty <name>. Reject names with path separators (/ or \) to prevent arbitrary path deletion:
if [[ -z "$NAME" ]]; then
echo "Error: remove requires a <name> argument." >&2
exit 2
fi
if [[ "$NAME" =~ [/\\] ]]; then
echo "Error: name must not contain path separators." >&2
exit 2
fi
Check the wiki exists:
if [[ ! -d "$WIKI_ROOT" ]]; then
echo "Error: wiki '$NAME' does not exist at $WIKI_ROOT." >&2
exit 3
fi
Show what will be deleted:
echo "About to delete wiki '$NAME' at $WIKI_ROOT."
echo "Contents:"
ls "$WIKI_ROOT/" 2>/dev/null | head -20
COMMIT_COUNT=$(git -C "$WIKI_ROOT" rev-list --count HEAD 2>/dev/null || echo "0")
TAG_COUNT=$(git -C "$WIKI_ROOT" tag | wc -l | tr -d ' ')
echo "History: $COMMIT_COUNT commits, $TAG_COUNT tags (ALL will be lost)."
Explicit confirmation prompt. The LLM must ask the user to type the wiki name as confirmation:
This will PERMANENTLY delete wiki '$NAME' and its git history at $WIKI_ROOT.
Type the wiki name exactly to confirm (or anything else to cancel):
If the user types anything other than the exact $NAME, abort:
if [[ "$user_response" != "$NAME" ]]; then
echo "Cancelled — name did not match."
exit 0
fi
Acquire the lockfile to prevent concurrent mutation during removal:
"$PY" -m academic_wiki_lib.cli acquire "$WIKI_ROOT" --op remove
# NO release needed — the lock file goes away with the directory.
Remove the qmd collection if installed:
if [[ -x "$QMD" ]]; then
env -u BUN_INSTALL "$QMD" collection remove "$NAME" 2>/dev/null || true
fi
Remove the directory and its nested git repo entirely:
rm -rf "$WIKI_ROOT"
Update the Obsidian vault's .gitignore if present — remove the entry for this wiki:
VAULT="$HOME/Documents/Obsidian Vault"
GITIGNORE="$VAULT/.gitignore"
if [[ -f "$GITIGNORE" ]]; then
# Remove any line matching `03-Resources/$NAME/` exactly
python3 -c "
import sys
path = '$GITIGNORE'
entry = '03-Resources/$NAME/'
with open(path) as f:
lines = f.readlines()
lines = [l for l in lines if l.rstrip() != entry]
with open(path, 'w') as f:
f.writelines(lines)
"
fi
If the vault is itself a git repo, commit the removal:
VAULT="$HOME/Documents/Obsidian Vault"
if [[ -d "$VAULT/.git" ]]; then
(
cd "$VAULT"
git add -u || true # Track the removal
git commit -m "remove: $NAME wiki" 2>/dev/null || true
)
fi
Print confirmation:
Wiki '$NAME' removed.
-rf is the right flag — the directory includes its own .git/ subdirectory and we want both gone.ingest/compile/etc. from racing the deletion and writing to a half-gone directory.init command added the entry; remove removes it for symmetry.If the deletion was accidental AND the vault is a git repo:
cd ~/Documents/"Obsidian Vault"
git reflog # find the pre-removal commit
git checkout <pre-remove-sha> -- "03-Resources/$NAME/"
If the wiki was NOT under the vault's git (typical — the nested repo was self-contained), recovery requires external backups. The plugin has no built-in undo.
Any wiki page can be exported as a slide deck using Marp (Markdown-based presentations).
Marp CLI is installed automatically by the SessionStart hook to:
${CLAUDE_PLUGIN_DATA}/node_modules/.bin/marp
If it's missing, install manually:
cd "${CLAUDE_PLUGIN_DATA}" && npm install @marp-team/marp-cli
Edit any wiki page (e.g., a concept page) and add these fields to the existing frontmatter:
---
...existing frontmatter...
marp: true
theme: default
paginate: true
---
Then structure the body with --- separators between slides:
# Slide 1 title
Content goes here.
---
# Slide 2 title
- bullet
- another bullet
---
# Slide 3
Summary.
Export to HTML (browser-renderable):
MARP="${CLAUDE_PLUGIN_DATA}/node_modules/.bin/marp"
"$MARP" "${WIKI_ROOT}/wiki/concepts/my-concept.md" -o output.html
Export to PDF (requires Chromium; first run downloads it):
"$MARP" --pdf "${WIKI_ROOT}/wiki/concepts/my-concept.md" -o output.pdf
Export to PPTX:
"$MARP" --pptx "${WIKI_ROOT}/wiki/concepts/my-concept.md" -o output.pptx
When a /academic-wiki:wiki query question contains "slides", the synthesized answer gets marp: true frontmatter AND is rendered to HTML in the same directory as the query-output file:
/academic-wiki:wiki query "Give me slides on the key insights of attention mechanisms"
produces:
wiki/queries/give-me-slides-on-the-key-insights-of-attention-mechanisms.md ← source
wiki/queries/give-me-slides-on-the-key-insights-of-attention-mechanisms.html ← rendered
Users can then open <path>.html to view the deck in a browser.
The Obsidian Marp plugin (separate install — search in Community Plugins for "Marp") gives you:
marp: true pages.Both ways of rendering (Obsidian plugin OR marp-cli CLI) produce equivalent output. Use whichever fits your workflow.
theme: default is the built-in theme. You can also use theme: gaia or theme: uncover. Custom themes require a CSS file — see Marp docs.$...$ (inline) or $$...$$ (display). The wiki uses the same convention, so math in wiki pages carries over directly. works if you use paths relative to the wiki root. Obsidian's embed syntax ![[fig-1.png]] does NOT render in Marp — use standard markdown image syntax.paginate: true shows slide numbers. Skip for title/cover slides via _paginate: false in a per-slide directive.Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
Applies a firm's KYC/AML rules grid to parsed onboarding records: assigns risk rating, checks required documents, outputs rule outcomes with citations, and routes for escalation.
Generates daily or weekly digests of activity from connected sources (chat, email, docs, tasks, CRM), highlighting action items, decisions, mentions, and project updates.
npx claudepluginhub tung491/academic-wiki --plugin academic-wiki