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.
- 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 +460 -39
- 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 +128 -3
- package/dist/index.js +268 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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"
|
|
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.
|
|
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((
|
|
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") {
|
|
@@ -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((
|
|
1306
|
-
waiter =
|
|
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
|
|
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
|
|
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((
|
|
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:
|
|
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((
|
|
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
|
|
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((
|
|
2226
|
-
this.waiters.push(
|
|
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((
|
|
2293
|
-
this.resolveEndpoint =
|
|
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((
|
|
2321
|
-
this.waiters.push(
|
|
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
|
|
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
|
|
2728
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2493
2729
|
function defaultConfigPath() {
|
|
2494
|
-
return
|
|
2730
|
+
return join3(homedir2(), ".reasonix", "config.json");
|
|
2495
2731
|
}
|
|
2496
2732
|
function readConfig(path = defaultConfigPath()) {
|
|
2497
2733
|
try {
|
|
2498
|
-
const raw =
|
|
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
|
-
|
|
2507
|
-
|
|
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.
|
|
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,
|