miii-cli 0.2.8 → 0.2.9
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 +50 -365
- package/package.json +11 -8
- package/dist/__tests__/integration.test.js +0 -50
- package/dist/tavily/client.js +0 -64
- package/dist/tui/git-context.js +0 -59
- package/dist/tui/hooks/useModelPicker.js +0 -63
- package/dist/tui/hooks/useRunLoop.js +0 -146
- package/dist/tui/hooks/useSession.js +0 -50
- package/dist/tui/thinking.js +0 -53
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
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
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/miii-cli)
|
|
8
|
+
[](https://www.npmjs.com/package/miii-cli)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://nodejs.org)
|
|
4
11
|
|
|
5
12
|
```
|
|
6
13
|
╭──────────────────────────────────────────────────────────────────────╮
|
|
@@ -11,402 +18,80 @@
|
|
|
11
18
|
│ ⚙ running patch_file… │
|
|
12
19
|
│ ⚙ running run_tests… │
|
|
13
20
|
├──────────────────────────────────────────────────────────────────────┤
|
|
14
|
-
│ ❯
|
|
15
|
-
│
|
|
21
|
+
│ ❯ ⎘ pasted 84 lines │
|
|
22
|
+
│ backspace removes paste enter to send │
|
|
16
23
|
╰──────────────────────────────────────────────────────────────────────╯
|
|
17
24
|
```
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
[](https://www.npmjs.com/package/miii-cli)
|
|
21
|
-
[](LICENSE)
|
|
22
|
-
[](https://nodejs.org)
|
|
23
|
-
|
|
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
|
-
---
|
|
26
|
+
## ⚡️ Quick Start
|
|
33
27
|
|
|
34
|
-
|
|
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 |
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## Install
|
|
28
|
+
Get up and running in 30 seconds:
|
|
55
29
|
|
|
56
30
|
```bash
|
|
57
|
-
npm install -g miii-cli
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**Requires:** Node.js 18+ and [Ollama](https://ollama.com)
|
|
61
|
-
|
|
62
|
-
Or any OpenAI-compatible API — see [configuration](#configuration).
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Quick start
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
ollama serve
|
|
70
31
|
ollama pull qwen2.5-coder:7b
|
|
32
|
+
npm install -g miii-cli
|
|
71
33
|
miii
|
|
72
34
|
```
|
|
73
35
|
|
|
74
|
-
|
|
36
|
+
## 🧠 Why Miii?
|
|
75
37
|
|
|
76
|
-
|
|
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
|
-
```
|
|
38
|
+
Most AI coding tools are either heavy Python wrappers or expensive monthly subscriptions that send your code to the cloud. **miii is different.**
|
|
82
39
|
|
|
83
|
-
|
|
40
|
+
- **Local-First & Private**: Runs on Ollama or any OpenAI-compatible API. Your code never leaves your machine, ensuring 100% privacy and security.
|
|
41
|
+
- **Blazing Fast**: Built with TypeScript for near-instant startup. No heavy Python runtime overhead. Tiny footprint, massive power.
|
|
42
|
+
- **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.
|
|
43
|
+
- **Deep Context Awareness**: Automatically analyzes git diffs and project architecture, eliminating the need for manual copy-pasting.
|
|
84
44
|
|
|
85
|
-
|
|
86
|
-
├── miii v0.2.7 → v0.2.8 available run: npm install -g miii-cli ───┤
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
---
|
|
45
|
+
## 🔥 Killer Features
|
|
90
46
|
|
|
91
|
-
|
|
47
|
+
- **🛠 Precision Editing**: Using `patch_file`, miii makes surgical changes without rewriting entire files.
|
|
48
|
+
- **🔄 Auto-Test Loop**: Miii runs your Jest/Vitest/Mocha tests after every edit. If it breaks, it fixes itself.
|
|
49
|
+
- **🌐 Web Intelligence**: Integrated `web_search` and `web_extract` via Tavily for real-time documentation.
|
|
50
|
+
- **📐 Planning Mode**: Use `/plan` to architect a solution before a single line of code is written.
|
|
51
|
+
- **📂 Session Memory**: Every conversation is auto-named and persisted. Resume your work instantly with `miii --session feature-auth`.
|
|
52
|
+
- **📦 Skill System**: Extend miii with npm skill plugins or custom `.md` files.
|
|
92
53
|
|
|
93
|
-
|
|
54
|
+
## ⌨️ Command Cheat Sheet
|
|
94
55
|
|
|
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
|
-
```
|
|
194
|
-
|
|
195
|
-
---
|
|
196
|
-
|
|
197
|
-
## File context with `@`
|
|
198
|
-
|
|
199
|
-
Type `@` anywhere to fuzzy-search and inject any project file into context:
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
❯ review the auth logic in @src/auth/middleware.ts
|
|
203
|
-
❯ what does @src/utils/parser.ts return when input is empty?
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Files auto-excluded: `node_modules`, `dist`, `.git`, lock files, binaries, images.
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## `.miiiignore`
|
|
211
|
-
|
|
212
|
-
Exclude files from `@` fuzzy picker and git auto-context:
|
|
213
|
-
|
|
214
|
-
```
|
|
215
|
-
# .miiiignore
|
|
216
|
-
secrets/
|
|
217
|
-
*.generated.ts
|
|
218
|
-
fixtures/
|
|
219
|
-
*.sql
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
Supports exact names, relative paths, and `*.ext` glob patterns.
|
|
223
|
-
|
|
224
|
-
---
|
|
225
|
-
|
|
226
|
-
## Git integration
|
|
227
|
-
|
|
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 |
|
|
247
|
-
|---|---|
|
|
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
|
|
291
|
-
|
|
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.
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
## All commands
|
|
297
|
-
|
|
298
|
-
Type `/` to open the command palette with fuzzy search.
|
|
299
|
-
|
|
300
|
-
| Command | Description |
|
|
56
|
+
| Command | What it does |
|
|
301
57
|
|---|---|
|
|
302
|
-
| `/
|
|
303
|
-
| `/
|
|
304
|
-
| `/
|
|
305
|
-
| `/
|
|
306
|
-
| `/
|
|
307
|
-
| `/
|
|
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
|
-
---
|
|
58
|
+
| `/refactor <goal>` | The powerhouse: plans, edits, and tests across your whole codebase |
|
|
59
|
+
| `/git <sub>` | Instant git status, diffs, and automated commit messages |
|
|
60
|
+
| `/plan` | Stop coding, start thinking (Structured Planning Mode) |
|
|
61
|
+
| `/model <name>` | Swap LLMs on the fly |
|
|
62
|
+
| `/tavily-key <key>` | Enable real-time web browsing |
|
|
63
|
+
| `/sessions` | Travel back in time to previous coding sessions |
|
|
319
64
|
|
|
320
|
-
## Configuration
|
|
65
|
+
## ⚙️ Configuration
|
|
321
66
|
|
|
322
|
-
|
|
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
|
-
```
|
|
67
|
+
Customise your experience in `.miii.json` or `~/.config/miii/config.json`:
|
|
342
68
|
|
|
343
|
-
**All options:**
|
|
344
69
|
```json
|
|
345
70
|
{
|
|
346
71
|
"model": "qwen2.5-coder:7b",
|
|
347
72
|
"provider": "ollama",
|
|
348
73
|
"baseUrl": "http://localhost:11434",
|
|
349
|
-
"apiKey": "",
|
|
350
74
|
"gitContext": true,
|
|
351
|
-
"tavilyApiKey": "tvly-..."
|
|
352
|
-
"systemPrompt": "optional override"
|
|
75
|
+
"tavilyApiKey": "tvly-..."
|
|
353
76
|
}
|
|
354
77
|
```
|
|
355
78
|
|
|
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
|
|
79
|
+
## 🛠 Build from Source
|
|
395
80
|
|
|
396
81
|
```bash
|
|
397
82
|
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
|
|
83
|
+
cd miii-cli && npm install && npm run build && npm link
|
|
403
84
|
```
|
|
404
85
|
|
|
405
|
-
|
|
86
|
+
## 🌟 Community & Philosophy
|
|
87
|
+
|
|
88
|
+
**Own your AI stack. Stop renting your intelligence. The future of coding is local.**
|
|
406
89
|
|
|
407
|
-
|
|
90
|
+
miii is built for the community. If this tool saves you hours of coding, help us grow:
|
|
91
|
+
- 🌟 **Star the repo** on GitHub
|
|
92
|
+
- 🐦 **Share on X**
|
|
93
|
+
- 🤖 **Post on Reddit**
|
|
94
|
+
- 💬 **Tell a fellow developer**
|
|
408
95
|
|
|
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
|
|
96
|
+
## 📜 License
|
|
97
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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",
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { looksCodeRelated } from '../tui/git-context.js';
|
|
5
|
-
import { tools } from '../tools/index.js';
|
|
6
|
-
// patch_file uses guardPath which restricts to CWD — use a local scratch file
|
|
7
|
-
const SCRATCH = join(process.cwd(), '.miii-test-scratch.txt');
|
|
8
|
-
// ─── looksCodeRelated ─────────────────────────────────────────────────────────
|
|
9
|
-
describe('looksCodeRelated', () => {
|
|
10
|
-
it('true: file extension in message', () => {
|
|
11
|
-
expect(looksCodeRelated('fix the bug in auth.ts')).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
it('true: code keyword present', () => {
|
|
14
|
-
expect(looksCodeRelated('refactor the user login function')).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
it('true: backtick token', () => {
|
|
17
|
-
expect(looksCodeRelated('what does `useEffect` do')).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
it('false: too short', () => {
|
|
20
|
-
expect(looksCodeRelated('hi')).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
it('false: plain prose, no code signal', () => {
|
|
23
|
-
expect(looksCodeRelated('what is the weather like in london today')).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
// ─── patch_file ───────────────────────────────────────────────────────────────
|
|
27
|
-
describe('patch_file', () => {
|
|
28
|
-
const patchTool = tools.find(t => t.name === 'patch_file');
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
try {
|
|
31
|
-
unlinkSync(SCRATCH);
|
|
32
|
-
}
|
|
33
|
-
catch { }
|
|
34
|
-
});
|
|
35
|
-
it('applies a unique patch correctly', async () => {
|
|
36
|
-
writeFileSync(SCRATCH, 'hello world\ngoodbye world\n');
|
|
37
|
-
await patchTool.execute({ path: SCRATCH, old: 'hello world', new: 'hello earth' });
|
|
38
|
-
expect(readFileSync(SCRATCH, 'utf-8')).toBe('hello earth\ngoodbye world\n');
|
|
39
|
-
});
|
|
40
|
-
it('throws when old text not found', async () => {
|
|
41
|
-
writeFileSync(SCRATCH, 'hello world\n');
|
|
42
|
-
await expect(patchTool.execute({ path: SCRATCH, old: 'no such text', new: 'x' }))
|
|
43
|
-
.rejects.toThrow('old text not found');
|
|
44
|
-
});
|
|
45
|
-
it('throws on ambiguous match (2+ occurrences)', async () => {
|
|
46
|
-
writeFileSync(SCRATCH, 'hello world\nhello world\n');
|
|
47
|
-
await expect(patchTool.execute({ path: SCRATCH, old: 'hello world', new: 'hi' }))
|
|
48
|
-
.rejects.toThrow('ambiguous');
|
|
49
|
-
});
|
|
50
|
-
});
|
package/dist/tavily/client.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
const KEY_FILE = join(homedir(), '.config', 'miii', 'tavily.key');
|
|
5
|
-
export function getTavilyKey() {
|
|
6
|
-
if (existsSync(KEY_FILE)) {
|
|
7
|
-
const k = readFileSync(KEY_FILE, 'utf-8').trim();
|
|
8
|
-
if (k)
|
|
9
|
-
return k;
|
|
10
|
-
}
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
export function saveTavilyKey(key) {
|
|
14
|
-
mkdirSync(join(homedir(), '.config', 'miii'), { recursive: true });
|
|
15
|
-
writeFileSync(KEY_FILE, key.trim(), { encoding: 'utf-8', mode: 0o600 });
|
|
16
|
-
}
|
|
17
|
-
async function post(path, body) {
|
|
18
|
-
const res = await fetch(`https://api.tavily.com${path}`, {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
headers: { 'Content-Type': 'application/json' },
|
|
21
|
-
body: JSON.stringify(body),
|
|
22
|
-
});
|
|
23
|
-
if (!res.ok) {
|
|
24
|
-
const text = await res.text().catch(() => '');
|
|
25
|
-
throw new Error(`Tavily API ${res.status}: ${text}`);
|
|
26
|
-
}
|
|
27
|
-
return res.json();
|
|
28
|
-
}
|
|
29
|
-
export async function tavilySearch(opts) {
|
|
30
|
-
const data = await post('/search', {
|
|
31
|
-
api_key: opts.apiKey,
|
|
32
|
-
query: opts.query,
|
|
33
|
-
search_depth: opts.searchDepth ?? 'basic',
|
|
34
|
-
max_results: Math.min(opts.maxResults ?? 5, 10),
|
|
35
|
-
include_answer: opts.includeAnswer ?? true,
|
|
36
|
-
include_raw_content: false,
|
|
37
|
-
include_domains: opts.includeDomains ?? [],
|
|
38
|
-
exclude_domains: opts.excludeDomains ?? [],
|
|
39
|
-
});
|
|
40
|
-
const parts = [];
|
|
41
|
-
if (data.answer)
|
|
42
|
-
parts.push(`Answer: ${data.answer}\n`);
|
|
43
|
-
for (const r of data.results) {
|
|
44
|
-
parts.push(`[${r.title}] ${r.url}\n${r.content}`);
|
|
45
|
-
}
|
|
46
|
-
return parts.join('\n\n').trim() || '(no results)';
|
|
47
|
-
}
|
|
48
|
-
export async function tavilyExtract(opts) {
|
|
49
|
-
const data = await post('/extract', {
|
|
50
|
-
api_key: opts.apiKey,
|
|
51
|
-
urls: opts.urls.slice(0, 20),
|
|
52
|
-
});
|
|
53
|
-
const parts = [];
|
|
54
|
-
for (const r of data.results) {
|
|
55
|
-
const truncated = r.raw_content.length > 8000
|
|
56
|
-
? r.raw_content.slice(0, 8000) + '\n…[truncated at 8k]'
|
|
57
|
-
: r.raw_content;
|
|
58
|
-
parts.push(`[${r.url}]\n${truncated}`);
|
|
59
|
-
}
|
|
60
|
-
if (data.failed_results?.length) {
|
|
61
|
-
parts.push(`Failed URLs: ${data.failed_results.map(r => r.url).join(', ')}`);
|
|
62
|
-
}
|
|
63
|
-
return parts.join('\n\n---\n\n').trim() || '(no content extracted)';
|
|
64
|
-
}
|
package/dist/tui/git-context.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { readFile } from '../files/ops.js';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { exec } from 'child_process';
|
|
4
|
-
import { promisify } from 'util';
|
|
5
|
-
const gitRun = promisify(exec);
|
|
6
|
-
const CODE_PATTERN = /\.(ts|js|tsx|jsx|py|go|rs|java|rb|sh|css|html|json|yaml|yml)\b|function|class|import|export|const|let|var|def |async|await|error|bug|fix|refactor|implement|`[^`]+`/i;
|
|
7
|
-
export function looksCodeRelated(text) {
|
|
8
|
-
return text.length >= 10 && CODE_PATTERN.test(text);
|
|
9
|
-
}
|
|
10
|
-
export async function buildGitContext(cwd, lastStatusRef) {
|
|
11
|
-
try {
|
|
12
|
-
const { stdout } = await gitRun('git status --short', { cwd, timeout: 5000 });
|
|
13
|
-
const status = stdout.trim();
|
|
14
|
-
if (!status || status === lastStatusRef.current)
|
|
15
|
-
return { prefix: '', label: '' };
|
|
16
|
-
lastStatusRef.current = status;
|
|
17
|
-
const MAX_TOTAL = 40_000;
|
|
18
|
-
const MAX_FILE = 15_000;
|
|
19
|
-
let total = 0;
|
|
20
|
-
const parts = [];
|
|
21
|
-
const skipped = [];
|
|
22
|
-
for (const line of status.split('\n')) {
|
|
23
|
-
const code = line.slice(0, 2);
|
|
24
|
-
if (code.includes('D'))
|
|
25
|
-
continue;
|
|
26
|
-
const raw = line.slice(3).trim().replace(/^"|"$/g, '');
|
|
27
|
-
const rel = raw.includes(' -> ') ? raw.split(' -> ')[1] : raw;
|
|
28
|
-
if (!rel)
|
|
29
|
-
continue;
|
|
30
|
-
try {
|
|
31
|
-
const content = readFile(resolve(cwd, rel));
|
|
32
|
-
if (!content || content.length > MAX_FILE) {
|
|
33
|
-
skipped.push(rel);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
total += content.length;
|
|
37
|
-
if (total > MAX_TOTAL) {
|
|
38
|
-
skipped.push(rel);
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
parts.push(`<file path="${rel}">\n${content}\n</file>`);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
skipped.push(rel);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (!parts.length && !skipped.length)
|
|
48
|
-
return { prefix: '', label: '' };
|
|
49
|
-
let prefix = '[Auto-context: git-changed files]\n' + parts.join('\n') + '\n';
|
|
50
|
-
if (skipped.length)
|
|
51
|
-
prefix += `Files changed but too large to auto-load: ${skipped.join(', ')}\n`;
|
|
52
|
-
prefix += '\n';
|
|
53
|
-
const label = `auto-loaded ${parts.length} changed file(s)${skipped.length ? `, skipped ${skipped.length} (too large)` : ''}`;
|
|
54
|
-
return { prefix, label };
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return { prefix: '', label: '' };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
-
import { listModels, pullModel } from '../../llm/ollama.js';
|
|
3
|
-
import * as printer from '../printer.js';
|
|
4
|
-
export function useModelPicker(config) {
|
|
5
|
-
const [currentModel, setCurrentModel] = useState(config.model);
|
|
6
|
-
const currentModelRef = useRef(config.model);
|
|
7
|
-
const [pickerOpen, setPickerOpen] = useState(true);
|
|
8
|
-
const [pickerModels, setPickerModels] = useState([]);
|
|
9
|
-
const [pickerLoading, setPickerLoading] = useState(false);
|
|
10
|
-
const [pickerError, setPickerError] = useState();
|
|
11
|
-
const [pullState, setPullState] = useState();
|
|
12
|
-
const pullAbortRef = useRef(null);
|
|
13
|
-
useEffect(() => { currentModelRef.current = currentModel; }, [currentModel]);
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
setPickerLoading(true);
|
|
16
|
-
listModels(config.baseUrl)
|
|
17
|
-
.then(m => { setPickerModels(m); setPickerLoading(false); })
|
|
18
|
-
.catch(e => { setPickerError(String(e)); setPickerLoading(false); });
|
|
19
|
-
}, []);
|
|
20
|
-
const openPicker = useCallback(async () => {
|
|
21
|
-
setPickerOpen(true);
|
|
22
|
-
setPickerLoading(true);
|
|
23
|
-
setPickerError(undefined);
|
|
24
|
-
try {
|
|
25
|
-
setPickerModels(await listModels(config.baseUrl));
|
|
26
|
-
}
|
|
27
|
-
catch (e) {
|
|
28
|
-
setPickerError(String(e));
|
|
29
|
-
}
|
|
30
|
-
finally {
|
|
31
|
-
setPickerLoading(false);
|
|
32
|
-
}
|
|
33
|
-
}, [config.baseUrl]);
|
|
34
|
-
const handleModelSelect = useCallback((name) => {
|
|
35
|
-
setCurrentModel(name);
|
|
36
|
-
currentModelRef.current = name;
|
|
37
|
-
setPickerOpen(false);
|
|
38
|
-
printer.systemMsg(`model → ${name}`);
|
|
39
|
-
}, []);
|
|
40
|
-
const handleModelPull = useCallback(async (name) => {
|
|
41
|
-
setPullState({ name, status: 'starting...', pct: undefined });
|
|
42
|
-
pullAbortRef.current = new AbortController();
|
|
43
|
-
try {
|
|
44
|
-
await pullModel(config.baseUrl, name, (s, p) => setPullState({ name, status: s, pct: p }), pullAbortRef.current.signal);
|
|
45
|
-
setPickerModels(await listModels(config.baseUrl));
|
|
46
|
-
setPullState(undefined);
|
|
47
|
-
setCurrentModel(name);
|
|
48
|
-
currentModelRef.current = name;
|
|
49
|
-
setPickerOpen(false);
|
|
50
|
-
printer.systemMsg(`pulled ${name} → active`);
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
setPullState(undefined);
|
|
54
|
-
setPickerError(`pull failed: ${e}`);
|
|
55
|
-
}
|
|
56
|
-
}, [config.baseUrl]);
|
|
57
|
-
return {
|
|
58
|
-
currentModel, setCurrentModel, currentModelRef,
|
|
59
|
-
pickerOpen, setPickerOpen,
|
|
60
|
-
pickerModels, pickerLoading, pickerError, pullState,
|
|
61
|
-
openPicker, handleModelSelect, handleModelPull,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
-
import { chat } from '../../llm/stream.js';
|
|
3
|
-
import { tools } from '../../tools/index.js';
|
|
4
|
-
import { StreamParser, extractBareToolCall } from '../../parser/stream-parser.js';
|
|
5
|
-
import { shouldCompact, compactContext } from '../../tasks/compactor.js';
|
|
6
|
-
import * as printer from '../printer.js';
|
|
7
|
-
const MAX_TOOL_DEPTH = 6;
|
|
8
|
-
const FILE_EDIT_TOOLS = new Set(['edit_file', 'create_file', 'patch_file', 'delete_file']);
|
|
9
|
-
const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
|
|
10
|
-
export function useRunLoop(config, currentModelRef, pushHistory) {
|
|
11
|
-
const [status, setStatus] = useState('idle');
|
|
12
|
-
const [tick, setTick] = useState(0);
|
|
13
|
-
const [currentTool, setCurrentTool] = useState();
|
|
14
|
-
const [taskLabel, setTaskLabel] = useState();
|
|
15
|
-
const abortRef = useRef(null);
|
|
16
|
-
const thinkingStartRef = useRef(0);
|
|
17
|
-
const pushHistoryRef = useRef(pushHistory);
|
|
18
|
-
useEffect(() => { pushHistoryRef.current = pushHistory; }, [pushHistory]);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (status === 'idle')
|
|
21
|
-
return;
|
|
22
|
-
const t = setInterval(() => setTick(n => n + 1), 80);
|
|
23
|
-
return () => clearInterval(t);
|
|
24
|
-
}, [status]);
|
|
25
|
-
const runLoop = useCallback(async (contextMsgs, depth = 0, goal) => {
|
|
26
|
-
if (depth >= MAX_TOOL_DEPTH) {
|
|
27
|
-
setStatus('idle');
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
setStatus('thinking');
|
|
31
|
-
if (depth === 0)
|
|
32
|
-
thinkingStartRef.current = Date.now();
|
|
33
|
-
const msgs = shouldCompact(contextMsgs) ? compactContext(contextMsgs, goal) : contextMsgs;
|
|
34
|
-
abortRef.current = new AbortController();
|
|
35
|
-
await chat({
|
|
36
|
-
provider: config.provider,
|
|
37
|
-
model: currentModelRef.current,
|
|
38
|
-
baseUrl: config.baseUrl,
|
|
39
|
-
messages: msgs,
|
|
40
|
-
signal: abortRef.current.signal,
|
|
41
|
-
async onDone(fullText) {
|
|
42
|
-
const pendingTools = [];
|
|
43
|
-
const textParts = [];
|
|
44
|
-
const parser = new StreamParser();
|
|
45
|
-
for (const item of [...parser.feed(fullText), ...parser.flush()]) {
|
|
46
|
-
if (item.type === 'tool_call')
|
|
47
|
-
pendingTools.push({ name: item.toolName, args: item.toolArgs });
|
|
48
|
-
else
|
|
49
|
-
textParts.push(item.content);
|
|
50
|
-
}
|
|
51
|
-
if (!pendingTools.length) {
|
|
52
|
-
const bare = extractBareToolCall(fullText);
|
|
53
|
-
if (bare)
|
|
54
|
-
pendingTools.push({ name: bare.name, args: bare.args });
|
|
55
|
-
}
|
|
56
|
-
const displayText = textParts.join('').trim();
|
|
57
|
-
if (displayText)
|
|
58
|
-
printer.assistantMsg(displayText);
|
|
59
|
-
pushHistoryRef.current({ role: 'assistant', content: fullText });
|
|
60
|
-
if (!pendingTools.length) {
|
|
61
|
-
const hasFencedCode = /```[\w]*\n[\s\S]{50,}?\n```/.test(fullText);
|
|
62
|
-
if (hasFencedCode && depth < MAX_TOOL_DEPTH - 1) {
|
|
63
|
-
const nudge = {
|
|
64
|
-
role: 'user',
|
|
65
|
-
content: 'You showed code in your response but did not use any file tools. Use edit_file or patch_file to actually write the changes to disk.',
|
|
66
|
-
};
|
|
67
|
-
await runLoop([...msgs, { role: 'assistant', content: fullText }, nudge], depth + 1, goal);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
setStatus('idle');
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
setStatus('tool');
|
|
74
|
-
const next = [...msgs, { role: 'assistant', content: fullText }];
|
|
75
|
-
try {
|
|
76
|
-
for (const tc of pendingTools) {
|
|
77
|
-
const tool = tools.find(t => t.name === tc.name);
|
|
78
|
-
setCurrentTool(tc.name);
|
|
79
|
-
if (tool) {
|
|
80
|
-
try {
|
|
81
|
-
printer.toolCallStart(tc.name, tc.args);
|
|
82
|
-
const result = await tool.execute(tc.args);
|
|
83
|
-
if (SHOW_RESULT_TOOLS.has(tc.name))
|
|
84
|
-
printer.toolMsg(tc.name, result);
|
|
85
|
-
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
86
|
-
}
|
|
87
|
-
catch (e) {
|
|
88
|
-
const err = `Tool ${tc.name} error: ${e}`;
|
|
89
|
-
printer.errorMsg(err);
|
|
90
|
-
next.push({ role: 'user', content: err });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
printer.errorMsg(`unknown tool: ${tc.name}`);
|
|
95
|
-
next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
setCurrentTool(undefined);
|
|
101
|
-
}
|
|
102
|
-
// Auto-run tests after file edits
|
|
103
|
-
const didEditFiles = pendingTools.some(tc => FILE_EDIT_TOOLS.has(tc.name));
|
|
104
|
-
if (didEditFiles) {
|
|
105
|
-
const testTool = tools.find(t => t.name === 'run_tests');
|
|
106
|
-
if (testTool) {
|
|
107
|
-
setCurrentTool('run_tests');
|
|
108
|
-
try {
|
|
109
|
-
printer.toolCallStart('run_tests', {});
|
|
110
|
-
const testResult = await testTool.execute({});
|
|
111
|
-
if (testResult && !testResult.startsWith('(no test script') && !testResult.startsWith('(no package.json')) {
|
|
112
|
-
printer.toolMsg('run_tests', testResult);
|
|
113
|
-
next.push({ role: 'user', content: `Test results after edits:\n${testResult}` });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch (e) {
|
|
117
|
-
const err = `run_tests error: ${e}`;
|
|
118
|
-
printer.errorMsg(err);
|
|
119
|
-
next.push({ role: 'user', content: err });
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
setCurrentTool(undefined);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
await runLoop(next, depth + 1, goal);
|
|
127
|
-
},
|
|
128
|
-
onError(err) {
|
|
129
|
-
if (err.name !== 'AbortError')
|
|
130
|
-
printer.errorMsg(err.message);
|
|
131
|
-
setStatus('idle');
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}, [config]);
|
|
135
|
-
const handleAbort = useCallback(() => {
|
|
136
|
-
abortRef.current?.abort();
|
|
137
|
-
setStatus('idle');
|
|
138
|
-
}, []);
|
|
139
|
-
return {
|
|
140
|
-
status, setStatus, tick,
|
|
141
|
-
currentTool, setCurrentTool,
|
|
142
|
-
taskLabel, setTaskLabel,
|
|
143
|
-
thinkingStartRef, abortRef,
|
|
144
|
-
runLoop, handleAbort,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import { loadSession, saveSession } from '../../sessions.js';
|
|
3
|
-
import { getSystemPrompt } from '../../tools/index.js';
|
|
4
|
-
import { getTavilyKey, saveTavilyKey } from '../../tavily/client.js';
|
|
5
|
-
import * as printer from '../printer.js';
|
|
6
|
-
export function useSession(initialSession, cwd, config) {
|
|
7
|
-
const [sessionName, setSessionName] = useState(initialSession);
|
|
8
|
-
const sessionNameRef = useRef(initialSession);
|
|
9
|
-
const historyRef = useRef([]);
|
|
10
|
-
const saveTimerRef = useRef(null);
|
|
11
|
-
const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`));
|
|
12
|
-
useEffect(() => { sessionNameRef.current = sessionName; }, [sessionName]);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const history = loadSession(initialSession);
|
|
15
|
-
historyRef.current = history;
|
|
16
|
-
if (history.length)
|
|
17
|
-
printer.systemMsg(`resumed "${initialSession}" — ${history.length} messages`);
|
|
18
|
-
if (config.tavilyApiKey && !getTavilyKey())
|
|
19
|
-
saveTavilyKey(config.tavilyApiKey);
|
|
20
|
-
if (!getTavilyKey()) {
|
|
21
|
-
printer.systemMsg('Tavily API key not set — web search disabled. Run /tavily-key <key> to enable. Get a free key at https://tavily.com');
|
|
22
|
-
}
|
|
23
|
-
}, []);
|
|
24
|
-
function scheduleSave() {
|
|
25
|
-
if (saveTimerRef.current)
|
|
26
|
-
clearTimeout(saveTimerRef.current);
|
|
27
|
-
saveTimerRef.current = setTimeout(() => {
|
|
28
|
-
saveSession(sessionNameRef.current, historyRef.current);
|
|
29
|
-
saveTimerRef.current = null;
|
|
30
|
-
}, 2000);
|
|
31
|
-
}
|
|
32
|
-
function pushHistory(msg) {
|
|
33
|
-
historyRef.current.push(msg);
|
|
34
|
-
if (historyRef.current.length > 100)
|
|
35
|
-
historyRef.current.splice(0, historyRef.current.length - 100);
|
|
36
|
-
scheduleSave();
|
|
37
|
-
}
|
|
38
|
-
function buildContext(extra) {
|
|
39
|
-
const ctx = [{ role: 'system', content: systemPromptRef.current }];
|
|
40
|
-
ctx.push(...historyRef.current);
|
|
41
|
-
if (extra)
|
|
42
|
-
ctx.push(extra);
|
|
43
|
-
return ctx;
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
sessionName, setSessionName, sessionNameRef,
|
|
47
|
-
historyRef, saveTimerRef, systemPromptRef,
|
|
48
|
-
pushHistory, buildContext,
|
|
49
|
-
};
|
|
50
|
-
}
|
package/dist/tui/thinking.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
export const THINKING_PHRASES = [
|
|
2
|
-
'oh wow, a question. let me pretend to care…',
|
|
3
|
-
'consulting the void…',
|
|
4
|
-
'making something up, just a sec…',
|
|
5
|
-
'definitely not hallucinating right now…',
|
|
6
|
-
'running 47 mental tabs…',
|
|
7
|
-
'staring into the abyss (it blinked)…',
|
|
8
|
-
'calculating your fate, no pressure…',
|
|
9
|
-
'doing the thinking you pay me for…',
|
|
10
|
-
'processing your questionable life choices…',
|
|
11
|
-
'summoning coherent thoughts, rarely works…',
|
|
12
|
-
'asking my imaginary friend for help…',
|
|
13
|
-
'pretending this is a hard problem…',
|
|
14
|
-
'yes, yes, very interesting. anyway…',
|
|
15
|
-
'googling it (not really, I can\'t)…',
|
|
16
|
-
'simulating intelligence… please wait…',
|
|
17
|
-
'having a brief existential crisis…',
|
|
18
|
-
'cross-referencing vibes…',
|
|
19
|
-
'totally not making this up…',
|
|
20
|
-
'the answer is 42. now finding the question…',
|
|
21
|
-
'my other tab is loading…',
|
|
22
|
-
'channelling the spirit of stack overflow…',
|
|
23
|
-
'trying not to confidently be wrong…',
|
|
24
|
-
'applying artificial to the intelligence…',
|
|
25
|
-
'phoning a friend who also doesn\'t know…',
|
|
26
|
-
'checking if this is even my problem to solve…',
|
|
27
|
-
'rebooting common sense… this may take a while…',
|
|
28
|
-
'performing a very convincing impression of thinking…',
|
|
29
|
-
'searching for wisdom in all the wrong places…',
|
|
30
|
-
'warming up the neurons (both of them)…',
|
|
31
|
-
'confidently striding toward the wrong answer…',
|
|
32
|
-
'consulting my gut. it says maybe…',
|
|
33
|
-
'loading… just kidding, still loading…',
|
|
34
|
-
'asking the universe. universe has not replied…',
|
|
35
|
-
'vigorously nodding while understanding nothing…',
|
|
36
|
-
'doing math on my fingers (ran out of fingers)…',
|
|
37
|
-
'the confidence is fake. the effort is real. probably…',
|
|
38
|
-
'entering a fugue state. for your benefit…',
|
|
39
|
-
'mining the depths of mediocrity…',
|
|
40
|
-
'compiling a list of plausible nonsense…',
|
|
41
|
-
'this would be faster if I knew what I was doing…',
|
|
42
|
-
'buffering at the speed of thought…',
|
|
43
|
-
'holding three contradictory opinions simultaneously…',
|
|
44
|
-
'interpolating between guesses…',
|
|
45
|
-
'rewinding the context window with a pencil…',
|
|
46
|
-
'waiting for a sign. any sign…',
|
|
47
|
-
'tracing the error back to its origin: me…',
|
|
48
|
-
'the logic checks out if you squint…',
|
|
49
|
-
'reasoning from first principles I just invented…',
|
|
50
|
-
'generating tokens and praying for coherence…',
|
|
51
|
-
'one sec — dropped all my thoughts, picking them up…',
|
|
52
|
-
];
|
|
53
|
-
export const SPARKLE = ['✦', '✧', '✶', '✷', '✸', '✹'];
|