Command reference¶
Every command of sb, with every option, its default, what it prints and how it exits. sb --help and sb <command> --help print the same list; this page adds what the help text has no room for. For a task ("how do I add a PDF?") start from How do I...?.
Examples show the default vault, ~/Documents/Obsidian/esbi. Your paths will differ.
Conventions¶
--config PATH. Every command that reads the config accepts it. When it is left out, the file is found as described in Configuration.sb version,sb update,sb schedule status,sb schedule uninstallandsb initdo not read a config (sb initwrites one:--config-file). A--configpath that does not exist is an error (error: config file not found: PATH), not a reason to use another file. A config whose[paths].vaultfolder does not exist is an error too (error: vault not found: PATH (runsb init, or fix [paths].vault),--jsoncodevault_not_found) for every command that opens the vault: nothing is created.sb doctor,sb initand the commands that only edit the config (sb ocr ...,sb email set-password) still work.- Exit codes.
0is success.1is an error the command already explained on the screen (a missing config, a bad argument, the model is down, aFAILline insb doctor).2is a usage mistake caught by the command-line parser (an unknown command or option); it prints the usage and a hint.130and143are a stop you asked for: Ctrl-C (SIGINT) andSIGTERM, the conventional128plus the signal number. They come fromsb run,sb ingestandsb reingest; see Stopping a command. A file or disk problem (a read-only or full vault, a missing permission, a vault's state database that cannot be used, a user with no home folder) is one line,error: Permission denied: /pathfor example, with exit1and no traceback (--json: codeos_error). A closed output pipe (sb run | head) stops quietly and puts the source being read back in the queue, with no attempt counted. --json.sb version,status,info,doctor,add,askandrunaccept--jsonand then print JSON for a program to read instead of text. The shapes are in JSON output.- Quote what the shell would read. A URL with
?or&must be in quotes, orzshanswersno matches foundbeforesbstarts:sb add "https://example.com/post?id=7". - Shell completion.
sb --install-completioninstalls it for the current shell,sb --show-completionprints the script to copy.
Commands at a glance¶
| Command | What it does |
|---|---|
sb |
A numbered menu of everyday actions |
sb init |
First-time setup: the vault, a config file, the optional pieces |
sb doctor |
Check the whole setup and say what to fix |
sb info |
Print where the config and the vault are, as key=value lines |
sb version |
Print the installed version; --check looks for a newer one |
sb update |
Update esbi-cli to the latest release (asks first) |
sb add |
Queue links or files for the next run |
sb scan |
Queue what is in inbox/ |
sb status |
Queue counts and sources that failed |
sb retry |
Put failed sources back in the queue |
sb drop |
Remove a source from the queue |
sb run |
The main job: mail, inbox, queue, summaries, lint, index |
sb ingest |
Ingest one source right now |
sb reingest |
Rebuild notes from the saved originals |
sb consolidate |
Write a consolidated summary on concept and entity pages |
sb import-legacy |
One-time import of an older folder of links and PDFs |
sb today |
Open (or print the path of) today's daily note |
sb index |
Register read ticks, rebuild the daily index |
sb lint |
Check the wiki's health |
sb ask |
Answer a question from the wiki |
sb eval |
Score retrieval on your own questions |
sb bench |
Compare models on your own material |
sb export |
Write a read-only HTML copy of the wiki |
sb ocr status, enable, disable |
Read images and scanned PDFs with a local vision model (optional) |
sb setup email, sb setup clipper |
Guided setups for Gmail capture and the Web Clipper |
sb email fetch, configure, set-password |
Mail capture |
sb schedule install, status, uninstall |
The nightly job (macOS launchd) |
Stopping a command¶
Ctrl-C or SIGTERM during sb run, sb ingest or sb reingest stops it cleanly. A note is never cut in half: a signal that arrives while a note and its pages are being written waits until they are whole, then the command stops. The source that was being read goes back to the queue without counting an attempt (three interruptions no longer park a good source), the command exits with 130 (Ctrl-C) or 143 (SIGTERM), and says Interrupted: 1 item put back in the queue. (N items for more, on stderr). sb ingest and sb reingest say Interrupted.; sb reingest resumes where it stopped. An interrupted scheduled run does not count as tonight's nightly run, so the job tries again.
sb (menu)¶
sb
With no command, a numbered menu. Each choice runs the real command, asking for what it needs, prints its output and returns to the menu. A mistake or a failing command never closes it.
esbi-cli
1. Queue status
2. Run now (ingest what is queued)
3. Ask the wiki a question
4. Add links or files to the queue
5. Open today's note in Obsidian
6. Check the wiki's health (lint)
7. Check my setup (doctor)
Choose (q to quit) [q]: 1
queued: 1
processing: 0
done: 1
failed: 0
Anything else than a number from 1 to 7 or q prints Not an option. The menu has no --config: it uses the usual search (set ESBI_CONFIG to point it elsewhere).
sb init¶
sb init [--vault PATH] [--config-file PATH] [--model local|subscription|api]
[--runtime ollama|lmstudio] [--local-model NAME] [--base-url URL]
[--language en|es] [--obsidian | --no-obsidian] [--nightly HH:MM|none] [--remote URL]
[--ocr | --no-ocr] [--ocr-model NAME] [--pull-ocr-model]
First-time setup. It creates the vault if it is missing, writes a config file and says what to do next. When git is installed it makes the vault a repository and a first commit (init: vault), so the vault's .gitignore, which keeps .esbi/ out, is tracked from the start; a commit that fails does not fail the setup. It only creates what is missing: running it again changes nothing that exists and never overwrites a config.
At a keyboard it is an installer that asks one question per setting; with no keyboard (a script, a pipe) it asks nothing and uses the flag, or the default. The walk-through, with what each answer changes, is in Getting started.
| Option | Default | Meaning |
|---|---|---|
--vault PATH |
~/Documents/Obsidian/esbi |
Where the vault lives. Created if missing |
--config-file PATH |
~/.config/esbi-cli/config.toml |
Where the config is written. An existing file is left alone |
--model |
local (no keyboard) |
local: a model on this machine, nothing leaves it. subscription: your Claude subscription through the official claude tool, with a local fallback. api: Anthropic's API, billed per token (set ANTHROPIC_API_KEY). The last two keep email on the local model |
--runtime |
ollama |
Which local runtime: ollama or lmstudio. Also the failsafe for subscription and api |
--local-model NAME |
llama3.2:latest for Ollama |
The local model's name. Required for lmstudio (see lms ls) |
--base-url URL |
this machine | Only with --model local: the address of an Ollama or LM Studio server that is not on this Mac. The notes' text then goes to that host, so sb init warns and sb doctor keeps warning |
--language |
en (no keyboard) |
The language the notes are written in, en or es: written as [notes].language, and used for the vault's SCHEMA.md and index.md. The command line itself is always English. See Language of the notes |
--obsidian / --no-obsidian |
--obsidian (no keyboard), no (asked) |
Writes viewer = "obsidian" or viewer = "none" in [notes] |
--nightly HH:MM or none |
none | Run every night at that time: installs the launchd job on macOS, prints a cron line elsewhere (with sb by absolute path). Only when the config is new: an existing config is left alone, and so is the job |
--remote URL |
none | Connects the vault to a git remote for backup (an existing remote is never replaced). Needs git |
--ocr / --no-ocr |
--no-ocr (asked at a keyboard, default No) |
Also writes an [llm.ocr] section so images and scanned PDFs are read. Needs --runtime ollama |
--ocr-model NAME |
qwen3-vl:2b-instruct |
Which Ollama vision model reads images. Goes with --ocr. A name that is not a plausible Ollama model name is refused |
--pull-ocr-model |
off | Download the vision model now if Ollama does not have it, showing progress (an interrupted download resumes). Goes with --ocr. Without this flag, or a typed yes at a keyboard, nothing is downloaded |
The images question. At a keyboard, after the backup remote, sb init asks Read images and scanned PDFs (jpg, png, ...)? It needs a local vision model. The default is No: reading images is optional and nothing is downloaded unless you say so. On yes it lists the known vision models with their download size and the memory each wants, marks the one that suits this machine, and lets you type any other Ollama model. qwen3-vl:2b-instruct (1.9 GB) is the recommended model and fits an 8 GB Mac; deepseek-ocr:3b (6.7 GB) is a choice for a machine with 16 GB of memory or more (on an 8 GB Mac it was measured unusable, about 100 times slower). It then says whether Ollama already has the model and, if not, asks Download it now? (default No). It never blocks on a small machine: it only warns. The question is skipped with --runtime lmstudio, and when the config already exists it is left alone (sb ocr enable changes it).
Errors it reports and exits 1 on: an unknown --model, --runtime or --language, --runtime lmstudio without --local-model, --ocr with LM Studio, --ocr-model or --pull-ocr-model without --ocr, --base-url with a cloud model or without a host, a --nightly that is not HH:MM.
sb init --vault ~/Documents/Obsidian/esbi --model local --no-obsidian
Vault ~/Documents/Obsidian/esbi: inbox/, raw/, attachments/, wiki/sources/, wiki/concepts/, wiki/entities/, wiki/syntheses/, wiki/review/, wiki/daily/, SCHEMA.md, index.md, log.md, .gitignore, git repository
Config ~/.config/esbi-cli/config.toml: written
Notes language: en (English). The CLI itself is English.
Model: local. Everything stays on this Mac: the notes are written by a local model.
Next: ollama pull llama3.2, then sb doctor. Add a link with `sb add URL` or drop a PDF in the vault's inbox/ folder.
Without git installed the vault is created without history and the message History: git is not installed, so the vault has no version history... appears. It does not install Ollama or pull a model.
sb doctor¶
sb doctor [--config PATH] [--json]
Checks the whole setup and prints one line per check, with the fix under anything that is not fine. ok is fine, WARN is an inconvenience, FAIL is broken. Exit code 1 only when a line is FAIL.
ok config: ~/.config/esbi-cli/config.toml
ok version: 0.4.1 (latest)
ok notes language: en (English)
ok vault: ~/Documents/Obsidian/esbi
ok obsidian: not used ([notes].viewer = "none")
ok queue: empty
WARN last run: no scheduled run yet
fix: the job does it after the nightly time; or try `sb run --if-due`
ok model summarize: ollama/llama3.2:latest is installed
ok ocr: off (optional): `sb ocr enable` turns on reading images and scanned PDFs with a local vision model ([llm.ocr], https://rubenamaury.github.io/esbi-cli/docs/reference/configuration/)
ok email privacy: all models are local
ok email: off (optional)
WARN nightly job: not installed
fix: sb schedule install
ok global install: /opt/homebrew/bin/sb
| Check | Looks at |
|---|---|
config |
Which config.toml applies, and that it is valid: a misspelled key, an unknown section or a value of the wrong type is a FAIL naming the setting |
version |
The installed version, and a WARN (X.Y.Z is available, runsb update`, X.Y.Z being the newer release) when a newer release is known. It reads the once-a-day cache and asks GitHub only when that is stale; if GitHub cannot be reached the line is just the version, and with[update].check = falseit says(update check is off)` |
notes language |
The language the notes are written in, from [notes].language (always ok) |
platform |
Only off macOS: WARN platform: esbi-cli is only supported on macOS for now (<system> is not supported: it may work, nothing is promised). Nothing else changes; the other checks run as usual. macOS shows no line |
vault |
The folder has SCHEMA.md and wiki/ |
vault history |
Whether the vault is a git repository (a warning only when git is installed but the vault is not a repository) |
obsidian |
The folder was opened once as a vault in Obsidian (.obsidian exists); with viewer = "none" it just says not used |
queue |
Queue counts; warns when sources failed for good |
last run |
The last scheduled run. WARN with the time and the model server was unreachable (nothing was lost; the sources stay queued) when the latest run, scheduled or manual, stopped because the model could not be reached: start Ollama or check the model, then sb run. A later run that reached the model clears it |
model <task> |
For each of summarize, synthesize, ask, private, embed that is set (and its fallback): Ollama is reachable and the model installed, LM Studio serves the model, claude is installed and logged in (claude auth status), codex is installed and logged in (codex login status), or the API key is in the environment |
ocr, model ocr |
Images and scanned PDFs are off (ok, optional; it says when a model choice is kept), or [llm.ocr] is not a model on this machine (FAIL), or the vision model is installed (and FAIL with the exact ollama pull command when it is not) |
ocr memory |
WARN only when reading is on with a known model and this machine has less memory than that model wants (for example deepseek-ocr:3b on 8 GB); the fix names the recommended model |
ocr ollama |
WARN only when the Ollama that serves the vision model is older than the one the model needs (brew upgrade ollama, or update the app) |
email privacy |
Email is read only by a local model; warns when a cloud model is set with no [llm.private]; fails when [llm.private] sends text away |
server <task> |
Warns when an Ollama or LM Studio model is served from another machine |
network |
Only when [network] use_environment_proxy = true: WARN, because the address checks are then the proxy's, not esbi-cli's (fix: set it to false unless this network only has a proxy) |
email |
Only when enabled: the app password is in the Keychain (off macOS, which is not supported, the system keyring: a server, a container or WSL has none, see Email where there is no keyring) |
nightly job |
The launchd job is installed, loaded and set for the time in the config, and does not point into a versioned Homebrew folder (Cellar) that brew upgrade deletes, nor into another uv tool or pipx environment than the one running (fix: sb schedule install). Off macOS it is a WARN: launchd only exists on macOS, and the fix is the cron line that sb schedule install prints |
global install |
sb is on your PATH. When it is not, the fix: line matches how esbi-cli was installed: brew upgrade rubenamaury/esbi-cli/esbi-cli, uv tool upgrade esbi-cli, pipx upgrade esbi-cli or pip install -U esbi-cli; uv tool install --editable <folder> only appears for a source checkout |
With --json it prints one object with ok and every check (see JSON output); the exit code is the same.
Every message and its fix are listed by symptom in Troubleshooting.
sb info¶
sb info [--config PATH] [--json]
Prints where things are, as key=value lines. The setup wizards read it; it is also the quickest answer to "which config is it using?".
config=~/.config/esbi-cli/config.toml
vault=~/Documents/Obsidian/esbi
viewer=none
language=en
email=disabled
With --json it prints the same facts, the models per task, whether each of them sends text out, and the nightly job as one object: see JSON output.
sb version¶
sb version [--check] [--json]
Prints the installed version, for example 0.4.1. It reads the package metadata, so it is the version that is installed, not the one in a checkout. Without options it is offline and instant.
--check also asks GitHub for the latest release (one anonymous HTTPS request, ignoring the once-a-day cache) and prints the installed: and latest: versions, then You are up to date., or A newer version is available (<release link>). Update with: sb update. If GitHub cannot be reached it says so and still exits 0.
--json prints {"contract": 1, "version": "0.4.1"} and nothing else (--check does not ask GitHub then): it is how a program learns which version, and which JSON contract, it is talking to.
sb update¶
sb update [--yes] [--dry-run]
Updates esbi-cli to the latest release. It asks GitHub for it (ignoring the once-a-day cache), prints the installed version, the latest version and how esbi-cli was installed (brew, uv-tool, pipx, pip, editable or unknown), then the exact command it will run and Run it now? [y/N]. Nothing is installed without that yes.
| Option | Meaning |
|---|---|
--yes, -y |
Do not ask; run the command |
--dry-run |
Only print the command; run nothing |
Without a terminal and without --yes it refuses (exit 1) and runs nothing. The command is run without a shell and its output is shown as it comes. On success it prints Updated to X.Y.Z. Run `sb version` to confirm. with the version that is now installed. If the command exits 0 but the installed version did not change (a uv tool pinned to an exact version, or a release the package index does not list yet) it says so instead: Nothing changed: X.Y.Z is still installed. The command found nothing to upgrade (an exact version pin, or the new release is not on the package index yet: try again in a few minutes). It does not claim an update that did not happen. If the command fails it exits with that command's code. When there is no command to run (a source checkout, an unknown install, or a uv tool pinned to a git ref or a wheel) it prints what to do by hand. When you are already current it says so. See Update esbi-cli.
sb add¶
sb add TARGET... [--config PATH] [--json]
Queues one or several sources for the next sb run. Nothing is fetched now. A target is an http(s) URL, or the path to an existing .pdf or .md file, or an image (.png, .jpg, .jpeg, .webp, .tif, .tiff) when [llm.ocr] is set; without it the error says how to turn it on.
If any target is invalid nothing is queued and the bad ones are listed (exit 1). A link must have a host and no spaces or line breaks, so several links passed as one argument (sb add "$LINKS", a paste) are refused: pass each link as its own argument. Tracking parameters, letter case and a trailing slash are ignored. A link already in the queue is counted as already known; a link already in the wiki is skipped and the note that has it is named. The rules for every kind of duplicate are in If a source already exists. A .md clip added from outside the vault's inbox/ (sb add clip.md) is read from where it is; when its note is written the original is copied unchanged into raw/inbox/ (clip (2).md if the name is taken), so it is kept the same as a clip dropped in inbox/.
sb add https://en.wikipedia.org/wiki/Zettelkasten
Queued 1 (0 already known). `sb run` processes them.
sb add notaurl
error: not an http(s) URL or an existing .pdf/.md file:
notaurl
With --json the result is one object (queued, already_known, in_wiki), and a bad target is an error object with the code bad_target: see JSON output.
sb scan¶
sb scan [--config PATH]
Queues what is waiting in the vault's inbox/: .md clips and .pdf files, and images when [llm.ocr] is set. Each file is moved to raw/inbox/, its permanent home, and queued from there. sb run does this for you; run sb scan by itself to see the queue before a run.
- A byte-identical copy of a file already in
raw/inbox/(under any name) is a duplicate: it is removed frominbox/, counted and not queued. - A different file with the same name is kept under a new name (
paper (2).pdf). Nothing inraw/is ever overwritten. - Hidden files and folders are skipped.
- Any other file that cannot be read stays where it is and is listed:
Not read, esbi-cli cannot read these files in inbox/: ..., with the way to turn OCR on when an image is among them.
Queued 1 sources from the inbox.
sb status¶
sb status [--config PATH] [--json]
Prints the number of sources queued, processing, done and failed, then one line per source that failed for good with its last error. A source still being retried (it failed once or twice) counts as queued; its last error is in the daily note under To review.
queued: 2
processing: 0
done: 0
failed: 1
failed: https://example.com/ (ExtractError: No readable article content found at https://example.com/)
With --json it prints the counts, the parked sources and the ones still being retried, each with its attempts and last error: see JSON output.
sb retry¶
sb retry [TARGET] [--config PATH]
Puts sources that failed three times (parked as failed) back in the queue with a fresh attempt count, so the next sb run tries them again. Without TARGET it requeues every parked source; with one (a URL or path as sb status shows it; tracking parameters are ignored) only that one.
Requeued 1 source.
When nothing is parked it prints Nothing to retry. and exits 0.
sb drop¶
sb drop TARGET [--config PATH]
Removes a source from the queue whatever its state (queued, retrying, failed or done). The original file, if there is one, is not touched, and a note already written stays. You can add the source again later. If TARGET is not in the queue: error: <target> is not in the queue, exit 1.
Dropped https://example.com/
sb run¶
sb run [--config PATH] [--limit N] [--if-due] [--json]
The whole pipeline, in this order:
- Fetch mail from the mailbox (only if
[email].enabled; a mail problem is a warning, never fatal). With[email].follow_linksthe links in new mail are queued here, not fetched. - Scan
inbox/and queue what is there. - Put back in the queue anything a crashed run had claimed.
- Ingest queued sources one by one; every success is a git commit in the vault.
- Record the run.
- Write consolidated summaries for the pages this run touched that are due, up to
[run].max_consolidations_per_run(0, off, by default: seesb consolidate). Skipped after an outage. - Lint the wiki.
- Register your read ticks, rebuild the daily index and
Home.md, push the vault to its remote if it has one.
| Option | Default | Meaning |
|---|---|---|
--limit N |
[run].max_sources_per_run (20) |
Ingest at most N sources this run |
--if-due |
off | Scheduled mode: run only when a scheduled batch is due, and otherwise print Not due: the nightly run already happened. A batch is due when today's nightly run has not happened yet, or when it has and sources are queued or files the run can read wait in inbox/ (mail still in the mailbox does not count), whatever ended the last batch (max_sources_per_run, max_tokens_per_run, max_usd_per_run, or an empty queue that filled again): the hourly tick then drains the queue, one batch per tick, and no more than [run].max_batches_per_day scheduled batches a day, default 6, counted from nightly_time and including the nightly run, which is what bounds a day's spend (a run that stopped for an outage or an interruption, and a manual run, do not count). Never after a scheduled run that stopped for an outage or an interruption (that waits for the next night, or is retried as described in Run it every night), and never while another run is active. Manual runs never count |
--json |
off | Print one JSON object per line (JSON Lines) for a program to read, instead of the progress text: started, source_started, step, source_done, source_failed, finished. Flushed line by line. See JSON output |
A run stops early when it reaches max_sources_per_run, max_tokens_per_run or max_usd_per_run (an estimate: tokens times the model's price in [bench.prices]; models on this machine and claude-cli or codex-cli count as 0, see [run]); what is left stays queued, with no attempt counted, and the run is recorded as max_sources, token_budget or usd_budget. A source that fails is retried on later runs (three attempts, then it is parked as failed). Every failure is reported on its own line, before the summary, with the source (its title if it has one, else its link or path), which attempt it was and a short reason; a source that has used all its attempts says it is parked and that sb retry puts it back. The summary line counts them (failed: 1). The full error is in sb status once the source is parked, and in the daily note under To review (Por revisar with language = "es"), which also lists the sources still being retried. Only one run can be active at a time.
Exit codes. 0 when the run finished or stopped early on a limit, and also when another run holds the lock (it prints Another run is in progress; skipping.) or --if-due found nothing due (Not due: the nightly run already happened.). 1 when the model is unreachable: nothing is counted against the sources, the index is still rebuilt, and the message is The LLM is unreachable (is Ollama running?). Nothing was lost; sources stay queued. 1 also for a config error, such as an unknown provider in model. 130 after Ctrl-C and 143 after SIGTERM: the source being read went back to the queue with no attempt counted, no lint or index is built, and the message on stderr is Interrupted: 1 item put back in the queue. (see Stopping a command).
sb run
... chunk 1 of 5
... chunk 2 of 5
... chunk 3 of 5
... chunk 4 of 5
... chunk 5 of 5
... synthesis
... detailed summary
... connections
+ Zettelkasten
... synthesis
... connections
+ Spaced Repetition
ingested: 2, skipped: 0, failed: 0 (20826 tokens)
Lint: 2 issues (see wiki/review/Lint.md)
Wrote wiki/daily/2026-10-03.md
A failure looks like this (the same source on its first and its third run):
! http://localhost:9/old-page: attempt 1 of 3 failed, will be tried again: ExtractError: Refused to fetch http://localhost:9/old-page: localhost resolves to a non-public address (127.0.0.1)
! http://localhost:9/old-page: attempt 3 of 3 failed, parked (`sb retry` puts it back): ExtractError: Refused to fetch http://localhost:9/old-page: localhost resolves to a non-public address (127.0.0.1)
The reason is cut to one line of at most 160 characters. The lines starting ... are steps of the source being read: chunk N of M is chunk N of M being read, synthesis the core note, detailed summary the abstract (long sources), connections the links to your other notes. A line with + is a source written. Warnings (a repaired one-liner, a dropped entity, a skipped chunk, Only half of chunk 2 of 5 could be read., Dropped 2 diagram edges: an end was not in the source or the note.) follow it.
sb ingest¶
sb ingest TARGET [--config PATH] [--force] [--dry-run] [--no-commit]
Ingests one source immediately, bypassing the queue and the run lock. TARGET is an http(s) URL, or the path to a PDF, a Markdown note (a Web Clipper clip) or an image. A clip from outside the vault's inbox/ is copied unchanged into raw/inbox/ when its note is written (name (2).md if the name is taken; the same applies to a clip queued by sb add and read by sb run), because the snapshot raw/<date>-<slug>.md holds the text after the page chrome was stripped.
| Option | Default | Meaning |
|---|---|---|
--force |
off | Ingest even if the same URL or the same text is already in the vault |
--dry-run |
off | Print the model's edit plan as JSON and write nothing |
--no-commit |
off | Write the files but do not commit them to the vault's git history |
A source already in the vault is skipped: Already in the vault as [[...]] (use --force to redo). After a successful ingest it prints the title and the pages created: and updated:, how many notes went to wiki/review/, references to unknown pages that were ignored, and entities dropped because their name is not in the source. Errors (an unreadable source, the model down, a refused URL) print error: ... and exit 1. Ctrl-C or SIGTERM stops it cleanly (Interrupted., exit 130 or 143): see Stopping a command. When the ingest makes a concept or entity page due for a summary, it is written at the end, up to [run].max_consolidations_per_run.
sb ingest ~/Documents/clips/active-recall.md
... synthesis
... connections
Ingested: Active recall versus rereading
created: Active recall versus rereading, Active Recall, Rereading
updated: -
ignored references to unknown pages: Spaced Repetition
dropped entities not found in the source: Roland Barthes
committed to the vault repo
sb reingest¶
sb reingest [TITLE...] [--config PATH] [--all]
Rebuilds source notes from their originals in raw/ with the current pipeline. It is how notes written by an older version, or by a weaker model, are upgraded; it needs no network (PDFs are read again so their figures come back).
| Argument / option | Meaning |
|---|---|
TITLE... |
Only the notes whose title contains one of these words, whatever their format |
--all |
Rebuild every note again, even those already in the current format (format: 2) |
| (neither) | Every note that is not yet in the current format |
What it keeps and what it replaces:
- Language: the note is rebuilt in the language
[notes].languagenames now, sosb reingest --allis how old notes follow a change of language. Notes you do not rebuild keep theirs. - Kept: the note's title (so every
[[link]]to it stays valid),status,read,captured,content_hash, and the tags you added. - Replaced: the
## From [[...]](or## Desde [[...]], whatever language it was written in) sections the old note added to concept and entity pages are removed and written again; a concept page nobody else cites is deleted. - Undo: before touching anything the vault is committed and tagged
pre-reingest-YYYY-MM-DD.git -C <vault> reset --hard pre-reingest-YYYY-MM-DDbrings everything back. - Resumable: a rebuilt note is marked
format: 2. If the run stops (the model went away, the Mac slept, Ctrl-C orSIGTERM, which end it withInterrupted.and exit130or143), run the command again and it continues. A signal never leaves a note half-rewritten: the note's read state is not lost either. - One git commit per note. A note whose original is missing in
raw/is skipped and listed; a note that fails is listed and the rest go on. - It takes the run lock: it will not start while
sb runworks (Another run is in progress; try again when it finishes., exit 1), and the nightly job skips its turn while it runs. - It refuses to run without git:
error: reingest rewrites every note and relies on the vault's git history to undo it; install git and run sb init first.
Expect minutes per source with a small local model.
[1/1] Active recall versus rereading
... synthesis
... connections
Vault tagged pre-reingest-2026-10-03 (undo: git -C ~/Documents/Obsidian/esbi reset --hard pre-reingest-2026-10-03)
rebuilt: 1, skipped: 0, failed: 0
sb consolidate¶
sb consolidate [--config PATH] [--all] [--only TITLE ...] [--dry-run]
Writes a consolidated ## Summary (## Resumen with language = "es") at the top of concept and entity pages, from the ## From [[source]] sections the page already has. The summary says what the concept is and what the sources add up to; the sections stay below it as the evidence. It is the manual twin of the automatic pass that sb run and sb ingest do for the pages they just wrote (up to [run].max_consolidations_per_run, which is 0, off, by default). This command has no cap.
Without options it takes the pages with three or more sources and no summary yet, or two or more sources since the last one. Each page costs one model call (two when the first answer fails its checks).
| Option | Default | Meaning |
|---|---|---|
--all |
off | Every concept and entity page with two or more sources, not only the due ones |
--only TITLE |
none | Only pages whose title contains this word, ignoring case and accents. Repeat it for several |
--dry-run |
off | List the pages that would be summarised and the number of sources each has; ask no model, write nothing |
What it checks before the page changes: the summary has 80 to 900 characters, no links, URLs or HTML, is in the notes' language, and states nothing the sections do not (a quoted phrase, a number, a capitalised name that is not in them). A failing summary is retried once. A second failure leaves the page as it was and writes the proposal to wiki/review/ as <page> - summary to review (<page> - resumen por revisar), with the reasons, for you to check. A page that email helped write is summarised only by [llm.private] or a model on this machine; if there is none, the page is skipped and listed. Pages that look like the same idea (same name ignoring case and accents, plural and singular, an acronym and its expansion, very similar titles) get a note <page> - possible merge (posible fusión) in wiki/review/: nothing is ever merged for you.
One git commit per page. It takes the run lock: while sb run works it says Another run is in progress; try again when it finishes. and exits 1. If the model goes away it stops with exit 1 (The LLM is unreachable (is Ollama running?). Nothing was lost: runsb consolidateagain.) and the next run continues with the pages still due.
[1/2] Agent harness
[2/2] Context window
consolidated: 2, skipped: 0, to review: 0, merge suggestions: 1
With nothing to do it prints Nothing to consolidate. Why the cap is off by default, and what to read first: Consolidated summaries.
sb import-legacy¶
sb import-legacy [--config PATH]
Queues every URL in an older folder's Links/*.md files (one [url] per line, oldest file first) and every PDF in its PDFs/ folder. It needs [paths].legacy_vault in the config; without it: error: set [paths].legacy_vault to an existing folder (exit 1). It never changes the old folder and is safe to repeat: known sources are counted as already known. The date of each links file becomes the note's captured date.
Queued 5 sources (0 already known).
Run again, it says Queued 0 sources (5 already known).
sb today¶
sb today [--config PATH]
Opens today's daily note. If the note does not exist yet it is built first. With viewer = "obsidian" it prints and opens an obsidian://open link (Obsidian must know the vault: open the folder once as a vault). The link is opened with open on macOS and xdg-open elsewhere; with no such command the link is printed with a note to open it yourself. With viewer = "none" it only prints the file's path, for any editor.
~/Documents/Obsidian/esbi/wiki/daily/2026-10-03.md
sb index¶
sb index [--config PATH]
Registers the sources you ticked in earlier daily notes (they become status: read), puts a read source back to processed when you unticked its box in the note of the day it was processed, then rewrites today's wiki/daily/YYYY-MM-DD.md and the managed block of Home.md, commits, and pushes to the vault's remote if it has one. It is cheap and needs no model: run it whenever you want the index refreshed. The sections of the note are in The daily index.
Marked 1 source as read.
Wrote wiki/daily/2026-10-03.md
An unticked box prints Put 1 source back to unread. (Put 2 sources back to unread.). Only the box in the daily note of the day the source was processed counts, and only when it is unticked there and ticked nowhere else in that note; a source with no box in that note is left as it is. sb run does the same before it rebuilds the index.
sb lint¶
sb lint [--config PATH]
Deterministic checks with no model. Prints a count per kind and writes wiki/review/Lint.md, which appears under To review in the daily note. The file exists only while there is something to fix. It only reports; it never edits the wiki.
| Kind | Meaning |
|---|---|
orphan |
No other page links to this one |
broken-link |
A [[link]] to a page that does not exist |
missing-field |
A page without title or summary |
unlinked-mention |
A concept's name appears in a page that does not link to it. Multi-word names match ignoring case and accents; a single word only matches exactly as written |
near-duplicate |
Two concepts or entities that look like the same idea (plural or accent variants, singular and plural such as Red/Redes, an acronym and its expansion such as IA/Inteligencia artificial, a shared alias) |
missing-concept |
A glossary term that two or more source notes define, with no concept or entity page of its own |
unlinked-mention: 1
near-duplicate: 1
Details in wiki/review/Lint.md
With nothing to report it prints No problems found.
sb ask¶
sb ask "QUESTION" [--config PATH] [--save] [--json]
Answers from the wiki only. It finds the relevant pages with full-text search (and by meaning too, when [llm.embed] is set), shows them to the model, and prints the answer plus Sources: [[...]]. Only citations that resolve to real pages are kept; an invented link becomes plain text. With no valid citation it prints I find nothing about this in the wiki. (in the notes' language) rather than an ungrounded answer (exit 0).
It also checks the sentences. Each sentence with facts in it must find its numbers, proper names and most of its key words in the pages the model was given or cited (accents, case, plurals and endings ignored; no model call). A sentence that fails gets a ⚠ after it, and one line at the end lists what was not found (Not found in the cited pages: Nobel, 1902; No encontrado en las páginas citadas: ... in Spanish). When most of the sentences fail, the answer is refused like an uncited one. A sentence that says the pages do not have the answer is not checked, and neither is an answer whose pages are all in another language than the notes. It is a vocabulary check, not a meaning check. [run].check_answers = false turns it off. Details and measurements: Ask your wiki.
| Option | Default | Meaning |
|---|---|---|
--json |
off | Print one JSON object (answer, cited, refused, saved, unsupported) instead of the text. A refused answer is a normal object with refused: true and exit 0. See JSON output |
--save |
off | File a grounded answer (with its ⚠ marks and footer, if it has any) as wiki/syntheses/<question>.md, update index.md and log.md, and commit. The page is named after your question (cut at about 80 characters); a name that already belongs to another page gets (synthesis) ((síntesis) in Spanish) |
sb ask "What is the forgetting curve and how does it relate to notes?"
The forgetting curve describes the rate at which learned material is forgotten unless reviewed [[Forgetting Curve]]. It suggests that most learned material is forgotten within days unless reviewed.
Sources: [[Spaced Repetition]], [[Forgetting Curve]]
For a concept or entity page the model reads the page's consolidated ## Summary first, then its other sections, within the same size budget. It uses [llm.ask] if set, otherwise [llm.summarize]; with run.rewrite_questions = true it first asks the model for search words in the notes' language and English. Every question is appended to .esbi/asks.jsonl (the pages retrieved and cited, how many sentences were marked unsupported, tokens, seconds). A model error prints error: ... and exits 1.
sb eval¶
sb eval [--config PATH] [--k 6] [--rewrite] [--answers]
Scores retrieval, the step where sb ask picks the pages the model will read, on questions whose source you know. The questions live in <vault>/.esbi/golden.jsonl, one JSON object per line, with the start of the expected title(s):
{"question": "What is an agent harness?", "expect": ["Code as Agent Harness"]}
| Option | Default | Meaning |
|---|---|---|
--k N |
6 | How many pages count as retrieved (sb ask uses 6) |
--rewrite |
off | First rewrite each question into search terms (one call each, with the ask model) |
--answers |
off | Also run sb ask on each question and report how many answers were grounded and cite the expected page (slow) |
With no flag it needs no model and finishes in about a second. Without the file it explains the format and exits 1. See Ask your wiki.
Retrieval on 4 questions: recall@6 75% (3/4), MRR 0.44
miss: How do I cook rice? (expected Arroz; got nothing)
sb bench¶
sb bench [--config PATH] [--models a,b,c] [--cases N]
Runs the same ingest and ask cases, built from your own vault (raw snapshots and concept pages), through each model, and prints a comparison: success rate, share needing no retry, median latency, tokens per case, estimated cost, the share written in the right language ([notes].language), concepts per source, and whether answers cite the right page. It ends with a routing suggestion and saves the report in .esbi/bench/. It reads the wiki but never writes to it and never changes config.toml.
| Option | Default | Meaning |
|---|---|---|
--models a,b,c |
[bench].models |
Comma-separated provider/model list |
--cases N |
[bench].cases (3) |
Cases per task |
A model is suggested only if it succeeds on at least 80% of its cases. Without models, or with an empty vault, it exits 1 (error: pass --models or set [bench].models in config.toml; error: the vault has no sources or concepts to test with yet). See Choose how the notes are written.
sb export¶
sb export [--out DIR] [--config PATH]
Writes the wiki as plain HTML files: a front page listing sources, concepts, entities and syntheses, one page per note, [[links]] turned into links, figures copied. raw/ is never exported.
| Option | Default | Meaning |
|---|---|---|
--out DIR |
<vault>/site |
Where to write the site |
Running it again replaces the previous export. It refuses to write into a folder that has other files in it and is not an export (error: ... is not an export of this wiki and has other files: not touching it, exit 1), so it can never wipe something else. The page loads Mermaid from a CDN, so diagrams need an internet connection; everything else works offline. See Export a website.
Exported 11 pages. Open ~/Documents/Obsidian/esbi/site/index.html
sb ocr status¶
sb ocr status [--config PATH]
Says whether reading images and scanned PDFs is on, which model it uses, whether Ollama has it, its download size, this machine's memory, and whether Ollama is new enough for it. It changes nothing and asks no model.
Reading images and scanned PDFs: on.
Model: ollama/qwen3-vl:2b-instruct
The model is installed in Ollama.
Download size: 1.9 GB.
Machine memory: 8 GB.
This model is best with 8 GB or more: this machine can likely run it.
Ollama: 0.13.5 (the model needs 0.12.7 or later).
Without an [llm.ocr] section it says off (never set up) and prints the recommended model. With enabled = false it says off and that the model choice is kept. If Ollama cannot be reached it says so instead of guessing.
sb ocr enable¶
sb ocr enable [--model NAME] [--pull] [--config PATH]
Turns reading images and scanned PDFs on by writing the [llm.ocr] section of the config (adding it, or setting enabled = true and the model; the rest of the file is not touched), then says whether Ollama has the model.
| Option | Default | Meaning |
|---|---|---|
--model NAME |
the model already in the config, else the recommended qwen3-vl:2b-instruct |
Which Ollama vision model to use. Only an Ollama model on this machine works |
--pull |
off | Download the model now if Ollama does not have it. Without it, at a keyboard it asks Download it now? (default No); in a script it only prints the ollama pull command |
It warns, and never blocks, when this machine has less memory than the model wants or Ollama is older than the model needs. deepseek-ocr:3b is a known choice for a machine with 16 GB of memory or more; it is never the default.
Reading images and scanned PDFs is on, with qwen3-vl:2b-instruct. Changed: [llm.ocr] in ~/.config/esbi-cli/config.toml
qwen3-vl:2b-instruct is already installed.
sb ocr disable¶
sb ocr disable [--config PATH]
Turns it off by setting enabled = false in [llm.ocr]; the model choice stays, so sb ocr enable brings it back without typing. Images in inbox/ are then listed by sb scan as not read, and scanned PDFs are rejected. When it is already off it says so and changes nothing.
sb setup email¶
sb setup email [--config PATH]
A guided wizard (bash, bundled with the app) that connects a Gmail mailbox in six stages: turn on 2-Step Verification, create the esbi-cli label, route you+esbi-cli@gmail.com into it, create an app password, save the settings and the password (the password goes straight into the Keychain), and test it. Stop with Ctrl-C and run it again: nothing changes until stage 5. The full guide is Capture email.
sb setup clipper¶
sb setup clipper [--config PATH]
A guided wizard in five stages: install the Obsidian Web Clipper extension, point it at your vault, import the two templates, clip a test page, and let the worker pick it up. It needs Obsidian: with viewer = "none" it explains that and exits 0. See Web Clipper and YouTube.
sb email fetch¶
sb email fetch [--config PATH]
Saves mail from the configured Gmail label as clip notes (and PDF and image attachments) in inbox/, skipping mail already saved (by Message-ID). It takes every unread mail in the label however old, every mail added to the label after the highest UID it has taken (remembered per mailbox generation in .esbi/mail-state.json), and every mail of the last 14 days counted from the day it was received: see Which mail is taken. It marks a mail as seen only after its note is written and never deletes mail. sb run does this for you.
Mail: 2 saved, 0 duplicates, 0 failed, 1 images saved, 3 links queued.
The last counts appear only when they are not zero, and there is one more, N not marked as read (the server refused), for mail that was saved but whose read flag the server would not set. images saved counts PNG, JPEG, WEBP and TIFF attachments kept in inbox/ (the bytes decide what a file is; pictures under 200 px or a few KB, such as signatures and tracking pixels, over 5 MB, or beyond 15 MB per mail are skipped); they are read later with the local [llm.ocr] model when it is on. links queued counts the links put in the queue when [email].follow_links is on: they are not fetched here (see Links in a mail). Both count as email for the privacy rules.
Needs [email].enabled = true and [email].user; otherwise error: email is disabled: set [email].enabled = true in config.toml (exit 1). A login or label error is printed with its cause and exits 1.
sb email configure¶
sb email configure --user ADDRESS [--label NAME] [--config PATH]
Turns on mail capture in config.toml without the wizard: replaces the [email] block (or adds it) and touches nothing else. The password is stored separately with sb email set-password.
| Option | Default | Meaning |
|---|---|---|
--user ADDRESS |
required | The Gmail address that receives your forwards |
--label NAME |
esbi-cli |
The Gmail label to read |
Mail capture on for you@gmail.com, label esbi-cli, in ~/.config/esbi-cli/config.toml
sb email set-password¶
sb email set-password [--stdin] [--config PATH]
Prompts (hidden) for the mailbox app password and stores it in the macOS Keychain, service esbi-cli-imap (off macOS, which is not supported, in the system keyring through a Secret Service such as GNOME Keyring or KWallet; where there is none, it says there is no keyring and where email capture works: see Email where there is no keyring). Spaces in a Gmail-style password are dropped. It needs [email].user in the config first. There is deliberately no --password VALUE option: a value on the command line is kept in your shell history and visible in the process list.
| Option | Meaning |
|---|---|
--stdin |
Read the password from standard input, to paste without typing: pbpaste \| sb email set-password --stdin. Clear the clipboard afterwards |
App password for you@gmail.com:
Saved to the Keychain.
(Saved to the system keyring. off macOS.)
sb schedule install¶
sb schedule install [--config PATH] [--agents-dir DIR]
Writes and loads the launchd agent com.esbi-cli.nightly that runs sb run --if-due at [run].nightly_time, at login, and hourly as a retry net (macOS only). It uses the environment that is running sb, so it works wherever that lives, and the config is resolved to an absolute path. Run it again after changing nightly_time, and after moving or reinstalling sb.
| Option | Default | Meaning |
|---|---|---|
--agents-dir DIR |
~/Library/LaunchAgents |
Where launchd agents live |
Installed ~/Library/LaunchAgents/com.esbi-cli.nightly.plist
Runs every day at 03:00 (or at the next wake). Log: ~/Documents/Obsidian/esbi/.esbi/logs/nightly.log
The first run needs a macOS permission: see Run it every night. On a system without launchd (not macOS, so not supported), the command stops with exit code 1, writes nothing and prints the cron line that does the same, the one sb init --nightly prints:
error: the nightly job uses launchd, which only macOS has. On this system add this line to cron (`crontab -e`; every hour: the first tick after [run].nightly_time runs the nightly batch, later ones drain what is queued, up to [run].max_batches_per_day batches a day):
0 * * * * /home/you/.local/share/uv/tools/esbi-cli/bin/sb run --if-due --config /home/you/.config/esbi-cli/config.toml
The line names sb by its absolute path (cron's PATH is /usr/bin:/bin, so a bare sb installed in ~/.local/bin would never be found), quotes a path with spaces and writes a % as \%.
sb schedule status¶
sb schedule status [--agents-dir DIR]
Prints whether the agent file exists (installed:) and whether launchd has it loaded (loaded:). Without launchd there is nothing to query, so it prints the same cron advice as sb schedule install and exits 0.
installed: yes
loaded: yes
sb schedule uninstall¶
sb schedule uninstall [--agents-dir DIR]
Unloads the agent and deletes its file. It also removes a job installed under the old project name. Prints Removed. or Nothing to remove. Without launchd it prints the same cron advice and exits 1: remove your cron line with crontab -e.