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:

  1. Same-origin request: No Origin header (curl, scripts)
  2. Valid Origin: Origin: http://localhost:<port> or http://127.0.0.1:<port>
  3. API Token: X-GNO-Token header (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 Origin header (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 /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"
  }
}
Field Description
targetRef Target path or wiki name
linkType wiki ([[Name]]) or markdown ()
targetAnchor Fragment/anchor without #
linkText Display text of the link
source parsed, user, or suggested
resolved Whether target doc exists in index
resolvedDocid Docid of resolved target (if found)
resolvedUri URI of resolved target (if found)
resolvedTitle Title of resolved target (if found)

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 /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 collection is specified, nodes are limited to that collection and edges are drawn only between those nodes, but node degree may reflect links to documents outside the filtered view. Note: Similarity edges use seq=0 embeddings 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:

  • relPath must 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.


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

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/query payloads remain valid.
  • queryModes is optional and only needed for explicit retrieval intent control.
  • If queryModes is 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 503 if generation model not loaded. Run gno models pull to 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