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 +84 -56
- 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/tasks/compactor.js +65 -26
- 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 +86 -44
- package/dist/tui/hooks/useRunLoop.js +94 -10
- package/dist/tui/hooks/useSession.js +6 -6
- package/dist/tui/hooks/useSubmit.js +88 -23
- package/dist/tui/printer.js +72 -2
- package/package.json +1 -1
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
|
|
|
@@ -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
|
-
|
|
77
|
+
Read 28 lines
|
|
59
78
|
|
|
60
|
-
|
|
79
|
+
─ plan (2 actions)
|
|
80
|
+
◦ edit_file src/auth/session.ts
|
|
81
|
+
◦ edit_file src/middleware/auth.ts
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
─ refactor done — 2 file(s) processed
|
|
94
|
+
```
|
|
71
95
|
|
|
72
96
|
---
|
|
73
97
|
|
|
74
98
|
## Killer Features
|
|
75
99
|
|
|
76
|
-
|
|
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.
|
|
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
|
|
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.
|
|
119
|
+
`patch_file` replaces exact strings in your files. No full rewrites. No formatting destruction. Exactly the change, nothing more.
|
|
87
120
|
|
|
88
|
-
|
|
89
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
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
|
+
}
|