Guides

File & export adapters

Index mail, calendar, transcript, browser, and JSONL exports as individually searchable records, with no account access and no live connectors.

An mbox file is not one document. It is four thousand messages that happen to share a file, and indexing it as a single blob makes every one of them unfindable. The same is true of a calendar export, a subtitle file, and a JSONL dump.

Export adapters split those containers into logical records: one message, one event, one cue, one bookmark, one row. Each becomes independently searchable and independently citable, while keeping its export path, exact source locator, dates, participants, thread or session identity, attachment inventory, and anchors.

The boundary is deliberate and worth stating up front. These are file adapters, not connectors. They read export files you produced and placed somewhere. They do not authenticate to an account, inspect a live browser profile database or cookie store, fetch a URL, open an attachment, execute embedded content, or unpack an archive. If you want yesterday’s mail indexed, you export yesterday’s mail.

What is supported

SourceFile typesOne record isNotes
JSON Lines.jsonl, .ndjsonOne valid object per lineOptional declarative field mapping; a malformed line is isolated rather than failing the file
Mail.eml, .mboxOne messageBounded MIME nesting, body, and attachment counts; attachments are inventoried, never opened or indexed
Calendar.icsOne VEVENT or recurrence exceptionTimezone-aware; recurrence anchors expand over a bounded local horizon rather than to infinity
Transcripts.vtt, .srtOne cue or segmentSpeaker and timestamp anchors retained
Browser exports.browser-exportOne bookmark, history, or reading-list itemMust contain a recognized export shape; live profile databases are rejected outright
Explicit transcript exportsConfigured .json or .txtOne segment or recordOpt-in only: generic JSON and text stay ordinary documents unless you declare otherwise

Markdown, PDF, Office, plain text, and source code continue through the existing one-file-one-document converter lane. Nothing about adapters changes how those are handled.

Getting started

For the automatic formats there is no configuration: add the export file or directory to a collection and index it.

gno collection add ~/exports --name exports \
  --pattern "**/*.{jsonl,eml,mbox,ics,vtt,srt}"
gno update

Collection include stays an extension allowlist: when it is non-empty, list the extensions you want. With the default empty include, an explicitly configured JSON transcript adapter automatically adds .json to the supported-extension scan.

Mapping JSONL fields

GNO recognises conventional id, title, text/content/body, and author names. When your export uses different ones, declare a closed mapping:

collections:
  - name: exports
    path: /Users/me/exports
    pattern: "**/*"
    include: [.jsonl, .eml, .mbox, .ics, .vtt, .srt, .browser-export]
    recordAdapters:
      jsonl:
        fieldMapping:
          id: /external_id
          title: /subject
          body: /payload/text
          author: /owner/name
          participants: /participants
          threadId: /thread_id
          dateFields:
            created: /created_at

Selectors are JSON Pointers, or ordered arrays of JSON Pointers when a field lives in different places across rows. They are data, not code: a selector cannot execute, traverse prototypes, read a file, or make a network request.

Opting into JSON or text transcripts

recordAdapters:
  transcript:
    format: json # json, text, vtt, or srt

Generic JSON and text are never guessed as transcripts. Heuristic detection here would misclassify ordinary config and data files, so it is per-collection opt-in.

Identity, updates, and deletions

Each record gets an opaque key derived from the adapter identity plus the record’s stable export identity. Reimport with the same key and source hash is a no-op; a changed source hash updates the existing virtual document in place.

Deletion is where export ingestion usually goes wrong, so GNO is strict. A complete snapshot deactivates records that disappeared from it. A partial snapshot never does. A malformed row, a truncated file, invalid framing, or any cap failure marks the import partial, and a damaged export is therefore incapable of authorizing deletion. A half-written mbox cannot wipe your mail history from the index.

For JSONL, configure fieldMapping.id or provide a conventional id when updates must preserve identity. Without one, identity is derived from the row’s canonical content, so editing a row appears as one removal plus one addition rather than an in-place update. That is correct behaviour for content-addressed rows, but it will surprise you if you expected stable IDs, so set one.

How records appear in results

Search and gno get report the real container path in source.relPath, plus a record object with the bounded locator and metadata. The unique gno:// URI addresses GNO’s internal virtual document: use that URI or its docid with gno get.

Ask and Context Capsules carry the same record metadata alongside exact canonical-mirror line spans, so a claim can cite “message 14 of this mbox, lines 4 to 9” and be verified later. record.adapter holds the adapter ID, version, and configuration fingerprint that produced the record, which is what lets you tell “the export changed” apart from “my mapping changed”.

Virtual documents live under the reserved .gno/records/ URI namespace, and a physical directory of that name is always excluded from collection walking so real files can never collide with the reserved space.

What this is good for