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 +993 -203
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +174 -14
- package/dist/index.js +486 -60
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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((
|
|
51
|
-
const timer = setTimeout(
|
|
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
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
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
|
-
|
|
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}
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
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.
|
|
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.
|
|
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 (
|
|
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((
|
|
1399
|
-
waiter =
|
|
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(
|
|
1977
|
+
raw = readFileSync2(resolve2(process.cwd(), path), "utf8");
|
|
1673
1978
|
} catch {
|
|
1674
1979
|
return;
|
|
1675
1980
|
}
|
|
@@ -2196,6 +2501,16 @@ var McpClient = class {
|
|
|
2196
2501
|
readerStarted = false;
|
|
2197
2502
|
initialized = false;
|
|
2198
2503
|
_serverCapabilities = {};
|
|
2504
|
+
_serverInfo = { name: "", version: "" };
|
|
2505
|
+
_protocolVersion = "";
|
|
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;
|
|
2199
2514
|
constructor(opts) {
|
|
2200
2515
|
this.transport = opts.transport;
|
|
2201
2516
|
this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
|
|
@@ -2205,6 +2520,18 @@ var McpClient = class {
|
|
|
2205
2520
|
get serverCapabilities() {
|
|
2206
2521
|
return this._serverCapabilities;
|
|
2207
2522
|
}
|
|
2523
|
+
/** Server's self-reported name + version, available after initialize(). */
|
|
2524
|
+
get serverInfo() {
|
|
2525
|
+
return this._serverInfo;
|
|
2526
|
+
}
|
|
2527
|
+
/** Protocol version the server agreed to during the handshake. */
|
|
2528
|
+
get protocolVersion() {
|
|
2529
|
+
return this._protocolVersion;
|
|
2530
|
+
}
|
|
2531
|
+
/** Optional free-form instructions the server provides at handshake. */
|
|
2532
|
+
get serverInstructions() {
|
|
2533
|
+
return this._instructions;
|
|
2534
|
+
}
|
|
2208
2535
|
/**
|
|
2209
2536
|
* Complete the initialize → initialized handshake. Must be called
|
|
2210
2537
|
* before any other method (otherwise compliant servers reject).
|
|
@@ -2223,6 +2550,9 @@ var McpClient = class {
|
|
|
2223
2550
|
clientInfo: this.clientInfo
|
|
2224
2551
|
});
|
|
2225
2552
|
this._serverCapabilities = result.capabilities ?? {};
|
|
2553
|
+
this._serverInfo = result.serverInfo ?? { name: "", version: "" };
|
|
2554
|
+
this._protocolVersion = result.protocolVersion ?? "";
|
|
2555
|
+
this._instructions = result.instructions;
|
|
2226
2556
|
await this.transport.send({
|
|
2227
2557
|
jsonrpc: "2.0",
|
|
2228
2558
|
method: "notifications/initialized"
|
|
@@ -2235,13 +2565,36 @@ var McpClient = class {
|
|
|
2235
2565
|
this.assertInitialized();
|
|
2236
2566
|
return this.request("tools/list", {});
|
|
2237
2567
|
}
|
|
2238
|
-
/**
|
|
2239
|
-
|
|
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 = {}) {
|
|
2240
2585
|
this.assertInitialized();
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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
|
+
}
|
|
2245
2598
|
}
|
|
2246
2599
|
/**
|
|
2247
2600
|
* List resources the server exposes. Supports a pagination cursor;
|
|
@@ -2295,24 +2648,56 @@ var McpClient = class {
|
|
|
2295
2648
|
assertInitialized() {
|
|
2296
2649
|
if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
|
|
2297
2650
|
}
|
|
2298
|
-
async request(method, params) {
|
|
2651
|
+
async request(method, params, signal) {
|
|
2299
2652
|
const id = this.nextId++;
|
|
2300
2653
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
2301
|
-
|
|
2654
|
+
let abortHandler = null;
|
|
2655
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2302
2656
|
const timeout = setTimeout(() => {
|
|
2303
2657
|
this.pending.delete(id);
|
|
2658
|
+
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
2304
2659
|
reject(
|
|
2305
2660
|
new Error(`MCP request ${method} (id=${id}) timed out after ${this.requestTimeoutMs}ms`)
|
|
2306
2661
|
);
|
|
2307
2662
|
}, this.requestTimeoutMs);
|
|
2308
2663
|
this.pending.set(id, {
|
|
2309
|
-
resolve:
|
|
2664
|
+
resolve: resolve4,
|
|
2310
2665
|
reject,
|
|
2311
2666
|
timeout
|
|
2312
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
|
+
}
|
|
2313
2688
|
});
|
|
2314
|
-
|
|
2315
|
-
|
|
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
|
+
}
|
|
2316
2701
|
}
|
|
2317
2702
|
startReaderIfNeeded() {
|
|
2318
2703
|
if (this.readerStarted) return;
|
|
@@ -2333,7 +2718,16 @@ var McpClient = class {
|
|
|
2333
2718
|
}
|
|
2334
2719
|
}
|
|
2335
2720
|
dispatch(msg) {
|
|
2336
|
-
if (!("id" in msg) || msg.id === null || msg.id === void 0)
|
|
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
|
+
}
|
|
2337
2731
|
if (!("result" in msg) && !("error" in msg)) return;
|
|
2338
2732
|
const pending = this.pending.get(msg.id);
|
|
2339
2733
|
if (!pending) return;
|
|
@@ -2390,12 +2784,12 @@ var StdioTransport = class {
|
|
|
2390
2784
|
}
|
|
2391
2785
|
async send(message) {
|
|
2392
2786
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
2393
|
-
return new Promise((
|
|
2787
|
+
return new Promise((resolve4, reject) => {
|
|
2394
2788
|
const line = `${JSON.stringify(message)}
|
|
2395
2789
|
`;
|
|
2396
2790
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
2397
2791
|
if (err) reject(err);
|
|
2398
|
-
else
|
|
2792
|
+
else resolve4();
|
|
2399
2793
|
});
|
|
2400
2794
|
});
|
|
2401
2795
|
}
|
|
@@ -2406,8 +2800,8 @@ var StdioTransport = class {
|
|
|
2406
2800
|
continue;
|
|
2407
2801
|
}
|
|
2408
2802
|
if (this.closed) return;
|
|
2409
|
-
const next = await new Promise((
|
|
2410
|
-
this.waiters.push(
|
|
2803
|
+
const next = await new Promise((resolve4) => {
|
|
2804
|
+
this.waiters.push(resolve4);
|
|
2411
2805
|
});
|
|
2412
2806
|
if (next === null) return;
|
|
2413
2807
|
yield next;
|
|
@@ -2473,8 +2867,8 @@ var SseTransport = class {
|
|
|
2473
2867
|
constructor(opts) {
|
|
2474
2868
|
this.url = opts.url;
|
|
2475
2869
|
this.headers = opts.headers ?? {};
|
|
2476
|
-
this.endpointReady = new Promise((
|
|
2477
|
-
this.resolveEndpoint =
|
|
2870
|
+
this.endpointReady = new Promise((resolve4, reject) => {
|
|
2871
|
+
this.resolveEndpoint = resolve4;
|
|
2478
2872
|
this.rejectEndpoint = reject;
|
|
2479
2873
|
});
|
|
2480
2874
|
this.endpointReady.catch(() => void 0);
|
|
@@ -2501,8 +2895,8 @@ var SseTransport = class {
|
|
|
2501
2895
|
continue;
|
|
2502
2896
|
}
|
|
2503
2897
|
if (this.closed) return;
|
|
2504
|
-
const next = await new Promise((
|
|
2505
|
-
this.waiters.push(
|
|
2898
|
+
const next = await new Promise((resolve4) => {
|
|
2899
|
+
this.waiters.push(resolve4);
|
|
2506
2900
|
});
|
|
2507
2901
|
if (next === null) return;
|
|
2508
2902
|
yield next;
|
|
@@ -2670,9 +3064,39 @@ function parseMcpSpec(input) {
|
|
|
2670
3064
|
return { transport: "stdio", name, command, args };
|
|
2671
3065
|
}
|
|
2672
3066
|
|
|
3067
|
+
// src/mcp/inspect.ts
|
|
3068
|
+
async function inspectMcpServer(client) {
|
|
3069
|
+
const tools = await trySection(() => client.listTools().then((r) => r.tools));
|
|
3070
|
+
const resources = await trySection(
|
|
3071
|
+
() => client.listResources().then((r) => r.resources)
|
|
3072
|
+
);
|
|
3073
|
+
const prompts = await trySection(() => client.listPrompts().then((r) => r.prompts));
|
|
3074
|
+
return {
|
|
3075
|
+
protocolVersion: client.protocolVersion || "(unknown)",
|
|
3076
|
+
serverInfo: client.serverInfo,
|
|
3077
|
+
capabilities: client.serverCapabilities ?? {},
|
|
3078
|
+
instructions: client.serverInstructions,
|
|
3079
|
+
tools,
|
|
3080
|
+
resources,
|
|
3081
|
+
prompts
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
async function trySection(load) {
|
|
3085
|
+
try {
|
|
3086
|
+
const items = await load();
|
|
3087
|
+
return { supported: true, items };
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
const msg = err.message ?? String(err);
|
|
3090
|
+
if (/-32601/.test(msg) || /method not found/i.test(msg)) {
|
|
3091
|
+
return { supported: false, reason: "method not found (-32601)" };
|
|
3092
|
+
}
|
|
3093
|
+
return { supported: false, reason: msg };
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
|
|
2673
3097
|
// src/code/edit-blocks.ts
|
|
2674
3098
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2675
|
-
import { dirname as
|
|
3099
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
2676
3100
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
2677
3101
|
function parseEditBlocks(text) {
|
|
2678
3102
|
const out = [];
|
|
@@ -2690,8 +3114,8 @@ function parseEditBlocks(text) {
|
|
|
2690
3114
|
return out;
|
|
2691
3115
|
}
|
|
2692
3116
|
function applyEditBlock(block, rootDir) {
|
|
2693
|
-
const absRoot =
|
|
2694
|
-
const absTarget =
|
|
3117
|
+
const absRoot = resolve3(rootDir);
|
|
3118
|
+
const absTarget = resolve3(absRoot, block.path);
|
|
2695
3119
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
2696
3120
|
return {
|
|
2697
3121
|
path: block.path,
|
|
@@ -2710,7 +3134,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
2710
3134
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
2711
3135
|
};
|
|
2712
3136
|
}
|
|
2713
|
-
mkdirSync2(
|
|
3137
|
+
mkdirSync2(dirname3(absTarget), { recursive: true });
|
|
2714
3138
|
writeFileSync2(absTarget, block.replace, "utf8");
|
|
2715
3139
|
return { path: block.path, status: "created" };
|
|
2716
3140
|
}
|
|
@@ -2741,13 +3165,13 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
2741
3165
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
2742
3166
|
}
|
|
2743
3167
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
2744
|
-
const absRoot =
|
|
3168
|
+
const absRoot = resolve3(rootDir);
|
|
2745
3169
|
const seen = /* @__PURE__ */ new Set();
|
|
2746
3170
|
const snapshots = [];
|
|
2747
3171
|
for (const b of blocks) {
|
|
2748
3172
|
if (seen.has(b.path)) continue;
|
|
2749
3173
|
seen.add(b.path);
|
|
2750
|
-
const abs =
|
|
3174
|
+
const abs = resolve3(absRoot, b.path);
|
|
2751
3175
|
if (!existsSync2(abs)) {
|
|
2752
3176
|
snapshots.push({ path: b.path, prevContent: null });
|
|
2753
3177
|
continue;
|
|
@@ -2761,9 +3185,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
2761
3185
|
return snapshots;
|
|
2762
3186
|
}
|
|
2763
3187
|
function restoreSnapshots(snapshots, rootDir) {
|
|
2764
|
-
const absRoot =
|
|
3188
|
+
const absRoot = resolve3(rootDir);
|
|
2765
3189
|
return snapshots.map((snap) => {
|
|
2766
|
-
const abs =
|
|
3190
|
+
const abs = resolve3(absRoot, snap.path);
|
|
2767
3191
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
2768
3192
|
return {
|
|
2769
3193
|
path: snap.path,
|
|
@@ -2797,7 +3221,7 @@ function sep() {
|
|
|
2797
3221
|
|
|
2798
3222
|
// src/code/prompt.ts
|
|
2799
3223
|
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
2800
|
-
import { join as
|
|
3224
|
+
import { join as join3 } from "path";
|
|
2801
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.
|
|
2802
3226
|
|
|
2803
3227
|
# When to edit vs. when to explore
|
|
@@ -2846,7 +3270,7 @@ Rules:
|
|
|
2846
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.
|
|
2847
3271
|
`;
|
|
2848
3272
|
function codeSystemPrompt(rootDir) {
|
|
2849
|
-
const gitignorePath =
|
|
3273
|
+
const gitignorePath = join3(rootDir, ".gitignore");
|
|
2850
3274
|
if (!existsSync3(gitignorePath)) return CODE_SYSTEM_PROMPT;
|
|
2851
3275
|
let content;
|
|
2852
3276
|
try {
|
|
@@ -2872,9 +3296,9 @@ ${truncated}
|
|
|
2872
3296
|
// src/config.ts
|
|
2873
3297
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
2874
3298
|
import { homedir as homedir2 } from "os";
|
|
2875
|
-
import { dirname as
|
|
3299
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
2876
3300
|
function defaultConfigPath() {
|
|
2877
|
-
return
|
|
3301
|
+
return join4(homedir2(), ".reasonix", "config.json");
|
|
2878
3302
|
}
|
|
2879
3303
|
function readConfig(path = defaultConfigPath()) {
|
|
2880
3304
|
try {
|
|
@@ -2886,7 +3310,7 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
2886
3310
|
return {};
|
|
2887
3311
|
}
|
|
2888
3312
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2889
|
-
mkdirSync3(
|
|
3313
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
2890
3314
|
writeFileSync3(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2891
3315
|
try {
|
|
2892
3316
|
chmodSync2(path, 384);
|
|
@@ -2953,6 +3377,7 @@ export {
|
|
|
2953
3377
|
formatLoopError,
|
|
2954
3378
|
harvest,
|
|
2955
3379
|
healLoadedMessages,
|
|
3380
|
+
inspectMcpServer,
|
|
2956
3381
|
isJsonRpcError,
|
|
2957
3382
|
isPlanStateEmpty,
|
|
2958
3383
|
isPlausibleKey,
|
|
@@ -2969,6 +3394,7 @@ export {
|
|
|
2969
3394
|
readTranscript,
|
|
2970
3395
|
recordFromLoopEvent,
|
|
2971
3396
|
redactKey,
|
|
3397
|
+
registerFilesystemTools,
|
|
2972
3398
|
renderMarkdown as renderDiffMarkdown,
|
|
2973
3399
|
renderSummaryTable as renderDiffSummary,
|
|
2974
3400
|
repairTruncatedJson,
|