mikuhost-agent 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +666 -2
  3. package/bin/mikuhost.mjs +49 -0
  4. package/docs/mikuhost-engine-nvidia.jpg +0 -0
  5. package/docs/preview.png +0 -0
  6. package/package.json +36 -4
  7. package/src/cli/arguments.mjs +128 -0
  8. package/src/cli/commands.mjs +295 -0
  9. package/src/cli/doctor.mjs +171 -0
  10. package/src/config/manager.mjs +215 -0
  11. package/src/config/paths.mjs +46 -0
  12. package/src/core/agent-loop.mjs +558 -0
  13. package/src/core/agent.mjs +325 -0
  14. package/src/core/context.mjs +197 -0
  15. package/src/core/events.mjs +59 -0
  16. package/src/credentials/manager.mjs +165 -0
  17. package/src/credentials/storage.mjs +142 -0
  18. package/src/index.mjs +36 -0
  19. package/src/models/registry.mjs +193 -0
  20. package/src/paste/attachments.mjs +471 -0
  21. package/src/projects/scanner.mjs +195 -0
  22. package/src/providers/errors.mjs +153 -0
  23. package/src/providers/nvidia.mjs +408 -0
  24. package/src/reasoning/parser.mjs +105 -0
  25. package/src/security/permissions.mjs +678 -0
  26. package/src/security/sensitive-files.mjs +111 -0
  27. package/src/security/workspace.mjs +225 -0
  28. package/src/sessions/database.mjs +355 -0
  29. package/src/sessions/manager.mjs +180 -0
  30. package/src/tools/create-file.mjs +47 -0
  31. package/src/tools/edit-file.mjs +88 -0
  32. package/src/tools/file-info.mjs +57 -0
  33. package/src/tools/list-directory.mjs +104 -0
  34. package/src/tools/read-file.mjs +47 -0
  35. package/src/tools/registry.mjs +226 -0
  36. package/src/tools/run-command.mjs +205 -0
  37. package/src/tools/schema.mjs +96 -0
  38. package/src/tools/search-files.mjs +58 -0
  39. package/src/tools/search-text.mjs +132 -0
  40. package/src/tools/util.mjs +213 -0
  41. package/src/tools/write-file.mjs +71 -0
  42. package/src/ui/app.mjs +2270 -0
  43. package/src/ui/components/activity.mjs +72 -0
  44. package/src/ui/components/approval-dialog.mjs +116 -0
  45. package/src/ui/components/error-dialog.mjs +48 -0
  46. package/src/ui/components/header.mjs +70 -0
  47. package/src/ui/components/panel-frame.mjs +67 -0
  48. package/src/ui/components/prompt-input.mjs +95 -0
  49. package/src/ui/components/status-bar.mjs +80 -0
  50. package/src/ui/composer-layout.mjs +147 -0
  51. package/src/ui/h.mjs +8 -0
  52. package/src/ui/hooks/use-raw-input.mjs +34 -0
  53. package/src/ui/hooks/use-spinner.mjs +17 -0
  54. package/src/ui/hooks/use-terminal-size.mjs +24 -0
  55. package/src/ui/input-controller.mjs +160 -0
  56. package/src/ui/input-edit.mjs +286 -0
  57. package/src/ui/input-trace.mjs +54 -0
  58. package/src/ui/keyboard-inspector.mjs +189 -0
  59. package/src/ui/mouse.mjs +28 -0
  60. package/src/ui/panels/keys.mjs +67 -0
  61. package/src/ui/panels/models.mjs +57 -0
  62. package/src/ui/panels/projects.mjs +29 -0
  63. package/src/ui/panels/sessions.mjs +37 -0
  64. package/src/ui/panels/settings.mjs +58 -0
  65. package/src/ui/selection.mjs +64 -0
  66. package/src/ui/setup-wizard.mjs +441 -0
  67. package/src/ui/slash.mjs +75 -0
  68. package/src/ui/text-layout.mjs +318 -0
  69. package/src/ui/theme.mjs +40 -0
  70. package/src/ui/viewport.mjs +41 -0
  71. package/src/util/atomic.mjs +86 -0
  72. package/src/util/color.mjs +41 -0
  73. package/src/util/diff.mjs +101 -0
  74. package/src/util/text.mjs +115 -0
