tenbrains
v2.5.1
Published
Agent-first CLI for X and YouTube research: analyze content, track accounts, surface suggestions, and persist every outcome locally.
Maintainers
Readme
tenbrains
An agent-first CLI for X and YouTube research. It analyzes posts and video transcripts, tracks followed accounts, surfaces
suggestions, and persists every outcome to a local SQLite database — all from the command line,
with no .env files and no hosted backend.
This is a ground-up rebuild of moeghashim/tenbrains
(originally a Convex-backed web workspace) reshaped around a single design goal: be consumed by
agents. Every command emits one stable JSON envelope on stdout, every failure carries a
machine-readable code and a deterministic exit status, and the entire surface is discoverable via
tenbrains manifest.
Why it's "agent-first"
- Structured output by default. stdout is always exactly one JSON envelope. Diagnostics and
progress go to stderr, so a parser never has to untangle the two.
--prettyswitches to human-readable output for terminal use. - Stable contract.
{ ok, command, data, meta }on success,{ ok, command, error }on failure. Errorcodes and process exit codes are fixed per failure class. - Self-describing.
tenbrains manifestreturns the full command tree, flags, provider catalog, error codes, and exit codes as JSON — an agent can discover the whole tool in one call. - Non-interactive. No blocking prompts. Content comes in via flags,
@file, or-(stdin); credentials are collected through commands, never by hand-editing a dotfile. - Everything persisted. Posts, analyses, takeaways, bookmarks, suggestions, learning tracks,
and learning objectives all land in one local SQLite file you can point anywhere with
--db. - Offline-capable. The built-in
mockprovider produces deterministic analysis and track material with no network, so agents and CI can exercise the full pipeline without API keys.
Requirements
- Node.js >= 24 (uses the built-in
node:sqlite— no native modules to compile).
Install
npm install -g tenbrains # from npm — puts `tenbrains` (and `tb`) on PATH
tenbrains --helpOr from a clone:
npm install
npm run build
node dist/bin/tenbrains.js --help # or: npm link -> tenbrains --helpDuring development you can run straight from TypeScript:
npm run dev -- analyze --provider mock --text "hello world"Quick start
# 1. Configure a provider once (stored in ~/.config/tenbrains/config.json, mode 0600).
# Optionally add an X API Bearer token in the same step (see "Fetching from X").
tenbrains setup --provider anthropic --api-key sk-ant-... --default
# 2a. Analyze a post you already have (paste the content).
tenbrains analyze --author levelsio --id 1790000000000000000 \
--text "Shipping an agent-first CLI today. Everything persists to SQLite, nothing in env files."
# 2b. Or analyze a tweet by URL — fetched free via X's oEmbed endpoint, no key needed.
tenbrains analyze --url "https://x.com/jack/status/20"
# 2c. Or fetch and analyze public YouTube captions, then add a narrative digest and study plan.
tenbrains analyze --url "https://www.youtube.com/watch?v=M7lc1UVf-VE" --summarize --learn
# 3. Read it back / explore.
tenbrains analyze list --limit 5
tenbrains search "agent cli"
tenbrains db stats --prettyNo API key handy? Use the deterministic offline provider:
tenbrains analyze --provider mock --text "vector databases power semantic search"The output contract
Success:
{
"ok": true,
"command": "analyze",
"data": { "post": { "...": "..." }, "analysis": { "topic": "...", "novelConcepts": ["..."] } },
"meta": { "analysisId": "ana_...", "provider": "anthropic", "model": "claude-sonnet-4-6", "mock": false, "persisted": true }
}Failure:
{
"ok": false,
"command": "analyze",
"error": { "code": "MISSING_CREDENTIALS", "message": "No API key configured for Anthropic Claude...", "retryable": false, "details": { "provider": "anthropic" } }
}Exit codes: 0 success · 2 usage · 3 not found · 4 missing credentials / config ·
5 provider error · 6 validation · 7 conflict · 1 internal. The full code→exit map is in
tenbrains manifest.
Commands
| Command | Purpose |
| --- | --- |
| analyze | Analyze a post or YouTube transcript (--text, --transcript, or --url to fetch) into topic, summary, intent, and concepts. --summarize adds a narrative digest; --learn builds a track and materializes it when it carries a learn-mode objective. |
| analyze list / analyze get <id> | Read stored analyses. |
| objective add\|list\|show\|focus\|archive\|link\|unlink\|mode\|refresh | Manage first-class objectives, switch learn/follow mode, explicitly tag records, and build local engagement-weighted follow tracks. |
| takeaway follow\|unfollow\|list\|refresh\|show | Track accounts; summarize recent posts (supplied via --posts or fetched from X) into snapshots. |
| suggest generate\|list\|save\|dismiss\|add | Rank un-saved posts against your saved signal, biased toward a described current objective focus; save/dismiss feedback. |
| bookmark add\|list\|show\|tag\|remove | Save posts with tags (auto-suggested from analysis) and notes. |
| learn generate\|materialize\|today\|quiz\|answer\|done\|show\|list | Build 7-day Feynman learning tracks, generate grounded lessons/quizzes, answer free-text quiz questions, and check off progress. |
| search <query> | Full-text search (SQLite FTS5, BM25-ranked, stemmed) across analyses, takeaways, and bookmarks; optionally filter by objective. |
| import x-archive <path> | Bulk-import your extracted official X archive: likes become bookmarked posts, your tweets become posts. Free, idempotent. |
| digest [--days N] | Markdown recap of analyses, takeaways, and bookmarks saved in the window (default 7 days), optionally filtered by objective. |
| setup / config set\|get\|list\|unset\|path | Collect and manage provider credentials and defaults. |
| record get <id> | Resolve any record by its prefixed id (post_, ana_, acc_, ...). |
| db stats\|migrate\|vacuum\|reindex\|reset | Inspect and maintain the database. |
| manifest | Emit a machine-readable description of the whole CLI. |
| serve [--port N] [--host HOST] [--no-open] | Serve the local web workspace and its authenticated command API. |
Global flags (valid on any command): --json (default), --pretty, --quiet,
--db <path>, --config-dir <path>.
Input flags accept inline text, @path to read a file, or - to read stdin:
echo "long post text..." | tenbrains analyze --provider mock --text -
tenbrains takeaway refresh levelsio --provider mock --posts @recent.jsonLocal web workspace
Start the first local web surface with the same database and command handlers the CLI uses:
tenbrains serve # http://127.0.0.1:4571 and opens a browser
tenbrains serve --port 5000 --db ./research.db
tenbrains serve --no-open # print the JSON startup envelope without opening a browserThe server binds to 127.0.0.1 by default. It creates a fresh, per-process session token, injects
it into the served page, and requires it in the X-Tenbrains-Session header for every /api/*
request. The page uses GET /api/manifest, POST /api/command, and GET /api/events; command
responses remain the ordinary tenbrains JSON envelopes. Do not expose it publicly: choosing a
non-loopback --host prints a warning because any reachable client can use that local session.
The API never returns revealed config secrets, even if a request supplies reveal: true.
The built page is a dependency-free TypeScript SPA for the full local workspace: Today’s daily loop,
Capture and extracted X-archive import, objective creation/detail/focus rollups, suggestions triage,
full-text Search with record hops, markdown Digest windows, and followed-account takeaways. Objective
mode, lesson, and quiz controls are discovered from the manifest. When the server publishes
learn quiz and learn answer, Today renders the lesson, per-question status, answer boxes, and
one-line verdicts without making quizzes a day-completion gate. Account refresh accepts
supplied post JSON in the page or fetches an X timeline when a bearer token is configured.
POST /api/import/x-archive imports the extracted X archive data files without giving the server a
filesystem path. Send JSON with files, each { "name": "like.js", "content": "..." }; accepted
names are like.js, like-partN.js, tweets.js, tweets-partN.js, tweet.js,
tweet-partN.js, and account.js (including part variants). bookmarks: false is the web
equivalent of --no-bookmarks, and limit is a non-negative per-kind item cap. The route shares
the CLI's idempotent ingest semantics and returns its import counts in a standard envelope. Unzip
the archive first: zip files are not accepted. It uses JSON rather than multipart to preserve the
zero-dependency server; the complete request is capped at 25 MiB and oversized uploads return a
VALIDATION envelope with HTTP 400.
Learning objectives
Objectives are persistent learning goals, separate from loose bookmark tags. Each objective has a
name, derived slug, optional description, learn (default) or follow mode, active/archived
lifecycle, and tagged-record counts. You can keep several active objectives while marking at most
one as the current focus:
tenbrains objective add "Stablecoins" \
--description "Understand reserve models, settlement, and failure modes." --mode learn --focus
tenbrains objective add "AI agents scene" --mode follow
tenbrains objective list
tenbrains objective show # defaults to the current focus
tenbrains objective focus ai-agents-scene
tenbrains objective focus --clear
tenbrains objective link post_... --objective stablecoins
tenbrains objective unlink post_... --objective stablecoins
tenbrains objective archive stablecoins
tenbrains objective refresh ai-agents-scene
tenbrains objective mode ai-agents-scene learnTag at creation time with a repeatable --objective flag:
tenbrains analyze --provider mock --text "..." \
--objective stablecoins --objective ai-agents
tenbrains takeaway follow levelsio --objective ai-agents
tenbrains bookmark add --post-id post_... --objective stablecoins
tenbrains learn generate --analysis ana_... --objective stablecoins
tenbrains search "reserve risk" --objective stablecoins
tenbrains digest --objective stablecoinsbookmark add tags the bookmark's post. Without an explicit --objective, learn generate inherits
the objective tags on its source analysis' post; supplying the flag overrides that inheritance.
Every referenced objective must already exist or the command returns NOT_FOUND without silently
creating one. Focus never tags records automatically. objective show groups tagged posts,
accounts, bookmarks, and tracks; record get includes an objectives array for linkable records.
Mode is a lens on future behavior only: it never changes explicit tags or current-focus behavior.
objective refresh <slug> [--force] [--limit N] works only for a follow objective (otherwise it
returns CONFLICT). It gathers its directly tagged analyzed posts plus analyzed posts authored by
its tagged accounts, ranks each post from local data only:
3·bookmarked + 2·min(authorTakeaways,5) + min(authorPostCount,10), then creates one 10-minute/day
track from the pooled concepts. There is no network call or scheduler. Fewer than three new posts
returns a successful data.track: null result unless --force is supplied; --limit caps the
selected pool. Every auto-track stores its consumed post ids in
learning_tracks.source_post_ids_json (the analysisId is its top-weighted source), so later refreshes
are idempotent.
When a selected or inherited objective has a description, learning tracks rank concepts first by
deterministic token overlap with that description, then by the existing interest and familiarity
ratings. With multiple described objectives, a concept uses its highest overlap with any one
description; matches are not summed across objectives. objective show also returns descriptive
progress counts under data.progress:
accounts followed, posts and transcripts analyzed, bookmarks saved, tracks completed, learning
days completed/total, quiz questions answered, and quiz answers correct. It never fabricates a
completion percentage.
suggest generate adds deterministic token-overlap relevance from the current focus description to
its existing saved-interest scores. With no focus or no usable focus description, its scores,
reasons, and ordering are unchanged. This bias never creates objective links. search and digest
accept one --objective <slug> filter and return only analyses whose posts, takeaways whose
accounts, and bookmarks (directly or through their posts) are tagged to that objective. Unknown
objectives return NOT_FOUND.
Personalized lessons and quizzes
When a newly built track has at least one learn-mode objective, tenbrains makes one batched
provider call to create a grounded lesson and three to five answer-reference questions for every
track day. The prompt uses the source post and analysis plus up to ten other analyzed posts tagged
to those objectives. The mock provider uses the same deterministic keyword-based path for offline
work and tests.
tenbrains learn generate --analysis ana_... --objective stablecoins --provider mock
tenbrains analyze --provider mock --text "..." --objective stablecoins --learn
tenbrains learn generate --analysis ana_... --objective stablecoins --no-materialize
tenbrains learn materialize trk_... --provider mock # later generation or retry
tenbrains learn materialize trk_... --day 3 --provider mock # replace one day
tenbrains learn today trk_... # lesson + quiz count, not answers
tenbrains learn quiz trk_... # questions + latest status, no expected answers
tenbrains learn answer trk_... --q 1 --answer "..." --provider mock--no-materialize leaves the normal track intact. A provider failure, including invalid generated
JSON, also leaves the track intact; retry with learn materialize. That command works for any
track, including an auto-generated follow track. learn quiz defaults to the next pending day (or
accepts --day N) and returns only questions plus the latest attempt status. learn answer accepts
inline text, @path, or -, grades one answer, and appends its attempt; retrying is allowed and
the latest attempt is the standing result. Quizzes never gate learn done. The mock grader is fully
offline: it marks an answer correct when it overlaps at least half of the expected answer's unique
content tokens (rounded up, minimum one). Tracks return their persisted material and latest
quizAttempts alongside progress on learn show and record get.
YouTube transcripts
Pass a public YouTube watch, youtu.be, Shorts, or embed URL to analyze. tenbrains selects a
caption track without an API key (manual before auto-generated; --lang, then English, then the
first available), stores the transcript and video metadata with the post, and runs the normal
analysis pipeline:
tenbrains analyze --url "https://youtu.be/M7lc1UVf-VE" --lang en
tenbrains analyze --url "https://youtu.be/M7lc1UVf-VE" --summarize --learn
tenbrains analyze --url "https://youtu.be/M7lc1UVf-VE" --transcript @captions.txt--summarize returns { summary, keyPoints[] } under data.summary, persists it in the post's
raw metadata, and uses that digest as the condensed input for concept extraction. It composes
with --learn. If a video is unavailable or has no captions, supply an existing transcript with
--transcript <text|@file|->. v1 is caption-only: it does not download audio or invoke Whisper.
YouTube's WEB caption URLs can return empty bodies, so the client retries through the embedded
Android player API; that undocumented client version is the primary maintenance surface.
Configuration & credentials
There is no .env. The CLI owns credential collection:
tenbrains setupcollects a key interactively (TTY) or via--api-key/--api-key -(stdin).tenbrains config set providers.openai.apiKey sk-...sets any value directly.--api-key/--provider/--modeloverride per invocation.
Values are written to a managed JSON file (tenbrains config path shows where) with 0600
permissions. Secrets are redacted in config get/config list unless --reveal is passed.
Resolution precedence: CLI flag → config store. Environment variables are intentionally not
consulted, keeping the credential source explicit and auditable.
Fetching from X
By default the agent supplies content via --text, but the CLI can also pull tweets itself —
designed free-first:
- Single tweets (
analyze) use X's public oEmbed endpoint by default: no API key, no paid tier.tenbrains analyze --url "https://x.com/user/status/123"fetches the tweet text + author and analyzes it. Control this with--fetch auto|oembed|api(defaultauto). - Account timelines (
takeaway) have no free path, so they use the official X API v2 with a Bearer token:tenbrains takeaway refresh <user> --count 20(omit--poststo fetch). Most accounts need a paid X API tier (Basic+) to read timelines. - Threads (
analyze --thread) are analyzed as one document. Supply the parts yourself for free (--thread '["part 1", "part 2"]',@file, or-), or pass bare--threadwith--url/--idto fetch the author's self-thread via the API (Bearer token; recent search covers ~7 days). - Your own history (
import x-archive) needs no API at all: request your account archive at X → Settings → "Download an archive of your data", extract the zip, and runtenbrains import x-archive <dir>. Likes land as bookmarked posts (instant signal forsuggest generate), your tweets as posts. Re-running dedupes on tweet id.
Store the token (used for timelines and as the --fetch api fallback) during setup or config:
tenbrains setup --provider anthropic --api-key sk-ant-... --x-bearer "AAAA..." # both at once
tenbrains config set x.bearerToken "AAAA..." # or just the X token
tenbrains analyze --url https://x.com/jack/status/20 --x-bearer - < token.txt # or per-call (stdin)If your tier can't read a tweet/timeline, the CLI returns a structured PROVIDER_UNAUTHORIZED /
PROVIDER_RATE_LIMITED error (exit 5) rather than crashing — fall back to --text / --posts.
Database
A single SQLite file (default ~/.local/share/tenbrains/tenbrains.db, override with --db). Schema
is versioned and migrated automatically on open. Tables: posts, analyses, accounts,
takeaway_snapshots, bookmarks, suggestions, learning_tracks, track_progress,
track_materials, quiz_attempts, objectives, and objective_links, plus a trigger-maintained FTS5 index
(search_fts) behind search — rebuild it anytime with tenbrains db reindex. Follow-track
consumption lives in learning_tracks.source_post_ids_json; objective mode lives in
objectives.mode; schema v6 adds track_materials; schema v7 adds append-only quiz_attempts.
tenbrains --db ./research.db analyze --provider mock --text "..." # isolate a workspace
tenbrains db statsProviders
Default is Anthropic Claude; openai, google, and xai are supported via --provider, and
mock runs offline. See tenbrains manifest for the live catalog and default models.
Use as a Claude skill
This repo ships an Agent Skill
in skill/ so Claude (Claude Code, claude.ai, or the Agent SDK) can drive the CLI
on your behalf — analyze a post, summarize an account, recall saved research — using the JSON
contract above. The skill is thin: it points Claude at the CLI and at tenbrains manifest for live
discovery.
Install it for your own Claude Code (make the tenbrains command available first, then copy the
skill in):
npm link # puts `tenbrains` on PATH
cp -r skill ~/.claude/skills/tenbrains # personal skill, available in every project
# or, project-scoped: cp -r skill .claude/skills/tenbrainsAfter installing, ask Claude something like "analyze this X post: …" and it will use the skill.
Development
npm run typecheck # tsc --noEmit
npm run lint # biome
npm test # node:test via tsx
npm run check # all threeLicense
MIT.
