miii-agent 0.1.9 → 0.1.11

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 (2) hide show
  1. package/dist/cli.js +209 -60
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -145,27 +145,64 @@ var init_client = __esm({
145
145
  });
146
146
 
147
147
  // src/tools/paths.ts
148
- import { resolve, relative as relative2, isAbsolute, sep } from "path";
148
+ import { resolve, relative as relative2, isAbsolute, sep, join as join4 } from "path";
149
+ import { homedir as homedir3 } from "os";
150
+ function isUnder(parent, child) {
151
+ const rel = relative2(parent, child);
152
+ return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !isAbsolute(rel);
153
+ }
149
154
  function confinePath(p) {
150
155
  if (typeof p !== "string" || p.length === 0) {
151
156
  throw new Error("Path is required.");
152
157
  }
153
158
  const root = process.cwd();
154
159
  const abs = resolve(root, p);
155
- const rel = relative2(root, abs);
156
- if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
157
- throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
160
+ if (isUnder(root, abs) || isUnder(SPILL_DIR, abs)) {
161
+ return abs;
158
162
  }
159
- return abs;
163
+ throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
160
164
  }
165
+ var SPILL_DIR;
161
166
  var init_paths = __esm({
162
167
  "src/tools/paths.ts"() {
163
168
  "use strict";
169
+ SPILL_DIR = resolve(join4(homedir3(), ".miii", "output"));
164
170
  }
165
171
  });
166
172
 
167
173
  // src/tools/edit_file.ts
168
174
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
175
+ function similarity(a, b) {
176
+ const x = a.trim();
177
+ const y = b.trim();
178
+ if (!x && !y) return 1;
179
+ const len = Math.max(x.length, y.length);
180
+ if (len === 0) return 0;
181
+ let same = 0;
182
+ for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] === y[i]) same++;
183
+ return same / len;
184
+ }
185
+ function nearMiss(src, old_str) {
186
+ const srcLines = src.split("\n");
187
+ const needle = old_str.split("\n").find((l) => l.trim()) ?? old_str;
188
+ let bestIdx = -1;
189
+ let bestScore = 0;
190
+ for (let i = 0; i < srcLines.length; i++) {
191
+ const s = similarity(srcLines[i], needle);
192
+ if (s > bestScore) {
193
+ bestScore = s;
194
+ bestIdx = i;
195
+ }
196
+ }
197
+ if (bestIdx === -1 || bestScore < 0.4) return "";
198
+ const from = Math.max(0, bestIdx - 3);
199
+ const to = Math.min(srcLines.length, bestIdx + 4);
200
+ const width = String(to).length;
201
+ const ctx = srcLines.slice(from, to).map((l, i) => `${String(from + i + 1).padStart(width, " ")} ${l}`).join("\n");
202
+ return `
203
+ Closest text in file (lines ${from + 1}-${to}):
204
+ ${ctx}`;
205
+ }
169
206
  var edit_file;
