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 +69 -359
- package/dist/init.js +4 -3
- package/dist/llm/stream.js +78 -10
- package/dist/memory/extractor.js +44 -0
- package/dist/memory/store.js +41 -0
- package/dist/tools/index.js +5 -1
- package/dist/tui/InputBar.js +70 -3
- package/dist/tui/components/InputArea.js +32 -8
- package/dist/tui/deepThink.js +94 -0
- package/dist/tui/hooks/useRunLoop.js +8 -6
- package/dist/tui/hooks/useSession.js +48 -5
- package/dist/tui/printer.js +15 -7
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -1,412 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ๐ Miii CLI โ High-Performance Local AI Coding Agent
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+

|
|
18
6
|
|
|
19
7
|
[](https://www.npmjs.com/package/miii-cli)
|
|
20
8
|
[](https://www.npmjs.com/package/miii-cli)
|
|
21
9
|
[](LICENSE)
|
|
22
10
|
[](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
|
-
|
|
28
|
+
> โ
= supported | โ ๏ธ = partial | โ = not supported
|
|
55
29
|
|
|
56
|
-
|
|
57
|
-
npm install -g miii-cli
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**Requires:** Node.js 18+ and [Ollama](https://ollama.com)
|
|
30
|
+
## โก๏ธ Quick Start
|
|
61
31
|
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
## ๐ฅ Killer Features
|
|
200
50
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
59
|
+
## ๐ง Deep Think
|
|
207
60
|
|
|
208
|
-
|
|
61
|
+
Deep think is a two-phase research engine built into miii:
|
|
209
62
|
|
|
210
|
-
|
|
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
|
-
|
|
66
|
+
**Two ways to trigger:**
|
|
213
67
|
|
|
214
68
|
```
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
| `
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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 ||
|
|
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
|
}
|
package/dist/llm/stream.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/tui/InputBar.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
:
|
|
281
|
-
? '
|
|
282
|
-
: overlay === '
|
|
302
|
+
: pasteLines > 0
|
|
303
|
+
? 'backspace removes paste enter to send'
|
|
304
|
+
: overlay === 'command' && !commandQuery.includes(' ')
|
|
283
305
|
? 'โโ navigate enter select esc close'
|
|
284
|
-
:
|
|
285
|
-
? '
|
|
286
|
-
:
|
|
287
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
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 >
|
|
35
|
-
historyRef.current.splice(0, historyRef.current.length -
|
|
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
|
}
|
package/dist/tui/printer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
+
write(body + '\n');
|
|
165
173
|
}
|
|
166
174
|
export function systemMsg(text) {
|
|
167
|
-
|
|
175
|
+
write(gray(`โ ${text}`) + '\n');
|
|
168
176
|
}
|
|
169
177
|
export function errorMsg(text) {
|
|
170
|
-
|
|
178
|
+
write(gray(`error: ${text}`) + '\n');
|
|
171
179
|
}
|
|
172
180
|
export function divider() {
|
|
173
181
|
const cols = process.stdout.columns ?? 80;
|
|
174
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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
|
-
"
|
|
19
|
+
"ai-coding-assistant",
|
|
20
|
+
"local-llm",
|
|
21
21
|
"ollama",
|
|
22
|
-
"
|
|
23
|
-
"coding
|
|
24
|
-
"
|
|
25
|
-
"
|
|
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",
|