miii-agent 0.1.12 → 0.1.14

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.
Files changed (3) hide show
  1. package/README.md +105 -80
  2. package/dist/cli.js +631 -170
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,10 +9,98 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/ollama/client.ts
12
+ // src/config.ts
13
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
14
+ import { join } from "path";
15
+ import { homedir } from "os";
16
+ function defaultProviders() {
17
+ return {
18
+ ollama: {
19
+ type: "ollama",
20
+ baseUrl: process.env.OLLAMA_HOST ?? "http://localhost:11434"
21
+ },
22
+ lmstudio: {
23
+ type: "openai",
24
+ baseUrl: process.env.LMSTUDIO_HOST ?? process.env.LLM_HOST ?? "http://localhost:1234",
25
+ ...process.env.LMSTUDIO_API_KEY ? { apiKey: process.env.LMSTUDIO_API_KEY } : {}
26
+ }
27
+ };
28
+ }
29
+ function migrate(raw) {
30
+ const providers = { ...defaultProviders(), ...raw.providers ?? {} };
31
+ if (raw.ollamaHost) providers.ollama = { ...providers.ollama, baseUrl: raw.ollamaHost };
32
+ if (raw.lmstudioHost) providers.lmstudio = { ...providers.lmstudio, baseUrl: raw.lmstudioHost };
33
+ return {
34
+ model: raw.model,
35
+ provider: raw.provider,
36
+ effort: raw.effort,
37
+ providers
38
+ };
39
+ }
40
+ function readRawConfig() {
41
+ if (!existsSync(CONFIG_PATH)) return {};
42
+ try {
43
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+ function loadConfig() {
49
+ return migrate(readRawConfig());
50
+ }
51
+ function saveConfig(config) {
52
+ mkdirSync(CONFIG_DIR, { recursive: true });
53
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
54
+ }
55
+ function providersOf(cfg) {
56
+ return cfg.providers && Object.keys(cfg.providers).length ? cfg.providers : defaultProviders();
57
+ }
58
+ function resolveProvider(cfg = loadConfig()) {
59
+ const providers = providersOf(cfg);
60
+ const name = cfg.provider && providers[cfg.provider] ? cfg.provider : providers.ollama ? "ollama" : Object.keys(providers)[0];
61
+ return { name, entry: providers[name] };
62
+ }
63
+ function listProviders(cfg = loadConfig()) {
64
+ return Object.keys(providersOf(cfg));
65
+ }
66
+ function providerEntries(cfg = loadConfig()) {
67
+ const providers = providersOf(cfg);
68
+ return Object.entries(providers).map(([name, entry]) => {
69
+ const local = entry.type === "ollama" || /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(entry.baseUrl);
70
+ return { name, entry, kind: local ? "local" : "api" };
71
+ });
72
+ }
73
+ function setModel(model) {
74
+ saveConfig({ ...readRawConfig(), model });
75
+ }
76
+ function setEffort(effort) {
77
+ saveConfig({ ...readRawConfig(), effort });
78
+ }
79
+ function setProvider(provider) {
80
+ saveConfig({ ...readRawConfig(), provider });
81
+ }
82
+ var CONFIG_DIR, CONFIG_PATH;
83
+ var init_config = __esm({
84
+ "src/config.ts"() {
85
+ "use strict";
86
+ CONFIG_DIR = join(homedir(), ".miii");
87
+ CONFIG_PATH = join(CONFIG_DIR, "config.json");
88
+ }
89
+ });
90
+
91
+ // src/llm/ollama.ts
13
92
  import { Ollama } from "ollama";
14
93
  import { execFileSync } from "child_process";
15
- function ollamaInstalled() {
94
+ function makeClient(entry, signal) {
95
+ const opts = { host: entry.baseUrl };
96
+ if (entry.apiKey) opts.headers = { Authorization: `Bearer ${entry.apiKey}` };
97
+ if (signal) {
98
+ opts.fetch = ((input, init) => fetch(input, { ...init, signal }));
99
+ }
100
+ return new Ollama(opts);
101
+ }
102
+ function isAvailable(entry) {
103
+ if (!LOCAL_HOST_RE.test(entry.baseUrl)) return true;
16
104
  try {
17
105
  const cmd2 = process.platform === "win32" ? "where" : "which";
18
106
  execFileSync(cmd2, ["ollama"], { stdio: "ignore" });
@@ -31,20 +119,20 @@ function isConnectionError(err) {
31
119
  const msg = err instanceof Error ? err.message : String(err);
32
120
  return msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("connect");
33
121
  }
34
- async function listModels() {
122
+ async function listModels(entry) {
35
123
  try {
36
- const { models } = await ollama.list();
124
+ const { models } = await makeClient(entry).list();
37
125
  return models.map((m) => m.name);
38
126
  } catch (err) {
39
127
  if (isConnectionError(err)) {
40
- throw new Error(OLLAMA_NOT_RUNNING);
128
+ throw new Error(NOT_RUNNING);
41
129
  }
42
130
  throw err;
43
131
  }
44
132
  }
45
- async function modelContext(model) {
133
+ async function modelContext(entry, model) {
46
134
  try {
47
- const info = await ollama.show({ model });
135
+ const info = await makeClient(entry).show({ model });
48
136
  const modelInfo = info.model_info;
49
137
  if (modelInfo) {
50
138
  const ctxKey = Object.keys(modelInfo).find((k) => k.includes("context_length"));
@@ -56,7 +144,7 @@ async function modelContext(model) {
56
144
  return 2048;
57
145
  } catch (err) {
58
146
  if (isConnectionError(err)) {
59
- throw new Error(OLLAMA_NOT_RUNNING);
147
+ throw new Error(NOT_RUNNING);
60
148
  }
61
149
  const msg = err instanceof Error ? err.message : String(err);
62
150
  if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
@@ -65,13 +153,10 @@ async function modelContext(model) {
65
153
  throw err;
66
154
  }
67
155
  }
68
- async function* chat(model, messages, tools, opts) {
156
+ async function* chat(entry, model, messages, tools, opts) {
69
157
  if (opts?.signal?.aborted) return;
70
158
  const signal = opts?.signal;
71
- const client = signal ? new Ollama({
72
- host: process.env.OLLAMA_HOST ?? "http://localhost:11434",
73
- fetch: ((input, init) => fetch(input, { ...init, signal }))
74
- }) : ollama;
159
+ const client = makeClient(entry, signal);
75
160
  let stream;
76
161
  const onAbort = () => {
77
162
  try {
@@ -99,7 +184,7 @@ async function* chat(model, messages, tools, opts) {
99
184
  } catch (err) {
100
185
  if (signal?.aborted) return;
101
186
  if (isConnectionError(err)) {
102
- throw new Error(OLLAMA_NOT_RUNNING);
187
+ throw new Error(NOT_RUNNING);
103
188
  }
104
189
  const msg = err instanceof Error ? err.message : String(err);
105
190
  if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
@@ -123,27 +208,278 @@ async function* chat(model, messages, tools, opts) {
123
208
  } catch (err) {
124
209
  if (opts?.signal?.aborted) return;
125
210
  if (isConnectionError(err)) {
126
- throw new Error(OLLAMA_NOT_RUNNING);
211
+ throw new Error(NOT_RUNNING);
127
212
  }
128
213
  throw err;
129
214
  } finally {
130
215
  if (opts?.signal) opts.signal.removeEventListener("abort", onAbort);
131
216
  }
132
217
  }
133
- var ollama, OLLAMA_NOT_INSTALLED, OLLAMA_NOT_RUNNING, HARMONY_RE, CHANNEL_LABEL_RE;
134
- var init_client = __esm({
135
- "src/ollama/client.ts"() {
218
+ var NOT_INSTALLED, NOT_RUNNING, LOCAL_HOST_RE, HARMONY_RE, CHANNEL_LABEL_RE;
219
+ var init_ollama = __esm({
220
+ "src/llm/ollama.ts"() {
136
221
  "use strict";
137
- ollama = new Ollama({
138
- host: process.env.OLLAMA_HOST ?? "http://localhost:11434"
139
- });
140
- OLLAMA_NOT_INSTALLED = "Ollama is not installed. Install it with: npm i -g ollama\nOr download from https://ollama.com/download";
141
- OLLAMA_NOT_RUNNING = "Ollama is not running. Start it with: ollama serve";
222
+ NOT_INSTALLED = "Ollama is not installed. Install it with: npm i -g ollama\nOr download from https://ollama.com/download";
223
+ NOT_RUNNING = "Ollama is not running. Start it with: ollama serve";
224
+ LOCAL_HOST_RE = /localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/;
142
225
  HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
143
226
  CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
144
227
  }
145
228
  });
146
229
 
230
+ // src/llm/openai.ts
231
+ function notAvailable(entry) {
232
+ return `Cannot reach OpenAI-compatible provider at ${entry.baseUrl}. Make sure the server is running and the baseUrl is correct.`;
233
+ }
234
+ function headers(entry) {
235
+ const h = { "Content-Type": "application/json" };
236
+ if (entry.apiKey) h["Authorization"] = `Bearer ${entry.apiKey}`;
237
+ return h;
238
+ }
239
+ function isConnectionError2(err) {
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ return msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("connect");
242
+ }
243
+ function isAvailable2(_entry) {
244
+ return true;
245
+ }
246
+ async function listModels2(entry) {
247
+ try {
248
+ const res = await fetch(`${entry.baseUrl}/v1/models`, {
249
+ headers: headers(entry),
250
+ signal: AbortSignal.timeout(5e3)
251
+ });
252
+ if (!res.ok) {
253
+ let detail = "";
254
+ try {
255
+ detail = await res.text();
256
+ } catch {
257
+ }
258
+ throw new Error(`Provider error (HTTP ${res.status}): ${detail || res.statusText}`);
259
+ }
260
+ const body = await res.json();
261
+ return body.data.map((m) => m.id);
262
+ } catch (err) {
263
+ if (isConnectionError2(err)) {
264
+ throw new Error(notAvailable(entry));
265
+ }
266
+ throw err;
267
+ }
268
+ }
269
+ async function modelContext2(_entry, _model) {
270
+ return DEFAULT_CONTEXT;
271
+ }
272
+ function toOpenAIMessages(msgs) {
273
+ return msgs.map((m) => {
274
+ if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
275
+ return {
276
+ role: "assistant",
277
+ content: m.content || null,
278
+ tool_calls: m.tool_calls.map((tc) => ({
279
+ id: tc.id ?? `call_${Math.random().toString(36).slice(2, 10)}`,
280
+ type: "function",
281
+ function: {
282
+ name: tc.function.name,
283
+ arguments: JSON.stringify(tc.function.arguments)
284
+ }
285
+ }))
286
+ };
287
+ }
288
+ if (m.role === "tool") {
289
+ return {
290
+ role: "tool",
291
+ content: m.content,
292
+ tool_call_id: m.tool_call_id ?? ""
293
+ };
294
+ }
295
+ return { role: m.role, content: m.content };
296
+ });
297
+ }
298
+ function toOpenAITools(tools) {
299
+ if (!tools || tools.length === 0) return void 0;
300
+ return tools.map((t) => ({
301
+ type: "function",
302
+ function: {
303
+ name: t.function.name,
304
+ description: t.function.description,
305
+ parameters: t.function.parameters
306
+ }
307
+ }));
308
+ }
309
+ function parseSSELine(line) {
310
+ if (!line.startsWith("data: ")) return null;
311
+ const data = line.slice(6).trim();
312
+ if (data === "[DONE]") return null;
313
+ try {
314
+ return JSON.parse(data);
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+ async function* chat2(entry, model, messages, tools, opts) {
320
+ if (opts?.signal?.aborted) return;
321
+ const oaMessages = toOpenAIMessages(messages);
322
+ const oaTools = toOpenAITools(tools);
323
+ const body = {
324
+ model,
325
+ messages: oaMessages,
326
+ stream: true,
327
+ temperature: opts?.temperature ?? 0.2
328
+ };
329
+ if (oaTools) body.tools = oaTools;
330
+ if (opts?.num_predict && opts.num_predict > 0) body.max_tokens = opts.num_predict;
331
+ const toolCallAccum = /* @__PURE__ */ new Map();
332
+ const TIMEOUT_MS = 18e4;
333
+ const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS);
334
+ const combinedSignal = opts?.signal && typeof AbortSignal.any === "function" ? AbortSignal.any([opts.signal, timeoutSignal]) : opts?.signal ?? timeoutSignal;
335
+ try {
336
+ const res = await fetch(`${entry.baseUrl}/v1/chat/completions`, {
337
+ method: "POST",
338
+ headers: headers(entry),
339
+ body: JSON.stringify(body),
340
+ signal: combinedSignal
341
+ });
342
+ if (!res.ok) {
343
+ let detail = "";
344
+ try {
345
+ detail = await res.text();
346
+ } catch {
347
+ }
348
+ if (res.status === 404) {
349
+ throw new Error(`Model "${model}" not found at ${entry.baseUrl}. Make sure it's available.`);
350
+ }
351
+ throw new Error(`Provider error (HTTP ${res.status}): ${detail || res.statusText}`);
352
+ }
353
+ const reader = res.body?.getReader();
354
+ if (!reader) throw new Error("No response body");
355
+ const decoder = new TextDecoder();
356
+ let buffer = "";
357
+ try {
358
+ readLoop: while (true) {
359
+ const { done: readerDone, value } = await reader.read();
360
+ if (readerDone || opts?.signal?.aborted) break;
361
+ buffer += decoder.decode(value, { stream: true });
362
+ const lines = buffer.split("\n");
363
+ buffer = lines.pop() ?? "";
364
+ for (const line of lines) {
365
+ const parsed = parseSSELine(line);
366
+ if (!parsed) continue;
367
+ const choices = parsed.choices;
368
+ if (!choices || choices.length === 0) continue;
369
+ const delta = choices[0].delta ?? {};
370
+ const finishReason = choices[0].finish_reason;
371
+ if (delta.content) {
372
+ yield { content: delta.content, done: false };
373
+ }
374
+ const deltaToolCalls = delta.tool_calls;
375
+ if (deltaToolCalls) {
376
+ for (const tc of deltaToolCalls) {
377
+ const idx = tc.index;
378
+ if (!toolCallAccum.has(idx)) {
379
+ toolCallAccum.set(idx, { args: "" });
380
+ }
381
+ const acc = toolCallAccum.get(idx);
382
+ if (tc.id) acc.id = tc.id;
383
+ if (tc.type) acc.type = tc.type;
384
+ if (tc.function) {
385
+ if (tc.function.name) acc.name = tc.function.name;
386
+ if (tc.function.arguments) acc.args += tc.function.arguments;
387
+ }
388
+ }
389
+ }
390
+ if (finishReason) {
391
+ break readLoop;
392
+ }
393
+ }
394
+ }
395
+ } finally {
396
+ reader.cancel().catch(() => {
397
+ });
398
+ }
399
+ } catch (err) {
400
+ if (opts?.signal?.aborted) {
401
+ yield { content: "", done: true, prompt_eval_count: 0, eval_count: 0 };
402
+ return;
403
+ }
404
+ if (isConnectionError2(err)) {
405
+ throw new Error(notAvailable(entry));
406
+ }
407
+ if (timeoutSignal.aborted && !opts?.signal?.aborted) {
408
+ throw new Error(`Provider request timed out after ${TIMEOUT_MS / 1e3}s. The model may still be loading or thinking.`);
409
+ }
410
+ throw err;
411
+ }
412
+ if (opts?.signal?.aborted) {
413
+ yield { content: "", done: true, prompt_eval_count: 0, eval_count: 0 };
414
+ return;
415
+ }
416
+ const toolCalls = [];
417
+ for (const [, acc] of toolCallAccum) {
418
+ let parsedArgs = {};
419
+ try {
420
+ parsedArgs = JSON.parse(acc.args);
421
+ } catch {
422
+ parsedArgs = { _raw: acc.args };
423
+ }
424
+ toolCalls.push({
425
+ id: acc.id,
426
+ function: {
427
+ name: acc.name ?? "",
428
+ arguments: parsedArgs
429
+ }
430
+ });
431
+ }
432
+ yield {
433
+ content: "",
434
+ done: true,
435
+ tool_calls: toolCalls.length > 0 ? toolCalls : void 0
436
+ };
437
+ }
438
+ var DEFAULT_CONTEXT;
439
+ var init_openai = __esm({
440
+ "src/llm/openai.ts"() {
441
+ "use strict";
442
+ DEFAULT_CONTEXT = 4096;
443
+ }
444
+ });
445
+
446
+ // src/llm/client.ts
447
+ function active() {
448
+ return resolveProvider();
449
+ }
450
+ function isAvailable3() {
451
+ const { entry } = active();
452
+ return entry.type === "ollama" ? isAvailable(entry) : isAvailable2(entry);
453
+ }
454
+ function NOT_AVAILABLE() {
455
+ const { entry } = active();
456
+ return entry.type === "ollama" ? NOT_INSTALLED : notAvailable(entry);
457
+ }
458
+ async function listModels3() {
459
+ const { entry } = active();
460
+ return entry.type === "ollama" ? listModels(entry) : listModels2(entry);
461
+ }
462
+ async function modelContext3(model) {
463
+ const { entry } = active();
464
+ return entry.type === "ollama" ? modelContext(entry, model) : modelContext2(entry, model);
465
+ }
466
+ async function* chat3(model, messages, tools, opts) {
467
+ const { entry } = active();
468
+ if (entry.type === "ollama") {
469
+ yield* chat(entry, model, messages, tools, opts);
470
+ } else {
471
+ yield* chat2(entry, model, messages, tools, opts);
472
+ }
473
+ }
474
+ var init_client = __esm({
475
+ "src/llm/client.ts"() {
476
+ "use strict";
477
+ init_config();
478
+ init_ollama();
479
+ init_openai();
480
+ }
481
+ });
482
+
147
483
  // src/tools/paths.ts
148
484
  import { resolve, relative as relative2, isAbsolute, sep, join as join4 } from "path";
149
485
  import { homedir as homedir3 } from "os";
@@ -533,18 +869,18 @@ var init_grep = __esm({
533
869
  const limit = max_results ?? 200;
534
870
  const ci = case_insensitive === true || String(case_insensitive) === "true";
535
871
  const tryRg = async () => {
536
- const args = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
537
- if (ci) args.push("-i");
538
- if (glob2) args.push("--glob", glob2);
539
- args.push("--", pattern, root);
540
- return execa2("rg", args, { reject: false, timeout: 2e4 });
872
+ const args2 = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
873
+ if (ci) args2.push("-i");
874
+ if (glob2) args2.push("--glob", glob2);
875
+ args2.push("--", pattern, root);
876
+ return execa2("rg", args2, { reject: false, timeout: 2e4 });
541
877
  };
542
878
  const tryGrep = async () => {
543
- const args = ["-R", "-n", "--color=never"];
544
- if (ci) args.push("-i");
545
- if (glob2) args.push("--include", glob2);
546
- args.push("--", pattern, root);
547
- return execa2("grep", args, { reject: false, timeout: 2e4 });
879
+ const args2 = ["-R", "-n", "--color=never"];
880
+ if (ci) args2.push("-i");
881
+ if (glob2) args2.push("--include", glob2);
882
+ args2.push("--", pattern, root);
883
+ return execa2("grep", args2, { reject: false, timeout: 2e4 });
548
884
  };
549
885
  try {
550
886
  let res;
@@ -891,6 +1227,7 @@ function toOllamaMessages(history, system) {
891
1227
  const ollamaMsg = { role: "assistant", content: text };
892
1228
  if (tool_uses.length > 0) {
893
1229
  ollamaMsg.tool_calls = tool_uses.map((u) => ({
1230
+ id: u.id,
894
1231
  function: { name: u.name, arguments: u.input }
895
1232
  }));
896
1233
  }
@@ -901,7 +1238,7 @@ function toOllamaMessages(history, system) {
901
1238
  const tool_results = msg.content.filter((b) => b.type === "tool_result");
902
1239
  const texts = msg.content.filter((b) => b.type === "text");
903
1240
  for (const tr of tool_results) {
904
- out.push({ role: "tool", content: tr.content });
1241
+ out.push({ role: "tool", content: tr.content, tool_call_id: tr.tool_use_id });
905
1242
  }
906
1243
  if (texts.length > 0) {
907
1244
  out.push({ role: "user", content: texts.map((t) => t.text).join("") });
@@ -947,9 +1284,9 @@ function tryParse(raw, knownToolNames) {
947
1284
  try {
948
1285
  const obj = JSON.parse(s);
949
1286
  const name = typeof obj.name === "string" ? obj.name : void 0;
950
- const args = obj.arguments ?? obj.parameters ?? obj.input ?? {};
1287
+ const args2 = obj.arguments ?? obj.parameters ?? obj.input ?? {};
951
1288
  if (!name || !knownToolNames.includes(name)) return null;
952
- return { function: { name, arguments: args } };
1289
+ return { function: { name, arguments: args2 } };
953
1290
  } catch {
954
1291
  return null;
955
1292
  }
@@ -1032,7 +1369,7 @@ async function* runAgent(opts) {
1032
1369
  const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
1033
1370
  if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
1034
1371
  try {
1035
- for await (const chunk of chat(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: NUM_PREDICT })) {
1372
+ for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: NUM_PREDICT })) {
1036
1373
  if (signal?.aborted) break;
1037
1374
  if (chunk.content) {
1038
1375
  text += chunk.content;
@@ -1191,7 +1528,7 @@ var init_loop = __esm({
1191
1528
  init_policy();
1192
1529
  init_adapter();
1193
1530
  MAX_TURNS = 25;
1194
- NUM_PREDICT = 4096;
1531
+ NUM_PREDICT = 8192;
1195
1532
  REPEAT_TAIL = 120;
1196
1533
  REPEAT_KILL = 4;
1197
1534
  }
@@ -1338,7 +1675,7 @@ function pad(s, n) {
1338
1675
  }
1339
1676
  async function resolveModels(modelsArg) {
1340
1677
  if (modelsArg !== "all") return modelsArg.split(",").map((m) => m.trim()).filter(Boolean);
1341
- return (await listModels()).filter((m) => !m.includes("cloud"));
1678
+ return (await listModels3()).filter((m) => !m.includes("cloud"));
1342
1679
  }
1343
1680
  function verdict(passed, total) {
1344
1681
  const ratio = total === 0 ? 0 : passed / total;
@@ -1385,10 +1722,10 @@ function printMatrix(models, picked, grid) {
1385
1722
  }
1386
1723
  console.log("\n + pass . fail ? not run");
1387
1724
  }
1388
- async function runEval(args) {
1725
+ async function runEval(args2) {
1389
1726
  const strip = (s) => (s ?? "").replace(/^-+/, "");
1390
- const modelsArg = strip(args[0]) || process.env.MIII_EVAL_MODEL || "all";
1391
- const filter = strip(args[1]);
1727
+ const modelsArg = strip(args2[0]) || process.env.MIII_EVAL_MODEL || "all";
1728
+ const filter = strip(args2[1]);
1392
1729
  const picked = filter ? scenarios.filter((s) => s.name.includes(filter)) : scenarios;
1393
1730
  if (picked.length === 0) {
1394
1731
  console.error(`No scenarios match "${filter}"`);
@@ -1421,36 +1758,12 @@ import { createElement } from "react";
1421
1758
 
1422
1759
  // src/ui/App.tsx
1423
1760
  init_client();
1424
- import { useState as useState5, useEffect as useEffect4 } from "react";
1761
+ init_config();
1762
+ import { useState as useState5, useEffect as useEffect4, useRef as useRef2 } from "react";
1425
1763
  import { Box as Box10, Text as Text10, useApp } from "ink";
1426
1764
  import { homedir as homedir6 } from "os";
1427
1765
  import { sep as sep2 } from "path";
1428
1766
 
1429
- // src/config.ts
1430
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1431
- import { join } from "path";
1432
- import { homedir } from "os";
1433
- var CONFIG_DIR = join(homedir(), ".miii");
1434
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
1435
- function loadConfig() {
1436
- if (!existsSync(CONFIG_PATH)) return {};
1437
- try {
1438
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1439
- } catch {
1440
- return {};
1441
- }
1442
- }
1443
- function saveConfig(config) {
1444
- mkdirSync(CONFIG_DIR, { recursive: true });
1445
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1446
- }
1447
- function setModel(model) {
1448
- saveConfig({ ...loadConfig(), model });
1449
- }
1450
- function setEffort(effort) {
1451
- saveConfig({ ...loadConfig(), effort });
1452
- }
1453
-
1454
1767
  // src/ui/WelcomeBlock.tsx
1455
1768
  import { Box, Text } from "ink";
1456
1769
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1483,27 +1796,10 @@ function WelcomeBlock({ model, activeCtx, effort, cwd }) {
1483
1796
  );
1484
1797
  }
1485
1798
 
1486
- // src/ui/ModelList.tsx
1487
- import { Box as Box2, Text as Text2 } from "ink";
1488
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1489
- function ModelList({ models, cursor, activeModel, showActive }) {
1490
- if (models.length === 0) {
1491
- return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1492
- "no models found. run: ollama pull ",
1493
- "<model>"
1494
- ] });
1495
- }
1496
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.map((m, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === cursor ? "blue" : void 0, dimColor: i !== cursor, children: [
1497
- i === cursor ? "\u276F " : " ",
1498
- m,
1499
- showActive && m === activeModel ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (active)" }) : null
1500
- ] }, m)) });
1501
- }
1502
-
1503
1799
  // src/ui/InputBar.tsx
1504
1800
  import { useEffect, useState } from "react";
1505
- import { Box as Box3, Text as Text3 } from "ink";
1506
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1801
+ import { Box as Box2, Text as Text2 } from "ink";
1802
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1507
1803
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1508
1804
  function InputBar({ input, disabled, processingLabel }) {
1509
1805
  const [frame, setFrame] = useState(0);
@@ -1512,8 +1808,8 @@ function InputBar({ input, disabled, processingLabel }) {
1512
1808
  const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 150);
1513
1809
  return () => clearInterval(t);
1514
1810
  }, [disabled]);
1515
- return /* @__PURE__ */ jsx3(
1516
- Box3,
1811
+ return /* @__PURE__ */ jsx2(
1812
+ Box2,
1517
1813
  {
1518
1814
  borderStyle: "single",
1519
1815
  borderTop: true,
@@ -1522,45 +1818,83 @@ function InputBar({ input, disabled, processingLabel }) {
1522
1818
  borderRight: false,
1523
1819
  borderColor: disabled ? "yellow" : "white dim",
1524
1820
  paddingX: 1,
1525
- children: disabled ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1526
- /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: SPIN[frame] + " " }),
1527
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, italic: true, children: processingLabel ?? "processing\u2026" }),
1528
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (esc to cancel)" })
1529
- ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
1530
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
1531
- /* @__PURE__ */ jsx3(Text3, { children: input }),
1532
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u258C" })
1821
+ children: disabled ? /* @__PURE__ */ jsxs2(Fragment, { children: [
1822
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: SPIN[frame] + " " }),
1823
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, italic: true, children: processingLabel ?? "processing\u2026" }),
1824
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (esc to cancel)" })
1825
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1826
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "> " }),
1827
+ /* @__PURE__ */ jsx2(Text2, { children: input }),
1828
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u258C" })
1533
1829
  ] })
1534
1830
  }
1535
1831
  );
1536
1832
  }
1537
1833
 
1538
1834
  // src/ui/ModelsView.tsx
1835
+ import { Box as Box3, Text as Text3 } from "ink";
1836
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1837
+ function ModelsView({ models, cursor, model, host, provider, effort, query, requireSelection }) {
1838
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
1839
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
1840
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1841
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
1842
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: provider }),
1843
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1844
+ " ",
1845
+ "host "
1846
+ ] }),
1847
+ /* @__PURE__ */ jsx3(Text3, { children: host })
1848
+ ] }),
1849
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1850
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "effort " }),
1851
+ /* @__PURE__ */ jsx3(Text3, { children: effort }),
1852
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (\u2190 \u2192)" })
1853
+ ] })
1854
+ ] }),
1855
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "select model" }),
1856
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: query ? `no models match "${query}"` : provider === "lmstudio" ? "no models. load a model in LM Studio and start the server." : "no models found." }) : models.map((m, i) => {
1857
+ const sel = i === cursor;
1858
+ return /* @__PURE__ */ jsxs3(Text3, { color: sel ? "blue" : void 0, dimColor: !sel, children: [
1859
+ sel ? "\u276F " : " ",
1860
+ m,
1861
+ m === model ? /* @__PURE__ */ jsx3(Text3, { color: "green", children: " \u25CF" }) : null
1862
+ ] }, m);
1863
+ }) }),
1864
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
1865
+ query ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `filter: ${query}` }) : null,
1866
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `\u2191\u2193 navigate enter select \u2190\u2192 effort tab provider type to filter${requireSelection ? " ctrl+c quit" : " esc close"}` })
1867
+ ] })
1868
+ ] });
1869
+ }
1870
+
1871
+ // src/ui/ProviderPicker.tsx
1539
1872
  import { Box as Box4, Text as Text4 } from "ink";
