miii-agent 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/cli.js +722 -195
- package/package.json +2 -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/
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
134
|
-
var
|
|
135
|
-
"src/ollama
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
537
|
-
if (ci)
|
|
538
|
-
if (glob2)
|
|
539
|
-
|
|
540
|
-
return execa2("rg",
|
|
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
|
|
544
|
-
if (ci)
|
|
545
|
-
if (glob2)
|
|
546
|
-
|
|
547
|
-
return execa2("grep",
|
|
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
|
|
1287
|
+
const args2 = obj.arguments ?? obj.parameters ?? obj.input ?? {};
|
|
951
1288
|
if (!name || !knownToolNames.includes(name)) return null;
|
|
952
|
-
return { function: { name, arguments:
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
1725
|
+
async function runEval(args2) {
|
|
1389
1726
|
const strip = (s) => (s ?? "").replace(/^-+/, "");
|
|
1390
|
-
const modelsArg = strip(
|
|
1391
|
-
const filter = strip(
|
|
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,40 +1758,16 @@ import { createElement } from "react";
|
|
|
1421
1758
|
|
|
1422
1759
|
// src/ui/App.tsx
|
|
1423
1760
|
init_client();
|
|
1424
|
-
|
|
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";
|
|
1457
|
-
function WelcomeBlock({ model, activeCtx, effort, cwd }) {
|
|
1770
|
+
function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable }) {
|
|
1458
1771
|
const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
|
|
1459
1772
|
return /* @__PURE__ */ jsxs(
|
|
1460
1773
|
Box,
|
|
@@ -1477,43 +1790,27 @@ function WelcomeBlock({ model, activeCtx, effort, cwd }) {
|
|
|
1477
1790
|
" effort"
|
|
1478
1791
|
] })
|
|
1479
1792
|
] }),
|
|
1480
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
|
|
1793
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd }),
|
|
1794
|
+
updateAvailable && /* @__PURE__ */ jsx(Text, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` })
|
|
1481
1795
|
]
|
|
1482
1796
|
}
|
|
1483
1797
|
);
|
|
1484
1798
|
}
|
|
1485
1799
|
|
|
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
1800
|
// src/ui/InputBar.tsx
|
|
1504
|
-
import { useEffect, useState } from "react";
|
|
1505
|
-
import { Box as
|
|
1506
|
-
import { Fragment, jsx as
|
|
1801
|
+
import { memo, useEffect, useState } from "react";
|
|
1802
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
1803
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1507
1804
|
var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1508
|
-
|
|
1805
|
+
var InputBar = memo(function InputBar2({ input, disabled, processingLabel }) {
|
|
1509
1806
|
const [frame, setFrame] = useState(0);
|
|
1510
1807
|
useEffect(() => {
|
|
1511
1808
|
if (!disabled) return;
|
|
1512
1809
|
const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 150);
|
|
1513
1810
|
return () => clearInterval(t);
|
|
1514
1811
|
}, [disabled]);
|
|
1515
|
-
return /* @__PURE__ */
|
|
1516
|
-
|
|
1812
|
+
return /* @__PURE__ */ jsx2(
|
|
1813
|
+
Box2,
|
|
1517
1814
|
{
|
|
1518
1815
|
borderStyle: "single",
|
|
1519
1816
|
borderTop: true,
|
|
@@ -1522,51 +1819,94 @@ function InputBar({ input, disabled, processingLabel }) {
|
|
|
1522
1819
|
borderRight: false,
|
|
1523
1820
|
borderColor: disabled ? "yellow" : "white dim",
|
|
1524
1821
|
paddingX: 1,
|
|
1525
|
-
children: disabled ? /* @__PURE__ */
|
|
1526
|
-
/* @__PURE__ */
|
|
1527
|
-
/* @__PURE__ */
|
|
1528
|
-
/* @__PURE__ */
|
|
1529
|
-
] }) : /* @__PURE__ */
|
|
1530
|
-
/* @__PURE__ */
|
|
1531
|
-
/* @__PURE__ */
|
|
1532
|
-
/* @__PURE__ */
|
|
1822
|
+
children: disabled ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1823
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: SPIN[frame] + " " }),
|
|
1824
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, italic: true, children: processingLabel ?? "processing\u2026" }),
|
|
1825
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (esc to cancel)" })
|
|
1826
|
+
] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1827
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "> " }),
|
|
1828
|
+
/* @__PURE__ */ jsx2(Text2, { children: input }),
|
|
1829
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u258C" })
|
|
1533
1830
|
] })
|
|
1534
1831
|
}
|
|
1535
1832
|
);
|
|
1536
|
-
}
|
|
1833
|
+
});
|
|
1537
1834
|
|
|
1538
1835
|
// src/ui/ModelsView.tsx
|
|
1836
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
1837
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1838
|
+
function ModelsView({ models, cursor, model, host, provider, effort, query, requireSelection }) {
|
|
1839
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
|
|
1840
|
+
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
|
|
1841
|
+
/* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
|
|
1842
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
|
|
1843
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: provider }),
|
|
1844
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1845
|
+
" ",
|
|
1846
|
+
"host "
|
|
1847
|
+
] }),
|
|
1848
|
+
/* @__PURE__ */ jsx3(Text3, { children: host })
|
|
1849
|
+
] }),
|
|
1850
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1851
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "effort " }),
|
|
1852
|
+
/* @__PURE__ */ jsx3(Text3, { children: effort }),
|
|
1853
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (\u2190 \u2192)" })
|
|
1854
|
+
] })
|
|
1855
|
+
] }),
|
|
1856
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "select model" }),
|
|
1857
|
+
/* @__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) => {
|
|
1858
|
+
const sel = i === cursor;
|
|
1859
|
+
return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", color: sel ? "blue" : void 0, dimColor: !sel, children: [
|
|
1860
|
+
sel ? "\u276F " : " ",
|
|
1861
|
+
m,
|
|
1862
|
+
m === model ? /* @__PURE__ */ jsx3(Text3, { color: "green", children: " \u25CF" }) : null
|
|
1863
|
+
] }, m);
|
|
1864
|
+
}) }),
|
|
1865
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
1866
|
+
query ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `filter: ${query}` }) : null,
|
|
1867
|
+
/* @__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"}` })
|
|
1868
|
+
] })
|
|
1869
|
+
] });
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/ui/ProviderPicker.tsx
|
|
1539
1873
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
1540
1874
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1541
|
-
function
|
|
1875
|
+
function ProviderPicker({ entries, cursor, activeName, query }) {
|
|
1876
|
+
const nameWidth = Math.max(8, ...entries.map((e) => e.name.length));
|
|
1542
1877
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
|
|
1543
|
-
/* @__PURE__ */
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1878
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "select provider" }),
|
|
1879
|
+
/* @__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) => {
|
|
1880
|
+
const sel = i === cursor;
|
|
1881
|
+
return /* @__PURE__ */ jsxs4(Text4, { color: sel ? "blue" : void 0, dimColor: !sel, children: [
|
|
1882
|
+
sel ? "\u276F " : " ",
|
|
1883
|
+
e.name.padEnd(nameWidth),
|
|
1884
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1885
|
+
" ",
|
|
1886
|
+
e.kind.padEnd(5)
|
|
1549
1887
|
] }),
|
|
1550
|
-
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1551
|
-
|
|
1552
|
-
|
|
1888
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1889
|
+
" ",
|
|
1890
|
+
e.entry.baseUrl
|
|
1553
1891
|
] }),
|
|
1554
|
-
/* @__PURE__ */
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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" }) })
|
|
1892
|
+
e.name === activeName ? /* @__PURE__ */ jsx4(Text4, { color: "green", children: " \u25CF" }) : null
|
|
1893
|
+
] }, e.name);
|
|
1894
|
+
}) }),
|
|
1895
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
1896
|
+
query ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `filter: ${query}` }) : null,
|
|
1897
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate enter select type to filter esc back" })
|
|
1898
|
+
] })
|
|
1564
1899
|
] });
|
|
1565
1900
|
}
|
|
1566
1901
|
|
|
1567
1902
|
// src/ui/SessionsView.tsx
|
|
1568
1903
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
1569
1904
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1905
|
+
function truncate(s, max) {
|
|
1906
|
+
if (max <= 0) return "";
|
|
1907
|
+
if (s.length <= max) return s;
|
|
1908
|
+
return s.slice(0, Math.max(0, max - 1)) + "\u2026";
|
|
1909
|
+
}
|
|
1570
1910
|
function relativeTime(iso) {
|
|
1571
1911
|
const diff = Date.now() - new Date(iso).getTime();
|
|
1572
1912
|
const min = Math.floor(diff / 6e4);
|
|
@@ -1581,14 +1921,17 @@ function SessionsView({ sessions, cursor }) {
|
|
|
1581
1921
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, children: [
|
|
1582
1922
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
|
|
1583
1923
|
/* @__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
|
|
1585
|
-
const
|
|
1924
|
+
const active2 = i === cursor;
|
|
1925
|
+
const meta = `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}`;
|
|
1926
|
+
const cols = process.stdout.columns ?? 80;
|
|
1927
|
+
const titleMax = cols - 9 - meta.length;
|
|
1928
|
+
const label = truncate(s.title, titleMax);
|
|
1586
1929
|
return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
|
|
1587
|
-
/* @__PURE__ */ jsxs5(Text5, { color:
|
|
1588
|
-
|
|
1930
|
+
/* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", color: active2 ? "blue" : void 0, dimColor: !active2, children: [
|
|
1931
|
+
active2 ? "\u276F " : " ",
|
|
1589
1932
|
label
|
|
1590
1933
|
] }),
|
|
1591
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children:
|
|
1934
|
+
/* @__PURE__ */ jsx5(Text5, { wrap: "truncate", dimColor: true, children: meta })
|
|
1592
1935
|
] }, s.id);
|
|
1593
1936
|
}) }),
|
|
1594
1937
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate enter resume d delete esc cancel" }) })
|
|
@@ -1599,7 +1942,8 @@ function SessionsView({ sessions, cursor }) {
|
|
|
1599
1942
|
import { Box as Box6, Text as Text6 } from "ink";
|
|
1600
1943
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1601
1944
|
var COMMANDS = [
|
|
1602
|
-
{ name: "/models", description: "
|
|
1945
|
+
{ name: "/models", description: "pick model \xB7 tab to change provider \xB7 \u2190\u2192 effort" },
|
|
1946
|
+
{ name: "/provider", description: "open provider picker (configured in ~/.miii/config.json)" },
|
|
1603
1947
|
{ name: "/new", description: "save current session and start fresh" },
|
|
1604
1948
|
{ name: "/sessions", description: "list sessions and resume one" },
|
|
1605
1949
|
{ name: "/clear", description: "clear chat and reset context" },
|
|
@@ -1620,10 +1964,10 @@ function CommandPalette({ filter, cursor }) {
|
|
|
1620
1964
|
paddingX: 1,
|
|
1621
1965
|
children: [
|
|
1622
1966
|
filtered.map((cmd2, i) => {
|
|
1623
|
-
const
|
|
1967
|
+
const active2 = i === cursor;
|
|
1624
1968
|
return /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
|
|
1625
|
-
/* @__PURE__ */ jsxs6(Text6, { bold:
|
|
1626
|
-
|
|
1969
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: active2, color: active2 ? "blue" : void 0, dimColor: !active2, children: [
|
|
1970
|
+
active2 ? "\u276F " : " ",
|
|
1627
1971
|
cmd2.name.padEnd(nameWidth)
|
|
1628
1972
|
] }),
|
|
1629
1973
|
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: cmd2.description })
|
|
@@ -1645,7 +1989,7 @@ import { join as join2 } from "path";
|
|
|
1645
1989
|
import { homedir as homedir2 } from "os";
|
|
1646
1990
|
import { randomUUID } from "crypto";
|
|
1647
1991
|
function encodeProjectDir(cwd) {
|
|
1648
|
-
return cwd.replace(/[
|
|
1992
|
+
return cwd.replace(/[:/\\]+/g, "-").replace(/^-+/, "");
|
|
1649
1993
|
}
|
|
1650
1994
|
var SESSION_DIR = join2(homedir2(), ".miii", "projects", encodeProjectDir(process.cwd()), "session");
|
|
1651
1995
|
function newSessionId() {
|
|
@@ -1768,7 +2112,7 @@ Request:
|
|
|
1768
2112
|
${text.slice(0, 2e3)}`;
|
|
1769
2113
|
try {
|
|
1770
2114
|
let out = "";
|
|
1771
|
-
for await (const chunk of
|
|
2115
|
+
for await (const chunk of chat3(
|
|
1772
2116
|
model,
|
|
1773
2117
|
[{ role: "user", content: prompt }],
|
|
1774
2118
|
void 0,
|
|
@@ -1850,9 +2194,9 @@ function FilePicker({ matches: matches2, cursor }) {
|
|
|
1850
2194
|
paddingX: 1,
|
|
1851
2195
|
children: [
|
|
1852
2196
|
matches2.map((f, i) => {
|
|
1853
|
-
const
|
|
1854
|
-
return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold:
|
|
1855
|
-
|
|
2197
|
+
const active2 = i === cursor;
|
|
2198
|
+
return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold: active2, color: active2 ? "blue" : void 0, dimColor: !active2, children: [
|
|
2199
|
+
active2 ? "\u276F " : " ",
|
|
1856
2200
|
f
|
|
1857
2201
|
] }) }, f);
|
|
1858
2202
|
}),
|
|
@@ -1863,8 +2207,9 @@ function FilePicker({ matches: matches2, cursor }) {
|
|
|
1863
2207
|
}
|
|
1864
2208
|
|
|
1865
2209
|
// src/ui/ChatView.tsx
|
|
1866
|
-
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
2210
|
+
import { memo as memo2, useState as useState3, useEffect as useEffect3 } from "react";
|
|
1867
2211
|
import { Box as Box9, Text as Text9 } from "ink";
|
|
2212
|
+
import { highlight } from "cli-highlight";
|
|
1868
2213
|
|
|
1869
2214
|
// src/ui/ThinkingBlock.tsx
|
|
1870
2215
|
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
@@ -1957,6 +2302,55 @@ function countLines(s) {
|
|
|
1957
2302
|
if (!s) return 0;
|
|
1958
2303
|
return s.split("\n").length;
|
|
1959
2304
|
}
|
|
2305
|
+
var EXT_LANG = {
|
|
2306
|
+
ts: "typescript",
|
|
2307
|
+
tsx: "typescript",
|
|
2308
|
+
mts: "typescript",
|
|
2309
|
+
cts: "typescript",
|
|
2310
|
+
js: "javascript",
|
|
2311
|
+
jsx: "javascript",
|
|
2312
|
+
mjs: "javascript",
|
|
2313
|
+
cjs: "javascript",
|
|
2314
|
+
json: "json",
|
|
2315
|
+
py: "python",
|
|
2316
|
+
rb: "ruby",
|
|
2317
|
+
go: "go",
|
|
2318
|
+
rs: "rust",
|
|
2319
|
+
java: "java",
|
|
2320
|
+
c: "c",
|
|
2321
|
+
h: "c",
|
|
2322
|
+
cpp: "cpp",
|
|
2323
|
+
cc: "cpp",
|
|
2324
|
+
hpp: "cpp",
|
|
2325
|
+
cs: "csharp",
|
|
2326
|
+
php: "php",
|
|
2327
|
+
swift: "swift",
|
|
2328
|
+
kt: "kotlin",
|
|
2329
|
+
scala: "scala",
|
|
2330
|
+
sh: "bash",
|
|
2331
|
+
bash: "bash",
|
|
2332
|
+
zsh: "bash",
|
|
2333
|
+
yml: "yaml",
|
|
2334
|
+
yaml: "yaml",
|
|
2335
|
+
html: "xml",
|
|
2336
|
+
xml: "xml",
|
|
2337
|
+
css: "css",
|
|
2338
|
+
scss: "scss",
|
|
2339
|
+
sql: "sql",
|
|
2340
|
+
md: "markdown"
|
|
2341
|
+
};
|
|
2342
|
+
function langFromPath(path) {
|
|
2343
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
2344
|
+
return ext ? EXT_LANG[ext] : void 0;
|
|
2345
|
+
}
|
|
2346
|
+
function highlightLine(text, lang) {
|
|
2347
|
+
if (!lang) return text;
|
|
2348
|
+
try {
|
|
2349
|
+
return highlight(text, { language: lang, ignoreIllegals: true });
|
|
2350
|
+
} catch {
|
|
2351
|
+
return text;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
1960
2354
|
function FileEditBlock({
|
|
1961
2355
|
label,
|
|
1962
2356
|
path,
|
|
@@ -1967,6 +2361,7 @@ function FileEditBlock({
|
|
|
1967
2361
|
const expanded = useToolExpanded();
|
|
1968
2362
|
const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
|
|
1969
2363
|
const extra = previewLines.length - shown.length;
|
|
2364
|
+
const lang = langFromPath(path);
|
|
1970
2365
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
1971
2366
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1972
2367
|
/* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
|
|
@@ -1984,15 +2379,19 @@ function FileEditBlock({
|
|
|
1984
2379
|
] }) }),
|
|
1985
2380
|
shown.map((ln, i) => {
|
|
1986
2381
|
const width = (process.stdout.columns ?? 80) - 6 - 20;
|
|
1987
|
-
const
|
|
1988
|
-
const
|
|
1989
|
-
|
|
2382
|
+
const textWidth = Math.max(0, width - 2);
|
|
2383
|
+
const plain = ln.text.length > textWidth ? ln.text.slice(0, textWidth) : ln.text.padEnd(textWidth);
|
|
2384
|
+
const code = ln.sign === " " ? plain : highlightLine(plain, lang);
|
|
2385
|
+
return /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(
|
|
1990
2386
|
Text9,
|
|
1991
2387
|
{
|
|
1992
2388
|
wrap: "truncate",
|
|
1993
2389
|
backgroundColor: ln.sign === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
|
|
1994
2390
|
dimColor: ln.sign === " ",
|
|
1995
|
-
children:
|
|
2391
|
+
children: [
|
|
2392
|
+
`${ln.sign} `,
|
|
2393
|
+
code
|
|
2394
|
+
]
|
|
1996
2395
|
}
|
|
1997
2396
|
) }, i);
|
|
1998
2397
|
}),
|
|
@@ -2011,7 +2410,7 @@ var TOOL_LABEL = {
|
|
|
2011
2410
|
glob: "Glob",
|
|
2012
2411
|
grep: "Grep"
|
|
2013
2412
|
};
|
|
2014
|
-
function
|
|
2413
|
+
function truncate2(s, max) {
|
|
2015
2414
|
if (s.length <= max) return s;
|
|
2016
2415
|
return s.slice(0, max - 1) + "\u2026";
|
|
2017
2416
|
}
|
|
@@ -2027,15 +2426,15 @@ function toolHeader(use) {
|
|
|
2027
2426
|
break;
|
|
2028
2427
|
case "run_bash": {
|
|
2029
2428
|
const cmd2 = String(input.command ?? "").replace(/\s+/g, " ");
|
|
2030
|
-
arg =
|
|
2429
|
+
arg = truncate2(cmd2, 120);
|
|
2031
2430
|
break;
|
|
2032
2431
|
}
|
|
2033
2432
|
case "glob":
|
|
2034
2433
|
case "grep":
|
|
2035
|
-
arg =
|
|
2434
|
+
arg = truncate2(String(input.pattern ?? ""), 120);
|
|
2036
2435
|
break;
|
|
2037
2436
|
default: {
|
|
2038
|
-
arg =
|
|
2437
|
+
arg = truncate2(JSON.stringify(input), 80);
|
|
2039
2438
|
}
|
|
2040
2439
|
}
|
|
2041
2440
|
return { label, arg };
|
|
@@ -2077,7 +2476,7 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
2077
2476
|
}
|
|
2078
2477
|
const MAX_LINE_WIDTH = 200;
|
|
2079
2478
|
const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
|
|
2080
|
-
const shown = visible.map((l) =>
|
|
2479
|
+
const shown = visible.map((l) => truncate2(l, MAX_LINE_WIDTH));
|
|
2081
2480
|
const extra = lines.length - shown.length;
|
|
2082
2481
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2083
2482
|
/* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
|
|
@@ -2127,7 +2526,13 @@ function ToolUseLine({ use, result }) {
|
|
|
2127
2526
|
result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
|
|
2128
2527
|
] });
|
|
2129
2528
|
}
|
|
2130
|
-
function
|
|
2529
|
+
var UserMessage = memo2(function UserMessage2({ msg }) {
|
|
2530
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
2531
|
+
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
|
|
2532
|
+
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2533
|
+
] });
|
|
2534
|
+
});
|
|
2535
|
+
var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
|
|
2131
2536
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
2132
2537
|
msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
|
|
2133
2538
|
/* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
|
|
@@ -2142,14 +2547,16 @@ function AssistantMessage({ msg }) {
|
|
|
2142
2547
|
msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
|
|
2143
2548
|
] }) })
|
|
2144
2549
|
] });
|
|
2145
|
-
}
|
|
2550
|
+
});
|
|
2146
2551
|
function summarizeInput(input) {
|
|
2147
2552
|
if (!input || typeof input !== "object") return "";
|
|
2148
2553
|
const obj = input;
|
|
2149
2554
|
const priority = ["path", "file_path", "command", "pattern", "query"];
|
|
2150
2555
|
for (const k of priority) {
|
|
2151
2556
|
const v = obj[k];
|
|
2152
|
-
if (typeof v === "string" && v.length > 0)
|
|
2557
|
+
if (typeof v === "string" && v.length > 0) {
|
|
2558
|
+
return `${k}: ${v.length > 120 ? v.slice(0, 120) + "\u2026" : v}`;
|
|
2559
|
+
}
|
|
2153
2560
|
}
|
|
2154
2561
|
const first = Object.entries(obj).find(([, v]) => typeof v === "string");
|
|
2155
2562
|
if (first) {
|
|
@@ -2174,7 +2581,7 @@ function PermissionPrompt({ req, cursor }) {
|
|
|
2174
2581
|
/* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
|
|
2175
2582
|
"?"
|
|
2176
2583
|
] }) }),
|
|
2177
|
-
summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: summary }) }),
|
|
2584
|
+
summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { wrap: "truncate", dimColor: true, children: summary }) }),
|
|
2178
2585
|
/* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
|
|
2179
2586
|
i === cursor ? "\u276F " : " ",
|
|
2180
2587
|
i + 1,
|
|
@@ -2205,10 +2612,7 @@ function ChatView({
|
|
|
2205
2612
|
] }, i))
|
|
2206
2613
|
] }),
|
|
2207
2614
|
messages.map(
|
|
2208
|
-
(msg, i) => msg.role === "user" ? /* @__PURE__ */
|
|
2209
|
-
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
|
|
2210
|
-
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2211
|
-
] }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
|
|
2615
|
+
(msg, i) => msg.role === "user" ? /* @__PURE__ */ jsx9(UserMessage, { msg }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
|
|
2212
2616
|
),
|
|
2213
2617
|
thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
|
|
2214
2618
|
streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
@@ -2451,6 +2855,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2451
2855
|
}
|
|
2452
2856
|
|
|
2453
2857
|
// src/ui/hooks/useKeyboard.ts
|
|
2858
|
+
init_config();
|
|
2454
2859
|
import { useInput } from "ink";
|
|
2455
2860
|
var EFFORTS = ["low", "medium", "high"];
|
|
2456
2861
|
function useKeyboard(opts) {
|
|
@@ -2465,6 +2870,9 @@ function useKeyboard(opts) {
|
|
|
2465
2870
|
cfg,
|
|
2466
2871
|
setCfg,
|
|
2467
2872
|
setActiveCtx,
|
|
2873
|
+
providers,
|
|
2874
|
+
pickerQuery,
|
|
2875
|
+
setPickerQuery,
|
|
2468
2876
|
agent,
|
|
2469
2877
|
input,
|
|
2470
2878
|
setInput,
|
|
@@ -2476,7 +2884,8 @@ function useKeyboard(opts) {
|
|
|
2476
2884
|
setSessionId,
|
|
2477
2885
|
sessions,
|
|
2478
2886
|
setSessions,
|
|
2479
|
-
setNotice
|
|
2887
|
+
setNotice,
|
|
2888
|
+
switchProvider
|
|
2480
2889
|
} = opts;
|
|
2481
2890
|
const {
|
|
2482
2891
|
pendingPermissionRef,
|
|
@@ -2523,6 +2932,38 @@ function useKeyboard(opts) {
|
|
|
2523
2932
|
abortRef.current.abort();
|
|
2524
2933
|
return;
|
|
2525
2934
|
}
|
|
2935
|
+
if (state === "providers") {
|
|
2936
|
+
if (key.upArrow) {
|
|
2937
|
+
setCursor((i) => Math.max(0, i - 1));
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (key.downArrow) {
|
|
2941
|
+
setCursor((i) => Math.min(providers.length - 1, i + 1));
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
if (key.escape) {
|
|
2945
|
+
setPickerQuery("");
|
|
2946
|
+
setCursor(() => 0);
|
|
2947
|
+
setState(cfg.model ? "models" : "select-model");
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
if (key.return && providers[cursor]) {
|
|
2951
|
+
const chosen = providers[cursor].name;
|
|
2952
|
+
setNotice(`switched to ${chosen}`);
|
|
2953
|
+
switchProvider(chosen);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
if (key.backspace || key.delete) {
|
|
2957
|
+
setPickerQuery(pickerQuery.slice(0, -1));
|
|
2958
|
+
setCursor(() => 0);
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
if (char && !key.ctrl && !key.meta && char.length === 1 && char >= " ") {
|
|
2962
|
+
setPickerQuery(pickerQuery + char);
|
|
2963
|
+
setCursor(() => 0);
|
|
2964
|
+
}
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2526
2967
|
if (state === "select-model" || state === "models") {
|
|
2527
2968
|
if (key.upArrow) {
|
|
2528
2969
|
setCursor((i) => Math.max(0, i - 1));
|
|
@@ -2537,21 +2978,45 @@ function useKeyboard(opts) {
|
|
|
2537
2978
|
setModel(chosen);
|
|
2538
2979
|
setCfg((c) => ({ ...c, model: chosen }));
|
|
2539
2980
|
if (contexts[chosen]) setActiveCtx(contexts[chosen]);
|
|
2981
|
+
setPickerQuery("");
|
|
2982
|
+
setCursor(() => 0);
|
|
2540
2983
|
setState("ready");
|
|
2541
2984
|
return;
|
|
2542
2985
|
}
|
|
2543
|
-
if (
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2986
|
+
if (key.tab) {
|
|
2987
|
+
setPickerQuery("");
|
|
2988
|
+
setCursor(() => 0);
|
|
2989
|
+
setState("providers");
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (key.rightArrow) {
|
|
2993
|
+
const next = EFFORTS[Math.min(EFFORTS.indexOf(effort) + 1, EFFORTS.length - 1)];
|
|
2994
|
+
setEffort(next);
|
|
2995
|
+
setCfg((c) => ({ ...c, effort: next }));
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
if (key.leftArrow) {
|
|
2999
|
+
const next = EFFORTS[Math.max(EFFORTS.indexOf(effort) - 1, 0)];
|
|
3000
|
+
setEffort(next);
|
|
3001
|
+
setCfg((c) => ({ ...c, effort: next }));
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
if (key.escape) {
|
|
3005
|
+
if (state === "models") {
|
|
3006
|
+
setPickerQuery("");
|
|
3007
|
+
setCursor(() => 0);
|
|
2553
3008
|
setState("ready");
|
|
2554
3009
|
}
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
if (key.backspace || key.delete) {
|
|
3013
|
+
setPickerQuery(pickerQuery.slice(0, -1));
|
|
3014
|
+
setCursor(() => 0);
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
if (char && !key.ctrl && !key.meta && char.length === 1 && char >= " ") {
|
|
3018
|
+
setPickerQuery(pickerQuery + char);
|
|
3019
|
+
setCursor(() => 0);
|
|
2555
3020
|
}
|
|
2556
3021
|
return;
|
|
2557
3022
|
}
|
|
@@ -2654,8 +3119,13 @@ function useKeyboard(opts) {
|
|
|
2654
3119
|
if (key.return) {
|
|
2655
3120
|
const trimmed = input.trim();
|
|
2656
3121
|
if (trimmed === "/models") {
|
|
3122
|
+
setPickerQuery("");
|
|
2657
3123
|
setCursor(() => Math.max(0, models.findIndex((m) => m === cfg.model)));
|
|
2658
3124
|
setState("models");
|
|
3125
|
+
} else if (trimmed === "/provider" || trimmed === "/providers") {
|
|
3126
|
+
setPickerQuery("");
|
|
3127
|
+
setCursor(() => Math.max(0, providers.findIndex((p) => p.name === cfg.provider)));
|
|
3128
|
+
setState("providers");
|
|
2659
3129
|
} else if (trimmed === "/clear") {
|
|
2660
3130
|
clearSession();
|
|
2661
3131
|
} else if (trimmed === "/new") {
|
|
@@ -2668,6 +3138,15 @@ function useKeyboard(opts) {
|
|
|
2668
3138
|
setState("sessions");
|
|
2669
3139
|
} else if (trimmed === "/exit") {
|
|
2670
3140
|
exit();
|
|
3141
|
+
} else if (trimmed.startsWith("/provider ")) {
|
|
3142
|
+
const p = trimmed.slice("/provider ".length).trim();
|
|
3143
|
+
const names = providers.map((x) => x.name);
|
|
3144
|
+
if (names.includes(p)) {
|
|
3145
|
+
setNotice(`switched to ${p}`);
|
|
3146
|
+
switchProvider(p);
|
|
3147
|
+
} else {
|
|
3148
|
+
setNotice(`unknown provider "${p}" \u2014 configured: ${names.join(", ")}`);
|
|
3149
|
+
}
|
|
2671
3150
|
} else if (trimmed) {
|
|
2672
3151
|
setNotice(null);
|
|
2673
3152
|
if (!agentHistory.length && cfg.model) {
|
|
@@ -2750,8 +3229,9 @@ function App() {
|
|
|
2750
3229
|
const [activeCtx, setActiveCtx] = useState5(null);
|
|
2751
3230
|
const [state, setState] = useState5("loading");
|
|
2752
3231
|
const [cursor, setCursor] = useState5(0);
|
|
3232
|
+
const [pickerQuery, setPickerQuery] = useState5("");
|
|
2753
3233
|
const [updateAvailable, setUpdateAvailable] = useState5(null);
|
|
2754
|
-
const [
|
|
3234
|
+
const [providerDown, setProviderDown] = useState5(false);
|
|
2755
3235
|
const [sessionId, setSessionId] = useState5(() => newSessionId());
|
|
2756
3236
|
const [sessions, setSessions] = useState5([]);
|
|
2757
3237
|
const [notice, setNotice] = useState5(null);
|
|
@@ -2767,36 +3247,67 @@ function App() {
|
|
|
2767
3247
|
useEffect4(() => {
|
|
2768
3248
|
if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
|
|
2769
3249
|
}, [agent.agentHistory, sessionId]);
|
|
2770
|
-
|
|
2771
|
-
|
|
3250
|
+
const loadGen = useRef2(0);
|
|
3251
|
+
const loadModels = (afterProvider = false) => {
|
|
3252
|
+
const gen = ++loadGen.current;
|
|
3253
|
+
const stale = () => gen !== loadGen.current;
|
|
3254
|
+
setProviderDown(false);
|
|
3255
|
+
listModels3().then((m) => {
|
|
3256
|
+
if (stale()) return;
|
|
2772
3257
|
setModels(m);
|
|
2773
|
-
|
|
2774
|
-
|
|
3258
|
+
const hasModel = !!cfg.model && m.includes(cfg.model);
|
|
3259
|
+
if (afterProvider) {
|
|
3260
|
+
setState(hasModel ? "models" : "select-model");
|
|
3261
|
+
} else {
|
|
3262
|
+
setState(hasModel ? "ready" : "select-model");
|
|
3263
|
+
}
|
|
3264
|
+
Promise.all(m.map((name) => modelContext3(name).then((ctx) => [name, ctx]))).then((pairs) => {
|
|
3265
|
+
if (stale()) return;
|
|
2775
3266
|
const map = Object.fromEntries(pairs);
|
|
2776
3267
|
setContexts(map);
|
|
2777
|
-
const
|
|
2778
|
-
if (
|
|
3268
|
+
const active2 = (hasModel ? cfg.model : void 0) ?? m[0];
|
|
3269
|
+
if (active2 && map[active2]) setActiveCtx(map[active2]);
|
|
2779
3270
|
}).catch(() => {
|
|
2780
3271
|
});
|
|
2781
3272
|
}).catch((err) => {
|
|
3273
|
+
if (stale()) return;
|
|
2782
3274
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2783
|
-
agent.setError(
|
|
2784
|
-
|
|
3275
|
+
agent.setError(isAvailable3() ? msg : NOT_AVAILABLE());
|
|
3276
|
+
setProviderDown(true);
|
|
2785
3277
|
setModels([]);
|
|
2786
|
-
|
|
3278
|
+
setPickerQuery("");
|
|
3279
|
+
setCursor(() => 0);
|
|
3280
|
+
setState("ready");
|
|
2787
3281
|
});
|
|
2788
|
-
}
|
|
3282
|
+
};
|
|
3283
|
+
useEffect4(loadModels, []);
|
|
3284
|
+
function switchProvider(p) {
|
|
3285
|
+
setProvider(p);
|
|
3286
|
+
setCfg((c) => ({ ...c, provider: p }));
|
|
3287
|
+
setPickerQuery("");
|
|
3288
|
+
setCursor(() => 0);
|
|
3289
|
+
agent.setError(null);
|
|
3290
|
+
loadModels(true);
|
|
3291
|
+
}
|
|
3292
|
+
const { name: provName, entry: provEntry } = resolveProvider(cfg);
|
|
3293
|
+
const q = pickerQuery.toLowerCase();
|
|
3294
|
+
const filteredModels = q ? models.filter((m) => m.toLowerCase().includes(q)) : models;
|
|
3295
|
+
const allProviders = providerEntries(cfg);
|
|
3296
|
+
const filteredProviders = q ? allProviders.filter((p) => p.name.toLowerCase().includes(q)) : allProviders;
|
|
2789
3297
|
useKeyboard({
|
|
2790
3298
|
exit,
|
|
2791
3299
|
state,
|
|
2792
3300
|
setState,
|
|
2793
|
-
models,
|
|
3301
|
+
models: filteredModels,
|
|
2794
3302
|
cursor,
|
|
2795
3303
|
setCursor,
|
|
2796
3304
|
contexts,
|
|
2797
3305
|
cfg,
|
|
2798
3306
|
setCfg,
|
|
2799
3307
|
setActiveCtx,
|
|
3308
|
+
providers: filteredProviders,
|
|
3309
|
+
pickerQuery,
|
|
3310
|
+
setPickerQuery,
|
|
2800
3311
|
agent,
|
|
2801
3312
|
input,
|
|
2802
3313
|
setInput,
|
|
@@ -2808,7 +3319,8 @@ function App() {
|
|
|
2808
3319
|
setSessionId,
|
|
2809
3320
|
sessions,
|
|
2810
3321
|
setSessions,
|
|
2811
|
-
setNotice
|
|
3322
|
+
setNotice,
|
|
3323
|
+
switchProvider
|
|
2812
3324
|
});
|
|
2813
3325
|
const effort = cfg.effort ?? "medium";
|
|
2814
3326
|
const contextWarning = (() => {
|
|
@@ -2819,9 +3331,8 @@ function App() {
|
|
|
2819
3331
|
return Math.round(used / activeCtx * 100);
|
|
2820
3332
|
})();
|
|
2821
3333
|
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
|
|
2822
|
-
/* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error }),
|
|
2823
|
-
|
|
2824
|
-
state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "connecting to ollama\u2026" }) }),
|
|
3334
|
+
/* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
|
|
3335
|
+
state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
|
|
2825
3336
|
agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
|
|
2826
3337
|
ChatView,
|
|
2827
3338
|
{
|
|
@@ -2832,19 +3343,26 @@ function App() {
|
|
|
2832
3343
|
error: agent.error
|
|
2833
3344
|
}
|
|
2834
3345
|
),
|
|
2835
|
-
state === "select-model" && /* @__PURE__ */
|
|
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(
|
|
3346
|
+
(state === "select-model" || state === "models") && /* @__PURE__ */ jsx10(
|
|
2841
3347
|
ModelsView,
|
|
2842
3348
|
{
|
|
2843
|
-
models,
|
|
3349
|
+
models: filteredModels,
|
|
2844
3350
|
cursor,
|
|
2845
3351
|
model: cfg.model,
|
|
2846
|
-
|
|
2847
|
-
|
|
3352
|
+
host: provEntry.baseUrl,
|
|
3353
|
+
provider: provName,
|
|
3354
|
+
effort,
|
|
3355
|
+
query: pickerQuery,
|
|
3356
|
+
requireSelection: state === "select-model"
|
|
3357
|
+
}
|
|
3358
|
+
),
|
|
3359
|
+
state === "providers" && /* @__PURE__ */ jsx10(
|
|
3360
|
+
ProviderPicker,
|
|
3361
|
+
{
|
|
3362
|
+
entries: filteredProviders,
|
|
3363
|
+
cursor,
|
|
3364
|
+
activeName: provName,
|
|
3365
|
+
query: pickerQuery
|
|
2848
3366
|
}
|
|
2849
3367
|
),
|
|
2850
3368
|
state === "sessions" && /* @__PURE__ */ jsx10(SessionsView, { sessions, cursor }),
|
|
@@ -2872,24 +3390,33 @@ function App() {
|
|
|
2872
3390
|
if (!m) return null;
|
|
2873
3391
|
return /* @__PURE__ */ jsx10(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
|
|
2874
3392
|
})(),
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
!agent.busy && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "type / to see commands" }) })
|
|
2878
|
-
] })
|
|
3393
|
+
/* @__PURE__ */ jsx10(InputBar, { input, disabled: agent.busy, processingLabel: agent.processingLabel }),
|
|
3394
|
+
!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
3395
|
] })
|
|
2880
3396
|
] });
|
|
2881
3397
|
}
|
|
2882
3398
|
|
|
2883
3399
|
// src/cli.tsx
|
|
2884
3400
|
init_spill();
|
|
3401
|
+
init_config();
|
|
2885
3402
|
cleanupSpill();
|
|
2886
|
-
var
|
|
3403
|
+
var args = process.argv.slice(2);
|
|
3404
|
+
var cmd;
|
|
3405
|
+
for (let i = 0; i < args.length; i++) {
|
|
3406
|
+
if ((args[i] === "--provider" || args[i] === "-p") && i + 1 < args.length) {
|
|
3407
|
+
const p = args[++i];
|
|
3408
|
+
if (listProviders().includes(p)) setProvider(p);
|
|
3409
|
+
} else if (!cmd) {
|
|
3410
|
+
cmd = args[i];
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
2887
3413
|
if (cmd === "update" || cmd === "--update" || cmd === "-u") {
|
|
2888
3414
|
const { spawnSync } = await import("child_process");
|
|
2889
3415
|
console.log("Updating miii-agent\u2026");
|
|
2890
3416
|
const r = spawnSync("npm", ["i", "-g", "miii-agent@latest"], { stdio: "inherit", shell: process.platform === "win32" });
|
|
2891
3417
|
process.exit(r.status ?? 1);
|
|
2892
3418
|
} else if (cmd === "doctor" || cmd === "eval") {
|
|
3419
|
+
const rest = args.filter((a) => a !== cmd);
|
|
2893
3420
|
const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
2894
3421
|
process.exit(await runEval2(rest));
|
|
2895
3422
|
} else {
|