Martini CLI

Every tool the Martini MCP server exposes, as a command. For agents with a shell and for scripts: one HTTPS request per call, a static key, no session.

Set this up with a coding agent

Paste this into Claude Code, Codex, Cursor, or another agent working in a shell. It covers install, sign in, context, every command family, the output and exit contract, waiting, files, and an acceptance checklist.

Overview

The martini command reaches the same tools as the MCP connector, with the same names and the same arguments. What differs is the transport: a tool call is one HTTPS request with a key, so a shell script, a coding agent, or a workflow run can use Martini without holding an MCP session open, and can pipe results, submit JSON it computed, and block until work settles.

MCP for interactive work — one read, one edit, a conversation about a shot. The CLI for bulk and scripts — many uploads or downloads, waiting on generations, memory files, and anything a script computes and submits as JSON.
npx @martini-film/cli --help martini login martini get_board_overview --project-query "Pilot" --fields project,scenes martini upload ./ref.png ./take.mp4 --canvas <canvas-id> martini create_node_and_generate --canvas <canvas-id> --prompt "…" --model-id <model> --wait martini files pull ./memory && martini files push ./memory

Install

npx @martini-film/cli --help # run it without installing npm install --global @martini-film/cli # or keep it on PATH as martini

Requires Node.js 20.10 or later; the package has no runtime dependencies. For environments without Node, a single-file Linux binary is attached to each release.martini version prints the version, the hash of the tool manifest the build was generated from, and the config file paths.

Sign in

martini login

$ martini login Open https://www.martini.film/cli/auth and enter the code 7KQ4-M2XR Waiting for approval…

Open the URL, enter the code, pick the organization, approve. The CLI receives a key that lives 90 days and saves it as a profile named after the organization in ~/.config/martini/config.json; the first profile becomes the default. Run martini login again for another organization and switch with --profile <name> or MARTINI_PROFILE. Any member of the organization can sign the CLI in to it — the same people who can connect the MCP.

A started pairing is remembered for ten minutes, so martini login run again resumes it instead of starting over. An agent whose shell step cannot block on a person runs martini login --no-wait, relays the code, and runs martini login again once it is approved.

{ "defaultProfile": "acme-films", "profiles": { "acme-films": { "apiKey": "mtn_cli_…", "apiUrl": "https://api.martini.film", "organizationId": "…", "expiresAt": "2026-12-09T13:00:00.000Z" } } }

The key appears under Settings → Martini API with its device label and expiry, and is revoked there. martini logout forgets the profile; martini whoami says which organization and key the CLI acts as, and where the config came from.

Two kinds of key

KeyMinted byLivesFor
mtn_cli_…martini login90 daysOne per device and organization; labelled with the device in Settings → Martini API.
mtn_live_…Settings → Martini APIUntil revokedThe same key the Generation and Workflows APIs take. For scripts and servers.

A raw Martini API key always works: MARTINI_API_KEY=mtn_live_… or --api-key. The CLI never needs the generation-API grant that the Generation and Workflows APIs require — the tool surface is open to every account, as the MCP is.

Where credentials come from

Most specific first:

  1. Flags: --api-key, --api-url, --profile.
  2. Environment: MARTINI_API_KEY, MARTINI_API_URL, MARTINI_PROFILE, MARTINI_CONFIG_FILE.
  3. The nearest .martini/config.json above the working directory. Inside a Martini workflow run one is already in place, so the CLI is signed in there with nothing to configure.
  4. ~/.config/martini/config.json — the profiles martini login saves.

Project and canvas context

Most tools take a projectId, many a canvasId, the workflow tools a runId. When a tool takes one and you did not pass it, the CLI fills it from --project / --canvas / --run, else MARTINI_PROJECT_ID / MARTINI_CANVAS_ID / MARTINI_RUN_ID, else the config file. Set the project once and stop repeating it.

export MARTINI_PROJECT_ID=<project-uuid> export MARTINI_CANVAS_ID=<canvas-uuid> martini get_board_assets --fields assets.id,assets.status martini get_asset --asset-id <id>

Commands

Every tool is a command

martini <tool> exists for every entry of the tool manifest (GET /v1/tools, the same tools the MCP connector exposes). Flags are derived from the tool's arguments: assetId is --asset-id, arrays repeat the flag or take a JSON list, objects take JSON, booleans stand alone. --json '{…}' or --json @args.json passes the whole arguments object instead — the way to submit a plan a script computed without retyping it.

