reasonix 0.3.2 → 0.4.3
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/README.md +32 -0
- package/dist/cli/chunk-2P2MZLCE.js +81 -0
- package/dist/cli/chunk-2P2MZLCE.js.map +1 -0
- package/dist/cli/index.js +661 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt-MMANQ36Z.js +10 -0
- package/dist/cli/prompt-MMANQ36Z.js.map +1 -0
- package/dist/index.d.ts +151 -3
- package/dist/index.js +327 -28
- 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((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 ??
|
|
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") {
|
|
@@ -1229,6 +1234,39 @@ var CacheFirstLoop = class {
|
|
|
1229
1234
|
abort() {
|
|
1230
1235
|
this._aborted = true;
|
|
1231
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Drop everything in the log after (and including) the most recent
|
|
1239
|
+
* user message. Used by `/retry` so the caller can re-send that
|
|
1240
|
+
* message with a fresh turn instead of layering another response on
|
|
1241
|
+
* top of the prior exchange. Returns the content of the dropped user
|
|
1242
|
+
* message, or `null` if there isn't one yet.
|
|
1243
|
+
*
|
|
1244
|
+
* Persists by rewriting the session file — otherwise the next
|
|
1245
|
+
* launch would rehydrate the old exchange and `/retry` would seem
|
|
1246
|
+
* to have done nothing.
|
|
1247
|
+
*/
|
|
1248
|
+
retryLastUser() {
|
|
1249
|
+
const entries = this.log.entries;
|
|
1250
|
+
let lastUserIdx = -1;
|
|
1251
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1252
|
+
if (entries[i].role === "user") {
|
|
1253
|
+
lastUserIdx = i;
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (lastUserIdx < 0) return null;
|
|
1258
|
+
const raw = entries[lastUserIdx].content;
|
|
1259
|
+
const userText = typeof raw === "string" ? raw : "";
|
|
1260
|
+
const preserved = entries.slice(0, lastUserIdx).map((m) => ({ ...m }));
|
|
1261
|
+
this.log.compactInPlace(preserved);
|
|
1262
|
+
if (this.sessionName) {
|
|
1263
|
+
try {
|
|
1264
|
+
rewriteSession(this.sessionName, preserved);
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return userText;
|
|
1269
|
+
}
|
|
1232
1270
|
async *step(userInput) {
|
|
1233
1271
|
this._turn++;
|
|
1234
1272
|
this.scratch.reset();
|
|
@@ -1242,9 +1280,17 @@ var CacheFirstLoop = class {
|
|
|
1242
1280
|
yield {
|
|
1243
1281
|
turn: this._turn,
|
|
1244
1282
|
role: "warning",
|
|
1245
|
-
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014
|
|
1283
|
+
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)`
|
|
1246
1284
|
};
|
|
1247
|
-
|
|
1285
|
+
const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
|
|
1286
|
+
this.appendAndPersist({ role: "assistant", content: stoppedMsg });
|
|
1287
|
+
yield {
|
|
1288
|
+
turn: this._turn,
|
|
1289
|
+
role: "assistant_final",
|
|
1290
|
+
content: stoppedMsg,
|
|
1291
|
+
forcedSummary: true
|
|
1292
|
+
};
|
|
1293
|
+
yield { turn: this._turn, role: "done", content: stoppedMsg };
|
|
1248
1294
|
return;
|
|
1249
1295
|
}
|
|
1250
1296
|
if (!warnedForIterBudget && iter >= warnAt) {
|
|
@@ -1302,8 +1348,8 @@ var CacheFirstLoop = class {
|
|
|
1302
1348
|
}
|
|
1303
1349
|
);
|
|
1304
1350
|
for (let k = 0; k < budget; k++) {
|
|
1305
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1306
|
-
waiter =
|
|
1351
|
+
const sample = queue.shift() ?? await new Promise((resolve3) => {
|
|
1352
|
+
waiter = resolve3;
|
|
1307
1353
|
});
|
|
1308
1354
|
yield {
|
|
1309
1355
|
turn: this._turn,
|
|
@@ -1423,9 +1469,28 @@ var CacheFirstLoop = class {
|
|
|
1423
1469
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
1424
1470
|
return;
|
|
1425
1471
|
}
|
|
1472
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1473
|
+
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
1474
|
+
yield {
|
|
1475
|
+
turn: this._turn,
|
|
1476
|
+
role: "warning",
|
|
1477
|
+
content: `context ${usage.promptTokens}/${ctxMax} (${Math.round(
|
|
1478
|
+
usage.promptTokens / ctxMax * 100
|
|
1479
|
+
)}%) \u2014 more tools would overflow. Forcing summary from what was gathered.`
|
|
1480
|
+
};
|
|
1481
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1426
1484
|
for (const call of repairedCalls) {
|
|
1427
1485
|
const name = call.function?.name ?? "";
|
|
1428
1486
|
const args = call.function?.arguments ?? "{}";
|
|
1487
|
+
yield {
|
|
1488
|
+
turn: this._turn,
|
|
1489
|
+
role: "tool_start",
|
|
1490
|
+
content: "",
|
|
1491
|
+
toolName: name,
|
|
1492
|
+
toolArgs: args
|
|
1493
|
+
};
|
|
1429
1494
|
const result = await this.tools.dispatch(name, args);
|
|
1430
1495
|
this.appendAndPersist({
|
|
1431
1496
|
role: "tool",
|
|
@@ -1447,13 +1512,19 @@ var CacheFirstLoop = class {
|
|
|
1447
1512
|
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
1448
1513
|
try {
|
|
1449
1514
|
const messages = this.buildMessages(null);
|
|
1515
|
+
messages.push({
|
|
1516
|
+
role: "user",
|
|
1517
|
+
content: "I'm out of tool-call budget for this turn. Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
|
|
1518
|
+
});
|
|
1450
1519
|
const resp = await this.client.chat({
|
|
1451
1520
|
model: this.model,
|
|
1452
1521
|
messages
|
|
1453
1522
|
// no tools → model is forced to answer in text
|
|
1454
1523
|
});
|
|
1455
|
-
const
|
|
1456
|
-
const
|
|
1524
|
+
const rawContent = resp.content?.trim() ?? "";
|
|
1525
|
+
const cleaned = stripHallucinatedToolMarkup(rawContent);
|
|
1526
|
+
const summary = cleaned || "(model emitted fake tool-call markup instead of a prose summary \u2014 try /retry with a narrower question, or /think to inspect R1's reasoning)";
|
|
1527
|
+
const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
|
|
1457
1528
|
const annotated = `${reasonPrefix}
|
|
1458
1529
|
|
|
1459
1530
|
${summary}`;
|
|
@@ -1463,11 +1534,12 @@ ${summary}`;
|
|
|
1463
1534
|
turn: this._turn,
|
|
1464
1535
|
role: "assistant_final",
|
|
1465
1536
|
content: annotated,
|
|
1466
|
-
stats: summaryStats
|
|
1537
|
+
stats: summaryStats,
|
|
1538
|
+
forcedSummary: true
|
|
1467
1539
|
};
|
|
1468
1540
|
yield { turn: this._turn, role: "done", content: summary };
|
|
1469
1541
|
} catch (err) {
|
|
1470
|
-
const label = opts.reason
|
|
1542
|
+
const label = errorLabelFor(opts.reason, this.maxToolIters);
|
|
1471
1543
|
yield {
|
|
1472
1544
|
turn: this._turn,
|
|
1473
1545
|
role: "error",
|
|
@@ -1492,6 +1564,26 @@ ${summary}`;
|
|
|
1492
1564
|
return msg;
|
|
1493
1565
|
}
|
|
1494
1566
|
};
|
|
1567
|
+
function stripHallucinatedToolMarkup(s) {
|
|
1568
|
+
let out = s;
|
|
1569
|
+
out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
|
|
1570
|
+
out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
|
|
1571
|
+
out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
|
|
1572
|
+
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
1573
|
+
return out.trim();
|
|
1574
|
+
}
|
|
1575
|
+
function reasonPrefixFor(reason, iterCap) {
|
|
1576
|
+
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1577
|
+
if (reason === "context-guard") {
|
|
1578
|
+
return "[context budget running low \u2014 summarizing before the next call would overflow]";
|
|
1579
|
+
}
|
|
1580
|
+
return `[tool-call budget (${iterCap}) reached \u2014 forcing summary from what I found]`;
|
|
1581
|
+
}
|
|
1582
|
+
function errorLabelFor(reason, iterCap) {
|
|
1583
|
+
if (reason === "aborted") return "aborted by user";
|
|
1584
|
+
if (reason === "context-guard") return "context-guard triggered (prompt > 80% of window)";
|
|
1585
|
+
return `tool-call budget (${iterCap}) reached`;
|
|
1586
|
+
}
|
|
1495
1587
|
function summarizeBranch(chosen, samples) {
|
|
1496
1588
|
return {
|
|
1497
1589
|
budget: samples.length,
|
|
@@ -2114,7 +2206,7 @@ var McpClient = class {
|
|
|
2114
2206
|
async request(method, params) {
|
|
2115
2207
|
const id = this.nextId++;
|
|
2116
2208
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
2117
|
-
const promise = new Promise((
|
|
2209
|
+
const promise = new Promise((resolve3, reject) => {
|
|
2118
2210
|
const timeout = setTimeout(() => {
|
|
2119
2211
|
this.pending.delete(id);
|
|
2120
2212
|
reject(
|
|
@@ -2122,7 +2214,7 @@ var McpClient = class {
|
|
|
2122
2214
|
);
|
|
2123
2215
|
}, this.requestTimeoutMs);
|
|
2124
2216
|
this.pending.set(id, {
|
|
2125
|
-
resolve:
|
|
2217
|
+
resolve: resolve3,
|
|
2126
2218
|
reject,
|
|
2127
2219
|
timeout
|
|
2128
2220
|
});
|
|
@@ -2206,12 +2298,12 @@ var StdioTransport = class {
|
|
|
2206
2298
|
}
|
|
2207
2299
|
async send(message) {
|
|
2208
2300
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
2209
|
-
return new Promise((
|
|
2301
|
+
return new Promise((resolve3, reject) => {
|
|
2210
2302
|
const line = `${JSON.stringify(message)}
|
|
2211
2303
|
`;
|
|
2212
2304
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
2213
2305
|
if (err) reject(err);
|
|
2214
|
-
else
|
|
2306
|
+
else resolve3();
|
|
2215
2307
|
});
|
|
2216
2308
|
});
|
|
2217
2309
|
}
|
|
@@ -2222,8 +2314,8 @@ var StdioTransport = class {
|
|
|
2222
2314
|
continue;
|
|
2223
2315
|
}
|
|
2224
2316
|
if (this.closed) return;
|
|
2225
|
-
const next = await new Promise((
|
|
2226
|
-
this.waiters.push(
|
|
2317
|
+
const next = await new Promise((resolve3) => {
|
|
2318
|
+
this.waiters.push(resolve3);
|
|
2227
2319
|
});
|
|
2228
2320
|
if (next === null) return;
|
|
2229
2321
|
yield next;
|
|
@@ -2289,8 +2381,8 @@ var SseTransport = class {
|
|
|
2289
2381
|
constructor(opts) {
|
|
2290
2382
|
this.url = opts.url;
|
|
2291
2383
|
this.headers = opts.headers ?? {};
|
|
2292
|
-
this.endpointReady = new Promise((
|
|
2293
|
-
this.resolveEndpoint =
|
|
2384
|
+
this.endpointReady = new Promise((resolve3, reject) => {
|
|
2385
|
+
this.resolveEndpoint = resolve3;
|
|
2294
2386
|
this.rejectEndpoint = reject;
|
|
2295
2387
|
});
|
|
2296
2388
|
this.endpointReady.catch(() => void 0);
|
|
@@ -2317,8 +2409,8 @@ var SseTransport = class {
|
|
|
2317
2409
|
continue;
|
|
2318
2410
|
}
|
|
2319
2411
|
if (this.closed) return;
|
|
2320
|
-
const next = await new Promise((
|
|
2321
|
-
this.waiters.push(
|
|
2412
|
+
const next = await new Promise((resolve3) => {
|
|
2413
|
+
this.waiters.push(resolve3);
|
|
2322
2414
|
});
|
|
2323
2415
|
if (next === null) return;
|
|
2324
2416
|
yield next;
|
|
@@ -2486,16 +2578,215 @@ function parseMcpSpec(input) {
|
|
|
2486
2578
|
return { transport: "stdio", name, command, args };
|
|
2487
2579
|
}
|
|
2488
2580
|
|
|
2581
|
+
// src/code/edit-blocks.ts
|
|
2582
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2583
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
2584
|
+
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
2585
|
+
function parseEditBlocks(text) {
|
|
2586
|
+
const out = [];
|
|
2587
|
+
BLOCK_RE.lastIndex = 0;
|
|
2588
|
+
let m = BLOCK_RE.exec(text);
|
|
2589
|
+
while (m !== null) {
|
|
2590
|
+
out.push({
|
|
2591
|
+
path: m[1].trim(),
|
|
2592
|
+
search: m[2],
|
|
2593
|
+
replace: m[3],
|
|
2594
|
+
offset: m.index
|
|
2595
|
+
});
|
|
2596
|
+
m = BLOCK_RE.exec(text);
|
|
2597
|
+
}
|
|
2598
|
+
return out;
|
|
2599
|
+
}
|
|
2600
|
+
function applyEditBlock(block, rootDir) {
|
|
2601
|
+
const absRoot = resolve2(rootDir);
|
|
2602
|
+
const absTarget = resolve2(absRoot, block.path);
|
|
2603
|
+
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
2604
|
+
return {
|
|
2605
|
+
path: block.path,
|
|
2606
|
+
status: "path-escape",
|
|
2607
|
+
message: `resolved path ${absTarget} is outside rootDir ${absRoot}`
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
const searchEmpty = block.search.length === 0;
|
|
2611
|
+
const exists = existsSync2(absTarget);
|
|
2612
|
+
try {
|
|
2613
|
+
if (!exists) {
|
|
2614
|
+
if (!searchEmpty) {
|
|
2615
|
+
return {
|
|
2616
|
+
path: block.path,
|
|
2617
|
+
status: "file-missing",
|
|
2618
|
+
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
mkdirSync2(dirname2(absTarget), { recursive: true });
|
|
2622
|
+
writeFileSync2(absTarget, block.replace, "utf8");
|
|
2623
|
+
return { path: block.path, status: "created" };
|
|
2624
|
+
}
|
|
2625
|
+
const content = readFileSync4(absTarget, "utf8");
|
|
2626
|
+
if (searchEmpty) {
|
|
2627
|
+
return {
|
|
2628
|
+
path: block.path,
|
|
2629
|
+
status: "not-found",
|
|
2630
|
+
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
const idx = content.indexOf(block.search);
|
|
2634
|
+
if (idx === -1) {
|
|
2635
|
+
return {
|
|
2636
|
+
path: block.path,
|
|
2637
|
+
status: "not-found",
|
|
2638
|
+
message: "SEARCH text does not match the current file content exactly"
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
|
|
2642
|
+
writeFileSync2(absTarget, replaced, "utf8");
|
|
2643
|
+
return { path: block.path, status: "applied" };
|
|
2644
|
+
} catch (err) {
|
|
2645
|
+
return { path: block.path, status: "error", message: err.message };
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
function applyEditBlocks(blocks, rootDir) {
|
|
2649
|
+
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
2650
|
+
}
|
|
2651
|
+
function snapshotBeforeEdits(blocks, rootDir) {
|
|
2652
|
+
const absRoot = resolve2(rootDir);
|
|
2653
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2654
|
+
const snapshots = [];
|
|
2655
|
+
for (const b of blocks) {
|
|
2656
|
+
if (seen.has(b.path)) continue;
|
|
2657
|
+
seen.add(b.path);
|
|
2658
|
+
const abs = resolve2(absRoot, b.path);
|
|
2659
|
+
if (!existsSync2(abs)) {
|
|
2660
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
try {
|
|
2664
|
+
snapshots.push({ path: b.path, prevContent: readFileSync4(abs, "utf8") });
|
|
2665
|
+
} catch {
|
|
2666
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return snapshots;
|
|
2670
|
+
}
|
|
2671
|
+
function restoreSnapshots(snapshots, rootDir) {
|
|
2672
|
+
const absRoot = resolve2(rootDir);
|
|
2673
|
+
return snapshots.map((snap) => {
|
|
2674
|
+
const abs = resolve2(absRoot, snap.path);
|
|
2675
|
+
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
2676
|
+
return {
|
|
2677
|
+
path: snap.path,
|
|
2678
|
+
status: "path-escape",
|
|
2679
|
+
message: "snapshot path escapes rootDir \u2014 refusing to restore"
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
try {
|
|
2683
|
+
if (snap.prevContent === null) {
|
|
2684
|
+
if (existsSync2(abs)) unlinkSync2(abs);
|
|
2685
|
+
return {
|
|
2686
|
+
path: snap.path,
|
|
2687
|
+
status: "applied",
|
|
2688
|
+
message: "removed (the edit had created it)"
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
writeFileSync2(abs, snap.prevContent, "utf8");
|
|
2692
|
+
return {
|
|
2693
|
+
path: snap.path,
|
|
2694
|
+
status: "applied",
|
|
2695
|
+
message: "restored to pre-edit content"
|
|
2696
|
+
};
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
return { path: snap.path, status: "error", message: err.message };
|
|
2699
|
+
}
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
function sep() {
|
|
2703
|
+
return process.platform === "win32" ? "\\" : "/";
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// src/code/prompt.ts
|
|
2707
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
2708
|
+
import { join as join2 } from "path";
|
|
2709
|
+
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.
|
|
2710
|
+
|
|
2711
|
+
# When to edit vs. when to explore
|
|
2712
|
+
|
|
2713
|
+
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:
|
|
2714
|
+
- analyze, read, explore, describe, or summarize a project
|
|
2715
|
+
- explain how something works
|
|
2716
|
+
- answer a question about the code
|
|
2717
|
+
|
|
2718
|
+
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.
|
|
2719
|
+
|
|
2720
|
+
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.
|
|
2721
|
+
|
|
2722
|
+
# Editing files
|
|
2723
|
+
|
|
2724
|
+
When you've been asked to change a file, output one or more SEARCH/REPLACE blocks in this exact format:
|
|
2725
|
+
|
|
2726
|
+
path/to/file.ext
|
|
2727
|
+
<<<<<<< SEARCH
|
|
2728
|
+
exact existing lines from the file, including whitespace
|
|
2729
|
+
=======
|
|
2730
|
+
the new lines
|
|
2731
|
+
>>>>>>> REPLACE
|
|
2732
|
+
|
|
2733
|
+
Rules:
|
|
2734
|
+
- 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.
|
|
2735
|
+
- One edit per block. Multiple blocks in one response are fine.
|
|
2736
|
+
- To create a new file, leave SEARCH empty:
|
|
2737
|
+
path/to/new.ts
|
|
2738
|
+
<<<<<<< SEARCH
|
|
2739
|
+
=======
|
|
2740
|
+
(whole file content here)
|
|
2741
|
+
>>>>>>> REPLACE
|
|
2742
|
+
- 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).
|
|
2743
|
+
- Paths are relative to the working directory. Don't use absolute paths.
|
|
2744
|
+
|
|
2745
|
+
# Exploration
|
|
2746
|
+
|
|
2747
|
+
- 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.
|
|
2748
|
+
- 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.
|
|
2749
|
+
|
|
2750
|
+
# Style
|
|
2751
|
+
|
|
2752
|
+
- Show edits; don't narrate them in prose. "Here's the fix:" is enough.
|
|
2753
|
+
- One short paragraph explaining *why*, then the blocks.
|
|
2754
|
+
- If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
|
|
2755
|
+
`;
|
|
2756
|
+
function codeSystemPrompt(rootDir) {
|
|
2757
|
+
const gitignorePath = join2(rootDir, ".gitignore");
|
|
2758
|
+
if (!existsSync3(gitignorePath)) return CODE_SYSTEM_PROMPT;
|
|
2759
|
+
let content;
|
|
2760
|
+
try {
|
|
2761
|
+
content = readFileSync5(gitignorePath, "utf8");
|
|
2762
|
+
} catch {
|
|
2763
|
+
return CODE_SYSTEM_PROMPT;
|
|
2764
|
+
}
|
|
2765
|
+
const MAX = 2e3;
|
|
2766
|
+
const truncated = content.length > MAX ? `${content.slice(0, MAX)}
|
|
2767
|
+
\u2026 (truncated ${content.length - MAX} chars)` : content;
|
|
2768
|
+
return `${CODE_SYSTEM_PROMPT}
|
|
2769
|
+
|
|
2770
|
+
# Project .gitignore
|
|
2771
|
+
|
|
2772
|
+
The user's repo ships this .gitignore \u2014 treat every pattern as "don't traverse or edit inside these paths unless explicitly asked":
|
|
2773
|
+
|
|
2774
|
+
\`\`\`
|
|
2775
|
+
${truncated}
|
|
2776
|
+
\`\`\`
|
|
2777
|
+
`;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2489
2780
|
// src/config.ts
|
|
2490
|
-
import { chmodSync as chmodSync2, mkdirSync as
|
|
2781
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
2491
2782
|
import { homedir as homedir2 } from "os";
|
|
2492
|
-
import { dirname as
|
|
2783
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2493
2784
|
function defaultConfigPath() {
|
|
2494
|
-
return
|
|
2785
|
+
return join3(homedir2(), ".reasonix", "config.json");
|
|
2495
2786
|
}
|
|
2496
2787
|
function readConfig(path = defaultConfigPath()) {
|
|
2497
2788
|
try {
|
|
2498
|
-
const raw =
|
|
2789
|
+
const raw = readFileSync6(path, "utf8");
|
|
2499
2790
|
const parsed = JSON.parse(raw);
|
|
2500
2791
|
if (parsed && typeof parsed === "object") return parsed;
|
|
2501
2792
|
} catch {
|
|
@@ -2503,8 +2794,8 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
2503
2794
|
return {};
|
|
2504
2795
|
}
|
|
2505
2796
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2506
|
-
|
|
2507
|
-
|
|
2797
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
2798
|
+
writeFileSync3(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2508
2799
|
try {
|
|
2509
2800
|
chmodSync2(path, 384);
|
|
2510
2801
|
} catch {
|
|
@@ -2530,9 +2821,10 @@ function redactKey(key) {
|
|
|
2530
2821
|
}
|
|
2531
2822
|
|
|
2532
2823
|
// src/index.ts
|
|
2533
|
-
var VERSION = "0.3
|
|
2824
|
+
var VERSION = "0.4.3";
|
|
2534
2825
|
export {
|
|
2535
2826
|
AppendOnlyLog,
|
|
2827
|
+
CODE_SYSTEM_PROMPT,
|
|
2536
2828
|
CacheFirstLoop,
|
|
2537
2829
|
DEFAULT_MAX_RESULT_CHARS,
|
|
2538
2830
|
DeepSeekClient,
|
|
@@ -2551,8 +2843,11 @@ export {
|
|
|
2551
2843
|
aggregateBranchUsage,
|
|
2552
2844
|
analyzeSchema,
|
|
2553
2845
|
appendSessionMessage,
|
|
2846
|
+
applyEditBlock,
|
|
2847
|
+
applyEditBlocks,
|
|
2554
2848
|
bridgeMcpTools,
|
|
2555
2849
|
claudeEquivalentCost,
|
|
2850
|
+
codeSystemPrompt,
|
|
2556
2851
|
computeReplayStats,
|
|
2557
2852
|
costUsd,
|
|
2558
2853
|
defaultConfigPath,
|
|
@@ -2575,6 +2870,7 @@ export {
|
|
|
2575
2870
|
loadSessionMessages,
|
|
2576
2871
|
nestArguments,
|
|
2577
2872
|
openTranscriptFile,
|
|
2873
|
+
parseEditBlocks,
|
|
2578
2874
|
parseMcpSpec,
|
|
2579
2875
|
parseTranscript,
|
|
2580
2876
|
readConfig,
|
|
@@ -2585,6 +2881,7 @@ export {
|
|
|
2585
2881
|
renderSummaryTable as renderDiffSummary,
|
|
2586
2882
|
repairTruncatedJson,
|
|
2587
2883
|
replayFromFile,
|
|
2884
|
+
restoreSnapshots,
|
|
2588
2885
|
runBranches,
|
|
2589
2886
|
sanitizeName as sanitizeSessionName,
|
|
2590
2887
|
saveApiKey,
|
|
@@ -2592,6 +2889,8 @@ export {
|
|
|
2592
2889
|
sessionPath,
|
|
2593
2890
|
sessionsDir,
|
|
2594
2891
|
similarity,
|
|
2892
|
+
snapshotBeforeEdits,
|
|
2893
|
+
stripHallucinatedToolMarkup,
|
|
2595
2894
|
truncateForModel,
|
|
2596
2895
|
writeConfig,
|
|
2597
2896
|
writeMeta,
|