miii-agent 0.1.13 → 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 (2) hide show
  1. package/dist/cli.js +627 -168
  2. 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
  }),
@@ -2451,6 +2786,7 @@ function useAgentRunner(model, activeCtx) {
2451
2786
  }
2452
2787
 
2453
2788
  // src/ui/hooks/useKeyboard.ts
2789
+ init_config();
2454
2790
  import { useInput } from "ink";
2455
2791
  var EFFORTS = ["low", "medium", "high"];
2456
2792
  function useKeyboard(opts) {
@@ -2465,6 +2801,9 @@ function useKeyboard(opts) {
2465
2801
  cfg,
2466
2802
  setCfg,
2467
2803
  setActiveCtx,
2804
+ providers,
2805
+ pickerQuery,
2806
+ setPickerQuery,
2468
2807
  agent,
2469
2808
  input,
2470
2809
  setInput,
@@ -2476,7 +2815,8 @@ function useKeyboard(opts) {
2476
2815
  setSessionId,
2477
2816
  sessions,
2478
2817
  setSessions,
2479
- setNotice
2818
+ setNotice,
2819
+ switchProvider
2480
2820
  } = opts;
2481
2821
  const {
2482
2822
  pendingPermissionRef,
@@ -2523,6 +2863,38 @@ function useKeyboard(opts) {
2523
2863
  abortRef.current.abort();
2524
2864
  return;
2525
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
+ }
2526
2898
  if (state === "select-model" || state === "models") {
2527
2899
  if (key.upArrow) {
2528
2900
  setCursor((i) => Math.max(0, i - 1));
@@ -2537,21 +2909,45 @@ function useKeyboard(opts) {
2537
2909
  setModel(chosen);
2538
2910
  setCfg((c) => ({ ...c, model: chosen }));
2539
2911
  if (contexts[chosen]) setActiveCtx(contexts[chosen]);
2912
+ setPickerQuery("");
2913
+ setCursor(() => 0);
2540
2914
  setState("ready");
2541
2915
  return;
2542
2916
  }
2543
- if (state === "models") {
2544
- if (key.rightArrow) {
2545
- const next = EFFORTS[Math.min(EFFORTS.indexOf(effort) + 1, EFFORTS.length - 1)];
2546
- setEffort(next);
2547
- setCfg((c) => ({ ...c, effort: next }));
2548
- } else if (key.leftArrow) {
2549
- const next = EFFORTS[Math.max(EFFORTS.indexOf(effort) - 1, 0)];
2550
- setEffort(next);
2551
- setCfg((c) => ({ ...c, effort: next }));
2552
- } 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);
2553
2939
  setState("ready");
2554
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);
2555
2951
  }
2556
2952
  return;
2557
2953
  }
@@ -2654,8 +3050,13 @@ function useKeyboard(opts) {
2654
3050
  if (key.return) {
2655
3051
  const trimmed = input.trim();
2656
3052
  if (trimmed === "/models") {
3053
+ setPickerQuery("");
2657
3054
  setCursor(() => Math.max(0, models.findIndex((m) => m === cfg.model)));
2658
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");
2659
3060
  } else if (trimmed === "/clear") {
2660
3061
  clearSession();
2661
3062
  } else if (trimmed === "/new") {
@@ -2668,6 +3069,15 @@ function useKeyboard(opts) {
2668
3069
  setState("sessions");
2669
3070
  } else if (trimmed === "/exit") {
2670
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
+ }
2671
3081
  } else if (trimmed) {
2672
3082
  setNotice(null);
2673
3083
  if (!agentHistory.length && cfg.model) {
@@ -2750,8 +3160,9 @@ function App() {
2750
3160
  const [activeCtx, setActiveCtx] = useState5(null);
2751
3161
  const [state, setState] = useState5("loading");
2752
3162
  const [cursor, setCursor] = useState5(0);
3163
+ const [pickerQuery, setPickerQuery] = useState5("");
2753
3164
  const [updateAvailable, setUpdateAvailable] = useState5(null);
2754
- const [ollamaDown, setOllamaDown] = useState5(false);
3165
+ const [providerDown, setProviderDown] = useState5(false);
2755
3166
  const [sessionId, setSessionId] = useState5(() => newSessionId());
2756
3167
  const [sessions, setSessions] = useState5([]);
2757
3168
  const [notice, setNotice] = useState5(null);
@@ -2767,36 +3178,67 @@ function App() {
2767
3178
  useEffect4(() => {
2768
3179
  if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
2769
3180
  }, [agent.agentHistory, sessionId]);
2770
- useEffect4(() => {
2771
- 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;
2772
3188
  setModels(m);
2773
- setState(cfg.model ? "ready" : "select-model");
2774
- 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;
2775
3197
  const map = Object.fromEntries(pairs);
2776
3198
  setContexts(map);
2777
- const active = cfg.model ?? m[0];
2778
- 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]);
2779
3201
  }).catch(() => {
2780
3202
  });
2781
3203
  }).catch((err) => {
3204
+ if (stale()) return;
2782
3205
  const msg = err instanceof Error ? err.message : String(err);
2783
- agent.setError(ollamaInstalled() ? msg : OLLAMA_NOT_INSTALLED);
2784
- setOllamaDown(true);
3206
+ agent.setError(isAvailable3() ? msg : NOT_AVAILABLE());
3207
+ setProviderDown(true);
2785
3208
  setModels([]);
2786
- setState(cfg.model ? "ready" : "select-model");
3209
+ setPickerQuery("");
3210
+ setCursor(() => 0);
3211
+ setState("ready");
2787
3212
  });
2788
- }, []);
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;
2789
3228
  useKeyboard({
2790
3229
  exit,
2791
3230
  state,
2792
3231
  setState,
2793
- models,
3232
+ models: filteredModels,
2794
3233
  cursor,
2795
3234
  setCursor,
2796
3235
  contexts,
2797
3236
  cfg,
2798
3237
  setCfg,
2799
3238
  setActiveCtx,
3239
+ providers: filteredProviders,
3240
+ pickerQuery,
3241
+ setPickerQuery,
2800
3242
  agent,
2801
3243
  input,
2802
3244
  setInput,
@@ -2808,7 +3250,8 @@ function App() {
2808
3250
  setSessionId,
2809
3251
  sessions,
2810
3252
  setSessions,
2811
- setNotice
3253
+ setNotice,
3254
+ switchProvider
2812
3255
  });
2813
3256
  const effort = cfg.effort ?? "medium";
2814
3257
  const contextWarning = (() => {
@@ -2821,7 +3264,7 @@ function App() {
2821
3264
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
2822
3265
  /* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error }),
2823
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` }) }),
2824
- 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` }) }),
2825
3268
  agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
2826
3269
  ChatView,
2827
3270
  {
@@ -2832,19 +3275,26 @@ function App() {
2832
3275
  error: agent.error
2833
3276
  }
2834
3277
  ),
2835
- state === "select-model" && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginLeft: 2, children: [
2836
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "no model configured \u2014 select one" }),
2837
- /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(ModelList, { models, cursor }) }),
2838
- models.length > 0 && /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2191\u2193 navigate enter select ctrl+c quit" }) })
2839
- ] }),
2840
- state === "models" && /* @__PURE__ */ jsx10(
3278
+ (state === "select-model" || state === "models") && /* @__PURE__ */ jsx10(
2841
3279
  ModelsView,
2842
3280
  {
2843
- models,
3281
+ models: filteredModels,
2844
3282
  cursor,
2845
3283
  model: cfg.model,
2846
- ollamaHost: cfg.ollamaHost,
2847
- 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
2848
3298
  }
2849
3299
  ),
2850
3300
  state === "sessions" && /* @__PURE__ */ jsx10(SessionsView, { sessions, cursor }),
@@ -2872,24 +3322,33 @@ function App() {
2872
3322
  if (!m) return null;
2873
3323
  return /* @__PURE__ */ jsx10(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
2874
3324
  })(),
2875
- !ollamaDown && /* @__PURE__ */ jsxs10(Fragment2, { children: [
2876
- /* @__PURE__ */ jsx10(InputBar, { input, disabled: agent.busy, processingLabel: agent.processingLabel }),
2877
- !agent.busy && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "type / to see commands" }) })
2878
- ] })
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" }) })
2879
3327
  ] })
2880
3328
  ] });
2881
3329
  }
2882
3330
 
2883
3331
  // src/cli.tsx
2884
3332
  init_spill();
3333
+ init_config();
2885
3334
  cleanupSpill();
2886
- 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
+ }
2887
3345
  if (cmd === "update" || cmd === "--update" || cmd === "-u") {
2888
3346
  const { spawnSync } = await import("child_process");
2889
3347
  console.log("Updating miii-agent\u2026");
2890
3348
  const r = spawnSync("npm", ["i", "-g", "miii-agent@latest"], { stdio: "inherit", shell: process.platform === "win32" });
2891
3349
  process.exit(r.status ?? 1);
2892
3350
  } else if (cmd === "doctor" || cmd === "eval") {
3351
+ const rest = args.filter((a) => a !== cmd);
2893
3352
  const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
2894
3353
  process.exit(await runEval2(rest));
2895
3354
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Terminal AI coding agent powered by Ollama",
5
5
  "type": "module",
6
6
  "bin": {