miii-agent 0.1.26 → 0.1.28

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 +16 -1
  2. package/dist/cli.js +224 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,10 +30,12 @@ Your code never leaves your disk. There's nothing to log in to. Pull a model, ty
30
30
 
31
31
  ```bash
32
32
  ollama pull qwen2.5-coder:14b # any coding model works
33
- npm install -g miii-agent
33
+ curl -fsSL https://raw.githubusercontent.com/maruakshay/miii-cli/main/install.sh | sh
34
34
  miii
35
35
  ```
36
36
 
37
+ Prefer npm? `npm install -g miii-agent`.
38
+
37
39
  Then just talk to it:
38
40
 
39
41
  ```
@@ -44,6 +46,19 @@ Then just talk to it:
44
46
 
45
47
  > **Needs:** Node ≥ 18 and [Ollama](https://ollama.com/download) running locally.
46
48
 
49
+ ## Staying up to date
50
+
51
+ miii checks npm on launch and, when a newer release exists, pulls it in the
52
+ background — it applies the next time you start. Manual options:
53
+
54
+ ```bash
55
+ miii update # update now
56
+ miii --version # what you're running
57
+ ```
58
+
59
+ Opt out of background updates by adding `"autoUpdate": false` to `~/.miii/config.json`,
60
+ or re-run the install script (`curl … | sh`) any time to update by hand.
61
+
47
62
  ## Why local-first?
48
63
 
49
64
  Most "AI coding tools" are just wrappers around a cloud API — slow, metered, and they ship your private codebase to someone else's server.
package/dist/cli.js CHANGED
@@ -35,9 +35,13 @@ function migrate(raw) {
35
35
  provider: raw.provider,
36
36
  effort: raw.effort,
37
37
  providers,
38
- modelContexts: raw.modelContexts
38
+ modelContexts: raw.modelContexts,
39
+ autoUpdate: raw.autoUpdate
39
40
  };
40
41
  }
42
+ function autoUpdateEnabled(cfg = loadConfig()) {
43
+ return cfg.autoUpdate !== false;
44
+ }
41
45
  function readRawConfig() {
42
46
  if (!existsSync(CONFIG_PATH)) return {};
43
47
  try {
@@ -195,7 +199,7 @@ async function* chat(entry, model, messages, tools, opts) {
195
199
  model,
196
200
  messages,
197
201
  stream: true,
198
- think: true,
202
+ think: opts?.think ?? true,
199
203
  keep_alive: opts?.keep_alive ?? "10m",
200
204
  options
201
205
  };
@@ -824,9 +828,8 @@ function spillIfLarge(full, label = "output", budget = INLINE_BUDGET) {
824
828
  }
825
829
  const head = Math.floor(budget * HEAD_FRACTION);
826
830
  const tail = budget - head;
827
- const totalLines = full.split("\n").length;
828
831
  const preview = full.slice(0, head) + "\n\u2026\n" + full.slice(-tail);
829
- 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.]`;
832
+ const notice = path ? `[${label} truncated: ${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.]`;
830
833
  return `${preview}
831
834
  ${notice}`;
832
835
  }
@@ -1303,8 +1306,11 @@ Ask in a numbered list. One round of questions per turn. Then wait.
1303
1306
 
1304
1307
  # Tool calls
1305
1308
  - When you need a tool, emit the tool call directly. No preamble, no narration, no "I will use X".
