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/README.md +114 -77
- package/dist/cli/chunk-2P2MZLCE.js +81 -0
- package/dist/cli/chunk-2P2MZLCE.js.map +1 -0
- package/dist/cli/index.js +529 -53
- 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 +142 -3
- package/dist/index.js +308 -27
- 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;
|
|
@@ -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 ??
|
|
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((
|
|
1270
|
-
waiter =
|
|
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
|
|
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:
|
|
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((
|
|
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:
|
|
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((
|
|
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
|
|
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((
|
|
2188
|
-
this.waiters.push(
|
|
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((
|
|
2255
|
-
this.resolveEndpoint =
|
|
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((
|
|
2283
|
-
this.waiters.push(
|
|
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
|
|
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
|
|
2728
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2455
2729
|
function defaultConfigPath() {
|
|
2456
|
-
return
|
|
2730
|
+
return join3(homedir2(), ".reasonix", "config.json");
|
|
2457
2731
|
}
|
|
2458
2732
|
function readConfig(path = defaultConfigPath()) {
|
|
2459
2733
|
try {
|
|
2460
|
-
const raw =
|
|
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
|
-
|
|
2469
|
-
|
|
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.
|
|
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,
|