muni CLI — Internal Wiki

Reference doc for the Python CLI + SDK that wraps the Muni platform. Audience: engineers working on muni-cli, muni-app, or tools-api. Last refreshed against master on 2026-06-15.

How to read this doc Every command card is tagged with its routing (where the bytes actually go) and a state chip (stable, mixed, or rough). The Watchlist section calls out anything currently half-baked or in active migration. This page is the canonical CLI command reference; the Muni Tools API wiki owns tool contracts, output schemas, provider behavior, and API-side runtime notes.
SDK/api/sdk on muni-app Tools API → Elysia (port 8000) Supabase → PostgREST / Auth / Realtime Local → no network stable mixed rough

Overview #

muni is the command-line interface and Python client for the Muni platform. It manages tools, GPU jobs, spaces, canvas pages, nodes, connections, and the user account. Two install modes:

pipx install .       # recommended — isolated global, exposes `muni` on PATH
pip install -e .     # editable dev install

Requires Python 3.11+. Only one runtime dependency (realtime, which pulls Pydantic v2). All HTTP uses stdlib urllib. The package surfaces both a CLI binary (muni) and an importable client (from muni import MuniClient).

Key facts at a glance

ItemValue
Current versionLatest vX.Y.Z Git tag (published automatically from master)
Entry pointmuni.cli:main
CLI module sizemuni/cli.py — 5,140 lines (single monolithic argparse build)
Config location~/.muni/config.json + ~/.muni/credentials.json
Local workspace binding.muni/workspace.json found by walking up from cwd
Profile switching--profile NAME or MUNI_PROFILE
JSON output--json on any command
Generated modelsmuni/_generated/models.pydo not edit, regenerated from muni-app's OpenAPI

Architecture #

The Muni stack has three repos. The TypeScript SDK in muni-app is the canonical contract; the Python SDK in this repo is derived from it; the CLI is a thin wrapper over the Python SDK.

