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/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.2.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 React7, { useState as useState4 } from "react";
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 Box5, Static, Text as Text5, useApp } from "ink";
1826
- import React5, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
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 Box2, Text as Text2 } from "ink";
1830
- import React2, { useEffect, useState } from "react";
2234
+ import { Box as Box3, Text as Text3 } from "ink";
2235
+ import React3, { useEffect, useState } from "react";
1831
2236
 
1832
- // src/cli/ui/markdown.tsx
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__ */ React.createElement(Text, { key: `t${idx++}` }, text.slice(last, start)));
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__ */ React.createElement(Text, { key: `b${idx++}`, bold: true }, m[2])
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__ */ React.createElement(Text, { key: `c${idx++}`, color: "yellow" }, m[3])
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__ */ React.createElement(Text, { key: `i${idx++}`, italic: true }, m[4])
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__ */ React.createElement(Text, { key: `t${idx++}` }, text.slice(last)));
2330
+ parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last)));
1910
2331
  }
1911
- return /* @__PURE__ */ React.createElement(Text, null, parts);
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__ */ React.createElement(Text, { bold: true, color: "cyan" }, /* @__PURE__ */ React.createElement(InlineMd, { text: block.text }));
2426
+ return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text }));
2006
2427
  case "paragraph":
2007
- return /* @__PURE__ */ React.createElement(InlineMd, { text: block.text });
2428
+ return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text });
2008
2429
  case "bullet":
2009
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React.createElement(Box, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React.createElement(InlineMd, { text: item }))));
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__ */ React.createElement(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, block.text));
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__ */ React.createElement(Text, { 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");
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 = React.useMemo(() => parseBlocks(cleaned), [cleaned]);
2019
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React.createElement(BlockView, { key: `${i}-${b.kind}`, block: b })));
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 = React2.memo(function EventRow2({ event }) {
2444
+ var EventRow = React3.memo(function EventRow2({ event }) {
2024
2445
  if (event.role === "user") {
2025
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React2.createElement(Text2, null, event.text));
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__ */ React2.createElement(StreamingAssistant, { event });
2029
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React2.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React2.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React2.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React2.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React2.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React2.createElement(Text2, { color: "magenta" }, event.repair) : null);
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__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", truncate2(event.text, 400)));
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__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, event.text));
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__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, event.text));
2459
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, event.text));
2039
2460
  }
2040
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, event.text));
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__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, per)));
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__ */ React2.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
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__ */ React2.createElement(Text2, { dimColor: true }, `${mm}:${ss}`);
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__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", "spread across T=0.0/0.5/1.0 \xB7 typical wait 30-90s for reasoner"));
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__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (", pct2, "%)", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { 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"));
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__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(streaming \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u25B8 ", tail) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
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__ */ React2.createElement(Text2, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
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 Box3, Text as Text3 } from "ink";
2515
+ import { Box as Box4, Text as Text4 } from "ink";
2103
2516
  import TextInput from "ink-text-input";
2104
- import React3 from "react";
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__ */ React3.createElement(Box3, { borderStyle: "round", borderColor: disabled ? "gray" : "cyan", paddingX: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: disabled ? "gray" : "cyan" }, "you \u203A", " "), /* @__PURE__ */ React3.createElement(
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 Box4, Text as Text4 } from "ink";
2127
- import React4 from "react";
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__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Box4, { justifyContent: "space-between" }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, model), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React4.createElement(Text4, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React4.createElement(Text4, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cache hit "), /* @__PURE__ */ React4.createElement(Text4, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cost "), /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React4.createElement(Text4, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "saving "), /* @__PURE__ */ React4.createElement(Text4, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
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({ system });
2303
- const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2, branch, session });
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__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(
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__ */ React5.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React5.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React5.createElement(Box5, { marginY: 1 }, /* @__PURE__ */ React5.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React5.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React5.createElement(CommandStrip, null));
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__ */ React5.createElement(Box5, { paddingX: 2 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /sessions \xB7 /model \xB7 /harvest \xB7 /branch \xB7 /clear \xB7 /exit"));
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 Box6, Text as Text6, useApp as useApp2 } from "ink";
2908
+ import { Box as Box7, Text as Text7, useApp as useApp2 } from "ink";
2493
2909
  import TextInput2 from "ink-text-input";
2494
- import React6, { useState as useState3 } from "react";
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__ */ React6.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React6.createElement(
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__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { color: "red" }, error)) : value ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "(Type /exit to abort.)")));
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__ */ React7.createElement(
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__ */ React7.createElement(
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
- const { waitUntilExit } = render(/* @__PURE__ */ React7.createElement(Root, { initialKey, ...opts }), {
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
- await waitUntilExit();
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
- function diffCommand(opts) {
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
- console.log(renderSummaryTable(report));
2577
- if (opts.mdPath) {
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
- function replayCommand(opts) {
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({ system: opts.system });
2693
- const loop = new CacheFirstLoop({ client, prefix, model: opts.model });
2694
- for await (const ev of loop.step(opts.task)) {
2695
- if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
2696
- if (ev.role === "tool") process.stdout.write(`
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
- if (ev.role === "error") process.stderr.write(`
3429
+ if (ev.role === "error") process.stderr.write(`
2700
3430
  [error] ${ev.error}
2701
3431
  `);
2702
- if (ev.role === "done") process.stdout.write("\n");
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)").action(async (opts) => {
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).action(async (task, opts) => {
2776
- await runCommand({ task, model: opts.model, system: opts.system });
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
- "Pretty-print a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
2783
- ).option("--head <n>", "Show only the first N records", (v) => Number.parseInt(v, 10)).option("--tail <n>", "Show only the last N records", (v) => Number.parseInt(v, 10)).action((transcript, opts) => {
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("Compare two transcripts: aggregate deltas + first divergence.").option("--md <path>", "Also write a markdown report (blog-ready) to this path").option("--label-a <label>", "Display label for transcript A (default: filename)").option("--label-b <label>", "Display label for transcript B (default: filename)").action((a, b, opts) => {
2791
- diffCommand({ a, b, mdPath: opts.md, labelA: opts.labelA, labelB: opts.labelB });
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) => {