reasonix 0.2.2 → 0.3.0-alpha.2

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/)) {
@@ -1448,12 +1459,20 @@ function computeReplayStats(records) {
1448
1459
  const prefixHashes = /* @__PURE__ */ new Set();
1449
1460
  let userTurns = 0;
1450
1461
  let toolCalls = 0;
1462
+ let harvestedTurns = 0;
1463
+ let totalUncertainties = 0;
1464
+ let totalSubgoals = 0;
1451
1465
  for (const rec of records) {
1452
1466
  if (rec.role === "user") userTurns++;
1453
1467
  else if (rec.role === "tool") toolCalls++;
1454
1468
  else if (rec.role === "assistant_final") {
1455
1469
  if (rec.model) models.add(rec.model);
1456
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
+ }
1457
1476
  if (rec.usage && rec.model) {
1458
1477
  const u = new Usage(
1459
1478
  rec.usage.prompt_tokens ?? 0,
@@ -1481,6 +1500,9 @@ function computeReplayStats(records) {
1481
1500
  prefixHashes: [...prefixHashes],
1482
1501
  userTurns,
1483
1502
  toolCalls,
1503
+ harvestedTurns,
1504
+ totalUncertainties,
1505
+ totalSubgoals,
1484
1506
  ...summarizeTurns(turns)
1485
1507
  };
1486
1508
  }
@@ -1664,6 +1686,41 @@ function renderSummaryTable(report, _opts = {}) {
1664
1686
  )
1665
1687
  );
1666
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
+ }
1667
1724
  lines.push("");
1668
1725
  const aPrefixStable = a.stats.prefixHashes.length <= 1;
1669
1726
  const bPrefixStable = b.stats.prefixHashes.length <= 1;
@@ -1735,6 +1792,17 @@ function renderMarkdown(report) {
1735
1792
  out.push(
1736
1793
  `| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | \u2014 |`
1737
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
+ }
1738
1806
  out.push("");
1739
1807
  out.push("## Turn-by-turn");
1740
1808
  out.push("");
@@ -1802,6 +1870,278 @@ function truncate(s, n) {
1802
1870
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
1803
1871
  }
1804
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
+ const shell = opts.shell ?? process.platform === "win32";
2008
+ if (shell) {
2009
+ const line = [
2010
+ opts.command,
2011
+ ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
2012
+ ].join(" ");
2013
+ this.child = spawn(line, [], {
2014
+ env,
2015
+ cwd: opts.cwd,
2016
+ stdio: ["pipe", "pipe", "inherit"],
2017
+ shell: true
2018
+ });
2019
+ } else {
2020
+ this.child = spawn(opts.command, opts.args ?? [], {
2021
+ env,
2022
+ cwd: opts.cwd,
2023
+ stdio: ["pipe", "pipe", "inherit"]
2024
+ });
2025
+ }
2026
+ this.child.stdout.setEncoding("utf8");
2027
+ this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
2028
+ this.child.on("close", () => this.onClose());
2029
+ this.child.on("error", (err) => {
2030
+ this.push({
2031
+ jsonrpc: "2.0",
2032
+ id: null,
2033
+ error: { code: -32e3, message: `transport error: ${err.message}` }
2034
+ });
2035
+ });
2036
+ }
2037
+ async send(message) {
2038
+ if (this.closed) throw new Error("MCP transport is closed");
2039
+ return new Promise((resolve2, reject) => {
2040
+ const line = `${JSON.stringify(message)}
2041
+ `;
2042
+ this.child.stdin.write(line, "utf8", (err) => {
2043
+ if (err) reject(err);
2044
+ else resolve2();
2045
+ });
2046
+ });
2047
+ }
2048
+ async *messages() {
2049
+ while (true) {
2050
+ if (this.queue.length > 0) {
2051
+ yield this.queue.shift();
2052
+ continue;
2053
+ }
2054
+ if (this.closed) return;
2055
+ const next = await new Promise((resolve2) => {
2056
+ this.waiters.push(resolve2);
2057
+ });
2058
+ if (next === null) return;
2059
+ yield next;
2060
+ }
2061
+ }
2062
+ async close() {
2063
+ if (this.closed) return;
2064
+ this.closed = true;
2065
+ while (this.waiters.length > 0) this.waiters.shift()(null);
2066
+ try {
2067
+ this.child.stdin.end();
2068
+ } catch {
2069
+ }
2070
+ if (this.child.exitCode === null && !this.child.killed) {
2071
+ this.child.kill("SIGTERM");
2072
+ }
2073
+ }
2074
+ /** Parse incoming stdout chunks into NDJSON messages. */
2075
+ onStdout(chunk) {
2076
+ this.stdoutBuffer += chunk;
2077
+ let newlineIdx;
2078
+ while ((newlineIdx = this.stdoutBuffer.indexOf("\n")) !== -1) {
2079
+ const line = this.stdoutBuffer.slice(0, newlineIdx).trim();
2080
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIdx + 1);
2081
+ if (!line) continue;
2082
+ try {
2083
+ const msg = JSON.parse(line);
2084
+ this.push(msg);
2085
+ } catch {
2086
+ }
2087
+ }
2088
+ }
2089
+ onClose() {
2090
+ this.closed = true;
2091
+ while (this.waiters.length > 0) this.waiters.shift()(null);
2092
+ }
2093
+ push(msg) {
2094
+ const waiter = this.waiters.shift();
2095
+ if (waiter) waiter(msg);
2096
+ else this.queue.push(msg);
2097
+ }
2098
+ };
2099
+ function quoteArg(s, windows) {
2100
+ if (!windows) {
2101
+ return `'${s.replace(/'/g, "'\\''")}'`;
2102
+ }
2103
+ return `"${s.replace(/"/g, '""')}"`;
2104
+ }
2105
+
2106
+ // src/mcp/registry.ts
2107
+ async function bridgeMcpTools(client, opts = {}) {
2108
+ const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
2109
+ const prefix = opts.namePrefix ?? "";
2110
+ const result = { registry, registeredNames: [], skipped: [] };
2111
+ const listed = await client.listTools();
2112
+ for (const mcpTool of listed.tools) {
2113
+ if (!mcpTool.name) {
2114
+ result.skipped.push({ name: "?", reason: "empty tool name" });
2115
+ continue;
2116
+ }
2117
+ const registeredName = `${prefix}${mcpTool.name}`;
2118
+ registry.register({
2119
+ name: registeredName,
2120
+ description: mcpTool.description ?? "",
2121
+ parameters: mcpTool.inputSchema,
2122
+ fn: async (args) => {
2123
+ const toolResult = await client.callTool(mcpTool.name, args);
2124
+ return flattenMcpResult(toolResult);
2125
+ }
2126
+ });
2127
+ result.registeredNames.push(registeredName);
2128
+ }
2129
+ return result;
2130
+ }
2131
+ function flattenMcpResult(result) {
2132
+ const parts = result.content.map(blockToString);
2133
+ const joined = parts.join("\n").trim();
2134
+ if (result.isError) {
2135
+ return `ERROR: ${joined || "(no error message from server)"}`;
2136
+ }
2137
+ return joined;
2138
+ }
2139
+ function blockToString(block) {
2140
+ if (block.type === "text") return block.text;
2141
+ if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
2142
+ return `[unknown block: ${JSON.stringify(block)}]`;
2143
+ }
2144
+
1805
2145
  // src/config.ts
