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.
- package/README.md +20 -2
- package/dist/cli.js +209 -140
- 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="
|
|
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
|
-
|
|
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/
|
|
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 };
|
|
@@ -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
|
|
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:
|
|
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:
|
|
3431
|
+
/* @__PURE__ */ jsxs8(Text8, { color: CHALK, children: [
|
|
3368
3432
|
FRAMES[frame],
|
|
3369
3433
|
" "
|
|
3370
3434
|
] }),
|
|
3371
|
-
/* @__PURE__ */ jsx8(Text8, {
|
|
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