miii-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
 
@@ -54,61 +72,63 @@ No babysitting. No copy-pasting error messages. No broken half-edits.
54
72
 
55
73
  ● Researching: refactor auth module to use JWT
56
74
  ● Reading src/auth/session.ts
75
+ Read 42 lines
57
76
  ● Reading src/middleware/auth.ts
58
- Reading src/routes/login.ts
77
+ Read 28 lines
59
78
 
60
- Planning: 3 file(s) to change
79
+ plan (2 actions)
80
+ ◦ edit_file src/auth/session.ts
81
+ ◦ edit_file src/middleware/auth.ts
61
82
 
62
- Editing src/auth/session.ts
63
- Editing src/middleware/auth.ts
64
- ● Editing src/routes/login.ts
65
- ● Running tests
83
+ edit_file src/auth/session.ts y approve n deny
84
+ > y
66
85
 
67
- refactor done — 3 file(s) processed
68
- ```
86
+ edit_file src/auth/session.ts
87
+ Wrote 12 lines
88
+ ● edit_file src/middleware/auth.ts
89
+ Wrote 8 lines
90
+ ● run_tests
91
+ ✅ Tests passed
69
92
 
70
- No prompts asking which files to change. No copy-pasting error messages. Just: describe the goal, watch it work.
93
+ refactor done 2 file(s) processed
94
+ ```
71
95
 
72
96
  ---
73
97
 
74
98
  ## Killer Features
75
99
 
76
- **🔍 Semantic Codebase Indexing** *(new in v0.3.2)*
100
+ **🔒 Privacy-First, Local by Default**
101
+ 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.
102
+
103
+ **🔄 Live Provider Switching**
104
+ 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.
105
+
106
+ **🛡 Permission Gates + File Checkpoints**
107
+ 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.
108
+
109
+ **🔍 Semantic Codebase Indexing**
77
110
  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
111
 
79
112
  **🧠 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.
113
+ Before answering complex questions, Miii runs a constrained research phase — reading files, checking git history, searching the web — then synthesizes a grounded answer.
81
114
 
82
115
  **🌐 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.
116
+ Tavily-powered web search, built in. Ask about breaking changes in a library you just upgraded. Get an answer that's actually current.
84
117
 
85
118
  **🛠 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.
119
+ `patch_file` replaces exact strings in your files. No full rewrites. No formatting destruction. Exactly the change, nothing more.
87
120
 
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.
121
+ **🔁 Self-Healing Test Loop**
122
+ 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
123
 
91
124
  **📂 Persistent Sessions**
92
- Pick up exactly where you left off. Named sessions mean your context, your history, and your goal survive terminal restarts.
125
+ Pick up exactly where you left off. Named sessions mean your context, history, and goal survive terminal restarts.
93
126
 
94
127
  **📦 Skill System**
95
128
  Extend Miii with plain Markdown files or npm packages. Ship reusable agent behaviors as versioned packages your whole team can pull.
96
129
 
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 |
130
+ **🔌 MCP Client**
131
+ Connect any MCP-compatible tool server. Miii discovers tools automatically and makes them available to the agent.
112
132
 
113
133
  ---
114
134
 
@@ -126,7 +146,7 @@ cd your-project
126
146
  miii
127
147
  ```
128
148
 
129
- That's it. No API keys. No account. No sign-up form.
149
+ No API keys. No account. No sign-up form. First run walks you through setup interactively.
130
150
 
131
151
  ---
132
152
 
@@ -134,6 +154,7 @@ That's it. No API keys. No account. No sign-up form.
134
154
 
135
155
  | Command | What it does |
136
156
  |---|---|
157
+ | `/config` | Open interactive picker — change provider, model, API key, base URL, Tavily key live |
137
158
  | `/think <question>` | Deep research: reads files + web, then answers |
138
159
  | `/refactor <goal>` | Autonomous multi-file refactor with test validation |
139
160
  | `/index build` | Build semantic vector index of your codebase |
@@ -149,7 +170,7 @@ That's it. No API keys. No account. No sign-up form.
149
170
 
150
171
  ## Semantic Codebase Indexing
151
172
 
152
- For large codebases, Miii can build and query a local vector index — no third-party APIs, no embeddings sent anywhere.
173
+ For large codebases, Miii builds and queries a local vector index — no third-party APIs, no embeddings sent anywhere.
153
174
 
154
175
  ```bash
155
176
  # Pull an embedding model (one time)
@@ -158,17 +179,16 @@ ollama pull nomic-embed-text
158
179
  # Index your project
159
180
  /index build
160
181
 
161
- # The agent now calls search_codebase automatically
162
- # when it needs to find code by concept
182
+ # The agent calls search_codebase automatically when it needs to find code by concept
163
183
  ```
164
184
 
165
- The agent calls `search_codebase` on its own when needed. You don't have to think about it.
166
-
167
185
  ---
168
186
 
169
187
  ## Configuration
170
188
 
171
- Drop a `.miii.json` in your project root or `~/.config/miii/config.json` globally:
189
+ **Interactive (recommended):** type `/config` inside Miii to open the picker.
190
+
191
+ **File-based:** drop a `.miii.json` in your project root or `~/.config/miii/config.json` globally:
172
192
 
173
193
  ```json
174
194
  {
@@ -176,11 +196,12 @@ Drop a `.miii.json` in your project root or `~/.config/miii/config.json` globall
176
196
  "provider": "ollama",
177
197
  "baseUrl": "http://localhost:11434",
178
198
  "gitContext": true,
179
- "tavilyApiKey": "tvly-...",
180
199
  "embedModel": "nomic-embed-text"
181
200
  }
182
201
  ```
183
202
 
203
+ Providers: `ollama` (local, free) · `anthropic` (Claude API) · `openai-compat` (OpenAI or any compatible endpoint)
204
+
184
205
  ---
185
206
 
186
207
  ## Build from Source
@@ -192,13 +213,20 @@ cd miii-cli && npm install && npm run build && npm link
192
213
 
193
214
  ---
194
215
 
216
+ ## Who Should Use Miii
217
+
218
+ - **Privacy-conscious developers** — won't send proprietary code to Anthropic or OpenAI
219
+ - **Cost-sensitive teams** — API bills compound; Ollama is $0
220
+ - **Air-gapped environments** — regulated industries, defense, offline infra
221
+ - **Model experimenters** — want to try llama3, mistral, qwen, Claude side-by-side without switching tools
222
+
223
+ ---
224
+
195
225
  ## The Bottom Line
196
226
 
197
227
  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
228
 
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.
229
+ 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
230
 
203
231
  **[⭐ Star on GitHub](https://github.com/maruakshay/miii-cli)**
204
232
 
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
+ }