miii-agent 0.1.15 → 0.1.16
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 +14 -1
- package/dist/cli.js +181 -32
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -120,6 +120,19 @@ Inside the TUI, interact naturally:
|
|
|
120
120
|
| `Ctrl+O` | Toggle full tool output view |
|
|
121
121
|
| `Ctrl+C` | Quit |
|
|
122
122
|
|
|
123
|
+
### Project Instructions (`MIII.md`)
|
|
124
|
+
|
|
125
|
+
Drop a `MIII.md` file in your project and miii reads it first, every turn — the same idea as `CLAUDE.md`. Use it to teach the agent your conventions, build/test commands, architecture, and do's and don'ts.
|
|
126
|
+
|
|
127
|
+
```markdown
|
|
128
|
+
# MIII.md
|
|
129
|
+
- Use tabs, not spaces.
|
|
130
|
+
- Run `npm test` before declaring any task done.
|
|
131
|
+
- The HTTP layer lives in src/server/ — never import it from src/core/.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
miii searches upward from the working directory to the repo root (the directory containing `.git`); the nearest `MIII.md` wins. It is treated as authoritative — higher priority than the agent's defaults — except it can never override the permission system or safety boundaries. Files over 32KB are truncated.
|
|
135
|
+
|
|
123
136
|
---
|
|
124
137
|
|
|
125
138
|
## Technical Deep Dive
|
|
@@ -137,7 +150,7 @@ miii ships with a built-in tool suite that the agent invokes autonomously:
|
|
|
137
150
|
| `grep` | Regex search across files |
|
|
138
151
|
| `run_bash` | Execute shell commands |
|
|
139
152
|
|
|
140
|
-
**Security & Safety:** Every sensitive operation is gated by a permission system. You approve what the agent can touch, and "always" approvals persist to `~/.miii/permissions.json`.
|
|
153
|
+
**Security & Safety:** Every sensitive operation is gated by a permission system. You approve what the agent can touch, and "always" approvals persist to `~/.miii/permissions.json`. The file tools (`read_file`, `write_file`, `edit_file`) are strictly confined to your working directory; `../` traversal and absolute paths outside the workspace are rejected. `run_bash` runs arbitrary shell commands and is **not** path-confined — its only boundary is the permission prompt, so review commands before approving (especially "always").
|
|
141
154
|
|
|
142
155
|
### Lossless Output Spill
|
|
143
156
|
|
package/dist/cli.js
CHANGED
|
@@ -79,10 +79,15 @@ function setEffort(effort) {
|
|
|
79
79
|
function setProvider(provider) {
|
|
80
80
|
saveConfig({ ...readRawConfig(), provider });
|
|
81
81
|
}
|
|
82
|
-
var CONFIG_DIR, CONFIG_PATH;
|
|
82
|
+
var EFFORT_OPTIONS, CONFIG_DIR, CONFIG_PATH;
|
|
83
83
|
var init_config = __esm({
|
|
84
84
|
"src/config.ts"() {
|
|
85
85
|
"use strict";
|
|
86
|
+
EFFORT_OPTIONS = {
|
|
87
|
+
low: { temperature: 0.2, num_predict: 1024 },
|
|
88
|
+
medium: { temperature: 0.7, num_predict: 2048 },
|
|
89
|
+
high: { temperature: 1, num_predict: -1 }
|
|
90
|
+
};
|
|
86
91
|
CONFIG_DIR = join(homedir(), ".miii");
|
|
87
92
|
CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
88
93
|
}
|
|
@@ -850,6 +855,7 @@ var grep;
|
|
|
850
855
|
var init_grep = __esm({
|
|
851
856
|
"src/tools/grep.ts"() {
|
|
852
857
|
"use strict";
|
|
858
|
+
init_paths();
|
|
853
859
|
grep = {
|
|
854
860
|
name: "grep",
|
|
855
861
|
description: "Search file contents for a regex pattern. Uses ripgrep if available, falls back to grep -R.",
|
|
@@ -865,7 +871,12 @@ var init_grep = __esm({
|
|
|
865
871
|
required: ["pattern"]
|
|
866
872
|
},
|
|
867
873
|
handler: async ({ pattern, path, glob: glob2, case_insensitive, max_results }) => {
|
|
868
|
-
|
|
874
|
+
let root;
|
|
875
|
+
try {
|
|
876
|
+
root = confinePath(path ?? ".");
|
|
877
|
+
} catch (err) {
|
|
878
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
879
|
+
}
|
|
869
880
|
const limit = max_results ?? 200;
|
|
870
881
|
const ci = case_insensitive === true || String(case_insensitive) === "true";
|
|
871
882
|
const tryRg = async () => {
|
|
@@ -931,6 +942,7 @@ var glob;
|
|
|
931
942
|
var init_glob = __esm({
|
|
932
943
|
"src/tools/glob.ts"() {
|
|
933
944
|
"use strict";
|
|
945
|
+
init_paths();
|
|
934
946
|
glob = {
|
|
935
947
|
name: "glob",
|
|
936
948
|
description: 'List files matching a glob pattern (e.g. "**/*.ts"). Uses ripgrep --files if available.',
|
|
@@ -944,7 +956,12 @@ var init_glob = __esm({
|
|
|
944
956
|
required: ["pattern"]
|
|
945
957
|
},
|
|
946
958
|
handler: async ({ pattern, path, max_results }) => {
|
|
947
|
-
|
|
959
|
+
let root;
|
|
960
|
+
try {
|
|
961
|
+
root = confinePath(path ?? ".");
|
|
962
|
+
} catch (err) {
|
|
963
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
964
|
+
}
|
|
948
965
|
const limit = max_results ?? 500;
|
|
949
966
|
const tryRg = () => execa3("rg", ["--files", "--hidden", "--glob", pattern, root], {
|
|
950
967
|
reject: false,
|
|
@@ -1056,12 +1073,61 @@ var init_validate = __esm({
|
|
|
1056
1073
|
}
|
|
1057
1074
|
});
|
|
1058
1075
|
|
|
1076
|
+
// src/prompt/context.ts
|
|
1077
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
1078
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
1079
|
+
function findContextFile(cwd) {
|
|
1080
|
+
let dir = cwd;
|
|
1081
|
+
for (; ; ) {
|
|
1082
|
+
const candidate = join6(dir, CONTEXT_FILENAME);
|
|
1083
|
+
if (existsSync3(candidate)) return candidate;
|
|
1084
|
+
if (existsSync3(join6(dir, ".git"))) return null;
|
|
1085
|
+
const parent = dirname2(dir);
|
|
1086
|
+
if (parent === dir) return null;
|
|
1087
|
+
dir = parent;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function loadProjectContext(cwd) {
|
|
1091
|
+
const source = findContextFile(cwd);
|
|
1092
|
+
if (!source) return EMPTY;
|
|
1093
|
+
try {
|
|
1094
|
+
if (statSync3(source).size === 0) return { ...EMPTY, source };
|
|
1095
|
+
const raw = readFileSync5(source, "utf8");
|
|
1096
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_CONTEXT_BYTES) {
|
|
1097
|
+
const clipped = Buffer.from(raw, "utf8").subarray(0, MAX_CONTEXT_BYTES).toString("utf8");
|
|
1098
|
+
return { content: clipped, source, truncated: true };
|
|
1099
|
+
}
|
|
1100
|
+
return { content: raw, source, truncated: false };
|
|
1101
|
+
} catch {
|
|
1102
|
+
return { ...EMPTY, source };
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
var CONTEXT_FILENAME, MAX_CONTEXT_BYTES, EMPTY;
|
|
1106
|
+
var init_context = __esm({
|
|
1107
|
+
"src/prompt/context.ts"() {
|
|
1108
|
+
"use strict";
|
|
1109
|
+
CONTEXT_FILENAME = "MIII.md";
|
|
1110
|
+
MAX_CONTEXT_BYTES = 32 * 1024;
|
|
1111
|
+
EMPTY = { content: "", source: null, truncated: false };
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1059
1115
|
// src/prompt/system.ts
|
|
1060
|
-
function buildSystemPrompt(tools, cwd) {
|
|
1116
|
+
function buildSystemPrompt(tools, cwd, project) {
|
|
1061
1117
|
const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
1118
|
+
const projectSection = project && project.content.trim() ? `
|
|
1119
|
+
# ${CONTEXT_FILENAME} \u2014 project instructions (authoritative, read first)
|
|
1120
|
+
The user maintains ${CONTEXT_FILENAME} at ${project.source} to steer how you work in this project: conventions, commands, architecture, do's and don'ts. Treat it as direct instruction from the user, higher priority than your defaults. When it conflicts with a default rule below, ${CONTEXT_FILENAME} wins (except permissions and safety, which you never override).${project.truncated ? `
|
|
1121
|
+
(Note: file exceeded ${"32KB"} and was truncated.)` : ""}
|
|
1122
|
+
|
|
1123
|
+
--- BEGIN ${CONTEXT_FILENAME} ---
|
|
1124
|
+
${project.content.trim()}
|
|
1125
|
+
--- END ${CONTEXT_FILENAME} ---
|
|
1126
|
+
` : "";
|
|
1062
1127
|
return `You are miii, a senior software engineer running in a terminal.
|
|
1063
1128
|
|
|
1064
1129
|
Working directory: ${cwd}
|
|
1130
|
+
${projectSection}
|
|
1065
1131
|
|
|
1066
1132
|
# Goal Understanding (read this first, every turn)
|
|
1067
1133
|
Before acting on any request, extract and hold three things:
|
|
@@ -1142,17 +1208,18 @@ ${toolLines}
|
|
|
1142
1208
|
var init_system = __esm({
|
|
1143
1209
|
"src/prompt/system.ts"() {
|
|
1144
1210
|
"use strict";
|
|
1211
|
+
init_context();
|
|
1145
1212
|
}
|
|
1146
1213
|
});
|
|
1147
1214
|
|
|
1148
1215
|
// src/permissions/policy.ts
|
|
1149
|
-
import { readFileSync as
|
|
1150
|
-
import { join as
|
|
1216
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync4, renameSync } from "fs";
|
|
1217
|
+
import { join as join7 } from "path";
|
|
1151
1218
|
import { homedir as homedir5 } from "os";
|
|
1152
1219
|
function loadRules() {
|
|
1153
|
-
if (!
|
|
1220
|
+
if (!existsSync4(RULES_PATH)) return [];
|
|
1154
1221
|
try {
|
|
1155
|
-
const data = JSON.parse(
|
|
1222
|
+
const data = JSON.parse(readFileSync6(RULES_PATH, "utf-8"));
|
|
1156
1223
|
return Array.isArray(data.rules) ? data.rules : [];
|
|
1157
1224
|
} catch {
|
|
1158
1225
|
return [];
|
|
@@ -1203,8 +1270,8 @@ var RULES_DIR, RULES_PATH, ALWAYS_ALLOW;
|
|
|
1203
1270
|
var init_policy = __esm({
|
|
1204
1271
|
"src/permissions/policy.ts"() {
|
|
1205
1272
|
"use strict";
|
|
1206
|
-
RULES_DIR =
|
|
1207
|
-
RULES_PATH =
|
|
1273
|
+
RULES_DIR = join7(homedir5(), ".miii");
|
|
1274
|
+
RULES_PATH = join7(RULES_DIR, "permissions.json");
|
|
1208
1275
|
ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
|
|
1209
1276
|
}
|
|
1210
1277
|
});
|
|
@@ -1346,11 +1413,37 @@ var init_adapter = __esm({
|
|
|
1346
1413
|
});
|
|
1347
1414
|
|
|
1348
1415
|
// src/agent/loop.ts
|
|
1416
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1417
|
+
function readGuard(name, input, seen) {
|
|
1418
|
+
if (name !== "edit_file" && name !== "write_file") return null;
|
|
1419
|
+
const p = input.path;
|
|
1420
|
+
if (typeof p !== "string" || !p) return null;
|
|
1421
|
+
let abs;
|
|
1422
|
+
try {
|
|
1423
|
+
abs = confinePath(p);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
if (seen.has(abs)) return null;
|
|
1428
|
+
if (name === "write_file" && !existsSync5(abs)) return null;
|
|
1429
|
+
const verb = name === "edit_file" ? "edit" : "overwrite";
|
|
1430
|
+
return `Refusing to ${verb} ${p}: you have not read it this turn. Call read_file on ${p} first, then retry the ${name}.`;
|
|
1431
|
+
}
|
|
1432
|
+
function markSeen(name, input, seen) {
|
|
1433
|
+
if (name !== "read_file" && name !== "edit_file" && name !== "write_file") return;
|
|
1434
|
+
const p = input.path;
|
|
1435
|
+
if (typeof p !== "string" || !p) return;
|
|
1436
|
+
try {
|
|
1437
|
+
seen.add(confinePath(p));
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1349
1441
|
async function* runAgent(opts) {
|
|
1350
1442
|
const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
|
|
1351
1443
|
const startTime = Date.now();
|
|
1352
|
-
const system = buildSystemPrompt(TOOLS, cwd);
|
|
1444
|
+
const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd));
|
|
1353
1445
|
const ollamaTools = toOllamaTools(TOOLS);
|
|
1446
|
+
const effort = EFFORT_OPTIONS[loadConfig().effort ?? "medium"];
|
|
1354
1447
|
const history = [
|
|
1355
1448
|
...opts.history,
|
|
1356
1449
|
{ role: "user", content: opts.userText }
|
|
@@ -1359,6 +1452,7 @@ async function* runAgent(opts) {
|
|
|
1359
1452
|
let evalTokens = 0;
|
|
1360
1453
|
let lastAssistantSig = "";
|
|
1361
1454
|
let repeatCount = 0;
|
|
1455
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1362
1456
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
1363
1457
|
let text = "";
|
|
1364
1458
|
let tool_calls;
|
|
@@ -1369,7 +1463,7 @@ async function* runAgent(opts) {
|
|
|
1369
1463
|
const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
|
|
1370
1464
|
if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
|
|
1371
1465
|
try {
|
|
1372
|
-
for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict:
|
|
1466
|
+
for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: effort.num_predict, temperature: effort.temperature })) {
|
|
1373
1467
|
if (signal?.aborted) break;
|
|
1374
1468
|
if (chunk.content) {
|
|
1375
1469
|
text += chunk.content;
|
|
@@ -1483,6 +1577,18 @@ async function* runAgent(opts) {
|
|
|
1483
1577
|
yield { type: "tool-result", block: r2 };
|
|
1484
1578
|
continue;
|
|
1485
1579
|
}
|
|
1580
|
+
const guard = readGuard(use.name, use.input, seenPaths);
|
|
1581
|
+
if (guard) {
|
|
1582
|
+
const r2 = {
|
|
1583
|
+
type: "tool_result",
|
|
1584
|
+
tool_use_id: use.id,
|
|
1585
|
+
content: guard,
|
|
1586
|
+
is_error: true
|
|
1587
|
+
};
|
|
1588
|
+
results.push(r2);
|
|
1589
|
+
yield { type: "tool-result", block: r2 };
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1486
1592
|
try {
|
|
1487
1593
|
await hooks?.firePre(use);
|
|
1488
1594
|
} catch {
|
|
@@ -1504,6 +1610,7 @@ async function* runAgent(opts) {
|
|
|
1504
1610
|
is_error: true
|
|
1505
1611
|
};
|
|
1506
1612
|
}
|
|
1613
|
+
if (!r.is_error) markSeen(use.name, use.input, seenPaths);
|
|
1507
1614
|
try {
|
|
1508
1615
|
await hooks?.firePost(use, r);
|
|
1509
1616
|
} catch {
|
|
@@ -1517,18 +1624,20 @@ async function* runAgent(opts) {
|
|
|
1517
1624
|
yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
|
|
1518
1625
|
return history;
|
|
1519
1626
|
}
|
|
1520
|
-
var MAX_TURNS,
|
|
1627
|
+
var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL;
|
|
1521
1628
|
var init_loop = __esm({
|
|
1522
1629
|
"src/agent/loop.ts"() {
|
|
1523
1630
|
"use strict";
|
|
1524
1631
|
init_client();
|
|
1632
|
+
init_paths();
|
|
1525
1633
|
init_registry();
|
|
1526
1634
|
init_validate();
|
|
1527
1635
|
init_system();
|
|
1636
|
+
init_context();
|
|
1528
1637
|
init_policy();
|
|
1638
|
+
init_config();
|
|
1529
1639
|
init_adapter();
|
|
1530
1640
|
MAX_TURNS = 25;
|
|
1531
|
-
NUM_PREDICT = 8192;
|
|
1532
1641
|
REPEAT_TAIL = 120;
|
|
1533
1642
|
REPEAT_KILL = 4;
|
|
1534
1643
|
}
|
|
@@ -1536,14 +1645,14 @@ var init_loop = __esm({
|
|
|
1536
1645
|
|
|
1537
1646
|
// eval/runner.ts
|
|
1538
1647
|
import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
|
|
1539
|
-
import { dirname as
|
|
1648
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1540
1649
|
import { tmpdir } from "os";
|
|
1541
1650
|
async function runScenario(model, s) {
|
|
1542
|
-
const dir = mkdtempSync(
|
|
1651
|
+
const dir = mkdtempSync(join8(tmpdir(), "miii-eval-"));
|
|
1543
1652
|
const prevCwd = process.cwd();
|
|
1544
1653
|
for (const [rel, content] of Object.entries(s.files ?? {})) {
|
|
1545
|
-
const abs =
|
|
1546
|
-
mkdirSync6(
|
|
1654
|
+
const abs = join8(dir, rel);
|
|
1655
|
+
mkdirSync6(dirname3(abs), { recursive: true });
|
|
1547
1656
|
writeFileSync7(abs, content, "utf-8");
|
|
1548
1657
|
}
|
|
1549
1658
|
const r = {
|
|
@@ -1605,13 +1714,13 @@ var init_runner = __esm({
|
|
|
1605
1714
|
});
|
|
1606
1715
|
|
|
1607
1716
|
// eval/scenarios.ts
|
|
1608
|
-
import { readFileSync as
|
|
1609
|
-
import { join as
|
|
1717
|
+
import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
|
|
1718
|
+
import { join as join9 } from "path";
|
|
1610
1719
|
var read, scenarios;
|
|
1611
1720
|
var init_scenarios = __esm({
|
|
1612
1721
|
"eval/scenarios.ts"() {
|
|
1613
1722
|
"use strict";
|
|
1614
|
-
read = (dir, f) =>
|
|
1723
|
+
read = (dir, f) => existsSync6(join9(dir, f)) ? readFileSync7(join9(dir, f), "utf-8") : null;
|
|
1615
1724
|
scenarios = [
|
|
1616
1725
|
{
|
|
1617
1726
|
name: "edit-exact-string",
|
|
@@ -2364,8 +2473,8 @@ function FileEditBlock({
|
|
|
2364
2473
|
const lang = langFromPath(path);
|
|
2365
2474
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2366
2475
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
2367
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2368
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "
|
|
2476
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
2477
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
|
|
2369
2478
|
label,
|
|
2370
2479
|
" "
|
|
2371
2480
|
] }),
|
|
@@ -2514,8 +2623,8 @@ function ToolUseLine({ use, result }) {
|
|
|
2514
2623
|
const { label, arg } = toolHeader(use);
|
|
2515
2624
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2516
2625
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
2517
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2518
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "
|
|
2626
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
2627
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
|
|
2519
2628
|
label,
|
|
2520
2629
|
" "
|
|
2521
2630
|
] }),
|
|
@@ -2528,14 +2637,14 @@ function ToolUseLine({ use, result }) {
|
|
|
2528
2637
|
}
|
|
2529
2638
|
var UserMessage = memo2(function UserMessage2({ msg }) {
|
|
2530
2639
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
2531
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2640
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u276F " }),
|
|
2532
2641
|
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2533
2642
|
] });
|
|
2534
2643
|
});
|
|
2535
2644
|
var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
|
|
2536
2645
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
2537
2646
|
msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
|
|
2538
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2647
|
+
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
|
|
2539
2648
|
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2540
2649
|
] }),
|
|
2541
2650
|
msg.tool_uses?.map((u) => {
|
|
@@ -2858,6 +2967,33 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2858
2967
|
init_config();
|
|
2859
2968
|
import { useInput } from "ink";
|
|
2860
2969
|
var EFFORTS = ["low", "medium", "high"];
|
|
2970
|
+
var PASTE_CHIP_LINES = 4;
|
|
2971
|
+
var PASTE_CHIP_CHARS = 200;
|
|
2972
|
+
var pasteStore = /* @__PURE__ */ new Map();
|
|
2973
|
+
var pasteCounter = 0;
|
|
2974
|
+
function clearPasteStore() {
|
|
2975
|
+
pasteStore.clear();
|
|
2976
|
+
pasteCounter = 0;
|
|
2977
|
+
}
|
|
2978
|
+
function expandPastes(text) {
|
|
2979
|
+
let out = text;
|
|
2980
|
+
for (const [chip, full] of pasteStore) out = out.split(chip).join(full);
|
|
2981
|
+
return out;
|
|
2982
|
+
}
|
|
2983
|
+
function stripControls(chunk) {
|
|
2984
|
+
return chunk.replace(/\x1b\[20[01]~/g, "").replace(/\t/g, " ").replace(/[\x00-\x09\x0b-\x1f\x7f]/g, "");
|
|
2985
|
+
}
|
|
2986
|
+
function sanitizePaste(chunk) {
|
|
2987
|
+
if (chunk.length <= 1) return chunk;
|
|
2988
|
+
const cleaned = stripControls(chunk).replace(/\r/g, "");
|
|
2989
|
+
const lines = cleaned.split("\n").length;
|
|
2990
|
+
if (lines > PASTE_CHIP_LINES || cleaned.length > PASTE_CHIP_CHARS) {
|
|
2991
|
+
const chip = `[Pasted #${++pasteCounter} \xB7 ${lines} line${lines === 1 ? "" : "s"}]`;
|
|
2992
|
+
pasteStore.set(chip, cleaned);
|
|
2993
|
+
return chip;
|
|
2994
|
+
}
|
|
2995
|
+
return cleaned.replace(/\n/g, " ");
|
|
2996
|
+
}
|
|
2861
2997
|
function useKeyboard(opts) {
|
|
2862
2998
|
const {
|
|
2863
2999
|
exit,
|
|
@@ -2913,6 +3049,7 @@ function useKeyboard(opts) {
|
|
|
2913
3049
|
setActiveToolResults([]);
|
|
2914
3050
|
setError(null);
|
|
2915
3051
|
setNotice(null);
|
|
3052
|
+
clearPasteStore();
|
|
2916
3053
|
}
|
|
2917
3054
|
const effort = cfg.effort ?? "medium";
|
|
2918
3055
|
useInput((char, key) => {
|
|
@@ -3094,6 +3231,7 @@ function useKeyboard(opts) {
|
|
|
3094
3231
|
return;
|
|
3095
3232
|
}
|
|
3096
3233
|
if (paletteOpen && key.escape) {
|
|
3234
|
+
clearPasteStore();
|
|
3097
3235
|
setInput(() => "");
|
|
3098
3236
|
setPaletteCursor(() => 0);
|
|
3099
3237
|
return;
|
|
@@ -3149,19 +3287,21 @@ function useKeyboard(opts) {
|
|
|
3149
3287
|
}
|
|
3150
3288
|
} else if (trimmed) {
|
|
3151
3289
|
setNotice(null);
|
|
3290
|
+
const message = expandPastes(trimmed);
|
|
3152
3291
|
if (!agentHistory.length && cfg.model) {
|
|
3153
3292
|
const id = sessionId;
|
|
3154
3293
|
const model = cfg.model;
|
|
3155
3294
|
void (async () => {
|
|
3156
3295
|
try {
|
|
3157
|
-
const title = await summarizeMessage(model,
|
|
3158
|
-
persistSession(id, [{ role: "user", content:
|
|
3296
|
+
const title = await summarizeMessage(model, message);
|
|
3297
|
+
persistSession(id, [{ role: "user", content: message }], title);
|
|
3159
3298
|
} catch {
|
|
3160
3299
|
}
|
|
3161
3300
|
})();
|
|
3162
3301
|
}
|
|
3163
|
-
sendMessage(
|
|
3302
|
+
sendMessage(message);
|
|
3164
3303
|
}
|
|
3304
|
+
clearPasteStore();
|
|
3165
3305
|
setInput(() => "");
|
|
3166
3306
|
setPaletteCursor(() => 0);
|
|
3167
3307
|
return;
|
|
@@ -3170,13 +3310,22 @@ function useKeyboard(opts) {
|
|
|
3170
3310
|
setInput((s) => {
|
|
3171
3311
|
setPaletteCursor(() => 0);
|
|
3172
3312
|
setFilePickerCursor(() => 0);
|
|
3313
|
+
let match = "";
|
|
3314
|
+
for (const chip of pasteStore.keys()) {
|
|
3315
|
+
if (s.endsWith(chip) && chip.length > match.length) match = chip;
|
|
3316
|
+
}
|
|
3317
|
+
if (match) {
|
|
3318
|
+
pasteStore.delete(match);
|
|
3319
|
+
return s.slice(0, -match.length);
|
|
3320
|
+
}
|
|
3173
3321
|
return s.slice(0, -1);
|
|
3174
3322
|
});
|
|
3175
3323
|
} else if (char && !key.ctrl && !key.meta && !key.tab) {
|
|
3176
|
-
|
|
3324
|
+
const text = sanitizePaste(char);
|
|
3325
|
+
if (text) setInput((s) => {
|
|
3177
3326
|
setPaletteCursor(() => 0);
|
|
3178
3327
|
setFilePickerCursor(() => 0);
|
|
3179
|
-
return s +
|
|
3328
|
+
return s + text;
|
|
3180
3329
|
});
|
|
3181
3330
|
}
|
|
3182
3331
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Terminal AI coding agent powered by Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"start": "tsx src/cli.tsx",
|
|
18
18
|
"eval": "tsx src/cli.tsx eval",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
20
22
|
"dev": "tsx watch src/cli.tsx",
|
|
21
23
|
"build": "tsup",
|
|
22
24
|
"postbuild": "node -e \"if(process.platform!=='win32')require('fs').chmodSync('dist/cli.js',0o755)\"",
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"@types/react": "^18.3.0",
|
|
50
52
|
"tsup": "^8.5.1",
|
|
51
53
|
"tsx": "^4.19.0",
|
|
52
|
-
"typescript": "^5.7.0"
|
|
54
|
+
"typescript": "^5.7.0",
|
|
55
|
+
"vitest": "^4.1.9"
|
|
53
56
|
}
|
|
54
57
|
}
|