Skip to content

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.

SourcesToolkits
DirectionCollect data in (batch)Serve tools out (live)
Entry pointdefineSource({ sync }) → documentsdefineToolkit({ tools }) → results
SDK@ontrove/extend/source@ontrove/extend/toolkit
ExecutesTrove cloud by default; Mac app for browser/local sourcesTrove cloud
Author fileextension.tsextension.ts
Ship viaAdd in the Mac apptrove 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.

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.

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 hand

manifest.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.

extension.ts
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.

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 documentStore full text in R2, index vectors in Vectorize
Track the per-feed sync cursor
Deduplicate by (feed, id)
Serve to Claude — search, recent, get

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.