martini --help # every command on one screen, tools grouped by noun martini create_nodes --help # the tool's own description, every flag with type and required, one example martini create_nodes --json @plan.json --fields nodes.nodeId martini set_asset_metadata --json '{"assetIds": ["<id>"], "displayNames": {"<id>": "Hero close-up"}}'

Tool descriptions, limits, and required arguments are the manifest's, so --help says what the MCP says. Unknown flags and missing required flags are refused before anything is sent (exit 2). Tools behind workflows (Actions) access answer TOOL_NOT_FOUND for accounts without it, as they are absent from the MCP.

Compound commands

What no single tool does:

CommandDoes
login, logout, whoamiPair this device through the browser and save a 90-day key; forget a saved profile; show which organization and key the CLI acts as, and where the config came from.
tools, modelsThe tools this build knows, and whether the server has a newer manifest; the model catalog, cached locally for an hour (--refresh, --query, --capability).
upload <path…>Upload local files as canvas assets — prepare, PUT, complete in one step, then wait for processing. --canvas, --bin, --content-type.
download <asset-id…>Save completed assets to files: --out for one, --dir for many.
wait <asset-id…> | --job | --run | --renderBlock until assets, a job, a run, or a render settle; exit 4 with the pending ids on timeout.
files ls | cat | write | pull | pushProject memory files, one at a time or as a directory. push refuses a file edited server-side since the pull (PROJECT_FILE_CHANGED); --force overwrites.
probe <path|asset-id>…Duration, resolution, fps, codecs, channels, and sha256 of local files or assets, in one JSON report (needs ffprobe).
init [--write [path]]Print, or append to AGENTS.md, the section that teaches a repository’s agents to use the CLI.
versionThe CLI version, the tool-manifest hash it was built from, and the config file paths.

Global flags on every command: --fields, --out, --args (alias --json), --no-context, --wait / --no-wait, --timeout, --project / --canvas / --run, --api-key / --api-url / --profile, --quiet. A tool never sees them as arguments.

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 when three fields are wanted; arrays are traversed element-wise (nodes.id), and martini <tool> --help lists the result fields of tools that declare them, as paths. --out <path> writes the result to a file and prints one summary line; --raw --out <path> writes the media bytes of tools such as view_asset or render_timeline_frame. --quiet prints nothing on success.

Errors are one JSON object on stderr, never mixed into stdout:

{ "error": { "code": "ASSET_NOT_READY", "message": "Asset 5b0c… is still processing.", "hint": "Run martini wait 5b0c… to block until it settles.", "tool": "get_asset_download_url", "exitCode": 4 } }

code is the tool's own MCP error code (ASSET_NOT_READY, NODE_NOT_FOUND, INSUFFICIENT_CREDITS, …), or USAGE for a flag problem. Branch on code and the exit code; the message may change.

ExitMeaning
0ok
1The tool failed. The error object carries the tool’s own code.
2Usage: unknown command, missing or bad flags. Nothing was sent.
3No key, or the key is invalid, revoked, or expired (API_KEY_EXPIRED after 90 days for a login key).
4Not ready, or a wait timed out. The result the command produced is still printed, with the pending ids, and the error names the martini wait command that resumes it.
5Insufficient olives.

Waiting

Generation and rendering are asynchronous. --wait blocks until the produced assets (or the render) settle and adds waited to the result — one shape whatever was waited on: { kind, status, assets? | job? | run? | delivery? }, where status is the terminal status and the settled records sit under the key that names them; martini wait prints the same object. It is on by default for single-item generation and rendering tools — create_node_and_generate, generate, render_timeline, enhance_draft, upscale_assets, render_blender_take — and off for batch tools, where you fan out and then wait once. --no-wait turns it off. --timeout <seconds> (default MARTINI_WAIT_TIMEOUT_SECONDS when set, else 50) ends a wait with exit 4: the result the command produced is still printed on stdout, with pending (and settled) ids, and the error names the martini wait that resumes it. Size --timeout to what your shell step allows — a video generation takes minutes.

