miii-cli 1.0.0 → 1.0.2

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,6 +1,6 @@
1
- # Miii — The High-Performance Local AI Coding Agent
1
+ # Miii — Local-First AI Coding Agent
2
2
 
3
- > **You're paying $200/month for an AI that reads your private code and sends it to a cloud server you don't control. There's a better way.**
3
+ > **The only coding CLI that runs fully local or cloud any model, zero lock-in, zero monthly bill.**
4
4
 
5
5
  ![MIII Demo](mii-cli.gif)
6
6
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  **Miii is a fully autonomous coding agent that runs entirely on your machine.** It plans, edits files, runs your tests, searches the web, indexes your codebase semantically, and iterates until the job is done — all without a single byte of your code leaving your network.
15
15
 
16
- Zero subscription. Zero cloud dependency. Zero Python overhead. **176 KB total.** Just raw engineering horsepower in your terminal.
16
+ Zero subscription. Zero cloud dependency. Zero Python overhead. **176 KB total.**
17
17
 
18
18
  ```bash
19
19
  npm install -g miii-cli && miii
@@ -21,29 +21,47 @@ npm install -g miii-cli && miii
21
21
 
22
22
  ---
23
23
 
24
- ## Why Engineers Are Switching
24
+ ## Why Miii Exists
25
25
 
26
- Claude Code is impressive. It's also a 50 MB binary that costs $200/month, requires an internet connection, and sends every line of your codebase to a server you don't own.
26
+ Claude Code is impressive. It's also cloud-only, costs $20–200/month, and sends every line of your codebase to a server you don't control.
27
27
 
28
- **Miii does everything Claude Code does. It's 176 KB. It's free. It runs on your laptop.**
28
+ OpenCode and Codex CLI have the same problem they're all cloud-first, all locked to specific providers, and all charge you indefinitely for the privilege of reading your private code.
29
29
 
30
- GitHub Copilot streams your proprietary code to Microsoft. Aider is a Python monolith that takes longer to boot than to write a function. All of them charge you monthly for the privilege of being the product.
30
+ **Miii flips the model.** Run on Ollama: $0/month, fully offline, code never leaves your machine. Switch to Anthropic or OpenAI when you need cloud power. Change providers live inside the app no config files, no restarts.
31
31
 
32
- Miii flips the model. Your compute. Your data. Your rules.
32
+ Your compute. Your data. Your rules.
33
+
34
+ ---
35
+
36
+ ## How Miii Compares
37
+
38
+ | | **Miii** | Claude Code | OpenCode | Codex CLI | Aider |
39
+ |---|:---:|:---:|:---:|:---:|:---:|
40
+ | Monthly cost | **$0** | $20–200 | API cost | API cost | $0 |
41
+ | Bundle size | **176 KB** | ~50 MB | ~30 MB | ~20 MB | ~200 MB |
42
+ | Local / offline (Ollama) | **✅** | ❌ | partial | ❌ | ⚠️ |
43
+ | Air-gapped | **✅** | ❌ | ❌ | ❌ | ❌ |
44
+ | Switch provider live | **✅** | ❌ | ❌ | ❌ | ❌ |
45
+ | File checkpoints (undo) | **✅** | ❌ | ❌ | ❌ | ❌ |
46
+ | Permission gates | **✅** | ✅ | partial | ✅ | ❌ |
47
+ | MCP client | **✅** | ✅ | ✅ | ❌ | ❌ |
48
+ | Semantic codebase index | **✅** | ❌ | ❌ | ❌ | ❌ |
49
+ | Skill/extension system | **✅** | plugins | ❌ | ❌ | ❌ |
50
+ | Startup time | **<100ms** | ~2s | ~1s | ~1s | ~4s |
51
+ | License | **MIT** | Proprietary | MIT | MIT | Apache 2.0 |
33
52
 
34
53
  ---
35
54
 
36
55
  ## What Miii Actually Does
37
56
 
38
- This isn't a fancy autocomplete. Miii is a **full autonomous agent loop:**
57
+ This isn't autocomplete. Miii is a **full autonomous agent loop:**
39
58
 
40
59
  1. You describe a goal
41
60
  2. Miii reads your codebase, plans the changes, edits the files
42
- 3. It runs your test suite automatically after every change
43
- 4. If tests fail, it reads the error, fixes the code, re-runs
44
- 5. It repeats until the work is done
45
-
46
- No babysitting. No copy-pasting error messages. No broken half-edits.
61
+ 3. It asks your permission before touching anything (edit, delete, run commands)
62
+ 4. It runs your test suite automatically after every change
63
+ 5. If tests fail, it reads the error, fixes the code, re-runs
64
+ 6. It repeats until the work is done — and checkpoints every file so you can abort safely
47
65
 
48
66
  ---
49
67
 
@@ -59,6 +77,9 @@ No babysitting. No copy-pasting error messages. No broken half-edits.
59
77
 
60
78
  Planning: 3 file(s) to change
61
79
 
80
+ ⚠ edit_file src/auth/session.ts y approve n deny
81
+ > y
82
+
62
83
  ● Editing src/auth/session.ts
63
84
  ● Editing src/middleware/auth.ts
64
85
  ● Editing src/routes/login.ts
@@ -67,48 +88,42 @@ No babysitting. No copy-pasting error messages. No broken half-edits.
67
88
  ─ refactor done — 3 file(s) processed
68
89
  ```
