reasonix 0.2.0 → 0.3.0-alpha.1
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 +32 -9
- package/dist/cli/index.js +944 -86
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +270 -2
- package/dist/index.js +327 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1360,6 +1360,14 @@ function recordFromLoopEvent(ev, extra) {
|
|
|
1360
1360
|
if (ev.toolName !== void 0) rec.tool = ev.toolName;
|
|
1361
1361
|
if (ev.toolArgs !== void 0) rec.args = ev.toolArgs;
|
|
1362
1362
|
if (ev.error !== void 0) rec.error = ev.error;
|
|
1363
|
+
if (ev.planState && !isPlanStateEmptyShape(ev.planState)) {
|
|
1364
|
+
rec.planState = {
|
|
1365
|
+
subgoals: [...ev.planState.subgoals],
|
|
1366
|
+
hypotheses: [...ev.planState.hypotheses],
|
|
1367
|
+
uncertainties: [...ev.planState.uncertainties],
|
|
1368
|
+
rejectedPaths: [...ev.planState.rejectedPaths]
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1363
1371
|
if (ev.stats) {
|
|
1364
1372
|
rec.usage = {
|
|
1365
1373
|
prompt_tokens: ev.stats.usage.promptTokens,
|
|
@@ -1395,6 +1403,9 @@ function readTranscript(path) {
|
|
|
1395
1403
|
const raw = readFileSync3(path, "utf8");
|
|
1396
1404
|
return parseTranscript(raw);
|
|
1397
1405
|
}
|
|
1406
|
+
function isPlanStateEmptyShape(s) {
|
|
1407
|
+
return s.subgoals.length === 0 && s.hypotheses.length === 0 && s.uncertainties.length === 0 && s.rejectedPaths.length === 0;
|
|
1408
|
+
}
|
|
1398
1409
|
function parseTranscript(raw) {
|
|
1399
1410
|
const out = { meta: null, records: [] };
|
|
1400
1411
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -1420,6 +1431,24 @@ function parseTranscript(raw) {
|
|
|
1420
1431
|
}
|
|
1421
1432
|
|
|
1422
1433
|
// src/replay.ts
|
|
1434
|
+
function groupRecordsByTurn(records) {
|
|
1435
|
+
const byTurn = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const rec of records) {
|
|
1437
|
+
const list = byTurn.get(rec.turn);
|
|
1438
|
+
if (list) list.push(rec);
|
|
1439
|
+
else byTurn.set(rec.turn, [rec]);
|
|
1440
|
+
}
|
|
1441
|
+
return [...byTurn.entries()].sort(([a], [b]) => a - b).map(([turn, records2]) => ({ turn, records: records2 }));
|
|
1442
|
+
}
|
|
1443
|
+
function computeCumulativeStats(pages, upToIdx) {
|
|
1444
|
+
if (upToIdx < 0) return computeReplayStats([]);
|
|
1445
|
+
const flat = [];
|
|
1446
|
+
for (let i = 0; i <= upToIdx && i < pages.length; i++) {
|
|
1447
|
+
const records = pages[i]?.records;
|
|
1448
|
+
if (records) flat.push(...records);
|
|
1449
|
+
}
|
|
1450
|
+
return computeReplayStats(flat);
|
|
1451
|
+
}
|
|
1423
1452
|
function replayFromFile(path) {
|
|
1424
1453
|
const parsed = readTranscript(path);
|
|
1425
1454
|
return { parsed, stats: computeReplayStats(parsed.records) };
|
|
@@ -1430,12 +1459,20 @@ function computeReplayStats(records) {
|
|
|
1430
1459
|
const prefixHashes = /* @__PURE__ */ new Set();
|
|
1431
1460
|
let userTurns = 0;
|
|
1432
1461
|
let toolCalls = 0;
|
|
1462
|
+
let harvestedTurns = 0;
|
|
1463
|
+
let totalUncertainties = 0;
|
|
1464
|
+
let totalSubgoals = 0;
|
|
1433
1465
|
for (const rec of records) {
|
|
1434
1466
|
if (rec.role === "user") userTurns++;
|
|
1435
1467
|
else if (rec.role === "tool") toolCalls++;
|
|
1436
1468
|
else if (rec.role === "assistant_final") {
|
|
1437
1469
|
if (rec.model) models.add(rec.model);
|
|
1438
1470
|
if (rec.prefixHash) prefixHashes.add(rec.prefixHash);
|
|
1471
|
+
if (rec.planState) {
|
|
1472
|
+
harvestedTurns++;
|
|
1473
|
+
totalUncertainties += rec.planState.uncertainties.length;
|
|
1474
|
+
totalSubgoals += rec.planState.subgoals.length;
|
|
1475
|
+
}
|
|
1439
1476
|
if (rec.usage && rec.model) {
|
|
1440
1477
|
const u = new Usage(
|
|
1441
1478
|
rec.usage.prompt_tokens ?? 0,
|
|
@@ -1463,6 +1500,9 @@ function computeReplayStats(records) {
|
|
|
1463
1500
|
prefixHashes: [...prefixHashes],
|
|
1464
1501
|
userTurns,
|
|
1465
1502
|
toolCalls,
|
|
1503
|
+
harvestedTurns,
|
|
1504
|
+
totalUncertainties,
|
|
1505
|
+
totalSubgoals,
|
|
1466
1506
|
...summarizeTurns(turns)
|
|
1467
1507
|
};
|
|
1468
1508
|
}
|
|
@@ -1491,6 +1531,19 @@ function round2(n, digits) {
|
|
|
1491
1531
|
}
|
|
1492
1532
|
|
|
1493
1533
|
// src/diff.ts
|
|
1534
|
+
function findNextDivergence(pairs, fromIdx) {
|
|
1535
|
+
for (let i = fromIdx + 1; i < pairs.length; i++) {
|
|
1536
|
+
if (pairs[i].kind !== "match") return i;
|
|
1537
|
+
}
|
|
1538
|
+
return -1;
|
|
1539
|
+
}
|
|
1540
|
+
function findPrevDivergence(pairs, fromIdx) {
|
|
1541
|
+
const start = Math.min(fromIdx - 1, pairs.length - 1);
|
|
1542
|
+
for (let i = start; i >= 0; i--) {
|
|
1543
|
+
if (pairs[i].kind !== "match") return i;
|
|
1544
|
+
}
|
|
1545
|
+
return -1;
|
|
1546
|
+
}
|
|
1494
1547
|
function diffTranscripts(a, b) {
|
|
1495
1548
|
const aSide = {
|
|
1496
1549
|
label: a.label,
|
|
@@ -1633,6 +1686,41 @@ function renderSummaryTable(report, _opts = {}) {
|
|
|
1633
1686
|
)
|
|
1634
1687
|
);
|
|
1635
1688
|
lines.push(statRow("prefix hashes", a.stats.prefixHashes.length, b.stats.prefixHashes.length));
|
|
1689
|
+
if (a.stats.harvestedTurns > 0 || b.stats.harvestedTurns > 0) {
|
|
1690
|
+
lines.push(
|
|
1691
|
+
row(
|
|
1692
|
+
[
|
|
1693
|
+
"harvest turns",
|
|
1694
|
+
`${a.stats.harvestedTurns}`,
|
|
1695
|
+
`${b.stats.harvestedTurns}`,
|
|
1696
|
+
signed(b.stats.harvestedTurns - a.stats.harvestedTurns)
|
|
1697
|
+
],
|
|
1698
|
+
[20, 14, 14, 14]
|
|
1699
|
+
)
|
|
1700
|
+
);
|
|
1701
|
+
lines.push(
|
|
1702
|
+
row(
|
|
1703
|
+
[
|
|
1704
|
+
" subgoals",
|
|
1705
|
+
`${a.stats.totalSubgoals}`,
|
|
1706
|
+
`${b.stats.totalSubgoals}`,
|
|
1707
|
+
signed(b.stats.totalSubgoals - a.stats.totalSubgoals)
|
|
1708
|
+
],
|
|
1709
|
+
[20, 14, 14, 14]
|
|
1710
|
+
)
|
|
1711
|
+
);
|
|
1712
|
+
lines.push(
|
|
1713
|
+
row(
|
|
1714
|
+
[
|
|
1715
|
+
" uncertainties",
|
|
1716
|
+
`${a.stats.totalUncertainties}`,
|
|
1717
|
+
`${b.stats.totalUncertainties}`,
|
|
1718
|
+
signed(b.stats.totalUncertainties - a.stats.totalUncertainties)
|
|
1719
|
+
],
|
|
1720
|
+
[20, 14, 14, 14]
|
|
1721
|
+
)
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1636
1724
|
lines.push("");
|
|
1637
1725
|
const aPrefixStable = a.stats.prefixHashes.length <= 1;
|
|
1638
1726
|
const bPrefixStable = b.stats.prefixHashes.length <= 1;
|
|
@@ -1704,6 +1792,17 @@ function renderMarkdown(report) {
|
|
|
1704
1792
|
out.push(
|
|
1705
1793
|
`| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | \u2014 |`
|
|
1706
1794
|
);
|
|
1795
|
+
if (a.stats.harvestedTurns > 0 || b.stats.harvestedTurns > 0) {
|
|
1796
|
+
out.push(
|
|
1797
|
+
`| harvest turns | ${a.stats.harvestedTurns} | ${b.stats.harvestedTurns} | ${signed(b.stats.harvestedTurns - a.stats.harvestedTurns)} |`
|
|
1798
|
+
);
|
|
1799
|
+
out.push(
|
|
1800
|
+
`| harvest subgoals | ${a.stats.totalSubgoals} | ${b.stats.totalSubgoals} | ${signed(b.stats.totalSubgoals - a.stats.totalSubgoals)} |`
|
|
1801
|
+
);
|
|
1802
|
+
out.push(
|
|
1803
|
+
`| harvest uncertainties | ${a.stats.totalUncertainties} | ${b.stats.totalUncertainties} | ${signed(b.stats.totalUncertainties - a.stats.totalUncertainties)} |`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1707
1806
|
out.push("");
|
|
1708
1807
|
out.push("## Turn-by-turn");
|
|
1709
1808
|
out.push("");
|
|
@@ -1771,6 +1870,258 @@ function truncate(s, n) {
|
|
|
1771
1870
|
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
1772
1871
|
}
|
|
1773
1872
|
|
|
1873
|
+
// src/mcp/types.ts
|
|
1874
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
1875
|
+
function isJsonRpcError(msg) {
|
|
1876
|
+
return "error" in msg;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/mcp/client.ts
|
|
1880
|
+
var McpClient = class {
|
|
1881
|
+
transport;
|
|
1882
|
+
clientInfo;
|
|
1883
|
+
requestTimeoutMs;
|
|
1884
|
+
pending = /* @__PURE__ */ new Map();
|
|
1885
|
+
nextId = 1;
|
|
1886
|
+
readerStarted = false;
|
|
1887
|
+
initialized = false;
|
|
1888
|
+
_serverCapabilities = {};
|
|
1889
|
+
constructor(opts) {
|
|
1890
|
+
this.transport = opts.transport;
|
|
1891
|
+
this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
|
|
1892
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e4;
|
|
1893
|
+
}
|
|
1894
|
+
/** Server's advertised capabilities, available after initialize(). */
|
|
1895
|
+
get serverCapabilities() {
|
|
1896
|
+
return this._serverCapabilities;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Complete the initialize → initialized handshake. Must be called
|
|
1900
|
+
* before any other method (otherwise compliant servers reject).
|
|
1901
|
+
*/
|
|
1902
|
+
async initialize() {
|
|
1903
|
+
if (this.initialized) throw new Error("MCP client already initialized");
|
|
1904
|
+
this.startReaderIfNeeded();
|
|
1905
|
+
const result = await this.request("initialize", {
|
|
1906
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
1907
|
+
capabilities: { tools: {} },
|
|
1908
|
+
clientInfo: this.clientInfo
|
|
1909
|
+
});
|
|
1910
|
+
this._serverCapabilities = result.capabilities ?? {};
|
|
1911
|
+
await this.transport.send({
|
|
1912
|
+
jsonrpc: "2.0",
|
|
1913
|
+
method: "notifications/initialized"
|
|
1914
|
+
});
|
|
1915
|
+
this.initialized = true;
|
|
1916
|
+
return result;
|
|
1917
|
+
}
|
|
1918
|
+
/** List tools the server exposes. */
|
|
1919
|
+
async listTools() {
|
|
1920
|
+
this.assertInitialized();
|
|
1921
|
+
return this.request("tools/list", {});
|
|
1922
|
+
}
|
|
1923
|
+
/** Invoke a tool by name. Returns the raw MCP result (caller unwraps content). */
|
|
1924
|
+
async callTool(name, args) {
|
|
1925
|
+
this.assertInitialized();
|
|
1926
|
+
return this.request("tools/call", {
|
|
1927
|
+
name,
|
|
1928
|
+
arguments: args ?? {}
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
/** Close the transport and reject any outstanding requests. */
|
|
1932
|
+
async close() {
|
|
1933
|
+
for (const [, pending] of this.pending) {
|
|
1934
|
+
clearTimeout(pending.timeout);
|
|
1935
|
+
pending.reject(new Error("MCP client closed"));
|
|
1936
|
+
}
|
|
1937
|
+
this.pending.clear();
|
|
1938
|
+
await this.transport.close();
|
|
1939
|
+
}
|
|
1940
|
+
// ---------- internals ----------
|
|
1941
|
+
assertInitialized() {
|
|
1942
|
+
if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
|
|
1943
|
+
}
|
|
1944
|
+
async request(method, params) {
|
|
1945
|
+
const id = this.nextId++;
|
|
1946
|
+
const frame = { jsonrpc: "2.0", id, method, params };
|
|
1947
|
+
const promise = new Promise((resolve2, reject) => {
|
|
1948
|
+
const timeout = setTimeout(() => {
|
|
1949
|
+
this.pending.delete(id);
|
|
1950
|
+
reject(
|
|
1951
|
+
new Error(`MCP request ${method} (id=${id}) timed out after ${this.requestTimeoutMs}ms`)
|
|
1952
|
+
);
|
|
1953
|
+
}, this.requestTimeoutMs);
|
|
1954
|
+
this.pending.set(id, {
|
|
1955
|
+
resolve: resolve2,
|
|
1956
|
+
reject,
|
|
1957
|
+
timeout
|
|
1958
|
+
});
|
|
1959
|
+
});
|
|
1960
|
+
await this.transport.send(frame);
|
|
1961
|
+
return promise;
|
|
1962
|
+
}
|
|
1963
|
+
startReaderIfNeeded() {
|
|
1964
|
+
if (this.readerStarted) return;
|
|
1965
|
+
this.readerStarted = true;
|
|
1966
|
+
void this.readLoop();
|
|
1967
|
+
}
|
|
1968
|
+
async readLoop() {
|
|
1969
|
+
try {
|
|
1970
|
+
for await (const msg of this.transport.messages()) {
|
|
1971
|
+
this.dispatch(msg);
|
|
1972
|
+
}
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
for (const [, pending] of this.pending) {
|
|
1975
|
+
clearTimeout(pending.timeout);
|
|
1976
|
+
pending.reject(err);
|
|
1977
|
+
}
|
|
1978
|
+
this.pending.clear();
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
dispatch(msg) {
|
|
1982
|
+
if (!("id" in msg) || msg.id === null || msg.id === void 0) return;
|
|
1983
|
+
if (!("result" in msg) && !("error" in msg)) return;
|
|
1984
|
+
const pending = this.pending.get(msg.id);
|
|
1985
|
+
if (!pending) return;
|
|
1986
|
+
this.pending.delete(msg.id);
|
|
1987
|
+
clearTimeout(pending.timeout);
|
|
1988
|
+
const resp = msg;
|
|
1989
|
+
if (isJsonRpcError(resp)) {
|
|
1990
|
+
pending.reject(new Error(`MCP ${resp.error.code}: ${resp.error.message}`));
|
|
1991
|
+
} else {
|
|
1992
|
+
pending.resolve(resp.result);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
// src/mcp/stdio.ts
|
|
1998
|
+
import { spawn } from "child_process";
|
|
1999
|
+
var StdioTransport = class {
|
|
2000
|
+
child;
|
|
2001
|
+
queue = [];
|
|
2002
|
+
waiters = [];
|
|
2003
|
+
closed = false;
|
|
2004
|
+
stdoutBuffer = "";
|
|
2005
|
+
constructor(opts) {
|
|
2006
|
+
const env = opts.replaceEnv ? { ...opts.env ?? {} } : { ...process.env, ...opts.env ?? {} };
|
|
2007
|
+
this.child = spawn(opts.command, opts.args ?? [], {
|
|
2008
|
+
env,
|
|
2009
|
+
cwd: opts.cwd,
|
|
2010
|
+
stdio: ["pipe", "pipe", "inherit"]
|
|
2011
|
+
});
|
|
2012
|
+
this.child.stdout.setEncoding("utf8");
|
|
2013
|
+
this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
|
2014
|
+
this.child.on("close", () => this.onClose());
|
|
2015
|
+
this.child.on("error", (err) => {
|
|
2016
|
+
this.push({
|
|
2017
|
+
jsonrpc: "2.0",
|
|
2018
|
+
id: null,
|
|
2019
|
+
error: { code: -32e3, message: `transport error: ${err.message}` }
|
|
2020
|
+
});
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
async send(message) {
|
|
2024
|
+
if (this.closed) throw new Error("MCP transport is closed");
|
|
2025
|
+
return new Promise((resolve2, reject) => {
|
|
2026
|
+
const line = `${JSON.stringify(message)}
|
|
2027
|
+
`;
|
|
2028
|
+
this.child.stdin.write(line, "utf8", (err) => {
|
|
2029
|
+
if (err) reject(err);
|
|
2030
|
+
else resolve2();
|
|
2031
|
+
});
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
async *messages() {
|
|
2035
|
+
while (true) {
|
|
2036
|
+
if (this.queue.length > 0) {
|
|
2037
|
+
yield this.queue.shift();
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
if (this.closed) return;
|
|
2041
|
+
const next = await new Promise((resolve2) => {
|
|
2042
|
+
this.waiters.push(resolve2);
|
|
2043
|
+
});
|
|
2044
|
+
if (next === null) return;
|
|
2045
|
+
yield next;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
async close() {
|
|
2049
|
+
if (this.closed) return;
|
|
2050
|
+
this.closed = true;
|
|
2051
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2052
|
+
try {
|
|
2053
|
+
this.child.stdin.end();
|
|
2054
|
+
} catch {
|
|
2055
|
+
}
|
|
2056
|
+
if (this.child.exitCode === null && !this.child.killed) {
|
|
2057
|
+
this.child.kill("SIGTERM");
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
/** Parse incoming stdout chunks into NDJSON messages. */
|
|
2061
|
+
onStdout(chunk) {
|
|
2062
|
+
this.stdoutBuffer += chunk;
|
|
2063
|
+
let newlineIdx;
|
|
2064
|
+
while ((newlineIdx = this.stdoutBuffer.indexOf("\n")) !== -1) {
|
|
2065
|
+
const line = this.stdoutBuffer.slice(0, newlineIdx).trim();
|
|
2066
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIdx + 1);
|
|
2067
|
+
if (!line) continue;
|
|
2068
|
+
try {
|
|
2069
|
+
const msg = JSON.parse(line);
|
|
2070
|
+
this.push(msg);
|
|
2071
|
+
} catch {
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
onClose() {
|
|
2076
|
+
this.closed = true;
|
|
2077
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2078
|
+
}
|
|
2079
|
+
push(msg) {
|
|
2080
|
+
const waiter = this.waiters.shift();
|
|
2081
|
+
if (waiter) waiter(msg);
|
|
2082
|
+
else this.queue.push(msg);
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
|
|
2086
|
+
// src/mcp/registry.ts
|
|
2087
|
+
async function bridgeMcpTools(client, opts = {}) {
|
|
2088
|
+
const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
|
|
2089
|
+
const prefix = opts.namePrefix ?? "";
|
|
2090
|
+
const result = { registry, registeredNames: [], skipped: [] };
|
|
2091
|
+
const listed = await client.listTools();
|
|
2092
|
+
for (const mcpTool of listed.tools) {
|
|
2093
|
+
if (!mcpTool.name) {
|
|
2094
|
+
result.skipped.push({ name: "?", reason: "empty tool name" });
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
const registeredName = `${prefix}${mcpTool.name}`;
|
|
2098
|
+
registry.register({
|
|
2099
|
+
name: registeredName,
|
|
2100
|
+
description: mcpTool.description ?? "",
|
|
2101
|
+
parameters: mcpTool.inputSchema,
|
|
2102
|
+
fn: async (args) => {
|
|
2103
|
+
const toolResult = await client.callTool(mcpTool.name, args);
|
|
2104
|
+
return flattenMcpResult(toolResult);
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
result.registeredNames.push(registeredName);
|
|
2108
|
+
}
|
|
2109
|
+
return result;
|
|
2110
|
+
}
|
|
2111
|
+
function flattenMcpResult(result) {
|
|
2112
|
+
const parts = result.content.map(blockToString);
|
|
2113
|
+
const joined = parts.join("\n").trim();
|
|
2114
|
+
if (result.isError) {
|
|
2115
|
+
return `ERROR: ${joined || "(no error message from server)"}`;
|
|
2116
|
+
}
|
|
2117
|
+
return joined;
|
|
2118
|
+
}
|
|
2119
|
+
function blockToString(block) {
|
|
2120
|
+
if (block.type === "text") return block.text;
|
|
2121
|
+
if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
|
|
2122
|
+
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
1774
2125
|
// src/config.ts
|
|
1775
2126
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
1776
2127
|
import { homedir as homedir2 } from "os";
|
|
@@ -1815,23 +2166,93 @@ function redactKey(key) {
|
|
|
1815
2166
|
}
|
|
1816
2167
|
|
|
1817
2168
|
// src/index.ts
|
|
1818
|
-
var VERSION = "0.
|
|
2169
|
+
var VERSION = "0.3.0-alpha.1";
|
|
1819
2170
|
|
|
1820
2171
|
// src/cli/commands/chat.tsx
|
|
1821
2172
|
import { render } from "ink";
|
|
1822
|
-
import
|
|
2173
|
+
import React8, { useState as useState4 } from "react";
|
|
2174
|
+
|
|
2175
|
+
// src/mcp/shell-split.ts
|
|
2176
|
+
function shellSplit(input) {
|
|
2177
|
+
const tokens = [];
|
|
2178
|
+
let cur = "";
|
|
2179
|
+
let quote = null;
|
|
2180
|
+
let i = 0;
|
|
2181
|
+
const s = input;
|
|
2182
|
+
while (i < s.length) {
|
|
2183
|
+
const ch = s[i];
|
|
2184
|
+
if (quote) {
|
|
2185
|
+
if (ch === quote) {
|
|
2186
|
+
quote = null;
|
|
2187
|
+
i++;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
if (ch === "\\" && quote === '"' && i + 1 < s.length) {
|
|
2191
|
+
cur += s[i + 1];
|
|
2192
|
+
i += 2;
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
cur += ch;
|
|
2196
|
+
i++;
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
if (ch === '"' || ch === "'") {
|
|
2200
|
+
quote = ch;
|
|
2201
|
+
i++;
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
2205
|
+
cur += s[i + 1];
|
|
2206
|
+
i += 2;
|
|
2207
|
+
continue;
|
|
2208
|
+
}
|
|
2209
|
+
if (ch === " " || ch === " ") {
|
|
2210
|
+
if (cur.length > 0) {
|
|
2211
|
+
tokens.push(cur);
|
|
2212
|
+
cur = "";
|
|
2213
|
+
}
|
|
2214
|
+
i++;
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
cur += ch;
|
|
2218
|
+
i++;
|
|
2219
|
+
}
|
|
2220
|
+
if (quote) {
|
|
2221
|
+
throw new Error(
|
|
2222
|
+
`shellSplit: unterminated ${quote === '"' ? "double" : "single"} quote in input`
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
if (cur.length > 0) tokens.push(cur);
|
|
2226
|
+
return tokens;
|
|
2227
|
+
}
|
|
1823
2228
|
|
|
1824
2229
|
// src/cli/ui/App.tsx
|
|
1825
|
-
import { Box as
|
|
1826
|
-
import
|
|
2230
|
+
import { Box as Box6, Static, Text as Text6, useApp } from "ink";
|
|
2231
|
+
import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
1827
2232
|
|
|
1828
2233
|
// src/cli/ui/EventLog.tsx
|
|
1829
|
-
import { Box as
|
|
1830
|
-
import
|
|
2234
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
2235
|
+
import React3, { useEffect, useState } from "react";
|
|
1831
2236
|
|
|
1832
|
-
// src/cli/ui/
|
|
2237
|
+
// src/cli/ui/PlanStateBlock.tsx
|
|
1833
2238
|
import { Box, Text } from "ink";
|
|
1834
2239
|
import React from "react";
|
|
2240
|
+
function PlanStateBlock({ planState }) {
|
|
2241
|
+
const fields = [];
|
|
2242
|
+
if (planState.subgoals.length) fields.push(["subgoals", planState.subgoals, "cyan", false]);
|
|
2243
|
+
if (planState.hypotheses.length)
|
|
2244
|
+
fields.push(["hypotheses", planState.hypotheses, "green", false]);
|
|
2245
|
+
if (planState.uncertainties.length)
|
|
2246
|
+
fields.push(["uncertainties", planState.uncertainties, "yellow", false]);
|
|
2247
|
+
if (planState.rejectedPaths.length)
|
|
2248
|
+
fields.push(["rejected", planState.rejectedPaths, "red", true]);
|
|
2249
|
+
if (fields.length === 0) return null;
|
|
2250
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color, dim]) => /* @__PURE__ */ React.createElement(Text, { key: label }, /* @__PURE__ */ React.createElement(Text, { color, bold: true, dimColor: dim }, "\u2039 ", label), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` (${items.length})`), /* @__PURE__ */ React.createElement(Text, null, `: ${items.join(" \xB7 ")}`))));
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// src/cli/ui/markdown.tsx
|
|
2254
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
2255
|
+
import React2 from "react";
|
|
1835
2256
|
var SUPERSCRIPT = {
|
|
1836
2257
|
"0": "\u2070",
|
|
1837
2258
|
"1": "\xB9",
|
|
@@ -1888,27 +2309,27 @@ function InlineMd({ text }) {
|
|
|
1888
2309
|
for (const m of text.matchAll(INLINE_RE)) {
|
|
1889
2310
|
const start = m.index ?? 0;
|
|
1890
2311
|
if (start > last) {
|
|
1891
|
-
parts.push(/* @__PURE__ */
|
|
2312
|
+
parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last, start)));
|
|
1892
2313
|
}
|
|
1893
2314
|
if (m[2] !== void 0) {
|
|
1894
2315
|
parts.push(
|
|
1895
|
-
/* @__PURE__ */
|
|
2316
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[2])
|
|
1896
2317
|
);
|
|
1897
2318
|
} else if (m[3] !== void 0) {
|
|
1898
2319
|
parts.push(
|
|
1899
|
-
/* @__PURE__ */
|
|
2320
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[3])
|
|
1900
2321
|
);
|
|
1901
2322
|
} else if (m[4] !== void 0) {
|
|
1902
2323
|
parts.push(
|
|
1903
|
-
/* @__PURE__ */
|
|
2324
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[4])
|
|
1904
2325
|
);
|
|
1905
2326
|
}
|
|
1906
2327
|
last = start + m[0].length;
|
|
1907
2328
|
}
|
|
1908
2329
|
if (last < text.length) {
|
|
1909
|
-
parts.push(/* @__PURE__ */
|
|
2330
|
+
parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last)));
|
|
1910
2331
|
}
|
|
1911
|
-
return /* @__PURE__ */
|
|
2332
|
+
return /* @__PURE__ */ React2.createElement(Text2, null, parts);
|
|
1912
2333
|
}
|
|
1913
2334
|
function parseBlocks(raw) {
|
|
1914
2335
|
const lines = raw.split(/\r?\n/);
|
|
@@ -2002,42 +2423,42 @@ function parseBlocks(raw) {
|
|
|
2002
2423
|
function BlockView({ block }) {
|
|
2003
2424
|
switch (block.kind) {
|
|
2004
2425
|
case "heading":
|
|
2005
|
-
return /* @__PURE__ */
|
|
2426
|
+
return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text }));
|
|
2006
2427
|
case "paragraph":
|
|
2007
|
-
return /* @__PURE__ */
|
|
2428
|
+
return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text });
|
|
2008
2429
|
case "bullet":
|
|
2009
|
-
return /* @__PURE__ */
|
|
2430
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React2.createElement(Box2, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React2.createElement(InlineMd, { text: item }))));
|
|
2010
2431
|
case "code":
|
|
2011
|
-
return /* @__PURE__ */
|
|
2432
|
+
return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
|
|
2012
2433
|
case "hr":
|
|
2013
|
-
return /* @__PURE__ */
|
|
2434
|
+
return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2014
2435
|
}
|
|
2015
2436
|
}
|
|
2016
2437
|
function Markdown({ text }) {
|
|
2017
2438
|
const cleaned = stripMath(text);
|
|
2018
|
-
const blocks =
|
|
2019
|
-
return /* @__PURE__ */
|
|
2439
|
+
const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
2440
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React2.createElement(BlockView, { key: `${i}-${b.kind}`, block: b })));
|
|
2020
2441
|
}
|
|
2021
2442
|
|
|
2022
2443
|
// src/cli/ui/EventLog.tsx
|
|
2023
|
-
var EventRow =
|
|
2444
|
+
var EventRow = React3.memo(function EventRow2({ event }) {
|
|
2024
2445
|
if (event.role === "user") {
|
|
2025
|
-
return /* @__PURE__ */
|
|
2446
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React3.createElement(Text3, null, event.text));
|
|
2026
2447
|
}
|
|
2027
2448
|
if (event.role === "assistant") {
|
|
2028
|
-
if (event.streaming) return /* @__PURE__ */
|
|
2029
|
-
return /* @__PURE__ */
|
|
2449
|
+
if (event.streaming) return /* @__PURE__ */ React3.createElement(StreamingAssistant, { event });
|
|
2450
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React3.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React3.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React3.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React3.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React3.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React3.createElement(Text3, { color: "magenta" }, event.repair) : null);
|
|
2030
2451
|
}
|
|
2031
2452
|
if (event.role === "tool") {
|
|
2032
|
-
return /* @__PURE__ */
|
|
2453
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", truncate2(event.text, 400)));
|
|
2033
2454
|
}
|
|
2034
2455
|
if (event.role === "error") {
|
|
2035
|
-
return /* @__PURE__ */
|
|
2456
|
+
return /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, event.text));
|
|
2036
2457
|
}
|
|
2037
2458
|
if (event.role === "info") {
|
|
2038
|
-
return /* @__PURE__ */
|
|
2459
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, event.text));
|
|
2039
2460
|
}
|
|
2040
|
-
return /* @__PURE__ */
|
|
2461
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
|
|
2041
2462
|
});
|
|
2042
2463
|
function BranchBlock({ branch }) {
|
|
2043
2464
|
const per = branch.uncertainties.map((u, i) => {
|
|
@@ -2045,21 +2466,13 @@ function BranchBlock({ branch }) {
|
|
|
2045
2466
|
const t = (branch.temperatures[i] ?? 0).toFixed(1);
|
|
2046
2467
|
return `${marker} #${i} T=${t} u=${u}`;
|
|
2047
2468
|
}).join(" ");
|
|
2048
|
-
return /* @__PURE__ */
|
|
2049
|
-
}
|
|
2050
|
-
function PlanStateBlock({ planState }) {
|
|
2051
|
-
const lines = [];
|
|
2052
|
-
if (planState.subgoals.length) lines.push(["subgoals", planState.subgoals]);
|
|
2053
|
-
if (planState.hypotheses.length) lines.push(["hypotheses", planState.hypotheses]);
|
|
2054
|
-
if (planState.uncertainties.length) lines.push(["uncertainties", planState.uncertainties]);
|
|
2055
|
-
if (planState.rejectedPaths.length) lines.push(["rejected", planState.rejectedPaths]);
|
|
2056
|
-
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, lines.map(([label, items]) => /* @__PURE__ */ React2.createElement(Text2, { key: label, color: "magenta" }, "\u2039 ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, label), ` (${items.length}): ${items.join(" \xB7 ")}`)));
|
|
2469
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React3.createElement(Text3, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, per)));
|
|
2057
2470
|
}
|
|
2058
2471
|
function ReasoningBlock({ reasoning }) {
|
|
2059
2472
|
const max = 220;
|
|
2060
2473
|
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
2061
2474
|
const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
|
|
2062
|
-
return /* @__PURE__ */
|
|
2475
|
+
return /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
|
|
2063
2476
|
}
|
|
2064
2477
|
function Elapsed() {
|
|
2065
2478
|
const [s, setS] = useState(0);
|
|
@@ -2070,20 +2483,20 @@ function Elapsed() {
|
|
|
2070
2483
|
}, []);
|
|
2071
2484
|
const mm = String(Math.floor(s / 60)).padStart(2, "0");
|
|
2072
2485
|
const ss = String(s % 60).padStart(2, "0");
|
|
2073
|
-
return /* @__PURE__ */
|
|
2486
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, `${mm}:${ss}`);
|
|
2074
2487
|
}
|
|
2075
2488
|
function StreamingAssistant({ event }) {
|
|
2076
2489
|
if (event.branchProgress) {
|
|
2077
2490
|
const p = event.branchProgress;
|
|
2078
2491
|
if (p.completed === 0) {
|
|
2079
|
-
return /* @__PURE__ */
|
|
2492
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", "spread across T=0.0/0.5/1.0 \xB7 typical wait 30-90s for reasoner"));
|
|
2080
2493
|
}
|
|
2081
2494
|
const pct2 = Math.round(p.completed / p.total * 100);
|
|
2082
|
-
return /* @__PURE__ */
|
|
2495
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (", pct2, "%)", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
|
|
2083
2496
|
}
|
|
2084
2497
|
const tail = lastLine(event.text, 140);
|
|
2085
2498
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
2086
|
-
return /* @__PURE__ */
|
|
2499
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(streaming \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
|
|
2087
2500
|
}
|
|
2088
2501
|
function lastLine(s, maxChars) {
|
|
2089
2502
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
@@ -2092,16 +2505,16 @@ function lastLine(s, maxChars) {
|
|
|
2092
2505
|
}
|
|
2093
2506
|
function StatsLine({ stats }) {
|
|
2094
2507
|
const hit = (stats.cacheHitRatio * 100).toFixed(1);
|
|
2095
|
-
return /* @__PURE__ */
|
|
2508
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
|
|
2096
2509
|
}
|
|
2097
2510
|
function truncate2(s, max) {
|
|
2098
2511
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
2099
2512
|
}
|
|
2100
2513
|
|
|
2101
2514
|
// src/cli/ui/PromptInput.tsx
|
|
2102
|
-
import { Box as
|
|
2515
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
2103
2516
|
import TextInput from "ink-text-input";
|
|
2104
|
-
import
|
|
2517
|
+
import React4 from "react";
|
|
2105
2518
|
function PromptInput({
|
|
2106
2519
|
value,
|
|
2107
2520
|
onChange,
|
|
@@ -2110,7 +2523,7 @@ function PromptInput({
|
|
|
2110
2523
|
placeholder
|
|
2111
2524
|
}) {
|
|
2112
2525
|
const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
|
|
2113
|
-
return /* @__PURE__ */
|
|
2526
|
+
return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: disabled ? "gray" : "cyan", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: disabled ? "gray" : "cyan" }, "you \u203A", " "), /* @__PURE__ */ React4.createElement(
|
|
2114
2527
|
TextInput,
|
|
2115
2528
|
{
|
|
2116
2529
|
value,
|
|
@@ -2123,8 +2536,8 @@ function PromptInput({
|
|
|
2123
2536
|
}
|
|
2124
2537
|
|
|
2125
2538
|
// src/cli/ui/StatsPanel.tsx
|
|
2126
|
-
import { Box as
|
|
2127
|
-
import
|
|
2539
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
2540
|
+
import React5 from "react";
|
|
2128
2541
|
function StatsPanel({
|
|
2129
2542
|
summary,
|
|
2130
2543
|
model,
|
|
@@ -2135,7 +2548,7 @@ function StatsPanel({
|
|
|
2135
2548
|
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
2136
2549
|
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
2137
2550
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
2138
|
-
return /* @__PURE__ */
|
|
2551
|
+
return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cache hit "), /* @__PURE__ */ React5.createElement(Text5, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cost "), /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React5.createElement(Text5, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "saving "), /* @__PURE__ */ React5.createElement(Text5, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
|
|
2139
2552
|
}
|
|
2140
2553
|
|
|
2141
2554
|
// src/cli/ui/slash.ts
|
|
@@ -2268,7 +2681,7 @@ function handleSlash(cmd, args, loop) {
|
|
|
2268
2681
|
|
|
2269
2682
|
// src/cli/ui/App.tsx
|
|
2270
2683
|
var FLUSH_INTERVAL_MS = 60;
|
|
2271
|
-
function App({ model, system, transcript, harvest: harvest2, branch, session }) {
|
|
2684
|
+
function App({ model, system, transcript, harvest: harvest2, branch, session, tools }) {
|
|
2272
2685
|
const { exit } = useApp();
|
|
2273
2686
|
const [historical, setHistorical] = useState2([]);
|
|
2274
2687
|
const [streaming, setStreaming] = useState2(null);
|
|
@@ -2299,11 +2712,14 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
2299
2712
|
const loop = useMemo(() => {
|
|
2300
2713
|
if (loopRef.current) return loopRef.current;
|
|
2301
2714
|
const client = new DeepSeekClient();
|
|
2302
|
-
const prefix = new ImmutablePrefix({
|
|
2303
|
-
|
|
2715
|
+
const prefix = new ImmutablePrefix({
|
|
2716
|
+
system,
|
|
2717
|
+
toolSpecs: tools?.specs()
|
|
2718
|
+
});
|
|
2719
|
+
const l = new CacheFirstLoop({ client, prefix, tools, model, harvest: harvest2, branch, session });
|
|
2304
2720
|
loopRef.current = l;
|
|
2305
2721
|
return l;
|
|
2306
|
-
}, [model, system, harvest2, branch, session]);
|
|
2722
|
+
}, [model, system, harvest2, branch, session, tools]);
|
|
2307
2723
|
const sessionBannerShown = useRef(false);
|
|
2308
2724
|
useEffect2(() => {
|
|
2309
2725
|
if (sessionBannerShown.current) return;
|
|
@@ -2466,7 +2882,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
2466
2882
|
},
|
|
2467
2883
|
[busy, exit, loop, writeTranscript]
|
|
2468
2884
|
);
|
|
2469
|
-
return /* @__PURE__ */
|
|
2885
|
+
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
|
|
2470
2886
|
StatsPanel,
|
|
2471
2887
|
{
|
|
2472
2888
|
summary,
|
|
@@ -2475,10 +2891,10 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
2475
2891
|
harvestOn: loop.harvestEnabled,
|
|
2476
2892
|
branchBudget: loop.branchOptions.budget
|
|
2477
2893
|
}
|
|
2478
|
-
), /* @__PURE__ */
|
|
2894
|
+
), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, null));
|
|
2479
2895
|
}
|
|
2480
2896
|
function CommandStrip() {
|
|
2481
|
-
return /* @__PURE__ */
|
|
2897
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /sessions \xB7 /model \xB7 /harvest \xB7 /branch \xB7 /clear \xB7 /exit"));
|
|
2482
2898
|
}
|
|
2483
2899
|
function describeRepair(repair) {
|
|
2484
2900
|
const parts = [];
|
|
@@ -2489,9 +2905,9 @@ function describeRepair(repair) {
|
|
|
2489
2905
|
}
|
|
2490
2906
|
|
|
2491
2907
|
// src/cli/ui/Setup.tsx
|
|
2492
|
-
import { Box as
|
|
2908
|
+
import { Box as Box7, Text as Text7, useApp as useApp2 } from "ink";
|
|
2493
2909
|
import TextInput2 from "ink-text-input";
|
|
2494
|
-
import
|
|
2910
|
+
import React7, { useState as useState3 } from "react";
|
|
2495
2911
|
function Setup({ onReady }) {
|
|
2496
2912
|
const [value, setValue] = useState3("");
|
|
2497
2913
|
const [error, setError] = useState3(null);
|
|
@@ -2515,7 +2931,7 @@ function Setup({ onReady }) {
|
|
|
2515
2931
|
}
|
|
2516
2932
|
onReady(trimmed);
|
|
2517
2933
|
};
|
|
2518
|
-
return /* @__PURE__ */
|
|
2934
|
+
return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React7.createElement(
|
|
2519
2935
|
TextInput2,
|
|
2520
2936
|
{
|
|
2521
2937
|
value,
|
|
@@ -2524,14 +2940,14 @@ function Setup({ onReady }) {
|
|
|
2524
2940
|
mask: "\u2022",
|
|
2525
2941
|
placeholder: "sk-..."
|
|
2526
2942
|
}
|
|
2527
|
-
)), error ? /* @__PURE__ */
|
|
2943
|
+
)), error ? /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "red" }, error)) : value ? /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "(Type /exit to abort.)")));
|
|
2528
2944
|
}
|
|
2529
2945
|
|
|
2530
2946
|
// src/cli/commands/chat.tsx
|
|
2531
|
-
function Root({ initialKey, ...appProps }) {
|
|
2947
|
+
function Root({ initialKey, tools, ...appProps }) {
|
|
2532
2948
|
const [key, setKey] = useState4(initialKey);
|
|
2533
2949
|
if (!key) {
|
|
2534
|
-
return /* @__PURE__ */
|
|
2950
|
+
return /* @__PURE__ */ React8.createElement(
|
|
2535
2951
|
Setup,
|
|
2536
2952
|
{
|
|
2537
2953
|
onReady: (k) => {
|
|
@@ -2542,7 +2958,7 @@ function Root({ initialKey, ...appProps }) {
|
|
|
2542
2958
|
);
|
|
2543
2959
|
}
|
|
2544
2960
|
process.env.DEEPSEEK_API_KEY = key;
|
|
2545
|
-
return /* @__PURE__ */
|
|
2961
|
+
return /* @__PURE__ */ React8.createElement(
|
|
2546
2962
|
App,
|
|
2547
2963
|
{
|
|
2548
2964
|
model: appProps.model,
|
|
@@ -2550,40 +2966,282 @@ function Root({ initialKey, ...appProps }) {
|
|
|
2550
2966
|
transcript: appProps.transcript,
|
|
2551
2967
|
harvest: appProps.harvest,
|
|
2552
2968
|
branch: appProps.branch,
|
|
2553
|
-
session: appProps.session
|
|
2969
|
+
session: appProps.session,
|
|
2970
|
+
tools
|
|
2554
2971
|
}
|
|
2555
2972
|
);
|
|
2556
2973
|
}
|
|
2557
2974
|
async function chatCommand(opts) {
|
|
2558
2975
|
loadDotenv();
|
|
2559
2976
|
const initialKey = loadApiKey();
|
|
2560
|
-
|
|
2977
|
+
let mcp;
|
|
2978
|
+
let tools;
|
|
2979
|
+
if (opts.mcp) {
|
|
2980
|
+
const argv = shellSplit(opts.mcp);
|
|
2981
|
+
if (argv.length === 0) {
|
|
2982
|
+
process.stderr.write("error: --mcp requires a command\n");
|
|
2983
|
+
process.exit(2);
|
|
2984
|
+
}
|
|
2985
|
+
const [command, ...args] = argv;
|
|
2986
|
+
if (!command) {
|
|
2987
|
+
process.stderr.write("error: --mcp command is empty\n");
|
|
2988
|
+
process.exit(2);
|
|
2989
|
+
}
|
|
2990
|
+
const transport = new StdioTransport({ command, args });
|
|
2991
|
+
mcp = new McpClient({ transport });
|
|
2992
|
+
try {
|
|
2993
|
+
await mcp.initialize();
|
|
2994
|
+
const bridge = await bridgeMcpTools(mcp, { namePrefix: opts.mcpPrefix });
|
|
2995
|
+
tools = bridge.registry;
|
|
2996
|
+
process.stderr.write(
|
|
2997
|
+
`\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
|
|
2998
|
+
`
|
|
2999
|
+
);
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
process.stderr.write(`MCP setup failed: ${err.message}
|
|
3002
|
+
`);
|
|
3003
|
+
await mcp.close();
|
|
3004
|
+
process.exit(1);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
const { waitUntilExit } = render(/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, ...opts }), {
|
|
2561
3008
|
exitOnCtrlC: true
|
|
2562
3009
|
});
|
|
2563
|
-
|
|
3010
|
+
try {
|
|
3011
|
+
await waitUntilExit();
|
|
3012
|
+
} finally {
|
|
3013
|
+
await mcp?.close();
|
|
3014
|
+
}
|
|
2564
3015
|
}
|
|
2565
3016
|
|
|
2566
3017
|
// src/cli/commands/diff.ts
|
|
2567
3018
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2568
3019
|
import { basename } from "path";
|
|
2569
|
-
|
|
3020
|
+
import { render as render2 } from "ink";
|
|
3021
|
+
import React11 from "react";
|
|
3022
|
+
|
|
3023
|
+
// src/cli/ui/DiffApp.tsx
|
|
3024
|
+
import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput } from "ink";
|
|
3025
|
+
import React10, { useState as useState5 } from "react";
|
|
3026
|
+
|
|
3027
|
+
// src/cli/ui/RecordView.tsx
|
|
3028
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
3029
|
+
import React9 from "react";
|
|
3030
|
+
function RecordView({ rec, compact = false }) {
|
|
3031
|
+
const toolArgsMax = compact ? 120 : 200;
|
|
3032
|
+
const toolContentMax = compact ? 200 : 400;
|
|
3033
|
+
if (rec.role === "user") {
|
|
3034
|
+
return /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React9.createElement(Text8, null, rec.content));
|
|
3035
|
+
}
|
|
3036
|
+
if (rec.role === "assistant_final") {
|
|
3037
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React9.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React9.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React9.createElement(Text8, null, rec.content) : /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "(tool-call response only)"));
|
|
3038
|
+
}
|
|
3039
|
+
if (rec.role === "tool") {
|
|
3040
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
|
|
3041
|
+
}
|
|
3042
|
+
if (rec.role === "error") {
|
|
3043
|
+
return /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React9.createElement(Text8, { color: "red" }, rec.error ?? rec.content));
|
|
3044
|
+
}
|
|
3045
|
+
if (rec.role === "done" || rec.role === "assistant_delta") {
|
|
3046
|
+
return null;
|
|
3047
|
+
}
|
|
3048
|
+
return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "[", rec.role, "] ", rec.content));
|
|
3049
|
+
}
|
|
3050
|
+
function CacheBadge({ usage }) {
|
|
3051
|
+
const hit = usage.prompt_cache_hit_tokens ?? 0;
|
|
3052
|
+
const miss = usage.prompt_cache_miss_tokens ?? 0;
|
|
3053
|
+
const total = hit + miss;
|
|
3054
|
+
if (total === 0) return null;
|
|
3055
|
+
const pct2 = hit / total * 100;
|
|
3056
|
+
const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
|
|
3057
|
+
return /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React9.createElement(Text8, { color }, pct2.toFixed(1), "%"));
|
|
3058
|
+
}
|
|
3059
|
+
function truncate3(s, max) {
|
|
3060
|
+
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// src/cli/ui/DiffApp.tsx
|
|
3064
|
+
function DiffApp({ report }) {
|
|
3065
|
+
const { exit } = useApp3();
|
|
3066
|
+
const maxIdx = Math.max(0, report.pairs.length - 1);
|
|
3067
|
+
const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
|
|
3068
|
+
const [idx, setIdx] = useState5(Math.max(0, initialIdx));
|
|
3069
|
+
useInput((input, key) => {
|
|
3070
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
3071
|
+
exit();
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
if (input === "j" || key.downArrow || input === " " || key.return) {
|
|
3075
|
+
setIdx((i) => Math.min(maxIdx, i + 1));
|
|
3076
|
+
} else if (input === "k" || key.upArrow) {
|
|
3077
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
3078
|
+
} else if (input === "g") {
|
|
3079
|
+
setIdx(0);
|
|
3080
|
+
} else if (input === "G") {
|
|
3081
|
+
setIdx(maxIdx);
|
|
3082
|
+
} else if (input === "n") {
|
|
3083
|
+
const next = findNextDivergence(report.pairs, idx);
|
|
3084
|
+
if (next !== -1) setIdx(next);
|
|
3085
|
+
} else if (input === "N" || input === "p") {
|
|
3086
|
+
const prev = findPrevDivergence(report.pairs, idx);
|
|
3087
|
+
if (prev !== -1) setIdx(prev);
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
const pair = report.pairs[idx];
|
|
3091
|
+
return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React10.createElement(DiffHeader, { report }), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React10.createElement(Text9, null, pair ? /* @__PURE__ */ React10.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React10.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React10.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React10.createElement(Text9, null, pair.divergenceNote)) : null, /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "j"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "k"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "N"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "g"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "q"), " ", "quit")));
|
|
3092
|
+
}
|
|
3093
|
+
function DiffHeader({ report }) {
|
|
3094
|
+
const a = report.a;
|
|
3095
|
+
const b = report.b;
|
|
3096
|
+
const cacheDelta = b.stats.cacheHitRatio - a.stats.cacheHitRatio;
|
|
3097
|
+
const costDelta2 = a.stats.totalCostUsd > 0 ? (b.stats.totalCostUsd - a.stats.totalCostUsd) / a.stats.totalCostUsd * 100 : 0;
|
|
3098
|
+
const aStable = a.stats.prefixHashes.length <= 1;
|
|
3099
|
+
const bStable = b.stats.prefixHashes.length <= 1;
|
|
3100
|
+
let prefixLine = null;
|
|
3101
|
+
if (aStable !== bStable) {
|
|
3102
|
+
const stableLabel = aStable ? report.a.label : report.b.label;
|
|
3103
|
+
const churnLabel = aStable ? report.b.label : report.a.label;
|
|
3104
|
+
const churnCount = aStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;
|
|
3105
|
+
prefixLine = `${stableLabel} stayed byte-stable; ${churnLabel} churned ${churnCount} distinct prefixes.`;
|
|
3106
|
+
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
3107
|
+
prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
|
|
3108
|
+
}
|
|
3109
|
+
return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React10.createElement(Box9, { justifyContent: "space-between" }, /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, a.label), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " vs B="), /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, b.label)), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "cache "), /* @__PURE__ */ React10.createElement(Text9, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React10.createElement(Text9, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React10.createElement(Text9, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "cost "), /* @__PURE__ */ React10.createElement(Text9, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React10.createElement(Text9, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React10.createElement(Text9, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "model calls "), /* @__PURE__ */ React10.createElement(Text9, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true, italic: true }, prefixLine)) : null);
|
|
3110
|
+
}
|
|
3111
|
+
function Pane({
|
|
3112
|
+
label,
|
|
3113
|
+
headerColor,
|
|
3114
|
+
records
|
|
3115
|
+
}) {
|
|
3116
|
+
return /* @__PURE__ */ React10.createElement(
|
|
3117
|
+
Box9,
|
|
3118
|
+
{
|
|
3119
|
+
flexDirection: "column",
|
|
3120
|
+
flexGrow: 1,
|
|
3121
|
+
paddingX: 1,
|
|
3122
|
+
borderStyle: "single",
|
|
3123
|
+
borderColor: headerColor
|
|
3124
|
+
},
|
|
3125
|
+
/* @__PURE__ */ React10.createElement(Text9, { color: headerColor, bold: true }, label),
|
|
3126
|
+
records.length === 0 ? /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React10.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React10.createElement(RecordView, { key, rec, compact: true }))
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
function KindBadge({ kind }) {
|
|
3130
|
+
if (kind === "match") {
|
|
3131
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "green" }, "\u2713 match");
|
|
3132
|
+
}
|
|
3133
|
+
if (kind === "diverge") {
|
|
3134
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u2605 diverge");
|
|
3135
|
+
}
|
|
3136
|
+
if (kind === "only_in_a") {
|
|
3137
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, "\u2190 only in A");
|
|
3138
|
+
}
|
|
3139
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, "\u2192 only in B");
|
|
3140
|
+
}
|
|
3141
|
+
function paneRecords(pair, side) {
|
|
3142
|
+
if (!pair) return [];
|
|
3143
|
+
const tools = side === "a" ? pair.aTools : pair.bTools;
|
|
3144
|
+
const assistant = side === "a" ? pair.aAssistant : pair.bAssistant;
|
|
3145
|
+
const out = [...tools];
|
|
3146
|
+
if (assistant) out.push(assistant);
|
|
3147
|
+
return out;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// src/cli/commands/diff.ts
|
|
3151
|
+
async function diffCommand(opts) {
|
|
2570
3152
|
const aParsed = readTranscript(opts.a);
|
|
2571
3153
|
const bParsed = readTranscript(opts.b);
|
|
2572
3154
|
const report = diffTranscripts(
|
|
2573
3155
|
{ label: opts.labelA ?? basename(opts.a), parsed: aParsed },
|
|
2574
3156
|
{ label: opts.labelB ?? basename(opts.b), parsed: bParsed }
|
|
2575
3157
|
);
|
|
2576
|
-
|
|
2577
|
-
|
|
3158
|
+
const wantMarkdown = !!opts.mdPath;
|
|
3159
|
+
const wantPrint = opts.print || !process.stdout.isTTY;
|
|
3160
|
+
const wantTui = opts.tui || !wantPrint && !wantMarkdown;
|
|
3161
|
+
if (wantMarkdown) {
|
|
3162
|
+
console.log(renderSummaryTable(report));
|
|
2578
3163
|
const md = renderMarkdown(report);
|
|
2579
3164
|
writeFileSync2(opts.mdPath, md, "utf8");
|
|
2580
3165
|
console.log(`
|
|
2581
3166
|
markdown report written to ${opts.mdPath}`);
|
|
3167
|
+
return;
|
|
2582
3168
|
}
|
|
3169
|
+
if (wantTui) {
|
|
3170
|
+
const { waitUntilExit } = render2(React11.createElement(DiffApp, { report }), {
|
|
3171
|
+
exitOnCtrlC: true
|
|
3172
|
+
});
|
|
3173
|
+
await waitUntilExit();
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
console.log(renderSummaryTable(report));
|
|
2583
3177
|
}
|
|
2584
3178
|
|
|
2585
3179
|
// src/cli/commands/replay.ts
|
|
2586
|
-
|
|
3180
|
+
import { render as render3 } from "ink";
|
|
3181
|
+
import React13 from "react";
|
|
3182
|
+
|
|
3183
|
+
// src/cli/ui/ReplayApp.tsx
|
|
3184
|
+
import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as useInput2 } from "ink";
|
|
3185
|
+
import React12, { useMemo as useMemo2, useState as useState6 } from "react";
|
|
3186
|
+
function ReplayApp({ meta, pages }) {
|
|
3187
|
+
const { exit } = useApp4();
|
|
3188
|
+
const maxIdx = Math.max(0, pages.length - 1);
|
|
3189
|
+
const [idx, setIdx] = useState6(maxIdx);
|
|
3190
|
+
useInput2((input, key) => {
|
|
3191
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
3192
|
+
exit();
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
if (input === "j" || key.downArrow || input === " " || key.return) {
|
|
3196
|
+
setIdx((i) => Math.min(maxIdx, i + 1));
|
|
3197
|
+
} else if (input === "k" || key.upArrow) {
|
|
3198
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
3199
|
+
} else if (input === "g") {
|
|
3200
|
+
setIdx(0);
|
|
3201
|
+
} else if (input === "G") {
|
|
3202
|
+
setIdx(maxIdx);
|
|
3203
|
+
} else if (input === "h" || key.leftArrow) {
|
|
3204
|
+
setIdx(0);
|
|
3205
|
+
} else if (input === "l" || key.rightArrow) {
|
|
3206
|
+
setIdx(maxIdx);
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
const cumStats = useMemo2(() => computeCumulativeStats(pages, idx), [pages, idx]);
|
|
3210
|
+
const summary = {
|
|
3211
|
+
turns: cumStats.turns,
|
|
3212
|
+
totalCostUsd: cumStats.totalCostUsd,
|
|
3213
|
+
claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
|
|
3214
|
+
savingsVsClaudePct: cumStats.savingsVsClaudePct,
|
|
3215
|
+
cacheHitRatio: cumStats.cacheHitRatio
|
|
3216
|
+
};
|
|
3217
|
+
const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
|
|
3218
|
+
const currentPage = pages[idx];
|
|
3219
|
+
const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
|
|
3220
|
+
return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(
|
|
3221
|
+
StatsPanel,
|
|
3222
|
+
{
|
|
3223
|
+
summary,
|
|
3224
|
+
model: cumStats.models[0] ?? meta?.model ?? "?",
|
|
3225
|
+
prefixHash
|
|
3226
|
+
}
|
|
3227
|
+
), /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React12.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React12.createElement(Text10, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React12.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React12.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React12.createElement(Text10, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "j"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "k"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "q"), " quit")));
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// src/cli/commands/replay.ts
|
|
3231
|
+
async function replayCommand(opts) {
|
|
3232
|
+
const wantPrint = opts.print || !process.stdout.isTTY || opts.head !== void 0 || opts.tail !== void 0;
|
|
3233
|
+
if (wantPrint) {
|
|
3234
|
+
printReplay(opts);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
const { parsed } = replayFromFile(opts.path);
|
|
3238
|
+
const pages = groupRecordsByTurn(parsed.records);
|
|
3239
|
+
const { waitUntilExit } = render3(React13.createElement(ReplayApp, { meta: parsed.meta, pages }), {
|
|
3240
|
+
exitOnCtrlC: true
|
|
3241
|
+
});
|
|
3242
|
+
await waitUntilExit();
|
|
3243
|
+
}
|
|
3244
|
+
function printReplay(opts) {
|
|
2587
3245
|
const { parsed, stats } = replayFromFile(opts.path);
|
|
2588
3246
|
if (parsed.meta) {
|
|
2589
3247
|
const m = parsed.meta;
|
|
@@ -2616,6 +3274,11 @@ function replayCommand(opts) {
|
|
|
2616
3274
|
} else if (stats.prefixHashes.length > 1) {
|
|
2617
3275
|
console.log(" (prefix churned \u2014 cache-hostile session)");
|
|
2618
3276
|
}
|
|
3277
|
+
if (stats.harvestedTurns > 0) {
|
|
3278
|
+
console.log(`harvest (Pillar 2): ${stats.harvestedTurns} turn(s) produced plan state`);
|
|
3279
|
+
console.log(` subgoals: ${stats.totalSubgoals}`);
|
|
3280
|
+
console.log(` uncertainties: ${stats.totalUncertainties}`);
|
|
3281
|
+
}
|
|
2619
3282
|
}
|
|
2620
3283
|
function sliceRecords(records, opts) {
|
|
2621
3284
|
if (opts.head !== void 0 && opts.head > 0) return records.slice(0, opts.head);
|
|
@@ -2635,6 +3298,21 @@ function renderRecord(rec) {
|
|
|
2635
3298
|
return total > 0 ? ` cache=${(hit / total * 100).toFixed(1)}%` : "";
|
|
2636
3299
|
})() : "";
|
|
2637
3300
|
console.log(`${turn} AGENT:${cost}${cache} ${oneLine(rec.content)}`);
|
|
3301
|
+
if (rec.planState) {
|
|
3302
|
+
const ps = rec.planState;
|
|
3303
|
+
if (ps.subgoals.length)
|
|
3304
|
+
console.log(` \u2039 subgoals (${ps.subgoals.length}): ${ps.subgoals.join(" \xB7 ")}`);
|
|
3305
|
+
if (ps.hypotheses.length)
|
|
3306
|
+
console.log(` \u2039 hypotheses (${ps.hypotheses.length}): ${ps.hypotheses.join(" \xB7 ")}`);
|
|
3307
|
+
if (ps.uncertainties.length)
|
|
3308
|
+
console.log(
|
|
3309
|
+
` \u2039 uncertainties(${ps.uncertainties.length}): ${ps.uncertainties.join(" \xB7 ")}`
|
|
3310
|
+
);
|
|
3311
|
+
if (ps.rejectedPaths.length)
|
|
3312
|
+
console.log(
|
|
3313
|
+
` \u2039 rejected (${ps.rejectedPaths.length}): ${ps.rejectedPaths.join(" \xB7 ")}`
|
|
3314
|
+
);
|
|
3315
|
+
}
|
|
2638
3316
|
} else if (rec.role === "tool") {
|
|
2639
3317
|
const args = rec.args ? ` args=${oneLine(rec.args, 80)}` : "";
|
|
2640
3318
|
console.log(`${turn} TOOL ${rec.tool ?? "?"}:${args} \u2192 ${oneLine(rec.content, 120)}`);
|
|
@@ -2688,18 +3366,76 @@ async function runCommand(opts) {
|
|
|
2688
3366
|
loadDotenv();
|
|
2689
3367
|
const apiKey = await ensureApiKey();
|
|
2690
3368
|
process.env.DEEPSEEK_API_KEY = apiKey;
|
|
3369
|
+
let mcp;
|
|
3370
|
+
let tools;
|
|
3371
|
+
if (opts.mcp) {
|
|
3372
|
+
const argv = shellSplit(opts.mcp);
|
|
3373
|
+
const [command, ...args] = argv;
|
|
3374
|
+
if (!command) {
|
|
3375
|
+
process.stderr.write("error: --mcp command is empty\n");
|
|
3376
|
+
process.exit(2);
|
|
3377
|
+
}
|
|
3378
|
+
mcp = new McpClient({ transport: new StdioTransport({ command, args }) });
|
|
3379
|
+
try {
|
|
3380
|
+
await mcp.initialize();
|
|
3381
|
+
const bridge = await bridgeMcpTools(mcp, { namePrefix: opts.mcpPrefix });
|
|
3382
|
+
tools = bridge.registry;
|
|
3383
|
+
process.stderr.write(
|
|
3384
|
+
`\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
|
|
3385
|
+
`
|
|
3386
|
+
);
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
process.stderr.write(`MCP setup failed: ${err.message}
|
|
3389
|
+
`);
|
|
3390
|
+
await mcp.close();
|
|
3391
|
+
process.exit(1);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
2691
3394
|
const client = new DeepSeekClient();
|
|
2692
|
-
const prefix = new ImmutablePrefix({
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
3395
|
+
const prefix = new ImmutablePrefix({
|
|
3396
|
+
system: opts.system,
|
|
3397
|
+
toolSpecs: tools?.specs()
|
|
3398
|
+
});
|
|
3399
|
+
const loop = new CacheFirstLoop({
|
|
3400
|
+
client,
|
|
3401
|
+
prefix,
|
|
3402
|
+
tools,
|
|
3403
|
+
model: opts.model,
|
|
3404
|
+
harvest: opts.harvest,
|
|
3405
|
+
branch: opts.branch
|
|
3406
|
+
});
|
|
3407
|
+
const prefixHash = prefix.fingerprint;
|
|
3408
|
+
let transcriptStream = null;
|
|
3409
|
+
if (opts.transcript) {
|
|
3410
|
+
transcriptStream = openTranscriptFile(opts.transcript, {
|
|
3411
|
+
version: 1,
|
|
3412
|
+
source: "reasonix run",
|
|
3413
|
+
model: opts.model,
|
|
3414
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3415
|
+
});
|
|
3416
|
+
writeRecord(transcriptStream, {
|
|
3417
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3418
|
+
turn: 1,
|
|
3419
|
+
role: "user",
|
|
3420
|
+
content: opts.task
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
try {
|
|
3424
|
+
for await (const ev of loop.step(opts.task)) {
|
|
3425
|
+
if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
|
|
3426
|
+
if (ev.role === "tool") process.stdout.write(`
|
|
2697
3427
|
[tool ${ev.toolName}] ${ev.content}
|
|
2698
3428
|
`);
|
|
2699
|
-
|
|
3429
|
+
if (ev.role === "error") process.stderr.write(`
|
|
2700
3430
|
[error] ${ev.error}
|
|
2701
3431
|
`);
|
|
2702
|
-
|
|
3432
|
+
if (ev.role === "done") process.stdout.write("\n");
|
|
3433
|
+
if (transcriptStream && ev.role !== "assistant_delta") {
|
|
3434
|
+
writeRecord(transcriptStream, recordFromLoopEvent(ev, { model: opts.model, prefixHash }));
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
} finally {
|
|
3438
|
+
transcriptStream?.end();
|
|
2703
3439
|
}
|
|
2704
3440
|
const s = loop.stats.summary();
|
|
2705
3441
|
process.stdout.write(
|
|
@@ -2707,6 +3443,90 @@ async function runCommand(opts) {
|
|
|
2707
3443
|
\u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
|
|
2708
3444
|
`
|
|
2709
3445
|
);
|
|
3446
|
+
if (opts.transcript) {
|
|
3447
|
+
process.stdout.write(`
|
|
3448
|
+
transcript: ${opts.transcript}
|
|
3449
|
+
`);
|
|
3450
|
+
process.stdout.write(` \u2192 npx reasonix replay ${opts.transcript}
|
|
3451
|
+
`);
|
|
3452
|
+
}
|
|
3453
|
+
await mcp?.close();
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// src/cli/commands/sessions.ts
|
|
3457
|
+
function sessionsCommand(opts) {
|
|
3458
|
+
if (opts.name) {
|
|
3459
|
+
inspectSession(opts.name, !!opts.verbose);
|
|
3460
|
+
} else {
|
|
3461
|
+
listAll();
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
function listAll() {
|
|
3465
|
+
const items = listSessions();
|
|
3466
|
+
if (items.length === 0) {
|
|
3467
|
+
console.log(
|
|
3468
|
+
"no saved sessions yet \u2014 run `reasonix chat` (sessions are auto-saved unless --no-session)."
|
|
3469
|
+
);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
console.log("Saved sessions (~/.reasonix/sessions/):");
|
|
3473
|
+
console.log("");
|
|
3474
|
+
console.log(` ${"name".padEnd(22)} ${"msgs".padStart(6)} ${"size".padStart(8)} modified`);
|
|
3475
|
+
console.log(` ${"\u2500".repeat(60)}`);
|
|
3476
|
+
for (const s of items) {
|
|
3477
|
+
const sizeKb = `${(s.size / 1024).toFixed(1)} KB`;
|
|
3478
|
+
const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
|
|
3479
|
+
console.log(
|
|
3480
|
+
` ${s.name.padEnd(22)} ${String(s.messageCount).padStart(6)} ${sizeKb.padStart(8)} ${when}`
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
console.log("");
|
|
3484
|
+
console.log("Inspect: reasonix sessions <name>");
|
|
3485
|
+
console.log("Resume: reasonix chat --session <name>");
|
|
3486
|
+
}
|
|
3487
|
+
function inspectSession(name, verbose) {
|
|
3488
|
+
const path = sessionPath(name);
|
|
3489
|
+
const messages = loadSessionMessages(name);
|
|
3490
|
+
if (messages.length === 0) {
|
|
3491
|
+
console.error(`no session named "${name}" (or it's empty).`);
|
|
3492
|
+
console.error(`looked at: ${path}`);
|
|
3493
|
+
process.exit(1);
|
|
3494
|
+
}
|
|
3495
|
+
console.log(`[session] ${name} ${messages.length} messages ${path}`);
|
|
3496
|
+
console.log("");
|
|
3497
|
+
let turnIndex = 0;
|
|
3498
|
+
for (const msg of messages) {
|
|
3499
|
+
renderMessage(msg, turnIndex, verbose);
|
|
3500
|
+
if (msg.role === "user") turnIndex++;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
function renderMessage(msg, turnIdx, verbose) {
|
|
3504
|
+
const turn = turnIdx > 0 ? `[t${turnIdx}]` : "[start]";
|
|
3505
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
3506
|
+
const flat = oneLine2(content);
|
|
3507
|
+
if (msg.role === "user") {
|
|
3508
|
+
console.log(`${turn} USER: ${flat}`);
|
|
3509
|
+
} else if (msg.role === "assistant") {
|
|
3510
|
+
console.log(`${turn} AGENT: ${flat || "(tool call only)"}`);
|
|
3511
|
+
if (verbose && msg.tool_calls?.length) {
|
|
3512
|
+
for (const tc of msg.tool_calls) {
|
|
3513
|
+
console.log(
|
|
3514
|
+
` \u2192 call ${tc.function?.name} ${truncate4(tc.function?.arguments ?? "", 80)}`
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
} else if (msg.role === "tool") {
|
|
3519
|
+
console.log(`${turn} TOOL ${msg.name ?? "?"}: ${truncate4(flat, 160)}`);
|
|
3520
|
+
} else if (msg.role === "system") {
|
|
3521
|
+
if (verbose) console.log(`${turn} SYSTEM: ${truncate4(flat, 160)}`);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
function oneLine2(s, max = 200) {
|
|
3525
|
+
const collapsed = s.replace(/\s+/g, " ").trim();
|
|
3526
|
+
return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
|
|
3527
|
+
}
|
|
3528
|
+
function truncate4(s, max) {
|
|
3529
|
+
return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
|
|
2710
3530
|
}
|
|
2711
3531
|
|
|
2712
3532
|
// src/cli/commands/stats.ts
|
|
@@ -2754,7 +3574,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
2754
3574
|
).option(
|
|
2755
3575
|
"--session <name>",
|
|
2756
3576
|
"Use a named session (default: 'default'). Resume the same session next time."
|
|
2757
|
-
).option("--no-session", "Disable session persistence for this run (ephemeral chat)").
|
|
3577
|
+
).option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
|
|
3578
|
+
"--mcp <command>",
|
|
3579
|
+
'Spawn an MCP server and bridge its tools. Shell-quoted: --mcp "npx -y @scope/server-x /path"'
|
|
3580
|
+
).option("--mcp-prefix <str>", "Prefix prepended to every MCP tool name (avoid collisions)").action(async (opts) => {
|
|
2758
3581
|
let session;
|
|
2759
3582
|
if (opts.session === false) {
|
|
2760
3583
|
session = void 0;
|
|
@@ -2769,26 +3592,61 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
2769
3592
|
transcript: opts.transcript,
|
|
2770
3593
|
harvest: !!opts.harvest,
|
|
2771
3594
|
branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
|
|
2772
|
-
session
|
|
3595
|
+
session,
|
|
3596
|
+
mcp: opts.mcp,
|
|
3597
|
+
mcpPrefix: opts.mcpPrefix
|
|
2773
3598
|
});
|
|
2774
3599
|
});
|
|
2775
|
-
program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).
|
|
2776
|
-
|
|
3600
|
+
program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).option(
|
|
3601
|
+
"--harvest",
|
|
3602
|
+
"Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
|
|
3603
|
+
).option(
|
|
3604
|
+
"--branch <n>",
|
|
3605
|
+
"Self-consistency: run N parallel samples per turn and pick the most confident",
|
|
3606
|
+
(v) => Number.parseInt(v, 10)
|
|
3607
|
+
).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
|
|
3608
|
+
"--mcp <command>",
|
|
3609
|
+
'Spawn an MCP server and bridge its tools. Shell-quoted: --mcp "npx -y @scope/server-x /path"'
|
|
3610
|
+
).option("--mcp-prefix <str>", "Prefix prepended to every MCP tool name (avoid collisions)").action(async (task, opts) => {
|
|
3611
|
+
await runCommand({
|
|
3612
|
+
task,
|
|
3613
|
+
model: opts.model,
|
|
3614
|
+
system: opts.system,
|
|
3615
|
+
harvest: !!opts.harvest,
|
|
3616
|
+
branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
|
|
3617
|
+
transcript: opts.transcript,
|
|
3618
|
+
mcp: opts.mcp,
|
|
3619
|
+
mcpPrefix: opts.mcpPrefix
|
|
3620
|
+
});
|
|
2777
3621
|
});
|
|
2778
3622
|
program.command("stats <transcript>").description("Summarize a JSONL transcript produced by `reasonix chat --transcript`.").action((transcript) => {
|
|
2779
3623
|
statsCommand({ transcript });
|
|
2780
3624
|
});
|
|
3625
|
+
program.command("sessions [name]").description("List saved chat sessions, or inspect one by name.").option("-v, --verbose", "Include system prompts + tool-call metadata when inspecting").action((name, opts) => {
|
|
3626
|
+
sessionsCommand({ name, verbose: !!opts.verbose });
|
|
3627
|
+
});
|
|
2781
3628
|
program.command("replay <transcript>").description(
|
|
2782
|
-
"
|
|
2783
|
-
).option("--head <n>", "
|
|
2784
|
-
replayCommand({
|
|
3629
|
+
"Interactive Ink TUI to scrub through a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
|
|
3630
|
+
).option("--print", "Dump to stdout instead of mounting the TUI (auto when piped)").option("--head <n>", "stdout mode only \u2014 show first N records", (v) => Number.parseInt(v, 10)).option("--tail <n>", "stdout mode only \u2014 show last N records", (v) => Number.parseInt(v, 10)).action(async (transcript, opts) => {
|
|
3631
|
+
await replayCommand({
|
|
2785
3632
|
path: transcript,
|
|
3633
|
+
print: !!opts.print,
|
|
2786
3634
|
head: Number.isFinite(opts.head) ? opts.head : void 0,
|
|
2787
3635
|
tail: Number.isFinite(opts.tail) ? opts.tail : void 0
|
|
2788
3636
|
});
|
|
2789
3637
|
});
|
|
2790
|
-
program.command("diff <a> <b>").description(
|
|
2791
|
-
|
|
3638
|
+
program.command("diff <a> <b>").description(
|
|
3639
|
+
"Compare two transcripts in a split-pane Ink TUI (default) or stdout table. Use n/N to jump across divergences."
|
|
3640
|
+
).option("--md <path>", "Write a markdown report (blog-ready) to this path").option("--print", "Force stdout table instead of the TUI (auto when piped)").option("--tui", "Force the TUI even when piped (rare)").option("--label-a <label>", "Display label for transcript A (default: filename)").option("--label-b <label>", "Display label for transcript B (default: filename)").action(async (a, b, opts) => {
|
|
3641
|
+
await diffCommand({
|
|
3642
|
+
a,
|
|
3643
|
+
b,
|
|
3644
|
+
mdPath: opts.md,
|
|
3645
|
+
labelA: opts.labelA,
|
|
3646
|
+
labelB: opts.labelB,
|
|
3647
|
+
print: !!opts.print,
|
|
3648
|
+
tui: !!opts.tui
|
|
3649
|
+
});
|
|
2792
3650
|
});
|
|
2793
3651
|
program.command("version").description("Print Reasonix version.").action(versionCommand);
|
|
2794
3652
|
program.parseAsync(process.argv).catch((err) => {
|