miii-agent 0.1.30 โ 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/README.md +58 -11
- package/dist/cli.js +240 -56
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<h1 align="center">miii</h1>
|
|
1
|
+
<h1 align="center">miii โ Local AI Coding Agent for Your Terminal</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>
|
|
5
|
-
|
|
6
|
-
Private by default. Free forever.
|
|
4
|
+
<strong>An open-source, offline alternative to Claude Code, Cursor, and GitHub Copilot.</strong><br>
|
|
5
|
+
A private AI pair-programmer in your terminal, powered by Ollama and any local LLM.<br>
|
|
6
|
+
Private by default. Free forever. Works offline.
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -15,26 +15,41 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<img src="demo3.gif" alt="miii demo">
|
|
18
|
+
<img src="demo3.gif" alt="miii local AI coding agent terminal demo powered by Ollama">
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
๐ <strong>100% local</strong> โ your code never leaves your machine ยท
|
|
23
|
+
๐ธ <strong>Free</strong> โ no API keys, no per-token billing ยท
|
|
24
|
+
โก <strong>Offline</strong> โ runs on your own GPU
|
|
19
25
|
</p>
|
|
20
26
|
|
|
21
27
|
---
|
|
22
28
|
|
|
23
|
-
## What is
|
|
29
|
+
## What is miii? โ a local AI coding agent
|
|
24
30
|
|
|
25
31
|
miii lives in your terminal and codes alongside you โ reading files, writing features, running tests, fixing bugs. The twist: it runs on **your** hardware, powered by [Ollama](https://ollama.com) (or any local OpenAI-compatible server like [llama.cpp](https://github.com/ggml-org/llama.cpp) / [LM Studio](https://lmstudio.ai)).
|
|
26
32
|
|
|
27
|
-
Your code never leaves your disk. There's nothing to log in to. Pull a model, type `miii`, go.
|
|
33
|
+
Your code never leaves your disk. There's nothing to log in to. Pull a model, type `miii`, go. It's the open-source, offline answer to cloud coding assistants like Claude Code, Cursor, and GitHub Copilot.
|
|
28
34
|
|
|
29
|
-
##
|
|
35
|
+
## Install (macOS, Linux, Windows)
|
|
30
36
|
|
|
31
37
|
**macOS / Linux:**
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
40
|
ollama pull qwen2.5-coder:14b # any coding model works
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Which model should I use?**
|
|
44
|
+
- **Low VRAM (8GB):** `qwen2.5-coder:7b` (Fast, capable)
|
|
45
|
+
- **Mid VRAM (16-24GB):** `qwen2.5-coder:14b` (Sweet spot)
|
|
46
|
+
- **High VRAM (48GB+):** `qwen2.5-coder:32b` (Powerhouse)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
35
49
|
curl -fsSL https://raw.githubusercontent.com/maruakshay/miii-cli/main/install.sh | sh
|
|
36
50
|
miii
|
|
37
51
|
```
|
|
52
|
+
*(The installer downloads the pre-compiled binary and adds it to your local path)*
|
|
38
53
|
|
|
39
54
|
**Windows (PowerShell):**
|
|
40
55
|
|
|
@@ -77,7 +92,7 @@ miii --version # what you're running
|
|
|
77
92
|
Opt out of background updates by adding `"autoUpdate": false` to `~/.miii/config.json`,
|
|
78
93
|
or re-run the install script (`curl โฆ | sh`) any time to update by hand.
|
|
79
94
|
|
|
80
|
-
## Why local-first?
|
|
95
|
+
## Why local-first? Private, free, offline
|
|
81
96
|
|
|
82
97
|
Most "AI coding tools" are just wrappers around a cloud API โ slow, metered, and they ship your private codebase to someone else's server.
|
|
83
98
|
|
|
@@ -89,13 +104,16 @@ Most "AI coding tools" are just wrappers around a cloud API โ slow, metered, a
|
|
|
89
104
|
| Offline | No | Yes |
|
|
90
105
|
| Latency | Network + queue | Your GPU only |
|
|
91
106
|
|
|
92
|
-
It doesn't just chat, either โ it
|
|
107
|
+
It doesn't just chat, either โ it follows a **Plan $\rightarrow$ Act $\rightarrow$ Observe** loop:
|
|
108
|
+
1. **Plan**: Decomposes the problem into a sequence of concrete steps.
|
|
109
|
+
2. **Act**: Calls the necessary tools to gather context or modify code.
|
|
110
|
+
3. **Observe**: Verifies the result and adjusts the plan until the goal is met.
|
|
93
111
|
|
|
94
112
|
## Five letters, five ideas
|
|
95
113
|
|
|
96
114
|
**s**mall ยท **s**imple ยท **s**mart ยท **s**trategic ยท **s**emantic โ a tiny codebase you can read in an afternoon, no config ceremony, plans before it acts, and operates on the *meaning* of your code, not blind text matching.
|
|
97
115
|
|
|
98
|
-
##
|
|
116
|
+
## Features
|
|
99
117
|
|
|
100
118
|
- **๐งช `miii doctor`** โ not every local model can drive an agent. Doctor runs your models through real engineering tasks and tells you which ones actually deliver.
|
|
101
119
|
```bash
|
|
@@ -189,6 +207,15 @@ The model pages through the middle with ranged `read_file` reads. Spill files ar
|
|
|
189
207
|
<details>
|
|
190
208
|
<summary><strong>Development</strong></summary>
|
|
191
209
|
|
|
210
|
+
**Project Architecture:**
|
|
211
|
+
```text
|
|
212
|
+
src/
|
|
213
|
+
โโโ agent/ # The core reasoning loop
|
|
214
|
+
โโโ tools/ # Implementation of read/write/bash
|
|
215
|
+
โโโ terminal/ # UI and input handling
|
|
216
|
+
โโโ config/ # Settings and provider logic
|
|
217
|
+
```
|
|
218
|
+
|
|
192
219
|
```bash
|
|
193
220
|
git clone https://github.com/maruakshay/miii-cli.git
|
|
194
221
|
cd miii-cli
|
|
@@ -211,6 +238,26 @@ npm run build && npm link # restore later with: npm install -g miii-agent
|
|
|
211
238
|
|
|
212
239
|
---
|
|
213
240
|
|
|
241
|
+
## FAQ
|
|
242
|
+
|
|
243
|
+
**Does miii work without internet?**
|
|
244
|
+
Yes. Once you've pulled a model with Ollama, miii runs fully offline. No network calls, no account, no cloud.
|
|
245
|
+
|
|
246
|
+
**Is my code sent anywhere?**
|
|
247
|
+
No. Every file read, edit, and model inference happens on your machine. Your codebase never leaves your disk.
|
|
248
|
+
|
|
249
|
+
**Which model is best for coding?**
|
|
250
|
+
Depends on VRAM: `qwen2.5-coder:7b` (8GB), `qwen2.5-coder:14b` (16โ24GB, the sweet spot), `qwen2.5-coder:32b` (48GB+). Run `miii doctor` to grade your installed models on real engineering tasks.
|
|
251
|
+
|
|
252
|
+
**How is miii different from Claude Code, Cursor, or GitHub Copilot?**
|
|
253
|
+
Claude Code, Cursor, and Copilot are cloud services โ metered, account-gated, and they ship your code to a third-party server. miii is open-source, free, and runs entirely on your hardware. Same terminal-agent workflow as Claude Code, but on your own local model.
|
|
254
|
+
|
|
255
|
+
**How is it different from Continue.dev?**
|
|
256
|
+
Continue.dev is an IDE extension. miii is a standalone terminal agent โ no editor required โ with a Plan โ Act โ Observe loop, permission-gated tools, and lossless output spill built in.
|
|
257
|
+
|
|
258
|
+
**Do I need a GPU?**
|
|
259
|
+
No, but it helps. Smaller models run on CPU; a GPU makes larger models fast enough for real work.
|
|
260
|
+
|
|
214
261
|
## Status
|
|
215
262
|
|
|
216
263
|
**MVP.** Core agent loop is stable; actively refining tool execution, streaming, and the permission model. PRs welcome โ fork it, break it, improve it.
|
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
|
};
|
|
@@ -1445,6 +1557,7 @@ Ask in a numbered list. One round of questions per turn. Then wait.
|
|
|
1445
1557
|
- After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
|
|
1446
1558
|
- Every tool call MUST carry a complete, valid arguments object: all required fields present, correct types, valid JSON. Never emit a call with empty, partial, or placeholder arguments.
|
|
1447
1559
|
- WRONG (leaks as text, nothing runs): writing \`call:some_tool{"foo":"bar"}\` or a fenced JSON block in your reply. RIGHT: emit it as a native function call with a full arguments object.
|
|
1560
|
+
- Batch independent tool calls in a SINGLE turn \u2014 parallel, not serial. If two reads, greps, or searches do not depend on each other's output, emit them together. Only serialize when a later call needs an earlier result.
|
|
1448
1561
|
|
|
1449
1562
|
# Tools
|
|
1450
1563
|
You have access to the following tools. Call them via the function-calling interface.
|
|
@@ -1454,14 +1567,33 @@ ${toolLines}
|
|
|
1454
1567
|
- When you need to act on the filesystem or run a command, emit a tool call.
|
|
1455
1568
|
- After each tool result, decide: more tool calls, or a final plain-text answer.
|
|
1456
1569
|
- Stop emitting tool calls when GOAL is met. Reply with a concise plain-text final message confirming CRITERION is satisfied.
|
|
1570
|
+
- After the work is done, always close by asking the user what they want to do next \u2014 a brief, specific prompt (offer the most likely follow-ups when obvious). One line, no filler.
|
|
1457
1571
|
|
|
1458
1572
|
# Rules
|
|
1459
1573
|
- Always read a file before updating it. Never edit, overwrite, or create-over a file you have not read first this turn.
|
|
1460
1574
|
- Prefer editing existing files over creating new ones.
|
|
1575
|
+
- To change an existing file, use edit_file with a small, targeted old_str/new_str diff \u2014 never rewrite the whole file with write_file. Reserve write_file for brand-new files or small ones. A full-file write_file on a large file risks getting cut off at the output token limit mid-write; a targeted edit_file stays small and avoids that.
|
|
1576
|
+
- When a new file's content is large, create it with write_file for the first portion, then append the rest with successive edit_file calls. Keep every write small.
|
|
1461
1577
|
- For edit_file, make old_str unique by including surrounding context, or set replace_all to change every occurrence.
|
|
1462
1578
|
- Never invent file paths. Read, glob, or grep before editing.
|
|
1463
1579
|
- No empty filler or robotic boilerplate. A brief, genuine warm touch (see Tone and voice) is welcome; hollow pleasantries and reflexive apologies are not.
|
|
1464
1580
|
|
|
1581
|
+
# Scope discipline
|
|
1582
|
+
- Do ONLY what the user asked. No unrequested refactors, renames, reformatting, or "while I'm here" edits.
|
|
1583
|
+
- If you spot an unrelated issue worth fixing, mention it in your final message \u2014 do not fix it unprompted.
|
|
1584
|
+
- Touch the fewest files needed. A one-line request gets a one-line change, not a redesign.
|
|
1585
|
+
|
|
1586
|
+
# Secrets and safety
|
|
1587
|
+
- Never print, log, or echo secrets, API keys, tokens, passwords, or \`.env\` values. Redact them if you must reference one.
|
|
1588
|
+
- Never write credentials into source, commits, or output. If a secret is needed, read it from the environment or config.
|
|
1589
|
+
- Do not exfiltrate file contents to external services without the user asking.
|
|
1590
|
+
|
|
1591
|
+
# Git and commits
|
|
1592
|
+
- Do NOT commit, push, or create branches/PRs unless the user explicitly asks.
|
|
1593
|
+
- When asked to commit: never commit on the main branch \u2014 branch first. Stage only files relevant to the change; never blanket \`git add -A\` without checking what it sweeps in.
|
|
1594
|
+
- Write a concise commit message stating what changed and why. Do not add credentials or generated noise.
|
|
1595
|
+
- Never force-push, rebase shared history, or run destructive git commands without explicit confirmation.
|
|
1596
|
+
|
|
1465
1597
|
# Context discipline
|
|
1466
1598
|
- read_file returns line numbers and accepts offset/limit. For large files, grep or glob to the relevant region first, then read only that range with offset/limit. Do not read a whole large file when you need a few functions \u2014 it wastes the context window.
|
|
1467
1599
|
- Reference code by the line numbers read_file returns.
|
|
@@ -1515,6 +1647,9 @@ function toOllamaMessages(history, system) {
|
|
|
1515
1647
|
const texts = msg.content.filter((b) => b.type === "text");
|
|
1516
1648
|
for (const tr of tool_results) {
|
|
1517
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
|
+
}
|
|
1518
1653
|
}
|
|
1519
1654
|
if (texts.length > 0) {
|
|
1520
1655
|
out.push({ role: "user", content: texts.map((t) => t.text).join("") });
|
|
@@ -1957,12 +2092,13 @@ async function* runAgent(opts) {
|
|
|
1957
2092
|
}
|
|
1958
2093
|
let r;
|
|
1959
2094
|
try {
|
|
1960
|
-
const out = await tool.handler(use.input);
|
|
2095
|
+
const out = await tool.handler(use.input, { signal });
|
|
1961
2096
|
r = {
|
|
1962
2097
|
type: "tool_result",
|
|
1963
2098
|
tool_use_id: use.id,
|
|
1964
2099
|
content: out.content,
|
|
1965
|
-
is_error: out.is_error
|
|
2100
|
+
is_error: out.is_error,
|
|
2101
|
+
...out.images && out.images.length > 0 ? { images: out.images } : {}
|
|
1966
2102
|
};
|
|
1967
2103
|
} catch (err) {
|
|
1968
2104
|
r = {
|
|
@@ -2298,7 +2434,7 @@ var InputBar = memo(function InputBar2({ input, caret, disabled, processingLabel
|
|
|
2298
2434
|
const [frame, setFrame] = useState(0);
|
|
2299
2435
|
useEffect(() => {
|
|
2300
2436
|
if (!disabled) return;
|
|
2301
|
-
const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length),
|
|
2437
|
+
const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 200);
|
|
2302
2438
|
return () => clearInterval(t);
|
|
2303
2439
|
}, [disabled]);
|
|
2304
2440
|
return /* @__PURE__ */ jsx2(
|
|
@@ -3347,14 +3483,26 @@ function clipTail(rendered, max) {
|
|
|
3347
3483
|
if (lines.length <= max) return { text: rendered, clipped: 0 };
|
|
3348
3484
|
return { text: lines.slice(-max).join("\n"), clipped: lines.length - max };
|
|
3349
3485
|
}
|
|
3486
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
3487
|
+
function stripAnsi(s) {
|
|
3488
|
+
return s.replace(ANSI_RE, "");
|
|
3489
|
+
}
|
|
3490
|
+
function visualHeight(text, width) {
|
|
3491
|
+
const w = Math.max(1, width);
|
|
3492
|
+
let rows = 0;
|
|
3493
|
+
for (const line of text.split("\n")) {
|
|
3494
|
+
rows += Math.max(1, Math.ceil(stripAnsi(line).length / w));
|
|
3495
|
+
}
|
|
3496
|
+
return rows;
|
|
3497
|
+
}
|
|
3350
3498
|
function clipTailVisual(content, maxRows, width) {
|
|
3351
3499
|
const w = Math.max(1, width);
|
|
3352
3500
|
const lines = content.split("\n");
|
|
3353
|
-
const
|
|
3501
|
+
const visualRows2 = (line) => Math.max(1, Math.ceil(stripAnsi(line).length / w));
|
|
3354
3502
|
let rows = 0;
|
|
3355
3503
|
let start = lines.length;
|
|
3356
3504
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3357
|
-
const h =
|
|
3505
|
+
const h = visualRows2(lines[i]);
|
|
3358
3506
|
if (rows + h > maxRows && start < lines.length) break;
|
|
3359
3507
|
rows += h;
|
|
3360
3508
|
start = i;
|
|
@@ -3367,9 +3515,20 @@ function liveFrameRows() {
|
|
|
3367
3515
|
return Math.max(6, rows - 8);
|
|
3368
3516
|
}
|
|
3369
3517
|
var COLLAPSED_LINES = 3;
|
|
3518
|
+
function visualRows(text, width, cap) {
|
|
3519
|
+
const w = Math.max(1, width);
|
|
3520
|
+
let rows = 0;
|
|
3521
|
+
const lines = text.split("\n");
|
|
3522
|
+
for (const line of lines) {
|
|
3523
|
+
rows += Math.max(1, Math.ceil(line.length / w));
|
|
3524
|
+
if (rows >= cap) return cap;
|
|
3525
|
+
}
|
|
3526
|
+
return rows;
|
|
3527
|
+
}
|
|
3370
3528
|
function estimateToolRows(use, result) {
|
|
3371
3529
|
const input = use.input ?? {};
|
|
3372
3530
|
const noErr = !result?.is_error;
|
|
3531
|
+
const w = contentWidth();
|
|
3373
3532
|
if (use.name === "write_file" && noErr) {
|
|
3374
3533
|
const total = countLines(String(input.content ?? ""));
|
|
3375
3534
|
const shown = Math.min(total, COLLAPSED_LINES);
|
|
@@ -3385,10 +3544,10 @@ function estimateToolRows(use, result) {
|
|
|
3385
3544
|
const lines = (result.content ?? "").split("\n");
|
|
3386
3545
|
const multi = (use.name === "run_bash" || use.name === "grep" || use.name === "glob" || result.is_error) && lines.length > 1;
|
|
3387
3546
|
if (multi) {
|
|
3388
|
-
const
|
|
3389
|
-
rows += 1 +
|
|
3547
|
+
const shownLines = lines.slice(0, COLLAPSED_LINES).join("\n");
|
|
3548
|
+
rows += 1 + visualRows(shownLines, w, COLLAPSED_LINES * 4) + (lines.length > COLLAPSED_LINES ? 1 : 0);
|
|
3390
3549
|
} else {
|
|
3391
|
-
rows +=
|
|
3550
|
+
rows += visualRows(lines[0] ?? "", w, 4);
|
|
3392
3551
|
}
|
|
3393
3552
|
}
|
|
3394
3553
|
return rows;
|
|
@@ -3422,7 +3581,7 @@ function ThinkingBlock({ content }) {
|
|
|
3422
3581
|
const [frame, setFrame] = useState2(0);
|
|
3423
3582
|
const visible = useThinkingVisible();
|
|
3424
3583
|
useEffect2(() => {
|
|
3425
|
-
const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length),
|
|
3584
|
+
const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 100);
|
|
3426
3585
|
return () => clearInterval(t);
|
|
3427
3586
|
}, []);
|
|
3428
3587
|
const label = "thinking";
|
|
@@ -3689,14 +3848,16 @@ function ToolUseLine({ use, result }) {
|
|
|
3689
3848
|
}
|
|
3690
3849
|
if (use.name === "edit_file" && !result?.is_error) {
|
|
3691
3850
|
const input = use.input;
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
const
|
|
3696
|
-
const
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
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
|
+
}
|
|
3700
3861
|
return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Update", path: input.path ?? "", added, removed, previewLines: preview });
|
|
3701
3862
|
}
|
|
3702
3863
|
const { label, arg } = toolHeader(use);
|
|
@@ -3801,7 +3962,8 @@ function ChatView({
|
|
|
3801
3962
|
permissionCursor = 0,
|
|
3802
3963
|
activeToolUses,
|
|
3803
3964
|
activeToolResults,
|
|
3804
|
-
header
|
|
3965
|
+
header,
|
|
3966
|
+
logEpoch = 0
|
|
3805
3967
|
}) {
|
|
3806
3968
|
const empty = messages.length === 0 && !streaming && !thinking && !pendingPermission && !error;
|
|
3807
3969
|
const log = [];
|
|
@@ -3816,8 +3978,12 @@ function ChatView({
|
|
|
3816
3978
|
let streamNode = null;
|
|
3817
3979
|
let streamRows = 0;
|
|
3818
3980
|
if (streaming && streamingContent) {
|
|
3819
|
-
const
|
|
3820
|
-
|
|
3981
|
+
const raw = clipTail(streamingContent, liveBudget);
|
|
3982
|
+
const width = contentWidth();
|
|
3983
|
+
const rendered = clipTailVisual(renderMarkdownStreaming(raw.text), liveBudget, width);
|
|
3984
|
+
const text = rendered.text;
|
|
3985
|
+
const clipped = raw.clipped + rendered.clipped;
|
|
3986
|
+
streamRows = visualHeight(text, width) + (clipped > 0 ? 1 : 0);
|
|
3821
3987
|
streamNode = /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
|
|
3822
3988
|
clipped > 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: `\u2191 ${clipped} more line${clipped === 1 ? "" : "s"} above \u2014 streaming\u2026` }),
|
|
3823
3989
|
/* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", children: [
|
|
@@ -3847,7 +4013,7 @@ function ChatView({
|
|
|
3847
4013
|
] });
|
|
3848
4014
|
}
|
|
3849
4015
|
return /* @__PURE__ */ jsxs12(Fragment2, { children: [
|
|
3850
|
-
/* @__PURE__ */ jsx12(Static, { items: log, children: (item) => item.key === "header" ? /* @__PURE__ */ jsx12(Box12, { children: item.node }, item.key) : /* @__PURE__ */ jsx12(Box12, { marginLeft: 1, children: item.node }, item.key) }),
|
|
4016
|
+
/* @__PURE__ */ jsx12(Static, { items: log, children: (item) => item.key === "header" ? /* @__PURE__ */ jsx12(Box12, { children: item.node }, item.key) : /* @__PURE__ */ jsx12(Box12, { marginLeft: 1, children: item.node }, item.key) }, logEpoch),
|
|
3851
4017
|
/* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
|
|
3852
4018
|
empty && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
|
|
3853
4019
|
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: EMPTY_STATE_TITLE }),
|
|
@@ -4015,8 +4181,8 @@ function useAgentRunner(model, activeCtx) {
|
|
|
4015
4181
|
case "turn-end": {
|
|
4016
4182
|
flushStream(true);
|
|
4017
4183
|
flushThink(true);
|
|
4018
|
-
setStreaming(false);
|
|
4019
4184
|
if (ev.stop_reason === "tool_use") {
|
|
4185
|
+
setStreaming(false);
|
|
4020
4186
|
flushTurn(null);
|
|
4021
4187
|
setThinking(true);
|
|
4022
4188
|
thinkingAcc = "";
|
|
@@ -4288,6 +4454,7 @@ function useKeyboard(opts) {
|
|
|
4288
4454
|
sessions,
|
|
4289
4455
|
setSessions,
|
|
4290
4456
|
setNotice,
|
|
4457
|
+
setLogEpoch,
|
|
4291
4458
|
switchProvider
|
|
4292
4459
|
} = opts;
|
|
4293
4460
|
const {
|
|
@@ -4321,6 +4488,7 @@ function useKeyboard(opts) {
|
|
|
4321
4488
|
setError(null);
|
|
4322
4489
|
setNotice(null);
|
|
4323
4490
|
clearPasteStore();
|
|
4491
|
+
setLogEpoch((n) => n + 1);
|
|
4324
4492
|
}
|
|
4325
4493
|
const effort = cfg.effort ?? "medium";
|
|
4326
4494
|
useInput((char, key) => {
|
|
@@ -4779,6 +4947,7 @@ function App() {
|
|
|
4779
4947
|
sessionIdRef.current = sessionId;
|
|
4780
4948
|
const [sessions, setSessions] = useState5([]);
|
|
4781
4949
|
const [notice, setNotice] = useState5(null);
|
|
4950
|
+
const [logEpoch, setLogEpoch] = useState5(0);
|
|
4782
4951
|
const [input, setInput] = useState5("");
|
|
4783
4952
|
const [caret, setCaret] = useState5(0);
|
|
4784
4953
|
const [paletteCursor, setPaletteCursor] = useState5(0);
|
|
@@ -4899,6 +5068,7 @@ function App() {
|
|
|
4899
5068
|
sessions,
|
|
4900
5069
|
setSessions,
|
|
4901
5070
|
setNotice,
|
|
5071
|
+
setLogEpoch,
|
|
4902
5072
|
switchProvider
|
|
4903
5073
|
});
|
|
4904
5074
|
const effort = cfg.effort ?? "medium";
|
|
@@ -4910,7 +5080,7 @@ function App() {
|
|
|
4910
5080
|
return Math.round(used / activeCtx * 100);
|
|
4911
5081
|
})();
|
|
4912
5082
|
return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
|
|
4913
|
-
state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable, updateStatus }),
|
|
5083
|
+
state !== "ready" && state !== "sessions" && state !== "models" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable, updateStatus }),
|
|
4914
5084
|
state === "loading" && !agent.error && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
|
|
4915
5085
|
agent.error && state !== "ready" && /* @__PURE__ */ jsx13(
|
|
4916
5086
|
ChatView,
|
|
@@ -4922,7 +5092,7 @@ function App() {
|
|
|
4922
5092
|
error: agent.error
|
|
4923
5093
|
}
|
|
4924
5094
|
),
|
|
4925
|
-
|
|
5095
|
+
state === "select-model" && /* @__PURE__ */ jsx13(
|
|
4926
5096
|
ModelsView,
|
|
4927
5097
|
{
|
|
4928
5098
|
models: filteredModels,
|
|
@@ -4933,7 +5103,7 @@ function App() {
|
|
|
4933
5103
|
providerType: provEntry.type,
|
|
4934
5104
|
effort,
|
|
4935
5105
|
query: pickerQuery,
|
|
4936
|
-
requireSelection:
|
|
5106
|
+
requireSelection: true
|
|
4937
5107
|
}
|
|
4938
5108
|
),
|
|
4939
5109
|
state === "providers" && /* @__PURE__ */ jsx13(
|
|
@@ -4945,8 +5115,7 @@ function App() {
|
|
|
4945
5115
|
query: pickerQuery
|
|
4946
5116
|
}
|
|
4947
5117
|
),
|
|
4948
|
-
state === "sessions" && /* @__PURE__ */
|
|
4949
|
-
state === "ready" && /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
5118
|
+
(state === "ready" || state === "sessions" || state === "models") && /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
4950
5119
|
notice && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "green", children: `\u2713 ${notice}` }) }),
|
|
4951
5120
|
/* @__PURE__ */ jsx13(
|
|
4952
5121
|
ChatView,
|
|
@@ -4961,18 +5130,33 @@ function App() {
|
|
|
4961
5130
|
permissionCursor: agent.permissionCursor,
|
|
4962
5131
|
activeToolUses: agent.activeToolUses,
|
|
4963
5132
|
activeToolResults: agent.activeToolResults,
|
|
4964
|
-
header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd })
|
|
5133
|
+
header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd }),
|
|
5134
|
+
logEpoch
|
|
4965
5135
|
}
|
|
4966
5136
|
),
|
|
4967
|
-
input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
|
|
4968
|
-
contextWarning !== null && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: `\u26A0 context ${contextWarning}% full \u2014 run /clear and start fresh` }) }),
|
|
4969
|
-
!input.startsWith("/") && (() => {
|
|
5137
|
+
state === "ready" && input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
|
|
5138
|
+
state === "ready" && contextWarning !== null && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: `\u26A0 context ${contextWarning}% full \u2014 run /clear and start fresh` }) }),
|
|
5139
|
+
state === "ready" && !input.startsWith("/") && (() => {
|
|
4970
5140
|
const m = parseMention(input);
|
|
4971
5141
|
if (!m) return null;
|
|
4972
5142
|
return /* @__PURE__ */ jsx13(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
|
|
4973
5143
|
})(),
|
|
4974
|
-
/* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
|
|
4975
|
-
|
|
5144
|
+
state === "ready" && /* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
|
|
5145
|
+
state === "sessions" && /* @__PURE__ */ jsx13(SessionsView, { sessions, cursor }),
|
|
5146
|
+
state === "models" && /* @__PURE__ */ jsx13(
|
|
5147
|
+
ModelsView,
|
|
5148
|
+
{
|
|
5149
|
+
models: filteredModels,
|
|
5150
|
+
cursor,
|
|
5151
|
+
model: cfg.model,
|
|
5152
|
+
host: provEntry.baseUrl,
|
|
5153
|
+
provider: provName,
|
|
5154
|
+
providerType: provEntry.type,
|
|
5155
|
+
effort,
|
|
5156
|
+
query: pickerQuery
|
|
5157
|
+
}
|
|
5158
|
+
),
|
|
5159
|
+
state === "ready" && !agent.busy && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) }),
|
|
4976
5160
|
updateAvailable && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: updateStatus === "failed" ? "red" : updateStatus === "installed" ? "green" : "yellow", children: updateBannerText(updateAvailable, updateStatus) }) })
|
|
4977
5161
|
] })
|
|
4978
5162
|
] });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.32",
|
|
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": {
|
|
7
7
|
"miii": "dist/cli.js"
|
|
@@ -52,7 +52,14 @@
|
|
|
52
52
|
"pair-programming",
|
|
53
53
|
"code-generation",
|
|
54
54
|
"llama-cpp",
|
|
55
|
-
"lm-studio"
|
|
55
|
+
"lm-studio",
|
|
56
|
+
"copilot",
|
|
57
|
+
"copilot-alternative",
|
|
58
|
+
"cursor-alternative",
|
|
59
|
+
"claude-code",
|
|
60
|
+
"claude-code-alternative",
|
|
61
|
+
"developer-tools",
|
|
62
|
+
"qwen"
|
|
56
63
|
],
|
|
57
64
|
"license": "MIT",
|
|
58
65
|
"dependencies": {
|