69
90
 
70
- No prompts asking which files to change. No copy-pasting error messages. Just: describe the goal, watch it work.
71
-
72
91
  ---
73
92
 
74
93
  ## Killer Features
75
94
 
76
- **🔍 Semantic Codebase Indexing** *(new in v0.3.2)*
95
+ **🔒 Privacy-First, Local by Default**
96
+ Run on Ollama and your code never leaves your machine. No account. No API key. No monthly bill. Switch to Anthropic or OpenAI when you need it — one command, live, mid-session.
97
+
98
+ **🔄 Live Provider Switching**
99
+ Type `/config` to open an interactive picker. Arrow-navigate between Ollama, Anthropic, and OpenAI-compatible endpoints. Change model, API key, base URL, or Tavily key without restarting. Config saves automatically.
100
+
101
+ **🛡 Permission Gates + File Checkpoints**
102
+ Miii asks before every edit, delete, or shell command — just like Claude Code. Every file is checkpointed before it's touched. Hit Esc to abort and all changes roll back automatically.
103
+
104
+ **🔍 Semantic Codebase Indexing**
77
105
  Build a vector index of your entire codebase using local embeddings. Ask "where is the auth logic?" and Miii finds it by meaning, not keyword. No data leaves your machine.
78
106
 
79
107
  **🧠 Deep Think Engine**
80
- Before answering complex questions, Miii runs a constrained research phase — reading files, checking git history, searching the web — then synthesizes a grounded answer. Not a hallucination. A conclusion.
108
+ Before answering complex questions, Miii runs a constrained research phase — reading files, checking git history, searching the web — then synthesizes a grounded answer.
81
109
 
82
110
  **🌐 Real-Time Web Access**
83
- Tavily-powered web search and page extraction, built in. Ask about breaking changes in a library you just upgraded. Get an answer that's actually current.
111
+ Tavily-powered web search, built in. Ask about breaking changes in a library you just upgraded. Get an answer that's actually current.
84
112
 
85
113
  **🛠 Surgical File Editing**
86
- `patch_file` replaces exact strings in your files. No full rewrites. No formatting destruction. No token waste. Exactly the change, nothing more.
114
+ `patch_file` replaces exact strings in your files. No full rewrites. No formatting destruction. Exactly the change, nothing more.
87
115
 
88
- **🔄 Self-Healing Test Loop**
89
- Miii runs `npm test` after every file change. If something breaks, it reads the failure trace and fixes it autonomously — up to 3 retries before surfacing the issue to you.
116
+ **🔁 Self-Healing Test Loop**
117
+ Runs `npm test` after every file change. If something breaks, reads the failure trace and fixes it autonomously — up to 3 retries before surfacing the issue.
90
118
 
91
119
  **📂 Persistent Sessions**
92
- Pick up exactly where you left off. Named sessions mean your context, your history, and your goal survive terminal restarts.
120
+ Pick up exactly where you left off. Named sessions mean your context, history, and goal survive terminal restarts.
93
121
 