1806
2146
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
1807
2147
  import { homedir as homedir2 } from "os";
@@ -1846,23 +2186,93 @@ function redactKey(key) {
1846
2186
  }
1847
2187
 
1848
2188
  // src/index.ts
1849
- var VERSION = "0.2.2";
2189
+ var VERSION = "0.3.0-alpha.2";
1850
2190
 
1851
2191
  // src/cli/commands/chat.tsx
1852
2192
  import { render } from "ink";
1853
- import React7, { useState as useState4 } from "react";
2193
+ import React8, { useState as useState4 } from "react";
2194
+
2195
+ // src/mcp/shell-split.ts
2196
+ function shellSplit(input) {
2197
+ const tokens = [];
2198
+ let cur = "";
2199
+ let quote = null;
2200
+ let i = 0;
2201
+ const s = input;
2202
+ while (i < s.length) {
2203
+ const ch = s[i];
2204
+ if (quote) {
2205
+ if (ch === quote) {
2206
+ quote = null;
2207
+ i++;
2208
+ continue;
2209
+ }
2210
+ if (ch === "\\" && quote === '"' && i + 1 < s.length) {
2211
+ cur += s[i + 1];
2212
+ i += 2;
2213
+ continue;
2214
+ }
2215
+ cur += ch;
2216
+ i++;
2217
+ continue;
2218
+ }
2219
+ if (ch === '"' || ch === "'") {
2220
+ quote = ch;
2221
+ i++;
2222
+ continue;
2223
+ }
2224
+ if (ch === "\\" && i + 1 < s.length) {
2225
+ cur += s[i + 1];
2226
+ i += 2;
2227
+ continue;
2228
+ }
2229
+ if (ch === " " || ch === " ") {
2230
+ if (cur.length > 0) {
2231
+ tokens.push(cur);
2232
+ cur = "";
2233
+ }
2234
+ i++;
2235
+ continue;
2236
+ }
2237
+ cur += ch;
2238
+ i++;
2239
+ }
2240
+ if (quote) {
2241
+ throw new Error(
2242
+ `shellSplit: unterminated ${quote === '"' ? "double" : "single"} quote in input`
2243
+ );
2244
+ }
2245
+ if (cur.length > 0) tokens.push(cur);
2246
+ return tokens;
2247
+ }
1854
2248
 
1855
2249
  // src/cli/ui/App.tsx
1856
- import { Box as Box5, Static, Text as Text5, useApp } from "ink";
1857
- import React5, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
2250
+ import { Box as Box6, Static, Text as Text6, useApp } from "ink";
2251
+ import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
1858
2252
 
1859
2253
  // src/cli/ui/EventLog.tsx
1860
- import { Box as Box2, Text as Text2 } from "ink";
1861
- import React2, { useEffect, useState } from "react";
2254
+ import { Box as Box3, Text as Text3 } from "ink";
2255
+ import React3, { useEffect, useState } from "react";
1862
2256
 
1863
- // src/cli/ui/markdown.tsx
2257
+ // src/cli/ui/PlanStateBlock.tsx
1864
2258
  import { Box, Text } from "ink";
1865
2259
  import React from "react";
2260
+ function PlanStateBlock({ planState }) {
2261
+ const fields = [];
2262
+ if (planState.subgoals.length) fields.push(["subgoals", planState.subgoals, "cyan", false]);
2263
+ if (planState.hypotheses.length)
2264
+ fields.push(["hypotheses", planState.hypotheses, "green", false]);
2265
+ if (planState.uncertainties.length)
2266
+ fields.push(["uncertainties", planState.uncertainties, "yellow", false]);
2267
+ if (planState.rejectedPaths.length)
2268
+ fields.push(["rejected", planState.rejectedPaths, "red", true]);
2269
+ if (fields.length === 0) return null;
2270
+ 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 ")}`))));
2271
+ }
2272
+
2273
+ // src/cli/ui/markdown.tsx
2274
+ import { Box as Box2, Text as Text2 } from "ink";
2275
+ import React2 from "react";
1866
2276
  var SUPERSCRIPT = {
1867
2277
  "0": "\u2070",
1868
2278
  "1": "\xB9",
@@ -1919,27 +2329,27 @@ function InlineMd({ text }) {
1919
2329
  for (const m of text.matchAll(INLINE_RE)) {
1920
2330
  const start = m.index ?? 0;
1921
2331
  if (start > last) {
1922
- parts.push(/* @__PURE__ */ React.createElement(Text, { key: `t${idx++}` }, text.slice(last, start)));
2332
+ parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last, start)));
1923
2333
  }
