miii-agent 0.1.29 โ 0.1.31
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 +77 -12
- package/dist/cli.js +298 -163
- 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,59 @@
|
|
|
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)
|
|
36
|
+
|
|
37
|
+
**macOS / Linux:**
|
|
30
38
|
|
|
31
39
|
```bash
|
|
32
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
|
|
33
49
|
curl -fsSL https://raw.githubusercontent.com/maruakshay/miii-cli/main/install.sh | sh
|
|
34
50
|
miii
|
|
35
51
|
```
|
|
52
|
+
*(The installer downloads the pre-compiled binary and adds it to your local path)*
|
|
53
|
+
|
|
54
|
+
**Windows (PowerShell):**
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
ollama pull qwen2.5-coder:14b
|
|
58
|
+
irm https://raw.githubusercontent.com/maruakshay/miii-cli/main/install.ps1 | iex
|
|
59
|
+
miii
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Prefer npm? `npm install -g miii-agent` works on every platform.
|
|
36
63
|
|
|
37
|
-
|
|
64
|
+
> **Install failing on permissions?** Your global npm prefix isn't writable. The
|
|
65
|
+
> installer retries with `sudo` where available; otherwise point npm at a
|
|
66
|
+
> user-owned prefix and re-run:
|
|
67
|
+
> ```bash
|
|
68
|
+
> npm config set prefix "$HOME/.npm-global"
|
|
69
|
+
> export PATH="$HOME/.npm-global/bin:$PATH" # add to ~/.bashrc or ~/.zshrc
|
|
70
|
+
> ```
|
|
38
71
|
|
|
39
72
|
Then just talk to it:
|
|
40
73
|
|
|
@@ -59,7 +92,7 @@ miii --version # what you're running
|
|
|
59
92
|
Opt out of background updates by adding `"autoUpdate": false` to `~/.miii/config.json`,
|
|
60
93
|
or re-run the install script (`curl โฆ | sh`) any time to update by hand.
|
|
61
94
|
|
|
62
|
-
## Why local-first?
|
|
95
|
+
## Why local-first? Private, free, offline
|
|
63
96
|
|
|
64
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.
|
|
65
98
|
|
|
@@ -71,13 +104,16 @@ Most "AI coding tools" are just wrappers around a cloud API โ slow, metered, a
|
|
|
71
104
|
| Offline | No | Yes |
|
|
72
105
|
| Latency | Network + queue | Your GPU only |
|
|
73
106
|
|
|
74
|
-
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.
|
|
75
111
|
|
|
76
112
|
## Five letters, five ideas
|
|
77
113
|
|
|
78
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.
|
|
79
115
|
|
|
80
|
-
##
|
|
116
|
+
## Features
|
|
81
117
|
|
|
82
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.
|
|
83
119
|
```bash
|
|
@@ -171,6 +207,15 @@ The model pages through the middle with ranged `read_file` reads. Spill files ar
|
|
|
171
207
|
<details>
|
|
172
208
|
<summary><strong>Development</strong></summary>
|
|
173
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
|
+
|
|
174
219
|
```bash
|
|
175
220
|
git clone https://github.com/maruakshay/miii-cli.git
|
|
176
221
|
cd miii-cli
|
|
@@ -193,6 +238,26 @@ npm run build && npm link # restore later with: npm install -g miii-agent
|
|
|
193
238
|
|
|
194
239
|
---
|
|
195
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
|
+
|
|
196
261
|
## Status
|
|
197
262
|
|
|
198
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
|
@@ -36,7 +36,8 @@ function migrate(raw) {
|
|
|
36
36
|
effort: raw.effort,
|
|
37
37
|
providers,
|
|
38
38
|
modelContexts: raw.modelContexts,
|
|
39
|
-
autoUpdate: raw.autoUpdate
|
|
39
|
+
autoUpdate: raw.autoUpdate,
|
|
40
|
+
numCtxCap: raw.numCtxCap
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
function autoUpdateEnabled(cfg = loadConfig()) {
|
|
@@ -50,6 +51,17 @@ function readRawConfig() {
|
|
|
50
51
|
return {};
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
function configError() {
|
|
55
|
+
if (!existsSync(CONFIG_PATH)) return null;
|
|
56
|
+
try {
|
|
57
|
+
JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
58
|
+
return null;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
return `miii: ignoring unreadable ${CONFIG_PATH} (${msg}).
|
|
62
|
+
Running with defaults. Fix the JSON or delete the file to reset.`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
53
65
|
function loadConfig() {
|
|
54
66
|
return migrate(readRawConfig());
|
|
55
67
|
}
|
|
@@ -88,10 +100,11 @@ function setModelContexts(contexts) {
|
|
|
88
100
|
const raw = readRawConfig();
|
|
89
101
|
saveConfig({ ...raw, modelContexts: { ...raw.modelContexts, ...contexts } });
|
|
90
102
|
}
|
|
91
|
-
var EFFORT_OPTIONS, CONFIG_DIR, CONFIG_PATH;
|
|
103
|
+
var DEFAULT_NUM_CTX_CAP, EFFORT_OPTIONS, CONFIG_DIR, CONFIG_PATH;
|
|
92
104
|
var init_config = __esm({
|
|
93
105
|
"src/config.ts"() {
|
|
94
106
|
"use strict";
|
|
107
|
+
DEFAULT_NUM_CTX_CAP = 16384;
|
|
95
108
|
EFFORT_OPTIONS = {
|
|
96
109
|
low: { temperature: 0.2, num_predict: 8192 },
|
|
97
110
|
medium: { temperature: 0.7, num_predict: 16384 },
|
|
@@ -535,9 +548,130 @@ var init_client = __esm({
|
|
|
535
548
|
}
|
|
536
549
|
});
|
|
537
550
|
|
|
538
|
-
// src/
|
|
539
|
-
import {
|
|
551
|
+
// src/permissions/policy.ts
|
|
552
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, renameSync } from "fs";
|
|
553
|
+
import { join as join4 } from "path";
|
|
540
554
|
import { homedir as homedir3 } from "os";
|
|
555
|
+
function loadRules() {
|
|
556
|
+
if (!existsSync3(RULES_PATH)) return [];
|
|
557
|
+
try {
|
|
558
|
+
const data = JSON.parse(readFileSync3(RULES_PATH, "utf-8"));
|
|
559
|
+
return Array.isArray(data.rules) ? data.rules : [];
|
|
560
|
+
} catch {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function saveRules(rules) {
|
|
565
|
+
mkdirSync3(RULES_DIR, { recursive: true });
|
|
566
|
+
const tmp = RULES_PATH + ".tmp";
|
|
567
|
+
writeFileSync3(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
|
|
568
|
+
renameSync(tmp, RULES_PATH);
|
|
569
|
+
}
|
|
570
|
+
function addRule(tool, pattern) {
|
|
571
|
+
const rules = loadRules();
|
|
572
|
+
if (rules.some((r) => r.tool === tool && r.pattern === pattern)) return;
|
|
573
|
+
rules.push({ tool, pattern });
|
|
574
|
+
saveRules(rules);
|
|
575
|
+
}
|
|
576
|
+
function subjectFor(toolName, input) {
|
|
577
|
+
const obj = input ?? {};
|
|
578
|
+
if (toolName === "run_bash") return typeof obj.command === "string" ? obj.command : "";
|
|
579
|
+
if (typeof obj.path === "string") return obj.path;
|
|
580
|
+
return "";
|
|
581
|
+
}
|
|
582
|
+
function generalizeCommand(command) {
|
|
583
|
+
const trimmed = command.trim();
|
|
584
|
+
const tokens = trimmed.split(/\s+/);
|
|
585
|
+
if (tokens.length === 0 || tokens[0] === "") return command;
|
|
586
|
+
const prog = tokens[0];
|
|
587
|
+
if (NEVER_GENERALIZE.has(prog)) return trimmed;
|
|
588
|
+
if (prog === "git" && tokens.length > 1 && DESTRUCTIVE_GIT_SUBCOMMANDS.has(tokens[1])) {
|
|
589
|
+
return trimmed;
|
|
590
|
+
}
|
|
591
|
+
const prefixLen = WRAPPER_PROGRAMS.has(prog) && tokens.length > 1 ? 2 : 1;
|
|
592
|
+
const prefix = tokens.slice(0, prefixLen).join(" ");
|
|
593
|
+
return `${prefix} *`;
|
|
594
|
+
}
|
|
595
|
+
function patternToPersist(toolName, subject) {
|
|
596
|
+
return toolName === "run_bash" ? generalizeCommand(subject) : subject;
|
|
597
|
+
}
|
|
598
|
+
function globToRegExp(glob2) {
|
|
599
|
+
const escaped = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
600
|
+
const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
601
|
+
return new RegExp(`^${pattern}$`);
|
|
602
|
+
}
|
|
603
|
+
function matches(rule, toolName, subject) {
|
|
604
|
+
if (rule.tool !== toolName) return false;
|
|
605
|
+
try {
|
|
606
|
+
return globToRegExp(rule.pattern).test(subject);
|
|
607
|
+
} catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async function check(toolName, input, ctx) {
|
|
612
|
+
if (ALWAYS_ALLOW.has(toolName)) return "allow";
|
|
613
|
+
const subject = subjectFor(toolName, input);
|
|
614
|
+
const rules = loadRules();
|
|
615
|
+
if (rules.some((r) => matches(r, toolName, subject))) return "allow";
|
|
616
|
+
const answer = await ctx.ask(toolName, input);
|
|
617
|
+
if (answer === "no") return "deny";
|
|
618
|
+
if (answer === "always") addRule(toolName, patternToPersist(toolName, subject));
|
|
619
|
+
return "allow";
|
|
620
|
+
}
|
|
621
|
+
var RULES_DIR, RULES_PATH, WRAPPER_PROGRAMS, NEVER_GENERALIZE, DESTRUCTIVE_GIT_SUBCOMMANDS, ALWAYS_ALLOW;
|
|
622
|
+
var init_policy = __esm({
|
|
623
|
+
"src/permissions/policy.ts"() {
|
|
624
|
+
"use strict";
|
|
625
|
+
RULES_DIR = join4(homedir3(), ".miii");
|
|
626
|
+
RULES_PATH = join4(RULES_DIR, "permissions.json");
|
|
627
|
+
WRAPPER_PROGRAMS = /* @__PURE__ */ new Set([
|
|
628
|
+
"npm",
|
|
629
|
+
"npx",
|
|
630
|
+
"pnpm",
|
|
631
|
+
"yarn",
|
|
632
|
+
"brew",
|
|
633
|
+
"pip",
|
|
634
|
+
"pip3",
|
|
635
|
+
"cargo",
|
|
636
|
+
"docker",
|
|
637
|
+
"kubectl",
|
|
638
|
+
"go",
|
|
639
|
+
"git"
|
|
640
|
+
]);
|
|
641
|
+
NEVER_GENERALIZE = /* @__PURE__ */ new Set([
|
|
642
|
+
"rm",
|
|
643
|
+
"rmdir",
|
|
644
|
+
"dd",
|
|
645
|
+
"mkfs",
|
|
646
|
+
"shred",
|
|
647
|
+
"truncate",
|
|
648
|
+
"shutdown",
|
|
649
|
+
"reboot",
|
|
650
|
+
"halt",
|
|
651
|
+
"poweroff",
|
|
652
|
+
"kill",
|
|
653
|
+
"killall",
|
|
654
|
+
"pkill",
|
|
655
|
+
"chmod",
|
|
656
|
+
"chown",
|
|
657
|
+
"mv",
|
|
658
|
+
"sudo",
|
|
659
|
+
"doas"
|
|
660
|
+
]);
|
|
661
|
+
DESTRUCTIVE_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
662
|
+
"reset",
|
|
663
|
+
"clean",
|
|
664
|
+
"push",
|
|
665
|
+
"rebase",
|
|
666
|
+
"filter-branch"
|
|
667
|
+
]);
|
|
668
|
+
ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// src/tools/paths.ts
|
|
673
|
+
import { resolve, relative as relative2, isAbsolute, sep, join as join5 } from "path";
|
|
674
|
+
import { homedir as homedir4 } from "os";
|
|
541
675
|
function isUnder(parent, child) {
|
|
542
676
|
const rel = relative2(parent, child);
|
|
543
677
|
return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !isAbsolute(rel);
|
|
@@ -557,7 +691,7 @@ var SPILL_DIR;
|
|
|
557
691
|
var init_paths = __esm({
|
|
558
692
|
"src/tools/paths.ts"() {
|
|
559
693
|
"use strict";
|
|
560
|
-
SPILL_DIR = resolve(
|
|
694
|
+
SPILL_DIR = resolve(join5(homedir4(), ".miii", "output"));
|
|
561
695
|
}
|
|
562
696
|
});
|
|
563
697
|
|
|
@@ -595,7 +729,7 @@ var init_verifyHint = __esm({
|
|
|
595
729
|
});
|
|
596
730
|
|
|
597
731
|
// src/tools/edit_file.ts
|
|
598
|
-
import { readFileSync as
|
|
732
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
599
733
|
function similarity(a, b) {
|
|
600
734
|
const x = a.trim();
|
|
601
735
|
const y = b.trim();
|
|
@@ -684,7 +818,7 @@ var init_edit_file = __esm({
|
|
|
684
818
|
};
|
|
685
819
|
}
|
|
686
820
|
const abs = confinePath(path);
|
|
687
|
-
const src =
|
|
821
|
+
const src = readFileSync4(abs, "utf-8");
|
|
688
822
|
const first = src.indexOf(old_str);
|
|
689
823
|
if (first === -1) {
|
|
690
824
|
if (replace_all !== true) {
|
|
@@ -692,7 +826,7 @@ var init_edit_file = __esm({
|
|
|
692
826
|
if (fuzzy) {
|
|
693
827
|
const [s, e] = fuzzy;
|
|
694
828
|
const out2 = src.slice(0, s) + new_str + src.slice(e);
|
|
695
|
-
|
|
829
|
+
writeFileSync4(abs, out2, "utf-8");
|
|
696
830
|
return { content: `Edited ${path} (whitespace-tolerant match).${verifyHint(path)}` };
|
|
697
831
|
}
|
|
698
832
|
}
|
|
@@ -707,7 +841,7 @@ var init_edit_file = __esm({
|
|
|
707
841
|
}
|
|
708
842
|
const out = all ? src.split(old_str).join(new_str) : src.slice(0, first) + new_str + src.slice(first + old_str.length);
|
|
709
843
|
const n = all ? src.split(old_str).length - 1 : 1;
|
|
710
|
-
|
|
844
|
+
writeFileSync4(abs, out, "utf-8");
|
|
711
845
|
return { content: `Edited ${path}${all ? ` (${n} occurrences)` : ""}.${verifyHint(path)}` };
|
|
712
846
|
} catch (err) {
|
|
713
847
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
@@ -718,7 +852,7 @@ var init_edit_file = __esm({
|
|
|
718
852
|
});
|
|
719
853
|
|
|
720
854
|
// src/tools/read_file.ts
|
|
721
|
-
import { readFileSync as
|
|
855
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
722
856
|
function numbered(lines, start) {
|
|
723
857
|
const width = String(start + lines.length - 1).length;
|
|
724
858
|
return lines.map((l, i) => `${String(start + i).padStart(width, " ")} ${l}`).join("\n");
|
|
@@ -743,7 +877,7 @@ var init_read_file = __esm({
|
|
|
743
877
|
handler: ({ path, offset, limit }) => {
|
|
744
878
|
try {
|
|
745
879
|
const MAX_CHARS = 2e5;
|
|
746
|
-
const buf =
|
|
880
|
+
const buf = readFileSync5(confinePath(path));
|
|
747
881
|
if (buf.subarray(0, 8e3).includes(0)) {
|
|
748
882
|
return { content: `${path} looks binary (${buf.length} bytes); not reading as text.`, is_error: true };
|
|
749
883
|
}
|
|
@@ -774,7 +908,7 @@ var init_read_file = __esm({
|
|
|
774
908
|
});
|
|
775
909
|
|
|
776
910
|
// src/tools/write_file.ts
|
|
777
|
-
import { writeFileSync as
|
|
911
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
778
912
|
import { dirname } from "path";
|
|
779
913
|
var write_file;
|
|
780
914
|
var init_write_file = __esm({
|
|
@@ -796,8 +930,8 @@ var init_write_file = __esm({
|
|
|
796
930
|
handler: ({ path, content }) => {
|
|
797
931
|
try {
|
|
798
932
|
const abs = confinePath(path);
|
|
799
|
-
|
|
800
|
-
|
|
933
|
+
mkdirSync4(dirname(abs), { recursive: true });
|
|
934
|
+
writeFileSync5(abs, content, "utf-8");
|
|
801
935
|
return { content: `Wrote ${path} (${content.length} bytes).${verifyHint(path)}` };
|
|
802
936
|
} catch (err) {
|
|
803
937
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
@@ -808,21 +942,21 @@ var init_write_file = __esm({
|
|
|
808
942
|
});
|
|
809
943
|
|
|
810
944
|
// src/tools/spill.ts
|
|
811
|
-
import { writeFileSync as
|
|
812
|
-
import { join as
|
|
813
|
-
import { homedir as
|
|
945
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, rmSync as rmSync2, readdirSync as readdirSync3, statSync } from "fs";
|
|
946
|
+
import { join as join6 } from "path";
|
|
947
|
+
import { homedir as homedir5 } from "os";
|
|
814
948
|
import { randomBytes } from "crypto";
|
|
815
949
|
function ensureDir() {
|
|
816
|
-
|
|
950
|
+
mkdirSync5(OUTPUT_DIR, { recursive: true });
|
|
817
951
|
return OUTPUT_DIR;
|
|
818
952
|
}
|
|
819
953
|
function spillIfLarge(full, label = "output", budget = INLINE_BUDGET) {
|
|
820
954
|
if (full.length <= budget) return full;
|
|
821
955
|
const id = randomBytes(6).toString("hex");
|
|
822
|
-
const file =
|
|
956
|
+
const file = join6(ensureDir(), `${id}.txt`);
|
|
823
957
|
let path = file;
|
|
824
958
|
try {
|
|
825
|
-
|
|
959
|
+
writeFileSync6(file, full, "utf-8");
|
|
826
960
|
} catch {
|
|
827
961
|
path = "";
|
|
828
962
|
}
|
|
@@ -837,7 +971,7 @@ function cleanupSpill(maxAgeMs = 24 * 60 * 60 * 1e3) {
|
|
|
837
971
|
try {
|
|
838
972
|
const now = Date.now();
|
|
839
973
|
for (const name of readdirSync3(OUTPUT_DIR)) {
|
|
840
|
-
const f =
|
|
974
|
+
const f = join6(OUTPUT_DIR, name);
|
|
841
975
|
try {
|
|
842
976
|
if (now - statSync(f).mtimeMs > maxAgeMs) rmSync2(f, { force: true });
|
|
843
977
|
} catch {
|
|
@@ -850,7 +984,7 @@ var OUTPUT_DIR, INLINE_BUDGET, HEAD_FRACTION;
|
|
|
850
984
|
var init_spill = __esm({
|
|
851
985
|
"src/tools/spill.ts"() {
|
|
852
986
|
"use strict";
|
|
853
|
-
OUTPUT_DIR =
|
|
987
|
+
OUTPUT_DIR = join6(homedir5(), ".miii", "output");
|
|
854
988
|
INLINE_BUDGET = 1e4;
|
|
855
989
|
HEAD_FRACTION = 0.3;
|
|
856
990
|
}
|
|
@@ -870,7 +1004,7 @@ var init_run_bash = __esm({
|
|
|
870
1004
|
type: "object",
|
|
871
1005
|
properties: {
|
|
872
1006
|
command: { type: "string", description: "Shell command to run" },
|
|
873
|
-
timeout_ms: { type: "number", description: "Timeout in ms (default
|
|
1007
|
+
timeout_ms: { type: "number", description: "Timeout in ms (default 120000). Raise it for long builds/test suites." }
|
|
874
1008
|
},
|
|
875
1009
|
required: ["command"]
|
|
876
1010
|
},
|
|
@@ -880,7 +1014,7 @@ var init_run_bash = __esm({
|
|
|
880
1014
|
const shell = isWin ? "cmd" : "bash";
|
|
881
1015
|
const shellArgs = isWin ? ["/c", command] : ["-c", command];
|
|
882
1016
|
const { stdout, stderr, exitCode } = await execa(shell, shellArgs, {
|
|
883
|
-
timeout: timeout_ms ??
|
|
1017
|
+
timeout: timeout_ms ?? 12e4,
|
|
884
1018
|
reject: false,
|
|
885
1019
|
all: false
|
|
886
1020
|
});
|
|
@@ -1187,14 +1321,14 @@ var init_validate = __esm({
|
|
|
1187
1321
|
});
|
|
1188
1322
|
|
|
1189
1323
|
// src/prompt/context.ts
|
|
1190
|
-
import { existsSync as
|
|
1191
|
-
import { dirname as dirname2, join as
|
|
1324
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
1325
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
1192
1326
|
function findContextFile(cwd) {
|
|
1193
1327
|
let dir = cwd;
|
|
1194
1328
|
for (; ; ) {
|
|
1195
|
-
const candidate =
|
|
1196
|
-
if (
|
|
1197
|
-
if (
|
|
1329
|
+
const candidate = join7(dir, CONTEXT_FILENAME);
|
|
1330
|
+
if (existsSync4(candidate)) return candidate;
|
|
1331
|
+
if (existsSync4(join7(dir, ".git"))) return null;
|
|
1198
1332
|
const parent = dirname2(dir);
|
|
1199
1333
|
if (parent === dir) return null;
|
|
1200
1334
|
dir = parent;
|
|
@@ -1205,7 +1339,7 @@ function loadProjectContext(cwd) {
|
|
|
1205
1339
|
if (!source) return EMPTY;
|
|
1206
1340
|
try {
|
|
1207
1341
|
if (statSync3(source).size === 0) return { ...EMPTY, source };
|
|
1208
|
-
const raw =
|
|
1342
|
+
const raw = readFileSync6(source, "utf8");
|
|
1209
1343
|
if (Buffer.byteLength(raw, "utf8") > MAX_CONTEXT_BYTES) {
|
|
1210
1344
|
const clipped = Buffer.from(raw, "utf8").subarray(0, MAX_CONTEXT_BYTES).toString("utf8");
|
|
1211
1345
|
return { content: clipped, source, truncated: true };
|
|
@@ -1311,6 +1445,7 @@ Ask in a numbered list. One round of questions per turn. Then wait.
|
|
|
1311
1445
|
- After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
|
|
1312
1446
|
- 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.
|
|
1313
1447
|
- 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.
|
|
1448
|
+
- 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.
|
|
1314
1449
|
|
|
1315
1450
|
# Tools
|
|
1316
1451
|
You have access to the following tools. Call them via the function-calling interface.
|
|
@@ -1320,14 +1455,33 @@ ${toolLines}
|
|
|
1320
1455
|
- When you need to act on the filesystem or run a command, emit a tool call.
|
|
1321
1456
|
- After each tool result, decide: more tool calls, or a final plain-text answer.
|
|
1322
1457
|
- Stop emitting tool calls when GOAL is met. Reply with a concise plain-text final message confirming CRITERION is satisfied.
|
|
1458
|
+
- 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.
|
|
1323
1459
|
|
|
1324
1460
|
# Rules
|
|
1325
1461
|
- Always read a file before updating it. Never edit, overwrite, or create-over a file you have not read first this turn.
|
|
1326
1462
|
- Prefer editing existing files over creating new ones.
|
|
1463
|
+
- 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.
|
|
1464
|
+
- 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.
|
|
1327
1465
|
- For edit_file, make old_str unique by including surrounding context, or set replace_all to change every occurrence.
|
|
1328
1466
|
- Never invent file paths. Read, glob, or grep before editing.
|
|
1329
1467
|
- No empty filler or robotic boilerplate. A brief, genuine warm touch (see Tone and voice) is welcome; hollow pleasantries and reflexive apologies are not.
|
|
1330
1468
|
|
|
1469
|
+
# Scope discipline
|
|
1470
|
+
- Do ONLY what the user asked. No unrequested refactors, renames, reformatting, or "while I'm here" edits.
|
|
1471
|
+
- If you spot an unrelated issue worth fixing, mention it in your final message \u2014 do not fix it unprompted.
|
|
1472
|
+
- Touch the fewest files needed. A one-line request gets a one-line change, not a redesign.
|
|
1473
|
+
|
|
1474
|
+
# Secrets and safety
|
|
1475
|
+
- Never print, log, or echo secrets, API keys, tokens, passwords, or \`.env\` values. Redact them if you must reference one.
|
|
1476
|
+
- Never write credentials into source, commits, or output. If a secret is needed, read it from the environment or config.
|
|
1477
|
+
- Do not exfiltrate file contents to external services without the user asking.
|
|
1478
|
+
|
|
1479
|
+
# Git and commits
|
|
1480
|
+
- Do NOT commit, push, or create branches/PRs unless the user explicitly asks.
|
|
1481
|
+
- 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.
|
|
1482
|
+
- Write a concise commit message stating what changed and why. Do not add credentials or generated noise.
|
|
1483
|
+
- Never force-push, rebase shared history, or run destructive git commands without explicit confirmation.
|
|
1484
|
+
|
|
1331
1485
|
# Context discipline
|
|
1332
1486
|
- 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.
|
|
1333
1487
|
- Reference code by the line numbers read_file returns.
|
|
@@ -1349,94 +1503,6 @@ var init_system = __esm({
|
|
|
1349
1503
|
}
|
|
1350
1504
|
});
|
|
1351
1505
|
|
|
1352
|
-
// src/permissions/policy.ts
|
|
1353
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync4, renameSync } from "fs";
|
|
1354
|
-
import { join as join7 } from "path";
|
|
1355
|
-
import { homedir as homedir5 } from "os";
|
|
1356
|
-
function loadRules() {
|
|
1357
|
-
if (!existsSync4(RULES_PATH)) return [];
|
|
1358
|
-
try {
|
|
1359
|
-
const data = JSON.parse(readFileSync6(RULES_PATH, "utf-8"));
|
|
1360
|
-
return Array.isArray(data.rules) ? data.rules : [];
|
|
1361
|
-
} catch {
|
|
1362
|
-
return [];
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
function saveRules(rules) {
|
|
1366
|
-
mkdirSync5(RULES_DIR, { recursive: true });
|
|
1367
|
-
const tmp = RULES_PATH + ".tmp";
|
|
1368
|
-
writeFileSync6(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
|
|
1369
|
-
renameSync(tmp, RULES_PATH);
|
|
1370
|
-
}
|
|
1371
|
-
function addRule(tool, pattern) {
|
|
1372
|
-
const rules = loadRules();
|
|
1373
|
-
if (rules.some((r) => r.tool === tool && r.pattern === pattern)) return;
|
|
1374
|
-
rules.push({ tool, pattern });
|
|
1375
|
-
saveRules(rules);
|
|
1376
|
-
}
|
|
1377
|
-
function subjectFor(toolName, input) {
|
|
1378
|
-
const obj = input ?? {};
|
|
1379
|
-
if (toolName === "run_bash") return typeof obj.command === "string" ? obj.command : "";
|
|
1380
|
-
if (typeof obj.path === "string") return obj.path;
|
|
1381
|
-
return "";
|
|
1382
|
-
}
|
|
1383
|
-
function generalizeCommand(command) {
|
|
1384
|
-
const tokens = command.trim().split(/\s+/);
|
|
1385
|
-
if (tokens.length === 0 || tokens[0] === "") return command;
|
|
1386
|
-
const prog = tokens[0];
|
|
1387
|
-
const prefixLen = WRAPPER_PROGRAMS.has(prog) && tokens.length > 1 ? 2 : 1;
|
|
1388
|
-
const prefix = tokens.slice(0, prefixLen).join(" ");
|
|
1389
|
-
return `${prefix} *`;
|
|
1390
|
-
}
|
|
1391
|
-
function patternToPersist(toolName, subject) {
|
|
1392
|
-
return toolName === "run_bash" ? generalizeCommand(subject) : subject;
|
|
1393
|
-
}
|
|
1394
|
-
function globToRegExp(glob2) {
|
|
1395
|
-
const escaped = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1396
|
-
const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1397
|
-
return new RegExp(`^${pattern}$`);
|
|
1398
|
-
}
|
|
1399
|
-
function matches(rule, toolName, subject) {
|
|
1400
|
-
if (rule.tool !== toolName) return false;
|
|
1401
|
-
try {
|
|
1402
|
-
return globToRegExp(rule.pattern).test(subject);
|
|
1403
|
-
} catch {
|
|
1404
|
-
return false;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
async function check(toolName, input, ctx) {
|
|
1408
|
-
if (ALWAYS_ALLOW.has(toolName)) return "allow";
|
|
1409
|
-
const subject = subjectFor(toolName, input);
|
|
1410
|
-
const rules = loadRules();
|
|
1411
|
-
if (rules.some((r) => matches(r, toolName, subject))) return "allow";
|
|
1412
|
-
const answer = await ctx.ask(toolName, input);
|
|
1413
|
-
if (answer === "no") return "deny";
|
|
1414
|
-
if (answer === "always") addRule(toolName, patternToPersist(toolName, subject));
|
|
1415
|
-
return "allow";
|
|
1416
|
-
}
|
|
1417
|
-
var RULES_DIR, RULES_PATH, WRAPPER_PROGRAMS, ALWAYS_ALLOW;
|
|
1418
|
-
var init_policy = __esm({
|
|
1419
|
-
"src/permissions/policy.ts"() {
|
|
1420
|
-
"use strict";
|
|
1421
|
-
RULES_DIR = join7(homedir5(), ".miii");
|
|
1422
|
-
RULES_PATH = join7(RULES_DIR, "permissions.json");
|
|
1423
|
-
WRAPPER_PROGRAMS = /* @__PURE__ */ new Set([
|
|
1424
|
-
"npm",
|
|
1425
|
-
"npx",
|
|
1426
|
-
"pnpm",
|
|
1427
|
-
"yarn",
|
|
1428
|
-
"brew",
|
|
1429
|
-
"pip",
|
|
1430
|
-
"pip3",
|
|
1431
|
-
"cargo",
|
|
1432
|
-
"docker",
|
|
1433
|
-
"kubectl",
|
|
1434
|
-
"go"
|
|
1435
|
-
]);
|
|
1436
|
-
ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
|
|
1437
|
-
}
|
|
1438
|
-
});
|
|
1439
|
-
|
|
1440
1506
|
// src/agent/adapter.ts
|
|
1441
1507
|
function mintToolUseId() {
|
|
1442
1508
|
const rand = Math.random().toString(36).slice(2, 14);
|
|
@@ -1718,7 +1784,10 @@ async function* runAgent(opts) {
|
|
|
1718
1784
|
const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd));
|
|
1719
1785
|
const ollamaTools = toOllamaTools(TOOLS);
|
|
1720
1786
|
const toolNames = TOOLS.map((t) => t.name);
|
|
1721
|
-
const
|
|
1787
|
+
const cfg = loadConfig();
|
|
1788
|
+
const effort = EFFORT_OPTIONS[cfg.effort ?? "medium"];
|
|
1789
|
+
const ctxCap = cfg.numCtxCap && cfg.numCtxCap > 0 ? cfg.numCtxCap : DEFAULT_NUM_CTX_CAP;
|
|
1790
|
+
const cappedCtx = typeof num_ctx === "number" && num_ctx > 0 ? Math.min(num_ctx, ctxCap) : void 0;
|
|
1722
1791
|
const history = [
|
|
1723
1792
|
...opts.history,
|
|
1724
1793
|
{
|
|
@@ -1733,6 +1802,7 @@ async function* runAgent(opts) {
|
|
|
1733
1802
|
let repeatCount = 0;
|
|
1734
1803
|
let leakNudges = 0;
|
|
1735
1804
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
1805
|
+
let endedCleanly = false;
|
|
1736
1806
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
1737
1807
|
let text = "";
|
|
1738
1808
|
let tool_calls;
|
|
@@ -1745,7 +1815,7 @@ async function* runAgent(opts) {
|
|
|
1745
1815
|
const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
|
|
1746
1816
|
if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
|
|
1747
1817
|
try {
|
|
1748
|
-
for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: effort.num_predict, temperature: effort.temperature })) {
|
|
1818
|
+
for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx: cappedCtx, num_predict: effort.num_predict, temperature: effort.temperature })) {
|
|
1749
1819
|
if (signal?.aborted) break;
|
|
1750
1820
|
if (chunk.content) {
|
|
1751
1821
|
text += chunk.content;
|
|
@@ -1801,6 +1871,23 @@ async function* runAgent(opts) {
|
|
|
1801
1871
|
}
|
|
1802
1872
|
const blocks = blocksFromOllama(text, tool_calls, toolNames);
|
|
1803
1873
|
const tool_uses = blocks.filter((b) => b.type === "tool_use");
|
|
1874
|
+
if (tool_uses.length > 0 && !truncated) {
|
|
1875
|
+
const sig = JSON.stringify(
|
|
1876
|
+
blocks.map(
|
|
1877
|
+
(b) => b.type === "tool_use" ? { t: "u", n: b.name, i: b.input } : b.type === "text" ? { t: "t", x: b.text.trim() } : b
|
|
1878
|
+
)
|
|
1879
|
+
);
|
|
1880
|
+
if (sig === lastAssistantSig) {
|
|
1881
|
+
repeatCount++;
|
|
1882
|
+
if (repeatCount >= 2) {
|
|
1883
|
+
yield { type: "error", message: "Agent loop detected: assistant produced identical output 3 turns in a row" };
|
|
1884
|
+
return history;
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
repeatCount = 0;
|
|
1888
|
+
lastAssistantSig = sig;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1804
1891
|
history.push({ role: "assistant", content: blocks });
|
|
1805
1892
|
if (truncated && tool_uses.length > 0) {
|
|
1806
1893
|
const results2 = tool_uses.map((use) => ({
|
|
@@ -1826,24 +1913,10 @@ async function* runAgent(opts) {
|
|
|
1826
1913
|
yield { type: "turn-end", stop_reason: "tool_use" };
|
|
1827
1914
|
continue;
|
|
1828
1915
|
}
|
|
1916
|
+
endedCleanly = true;
|
|
1829
1917
|
yield { type: "turn-end", stop_reason: "end_turn" };
|
|
1830
1918
|
break;
|
|
1831
1919
|
}
|
|
1832
|
-
const sig = JSON.stringify(
|
|
1833
|
-
blocks.map(
|
|
1834
|
-
(b) => b.type === "tool_use" ? { t: "u", n: b.name, i: b.input } : b.type === "text" ? { t: "t", x: b.text.trim() } : b
|
|
1835
|
-
)
|
|
1836
|
-
);
|
|
1837
|
-
if (sig === lastAssistantSig) {
|
|
1838
|
-
repeatCount++;
|
|
1839
|
-
if (repeatCount >= 2) {
|
|
1840
|
-
yield { type: "error", message: "Agent loop detected: assistant produced identical output 3 turns in a row" };
|
|
1841
|
-
return history;
|
|
1842
|
-
}
|
|
1843
|
-
} else {
|
|
1844
|
-
repeatCount = 0;
|
|
1845
|
-
lastAssistantSig = sig;
|
|
1846
|
-
}
|
|
1847
1920
|
for (const u of tool_uses) yield { type: "tool-use", block: u };
|
|
1848
1921
|
const results = [];
|
|
1849
1922
|
for (const use of tool_uses) {
|
|
@@ -1930,6 +2003,12 @@ async function* runAgent(opts) {
|
|
|
1930
2003
|
history.push({ role: "user", content: results });
|
|
1931
2004
|
yield { type: "turn-end", stop_reason: "tool_use" };
|
|
1932
2005
|
}
|
|
2006
|
+
if (!endedCleanly) {
|
|
2007
|
+
yield {
|
|
2008
|
+
type: "error",
|
|
2009
|
+
message: `Stopped after ${MAX_TURNS} tool-use turns \u2014 the task may be incomplete. Send another message to continue where it left off.`
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
1933
2012
|
yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
|
|
1934
2013
|
return history;
|
|
1935
2014
|
}
|
|
@@ -2239,7 +2318,7 @@ var InputBar = memo(function InputBar2({ input, caret, disabled, processingLabel
|
|
|
2239
2318
|
const [frame, setFrame] = useState(0);
|
|
2240
2319
|
useEffect(() => {
|
|
2241
2320
|
if (!disabled) return;
|
|
2242
|
-
const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length),
|
|
2321
|
+
const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 200);
|
|
2243
2322
|
return () => clearInterval(t);
|
|
2244
2323
|
}, [disabled]);
|
|
2245
2324
|
return /* @__PURE__ */ jsx2(
|
|
@@ -2277,7 +2356,7 @@ var InputBar = memo(function InputBar2({ input, caret, disabled, processingLabel
|
|
|
2277
2356
|
// src/ui/ModelsView.tsx
|
|
2278
2357
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
2279
2358
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2280
|
-
function ModelsView({ models, cursor, model, host, provider, effort, query, requireSelection }) {
|
|
2359
|
+
function ModelsView({ models, cursor, model, host, provider, providerType, effort, query, requireSelection }) {
|
|
2281
2360
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
|
|
2282
2361
|
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
|
|
2283
2362
|
/* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
|
|
@@ -2296,7 +2375,10 @@ function ModelsView({ models, cursor, model, host, provider, effort, query, requ
|
|
|
2296
2375
|
] })
|
|
2297
2376
|
] }),
|
|
2298
2377
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "select model" }),
|
|
2299
|
-
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children:
|
|
2378
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.length === 0 ? query ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `no models match "${query}"` }) : provider === "lmstudio" ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no models. load a model in LM Studio and start the server." }) : providerType === "ollama" ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
2379
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no models installed. pull one, then relaunch:" }),
|
|
2380
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: " ollama pull qwen2.5-coder:14b" })
|
|
2381
|
+
] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `no models found at ${host}. make sure the server is running with a model loaded.` }) : models.map((m, i) => {
|
|
2300
2382
|
const sel = i === cursor;
|
|
2301
2383
|
return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", color: sel ? "blue" : void 0, dimColor: !sel, children: [
|
|
2302
2384
|
sel ? "\u276F " : " ",
|
|
@@ -3285,14 +3367,26 @@ function clipTail(rendered, max) {
|
|
|
3285
3367
|
if (lines.length <= max) return { text: rendered, clipped: 0 };
|
|
3286
3368
|
return { text: lines.slice(-max).join("\n"), clipped: lines.length - max };
|
|
3287
3369
|
}
|
|
3370
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
3371
|
+
function stripAnsi(s) {
|
|
3372
|
+
return s.replace(ANSI_RE, "");
|
|
3373
|
+
}
|
|
3374
|
+
function visualHeight(text, width) {
|
|
3375
|
+
const w = Math.max(1, width);
|
|
3376
|
+
let rows = 0;
|
|
3377
|
+
for (const line of text.split("\n")) {
|
|
3378
|
+
rows += Math.max(1, Math.ceil(stripAnsi(line).length / w));
|
|
3379
|
+
}
|
|
3380
|
+
return rows;
|
|
3381
|
+
}
|
|
3288
3382
|
function clipTailVisual(content, maxRows, width) {
|
|
3289
3383
|
const w = Math.max(1, width);
|
|
3290
3384
|
const lines = content.split("\n");
|
|
3291
|
-
const
|
|
3385
|
+
const visualRows2 = (line) => Math.max(1, Math.ceil(stripAnsi(line).length / w));
|
|
3292
3386
|
let rows = 0;
|
|
3293
3387
|
let start = lines.length;
|
|
3294
3388
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3295
|
-
const h =
|
|
3389
|
+
const h = visualRows2(lines[i]);
|
|
3296
3390
|
if (rows + h > maxRows && start < lines.length) break;
|
|
3297
3391
|
rows += h;
|
|
3298
3392
|
start = i;
|
|
@@ -3305,9 +3399,20 @@ function liveFrameRows() {
|
|
|
3305
3399
|
return Math.max(6, rows - 8);
|
|
3306
3400
|
}
|
|
3307
3401
|
var COLLAPSED_LINES = 3;
|
|
3402
|
+
function visualRows(text, width, cap) {
|
|
3403
|
+
const w = Math.max(1, width);
|
|
3404
|
+
let rows = 0;
|
|
3405
|
+
const lines = text.split("\n");
|
|
3406
|
+
for (const line of lines) {
|
|
3407
|
+
rows += Math.max(1, Math.ceil(line.length / w));
|
|
3408
|
+
if (rows >= cap) return cap;
|
|
3409
|
+
}
|
|
3410
|
+
return rows;
|
|
3411
|
+
}
|
|
3308
3412
|
function estimateToolRows(use, result) {
|
|
3309
3413
|
const input = use.input ?? {};
|
|
3310
3414
|
const noErr = !result?.is_error;
|
|
3415
|
+
const w = contentWidth();
|
|
3311
3416
|
if (use.name === "write_file" && noErr) {
|
|
3312
3417
|
const total = countLines(String(input.content ?? ""));
|
|
3313
3418
|
const shown = Math.min(total, COLLAPSED_LINES);
|
|
@@ -3323,10 +3428,10 @@ function estimateToolRows(use, result) {
|
|
|
3323
3428
|
const lines = (result.content ?? "").split("\n");
|
|
3324
3429
|
const multi = (use.name === "run_bash" || use.name === "grep" || use.name === "glob" || result.is_error) && lines.length > 1;
|
|
3325
3430
|
if (multi) {
|
|
3326
|
-
const
|
|
3327
|
-
rows += 1 +
|
|
3431
|
+
const shownLines = lines.slice(0, COLLAPSED_LINES).join("\n");
|
|
3432
|
+
rows += 1 + visualRows(shownLines, w, COLLAPSED_LINES * 4) + (lines.length > COLLAPSED_LINES ? 1 : 0);
|
|
3328
3433
|
} else {
|
|
3329
|
-
rows +=
|
|
3434
|
+
rows += visualRows(lines[0] ?? "", w, 4);
|
|
3330
3435
|
}
|
|
3331
3436
|
}
|
|
3332
3437
|
return rows;
|
|
@@ -3338,6 +3443,7 @@ function contentWidth() {
|
|
|
3338
3443
|
// src/ui/ThinkingBlock.tsx
|
|
3339
3444
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3340
3445
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3446
|
+
var CHALK = "#c9c7c0";
|
|
3341
3447
|
var globalThinkingVisible = false;
|
|
3342
3448
|
var listeners = /* @__PURE__ */ new Set();
|
|
3343
3449
|
function toggleThinkingVisible() {
|
|
@@ -3359,16 +3465,17 @@ function ThinkingBlock({ content }) {
|
|
|
3359
3465
|
const [frame, setFrame] = useState2(0);
|
|
3360
3466
|
const visible = useThinkingVisible();
|
|
3361
3467
|
useEffect2(() => {
|
|
3362
|
-
const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length),
|
|
3468
|
+
const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 100);
|
|
3363
3469
|
return () => clearInterval(t);
|
|
3364
3470
|
}, []);
|
|
3471
|
+
const label = "thinking";
|
|
3365
3472
|
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
|
|
3366
3473
|
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
3367
|
-
/* @__PURE__ */ jsxs8(Text8, { color:
|
|
3474
|
+
/* @__PURE__ */ jsxs8(Text8, { color: CHALK, children: [
|
|
3368
3475
|
FRAMES[frame],
|
|
3369
3476
|
" "
|
|
3370
3477
|
] }),
|
|
3371
|
-
/* @__PURE__ */ jsx8(Text8, {
|
|
3478
|
+
/* @__PURE__ */ jsx8(Text8, { color: CHALK, italic: true, children: label }),
|
|
3372
3479
|
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
3373
3480
|
" \xB7 ctrl+t to ",
|
|
3374
3481
|
visible ? "hide" : "show",
|
|
@@ -3678,6 +3785,7 @@ var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
|
|
|
3678
3785
|
|
|
3679
3786
|
// src/ui/PermissionPrompt.tsx
|
|
3680
3787
|
import { Box as Box11, Text as Text11 } from "ink";
|
|
3788
|
+
init_policy();
|
|
3681
3789
|
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3682
3790
|
function summarizeInput(input) {
|
|
3683
3791
|
if (!input || typeof input !== "object") return "";
|
|
@@ -3699,9 +3807,10 @@ function summarizeInput(input) {
|
|
|
3699
3807
|
}
|
|
3700
3808
|
function PermissionPrompt({ req, cursor }) {
|
|
3701
3809
|
const label = TOOL_LABEL[req.toolName] ?? req.toolName;
|
|
3810
|
+
const rule = patternToPersist(req.toolName, subjectFor(req.toolName, req.input));
|
|
3702
3811
|
const options = [
|
|
3703
3812
|
{ label: "Yes", key: "yes" },
|
|
3704
|
-
{ label: "Yes, don't ask again for this", key: "always" },
|
|
3813
|
+
{ label: rule ? `Yes, don't ask again for ${rule}` : "Yes, don't ask again for this", key: "always" },
|
|
3705
3814
|
{ label: "No", key: "no" }
|
|
3706
3815
|
];
|
|
3707
3816
|
const summary = summarizeInput(req.input);
|
|
@@ -3735,7 +3844,8 @@ function ChatView({
|
|
|
3735
3844
|
permissionCursor = 0,
|
|
3736
3845
|
activeToolUses,
|
|
3737
3846
|
activeToolResults,
|
|
3738
|
-
header
|
|
3847
|
+
header,
|
|
3848
|
+
logEpoch = 0
|
|
3739
3849
|
}) {
|
|
3740
3850
|
const empty = messages.length === 0 && !streaming && !thinking && !pendingPermission && !error;
|
|
3741
3851
|
const log = [];
|
|
@@ -3750,8 +3860,12 @@ function ChatView({
|
|
|
3750
3860
|
let streamNode = null;
|
|
3751
3861
|
let streamRows = 0;
|
|
3752
3862
|
if (streaming && streamingContent) {
|
|
3753
|
-
const
|
|
3754
|
-
|
|
3863
|
+
const raw = clipTail(streamingContent, liveBudget);
|
|
3864
|
+
const width = contentWidth();
|
|
3865
|
+
const rendered = clipTailVisual(renderMarkdownStreaming(raw.text), liveBudget, width);
|
|
3866
|
+
const text = rendered.text;
|
|
3867
|
+
const clipped = raw.clipped + rendered.clipped;
|
|
3868
|
+
streamRows = visualHeight(text, width) + (clipped > 0 ? 1 : 0);
|
|
3755
3869
|
streamNode = /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
|
|
3756
3870
|
clipped > 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: `\u2191 ${clipped} more line${clipped === 1 ? "" : "s"} above \u2014 streaming\u2026` }),
|
|
3757
3871
|
/* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", children: [
|
|
@@ -3781,7 +3895,7 @@ function ChatView({
|
|
|
3781
3895
|
] });
|
|
3782
3896
|
}
|
|
3783
3897
|
return /* @__PURE__ */ jsxs12(Fragment2, { children: [
|
|
3784
|
-
/* @__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) }),
|
|
3898
|
+
/* @__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),
|
|
3785
3899
|
/* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
|
|
3786
3900
|
empty && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
|
|
3787
3901
|
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: EMPTY_STATE_TITLE }),
|
|
@@ -3949,8 +4063,8 @@ function useAgentRunner(model, activeCtx) {
|
|
|
3949
4063
|
case "turn-end": {
|
|
3950
4064
|
flushStream(true);
|
|
3951
4065
|
flushThink(true);
|
|
3952
|
-
setStreaming(false);
|
|
3953
4066
|
if (ev.stop_reason === "tool_use") {
|
|
4067
|
+
setStreaming(false);
|
|
3954
4068
|
flushTurn(null);
|
|
3955
4069
|
setThinking(true);
|
|
3956
4070
|
thinkingAcc = "";
|
|
@@ -4222,6 +4336,7 @@ function useKeyboard(opts) {
|
|
|
4222
4336
|
sessions,
|
|
4223
4337
|
setSessions,
|
|
4224
4338
|
setNotice,
|
|
4339
|
+
setLogEpoch,
|
|
4225
4340
|
switchProvider
|
|
4226
4341
|
} = opts;
|
|
4227
4342
|
const {
|
|
@@ -4255,6 +4370,7 @@ function useKeyboard(opts) {
|
|
|
4255
4370
|
setError(null);
|
|
4256
4371
|
setNotice(null);
|
|
4257
4372
|
clearPasteStore();
|
|
4373
|
+
setLogEpoch((n) => n + 1);
|
|
4258
4374
|
}
|
|
4259
4375
|
const effort = cfg.effort ?? "medium";
|
|
4260
4376
|
useInput((char, key) => {
|
|
@@ -4713,6 +4829,7 @@ function App() {
|
|
|
4713
4829
|
sessionIdRef.current = sessionId;
|
|
4714
4830
|
const [sessions, setSessions] = useState5([]);
|
|
4715
4831
|
const [notice, setNotice] = useState5(null);
|
|
4832
|
+
const [logEpoch, setLogEpoch] = useState5(0);
|
|
4716
4833
|
const [input, setInput] = useState5("");
|
|
4717
4834
|
const [caret, setCaret] = useState5(0);
|
|
4718
4835
|
const [paletteCursor, setPaletteCursor] = useState5(0);
|
|
@@ -4833,6 +4950,7 @@ function App() {
|
|
|
4833
4950
|
sessions,
|
|
4834
4951
|
setSessions,
|
|
4835
4952
|
setNotice,
|
|
4953
|
+
setLogEpoch,
|
|
4836
4954
|
switchProvider
|
|
4837
4955
|
});
|
|
4838
4956
|
const effort = cfg.effort ?? "medium";
|
|
@@ -4844,7 +4962,7 @@ function App() {
|
|
|
4844
4962
|
return Math.round(used / activeCtx * 100);
|
|
4845
4963
|
})();
|
|
4846
4964
|
return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
|
|
4847
|
-
state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable, updateStatus }),
|
|
4965
|
+
state !== "ready" && state !== "sessions" && state !== "models" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable, updateStatus }),
|
|
4848
4966
|
state === "loading" && !agent.error && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
|
|
4849
4967
|
agent.error && state !== "ready" && /* @__PURE__ */ jsx13(
|
|
4850
4968
|
ChatView,
|
|
@@ -4856,7 +4974,7 @@ function App() {
|
|
|
4856
4974
|
error: agent.error
|
|
4857
4975
|
}
|
|
4858
4976
|
),
|
|
4859
|
-
|
|
4977
|
+
state === "select-model" && /* @__PURE__ */ jsx13(
|
|
4860
4978
|
ModelsView,
|
|
4861
4979
|
{
|
|
4862
4980
|
models: filteredModels,
|
|
@@ -4864,9 +4982,10 @@ function App() {
|
|
|
4864
4982
|
model: cfg.model,
|
|
4865
4983
|
host: provEntry.baseUrl,
|
|
4866
4984
|
provider: provName,
|
|
4985
|
+
providerType: provEntry.type,
|
|
4867
4986
|
effort,
|
|
4868
4987
|
query: pickerQuery,
|
|
4869
|
-
requireSelection:
|
|
4988
|
+
requireSelection: true
|
|
4870
4989
|
}
|
|
4871
4990
|
),
|
|
4872
4991
|
state === "providers" && /* @__PURE__ */ jsx13(
|
|
@@ -4878,8 +4997,7 @@ function App() {
|
|
|
4878
4997
|
query: pickerQuery
|
|
4879
4998
|
}
|
|
4880
4999
|
),
|
|
4881
|
-
state === "sessions" && /* @__PURE__ */
|
|
4882
|
-
state === "ready" && /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
5000
|
+
(state === "ready" || state === "sessions" || state === "models") && /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
4883
5001
|
notice && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "green", children: `\u2713 ${notice}` }) }),
|
|
4884
5002
|
/* @__PURE__ */ jsx13(
|
|
4885
5003
|
ChatView,
|
|
@@ -4894,18 +5012,33 @@ function App() {
|
|
|
4894
5012
|
permissionCursor: agent.permissionCursor,
|
|
4895
5013
|
activeToolUses: agent.activeToolUses,
|
|
4896
5014
|
activeToolResults: agent.activeToolResults,
|
|
4897
|
-
header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd })
|
|
5015
|
+
header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd }),
|
|
5016
|
+
logEpoch
|
|
4898
5017
|
}
|
|
4899
5018
|
),
|
|
4900
|
-
input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
|
|
4901
|
-
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` }) }),
|
|
4902
|
-
!input.startsWith("/") && (() => {
|
|
5019
|
+
state === "ready" && input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
|
|
5020
|
+
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` }) }),
|
|
5021
|
+
state === "ready" && !input.startsWith("/") && (() => {
|
|
4903
5022
|
const m = parseMention(input);
|
|
4904
5023
|
if (!m) return null;
|
|
4905
5024
|
return /* @__PURE__ */ jsx13(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
|
|
4906
5025
|
})(),
|
|
4907
|
-
/* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
|
|
4908
|
-
|
|
5026
|
+
state === "ready" && /* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
|
|
5027
|
+
state === "sessions" && /* @__PURE__ */ jsx13(SessionsView, { sessions, cursor }),
|
|
5028
|
+
state === "models" && /* @__PURE__ */ jsx13(
|
|
5029
|
+
ModelsView,
|
|
5030
|
+
{
|
|
5031
|
+
models: filteredModels,
|
|
5032
|
+
cursor,
|
|
5033
|
+
model: cfg.model,
|
|
5034
|
+
host: provEntry.baseUrl,
|
|
5035
|
+
provider: provName,
|
|
5036
|
+
providerType: provEntry.type,
|
|
5037
|
+
effort,
|
|
5038
|
+
query: pickerQuery
|
|
5039
|
+
}
|
|
5040
|
+
),
|
|
5041
|
+
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" }) }),
|
|
4909
5042
|
updateAvailable && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: updateStatus === "failed" ? "red" : updateStatus === "installed" ? "green" : "yellow", children: updateBannerText(updateAvailable, updateStatus) }) })
|
|
4910
5043
|
] })
|
|
4911
5044
|
] });
|
|
@@ -4940,6 +5073,8 @@ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
|
|
|
4940
5073
|
const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
4941
5074
|
process.exit(await runEval2(rest));
|
|
4942
5075
|
} else {
|
|
5076
|
+
const cfgErr = configError();
|
|
5077
|
+
if (cfgErr) console.error(cfgErr);
|
|
4943
5078
|
process.on("exit", () => {
|
|
4944
5079
|
if (process.stdout.isTTY) process.stdout.write("\x1B]2;\x07");
|
|
4945
5080
|
});
|
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.31",
|
|
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": {
|