Plansmith CLI

Plan, spec, and push from your terminal. Zero dependencies, every command scriptable.

Review documents and follow conversations

Use plansmith discussion <operation> --file request.json --json for threads, replies, suggested edits and mentions. plansmith discussion help lists the supported operations. plansmith inbox list --unread --json reads your Inbox; inbox preferences reads email preferences. See Comments and suggested edits for an example request and retry behavior.

Install and sign in

npm install -g plansmith.co
plansmith auth

Signing in uses the device flow: the CLI shows a short code, your browser confirms it, and the CLI receives a token bound to the workspace you picked.

The package is published as plansmith.co; the binary it installs is plansmith.

Staying current

Features reach this page when they ship in the product, and reach your machine when you upgrade, so the two can drift. The CLI closes the gap itself: it reads the current version from the API and tells you once a day when yours is behind.

A newer plansmith is available (0.3.0 → you have 0.2.0).
npm i -g plansmith.co

That line goes to stderr and never appears under --json, off a terminal, or when PLANSMITH_NO_UPDATE_NOTICE is set, so it cannot get into piped data. You can always ask directly:

plansmith --version
curl -s https://api.plansmith.co/v1 | jq -r .cli.latest

Anything on this page that needs a particular version says so.

Everyday commands

plansmith use recipe-box            # pin a default project
plansmith plan list                 # plans you can see
plansmith plan tree PLAN-12         # specs, sub-plans, dependencies
plansmith spec show PLAN-12/SPEC-3
plansmith context add ./notes.md --plan PLAN-12
plansmith plan status PLAN-12 READY
plansmith push PLAN-12 --to linear

A plan can be named three ways: fully qualified (recipe-box/PLAN-12), with --project, or bare once you have pinned a default with plansmith use.

Adding images

plansmith image add ./checkout.png --plan PLAN-12 --spec SPEC-3 --alt "Checkout screen"
plansmith image add https://example.com/flow.png --plan PLAN-12 --epic
plansmith image add ./wireframe.webp --plan PLAN-12        # stored on the plan, printed as markdown

PNG, JPEG, GIF or WebP up to 10 MB. With --spec or --epic the image goes at the end of that document; without either it is stored on the plan and the command prints the ![alt](url) markdown to paste anywhere. A file over 4 MB uploads straight to storage, so large screenshots work too. A URL must be public https; the server fetches it and follows no redirects. If the document changed while the command ran, it retries once with the image it already uploaded, so a retry never stores a second copy. See Images in documents.

Pulling a plan to your machine

plansmith plan pull PLAN-12 --out plan            # epic.md, specs/SPEC-n.md, index.json
plansmith plan pull --check --out plan            # exit 0 when current, 1 when behind
plansmith plan pull --check --out plan || plansmith plan pull PLAN-12 --out plan

plan pull saves the plan as files an agent can read without parsing markdown. Each spec lands in specs/SPEC-n.md exactly as it is stored, the epic in epic.md, and index.json describes all of it as data: for each spec its title, issue tracker key, parent, what it needs first, status, readiness, open questions, a heading outline, and its acceptance criteria with id, text, checked and body (the lines written under the criterion, such as a Given/When/Then block). Read the criteria from index.json rather than from the markdown: they are parsed once, by Plansmith, with the same rules its own checks use. Without --out the folder is named after the plan.

Every spec and the epic carry a revision, a hash of what the file says. --check compares the folder's revisions with Plansmith's and writes nothing: it exits 0 when they match and 1 when they do not, listing each spec that changed, was added or was removed, a new plan name or epic, and specs that moved to a new order. A status or readiness change alone does not count, because the files would not change; a new issue tracker key does. --check reads the plan from the folder's index.json, so you can leave it out, and it refuses a folder pulled from another workspace rather than compare it with a plan that shares the handle. Add --json for { upToDate, changes }.

A pull replaces the folder in one step, so a reader never sees half of one. It only replaces a folder that is empty or an earlier pull of the same plan from the same workspace. A folder of your own files is refused, and so is the folder you are standing in. An earlier pull of another plan, or one holding files the pull did not write (notes you or an agent added beside the specs), needs --force, which deletes them. Keep your own files and any earlier version outside the folder; a pull keeps no history. The same index comes back from GET /v1/projects/{project}/plans/{plan}/export?format=index and from the MCP tool plansmith_export_plan with format: "index".

Building a repeatable coding-agent workflow from these files? Build a skill around a plan covers the package layout, context and prototype pulls, coverage checks, fresh review loop, publication and readback.

Reading and maintaining formats

plansmith format show spec --project web --json
plansmith format set spec --file format.md --project web --expected-revision absent
plansmith format reset epic --project web --expected-revision <hash>

