@microboxlabs/miot-chat
v0.1.0
Published
A Copilot-style agentic chat CLI for the `miot-harness` SSE streaming API. Lives at `turbo-repo/packages/miot-chat`.
Readme
@microboxlabs/miot-chat
A Copilot-style agentic chat CLI for the miot-harness SSE streaming API. Lives at turbo-repo/packages/miot-chat.
Also reachable as
miot chatfrom@microboxlabs/miot-cli— the standalone bin and themiotsubcommand share the samerunMiotChat()library entry, so flag names, env vars, config files, and behavior are identical between the two.
Install
npm install -g @microboxlabs/miot-chat
miot-chat --help
# or run without installing
npx @microboxlabs/miot-chat --helpIf you already have @microboxlabs/miot-cli installed, miot chat works without a second install — the standalone bin and the miot subcommand share the same library entry.
Inside the monorepo it's available as a workspace package. To use the built binary directly:
cd turbo-repo/packages/miot-chat
npm run build
node ./dist/cli.js --helpQuick start
# 1. Install
npm install -g @microboxlabs/miot-chat
# 2. Log in through the browser (platform sign-in); saves token + org to ~/.miot-chat/config.json
miot-chat login --base-url https://<platform-host>
# 3. Ask — runs route through the platform's harness proxy for your organization
miot-chat ask "what is the ETA status for today's deliveries?"<platform-host> is the deployment you were given access to (there are several production environments, so there is no baked-in default — --base-url or MIOT_CHAT_BASE_URL is required).
Log in
miot-chat login opens your platform's own sign-in page in the browser. After you sign in, the CLI receives a token plus your active organization and persists them as the default profile (platform) in ~/.miot-chat/config.json. Subsequent miot-chat ask / TUI sessions use that profile automatically and route through {baseUrl}/api/v1/orgs/{org}/harness.
miot-chat login --base-url https://<platform-host>| Flag | Effect |
|---|---|
| --login-url <url> | Override the platform CLI login handoff endpoint (default {baseUrl}/app/cli/auth/login) |
| --token-url <url> | Override the token endpoint (default {baseUrl}/app/api/cli/auth/token) |
| --auth-url <url>, --client-id <id>, --audience <a>, --scope <s> | Direct OAuth/PKCE mode against an authorization server instead of the platform handoff |
| --timeout <seconds> | Login timeout (positive integer) |
| --no-open | Print the login URL instead of opening the browser |
In split-origin local dev (app and API on different ports) pass the endpoints explicitly:
miot-chat login --base-url http://localhost:8180 \
--login-url http://localhost:3050/app/cli/auth/login \
--token-url http://localhost:3050/app/api/cli/auth/tokenIf you previously logged in with miot-cli, miot-chat reuses that session from ~/.miotrc.json as a fallback (baseUrl/token/org only) whenever no token is configured via flags, env, or the chat profile.
Other commands
The CLI talks to miot-harness over the Phase A SSE surface (POST /runs:start + GET /runs/{id}/stream) — directly via --base-url, or through the platform harness proxy when an org is set (--org, MIOT_CHAT_ORG, or the org saved by login).
# Interactive TUI (default in a real terminal)
miot-chat
# One-shot against a local harness (no login needed)
miot-chat ask "what's in stock?" --base-url http://localhost:8000 --tenant demo-tenant --mode canned
# Resume — TUI mode opens the saved-sessions picker; piped stdin re-seeds the headless REPL
miot-chat resume
# Replay a past run offline
miot-chat runs run_abc123
# Help
miot-chat --helpInteractive TUI vs headless
miot-chat mounts an Ink-based TUI when BOTH stdin AND stdout are TTYs and the MIOT_CHAT_NO_TUI env var is unset. Otherwise it falls back to a line-based REPL that reuses the existing renderer — handy for piped input (echo "ping" | miot-chat), CI smoke runs, and golden-output tests.
| Mode | When | What it gives you |
|---|---|---|
| TUI | TTY in + TTY out, no override | Multi-line editor with cursor / paste / history, persistent header bar, slash palette, modal stack (context, resume, theme, runs, approval), markdown rendering for final answers, theme support |
| Headless | piped stdin OR redirected stdout OR MIOT_CHAT_NO_TUI=1 | Line-based prompt-per-newline, the legacy ANSI renderer, one-line status updates |
Force headless explicitly:
MIOT_CHAT_NO_TUI=1 miot-chat --tenant demo-tenantTUI features
- Header bar: tenant · user · conv (short id) · mode · baseUrl · profile · pending-approvals count. Warns in yellow on
mode=agentic+ non-mintraltenant. - Multi-line input editor with bracketed-paste support, cursor movement (arrows, ctrl-arrow word jumps, home/end), backspace + forward-delete, kill-line, and an in-memory history ring (200 entries, file-backed at
~/.miot-chat/history). Up/Down arrows recall history when the buffer is empty;Alt-Enteradds a newline; plainEntersubmits. - Live transcript with structured per-event items: tool start/complete collapse to one line with a spinner, freshness warnings show inline, routes/agent turns/plans dim in. Completed turns flush into Ink's
<Static>so they live in terminal scrollback. - Slash-command palette: type
/to filter commands by substring, Tab completes the unique match, Enter dispatches. - Modals:
/context,/resume,/theme,/runs, and (behindMIOT_CHAT_APPROVALS_UI=1)/approve. Esc dismisses. - Themes:
dark(default),light,high-contrastbuiltins, or your own token overrides via~/.miot-chat/config.json(see Configuration below)./themeopens a picker;/theme <name>jumps to it. - Markdown rendering for final assistant answers — headings, bold/italic, fenced code blocks, lists, and links render as Ink components.
Slash commands
All slash commands work in the TUI palette. The headless REPL supports the legacy subset (/exit, /reset, /mode, /tenant, /save) for backwards compatibility.
| Command | Where | Effect |
|---|---|---|
| /help | both | List every registered command |
| /exit (or Ctrl-D) | both | Persist the session and exit |
| /clear | both | Clear the on-screen transcript (conversation id kept) |
| /reset | both | Mint a fresh conversation_id |
| /mode auto\|canned\|meta\|agentic | both | Change dispatch mode |
| /tenant <id> | both | Change tenant |
| /user <id> | TUI | Change user id |
| /save <path> | both | Dump {conversation_id, transcript} as JSON |
| /export <path> | TUI | Write the transcript as markdown |
| /context | TUI | Open a modal with the full session metadata |
| /whoami | TUI | Print user=… tenant=… conv=… |
| /theme [name] | TUI | Pick a color theme (modal); /theme dark jumps directly |
| /resume | TUI | Pick a saved session from ~/.miot-chat/sessions/ |
| /runs | TUI | Pick a run from the current session to replay |
| /approve <approve\|deny\|later> <id> | TUI | Resolve a pending approval from the keyboard |
Ctrl-C aborts the in-flight harness run and keeps the editor alive.
Configuration
~/.miot-chat/config.json (file mode 0600):
{
"defaultProfile": "local",
"profiles": {
"local": { "baseUrl": "http://localhost:8000", "token": null, "tenantId": "demo-tenant", "userId": "demo-user" },
"staging": { "baseUrl": "https://...", "token": null, "tenantId": "mintral", "userId": "ops" }
},
"theme": "dark"
}The theme field is optional. Valid values:
- A builtin name:
"dark","light", or"high-contrast". - An object:
{ "name": "dark", "tokens": { "accent": "#ff0080", "prompt": "magenta" } }. Token keys:accent,assistant,user,dim,warn,err,ok,border,prompt,spinner. Values are anything Ink's<Text color>accepts (named or hex). Invalid theme names degrade todarkwith a one-line warning rendered above the transcript.
Resolution precedence: CLI flag > env (MIOT_CHAT_*) > profile > defaults.
Env vars:
| Var | Effect |
|---|---|
| MIOT_CHAT_BASE_URL | Override baseUrl |
| MIOT_CHAT_TOKEN | Override bearer token |
| MIOT_CHAT_TENANT_ID | Override tenant |
| MIOT_CHAT_USER_ID | Override user |
| MIOT_CHAT_MODE | Override dispatch mode |
| MIOT_CHAT_ORG | Override the organization slug (routes runs through the platform harness proxy) |
| MIOT_CHAT_PROFILE | Pick a profile from the config file |
| MIOT_CHAT_DEBUG | 1 streams full tool inputs and truncated outputs (tenant must be allow-listed) |
| MIOT_CHAT_NO_TUI | 1 forces headless mode even in a TTY |
| MIOT_CHAT_APPROVALS_UI | 1 enables the approval.requested modal (reply transport not yet wired) |
| NO_COLOR | Disable ANSI output in the headless renderer |
Conversation memory
Each session mints a fresh conversation_id (UUIDv4) and sends it on every turn. Phase 13's ConversationStore on the harness rehydrates prior turns server-side, so the CLI carries no per-turn state.
- The TUI auto-persists each committed turn to
~/.miot-chat/sessions/<conv-id>.json(mode 0600, atomic write)./resumelists the newest 10 and re-seeds the reducer viaLOAD_SESSION. The 500-most-recent items are kept; older entries are summarized into a single(elided N earlier items)system row. - The headless REPL keeps using
~/.miot-chat/last-conversation(single id) for the legacymiot-chat resumeflow.
Develop
npm run build # tsup
npm test # vitest
npm run lint
npm run check-typesDesign notes
- Runtime deps:
commanderfor arg parsing,ink@^7+react@^19.2for the TUI,marked@^14for the lexer used by/exportand the assistant-turn renderer, and@microboxlabs/miot-harness-clientfor the HTTP + SSE work.readline,crypto.randomUUID,fetch, andReadableStreamare native on Node ≥20. - HTTP + SSE live in the sibling library — this package owns the interactive UX layer. The same library powers the non-streaming
miot harness create/miot harness runs getsubcommands inmiot-cli. - Architecture (TUI): a pure session reducer in
src/tui/session/reducer.tsownsmeta,transcript,pendingApprovals, andcurrentRunId.STREAM_EVENTdelegates tosrc/tui/transcript/project.ts, which mirrors the field-precedence rules of the legacyrenderer.ts:statusForbut produces typedTranscriptItems. Components subscribe viauseSessionand render via Ink. The same projector is reused by/exportmarkdown serialization and (eventually) the/runsreplay panel. - Architecture (headless): unchanged from earlier phases —
src/repl/loop.tsreads lines, callsclient.runs.create+client.runs.stream, and pipes events through the original(state, event) → {state, output}renderer. Marked deprecated in the source; future work re-implements headless mode on top ofuseSession. - The library's SSE parser surfaces the harness
event: errorframe as a thrownMiotHarnessApiError. Both code paths catch and render it. - Terminal signal is
run.completed/run.failed, notanswer.completed(which the supervisor can emit more than once when a mode is denied and a fallback runs). The projector handles the dual-emit case by upserting the in-flight assistant item. approval.requestedevents arrive from the harness, but no reply endpoint exists yet. The modal ships behindMIOT_CHAT_APPROVALS_UI=1and its resolve callback only records the decision locally — wiring will land when the harness exposes the reply transport.