TypeScript SDK (canonical) → Python SDK (derived) → CLI (wraps SDK) muni-app/src/lib/ muni/clients/*.py muni/cli.py muni/sdk/client/ ┌───────────────────────────────────────────────────────────────────────────┐ │ muni CLI (this repo) │ │ │ │ ┌─────────────┐ ┌──────────────────────────────────────────────────┐ │ │ │ cli.py │──▶│ client.py (MuniClient domain wiring) │ │ │ │ (argparse) │ │ │ │ │ └─────────────┘ │ ┌──────────────────────────────────────────┐ │ │ │ │ │ Sub-clients (clients/*.py) │ │ │ │ │ │ user · tools · jobs · job_outputs │ │ │ │ │ │ spaces · pages · canvas · nodes · │ │ │ │ │ │ node_files · node_data · connections · │ │ │ │ │ │ text · image · pdf · code · scripts │ │ │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ ▼ POST /api/sdk │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ api.py (legacy Tools API helpers) supabase.py (PostgREST + Auth) │ │ auth.py (login flows) realtime.py (job broadcast waiter) │ └───────────────────────────────────────────────────────────────────────────┘ │ │ │ ▼ ▼ ▼ muni-app (3000) Tools API (8000) Supabase /api/sdk /tools /jobs Auth/PostgREST/Realtime

Why three transports?

The CLI didn't get to ship with one perfect backend — it accreted three over time, each with a defensible reason for staying:

Routing reference #

Transport Where the code lives What it covers Response shape
SDK muni/clients/*.py via SDKClientBase._call Spaces, pages, canvas, nodes, connections, jobs (CRUD + materialization), tools, sandbox, scripts, balance Typed Pydantic models (_generated/models.py) — .model_dump(by_alias=False) for snake_case JSON
Tools API muni/api.py Backend job execution service plus legacy helper module in muni/api.py Not the normal command path; app server mediates CLI calls through /api/sdk
Supabase muni/auth.py, muni/supabase.py, muni/realtime.py Login (browser / device / password), token refresh, Realtime broadcast channel jobs:user:<uid> Auth tokens; broadcast events fire waiter.wait_for_update()
Local muni/config.py Profile mgmt (config show/profiles/use), active space/page caching, logout, version check Files in ~/.muni/
One Python SDK surface MuniClient exposes namespaced domain clients only. The CLI calls those same methods; response formatting happens at the CLI boundary.

Profiles & config #

Files

PathPurpose
~/.muni/config.jsonProfiles, URLs, active space/page
~/.muni/credentials.jsonPer-profile access/refresh tokens
.muni/workspace.jsonProject-local profile, space, and page binding for agent runs

Environment overrides

VariableEffect
MUNI_PROFILESwitch profile for the current process
MUNI_API_URLOverride Tools API URL; retained for compatibility and local app URL inference
MUNI_APP_URLOverride muni-app URL
MUNI_SUPABASE_URLOverride Supabase URL
MUNI_SUPABASE_PUBLISHABLE_KEYOverride Supabase publishable key
MUNI_SPACE_ID / MUNI_PAGE_IDPer-process or per-command space/page override (without writing to config)

Local workspace binding

For coding-agent workflows, prefer a local binding over global active space/page state. muni link SPACE_ID writes .muni/workspace.json in the current directory. Any command run under that directory resolves the linked profile, space, and page before the profile's global active space/page. Environment variables still override this for an intentionally scoped shell.

mkdir ~/muni-work/protein-analysis
cd ~/muni-work/protein-analysis
muni link space_... --page "Main"
muni workspace show
muni nodes

This makes a Muni space feel like a code project: one terminal/agent can be rooted in Space A while another terminal/agent is rooted in Space B. Avoid muni space use in shared-profile agent sessions unless you explicitly want to mutate global profile state.

muni link / workspace Localstable

muni link SPACE_ID [--page PAGE_ID_OR_TITLE]
muni workspace [show | status | link SPACE_ID | unlink]
  • muni link SPACE_ID — create .muni/workspace.json in cwd
  • muni workspace show — print the local profile/space/page binding
  • muni workspace unlink — remove the local binding

Implementation: load_workspace() / save_workspace() in muni/config.py, cmd_link / cmd_workspace in muni/cli.py.

Profile commands

muni config Localstable

muni config [show | profiles | use NAME]
  • muni config — interactive prompt to set API/app/Supabase URLs & keys
  • muni config show — print active profile (masks key)
  • muni config profiles — list configured profiles
  • muni config use NAME — switch the default profile

Implementation: cmd_config in muni/cli.py. Auto-migrates legacy api_url values via _replace_legacy_api_urls().

Auth & account #

muni login Supabasestable

muni login [--device | --password-flow] [--email EMAIL] [--password PW] [--no-browser] [--client-name NAME]

Three flows:

  • Browser PKCE (default) — opens muni.bio/cli/authorize, captures callback on localhost. Best for daily use.
  • Device code (--device) — prints a code; user enters it at muni.bio/device. Use over SSH or in tmux without a browser.
  • Password (--email/--password or --password-flow) — legacy Supabase email/password. Kept for CI scripts; not the default.

Implementation: muni/auth.py + muni/oauth.py. Refresh tokens stored alongside access tokens; ensure_token() auto-refreshes near expiry.

Concurrent refresh — fixed in v0.1.40: refresh tokens rotate on every refresh. Parallel muni processes sharing ~/.muni used to race near expiry — each refreshing the same token, minting multiple server-side tokens (orphaning all but one) and sometimes leaving a process holding a revoked token, which forced a fresh muni login every few days. ensure_token() now takes an exclusive fcntl lock on ~/.muni/refresh.lock and re-reads credentials under it, so only one process refreshes and the rest reuse its rotated token (_refresh_lock / _refresh_credentials in muni/auth.py; regression test tests/test_auth_refresh_lock.py). Server-side hardening of the rotation endpoint — atomic compare-and-swap, terminal revocation with no resurrection grace, and revoking prior device tokens on login — is staged in muni-app and removes the race for any client (old or new) once deployed.

muni logout Localstable

muni logout

Clears credentials for the current profile.

muni whoami SDKLocalstable

muni whoami [--json]

Locally inspects credentials, then best-effort hits client.user.balance() and resolves the active space. Balance failure is non-fatal.

muni balance SDKstable

muni balance [--json]

Credit balance via client.user.balance()POST /api/sdk user.balance.

muni update Localstable

muni update [--check]

Detects install method (pipx / uv tool / pip) by inspecting sys.executable and shells out to upgrade. Refuses to touch editable installs.

Spaces & pages #

muni spaces SDKstable

muni spaces [-q QUERY] [--limit N] [--offset N] [--all] [--roles|--access] [--role ROLE] [--json]

List spaces with server-side search and pagination. Marks the active one. Backed by client.spaces.search()POST /api/sdk spaces.search. Add --roles to include the current user's email and role; --role filters by that role. JSON output is an envelope with spaces, limit, offset, has_more, and next_offset.

muni space SDKstable

muni space <create | use | members | invite | delete> [args]
  • create NAME — also sets it active and pins its default page
  • use [ID|NAME|none] — set active; interactive picker if no arg; none/api-only clears the active space
  • members SPACE_ID — list member emails and roles
  • invite SPACE_ID EMAIL — invite a user
  • delete SPACE_ID — delete (destructive)

Note: space members currently returns raw dicts from the helper even though the generated Pydantic models include the member shape. Display formatter is tolerant of user_id/userId.

muni pages SDKstable

muni pages [--space-id SPACE_ID] [--json]

List pages in the active space (or override with --space-id).

muni page SDKstable

muni page use [PAGE_ID|TITLE] [--space-id SPACE_ID]

Set the active page. Resolves by ID or exact title within the active space. Interactive picker if PAGE_ID omitted.

Only the use sub-action is wired up at the CLI today. The sub-client (client.pages) also exposes create, delete, reorder, get_default; none have CLI commands yet.

Canvas #

muni canvas position SDKstable

muni canvas position [--page-id ID] [--space-id ID] [--json]

Show the centre and visible viewport rectangle the user is currently looking at. Useful for placing follow-up nodes near what's on screen.

muni canvas placement SDKstable

muni canvas placement [--type table|chart|…] [--width W] [--height H]

Preview where the next unpositioned node of a given type would land. Used by the UI smart-placement algorithm; exposed in CLI for diagnostics and pre-flight.

Nodes #

This is the largest command surface in the CLI. Nodes are the things on a Muni canvas page — tables, charts, scripts, structures, images, jobs, pipelines, groups. The CLI exposes the full lifecycle: create, edit, run, connect, group, query data, select rows.

muni nodes SDKstable

muni nodes [--type TYPE] [--limit N] [--page-id ID] [--space-id ID] [--json] muni nodes search QUERY [--type TYPE]

List or search nodes. Filterable by type. Default limit: 20.

muni node create SDKstable

muni node create TYPE TITLE [--script PATH | --script-text TEXT] [--config JSON_OR_PATH] [--python PATH] [--file PATH | --url URL | --content TEXT | --smiles SMILES] [--tool TOOL --param KEY=VALUE ...] [--submit] [--x X --y Y] [--from NODE_ID ...] [--source-port PORT] [--target-port PORT] [--group-id GROUP_ID] [--wait] [--timeout 60] [--expect-output KEY:TYPE ...] [--expect-min-rows N] [--expect-min-columns N]

Valid node types (from VALID_NODE_TYPES): table, code, json, structure, compound, chart, text, image, pdf, job, pipeline, group. This noun-first form is the only documented allocation grammar.

Hydration is selected by node capability. Text hydrates directly; Image/PDF publish validated private object references; Table, JSON, and Compound validate before allocation and load their facets directly; Structure publishes an immutable coordinate reference. These paths defer runtime dispatch and do not generate executable source. Code alone seeds its complete Python/TypeScript workspace inside the compensated allocation call. Job creation saves a validated declaration and submits provider work only with explicit --submit.

Assertions: --expect-output rows:dataframe, --expect-min-rows 10, --expect-min-columns 3 exit non-zero if the produced output doesn't match. Use in CI to validate node behaviour.

muni node check SDKstable

muni node check [--type TYPE] [--script PATH | --script-text TEXT]

Runs the real server-side preflight engine (syntax / typecheck / api-usage / ports / literal-schema / graph) via client.scripts.preflight. Diagnostics are byte-identical to what the server pipeline would record on muni_node_runs.preflight_* — no local-only linting.

muni node read SDKstable

muni node read NODE_ID [--json]

Full state: script, position, runtime, currentRun, compile/execute/validate results, render health, output data, connections, Python files (for code nodes).

muni node edit SDKstable

muni node edit NODE_ID [--title T] [--script PATH] [--python PATH] [--file PATH] [--run] [--timeout 60]

Update title, script, or Python source. --file works the same as create --file. --run submits the node after editing and waits for the result.

muni node content SDKSupabasemixed

muni node content NODE_ID [--output-key primary] [--json]

Reads node output from muni-node-data Storage. Hybrid: pulls the data row through the SDK, then hits Supabase Storage directly (/storage/v1/object/muni-node-data/…) using the user's access token. Auto-parses JSON; for kind: "dataframe" blobs, prints first 20 rows tab-separated.

Flagged mixed because of the direct Storage hit — fine in practice but the only spot in the CLI that bypasses the SDK for read.

muni node remove SDKstable

muni node remove NODE_ID

Soft-delete via nodes.remove with origin: "cli-remove" for the audit trail. Recoverable via client.nodes.restore() (no CLI command for restore yet).

muni node submit SDKstable

muni node submit NODE_ID [--wait] [--timeout 60]

Compile + execute + validate + store + cascade. For async (job-backed) nodes, --wait polls currentRun.overallStatus until terminal.

muni node move / select / focus SDKstable

muni node move NODE_ID X Y muni node select NODE_ID [NODE_ID ...] [--page-id ID] muni node focus NODE_ID [--page-id ID]

Canvas-side ops that broadcast over Realtime to attached clients.

muni node structure SDKstable

muni node structure NODE_ID (--code JS | --file PATH) [--gallery-index N] [--page-id ID]

Executes an async JavaScript function body against the node's live Mol* viewer through nodes.structureScript. The body receives (plugin, molstar, ctx) and returns a real success/error result. Use --file - for stdin. An open canvas must be connected; incoming commands automatically activate and focus the target structure node. The server resolves the owning page from NODE_ID, so no active page is required. The legacy --page-id flag is optional and is forwarded only as a consistency assertion.

muni node connect SDKstable

muni node connect SOURCE_ID TARGET_ID [--source-port PORT] [--target-port PORT] [--kind data|signal|provenance] [--no-run] [--wait]

Convenience alias for muni connection create. Defaults: source port primary, target port rows, kind data. Auto-reruns target unless --no-run.

muni node group / layout-group / ungroup SDKstable

muni node group NODE_ID [NODE_ID ...] --label LABEL muni node layout-group GROUP_ID [--algorithm elk|simple] [--direction RIGHT|DOWN] muni node ungroup GROUP_ID

Group loose nodes under a new parent. layout-group runs ELK (default) or a simple layout and resizes the frame.

muni node table-summary / table-query / table-select SDKstable

muni node table-summary NODE_ID muni node table-query NODE_ID [--columns C1,C2 | --columns C1 C2] [--sort-by C] [--sort-order asc|desc] [--filter C:OP:VALUE] [--limit 20] [--offset 0] muni node table-select NODE_ID get|select|deselect|clear [--rows 1 2 3] [--filter C:OP:VALUE]

Schema and read access for table nodes; selection state persists in runtime.viewState. Filter ops: gt, gte, lt, lte, eq, neq, contains, in. Row indices are 1-based at the CLI (the SDK is 0-based; the CLI converts).

muni node examples Localrough

muni node examples [KIND] [--scenario NAME]

Show copyable examples for a node kind. Currently only structure has examples (hard-coded in NODE_EXAMPLES in cli.py). Pre-baked PXDesign gallery template lives here too.

See Half-baked — this is the only example registry and it covers one kind.

Connections #

muni connections SDKstable

muni connections [--page-id ID] [--space-id ID] [--json]

List edges (source → target with port + kind) on the active page.

muni connection <create|edit|remove> SDKstable

muni connection create SOURCE_ID TARGET_ID [--source-port PORT] [--target-port PORT] [--kind data|signal|provenance] [--no-run] [--wait] [--timeout 60] muni connection edit CONN_ID [--source-node-id ID] [--target-node-id ID] [--source-port PORT] [--target-port PORT] [--kind ...] [--enabled|--disabled] [--no-run] [--wait] muni connection remove CONN_ID [--no-run] [--wait]

Auto-reruns the target node after any change unless --no-run. --wait blocks until the target run reaches terminal state.

The sub-client also exposes disable and enable; surfaced via --enabled/--disabled on edit.

Tools #

muni tools SDKstable

muni tools [--query KEYWORD] [--category CAT] [--limit N] [--offset N] [--all] [--json]

List/search the tool catalog. --all paginates through everything; otherwise default page size is shown.

Even though sub-client is the path, the wrapper converts back to legacy dict shape so the display formatter (which expects snake_case + reference_url) keeps working. See migration.

muni tool SDKstable

muni tool NAME [--inputs | --outputs | --schemas | --schema [VERSION] | --examples] [--json]
  • (no flag) — full tool detail (header, params, examples)
  • --inputs — input parameter schema only
  • --outputs — declared outputs and their provider/derived paths (falls back to legacy tool.outputs if no versioned schema is published)
  • --schemas — list of available output schema versions
  • --schema [VER] — full JSON Schema for one version (defaults to latest)
  • --examples — curated usage examples

Flags are mutually exclusive — passing more than one fails with a clear error.

Run (job submission) #

muni run SDKstable

muni run TOOL [KEY=VALUE ...] [--params-file FILE] [--batch FILE | -] [--space-id ID] [--page-id ID] [--no-space] [--auto-materialize | --no-auto-materialize] [--no-nodes] [--no-result-nodes] [--follow | --wait | -f] [--timeout 1800] [--title TITLE] [--dry-run] [--print-job-id] [--submit-concurrency 10] [--log-lines 120]

KEY=VALUE parsing

Values are JSON-parsed first; falls back to literal string. Special prefixes:

  • @file — file contents (auto-JSON if extension is .json)
  • @json:file — force JSON parse
  • @text:file — force raw text (no JSON parse)

Display intent (affects what shows up on the canvas)

Flag comboWhat gets created
default (active space + page)Canvas job node + auto-materialised result nodes
--auto-materializeExplicit automatic result nodes; requires a resolved canvas target and conflicts with --no-space/--no-nodes
--no-auto-materializeJob node only; result nodes remain manual
--no-result-nodesJob node only; user clicks "Show results" later
--no-nodesNo job node; visible in Jobs panel only (still belongs to the space)
--no-space or no active spaceHeadless / API-only — no canvas footprint

--auto-materialize cannot be combined with legacy --no-result-nodes. Omitting the Boolean flags preserves existing behavior.

Batch

JSONL only. --batch - reads from stdin. Concurrent submission (default 10). Locally-failed lines + API-submitted lines merge into one summary; CLI exits non-zero if any failed.

Follow / wait

--follow and its --wait alias use Supabase Realtime by default (channel jobs:user:<uid>), falling back to polling. Exits non-zero on failure.

Jobs (introspection & materialisation) #

muni jobs SDKstable

muni jobs [--space-id ID] [--status STATUS] [--tool TOOL_NAME] [--title TITLE] [--limit 20] [--json]

Search/list your past jobs. Backed by client.jobs.search() → typed JobsSearchOutput, returned most-recent first.

Filter / organise by

AxisHow
Status (succeeded / failed / running)--status completed|failed|running|… (alias -s)
Job type (tool)--tool TOOL_NAME (alias -t)
Title--title TEXT — forwards to jobs.search(title=…)
Space--space-id ID
Recency / count--limit N (alias -n, default 20); every row carries createdAt

The human table shows Job ID · Status · Type · Title · Created; --json adds providerJobId and providerUrl.

Input parameters → muni job params JOB_ID The parameters a job was submitted with are stored in public.jobs.parameters (jsonb) and are surfaced by muni job params JOB_ID, backed by the canonical jobs.detail SDK method (client.jobs.detail() → reads the single job row, incl. parameters, from Supabase via /api/sdk). Requires the muni-app jobs.detail change (branch feat/jobs-parameters) to be deployed; running pnpm codegen:python from a synced main then adds a typed JobsDetailOutput model (until then the CLI returns the raw dict, like jobs.summary/query).

Per-job drill-down: muni status JOB_ID (status / title / runtime / error), muni results, muni job summary|query|result|schema|files, muni logs.

muni status SDKstable

muni status JOB_ID [--json]

Single-job status. Field normalisation flattens camelCase (createdAtcreated_at) and aliases (tool_namejob_type) for back-compat with display formatters.

muni results SDKstable

muni results JOB_ID [--fields F1,F2,…] [--sequences] [--json]

Pretty result print or structured projection.

  • --fields — dot-notation paths against the preferred record container (rankings/top_designs/sequences/designs/metrics/ranked_poses/poses/affinities)
  • --sequences — extracts designed sequences from any of the recognized shapes

Includes a "next steps" hint (muni files JOB_ID / muni results … --fields …) heuristically picked from result shape.

muni wait SDKSupabasestable

muni wait JOB_ID [JOB_ID ...] [--timeout 7200] [--interval 5] [--follow] [--json] [--log-lines N]

Wait for one or many jobs. Realtime broadcast preferred, polling fallback. --follow streams status changes; --log-lines shows provider logs for failed/cancelled jobs. Periodic token refresh (every 40 min) for long-running jobs.

muni cancel SDKstable

muni cancel JOB_ID [--json]

Cancel a running job. Exit 1 if already terminal (HTTP 409 — handled cleanly).

muni job <subcommand> SDKstable

muni job summary JOB_ID muni job params JOB_ID muni job result JOB_ID [--schema] [--files] muni job schema JOB_ID muni job files JOB_ID [--path PREFIX] muni job read JOB_ID FILE [--max-lines N] [--all] muni job query JOB_ID [--columns C1,C2] [--sort-by C] [--sort-dir asc|desc] [--limit 20] [--offset 0] muni job nodes JOB_ID [--apply] [--space-id ID] [--page-id ID] [--launcher-node-id ID] [--no-refresh-contract] [--no-prune-stale] muni job materialize JOB_ID [--preview] [--space-id ID] [--page-id ID] muni job show-results JOB_ID [--space-id ID] [--page-id ID] muni job download JOB_ID FILE [--output PATH | --stdout] muni job download-all JOB_ID [--output-dir DIR] [--path PREFIX] [--glob PATTERN] muni job scaffold-table JOB_ID --kind pxdesign-summary [--output PATH]

Rich job introspection plus the materialisation surface that powers the UI's "Show results" button.

  • summary / schema / result — schema-owned introspection
  • params — the input parameters the job was submitted with (via the jobs.detail SDK method)
  • nodes / materialize / show-results — drive canonical /api/sdk job-output methods
  • scaffold-table --kind pxdesign-summary — locates design_outputs/config/summary.csv, downloads it, runs the table script generator

Files & logs #

muni files SDKstable

muni files JOB_ID [--path PREFIX] [--json]

List output files (tree view by default).

muni logs SDKstable

muni logs JOB_ID [--follow] [--interval 5] [--max-lines 200] [--json]

Provider logs. --follow polls until terminal status.

muni download SDKstable

muni download JOB_ID FILE_PATH [--output PATH | --stdout]

One file. Auto-falls-back to download-all with a path prefix if the target is a directory. --stdout prints text directly or base64 for binary.

muni download-all SDKstable

muni download-all JOB_ID [--output-dir DIR] [--path PREFIX] [--glob PATTERN]

Bulk download with prefix + glob filtering (AND'd). Glob matches against either full path or basename.

Memory-resident — base64 for binary content. Not streaming; very large outputs may pressure memory.

Python SDK #

The CLI is a thin shell over MuniClient. You can use it directly in Python:

from muni import MuniClient

client = MuniClient()

# Sub-client path (preferred)
spaces = client.spaces.list()                      # → list[SpacesListOutputItem]
page = client.spaces.search(query="protein", limit=50)  # → dict envelope
node = client.nodes.get("node_abc123")             # → dict (camelCase)
result = client.jobs.submit("rfd3", {"pdb_id": "1UBQ"}, title="test")

# Schema-owned job outputs
files = client.job_outputs.list(result.job_id)
summary = client.job_outputs.summary(result.job_id)

Sub-clients #

Sub-client Backing Key methods CLI surface
client.user SDK whoami (local), balance, last_visited whoami, balance
client.tools SDK list, get, schema tools, tool
client.jobs SDK submit, get, wait, search, cancel, logs run, job list/get/wait/cancel/logs
client.job_outputs SDK list, read, summary, query, plan, materialize job files/read/query/materialize
client.spaces SDK list, get, create, delete, members, invite, resolve spaces, space
client.pages SDK list, get, get_default, create, rename pages, page create/list/use
client.canvas SDK current_position, preview_placement canvas position/placement
client.nodes SDK list, get, create, update, save, submit, remove, restore, duplicate, move, select, focus, group, layout_group, ungroup, table_summary, table_query, table_select, import_image, structure_script, structure_gallery, get/set_python_code, report_render most node subcommands
client.node_files SDK list, get, update, read_workspace, replace_workspace indirectly via typed Code commands
client.node_data SDK read, upload_blob used by bounded node-data reads and validated imports
client.connections SDK list, create, update, remove, disable connections, connection, node connect
client.scripts SDK generate, typed builders, starter, preflight used by node creation and preflight

Script generation #

Executable and declarative node scripts use the @muni/sdk node() primitive. Generating those by hand is error-prone, so the canonical generator lives server-side in the TS SDK and is exposed via client.scripts.*. Content/facet imports do not use these generators; typed Text/Image/PDF/Table/JSON/Structure/ Compound helpers own those values directly.

script = client.scripts.job(tool="rfd3", params={"pdb_id":"1UBQ"})  # job node
script = client.scripts.starter(kind="python")                       # blank scaffold

Outcome: UI drag-drop, CLI imports, and AI chat produce byte-identical scripts for the same input. The CLI used to have its own _build_job_node_script() helper — it was removed in v0.1.34 in favour of client.scripts.job().

Preflight (client.scripts.preflight) runs the real server-side analyzer pipeline: syntax / typecheck / api-usage / ports / literal-schema / graph. Identical diagnostics to what gets persisted on muni_node_runs.preflight_* at execution time.

Realtime & polling #

muni wait, muni run --follow, and node-submit polling all share the same waiter in muni/realtime.py:

  1. Try to subscribe to the Supabase broadcast channel jobs:user:<user_id> (DB trigger broadcast_job_changes fires on every jobs INSERT/UPDATE).
  2. If the realtime package is missing, the websocket can't connect within 8 s, or the token can't be refreshed, fall through to plain HTTP polling.
  3. For long jobs, refresh the access token every 40 min (JWTs expire after ~1 hr).

This is the only piece of the CLI that talks to Supabase Realtime; nothing else needs it.

Watchlist #

Things to keep an eye on. Not bugs — items that are mid-flight, partial, or surprisingly shaped.

ItemStateNotes
cli.py size monolith 5,140 lines, all command handlers and the full argparse build. No formal cap, but it's a long-standing split-candidate. Splitting is mechanical but loses the single-grep-to-find-it ergonomics.
Schema-versioned tool outputs in-flight Recent releases introduced tool --schemas, tool --schema, job result/schema/files/read/query. tool --outputs still falls back to legacy tool.outputs if no versioned schema is published.
Display intent (--no-nodes / --no-result-nodes) recent Three-way distinction (auto / no-result-nodes / no-nodes) only landed in v0.1.34. Older docs and examples still assume "always-create job node". Worth cross-checking README and tool examples.
Generated models stale-detection manual muni/_generated/models.py is regenerated by pnpm codegen:python in muni-app. No CI step here verifies that the local copy matches muni-app's master. Drift between repos goes silent until a typed call fails validation.
Realtime opt-in soft-dep realtime is pulled in via Pydantic but the websocket path can quietly fall back to polling without telling the user. Diagnostic --debug flag would help.
node restore command shipped client.nodes.restore() and muni node restore both restore soft-deleted nodes.
Remaining page lifecycle partial muni page create TITLE --space SPACE, list, and use are first-class. The SDK also exposes rename; delete and reorder are not in the current public contract.
node content Storage bypass mixed Only place in the CLI that hits Supabase Storage directly instead of going through the SDK. Works fine, but it's the one routing exception worth knowing about.

Half-baked & rough edges #

Honest assessment The codebase is mature for a 0.1.x — no FIXMEs or TODOs hiding in the command handlers, no stubbed subparsers. Everything in this section is "real but rougher than the rest", not "broken".

1. Node examples are placeholder-grade

NODE_EXAMPLES in cli.py hard-codes a single kind (structure) with two scenarios. The PXDesign gallery script is embedded as a triple-quoted Python string. Adding a new kind means editing the dict in cli.py, not a data file. Ideally these should move to the same registry the app uses.

2. Job ID parsing is heuristic

The CLI accepts three flavours of job identifier (job_<uuid>, muni-<tool>-job_<uuid>, rw-<workflow_id> for Rowan, plus the legacy bioarena-<tool>-…). Most code paths handle them but a few helpers prefer one shape. If something behaves oddly with a mixed ID, normalise to job_<uuid>.

4. --json shape isn't formally specified

Most commands honour --json and emit something sensible — but the exact key set isn't pinned with a Pydantic model on the CLI side; it's whatever the handler happens to dict-build. Reasonable to call this "JSON for humans" rather than "JSON for scripting". A formal contract would help downstream agents.

5. Download is memory-resident

muni download-all reads each file into memory (base64 for binary). For typical protein-design outputs this is fine; for very large structure ensembles it could matter. No streaming path today.

6. config.py auto-migration is silent

_replace_legacy_api_urls() quietly rewrites old URLs in ~/.muni/config.json. It's doing the right thing but the user never sees a message — if a profile suddenly points somewhere different, this is why.

7. Logs --follow is poll-based

Realtime only carries job-status updates, not log lines. muni logs --follow polls every --interval seconds (default 5). Fine, just worth knowing it's not "live".

Canonical SDK domains #

The retired flat facade has been removed. Public Python operations live on their owning clients: user, spaces, pages, tools, jobs, job_outputs, nodes, typed node clients, and pipeline. The CLI performs only presentation conversion and argument translation around those methods.

Dependencies #

DepWhy
realtime >=2.28.0, <3Supabase Realtime websocket client; transitively pulls Pydantic v2 which we use for the generated models
Python stdlib only for HTTPAll urllib.request; no requests, no httpx. Keeps install fast and isolation clean.

Local dev #

# Editable install
pip install -e .

# Run against a local stack (muni-app on 3000, tools-api on 8000)
export MUNI_API_URL=http://localhost:8000
export MUNI_APP_URL=http://localhost:3000
export MUNI_SUPABASE_URL=http://localhost:54321
muni login --device   # or --email/--password if password auth is enabled

# Run a single command
muni whoami

Testing #

Tests live in tests/. Run with:

python -m pytest tests/

Notable suites (some currently modified in working tree):

Release #

Versions come from Git tags through setuptools-scm. Every commit pushed to master is tested, tagged with the next patch version, and published to PyPI by .github/workflows/publish.yml. For a major or minor release, run that workflow manually and provide the desired X.Y.Z version.

The companion PYPI_README.md is what ships to PyPI users — keep it user-facing. This wiki lives at docs/wiki.html and is intended to stay internal.


Wiki refreshed against muni-cli master @ v0.1.40 on 2026-05-30. Source-of-truth for any command behaviour is muni/cli.py + the relevant sub-client in muni/clients/. If anything here disagrees with the code, the code wins — please update this file.