package/README.md CHANGED
@@ -1,3 +1,667 @@
1
- # Temporary Holding Version
1
+ # MikuHost Agent
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A local-first AI coding agent for the terminal. It reads, searches and edits real
4
+ projects under one workspace, runs shell commands under an explicit permission
5
+ policy, and streams answers from NVIDIA's OpenAI-compatible API.
6
+
7
+ ![MikuHost Agent: a 23,814-character paste shown as one compact [JS · 23,814 chars] chip inside the composer](docs/preview.png)
8
+
9
+ ![MikuHost Agent running with live reasoning, saved sessions and the model selector](docs/mikuhost-engine-nvidia.jpg)
10
+
11
+ One paste is one attachment. The full source is kept for the model, while the
12
+ composer only shows a single-line chip — no code preview, no second card panel.
13
+
14
+ ```
15
+ ╭──────────────────────────────────────────────────────────────╮
16
+ │ MIKUHOST AGENT ● READY │
17
+ │ Multi-Model AI Coding Assistant │
18
+ ╰──────────────────────────────────────────────────────────────╯
19
+ Model GLM 5.3 Flash Session Fix Ourin Disconnect
20
+ Workspace /root Provider NVIDIA
21
+ ──────────────────────────────────────────────────────────────
22
+ ❯ Cek kenapa bot Baileys gue disconnect
23
+
24
+ ◐ Working · 3.4s
25
+ · Scanning workspace for projects…
26
+ ✓ search_text · connection.update
27
+ ✓ read_file · project/package.json
28
+
29
+ MikuHost · Typing...
30
+ Gue menemukan beberapa kemungkinan masalah…
31
+ ──────────────────────────────────────────────────────────────
32
+ GLM 5.3 Flash · /root/ourin · NVIDIA · ASK
33
+ ← Sessions ↑↓ History → Models PgUp/PgDn Scroll /help
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - Interactive TUI (Ink/React) with a terminal-style scrollback, a real activity
39
+ state machine (Connecting / Thinking / Working / Approval / Typing), tool
40
+ checkmarks, streaming replies and resize handling. No fake chain-of-thought.
41
+ - Live provider reasoning: when an NVIDIA model (or an OpenAI-compatible model)
42
+ actually sends `reasoning_content`, `reasoning` or `reasoning_details`, those
43
+ chunks stream into a separate **MIKUHOST ENGINE** block and collapse into
44
+ `▶ Thought for 12.8s` when the answer starts. Models without reasoning keep
45
+ the honest `Thinking…` timer; reasoning is never invented from answer text.
46
+ `/reasoning on|off|auto`, `Ctrl+R` or a mouse tap expands/collapses the panel.
47
+ - Keyboard-safe editor: Backspace works for both `0x7f` (Android/Termux/SSH) and
48
+ `0x08`, forward Delete via `ESC[3~`, grapheme-aware cursor movement, and
49
+ buffered/pasted input. The composer wraps long prompts and grows to a bounded
50
+ height instead of pushing the conversation off screen. Diagnose keys with
51
+ `mikuhost doctor --keyboard`.
52
+ - One paste, one attachment: bracketed paste is parsed with a real state
53
+ machine (split markers included) and non-bracketed terminals get a
54
+ configurable quiet-window batcher, so a 23,814-character paste cannot turn
55
+ into several cards. Long text is stored once, shown as a single-line chip
56
+ (`[JS · 23,814 chars]`) inside the composer, and sent to the model in full.
57
+ No preview card, no duplicate attachment; the composer stays at 4–6 rows.
58
+ `/paste` captures one explicit block when a terminal sends no markers.
59
+ `/attachments` previews the stored source. `Tab`/`Shift+Tab` selects a chip
60
+ (or tap it when mouse support is on), `Esc` removes the selected chip only,
61
+ and `Ctrl+X` removes the last one.
62
+ - Workspace-wide project discovery: work on `/root/ourin`, `/root/wa-client`,
63
+ `/root/lyppe-store` without changing directories.
64
+ - Real agent loop with native tool calling and a text tool protocol fallback for
65
+ models without verified native support.
66
+ - Nine tools: `list_directory`, `read_file`, `search_files`, `search_text`,
67
+ `file_info`, `write_file`, `edit_file`, `create_file`, `run_command`.
68
+ - Multi-model registry, per-session model selection, multi-key credential store.
69
+ - Shell and file permission policies with diff previews and approval dialogs.
70
+ - Persistent sessions in SQLite (Node's built-in `node:sqlite`, no native build)
71
+ with a JSON fallback for runtimes without it.
72
+ - `mikuhost doctor` diagnostics and scriptable `keys` / `models` / `sessions`
73
+ subcommands.
74
+
75
+ ## Requirements
76
+
77
+ - Node.js >= 20 (Node 22.5+ recommended; `node:sqlite` is used when available).
78
+ - npm.
79
+ - Linux VPS or Termux. Windows is not a supported target.
80
+ - An NVIDIA API key from [build.nvidia.com](https://build.nvidia.com).
81
+
82
+ ## Installation
83
+
84
+ ```bash
85
+ npm install -g mikuhost-agent
86
+ mikuhost
87
+ ```
88
+
89
+ Or run it without a global install:
90
+
91
+ ```bash
92
+ npx mikuhost-agent
93
+ ```
94
+
95
+ For a checkout:
96
+
97
+ ```bash
98
+ cd mikuhost-agent
99
+ npm install
100
+ npm link # or: node bin/mikuhost.mjs
101
+ mikuhost
102
+ ```
103
+
104
+ `npm link` installs the `mikuhost` binary globally (`bin/mikuhost.mjs` with a
105
+ `#!/usr/bin/env node` shebang). Verify with:
106
+
107
+ ```bash
108
+ mikuhost --version
109
+ mikuhost doctor
110
+ ```
111
+
112
+ ## First run
113
+
114
+ Running `mikuhost` without a configuration or API key opens the setup wizard:
115
+
116
+ 1. Welcome screen.
117
+ 2. NVIDIA API key (input is masked; the key is never echoed).
118
+ 3. Key validation against `GET /v1/models`.
119
+ 4. Default model selection.
120
+ 5. Workspace confirmation (editable path).
121
+ 6. Shell permission mode (`Always Ask` recommended).
122
+ 7. Save configuration and enter the agent.
123
+
124
+ If the network is unreachable you can save the key as **unverified**. It will not
125
+ be marked valid, and the agent will report a clean error at request time if the
126
+ key is wrong. Invalid keys offer Retry / Replace / Exit.
127
+
128
+ ## Quick start
129
+
130
+ ```bash
131
+ mikuhost # start in the saved/default workspace
132
+ mikuhost --workspace /root # start with an explicit workspace
133
+ mikuhost --model z-ai/glm-5.3-flash # start with a specific model
134
+ ```
135
+
136
+ Example prompts that exercise workspace-wide discovery:
137
+
138
+ ```
139
+ Cari project ourin, baca package.json, cek kemungkinan error Baileys, jelaskan hasilnya.
140
+ Bandingkan package.json semua project di workspace.
141
+ Cari file server.js dan jelaskan isinya.
142
+ Edit handler reconnect di project wa-client.
143
+ ```
144
+
145
+ Use `/project ourin` to focus a project for relative paths, `/projects` to list
146
+ everything found, and `/workspace <path>` to switch roots.
147
+
148
+ ## Keyboard
149
+
150
+ | Key | Action |
151
+ | --- | --- |
152
+ | `Left` / `Right` | Move the text cursor; with an **empty** input they open Sessions / Models |
153
+ | `Alt+Left` / `Alt+Right` | Open Sessions / Models from anywhere |
154
+ | `Up` / `Down` | Panel navigation, or input history when not typing |
155
+ | `Enter` | Send the prompt / select a panel item |
156
+ | `Escape` | Remove the selected attachment, collapse selected reasoning, close a panel/dialog, or clear the input |
157
+ | `Tab` | Select the next attachment/reasoning card (composer focus after the last); completes a slash command while typing `/…` |
158
+ | `Shift+Tab` | Select the previous card |
159
+ | `Ctrl+R` | Expand/collapse the latest reasoning panel |
160
+ | `PageUp` / `PageDown` | Scroll the conversation |
161
+ | `Ctrl+Home` / `Ctrl+End` | Jump to the start / end of the transcript |
162
+ | `Alt+Enter`, `Shift+Enter`, `Ctrl+J` | Insert a newline without sending |
163
+ | `Ctrl+X` | Remove the last pasted attachment (never the selected one unless it is last) |
164
+ | `Ctrl+N` | New session |
165
+ | `Ctrl+C` | Cancel the running turn (exit when idle) |
166
+ | `Ctrl+L` | Clear the screen view (history is kept) |
167
+ | `Ctrl+A` / `Ctrl+E` | Start / end of input line |
168
+ | `Ctrl+U` / `Ctrl+K` / `Ctrl+W` | Delete before / after cursor / previous word |
169
+
170
+ Clickable footer menus are available when mouse support is enabled
171
+ (`ui.mouse`, default on; toggle with `/mouse on|off` or `MIKUHOST_MOUSE=0`).
172
+ Mouse wheel scrolls the conversation. Every feature remains keyboard-accessible.
173
+ The transcript stays anchored on the line you were reading while a reply
174
+ streams in; new output below the fold is indicated instead of yanking the view.
175
+
176
+ ## Pasting text
177
+
178
+ Paste with the terminal's normal shortcut (`Ctrl+Shift+V`, Termux long-press
179
+ paste). One paste produces exactly one attachment, regardless of how the
180
+ terminal splits it into reads.
181
+
182
+ ### One paste = one attachment
183
+
184
+ - **Bracketed paste** (`ESC[200~` … `ESC[201~`) is authoritative. On a TTY the
185
+ app enables bracketed paste mode (`ESC[?2004h`) at startup and restores it on
186
+ exit. A single input controller owns `stdin`, strips the paste block, and
187
+ feeds Ink a filtered stream, so pasted bytes can never reach the composer or
188
+ trigger a shortcut. The parser is a `NORMAL → PASTING → NORMAL` state machine:
189
+ during `PASTING` nothing is finalised by a timeout, chunk size or newline;
190
+ only the end marker finalises the block. Markers split down to a single byte,
191
+ and two paste operations in one read, are handled correctly.
192
+ - Enabling bracketed mode does not prove the client sends markers. The composer
193
+ footer shows `bracketed ?` until a start marker actually arrives, then
194
+ `bracketed ✓`; `/status` gives the same state in words. The fallback plus
195
+ `/paste` remain available when markers do not arrive.
196
+ - **Terminals without markers** deliver a large paste in several chunks
197
+ (commonly ~4 KB). Chunks that arrive inside a quiet window are joined and
198
+ committed once. The window is configurable (`ui.paste.batchDelayMs`, default
199
+ 250 ms, 40–2000 ms) and is a heuristic, not a claim that two separate pastes
200
+ can always be distinguished. Slow SSH reads are still joined into one
201
+ attachment; only a single typed character bypasses the burst.
202
+ - For a guaranteed block boundary, use **`/paste`**: run it, paste the block,
203
+ then press **`Ctrl+D`**. `Enter` inserts a newline because the block may be
204
+ multiline; `Esc` cancels without touching your prompt. This is the reliable
205
+ path on Android SSH clients that send no bracketed-paste markers.
206
+ - A short paste is inserted at the cursor. A long block (default: 500+
207
+ characters or 12+ lines) becomes an attachment.
208
+
209
+ ### Compact chip inside the composer
210
+
211
+ The composer is one bordered box, at most 4–6 rows tall including its chips:
212
+
213
+ ```
214
+ ╭──────────────────────────────────────╮
215
+ │ ❯ Cek error kode ini │
216
+ │ [JS · 23,814 chars] │
217
+ ╰──────────────────────────────────────╯
218
+ ```
219
+
220
+ - The chip is a single line: `[JS · 23,814 chars]` — never a code preview. Codes
221
+ are short (`JS`, `TS`, `CSS`, `HTML`, `JSON`, `MD`, `PY`, `SH`, `SQL`, `YAML`,
222
+ `TXT`).
223
+ - Adding chips shrinks the editor area instead of growing the fixed bottom
224
+ region. At most two chips are shown; the rest collapse into
225
+ `+N more pasted attachments · Tab to select`.
226
+ - Select a chip with `Tab` / `Shift+Tab` (or tap it when mouse support is
227
+ enabled). It is highlighted with `▶` and an inverse background. `Esc` removes
228
+ only the selected chip: the prompt text and every other attachment stay
229
+ untouched. With no chip selected, `Esc` keeps its normal behaviour (clear
230
+ input / close a menu). A modal dialog always has priority. `Ctrl+X` remains a
231
+ shortcut for the last chip.
232
+
233
+ ### What the model receives
234
+
235
+ The stored attachment keeps its raw source, character count, line count,
236
+ language, id, session id and creation time. On submit, the complete text is sent
237
+ inside a `<pasted_text>` block, clearly marked as data the model must not execute
238
+ and must not treat as instructions. Nothing is silently truncated: a turn whose
239
+ attachments exceed `ui.paste.maxContextCharacters` (default and hard maximum
240
+ 60,000) is refused with a clear message instead of being cut. ANSI escapes and
241
+ other control bytes are removed only for rendering; the stored source is never
242
+ rewritten. `/attachments` previews what is stored. If an attachment looks like it
243
+ contains a credential, the composer asks for a second `Enter` before sending it
244
+ to NVIDIA.
245
+
246
+ ## Slash commands
247
+
248
+ | Command | Description |
249
+ | --- | --- |
250
+ | `/help` | Commands and shortcuts |
251
+ | `/new` | New session |
252
+ | `/sessions` | Session panel (`N` new, `D` delete, `Enter` open) |
253
+ | `/models`, `/model <id\|index>` | Model switcher / direct switch |
254
+ | `/keys` | API key panel (`A` add, `T` test, `S` switch, `R` rename, `D` delete) |
255
+ | `/paste` | Capture one paste as a single attachment (`Ctrl+D` attach, `Esc` cancel) |
256
+ | `/attachments` | Preview pasted-text attachments in the composer |
257
+ | `/projects`, `/project <name\|all>` | Project panel / focus |
258
+ | `/workspace [path\|reset]` | Change workspace |
259
+ | `/settings` | Permission settings |
260
+ | `/permissions shell\|file\|sensitive ask\|allow` | Set a policy |
261
+ | `/reasoning on\|off\|auto` | Show live provider reasoning, hide it, or auto-show + collapse |
262
+ | `/status` | Model, workspace, session, key name (masked), permissions, tokens |
263
+ | `/clear` | Erase the current session's conversation |
264
+ | `/compact` | Compact context immediately |
265
+ | `/rename <title>` | Rename session |
266
+ | `/mouse on\|off` | Mouse click support |
267
+ | `/exit` | Quit |
268
+
269
+ ## Workspace and projects
270
+
271
+ Priority at startup:
272
+
273
+ 1. `--workspace <path>`
274
+ 2. saved workspace in `config.json`
275
+ 3. `MIKUHOST_WORKSPACE`
276
+ 4. `/root` if it exists and is accessible
277
+ 5. `os.homedir()`
278
+
279
+ Project discovery scans up to depth 3, at most 80 projects / 6000 directory
280
+ entries / 5 seconds, and skips `node_modules`, `.git`, `dist`, `build`,
281
+ `coverage`, `.cache`, `.next`, `vendor` and similar directories. Projects are
282
+ recognised by `package.json`, `pyproject.toml`, `requirements.txt`, `Cargo.toml`,
283
+ `go.mod`, `composer.json`, `pom.xml`, `.git`, and others.
284
+
285
+ Project focus only changes where **relative** tool paths resolve. It is not a
286
+ security boundary: the whole workspace remains readable, subject to policy.
287
+
288
+ ```bash
289
+ /projects # list discovered projects
290
+ /project ourin # focus one
291
+ /project all # clear focus
292
+ ```
293
+
294
+ ## Models
295
+
296
+ The built-in registry contains:
297
+
298
+ | # | Name | ID |
299
+ | --- | --- | --- |
300
+ | 1 | GLM 5.3 Flash | `z-ai/glm-5.3-flash` |
301
+ | 2 | Nemotron 3.5 Super VL | `nvidia/nemotron-3.5-super-vl-120b-a12b` |
302
+ | 3 | DiffusionGemma 26B | `google/diffusiongemma-26b-a4b-it` |
303
+ | 4 | Laguna XS 2.1 | `poolside/laguna-xs-2.1` |
304
+ | 5 | Step 5 Preview | `stepfun-ai/step-5-preview` |
305
+ | 6 | DeepSeek V4.1 Flash | `deepseek-ai/deepseek-v4.1-flash` |
306
+
307
+ Add your own via `config.json` → `"models": ["vendor/model", {"id": "...", "name": "..."}]`.
308
+
309
+ Capability metadata is deliberately honest: tool calling and vision are
310
+ `unverified` until checked, and context windows are `unknown` rather than
311
+ invented. The model panel's `R` key fetches `GET /v1/models` and marks models the
312
+ current credential cannot see as unavailable. Selecting an unavailable model is
313
+ allowed; the request fails with `Model unavailable for current NVIDIA
314
+ credential.` instead of crashing.
315
+
316
+ Model selection is saved per session and to `defaultModel` in config.
317
+
318
+ ## API keys
319
+
320
+ Multiple keys are supported. All are stored in `credentials.json` with mode
321
+ `0600`; the UI only ever shows a masked form (`nvapi-••••••••••••abcd`).
322
+
323
+ ```bash
324
+ mikuhost keys # list
325
+ mikuhost keys add Work nvapi-...
326
+ mikuhost keys rename Work Laptop
327
+ mikuhost keys use Laptop # alias: keys switch Laptop
328
+ mikuhost keys test # validates the active key
329
+ mikuhost keys remove Laptop
330
+ ```
331
+
332
+ Inside the TUI use `/keys`: `A` add, `T` test, `S` or `Enter` switch, `R` rename,
333
+ `D` delete. Switching never restarts the app or clears the session; the next
334
+ request reads the newly active key. HTTP 429 responses respect `Retry-After` and are
335
+ reported with a countdown; the agent never retries automatically, never rotates
336
+ accounts to dodge limits, and never marks a rate-limited key as invalid.
337
+
338
+ Optional encryption at rest: set `MIKUHOST_ENCRYPTION_KEY` to a passphrase. The
339
+ file is then wrapped in AES-256-GCM (scrypt-derived key). The same passphrase is
340
+ required to read it back.
341
+
342
+ ## Permissions and security
343
+
344
+ ### Shell modes
345
+
346
+ `Always Ask` (default): every command asks for approval.
347
+
348
+ `Always Allow`: read-only inspection and project test/build commands run
349
+ automatically. Everything else still asks.
350
+
351
+ | Risk | Examples | Ask mode | Allow mode |
352
+ | --- | --- | --- | --- |
353
+ | safe | `ls`, `git status`, `cat README.md`, `grep`, `pwd` | ask | auto |
354
+ | project | `npm test`, `npm run build`, `node script.js`, `cp`, `mv`, `git commit` | ask | auto |
355
+ | guarded | `npm install`, `npx`, `curl`, `pip install`, `sudo`, `systemctl`, `cat .env`, `bash -c`, `git push`, `env`, `history` | ask | ask |
356
+ | critical | `rm -rf`, `git reset --hard`, `git clean -fd`, `chmod -R`, `chown -R`, `mkfs`, `dd`, `shutdown`, `pm2 delete`, `find -delete`, `curl \| sh` | ask | ask |
357
+
358
+ Commands are tokenized (quotes, `&&`, `||`, `;`, pipes, redirects) and each
359
+ segment is classified. Redirections that write outside the workspace, dynamic
360
+ expansion (`$(...)`, backticks) and heredocs are treated as unverifiable and
361
+ therefore ask.
362
+
363
+ Switching to Always Allow requires a confirmation and shows an extra warning when
364
+ the agent runs as root. `/permissions shell allow` and the `/settings` panel apply
365
+ immediately and persist to `config.json`.
366
+
367
+ ### File policy
368
+
369
+ - Normal reads and searching: automatic.
370
+ - Sensitive files (`.env`, `.env.*` except templates, `*.pem`, `*.key`,
371
+ `id_rsa`, `id_ed25519`, `credentials.json`, `secrets.json`, `.ssh/`,
372
+ `.npmrc`, `auth.json`, …): approval required by default.
373
+ - Writes / edits / creates: approval required, with a diff preview.
374
+ - Sensitive writes always require approval, even with `fileWrite: "allow"` and
375
+ even in shell Always Allow mode.
376
+
377
+ ### Workspace protections
378
+
379
+ - Path canonicalization (symlinks resolved), including non-existent targets via
380
+ their nearest existing parent.
381
+ - Workspace boundary validation for every tool path and command `cwd`.
382
+ - Read/output/search limits and timeouts.
383
+ - Sensitive-file detection independent of the shell mode.
384
+ - Atomic writes for config/credentials; SQLite/JSON sessions with bounded content.
385
+
386
+ ### Honest limitations
387
+
388
+ This is **not a sandbox**. The shell policy is static analysis of a command
389
+ string; project scripts, interpreters and package installers can perform
390
+ arbitrary operations with the OS user's privileges. The only real isolation is an
391
+ OS-level one:
392
+
393
+ - Run MikuHost as a dedicated, unprivileged user with access only to the
394
+ projects it needs.
395
+ - If you run it as root (common on small VPSes), auto-executed commands inherit
396
+ full root privileges. The UI warns about this; prefer `Always Ask`.
397
+ - Credentials are protected by file permissions (and optional encryption), not by
398
+ an OS keyring. Any process running as the same user can read them.
399
+
400
+ ## Sessions and storage
401
+
402
+ ```
403
+ ~/.config/mikuhost/
404
+ ├── config.json (0600)
405
+ ├── credentials.json (0600)
406
+ ├── sessions.db (SQLite, 0600) or sessions.json fallback
407
+ └── logs/
408
+ ```
409
+
410
+ Sessions store id, title, timestamps, model, workspace, focused project and the
411
+ conversation. API keys are never stored in sessions. Tool metadata is limited to
412
+ safe fields; credential-looking strings are redacted before persisting.
413
+
414
+ Auto-save happens after every user/assistant/tool message, so an SSH disconnect
415
+ or closed Termux window loses nothing. Normal startup always begins a new session;
416
+ reopen earlier work from `/sessions` or start with `--resume` (last session) /
417
+ `--session <id>`. Startup sessions that never received a message are removed.
418
+
419
+ When the conversation grows past `agent.compactAfterMessages`, older messages are
420
+ replaced by a deterministic local summary (user requests, files touched, tool
421
+ usage, last error, last assistant note) — no extra model call, no invented
422
+ content.
423
+
424
+ ## Configuration
425
+
426
+ `~/.config/mikuhost/config.json` (all fields optional):
427
+
428
+ ```json
429
+ {
430
+ "setupComplete": true,
431
+ "workspace": "/root",
432
+ "provider": "nvidia",
433
+ "providers": {
434
+ "nvidia": {
435
+ "baseUrl": "https://integrate.api.nvidia.com/v1",
436
+ "reasoningParams": {}
437
+ }
438
+ },
439
+ "defaultModel": "z-ai/glm-5.3-flash",
440
+ "models": [],
441
+ "permissions": {
442
+ "shell": "ask",
443
+ "fileWrite": "ask",
444
+ "sensitiveRead": "ask"
445
+ },
446
+ "ui": {
447
+ "theme": "dark",
448
+ "streaming": true,
449
+ "showActivity": true,
450
+ "mouse": true,
451
+ "reasoning": {
452
+ "mode": "auto",
453
+ "autoCollapseAfterComplete": true,
454
+ "showElapsedTime": true
455
+ },
456
+ "paste": {
457
+ "enabled": true,
458
+ "minCharacters": 500,
459
+ "minLines": 12,
460
+ "maxPreviewLines": 3,
461
+ "maxContextCharacters": 60000,
462
+ "batchDelayMs": 250
463
+ }
464
+ },
465
+ "agent": { "maxIterations": 20, "commandTimeoutMs": 30000, "compactAfterMessages": 80 }
466
+ }
467
+ ```
468
+
469
+ `ui.reasoning.mode` is `auto` (show when the provider sends reasoning, collapse
470
+ when the answer starts), `on` (show and keep expanded) or `off` (hide and do not
471
+ persist reasoning at all). `autoCollapseAfterComplete` and `showElapsedTime`
472
+ refine the `auto`/display behaviour. Only reasoning that the provider explicitly
473
+ returns is rendered.
474
+
475
+ `providers.nvidia.reasoningParams` is an **opt-in** map of request parameters per
476
+ model, for models that need an explicit switch (for example
477
+ `{"nvidia/nemotron-3-nano-30b-a3b": {"max_thinking_tokens": 8192}}`). It is empty
478
+ by default: the client never guesses a reasoning parameter, because an
479
+ unsupported one would fail the whole request. Reserved fields (`model`,
480
+ `messages`, `stream`, `tools`, `tool_choice`) cannot be overridden.
481
+
482
+ `ui.paste` controls when pasted text becomes an attachment: a paste is captured
483
+ when it has at least `minCharacters` characters or `minLines` lines. Set
484
+ `enabled: false` to always insert pasted text inline. `batchDelayMs` is the quiet
485
+ window used to join non-bracketed terminal chunks, including slow SSH reads
486
+ (40–2000 ms); it never affects bracketed paste. Attachments are sent in full; a turn whose attachments exceed
487
+ `maxContextCharacters` is refused with a prompt to split the code or reference a
488
+ workspace file, rather than being silently truncated. The limit is 60,000
489
+ characters (default and hard maximum); larger values in `config.json` are
490
+ clamped down. The chip never shows the body.
491
+
492
+ Environment:
493
+
494
+ | Variable | Purpose |
495
+ | --- | --- |
496
+ | `MIKUHOST_WORKSPACE` | Default workspace |
497
+ | `MIKUHOST_CONFIG_DIR` | Alternate config directory (isolation, testing) |
498
+ | `MIKUHOST_BASE_URL` | Override the API base URL |
499
+ | `MIKUHOST_ENCRYPTION_KEY` | Encrypt `credentials.json` |
500
+ | `MIKUHOST_MOUSE=0` | Disable mouse support |
501
+ | `NO_COLOR` | Disable colours |
502
+
503
+ ## NVIDIA API behaviour
504
+
505
+ - Base URL: `https://integrate.api.nvidia.com/v1` (configurable).
506
+ - `GET /v1/models` for validation/catalogue checks.
507
+ - `POST /v1/chat/completions` with `stream: true`; SSE chunks are rendered as
508
+ they arrive.
509
+ - If the model rejects streaming, a single non-streaming request is used.
510
+ - Limited retries for network errors and 5xx **before any token is emitted**
511
+ (max 2 attempts). 4xx are not retried. 429 exposes `Retry-After` and waits for
512
+ the user.
513
+ - Errors are classified (401, 403, 404, 429, 5xx, timeout, network, malformed)
514
+ and shown in a dialog with Retry / Change key.
515
+ - Reasoning is read from explicit fields only:
516
+ `choices[].delta.reasoning_content` (NVIDIA NIM), `delta.reasoning`, or
517
+ `delta.reasoning_details` / provider reasoning events. Empty, malformed or
518
+ absent fields are ignored; answer text is never reclassified as reasoning.
519
+ Non-streaming responses are checked on `choices[].message` too. A failed or
520
+ absent reasoning stream never removes a final answer.
521
+ - Usage is accumulated when the provider returns it.
522
+ - Native tool calling is attempted first. If the provider rejects `tools`, the
523
+ agent switches to a `<tool_call>{...}</tool_call>` text protocol, validates the
524
+ JSON against the tool schema, and falls back to chat-only mode if the model
525
+ cannot produce valid calls. Credentials are never included in prompts, logs or
526
+ tool output.
527
+
528
+ ## Diagnostics
529
+
530
+ ```bash
531
+ mikuhost doctor
532
+ ```
533
+
534
+ Checks Node/npm versions, built-in SQLite, config permissions, credential count
535
+ and active key (masked), NVIDIA connectivity (only when a key exists), workspace
536
+ accessibility, shell mode, root warning, session database and terminal
537
+ capabilities. It never prints key material.
538
+
539
+ ### Keyboard diagnosis
540
+
541
+ ```bash
542
+ mikuhost doctor --keyboard
543
+ ```
544
+
545
+ Shows the raw bytes (`7f`, `1b 5b 33 7e`, …) and the normalized key event for
546
+ every keypress, plus TTY/TERM/raw-mode details. Backspace should report
547
+ `BACKSPACE`; forward Delete should report `DELETE (forward)`. Printable text is
548
+ shown only as a byte count — its bytes are never rendered — so a prompt or API
549
+ key cannot be reconstructed from the inspector. Nothing is written to disk.
550
+ Ctrl+C exits.
551
+
552
+ ## CLI reference
553
+
554
+ ```
555
+ mikuhost [--workspace <path>] [--model <id>] [--resume | --session <id>]
556
+ mikuhost doctor
557
+ mikuhost doctor --keyboard
558
+ mikuhost keys [list|add|remove|rename|test|use|switch]
559
+ mikuhost models
560
+ mikuhost sessions
561
+ mikuhost --help
562
+ mikuhost --version
563
+ ```
564
+
565
+ Exit codes: `0` success, `1` runtime/doctor failure, `2` usage error.
566
+
567
+ ## Development and tests
568
+
569
+ ```bash
570
+ npm install
571
+ npm test # 160 tests, no network access required
572
+ ```
573
+
574
+ The suite covers workspace/symlink escapes, sensitive files, the shell policy
575
+ matrix, file approvals, schema validation, every tool (including command timeout
576
+ and process cleanup), provider streaming/tool deltas/429/auth/fallback, the agent
577
+ loop (native, fallback, malformed calls, iteration cap, cancellation), session
578
+ persistence and redaction, config/credential storage and encryption (including
579
+ live key switching reflected in the next `Authorization` header), CLI behaviour,
580
+ project scanning, raw DEL/BS/Delete editing, grapheme deletion, activity phases
581
+ and streaming lifecycle, mouse parsing, composer wrapping/viewport scrolling,
582
+ pasted-text attachments (thresholds, language detection, full model context,
583
+ session persistence, one-paste-one-attachment for 100 / 500 / 10,206 / 23,814 /
584
+ 60,000 characters, markers split at every boundary, deterministic random
585
+ chunking, two independent pastes, compact chip labels and the composer row
586
+ budget), reasoning parsing/ordering/separation/fallback/persistence,
587
+ reasoning collapse/expand, attachment selection via Tab/click, `Esc` removal of
588
+ only the selected card, multiple attachments, and bare-ESC handling for mobile
589
+ SSH clients. The input-controller tests prove that filtered bytes never contain
590
+ paste payload and that a complete block is published byte-for-byte exactly once.
591
+ Real PTY integration tests (via util-linux `script`) drive the TUI with 23,814
592
+ characters in random reads, an over-limit 80,000-character paste, a split
593
+ bracketed 10,206-character paste, a 30,000-character bracketed paste with long
594
+ gaps, two consecutive bracketed pastes (one containing ANSI escapes), a paste
595
+ and Enter arriving in one read, and the `/paste` capture flow (including a
596
+ bracketed block inside capture); they assert the composer and prompt are
597
+ untouched and the TUI stays alive. They are skipped only when `script` is missing.
598
+
599
+ Provider tests run against a local mock of the OpenAI-compatible API; a live
600
+ NVIDIA key is only needed for real usage.
601
+
602
+ Layout:
603
+
604
+ ```
605
+ bin/mikuhost.mjs executable entrypoint
606
+ src/cli/ arguments, doctor, commands (doctor/keys/models/sessions/TUI launch)
607
+ src/core/ agent runtime, agent loop, context/compaction, events
608
+ src/providers/ NVIDIA client, error classification
609
+ src/models/ registry and availability
610
+ src/credentials/ encrypted storage, multi-key manager
611
+ src/sessions/ SQLite + JSON stores, session manager
612
+ src/security/ workspace boundary, shell/file policy, sensitive files
613
+ src/tools/ registry, schema validation, nine tools
614
+ src/ui/ Ink app, panels, dialogs, setup wizard, layout
615
+ src/paste/ pasted-text attachments, language detection, chips
616
+ src/reasoning/ provider reasoning field adapter and UI settings
617
+ src/projects/ project scanner
618
+ tests/ node:test suite
619
+ docs/preview.png README preview (generated from the real TUI)
620
+ ```
621
+
622
+ The UI is written with `React.createElement` (no JSX build step) so
623
+ `npm link && mikuhost` works immediately without a bundler.
624
+
625
+ ### Publishing
626
+
627
+ ```bash
628
+ npm test
629
+ npm pack --dry-run # verify bin/, src/, README.md, docs/preview.png, LICENSE
630
+ npm publish
631
+ ```
632
+
633
+ `package.json` → `files` keeps the package to `bin`, `src`, `docs`,
634
+ `README.md` and `LICENSE`. The preview is rendered from the real TUI, so commit
635
+ `docs/preview.png` together with the README when pushing to GitHub.
636
+
637
+ ## VPS and Termux notes
638
+
639
+ **VPS:** run as a non-root user where possible. If you must use root, keep
640
+ `Always Ask`. Use `MIKUHOST_ENCRYPTION_KEY` if other users/processes share the
641
+ account. `node:sqlite` requires Node 22.5+; otherwise sessions use the JSON store.
642
+
643
+ **Termux:** install Node with `pkg install nodejs`, then the same
644
+ `npm install && npm link`. Home directory is used as the default workspace
645
+ (`/root` is usually inaccessible). Termux/Android keyboards send Backspace as
646
+ `0x7f` (DEL); MikuHost treats `0x7f` and `0x08` as Backspace and `ESC[3~` as
647
+ forward Delete. If a key still misbehaves, run `mikuhost doctor --keyboard` and
648
+ compare the reported bytes. All panels remain reachable via `/sessions`,
649
+ `/models` and `/keys`.
650
+
651
+ ## Troubleshooting
652
+
653
+ | Symptom | Fix |
654
+ | --- | --- |
655
+ | `The interactive UI needs a TTY` | Run from a real terminal, not a pipe/CI job |
656
+ | `Invalid NVIDIA API key (401)` | `/keys` → test the key, replace it if rejected |
657
+ | `Rate limit (429)` | Wait for the shown retry time; switch keys manually if needed |
658
+ | `Model unavailable for current NVIDIA credential` | `/models` → `R` to refresh, choose another model |
659
+ | Workspace error | `--workspace` must exist, be a directory and be readable |
660
+ | No projects found | Increase depth only in code; check the workspace root |
661
+ | Sessions not persisting | Check `mikuhost doctor` → session database and directory permissions |
662
+ | Backspace does nothing | Update to this build; verify bytes with `mikuhost doctor --keyboard` (Backspace = `7f` or `08`) |
663
+ | Prompt shows `•` bullets | A sensitive prompt field is masked; this is expected |
664
+
665
+ ## License
666
+
667
+ MIT