1924
2334
  if (m[2] !== void 0) {
1925
2335
  parts.push(
1926
- /* @__PURE__ */ React.createElement(Text, { key: `b${idx++}`, bold: true }, m[2])
2336
+ /* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[2])
1927
2337
  );
1928
2338
  } else if (m[3] !== void 0) {
1929
2339
  parts.push(
1930
- /* @__PURE__ */ React.createElement(Text, { key: `c${idx++}`, color: "yellow" }, m[3])
2340
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[3])
1931
2341
  );
1932
2342
  } else if (m[4] !== void 0) {
1933
2343
  parts.push(
1934
- /* @__PURE__ */ React.createElement(Text, { key: `i${idx++}`, italic: true }, m[4])
2344
+ /* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[4])
1935
2345
  );
1936
2346
  }
1937
2347
  last = start + m[0].length;
1938
2348
  }
1939
2349
  if (last < text.length) {
1940
- parts.push(/* @__PURE__ */ React.createElement(Text, { key: `t${idx++}` }, text.slice(last)));
2350
+ parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last)));
1941
2351
  }
1942
- return /* @__PURE__ */ React.createElement(Text, null, parts);
2352
+ return /* @__PURE__ */ React2.createElement(Text2, null, parts);
1943
2353
  }
1944
2354
  function parseBlocks(raw) {
1945
2355
  const lines = raw.split(/\r?\n/);
@@ -2033,42 +2443,42 @@ function parseBlocks(raw) {
2033
2443
  function BlockView({ block }) {
2034
2444
  switch (block.kind) {
2035
2445
  case "heading":
2036
- return /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, /* @__PURE__ */ React.createElement(InlineMd, { text: block.text }));
2446
+ return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text }));
2037
2447
  case "paragraph":
2038
- return /* @__PURE__ */ React.createElement(InlineMd, { text: block.text });
2448
+ return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text });
2039
2449
  case "bullet":
2040
- 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 }))));
2450
+ 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 }))));
2041
2451
  case "code":
2042
- return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, block.text));
2452
+ return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
2043
2453
  case "hr":
2044
- 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");
2454
+ 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");
2045
2455
  }
2046
2456
  }
2047
2457
  function Markdown({ text }) {
2048
2458
  const cleaned = stripMath(text);
2049
- const blocks = React.useMemo(() => parseBlocks(cleaned), [cleaned]);
2050
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React.createElement(BlockView, { key: `${i}-${b.kind}`, block: b })));
2459
+ const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
2460
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React2.createElement(BlockView, { key: `${i}-${b.kind}`, block: b })));
2051
2461
  }
2052
2462
 
2053
2463
  // src/cli/ui/EventLog.tsx
2054
- var EventRow = React2.memo(function EventRow2({ event }) {
2464
+ var EventRow = React3.memo(function EventRow2({ event }) {
2055
2465
  if (event.role === "user") {
2056
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React2.createElement(Text2, null, event.text));
2466
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React3.createElement(Text3, null, event.text));
2057
2467
  }
2058
2468
  if (event.role === "assistant") {
2059
- if (event.streaming) return /* @__PURE__ */ React2.createElement(StreamingAssistant, { event });
2060
- 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);
2469
+ if (event.streaming) return /* @__PURE__ */ React3.createElement(StreamingAssistant, { event });
2470
+ 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);
2061
2471
  }
2062
2472
  if (event.role === "tool") {
2063
- 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)));
2473
+ 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)));
2064
2474
  }
2065
2475
  if (event.role === "error") {
2066
- return /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, event.text));
2476
+ return /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, event.text));
2067
2477
  }
2068
2478
  if (event.role === "info") {
2069
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, event.text));
2479
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, event.text));
2070
2480
  }
2071
- return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, event.text));
2481
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
2072
2482
  });
2073
2483
  function BranchBlock({ branch }) {
2074
2484
  const per = branch.uncertainties.map((u, i) => {
@@ -2076,21 +2486,13 @@ function BranchBlock({ branch }) {
2076
2486
  const t = (branch.temperatures[i] ?? 0).toFixed(1);
2077
2487
  return `${marker} #${i} T=${t} u=${u}`;
2078
2488
  }).join(" ");
2079
- 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)));
2080
- }
2081
- function PlanStateBlock({ planState }) {
2082
- const lines = [];
2083
- if (planState.subgoals.length) lines.push(["subgoals", planState.subgoals]);
2084
- if (planState.hypotheses.length) lines.push(["hypotheses", planState.hypotheses]);
2085
- if (planState.uncertainties.length) lines.push(["uncertainties", planState.uncertainties]);
2086
- if (planState.rejectedPaths.length) lines.push(["rejected", planState.rejectedPaths]);
2087
- 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 ")}`)));
2489
+ 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)));
2088
2490
  }
2089
2491
  function ReasoningBlock({ reasoning }) {
2090
2492
  const max = 220;
2091
2493
  const flat = reasoning.replace(/\s+/g, " ").trim();
2092
2494
  const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
2093
- return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
2495
+ return /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
2094
2496
  }
2095
2497
  function Elapsed() {
2096
2498
  const [s, setS] = useState(0);
@@ -2101,20 +2503,20 @@ function Elapsed() {
2101
2503
  }, []);
2102
2504
  const mm = String(Math.floor(s / 60)).padStart(2, "0");
2103
2505
  const ss = String(s % 60).padStart(2, "0");
2104
- return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, `${mm}:${ss}`);
2506
+ return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, `${mm}:${ss}`);
2105
2507
  }
2106
2508
  function StreamingAssistant({ event }) {
2107
2509
  if (event.branchProgress) {
2108
2510
  const p = event.branchProgress;
2109
2511
  if (p.completed === 0) {
2110
- 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"));
2512
+ 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"));
2111
2513
  }
2112
2514
  const pct2 = Math.round(p.completed / p.total * 100);
2113
- 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"));
2515
+ 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"));
2114
2516
  }
2115
2517
  const tail = lastLine(event.text, 140);
2116
2518
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
2117
- 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)"));
2519
+ 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)"));
2118
2520
  }
