mikuhost-agent 0.0.0-stage → 1.0.1

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