Omit --project for company scope. Reads include the effective source, direct document links, and the destination's overrideRevision. Workspace owners can save and remove either spec or epic formats at both scopes. An active workspace and write credential are required.

Use the reviewed hash with --expected-revision, or absent to create a new override. If omitted, the CLI reads the current destination revision before writing. A conflict requires another read and review. Reset returns the inherited format now in force. See Formats.

Moving context around

plansmith context add ./report.pdf --plan PLAN-12 --folder research
plansmith context add ./design.md --plan PLAN-12 --folder docs --replace
plansmith context list --plan PLAN-12                                  # ids, folders, sizes
plansmith context download --plan PLAN-12 --out plan-context.zip
plansmith context download --plan PLAN-12 --folder research --out research.zip
plansmith context copy --from PLAN-12 --to PLAN-19 --dry-run

context add <file> uploads the file itself for the types Plansmith can read as files (.pdf, .docx, .doc, .xlsx, .pptx, .csv, .md, .txt, .json, .yaml, .xml, .xsd, .xsl, .xslt, .html, .svg, images, and recordings as .mp4, .mov or .webm). Files up to 25 MB, a legacy .doc up to 4 MB, and a recording up to 2 GB and 2 hours. The old 4 MB limit is gone for every other format, because a large file now uploads straight to storage instead of through the API. Anything else that decodes as text is still added as a note, exactly as before. An older .xls or .ppt is refused with a message naming the format to save it as. Pass --folder to file it somewhere in the tree; without one it lands at the scope root. A URL argument is fetched server-side and - reads stdin as a note, both unchanged.

To republish a document, add it again with --replace. The document with the same title in that folder, at that scope, gets the new file and its text, and keeps its id, so nothing pointing at it breaks. The title matches with or without its extension, so design.md finds a document the app titled design. With no title match, a document in that folder with exactly the same bytes is the one replaced; with neither, the file is added as usual. When two documents share the title, --replace refuses and lists both ids; pass the one you mean with --id <source id>, which replaces exactly that document whatever the new file is called, in its own folder. --replace applies to files: a URL, stdin, or a file Plansmith adds as a note cannot be replaced this way, and --id naming a note or a web page is refused. On the API it is the upload's onConflict=replace field, or replaceSourceId for a named document.

Adding a recording prints Transcript: pending. The file is stored immediately; the transcript is written in the background and appears on the source once it is ready, usually within a minute or two. To tell "still working" from "finished", read enrichment.status on the source, at the scope you added it to:

GET /v1/context/{sourceId}                                            # company
GET /v1/projects/{project}/context/{sourceId}                         # project
GET /v1/projects/{project}/plans/{plan}/context/{sourceId}            # plan

status is one of pending, running, done, failed or skipped, and reason says why on the last two ("no usage left", "no speech was found in this recording"). ⚠ Each path only answers for a source at ITS scope, so poll the one matching where you added the file.

context download writes a zip of everything that lives at the scope you name: a plan with --plan, a project with --project, or your whole workspace with neither. That covers both the documents you added and the files the agent wrote into the plan's workspace, such as imported pages and its own research. Your original files come back in the folders you filed them under, alongside a manifest.json describing every entry, including each document's real title; an agent-written file is marked "sourceKind": "AGENT_FILE" so you can tell the two apart. It will not overwrite an existing file unless you pass --force.

The archive is currently capped at 4 MB, which is what a single response can carry. A larger scope is refused with a message naming the size, so download it in parts: --folder <path> takes one folder and everything under it, and --only <id,id> takes the entries you name (both together take the named entries in that folder). context list shows each entry's size (sizeBytes with --json, the bytes it adds to a download), its SCOPE and its id, so you can pick parts under the cap before asking. A plan's list also shows what it inherits from its project and the workspace, while a download carries only what lives at the scope you name: pick ids whose SCOPE matches. Files the agent wrote are in the download but not in the list; the zip's manifest.json names them. An id that is not at the scope you named is refused and listed, never quietly left out of the zip. On the API these are the folder and only query parameters of every …/context/download route, and the X-Context-Bytes header reports the content bytes a download carried.

context copy copies the context a plan owns onto another plan in the same project, keeping folders, descriptions and file originals. That means both the documents you added and the files the agent wrote into the plan's workspace, such as imported pages, its own research and the plan's AGENTS.md, plus the images and attachments those imported pages reference, whose links are re-pointed at the new plan so they open there. Inherited documents are not copied, because the destination already sees those. Add --dry-run to see what would move before anything is written, and re-run it safely: anything already on the destination is counted as skipped rather than duplicated, and a file the destination has since changed is reported in failed rather than overwritten. The one exception is a file this copy itself wrote and has since learned to write differently: a destination file still byte-identical to the source is re-pointed in place.