2119
2521
  function lastLine(s, maxChars) {
2120
2522
  const flat = s.replace(/\s+/g, " ").trim();
@@ -2123,16 +2525,16 @@ function lastLine(s, maxChars) {
2123
2525
  }
2124
2526
  function StatsLine({ stats }) {
2125
2527
  const hit = (stats.cacheHitRatio * 100).toFixed(1);
2126
- return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
2528
+ return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
2127
2529
  }
2128
2530
  function truncate2(s, max) {
2129
2531
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
2130
2532
  }
2131
2533
 
2132
2534
  // src/cli/ui/PromptInput.tsx
2133
- import { Box as Box3, Text as Text3 } from "ink";
2535
+ import { Box as Box4, Text as Text4 } from "ink";
2134
2536
  import TextInput from "ink-text-input";
2135
- import React3 from "react";
2537
+ import React4 from "react";
2136
2538
  function PromptInput({
2137
2539
  value,
2138
2540
  onChange,
@@ -2141,7 +2543,7 @@ function PromptInput({
2141
2543
  placeholder
2142
2544
  }) {
2143
2545
  const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
2144
- 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(
2546
+ 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(
2145
2547
  TextInput,
2146
2548
  {
2147
2549
  value,
@@ -2154,8 +2556,8 @@ function PromptInput({
2154
2556
  }
2155
2557
 
2156
2558
  // src/cli/ui/StatsPanel.tsx
2157
- import { Box as Box4, Text as Text4 } from "ink";
2158
- import React4 from "react";
2559
+ import { Box as Box5, Text as Text5 } from "ink";
2560
+ import React5 from "react";
2159
2561
  function StatsPanel({
2160
2562
  summary,
2161
2563
  model,
@@ -2166,7 +2568,7 @@ function StatsPanel({
2166
2568
  const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
2167
2569
  const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
2168
2570
  const branchOn = (branchBudget ?? 1) > 1;
2169
- 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), "%"))));
2571
+ 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), "%"))));
2170
2572
  }
2171
2573
 
2172
2574
  // src/cli/ui/slash.ts
@@ -2299,7 +2701,7 @@ function handleSlash(cmd, args, loop) {
2299
2701
 
2300
2702
  // src/cli/ui/App.tsx
2301
2703
  var FLUSH_INTERVAL_MS = 60;
2302
- function App({ model, system, transcript, harvest: harvest2, branch, session }) {
2704
+ function App({ model, system, transcript, harvest: harvest2, branch, session, tools }) {
2303
2705
  const { exit } = useApp();
2304
2706
  const [historical, setHistorical] = useState2([]);
2305
2707
  const [streaming, setStreaming] = useState2(null);
@@ -2330,11 +2732,14 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
2330
2732
  const loop = useMemo(() => {
2331
2733
  if (loopRef.current) return loopRef.current;
2332
2734
  const client = new DeepSeekClient();
2333
- const prefix = new ImmutablePrefix({ system });
2334
- const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2, branch, session });
2735
+ const prefix = new ImmutablePrefix({
2736
+ system,
2737
+ toolSpecs: tools?.specs()
2738
+ });
2739
+ const l = new CacheFirstLoop({ client, prefix, tools, model, harvest: harvest2, branch, session });
2335
2740
  loopRef.current = l;
2336
2741
  return l;
2337
- }, [model, system, harvest2, branch, session]);
2742
+ }, [model, system, harvest2, branch, session, tools]);
2338
2743
  const sessionBannerShown = useRef(false);
2339
2744
  useEffect2(() => {
2340
2745
  if (sessionBannerShown.current) return;
@@ -2497,7 +2902,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
2497
2902
  },
2498
2903
  [busy, exit, loop, writeTranscript]
2499
2904
  );
