reasonix 0.4.5 → 0.4.9

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
@@ -96,8 +96,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
96
96
  }
97
97
  function sleep(ms, signal) {
98
98
  if (ms <= 0) return Promise.resolve();
99
- return new Promise((resolve4, reject) => {
100
- const timer = setTimeout(resolve4, ms);
99
+ return new Promise((resolve5, reject) => {
100
+ const timer = setTimeout(resolve5, ms);
101
101
  if (signal) {
102
102
  const onAbort = () => {
103
103
  clearTimeout(timer);
@@ -563,7 +563,7 @@ var ToolRegistry = class {
563
563
  }
564
564
  }));
565
565
  }
566
- async dispatch(name, argumentsRaw) {
566
+ async dispatch(name, argumentsRaw, opts = {}) {
567
567
  const tool = this._tools.get(name);
568
568
  if (!tool) {
569
569
  return JSON.stringify({ error: `unknown tool: ${name}` });
@@ -580,7 +580,7 @@ var ToolRegistry = class {
580
580
  args = nestArguments(args);
581
581
  }
582
582
  try {
583
- const result = await tool.fn(args);
583
+ const result = await tool.fn(args, { signal: opts.signal });
584
584
  return typeof result === "string" ? result : JSON.stringify(result);
585
585
  } catch (err) {
586
586
  return JSON.stringify({
@@ -614,8 +614,20 @@ async function bridgeMcpTools(client, opts = {}) {
614
614
  name: registeredName,
615
615
  description: mcpTool.description ?? "",
616
616
  parameters: mcpTool.inputSchema,
617
- fn: async (args) => {
618
- const toolResult = await client.callTool(mcpTool.name, args);
617
+ fn: async (args, ctx) => {
618
+ const toolResult = await client.callTool(mcpTool.name, args, {
619
+ // Forward server-side progress frames to the bridge caller,
620
+ // tagged with the registered name so multi-server UIs can
621
+ // disambiguate. No-op when `onProgress` isn't configured —
622
+ // the client then also omits the _meta.progressToken and
623
+ // the server won't emit progress.
624
+ onProgress: opts.onProgress ? (info) => opts.onProgress({ toolName: registeredName, ...info }) : void 0,
625
+ // Thread the tool-dispatch AbortSignal all the way down to
626
+ // the MCP request so Esc truly cancels in flight — the
627
+ // client will emit notifications/cancelled AND reject the
628
+ // pending promise immediately, no "wait for subprocess".
629
+ signal: ctx?.signal
630
+ });
619
631
  return flattenMcpResult(toolResult, { maxChars: maxResultChars });
620
632
  }
621
633
  });
@@ -1204,11 +1216,13 @@ var CacheFirstLoop = class {
1204
1216
  _turn = 0;
1205
1217
  _streamPreference;
1206
1218
  /**
1207
- * Set by {@link abort} to short-circuit the tool-call loop after the
1208
- * current iteration. Reset at the start of each `step()` so an Esc
1209
- * during one turn doesn't poison the next.
1219
+ * AbortController per active turn. Threaded through the DeepSeek
1220
+ * HTTP calls AND every tool dispatch so Esc actually cancels the
1221
+ * in-flight network/subprocess work — not "we'll get to it after
1222
+ * the current call finishes." Re-created at the start of each
1223
+ * `step()` (the prior turn's signal has already fired).
1210
1224
  */
1211
- _aborted = false;
1225
+ _turnAbort = new AbortController();
1212
1226
  constructor(opts) {
1213
1227
  this.client = opts.client;
1214
1228
  this.prefix = opts.prefix;
@@ -1321,14 +1335,15 @@ var CacheFirstLoop = class {
1321
1335
  return msgs;
1322
1336
  }
1323
1337
  /**
1324
- * Signal the currently-running {@link step} that the user wants to
1325
- * stop exploring. Takes effect at the next iteration boundary — if a
1326
- * tool call is mid-flight it will be allowed to finish, then the
1327
- * loop diverts to the forced-summary path so the user gets an
1328
- * answer instead of a cliff. Called by the TUI on Esc.
1338
+ * Signal the currently-running {@link step} to stop **now**. Cancels
1339
+ * the in-flight network request (DeepSeek HTTP/SSE) AND any tool call
1340
+ * currently dispatching (MCP `notifications/cancelled` + promise
1341
+ * reject). The loop itself also sees `signal.aborted` at each
1342
+ * iteration boundary and exits quickly instead of looping again.
1343
+ * Called by the TUI on Esc.
1329
1344
  */
1330
1345
  abort() {
1331
- this._aborted = true;
1346
+ this._turnAbort.abort();
1332
1347
  }
1333
1348
  /**
1334
1349
  * Drop everything in the log after (and including) the most recent
@@ -1366,13 +1381,14 @@ var CacheFirstLoop = class {
1366
1381
  async *step(userInput) {
1367
1382
  this._turn++;
1368
1383
  this.scratch.reset();
1369
- this._aborted = false;
1384
+ this._turnAbort = new AbortController();
1385
+ const signal = this._turnAbort.signal;
1370
1386
  let pendingUser = userInput;
1371
1387
  const toolSpecs = this.prefix.tools();
1372
1388
  const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
1373
1389
  let warnedForIterBudget = false;
1374
1390
  for (let iter = 0; iter < this.maxToolIters; iter++) {
1375
- if (this._aborted) {
1391
+ if (signal.aborted) {
1376
1392
  yield {
1377
1393
  turn: this._turn,
1378
1394
  role: "warning",
@@ -1435,7 +1451,8 @@ var CacheFirstLoop = class {
1435
1451
  {
1436
1452
  model: this.model,
1437
1453
  messages,
1438
- tools: toolSpecs.length ? toolSpecs : void 0
1454
+ tools: toolSpecs.length ? toolSpecs : void 0,
1455
+ signal
1439
1456
  },
1440
1457
  {
1441
1458
  ...this.branchOptions,
@@ -1444,8 +1461,8 @@ var CacheFirstLoop = class {
1444
1461
  }
1445
1462
  );
1446
1463
  for (let k = 0; k < budget; k++) {
1447
- const sample = queue.shift() ?? await new Promise((resolve4) => {
1448
- waiter = resolve4;
1464
+ const sample = queue.shift() ?? await new Promise((resolve5) => {
1465
+ waiter = resolve5;
1449
1466
  });
1450
1467
  yield {
1451
1468
  turn: this._turn,
@@ -1485,7 +1502,8 @@ var CacheFirstLoop = class {
1485
1502
  for await (const chunk of this.client.stream({
1486
1503
  model: this.model,
1487
1504
  messages,
1488
- tools: toolSpecs.length ? toolSpecs : void 0
1505
+ tools: toolSpecs.length ? toolSpecs : void 0,
1506
+ signal
1489
1507
  })) {
1490
1508
  if (chunk.contentDelta) {
1491
1509
  assistantContent += chunk.contentDelta;
@@ -1524,7 +1542,8 @@ var CacheFirstLoop = class {
1524
1542
  const resp = await this.client.chat({
1525
1543
  model: this.model,
1526
1544
  messages,
1527
- tools: toolSpecs.length ? toolSpecs : void 0
1545
+ tools: toolSpecs.length ? toolSpecs : void 0,
1546
+ signal
1528
1547
  });
1529
1548
  assistantContent = resp.content;
1530
1549
  reasoningContent = resp.reasoningContent ?? "";
@@ -1588,7 +1607,7 @@ var CacheFirstLoop = class {
1588
1607
  toolName: name,
1589
1608
  toolArgs: args
1590
1609
  };
1591
- const result = await this.tools.dispatch(name, args);
1610
+ const result = await this.tools.dispatch(name, args, { signal });
1592
1611
  this.appendAndPersist({
1593
1612
  role: "tool",
1594
1613
  tool_call_id: call.id ?? "",
@@ -1615,8 +1634,9 @@ var CacheFirstLoop = class {
1615
1634
  });
1616
1635
  const resp = await this.client.chat({
1617
1636
  model: this.model,
1618
- messages
1637
+ messages,
1619
1638
  // no tools → model is forced to answer in text
1639
+ signal: this._turnAbort.signal
1620
1640
  });
1621
1641
  const rawContent = resp.content?.trim() ?? "";
1622
1642
  const cleaned = stripHallucinatedToolMarkup(rawContent);
@@ -1712,13 +1732,298 @@ function formatLoopError(err) {
1712
1732
  return msg;
1713
1733
  }
1714
1734
 
1735
+ // src/tools/filesystem.ts
1736
+ import { promises as fs } from "fs";
1737
+ import * as pathMod from "path";
1738
+ var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
1739
+ var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
1740
+ function registerFilesystemTools(registry, opts) {
1741
+ const rootDir = pathMod.resolve(opts.rootDir);
1742
+ const allowWriting = opts.allowWriting !== false;
1743
+ const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
1744
+ const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
1745
+ const safePath = (raw) => {
1746
+ if (typeof raw !== "string" || raw.length === 0) {
1747
+ throw new Error("path must be a non-empty string");
1748
+ }
1749
+ const resolved = pathMod.resolve(rootDir, raw);
1750
+ const normRoot = pathMod.resolve(rootDir);
1751
+ const rel = pathMod.relative(normRoot, resolved);
1752
+ if (rel.startsWith("..") || pathMod.isAbsolute(rel)) {
1753
+ throw new Error(`path escapes sandbox root (${normRoot}): ${raw}`);
1754
+ }
1755
+ return resolved;
1756
+ };
1757
+ registry.register({
1758
+ name: "read_file",
1759
+ description: "Read a file under the sandbox root. Returns the full contents (truncated with a notice if larger than the per-call cap). Paths may be relative to the root or absolute-under-root.",
1760
+ parameters: {
1761
+ type: "object",
1762
+ properties: {
1763
+ path: { type: "string", description: "Path to read (relative to rootDir or absolute)." },
1764
+ head: { type: "integer", description: "If set, return only the first N lines." },
1765
+ tail: { type: "integer", description: "If set, return only the last N lines." }
1766
+ },
1767
+ required: ["path"]
1768
+ },
1769
+ fn: async (args) => {
1770
+ const abs = safePath(args.path);
1771
+ const stat = await fs.stat(abs);
1772
+ if (stat.isDirectory()) {
1773
+ throw new Error(`not a file: ${args.path} (it's a directory)`);
1774
+ }
1775
+ const raw = await fs.readFile(abs);
1776
+ if (raw.length > maxReadBytes) {
1777
+ const head = raw.slice(0, maxReadBytes).toString("utf8");
1778
+ return `${head}
1779
+
1780
+ [\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail for targeted view.]`;
1781
+ }
1782
+ const text = raw.toString("utf8");
1783
+ if (typeof args.head === "number" && args.head > 0) {
1784
+ return text.split(/\r?\n/).slice(0, args.head).join("\n");
1785
+ }
1786
+ if (typeof args.tail === "number" && args.tail > 0) {
1787
+ let lines = text.split(/\r?\n/);
1788
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
1789
+ return lines.slice(Math.max(0, lines.length - args.tail)).join("\n");
1790
+ }
1791
+ return text;
1792
+ }
1793
+ });
1794
+ registry.register({
1795
+ name: "list_directory",
1796
+ description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
1797
+ parameters: {
1798
+ type: "object",
1799
+ properties: {
1800
+ path: { type: "string", description: "Directory to list (default: root)." }
1801
+ }
1802
+ },
1803
+ fn: async (args) => {
1804
+ const abs = safePath(args.path ?? ".");
1805
+ const entries = await fs.readdir(abs, { withFileTypes: true });
1806
+ const lines = [];
1807
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1808
+ lines.push(e.isDirectory() ? `${e.name}/` : e.name);
1809
+ }
1810
+ return lines.join("\n") || "(empty directory)";
1811
+ }
1812
+ });
1813
+ registry.register({
1814
+ name: "directory_tree",
1815
+ description: "Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Caps output so a huge tree doesn't drown the context.",
1816
+ parameters: {
1817
+ type: "object",
1818
+ properties: {
1819
+ path: { type: "string", description: "Root of the tree (default: sandbox root)." },
1820
+ maxDepth: { type: "integer", description: "Max recursion depth (default 4)." }
1821
+ }
1822
+ },
1823
+ fn: async (args) => {
1824
+ const startAbs = safePath(args.path ?? ".");
1825
+ const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 4;
1826
+ const lines = [];
1827
+ let totalBytes = 0;
1828
+ let truncated = false;
1829
+ const walk2 = async (dir, depth) => {
1830
+ if (truncated) return;
1831
+ if (depth > maxDepth) return;
1832
+ let entries;
1833
+ try {
1834
+ entries = await fs.readdir(dir, { withFileTypes: true });
1835
+ } catch {
1836
+ return;
1837
+ }
1838
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1839
+ for (const e of entries) {
1840
+ if (truncated) return;
1841
+ const indent = " ".repeat(depth);
1842
+ const line = e.isDirectory() ? `${indent}${e.name}/` : `${indent}${e.name}`;
1843
+ totalBytes += line.length + 1;
1844
+ if (totalBytes > maxListBytes) {
1845
+ lines.push(` [\u2026 tree truncated at ${maxListBytes} bytes \u2026]`);
1846
+ truncated = true;
1847
+ return;
1848
+ }
1849
+ lines.push(line);
1850
+ if (e.isDirectory()) {
1851
+ await walk2(pathMod.join(dir, e.name), depth + 1);
1852
+ }
1853
+ }
1854
+ };
1855
+ await walk2(startAbs, 0);
1856
+ return lines.join("\n") || "(empty tree)";
1857
+ }
1858
+ });
1859
+ registry.register({
1860
+ name: "search_files",
1861
+ description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line.",
1862
+ parameters: {
1863
+ type: "object",
1864
+ properties: {
1865
+ path: { type: "string", description: "Directory to start the search at (default: root)." },
1866
+ pattern: {
1867
+ type: "string",
1868
+ description: "Substring (or regex) to match against filenames."
1869
+ }
1870
+ },
1871
+ required: ["pattern"]
1872
+ },
1873
+ fn: async (args) => {
1874
+ const startAbs = safePath(args.path ?? ".");
1875
+ const needle = args.pattern.toLowerCase();
1876
+ let re = null;
1877
+ try {
1878
+ re = new RegExp(args.pattern, "i");
1879
+ } catch {
1880
+ re = null;
1881
+ }
1882
+ const matches = [];
1883
+ let totalBytes = 0;
1884
+ const walk2 = async (dir) => {
1885
+ let entries;
1886
+ try {
1887
+ entries = await fs.readdir(dir, { withFileTypes: true });
1888
+ } catch {
1889
+ return;
1890
+ }
1891
+ for (const e of entries) {
1892
+ const full = pathMod.join(dir, e.name);
1893
+ const lower = e.name.toLowerCase();
1894
+ const hit = re ? re.test(e.name) : lower.includes(needle);
1895
+ if (hit) {
1896
+ const rel = pathMod.relative(rootDir, full);
1897
+ if (totalBytes + rel.length + 1 > maxListBytes) {
1898
+ matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
1899
+ return;
1900
+ }
1901
+ matches.push(rel);
1902
+ totalBytes += rel.length + 1;
1903
+ }
1904
+ if (e.isDirectory()) await walk2(full);
1905
+ }
1906
+ };
1907
+ await walk2(startAbs);
1908
+ return matches.length === 0 ? "(no matches)" : matches.join("\n");
1909
+ }
1910
+ });
1911
+ registry.register({
1912
+ name: "get_file_info",
1913
+ description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
1914
+ parameters: {
1915
+ type: "object",
1916
+ properties: {
1917
+ path: { type: "string" }
1918
+ },
1919
+ required: ["path"]
1920
+ },
1921
+ fn: async (args) => {
1922
+ const abs = safePath(args.path);
1923
+ const st = await fs.lstat(abs);
1924
+ const type = st.isDirectory() ? "directory" : st.isSymbolicLink() ? "symlink" : "file";
1925
+ return JSON.stringify({
1926
+ type,
1927
+ size: st.size,
1928
+ mtime: st.mtime.toISOString()
1929
+ });
1930
+ }
1931
+ });
1932
+ if (!allowWriting) return registry;
1933
+ registry.register({
1934
+ name: "write_file",
1935
+ description: "Create or overwrite a file under the sandbox root with the given content. Parent directories are created as needed.",
1936
+ parameters: {
1937
+ type: "object",
1938
+ properties: {
1939
+ path: { type: "string" },
1940
+ content: { type: "string" }
1941
+ },
1942
+ required: ["path", "content"]
1943
+ },
1944
+ fn: async (args) => {
1945
+ const abs = safePath(args.path);
1946
+ await fs.mkdir(pathMod.dirname(abs), { recursive: true });
1947
+ await fs.writeFile(abs, args.content, "utf8");
1948
+ return `wrote ${args.content.length} chars to ${pathMod.relative(rootDir, abs)}`;
1949
+ }
1950
+ });
1951
+ registry.register({
1952
+ name: "edit_file",
1953
+ description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites. This flat-string shape replaces the `{oldText, newText}[]` JSON array form that previously triggered R1 DSML hallucinations.",
1954
+ parameters: {
1955
+ type: "object",
1956
+ properties: {
1957
+ path: { type: "string" },
1958
+ search: { type: "string", description: "Exact text to find (must be unique)." },
1959
+ replace: { type: "string", description: "Text to substitute in place of `search`." }
1960
+ },
1961
+ required: ["path", "search", "replace"]
1962
+ },
1963
+ fn: async (args) => {
1964
+ const abs = safePath(args.path);
1965
+ const before = await fs.readFile(abs, "utf8");
1966
+ if (args.search.length === 0) {
1967
+ throw new Error("edit_file: search cannot be empty");
1968
+ }
1969
+ const firstIdx = before.indexOf(args.search);
1970
+ if (firstIdx < 0) {
1971
+ throw new Error(`edit_file: search text not found in ${pathMod.relative(rootDir, abs)}`);
1972
+ }
1973
+ const nextIdx = before.indexOf(args.search, firstIdx + 1);
1974
+ if (nextIdx >= 0) {
1975
+ throw new Error(
1976
+ `edit_file: search text appears multiple times in ${pathMod.relative(rootDir, abs)} \u2014 include more context to disambiguate`
1977
+ );
1978
+ }
1979
+ const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
1980
+ await fs.writeFile(abs, after, "utf8");
1981
+ return `edited ${pathMod.relative(rootDir, abs)} (${args.search.length}\u2192${args.replace.length} chars)`;
1982
+ }
1983
+ });
1984
+ registry.register({
1985
+ name: "create_directory",
1986
+ description: "Create a directory (and any missing parents) under the sandbox root.",
1987
+ parameters: {
1988
+ type: "object",
1989
+ properties: { path: { type: "string" } },
1990
+ required: ["path"]
1991
+ },
1992
+ fn: async (args) => {
1993
+ const abs = safePath(args.path);
1994
+ await fs.mkdir(abs, { recursive: true });
1995
+ return `created ${pathMod.relative(rootDir, abs)}/`;
1996
+ }
1997
+ });
1998
+ registry.register({
1999
+ name: "move_file",
2000
+ description: "Rename/move a file or directory under the sandbox root.",
2001
+ parameters: {
2002
+ type: "object",
2003
+ properties: {
2004
+ source: { type: "string" },
2005
+ destination: { type: "string" }
2006
+ },
2007
+ required: ["source", "destination"]
2008
+ },
2009
+ fn: async (args) => {
2010
+ const src = safePath(args.source);
2011
+ const dst = safePath(args.destination);
2012
+ await fs.mkdir(pathMod.dirname(dst), { recursive: true });
2013
+ await fs.rename(src, dst);
2014
+ return `moved ${pathMod.relative(rootDir, src)} \u2192 ${pathMod.relative(rootDir, dst)}`;
2015
+ }
2016
+ });
2017
+ return registry;
2018
+ }
2019
+
1715
2020
  // src/env.ts
1716
2021
  import { readFileSync as readFileSync3 } from "fs";
1717
- import { resolve } from "path";
2022
+ import { resolve as resolve2 } from "path";
1718
2023
  function loadDotenv(path = ".env") {
1719
2024
  let raw;
1720
2025
  try {
1721
- raw = readFileSync3(resolve(process.cwd(), path), "utf8");
2026
+ raw = readFileSync3(resolve2(process.cwd(), path), "utf8");
1722
2027
  } catch {
1723
2028
  return;
1724
2029
  }
@@ -2276,6 +2581,16 @@ var McpClient = class {
2276
2581
  readerStarted = false;
2277
2582
  initialized = false;
2278
2583
  _serverCapabilities = {};
2584
+ _serverInfo = { name: "", version: "" };
2585
+ _protocolVersion = "";
2586
+ _instructions;
2587
+ // Progress-token → handler for notifications/progress routing. Tokens
2588
+ // are minted per call when the caller supplies an onProgress
2589
+ // callback; cleared when the final response lands (or the pending
2590
+ // request rejects). No leaks — the `try/finally` in callTool
2591
+ // guarantees cleanup even on timeout.
2592
+ progressHandlers = /* @__PURE__ */ new Map();
2593
+ nextProgressToken = 1;
2279
2594
  constructor(opts) {
2280
2595
  this.transport = opts.transport;
2281
2596
  this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
@@ -2285,6 +2600,18 @@ var McpClient = class {
2285
2600
  get serverCapabilities() {
2286
2601
  return this._serverCapabilities;
2287
2602
  }
2603
+ /** Server's self-reported name + version, available after initialize(). */
2604
+ get serverInfo() {
2605
+ return this._serverInfo;
2606
+ }
2607
+ /** Protocol version the server agreed to during the handshake. */
2608
+ get protocolVersion() {
2609
+ return this._protocolVersion;
2610
+ }
2611
+ /** Optional free-form instructions the server provides at handshake. */
2612
+ get serverInstructions() {
2613
+ return this._instructions;
2614
+ }
2288
2615
  /**
2289
2616
  * Complete the initialize → initialized handshake. Must be called
2290
2617
  * before any other method (otherwise compliant servers reject).
@@ -2303,6 +2630,9 @@ var McpClient = class {
2303
2630
  clientInfo: this.clientInfo
2304
2631
  });
2305
2632
  this._serverCapabilities = result.capabilities ?? {};
2633
+ this._serverInfo = result.serverInfo ?? { name: "", version: "" };
2634
+ this._protocolVersion = result.protocolVersion ?? "";
2635
+ this._instructions = result.instructions;
2306
2636
  await this.transport.send({
2307
2637
  jsonrpc: "2.0",
2308
2638
  method: "notifications/initialized"
@@ -2315,13 +2645,36 @@ var McpClient = class {
2315
2645
  this.assertInitialized();
2316
2646
  return this.request("tools/list", {});
2317
2647
  }
2318
- /** Invoke a tool by name. Returns the raw MCP result (caller unwraps content). */
2319
- async callTool(name, args) {
2648
+ /**
2649
+ * Invoke a tool by name. When `onProgress` is supplied, attaches a
2650
+ * fresh progress token so the server can send incremental updates
2651
+ * via `notifications/progress`; they're routed to the callback until
2652
+ * the final response arrives (or the request times out, in which
2653
+ * case the handler is simply dropped — no extra notification).
2654
+ *
2655
+ * When `signal` is supplied, aborting it:
2656
+ * 1) fires `notifications/cancelled` to the server (MCP 2024-11-05
2657
+ * way of saying "forget this request, I no longer care"), and
2658
+ * 2) rejects the pending promise immediately with an AbortError,
2659
+ * so the caller doesn't have to wait for the subprocess to
2660
+ * finish its in-flight file write or network request.
2661
+ * The server MAY still emit a late response; we drop it in dispatch
2662
+ * since the request id is gone from `pending`.
2663
+ */
2664
+ async callTool(name, args, opts = {}) {
2320
2665
  this.assertInitialized();
2321
- return this.request("tools/call", {
2322
- name,
2323
- arguments: args ?? {}
2324
- });
2666
+ const params = { name, arguments: args ?? {} };
2667
+ let token;
2668
+ if (opts.onProgress) {
2669
+ token = this.nextProgressToken++;
2670
+ this.progressHandlers.set(token, opts.onProgress);
2671
+ params._meta = { progressToken: token };
2672
+ }
2673
+ try {
2674
+ return await this.request("tools/call", params, opts.signal);
2675
+ } finally {
2676
+ if (token !== void 0) this.progressHandlers.delete(token);
2677
+ }
2325
2678
  }
2326
2679
  /**
2327
2680
  * List resources the server exposes. Supports a pagination cursor;
@@ -2375,24 +2728,56 @@ var McpClient = class {
2375
2728
  assertInitialized() {
2376
2729
  if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
2377
2730
  }
2378
- async request(method, params) {
2731
+ async request(method, params, signal) {
2379
2732
  const id = this.nextId++;
2380
2733
  const frame = { jsonrpc: "2.0", id, method, params };
2381
- const promise = new Promise((resolve4, reject) => {
2734
+ let abortHandler = null;
2735
+ const promise = new Promise((resolve5, reject) => {
2382
2736
  const timeout = setTimeout(() => {
2383
2737
  this.pending.delete(id);
2738
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
2384
2739
  reject(
2385
2740
  new Error(`MCP request ${method} (id=${id}) timed out after ${this.requestTimeoutMs}ms`)
2386
2741
  );
2387
2742
  }, this.requestTimeoutMs);
2388
2743
  this.pending.set(id, {
2389
- resolve: resolve4,
2744
+ resolve: resolve5,
2390
2745
  reject,
2391
2746
  timeout
2392
2747
  });
2748
+ if (signal) {
2749
+ if (signal.aborted) {
2750
+ this.pending.delete(id);
2751
+ clearTimeout(timeout);
2752
+ reject(new Error(`MCP request ${method} (id=${id}) aborted before send`));
2753
+ return;
2754
+ }
2755
+ abortHandler = () => {
2756
+ this.pending.delete(id);
2757
+ clearTimeout(timeout);
2758
+ void this.transport.send({
2759
+ jsonrpc: "2.0",
2760
+ method: "notifications/cancelled",
2761
+ params: { requestId: id, reason: "aborted by user" }
2762
+ }).catch(() => {
2763
+ });
2764
+ reject(new Error(`MCP request ${method} (id=${id}) aborted by user`));
2765
+ };
2766
+ signal.addEventListener("abort", abortHandler, { once: true });
2767
+ }
2393
2768
  });
2394
- await this.transport.send(frame);
2395
- return promise;
2769
+ try {
2770
+ await this.transport.send(frame);
2771
+ } catch (err) {
2772
+ this.pending.delete(id);
2773
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
2774
+ throw err;
2775
+ }
2776
+ try {
2777
+ return await promise;
2778
+ } finally {
2779
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
2780
+ }
2396
2781
  }
2397
2782
  startReaderIfNeeded() {
2398
2783
  if (this.readerStarted) return;
@@ -2413,7 +2798,16 @@ var McpClient = class {
2413
2798
  }
2414
2799
  }
2415
2800
  dispatch(msg) {
2416
- if (!("id" in msg) || msg.id === null || msg.id === void 0) return;
2801
+ if (!("id" in msg) || msg.id === null || msg.id === void 0) {
2802
+ if ("method" in msg && msg.method === "notifications/progress") {
2803
+ const p = msg.params;
2804
+ if (!p || p.progressToken === void 0) return;
2805
+ const handler = this.progressHandlers.get(p.progressToken);
2806
+ if (!handler) return;
2807
+ handler({ progress: p.progress, total: p.total, message: p.message });
2808
+ }
2809
+ return;
2810
+ }
2417
2811
  if (!("result" in msg) && !("error" in msg)) return;
2418
2812
  const pending = this.pending.get(msg.id);
2419
2813
  if (!pending) return;
@@ -2470,12 +2864,12 @@ var StdioTransport = class {
2470
2864
  }
2471
2865
  async send(message) {
2472
2866
  if (this.closed) throw new Error("MCP transport is closed");
2473
- return new Promise((resolve4, reject) => {
2867
+ return new Promise((resolve5, reject) => {
2474
2868
  const line = `${JSON.stringify(message)}
2475
2869
  `;
2476
2870
  this.child.stdin.write(line, "utf8", (err) => {
2477
2871
  if (err) reject(err);
2478
- else resolve4();
2872
+ else resolve5();
2479
2873
  });
2480
2874
  });
2481
2875
  }
@@ -2486,8 +2880,8 @@ var StdioTransport = class {
2486
2880
  continue;
2487
2881
  }
2488
2882
  if (this.closed) return;
2489
- const next = await new Promise((resolve4) => {
2490
- this.waiters.push(resolve4);
2883
+ const next = await new Promise((resolve5) => {
2884
+ this.waiters.push(resolve5);
2491
2885
  });
2492
2886
  if (next === null) return;
2493
2887
  yield next;
@@ -2553,8 +2947,8 @@ var SseTransport = class {
2553
2947
  constructor(opts) {
2554
2948
  this.url = opts.url;
2555
2949
  this.headers = opts.headers ?? {};
2556
- this.endpointReady = new Promise((resolve4, reject) => {
2557
- this.resolveEndpoint = resolve4;
2950
+ this.endpointReady = new Promise((resolve5, reject) => {
2951
+ this.resolveEndpoint = resolve5;
2558
2952
  this.rejectEndpoint = reject;
2559
2953
  });
2560
2954
  this.endpointReady.catch(() => void 0);
@@ -2581,8 +2975,8 @@ var SseTransport = class {
2581
2975
  continue;
2582
2976
  }
2583
2977
  if (this.closed) return;
2584
- const next = await new Promise((resolve4) => {
2585
- this.waiters.push(resolve4);
2978
+ const next = await new Promise((resolve5) => {
2979
+ this.waiters.push(resolve5);
2586
2980
  });
2587
2981
  if (next === null) return;
2588
2982
  yield next;
@@ -2750,9 +3144,39 @@ function parseMcpSpec(input) {
2750
3144
  return { transport: "stdio", name, command, args };
2751
3145
  }
2752
3146
 
3147
+ // src/mcp/inspect.ts
3148
+ async function inspectMcpServer(client) {
3149
+ const tools = await trySection(() => client.listTools().then((r) => r.tools));
3150
+ const resources = await trySection(
3151
+ () => client.listResources().then((r) => r.resources)
3152
+ );
3153
+ const prompts = await trySection(() => client.listPrompts().then((r) => r.prompts));
3154
+ return {
3155
+ protocolVersion: client.protocolVersion || "(unknown)",
3156
+ serverInfo: client.serverInfo,
3157
+ capabilities: client.serverCapabilities ?? {},
3158
+ instructions: client.serverInstructions,
3159
+ tools,
3160
+ resources,
3161
+ prompts
3162
+ };
3163
+ }
3164
+ async function trySection(load) {
3165
+ try {
3166
+ const items = await load();
3167
+ return { supported: true, items };
3168
+ } catch (err) {
3169
+ const msg = err.message ?? String(err);
3170
+ if (/-32601/.test(msg) || /method not found/i.test(msg)) {
3171
+ return { supported: false, reason: "method not found (-32601)" };
3172
+ }
3173
+ return { supported: false, reason: msg };
3174
+ }
3175
+ }
3176
+
2753
3177
  // src/code/edit-blocks.ts
2754
3178
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
2755
- import { dirname as dirname3, resolve as resolve2 } from "path";
3179
+ import { dirname as dirname4, resolve as resolve3 } from "path";
2756
3180
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
2757
3181
  function parseEditBlocks(text) {
2758
3182
  const out = [];
@@ -2770,8 +3194,8 @@ function parseEditBlocks(text) {
2770
3194
  return out;
2771
3195
  }
2772
3196
  function applyEditBlock(block, rootDir) {
2773
- const absRoot = resolve2(rootDir);
2774
- const absTarget = resolve2(absRoot, block.path);
3197
+ const absRoot = resolve3(rootDir);
3198
+ const absTarget = resolve3(absRoot, block.path);
2775
3199
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
2776
3200
  return {
2777
3201
  path: block.path,
@@ -2790,7 +3214,7 @@ function applyEditBlock(block, rootDir) {
2790
3214
  message: "file does not exist; to create it, use an empty SEARCH block"
2791
3215
  };
2792
3216
  }
2793
- mkdirSync3(dirname3(absTarget), { recursive: true });
3217
+ mkdirSync3(dirname4(absTarget), { recursive: true });
2794
3218
  writeFileSync3(absTarget, block.replace, "utf8");
2795
3219
  return { path: block.path, status: "created" };
2796
3220
  }
@@ -2821,13 +3245,13 @@ function applyEditBlocks(blocks, rootDir) {
2821
3245
  return blocks.map((b) => applyEditBlock(b, rootDir));
2822
3246
  }
2823
3247
  function snapshotBeforeEdits(blocks, rootDir) {
2824
- const absRoot = resolve2(rootDir);
3248
+ const absRoot = resolve3(rootDir);
2825
3249
  const seen = /* @__PURE__ */ new Set();
2826
3250
  const snapshots = [];
2827
3251
  for (const b of blocks) {
2828
3252
  if (seen.has(b.path)) continue;
2829
3253
  seen.add(b.path);
2830
- const abs = resolve2(absRoot, b.path);
3254
+ const abs = resolve3(absRoot, b.path);
2831
3255
  if (!existsSync2(abs)) {
2832
3256
  snapshots.push({ path: b.path, prevContent: null });
2833
3257
  continue;
@@ -2841,9 +3265,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
2841
3265
  return snapshots;
2842
3266
  }
2843
3267
  function restoreSnapshots(snapshots, rootDir) {
2844
- const absRoot = resolve2(rootDir);
3268
+ const absRoot = resolve3(rootDir);
2845
3269
  return snapshots.map((snap) => {
2846
- const abs = resolve2(absRoot, snap.path);
3270
+ const abs = resolve3(absRoot, snap.path);
2847
3271
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
2848
3272
  return {
2849
3273
  path: snap.path,
@@ -2880,11 +3304,11 @@ var VERSION = "0.4.3";
2880
3304
 
2881
3305
  // src/cli/commands/chat.tsx
2882
3306
  import { render } from "ink";
2883
- import React8, { useState as useState4 } from "react";
3307
+ import React9, { useState as useState5 } from "react";
2884
3308
 
2885
3309
  // src/cli/ui/App.tsx
2886
- import { Box as Box6, Static, Text as Text6, useApp, useInput } from "ink";
2887
- import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
3310
+ import { Box as Box7, Static, Text as Text7, useApp, useInput as useInput2 } from "ink";
3311
+ import React7, { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
2888
3312
 
2889
3313
  // src/cli/ui/EventLog.tsx
2890
3314
  import { Box as Box3, Text as Text3 } from "ink";
@@ -3223,9 +3647,44 @@ function truncate2(s, max) {
3223
3647
  }
3224
3648
 
3225
3649
  // src/cli/ui/PromptInput.tsx
3226
- import { Box as Box4, Text as Text4 } from "ink";
3227
- import TextInput from "ink-text-input";
3228
- import React4 from "react";
3650
+ import { Box as Box4, Text as Text4, useInput } from "ink";
3651
+ import React4, { useEffect as useEffect2, useState as useState2 } from "react";
3652
+
3653
+ // src/cli/ui/multiline-keys.ts
3654
+ var BACKSLASH_SUFFIX = /\\$/;
3655
+ function processMultilineKey(value, key) {
3656
+ if (key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.escape || key.pageUp || key.pageDown) {
3657
+ return { next: null, submit: false };
3658
+ }
3659
+ if (key.input === "\n" || key.ctrl && key.input === "j") {
3660
+ return { next: `${value}
3661
+ `, submit: false };
3662
+ }
3663
+ if (key.return) {
3664
+ if (key.shift) {
3665
+ return { next: `${value}
3666
+ `, submit: false };
3667
+ }
3668
+ if (BACKSLASH_SUFFIX.test(value)) {
3669
+ return { next: `${value.slice(0, -1)}
3670
+ `, submit: false };
3671
+ }
3672
+ return { next: null, submit: true, submitValue: value };
3673
+ }
3674
+ if (key.backspace || key.delete) {
3675
+ if (value.length === 0) return { next: null, submit: false };
3676
+ return { next: value.slice(0, -1), submit: false };
3677
+ }
3678
+ if ((key.ctrl || key.meta) && key.input.length === 0) {
3679
+ return { next: null, submit: false };
3680
+ }
3681
+ if (key.input.length > 0 && !key.ctrl && !key.meta) {
3682
+ return { next: value + key.input, submit: false };
3683
+ }
3684
+ return { next: null, submit: false };
3685
+ }
3686
+
3687
+ // src/cli/ui/PromptInput.tsx
3229
3688
  function PromptInput({
3230
3689
  value,
3231
3690
  onChange,
@@ -3233,22 +3692,87 @@ function PromptInput({
3233
3692
  disabled,
3234
3693
  placeholder
3235
3694
  }) {
3236
- const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
3237
- 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(
3238
- TextInput,
3239
- {
3240
- value,
3241
- onChange,
3242
- onSubmit,
3243
- focus: !disabled,
3244
- placeholder: effectivePlaceholder
3695
+ const [showCursor, setShowCursor] = useState2(true);
3696
+ useEffect2(() => {
3697
+ if (disabled) {
3698
+ setShowCursor(false);
3699
+ return;
3245
3700
  }
3246
- ));
3701
+ setShowCursor(true);
3702
+ const id = setInterval(() => setShowCursor((s) => !s), 500);
3703
+ return () => clearInterval(id);
3704
+ }, [disabled]);
3705
+ useInput(
3706
+ (input, key) => {
3707
+ const keyEvent = {
3708
+ input,
3709
+ return: key.return,
3710
+ shift: key.shift,
3711
+ ctrl: key.ctrl,
3712
+ meta: key.meta,
3713
+ backspace: key.backspace,
3714
+ delete: key.delete,
3715
+ tab: key.tab,
3716
+ upArrow: key.upArrow,
3717
+ downArrow: key.downArrow,
3718
+ leftArrow: key.leftArrow,
3719
+ rightArrow: key.rightArrow,
3720
+ escape: key.escape,
3721
+ pageUp: key.pageUp,
3722
+ pageDown: key.pageDown
3723
+ };
3724
+ const action = processMultilineKey(value, keyEvent);
3725
+ if (action.next !== null) onChange(action.next);
3726
+ if (action.submit) onSubmit(action.submitValue ?? value);
3727
+ },
3728
+ { isActive: !disabled }
3729
+ );
3730
+ const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command \xB7 Ctrl+J for newline";
3731
+ const lines = value.length > 0 ? value.split("\n") : [""];
3732
+ const borderColor = disabled ? "gray" : "cyan";
3733
+ return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
3734
+ const isLast = i === lines.length - 1;
3735
+ const isFirst = i === 0;
3736
+ const showPlaceholder = isFirst && value.length === 0;
3737
+ return (
3738
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
3739
+ /* @__PURE__ */ React4.createElement(Box4, { key: i }, isFirst ? /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " "), showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React4.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null, showPlaceholder ? /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, effectivePlaceholder) : /* @__PURE__ */ React4.createElement(Text4, null, line), !showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React4.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null)
3740
+ );
3741
+ }));
3247
3742
  }
3248
3743
 
3249
- // src/cli/ui/StatsPanel.tsx
3744
+ // src/cli/ui/SlashSuggestions.tsx
3250
3745
  import { Box as Box5, Text as Text5 } from "ink";
3251
3746
  import React5 from "react";
3747
+ function SlashSuggestions({
3748
+ matches,
3749
+ selectedIndex
3750
+ }) {
3751
+ if (matches === null) return null;
3752
+ if (matches.length === 0) {
3753
+ return /* @__PURE__ */ React5.createElement(Box5, { paddingX: 1 }, /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
3754
+ }
3755
+ const MAX = 8;
3756
+ const total = matches.length;
3757
+ const windowStart = total <= MAX ? 0 : Math.max(0, Math.min(selectedIndex - Math.floor(MAX / 2), total - MAX));
3758
+ const shown = matches.slice(windowStart, windowStart + MAX);
3759
+ const hiddenAbove = windowStart;
3760
+ const hiddenBelow = total - windowStart - shown.length;
3761
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React5.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \u2191/\u2193 navigate \xB7 Tab or Enter to pick"));
3762
+ }
3763
+ function SuggestionRow({ spec, isSelected }) {
3764
+ const marker = isSelected ? "\u25B8" : " ";
3765
+ const name = `/${spec.cmd}`;
3766
+ const argsSuffix = spec.argsHint ? ` ${spec.argsHint}` : "";
3767
+ if (isSelected) {
3768
+ return /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, " ", spec.summary));
3769
+ }
3770
+ return /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
3771
+ }
3772
+
3773
+ // src/cli/ui/StatsPanel.tsx
3774
+ import { Box as Box6, Text as Text6 } from "ink";
3775
+ import React6 from "react";
3252
3776
  function StatsPanel({
3253
3777
  summary,
3254
3778
  model,
@@ -3262,7 +3786,7 @@ function StatsPanel({
3262
3786
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
3263
3787
  const ctxRatio = summary.lastPromptTokens / ctxMax;
3264
3788
  const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
3265
- 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), "%")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "ctx "), /* @__PURE__ */ React5.createElement(Text5, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React5.createElement(Text5, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null));
3789
+ return /* @__PURE__ */ React6.createElement(Box6, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Box6, { justifyContent: "space-between" }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, model), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React6.createElement(Text6, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "cache hit "), /* @__PURE__ */ React6.createElement(Text6, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "cost "), /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React6.createElement(Text6, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "saving "), /* @__PURE__ */ React6.createElement(Text6, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "ctx "), /* @__PURE__ */ React6.createElement(Text6, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React6.createElement(Text6, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null));
3266
3790
  }
3267
3791
  function formatTokens(n) {
3268
3792
  if (n < 1e3) return String(n);
@@ -3272,6 +3796,45 @@ function formatTokens(n) {
3272
3796
 
3273
3797
  // src/cli/ui/slash.ts
3274
3798
  import { spawnSync } from "child_process";
3799
+ var SLASH_COMMANDS = [
3800
+ { cmd: "help", summary: "show the full command reference" },
3801
+ { cmd: "status", summary: "current model, flags, context, session" },
3802
+ {
3803
+ cmd: "preset",
3804
+ argsHint: "<fast|smart|max>",
3805
+ summary: "one-tap model + harvest + branch bundle"
3806
+ },
3807
+ { cmd: "model", argsHint: "<id>", summary: "switch DeepSeek model id" },
3808
+ { cmd: "harvest", argsHint: "[on|off]", summary: "toggle Pillar-2 plan-state extraction" },
3809
+ { cmd: "branch", argsHint: "<N|off>", summary: "run N parallel samples per turn (N>=2)" },
3810
+ { cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
3811
+ { cmd: "tool", argsHint: "[N]", summary: "dump full output of the Nth tool call (1=latest)" },
3812
+ { cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
3813
+ { cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
3814
+ { cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
3815
+ { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
3816
+ { cmd: "forget", summary: "delete the current session from disk" },
3817
+ { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
3818
+ { cmd: "clear", summary: "clear the visible scrollback (log + session kept)" },
3819
+ { cmd: "exit", summary: "quit the TUI" },
3820
+ // Code-mode only
3821
+ { cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
3822
+ { cmd: "discard", summary: "drop pending edit blocks without writing", contextual: "code" },
3823
+ { cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
3824
+ {
3825
+ cmd: "commit",
3826
+ argsHint: '"msg"',
3827
+ summary: "git add -A && git commit -m ...",
3828
+ contextual: "code"
3829
+ }
3830
+ ];
3831
+ function suggestSlashCommands(prefix, codeMode = false) {
3832
+ const p = prefix.toLowerCase();
3833
+ return SLASH_COMMANDS.filter((c) => {
3834
+ if (c.contextual === "code" && !codeMode) return false;
3835
+ return c.cmd.startsWith(p);
3836
+ });
3837
+ }
3275
3838
  function parseSlash(text) {
3276
3839
  if (!text.startsWith("/")) return null;
3277
3840
  const parts = text.slice(1).trim().split(/\s+/);
@@ -3323,13 +3886,34 @@ function handleSlash(cmd, args, loop, ctx = {}) {
3323
3886
  ].join("\n")
3324
3887
  };
3325
3888
  case "mcp": {
3889
+ const servers = ctx.mcpServers ?? [];
3326
3890
  const specs = ctx.mcpSpecs ?? [];
3327
3891
  const toolSpecs = loop.prefix.toolSpecs ?? [];
3328
- if (specs.length === 0 && toolSpecs.length === 0) {
3892
+ if (servers.length === 0 && specs.length === 0 && toolSpecs.length === 0) {
3329
3893
  return {
3330
3894
  info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
3331
3895
  };
3332
3896
  }
3897
+ if (servers.length > 0) {
3898
+ const lines2 = [];
3899
+ for (const s of servers) {
3900
+ const { report } = s;
3901
+ const serverName = report.serverInfo.name || "(unknown)";
3902
+ const serverVer = report.serverInfo.version ? ` v${report.serverInfo.version}` : "";
3903
+ lines2.push(`[${s.label}] ${serverName}${serverVer} \u2014 ${s.spec}`);
3904
+ lines2.push(` tools ${s.toolCount}`);
3905
+ appendSection(lines2, "resources", report.resources);
3906
+ appendSection(lines2, "prompts ", report.prompts);
3907
+ lines2.push("");
3908
+ }
3909
+ lines2.push(
3910
+ "Chat mode consumes tools today; resources+prompts are surfaced here for awareness."
3911
+ );
3912
+ lines2.push(
3913
+ "Full catalog: `reasonix mcp list` \xB7 deeper diagnosis: `reasonix mcp inspect <spec>`."
3914
+ );
3915
+ return { info: lines2.join("\n") };
3916
+ }
3333
3917
  const lines = [];
3334
3918
  if (specs.length > 0) {
3335
3919
  lines.push(`MCP servers (${specs.length}):`);
@@ -3559,6 +4143,22 @@ ${entry.text}`
3559
4143
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
3560
4144
  }
3561
4145
  }
4146
+ function appendSection(lines, label, section) {
4147
+ if (!section || !section.supported) {
4148
+ lines.push(
4149
+ ` ${label.trim()} ${section?.supported === false ? "(not supported)" : "(none)"}`
4150
+ );
4151
+ return;
4152
+ }
4153
+ const names = section.items.map((i) => i.name);
4154
+ if (names.length === 0) {
4155
+ lines.push(` ${label.trim()} (none)`);
4156
+ return;
4157
+ }
4158
+ const head = names.slice(0, 5).join(", ");
4159
+ const more = names.length > 5 ? ` +${names.length - 5} more` : "";
4160
+ lines.push(` ${label.trim()} ${names.length} [${head}${more}]`);
4161
+ }
3562
4162
  function formatToolList(history) {
3563
4163
  const total = history.length;
3564
4164
  const header = `Tool calls in this session (${total}, most recent first):`;
@@ -3631,21 +4231,25 @@ function App({
3631
4231
  session,
3632
4232
  tools,
3633
4233
  mcpSpecs,
4234
+ mcpServers,
4235
+ progressSink,
3634
4236
  codeMode
3635
4237
  }) {
3636
4238
  const { exit } = useApp();
3637
- const [historical, setHistorical] = useState2([]);
3638
- const [streaming, setStreaming] = useState2(null);
3639
- const [input, setInput] = useState2("");
3640
- const [busy, setBusy] = useState2(false);
4239
+ const [historical, setHistorical] = useState3([]);
4240
+ const [streaming, setStreaming] = useState3(null);
4241
+ const [input, setInput] = useState3("");
4242
+ const [busy, setBusy] = useState3(false);
3641
4243
  const abortedThisTurn = useRef(false);
3642
- const [ongoingTool, setOngoingTool] = useState2(null);
4244
+ const [ongoingTool, setOngoingTool] = useState3(null);
4245
+ const [toolProgress, setToolProgress] = useState3(null);
3643
4246
  const lastEditSnapshots = useRef(null);
3644
4247
  const pendingEdits = useRef([]);
3645
4248
  const promptHistory = useRef([]);
3646
4249
  const historyCursor = useRef(-1);
3647
4250
  const toolHistoryRef = useRef([]);
3648
- const [summary, setSummary] = useState2({
4251
+ const [slashSelected, setSlashSelected] = useState3(0);
4252
+ const [summary, setSummary] = useState3({
3649
4253
  turns: 0,
3650
4254
  totalCostUsd: 0,
3651
4255
  claudeEquivalentUsd: 0,
@@ -3662,11 +4266,22 @@ function App({
3662
4266
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
3663
4267
  });
3664
4268
  }
3665
- useEffect2(() => {
4269
+ useEffect3(() => {
3666
4270
  return () => {
3667
4271
  transcriptRef.current?.end();
3668
4272
  };
3669
4273
  }, []);
4274
+ const slashMatches = useMemo(() => {
4275
+ if (!input.startsWith("/") || input.includes(" ")) return null;
4276
+ return suggestSlashCommands(input.slice(1), !!codeMode);
4277
+ }, [input, codeMode]);
4278
+ useEffect3(() => {
4279
+ setSlashSelected((prev) => {
4280
+ if (!slashMatches || slashMatches.length === 0) return 0;
4281
+ if (prev >= slashMatches.length) return slashMatches.length - 1;
4282
+ return prev;
4283
+ });
4284
+ }, [slashMatches]);
3670
4285
  const loopRef = useRef(null);
3671
4286
  const loop = useMemo(() => {
3672
4287
  if (loopRef.current) return loopRef.current;
@@ -3679,8 +4294,21 @@ function App({
3679
4294
  loopRef.current = l;
3680
4295
  return l;
3681
4296
  }, [model, system, harvest2, branch, session, tools]);
4297
+ useEffect3(() => {
4298
+ if (!progressSink) return;
4299
+ progressSink.current = (info) => {
4300
+ setToolProgress({
4301
+ progress: info.progress,
4302
+ total: info.total,
4303
+ message: info.message
4304
+ });
4305
+ };
4306
+ return () => {
4307
+ if (progressSink.current) progressSink.current = null;
4308
+ };
4309
+ }, [progressSink]);
3682
4310
  const sessionBannerShown = useRef(false);
3683
- useEffect2(() => {
4311
+ useEffect3(() => {
3684
4312
  if (sessionBannerShown.current) return;
3685
4313
  sessionBannerShown.current = true;
3686
4314
  if (!session) {
@@ -3712,7 +4340,7 @@ function App({
3712
4340
  ]);
3713
4341
  }
3714
4342
  }, [session, loop]);
3715
- useInput((_input, key) => {
4343
+ useInput2((_input, key) => {
3716
4344
  if (key.escape && busy) {
3717
4345
  if (abortedThisTurn.current) return;
3718
4346
  abortedThisTurn.current = true;
@@ -3720,6 +4348,21 @@ function App({
3720
4348
  return;
3721
4349
  }
3722
4350
  if (busy) return;
4351
+ if (slashMatches && slashMatches.length > 0) {
4352
+ if (key.upArrow) {
4353
+ setSlashSelected((i) => Math.max(0, i - 1));
4354
+ return;
4355
+ }
4356
+ if (key.downArrow) {
4357
+ setSlashSelected((i) => Math.min(slashMatches.length - 1, i + 1));
4358
+ return;
4359
+ }
4360
+ if (key.tab) {
4361
+ const sel = slashMatches[slashSelected] ?? slashMatches[0];
4362
+ if (sel) setInput(`/${sel.cmd}`);
4363
+ return;
4364
+ }
4365
+ }
3723
4366
  const hist = promptHistory.current;
3724
4367
  if (key.upArrow) {
3725
4368
  if (hist.length === 0) return;
@@ -3778,6 +4421,15 @@ function App({
3778
4421
  async (raw) => {
3779
4422
  let text = raw.trim();
3780
4423
  if (!text || busy) return;
4424
+ if (text.startsWith("/") && !text.includes(" ")) {
4425
+ const typed = text.slice(1).toLowerCase();
4426
+ const matches = suggestSlashCommands(typed, !!codeMode);
4427
+ const exact = matches.find((m) => m.cmd === typed);
4428
+ if (!exact && matches.length > 0) {
4429
+ const chosen = matches[slashSelected] ?? matches[0];
4430
+ if (chosen) text = `/${chosen.cmd}`;
4431
+ }
4432
+ }
3781
4433
  setInput("");
3782
4434
  historyCursor.current = -1;
3783
4435
  if (codeMode && pendingEdits.current.length > 0 && (text === "y" || text === "n")) {
@@ -3790,6 +4442,7 @@ function App({
3790
4442
  if (slash) {
3791
4443
  const result = handleSlash(slash.cmd, slash.args, loop, {
3792
4444
  mcpSpecs,
4445
+ mcpServers,
3793
4446
  codeUndo: codeMode ? codeUndo : void 0,
3794
4447
  codeApply: codeMode ? codeApply : void 0,
3795
4448
  codeDiscard: codeMode ? codeDiscard : void 0,
@@ -3905,9 +4558,11 @@ function App({
3905
4558
  }
3906
4559
  } else if (ev.role === "tool_start") {
3907
4560
  setOngoingTool({ name: ev.toolName ?? "?", args: ev.toolArgs });
4561
+ setToolProgress(null);
3908
4562
  } else if (ev.role === "tool") {
3909
4563
  flush();
3910
4564
  setOngoingTool(null);
4565
+ setToolProgress(null);
3911
4566
  toolHistoryRef.current.push({
3912
4567
  toolName: ev.toolName ?? "?",
3913
4568
  text: ev.content
@@ -3938,13 +4593,26 @@ function App({
3938
4593
  clearInterval(timer);
3939
4594
  setStreaming(null);
3940
4595
  setOngoingTool(null);
4596
+ setToolProgress(null);
3941
4597
  setSummary(loop.stats.summary());
3942
4598
  setBusy(false);
3943
4599
  }
3944
4600
  },
3945
- [busy, codeApply, codeDiscard, codeMode, codeUndo, exit, loop, mcpSpecs, writeTranscript]
4601
+ [
4602
+ busy,
4603
+ codeApply,
4604
+ codeDiscard,
4605
+ codeMode,
4606
+ codeUndo,
4607
+ exit,
4608
+ loop,
4609
+ mcpSpecs,
4610
+ mcpServers,
4611
+ slashSelected,
4612
+ writeTranscript
4613
+ ]
3946
4614
  );
3947
- return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
4615
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(
3948
4616
  StatsPanel,
3949
4617
  {
3950
4618
  summary,
@@ -3953,12 +4621,15 @@ function App({
3953
4621
  harvestOn: loop.harvestEnabled,
3954
4622
  branchBudget: loop.branchOptions.budget
3955
4623
  }
3956
- ), /* @__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, ongoingTool ? /* @__PURE__ */ React6.createElement(OngoingToolRow, { tool: ongoingTool }) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, { codeMode: !!codeMode }));
4624
+ ), /* @__PURE__ */ React7.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React7.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React7.createElement(Box7, { marginY: 1 }, /* @__PURE__ */ React7.createElement(EventRow, { event: streaming })) : null, ongoingTool ? /* @__PURE__ */ React7.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, /* @__PURE__ */ React7.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React7.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }));
3957
4625
  }
3958
- function OngoingToolRow({ tool }) {
3959
- const [tick, setTick] = useState2(0);
3960
- const [elapsed, setElapsed] = useState2(0);
3961
- useEffect2(() => {
4626
+ function OngoingToolRow({
4627
+ tool,
4628
+ progress
4629
+ }) {
4630
+ const [tick, setTick] = useState3(0);
4631
+ const [elapsed, setElapsed] = useState3(0);
4632
+ useEffect3(() => {
3962
4633
  const start = Date.now();
3963
4634
  const frameId = setInterval(() => setTick((t) => t + 1), 120);
3964
4635
  const secId = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1e3)), 1e3);
@@ -3969,7 +4640,19 @@ function OngoingToolRow({ tool }) {
3969
4640
  }, []);
3970
4641
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3971
4642
  const summary = summarizeToolArgs(tool.name, tool.args);
3972
- return /* @__PURE__ */ React6.createElement(Box6, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, ` ${elapsed}s`)), summary ? /* @__PURE__ */ React6.createElement(Box6, { paddingLeft: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, summary)) : null);
4643
+ return /* @__PURE__ */ React7.createElement(Box7, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box7, null, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React7.createElement(Text7, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React7.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React7.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, summary)) : null);
4644
+ }
4645
+ function renderProgressLine(p) {
4646
+ const msg = p.message ? ` ${p.message}` : "";
4647
+ if (p.total && p.total > 0) {
4648
+ const ratio = Math.max(0, Math.min(1, p.progress / p.total));
4649
+ const width = 20;
4650
+ const filled = Math.round(ratio * width);
4651
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
4652
+ const pct2 = (ratio * 100).toFixed(0);
4653
+ return `[${bar}] ${p.progress}/${p.total} ${pct2}%${msg}`;
4654
+ }
4655
+ return `progress: ${p.progress}${msg}`;
3973
4656
  }
3974
4657
  function summarizeToolArgs(name, args) {
3975
4658
  if (!args || args === "{}") return "";
@@ -4011,9 +4694,6 @@ function summarizeToolArgs(name, args) {
4011
4694
  }
4012
4695
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
4013
4696
  }
4014
- function CommandStrip({ codeMode }) {
4015
- return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"), codeMode ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, 'code mode: /apply (y) \xB7 /discard (n) \xB7 /undo \xB7 /commit "msg" \u2014 edits NEVER write without /apply') : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "\u2191/\u2193 recall prompts \xB7 /retry re-send last \xB7 /think see R1 reasoning \xB7 /tool N full tool output"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
4016
- }
4017
4697
  function formatEditResults(results) {
4018
4698
  const lines = results.map((r) => {
4019
4699
  const mark = r.status === "applied" || r.status === "created" ? "\u2713" : "\u2717";
@@ -4056,12 +4736,12 @@ function describeRepair(repair) {
4056
4736
  }
4057
4737
 
4058
4738
  // src/cli/ui/Setup.tsx
4059
- import { Box as Box7, Text as Text7, useApp as useApp2 } from "ink";
4060
- import TextInput2 from "ink-text-input";
4061
- import React7, { useState as useState3 } from "react";
4739
+ import { Box as Box8, Text as Text8, useApp as useApp2 } from "ink";
4740
+ import TextInput from "ink-text-input";
4741
+ import React8, { useState as useState4 } from "react";
4062
4742
  function Setup({ onReady }) {
4063
- const [value, setValue] = useState3("");
4064
- const [error, setError] = useState3(null);
4743
+ const [value, setValue] = useState4("");
4744
+ const [error, setError] = useState4(null);
4065
4745
  const { exit } = useApp2();
4066
4746
  const handleSubmit = (raw) => {
4067
4747
  const trimmed = raw.trim();
@@ -4082,8 +4762,8 @@ function Setup({ onReady }) {
4082
4762
  }
4083
4763
  onReady(trimmed);
4084
4764
  };
4085
- 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(
4086
- TextInput2,
4765
+ return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React8.createElement(
4766
+ TextInput,
4087
4767
  {
4088
4768
  value,
4089
4769
  onChange: setValue,
@@ -4091,14 +4771,14 @@ function Setup({ onReady }) {
4091
4771
  mask: "\u2022",
4092
4772
  placeholder: "sk-..."
4093
4773
  }
4094
- )), 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.)")));
4774
+ )), error ? /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { color: "red" }, error)) : value ? /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "(Type /exit to abort.)")));
4095
4775
  }
4096
4776
 
4097
4777
  // src/cli/commands/chat.tsx
4098
- function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
4099
- const [key, setKey] = useState4(initialKey);
4778
+ function Root({ initialKey, tools, mcpSpecs, mcpServers, progressSink, ...appProps }) {
4779
+ const [key, setKey] = useState5(initialKey);
4100
4780
  if (!key) {
4101
- return /* @__PURE__ */ React8.createElement(
4781
+ return /* @__PURE__ */ React9.createElement(
4102
4782
  Setup,
4103
4783
  {
4104
4784
  onReady: (k) => {
@@ -4109,7 +4789,7 @@ function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
4109
4789
  );
4110
4790
  }
4111
4791
  process.env.DEEPSEEK_API_KEY = key;
4112
- return /* @__PURE__ */ React8.createElement(
4792
+ return /* @__PURE__ */ React9.createElement(
4113
4793
  App,
4114
4794
  {
4115
4795
  model: appProps.model,
@@ -4120,6 +4800,8 @@ function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
4120
4800
  session: appProps.session,
4121
4801
  tools,
4122
4802
  mcpSpecs,
4803
+ mcpServers,
4804
+ progressSink,
4123
4805
  codeMode: appProps.codeMode
4124
4806
  }
4125
4807
  );
@@ -4131,9 +4813,11 @@ async function chatCommand(opts) {
4131
4813
  const clients = [];
4132
4814
  const successfulSpecs = [];
4133
4815
  const failedSpecs = [];
4134
- let tools;
4816
+ const mcpServers = [];
4817
+ const progressSink = { current: null };
4818
+ let tools = opts.seedTools;
4135
4819
  if (requestedSpecs.length > 0) {
4136
- tools = new ToolRegistry();
4820
+ if (!tools) tools = new ToolRegistry();
4137
4821
  for (const raw of requestedSpecs) {
4138
4822
  try {
4139
4823
  const spec = parseMcpSpec(raw);
@@ -4141,7 +4825,24 @@ async function chatCommand(opts) {
4141
4825
  const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
4142
4826
  const mcp2 = new McpClient({ transport });
4143
4827
  await mcp2.initialize();
4144
- const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix });
4828
+ const bridge = await bridgeMcpTools(mcp2, {
4829
+ registry: tools,
4830
+ namePrefix: prefix,
4831
+ onProgress: (info) => progressSink.current?.(info)
4832
+ });
4833
+ let report;
4834
+ try {
4835
+ report = await inspectMcpServer(mcp2);
4836
+ } catch {
4837
+ report = {
4838
+ protocolVersion: mcp2.protocolVersion,
4839
+ serverInfo: mcp2.serverInfo,
4840
+ capabilities: mcp2.serverCapabilities ?? {},
4841
+ tools: { supported: true, items: [] },
4842
+ resources: { supported: false, reason: "inspect failed" },
4843
+ prompts: { supported: false, reason: "inspect failed" }
4844
+ };
4845
+ }
4145
4846
  const label = spec.name ?? "anon";
4146
4847
  const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
4147
4848
  process.stderr.write(
@@ -4150,6 +4851,12 @@ async function chatCommand(opts) {
4150
4851
  );
4151
4852
  clients.push(mcp2);
4152
4853
  successfulSpecs.push(raw);
4854
+ mcpServers.push({
4855
+ label,
4856
+ spec: raw,
4857
+ toolCount: bridge.registeredNames.length,
4858
+ report
4859
+ });
4153
4860
  } catch (err) {
4154
4861
  const reason = err.message;
4155
4862
  failedSpecs.push({ spec: raw, reason });
@@ -4160,13 +4867,23 @@ async function chatCommand(opts) {
4160
4867
  );
4161
4868
  }
4162
4869
  }
4163
- if (successfulSpecs.length === 0) {
4870
+ if (successfulSpecs.length === 0 && !opts.seedTools) {
4164
4871
  tools = void 0;
4165
4872
  }
4166
4873
  }
4167
4874
  const mcpSpecs = successfulSpecs;
4168
4875
  const { waitUntilExit } = render(
4169
- /* @__PURE__ */ React8.createElement(Root, { initialKey, tools, mcpSpecs, ...opts }),
4876
+ /* @__PURE__ */ React9.createElement(
4877
+ Root,
4878
+ {
4879
+ initialKey,
4880
+ tools,
4881
+ mcpSpecs,
4882
+ mcpServers,
4883
+ progressSink,
4884
+ ...opts
4885
+ }
4886
+ ),
4170
4887
  { exitOnCtrlC: true }
4171
4888
  );
4172
4889
  try {
@@ -4177,14 +4894,15 @@ async function chatCommand(opts) {
4177
4894
  }
4178
4895
 
4179
4896
  // src/cli/commands/code.tsx
4180
- import { basename, resolve as resolve3 } from "path";
4897
+ import { basename, resolve as resolve4 } from "path";
4181
4898
  async function codeCommand(opts = {}) {
4182
4899
  const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-MMANQ36Z.js");
4183
- const rootDir = resolve3(opts.dir ?? process.cwd());
4900
+ const rootDir = resolve4(opts.dir ?? process.cwd());
4184
4901
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
4185
- const fsSpec = `filesystem=npx -y @modelcontextprotocol/server-filesystem ${quoteIfNeeded(rootDir)}`;
4902
+ const tools = new ToolRegistry();
4903
+ registerFilesystemTools(tools, { rootDir });
4186
4904
  process.stderr.write(
4187
- `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}"
4905
+ `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native fs tool(s)
4188
4906
  `
4189
4907
  );
4190
4908
  await chatCommand({
@@ -4194,46 +4912,43 @@ async function codeCommand(opts = {}) {
4194
4912
  system: codeSystemPrompt2(rootDir),
4195
4913
  transcript: opts.transcript,
4196
4914
  session,
4197
- mcp: [fsSpec],
4915
+ seedTools: tools,
4198
4916
  codeMode: { rootDir }
4199
4917
  });
4200
4918
  }
4201
- function quoteIfNeeded(s) {
4202
- return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
4203
- }
4204
4919
 
4205
4920
  // src/cli/commands/diff.ts
4206
4921
  import { writeFileSync as writeFileSync4 } from "fs";
4207
4922
  import { basename as basename2 } from "path";
4208
4923
  import { render as render2 } from "ink";
4209
- import React11 from "react";
4924
+ import React12 from "react";
4210
4925
 
4211
4926
  // src/cli/ui/DiffApp.tsx
4212
- import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput as useInput2 } from "ink";
4213
- import React10, { useState as useState5 } from "react";
4927
+ import { Box as Box10, Static as Static2, Text as Text10, useApp as useApp3, useInput as useInput3 } from "ink";
4928
+ import React11, { useState as useState6 } from "react";
4214
4929
 
4215
4930
  // src/cli/ui/RecordView.tsx
4216
- import { Box as Box8, Text as Text8 } from "ink";
4217
- import React9 from "react";
4931
+ import { Box as Box9, Text as Text9 } from "ink";
4932
+ import React10 from "react";
4218
4933
  function RecordView({ rec, compact = false }) {
4219
4934
  const toolArgsMax = compact ? 120 : 200;
4220
4935
  const toolContentMax = compact ? 200 : 400;
4221
4936
  if (rec.role === "user") {
4222
- return /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React9.createElement(Text8, null, rec.content));
4937
+ return /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React10.createElement(Text9, null, rec.content));
4223
4938
  }
4224
4939
  if (rec.role === "assistant_final") {
4225
- 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)"));
4940
+ return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Text9, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React10.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React10.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React10.createElement(Text9, null, rec.content) : /* @__PURE__ */ React10.createElement(Text9, { dimColor: true, italic: true }, "(tool-call response only)"));
4226
4941
  }
4227
4942
  if (rec.role === "tool") {
4228
- 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)));
4943
+ return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
4229
4944
  }
4230
4945
  if (rec.role === "error") {
4231
- 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));
4946
+ return /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React10.createElement(Text9, { color: "red" }, rec.error ?? rec.content));
4232
4947
  }
4233
4948
  if (rec.role === "done" || rec.role === "assistant_delta") {
4234
4949
  return null;
4235
4950
  }
4236
- return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "[", rec.role, "] ", rec.content));
4951
+ return /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "[", rec.role, "] ", rec.content));
4237
4952
  }
4238
4953
  function CacheBadge({ usage }) {
4239
4954
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -4242,7 +4957,7 @@ function CacheBadge({ usage }) {
4242
4957
  if (total === 0) return null;
4243
4958
  const pct2 = hit / total * 100;
4244
4959
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
4245
- return /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React9.createElement(Text8, { color }, pct2.toFixed(1), "%"));
4960
+ return /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React10.createElement(Text9, { color }, pct2.toFixed(1), "%"));
4246
4961
  }
4247
4962
  function truncate3(s, max) {
4248
4963
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -4253,8 +4968,8 @@ function DiffApp({ report }) {
4253
4968
  const { exit } = useApp3();
4254
4969
  const maxIdx = Math.max(0, report.pairs.length - 1);
4255
4970
  const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
4256
- const [idx, setIdx] = useState5(Math.max(0, initialIdx));
4257
- useInput2((input, key) => {
4971
+ const [idx, setIdx] = useState6(Math.max(0, initialIdx));
4972
+ useInput3((input, key) => {
4258
4973
  if (input === "q" || key.ctrl && input === "c") {
4259
4974
  exit();
4260
4975
  return;
@@ -4276,7 +4991,7 @@ function DiffApp({ report }) {
4276
4991
  }
4277
4992
  });
4278
4993
  const pair = report.pairs[idx];
4279
- 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")));
4994
+ return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React11.createElement(DiffHeader, { report }), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text10, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React11.createElement(Text10, null, pair ? /* @__PURE__ */ React11.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React11.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React11.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React11.createElement(Text10, null, pair.divergenceNote)) : null, /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "j"), "/", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "k"), "/", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "N"), "/", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "g"), "/", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "q"), " ", "quit")));
4280
4995
  }
4281
4996
  function DiffHeader({ report }) {
4282
4997
  const a = report.a;
@@ -4294,15 +5009,15 @@ function DiffHeader({ report }) {
4294
5009
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
4295
5010
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
4296
5011
  }
4297
- 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);
5012
+ return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React11.createElement(Text10, { color: "blue" }, a.label), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " vs B="), /* @__PURE__ */ React11.createElement(Text10, { color: "magenta" }, b.label)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React11.createElement(Text10, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React11.createElement(Text10, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React11.createElement(Text10, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React11.createElement(Text10, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "model calls "), /* @__PURE__ */ React11.createElement(Text10, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true, italic: true }, prefixLine)) : null);
4298
5013
  }
4299
5014
  function Pane({
4300
5015
  label,
4301
5016
  headerColor,
4302
5017
  records
4303
5018
  }) {
4304
- return /* @__PURE__ */ React10.createElement(
4305
- Box9,
5019
+ return /* @__PURE__ */ React11.createElement(
5020
+ Box10,
4306
5021
  {
4307
5022
  flexDirection: "column",
4308
5023
  flexGrow: 1,
@@ -4310,21 +5025,21 @@ function Pane({
4310
5025
  borderStyle: "single",
4311
5026
  borderColor: headerColor
4312
5027
  },
4313
- /* @__PURE__ */ React10.createElement(Text9, { color: headerColor, bold: true }, label),
4314
- 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 }))
5028
+ /* @__PURE__ */ React11.createElement(Text10, { color: headerColor, bold: true }, label),
5029
+ records.length === 0 ? /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React11.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React11.createElement(RecordView, { key, rec, compact: true }))
4315
5030
  );
4316
5031
  }
4317
5032
  function KindBadge({ kind }) {
4318
5033
  if (kind === "match") {
4319
- return /* @__PURE__ */ React10.createElement(Text9, { color: "green" }, "\u2713 match");
5034
+ return /* @__PURE__ */ React11.createElement(Text10, { color: "green" }, "\u2713 match");
4320
5035
  }
4321
5036
  if (kind === "diverge") {
4322
- return /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u2605 diverge");
5037
+ return /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, "\u2605 diverge");
4323
5038
  }
4324
5039
  if (kind === "only_in_a") {
4325
- return /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, "\u2190 only in A");
5040
+ return /* @__PURE__ */ React11.createElement(Text10, { color: "blue" }, "\u2190 only in A");
4326
5041
  }
4327
- return /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, "\u2192 only in B");
5042
+ return /* @__PURE__ */ React11.createElement(Text10, { color: "magenta" }, "\u2192 only in B");
4328
5043
  }
4329
5044
  function paneRecords(pair, side) {
4330
5045
  if (!pair) return [];
@@ -4355,7 +5070,7 @@ markdown report written to ${opts.mdPath}`);
4355
5070
  return;
4356
5071
  }
4357
5072
  if (wantTui) {
4358
- const { waitUntilExit } = render2(React11.createElement(DiffApp, { report }), {
5073
+ const { waitUntilExit } = render2(React12.createElement(DiffApp, { report }), {
4359
5074
  exitOnCtrlC: true
4360
5075
  });
4361
5076
  await waitUntilExit();
@@ -4364,6 +5079,70 @@ markdown report written to ${opts.mdPath}`);
4364
5079
  console.log(renderSummaryTable(report));
4365
5080
  }
