npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bluearch/mission-control

v1.2.3

Published

BlueArch Mission Control — keyboard-first terminal TUI for real Claude Code agents. Up to 64 live at once, every conversation restored with one command, per-session cost measured and hard spend caps that refuse to go past them. By BlueArch, a cloud govern

Downloads

942

Readme

This is not a mockup. Every session is a real claude CLI process in its own pseudo-terminal. Tokens, context and git status are read from claude's own session files and from git itself; cost is computed from those measured token counts against a published per-model rate table, and a rate we have not verified is marked as an estimate on the card rather than quietly presented as fact. Mission Control stores nothing off your machine and reports nothing to us: no telemetry, no analytics, no BlueArch account, and nothing listening on a port. btop × lazygit aesthetic, built on Ink (React for terminals) with no build step.

What it does

Bring the whole fleet back. Close the terminal, reboot, come back tomorrow — :resume-all restarts every session that was open, each one reattached to its own conversation through claude --resume, in its own folder and branch, on the model it was using, with the tokens and dollars it had already spent carried forward. Not empty terminals in the right directories. The actual conversations.

See the bill while it runs. Every session shows its own token counts, context pressure and running cost, derived from the usage claude records for each turn and priced against a per-model rate table. Sub-agent and workflow fan-out is attributed back to the session that spawned it, so one agent spawning five still gives you one honest number.

Stop a runaway agent, don't just watch it. :cap <slot> <usd> refuses to send another message once a session passes its limit. :budget <usd> refuses to launch anything new once the fleet passes its daily figure. These are enforcement, not a dashboard that tells you afterwards.

Never hunt for the stuck one. Status comes from Claude Code's own lifecycle hooks, not from watching a pane go quiet, so a session blocked on a permission prompt reads as needing you rather than as idle. Answer it with one key from the grid.

Find the agents you forgot you were paying for. :bg lists the conversations claude is running outside your fleet — the ones a force-closed terminal left behind, still open, still holding a session id you cannot resume. Measured on one machine: six of them, the oldest blocked for 9.8 days, together holding 1875 MB across 13 processes. Two keystrokes clears one.

Nothing leaves your machine. No analytics, no account with us, no service listening on a port. The tool ships five runtime dependencies and none of them is an HTTP client or an analytics SDK.

Who this is for

  • macOS or Linux developers using Claude Code who run more than one agent at a time and lose track of which one needs them.
  • People who want fleet-level awareness — cost, context pressure, sub-agent fan-out, stuck detection, approval prompts — without babysitting each tab.
  • Terminal-first, keyboard-first users who'd rather not run a GUI app or Electron.
  • Anyone who wants their agent tooling to be open source and self-hosted — auditable, local-first, and yours to theme.

Why not tmux or cmux?

Mission Control isn't a generic multiplexer or a native app — it's a purpose-built TUI that understands Claude Code agents. It happily runs alongside tmux and over SSH.

Each row below describes what the three tools document about themselves; the Mission Control column is the one we can prove from this repository.

| | tmux | cmux | Mission Control | |---|:---:|:---:|:---:| | What it is | terminal multiplexer | native macOS GUI app | terminal TUI | | Runs anywhere (SSH, any terminal) | ✅ | local macOS | ✅ | | Purpose-built for Claude Code agents | ❌ | ✅ | ✅ | | Per-agent cost / context / token tracking | ❌ | ❌ | ✅ | | Sub-agent fan-out awareness (⋔) | ❌ | ❌ | ✅ | | Approval / permission routing | ❌ | partial | ✅ | | Resume all active sessions | ❌ | ❌ | ✅ | | Broadcast slash commands to all agents | ❌ | ❌ | ✅ | | No prefix keys, zero config to start | ❌ | ✅ | ✅ | | No GUI / no Electron | ✅ | native (no Electron) | ✅ | | Open source & self-hosted | ✅ | ✅ | ✅ |

Quick start

Requires Node 20+ and platform build tools for the PTY layer (macOS: Xcode Command Line Tools; Linux: build-essential).

# Run it now, no install:
npx @bluearch/mission-control

# …or install the `mc` command globally:
npm install -g @bluearch/mission-control
mc

npm ≥ 11.19 (allow-scripts): newer npm blocks packages' install scripts until you allowlist them. mc self-heals at boot if its postinstall was skipped, but for a fully clean install allow the three that matter once: npm config set allow-scripts=@bluearch/mission-control,node-pty,esbuild --location=user (mc fixes node-pty's spawn-helper permissions; node-pty builds its native PTY binding; esbuild fetches the binary tsx runs on.)

Installing from the repo also works (npx github:xxyjoel/ba-mission-control) and tracks main instead of the released version. A Homebrew tap is planned.

Prefer to hack on it? See From source below and CONTRIBUTING.md.

Themes

Seven built-in palettes ship out of the box — BlueArch (default), Tokyo Night, Gruvbox Dark, Catppuccin Mocha, Solarized Dark, Amber (CRT), and the green-phosphor Matrix theme. Switch live with :theme <name> (e.g. :theme matrix) or in Settings → Colors. Self-hosted means it's yours to re-theme — palettes live in tui/lib/themes.js.

Requirements

  • macOS (uses POSIX signals for pause/resume — Linux works too; Windows untested)

  • Node 20+

  • claude CLI on $PATH (Claude Code 2.x). Verify with claude --version.

  • A signed-in account — either ANTHROPIC_API_KEY, or a stored OAuth session from claude auth login. Mission Control runs claude auth status on startup and prints the account it found:

    [mc] claude: 2.1.142 (Claude Code)
    [mc] auth · [email protected] · max plan · claude.ai

    The same info shows as a chip in the top-right of the header strip and can be re-probed at any time with :whoami (alias :auth). If the banner says not signed in, quit and run claude auth login.

  • git on $PATH

  • A terminal that supports 24-bit color + Unicode box-drawing (modern macOS Terminal / iTerm2 / Alacritty / Ghostty / wezterm all qualify)

From source

For contributors, or to run the latest main:

git clone https://github.com/xxyjoel/ba-mission-control.git
cd ba-mission-control
npm install
npm start

The TUI takes over the terminal. To leave, press q (or Ctrl-C).

After npm install, you can also invoke the bin directly:

./bin/mc.mjs

…or link the mc command globally:

npm link
mc