2500
- return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(
2905
+ return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
2501
2906
  StatsPanel,
2502
2907
  {
2503
2908
  summary,
@@ -2506,10 +2911,10 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
2506
2911
  harvestOn: loop.harvestEnabled,
2507
2912
  branchBudget: loop.branchOptions.budget
2508
2913
  }
2509
- ), /* @__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));
2914
+ ), /* @__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));
2510
2915
  }
2511
2916
  function CommandStrip() {
2512
- 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"));
2917
+ 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"));
2513
2918
  }
2514
2919
  function describeRepair(repair) {
2515
2920
  const parts = [];
@@ -2520,9 +2925,9 @@ function describeRepair(repair) {
2520
2925
  }
2521
2926
 
2522
2927
  // src/cli/ui/Setup.tsx
2523
- import { Box as Box6, Text as Text6, useApp as useApp2 } from "ink";
2928
+ import { Box as Box7, Text as Text7, useApp as useApp2 } from "ink";
2524
2929
  import TextInput2 from "ink-text-input";
2525
- import React6, { useState as useState3 } from "react";
2930
+ import React7, { useState as useState3 } from "react";
2526
2931
  function Setup({ onReady }) {
2527
2932
  const [value, setValue] = useState3("");
2528
2933
  const [error, setError] = useState3(null);
@@ -2546,7 +2951,7 @@ function Setup({ onReady }) {
2546
2951
  }
2547
2952
  onReady(trimmed);
2548
2953
  };
2549
- 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(
2954
+ 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(
2550
2955
  TextInput2,
2551
2956
  {
2552
2957
  value,
@@ -2555,14 +2960,14 @@ function Setup({ onReady }) {
2555
2960
  mask: "\u2022",
2556
2961
  placeholder: "sk-..."
2557
2962
  }
2558
- )), 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.)")));
2963
+ )), 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.)")));
2559
2964
  }
2560
2965
 
2561
2966
  // src/cli/commands/chat.tsx
2562
- function Root({ initialKey, ...appProps }) {
2967
+ function Root({ initialKey, tools, ...appProps }) {
2563
2968
  const [key, setKey] = useState4(initialKey);
2564
2969
  if (!key) {
2565
- return /* @__PURE__ */ React7.createElement(
2970
+ return /* @__PURE__ */ React8.createElement(
2566
2971
  Setup,
2567
2972
  {
2568
2973
  onReady: (k) => {
@@ -2573,7 +2978,7 @@ function Root({ initialKey, ...appProps }) {
2573
2978
  );
2574
2979
  }
2575
2980
  process.env.DEEPSEEK_API_KEY = key;
2576
- return /* @__PURE__ */ React7.createElement(
2981
+ return /* @__PURE__ */ React8.createElement(
2577
2982
  App,
2578
2983
  {
2579
2984
  model: appProps.model,
@@ -2581,51 +2986,86 @@ function Root({ initialKey, ...appProps }) {
2581
2986
  transcript: appProps.transcript,
2582
2987
  harvest: appProps.harvest,
2583
2988
  branch: appProps.branch,
2584
- session: appProps.session
2989
+ session: appProps.session,
2990
+ tools
2585
2991
  }
2586
2992
  );
2587
2993
  }
2588
2994
  async function chatCommand(opts) {
2589
2995
  loadDotenv();
2590
2996
  const initialKey = loadApiKey();
2591
- const { waitUntilExit } = render(/* @__PURE__ */ React7.createElement(Root, { initialKey, ...opts }), {
2997
+ let mcp;
2998
+ let tools;
2999
+ if (opts.mcp) {
3000
+ const argv = shellSplit(opts.mcp);
3001
+ if (argv.length === 0) {
3002
+ process.stderr.write("error: --mcp requires a command\n");
3003
+ process.exit(2);
3004
+ }
3005
+ const [command, ...args] = argv;
3006
+ if (!command) {
3007
+ process.stderr.write("error: --mcp command is empty\n");
3008
+ process.exit(2);
3009
+ }
3010
+ const transport = new StdioTransport({ command, args });
3011
+ mcp = new McpClient({ transport });
3012
+ try {
3013
+ await mcp.initialize();
3014
+ const bridge = await bridgeMcpTools(mcp, { namePrefix: opts.mcpPrefix });
3015
+ tools = bridge.registry;
3016
+ process.stderr.write(
3017
+ `\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
3018
+ `
3019
+ );
3020
+ } catch (err) {
3021
+ process.stderr.write(`MCP setup failed: ${err.message}
3022
+ `);
3023
+ await mcp.close();
3024
+ process.exit(1);
3025
+ }
3026
+ }
3027
+ const { waitUntilExit } = render(/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, ...opts }), {
2592
3028
  exitOnCtrlC: true
2593
3029
  });
2594
- await waitUntilExit();
3030
+ try {
3031
+ await waitUntilExit();
3032
+ } finally {
3033
+ await mcp?.close();
3034
+ }
2595
3035
  }
2596
3036
 
2597
3037
  // src/cli/commands/diff.ts
2598
3038
  import { writeFileSync as writeFileSync2 } from "fs";
2599
3039
  import { basename } from "path";
2600
3040
  import { render as render2 } from "ink";
2601
- import React10 from "react";
3041
+ import React11 from "react";
2602
3042
 
2603
3043
  // src/cli/ui/DiffApp.tsx
2604
- import { Box as Box8, Static as Static2, Text as Text8, useApp as useApp3, useInput } from "ink";
2605
- import React9, { useState as useState5 } from "react";
3044
+ import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput } from "ink";
3045
+ import React10, { useState as useState5 } from "react";
2606
3046
 
2607
3047
  // src/cli/ui/RecordView.tsx
2608
- import { Box as Box7, Text as Text7 } from "ink";
2609
- import React8 from "react";
3048
+ import { Box as Box8, Text as Text8 } from "ink";
3049
+ import React9 from "react";
2610
3050
  function RecordView({ rec, compact = false }) {
2611
3051
  const toolArgsMax = compact ? 120 : 200;
2612
3052
  const toolContentMax = compact ? 200 : 400;
2613
3053
  if (rec.role === "user") {
2614
- return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React8.createElement(Text7, null, rec.content));
3054
+ return /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React9.createElement(Text8, null, rec.content));
2615
3055
  }
2616
3056
  if (rec.role === "assistant_final") {
2617
- return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React8.createElement(CacheBadge, { usage: rec.usage }) : null), rec.content ? /* @__PURE__ */ React8.createElement(Text7, null, rec.content) : /* @__PURE__ */ React8.createElement(Text7, { dimColor: true, italic: true }, "(tool-call response only)"));
3057
+ 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)"));
2618
3058
  }
2619
3059
  if (rec.role === "tool") {
2620
- return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
3060
+ 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)));
2621
3061
  }
2622
3062
  if (rec.role === "error") {
2623
- return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React8.createElement(Text7, { color: "red" }, rec.error ?? rec.content));
3063
+ 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));
2624
3064
  }
2625
3065
  if (rec.role === "done" || rec.role === "assistant_delta") {
2626
3066
  return null;
2627
3067
  }
2628
- return /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, "[", rec.role, "] ", rec.content));
3068
+ return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "[", rec.role, "] ", rec.content));
2629
3069
  }
2630
3070
  function CacheBadge({ usage }) {
2631
3071
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -2634,7 +3074,7 @@ function CacheBadge({ usage }) {
2634
3074
  if (total === 0) return null;
2635
3075
  const pct2 = hit / total * 100;
2636
3076
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
2637
- return /* @__PURE__ */ React8.createElement(Text7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React8.createElement(Text7, { color }, pct2.toFixed(1), "%"));
3077
+ return /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React9.createElement(Text8, { color }, pct2.toFixed(1), "%"));
2638
3078
  }
2639
3079
  function truncate3(s, max) {
2640
3080
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -2668,7 +3108,7 @@ function DiffApp({ report }) {
2668
3108
  }
2669
3109
  });
2670
3110
  const pair = report.pairs[idx];
2671
- return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(DiffHeader, { report }), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React9.createElement(Text8, null, pair ? /* @__PURE__ */ React9.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React9.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React9.createElement(Text8, null, pair.divergenceNote)) : null, /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "j"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "k"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "N"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "g"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "q"), " ", "quit")));
3111
+ 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")));
2672
3112
  }