4366
5081
 
5082
+ // src/cli/commands/mcp-inspect.ts
5083
+ async function mcpInspectCommand(opts) {
5084
+ const spec = parseMcpSpec(opts.spec);
5085
+ const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
5086
+ const client = new McpClient({ transport });
5087
+ try {
5088
+ await client.initialize();
5089
+ const report = await inspectMcpServer(client);
5090
+ if (opts.json) {
5091
+ console.log(JSON.stringify(report, null, 2));
5092
+ } else {
5093
+ console.log(formatReport(spec.name ?? "(anon)", report));
5094
+ }
5095
+ } finally {
5096
+ await client.close();
5097
+ }
5098
+ }
5099
+ function formatReport(nsName, r) {
5100
+ const lines = [];
5101
+ lines.push(`MCP server [${nsName}]`);
5102
+ lines.push(
5103
+ ` server ${r.serverInfo.name || "(unknown)"}${r.serverInfo.version ? ` v${r.serverInfo.version}` : ""}`
5104
+ );
5105
+ lines.push(` protocol ${r.protocolVersion}`);
5106
+ const capKeys = Object.keys(r.capabilities);
5107
+ lines.push(` caps ${capKeys.length > 0 ? capKeys.join(", ") : "(none advertised)"}`);
5108
+ if (r.instructions) {
5109
+ lines.push(` notes ${r.instructions.trim().slice(0, 200)}`);
5110
+ }
5111
+ lines.push("");
5112
+ lines.push(formatSection("Tools", r.tools, toolLine));
5113
+ lines.push(formatSection("Resources", r.resources, resourceLine));
5114
+ lines.push(formatSection("Prompts", r.prompts, promptLine));
5115
+ return lines.join("\n");
5116
+ }
5117
+ function formatSection(title, section, render5) {
5118
+ if (!section.supported) {
5119
+ return `${title}: (not supported \u2014 ${section.reason})`;
5120
+ }
5121
+ if (section.items.length === 0) {
5122
+ return `${title}: (none)`;
5123
+ }
5124
+ const lines = [`${title} (${section.items.length}):`];
5125
+ for (const item of section.items) lines.push(` ${render5(item)}`);
5126
+ return lines.join("\n");
5127
+ }
5128
+ function toolLine(t) {
5129
+ const desc = t.description ? ` \u2014 ${oneLine(t.description, 80)}` : "";
5130
+ return `\xB7 ${t.name}${desc}`;
5131
+ }
5132
+ function resourceLine(r) {
5133
+ const mime = r.mimeType ? ` [${r.mimeType}]` : "";
5134
+ return `\xB7 ${r.name}${mime} ${r.uri}`;
5135
+ }
5136
+ function promptLine(p) {
5137
+ const argPart = p.arguments && p.arguments.length > 0 ? ` (${p.arguments.map((a) => a.required ? a.name : `${a.name}?`).join(", ")})` : "";
5138
+ const desc = p.description ? ` \u2014 ${oneLine(p.description, 80)}` : "";
5139
+ return `\xB7 ${p.name}${argPart}${desc}`;
5140
+ }
5141
+ function oneLine(s, max) {
5142
+ const flat = s.replace(/\s+/g, " ").trim();
5143
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
5144
+ }
5145
+
4367
5146
  // src/mcp/catalog.ts
