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

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/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

Authentication

None required. The API binds to 127.0.0.1 only and is not accessible from the network.

Security: Cross-origin requests are blocked. Only same-origin requests from localhost are allowed.


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" }
]

List Documents

GET /api/docs?collection=notes&limit=20&offset=0

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

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
}

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",
  "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'

POST /api/search

Keyword search using BM25 algorithm.

Request Body:

{
  "query": "authentication",
  "limit": 10,
  "minScore": 0.1,
  "collection": "notes"
}
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

Response:

{
  "query": "authentication",
  "mode": "bm25",
  "results": [
    {
      "docid": "abc123",
      "uri": "gno://notes/auth.md",
      "title": "Authentication Guide",
      "collection": "notes",
      "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",
  "noExpand": false,
  "noRerank": false
}
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
noExpand boolean false Disable query expansion
noRerank boolean false Disable cross-encoder reranking

Response:

{
  "query": "how to handle authentication errors",
  "mode": "hybrid",
  "queryLanguage": "en",
  "results": [
    {
      "docid": "abc123",
      "uri": "gno://notes/auth.md",
      "title": "Authentication Guide",
      "collection": "notes",
      "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",
  "maxAnswerTokens": 512
}
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
maxAnswerTokens number 512 Max tokens in answer

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": [
    { "index": 1, "docid": "abc123", "uri": "gno://notes/auth.md" },
    { "index": 2, "docid": "def456", "uri": "gno://notes/security.md" }
  ],
  "results": [...],
  "meta": {
    "expanded": true,
    "reranked": true,
    "vectorsUsed": true,
    "answerGenerated": true,
    "totalResults": 5
  }
}

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 (Fast, ~1GB)",
      "embed": "hf:...bge-m3-Q4...",
      "rerank": "hf:...reranker-Q4...",
      "gen": "hf:...smollm-Q4...",
      "active": false
    },
    {
      "id": "balanced",
      "name": "Balanced (Default, ~2GB)",
      "active": true
    }
  ],
  "activePreset": "balanced"
}

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
NOT_FOUND 404 Resource not found
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