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/index.js CHANGED
@@ -47,8 +47,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
47
47
  }
48
48
  function sleep(ms, signal) {
49
49
  if (ms <= 0) return Promise.resolve();
50
- return new Promise((resolve3, reject) => {
51
- const timer = setTimeout(resolve3, ms);
50
+ return new Promise((resolve4, reject) => {
51
+ const timer = setTimeout(resolve4, ms);
52
52
  if (signal) {
53
53
  const onAbort = () => {
54
54
  clearTimeout(timer);
@@ -514,7 +514,7 @@ var ToolRegistry = class {
514
514
  }
515
515
  }));
516
516
  }
517
- async dispatch(name, argumentsRaw) {
517
+ async dispatch(name, argumentsRaw, opts = {}) {
518
518
  const tool = this._tools.get(name);
519
519
  if (!tool) {
520
520
  return JSON.stringify({ error: `unknown tool: ${name}` });
@@ -531,7 +531,7 @@ var ToolRegistry = class {
531
531
  args = nestArguments(args);
532
532
  }
533
533
  try {
534
- const result = await tool.fn(args);
534
+ const result = await tool.fn(args, { signal: opts.signal });
535
535
  return typeof result === "string" ? result : JSON.stringify(result);
536
536
  } catch (err) {
537
537
  return JSON.stringify({
@@ -565,8 +565,20 @@ async function bridgeMcpTools(client, opts = {}) {
565
565
  name: registeredName,
566
566
  description: mcpTool.description ?? "",
567
567
  parameters: mcpTool.inputSchema,
568
- fn: async (args) => {
569
- const toolResult = await client.callTool(mcpTool.name, args);
568
+ fn: async (args, ctx) => {
569
+ const toolResult = await client.callTool(mcpTool.name, args, {
570
+ // Forward server-side progress frames to the bridge caller,
571
+ // tagged with the registered name so multi-server UIs can
572
+ // disambiguate. No-op when `onProgress` isn't configured —
573
+ // the client then also omits the _meta.progressToken and
574
+ // the server won't emit progress.
575
+ onProgress: opts.onProgress ? (info) => opts.onProgress({ toolName: registeredName, ...info }) : void 0,
576
+ // Thread the tool-dispatch AbortSignal all the way down to
577
+ // the MCP request so Esc truly cancels in flight — the
578
+ // client will emit notifications/cancelled AND reject the
579
+ // pending promise immediately, no "wait for subprocess".
580
+ signal: ctx?.signal
581
+ });
570
582
  return flattenMcpResult(toolResult, { maxChars: maxResultChars });
571
583
  }
572
584
  });
@@ -1155,11 +1167,13 @@ var CacheFirstLoop = class {
1155
1167
  _turn = 0;
1156
1168
  _streamPreference;
1157
1169
  /**
1158
- * Set by {@link abort} to short-circuit the tool-call loop after the
1159
- * current iteration. Reset at the start of each `step()` so an Esc
1160
- * during one turn doesn't poison the next.
1170
+ * AbortController per active turn. Threaded through the DeepSeek
1171
+ * HTTP calls AND every tool dispatch so Esc actually cancels the
1172
+ * in-flight network/subprocess work — not "we'll get to it after
1173
+ * the current call finishes." Re-created at the start of each
1174
+ * `step()` (the prior turn's signal has already fired).
1161
1175
  */
1162
- _aborted = false;
1176
+ _turnAbort = new AbortController();
1163
1177
  constructor(opts) {
1164
1178
  this.client = opts.client;
1165
1179
  this.prefix = opts.prefix;
@@ -1272,14 +1286,15 @@ var CacheFirstLoop = class {
1272
1286
  return msgs;
1273
1287
  }
1274
1288
  /**
1275
- * Signal the currently-running {@link step} that the user wants to
1276
- * stop exploring. Takes effect at the next iteration boundary — if a
1277
- * tool call is mid-flight it will be allowed to finish, then the
1278
- * loop diverts to the forced-summary path so the user gets an
1279
- * answer instead of a cliff. Called by the TUI on Esc.
1289
+ * Signal the currently-running {@link step} to stop **now**. Cancels
1290
+ * the in-flight network request (DeepSeek HTTP/SSE) AND any tool call
1291
+ * currently dispatching (MCP `notifications/cancelled` + promise
1292
+ * reject). The loop itself also sees `signal.aborted` at each
1293
+ * iteration boundary and exits quickly instead of looping again.
1294
+ * Called by the TUI on Esc.
1280
1295
  */
1281
1296
  abort() {
1282
- this._aborted = true;
1297
+ this._turnAbort.abort();
1283
1298
  }
1284
1299
  /**
1285
1300
  * Drop everything in the log after (and including) the most recent
@@ -1317,13 +1332,14 @@ var CacheFirstLoop = class {
1317
1332
  async *step(userInput) {
1318
1333
  this._turn++;
1319
1334
  this.scratch.reset();
1320
- this._aborted = false;
1335
+ this._turnAbort = new AbortController();
1336
+ const signal = this._turnAbort.signal;
1321
1337
  let pendingUser = userInput;
1322
1338
  const toolSpecs = this.prefix.tools();
1323
1339
  const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
1324
1340
  let warnedForIterBudget = false;
1325
1341
  for (let iter = 0; iter < this.maxToolIters; iter++) {
1326
- if (this._aborted) {
1342
+ if (signal.aborted) {
1327
1343
  yield {
1328
1344
  turn: this._turn,
1329
1345
  role: "warning",
@@ -1386,7 +1402,8 @@ var CacheFirstLoop = class {
1386
1402
  {
1387
1403
  model: this.model,
1388
1404
  messages,
1389
- tools: toolSpecs.length ? toolSpecs : void 0
1405
+ tools: toolSpecs.length ? toolSpecs : void 0,
1406
+ signal
1390
1407
  },
1391
1408
  {
1392
1409
  ...this.branchOptions,
@@ -1395,8 +1412,8 @@ var CacheFirstLoop = class {
1395
1412
  }
1396
1413
  );
1397
1414
  for (let k = 0; k < budget; k++) {
1398
- const sample = queue.shift() ?? await new Promise((resolve3) => {
1399
- waiter = resolve3;
1415
+ const sample = queue.shift() ?? await new Promise((resolve4) => {
1416
+ waiter = resolve4;
1400
1417
  });
1401
1418
  yield {
1402
1419
  turn: this._turn,
@@ -1436,7 +1453,8 @@ var CacheFirstLoop = class {
1436
1453
  for await (const chunk of this.client.stream({
1437
1454
  model: this.model,
1438
1455
  messages,
1439
- tools: toolSpecs.length ? toolSpecs : void 0
1456
+ tools: toolSpecs.length ? toolSpecs : void 0,
1457
+ signal
1440
1458
  })) {
1441
1459
  if (chunk.contentDelta) {
1442
1460
  assistantContent += chunk.contentDelta;
@@ -1475,7 +1493,8 @@ var CacheFirstLoop = class {
1475
1493
  const resp = await this.client.chat({
1476
1494
  model: this.model,
1477
1495
  messages,
1478
- tools: toolSpecs.length ? toolSpecs : void 0
1496
+ tools: toolSpecs.length ? toolSpecs : void 0,
1497
+ signal
1479
1498
  });
1480
1499
  assistantContent = resp.content;
1481
1500
  reasoningContent = resp.reasoningContent ?? "";
@@ -1539,7 +1558,7 @@ var CacheFirstLoop = class {
1539
1558
  toolName: name,
1540
1559
  toolArgs: args
1541
1560
  };
1542
- const result = await this.tools.dispatch(name, args);
1561
+ const result = await this.tools.dispatch(name, args, { signal });
1543
1562
  this.appendAndPersist({
1544
1563
  role: "tool",
1545
1564
  tool_call_id: call.id ?? "",
@@ -1566,8 +1585,9 @@ var CacheFirstLoop = class {
1566
1585
  });
1567
1586
  const resp = await this.client.chat({
1568
1587
  model: this.model,
1569
- messages
1588
+ messages,
1570
1589
  // no tools → model is forced to answer in text
1590
+ signal: this._turnAbort.signal
1571
1591
  });
1572
1592
  const rawContent = resp.content?.trim() ?? "";
1573
1593
  const cleaned = stripHallucinatedToolMarkup(rawContent);
@@ -1663,13 +1683,298 @@ function formatLoopError(err) {
1663
1683
  return msg;
1664
1684
  }
1665
1685
 
1686
+ // src/tools/filesystem.ts
1687
+ import { promises as fs } from "fs";
1688
+ import * as pathMod from "path";
1689
+ var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
1690
+ var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
1691
+ function registerFilesystemTools(registry, opts) {
1692
+ const rootDir = pathMod.resolve(opts.rootDir);
1693
+ const allowWriting = opts.allowWriting !== false;
1694
+ const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
1695
+ const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
1696
+ const safePath = (raw) => {
1697
+ if (typeof raw !== "string" || raw.length === 0) {
1698
+ throw new Error("path must be a non-empty string");
1699
+ }
1700
+ const resolved = pathMod.resolve(rootDir, raw);
1701
+ const normRoot = pathMod.resolve(rootDir);
1702
+ const rel = pathMod.relative(normRoot, resolved);
1703
+ if (rel.startsWith("..") || pathMod.isAbsolute(rel)) {
1704
+ throw new Error(`path escapes sandbox root (${normRoot}): ${raw}`);
1705
+ }
1706
+ return resolved;
1707
+ };
1708
+ registry.register({
1709
+ name: "read_file",
1710
+ 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.",
1711
+ parameters: {
1712
+ type: "object",
1713
+ properties: {
1714
+ path: { type: "string", description: "Path to read (relative to rootDir or absolute)." },
1715
+ head: { type: "integer", description: "If set, return only the first N lines." },
1716
+ tail: { type: "integer", description: "If set, return only the last N lines." }
1717
+ },
1718
+ required: ["path"]
1719
+ },
1720
+ fn: async (args) => {
1721
+ const abs = safePath(args.path);
1722
+ const stat = await fs.stat(abs);
1723
+ if (stat.isDirectory()) {
1724
+ throw new Error(`not a file: ${args.path} (it's a directory)`);
1725
+ }
1726
+ const raw = await fs.readFile(abs);
1727
+ if (raw.length > maxReadBytes) {
1728
+ const head = raw.slice(0, maxReadBytes).toString("utf8");
1729
+ return `${head}
1730
+
1731
+ [\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail for targeted view.]`;
1732
+ }
1733
+ const text = raw.toString("utf8");
1734
+ if (typeof args.head === "number" && args.head > 0) {
1735
+ return text.split(/\r?\n/).slice(0, args.head).join("\n");
1736
+ }
1737
+ if (typeof args.tail === "number" && args.tail > 0) {
1738
+ let lines = text.split(/\r?\n/);
1739
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
1740
+ return lines.slice(Math.max(0, lines.length - args.tail)).join("\n");
1741
+ }
1742
+ return text;
1743
+ }
1744
+ });
1745
+ registry.register({
1746
+ name: "list_directory",
1747
+ 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.",
1748
+ parameters: {
1749
+ type: "object",
1750
+ properties: {
1751
+ path: { type: "string", description: "Directory to list (default: root)." }
1752
+ }
1753
+ },
1754
+ fn: async (args) => {
1755
+ const abs = safePath(args.path ?? ".");
1756
+ const entries = await fs.readdir(abs, { withFileTypes: true });
1757
+ const lines = [];
1758
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1759
+ lines.push(e.isDirectory() ? `${e.name}/` : e.name);
1760
+ }
1761
+ return lines.join("\n") || "(empty directory)";
1762
+ }
1763
+ });
1764
+ registry.register({
1765
+ name: "directory_tree",
1766
+ 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.",
1767
+ parameters: {
1768
+ type: "object",
1769
+ properties: {
1770
+ path: { type: "string", description: "Root of the tree (default: sandbox root)." },
1771
+ maxDepth: { type: "integer", description: "Max recursion depth (default 4)." }
1772
+ }
1773
+ },
1774
+ fn: async (args) => {
1775
+ const startAbs = safePath(args.path ?? ".");
1776
+ const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 4;
1777
+ const lines = [];
1778
+ let totalBytes = 0;
1779
+ let truncated = false;
1780
+ const walk2 = async (dir, depth) => {
1781
+ if (truncated) return;
1782
+ if (depth > maxDepth) return;
1783
+ let entries;
1784
+ try {
1785
+ entries = await fs.readdir(dir, { withFileTypes: true });
1786
+ } catch {
1787
+ return;
1788
+ }
1789
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1790
+ for (const e of entries) {
1791
+ if (truncated) return;
1792
+ const indent = " ".repeat(depth);
1793
+ const line = e.isDirectory() ? `${indent}${e.name}/` : `${indent}${e.name}`;
1794
+ totalBytes += line.length + 1;
1795
+ if (totalBytes > maxListBytes) {
1796
+ lines.push(` [\u2026 tree truncated at ${maxListBytes} bytes \u2026]`);
1797
+ truncated = true;
1798
+ return;
1799
+ }
1800
+ lines.push(line);
1801
+ if (e.isDirectory()) {
1802
+ await walk2(pathMod.join(dir, e.name), depth + 1);
1803
+ }
1804
+ }
1805
+ };
1806
+ await walk2(startAbs, 0);
1807
+ return lines.join("\n") || "(empty tree)";
1808
+ }
1809
+ });
1810
+ registry.register({
1811
+ name: "search_files",
1812
+ 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.",
1813
+ parameters: {
1814
+ type: "object",
1815
+ properties: {
1816
+ path: { type: "string", description: "Directory to start the search at (default: root)." },
1817
+ pattern: {
1818
+ type: "string",
1819
+ description: "Substring (or regex) to match against filenames."
1820
+ }
1821
+ },
1822
+ required: ["pattern"]
1823
+ },
1824
+ fn: async (args) => {
1825
+ const startAbs = safePath(args.path ?? ".");
1826
+ const needle = args.pattern.toLowerCase();
1827
+ let re = null;
1828
+ try {
1829
+ re = new RegExp(args.pattern, "i");
1830
+ } catch {
1831
+ re = null;
1832
+ }
1833
+ const matches = [];
1834
+ let totalBytes = 0;
1835
+ const walk2 = async (dir) => {
1836
+ let entries;
1837
+ try {
1838
+ entries = await fs.readdir(dir, { withFileTypes: true });
1839
+ } catch {
1840
+ return;
1841
+ }
1842
+ for (const e of entries) {
1843
+ const full = pathMod.join(dir, e.name);
1844
+ const lower = e.name.toLowerCase();
1845
+ const hit = re ? re.test(e.name) : lower.includes(needle);
1846
+ if (hit) {
1847
+ const rel = pathMod.relative(rootDir, full);
1848
+ if (totalBytes + rel.length + 1 > maxListBytes) {
1849
+ matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
1850
+ return;
1851
+ }
1852
+ matches.push(rel);
1853
+ totalBytes += rel.length + 1;
1854
+ }
1855
+ if (e.isDirectory()) await walk2(full);
1856
+ }
1857
+ };
1858
+ await walk2(startAbs);
1859
+ return matches.length === 0 ? "(no matches)" : matches.join("\n");
1860
+ }
1861
+ });
1862
+ registry.register({
1863
+ name: "get_file_info",
1864
+ description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
1865
+ parameters: {
1866
+ type: "object",
1867
+ properties: {
1868
+ path: { type: "string" }
1869
+ },
1870
+ required: ["path"]
1871
+ },
1872
+ fn: async (args) => {
1873
+ const abs = safePath(args.path);
1874
+ const st = await fs.lstat(abs);
1875
+ const type = st.isDirectory() ? "directory" : st.isSymbolicLink() ? "symlink" : "file";
1876
+ return JSON.stringify({
1877
+ type,
1878
+ size: st.size,
1879
+ mtime: st.mtime.toISOString()
1880
+ });
1881
+ }
1882
+ });
1883
+ if (!allowWriting) return registry;
1884
+ registry.register({
1885
+ name: "write_file",
1886
+ description: "Create or overwrite a file under the sandbox root with the given content. Parent directories are created as needed.",
1887
+ parameters: {
1888
+ type: "object",
1889
+ properties: {
1890
+ path: { type: "string" },
1891
+ content: { type: "string" }
1892
+ },
1893
+ required: ["path", "content"]
1894
+ },
1895
+ fn: async (args) => {
1896
+ const abs = safePath(args.path);
1897
+ await fs.mkdir(pathMod.dirname(abs), { recursive: true });
1898
+ await fs.writeFile(abs, args.content, "utf8");
1899
+ return `wrote ${args.content.length} chars to ${pathMod.relative(rootDir, abs)}`;
1900
+ }
1901
+ });
1902
+ registry.register({
1903
+ name: "edit_file",
1904
+ 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.",
1905
+ parameters: {
1906
+ type: "object",
1907
+ properties: {
1908
+ path: { type: "string" },
1909
+ search: { type: "string", description: "Exact text to find (must be unique)." },
1910
+ replace: { type: "string", description: "Text to substitute in place of `search`." }
1911
+ },
1912
+ required: ["path", "search", "replace"]
1913
+ },
1914
+ fn: async (args) => {
1915
+ const abs = safePath(args.path);
1916
+ const before = await fs.readFile(abs, "utf8");
1917
+ if (args.search.length === 0) {
1918
+ throw new Error("edit_file: search cannot be empty");
1919
+ }
1920
+ const firstIdx = before.indexOf(args.search);
1921
+ if (firstIdx < 0) {
1922
+ throw new Error(`edit_file: search text not found in ${pathMod.relative(rootDir, abs)}`);
1923
+ }
1924
+ const nextIdx = before.indexOf(args.search, firstIdx + 1);
1925
+ if (nextIdx >= 0) {
1926
+ throw new Error(
1927
+ `edit_file: search text appears multiple times in ${pathMod.relative(rootDir, abs)} \u2014 include more context to disambiguate`
1928
+ );
1929
+ }
1930
+ const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
1931
+ await fs.writeFile(abs, after, "utf8");
1932
+ return `edited ${pathMod.relative(rootDir, abs)} (${args.search.length}\u2192${args.replace.length} chars)`;
1933
+ }
1934
+ });
1935
+ registry.register({
1936
+ name: "create_directory",
1937
+ description: "Create a directory (and any missing parents) under the sandbox root.",
1938
+ parameters: {
1939
+ type: "object",
1940
+ properties: { path: { type: "string" } },
1941
+ required: ["path"]
1942
+ },
1943
+ fn: async (args) => {
1944
+ const abs = safePath(args.path);
1945
+ await fs.mkdir(abs, { recursive: true });
1946
+ return `created ${pathMod.relative(rootDir, abs)}/`;
1947
+ }
1948
+ });
1949
+ registry.register({
1950
+ name: "move_file",
1951
+ description: "Rename/move a file or directory under the sandbox root.",
1952
+ parameters: {
1953
+ type: "object",
1954
+ properties: {
1955
+ source: { type: "string" },
1956
+ destination: { type: "string" }
1957
+ },
1958
+ required: ["source", "destination"]
1959
+ },
1960
+ fn: async (args) => {
1961
+ const src = safePath(args.source);
1962
+ const dst = safePath(args.destination);
1963
+ await fs.mkdir(pathMod.dirname(dst), { recursive: true });
1964
+ await fs.rename(src, dst);
1965
+ return `moved ${pathMod.relative(rootDir, src)} \u2192 ${pathMod.relative(rootDir, dst)}`;
1966
+ }
1967
+ });
1968
+ return registry;
1969
+ }
1970
+
1666
1971
  // src/env.ts
1667
1972
  import { readFileSync as readFileSync2 } from "fs";
1668
- import { resolve } from "path";
1973
+ import { resolve as resolve2 } from "path";
1669
1974
  function loadDotenv(path = ".env") {
1670
1975
  let raw;
1671
1976
  try {
1672
- raw = readFileSync2(resolve(process.cwd(), path), "utf8");
1977
+ raw = readFileSync2(resolve2(process.cwd(), path), "utf8");
1673
1978
  } catch {
1674
1979
  return;
1675
1980
  }
@@ -2199,6 +2504,13 @@ var McpClient = class {
2199
2504
  _serverInfo = { name: "", version: "" };
2200
2505
  _protocolVersion = "";
2201
2506
  _instructions;
2507
+ // Progress-token → handler for notifications/progress routing. Tokens
2508
+ // are minted per call when the caller supplies an onProgress
2509
+ // callback; cleared when the final response lands (or the pending
2510
+ // request rejects). No leaks — the `try/finally` in callTool
2511
+ // guarantees cleanup even on timeout.
2512
+ progressHandlers = /* @__PURE__ */ new Map();
2513
+ nextProgressToken = 1;
2202
2514
  constructor(opts) {
2203
2515
  this.transport = opts.transport;
2204
2516
  this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
@@ -2253,13 +2565,36 @@ var McpClient = class {
2253
2565
  this.assertInitialized();
2254
2566
  return this.request("tools/list", {});
2255
2567
  }
2256
- /** Invoke a tool by name. Returns the raw MCP result (caller unwraps content). */
2257
- async callTool(name, args) {
2568
+ /**
2569
+ * Invoke a tool by name. When `onProgress` is supplied, attaches a
2570
+ * fresh progress token so the server can send incremental updates
2571
+ * via `notifications/progress`; they're routed to the callback until
2572
+ * the final response arrives (or the request times out, in which
2573
+ * case the handler is simply dropped — no extra notification).
2574
+ *
2575
+ * When `signal` is supplied, aborting it:
2576
+ * 1) fires `notifications/cancelled` to the server (MCP 2024-11-05
2577
+ * way of saying "forget this request, I no longer care"), and
2578
+ * 2) rejects the pending promise immediately with an AbortError,
2579
+ * so the caller doesn't have to wait for the subprocess to
2580
+ * finish its in-flight file write or network request.
2581
+ * The server MAY still emit a late response; we drop it in dispatch
2582
+ * since the request id is gone from `pending`.
2583
+ */
2584
+ async callTool(name, args, opts = {}) {
2258
2585
  this.assertInitialized();
2259
- return this.request("tools/call", {
2260
- name,
2261
- arguments: args ?? {}
2262
- });
2586
+ const params = { name, arguments: args ?? {} };
2587
+ let token;
2588
+ if (opts.onProgress) {
2589
+ token = this.nextProgressToken++;
2590
+ this.progressHandlers.set(token, opts.onProgress);
2591
+ params._meta = { progressToken: token };
2592
+ }
2593
+ try {
2594
+ return await this.request("tools/call", params, opts.signal);
2595
+ } finally {
2596
+ if (token !== void 0) this.progressHandlers.delete(token);
2597
+ }
2263
2598
  }
2264
2599
  /**
2265
2600
  * List resources the server exposes. Supports a pagination cursor;
@@ -2313,24 +2648,56 @@ var McpClient = class {
2313
2648
  assertInitialized() {
2314
2649
  if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
2315
2650
  }
2316
- async request(method, params) {
2651
+ async request(method, params, signal) {
2317
2652
  const id = this.nextId++;
2318
2653
  const frame = { jsonrpc: "2.0", id, method, params };
2319
- const promise = new Promise((resolve3, reject) => {
2654
+ let abortHandler = null;
2655
+ const promise = new Promise((resolve4, reject) => {
2320
2656
  const timeout = setTimeout(() => {
2321
2657
  this.pending.delete(id);
2658
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
2322
2659
  reject(
2323
2660
  new Error(`MCP request ${method} (id=${id}) timed out after ${this.requestTimeoutMs}ms`)
2324
2661
  );
2325
2662
  }, this.requestTimeoutMs);
2326
2663
  this.pending.set(id, {
2327
- resolve: resolve3,
2664
+ resolve: resolve4,
2328
2665
  reject,
2329
2666
  timeout
2330
2667
  });
2668
+ if (signal) {
2669
+ if (signal.aborted) {
2670
+ this.pending.delete(id);
2671
+ clearTimeout(timeout);
2672
+ reject(new Error(`MCP request ${method} (id=${id}) aborted before send`));
2673
+ return;
2674
+ }
2675
+ abortHandler = () => {
2676
+ this.pending.delete(id);
2677
+ clearTimeout(timeout);
2678
+ void this.transport.send({
2679
+ jsonrpc: "2.0",
2680
+ method: "notifications/cancelled",
2681
+ params: { requestId: id, reason: "aborted by user" }
2682
+ }).catch(() => {
2683
+ });
2684
+ reject(new Error(`MCP request ${method} (id=${id}) aborted by user`));
2685
+ };
2686
+ signal.addEventListener("abort", abortHandler, { once: true });
2687
+ }
2331
2688
  });
2332
- await this.transport.send(frame);
2333
- return promise;
2689
+ try {
2690
+ await this.transport.send(frame);
2691
+ } catch (err) {
2692
+ this.pending.delete(id);
2693
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
2694
+ throw err;
2695
+ }
2696
+ try {
2697
+ return await promise;
2698
+ } finally {
2699
+ if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
2700
+ }
2334
2701
  }
2335
2702
  startReaderIfNeeded() {
2336
2703
  if (this.readerStarted) return;
@@ -2351,7 +2718,16 @@ var McpClient = class {
2351
2718
  }
2352
2719
  }
2353
2720
  dispatch(msg) {
2354
- if (!("id" in msg) || msg.id === null || msg.id === void 0) return;
2721
+ if (!("id" in msg) || msg.id === null || msg.id === void 0) {
2722
+ if ("method" in msg && msg.method === "notifications/progress") {
2723
+ const p = msg.params;
2724
+ if (!p || p.progressToken === void 0) return;
2725
+ const handler = this.progressHandlers.get(p.progressToken);
2726
+ if (!handler) return;
2727
+ handler({ progress: p.progress, total: p.total, message: p.message });
2728
+ }
2729
+ return;
2730
+ }
2355
2731
  if (!("result" in msg) && !("error" in msg)) return;
2356
2732
  const pending = this.pending.get(msg.id);
2357
2733
  if (!pending) return;
@@ -2408,12 +2784,12 @@ var StdioTransport = class {
2408
2784
  }
2409
2785
  async send(message) {
2410
2786
  if (this.closed) throw new Error("MCP transport is closed");
2411
- return new Promise((resolve3, reject) => {
2787
+ return new Promise((resolve4, reject) => {
2412
2788
  const line = `${JSON.stringify(message)}
2413
2789
  `;
2414
2790
  this.child.stdin.write(line, "utf8", (err) => {
2415
2791
  if (err) reject(err);
2416
- else resolve3();
2792
+ else resolve4();
2417
2793
  });
2418
2794
  });
2419
2795
  }
@@ -2424,8 +2800,8 @@ var StdioTransport = class {
2424
2800
  continue;
2425
2801
  }
2426
2802
  if (this.closed) return;
2427
- const next = await new Promise((resolve3) => {
2428
- this.waiters.push(resolve3);
2803
+ const next = await new Promise((resolve4) => {
2804
+ this.waiters.push(resolve4);
2429
2805
  });
2430
2806
  if (next === null) return;
2431
2807
  yield next;
@@ -2491,8 +2867,8 @@ var SseTransport = class {
2491
2867
  constructor(opts) {
2492
2868
  this.url = opts.url;
2493
2869
  this.headers = opts.headers ?? {};
2494
- this.endpointReady = new Promise((resolve3, reject) => {
2495
- this.resolveEndpoint = resolve3;
2870
+ this.endpointReady = new Promise((resolve4, reject) => {
2871
+ this.resolveEndpoint = resolve4;
2496
2872
  this.rejectEndpoint = reject;
2497
2873
  });
2498
2874
  this.endpointReady.catch(() => void 0);
@@ -2519,8 +2895,8 @@ var SseTransport = class {
2519
2895
  continue;
2520
2896
  }
2521
2897
  if (this.closed) return;
2522
- const next = await new Promise((resolve3) => {
2523
- this.waiters.push(resolve3);
2898
+ const next = await new Promise((resolve4) => {
2899
+ this.waiters.push(resolve4);
2524
2900
  });
2525
2901
  if (next === null) return;
2526
2902
  yield next;
@@ -2720,7 +3096,7 @@ async function trySection(load) {
2720
3096
 
2721
3097
  // src/code/edit-blocks.ts
2722
3098
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
2723
- import { dirname as dirname2, resolve as resolve2 } from "path";
3099
+ import { dirname as dirname3, resolve as resolve3 } from "path";
2724
3100
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
2725
3101
  function parseEditBlocks(text) {
2726
3102
  const out = [];
@@ -2738,8 +3114,8 @@ function parseEditBlocks(text) {
2738
3114
  return out;
2739
3115
  }
2740
3116
  function applyEditBlock(block, rootDir) {
2741
- const absRoot = resolve2(rootDir);
2742
- const absTarget = resolve2(absRoot, block.path);
3117
+ const absRoot = resolve3(rootDir);
3118
+ const absTarget = resolve3(absRoot, block.path);
2743
3119
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
2744
3120
  return {
2745
3121
  path: block.path,
@@ -2758,7 +3134,7 @@ function applyEditBlock(block, rootDir) {
2758
3134
  message: "file does not exist; to create it, use an empty SEARCH block"
2759
3135
  };
2760
3136
  }
2761
- mkdirSync2(dirname2(absTarget), { recursive: true });
3137
+ mkdirSync2(dirname3(absTarget), { recursive: true });
2762
3138
  writeFileSync2(absTarget, block.replace, "utf8");
2763
3139
  return { path: block.path, status: "created" };
2764
3140
  }
@@ -2789,13 +3165,13 @@ function applyEditBlocks(blocks, rootDir) {
2789
3165
  return blocks.map((b) => applyEditBlock(b, rootDir));
2790
3166
  }
2791
3167
  function snapshotBeforeEdits(blocks, rootDir) {
2792
- const absRoot = resolve2(rootDir);
3168
+ const absRoot = resolve3(rootDir);
2793
3169
  const seen = /* @__PURE__ */ new Set();
2794
3170
  const snapshots = [];
2795
3171
  for (const b of blocks) {
2796
3172
  if (seen.has(b.path)) continue;
2797
3173
  seen.add(b.path);
2798
- const abs = resolve2(absRoot, b.path);
3174
+ const abs = resolve3(absRoot, b.path);
2799
3175
  if (!existsSync2(abs)) {
2800
3176
  snapshots.push({ path: b.path, prevContent: null });
2801
3177
  continue;
@@ -2809,9 +3185,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
2809
3185
  return snapshots;
2810
3186
  }
2811
3187
  function restoreSnapshots(snapshots, rootDir) {
2812
- const absRoot = resolve2(rootDir);
3188
+ const absRoot = resolve3(rootDir);
2813
3189
  return snapshots.map((snap) => {
2814
- const abs = resolve2(absRoot, snap.path);
3190
+ const abs = resolve3(absRoot, snap.path);
2815
3191
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
2816
3192
  return {
2817
3193
  path: snap.path,
@@ -2845,7 +3221,7 @@ function sep() {
2845
3221
 
2846
3222
  // src/code/prompt.ts
2847
3223
  import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
2848
- import { join as join2 } from "path";
3224
+ import { join as join3 } from "path";
2849
3225
  var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
2850
3226
 
2851
3227
  # When to edit vs. when to explore
@@ -2894,7 +3270,7 @@ Rules:
2894
3270
  - If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
2895
3271
  `;
2896
3272
  function codeSystemPrompt(rootDir) {
2897
- const gitignorePath = join2(rootDir, ".gitignore");
3273
+ const gitignorePath = join3(rootDir, ".gitignore");
2898
3274
  if (!existsSync3(gitignorePath)) return CODE_SYSTEM_PROMPT;
2899
3275
  let content;
2900
3276
  try {
@@ -2920,9 +3296,9 @@ ${truncated}
2920
3296
  // src/config.ts
2921
3297
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2922
3298
  import { homedir as homedir2 } from "os";
2923
- import { dirname as dirname3, join as join3 } from "path";
3299
+ import { dirname as dirname4, join as join4 } from "path";
2924
3300
  function defaultConfigPath() {
2925
- return join3(homedir2(), ".reasonix", "config.json");
3301
+ return join4(homedir2(), ".reasonix", "config.json");
2926
3302
  }
2927
3303
  function readConfig(path = defaultConfigPath()) {
2928
3304
  try {
@@ -2934,7 +3310,7 @@ function readConfig(path = defaultConfigPath()) {
2934
3310
  return {};
2935
3311
  }
2936
3312
  function writeConfig(cfg, path = defaultConfigPath()) {
2937
- mkdirSync3(dirname3(path), { recursive: true });
3313
+ mkdirSync3(dirname4(path), { recursive: true });
2938
3314
  writeFileSync3(path, JSON.stringify(cfg, null, 2), "utf8");
2939
3315
  try {
2940
3316
  chmodSync2(path, 384);
@@ -3018,6 +3394,7 @@ export {
3018
3394
  readTranscript,
3019
3395
  recordFromLoopEvent,
3020
3396
  redactKey,
3397
+ registerFilesystemTools,
3021
3398
  renderMarkdown as renderDiffMarkdown,
3022
3399
  renderSummaryTable as renderDiffSummary,
3023
3400
  repairTruncatedJson,