4368
5147
  var MCP_CATALOG = [
4369
5148
  {
@@ -4431,16 +5210,16 @@ function pad(s, width) {
4431
5210
 
4432
5211
  // src/cli/commands/replay.ts
4433
5212
  import { render as render3 } from "ink";
4434
- import React13 from "react";
5213
+ import React14 from "react";
4435
5214
 
4436
5215
  // src/cli/ui/ReplayApp.tsx
4437
- import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as useInput3 } from "ink";
4438
- import React12, { useMemo as useMemo2, useState as useState6 } from "react";
5216
+ import { Box as Box11, Static as Static3, Text as Text11, useApp as useApp4, useInput as useInput4 } from "ink";
5217
+ import React13, { useMemo as useMemo2, useState as useState7 } from "react";
4439
5218
  function ReplayApp({ meta, pages }) {
4440
5219
  const { exit } = useApp4();
4441
5220
  const maxIdx = Math.max(0, pages.length - 1);
4442
- const [idx, setIdx] = useState6(maxIdx);
4443
- useInput3((input, key) => {
5221
+ const [idx, setIdx] = useState7(maxIdx);
5222
+ useInput4((input, key) => {
4444
5223
  if (input === "q" || key.ctrl && input === "c") {
4445
5224
  exit();
4446
5225
  return;
@@ -4472,14 +5251,14 @@ function ReplayApp({ meta, pages }) {
4472
5251
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
4473
5252
  const currentPage = pages[idx];
4474
5253
  const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
4475
- return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(
5254
+ return /* @__PURE__ */ React13.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React13.createElement(
4476
5255
  StatsPanel,
4477
5256
  {
4478
5257
  summary,
4479
5258
  model: cumStats.models[0] ?? meta?.model ?? "?",
4480
5259
  prefixHash
4481
5260
  }
4482
- ), /* @__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")));
5261
+ ), /* @__PURE__ */ React13.createElement(Box11, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React13.createElement(Box11, { justifyContent: "space-between" }, /* @__PURE__ */ React13.createElement(Text11, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React13.createElement(Text11, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React13.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React13.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React13.createElement(Text11, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React13.createElement(Box11, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React13.createElement(Text11, { dimColor: true }, /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "j"), "/", /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "k"), "/", /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React13.createElement(Text11, { bold: true }, "q"), " quit")));
4483
5262
  }
4484
5263
 
4485
5264
  // src/cli/commands/replay.ts
@@ -4491,7 +5270,7 @@ async function replayCommand(opts) {
4491
5270
  }
4492
5271
  const { parsed } = replayFromFile(opts.path);
4493
5272
  const pages = groupRecordsByTurn(parsed.records);
4494
- const { waitUntilExit } = render3(React13.createElement(ReplayApp, { meta: parsed.meta, pages }), {
5273
+ const { waitUntilExit } = render3(React14.createElement(ReplayApp, { meta: parsed.meta, pages }), {
4495
5274
  exitOnCtrlC: true
4496
5275
  });
4497
5276
  await waitUntilExit();
@@ -4543,7 +5322,7 @@ function sliceRecords(records, opts) {
4543
5322
  function renderRecord(rec) {
4544
5323
  const turn = `[t${rec.turn}]`;
4545
5324
  if (rec.role === "user") {
4546
- console.log(`${turn} USER: ${oneLine(rec.content)}`);
5325
+ console.log(`${turn} USER: ${oneLine2(rec.content)}`);
4547
5326
  } else if (rec.role === "assistant_final") {
4548
5327
  const cost = rec.cost !== void 0 ? ` $${rec.cost.toFixed(6)}` : "";
4549
5328
  const cache = rec.usage && (rec.usage.prompt_cache_hit_tokens !== void 0 || rec.usage.prompt_cache_miss_tokens !== void 0) ? (() => {
@@ -4552,7 +5331,7 @@ function renderRecord(rec) {
4552
5331
  const total = hit + miss;
4553
5332
  return total > 0 ? ` cache=${(hit / total * 100).toFixed(1)}%` : "";
4554
5333
  })() : "";
4555
- console.log(`${turn} AGENT:${cost}${cache} ${oneLine(rec.content)}`);
5334
+ console.log(`${turn} AGENT:${cost}${cache} ${oneLine2(rec.content)}`);
4556
5335
  if (rec.planState) {
4557
5336
  const ps = rec.planState;
4558
5337
  if (ps.subgoals.length)
@@ -4569,16 +5348,16 @@ function renderRecord(rec) {
4569
5348
  );
4570
5349
  }
4571
5350
  } else if (rec.role === "tool") {
4572
- const args = rec.args ? ` args=${oneLine(rec.args, 80)}` : "";
4573
- console.log(`${turn} TOOL ${rec.tool ?? "?"}:${args} \u2192 ${oneLine(rec.content, 120)}`);
5351
+ const args = rec.args ? ` args=${oneLine2(rec.args, 80)}` : "";
5352
+ console.log(`${turn} TOOL ${rec.tool ?? "?"}:${args} \u2192 ${oneLine2(rec.content, 120)}`);
4574
5353
  } else if (rec.role === "error") {
4575
5354
  console.log(`${turn} ERROR: ${rec.error ?? rec.content}`);
4576
5355
  } else if (rec.role === "done") {
4577
5356
  } else {
4578
- console.log(`${turn} ${rec.role}: ${oneLine(rec.content)}`);
5357
+ console.log(`${turn} ${rec.role}: ${oneLine2(rec.content)}`);
4579
5358
  }
4580
5359
  }
4581
- function oneLine(s, max = 200) {
5360
+ function oneLine2(s, max = 200) {
4582
5361
  const collapsed = s.replace(/\s+/g, " ").trim();
4583
5362
  return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
4584
5363
  }
@@ -4764,7 +5543,7 @@ function inspectSession(name, verbose) {
4764
5543
  function renderMessage(msg, turnIdx, verbose) {
4765
5544
  const turn = turnIdx > 0 ? `[t${turnIdx}]` : "[start]";
4766
5545
  const content = typeof msg.content === "string" ? msg.content : "";
4767
- const flat = oneLine2(content);
5546
+ const flat = oneLine3(content);
4768
5547
  if (msg.role === "user") {
4769
5548
  console.log(`${turn} USER: ${flat}`);
4770
5549
  } else if (msg.role === "assistant") {
@@ -4782,7 +5561,7 @@ function renderMessage(msg, turnIdx, verbose) {
4782
5561
  if (verbose) console.log(`${turn} SYSTEM: ${truncate4(flat, 160)}`);
4783
5562
  }
4784
5563
  }
4785
- function oneLine2(s, max = 200) {
5564
+ function oneLine3(s, max = 200) {
4786
5565
  const collapsed = s.replace(/\s+/g, " ").trim();
4787
5566
  return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
4788
5567
  }
@@ -4792,16 +5571,16 @@ function truncate4(s, max) {
4792
5571
 
4793
5572
  // src/cli/commands/setup.tsx
4794
5573
  import { render as render4 } from "ink";
4795
- import React16 from "react";
5574
+ import React17 from "react";
4796
5575
 
4797
5576
  // src/cli/ui/Wizard.tsx
4798
- import { Box as Box12, Text as Text12, useApp as useApp5, useInput as useInput5 } from "ink";
4799
- import TextInput3 from "ink-text-input";
4800
- import React15, { useState as useState8 } from "react";
5577
+ import { Box as Box13, Text as Text13, useApp as useApp5, useInput as useInput6 } from "ink";
5578
+ import TextInput2 from "ink-text-input";
5579
+ import React16, { useState as useState9 } from "react";
4801
5580
 
4802
5581
  // src/cli/ui/Select.tsx
4803
- import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
4804
- import React14, { useState as useState7 } from "react";
5582
+ import { Box as Box12, Text as Text12, useInput as useInput5 } from "ink";
5583
+ import React15, { useState as useState8 } from "react";
4805
5584
  function SingleSelect({
4806
5585
  items,
4807
5586
  initialValue,
@@ -4812,8 +5591,8 @@ function SingleSelect({
4812
5591
  0,
4813
5592
  items.findIndex((i) => i.value === initialValue && !i.disabled)
4814
5593
  );
4815
- const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
4816
- useInput4((_input, key) => {
5594
+ const [index, setIndex] = useState8(initialIndex === -1 ? 0 : initialIndex);
5595
+ useInput5((_input, key) => {
4817
5596
  if (key.upArrow) {
4818
5597
  setIndex((i) => findNextEnabled(items, i, -1));
4819
5598
  } else if (key.downArrow) {
@@ -4825,7 +5604,7 @@ function SingleSelect({
4825
5604
  onCancel();
4826
5605
  }
4827
5606
  });
4828
- return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React14.createElement(
5607
+ return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React15.createElement(
4829
5608
  SelectRow,
4830
5609
  {
4831
5610
  key: item.value,
@@ -4842,12 +5621,12 @@ function MultiSelect({
4842
5621
  onCancel,
4843
5622
  footer
4844
5623
  }) {
4845
- const [index, setIndex] = useState7(() => {
5624
+ const [index, setIndex] = useState8(() => {
4846
5625
  const first = items.findIndex((i) => !i.disabled);
4847
5626
  return first === -1 ? 0 : first;
4848
5627
  });
4849
- const [selected, setSelected] = useState7(new Set(initialSelected));
4850
- useInput4((input, key) => {
5628
+ const [selected, setSelected] = useState8(new Set(initialSelected));
5629
+ useInput5((input, key) => {
4851
5630
  if (key.upArrow) {
4852
5631
  setIndex((i) => findNextEnabled(items, i, -1));
4853
5632
  } else if (key.downArrow) {
@@ -4868,10 +5647,10 @@ function MultiSelect({
4868
5647
  onCancel();
4869
5648
  }
4870
5649
  });
4871
- return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => {
5650
+ return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, items.map((item, i) => {
4872
5651
  const checked = selected.has(item.value);
4873
5652
  const marker = checked ? "[x]" : "[ ]";
4874
- return /* @__PURE__ */ React14.createElement(
5653
+ return /* @__PURE__ */ React15.createElement(
4875
5654
  SelectRow,
4876
5655
  {
4877
5656
  key: item.value,
@@ -4880,7 +5659,7 @@ function MultiSelect({
4880
5659
  marker: `${i === index ? "\u25B8" : " "} ${marker}`
4881
5660
  }
4882
5661
  );
4883
- }), footer ? /* @__PURE__ */ React14.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, footer)) : null);
5662
+ }), footer ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, footer)) : null);
4884
5663
  }
4885
5664
  function SelectRow({
4886
5665
  item,
@@ -4888,7 +5667,7 @@ function SelectRow({
4888
5667
  marker
4889
5668
  }) {
4890
5669
  const color = item.disabled ? "gray" : active ? "cyan" : void 0;
4891
- return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React14.createElement(Box11, null, /* @__PURE__ */ React14.createElement(Text11, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React14.createElement(Box11, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, item.hint)) : null);
5670
+ return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React15.createElement(Box12, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, item.hint)) : null);
4892
5671
  }
4893
5672
  function findNextEnabled(items, from, step) {
4894
5673
  if (items.length === 0) return 0;
@@ -4925,19 +5704,19 @@ var PRESET_DESCRIPTIONS = {
4925
5704
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
4926
5705
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
4927
5706
  const { exit } = useApp5();
4928
- const [step, setStep] = useState8(existingApiKey ? "preset" : "apiKey");
4929
- const [data, setData] = useState8({
5707
+ const [step, setStep] = useState9(existingApiKey ? "preset" : "apiKey");
5708
+ const [data, setData] = useState9({
4930
5709
  apiKey: existingApiKey ?? "",
4931
5710
  preset: initial?.preset ?? "fast",
4932
5711
  selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
4933
5712
  catalogArgs: {}
4934
5713
  });
4935
- const [error, setError] = useState8(null);
4936
- useInput5((_input, key) => {
5714
+ const [error, setError] = useState9(null);
5715
+ useInput6((_input, key) => {
4937
5716
  if (key.escape && step !== "saved" && onCancel) onCancel();
4938
5717
  });
4939
5718
  if (step === "apiKey") {
4940
- return /* @__PURE__ */ React15.createElement(
5719
+ return /* @__PURE__ */ React16.createElement(
4941
5720
  ApiKeyStep,
4942
5721
  {
4943
5722
  onSubmit: (key) => {
@@ -4951,7 +5730,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
4951
5730
  );
4952
5731
  }
4953
5732
  if (step === "preset") {
4954
- return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React15.createElement(
5733
+ return /* @__PURE__ */ React16.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React16.createElement(
4955
5734
  SingleSelect,
4956
5735
  {
4957
5736
  items: presetItems(),
@@ -4961,10 +5740,10 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
4961
5740
  setStep("mcp");
4962
5741
  }
4963
5742
  }
4964
- ), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
5743
+ ), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
4965
5744
  }
4966
5745
  if (step === "mcp") {
4967
- return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React15.createElement(
5746
+ return /* @__PURE__ */ React16.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React16.createElement(
4968
5747
  MultiSelect,
4969
5748
  {
4970
5749
  items: mcpItems(),
@@ -4989,7 +5768,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
4989
5768
  }
4990
5769
  const currentName = pending[0];
4991
5770
  const entry = CATALOG_BY_NAME.get(currentName);
4992
- return /* @__PURE__ */ React15.createElement(
5771
+ return /* @__PURE__ */ React16.createElement(
4993
5772
  McpArgsStep,
4994
5773
  {
4995
5774
  entry,
@@ -5007,7 +5786,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
5007
5786
  }
5008
5787
  if (step === "review") {
5009
5788
  const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
5010
- return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React15.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React15.createElement(
5789
+ return /* @__PURE__ */ React16.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React16.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React16.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React16.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React16.createElement(
5011
5790
  SummaryLine,
5012
5791
  {
5013
5792
  label: "MCP",
@@ -5015,8 +5794,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
5015
5794
  }
5016
5795
  ), specs.map((spec, i) => (
5017
5796
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
5018
- /* @__PURE__ */ React15.createElement(Box12, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\xB7 ", spec))
5019
- )), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null, /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React15.createElement(
5797
+ /* @__PURE__ */ React16.createElement(Box13, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "\xB7 ", spec))
5798
+ )), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { color: "red" }, error)) : null, /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React16.createElement(
5020
5799
  ReviewConfirm,
5021
5800
  {
5022
5801
  onConfirm: () => {
@@ -5042,16 +5821,16 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
5042
5821
  }
5043
5822
  ));
5044
5823
  }
5045
- return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React15.createElement(ExitOnEnter, { onExit: exit }));
5824
+ return /* @__PURE__ */ React16.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React16.createElement(ExitOnEnter, { onExit: exit }));
5046
5825
  }
5047
5826
  function ApiKeyStep({
5048
5827
  onSubmit,
5049
5828
  error,
5050
5829
  onError
5051
5830
  }) {
5052
- const [value, setValue] = useState8("");
5053
- return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React15.createElement(
5054
- TextInput3,
5831
+ const [value, setValue] = useState9("");
5832
+ return /* @__PURE__ */ React16.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React16.createElement(
5833
+ TextInput2,
5055
5834
  {
5056
5835
  value,
5057
5836
  onChange: setValue,
@@ -5067,7 +5846,7 @@ function ApiKeyStep({
5067
5846
  mask: "\u2022",
5068
5847
  placeholder: "sk-..."
5069
5848
  }
5070
- )), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : value ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "preview: ", redactKey(value))) : null);
5849
+ )), error ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { color: "red" }, error)) : value ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "preview: ", redactKey(value))) : null);
5071
5850
  }
5072
5851
  function McpArgsStep({
5073
5852
  entry,
@@ -5075,9 +5854,9 @@ function McpArgsStep({
5075
5854
  onSubmit,
5076
5855
  onError
5077
5856
  }) {
5078
- const [value, setValue] = useState8("");
5079
- return /* @__PURE__ */ React15.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(Text12, null, entry.summary), entry.note ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Required parameter: "), /* @__PURE__ */ React15.createElement(Text12, { bold: true }, entry.userArgs)), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React15.createElement(
5080
- TextInput3,
5857
+ const [value, setValue] = useState9("");
5858
+ return /* @__PURE__ */ React16.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React16.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React16.createElement(Text13, null, entry.summary), entry.note ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, null, "Required parameter: "), /* @__PURE__ */ React16.createElement(Text13, { bold: true }, entry.userArgs)), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React16.createElement(
5859
+ TextInput2,
5081
5860
  {
5082
5861
  value,
5083
5862
  onChange: setValue,
@@ -5092,16 +5871,16 @@ function McpArgsStep({
5092
5871
  },
5093
5872
  placeholder: placeholderFor(entry)
5094
5873
  }
5095
- )), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null));
5874
+ )), error ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { color: "red" }, error)) : null));
5096
5875
  }
5097
5876
  function ReviewConfirm({ onConfirm }) {
5098
- useInput5((_i, key) => {
5877
+ useInput6((_i, key) => {
5099
5878
  if (key.return) onConfirm();
5100
5879
  });
5101
5880
  return null;
5102
5881
  }
5103
5882
  function ExitOnEnter({ onExit }) {
5104
- useInput5((_i, key) => {
5883
+ useInput6((_i, key) => {
5105
5884
  if (key.return) onExit();
5106
5885
  });
5107
5886
  return null;
@@ -5112,10 +5891,10 @@ function StepFrame({
5112
5891
  total,
5113
5892
  children
5114
5893
  }) {
5115
- return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1, flexDirection: "column" }, children));
5894
+ return /* @__PURE__ */ React16.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React16.createElement(Box13, null, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1, flexDirection: "column" }, children));
5116
5895
  }
5117
5896
  function SummaryLine({ label, value }) {
5118
- return /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, null, label.padEnd(12)), /* @__PURE__ */ React15.createElement(Text12, { bold: true }, value));
5897
+ return /* @__PURE__ */ React16.createElement(Box13, null, /* @__PURE__ */ React16.createElement(Text13, null, label.padEnd(12)), /* @__PURE__ */ React16.createElement(Text13, { bold: true }, value));
5119
5898
  }
5120
5899
  function presetItems() {
5121
5900
  return ["fast", "smart", "max"].map((name) => ({
@@ -5158,10 +5937,10 @@ function buildSpec(name, argsByName) {
5158
5937
  const entry = CATALOG_BY_NAME.get(name);
5159
5938
  if (!entry) return name;
5160
5939
  const userArg = entry.userArgs ? argsByName[name] : void 0;
5161
- const tail = userArg ? ` ${quoteIfNeeded2(userArg)}` : "";
5940
+ const tail = userArg ? ` ${quoteIfNeeded(userArg)}` : "";
5162
5941
  return `${entry.name}=npx -y ${entry.package}${tail}`;
5163
5942
  }
5164
- function quoteIfNeeded2(s) {
5943
+ function quoteIfNeeded(s) {
5165
5944
  return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
5166
5945
  }
5167
5946
 
@@ -5171,7 +5950,7 @@ async function setupCommand(_opts = {}) {
5171
5950
  const existingKey = loadApiKey();
5172
5951
  const existing = readConfig();
5173
5952
  const { waitUntilExit, unmount } = render4(
5174
- /* @__PURE__ */ React16.createElement(
5953
+ /* @__PURE__ */ React17.createElement(
5175
5954
  Wizard,
5176
5955
  {
5177
5956
  existingApiKey: existingKey,
@@ -5390,6 +6169,17 @@ var mcp = program.command("mcp").description("Model Context Protocol helpers \u2
5390
6169
  mcp.command("list").description("Show a curated catalog of popular MCP servers with ready-to-use --mcp commands.").option("--json", "Emit the catalog as JSON instead of the human-readable table").action((opts) => {
5391
6170
  mcpListCommand({ json: !!opts.json });
5392
6171
  });
6172
+ mcp.command("inspect <spec>").description(
6173
+ 'Connect to one MCP server and print its server info + tools/resources/prompts. <spec> takes the same forms as --mcp ("name=cmd args", "cmd args", or an SSE URL).'
6174
+ ).option("--json", "Emit the full inspection report as JSON instead of the human-readable summary").action(async (spec, opts) => {
6175
+ try {
6176
+ await mcpInspectCommand({ spec, json: !!opts.json });
6177
+ } catch (err) {
6178
+ process.stderr.write(`mcp inspect failed: ${err.message}
6179
+ `);
6180
+ process.exit(1);
6181
+ }
6182
+ });
5393
6183
  program.command("version").description("Print Reasonix version.").action(versionCommand);
5394
6184
  program.parseAsync(process.argv).catch((err) => {
5395
6185
  console.error(err);