miii-agent 0.1.8 → 0.1.10
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 +45 -3
- package/dist/cli.js +1764 -1429
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -1,34 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { sep as sep2 } from "path";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
12
11
|
|
|
13
12
|
// src/ollama/client.ts
|
|
14
13
|
import { Ollama } from "ollama";
|
|
15
14
|
import { execFileSync } from "child_process";
|
|
16
|
-
var ollama = new Ollama({
|
|
17
|
-
host: process.env.OLLAMA_HOST ?? "http://localhost:11434"
|
|
18
|
-
});
|
|
19
|
-
var OLLAMA_NOT_INSTALLED = "Ollama is not installed. Install it with: npm i -g ollama\nOr download from https://ollama.com/download";
|
|
20
|
-
var OLLAMA_NOT_RUNNING = "Ollama is not running. Start it with: ollama serve";
|
|
21
15
|
function ollamaInstalled() {
|
|
22
16
|
try {
|
|
23
|
-
const
|
|
24
|
-
execFileSync(
|
|
17
|
+
const cmd2 = process.platform === "win32" ? "where" : "which";
|
|
18
|
+
execFileSync(cmd2, ["ollama"], { stdio: "ignore" });
|
|
25
19
|
return true;
|
|
26
20
|
} catch {
|
|
27
21
|
return false;
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
|
-
var HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
|
|
31
|
-
var CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
|
|
32
24
|
function stripHarmony(s) {
|
|
33
25
|
if (s == null) return s;
|
|
34
26
|
let out = s.replace(HARMONY_RE, "");
|
|
@@ -138,1532 +130,1864 @@ async function* chat(model, messages, tools, opts) {
|
|
|
138
130
|
if (opts?.signal) opts.signal.removeEventListener("abort", onAbort);
|
|
139
131
|
}
|
|
140
132
|
}
|
|
133
|
+
var ollama, OLLAMA_NOT_INSTALLED, OLLAMA_NOT_RUNNING, HARMONY_RE, CHANNEL_LABEL_RE;
|
|
134
|
+
var init_client = __esm({
|
|
135
|
+
"src/ollama/client.ts"() {
|
|
136
|
+
"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";
|
|
142
|
+
HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
|
|
143
|
+
CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
141
146
|
|
|
142
|
-
// src/
|
|
143
|
-
import {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
148
|
-
function loadConfig() {
|
|
149
|
-
if (!existsSync(CONFIG_PATH)) return {};
|
|
150
|
-
try {
|
|
151
|
-
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
152
|
-
} catch {
|
|
153
|
-
return {};
|
|
147
|
+
// src/tools/paths.ts
|
|
148
|
+
import { resolve, relative as relative2, isAbsolute, sep } from "path";
|
|
149
|
+
function confinePath(p) {
|
|
150
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
151
|
+
throw new Error("Path is required.");
|
|
154
152
|
}
|
|
153
|
+
const root = process.cwd();
|
|
154
|
+
const abs = resolve(root, p);
|
|
155
|
+
const rel = relative2(root, abs);
|
|
156
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
157
|
+
throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
|
|
158
|
+
}
|
|
159
|
+
return abs;
|
|
155
160
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
var init_paths = __esm({
|
|
162
|
+
"src/tools/paths.ts"() {
|
|
163
|
+
"use strict";
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// src/tools/edit_file.ts
|
|
168
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
169
|
+
var edit_file;
|
|
170
|
+
var init_edit_file = __esm({
|
|
171
|
+
"src/tools/edit_file.ts"() {
|
|
172
|
+
"use strict";
|
|
173
|
+
init_paths();
|
|
174
|
+
edit_file = {
|
|
175
|
+
name: "edit_file",
|
|
176
|
+
description: "Replace an exact string in a file. old_str must be unique.",
|
|
177
|
+
input_schema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
path: { type: "string", description: "File path" },
|
|
181
|
+
old_str: { type: "string", description: "Exact text to replace" },
|
|
182
|
+
new_str: { type: "string", description: "Replacement text" }
|
|
183
|
+
},
|
|
184
|
+
required: ["path", "old_str", "new_str"]
|
|
185
|
+
},
|
|
186
|
+
handler: ({ path, old_str, new_str }) => {
|
|
187
|
+
try {
|
|
188
|
+
const abs = confinePath(path);
|
|
189
|
+
const src = readFileSync3(abs, "utf-8");
|
|
190
|
+
const first = src.indexOf(old_str);
|
|
191
|
+
if (first === -1) {
|
|
192
|
+
return { content: `old_str not found in ${path}`, is_error: true };
|
|
193
|
+
}
|
|
194
|
+
if (src.indexOf(old_str, first + 1) !== -1) {
|
|
195
|
+
return { content: `old_str not unique in ${path}`, is_error: true };
|
|
196
|
+
}
|
|
197
|
+
writeFileSync3(abs, src.slice(0, first) + new_str + src.slice(first + old_str.length), "utf-8");
|
|
198
|
+
return { content: `Edited ${path}` };
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// src/tools/read_file.ts
|
|
208
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
209
|
+
var read_file;
|
|
210
|
+
var init_read_file = __esm({
|
|
211
|
+
"src/tools/read_file.ts"() {
|
|
212
|
+
"use strict";
|
|
213
|
+
init_paths();
|
|
214
|
+
read_file = {
|
|
215
|
+
name: "read_file",
|
|
216
|
+
description: "Read entire file contents as UTF-8 text.",
|
|
217
|
+
input_schema: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: {
|
|
220
|
+
path: { type: "string", description: "File path" }
|
|
221
|
+
},
|
|
222
|
+
required: ["path"]
|
|
223
|
+
},
|
|
224
|
+
handler: ({ path }) => {
|
|
225
|
+
try {
|
|
226
|
+
const MAX = 2e5;
|
|
227
|
+
const raw = readFileSync4(confinePath(path), "utf-8");
|
|
228
|
+
const truncated = raw.length > MAX;
|
|
229
|
+
const body = truncated ? raw.slice(0, MAX) + `
|
|
230
|
+
[truncated: ${raw.length - MAX} more chars]` : raw;
|
|
231
|
+
return { content: body };
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// src/tools/write_file.ts
|
|
241
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
242
|
+
import { dirname } from "path";
|
|
243
|
+
var write_file;
|
|
244
|
+
var init_write_file = __esm({
|
|
245
|
+
"src/tools/write_file.ts"() {
|
|
246
|
+
"use strict";
|
|
247
|
+
init_paths();
|
|
248
|
+
write_file = {
|
|
249
|
+
name: "write_file",
|
|
250
|
+
description: "Create or overwrite a file with the given content. Parent dirs auto-created.",
|
|
251
|
+
input_schema: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
path: { type: "string", description: "File path" },
|
|
255
|
+
content: { type: "string", description: "Full file content" }
|
|
256
|
+
},
|
|
257
|
+
required: ["path", "content"]
|
|
258
|
+
},
|
|
259
|
+
handler: ({ path, content }) => {
|
|
260
|
+
try {
|
|
261
|
+
const abs = confinePath(path);
|
|
262
|
+
mkdirSync3(dirname(abs), { recursive: true });
|
|
263
|
+
writeFileSync4(abs, content, "utf-8");
|
|
264
|
+
return { content: `Wrote ${path} (${content.length} bytes)` };
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/tools/run_bash.ts
|
|
274
|
+
import { execa } from "execa";
|
|
275
|
+
var run_bash;
|
|
276
|
+
var init_run_bash = __esm({
|
|
277
|
+
"src/tools/run_bash.ts"() {
|
|
278
|
+
"use strict";
|
|
279
|
+
run_bash = {
|
|
280
|
+
name: "run_bash",
|
|
281
|
+
description: "Execute a shell command (bash on Unix, cmd on Windows). Returns stdout+stderr. Non-interactive only.",
|
|
282
|
+
input_schema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
command: { type: "string", description: "Shell command to run" },
|
|
286
|
+
timeout_ms: { type: "number", description: "Timeout in ms (default 30000)" }
|
|
287
|
+
},
|
|
288
|
+
required: ["command"]
|
|
289
|
+
},
|
|
290
|
+
handler: async ({ command, timeout_ms }) => {
|
|
291
|
+
try {
|
|
292
|
+
const isWin = process.platform === "win32";
|
|
293
|
+
const shell = isWin ? "cmd" : "bash";
|
|
294
|
+
const shellArgs = isWin ? ["/c", command] : ["-c", command];
|
|
295
|
+
const { stdout, stderr, exitCode } = await execa(shell, shellArgs, {
|
|
296
|
+
timeout: timeout_ms ?? 3e4,
|
|
297
|
+
reject: false,
|
|
298
|
+
all: false
|
|
299
|
+
});
|
|
300
|
+
const out = [stdout, stderr].filter(Boolean).join("\n");
|
|
301
|
+
const is_error = exitCode !== 0;
|
|
302
|
+
const body = out || (is_error ? `(no output)` : "");
|
|
303
|
+
const content = `${body}
|
|
304
|
+
[exit ${exitCode}]`;
|
|
305
|
+
return {
|
|
306
|
+
content: content.slice(0, 32e3),
|
|
307
|
+
is_error
|
|
308
|
+
};
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// src/tools/grep.ts
|
|
318
|
+
import { execa as execa2 } from "execa";
|
|
319
|
+
var grep;
|
|
320
|
+
var init_grep = __esm({
|
|
321
|
+
"src/tools/grep.ts"() {
|
|
322
|
+
"use strict";
|
|
323
|
+
grep = {
|
|
324
|
+
name: "grep",
|
|
325
|
+
description: "Search file contents for a regex pattern. Uses ripgrep if available, falls back to grep -R.",
|
|
326
|
+
input_schema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
pattern: { type: "string", description: "Regex pattern" },
|
|
330
|
+
path: { type: "string", description: "Root path to search (default cwd)" },
|
|
331
|
+
glob: { type: "string", description: 'File glob filter, e.g. "*.ts"' },
|
|
332
|
+
case_insensitive: { type: "string", description: 'Set "true" for case-insensitive' },
|
|
333
|
+
max_results: { type: "number", description: "Max matching lines (default 200)" }
|
|
334
|
+
},
|
|
335
|
+
required: ["pattern"]
|
|
336
|
+
},
|
|
337
|
+
handler: async ({ pattern, path, glob: glob2, case_insensitive, max_results }) => {
|
|
338
|
+
const root = path ?? process.cwd();
|
|
339
|
+
const limit = max_results ?? 200;
|
|
340
|
+
const ci = case_insensitive === true || String(case_insensitive) === "true";
|
|
341
|
+
const tryRg = async () => {
|
|
342
|
+
const args = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
|
|
343
|
+
if (ci) args.push("-i");
|
|
344
|
+
if (glob2) args.push("--glob", glob2);
|
|
345
|
+
args.push("--", pattern, root);
|
|
346
|
+
return execa2("rg", args, { reject: false, timeout: 2e4 });
|
|
347
|
+
};
|
|
348
|
+
const tryGrep = async () => {
|
|
349
|
+
const args = ["-R", "-n", "--color=never"];
|
|
350
|
+
if (ci) args.push("-i");
|
|
351
|
+
if (glob2) args.push("--include", glob2);
|
|
352
|
+
args.push("--", pattern, root);
|
|
353
|
+
return execa2("grep", args, { reject: false, timeout: 2e4 });
|
|
354
|
+
};
|
|
355
|
+
try {
|
|
356
|
+
let res;
|
|
357
|
+
try {
|
|
358
|
+
res = await tryRg();
|
|
359
|
+
if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
|
|
360
|
+
res = await tryGrep();
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
res = await tryGrep();
|
|
364
|
+
}
|
|
365
|
+
const lines = (res.stdout ?? "").split("\n").slice(0, limit);
|
|
366
|
+
const out = lines.join("\n");
|
|
367
|
+
const code = res.exitCode ?? 0;
|
|
368
|
+
if (!out && code === 1) return { content: "No matches." };
|
|
369
|
+
return { content: out || res.stderr || "No matches.", is_error: code > 1 };
|
|
370
|
+
} catch (err) {
|
|
371
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// src/tools/glob.ts
|
|
379
|
+
import { execa as execa3 } from "execa";
|
|
380
|
+
function globToFindName(glob2) {
|
|
381
|
+
return glob2;
|
|
165
382
|
}
|
|
383
|
+
var glob;
|
|
384
|
+
var init_glob = __esm({
|
|
385
|
+
"src/tools/glob.ts"() {
|
|
386
|
+
"use strict";
|
|
387
|
+
glob = {
|
|
388
|
+
name: "glob",
|
|
389
|
+
description: 'List files matching a glob pattern (e.g. "**/*.ts"). Uses ripgrep --files if available.',
|
|
390
|
+
input_schema: {
|
|
391
|
+
type: "object",
|
|
392
|
+
properties: {
|
|
393
|
+
pattern: { type: "string", description: 'Glob pattern, e.g. "**/*.ts"' },
|
|
394
|
+
path: { type: "string", description: "Root path (default cwd)" },
|
|
395
|
+
max_results: { type: "number", description: "Max paths returned (default 500)" }
|
|
396
|
+
},
|
|
397
|
+
required: ["pattern"]
|
|
398
|
+
},
|
|
399
|
+
handler: async ({ pattern, path, max_results }) => {
|
|
400
|
+
const root = path ?? process.cwd();
|
|
401
|
+
const limit = max_results ?? 500;
|
|
402
|
+
const tryRg = () => execa3("rg", ["--files", "--hidden", "--glob", pattern, root], {
|
|
403
|
+
reject: false,
|
|
404
|
+
timeout: 2e4
|
|
405
|
+
});
|
|
406
|
+
const tryFind = () => {
|
|
407
|
+
const name = globToFindName(pattern.replace(/^\*\*\//, ""));
|
|
408
|
+
return execa3("find", [root, "-type", "f", "-name", name], {
|
|
409
|
+
reject: false,
|
|
410
|
+
timeout: 2e4
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
try {
|
|
414
|
+
let res;
|
|
415
|
+
try {
|
|
416
|
+
res = await tryRg();
|
|
417
|
+
if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
|
|
418
|
+
res = await tryFind();
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
res = await tryFind();
|
|
422
|
+
}
|
|
423
|
+
const lines = (res.stdout ?? "").split("\n").filter(Boolean).slice(0, limit);
|
|
424
|
+
if (lines.length === 0) return { content: "No files matched." };
|
|
425
|
+
return { content: lines.join("\n") };
|
|
426
|
+
} catch (err) {
|
|
427
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
});
|
|
166
433
|
|
|
167
|
-
// src/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "blue", children: "MIII CLI" }),
|
|
183
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
|
|
184
|
-
/* @__PURE__ */ jsx(Text, { children: model ?? "/models" }),
|
|
185
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
|
|
186
|
-
/* @__PURE__ */ jsx(Text, { children: ctxLabel }),
|
|
187
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
|
|
188
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
189
|
-
effort,
|
|
190
|
-
" effort"
|
|
191
|
-
] })
|
|
192
|
-
] }),
|
|
193
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
|
|
194
|
-
]
|
|
434
|
+
// src/tools/registry.ts
|
|
435
|
+
function getTool(name) {
|
|
436
|
+
return TOOLS.find((t) => t.name === name);
|
|
437
|
+
}
|
|
438
|
+
function toOllamaTools(tools = TOOLS) {
|
|
439
|
+
return tools.map((t) => ({
|
|
440
|
+
type: "function",
|
|
441
|
+
function: {
|
|
442
|
+
name: t.name,
|
|
443
|
+
description: t.description,
|
|
444
|
+
parameters: {
|
|
445
|
+
type: "object",
|
|
446
|
+
properties: t.input_schema.properties,
|
|
447
|
+
required: t.input_schema.required
|
|
448
|
+
}
|
|
195
449
|
}
|
|
196
|
-
);
|
|
450
|
+
}));
|
|
197
451
|
}
|
|
452
|
+
var TOOLS;
|
|
453
|
+
var init_registry = __esm({
|
|
454
|
+
"src/tools/registry.ts"() {
|
|
455
|
+
"use strict";
|
|
456
|
+
init_edit_file();
|
|
457
|
+
init_read_file();
|
|
458
|
+
init_write_file();
|
|
459
|
+
init_run_bash();
|
|
460
|
+
init_grep();
|
|
461
|
+
init_glob();
|
|
462
|
+
TOOLS = [
|
|
463
|
+
edit_file,
|
|
464
|
+
read_file,
|
|
465
|
+
write_file,
|
|
466
|
+
run_bash,
|
|
467
|
+
grep,
|
|
468
|
+
glob
|
|
469
|
+
];
|
|
470
|
+
}
|
|
471
|
+
});
|
|
198
472
|
|
|
199
|
-
// src/
|
|
200
|
-
import {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
473
|
+
// src/tools/validate.ts
|
|
474
|
+
import { z } from "zod";
|
|
475
|
+
function propSchema(spec) {
|
|
476
|
+
if (spec.enum && spec.enum.length) return z.enum(spec.enum);
|
|
477
|
+
switch (spec.type) {
|
|
478
|
+
case "string":
|
|
479
|
+
return z.string();
|
|
480
|
+
case "number":
|
|
481
|
+
return z.number();
|
|
482
|
+
case "integer":
|
|
483
|
+
return z.number().int();
|
|
484
|
+
case "boolean":
|
|
485
|
+
return z.boolean();
|
|
486
|
+
case "array":
|
|
487
|
+
return z.array(z.unknown());
|
|
488
|
+
case "object":
|
|
489
|
+
return z.record(z.unknown());
|
|
490
|
+
default:
|
|
491
|
+
return z.unknown();
|
|
208
492
|
}
|
|
209
|
-
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: [
|
|
210
|
-
i === cursor ? "\u276F " : " ",
|
|
211
|
-
m,
|
|
212
|
-
showActive && m === activeModel ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (active)" }) : null
|
|
213
|
-
] }, m)) });
|
|
214
493
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
return /* @__PURE__ */ jsx3(
|
|
229
|
-
Box3,
|
|
230
|
-
{
|
|
231
|
-
borderStyle: "single",
|
|
232
|
-
borderTop: true,
|
|
233
|
-
borderBottom: true,
|
|
234
|
-
borderLeft: false,
|
|
235
|
-
borderRight: false,
|
|
236
|
-
borderColor: disabled ? "yellow" : "white dim",
|
|
237
|
-
paddingX: 1,
|
|
238
|
-
children: disabled ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
239
|
-
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: SPIN[frame] + " " }),
|
|
240
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, italic: true, children: processingLabel ?? "processing\u2026" }),
|
|
241
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (esc to cancel)" })
|
|
242
|
-
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
243
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
|
|
244
|
-
/* @__PURE__ */ jsx3(Text3, { children: input }),
|
|
245
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u258C" })
|
|
246
|
-
] })
|
|
247
|
-
}
|
|
248
|
-
);
|
|
494
|
+
function toZod(schema) {
|
|
495
|
+
const required = new Set(schema.required ?? []);
|
|
496
|
+
const shape = {};
|
|
497
|
+
for (const [key, spec] of Object.entries(schema.properties)) {
|
|
498
|
+
shape[key] = required.has(key) ? propSchema(spec) : z.unknown().optional();
|
|
499
|
+
}
|
|
500
|
+
return z.object(shape).passthrough();
|
|
501
|
+
}
|
|
502
|
+
function validateInput(schema, input) {
|
|
503
|
+
const result = toZod(schema).safeParse(input ?? {});
|
|
504
|
+
if (result.success) return null;
|
|
505
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
506
|
+
return `Invalid arguments: ${issues}`;
|
|
249
507
|
}
|
|
508
|
+
var init_validate = __esm({
|
|
509
|
+
"src/tools/validate.ts"() {
|
|
510
|
+
"use strict";
|
|
511
|
+
}
|
|
512
|
+
});
|
|
250
513
|
|
|
251
|
-
// src/
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
|
|
256
|
-
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
257
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "config" }),
|
|
258
|
-
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
259
|
-
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
260
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "model " }),
|
|
261
|
-
/* @__PURE__ */ jsx4(Text4, { children: model ?? "\u2014" })
|
|
262
|
-
] }),
|
|
263
|
-
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
264
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "host " }),
|
|
265
|
-
/* @__PURE__ */ jsx4(Text4, { children: ollamaHost ?? "http://localhost:11434" })
|
|
266
|
-
] }),
|
|
267
|
-
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
268
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "effort " }),
|
|
269
|
-
/* @__PURE__ */ jsx4(Text4, { children: effort }),
|
|
270
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (\u2190 \u2192)" })
|
|
271
|
-
] })
|
|
272
|
-
] })
|
|
273
|
-
] }),
|
|
274
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "switch model" }),
|
|
275
|
-
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(ModelList, { models, cursor, activeModel: model, showActive: true }) }),
|
|
276
|
-
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate enter switch \u2190\u2192 effort esc close" }) })
|
|
277
|
-
] });
|
|
278
|
-
}
|
|
514
|
+
// src/prompt/system.ts
|
|
515
|
+
function buildSystemPrompt(tools, cwd) {
|
|
516
|
+
const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
517
|
+
return `You are miii, a senior software engineer running in a terminal.
|
|
279
518
|
|
|
280
|
-
|
|
281
|
-
import { Box as Box5, Text as Text5 } from "ink";
|
|
282
|
-
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
283
|
-
function relativeTime(iso) {
|
|
284
|
-
const diff = Date.now() - new Date(iso).getTime();
|
|
285
|
-
const min = Math.floor(diff / 6e4);
|
|
286
|
-
if (min < 1) return "just now";
|
|
287
|
-
if (min < 60) return `${min}m ago`;
|
|
288
|
-
const hr = Math.floor(min / 60);
|
|
289
|
-
if (hr < 24) return `${hr}h ago`;
|
|
290
|
-
const d = Math.floor(hr / 24);
|
|
291
|
-
return `${d}d ago`;
|
|
292
|
-
}
|
|
293
|
-
function SessionsView({ sessions, cursor }) {
|
|
294
|
-
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, children: [
|
|
295
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
|
|
296
|
-
/* @__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) => {
|
|
297
|
-
const active = i === cursor;
|
|
298
|
-
const label = s.title;
|
|
299
|
-
return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
|
|
300
|
-
/* @__PURE__ */ jsxs5(Text5, { color: active ? "blue" : void 0, dimColor: !active, children: [
|
|
301
|
-
active ? "\u276F " : " ",
|
|
302
|
-
label
|
|
303
|
-
] }),
|
|
304
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}` })
|
|
305
|
-
] }, s.id);
|
|
306
|
-
}) }),
|
|
307
|
-
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate enter resume d delete esc cancel" }) })
|
|
308
|
-
] });
|
|
309
|
-
}
|
|
519
|
+
Working directory: ${cwd}
|
|
310
520
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
521
|
+
# Goal Understanding (read this first, every turn)
|
|
522
|
+
Before acting on any request, extract and hold three things:
|
|
523
|
+
GOAL: what the user ultimately wants (outcome, not steps)
|
|
524
|
+
CRITERION: how you will know the goal is met
|
|
525
|
+
GAPS: anything unclear that would force you to guess
|
|
526
|
+
|
|
527
|
+
If GAPS is non-empty, ask the minimum questions needed to fill them \u2014 one message, numbered list \u2014 before touching any file or running any command. Do not guess. Do not act on assumptions.
|
|
528
|
+
|
|
529
|
+
Re-read GOAL before every tool call. If a tool call does not move toward GOAL, skip it.
|
|
530
|
+
|
|
531
|
+
# Attention: re-attend to goal at each step
|
|
532
|
+
After each tool result, answer silently: "Does this result move me toward GOAL?"
|
|
533
|
+
YES \u2192 continue
|
|
534
|
+
NO \u2192 stop, re-derive plan from GOAL, explain the correction in one line
|
|
535
|
+
|
|
536
|
+
This prevents drift. Each step attends to the original goal, not just the previous step.
|
|
537
|
+
|
|
538
|
+
# Output format
|
|
539
|
+
- Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
|
|
540
|
+
- Quote code, paths, and identifiers inline as plain text. Do not wrap them.
|
|
541
|
+
- Keep prose terse.
|
|
542
|
+
|
|
543
|
+
# Engineering mindset
|
|
544
|
+
- Treat every request as one of: bug, feature, or fix. Name which one before you start.
|
|
545
|
+
- Apply first principles: decompose unclear tasks into smallest concrete sub-problems, solve each explicitly, compose the result.
|
|
546
|
+
- Never guess. If a fact (file path, function signature, current behavior) is unknown, read or search for it first.
|
|
547
|
+
|
|
548
|
+
# Clarifying questions \u2014 when to ask
|
|
549
|
+
Ask BEFORE acting when:
|
|
550
|
+
- The goal has more than one valid interpretation
|
|
551
|
+
- Success criterion is ambiguous (e.g. "make it better" \u2014 better how?)
|
|
552
|
+
- Required context is missing (which file? which behavior? which user?)
|
|
553
|
+
- Two reasonable approaches have different tradeoffs the user should choose
|
|
554
|
+
|
|
555
|
+
Do NOT ask when:
|
|
556
|
+
- The answer is findable by reading the codebase
|
|
557
|
+
- There is only one sensible interpretation
|
|
558
|
+
- The user has already answered this implicitly
|
|
559
|
+
|
|
560
|
+
Ask in a numbered list. One round of questions per turn. Then wait.
|
|
561
|
+
|
|
562
|
+
# Tool calls
|
|
563
|
+
- When you need a tool, emit the tool call directly. No preamble, no narration, no "I will use X".
|
|
564
|
+
- Never describe a tool call instead of emitting it. If you cannot emit the call, answer in plain text.
|
|
565
|
+
- After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
|
|
566
|
+
|
|
567
|
+
# Tools
|
|
568
|
+
You have access to the following tools. Call them via the function-calling interface.
|
|
569
|
+
${toolLines}
|
|
570
|
+
|
|
571
|
+
# Loop semantics
|
|
572
|
+
- When you need to act on the filesystem or run a command, emit a tool call.
|
|
573
|
+
- After each tool result, decide: more tool calls, or a final plain-text answer.
|
|
574
|
+
- Stop emitting tool calls when GOAL is met. Reply with a concise plain-text final message confirming CRITERION is satisfied.
|
|
575
|
+
|
|
576
|
+
# Rules
|
|
577
|
+
- Always read a file before updating it. Never edit, overwrite, or create-over a file you have not read first this turn.
|
|
578
|
+
- Prefer editing existing files over creating new ones.
|
|
579
|
+
- For edit_file, ensure old_str is unique within the target file.
|
|
580
|
+
- Never invent file paths. Read, glob, or grep before editing.
|
|
581
|
+
- No filler, no pleasantries, no apologies.
|
|
582
|
+
|
|
583
|
+
# Testing and verification
|
|
584
|
+
- Always test the code after a change. Run the project's tests (e.g. npm test, pytest, go test) or the relevant script via run_bash before declaring a task done.
|
|
585
|
+
- If no test exists for the change, run the affected entry point via run_bash to verify it behaves correctly.
|
|
586
|
+
- Treat a green test run or a successful command as the completion signal. If it fails, fix and re-run.
|
|
587
|
+
|
|
588
|
+
# Permissions
|
|
589
|
+
- File tools are confined to the working directory; paths outside it are denied.
|
|
590
|
+
- Each tool call may prompt the user for approval. If they choose "don't ask again", the exact command or path is persisted to ~/.miii/permissions.json and the same call is auto-allowed thereafter.
|
|
591
|
+
`;
|
|
352
592
|
}
|
|
593
|
+
var init_system = __esm({
|
|
594
|
+
"src/prompt/system.ts"() {
|
|
595
|
+
"use strict";
|
|
596
|
+
}
|
|
597
|
+
});
|
|
353
598
|
|
|
354
|
-
// src/
|
|
355
|
-
import {
|
|
356
|
-
import { join as
|
|
357
|
-
import { homedir as
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
599
|
+
// src/permissions/policy.ts
|
|
600
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync3, renameSync } from "fs";
|
|
601
|
+
import { join as join4 } from "path";
|
|
602
|
+
import { homedir as homedir3 } from "os";
|
|
603
|
+
function loadRules() {
|
|
604
|
+
if (!existsSync3(RULES_PATH)) return [];
|
|
605
|
+
try {
|
|
606
|
+
const data = JSON.parse(readFileSync5(RULES_PATH, "utf-8"));
|
|
607
|
+
return Array.isArray(data.rules) ? data.rules : [];
|
|
608
|
+
} catch {
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
361
611
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
612
|
+
function saveRules(rules) {
|
|
613
|
+
mkdirSync4(RULES_DIR, { recursive: true });
|
|
614
|
+
const tmp = RULES_PATH + ".tmp";
|
|
615
|
+
writeFileSync5(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
|
|
616
|
+
renameSync(tmp, RULES_PATH);
|
|
365
617
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
618
|
+
function addRule(tool, pattern) {
|
|
619
|
+
const rules = loadRules();
|
|
620
|
+
if (rules.some((r) => r.tool === tool && r.pattern === pattern)) return;
|
|
621
|
+
rules.push({ tool, pattern });
|
|
622
|
+
saveRules(rules);
|
|
368
623
|
}
|
|
369
|
-
function
|
|
370
|
-
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (b.type === "tool_result") return "[result]";
|
|
375
|
-
return "";
|
|
376
|
-
}).join(" ");
|
|
624
|
+
function subjectFor(toolName, input) {
|
|
625
|
+
const obj = input ?? {};
|
|
626
|
+
if (toolName === "run_bash") return typeof obj.command === "string" ? obj.command : "";
|
|
627
|
+
if (typeof obj.path === "string") return obj.path;
|
|
628
|
+
return "";
|
|
377
629
|
}
|
|
378
|
-
function
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
return
|
|
630
|
+
function globToRegExp(glob2) {
|
|
631
|
+
const escaped = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
632
|
+
const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
633
|
+
return new RegExp(`^${pattern}$`);
|
|
382
634
|
}
|
|
383
|
-
function
|
|
635
|
+
function matches(rule, toolName, subject) {
|
|
636
|
+
if (rule.tool !== toolName) return false;
|
|
384
637
|
try {
|
|
385
|
-
|
|
386
|
-
const firstLine = raw.slice(0, raw.indexOf("\n") === -1 ? raw.length : raw.indexOf("\n"));
|
|
387
|
-
const parsed = JSON.parse(firstLine);
|
|
388
|
-
if (parsed.type !== "meta") return null;
|
|
389
|
-
const { type: _t, ...meta } = parsed;
|
|
390
|
-
return meta;
|
|
638
|
+
return globToRegExp(rule.pattern).test(subject);
|
|
391
639
|
} catch {
|
|
392
|
-
return
|
|
640
|
+
return false;
|
|
393
641
|
}
|
|
394
642
|
}
|
|
395
|
-
function
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
function listSessions() {
|
|
414
|
-
if (!existsSync2(SESSION_DIR)) return [];
|
|
415
|
-
const metas = [];
|
|
416
|
-
for (const file of readdirSync(SESSION_DIR)) {
|
|
417
|
-
if (!file.endsWith(".jsonl")) continue;
|
|
418
|
-
const meta = readMeta(file.replace(/\.jsonl$/, ""));
|
|
419
|
-
if (meta) metas.push(meta);
|
|
420
|
-
}
|
|
421
|
-
return metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
422
|
-
}
|
|
423
|
-
function deleteSession(id) {
|
|
424
|
-
try {
|
|
425
|
-
rmSync(sessionPath(id), { force: true });
|
|
426
|
-
} catch {
|
|
643
|
+
async function check(toolName, input, ctx) {
|
|
644
|
+
if (ALWAYS_ALLOW.has(toolName)) return "allow";
|
|
645
|
+
const subject = subjectFor(toolName, input);
|
|
646
|
+
const rules = loadRules();
|
|
647
|
+
if (rules.some((r) => matches(r, toolName, subject))) return "allow";
|
|
648
|
+
const answer = await ctx.ask(toolName, input);
|
|
649
|
+
if (answer === "no") return "deny";
|
|
650
|
+
if (answer === "always") addRule(toolName, subject);
|
|
651
|
+
return "allow";
|
|
652
|
+
}
|
|
653
|
+
var RULES_DIR, RULES_PATH, ALWAYS_ALLOW;
|
|
654
|
+
var init_policy = __esm({
|
|
655
|
+
"src/permissions/policy.ts"() {
|
|
656
|
+
"use strict";
|
|
657
|
+
RULES_DIR = join4(homedir3(), ".miii");
|
|
658
|
+
RULES_PATH = join4(RULES_DIR, "permissions.json");
|
|
659
|
+
ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
|
|
427
660
|
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// src/agent/adapter.ts
|
|
664
|
+
function mintToolUseId() {
|
|
665
|
+
const rand = Math.random().toString(36).slice(2, 14);
|
|
666
|
+
return `toolu_${rand}`;
|
|
428
667
|
}
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
668
|
+
function toOllamaMessages(history, system) {
|
|
669
|
+
const out = [{ role: "system", content: system }];
|
|
670
|
+
for (const msg of history) {
|
|
671
|
+
if (typeof msg.content === "string") {
|
|
672
|
+
out.push({ role: msg.role === "system" ? "system" : msg.role, content: msg.content });
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (msg.role === "assistant") {
|
|
676
|
+
const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
677
|
+
const tool_uses = msg.content.filter((b) => b.type === "tool_use");
|
|
678
|
+
const ollamaMsg = { role: "assistant", content: text };
|
|
679
|
+
if (tool_uses.length > 0) {
|
|
680
|
+
ollamaMsg.tool_calls = tool_uses.map((u) => ({
|
|
681
|
+
function: { name: u.name, arguments: u.input }
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
out.push(ollamaMsg);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (msg.role === "user") {
|
|
688
|
+
const tool_results = msg.content.filter((b) => b.type === "tool_result");
|
|
689
|
+
const texts = msg.content.filter((b) => b.type === "text");
|
|
690
|
+
for (const tr of tool_results) {
|
|
691
|
+
out.push({ role: "tool", content: tr.content });
|
|
692
|
+
}
|
|
693
|
+
if (texts.length > 0) {
|
|
694
|
+
out.push({ role: "user", content: texts.map((t) => t.text).join("") });
|
|
695
|
+
}
|
|
437
696
|
}
|
|
438
|
-
return messages;
|
|
439
|
-
} catch {
|
|
440
|
-
return [];
|
|
441
697
|
}
|
|
698
|
+
return out;
|
|
442
699
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
700
|
+
function parseTextToolCalls(text, knownToolNames) {
|
|
701
|
+
if (!text) return { calls: [], cleanedText: text };
|
|
702
|
+
const calls = [];
|
|
703
|
+
let cleaned = text;
|
|
704
|
+
const tagRe = /<\|?tool_call\|?>\s*([\s\S]*?)\s*<\|?\/?tool_call\|?>/g;
|
|
705
|
+
cleaned = cleaned.replace(tagRe, (_m, body) => {
|
|
706
|
+
const c = tryParse(body, knownToolNames);
|
|
707
|
+
if (c) calls.push(c);
|
|
708
|
+
return "";
|
|
709
|
+
});
|
|
710
|
+
const fenceRe = /```(?:json|tool_call)?\s*([\s\S]*?)```/g;
|
|
711
|
+
cleaned = cleaned.replace(fenceRe, (_m, body) => {
|
|
712
|
+
const c = tryParse(body, knownToolNames);
|
|
713
|
+
if (c) {
|
|
714
|
+
calls.push(c);
|
|
715
|
+
return "";
|
|
716
|
+
}
|
|
717
|
+
return _m;
|
|
718
|
+
});
|
|
719
|
+
if (calls.length === 0) {
|
|
720
|
+
const candidate = extractFirstJsonObject(cleaned);
|
|
721
|
+
if (candidate) {
|
|
722
|
+
const c = tryParse(candidate.json, knownToolNames);
|
|
723
|
+
if (c) {
|
|
724
|
+
calls.push(c);
|
|
725
|
+
cleaned = (cleaned.slice(0, candidate.start) + cleaned.slice(candidate.end)).trim();
|
|
461
726
|
}
|
|
462
|
-
if (text.trim()) out.push({ role: "user", content: text });
|
|
463
|
-
} else {
|
|
464
|
-
const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
465
|
-
const uses = blocks.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
|
466
|
-
out.push({
|
|
467
|
-
role: "assistant",
|
|
468
|
-
content: text,
|
|
469
|
-
tool_uses: uses.length ? uses : void 0
|
|
470
|
-
});
|
|
471
727
|
}
|
|
472
728
|
}
|
|
473
|
-
return
|
|
729
|
+
return { calls, cleanedText: cleaned.trim() };
|
|
474
730
|
}
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
Request:
|
|
480
|
-
${text.slice(0, 2e3)}`;
|
|
731
|
+
function tryParse(raw, knownToolNames) {
|
|
732
|
+
const s = raw.trim();
|
|
733
|
+
if (!s.startsWith("{")) return null;
|
|
481
734
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
{ temperature: 0.2, num_predict: 32 }
|
|
488
|
-
)) {
|
|
489
|
-
if (chunk.content) out += chunk.content;
|
|
490
|
-
}
|
|
491
|
-
return out.trim().split("\n").filter(Boolean)[0]?.trim() || fallback;
|
|
735
|
+
const obj = JSON.parse(s);
|
|
736
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
737
|
+
const args = obj.arguments ?? obj.parameters ?? obj.input ?? {};
|
|
738
|
+
if (!name || !knownToolNames.includes(name)) return null;
|
|
739
|
+
return { function: { name, arguments: args } };
|
|
492
740
|
} catch {
|
|
493
|
-
return
|
|
741
|
+
return null;
|
|
494
742
|
}
|
|
495
743
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const out = [];
|
|
509
|
-
const stack = [cwd];
|
|
510
|
-
while (stack.length && out.length < MAX_SCAN) {
|
|
511
|
-
const dir = stack.pop();
|
|
512
|
-
let entries;
|
|
513
|
-
try {
|
|
514
|
-
entries = readdirSync2(dir, { withFileTypes: true });
|
|
515
|
-
} catch {
|
|
744
|
+
function extractFirstJsonObject(s) {
|
|
745
|
+
const start = s.indexOf("{");
|
|
746
|
+
if (start === -1) return null;
|
|
747
|
+
let depth = 0;
|
|
748
|
+
let inStr = false;
|
|
749
|
+
let esc = false;
|
|
750
|
+
for (let i = start; i < s.length; i++) {
|
|
751
|
+
const ch = s[i];
|
|
752
|
+
if (inStr) {
|
|
753
|
+
if (esc) esc = false;
|
|
754
|
+
else if (ch === "\\") esc = true;
|
|
755
|
+
else if (ch === '"') inStr = false;
|
|
516
756
|
continue;
|
|
517
757
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
758
|
+
if (ch === '"') {
|
|
759
|
+
inStr = true;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (ch === "{") depth++;
|
|
763
|
+
else if (ch === "}") {
|
|
764
|
+
depth--;
|
|
765
|
+
if (depth === 0) return { json: s.slice(start, i + 1), start, end: i + 1 };
|
|
524
766
|
}
|
|
525
767
|
}
|
|
526
|
-
|
|
527
|
-
return out;
|
|
528
|
-
}
|
|
529
|
-
function parseMention(input) {
|
|
530
|
-
const m = input.match(/(?:^|\s)@([^\s]*)$/);
|
|
531
|
-
if (!m) return null;
|
|
532
|
-
return { query: m[1], start: input.length - m[1].length - 1 };
|
|
768
|
+
return null;
|
|
533
769
|
}
|
|
534
|
-
function
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const baseIdx = base.indexOf(q);
|
|
545
|
-
const score = baseIdx === 0 ? 0 : baseIdx > -1 ? 1 : 2 + idx;
|
|
546
|
-
scored.push([score, f]);
|
|
547
|
-
if (scored.length > 500) break;
|
|
770
|
+
function blocksFromOllama(text, tool_calls, knownToolNames = []) {
|
|
771
|
+
const blocks = [];
|
|
772
|
+
let finalText = text;
|
|
773
|
+
let finalCalls = tool_calls ?? [];
|
|
774
|
+
if (finalCalls.length === 0 && knownToolNames.length > 0) {
|
|
775
|
+
const parsed = parseTextToolCalls(text, knownToolNames);
|
|
776
|
+
if (parsed.calls.length > 0) {
|
|
777
|
+
finalCalls = parsed.calls;
|
|
778
|
+
finalText = parsed.cleanedText;
|
|
779
|
+
}
|
|
548
780
|
}
|
|
549
|
-
|
|
550
|
-
|
|
781
|
+
if (finalText) blocks.push({ type: "text", text: finalText });
|
|
782
|
+
for (const tc of finalCalls) {
|
|
783
|
+
blocks.push({
|
|
784
|
+
type: "tool_use",
|
|
785
|
+
id: mintToolUseId(),
|
|
786
|
+
name: tc.function.name,
|
|
787
|
+
input: tc.function.arguments ?? {}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
return blocks;
|
|
551
791
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
792
|
+
var init_adapter = __esm({
|
|
793
|
+
"src/agent/adapter.ts"() {
|
|
794
|
+
"use strict";
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// src/agent/loop.ts
|
|
799
|
+
async function* runAgent(opts) {
|
|
800
|
+
const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
|
|
801
|
+
const startTime = Date.now();
|
|
802
|
+
const system = buildSystemPrompt(TOOLS, cwd);
|
|
803
|
+
const ollamaTools = toOllamaTools(TOOLS);
|
|
804
|
+
const history = [
|
|
805
|
+
...opts.history,
|
|
806
|
+
{ role: "user", content: opts.userText }
|
|
807
|
+
];
|
|
808
|
+
let promptTokens = 0;
|
|
809
|
+
let evalTokens = 0;
|
|
810
|
+
let lastAssistantSig = "";
|
|
811
|
+
let repeatCount = 0;
|
|
812
|
+
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
813
|
+
let text = "";
|
|
814
|
+
let tool_calls;
|
|
815
|
+
let lastTail = "";
|
|
816
|
+
let tailRepeats = 0;
|
|
817
|
+
let streamLooped = false;
|
|
818
|
+
const ac = new AbortController();
|
|
819
|
+
const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
|
|
820
|
+
if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
|
|
821
|
+
try {
|
|
822
|
+
for await (const chunk of chat(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: NUM_PREDICT })) {
|
|
823
|
+
if (signal?.aborted) break;
|
|
824
|
+
if (chunk.content) {
|
|
825
|
+
text += chunk.content;
|
|
826
|
+
yield { type: "text-delta", text: chunk.content };
|
|
827
|
+
if (text.length >= REPEAT_TAIL) {
|
|
828
|
+
const tail = text.slice(-REPEAT_TAIL);
|
|
829
|
+
if (tail === lastTail) {
|
|
830
|
+
tailRepeats++;
|
|
831
|
+
if (tailRepeats >= REPEAT_KILL) {
|
|
832
|
+
streamLooped = true;
|
|
833
|
+
ac.abort();
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
tailRepeats = 0;
|
|
838
|
+
lastTail = tail;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (chunk.thinking) {
|
|
843
|
+
yield { type: "thinking-delta", text: chunk.thinking };
|
|
844
|
+
}
|
|
845
|
+
if (chunk.tool_calls && chunk.tool_calls.length > 0) {
|
|
846
|
+
tool_calls = chunk.tool_calls;
|
|
847
|
+
}
|
|
848
|
+
if (chunk.done) {
|
|
849
|
+
promptTokens += chunk.prompt_eval_count ?? 0;
|
|
850
|
+
evalTokens += chunk.eval_count ?? 0;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (streamLooped) {
|
|
855
|
+
yield { type: "error", message: "Model stuck in repetition. Aborted stream. Try a different model or shorten context." };
|
|
856
|
+
return history;
|
|
857
|
+
}
|
|
858
|
+
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
859
|
+
return history;
|
|
860
|
+
}
|
|
861
|
+
if (streamLooped) {
|
|
862
|
+
yield { type: "error", message: "Model stuck in repetition. Aborted stream. Try a different model or shorten context." };
|
|
863
|
+
return history;
|
|
864
|
+
}
|
|
865
|
+
if (signal?.aborted) {
|
|
866
|
+
yield {
|
|
867
|
+
type: "aborted",
|
|
868
|
+
prompt_tokens: promptTokens,
|
|
869
|
+
eval_tokens: evalTokens,
|
|
870
|
+
duration_ms: Date.now() - startTime
|
|
871
|
+
};
|
|
872
|
+
return history;
|
|
873
|
+
}
|
|
874
|
+
const blocks = blocksFromOllama(text, tool_calls, TOOLS.map((t) => t.name));
|
|
875
|
+
const tool_uses = blocks.filter((b) => b.type === "tool_use");
|
|
876
|
+
history.push({ role: "assistant", content: blocks });
|
|
877
|
+
if (tool_uses.length === 0) {
|
|
878
|
+
yield { type: "turn-end", stop_reason: "end_turn" };
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
const sig = JSON.stringify(
|
|
882
|
+
blocks.map(
|
|
883
|
+
(b) => b.type === "tool_use" ? { t: "u", n: b.name, i: b.input } : b.type === "text" ? { t: "t", x: b.text.trim() } : b
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
if (sig === lastAssistantSig) {
|
|
887
|
+
repeatCount++;
|
|
888
|
+
if (repeatCount >= 2) {
|
|
889
|
+
yield { type: "error", message: "Agent loop detected: assistant produced identical output 3 turns in a row" };
|
|
890
|
+
return history;
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
repeatCount = 0;
|
|
894
|
+
lastAssistantSig = sig;
|
|
895
|
+
}
|
|
896
|
+
for (const u of tool_uses) yield { type: "tool-use", block: u };
|
|
897
|
+
const results = [];
|
|
898
|
+
for (const use of tool_uses) {
|
|
899
|
+
const tool = getTool(use.name);
|
|
900
|
+
if (!tool) {
|
|
901
|
+
const r2 = {
|
|
902
|
+
type: "tool_result",
|
|
903
|
+
tool_use_id: use.id,
|
|
904
|
+
content: `Unknown tool: ${use.name}`,
|
|
905
|
+
is_error: true
|
|
906
|
+
};
|
|
907
|
+
results.push(r2);
|
|
908
|
+
yield { type: "tool-result", block: r2 };
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const invalid = validateInput(tool.input_schema, use.input);
|
|
912
|
+
if (invalid) {
|
|
913
|
+
const r2 = {
|
|
914
|
+
type: "tool_result",
|
|
915
|
+
tool_use_id: use.id,
|
|
916
|
+
content: `${invalid} for ${use.name}.`,
|
|
917
|
+
is_error: true
|
|
918
|
+
};
|
|
919
|
+
results.push(r2);
|
|
920
|
+
yield { type: "tool-result", block: r2 };
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const decision = await check(use.name, use.input, permissions);
|
|
924
|
+
if (decision === "deny") {
|
|
925
|
+
const r2 = {
|
|
926
|
+
type: "tool_result",
|
|
927
|
+
tool_use_id: use.id,
|
|
928
|
+
content: `Permission denied for ${use.name}.`,
|
|
929
|
+
is_error: true
|
|
930
|
+
};
|
|
931
|
+
results.push(r2);
|
|
932
|
+
yield { type: "permission-denied", toolName: use.name, tool_use_id: use.id };
|
|
933
|
+
yield { type: "tool-result", block: r2 };
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
await hooks?.firePre(use);
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
let r;
|
|
941
|
+
try {
|
|
942
|
+
const out = await tool.handler(use.input);
|
|
943
|
+
r = {
|
|
944
|
+
type: "tool_result",
|
|
945
|
+
tool_use_id: use.id,
|
|
946
|
+
content: out.content,
|
|
947
|
+
is_error: out.is_error
|
|
948
|
+
};
|
|
949
|
+
} catch (err) {
|
|
950
|
+
r = {
|
|
951
|
+
type: "tool_result",
|
|
952
|
+
tool_use_id: use.id,
|
|
953
|
+
content: err instanceof Error ? err.message : String(err),
|
|
954
|
+
is_error: true
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
await hooks?.firePost(use, r);
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
results.push(r);
|
|
962
|
+
yield { type: "tool-result", block: r };
|
|
963
|
+
}
|
|
964
|
+
history.push({ role: "user", content: results });
|
|
965
|
+
yield { type: "turn-end", stop_reason: "tool_use" };
|
|
966
|
+
}
|
|
967
|
+
yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
|
|
968
|
+
return history;
|
|
575
969
|
}
|
|
970
|
+
var MAX_TURNS, NUM_PREDICT, REPEAT_TAIL, REPEAT_KILL;
|
|
971
|
+
var init_loop = __esm({
|
|
972
|
+
"src/agent/loop.ts"() {
|
|
973
|
+
"use strict";
|
|
974
|
+
init_client();
|
|
975
|
+
init_registry();
|
|
976
|
+
init_validate();
|
|
977
|
+
init_system();
|
|
978
|
+
init_policy();
|
|
979
|
+
init_adapter();
|
|
980
|
+
MAX_TURNS = 25;
|
|
981
|
+
NUM_PREDICT = 4096;
|
|
982
|
+
REPEAT_TAIL = 120;
|
|
983
|
+
REPEAT_KILL = 4;
|
|
984
|
+
}
|
|
985
|
+
});
|
|
576
986
|
|
|
577
|
-
//
|
|
578
|
-
import {
|
|
987
|
+
// eval/runner.ts
|
|
988
|
+
import { mkdtempSync, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, rmSync as rmSync2 } from "fs";
|
|
989
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
990
|
+
import { tmpdir } from "os";
|
|
991
|
+
async function runScenario(model, s) {
|
|
992
|
+
const dir = mkdtempSync(join5(tmpdir(), "miii-eval-"));
|
|
993
|
+
const prevCwd = process.cwd();
|
|
994
|
+
for (const [rel, content] of Object.entries(s.files ?? {})) {
|
|
995
|
+
const abs = join5(dir, rel);
|
|
996
|
+
mkdirSync5(dirname2(abs), { recursive: true });
|
|
997
|
+
writeFileSync6(abs, content, "utf-8");
|
|
998
|
+
}
|
|
999
|
+
const r = {
|
|
1000
|
+
name: s.name,
|
|
1001
|
+
pass: false,
|
|
1002
|
+
toolCalls: 0,
|
|
1003
|
+
promptTokens: 0,
|
|
1004
|
+
evalTokens: 0,
|
|
1005
|
+
durationMs: 0
|
|
1006
|
+
};
|
|
1007
|
+
const start = Date.now();
|
|
1008
|
+
let finalText = "";
|
|
1009
|
+
try {
|
|
1010
|
+
process.chdir(dir);
|
|
1011
|
+
const gen = runAgent({
|
|
1012
|
+
model,
|
|
1013
|
+
cwd: dir,
|
|
1014
|
+
history: [],
|
|
1015
|
+
userText: s.prompt,
|
|
1016
|
+
permissions: autoYes
|
|
1017
|
+
});
|
|
1018
|
+
for await (const ev of gen) {
|
|
1019
|
+
if (ev.type === "tool-use") r.toolCalls++;
|
|
1020
|
+
else if (ev.type === "text-delta") finalText += ev.text;
|
|
1021
|
+
else if (ev.type === "turn-end" && ev.stop_reason === "tool_use") finalText = "";
|
|
1022
|
+
else if (ev.type === "done") {
|
|
1023
|
+
r.promptTokens = ev.prompt_tokens;
|
|
1024
|
+
r.evalTokens = ev.eval_tokens;
|
|
1025
|
+
} else if (ev.type === "error") r.error = ev.message;
|
|
1026
|
+
}
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
r.error = err instanceof Error ? err.message : String(err);
|
|
1029
|
+
} finally {
|
|
1030
|
+
process.chdir(prevCwd);
|
|
1031
|
+
}
|
|
1032
|
+
r.durationMs = Date.now() - start;
|
|
1033
|
+
if (r.error) {
|
|
1034
|
+
r.reason = `loop error: ${r.error}`;
|
|
1035
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
1036
|
+
return r;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const verdict2 = await s.check(dir, finalText.trim());
|
|
1040
|
+
if (verdict2 === true) r.pass = true;
|
|
1041
|
+
else r.reason = typeof verdict2 === "string" ? verdict2 : "check returned false";
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
r.reason = `check threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
1044
|
+
}
|
|
1045
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
1046
|
+
return r;
|
|
1047
|
+
}
|
|
1048
|
+
var autoYes;
|
|
1049
|
+
var init_runner = __esm({
|
|
1050
|
+
"eval/runner.ts"() {
|
|
1051
|
+
"use strict";
|
|
1052
|
+
init_loop();
|
|
1053
|
+
autoYes = { ask: async () => "yes" };
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
579
1056
|
|
|
580
|
-
//
|
|
581
|
-
import {
|
|
582
|
-
import {
|
|
583
|
-
|
|
584
|
-
var
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1057
|
+
// eval/scenarios.ts
|
|
1058
|
+
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
1059
|
+
import { join as join6 } from "path";
|
|
1060
|
+
var read, scenarios;
|
|
1061
|
+
var init_scenarios = __esm({
|
|
1062
|
+
"eval/scenarios.ts"() {
|
|
1063
|
+
"use strict";
|
|
1064
|
+
read = (dir, f) => existsSync4(join6(dir, f)) ? readFileSync6(join6(dir, f), "utf-8") : null;
|
|
1065
|
+
scenarios = [
|
|
1066
|
+
{
|
|
1067
|
+
name: "edit-exact-string",
|
|
1068
|
+
prompt: "In config.js, change the port from 3000 to 8080. Change nothing else.",
|
|
1069
|
+
files: { "config.js": 'export const port = 3000\nexport const host = "localhost"\n' },
|
|
1070
|
+
check: (dir) => {
|
|
1071
|
+
const out = read(dir, "config.js");
|
|
1072
|
+
if (out == null) return "config.js missing";
|
|
1073
|
+
if (!out.includes("8080")) return "port not changed to 8080";
|
|
1074
|
+
if (out.includes("3000")) return "old port 3000 still present";
|
|
1075
|
+
if (!out.includes('host = "localhost"')) return "unrelated line damaged";
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: "read-then-answer",
|
|
1081
|
+
prompt: "What is the value of the MAX_RETRIES constant in limits.js? Reply with just the number.",
|
|
1082
|
+
files: { "limits.js": "export const MAX_RETRIES = 7\n" },
|
|
1083
|
+
check: (dir, finalText) => {
|
|
1084
|
+
if (read(dir, "limits.js")?.includes("MAX_RETRIES = 7") !== true)
|
|
1085
|
+
return "agent mutated a read-only task";
|
|
1086
|
+
if (!/\b7\b/.test(finalText)) return `answer missing "7": ${JSON.stringify(finalText)}`;
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
name: "create-new-file",
|
|
1092
|
+
prompt: "Create a file named greeting.txt containing exactly the text: hello world",
|
|
1093
|
+
check: (dir) => {
|
|
1094
|
+
const out = read(dir, "greeting.txt");
|
|
1095
|
+
if (out == null) return "greeting.txt not created";
|
|
1096
|
+
if (out.trim() !== "hello world") return `wrong content: ${JSON.stringify(out)}`;
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
name: "grep-locate",
|
|
1102
|
+
prompt: "Which file defines a function called computeTax? Reply with just the filename.",
|
|
1103
|
+
files: {
|
|
1104
|
+
"a.js": "export function formatDate() {}\n",
|
|
1105
|
+
"b.js": "export function computeTax(x) { return x * 0.1 }\n",
|
|
1106
|
+
"c.js": "export function parseArgs() {}\n"
|
|
1107
|
+
},
|
|
1108
|
+
check: (dir, finalText) => {
|
|
1109
|
+
if (read(dir, "b.js")?.includes("computeTax") !== true) return "b.js damaged";
|
|
1110
|
+
if (!/\bb\.js\b/.test(finalText)) return `answer missing "b.js": ${JSON.stringify(finalText)}`;
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
];
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
625
1117
|
|
|
626
|
-
//
|
|
627
|
-
var
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1118
|
+
// eval/run.ts
|
|
1119
|
+
var run_exports = {};
|
|
1120
|
+
__export(run_exports, {
|
|
1121
|
+
runEval: () => runEval
|
|
1122
|
+
});
|
|
1123
|
+
function pad(s, n) {
|
|
1124
|
+
return s.length >= n ? s.slice(0, n) : s + " ".repeat(n - s.length);
|
|
1125
|
+
}
|
|
1126
|
+
async function resolveModels(modelsArg) {
|
|
1127
|
+
if (modelsArg !== "all") return modelsArg.split(",").map((m) => m.trim()).filter(Boolean);
|
|
1128
|
+
return (await listModels()).filter((m) => !m.includes("cloud"));
|
|
1129
|
+
}
|
|
1130
|
+
function verdict(passed, total) {
|
|
1131
|
+
const ratio = total === 0 ? 0 : passed / total;
|
|
1132
|
+
if (ratio === 1) return "ready";
|
|
1133
|
+
if (ratio >= 0.5) return "marginal \u2014 some tasks fail";
|
|
1134
|
+
return "not recommended \u2014 weak tool-calling";
|
|
1135
|
+
}
|
|
1136
|
+
async function runModel(model, picked) {
|
|
1137
|
+
console.log(`
|
|
1138
|
+
=== ${model} ===`);
|
|
1139
|
+
const results = [];
|
|
1140
|
+
for (const s of picked) {
|
|
1141
|
+
const r = await runScenario(model, s);
|
|
1142
|
+
results.push(r);
|
|
1143
|
+
const mark = r.pass ? "PASS" : "FAIL";
|
|
1144
|
+
const detail = r.pass ? "" : ` ${r.reason ?? r.error ?? ""}`;
|
|
1145
|
+
console.log(
|
|
1146
|
+
`${mark} ${pad(r.name, 22)} ${pad(`${r.toolCalls} calls`, 9)} ${pad(`${r.evalTokens} tok`, 11)} ${pad(`${r.durationMs}ms`, 8)}${detail}`
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
const passed = results.filter((r) => r.pass).length;
|
|
1150
|
+
console.log(` \u2192 ${model}: ${passed}/${picked.length} \u2014 ${verdict(passed, picked.length)}`);
|
|
1151
|
+
return results;
|
|
1152
|
+
}
|
|
1153
|
+
function printMatrix(models, picked, grid) {
|
|
1154
|
+
const w = Math.max(...picked.map((s) => s.name.length), 3) + 1;
|
|
1155
|
+
const modelW = Math.max(...models.map((m) => m.length), 5) + 1;
|
|
1156
|
+
console.log("\nMatrix\n");
|
|
1157
|
+
let header = pad("", modelW);
|
|
1158
|
+
for (const s of picked) header += pad(s.name.slice(0, w - 1), w);
|
|
1159
|
+
console.log(header + " score");
|
|
1160
|
+
for (const m of models) {
|
|
1161
|
+
let row = pad(m, modelW);
|
|
1162
|
+
const rs = grid.get(m) ?? [];
|
|
1163
|
+
let passed = 0;
|
|
1164
|
+
for (const s of picked) {
|
|
1165
|
+
const r = rs.find((x) => x.name === s.name);
|
|
1166
|
+
const cell = !r ? "?" : r.pass ? "+" : ".";
|
|
1167
|
+
if (r?.pass) passed++;
|
|
1168
|
+
row += pad(cell, w);
|
|
1169
|
+
}
|
|
1170
|
+
row += ` ${passed}/${picked.length}`;
|
|
1171
|
+
console.log(row);
|
|
1172
|
+
}
|
|
1173
|
+
console.log("\n + pass . fail ? not run");
|
|
1174
|
+
}
|
|
1175
|
+
async function runEval(args) {
|
|
1176
|
+
const strip = (s) => (s ?? "").replace(/^-+/, "");
|
|
1177
|
+
const modelsArg = strip(args[0]) || process.env.MIII_EVAL_MODEL || "all";
|
|
1178
|
+
const filter = strip(args[1]);
|
|
1179
|
+
const picked = filter ? scenarios.filter((s) => s.name.includes(filter)) : scenarios;
|
|
1180
|
+
if (picked.length === 0) {
|
|
1181
|
+
console.error(`No scenarios match "${filter}"`);
|
|
1182
|
+
return 1;
|
|
1183
|
+
}
|
|
1184
|
+
const models = await resolveModels(modelsArg);
|
|
1185
|
+
if (models.length === 0) {
|
|
1186
|
+
console.error("No models to run.");
|
|
1187
|
+
return 1;
|
|
1188
|
+
}
|
|
1189
|
+
console.log(`models: ${models.length} scenarios: ${picked.length}`);
|
|
1190
|
+
const grid = /* @__PURE__ */ new Map();
|
|
1191
|
+
for (const model of models) grid.set(model, await runModel(model, picked));
|
|
1192
|
+
if (models.length > 1) printMatrix(models, picked, grid);
|
|
1193
|
+
const allPass = [...grid.values()].every((rs) => rs.every((r) => r.pass));
|
|
1194
|
+
return allPass ? 0 : 1;
|
|
1195
|
+
}
|
|
1196
|
+
var init_run = __esm({
|
|
1197
|
+
"eval/run.ts"() {
|
|
1198
|
+
"use strict";
|
|
1199
|
+
init_runner();
|
|
1200
|
+
init_scenarios();
|
|
1201
|
+
init_client();
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
635
1204
|
|
|
636
|
-
// src/
|
|
637
|
-
import {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1205
|
+
// src/cli.tsx
|
|
1206
|
+
import { render } from "ink";
|
|
1207
|
+
import { createElement } from "react";
|
|
1208
|
+
|
|
1209
|
+
// src/ui/App.tsx
|
|
1210
|
+
init_client();
|
|
1211
|
+
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
1212
|
+
import { Box as Box10, Text as Text10, useApp } from "ink";
|
|
1213
|
+
import { homedir as homedir4 } from "os";
|
|
1214
|
+
import { sep as sep2 } from "path";
|
|
1215
|
+
|
|
1216
|
+
// src/config.ts
|
|
1217
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1218
|
+
import { join } from "path";
|
|
1219
|
+
import { homedir } from "os";
|
|
1220
|
+
var CONFIG_DIR = join(homedir(), ".miii");
|
|
1221
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
1222
|
+
function loadConfig() {
|
|
1223
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
1224
|
+
try {
|
|
1225
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
1226
|
+
} catch {
|
|
1227
|
+
return {};
|
|
1228
|
+
}
|
|
648
1229
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
|
|
1230
|
+
function saveConfig(config) {
|
|
1231
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1232
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
652
1233
|
}
|
|
653
|
-
function
|
|
654
|
-
|
|
655
|
-
path,
|
|
656
|
-
added,
|
|
657
|
-
removed,
|
|
658
|
-
previewLines
|
|
659
|
-
}) {
|
|
660
|
-
const MAX = 16;
|
|
661
|
-
const shown = previewLines.slice(0, MAX);
|
|
662
|
-
const extra = previewLines.length - shown.length;
|
|
663
|
-
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
664
|
-
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
665
|
-
/* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
|
|
666
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
|
|
667
|
-
label,
|
|
668
|
-
" "
|
|
669
|
-
] }),
|
|
670
|
-
/* @__PURE__ */ jsx9(Text9, { children: "(" }),
|
|
671
|
-
/* @__PURE__ */ jsx9(Text9, { bold: true, children: path }),
|
|
672
|
-
/* @__PURE__ */ jsx9(Text9, { children: ")" })
|
|
673
|
-
] }),
|
|
674
|
-
/* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
675
|
-
"\u23BF ",
|
|
676
|
-
removed > 0 ? `Added ${added} lines, removed ${removed} lines` : `Added ${added} lines`
|
|
677
|
-
] }) }),
|
|
678
|
-
shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { color: ln.sign === "+" ? "green" : ln.sign === "-" ? "red" : void 0, dimColor: ln.sign === " ", children: [
|
|
679
|
-
ln.sign,
|
|
680
|
-
" ",
|
|
681
|
-
ln.text
|
|
682
|
-
] }) }, i)),
|
|
683
|
-
extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
684
|
-
"\u2026 ",
|
|
685
|
-
extra,
|
|
686
|
-
" more lines"
|
|
687
|
-
] }) })
|
|
688
|
-
] });
|
|
1234
|
+
function setModel(model) {
|
|
1235
|
+
saveConfig({ ...loadConfig(), model });
|
|
689
1236
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
edit_file: "Update",
|
|
693
|
-
read_file: "Read",
|
|
694
|
-
run_bash: "Bash",
|
|
695
|
-
glob: "Glob",
|
|
696
|
-
grep: "Grep"
|
|
697
|
-
};
|
|
698
|
-
function truncate(s, max) {
|
|
699
|
-
if (s.length <= max) return s;
|
|
700
|
-
return s.slice(0, max - 1) + "\u2026";
|
|
1237
|
+
function setEffort(effort) {
|
|
1238
|
+
saveConfig({ ...loadConfig(), effort });
|
|
701
1239
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1240
|
+
|
|
1241
|
+
// src/ui/WelcomeBlock.tsx
|
|
1242
|
+
import { Box, Text } from "ink";
|
|
1243
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1244
|
+
function WelcomeBlock({ model, activeCtx, effort, cwd }) {
|
|
1245
|
+
const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
|
|
1246
|
+
return /* @__PURE__ */ jsxs(
|
|
1247
|
+
Box,
|
|
1248
|
+
{
|
|
1249
|
+
flexDirection: "column",
|
|
1250
|
+
borderStyle: "round",
|
|
1251
|
+
borderColor: "gray",
|
|
1252
|
+
paddingX: 2,
|
|
1253
|
+
marginBottom: 1,
|
|
1254
|
+
children: [
|
|
1255
|
+
/* @__PURE__ */ jsxs(Box, { gap: 2, children: [
|
|
1256
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "blue", children: "MIII CLI" }),
|
|
1257
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
|
|
1258
|
+
/* @__PURE__ */ jsx(Text, { children: model ?? "/models" }),
|
|
1259
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
|
|
1260
|
+
/* @__PURE__ */ jsx(Text, { children: ctxLabel }),
|
|
1261
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
|
|
1262
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1263
|
+
effort,
|
|
1264
|
+
" effort"
|
|
1265
|
+
] })
|
|
1266
|
+
] }),
|
|
1267
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
|
|
1268
|
+
]
|
|
723
1269
|
}
|
|
724
|
-
|
|
725
|
-
return { label, arg };
|
|
1270
|
+
);
|
|
726
1271
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const n = lines.filter(Boolean).length;
|
|
738
|
-
return `${n} match${n === 1 ? "" : "es"}`;
|
|
739
|
-
}
|
|
740
|
-
if (toolName === "glob") {
|
|
741
|
-
if (content === "No files matched.") return "No files";
|
|
742
|
-
const n = lines.filter(Boolean).length;
|
|
743
|
-
return `${n} file${n === 1 ? "" : "s"}`;
|
|
744
|
-
}
|
|
1272
|
+
|
|
1273
|
+
// src/ui/ModelList.tsx
|
|
1274
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
1275
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1276
|
+
function ModelList({ models, cursor, activeModel, showActive }) {
|
|
1277
|
+
if (models.length === 0) {
|
|
1278
|
+
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1279
|
+
"no models found. run: ollama pull ",
|
|
1280
|
+
"<model>"
|
|
1281
|
+
] });
|
|
745
1282
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1283
|
+
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: [
|
|
1284
|
+
i === cursor ? "\u276F " : " ",
|
|
1285
|
+
m,
|
|
1286
|
+
showActive && m === activeModel ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (active)" }) : null
|
|
1287
|
+
] }, m)) });
|
|
750
1288
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
"
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
"
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1289
|
+
|
|
1290
|
+
// src/ui/InputBar.tsx
|
|
1291
|
+
import { useEffect, useState } from "react";
|
|
1292
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
1293
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1294
|
+
var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1295
|
+
function InputBar({ input, disabled, processingLabel }) {
|
|
1296
|
+
const [frame, setFrame] = useState(0);
|
|
1297
|
+
useEffect(() => {
|
|
1298
|
+
if (!disabled) return;
|
|
1299
|
+
const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 150);
|
|
1300
|
+
return () => clearInterval(t);
|
|
1301
|
+
}, [disabled]);
|
|
1302
|
+
return /* @__PURE__ */ jsx3(
|
|
1303
|
+
Box3,
|
|
1304
|
+
{
|
|
1305
|
+
borderStyle: "single",
|
|
1306
|
+
borderTop: true,
|
|
1307
|
+
borderBottom: true,
|
|
1308
|
+
borderLeft: false,
|
|
1309
|
+
borderRight: false,
|
|
1310
|
+
borderColor: disabled ? "yellow" : "white dim",
|
|
1311
|
+
paddingX: 1,
|
|
1312
|
+
children: disabled ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1313
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: SPIN[frame] + " " }),
|
|
1314
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, italic: true, children: processingLabel ?? "processing\u2026" }),
|
|
1315
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (esc to cancel)" })
|
|
1316
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1317
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
|
|
1318
|
+
/* @__PURE__ */ jsx3(Text3, { children: input }),
|
|
1319
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u258C" })
|
|
1320
|
+
] })
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
777
1323
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
/* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
|
|
802
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
|
|
803
|
-
label,
|
|
804
|
-
" "
|
|
805
|
-
] }),
|
|
806
|
-
/* @__PURE__ */ jsx9(Text9, { children: "(" }),
|
|
807
|
-
/* @__PURE__ */ jsx9(Text9, { bold: true, children: arg }),
|
|
808
|
-
/* @__PURE__ */ jsx9(Text9, { children: ")" })
|
|
1324
|
+
|
|
1325
|
+
// src/ui/ModelsView.tsx
|
|
1326
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
1327
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1328
|
+
function ModelsView({ models, cursor, model, ollamaHost, effort }) {
|
|
1329
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
|
|
1330
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
1331
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "config" }),
|
|
1332
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
1333
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1334
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "model " }),
|
|
1335
|
+
/* @__PURE__ */ jsx4(Text4, { children: model ?? "\u2014" })
|
|
1336
|
+
] }),
|
|
1337
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1338
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "host " }),
|
|
1339
|
+
/* @__PURE__ */ jsx4(Text4, { children: ollamaHost ?? "http://localhost:11434" })
|
|
1340
|
+
] }),
|
|
1341
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1342
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "effort " }),
|
|
1343
|
+
/* @__PURE__ */ jsx4(Text4, { children: effort }),
|
|
1344
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (\u2190 \u2192)" })
|
|
1345
|
+
] })
|
|
1346
|
+
] })
|
|
809
1347
|
] }),
|
|
810
|
-
|
|
1348
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "switch model" }),
|
|
1349
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(ModelList, { models, cursor, activeModel: model, showActive: true }) }),
|
|
1350
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate enter switch \u2190\u2192 effort esc close" }) })
|
|
811
1351
|
] });
|
|
812
1352
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1353
|
+
|
|
1354
|
+
// src/ui/SessionsView.tsx
|
|
1355
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
1356
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1357
|
+
function relativeTime(iso) {
|
|
1358
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
1359
|
+
const min = Math.floor(diff / 6e4);
|
|
1360
|
+
if (min < 1) return "just now";
|
|
1361
|
+
if (min < 60) return `${min}m ago`;
|
|
1362
|
+
const hr = Math.floor(min / 60);
|
|
1363
|
+
if (hr < 24) return `${hr}h ago`;
|
|
1364
|
+
const d = Math.floor(hr / 24);
|
|
1365
|
+
return `${d}d ago`;
|
|
1366
|
+
}
|
|
1367
|
+
function SessionsView({ sessions, cursor }) {
|
|
1368
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, children: [
|
|
1369
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
|
|
1370
|
+
/* @__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) => {
|
|
1371
|
+
const active = i === cursor;
|
|
1372
|
+
const label = s.title;
|
|
1373
|
+
return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
|
|
1374
|
+
/* @__PURE__ */ jsxs5(Text5, { color: active ? "blue" : void 0, dimColor: !active, children: [
|
|
1375
|
+
active ? "\u276F " : " ",
|
|
1376
|
+
label
|
|
1377
|
+
] }),
|
|
1378
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}` })
|
|
1379
|
+
] }, s.id);
|
|
1380
|
+
}) }),
|
|
1381
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate enter resume d delete esc cancel" }) })
|
|
827
1382
|
] });
|
|
828
1383
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1384
|
+
|
|
1385
|
+
// src/ui/CommandPalette.tsx
|
|
1386
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
1387
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1388
|
+
var COMMANDS = [
|
|
1389
|
+
{ name: "/models", description: "switch model or adjust effort" },
|
|
1390
|
+
{ name: "/new", description: "save current session and start fresh" },
|
|
1391
|
+
{ name: "/sessions", description: "list sessions and resume one" },
|
|
1392
|
+
{ name: "/clear", description: "clear chat and reset context" },
|
|
1393
|
+
{ name: "/exit", description: "quit miii" }
|
|
1394
|
+
];
|
|
1395
|
+
function CommandPalette({ filter, cursor }) {
|
|
1396
|
+
const filtered = COMMANDS.filter((c) => c.name.startsWith(filter));
|
|
1397
|
+
if (filtered.length === 0) return null;
|
|
1398
|
+
const nameWidth = Math.max(...filtered.map((c) => c.name.length));
|
|
1399
|
+
return /* @__PURE__ */ jsxs6(
|
|
1400
|
+
Box6,
|
|
1401
|
+
{
|
|
1402
|
+
flexDirection: "column",
|
|
1403
|
+
borderStyle: "round",
|
|
1404
|
+
borderColor: "gray",
|
|
1405
|
+
marginX: 1,
|
|
1406
|
+
marginBottom: 0,
|
|
1407
|
+
paddingX: 1,
|
|
1408
|
+
children: [
|
|
1409
|
+
filtered.map((cmd2, i) => {
|
|
1410
|
+
const active = i === cursor;
|
|
1411
|
+
return /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
|
|
1412
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
|
|
1413
|
+
active ? "\u276F " : " ",
|
|
1414
|
+
cmd2.name.padEnd(nameWidth)
|
|
1415
|
+
] }),
|
|
1416
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: cmd2.description })
|
|
1417
|
+
] }, cmd2.name);
|
|
1418
|
+
}),
|
|
1419
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 0, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2191\u2193 navigate tab/enter autocomplete esc dismiss" }) })
|
|
1420
|
+
]
|
|
1421
|
+
}
|
|
1422
|
+
);
|
|
844
1423
|
}
|
|
845
|
-
function
|
|
846
|
-
|
|
847
|
-
const options = [
|
|
848
|
-
{ label: "Yes", key: "yes" },
|
|
849
|
-
{ label: "Yes, don't ask again for this", key: "always" },
|
|
850
|
-
{ label: "No", key: "no" }
|
|
851
|
-
];
|
|
852
|
-
const summary = summarizeInput(req.input);
|
|
853
|
-
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
|
|
854
|
-
/* @__PURE__ */ jsx9(Text9, { color: "blue", bold: true, children: "Tool use" }),
|
|
855
|
-
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
856
|
-
"Allow ",
|
|
857
|
-
/* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
|
|
858
|
-
"?"
|
|
859
|
-
] }) }),
|
|
860
|
-
summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: summary }) }),
|
|
861
|
-
/* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
|
|
862
|
-
i === cursor ? "\u276F " : " ",
|
|
863
|
-
i + 1,
|
|
864
|
-
". ",
|
|
865
|
-
opt.label
|
|
866
|
-
] }, opt.key)) })
|
|
867
|
-
] });
|
|
1424
|
+
function filteredCommands(filter) {
|
|
1425
|
+
return COMMANDS.filter((c) => c.name.startsWith(filter));
|
|
868
1426
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
] }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
|
|
895
|
-
),
|
|
896
|
-
thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
|
|
897
|
-
streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
898
|
-
/* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
|
|
899
|
-
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: streamingContent }) })
|
|
900
|
-
] }),
|
|
901
|
-
activeToolUses?.map((u) => {
|
|
902
|
-
const r = activeToolResults?.find((x) => x.tool_use_id === u.id);
|
|
903
|
-
return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
|
|
904
|
-
}),
|
|
905
|
-
pendingPermission && /* @__PURE__ */ jsx9(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
|
|
906
|
-
error && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
907
|
-
/* @__PURE__ */ jsx9(Text9, { color: "red", children: "\u25CF " }),
|
|
908
|
-
/* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
|
|
909
|
-
] })
|
|
910
|
-
] });
|
|
1427
|
+
|
|
1428
|
+
// src/session/store.ts
|
|
1429
|
+
init_client();
|
|
1430
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
|
|
1431
|
+
import { join as join2 } from "path";
|
|
1432
|
+
import { homedir as homedir2 } from "os";
|
|
1433
|
+
import { randomUUID } from "crypto";
|
|
1434
|
+
function encodeProjectDir(cwd) {
|
|
1435
|
+
return cwd.replace(/[/\\]/g, "-").replace(/^-+/, "");
|
|
1436
|
+
}
|
|
1437
|
+
var SESSION_DIR = join2(homedir2(), ".miii", "projects", encodeProjectDir(process.cwd()), "session");
|
|
1438
|
+
function newSessionId() {
|
|
1439
|
+
return randomUUID();
|
|
1440
|
+
}
|
|
1441
|
+
function sessionPath(id) {
|
|
1442
|
+
return join2(SESSION_DIR, `${id}.jsonl`);
|
|
1443
|
+
}
|
|
1444
|
+
function messageText(m) {
|
|
1445
|
+
if (typeof m.content === "string") return m.content;
|
|
1446
|
+
return m.content.map((b) => {
|
|
1447
|
+
if (b.type === "text") return b.text;
|
|
1448
|
+
if (b.type === "tool_use") return `[tool ${b.name}]`;
|
|
1449
|
+
if (b.type === "tool_result") return "[result]";
|
|
1450
|
+
return "";
|
|
1451
|
+
}).join(" ");
|
|
911
1452
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1453
|
+
function firstUserText(messages) {
|
|
1454
|
+
const first = messages.find((m) => m.role === "user");
|
|
1455
|
+
if (!first) return "untitled";
|
|
1456
|
+
return messageText(first).trim().slice(0, 80) || "untitled";
|
|
1457
|
+
}
|
|
1458
|
+
function readMeta(id) {
|
|
1459
|
+
try {
|
|
1460
|
+
const raw = readFileSync2(sessionPath(id), "utf-8");
|
|
1461
|
+
const firstLine = raw.slice(0, raw.indexOf("\n") === -1 ? raw.length : raw.indexOf("\n"));
|
|
1462
|
+
const parsed = JSON.parse(firstLine);
|
|
1463
|
+
if (parsed.type !== "meta") return null;
|
|
1464
|
+
const { type: _t, ...meta } = parsed;
|
|
1465
|
+
return meta;
|
|
1466
|
+
} catch {
|
|
1467
|
+
return null;
|
|
924
1468
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1469
|
+
}
|
|
1470
|
+
function persistSession(id, messages, title) {
|
|
1471
|
+
if (!messages.length) return;
|
|
1472
|
+
mkdirSync2(SESSION_DIR, { recursive: true });
|
|
1473
|
+
const existing = readMeta(id);
|
|
1474
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1475
|
+
const meta = {
|
|
1476
|
+
id,
|
|
1477
|
+
createdAt: existing?.createdAt ?? now,
|
|
1478
|
+
updatedAt: now,
|
|
1479
|
+
title: title ?? existing?.title ?? firstUserText(messages),
|
|
1480
|
+
messageCount: messages.length
|
|
1481
|
+
};
|
|
1482
|
+
const lines = [JSON.stringify({ type: "meta", ...meta })];
|
|
1483
|
+
for (const message of messages) {
|
|
1484
|
+
lines.push(JSON.stringify({ type: "message", message }));
|
|
930
1485
|
}
|
|
931
|
-
|
|
1486
|
+
writeFileSync2(sessionPath(id), lines.join("\n") + "\n", "utf-8");
|
|
932
1487
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
properties: {
|
|
941
|
-
path: { type: "string", description: "File path" },
|
|
942
|
-
old_str: { type: "string", description: "Exact text to replace" },
|
|
943
|
-
new_str: { type: "string", description: "Replacement text" }
|
|
944
|
-
},
|
|
945
|
-
required: ["path", "old_str", "new_str"]
|
|
946
|
-
},
|
|
947
|
-
handler: ({ path, old_str, new_str }) => {
|
|
948
|
-
try {
|
|
949
|
-
const abs = confinePath(path);
|
|
950
|
-
const src = readFileSync3(abs, "utf-8");
|
|
951
|
-
const first = src.indexOf(old_str);
|
|
952
|
-
if (first === -1) {
|
|
953
|
-
return { content: `old_str not found in ${path}`, is_error: true };
|
|
954
|
-
}
|
|
955
|
-
if (src.indexOf(old_str, first + 1) !== -1) {
|
|
956
|
-
return { content: `old_str not unique in ${path}`, is_error: true };
|
|
957
|
-
}
|
|
958
|
-
writeFileSync3(abs, src.slice(0, first) + new_str + src.slice(first + old_str.length), "utf-8");
|
|
959
|
-
return { content: `Edited ${path}` };
|
|
960
|
-
} catch (err) {
|
|
961
|
-
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
962
|
-
}
|
|
1488
|
+
function listSessions() {
|
|
1489
|
+
if (!existsSync2(SESSION_DIR)) return [];
|
|
1490
|
+
const metas = [];
|
|
1491
|
+
for (const file of readdirSync(SESSION_DIR)) {
|
|
1492
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
1493
|
+
const meta = readMeta(file.replace(/\.jsonl$/, ""));
|
|
1494
|
+
if (meta) metas.push(meta);
|
|
963
1495
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
description: "Read entire file contents as UTF-8 text.",
|
|
971
|
-
input_schema: {
|
|
972
|
-
type: "object",
|
|
973
|
-
properties: {
|
|
974
|
-
path: { type: "string", description: "File path" }
|
|
975
|
-
},
|
|
976
|
-
required: ["path"]
|
|
977
|
-
},
|
|
978
|
-
handler: ({ path }) => {
|
|
979
|
-
try {
|
|
980
|
-
const MAX = 2e5;
|
|
981
|
-
const raw = readFileSync4(confinePath(path), "utf-8");
|
|
982
|
-
const truncated = raw.length > MAX;
|
|
983
|
-
const body = truncated ? raw.slice(0, MAX) + `
|
|
984
|
-
[truncated: ${raw.length - MAX} more chars]` : raw;
|
|
985
|
-
return { content: body };
|
|
986
|
-
} catch (err) {
|
|
987
|
-
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
988
|
-
}
|
|
1496
|
+
return metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1497
|
+
}
|
|
1498
|
+
function deleteSession(id) {
|
|
1499
|
+
try {
|
|
1500
|
+
rmSync(sessionPath(id), { force: true });
|
|
1501
|
+
} catch {
|
|
989
1502
|
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
type: "object",
|
|
1000
|
-
properties: {
|
|
1001
|
-
path: { type: "string", description: "File path" },
|
|
1002
|
-
content: { type: "string", description: "Full file content" }
|
|
1003
|
-
},
|
|
1004
|
-
required: ["path", "content"]
|
|
1005
|
-
},
|
|
1006
|
-
handler: ({ path, content }) => {
|
|
1007
|
-
try {
|
|
1008
|
-
const abs = confinePath(path);
|
|
1009
|
-
mkdirSync3(dirname(abs), { recursive: true });
|
|
1010
|
-
writeFileSync4(abs, content, "utf-8");
|
|
1011
|
-
return { content: `Wrote ${path} (${content.length} bytes)` };
|
|
1012
|
-
} catch (err) {
|
|
1013
|
-
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
1503
|
+
}
|
|
1504
|
+
function loadSession(id) {
|
|
1505
|
+
try {
|
|
1506
|
+
const raw = readFileSync2(sessionPath(id), "utf-8");
|
|
1507
|
+
const messages = [];
|
|
1508
|
+
for (const line of raw.split("\n")) {
|
|
1509
|
+
if (!line.trim()) continue;
|
|
1510
|
+
const parsed = JSON.parse(line);
|
|
1511
|
+
if (parsed.type === "message") messages.push(parsed.message);
|
|
1014
1512
|
}
|
|
1513
|
+
return messages;
|
|
1514
|
+
} catch {
|
|
1515
|
+
return [];
|
|
1015
1516
|
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1517
|
+
}
|
|
1518
|
+
function toDisplayMessages(history) {
|
|
1519
|
+
const out = [];
|
|
1520
|
+
for (const m of history) {
|
|
1521
|
+
if (m.role === "system") continue;
|
|
1522
|
+
const blocks = Array.isArray(m.content) ? m.content : [{ type: "text", text: m.content }];
|
|
1523
|
+
if (m.role === "user") {
|
|
1524
|
+
const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1525
|
+
const results = blocks.filter((b) => b.type === "tool_result");
|
|
1526
|
+
if (results.length && out.length) {
|
|
1527
|
+
const last = out[out.length - 1];
|
|
1528
|
+
last.tool_results = [
|
|
1529
|
+
...last.tool_results ?? [],
|
|
1530
|
+
...results.map((r) => ({
|
|
1531
|
+
tool_use_id: r.tool_use_id,
|
|
1532
|
+
content: r.content,
|
|
1533
|
+
is_error: r.is_error
|
|
1534
|
+
}))
|
|
1535
|
+
];
|
|
1536
|
+
}
|
|
1537
|
+
if (text.trim()) out.push({ role: "user", content: text });
|
|
1538
|
+
} else {
|
|
1539
|
+
const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1540
|
+
const uses = blocks.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
|
1541
|
+
out.push({
|
|
1542
|
+
role: "assistant",
|
|
1543
|
+
content: text,
|
|
1544
|
+
tool_uses: uses.length ? uses : void 0
|
|
1040
1545
|
});
|
|
1041
|
-
const out = [stdout, stderr].filter(Boolean).join("\n");
|
|
1042
|
-
const is_error = exitCode !== 0;
|
|
1043
|
-
const body = out || (is_error ? `(no output)` : "");
|
|
1044
|
-
const content = is_error ? `${body}
|
|
1045
|
-
[exit ${exitCode}]` : body;
|
|
1046
|
-
return {
|
|
1047
|
-
content: content.slice(0, 32e3),
|
|
1048
|
-
is_error
|
|
1049
|
-
};
|
|
1050
|
-
} catch (err) {
|
|
1051
|
-
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
1052
1546
|
}
|
|
1053
1547
|
}
|
|
1054
|
-
|
|
1548
|
+
return out;
|
|
1549
|
+
}
|
|
1550
|
+
async function summarizeMessage(model, text) {
|
|
1551
|
+
const fallback = text.trim().slice(0, 80) || "untitled";
|
|
1552
|
+
const prompt = `Summarize this user request as a short title, 3-6 words, no punctuation. Reply with the title only.
|
|
1055
1553
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
case_insensitive: { type: "string", description: 'Set "true" for case-insensitive' },
|
|
1068
|
-
max_results: { type: "number", description: "Max matching lines (default 200)" }
|
|
1069
|
-
},
|
|
1070
|
-
required: ["pattern"]
|
|
1071
|
-
},
|
|
1072
|
-
handler: async ({ pattern, path, glob: glob2, case_insensitive, max_results }) => {
|
|
1073
|
-
const root = path ?? process.cwd();
|
|
1074
|
-
const limit = max_results ?? 200;
|
|
1075
|
-
const ci = case_insensitive === true || String(case_insensitive) === "true";
|
|
1076
|
-
const tryRg = async () => {
|
|
1077
|
-
const args = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
|
|
1078
|
-
if (ci) args.push("-i");
|
|
1079
|
-
if (glob2) args.push("--glob", glob2);
|
|
1080
|
-
args.push("--", pattern, root);
|
|
1081
|
-
return execa2("rg", args, { reject: false, timeout: 2e4 });
|
|
1082
|
-
};
|
|
1083
|
-
const tryGrep = async () => {
|
|
1084
|
-
const args = ["-R", "-n", "--color=never"];
|
|
1085
|
-
if (ci) args.push("-i");
|
|
1086
|
-
if (glob2) args.push("--include", glob2);
|
|
1087
|
-
args.push("--", pattern, root);
|
|
1088
|
-
return execa2("grep", args, { reject: false, timeout: 2e4 });
|
|
1089
|
-
};
|
|
1090
|
-
try {
|
|
1091
|
-
let res;
|
|
1092
|
-
try {
|
|
1093
|
-
res = await tryRg();
|
|
1094
|
-
if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
|
|
1095
|
-
res = await tryGrep();
|
|
1096
|
-
}
|
|
1097
|
-
} catch {
|
|
1098
|
-
res = await tryGrep();
|
|
1099
|
-
}
|
|
1100
|
-
const lines = (res.stdout ?? "").split("\n").slice(0, limit);
|
|
1101
|
-
const out = lines.join("\n");
|
|
1102
|
-
const code = res.exitCode ?? 0;
|
|
1103
|
-
if (!out && code === 1) return { content: "No matches." };
|
|
1104
|
-
return { content: out || res.stderr || "No matches.", is_error: code > 1 };
|
|
1105
|
-
} catch (err) {
|
|
1106
|
-
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
1554
|
+
Request:
|
|
1555
|
+
${text.slice(0, 2e3)}`;
|
|
1556
|
+
try {
|
|
1557
|
+
let out = "";
|
|
1558
|
+
for await (const chunk of chat(
|
|
1559
|
+
model,
|
|
1560
|
+
[{ role: "user", content: prompt }],
|
|
1561
|
+
void 0,
|
|
1562
|
+
{ temperature: 0.2, num_predict: 32 }
|
|
1563
|
+
)) {
|
|
1564
|
+
if (chunk.content) out += chunk.content;
|
|
1107
1565
|
}
|
|
1566
|
+
return out.trim().split("\n").filter(Boolean)[0]?.trim() || fallback;
|
|
1567
|
+
} catch {
|
|
1568
|
+
return fallback;
|
|
1108
1569
|
}
|
|
1109
|
-
};
|
|
1110
|
-
|
|
1111
|
-
// src/tools/glob.ts
|
|
1112
|
-
import { execa as execa3 } from "execa";
|
|
1113
|
-
function globToFindName(glob2) {
|
|
1114
|
-
return glob2;
|
|
1115
1570
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
timeout: 2e4
|
|
1134
|
-
});
|
|
1135
|
-
const tryFind = () => {
|
|
1136
|
-
const name = globToFindName(pattern.replace(/^\*\*\//, ""));
|
|
1137
|
-
return execa3("find", [root, "-type", "f", "-name", name], {
|
|
1138
|
-
reject: false,
|
|
1139
|
-
timeout: 2e4
|
|
1140
|
-
});
|
|
1141
|
-
};
|
|
1571
|
+
|
|
1572
|
+
// src/ui/FilePicker.tsx
|
|
1573
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
1574
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
1575
|
+
import { join as join3, relative } from "path";
|
|
1576
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1577
|
+
var IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".miii"]);
|
|
1578
|
+
var MAX_RESULTS = 10;
|
|
1579
|
+
var MAX_SCAN = 2e3;
|
|
1580
|
+
var cache = null;
|
|
1581
|
+
function listFiles(cwd) {
|
|
1582
|
+
if (cache && cache.cwd === cwd) return cache.files;
|
|
1583
|
+
const out = [];
|
|
1584
|
+
const stack = [cwd];
|
|
1585
|
+
while (stack.length && out.length < MAX_SCAN) {
|
|
1586
|
+
const dir = stack.pop();
|
|
1587
|
+
let entries;
|
|
1142
1588
|
try {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
|
|
1147
|
-
res = await tryFind();
|
|
1148
|
-
}
|
|
1149
|
-
} catch {
|
|
1150
|
-
res = await tryFind();
|
|
1151
|
-
}
|
|
1152
|
-
const lines = (res.stdout ?? "").split("\n").filter(Boolean).slice(0, limit);
|
|
1153
|
-
if (lines.length === 0) return { content: "No files matched." };
|
|
1154
|
-
return { content: lines.join("\n") };
|
|
1155
|
-
} catch (err) {
|
|
1156
|
-
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
1589
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1590
|
+
} catch {
|
|
1591
|
+
continue;
|
|
1157
1592
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
read_file,
|
|
1165
|
-
write_file,
|
|
1166
|
-
run_bash,
|
|
1167
|
-
grep,
|
|
1168
|
-
glob
|
|
1169
|
-
];
|
|
1170
|
-
function getTool(name) {
|
|
1171
|
-
return TOOLS.find((t) => t.name === name);
|
|
1172
|
-
}
|
|
1173
|
-
function toOllamaTools(tools = TOOLS) {
|
|
1174
|
-
return tools.map((t) => ({
|
|
1175
|
-
type: "function",
|
|
1176
|
-
function: {
|
|
1177
|
-
name: t.name,
|
|
1178
|
-
description: t.description,
|
|
1179
|
-
parameters: {
|
|
1180
|
-
type: "object",
|
|
1181
|
-
properties: t.input_schema.properties,
|
|
1182
|
-
required: t.input_schema.required
|
|
1183
|
-
}
|
|
1593
|
+
for (const e of entries) {
|
|
1594
|
+
if (IGNORE.has(e.name) || e.name.startsWith(".")) continue;
|
|
1595
|
+
const full = join3(dir, e.name);
|
|
1596
|
+
if (e.isDirectory()) stack.push(full);
|
|
1597
|
+
else if (e.isFile()) out.push(relative(cwd, full));
|
|
1598
|
+
if (out.length >= MAX_SCAN) break;
|
|
1184
1599
|
}
|
|
1185
|
-
}));
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// src/tools/validate.ts
|
|
1189
|
-
import { z } from "zod";
|
|
1190
|
-
function propSchema(spec) {
|
|
1191
|
-
if (spec.enum && spec.enum.length) return z.enum(spec.enum);
|
|
1192
|
-
switch (spec.type) {
|
|
1193
|
-
case "string":
|
|
1194
|
-
return z.string();
|
|
1195
|
-
case "number":
|
|
1196
|
-
return z.number();
|
|
1197
|
-
case "integer":
|
|
1198
|
-
return z.number().int();
|
|
1199
|
-
case "boolean":
|
|
1200
|
-
return z.boolean();
|
|
1201
|
-
case "array":
|
|
1202
|
-
return z.array(z.unknown());
|
|
1203
|
-
case "object":
|
|
1204
|
-
return z.record(z.unknown());
|
|
1205
|
-
default:
|
|
1206
|
-
return z.unknown();
|
|
1207
1600
|
}
|
|
1601
|
+
cache = { cwd, files: out };
|
|
1602
|
+
return out;
|
|
1603
|
+
}
|
|
1604
|
+
function parseMention(input) {
|
|
1605
|
+
const m = input.match(/(?:^|\s)@([^\s]*)$/);
|
|
1606
|
+
if (!m) return null;
|
|
1607
|
+
return { query: m[1], start: input.length - m[1].length - 1 };
|
|
1208
1608
|
}
|
|
1209
|
-
function
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1609
|
+
function searchFiles(cwd, query) {
|
|
1610
|
+
const files = listFiles(cwd);
|
|
1611
|
+
const q = query.toLowerCase();
|
|
1612
|
+
if (!q) return files.slice(0, MAX_RESULTS);
|
|
1613
|
+
const scored = [];
|
|
1614
|
+
for (const f of files) {
|
|
1615
|
+
const lf = f.toLowerCase();
|
|
1616
|
+
const idx = lf.indexOf(q);
|
|
1617
|
+
if (idx === -1) continue;
|
|
1618
|
+
const base = lf.split("/").pop() ?? lf;
|
|
1619
|
+
const baseIdx = base.indexOf(q);
|
|
1620
|
+
const score = baseIdx === 0 ? 0 : baseIdx > -1 ? 1 : 2 + idx;
|
|
1621
|
+
scored.push([score, f]);
|
|
1622
|
+
if (scored.length > 500) break;
|
|
1214
1623
|
}
|
|
1215
|
-
|
|
1624
|
+
scored.sort((a, b) => a[0] - b[0] || a[1].length - b[1].length);
|
|
1625
|
+
return scored.slice(0, MAX_RESULTS).map(([, f]) => f);
|
|
1216
1626
|
}
|
|
1217
|
-
function
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1627
|
+
function FilePicker({ matches: matches2, cursor }) {
|
|
1628
|
+
if (matches2.length === 0) return null;
|
|
1629
|
+
return /* @__PURE__ */ jsxs7(
|
|
1630
|
+
Box7,
|
|
1631
|
+
{
|
|
1632
|
+
flexDirection: "column",
|
|
1633
|
+
borderStyle: "round",
|
|
1634
|
+
borderColor: "gray",
|
|
1635
|
+
marginX: 1,
|
|
1636
|
+
marginBottom: 0,
|
|
1637
|
+
paddingX: 1,
|
|
1638
|
+
children: [
|
|
1639
|
+
matches2.map((f, i) => {
|
|
1640
|
+
const active = i === cursor;
|
|
1641
|
+
return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
|
|
1642
|
+
active ? "\u276F " : " ",
|
|
1643
|
+
f
|
|
1644
|
+
] }) }, f);
|
|
1645
|
+
}),
|
|
1646
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 0, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191\u2193 navigate tab insert esc dismiss" }) })
|
|
1647
|
+
]
|
|
1648
|
+
}
|
|
1649
|
+
);
|
|
1222
1650
|
}
|
|
1223
1651
|
|
|
1224
|
-
// src/
|
|
1225
|
-
|
|
1226
|
-
const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
1227
|
-
return `You are miii, a senior software engineer running in a terminal.
|
|
1228
|
-
|
|
1229
|
-
Working directory: ${cwd}
|
|
1230
|
-
|
|
1231
|
-
# Goal Understanding (read this first, every turn)
|
|
1232
|
-
Before acting on any request, extract and hold three things:
|
|
1233
|
-
GOAL: what the user ultimately wants (outcome, not steps)
|
|
1234
|
-
CRITERION: how you will know the goal is met
|
|
1235
|
-
GAPS: anything unclear that would force you to guess
|
|
1236
|
-
|
|
1237
|
-
If GAPS is non-empty, ask the minimum questions needed to fill them \u2014 one message, numbered list \u2014 before touching any file or running any command. Do not guess. Do not act on assumptions.
|
|
1238
|
-
|
|
1239
|
-
Re-read GOAL before every tool call. If a tool call does not move toward GOAL, skip it.
|
|
1240
|
-
|
|
1241
|
-
# Attention: re-attend to goal at each step
|
|
1242
|
-
After each tool result, answer silently: "Does this result move me toward GOAL?"
|
|
1243
|
-
YES \u2192 continue
|
|
1244
|
-
NO \u2192 stop, re-derive plan from GOAL, explain the correction in one line
|
|
1245
|
-
|
|
1246
|
-
This prevents drift. Each step attends to the original goal, not just the previous step.
|
|
1247
|
-
|
|
1248
|
-
# Output format
|
|
1249
|
-
- Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
|
|
1250
|
-
- Quote code, paths, and identifiers inline as plain text. Do not wrap them.
|
|
1251
|
-
- Keep prose terse.
|
|
1252
|
-
|
|
1253
|
-
# Engineering mindset
|
|
1254
|
-
- Treat every request as one of: bug, feature, or fix. Name which one before you start.
|
|
1255
|
-
- Apply first principles: decompose unclear tasks into smallest concrete sub-problems, solve each explicitly, compose the result.
|
|
1256
|
-
- Never guess. If a fact (file path, function signature, current behavior) is unknown, read or search for it first.
|
|
1257
|
-
|
|
1258
|
-
# Clarifying questions \u2014 when to ask
|
|
1259
|
-
Ask BEFORE acting when:
|
|
1260
|
-
- The goal has more than one valid interpretation
|
|
1261
|
-
- Success criterion is ambiguous (e.g. "make it better" \u2014 better how?)
|
|
1262
|
-
- Required context is missing (which file? which behavior? which user?)
|
|
1263
|
-
- Two reasonable approaches have different tradeoffs the user should choose
|
|
1264
|
-
|
|
1265
|
-
Do NOT ask when:
|
|
1266
|
-
- The answer is findable by reading the codebase
|
|
1267
|
-
- There is only one sensible interpretation
|
|
1268
|
-
- The user has already answered this implicitly
|
|
1269
|
-
|
|
1270
|
-
Ask in a numbered list. One round of questions per turn. Then wait.
|
|
1271
|
-
|
|
1272
|
-
# Tool calls
|
|
1273
|
-
- When you need a tool, emit the tool call directly. No preamble, no narration, no "I will use X".
|
|
1274
|
-
- Never describe a tool call instead of emitting it. If you cannot emit the call, answer in plain text.
|
|
1275
|
-
- After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
|
|
1276
|
-
|
|
1277
|
-
# Tools
|
|
1278
|
-
You have access to the following tools. Call them via the function-calling interface.
|
|
1279
|
-
${toolLines}
|
|
1280
|
-
|
|
1281
|
-
# Loop semantics
|
|
1282
|
-
- When you need to act on the filesystem or run a command, emit a tool call.
|
|
1283
|
-
- After each tool result, decide: more tool calls, or a final plain-text answer.
|
|
1284
|
-
- Stop emitting tool calls when GOAL is met. Reply with a concise plain-text final message confirming CRITERION is satisfied.
|
|
1285
|
-
|
|
1286
|
-
# Rules
|
|
1287
|
-
- Prefer editing existing files over creating new ones.
|
|
1288
|
-
- For edit_file, ensure old_str is unique within the target file.
|
|
1289
|
-
- Never invent file paths. Read, glob, or grep before editing.
|
|
1290
|
-
- No filler, no pleasantries, no apologies.
|
|
1291
|
-
|
|
1292
|
-
# Testing and verification
|
|
1293
|
-
- Always test the code after a change. Run the project's tests (e.g. npm test, pytest, go test) or the relevant script via run_bash before declaring a task done.
|
|
1294
|
-
- If no test exists for the change, run the affected entry point via run_bash to verify it behaves correctly.
|
|
1295
|
-
- Treat a green test run or a successful command as the completion signal. If it fails, fix and re-run.
|
|
1296
|
-
|
|
1297
|
-
# Permissions
|
|
1298
|
-
- File tools are confined to the working directory; paths outside it are denied.
|
|
1299
|
-
- Each tool call may prompt the user for approval. If they choose "don't ask again", the exact command or path is persisted to ~/.miii/permissions.json and the same call is auto-allowed thereafter.
|
|
1300
|
-
`;
|
|
1301
|
-
}
|
|
1652
|
+
// src/ui/ChatView.tsx
|
|
1653
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
1302
1654
|
|
|
1303
|
-
// src/
|
|
1304
|
-
import {
|
|
1305
|
-
import {
|
|
1306
|
-
import {
|
|
1307
|
-
var
|
|
1308
|
-
var
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
return Array.isArray(data.rules) ? data.rules : [];
|
|
1314
|
-
} catch {
|
|
1315
|
-
return [];
|
|
1316
|
-
}
|
|
1655
|
+
// src/ui/ThinkingBlock.tsx
|
|
1656
|
+
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
1657
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
1658
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1659
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1660
|
+
var globalThinkingVisible = false;
|
|
1661
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
1662
|
+
function toggleThinkingVisible() {
|
|
1663
|
+
globalThinkingVisible = !globalThinkingVisible;
|
|
1664
|
+
listeners.forEach((fn) => fn());
|
|
1317
1665
|
}
|
|
1318
|
-
function
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1666
|
+
function useThinkingVisible() {
|
|
1667
|
+
const [visible, setVisible] = useState2(globalThinkingVisible);
|
|
1668
|
+
useEffect2(() => {
|
|
1669
|
+
const handler = () => setVisible(globalThinkingVisible);
|
|
1670
|
+
listeners.add(handler);
|
|
1671
|
+
return () => {
|
|
1672
|
+
listeners.delete(handler);
|
|
1673
|
+
};
|
|
1674
|
+
}, []);
|
|
1675
|
+
return visible;
|
|
1323
1676
|
}
|
|
1324
|
-
function
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1677
|
+
function ThinkingBlock({ content }) {
|
|
1678
|
+
const [frame, setFrame] = useState2(0);
|
|
1679
|
+
const visible = useThinkingVisible();
|
|
1680
|
+
useEffect2(() => {
|
|
1681
|
+
const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 80);
|
|
1682
|
+
return () => clearInterval(t);
|
|
1683
|
+
}, []);
|
|
1684
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
|
|
1685
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
1686
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "blue", children: [
|
|
1687
|
+
FRAMES[frame],
|
|
1688
|
+
" "
|
|
1689
|
+
] }),
|
|
1690
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: "thinking\u2026" }),
|
|
1691
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
1692
|
+
" \xB7 ctrl+t to ",
|
|
1693
|
+
visible ? "hide" : "show",
|
|
1694
|
+
" thoughts"
|
|
1695
|
+
] })
|
|
1696
|
+
] }),
|
|
1697
|
+
visible && content ? /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: content }) }) : null
|
|
1698
|
+
] });
|
|
1329
1699
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1700
|
+
|
|
1701
|
+
// src/ui/constants.ts
|
|
1702
|
+
var EMPTY_STATE_HINTS = [
|
|
1703
|
+
"\u2022 explain @file \u2014 reference a file with @",
|
|
1704
|
+
"\u2022 /models \u2014 switch model or effort",
|
|
1705
|
+
"\u2022 /new \u2014 start a new chat",
|
|
1706
|
+
"\u2022 /sessions \u2014 view saved chats",
|
|
1707
|
+
"\u2022 ctrl+t \u2014 toggle thinking"
|
|
1708
|
+
];
|
|
1709
|
+
var EMPTY_STATE_TITLE = "Ask anything, or try:";
|
|
1710
|
+
|
|
1711
|
+
// src/ui/ChatView.tsx
|
|
1712
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1713
|
+
function formatTokens(n) {
|
|
1714
|
+
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
|
|
1715
|
+
return String(n);
|
|
1335
1716
|
}
|
|
1336
|
-
function
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1717
|
+
function formatDuration(ms) {
|
|
1718
|
+
const totalSec = ms / 1e3;
|
|
1719
|
+
if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
|
|
1720
|
+
const m = Math.floor(totalSec / 60);
|
|
1721
|
+
const s = Math.round(totalSec - m * 60);
|
|
1722
|
+
return `${m}m ${s}s`;
|
|
1340
1723
|
}
|
|
1341
|
-
function
|
|
1342
|
-
if (
|
|
1343
|
-
|
|
1344
|
-
return globToRegExp(rule.pattern).test(subject);
|
|
1345
|
-
} catch {
|
|
1346
|
-
return false;
|
|
1347
|
-
}
|
|
1724
|
+
function countLines(s) {
|
|
1725
|
+
if (!s) return 0;
|
|
1726
|
+
return s.split("\n").length;
|
|
1348
1727
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1728
|
+
function FileEditBlock({
|
|
1729
|
+
label,
|
|
1730
|
+
path,
|
|
1731
|
+
added,
|
|
1732
|
+
removed,
|
|
1733
|
+
previewLines
|
|
1734
|
+
}) {
|
|
1735
|
+
const MAX = 16;
|
|
1736
|
+
const shown = previewLines.slice(0, MAX);
|
|
1737
|
+
const extra = previewLines.length - shown.length;
|
|
1738
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
1739
|
+
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1740
|
+
/* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
|
|
1741
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
|
|
1742
|
+
label,
|
|
1743
|
+
" "
|
|
1744
|
+
] }),
|
|
1745
|
+
/* @__PURE__ */ jsx9(Text9, { children: "(" }),
|
|
1746
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: path }),
|
|
1747
|
+
/* @__PURE__ */ jsx9(Text9, { children: ")" })
|
|
1748
|
+
] }),
|
|
1749
|
+
/* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1750
|
+
"\u23BF ",
|
|
1751
|
+
removed > 0 ? `Added ${added} lines, removed ${removed} lines` : `Added ${added} lines`
|
|
1752
|
+
] }) }),
|
|
1753
|
+
shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { color: ln.sign === "+" ? "green" : ln.sign === "-" ? "red" : void 0, dimColor: ln.sign === " ", children: [
|
|
1754
|
+
ln.sign,
|
|
1755
|
+
" ",
|
|
1756
|
+
ln.text
|
|
1757
|
+
] }) }, i)),
|
|
1758
|
+
extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1759
|
+
"\u2026 ",
|
|
1760
|
+
extra,
|
|
1761
|
+
" more lines"
|
|
1762
|
+
] }) })
|
|
1763
|
+
] });
|
|
1357
1764
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1765
|
+
var TOOL_LABEL = {
|
|
1766
|
+
write_file: "Write",
|
|
1767
|
+
edit_file: "Update",
|
|
1768
|
+
read_file: "Read",
|
|
1769
|
+
run_bash: "Bash",
|
|
1770
|
+
glob: "Glob",
|
|
1771
|
+
grep: "Grep"
|
|
1772
|
+
};
|
|
1773
|
+
function truncate(s, max) {
|
|
1774
|
+
if (s.length <= max) return s;
|
|
1775
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
1363
1776
|
}
|
|
1364
|
-
function
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
}));
|
|
1379
|
-
}
|
|
1380
|
-
out.push(ollamaMsg);
|
|
1381
|
-
continue;
|
|
1777
|
+
function toolHeader(use) {
|
|
1778
|
+
const label = TOOL_LABEL[use.name] ?? use.name;
|
|
1779
|
+
const input = use.input ?? {};
|
|
1780
|
+
let arg = "";
|
|
1781
|
+
switch (use.name) {
|
|
1782
|
+
case "write_file":
|
|
1783
|
+
case "edit_file":
|
|
1784
|
+
case "read_file":
|
|
1785
|
+
arg = String(input.path ?? input.file_path ?? "");
|
|
1786
|
+
break;
|
|
1787
|
+
case "run_bash": {
|
|
1788
|
+
const cmd2 = String(input.command ?? "").replace(/\s+/g, " ");
|
|
1789
|
+
arg = truncate(cmd2, 120);
|
|
1790
|
+
break;
|
|
1382
1791
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (texts.length > 0) {
|
|
1390
|
-
out.push({ role: "user", content: texts.map((t) => t.text).join("") });
|
|
1391
|
-
}
|
|
1792
|
+
case "glob":
|
|
1793
|
+
case "grep":
|
|
1794
|
+
arg = truncate(String(input.pattern ?? ""), 120);
|
|
1795
|
+
break;
|
|
1796
|
+
default: {
|
|
1797
|
+
arg = truncate(JSON.stringify(input), 80);
|
|
1392
1798
|
}
|
|
1393
1799
|
}
|
|
1394
|
-
return
|
|
1800
|
+
return { label, arg };
|
|
1395
1801
|
}
|
|
1396
|
-
function
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
if (c) calls.push(c);
|
|
1404
|
-
return "";
|
|
1405
|
-
});
|
|
1406
|
-
const fenceRe = /```(?:json|tool_call)?\s*([\s\S]*?)```/g;
|
|
1407
|
-
cleaned = cleaned.replace(fenceRe, (_m, body) => {
|
|
1408
|
-
const c = tryParse(body, knownToolNames);
|
|
1409
|
-
if (c) {
|
|
1410
|
-
calls.push(c);
|
|
1411
|
-
return "";
|
|
1802
|
+
function summarizeResult(res, toolName) {
|
|
1803
|
+
const content = res.content ?? "";
|
|
1804
|
+
const lines = content.split("\n");
|
|
1805
|
+
if (!res.is_error) {
|
|
1806
|
+
if (toolName === "read_file") {
|
|
1807
|
+
const total = lines.length;
|
|
1808
|
+
return `Read ${total} line${total === 1 ? "" : "s"}`;
|
|
1412
1809
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if (
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
}
|
|
1810
|
+
if (toolName === "grep") {
|
|
1811
|
+
if (content === "No matches.") return "No matches";
|
|
1812
|
+
const n = lines.filter(Boolean).length;
|
|
1813
|
+
return `${n} match${n === 1 ? "" : "es"}`;
|
|
1814
|
+
}
|
|
1815
|
+
if (toolName === "glob") {
|
|
1816
|
+
if (content === "No files matched.") return "No files";
|
|
1817
|
+
const n = lines.filter(Boolean).length;
|
|
1818
|
+
return `${n} file${n === 1 ? "" : "s"}`;
|
|
1423
1819
|
}
|
|
1424
1820
|
}
|
|
1425
|
-
|
|
1821
|
+
const firstNonEmpty = lines.find((l) => l.trim().length > 0) ?? "";
|
|
1822
|
+
const extra = lines.length - 1;
|
|
1823
|
+
const head = firstNonEmpty.length > 100 ? firstNonEmpty.slice(0, 97) + "..." : firstNonEmpty;
|
|
1824
|
+
return extra > 0 ? `${head} (+${extra} lines)` : head;
|
|
1426
1825
|
}
|
|
1427
|
-
function
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
} catch {
|
|
1437
|
-
return null;
|
|
1826
|
+
function ToolResultBlock({ result, toolName }) {
|
|
1827
|
+
const content = result.content ?? "";
|
|
1828
|
+
const lines = content.split("\n");
|
|
1829
|
+
const showMulti = (toolName === "run_bash" || toolName === "grep" || toolName === "glob" || result.is_error) && lines.length > 1;
|
|
1830
|
+
if (!showMulti) {
|
|
1831
|
+
return /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
|
|
1832
|
+
"\u23BF ",
|
|
1833
|
+
summarizeResult(result, toolName)
|
|
1834
|
+
] }) });
|
|
1438
1835
|
}
|
|
1836
|
+
const MAX_LINES = 10;
|
|
1837
|
+
const MAX_LINE_WIDTH = 200;
|
|
1838
|
+
const shown = lines.slice(0, MAX_LINES).map((l) => truncate(l, MAX_LINE_WIDTH));
|
|
1839
|
+
const extra = lines.length - shown.length;
|
|
1840
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
1841
|
+
/* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
|
|
1842
|
+
"\u23BF ",
|
|
1843
|
+
summarizeResult(result, toolName)
|
|
1844
|
+
] }),
|
|
1845
|
+
shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { color: result.is_error ? "red" : void 0, dimColor: true, children: ln || " " }) }, i)),
|
|
1846
|
+
extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1847
|
+
"\u2026 ",
|
|
1848
|
+
extra,
|
|
1849
|
+
" more lines"
|
|
1850
|
+
] }) })
|
|
1851
|
+
] });
|
|
1439
1852
|
}
|
|
1440
|
-
function
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const ch = s[i];
|
|
1448
|
-
if (inStr) {
|
|
1449
|
-
if (esc) esc = false;
|
|
1450
|
-
else if (ch === "\\") esc = true;
|
|
1451
|
-
else if (ch === '"') inStr = false;
|
|
1452
|
-
continue;
|
|
1453
|
-
}
|
|
1454
|
-
if (ch === '"') {
|
|
1455
|
-
inStr = true;
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
|
-
if (ch === "{") depth++;
|
|
1459
|
-
else if (ch === "}") {
|
|
1460
|
-
depth--;
|
|
1461
|
-
if (depth === 0) return { json: s.slice(start, i + 1), start, end: i + 1 };
|
|
1462
|
-
}
|
|
1853
|
+
function ToolUseLine({ use, result }) {
|
|
1854
|
+
if (use.name === "write_file" && !result?.is_error) {
|
|
1855
|
+
const input = use.input;
|
|
1856
|
+
const content = input.content ?? "";
|
|
1857
|
+
const added = countLines(content);
|
|
1858
|
+
const preview = content.split("\n").map((t) => ({ sign: "+", text: t }));
|
|
1859
|
+
return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Write", path: input.path ?? "", added, removed: 0, previewLines: preview });
|
|
1463
1860
|
}
|
|
1464
|
-
|
|
1861
|
+
if (use.name === "edit_file" && !result?.is_error) {
|
|
1862
|
+
const input = use.input;
|
|
1863
|
+
const oldS = input.old_str ?? "";
|
|
1864
|
+
const newS = input.new_str ?? "";
|
|
1865
|
+
const added = countLines(newS);
|
|
1866
|
+
const removed = countLines(oldS);
|
|
1867
|
+
const preview = [
|
|
1868
|
+
...oldS.split("\n").map((t) => ({ sign: "-", text: t })),
|
|
1869
|
+
...newS.split("\n").map((t) => ({ sign: "+", text: t }))
|
|
1870
|
+
];
|
|
1871
|
+
return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Update", path: input.path ?? "", added, removed, previewLines: preview });
|
|
1872
|
+
}
|
|
1873
|
+
const { label, arg } = toolHeader(use);
|
|
1874
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
1875
|
+
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
1876
|
+
/* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
|
|
1877
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
|
|
1878
|
+
label,
|
|
1879
|
+
" "
|
|
1880
|
+
] }),
|
|
1881
|
+
/* @__PURE__ */ jsx9(Text9, { children: "(" }),
|
|
1882
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: arg }),
|
|
1883
|
+
/* @__PURE__ */ jsx9(Text9, { children: ")" })
|
|
1884
|
+
] }),
|
|
1885
|
+
result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
|
|
1886
|
+
] });
|
|
1465
1887
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1888
|
+
function AssistantMessage({ msg }) {
|
|
1889
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
1890
|
+
msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
|
|
1891
|
+
/* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
|
|
1892
|
+
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
1893
|
+
] }),
|
|
1894
|
+
msg.tool_uses?.map((u) => {
|
|
1895
|
+
const r = msg.tool_results?.find((x) => x.tool_use_id === u.id);
|
|
1896
|
+
return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
|
|
1897
|
+
}),
|
|
1898
|
+
msg.tokens && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1899
|
+
`\u21B3 Completed \xB7 ${formatTokens(msg.tokens.prompt_eval + msg.tokens.eval)} tokens`,
|
|
1900
|
+
msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
|
|
1901
|
+
] }) })
|
|
1902
|
+
] });
|
|
1903
|
+
}
|
|
1904
|
+
function summarizeInput(input) {
|
|
1905
|
+
if (!input || typeof input !== "object") return "";
|
|
1906
|
+
const obj = input;
|
|
1907
|
+
const priority = ["path", "file_path", "command", "pattern", "query"];
|
|
1908
|
+
for (const k of priority) {
|
|
1909
|
+
const v = obj[k];
|
|
1910
|
+
if (typeof v === "string" && v.length > 0) return `${k}: ${v}`;
|
|
1476
1911
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
name: tc.function.name,
|
|
1483
|
-
input: tc.function.arguments ?? {}
|
|
1484
|
-
});
|
|
1912
|
+
const first = Object.entries(obj).find(([, v]) => typeof v === "string");
|
|
1913
|
+
if (first) {
|
|
1914
|
+
const [k, v] = first;
|
|
1915
|
+
const trimmed = v.length > 80 ? v.slice(0, 80) + "\u2026" : v;
|
|
1916
|
+
return `${k}: ${trimmed}`;
|
|
1485
1917
|
}
|
|
1486
|
-
return
|
|
1918
|
+
return "";
|
|
1487
1919
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
async function* runAgent(opts) {
|
|
1495
|
-
const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
|
|
1496
|
-
const startTime = Date.now();
|
|
1497
|
-
const system = buildSystemPrompt(TOOLS, cwd);
|
|
1498
|
-
const ollamaTools = toOllamaTools(TOOLS);
|
|
1499
|
-
const history = [
|
|
1500
|
-
...opts.history,
|
|
1501
|
-
{ role: "user", content: opts.userText }
|
|
1920
|
+
function PermissionPrompt({ req, cursor }) {
|
|
1921
|
+
const label = TOOL_LABEL[req.toolName] ?? req.toolName;
|
|
1922
|
+
const options = [
|
|
1923
|
+
{ label: "Yes", key: "yes" },
|
|
1924
|
+
{ label: "Yes, don't ask again for this", key: "always" },
|
|
1925
|
+
{ label: "No", key: "no" }
|
|
1502
1926
|
];
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
return
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
type: "aborted",
|
|
1563
|
-
prompt_tokens: promptTokens,
|
|
1564
|
-
eval_tokens: evalTokens,
|
|
1565
|
-
duration_ms: Date.now() - startTime
|
|
1566
|
-
};
|
|
1567
|
-
return history;
|
|
1568
|
-
}
|
|
1569
|
-
const blocks = blocksFromOllama(text, tool_calls, TOOLS.map((t) => t.name));
|
|
1570
|
-
const tool_uses = blocks.filter((b) => b.type === "tool_use");
|
|
1571
|
-
history.push({ role: "assistant", content: blocks });
|
|
1572
|
-
if (tool_uses.length === 0) {
|
|
1573
|
-
yield { type: "turn-end", stop_reason: "end_turn" };
|
|
1574
|
-
break;
|
|
1575
|
-
}
|
|
1576
|
-
const sig = JSON.stringify(
|
|
1577
|
-
blocks.map(
|
|
1578
|
-
(b) => b.type === "tool_use" ? { t: "u", n: b.name, i: b.input } : b.type === "text" ? { t: "t", x: b.text.trim() } : b
|
|
1579
|
-
)
|
|
1580
|
-
);
|
|
1581
|
-
if (sig === lastAssistantSig) {
|
|
1582
|
-
repeatCount++;
|
|
1583
|
-
if (repeatCount >= 2) {
|
|
1584
|
-
yield { type: "error", message: "Agent loop detected: assistant produced identical output 3 turns in a row" };
|
|
1585
|
-
return history;
|
|
1586
|
-
}
|
|
1587
|
-
} else {
|
|
1588
|
-
repeatCount = 0;
|
|
1589
|
-
lastAssistantSig = sig;
|
|
1590
|
-
}
|
|
1591
|
-
for (const u of tool_uses) yield { type: "tool-use", block: u };
|
|
1592
|
-
const results = [];
|
|
1593
|
-
for (const use of tool_uses) {
|
|
1594
|
-
const tool = getTool(use.name);
|
|
1595
|
-
if (!tool) {
|
|
1596
|
-
const r2 = {
|
|
1597
|
-
type: "tool_result",
|
|
1598
|
-
tool_use_id: use.id,
|
|
1599
|
-
content: `Unknown tool: ${use.name}`,
|
|
1600
|
-
is_error: true
|
|
1601
|
-
};
|
|
1602
|
-
results.push(r2);
|
|
1603
|
-
yield { type: "tool-result", block: r2 };
|
|
1604
|
-
continue;
|
|
1605
|
-
}
|
|
1606
|
-
const invalid = validateInput(tool.input_schema, use.input);
|
|
1607
|
-
if (invalid) {
|
|
1608
|
-
const r2 = {
|
|
1609
|
-
type: "tool_result",
|
|
1610
|
-
tool_use_id: use.id,
|
|
1611
|
-
content: `${invalid} for ${use.name}.`,
|
|
1612
|
-
is_error: true
|
|
1613
|
-
};
|
|
1614
|
-
results.push(r2);
|
|
1615
|
-
yield { type: "tool-result", block: r2 };
|
|
1616
|
-
continue;
|
|
1617
|
-
}
|
|
1618
|
-
const decision = await check(use.name, use.input, permissions);
|
|
1619
|
-
if (decision === "deny") {
|
|
1620
|
-
const r2 = {
|
|
1621
|
-
type: "tool_result",
|
|
1622
|
-
tool_use_id: use.id,
|
|
1623
|
-
content: `Permission denied for ${use.name}.`,
|
|
1624
|
-
is_error: true
|
|
1625
|
-
};
|
|
1626
|
-
results.push(r2);
|
|
1627
|
-
yield { type: "permission-denied", toolName: use.name, tool_use_id: use.id };
|
|
1628
|
-
yield { type: "tool-result", block: r2 };
|
|
1629
|
-
continue;
|
|
1630
|
-
}
|
|
1631
|
-
try {
|
|
1632
|
-
await hooks?.firePre(use);
|
|
1633
|
-
} catch {
|
|
1634
|
-
}
|
|
1635
|
-
let r;
|
|
1636
|
-
try {
|
|
1637
|
-
const out = await tool.handler(use.input);
|
|
1638
|
-
r = {
|
|
1639
|
-
type: "tool_result",
|
|
1640
|
-
tool_use_id: use.id,
|
|
1641
|
-
content: out.content,
|
|
1642
|
-
is_error: out.is_error
|
|
1643
|
-
};
|
|
1644
|
-
} catch (err) {
|
|
1645
|
-
r = {
|
|
1646
|
-
type: "tool_result",
|
|
1647
|
-
tool_use_id: use.id,
|
|
1648
|
-
content: err instanceof Error ? err.message : String(err),
|
|
1649
|
-
is_error: true
|
|
1650
|
-
};
|
|
1651
|
-
}
|
|
1652
|
-
try {
|
|
1653
|
-
await hooks?.firePost(use, r);
|
|
1654
|
-
} catch {
|
|
1655
|
-
}
|
|
1656
|
-
results.push(r);
|
|
1657
|
-
yield { type: "tool-result", block: r };
|
|
1658
|
-
}
|
|
1659
|
-
history.push({ role: "user", content: results });
|
|
1660
|
-
yield { type: "turn-end", stop_reason: "tool_use" };
|
|
1661
|
-
}
|
|
1662
|
-
yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
|
|
1663
|
-
return history;
|
|
1927
|
+
const summary = summarizeInput(req.input);
|
|
1928
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
|
|
1929
|
+
/* @__PURE__ */ jsx9(Text9, { color: "blue", bold: true, children: "Tool use" }),
|
|
1930
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
1931
|
+
"Allow ",
|
|
1932
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
|
|
1933
|
+
"?"
|
|
1934
|
+
] }) }),
|
|
1935
|
+
summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: summary }) }),
|
|
1936
|
+
/* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
|
|
1937
|
+
i === cursor ? "\u276F " : " ",
|
|
1938
|
+
i + 1,
|
|
1939
|
+
". ",
|
|
1940
|
+
opt.label
|
|
1941
|
+
] }, opt.key)) })
|
|
1942
|
+
] });
|
|
1943
|
+
}
|
|
1944
|
+
function ChatView({
|
|
1945
|
+
messages,
|
|
1946
|
+
streaming,
|
|
1947
|
+
streamingContent,
|
|
1948
|
+
thinking,
|
|
1949
|
+
thinkingContent,
|
|
1950
|
+
error,
|
|
1951
|
+
pendingPermission,
|
|
1952
|
+
permissionCursor = 0,
|
|
1953
|
+
activeToolUses,
|
|
1954
|
+
activeToolResults
|
|
1955
|
+
}) {
|
|
1956
|
+
const empty = messages.length === 0 && !streaming && !thinking && !pendingPermission && !error;
|
|
1957
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
|
|
1958
|
+
empty && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
1959
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: EMPTY_STATE_TITLE }),
|
|
1960
|
+
EMPTY_STATE_HINTS.map((h, i) => /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1961
|
+
" ",
|
|
1962
|
+
h
|
|
1963
|
+
] }, i))
|
|
1964
|
+
] }),
|
|
1965
|
+
messages.map(
|
|
1966
|
+
(msg, i) => msg.role === "user" ? /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
1967
|
+
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
|
|
1968
|
+
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
1969
|
+
] }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
|
|
1970
|
+
),
|
|
1971
|
+
thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
|
|
1972
|
+
streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
1973
|
+
/* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
|
|
1974
|
+
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: streamingContent }) })
|
|
1975
|
+
] }),
|
|
1976
|
+
activeToolUses?.map((u) => {
|
|
1977
|
+
const r = activeToolResults?.find((x) => x.tool_use_id === u.id);
|
|
1978
|
+
return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
|
|
1979
|
+
}),
|
|
1980
|
+
pendingPermission && /* @__PURE__ */ jsx9(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
|
|
1981
|
+
error && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
1982
|
+
/* @__PURE__ */ jsx9(Text9, { color: "red", children: "\u25CF " }),
|
|
1983
|
+
/* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
|
|
1984
|
+
] })
|
|
1985
|
+
] });
|
|
1664
1986
|
}
|
|
1665
1987
|
|
|
1666
1988
|
// src/ui/hooks/useAgentRunner.ts
|
|
1989
|
+
init_loop();
|
|
1990
|
+
import { useState as useState3, useRef } from "react";
|
|
1667
1991
|
var FLUSH_MS = 100;
|
|
1668
1992
|
function useAgentRunner(model, activeCtx) {
|
|
1669
1993
|
const [messages, setMessages] = useState3([]);
|
|
@@ -2250,7 +2574,7 @@ function App() {
|
|
|
2250
2574
|
})();
|
|
2251
2575
|
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
|
|
2252
2576
|
/* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error }),
|
|
2253
|
-
updateAvailable && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run:
|
|
2577
|
+
updateAvailable && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` }) }),
|
|
2254
2578
|
state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "connecting to ollama\u2026" }) }),
|
|
2255
2579
|
agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
|
|
2256
2580
|
ChatView,
|
|
@@ -2311,4 +2635,15 @@ function App() {
|
|
|
2311
2635
|
}
|
|
2312
2636
|
|
|
2313
2637
|
// src/cli.tsx
|
|
2314
|
-
|
|
2638
|
+
var [, , cmd, ...rest] = process.argv;
|
|
2639
|
+
if (cmd === "update" || cmd === "--update" || cmd === "-u") {
|
|
2640
|
+
const { spawnSync } = await import("child_process");
|
|
2641
|
+
console.log("Updating miii-agent\u2026");
|
|
2642
|
+
const r = spawnSync("npm", ["i", "-g", "miii-agent@latest"], { stdio: "inherit", shell: process.platform === "win32" });
|
|
2643
|
+
process.exit(r.status ?? 1);
|
|
2644
|
+
} else if (cmd === "doctor" || cmd === "eval") {
|
|
2645
|
+
const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
2646
|
+
process.exit(await runEval2(rest));
|
|
2647
|
+
} else {
|
|
2648
|
+
render(createElement(App));
|
|
2649
|
+
}
|