REST API
HTTP API for programmatic access to GNO search and retrieval.
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 |
/api/capabilities |
GET | Available features |
/api/collections |
GET | List collections |
/api/docs |
GET | List documents |
/api/doc |
GET | Get document content |
/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/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/deactivate |
POST | Unindex document |
/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 and health.
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,
"lastUpdated": "2025-01-15T10:30:00Z",
"healthy": true
}
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" },
{ "name": "work", "path": "/Users/you/work/docs" }
]
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
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"
}
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
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": {
"mime": "text/markdown",
"ext": ".md",
"modifiedAt": "2025-01-15T09:00:00Z",
"sizeBytes": 4523
}
}
Example:
curl "http://localhost:3000/api/doc?uri=gno://notes/readme.md" | jq '.content'
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 |
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."}'
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",
"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 |
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",
"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 |
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. queryModesis optional and only needed for explicit retrieval intent control.- If
queryModesis provided, generated expansion is skipped and provided entries are used directly.
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}'
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",
"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 |
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) |
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",
"name": "Slim (Default, ~1GB)",
"embed": "hf:...bge-m3-Q4...",
"rerank": "hf:...reranker-Q4...",
"gen": "hf:...qwen3-1.7b-Q4...",
"active": true
},
{
"id": "balanced",
"name": "Balanced (~2GB)",
"active": false
}
],
"activePreset": "slim"
}
Switch Preset
POST /api/presets
Switch to a different model preset. Reloads models automatically.
Request Body:
{
"presetId": "quality"
}
Response:
{
"success": true,
"activePreset": "quality",
"capabilities": {
"bm25": true,
"vector": true,
"hybrid": true,
"answer": true
}
}
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