94
122
  **📦 Skill System**
95
123
  Extend Miii with plain Markdown files or npm packages. Ship reusable agent behaviors as versioned packages your whole team can pull.
96
124
 
97
- ---
98
-
99
- ## The Numbers That Matter
100
-
101
- | | **Miii** | Claude Code | Aider |
102
- |---|:---:|:---:|:---:|
103
- | Monthly cost | **$0** | $20–200 | $0 |
104
- | Bundle size | **176 KB** | ~50 MB | ~200 MB |
105
- | Your code stays local | **✅** | ❌ | ⚠️ |
106
- | Startup time | **<100ms** | ~2s | ~4s |
107
- | Semantic codebase index | **✅** | ❌ | ❌ |
108
- | Deep research mode | **✅** | ❌ | ❌ |
109
- | Auto test loop | **✅** | ⚠️ | ⚠️ |
110
- | Works air-gapped | **✅** | ❌ | ❌ |
111
- | License | **MIT** | Proprietary | Apache 2.0 |
125
+ **🔌 MCP Client**
126
+ Connect any MCP-compatible tool server. Miii discovers tools automatically and makes them available to the agent.
112
127
 
113
128
  ---
114
129
 
@@ -126,7 +141,7 @@ cd your-project
126
141
  miii
127
142
  ```
128
143
 
129
- That's it. No API keys. No account. No sign-up form.
144
+ No API keys. No account. No sign-up form. First run walks you through setup interactively.
130
145
 
131
146
  ---
132
147
 
@@ -134,6 +149,7 @@ That's it. No API keys. No account. No sign-up form.
134
149
 
135
150
  | Command | What it does |
136
151
  |---|---|
152
+ | `/config` | Open interactive picker — change provider, model, API key, base URL, Tavily key live |
137
153
  | `/think <question>` | Deep research: reads files + web, then answers |
138
154
  | `/refactor <goal>` | Autonomous multi-file refactor with test validation |
139
155
  | `/index build` | Build semantic vector index of your codebase |
@@ -149,7 +165,7 @@ That's it. No API keys. No account. No sign-up form.
149
165
 
150
166
  ## Semantic Codebase Indexing
151
167
 
152
- For large codebases, Miii can build and query a local vector index — no third-party APIs, no embeddings sent anywhere.
168
+ For large codebases, Miii builds and queries a local vector index — no third-party APIs, no embeddings sent anywhere.
153
169
 
154
170
  ```bash
155
171
  # Pull an embedding model (one time)
@@ -158,17 +174,16 @@ ollama pull nomic-embed-text
158
174
  # Index your project
159
175
  /index build
160
176
 
161
- # The agent now calls search_codebase automatically
162
- # when it needs to find code by concept
177
+ # The agent calls search_codebase automatically when it needs to find code by concept
163
178
  ```
164
179
 
165
- The agent calls `search_codebase` on its own when needed. You don't have to think about it.
166
-
167
180
  ---
168
181
 
169
182
  ## Configuration
170
183
 
171
- Drop a `.miii.json` in your project root or `~/.config/miii/config.json` globally:
184
+ **Interactive (recommended):** type `/config` inside Miii to open the picker.
185
+
186
+ **File-based:** drop a `.miii.json` in your project root or `~/.config/miii/config.json` globally:
172
187
 
173
188
  ```json
174
189
  {
@@ -176,11 +191,12 @@ Drop a `.miii.json` in your project root or `~/.config/miii/config.json` globall
176
191
  "provider": "ollama",
177
192
  "baseUrl": "http://localhost:11434",
178
193
  "gitContext": true,
179
- "tavilyApiKey": "tvly-...",
180
194
  "embedModel": "nomic-embed-text"
181
195
  }