1306
- - Never describe a tool call instead of emitting it. If you cannot emit the call, answer in plain text.
1309
+ - Use the native function-calling interface as the ONLY channel for tool calls. Never print a tool call as text \u2014 not as JSON, not as a fenced code block, not as \`call:name{...}\`, not in any custom or tagged syntax. Text-form calls do NOT execute; they leak to the user and nothing happens.
1310
+ - If you cannot emit a real function call, do not fake one in prose \u2014 answer in plain text instead.
1307
1311
  - After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
1312
+ - Every tool call MUST carry a complete, valid arguments object: all required fields present, correct types, valid JSON. Never emit a call with empty, partial, or placeholder arguments.
1313
+ - WRONG (leaks as text, nothing runs): writing \`call:some_tool{"foo":"bar"}\` or a fenced JSON block in your reply. RIGHT: emit it as a native function call with a full arguments object.
1308
1314
 
1309
1315
  # Tools
1310
1316
  You have access to the following tools. Call them via the function-calling interface.
@@ -1475,6 +1481,11 @@ function parseTextToolCalls(text, knownToolNames) {
1475
1481
  if (!text) return { calls: [], cleanedText: text };
1476
1482
  const calls = [];
1477
1483
  let cleaned = text;
1484
+ const callSyntax = parseCallSyntax(cleaned, knownToolNames);
1485
+ if (callSyntax.calls.length > 0) {
1486
+ calls.push(...callSyntax.calls);
1487
+ cleaned = callSyntax.cleanedText;
1488
+ }
1478
1489
  const tagRe = /<\|?tool_call\|?>\s*([\s\S]*?)\s*<\|?\/?tool_call\|?>/g;
1479
1490
  cleaned = cleaned.replace(tagRe, (_m, body) => {
1480
1491
  const c = tryParse(body, knownToolNames);
@@ -1515,6 +1526,74 @@ function tryParse(raw, knownToolNames) {
1515
1526
  return null;
1516
1527
  }
1517
1528
  }
1529
+ function parseCallSyntax(text, knownToolNames) {
1530
+ const calls = [];
1531
+ const headRe = /call:\s*([a-zA-Z_]\w*)\s*\{/g;
1532
+ let m;
1533
+ let cleaned = "";
1534
+ let lastEnd = 0;
1535
+ while ((m = headRe.exec(text)) !== null) {
1536
+ const name = m[1];
1537
+ const body = parseCallBody(text, m.index + m[0].length);
1538
+ if (!body) continue;
1539
+ headRe.lastIndex = body.end;
1540
+ if (!knownToolNames.includes(name)) continue;
1541
+ calls.push({ function: { name, arguments: body.args } });
1542
+ cleaned += text.slice(lastEnd, m.index);
1543
+ lastEnd = body.end;
1544
+ }
1545
+ cleaned += text.slice(lastEnd);
1546
+ return { calls, cleanedText: cleaned.trim() };
1547
+ }
1548
+ function parseCallBody(text, start) {
1549
+ const args2 = {};
1550
+ let i = start;
1551
+ const isWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
1552
+ while (i < text.length) {
1553
+ while (i < text.length && (isWs(text[i]) || text[i] === ",")) i++;
1554
+ if (i >= text.length) return null;
1555
+ if (text[i] === "}") return { args: args2, end: i + 1 };
1556
+ const keyStart = i;
1557
+ while (i < text.length && text[i] !== ":" && text[i] !== "}") i++;
1558
+ if (i >= text.length || text[i] !== ":") return null;
1559
+ const key = text.slice(keyStart, i).trim().replace(/^["']|["']$/g, "");
1560
+ i++;
1561
+ while (i < text.length && isWs(text[i])) i++;
1562
+ if (text.startsWith(VAL_DELIM, i)) {
1563
+ const vStart = i + VAL_DELIM.length;
1564
+ const vEnd = text.indexOf(VAL_DELIM, vStart);
1565
+ if (vEnd === -1) return null;
1566
+ args2[key] = text.slice(vStart, vEnd);
1567
+ i = vEnd + VAL_DELIM.length;
1568
+ } else {
1569
+ const vStart = i;
1570
+ let depth = 0;
1571
+ let inStr = null;
1572
+ let esc = false;
1573
+ while (i < text.length) {
1574
+ const ch = text[i];
1575
+ if (inStr) {
1576
+ if (esc) esc = false;
1577
+ else if (ch === "\\") esc = true;
1578
+ else if (ch === inStr) inStr = null;
1579
+ } else if (ch === '"' || ch === "'") inStr = ch;
1580
+ else if (ch === "{" || ch === "[") depth++;
1581
+ else if (ch === "}" || ch === "]") {
1582
+ if (depth === 0) break;
1583
+ depth--;
1584
+ } else if (ch === "," && depth === 0) break;
1585
+ i++;
1586
+ }
1587
+ const rawVal = text.slice(vStart, i).trim();
1588
+ try {
1589
+ args2[key] = JSON.parse(rawVal);
1590
+ } catch {
1591
+ args2[key] = rawVal.replace(/^["']|["']$/g, "");
1592
+ }
1593
+ }
1594
+ }
1595
+ return null;
1596
+ }
1518
1597
  function extractFirstJsonObject(s) {
1519
1598
  const start = s.indexOf("{");
1520
1599
  if (start === -1) return null;
@@ -1541,6 +1620,18 @@ function extractFirstJsonObject(s) {
1541
1620
  }
1542
1621
  return null;
1543
1622
  }
1623
+ function looksLikeLeakedToolCall(text, knownToolNames) {
1624
+ if (!text) return false;
1625
+ if (text.includes(VAL_DELIM)) return true;
1626
+ if (/<\|?tool_call/i.test(text)) return true;
1627
+ const callMatch = text.match(/\bcall:\s*(\w+)\s*\{/);
1628
+ if (callMatch && knownToolNames.includes(callMatch[1])) return true;
1629
+ if (/"name"\s*:\s*"([^"]+)"/.test(text) && /"(arguments|parameters|input)"\s*:/.test(text)) {
1630
+ const name = text.match(/"name"\s*:\s*"([^"]+)"/)?.[1];
1631
+ if (name && knownToolNames.includes(name)) return true;
1632
+ }
1633
+ return false;
1634
+ }
1544
1635
  function blocksFromOllama(text, tool_calls, knownToolNames = []) {
1545
1636
  const blocks = [];
1546
1637
  let finalText = text;
@@ -1563,9 +1654,11 @@ function blocksFromOllama(text, tool_calls, knownToolNames = []) {
1563
1654
  }
1564
1655
  return blocks;
1565
1656
  }
1657
+ var VAL_DELIM;
1566
1658
  var init_adapter = __esm({
1567
1659
  "src/agent/adapter.ts"() {
1568
1660
  "use strict";
1661
+ VAL_DELIM = '<|"|>';
1569
1662
  }
1570
1663
  });
1571
1664
 
@@ -1638,6 +1731,7 @@ async function* runAgent(opts) {
1638
1731
  let evalTokens = 0;
1639
1732
  let lastAssistantSig = "";
1640
1733
  let repeatCount = 0;
1734
+ let leakNudges = 0;
1641
1735
  const seenPaths = /* @__PURE__ */ new Set();
1642
1736
  for (let turn = 0; turn < MAX_TURNS; turn++) {
1643
1737
  let text = "";
@@ -1722,6 +1816,16 @@ async function* runAgent(opts) {
1722
1816
  continue;
1723
1817
  }
1724
1818
  if (tool_uses.length === 0) {
1819
+ const assistantText = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1820
+ if (leakNudges < MAX_LEAK_NUDGES && looksLikeLeakedToolCall(assistantText, toolNames)) {
1821
+ leakNudges++;
1822
+ history.push({
1823
+ role: "user",
1824
+ content: "That tool call was written as plain text, so it did not run and nothing happened. Re-issue it using the function-calling interface only \u2014 do not print the call as text, JSON, or any custom syntax. If you did not mean to call a tool, answer in prose."
1825
+ });
1826
+ yield { type: "turn-end", stop_reason: "tool_use" };
1827
+ continue;
1828
+ }
1725
1829
  yield { type: "turn-end", stop_reason: "end_turn" };
1726
1830
  break;
1727
1831
  }
@@ -1829,7 +1933,7 @@ async function* runAgent(opts) {
1829
1933
  yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1830
1934
  return history;
1831
1935
  }
1832
- var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, BIG_WRITE_TOOLS;
1936
+ var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, MAX_LEAK_NUDGES, BIG_WRITE_TOOLS;
1833
1937
  var init_loop = __esm({
1834
1938
  "src/agent/loop.ts"() {
1835
1939
  "use strict";
@@ -1845,21 +1949,22 @@ var init_loop = __esm({
1845
1949
  MAX_TURNS = 25;
1846
1950
  REPEAT_TAIL = 120;
1847
1951
  REPEAT_KILL = 4;
1952
+ MAX_LEAK_NUDGES = 2;
1848
1953
  BIG_WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file"]);
1849
1954
  }
1850
1955
  });
1851
1956
 
1852
1957
  // eval/runner.ts
1853
- import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "fs";
1854
- import { dirname as dirname3, join as join10 } from "path";
1958
+ import { mkdtempSync, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "fs";
1959
+ import { dirname as dirname3, join as join11 } from "path";
1855
1960
  import { tmpdir as tmpdir2 } from "os";
1856
1961
  async function runScenario(model, s) {
1857
- const dir = mkdtempSync(join10(tmpdir2(), "miii-eval-"));
1962
+ const dir = mkdtempSync(join11(tmpdir2(), "miii-eval-"));
1858
1963
  const prevCwd = process.cwd();
1859
1964
  for (const [rel, content] of Object.entries(s.files ?? {})) {
1860
- const abs = join10(dir, rel);
1861
- mkdirSync6(dirname3(abs), { recursive: true });
1862
- writeFileSync7(abs, content, "utf-8");
1965
+ const abs = join11(dir, rel);
1966
+ mkdirSync7(dirname3(abs), { recursive: true });
1967
+ writeFileSync8(abs, content, "utf-8");
1863
1968
  }
1864
1969
  const r = {
1865
1970
  name: s.name,
@@ -1920,13 +2025,13 @@ var init_runner = __esm({
1920
2025
  });
1921
2026
 
1922
2027
  // eval/scenarios.ts
1923
- import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
1924
- import { join as join11 } from "path";
2028
+ import { readFileSync as readFileSync10, existsSync as existsSync8 } from "fs";
2029
+ import { join as join12 } from "path";
1925
2030
  var read, scenarios;
1926
2031
  var init_scenarios = __esm({
1927
2032
  "eval/scenarios.ts"() {
1928
2033
  "use strict";
1929
- read = (dir, f) => existsSync8(join11(dir, f)) ? readFileSync9(join11(dir, f), "utf-8") : null;
2034
+ read = (dir, f) => existsSync8(join12(dir, f)) ? readFileSync10(join12(dir, f), "utf-8") : null;
1930
2035
  scenarios = [
1931
2036
  {
1932
2037
  name: "edit-exact-string",
@@ -2076,13 +2181,25 @@ init_client();
2076
2181
  init_config();
2077
2182
  import { useState as useState5, useEffect as useEffect4, useRef as useRef2 } from "react";
2078
2183
  import { Box as Box13, Text as Text13, useApp } from "ink";
2079
- import { homedir as homedir7 } from "os";
2184
+ import { homedir as homedir8 } from "os";
2080
2185
  import { sep as sep2 } from "path";
2081
2186
 
2082
2187
  // src/ui/WelcomeBlock.tsx
2083
2188
  import { Box, Text } from "ink";
2084
2189
  import { jsx, jsxs } from "react/jsx-runtime";
2085
- function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable }) {
2190
+ function updateBannerText(version, status) {
2191
+ switch (status) {
2192
+ case "downloading":
2193
+ return `\u2191 v${version} downloading in the background \u2014 restart miii to apply`;
2194
+ case "installed":
2195
+ return `\u2713 v${version} installed \u2014 restart miii to apply`;
2196
+ case "failed":
2197
+ return `\u2191 v${version} update failed \u2014 run \`miii update\` manually`;
2198
+ default:
2199
+ return `\u2191 v${version} available \u2014 run \`miii update\` to upgrade`;
2200
+ }
2201
+ }
2202
+ function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable, updateStatus = "idle" }) {
2086
2203
  const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
2087
2204
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
2088
2205
  /* @__PURE__ */ jsxs(
@@ -2109,7 +2226,7 @@ function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable }) {
2109
2226
  ]