2673
3113
  function DiffHeader({ report }) {
2674
3114
  const a = report.a;
@@ -2686,15 +3126,15 @@ function DiffHeader({ report }) {
2686
3126
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
2687
3127
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
2688
3128
  }
2689
- return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Box8, { justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, a.label), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " vs B="), /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, b.label)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cache "), /* @__PURE__ */ React9.createElement(Text8, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cost "), /* @__PURE__ */ React9.createElement(Text8, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "model calls "), /* @__PURE__ */ React9.createElement(Text8, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, prefixLine)) : null);
3129
+ 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);
2690
3130
  }
2691
3131
  function Pane({
2692
3132
  label,
2693
3133
  headerColor,
2694
3134
  records
2695
3135
  }) {
2696
- return /* @__PURE__ */ React9.createElement(
2697
- Box8,
3136
+ return /* @__PURE__ */ React10.createElement(
3137
+ Box9,
2698
3138
  {
2699
3139
  flexDirection: "column",
2700
3140
  flexGrow: 1,
@@ -2702,21 +3142,21 @@ function Pane({
2702
3142
  borderStyle: "single",
2703
3143
  borderColor: headerColor
2704
3144
  },
2705
- /* @__PURE__ */ React9.createElement(Text8, { color: headerColor, bold: true }, label),
2706
- records.length === 0 ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React9.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React9.createElement(RecordView, { key, rec, compact: true }))
3145
+ /* @__PURE__ */ React10.createElement(Text9, { color: headerColor, bold: true }, label),
3146
+ 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 }))
2707
3147
  );
2708
3148
  }
2709
3149
  function KindBadge({ kind }) {
2710
3150
  if (kind === "match") {
2711
- return /* @__PURE__ */ React9.createElement(Text8, { color: "green" }, "\u2713 match");
3151
+ return /* @__PURE__ */ React10.createElement(Text9, { color: "green" }, "\u2713 match");
2712
3152
  }
2713
3153
  if (kind === "diverge") {
2714
- return /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 diverge");
3154
+ return /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u2605 diverge");
2715
3155
  }
2716
3156
  if (kind === "only_in_a") {
2717
- return /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, "\u2190 only in A");
3157
+ return /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, "\u2190 only in A");
2718
3158
  }
2719
- return /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, "\u2192 only in B");
3159
+ return /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, "\u2192 only in B");
2720
3160
  }
2721
3161
  function paneRecords(pair, side) {
2722
3162
  if (!pair) return [];
@@ -2747,7 +3187,7 @@ markdown report written to ${opts.mdPath}`);
2747
3187
  return;
2748
3188
  }
2749
3189
  if (wantTui) {
2750
- const { waitUntilExit } = render2(React10.createElement(DiffApp, { report }), {
3190
+ const { waitUntilExit } = render2(React11.createElement(DiffApp, { report }), {
2751
3191
  exitOnCtrlC: true
2752
3192
  });
2753
3193
  await waitUntilExit();
@@ -2758,11 +3198,11 @@ markdown report written to ${opts.mdPath}`);
2758
3198
 
2759
3199
  // src/cli/commands/replay.ts
2760
3200
  import { render as render3 } from "ink";
2761
- import React12 from "react";
3201
+ import React13 from "react";
2762
3202
 
2763
3203
  // src/cli/ui/ReplayApp.tsx