The response counts the two stores apart, so you can always tell which is which: copied and skipped are documents, files and skippedFiles are workspace files, and folders and fileFolders are their respective folders. One copy carries at most 750 documents and 2,000 workspace files, counting the plan's context files and its attachments against the same budget; past the document limit the request is refused with a 413 whose message names it, and a part of the workspace that does not fit is reported in failed while the rest still copies.

Instructions

AGENTS.md exists at four scopes, and the same command reaches all of them:

plansmith instructions show                          # the workspace (owner only to change)
plansmith instructions show --project recipe-box     # one project
plansmith instructions show --plan PLAN-12           # one plan
plansmith instructions show --me                     # your own, private to you

plansmith instructions set --file AGENTS.md --project recipe-box
plansmith instructions clear --me

The TIER comes from the flags and never from the project you pinned with plansmith use: writing workspace instructions when you meant a project would change every member's planning turns. A flag you typed but left empty is refused rather than widened.

Every read and write prints the scope the server answered with, not the one your flags asked for:

$ plansmith instructions show --project recipe-box
  scope: project · recipe-box · 952 of 1,500 characters

If those two disagree, the line says so, and a set that landed on a different tier exits non-zero rather than reporting success, because a document you did not aim at has already changed. A CLI older than 0.3.0 dropped --project on this command entirely, so upgrade if you see that warning.

--plan PLAN-12 still resolves its PROJECT from the pin, as everywhere else in the CLI. Plan handles are unique per project, so a stale pin lands on a real but different PLAN-12. Qualify it as recipe-box/PLAN-12 in anything scripted.

--me names your own file, so it cannot be combined with --project or --plan. Those two work together, as everywhere else in the CLI: --project recipe-box --plan PLAN-4.

A save can succeed and still be longer than the agent reads. show counts characters against the injection cap and says so when a document is clipped; --json carries the byte cap too.

Version history and restore

Needs plansmith 0.3.0 or newer.

plansmith instructions history --project recipe-box
plansmith instructions restore --at 4790 --project recipe-box [--yes]

history lists every version newest first. The AT column is what restore takes: the ledger's own handle, not the friendly version number beside it, because version numbers are per-document and the handle is workspace-wide.

restore puts an earlier version back. It states both sizes before it acts, and it is itself recorded as a new version, so nothing ever leaves the record. Restoring version 2 over version 4 leaves you on a version 5 whose message reads restored version 2.

restore refuses without --yes when there is no terminal to ask. It also refuses if the document changed after history read it, rather than overwriting someone else's edit. Re-run history and try again.

The same history and one-click restore are in Settings → Instructions, on the History tab.

Unrecognised flags

A flag this CLI does not know is refused, with the nearest match and the flags that command accepts. It is never accepted and ignored:

$ plansmith instructions set --file AGENTS.md --projct recipe-box
✗ Unknown flag --projct for "instructions set". Did you mean --project?
  Accepted here: --file, --help, --json, --me, --no-color, --plan, --project
  Nothing was sent.

Flags belong to a specific verb, so a sibling cannot lend one: instructions show --file x.md is refused too, and says --file belongs to instructions set.

Upgrading from 0.2.x: a script passing a flag the old CLI silently ignored now exits non-zero. That flag was never doing anything, and the exit code is the first time you are being told.

Usage

plansmith usage shows what each person in the workspace has used this month, in dollars of their monthly total, and how much of it they can still move. MOVABLE is a share of that person's own allowance; for the shared pool, which has no allowance to be a share of, it is money.

plansmith usage
PERSON              USED                      MOVABLE   RESETS
ana@example.com     $40 of $200 (20%)         80%       2026-10-03
bruno@example.com   $195.40 of $200 (97.7%)   2.3%      2026-10-03
shared pool         -                         $48       2027-04-18

Movable excludes usage that a plan currently running has already reserved.

Moving usage between people

A workspace owner can move part of one person's remaining usage to another person, or to and from the shared pool. --from and --to take an email, me, or pool. Say how much in dollars with --amount, or as a share of what --from can move with --percentage; exactly one of the two.

plansmith usage move --from ana@example.com --to bruno@example.com \
  --amount 80 --reason "Bruno is finishing the migration plan"

It quotes first and stops: you see what each person has left before and after, when the moved usage expires, and whether part of it clears a shortfall. Nothing moves until you add --yes.

Moving $80.00 from ana@example.com to bruno@example.com
  ana@example.com     $160.00 left  ->  $80.00 left
  bruno@example.com   $4.60 left  ->  $84.60 left
  expires 2026-10-03
Add --yes to move it. Re-run with --operation-key 8f0b… to retry.

An amount larger than --from can move is refused with the most it can move, in dollars. Monthly usage cannot go to the shared pool, so only purchased usage moves there.

Moved usage keeps the lifetime it already had. A slice of someone's monthly allowance still expires at the end of that period, so it does not become permanent by moving.

