@martini-film/cli
v0.2.0
Published
The Martini command-line interface for agents and scripts: every MCP tool as a command, plus upload, download, wait, files, probe, and login
Readme
@martini-film/cli
The Martini command-line interface for agents and scripts. Every tool the Martini MCP server exposes is a command with the same name; on top, a handful of compound commands do what no single tool does — upload local files, download and measure assets, block until work settles, edit project memory files as a directory, and sign in. One HTTPS request per call, a static key, no MCP session.
npx @martini-film/cli --help # or: pnpm add -g @martini-film/cli → martini
martini login # pair this device through the browser (90-day key); --no-wait to return at once, rerun to collect
martini get_board_overview --project <id> # any MCP tool, flags from its arguments
martini get_asset --asset-id <id> --fields asset.status,asset.imageUrl
martini upload ./ref.png ./take.mp4 --canvas <id>
martini create_node_and_generate --canvas-id <id> --prompt "…" --reference-images <url> --wait
martini wait <asset-id…> # resume a wait; exit 4 with the pending ids on timeout
martini files pull ./memory && martini files push ./memory
martini probe <path|asset-id>… # duration, resolution, fps, codecs, sha256 (needs ffprobe)How commands are generated
martini <tool> exists for every entry of the tool manifest the build carries — the same list the Martini
MCP server publishes; martini tools says when the server has a newer one. Flags are derived from each tool's JSON Schema: assetId becomes --asset-id, arrays repeat the flag
(or take a JSON list), objects take JSON, a string | null argument takes the literal null to clear it, a
"start" | "end" | {…} union takes the bare word, and --args '{…}' / --args @args.json passes the whole
arguments object (--json is an alias, except on a tool whose own argument is named json) — so a plan a script
computed is submitted without retyping it. --no-context sends only what you typed, for tools that refuse a
projectId next to another selector. martini <tool> --help prints the tool's own description, every flag with
its type and whether it is required, the result fields when the tool declares them (as --fields paths), and one
example.
projectId, canvasId, and runId are filled from context when a tool takes them and the caller did not say:
--project / --canvas / --run, else MARTINI_PROJECT_ID / MARTINI_CANVAS_ID / MARTINI_RUN_ID, else the
config file. A typed selector that replaces one of them keeps it out (get_board_overview --project-query,
run_workflow --workflow-id, which runs a saved workflow on a canvas of its own).
Credentials and context
Most specific first:
- Flags:
--api-key,--api-url,--profile. - Environment:
MARTINI_API_KEY,MARTINI_API_URL,MARTINI_PROFILE,MARTINI_CONFIG_FILE. - The nearest
.martini/config.jsonabove the working directory — what a workflow run's sandbox has. ~/.config/martini/config.json— the profilesmartini loginsaves, one per organization (named after it,test-team), with a default.
--project-id, --canvas-id, and --run-id (the tools' spelling) are accepted wherever --project, --canvas,
and --run are.
A workflow run's sandbox therefore needs nothing but its working directory; a laptop needs one martini login;
a script sets MARTINI_API_KEY. A raw Martini API key (mtn_live_…) always works.
Output and exit codes
Results are JSON on stdout — compact when piped, indented on a terminal. --fields a,b.c keeps only those
dotted paths, applied on the server so a large listing never crosses the wire; a * segment steps into every value of
an id-keyed map (assets.*.status) and is applied in the CLI. With --wait (including default
waits), the CLI keeps the submission identifiers for polling and applies --fields to the final result.
--out <path> writes the result
(or, with --raw, the media bytes of tools like view_asset) to a file and prints one summary line. --quiet
prints nothing on success. Errors are one JSON object on stderr: { "error": { "code", "message", "hint", "tool",
"exitCode" } }, with the tool's own MCP error code (ASSET_NOT_READY, NODE_NOT_FOUND, …).
| Exit | Meaning | | ---- | -------------------------------------------- | | 0 | ok | | 1 | the tool failed | | 2 | usage: unknown command, missing or bad flags | | 3 | no key, or the key is invalid / expired | | 4 | not ready or timed out (pending ids named) | | 5 | insufficient olives |
Waiting
Generation and rendering are asynchronous. --wait blocks until the produced assets (or the render) settle and
adds waited to the result; it is on by default for single-item generation and rendering tools and off for batch
tools, where you fan out and then martini wait <ids…> once. waited has one shape whatever was waited on:
{ kind, status, assets? | job? | run? | delivery? } — status is the terminal status (for assets, failed when
any failed) and the settled records sit under the key that names them; martini wait prints the same object.
--timeout <seconds> (default MARTINI_WAIT_TIMEOUT_SECONDS when set, else 50; a malformed value is a usage error) ends the wait with exit 4: the
result the command produced is still printed on stdout, with pending (and settled) ids, and the error on
stderr names the martini wait … command that resumes it. Size --timeout to what your shell step allows — a
video generation takes minutes.
Approving a quote
generate_nodes, upscale_assets, and enhance_draft answer a quote first (approvalNeeded: true, planHash,
the estimated olives) and start nothing until called again with that planHash (plus batchRequestId for
upscales) and the approval argument. --approve --max-olives <n> does both in one command: it takes the quote,
refuses when the estimated total is over the cap (PLAN_OVER_BUDGET) or has no number (PLAN_UNPRICED), with
the quote on stdout and nothing started, and otherwise re-calls the tool with exactly what it asked for; the
result carries approvedPlan. The cap is required: an approval always states how much it is for. A tool with an
approval argument but no quote step (create_nodes_and_generate) is only attested by --approve and cannot be
capped — for a capped batch, create_nodes then generate_nodes --approve --max-olives <n>.
Compound commands
| Command | Does |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| login, logout, whoami | device-code pairing (a started pairing is resumed by running login again; --no-wait returns at once), forget a profile, who the CLI acts as |
| tools, models | the manifest this build knows (and whether the server has a newer one); the cached model catalog |
| upload <paths…> | prepare → PUT → complete from local files, then wait for processing |
| download <ids…> | presigned URL → bytes → file |
| wait <ids…> \| --job \| --run \| --render | block until assets, a job, a run, or a render settles |
| files ls\|cat\|write\|pull\|push | project memory files as a directory; push refuses a file edited server-side since the pull |
| probe <path\|id>… | ffprobe + sha256 in one JSON report |
| init [--write] | the AGENTS.md snippet that teaches a repo's agents to use the CLI |
Inside a workflow run
The sandbox of a Martini workflow run carries a run key in .martini/config.json, written when the run's agent
environment opens and revoked when it closes. Calls made with it go through the run's own supervisor — the same
budgets, spend fences, and trace as the run's tool calls — and the tool set is the run's. A run that is not
executing answers RUN_NOT_ACTIVE (exit 4).
Single-file binary
pnpm run build:binary compiles dist/bin.js into dist/martini-linux-x64 with Bun for environments without
Node (the agent-environment sandbox). Bun is only needed on the build machine. Every release publishes it at https://downloads.martini.film/cli-releases/<version>/martini-linux-x64;
the npm package is published separately.
