@verevoir/sources
v0.5.0
Published
Source-adapter primitive: a contract over remote + local file sources (read/list/tree/write/branch/fork/PR) with implementations as subpath imports. GitHub + local FS + Notion today; GitLab, Bitbucket, S3 follow.
Readme
@verevoir/sources
Source-adapter primitive: a contract over remote file sources (read, list, tree, write, branch, fork, PR open) with implementations as subpath imports.
Purpose
Lets a downstream project read and write files in remote repositories without coupling to a specific source-host SDK. Pick the source via a subpath import; unused implementations don't enter your bundle.
Built for LLM-driven workflows that need API-based code access (no on-disk clones, no resident language servers). The companion package @verevoir/context layers an in-process cache + symbol index on top of these reads.
Most consumers reach this via MCP
If you're driving an LLM agent (Claude Code, custom Anthropic-SDK app, etc.) and want these source operations as tools, you usually don't import @verevoir/sources directly — you run the @verevoir/mcp server, which wraps this contract (via the cached drop-ins from @verevoir/context) and exposes read_file, list_files, get_repo_tree, grep, find_symbol, write_file as MCP tools. See that package's README for Claude Code configuration; the key recommendation is "alwaysLoad": true so the tools surface as first-class instead of being deferred behind ToolSearch.
Direct in-process consumption (the usage shown below) is for: writing your own MCP server, embedding the source surface inside a non-MCP runtime (e.g. server-side action handlers), or building higher-level libraries that compose with adapters.
Subpaths
@verevoir/sources— core types, theSourceAdaptercontract, theSourceApiErrorclass, and theenvFromProcessEnvhelper. No source dependency.@verevoir/sources/github— GitHub REST + Git Data adapter. Uses nativefetch, no SDK dependency.@verevoir/sources/fs— local filesystem adapter.repoUrlis a local directory path. No auth, no API. Reads/lists/walks/writes; fork + PR throw 501 (not applicable to a local filesystem).@verevoir/sources/notion— Notion adapter via@notionhq/client(optional peer dep). Models a Notion workspace as a documentation tree: pages are files, child pages are subdirectories, content is the page's blocks rendered to Markdown. Ships a minimal Markdown↔blocks converter for round-tripping aigency-generated content; rich Notion features (tables, callouts, etc.) read as best-effort placeholders.
Future adapters land alongside (@verevoir/sources/gitlab, @verevoir/sources/bitbucket, @verevoir/sources/s3) under the same contract.
Install
npm install @verevoir/sourcesNo mandatory peer dependencies — the GitHub adapter uses native fetch.
Canonical usage — GitHub
import { envFromProcessEnv } from '@verevoir/sources';
import { readFile, writeFile, openPullRequest } from '@verevoir/sources/github';
const env = envFromProcessEnv();
if (!env) throw new Error('GITHUB_TOKEN not set');
// Read a file from the default branch.
const { content, sha } = await readFile(env, 'https://github.com/acme/charts', 'README.md');
// Write a file on a feature branch (branch is created if missing).
await writeFile(
env,
'https://github.com/acme/charts',
'docs/notes.md',
'# Notes\n\nBody.\n',
'feature/notes',
'Add notes'
);
// Open a PR from the feature branch to main.
const prUrl = await openPullRequest(
env,
'https://github.com/acme/charts',
'feature/notes',
'main',
'Add docs/notes.md',
'Body of the PR.'
);Canonical usage — Local filesystem
Same contract; no auth required. repoUrl is interpreted as a directory path.
import { readFile, listFiles, getRepoTree, writeFile } from '@verevoir/sources/fs';
const env = { token: '', forkOrg: '' }; // FS adapter ignores both
// Walk the working tree (skipping node_modules, .git, dist, etc.).
const tree = await getRepoTree(env, '/path/to/project');
console.log(`${tree.entries.filter((e) => e.type === 'blob').length} files`);
// Read + write the same way as GitHub.
const readme = await readFile(env, '/path/to/project', 'README.md');
await writeFile(
env,
'/path/to/project',
'docs/notes.md',
'# Notes\n',
'ignored', // FS adapter ignores branch
'ignored' // FS adapter ignores commit message
);ensureFork and openPullRequest throw 501 on the FS adapter — there's no local-FS equivalent. The customer manages git operations themselves.
Fork-pivot pattern
When a writeFile to an upstream repo returns 403 (no write access), the caller can fork the upstream, write to the fork, and open a PR back:
import { ensureFork, writeFile, openPullRequest, SourceApiError } from '@verevoir/sources/github';
try {
await writeFile(env, upstreamUrl, path, content, branch, message);
} catch (err) {
if (err instanceof SourceApiError && err.status === 403) {
const forkUrl = await ensureFork(env, upstreamUrl);
await writeFile(env, forkUrl, path, content, branch, message);
await openPullRequest(env, upstreamUrl, `${env.forkOrg}:${branch}`, 'main', 'Title', 'Body');
} else {
throw err;
}
}The contract
Every subpath exposes the same set of functions (or a strict subset for read-only sources):
readFile(env, repoUrl, path, ref?) → Promise<{ content, sha }>
listFiles(env, repoUrl, prefix, ref?) → Promise<DirEntry[]>
getRepoTree(env, repoUrl, ref?) → Promise<RepoTree>
isFresh(env, repoUrl, path, version, ref?) → Promise<boolean>
writeFile(env, repoUrl, path, content, branch, message) → Promise<void>
ensureBranch(env, repoUrl, branch) → Promise<void>
ensureFork(env, upstreamUrl) → Promise<string>
openPullRequest(env, target, head, base, title, body) → Promise<string>
getDefaultBranch(env, repoUrl) → Promise<string>isFresh answers "is the version I'm holding still the live one for (repoUrl, path, ref)?" — the cheap freshness check cache layers (@verevoir/context's wrapWithCache) use to validate held entries without re-fetching content. Returns false when the source has moved (including when the path no longer resolves).
The SourceAdapter interface in @verevoir/sources captures this exactly. An aggregate export (e.g. github) is also available per subpath so generic callers can pass an adapter around as a single value.
Errors
SourceApiError is thrown on transport / API failures. status carries the HTTP status when present; detail carries the truncated response body for non-404 errors. 404 is the conventional "ref / path doesn't exist" signal — callers fall back to default-branch reads or other recovery on that status.
What this is NOT
- Not an Octokit replacement. Functions cover read + write + branch + fork + PR open; everything else (issues, releases, etc.) stays on the source's own SDK.
- Not a sync engine. Each call is independent; no local working tree, no shadow state.
- Not a language-aware index. Symbol extraction + content cache live in
@verevoir/contexton top.
See also
@verevoir/context— in-process content + symbol cache for LLM context windows.@verevoir/llm— provider-agnostic LLM call surface.
License
Apache-2.0.
