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.
Install
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
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.
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
| Key | Minted by | Lives | For |
|---|---|---|---|
mtn_cli_… | martini login | 90 days | One per device and organization; labelled with the device in Settings → Martini API. |
mtn_live_… | Settings → Martini API | Until revoked | The 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:
- 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. Inside a Martini workflow run one is already in place, so the CLI is signed in there with nothing to configure. ~/.config/martini/config.json— the profilesmartini loginsaves.
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.
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.
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:
| Command | Does |
|---|---|
| login, logout, whoami | Pair 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, models | The 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 | --render | Block until assets, a job, a run, or a render settle; exit 4 with the pending ids on timeout. |
| files ls | cat | write | pull | push | Project 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. |
| version | The 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:
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.
| Exit | Meaning |
|---|---|
0 | ok |
1 | The tool failed. The error object carries the tool’s own code. |
2 | Usage: unknown command, missing or bad flags. Nothing was sent. |
3 | No key, or the key is invalid, revoked, or expired (API_KEY_EXPIRED after 90 days for a login key). |
4 | Not 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. |
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 — 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.
Material and memory files
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.
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.
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.
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.
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 withget_board_overviewand--fieldsfor 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.
--waitreturns once, with the settled result.