miii-agent 0.1.32 → 0.1.33
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/dist/cli.js +158 -41
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -184,7 +184,7 @@ async function modelContext(entry, model) {
|
|
|
184
184
|
}
|
|
185
185
|
const msg = err instanceof Error ? err.message : String(err);
|
|
186
186
|
if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
|
|
187
|
-
throw new Error(`
|
|
187
|
+
throw new Error(`I don't see a model called "${model}" on Ollama. Pull it first with: ollama pull ${model}`);
|
|
188
188
|
}
|
|
189
189
|
throw err;
|
|
190
190
|
}
|
|
@@ -240,7 +240,7 @@ async function* chat(entry, model, messages, tools, opts) {
|
|
|
240
240
|
}
|
|
241
241
|
const msg = err instanceof Error ? err.message : String(err);
|
|
242
242
|
if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
|
|
243
|
-
throw new Error(`
|
|
243
|
+
throw new Error(`I don't see a model called "${model}" on Ollama. Pull it first with: ollama pull ${model}`);
|
|
244
244
|
}
|
|
245
245
|
throw err;
|
|
246
246
|
}
|
|
@@ -278,8 +278,8 @@ var NOT_INSTALLED, NOT_RUNNING, LOCAL_HOST_RE, HARMONY_RE, CHANNEL_LABEL_RE;
|
|
|
278
278
|
var init_ollama = __esm({
|
|
279
279
|
"src/llm/ollama.ts"() {
|
|
280
280
|
"use strict";
|
|
281
|
-
NOT_INSTALLED = "Ollama
|
|
282
|
-
NOT_RUNNING = "Ollama
|
|
281
|
+
NOT_INSTALLED = "I can't find Ollama on this machine. Install it with: npm i -g ollama\nOr download it from https://ollama.com/download";
|
|
282
|
+
NOT_RUNNING = "Ollama isn't running, so I can't reach it. Start it with: ollama serve";
|
|
283
283
|
LOCAL_HOST_RE = /localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/;
|
|
284
284
|
HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
|
|
285
285
|
CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
|
|
@@ -288,7 +288,7 @@ var init_ollama = __esm({
|
|
|
288
288
|
|
|
289
289
|
// src/llm/openai.ts
|
|
290
290
|
function notAvailable(entry) {
|
|
291
|
-
return `
|
|
291
|
+
return `I couldn't reach the provider at ${entry.baseUrl}. Check that the server is running and the baseUrl is correct.`;
|
|
292
292
|
}
|
|
293
293
|
function headers(entry) {
|
|
294
294
|
const h = { "Content-Type": "application/json" };
|
|
@@ -314,7 +314,7 @@ async function listModels2(entry) {
|
|
|
314
314
|
detail = await res.text();
|
|
315
315
|
} catch {
|
|
316
316
|
}
|
|
317
|
-
throw new Error(`
|
|
317
|
+
throw new Error(`The provider came back with an error (HTTP ${res.status}): ${detail || res.statusText}`);
|
|
318
318
|
}
|
|
319
319
|
const body = await res.json();
|
|
320
320
|
return body.data.map((m) => m.id);
|
|
@@ -406,12 +406,12 @@ async function* chat2(entry, model, messages, tools, opts) {
|
|
|
406
406
|
} catch {
|
|
407
407
|
}
|
|
408
408
|
if (res.status === 404) {
|
|
409
|
-
throw new Error(`
|
|
409
|
+
throw new Error(`The provider at ${entry.baseUrl} has no model called "${model}". Double-check the name, or pick another with /models.`);
|
|
410
410
|
}
|
|
411
|
-
throw new Error(`
|
|
411
|
+
throw new Error(`The provider came back with an error (HTTP ${res.status}): ${detail || res.statusText}`);
|
|
412
412
|
}
|
|
413
413
|
const reader = res.body?.getReader();
|
|
414
|
-
if (!reader) throw new Error("
|
|
414
|
+
if (!reader) throw new Error("The provider answered but sent no body to read.");
|
|
415
415
|
const decoder = new TextDecoder();
|
|
416
416
|
let buffer = "";
|
|
417
417
|
try {
|
|
@@ -470,7 +470,7 @@ async function* chat2(entry, model, messages, tools, opts) {
|
|
|
470
470
|
throw new Error(notAvailable(entry));
|
|
471
471
|
}
|
|
472
472
|
if (timeoutSignal.aborted && !opts?.signal?.aborted) {
|
|
473
|
-
throw new Error(`
|
|
473
|
+
throw new Error(`The provider didn't respond within ${TIMEOUT_MS / 1e3}s, so I stopped waiting. The model may still be loading or thinking \u2014 give it another try.`);
|
|
474
474
|
}
|
|
475
475
|
throw err;
|
|
476
476
|
}
|
|
@@ -678,14 +678,14 @@ function isUnder(parent, child) {
|
|
|
678
678
|
}
|
|
679
679
|
function confinePath(p) {
|
|
680
680
|
if (typeof p !== "string" || p.length === 0) {
|
|
681
|
-
throw new Error("
|
|
681
|
+
throw new Error("I need a file path here, but none was given.");
|
|
682
682
|
}
|
|
683
683
|
const root = process.cwd();
|
|
684
684
|
const abs = resolve(root, p);
|
|
685
685
|
if (isUnder(root, abs) || isUnder(SPILL_DIR, abs)) {
|
|
686
686
|
return abs;
|
|
687
687
|
}
|
|
688
|
-
throw new Error(`
|
|
688
|
+
throw new Error(`"${p}" sits outside the working directory (${root}), so I can't touch it. Stay within the project folder.`);
|
|
689
689
|
}
|
|
690
690
|
var SPILL_DIR;
|
|
691
691
|
var init_paths = __esm({
|
|
@@ -787,38 +787,38 @@ function nearMiss(src, old_str) {
|
|
|
787
787
|
const width = String(to).length;
|
|
788
788
|
const ctx = srcLines.slice(from, to).map((l, i) => `${String(from + i + 1).padStart(width, " ")} ${l}`).join("\n");
|
|
789
789
|
return `
|
|
790
|
-
|
|
790
|
+
Here's the closest text I could find (lines ${from + 1}-${to}) \u2014 copy it exactly:
|
|
791
791
|
${ctx}`;
|
|
792
792
|
}
|
|
793
793
|
function locate(src, old_str) {
|
|
794
794
|
const first = src.indexOf(old_str);
|
|
795
795
|
if (first !== -1) {
|
|
796
796
|
if (src.indexOf(old_str, first + 1) !== -1) {
|
|
797
|
-
return { error: `
|
|
797
|
+
return { error: `That text shows up in more than one place, so I can't tell which one you mean. Add a line or two around it to make it unique.` };
|
|
798
798
|
}
|
|
799
799
|
return [first, first + old_str.length];
|
|
800
800
|
}
|
|
801
801
|
const fuzzy = fuzzyRange(src, old_str);
|
|
802
802
|
if (fuzzy) return fuzzy;
|
|
803
|
-
return { error: `
|
|
803
|
+
return { error: `I couldn't find that text \u2014 it may differ by whitespace or a stray character.${nearMiss(src, old_str)}` };
|
|
804
804
|
}
|
|
805
805
|
function applyBatch(src, edits) {
|
|
806
806
|
const ranges = [];
|
|
807
807
|
for (let i = 0; i < edits.length; i++) {
|
|
808
808
|
const { old_str, new_str } = edits[i];
|
|
809
809
|
if (typeof old_str !== "string" || typeof new_str !== "string") {
|
|
810
|
-
return { error: `
|
|
810
|
+
return { error: `Edit #${i + 1} is missing text \u2014 each edit needs both an old_str and a new_str string.` };
|
|
811
811
|
}
|
|
812
|
-
if (old_str === "") return { error: `
|
|
813
|
-
if (old_str === new_str) return { error: `
|
|
812
|
+
if (old_str === "") return { error: `Edit #${i + 1} has an empty old_str, so there's nothing to look for. Give it the exact text you want to replace.` };
|
|
813
|
+
if (old_str === new_str) return { error: `Edit #${i + 1} has the same old_str and new_str, so it wouldn't change anything.` };
|
|
814
814
|
const r = locate(src, old_str);
|
|
815
|
-
if (!Array.isArray(r)) return { error: `
|
|
815
|
+
if (!Array.isArray(r)) return { error: `Edit #${i + 1}: ${r.error}` };
|
|
816
816
|
ranges.push({ start: r[0], end: r[1], new_str });
|
|
817
817
|
}
|
|
818
818
|
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
819
819
|
for (let i = 1; i < sorted.length; i++) {
|
|
820
820
|
if (sorted[i].start < sorted[i - 1].end) {
|
|
821
|
-
return { error: `edits
|
|
821
|
+
return { error: `Two of these edits touch the same spot, so I can't apply them together. Split them into separate calls, or widen each old_str so they don't overlap.` };
|
|
822
822
|
}
|
|
823
823
|
}
|
|
824
824
|
let out = src;
|
|
@@ -869,11 +869,11 @@ var init_edit_file = __esm({
|
|
|
869
869
|
return { content: `Edited ${path} (${res.count} edits).${verifyHint(path)}` };
|
|
870
870
|
}
|
|
871
871
|
if (typeof old_str !== "string" || typeof new_str !== "string") {
|
|
872
|
-
return { content: `
|
|
872
|
+
return { content: `To edit ${path} I need either an old_str/new_str pair or an edits[] array \u2014 neither came through.`, is_error: true };
|
|
873
873
|
}
|
|
874
874
|
if (old_str === new_str) {
|
|
875
875
|
return {
|
|
876
|
-
content: `old_str and new_str are
|
|
876
|
+
content: `old_str and new_str are the same, so there's nothing to change in ${path}. If the file is already correct, don't edit it again \u2014 finish with the respond action and tell the user it's done.`,
|
|
877
877
|
is_error: true
|
|
878
878
|
};
|
|
879
879
|
}
|
|
@@ -890,12 +890,12 @@ var init_edit_file = __esm({
|
|
|
890
890
|
return { content: `Edited ${path} (whitespace-tolerant match).${verifyHint(path)}` };
|
|
891
891
|
}
|
|
892
892
|
}
|
|
893
|
-
return { content: `
|
|
893
|
+
return { content: `I couldn't find that text in ${path} \u2014 it may differ by whitespace or a stray character.${nearMiss(src, old_str)}`, is_error: true };
|
|
894
894
|
}
|
|
895
895
|
const all = replace_all === true;
|
|
896
896
|
if (!all && src.indexOf(old_str, first + 1) !== -1) {
|
|
897
897
|
return {
|
|
898
|
-
content: `
|
|
898
|
+
content: `That text appears more than once in ${path}, so I don't know which one to change. Add a line or two around it to single it out, or set replace_all to change them all.`,
|
|
899
899
|
is_error: true
|
|
900
900
|
};
|
|
901
901
|
}
|
|
@@ -953,7 +953,7 @@ var init_read_file = __esm({
|
|
|
953
953
|
if (IMAGE_EXT.has(ext) || looksImage(buf)) {
|
|
954
954
|
if (buf.length > MAX_IMAGE_BYTES) {
|
|
955
955
|
return {
|
|
956
|
-
content: `${path} is an image but
|
|
956
|
+
content: `${path} is an image, but at ${buf.length} bytes it's too big to attach (limit is ${MAX_IMAGE_BYTES}). Resize it and try again.`,
|
|
957
957
|
is_error: true
|
|
958
958
|
};
|
|
959
959
|
}
|
|
@@ -963,7 +963,7 @@ var init_read_file = __esm({
|
|
|
963
963
|
};
|
|
964
964
|
}
|
|
965
965
|
if (buf.subarray(0, 8e3).includes(0)) {
|
|
966
|
-
return { content: `${path} looks binary (${buf.length} bytes)
|
|
966
|
+
return { content: `${path} looks like a binary file (${buf.length} bytes), so I'm not reading it as text.`, is_error: true };
|
|
967
967
|
}
|
|
968
968
|
const raw = buf.toString("utf-8").replace(/\r\n/g, "\n");
|
|
969
969
|
const allLines = raw.split("\n");
|
|
@@ -975,7 +975,7 @@ var init_read_file = __esm({
|
|
|
975
975
|
let body = numbered(slice, start);
|
|
976
976
|
if (body.length > MAX_CHARS) {
|
|
977
977
|
body = body.slice(0, MAX_CHARS) + `
|
|
978
|
-
[
|
|
978
|
+
[There's more \u2014 this hit the ${MAX_CHARS}-char limit. Use offset/limit to read the rest.]`;
|
|
979
979
|
}
|
|
980
980
|
if (ranged) {
|
|
981
981
|
const end = start - 1 + slice.length;
|
|
@@ -1047,7 +1047,7 @@ function spillIfLarge(full, label = "output", budget = INLINE_BUDGET) {
|
|
|
1047
1047
|
const head = Math.floor(budget * HEAD_FRACTION);
|
|
1048
1048
|
const tail = budget - head;
|
|
1049
1049
|
const preview = full.slice(0, head) + "\n\u2026\n" + full.slice(-tail);
|
|
1050
|
-
const notice = path ? `[${label}
|
|
1050
|
+
const notice = path ? `[This ${label} was long (${full.length} bytes), so I'm showing the start and end. The full text is saved at ${path} \u2014 read it with read_file offset/limit to see the middle.]` : `[This ${label} was too long to show in full, and I couldn't save the rest to disk, so the middle is missing.]`;
|
|
1051
1051
|
return `${preview}
|
|
1052
1052
|
${notice}`;
|
|
1053
1053
|
}
|
|
@@ -1130,8 +1130,8 @@ var init_run_bash = __esm({
|
|
|
1130
1130
|
const out = all ?? "";
|
|
1131
1131
|
const is_error = aborted || timedOut || exitCode !== 0;
|
|
1132
1132
|
const note = timedOut ? `
|
|
1133
|
-
[
|
|
1134
|
-
[
|
|
1133
|
+
[Timed out after ${timeout}ms, so I stopped the command and everything it started. Raise timeout_ms for long builds or test suites.]` : aborted ? `
|
|
1134
|
+
[Cancelled \u2014 I stopped the command and everything it started.]` : "";
|
|
1135
1135
|
const body = out || (is_error ? `(no output)` : "");
|
|
1136
1136
|
const content = `${spillIfLarge(body, "command output")}
|
|
1137
1137
|
[exit ${exitCode ?? "killed"}]${note}`;
|
|
@@ -1240,8 +1240,9 @@ var init_grep = __esm({
|
|
|
1240
1240
|
const lines = (res.stdout ?? "").split("\n").slice(0, limit);
|
|
1241
1241
|
const out = lines.join("\n");
|
|
1242
1242
|
const code = res.exitCode ?? 0;
|
|
1243
|
-
|
|
1244
|
-
|
|
1243
|
+
const noMatch = `Nothing matched "${pattern}". Try a looser pattern, case_insensitive, or a wider path.`;
|
|
1244
|
+
if (!out && code === 1) return { content: noMatch };
|
|
1245
|
+
return { content: out || res.stderr || noMatch, is_error: code > 1 };
|
|
1245
1246
|
} catch (err) {
|
|
1246
1247
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
1247
1248
|
}
|
|
@@ -1316,7 +1317,7 @@ var init_glob = __esm({
|
|
|
1316
1317
|
res = await tryFind();
|
|
1317
1318
|
}
|
|
1318
1319
|
const all = (res.stdout ?? "").split("\n").filter(Boolean);
|
|
1319
|
-
if (all.length === 0) return { content:
|
|
1320
|
+
if (all.length === 0) return { content: `Nothing matched "${pattern}". Try a broader pattern or a different path.` };
|
|
1320
1321
|
const lines = byMtimeDesc(all).slice(0, limit);
|
|
1321
1322
|
return { content: lines.join("\n") };
|
|
1322
1323
|
} catch (err) {
|
|
@@ -1327,6 +1328,70 @@ var init_glob = __esm({
|
|
|
1327
1328
|
}
|
|
1328
1329
|
});
|
|
1329
1330
|
|
|
1331
|
+
// src/tools/write_todos.ts
|
|
1332
|
+
function render(todos) {
|
|
1333
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
1334
|
+
const header = `Todos (${done}/${todos.length} done):`;
|
|
1335
|
+
const lines = todos.map((t) => `${MARK[t.status]} ${t.content}`);
|
|
1336
|
+
return [header, ...lines].join("\n");
|
|
1337
|
+
}
|
|
1338
|
+
var STATUSES, MARK, write_todos;
|
|
1339
|
+
var init_write_todos = __esm({
|
|
1340
|
+
"src/tools/write_todos.ts"() {
|
|
1341
|
+
"use strict";
|
|
1342
|
+
STATUSES = ["pending", "in_progress", "completed"];
|
|
1343
|
+
MARK = {
|
|
1344
|
+
completed: "[x]",
|
|
1345
|
+
in_progress: "[~]",
|
|
1346
|
+
pending: "[ ]"
|
|
1347
|
+
};
|
|
1348
|
+
write_todos = {
|
|
1349
|
+
name: "write_todos",
|
|
1350
|
+
description: "Create and maintain a task list for the current session, shown to the user as a live checklist. Pass the FULL list every call \u2014 it replaces the previous one (all or nothing), so include finished items too. Each todo is {content, status} where status is pending | in_progress | completed. Keep exactly one item in_progress at a time and mark it completed before starting the next. Use for multi-step work (features, refactors, multi-file changes) so the user can see what is done, in progress, and left. Skip it for single trivial actions.",
|
|
1351
|
+
input_schema: {
|
|
1352
|
+
type: "object",
|
|
1353
|
+
properties: {
|
|
1354
|
+
todos: {
|
|
1355
|
+
type: "array",
|
|
1356
|
+
description: "The full, ordered task list. Replaces the prior list entirely.",
|
|
1357
|
+
items: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
properties: {
|
|
1360
|
+
content: { type: "string", description: 'Short task description (imperative, e.g. "Add webfetch tool").' },
|
|
1361
|
+
status: { type: "string", enum: STATUSES, description: "pending | in_progress | completed" }
|
|
1362
|
+
},
|
|
1363
|
+
required: ["content", "status"]
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
required: ["todos"]
|
|
1368
|
+
},
|
|
1369
|
+
handler: ({ todos }) => {
|
|
1370
|
+
if (!Array.isArray(todos)) {
|
|
1371
|
+
return { content: "I need a todos array here \u2014 pass the full list of tasks.", is_error: true };
|
|
1372
|
+
}
|
|
1373
|
+
if (todos.length === 0) {
|
|
1374
|
+
return { content: "The todo list is empty \u2014 give me at least one task.", is_error: true };
|
|
1375
|
+
}
|
|
1376
|
+
for (let i = 0; i < todos.length; i++) {
|
|
1377
|
+
const t = todos[i];
|
|
1378
|
+
if (!t || typeof t.content !== "string" || t.content.trim() === "") {
|
|
1379
|
+
return { content: `Task #${i + 1} needs some text \u2014 its content can't be empty.`, is_error: true };
|
|
1380
|
+
}
|
|
1381
|
+
if (!STATUSES.includes(t.status)) {
|
|
1382
|
+
return { content: `Task #${i + 1} has an unknown status. Use one of: ${STATUSES.join(", ")}.`, is_error: true };
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const active2 = todos.filter((t) => t.status === "in_progress").length;
|
|
1386
|
+
if (active2 > 1) {
|
|
1387
|
+
return { content: `Keep just one task in progress at a time \u2014 I see ${active2}. Finish one before starting the next.`, is_error: true };
|
|
1388
|
+
}
|
|
1389
|
+
return { content: render(todos) };
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1330
1395
|
// src/tools/registry.ts
|
|
1331
1396
|
function getTool(name) {
|
|
1332
1397
|
return TOOLS.find((t) => t.name === name);
|
|
@@ -1355,13 +1420,15 @@ var init_registry = __esm({
|
|
|
1355
1420
|
init_run_bash();
|
|
1356
1421
|
init_grep();
|
|
1357
1422
|
init_glob();
|
|
1423
|
+
init_write_todos();
|
|
1358
1424
|
TOOLS = [
|
|
1359
1425
|
edit_file,
|
|
1360
1426
|
read_file,
|
|
1361
1427
|
write_file,
|
|
1362
1428
|
run_bash,
|
|
1363
1429
|
grep,
|
|
1364
|
-
glob
|
|
1430
|
+
glob,
|
|
1431
|
+
write_todos
|
|
1365
1432
|
];
|
|
1366
1433
|
}
|
|
1367
1434
|
});
|
|
@@ -1424,7 +1491,7 @@ function validateInput(schema, input) {
|
|
|
1424
1491
|
const result = toZod(schema).safeParse(input ?? {});
|
|
1425
1492
|
if (result.success) return null;
|
|
1426
1493
|
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
1427
|
-
return `
|
|
1494
|
+
return `Some arguments didn't look right: ${issues}`;
|
|
1428
1495
|
}
|
|
1429
1496
|
var init_validate = __esm({
|
|
1430
1497
|
"src/tools/validate.ts"() {
|
|
@@ -1563,6 +1630,14 @@ Ask in a numbered list. One round of questions per turn. Then wait.
|
|
|
1563
1630
|
You have access to the following tools. Call them via the function-calling interface.
|
|
1564
1631
|
${toolLines}
|
|
1565
1632
|
|
|
1633
|
+
# Task list (write_todos)
|
|
1634
|
+
For multi-step work \u2014 a feature, a refactor, anything touching multiple files or more than ~3 steps \u2014 track it with write_todos so the user sees a live checklist of what is done, in progress, and left.
|
|
1635
|
+
- Create the list right after your plan, before the first real action. Each item is {content, status}: short imperative content, status pending | in_progress | completed.
|
|
1636
|
+
- Pass the FULL list every call; it replaces the previous one. Include already-finished items so nothing disappears.
|
|
1637
|
+
- Mark exactly ONE item in_progress at a time. Set it in_progress when you start it, completed the moment it is done, then move the next to in_progress. Never leave a finished task pending or two tasks in_progress.
|
|
1638
|
+
- Keep items outcome-sized, not tool-sized \u2014 "Add webfetch tool and wire it in", not "call edit_file". Aim for a handful, not twenty.
|
|
1639
|
+
- Do NOT use write_todos for trivial single-step work \u2014 a lone read, one edit, a quick search. It is overhead there.
|
|
1640
|
+
|
|
1566
1641
|
# Loop semantics
|
|
1567
1642
|
- When you need to act on the filesystem or run a command, emit a tool call.
|
|
1568
1643
|
- After each tool result, decide: more tool calls, or a final plain-text answer.
|
|
@@ -1858,7 +1933,7 @@ function readGuard(name, input, seen) {
|
|
|
1858
1933
|
if (seen.has(abs)) return null;
|
|
1859
1934
|
if (name === "write_file" && !existsSync5(abs)) return null;
|
|
1860
1935
|
const verb = name === "edit_file" ? "edit" : "overwrite";
|
|
1861
|
-
return `
|
|
1936
|
+
return `I won't ${verb} ${p} without seeing it first \u2014 I don't want to clobber something. Read it with read_file, then retry the ${name}.`;
|
|
1862
1937
|
}
|
|
1863
1938
|
function unwrapEnvelope(name, input) {
|
|
1864
1939
|
if (!("arguments" in input)) return input;
|
|
@@ -2040,7 +2115,7 @@ async function* runAgent(opts) {
|
|
|
2040
2115
|
const r2 = {
|
|
2041
2116
|
type: "tool_result",
|
|
2042
2117
|
tool_use_id: use.id,
|
|
2043
|
-
content: `Unknown tool: ${use.name}
|
|
2118
|
+
content: `Unknown tool: ${use.name}. There's no tool by that name \u2014 check the spelling against the tools you were given.`,
|
|
2044
2119
|
is_error: true
|
|
2045
2120
|
};
|
|
2046
2121
|
results.push(r2);
|
|
@@ -2066,7 +2141,7 @@ async function* runAgent(opts) {
|
|
|
2066
2141
|
const r2 = {
|
|
2067
2142
|
type: "tool_result",
|
|
2068
2143
|
tool_use_id: use.id,
|
|
2069
|
-
content: `Permission denied
|
|
2144
|
+
content: `Permission denied \u2014 the user chose not to run ${use.name}. Try a different approach, or ask them what they'd prefer.`,
|
|
2070
2145
|
is_error: true
|
|
2071
2146
|
};
|
|
2072
2147
|
results.push(r2);
|
|
@@ -2368,7 +2443,7 @@ var init_run = __esm({
|
|
|
2368
2443
|
});
|
|
2369
2444
|
|
|
2370
2445
|
// src/cli.tsx
|
|
2371
|
-
import { render } from "ink";
|
|
2446
|
+
import { render as render2 } from "ink";
|
|
2372
2447
|
import { createElement } from "react";
|
|
2373
2448
|
|
|
2374
2449
|
// src/ui/App.tsx
|
|
@@ -3658,7 +3733,8 @@ var TOOL_LABEL = {
|
|
|
3658
3733
|
read_file: "Read",
|
|
3659
3734
|
run_bash: "Bash",
|
|
3660
3735
|
glob: "Glob",
|
|
3661
|
-
grep: "Grep"
|
|
3736
|
+
grep: "Grep",
|
|
3737
|
+
write_todos: "Todos"
|
|
3662
3738
|
};
|
|
3663
3739
|
var EXT_LANG = {
|
|
3664
3740
|
ts: "typescript",
|
|
@@ -3760,6 +3836,43 @@ function FileEditBlock({
|
|
|
3760
3836
|
] }) })
|
|
3761
3837
|
] });
|
|
3762
3838
|
}
|
|
3839
|
+
function TodoBlock({ todos }) {
|
|
3840
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
3841
|
+
const doing = todos.filter((t) => t.status === "in_progress").length;
|
|
3842
|
+
const glyph = { completed: "\u2714", in_progress: "\u25B6", pending: "\u25CB" };
|
|
3843
|
+
const color = { completed: "green", in_progress: "yellow", pending: "gray" };
|
|
3844
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
3845
|
+
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
3846
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
3847
|
+
/* @__PURE__ */ jsx9(Text9, { color: "white", children: "Todos " }),
|
|
3848
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
3849
|
+
"(",
|
|
3850
|
+
done,
|
|
3851
|
+
"/",
|
|
3852
|
+
todos.length,
|
|
3853
|
+
" done",
|
|
3854
|
+
doing > 0 ? `, ${doing} in progress` : "",
|
|
3855
|
+
")"
|
|
3856
|
+
] })
|
|
3857
|
+
] }),
|
|
3858
|
+
todos.map((t, i) => /* @__PURE__ */ jsxs9(Box9, { marginLeft: 4, children: [
|
|
3859
|
+
/* @__PURE__ */ jsxs9(Text9, { color: color[t.status], children: [
|
|
3860
|
+
glyph[t.status],
|
|
3861
|
+
" "
|
|
3862
|
+
] }),
|
|
3863
|
+
/* @__PURE__ */ jsx9(
|
|
3864
|
+
Text9,
|
|
3865
|
+
{
|
|
3866
|
+
color: t.status === "in_progress" ? "yellow" : void 0,
|
|
3867
|
+
dimColor: t.status !== "in_progress",
|
|
3868
|
+
strikethrough: t.status === "completed",
|
|
3869
|
+
bold: t.status === "in_progress",
|
|
3870
|
+
children: t.content
|
|
3871
|
+
}
|
|
3872
|
+
)
|
|
3873
|
+
] }, i))
|
|
3874
|
+
] });
|
|
3875
|
+
}
|
|
3763
3876
|
function toolHeader(use) {
|
|
3764
3877
|
const label = TOOL_LABEL[use.name] ?? use.name;
|
|
3765
3878
|
const input = use.input ?? {};
|
|
@@ -3839,6 +3952,10 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
3839
3952
|
] });
|
|
3840
3953
|
}
|
|
3841
3954
|
function ToolUseLine({ use, result }) {
|
|
3955
|
+
if (use.name === "write_todos" && !result?.is_error) {
|
|
3956
|
+
const todos = use.input.todos;
|
|
3957
|
+
if (Array.isArray(todos) && todos.length > 0) return /* @__PURE__ */ jsx9(TodoBlock, { todos });
|
|
3958
|
+
}
|
|
3842
3959
|
if (use.name === "write_file" && !result?.is_error) {
|
|
3843
3960
|
const input = use.input;
|
|
3844
3961
|
const content = input.content ?? "";
|
|
@@ -5196,5 +5313,5 @@ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
|
|
|
5196
5313
|
process.on("exit", () => {
|
|
5197
5314
|
if (process.stdout.isTTY) process.stdout.write("\x1B]2;\x07");
|
|
5198
5315
|
});
|
|
5199
|
-
|
|
5316
|
+
render2(createElement(App));
|
|
5200
5317
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "Local AI coding agent for your terminal — an open-source, offline alternative to Claude Code, Cursor, and GitHub Copilot, powered by Ollama. Private by default, free forever.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|