REST API
HTTP API for GNO search, retrieval, documents, graph data, model state, and workspace automation from the same local knowledge engine.
REST API
Programmatic access to the same local knowledge workspace that powers the CLI, web UI, desktop shell, and agent integrations.
gno serve
# API available at http://localhost:3000/api/*
Overview
The GNO REST API provides programmatic access to your local knowledge index. Use it to:
- Search documents from scripts and applications
- Build custom integrations
- Automate workflows
- Create dashboards and tools
All endpoints are JSON-based and run entirely on your machine.
Quick Reference
Read Operations
| Endpoint | Method | Description |
|---|---|---|
/api/health |
GET | Health check |
/api/status |
GET | Index statistics, onboarding, health, background, bootstrap |
/api/capabilities |
GET | Available features |
/api/collections |
GET | List collections |
/api/connectors |
GET | Detect in-app connector install state |
/api/docs |
GET | List documents |
/api/docs/autocomplete |
GET | Title/path suggestions for wiki-linking and quick switcher |
/api/note-presets |
GET | List note presets and scaffold previews |
/api/doc |
GET | Get document content |
/api/doc/:id/sections |
GET | Get extracted heading/section structure |
/api/events |
GET | Server-sent document change events |
/api/doc/:id/links |
GET | Get outgoing links from doc |
/api/doc/:id/backlinks |
GET | Get docs linking to this |
/api/doc/:id/similar |
GET | Find semantically similar |
/api/graph |
GET | Knowledge graph of links |
/api/tags |
GET | List tags with counts |
/api/search |
POST | BM25 keyword search |
/api/query |
POST | Hybrid search |
/api/ask |
POST | AI-powered Q&A |
/api/presets |
GET | List model presets |
/api/presets |
POST | Switch preset |
/api/models/status |
GET | Download status |
/api/models/pull |
POST | Start model download |
Write Operations
| Endpoint | Method | Description |
|---|---|---|
/api/collections |
POST | Add new collection |
/api/connectors/install |
POST | Install connector |
/api/collections/:name |
DELETE | Remove collection |
/api/sync |
POST | Trigger re-index |
/api/docs |
POST | Create new document |
/api/docs/:id |
PUT | Update document |
/api/docs/:id/refactor-plan |
POST | Preview rename/move/duplicate warnings |
/api/docs/:id/move |
POST | Move editable document |
/api/docs/:id/duplicate |
POST | Duplicate editable document |
/api/docs/:id/deactivate |
POST | Unindex document |
/api/folders |
POST | Create folder in collection |
/api/jobs/active |
GET | Get active job |
/api/jobs/:id |
GET | Poll job status |
Authentication & Security
The API binds to 127.0.0.1 only and is not accessible from the network.
CSRF Protection
All mutating requests (POST, DELETE) require one of:
- Same-origin request: No
Originheader (curl, scripts) - Valid Origin:
Origin: http://localhost:<port>orhttp://127.0.0.1:<port> - API Token:
X-GNO-Tokenheader (for non-browser clients)
Cross-origin requests from other domains are rejected with 403 Forbidden.
Token Authentication
For non-browser clients (Raycast, scripts), set the GNO_API_TOKEN environment variable:
export GNO_API_TOKEN="your-secret-token"
gno serve
Then include the token in requests:
curl -X POST http://localhost:3000/api/collections \
-H "X-GNO-Token: your-secret-token" \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/folder"}'
Note: Token auth is optional. Requests without an
Originheader (like curl) work without a token.
Endpoints
Health Check
GET /api/health
Response:
{
"ok": true
}
Index Status
GET /api/status
Returns index statistics plus first-run onboarding, health-center state, background-service telemetry, and bootstrap/runtime-model provisioning state for the dashboard.
Response:
{
"indexName": "default",
"configPath": "/Users/you/.config/gno/index.yml",
"dbPath": "/Users/you/.local/share/gno/index-default.sqlite",
"collections": [
{
"name": "notes",
"path": "/Users/you/notes",
"documentCount": 142,
"chunkCount": 1853,
"embeddedCount": 1853
}
],
"totalDocuments": 142,
"totalChunks": 1853,
"embeddingBacklog": 0,
"recentErrors": 0,
"lastUpdated": "2025-01-15T10:30:00Z",
"healthy": true,
"activePreset": {
"id": "slim-tuned",
"name": "GNO Slim Tuned (Default, ~1GB)"
},
"capabilities": {
"bm25": true,
"vector": true,
"hybrid": true,
"answer": true
},
"onboarding": {
"ready": false,
"stage": "indexing",
"headline": "GNO is almost ready. Finish the first indexing run",
"detail": "Run the first sync to populate the index from the folders you connected.",
"suggestedCollections": [
{
"label": "Documents",
"path": "/Users/you/Documents",
"reason": "Good default for notes and docs"
}
],
"steps": [
{
"id": "folders",
"title": "Pick folders",
"status": "complete",
"detail": "1 folder connected."
}
]
},
"health": {
"state": "needs-attention",
"summary": "GNO works, but a few issues still need attention before it feels reliable.",
"checks": [
{
"id": "models",
"title": "Models",
"status": "warn",
"summary": "Balanced is usable, but answer models are still missing",
"detail": "Core search is ready. Download the rest of the preset for best ranking and local AI answers.",
"actionLabel": "Download models",
"actionKind": "download-models"
}
]
},
"background": {
"watcher": {
"expectedCollections": ["notes"],
"activeCollections": ["notes"],
"failedCollections": [],
"queuedCollections": [],
"syncingCollections": [],
"lastEventAt": "2025-01-15T10:31:00Z",
"lastSyncAt": "2025-01-15T10:31:02Z"
},
"embedding": {
"available": true,
"pendingDocCount": 0,
"running": false,
"nextRunAt": null,
"lastRunAt": 1736937062000,
"lastResult": {
"embedded": 12,
"errors": 0
}
},
"events": {
"connectedClients": 2,
"retryMs": 2000
}
},
"bootstrap": {
"runtime": {
"kind": "bun",
"strategy": "manual-install-beta",
"currentVersion": "1.3.6",
"requiredVersion": ">=1.3.0",
"ready": true,
"managedByApp": false,
"summary": "This beta runs on Bun 1.3.6.",
"detail": "Current beta installs still expect Bun to be present on the machine. Final desktop packaging work is separate."
},
"policy": {
"offline": false,
"allowDownload": true,
"source": "default",
"summary": "Models can auto-download on first use."
},
"cache": {
"path": "/Users/you/Library/Caches/gno",
"totalSizeBytes": 2147483648,
"totalSizeLabel": "2.0 GB"
},
"models": {
"activePresetId": "slim-tuned",
"activePresetName": "GNO Slim Tuned (Default, ~1GB)",
"estimatedFootprint": "~1GB",
"downloading": false,
"cachedCount": 4,
"totalCount": 4,
"summary": "GNO Slim Tuned (Default, ~1GB) is fully cached.",
"entries": []
}
}
}
onboarding.stage is one of add-collection, models, indexing, or ready.
health.checks gives per-area status cards for folders, indexing, models, and disk. Actions map to dashboard buttons such as add folder, run sync, or download models.
background is the reliability block:
watchershows which collections are expected, actively watched, queued, syncing, or failedembeddingreports pending/running background embedding stateeventsreports current SSE clients and recommended reconnect retry
bootstrap is the install/runtime/model block:
runtimeexplains the current beta runtime strategy and versionpolicyexplains whether models auto-download, stay offline, or require manual pullcacheshows where models live and how much disk they usemodelsshows active preset readiness role by role
Example:
curl http://localhost:3000/api/status | jq
Capabilities
GET /api/capabilities
Returns available features based on loaded models.
Response:
{
"bm25": true,
"vector": true,
"hybrid": true,
"answer": true
}
| Field | Description |
|---|---|
bm25 |
BM25 search (always true) |
vector |
Vector search available |
hybrid |
Hybrid search available |
answer |
AI answer generation available |
List Collections
GET /api/collections
Response:
[
{
"name": "notes",
"path": "/Users/you/notes",
"pattern": "**/*.md",
"include": [],
"exclude": [".git", "node_modules"],
"models": {
"embed": "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
},
"effectiveModels": {
"embed": "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf",
"rerank": "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",
"expand": "hf:guiltylemon/gno-expansion-slim-retrieval-v1/gno-expansion-auto-entity-lock-default-mix-lr95-f16.gguf",
"gen": "hf:unsloth/Qwen3-1.7B-GGUF/Qwen3-1.7B-Q4_K_M.gguf"
},
"modelSources": {
"embed": "override",
"rerank": "preset",
"expand": "preset",
"gen": "preset"
},
"activePresetId": "slim-tuned"
}
]
effectiveModels and modelSources exist so clients can show inherited-vs-overridden collection model state without re-implementing preset resolution logic.
Add Collection
POST /api/collections
Add a folder to the index as a new collection. Starts background indexing job.
Request Body:
{
"path": "/Users/you/notes",
"name": "notes",
"pattern": "**/*.md",
"include": "docs/**",
"exclude": "node_modules/**",
"gitPull": false
}
| Field | Type | Required | Description |
|---|---|---|---|
path |
string | Yes | Absolute path to folder |
name |
string | No | Collection name (defaults to folder name) |
pattern |
string | No | Glob pattern for files (default: **/*.md) |
include |
string | No | Additional include patterns |
exclude |
string | No | Exclude patterns |
gitPull |
boolean | No | Run git pull before indexing |
Response (202 Accepted):
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"collection": {
"name": "notes",
"path": "/Users/you/notes"
}
}
Errors:
| Code | Status | Description |
|---|---|---|
VALIDATION |
400 | Missing or invalid path |
PATH_NOT_FOUND |
400 | Path does not exist |
DUPLICATE |
409 | Collection name already exists |
CONFLICT |
409 | Another job is running |
Example:
curl -X POST http://localhost:3000/api/collections \
-H "Content-Type: application/json" \
-d '{"path": "/Users/you/notes", "name": "notes"}'
Delete Collection
DELETE /api/collections/:name
Remove a collection from the config. Indexed documents remain in DB but won’t appear in searches.
Response:
{
"success": true,
"collection": "notes",
"note": "Collection removed from config. Indexed documents remain in DB."
}
Errors:
| Code | Status | Description |
|---|---|---|
NOT_FOUND |
404 | Collection does not exist |
HAS_REFERENCES |
400 | Collection has context references |
Example:
curl -X DELETE http://localhost:3000/api/collections/notes
Update Collection Model Overrides
PATCH /api/collections/:name
Update per-collection model overrides without changing the global active preset.
Request Body:
{
"models": {
"embed": "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf",
"rerank": null
}
}
Rules:
- omitted roles are left unchanged
- string values set/replace one override
nullclears one override and returns that role to preset inheritance
Response:
{
"success": true,
"collection": {
"name": "notes",
"path": "/Users/you/notes",
"models": {
"embed": "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
},
"effectiveModels": {
"embed": "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf",
"rerank": "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",
"expand": "hf:guiltylemon/gno-expansion-slim-retrieval-v1/gno-expansion-auto-entity-lock-default-mix-lr95-f16.gguf",
"gen": "hf:unsloth/Qwen3-1.7B-GGUF/Qwen3-1.7B-Q4_K_M.gguf"
},
"modelSources": {
"embed": "override",
"rerank": "preset",
"expand": "preset",
"gen": "preset"
},
"activePresetId": "slim-tuned"
}
}
Errors:
| Code | Status | Description |
|---|---|---|
VALIDATION |
400 | Invalid body or invalid role value |
NOT_FOUND |
404 | Collection does not exist |
Example:
curl -X PATCH http://localhost:3000/api/collections/notes \
-H "Content-Type: application/json" \
-d '{"models":{"embed":"hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"}}'
Clear Collection Embeddings
POST /api/collections/:name/embeddings/clear
Clear embeddings for one collection.
Request Body:
{
"mode": "stale"
}
Modes:
stale- remove embeddings for models other than the active embed model for that collectionall- remove all embeddings for that collection
Response:
{
"success": true,
"stats": {
"collection": "notes",
"mode": "stale",
"deletedVectors": 24,
"deletedModels": ["hf:old/model.gguf"],
"protectedSharedVectors": 3
},
"note": "Some shared vectors were retained because other active collections still use the same content."
}
If mode is all, the response note points users to gno embed --collection <name>.
Sync / Re-index
POST /api/sync
Trigger re-indexing of all collections or a specific one.
Note: After sync completes, embeddings are automatically generated for any new/updated chunks (debounced, runs in background).
Request Body:
{
"collection": "notes",
"gitPull": false
}
| Field | Type | Required | Description |
|---|---|---|---|
collection |
string | No | Specific collection to sync (case-insensitive) |
gitPull |
boolean | No | Run git pull before sync |
Response (202 Accepted):
{
"jobId": "550e8400-e29b-41d4-a716-446655440000"
}
Error (sync already running):
{
"error": {
"code": "CONFLICT",
"message": "Job 550e8400-e29b-41d4-a716-446655440000 already running",
"details": {
"activeJobId": "550e8400-e29b-41d4-a716-446655440000"
}
}
}
Example:
# Sync all collections
curl -X POST http://localhost:3000/api/sync
# Sync specific collection
curl -X POST http://localhost:3000/api/sync \
-H "Content-Type: application/json" \
-d '{"collection": "notes"}'
Job Status
GET /api/jobs/:id
Poll the status of a background job (indexing, sync).
Response (running):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "add",
"status": "running",
"createdAt": 1704067200000
}
Response (completed):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "sync",
"status": "completed",
"createdAt": 1704067200000,
"result": {
"collections": [
{
"collection": "notes",
"filesProcessed": 42,
"filesAdded": 5,
"filesUpdated": 3,
"filesUnchanged": 34,
"filesErrored": 0,
"filesSkipped": 0,
"durationMs": 1250
}
],
"totalDurationMs": 1250,
"totalFilesProcessed": 42,
"totalFilesAdded": 5,
"totalFilesUpdated": 3,
"totalFilesErrored": 0,
"totalFilesSkipped": 0
}
}
Response (failed):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "add",
"status": "failed",
"createdAt": 1704067200000,
"error": "Permission denied: /private/folder"
}
| Status | Description |
|---|---|
running |
Job in progress |
completed |
Job finished successfully |
failed |
Job failed with error |
Example:
# Poll until complete
JOB_ID="550e8400-e29b-41d4-a716-446655440000"
while true; do
STATUS=$(curl -s "http://localhost:3000/api/jobs/$JOB_ID" | jq -r '.status')
echo "Status: $STATUS"
[ "$STATUS" != "running" ] && break
sleep 1
done
Active Job
GET /api/jobs/active
Return the current active background job in structured form, or null when the
server is idle.
Response (idle):
{
"activeJob": null
}
Response (running):
{
"activeJob": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "sync",
"status": "running",
"createdAt": 1704067200000
}
}
Use this when a client needs the active job id without scraping it out of a
409 CONFLICT message.
List Tags
GET /api/tags?collection=notes&prefix=project
List all tags with document counts.
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
collection |
string | — | Filter by collection name |
prefix |
string | — | Filter by tag prefix (hierarchical) |
Response:
{
"tags": [
{ "tag": "work", "count": 15 },
{ "tag": "project/alpha", "count": 8 },
{ "tag": "urgent", "count": 3 }
],
"meta": {
"total": 3,
"collection": "notes",
"prefix": "project"
}
}
Example:
# All tags
curl http://localhost:3000/api/tags | jq
# Tags in collection
curl "http://localhost:3000/api/tags?collection=notes" | jq
# Tags with prefix
curl "http://localhost:3000/api/tags?prefix=project" | jq
List Documents
GET /api/docs?collection=notes&limit=20&offset=0&tagsAll=work&tagsAny=urgent,meeting&sortField=published_at&sortOrder=desc
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
collection |
string | — | Filter by collection name |
limit |
number | 20 | Results per page (max 100) |
offset |
number | 0 | Pagination offset |
tagsAll |
string | — | Comma-separated tags (must have ALL) |
tagsAny |
string | — | Comma-separated tags (must have ANY) |
sortField |
string | modified | modified or frontmatter date key |
sortOrder |
string | desc | asc or desc |
Response:
{
"documents": [
{
"docid": "abc123def456",
"uri": "gno://notes/projects/readme.md",
"title": "Project README",
"collection": "notes",
"relPath": "projects/readme.md",
"sourceExt": ".md",
"sourceMime": "text/markdown",
"updatedAt": "2025-01-15T09:00:00Z"
}
],
"total": 142,
"limit": 20,
"offset": 0,
"availableDateFields": ["deadline", "published_at"],
"sortField": "published_at",
"sortOrder": "desc"
}
Example:
curl "http://localhost:3000/api/docs?collection=notes&limit=10" | jq
Get Document
GET /api/doc?uri=gno://notes/projects/readme.md
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
uri |
string | Yes | Document URI |
Response:
{
"docid": "abc123def456",
"uri": "gno://notes/projects/readme.md",
"title": "Project README",
"content": "# Project\n\nThis is the full document content...",
"contentAvailable": true,
"collection": "notes",
"relPath": "projects/readme.md",
"tags": ["work", "project/alpha"],
"source": {
"absPath": "/Users/you/notes/projects/readme.md",
"mime": "text/markdown",
"ext": ".md",
"modifiedAt": "2025-01-15T09:00:00Z",
"sizeBytes": 4523,
"sourceHash": "7b3c..."
},
"capabilities": {
"editable": true,
"tagsEditable": true,
"tagsWriteback": true,
"canCreateEditableCopy": false,
"mode": "editable"
}
}
For converted source formats such as PDF or DOCX, capabilities.editable is false and capabilities.canCreateEditableCopy is true. Those documents remain viewable/searchable, but GNO will not write converted markdown back into the original binary source file.
Example:
curl "http://localhost:3000/api/doc?uri=gno://notes/readme.md" | jq '.content'
Document Autocomplete
GET /api/docs/autocomplete?query=auth&collection=notes&limit=8
Returns lightweight document suggestions for title/path-driven UIs such as wiki-link autocomplete and the quick switcher.
Document Events
GET /api/events
Server-sent event stream used by the Web UI to refresh document/search state after local edits and external file changes.
Get Document Links
GET /api/doc/:id/links?type=wiki
Get outgoing links from a document (wiki links and markdown links).
URL Parameters:
| Param | Description |
|---|---|
:id |
Document ID (the #hexhash from docid, URL-encoded as %23hexhash) |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
type |
string | — | Filter by link type: wiki or markdown |
Response:
{
"links": [
{
"targetRef": "Other Note",
"targetRefNorm": "other note",
"linkType": "wiki",
"startLine": 5,
"startCol": 1,
"endLine": 5,
"endCol": 17,
"source": "parsed",
"resolved": true,
"resolvedDocid": "#def456",
"resolvedUri": "gno://notes/other.md",
"resolvedTitle": "Other Note"
},
{
"targetRef": "./related.md",
"targetRefNorm": "related.md",
"targetAnchor": "section",
"linkType": "markdown",
"linkText": "see related",
"startLine": 10,
"startCol": 1,
"endLine": 10,
"endCol": 30,
"source": "parsed"
}
],
"meta": {
"docid": "#abc123",
"totalLinks": 2,
"resolvedCount": 1,
"resolutionAvailable": true,
"typeFilter": "wiki"
}
}
Resolution fields are only included when meta.resolutionAvailable is true.
| Meta Field | Description |
|---|---|
resolvedCount |
Number of links resolved |
resolutionAvailable |
Whether resolution completed normally |
Example:
# All links
curl "http://localhost:3000/api/doc/%23abc123/links" | jq
# Only wiki links
curl "http://localhost:3000/api/doc/%23abc123/links?type=wiki" | jq
Get Document Backlinks
GET /api/doc/:id/backlinks
Get documents that link TO this document.
URL Parameters:
| Param | Description |
|---|---|
:id |
Document ID (the #hexhash from docid, URL-encoded as %23hexhash) |
Response:
{
"backlinks": [
{
"sourceDocid": "#def456",
"sourceUri": "gno://notes/source.md",
"sourceTitle": "Source Document",
"linkText": "see also",
"startLine": 10,
"startCol": 5
}
],
"meta": {
"docid": "#abc123",
"totalBacklinks": 1
}
}
| Field | Description |
|---|---|
sourceDocid |
Docid of the linking document |
sourceUri |
URI of the linking document |
sourceTitle |
Title of the linking document |
linkText |
Display text of the link |
startLine |
Line number where link appears |
Example:
curl "http://localhost:3000/api/doc/%23abc123/backlinks" | jq
Find Similar Documents
GET /api/doc/:id/similar?limit=5&threshold=0.7&crossCollection=true
Find semantically similar documents using vector embeddings. Uses the doc’s
seq=0 embedding (falls back to first chunk).
URL Parameters:
| Param | Description |
|---|---|
:id |
Document ID (the #hexhash from docid, URL-encoded as %23hexhash) |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit |
number | 5 | Max results (1-20) |
threshold |
number | 0.5 | Min similarity score (0-1) |
crossCollection |
boolean | false | Search across all collections |
Response:
{
"similar": [
{
"docid": "#def456",
"uri": "gno://notes/similar.md",
"title": "Similar Note",
"collection": "notes",
"score": 0.85
},
{
"docid": "#ghi789",
"uri": "gno://notes/related.md",
"score": 0.72
}
],
"meta": {
"docid": "#abc123",
"totalResults": 2,
"threshold": 0.7,
"crossCollection": true
}
}
| Field | Description |
|---|---|
score |
Similarity score (0-1, higher = more similar) |
Errors:
| Code | Status | Description |
|---|---|---|
NOT_FOUND |
404 | Document not found |
UNAVAILABLE |
503 | Vector search not available. Run gno embed |
Example:
# Find similar docs in same collection
curl "http://localhost:3000/api/doc/%23abc123/similar?limit=10" | jq
# Find similar across all collections
curl "http://localhost:3000/api/doc/%23abc123/similar?crossCollection=true&threshold=0.6" | jq
Get Knowledge Graph
GET /api/graph
Returns a knowledge graph of document links (wiki links, markdown links, and optionally similarity edges).
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
collection |
string | - | Filter to single collection |
limit |
number | 2000 | Max nodes (1-5000) |
edgeLimit |
number | 10000 | Max edges (1-50000) |
includeSimilar |
boolean | false | Include similarity edges |
threshold |
number | 0.7 | Similarity threshold (0-1) |
linkedOnly |
boolean | true | Exclude isolated nodes (no links) |
similarTopK |
number | 5 | Similar docs per node (1-20) |
Note: When
collectionis specified, nodes are limited to that collection and edges are drawn only between those nodes, but nodedegreemay reflect links to documents outside the filtered view. Note: Similarity edges useseq=0embeddings only (no fallback).
Response:
{
"nodes": [
{
"id": "#abc123",
"uri": "gno://notes/readme.md",
"title": "Project README",
"collection": "notes",
"relPath": "readme.md",
"degree": 5
}
],
"links": [
{
"source": "#abc123",
"target": "#def456",
"type": "wiki",
"weight": 1
},
{
"source": "#abc123",
"target": "#ghi789",
"type": "similar",
"weight": 0.85
}
],
"meta": {
"collection": null,
"nodeLimit": 2000,
"edgeLimit": 10000,
"totalNodes": 42,
"totalEdges": 67,
"totalEdgesUnresolved": 0,
"returnedNodes": 42,
"returnedEdges": 67,
"truncated": false,
"linkedOnly": true,
"includedSimilar": false,
"similarAvailable": true,
"similarTopK": 5,
"similarTruncatedByComputeBudget": false,
"warnings": []
}
}
| Field | Description |
|---|---|
nodes[].id |
Document ID (hash) |
nodes[].uri |
Virtual URI |
nodes[].title |
Document title |
nodes[].collection |
Source collection |
nodes[].relPath |
Relative path in collection |
nodes[].degree |
Number of connections (in + out) |
links[].source |
Source node ID |
links[].target |
Target node ID |
links[].type |
Link type: wiki, markdown, or similar |
links[].weight |
Edge weight (count for links, score for similar) |
meta.truncated |
True if results hit limit |
meta.similarAvailable |
True if similarity edges can be computed |
Example:
# Get graph for notes collection
curl "http://localhost:3000/api/graph?collection=notes" | jq
# Include similarity edges with 0.8 threshold
curl "http://localhost:3000/api/graph?includeSimilar=true&threshold=0.8" | jq
# Get all nodes including isolated ones
curl "http://localhost:3000/api/graph?linkedOnly=false&limit=500" | jq
Create Document
POST /api/docs
Create a new document file in a collection. Triggers background sync to index it.
Request Body:
{
"collection": "notes",
"relPath": "ideas/new-feature.md",
"content": "# New Feature\n\nDescription of the feature...",
"overwrite": false
}
| Field | Type | Required | Description |
|---|---|---|---|
collection |
string | Yes | Target collection name |
relPath |
string | Yes | Relative path within collection |
content |
string | Yes | File content (markdown) |
overwrite |
boolean | No | Overwrite if exists (default: false) |
Response (202 Accepted):
{
"uri": "file:///Users/you/notes/ideas/new-feature.md",
"path": "/Users/you/notes/ideas/new-feature.md",
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"note": "File created. Sync job started - poll /api/jobs/:id for status."
}
Errors:
| Code | Status | Description |
|---|---|---|
VALIDATION |
400 | Missing collection, relPath, or content |
NOT_FOUND |
404 | Collection does not exist |
CONFLICT |
409 | File exists and overwrite=false |
Path Validation:
relPathmust be relative (no leading/)- Path traversal (
..) is rejected - Null bytes are rejected
Example:
curl -X POST http://localhost:3000/api/docs \
-H "Content-Type: application/json" \
-d '{
"collection": "notes",
"relPath": "journal/2025-01-01.md",
"content": "# January 1st\n\nNew year, new notes!"
}'
Update Document
PUT /api/docs/:id
Update an existing document’s content. Triggers background sync to re-index.
URL Parameters:
| Param | Description |
|---|---|
:id |
Document ID (the #hexhash from docid, URL-encoded as %23hexhash) |
Request Body:
{
"content": "# Updated Content\n\nNew document content...",
"tags": ["work", "project/alpha", "urgent"]
}
| Field | Type | Required | Description |
|---|---|---|---|
content |
string | No* | New file content |
tags |
string[] | No* | Tags to set (replaces frontmatter tags on write) |
*At least one of content or tags must be provided.
When tags is provided, the tags are written to the document’s YAML frontmatter. If the document has no frontmatter, one is added. If it already has a tags: field, it is replaced.
Response:
{
"success": true,
"docId": "#abc123",
"uri": "file:///Users/you/notes/projects/readme.md",
"path": "/Users/you/notes/projects/readme.md",
"jobId": "550e8400-e29b-41d4-a716-446655440000"
}
Errors:
| Code | Status | Description |
|---|---|---|
VALIDATION |
400 | Missing or invalid content |
READ_ONLY |
409 | Source format cannot be edited in place |
NOT_FOUND |
404 | Document not found in index |
FILE_NOT_FOUND |
404 | Source file no longer exists |
CONFLICT |
409 | Sync job already running |
RUNTIME |
500 | Failed to write file |
Example:
# Note: # must be URL-encoded as %23
curl -X PUT "http://localhost:3000/api/docs/%23abc123" \
-H "Content-Type: application/json" \
-d '{"content": "# Updated\n\nNew content here."}'
For read-only converted documents, create a markdown note instead:
POST /api/docs/:id/editable-copy
This creates a new markdown document using the converted content plus source provenance frontmatter. The original PDF/DOCX/etc. is left untouched.
Deactivate Document
POST /api/docs/:id/deactivate
Remove a document from the index. The file remains on disk.
URL Parameters:
| Param | Description |
|---|---|
:id |
Document ID (the #hexhash from docid, URL-encoded as %23hexhash) |
Response:
{
"success": true,
"docId": "#abc123",
"path": "gno://notes/old-file.md",
"warning": "File still exists on disk. Will be re-indexed unless excluded."
}
Errors:
| Code | Status | Description |
|---|---|---|
NOT_FOUND |
404 | Document not found |
Example:
# Note: # must be URL-encoded as %23
curl -X POST "http://localhost:3000/api/docs/%23abc123/deactivate"
Note: The document will be re-indexed on next sync unless you add it to the collection’s exclude pattern.
BM25 Search
POST /api/search
Keyword search using BM25 algorithm.
Request Body:
{
"query": "authentication",
"limit": 10,
"minScore": 0.1,
"collection": "notes",
"intent": "web authentication and session security",
"exclude": "hiring,reviews",
"since": "last month",
"until": "today",
"category": "meeting,notes",
"author": "gordon",
"tagsAll": "work,project",
"tagsAny": "urgent,high"
}
| Field | Type | Default | Description |
|---|---|---|---|
query |
string | — | Search query (required) |
limit |
number | 10 | Max results (max 50) |
minScore |
number | — | Minimum score threshold (0-1) |
collection |
string | — | Filter by collection |
intent |
string | — | Disambiguating context for ambiguous queries; steers snippet choice without being searched directly |
exclude |
string | — | Comma-separated exclusion terms; matching docs are hard-pruned by title/path/body |
since |
string | — | Modified-at lower bound (ISO date/time or token) |
until |
string | — | Modified-at upper bound (ISO date/time or token) |
category |
string | — | Comma-separated category/content-type filters (ANY match) |
author |
string | — | Author contains value (case-insensitive) |
tagsAll |
string | — | Comma-separated tags (must have ALL) |
tagsAny |
string | — | Comma-separated tags (must have ANY) |
If query text includes recency intent (latest, newest, recent), results are ordered newest-first by canonical frontmatter date when present, otherwise by source modified time.
Response:
{
"query": "authentication",
"mode": "bm25",
"results": [
{
"docid": "abc123",
"uri": "gno://notes/auth.md",
"title": "Authentication Guide",
"collection": "notes",
"tags": ["backend", "auth"],
"score": 0.87,
"chunk": {
"text": "...relevant text snippet...",
"index": 2
}
}
],
"meta": {
"totalResults": 5
}
}
Example:
curl -X POST http://localhost:3000/api/search \
-H "Content-Type: application/json" \
-d '{"query": "handleAuth", "limit": 5}'
Hybrid Search
POST /api/query
Combined BM25 + vector search with optional reranking. Recommended for best results.
Request Body:
{
"query": "how to handle authentication errors",
"limit": 20,
"minScore": 0.1,
"collection": "notes",
"lang": "en",
"intent": "web authentication and request latency",
"candidateLimit": 12,
"exclude": "hiring,reviews",
"since": "2025-01-01",
"until": "today",
"category": "backend,meeting",
"author": "gordon",
"queryModes": [
{ "mode": "term", "text": "\"refresh token\" -oauth1" },
{ "mode": "intent", "text": "how token rotation is implemented" },
{
"mode": "hyde",
"text": "Refresh tokens are rotated on every use and prior tokens are invalidated."
}
],
"noExpand": false,
"noRerank": false,
"tagsAll": "backend",
"tagsAny": "auth,security"
}
| Field | Type | Default | Description |
|---|---|---|---|
query |
string | — | Search query (required) |
limit |
number | 20 | Max results (max 50) |
minScore |
number | — | Minimum score threshold (0-1) |
collection |
string | — | Filter by collection |
lang |
string | auto | Query language hint |
intent |
string | — | Disambiguating context for ambiguous queries; steers expansion, reranking, and snippet selection without being searched directly |
candidateLimit |
number | 20 | Max candidates sent to reranking (max 100) |
exclude |
string | — | Comma-separated exclusion terms; matching docs are hard-pruned by title/path/body |
since |
string | — | Modified-at lower bound (ISO date/time or token) |
until |
string | — | Modified-at upper bound (ISO date/time or token) |
category |
string | — | Comma-separated category/content-type filters (ANY match) |
author |
string | — | Author contains value (case-insensitive) |
queryModes |
array | — | Optional structured mode entries (term, intent, hyde) |
noExpand |
boolean | false | Disable query expansion |
noRerank |
boolean | false | Disable cross-encoder reranking |
tagsAll |
string | — | Comma-separated tags (must have ALL) |
tagsAny |
string | — | Comma-separated tags (must have ANY) |
Compatibility notes:
- Existing
/api/querypayloads remain valid. intentis orthogonal toqueryModes: intent steers scoring/prompting, while query modes inject caller-provided retrieval expansions.queryModesis optional and only needed for explicit retrieval intent control.- If
queryModesis provided, generated expansion is skipped and provided entries are used directly. querycan also be a multi-line structured query document usingterm:,intent:, andhyde:lines. See Structured Query Syntax.
Response:
{
"query": "how to handle authentication errors",
"mode": "hybrid",
"queryLanguage": "en",
"results": [
{
"docid": "abc123",
"uri": "gno://notes/auth.md",
"title": "Authentication Guide",
"collection": "notes",
"tags": ["backend", "auth"],
"score": 0.92,
"chunk": {
"text": "...relevant text snippet...",
"index": 2
}
}
],
"meta": {
"expanded": true,
"reranked": true,
"vectorsUsed": true,
"totalResults": 12
}
}
Example:
curl -X POST http://localhost:3000/api/query \
-H "Content-Type: application/json" \
-d '{"query": "error handling best practices", "limit": 10}'
curl -X POST http://localhost:3000/api/query \
-H "Content-Type: application/json" \
-d '{"query": "auth flow\nterm: \"refresh token\"\nintent: token rotation"}'
AI Answer
POST /api/ask
Get an AI-generated answer with citations from your documents.
Request Body:
{
"query": "What is our authentication strategy?",
"limit": 5,
"collection": "notes",
"lang": "en",
"intent": "web authentication and request latency",
"candidateLimit": 12,
"exclude": "hiring,reviews",
"queryModes": [
{ "mode": "term", "text": "\"refresh token\" -oauth1" },
{ "mode": "intent", "text": "how token rotation is implemented" }
],
"since": "last month",
"until": "today",
"category": "backend,notes",
"author": "gordon",
"maxAnswerTokens": 512,
"noExpand": false,
"noRerank": false,
"tagsAll": "backend",
"tagsAny": "auth,security"
}
| Field | Type | Default | Description |
|---|---|---|---|
query |
string | — | Question (required) |
limit |
number | 5 | Number of sources to consider (max 20) |
collection |
string | — | Filter by collection |
lang |
string | auto | Query language hint |
intent |
string | — | Disambiguating context for ambiguous questions without searching on that text |
candidateLimit |
number | 20 | Max candidates sent to reranking (max 100) |
exclude |
string | — | Comma-separated exclusion terms; matching docs are hard-pruned by title/path/body |
queryModes |
array | — | Optional structured mode entries (term, intent, hyde) |
since |
string | — | Modified-at lower bound (ISO date/time or token) |
until |
string | — | Modified-at upper bound (ISO date/time or token) |
category |
string | — | Comma-separated category/content-type filters (ANY match) |
author |
string | — | Author contains value (case-insensitive) |
maxAnswerTokens |
number | 512 | Max tokens in answer |
noExpand |
boolean | false | Disable query expansion |
noRerank |
boolean | false | Disable cross-encoder reranking |
tagsAll |
string | — | Comma-separated tags (must have ALL) |
tagsAny |
string | — | Comma-separated tags (must have ANY) |
Compatibility notes:
- Existing
/api/askpayloads remain valid. queryModesis optional and only needed for explicit retrieval steering during Q&A.- If
queryModesis provided, generated expansion is skipped and provided entries are used directly. querycan also be a multi-line structured query document usingterm:,intent:, andhyde:lines. See Structured Query Syntax.
Response:
{
"query": "What is our authentication strategy?",
"mode": "hybrid",
"queryLanguage": "en",
"answer": "Based on your documents, the authentication strategy uses JWT tokens with refresh rotation. Key points:\n\n1. Access tokens expire in 15 minutes [1]\n2. Refresh tokens are rotated on each use [2]\n3. Sessions are stored in Redis [1]",
"citations": [
{ "docid": "#abc123", "uri": "gno://notes/auth.md" },
{ "docid": "#def456", "uri": "gno://notes/security.md" }
],
"results": [...],
"meta": {
"expanded": true,
"reranked": true,
"vectorsUsed": true,
"answerGenerated": true,
"totalResults": 5,
"answerContext": {
"strategy": "adaptive_coverage_v1",
"targetSources": 4,
"facets": ["authentication strategy", "session storage"],
"selected": [
{
"docid": "#abc123",
"uri": "gno://notes/auth.md",
"score": 0.94,
"queryTokenHits": 4,
"facetHits": 2,
"reason": "new_facet_coverage"
}
],
"dropped": []
}
}
}
Example:
curl -X POST http://localhost:3000/api/ask \
-H "Content-Type: application/json" \
-d '{"query": "What did we decide about caching?"}'
Note: Returns
503if generation model not loaded. Rungno models pullto download.
List Presets
GET /api/presets
Response:
{
"presets": [
{
"id": "slim-tuned",
"name": "GNO Slim Tuned (Default, ~1GB)",
"embed": "hf:...Qwen3-Embedding-0.6B-Q8_0...",
"rerank": "hf:...reranker-Q4...",
"expand": "hf:...gno-expansion-auto-entity-lock-default-mix...",
"gen": "hf:...qwen3-4b-Q4...",
"active": true
},
{
"id": "slim",
"name": "Slim (~1GB)",
"active": false
},
{
"id": "balanced",
"name": "Balanced (~2GB)",
"active": false
}
],
"activePreset": "slim-tuned"
}
Switch Preset
POST /api/presets
Switch to a different model preset. Reloads models automatically.
Request Body:
{
"presetId": "quality"
}
Response:
{
"success": true,
"activePreset": "quality",
"embedModelChanged": true,
"note": "Embedding model changed. Existing collections may need gno embed so vector results catch up.",
"capabilities": {
"bm25": true,
"vector": true,
"hybrid": true,
"answer": true
}
}
If embedModelChanged is true, old vectors are kept but no longer count toward
the active embedding model’s backlog/readiness. Run gno embed (or re-index in
the Web UI) so vector and hybrid search catch up.
Example:
curl -X POST http://localhost:3000/api/presets \
-H "Content-Type: application/json" \
-d '{"presetId": "quality"}'
Model Download Status
GET /api/models/status
Check the status of model downloads.
Response:
{
"active": true,
"currentType": "embed",
"progress": {
"downloadedBytes": 104857600,
"totalBytes": 524288000,
"percent": 20
},
"completed": [],
"failed": [],
"startedAt": 1706000000000
}
| Field | Description |
|---|---|
active |
Whether download is in progress |
currentType |
Current model: embed, gen, or rerank |
progress |
Download progress for current model |
completed |
Successfully downloaded model types |
failed |
Failed downloads with error messages |
Start Model Download
POST /api/models/pull
Start downloading models for the active preset. Returns immediately and downloads in background. Poll /api/models/status for progress.
Response:
{
"started": true,
"message": "Download started. Poll /api/models/status for progress."
}
Error (download already in progress):
{
"error": {
"code": "CONFLICT",
"message": "Download already in progress"
}
}
Example:
# Start download
curl -X POST http://localhost:3000/api/models/pull
# Poll status until complete
while true; do
curl -s http://localhost:3000/api/models/status | jq
sleep 2
done
Error Responses
All errors follow a consistent format:
{
"error": {
"code": "VALIDATION",
"message": "Missing or invalid query"
}
}
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION |
400 | Invalid request parameters |
PATH_NOT_FOUND |
400 | Specified path does not exist |
HAS_REFERENCES |
400 | Resource has dependencies (e.g., collection in contexts) |
CSRF_VIOLATION |
403 | Cross-origin request rejected |
NOT_FOUND |
404 | Resource not found |
DUPLICATE |
409 | Resource already exists |
CONFLICT |
409 | Operation already in progress |
UNAVAILABLE |
503 | Feature not available (model not loaded) |
RUNTIME |
500 | Internal error |
Usage Examples
Search from a Script
#!/bin/bash
# search.sh - Search GNO from command line
QUERY="$1"
curl -s -X POST http://localhost:3000/api/query \
-H "Content-Type: application/json" \
-d "{\"query\": \"$QUERY\", \"limit\": 5}" \
| jq -r '.results[] | "\(.score | tostring | .[0:4]) \(.title)"'
Python Integration
import requests
def search_gno(query: str, limit: int = 10) -> list:
"""Search GNO index."""
resp = requests.post(
"http://localhost:3000/api/query",
json={"query": query, "limit": limit}
)
resp.raise_for_status()
return resp.json()["results"]
def ask_gno(question: str) -> str:
"""Get AI answer from GNO."""
resp = requests.post(
"http://localhost:3000/api/ask",
json={"query": question}
)
resp.raise_for_status()
return resp.json().get("answer", "No answer generated")
# Usage
results = search_gno("authentication patterns")
answer = ask_gno("What is our deployment process?")
JavaScript/TypeScript
async function searchGno(query: string): Promise<SearchResult[]> {
const resp = await fetch("http://localhost:3000/api/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, limit: 10 }),
});
const data = await resp.json();
return data.results;
}
async function askGno(question: string): Promise<string> {
const resp = await fetch("http://localhost:3000/api/ask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: question }),
});
const data = await resp.json();
return data.answer ?? "No answer generated";
}
Raycast Script Command
#!/bin/bash
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title Search Notes
# @raycast.mode fullOutput
# @raycast.argument1 { "type": "text", "placeholder": "Query" }
curl -s -X POST http://localhost:3000/api/query \
-H "Content-Type: application/json" \
-d "{\"query\": \"$1\", \"limit\": 5}" \
| jq -r '.results[] | "• \(.title)\n \(.chunk.text | .[0:100])...\n"'
Rate Limits
None. The API runs locally with no rate limiting. Performance depends on your hardware and model configuration.
See Also
- Web UI Guide: Visual interface documentation
- CLI Reference: Command-line interface
- MCP Integration: AI assistant integration