Skip to content

Latest commit

 

History

History
370 lines (288 loc) · 11.7 KB

File metadata and controls

370 lines (288 loc) · 11.7 KB

LLM Wiki API v1 — Endpoint reference

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>

GET /api/v1/health

Auth: not required.

{
  "ok": true,
  "status": "running",
  "version": "0.4.x",
  "enabled": true,
  "authRequired": true,
  "authConfigured": true,
  "allowUnauthenticated": false,
  "tokenSource": "store"
}

Field reference:

  • statusstarting / running / port_conflict / error
  • enabledfalse = user toggled the API off; all non-/health endpoints return 503
  • authRequiredfalse iff allowUnauthenticated: true
  • authConfiguredtrue if env LLM_WIKI_API_TOKEN or apiConfig.token is set
  • allowUnauthenticated — anonymous local-process mode (rare; user opt-in only)
  • tokenSourceenv / store / none. If env, the desktop UI token field is ignored.

GET /api/v1/projects

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

Resolving a user-spoken project name

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:

  1. GET /api/v1/projects → array of {id, name, path, current}
  2. Case-insensitive substring match on name:
    const matches = projects.filter(p =>
      p.name.toLowerCase().includes(spokenName.toLowerCase())
    )
  3. Handle the cardinality:
    • 0 matches → tell the user, list available names, ask. Don't silently fall back to current.
    • 1 match → use matches[0].id for all subsequent calls in this conversation.
    • 2+ matches → ask the user to disambiguate (show name + path for each).
  4. Cache the resolved id for 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 / encodeURIComponent all do this for you.
  • If you get 404, fall back to GET /api/v1/projects, find the project there, and use its id (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.


GET /api/v1/projects/{id}/files

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.


GET /api/v1/projects/{id}/files/content

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 exist
  • 413 — file > 2 MB
  • 415 — file is binary / non-UTF-8 (e.g., PNG, PDF). Use the desktop UI to view; the API is text-only.

POST /api/v1/projects/{id}/search

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
    }
  ]
}

Retrieval mode

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/610.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.

Per-result fields

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 ![alt](url) 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.


GET /api/v1/projects/{id}/graph

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.


POST /api/v1/projects/{id}/sources/rescan

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.


POST /api/v1/projects/{id}/chat

Returns 501. Chat / RAG pipeline lives in the WebView in v1. Don't invoke. Tell the user to use the desktop chat UI.


Limits & defenses

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.