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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/cli.js +181 -32
  3. 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`. 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",
@@ -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: "yellow", children: "\u25CF " }),
2368
- /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
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: "yellow", children: "\u25CF " }),
2518
- /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
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: "blue", children: "\u25CF " }),
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: "white", children: "\u25CF " }),
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, trimmed);
3158
- persistSession(id, [{ role: "user", content: trimmed }], title);
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(trimmed);
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
- setInput((s) => {
3324
+ const text = sanitizePaste(char);
3325
+ if (text) setInput((s) => {
3177
3326
  setPaletteCursor(() => 0);
3178
3327
  setFilePickerCursor(() => 0);
3179
- return s + char;
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.15",
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
  }