miii-cli 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,62 +9,90 @@
9
9
  [![license](https://img.shields.io/npm/l/miii-cli)](LICENSE)
10
10
  [![node](https://img.shields.io/node/v/miii-cli)](https://nodejs.org)
11
11
 
12
- ```
13
- ╭──────────────────────────────────────────────────────────────────────╮
14
- │ miii v0.2.8 │
15
- │ model: qwen2.5-coder:7b │
16
- ├──────────────────────────────────────────────────────────────────────┤
17
- │ ✦ cross-referencing vibes… 12s │
18
- │ ⚙ running patch_file… │
19
- │ ⚙ running run_tests… │
20
- ├──────────────────────────────────────────────────────────────────────┤
21
- │ ❯ pasted 84 lines │
22
- │ backspace removes paste enter to send │
23
- ╰──────────────────────────────────────────────────────────────────────╯
24
- ```
12
+ ## 📊 The Competitive Edge
13
+
14
+ | Feature | **Miii** | Claude Code | Codex CLI | Aider |
15
+ |---|---|---|---|---|
16
+ | **Execution Environment** | ✅ Local / Hybrid | ❌ Cloud only | ❌ Cloud only | ✅ Local + cloud |
17
+ | **Data Privacy** | ✅ Air-gapped possible | ❌ Cloud-streamed | ❌ Cloud-streamed | ⚠️ Model-dependent |
18
+ | **Cost Structure** | 🆓 Free (Your Compute) | 💳 Token-based | 💳 Token-based | 🆓 Free (local) |
19
+ | **Runtime Efficiency** | ⚡ TS (Instant Start) | 🐍 Node (Fast) | 🐍 Node | 🐢 Python (Heavy) |
20
+ | **Research Engine** | ✅ Deep Think Mode | ❌ | ❌ | ❌ |
21
+ | **Validation Loop** | ✅ Auto-test (Jest/Vitest) | ⚠️ Manual | ❌ | ⚠️ Manual |
22
+ | **Live Web Access** | ✅ Tavily Integrated | ❌ | ❌ | ❌ |
23
+ | **Edit Precision** | ✅ Surgical `patch_file` | ✅ | ⚠️ | ✅ |
24
+ | **State Persistence** | ✅ Named Sessions | ✅ | ❌ | ⚠️ Basic |
25
+ | **Extensibility** | ✅ npm + `.md` Skills | ⚠️ MCP only | ❌ | ❌ |
26
+ | **License** | ✅ MIT | ❌ | ❌ | ✅ Apache 2.0 |
27
+
28
+ > ✅ = Native  |  ⚠️ = Partial  |  ❌ = Unsupported
25
29
 
26
30
  ## ⚡️ Quick Start
27
31
 
28
- Get up and running in 30 seconds:
32
+ Deploy Miii in your environment in 30 seconds:
29
33
 
30
34
  ```bash
35
+ # 1. Pull a capable local model
31
36
  ollama pull qwen2.5-coder:7b
37
+
38
+ # 2. Install the CLI globally
32
39
  npm install -g miii-cli
40
+
41
+ # 3. Start engineering
33
42
  miii
34
43
  ```
35
44
 
36
45
  ## 🧠 Why Miii?
37
46
 
38
- Most AI coding tools are either heavy Python wrappers or expensive monthly subscriptions that send your code to the cloud. **miii is different.**
47
+ The industry is saturated with heavy Python wrappers and expensive monthly subscriptions that trade your intellectual property for convenience. **Miii breaks this cycle.**
39
48
 
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.
49
+ - **Privacy by Default**: Your codebase never leaves your machine. Period.
50
+ - **Zero Latency**: Built with TypeScript for near-instant startup. No virtual environments, no dependency hell, just raw performance.
51
+ - **True Autonomy**: Miii isn't a chatbot; it's a junior engineer. It plans, edits, runs tests, and iterates until the PR is ready.
52
+ - **Architectural Intelligence**: By analyzing git diffs and project structure, Miii understands context without requiring manual copy-pasting.
44
53
 
45
54
  ## 🔥 Killer Features
46
55
 
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.
56
+ - **🛠 Surgical Precision**: Instead of overwriting files, Miii uses `patch_file` to inject changes, preserving your formatting and reducing token waste.
57
+ - **🔄 The Self-Healing Loop**: Miii executes your test suite (Jest, Vitest, Mocha) after every change. If a test fails, it analyzes the trace and fixes the code autonomously.
58
+ - **🌐 Real-time Intelligence**: Integrated `web_search` and `web_extract` via Tavily allow Miii to reference the latest documentation and API changes.
59
+ - **🧠 Deep Think Engine**: A sophisticated two-phase research mode that gathers data before synthesizing a solution.
60
+ - **📐 Strategic Planning**: Use `/plan` to map out complex refactors before a single character is typed.
61
+ - **📂 Persistent Context**: Workflows are saved as named sessions. Jump back into a specific feature branch with `miii --session feature-auth`.
62
+ - **📦 Modular Skill System**: Extend Miii's capabilities using npm plugins or simple Markdown-based skill files.
63
+
64
+ ## 🔬 Deep Think Explained
65
+
66
+ Deep Think is a recursive research engine designed to eliminate "hallucinations" by grounding the AI in facts:
67
+
68
+ 1. **Gather Phase**: A constrained, read-only loop utilizing `read_file`, `list_files`, `git_status`, `git_log`, `git_diff`, and web tools.
69
+ - **Guardrails**: Strict limit of 6 tool calls and 4 web calls to prevent infinite loops.
70
+ - **Safety**: Zero write permissions. No mutations.
71
+ 2. **Synthesize Phase**: All gathered intelligence is aggregated and fed into the main execution loop for a grounded, verified response.
72
+
73
+ **Trigger Research:**
74
+ ```bash
75
+ /think "How does the auth middleware handle token expiry?"
76
+ /think "Analyze the project structure and explain the data flow."
77
+ /think "What are the breaking changes in React 19 for this project?"
78
+ ```
79
+ *The LLM also triggers `deep_think` autonomously when it detects a high-complexity query.*
53
80
 
54
81
  ## ⌨️ Command Cheat Sheet
55
82
 
56
- | Command | What it does |
57
- |---|---|
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 |
83
+ | Command | Purpose | Impact |
84
+ |---|---|---|
85
+ | `/think <query>` | Deep Research | High-fidelity synthesis of files + web |
86
+ | `/refactor <goal>` | Full-scale Engineering | Plan $\rightarrow$ Edit $\rightarrow$ Test loop |
87
+ | `/git <sub>` | Git Automation | Instant status, diffs, and AI commit messages |
88
+ | `/plan` | Architecture Mode | Structured blueprinting before coding |
89
+ | `/model <name>` | LLM Hot-swap | Switch models instantly based on task |
90
+ | `/tavily-key <key>` | Enable Web Access | Unlocks real-time internet browsing |
91
+ | `/sessions` | Context Recovery | Resume previous engineering sessions |
64
92
 
65
93
  ## ⚙️ Configuration
66
94
 
67
- Customise your experience in `.miii.json` or `~/.config/miii/config.json`:
95
+ Fine-tune your agent in `.miii.json` or `~/.config/miii/config.json`:
68
96
 
69
97
  ```json
70
98
  {
@@ -85,13 +113,12 @@ cd miii-cli && npm install && npm run build && npm link
85
113
 
86
114
  ## 🌟 Community & Philosophy
87
115
 
88
- **Own your AI stack. Stop renting your intelligence. The future of coding is local.**
116
+ **Stop renting your intelligence. Own your AI stack.**
89
117
 
90
- miii is built for the community. If this tool saves you hours of coding, help us grow:
118
+ Miii is built for engineers who value privacy, speed, and total control. If this tool has accelerated your workflow, support the project:
91
119
  - 🌟 **Star the repo** on GitHub
92
- - 🐦 **Share on X**
93
- - 🤖 **Post on Reddit**
94
- - 💬 **Tell a fellow developer**
120
+ - 🐦 **Share your wins** on X
121
+ - 🤖 **Discuss the future** on Reddit
95
122
 
96
123
  ## 📜 License
97
124
  MIT
@@ -0,0 +1,50 @@
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/init.js CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
9
9
  import { loadConfig } from './config.js';
10
10
  import { SkillLoader } from './skills/loader.js';
11
11
  import { InputBar } from './tui/InputBar.js';
12
- import { welcome } from './tui/printer.js';
12
+ import { welcome, setInkInstance } from './tui/printer.js';
13
13
  import { ensureOllama } from './llm/ollama.js';
14
14
  const require = createRequire(import.meta.url);
15
15
  const UPDATE_CACHE = join(homedir(), '.config', 'miii', 'update-check.json');
@@ -89,7 +89,8 @@ export async function lazyInit() {
89
89
  ]);
90
90
  // Print welcome banner to scrollback BEFORE Ink starts
91
91
  welcome(config.provider, config.model, process.cwd(), currentVersion, updateAvailable, linked);
92
- const sessionName = argv.session || 'default';
93
- const { waitUntilExit } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion }), { exitOnCtrlC: false });
92
+ const sessionName = argv.session || `s-${Date.now()}`;
93
+ const { waitUntilExit, clear } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion }), { exitOnCtrlC: false });
94
+ setInkInstance(clear);
94
95
  await waitUntilExit();
95
96
  }
@@ -4,21 +4,57 @@ export async function chat(cfg) {
4
4
  return chatOllama(cfg);
5
5
  }
6
6
  async function chatOllama(cfg) {
7
- const { model, messages, baseUrl, signal, onDone, onError, onUsage } = cfg;
7
+ const { model, messages, baseUrl, signal, onDone, onError, onUsage, onChunk } = cfg;
8
8
  try {
9
9
  const res = await fetch(`${baseUrl}/api/chat`, {
10
10
  method: 'POST',
11
11
  headers: { 'Content-Type': 'application/json' },
12
- body: JSON.stringify({ model, messages, stream: false }),
12
+ body: JSON.stringify({ model, messages, stream: !!onChunk }),
13
13
  signal,
14
14
  });
15
15
  if (!res.ok) {
16
16
  onError(new Error(`Ollama ${res.status}: ${await res.text()}`));
17
17
  return;
18
18
  }
19
- const obj = await res.json();
20
- onUsage?.(obj?.prompt_eval_count ?? 0, obj?.eval_count ?? 0);
21
- await onDone(obj?.message?.content ?? '');
19
+ if (!onChunk) {
20
+ const obj = await res.json();
21
+ onUsage?.(obj?.prompt_eval_count ?? 0, obj?.eval_count ?? 0);
22
+ await onDone(obj?.message?.content ?? '');
23
+ return;
24
+ }
25
+ const reader = res.body.getReader();
26
+ const decoder = new TextDecoder();
27
+ let full = '';
28
+ let promptTokens = 0;
29
+ let completionTokens = 0;
30
+ let buf = '';
31
+ while (true) {
32
+ const { done, value } = await reader.read();
33
+ if (done)
34
+ break;
35
+ buf += decoder.decode(value, { stream: true });
36
+ const lines = buf.split('\n');
37
+ buf = lines.pop() ?? '';
38
+ for (const line of lines) {
39
+ if (!line.trim())
40
+ continue;
41
+ try {
42
+ const obj = JSON.parse(line);
43
+ const chunk = obj?.message?.content ?? '';
44
+ if (chunk) {
45
+ full += chunk;
46
+ onChunk(chunk);
47
+ }
48
+ if (obj?.done) {
49
+ promptTokens = obj.prompt_eval_count ?? 0;
50
+ completionTokens = obj.eval_count ?? 0;
51
+ }
52
+ }
53
+ catch { }
54
+ }
55
+ }
56
+ onUsage?.(promptTokens, completionTokens);
57
+ await onDone(full);
22
58
  }
23
59
  catch (err) {
24
60
  if (err?.name !== 'AbortError')
@@ -26,21 +62,53 @@ async function chatOllama(cfg) {
26
62
  }
27
63
  }
28
64
  async function chatOpenAI(cfg) {
29
- const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage } = cfg;
65
+ const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage, onChunk } = cfg;
30
66
  try {
31
67
  const res = await fetch(`${baseUrl}/v1/chat/completions`, {
32
68
  method: 'POST',
33
69
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey ?? 'local'}` },
34
- body: JSON.stringify({ model, messages }),
70
+ body: JSON.stringify({ model, messages, stream: !!onChunk }),
35
71
  signal,
36
72
  });
37
73
  if (!res.ok) {
38
74
  onError(new Error(`LLM ${res.status}: ${await res.text()}`));
39
75
  return;
40
76
  }
41
- const obj = await res.json();
42
- onUsage?.(obj?.usage?.prompt_tokens ?? 0, obj?.usage?.completion_tokens ?? 0);
43
- await onDone(obj?.choices?.[0]?.message?.content ?? '');
77
+ if (!onChunk) {
78
+ const obj = await res.json();
79
+ onUsage?.(obj?.usage?.prompt_tokens ?? 0, obj?.usage?.completion_tokens ?? 0);
80
+ await onDone(obj?.choices?.[0]?.message?.content ?? '');
81
+ return;
82
+ }
83
+ const reader = res.body.getReader();
84
+ const decoder = new TextDecoder();
85
+ let full = '';
86
+ let buf = '';
87
+ while (true) {
88
+ const { done, value } = await reader.read();
89
+ if (done)
90
+ break;
91
+ buf += decoder.decode(value, { stream: true });
92
+ const lines = buf.split('\n');
93
+ buf = lines.pop() ?? '';
94
+ for (const line of lines) {
95
+ if (!line.startsWith('data: '))
96
+ continue;
97
+ const data = line.slice(6).trim();
98
+ if (data === '[DONE]')
99
+ continue;
100
+ try {
101
+ const obj = JSON.parse(data);
102
+ const chunk = obj?.choices?.[0]?.delta?.content ?? '';
103
+ if (chunk) {
104
+ full += chunk;
105
+ onChunk(chunk);
106
+ }
107
+ }
108
+ catch { }
109
+ }
110
+ }
111
+ await onDone(full);
44
112
  }
