Commit Graph

5 Commits

Author SHA1 Message Date
ar9av ee577fbd06 fix(server): bind loopback by default, WIKI_HOST to override
main() bound 0.0.0.0 unconditionally. That was defensible while the server
was an API for remote agents; now that /ui serves a browser console over the
whole vault, a bare `python -m obsidian_wiki.server` on a box with a public
address exposes it to the internet.

Default to 127.0.0.1 and read WIKI_HOST to override. A container has to
listen on every interface, so the Dockerfile now sets WIKI_HOST=0.0.0.0
explicitly — `docker run -p 8080:8080` behaves exactly as before.

Closes the loopback-by-default point raised in #208.
2026-09-09 02:26:33 -07:00
Arnav c8f29b3e84 feat(server): read-only operations console at /ui (#210)
* feat(server): read-only operations console at /ui

Addresses the Phase 1 scope of #208 — a browser view of vault operations —
without the parts that turned out not to be needed.

Three read-only routes, all derived from the vault at request time (no
database, no cached state):

  GET /v1/status   page/category counts, _raw and _staging depth, manifest
                   source freshness (flagging pages_produced entries that no
                   longer exist), recent changes, git branch/dirty/log
  GET /v1/health   run_doctor() + lint_vault(), called directly rather than
                   shelling out to the CLI and parsing its output
  GET /v1/staging  staged pages with a unified diff against the live page

plus GET /ui, a single static file with no build step and no dependencies.

The console shell carries no vault data: it fetches with the same bearer key
API clients use, held in sessionStorage. No cookie, so no CSRF surface.
Diffs come from difflib as plain text and are inserted with textContent, so
agent-authored markdown cannot become HTML — covered by a test that stages
a page containing a script tag and an onerror handler.

Staged promotion and discard are deliberately absent: that behavior lives in
.skills/wiki-stage-commit/SKILL.md as agent instructions, not in this package,
so a UI button would mean a second promotion workflow. The view links out to
the skill instead.

* fix(server): read every manifest container shape, not just "sources"

Running the console against a real vault surfaced an empty sources panel:
that vault's .manifest.json has no "sources" key at all. The manifest is
written by several skills and is genuinely polymorphic — wiki-ingest keys
entries under "sources", wiki-update under "projects", wiki-research under
"research_sessions" — and each names its page list differently
(pages_produced / pages_created / pages_in_vault).

Read all three containers, reusing cache._iter_entries for the dict-or-list
handling it already does.
2026-09-09 02:20:53 -07:00
bacca87 29e384cda2 fix(skills): parser-safe title/summary in canonical page template and server write_page (#207)
* fix(skills): parser-safe title/summary in canonical page template and server write_page

The canonical Page Template in llm-wiki/SKILL.md showed `title:` and
`summary:` as bare YAML scalars, and server.py write_page interpolated
them the same way. Free-text values containing ": " (colon + space),
"#", or quotes break YAML parsing - Obsidian then reports "Invalid
properties" and hides the frontmatter. Any ingest following the
canonical template can corrupt pages this way: one real vault ended up
with 25 unparseable pages from a single ingest of colon-heavy technical
summaries.

PR #3 already fixed wiki-update's own template with folded scalars; the
canonical template every other write skill follows (wiki-ingest: "Use
the page template from the llm-wiki skill") was left unguarded, and PR
#2 (quoted variant) was closed unmerged. PR #103's OKF export now
auto-repairs unquoted colons at read time - this fixes the source.

- llm-wiki/SKILL.md: Page Template (and the project-overview example)
  now emit title/summary as folded scalars (>-), with explicit
  parser-safety guidance matching wiki-update's merged wording.
- server.py write_page: emit title/summary via json.dumps - YAML 1.2 is
  a superset of JSON, so the emitted double-quoted scalar always parses,
  for any caller-supplied value.
- tests: regression test writes a page whose title/summary contain
  ": ", "#", and quotes and asserts both round-trip through a JSON
  parse of the emitted scalar; the existing round-trip assertion is
  updated to the quoted form.

* fix(server): emit folded scalars, not JSON, so the vault's own readers agree

json.dumps satisfies a YAML 1.2 parser, but _parse_frontmatter_values only
strips outer quotes — it never unescapes. A quoted title came back with
literal backslashes, and ensure_ascii=True turned every non-ASCII character
into a \uXXXX escape both on disk and in every downstream read.

A folded block needs no escaping, round-trips through pyyaml and the vault's
own parser alike, stays readable in the file, and matches the >- convention
this PR documents in llm-wiki/SKILL.md.

---------

Co-authored-by: Marco Baccarani <marco.baccarani@committa.it>
Co-authored-by: ar9av <ar9avg@gmail.com>
2026-09-05 14:05:40 -07:00
Amir Fathi 78ac04bddd fix: preserve reserved leading-underscore categories in write_page's slug
_slug() strips every leading non-alphanumeric character, so write_page's
rel-path build (`_slug(category)/_slug(title).md`) drops the underscore
from a reserved category like `_raw`. The four skip lists that exclude
unreviewed captures from graph analysis, lint, context packing and trust
scoring all key on the exact string `_raw` (and its siblings), so a page
written with category `_raw` lands in `raw/` instead and is silently
pulled into compiled-knowledge processing.

Add _category_slug(), which slugifies the category and prepends the
leading underscore when the caller's category started with one, and use
it wherever write_page builds the page path or its frontmatter.

Fixes #199
2026-08-30 14:22:49 +00:00
Arnav 52c9f2bae2 feat(server): run a vault as a Dockerized HTTP + MCP memory service (#185)
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.
2026-08-21 14:37:37 -07:00