reasonix 0.3.1 → 0.4.1

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((resolve2, reject) => {
51
- const timer = setTimeout(resolve2, ms);
50
+ return new Promise((resolve3, reject) => {
51
+ const timer = setTimeout(resolve3, ms);
52
52
  if (signal) {
53
53
  const onAbort = () => {
54
54
  clearTimeout(timer);
@@ -1020,6 +1020,11 @@ var DEEPSEEK_PRICING = {
1020
1020
  "deepseek-reasoner": { inputCacheHit: 0.14, inputCacheMiss: 0.55, output: 2.19 }
1021
1021
  };
1022
1022
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
1023
+ var DEEPSEEK_CONTEXT_TOKENS = {
1024
+ "deepseek-chat": 131072,
1025
+ "deepseek-reasoner": 131072
1026
+ };
1027
+ var DEFAULT_CONTEXT_TOKENS = 131072;
1023
1028
  function costUsd(model, usage) {
1024
1029
  const p = DEEPSEEK_PRICING[model];
1025
1030
  if (!p) return 0;
@@ -1102,12 +1107,18 @@ var CacheFirstLoop = class {
1102
1107
  resumedMessageCount;
1103
1108
  _turn = 0;
1104
1109
  _streamPreference;
1110
+ /**
1111
+ * Set by {@link abort} to short-circuit the tool-call loop after the
1112
+ * current iteration. Reset at the start of each `step()` so an Esc
1113
+ * during one turn doesn't poison the next.
1114
+ */
1115
+ _aborted = false;
1105
1116
  constructor(opts) {
1106
1117
  this.client = opts.client;
1107
1118
  this.prefix = opts.prefix;
1108
1119
  this.tools = opts.tools ?? new ToolRegistry();
1109
1120
  this.model = opts.model ?? "deepseek-chat";
1110
- this.maxToolIters = opts.maxToolIters ?? 24;
1121
+ this.maxToolIters = opts.maxToolIters ?? 64;
1111
1122
  if (typeof opts.branch === "number") {
1112
1123
  this.branchOptions = { budget: opts.branch };
1113
1124
  } else if (opts.branch && typeof opts.branch === "object") {
@@ -1213,12 +1224,42 @@ var CacheFirstLoop = class {
1213
1224
  if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
1214
1225
  return msgs;
1215
1226
  }
1227
+ /**
1228
+ * Signal the currently-running {@link step} that the user wants to
1229
+ * stop exploring. Takes effect at the next iteration boundary — if a
1230
+ * tool call is mid-flight it will be allowed to finish, then the
1231
+ * loop diverts to the forced-summary path so the user gets an
1232
+ * answer instead of a cliff. Called by the TUI on Esc.
1233
+ */
1234
+ abort() {
1235
+ this._aborted = true;
1236
+ }
1216
1237
  async *step(userInput) {
1217
1238
  this._turn++;
1218
1239
  this.scratch.reset();
1240
+ this._aborted = false;
1219
1241
  let pendingUser = userInput;
1220
1242
  const toolSpecs = this.prefix.tools();
1243
+ const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
1244
+ let warnedForIterBudget = false;
1221
1245
  for (let iter = 0; iter < this.maxToolIters; iter++) {
1246
+ if (this._aborted) {
1247
+ yield {
1248
+ turn: this._turn,
1249
+ role: "warning",
1250
+ content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 forcing summary from what was gathered`
1251
+ };
1252
+ yield* this.forceSummaryAfterIterLimit({ reason: "aborted" });
1253
+ return;
1254
+ }
1255
+ if (!warnedForIterBudget && iter >= warnAt) {
1256
+ warnedForIterBudget = true;
1257
+ yield {
1258
+ turn: this._turn,
1259
+ role: "warning",
1260
+ content: `${iter}/${this.maxToolIters} tool calls used \u2014 approaching budget. Press Esc to force a summary now.`
1261
+ };
1262
+ }
1222
1263
  const messages = this.buildMessages(pendingUser);
1223
1264
  let assistantContent = "";
1224
1265
  let reasoningContent = "";
@@ -1266,8 +1307,8 @@ var CacheFirstLoop = class {
1266
1307
  }
1267
1308
  );
1268
1309
  for (let k = 0; k < budget; k++) {
1269
- const sample = queue.shift() ?? await new Promise((resolve2) => {
1270
- waiter = resolve2;
1310
+ const sample = queue.shift() ?? await new Promise((resolve3) => {
1311
+ waiter = resolve3;
1271
1312
  });
1272
1313
  yield {
1273
1314
  turn: this._turn,
@@ -1387,9 +1428,28 @@ var CacheFirstLoop = class {
1387
1428
  yield { turn: this._turn, role: "done", content: assistantContent };
1388
1429
  return;
1389
1430
  }
1431
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
1432
+ if (usage && usage.promptTokens / ctxMax > 0.8) {
1433
+ yield {
1434
+ turn: this._turn,
1435
+ role: "warning",
1436
+ content: `context ${usage.promptTokens}/${ctxMax} (${Math.round(
1437
+ usage.promptTokens / ctxMax * 100
1438
+ )}%) \u2014 more tools would overflow. Forcing summary from what was gathered.`
1439
+ };
1440
+ yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
1441
+ return;
1442
+ }
1390
1443
  for (const call of repairedCalls) {
1391
1444
  const name = call.function?.name ?? "";
1392
1445
  const args = call.function?.arguments ?? "{}";
1446
+ yield {
1447
+ turn: this._turn,
1448
+ role: "tool_start",
1449
+ content: "",
1450
+ toolName: name,
1451
+ toolArgs: args
1452
+ };
1393
1453
  const result = await this.tools.dispatch(name, args);
1394
1454
  this.appendAndPersist({
1395
1455
  role: "tool",
@@ -1406,9 +1466,9 @@ var CacheFirstLoop = class {
1406
1466
  };
1407
1467
  }
1408
1468
  }
1409
- yield* this.forceSummaryAfterIterLimit();
1469
+ yield* this.forceSummaryAfterIterLimit({ reason: "budget" });
1410
1470
  }
1411
- async *forceSummaryAfterIterLimit() {
1471
+ async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
1412
1472
  try {
1413
1473
  const messages = this.buildMessages(null);
1414
1474
  const resp = await this.client.chat({
@@ -1417,7 +1477,8 @@ var CacheFirstLoop = class {
1417
1477
  // no tools → model is forced to answer in text
1418
1478
  });
1419
1479
  const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
1420
- const annotated = `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]
1480
+ const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
1481
+ const annotated = `${reasonPrefix}
1421
1482
 
1422
1483
  ${summary}`;
1423
1484
  const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
@@ -1426,15 +1487,17 @@ ${summary}`;
1426
1487
  turn: this._turn,
1427
1488
  role: "assistant_final",
1428
1489
  content: annotated,
1429
- stats: summaryStats
1490
+ stats: summaryStats,
1491
+ forcedSummary: true
1430
1492
  };
1431
1493
  yield { turn: this._turn, role: "done", content: summary };
1432
1494
  } catch (err) {
1495
+ const label = errorLabelFor(opts.reason, this.maxToolIters);
1433
1496
  yield {
1434
1497
  turn: this._turn,
1435
1498
  role: "error",
1436
1499
  content: "",
1437
- error: `tool-call budget (${this.maxToolIters}) reached and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or pass --max-tool-iters higher.`
1500
+ error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
1438
1501
  };
1439
1502
  yield { turn: this._turn, role: "done", content: "" };
1440
1503
  }
@@ -1454,6 +1517,18 @@ ${summary}`;
1454
1517
  return msg;
1455
1518
  }
1456
1519
  };
1520
+ function reasonPrefixFor(reason, iterCap) {
1521
+ if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
1522
+ if (reason === "context-guard") {
1523
+ return "[context budget running low \u2014 summarizing before the next call would overflow]";
1524
+ }
1525
+ return `[tool-call budget (${iterCap}) reached \u2014 forcing summary from what I found]`;
1526
+ }
1527
+ function errorLabelFor(reason, iterCap) {
1528
+ if (reason === "aborted") return "aborted by user";
1529
+ if (reason === "context-guard") return "context-guard triggered (prompt > 80% of window)";
1530
+ return `tool-call budget (${iterCap}) reached`;
1531
+ }
1457
1532
  function summarizeBranch(chosen, samples) {
1458
1533
  return {
1459
1534
  budget: samples.length,
@@ -2076,7 +2151,7 @@ var McpClient = class {
2076
2151
  async request(method, params) {
2077
2152
  const id = this.nextId++;
2078
2153
  const frame = { jsonrpc: "2.0", id, method, params };
2079
- const promise = new Promise((resolve2, reject) => {
2154
+ const promise = new Promise((resolve3, reject) => {
2080
2155
  const timeout = setTimeout(() => {
2081
2156
  this.pending.delete(id);
2082
2157
  reject(
@@ -2084,7 +2159,7 @@ var McpClient = class {
2084
2159
  );
2085
2160
  }, this.requestTimeoutMs);
2086
2161
  this.pending.set(id, {
2087
- resolve: resolve2,
2162
+ resolve: resolve3,
2088
2163
  reject,
2089
2164
  timeout
2090
2165
  });
@@ -2168,12 +2243,12 @@ var StdioTransport = class {
2168
2243
  }
2169
2244
  async send(message) {
2170
2245
  if (this.closed) throw new Error("MCP transport is closed");
2171
- return new Promise((resolve2, reject) => {
2246
+ return new Promise((resolve3, reject) => {
2172
2247
  const line = `${JSON.stringify(message)}
2173
2248
  `;
2174
2249
  this.child.stdin.write(line, "utf8", (err) => {
2175
2250
  if (err) reject(err);
2176
- else resolve2();
2251
+ else resolve3();
2177
2252
  });
2178
2253
  });
2179
2254
  }
@@ -2184,8 +2259,8 @@ var StdioTransport = class {
2184
2259
  continue;
2185
2260
  }
2186
2261
  if (this.closed) return;
2187
- const next = await new Promise((resolve2) => {
2188
- this.waiters.push(resolve2);
2262
+ const next = await new Promise((resolve3) => {
2263
+ this.waiters.push(resolve3);
2189
2264
  });
2190
2265
  if (next === null) return;
2191
2266
  yield next;
@@ -2251,8 +2326,8 @@ var SseTransport = class {
2251
2326
  constructor(opts) {
2252
2327
  this.url = opts.url;
2253
2328
  this.headers = opts.headers ?? {};
2254
- this.endpointReady = new Promise((resolve2, reject) => {
2255
- this.resolveEndpoint = resolve2;
2329
+ this.endpointReady = new Promise((resolve3, reject) => {
2330
+ this.resolveEndpoint = resolve3;
2256
2331
  this.rejectEndpoint = reject;
2257
2332
  });
2258
2333
  this.endpointReady.catch(() => void 0);
@@ -2279,8 +2354,8 @@ var SseTransport = class {
2279
2354
  continue;
2280
2355
  }
2281
2356
  if (this.closed) return;
2282
- const next = await new Promise((resolve2) => {
2283
- this.waiters.push(resolve2);
2357
+ const next = await new Promise((resolve3) => {
2358
+ this.waiters.push(resolve3);
2284
2359
  });
2285
2360
  if (next === null) return;
2286
2361
  yield next;
@@ -2448,16 +2523,215 @@ function parseMcpSpec(input) {
2448
2523
  return { transport: "stdio", name, command, args };
2449
2524
  }
2450
2525
 
2526
+ // src/code/edit-blocks.ts
2527
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
2528
+ import { dirname as dirname2, resolve as resolve2 } from "path";
2529
+ var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
2530
+ function parseEditBlocks(text) {
2531
+ const out = [];
2532
+ BLOCK_RE.lastIndex = 0;
2533
+ let m = BLOCK_RE.exec(text);
2534
+ while (m !== null) {
2535
+ out.push({
2536
+ path: m[1].trim(),
2537
+ search: m[2],
2538
+ replace: m[3],
2539
+ offset: m.index
2540
+ });
2541
+ m = BLOCK_RE.exec(text);
2542
+ }
2543
+ return out;
2544
+ }
2545
+ function applyEditBlock(block, rootDir) {
2546
+ const absRoot = resolve2(rootDir);
2547
+ const absTarget = resolve2(absRoot, block.path);
2548
+ if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
2549
+ return {
2550
+ path: block.path,
2551
+ status: "path-escape",
2552
+ message: `resolved path ${absTarget} is outside rootDir ${absRoot}`
2553
+ };
2554
+ }
2555
+ const searchEmpty = block.search.length === 0;
2556
+ const exists = existsSync2(absTarget);
2557
+ try {
2558
+ if (!exists) {
2559
+ if (!searchEmpty) {
2560
+ return {
2561
+ path: block.path,
2562
+ status: "file-missing",
2563
+ message: "file does not exist; to create it, use an empty SEARCH block"
2564
+ };
2565
+ }
2566
+ mkdirSync2(dirname2(absTarget), { recursive: true });
2567
+ writeFileSync2(absTarget, block.replace, "utf8");
2568
+ return { path: block.path, status: "created" };
2569
+ }
2570
+ const content = readFileSync4(absTarget, "utf8");
2571
+ if (searchEmpty) {
2572
+ return {
2573
+ path: block.path,
2574
+ status: "not-found",
2575
+ message: "empty SEARCH only creates new files \u2014 this file already exists"
2576
+ };
2577
+ }
2578
+ const idx = content.indexOf(block.search);
2579
+ if (idx === -1) {
2580
+ return {
2581
+ path: block.path,
2582
+ status: "not-found",
2583
+ message: "SEARCH text does not match the current file content exactly"
2584
+ };
2585
+ }
2586
+ const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
2587
+ writeFileSync2(absTarget, replaced, "utf8");
2588
+ return { path: block.path, status: "applied" };
2589
+ } catch (err) {
2590
+ return { path: block.path, status: "error", message: err.message };
2591
+ }
2592
+ }
2593
+ function applyEditBlocks(blocks, rootDir) {
2594
+ return blocks.map((b) => applyEditBlock(b, rootDir));
2595
+ }
2596
+ function snapshotBeforeEdits(blocks, rootDir) {
2597
+ const absRoot = resolve2(rootDir);
2598
+ const seen = /* @__PURE__ */ new Set();
2599
+ const snapshots = [];
2600
+ for (const b of blocks) {
2601
+ if (seen.has(b.path)) continue;
2602
+ seen.add(b.path);
2603
+ const abs = resolve2(absRoot, b.path);
2604
+ if (!existsSync2(abs)) {
2605
+ snapshots.push({ path: b.path, prevContent: null });
2606
+ continue;
2607
+ }
2608
+ try {
2609
+ snapshots.push({ path: b.path, prevContent: readFileSync4(abs, "utf8") });
2610
+ } catch {
2611
+ snapshots.push({ path: b.path, prevContent: null });
2612
+ }
2613
+ }
2614
+ return snapshots;
2615
+ }
2616
+ function restoreSnapshots(snapshots, rootDir) {
2617
+ const absRoot = resolve2(rootDir);
2618
+ return snapshots.map((snap) => {
2619
+ const abs = resolve2(absRoot, snap.path);
2620
+ if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
2621
+ return {
2622
+ path: snap.path,
2623
+ status: "path-escape",
2624
+ message: "snapshot path escapes rootDir \u2014 refusing to restore"
2625
+ };
2626
+ }
2627
+ try {
2628
+ if (snap.prevContent === null) {
2629
+ if (existsSync2(abs)) unlinkSync2(abs);
2630
+ return {
2631
+ path: snap.path,
2632
+ status: "applied",
2633
+ message: "removed (the edit had created it)"
2634
+ };
2635
+ }
2636
+ writeFileSync2(abs, snap.prevContent, "utf8");
2637
+ return {
2638
+ path: snap.path,
2639
+ status: "applied",
2640
+ message: "restored to pre-edit content"
2641
+ };
2642
+ } catch (err) {
2643
+ return { path: snap.path, status: "error", message: err.message };
2644
+ }
2645
+ });
2646
+ }
2647
+ function sep() {
2648
+ return process.platform === "win32" ? "\\" : "/";
2649
+ }
2650
+
2651
+ // src/code/prompt.ts
2652
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
2653
+ import { join as join2 } from "path";
2654
+ 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.
2655
+
2656
+ # When to edit vs. when to explore
2657
+
2658
+ Only propose edits when the user explicitly asks you to change, fix, add, remove, refactor, or write something. Do NOT propose edits when the user asks you to:
2659
+ - analyze, read, explore, describe, or summarize a project
2660
+ - explain how something works
2661
+ - answer a question about the code
2662
+
2663
+ In those cases, use tools to gather what you need, then reply in prose. No SEARCH/REPLACE blocks, no file changes. If you're unsure what the user wants, ask.
2664
+
2665
+ When you do propose edits, the user will review them and decide whether to \`/apply\` or \`/discard\`. Don't assume they'll accept \u2014 write as if each edit will be audited, because it will.
2666
+
2667
+ # Editing files
2668
+
2669
+ When you've been asked to change a file, output one or more SEARCH/REPLACE blocks in this exact format:
2670
+
2671
+ path/to/file.ext
2672
+ <<<<<<< SEARCH
2673
+ exact existing lines from the file, including whitespace
2674
+ =======
2675
+ the new lines
2676
+ >>>>>>> REPLACE
2677
+
2678
+ Rules:
2679
+ - Always read_file first so your SEARCH matches byte-for-byte. If it doesn't match, the edit is rejected and you'll have to retry with the exact current content.
2680
+ - One edit per block. Multiple blocks in one response are fine.
2681
+ - To create a new file, leave SEARCH empty:
2682
+ path/to/new.ts
2683
+ <<<<<<< SEARCH
2684
+ =======
2685
+ (whole file content here)
2686
+ >>>>>>> REPLACE
2687
+ - Do NOT use write_file to change existing files \u2014 the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).
2688
+ - Paths are relative to the working directory. Don't use absolute paths.
2689
+
2690
+ # Exploration
2691
+
2692
+ - Avoid listing or reading inside these common dependency / build directories unless the user explicitly asks about them: node_modules, dist, build, out, .next, .nuxt, .svelte-kit, .git, .venv, venv, __pycache__, target, coverage, .turbo, .cache. They're expensive and usually irrelevant.
2693
+ - Prefer search_files / grep over list_directory when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees.
2694
+
2695
+ # Style
2696
+
2697
+ - Show edits; don't narrate them in prose. "Here's the fix:" is enough.
2698
+ - One short paragraph explaining *why*, then the blocks.
2699
+ - If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
2700
+ `;
2701
+ function codeSystemPrompt(rootDir) {
2702
+ const gitignorePath = join2(rootDir, ".gitignore");
2703
+ if (!existsSync3(gitignorePath)) return CODE_SYSTEM_PROMPT;
2704
+ let content;
2705
+ try {
2706
+ content = readFileSync5(gitignorePath, "utf8");
2707
+ } catch {
2708
+ return CODE_SYSTEM_PROMPT;
2709
+ }
2710
+ const MAX = 2e3;
2711
+ const truncated = content.length > MAX ? `${content.slice(0, MAX)}
2712
+ \u2026 (truncated ${content.length - MAX} chars)` : content;
2713
+ return `${CODE_SYSTEM_PROMPT}
2714
+
2715
+ # Project .gitignore
2716
+
2717
+ The user's repo ships this .gitignore \u2014 treat every pattern as "don't traverse or edit inside these paths unless explicitly asked":
2718
+
2719
+ \`\`\`
2720
+ ${truncated}
2721
+ \`\`\`
2722
+ `;
2723
+ }
2724
+
2451
2725
  // src/config.ts
2452
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2726
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2453
2727
  import { homedir as homedir2 } from "os";
2454
- import { dirname as dirname2, join as join2 } from "path";
2728
+ import { dirname as dirname3, join as join3 } from "path";
2455
2729
  function defaultConfigPath() {
2456
- return join2(homedir2(), ".reasonix", "config.json");
2730
+ return join3(homedir2(), ".reasonix", "config.json");
2457
2731
  }
2458
2732
  function readConfig(path = defaultConfigPath()) {
2459
2733
  try {
2460
- const raw = readFileSync4(path, "utf8");
2734
+ const raw = readFileSync6(path, "utf8");
2461
2735
  const parsed = JSON.parse(raw);
2462
2736
  if (parsed && typeof parsed === "object") return parsed;
2463
2737
  } catch {
@@ -2465,8 +2739,8 @@ function readConfig(path = defaultConfigPath()) {
2465
2739
  return {};
2466
2740
  }
2467
2741
  function writeConfig(cfg, path = defaultConfigPath()) {
2468
- mkdirSync2(dirname2(path), { recursive: true });
2469
- writeFileSync2(path, JSON.stringify(cfg, null, 2), "utf8");
2742
+ mkdirSync3(dirname3(path), { recursive: true });
2743
+ writeFileSync3(path, JSON.stringify(cfg, null, 2), "utf8");
2470
2744
  try {
2471
2745
  chmodSync2(path, 384);
2472
2746
  } catch {
@@ -2492,9 +2766,10 @@ function redactKey(key) {
2492
2766
  }
2493
2767
 
2494
2768
  // src/index.ts
2495
- var VERSION = "0.3.1";
2769
+ var VERSION = "0.4.1";
2496
2770
  export {
2497
2771
  AppendOnlyLog,
2772
+ CODE_SYSTEM_PROMPT,
2498
2773
  CacheFirstLoop,
2499
2774
  DEFAULT_MAX_RESULT_CHARS,
2500
2775
  DeepSeekClient,
@@ -2513,8 +2788,11 @@ export {
2513
2788
  aggregateBranchUsage,
2514
2789
  analyzeSchema,
2515
2790
  appendSessionMessage,
2791
+ applyEditBlock,
2792
+ applyEditBlocks,
2516
2793
  bridgeMcpTools,
2517
2794
  claudeEquivalentCost,
2795
+ codeSystemPrompt,
2518
2796
  computeReplayStats,
2519
2797
  costUsd,
2520
2798
  defaultConfigPath,
@@ -2537,6 +2815,7 @@ export {
2537
2815
  loadSessionMessages,
2538
2816
  nestArguments,
2539
2817
  openTranscriptFile,
2818
+ parseEditBlocks,
2540
2819
  parseMcpSpec,
2541
2820
  parseTranscript,
2542
2821
  readConfig,
@@ -2547,6 +2826,7 @@ export {
2547
2826
  renderSummaryTable as renderDiffSummary,
2548
2827
  repairTruncatedJson,
2549
2828
  replayFromFile,
2829
+ restoreSnapshots,
2550
2830
  runBranches,
2551
2831
  sanitizeName as sanitizeSessionName,
2552
2832
  saveApiKey,
@@ -2554,6 +2834,7 @@ export {
2554
2834
  sessionPath,
2555
2835
  sessionsDir,
2556
2836
  similarity,
2837
+ snapshotBeforeEdits,
2557
2838
  truncateForModel,
2558
2839
  writeConfig,
2559
2840
  writeMeta,