2110
2227
  }
2111
2228
  ),
2112
- updateAvailable && /* @__PURE__ */ jsx(Text, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` })
2229
+ updateAvailable && /* @__PURE__ */ jsx(Text, { color: updateStatus === "failed" ? "red" : updateStatus === "installed" ? "green" : "yellow", children: updateBannerText(updateAvailable, updateStatus) })
2113
2230
  ] });
2114
2231
  }
2115
2232
 
@@ -2469,7 +2586,9 @@ ${convo}`;
2469
2586
  model,
2470
2587
  [{ role: "user", content: prompt }],
2471
2588
  void 0,
2472
- { temperature: 0.2, num_predict: 32 }
2589
+ // Disable thinking + give content room: reasoning models otherwise burn
2590
+ // the whole budget on hidden thinking and emit no title text.
2591
+ { temperature: 0.2, num_predict: 64, think: false }
2473
2592
  )) {
2474
2593
  if (chunk.content) out += chunk.content;
2475
2594
  }
@@ -2480,6 +2599,22 @@ ${convo}`;
2480
2599
  }
2481
2600
  }
2482
2601
 
2602
+ // src/ui/terminalTitle.ts
2603
+ var OSC = "\x1B]2;";
2604
+ var BEL = "\x07";
2605
+ function clean(title) {
2606
+ return title.replace(/[\x00-\x1f\x7f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 80);
2607
+ }
2608
+ function setTerminalTitle(title) {
2609
+ if (!process.stdout.isTTY) return;
2610
+ const text = clean(title);
2611
+ const label = text ? `\u2733 ${text}` : "";
2612
+ process.stdout.write(`${OSC}${label}${BEL}`);
2613
+ }
2614
+ function resetTerminalTitle() {
2615
+ setTerminalTitle("");
2616
+ }
2617
+
2483
2618
  // src/ui/FilePicker.tsx
2484
2619
  import { Box as Box7, Text as Text7 } from "ink";
2485
2620
  import { readdirSync as readdirSync2 } from "fs";
@@ -4260,6 +4395,7 @@ function useKeyboard(opts) {
4260
4395
  setActiveToolResults([]);
4261
4396
  setError(null);
4262
4397
  setSessionId(meta.id);
4398
+ setTerminalTitle(meta.title);
4263
4399
  onResumeSession(meta.id);
4264
4400
  setNotice(`resumed \xB7 ${meta.title}`);
4265
4401
  setState("ready");
@@ -4389,6 +4525,7 @@ function useKeyboard(opts) {
4389
4525
  } else if (trimmed === "/new") {
4390
4526
  if (agentHistory.length) setNotice("session saved");
4391
4527
  setSessionId(newSessionId());
4528
+ resetTerminalTitle();
4392
4529
  hardClear();
4393
4530
  clearSession();
4394
4531
  } else if (trimmed === "/sessions") {
@@ -4477,8 +4614,28 @@ function useKeyboard(opts) {
4477
4614
 
4478
4615
  // src/updateCheck.ts
4479
4616
  import { createRequire } from "module";
4617
+ import { homedir as homedir7 } from "os";
4618
+ import { join as join10 } from "path";
4619
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
4480
4620
  var require2 = createRequire(import.meta.url);
4481
4621
  var PKG_NAME = "miii-agent";
4622
+ var UPDATE_ATTEMPT_PATH = join10(homedir7(), ".miii", ".last-update-attempt");
4623
+ var UPDATE_ATTEMPT_COOLDOWN_MS = 10 * 60 * 1e3;
4624
+ function recentlyAttemptedUpdate() {
4625
+ try {
4626
+ const last = Number(readFileSync9(UPDATE_ATTEMPT_PATH, "utf8").trim());
4627
+ return Number.isFinite(last) && Date.now() - last < UPDATE_ATTEMPT_COOLDOWN_MS;
4628
+ } catch {
4629
+ return false;
4630
+ }
4631
+ }
4632
+ function markUpdateAttempt() {
4633
+ try {
4634
+ mkdirSync6(join10(homedir7(), ".miii"), { recursive: true });
4635
+ writeFileSync7(UPDATE_ATTEMPT_PATH, String(Date.now()));
4636
+ } catch {
4637
+ }
4638
+ }
4482
4639
  function currentVersion() {
4483
4640
  try {
4484
4641
  return require2("../package.json").version;
@@ -4494,6 +4651,30 @@ function newerVersion(current, latest) {
4494
4651
  if (lb !== cb) return lb > cb;
4495
4652
  return lc > cc;
4496
4653
  }
4654
+ function autoUpdate(onDone) {
4655
+ if (recentlyAttemptedUpdate()) return false;
4656
+ try {
4657
+ markUpdateAttempt();
4658
+ const { spawn } = require2("child_process");
4659
+ const child = spawn("npm", ["i", "-g", `${PKG_NAME}@latest`], {
4660
+ detached: true,
4661
+ stdio: "ignore",
4662
+ shell: process.platform === "win32"
4663
+ });
4664
+ let settled = false;
4665
+ const settle = (ok) => {
4666
+ if (settled) return;
4667
+ settled = true;
4668
+ onDone?.(ok);
4669
+ };
4670
+ child.on("error", () => settle(false));
4671
+ child.on("exit", (code) => settle(code === 0));
4672
+ child.unref();
4673
+ return true;
4674
+ } catch {
4675
+ return false;
4676
+ }
4677
+ }
4497
4678
  async function checkForUpdate() {
4498
4679
  try {
4499
4680
  const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {
@@ -4514,7 +4695,7 @@ async function checkForUpdate() {
4514
4695
  import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
4515
4696
  function App() {
4516
4697
  const { exit } = useApp();
4517
- const cwd = process.cwd().replace(homedir7(), "~").split(sep2).join("/");
4698
+ const cwd = process.cwd().replace(homedir8(), "~").split(sep2).join("/");
4518
4699
  const [cfg, setCfg] = useState5(loadConfig());
4519
4700
  const [models, setModels] = useState5([]);
4520
4701
  const [contexts, setContexts] = useState5(() => cfg.modelContexts ?? {});
@@ -4525,8 +4706,11 @@ function App() {
4525
4706
  const [cursor, setCursor] = useState5(0);
4526
4707
  const [pickerQuery, setPickerQuery] = useState5("");
4527
4708
  const [updateAvailable, setUpdateAvailable] = useState5(null);
4709
+ const [updateStatus, setUpdateStatus] = useState5("idle");
4528
4710
  const [providerDown, setProviderDown] = useState5(false);
4529
4711
  const [sessionId, setSessionId] = useState5(() => newSessionId());
4712
+ const sessionIdRef = useRef2(sessionId);
4713
+ sessionIdRef.current = sessionId;
4530
4714
  const [sessions, setSessions] = useState5([]);
4531
4715
  const [notice, setNotice] = useState5(null);
4532
4716
  const [input, setInput] = useState5("");
@@ -4536,9 +4720,15 @@ function App() {
4536
4720
  const agent = useAgentRunner(cfg.model, activeCtx);
4537
4721
  useEffect4(() => {
4538
4722
  checkForUpdate().then((v) => {
4539
- if (v) setUpdateAvailable(v);
4723
+ if (!v) return;
4724
+ setUpdateAvailable(v);
4725
+ if (autoUpdateEnabled()) {
4726
+ const started = autoUpdate((ok) => setUpdateStatus(ok ? "installed" : "failed"));
4727
+ if (started) setUpdateStatus("downloading");
4728
+ }
4540
4729
  });
4541
4730
  }, []);
4731
+ useEffect4(() => resetTerminalTitle, []);
4542
4732
  const titledSessions = useRef2(/* @__PURE__ */ new Set());
4543
4733
  useEffect4(() => {
4544
4734
  const history = agent.agentHistory;
@@ -4553,6 +4743,7 @@ function App() {
4553
4743
  try {
4554
4744
  const title = await summarizeConversation(model, snapshot);
4555
4745
  setSessionTitle(id, title);
4746
+ if (id === sessionIdRef.current) setTerminalTitle(title);
4556
4747
  } catch {
4557
4748
  }
4558
4749
  })();
@@ -4653,7 +4844,7 @@ function App() {
4653
4844
  return Math.round(used / activeCtx * 100);
4654
4845
  })();
4655
4846
  return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
4656
- state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
4847
+ state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable, updateStatus }),
4657
4848
  state === "loading" && !agent.error && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
4658
4849
  agent.error && state !== "ready" && /* @__PURE__ */ jsx13(
4659
4850
  ChatView,
@@ -4714,7 +4905,8 @@ function App() {
4714
4905
  return /* @__PURE__ */ jsx13(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
4715
4906
  })(),
4716
4907
  /* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
4717
- !agent.busy && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) })
4908
+ !agent.busy && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) }),
4909
+ updateAvailable && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: updateStatus === "failed" ? "red" : updateStatus === "installed" ? "green" : "yellow", children: updateBannerText(updateAvailable, updateStatus) }) })
4718
4910
  ] })
4719
4911
  ] });
4720
4912
  }
@@ -4733,7 +4925,12 @@ for (let i = 0; i < args.length; i++) {
4733
4925
  cmd = args[i];
4734
4926
  }
4735
4927
  }
4736
- if (cmd === "update" || cmd === "--update" || cmd === "-u") {
4928
+ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
4929
+ const { createRequire: createRequire2 } = await import("module");
4930
+ const pkg = createRequire2(import.meta.url)("../package.json");
4931
+ console.log(pkg.version);
4932
+ process.exit(0);
4933
+ } else if (cmd === "update" || cmd === "--update" || cmd === "-u") {
4737
4934
  const { spawnSync } = await import("child_process");
4738
4935
  console.log("Updating miii-agent\u2026");
4739
4936
  const r = spawnSync("npm", ["i", "-g", "miii-agent@latest"], { stdio: "inherit", shell: process.platform === "win32" });
@@ -4743,5 +4940,8 @@ if (cmd === "update" || cmd === "--update" || cmd === "-u") {
4743
4940
  const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
4744
4941
  process.exit(await runEval2(rest));
4745
4942
  } else {
4943
+ process.on("exit", () => {
4944
+ if (process.stdout.isTTY) process.stdout.write("\x1B]2;\x07");
4945
+ });
4746
4946
  render(createElement(App));
4747
4947
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Cursor / Claude Code, but local. An offline AI pair-programmer in your terminal, powered by Ollama. Private by default, free forever.",
5
5
  "type": "module",
6
6
  "bin": {