
Llm Wiki
- 1.3k installs
- 127 repo stars
- Updated May 19, 2026
- nashsu/llm_wiki_skill
llm-wiki is an agent skill that securely queries and retrieves information from a personal LLM Wiki knowledge base for developers who keep project notes in the LLM Wiki desktop app during coding sessions.
About
llm-wiki is an agent skill that connects coding agents to the LLM Wiki desktop application at 127.0.0.1:19828, explicitly not Obsidian, Notion, Apple Notes, or Logseq. The skill triggers only when users name LLM Wiki, say my wiki or knowledge base, or ask to read wiki pages, show the knowledge graph, search a wiki project, or rescan wiki sources. Capabilities include wiki page search, file listing, content reads, knowledge graph inspection, and source rescanning by project ID. Developers reach for llm-wiki when they maintain a personal LLM Wiki knowledge base and want agents to pull project context, prior notes, or indexed documents into the current coding task without manual copy-paste.
- Queries the locally-running LLM Wiki desktop app at 127.0.0.1:19828 via its built-in HTTP API
- Supports wiki page search, file listing, content retrieval, knowledge graph navigation, and source rescan
- Strict trigger rules: activates only on explicit mentions of LLM Wiki, 'my wiki', '知识库', or specific wiki project refere
- Read-only by default with controlled rescan capability
- No SDK or client library required — uses standard HTTP tools like curl, fetch, or requests
Llm Wiki by the numbers
- 1,288 all-time installs (skills.sh)
- +65 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #890 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nashsu/llm_wiki_skill --skill llm-wikiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 127 |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 19, 2026 |
| Repository | nashsu/llm_wiki_skill ↗ |
How do you query LLM Wiki from a coding agent?
Let their coding agent securely query and retrieve information from their personal LLM Wiki knowledge base during development.
Who is it for?
Developers running the LLM Wiki desktop app who want coding agents to search and read their personal wiki knowledge base by project ID.
Skip if: Developers using Obsidian, Notion, Logseq, or other PKM tools without the LLM Wiki desktop app installed locally.
When should I use this skill?
The user explicitly names LLM Wiki, asks to search or read their wiki, show the knowledge graph, or rescan wiki sources.
What you get
Retrieved wiki page content, search results, file listings, and knowledge graph data from the local LLM Wiki instance.
- Wiki search results
- Page content reads
- Knowledge graph data
By the numbers
- LLM Wiki desktop app listens at 127.0.0.1:19828
Files
LLM Wiki Local API Skill
Talk to the user's locally-running LLM Wiki app over its built-in HTTP API. This is a standard JSON API — call it directly with whatever HTTP tool is already in your environment (curl, fetch, requests, http middleware, etc.). No client library to install, no SDK to learn.
Treat the wiki as a private, structured knowledge base the user has been curating: pages live as wiki/**.md, raw documents under raw/sources/, wikilinks form a graph.
When to invoke
Invoke only when the user is clearly referring to LLM Wiki specifically — by app name, by wiki framing, or by 知识库 framing. Concretely:
- asks a question framed as "what does my wiki / my knowledge base / 我的知识库 / LLM Wiki say about X"
- asks to "search my wiki / LLM Wiki project / 我的知识库 for X"
- references a wiki page by stem / title and wants to read or cross-link
- asks for the wiki graph / 知识图谱 / wiki overview / wiki structure
- has just added or edited files under the LLM Wiki source folder and wants ingest re-run / 重新索引
- says "use my wiki for context" / "ground your answer in my wiki" / "check my LLM Wiki"
- names a wiki project (by ID, by absolute path, or by
current)
Do NOT invoke when the user says:
- "search my notes" without further qualification — likely Obsidian / Apple Notes / Notion / Logseq / Bear / etc.
- "find in my notebook" — likely Jupyter / OneNote / Notability
- "check my Obsidian / Notion / Roam / Logseq vault" — explicitly a different tool
- "look up my Anki / Readwise / Pocket" — different tool
- "search my files / my Documents folder" — generic filesystem, not the wiki
- general world knowledge, current events, or anything the user clearly wants from the open web
When in doubt about which knowledge tool the user means, ask: "Do you mean your LLM Wiki specifically, or another tool?" — don't silently call the LLM Wiki API on what might be an Obsidian vault.
Quick start
The whole API is plain HTTP + JSON. The fastest path:
BASE=http://127.0.0.1:19828
TOKEN="${LLM_WIKI_API_TOKEN:-<paste-from-Settings>}"
# 1. probe state — no auth needed
curl -s $BASE/api/v1/health
# 2. list projects
curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/v1/projects
# 3. search
curl -s -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"rope embedding","topK":5}' \
$BASE/api/v1/projects/current/search
# 4. read a page
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/projects/current/files/content?path=wiki/concepts/rope.md"If you're writing TypeScript / JavaScript:
const res = await fetch("http://127.0.0.1:19828/api/v1/projects/current/search", {
method: "POST",
headers: { "Authorization": `Bearer ${process.env.LLM_WIKI_API_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ query: "rope embedding", topK: 5 }),
})
const { results } = await res.json()Python is the same shape — urllib.request, requests, httpx, whatever you already have. Don't install anything new.
Auth model
The API is localhost-only. The token is one of:
1. LLM_WIKI_API_TOKEN environment variable (if set, overrides UI) 2. The user's apiConfig.token saved via Settings → API Server 3. allowUnauthenticated: true mode (no token needed; rare, user opt-in only)
Always check /api/v1/health first — it returns { enabled, authConfigured, allowUnauthenticated, tokenSource }. If `authConfigured: false && allowUnauthenticated: false`, ask the user to open `Settings → API Server → Generate new token`. Do not proceed without auth being set up.
Three equivalent ways to send the token:
Authorization: Bearer <token> # preferred
X-LLM-Wiki-Token: <token> # alternative header
?token=<urlencoded-token> # query param — last resort, leaks into logsNever log or echo the token. Never put it in any URL the user can see in your output (Referer / shell history / logs all leak it).
Standard workflow
When the user asks "look it up in my wiki":
1. Resolve project (see Project resolution below). 2. Search: POST /api/v1/projects/{id}/search with { query, topK: 5..10 } → ranked hits (path, title, snippet, score, titleMatch, optional vectorScore, images). Inspect response.mode to know whether hybrid retrieval kicked in. 3. Read top hits: for each promising hit, GET /api/v1/projects/{id}/files/content?path=... for the full markdown. Or pass includeContent: true to the search to avoid the round-trip. 4. Cite + answer: synthesize an answer grounded in the read pages. Quote the `path` of each page you used so the user can verify and jump in-app.
Reading the score
The score field's scale depends on mode:
- `mode: "keyword"` — additive keyword score. Filename-exact hits are ~200; phrase-in-title ~50+; bag-of-tokens lands in single digits. Treat anything below ~5% of the top result as low-confidence.
- `mode: "hybrid"` or `"vector"` — RRF (Reciprocal Rank Fusion) score, typically in the 0.015–0.035 range. The absolute number is small; relative ordering is what matters. Use the per-result
vectorScore(raw cosine 0–1) for "how strongly did the embedding match" if you need it.
Don't apply a fixed score threshold across modes. Sort by score descending and rely on relative gaps.
Project resolution
{id} in every project-scoped endpoint accepts four forms:
| Form | When to use | Example |
|---|---|---|
current (literal) | Default for "my wiki / 我的知识库 / this project / this wiki". The user is referring to whatever is open in the desktop UI. | /api/v1/projects/current/search |
| UUID | The user pasted a project ID, OR you previously resolved a name to an ID and want to re-use it. | /api/v1/projects/a0e90b29-fcf3-4364-9502-8bd1272de820/files |
| Absolute filesystem path (URL-encoded) | The user named the path (e.g. ~/notes/research). Useful when the user has multiple projects with similar names. | /api/v1/projects/%2FUsers%2Fme%2Fwiki%2Fresearch/files |
| Project name | Not supported directly. You must GET /api/v1/projects first, find a match by name, then use that project's id. |
Decision tree for what the user said:
"my wiki" / "my 知识库" / "this wiki" / "this project" / unspecified
→ use `current`
"my Research project" / "in Reading"
→ GET /api/v1/projects
→ name-match (case-insensitive substring on `name`)
→ use the resulting `id`
→ if 0 matches: tell the user, list available names, fall back to `current` only if they confirm
→ if 2+ matches: ask the user to disambiguate, quoting both names + paths
"the project at /Users/me/foo"
→ URL-encode the path, use directly
→ if the API returns 404, the project isn't registered — list and let user pick
"project a0e90b29-…"
→ use the UUID literallyCache the resolved id for the rest of the conversation — there's no need to re-GET /projects for every call. But if the user switches contexts mid-conversation ("now look in my Reading project"), re-resolve.
When the user is silent about which project, default to `current` and mention it once: "Looking in your active project (Research Notes)…". This avoids cross-project surprises.
For graph / cross-reference questions:
GET /api/v1/projects/{id}/graph?limit=200→{ nodes: [{id, label, nodeType, path, linkCount}], edges: [{source, target, weight}] }- Filter via
?q=term(substring of id/label, case-insensitive) and?nodeType=entity|concept|...
For "I added new docs" requests:
POST /api/v1/projects/{id}/sources/rescan→ returns{ queue: { tasks }, changedTasks: [...] }. Tell the user how many files changed. Actual ingest runs asynchronously via the desktop queue.
Endpoint contract (v1)
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/health | No auth. Returns { ok, status, version, enabled, authRequired, authConfigured, allowUnauthenticated, tokenSource }. |
| GET | /api/v1/projects | List projects. Each: { id, name, path, current }. |
| GET | `/api/v1/projects/{id}/files?root=wiki\ | sources\ |
| GET | /api/v1/projects/{id}/files/content?path=wiki/foo.md | Text files only (md/mdx/txt/json/yaml/yml/csv/html/htm/xml/rtf/log). 2 MB max. 415 on binary, 413 on oversize, 403 on out-of-scope path. |
| POST | /api/v1/projects/{id}/search | Body: { "query": "...", "topK": 10, "includeContent": false }. Hybrid (keyword + vector) when the user has embeddings configured in Settings; falls back to keyword-only otherwise. Response carries `mode: "keyword" \ |
| GET | /api/v1/projects/{id}/graph?q=&nodeType=&limit=200 | Wikilinks graph from wiki/*.md. Limit clamped to 1000. |
| POST | /api/v1/projects/{id}/sources/rescan | Triggers a backend rescan using the user's Source Watch config. Returns post-rescan queue + actually-changed tasks. |
| POST | /api/v1/projects/{id}/chat | 501 — not implemented in v1. Don't call. |
{id} accepts a UUID, an absolute filesystem path (URL-encoded), or the literal string current.
Error handling
Always treat the status code as the contract:
| Status | Meaning | What to do |
|---|---|---|
| 200 | OK | Use body.ok === true belt-and-suspenders; payload is in the same object. |
| 400 | Bad request | Show body.error. Typical: empty query, invalid ?root=, oversized body. |
| 401 | Unauthorized | Token missing/wrong. Tell user to set/regenerate in Settings → API Server. |
| 403 | Forbidden | Path traversal or out-of-scope (e.g. ../app-state.json). Don't retry the same path. |
| 404 | Not found | Unknown project id or unknown route. On unknown project, list projects first to recover. |
| 405 | Method not allowed | Wrong HTTP verb. |
| 413 | Payload too large | File > 2 MB, file tree > maxFiles, or request body > 1 MB. Suggest narrower scope. |
| 415 | Unsupported media | Binary or non-UTF-8 file content. API is text-only. |
| 429 | Too many requests | Rate limit (120 req/sec global). Back off ≥1 second. |
| 500 | Internal error | Log + report; don't loop. |
| 501 | Not implemented | /chat stub. Don't retry. |
| 503 | Service unavailable | Two flavors: API toggled off (error contains "disabled"); in-flight cap (64) reached ("busy"). Back off ≥2s. |
If the HTTP call itself fails (connection refused / ENOTFOUND): the desktop app is not running. Tell the user: "Launch LLM Wiki, then re-try."
Etiquette
- Cite paths. When you answer using wiki content, name the page:
(from wiki/concepts/rope.md). The user uses these to verify and to jump in-app. - Stay read-only by default. Only
sources/rescanmutates state; everything else is reads. Don't invent write endpoints — they don't exist in v1. - Don't dump full pages unless asked. Snippet + path is usually enough. Pull full content only when reasoning genuinely needs it.
- Respect the project boundary. The current project is the user's active context. Do not silently switch projects.
- Honor the rate limit. 120 req/sec is plenty for sequential work, but parallel page reads can burst close to it. Batch where the API allows (
includeContent: trueon search avoids N+1 reads). - Never leak the token. Headers are safe; query params and your own output text are not.
See also
api-reference.md— full endpoint shapes with request / response examplesexamples.md— common conversational patterns mapped to directcurl/fetchsequencesREADME.md— human setup notes (token generation, port conflicts, troubleshooting)
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:
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.
---
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].idfor all subsequent calls in this conversation. - 2+ matches → ask the user to disambiguate (show
name+pathfor 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/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.
---
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 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.
---
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/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.
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  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.
Conversation → API patterns
Treat these as recipes, not scripts. The agent decides when to combine them. Every example is plain HTTP — pick whichever client your environment already has (curl, fetch, requests, etc.).
---
Cross-platform note (Windows / macOS / Linux)
The bash + curl + jq snippets below are written for macOS / Linux. On Windows, translate them to whichever shell the user is in:
| What the bash example does | PowerShell equivalent | cmd.exe equivalent |
|---|---|---|
$TOKEN env var | $env:LLM_WIKI_API_TOKEN | %LLM_WIKI_API_TOKEN% |
| URL-encode a string<br>`printf %s "$x" \ | jq -sRr @uri` | [System.Uri]::EscapeDataString($x) |
curl -s -H "Authorization: …" | curl.exe -s -H "Authorization: …" (use curl.exe to avoid PowerShell's curl → Invoke-WebRequest alias) | curl -s -H "Authorization: …" |
Backtick line-continuation \ at end of line | backtick ` `` at end of line | ^ at end of line |
Paths on Windows:
- Always pass forward slashes in API request paths (
wiki/concepts/foo.md, neverwiki\concepts\foo.md). The server stores and accepts the forward-slash form. - When using a Windows filesystem path as
{id}(e.g.C:/Users/me/wiki), percent-encode the colon (C%3A/Users/me/wiki).EscapeDataString/encodeURIComponent/jq @uriall do this correctly. - If a path-as-id call returns 404 on Windows, fall back to
GET /api/v1/projectsand use the project's UUID — UUIDs are platform-agnostic and don't need encoding.
If you're calling from JavaScript / Python / Go / any other language with a real HTTP client (fetch, requests, httpx, net/http), platform doesn't matter — just encodeURIComponent or its equivalent and forget the shell quirks.
---
"What does my wiki say about X?"
The single most common ask. Workflow:
1. POST /api/v1/projects/current/search with { query: X, topK: 5, includeContent: true } 2. Inspect `mode` in the response to know how to read scores (see below). 3. If the top results are clearly above the rest (big gap in score), read those and synthesize. Otherwise read the top 3-5 and merge. 4. Cite each `path` you used. Quote snippets directly. 5. If nothing is found (empty results or a flat distribution with no clear winners), say so honestly. Do not fabricate.
curl -s -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"rope rotary position embedding","topK":5,"includeContent":true}' \
$BASE/api/v1/projects/current/searchHow to read scores
The score's scale depends on mode:
mode | Typical top score | What "good" looks like |
|---|---|---|
keyword | 50–300+ (additive: filename-exact ≈ 200, phrase-in-title ≈ 50) | A clear gap (2×+) between top result and the rest. |
hybrid / vector | 0.015–0.035 (RRF: 1/(60+rank)-based) | Top RRF score near 0.032 ≈ matched in both keyword and vector top-1. |
Don't apply a fixed threshold across modes. Sort by score descending and rely on the relative gap. Use vectorScore (when present) for "how strong was the semantic match" — it's a raw similarity in [0, 1], much easier to threshold than RRF.
Answer template:
Per wiki/concepts/rope.md (matched via hybrid, vectorScore=0.94), rotary position embedding works by rotating Q and K vectors by an angle proportional to position. Your wiki specifically mentions …---
"Read me the page about X"
User wants the full text, not a synthesis.
1. If the user named a slug-like identifier (rope, flash-attention), search first with topK: 1 to disambiguate. 2. GET /api/v1/projects/current/files/content?path=wiki/concepts/rope.md 3. Render the content as markdown.
PATH_REL="wiki/concepts/rope.md"
# url-encode path component
ENCODED=$(printf %s "$PATH_REL" | jq -sRr @uri)
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/projects/current/files/content?path=$ENCODED"In JS:
const encoded = encodeURIComponent("wiki/concepts/rope.md")
const r = await fetch(`${BASE}/api/v1/projects/current/files/content?path=${encoded}`, {
headers: { Authorization: `Bearer ${TOKEN}` },
})
const { content } = await r.json()---
"What pages link to X?" / "Show me the neighborhood of X"
1. GET /api/v1/projects/current/graph?limit=1000 — pull the whole graph once (cheap, < 1 MB for typical projects). 2. Find nodes[i].id === X (or label substring match). 3. Filter edges for source === X || target === X. The other endpoint is a neighbor.
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/projects/current/graph?limit=1000"You can also let the API filter for you:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/projects/current/graph?q=rope&limit=200"This applies a substring filter on id or label (case-insensitive) and returns the matching subgraph including the edges between matched nodes.
Render a small mermaid graph when the user wants a visual:
graph LR
rope --- attention
rope --- transformer
attention --- flash-attention---
"What's in my wiki?" / "Give me an overview"
Two angles:
Structural overview — file tree:
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/projects/current/files?root=wiki&recursive=true&maxFiles=500"Summarize the directory structure (concepts/, entities/, sources/…) and rough page counts per category.
Topical overview — read the curated index:
for path in wiki/index.md wiki/overview.md purpose.md; do
encoded=$(printf %s "$path" | jq -sRr @uri)
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/projects/current/files/content?path=$encoded"
echo
doneThe user's purpose.md describes intent; index.md enumerates pages; overview.md is the AI-generated topical summary. Quote the relevant chunks.
---
"I added new docs to the source folder — re-index"
1. POST /api/v1/projects/current/sources/rescan 2. Read back changedTasks. Report:
- "Detected N new / M modified / K deleted files."
- List the first ~5 file paths so the user can verify.
3. Tell the user the actual ingest runs asynchronously via the desktop queue — encourage them to open the Activity panel if they want progress.
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/projects/current/sources/rescan"If changedTasks is empty:
No file changes detected. If you added files but they're not appearing, checkSettings → Source Watch— your filters may be excluding them (e.g.,.jsonis excluded by default).
---
"Find every page that mentions Y" (broad sweep)
Search is ranked (hybrid when embeddings are configured, keyword otherwise) and capped at 50 hits per call. For exhaustive sweeps:
1. Run POST .../search with topK: 50 and your term. 2. If the 50th result still has a non-trivial score (relative to the top), run again with a more specific query — the API will not return more than 50 in one call. 3. For exact-string sweeps where keyword tokenization mangles your phrase (e.g. CJK punctuation boundaries, code identifiers with underscores), walk every wiki/*.md via files + files/content and grep client-side. Slow but reliable. 4. Pure-semantic sweeps: set topK: 50 and read vectorScore on each hit — pages without vectorScore matched only via keyword.
---
"Search in my Reading project, not the current one"
When the user names a specific project rather than implying the active one.
1. List projects to resolve the name:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/projects"Returns:
{
"projects": [
{"id":"abc-…","name":"Research Notes","path":"/Users/me/wiki/research","current":true},
{"id":"def-…","name":"Reading","path":"/Users/me/wiki/reading","current":false}
]
}2. Match the user's spoken name. Case-insensitive substring on name:
const projects = (await (await fetch(`${BASE}/api/v1/projects`, {
headers: { Authorization: `Bearer ${TOKEN}` },
})).json()).projects
const match = projects.filter(p => p.name.toLowerCase().includes("reading"))3. Handle ambiguity:
- 0 matches → tell the user, list available names, ask which one. Don't silently fall back to
current— that would answer the wrong question. - 1 match → use its
idin all subsequent calls for this conversation. - 2+ matches → ask the user to disambiguate, showing both
name+path.
4. Use the resolved id directly:
PROJECT_ID="def-…" # from step 2
curl -s -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"narrative voice","topK":5}' \
"$BASE/api/v1/projects/$PROJECT_ID/search"5. Cache the id for the rest of the conversation. Don't re-list projects on every call. Only re-resolve if the user switches contexts ("now search my Research project instead").
You can also pass the project's filesystem path directly (URL-encoded) when the user references it that way:
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"---
"Compare what my Research and Reading projects say about X"
User wants cross-project synthesis.
1. GET /api/v1/projects once → grab both ids. 2. Search each separately with the same query:
for ID in research-id reading-id; do
curl -s -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"narrative voice","topK":3,"includeContent":true}' \
"$BASE/api/v1/projects/$ID/search"
done3. Diff / contrast the result sets. Cite both project name and page path: "In Research Notes (`wiki/concepts/narrative.md`)… vs. in Reading (`wiki/concepts/voice.md`)…".
current only refers to the active project; for multi-project queries always pass explicit IDs.
---
"Switch to project X" (mid-conversation)
User has been asking about the active project, then says "now check my Reading project for the same thing."
1. Re-resolve via GET /api/v1/projects (or use cached list if recent). 2. Replace your cached project id for the rest of the conversation. 3. Confirm the switch in your reply once: "Switching to your Reading project…". Don't silently apply. 4. Keep the user's query — apply it to the new project.
The desktop UI's active project does not change just because you used a different {id} — your API calls scoped to a non-current id are read-only and don't affect the user's UI state.
---
Anti-patterns
- Don't `POST /chat` — it returns 501. Use the desktop chat UI, or call your own LLM with the wiki content as context.
- Don't paginate the file tree with maxFiles=1, maxFiles=2, … — there's no offset/cursor. Use a higher cap or filter by
root=. - Don't bypass the API for writes. The API is read-mostly. Writing to
wiki/**orraw/sources/**via the filesystem directly bypasses the file-sync watcher. Use the desktop UI's Sources → Import flow or the ingest pipeline. - Don't include the token in URLs you echo / log / show. Use headers in any output visible to the user.
- Don't retry blindly on 429 / 503 — back off explicitly (1s / 2s respectively).
- Don't shell out to a wrapper script. The API is the contract. Direct
curl/fetchis the interface.
---
End-to-end transcript
User: "What does my wiki say about flash attention? Show me the related concepts too."
Agent (internally):
# 1. health + auth pre-flight
curl -s "$BASE/api/v1/health"
# → tokenSource=store, enabled=true, authConfigured=true ✓
# 2. search
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"flash attention","topK":5,"includeContent":true}' \
"$BASE/api/v1/projects/current/search"
# 3. graph filter for "flash-attention" neighborhood
curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/projects/current/graph?q=flash&limit=200"Agent (to user):
Looking in your active project. Search ran in hybrid mode (tokenHits=12, vectorHits=8) — top hitwiki/concepts/flash-attention.mdmatched both keyword and vector signals (vectorScore=0.91).
>
Your wiki explains FlashAttention as a memory-efficient exact attention implementation that tiles the QKV matrices to avoid materializing the full N×N attention map. Key claim: 2-4× speedup at long context on A100s (per your wiki).
>
Related concepts in your graph:
- wiki/concepts/attention.md (parent)-wiki/concepts/long-context.md(sibling, both reachable fromflash-attention)
- wiki/entities/tri-dao.md (cited author)>
Want me to read any of these in full?
llm-wiki skill
Lets an AI agent (Claude Code, Codex, etc.) query the user's locally-running LLM Wiki desktop app over its built-in HTTP API.
This skill is documentation only. The API is a standard HTTP+JSON contract — call it with curl, fetch, requests, or whatever HTTP tool your environment already has. There is no client library, no SDK, no compile step.
Installation
Recommended — one-liner via skills CLI
npx skills add https://github.com/nashsu/llm_wiki_skill.git --skill llm-wikiThis fetches the latest version of the skill from GitHub and registers it with your agent runtime. Re-run the same command to update to a newer revision later.
Alternative — clone + symlink (Claude Code)
If you prefer to manage the source locally and link it into Claude Code (skills live at ~/.claude/skills/):
git clone https://github.com/nashsu/llm_wiki_skill.git
ln -s "$(pwd)/llm_wiki_skill" ~/.claude/skills/llm-wikiAlternative — manual drop-in
The skill is three markdown files and nothing else. Drop the folder anywhere your agent runtime discovers skills (e.g. Codex agents/, custom MCP server skill dir). No dependencies, no build step.
What this skill enables
- "What does my wiki say about X?" → hybrid search (keyword + vector) + read
- "Show me the neighborhood of node Y in my graph" → wikilinks traversal
- "Read me the page about Z" → file content fetch
- "I just added new docs, re-index" → backend rescan
- "Give me a structural overview of my wiki" → file tree + index.md
All read-only except sources/rescan (triggers an internal queue diff).
Files
| File | Purpose |
|---|---|
SKILL.md | Agent-facing instructions. Loaded automatically by the AI runtime. |
api-reference.md | Full endpoint contract (status codes, params, response shapes). |
examples.md | Conversation → API recipe patterns. |
README.md | This file — human setup / install / troubleshooting. |
No scripts, no wrappers. If you find yourself wanting one, just curl.
Prerequisites
1. LLM Wiki desktop app installed and running. The API binds to 127.0.0.1:19828 only while the app is open. If the app isn't running, the agent will see connection refused and tell you.
2. API server enabled. Default: on. Verify in Settings → API Server → "Enable local HTTP API" is checked.
3. Token configured. Default: off (token field empty → every endpoint returns 401). Either:
- In the app:
Settings → API Server → Generate new token. Copy and stash it. - Via env:
export LLM_WIKI_API_TOKEN=...(overrides UI; persists for the agent process).
You can also flip Settings → API Server → "Allow access without a token", which removes auth entirely. Local-only — but any process on this machine can then read your wiki. Use only with trusted setups.
Quick smoke test
Windows users: replaceexport LLM_WIKI_API_TOKEN=...with$env:LLM_WIKI_API_TOKEN = "..."(PowerShell) orset LLM_WIKI_API_TOKEN=...(cmd.exe). Usecurl.exeinstead ofcurlin PowerShell to bypass theInvoke-WebRequestalias. Backslash line-continuations (\) become backticks (``) in PowerShell and^` in cmd.
export LLM_WIKI_API_TOKEN=...
# 1. Probe — no auth needed
curl -s http://127.0.0.1:19828/api/v1/health
# Expected:
# {
# "ok": true,
# "status": "running",
# "enabled": true,
# "authConfigured": true,
# "tokenSource": "env",
# "allowUnauthenticated": false,
# ...
# }
# 2. List projects
curl -s -H "Authorization: Bearer $LLM_WIKI_API_TOKEN" \
http://127.0.0.1:19828/api/v1/projects
# 3. Search the active project
curl -s -H "Authorization: Bearer $LLM_WIKI_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"rope","topK":3}' \
http://127.0.0.1:19828/api/v1/projects/current/search
# 4. Read a page
curl -s -H "Authorization: Bearer $LLM_WIKI_API_TOKEN" \
"http://127.0.0.1:19828/api/v1/projects/current/files/content?path=wiki/concepts/rope.md"Add | jq at the end of any of these if you have jq installed and want pretty output. Not required.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
curl: (7) Failed to connect to 127.0.0.1 port 19828 | App not running | Launch LLM Wiki desktop, wait ~2s for the API server to bind. |
Status: port_conflict in /health | Another process on 19828 | lsof -i :19828 to find the squatter; quit and restart the app. |
| Every endpoint returns 401 | No token configured | Settings → API Server → Generate; OR export LLM_WIKI_API_TOKEN=... |
| Every endpoint returns 503 with "disabled" | Kill switch is on | Settings → API Server → check "Enable local HTTP API". |
| Every endpoint returns 503 with "busy" | In-flight cap reached (64 concurrent) | Back off, send fewer parallel requests. |
| 429 on rapid requests | Rate limit: 120 req/sec global | Back off ≥1s. |
files/content returns 415 on .pdf | API is text-only | Read source PDFs via the desktop UI; only text-ish extensions are exposed. |
tokenSource: "env" even though I cleared it from UI | LLM_WIKI_API_TOKEN env var is set somewhere | unset LLM_WIKI_API_TOKEN; or set its value to match what's in your UI. |
Security model
- The API listens on
127.0.0.1only. It is not reachable from other hosts on your network. - Token-based auth is constant-time compared (no timing leak in the auth check).
- Path traversal blocked at the route handler (
safe_joinwith canonical-path prefix check). - File reads restricted to a whitelist (
purpose.md,schema.md,wiki/**,raw/sources/**); text extensions only; 2 MB cap. - Body limit 1 MB; in-flight cap 64; rate limit 120/sec.
- No write endpoints in v1.
/sources/rescanis the only mutation, and it only reads disk + queues internal work.
What's not in the threat model:
- A malicious local process with the token can read everything in your active project's wiki + sources. Treat the token as a local secret.
- A malicious local process can probe
/health(no auth) to discover the API exists. Information disclosure, no content leak. - Cross-origin browser pages can hit the API only if they have the token. Default CORS is
*— relies on token secrecy. Don't paste your token into untrusted web tools.
Version
This skill matches LLM Wiki API v1 as shipped in app version 0.4.10+. Hybrid retrieval (keyword + vector) is live; the response carries mode: "keyword" | "vector" | "hybrid", tokenHits, vectorHits, and per-result vectorScore. If the desktop app's /health reports a major version bump, check api-reference.md for drift before relying on the contract verbatim.
Updating
If you installed via npx skills add, re-run the same command to pull the latest revision:
npx skills add https://github.com/nashsu/llm_wiki_skill.git --skill llm_wiki_skillIf you cloned the repo, git pull inside the cloned folder is enough — the symlink picks up changes automatically.
Contributing
Source repo: <https://github.com/nashsu/llm_wiki_skill>
Issues and PRs welcome. The skill should stay client-less — no scripts, no SDK shim, no wrapper binaries. The whole point of the underlying API is that any HTTP tool can call it; this skill is the contract documentation, not a runtime.
License
See the repository for license details.
Related skills
How it compares
Use llm-wiki only when the knowledge base is LLM Wiki, not for Obsidian or Notion integrations.
FAQ
What app does llm-wiki connect to?
llm-wiki connects to the LLM Wiki desktop application at 127.0.0.1:19828. The skill does not target Obsidian, Notion, Apple Notes, Logseq, or other PKM tools.
When should agents invoke llm-wiki?
llm-wiki triggers when users explicitly name LLM Wiki, reference my wiki or knowledge base, or ask to search wiki pages, read content, show the knowledge graph, or rescan wiki sources by project ID.
Is Llm Wiki safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.