Base URL: http://127.0.0.1:19828
Prefix: /api/v1
Default content type: application/json; charset=utf-8
All non-/health endpoints require auth via one of:
Authorization: Bearer <token>
X-LLM-Wiki-Token: <token>
?token=<urlencoded-token>
Auth: not required.
{
"ok": true,
"status": "running",
"version": "0.4.x",
"enabled": true,
"authRequired": true,
"authConfigured": true,
"allowUnauthenticated": false,
"tokenSource": "store"
}Field reference:
status—starting/running/port_conflict/errorenabled—false= user toggled the API off; all non-/healthendpoints return 503authRequired—falseiffallowUnauthenticated: trueauthConfigured—trueif envLLM_WIKI_API_TOKENorapiConfig.tokenis setallowUnauthenticated— anonymous local-process mode (rare; user opt-in only)tokenSource—env/store/none. Ifenv, the desktop UI token field is ignored.
Auth: required.
{
"ok": true,
"projects": [
{
"id": "a0e90b29-fcf3-4364-9502-8bd1272de820",
"name": "Research Notes",
"path": "/Users/me/wiki-projects/research",
"current": true
},
{
"id": "...",
"name": "Reading",
"path": "/Users/me/wiki-projects/reading",
"current": false
}
]
}The current flag marks the project that is currently open in the desktop UI. Use it when the user doesn't name a project explicitly.
{id} placeholder in every project-scoped endpoint accepts:
- the project UUID (
p.id) - the project filesystem path (
p.path, URL-encoded) - the literal string
current
There is no ?name= filter on this endpoint and {id} does not accept a name directly — names are resolved entirely client-side after listing all projects.
Algorithm:
GET /api/v1/projects→ array of{id, name, path, current}- Case-insensitive substring match on
name:const matches = projects.filter(p => p.name.toLowerCase().includes(spokenName.toLowerCase()) )
- Handle the cardinality:
- 0 matches → tell the user, list available names, ask. Don't silently fall back to
current. - 1 match → use
matches[0].idfor all subsequent calls in this conversation. - 2+ matches → ask the user to disambiguate (show
name+pathfor each).
- 0 matches → tell the user, list available names, ask. Don't silently fall back to
- Cache the resolved
idfor the rest of the conversation. Only re-list when the user switches contexts.
If the user gives a filesystem path verbatim, you can URL-encode it and pass it as {id} directly — no list lookup needed:
# macOS / Linux (bash / zsh)
PROJECT_PATH=$(printf %s "/Users/me/wiki/reading" | jq -sRr @uri)
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/projects/$PROJECT_PATH/files?root=wiki"# Windows (PowerShell)
$projectPath = [System.Uri]::EscapeDataString("C:/Users/me/wiki/reading")
curl.exe -s -H "Authorization: Bearer $env:LLM_WIKI_API_TOKEN" `
"$env:BASE/api/v1/projects/$projectPath/files?root=wiki"Windows path gotchas — match the form the desktop app stored, otherwise you'll get 404:
- Use forward slashes (
C:/Users/me/wiki), not backslashes. The desktop app normalizes paths to forward slashes before saving. - Preserve the case the user actually has on disk (
C:/Users/Me/...≠c:/users/me/...for the API's string compare, even though Windows itself is case-insensitive). - The colon after the drive letter must be percent-encoded (
%3A) — it's a reserved URI delimiter.EscapeDataString/jq @uri/encodeURIComponentall do this for you. - If you get 404, fall back to
GET /api/v1/projects, find the project there, and use itsid(UUID) — UUIDs are platform-agnostic and never need encoding.
If the path isn't registered in the desktop app, you'll get 404 — fall back to listing and asking.
Auth: required.
Query params:
| Param | Default | Notes |
|---|---|---|
root |
wiki |
One of wiki / sources (alias raw, raw/sources) / all. all lists every public sub-tree (purpose.md, schema.md, wiki/, raw/sources/). |
recursive |
true |
false → only one level. |
maxFiles |
2000 |
Clamped to [1, 10000]. Exceed → 413. |
Response:
{
"ok": true,
"projectId": "...",
"root": "wiki",
"files": [
{
"name": "concepts",
"path": "wiki/concepts",
"isDir": true,
"size": null,
"children": [
{
"name": "rope.md",
"path": "wiki/concepts/rope.md",
"isDir": false,
"size": 4321,
"children": null
}
]
}
],
"truncated": false
}Hidden files (dotfiles) and symlinks are silently skipped.
Auth: required.
Query params:
| Param | Required | Notes |
|---|---|---|
path |
yes | Project-relative. Allow-list: purpose.md, schema.md, wiki/**, raw/sources/**. No dotfile segments. No ... |
Response:
{
"ok": true,
"projectId": "...",
"path": "wiki/concepts/rope.md",
"content": "---\ntitle: RoPE\n---\n\n# Rotary Position Embedding\n..."
}Failure modes:
403— path outside the allow-list (e.g.,../app-state.json,.llm-wiki/foo.json)404— file does not exist413— file > 2 MB415— file is binary / non-UTF-8 (e.g., PNG, PDF). Use the desktop UI to view; the API is text-only.
Auth: required.
Body:
{
"query": "rope rotary position embedding",
"topK": 10,
"includeContent": false,
"queryEmbedding": null
}| Field | Default | Notes |
|---|---|---|
query |
(required) | Non-empty. Empty / whitespace-only → 400. |
topK |
10 |
Clamped to [1, 50]. |
includeContent |
false |
When true, each hit carries content (full markdown). Skip the per-page content fetch round-trip. |
queryEmbedding |
null |
Optional number[]. If you precomputed a query embedding (your own model, batched offline), pass it here and the server skips its own embed call. Must be a non-empty array of finite numbers, otherwise → 400. |
Response:
{
"ok": true,
"projectId": "...",
"mode": "hybrid",
"tokenHits": 78,
"vectorHits": 14,
"note": "Search uses the shared backend retrieval service. When embeddingConfig is enabled, the API automatically includes LanceDB vector results; clients may also pass queryEmbedding explicitly.",
"results": [
{
"path": "wiki/concepts/rope.md",
"title": "Rotary Position Embedding",
"snippet": "...inject positional information by rotating Q and K...",
"titleMatch": true,
"score": 0.0315136476426799,
"vectorScore": 0.94,
"images": [
{ "url": "wiki/media/rope-diagram.png", "alt": "RoPE rotation diagram" }
],
"content": null
}
]
}The server picks the mode automatically based on whether the active project has embeddings configured (Settings → Embeddings) and whether the vector index for the project has data:
mode |
Trigger | Score scale |
|---|---|---|
"keyword" |
No embeddingConfig, OR embedding fetch failed, OR vector index empty. |
Additive keyword score: filename-exact ~200, phrase-in-title ~50+, token-bag scoring in single digits. |
"vector" |
Vector index returned hits but keyword scoring matched nothing. Rare in practice. | RRF rank score, typically 1 / (60 + rank) ≈ 0.015–0.017. |
"hybrid" |
Both keyword and vector pipelines produced hits — the common case when embeddings are enabled. | RRF combined: up to 1/61 + 1/61 ≈ 0.0328 for a top hit. |
tokenHits is the number of pages the keyword pass scored; vectorHits is the number of distinct pages LanceDB returned. Either can be 0.
| Field | Always present? | Notes |
|---|---|---|
path |
yes | Project-relative path to the markdown page. |
title |
yes | Front-matter title: if present; else first # Heading; else filename with dashes → spaces. |
snippet |
yes | ~160-char window. In keyword mode: centered on the query/anchor token in the page body. In vector-only matches: the actual matching chunk text, optionally prefixed with the chunk's heading path (e.g. "Section > Detail: chunk text..."). |
titleMatch |
yes | true when a token or phrase hit the title (boosts ranking). |
score |
yes | Final ranking score. See "Retrieval mode" for scale. |
vectorScore |
optional | Raw vector similarity (≈ cosine 0–1) when the page matched via the vector index. Useful for "how strong was the semantic match" decisions. Absent on pure keyword hits. |
images |
yes | Embedded  references discovered in the markdown, deduped by URL. Useful for the agent to surface diagrams. |
content |
optional | Full markdown, only when includeContent: true. |
results is sorted descending by score. Don't compare scores across modes (keyword scores are 100×+ larger than RRF scores by construction). Rely on relative ordering within one response.
Auth: required.
Query params:
| Param | Default | Notes |
|---|---|---|
q |
— | Substring filter on id or label, case-insensitive. |
nodeType |
— | Filter on frontmatter type: (e.g., entity, concept, query, other). |
limit |
200 |
Clamped to [1, 1000]. |
Response:
{
"ok": true,
"projectId": "...",
"nodes": [
{
"id": "rope",
"label": "Rotary Position Embedding",
"nodeType": "concept",
"path": "wiki/concepts/rope.md",
"linkCount": 4
}
],
"edges": [
{
"source": "rope",
"target": "attention",
"weight": 1.0
}
]
}Edges are derived from [[wikilink]] references inside wiki/*.md. Deduplicated by unordered pair (source, target) — [[a]] in b and [[b]] in a produce one edge. Self-edges are dropped. weight is 1.0 in v1.
linkCount is the node's degree in the deduped graph.
Auth: required. Mutates state.
No body required.
{
"ok": true,
"projectId": "...",
"result": {
"queue": {
"version": 1,
"tasks": []
},
"changedTasks": [
{
"id": "...",
"path": "raw/sources/new-paper.pdf",
"kind": "created"
}
]
}
}changedTasks contains the files this rescan actually detected as changed (created / modified / deleted). The downstream ingest queue picks these up asynchronously — the API call returns as soon as the diff is queued, not when ingest finishes.
The user's sourceWatchConfig (file type filters, exclude dirs, max size) is honored. If the user disabled auto-ingest, files are still detected but not queued for the LLM pipeline.
Returns 501. Chat / RAG pipeline lives in the WebView in v1. Don't invoke. Tell the user to use the desktop chat UI.
| Limit | Value | Effect |
|---|---|---|
| Body size | 1 MiB | Exceed → 400. |
| File content read | 2 MiB | Exceed → 413. |
| File-tree node count | 10000 hard cap | Exceed → 413. |
Search topK |
50 max | Silently clamped. |
Graph limit |
1000 max | Silently clamped. |
| Rate limit | 120 req/sec (global) | 429 with Retry-After semantics implied (back off ≥1s). |
| In-flight requests | 64 concurrent | 503 "API server is busy" — back off ≥2s. |
CORS: Access-Control-Allow-Origin: *. Preflight cached 10 min via Access-Control-Max-Age: 600. Allowed headers include Authorization, X-LLM-Wiki-Token, Content-Type.
Non-GET / non-POST methods → 405.