miii-cli 0.2.7 → 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 CHANGED
@@ -1,402 +1,97 @@
1
- # miii
1
+ # 🚀 Miii CLI — High-Performance Local AI Coding Agent
2
2
 
3
- > Claude Code-level terminal AI runs on your machine, zero cloud required.
3
+ **The definitive local AI coding agent for your terminal. Automate complex engineering workflows with total control, zero cloud, and zero Python overhead.**
4
+
5
+ ![MIII Demo](mii-cli.gif)
6
+
7
+ [![npm version](https://img.shields.io/npm/v/miii-cli)](https://www.npmjs.com/package/miii-cli)
8
+ [![npm downloads](https://img.shields.io/npm/dm/miii-cli)](https://www.npmjs.com/package/miii-cli)
9
+ [![license](https://img.shields.io/npm/l/miii-cli)](LICENSE)
10
+ [![node](https://img.shields.io/node/v/miii-cli)](https://nodejs.org)
4
11
 
5
12
  ```
6
13
  ╭──────────────────────────────────────────────────────────────────────╮
7
- │ miii v0.2.5
14
+ │ miii v0.2.8
8
15
  │ model: qwen2.5-coder:7b │
9
16
  ├──────────────────────────────────────────────────────────────────────┤
10
17
  │ ✦ cross-referencing vibes… 12s │
11
18
  │ ⚙ running patch_file… │
12
19
  │ ⚙ running run_tests… │
13
20
  ├──────────────────────────────────────────────────────────────────────┤
14
- │ ❯
15
- @ file / command enter send ctrl+c exit
21
+ │ ❯ ⎘ pasted 84 lines
22
+ backspace removes paste enter to send
16
23
  ╰──────────────────────────────────────────────────────────────────────╯
17
24
  ```
18
25
 
19
- [![npm version](https://img.shields.io/npm/v/miii-cli)](https://www.npmjs.com/package/miii-cli)
20
- [![npm downloads](https://img.shields.io/npm/dm/miii-cli)](https://www.npmjs.com/package/miii-cli)
21
- [![license](https://img.shields.io/npm/l/miii-cli)](LICENSE)
22
- [![node](https://img.shields.io/node/v/miii-cli)](https://nodejs.org)
26
+ ## ⚡️ Quick Start
23
27
 
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 |
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
- Model picker opens on launch. Select a model. Start coding.
75
-
76
- ```bash
77
- miii # default session
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.5 → v0.2.6 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
36
+ ## 🧠 Why Miii?
110
37
 
111
- One goal, executed across the whole codebase:
38
+ Most AI coding tools are either heavy Python wrappers or expensive monthly subscriptions that send your code to the cloud. **miii is different.**
112
39
 
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
- ```
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.
172
44
 
173
- Markdown skills still work too — drop a `.md` file in `~/.config/miii/skills/` and it becomes a `/command` instantly.
45
+ ## 🔥 Killer Features
174
46
 
175
- ---
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.
176
53
 
177
- ## Planning mode
54
+ ## ⌨️ Command Cheat Sheet
178
55
 
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 |
56
+ | Command | What it does |
247
57
  |---|---|
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
- ---
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 |
268
64
 
269
- ## Sessions
65
+ ## ⚙️ Configuration
270
66
 
271
- Every conversation persists automatically to disk.
67
+ Customise your experience in `.miii.json` or `~/.config/miii/config.json`:
272
68
 
273
- ```bash
274
- miii # resumes last session
275
- miii --session feature-auth # resumes or creates "feature-auth"
276
- ```
277
-
278
- ```
279
- /session <name> switch to a session (creates if new)
280
- /sessions list all sessions with message counts
281
- /new fresh auto-named session
282
- /clear clear current session
283
- ```
284
-
285
- Sessions at `~/.config/miii/sessions/`. History capped at 100 messages in-context, full history on disk. Debounced writes — no I/O on every message.
286
-
287
- ---
288
-
289
- ## Context compaction
290
-
291
- 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.
292
-
293
- ---
294
-
295
- ## All commands
296
-
297
- Type `/` to open the command palette with fuzzy search.
298
-
299
- | Command | Description |
300
- |---|---|
301
- | `/model <name>` | Switch model mid-session — no restart |
302
- | `/models` | Model picker, pull new Ollama models |
303
- | `/session <name>` | Switch or create session |
304
- | `/sessions` | List all sessions |
305
- | `/new` | Fresh auto-named session |
306
- | `/clear` | Clear current history |
307
- | `/plan [topic]` | Planning mode |
308
- | `/refactor <goal>` | Multi-file refactor |
309
- | `/git <sub>` | Git commands |
310
- | `/skills <sub>` | Install / uninstall / list npm skills |
311
- | `/tavily-key <key>` | Set web search API key |
312
- | `/version` | Show current version |
313
- | `/list` | List all loaded skills |
314
- | `/exit` | Exit |
315
-
316
- ---
317
-
318
- ## Configuration
319
-
320
- Loaded in order from `.miii.json` (project) → `~/.config/miii/config.json` (global).
321
-
322
- **Ollama (default):**
323
- ```json
324
- {
325
- "model": "qwen2.5-coder:7b",
326
- "provider": "ollama",
327
- "baseUrl": "http://localhost:11434"
328
- }
329
- ```
330
-
331
- **Any OpenAI-compatible API** (LM Studio, vLLM, Groq, Together, OpenRouter…):
332
- ```json
333
- {
334
- "model": "gpt-4o",
335
- "provider": "openai-compat",
336
- "baseUrl": "https://api.openai.com/v1",
337
- "apiKey": "sk-..."
338
- }
339
- ```
340
-
341
- **All options:**
342
69
  ```json
343
70
  {
344
71
  "model": "qwen2.5-coder:7b",
345
72
  "provider": "ollama",
346
73
  "baseUrl": "http://localhost:11434",
347
- "apiKey": "",
348
74
  "gitContext": true,
349
- "tavilyApiKey": "tvly-...",
350
- "systemPrompt": "optional override"
75
+ "tavilyApiKey": "tvly-..."
351
76
  }
352
77
  ```
353
78
 
354
- ---
355
-
356
- ## Security
357
-
358
- | Threat | Defense |
359
- |---|---|
360
- | Path traversal (OWASP A01) | All file ops restricted to cwd via `guardPath()` |
361
- | `@file` injection | Refs validated against cwd before reading |
362
- | Session name injection | Names sanitized to alphanumeric + hyphens |
363
- | Shell injection (OWASP A03) | `run_command` enforces 30s hard timeout |
364
- | Config injection (OWASP A08) | Config key whitelist; session data validated as array |
365
- | API key exposure | Tavily key stored at `~/.config/miii/tavily.key` mode 600 |
366
-
367
- ---
368
-
369
- ## Keybindings
370
-
371
- | Key | Action |
372
- |---|---|
373
- | `enter` | Send |
374
- | `↑ / ↓` | Navigate command palette or file picker |
375
- | `esc` | Close overlay / abort in-flight request |
376
- | `ctrl+c` | Abort current request or exit |
377
-
378
- ---
379
-
380
- ## Build from source
79
+ ## 🛠 Build from Source
381
80
 
382
81
  ```bash
383
82
  git clone https://github.com/maruakshay/miii-cli
384
- cd miii-cli
385
- npm install
386
- npm run build
387
- npm link
388
- npm test # 8 integration tests
83
+ cd miii-cli && npm install && npm run build && npm link
389
84
  ```
390
85
 
391
- ---
86
+ ## 🌟 Community & Philosophy
87
+
88
+ **Own your AI stack. Stop renting your intelligence. The future of coding is local.**
392
89
 
393
- ## What's new in 0.2.5
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**
394
95
 
395
- - **Web search** — `web_search` + `web_extract` tools powered by Tavily
396
- - **npm skill ecosystem** — install/uninstall `miii-skill-*` packages, write your own
397
- - **Auto-test after edits** — model runs test suite after every file change, feeds failures back
398
- - **Live model switching** — `/model <name>` mid-session, no restart
399
- - **Update check** — startup banner when a new version is available
400
- - **Hook architecture** — `useSession`, `useModelPicker`, `useRunLoop` for clean internals
401
- - **Ambiguous patch detection** — `patch_file` throws on multiple matches
402
- - **176K bundle** — vs ~50MB for the Python alternatives
96
+ ## 📜 License
97
+ MIT
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
- "description": "Claude Code-level terminal workflows powered by your local models.",
5
+ "description": "The high-performance local AI coding agent for your terminal. Automate complex workflows with local LLMs.",
6
6
  "license": "MIT",
7
7
  "engines": {
8
8
  "node": ">=18"
@@ -16,13 +16,16 @@
16
16
  "url": "https://github.com/maruakshay/miii-cli/issues"
17
17
  },
18
18
  "keywords": [
19
- "ai",
20
- "cli",
19
+ "ai-coding-assistant",
20
+ "local-llm",
21
21
  "ollama",
22
- "llm",
23
- "coding-assistant",
24
- "terminal",
25
- "local-ai"
22
+ "terminal-agent",
23
+ "autonomous-coding",
24
+ "cli-tool",
25
+ "developer-experience",
26
+ "open-source-ai",
27
+ "local-ai",
28
+ "software-engineering"
26
29
  ],
27
30
  "files": [
28
31
  "dist",
@@ -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
- });
@@ -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
- }
@@ -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
- }
@@ -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 = ['✦', '✧', '✶', '✷', '✸', '✹'];