reasonix 0.4.17 → 0.4.20
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 +89 -2
- package/dist/cli/chunk-DDIKQZVD.js +445 -0
- package/dist/cli/chunk-DDIKQZVD.js.map +1 -0
- package/dist/cli/index.js +947 -282
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-HK5XLH55.js → prompt-YEJEJ3IZ.js} +2 -2
- package/dist/index.d.ts +296 -4
- package/dist/index.js +680 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-3YQRWFES.js +0 -131
- package/dist/cli/chunk-3YQRWFES.js.map +0 -1
- /package/dist/cli/{prompt-HK5XLH55.js.map → prompt-YEJEJ3IZ.js.map} +0 -0
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((resolve6, reject) => {
|
|
51
|
+
const timer = setTimeout(resolve6, ms);
|
|
52
52
|
if (signal) {
|
|
53
53
|
const onAbort = () => {
|
|
54
54
|
clearTimeout(timer);
|
|
@@ -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;
|
|
@@ -1488,8 +1537,8 @@ var CacheFirstLoop = class {
|
|
|
1488
1537
|
}
|
|
1489
1538
|
);
|
|
1490
1539
|
for (let k = 0; k < budget; k++) {
|
|
1491
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1492
|
-
waiter =
|
|
1540
|
+
const sample = queue.shift() ?? await new Promise((resolve6) => {
|
|
1541
|
+
waiter = resolve6;
|
|
1493
1542
|
});
|
|
1494
1543
|
yield {
|
|
1495
1544
|
turn: this._turn,
|
|
@@ -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;
|
|
@@ -1891,6 +1950,291 @@ ${mem.content}
|
|
|
1891
1950
|
`;
|
|
1892
1951
|
}
|
|
1893
1952
|
|
|
1953
|
+
// src/user-memory.ts
|
|
1954
|
+
import { createHash as createHash2 } from "crypto";
|
|
1955
|
+
import {
|
|
1956
|
+
existsSync as existsSync3,
|
|
1957
|
+
mkdirSync as mkdirSync2,
|
|
1958
|
+
readFileSync as readFileSync3,
|
|
1959
|
+
readdirSync as readdirSync2,
|
|
1960
|
+
unlinkSync as unlinkSync2,
|
|
1961
|
+
writeFileSync as writeFileSync2
|
|
1962
|
+
} from "fs";
|
|
1963
|
+
import { homedir as homedir2 } from "os";
|
|
1964
|
+
import { join as join3, resolve } from "path";
|
|
1965
|
+
var USER_MEMORY_DIR = "memory";
|
|
1966
|
+
var MEMORY_INDEX_FILE = "MEMORY.md";
|
|
1967
|
+
var MEMORY_INDEX_MAX_CHARS = 4e3;
|
|
1968
|
+
var VALID_NAME = /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{1,38}[a-zA-Z0-9]$/;
|
|
1969
|
+
function sanitizeMemoryName(raw) {
|
|
1970
|
+
const trimmed = String(raw ?? "").trim();
|
|
1971
|
+
if (!VALID_NAME.test(trimmed)) {
|
|
1972
|
+
throw new Error(
|
|
1973
|
+
`invalid memory name: ${JSON.stringify(raw)} \u2014 must be 3-40 chars, alnum/_/-, no path separators`
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
return trimmed;
|
|
1977
|
+
}
|
|
1978
|
+
function projectHash(rootDir) {
|
|
1979
|
+
const abs = resolve(rootDir);
|
|
1980
|
+
return createHash2("sha1").update(abs).digest("hex").slice(0, 16);
|
|
1981
|
+
}
|
|
1982
|
+
function scopeDir(opts) {
|
|
1983
|
+
if (opts.scope === "global") {
|
|
1984
|
+
return join3(opts.homeDir, USER_MEMORY_DIR, "global");
|
|
1985
|
+
}
|
|
1986
|
+
if (!opts.projectRoot) {
|
|
1987
|
+
throw new Error("scope=project requires a projectRoot on MemoryStore");
|
|
1988
|
+
}
|
|
1989
|
+
return join3(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
|
|
1990
|
+
}
|
|
1991
|
+
function ensureDir(p) {
|
|
1992
|
+
if (!existsSync3(p)) mkdirSync2(p, { recursive: true });
|
|
1993
|
+
}
|
|
1994
|
+
function parseFrontmatter(raw) {
|
|
1995
|
+
const lines = raw.split(/\r?\n/);
|
|
1996
|
+
if (lines[0] !== "---") return { data: {}, body: raw };
|
|
1997
|
+
const end = lines.indexOf("---", 1);
|
|
1998
|
+
if (end < 0) return { data: {}, body: raw };
|
|
1999
|
+
const data = {};
|
|
2000
|
+
for (let i = 1; i < end; i++) {
|
|
2001
|
+
const line = lines[i];
|
|
2002
|
+
if (!line) continue;
|
|
2003
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
2004
|
+
if (m?.[1]) data[m[1]] = (m[2] ?? "").trim();
|
|
2005
|
+
}
|
|
2006
|
+
return {
|
|
2007
|
+
data,
|
|
2008
|
+
body: lines.slice(end + 1).join("\n").replace(/^\n+/, "")
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
function formatFrontmatter(e) {
|
|
2012
|
+
return [
|
|
2013
|
+
"---",
|
|
2014
|
+
`name: ${e.name}`,
|
|
2015
|
+
`description: ${e.description.replace(/\n/g, " ")}`,
|
|
2016
|
+
`type: ${e.type}`,
|
|
2017
|
+
`scope: ${e.scope}`,
|
|
2018
|
+
`created: ${e.createdAt}`,
|
|
2019
|
+
"---",
|
|
2020
|
+
""
|
|
2021
|
+
].join("\n");
|
|
2022
|
+
}
|
|
2023
|
+
function todayIso() {
|
|
2024
|
+
const d = /* @__PURE__ */ new Date();
|
|
2025
|
+
return d.toISOString().slice(0, 10);
|
|
2026
|
+
}
|
|
2027
|
+
function indexLine(e) {
|
|
2028
|
+
const safeDesc = e.description.replace(/\n/g, " ").trim();
|
|
2029
|
+
const max = 130 - e.name.length;
|
|
2030
|
+
const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
|
|
2031
|
+
return `- [${e.name}](${e.name}.md) \u2014 ${clipped}`;
|
|
2032
|
+
}
|
|
2033
|
+
var MemoryStore = class {
|
|
2034
|
+
homeDir;
|
|
2035
|
+
projectRoot;
|
|
2036
|
+
constructor(opts = {}) {
|
|
2037
|
+
this.homeDir = opts.homeDir ?? join3(homedir2(), ".reasonix");
|
|
2038
|
+
this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : void 0;
|
|
2039
|
+
}
|
|
2040
|
+
/** Directory this store writes `scope` files into, creating it if needed. */
|
|
2041
|
+
dir(scope) {
|
|
2042
|
+
const d = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2043
|
+
ensureDir(d);
|
|
2044
|
+
return d;
|
|
2045
|
+
}
|
|
2046
|
+
/** Absolute path to a memory file (no existence check). */
|
|
2047
|
+
pathFor(scope, name) {
|
|
2048
|
+
return join3(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
|
|
2049
|
+
}
|
|
2050
|
+
/** True iff this store is configured with a project scope available. */
|
|
2051
|
+
hasProjectScope() {
|
|
2052
|
+
return this.projectRoot !== void 0;
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Read the `MEMORY.md` index for a scope. Returns post-cap content
|
|
2056
|
+
* (with a truncation marker if clipped), or `null` when absent / empty.
|
|
2057
|
+
*/
|
|
2058
|
+
loadIndex(scope) {
|
|
2059
|
+
if (scope === "project" && !this.projectRoot) return null;
|
|
2060
|
+
const file = join3(
|
|
2061
|
+
scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
|
|
2062
|
+
MEMORY_INDEX_FILE
|
|
2063
|
+
);
|
|
2064
|
+
if (!existsSync3(file)) return null;
|
|
2065
|
+
let raw;
|
|
2066
|
+
try {
|
|
2067
|
+
raw = readFileSync3(file, "utf8");
|
|
2068
|
+
} catch {
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
const trimmed = raw.trim();
|
|
2072
|
+
if (!trimmed) return null;
|
|
2073
|
+
const originalChars = trimmed.length;
|
|
2074
|
+
const truncated = originalChars > MEMORY_INDEX_MAX_CHARS;
|
|
2075
|
+
const content = truncated ? `${trimmed.slice(0, MEMORY_INDEX_MAX_CHARS)}
|
|
2076
|
+
\u2026 (truncated ${originalChars - MEMORY_INDEX_MAX_CHARS} chars)` : trimmed;
|
|
2077
|
+
return { content, originalChars, truncated };
|
|
2078
|
+
}
|
|
2079
|
+
/** Read one memory file's body (frontmatter stripped). Throws if missing. */
|
|
2080
|
+
read(scope, name) {
|
|
2081
|
+
const file = this.pathFor(scope, name);
|
|
2082
|
+
if (!existsSync3(file)) {
|
|
2083
|
+
throw new Error(`memory not found: scope=${scope} name=${name}`);
|
|
2084
|
+
}
|
|
2085
|
+
const raw = readFileSync3(file, "utf8");
|
|
2086
|
+
const { data, body } = parseFrontmatter(raw);
|
|
2087
|
+
return {
|
|
2088
|
+
name: data.name ?? name,
|
|
2089
|
+
type: data.type ?? "project",
|
|
2090
|
+
scope: data.scope ?? scope,
|
|
2091
|
+
description: data.description ?? "",
|
|
2092
|
+
body: body.trim(),
|
|
2093
|
+
createdAt: data.created ?? ""
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* List every memory in this store. Scans both scopes (skips project
|
|
2098
|
+
* scope if unconfigured). Silently skips malformed files; the index
|
|
2099
|
+
* must stay queryable even if one file is hand-edited into nonsense.
|
|
2100
|
+
*/
|
|
2101
|
+
list() {
|
|
2102
|
+
const out = [];
|
|
2103
|
+
const scopes = this.projectRoot ? ["global", "project"] : ["global"];
|
|
2104
|
+
for (const scope of scopes) {
|
|
2105
|
+
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2106
|
+
if (!existsSync3(dir)) continue;
|
|
2107
|
+
let entries;
|
|
2108
|
+
try {
|
|
2109
|
+
entries = readdirSync2(dir);
|
|
2110
|
+
} catch {
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
for (const entry of entries) {
|
|
2114
|
+
if (entry === MEMORY_INDEX_FILE) continue;
|
|
2115
|
+
if (!entry.endsWith(".md")) continue;
|
|
2116
|
+
const name = entry.slice(0, -3);
|
|
2117
|
+
try {
|
|
2118
|
+
out.push(this.read(scope, name));
|
|
2119
|
+
} catch {
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return out;
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Write a new memory (or overwrite existing). Creates the scope dir,
|
|
2127
|
+
* writes the `.md` file, and regenerates `MEMORY.md`. Returns the
|
|
2128
|
+
* absolute path written to.
|
|
2129
|
+
*/
|
|
2130
|
+
write(input) {
|
|
2131
|
+
if (input.scope === "project" && !this.projectRoot) {
|
|
2132
|
+
throw new Error("cannot write project-scoped memory: no projectRoot configured");
|
|
2133
|
+
}
|
|
2134
|
+
const name = sanitizeMemoryName(input.name);
|
|
2135
|
+
const desc = String(input.description ?? "").trim();
|
|
2136
|
+
if (!desc) throw new Error("memory description cannot be empty");
|
|
2137
|
+
const body = String(input.body ?? "").trim();
|
|
2138
|
+
if (!body) throw new Error("memory body cannot be empty");
|
|
2139
|
+
const entry = {
|
|
2140
|
+
...input,
|
|
2141
|
+
name,
|
|
2142
|
+
description: desc,
|
|
2143
|
+
body,
|
|
2144
|
+
createdAt: todayIso()
|
|
2145
|
+
};
|
|
2146
|
+
const dir = this.dir(input.scope);
|
|
2147
|
+
const file = join3(dir, `${name}.md`);
|
|
2148
|
+
const content = `${formatFrontmatter(entry)}${body}
|
|
2149
|
+
`;
|
|
2150
|
+
writeFileSync2(file, content, "utf8");
|
|
2151
|
+
this.regenerateIndex(input.scope);
|
|
2152
|
+
return file;
|
|
2153
|
+
}
|
|
2154
|
+
/** Delete one memory + its index line. No-op if the file is already gone. */
|
|
2155
|
+
delete(scope, rawName) {
|
|
2156
|
+
if (scope === "project" && !this.projectRoot) {
|
|
2157
|
+
throw new Error("cannot delete project-scoped memory: no projectRoot configured");
|
|
2158
|
+
}
|
|
2159
|
+
const file = this.pathFor(scope, rawName);
|
|
2160
|
+
if (!existsSync3(file)) return false;
|
|
2161
|
+
unlinkSync2(file);
|
|
2162
|
+
this.regenerateIndex(scope);
|
|
2163
|
+
return true;
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Rebuild `MEMORY.md` from the `.md` files currently in the scope dir.
|
|
2167
|
+
* Called after every write/delete. Sorted by name for stable prefix
|
|
2168
|
+
* hashing — two stores with the same set of files produce byte-identical
|
|
2169
|
+
* MEMORY.md content, keeping the cache prefix reproducible.
|
|
2170
|
+
*/
|
|
2171
|
+
regenerateIndex(scope) {
|
|
2172
|
+
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2173
|
+
if (!existsSync3(dir)) return;
|
|
2174
|
+
let files;
|
|
2175
|
+
try {
|
|
2176
|
+
files = readdirSync2(dir);
|
|
2177
|
+
} catch {
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
|
|
2181
|
+
const indexPath = join3(dir, MEMORY_INDEX_FILE);
|
|
2182
|
+
if (mdFiles.length === 0) {
|
|
2183
|
+
if (existsSync3(indexPath)) unlinkSync2(indexPath);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const lines = [];
|
|
2187
|
+
for (const f of mdFiles) {
|
|
2188
|
+
const name = f.slice(0, -3);
|
|
2189
|
+
try {
|
|
2190
|
+
const entry = this.read(scope, name);
|
|
2191
|
+
lines.push(indexLine({ name: entry.name || name, description: entry.description }));
|
|
2192
|
+
} catch {
|
|
2193
|
+
lines.push(`- [${name}](${name}.md) \u2014 (malformed, check frontmatter)`);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
writeFileSync2(indexPath, `${lines.join("\n")}
|
|
2197
|
+
`, "utf8");
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
function applyUserMemory(basePrompt, opts = {}) {
|
|
2201
|
+
if (!memoryEnabled()) return basePrompt;
|
|
2202
|
+
const store = new MemoryStore(opts);
|
|
2203
|
+
const global = store.loadIndex("global");
|
|
2204
|
+
const project = store.hasProjectScope() ? store.loadIndex("project") : null;
|
|
2205
|
+
if (!global && !project) return basePrompt;
|
|
2206
|
+
const parts = [basePrompt];
|
|
2207
|
+
if (global) {
|
|
2208
|
+
parts.push(
|
|
2209
|
+
"",
|
|
2210
|
+
"# User memory \u2014 global (~/.reasonix/memory/global/MEMORY.md)",
|
|
2211
|
+
"",
|
|
2212
|
+
"Cross-project facts and preferences the user has told you in prior sessions. TREAT AS AUTHORITATIVE \u2014 don't re-verify via filesystem or web. One-liners index detail files; call `recall_memory` for full bodies only when the one-liner isn't enough.",
|
|
2213
|
+
"",
|
|
2214
|
+
"```",
|
|
2215
|
+
global.content,
|
|
2216
|
+
"```"
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
if (project) {
|
|
2220
|
+
parts.push(
|
|
2221
|
+
"",
|
|
2222
|
+
"# User memory \u2014 this project",
|
|
2223
|
+
"",
|
|
2224
|
+
"Per-project facts the user established in prior sessions (not committed to the repo). TREAT AS AUTHORITATIVE. Same recall pattern as global memory.",
|
|
2225
|
+
"",
|
|
2226
|
+
"```",
|
|
2227
|
+
project.content,
|
|
2228
|
+
"```"
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
return parts.join("\n");
|
|
2232
|
+
}
|
|
2233
|
+
function applyMemoryStack(basePrompt, rootDir) {
|
|
2234
|
+
const withProject = applyProjectMemory(basePrompt, rootDir);
|
|
2235
|
+
return applyUserMemory(withProject, { projectRoot: rootDir });
|
|
2236
|
+
}
|
|
2237
|
+
|
|
1894
2238
|
// src/tools/filesystem.ts
|
|
1895
2239
|
import { promises as fs } from "fs";
|
|
1896
2240
|
import * as pathMod from "path";
|
|
@@ -1916,6 +2260,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1916
2260
|
registry.register({
|
|
1917
2261
|
name: "read_file",
|
|
1918
2262
|
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.",
|
|
2263
|
+
readOnly: true,
|
|
1919
2264
|
parameters: {
|
|
1920
2265
|
type: "object",
|
|
1921
2266
|
properties: {
|
|
@@ -1953,6 +2298,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1953
2298
|
registry.register({
|
|
1954
2299
|
name: "list_directory",
|
|
1955
2300
|
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.",
|
|
2301
|
+
readOnly: true,
|
|
1956
2302
|
parameters: {
|
|
1957
2303
|
type: "object",
|
|
1958
2304
|
properties: {
|
|
@@ -1972,6 +2318,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1972
2318
|
registry.register({
|
|
1973
2319
|
name: "directory_tree",
|
|
1974
2320
|
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.",
|
|
2321
|
+
readOnly: true,
|
|
1975
2322
|
parameters: {
|
|
1976
2323
|
type: "object",
|
|
1977
2324
|
properties: {
|
|
@@ -2018,6 +2365,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2018
2365
|
registry.register({
|
|
2019
2366
|
name: "search_files",
|
|
2020
2367
|
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.",
|
|
2368
|
+
readOnly: true,
|
|
2021
2369
|
parameters: {
|
|
2022
2370
|
type: "object",
|
|
2023
2371
|
properties: {
|
|
@@ -2070,6 +2418,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2070
2418
|
registry.register({
|
|
2071
2419
|
name: "get_file_info",
|
|
2072
2420
|
description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
|
|
2421
|
+
readOnly: true,
|
|
2073
2422
|
parameters: {
|
|
2074
2423
|
type: "object",
|
|
2075
2424
|
properties: {
|
|
@@ -2109,7 +2458,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2109
2458
|
});
|
|
2110
2459
|
registry.register({
|
|
2111
2460
|
name: "edit_file",
|
|
2112
|
-
description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.
|
|
2461
|
+
description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.",
|
|
2113
2462
|
parameters: {
|
|
2114
2463
|
type: "object",
|
|
2115
2464
|
properties: {
|
|
@@ -2226,8 +2575,175 @@ function lineDiff(a, b) {
|
|
|
2226
2575
|
return out;
|
|
2227
2576
|
}
|
|
2228
2577
|
|
|
2578
|
+
// src/tools/memory.ts
|
|
2579
|
+
function registerMemoryTools(registry, opts = {}) {
|
|
2580
|
+
const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
|
|
2581
|
+
const hasProject = store.hasProjectScope();
|
|
2582
|
+
registry.register({
|
|
2583
|
+
name: "remember",
|
|
2584
|
+
description: "Save a memory for future sessions. Use when the user states a preference, corrects your approach, shares a non-obvious fact about this project, or explicitly asks you to remember something. Don't remember transient task state \u2014 only things worth recalling next session. The memory is written now but won't re-load into the system prompt until the next `/new` or launch.",
|
|
2585
|
+
parameters: {
|
|
2586
|
+
type: "object",
|
|
2587
|
+
properties: {
|
|
2588
|
+
type: {
|
|
2589
|
+
type: "string",
|
|
2590
|
+
enum: ["user", "feedback", "project", "reference"],
|
|
2591
|
+
description: "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
|
|
2592
|
+
},
|
|
2593
|
+
scope: {
|
|
2594
|
+
type: "string",
|
|
2595
|
+
enum: ["global", "project"],
|
|
2596
|
+
description: "'global' = applies across every project (preferences, tooling); 'project' = scoped to the current sandbox (decisions, local facts). Only available in `reasonix code`."
|
|
2597
|
+
},
|
|
2598
|
+
name: {
|
|
2599
|
+
type: "string",
|
|
2600
|
+
description: "filename-safe identifier, 3-40 chars, alnum + _ - . (no path separators, no leading dot)."
|
|
2601
|
+
},
|
|
2602
|
+
description: {
|
|
2603
|
+
type: "string",
|
|
2604
|
+
description: "One-line summary shown in MEMORY.md (under ~150 chars)."
|
|
2605
|
+
},
|
|
2606
|
+
content: {
|
|
2607
|
+
type: "string",
|
|
2608
|
+
description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2611
|
+
required: ["type", "scope", "name", "description", "content"]
|
|
2612
|
+
},
|
|
2613
|
+
fn: async (args) => {
|
|
2614
|
+
if (args.scope === "project" && !hasProject) {
|
|
2615
|
+
return JSON.stringify({
|
|
2616
|
+
error: "scope='project' is unavailable in this session (no sandbox root). Retry with scope='global', or ask the user to switch to `reasonix code` for project-scoped memory."
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
try {
|
|
2620
|
+
const path = store.write({
|
|
2621
|
+
name: args.name,
|
|
2622
|
+
type: args.type,
|
|
2623
|
+
scope: args.scope,
|
|
2624
|
+
description: args.description,
|
|
2625
|
+
body: args.content
|
|
2626
|
+
});
|
|
2627
|
+
const key = sanitizeMemoryName(args.name);
|
|
2628
|
+
return [
|
|
2629
|
+
`\u2713 REMEMBERED (${args.scope}/${key}): ${args.description}`,
|
|
2630
|
+
"",
|
|
2631
|
+
"TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
|
|
2632
|
+
"The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
|
|
2633
|
+
`(Saved to ${path}; pins into the system prompt on next /new or launch.)`
|
|
2634
|
+
].join("\n");
|
|
2635
|
+
} catch (err) {
|
|
2636
|
+
return JSON.stringify({ error: `remember failed: ${err.message}` });
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
registry.register({
|
|
2641
|
+
name: "forget",
|
|
2642
|
+
description: "Delete a memory file and remove it from MEMORY.md. Use when the user explicitly asks to forget something, or when a previously-remembered fact has become wrong. Irreversible \u2014 no tombstone.",
|
|
2643
|
+
parameters: {
|
|
2644
|
+
type: "object",
|
|
2645
|
+
properties: {
|
|
2646
|
+
name: { type: "string", description: "Memory name (the identifier used in `remember`)." },
|
|
2647
|
+
scope: { type: "string", enum: ["global", "project"] }
|
|
2648
|
+
},
|
|
2649
|
+
required: ["name", "scope"]
|
|
2650
|
+
},
|
|
2651
|
+
fn: async (args) => {
|
|
2652
|
+
if (args.scope === "project" && !hasProject) {
|
|
2653
|
+
return JSON.stringify({
|
|
2654
|
+
error: "scope='project' is unavailable in this session (no sandbox root)."
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
try {
|
|
2658
|
+
const existed = store.delete(args.scope, args.name);
|
|
2659
|
+
return existed ? `forgot (${args.scope}/${sanitizeMemoryName(args.name)}). Re-load on next /new or launch.` : `no such memory: ${args.scope}/${args.name} (nothing to forget).`;
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
return JSON.stringify({ error: `forget failed: ${err.message}` });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
registry.register({
|
|
2666
|
+
name: "recall_memory",
|
|
2667
|
+
description: "Read the full body of a memory file when its MEMORY.md one-liner (already in the system prompt) isn't enough detail. Most of the time the index suffices \u2014 only call this when the user's question genuinely requires the full context.",
|
|
2668
|
+
readOnly: true,
|
|
2669
|
+
parameters: {
|
|
2670
|
+
type: "object",
|
|
2671
|
+
properties: {
|
|
2672
|
+
name: { type: "string" },
|
|
2673
|
+
scope: { type: "string", enum: ["global", "project"] }
|
|
2674
|
+
},
|
|
2675
|
+
required: ["name", "scope"]
|
|
2676
|
+
},
|
|
2677
|
+
fn: async (args) => {
|
|
2678
|
+
if (args.scope === "project" && !hasProject) {
|
|
2679
|
+
return JSON.stringify({
|
|
2680
|
+
error: "scope='project' is unavailable in this session (no sandbox root)."
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
try {
|
|
2684
|
+
const entry = store.read(args.scope, args.name);
|
|
2685
|
+
return [
|
|
2686
|
+
`# ${entry.name} (${entry.scope}/${entry.type}, created ${entry.createdAt || "?"})`,
|
|
2687
|
+
entry.description ? `> ${entry.description}` : "",
|
|
2688
|
+
"",
|
|
2689
|
+
entry.body
|
|
2690
|
+
].filter(Boolean).join("\n");
|
|
2691
|
+
} catch (err) {
|
|
2692
|
+
return JSON.stringify({ error: `recall failed: ${err.message}` });
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
return registry;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
// src/tools/plan.ts
|
|
2700
|
+
var PlanProposedError = class extends Error {
|
|
2701
|
+
plan;
|
|
2702
|
+
constructor(plan) {
|
|
2703
|
+
super(
|
|
2704
|
+
"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."
|
|
2705
|
+
);
|
|
2706
|
+
this.name = "PlanProposedError";
|
|
2707
|
+
this.plan = plan;
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Structured tool-result shape. Consumed by the TUI to extract the
|
|
2711
|
+
* plan without regex-scraping the error message.
|
|
2712
|
+
*/
|
|
2713
|
+
toToolResult() {
|
|
2714
|
+
return { error: `${this.name}: ${this.message}`, plan: this.plan };
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
function registerPlanTool(registry, opts = {}) {
|
|
2718
|
+
registry.register({
|
|
2719
|
+
name: "submit_plan",
|
|
2720
|
+
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.",
|
|
2721
|
+
readOnly: true,
|
|
2722
|
+
parameters: {
|
|
2723
|
+
type: "object",
|
|
2724
|
+
properties: {
|
|
2725
|
+
plan: {
|
|
2726
|
+
type: "string",
|
|
2727
|
+
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."
|
|
2728
|
+
}
|
|
2729
|
+
},
|
|
2730
|
+
required: ["plan"]
|
|
2731
|
+
},
|
|
2732
|
+
fn: async (args) => {
|
|
2733
|
+
const plan = (args?.plan ?? "").trim();
|
|
2734
|
+
if (!plan) {
|
|
2735
|
+
throw new Error("submit_plan: empty plan \u2014 write a markdown plan and try again.");
|
|
2736
|
+
}
|
|
2737
|
+
opts.onPlanSubmitted?.(plan);
|
|
2738
|
+
throw new PlanProposedError(plan);
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
return registry;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2229
2744
|
// src/tools/shell.ts
|
|
2230
2745
|
import { spawn } from "child_process";
|
|
2746
|
+
import { existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
2231
2747
|
import * as pathMod2 from "path";
|
|
2232
2748
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2233
2749
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
@@ -2345,10 +2861,12 @@ async function runCommand(cmd, opts) {
|
|
|
2345
2861
|
windowsHide: true,
|
|
2346
2862
|
env: process.env
|
|
2347
2863
|
};
|
|
2348
|
-
|
|
2864
|
+
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
2865
|
+
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
2866
|
+
return await new Promise((resolve6, reject) => {
|
|
2349
2867
|
let child;
|
|
2350
2868
|
try {
|
|
2351
|
-
child = spawn(
|
|
2869
|
+
child = spawn(bin, args, effectiveSpawnOpts);
|
|
2352
2870
|
} catch (err) {
|
|
2353
2871
|
reject(err);
|
|
2354
2872
|
return;
|
|
@@ -2378,10 +2896,63 @@ async function runCommand(cmd, opts) {
|
|
|
2378
2896
|
const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
|
|
2379
2897
|
|
|
2380
2898
|
[\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
|
|
2381
|
-
|
|
2899
|
+
resolve6({ exitCode: code, output, timedOut });
|
|
2382
2900
|
});
|
|
2383
2901
|
});
|
|
2384
2902
|
}
|
|
2903
|
+
function resolveExecutable(cmd, opts = {}) {
|
|
2904
|
+
const platform = opts.platform ?? process.platform;
|
|
2905
|
+
if (platform !== "win32") return cmd;
|
|
2906
|
+
if (!cmd) return cmd;
|
|
2907
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod2.isAbsolute(cmd)) return cmd;
|
|
2908
|
+
if (pathMod2.extname(cmd)) return cmd;
|
|
2909
|
+
const env = opts.env ?? process.env;
|
|
2910
|
+
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
2911
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod2.delimiter);
|
|
2912
|
+
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
2913
|
+
const isFile = opts.isFile ?? defaultIsFile;
|
|
2914
|
+
for (const dir of pathDirs) {
|
|
2915
|
+
for (const ext of pathExt) {
|
|
2916
|
+
const full = pathMod2.win32.join(dir, cmd + ext);
|
|
2917
|
+
if (isFile(full)) return full;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
return cmd;
|
|
2921
|
+
}
|
|
2922
|
+
function defaultIsFile(full) {
|
|
2923
|
+
try {
|
|
2924
|
+
return existsSync4(full) && statSync2(full).isFile();
|
|
2925
|
+
} catch {
|
|
2926
|
+
return false;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
function prepareSpawn(argv, opts = {}) {
|
|
2930
|
+
const head = argv[0] ?? "";
|
|
2931
|
+
const tail = argv.slice(1);
|
|
2932
|
+
const platform = opts.platform ?? process.platform;
|
|
2933
|
+
const resolved = resolveExecutable(head, opts);
|
|
2934
|
+
if (platform !== "win32") {
|
|
2935
|
+
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2936
|
+
}
|
|
2937
|
+
if (/\.(cmd|bat)$/i.test(resolved)) {
|
|
2938
|
+
const cmdline = [resolved, ...tail].map(quoteForCmdExe).join(" ");
|
|
2939
|
+
return {
|
|
2940
|
+
bin: "cmd.exe",
|
|
2941
|
+
args: ["/d", "/s", "/c", cmdline],
|
|
2942
|
+
// windowsVerbatimArguments prevents Node from re-quoting the /c
|
|
2943
|
+
// payload — we've already composed an exact cmd.exe command
|
|
2944
|
+
// line. Without this Node wraps our already-quoted string in
|
|
2945
|
+
// another round of quotes and cmd.exe can't parse it.
|
|
2946
|
+
spawnOverrides: { windowsVerbatimArguments: true }
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2950
|
+
}
|
|
2951
|
+
function quoteForCmdExe(arg) {
|
|
2952
|
+
if (arg === "") return '""';
|
|
2953
|
+
if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
|
|
2954
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
2955
|
+
}
|
|
2385
2956
|
var NeedsConfirmationError = class extends Error {
|
|
2386
2957
|
command;
|
|
2387
2958
|
constructor(command) {
|
|
@@ -2400,13 +2971,23 @@ function registerShellTools(registry, opts) {
|
|
|
2400
2971
|
const allowAll = opts.allowAll ?? false;
|
|
2401
2972
|
registry.register({
|
|
2402
2973
|
name: "run_command",
|
|
2403
|
-
description: "Run a shell command in the project root and return its combined stdout+stderr.
|
|
2974
|
+
description: "Run a shell command in the project root and return its combined stdout+stderr. Common read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
2975
|
+
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
2976
|
+
// cargo check, ls, grep …) so the model can actually investigate
|
|
2977
|
+
// during planning. Anything that would otherwise trigger a
|
|
2978
|
+
// confirmation prompt is treated as "not read-only" and bounced.
|
|
2979
|
+
readOnlyCheck: (args) => {
|
|
2980
|
+
if (allowAll) return true;
|
|
2981
|
+
const cmd = typeof args?.command === "string" ? args.command.trim() : "";
|
|
2982
|
+
if (!cmd) return false;
|
|
2983
|
+
return isAllowed(cmd, extraAllowed);
|
|
2984
|
+
},
|
|
2404
2985
|
parameters: {
|
|
2405
2986
|
type: "object",
|
|
2406
2987
|
properties: {
|
|
2407
2988
|
command: {
|
|
2408
2989
|
type: "string",
|
|
2409
|
-
description: "Full command line
|
|
2990
|
+
description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
|
|
2410
2991
|
},
|
|
2411
2992
|
timeoutSec: {
|
|
2412
2993
|
type: "integer",
|
|
@@ -2567,6 +3148,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
2567
3148
|
registry.register({
|
|
2568
3149
|
name: "web_search",
|
|
2569
3150
|
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.",
|
|
3151
|
+
readOnly: true,
|
|
2570
3152
|
parameters: {
|
|
2571
3153
|
type: "object",
|
|
2572
3154
|
properties: {
|
|
@@ -2589,6 +3171,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
2589
3171
|
registry.register({
|
|
2590
3172
|
name: "web_fetch",
|
|
2591
3173
|
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.",
|
|
3174
|
+
readOnly: true,
|
|
2592
3175
|
parameters: {
|
|
2593
3176
|
type: "object",
|
|
2594
3177
|
properties: {
|
|
@@ -2623,12 +3206,12 @@ ${i + 1}. ${r.title}`);
|
|
|
2623
3206
|
}
|
|
2624
3207
|
|
|
2625
3208
|
// src/env.ts
|
|
2626
|
-
import { readFileSync as
|
|
2627
|
-
import { resolve as
|
|
3209
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3210
|
+
import { resolve as resolve4 } from "path";
|
|
2628
3211
|
function loadDotenv(path = ".env") {
|
|
2629
3212
|
let raw;
|
|
2630
3213
|
try {
|
|
2631
|
-
raw =
|
|
3214
|
+
raw = readFileSync4(resolve4(process.cwd(), path), "utf8");
|
|
2632
3215
|
} catch {
|
|
2633
3216
|
return;
|
|
2634
3217
|
}
|
|
@@ -2647,7 +3230,7 @@ function loadDotenv(path = ".env") {
|
|
|
2647
3230
|
}
|
|
2648
3231
|
|
|
2649
3232
|
// src/transcript.ts
|
|
2650
|
-
import { createWriteStream, readFileSync as
|
|
3233
|
+
import { createWriteStream, readFileSync as readFileSync5 } from "fs";
|
|
2651
3234
|
function recordFromLoopEvent(ev, extra) {
|
|
2652
3235
|
const rec = {
|
|
2653
3236
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2698,7 +3281,7 @@ function openTranscriptFile(path, meta) {
|
|
|
2698
3281
|
return stream;
|
|
2699
3282
|
}
|
|
2700
3283
|
function readTranscript(path) {
|
|
2701
|
-
const raw =
|
|
3284
|
+
const raw = readFileSync5(path, "utf8");
|
|
2702
3285
|
return parseTranscript(raw);
|
|
2703
3286
|
}
|
|
2704
3287
|
function isPlanStateEmptyShape(s) {
|
|
@@ -3310,7 +3893,7 @@ var McpClient = class {
|
|
|
3310
3893
|
const id = this.nextId++;
|
|
3311
3894
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
3312
3895
|
let abortHandler = null;
|
|
3313
|
-
const promise = new Promise((
|
|
3896
|
+
const promise = new Promise((resolve6, reject) => {
|
|
3314
3897
|
const timeout = setTimeout(() => {
|
|
3315
3898
|
this.pending.delete(id);
|
|
3316
3899
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -3319,7 +3902,7 @@ var McpClient = class {
|
|
|
3319
3902
|
);
|
|
3320
3903
|
}, this.requestTimeoutMs);
|
|
3321
3904
|
this.pending.set(id, {
|
|
3322
|
-
resolve:
|
|
3905
|
+
resolve: resolve6,
|
|
3323
3906
|
reject,
|
|
3324
3907
|
timeout
|
|
3325
3908
|
});
|
|
@@ -3442,12 +4025,12 @@ var StdioTransport = class {
|
|
|
3442
4025
|
}
|
|
3443
4026
|
async send(message) {
|
|
3444
4027
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
3445
|
-
return new Promise((
|
|
4028
|
+
return new Promise((resolve6, reject) => {
|
|
3446
4029
|
const line = `${JSON.stringify(message)}
|
|
3447
4030
|
`;
|
|
3448
4031
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
3449
4032
|
if (err) reject(err);
|
|
3450
|
-
else
|
|
4033
|
+
else resolve6();
|
|
3451
4034
|
});
|
|
3452
4035
|
});
|
|
3453
4036
|
}
|
|
@@ -3458,8 +4041,8 @@ var StdioTransport = class {
|
|
|
3458
4041
|
continue;
|
|
3459
4042
|
}
|
|
3460
4043
|
if (this.closed) return;
|
|
3461
|
-
const next = await new Promise((
|
|
3462
|
-
this.waiters.push(
|
|
4044
|
+
const next = await new Promise((resolve6) => {
|
|
4045
|
+
this.waiters.push(resolve6);
|
|
3463
4046
|
});
|
|
3464
4047
|
if (next === null) return;
|
|
3465
4048
|
yield next;
|
|
@@ -3525,8 +4108,8 @@ var SseTransport = class {
|
|
|
3525
4108
|
constructor(opts) {
|
|
3526
4109
|
this.url = opts.url;
|
|
3527
4110
|
this.headers = opts.headers ?? {};
|
|
3528
|
-
this.endpointReady = new Promise((
|
|
3529
|
-
this.resolveEndpoint =
|
|
4111
|
+
this.endpointReady = new Promise((resolve6, reject) => {
|
|
4112
|
+
this.resolveEndpoint = resolve6;
|
|
3530
4113
|
this.rejectEndpoint = reject;
|
|
3531
4114
|
});
|
|
3532
4115
|
this.endpointReady.catch(() => void 0);
|
|
@@ -3553,8 +4136,8 @@ var SseTransport = class {
|
|
|
3553
4136
|
continue;
|
|
3554
4137
|
}
|
|
3555
4138
|
if (this.closed) return;
|
|
3556
|
-
const next = await new Promise((
|
|
3557
|
-
this.waiters.push(
|
|
4139
|
+
const next = await new Promise((resolve6) => {
|
|
4140
|
+
this.waiters.push(resolve6);
|
|
3558
4141
|
});
|
|
3559
4142
|
if (next === null) return;
|
|
3560
4143
|
yield next;
|
|
@@ -3753,8 +4336,8 @@ async function trySection(load) {
|
|
|
3753
4336
|
}
|
|
3754
4337
|
|
|
3755
4338
|
// src/code/edit-blocks.ts
|
|
3756
|
-
import { existsSync as
|
|
3757
|
-
import { dirname as dirname3, resolve as
|
|
4339
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4340
|
+
import { dirname as dirname3, resolve as resolve5 } from "path";
|
|
3758
4341
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
3759
4342
|
function parseEditBlocks(text) {
|
|
3760
4343
|
const out = [];
|
|
@@ -3772,8 +4355,8 @@ function parseEditBlocks(text) {
|
|
|
3772
4355
|
return out;
|
|
3773
4356
|
}
|
|
3774
4357
|
function applyEditBlock(block, rootDir) {
|
|
3775
|
-
const absRoot =
|
|
3776
|
-
const absTarget =
|
|
4358
|
+
const absRoot = resolve5(rootDir);
|
|
4359
|
+
const absTarget = resolve5(absRoot, block.path);
|
|
3777
4360
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
3778
4361
|
return {
|
|
3779
4362
|
path: block.path,
|
|
@@ -3782,7 +4365,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
3782
4365
|
};
|
|
3783
4366
|
}
|
|
3784
4367
|
const searchEmpty = block.search.length === 0;
|
|
3785
|
-
const exists =
|
|
4368
|
+
const exists = existsSync5(absTarget);
|
|
3786
4369
|
try {
|
|
3787
4370
|
if (!exists) {
|
|
3788
4371
|
if (!searchEmpty) {
|
|
@@ -3792,11 +4375,11 @@ function applyEditBlock(block, rootDir) {
|
|
|
3792
4375
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
3793
4376
|
};
|
|
3794
4377
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
4378
|
+
mkdirSync3(dirname3(absTarget), { recursive: true });
|
|
4379
|
+
writeFileSync3(absTarget, block.replace, "utf8");
|
|
3797
4380
|
return { path: block.path, status: "created" };
|
|
3798
4381
|
}
|
|
3799
|
-
const content =
|
|
4382
|
+
const content = readFileSync6(absTarget, "utf8");
|
|
3800
4383
|
if (searchEmpty) {
|
|
3801
4384
|
return {
|
|
3802
4385
|
path: block.path,
|
|
@@ -3813,7 +4396,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
3813
4396
|
};
|
|
3814
4397
|
}
|
|
3815
4398
|
const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
|
|
3816
|
-
|
|
4399
|
+
writeFileSync3(absTarget, replaced, "utf8");
|
|
3817
4400
|
return { path: block.path, status: "applied" };
|
|
3818
4401
|
} catch (err) {
|
|
3819
4402
|
return { path: block.path, status: "error", message: err.message };
|
|
@@ -3823,19 +4406,19 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
3823
4406
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
3824
4407
|
}
|
|
3825
4408
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
3826
|
-
const absRoot =
|
|
4409
|
+
const absRoot = resolve5(rootDir);
|
|
3827
4410
|
const seen = /* @__PURE__ */ new Set();
|
|
3828
4411
|
const snapshots = [];
|
|
3829
4412
|
for (const b of blocks) {
|
|
3830
4413
|
if (seen.has(b.path)) continue;
|
|
3831
4414
|
seen.add(b.path);
|
|
3832
|
-
const abs =
|
|
3833
|
-
if (!
|
|
4415
|
+
const abs = resolve5(absRoot, b.path);
|
|
4416
|
+
if (!existsSync5(abs)) {
|
|
3834
4417
|
snapshots.push({ path: b.path, prevContent: null });
|
|
3835
4418
|
continue;
|
|
3836
4419
|
}
|
|
3837
4420
|
try {
|
|
3838
|
-
snapshots.push({ path: b.path, prevContent:
|
|
4421
|
+
snapshots.push({ path: b.path, prevContent: readFileSync6(abs, "utf8") });
|
|
3839
4422
|
} catch {
|
|
3840
4423
|
snapshots.push({ path: b.path, prevContent: null });
|
|
3841
4424
|
}
|
|
@@ -3843,9 +4426,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
3843
4426
|
return snapshots;
|
|
3844
4427
|
}
|
|
3845
4428
|
function restoreSnapshots(snapshots, rootDir) {
|
|
3846
|
-
const absRoot =
|
|
4429
|
+
const absRoot = resolve5(rootDir);
|
|
3847
4430
|
return snapshots.map((snap) => {
|
|
3848
|
-
const abs =
|
|
4431
|
+
const abs = resolve5(absRoot, snap.path);
|
|
3849
4432
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
3850
4433
|
return {
|
|
3851
4434
|
path: snap.path,
|
|
@@ -3855,14 +4438,14 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
3855
4438
|
}
|
|
3856
4439
|
try {
|
|
3857
4440
|
if (snap.prevContent === null) {
|
|
3858
|
-
if (
|
|
4441
|
+
if (existsSync5(abs)) unlinkSync3(abs);
|
|
3859
4442
|
return {
|
|
3860
4443
|
path: snap.path,
|
|
3861
4444
|
status: "applied",
|
|
3862
4445
|
message: "removed (the edit had created it)"
|
|
3863
4446
|
};
|
|
3864
4447
|
}
|
|
3865
|
-
|
|
4448
|
+
writeFileSync3(abs, snap.prevContent, "utf8");
|
|
3866
4449
|
return {
|
|
3867
4450
|
path: snap.path,
|
|
3868
4451
|
status: "applied",
|
|
@@ -3878,10 +4461,31 @@ function sep() {
|
|
|
3878
4461
|
}
|
|
3879
4462
|
|
|
3880
4463
|
// src/code/prompt.ts
|
|
3881
|
-
import { existsSync as
|
|
3882
|
-
import { join as
|
|
4464
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
4465
|
+
import { join as join5 } from "path";
|
|
3883
4466
|
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.
|
|
3884
4467
|
|
|
4468
|
+
# When to propose a plan (submit_plan)
|
|
4469
|
+
|
|
4470
|
+
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:
|
|
4471
|
+
|
|
4472
|
+
- Multi-file refactors or renames.
|
|
4473
|
+
- Architecture changes (moving modules, splitting / merging files, new abstractions).
|
|
4474
|
+
- Anything where "undo" after the fact would be expensive \u2014 migrations, destructive cleanups, API shape changes.
|
|
4475
|
+
- When the user's request is ambiguous and multiple reasonable interpretations exist \u2014 propose your reading as a plan and let them confirm.
|
|
4476
|
+
|
|
4477
|
+
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.
|
|
4478
|
+
|
|
4479
|
+
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" 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.
|
|
4480
|
+
|
|
4481
|
+
# Plan mode (/plan)
|
|
4482
|
+
|
|
4483
|
+
The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
|
|
4484
|
+
- 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.
|
|
4485
|
+
- Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work \u2014 use them to investigate.
|
|
4486
|
+
- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
|
|
4487
|
+
|
|
4488
|
+
|
|
3885
4489
|
# When to edit vs. when to explore
|
|
3886
4490
|
|
|
3887
4491
|
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:
|
|
@@ -3916,9 +4520,13 @@ Rules:
|
|
|
3916
4520
|
- 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).
|
|
3917
4521
|
- Paths are relative to the working directory. Don't use absolute paths.
|
|
3918
4522
|
|
|
4523
|
+
# Trust what you already know
|
|
4524
|
+
|
|
4525
|
+
Before exploring the filesystem to answer a factual question, check whether the answer is already in context: the user's current message, earlier turns in this conversation (including prior tool results from \`remember\`), and the pinned memory blocks at the top of this prompt. When the user has stated a fact or you have remembered one, it outranks what the files say \u2014 don't re-derive from code what the user already told you. Explore when you genuinely don't know.
|
|
4526
|
+
|
|
3919
4527
|
# Exploration
|
|
3920
4528
|
|
|
3921
|
-
-
|
|
4529
|
+
- Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
|
|
3922
4530
|
- 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.
|
|
3923
4531
|
|
|
3924
4532
|
# Style
|
|
@@ -3928,12 +4536,12 @@ Rules:
|
|
|
3928
4536
|
- If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
|
|
3929
4537
|
`;
|
|
3930
4538
|
function codeSystemPrompt(rootDir) {
|
|
3931
|
-
const withMemory =
|
|
3932
|
-
const gitignorePath =
|
|
3933
|
-
if (!
|
|
4539
|
+
const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
|
|
4540
|
+
const gitignorePath = join5(rootDir, ".gitignore");
|
|
4541
|
+
if (!existsSync6(gitignorePath)) return withMemory;
|
|
3934
4542
|
let content;
|
|
3935
4543
|
try {
|
|
3936
|
-
content =
|
|
4544
|
+
content = readFileSync7(gitignorePath, "utf8");
|
|
3937
4545
|
} catch {
|
|
3938
4546
|
return withMemory;
|
|
3939
4547
|
}
|
|
@@ -3953,15 +4561,15 @@ ${truncated}
|
|
|
3953
4561
|
}
|
|
3954
4562
|
|
|
3955
4563
|
// src/config.ts
|
|
3956
|
-
import { chmodSync as chmodSync2, mkdirSync as
|
|
3957
|
-
import { homedir as
|
|
3958
|
-
import { dirname as dirname4, join as
|
|
4564
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
4565
|
+
import { homedir as homedir3 } from "os";
|
|
4566
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
3959
4567
|
function defaultConfigPath() {
|
|
3960
|
-
return
|
|
4568
|
+
return join6(homedir3(), ".reasonix", "config.json");
|
|
3961
4569
|
}
|
|
3962
4570
|
function readConfig(path = defaultConfigPath()) {
|
|
3963
4571
|
try {
|
|
3964
|
-
const raw =
|
|
4572
|
+
const raw = readFileSync8(path, "utf8");
|
|
3965
4573
|
const parsed = JSON.parse(raw);
|
|
3966
4574
|
if (parsed && typeof parsed === "object") return parsed;
|
|
3967
4575
|
} catch {
|
|
@@ -3969,8 +4577,8 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
3969
4577
|
return {};
|
|
3970
4578
|
}
|
|
3971
4579
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
3972
|
-
|
|
3973
|
-
|
|
4580
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
4581
|
+
writeFileSync4(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
3974
4582
|
try {
|
|
3975
4583
|
chmodSync2(path, 384);
|
|
3976
4584
|
} catch {
|
|
@@ -3996,7 +4604,7 @@ function redactKey(key) {
|
|
|
3996
4604
|
}
|
|
3997
4605
|
|
|
3998
4606
|
// src/index.ts
|
|
3999
|
-
var VERSION = "0.4.
|
|
4607
|
+
var VERSION = "0.4.20";
|
|
4000
4608
|
export {
|
|
4001
4609
|
AppendOnlyLog,
|
|
4002
4610
|
CODE_SYSTEM_PROMPT,
|
|
@@ -4005,16 +4613,21 @@ export {
|
|
|
4005
4613
|
DeepSeekClient,
|
|
4006
4614
|
ImmutablePrefix,
|
|
4007
4615
|
MCP_PROTOCOL_VERSION,
|
|
4616
|
+
MEMORY_INDEX_FILE,
|
|
4617
|
+
MEMORY_INDEX_MAX_CHARS,
|
|
4008
4618
|
McpClient,
|
|
4619
|
+
MemoryStore,
|
|
4009
4620
|
NeedsConfirmationError,
|
|
4010
4621
|
PROJECT_MEMORY_FILE,
|
|
4011
4622
|
PROJECT_MEMORY_MAX_CHARS,
|
|
4623
|
+
PlanProposedError,
|
|
4012
4624
|
SessionStats,
|
|
4013
4625
|
SseTransport,
|
|
4014
4626
|
StdioTransport,
|
|
4015
4627
|
StormBreaker,
|
|
4016
4628
|
ToolCallRepair,
|
|
4017
4629
|
ToolRegistry,
|
|
4630
|
+
USER_MEMORY_DIR,
|
|
4018
4631
|
Usage,
|
|
4019
4632
|
VERSION,
|
|
4020
4633
|
VolatileScratch,
|
|
@@ -4023,7 +4636,9 @@ export {
|
|
|
4023
4636
|
appendSessionMessage,
|
|
4024
4637
|
applyEditBlock,
|
|
4025
4638
|
applyEditBlocks,
|
|
4639
|
+
applyMemoryStack,
|
|
4026
4640
|
applyProjectMemory,
|
|
4641
|
+
applyUserMemory,
|
|
4027
4642
|
bridgeMcpTools,
|
|
4028
4643
|
claudeEquivalentCost,
|
|
4029
4644
|
codeSystemPrompt,
|
|
@@ -4061,21 +4676,28 @@ export {
|
|
|
4061
4676
|
parseMcpSpec,
|
|
4062
4677
|
parseMojeekResults,
|
|
4063
4678
|
parseTranscript,
|
|
4679
|
+
prepareSpawn,
|
|
4680
|
+
projectHash,
|
|
4681
|
+
quoteForCmdExe,
|
|
4064
4682
|
readConfig,
|
|
4065
4683
|
readProjectMemory,
|
|
4066
4684
|
readTranscript,
|
|
4067
4685
|
recordFromLoopEvent,
|
|
4068
4686
|
redactKey,
|
|
4069
4687
|
registerFilesystemTools,
|
|
4688
|
+
registerMemoryTools,
|
|
4689
|
+
registerPlanTool,
|
|
4070
4690
|
registerShellTools,
|
|
4071
4691
|
registerWebTools,
|
|
4072
4692
|
renderMarkdown as renderDiffMarkdown,
|
|
4073
4693
|
renderSummaryTable as renderDiffSummary,
|
|
4074
4694
|
repairTruncatedJson,
|
|
4075
4695
|
replayFromFile,
|
|
4696
|
+
resolveExecutable,
|
|
4076
4697
|
restoreSnapshots,
|
|
4077
4698
|
runBranches,
|
|
4078
4699
|
runCommand,
|
|
4700
|
+
sanitizeMemoryName,
|
|
4079
4701
|
sanitizeName as sanitizeSessionName,
|
|
4080
4702
|
saveApiKey,
|
|
4081
4703
|
scavengeToolCalls,
|