Scrolling back through output. In the Zoom view press Ctrl+F to enter scroll mode, then w/s for a line, f/b for half a page, g for the top and G to return to the live output. In the ! shell overlay use PageUp and PageDown; typing anything returns you to the live output. Both views hold your place while the session keeps printing, and both are limited by the 5,000 rows of history each session keeps — scroll past that and you are clamped to the oldest row still held.

Configurable env vars:

| Var | Default | Effect | | -------------- | -------------- | ------ | | CLAUDE_BIN | claude | Path to the claude CLI | | REPO_PARENTS | see server/repos.mjs | Colon-separated parent dirs scanned for the New Session repo picker. Recursive to depth 3. Overridden by a folder chosen in-app via :repos (which persists to settings). | | MC_MOCK | unset | Fixture name (e.g. approval-request) — when set, every launched session replays a JSONL fixture from server/fixtures/ instead of spawning a real claude subprocess. Use for deterministic Zoom UX iteration without API spend. | | MC_HEAP_LOG | unset | When set, logs memory (rss/heap) + per-structure counts every 60 s to ~/.local/state/claude-mc/heap/ for diagnosing long-uptime memory growth. Independent of this, kill -USR2 <mc pid> writes a heap snapshot on demand (owner-only, 0600). Off by default. |

Settings (theme, density, grid columns, max windows per pane, ctx threshold, etc) persist to ~/.config/claude-mc/settings.json — open with Esc in the TUI.

Card anatomy. Each grid tile is a pure-stats dashboard (fixed height, no wrapping): title + status, model + branch + git, ctx bar, tok/min sparkline (empty at zero throughput) with the claude subprocess's live CPU% + memory right-aligned (4% 182M, one shared ps sample per fleet tick), a triage row (▸ 5/7 ██████░░ <next action> — todo burndown from the session's live TodoWrite list plus a status-driven next-action verb; blank when there's nothing actionable — status and its duration already live in the card's corners), the current item (↳ <in-progress todo>), session vitals (small ●<score> health dot + turns / messages / uptime / time-in-state), and a cost + tokens foot. It intentionally shows no session text — read the running conversation by zooming (↵) or in the fleet log.

Which conversation a card is showing. The model row carries the first eight characters of the session id — the same eight :bg and claude agents print — so a card can be matched against them by eye. A yellow !N beside it means claude is holding N other live conversations in the same folder. That is a warning, not an error: your typing goes to the session the card names, and the others cannot see it. Open :bg to look at them. No mark is drawn when the count is zero, and none is drawn when claude's session list could not be read — an absent mark never claims there is only one conversation.

The triage row answers the scan-10-cards question "does this need me, when, and what next": check back (working), ready to review → (idle, plan complete), needs a nudge → (idle, plan unfinished), needs input · answer to continue (waiting) — colored by urgency.

Paging. The grid shows at most Max windows per pane cards at once (Settings → LAYOUT, windowsPerPane, default 9 — a 3×3); when more sessions are live, or the terminal is too short to fit them, the extra cards spill onto additional panes instead of being clipped. [ / ] switch panes; the active pane follows the focused card.

What's actually running

Each slot runs a real interactive claude in its own pseudo-terminal — the same binary and the same interface you get in a normal shell — spawned with --session-id, --model, --permission-mode and a status hook Mission Control installs for that session. It does not drive claude through a wire protocol. It reads what claude already writes: the session JSONL under ~/.claude/projects/ for tokens, cost and tool activity, and the hook's own status stream for whether a session is working, idle, or blocked on a prompt.

A legacy path still exists behind FLEET_USE_PTY=0. It drives claude --print --input-format stream-json --output-format stream-json over stdin and stdout and maps those events onto the same UI state. It is kept as a rollback and is not what you get by default. The table below describes that path:

| stream-json event (legacy path only) | UI effect | | --- | --- | | system.init | session attached; appended to tail | | stream_event.content_block_delta (text) | live activity line during a turn | | assistant (text) | activity line + log entry, usage → tokens, input_tokens → context | | assistant (tool_use) | log entry ▸ <tool>: <summary> | | user (tool_result) | log entry ← tool_result <preview> | | result | total_cost_usd → cost, status back to idle |