182
196
  ```
183
197
 
198
+ Providers: `ollama` (local, free) · `anthropic` (Claude API) · `openai-compat` (OpenAI or any compatible endpoint)
199
+
184
200
  ---
185
201
 
186
202
  ## Build from Source
@@ -192,13 +208,20 @@ cd miii-cli && npm install && npm run build && npm link
192
208
 
193
209
  ---
194
210
 
211
+ ## Who Should Use Miii
212
+
213
+ - **Privacy-conscious developers** — won't send proprietary code to Anthropic or OpenAI
214
+ - **Cost-sensitive teams** — API bills compound; Ollama is $0
215
+ - **Air-gapped environments** — regulated industries, defense, offline infra
216
+ - **Model experimenters** — want to try llama3, mistral, qwen, Claude side-by-side without switching tools
217
+
218
+ ---
219
+
195
220
  ## The Bottom Line
196
221
 
197
222
  The AI coding tools you're paying for right now will raise their prices, change their terms, and keep reading your code. **Miii won't.** It's MIT licensed, runs locally, and gets better every time Ollama ships a new model.
198
223
 
199
- One engineer built a 176 KB tool that replaces a $200/month cloud product. That shouldn't be a surprise it should be the baseline.
200
-
201
- If this saves you time or money, **star the repo**. It's the only metric that tells other engineers this is worth their attention.
224
+ If this saves you time or money, **star the repo** it's the only metric that tells other engineers this is worth their attention.
202
225
 
203
226
  **[⭐ Star on GitHub](https://github.com/maruakshay/miii-cli)**
204
227
 
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync } from 'fs';
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
4
  const defaults = {
@@ -9,6 +9,23 @@ const defaults = {
9
9
  const ALLOWED_KEYS = new Set(['model', 'provider', 'baseUrl', 'systemPrompt', 'apiKey', 'gitContext', 'tavilyApiKey', 'embedModel']);
10
10
  const PROJECT_CONFIG = join(process.cwd(), '.miii.json');
11
11
  const GLOBAL_CONFIG = join(homedir(), '.config', 'miii', 'config.json');
12
+ export function saveConfig(config) {
13
+ mkdirSync(join(homedir(), '.config', 'miii'), { recursive: true });
14
+ const existing = existsSync(GLOBAL_CONFIG)
15
+ ? (() => { try {
16
+ return JSON.parse(readFileSync(GLOBAL_CONFIG, 'utf-8'));
17
+ }
18
+ catch {
19
+ return {};
20
+ } })()
21
+ : {};
22
+ const merged = { ...existing };
23
+ for (const key of ALLOWED_KEYS) {
24
+ if (key in config)
25
+ merged[key] = config[key];
26
+ }
27
+ writeFileSync(GLOBAL_CONFIG, JSON.stringify(merged, null, 2), { mode: 0o600 });
28
+ }
12
29
  export function loadConfig() {
13
30
  const candidates = [PROJECT_CONFIG, GLOBAL_CONFIG];
14
31
  for (const p of candidates) {
package/dist/init.js CHANGED
@@ -11,6 +11,8 @@ import { SkillLoader } from './skills/loader.js';
11
11
  import { InputBar } from './tui/InputBar.js';
12
12
  import { welcome } from './tui/printer.js';
13
13
  import { ensureOllama } from './llm/ollama.js';
14
+ import { loadMCPTools } from './mcp/client.js';
15
+ import { needsSetup, runSetup } from './setup.js';
14
16
  const require = createRequire(import.meta.url);
15
17
  const UPDATE_CACHE = join(homedir(), '.config', 'miii', 'update-check.json');
16
18
  const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
@@ -71,6 +73,8 @@ export async function lazyInit() {
71
73
  boolean: ['update'],
72
74
  alias: { m: 'model', u: 'url', p: 'provider', s: 'session' },
73
75
  });
76
+ if (needsSetup())
77
+ await runSetup();
74
78
  const config = loadConfig();
75
79
  if (argv.model)
76
80
  config.model = argv.model;
@@ -90,9 +94,21 @@ export async function lazyInit() {
90
94
  skills.loadAll(),
91
95
  checkLatestVersion(currentVersion, !!argv.update),
92
96
  ]);
97
+ // Load MCP servers if configured
98
+ let mcpTools = [];
99
+ let mcpClients = [];
100
+ if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
101
+ const result = await loadMCPTools(config.mcpServers);
102
+ mcpTools = result.tools;
103
+ mcpClients = result.clients;
104
+ if (mcpTools.length)
105
+ process.stderr.write(`MCP: loaded ${mcpTools.length} tool(s) from ${mcpClients.length} server(s)\n`);
106
+ }
93
107
  // Print welcome banner to scrollback BEFORE Ink starts
94
108
  welcome(config.provider, config.model, process.cwd(), currentVersion, updateAvailable, linked);
95
109
  const sessionName = argv.session || `s-${Date.now()}`;
96
- const { waitUntilExit } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion }), { exitOnCtrlC: false });
110
+ const { waitUntilExit } = render(React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName, version: currentVersion, mcpTools }), { exitOnCtrlC: false });
97
111
  await waitUntilExit();
112
+ for (const c of mcpClients)
113
+ c.close();
98
114
  }
@@ -1,4 +1,6 @@
1
1
  export async function chat(cfg) {
2
+ if (cfg.provider === 'anthropic')
3
+ return chatAnthropic(cfg);
2
4
  if (cfg.provider === 'openai-compat')
3
5
  return chatOpenAI(cfg);
4
6
  return chatOllama(cfg);
@@ -115,6 +117,45 @@ async function chatOpenAI(cfg) {
115
117
  onError(toError(err));
116
118
  }
117
119
  }
120
+ async function chatAnthropic(cfg) {
121
+ const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage } = cfg;
122
+ const url = baseUrl && baseUrl !== 'http://localhost:11434'
123
+ ? `${baseUrl}/v1/messages`
124
+ : 'https://api.anthropic.com/v1/messages';
125
+ const systemParts = messages.filter(m => m.role === 'system').map(m => m.content);
126
+ const filtered = messages.filter(m => m.role !== 'system');
127
+ try {
128
+ const body = {
129
+ model,
130
+ max_tokens: 8192,
131
+ messages: filtered,
132
+ };
133
+ if (systemParts.length)
134
+ body.system = systemParts.join('\n\n');
135
+ const res = await fetch(url, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'content-type': 'application/json',
139
+ 'x-api-key': apiKey ?? '',
140
+ 'anthropic-version': '2023-06-01',
141
+ },
142
+ body: JSON.stringify(body),
143
+ signal,
144
+ });
145
+ if (!res.ok) {
146
+ onError(new Error(`Anthropic ${res.status}: ${await res.text()}`));
147
+ return;
148
+ }
149
+ const obj = await res.json();
150
+ const text = (obj.content ?? []).filter(c => c.type === 'text').map(c => c.text).join('');
151
+ onUsage?.(obj.usage?.input_tokens ?? 0, obj.usage?.output_tokens ?? 0);
152
+ await onDone(text);
153
+ }
154
+ catch (err) {
155
+ if (err?.name !== 'AbortError')
156
+ onError(toError(err));
157
+ }
158
+ }
118
159
  function toError(e) {
119
160
  return e instanceof Error ? e : new Error(String(e));
120
161
  }
@@ -0,0 +1,110 @@
1
+ import { spawn } from 'child_process';
2
+ import { createInterface } from 'readline';
3
+ function schemaToParams(def) {
4
+ const props = def.inputSchema?.properties ?? {};
5
+ const required = new Set(def.inputSchema?.required ?? []);
6
+ const entries = Object.entries(props).map(([k, v]) => {
7
+ const t = v?.type ?? 'any';
8
+ return `"${k}": "${t}${required.has(k) ? '' : ' (optional)'}"`;
9
+ });
10
+ return '{' + entries.join(', ') + '}';
11
+ }
12
+ export class MCPClient {
13
+ proc = null;
14
+ pending = new Map();
15
+ nextId = 1;
16
+ name;
17
+ constructor(name) {
18
+ this.name = name;
19
+ }
20
+ async connect(cfg) {
21
+ this.proc = spawn(cfg.command, cfg.args ?? [], {
22
+ stdio: ['pipe', 'pipe', 'pipe'],
23
+ env: { ...process.env, ...cfg.env },
24
+ });
25
+ this.proc.stderr?.on('data', () => { });
26
+ const rl = createInterface({ input: this.proc.stdout });
27
+ rl.on('line', (line) => {
28
+ if (!line.trim())
29
+ return;
30
+ try {
31
+ const msg = JSON.parse(line);
32
+ if (msg.id !== undefined) {
33
+ const p = this.pending.get(msg.id);
34
+ if (p) {
35
+ this.pending.delete(msg.id);
36
+ if (msg.error)
37
+ p.reject(new Error(msg.error.message));
38
+ else
39
+ p.resolve(msg.result);
40
+ }
41
+ }
42
+ }
43
+ catch { }
44
+ });
45
+ this.proc.on('error', (err) => {
46
+ for (const p of this.pending.values())
47
+ p.reject(err);
48
+ this.pending.clear();
49
+ });
50
+ await this.send('initialize', {
51
+ protocolVersion: '2024-11-05',
52
+ capabilities: { tools: {} },
53
+ clientInfo: { name: 'miii', version: '1.0.0' },
54
+ });
55
+ this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
56
+ }
57
+ async listTools() {
58
+ const result = await this.send('tools/list');
59
+ return result?.tools ?? [];
60
+ }
61
+ async callTool(name, args) {
62
+ const result = await this.send('tools/call', { name, arguments: args });
63
+ return (result?.content ?? [])
64
+ .filter(c => c.type === 'text')
65
+ .map(c => c.text ?? '')
66
+ .join('\n');
67
+ }
68
+ send(method, params) {
69
+ return new Promise((resolve, reject) => {
70
+ const id = this.nextId++;
71
+ this.pending.set(id, { resolve, reject });
72
+ this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
73
+ setTimeout(() => {
74
+ if (this.pending.has(id)) {
75
+ this.pending.delete(id);
76
+ reject(new Error(`MCP timeout: ${method}`));
77
+ }
78
+ }, 10_000);
79
+ });
80
+ }
81
+ close() {
82
+ this.proc?.kill();
83
+ }
84
+ }
85
+ export async function loadMCPTools(servers) {
86
+ const clients = [];
87
+ const tools = [];
88
+ for (const [serverName, cfg] of Object.entries(servers)) {
89
+ const client = new MCPClient(serverName);
90
+ try {
91
+ await client.connect(cfg);
92
+ const defs = await client.listTools();
93
+ clients.push(client);
94
+ for (const def of defs) {
95
+ const toolName = `mcp_${serverName}_${def.name}`.replace(/[^a-zA-Z0-9_]/g, '_');
96
+ tools.push({
97
+ name: toolName,
98
+ description: `[MCP:${serverName}] ${def.description ?? def.name}`,
99
+ params: schemaToParams(def),
100
+ execute: async (args) => client.callTool(def.name, args),
101
+ });
102
+ }
103
+ }
104
+ catch (err) {
105
+ process.stderr.write(`MCP server "${serverName}" failed to connect: ${err}\n`);
106
+ client.close();
107
+ }
108
+ }
109
+ return { tools, clients };
110
+ }
package/dist/setup.js ADDED
@@ -0,0 +1,183 @@
1
+ import { createInterface } from 'readline';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ const GLOBAL_CONFIG = join(homedir(), '.config', 'miii', 'config.json');
6
+ const R = '\x1b[0m';
7
+ const BOLD = '\x1b[1m';
8
+ const DIM = '\x1b[2m';
9
+ const CYAN = '\x1b[96m';
10
+ const GREEN = '\x1b[92m';
11
+ const GRAY = '\x1b[90m';
12
+ const YELLOW = '\x1b[93m';
13
+ const PURPLE = '\x1b[95m';
14
+ const WHITE = '\x1b[97m';
15
+ const b = (s) => `${BOLD}${s}${R}`;
16
+ const cy = (s) => `${CYAN}${s}${R}`;
17
+ const gr = (s) => `${GRAY}${s}${R}`;
18
+ const gn = (s) => `${GREEN}${s}${R}`;
19
+ const yw = (s) => `${YELLOW}${s}${R}`;
20
+ const wh = (s) => `${WHITE}${s}${R}`;
21
+ const dim = (s) => `${DIM}${s}${R}`;
22
+ const PROVIDERS = [
23
+ { key: 'ollama', label: 'Ollama', desc: 'local · free · air-gapped' },
24
+ { key: 'anthropic', label: 'Anthropic', desc: 'Claude API (cloud)' },
25
+ { key: 'openai-compat', label: 'OpenAI / Custom', desc: 'OpenAI or compatible endpoint' },
26
+ ];
27
+ const MODEL_SUGGESTIONS = {
28
+ 'ollama': ['qwen2.5-coder:7b', 'llama3.2', 'deepseek-r1:7b', 'codellama:13b'],
29
+ 'anthropic': ['claude-sonnet-4-6', 'claude-opus-4-7', 'claude-haiku-4-5-20251001'],
30
+ 'openai-compat': ['gpt-4o', 'gpt-4o-mini', 'o1-mini'],
31
+ };
32
+ const w = process.stdout.write.bind(process.stdout);
33
+ const ln = (s = '') => w(s + '\n');
34
+ function divider() {
35
+ ln(gr(' ' + '─'.repeat(46)));
36
+ }
37
+ export function needsSetup() {
38
+ return !existsSync(GLOBAL_CONFIG);
39
+ }
40
+ export async function runSetup() {
41
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
42
+ const ask = (prompt) => new Promise(resolve => rl.question(prompt, ans => resolve(ans.trim())));
43
+ // ── Header ────────────────────────────────────────────────────────────────
44
+ ln();
45
+ ln(` ${PURPLE}${BOLD}● ●${R}`);
46
+ ln(` ${PURPLE}${BOLD}╲●╱${R} ${b(wh('Miii'))} ${gr('first-time setup')}`);
47
+ ln();
48
+ // ── Provider ──────────────────────────────────────────────────────────────
49
+ ln(yw(b(' Provider')));
50
+ divider();
51
+ for (let i = 0; i < PROVIDERS.length; i++) {
52
+ const p = PROVIDERS[i];
53
+ ln(` ${cy(b(String(i + 1)))} ${wh(p.label.padEnd(16))} ${gr(dim(p.desc))}`);
54
+ }
55
+ ln();
56
+ let providerKey = 'ollama';
57
+ while (true) {
58
+ const raw = await ask(` ${cy('›')} ${gr('[1–3]: ')}`);
59
+ const choice = raw || '1';
60
+ const idx = parseInt(choice, 10) - 1;
61
+ if (idx >= 0 && idx < PROVIDERS.length) {
62
+ providerKey = PROVIDERS[idx].key;
63
+ w(` ${gn('✓')} ${wh(PROVIDERS[idx].label)}\n`);
64
+ break;
65
+ }
66
+ ln(gr(' enter 1, 2, or 3'));
67
+ }
68
+ ln();
69
+ // ── Credentials / URL ─────────────────────────────────────────────────────
70
+ let apiKey;
71
+ let baseUrl = 'http://localhost:11434';
72
+ if (providerKey === 'anthropic') {
73
+ ln(yw(b(' API Key')));
74
+ divider();
75
+ ln(gr(' console.anthropic.com → API Keys'));
76
+ ln();
77
+ while (true) {
78
+ const raw = await ask(` ${cy('›')} sk-ant-...: `);
79
+ if (raw.startsWith('sk-ant-') || raw.startsWith('sk-')) {
80
+ apiKey = raw;
81
+ ln(` ${gn('✓')} key saved`);
82
+ break;
83
+ }
84
+ ln(gr(' key should start with sk-ant-'));
85
+ }
86
+ baseUrl = 'https://api.anthropic.com';
87
+ ln();
88
+ }
89
+ if (providerKey === 'openai-compat') {
90
+ ln(yw(b(' Endpoint')));
91
+ divider();
92
+ const rawUrl = await ask(` ${cy('›')} Base URL ${gr('[https://api.openai.com]')}: `);
93
+ baseUrl = rawUrl || 'https://api.openai.com';
94
+ ln();
95
+ const rawKey = await ask(` ${cy('›')} API key ${gr('(optional)')}: `);
96
+ if (rawKey) {
97
+ apiKey = rawKey;
98
+ ln(` ${gn('✓')} key saved`);
99
+ }
100
+ else {
101
+ ln(` ${gr('─')} no key set`);
102
+ }
103
+ ln();
104
+ }
105
+ if (providerKey === 'ollama') {
106
+ // Try default URL silently; only ask if unreachable
107
+ try {
108
+ await fetch('http://localhost:11434/api/tags', { signal: AbortSignal.timeout(2000) });
109
+ }
110
+ catch {
111
+ ln(yw(b(' Ollama URL')));
112
+ divider();
113
+ ln(gr(' Could not reach http://localhost:11434'));
114
+ ln();
115
+ const rawUrl = await ask(` ${cy('›')} URL ${gr('[http://localhost:11434]')}: `);
116
+ baseUrl = rawUrl || 'http://localhost:11434';
117
+ ln(` ${gn('✓')} ${baseUrl}`);
118
+ ln();
119
+ }
120
+ }
121
+ // ── Model ─────────────────────────────────────────────────────────────────
122
+ ln(yw(b(' Model')));
123
+ divider();
124
+ let suggestions = MODEL_SUGGESTIONS[providerKey] ?? [];
125
+ if (providerKey === 'ollama') {
126
+ try {
127
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(4000) });
128
+ if (res.ok) {
129
+ const data = await res.json();
130
+ const pulled = (data.models ?? []).map(m => m.name).filter(Boolean);
131
+ if (pulled.length)
132
+ suggestions = pulled;
133
+ }
134
+ }
135
+ catch { }
136
+ if (!suggestions.length) {
137
+ ln(gr(' No models found — enter name manually (e.g. qwen2.5-coder:7b)'));
138
+ ln();
139
+ const rawModel = await ask(` ${cy('›')} model name: `);
140
+ const model = rawModel || 'qwen2.5-coder:7b';
141
+ ln(` ${gn('✓')} ${model}`);
142
+ ln();
143
+ rl.close();
144
+ return saveConfig({ provider: 'ollama', model, baseUrl, apiKey });
145
+ }
146
+ }
147
+ for (let i = 0; i < suggestions.length; i++) {
148
+ ln(` ${cy(b(String(i + 1).padStart(2)))} ${suggestions[i]}`);
149
+ }
150
+ ln();
151
+ const defaultModel = suggestions[0] ?? 'llama3.2';
152
+ let model = defaultModel;
153
+ while (true) {
154
+ const raw = await ask(` ${cy('›')} ${gr(`[1–${suggestions.length} or name]: `)}`);
155
+ if (!raw)
156
+ break;
157
+ const idx = parseInt(raw, 10) - 1;
158
+ if (idx >= 0 && idx < suggestions.length) {
159
+ model = suggestions[idx];
160
+ break;
161
+ }
162
+ if (raw.length > 0) {
163
+ model = raw;
164
+ break;
165
+ }
166
+ }
167
+ ln(` ${gn('✓')} ${model}`);
168
+ ln();
169
+ rl.close();
170
+ return saveConfig({ provider: providerKey, model, baseUrl, apiKey });
171
+ }
172
+ function saveConfig(cfg) {
173
+ const config = { provider: cfg.provider, model: cfg.model, baseUrl: cfg.baseUrl };
174
+ if (cfg.apiKey)
175
+ config.apiKey = cfg.apiKey;
176
+ mkdirSync(join(homedir(), '.config', 'miii'), { recursive: true });
177
+ writeFileSync(GLOBAL_CONFIG, JSON.stringify(config, null, 2), { mode: 0o600 });
178
+ const w = process.stdout.write.bind(process.stdout);
179
+ const gr = (s) => `\x1b[90m${s}\x1b[0m`;
180
+ const gn = (s) => `\x1b[92m${s}\x1b[0m`;
181
+ w(` ${gn('✓')} config saved ${gr(GLOBAL_CONFIG)}\n\n`);
182
+ return config;
183
+ }
@@ -246,8 +246,9 @@ export const tools = [
246
246
  },
247
247
  },
248
248
  ];
249
- export function getSystemPrompt(extra = '') {
250
- const toolDocs = tools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
249
+ export function getSystemPrompt(extra = '', extraTools = []) {
250
+ const allTools = extraTools.length ? [...tools, ...extraTools] : tools;
251
+ const toolDocs = allTools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
251
252
  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.
252
253
  - search_codebase({"query": "string", "k": "number (optional)"}): Semantic vector search over the indexed codebase. Returns top-k relevant code snippets by meaning. Requires the user to have run /index build. Use this when you need to find code by concept rather than exact string — e.g. "authentication logic", "error handling patterns", "database queries".`;
253
254
  return `You are Miii — a fast, local AI coding assistant.