reasonix 0.4.6 → 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
  }
@@ -2279,6 +2584,13 @@ var McpClient = class {
2279
2584
  _serverInfo = { name: "", version: "" };
2280
2585
  _protocolVersion = "";
2281
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;
2282
2594
  constructor(opts) {
2283
2595
  this.transport = opts.transport;
2284
2596
  this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
@@ -2333,13 +2645,36 @@ var McpClient = class {
2333
2645
  this.assertInitialized();
2334
2646
  return this.request("tools/list", {});
2335
2647
  }
2336
- /** Invoke a tool by name. Returns the raw MCP result (caller unwraps content). */
2337
- 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 = {}) {
2338
2665
  this.assertInitialized();
2339
- return this.request("tools/call", {
2340
- name,
2341
- arguments: args ?? {}
2342
- });
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
+ }
2343
2678
  }
2344
2679
  /**
2345
2680
  * List resources the server exposes. Supports a pagination cursor;
@@ -2393,24 +2728,56 @@ var McpClient = class {
2393
2728
  assertInitialized() {
2394
2729
  if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
2395
2730
  }
2396
- async request(method, params) {
2731
+ async request(method, params, signal) {
2397
2732
  const id = this.nextId++;
2398
2733
  const frame = { jsonrpc: "2.0", id, method, params };
2399
- const promise = new Promise((resolve4, reject) => {
2734
+ let abortHandler = null;
2735
+ const promise = new Promise((resolve5, reject) => {
2400
2736
  const timeout = setTimeout(() => {
2401
2737
  this.pending.delete(id);
2738
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
2402
2739
  reject(
2403
2740
  new Error(`MCP request ${method} (id=${id}) timed out after ${this.requestTimeoutMs}ms`)
2404
2741
  );
2405
2742
  }, this.requestTimeoutMs);
2406
2743
  this.pending.set(id, {
2407
- resolve: resolve4,
2744
+ resolve: resolve5,
2408
2745
  reject,
2409
2746
  timeout
2410
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
+ }
2411
2768
  });
2412
- await this.transport.send(frame);
2413
- 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
+ }
2414
2781
  }
2415
2782
  startReaderIfNeeded() {
2416
2783
  if (this.readerStarted) return;
@@ -2431,7 +2798,16 @@ var McpClient = class {
2431
2798
  }
2432
2799
  }
2433
2800
  dispatch(msg) {
2434
- 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
+ }
2435
2811
  if (!("result" in msg) && !("error" in msg)) return;
2436
2812
  const pending = this.pending.get(msg.id);
2437
2813
  if (!pending) return;
@@ -2488,12 +2864,12 @@ var StdioTransport = class {
2488
2864
  }
2489
2865
  async send(message) {
2490
2866
  if (this.closed) throw new Error("MCP transport is closed");
2491
- return new Promise((resolve4, reject) => {
2867
+ return new Promise((resolve5, reject) => {
2492
2868
  const line = `${JSON.stringify(message)}
2493
2869
  `;
2494
2870
  this.child.stdin.write(line, "utf8", (err) => {
2495
2871
  if (err) reject(err);
2496
- else resolve4();
2872
+ else resolve5();
2497
2873
  });
2498
2874
  });
2499
2875
  }
@@ -2504,8 +2880,8 @@ var StdioTransport = class {
2504
2880
  continue;
2505
2881
  }
2506
2882
  if (this.closed) return;
2507
- const next = await new Promise((resolve4) => {
2508
- this.waiters.push(resolve4);
2883
+ const next = await new Promise((resolve5) => {
2884
+ this.waiters.push(resolve5);
2509
2885
  });
2510
2886
  if (next === null) return;
2511
2887
  yield next;
@@ -2571,8 +2947,8 @@ var SseTransport = class {
2571
2947
  constructor(opts) {
2572
2948
  this.url = opts.url;
2573
2949
  this.headers = opts.headers ?? {};
2574
- this.endpointReady = new Promise((resolve4, reject) => {
2575
- this.resolveEndpoint = resolve4;
2950
+ this.endpointReady = new Promise((resolve5, reject) => {
2951
+ this.resolveEndpoint = resolve5;
2576
2952
  this.rejectEndpoint = reject;
2577
2953
  });
2578
2954
  this.endpointReady.catch(() => void 0);
@@ -2599,8 +2975,8 @@ var SseTransport = class {
2599
2975
  continue;
2600
2976
  }
2601
2977
  if (this.closed) return;
2602
- const next = await new Promise((resolve4) => {
2603
- this.waiters.push(resolve4);
2978
+ const next = await new Promise((resolve5) => {
2979
+ this.waiters.push(resolve5);
2604
2980
  });
2605
2981
  if (next === null) return;
2606
2982
  yield next;
@@ -2800,7 +3176,7 @@ async function trySection(load) {
2800
3176
 
2801
3177
  // src/code/edit-blocks.ts
2802
3178
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
2803
- import { dirname as dirname3, resolve as resolve2 } from "path";
3179
+ import { dirname as dirname4, resolve as resolve3 } from "path";
2804
3180
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
2805
3181
  function parseEditBlocks(text) {
2806
3182
  const out = [];
@@ -2818,8 +3194,8 @@ function parseEditBlocks(text) {
2818
3194
  return out;
2819
3195
  }
2820
3196
  function applyEditBlock(block, rootDir) {
2821
- const absRoot = resolve2(rootDir);
2822
- const absTarget = resolve2(absRoot, block.path);
3197
+ const absRoot = resolve3(rootDir);
3198
+ const absTarget = resolve3(absRoot, block.path);
2823
3199
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
2824
3200
  return {
2825
3201
  path: block.path,
@@ -2838,7 +3214,7 @@ function applyEditBlock(block, rootDir) {
2838
3214
  message: "file does not exist; to create it, use an empty SEARCH block"
2839
3215
  };
2840
3216
  }
2841
- mkdirSync3(dirname3(absTarget), { recursive: true });
3217
+ mkdirSync3(dirname4(absTarget), { recursive: true });
2842
3218
  writeFileSync3(absTarget, block.replace, "utf8");
2843
3219
  return { path: block.path, status: "created" };
2844
3220
  }
@@ -2869,13 +3245,13 @@ function applyEditBlocks(blocks, rootDir) {
2869
3245
  return blocks.map((b) => applyEditBlock(b, rootDir));
2870
3246
  }
2871
3247
  function snapshotBeforeEdits(blocks, rootDir) {
2872
- const absRoot = resolve2(rootDir);
3248
+ const absRoot = resolve3(rootDir);
2873
3249
  const seen = /* @__PURE__ */ new Set();