1540
1873
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1541
- function ModelsView({ models, cursor, model, ollamaHost, effort }) {
1874
+ function ProviderPicker({ entries, cursor, activeName, query }) {
1875
+ const nameWidth = Math.max(8, ...entries.map((e) => e.name.length));
1542
1876
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
1543
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
1544
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "config" }),
1545
- /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
1546
- /* @__PURE__ */ jsxs4(Text4, { children: [
1547
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "model " }),
1548
- /* @__PURE__ */ jsx4(Text4, { children: model ?? "\u2014" })
1877
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "select provider" }),
1878
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: entries.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "no providers configured \u2014 add one in ~/.miii/config.json" }) : entries.map((e, i) => {
1879
+ const sel = i === cursor;
1880
+ return /* @__PURE__ */ jsxs4(Text4, { color: sel ? "blue" : void 0, dimColor: !sel, children: [
1881
+ sel ? "\u276F " : " ",
1882
+ e.name.padEnd(nameWidth),
1883
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1884
+ " ",
1885
+ e.kind.padEnd(5)
1549
1886
  ] }),
1550
- /* @__PURE__ */ jsxs4(Text4, { children: [
1551
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "host " }),
1552
- /* @__PURE__ */ jsx4(Text4, { children: ollamaHost ?? "http://localhost:11434" })
1887
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1888
+ " ",
1889
+ e.entry.baseUrl
1553
1890
  ] }),
