Sources
A source is a thing that fills your Library — it continuously pulls content into your Trove knowledge base: a blog’s RSS feed, your Hacker News upvotes, a podcast subscription, a folder of notes. The code behind a source is a source adapter: one defineSource call in extension.ts, holding both what the source is and what it does — and Trove handles everything downstream: transcription, embedding, indexing, dedup, search, and serving to Claude.
A source is the unit of “a thing you connected.” Each source has one or more feeds (individual publications or accounts) and produces documents: full text plus metadata. Your code’s only job is to return documents; Trove takes it from there.
Collect vs. serve: the two authoring paths
Section titled “Collect vs. serve: the two authoring paths”Trove is built around a symmetric pair of authoring patterns. Knowing which one you want is the first decision.
| Sources | Toolkits | |
|---|---|---|
| Direction | Collect data in (batch) | Serve tools out (live) |
| Entry point | defineSource({ sync }) → documents | defineToolkit({ tools }) → results |
| SDK | @ontrove/extend/source | @ontrove/extend/toolkit |
| Executes | Trove cloud by default; Mac app for browser/local sources | Trove cloud |
| Author file | extension.ts | extension.ts |
| Ship via | Add in the Mac app | trove deploy |
A source feeds your knowledge base on a schedule — its documents become searchable through trove_search and the other core tools. A toolkit answers live questions against a system of record at call time. A developer who has written one can write the other in minutes.
This series covers the collect path. For the serve path, see Toolkits.
When to build a source adapter
Section titled “When to build a source adapter”Build a source adapter when you have an upstream system Trove doesn’t index yet and you want its content continuously synced and searchable — not fetched live on demand. Good candidates:
- A public feed: an RSS/Atom feed, a Substack, an arXiv topic, a JSON API of articles.
- An account you log into: your Hacker News upvotes, your Twitter bookmarks, your Readwise highlights.
- Audio you want transcribed: a podcast subscription, voice memos, meeting recordings.
- Local content: a folder of Markdown notes, an Obsidian vault, your Apple Notes.
If instead you want Claude to call a tool that returns a live answer (a stock quote, the status of an internal order), build a toolkit instead.
One call declares the source
Section titled “One call declares the source”A source adapter is one defineSource call in extension.ts, holding its identity, how it runs, and its sync. Nothing else is written by hand.
my-source/ extension.ts ← the whole source manifest.json ← generated from it; never edited by handmanifest.json still exists, because the readers cannot execute your source — Trove’s catalog build and the Mac app both read it off disk, and neither runs TypeScript. It carries "generated": true and is rewritten on every build. See the Source Manifest Reference for every field you can declare.
import { defineSource } from "@ontrove/extend/source";
export default defineSource({ id: "hacker-news", name: "Hacker News Upvotes", description: "Index stories you've upvoted on HN.", icon: "🟠", version: "1.0.0", author: "you", kind: "scheduled-sync", transport: "api", cursor: "none", ingest: "append", runsIn: "cloud", schedule: "every 6 hours", status: "implemented", needsBrowser: false, egress: ["hn.algolia.com"], config: { username: { label: "HN Username", type: "text" }, },
async sync(ctx) { const res = await ctx.fetch( `https://hn.algolia.com/api/v1/search?tags=upvoted_${ctx.config.username as string}` ); const data = (await res.json()) as { hits: { objectID: string; title: string; story_text?: string; url?: string; author: string; created_at: string }[]; };
return data.hits.map((story) => ({ id: story.objectID, title: story.title, text: `${story.title}\n\n${story.story_text ?? story.url ?? ""}`, url: `https://news.ycombinator.com/item?id=${story.objectID}`, author: story.author, date: story.created_at, })); },});Declaring it in TypeScript is what makes the compiler the thing that notices. defineSource also validates the manifest half as the module is imported, so an unrecognised cadence or a credential smuggled into config throws at your desk rather than on the first scheduled run.
That’s the whole contract. Your source adapter returns documents; Trove uploads the text to the vector store, tracks the sync cursor, deduplicates by document id, and makes everything searchable from Claude.
What you provide vs. what Trove does
Section titled “What you provide vs. what Trove does”You (in sync(ctx)) | Trove (after ingestDocuments) |
|---|---|
Fetch data (ctx.fetch, Playwright, filesystem) | Transcribe audio → text (Whisper) |
Return documents (text, or audioUrl for audio) | Embed text → vectors |
Use a stable id per document | Store full text in R2, index vectors in Vectorize |
| Track the per-feed sync cursor | |
Deduplicate by (feed, id) | |
| Serve to Claude — search, recent, get |
Where the SDK lives
Section titled “Where the SDK lives”The source SDK ships in @ontrove/extend, imported as @ontrove/extend/source. The source-adapter catalog is just a git repo with a folder per adapter — the Mac app reads its registry.json, lists the adapters, and adds them, much like a Homebrew tap.
Next steps
Section titled “Next steps”- Quickstart — build your first source adapter in about 10 minutes
- SDK Reference — the
@ontrove/extend/sourceAPI, thectxobject, and the document shape - Source Manifest Reference — every field you declare, explained
- Cursors and Feeds — multi-feed sources, cursors, and the cursor protocol