miii-agent 0.1.28 → 0.1.30

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 +20 -2
  2. package/dist/cli.js +209 -140
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  </p>
16
16
 
17
17
  <p align="center">
18
- <img src="demo.gif" alt="miii demo">
18
+ <img src="demo3.gif" alt="miii demo">
19
19
  </p>
20
20
 
21
21
  ---
@@ -28,13 +28,31 @@ Your code never leaves your disk. There's nothing to log in to. Pull a model, ty
28
28
 
29
29
  ## Try it in 30 seconds
30
30
 
31
+ **macOS / Linux:**
32
+
31
33
  ```bash
32
34
  ollama pull qwen2.5-coder:14b # any coding model works
33
35
  curl -fsSL https://raw.githubusercontent.com/maruakshay/miii-cli/main/install.sh | sh
34
36
  miii
35
37
  ```
36
38
 
37
- Prefer npm? `npm install -g miii-agent`.
39
+ **Windows (PowerShell):**
40
+
41
+ ```powershell
42
+ ollama pull qwen2.5-coder:14b
43
+ irm https://raw.githubusercontent.com/maruakshay/miii-cli/main/install.ps1 | iex
44
+ miii
45
+ ```
46
+
47
+ Prefer npm? `npm install -g miii-agent` works on every platform.
48
+
49
+ > **Install failing on permissions?** Your global npm prefix isn't writable. The
50
+ > installer retries with `sudo` where available; otherwise point npm at a
51
+ > user-owned prefix and re-run:
52
+ > ```bash
53
+ > npm config set prefix "$HOME/.npm-global"
54
+ > export PATH="$HOME/.npm-global/bin:$PATH" # add to ~/.bashrc or ~/.zshrc
55
+ > ```
38
56
 
39
57
  Then just talk to it:
40
58
 
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 };
@@ -1349,94 +1483,6 @@ var init_system = __esm({
1349
1483
  }
1350
1484
  });
1351
1485
 
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
1486
  // src/agent/adapter.ts
1441
1487
  function mintToolUseId() {
1442
1488
  const rand = Math.random().toString(36).slice(2, 14);
@@ -1718,7 +1764,10 @@ async function* runAgent(opts) {
1718
1764
  const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd));
1719
1765
  const ollamaTools = toOllamaTools(TOOLS);
1720
1766
  const toolNames = TOOLS.map((t) => t.name);