1554
- /* @__PURE__ */ jsxs4(Text4, { children: [
1555
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "effort " }),
1556
- /* @__PURE__ */ jsx4(Text4, { children: effort }),
1557
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (\u2190 \u2192)" })
1558
- ] })
1559
- ] })
1560
- ] }),
1561
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "switch model" }),
1562
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(ModelList, { models, cursor, activeModel: model, showActive: true }) }),
1563
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate enter switch \u2190\u2192 effort esc close" }) })
1891
+ e.name === activeName ? /* @__PURE__ */ jsx4(Text4, { color: "green", children: " \u25CF" }) : null
1892
+ ] }, e.name);
1893
+ }) }),
1894
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
1895
+ query ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `filter: ${query}` }) : null,
1896
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate enter select type to filter esc back" })
1897
+ ] })
1564
1898
  ] });
1565
1899
  }
1566
1900
 
@@ -1581,11 +1915,11 @@ function SessionsView({ sessions, cursor }) {
1581
1915
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, children: [
1582
1916
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
1583
1917
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: sessions.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "no saved sessions yet" }) : sessions.map((s, i) => {
1584
- const active = i === cursor;
1918
+ const active2 = i === cursor;
1585
1919
  const label = s.title;
1586
1920
  return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
1587
- /* @__PURE__ */ jsxs5(Text5, { color: active ? "blue" : void 0, dimColor: !active, children: [
1588
- active ? "\u276F " : " ",
1921
+ /* @__PURE__ */ jsxs5(Text5, { color: active2 ? "blue" : void 0, dimColor: !active2, children: [
1922
+ active2 ? "\u276F " : " ",
1589
1923
  label
1590
1924
  ] }),
1591
1925
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}` })
@@ -1599,7 +1933,8 @@ function SessionsView({ sessions, cursor }) {
1599
1933
  import { Box as Box6, Text as Text6 } from "ink";
1600
1934
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1601
1935
  var COMMANDS = [
1602
- { name: "/models", description: "switch model or adjust effort" },
1936
+ { name: "/models", description: "pick model \xB7 tab to change provider \xB7 \u2190\u2192 effort" },
1937
+ { name: "/provider", description: "open provider picker (configured in ~/.miii/config.json)" },
1603
1938
  { name: "/new", description: "save current session and start fresh" },
1604
1939
  { name: "/sessions", description: "list sessions and resume one" },
1605
1940
  { name: "/clear", description: "clear chat and reset context" },
@@ -1620,10 +1955,10 @@ function CommandPalette({ filter, cursor }) {
1620
1955
  paddingX: 1,
1621
1956
  children: [
1622
1957
  filtered.map((cmd2, i) => {
1623
- const active = i === cursor;
1958
+ const active2 = i === cursor;
1624
1959
  return /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
1625
- /* @__PURE__ */ jsxs6(Text6, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
1626
- active ? "\u276F " : " ",
1960
+ /* @__PURE__ */ jsxs6(Text6, { bold: active2, color: active2 ? "blue" : void 0, dimColor: !active2, children: [
1961
+ active2 ? "\u276F " : " ",
1627
1962
  cmd2.name.padEnd(nameWidth)
1628
1963
  ] }),
1629
1964
  /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: cmd2.description })
@@ -1645,7 +1980,7 @@ import { join as join2 } from "path";
1645
1980
  import { homedir as homedir2 } from "os";
1646
1981
  import { randomUUID } from "crypto";
1647
1982
  function encodeProjectDir(cwd) {
1648
- return cwd.replace(/[/\\]/g, "-").replace(/^-+/, "");
1983
+ return cwd.replace(/[:/\\]+/g, "-").replace(/^-+/, "");
1649
1984
  }
1650
1985
  var SESSION_DIR = join2(homedir2(), ".miii", "projects", encodeProjectDir(process.cwd()), "session");
1651
1986
  function newSessionId() {
@@ -1768,7 +2103,7 @@ Request:
1768
2103
  ${text.slice(0, 2e3)}`;
1769
2104
  try {
1770
2105
  let out = "";
1771
- for await (const chunk of chat(
2106
+ for await (const chunk of chat3(
1772
2107
  model,
1773
2108
  [{ role: "user", content: prompt }],
1774
2109
  void 0,
@@ -1850,9 +2185,9 @@ function FilePicker({ matches: matches2, cursor }) {
1850
2185
  paddingX: 1,
1851
2186
  children: [
1852
2187
  matches2.map((f, i) => {
1853
- const active = i === cursor;
1854
- return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
1855
- active ? "\u276F " : " ",
2188
+ const active2 = i === cursor;
2189
+ return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold: active2, color: active2 ? "blue" : void 0, dimColor: !active2, children: [
2190
+ active2 ? "\u276F " : " ",
1856
2191
  f
1857
2192
  ] }) }, f);
1858
2193
  }),
@@ -1983,11 +2318,13 @@ function FileEditBlock({
1983
2318
  removed > 0 ? `Added ${added} lines, removed ${removed} lines` : `Added ${added} lines`
1984
2319
  ] }) }),
1985
2320
  shown.map((ln, i) => {
1986
- const width = (process.stdout.columns ?? 80) - 6;
1987
- const content = `${ln.sign} ${ln.text}`.padEnd(width);
2321
+ const width = (process.stdout.columns ?? 80) - 6 - 20;
2322
+ const raw = `${ln.sign} ${ln.text}`;
2323
+ const content = raw.length > width ? raw.slice(0, width) : raw.padEnd(width);
1988
2324
  return /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(
1989
2325
  Text9,
1990
2326
  {
2327
+ wrap: "truncate",
1991
2328
  backgroundColor: ln.sign === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
1992
2329
  dimColor: ln.sign === " ",
1993
2330
  children: content
@@ -2449,6 +2786,7 @@ function useAgentRunner(model, activeCtx) {
2449
2786
  }
2450
2787
 
2451
2788
  // src/ui/hooks/useKeyboard.ts
2789
+ init_config();
2452
2790
  import { useInput } from "ink";
2453
2791
  var EFFORTS = ["low", "medium", "high"];
2454
2792
  function useKeyboard(opts) {
@@ -2463,6 +2801,9 @@ function useKeyboard(opts) {
2463
2801
  cfg,
2464
2802
  setCfg,
2465
2803
  setActiveCtx,
2804
+ providers,
2805
+ pickerQuery,
2806
+ setPickerQuery,
2466
2807
  agent,
2467
2808
  input,
2468
2809
  setInput,
@@ -2474,7 +2815,8 @@ function useKeyboard(opts) {
2474
2815
  setSessionId,
2475
2816
  sessions,
2476
2817
  setSessions,
2477
- setNotice
2818
+ setNotice,
2819
+ switchProvider
2478
2820
  } = opts;
2479
2821
  const {
2480
2822
  pendingPermissionRef,
@@ -2521,6 +2863,38 @@ function useKeyboard(opts) {
2521
2863
  abortRef.current.abort();
2522
2864
  return;
2523
2865
  }
2866
+ if (state === "providers") {
2867
+ if (key.upArrow) {
2868
+ setCursor((i) => Math.max(0, i - 1));
2869
+ return;
2870
+ }
2871
+ if (key.downArrow) {
2872
+ setCursor((i) => Math.min(providers.length - 1, i + 1));
2873
+ return;
2874
+ }
2875
+ if (key.escape) {
2876
+ setPickerQuery("");
2877
+ setCursor(() => 0);
2878
+ setState(cfg.model ? "models" : "select-model");
2879
+ return;
2880
+ }
2881
+ if (key.return && providers[cursor]) {
2882
+ const chosen = providers[cursor].name;
2883
+ setNotice(`switched to ${chosen}`);
2884
+ switchProvider(chosen);
2885
+ return;
2886
+ }
2887
+ if (key.backspace || key.delete) {
2888
+ setPickerQuery(pickerQuery.slice(0, -1));
2889
+ setCursor(() => 0);
2890
+ return;
2891
+ }
2892
+ if (char && !key.ctrl && !key.meta && char.length === 1 && char >= " ") {
2893
+ setPickerQuery(pickerQuery + char);
2894
+ setCursor(() => 0);
2895
+ }
2896
+ return;
2897
+ }
2524
2898
  if (state === "select-model" || state === "models") {
2525
2899
  if (key.upArrow) {
2526
2900
  setCursor((i) => Math.max(0, i - 1));
@@ -2535,21 +2909,45 @@ function useKeyboard(opts) {
2535
2909
  setModel(chosen);
2536
2910
  setCfg((c) => ({ ...c, model: chosen }));
2537
2911
  if (contexts[chosen]) setActiveCtx(contexts[chosen]);
2912
+ setPickerQuery("");
2913
+ setCursor(() => 0);
2538
2914
  setState("ready");
2539
2915
  return;
2540
2916
  }
2541
- if (state === "models") {
2542
- if (key.rightArrow) {
2543
- const next = EFFORTS[Math.min(EFFORTS.indexOf(effort) + 1, EFFORTS.length - 1)];
2544
- setEffort(next);
2545
- setCfg((c) => ({ ...c, effort: next }));
2546
- } else if (key.leftArrow) {
2547
- const next = EFFORTS[Math.max(EFFORTS.indexOf(effort) - 1, 0)];
2548
- setEffort(next);
2549
- setCfg((c) => ({ ...c, effort: next }));
2550
- } else if (key.escape) {
2917
+ if (key.tab) {
2918
+ setPickerQuery("");
2919
+ setCursor(() => 0);
2920
+ setState("providers");
2921
+ return;
2922
+ }
2923
+ if (key.rightArrow) {
2924
+ const next = EFFORTS[Math.min(EFFORTS.indexOf(effort) + 1, EFFORTS.length - 1)];
2925
+ setEffort(next);
2926
+ setCfg((c) => ({ ...c, effort: next }));
2927
+ return;
2928
+ }
2929
+ if (key.leftArrow) {
2930
+ const next = EFFORTS[Math.max(EFFORTS.indexOf(effort) - 1, 0)];
2931
+ setEffort(next);
2932
+ setCfg((c) => ({ ...c, effort: next }));
2933
+ return;
2934
+ }
2935
+ if (key.escape) {
2936
+ if (state === "models") {
2937
+ setPickerQuery("");
2938
+ setCursor(() => 0);
2551
2939
  setState("ready");
2552
2940
  }
2941
+ return;
2942
+ }
2943
+ if (key.backspace || key.delete) {
2944
+ setPickerQuery(pickerQuery.slice(0, -1));
2945
+ setCursor(() => 0);
2946
+ return;
2947
+ }
2948
+ if (char && !key.ctrl && !key.meta && char.length === 1 && char >= " ") {
2949
+ setPickerQuery(pickerQuery + char);
2950
+ setCursor(() => 0);
2553
2951
  }
2554
2952
  return;
2555
2953
  }
@@ -2652,8 +3050,13 @@ function useKeyboard(opts) {
2652
3050
  if (key.return) {
2653
3051
  const trimmed = input.trim();
2654
3052
  if (trimmed === "/models") {
3053
+ setPickerQuery("");
2655
3054
  setCursor(() => Math.max(0, models.findIndex((m) => m === cfg.model)));
2656
3055
  setState("models");
3056
+ } else if (trimmed === "/provider" || trimmed === "/providers") {
3057
+ setPickerQuery("");
3058
+ setCursor(() => Math.max(0, providers.findIndex((p) => p.name === cfg.provider)));
3059
+ setState("providers");
2657
3060
  } else if (trimmed === "/clear") {
2658
3061
  clearSession();
2659
3062
  } else if (trimmed === "/new") {
@@ -2666,6 +3069,15 @@ function useKeyboard(opts) {
2666
3069
  setState("sessions");
2667
3070
  } else if (trimmed === "/exit") {
2668
3071
  exit();
3072
+ } else if (trimmed.startsWith("/provider ")) {
3073
+ const p = trimmed.slice("/provider ".length).trim();
3074
+ const names = providers.map((x) => x.name);
3075
+ if (names.includes(p)) {
3076
+ setNotice(`switched to ${p}`);
3077
+ switchProvider(p);
3078
+ } else {
3079
+ setNotice(`unknown provider "${p}" \u2014 configured: ${names.join(", ")}`);
3080
+ }
2669
3081
  } else if (trimmed) {
2670
3082
  setNotice(null);
2671
3083
  if (!agentHistory.length && cfg.model) {
@@ -2748,8 +3160,9 @@ function App() {
2748
3160
  const [activeCtx, setActiveCtx] = useState5(null);
2749
3161
  const [state, setState] = useState5("loading");
2750
3162
  const [cursor, setCursor] = useState5(0);
3163
+ const [pickerQuery, setPickerQuery] = useState5("");
2751
3164
  const [updateAvailable, setUpdateAvailable] = useState5(null);
2752
- const [ollamaDown, setOllamaDown] = useState5(false);
3165
+ const [providerDown, setProviderDown] = useState5(false);
2753
3166
  const [sessionId, setSessionId] = useState5(() => newSessionId());
2754
3167
  const [sessions, setSessions] = useState5([]);
2755
3168
  const [notice, setNotice] = useState5(null);
@@ -2765,36 +3178,67 @@ function App() {
2765
3178
  useEffect4(() => {
2766
3179
  if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
2767
3180
  }, [agent.agentHistory, sessionId]);
2768
- useEffect4(() => {
2769
- listModels().then((m) => {
3181
+ const loadGen = useRef2(0);
3182
+ const loadModels = (afterProvider = false) => {
3183
+ const gen = ++loadGen.current;
3184
+ const stale = () => gen !== loadGen.current;
3185
+ setProviderDown(false);
3186
+ listModels3().then((m) => {
3187
+ if (stale()) return;
2770
3188
  setModels(m);
2771
- setState(cfg.model ? "ready" : "select-model");
2772
- Promise.all(m.map((name) => modelContext(name).then((ctx) => [name, ctx]))).then((pairs) => {
3189
+ const hasModel = !!cfg.model && m.includes(cfg.model);
3190
+ if (afterProvider) {
3191
+ setState(hasModel ? "models" : "select-model");
3192
+ } else {
3193
+ setState(hasModel ? "ready" : "select-model");
3194
+ }
3195
+ Promise.all(m.map((name) => modelContext3(name).then((ctx) => [name, ctx]))).then((pairs) => {
3196
+ if (stale()) return;
2773
3197
  const map = Object.fromEntries(pairs);
2774
3198
  setContexts(map);
2775
- const active = cfg.model ?? m[0];
2776
- if (active && map[active]) setActiveCtx(map[active]);
3199
+ const active2 = (hasModel ? cfg.model : void 0) ?? m[0];
3200
+ if (active2 && map[active2]) setActiveCtx(map[active2]);
2777
3201
  }).catch(() => {
2778
3202
  });
2779
3203
  }).catch((err) => {
3204
+ if (stale()) return;
2780
3205
  const msg = err instanceof Error ? err.message : String(err);
2781
- agent.setError(ollamaInstalled() ? msg : OLLAMA_NOT_INSTALLED);
2782
- setOllamaDown(true);
3206
+ agent.setError(isAvailable3() ? msg : NOT_AVAILABLE());
3207
+ setProviderDown(true);
2783
3208
  setModels([]);
2784
- setState(cfg.model ? "ready" : "select-model");
3209
+ setPickerQuery("");
3210
+ setCursor(() => 0);
3211
+ setState("ready");
2785
3212
  });
2786
- }, []);
3213
+ };
3214
+ useEffect4(loadModels, []);
3215
+ function switchProvider(p) {
3216
+ setProvider(p);
3217
+ setCfg((c) => ({ ...c, provider: p }));
3218
+ setPickerQuery("");
3219
+ setCursor(() => 0);
3220
+ agent.setError(null);
3221
+ loadModels(true);
3222
+ }
3223
+ const { name: provName, entry: provEntry } = resolveProvider(cfg);
3224
+ const q = pickerQuery.toLowerCase();
3225
+ const filteredModels = q ? models.filter((m) => m.toLowerCase().includes(q)) : models;
3226
+ const allProviders = providerEntries(cfg);
3227
+ const filteredProviders = q ? allProviders.filter((p) => p.name.toLowerCase().includes(q)) : allProviders;
2787
3228
  useKeyboard({
2788
3229
  exit,
2789
3230
  state,
2790
3231
  setState,
2791
- models,
3232
+ models: filteredModels,
2792
3233
  cursor,
2793
3234
  setCursor,
2794
3235
  contexts,
2795
3236
  cfg,
2796
3237
  setCfg,
2797
3238
  setActiveCtx,
3239
+ providers: filteredProviders,
3240
+ pickerQuery,
3241
+ setPickerQuery,
2798
3242
  agent,
2799
3243
  input,
2800
3244
  setInput,
@@ -2806,7 +3250,8 @@ function App() {
2806
3250
  setSessionId,
2807
3251
  sessions,
2808
3252
  setSessions,
2809
- setNotice
3253
+ setNotice,
3254
+ switchProvider
2810
3255
  });
2811
3256
  const effort = cfg.effort ?? "medium";
2812
3257
  const contextWarning = (() => {
@@ -2819,7 +3264,7 @@ function App() {
2819
3264
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
2820
3265
  /* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error }),
2821
3266
  updateAvailable && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` }) }),
2822
- state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "connecting to ollama\u2026" }) }),
3267
+ state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
2823
3268
  agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
2824
3269
  ChatView,
2825
3270
  {
@@ -2830,19 +3275,26 @@ function App() {
2830
3275
  error: agent.error
2831
3276
  }
2832
3277
  ),
2833
- state === "select-model" && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginLeft: 2, children: [
2834
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "no model configured \u2014 select one" }),
2835
- /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(ModelList, { models, cursor }) }),
2836
- models.length > 0 && /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2191\u2193 navigate enter select ctrl+c quit" }) })
2837
- ] }),
2838
- state === "models" && /* @__PURE__ */ jsx10(
3278
+ (state === "select-model" || state === "models") && /* @__PURE__ */ jsx10(
2839
3279
  ModelsView,
2840
3280
  {
2841
- models,
3281
+ models: filteredModels,
2842
3282
  cursor,
2843
3283
  model: cfg.model,
2844
- ollamaHost: cfg.ollamaHost,
2845
- effort
3284
+ host: provEntry.baseUrl,
3285
+ provider: provName,
3286
+ effort,
3287
+ query: pickerQuery,
3288
+ requireSelection: state === "select-model"
3289
+ }
3290
+ ),
3291
+ state === "providers" && /* @__PURE__ */ jsx10(
3292
+ ProviderPicker,
3293
+ {
3294
+ entries: filteredProviders,
3295
+ cursor,
3296
+ activeName: provName,
3297
+ query: pickerQuery
2846
3298
  }
2847
3299
  ),
2848
3300
  state === "sessions" && /* @__PURE__ */ jsx10(SessionsView, { sessions, cursor }),
@@ -2870,24 +3322,33 @@ function App() {
2870
3322
  if (!m) return null;
2871
3323
  return /* @__PURE__ */ jsx10(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
2872
3324
  })(),
2873
- !ollamaDown && /* @__PURE__ */ jsxs10(Fragment2, { children: [
2874
- /* @__PURE__ */ jsx10(InputBar, { input, disabled: agent.busy, processingLabel: agent.processingLabel }),
2875
- !agent.busy && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "type / to see commands" }) })
2876
- ] })
3325
+ /* @__PURE__ */ jsx10(InputBar, { input, disabled: agent.busy, processingLabel: agent.processingLabel }),
3326
+ !agent.busy && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) })
2877
3327
  ] })
2878
3328
  ] });
2879
3329
  }
2880
3330
 
2881
3331
  // src/cli.tsx
2882
3332
  init_spill();
3333
+ init_config();
2883
3334
  cleanupSpill();
2884
- var [, , cmd, ...rest] = process.argv;
3335
+ var args = process.argv.slice(2);
3336
+ var cmd;
3337
+ for (let i = 0; i < args.length; i++) {
3338
+ if ((args[i] === "--provider" || args[i] === "-p") && i + 1 < args.length) {
3339
+ const p = args[++i];
3340
+ if (listProviders().includes(p)) setProvider(p);
3341
+ } else if (!cmd) {
3342
+ cmd = args[i];
3343
+ }
3344
+ }
2885
3345
  if (cmd === "update" || cmd === "--update" || cmd === "-u") {
2886
3346
  const { spawnSync } = await import("child_process");
2887
3347
  console.log("Updating miii-agent\u2026");
2888
3348
  const r = spawnSync("npm", ["i", "-g", "miii-agent@latest"], { stdio: "inherit", shell: process.platform === "win32" });
2889
3349
  process.exit(r.status ?? 1);
2890
3350
  } else if (cmd === "doctor" || cmd === "eval") {
3351
+ const rest = args.filter((a) => a !== cmd);
2891
3352
  const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
2892
3353
  process.exit(await runEval2(rest));
2893
3354
  } else {