martini create_node_and_generate --prompt "…" --model-id <model> --fields waited --timeout 290 martini create_nodes_and_generate --json @shots.json --fields nodes.nodeId > nodes.json martini wait $(jq -r '.nodes[].nodeId' nodes.json) --timeout 300 martini wait --run <run-id> --timeout 600 # a workflow run; get_run long-polls on the server martini wait --render <render-id> # a timeline render

Material and memory files

martini upload ./ref.png ./take.mp4 --canvas <canvas-id> # prepare → PUT → complete, then wait for processing martini download <asset-id> --out ./take.mp4 # one file martini download <id> <id> --dir ./out # many martini probe ./take.mp4 <asset-id> # duration, resolution, fps, codecs, channels, sha256

upload is the three-call upload (upload_assets_prepare, the PUT to storage, upload_assets_complete) in one step, from files on disk, so nothing rides through the agent's context as base64. download follows the presigned URL and writes the bytes. probe needs ffprobe on PATH.

martini files ls martini files cat notes/continuity.md martini files write notes/continuity.md @continuity.md martini files pull ./memory # every memory file to a directory, with the versions you saw $EDITOR ./memory/notes/continuity.md martini files push ./memory # writes what changed; refuses a file edited server-side since the pull

Project memory files are the notes and instructions a project keeps for its agents (read_project_file, write_project_file). files pull and files push edit them as a directory; push compares each file's server version with the one pulled and answers PROJECT_FILE_CHANGED for a file changed since — pull again, merge, push. --force overwrites.

Agents

martini init prints the section that teaches a repository's agents to use the CLI — when to prefer it over MCP tool calls, the commands, the output contract, the exit codes. martini init --write appends it to AGENTS.md (or the path you give) once. The same rule reaches agents already connected over MCP: the connector's server instructions state it, and get_board_overview repeats it to callers it recognizes as a terminal or IDE agent.

martini init # print the AGENTS.md section martini init --write # append it to ./AGENTS.md (idempotent)

Every call names the CLI and its version in X-Martini-Client, and the User-Agent carries the host agent it runs inside when the host's environment says (Claude Code, Cursor, Codex, Gemini CLI, opencode, Windsurf, a workflow run), so a call from the CLI under Claude Code is distinguishable from one Claude Code made over MCP. MARTINI_HOST_AGENT names the host explicitly; nothing is guessed. In Martini the call shows up as the CLI in the collaborator strip and in analytics.

Generations cost olives and cannot be undone. An agent should lay out the plan — model, prompts, count, rough cost — and get the user's approval before a batch, exactly as over MCP. The tools that quote first (generate_nodes, upscale_assets, enhance_draft) start nothing until called again with the quote's planHash and an approval; --approve --max-olives <n> does both in one command and refuses, with nothing started, when the estimated total is over the cap.

martini generate_nodes --node-ids <id> <id> --approve --max-olives 20 --wait

The HTTP surface underneath

The CLI is a client of two routes any language can call with a Martini API key: GET /v1/tools, the manifest (every tool with its JSON Schema, annotations, and profile; the response ETag is its hash), and POST /v1/tools/{name}, which runs one tool with the JSON body as its arguments and answers the result with the MCP envelope removed. ?select=a,b.c is the server-side projection behind --fields; ?format=raw answers a single media result as its bytes.

curl https://api.martini.film/v1/tools \ -H "Authorization: Bearer $MARTINI_API_KEY" curl -X POST "https://api.martini.film/v1/tools/get_asset?select=asset.id,asset.status,asset.imageUrl" \ -H "Authorization: Bearer $MARTINI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "projectId": "…", "assetId": "…" }'

Errors are { "error": "<copy>", "code": "<STABLE_CODE>", "tool": "<name>" } with the tool's own code where it has one; a name that is not in the manifest answers TOOL_NOT_FOUND. @martini-film/client exposes the pair as martini.tools.list() and martini.tools.call(name, args, { select }); martini-client (Python) as martini.tools.list() and martini.tools.call(name, arguments, select=…). The full /v1 surface is described at api.martini.film/v1/openapi.json.

Not yet available

  • A local mirror of a canvas as a file tree (martini mirror pull). Read with get_board_overview and --fields for now.
  • Compiled binaries for macOS and Windows. The npm package runs wherever Node.js 20.10 or later does; the single-file binary is Linux x64.
  • Streaming progress while waiting. --wait returns once, with the settled result.

© 2026 C47 Inc.