Run docmgr as a local HTTP server with a JSON search API (v1), cursor pagination, and explicit index refresh.
docmgr can run as a local HTTP server exposing a versioned JSON API for searching documentation using the same query engine as the CLI.
This is intended for:
See also: docmgr-web-ui.md (Slug: web-ui) for running the bundled Search Web UI.
Security note: the server is local-first. Bind to 127.0.0.1 by default and don’t expose it publicly unless you add authentication and threat-model it.
Build and run the server:
go build -tags "sqlite_fts5,embed" -o /tmp/docmgr ./cmd/docmgr
/tmp/docmgr api serve --addr 127.0.0.1:8787 --root ttmp
Notes:
sqlite_fts5 enables full-text search.embed bundles the web UI assets into the binary (optional for API-only usage).Check health:
curl -s http://127.0.0.1:8787/api/v1/healthz
Refresh the index (explicit):
curl -s -X POST http://127.0.0.1:8787/api/v1/index/refresh
Search:
curl -s "http://127.0.0.1:8787/api/v1/search/docs?query=websocket&orderBy=rank&pageSize=50"
The server builds an in-memory SQLite index on startup and reuses it for requests.
POST /api/v1/index/refresh rebuilds the index from disk and swaps it in atomicallyThis is intentionally “refresh-on-demand” for simplicity (no file watching yet).
The query parameter uses SQLite FTS5 MATCH syntax and is not a substring/contains search.
-tags sqlite_fts5 to enable full-text search.query, the API returns an error.Ranking:
orderBy=rank orders by bm25 score (best matches first).GET /api/v1/search/docs supports cursor-based pagination:
pageSize + cursornextCursor (opaque; pass it back as cursor to fetch the next page)For v1, cursors may be implemented internally using offsets but are treated as opaque by clients.
docmgr api serve --addr 127.0.0.1:8787 --root ttmp
Flags:
--addr: bind address (default 127.0.0.1:8787)--root: docs root directory (default ttmp)--cors-origin: if set, adds CORS headers for browser-based UIsBase path: /api/v1
GET /api/v1/healthz
Response:
{ "ok": true }
GET /api/v1/workspace/status
Purpose: show which workspace is currently indexed and basic index metadata.
Response (shape):
{
"root": "/abs/path/to/ttmp",
"configDir": "/abs/path",
"repoRoot": "/abs/path/to/repo",
"configPath": "/abs/path/to/.ttmp.yaml",
"vocabularyPath": "/abs/path/to/ttmp/vocabulary.yaml",
"indexedAt": "2026-01-04T21:05:04.583Z",
"docsIndexed": 200,
"ftsAvailable": true
}
GET /api/v1/workspace/summary
Purpose: render the Workspace home/dashboard with one call (basic stats + recent tickets + recent docs).
Response (shape):
{
"root": "/abs/path/to/ttmp",
"repoRoot": "/abs/path/to/repo",
"indexedAt": "2026-01-05T00:00:00Z",
"docsIndexed": 413,
"stats": {
"ticketsTotal": 128,
"ticketsActive": 12,
"ticketsComplete": 84,
"ticketsReview": 9,
"ticketsDraft": 23
},
"recent": {
"tickets": [
{
"ticket": "001-ADD-DOCMGR-UI",
"title": "Add docmgr Web UI",
"status": "active",
"topics": ["docmgr", "ui"],
"owners": [],
"intent": "long-term",
"createdAt": "2026-01-03",
"updatedAt": "2026-01-05T00:00:00Z",
"ticketDir": "2026/01/03/001-ADD-DOCMGR-UI--add-docmgr-web-ui",
"indexPath": "2026/01/03/001-ADD-DOCMGR-UI--add-docmgr-web-ui/index.md",
"snippet": "",
"stats": null
}
],
"docs": [
{
"path": "2026/01/03/.../design/03-workspace-rest-api.md",
"ticket": "001-ADD-DOCMGR-UI",
"title": "Design: docmgr Workspace REST API (for full-site navigation)",
"docType": "design-doc",
"status": "active",
"topics": ["docmgr", "ui", "http", "api", "workspace"],
"updatedAt": "2026-01-05T00:00:00Z"
}
]
}
}
POST /api/v1/index/refresh
Response (shape):
{
"refreshed": true,
"indexedAt": "2026-01-04T21:05:04.583Z",
"docsIndexed": 200,
"ftsAvailable": true
}
GET /api/v1/workspace/tickets
Purpose: list tickets (workspace-wide) derived from the ticket index.md docs (DocType: index).
Query parameters:
status (string): active|review|complete|draft| (empty = all)ticket (string): exact ticket ID match (optional)topics (string): comma-separated, match any topic (optional)owners (string): comma-separated, match any owner (optional)intent (string): exact match (optional)q (string): full-text query (FTS5) applied to index docs only (optional)orderBy (string): last_updated|ticket|title (default last_updated)reverse (bool, default false)includeArchived (bool, default true)includeStats (bool, default false): when true, computes per-ticket stats (tasks/docs/related files)pageSize (int, default 200, max 1000)cursor (string, optional)Response (shape):
{
"query": {
"q": "",
"status": "active",
"ticket": "",
"topics": ["docmgr", "ui"],
"owners": [],
"intent": "",
"orderBy": "last_updated",
"reverse": false,
"includeArchived": true,
"includeStats": false,
"pageSize": 200,
"cursor": ""
},
"total": 128,
"results": [
{
"ticket": "001-ADD-DOCMGR-UI",
"title": "Add docmgr Web UI",
"status": "active",
"topics": ["docmgr", "ui"],
"owners": [],
"intent": "long-term",
"createdAt": "2026-01-03",
"updatedAt": "2026-01-05T00:00:00Z",
"ticketDir": "2026/01/03/001-ADD-DOCMGR-UI--add-docmgr-web-ui",
"indexPath": "2026/01/03/001-ADD-DOCMGR-UI--add-docmgr-web-ui/index.md",
"snippet": "",
"stats": null
}
],
"nextCursor": ""
}
GET /api/v1/workspace/facets
Purpose: drive Workspace filters (statuses/docTypes/intents/topics/owners).
Query parameters:
includeArchived (bool, default true)Response (shape):
{
"statuses": ["active", "review", "complete", "draft"],
"docTypes": ["index", "design-doc", "reference", "analysis", "sources"],
"intents": ["short-term", "long-term", "evergreen"],
"topics": ["docmgr", "ui", "tooling"],
"owners": ["manuel", "alex"]
}
Notes:
vocabulary.yaml for statuses/docTypes/intents/topics when present, but falls back to deriving from indexed docs.GET /api/v1/workspace/recent
Purpose: show “recently updated tickets” and “recently updated docs”.
Query parameters:
ticketsLimit (int, default 20, max 1000)docsLimit (int, default 20, max 1000)includeArchived (bool, default true)Response (shape):
{
"tickets": [ /* TicketListItem[] (same as /workspace/tickets results) */ ],
"docs": [
{
"path": "2026/01/03/.../design/03-workspace-rest-api.md",
"ticket": "001-ADD-DOCMGR-UI",
"title": "Design: docmgr Workspace REST API (for full-site navigation)",
"docType": "design-doc",
"status": "active",
"topics": ["docmgr", "ui"],
"updatedAt": "2026-01-05T00:00:00Z"
}
]
}
GET /api/v1/workspace/topics
Purpose: list topics (workspace-wide) with basic counts.
Query parameters:
includeArchived (bool, default true)Response (shape):
{
"total": 42,
"results": [
{ "topic": "docmgr", "docsTotal": 120, "ticketsTotal": 14, "updatedAt": "2026-01-05T00:00:00Z" }
]
}
GET /api/v1/workspace/topics/get
Query parameters:
topic (string, required)includeArchived (bool, default true)docsLimit (int, default 20, max 1000)Response (shape):
{
"topic": "docmgr",
"stats": {
"ticketsTotal": 14,
"ticketsActive": 6,
"ticketsComplete": 4,
"ticketsReview": 2,
"ticketsDraft": 2
},
"tickets": [ /* TicketListItem[] */ ],
"docs": [ /* RecentDocItem[] */ ]
}
GET /api/v1/search/docs
Query parameters:
query (string): FTS5 MATCH query stringticket (string)topics (string): comma-separateddocType (string)status (string)file (string): reverse lookupdir (string): reverse lookupexternalSource (string)since (string)until (string)createdSince (string)updatedSince (string)Visibility toggles (defaults mirror CLI behavior):
includeArchived (bool, default true)includeScripts (bool, default true)includeControlDocs (bool, default true)includeDiagnostics (bool, default true)includeErrors (bool, default false)Sorting:
orderBy: path|last_updated|rank (default path)reverse (bool, default false)Reverse lookup notes:
reverse=true searches docs by RelatedFiles references.file or dir.reverse=true and file/dir are empty but query is set, the server treats query as file.Pagination:
pageSize (int, default 200, max 1000)cursor (string, optional)Response (shape):
{
"query": { "query": "websocket", "pageSize": 50, "cursor": "" },
"total": 12,
"results": [
{
"ticket": "MEN-4242",
"title": "Chat WebSocket Lifecycle",
"docType": "reference",
"status": "active",
"topics": ["chat", "backend", "websocket"],
"path": "2026/01/04/MEN-4242--.../reference/01-chat-websocket-lifecycle.md",
"lastUpdated": "2026-01-04T15:04:05Z",
"snippet": "...",
"relatedFiles": [
{ "path": "backend/chat/ws/manager.go", "note": "WebSocket lifecycle (scenario)" }
],
"matchedFiles": ["backend/chat/ws/manager.go"],
"matchedNotes": ["WebSocket lifecycle (scenario)"]
}
],
"diagnostics": [],
"nextCursor": "..."
}
GET /api/v1/search/files
Query parameters:
query (string)ticket (string)topics (string): comma-separatedlimit (int, default 200, max 1000)Response (shape):
{
"total": 3,
"results": [
{ "file": "backend/chat/ws/manager.go", "source": "related_files", "reason": "..." }
]
}
GET /api/v1/docs/get
Query parameters:
path (string, required): doc-relative path under the docs root (same value as SearchDocResult.path)Response (shape):
{
"path": "2026/01/03/TICKET--slug/design/01-doc.md",
"doc": {
"title": "...",
"ticket": "...",
"status": "...",
"topics": ["..."],
"docType": "...",
"intent": "...",
"owners": ["..."],
"relatedFiles": [{ "path": "internal/foo.go", "note": "..." }],
"externalSources": [],
"summary": "",
"lastUpdated": "2026-01-04T19:22:44-05:00",
"whatFor": "",
"whenToUse": ""
},
"relatedFiles": [{ "path": "internal/foo.go", "note": "..." }],
"body": "# Markdown…",
"stats": { "sizeBytes": 12345, "modTime": "2026-01-04T19:22:44-05:00" },
"diagnostic": null
}
Notes:
doc will be omitted and diagnostic may be present; body still returns the markdown body (best-effort).GET /api/v1/files/get
Query parameters:
path (string, required): a file path (repo-relative is recommended; absolute paths are only accepted if they resolve inside the allowed root)root (string, optional): repo|docs (default repo)Response (shape):
{
"path": "internal/httpapi/server.go",
"root": "repo",
"language": "go",
"contentType": "text/x-go; charset=utf-8",
"truncated": false,
"content": "package httpapi\n...",
"stats": { "sizeBytes": 12345, "modTime": "2026-01-04T19:22:44-05:00" }
}
Safety behavior:
unsupported_media_type).truncated).All error responses use a stable JSON envelope:
{
"error": {
"code": "invalid_argument",
"message": "must provide at least a query or filter",
"details": {}
}
}
Common error codes:
index_not_ready (503): the index is not initializedinvalid_cursor (400): cursor is malformedfts_not_available (400): request uses query but FTS is unavailableinternal (500): unexpected server errorfts_not_availableBuild with -tags sqlite_fts5 and restart the server:
go build -tags sqlite_fts5 -o /tmp/docmgr ./cmd/docmgr
/tmp/docmgr api serve --addr 127.0.0.1:8787 --root ttmp
Call refresh:
curl -s -X POST http://127.0.0.1:8787/api/v1/index/refresh