miii-agent 0.1.15 → 0.1.17
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/LICENSE +21 -0
- package/README.md +14 -1
- package/dist/cli.js +236 -51
- package/package.json +26 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 maruakshay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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",
|
|
@@ -2010,7 +2119,7 @@ function messageText(m) {
|
|
|
2010
2119
|
function firstUserText(messages) {
|
|
2011
2120
|
const first = messages.find((m) => m.role === "user");
|
|
2012
2121
|
if (!first) return "untitled";
|
|
2013
|
-
return messageText(first)
|
|
2122
|
+
return flattenForTitle(messageText(first)).slice(0, 80) || "untitled";
|
|
2014
2123
|
}
|
|
2015
2124
|
function readMeta(id) {
|
|
2016
2125
|
try {
|
|
@@ -2042,6 +2151,10 @@ function persistSession(id, messages, title) {
|
|
|
2042
2151
|
}
|
|
2043
2152
|
writeFileSync2(sessionPath(id), lines.join("\n") + "\n", "utf-8");
|
|
2044
2153
|
}
|
|
2154
|
+
function setSessionTitle(id, title) {
|
|
2155
|
+
if (!readMeta(id)) return;
|
|
2156
|
+
persistSession(id, loadSession(id), title);
|
|
2157
|
+
}
|
|
2045
2158
|
function listSessions() {
|
|
2046
2159
|
if (!existsSync2(SESSION_DIR)) return [];
|
|
2047
2160
|
const metas = [];
|
|
@@ -2104,12 +2217,35 @@ function toDisplayMessages(history) {
|
|
|
2104
2217
|
}
|
|
2105
2218
|
return out;
|
|
2106
2219
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2220
|
+
function flattenForTitle(text) {
|
|
2221
|
+
return text.replace(/<[^>]*>/g, " ").replace(/[`*_#>|]/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/\s+/g, " ").trim();
|
|
2222
|
+
}
|
|
2223
|
+
function looksLikeJunkTitle(title) {
|
|
2224
|
+
return !title || /[<>]/.test(title) || title.length > 80;
|
|
2225
|
+
}
|
|
2226
|
+
async function summarizeConversation(model, messages) {
|
|
2227
|
+
const parts = [];
|
|
2228
|
+
let sawUser = false;
|
|
2229
|
+
let sawAssistant = false;
|
|
2230
|
+
for (const m of messages) {
|
|
2231
|
+
if (m.role === "system") continue;
|
|
2232
|
+
const t = flattenForTitle(messageText(m));
|
|
2233
|
+
if (!t) continue;
|
|
2234
|
+
if (m.role === "user" && !sawUser) {
|
|
2235
|
+
parts.push(`User: ${t}`);
|
|
2236
|
+
sawUser = true;
|
|
2237
|
+
} else if (m.role === "assistant" && !sawAssistant) {
|
|
2238
|
+
parts.push(`Assistant: ${t}`);
|
|
2239
|
+
sawAssistant = true;
|
|
2240
|
+
}
|
|
2241
|
+
if (sawUser && sawAssistant) break;
|
|
2242
|
+
}
|
|
2243
|
+
const convo = parts.join("\n").slice(0, 2e3);
|
|
2244
|
+
const fallback = (parts[0]?.replace(/^User: /, "") ?? "").slice(0, 80) || "untitled";
|
|
2245
|
+
const prompt = `Summarize this conversation as a short title, 3-6 words, no punctuation. Reply with the title only.
|
|
2110
2246
|
|
|
2111
|
-
|
|
2112
|
-
${
|
|
2247
|
+
Conversation:
|
|
2248
|
+
${convo}`;
|
|
2113
2249
|
try {
|
|
2114
2250
|
let out = "";
|
|
2115
2251
|
for await (const chunk of chat3(
|
|
@@ -2120,7 +2256,8 @@ ${text.slice(0, 2e3)}`;
|
|
|
2120
2256
|
)) {
|
|
2121
2257
|
if (chunk.content) out += chunk.content;
|
|
2122
2258
|
}
|
|
2123
|
-
|
|
2259
|
+
const title = out.trim().split("\n").filter(Boolean)[0]?.trim() ?? "";
|
|
2260
|
+
return looksLikeJunkTitle(title) ? fallback : title;
|
|
2124
2261
|
} catch {
|
|
2125
2262
|
return fallback;
|
|
2126
2263
|
}
|
|
@@ -2364,8 +2501,8 @@ function FileEditBlock({
|
|
|
2364
2501
|
const lang = langFromPath(path);
|
|
2365
2502
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2366
2503
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
2367
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2368
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "
|
|
2504
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
2505
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
|
|
2369
2506
|
label,
|
|
2370
2507
|
" "
|
|
2371
2508
|
] }),
|
|
@@ -2386,7 +2523,7 @@ function FileEditBlock({
|
|
|
2386
2523
|
Text9,
|
|
2387
2524
|
{
|
|
2388
2525
|
wrap: "truncate",
|
|
2389
|
-
backgroundColor: ln.sign === "
|
|
2526
|
+
backgroundColor: ln.sign === "-" ? "#3b1414" : ln.sign === "+" && label !== "Write" ? "#13351f" : void 0,
|
|
2390
2527
|
dimColor: ln.sign === " ",
|
|
2391
2528
|
children: [
|
|
2392
2529
|
`${ln.sign} `,
|
|
@@ -2514,8 +2651,8 @@ function ToolUseLine({ use, result }) {
|
|
|
2514
2651
|
const { label, arg } = toolHeader(use);
|
|
2515
2652
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2516
2653
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
2517
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2518
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "
|
|
2654
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
2655
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
|
|
2519
2656
|
label,
|
|
2520
2657
|
" "
|
|
2521
2658
|
] }),
|
|
@@ -2528,14 +2665,14 @@ function ToolUseLine({ use, result }) {
|
|
|
2528
2665
|
}
|
|
2529
2666
|
var UserMessage = memo2(function UserMessage2({ msg }) {
|
|
2530
2667
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
2531
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2668
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u276F " }),
|
|
2532
2669
|
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2533
2670
|
] });
|
|
2534
2671
|
});
|
|
2535
2672
|
var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
|
|
2536
2673
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
2537
2674
|
msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
|
|
2538
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2675
|
+
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
|
|
2539
2676
|
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2540
2677
|
] }),
|
|
2541
2678
|
msg.tool_uses?.map((u) => {
|
|
@@ -2858,6 +2995,33 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2858
2995
|
init_config();
|
|
2859
2996
|
import { useInput } from "ink";
|
|
2860
2997
|
var EFFORTS = ["low", "medium", "high"];
|
|
2998
|
+
var PASTE_CHIP_LINES = 4;
|
|
2999
|
+
var PASTE_CHIP_CHARS = 200;
|
|
3000
|
+
var pasteStore = /* @__PURE__ */ new Map();
|
|
3001
|
+
var pasteCounter = 0;
|
|
3002
|
+
function clearPasteStore() {
|
|
3003
|
+
pasteStore.clear();
|
|
3004
|
+
pasteCounter = 0;
|
|
3005
|
+
}
|
|
3006
|
+
function expandPastes(text) {
|
|
3007
|
+
let out = text;
|
|
3008
|
+
for (const [chip, full] of pasteStore) out = out.split(chip).join(full);
|
|
3009
|
+
return out;
|
|
3010
|
+
}
|
|
3011
|
+
function stripControls(chunk) {
|
|
3012
|
+
return chunk.replace(/\x1b\[20[01]~/g, "").replace(/\t/g, " ").replace(/[\x00-\x09\x0b-\x1f\x7f]/g, "");
|
|
3013
|
+
}
|
|
3014
|
+
function sanitizePaste(chunk) {
|
|
3015
|
+
if (chunk.length <= 1) return chunk;
|
|
3016
|
+
const cleaned = stripControls(chunk).replace(/\r/g, "");
|
|
3017
|
+
const lines = cleaned.split("\n").length;
|
|
3018
|
+
if (lines > PASTE_CHIP_LINES || cleaned.length > PASTE_CHIP_CHARS) {
|
|
3019
|
+
const chip = `[Pasted #${++pasteCounter} \xB7 ${lines} line${lines === 1 ? "" : "s"}]`;
|
|
3020
|
+
pasteStore.set(chip, cleaned);
|
|
3021
|
+
return chip;
|
|
3022
|
+
}
|
|
3023
|
+
return cleaned.replace(/\n/g, " ");
|
|
3024
|
+
}
|
|
2861
3025
|
function useKeyboard(opts) {
|
|
2862
3026
|
const {
|
|
2863
3027
|
exit,
|
|
@@ -2882,6 +3046,7 @@ function useKeyboard(opts) {
|
|
|
2882
3046
|
setFilePickerCursor,
|
|
2883
3047
|
sessionId,
|
|
2884
3048
|
setSessionId,
|
|
3049
|
+
onResumeSession,
|
|
2885
3050
|
sessions,
|
|
2886
3051
|
setSessions,
|
|
2887
3052
|
setNotice,
|
|
@@ -2913,6 +3078,7 @@ function useKeyboard(opts) {
|
|
|
2913
3078
|
setActiveToolResults([]);
|
|
2914
3079
|
setError(null);
|
|
2915
3080
|
setNotice(null);
|
|
3081
|
+
clearPasteStore();
|
|
2916
3082
|
}
|
|
2917
3083
|
const effort = cfg.effort ?? "medium";
|
|
2918
3084
|
useInput((char, key) => {
|
|
@@ -3053,6 +3219,7 @@ function useKeyboard(opts) {
|
|
|
3053
3219
|
setActiveToolResults([]);
|
|
3054
3220
|
setError(null);
|
|
3055
3221
|
setSessionId(meta.id);
|
|
3222
|
+
onResumeSession(meta.id);
|
|
3056
3223
|
setNotice(`resumed \xB7 ${meta.title}`);
|
|
3057
3224
|
setState("ready");
|
|
3058
3225
|
}
|
|
@@ -3094,6 +3261,7 @@ function useKeyboard(opts) {
|
|
|
3094
3261
|
return;
|
|
3095
3262
|
}
|
|
3096
3263
|
if (paletteOpen && key.escape) {
|
|
3264
|
+
clearPasteStore();
|
|
3097
3265
|
setInput(() => "");
|
|
3098
3266
|
setPaletteCursor(() => 0);
|
|
3099
3267
|
return;
|
|
@@ -3149,19 +3317,10 @@ function useKeyboard(opts) {
|
|
|
3149
3317
|
}
|
|
3150
3318
|
} else if (trimmed) {
|
|
3151
3319
|
setNotice(null);
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const model = cfg.model;
|
|
3155
|
-
void (async () => {
|
|
3156
|
-
try {
|
|
3157
|
-
const title = await summarizeMessage(model, trimmed);
|
|
3158
|
-
persistSession(id, [{ role: "user", content: trimmed }], title);
|
|
3159
|
-
} catch {
|
|
3160
|
-
}
|
|
3161
|
-
})();
|
|
3162
|
-
}
|
|
3163
|
-
sendMessage(trimmed);
|
|
3320
|
+
const message = expandPastes(trimmed);
|
|
3321
|
+
sendMessage(message);
|
|
3164
3322
|
}
|
|
3323
|
+
clearPasteStore();
|
|
3165
3324
|
setInput(() => "");
|
|
3166
3325
|
setPaletteCursor(() => 0);
|
|
3167
3326
|
return;
|
|
@@ -3170,13 +3329,22 @@ function useKeyboard(opts) {
|
|
|
3170
3329
|
setInput((s) => {
|
|
3171
3330
|
setPaletteCursor(() => 0);
|
|
3172
3331
|
setFilePickerCursor(() => 0);
|
|
3332
|
+
let match = "";
|
|
3333
|
+
for (const chip of pasteStore.keys()) {
|
|
3334
|
+
if (s.endsWith(chip) && chip.length > match.length) match = chip;
|
|
3335
|
+
}
|
|
3336
|
+
if (match) {
|
|
3337
|
+
pasteStore.delete(match);
|
|
3338
|
+
return s.slice(0, -match.length);
|
|
3339
|
+
}
|
|
3173
3340
|
return s.slice(0, -1);
|
|
3174
3341
|
});
|
|
3175
3342
|
} else if (char && !key.ctrl && !key.meta && !key.tab) {
|
|
3176
|
-
|
|
3343
|
+
const text = sanitizePaste(char);
|
|
3344
|
+
if (text) setInput((s) => {
|
|
3177
3345
|
setPaletteCursor(() => 0);
|
|
3178
3346
|
setFilePickerCursor(() => 0);
|
|
3179
|
-
return s +
|
|
3347
|
+
return s + text;
|
|
3180
3348
|
});
|
|
3181
3349
|
}
|
|
3182
3350
|
}
|
|
@@ -3244,9 +3412,25 @@ function App() {
|
|
|
3244
3412
|
if (v) setUpdateAvailable(v);
|
|
3245
3413
|
});
|
|
3246
3414
|
}, []);
|
|
3415
|
+
const titledSessions = useRef2(/* @__PURE__ */ new Set());
|
|
3247
3416
|
useEffect4(() => {
|
|
3248
|
-
|
|
3249
|
-
|
|
3417
|
+
const history = agent.agentHistory;
|
|
3418
|
+
if (!history.length) return;
|
|
3419
|
+
persistSession(sessionId, history);
|
|
3420
|
+
if (!titledSessions.current.has(sessionId) && cfg.model && history.some((m) => m.role === "assistant")) {
|
|
3421
|
+
titledSessions.current.add(sessionId);
|
|
3422
|
+
const id = sessionId;
|
|
3423
|
+
const model = cfg.model;
|
|
3424
|
+
const snapshot = history;
|
|
3425
|
+
void (async () => {
|
|
3426
|
+
try {
|
|
3427
|
+
const title = await summarizeConversation(model, snapshot);
|
|
3428
|
+
setSessionTitle(id, title);
|
|
3429
|
+
} catch {
|
|
3430
|
+
}
|
|
3431
|
+
})();
|
|
3432
|
+
}
|
|
3433
|
+
}, [agent.agentHistory, sessionId, cfg.model]);
|
|
3250
3434
|
const loadGen = useRef2(0);
|
|
3251
3435
|
const loadModels = (afterProvider = false) => {
|
|
3252
3436
|
const gen = ++loadGen.current;
|
|
@@ -3317,6 +3501,7 @@ function App() {
|
|
|
3317
3501
|
setFilePickerCursor,
|
|
3318
3502
|
sessionId,
|
|
3319
3503
|
setSessionId,
|
|
3504
|
+
onResumeSession: (id) => titledSessions.current.add(id),
|
|
3320
3505
|
sessions,
|
|
3321
3506
|
setSessions,
|
|
3322
3507
|
setNotice,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Terminal AI coding agent powered by Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=18"
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
"start": "tsx src/cli.tsx",
|
|
18
19
|
"eval": "tsx src/cli.tsx eval",
|
|
19
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
20
23
|
"dev": "tsx watch src/cli.tsx",
|
|
21
24
|
"build": "tsup",
|
|
22
25
|
"postbuild": "node -e \"if(process.platform!=='win32')require('fs').chmodSync('dist/cli.js',0o755)\"",
|
|
@@ -26,13 +29,30 @@
|
|
|
26
29
|
"type": "git",
|
|
27
30
|
"url": "git+https://github.com/maruakshay/miii-cli.git"
|
|
28
31
|
},
|
|
32
|
+
"homepage": "https://github.com/maruakshay/miii-cli#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/maruakshay/miii-cli/issues"
|
|
35
|
+
},
|
|
29
36
|
"keywords": [
|
|
30
37
|
"cli",
|
|
31
38
|
"ai",
|
|
39
|
+
"ai-agent",
|
|
40
|
+
"coding-agent",
|
|
41
|
+
"ai-coding-assistant",
|
|
32
42
|
"ollama",
|
|
33
|
-
"
|
|
43
|
+
"llm",
|
|
44
|
+
"local-llm",
|
|
45
|
+
"local-first",
|
|
46
|
+
"offline",
|
|
47
|
+
"privacy",
|
|
48
|
+
"terminal",
|
|
49
|
+
"tui",
|
|
34
50
|
"ink",
|
|
35
|
-
"
|
|
51
|
+
"agent",
|
|
52
|
+
"pair-programming",
|
|
53
|
+
"code-generation",
|
|
54
|
+
"llama-cpp",
|
|
55
|
+
"lm-studio"
|
|
36
56
|
],
|
|
37
57
|
"license": "MIT",
|
|
38
58
|
"dependencies": {
|
|
@@ -49,6 +69,7 @@
|
|
|
49
69
|
"@types/react": "^18.3.0",
|
|
50
70
|
"tsup": "^8.5.1",
|
|
51
71
|
"tsx": "^4.19.0",
|
|
52
|
-
"typescript": "^5.7.0"
|
|
72
|
+
"typescript": "^5.7.0",
|
|
73
|
+
"vitest": "^4.1.9"
|
|
53
74
|
}
|
|
54
75
|
}
|