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 +72 -49
- package/dist/config.js +18 -1
- package/dist/init.js +17 -1
- package/dist/llm/stream.js +41 -0
- package/dist/mcp/client.js +110 -0
- package/dist/setup.js +183 -0
- package/dist/tools/index.js +3 -2
- package/dist/tui/InputBar.js +27 -12
- package/dist/tui/components/ConfigPicker.js +178 -0
- package/dist/tui/components/InputArea.js +58 -40
- package/dist/tui/hooks/useRunLoop.js +61 -2
- package/dist/tui/hooks/useSession.js +6 -6
- package/dist/tui/hooks/useSubmit.js +62 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Miii —
|
|
1
|
+
# Miii — Local-First AI Coding Agent
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **The only coding CLI that runs fully local or cloud — any model, zero lock-in, zero monthly bill.**
|
|
4
4
|
|
|
5
5
|

|
|
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.**
|
|
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
|
|
24
|
+
## Why Miii Exists
|
|
25
25
|
|
|
26
|
-
Claude Code is impressive. It's also
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
43
|
-
4.
|
|
44
|
-
5.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
114
|
+
`patch_file` replaces exact strings in your files. No full rewrites. No formatting destruction. Exactly the change, nothing more.
|
|
87
115
|
|
|
88
|
-
|
|
89
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/llm/stream.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -246,8 +246,9 @@ export const tools = [
|
|
|
246
246
|
},
|
|
247
247
|
},
|
|
248
248
|
];
|
|
249
|
-
export function getSystemPrompt(extra = '') {
|
|
250
|
-
const
|
|
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.
|