Verifiability — every action is observable:

  • The per-session log tail (shown in the zoom overlay and merged into the fleet log) records every spawn / SIGSTOP / SIGCONT / user message / tool call / tool result / turn-complete event with timestamps. (The grid card is a pure-stats tile — it no longer renders the tail; see Card anatomy below.)
  • The fleet log pane at the bottom is a chronological merge of all live agents' tails — like tail -f over the whole fleet. Its height is exactly the Fleet log lines setting (clamped only on terminals too short to fit).
  • Costs are computed from the usage token counts claude records for each assistant message, priced against the per-model rate table in tui/lib/models.js and deduplicated by message id. They are an accurate estimate at published rates, not a copy of your invoice. A model whose rate we have not verified inherits the newest rate in its family and is marked ~ on the card, and an unknown model is never priced at $0.
  • Context window (ctx) tracks the main thread only: sub-agent (Task) turns carry isSidechain and are excluded so the gauge never dips to a sub-agent's smaller context mid-turn. in / out / cost are cumulative session totals (sub-agent spend included) and reset on /clear alongside ctx; they persist across a relaunch only through a proper save & quit.
  • Tokens in counts fresh input only (input_tokens + cache_creation). With prompt caching on, claude re-reads the whole context window from cache on every assistant message, so cache_read_input_tokens re-counts the same tokens each turn and, if summed into in, dwarfs it ~100× (e.g. 67M "in" on a 60k window). Those re-reads are broken out as cache (shown in Zoom, billed at the 0.1× cache-read rate in cost). ctx still reflects the full live window (fresh + cache read), which is the real size on the wire.
  • Parallel sub-agents (Task / Workflow fan-out) are surfaced on the card: when any are in flight the current-item row shows ⋔{n} (the count, or the single agent's label), and Zoom lists each with its elapsed time. The server pairs each tool_use with its returning tool_result to know what's live. STUCK is suppressed while sub-agents run — they work on sidechains the main thread can't see, so the parent's activity clock legitimately goes quiet.
  • Sub-agent token + cost consumption is counted. Sub-agent turns are written to a separate tree (<sessionId>/subagents/agent-*.jsonl) the main tailer never reads, so their tokens, cost, and tok/min used to be invisible — a fan-out session read near-zero throughput. A dedicated sidechain tailer folds that usage into the parent's in / cache / out / cost / tok-min (not ctx — sidechains keep their own window). On resume, pre-existing sub-agent files are primed at EOF so historical spend isn't double-counted.
  • Git state (branch, dirty count, ahead/behind) is read via git calls in the session's cwd on launch and after each turn.

Session states

Each agent has exactly one status from this six-value enum (server/agent.mjs):

| State | Meaning | | --- | --- | | idle | attached, no current activity | | working | actively processing — streaming text, calling tools, thinking | | waiting | model delivered a prompt awaiting user approval (a.k.a. "needs input") | | paused | process frozen via SIGSTOP (user pressed pause) | | error | crashed or API failure — auto-restart may retry | | empty | slot is vacant, no agent assigned |

The fleet header (Header.jsx) shows live counts for work / wait / err plus (when any session is live) how many are over the context threshold (ctx≥600k 0/3 = zero of three sessions above 600k tokens of context), and an aggregate NOMINAL / AWAITING / DEGRADED pill. Session uptime and UTC clock sit early in the strip so they survive a narrow resize. When two or more subscriptions are connected, the header says all sessions (count alongside); each provider’s green ◆ active marker lives on its Aggregate row, not as CC/CUR chips in the header.

The Aggregate line under the header is Claude-only (tokens, session/week cost, 5h/7d plan %) when only Claude is connected. With multiple subscriptions it splits into one Aggregate row per subscription — each provider keeps its own meters (Claude’s rolling plan windows stay on the Claude row; Cursor shows measured $ / tokens when usage sync is on, otherwise sync off / $-.--). Metrics are never blended across providers whose baselines do not match.

Two live slots may share a project path; transcripts and PTYs stay isolated, but the working tree and git index are shared. Prefer git worktree (or careful sequencing) for parallel writers — git commits are the source of truth for history, not for concurrent mid-edit races. Launching into a cwd already used by another live slot shows a soft warn toast.

Not states (derived indicators):

  • STUCK Nm — a red chip on the card when an agent is working or waiting AND has been silent for ≥ 5 minutes (agent.stuckMin). Not a status — the underlying state is still working/waiting.
  • ctx high / ctx full — derived from agent.context vs the warn-band / threshold settings, not from status.
  • Session Health dot — when the optional Session Health Benchmark Stop hook is installed, mc reads each project's .project-health/history.jsonl and shows a small ●<score><trend> dot at the left of the card's vitals row (colored by verdict: green HEALTHY / cyan STABLE / yellow / red), plus a health segment on the zoom stats line. The dot is simply omitted until the project has logged a scored turn; mc only reads the score, it doesn't compute it (tui/lib/projectHealth.js). The untrusted verdict string is no longer rendered on the card at all — only the numeric score — so it carries no terminal-escape surface (zoom still shows the verdict word, humanize()d).

Status accuracy — hook-driven source of truth. The session JSONL has no permission-prompt event and marks end_turn mid-work, so a JSONL-only status misreads: it shows working when the agent is actually blocked on a tool-approval prompt, or idle mid-turn. mc fixes this by injecting Claude Code's own lifecycle hooks into every spawned session (--settings → server/hooks/emit-status.mjs writes PreToolUse / Notification / Stop events to ~/.local/state/claude-mc/status/<sid>.ndjson; server/statusHookTailer.mjs tails it). PtyAgent.toJSON then derives status from those events:

  • Notification:permission_prompt → waiting (the connector is blind to prompts — this is the "working when it's actually asking for input" fix);
  • PreToolUse → working, held until Stop (covers the mid-turn end_turn flash without terminal scraping);
  • Stop / Notification:idle_prompt → idle, winning over a stale connector working (compared against a JSONL-only clock, lastConnectorTs, so Claude's constantly-repainting TUI can't keep a finished session pinned to working).

detectApprovalPrompt is kept as an instant-INPUT fast-path while a tool is outstanding (the permission_prompt hook is delayed ~10–20s). Sessions with no hook events yet — the legacy FLEET_USE_PTY=0 Agent, or a PTY session before its first event — fall back to the older terminal-scrape overlay (detectWorking / detectApprovalPrompt in server/ptyAgent.mjs). Hooks are auto-injected; nothing to configure. (Takes effect per session on next spawn — a session already running when mc updated keeps its old status source until relaunched.)

Session summaries (/compact, /compact-restart) are manual, user-invoked actions — they're not bound to a state transition. There is no automatic stage-bound summary today.

Hotkeys

| Keys | Action | | --- | --- | | ← ↑ ↓ → (or h j k l) | Move focus across the grid | | ↵ | Zoom focused session — or open New Session if nothing is live | | 1–9, 0 | Jump to slot 1–10 (slots 11+ via arrow nav or :goto <slot>) | | [ / ] | Switch to the previous / next pane — only when the grid pages (see LAYOUT → Max windows per pane) | | Esc | Open settings menu (or close current overlay) | | , | Settings menu | | B | Broadcast modal — types the message into each targeted session and submits it (no manual Enter per session) | | D | Fleet dashboard — sortable one-row-per-slot table for at-scale triage | | n / N / Ctrl+N | New session — appended below the last active card (fills a killed-slot hole only when there's no room to append) | | P / R | Pause (SIGSTOP) / Resume (SIGCONT) | | K | Kill — uppercase only (lowercase k stays vim-up and never kills); armed by first press (3s window); confirms on second K. :kill <slot> follows the same arm/confirm flow; :kill! <slot> bypasses. | | A | Approve — send a generic "continue" message to the focused session. Only accepted while the session is waiting for input (a warn toast explains otherwise) | | Shift+Tab | Cycle focused session's permission mode: plan → auto → acceptEdits | | ! | Open shell overlay — a persistent $SHELL pane for aws sso login, git, kubectl, etc. | | ? | Help | | / | Filter — type a substring (matches name/branch/model/status); non-matches dim. Press / again to clear. | | : | Command bar (see below) | | Q | Quit — opens a confirm with an explicit choice: [s] save & quit (or Enter) vs [d] quit, no save vs [n]/Esc cancel | | Ctrl-C | Quit immediately (treated as no save — see below) |

Session save / restore

Save is opt-in; every other exit is a "clear." Only a proper [s] save & quit preserves the live conversations and their token/cost totals so :resume-all can rehydrate them with claude --resume. Any other exit — [d] quit-no-save, closing the terminal (SIGHUP), Ctrl-C, or a crash — records only the open repo locations: :resume-all then reopens those repos as fresh sessions (no history, in/out/cost reset to 0). Per-slot crash recovery during a running session still resumes the conversation.

| Command | What it does | | --- | --- | | :resume-all | Restart the slots that were open when mc last closed. After a save quit each is rehydrated via claude --resume (conversation + totals restored); after any non-save exit each reopens fresh in its repo. The toast reports resuming N · M fresh. Killed/closed slots are excluded. On boot, a toast surfaces this if records exist. | | :resume <slot> [slot ...] | Restore specific slots — e.g. :resume 1 3 5 or :resume 1,3,5. Bare :resume resumes (SIGCONT) the focused live session — the pair to :pause — or, when the focused slot is empty, restores that slot's saved record. | | :history [n] | View-only browse of the last N sessions for historical reference. Never bulk-restores (by design). | | :sessions (alias :ls) | List saved sessions (bySlot) for the current resumable set. | | :forget <slot> | Drop one slot's saved state. |

Configurable in Settings → GENERAL:

  • Auto-resume sessions on startup (autoResumeOnStart, default off) — when on, mc runs :resume-all implicitly at boot.
  • Session history limit (sessionHistoryLimit, default 20) — how many sessions the LITE history (:history) remembers.

Zoom (focused session)

↵ on a live card opens the Zoom view. Claude's own "update available" banner is lifted out of the body and shown as a discrete ⬆ update chip on the right of the zoom header so it doesn't encroach on the conversation — toggle with Hide claude update banner in zoom (hideClaudeUpdateBanner, default on) in Settings → LAYOUT. Keys available there:

| Keys | Action | | --- | --- | | / | Type a slash command — autocomplete dropdown appears above the composer. Tab fills the highlighted name (keeping any args you've typed); ↵ runs it. See below for the catalog. | | ⌥↵ · Ctrl+J | Newline in composer (plain ↵ submits) | | ↑ / ↓ | Recall prior submitted prompt (history nav in composer) | | Ctrl+F | Enter scroll mode — view scrollback without forwarding keys to claude (the embedded session owns the screen, so mc brackets a dedicated mode rather than fighting it for arrow keys) | | w / s (scroll mode) | Scroll one line back / forward through history | | f / b (scroll mode) | Scroll half a page up / down | | g / G (scroll mode) | Jump to the oldest / newest (live) line | | Esc (scroll mode) | Exit scroll mode (any other key also exits, returning input to claude) | | Ctrl+U | Expand / collapse the stats panel (defaults to a compact one-liner) | | Ctrl+K | Show / hide tool-call events in the log | | Ctrl+Q | Close the zoom view | | Esc, Ctrl+T, Ctrl+S, Shift+Tab | Forwarded to claude — its own cancel/back-out, todos, stash, and permission-mode cycle. mc no longer shadows these (chrome keys are Ctrl+Q/Y/K/U, all unused by claude). |

Session geometry is fixed, on purpose. Every session's claude runs at the zoom body size — computed once from your terminal (tui/lib/zoomGeometry.js) and applied at spawn — and only a real terminal resize changes it. Claude reprints its entire frame on every resize and the pre-resize copy stays in mc's scrollback, so a resize costs you one extra, differently-wrapped copy of the conversation (measured: 1 copy → 2 after widening → 3 after widening again). Zooming in, zooming out, a toast landing, and opening the stats or tasks panel therefore resize nothing; the zoom pane renders the bottom slice of the emulator instead, and Ctrl+F scroll reaches whatever the window skipped.

Slash commands (in zoom)

The zoom body is a real claude PTY, so slash commands typed there are claude's own (/compact, /model, /clear, …) and are handled by claude itself. mc no longer intercepts a client-side slash catalog in zoom — the mc-side verbs live in the :cmd command bar (below), available from the grid.

Shell overlay (!)

Press ! from the grid or a focused card to open a persistent $SHELL pane — useful for aws sso login, git, kubectl, and any other shell chore that would otherwise force you out to a separate terminal.

  • Open: ! (from FleetView or a focused card)
  • Close: Ctrl+Q — returns you to the grid. Every other key (including Ctrl+K, Ctrl+U, Ctrl+J) forwards straight to the shell.
  • Keep-warm: the shell process is long-lived. It survives close/reopen — your history, cwd, and any in-flight aws sso device-flow are preserved across toggles. The shell is killed only on app shutdown.
  • Focused-card cd: when you open the overlay from a focused card, the shell automatically cds into that card's working directory so git and aws operate on the right repo. Opening from FleetView (no focused card) leaves the shell wherever it last was.

The overlay chrome matches the Zoom modal: shell · <$SHELL> · <cwd> in the header, ⌃Q close · all other keys → shell in the footer.

Command bar

| Command | Effect | | --- | --- | | :theme <name> | Cycle palette (any substring match) | | :cols 3\|4\|5 | Change grid columns | | :perm <mode> | Set default permission mode (default, acceptEdits, bypassPermissions, plan) | | :model | Show the focused session's requested vs. resolved model | | :model <id> | Switch the focused session's model live (restarts the subprocess) | | :model default <id> | Set the fleet default model for new launches | | :model refresh | Programmatically probe the live model catalog — see Model catalog | | :kill [slot] | Kill focused (or specified) session | | :bg (alias :background) | List the sessions claude is running that are not in your fleet — id, age, state, project. These are created by claude when a conversation moves to the background, not by Mission Control, and they keep running until removed. X twice on a row deletes that conversation. The fleet row shows the count as bg N, flagging the oldest idle more than a day. | | :pause / :resume | SIGSTOP / SIGCONT the focused live session | | :approve (or :a) | Same as the A hotkey — only accepted while the session is waiting for input | | :resume <slot ...> | Rehydrate saved session(s) from disk via claude --resume (bare :resume with an empty focused slot restores that slot) | | :sessions | Show saved sessions (toast) | | :forget <slot> | Drop the saved session for a slot | | :repos | Open the folder picker to choose where repos are scanned. The chosen folder replaces the built-in defaults. :repos clear resets to defaults. | | :update | Report claude version drift: the on-disk claude --version vs the version each live session actually launched on (running processes keep their old binary until restarted). Drifted slots get a fleet-log line; converge by quit+relaunch. | | :whoami (or :auth) | Re-probe claude auth status and surface email + subscription | | :usage | Re-read plan-side rate-limit telemetry (5h and 7d quota %) | | :note <text> (or :n) | Inject a local annotation into the focused session's chat log (not sent to claude) | | :slack <url> | Set the Slack incoming-webhook URL. :slack clear removes it. | | :feedback <msg> | Send feedback to Slack (includes auth + fleet + plan-usage context) | | :request <msg> | Send customer request to Slack | | :quit | Exit |

Model catalog

The static table in tui/lib/models.js is only the verified pricing book — models are never hand-added to it. Each entry maps a friendly id (opus-4.8, sonnet-4.6, …) to the CLI model name passed to claude --model, plus display metadata (context window, per-MTok pricing, colour). Every selector (Settings → GENERAL, the NewSession ←/→ cycler, :model validation) reads the live catalog via modelIds(), so models discovered at runtime are immediately selectable. The default for new sessions is auto — the newest Opus in the live catalog, today opus-4.8 (1M-token context), so a newly released model becomes the default the moment it is discovered. Pin an explicit id in Settings → GENERAL or with :model default <id>.

Automatic discovery

mc syncs from two live sources on boot, in the background, so you rarely add a model by hand:

  1. Models API inventory (GET /v1/models — the endpoint the Anthropic SDK's client.models.list() wraps). Free, complete, and diffed against mc's catalog on every boot when an API credential is in the environment (ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN): unknown models are added, known models' real context/output limits are updated, and models the API no longer serves are marked retired (kept for cost history). Skipped silently on claude-CLI-only logins, where no env credential exists.
  2. claude CLI alias probe — resolves what opus / sonnet / haiku point at today (which the API can't say) and covers credential-less installs. Each probe is a billed turn, so it fires only when claude --version differs from the version stamped in models-cache.json. That's how Opus 5 appeared with zero code changes when v2.1.220 shipped. Failed probes don't stamp, so discovery retries next boot. A brand-new model family (its own alias) reaching credential-less installs needs a one-string addition to KNOWN_ALIASES in tui/lib/modelProbe.js — the API path needs nothing.

The known gap. A new model family is invisible to both sources when you sign in through the claude CLI subscription rather than an API key. Source 1 finds nothing without an env credential, and source 2 has no alias for the new family. Claude Fable 5.1 hit exactly this, so fable-5.1 is hand-added to tui/lib/models.js with limits from a live /v1/models call. Its pricing is inherited from Fable 5 and flagged estimatedPricing — treat the cost column for that model as an estimate. Remove the entry once discovery finds it.

Minimum claude version. A model can require a newer CLI than you have. claude 2.1.220 rejects Fable 5.1 with 400 … version 2.1.251 or newer is required, and mc surfaces that as a failed session, not as a refusal before launch. Upgrade the CLI the way you installed it — Homebrew users run brew upgrade --cask claude-code, not claude update.

The default model is auto: new sessions launch on the newest Opus in the live catalog, so a freshly discovered release becomes the default the moment it lands — no pinned id anywhere. Pin an explicit model in Settings (or :model default <id>) if you'd rather stay put.

Sandboxed runs (MC_CONFIG_DIR set — dev sandboxes, tests) skip boot discovery so throwaway configs never trigger billed probes. To make a sandbox behave like a real first-run install — e.g. when trying the published package as a user would — set MC_SYNC_MODELS=1:

MC_SYNC_MODELS=1 MC_CONFIG_DIR=$(mktemp -d) mc

Manual refresh (:model refresh)

The claude CLI has no "list models" command, so mc learns what an alias resolves to — and its real context window — by running a one-shot query and reading the modelUsage block of the JSON result:

claude -p --model opus --output-format json 'hi'
# → "modelUsage": { "claude-opus-4-8": { "contextWindow": 1000000, … } }

:model refresh runs that probe for opus / sonnet / haiku concurrently, then:

  • updates the context window of any catalog model whose CLI name matches (so per-card ctx% is computed against the true window), and
  • discovers any newly-shipped model an alias now points at, adding it to the catalog (pricing inherited from the same family and flagged as estimated until confirmed).

Each probe is a real, billed turn (~$0.10–0.15, ~2s), so probes run only on manual :model refresh or a detected claude version change (above) — never on a steady-state boot. The result is cached to ~/.config/claude-mc/models-cache.json (stamped with the CLI version) and overlaid onto the static catalog offline on every boot. A discovered model's pricing is inherited from its newest same-family sibling and flagged estimatedPricing until a verified row lands in the pricing book.

Slack feedback / customer requests

If you set an incoming-webhook URL via :slack <url>, then :feedback <message> and :request <message> will POST a structured payload to that channel — auto-tagged with your account email, current fleet state (live sessions, models, branches), and plan-side usage (5h / 7d %). The webhook URL is stored in ~/.config/claude-mc/settings.json and never displayed in the UI; Settings → FEEDBACK shows only whether one is configured.

Plan-side usage

The aggregate strip shows the same numbers Claude Code's /usage slash command reports — read from ~/.claude/abtop-rate-limits.json:

plan  5h 1% ↻4h 50m  ·  7d 29% ↻2d 19h

These are plan-side percentages (the actual Anthropic quota), not just what Mission Control has observed. The file is rewritten by every claude invocation on the machine; we re-read it every 8 seconds. The locally-tracked cost·week figure to the left of it is independent — it's the rolling $ spend Mission Control itself has booked.

Inside modals, Esc closes and Tab cycles fields.

Errors and confirmations (launch failures, broadcasts, command results) appear as transient toasts above the status bar.

Project layout

bin/
  mc.mjs              CLI entry — registers tsx JSX loader, boots tui/main.jsx
tui/
  main.jsx            Boot: constructs Fleet, renders <App/>, wires shutdown
  App.jsx             Top-level: hotkeys, focus, modal routing, fleet sub
  Header.jsx          Top status strip (+ connected subscription chips)
  Aggregate.jsx       Token/cost line (per-sub when multi-provider)
  Card.jsx            One agent tile
  FleetLog.jsx        Bottom pane — aggregated activity stream
  StatusBar.jsx       Vim-style status bar
  modals/
    Help.jsx          Keymap reference
    Broadcast.jsx     Send one prompt to N sessions
    NewSession.jsx    Single-input launcher: fuzzy-match recents or type a path
    Settings.jsx      btop-style settings (tabs: GENERAL / LAYOUT / COLORS / ALERTS / SAFETY / NOTES)
    Zoom.jsx          Single-session detail (full tail + ctx bar + msg input)
    RepoPicker.jsx    Filesystem browser to choose the repo scan folder (:repos)
  lib/
    themes.js         7 palettes (BlueArch / Tokyo Night / Gruvbox / Catppuccin / Solarized / Amber / Matrix)
    format.js         bar / sparkline / fmtK / fmtMoney / trunc / fmtClock
    models.js         Claude model metadata (label, maxCtx, kind, costs)
    settings.js       Schema + defaults + on-disk persistence
    TextField.jsx     Minimal single-line input (Ink has none built-in)
server/
  fleet.mjs           10-slot fleet manager + pub-sub (EventEmitter)
  agent.mjs           One claude subprocess wrapper (stream-json I/O)
  mockAgent.mjs       Fixture-driven Agent stand-in for UX iteration (MC_MOCK)
  fixtures/           JSONL fixtures replayed by MockAgent
  git.mjs             branch / dirty / ahead-behind via `git`
  repos.mjs           Recursive repo scanner for the New Session picker

Mock mode

For iterating on Zoom UX without burning real claude sessions, set MC_MOCK=<fixture> before launching. Every session opened in the TUI will then replay the named fixture from server/fixtures/ instead of spawning a real subprocess. Available fixtures:

| Fixture | Exercises | | ------------------ | --------- | | quick-reply | text-only assistant turn | | tool-loop | assistant → tool_use → tool_result → assistant | | long-thinking | extended-thinking block + streamed answer | | approval-request | tool approval banner; pauses at waiting, resumes when you reply (a/r in zoom) |

Example:

MC_MOCK=approval-request npm start

Drop a new fixture by adding server/fixtures/<name>.jsonl. The directive schema is documented at the top of server/mockAgent.mjs.

server/ is the data layer — it has no Express anymore. The TUI reads fleet.snapshot() directly and subscribes to fleet.on('change', …).

Permission mode

Sessions default to acceptEdits — claude can read and edit files in the session cwd without prompting, but still blocks unsafe bash. Valid values are default, acceptEdits, bypassPermissions, plan. Configurable in the Settings menu (Esc) under GENERAL → Default permission mode, or via the command bar with :perm <mode>. The mode is swappable mid-session (which is why it isn't part of the launcher anymore). bypassPermissions removes all guardrails — only use it in trusted, scratch directories.

Resume previous sessions

Each running session is mirrored to ~/.config/claude-mc/sessions.json along with the claude session UUID. To pick up where you left off:

  • From the command bar — :resume <slot> rehydrates in place.
  • List what's saved — :sessions. Drop a record with :forget <slot>.

Under the hood we call claude --resume <session-id> against the same working directory. claude restores the prior transcript from its own on-disk session store.

Quality-of-life features

Built on top of the reliability layer below.

Fleet dashboard (D or :dash)

A single-screen table — one row per live agent — for triage when you have more than a handful of slots running. Columns: slot, name, model, status pill, ctx %, tok/min, $ session, age, activity. Sortable:

  • S cycles the sort column (slot → status → ctx → tpm → cost → age)
  • R reverses direction
  • ↑ ↓ moves the highlight (the focused slot tracks)
  • ↵ zooms the highlighted slot
  • D or esc closes

The header also surfaces today's fleet spend and the configured budget when one is set — so "how much have I spent today?" is one keystroke from any view.

Cost guardrails — :cap and :budget

Two tiers of cost protection, both off by default (set the values to opt in):

  • Per-slot cap — :cap <slot> <usd> rejects further user messages to that slot once its costSession crosses the cap. :cap default <usd> persists a fleet-wide default applied to every new launch. Raise the cap with the same command to continue (:cap 3 10 bumps slot 3 to $10).
  • Daily budget — :budget <usd> blocks NEW launches once today's fleet-wide spend exceeds the cap. Existing sessions keep running. Use :budget 0 to disable. :budget alone shows today's spend.

Both also have form-editable rows under the SAFETY section of the settings menu (, or esc).

Session templates — :template <name>

Pre-configured bundles of N session launches with model / permission / prompt baked in. Bundled defaults at first launch (auto-written to ~/.config/claude-mc/templates.json):

  • review — 3 sessions (Opus architecture + 2 Sonnets) reviewing the same repo in plan mode.
  • explore — 2-session parallel exploration: Opus deep + Sonnet fast.
  • spec-then-implement — 2 sessions: Opus writes a spec in plan mode; Sonnet implements in acceptEdits.

Usage:

:template                       # list available templates
:template review                # launch into next N empty slots, using
                                # focused agent's cwd (or process.cwd)
:template review ~/my-other-repo

Edit ~/.config/claude-mc/templates.json to add your own.

@file mention autocomplete (in Zoom composer)

Type @ followed by a partial filename to summon a dropdown of files and folders under the session's working directory:

  • ↑ ↓ navigates
  • Tab or ↵ accepts (folders get a trailing / so you can keep descending — same UX as the path autocomplete in New Session)
  • Suppressed while the slash dropdown owns the input — the prefixes (@ vs /) are distinct so they never need to coexist

Useful for "look at @src/auth/oauth.ts" without copy-pasting from another terminal.

Reliability features

These guard against the failure modes that hit hardest at fleet scale.

  • Quiet at idle (battery-friendly). An idle fleet coalesces all of its background polling into a single shared wakeup (stretching to 3s when nothing is working), gates the invisible command-bar caret, and detects transcript rotation with one directory stat instead of a per-file sweep. Measured: idle CPU cut ~3× (1.07% → 0.35% of one core, zero agents); reproduce with node scripts/measure-idle.mjs. Renders, statuses, and keystroke latency are unchanged — active sessions keep full cadence.

  • Status truth, regression-proofed. A card blocked on a question (AskUserQuestion / plan approval) reads INPUT for the entire wait — the hook channel goes silent while an ask is pending, and the pending prompt now outranks the last "tool running" signal however stale it gets. Every real status incident is checked in as a recording under tests/fixtures/status-corpus/ and replayed through the live pipeline (tests/status.replay.test.mjs) with virtualized time, so a fixed lie stays fixed.

  • Press-K-twice to kill. A first press arms the kill action for 3 seconds and shows a warning toast; the second K confirms and SIGTERMs the subprocess. Eliminates the most painful misfire (accidental loss of work). The :kill command bar entry follows the same arm-then-confirm flow; :kill! (bang) bypasses for explicit batch use.

  • Auto-restart on transient errors. When a claude subprocess exits unprompted with a non-zero code, the slot retries up to 3 times with widening backoff (2s → 5s → 15s), restarting via --resume <session-id> so the in-progress conversation is preserved. The retry counter resets on the next successful init event, so a recovered slot is eligible again on its next independent failure. After 3 failed restarts the slot enters the errored state with K clears slot hint.

  • Quit can't stall. q → save SIGTERMs every child, gives them a 1.5s grace, SIGKILLs any straggler, and exits — a claude wedged on a permission prompt can no longer hold mc's shutdown hostage (previously the quit hung until a force-close, which orphaned that child into claude's daemon).

  • Background-agent claim detection. If a session can't resume because claude's daemon holds it as a background agent (the usual leftover of a force-closed terminal), the card says exactly that — with the fix (claude agents → stop the orphan, then :resume the slot) — instead of silently burning restart attempts on a failure that can't succeed.

  • Stuck-detection. When a slot is in working or waiting status but hasn't emitted any event from the subprocess in the last 5 minutes, the card shows a red STUCK Nm chip and a one-shot toast fires (slot N · stuck 5m · no events while working). Re-arms automatically when the slot starts emitting again.

  • Context-pressure toasts. Crossing 80% / 90% of the model's max context fires a yellow / red toast (slot N · context 80% · consider /compact). Each crossing is fired exactly once; the trigger re-arms when the slot drops back under the threshold (post-compaction).

  • Title-row context chip. When a slot's context passes warnPct of the ctxThreshold setting (defaults: 85% of 150k tokens), the status pill on the card title gets a · 89% chip in the matching urgency color — yellow near the threshold, red past it. Both values are in Settings → GENERAL. Pairs with the toast but stays visible at-a-glance.

  • Per-session status stream on disk. Every lifecycle event the status hook emits for a session is appended as NDJSON to:

    $XDG_STATE_HOME/claude-mc/status/<sessionId>.ndjson

    (defaults to ~/.local/state/claude-mc/status/). It survives process restarts, slot reassignments and reboots, and it is what to grep when a card showed something you did not expect. The conversation itself is not copied here — claude keeps that in its own session JSONL under ~/.claude/projects/, and :transcript prints the path to it for the focused slot.

    The legacy FLEET_USE_PTY=0 path additionally writes a combined inbound/outbound transcript to .../claude-mc/sessions/<sessionId>.jsonl, disabled with MC_NO_TRANSCRIPT=1. The default PTY path does not write it.

Known caveats

  • Single user. No multi-user sessions, no per-user repos.
  • Force-killing mc orphans its children. claude's daemon adopts an orphaned pane as a background agent, and that session then refuses --resume ("currently running as a background agent") until the orphan is stopped via claude agents. mc's card surfaces this state with the remediation; prefer q → save (which now cannot stall) over killing the terminal.
  • Pause via SIGSTOP freezes the process but does not cancel in-flight API requests. The claude subprocess will receive their results when SIGCONT'd.
  • Permission prompts in default mode. The stream-json wire format does not expose a structured permission event. The A hotkey sends a generic "yes, continue" message that unblocks most prompts, but for tight control prefer acceptEdits or bypassPermissions.

State persisted across runs

  • Settings (theme, layout, default model + permission mode, thresholds) → ~/.config/claude-mc/settings.json.
  • Weekly cost — every cost delta the fleet reports is folded into an ISO-week bucket in ~/.config/claude-mc/costs-week.json. The week rotates automatically every Monday 00:00 UTC.
  • Saved sessions — per-slot snapshot of cwd / branch / model / session-id in ~/.config/claude-mc/sessions.json so the next launch can --resume.

Smoke test

npm install
npm start

On first launch the grid is empty — you'll see the header, the aggregate strip, and a "no sessions running" hint. Press n (or Ctrl+N) to open the New Session picker. As you launch sessions, cards appear and autosize to fill the row; the grid wraps to multiple rows once you pass settings.gridCols columns. The bottom fleet log streams events from every live session.

New Session modal

One path input. As you type, the dropdown below it blends two sources:

  • recents — the auto-discovered repo list, filtered by case-insensitive substring match on repo name or tildified path. Configure the scan roots with :repos.
  • filesystem completions — when your query starts with / or ~ (or contains a /), child directories of the deepest existing parent are appended. Hidden dirs, node_modules, dist, and build are skipped.

Hotkeys:

| Key | Action | | --- | ------ | | ↑ / ↓ | move the highlight through suggestions | | ↵ | launch the highlighted suggestion — or, if nothing is highlighted, the typed path as-is (must already exist) | | ← / → | cycle the model | | Ctrl+B | open the filesystem browser (familiar cd/ls-style nav); ↵ inside the picker launches the highlighted folder | | esc | cancel |

Inside the filesystem browser (Ctrl+B): ↑/↓ or k/j move the highlight, →/l descends into the highlighted folder, ←/h goes up a level, . picks the folder you're currently in, ↵ launches the highlighted folder immediately. esc returns to the path input.

Intentionally absent: mode toggle, create-new (mkdir + git init), resume banner, branch input, permission picker, initial prompt. The launcher does one thing: pick a repo and launch. Branch follows the repo's default; permission mode and prompt are post-launch concerns and changeable from inside the running session.

Without claude on $PATH the New Session launch will still succeed at the fleet level, but the spawned process will exit immediately — you'll see the error in the per-session tail.

Tests

npm test           # run the suite once
npm run test:watch # rerun on file change

npm test runs scripts/run-tests.mjs, which executes every tests/**/*.test.* in its own process. In headless CI (CI=true) it skips the real-terminal suites — the node-pty recipes under tests/recipes/ and the *.realparser.test.* ink-keypress-parser tests — because the GitHub runner has no usable TTY/PTY (node-pty emits no output; ink mis-parses control bytes). Those run normally for you locally and on every push (the pre-push hook runs npm test with CI unset). Set MC_RUN_PTY=1 to force them on anywhere.

The suite uses Node's built-in node:test runner plus ink-testing-library for component rendering. Coverage includes:

  • tests/detectPrompt.test.mjs — structured-prompt classification (numbered, checkbox, lettered Option A/B/C, binary fallback, priority rules, the screenshot regression).
  • tests/Zoom.chips.test.jsx — chip render + keystroke dispatch (letter and digit keys both work, wire format includes the original marker, multi-select seeds from pre-checked defaults).
  • tests/RepoPicker.test.jsx — filesystem browser lists real subdirs (filtering dotfiles / node_modules / files), ↵ picks a child, . picks the current folder, esc cancels.
  • tests/NewSession.test.jsx — single-input launcher: substring filtering, ↵ launches the highlighted suggestion, ↵ on a typed real path launches it, ←/→ cycles the model, esc closes, nonexistent paths show an error and don't launch.
  • tests/TextField.test.jsx — plain ↵ submits; ⌥↵ (ESC+CR) and Ctrl+J insert a newline; the meta/shift Return paths are checked before the plain-return submit branch so macOS users don't accidentally submit half-typed messages.
  • tests/Zoom.input.test.jsx — stats panel defaults to compact + Ctrl+S toggles; PgUp pins the log and shows the "↓ N below" indicator; Ctrl+G snaps back to live; ↑/↓ walk through composer history.
  • tests/agent.costCap.test.mjs — a per-slot cap refuses the send that would cross it, in both agent classes, and a respawn cannot tunnel past it.
  • tests/costStore.twoInstances.test.mjs — two running copies of mc cannot corrupt each other's spend ledger.
  • tests/status.replay.test.mjs — every status misreport ever filed is checked in as a recording and replayed through the live pipeline, so a status bug that is fixed stays fixed.
  • tests/recipes/zoom.recipes.test.jsx, tests/recipes/newsession.recipes.test.jsx, tests/recipes/repopicker.recipes.test.jsx, tests/recipes/pty.recipes.test.jsx — declarative recipe coverage for each surface (see the section below).

Recipe-based QA runner

For new feature coverage, prefer the declarative recipe runner in tests/lib/recipe.js over hand-writing render / stdin / assert boilerplate. A recipe is a JSON-shaped list of steps that drive a rendered Ink component and assert on the frame at each step. Each step can type characters, press a key, tick for timers, assert on expectFrame / expectNotFrame substrings, or check expectCallback arg arrays. On failure the runner throws with the failing step's label, the assertion, and the last frame — so you can diff actual UI against intent.

Example, from tests/recipes/zoom.recipes.test.jsx:

import { runRecipe } from '../lib/recipe.js';
import Zoom from '../../tui/modals/Zoom.jsx';
import { theme, makeAgent } from '../lib/fixtures.js';

test('recipe: Ctrl+S expands the stats panel', () => runRecipe({
  component: Zoom,
  props: { agent: makeAgent(), theme, threshold: 150000, weekCost: 0,
           onSendMessage: () => {}, onSlashCommand: () => {},
           onClose: () => {}, onCyclePerm: () => {} },
  steps: [
    { expectFrame: [/ctrl\+s expand/], expectNotFrame: [/USAGE · SESSION/] },
    { press: '\x13' },                  // Ctrl+S
    { expectFrame: [/CONTEXT/, /USAGE · SESSION/] },
  ],
}));

Common fixtures (makeAgent, chatTail, the active theme) live in tests/lib/fixtures.js so recipes don't have to redefine boilerplate. The intent: adding "press X, expect Y" coverage should feel like writing a JSON document, not a React component.

  • tests/lib/recipe.js — in-process runner (Ink + ink-testing-library).
  • tests/lib/fixtures.js — agent / tail / theme helpers.
  • tests/recipes/*.recipes.test.jsx — one file per UI surface.
  • tests/Card.tier.test.jsx — Card tail filters tier-2 entries by default and shows a hidden-count hint.

Full-PTY recipe backend

The in-process runner is fast but renders Ink in a fake stdout — it can't catch terminal-specific bugs (e.g. the macOS Option+Return split-read where ESC and CR arrive in two reads instead of one). The runRecipePty backend in tests/lib/recipe-pty.js spawns a real subprocess inside a pseudo-terminal (node-pty) and pipes its output through a headless xterm.js Terminal so escape sequences are processed exactly the way iTerm/Terminal.app would process them. Same step DSL (type / press / tick / expectFrame / expectNotFrame), different backend.

import { runRecipePty } from '../lib/recipe-pty.js';

test('counter app: + increments', () => runRecipePty({
  command: process.execPath,
  args: ['tests/lib/pty-fixtures/counter-app.mjs'],
  bootDelayMs: 600,
  steps: [
    { expectFrame: [/counter:/, /\b0\b/] },
    { type: '++' },
    { tick: 80 },
    { expectFrame: [/counter:[^\n]*\b2\b/] },
    { press: 'q', expectExit: 1500 },
  ],
}));

Use the in-process runner for fast component tests and the PTY runner for "does this work when wrapped by a real terminal." See tests/recipes/pty.recipes.test.jsx for working examples.

node-pty ships a prebuilt spawn-helper whose executable bit is sometimes dropped by npm install. A postinstall script (scripts/fix-node-pty.mjs) restores it automatically — if you see posix_spawnp failed after a fresh install, run npm install again or re-run the script manually.

  • tests/humanize.test.mjs — ANSI strip, path collapse, UUID shorten, JSON collapse, idempotency.
  • tests/MockAgent.replay.test.mjs — every shipped fixture replays cleanly; approval fixture pauses at waiting and resumes on send().

When iterating on UI without API spend, prefer:

MC_MOCK=approval-request npm start

…to exercise the structured-prompt + APPROVE? marker flow against the canned fixture instead of a real claude session.


About BlueArch

Mission Control is designed and built by BlueArch, a cloud governance and efficiency company.

Our work is about the same problem in two places. In the cloud it is idle capacity, oversized instances and spend nobody attributed. In AI development it is agents left running in a closed terminal, fan-out whose cost lands on no session, and a bill that arrives a month after the decision that caused it.

Mission Control answers that for Claude Code: measure the spend per session while it happens, attribute the sub-agents back to the run that spawned them, surface the sessions you forgot were open, and let you set a figure the fleet is not allowed to pass. Governance you can act on at the moment it matters, not a report afterwards.