Skip to content

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 uninstall and sb init do not read a config (sb init writes one: --config-file). A --config path that does not exist is an error (error: config file not found: PATH), not a reason to use another file. A config whose [paths].vault folder does not exist is an error too (error: vault not found: PATH (runsb init, or fix [paths].vault), --json code vault_not_found) for every command that opens the vault: nothing is created. sb doctor, sb init and the commands that only edit the config (sb ocr ..., sb email set-password) still work.
  • Exit codes. 0 is success. 1 is an error the command already explained on the screen (a missing config, a bad argument, the model is down, a FAIL line in sb doctor). 2 is a usage mistake caught by the command-line parser (an unknown command or option); it prints the usage and a hint. 130 and 143 are a stop you asked for: Ctrl-C (SIGINT) and SIGTERM, the conventional 128 plus the signal number. They come from sb run, sb ingest and sb 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: /path for example, with exit 1 and no traceback (--json: code os_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, ask and run accept --json and 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, or zsh answers no matches found before sb starts: sb add "https://example.com/post?id=7".
  • Shell completion. sb --install-completion installs it for the current shell, sb --show-completion prints 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 from inbox/, counted and not queued.
  • A different file with the same name is kept under a new name (paper (2).pdf). Nothing in raw/ 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:

  1. Fetch mail from the mailbox (only if [email].enabled; a mail problem is a warning, never fatal). With [email].follow_links the links in new mail are queued here, not fetched.
  2. Scan inbox/ and queue what is there.
  3. Put back in the queue anything a crashed run had claimed.
  4. Ingest queued sources one by one; every success is a git commit in the vault.
  5. Record the run.
  6. Write consolidated summaries for the pages this run touched that are due, up to [run].max_consolidations_per_run (0, off, by default: see sb consolidate). Skipped after an outage.
  7. Lint the wiki.
  8. 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].language names now, so sb reingest --all is 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-DD brings everything back.
  • Resumable: a rebuilt note is marked format: 2. If the run stops (the model went away, the Mac slept, Ctrl-C or SIGTERM, which end it with Interrupted. and exit 130 or 143), 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 run works (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 \%.

See cron on other systems.

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.