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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -1
  3. package/dist/cli.js +236 -51
  4. 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`. File tools are strictly confined to your working directory; `../` traversal and absolute paths outside the workspace are rejected.
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
- const root = path ?? process.cwd();
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
- const root = path ?? process.cwd();
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 readFileSync5, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync3, renameSync } from "fs";
1150
- import { join as join6 } from "path";
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 (!existsSync3(RULES_PATH)) return [];
1220
+ if (!existsSync4(RULES_PATH)) return [];
1154
1221
  try {
1155
- const data = JSON.parse(readFileSync5(RULES_PATH, "utf-8"));
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 = join6(homedir5(), ".miii");
1207
- RULES_PATH = join6(RULES_DIR, "permissions.json");
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: 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, NUM_PREDICT, REPEAT_TAIL, REPEAT_KILL;
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 dirname2, join as join7 } from "path";
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(join7(tmpdir(), "miii-eval-"));
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 = join7(dir, rel);
1546
- mkdirSync6(dirname2(abs), { recursive: true });
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 readFileSync6, existsSync as existsSync4 } from "fs";
1609
- import { join as join8 } from "path";
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) => existsSync4(join8(dir, f)) ? readFileSync6(join8(dir, f), "utf-8") : null;
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).trim().slice(0, 80) || "untitled";
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
- async function summarizeMessage(model, text) {
2108
- const fallback = text.trim().slice(0, 80) || "untitled";
2109
- const prompt = `Summarize this user request as a short title, 3-6 words, no punctuation. Reply with the title only.
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
- Request:
2112
- ${text.slice(0, 2e3)}`;
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
- return out.trim().split("\n").filter(Boolean)[0]?.trim() || fallback;
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: "yellow", children: "\u25CF " }),
2368
- /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
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 === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
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: "yellow", children: "\u25CF " }),
2518
- /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
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: "blue", children: "\u25CF " }),
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: "white", children: "\u25CF " }),
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
- if (!agentHistory.length && cfg.model) {
3153
- const id = sessionId;
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
- setInput((s) => {
3343
+ const text = sanitizePaste(char);
3344
+ if (text) setInput((s) => {
3177
3345
  setPaletteCursor(() => 0);
3178
3346
  setFilePickerCursor(() => 0);
3179
- return s + char;
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
- if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
3249
- }, [agent.agentHistory, sessionId]);
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.15",
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
- "agent",
43
+ "llm",
44
+ "local-llm",
45
+ "local-first",
46
+ "offline",
47
+ "privacy",
48
+ "terminal",
49
+ "tui",
34
50
  "ink",
35
- "tui"
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
  }