--operation-key is what makes a retry safe. The CLI prints one on every quote; pass it back and a re-run lands on the original move instead of making a second one, including when the first attempt committed and the response never arrived.

plansmith usage move --from ana@example.com --to pool \
  --percentage 25 --reason "Parking the remainder" \
  --operation-key 8f0b1f4e-… --yes

Owners only. --amount takes dollars and cents (88.33); --percentage takes a number between 0 and 100 with at most one decimal.

Upgrading from 0.4.x: usage move --credits <n> is gone. Use --amount (dollars) or --percentage instead. The CLI refuses the old flag, so nothing moves by accident.

Scripting

Every command takes --json and prints the API response unchanged, so your script depends on the API rather than on how the CLI formats things. Human-facing output goes to stderr, so redirecting stdout gives you clean data.

plansmith plan tree PLAN-12 --json | jq '.specs[] | .shortId + " " + .title'

CI

PLANSMITH_KEY=psk_read_… plansmith plan list -p recipe-box --json

A static key skips the saved session entirely and never refreshes, so CI behaves identically on every run. Mint one with plansmith keys create ci --scope read.

Sharing

Sharing lives on the top-level plan and covers every sub-plan under it. Address a sub-plan and the change still lands on the plan above it, so visibility, add and rm read the plan first and print which plan that is before they change anything.

plansmith share status PLAN-12                    # visibility, people, live public links
plansmith share visibility PLAN-12 TEAM           # or PRIVATE
plansmith share visibility PLAN-12 PRIVATE --yes  # skip the confirmation in a script
plansmith share add PLAN-12 sam@acme.co --role EDITOR
plansmith share rm PLAN-12 <userId>
plansmith share links list|mint|revoke PLAN-12    # public prototype links

share links list|mint|revoke has no pre-flight and prints no scope sentence. ⚠ links revoke closes the public prototype links on the plan you name, not across the whole tree, while the count in share status counts the tree, so a sub-plan's link can survive a revoke run on its root.

--yes is required on a sub-plan in a script. When the plan you named is a sub-plan, those three commands ask before changing the whole tree. With no terminal to ask (a cron job, CI), --yes is how you say yes in advance; without it the command refuses and exits non-zero rather than guessing. A top-level plan is unaffected, so existing scripts keep working.

--json selects a format and does not imply --yes. Adding it to get a parseable log must not quietly remove the confirmation.

Every share command takes --json (status, visibility, add, rm and links revoke) and prints the API response unchanged. Under --json nothing else reaches stdout, so a refusal goes to stderr and leaves your parser reading an empty stream rather than half an object.

Going PRIVATE does not kill a public prototype link that is already out there; the response says so, and share links revoke is what closes them, one plan at a time, per the note above.

Prototypes

Read a plan's prototype from the terminal, with the CLI's own sign-in: no browser session needed.

plansmith prototype status PLAN-12 --project recipe-box     # status, size, and if it is stale
plansmith prototype pull PLAN-12 --project recipe-box       # saves PLAN-12-prototype.html here
plansmith prototype pull PLAN-12 --out mock.html --force    # another name; --force overwrites
plansmith prototype pull PLAN-12 --stdout > mock.html       # the document on stdout
plansmith prototype open PLAN-12 --project recipe-box       # opens it in your browser
plansmith prototype open PLAN-12 --no-browser               # saves it and prints the path

pull gives the stored document exactly and refuses to overwrite a file without --force. open saves a page that runs the prototype in the same sandbox the web app uses, in a folder only you can read (on Linux under ~/Downloads, because snap browsers cannot read /tmp), and always prints its path so an agent can open it with its own browser tool. A plan you cannot read answers the same as a plan with no prototype.

Prototype template

The chrome every prototype is assembled into, per workspace or per project. set validates the document before anything is stored and prints the findings on a refusal; reset prints the tier now in force, which after a project reset is the company's when one exists.

plansmith prototype template get [--project recipe-box]
plansmith prototype template set --file template.html [--project recipe-box]
plansmith prototype template reset [--project recipe-box]
plansmith prototype rebuild PLAN-12 --project recipe-box     # re-render in the current template

The workspace template is owner-only, and a valueless --project is refused rather than widened to the workspace.

Environment

PLANSMITH_KEY          static API key (overrides the saved session)
PLANSMITH_API_URL      default https://api.plansmith.co
PLANSMITH_AUTH_URL     default https://www.plansmith.co
PLANSMITH_MCP_URL      default https://mcp.plansmith.co
PLANSMITH_CONFIG_DIR   default ~/.config/plansmith

Pointing the CLI at a local server needs the /api segment: PLANSMITH_API_URL=http://localhost:3000/api. The api.plansmith.co host rewrites /v1 onto it for you, and nothing else does.