miii-agent 0.1.31 → 0.1.32
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 +151 -33
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -790,6 +790,43 @@ function nearMiss(src, old_str) {
|
|
|
790
790
|
Closest text in file (lines ${from + 1}-${to}):
|
|
791
791
|
${ctx}`;
|
|
792
792
|
}
|
|
793
|
+
function locate(src, old_str) {
|
|
794
|
+
const first = src.indexOf(old_str);
|
|
795
|
+
if (first !== -1) {
|
|
796
|
+
if (src.indexOf(old_str, first + 1) !== -1) {
|
|
797
|
+
return { error: `old_str not unique \u2014 add surrounding context to disambiguate.` };
|
|
798
|
+
}
|
|
799
|
+
return [first, first + old_str.length];
|
|
800
|
+
}
|
|
801
|
+
const fuzzy = fuzzyRange(src, old_str);
|
|
802
|
+
if (fuzzy) return fuzzy;
|
|
803
|
+
return { error: `old_str not found.${nearMiss(src, old_str)}` };
|
|
804
|
+
}
|
|
805
|
+
function applyBatch(src, edits) {
|
|
806
|
+
const ranges = [];
|
|
807
|
+
for (let i = 0; i < edits.length; i++) {
|
|
808
|
+
const { old_str, new_str } = edits[i];
|
|
809
|
+
if (typeof old_str !== "string" || typeof new_str !== "string") {
|
|
810
|
+
return { error: `edits[${i}] must have string old_str and new_str.` };
|
|
811
|
+
}
|
|
812
|
+
if (old_str === "") return { error: `edits[${i}].old_str is empty.` };
|
|
813
|
+
if (old_str === new_str) return { error: `edits[${i}] old_str and new_str are identical \u2014 nothing to change.` };
|
|
814
|
+
const r = locate(src, old_str);
|
|
815
|
+
if (!Array.isArray(r)) return { error: `edits[${i}]: ${r.error}` };
|
|
816
|
+
ranges.push({ start: r[0], end: r[1], new_str });
|
|
817
|
+
}
|
|
818
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
819
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
820
|
+
if (sorted[i].start < sorted[i - 1].end) {
|
|
821
|
+
return { error: `edits overlap in the file \u2014 split them into separate calls or widen the context.` };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
let out = src;
|
|
825
|
+
for (const r of [...ranges].sort((a, b) => b.start - a.start)) {
|
|
826
|
+
out = out.slice(0, r.start) + r.new_str + out.slice(r.end);
|
|
827
|
+
}
|
|
828
|
+
return { out, count: ranges.length };
|
|
829
|
+
}
|
|
793
830
|
var edit_file;
|
|
794
831
|
var init_edit_file = __esm({
|
|
795
832
|
"src/tools/edit_file.ts"() {
|
|
@@ -798,19 +835,42 @@ var init_edit_file = __esm({
|
|
|
798
835
|
init_verifyHint();
|
|
799
836
|
edit_file = {
|
|
800
837
|
name: "edit_file",
|
|
801
|
-
description: "Replace an exact string in a file. old_str must be unique unless replace_all is set. On no match, returns the closest text in the file.",
|
|
838
|
+
description: "Replace an exact string in a file. old_str must be unique unless replace_all is set. On no match, returns the closest text in the file. To make several edits to one file at once, pass an `edits` array of {old_str,new_str} \u2014 they apply atomically (all or nothing).",
|
|
802
839
|
input_schema: {
|
|
803
840
|
type: "object",
|
|
804
841
|
properties: {
|
|
805
842
|
path: { type: "string", description: "File path" },
|
|
806
|
-
old_str: { type: "string", description: "Exact text to replace (whitespace-sensitive)" },
|
|
807
|
-
new_str: { type: "string", description: "Replacement text" },
|
|
808
|
-
replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring uniqueness" }
|
|
843
|
+
old_str: { type: "string", description: "Exact text to replace (whitespace-sensitive). Omit when using edits[]." },
|
|
844
|
+
new_str: { type: "string", description: "Replacement text. Omit when using edits[]." },
|
|
845
|
+
replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring uniqueness" },
|
|
846
|
+
edits: {
|
|
847
|
+
type: "array",
|
|
848
|
+
description: "Batch mode: several edits applied atomically. Each old_str must be unique in the file. Alternative to old_str/new_str.",
|
|
849
|
+
items: {
|
|
850
|
+
type: "object",
|
|
851
|
+
properties: {
|
|
852
|
+
old_str: { type: "string", description: "Exact text to replace (whitespace-sensitive)" },
|
|
853
|
+
new_str: { type: "string", description: "Replacement text" }
|
|
854
|
+
},
|
|
855
|
+
required: ["old_str", "new_str"]
|
|
856
|
+
}
|
|
857
|
+
}
|
|
809
858
|
},
|
|
810
|
-
required: ["path"
|
|
859
|
+
required: ["path"]
|
|
811
860
|
},
|
|
812
|
-
handler: ({ path, old_str, new_str, replace_all }) => {
|
|
861
|
+
handler: ({ path, old_str, new_str, replace_all, edits }) => {
|
|
813
862
|
try {
|
|
863
|
+
if (Array.isArray(edits) && edits.length > 0) {
|
|
864
|
+
const abs2 = confinePath(path);
|
|
865
|
+
const src2 = readFileSync4(abs2, "utf-8");
|
|
866
|
+
const res = applyBatch(src2, edits);
|
|
867
|
+
if ("error" in res) return { content: `${res.error} (in ${path})`, is_error: true };
|
|
868
|
+
writeFileSync4(abs2, res.out, "utf-8");
|
|
869
|
+
return { content: `Edited ${path} (${res.count} edits).${verifyHint(path)}` };
|
|
870
|
+
}
|
|
871
|
+
if (typeof old_str !== "string" || typeof new_str !== "string") {
|
|
872
|
+
return { content: `edit_file needs old_str and new_str (or an edits[] array) for ${path}.`, is_error: true };
|
|
873
|
+
}
|
|
814
874
|
if (old_str === new_str) {
|
|
815
875
|
return {
|
|
816
876
|
content: `old_str and new_str are identical \u2014 nothing to change in ${path}. If the file is already correct, do NOT edit again: finish with the respond action and tell the user it is done.`,
|
|
@@ -857,11 +917,22 @@ function numbered(lines, start) {
|
|
|
857
917
|
const width = String(start + lines.length - 1).length;
|
|
858
918
|
return lines.map((l, i) => `${String(start + i).padStart(width, " ")} ${l}`).join("\n");
|
|
859
919
|
}
|
|
860
|
-
|
|
920
|
+
function looksImage(buf) {
|
|
921
|
+
if (buf.length < 4) return false;
|
|
922
|
+
if (buf[0] === 137 && buf[1] === 80) return true;
|
|
923
|
+
if (buf[0] === 255 && buf[1] === 216) return true;
|
|
924
|
+
if (buf[0] === 71 && buf[1] === 73) return true;
|
|
925
|
+
if (buf[0] === 66 && buf[1] === 77) return true;
|
|
926
|
+
if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP") return true;
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
var IMAGE_EXT, MAX_IMAGE_BYTES, read_file;
|
|
861
930
|
var init_read_file = __esm({
|
|
862
931
|
"src/tools/read_file.ts"() {
|
|
863
932
|
"use strict";
|
|
864
933
|
init_paths();
|
|
934
|
+
IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp"]);
|
|
935
|
+
MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
865
936
|
read_file = {
|
|
866
937
|
name: "read_file",
|
|
867
938
|
description: "Read file contents as UTF-8 text with line numbers. Use offset/limit to read a range of a large file instead of the whole thing.",
|
|
@@ -878,6 +949,19 @@ var init_read_file = __esm({
|
|
|
878
949
|
try {
|
|
879
950
|
const MAX_CHARS = 2e5;
|
|
880
951
|
const buf = readFileSync5(confinePath(path));
|
|
952
|
+
const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
|
|
953
|
+
if (IMAGE_EXT.has(ext) || looksImage(buf)) {
|
|
954
|
+
if (buf.length > MAX_IMAGE_BYTES) {
|
|
955
|
+
return {
|
|
956
|
+
content: `${path} is an image but too large to attach (${buf.length} bytes > ${MAX_IMAGE_BYTES}). Resize it first.`,
|
|
957
|
+
is_error: true
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
content: `[image ${path} \u2014 ${buf.length} bytes, attached for viewing]`,
|
|
962
|
+
images: [buf.toString("base64")]
|
|
963
|
+
};
|
|
964
|
+
}
|
|
881
965
|
if (buf.subarray(0, 8e3).includes(0)) {
|
|
882
966
|
return { content: `${path} looks binary (${buf.length} bytes); not reading as text.`, is_error: true };
|
|
883
967
|
}
|
|
@@ -992,6 +1076,17 @@ var init_spill = __esm({
|
|
|
992
1076
|
|
|
993
1077
|
// src/tools/run_bash.ts
|
|
994
1078
|
import { execa } from "execa";
|
|
1079
|
+
function killTree(pid, isWin) {
|
|
1080
|
+
if (!pid) return;
|
|
1081
|
+
try {
|
|
1082
|
+
if (isWin) {
|
|
1083
|
+
execa("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
1084
|
+
} else {
|
|
1085
|
+
process.kill(-pid, "SIGKILL");
|
|
1086
|
+
}
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
995
1090
|
var run_bash;
|
|
996
1091
|
var init_run_bash = __esm({
|
|
997
1092
|
"src/tools/run_bash.ts"() {
|
|
@@ -1008,27 +1103,44 @@ var init_run_bash = __esm({
|
|
|
1008
1103
|
},
|
|
1009
1104
|
required: ["command"]
|
|
1010
1105
|
},
|
|
1011
|
-
handler: async ({ command, timeout_ms }) => {
|
|
1106
|
+
handler: async ({ command, timeout_ms }, ctx) => {
|
|
1107
|
+
const isWin = process.platform === "win32";
|
|
1108
|
+
const shell = isWin ? "cmd" : "bash";
|
|
1109
|
+
const shellArgs = isWin ? ["/c", command] : ["-c", command];
|
|
1110
|
+
const timeout = timeout_ms ?? 12e4;
|
|
1111
|
+
const child = execa(shell, shellArgs, {
|
|
1112
|
+
reject: false,
|
|
1113
|
+
all: true,
|
|
1114
|
+
detached: !isWin
|
|
1115
|
+
// POSIX: new process group so killTree(-pid) hits the whole tree
|
|
1116
|
+
});
|
|
1117
|
+
let timedOut = false;
|
|
1118
|
+
let aborted = false;
|
|
1119
|
+
const timer = setTimeout(() => {
|
|
1120
|
+
timedOut = true;
|
|
1121
|
+
killTree(child.pid, isWin);
|
|
1122
|
+
}, timeout);
|
|
1123
|
+
const onAbort = () => {
|
|
1124
|
+
aborted = true;
|
|
1125
|
+
killTree(child.pid, isWin);
|
|
1126
|
+
};
|
|
1127
|
+
ctx?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1012
1128
|
try {
|
|
1013
|
-
const
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
all: false
|
|
1020
|
-
});
|
|
1021
|
-
const out = [stdout, stderr].filter(Boolean).join("\n");
|
|
1022
|
-
const is_error = exitCode !== 0;
|
|
1129
|
+
const { all, exitCode } = await child;
|
|
1130
|
+
const out = all ?? "";
|
|
1131
|
+
const is_error = aborted || timedOut || exitCode !== 0;
|
|
1132
|
+
const note = timedOut ? `
|
|
1133
|
+
[timed out after ${timeout}ms \u2014 process tree killed]` : aborted ? `
|
|
1134
|
+
[aborted \u2014 process tree killed]` : "";
|
|
1023
1135
|
const body = out || (is_error ? `(no output)` : "");
|
|
1024
1136
|
const content = `${spillIfLarge(body, "command output")}
|
|
1025
|
-
[exit ${exitCode}]`;
|
|
1026
|
-
return {
|
|
1027
|
-
content,
|
|
1028
|
-
is_error
|
|
1029
|
-
};
|
|
1137
|
+
[exit ${exitCode ?? "killed"}]${note}`;
|
|
1138
|
+
return { content, is_error };
|
|
1030
1139
|
} catch (err) {
|
|
1031
1140
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
1141
|
+
} finally {
|
|
1142
|
+
clearTimeout(timer);
|
|
1143
|
+
ctx?.signal?.removeEventListener("abort", onAbort);
|
|
1032
1144
|
}
|
|
1033
1145
|
}
|
|
1034
1146
|
};
|
|
@@ -1535,6 +1647,9 @@ function toOllamaMessages(history, system) {
|
|
|
1535
1647
|
const texts = msg.content.filter((b) => b.type === "text");
|
|
1536
1648
|
for (const tr of tool_results) {
|
|
1537
1649
|
out.push({ role: "tool", content: tr.content, tool_call_id: tr.tool_use_id });
|
|
1650
|
+
if (tr.images && tr.images.length > 0) {
|
|
1651
|
+
out.push({ role: "user", content: "Image content from the previous tool result:", images: tr.images });
|
|
1652
|
+
}
|
|
1538
1653
|
}
|
|
1539
1654
|
if (texts.length > 0) {
|
|
1540
1655
|
out.push({ role: "user", content: texts.map((t) => t.text).join("") });
|
|
@@ -1977,12 +2092,13 @@ async function* runAgent(opts) {
|
|
|
1977
2092
|
}
|
|
1978
2093
|
let r;
|
|
1979
2094
|
try {
|
|
1980
|
-
const out = await tool.handler(use.input);
|
|
2095
|
+
const out = await tool.handler(use.input, { signal });
|
|
1981
2096
|
r = {
|
|
1982
2097
|
type: "tool_result",
|
|
1983
2098
|
tool_use_id: use.id,
|
|
1984
2099
|
content: out.content,
|
|
1985
|
-
is_error: out.is_error
|
|
2100
|
+
is_error: out.is_error,
|
|
2101
|
+
...out.images && out.images.length > 0 ? { images: out.images } : {}
|
|
1986
2102
|
};
|
|
1987
2103
|
} catch (err) {
|
|
1988
2104
|
r = {
|
|
@@ -3732,14 +3848,16 @@ function ToolUseLine({ use, result }) {
|
|
|
3732
3848
|
}
|
|
3733
3849
|
if (use.name === "edit_file" && !result?.is_error) {
|
|
3734
3850
|
const input = use.input;
|
|
3735
|
-
const
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
const
|
|
3739
|
-
const
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3851
|
+
const pairs = Array.isArray(input.edits) && input.edits.length > 0 ? input.edits.map((e) => ({ oldS: e.old_str ?? "", newS: e.new_str ?? "" })) : [{ oldS: input.old_str ?? "", newS: input.new_str ?? "" }];
|
|
3852
|
+
let added = 0;
|
|
3853
|
+
let removed = 0;
|
|
3854
|
+
const preview = [];
|
|
3855
|
+
for (const { oldS, newS } of pairs) {
|
|
3856
|
+
added += countLines(newS);
|
|
3857
|
+
removed += countLines(oldS);
|
|
3858
|
+
preview.push(...oldS.split("\n").map((t) => ({ sign: "-", text: t })));
|
|
3859
|
+
preview.push(...newS.split("\n").map((t) => ({ sign: "+", text: t })));
|
|
3860
|
+
}
|
|
3743
3861
|
return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Update", path: input.path ?? "", added, removed, previewLines: preview });
|
|
3744
3862
|
}
|
|
3745
3863
|
const { label, arg } = toolHeader(use);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
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": {
|