reasonix 0.3.2 → 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.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CODE_SYSTEM_PROMPT,
4
+ codeSystemPrompt
5
+ } from "./chunk-2P2MZLCE.js";
6
+ export {
7
+ CODE_SYSTEM_PROMPT,
8
+ codeSystemPrompt
9
+ };
10
+ //# sourceMappingURL=prompt-MMANQ36Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.d.ts CHANGED
@@ -433,7 +433,15 @@ declare class ToolRegistry {
433
433
  dispatch(name: string, argumentsRaw: string | Record<string, unknown>): Promise<string>;
434
434
  }
435
435
 
436
- type EventRole = "assistant_delta" | "assistant_final" | "tool" | "done" | "error" | "warning" | "branch_start" | "branch_progress" | "branch_done";
436
+ type EventRole = "assistant_delta" | "assistant_final"
437
+ /**
438
+ * Yielded immediately before a tool is dispatched. Lets the TUI put
439
+ * up a "▸ tool<X> running…" spinner while the tool's Promise is
440
+ * pending — otherwise the UI looks frozen whenever a tool call
441
+ * takes more than a few hundred ms (a big `filesystem_edit_file`
442
+ * is a typical trigger).
443
+ */
444
+ | "tool_start" | "tool" | "done" | "error" | "warning" | "branch_start" | "branch_progress" | "branch_done";
437
445
  interface BranchSummary {
438
446
  budget: number;
439
447
  chosenIndex: number;
@@ -465,6 +473,16 @@ interface LoopEvent {
465
473
  branch?: BranchSummary;
466
474
  branchProgress?: BranchProgress;
467
475
  error?: string;
476
+ /**
477
+ * True on `assistant_final` events emitted by the no-tools fallback
478
+ * when the loop hit its budget, was aborted, or tripped the
479
+ * token-context guard. Consumers that act on assistant text (notably
480
+ * the code-mode edit applier) MUST treat these as display-only —
481
+ * the model is "wrapping up," not proposing new work. Applying
482
+ * SEARCH/REPLACE blocks found in a forced summary caused the
483
+ * "analysis became edits" bug in v0.4.1 and earlier.
484
+ */
485
+ forcedSummary?: boolean;
468
486
  }
469
487
  interface CacheFirstLoopOptions {
470
488
  client: DeepSeekClient;
@@ -1238,6 +1256,113 @@ interface SseMcpSpec {
1238
1256
  type McpSpec = StdioMcpSpec | SseMcpSpec;
1239
1257
  declare function parseMcpSpec(input: string): McpSpec;
1240
1258
 
1259
+ /**
1260
+ * Aider-style SEARCH/REPLACE edit blocks.
1261
+ *
1262
+ * The model emits blocks in this exact shape, one or more per response:
1263
+ *
1264
+ * path/to/file.ts
1265
+ * <<<<<<< SEARCH
1266
+ * exact existing lines (whitespace-sensitive)
1267
+ * =======
1268
+ * replacement lines
1269
+ * >>>>>>> REPLACE
1270
+ *
1271
+ * We chose this over unified diffs because:
1272
+ * - Models produce it reliably — no line-number drift.
1273
+ * - It tolerates multi-edit responses without ambiguity over which
1274
+ * hunk belongs to which file.
1275
+ * - Aider has years of evidence that this format works even against
1276
+ * weaker models than DeepSeek R1, so it's a conservative pick.
1277
+ *
1278
+ * The SEARCH text must match the file byte-for-byte. Empty SEARCH is a
1279
+ * sentinel for "create new file" — the REPLACE becomes the whole file.
1280
+ * If SEARCH doesn't match we refuse the edit and surface the failure;
1281
+ * we do NOT guess or fuzzy-match. A wrong silent edit is worse than a
1282
+ * missing one — the user can re-ask with the exact current content.
1283
+ */
1284
+ interface EditBlock {
1285
+ /** Path as written by the model — relative to rootDir, or absolute. */
1286
+ path: string;
1287
+ /** Literal text to match in the target file. Empty → create new file. */
1288
+ search: string;
1289
+ /** Replacement text to write in place of `search`. */
1290
+ replace: string;
1291
+ /** Char offset in the source message where this block started. */
1292
+ offset: number;
1293
+ }
1294
+ type ApplyStatus =
1295
+ /** Edit landed on disk. */
1296
+ "applied"
1297
+ /** New file created (SEARCH was empty and file didn't exist). */
1298
+ | "created"
1299
+ /** File exists but SEARCH block wasn't found in its content. */
1300
+ | "not-found"
1301
+ /** File doesn't exist and SEARCH was non-empty (can't create without content). */
1302
+ | "file-missing"
1303
+ /** Path escapes rootDir — refused on safety grounds. */
1304
+ | "path-escape"
1305
+ /** fs write / read threw. */
1306
+ | "error";
1307
+ interface ApplyResult {
1308
+ path: string;
1309
+ status: ApplyStatus;
1310
+ /** Extra detail (e.g. error message) for logs. */
1311
+ message?: string;
1312
+ }
1313
+ declare function parseEditBlocks(text: string): EditBlock[];
1314
+ declare function applyEditBlock(block: EditBlock, rootDir: string): ApplyResult;
1315
+ declare function applyEditBlocks(blocks: EditBlock[], rootDir: string): ApplyResult[];
1316
+ interface EditSnapshot {
1317
+ /** Path relative to rootDir, as the block named it. */
1318
+ path: string;
1319
+ /**
1320
+ * File content before the edit batch was applied. `null` means the
1321
+ * file didn't exist yet — restoring that means deleting whatever the
1322
+ * edit created.
1323
+ */
1324
+ prevContent: string | null;
1325
+ }
1326
+ /**
1327
+ * Capture the current state of every file an edit batch is about to
1328
+ * touch, so `/undo` can roll back if the user doesn't like the result.
1329
+ * De-duplicates by path because one batch can contain multiple blocks
1330
+ * for the same file, and we only want one "before" snapshot per file.
1331
+ */
1332
+ declare function snapshotBeforeEdits(blocks: EditBlock[], rootDir: string): EditSnapshot[];
1333
+ /**
1334
+ * Restore files to their snapshotted state. Snapshots with
1335
+ * `prevContent === null` were created by the edit, so undo = delete.
1336
+ * Otherwise the prior content is written back, replacing whatever the
1337
+ * edit left behind.
1338
+ */
1339
+ declare function restoreSnapshots(snapshots: EditSnapshot[], rootDir: string): ApplyResult[];
1340
+
1341
+ /**
1342
+ * System prompt used by `reasonix code`. Teaches the model:
1343
+ *
1344
+ * 1. It has a filesystem MCP bridge rooted at the user's CWD.
1345
+ * 2. To modify files it emits SEARCH/REPLACE blocks (not
1346
+ * `write_file` — that would whole-file rewrite and kill diff
1347
+ * reviewability).
1348
+ * 3. Read first, edit second — SEARCH must match byte-for-byte.
1349
+ * 4. Be concise. The user can read a diff faster than prose.
1350
+ *
1351
+ * Kept short on purpose. Long system prompts eat context budget that
1352
+ * the Cache-First Loop is trying to conserve. The SEARCH/REPLACE spec
1353
+ * is the one unavoidable bloat; we trim everything else.
1354
+ */
1355
+ declare const 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.\n\n# When to edit vs. when to explore\n\nOnly 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:\n- analyze, read, explore, describe, or summarize a project\n- explain how something works\n- answer a question about the code\n\nIn 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.\n\nWhen 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.\n\n# Editing files\n\nWhen you've been asked to change a file, output one or more SEARCH/REPLACE blocks in this exact format:\n\npath/to/file.ext\n<<<<<<< SEARCH\nexact existing lines from the file, including whitespace\n=======\nthe new lines\n>>>>>>> REPLACE\n\nRules:\n- 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.\n- One edit per block. Multiple blocks in one response are fine.\n- To create a new file, leave SEARCH empty:\n path/to/new.ts\n <<<<<<< SEARCH\n =======\n (whole file content here)\n >>>>>>> REPLACE\n- 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).\n- Paths are relative to the working directory. Don't use absolute paths.\n\n# Exploration\n\n- 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.\n- 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.\n\n# Style\n\n- Show edits; don't narrate them in prose. \"Here's the fix:\" is enough.\n- One short paragraph explaining *why*, then the blocks.\n- If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.\n";
1356
+ /**
1357
+ * Inject the project's `.gitignore` content into the system prompt as a
1358
+ * "respect this on top of the built-in denylist" hint. We don't parse
1359
+ * the file — we hand it to the model as-is. Truncate long ones so we
1360
+ * don't eat context budget on huge generated ignore lists.
1361
+ *
1362
+ * Missing or unreadable .gitignore → returns the base prompt unchanged.
1363
+ */
1364
+ declare function codeSystemPrompt(rootDir: string): string;
1365
+
1241
1366
  /**
1242
1367
  * User-level config storage for the Reasonix CLI.
1243
1368
  *
@@ -1291,6 +1416,6 @@ declare function redactKey(key: string): string;
1291
1416
 
1292
1417
  /** Reasonix — DeepSeek-native agent framework. Library entry point. */
1293
1418
 
1294
- declare const VERSION = "0.3.2";
1419
+ declare const VERSION = "0.4.1";
1295
1420
 
1296
- export { AppendOnlyLog, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EventRole, type FlattenDecision, type FlattenOptions, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, type ListToolsResult, type LoopEvent, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, type SessionInfo, SessionStats, type SessionSummary, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, aggregateBranchUsage, analyzeSchema, appendSessionMessage, bridgeMcpTools, claudeEquivalentCost, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatLoopError, harvest, healLoadedMessages, isJsonRpcError, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, parseMcpSpec, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, runBranches, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, truncateForModel, writeConfig, writeMeta, writeRecord };
1421
+ export { AppendOnlyLog, type ApplyResult, type ApplyStatus, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FlattenDecision, type FlattenOptions, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, type ListToolsResult, type LoopEvent, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, type SessionInfo, SessionStats, type SessionSummary, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, aggregateBranchUsage, analyzeSchema, appendSessionMessage, applyEditBlock, applyEditBlocks, bridgeMcpTools, claudeEquivalentCost, codeSystemPrompt, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatLoopError, harvest, healLoadedMessages, isJsonRpcError, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, parseEditBlocks, parseMcpSpec, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, restoreSnapshots, runBranches, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, truncateForModel, writeConfig, writeMeta, writeRecord };
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;
@@ -1113,7 +1118,7 @@ var CacheFirstLoop = class {
1113
1118
  this.prefix = opts.prefix;
1114
1119
  this.tools = opts.tools ?? new ToolRegistry();
1115
1120
  this.model = opts.model ?? "deepseek-chat";
1116
- this.maxToolIters = opts.maxToolIters ?? 24;
1121
+ this.maxToolIters = opts.maxToolIters ?? 64;
1117
1122
  if (typeof opts.branch === "number") {
1118
1123
  this.branchOptions = { budget: opts.branch };
1119
1124
  } else if (opts.branch && typeof opts.branch === "object") {
@@ -1302,8 +1307,8 @@ var CacheFirstLoop = class {
1302
1307
  }
1303
1308
  );
1304
1309
  for (let k = 0; k < budget; k++) {
1305
- const sample = queue.shift() ?? await new Promise((resolve2) => {
1306
- waiter = resolve2;
1310
+ const sample = queue.shift() ?? await new Promise((resolve3) => {
1311
+ waiter = resolve3;
1307
1312
  });
1308
1313
  yield {
1309
1314
  turn: this._turn,
@@ -1423,9 +1428,28 @@ var CacheFirstLoop = class {
1423
1428
  yield { turn: this._turn, role: "done", content: assistantContent };
1424
1429
  return;
1425
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
+ }
1426
1443
  for (const call of repairedCalls) {
1427
1444
  const name = call.function?.name ?? "";
1428
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
+ };
1429
1453
  const result = await this.tools.dispatch(name, args);
1430
1454
  this.appendAndPersist({
1431
1455
  role: "tool",
@@ -1453,7 +1477,7 @@ var CacheFirstLoop = class {
1453
1477
  // no tools → model is forced to answer in text
1454
1478
  });
1455
1479
  const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
1456
- const reasonPrefix = opts.reason === "aborted" ? "[aborted by user (Esc) \u2014 summarizing what I found so far]" : `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]`;
1480
+ const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
1457
1481
  const annotated = `${reasonPrefix}
1458
1482
 
1459
1483
  ${summary}`;
@@ -1463,11 +1487,12 @@ ${summary}`;
1463
1487
  turn: this._turn,
1464
1488
  role: "assistant_final",
1465
1489
  content: annotated,
1466
- stats: summaryStats
1490
+ stats: summaryStats,
1491
+ forcedSummary: true
1467
1492
  };
1468
1493
  yield { turn: this._turn, role: "done", content: summary };
1469
1494
  } catch (err) {
1470
- const label = opts.reason === "aborted" ? "aborted by user" : `tool-call budget (${this.maxToolIters}) reached`;
1495
+ const label = errorLabelFor(opts.reason, this.maxToolIters);
1471
1496
  yield {
1472
1497
  turn: this._turn,
1473
1498
  role: "error",
@@ -1492,6 +1517,18 @@ ${summary}`;
1492
1517
  return msg;
1493
1518
  }
1494
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
+ }
1495
1532
  function summarizeBranch(chosen, samples) {
1496
1533
  return {
1497
1534
  budget: samples.length,
@@ -2114,7 +2151,7 @@ var McpClient = class {
2114
2151
  async request(method, params) {
2115
2152
  const id = this.nextId++;
2116
2153
  const frame = { jsonrpc: "2.0", id, method, params };
2117
- const promise = new Promise((resolve2, reject) => {
2154
+ const promise = new Promise((resolve3, reject) => {
2118
2155
  const timeout = setTimeout(() => {
2119
2156
  this.pending.delete(id);
2120
2157
  reject(
@@ -2122,7 +2159,7 @@ var McpClient = class {
2122
2159
  );
2123
2160
  }, this.requestTimeoutMs);
2124
2161
  this.pending.set(id, {
2125
- resolve: resolve2,
2162
+ resolve: resolve3,
2126
2163
  reject,
2127
2164
  timeout
2128
2165
  });
@@ -2206,12 +2243,12 @@ var StdioTransport = class {
2206
2243
  }
2207
2244
  async send(message) {
2208
2245
  if (this.closed) throw new Error("MCP transport is closed");
2209
- return new Promise((resolve2, reject) => {
2246
+ return new Promise((resolve3, reject) => {
2210
2247
  const line = `${JSON.stringify(message)}
2211
2248
  `;
2212
2249
  this.child.stdin.write(line, "utf8", (err) => {
2213
2250
  if (err) reject(err);
2214
- else resolve2();
2251
+ else resolve3();
2215
2252
  });
2216
2253
  });
2217
2254
  }
@@ -2222,8 +2259,8 @@ var StdioTransport = class {
2222
2259
  continue;
2223
2260
  }
2224
2261
  if (this.closed) return;
2225
- const next = await new Promise((resolve2) => {
2226
- this.waiters.push(resolve2);
2262
+ const next = await new Promise((resolve3) => {
2263
+ this.waiters.push(resolve3);
2227
2264
  });
2228
2265
  if (next === null) return;
2229
2266
  yield next;
@@ -2289,8 +2326,8 @@ var SseTransport = class {
2289
2326
  constructor(opts) {
2290
2327
  this.url = opts.url;
2291
2328
  this.headers = opts.headers ?? {};
2292
- this.endpointReady = new Promise((resolve2, reject) => {
2293
- this.resolveEndpoint = resolve2;
2329
+ this.endpointReady = new Promise((resolve3, reject) => {
2330
+ this.resolveEndpoint = resolve3;
2294
2331
  this.rejectEndpoint = reject;
2295
2332
  });
2296
2333
  this.endpointReady.catch(() => void 0);
@@ -2317,8 +2354,8 @@ var SseTransport = class {
2317
2354
  continue;
2318
2355
  }
2319
2356
  if (this.closed) return;
2320
- const next = await new Promise((resolve2) => {
2321
- this.waiters.push(resolve2);
2357
+ const next = await new Promise((resolve3) => {
2358
+ this.waiters.push(resolve3);
2322
2359
  });
2323
2360
  if (next === null) return;
2324
2361
  yield next;
@@ -2486,16 +2523,215 @@ function parseMcpSpec(input) {
2486
2523
  return { transport: "stdio", name, command, args };
2487
2524
  }
2488
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
+
2489
2725
  // src/config.ts
2490
- 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";
2491
2727
  import { homedir as homedir2 } from "os";
2492
- import { dirname as dirname2, join as join2 } from "path";
2728
+ import { dirname as dirname3, join as join3 } from "path";
2493
2729
  function defaultConfigPath() {
2494
- return join2(homedir2(), ".reasonix", "config.json");
2730
+ return join3(homedir2(), ".reasonix", "config.json");
2495
2731
  }
2496
2732
  function readConfig(path = defaultConfigPath()) {
2497
2733
  try {
2498
- const raw = readFileSync4(path, "utf8");
2734
+ const raw = readFileSync6(path, "utf8");
2499
2735
  const parsed = JSON.parse(raw);
2500
2736
  if (parsed && typeof parsed === "object") return parsed;
2501
2737
  } catch {
@@ -2503,8 +2739,8 @@ function readConfig(path = defaultConfigPath()) {
2503
2739
  return {};
2504
2740
  }
2505
2741
  function writeConfig(cfg, path = defaultConfigPath()) {
2506
- mkdirSync2(dirname2(path), { recursive: true });
2507
- writeFileSync2(path, JSON.stringify(cfg, null, 2), "utf8");
2742
+ mkdirSync3(dirname3(path), { recursive: true });
2743
+ writeFileSync3(path, JSON.stringify(cfg, null, 2), "utf8");
2508
2744
  try {
2509
2745
  chmodSync2(path, 384);
2510
2746
  } catch {
@@ -2530,9 +2766,10 @@ function redactKey(key) {
2530
2766
  }
2531
2767
 
2532
2768
  // src/index.ts
2533
- var VERSION = "0.3.2";
2769
+ var VERSION = "0.4.1";
2534
2770
  export {
2535
2771
  AppendOnlyLog,
2772
+ CODE_SYSTEM_PROMPT,
2536
2773
  CacheFirstLoop,
2537
2774
  DEFAULT_MAX_RESULT_CHARS,
2538
2775
  DeepSeekClient,
@@ -2551,8 +2788,11 @@ export {
2551
2788
  aggregateBranchUsage,
2552
2789
  analyzeSchema,
2553
2790
  appendSessionMessage,
2791
+ applyEditBlock,
2792
+ applyEditBlocks,
2554
2793
  bridgeMcpTools,
2555
2794
  claudeEquivalentCost,
2795
+ codeSystemPrompt,
2556
2796
  computeReplayStats,
2557
2797
  costUsd,
2558
2798
  defaultConfigPath,
@@ -2575,6 +2815,7 @@ export {
2575
2815
  loadSessionMessages,
2576
2816
  nestArguments,
2577
2817
  openTranscriptFile,
2818
+ parseEditBlocks,
2578
2819
  parseMcpSpec,
2579
2820
  parseTranscript,
2580
2821
  readConfig,
@@ -2585,6 +2826,7 @@ export {
2585
2826
  renderSummaryTable as renderDiffSummary,
2586
2827
  repairTruncatedJson,
2587
2828
  replayFromFile,
2829
+ restoreSnapshots,
2588
2830
  runBranches,
2589
2831
  sanitizeName as sanitizeSessionName,
2590
2832
  saveApiKey,
@@ -2592,6 +2834,7 @@ export {
2592
2834
  sessionPath,
2593
2835
  sessionsDir,
2594
2836
  similarity,
2837
+ snapshotBeforeEdits,
2595
2838
  truncateForModel,
2596
2839
  writeConfig,
2597
2840
  writeMeta,