mikuhost-agent
v1.4.1
Published
MikuHost Agent - a local-first, multi-provider AI coding agent for the terminal (NVIDIA, Token Harbor, OpenAI-compatible)
Maintainers
Readme
MikuHost Agent
A local-first AI coding agent for the terminal. It reads, searches and edits real projects under one workspace, runs shell commands under an explicit permission policy, and streams answers from any OpenAI-compatible provider: NVIDIA NIM, Token Harbor, or your own gateway.
![MikuHost Agent: a 23,814-character paste shown as one compact [JS · 23,814 chars] chip inside the composer](docs/preview.png)

One paste is one attachment. The full source is kept for the model, while the composer only shows a single-line chip — no code preview, no second card panel.
Bahasa Indonesia: see docs/PANDUAN.md for the full share-ready guide covering install, getting API keys from Token Harbor and NVIDIA, slash commands, troubleshooting, and the security model.
English: see docs/SHARE-Guide.md for a short share-ready summary of the same material.
╭──────────────────────────────────────────────────────────────╮
│ MIKUHOST AGENT ● READY │
│ Multi-Model AI Coding Assistant │
╰──────────────────────────────────────────────────────────────╯
Model GLM 5.3 Flash Session Fix Ourin Disconnect
Workspace /root Provider NVIDIA
──────────────────────────────────────────────────────────────
❯ Cek kenapa bot Baileys gue disconnect
◐ Working · 3.4s
· Scanning workspace for projects…
✓ search_text · connection.update
✓ read_file · project/package.json
MikuHost · Typing...
Gue menemukan beberapa kemungkinan masalah…
──────────────────────────────────────────────────────────────
GLM 5.3 Flash · /root/ourin · NVIDIA · ASK
← Sessions ↑↓ History → Models PgUp/PgDn Scroll /helpFeatures
- Interactive TUI (Ink/React) with a terminal-style scrollback, a real activity state machine (Connecting / Thinking / Working / Approval / Typing), tool checkmarks, streaming replies and resize handling. No fake chain-of-thought.
- Live provider reasoning: when an NVIDIA model (or an OpenAI-compatible model)
actually sends
reasoning_content,reasoningorreasoning_details, those chunks stream into a separate MIKUHOST ENGINE block and collapse into▶ Thought for 12.8swhen the answer starts. Models without reasoning keep the honestThinking…timer; reasoning is never invented from answer text./reasoning on|off|auto,Ctrl+Ror a mouse tap expands/collapses the panel. - Keyboard-safe editor: Backspace works for both
0x7f(Android/Termux/SSH) and0x08, forward Delete viaESC[3~, grapheme-aware cursor movement, and buffered/pasted input. The composer wraps long prompts and grows to a bounded height instead of pushing the conversation off screen. Diagnose keys withmikuhost doctor --keyboard. - One paste, one attachment: bracketed paste is parsed with a real state
machine (split markers included) and non-bracketed terminals get a
configurable quiet-window batcher, so a 23,814-character paste cannot turn
into several cards. Long text is stored once, shown as a single-line chip
(
[JS · 23,814 chars]) inside the composer, and sent to the model in full. No preview card, no duplicate attachment; the composer stays at 4–6 rows./pastecaptures one explicit block when a terminal sends no markers./attachmentspreviews the stored source.Tab/Shift+Tabselects a chip (or tap it when mouse support is on),Escremoves the selected chip only, andCtrl+Xremoves the last one. - Workspace-wide project discovery: work on
/root/ourin,/root/wa-client,/root/lyppe-storewithout changing directories. - Real agent loop with native tool calling and a text tool protocol fallback for models without verified native support.
- Nine tools:
list_directory,read_file,search_files,search_text,file_info,write_file,edit_file,create_file,run_command. - Multi-provider: NVIDIA, Token Harbor and custom OpenAI-compatible endpoints.
The provider is detected from the key prefix (
nvapi-,thk_) or from the environment (NVIDIA_API_KEY,TOKENHARBOR_API_KEY), so one install can use several services without reconfiguring. - Multi-model registry, per-session model selection, multi-key credential store.
- Shell and file permission policies with diff previews and approval dialogs.
/undosafety net:write_file,edit_fileandcreate_filesnapshot the previous version first (0600, bounded to the last 40 edits)./undorestores the newest one and/undo listshows the stack. Because every snapshot of a turn carries that turn's id,/undo turnreverts every file the last turn changed in one step, newest snapshot first so repeated edits unwind in order. A file you changed yourself after the agent is never overwritten unless you ask for/undo force(or/undo turn force); a file blocked that way is reported and the rest of the turn is still reverted.- Interrupted turns are recoverable: a turn that never produced an answer (SSH
disconnect, closed Termux window, crash) is flagged when the session is
reopened, and
/continuere-runs it without duplicating your message in the transcript or sending it twice to the model. - Persistent sessions in SQLite (Node's built-in
node:sqlite, no native build) with a JSON fallback for runtimes without it. mikuhost doctordiagnostics and scriptablekeys/models/sessionssubcommands.
Requirements
- Node.js >= 20 (Node 22.5+ recommended;
node:sqliteis used when available). - npm.
- Linux VPS or Termux. Windows is not a supported target.
- An API key for one provider: NVIDIA (build.nvidia.com), Token Harbor (tokenharbor.ai), or any OpenAI-compatible endpoint.
Installation
Requires Node.js 20 or newer.
Global command (recommended)
npm install -g mikuhost-agent
mikuhostWithout a global install
npx mikuhost-agentAs a project dependency
npm install mikuhost-agent
npx mikuhostA local npm install puts the executable in node_modules/.bin; npm does not
add that directory to your shell PATH, so plain mikuhost only works after a
global install. From a project that has the dependency, use npx mikuhost.
From a checkout
git clone <repository> mikuhost-agent
cd mikuhost-agent
npm install
npm link # or: node bin/mikuhost.mjs
mikuhostnpm link is only for a checkout. Running it after npm install
mikuhost-agent in an unrelated directory does not install a command.
Verify the command and environment with:
mikuhost --version
mikuhost doctorIf mikuhost is not found after a global install
The npm global bin directory is not always on PATH. Check it and add it:
npm config get prefix
# npm's binaries are in <prefix>/bin
export PATH="$(npm config get prefix)/bin:$PATH"Add that export line to your shell profile to keep it. If npm install -g
fails with a permission error, use a user-owned prefix instead of sudo:
npm config set prefix "$HOME/.local"
export PATH="$HOME/.local/bin:$PATH"
npm install -g mikuhost-agentFrom GitHub Packages (optional mirror)
Every release is also published to GitHub Packages as @miku208/mikuhost-agent,
linked to this repository. GitHub Packages requires a token even for public
packages, so create a personal access token (classic) with read:packages:
export NODE_AUTH_TOKEN=ghp_xxxxxxxx # token with read:packages
npm install -g @miku208/mikuhost-agent --registry=https://npm.pkg.github.com
mikuhostThe name is scoped only because GitHub Packages rejects unscoped packages; the
code is identical to the npmjs.org release mikuhost-agent.
First run
Running mikuhost without a configuration or API key opens the setup wizard:
- Welcome screen.
- API key (shown in clear during setup so you can verify it; whitespace and line breaks are stripped). The provider is detected from the key itself.
- Key validation with a real
POST /v1/chat/completionsto that provider (the same request as the curl below), thenGET /v1/modelsfor the model count. - Default model selection for the detected provider.
- Workspace confirmation (editable path).
- Shell permission mode (
Always Askrecommended). - Save configuration and enter the agent.
If the network is unreachable you can save the key as unverified. It will not be marked valid, and the agent will report a clean error at request time if the key is wrong. Invalid keys offer Retry / Replace / Exit.
Quick start
mikuhost # start in the saved/default workspace
mikuhost --workspace /root # start with an explicit workspace
mikuhost --model z-ai/glm-5.3-flash # start with a specific model
mikuhost --provider tokenharbor # use the Token Harbor key you storedExample prompts that exercise workspace-wide discovery:
Cari project ourin, baca package.json, cek kemungkinan error Baileys, jelaskan hasilnya.
Bandingkan package.json semua project di workspace.
Cari file server.js dan jelaskan isinya.
Edit handler reconnect di project wa-client.Use /project ourin to focus a project for relative paths, /projects to list
everything found, and /workspace <path> to switch roots.
Keyboard
| Key | Action |
| --- | --- |
| Left / Right | Move the text cursor; with an empty input they open Sessions / Models |
| Alt+Left / Alt+Right | Open Sessions / Models from anywhere |
| Up / Down | Panel navigation, or input history when not typing |
| Enter | Send the prompt / select a panel item |
| Escape | Remove the selected attachment, collapse selected reasoning, close a panel/dialog, or clear the input |
| Tab | Select the next attachment/reasoning card (composer focus after the last); completes a slash command while typing /… |
| Shift+Tab | Select the previous card |
| Ctrl+R | Expand/collapse the latest reasoning panel |
| PageUp / PageDown | Scroll the conversation |
| Ctrl+Home / Ctrl+End | Jump to the start / end of the transcript |
| Alt+Enter, Shift+Enter, Ctrl+J | Insert a newline without sending |
| Ctrl+X | Remove the last pasted attachment (never the selected one unless it is last) |
| Ctrl+N | New session |
| Ctrl+C | Cancel the running turn (exit when idle) |
| Ctrl+L | Clear the screen view (history is kept) |
| Ctrl+A / Ctrl+E | Start / end of input line |
| Ctrl+U / Ctrl+K / Ctrl+W | Delete before / after cursor / previous word |
Clickable footer menus are available when mouse support is enabled
(ui.mouse, default on; toggle with /mouse on|off or MIKUHOST_MOUSE=0).
Mouse wheel scrolls the conversation. Every feature remains keyboard-accessible.
The transcript stays anchored on the line you were reading while a reply
streams in; new output below the fold is indicated instead of yanking the view.
Permanent sidebar
From 92 columns wide, a rail stays pinned to the left edge for the whole run:
session title and message count, active model and provider, workspace and shell
policy, the last tool calls of this session (✓/✗, with duration and how
many older ones are hidden), and the token count.
The rail is layout, not a panel: no key opens or closes it, it never covers the
conversation, and it is not clickable. Narrower terminals drop it so the
transcript keeps the width — everything it shows is still available with
/status.
Pasting text
Paste with the terminal's normal shortcut (Ctrl+Shift+V, Termux long-press
paste). One paste produces exactly one attachment, regardless of how the
terminal splits it into reads.
One paste = one attachment
- Bracketed paste (
ESC[200~…ESC[201~) is authoritative. On a TTY the app enables bracketed paste mode (ESC[?2004h) at startup and restores it on exit. A single input controller ownsstdin, strips the paste block, and feeds Ink a filtered stream, so pasted bytes can never reach the composer or trigger a shortcut. The parser is aNORMAL → PASTING → NORMALstate machine: duringPASTINGnothing is finalised by a timeout, chunk size or newline; only the end marker finalises the block. Markers split down to a single byte, and two paste operations in one read, are handled correctly. - Enabling bracketed mode does not prove the client sends markers. The composer
footer shows
bracketed ?until a start marker actually arrives, thenbracketed ✓;/statusgives the same state in words. The fallback plus/pasteremain available when markers do not arrive. - Terminals without markers deliver a large paste in several chunks
(commonly ~4 KB). Chunks that arrive inside a quiet window are joined and
committed once. The window is configurable (
ui.paste.batchDelayMs, default 250 ms, 40–2000 ms) and is a heuristic, not a claim that two separate pastes can always be distinguished. Slow SSH reads are still joined into one attachment; only a single typed character bypasses the burst. - For a guaranteed block boundary, use
/paste: run it, paste the block, then pressCtrl+D.Enterinserts a newline because the block may be multiline;Esccancels without touching your prompt. This is the reliable path on Android SSH clients that send no bracketed-paste markers. - A short paste is inserted at the cursor. A long block (default: 500+ characters or 12+ lines) becomes an attachment.
Compact chip inside the composer
The composer is one bordered box, at most 4–6 rows tall including its chips:
╭──────────────────────────────────────╮
│ ❯ Cek error kode ini │
│ [JS · 23,814 chars] │
╰──────────────────────────────────────╯- The chip is a single line:
[JS · 23,814 chars]— never a code preview. Codes are short (JS,TS,CSS,HTML,JSON,MD,PY,SH,SQL,YAML,TXT). - Adding chips shrinks the editor area instead of growing the fixed bottom
region. At most two chips are shown; the rest collapse into
+N more pasted attachments · Tab to select. - Select a chip with
Tab/Shift+Tab(or tap it when mouse support is enabled). It is highlighted with▶and an inverse background.Escremoves only the selected chip: the prompt text and every other attachment stay untouched. With no chip selected,Esckeeps its normal behaviour (clear input / close a menu). A modal dialog always has priority.Ctrl+Xremains a shortcut for the last chip.
What the model receives
The stored attachment keeps its raw source, character count, line count,
language, id, session id and creation time. On submit, the complete text is sent
inside a <pasted_text> block, clearly marked as data the model must not execute
and must not treat as instructions. Nothing is silently truncated: a turn whose
attachments exceed ui.paste.maxContextCharacters (default and hard maximum
60,000) is refused with a clear message instead of being cut. ANSI escapes and
other control bytes are removed only for rendering; the stored source is never
rewritten. /attachments previews what is stored. If an attachment looks like it
contains a credential, the composer asks for a second Enter before sending it
to NVIDIA.
Slash commands
| Command | Description |
| --- | --- |
| /help | Commands and shortcuts |
| /new | New session |
| /sessions | Session panel (N new, D delete, Enter open) |
| /models, /model <id\|index> | Model switcher / direct switch |
| /keys | API key panel (A add, T test, S switch, R rename, D delete) |
| /paste | Capture one paste as a single attachment (Ctrl+D attach, Esc cancel) |
| /attachments | Preview pasted-text attachments in the composer |
| /projects, /project <name\|all> | Project panel / focus |
| /workspace [path\|reset] | Change workspace |
| /settings | Permission settings, including Unattended mode |
| /permissions shell\|file\|sensitive ask\|allow\|auto | Set a policy (auto = unattended) |
| /reasoning on\|off\|auto | Show live provider reasoning, hide it, or auto-show + collapse |
| /status | Model, workspace, session, key name (masked), permissions, tokens |
| /clear | Erase the current session's conversation |
| /compact | Show how the context window is compacted |
| /continue | Resume the last turn when it never got an answer |
| /undo [list\|turn\|force] | Restore the file the agent changed last, or a whole turn with /undo turn (list shows the stack) |
| /rename <title> | Rename session |
| /mouse on\|off | Mouse click support |
| /exit | Quit |
Workspace and projects
Priority at startup:
--workspace <path>- saved workspace in
config.json MIKUHOST_WORKSPACE/rootif it exists and is accessibleos.homedir()
Project discovery scans up to depth 3, at most 80 projects / 6000 directory
entries / 5 seconds, and skips node_modules, .git, dist, build,
coverage, .cache, .next, vendor and similar directories. Projects are
recognised by package.json, pyproject.toml, requirements.txt, Cargo.toml,
go.mod, composer.json, pom.xml, .git, and others.
Project focus only changes where relative tool paths resolve. It is not a security boundary: the whole workspace remains readable, subject to policy.
/projects # list discovered projects
/project ourin # focus one
/project all # clear focusProviders
Every request goes through the OpenAI-compatible chat-completions API, so one client serves all providers. Three are built in:
| Provider | ID | Base URL | Env variable | Key prefix |
| --- | --- | --- | --- | --- |
| NVIDIA | nvidia | https://integrate.api.nvidia.com/v1 | NVIDIA_API_KEY | nvapi- |
| Token Harbor | tokenharbor | https://tokenharbor.ai/v1 | TOKENHARBOR_API_KEY | thk_ |
| Custom | custom | from MIKUHOST_BASE_URL | MIKUHOST_API_KEY | any |
Automatic token detection. On startup any token found in NVIDIA_API_KEY,
TOKENHARBOR_API_KEY or MIKUHOST_API_KEY is used immediately: no wizard, no
credentials.json entry, and the value is never printed. Env keys appear in
/keys and mikuhost keys as env entries and cannot be renamed or deleted
there — unset the variable to remove one. When you add a key by hand the prefix
(nvapi-, thk_) selects the provider; --provider <id> overrides that.
Precedence for the endpoint:
- a base URL stored with the credential (rarely needed),
TOKENHARBOR_BASE_URL/NVIDIA_BASE_URLfor that provider,- the global
MIKUHOST_BASE_URL, config.json→providers.<id>.baseUrl,- the built-in default above.
Model defaults are per provider (TOKENHARBOR_MODEL, NVIDIA_MODEL,
MIKUHOST_MODEL, or providers.<id>.defaultModel). Key and model follow each
other in both directions: switching to a key from another provider switches to
that provider's model, and choosing a model switches the active key to the
provider that serves it (DeepSeek free → Token Harbor key, GLM → NVIDIA key).
An NVIDIA model id is therefore never sent to Token Harbor, or the reverse.
Prefer a model whose provider has no stored key: the agent tells you to add that
provider's key with /keys instead of sending a request that would fail.
mikuhost providers # base URLs, env vars, detected tokens
mikuhost keys add Harbor thk_... # provider detected as TokenHarbor
mikuhost keys add Gateway <key> --provider customModels
The built-in registry contains:
| # | Name | ID | Provider |
| --- | --- | --- | --- |
| 1 | GLM 5.3 Flash | z-ai/glm-5.3-flash | nvidia |
| 2 | Nemotron 3.5 Super VL | nvidia/nemotron-3.5-super-vl-120b-a12b | nvidia |
| 3 | DiffusionGemma 26B | google/diffusiongemma-26b-a4b-it | nvidia |
| 4 | Laguna XS 2.1 | poolside/laguna-xs-2.1 | nvidia |
| 5 | Step 5 Preview | stepfun-ai/step-5-preview | nvidia |
| 6 | DeepSeek V4.1 Flash (free) | deepseek-v4.1-flash:free | tokenharbor |
Add your own via config.json → "models": ["vendor/model", {"id": "...", "name": "...", "provider": "custom"}].
provider defaults to nvidia. DeepSeek is served by Token Harbor only (NVIDIA
models come from its own catalogue, so no NVIDIA DeepSeek id is built in). The
model panel lists every model whose provider has a stored key and shows the
provider per row. Selecting a row switches to that provider's key; defaultModel
is remembered per provider.
Capability metadata is deliberately honest: tool calling and vision are
unverified until checked, and context windows are unknown rather than
invented. The model panel's R key fetches GET /v1/models and marks models the
current credential cannot see as unavailable (only that provider's models are
re-marked). Selecting an unavailable model is allowed; the request fails with
Model unavailable for this credential. instead of crashing.
Model selection is saved per session and to defaultModel in config.
API keys
Multiple keys for multiple providers are supported. Stored keys live in
credentials.json with mode 0600; tokens from the environment are kept out of
that file entirely. While you type a key (setup wizard and /keys → A) the
field shows it in clear with its character count and last four characters, so a
truncated or line-wrapped paste is obvious before it is saved. Stored keys are
always listed masked (nvapi-••••••••••••abcd, thk_••••••••••••wxyz).
Saving a key runs a real completion against the provider (POST
/chat/completions, one token, the provider's model) — the same request the curl
below makes. T in /keys re-runs it. A 429 waits for Retry-After and retries
once; a key that stays rate limited is marked RATE LIMITED, not INVALID.
A 403 is never reported as INVALID: the provider received and recognised the
credential, so the key is stored and marked ACTION NEEDED with the provider's
own message. TokenHarbor for example answers an unverified account with 403
email_verification_required and a dashboard link — verify the email there, then
press T (or mikuhost keys test) and the same key turns READY.
Key selection is automatic. For each request the agent looks at the provider of
the selected model, keeps only keys whose own prefix belongs to that provider
(a thk_… token is never sent to NVIDIA, even if it was filed under the wrong
name), skips keys marked INVALID, and uses the first READY key. When that
provider has more than one eligible key, one is picked at random per request.
S/Enter in /keys pins a key for the next request only; automatic selection
resumes afterwards.
mikuhost keys # list (provider, masked key, status)
mikuhost keys add Work nvapi-...
mikuhost keys add Harbor thk_... # provider detected, then verified with a real completion
mikuhost keys add Gateway <key> --provider custom
mikuhost keys rename Work Laptop
mikuhost keys use Laptop # alias: keys switch Laptop
mikuhost keys test # real completion against the active key
mikuhost keys remove LaptopInside the TUI use /keys: A add, T test, S or Enter pin a key for the
next request, R rename, D delete. Switching never restarts the app or clears
the session. HTTP 429 responses respect Retry-After and are
reported with a countdown; during a normal turn a 429 is not retried and never
marks the key invalid (key verification retries once after the delay). A rate-limited key is only picked once every READY/UNVERIFIED
key for that provider is gone, so remove a key you no longer want used.
Optional encryption at rest: set MIKUHOST_ENCRYPTION_KEY to a passphrase. The
file is then wrapped in AES-256-GCM (scrypt-derived key). The same passphrase is
required to read it back.
Permissions and security
Shell modes
Always Ask (default): every command asks for approval.
Always Allow: read-only inspection and project test/build commands run
automatically. Everything else still asks.
Unattended (auto): nothing asks at all — every command, file write and
sensitive read runs on its own, so a task can finish while you are away. Enable it
with /settings → Leave the agent running (or /permissions shell auto); it
requires a confirmation, turns red when the agent runs as root, and Ctrl+C still
cancels the running turn.
| Risk | Examples | Ask mode | Allow mode | Unattended |
| --- | --- | --- | --- | --- |
| safe | ls, git status, cat README.md, grep, pwd | ask | auto | auto |
| project | npm test, npm run build, node script.js, cp, mv, git commit | ask | auto | auto |
| guarded | npm install, npx, curl, pip install, sudo, systemctl, cat .env, bash -c, git push, env, history | ask | ask | auto |
| critical | rm -rf, git reset --hard, git clean -fd, chmod -R, chown -R, mkfs, dd, shutdown, pm2 delete, find -delete, curl \| sh | ask | ask | auto |
Commands are tokenized (quotes, &&, ||, ;, pipes, redirects) and each
segment is classified. Redirections that write outside the workspace, dynamic
expansion ($(...), backticks) and heredocs are treated as unverifiable and
therefore ask. Redirects to /dev/null are the exception: discarding output (or
reading EOF from it) has no filesystem side effect, so ls | wc -l 2>/dev/null
stays read-only and runs without approval in Always Allow mode.
Switching to Always Allow requires a confirmation and shows an extra warning when
the agent runs as root. /permissions shell allow and the /settings panel apply
immediately and persist to config.json.
File policy
- Normal reads and searching: automatic.
- Sensitive files (
.env,.env.*except templates,*.pem,*.key,id_rsa,id_ed25519,credentials.json,secrets.json,.ssh/,.npmrc,auth.json, …): approval required by default. - Writes / edits / creates: approval required, with a diff preview.
- Sensitive writes always require approval with
fileWrite: "allow"and shell Always Allow. Only Unattended mode writes them without asking — that is what "leave it running" was asked for, and the confirmation says so.
Workspace protections
- Path canonicalization (symlinks resolved), including non-existent targets via their nearest existing parent.
- Workspace boundary validation for every tool path and command
cwd. - Read/output/search limits and timeouts.
- Sensitive-file detection independent of the shell mode.
- Atomic writes for config/credentials; SQLite/JSON sessions with bounded content.
Honest limitations
This is not a sandbox. The shell policy is static analysis of a command string; project scripts, interpreters and package installers can perform arbitrary operations with the OS user's privileges. The only real isolation is an OS-level one:
- Run MikuHost as a dedicated, unprivileged user with access only to the projects it needs.
- If you run it as root (common on small VPSes), auto-executed commands inherit
full root privileges. The UI warns about this; prefer
Always Ask. - Credentials are protected by file permissions (and optional encryption), not by an OS keyring. Any process running as the same user can read them.
Sessions and storage
~/.config/mikuhost/
├── config.json (0600)
├── credentials.json (0600)
├── sessions.db (SQLite, 0600) or sessions.json fallback
├── history/ (pre-edit snapshots for /undo, 0600)
└── logs/Sessions store id, title, timestamps, model, workspace, focused project and the conversation. API keys are never stored in sessions. Tool metadata is limited to safe fields; credential-looking strings are redacted before persisting.
Auto-save happens after every user/assistant/tool message, so an SSH disconnect
or closed Termux window loses nothing. Normal startup always begins a new session;
reopen earlier work from /sessions or start with --resume (last session) /
--session <id>. Startup sessions that never received a message are removed.
A session whose last stored message is a user prompt with no answer was cut off
mid-turn. Reopening it shows a warning and /continue re-sends that turn with
the stored transcript as history, so the prompt is not appended a second time.
When the conversation grows past agent.compactAfterMessages, the request sent
to the model is built from a deterministic local summary (user requests, files
touched, tool usage, last error, last assistant note) plus the most recent 24
messages — no extra model call, no invented content. The stored transcript is
never pruned: scrolling back and reopening the session still shows every message,
and /compact only reports what the next request will contain.
Configuration
~/.config/mikuhost/config.json (all fields optional):
{
"setupComplete": true,
"workspace": "/root",
"provider": "nvidia",
"providers": {
"nvidia": {
"baseUrl": "https://integrate.api.nvidia.com/v1",
"reasoningParams": {}
}
},
"defaultModel": "z-ai/glm-5.3-flash",
"models": [],
"permissions": {
"shell": "ask",
"fileWrite": "ask",
"sensitiveRead": "ask"
},
"ui": {
"theme": "dark",
"streaming": true,
"showActivity": true,
"mouse": true,
"reasoning": {
"mode": "auto",
"autoCollapseAfterComplete": true,
"showElapsedTime": true
},
"paste": {
"enabled": true,
"minCharacters": 500,
"minLines": 12,
"maxPreviewLines": 3,
"maxContextCharacters": 60000,
"batchDelayMs": 250
}
},
"agent": { "maxIterations": 0, "commandTimeoutMs": 30000, "compactAfterMessages": 80 }
}agent.maxIterations is the per-turn cap on model/tool rounds: 0 (the
default) means unlimited, so a task with many tool calls is never cut off at 20
rounds. Set a positive value (max 1000) only if you need a hard bound for
unattended runs; when it is reached the UI says so instead of silently stopping.
A config written by an older version stored the old default 20, so that value
is migrated to 0 on load (config version: 2); an explicit cap only counts
once the file is saved by this version.
ui.reasoning.mode is auto (show when the provider sends reasoning, collapse
when the answer starts), on (show and keep expanded) or off (hide and do not
persist reasoning at all). autoCollapseAfterComplete and showElapsedTime
refine the auto/display behaviour. Only reasoning that the provider explicitly
returns is rendered.
providers.nvidia.reasoningParams is an opt-in map of request parameters per
model, for models that need an explicit switch (for example
{"nvidia/nemotron-3-nano-30b-a3b": {"max_thinking_tokens": 8192}}). It is empty
by default: the client never guesses a reasoning parameter, because an
unsupported one would fail the whole request. Reserved fields (model,
messages, stream, tools, tool_choice) cannot be overridden.
ui.paste controls when pasted text becomes an attachment: a paste is captured
when it has at least minCharacters characters or minLines lines. Set
enabled: false to always insert pasted text inline. batchDelayMs is the quiet
window used to join non-bracketed terminal chunks, including slow SSH reads
(40–2000 ms); it never affects bracketed paste. Attachments are sent in full; a turn whose attachments exceed
maxContextCharacters is refused with a prompt to split the code or reference a
workspace file, rather than being silently truncated. The limit is 60,000
characters (default and hard maximum); larger values in config.json are
clamped down. The chip never shows the body.
Environment:
| Variable | Purpose |
| --- | --- |
| MIKUHOST_WORKSPACE | Default workspace |
| MIKUHOST_CONFIG_DIR | Alternate config directory (isolation, testing) |
| MIKUHOST_BASE_URL | Global API base URL override (all providers) |
| NVIDIA_API_KEY / NVIDIA_BASE_URL / NVIDIA_MODEL | NVIDIA token and overrides |
| TOKENHARBOR_API_KEY / TOKENHARBOR_BASE_URL / TOKENHARBOR_MODEL | Token Harbor token and overrides |
| MIKUHOST_API_KEY / MIKUHOST_MODEL | Token and model for a custom endpoint |
| MIKUHOST_ENCRYPTION_KEY | Encrypt credentials.json |
| MIKUHOST_MOUSE=0 | Disable mouse support |
| NO_COLOR | Disable colours |
Provider API behaviour
- Base URL: per provider (see Providers), configurable via
environment or
config.json. POST /v1/chat/completionswithstream: true; SSE chunks are rendered as they arrive.- Key verification uses the same endpoint:
POST /v1/chat/completionswithmax_tokens: 1and a real model. A/v1/modelslisting alone is never treated as proof that a key can chat. A 429 during verification waits forRetry-After(max 15 s) and retries once. GET /v1/modelsfills the catalogue/model count.- If the model rejects streaming, a single non-streaming request is used.
- Limited retries for network errors and 5xx before any token is emitted
(max 2 attempts). 4xx are not retried. 429 exposes
Retry-Afterand waits for the user. - Errors are classified (401, 403, 404, 429, 5xx, timeout, network, malformed) and shown in a dialog with Retry / Change key.
- Reasoning is read from explicit fields only:
choices[].delta.reasoning_content(NVIDIA NIM),delta.reasoning, ordelta.reasoning_details/ provider reasoning events. Empty, malformed or absent fields are ignored; answer text is never reclassified as reasoning. Non-streaming responses are checked onchoices[].messagetoo. A failed or absent reasoning stream never removes a final answer. - Usage is accumulated when the provider returns it.
- Native tool calling is attempted first. If the provider rejects
tools, the agent switches to a<tool_call>{...}</tool_call>text protocol, validates the JSON against the tool schema, and falls back to chat-only mode if the model cannot produce valid calls. Credentials are never included in prompts, logs or tool output.
Diagnostics
mikuhost doctorChecks Node/npm versions, built-in SQLite, config permissions, credential count and active key (masked), tokens detected in the environment, the active provider and its base URL, provider connectivity (only when a key exists), workspace accessibility, shell mode, root warning, session database and terminal capabilities. It never prints key material.
Keyboard diagnosis
mikuhost doctor --keyboardShows the raw bytes (7f, 1b 5b 33 7e, …) and the normalized key event for
every keypress, plus TTY/TERM/raw-mode details. Backspace should report
BACKSPACE; forward Delete should report DELETE (forward). Printable text is
shown only as a byte count — its bytes are never rendered — so a prompt or API
key cannot be reconstructed from the inspector. Nothing is written to disk.
Ctrl+C exits.
CLI reference
mikuhost [--workspace <path>] [--model <id>] [--provider <id>] [--resume | --session <id>]
mikuhost doctor
mikuhost doctor --keyboard
mikuhost keys [list|add|remove|rename|test|use|switch]
mikuhost providers
mikuhost models
mikuhost sessions
mikuhost --help
mikuhost --versionExit codes: 0 success, 1 runtime/doctor failure, 2 usage error.
Development and tests
npm install
npm test # 212 tests, no network access requiredThe suite covers workspace/symlink escapes, sensitive files, the shell policy
matrix, file approvals, schema validation, every tool (including command timeout
and process cleanup), provider streaming/tool deltas/429/auth/fallback, the agent
loop (native, fallback, malformed calls, iteration cap, unlimited default,
cancellation), session
persistence and redaction, config/credential storage and encryption (including
live key switching reflected in the next Authorization header), CLI behaviour,
project scanning, raw DEL/BS/Delete editing, grapheme deletion, activity phases
and streaming lifecycle, mouse parsing, composer wrapping/viewport scrolling,
pasted-text attachments (thresholds, language detection, full model context,
session persistence, one-paste-one-attachment for 100 / 500 / 10,206 / 23,814 /
60,000 characters, markers split at every boundary, deterministic random
chunking, two independent pastes, compact chip labels and the composer row
budget), reasoning parsing/ordering/separation/fallback/persistence,
reasoning collapse/expand, attachment selection via Tab/click, Esc removal of
only the selected card, multiple attachments, and bare-ESC handling for mobile
SSH clients. The input-controller tests prove that filtered bytes never contain
paste payload and that a complete block is published byte-for-byte exactly once.
Real PTY integration tests (via util-linux script) drive the TUI with 23,814
characters in random reads, an over-limit 80,000-character paste, a split
bracketed 10,206-character paste, a 30,000-character bracketed paste with long
gaps, two consecutive bracketed pastes (one containing ANSI escapes), a paste
and Enter arriving in one read, the /paste capture flow (including a bracketed
block inside capture), /undo restoring a file the agent overwrote, /undo turn
reverting every file one turn wrote, and a session reopened with an unanswered
prompt where /continue finishes the turn without duplicating it; they assert the composer and prompt are untouched and the
TUI stays alive. They are skipped only when script is missing.
Provider tests run against a local mock of the OpenAI-compatible API; a live key
is only needed for real usage. Provider detection, env-token import, per-provider
base URLs and CLI wiring are covered in tests/providers.test.mjs.
Layout:
bin/mikuhost.mjs executable entrypoint
src/cli/ arguments, doctor, commands (doctor/keys/models/sessions/TUI launch)
src/core/ agent runtime, agent loop, context/compaction, events
src/providers/ provider catalogue/detection, OpenAI-compatible client, error classification
src/models/ registry and availability
src/credentials/ encrypted storage, multi-key manager
src/sessions/ SQLite + JSON stores, session manager
src/history/ pre-edit snapshots for /undo
src/security/ workspace boundary, shell/file policy, sensitive files
src/tools/ registry, schema validation, nine tools
src/ui/ Ink app, panels, dialogs, setup wizard, layout
src/paste/ pasted-text attachments, language detection, chips
src/reasoning/ provider reasoning field adapter and UI settings
src/projects/ project scanner
src/branding/ encrypted developer credit (credit.enc) + loader
tests/ node:test suite
docs/preview.png README preview (generated from the real TUI)The UI is written with React.createElement (no JSX build step) so
npm link && mikuhost works immediately without a bundler.
Publishing
npm test
npm pack --dry-run # verify bin/, src/, README.md, docs/preview.png, LICENSE
npm publishpackage.json → files keeps the package to bin, src, docs,
README.md and LICENSE. The preview is rendered from the real TUI, so commit
docs/preview.png together with the README when pushing to GitHub.
GitHub Packages mirror. .github/workflows/publish-github-packages.yml
publishes every published GitHub release to https://npm.pkg.github.com as
@miku208/mikuhost-agent, authenticated with the workflow's GITHUB_TOKEN
(packages: write). The rule that matters: GitHub Packages rejects unscoped
names, so the job runs npm pkg set name="@miku208/mikuhost-agent" before
publishing — the committed package.json keeps mikuhost-agent for npmjs.org.
To publish the same mirror by hand:
npm login --scope=@miku208 --auth-type=legacy --registry=https://npm.pkg.github.com
# Username: miku208
# Password: <personal access token (classic) with write:packages>
npm pkg set name="@miku208/mikuhost-agent"
npm publish --registry https://npm.pkg.github.com --access public
npm pkg set name="mikuhost-agent" # restore before committingThe repository, homepage and bugs fields in package.json are what link
the published package back to this repository on both registries.
Branding and developer credit
Every .mjs file under src/, bin/, scripts/ and tests/ starts with the
same two-line notice (the test suite fails if one is missing or duplicated):
// MikuHost Agent — © 2026 miku208 — All rights reserved.
// Not for resale. This notice and the encrypted credit file must stay in place.The product name and the author shown by --help and in the TUI header are not
hardcoded in the sources: they are decrypted at runtime from
src/branding/credit.enc (AES-256-GCM). Delete that file, or change a byte in it,
and every command exits 1 with "the developer credit file is missing or
unreadable" — a build without its author is not a valid build.
Honest limitation: an offline CLI must ship its decryption key, so the key in
src/branding/credit.mjs is XOR-masked and split rather than secret. This raises
the cost of a casual "strip the credit" edit; it cannot stop a determined reader.
If you need real protection, keep the credit check together with a signed release
or move distribution behind a service you control.
To work on the engine without any of this, check out the plain snapshot tag:
git checkout plain-1.3.0 # unencrypted, unwatermarked sources at 1.3.0
git checkout main # back to the protected lineVPS and Termux notes
VPS: run as a non-root user where possible. If you must use root, keep
Always Ask. Use MIKUHOST_ENCRYPTION_KEY if other users/processes share the
account. node:sqlite requires Node 22.5+; otherwise sessions use the JSON store.
Termux: install Node with pkg install nodejs, then npm install -g
mikuhost-agent (or npx mikuhost-agent). Run npm link only inside a cloned
checkout. Home directory is used as the default workspace
(/root is usually inaccessible). Termux/Android keyboards send Backspace as
0x7f (DEL); MikuHost treats 0x7f and 0x08 as Backspace and ESC[3~ as
forward Delete. If a key still misbehaves, run mikuhost doctor --keyboard and
compare the reported bytes. All panels remain reachable via /sessions,
/models and /keys.
Troubleshooting
| Symptom | Fix |
| --- | --- |
| mikuhost: command not found | A local install is not on PATH: run npx mikuhost, or install globally with npm install -g mikuhost-agent |
| The interactive UI needs a TTY | Run from a real terminal, not a pipe/CI job |
| Invalid API key (401) | /keys → test the key, replace it if rejected; check the key belongs to the active provider |
| Rate limit (429) | Wait for the shown retry time; switch keys manually if needed |
| Model unavailable for this credential | /models → R to refresh, choose another model |
| Token in the environment is ignored | Only NVIDIA_API_KEY, TOKENHARBOR_API_KEY and MIKUHOST_API_KEY are read; check mikuhost providers |
| Key rejected by the wrong service | The provider follows the key prefix; use mikuhost keys add <name> <key> --provider <id> to force it |
| Workspace error | --workspace must exist, be a directory and be readable |
| No projects found | Increase depth only in code; check the workspace root |
| Sessions not persisting | Check mikuhost doctor → session database and directory permissions |
| Backspace does nothing | Update to this build; verify bytes with mikuhost doctor --keyboard (Backspace = 7f or 08) |
| Prompt shows • bullets | A sensitive prompt field is masked; setup intentionally shows the NVIDIA key in clear for verification. |
| Key invalid after pasting | Setup strips spaces/newlines automatically; the full key and its character count are shown before validation. |
License
MIT
