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.
/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
| Item | Value |
|---|---|
| Current version | Latest vX.Y.Z Git tag (published automatically from master) |
| Entry point | muni.cli:main |
| CLI module size | muni/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 models | muni/_generated/models.py — do 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.
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:
- SDK (
/api/sdk) is the new, preferred path. Every sub-client routes here. The server runs the canonical TypeScript SDK so the CLI and the UI hit identical code paths (validation, side effects, broadcasts). Returns typed Pydantic models generated from Zod schemas. - Tools API (port 8000, Elysia/Bun) is where heavy job execution lives behind
the app server. The CLI keeps
MUNI_API_URLfor configuration compatibility and local app URL inference, but command traffic routes through/api/sdk. - Supabase is hit directly for auth (token exchange + refresh) and for the Realtime
broadcast channel that powers
muni wait --follow-style behaviour.
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/ |
MuniClient (list_tools, get_tool,
job_status, job_results, cancel_job) wrap sub-client calls and then
re-shape responses for display. The hand-off is real but minimal — see the
migration section.
Profiles & config #
Files
| Path | Purpose |
|---|---|
~/.muni/config.json | Profiles, URLs, active space/page |
~/.muni/credentials.json | Per-profile access/refresh tokens |
.muni/workspace.json | Project-local profile, space, and page binding for agent runs |
Environment overrides
| Variable | Effect |
|---|---|
MUNI_PROFILE | Switch profile for the current process |
MUNI_API_URL | Override Tools API URL; retained for compatibility and local app URL inference |
MUNI_APP_URL | Override muni-app URL |
MUNI_SUPABASE_URL | Override Supabase URL |
MUNI_SUPABASE_PUBLISHABLE_KEY | Override Supabase publishable key |
MUNI_SPACE_ID / MUNI_PAGE_ID | Per-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— create.muni/workspace.jsonin cwdmuni workspace show— print the local profile/space/page bindingmuni workspace unlink— remove the local binding
Profile commands
muni config Localstable
muni config— interactive prompt to set API/app/Supabase URLs & keysmuni config show— print active profile (masks key)muni config profiles— list configured profilesmuni config use NAME— switch the default profile
Auth & account #
muni login Supabasestable
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 atmuni.bio/device. Use over SSH or in tmux without a browser. - Password (
--email/--passwordor--password-flow) — legacy Supabase email/password. Kept for CI scripts; not the default.
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
Clears credentials for the current profile.
muni whoami SDKLocalstable
Locally inspects credentials, then best-effort hits client.user.balance() and resolves the active space.
Balance failure is non-fatal.
muni balance SDKstable
Credit balance via client.user.balance() → POST /api/sdk user.balance.
muni update Localstable
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
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
create NAME— also sets it active and pins its default pageuse [ID|NAME|none]— set active; interactive picker if no arg;none/api-onlyclears the active spacemembers SPACE_ID— list member emails and rolesinvite SPACE_ID EMAIL— invite a userdelete SPACE_ID— delete (destructive)
muni pages SDKstable
List pages in the active space (or override with --space-id).
muni page SDKstable
Set the active page. Resolves by ID or exact title within the active space. Interactive picker if PAGE_ID omitted.
Canvas #
muni canvas position SDKstable
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
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
List or search nodes. Filterable by type. Default limit: 20.
muni node create SDKstable
Valid node types (from VALID_NODE_TYPES): table, code, json, structure, compound, chart, text, image, job, pipeline, group. The legacy muni node create TITLE --type TYPE form remains accepted for compatibility.
Hydration is selected by node capability. Text BasicNodes use nodes.loadData({kind: "text-content"}) directly with --content, --text, or --file; they do not execute a generated script. Other content adapters are moving to the same direct-hydration model as their SDK capabilities land.
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
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
Full state: script, position, runtime, currentRun, compile/execute/validate results, render health, output data, connections, Python files (for code nodes).
muni node edit SDKstable
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
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.
muni node remove SDKstable
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
Compile + execute + validate + store + cascade. For async (job-backed) nodes,
--wait polls currentRun.overallStatus until terminal.
muni node move / select / focus SDKstable
Canvas-side ops that broadcast over Realtime to attached clients.
muni node structure SDKstable
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
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
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
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
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.
Connections #
muni connections SDKstable
List edges (source → target with port + kind) on the active page.
muni connection <create|edit|remove> SDKstable
Auto-reruns the target node after any change unless --no-run. --wait blocks until the
target run reaches terminal state.
Tools #
muni tools SDKstable
List/search the tool catalog. --all paginates through everything; otherwise default page size is shown.
muni tool SDKstable
(no flag)— full tool detail (header, params, examples)--inputs— input parameter schema only--outputs— declared outputs and their provider/derived paths (falls back to legacytool.outputsif no versioned schema is published)--schemas— list of available output schema versions--schema [VER]— full JSON Schema for one version (defaults tolatest)--examples— curated usage examples
Flags are mutually exclusive — passing more than one fails with a clear error.
Run (job submission) #
muni run SDKstable
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 combo | What gets created |
|---|---|
| default (active space + page) | Canvas job node + auto-materialised result nodes |
--auto-materialize | Explicit automatic result nodes; requires a resolved canvas target and conflicts with --no-space/--no-nodes |
--no-auto-materialize | Job node only; result nodes remain manual |
--no-result-nodes | Job node only; user clicks "Show results" later |
--no-nodes | No job node; visible in Jobs panel only (still belongs to the space) |
--no-space or no active space | Headless / API-only — no canvas footprint |
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
Search/list your past jobs. Backed by client.jobs.search() → typed JobsSearchOutput,
returned most-recent first.
Filter / organise by
| Axis | How |
|---|---|
| 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.
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).
muni status SDKstable
Single-job status. Field normalisation flattens camelCase (createdAt → created_at) and aliases (tool_name → job_type) for back-compat with display formatters.
muni results SDKstable
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
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
Cancel a running job. Exit 1 if already terminal (HTTP 409 — handled cleanly).
muni job <subcommand> SDKstable
Rich job introspection plus the materialisation surface that powers the UI's "Show results" button.
summary / schema / result— schema-owned introspectionparams— the input parameters the job was submitted with (via thejobs.detailSDK method)nodes / materialize / show-results— drive canonical/api/sdkjob-output methodsscaffold-table --kind pxdesign-summary— locatesdesign_outputs/config/summary.csv, downloads it, runs the table script generator
Files & logs #
muni files SDKstable
List output files (tree view by default).
muni logs SDKstable
Provider logs. --follow polls until terminal status.
muni download SDKstable
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
Bulk download with prefix + glob filtering (AND'd). Glob matches against either full path or basename.
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")
# Legacy flat methods (still supported)
spaces_dicts = client.list_spaces() # → list[dict] (snake_case)
status, results = client.job_results("muni-rfd3-job_...")
Sub-clients #
| Sub-client | Backing | Key methods | CLI surface |
|---|---|---|---|
client.user |
SDK | whoami (local), balance, last_visited | whoami, balance |
client.tools |
SDK | list, get, examples, schema_versions, schema | tools, tool |
client.jobs |
SDK | submit, status, wait, results, search, cancel, summary, query, logs, batch | run, jobs, status, results, wait, cancel, job summary/query/logs |
client.job_outputs |
SDK | list_files, summary, query, read_file, plan_materialization, show_on_canvas | job files/read/query/nodes/materialize/show-results |
client.files |
SDK | list, read, download, save, save_all (compat aliases for job_outputs + local save) | files, download, download-all |
client.spaces |
SDK | list, get, create, delete, members, invite, resolve | spaces, space |
client.pages |
SDK | list, get, get_default, create, delete, reorder | pages, page 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, create, update (Python workspace files) | indirectly via node create --python / node edit |
client.node_data |
SDK | read, list, upload_blob | used by node content and the script-first import path |
client.connections |
SDK | list, create, update, remove, enable, disable | connections, connection, node connect |
client.chat |
SDK | create_session, list_sessions, get_session, delete_session, list_messages | no CLI yet |
client.sandbox |
SDK | run (JS/TS in isolated VM) | no CLI yet |
client.scripts |
SDK | build (dispatcher), compound, text, instruction, json, table, structure, image, starter, job, preflight | used by node create --file, node check, run |
Script generation #
Node scripts are TypeScript wrapping the @muni/sdk node() primitive. Generating
these by hand is error-prone, so the canonical generator lives server-side in the TS SDK
and is exposed via client.scripts.*. The CLI prepares primitives (parses CSV, reads files,
derives labels) and hands them off:
script = client.scripts.compound(smiles="CCO", label="Ethanol")
script = client.scripts.table(columns=["a","b"], rows=[{"a":1,"b":2}], label="data")
script = client.scripts.structure(bucket="muni-node-data", path="foo/bar.cif", filename="bar.cif")
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:
- Try to subscribe to the Supabase broadcast channel
jobs:user:<user_id>(DB triggerbroadcast_job_changesfires on everyjobsINSERT/UPDATE). - If the
realtimepackage is missing, the websocket can't connect within 8 s, or the token can't be refreshed, fall through to plain HTTP polling. - 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.
| Item | State | Notes |
|---|---|---|
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. |
| Chat & Sandbox sub-clients | no CLI | Both fully wired in Python (client.chat, client.sandbox) but no
muni chat / muni sandbox commands. If anyone asks for them, they're a small lift. |
node restore command |
no CLI | client.nodes.restore() exists; no muni node restore CLI. Soft-deleted nodes are
recoverable, just not from the terminal. |
pages.create/delete/reorder |
no CLI | SDK methods exist. CLI only has page use. page create/delete would round out the UX. |
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 #
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 (or be fetched live via client.tools.examples).
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".
Dual-path migration #
Several legacy flat methods on MuniClient wrap a sub-client call and then re-shape the response
for back-compat. They are not separate codepaths — they delegate — but they exist for two reasons:
- Display formatters in
display.pyexpect snake_case field names (reference_url, notreferenceUrl;job_type, nottool_name). - Python users importing
MuniClienthistorically called the flat methods. Removing them would be a breaking change.
The flat → sub-client map:
| Legacy flat method | Preferred sub-client | What changes |
|---|---|---|
get_balance() | user.balance() | flat returns dict; sub-client returns UserBalanceOutput |
search_spaces() / list_spaces() / create_space() / delete_space() / invite_to_space() | spaces.* | flat returns snake_case dicts; search_spaces() is paginated |
list_tools() / get_tool() / get_examples() | tools.* | flat preserves reference_url, paginates client-side |
run() / run_batch() | jobs.submit() / jobs.batch() | flat normalises into {status, tool, job_id, provider_job_id} |
job_status() / job_results() / cancel_job() | jobs.* | _normalize_job_dict applies snake_case + tool_name→job_type |
list_files() / download_file() / save_file() / save_all_files() | job_outputs.* (or files.*) | flat adds local fs writes + base64 handling |
create_node() / read_node() / edit_node() / submit_node() / remove_node() | nodes.* | flat returns a stable shape regardless of currentRun presence |
table_summary() / table_query() / table_select() | nodes.* | flat translates 1-based row indices to 0-based |
Direction of travel: the flat surface is frozen, sub-clients are where new methods land. Don't add new flat wrappers — call the sub-client directly. If a display formatter blocks you, update the formatter to accept either shape.
Dependencies #
| Dep | Why |
|---|---|
realtime >=2.28.0, <3 | Supabase Realtime websocket client; transitively pulls Pydantic v2 which we use for the generated models |
| Python stdlib only for HTTP | All 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):
tests/test_cli_page_resolution.py— active space/page resolution edge casestests/test_display.py— output formatterstests/test_jobs.py— job lifecycle helperstests/test_tools_cli.py— tool listing/gettests/test_sdk_contract.py— new (uncommitted) contract checks against generated modelstests/test_auth_refresh_lock.py— concurrency test provingensure_token()'s file lock refreshes exactly once under parallel calls, with a no-lock negative control (added v0.1.40)
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.