🌺 Jamgraf
A small, local, SQLite-backed wiki built for Human ↔ LLM collaboration — where Claude (via Claude Desktop over MCP) and you edit the same document at the same time, seeing each other's changes live.
Jamgraf used to be called Waikiki. If you're upgrading, there is nothing to do: the first launch moves your wikis, your Kahala sign-in and the app itself to the new name, and everything that still says the old one keeps working — your Claude Desktop config, doorman,
waikiki://links, all of it. If you had both an old Waikiki folder and a new Jamgraf one full of wikis, nothing is merged or moved; Manage wikis tells you where each is. The details, and what to never do (create a repo namedVerinFast/waikiki), are in docs/rename-to-jamgraf.md.
Requirements → how
| Requirement | How |
|---|---|
| Python + SQLite-backed | FastAPI app; one SQLite file per wiki (data/wikis/<slug>.db) holds its pages, images, FTS index, and vectors |
| Multiple isolated wikis | Separate DB per wiki; pages, search, and [[links]] never cross |
| Themes | Swappable CSS in jamgraf/static/themes/ (default / dark / sepia) |
| Images | Upload / paste / drag → stored in SQLite, served at /image/{id} |
| Extensible | Layered routes → store (repository) → db (SQLite); open-source deps |
| MCP + REST | jamgraf.mcp_server (FastMCP) and /api/* share one code path |
| Markdown → HTML | markdown-it-py (GFM), [[wiki links]], Pygments code |
| Tables | GFM tables via the gfm-like preset |
| Human editing | EasyMDE editor with live preview |
| Versioning | Every save snapshots to page_versions (human / ai / collab authored) |
| BM25 / RAG | SQLite FTS5 bm25() + sqlite-vec vectors, fused with RRF |
| Real-time co-editing (CRDT) | Yjs / pycrdt room per (wiki, page); browser + Claude edit live with presence |
| AI streaming | Claude writes into the live doc via MCP; plus an optional pull-model "Generate" button |
Multiple isolated wikis
Jamgraf hosts several fully isolated wikis (e.g. Beaconlight, Crosslake,
StartupOS) — each is its own SQLite file under data/wikis/. Pages, search, and
[[wiki links]] never cross between wikis: a link resolves only within the
active wiki's own database, so contamination is structurally impossible.
- Humans switch with the wiki dropdown in the header (a per-browser cookie).
- Claude has a separate active wiki, changed only with the MCP
switch_wikitool. Every content tool refuses to run until a wiki is chosen and echoes the wiki it acted on, so the AI can never silently cross wikis. - Each agent gets its own active wiki, scoped to its MCP session. Two agents can work in two different wikis at once without moving each other, and the choice is never written to disk — so a restarted server leaves an agent with no active wiki rather than inheriting whichever one another agent last picked.
These are all independent by design — the human browsing Crosslake doesn't move
Claude, one agent doesn't move another, and vice-versa. To co-edit, ask Claude to
switch_wiki to the one you're in.
Renaming a wiki
A wiki has a name and an address, and Rename… on the Wikis page offers them separately, because they are not the same kind of change.
The name is the label you read in the switcher and the header. Changing it
touches nothing else, which is what you want for a wiki that arrived from
somewhere else under a name you wouldn't have picked — one cloned from Kahala as
startupos that should read StartupOS.
The address is the wiki's identity: it is in every URL, it is the filename of
its database, and it is what Claude's switch_wiki takes. Changing it renames
the file, so links using the old address stop working and any tab with that wiki
open needs a reload. Jamgraf handles the rest — your browser follows the new
address, unsaved editor text is written out first, and an agent still holding the
old one is told to pick the wiki up again rather than being quietly moved into a
different wiki. The Help wiki keeps its address (the app re-creates help
whenever it's missing, so a renamed one would come back as a second copy); its
name is yours to change like any other.
Claude can change the name with the rename_wiki tool. The address is the app's to change, not Claude's: the live editors that have to be saved and closed first are held by the app itself, in a different process from the one Claude talks to.
Save / Open wikis to files
Because each wiki is a single self-contained SQLite file, you can Save one to a location you choose and Open an external wiki file back in — from the Wikis page (⚙ in the header):
- Save to file… writes a consistent
.wikisnapshot (safe even while the wiki is in use). In the desktop app this is a native Save dialog; in a browser it downloads the file. - Open wiki file… validates the file and brings it in as a new isolated wiki. Native Open dialog in the app; a file upload in a browser.
- Export .md downloads every page as a folder of markdown, and Open markdown folder… (desktop app) reads one back in.
.wiki files are just SQLite, so they're easy to back up, move between machines,
or share.
Open takes any shape Jamgraf can write. A .wiki save wraps a whole
database; a wiki bundle — what Push/Pull with Kahala moves, and what
Kahala's own export downloads — carries each page as an interchange snapshot and
no database at all; markdown is one .md per page and nothing else. They
are all zips (markdown can also be a plain folder), so Open reads what the file
is rather than what it is called. A bundle opens as a new wiki carrying its
pages, hierarchy, elements, templates and images, and is named after the wiki it
came from, not the file: a bundle labelled StartupOS opens as StartupOS,
however the download happened to be named. A bundle that can't be read is
refused in the bundle's own words and leaves nothing behind.
A folder of markdown comes back as a wiki. Each file's name is its page's
address, and the frontmatter header the export writes carries the page's title
and — for a sub-page — its parent, so the pages come back under the same names
with the same shape and their [[links]] still resolving. Tags and any other
frontmatter you wrote stay exactly as they are. Opening a folder makes a new
wiki; Claude can also read one straight back into a wiki you already have
(import_markdown), which is the loop worth knowing: export a wiki into a
repo's docs/, edit the text there with everything else, bring it back. A page
that comes back changed is updated, not replaced — the previous text is in its
history — nothing is ever deleted for being absent from the folder, and two
files that would claim the same page are refused before anything is written.
Markdown is text only, though: no images, history, comments, templates or
trash. It is the shape for a repo; a .wiki file or a bundle is the faithful
copy.
Anything Jamgraf can export, Jamgraf can open again. That's a rule, not a coincidence — an export you can't restore is a file that only looks like a backup. The one exception is PDF, which is a page rendered for a person to read rather than a copy of the wiki. The decision, the release this was learned from, and what a folder of markdown means on the way in are in docs/export-import-parity.md.
Opening a file also rebuilds the search index when the file arrived without a usable one. Search runs over a chunk index, not over the page text directly, so a file whose index didn't travel — or one embedded by a different model on the other machine — would otherwise open as a wiki that renders and links perfectly and finds nothing, with no error to explain it. A file that arrives correctly indexed is left alone, so opening a large healthy wiki is still instant.
History, trash & retention
- Version history: every save snapshots the page. Each article says when it last changed — Edited 3 minutes ago — 4 earlier versions you can go back to, under the tabs — and that line opens the page's History with the versions listed. (A page nobody has edited yet says so instead of pointing at an empty list.) The same place is reachable the long way round: Details → History. From there, view any version (with a diff vs current) and Restore it. Restoring is itself a save, so the text you just undid is still there. Retention keeps the last N versions per page (default 50; set per wiki in Settings, 0 = all).
- Trash (soft delete): deleting a page moves it to the Trash (header link),
hidden from lists and search but restorable. Restore brings it back;
Delete forever removes it permanently. Trashed pages are auto-purged after
N days (default 30; per wiki in Settings, 0 = never). Claude's
delete_pageis also soft; it haslist_trash/restore_pagetools. - Wiki stats: the Wikis page shows per-wiki article count, internal-link count (with broken — red-link — count), size on disk, and trash count.
Backups & restore
Jamgraf backs itself up automatically, on by default: every 24 hours it takes
a consistent snapshot of every wiki (SQLite's online backup API, so it's safe
mid-write) and keeps the last 7. They land in
<data>/backups/<YYYY-MM-DD_HHMM>/<slug>.db; the schedule, the count, and a
Back up now button are in Settings → Backups.
To restore: quit Jamgraf, then Manage wikis → Open on a .db from a
backup folder. It opens as a separate wiki, so you can compare before replacing
anything. Images and version history come with it — a snapshot is a complete
copy of the wiki.
These are local copies on the same disk as your wikis: insurance against
corruption, a bad import, or a mistake — not against losing the machine. Keep
a .wiki export somewhere else too. Backups only run while the app is open, so
days you never launched it have no snapshot.
If a wiki file is damaged, the app still starts and every other wiki keeps working — each wiki is a separate file, and one bad file is not allowed to take the app down with it. The broken wiki says so where you meet it: Manage wikis lists it as can't be read, with the reason, the backups folder, and the steps to open a snapshot; visiting one of its pages explains the same thing instead of showing an error. Nothing is deleted, rewritten or "repaired" — the damaged file is your data, and it may still be recoverable. The scheduled backup skips it and still snapshots everything else.
What survives a crash, a corrupt file, a failed import or a bad edit — and what doesn't — is audited end to end, with the experiments that established each answer, in docs/data-safety.md.
How the collaboration works
your browser <--y-websocket--> [ CRDT room (pycrdt) ] <--HTTP inject-- MCP server
(EasyMDE + Yjs, presence) | (Claude Desktop)
(debounced)
v
render HTML + snapshot to SQLite + RAG reindex
- Open a page's editor and you join a CRDT room; a colored presence chip shows who else is there.
- When Claude (through the MCP
append_to_page/replace_pagetools) writes, it lands in the same room and streams into your open editor live — and a "Claude" presence chip appears while it writes. - Concurrent edits merge (CRDT), so you don't overwrite each other.
- Edits are debounced-persisted to SQLite, re-rendered to HTML, and re-embedded for search.
The old in-editor ✦ Generate button is a separate convenience: it pulls a
one-off draft from the Anthropic API (needs ANTHROPIC_API_KEY). The real
collaboration path above uses no API key — the text comes from Claude over MCP.
Architecture — layers
Data access is layered so routes stay thin and all SQL lives behind one seam:
HTTP routes / MCP tools api.py, mcp_server.py parse + validate, call the repository
▼
Repository (data access) store.py, elements.py, wikis.py, rag.py owns the SQL
▼
Infrastructure db.py SQLite connection, schema, FTS5 + sqlite-vec
▼
SQLite file per wiki data/wikis/<slug>.db
Route handlers never open a cursor and never contain SQL — they parse the
request, call a repository function (store.get_page, store.parent_of,
store.get_setting, …), and shape the response. That invariant is guarded by
tests/test_repository_chokepoint.py. This is Phase 0 of
RFC 0001; see
docs/repository-layer.md for the full rationale and
the seam where multi-tenant scoping attaches later.
Typed page metadata (optional)
Frontmatter properties are plain strings, and stay plain strings. What a
template can declare is what those strings are supposed to mean — a few
lines of name[*]: type in the template editor (str, int, float, bool,
date, list[str], or a choice like player | npc), compiled to pydantic
checks in jamgraf/metaschema.py.
Pages made from such a template carry a template: property and are checked
against it on their Metadata tab and through MCP's get_metadata/set_metadata.
Checking warns; it never blocks. A wiki is a place for half-finished notes:
the write always lands, the value on disk is never rewritten (20 / 100 stays
20 / 100), and the mismatch is reported instead. A template that declares
nothing behaves exactly as it always did, and pages written before a schema
existed are untouched by it.
The Y.Doc is canonical
A page's content is a CRDT, not a string. The full encoded pycrdt Y.Doc for
every page is persisted in page_ydoc and is the source of truth; the
markdown/html columns are a projection kept alongside it for full-text
search, rendering, and RAG. Every content write goes through the repository and
advances the canonical Y.Doc, so the CRDT accumulates real history across saves
and restarts (the live co-editing room in collab.py is just an editing buffer
whose flush lands through the same seam). See jamgraf/ydoc.py.
One save is one transaction: the page row, its version snapshot, its tags and the canonical Y.Doc all land together or not at all, so a crash or a power cut cannot leave the truth a revision behind the text you can see. The search index is the one thing deliberately left outside — it embeds, which can be slow, and it is a cache that can be rebuilt from the markdown at any time, so it is refreshed just after the save commits rather than while it holds the write lock. The reasoning and the measurements are in docs/data-safety.md.
Because the persisted state is a genuine CRDT, Jamgraf can export a page as a
snapshot (full Y.Doc + a content-addressed image sidecar) or a changelog (updates
since a peer's state vector), and import the same from a peer — the local half
of the round-trip with the hosted platform. The format is the vendored,
version-pinned wiki_interchange library; it carries content
only (no tenancy or permissions — those are the server's), regenerates embeddings
locally on import, and rejects an incompatible spec/protocol version rather
than merging bad bytes.
A whole wiki travels as a bundle (store.export_wiki_bundle /
import_wiki_bundle): every page's Y.Doc, the page hierarchy by slug, the
manual order and starred flags, the custom elements, the templates with the
metadata schemas they declare, and one copy of each distinct image blob however
many pages embed it. It streams to and from a file — a real wiki is hundreds of
pages and tens of megabytes — and a malformed or version-incompatible bundle is
refused in full before the first local write, so a failure part-way through
leaves the wiki untouched rather than half-imported. Pages merge by slug:
existing ones are updated (and versioned) in place, and nothing local is deleted.
Hierarchy travels by slug because integer page ids are local — the same page is a
different number in every wiki.
Sync with Kahala
Kahala is the shared, server-side half of the same wiki. A local wiki can be linked to one there, then pushed up or pulled down; a remote wiki can be cloned into a new local one. It's all in the Kahala pane.
Only content travels — pages, hierarchy, elements, templates and images. Tenancy and permissions are the server's and are re-attached there; embeddings are regenerated on arrival rather than shipped.
Push and pull both merge, and neither deletes. A push adds and updates pages on Kahala and removes nothing there; a pull does the same here. So a page you deleted locally is still on Kahala afterwards, and the next pull brings it back. Removing a page from both is a deliberate act on both.
The wiki has to already exist on Kahala — a push won't create it, because creating one there is a browser form tied to your Kahala session rather than something the app's token can reach. Open Kahala… takes you to the page where you make one.
Only what changed travels — a few kilobytes rather than the whole wiki — and custom elements, templates and images travel with the pages, so a page never arrives referring to a definition that didn't. If the server is running an older version, or an image doesn't survive the trip, Jamgraf transfers the whole wiki instead and tells you that's what it did. Transfer the whole wiki forces it. If a transfer stops part-way — a full disk, say — it says it only partly merged rather than claiming it was refused: nothing is ever deleted, every page that arrived is an ordinary page with its history, and running the same transfer again finishes the job.
Signing in uses Kahala's own sign-in page — OpenID Connect with PKCE, no client secret, and the callback comes back to Jamgraf's own loopback port. In the desktop app it opens in your web browser rather than inside Jamgraf's window, so you get your usual session, your password manager and whatever second factor your organisation uses; the app notices by itself when you're done. The refresh token goes in your Keychain, never into a wiki file (that file is what "Save wiki" hands over) and never into a plain config file. Where there's no secure store, signing in is switched off and says so rather than falling back to writing the token somewhere less safe. The link itself — the address and the remote wiki's name — is recorded outside the wiki too, so it can't travel to someone you share the wiki with.
Someone who joins over LAN sharing can't reach any of it. An agent connected over MCP can push and pull, but can't link, clone or sign in — choosing where a wiki gets sent is yours.
Full detail, including every refusal and why redirects are never followed, in docs/kahala-sync.md.
Setup
cd ~/localdev/jamgraf
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
First run downloads the default embedding model (fastembed BGE-small, ONNX,
~130 MB) and warms it at startup. No PyTorch required.
Embedding models are managed in the UI — Settings → Embedding model → paste
a HuggingFace slug and click Add model. It downloads, becomes active, and all
pages are re-embedded (vector-dimension changes handled automatically). The
fastembed provider (ONNX, default) needs no PyTorch; the local provider
(any sentence-transformers slug) needs the optional requirements-local.txt.
Run the web app
python run.py
# → http://127.0.0.1:8787
Desktop app (macOS)
Jamgraf ships as a native .app (a WKWebView window over the local server) — see
Releases. Or build it yourself:
bash scripts/build_macos.sh
# → dist/Jamgraf.app (+ dist/Jamgraf-macos.zip)
The app is currently unsigned, so on first launch macOS Gatekeeper will warn: right-click the app → Open → Open to allow it (once). Signing/notarization is a later step.
The packaged app stores its data in ~/Library/Application Support/Jamgraf
(not inside the bundle), so it survives moving/replacing the app. Override with
JAMGRAF_DATA.
Updates
Settings → Updates checks GitHub for a newer release and can install it: the app backs up every wiki, downloads the release, verifies it, then quits and relaunches itself to finish. Your content lives outside the bundle, so an update replaces code only.
Because the app carries no Apple identity, it can't rely on macOS to tell a real release from a tampered one — so every release zip is Ed25519-signed and the app verifies it against a public key pinned at build time, before unpacking. Anything unsigned, wrongly signed, or modified is refused, and a build with no pinned key disables updating rather than trusting the download.
Checks are check-only and hourly at most; installing is always an explicit click,
since the swap restarts the app. Cutting a signed release is
./scripts/release.sh v0.14.0 — see docs/updates.md for
the trust model, key handling, and failure modes.
Capabilities and one-click fixes
Jamgraf shells out to tools it doesn't ship — the claude/gemini CLIs, an
image CLI, cloudflared, Ollama. Settings → Capabilities says which of them
this machine actually has, what each one powers, and what is missing, before
you press anything. Chat, ✦ Generate and the editor's ✨ image button render
greyed out and linked to that view when they can't work, instead of failing a
minute after the click.
Where something can be fixed, there's a button, not a command to copy — and
it offers the step that is actually possible: without npm, "install the Claude
Code CLI" becomes "install Node.js first", and with no Homebrew either it becomes
instructions with a link rather than a button that would fail. Installing asks
first, says what it is about to do, and reports honestly when it doesn't work
(npm i -g failing on permissions is common, and reads as exactly that).
Where nobody publishes an install path, Jamgraf says so and points at the setting you can change — it won't guess at a command. Doorman is listed but never offered as a fix: it is your app, and Jamgraf never installs or starts it. The one installer that pipes a download into a shell (Google's Antigravity CLI, the default image tool) names the host it fetches from before it runs. See docs/capabilities.md.
Doorman (optional)
If you also run Doorman, Jamgraf will notice and use it for things it does better:
- Speech — its voices for Listen and "Say this word".
- Generation, chat and images — the agents and image models you already configured there, instead of a second set of API keys and CLIs here. Whatever answers is named in the editor's status line, under the chat reply, and in Settings → Doorman, so you always know which model wrote something.
Each of those is capability-probed, not version-sniffed: an older Doorman without them simply keeps Jamgraf on its own Anthropic/Ollama/CLI path, silently.
It is optional in every direction: Jamgraf never starts or installs Doorman, everything works identically without it, and you can decline the integration even while Doorman is running (Settings → Doorman). The one exception is Jamgraf displayed inside Doorman's own window, where "optional" has no meaning — there the setting is shown locked, with the reason. See docs/doorman.md.
Deep links
jamgraf:// URLs open the app at a specific place, and survive the app picking a
different port (which an http://127.0.0.1:8787 link doesn't):
jamgraf://beaconlight/meru # a page
jamgraf://beaconlight/meru#abilities # a section
jamgraf://beaconlight # a wiki's front page
jamgraf://beaconlight?q=clockwork # search, inside that wiki
jamgraf:// # the front page
Page options → Copy link copies one, and MCP get_page returns the same
thing as link — so an agent can hand you something you can actually open.
The wiki is the authority, so there are no reserved verbs to shadow a wiki named
search or home. A URL scheme is an input anything on the machine can fire, and
the app window has owner rights, so parsing is a strict allow-list: every slug
validated, paths constructed rather than echoed, search always scoped to the
validated wiki. See
docs/deep-links.md. Deep links work in the packaged
.app only — a source run has no Info.plist for macOS to route through.
Calendar feeds
A calendar element can show events typed into the page, subscribe to a real
calendar, or both:
```calendar
ics: https://calendar.google.com/calendar/ical/.../basic.ics
tz: America/New_York
events: [{"title":"Troop 63 Campout","start":"2026-09-11","end":"2026-09-13","link":"troop-63-campout"}]
```
The ics address keeps itself current; the events list holds the entries that
link to pages in this wiki, which a subscription can't do. To add or change a
calendar, edit the ics: line — in Google Calendar the address is Settings →
your calendar → Integrate calendar → Secret address in iCal format. Nothing else
to configure.
Jamgraf fetches the calendar server-side because no provider allows a web page to
read one (there are no CORS headers on any of them), via
/api/calendar-feed?url=…. Since the page chooses that URL, the allow-list is
the only thing keeping this from being an open proxy: any web page in your
browser can call our loopback port, so the route accepts nothing but https at a
known calendar host — otherwise a website could aim it at your router or a
metadata endpoint. Note the trade: the address sits in page content, and a Google
secret address reads the whole calendar, so it is visible to anyone who can read
the page.
Recurring events are expanded properly — repeat rules, deleted occurrences, and single occurrences that were moved — and placed on the day they fall on where you are, so an 8pm event doesn't show up on tomorrow. If the calendar host can't be reached the element says so rather than rendering an empty month, which would claim nothing is scheduled. See docs/calendar-feeds.md.
Reports from agents
An agent that hits a bug or a limitation in Jamgraf can write it down with
report_bug while it still has the failing call in hand. Reports are queued,
not sent: they appear under Reports from agents (the menu entry shows up only
when something is waiting), and Open on GitHub opens GitHub's own new-issue
form pre-filled, so nothing is published until you submit it there.
That indirection is deliberate. Filing directly would mean Jamgraf holding a GitHub token — and, more to the point, a report written at the moment something failed often quotes the page it was working on. Reading it first is what keeps your wiki's content off a public tracker.
Connect Claude Desktop (MCP)
Easiest: open Jamgraf and click Connect Claude in the header (or visit
/help). That page shows a config pre-filled with this install's real paths
and a copy button — paste it into Claude Desktop's config and you're done.
The config file lives at
~/Library/Application Support/Claude/claude_desktop_config.json (create it if
it doesn't exist). Keep Jamgraf running, paste the config, then fully quit and
reopen Claude Desktop (⌘Q). Two forms:
A) Packaged app — Claude Desktop launches the .app itself in MCP mode
(no source checkout needed). Replace the path with where your app lives:
{
"mcpServers": {
"jamgraf": {
"command": "/Applications/Jamgraf.app/Contents/MacOS/Jamgraf",
"env": {
"JAMGRAF_MCP": "1",
"JAMGRAF_DATA": "/Users/YOU/Library/Application Support/Jamgraf",
"JAMGRAF_WEB_URL": "http://127.0.0.1:8787"
}
}
}
}
B) Running from source — launch the venv Python:
{
"mcpServers": {
"jamgraf": {
"command": "/Users/YOU/localdev/jamgraf/.venv/bin/python",
"args": ["-m", "jamgraf.mcp_server"],
"env": {
"PYTHONPATH": "/Users/YOU/localdev/jamgraf",
"JAMGRAF_DATA": "/Users/YOU/localdev/jamgraf/data",
"JAMGRAF_WEB_URL": "http://127.0.0.1:8787"
}
}
}
}
JAMGRAF_DATAmust match the data directory the running app uses, so the MCP server and the window share the same wikis. The in-app Help page fills this in for you.
Then, in Claude: "list the jamgraf wikis and switch to Beaconlight." Claude
must switch_wiki before it can read or write — this is what keeps wikis from
mixing. Open a page's editor and ask Claude to add a section; watch it type in
beside you.
MCP tools: list_wikis, current_wiki, switch_wiki, create_wiki (wiki
selection — required first), then list_pages (top-level by default, like the
sidebar; children=true for the whole wiki, children=["a-parent"] for one
branch — and it always reports how many sub-pages it withheld, so an agent never
reads silence as absence), get_page (returns a heading outline plus its
resolved outbound links — target, title, the label the reader sees, and
whether the page exists, plus a one-line hint asking the agent to read the
linked pages it is about to rely on), read_pages (the same payload for up to 10
slugs in one call, so following those links costs one round-trip instead of ten —
slugs that don't exist come back in missing rather than failing the batch, and
anything past the cap in dropped), create_page, and a family of merge-safe
live edit tools:
edit_page (find/replace — preferred), replace_section, insert_after /
insert_before, prepend_to_page, remove_from_page, append_to_page, and
replace_page (full rewrite, last resort). Plus changes_since (change feed),
backlinks, broken_links, delete_page (to trash), list_trash,
restore_page, and search (hybrid RAG) — all scoped to the active wiki.
Templates get the same read-before-you-write treatment: get_template returns a
template's markdown and metadata schema, and edit_template replaces one exact
snippet of it. Without those, changing a single line of a template meant
rebuilding the body from a page it produced and overwriting everything that page
didn't happen to show.
REST API
| Method | Path | |
|---|---|---|
| GET | /api/pages |
list |
| POST | /api/pages |
{title, markdown} |
| GET/PUT/DELETE | /api/pages/{slug} |
get / update / delete |
| GET | /api/search?q=&k= |
hybrid BM25+vector RAG |
| POST | /api/images |
multipart upload |
| POST | /api/collab/{slug}/append · /replace |
inject a live edit (used by MCP) |
| GET | /api/collab/{slug}/live |
current live (unsaved) markdown |
| POST | /api/ai/stream |
SSE token stream (pull-model Generate button) |
| GET | /api/calendar-feed?url=&tz= |
events from a subscribed calendar (allow-listed hosts only) |
Interactive docs at /docs. Websocket sync at ws://host/collab/{wiki}/{slug}.
REST/collab requests select the wiki via the X-Jamgraf-Wiki header (default:
the registry's default wiki).
Notes
- sqlite-vec loads via apsw (bundles a SQLite with loadable-extension support, which stock CPython often lacks). If unavailable, search degrades to BM25-only and everything else still works.
- Switching the embedder in Settings changes the vector dimension, so the vector index is rebuilt and all pages re-embedded automatically.
- Collaborative editing loads Yjs (
yjs,y-websocket,y-codemirror) from esm.sh and EasyMDE from unpkg — an internet connection is needed for the editor libs (they can be vendored locally later). The read view, REST, and MCP work fully offline.
License
Elastic License 2.0 — source-available, not open source.
You can read it, run it, modify it, and redistribute it. The one substantive restriction is that you may not offer Jamgraf to third parties as a hosted or managed service. Use it yourself, inside your company, on your own machines, freely.
The vendored wiki_interchange package (jamgraf/vendor/) is first-party and
covered by the same license — see docs/vendoring.md.
Metadata
Release files for jamgraf 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| jamgraf-1.1.0.tar.gz | 518.8 kB | With provenance | Details for jamgraf-1.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| jamgraf-1.1.0-py3-none-any.whl | Python 3 | none | any | Details for jamgraf-1.1.0-py3-none-any.whl |
Total release size: 888.8 kB
Release files / jamgraf-1.1.0.tar.gz
| Download URL | jamgraf-1.1.0.tar.gz |
|---|---|
| Size | 518.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c4e9e445346f014961ed9facdead46a9a493ae12db0296c461184cb4b0c2c6c6
|
|
BLAKE2b-256 checksum How to use checksums |
c1dc76e8698e1c949b44f9dedf570b15b4de13817327d78c07380b91aad8b444
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logRelease files / jamgraf-1.1.0-py3-none-any.whl
| Download URL | jamgraf-1.1.0-py3-none-any.whl |
|---|---|
| Size | 370.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
74fed073f20682711e40668be92f8e101920cfcd06f958b69a8bf72e700f654d
|
|
BLAKE2b-256 checksum How to use checksums |
43814fbd3eb7d1cd1c65d8e2a54bdf78ca00219a50b166f59ae95401773cd56f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logSecurity for jamgraf 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for jamgraf-1.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.