1721
- const effort = EFFORT_OPTIONS[loadConfig().effort ?? "medium"];
1767
+ const cfg = loadConfig();
1768
+ const effort = EFFORT_OPTIONS[cfg.effort ?? "medium"];
1769
+ const ctxCap = cfg.numCtxCap && cfg.numCtxCap > 0 ? cfg.numCtxCap : DEFAULT_NUM_CTX_CAP;
1770
+ const cappedCtx = typeof num_ctx === "number" && num_ctx > 0 ? Math.min(num_ctx, ctxCap) : void 0;
1722
1771
  const history = [
1723
1772
  ...opts.history,
1724
1773
  {
@@ -1733,6 +1782,7 @@ async function* runAgent(opts) {
1733
1782
  let repeatCount = 0;
1734
1783
  let leakNudges = 0;
1735
1784
  const seenPaths = /* @__PURE__ */ new Set();
1785
+ let endedCleanly = false;
1736
1786
  for (let turn = 0; turn < MAX_TURNS; turn++) {
1737
1787
  let text = "";
1738
1788
  let tool_calls;
@@ -1745,7 +1795,7 @@ async function* runAgent(opts) {
1745
1795
  const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
1746
1796
  if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
1747
1797
  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 })) {
1798
+ 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
1799
  if (signal?.aborted) break;
1750
1800
  if (chunk.content) {
1751
1801
  text += chunk.content;
@@ -1801,6 +1851,23 @@ async function* runAgent(opts) {
1801
1851
  }
1802
1852
  const blocks = blocksFromOllama(text, tool_calls, toolNames);
1803
1853
  const tool_uses = blocks.filter((b) => b.type === "tool_use");
1854
+ if (tool_uses.length > 0 && !truncated) {
1855
+ const sig = JSON.stringify(
1856
+ blocks.map(
1857
+ (b) => b.type === "tool_use" ? { t: "u", n: b.name, i: b.input } : b.type === "text" ? { t: "t", x: b.text.trim() } : b
1858
+ )
1859
+ );
1860
+ if (sig === lastAssistantSig) {
1861
+ repeatCount++;
1862
+ if (repeatCount >= 2) {
1863
+ yield { type: "error", message: "Agent loop detected: assistant produced identical output 3 turns in a row" };
1864
+ return history;
1865
+ }
1866
+ } else {
1867
+ repeatCount = 0;
1868
+ lastAssistantSig = sig;
1869
+ }
1870
+ }
1804
1871
  history.push({ role: "assistant", content: blocks });
1805
1872
  if (truncated && tool_uses.length > 0) {
1806
1873
  const results2 = tool_uses.map((use) => ({
@@ -1826,24 +1893,10 @@ async function* runAgent(opts) {
1826
1893
  yield { type: "turn-end", stop_reason: "tool_use" };
1827
1894
  continue;
1828
1895
  }
1896
+ endedCleanly = true;
1829
1897
  yield { type: "turn-end", stop_reason: "end_turn" };
1830
1898
  break;
1831
1899
  }
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
1900
  for (const u of tool_uses) yield { type: "tool-use", block: u };
1848
1901
  const results = [];
1849
1902
  for (const use of tool_uses) {
@@ -1930,6 +1983,12 @@ async function* runAgent(opts) {
1930
1983
  history.push({ role: "user", content: results });
1931
1984
  yield { type: "turn-end", stop_reason: "tool_use" };
1932
1985
  }
1986
+ if (!endedCleanly) {
1987
+ yield {
1988
+ type: "error",
1989
+ message: `Stopped after ${MAX_TURNS} tool-use turns \u2014 the task may be incomplete. Send another message to continue where it left off.`
1990
+ };
1991
+ }
1933
1992
  yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1934
1993
  return history;
1935
1994
  }
@@ -2277,7 +2336,7 @@ var InputBar = memo(function InputBar2({ input, caret, disabled, processingLabel
2277
2336
  // src/ui/ModelsView.tsx
2278
2337
  import { Box as Box3, Text as Text3 } from "ink";
2279
2338
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2280
- function ModelsView({ models, cursor, model, host, provider, effort, query, requireSelection }) {
2339
+ function ModelsView({ models, cursor, model, host, provider, providerType, effort, query, requireSelection }) {
2281
2340
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
2282
2341
  /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
2283
2342
  /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
@@ -2296,7 +2355,10 @@ function ModelsView({ models, cursor, model, host, provider, effort, query, requ
2296
2355
  ] })
2297
2356
  ] }),
2298
2357
  /* @__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) => {
2358
+ /* @__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: [
2359
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no models installed. pull one, then relaunch:" }),
2360
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: " ollama pull qwen2.5-coder:14b" })
2361
+ ] }) : /* @__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
2362
  const sel = i === cursor;
2301
2363
  return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", color: sel ? "blue" : void 0, dimColor: !sel, children: [
2302
2364
  sel ? "\u276F " : " ",
@@ -3338,6 +3400,7 @@ function contentWidth() {
3338
3400
  // src/ui/ThinkingBlock.tsx
3339
3401
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
3340
3402
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3403
+ var CHALK = "#c9c7c0";
3341
3404
  var globalThinkingVisible = false;
3342
3405
  var listeners = /* @__PURE__ */ new Set();
3343
3406
  function toggleThinkingVisible() {
@@ -3362,13 +3425,14 @@ function ThinkingBlock({ content }) {
3362
3425
  const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 80);
3363
3426
  return () => clearInterval(t);
3364
3427
  }, []);
3428
+ const label = "thinking";
3365
3429
  return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
3366
3430
  /* @__PURE__ */ jsxs8(Box8, { children: [
3367
- /* @__PURE__ */ jsxs8(Text8, { color: "blue", children: [
3431
+ /* @__PURE__ */ jsxs8(Text8, { color: CHALK, children: [
3368
3432
  FRAMES[frame],
3369
3433
  " "
3370
3434
  ] }),
3371
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: "thinking\u2026" }),
3435
+ /* @__PURE__ */ jsx8(Text8, { color: CHALK, italic: true, children: label }),
3372
3436
  /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3373
3437
  " \xB7 ctrl+t to ",
3374
3438
  visible ? "hide" : "show",
@@ -3678,6 +3742,7 @@ var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
3678
3742
 
3679
3743
  // src/ui/PermissionPrompt.tsx
3680
3744
  import { Box as Box11, Text as Text11 } from "ink";
3745
+ init_policy();
3681
3746
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3682
3747
  function summarizeInput(input) {
3683
3748
  if (!input || typeof input !== "object") return "";
@@ -3699,9 +3764,10 @@ function summarizeInput(input) {
3699
3764
  }
3700
3765
  function PermissionPrompt({ req, cursor }) {
3701
3766
  const label = TOOL_LABEL[req.toolName] ?? req.toolName;
3767
+ const rule = patternToPersist(req.toolName, subjectFor(req.toolName, req.input));
3702
3768
  const options = [
3703
3769
  { label: "Yes", key: "yes" },
3704
- { label: "Yes, don't ask again for this", key: "always" },
3770
+ { label: rule ? `Yes, don't ask again for ${rule}` : "Yes, don't ask again for this", key: "always" },
3705
3771
  { label: "No", key: "no" }
3706
3772
  ];
3707
3773
  const summary = summarizeInput(req.input);
@@ -4864,6 +4930,7 @@ function App() {
4864
4930
  model: cfg.model,
4865
4931
  host: provEntry.baseUrl,
4866
4932
  provider: provName,
4933
+ providerType: provEntry.type,
4867
4934
  effort,
4868
4935
  query: pickerQuery,
4869
4936
  requireSelection: state === "select-model"
@@ -4940,6 +5007,8 @@ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
4940
5007
  const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
4941
5008
  process.exit(await runEval2(rest));
4942
5009
  } else {
5010
+ const cfgErr = configError();
5011
+ if (cfgErr) console.error(cfgErr);
4943
5012
  process.on("exit", () => {
4944
5013
  if (process.stdout.isTTY) process.stdout.write("\x1B]2;\x07");
4945
5014
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Cursor / Claude Code, but local. An offline AI pair-programmer in your terminal, powered by Ollama. Private by default, free forever.",
5
5
  "type": "module",
6
6
  "bin": {