An unofficial community CLI for Capacities — script and automate your knowledge base from the terminal.
Beta. This is a community tool built while an official CLI does not exist. Once Capacities ships an official CLI, this project will align its UX with the official tool and retire or evolve depending on what the official one covers. If the official one is open-sourced, relevant contributions will go upstream.
- Multiple spaces — manage personal, work, and other Capacities spaces from one tool, switching with a flag or an environment variable
- Local cache — search results are cached for 10 minutes, object content for 1 hour, structure definitions for 24 hours; most repeated reads hit disk, not the API
- Secure secrets — tokens are encrypted with age before being written to disk; the CLI never echoes or logs them
- Scripting-friendly —
--jsonoutput,--quiet+ exit codes,CAPACITIES_TOKENfor CI
This is not a replacement for the Capacities MCP server. MCP is ideal for AI-assisted workflows inside editors; this CLI is for shell scripts, cron jobs, and terminal workflows where you want direct control.
npm install -g capacities-cliRequires Node >= 20.
age encrypts your API tokens at rest. The private key lives only on your machine.
capacities auth keygen
# Writes ~/.age/key.txt — never commit this file
# Prints the public recipient key (used internally for encryption)Why age? Your Capacities API token grants full read/write access to your knowledge base. Storing it as plaintext in a config file means any process that can read your home directory can exfiltrate it. age encryption means the token is useless without the key file, which stays offline and out of source control.
capacities auth add personal
# Opens $VISUAL / $EDITOR with a config template
# Paste your API token, save, and quit
# Token: Capacities → Settings → Capacities API → New TokenThe CLI verifies the token is live before saving. Secrets are encrypted and stored at ~/.config/capacities/spaces/personal.age. The .age file is safe to commit to a dotfiles repo; the key file is not.
capacities search "project"
capacities get <objectId># Auth / space management
capacities auth add <name> # add a space (editor-based, token never echoed)
capacities auth edit <name> # rotate or change credentials for a space
capacities auth use <name> # set the default active space
capacities auth list # list configured spaces (* = active)
capacities auth remove <name> # remove a space, its secrets, and its cache
capacities auth keygen # generate age keypair → ~/.age/key.txt
# Content
capacities search <query> [--type <type>] # search objects (cached 10 min)
capacities get <objectId> # get object as markdown (cached 1h)
capacities link <objectId> <propertyKey> <targetId...> # set entity field (one call, N targets)
capacities create --type <type> --title <title> [--desc <desc>] [--tags <tags>] \
[-f key=value ...] [--markdown <path|->]
capacities update <objectId> <propertyKey> <value>
capacities update <objectId> --props <file|-> # apply frontmatter props only
capacities append <objectId> [content] [--markdown <file|->] [--position end|start]
capacities daily-note <markdown|-> [--date <YYYY-MM-DD>] [--no-timestamp]
capacities validate --type <type> [--json] # reads stdin, writes corrected markdown
capacities types # table: name | structureId
capacities types <name> # field table for named type
capacities types --name <name> # bare structureId (shell-friendly)
capacities open <objectId> # open in web app (prints URL if headless/CI)
capacities open <objectId> --print # always print URL, never open browser
# Save
capacities save url <url> [--title <t>] [--desc <d>] [--markdown <notes>] # save a URL as a weblink/media object
capacities save file <path> [--title <t>] [--collections <uuid,...>] # upload a local file as a media objectThe -f / --field flag on create injects a custom property line into the
frontmatter (repeatable: -f ring=Trial -f quadrant=Tool). --markdown
reads a full frontmatter+body blob from a file or stdin (-); when set,
--title is optional and --field is ignored.
update has two modes: <propertyKey> <value> updates a single named property
(resolves label values automatically); --props <file> reads YAML frontmatter
from a file and applies each key as a property update via PATCH /object/markdown.
Body content after the closing --- is ignored by the API — use append instead.
append adds markdown content to an object's body via POST /blocks/append. The
API converts the markdown to blocks and inserts them at the specified position
(end by default, or start). Pass content inline, via --markdown <file>, or
pipe it with --markdown -. This is the only way to add body content via the
CLI — update --props only touches properties.
daily-note appends content to the daily note. Pass "-" to read from
stdin, making it composable with any pipeline. --date backfills a past
date; --no-timestamp suppresses the automatic header Capacities adds.
save url saves a URL as a Capacities object. Capacities auto-detects the type from the URL content (weblink, image, PDF, audio, video). Use --markdown to attach inline notes to the saved object. save file uploads a local file; the object type is inferred from the file extension (MediaImage, MediaPDF, MediaAudio, MediaVideo, or MediaFile). Use --collections to assign the file to one or more collections on creation.
open opens an object in the Capacities web app. In headless environments
(CI, piped output) it prints the URL instead of launching a browser. Pass
--print to always print the URL regardless of environment — useful for
copying or composing with other commands.
types lists all object types defined in your space, with their Capacities
structureIds. Pass a type name as a positional argument to see the field
definitions for that type — field names, types, and allowed values for
label fields. The --name flag prints just the bare structureId, suitable
for shell substitution into other commands.
validate reads frontmatter+body from stdin, checks it against the live
type definition, and writes corrected markdown to stdout. Warnings (field
casing, label normalisation, auto-filled fields) go to stderr. Exit 0 means
valid; exit 1 means a required field is missing. Pipe directly into
create --markdown - for a validate-then-create workflow:
echo "$frontmatter" | capacities validate --type Blip \
| capacities create --type Blip --markdown - --quiet-s, --space <name> override active space for this call
--json output raw JSON (pipe to jq)
-q, --quiet suppress output; use exit code only
--no-color disable ANSI colours (or set NO_COLOR)
--debug verbose logging for this invocation
Every read is cached locally to avoid redundant API calls:
| Command | Cache TTL | Cache key |
|---|---|---|
search |
10 minutes | query + type |
get |
1 hour | object ID |
| structure list (internal) | 24 hours | per space |
Cache is stored under ~/.cache/capacities/<space>/. After mutations (link, create, update, append) the affected object's cache entry is busted automatically.
To clear everything for a space: capacities auth remove <name> followed by capacities auth add <name>.
After every mutation the CLI fetches the updated object and writes it locally:
~/.local/share/capacities/<space>/objects/<type>/<title>.md
The format matches Capacities' markdown export, so the directory works as a plain-text mirror of your mutated objects. Point CAPACITIES_OBJECTS_DIR at a git-tracked folder to keep a version-controlled log of changes.
capacities auth add personal
capacities auth add work
capacities auth use personal # set default
capacities search "meeting notes" # uses personal
capacities search "sprint" --space work # override for one call
CAPACITIES_SPACE=work capacities get <objectId> # via env varFor environments where the age key file is unavailable, pass the token directly:
CAPACITIES_TOKEN=cap-api-... capacities search "release"For CI with age available, inject the private key inline:
CAPACITIES_AGE_KEY="$(cat ~/.age/key.txt)" capacities get <objectId>Config file: ~/.config/capacities/config.toml (XDG paths; Linux defaults apply on macOS)
active_space = "personal"
[spaces.personal]
objects_dir = "~/.local/share/capacities/personal/objects"No secrets in config.toml — safe to commit to a dotfiles repo.
| Variable | Purpose |
|---|---|
CAPACITIES_CONFIG |
override config file path |
CAPACITIES_SPACE |
override active space |
CAPACITIES_TOKEN |
plaintext API token (for CI without age) |
CAPACITIES_AGE_KEY_FILE |
path to age private key (default: ~/.age/key.txt) |
CAPACITIES_AGE_KEY |
inline age private key (for CI secrets injection) |
CAPACITIES_OBJECTS_DIR |
override write-through mirror directory |
CAPACITIES_CACHE_DIR |
override cache root |
CAPACITIES_LOG_LEVEL |
debug|info|warn|error|silent (default: warn) |
NO_COLOR |
disable ANSI colours |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Unexpected error |
| 2 | Config / auth error |
| 3 | API error |
| 4 | Not found |
| 5 | Rate limit exceeded |
npm install
npm run dev -- search "test" # run via tsx without building
npm run build # compile → dist/index.js
npm test # unit + integration tests
npm run coverage # coverage report- Unit (
vitest+vi.mock): command logic, cache TTL expiry, error code mapping - Integration (
mswv2 +openapi-backend): full command → SDK → HTTP, requests validated against the committedopenapi.jsonspec
npx vitest run tests/integration| Language | TypeScript |
| SDK | @capacities/api (official Capacities SDK) |
| CLI framework | Commander.js |
| Config | TOML via smol-toml |
| Secrets | age-encryption (FiloSottile's JS port) |
| Build | tsup |
| Tests | vitest, msw v2, openapi-backend |