45
113
  catch (err) {
46
114
  if (err?.name !== 'AbortError')
@@ -0,0 +1,44 @@
1
+ import { chat } from '../llm/stream.js';
2
+ const SYSTEM = `You extract memorable facts from conversations for long-term memory. Output ONLY a valid JSON array of concise fact strings.
3
+
4
+ Extract: user preferences, decisions made, key file paths, functions or variables, code patterns established, constraints, goals.
5
+ Skip: trivial exchanges, transient state, tool output noise.
6
+ Max 8 facts. Be specific and concrete.
7
+
8
+ Example output:
9
+ ["User prefers patch_file over full rewrites","entry point is src/index.ts","decided to use Zod for validation"]`;
10
+ export function extractFacts(messages, config, model) {
11
+ const lines = messages
12
+ .filter(m => m.role !== 'system')
13
+ .map(m => `${m.role}: ${m.content.slice(0, 400)}`)
14
+ .join('\n');
15
+ if (!lines.trim())
16
+ return Promise.resolve([]);
17
+ return new Promise(resolve => {
18
+ chat({
19
+ provider: config.provider,
20
+ model,
21
+ baseUrl: config.baseUrl,
22
+ apiKey: config.apiKey,
23
+ messages: [
24
+ { role: 'system', content: SYSTEM },
25
+ { role: 'user', content: lines },
26
+ ],
27
+ onDone(text) {
28
+ try {
29
+ const m = text.match(/\[[\s\S]*?\]/);
30
+ if (!m) {
31
+ resolve([]);
32
+ return;
33
+ }
34
+ const arr = JSON.parse(m[0]);
35
+ resolve(Array.isArray(arr) ? arr.filter((f) => typeof f === 'string') : []);
36
+ }
37
+ catch {
38
+ resolve([]);
39
+ }
40
+ },
41
+ onError() { resolve([]); },
42
+ });
43
+ });
44
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ const MEMORY_DIR = join(homedir(), '.config', 'miii', 'memory');
5
+ const MAX_FACTS = 200;
6
+ function ensureDir() {
7
+ mkdirSync(MEMORY_DIR, { recursive: true });
8
+ }
9
+ export function loadLongMemory(sessionName) {
10
+ ensureDir();
11
+ const p = join(MEMORY_DIR, `${sessionName}.json`);
12
+ if (!existsSync(p))
13
+ return [];
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(p, 'utf-8'));
16
+ return Array.isArray(parsed) ? parsed : [];
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ }
22
+ export function saveLongMemory(sessionName, facts) {
23
+ ensureDir();
24
+ writeFileSync(join(MEMORY_DIR, `${sessionName}.json`), JSON.stringify(facts));
25
+ }
26
+ export function mergeFacts(existing, newTexts) {
27
+ const existingSet = new Set(existing.map(f => f.text.toLowerCase()));
28
+ const ts = Date.now();
29
+ const added = newTexts
30
+ .filter(t => t.trim() && !existingSet.has(t.toLowerCase()))
31
+ .map(text => ({ text, ts }));
32
+ const merged = [...existing, ...added];
33
+ if (merged.length > MAX_FACTS)
34
+ merged.splice(0, merged.length - MAX_FACTS);
35
+ return merged;
36
+ }
37
+ export function formatMemoryBlock(facts) {
38
+ if (!facts.length)
39
+ return '';
40
+ return `\n\n[Long-term memory — recalled from prior conversation]\n${facts.map(f => `- ${f.text}`).join('\n')}`;
41
+ }
@@ -0,0 +1,64 @@
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
+ }
@@ -235,6 +235,7 @@ export const tools = [
235
235
  ];