2874
3250
  const snapshots = [];
2875
3251
  for (const b of blocks) {
2876
3252
  if (seen.has(b.path)) continue;
2877
3253
  seen.add(b.path);
2878
- const abs = resolve2(absRoot, b.path);
3254
+ const abs = resolve3(absRoot, b.path);
2879
3255
  if (!existsSync2(abs)) {
2880
3256
  snapshots.push({ path: b.path, prevContent: null });
2881
3257
  continue;
@@ -2889,9 +3265,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
2889
3265
  return snapshots;
2890
3266
  }
2891
3267
  function restoreSnapshots(snapshots, rootDir) {
2892
- const absRoot = resolve2(rootDir);
3268
+ const absRoot = resolve3(rootDir);
2893
3269
  return snapshots.map((snap) => {
2894
- const abs = resolve2(absRoot, snap.path);
3270
+ const abs = resolve3(absRoot, snap.path);
2895
3271
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
2896
3272
  return {
2897
3273
  path: snap.path,
@@ -2928,11 +3304,11 @@ var VERSION = "0.4.3";
2928
3304
 
2929
3305
  // src/cli/commands/chat.tsx
2930
3306
  import { render } from "ink";
2931
- import React9, { useState as useState4 } from "react";
3307
+ import React9, { useState as useState5 } from "react";
2932
3308
 
2933
3309
  // src/cli/ui/App.tsx
2934
- import { Box as Box7, Static, Text as Text7, useApp, useInput } from "ink";
2935
- import React7, { 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";
2936
3312
 
2937
3313
  // src/cli/ui/EventLog.tsx
2938
3314
  import { Box as Box3, Text as Text3 } from "ink";
@@ -3271,9 +3647,44 @@ function truncate2(s, max) {
3271
3647
  }
3272
3648
 
3273
3649
  // src/cli/ui/PromptInput.tsx
3274
- import { Box as Box4, Text as Text4 } from "ink";
3275
- import TextInput from "ink-text-input";
3276
- 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
3277
3688
  function PromptInput({
3278
3689
  value,
3279
3690
  onChange,
@@ -3281,17 +3692,53 @@ function PromptInput({
3281
3692
  disabled,
3282
3693
  placeholder
3283
3694
  }) {
3284
- const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
3285
- 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(
3286
- TextInput,
3287
- {
3288
- value,
3289
- onChange,
3290
- onSubmit,
3291
- focus: !disabled,
3292
- placeholder: effectivePlaceholder
3695
+ const [showCursor, setShowCursor] = useState2(true);
3696
+ useEffect2(() => {
3697
+ if (disabled) {
3698
+ setShowCursor(false);
3699
+ return;
3293
3700
  }
3294
- ));
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
+ }));
3295
3742
  }
3296
3743
 
3297
3744
  // src/cli/ui/SlashSuggestions.tsx
@@ -3785,22 +4232,24 @@ function App({
3785
4232
  tools,
3786
4233
  mcpSpecs,
3787
4234
  mcpServers,
4235
+ progressSink,
3788
4236
  codeMode
3789
4237
  }) {
3790
4238
  const { exit } = useApp();
3791
- const [historical, setHistorical] = useState2([]);
3792
- const [streaming, setStreaming] = useState2(null);
3793
- const [input, setInput] = useState2("");
3794
- 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);
3795
4243
  const abortedThisTurn = useRef(false);
3796
- const [ongoingTool, setOngoingTool] = useState2(null);
4244
+ const [ongoingTool, setOngoingTool] = useState3(null);
4245
+ const [toolProgress, setToolProgress] = useState3(null);
3797
4246
  const lastEditSnapshots = useRef(null);
3798
4247
  const pendingEdits = useRef([]);
3799
4248
  const promptHistory = useRef([]);
3800
4249
  const historyCursor = useRef(-1);
3801
4250
  const toolHistoryRef = useRef([]);
3802
- const [slashSelected, setSlashSelected] = useState2(0);
3803
- const [summary, setSummary] = useState2({
4251
+ const [slashSelected, setSlashSelected] = useState3(0);
4252
+ const [summary, setSummary] = useState3({
3804
4253
  turns: 0,
3805
4254
  totalCostUsd: 0,
3806
4255
  claudeEquivalentUsd: 0,
@@ -3817,7 +4266,7 @@ function App({
3817
4266
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
3818
4267
  });
3819
4268
  }
3820
- useEffect2(() => {
4269
+ useEffect3(() => {
3821
4270
  return () => {
3822
4271
  transcriptRef.current?.end();
3823
4272
  };
@@ -3826,7 +4275,7 @@ function App({
3826
4275
  if (!input.startsWith("/") || input.includes(" ")) return null;
3827
4276
  return suggestSlashCommands(input.slice(1), !!codeMode);
3828
4277
  }, [input, codeMode]);
3829
- useEffect2(() => {
4278
+ useEffect3(() => {
3830
4279
  setSlashSelected((prev) => {
3831
4280
  if (!slashMatches || slashMatches.length === 0) return 0;
3832
4281
  if (prev >= slashMatches.length) return slashMatches.length - 1;
@@ -3845,8 +4294,21 @@ function App({
3845
4294
  loopRef.current = l;
3846
4295
  return l;
3847
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]);
3848
4310
  const sessionBannerShown = useRef(false);
3849
- useEffect2(() => {
4311
+ useEffect3(() => {
3850
4312
  if (sessionBannerShown.current) return;
3851
4313
  sessionBannerShown.current = true;
3852
4314
  if (!session) {
@@ -3878,7 +4340,7 @@ function App({
3878
4340
  ]);
3879
4341
  }
3880
4342
  }, [session, loop]);
3881
- useInput((_input, key) => {
4343
+ useInput2((_input, key) => {
3882
4344
  if (key.escape && busy) {
3883
4345
  if (abortedThisTurn.current) return;
3884
4346
  abortedThisTurn.current = true;
@@ -4096,9 +4558,11 @@ function App({
4096
4558
  }
4097
4559
  } else if (ev.role === "tool_start") {
4098
4560
  setOngoingTool({ name: ev.toolName ?? "?", args: ev.toolArgs });
4561
+ setToolProgress(null);
4099
4562
  } else if (ev.role === "tool") {
4100
4563
  flush();
4101
4564
  setOngoingTool(null);
4565
+ setToolProgress(null);
4102
4566
  toolHistoryRef.current.push({
4103
4567
  toolName: ev.toolName ?? "?",
4104
4568
  text: ev.content
@@ -4129,6 +4593,7 @@ function App({
4129
4593
  clearInterval(timer);
4130
4594
  setStreaming(null);
4131
4595
  setOngoingTool(null);
4596
+ setToolProgress(null);
4132
4597
  setSummary(loop.stats.summary());
4133
4598
  setBusy(false);
4134
4599
  }
@@ -4156,12 +4621,15 @@ function App({
4156
4621
  harvestOn: loop.harvestEnabled,
4157
4622
  branchBudget: loop.branchOptions.budget
4158
4623
  }
4159
- ), /* @__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 }) : null, /* @__PURE__ */ React7.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React7.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }));
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 }));
4160
4625
  }
4161
- function OngoingToolRow({ tool }) {
4162
- const [tick, setTick] = useState2(0);
4163
- const [elapsed, setElapsed] = useState2(0);
4164
- useEffect2(() => {
4626
+ function OngoingToolRow({
4627
+ tool,
4628
+ progress
4629
+ }) {
4630
+ const [tick, setTick] = useState3(0);
4631
+ const [elapsed, setElapsed] = useState3(0);
4632
+ useEffect3(() => {
4165
4633
  const start = Date.now();
4166
4634
  const frameId = setInterval(() => setTick((t) => t + 1), 120);
4167
4635
  const secId = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1e3)), 1e3);
@@ -4172,7 +4640,19 @@ function OngoingToolRow({ tool }) {
4172
4640
  }, []);
4173
4641
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4174
4642
  const summary = summarizeToolArgs(tool.name, tool.args);
4175
- 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`)), summary ? /* @__PURE__ */ React7.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text7, { 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}`;
4176
4656
  }
4177
4657
  function summarizeToolArgs(name, args) {
4178
4658
  if (!args || args === "{}") return "";
@@ -4257,11 +4737,11 @@ function describeRepair(repair) {
4257
4737
 
4258
4738
  // src/cli/ui/Setup.tsx
4259
4739
  import { Box as Box8, Text as Text8, useApp as useApp2 } from "ink";
4260
- import TextInput2 from "ink-text-input";
4261
- import React8, { useState as useState3 } from "react";
4740
+ import TextInput from "ink-text-input";
4741
+ import React8, { useState as useState4 } from "react";
4262
4742
  function Setup({ onReady }) {
4263
- const [value, setValue] = useState3("");
4264
- const [error, setError] = useState3(null);
4743
+ const [value, setValue] = useState4("");
4744
+ const [error, setError] = useState4(null);
4265
4745
  const { exit } = useApp2();
4266
4746
  const handleSubmit = (raw) => {
4267
4747
  const trimmed = raw.trim();
@@ -4283,7 +4763,7 @@ function Setup({ onReady }) {
4283
4763
  onReady(trimmed);
4284
4764
  };
4285
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(
4286
- TextInput2,
4766
+ TextInput,
4287
4767
  {
4288
4768
  value,
4289
4769
  onChange: setValue,
@@ -4295,8 +4775,8 @@ function Setup({ onReady }) {
4295
4775
  }
4296
4776
 
4297
4777
  // src/cli/commands/chat.tsx
4298
- function Root({ initialKey, tools, mcpSpecs, mcpServers, ...appProps }) {
4299
- const [key, setKey] = useState4(initialKey);
4778
+ function Root({ initialKey, tools, mcpSpecs, mcpServers, progressSink, ...appProps }) {
4779
+ const [key, setKey] = useState5(initialKey);
4300
4780
  if (!key) {
4301
4781
  return /* @__PURE__ */ React9.createElement(
4302
4782
  Setup,
@@ -4321,6 +4801,7 @@ function Root({ initialKey, tools, mcpSpecs, mcpServers, ...appProps }) {
4321
4801
  tools,
4322
4802
  mcpSpecs,
4323
4803
  mcpServers,
4804
+ progressSink,
4324
4805
  codeMode: appProps.codeMode
4325
4806
  }
4326
4807
  );
@@ -4333,9 +4814,10 @@ async function chatCommand(opts) {
4333
4814
  const successfulSpecs = [];
4334
4815
  const failedSpecs = [];
4335
4816
  const mcpServers = [];
4336
- let tools;
4817
+ const progressSink = { current: null };
4818
+ let tools = opts.seedTools;
4337
4819
  if (requestedSpecs.length > 0) {
4338
- tools = new ToolRegistry();
4820
+ if (!tools) tools = new ToolRegistry();
4339
4821
  for (const raw of requestedSpecs) {
4340
4822
  try {
4341
4823
  const spec = parseMcpSpec(raw);
@@ -4343,7 +4825,11 @@ async function chatCommand(opts) {
4343
4825
  const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
4344
4826
  const mcp2 = new McpClient({ transport });
4345
4827
  await mcp2.initialize();
4346
- 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
+ });
4347
4833
  let report;
4348
4834
  try {
4349
4835
  report = await inspectMcpServer(mcp2);
@@ -4381,7 +4867,7 @@ async function chatCommand(opts) {
4381
4867
  );
4382
4868
  }
4383
4869
  }
4384
- if (successfulSpecs.length === 0) {
4870
+ if (successfulSpecs.length === 0 && !opts.seedTools) {
4385
4871
  tools = void 0;
4386
4872
  }
4387
4873
  }
@@ -4394,6 +4880,7 @@ async function chatCommand(opts) {
4394
4880
  tools,
4395
4881
  mcpSpecs,
4396
4882
  mcpServers,
4883
+ progressSink,
4397
4884
  ...opts
4398
4885
  }
4399
4886
  ),
@@ -4407,14 +4894,15 @@ async function chatCommand(opts) {
4407
4894
  }
4408
4895
 
4409
4896
  // src/cli/commands/code.tsx
4410
- import { basename, resolve as resolve3 } from "path";
4897
+ import { basename, resolve as resolve4 } from "path";
4411
4898
  async function codeCommand(opts = {}) {
4412
4899
  const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-MMANQ36Z.js");
4413
- const rootDir = resolve3(opts.dir ?? process.cwd());
4900
+ const rootDir = resolve4(opts.dir ?? process.cwd());
4414
4901
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
4415
- const fsSpec = `filesystem=npx -y @modelcontextprotocol/server-filesystem ${quoteIfNeeded(rootDir)}`;
4902
+ const tools = new ToolRegistry();
4903
+ registerFilesystemTools(tools, { rootDir });
4416
4904
  process.stderr.write(
4417
- `\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)
4418
4906
  `
4419
4907
  );
4420
4908
  await chatCommand({
@@ -4424,13 +4912,10 @@ async function codeCommand(opts = {}) {
4424
4912
  system: codeSystemPrompt2(rootDir),
4425
4913
  transcript: opts.transcript,
4426
4914
  session,
4427
- mcp: [fsSpec],
4915
+ seedTools: tools,
4428
4916
  codeMode: { rootDir }
4429
4917
  });
4430
4918
  }
4431
- function quoteIfNeeded(s) {
4432
- return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
4433
- }
4434
4919
 
4435
4920
  // src/cli/commands/diff.ts
4436
4921
  import { writeFileSync as writeFileSync4 } from "fs";
@@ -4439,8 +4924,8 @@ import { render as render2 } from "ink";
4439
4924
  import React12 from "react";
4440
4925
 
4441
4926
  // src/cli/ui/DiffApp.tsx
4442
- import { Box as Box10, Static as Static2, Text as Text10, useApp as useApp3, useInput as useInput2 } from "ink";
4443
- import React11, { 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";
4444
4929
 
4445
4930
  // src/cli/ui/RecordView.tsx
4446
4931
  import { Box as Box9, Text as Text9 } from "ink";
@@ -4483,8 +4968,8 @@ function DiffApp({ report }) {
4483
4968
  const { exit } = useApp3();
4484
4969
  const maxIdx = Math.max(0, report.pairs.length - 1);
4485
4970
  const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
4486
- const [idx, setIdx] = useState5(Math.max(0, initialIdx));
4487
- useInput2((input, key) => {
4971
+ const [idx, setIdx] = useState6(Math.max(0, initialIdx));
4972
+ useInput3((input, key) => {
4488
4973
  if (input === "q" || key.ctrl && input === "c") {
4489
4974
  exit();
4490
4975
  return;
@@ -4728,13 +5213,13 @@ import { render as render3 } from "ink";
4728
5213
  import React14 from "react";
4729
5214
 
4730
5215
  // src/cli/ui/ReplayApp.tsx
4731
- import { Box as Box11, Static as Static3, Text as Text11, useApp as useApp4, useInput as useInput3 } from "ink";
4732
- import React13, { 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";
4733
5218
  function ReplayApp({ meta, pages }) {
4734
5219
  const { exit } = useApp4();
4735
5220
  const maxIdx = Math.max(0, pages.length - 1);
4736
- const [idx, setIdx] = useState6(maxIdx);
4737
- useInput3((input, key) => {
5221
+ const [idx, setIdx] = useState7(maxIdx);
5222
+ useInput4((input, key) => {
4738
5223
  if (input === "q" || key.ctrl && input === "c") {
4739
5224
  exit();
4740
5225
  return;
@@ -5089,13 +5574,13 @@ import { render as render4 } from "ink";
5089
5574
  import React17 from "react";
5090
5575
 
5091
5576
  // src/cli/ui/Wizard.tsx
5092
- import { Box as Box13, Text as Text13, useApp as useApp5, useInput as useInput5 } from "ink";
5093
- import TextInput3 from "ink-text-input";
5094
- import React16, { 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";
5095
5580
 
5096
5581
  // src/cli/ui/Select.tsx
5097
- import { Box as Box12, Text as Text12, useInput as useInput4 } from "ink";
5098
- import React15, { 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";
5099
5584
  function SingleSelect({
5100
5585
  items,
5101
5586
  initialValue,
@@ -5106,8 +5591,8 @@ function SingleSelect({
5106
5591
  0,
5107
5592
  items.findIndex((i) => i.value === initialValue && !i.disabled)
5108
5593
  );
5109
- const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
5110
- useInput4((_input, key) => {
5594
+ const [index, setIndex] = useState8(initialIndex === -1 ? 0 : initialIndex);
5595
+ useInput5((_input, key) => {
5111
5596
  if (key.upArrow) {
5112
5597
  setIndex((i) => findNextEnabled(items, i, -1));
5113
5598
  } else if (key.downArrow) {
@@ -5136,12 +5621,12 @@ function MultiSelect({
5136
5621
  onCancel,
5137
5622
  footer
5138
5623
  }) {
5139
- const [index, setIndex] = useState7(() => {
5624
+ const [index, setIndex] = useState8(() => {
5140
5625
  const first = items.findIndex((i) => !i.disabled);
5141
5626
  return first === -1 ? 0 : first;
5142
5627
  });
5143
- const [selected, setSelected] = useState7(new Set(initialSelected));
5144
- useInput4((input, key) => {
5628
+ const [selected, setSelected] = useState8(new Set(initialSelected));
5629
+ useInput5((input, key) => {
5145
5630
  if (key.upArrow) {
5146
5631
  setIndex((i) => findNextEnabled(items, i, -1));
5147
5632
  } else if (key.downArrow) {
@@ -5219,15 +5704,15 @@ var PRESET_DESCRIPTIONS = {
5219
5704
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
5220
5705
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
5221
5706
  const { exit } = useApp5();
5222
- const [step, setStep] = useState8(existingApiKey ? "preset" : "apiKey");
5223
- const [data, setData] = useState8({
5707
+ const [step, setStep] = useState9(existingApiKey ? "preset" : "apiKey");
5708
+ const [data, setData] = useState9({
5224
5709
  apiKey: existingApiKey ?? "",
5225
5710
  preset: initial?.preset ?? "fast",
5226
5711
  selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
5227
5712
  catalogArgs: {}
5228
5713
  });
5229
- const [error, setError] = useState8(null);
5230
- useInput5((_input, key) => {
5714
+ const [error, setError] = useState9(null);
5715
+ useInput6((_input, key) => {
5231
5716
  if (key.escape && step !== "saved" && onCancel) onCancel();
5232
5717
  });
5233
5718
  if (step === "apiKey") {
@@ -5343,9 +5828,9 @@ function ApiKeyStep({
5343
5828
  error,
5344
5829
  onError
5345
5830
  }) {
5346
- const [value, setValue] = useState8("");
5831
+ const [value, setValue] = useState9("");
5347
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(
5348
- TextInput3,
5833
+ TextInput2,
5349
5834
  {
5350
5835
  value,
5351
5836
  onChange: setValue,
@@ -5369,9 +5854,9 @@ function McpArgsStep({
5369
5854
  onSubmit,
5370
5855
  onError
5371
5856
  }) {
5372
- const [value, setValue] = useState8("");
5857
+ const [value, setValue] = useState9("");
5373
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(
5374
- TextInput3,
5859
+ TextInput2,
5375
5860
  {
5376
5861
  value,
5377
5862
  onChange: setValue,
@@ -5389,13 +5874,13 @@ function McpArgsStep({
5389
5874
  )), error ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { color: "red" }, error)) : null));
5390
5875
  }
5391
5876
  function ReviewConfirm({ onConfirm }) {
5392
- useInput5((_i, key) => {
5877
+ useInput6((_i, key) => {
5393
5878
  if (key.return) onConfirm();
5394
5879
  });
5395
5880
  return null;
5396
5881
  }
5397
5882
  function ExitOnEnter({ onExit }) {
5398
- useInput5((_i, key) => {
5883
+ useInput6((_i, key) => {
5399
5884
  if (key.return) onExit();
5400
5885
  });
5401
5886
  return null;
@@ -5452,10 +5937,10 @@ function buildSpec(name, argsByName) {
5452
5937
  const entry = CATALOG_BY_NAME.get(name);
5453
5938
  if (!entry) return name;
5454
5939
  const userArg = entry.userArgs ? argsByName[name] : void 0;
5455
- const tail = userArg ? ` ${quoteIfNeeded2(userArg)}` : "";
5940
+ const tail = userArg ? ` ${quoteIfNeeded(userArg)}` : "";
5456
5941
  return `${entry.name}=npx -y ${entry.package}${tail}`;
5457
5942
  }
5458
- function quoteIfNeeded2(s) {
5943
+ function quoteIfNeeded(s) {
5459
5944
  return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
5460
5945
  }
5461
5946