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.
Files changed (3) hide show
  1. package/README.md +77 -12
  2. package/dist/cli.js +298 -163
  3. 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>Cursor / Claude Code, but local.</strong><br>
5
- An offline AI pair-programmer in your terminal, powered by Ollama.<br>
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 &nbsp;ยท&nbsp;
23
+ ๐Ÿ’ธ <strong>Free</strong> โ€” no API keys, no per-token billing &nbsp;ยท&nbsp;
24
+ โšก <strong>Offline</strong> โ€” runs on your own GPU
19
25
  </p>
20
26
 
21
27
  ---
22
28
 
23
- ## What is this?
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
- ## Try it in 30 seconds
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
- Prefer npm? `npm install -g miii-agent`.
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 decomposes the problem, calls tools, and checks its own work before claiming victory.
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
- ## A few things that make it fun
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/tools/paths.ts
539
- import { resolve, relative as relative2, isAbsolute, sep, join as join4 } from "path";
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(join4(homedir3(), ".miii", "output"));
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 readFileSync3, writeFileSync as writeFileSync3 } from "fs";
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 = readFileSync3(abs, "utf-8");
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
- writeFileSync3(abs, out2, "utf-8");
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
- writeFileSync3(abs, out, "utf-8");
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 readFileSync4 } from "fs";
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 = readFileSync4(confinePath(path));
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 writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
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
- mkdirSync3(dirname(abs), { recursive: true });
800
- writeFileSync4(abs, content, "utf-8");
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 writeFileSync5, mkdirSync as mkdirSync4, rmSync as rmSync2, readdirSync as readdirSync3, statSync } from "fs";
812
- import { join as join5 } from "path";
813
- import { homedir as homedir4 } from "os";
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
- mkdirSync4(OUTPUT_DIR, { recursive: true });
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 = join5(ensureDir(), `${id}.txt`);
956
+ const file = join6(ensureDir(), `${id}.txt`);
823
957
  let path = file;
824
958
  try {
825
- writeFileSync5(file, full, "utf-8");
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 = join5(OUTPUT_DIR, name);
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 = join5(homedir4(), ".miii", "output");
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 30000)" }
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 ?? 3e4,
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 existsSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
1191
- import { dirname as dirname2, join as join6 } from "path";
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 = join6(dir, CONTEXT_FILENAME);
1196
- if (existsSync3(candidate)) return candidate;
1197
- if (existsSync3(join6(dir, ".git"))) return null;
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 = readFileSync5(source, "utf8");
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 effort = EFFORT_OPTIONS[loadConfig().effort ?? "medium"];
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), 150);
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: query ? `no models match "${query}"` : provider === "lmstudio" ? "no models. load a model in LM Studio and start the server." : "no models found." }) : models.map((m, i) => {
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 visualRows = (line) => Math.max(1, Math.ceil(line.length / w));
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 = visualRows(lines[i]);
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 shown = Math.min(lines.length, COLLAPSED_LINES);
3327
- rows += 1 + shown + (lines.length > shown ? 1 : 0);
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 += 1;
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), 80);
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: "blue", children: [
3474
+ /* @__PURE__ */ jsxs8(Text8, { color: CHALK, children: [
3368
3475
  FRAMES[frame],
3369
3476
  " "
3370
3477
  ] }),
3371
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: "thinking\u2026" }),
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 { text, clipped } = clipTail(renderMarkdownStreaming(streamingContent), liveBudget);
3754
- streamRows = text.split("\n").length + (clipped > 0 ? 1 : 0);
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
- (state === "select-model" || state === "models") && /* @__PURE__ */ jsx13(
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: state === "select-model"
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__ */ jsx13(SessionsView, { sessions, cursor }),
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
- !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" }) }),
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.29",
4
- "description": "Cursor / Claude Code, but local. An offline AI pair-programmer in your terminal, powered by Ollama. Private by default, free forever.",
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": {