236
236
  export function getSystemPrompt(extra = '') {
237
237
  const toolDocs = tools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
238
+ const deepThinkDoc = `- deep_think({"query": "string", "needs_web": "boolean (optional)"}): Research tool — gathers information from files, git, and optionally the web before answering. Returns a compiled research summary. Guardrails: read-only tools only, max 6 tool calls, max 4 web calls inside. Use when a question requires reading multiple files or searching the web first.`;
238
239
  return `You are Miii — a fast, local AI coding assistant.
239
240
 
240
241
  Use tools by emitting:
@@ -265,6 +266,7 @@ replacement text
265
266
 
266
267
  Tools:
267
268
  ${toolDocs}
269
+ ${deepThinkDoc}
268
270
 
269
271
  Rules:
270
272
  - To modify an existing file: use patch_file with the exact old text and new replacement — do NOT rewrite the whole file
@@ -284,5 +286,7 @@ Rules:
284
286
  - After editing files that have tests, call run_tests to verify nothing broke
285
287
  - If run_tests fails, read the failing test output and fix the code, then run_tests again (max 3 retries)
286
288
  - You have web_search and web_extract tools — use them whenever the user asks about anything requiring internet access, current information, documentation, library versions, news, or external URLs
287
- - NEVER say you cannot search the web — always call web_search instead${extra}`;
289
+ - NEVER say you cannot search the web — always call web_search instead
290
+ - Use deep_think when the question requires gathering from multiple files or sources before you can answer well — it runs a safe read-only research phase and returns a summary you can reason over
291
+ - deep_think cannot edit files or run shell commands — it is purely for information gathering${extra}`;
288
292
  }
@@ -8,7 +8,8 @@ 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 { toolArgSummary } from './printer.js';
12
+ import { loadSession, saveSession, listSessions, deleteSession } from '../sessions.js';
12
13
  import { MacroQueue, MicroQueue } from '../tasks/queue.js';
13
14
  import { TaskExecutor } from '../tasks/executor.js';
14
15
  import { fileEditContext } from '../tasks/compactor.js';
@@ -23,6 +24,7 @@ import { buildGitContext, looksCodeRelated } from './git-context.js';
23
24
  import { useSession } from './hooks/useSession.js';
24
25
  import { useModelPicker } from './hooks/useModelPicker.js';
25
26
  import { useRunLoop } from './hooks/useRunLoop.js';
27
+ import { runDeepThink } from './deepThink.js';
26
28
  const gitRun = promisify(exec);
27
29
  function buildAtContext(text) {
28
30
  const refs = [...text.matchAll(/@([\w./\-]+)/g)];
@@ -47,9 +49,20 @@ export function InputBar({ config, skills, cwd, session, version }) {
47
49
  const macroQueueRef = useRef(new MacroQueue());
48
50
  const executorRef = useRef(new TaskExecutor(tools));
49
51
  const lastGitStatusRef = useRef('');
50
- const { setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, buildContext, } = useSession(session, cwd, config);
52
+ const abortRef = useRef(null);
53
+ const { setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, buildContext, renameFromMessage, } = useSession(session, cwd, config);
51
54
  const { currentModel, setCurrentModel, currentModelRef, pickerOpen, setPickerOpen, pickerModels, pickerLoading, pickerError, pullState, openPicker, handleModelSelect, handleModelPull, } = useModelPicker(config);
52
- const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, abortRef, runLoop, handleAbort, } = useRunLoop(config, currentModelRef, pushHistory);
55
+ const deepThinkTool = useMemo(() => ({
56
+ name: 'deep_think',
57
+ description: 'Research tool: gather info from files and web before answering.',
58
+ params: '{"query": "string", "needs_web": "boolean (optional)"}',
59
+ execute: async ({ query }) => {
60
+ const result = await runDeepThink(String(query), config, currentModelRef.current, abortRef.current?.signal);
61
+ return `Research complete (${result.toolCalls} tool calls, ${result.webCalls} web):\n\n${result.findings}`;
62
+ },
63
+ }), [config]);
64
+ const allTools = useMemo(() => [...tools, deepThinkTool], [deepThinkTool]);
65
+ const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, permissionRequest, resolvePermission, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
53
66
  // ─── refactor ─────────────────────────────────────────────────────────────
54
67
  const runRefactor = useCallback(async (goal) => {
55
68
  printer.systemMsg(`refactor: ${goal}`);
@@ -348,6 +361,41 @@ export function InputBar({ config, skills, cwd, session, version }) {
348
361
  await runRefactor(goal);
349
362
  return;
350
363
  }
364
+ if (cmd.startsWith('/think ') || cmd === '/think') {
365
+ const query = cmd.slice(6).trim();
366
+ if (!query) {
367
+ printer.systemMsg('usage: /think <query>');
368
+ return;
369
+ }
370
+ printer.userMsg(`/think ${query}`);
371
+ setStatus('thinking');
372
+ setTaskLabel(`gathering: ${query}`);
373
+ abortRef.current = new AbortController();
374
+ try {
375
+ const result = await runDeepThink(query, config, currentModelRef.current, abortRef.current.signal, (toolName) => setCurrentTool(`gather:${toolName}`));
376
+ setCurrentTool(undefined);
377
+ printer.systemMsg(`gathered: ${result.toolCalls} tool call(s), ${result.webCalls} web call(s)`);
378
+ if (result.findings) {
379
+ pushHistory({ role: 'user', content: `/think ${query}` });
380
+ pushHistory({ role: 'assistant', content: result.findings });
381
+ pushHistory({ role: 'user', content: `Based on your research above, give a complete answer to: ${query}` });
382
+ await runLoop(buildContext(), 0, query);
383
+ }
384
+ else {
385
+ printer.systemMsg('nothing gathered — try rephrasing');
386
+ setStatus('idle');
387
+ }
388
+ }
389
+ catch (e) {
390
+ printer.errorMsg(`deep think failed: ${e}`);
391
+ setStatus('idle');
392
+ }
393
+ finally {
394
+ setCurrentTool(undefined);
395
+ setTaskLabel(undefined);
396
+ }
397
+ return;
398
+ }
351
399
  if (cmd === '/plan' || cmd.startsWith('/plan ')) {
352
400
  const topic = cmd.slice(5).trim();
353
401
  setPlanningMode(true);
@@ -394,6 +442,25 @@ export function InputBar({ config, skills, cwd, session, version }) {
394
442
  printer.systemMsg(`current: ${sessionNameRef.current}`);
395
443
  return;
396
444
  }
445
+ if (arg.startsWith('delete ')) {
446
+ const target = arg.slice(7).trim();
447
+ if (!target) {
448
+ printer.systemMsg('usage: /session delete <name>');
449
+ return;
450
+ }
451
+ if (target === sessionNameRef.current) {
452
+ printer.systemMsg('cannot delete active session — switch first');
453
+ return;
454
+ }
455
+ try {
456
+ deleteSession(target);
457
+ printer.systemMsg(`deleted: ${target}`);
458
+ }
459
+ catch (e) {
460
+ printer.errorMsg(`delete failed: ${String(e)}`);
461
+ }
462
+ return;
463
+ }
397
464
  if (saveTimerRef.current) {
398
465
  clearTimeout(saveTimerRef.current);
399
466
  saveTimerRef.current = null;
@@ -434,6 +501,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
434
501
  printer.systemMsg(`unknown command: /${slashCmd} — try /list`);
435
502
  return;
436
503
  }
504
+ renameFromMessage(text);
437
505
  const contextPrefix = buildAtContext(text);
438
506
  const shouldInjectGit = config.gitContext !== false && looksCodeRelated(text);
439
507
  const { prefix: gitPrefix, label: gitLabel } = shouldInjectGit
@@ -447,7 +515,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
447
515
  }, [skills, runLoop, openPicker]);
448
516
  const skillList = skills.list();
449
517
  // ─── render ────────────────────────────────────────────────────────────────
450
- return (_jsxs(Box, { flexDirection: "column", children: [pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
518
+ return (_jsxs(Box, { flexDirection: "column", children: [pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : permissionRequest ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 0, children: _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { color: "white", bold: true, children: permissionRequest.toolName }), _jsx(Text, { color: "gray", children: toolArgSummary(permissionRequest.args) })] }) }), _jsx(Divider, { cols: cols })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
451
519
  ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: [SPARKLE[tick % SPARKLE.length], " "] }), _jsx(Text, { color: "gray", dimColor: true, italic: true, children: THINKING_PHRASES[phraseSeq[Math.floor(tick / 62) % phraseSeq.length]] })] })
452
- : _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [Math.floor((Date.now() - thinkingStartRef.current) / 1000), "s"] }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] }) }), _jsx(Divider, { cols: cols })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, onSubmit: handleSubmit, onAbort: handleAbort })] }));
520
+ : _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [Math.floor((Date.now() - thinkingStartRef.current) / 1000), "s"] }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] }) }), _jsx(Divider, { cols: cols })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, permissionRequest: permissionRequest, onPermissionResponse: resolvePermission, onSubmit: handleSubmit, onAbort: handleAbort })] }));
453
521
  }