170
207
  var init_edit_file = __esm({
171
208
  "src/tools/edit_file.ts"() {
@@ -173,29 +210,39 @@ var init_edit_file = __esm({
173
210
  init_paths();
174
211
  edit_file = {
175
212
  name: "edit_file",
176
- description: "Replace an exact string in a file. old_str must be unique.",
213
+ description: "Replace an exact string in a file. old_str must be unique unless replace_all is set. On no match, returns the closest text in the file.",
177
214
  input_schema: {
178
215
  type: "object",
179
216
  properties: {
180
217
  path: { type: "string", description: "File path" },
181
- old_str: { type: "string", description: "Exact text to replace" },
182
- new_str: { type: "string", description: "Replacement text" }
218
+ old_str: { type: "string", description: "Exact text to replace (whitespace-sensitive)" },
219
+ new_str: { type: "string", description: "Replacement text" },
220
+ replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring uniqueness" }
183
221
  },
184
222
  required: ["path", "old_str", "new_str"]
185
223
  },
186
- handler: ({ path, old_str, new_str }) => {
224
+ handler: ({ path, old_str, new_str, replace_all }) => {
187
225
  try {
226
+ if (old_str === new_str) {
227
+ return { content: `old_str and new_str are identical \u2014 nothing to change in ${path}.`, is_error: true };
228
+ }
188
229
  const abs = confinePath(path);
189
230
  const src = readFileSync3(abs, "utf-8");
190
231
  const first = src.indexOf(old_str);
191
232
  if (first === -1) {
192
- return { content: `old_str not found in ${path}`, is_error: true };
233
+ return { content: `old_str not found in ${path}.${nearMiss(src, old_str)}`, is_error: true };
193
234
  }
194
- if (src.indexOf(old_str, first + 1) !== -1) {
195
- return { content: `old_str not unique in ${path}`, is_error: true };
235
+ const all = replace_all === true;
236
+ if (!all && src.indexOf(old_str, first + 1) !== -1) {
237
+ return {
238
+ content: `old_str not unique in ${path}. Add surrounding context to disambiguate, or set replace_all.`,
239
+ is_error: true
240
+ };
196
241
  }
197
- writeFileSync3(abs, src.slice(0, first) + new_str + src.slice(first + old_str.length), "utf-8");
198
- return { content: `Edited ${path}` };
242
+ const out = all ? src.split(old_str).join(new_str) : src.slice(0, first) + new_str + src.slice(first + old_str.length);
243
+ const n = all ? src.split(old_str).length - 1 : 1;
244
+ writeFileSync3(abs, out, "utf-8");
245
+ return { content: `Edited ${path}${all ? ` (${n} occurrences)` : ""}` };
199
246
  } catch (err) {
200
247
  return { content: err instanceof Error ? err.message : String(err), is_error: true };
201
248
  }
@@ -206,6 +253,10 @@ var init_edit_file = __esm({
206
253
 
207
254
  // src/tools/read_file.ts
208
255
  import { readFileSync as readFileSync4 } from "fs";
256
+ function numbered(lines, start) {
257
+ const width = String(start + lines.length - 1).length;
258
+ return lines.map((l, i) => `${String(start + i).padStart(width, " ")} ${l}`).join("\n");
259
+ }
209
260
  var read_file;
210
261
  var init_read_file = __esm({
211
262
  "src/tools/read_file.ts"() {
@@ -213,21 +264,40 @@ var init_read_file = __esm({
213
264
  init_paths();
214
265
  read_file = {
215
266
  name: "read_file",
216
- description: "Read entire file contents as UTF-8 text.",
267
+ description: "Read file contents as UTF-8 text with line numbers. Use offset/limit to read a range of a large file instead of the whole thing.",
217
268
  input_schema: {
218
269
  type: "object",
219
270
  properties: {
220
- path: { type: "string", description: "File path" }
271
+ path: { type: "string", description: "File path" },
272
+ offset: { type: "number", description: "1-based line to start from (default 1)" },
273
+ limit: { type: "number", description: "Max lines to return (default all / capped)" }
221
274
  },
222
275
  required: ["path"]
223
276
  },
224
- handler: ({ path }) => {
277
+ handler: ({ path, offset, limit }) => {
225
278
  try {
226
- const MAX = 2e5;
227
- const raw = readFileSync4(confinePath(path), "utf-8");
228
- const truncated = raw.length > MAX;
229
- const body = truncated ? raw.slice(0, MAX) + `
230
- [truncated: ${raw.length - MAX} more chars]` : raw;
279
+ const MAX_CHARS = 2e5;
280
+ const buf = readFileSync4(confinePath(path));
281
+ if (buf.subarray(0, 8e3).includes(0)) {
282
+ return { content: `${path} looks binary (${buf.length} bytes); not reading as text.`, is_error: true };
283
+ }
284
+ const raw = buf.toString("utf-8").replace(/\r\n/g, "\n");
285
+ const allLines = raw.split("\n");
286
+ const total = allLines.length;
287
+ const start = Math.max(1, Math.floor(offset ?? 1));
288
+ const ranged = offset != null || limit != null;
289
+ const count = limit != null ? Math.max(0, Math.floor(limit)) : total;
290
+ const slice = allLines.slice(start - 1, start - 1 + count);
291
+ let body = numbered(slice, start);
292
+ if (body.length > MAX_CHARS) {
293
+ body = body.slice(0, MAX_CHARS) + `
294
+ [truncated: output exceeded ${MAX_CHARS} chars \u2014 use offset/limit]`;
295
+ }
296
+ if (ranged) {
297
+ const end = start - 1 + slice.length;
298
+ body += `
299
+ [showing lines ${start}-${end} of ${total}]`;
300
+ }
231
301
  return { content: body };
232
302
  } catch (err) {
233
303
  return { content: err instanceof Error ? err.message : String(err), is_error: true };
@@ -270,12 +340,63 @@ var init_write_file = __esm({
270
340
  }
271
341
  });
272
342
 
343
+ // src/tools/spill.ts
344
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, rmSync as rmSync2, readdirSync as readdirSync3, statSync } from "fs";
345
+ import { join as join5 } from "path";
346
+ import { homedir as homedir4 } from "os";
347
+ import { randomBytes } from "crypto";
348
+ function ensureDir() {
349
+ mkdirSync4(OUTPUT_DIR, { recursive: true });
350
+ return OUTPUT_DIR;
351
+ }
352
+ function spillIfLarge(full, label = "output", budget = INLINE_BUDGET) {
353
+ if (full.length <= budget) return full;
354
+ const id = randomBytes(6).toString("hex");
355
+ const file = join5(ensureDir(), `${id}.txt`);
356
+ let path = file;
357
+ try {
358
+ writeFileSync5(file, full, "utf-8");
359
+ } catch {
360
+ path = "";
361
+ }
362
+ const head = Math.floor(budget * HEAD_FRACTION);
363
+ const tail = budget - head;
364
+ const totalLines = full.split("\n").length;
365
+ const preview = full.slice(0, head) + "\n\u2026\n" + full.slice(-tail);
366
+ const notice = path ? `[${label} truncated: ${totalLines} lines / ${full.length} bytes. Full output at ${path} \u2014 read it with read_file offset/limit to see the elided middle.]` : `[${label} truncated to ${budget} bytes; spill to disk failed, middle is lost.]`;
367
+ return `${preview}
368
+ ${notice}`;
369
+ }
370
+ function cleanupSpill(maxAgeMs = 24 * 60 * 60 * 1e3) {
371
+ try {
372
+ const now = Date.now();
373
+ for (const name of readdirSync3(OUTPUT_DIR)) {
374
+ const f = join5(OUTPUT_DIR, name);
375
+ try {
376
+ if (now - statSync(f).mtimeMs > maxAgeMs) rmSync2(f, { force: true });
377
+ } catch {
378
+ }
379
+ }
380
+ } catch {
381
+ }
382
+ }
383
+ var OUTPUT_DIR, INLINE_BUDGET, HEAD_FRACTION;
384
+ var init_spill = __esm({
385
+ "src/tools/spill.ts"() {
386
+ "use strict";
387
+ OUTPUT_DIR = join5(homedir4(), ".miii", "output");
388
+ INLINE_BUDGET = 1e4;
389
+ HEAD_FRACTION = 0.3;
390
+ }
391
+ });
392
+
273
393
  // src/tools/run_bash.ts
274
394
  import { execa } from "execa";
275
395
  var run_bash;
276
396
  var init_run_bash = __esm({
277
397
  "src/tools/run_bash.ts"() {
278
398
  "use strict";
399
+ init_spill();
279
400
  run_bash = {
280
401
  name: "run_bash",
281
402
  description: "Execute a shell command (bash on Unix, cmd on Windows). Returns stdout+stderr. Non-interactive only.",
@@ -300,10 +421,10 @@ var init_run_bash = __esm({
300
421
  const out = [stdout, stderr].filter(Boolean).join("\n");
301
422
  const is_error = exitCode !== 0;
302
423
  const body = out || (is_error ? `(no output)` : "");
303
- const content = is_error ? `${body}
304
- [exit ${exitCode}]` : body;
424
+ const content = `${spillIfLarge(body, "command output")}
425
+ [exit ${exitCode}]`;
305
426
  return {
306
- content: content.slice(0, 32e3),
427
+ content,
307
428
  is_error
308
429
  };
309
430
  } catch (err) {
@@ -329,7 +450,7 @@ var init_grep = __esm({
329
450
  pattern: { type: "string", description: "Regex pattern" },
330
451
  path: { type: "string", description: "Root path to search (default cwd)" },
331
452
  glob: { type: "string", description: 'File glob filter, e.g. "*.ts"' },
332
- case_insensitive: { type: "string", description: 'Set "true" for case-insensitive' },
453
+ case_insensitive: { type: "boolean", description: "Case-insensitive match" },
333
454
  max_results: { type: "number", description: "Max matching lines (default 200)" }
334
455
  },
335
456
  required: ["pattern"]
@@ -377,8 +498,25 @@ var init_grep = __esm({
377
498
 
378
499
  // src/tools/glob.ts
379
500
  import { execa as execa3 } from "execa";
380
- function globToFindName(glob2) {
381
- return glob2;
501
+ import { statSync as statSync2 } from "fs";
502
+ function byMtimeDesc(paths) {
503
+ const mtime = /* @__PURE__ */ new Map();
504
+ for (const p of paths) {
505
+ try {
506
+ mtime.set(p, statSync2(p).mtimeMs);
507
+ } catch {
508
+ mtime.set(p, 0);
509
+ }
510
+ }
511
+ return [...paths].sort((a, b) => (mtime.get(b) ?? 0) - (mtime.get(a) ?? 0));
512
+ }
513
+ function globToFindArgs(root, glob2) {
514
+ const stripped = glob2.replace(/^\*\*\//, "");
515
+ if (!stripped.includes("/")) {
516
+ return [root, "-type", "f", "-name", stripped];
517
+ }
518
+ const pathPat = "*/" + glob2.replace(/\*\*/g, "*");
519
+ return [root, "-type", "f", "-path", pathPat];
382
520
  }
383
521
  var glob;
384
522
  var init_glob = __esm({
@@ -403,13 +541,10 @@ var init_glob = __esm({
403
541
  reject: false,
404
542
  timeout: 2e4
405
543
  });
406
- const tryFind = () => {
407
- const name = globToFindName(pattern.replace(/^\*\*\//, ""));
408
- return execa3("find", [root, "-type", "f", "-name", name], {
409
- reject: false,
410
- timeout: 2e4
411
- });
412
- };
544
+ const tryFind = () => execa3("find", globToFindArgs(root, pattern), {
545
+ reject: false,
546
+ timeout: 2e4
547
+ });
413
548
  try {
414
549
  let res;
415
550
  try {
@@ -420,8 +555,9 @@ var init_glob = __esm({
420
555
  } catch {
421
556
  res = await tryFind();
422
557
  }
423
- const lines = (res.stdout ?? "").split("\n").filter(Boolean).slice(0, limit);
424
- if (lines.length === 0) return { content: "No files matched." };
558
+ const all = (res.stdout ?? "").split("\n").filter(Boolean);
559
+ if (all.length === 0) return { content: "No files matched." };
560
+ const lines = byMtimeDesc(all).slice(0, limit);
425
561
  return { content: lines.join("\n") };
426
562
  } catch (err) {
427
563
  return { content: err instanceof Error ? err.message : String(err), is_error: true };
@@ -576,10 +712,14 @@ ${toolLines}
576
712
  # Rules
577
713
  - Always read a file before updating it. Never edit, overwrite, or create-over a file you have not read first this turn.
578
714
  - Prefer editing existing files over creating new ones.
579
- - For edit_file, ensure old_str is unique within the target file.
715
+ - For edit_file, make old_str unique by including surrounding context, or set replace_all to change every occurrence.
580
716
  - Never invent file paths. Read, glob, or grep before editing.
581
717
  - No filler, no pleasantries, no apologies.
582
718
 
719
+ # Context discipline
720
+ - read_file returns line numbers and accepts offset/limit. For large files, grep or glob to the relevant region first, then read only that range with offset/limit. Do not read a whole large file when you need a few functions \u2014 it wastes the context window.
721
+ - Reference code by the line numbers read_file returns.
722
+
583
723
  # Testing and verification
584
724
  - Always test the code after a change. Run the project's tests (e.g. npm test, pytest, go test) or the relevant script via run_bash before declaring a task done.
585
725
  - If no test exists for the change, run the affected entry point via run_bash to verify it behaves correctly.
@@ -597,9 +737,9 @@ var init_system = __esm({
597
737
  });
598
738
 
599
739
  // src/permissions/policy.ts
600
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync3, renameSync } from "fs";
601
- import { join as join4 } from "path";
602
- import { homedir as homedir3 } from "os";
740
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync3, renameSync } from "fs";
741
+ import { join as join6 } from "path";
742
+ import { homedir as homedir5 } from "os";
603
743
  function loadRules() {
604
744
  if (!existsSync3(RULES_PATH)) return [];
605
745
  try {
@@ -610,9 +750,9 @@ function loadRules() {
610
750
  }
611
751
  }
612
752
  function saveRules(rules) {
613
- mkdirSync4(RULES_DIR, { recursive: true });
753
+ mkdirSync5(RULES_DIR, { recursive: true });
614
754
  const tmp = RULES_PATH + ".tmp";
615
- writeFileSync5(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
755
+ writeFileSync6(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
616
756
  renameSync(tmp, RULES_PATH);
617
757
  }
618
758
  function addRule(tool, pattern) {
@@ -641,6 +781,7 @@ function matches(rule, toolName, subject) {
641
781
  }
642
782
  }
643
783
  async function check(toolName, input, ctx) {
784
+ if (ALWAYS_ALLOW.has(toolName)) return "allow";
644
785
  const subject = subjectFor(toolName, input);
645
786
  const rules = loadRules();
646
787
  if (rules.some((r) => matches(r, toolName, subject))) return "allow";
@@ -649,12 +790,13 @@ async function check(toolName, input, ctx) {
649
790
  if (answer === "always") addRule(toolName, subject);
650
791
  return "allow";
651
792
  }
652
- var RULES_DIR, RULES_PATH;
793
+ var RULES_DIR, RULES_PATH, ALWAYS_ALLOW;
653
794
  var init_policy = __esm({
654
795
  "src/permissions/policy.ts"() {
655
796
  "use strict";
656
- RULES_DIR = join4(homedir3(), ".miii");
657
- RULES_PATH = join4(RULES_DIR, "permissions.json");
797
+ RULES_DIR = join6(homedir5(), ".miii");
798
+ RULES_PATH = join6(RULES_DIR, "permissions.json");
799
+ ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
658
800
  }
659
801
  });
660
802
 
@@ -983,16 +1125,16 @@ var init_loop = __esm({
983
1125
  });
984
1126
 
985
1127
  // eval/runner.ts
986
- import { mkdtempSync, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, rmSync as rmSync2 } from "fs";
987
- import { dirname as dirname2, join as join5 } from "path";
1128
+ import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
1129
+ import { dirname as dirname2, join as join7 } from "path";
988
1130
  import { tmpdir } from "os";
989
1131
  async function runScenario(model, s) {
990
- const dir = mkdtempSync(join5(tmpdir(), "miii-eval-"));
1132
+ const dir = mkdtempSync(join7(tmpdir(), "miii-eval-"));
991
1133
  const prevCwd = process.cwd();
992
1134
  for (const [rel, content] of Object.entries(s.files ?? {})) {
993
- const abs = join5(dir, rel);
994
- mkdirSync5(dirname2(abs), { recursive: true });
995
- writeFileSync6(abs, content, "utf-8");
1135
+ const abs = join7(dir, rel);
1136
+ mkdirSync6(dirname2(abs), { recursive: true });
1137
+ writeFileSync7(abs, content, "utf-8");
996
1138
  }
997
1139
  const r = {
998
1140
  name: s.name,
@@ -1030,7 +1172,7 @@ async function runScenario(model, s) {
1030
1172
  r.durationMs = Date.now() - start;
1031
1173
  if (r.error) {
1032
1174
  r.reason = `loop error: ${r.error}`;
1033
- rmSync2(dir, { recursive: true, force: true });
1175
+ rmSync3(dir, { recursive: true, force: true });
1034
1176
  return r;
1035
1177
  }
1036
1178
  try {
@@ -1040,7 +1182,7 @@ async function runScenario(model, s) {
1040
1182
  } catch (err) {
1041
1183
  r.reason = `check threw: ${err instanceof Error ? err.message : String(err)}`;
1042
1184
  }
1043
- rmSync2(dir, { recursive: true, force: true });
1185
+ rmSync3(dir, { recursive: true, force: true });
1044
1186
  return r;
1045
1187
  }
1046
1188
  var autoYes;
@@ -1054,12 +1196,12 @@ var init_runner = __esm({
1054
1196
 
1055
1197
  // eval/scenarios.ts
1056
1198
  import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
1057
- import { join as join6 } from "path";
1199
+ import { join as join8 } from "path";
1058
1200
  var read, scenarios;
1059
1201
  var init_scenarios = __esm({
1060
1202
  "eval/scenarios.ts"() {
1061
1203
  "use strict";
1062
- read = (dir, f) => existsSync4(join6(dir, f)) ? readFileSync6(join6(dir, f), "utf-8") : null;
1204
+ read = (dir, f) => existsSync4(join8(dir, f)) ? readFileSync6(join8(dir, f), "utf-8") : null;
1063
1205
  scenarios = [
1064
1206
  {
1065
1207
  name: "edit-exact-string",
@@ -1208,7 +1350,7 @@ import { createElement } from "react";
1208
1350
  init_client();
1209
1351
  import { useState as useState4, useEffect as useEffect3 } from "react";
1210
1352
  import { Box as Box10, Text as Text10, useApp } from "ink";
1211
- import { homedir as homedir4 } from "os";
1353
+ import { homedir as homedir6 } from "os";
1212
1354
  import { sep as sep2 } from "path";
1213
1355
 
1214
1356
  // src/config.ts
@@ -2495,7 +2637,7 @@ async function checkForUpdate() {
2495
2637
  import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2496
2638
  function App() {
2497
2639
  const { exit } = useApp();
2498
- const cwd = process.cwd().replace(homedir4(), "~").split(sep2).join("/");
2640
+ const cwd = process.cwd().replace(homedir6(), "~").split(sep2).join("/");
2499
2641
  const [cfg, setCfg] = useState4(loadConfig());
2500
2642
  const [models, setModels] = useState4([]);
2501
2643
  const [contexts, setContexts] = useState4({});
@@ -2572,7 +2714,7 @@ function App() {
2572
2714
  })();
2573
2715
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
2574
2716
  /* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error }),
2575
- updateAvailable && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: npm i -g miii-agent` }) }),
2717
+ updateAvailable && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` }) }),
2576
2718
  state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "connecting to ollama\u2026" }) }),
2577
2719
  agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
2578
2720
  ChatView,
@@ -2633,8 +2775,15 @@ function App() {
2633
2775
  }
2634
2776
 
2635
2777
  // src/cli.tsx
2778
+ init_spill();
2779
+ cleanupSpill();
2636
2780
  var [, , cmd, ...rest] = process.argv;
2637
- if (cmd === "doctor" || cmd === "eval") {
2781
+ if (cmd === "update" || cmd === "--update" || cmd === "-u") {
2782
+ const { spawnSync } = await import("child_process");
2783
+ console.log("Updating miii-agent\u2026");
2784
+ const r = spawnSync("npm", ["i", "-g", "miii-agent@latest"], { stdio: "inherit", shell: process.platform === "win32" });
2785
+ process.exit(r.status ?? 1);
2786
+ } else if (cmd === "doctor" || cmd === "eval") {
2638
2787
  const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
2639
2788
  process.exit(await runEval2(rest));
2640
2789
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Terminal AI coding agent powered by Ollama",
5
5
  "type": "module",
6
6
  "bin": {