Import GNO directly into a Bun or TypeScript app with createGnoClient and the same local retrieval engine.
The SDK is for apps that want GNO in-process. Use it when a local Bun or TypeScript app needs search, retrieval, document access, indexing, or graph navigation without shelling out to gno or running gno serve.
bun add @gmickel/gnoimport { createDefaultConfig, createGnoClient } from "@gmickel/gno"
const config = createDefaultConfig()
config.collections = [{
name: "notes",
path: "/Users/me/notes",
pattern: "**/*",
include: [],
exclude: [],
}]
const client = await createGnoClient({
config,
dbPath: "/tmp/gno-sdk.sqlite",
})
try {
await client.index({ noEmbed: true })
const results = await client.search("authentication")
for (const hit of results.results) {
console.log(hit.uri, hit.score)
}
} finally {
await client.close()
}The client runs indexing and search in your process. Local models run in a helper process that GNO starts and stops for you. Always close the client in finally; if you create an adapter yourself, call LlmAdapter.dispose() when done. Loaded models stay warm for five minutes of inactivity by default, and the next inference after that reloads them. Metadata calls do not keep them warm. GNO does not retry a failed local model call on its own; call again to retry.
Without dbPath, the client opens the normal per-index location. If your config omits collections that location already holds, createGnoClient fails with an error naming them and changes nothing, so give a test or sandbox config its own dbPath. A long-running client refuses calls the same way if another client later indexes a collection its config omits.
search, vsearch, query, and ask accept signal?: AbortSignal and deadlineAt?: number in their options. The deadline is an absolute Unix epoch time in milliseconds spanning admission, queueing, loading, evaluation, and publication. Nested retrieval and verification cannot relax it. Caller abort uses INFERENCE_FAILED with an AbortError cause; expiry uses TIMEOUT with a TimeoutError cause. An aborted or expired call never returns a late result. Model work that was already running keeps its capacity until it finishes.
Supported owner filters apply before candidate budgets, including matching-language chunks for vector/hybrid retrieval. Short or empty results are valid. Hybrid fallback diagnostics distinguish unavailable semantic work from successful vector retrieval with no matches. Ask keeps its existing evidence, context, and generation budgets.
Local embedding checks the model file, dimensions, context and truncation settings, and runtime before it embeds. HTTP and custom embedding ports may not report a verified model identity. GNO does not reuse stored vectors under an unverified or different identity.
client.search(query, opts?) — BM25 keyword search for exact words, phrases, filenames, and identifiers.client.vsearch(query, opts?) — vector similarity for conceptual matches.client.query(query, opts?) — hybrid retrieval with expansion, default bounded graph candidates, fusion, and reranking controls. Pass { graph: false } to skip graph expansion or { explain: true } for a retrieval scoring receipt.client.ask(question, opts?) — retrieval-only or cited answer generation. Pass { verify: true } for the closed-Capsule claim-verification path and { explain: true } for the same retrieval scoring receipt.client.get(ref) and client.multiGet(refs) — fetch indexed documents by URI, doc ID, or path.client.getSections(ref) — heading outline for a document. Readable anchors stay compatible with the Web UI.client.createSectionTarget(ref, selector) and client.resolveSectionTarget(ref, target) — durable section locators with the same fail-closed citation semantics as REST and MCP. Prefer ordinary get/search for default retrieval.client.links(ref), client.backlinks(ref), client.similar(ref) — navigate direct and semantic relationships.client.list() and client.status() — inspect document inventory and index health. A collection whose folder is missing or unreadable right now carries rootError ({ code, message }) and makes healthy false; its documents stay indexed and searchable.client.listMetadata({ collection, values }) — typed metadata keys, types, document counts and common values.client.capture(input) — write a note with provenance and an optional presetId, then return the same capture receipt shape as CLI, API, and MCP. Content is text-only, collision policies are runtime-validated, legacy overwrite is not accepted, and writes fail on late-arriving files. A lexical sync failure still resolves with sync.status: "failed" when no requestId is sent.client.requestStatus(requestId) — look up a requestId sent with client.capture() or client.remember() before retrying it. See Retry-safe writes.client.previewRenameNote() / client.previewMoveNote(), followed by client.renameNote() / client.moveNote() — inspect a canonical impact plan, then apply only its exact version, digest, and confirmation. Supported reference edits and the source move share one all-or-rollback filesystem boundary.client.update(), client.embed(), client.index() — sync files, embed chunks, or do both.client.close() — release file handles and SQLite connections.search, vsearch, and query accept collections to search several collections at once, filtered before the result limit. Names are trimmed, lowercased, de-duplicated, and sorted, and the result lists them in meta.collections. Passing both collection and collections, an empty list, more than 256 names, or a name that is not configured throws a GnoSdkError with code VALIDATION. ask does not take collections.
const scoped = await client.query("rollout plan", {
collections: ["work", "ops"],
})
scoped.meta.collections // ["ops", "work"]convertFile(path) converts one file without a client, config, or index. It returns the same object as gno convert --json and throws a GnoSdkError with the reason in details.reason.
import { convertFile } from "@gmickel/gno"
const { markdown, mirrorHash } = await convertFile("report.pdf")Retrieval options may include up to 16 projectHints. SDK hints are opaque and untrusted, are never resolved or reflected, never trigger filesystem access, and currently have zero ranking effect. Leaving the option absent preserves existing results exactly.
Optional chunking in the client config sets index-wide maxTokens and overlapPercent. Update/index apply it to cached mirrors, and (await client.status()).chunking reports configured/applied parameters and pending layout counts. Reopen the client after CHUNKING_POLICY_CONFLICT. See chunking configuration for defaults and separate-index comparisons.
const verified = await client.ask("Who owns the launch decision?", {
verify: true,
contextBudgetTokens: 12_000,
})
if (verified.verification?.claims.answerStatus === "abstained") {
console.log(verified.verification.claims.abstentionReason)
}Verified SDK results use the same contract as CLI, REST, MCP, and Web: the closed Capsule and freshness receipt, exact evidence IDs, lines, and hashes, four-state claim verdicts, 100% support threshold, and explicit semantic-verifier degradation. Raw client.ask() behavior remains compatible when verify is not true.
Indexed URIs such as gno://notes/plan.md?index=research select and read that named database, even when the client was created for another index. Missing indexes fail without being created. Keep each multiGet() batch on one index.
When private retrieval tracing is enabled, search, query, ask, get, and Context Capsule results carry a non-enumerable RETRIEVAL_TRACE_METADATA symbol. It exposes the local receipt ID without changing JSON output. Read it with getRetrievalTraceMetadata(result), then pass traceId to client.get() to record the exact opened line range against an open query receipt.
const receipt = await client.capture({
collection: "notes",
title: "Customer call",
content: "Follow up on renewal timeline.",
presetId: "meeting",
tags: ["customer", "follow-up"],
source: {
kind: "meeting",
title: "Customer call",
},
})
console.log(receipt.uri, receipt.contentHash)const plan = await client.previewRenameNote({
ref: "gno://notes/old-note.md",
name: "new-note.md",
})
if (plan.canApply) {
const result = await client.renameNote({
ref: "gno://notes/old-note.md",
name: "new-note.md",
schemaVersion: plan.schemaVersion,
planDigest: plan.planDigest,
confirmation: "apply",
})
console.log(result.status)
}Define collections in code when your app owns the workspace, or point at an existing GNO config when the user already has one.
const fromExisting = await createGnoClient({
configPath: "/Users/me/Library/Application Support/gno/config/index.yml",
})
const noDownloads = await createGnoClient({
config,
downloadPolicy: { offline: false, allowDownload: false },
})Always call await client.close() when your process is shutting down. After close, further calls throw a GNO SDK error. This matters for desktop apps, tests, and short-lived automations because the SDK holds SQLite connections and file handles.
Retrieval options accept filter using the exported MetadataPredicate type. For example: client.search("rollout", { filter: { op: "gte", key: "confidence", value: 0.8 } }). Scope and authority still constrain eligible documents.
Read the metadata guide: examples, operators, missing fields, limits, and recovery.
Open a client with the archive config and index, createGnoClient({ configPath, indexName: "sessions" }), then call sessionsStatus(), discoverSessions(), or importSessions({ sourceId }) / importSessions({ paths, collection, format? }) with optional dryRun and limit. Import returns the shared receipt. runSessionsAutomation({ profileId }) runs a configured automation profile now and returns the shared automation run result; status.automation reports daemon state and per-profile hook and schedule status. Switch hooks and schedules on with gno sessions automation enable or the Web UI on the same host; the SDK can only run a configured profile. Sessions errors are GnoSdkError VALIDATION or RUNTIME with the specific code in details.code. See the session error codes.