2764
- import { Box as Box9, Static as Static3, Text as Text9, useApp as useApp4, useInput as useInput2 } from "ink";
2765
- import React11, { useMemo as useMemo2, useState as useState6 } from "react";
3204
+ import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as useInput2 } from "ink";
3205
+ import React12, { useMemo as useMemo2, useState as useState6 } from "react";
2766
3206
  function ReplayApp({ meta, pages }) {
2767
3207
  const { exit } = useApp4();
2768
3208
  const maxIdx = Math.max(0, pages.length - 1);
@@ -2797,14 +3237,14 @@ function ReplayApp({ meta, pages }) {
2797
3237
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
2798
3238
  const currentPage = pages[idx];
2799
3239
  const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
2800
- return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React11.createElement(
3240
+ return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(
2801
3241
  StatsPanel,
2802
3242
  {
2803
3243
  summary,
2804
3244
  model: cumStats.models[0] ?? meta?.model ?? "?",
2805
3245
  prefixHash
2806
3246
  }
2807
- ), /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box9, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text9, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React11.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React11.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React11.createElement(Text9, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React11.createElement(Box9, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "j"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "k"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "q"), " quit")));
3247
+ ), /* @__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")));
2808
3248
  }
2809
3249
 
2810
3250
  // src/cli/commands/replay.ts
@@ -2816,7 +3256,7 @@ async function replayCommand(opts) {
2816
3256
  }
2817
3257
  const { parsed } = replayFromFile(opts.path);
2818
3258
  const pages = groupRecordsByTurn(parsed.records);
2819
- const { waitUntilExit } = render3(React12.createElement(ReplayApp, { meta: parsed.meta, pages }), {
3259
+ const { waitUntilExit } = render3(React13.createElement(ReplayApp, { meta: parsed.meta, pages }), {
2820
3260
  exitOnCtrlC: true
2821
3261
  });
2822
3262
  await waitUntilExit();
@@ -2854,6 +3294,11 @@ function printReplay(opts) {
2854
3294
  } else if (stats.prefixHashes.length > 1) {
2855
3295
  console.log(" (prefix churned \u2014 cache-hostile session)");
2856
3296
  }
3297
+ if (stats.harvestedTurns > 0) {
3298
+ console.log(`harvest (Pillar 2): ${stats.harvestedTurns} turn(s) produced plan state`);
3299
+ console.log(` subgoals: ${stats.totalSubgoals}`);
3300
+ console.log(` uncertainties: ${stats.totalUncertainties}`);
3301
+ }
2857
3302
  }
2858
3303
  function sliceRecords(records, opts) {
2859
3304
  if (opts.head !== void 0 && opts.head > 0) return records.slice(0, opts.head);
@@ -2873,6 +3318,21 @@ function renderRecord(rec) {
2873
3318
  return total > 0 ? ` cache=${(hit / total * 100).toFixed(1)}%` : "";
2874
3319
  })() : "";
2875
3320
  console.log(`${turn} AGENT:${cost}${cache} ${oneLine(rec.content)}`);
3321
+ if (rec.planState) {
3322
+ const ps = rec.planState;
3323
+ if (ps.subgoals.length)
3324
+ console.log(` \u2039 subgoals (${ps.subgoals.length}): ${ps.subgoals.join(" \xB7 ")}`);
3325
+ if (ps.hypotheses.length)
3326
+ console.log(` \u2039 hypotheses (${ps.hypotheses.length}): ${ps.hypotheses.join(" \xB7 ")}`);
3327
+ if (ps.uncertainties.length)
3328
+ console.log(
3329
+ ` \u2039 uncertainties(${ps.uncertainties.length}): ${ps.uncertainties.join(" \xB7 ")}`
3330
+ );
3331
+ if (ps.rejectedPaths.length)
3332
+ console.log(
3333
+ ` \u2039 rejected (${ps.rejectedPaths.length}): ${ps.rejectedPaths.join(" \xB7 ")}`
3334
+ );
3335
+ }
2876
3336
  } else if (rec.role === "tool") {
2877
3337
  const args = rec.args ? ` args=${oneLine(rec.args, 80)}` : "";
2878
3338
  console.log(`${turn} TOOL ${rec.tool ?? "?"}:${args} \u2192 ${oneLine(rec.content, 120)}`);
@@ -2926,18 +3386,76 @@ async function runCommand(opts) {
2926
3386
  loadDotenv();
2927
3387
  const apiKey = await ensureApiKey();
2928
3388
  process.env.DEEPSEEK_API_KEY = apiKey;
3389
+ let mcp;
3390
+ let tools;
3391
+ if (opts.mcp) {
3392
+ const argv = shellSplit(opts.mcp);
3393
+ const [command, ...args] = argv;
3394
+ if (!command) {
3395
+ process.stderr.write("error: --mcp command is empty\n");
3396
+ process.exit(2);
3397
+ }
3398
+ mcp = new McpClient({ transport: new StdioTransport({ command, args }) });
3399
+ try {
3400
+ await mcp.initialize();
3401
+ const bridge = await bridgeMcpTools(mcp, { namePrefix: opts.mcpPrefix });
3402
+ tools = bridge.registry;
3403
+ process.stderr.write(
3404
+ `\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
3405
+ `
3406
+ );
3407
+ } catch (err) {
3408
+ process.stderr.write(`MCP setup failed: ${err.message}
3409
+ `);
3410
+ await mcp.close();
3411
+ process.exit(1);
3412
+ }
3413
+ }
2929
3414
  const client = new DeepSeekClient();
2930
- const prefix = new ImmutablePrefix({ system: opts.system });
2931
- const loop = new CacheFirstLoop({ client, prefix, model: opts.model });
2932
- for await (const ev of loop.step(opts.task)) {
2933
- if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
2934
- if (ev.role === "tool") process.stdout.write(`
3415
+ const prefix = new ImmutablePrefix({
3416
+ system: opts.system,
3417
+ toolSpecs: tools?.specs()
3418
+ });
3419
+ const loop = new CacheFirstLoop({
3420
+ client,
3421
+ prefix,
3422
+ tools,
3423
+ model: opts.model,
3424
+ harvest: opts.harvest,
3425
+ branch: opts.branch
3426
+ });
3427
+ const prefixHash = prefix.fingerprint;
3428
+ let transcriptStream = null;
3429
+ if (opts.transcript) {
3430
+ transcriptStream = openTranscriptFile(opts.transcript, {
3431
+ version: 1,
3432
+ source: "reasonix run",
3433
+ model: opts.model,
3434
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
3435
+ });
3436
+ writeRecord(transcriptStream, {
3437
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3438
+ turn: 1,
3439
+ role: "user",
3440
+ content: opts.task
3441
+ });
3442
+ }
3443
+ try {
3444
+ for await (const ev of loop.step(opts.task)) {
3445
+ if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
3446
+ if (ev.role === "tool") process.stdout.write(`
2935
3447
  [tool ${ev.toolName}] ${ev.content}
2936
3448
  `);
2937
- if (ev.role === "error") process.stderr.write(`
3449
+ if (ev.role === "error") process.stderr.write(`
2938
3450
  [error] ${ev.error}
2939
3451
  `);
2940
- if (ev.role === "done") process.stdout.write("\n");
3452
+ if (ev.role === "done") process.stdout.write("\n");
3453
+ if (transcriptStream && ev.role !== "assistant_delta") {
3454
+ writeRecord(transcriptStream, recordFromLoopEvent(ev, { model: opts.model, prefixHash }));
3455
+ }
3456
+ }
3457
+ } finally {
3458
+ transcriptStream?.end();
2941
3459
  }
2942
3460
  const s = loop.stats.summary();
2943
3461
  process.stdout.write(
@@ -2945,6 +3463,90 @@ async function runCommand(opts) {
2945
3463
  \u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
2946
3464
  `
2947
3465
  );
3466
+ if (opts.transcript) {
3467
+ process.stdout.write(`
3468
+ transcript: ${opts.transcript}
3469
+ `);
3470
+ process.stdout.write(` \u2192 npx reasonix replay ${opts.transcript}
3471
+ `);
3472
+ }
3473
+ await mcp?.close();
3474
+ }
3475
+
3476
+ // src/cli/commands/sessions.ts
3477
+ function sessionsCommand(opts) {
3478
+ if (opts.name) {
3479
+ inspectSession(opts.name, !!opts.verbose);
3480
+ } else {
3481
+ listAll();
3482
+ }
3483
+ }
3484
+ function listAll() {
3485
+ const items = listSessions();
3486
+ if (items.length === 0) {
3487
+ console.log(
3488
+ "no saved sessions yet \u2014 run `reasonix chat` (sessions are auto-saved unless --no-session)."
3489
+ );
3490
+ return;
3491
+ }
3492
+ console.log("Saved sessions (~/.reasonix/sessions/):");
3493
+ console.log("");
3494
+ console.log(` ${"name".padEnd(22)} ${"msgs".padStart(6)} ${"size".padStart(8)} modified`);
3495
+ console.log(` ${"\u2500".repeat(60)}`);
3496
+ for (const s of items) {
3497
+ const sizeKb = `${(s.size / 1024).toFixed(1)} KB`;
3498
+ const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
3499
+ console.log(
3500
+ ` ${s.name.padEnd(22)} ${String(s.messageCount).padStart(6)} ${sizeKb.padStart(8)} ${when}`
3501
+ );
3502
+ }
3503
+ console.log("");
3504
+ console.log("Inspect: reasonix sessions <name>");
3505
+ console.log("Resume: reasonix chat --session <name>");
3506
+ }
3507
+ function inspectSession(name, verbose) {
3508
+ const path = sessionPath(name);
3509
+ const messages = loadSessionMessages(name);
3510
+ if (messages.length === 0) {
3511
+ console.error(`no session named "${name}" (or it's empty).`);
3512
+ console.error(`looked at: ${path}`);
3513
+ process.exit(1);
3514
+ }
3515
+ console.log(`[session] ${name} ${messages.length} messages ${path}`);
3516
+ console.log("");
3517
+ let turnIndex = 0;
3518
+ for (const msg of messages) {
3519
+ renderMessage(msg, turnIndex, verbose);
3520
+ if (msg.role === "user") turnIndex++;
3521
+ }
3522
+ }
3523
+ function renderMessage(msg, turnIdx, verbose) {
3524
+ const turn = turnIdx > 0 ? `[t${turnIdx}]` : "[start]";
3525
+ const content = typeof msg.content === "string" ? msg.content : "";
3526
+ const flat = oneLine2(content);
3527
+ if (msg.role === "user") {
3528
+ console.log(`${turn} USER: ${flat}`);
3529
+ } else if (msg.role === "assistant") {
3530
+ console.log(`${turn} AGENT: ${flat || "(tool call only)"}`);
3531
+ if (verbose && msg.tool_calls?.length) {
3532
+ for (const tc of msg.tool_calls) {
3533
+ console.log(
3534
+ ` \u2192 call ${tc.function?.name} ${truncate4(tc.function?.arguments ?? "", 80)}`
3535
+ );
3536
+ }
3537
+ }
3538
+ } else if (msg.role === "tool") {
3539
+ console.log(`${turn} TOOL ${msg.name ?? "?"}: ${truncate4(flat, 160)}`);
3540
+ } else if (msg.role === "system") {
3541
+ if (verbose) console.log(`${turn} SYSTEM: ${truncate4(flat, 160)}`);
3542
+ }
3543
+ }
3544
+ function oneLine2(s, max = 200) {
3545
+ const collapsed = s.replace(/\s+/g, " ").trim();
3546
+ return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
3547
+ }
3548
+ function truncate4(s, max) {
3549
+ return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
2948
3550
  }
2949
3551
 
2950
3552
  // src/cli/commands/stats.ts
@@ -2992,7 +3594,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
2992
3594
  ).option(
2993
3595
  "--session <name>",
2994
3596
  "Use a named session (default: 'default'). Resume the same session next time."
2995
- ).option("--no-session", "Disable session persistence for this run (ephemeral chat)").action(async (opts) => {
3597
+ ).option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
3598
+ "--mcp <command>",
3599
+ 'Spawn an MCP server and bridge its tools. Shell-quoted: --mcp "npx -y @scope/server-x /path"'
3600
+ ).option("--mcp-prefix <str>", "Prefix prepended to every MCP tool name (avoid collisions)").action(async (opts) => {
2996
3601
  let session;
2997
3602
  if (opts.session === false) {
2998
3603
  session = void 0;
@@ -3007,15 +3612,39 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
3007
3612
  transcript: opts.transcript,
3008
3613
  harvest: !!opts.harvest,
3009
3614
  branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
3010
- session
3615
+ session,
3616
+ mcp: opts.mcp,
3617
+ mcpPrefix: opts.mcpPrefix
3011
3618
  });
3012
3619
  });
3013
- 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) => {
3014
- await runCommand({ task, model: opts.model, system: opts.system });
3620
+ 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(
3621
+ "--harvest",
3622
+ "Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
3623
+ ).option(
3624
+ "--branch <n>",
3625
+ "Self-consistency: run N parallel samples per turn and pick the most confident",
3626
+ (v) => Number.parseInt(v, 10)
3627
+ ).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
3628
+ "--mcp <command>",
3629
+ 'Spawn an MCP server and bridge its tools. Shell-quoted: --mcp "npx -y @scope/server-x /path"'
3630
+ ).option("--mcp-prefix <str>", "Prefix prepended to every MCP tool name (avoid collisions)").action(async (task, opts) => {
3631
+ await runCommand({
3632
+ task,
3633
+ model: opts.model,
3634
+ system: opts.system,
3635
+ harvest: !!opts.harvest,
3636
+ branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
3637
+ transcript: opts.transcript,
3638
+ mcp: opts.mcp,
3639
+ mcpPrefix: opts.mcpPrefix
3640
+ });
3015
3641
  });
3016
3642
  program.command("stats <transcript>").description("Summarize a JSONL transcript produced by `reasonix chat --transcript`.").action((transcript) => {
3017
3643
  statsCommand({ transcript });
3018
3644
  });
3645
+ 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) => {
3646
+ sessionsCommand({ name, verbose: !!opts.verbose });
3647
+ });
3019
3648
  program.command("replay <transcript>").description(
3020
3649
  "Interactive Ink TUI to scrub through a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
3021
3650
  ).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) => {