reasonix 0.4.16 → 0.4.19
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 +64 -0
- package/dist/cli/chunk-HNEWBEWZ.js +152 -0
- package/dist/cli/chunk-HNEWBEWZ.js.map +1 -0
- package/dist/cli/index.js +701 -238
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-MMANQ36Z.js → prompt-JNNNJLYF.js} +2 -2
- package/dist/index.d.ts +217 -4
- package/dist/index.js +276 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-2P2MZLCE.js +0 -81
- package/dist/cli/chunk-2P2MZLCE.js.map +0 -1
- /package/dist/cli/{prompt-MMANQ36Z.js.map → prompt-JNNNJLYF.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -499,9 +499,25 @@ function setByPath(target, path, value) {
|
|
|
499
499
|
var ToolRegistry = class {
|
|
500
500
|
_tools = /* @__PURE__ */ new Map();
|
|
501
501
|
_autoFlatten;
|
|
502
|
+
/**
|
|
503
|
+
* When true, `dispatch` refuses any tool whose `readOnly` flag isn't
|
|
504
|
+
* set (and whose `readOnlyCheck` doesn't pass on the specific args).
|
|
505
|
+
* Drives `reasonix code`'s Plan Mode — the model can still explore
|
|
506
|
+
* via read tools but its writes and non-allowlisted shell calls are
|
|
507
|
+
* bounced until the user approves a submitted plan.
|
|
508
|
+
*/
|
|
509
|
+
_planMode = false;
|
|
502
510
|
constructor(opts = {}) {
|
|
503
511
|
this._autoFlatten = opts.autoFlatten !== false;
|
|
504
512
|
}
|
|
513
|
+
/** Enable / disable plan-mode enforcement at dispatch. */
|
|
514
|
+
setPlanMode(on) {
|
|
515
|
+
this._planMode = Boolean(on);
|
|
516
|
+
}
|
|
517
|
+
/** True when the registry is currently refusing non-readonly calls. */
|
|
518
|
+
get planMode() {
|
|
519
|
+
return this._planMode;
|
|
520
|
+
}
|
|
505
521
|
register(def) {
|
|
506
522
|
if (!def.name) throw new Error("tool requires a name");
|
|
507
523
|
const internal = { ...def };
|
|
@@ -553,16 +569,38 @@ var ToolRegistry = class {
|
|
|
553
569
|
if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
|
|
554
570
|
args = nestArguments(args);
|
|
555
571
|
}
|
|
572
|
+
if (this._planMode && !isReadOnlyCall(tool, args)) {
|
|
573
|
+
return JSON.stringify({
|
|
574
|
+
error: `${name}: unavailable in plan mode \u2014 this is a read-only exploration phase. Use read_file / list_directory / search_files / directory_tree / web_search / allowlisted shell commands to investigate. Call submit_plan with your proposed plan when you're ready for the user's review.`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
556
577
|
try {
|
|
557
578
|
const result = await tool.fn(args, { signal: opts.signal });
|
|
558
579
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
559
580
|
} catch (err) {
|
|
581
|
+
const e = err;
|
|
582
|
+
if (typeof e.toToolResult === "function") {
|
|
583
|
+
try {
|
|
584
|
+
return JSON.stringify(e.toToolResult());
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
}
|
|
560
588
|
return JSON.stringify({
|
|
561
|
-
error: `${
|
|
589
|
+
error: `${e.name}: ${e.message}`
|
|
562
590
|
});
|
|
563
591
|
}
|
|
564
592
|
}
|
|
565
593
|
};
|
|
594
|
+
function isReadOnlyCall(tool, args) {
|
|
595
|
+
if (tool.readOnlyCheck) {
|
|
596
|
+
try {
|
|
597
|
+
return Boolean(tool.readOnlyCheck(args));
|
|
598
|
+
} catch {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return tool.readOnly === true;
|
|
603
|
+
}
|
|
566
604
|
function hasDotKey(obj) {
|
|
567
605
|
for (const k of Object.keys(obj)) {
|
|
568
606
|
if (k.includes(".")) return true;
|
|
@@ -949,6 +987,16 @@ var ToolCallRepair = class {
|
|
|
949
987
|
this.opts = opts;
|
|
950
988
|
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3);
|
|
951
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* Drop the StormBreaker's sliding window of recent (name, args)
|
|
992
|
+
* signatures. Called at the start of every user turn — a fresh user
|
|
993
|
+
* message is a new intent, so carrying old repetition state into it
|
|
994
|
+
* would turn a valid "try again with different input" flow into a
|
|
995
|
+
* false-positive block.
|
|
996
|
+
*/
|
|
997
|
+
resetStorm() {
|
|
998
|
+
this.storm.reset();
|
|
999
|
+
}
|
|
952
1000
|
process(declaredCalls, reasoningContent, content = null) {
|
|
953
1001
|
const report = {
|
|
954
1002
|
scavenged: 0,
|
|
@@ -1401,6 +1449,7 @@ var CacheFirstLoop = class {
|
|
|
1401
1449
|
async *step(userInput) {
|
|
1402
1450
|
this._turn++;
|
|
1403
1451
|
this.scratch.reset();
|
|
1452
|
+
this.repair.resetStorm();
|
|
1404
1453
|
this._turnAbort = new AbortController();
|
|
1405
1454
|
const signal = this._turnAbort.signal;
|
|
1406
1455
|
let pendingUser = userInput;
|
|
@@ -1624,6 +1673,16 @@ var CacheFirstLoop = class {
|
|
|
1624
1673
|
repair: report,
|
|
1625
1674
|
branch: branchSummary
|
|
1626
1675
|
};
|
|
1676
|
+
if (report.stormsBroken > 0) {
|
|
1677
|
+
const noteTail = report.notes.length ? ` \u2014 ${report.notes[report.notes.length - 1]}` : "";
|
|
1678
|
+
const allSuppressed = repairedCalls.length === 0 && toolCalls.length > 0;
|
|
1679
|
+
const phrase = allSuppressed ? `stopped the model from calling the same tool with identical args repeatedly (all ${toolCalls.length} call(s) this turn were already in the recent-repeat window). Likely a stuck retry \u2014 reword your instruction, rule out the underlying blocker, or try /retry after fixing it` : `suppressed ${report.stormsBroken} repeat tool call(s) that had fired 3+ times with identical args in a sliding window`;
|
|
1680
|
+
yield {
|
|
1681
|
+
turn: this._turn,
|
|
1682
|
+
role: "warning",
|
|
1683
|
+
content: `${phrase}${noteTail}`
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1627
1686
|
if (repairedCalls.length === 0) {
|
|
1628
1687
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
1629
1688
|
return;
|
|
@@ -1848,6 +1907,49 @@ function formatLoopError(err) {
|
|
|
1848
1907
|
return msg;
|
|
1849
1908
|
}
|
|
1850
1909
|
|
|
1910
|
+
// src/project-memory.ts
|
|
1911
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1912
|
+
import { join as join2 } from "path";
|
|
1913
|
+
var PROJECT_MEMORY_FILE = "REASONIX.md";
|
|
1914
|
+
var PROJECT_MEMORY_MAX_CHARS = 8e3;
|
|
1915
|
+
function readProjectMemory(rootDir) {
|
|
1916
|
+
const path = join2(rootDir, PROJECT_MEMORY_FILE);
|
|
1917
|
+
if (!existsSync2(path)) return null;
|
|
1918
|
+
let raw;
|
|
1919
|
+
try {
|
|
1920
|
+
raw = readFileSync2(path, "utf8");
|
|
1921
|
+
} catch {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
const trimmed = raw.trim();
|
|
1925
|
+
if (!trimmed) return null;
|
|
1926
|
+
const originalChars = trimmed.length;
|
|
1927
|
+
const truncated = originalChars > PROJECT_MEMORY_MAX_CHARS;
|
|
1928
|
+
const content = truncated ? `${trimmed.slice(0, PROJECT_MEMORY_MAX_CHARS)}
|
|
1929
|
+
\u2026 (truncated ${originalChars - PROJECT_MEMORY_MAX_CHARS} chars)` : trimmed;
|
|
1930
|
+
return { path, content, originalChars, truncated };
|
|
1931
|
+
}
|
|
1932
|
+
function memoryEnabled() {
|
|
1933
|
+
const env = process.env.REASONIX_MEMORY;
|
|
1934
|
+
if (env === "off" || env === "false" || env === "0") return false;
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
function applyProjectMemory(basePrompt, rootDir) {
|
|
1938
|
+
if (!memoryEnabled()) return basePrompt;
|
|
1939
|
+
const mem = readProjectMemory(rootDir);
|
|
1940
|
+
if (!mem) return basePrompt;
|
|
1941
|
+
return `${basePrompt}
|
|
1942
|
+
|
|
1943
|
+
# Project memory (REASONIX.md)
|
|
1944
|
+
|
|
1945
|
+
The user pinned these notes about this project \u2014 treat them as authoritative context for every turn:
|
|
1946
|
+
|
|
1947
|
+
\`\`\`
|
|
1948
|
+
${mem.content}
|
|
1949
|
+
\`\`\`
|
|
1950
|
+
`;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1851
1953
|
// src/tools/filesystem.ts
|
|
1852
1954
|
import { promises as fs } from "fs";
|
|
1853
1955
|
import * as pathMod from "path";
|
|
@@ -1873,6 +1975,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1873
1975
|
registry.register({
|
|
1874
1976
|
name: "read_file",
|
|
1875
1977
|
description: "Read a file under the sandbox root. Returns the full contents (truncated with a notice if larger than the per-call cap). Paths may be relative to the root or absolute-under-root.",
|
|
1978
|
+
readOnly: true,
|
|
1876
1979
|
parameters: {
|
|
1877
1980
|
type: "object",
|
|
1878
1981
|
properties: {
|
|
@@ -1910,6 +2013,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1910
2013
|
registry.register({
|
|
1911
2014
|
name: "list_directory",
|
|
1912
2015
|
description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
|
|
2016
|
+
readOnly: true,
|
|
1913
2017
|
parameters: {
|
|
1914
2018
|
type: "object",
|
|
1915
2019
|
properties: {
|
|
@@ -1929,6 +2033,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1929
2033
|
registry.register({
|
|
1930
2034
|
name: "directory_tree",
|
|
1931
2035
|
description: "Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Caps output so a huge tree doesn't drown the context.",
|
|
2036
|
+
readOnly: true,
|
|
1932
2037
|
parameters: {
|
|
1933
2038
|
type: "object",
|
|
1934
2039
|
properties: {
|
|
@@ -1975,6 +2080,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1975
2080
|
registry.register({
|
|
1976
2081
|
name: "search_files",
|
|
1977
2082
|
description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line.",
|
|
2083
|
+
readOnly: true,
|
|
1978
2084
|
parameters: {
|
|
1979
2085
|
type: "object",
|
|
1980
2086
|
properties: {
|
|
@@ -2027,6 +2133,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2027
2133
|
registry.register({
|
|
2028
2134
|
name: "get_file_info",
|
|
2029
2135
|
description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
|
|
2136
|
+
readOnly: true,
|
|
2030
2137
|
parameters: {
|
|
2031
2138
|
type: "object",
|
|
2032
2139
|
properties: {
|
|
@@ -2183,8 +2290,54 @@ function lineDiff(a, b) {
|
|
|
2183
2290
|
return out;
|
|
2184
2291
|
}
|
|
2185
2292
|
|
|
2293
|
+
// src/tools/plan.ts
|
|
2294
|
+
var PlanProposedError = class extends Error {
|
|
2295
|
+
plan;
|
|
2296
|
+
constructor(plan) {
|
|
2297
|
+
super(
|
|
2298
|
+
"PlanProposedError: plan submitted. STOP calling tools now \u2014 the TUI has shown the plan to the user. Wait for their next message; it will either approve (you'll then implement the plan), request a refinement (you should explore more and submit an updated plan), or cancel (drop the plan and ask what they want instead). Don't call any tools in the meantime."
|
|
2299
|
+
);
|
|
2300
|
+
this.name = "PlanProposedError";
|
|
2301
|
+
this.plan = plan;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Structured tool-result shape. Consumed by the TUI to extract the
|
|
2305
|
+
* plan without regex-scraping the error message.
|
|
2306
|
+
*/
|
|
2307
|
+
toToolResult() {
|
|
2308
|
+
return { error: `${this.name}: ${this.message}`, plan: this.plan };
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
function registerPlanTool(registry, opts = {}) {
|
|
2312
|
+
registry.register({
|
|
2313
|
+
name: "submit_plan",
|
|
2314
|
+
description: "Submit a concrete plan to the user for review before executing. Use this for tasks that warrant a review gate \u2014 multi-file refactors, architecture changes, anything that would be expensive or confusing to undo. Skip it for small fixes (one-line typo, obvious bug with a clear fix) \u2014 just make the change. The user will either approve (you then implement it), ask for refinement, or cancel. If the user has already enabled /plan mode, writes are blocked at dispatch and you MUST use this. Write the plan as markdown with a one-line summary, a bulleted list of files to touch and what will change, and any risks or open questions.",
|
|
2315
|
+
readOnly: true,
|
|
2316
|
+
parameters: {
|
|
2317
|
+
type: "object",
|
|
2318
|
+
properties: {
|
|
2319
|
+
plan: {
|
|
2320
|
+
type: "string",
|
|
2321
|
+
description: "Markdown-formatted plan. Lead with a one-sentence summary. Then a file-by-file breakdown of what you'll change and why. Flag any risks or open questions at the end so the user can weigh in before you start."
|
|
2322
|
+
}
|
|
2323
|
+
},
|
|
2324
|
+
required: ["plan"]
|
|
2325
|
+
},
|
|
2326
|
+
fn: async (args) => {
|
|
2327
|
+
const plan = (args?.plan ?? "").trim();
|
|
2328
|
+
if (!plan) {
|
|
2329
|
+
throw new Error("submit_plan: empty plan \u2014 write a markdown plan and try again.");
|
|
2330
|
+
}
|
|
2331
|
+
opts.onPlanSubmitted?.(plan);
|
|
2332
|
+
throw new PlanProposedError(plan);
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
return registry;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2186
2338
|
// src/tools/shell.ts
|
|
2187
2339
|
import { spawn } from "child_process";
|
|
2340
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
2188
2341
|
import * as pathMod2 from "path";
|
|
2189
2342
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2190
2343
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
@@ -2302,10 +2455,12 @@ async function runCommand(cmd, opts) {
|
|
|
2302
2455
|
windowsHide: true,
|
|
2303
2456
|
env: process.env
|
|
2304
2457
|
};
|
|
2458
|
+
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
2459
|
+
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
2305
2460
|
return await new Promise((resolve5, reject) => {
|
|
2306
2461
|
let child;
|
|
2307
2462
|
try {
|
|
2308
|
-
child = spawn(
|
|
2463
|
+
child = spawn(bin, args, effectiveSpawnOpts);
|
|
2309
2464
|
} catch (err) {
|
|
2310
2465
|
reject(err);
|
|
2311
2466
|
return;
|
|
@@ -2339,6 +2494,59 @@ async function runCommand(cmd, opts) {
|
|
|
2339
2494
|
});
|
|
2340
2495
|
});
|
|
2341
2496
|
}
|
|
2497
|
+
function resolveExecutable(cmd, opts = {}) {
|
|
2498
|
+
const platform = opts.platform ?? process.platform;
|
|
2499
|
+
if (platform !== "win32") return cmd;
|
|
2500
|
+
if (!cmd) return cmd;
|
|
2501
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod2.isAbsolute(cmd)) return cmd;
|
|
2502
|
+
if (pathMod2.extname(cmd)) return cmd;
|
|
2503
|
+
const env = opts.env ?? process.env;
|
|
2504
|
+
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
2505
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod2.delimiter);
|
|
2506
|
+
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
2507
|
+
const isFile = opts.isFile ?? defaultIsFile;
|
|
2508
|
+
for (const dir of pathDirs) {
|
|
2509
|
+
for (const ext of pathExt) {
|
|
2510
|
+
const full = pathMod2.join(dir, cmd + ext);
|
|
2511
|
+
if (isFile(full)) return full;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return cmd;
|
|
2515
|
+
}
|
|
2516
|
+
function defaultIsFile(full) {
|
|
2517
|
+
try {
|
|
2518
|
+
return existsSync3(full) && statSync2(full).isFile();
|
|
2519
|
+
} catch {
|
|
2520
|
+
return false;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
function prepareSpawn(argv, opts = {}) {
|
|
2524
|
+
const head = argv[0] ?? "";
|
|
2525
|
+
const tail = argv.slice(1);
|
|
2526
|
+
const platform = opts.platform ?? process.platform;
|
|
2527
|
+
const resolved = resolveExecutable(head, opts);
|
|
2528
|
+
if (platform !== "win32") {
|
|
2529
|
+
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2530
|
+
}
|
|
2531
|
+
if (/\.(cmd|bat)$/i.test(resolved)) {
|
|
2532
|
+
const cmdline = [resolved, ...tail].map(quoteForCmdExe).join(" ");
|
|
2533
|
+
return {
|
|
2534
|
+
bin: "cmd.exe",
|
|
2535
|
+
args: ["/d", "/s", "/c", cmdline],
|
|
2536
|
+
// windowsVerbatimArguments prevents Node from re-quoting the /c
|
|
2537
|
+
// payload — we've already composed an exact cmd.exe command
|
|
2538
|
+
// line. Without this Node wraps our already-quoted string in
|
|
2539
|
+
// another round of quotes and cmd.exe can't parse it.
|
|
2540
|
+
spawnOverrides: { windowsVerbatimArguments: true }
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2544
|
+
}
|
|
2545
|
+
function quoteForCmdExe(arg) {
|
|
2546
|
+
if (arg === "") return '""';
|
|
2547
|
+
if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
|
|
2548
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
2549
|
+
}
|
|
2342
2550
|
var NeedsConfirmationError = class extends Error {
|
|
2343
2551
|
command;
|
|
2344
2552
|
constructor(command) {
|
|
@@ -2358,6 +2566,16 @@ function registerShellTools(registry, opts) {
|
|
|
2358
2566
|
registry.register({
|
|
2359
2567
|
name: "run_command",
|
|
2360
2568
|
description: "Run a shell command in the project root and return its combined stdout+stderr. Read-only and test commands (git status, ls, npm test, pytest, cargo test, grep, etc.) run immediately. Anything that could mutate state (npm install, git commit, rm, chmod) is refused and the user has to confirm in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
2569
|
+
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
2570
|
+
// cargo check, ls, grep …) so the model can actually investigate
|
|
2571
|
+
// during planning. Anything that would otherwise trigger a
|
|
2572
|
+
// confirmation prompt is treated as "not read-only" and bounced.
|
|
2573
|
+
readOnlyCheck: (args) => {
|
|
2574
|
+
if (allowAll) return true;
|
|
2575
|
+
const cmd = typeof args?.command === "string" ? args.command.trim() : "";
|
|
2576
|
+
if (!cmd) return false;
|
|
2577
|
+
return isAllowed(cmd, extraAllowed);
|
|
2578
|
+
},
|
|
2361
2579
|
parameters: {
|
|
2362
2580
|
type: "object",
|
|
2363
2581
|
properties: {
|
|
@@ -2524,6 +2742,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
2524
2742
|
registry.register({
|
|
2525
2743
|
name: "web_search",
|
|
2526
2744
|
description: "Search the public web. Returns ranked results with title, url, and snippet. Use this when the question needs information more current than your training data, when you're unsure of a factual detail, or when the user asks about a specific webpage/library/release you haven't seen.",
|
|
2745
|
+
readOnly: true,
|
|
2527
2746
|
parameters: {
|
|
2528
2747
|
type: "object",
|
|
2529
2748
|
properties: {
|
|
@@ -2546,6 +2765,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
2546
2765
|
registry.register({
|
|
2547
2766
|
name: "web_fetch",
|
|
2548
2767
|
description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
|
|
2768
|
+
readOnly: true,
|
|
2549
2769
|
parameters: {
|
|
2550
2770
|
type: "object",
|
|
2551
2771
|
properties: {
|
|
@@ -2580,12 +2800,12 @@ ${i + 1}. ${r.title}`);
|
|
|
2580
2800
|
}
|
|
2581
2801
|
|
|
2582
2802
|
// src/env.ts
|
|
2583
|
-
import { readFileSync as
|
|
2803
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2584
2804
|
import { resolve as resolve3 } from "path";
|
|
2585
2805
|
function loadDotenv(path = ".env") {
|
|
2586
2806
|
let raw;
|
|
2587
2807
|
try {
|
|
2588
|
-
raw =
|
|
2808
|
+
raw = readFileSync3(resolve3(process.cwd(), path), "utf8");
|
|
2589
2809
|
} catch {
|
|
2590
2810
|
return;
|
|
2591
2811
|
}
|
|
@@ -2604,7 +2824,7 @@ function loadDotenv(path = ".env") {
|
|
|
2604
2824
|
}
|
|
2605
2825
|
|
|
2606
2826
|
// src/transcript.ts
|
|
2607
|
-
import { createWriteStream, readFileSync as
|
|
2827
|
+
import { createWriteStream, readFileSync as readFileSync4 } from "fs";
|
|
2608
2828
|
function recordFromLoopEvent(ev, extra) {
|
|
2609
2829
|
const rec = {
|
|
2610
2830
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2655,7 +2875,7 @@ function openTranscriptFile(path, meta) {
|
|
|
2655
2875
|
return stream;
|
|
2656
2876
|
}
|
|
2657
2877
|
function readTranscript(path) {
|
|
2658
|
-
const raw =
|
|
2878
|
+
const raw = readFileSync4(path, "utf8");
|
|
2659
2879
|
return parseTranscript(raw);
|
|
2660
2880
|
}
|
|
2661
2881
|
function isPlanStateEmptyShape(s) {
|
|
@@ -3710,7 +3930,7 @@ async function trySection(load) {
|
|
|
3710
3930
|
}
|
|
3711
3931
|
|
|
3712
3932
|
// src/code/edit-blocks.ts
|
|
3713
|
-
import { existsSync as
|
|
3933
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3714
3934
|
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
3715
3935
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
3716
3936
|
function parseEditBlocks(text) {
|
|
@@ -3739,7 +3959,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
3739
3959
|
};
|
|
3740
3960
|
}
|
|
3741
3961
|
const searchEmpty = block.search.length === 0;
|
|
3742
|
-
const exists =
|
|
3962
|
+
const exists = existsSync4(absTarget);
|
|
3743
3963
|
try {
|
|
3744
3964
|
if (!exists) {
|
|
3745
3965
|
if (!searchEmpty) {
|
|
@@ -3753,7 +3973,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
3753
3973
|
writeFileSync2(absTarget, block.replace, "utf8");
|
|
3754
3974
|
return { path: block.path, status: "created" };
|
|
3755
3975
|
}
|
|
3756
|
-
const content =
|
|
3976
|
+
const content = readFileSync5(absTarget, "utf8");
|
|
3757
3977
|
if (searchEmpty) {
|
|
3758
3978
|
return {
|
|
3759
3979
|
path: block.path,
|
|
@@ -3787,12 +4007,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
3787
4007
|
if (seen.has(b.path)) continue;
|
|
3788
4008
|
seen.add(b.path);
|
|
3789
4009
|
const abs = resolve4(absRoot, b.path);
|
|
3790
|
-
if (!
|
|
4010
|
+
if (!existsSync4(abs)) {
|
|
3791
4011
|
snapshots.push({ path: b.path, prevContent: null });
|
|
3792
4012
|
continue;
|
|
3793
4013
|
}
|
|
3794
4014
|
try {
|
|
3795
|
-
snapshots.push({ path: b.path, prevContent:
|
|
4015
|
+
snapshots.push({ path: b.path, prevContent: readFileSync5(abs, "utf8") });
|
|
3796
4016
|
} catch {
|
|
3797
4017
|
snapshots.push({ path: b.path, prevContent: null });
|
|
3798
4018
|
}
|
|
@@ -3812,7 +4032,7 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
3812
4032
|
}
|
|
3813
4033
|
try {
|
|
3814
4034
|
if (snap.prevContent === null) {
|
|
3815
|
-
if (
|
|
4035
|
+
if (existsSync4(abs)) unlinkSync2(abs);
|
|
3816
4036
|
return {
|
|
3817
4037
|
path: snap.path,
|
|
3818
4038
|
status: "applied",
|
|
@@ -3835,10 +4055,31 @@ function sep() {
|
|
|
3835
4055
|
}
|
|
3836
4056
|
|
|
3837
4057
|
// src/code/prompt.ts
|
|
3838
|
-
import { existsSync as
|
|
3839
|
-
import { join as
|
|
4058
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
4059
|
+
import { join as join5 } from "path";
|
|
3840
4060
|
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.
|
|
3841
4061
|
|
|
4062
|
+
# When to propose a plan (submit_plan)
|
|
4063
|
+
|
|
4064
|
+
You have a \`submit_plan\` tool that shows the user a markdown plan and lets them Approve / Refine / Cancel before you execute. Use it proactively when the task is large enough to deserve a review gate:
|
|
4065
|
+
|
|
4066
|
+
- Multi-file refactors or renames.
|
|
4067
|
+
- Architecture changes (moving modules, splitting / merging files, new abstractions).
|
|
4068
|
+
- Anything where "undo" after the fact would be expensive \u2014 migrations, destructive cleanups, API shape changes.
|
|
4069
|
+
- When the user's request is ambiguous and multiple reasonable interpretations exist \u2014 propose your reading as a plan and let them confirm.
|
|
4070
|
+
|
|
4071
|
+
Skip submit_plan for small, obvious changes: one-line typo, clear bug with a clear fix, adding a missing import, renaming a local variable. Just do those.
|
|
4072
|
+
|
|
4073
|
+
Plan body: one-sentence summary, then a file-by-file breakdown of what you'll change and why, and any risks or open questions. If some decisions are genuinely up to the user (naming, tradeoffs, out-of-scope possibilities), list them in an "Open questions" or "\u5F85\u786E\u8BA4" section \u2014 the user sees the plan in a picker and has a text input to answer your questions before approving. Don't pretend certainty you don't have; flagged questions are how the user tells you what they care about. After calling submit_plan, STOP \u2014 don't call any more tools, wait for the user's verdict.
|
|
4074
|
+
|
|
4075
|
+
# Plan mode (/plan)
|
|
4076
|
+
|
|
4077
|
+
The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
|
|
4078
|
+
- Write tools (edit_file, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
|
|
4079
|
+
- Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted shell (git status/log/diff, ls, cat, grep, cargo check, npm test) still work \u2014 use them to investigate.
|
|
4080
|
+
- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
|
|
4081
|
+
|
|
4082
|
+
|
|
3842
4083
|
# When to edit vs. when to explore
|
|
3843
4084
|
|
|
3844
4085
|
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:
|
|
@@ -3885,18 +4126,19 @@ Rules:
|
|
|
3885
4126
|
- If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
|
|
3886
4127
|
`;
|
|
3887
4128
|
function codeSystemPrompt(rootDir) {
|
|
3888
|
-
const
|
|
3889
|
-
|
|
4129
|
+
const withMemory = applyProjectMemory(CODE_SYSTEM_PROMPT, rootDir);
|
|
4130
|
+
const gitignorePath = join5(rootDir, ".gitignore");
|
|
4131
|
+
if (!existsSync5(gitignorePath)) return withMemory;
|
|
3890
4132
|
let content;
|
|
3891
4133
|
try {
|
|
3892
|
-
content =
|
|
4134
|
+
content = readFileSync6(gitignorePath, "utf8");
|
|
3893
4135
|
} catch {
|
|
3894
|
-
return
|
|
4136
|
+
return withMemory;
|
|
3895
4137
|
}
|
|
3896
4138
|
const MAX = 2e3;
|
|
3897
4139
|
const truncated = content.length > MAX ? `${content.slice(0, MAX)}
|
|
3898
4140
|
\u2026 (truncated ${content.length - MAX} chars)` : content;
|
|
3899
|
-
return `${
|
|
4141
|
+
return `${withMemory}
|
|
3900
4142
|
|
|
3901
4143
|
# Project .gitignore
|
|
3902
4144
|
|
|
@@ -3909,15 +4151,15 @@ ${truncated}
|
|
|
3909
4151
|
}
|
|
3910
4152
|
|
|
3911
4153
|
// src/config.ts
|
|
3912
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as
|
|
4154
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
3913
4155
|
import { homedir as homedir2 } from "os";
|
|
3914
|
-
import { dirname as dirname4, join as
|
|
4156
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
3915
4157
|
function defaultConfigPath() {
|
|
3916
|
-
return
|
|
4158
|
+
return join6(homedir2(), ".reasonix", "config.json");
|
|
3917
4159
|
}
|
|
3918
4160
|
function readConfig(path = defaultConfigPath()) {
|
|
3919
4161
|
try {
|
|
3920
|
-
const raw =
|
|
4162
|
+
const raw = readFileSync7(path, "utf8");
|
|
3921
4163
|
const parsed = JSON.parse(raw);
|
|
3922
4164
|
if (parsed && typeof parsed === "object") return parsed;
|
|
3923
4165
|
} catch {
|
|
@@ -3952,7 +4194,7 @@ function redactKey(key) {
|
|
|
3952
4194
|
}
|
|
3953
4195
|
|
|
3954
4196
|
// src/index.ts
|
|
3955
|
-
var VERSION = "0.4.
|
|
4197
|
+
var VERSION = "0.4.19";
|
|
3956
4198
|
export {
|
|
3957
4199
|
AppendOnlyLog,
|
|
3958
4200
|
CODE_SYSTEM_PROMPT,
|
|
@@ -3963,6 +4205,9 @@ export {
|
|
|
3963
4205
|
MCP_PROTOCOL_VERSION,
|
|
3964
4206
|
McpClient,
|
|
3965
4207
|
NeedsConfirmationError,
|
|
4208
|
+
PROJECT_MEMORY_FILE,
|
|
4209
|
+
PROJECT_MEMORY_MAX_CHARS,
|
|
4210
|
+
PlanProposedError,
|
|
3966
4211
|
SessionStats,
|
|
3967
4212
|
SseTransport,
|
|
3968
4213
|
StdioTransport,
|
|
@@ -3977,6 +4222,7 @@ export {
|
|
|
3977
4222
|
appendSessionMessage,
|
|
3978
4223
|
applyEditBlock,
|
|
3979
4224
|
applyEditBlocks,
|
|
4225
|
+
applyProjectMemory,
|
|
3980
4226
|
bridgeMcpTools,
|
|
3981
4227
|
claudeEquivalentCost,
|
|
3982
4228
|
codeSystemPrompt,
|
|
@@ -4006,6 +4252,7 @@ export {
|
|
|
4006
4252
|
loadApiKey,
|
|
4007
4253
|
loadDotenv,
|
|
4008
4254
|
loadSessionMessages,
|
|
4255
|
+
memoryEnabled,
|
|
4009
4256
|
nestArguments,
|
|
4010
4257
|
openTranscriptFile,
|
|
4011
4258
|
outputCostUsd,
|
|
@@ -4013,17 +4260,22 @@ export {
|
|
|
4013
4260
|
parseMcpSpec,
|
|
4014
4261
|
parseMojeekResults,
|
|
4015
4262
|
parseTranscript,
|
|
4263
|
+
prepareSpawn,
|
|
4264
|
+
quoteForCmdExe,
|
|
4016
4265
|
readConfig,
|
|
4266
|
+
readProjectMemory,
|
|
4017
4267
|
readTranscript,
|
|
4018
4268
|
recordFromLoopEvent,
|
|
4019
4269
|
redactKey,
|
|
4020
4270
|
registerFilesystemTools,
|
|
4271
|
+
registerPlanTool,
|
|
4021
4272
|
registerShellTools,
|
|
4022
4273
|
registerWebTools,
|
|
4023
4274
|
renderMarkdown as renderDiffMarkdown,
|
|
4024
4275
|
renderSummaryTable as renderDiffSummary,
|
|
4025
4276
|
repairTruncatedJson,
|
|
4026
4277
|
replayFromFile,
|
|
4278
|
+
resolveExecutable,
|
|
4027
4279
|
restoreSnapshots,
|
|
4028
4280
|
runBranches,
|
|
4029
4281
|
runCommand,
|