miii-cli 0.2.8 โ†’ 0.3.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.
package/README.md CHANGED
@@ -1,412 +1,122 @@
1
- # miii
1
+ # ๐Ÿš€ Miii CLI โ€” High-Performance Local AI Coding Agent
2
2
 
3
- > Claude Code-level terminal AI โ€” runs on your machine, zero cloud required.
3
+ **The definitive local AI coding agent for your terminal. Automate complex engineering workflows with total control, zero cloud, and zero Python overhead.**
4
4
 
5
- ```
6
- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
7
- โ”‚ miii v0.2.8 โ”‚
8
- โ”‚ model: qwen2.5-coder:7b โ”‚
9
- โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
10
- โ”‚ โœฆ cross-referencing vibesโ€ฆ 12s โ”‚
11
- โ”‚ โš™ running patch_fileโ€ฆ โ”‚
12
- โ”‚ โš™ running run_testsโ€ฆ โ”‚
13
- โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
14
- โ”‚ โฏ โ–ˆ โ”‚
15
- โ”‚ @ file / command enter send ctrl+c exit โ”‚
16
- โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
17
- ```
5
+ ![MIII Demo](mii-cli.gif)
18
6
 
19
7
  [![npm version](https://img.shields.io/npm/v/miii-cli)](https://www.npmjs.com/package/miii-cli)
20
8
  [![npm downloads](https://img.shields.io/npm/dm/miii-cli)](https://www.npmjs.com/package/miii-cli)
21
9
  [![license](https://img.shields.io/npm/l/miii-cli)](LICENSE)
22
10
  [![node](https://img.shields.io/node/v/miii-cli)](https://nodejs.org)
23
11
 
24
- ---
25
-
26
- ## What is this
27
-
28
- A local AI coding assistant with the workflow depth of Claude Code โ€” file editing, multi-file refactors, test running, git integration, web search โ€” except it runs entirely on your machine using Ollama, or any OpenAI-compatible API.
29
-
30
- No Python. No cloud. No API key required to start. 176K bundle.
31
-
32
- ---
33
-
34
- ## Why it beats the alternatives
35
-
36
- | Feature | miii | aider | shell_gpt | open-interpreter |
37
- |---|:---:|:---:|:---:|:---:|
38
- | Ink terminal UI (not raw text) | โœ… | โŒ | โŒ | โŒ |
39
- | Zero Python | โœ… | โŒ | โŒ | โŒ |
40
- | Auto git context injection | โœ… | โœ… | โŒ | โŒ |
41
- | Multi-file refactor queue | โœ… | partial | โŒ | โŒ |
42
- | Context compaction (keeps local models on-track) | โœ… | โœ… | โŒ | โŒ |
43
- | Auto-runs tests after file edits | โœ… | โŒ | โŒ | โŒ |
44
- | Web search + extract (Tavily) | โœ… | โŒ | โŒ | partial |
45
- | npm skill plugin system | โœ… | โŒ | โŒ | โŒ |
46
- | Planning mode | โœ… | โŒ | โŒ | โŒ |
47
- | Named sessions + persistence | โœ… | โŒ | โŒ | โŒ |
48
- | `.miiiignore` | โœ… | โœ… | โŒ | โŒ |
49
- | Live model switching mid-session | โœ… | โŒ | โŒ | โŒ |
50
- | Bundle size | **176K** | ~50MB | ~40MB | ~100MB |
12
+ ## ๐Ÿ“Š How Miii Stacks Up
51
13
 
52
- ---
14
+ | Feature | **Miii** | Claude Code | Codex CLI | Aider |
15
+ |---|---|---|---|---|
16
+ | **Runs locally** | โœ… Ollama / any API | โŒ Cloud only | โŒ Cloud only | โœ… Local + cloud |
17
+ | **Code stays private** | โœ… Never leaves machine | โŒ Sent to Anthropic | โŒ Sent to OpenAI | โš ๏ธ Depends on model |
18
+ | **Cost** | ๐Ÿ†“ Free (your compute) | ๐Ÿ’ณ Pay per token | ๐Ÿ’ณ Pay per token | ๐Ÿ†“ Free (local) |
19
+ | **Runtime** | โšก TypeScript โ€” instant start | ๐Ÿ Node (fast) | ๐Ÿ Node | ๐Ÿข Python |
20
+ | **Deep Think mode** | โœ… Gather + synthesize | โŒ | โŒ | โŒ |
21
+ | **Auto-test loop** | โœ… Jest / Vitest / Mocha | โš ๏ธ Manual | โŒ | โš ๏ธ Manual |
22
+ | **Web search built-in** | โœ… Tavily | โŒ | โŒ | โŒ |
23
+ | **Surgical patch edits** | โœ… `patch_file` | โœ… | โš ๏ธ | โœ… |
24
+ | **Session memory** | โœ… Named, persistent | โœ… | โŒ | โš ๏ธ Basic |
25
+ | **Skill / plugin system** | โœ… npm + `.md` skills | โš ๏ธ MCP only | โŒ | โŒ |
26
+ | **Open source** | โœ… MIT | โŒ | โŒ | โœ… Apache 2.0 |
53
27
 
54
- ## Install
28
+ > โœ… = supported  |  โš ๏ธ = partial  |  โŒ = not supported
55
29
 
56
- ```bash
57
- npm install -g miii-cli
58
- ```
59
-
60
- **Requires:** Node.js 18+ and [Ollama](https://ollama.com)
30
+ ## โšก๏ธ Quick Start
61
31
 
62
- Or any OpenAI-compatible API โ€” see [configuration](#configuration).
63
-
64
- ---
65
-
66
- ## Quick start
32
+ Get up and running in 30 seconds:
67
33
 
68
34
  ```bash
69
- ollama serve
70
35
  ollama pull qwen2.5-coder:7b
36
+ npm install -g miii-cli
71
37
  miii
72
38
  ```
73
39
 
74
- Model picker opens on launch. Select a model. Start coding.
75
-
76
- ```bash
77
- miii # new session, named from first message
78
- miii --model qwen2.5-coder # specific model
79
- miii --session myproject # named session
80
- miii -s work -m codellama # short flags
81
- ```
82
-
83
- miii checks for updates on startup and lets you know when a new version is available:
84
-
85
- ```
86
- โ”œโ”€โ”€ miii v0.2.7 โ†’ v0.2.8 available run: npm install -g miii-cli โ”€โ”€โ”€โ”ค
87
- ```
88
-
89
- ---
90
-
91
- ## Auto git context
92
-
93
- miii watches `git status` and silently injects your changed files into context โ€” before you even type `@file`.
94
-
95
- ```
96
- โฏ fix the type error in the auth middleware
97
-
98
- [auto-loaded 3 changed file(s)]
99
- ```
100
-
101
- Smart enough to skip it for non-code questions. Deduped โ€” same files don't re-inject unless they change on disk. Disable per-project:
102
-
103
- ```json
104
- { "gitContext": false }
105
- ```
106
-
107
- ---
108
-
109
- ## Multi-file refactor
110
-
111
- One goal, executed across the whole codebase:
112
-
113
- ```
114
- /refactor extract all database queries into a repository layer
115
- /refactor rename UserService to AccountService everywhere
116
- /refactor add input validation to all API route handlers
117
- ```
118
-
119
- How it works: model plans which files change โ†’ reads all in parallel โ†’ per-file LLM call with isolated context โ†’ writes queued changes โ†’ runs tests. Each file gets its own fresh context so local models never lose the thread.
120
-
121
- ---
122
-
123
- ## Auto-test after edits
124
-
125
- Every time the model edits a file, miii runs your test suite automatically and feeds results back into the conversation โ€” without you asking.
126
-
127
- ```
128
- โš™ running run_testsโ€ฆ
129
- โ— src/auth/middleware.test.ts โ€” 2 tests failed
130
- ```
131
-
132
- Model sees the failures and fixes them on the next hop. Supports jest, vitest, mocha โ€” auto-detected from `package.json`.
133
-
134
- ---
135
-
136
- ## Web search
137
-
138
- Add a Tavily key and the model can search the web and scrape pages as tools, mid-conversation:
139
-
140
- ```bash
141
- /tavily-key tvly-your-key-here
142
- ```
143
-
144
- Get a free key at [tavily.com](https://tavily.com) โ€” 1000 free searches/month.
145
-
146
- ```
147
- โฏ what's the latest breaking change in React 19?
148
- โฏ find the docs for the Hono.js routing API and implement it here
149
- โฏ search for the error: "Cannot read properties of undefined (reading 'map')"
150
- ```
151
-
152
- Tools available to the model: `web_search` (semantic search, configurable depth) and `web_extract` (scrape and summarize any URL). API key stored at `~/.config/miii/tavily.key` with mode 600.
153
-
154
- ---
155
-
156
- ## npm skill ecosystem
157
-
158
- Write your own:
159
-
160
- ```typescript
161
- // miii-skill-mytool/index.js
162
- export default {
163
- name: 'mytool',
164
- ns: 'custom',
165
- description: 'does something useful',
166
- execute: async (args, ctx) => {
167
- ctx.setSystemPrompt(ctx.getSystemPrompt() + '\nExtra context here.')
168
- return 'skill activated'
169
- }
170
- }
171
- ```
172
-
173
- Markdown skills still work too โ€” drop a `.md` file in `~/.config/miii/skills/` and it becomes a `/command` instantly.
174
-
175
- ---
176
-
177
- ## Planning mode
178
-
179
- Think before you code:
180
-
181
- ```
182
- /plan add OAuth2 to this Express app
183
- /plan refactor the frontend to use React Query
184
- ```
185
-
186
- Switches the model into a structured planning mode โ€” no code, just questions, breakdowns, and concrete steps. Then:
187
-
188
- ```
189
- /plan:next next concrete steps
190
- /plan:breakdown break into subtasks
191
- /plan:review critique the plan so far
192
- /plan:done exit, go build
193
- ```
40
+ ## ๐Ÿง  Why Miii?
194
41
 
195
- ---
42
+ Most AI coding tools are either heavy Python wrappers or expensive monthly subscriptions that send your code to the cloud. **miii is different.**
196
43
 
197
- ## File context with `@`
44
+ - **Local-First & Private**: Runs on Ollama or any OpenAI-compatible API. Your code never leaves your machine, ensuring 100% privacy and security.
45
+ - **Blazing Fast**: Built with TypeScript for near-instant startup. No heavy Python runtime overhead. Tiny footprint, massive power.
46
+ - **Fully Autonomous**: Miii doesn't just suggest code; it acts as a junior engineerโ€”editing files, running your test suite, and iterating until the bugs are gone.
47
+ - **Deep Context Awareness**: Automatically analyzes git diffs and project architecture, eliminating the need for manual copy-pasting.
198
48
 
199
- Type `@` anywhere to fuzzy-search and inject any project file into context:
49
+ ## ๐Ÿ”ฅ Killer Features
200
50
 
201
- ```
202
- โฏ review the auth logic in @src/auth/middleware.ts
203
- โฏ what does @src/utils/parser.ts return when input is empty?
204
- ```
51
+ - **๐Ÿ›  Precision Editing**: Using `patch_file`, miii makes surgical changes without rewriting entire files.
52
+ - **๐Ÿ”„ Auto-Test Loop**: Miii runs your Jest/Vitest/Mocha tests after every edit. If it breaks, it fixes itself.
53
+ - **๐ŸŒ Web Intelligence**: Integrated `web_search` and `web_extract` via Tavily for real-time documentation.
54
+ - **๐Ÿง  Deep Think**: Two-phase research mode โ€” gathers from files, git, and web first, then synthesizes a complete answer. Available as `/think <query>` or as a tool the LLM calls autonomously.
55
+ - **๐Ÿ“ Planning Mode**: Use `/plan` to architect a solution before a single line of code is written.
56
+ - **๐Ÿ“‚ Session Memory**: Every conversation is auto-named and persisted. Resume your work instantly with `miii --session feature-auth`.
57
+ - **๐Ÿ“ฆ Skill System**: Extend miii with npm skill plugins or custom `.md` files.
205
58
 
206
- Files auto-excluded: `node_modules`, `dist`, `.git`, lock files, binaries, images.
59
+ ## ๐Ÿง  Deep Think
207
60
 
208
- ---
61
+ Deep think is a two-phase research engine built into miii:
209
62
 
210
- ## `.miiiignore`
63
+ 1. **Gather phase** โ€” runs a constrained inner loop with read-only tools: `read_file`, `list_files`, `git_status`, `git_log`, `git_diff`, `web_search`, `web_extract`. Guardrails enforce a hard cap of 6 tool calls and 4 web calls. No file writes, no shell mutations.
64
+ 2. **Synthesize phase** โ€” gathered findings feed into the main run loop for a complete, grounded answer.
211
65
 
212
- Exclude files from `@` fuzzy picker and git auto-context:
66
+ **Two ways to trigger:**
213
67
 
214
68
  ```
215
- # .miiiignore
216
- secrets/
217
- *.generated.ts
218
- fixtures/
219
- *.sql
69
+ /think how does the auth middleware handle token expiry?
70
+ /think what does this codebase do and how is it structured?
71
+ /think latest breaking changes in react 19
220
72
  ```
221
73
 
222
- Supports exact names, relative paths, and `*.ext` glob patterns.
74
+ The LLM can also call `deep_think` autonomously mid-conversation when it decides a question needs multi-source research before answering.
223
75
 
224
- ---
76
+ > Requires a Tavily key (`/tavily-key tvly-...`) for web calls. File/git research works without it.
225
77
 
226
- ## Git integration
78
+ ## โŒจ๏ธ Command Cheat Sheet
227
79
 
228
- ```
229
- /git status working tree
230
- /git diff unstaged changes
231
- /git diff --staged staged diff
232
- /git log recent commits (n optional: /git log 20)
233
- /git review AI reviews current changes for bugs + improvements
234
- /git branch list branches
235
- /git commit <msg> stage all and commit
236
- ```
237
-
238
- The model also has `git_status`, `git_diff`, `git_log`, `git_commit` as autonomous tools โ€” it checks status and commits without being asked.
239
-
240
- ---
241
-
242
- ## All built-in tools
243
-
244
- The model calls these autonomously as needed:
245
-
246
- | Tool | What it does |
80
+ | Command | What it does |
247
81
  |---|---|
248
- | `read_file` | Read any file in cwd |
249
- | `list_files` | List directory, respects `.miiiignore` |
250
- | `create_file` | Create new file โ€” throws if already exists |
251
- | `edit_file` | Create or fully rewrite a file |
252
- | `patch_file` | Targeted string replace โ€” throws on ambiguous match |
253
- | `delete_file` | Delete a file |
254
- | `move_file` | Move or rename |
255
- | `create_folder` | mkdir -p |
256
- | `run_command` | Shell command, cwd, 30s timeout |
257
- | `run_tests` | Run test suite (jest/vitest/mocha auto-detected) |
258
- | `git_status` | Working tree status |
259
- | `git_diff` | Diff, staged or unstaged, 8K truncated |
260
- | `git_log` | Commit history |
261
- | `git_commit` | Stage + commit |
262
- | `web_search` | Tavily semantic search (requires API key) |
263
- | `web_extract` | Scrape + summarize URLs (requires API key) |
264
-
265
- Chains up to 6 tool hops per response โ€” read, edit, test, verify, commit in one shot.
266
-
267
- ---
268
-
269
- ## Sessions
270
-
271
- Every `miii` run starts a fresh session automatically. The session is named after your first message โ€” so `fix the auth bug` becomes the session `fix-the-auth-bug`. Use `--session` to resume a specific one.
272
-
273
- ```bash
274
- miii # new session every time, named from first message
275
- miii --session feature-auth # resumes or creates "feature-auth"
276
- ```
277
-
278
- ```
279
- /session <name> switch to a session (creates if new)
280
- /session delete <name> delete a saved session
281
- /sessions list all sessions with message counts
282
- /new fresh auto-named session
283
- /clear clear current session history
284
- ```
285
-
286
- Sessions at `~/.config/miii/sessions/`. History capped at 100 messages in-context, full history on disk. Debounced writes โ€” no I/O on every message.
287
-
288
- ---
289
-
290
- ## Context compaction
82
+ | `/think <query>` | Deep research: gather from files + web, then synthesize answer |
83
+ | `/refactor <goal>` | The powerhouse: plans, edits, and tests across your whole codebase |
84
+ | `/git <sub>` | Instant git status, diffs, and automated commit messages |
85
+ | `/plan` | Stop coding, start thinking (Structured Planning Mode) |
86
+ | `/model <name>` | Swap LLMs on the fly |
87
+ | `/tavily-key <key>` | Enable real-time web browsing |
88
+ | `/sessions` | Travel back in time to previous coding sessions |
291
89
 
292
- Local models lose coherence around 15โ€“20 messages. miii auto-compacts when context gets long: keeps system prompt + original goal + tool result summary + last 6 exchanges. You keep going without restarting. Session history always preserved on disk โ€” only the LLM window gets trimmed.
90
+ ## โš™๏ธ Configuration
293
91
 
294
- ---
92
+ Customise your experience in `.miii.json` or `~/.config/miii/config.json`:
295
93
 
296
- ## All commands
297
-
298
- Type `/` to open the command palette with fuzzy search.
299
-
300
- | Command | Description |
301
- |---|---|
302
- | `/model <name>` | Switch model mid-session โ€” no restart |
303
- | `/models` | Model picker, pull new Ollama models |
304
- | `/session <name>` | Switch or create session |
305
- | `/session delete <name>` | Delete a saved session |
306
- | `/sessions` | List all sessions |
307
- | `/new` | Fresh auto-named session |
308
- | `/clear` | Clear current history |
309
- | `/plan [topic]` | Planning mode |
310
- | `/refactor <goal>` | Multi-file refactor |
311
- | `/git <sub>` | Git commands |
312
- | `/skills <sub>` | Install / uninstall / list npm skills |
313
- | `/tavily-key <key>` | Set web search API key |
314
- | `/version` | Show current version |
315
- | `/list` | List all loaded skills |
316
- | `/exit` | Exit |
317
-
318
- ---
319
-
320
- ## Configuration
321
-
322
- Loaded in order from `.miii.json` (project) โ†’ `~/.config/miii/config.json` (global).
323
-
324
- **Ollama (default):**
325
- ```json
326
- {
327
- "model": "qwen2.5-coder:7b",
328
- "provider": "ollama",
329
- "baseUrl": "http://localhost:11434"
330
- }
331
- ```
332
-
333
- **Any OpenAI-compatible API** (LM Studio, vLLM, Groq, Together, OpenRouterโ€ฆ):
334
- ```json
335
- {
336
- "model": "gpt-4o",
337
- "provider": "openai-compat",
338
- "baseUrl": "https://api.openai.com/v1",
339
- "apiKey": "sk-..."
340
- }
341
- ```
342
-
343
- **All options:**
344
94
  ```json
345
95
  {
346
96
  "model": "qwen2.5-coder:7b",
347
97
  "provider": "ollama",
348
98
  "baseUrl": "http://localhost:11434",
349
- "apiKey": "",
350
99
  "gitContext": true,
351
- "tavilyApiKey": "tvly-...",
352
- "systemPrompt": "optional override"
100
+ "tavilyApiKey": "tvly-..."
353
101
  }
354
102
  ```
355
103
 
356
- ---
357
-
358
- ## Security
359
-
360
- | Threat | Defense |
361
- |---|---|
362
- | Path traversal (OWASP A01) | All file ops restricted to cwd via `guardPath()` |
363
- | `@file` injection | Refs validated against cwd before reading |
364
- | Session name injection | Names sanitized to alphanumeric + hyphens |
365
- | Shell injection (OWASP A03) | `run_command` enforces 30s hard timeout |
366
- | Config injection (OWASP A08) | Config key whitelist; session data validated as array |
367
- | API key exposure | Tavily key stored at `~/.config/miii/tavily.key` mode 600 |
368
-
369
- ---
370
-
371
- ## Keybindings
372
-
373
- | Key | Action |
374
- |---|---|
375
- | `enter` | Send |
376
- | `โ†‘ / โ†“` | Navigate command palette or file picker |
377
- | `esc` | Close overlay / abort in-flight request |
378
- | `ctrl+c` | Abort current request or exit |
379
- | `backspace` | Remove pasted content chip |
380
-
381
- ## Paste detection
382
-
383
- Paste a large file or code block and miii collapses it into a chip instead of flooding the input:
384
-
385
- ```
386
- โฏ โŽ˜ pasted 84 lines
387
- backspace removes paste enter to send
388
- ```
389
-
390
- The full content is sent with your message when you press enter. Threshold: โ‰ฅ 3 lines or โ‰ฅ 200 characters.
391
-
392
- ---
393
-
394
- ## Build from source
104
+ ## ๐Ÿ›  Build from Source
395
105
 
396
106
  ```bash
397
107
  git clone https://github.com/maruakshay/miii-cli
398
- cd miii-cli
399
- npm install
400
- npm run build
401
- npm link
402
- npm test # 8 integration tests
108
+ cd miii-cli && npm install && npm run build && npm link
403
109
  ```
404
110
 
405
- ---
111
+ ## ๐ŸŒŸ Community & Philosophy
112
+
113
+ **Own your AI stack. Stop renting your intelligence. The future of coding is local.**
406
114
 
407
- ## What's new in 0.2.8
115
+ miii is built for the community. If this tool saves you hours of coding, help us grow:
116
+ - ๐ŸŒŸ **Star the repo** on GitHub
117
+ - ๐Ÿฆ **Share on X**
118
+ - ๐Ÿค– **Post on Reddit**
119
+ - ๐Ÿ’ฌ **Tell a fellow developer**
408
120
 
409
- - **Auto-named sessions** โ€” every run starts fresh; session named from first message (`fix-the-auth-bug`)
410
- - **Session delete** โ€” `/session delete <name>` to remove saved sessions
411
- - **Paste detection** โ€” large pastes collapse to `โŽ˜ pasted N lines` chip; full content sent on enter
412
- - **Thinking animation fix** โ€” messages and tool calls no longer bleed into the scrollback buffer
121
+ ## ๐Ÿ“œ License
122
+ MIT
package/dist/init.js CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
9
9
  import { loadConfig } from './config.js';
10
10
  import { SkillLoader } from './skills/loader.js';
11
11
  import { InputBar } from './tui/InputBar.js';
12
- import { welcome } from './tui/printer.js';
12
+ import { welcome, setInkInstance } from './tui/printer.js';
13
13
  import { ensureOllama } from './llm/ollama.js';
14
14
  const require = createRequire(import.meta.url);
15
15
  const UPDATE_CACHE = join(homedir(), '.config', 'miii', 'update-check.json');
@@ -89,7 +89,8 @@ export async function lazyInit() {
89
89
  ]);
90
90
  // Print welcome banner to scrollback BEFORE Ink starts
91
91
  welcome(config.provider, config.model, process.cwd(), currentVersion, updateAvailable, linked);
92
- const sessionName = argv.session || 'default';
93
- const { waitUntilExit } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion }), { exitOnCtrlC: false });
92
+ const sessionName = argv.session || `s-${Date.now()}`;
93
+ const { waitUntilExit, clear } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion }), { exitOnCtrlC: false });
94
+ setInkInstance(clear);
94
95
  await waitUntilExit();
95
96
  }
@@ -4,21 +4,57 @@ export async function chat(cfg) {
4
4
  return chatOllama(cfg);
5
5
  }
6
6
  async function chatOllama(cfg) {
7
- const { model, messages, baseUrl, signal, onDone, onError, onUsage } = cfg;
7
+ const { model, messages, baseUrl, signal, onDone, onError, onUsage, onChunk } = cfg;
8
8
  try {
9
9
  const res = await fetch(`${baseUrl}/api/chat`, {
10
10
  method: 'POST',
11
11
  headers: { 'Content-Type': 'application/json' },
12
- body: JSON.stringify({ model, messages, stream: false }),
12
+ body: JSON.stringify({ model, messages, stream: !!onChunk }),
13
13
  signal,
14
14
  });
15
15
  if (!res.ok) {
16
16
  onError(new Error(`Ollama ${res.status}: ${await res.text()}`));
17
17
  return;
18
18
  }
19
- const obj = await res.json();
20
- onUsage?.(obj?.prompt_eval_count ?? 0, obj?.eval_count ?? 0);
21
- await onDone(obj?.message?.content ?? '');
19
+ if (!onChunk) {
20
+ const obj = await res.json();
21
+ onUsage?.(obj?.prompt_eval_count ?? 0, obj?.eval_count ?? 0);
22
+ await onDone(obj?.message?.content ?? '');
23
+ return;
24
+ }
25
+ const reader = res.body.getReader();
26
+ const decoder = new TextDecoder();
27
+ let full = '';
28
+ let promptTokens = 0;
29
+ let completionTokens = 0;
30
+ let buf = '';
31
+ while (true) {
32
+ const { done, value } = await reader.read();
33
+ if (done)
34
+ break;
35
+ buf += decoder.decode(value, { stream: true });
36
+ const lines = buf.split('\n');
37
+ buf = lines.pop() ?? '';
38
+ for (const line of lines) {
39
+ if (!line.trim())
40
+ continue;
41
+ try {
42
+ const obj = JSON.parse(line);
43
+ const chunk = obj?.message?.content ?? '';
44
+ if (chunk) {
45
+ full += chunk;
46
+ onChunk(chunk);
47
+ }
48
+ if (obj?.done) {
49
+ promptTokens = obj.prompt_eval_count ?? 0;
50
+ completionTokens = obj.eval_count ?? 0;
51
+ }
52
+ }
53
+ catch { }
54
+ }
55
+ }
56
+ onUsage?.(promptTokens, completionTokens);
57
+ await onDone(full);
22
58
  }
23
59
  catch (err) {
24
60
  if (err?.name !== 'AbortError')
@@ -26,21 +62,53 @@ async function chatOllama(cfg) {
26
62
  }
27
63
  }
28
64
  async function chatOpenAI(cfg) {
29
- const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage } = cfg;
65
+ const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage, onChunk } = cfg;
30
66
  try {
31
67
  const res = await fetch(`${baseUrl}/v1/chat/completions`, {
32
68
  method: 'POST',
33
69
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey ?? 'local'}` },
34
- body: JSON.stringify({ model, messages }),
70
+ body: JSON.stringify({ model, messages, stream: !!onChunk }),
35
71
  signal,
36
72
  });
37
73
  if (!res.ok) {
38
74
  onError(new Error(`LLM ${res.status}: ${await res.text()}`));
39
75
  return;
40
76
  }
41
- const obj = await res.json();
42
- onUsage?.(obj?.usage?.prompt_tokens ?? 0, obj?.usage?.completion_tokens ?? 0);
43
- await onDone(obj?.choices?.[0]?.message?.content ?? '');
77
+ if (!onChunk) {
78
+ const obj = await res.json();
79
+ onUsage?.(obj?.usage?.prompt_tokens ?? 0, obj?.usage?.completion_tokens ?? 0);
80
+ await onDone(obj?.choices?.[0]?.message?.content ?? '');
81
+ return;
82
+ }
83
+ const reader = res.body.getReader();
84
+ const decoder = new TextDecoder();
85
+ let full = '';
86
+ let buf = '';
87
+ while (true) {
88
+ const { done, value } = await reader.read();
89
+ if (done)
90
+ break;
91
+ buf += decoder.decode(value, { stream: true });
92
+ const lines = buf.split('\n');
93
+ buf = lines.pop() ?? '';
94
+ for (const line of lines) {
95
+ if (!line.startsWith('data: '))
96
+ continue;
97
+ const data = line.slice(6).trim();
98
+ if (data === '[DONE]')
99
+ continue;
100
+ try {
101
+ const obj = JSON.parse(data);
102
+ const chunk = obj?.choices?.[0]?.delta?.content ?? '';
103
+ if (chunk) {
104
+ full += chunk;
105
+ onChunk(chunk);
106
+ }
107
+ }
108
+ catch { }
109
+ }
110
+ }
111
+ await onDone(full);
44
112
  }
45
113
  catch (err) {
46
114
  if (err?.name !== 'AbortError')
@@ -0,0 +1,44 @@
1
+ import { chat } from '../llm/stream.js';
2
+ const SYSTEM = `You extract memorable facts from conversations for long-term memory. Output ONLY a valid JSON array of concise fact strings.
3
+
4
+ Extract: user preferences, decisions made, key file paths, functions or variables, code patterns established, constraints, goals.
5
+ Skip: trivial exchanges, transient state, tool output noise.
6
+ Max 8 facts. Be specific and concrete.
7
+
8
+ Example output:
9
+ ["User prefers patch_file over full rewrites","entry point is src/index.ts","decided to use Zod for validation"]`;
10
+ export function extractFacts(messages, config, model) {
11
+ const lines = messages
12
+ .filter(m => m.role !== 'system')
13
+ .map(m => `${m.role}: ${m.content.slice(0, 400)}`)
14
+ .join('\n');
15
+ if (!lines.trim())
16
+ return Promise.resolve([]);
17
+ return new Promise(resolve => {
18
+ chat({
19
+ provider: config.provider,
20
+ model,
21
+ baseUrl: config.baseUrl,
22
+ apiKey: config.apiKey,
23
+ messages: [
24
+ { role: 'system', content: SYSTEM },
25
+ { role: 'user', content: lines },
26
+ ],
27
+ onDone(text) {
28
+ try {
29
+ const m = text.match(/\[[\s\S]*?\]/);
30
+ if (!m) {
31
+ resolve([]);
32
+ return;
33
+ }
34
+ const arr = JSON.parse(m[0]);
35
+ resolve(Array.isArray(arr) ? arr.filter((f) => typeof f === 'string') : []);
36
+ }
37
+ catch {
38
+ resolve([]);
39
+ }
40
+ },
41
+ onError() { resolve([]); },
42
+ });
43
+ });
44
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ const MEMORY_DIR = join(homedir(), '.config', 'miii', 'memory');
5
+ const MAX_FACTS = 200;
6
+ function ensureDir() {
7
+ mkdirSync(MEMORY_DIR, { recursive: true });
8
+ }
9
+ export function loadLongMemory(sessionName) {
10
+ ensureDir();
11
+ const p = join(MEMORY_DIR, `${sessionName}.json`);
12
+ if (!existsSync(p))
13
+ return [];
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(p, 'utf-8'));
16
+ return Array.isArray(parsed) ? parsed : [];
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ }
22
+ export function saveLongMemory(sessionName, facts) {
23
+ ensureDir();
24
+ writeFileSync(join(MEMORY_DIR, `${sessionName}.json`), JSON.stringify(facts));
25
+ }
26
+ export function mergeFacts(existing, newTexts) {
27
+ const existingSet = new Set(existing.map(f => f.text.toLowerCase()));
28
+ const ts = Date.now();
29
+ const added = newTexts
30
+ .filter(t => t.trim() && !existingSet.has(t.toLowerCase()))
31
+ .map(text => ({ text, ts }));
32
+ const merged = [...existing, ...added];
33
+ if (merged.length > MAX_FACTS)
34
+ merged.splice(0, merged.length - MAX_FACTS);
35
+ return merged;
36
+ }
37
+ export function formatMemoryBlock(facts) {
38
+ if (!facts.length)
39
+ return '';
40
+ return `\n\n[Long-term memory โ€” recalled from prior conversation]\n${facts.map(f => `- ${f.text}`).join('\n')}`;
41
+ }
@@ -235,6 +235,7 @@ export const tools = [
235
235
  ];
236
236
  export function getSystemPrompt(extra = '') {
237
237
  const toolDocs = tools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
238
+ const deepThinkDoc = `- deep_think({"query": "string", "needs_web": "boolean (optional)"}): Research tool โ€” gathers information from files, git, and optionally the web before answering. Returns a compiled research summary. Guardrails: read-only tools only, max 6 tool calls, max 4 web calls inside. Use when a question requires reading multiple files or searching the web first.`;
238
239
  return `You are Miii โ€” a fast, local AI coding assistant.
239
240
 
240
241
  Use tools by emitting:
@@ -265,6 +266,7 @@ replacement text
265
266
 
266
267
  Tools:
267
268
  ${toolDocs}
269
+ ${deepThinkDoc}
268
270
 
269
271
  Rules:
270
272
  - To modify an existing file: use patch_file with the exact old text and new replacement โ€” do NOT rewrite the whole file
@@ -284,5 +286,7 @@ Rules:
284
286
  - After editing files that have tests, call run_tests to verify nothing broke
285
287
  - If run_tests fails, read the failing test output and fix the code, then run_tests again (max 3 retries)
286
288
  - You have web_search and web_extract tools โ€” use them whenever the user asks about anything requiring internet access, current information, documentation, library versions, news, or external URLs
287
- - NEVER say you cannot search the web โ€” always call web_search instead${extra}`;
289
+ - NEVER say you cannot search the web โ€” always call web_search instead
290
+ - Use deep_think when the question requires gathering from multiple files or sources before you can answer well โ€” it runs a safe read-only research phase and returns a summary you can reason over
291
+ - deep_think cannot edit files or run shell commands โ€” it is purely for information gathering${extra}`;
288
292
  }
@@ -8,7 +8,7 @@ import { tools } from '../tools/index.js';
8
8
  import { readFile } from '../files/ops.js';
9
9
  import { generateId } from '../types.js';
10
10
  import * as printer from './printer.js';
11
- import { loadSession, saveSession, listSessions } from '../sessions.js';
11
+ import { loadSession, saveSession, listSessions, deleteSession } from '../sessions.js';
12
12
  import { MacroQueue, MicroQueue } from '../tasks/queue.js';
13
13
  import { TaskExecutor } from '../tasks/executor.js';
14
14
  import { fileEditContext } from '../tasks/compactor.js';
@@ -23,6 +23,7 @@ import { buildGitContext, looksCodeRelated } from './git-context.js';
23
23
  import { useSession } from './hooks/useSession.js';
24
24
  import { useModelPicker } from './hooks/useModelPicker.js';
25
25
  import { useRunLoop } from './hooks/useRunLoop.js';
26
+ import { runDeepThink } from './deepThink.js';
26
27
  const gitRun = promisify(exec);
27
28
  function buildAtContext(text) {
28
29
  const refs = [...text.matchAll(/@([\w./\-]+)/g)];
@@ -47,9 +48,20 @@ export function InputBar({ config, skills, cwd, session, version }) {
47
48
  const macroQueueRef = useRef(new MacroQueue());
48
49
  const executorRef = useRef(new TaskExecutor(tools));
49
50
  const lastGitStatusRef = useRef('');
50
- const { setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, buildContext, } = useSession(session, cwd, config);
51
+ const abortRef = useRef(null);
52
+ const { setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, buildContext, renameFromMessage, } = useSession(session, cwd, config);
51
53
  const { currentModel, setCurrentModel, currentModelRef, pickerOpen, setPickerOpen, pickerModels, pickerLoading, pickerError, pullState, openPicker, handleModelSelect, handleModelPull, } = useModelPicker(config);
52
- const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, abortRef, runLoop, handleAbort, } = useRunLoop(config, currentModelRef, pushHistory);
54
+ const deepThinkTool = useMemo(() => ({
55
+ name: 'deep_think',
56
+ description: 'Research tool: gather info from files and web before answering.',
57
+ params: '{"query": "string", "needs_web": "boolean (optional)"}',
58
+ execute: async ({ query }) => {
59
+ const result = await runDeepThink(String(query), config, currentModelRef.current, abortRef.current?.signal);
60
+ return `Research complete (${result.toolCalls} tool calls, ${result.webCalls} web):\n\n${result.findings}`;
61
+ },
62
+ }), [config]);
63
+ const allTools = useMemo(() => [...tools, deepThinkTool], [deepThinkTool]);
64
+ const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
53
65
  // โ”€โ”€โ”€ refactor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
54
66
  const runRefactor = useCallback(async (goal) => {
55
67
  printer.systemMsg(`refactor: ${goal}`);
@@ -348,6 +360,41 @@ export function InputBar({ config, skills, cwd, session, version }) {
348
360
  await runRefactor(goal);
349
361
  return;
350
362
  }
363
+ if (cmd.startsWith('/think ') || cmd === '/think') {
364
+ const query = cmd.slice(6).trim();
365
+ if (!query) {
366
+ printer.systemMsg('usage: /think <query>');
367
+ return;
368
+ }
369
+ printer.userMsg(`/think ${query}`);
370
+ setStatus('thinking');
371
+ setTaskLabel(`gathering: ${query}`);
372
+ abortRef.current = new AbortController();
373
+ try {
374
+ const result = await runDeepThink(query, config, currentModelRef.current, abortRef.current.signal, (toolName) => setCurrentTool(`gather:${toolName}`));
375
+ setCurrentTool(undefined);
376
+ printer.systemMsg(`gathered: ${result.toolCalls} tool call(s), ${result.webCalls} web call(s)`);
377
+ if (result.findings) {
378
+ pushHistory({ role: 'user', content: `/think ${query}` });
379
+ pushHistory({ role: 'assistant', content: result.findings });
380
+ pushHistory({ role: 'user', content: `Based on your research above, give a complete answer to: ${query}` });
381
+ await runLoop(buildContext(), 0, query);
382
+ }
383
+ else {
384
+ printer.systemMsg('nothing gathered โ€” try rephrasing');
385
+ setStatus('idle');
386
+ }
387
+ }
388
+ catch (e) {
389
+ printer.errorMsg(`deep think failed: ${e}`);
390
+ setStatus('idle');
391
+ }
392
+ finally {
393
+ setCurrentTool(undefined);
394
+ setTaskLabel(undefined);
395
+ }
396
+ return;
397
+ }
351
398
  if (cmd === '/plan' || cmd.startsWith('/plan ')) {
352
399
  const topic = cmd.slice(5).trim();
353
400
  setPlanningMode(true);
@@ -394,6 +441,25 @@ export function InputBar({ config, skills, cwd, session, version }) {
394
441
  printer.systemMsg(`current: ${sessionNameRef.current}`);
395
442
  return;
396
443
  }
444
+ if (arg.startsWith('delete ')) {
445
+ const target = arg.slice(7).trim();
446
+ if (!target) {
447
+ printer.systemMsg('usage: /session delete <name>');
448
+ return;
449
+ }
450
+ if (target === sessionNameRef.current) {
451
+ printer.systemMsg('cannot delete active session โ€” switch first');
452
+ return;
453
+ }
454
+ try {
455
+ deleteSession(target);
456
+ printer.systemMsg(`deleted: ${target}`);
457
+ }
458
+ catch (e) {
459
+ printer.errorMsg(`delete failed: ${String(e)}`);
460
+ }
461
+ return;
462
+ }
397
463
  if (saveTimerRef.current) {
398
464
  clearTimeout(saveTimerRef.current);
399
465
  saveTimerRef.current = null;
@@ -434,6 +500,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
434
500
  printer.systemMsg(`unknown command: /${slashCmd} โ€” try /list`);
435
501
  return;
436
502
  }
503
+ renameFromMessage(text);
437
504
  const contextPrefix = buildAtContext(text);
438
505
  const shouldInjectGit = config.gitContext !== false && looksCodeRelated(text);
439
506
  const { prefix: gitPrefix, label: gitLabel } = shouldInjectGit
@@ -32,11 +32,15 @@ const PLANNING_COMMANDS = [
32
32
  { ns: 'plan', name: 'review', description: 'review and critique the plan so far' },
33
33
  { ns: 'plan', name: 'done', description: 'exit planning mode' },
34
34
  ];
35
+ const PASTE_MIN_LINES = 3;
36
+ const PASTE_MIN_CHARS = 200;
35
37
  export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort }) {
36
38
  const [lines, setLines] = useState(['']);
37
39
  const [cursor, setCursor] = useState({ row: 0, col: 0 });
38
40
  const [overlay, setOverlay] = useState('none');
39
41
  const [overlayIdx, setOverlayIdx] = useState(0);
42
+ const [pasteLines, setPasteLines] = useState(0);
43
+ const pasteRef = useRef(null);
40
44
  const [files, setFiles] = useState([]);
41
45
  const filesLoadedRef = useRef(false);
42
46
  // built-ins first, then loaded skills (deduplicated by name)
@@ -87,6 +91,8 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
87
91
  setCursor({ row: 0, col: 0 });
88
92
  setOverlay('none');
89
93
  setOverlayIdx(0);
94
+ pasteRef.current = null;
95
+ setPasteLines(0);
90
96
  }
91
97
  function appendChar(ch) {
92
98
  setLines(prev => {
@@ -200,7 +206,11 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
200
206
  // backspace/typing falls through to normal handling below
201
207
  }
202
208
  if (key.return) {
203
- const text = fullInput.trim();
209
+ const typed = fullInput.trim();
210
+ const pasted = pasteRef.current;
211
+ const text = pasted
212
+ ? typed ? `${typed}\n${pasted}` : pasted
213
+ : typed;
204
214
  if (text) {
205
215
  clearInput();
206
216
  onSubmit(text);
@@ -208,6 +218,11 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
208
218
  return;
209
219
  }
210
220
  if (key.backspace || key.delete) {
221
+ if (pasteRef.current) {
222
+ pasteRef.current = null;
223
+ setPasteLines(0);
224
+ return;
225
+ }
211
226
  deleteChar();
212
227
  // Recompute overlay trigger for updated input
213
228
  const r = cursor.row;
@@ -245,6 +260,13 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
245
260
  return;
246
261
  }
247
262
  if (input && !key.ctrl && !key.meta) {
263
+ // Detect paste: Ink delivers entire pasted chunk as one input string
264
+ const lineCount = input.split('\n').length;
265
+ if (input.length > 1 && (lineCount >= PASTE_MIN_LINES || input.length >= PASTE_MIN_CHARS)) {
266
+ pasteRef.current = input;
267
+ setPasteLines(lineCount);
268
+ return;
269
+ }
248
270
  // Compute prospective new input to decide overlay
249
271
  const r = cursor.row;
250
272
  const col = cursor.col;
@@ -277,14 +299,16 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
277
299
  const borderColor = isProcessing ? 'yellow' : 'cyan';
278
300
  const hint = isProcessing
279
301
  ? 'esc to abort'
280
- : overlay === 'command' && !commandQuery.includes(' ')
281
- ? 'โ†‘โ†“ navigate enter select esc close'
282
- : overlay === 'at'
302
+ : pasteLines > 0
303
+ ? 'backspace removes paste enter to send'
304
+ : overlay === 'command' && !commandQuery.includes(' ')
283
305
  ? 'โ†‘โ†“ navigate enter select esc close'
284
- : planningMode
285
- ? '๐Ÿ“‹ planning mode / suggestions enter send /plan:done to exit'
286
- : '@ file / command enter send ctrl+c exit';
287
- return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsxs(Box, { borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: borderColor, bold: true, children: 'โฏ ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: lines.length === 1 && !lines[0] ? (_jsx(Text, { color: isActive ? 'white' : 'gray', dimColor: isProcessing, children: isActive ? 'โ–ˆ' : 'processing...' })) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
306
+ : overlay === 'at'
307
+ ? 'โ†‘โ†“ navigate enter select esc close'
308
+ : planningMode
309
+ ? '๐Ÿ“‹ planning mode / suggestions enter send /plan:done to exit'
310
+ : '@ file / command enter send ctrl+c exit';
311
+ return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsxs(Box, { borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: borderColor, bold: true, children: 'โฏ ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: pasteLines > 0 ? (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " lines"] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] })) : lines.length === 1 && !lines[0] ? (_jsx(Text, { color: isActive ? 'white' : 'gray', dimColor: isProcessing, children: isActive ? 'โ–ˆ' : 'processing...' })) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
288
312
  ? renderLineWithCursor(line, cursor.col, isActive)
289
313
  : line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: hint })] })] }));
290
314
  }
@@ -0,0 +1,94 @@
1
+ import { chat } from '../llm/stream.js';
2
+ import { tools as staticTools } from '../tools/index.js';
3
+ import { StreamParser } from '../parser/stream-parser.js';
4
+ const ALLOWED_TOOLS = new Set([
5
+ 'read_file', 'list_files', 'web_search', 'web_extract',
6
+ 'git_status', 'git_log', 'git_diff',
7
+ ]);
8
+ const MAX_DEPTH = 6;
9
+ const MAX_WEB = 4;
10
+ export async function runDeepThink(query, config, model, signal, onStep) {
11
+ const gatherTools = staticTools.filter(t => ALLOWED_TOOLS.has(t.name));
12
+ const toolDocs = gatherTools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
13
+ const sysPrompt = `You are a research agent. Gather information to answer: "${query}"
14
+
15
+ Available tools (read-only โ€” no file writes, no mutations):
16
+ ${toolDocs}
17
+
18
+ Guardrails:
19
+ - Max ${MAX_DEPTH} tool calls total
20
+ - Max ${MAX_WEB} web calls (web_search + web_extract combined)
21
+ - No file edits, no shell commands that modify state
22
+ - When you have enough info, output a detailed plain-text research summary
23
+ - No markdown formatting in output`;
24
+ const messages = [
25
+ { role: 'system', content: sysPrompt },
26
+ { role: 'user', content: `Research and gather all relevant information for: ${query}` },
27
+ ];
28
+ let depth = 0;
29
+ let webCalls = 0;
30
+ let totalCalls = 0;
31
+ let findings = '';
32
+ async function gather(msgs) {
33
+ if (depth >= MAX_DEPTH)
34
+ return;
35
+ depth++;
36
+ let fullText = '';
37
+ await chat({
38
+ provider: config.provider,
39
+ model,
40
+ baseUrl: config.baseUrl,
41
+ apiKey: config.apiKey,
42
+ messages: msgs,
43
+ signal,
44
+ async onDone(text) { fullText = text; },
45
+ onError(err) { if (err.name !== 'AbortError')
46
+ throw err; },
47
+ });
48
+ if (!fullText)
49
+ return;
50
+ const pending = [];
51
+ const parser = new StreamParser();
52
+ for (const item of [...parser.feed(fullText), ...parser.flush()]) {
53
+ if (item.type === 'tool_call')
54
+ pending.push({ name: item.toolName, args: item.toolArgs });
55
+ }
56
+ if (!pending.length) {
57
+ findings = fullText;
58
+ return;
59
+ }
60
+ const next = [...msgs, { role: 'assistant', content: fullText }];
61
+ for (const tc of pending) {
62
+ if (!ALLOWED_TOOLS.has(tc.name)) {
63
+ next.push({ role: 'user', content: `Tool "${tc.name}" not permitted in research phase.` });
64
+ continue;
65
+ }
66
+ const isWeb = tc.name === 'web_search' || tc.name === 'web_extract';
67
+ if (isWeb && webCalls >= MAX_WEB) {
68
+ next.push({ role: 'user', content: `Web call limit (${MAX_WEB}) reached. Summarize findings now.` });
69
+ continue;
70
+ }
71
+ if (totalCalls >= MAX_DEPTH) {
72
+ next.push({ role: 'user', content: `Tool call limit (${MAX_DEPTH}) reached. Summarize findings now.` });
73
+ continue;
74
+ }
75
+ const tool = gatherTools.find(t => t.name === tc.name);
76
+ if (!tool)
77
+ continue;
78
+ onStep?.(tc.name);
79
+ totalCalls++;
80
+ if (isWeb)
81
+ webCalls++;
82
+ try {
83
+ const result = await tool.execute(tc.args);
84
+ next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
85
+ }
86
+ catch (e) {
87
+ next.push({ role: 'user', content: `Tool ${tc.name} error: ${e}` });
88
+ }
89
+ }
90
+ await gather(next);
91
+ }
92
+ await gather(messages);
93
+ return { findings, toolCalls: totalCalls, webCalls };
94
+ }
@@ -1,19 +1,20 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import { chat } from '../../llm/stream.js';
3
- import { tools } from '../../tools/index.js';
3
+ import { tools as staticTools } from '../../tools/index.js';
4
4
  import { StreamParser, extractBareToolCall } from '../../parser/stream-parser.js';
5
5
  import { shouldCompact, compactContext } from '../../tasks/compactor.js';
6
6
  import * as printer from '../printer.js';
7
7
  const MAX_TOOL_DEPTH = 6;
8
8
  const FILE_EDIT_TOOLS = new Set(['edit_file', 'create_file', 'patch_file', 'delete_file']);
9
9
  const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
10
- export function useRunLoop(config, currentModelRef, pushHistory) {
10
+ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = [], abortRef) {
11
11
  const [status, setStatus] = useState('idle');
12
12
  const [tick, setTick] = useState(0);
13
13
  const [currentTool, setCurrentTool] = useState();
14
14
  const [taskLabel, setTaskLabel] = useState();
15
- const abortRef = useRef(null);
16
15
  const thinkingStartRef = useRef(0);
16
+ const extraToolsRef = useRef(extraTools);
17
+ extraToolsRef.current = extraTools;
17
18
  const pushHistoryRef = useRef(pushHistory);
18
19
  useEffect(() => { pushHistoryRef.current = pushHistory; }, [pushHistory]);
19
20
  useEffect(() => {
@@ -74,7 +75,8 @@ export function useRunLoop(config, currentModelRef, pushHistory) {
74
75
  const next = [...msgs, { role: 'assistant', content: fullText }];
75
76
  try {
76
77
  for (const tc of pendingTools) {
77
- const tool = tools.find(t => t.name === tc.name);
78
+ const allTools = [...staticTools, ...extraToolsRef.current];
79
+ const tool = allTools.find(t => t.name === tc.name);
78
80
  setCurrentTool(tc.name);
79
81
  if (tool) {
80
82
  try {
@@ -102,7 +104,7 @@ export function useRunLoop(config, currentModelRef, pushHistory) {
102
104
  // Auto-run tests after file edits
103
105
  const didEditFiles = pendingTools.some(tc => FILE_EDIT_TOOLS.has(tc.name));
104
106
  if (didEditFiles) {
105
- const testTool = tools.find(t => t.name === 'run_tests');
107
+ const testTool = staticTools.find(t => t.name === 'run_tests');
106
108
  if (testTool) {
107
109
  setCurrentTool('run_tests');
108
110
  try {
@@ -140,7 +142,7 @@ export function useRunLoop(config, currentModelRef, pushHistory) {
140
142
  status, setStatus, tick,
141
143
  currentTool, setCurrentTool,
142
144
  taskLabel, setTaskLabel,
143
- thinkingStartRef, abortRef,
145
+ thinkingStartRef,
144
146
  runLoop, handleAbort,
145
147
  };
146
148
  }
@@ -1,16 +1,29 @@
1
1
  import { useState, useRef, useEffect } from 'react';
2
- import { loadSession, saveSession } from '../../sessions.js';
2
+ import { loadSession, saveSession, deleteSession } from '../../sessions.js';
3
3
  import { getSystemPrompt } from '../../tools/index.js';
4
4
  import { getTavilyKey, saveTavilyKey } from '../../tavily/client.js';
5
5
  import * as printer from '../printer.js';
6
+ import { loadLongMemory, saveLongMemory, mergeFacts, formatMemoryBlock } from '../../memory/store.js';
7
+ import { extractFacts } from '../../memory/extractor.js';
8
+ const SHORT_MEMORY_SIZE = 40;
9
+ function buildSystemPrompt(cwd, facts) {
10
+ return getSystemPrompt(`\n- CWD: ${cwd}`) + formatMemoryBlock(facts);
11
+ }
6
12
  export function useSession(initialSession, cwd, config) {
7
13
  const [sessionName, setSessionName] = useState(initialSession);
8
14
  const sessionNameRef = useRef(initialSession);
9
15
  const historyRef = useRef([]);
10
16
  const saveTimerRef = useRef(null);
11
- const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`));
17
+ const firstMessageSentRef = useRef(false);
18
+ const longMemoryRef = useRef([]);
19
+ const systemPromptRef = useRef(buildSystemPrompt(cwd, []));
12
20
  useEffect(() => { sessionNameRef.current = sessionName; }, [sessionName]);
13
21
  useEffect(() => {
22
+ const facts = loadLongMemory(initialSession);
23
+ longMemoryRef.current = facts;
24
+ systemPromptRef.current = buildSystemPrompt(cwd, facts);
25
+ if (facts.length)
26
+ printer.systemMsg(`long memory: ${facts.length} facts loaded`);
14
27
  const history = loadSession(initialSession);
15
28
  historyRef.current = history;
16
29
  if (history.length)
@@ -31,10 +44,40 @@ export function useSession(initialSession, cwd, config) {
31
44
  }
32
45
  function pushHistory(msg) {
33
46
  historyRef.current.push(msg);
34
- if (historyRef.current.length > 100)
35
- historyRef.current.splice(0, historyRef.current.length - 100);
47
+ if (historyRef.current.length > SHORT_MEMORY_SIZE) {
48
+ const dropped = historyRef.current.splice(0, historyRef.current.length - SHORT_MEMORY_SIZE);
49
+ extractFacts(dropped, config, config.model).then(newFacts => {
50
+ if (!newFacts.length)
51
+ return;
52
+ const updated = mergeFacts(longMemoryRef.current, newFacts);
53
+ longMemoryRef.current = updated;
54
+ systemPromptRef.current = buildSystemPrompt(cwd, updated);
55
+ saveLongMemory(sessionNameRef.current, updated);
56
+ });
57
+ }
36
58
  scheduleSave();
37
59
  }
60
+ function renameFromMessage(text) {
61
+ if (firstMessageSentRef.current)
62
+ return;
63
+ firstMessageSentRef.current = true;
64
+ const slug = text
65
+ .toLowerCase()
66
+ .split(/\s+/)
67
+ .slice(0, 5)
68
+ .join('-')
69
+ .replace(/[^\w-]/g, '')
70
+ .replace(/-+/g, '-')
71
+ .replace(/^-|-$/g, '')
72
+ .slice(0, 40) || 'chat';
73
+ const oldName = sessionNameRef.current;
74
+ sessionNameRef.current = slug;
75
+ setSessionName(slug);
76
+ try {
77
+ deleteSession(oldName);
78
+ }
79
+ catch { }
80
+ }
38
81
  function buildContext(extra) {
39
82
  const ctx = [{ role: 'system', content: systemPromptRef.current }];
40
83
  ctx.push(...historyRef.current);
@@ -45,6 +88,6 @@ export function useSession(initialSession, cwd, config) {
45
88
  return {
46
89
  sessionName, setSessionName, sessionNameRef,
47
90
  historyRef, saveTimerRef, systemPromptRef,
48
- pushHistory, buildContext,
91
+ pushHistory, buildContext, renameFromMessage,
49
92
  };
50
93
  }
@@ -1,4 +1,12 @@
1
1
  // ANSI-formatted stdout output โ€” goes into terminal scrollback
2
+ let _inkClear = null;
3
+ export function setInkInstance(clear) {
4
+ _inkClear = clear;
5
+ }
6
+ function write(s) {
7
+ _inkClear?.();
8
+ process.stdout.write(s);
9
+ }
2
10
  const R = '\x1b[0m';
3
11
  const BOLD = '\x1b[1m';
4
12
  const DIM = '\x1b[2m';
@@ -134,7 +142,7 @@ export function welcome(provider, model, cwd, version, updateAvailable, linked)
134
142
  }
135
143
  export function userMsg(text) {
136
144
  const atHighlighted = text.replace(/(@[\w./\-]+)/g, (m) => cyan(m));
137
- console.log(`\n${gray('>>')} ${atHighlighted}`);
145
+ write(`\n${gray('>>')} ${atHighlighted}\n`);
138
146
  }
139
147
  export function assistantMsg(text) {
140
148
  const content = formatContent(text);
@@ -146,14 +154,14 @@ export function assistantMsg(text) {
146
154
  return;
147
155
  const head = lines[idx].replace(/^ {2}/, '');
148
156
  const tail = lines.slice(idx + 1).join('\n');
149
- console.log(`\n${blue('โ—')} ${head}${tail ? '\n' + tail : ''}`);
157
+ write(`\n${blue('โ—')} ${head}${tail ? '\n' + tail : ''}\n`);
150
158
  }
151
159
  const EDIT_TOOLS = new Set(['edit_file', 'patch_file', 'create_file', 'write_file']);
152
160
  const DELETE_TOOLS = new Set(['delete_file', 'remove_file']);
153
161
  export function toolCallStart(name, args) {
154
162
  const summary = toolArgSummary(args);
155
163
  const dot = DELETE_TOOLS.has(name) ? red('โ—') : EDIT_TOOLS.has(name) ? green('โ—') : blue('โ—');
156
- process.stdout.write(` ${dot} ${cyan(name)}${summary ? gray('(' + summary + ')') : ''}\n`);
164
+ write(` ${dot} ${cyan(name)}${summary ? gray('(' + summary + ')') : ''}\n`);
157
165
  }
158
166
  export function toolMsg(name, result) {
159
167
  const preview = result.length > 250 ? result.slice(0, 250) + 'โ€ฆ' : result;
@@ -161,15 +169,15 @@ export function toolMsg(name, result) {
161
169
  ? preview.split('\n').map(l => gray(' ' + l)).join('\n')
162
170
  : '';
163
171
  if (body)
164
- console.log(body);
172
+ write(body + '\n');
165
173
  }
166
174
  export function systemMsg(text) {
167
- console.log(gray(`โ”€ ${text}`));
175
+ write(gray(`โ”€ ${text}`) + '\n');
168
176
  }
169
177
  export function errorMsg(text) {
170
- console.log(gray(`error: ${text}`));
178
+ write(gray(`error: ${text}`) + '\n');
171
179
  }
172
180
  export function divider() {
173
181
  const cols = process.stdout.columns ?? 80;
174
- process.stdout.write(`${gray('โ”€'.repeat(cols))}\n`);
182
+ write(`${gray('โ”€'.repeat(cols))}\n`);
175
183
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
- "description": "Claude Code-level terminal workflows powered by your local models.",
5
+ "description": "The high-performance local AI coding agent for your terminal. Automate complex workflows with local LLMs.",
6
6
  "license": "MIT",
7
7
  "engines": {
8
8
  "node": ">=18"
@@ -16,13 +16,16 @@
16
16
  "url": "https://github.com/maruakshay/miii-cli/issues"
17
17
  },
18
18
  "keywords": [
19
- "ai",
20
- "cli",
19
+ "ai-coding-assistant",
20
+ "local-llm",
21
21
  "ollama",
22
- "llm",
23
- "coding-assistant",
24
- "terminal",
25
- "local-ai"
22
+ "terminal-agent",
23
+ "autonomous-coding",
24
+ "cli-tool",
25
+ "developer-experience",
26
+ "open-source-ai",
27
+ "local-ai",
28
+ "software-engineering"
26
29
  ],
27
30
  "files": [
28
31
  "dist",