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.
- package/README.md +16 -1
- package/dist/cli.js +224 -24
- 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
|
-
|
|
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: ${
|
|
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
|
|
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
|
|
1854
|
-
import { dirname as dirname3, join as
|
|
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(
|
|
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 =
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
|
1924
|
-
import { join as
|
|
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(
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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 === "
|
|
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