| name | craft-cli |
| description | Fast CLI wrapper over the Craft Docs "API for All Docs" for reading, searching, and editing Pavel's Craft vault from the shell. Triggers when the user mentions craft docs, the PKM vault, LTM, daily notes, searching or editing craft content, or says "c:" / "in craft". Prefer this CLI over the Craft MCP server for bulk scans, multi-doc edits, scripting, or anything repetitive. Use the MCP server for small interactive one-offs when the CLI is absent. |
craft-cli — Craft Docs from the shell
craft is a compiled Bun binary at ~/.local/bin/craft. Source: ~/dev/tools/craft-cli/. Library exports at @1ar/craft-cli/lib for Raycast/Node reuse.
When to use this vs the Craft MCP server
- Use
craft CLI: bulk scans across docs, tag renames, anything touching >5 blocks, scripted pipelines (pipe to jq, grep), cases where MCP's rate limits bite, anything you want to repeat via shell history.
- Use Craft MCP (
mcp__claude_ai_Craft__*): single interactive read of a known block, when the CLI isn't installed, quick one-off edits.
- Both are safe: they hit the same API. The CLI is just faster and more scriptable.
Setup check
command -v craft >/dev/null && craft whoami
If that fails → craft setup --url <URL> --key <KEY>. Credentials live at ~/.config/craft-cli/config.json (0600). Main profile is already configured for the 1ar space.
Env overrides: CRAFT_URL, CRAFT_KEY, CRAFT_PROFILE, CRAFT_SOURCE (see Source section), legacy CRAFT_MODE, CRAFT_LOCAL_PATH, CRAFT_LOCAL_TIMEOUT_MS.
Command cheatsheet
craft doctor --json
craft agent-context
craft which backlinks
craft whoami
craft profiles list
craft source
craft source api
craft source auto
craft source local
craft mode api
craft mode hybrid
craft folders ls
craft folders mk "New Project" --parent <folderId>
craft folders rm <id>
craft docs ls [--location unsorted|trash|templates|daily_notes] [--folder ID]
craft docs search "regex" [--folder ID] [--include] [--fetch-blocks]
craft docs get <id>
craft docs get <id> --raw
craft docs get <id> --json
craft docs get <id> --depth 1
craft docs get <id> --no-links
craft docs get <id> --exhaustive
craft docs daily [DATE]
craft docs mk "Title" --folder <id>
craft docs mv <id> --to <folderId|unsorted|templates>
craft docs rm <id>
craft docs open <id>
craft blocks get <id> [--depth N]
craft blocks search <docId> "regex" [--before 2 --after 2 --fetch]
craft blocks append <docId> --markdown "text"
craft blocks append --date today --markdown "daily note line"
echo "## piped content" | craft blocks append <docId> -
craft blocks insert <docId> --file blocks.json
craft blocks update <blockId> --markdown "new text"
craft blocks mv <blockId>... --to <pageId>
craft blocks rm <blockId>...
craft tasks
craft tasks ls [all|inbox|active|upcoming|logbook]
craft tasks ls document --doc <id>
craft tasks ls --state todo --document "Project" --deadline-to tomorrow
craft tasks ls --doc <id> --scheduled none --reminder no --json
craft tasks ls --date today --location daily --text "report" --limit 20
craft tasks ls --repeat yes --notification yes
craft tasks add "buy milk" --to inbox
craft tasks add "review doc" --to daily --date today --schedule tomorrow
craft tasks update <id> --state done
craft tasks update <id> --schedule none --deadline tomorrow --to doc --doc <id>
craft tasks rm <id>
craft col ls [--doc ID]
craft col schema <collectionId>
craft col items <collectionId>
craft col items add <id> --file items.json
craft col items update <id> --file updates.json
craft col items rm <colId> <itemId>...
craft col views <collectionId>
craft col views create <collectionId> --file view.json
craft col views update <collectionId> <viewId> --file view.json
craft col views active <collectionId> <viewId>
craft col views rm <collectionId> <viewId>
craft links out <blockId>
craft links in <blockId>
craft links in <blockId> --text "custom label"
craft links in <blockId> --exhaustive
craft patch <docId> --old "existing text" --new "replacement text"
craft patch <docId> --old "text" --new "text" --dry-run
echo "old text\n---\nnew text" | craft patch <docId>
craft cat <id1> <id2> <id3>
craft diff <docId|blockId>
craft undo [docId]
craft undo --dry-run
craft undo --force
craft log [docId]
craft log --last 5
craft log --since 2026-04-01
craft upload photo.png --parent <docId>
craft comment <blockId> "nice point #by/claude"
craft wb mk --parent <docId>
craft wb el add <wbId> --file elements.json
craft skills ls
craft skills search media
craft skills show media-analyze
craft skills validate media-analyze
craft skills run media-analyze analyze <blockId> --estimate
craft media analyze <blockId>
craft media analyze <blockId> --estimate
craft media analyze <blockId> --max-cost 0.50 --json
craft raw GET /connection
craft raw POST /blocks --body payload.json
Global flags on every command: --json (machine output), --select id,title (project JSON fields), --profile NAME, --quiet, --source auto|api|local, --api (legacy shortcut for --source api), --dry-run on write commands.
Skills
craft skills discovers bundled repo skills and explicit local skills from ~/.craft-cli/skills. V1 has no remote/community install flow. Search is manifest keyword search over name, description, tags, and command descriptions.
Skill runtime contract:
- skills execute as subprocesses with structured JSON stdin/stdout.
craft-cli fetches Craft context for commands that declare a source block.
- skill code may propose writes, but
craft-cli performs Craft writes and journal records.
- default max cost is EUR 1 unless
--max-cost is passed.
--estimate returns the manifest estimate without running expensive work.
Bundled media skill:
craft media analyze <blockId>
Alias for craft skills run media-analyze analyze <blockId>. It downloads media to ~/.cache/craft-cli/media-analyze, uses OpenAI for generic analysis/transcription, and writes a Craft-visible run block under the source block with status, analysis, transcript when available, contact-sheet path, metadata JSON, and model/cost metadata. Requires OPENAI_API_KEY; video/audio extraction needs ffmpeg/ffprobe.
craft doctor and craft whoami use short health-check retries/timeouts. If Craft's /connection endpoint stalls, they should fail quickly with a clear timeout instead of hanging through normal API retry windows.
Read source: auto vs api vs local
On Mac with Craft app installed, the CLI reads from Craft's local SQLite FTS5 database for docs ls and docs search (1700x faster than API). All writes always go through the API.
Three sources:
- auto (default): try local first with a bounded helper-process probe; fall back to API. Use on Mac with Craft installed.
- api: never touch local, every read hits the API. Use on Linux, Docker containers, or any host where Craft is not installed. Slower reads but identical behavior; journal (undo/log/diff) keeps working.
- local: local-only. Fails clearly if Craft Desktop data is unavailable or the query needs API-only filters. Use for debugging local cache behavior.
How to set it (agent workflow):
craft source
craft source api
craft source auto
craft source local
craft source --json
craft source api prints a status block the agent should relay to the user — it confirms the persisted state, tells the user journal still works, and shows how to temporarily flip the source.
Precedence (highest wins):
- Per-command
--source auto|api|local
- Per-command
--api shortcut for --source api
CRAFT_SOURCE=auto|api|local env var
- legacy
CRAFT_MODE=api|hybrid env var (hybrid maps to auto)
- persisted
config.source set via craft source <source>
- legacy
config.mode
auto default when nothing is configured
When to run craft source api:
- Linux hosts, Docker containers, any headless box without the Craft desktop app
- Mac machines where the Craft app is installed but not running / not syncing (prevents stale local reads)
- CI / scripted environments where you want deterministic API-only behavior
The journal at ~/.cache/craft-cli/journal.db is cross-platform and always on — undo, log, and diff work with every source.
Hybrid local reads are bounded by CRAFT_LOCAL_TIMEOUT_MS (default 1500ms). If local discovery/list/search times out or errors, read commands fall back to the API without printing local document content to logs.
Top recipes
1. Fetch a specific doc by title
id=$(craft docs search "^LTM$" --json | jq -r '.items[0].documentId')
craft docs get "$id"
Or for a fuzzy title → use --include mode (case-insensitive phrase match):
craft docs search "LTM" --include --json | jq -r '.items[].documentId'
2. Read today's daily note
craft docs daily
With structured content: craft docs daily --json --depth 1
3. Append to today's daily note
craft blocks append --date today --markdown "15:42 #by/claude idea: …"
4. Rename a tag across the entire vault
craft docs search '#type/idea' --fetch-blocks --json |
jq -r '.items[].blocks[] | select(.markdown | contains("#type/idea")) | .id' |
while read -r blockId; do
old=$(craft blocks get "$blockId" --json | jq -r '.markdown')
new=${old//#type\/idea/#idea}
craft blocks update "$blockId" --markdown "$new"
done
Or reuse the old rename-tag.ts (it's still in ~/dev/craft-docs/craft-do-api/).
5. Explore tasks across the space
craft tasks --state todo --json | jq '.items[] | {id, state: .taskInfo.state, location, task: .markdown}'
craft tasks ls --document "Project" --deadline-to tomorrow --reminder yes --json
Task list filters compose locally after one API call:
- content/location:
--text TEXT, --doc ID, --document TITLE, --location inbox|document|daily
- state/date:
--state todo|done|canceled, --date, --date-from, --date-to, --scheduled, --scheduled-from, --scheduled-to, --deadline, --deadline-from, --deadline-to, --overdue
- task configuration:
--repeat yes|no, --reminder yes|no (--notification alias), --priority VALUE, --limit N
- dates accept
YYYY-MM-DD, today, yesterday, or tomorrow; --scheduled none and --deadline none select tasks without those dates
- Craft currently defines practical priority through schedules and deadlines and does not expose a native task-priority field.
--priority is forward-compatible for API payloads that include one; --priority none selects tasks without one.
6. Add a task without leaving the terminal
craft tasks add "call the accountant about VAT" --to inbox
7. Bulk scan + extract via jq pipeline
craft docs search "#ref" --fetch-blocks --json |
jq -r '.items[] | .markdown'
8. Find all backlinks to a document
craft links in <blockId>
craft links in <blockId> --text "the phrase used in the link"
craft links in <blockId> --exhaustive
9. Get a deeplink to open in the Craft app
craft docs open <id>
craft docs search "query" --json | jq -r '.items[0].documentId' | xargs craft docs open
10. Surgical edit (patch) — the Edit tool for Craft
craft patch <docId> --old "misspelled wrods" --new "misspelled words"
Finds the exact block containing the old text, replaces it, journals the change. Like Claude Code's Edit tool but for Craft blocks.
11. Review what changed since your last edit
craft diff <docId>
12. Oops, undo that
craft undo
craft undo <docId>
craft undo --dry-run
13. Read multiple docs at once
craft cat <id1> <id2> <id3>
Caveats (from real trials — see ~/dev/craft-docs/craft-do-api/trials/CAVEATS.md)
docs search defaults to regexps mode. The API's include mode silently misses tokens with underscores. Use --include only for phrase/word matching, stick with the default for anything else.
- Regex is RE2. Escape backslashes for the shell:
craft docs search 'tag_\w+'.
docs get strips the <page>/<pageTitle>/<content> wrapper by default. Pass --raw if you need the original, or --json for structured blocks.
- The CLI refuses to insert blocks without an explicit target. The API silently routes
position: end with no pageId/date to today's daily note — a footgun. The CLI throws before sending.
maxDepth: 0 omits the content key entirely (not an empty array). Use "content" in obj checks when parsing.
- Error exit codes: 0 ok, 1 user error, 2 API error, 3 auth, 4 not found. Script accordingly.
- Large list latency:
craft docs ls with no filter takes ~3.4s via API. In source auto on a Mac with Craft installed, it's instant (~27ms). On Linux / headless hosts, run craft source api once after setup to skip local discovery entirely, or pass --source api/--api per-command, or set CRAFT_SOURCE=api in the environment.
- Rate limits: generous. 60 parallel calls tested without 429. Default concurrency in scan pipelines can be 15+.
- Tasks & collections have inconsistent payload keys (
tasks vs tasksToUpdate vs idsToDelete). The CLI abstracts this — you don't need to care unless you use craft raw. The live task API also accepts undocumented scope=all; craft tasks uses it by default so unscheduled document tasks are not lost.
- Partial block updates preserve children.
craft blocks update <id> --markdown "new" renames without dropping the sub-tree.
- Daily note auto-creates when you append with
--date today and no note exists yet.
- Always tag AI-generated content with
#by/claude in the markdown (Pavel's global rule, see ~/.claude/CLAUDE.md).
- Links & backlinks:
- Outgoing links are free — every
[text](block://UUID) reference is already in the block's markdown after a normal fetch. craft links out just parses it.
- Incoming links (backlinks) are NOT natively supported. Craft's search index strips
block://UUID URIs — searching for a raw UUID returns zero hits. The CLI uses Pavel's trick: the visible anchor text of a link IS indexed, and Craft's default link text is the target's title, so docs/search for the title followed by a local block://<id> filter finds backlinks in one API call. Set --text when authors use custom labels. Fall back to --exhaustive only when the fast path looks suspiciously empty.
docs get / blocks get / docs daily include backlinks by default. In markdown mode they append a ## Backlinks section; in JSON mode they add a top-level backlinks array. Pass --no-links to skip when you only need content and want to save ~1-2s.
clickableLink lives at metadata.clickableLink on GET /blocks responses when fetchMetadata=true, and at the top level on list/create responses.
- Search freshness lag: newly created child pages may appear in parent reads (
docs get <parentId> --depth N) before they show up in docs search. If search misses something recent, fetch the parent with depth as a fallback: craft docs get <parentId> --depth 2 or craft docs daily --depth 2.
- Typed block insert fidelity:
craft blocks insert --file blocks.json accepts every native block variant (text, page, richUrl, video, image, file, line, code, table) with its native fields (url, title, description, listStyle, textStyle, color, decorations, nested content, etc.). Use this — not blocks append --markdown — when copying blocks between docs, because append goes through markdown parsing and loses native types (video/richUrl/image collapse to text).
- r.craft.do URLs are signed and time-limited — they rotate on each
GET /blocks fetch. When cloning video/image/file blocks between docs, always fetch the source LIVE right before inserting and pass the fresh URL; omit uploaded so the API re-fetches and re-signs. If you pass a stale URL with uploaded: true, the block will be created but the asset will display "not available" when the signature expires. normalizeCraftMediaBlocks is available as an opt-in helper for the rare case where you want to force-store an as-is URL.
Library usage (Raycast / Node scripts)
import { CraftClient } from "@1ar/craft-cli/lib";
const c = new CraftClient({ url: process.env.CRAFT_URL!, key: process.env.CRAFT_KEY! });
const hits = await c.documents.search({ regexps: "LTM|memory" });
const fullDoc = await c.blocks.get(hits.items[0]!.documentId, { format: "markdown" });
Files
- CLI source:
~/dev/tools/craft-cli/
- Compiled binary:
~/dev/tools/craft-cli/dist/craft → symlinked to ~/.local/bin/craft
- Config:
~/.config/craft-cli/config.json (mode 0600)
- API docs:
~/dev/craft-docs/craft-do-api/craft-do-api-docs.md
- OpenAPI spec:
~/dev/craft-docs/craft-do-api/craft-do-openapi.json
- Trial fixtures + caveats:
~/dev/craft-docs/craft-do-api/trials/
- Rebuild:
cd ~/dev/tools/craft-cli && bun run build
- Tests:
bun test (unit), bun test tests/integration (gated on CRAFT_URL+CRAFT_KEY)