Everything else here is local: skills on your machine, a vault on your disk.
This puts a vault behind a URL so an agent elsewhere can search and write it.
One container, one vault, one API key. The vault stays plain markdown on a
mounted volume, so Obsidian still opens it. The container does no LLM work --
search and packing delegate to the existing graphrag/context_pack modules, and
the caller's agent does the thinking.
- obsidian_wiki/server.py: four vault functions, five REST routes and four MCP
tools as transport over them. Auth is one bearer key; the process refuses to
start without WIKI_API_KEY unless WIKI_ALLOW_ANONYMOUS=1. Every path is
resolved inside the vault or refused.
- Dockerfile, docker-compose.yml, .dockerignore, and a ghcr push on v* tags.
- New [server] extra; the core wheel stays dependency-free.
- docs/deployment.md, plus the WIKI_* vars in configuration.md and .env.example.
Requires mcp>=2.0: 2.0 renamed FastMCP to MCPServer, and session_manager only
exists once streamable_http_app() has been called -- a mounted sub-app's own
lifespan is ignored, so it is wired into the FastAPI lifespan by hand.
* fix(tests): isolate CLI subprocess tests, and run the suite in CI
12 tests failed on a clean checkout. Neither was a product bug — both were
test-harness isolation gaps, and CI never caught them because the suite was
never run there.
1. Wrong package under test. The 18 test files that spawn
`python -m obsidian_wiki.cli` each build their own env from
os.environ.copy() and never set PYTHONPATH, so sys.path[0] was the
subprocess cwd. Any test running from a temp dir imported whatever
obsidian_wiki happened to be pip-installed instead of the working tree —
here a stale build predating `code-understand`, so all 8
test_code_understand_cli tests died at argparse with "invalid choice",
plus 4 in test_context_pack_cli. One autouse session fixture in
conftest.py now pins PYTHONPATH for every helper at once.
2. Developer .env leaking in. test_lint's `_run` and test_context_pack's
`run_cli` inherited pytest's cwd (the repo), so config resolution walked
up and found a developer's own untracked .env, pointing the run at their
real vault instead of the fixture. Both now run from the fake HOME.
Verified the fix isn't masking anything: sabotaging the config resolver makes
the relevant tests fail, and the suite passes in a venv with no obsidian_wiki
installed at all — so PYTHONPATH is genuinely doing the work.
Adds .github/workflows/tests.yml to run pytest on PRs and main. The workflows
only ran setup.sh, wheel publish, and the README-sync checker, so nothing
executed tests/ — the real reason this drifted.
Also drops the stale "RED by design / command does not exist yet" docstring
in test_code_understand_cli.py; the wiring landed in fd30720.
566 passed, 1 skipped (was 554 passed / 12 failed).
* fix(tests): make three test modules importable on Python 3.9
test_batch, test_graph_analysis and test_graphrag annotate helpers with
`int | None` / `list[str] | None` but lack `from __future__ import
annotations`, so 3.9 evaluates the union at import time and collection dies
with "unsupported operand type(s) for |". Every module under obsidian_wiki/
already has the future import; only these three tests were missing it.
Surfaced by the new pytest CI job — the 3.9 leg (the requires-python floor)
failed at collection while 3.12 passed.
* feat(config): resolve global config from an XDG-style location (#172)
The global config lived at ~/.obsidian-wiki, which isn't XDG-compliant.
It now resolves to $XDG_CONFIG_HOME/obsidian-wiki (default
~/.config/obsidian-wiki).
Existing installs are unaffected: if ~/.obsidian-wiki is present and the
XDG path is not, the legacy directory is used as-is. No migration is
required, and nothing moves on upgrade. Once the XDG path exists it wins,
so `mv ~/.obsidian-wiki ~/.config/obsidian-wiki` is all a migration takes.
Resolution is implemented identically in Python (cli.py), bash (setup.sh,
scripts/daily-update.sh, scripts/wiki-notify.sh), and the Config
Resolution Protocol that every skill defers to, so vault-scoped state and
named `config.<name>` profiles stay in one place.
* fix(config): stop setup from wiping user-added config keys
`write_config` rewrote the global config with only the three setup-managed
keys, so every `obsidian-wiki setup` — including the one a pip upgrade
prompts for — silently destroyed anything else in the file:
OBSIDIAN_LINK_FORMAT, QMD_WIKI_COLLECTION, WIKI_SKIP_PROJECTS, sync
settings, and comments.
Setup now owns only OBSIDIAN_VAULT_PATH, OBSIDIAN_WIKI_REPO and
OBSIDIAN_WIKI_VERSION, updating those in place and carrying every other
line over untouched. Duplicate definitions of a managed key collapse to
one.
Predates the XDG change (same behavior on main) but is fixed here because
this branch already touches config writing, and the XDG work makes
re-running setup more likely.
Also pins PYTHONPATH in both new test files so they exercise this
checkout rather than whatever obsidian_wiki happens to be installed.
The readme-sync workflow no longer fails when README.md and
README_TW.md don't change together. Instead, check_readme_sync.py
reports drift: it lists commits that touched README.md after the
last README_TW.md update, plus the pending English diff to translate
and backfill. CI surfaces drift as a warning annotation only.
Claude-Session: https://claude.ai/code/session_01PG9Pz6tcUfgpTHLjq3BT5Q
data-ingest was effectively wiki-ingest plus a format-detection
preamble — its own process step said "Follow the wiki-ingest skill's
process for creating/updating pages". The split forced users and the
skill router to disambiguate two overlapping ingest skills (a .md
ChatGPT export fit either one).
Fold it in to keep the skill surface small:
- wiki-ingest now handles structured docs AND raw/unstructured text
(chat exports, Slack/Discord JSON, CSV/JSON, transcripts, logs).
- Added an "Unstructured & conversational sources" section to Step 1
with format detection, chat-export shapes, and "distill substance,
not dialogue" guidance carried over from data-ingest.
- Expanded the description to absorb data-ingest's triggers.
- Removed .skills/data-ingest/ and repointed every reference
(CLAUDE.md/AGENTS.md, README, SETUP, llm-wiki, wiki-status,
wiki-rebuild, wiki-quick-chat-capture, and the mirror rule files
for cursor/windsurf/agent/kiro/copilot).
Adds contents:write permission and a gh release create step to the
publish job so that each v* tag push produces a GitHub Release with
the built wheel and sdist attached, alongside the existing PyPI publish.
* feat(pypi): package obsidian-wiki as a pip-installable distribution (#71)
Add a PyPI package so users can install without cloning the repo.
- pyproject.toml (hatchling, no runtime deps); force-include .skills/ and
bootstrap files into the wheel under obsidian_wiki/_data/ — repo root stays
the single source of truth. Force-include .skills/ into the sdist too, or
hatchling's VCS walker drops it (the committed agent symlink mirrors shadow
the real .skills/<name> files).
- obsidian_wiki CLI: Python port of setup.sh. `obsidian-wiki setup` writes
~/.obsidian-wiki/config and symlinks all skills into every supported agent
(--copy / --project / --project-only); plus `list` and `info`.
- .github/workflows/publish.yml: build + twine check + skill-bundle assertion,
publish to PyPI on v* tags via Trusted Publishing (OIDC).
- README + SETUP: pip install instructions; gitignore build artifacts.
* build(pypi): derive version from git tag via hatch-vcs; skip-existing on publish
Align the package version with the repo's CalVer release tags (v2026.05.2 ->
2026.5.2) so tagging a release is the version bump and future release tags
auto-publish the matching version.
* fix(setup): use relative symlinks for in-repo skill mirrors
setup.sh has been writing absolute symlinks (e.g. /Users/<name>/obsidian-wiki/.skills/...)
into the in-repo skill mirror directories (.agents/, .claude/, .cursor/, .kiro/,
.windsurf/), even though those mirrors are tracked in git as relative symlinks
(../../.skills/<name>). The result: every fresh clone is left with ~160 modified
files immediately after running setup.sh.
This is the same class of bug that 476ac88 ("complete relative symlink migration
across all mirrors and skills") fixed in the committed tree — the migration
updated the symlinks themselves but missed the installer that recreates them.
A contributor running `git add -A` after setup would silently re-commit their
home directory into the public repo, undoing 476ac88.
Add an optional third `mode` argument to install_skills(). When "relative" is
passed, the link target is hardcoded to ../../.skills/<name> (correct for every
in-repo mirror, all of which live at $REPO/<agent-dir>/skills). The global
target dirs under $HOME continue to use absolute paths, which is what they need.
Also corrects the post-install log line — it previously said "Installed global
skills" even for in-repo dirs.
Tested: on a clean checkout, `bash setup.sh && git status` now reports a clean
tree. In-repo links read ../../.skills/<name>; global links under ~/.codex,
~/.gemini, etc. remain absolute.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* setup: harden install_skills + add CI guard
Addresses review feedback on the in-repo symlink fix:
1. Compute the relative-path prefix from target_dir's depth under
$SCRIPT_DIR instead of hardcoding ../../. The old comment claimed
"in-repo mirrors are always two levels deep" — true today, silently
wrong the moment anyone adds a deeper target. Now correct for any
depth.
2. Validate the mode arg. A typo at the call site (e.g. "rleative")
previously fell through to absolute and reintroduced the original
bug. case-statement now exits with a clear message.
3. Guard against relative mode on out-of-repo targets. Bails rather
than emitting a wrong link.
4. Assert each new symlink resolves to a SKILL.md. Catches future
path-math mistakes at install time instead of when an agent later
fails to discover skills.
5. Fold the inline ~/.claude/skills loop into install_skills via a new
optional subset argument. Removes 17 lines of duplicated
delete-and-create logic; continues the consolidation that 9377be4
started.
6. Compress install_skills docstring from 8 lines to 4.
7. Add .github/workflows/setup.yml: runs setup.sh on every PR and
fails if it modifies a tracked file. Re-runs to verify idempotency.
This is the missing guard that let the absolute-symlink regression
reach main in the first place.
Tested locally:
- bad mode → exits 1 with clear error
- relative + /tmp target → exits 1 with clear error
- subset filter installs exactly the named skills
- 4-deep test target (_test_deep/a/b/skills) gets ../../../../.skills/...
- git status clean after install; re-run is idempotent
- ~/.claude/skills contains only wiki-update + wiki-query (absolute)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Tarek Rached <tarek@kisotechnology.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>