reasonix 0.4.19 → 0.4.21
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 +47 -2
- package/dist/cli/chunk-K6MR4SWS.js +601 -0
- package/dist/cli/chunk-K6MR4SWS.js.map +1 -0
- package/dist/cli/index.js +460 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-JNNNJLYF.js → prompt-VDN5U3YE.js} +2 -2
- package/dist/index.d.ts +149 -5
- package/dist/index.js +658 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-HNEWBEWZ.js +0 -152
- package/dist/cli/chunk-HNEWBEWZ.js.map +0 -1
- /package/dist/cli/{prompt-JNNNJLYF.js.map → prompt-VDN5U3YE.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((resolve7, reject) => {
|
|
51
|
+
const timer = setTimeout(resolve7, ms);
|
|
52
52
|
if (signal) {
|
|
53
53
|
const onAbort = () => {
|
|
54
54
|
clearTimeout(timer);
|
|
@@ -1537,8 +1537,8 @@ var CacheFirstLoop = class {
|
|
|
1537
1537
|
}
|
|
1538
1538
|
);
|
|
1539
1539
|
for (let k = 0; k < budget; k++) {
|
|
1540
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1541
|
-
waiter =
|
|
1540
|
+
const sample = queue.shift() ?? await new Promise((resolve7) => {
|
|
1541
|
+
waiter = resolve7;
|
|
1542
1542
|
});
|
|
1543
1543
|
yield {
|
|
1544
1544
|
turn: this._turn,
|
|
@@ -1950,6 +1950,448 @@ ${mem.content}
|
|
|
1950
1950
|
`;
|
|
1951
1951
|
}
|
|
1952
1952
|
|
|
1953
|
+
// src/user-memory.ts
|
|
1954
|
+
import { createHash as createHash2 } from "crypto";
|
|
1955
|
+
import {
|
|
1956
|
+
existsSync as existsSync4,
|
|
1957
|
+
mkdirSync as mkdirSync2,
|
|
1958
|
+
readFileSync as readFileSync4,
|
|
1959
|
+
readdirSync as readdirSync3,
|
|
1960
|
+
unlinkSync as unlinkSync2,
|
|
1961
|
+
writeFileSync as writeFileSync2
|
|
1962
|
+
} from "fs";
|
|
1963
|
+
import { homedir as homedir3 } from "os";
|
|
1964
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
1965
|
+
|
|
1966
|
+
// src/skills.ts
|
|
1967
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1968
|
+
import { homedir as homedir2 } from "os";
|
|
1969
|
+
import { join as join3, resolve } from "path";
|
|
1970
|
+
var SKILLS_DIRNAME = "skills";
|
|
1971
|
+
var SKILL_FILE = "SKILL.md";
|
|
1972
|
+
var SKILLS_INDEX_MAX_CHARS = 4e3;
|
|
1973
|
+
var VALID_SKILL_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
1974
|
+
function parseFrontmatter(raw) {
|
|
1975
|
+
const lines = raw.split(/\r?\n/);
|
|
1976
|
+
if (lines[0] !== "---") return { data: {}, body: raw };
|
|
1977
|
+
const end = lines.indexOf("---", 1);
|
|
1978
|
+
if (end < 0) return { data: {}, body: raw };
|
|
1979
|
+
const data = {};
|
|
1980
|
+
for (let i = 1; i < end; i++) {
|
|
1981
|
+
const line = lines[i];
|
|
1982
|
+
if (!line) continue;
|
|
1983
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
1984
|
+
if (m?.[1]) data[m[1]] = (m[2] ?? "").trim();
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
data,
|
|
1988
|
+
body: lines.slice(end + 1).join("\n").replace(/^\n+/, "")
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function isValidSkillName(name) {
|
|
1992
|
+
return VALID_SKILL_NAME.test(name);
|
|
1993
|
+
}
|
|
1994
|
+
var SkillStore = class {
|
|
1995
|
+
homeDir;
|
|
1996
|
+
projectRoot;
|
|
1997
|
+
constructor(opts = {}) {
|
|
1998
|
+
this.homeDir = opts.homeDir ?? homedir2();
|
|
1999
|
+
this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : void 0;
|
|
2000
|
+
}
|
|
2001
|
+
/** True iff this store was configured with a project root. */
|
|
2002
|
+
hasProjectScope() {
|
|
2003
|
+
return this.projectRoot !== void 0;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Root directories scanned, in priority order. Project scope first
|
|
2007
|
+
* so a per-repo skill overrides a global one with the same name —
|
|
2008
|
+
* users expect the local copy to win when both exist.
|
|
2009
|
+
*/
|
|
2010
|
+
roots() {
|
|
2011
|
+
const out = [];
|
|
2012
|
+
if (this.projectRoot) {
|
|
2013
|
+
out.push({
|
|
2014
|
+
dir: join3(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
|
|
2015
|
+
scope: "project"
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
out.push({ dir: join3(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
|
|
2019
|
+
return out;
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* List every skill visible to this store. On name collisions the
|
|
2023
|
+
* higher-priority root (project over global) wins. Sorted by name
|
|
2024
|
+
* for stable prefix hashing.
|
|
2025
|
+
*/
|
|
2026
|
+
list() {
|
|
2027
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2028
|
+
for (const { dir, scope } of this.roots()) {
|
|
2029
|
+
if (!existsSync3(dir)) continue;
|
|
2030
|
+
let entries;
|
|
2031
|
+
try {
|
|
2032
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
2033
|
+
} catch {
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
for (const entry of entries) {
|
|
2037
|
+
const skill = this.readEntry(dir, scope, entry);
|
|
2038
|
+
if (!skill) continue;
|
|
2039
|
+
if (!byName.has(skill.name)) byName.set(skill.name, skill);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2043
|
+
}
|
|
2044
|
+
/** Resolve one skill by name. Returns `null` if not found or malformed. */
|
|
2045
|
+
read(name) {
|
|
2046
|
+
if (!isValidSkillName(name)) return null;
|
|
2047
|
+
for (const { dir, scope } of this.roots()) {
|
|
2048
|
+
if (!existsSync3(dir)) continue;
|
|
2049
|
+
const dirCandidate = join3(dir, name, SKILL_FILE);
|
|
2050
|
+
if (existsSync3(dirCandidate) && statSync2(dirCandidate).isFile()) {
|
|
2051
|
+
return this.parse(dirCandidate, name, scope);
|
|
2052
|
+
}
|
|
2053
|
+
const flatCandidate = join3(dir, `${name}.md`);
|
|
2054
|
+
if (existsSync3(flatCandidate) && statSync2(flatCandidate).isFile()) {
|
|
2055
|
+
return this.parse(flatCandidate, name, scope);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
readEntry(dir, scope, entry) {
|
|
2061
|
+
if (entry.isDirectory()) {
|
|
2062
|
+
if (!isValidSkillName(entry.name)) return null;
|
|
2063
|
+
const file = join3(dir, entry.name, SKILL_FILE);
|
|
2064
|
+
if (!existsSync3(file)) return null;
|
|
2065
|
+
return this.parse(file, entry.name, scope);
|
|
2066
|
+
}
|
|
2067
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2068
|
+
const stem = entry.name.slice(0, -3);
|
|
2069
|
+
if (!isValidSkillName(stem)) return null;
|
|
2070
|
+
return this.parse(join3(dir, entry.name), stem, scope);
|
|
2071
|
+
}
|
|
2072
|
+
return null;
|
|
2073
|
+
}
|
|
2074
|
+
parse(path, stem, scope) {
|
|
2075
|
+
let raw;
|
|
2076
|
+
try {
|
|
2077
|
+
raw = readFileSync3(path, "utf8");
|
|
2078
|
+
} catch {
|
|
2079
|
+
return null;
|
|
2080
|
+
}
|
|
2081
|
+
const { data, body } = parseFrontmatter(raw);
|
|
2082
|
+
const name = data.name && isValidSkillName(data.name) ? data.name : stem;
|
|
2083
|
+
return {
|
|
2084
|
+
name,
|
|
2085
|
+
description: (data.description ?? "").trim(),
|
|
2086
|
+
body: body.trim(),
|
|
2087
|
+
scope,
|
|
2088
|
+
path,
|
|
2089
|
+
allowedTools: data["allowed-tools"]
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
function skillIndexLine(s) {
|
|
2094
|
+
const safeDesc = s.description.replace(/\n/g, " ").trim();
|
|
2095
|
+
const max = 130 - s.name.length;
|
|
2096
|
+
const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
|
|
2097
|
+
return clipped ? `- ${s.name} \u2014 ${clipped}` : `- ${s.name}`;
|
|
2098
|
+
}
|
|
2099
|
+
function applySkillsIndex(basePrompt, opts = {}) {
|
|
2100
|
+
const store = new SkillStore(opts);
|
|
2101
|
+
const skills = store.list().filter((s) => s.description);
|
|
2102
|
+
if (skills.length === 0) return basePrompt;
|
|
2103
|
+
const lines = skills.map(skillIndexLine);
|
|
2104
|
+
const joined = lines.join("\n");
|
|
2105
|
+
const truncated = joined.length > SKILLS_INDEX_MAX_CHARS ? `${joined.slice(0, SKILLS_INDEX_MAX_CHARS)}
|
|
2106
|
+
\u2026 (truncated ${joined.length - SKILLS_INDEX_MAX_CHARS} chars)` : joined;
|
|
2107
|
+
return [
|
|
2108
|
+
basePrompt,
|
|
2109
|
+
"",
|
|
2110
|
+
"# Skills \u2014 user-defined prompt packs",
|
|
2111
|
+
"",
|
|
2112
|
+
'One-liner index. Each skill is a self-contained instruction block (plus optional tool hints) the user or an earlier session saved. To load the full body, call `run_skill({ name: "<skill-name>" })` \u2014 the body is NOT in this prompt, only the name and description are. The user can also invoke a skill directly as `/skill <name>`.',
|
|
2113
|
+
"",
|
|
2114
|
+
"```",
|
|
2115
|
+
truncated,
|
|
2116
|
+
"```"
|
|
2117
|
+
].join("\n");
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/user-memory.ts
|
|
2121
|
+
var USER_MEMORY_DIR = "memory";
|
|
2122
|
+
var MEMORY_INDEX_FILE = "MEMORY.md";
|
|
2123
|
+
var MEMORY_INDEX_MAX_CHARS = 4e3;
|
|
2124
|
+
var VALID_NAME = /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{1,38}[a-zA-Z0-9]$/;
|
|
2125
|
+
function sanitizeMemoryName(raw) {
|
|
2126
|
+
const trimmed = String(raw ?? "").trim();
|
|
2127
|
+
if (!VALID_NAME.test(trimmed)) {
|
|
2128
|
+
throw new Error(
|
|
2129
|
+
`invalid memory name: ${JSON.stringify(raw)} \u2014 must be 3-40 chars, alnum/_/-, no path separators`
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
return trimmed;
|
|
2133
|
+
}
|
|
2134
|
+
function projectHash(rootDir) {
|
|
2135
|
+
const abs = resolve2(rootDir);
|
|
2136
|
+
return createHash2("sha1").update(abs).digest("hex").slice(0, 16);
|
|
2137
|
+
}
|
|
2138
|
+
function scopeDir(opts) {
|
|
2139
|
+
if (opts.scope === "global") {
|
|
2140
|
+
return join4(opts.homeDir, USER_MEMORY_DIR, "global");
|
|
2141
|
+
}
|
|
2142
|
+
if (!opts.projectRoot) {
|
|
2143
|
+
throw new Error("scope=project requires a projectRoot on MemoryStore");
|
|
2144
|
+
}
|
|
2145
|
+
return join4(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
|
|
2146
|
+
}
|
|
2147
|
+
function ensureDir(p) {
|
|
2148
|
+
if (!existsSync4(p)) mkdirSync2(p, { recursive: true });
|
|
2149
|
+
}
|
|
2150
|
+
function parseFrontmatter2(raw) {
|
|
2151
|
+
const lines = raw.split(/\r?\n/);
|
|
2152
|
+
if (lines[0] !== "---") return { data: {}, body: raw };
|
|
2153
|
+
const end = lines.indexOf("---", 1);
|
|
2154
|
+
if (end < 0) return { data: {}, body: raw };
|
|
2155
|
+
const data = {};
|
|
2156
|
+
for (let i = 1; i < end; i++) {
|
|
2157
|
+
const line = lines[i];
|
|
2158
|
+
if (!line) continue;
|
|
2159
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
2160
|
+
if (m?.[1]) data[m[1]] = (m[2] ?? "").trim();
|
|
2161
|
+
}
|
|
2162
|
+
return {
|
|
2163
|
+
data,
|
|
2164
|
+
body: lines.slice(end + 1).join("\n").replace(/^\n+/, "")
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
function formatFrontmatter(e) {
|
|
2168
|
+
return [
|
|
2169
|
+
"---",
|
|
2170
|
+
`name: ${e.name}`,
|
|
2171
|
+
`description: ${e.description.replace(/\n/g, " ")}`,
|
|
2172
|
+
`type: ${e.type}`,
|
|
2173
|
+
`scope: ${e.scope}`,
|
|
2174
|
+
`created: ${e.createdAt}`,
|
|
2175
|
+
"---",
|
|
2176
|
+
""
|
|
2177
|
+
].join("\n");
|
|
2178
|
+
}
|
|
2179
|
+
function todayIso() {
|
|
2180
|
+
const d = /* @__PURE__ */ new Date();
|
|
2181
|
+
return d.toISOString().slice(0, 10);
|
|
2182
|
+
}
|
|
2183
|
+
function indexLine(e) {
|
|
2184
|
+
const safeDesc = e.description.replace(/\n/g, " ").trim();
|
|
2185
|
+
const max = 130 - e.name.length;
|
|
2186
|
+
const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
|
|
2187
|
+
return `- [${e.name}](${e.name}.md) \u2014 ${clipped}`;
|
|
2188
|
+
}
|
|
2189
|
+
var MemoryStore = class {
|
|
2190
|
+
homeDir;
|
|
2191
|
+
projectRoot;
|
|
2192
|
+
constructor(opts = {}) {
|
|
2193
|
+
this.homeDir = opts.homeDir ?? join4(homedir3(), ".reasonix");
|
|
2194
|
+
this.projectRoot = opts.projectRoot ? resolve2(opts.projectRoot) : void 0;
|
|
2195
|
+
}
|
|
2196
|
+
/** Directory this store writes `scope` files into, creating it if needed. */
|
|
2197
|
+
dir(scope) {
|
|
2198
|
+
const d = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2199
|
+
ensureDir(d);
|
|
2200
|
+
return d;
|
|
2201
|
+
}
|
|
2202
|
+
/** Absolute path to a memory file (no existence check). */
|
|
2203
|
+
pathFor(scope, name) {
|
|
2204
|
+
return join4(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
|
|
2205
|
+
}
|
|
2206
|
+
/** True iff this store is configured with a project scope available. */
|
|
2207
|
+
hasProjectScope() {
|
|
2208
|
+
return this.projectRoot !== void 0;
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Read the `MEMORY.md` index for a scope. Returns post-cap content
|
|
2212
|
+
* (with a truncation marker if clipped), or `null` when absent / empty.
|
|
2213
|
+
*/
|
|
2214
|
+
loadIndex(scope) {
|
|
2215
|
+
if (scope === "project" && !this.projectRoot) return null;
|
|
2216
|
+
const file = join4(
|
|
2217
|
+
scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
|
|
2218
|
+
MEMORY_INDEX_FILE
|
|
2219
|
+
);
|
|
2220
|
+
if (!existsSync4(file)) return null;
|
|
2221
|
+
let raw;
|
|
2222
|
+
try {
|
|
2223
|
+
raw = readFileSync4(file, "utf8");
|
|
2224
|
+
} catch {
|
|
2225
|
+
return null;
|
|
2226
|
+
}
|
|
2227
|
+
const trimmed = raw.trim();
|
|
2228
|
+
if (!trimmed) return null;
|
|
2229
|
+
const originalChars = trimmed.length;
|
|
2230
|
+
const truncated = originalChars > MEMORY_INDEX_MAX_CHARS;
|
|
2231
|
+
const content = truncated ? `${trimmed.slice(0, MEMORY_INDEX_MAX_CHARS)}
|
|
2232
|
+
\u2026 (truncated ${originalChars - MEMORY_INDEX_MAX_CHARS} chars)` : trimmed;
|
|
2233
|
+
return { content, originalChars, truncated };
|
|
2234
|
+
}
|
|
2235
|
+
/** Read one memory file's body (frontmatter stripped). Throws if missing. */
|
|
2236
|
+
read(scope, name) {
|
|
2237
|
+
const file = this.pathFor(scope, name);
|
|
2238
|
+
if (!existsSync4(file)) {
|
|
2239
|
+
throw new Error(`memory not found: scope=${scope} name=${name}`);
|
|
2240
|
+
}
|
|
2241
|
+
const raw = readFileSync4(file, "utf8");
|
|
2242
|
+
const { data, body } = parseFrontmatter2(raw);
|
|
2243
|
+
return {
|
|
2244
|
+
name: data.name ?? name,
|
|
2245
|
+
type: data.type ?? "project",
|
|
2246
|
+
scope: data.scope ?? scope,
|
|
2247
|
+
description: data.description ?? "",
|
|
2248
|
+
body: body.trim(),
|
|
2249
|
+
createdAt: data.created ?? ""
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* List every memory in this store. Scans both scopes (skips project
|
|
2254
|
+
* scope if unconfigured). Silently skips malformed files; the index
|
|
2255
|
+
* must stay queryable even if one file is hand-edited into nonsense.
|
|
2256
|
+
*/
|
|
2257
|
+
list() {
|
|
2258
|
+
const out = [];
|
|
2259
|
+
const scopes = this.projectRoot ? ["global", "project"] : ["global"];
|
|
2260
|
+
for (const scope of scopes) {
|
|
2261
|
+
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2262
|
+
if (!existsSync4(dir)) continue;
|
|
2263
|
+
let entries;
|
|
2264
|
+
try {
|
|
2265
|
+
entries = readdirSync3(dir);
|
|
2266
|
+
} catch {
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
for (const entry of entries) {
|
|
2270
|
+
if (entry === MEMORY_INDEX_FILE) continue;
|
|
2271
|
+
if (!entry.endsWith(".md")) continue;
|
|
2272
|
+
const name = entry.slice(0, -3);
|
|
2273
|
+
try {
|
|
2274
|
+
out.push(this.read(scope, name));
|
|
2275
|
+
} catch {
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Write a new memory (or overwrite existing). Creates the scope dir,
|
|
2283
|
+
* writes the `.md` file, and regenerates `MEMORY.md`. Returns the
|
|
2284
|
+
* absolute path written to.
|
|
2285
|
+
*/
|
|
2286
|
+
write(input) {
|
|
2287
|
+
if (input.scope === "project" && !this.projectRoot) {
|
|
2288
|
+
throw new Error("cannot write project-scoped memory: no projectRoot configured");
|
|
2289
|
+
}
|
|
2290
|
+
const name = sanitizeMemoryName(input.name);
|
|
2291
|
+
const desc = String(input.description ?? "").trim();
|
|
2292
|
+
if (!desc) throw new Error("memory description cannot be empty");
|
|
2293
|
+
const body = String(input.body ?? "").trim();
|
|
2294
|
+
if (!body) throw new Error("memory body cannot be empty");
|
|
2295
|
+
const entry = {
|
|
2296
|
+
...input,
|
|
2297
|
+
name,
|
|
2298
|
+
description: desc,
|
|
2299
|
+
body,
|
|
2300
|
+
createdAt: todayIso()
|
|
2301
|
+
};
|
|
2302
|
+
const dir = this.dir(input.scope);
|
|
2303
|
+
const file = join4(dir, `${name}.md`);
|
|
2304
|
+
const content = `${formatFrontmatter(entry)}${body}
|
|
2305
|
+
`;
|
|
2306
|
+
writeFileSync2(file, content, "utf8");
|
|
2307
|
+
this.regenerateIndex(input.scope);
|
|
2308
|
+
return file;
|
|
2309
|
+
}
|
|
2310
|
+
/** Delete one memory + its index line. No-op if the file is already gone. */
|
|
2311
|
+
delete(scope, rawName) {
|
|
2312
|
+
if (scope === "project" && !this.projectRoot) {
|
|
2313
|
+
throw new Error("cannot delete project-scoped memory: no projectRoot configured");
|
|
2314
|
+
}
|
|
2315
|
+
const file = this.pathFor(scope, rawName);
|
|
2316
|
+
if (!existsSync4(file)) return false;
|
|
2317
|
+
unlinkSync2(file);
|
|
2318
|
+
this.regenerateIndex(scope);
|
|
2319
|
+
return true;
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Rebuild `MEMORY.md` from the `.md` files currently in the scope dir.
|
|
2323
|
+
* Called after every write/delete. Sorted by name for stable prefix
|
|
2324
|
+
* hashing — two stores with the same set of files produce byte-identical
|
|
2325
|
+
* MEMORY.md content, keeping the cache prefix reproducible.
|
|
2326
|
+
*/
|
|
2327
|
+
regenerateIndex(scope) {
|
|
2328
|
+
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
2329
|
+
if (!existsSync4(dir)) return;
|
|
2330
|
+
let files;
|
|
2331
|
+
try {
|
|
2332
|
+
files = readdirSync3(dir);
|
|
2333
|
+
} catch {
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
|
|
2337
|
+
const indexPath = join4(dir, MEMORY_INDEX_FILE);
|
|
2338
|
+
if (mdFiles.length === 0) {
|
|
2339
|
+
if (existsSync4(indexPath)) unlinkSync2(indexPath);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
const lines = [];
|
|
2343
|
+
for (const f of mdFiles) {
|
|
2344
|
+
const name = f.slice(0, -3);
|
|
2345
|
+
try {
|
|
2346
|
+
const entry = this.read(scope, name);
|
|
2347
|
+
lines.push(indexLine({ name: entry.name || name, description: entry.description }));
|
|
2348
|
+
} catch {
|
|
2349
|
+
lines.push(`- [${name}](${name}.md) \u2014 (malformed, check frontmatter)`);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
writeFileSync2(indexPath, `${lines.join("\n")}
|
|
2353
|
+
`, "utf8");
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
function applyUserMemory(basePrompt, opts = {}) {
|
|
2357
|
+
if (!memoryEnabled()) return basePrompt;
|
|
2358
|
+
const store = new MemoryStore(opts);
|
|
2359
|
+
const global = store.loadIndex("global");
|
|
2360
|
+
const project = store.hasProjectScope() ? store.loadIndex("project") : null;
|
|
2361
|
+
if (!global && !project) return basePrompt;
|
|
2362
|
+
const parts = [basePrompt];
|
|
2363
|
+
if (global) {
|
|
2364
|
+
parts.push(
|
|
2365
|
+
"",
|
|
2366
|
+
"# User memory \u2014 global (~/.reasonix/memory/global/MEMORY.md)",
|
|
2367
|
+
"",
|
|
2368
|
+
"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.",
|
|
2369
|
+
"",
|
|
2370
|
+
"```",
|
|
2371
|
+
global.content,
|
|
2372
|
+
"```"
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
if (project) {
|
|
2376
|
+
parts.push(
|
|
2377
|
+
"",
|
|
2378
|
+
"# User memory \u2014 this project",
|
|
2379
|
+
"",
|
|
2380
|
+
"Per-project facts the user established in prior sessions (not committed to the repo). TREAT AS AUTHORITATIVE. Same recall pattern as global memory.",
|
|
2381
|
+
"",
|
|
2382
|
+
"```",
|
|
2383
|
+
project.content,
|
|
2384
|
+
"```"
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
return parts.join("\n");
|
|
2388
|
+
}
|
|
2389
|
+
function applyMemoryStack(basePrompt, rootDir) {
|
|
2390
|
+
const withProject = applyProjectMemory(basePrompt, rootDir);
|
|
2391
|
+
const withMemory = applyUserMemory(withProject, { projectRoot: rootDir });
|
|
2392
|
+
return applySkillsIndex(withMemory, { projectRoot: rootDir });
|
|
2393
|
+
}
|
|
2394
|
+
|
|
1953
2395
|
// src/tools/filesystem.ts
|
|
1954
2396
|
import { promises as fs } from "fs";
|
|
1955
2397
|
import * as pathMod from "path";
|
|
@@ -2173,7 +2615,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2173
2615
|
});
|
|
2174
2616
|
registry.register({
|
|
2175
2617
|
name: "edit_file",
|
|
2176
|
-
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.
|
|
2618
|
+
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.",
|
|
2177
2619
|
parameters: {
|
|
2178
2620
|
type: "object",
|
|
2179
2621
|
properties: {
|
|
@@ -2290,6 +2732,127 @@ function lineDiff(a, b) {
|
|
|
2290
2732
|
return out;
|
|
2291
2733
|
}
|
|
2292
2734
|
|
|
2735
|
+
// src/tools/memory.ts
|
|
2736
|
+
function registerMemoryTools(registry, opts = {}) {
|
|
2737
|
+
const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
|
|
2738
|
+
const hasProject = store.hasProjectScope();
|
|
2739
|
+
registry.register({
|
|
2740
|
+
name: "remember",
|
|
2741
|
+
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.",
|
|
2742
|
+
parameters: {
|
|
2743
|
+
type: "object",
|
|
2744
|
+
properties: {
|
|
2745
|
+
type: {
|
|
2746
|
+
type: "string",
|
|
2747
|
+
enum: ["user", "feedback", "project", "reference"],
|
|
2748
|
+
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."
|
|
2749
|
+
},
|
|
2750
|
+
scope: {
|
|
2751
|
+
type: "string",
|
|
2752
|
+
enum: ["global", "project"],
|
|
2753
|
+
description: "'global' = applies across every project (preferences, tooling); 'project' = scoped to the current sandbox (decisions, local facts). Only available in `reasonix code`."
|
|
2754
|
+
},
|
|
2755
|
+
name: {
|
|
2756
|
+
type: "string",
|
|
2757
|
+
description: "filename-safe identifier, 3-40 chars, alnum + _ - . (no path separators, no leading dot)."
|
|
2758
|
+
},
|
|
2759
|
+
description: {
|
|
2760
|
+
type: "string",
|
|
2761
|
+
description: "One-line summary shown in MEMORY.md (under ~150 chars)."
|
|
2762
|
+
},
|
|
2763
|
+
content: {
|
|
2764
|
+
type: "string",
|
|
2765
|
+
description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
|
|
2766
|
+
}
|
|
2767
|
+
},
|
|
2768
|
+
required: ["type", "scope", "name", "description", "content"]
|
|
2769
|
+
},
|
|
2770
|
+
fn: async (args) => {
|
|
2771
|
+
if (args.scope === "project" && !hasProject) {
|
|
2772
|
+
return JSON.stringify({
|
|
2773
|
+
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."
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
try {
|
|
2777
|
+
const path = store.write({
|
|
2778
|
+
name: args.name,
|
|
2779
|
+
type: args.type,
|
|
2780
|
+
scope: args.scope,
|
|
2781
|
+
description: args.description,
|
|
2782
|
+
body: args.content
|
|
2783
|
+
});
|
|
2784
|
+
const key = sanitizeMemoryName(args.name);
|
|
2785
|
+
return [
|
|
2786
|
+
`\u2713 REMEMBERED (${args.scope}/${key}): ${args.description}`,
|
|
2787
|
+
"",
|
|
2788
|
+
"TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
|
|
2789
|
+
"The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
|
|
2790
|
+
`(Saved to ${path}; pins into the system prompt on next /new or launch.)`
|
|
2791
|
+
].join("\n");
|
|
2792
|
+
} catch (err) {
|
|
2793
|
+
return JSON.stringify({ error: `remember failed: ${err.message}` });
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
registry.register({
|
|
2798
|
+
name: "forget",
|
|
2799
|
+
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.",
|
|
2800
|
+
parameters: {
|
|
2801
|
+
type: "object",
|
|
2802
|
+
properties: {
|
|
2803
|
+
name: { type: "string", description: "Memory name (the identifier used in `remember`)." },
|
|
2804
|
+
scope: { type: "string", enum: ["global", "project"] }
|
|
2805
|
+
},
|
|
2806
|
+
required: ["name", "scope"]
|
|
2807
|
+
},
|
|
2808
|
+
fn: async (args) => {
|
|
2809
|
+
if (args.scope === "project" && !hasProject) {
|
|
2810
|
+
return JSON.stringify({
|
|
2811
|
+
error: "scope='project' is unavailable in this session (no sandbox root)."
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
try {
|
|
2815
|
+
const existed = store.delete(args.scope, args.name);
|
|
2816
|
+
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).`;
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
return JSON.stringify({ error: `forget failed: ${err.message}` });
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
registry.register({
|
|
2823
|
+
name: "recall_memory",
|
|
2824
|
+
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.",
|
|
2825
|
+
readOnly: true,
|
|
2826
|
+
parameters: {
|
|
2827
|
+
type: "object",
|
|
2828
|
+
properties: {
|
|
2829
|
+
name: { type: "string" },
|
|
2830
|
+
scope: { type: "string", enum: ["global", "project"] }
|
|
2831
|
+
},
|
|
2832
|
+
required: ["name", "scope"]
|
|
2833
|
+
},
|
|
2834
|
+
fn: async (args) => {
|
|
2835
|
+
if (args.scope === "project" && !hasProject) {
|
|
2836
|
+
return JSON.stringify({
|
|
2837
|
+
error: "scope='project' is unavailable in this session (no sandbox root)."
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
try {
|
|
2841
|
+
const entry = store.read(args.scope, args.name);
|
|
2842
|
+
return [
|
|
2843
|
+
`# ${entry.name} (${entry.scope}/${entry.type}, created ${entry.createdAt || "?"})`,
|
|
2844
|
+
entry.description ? `> ${entry.description}` : "",
|
|
2845
|
+
"",
|
|
2846
|
+
entry.body
|
|
2847
|
+
].filter(Boolean).join("\n");
|
|
2848
|
+
} catch (err) {
|
|
2849
|
+
return JSON.stringify({ error: `recall failed: ${err.message}` });
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
return registry;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2293
2856
|
// src/tools/plan.ts
|
|
2294
2857
|
var PlanProposedError = class extends Error {
|
|
2295
2858
|
plan;
|
|
@@ -2337,7 +2900,7 @@ function registerPlanTool(registry, opts = {}) {
|
|
|
2337
2900
|
|
|
2338
2901
|
// src/tools/shell.ts
|
|
2339
2902
|
import { spawn } from "child_process";
|
|
2340
|
-
import { existsSync as
|
|
2903
|
+
import { existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
2341
2904
|
import * as pathMod2 from "path";
|
|
2342
2905
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2343
2906
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
@@ -2457,7 +3020,7 @@ async function runCommand(cmd, opts) {
|
|
|
2457
3020
|
};
|
|
2458
3021
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
2459
3022
|
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
2460
|
-
return await new Promise((
|
|
3023
|
+
return await new Promise((resolve7, reject) => {
|
|
2461
3024
|
let child;
|
|
2462
3025
|
try {
|
|
2463
3026
|
child = spawn(bin, args, effectiveSpawnOpts);
|
|
@@ -2490,7 +3053,7 @@ async function runCommand(cmd, opts) {
|
|
|
2490
3053
|
const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
|
|
2491
3054
|
|
|
2492
3055
|
[\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
|
|
2493
|
-
|
|
3056
|
+
resolve7({ exitCode: code, output, timedOut });
|
|
2494
3057
|
});
|
|
2495
3058
|
});
|
|
2496
3059
|
}
|
|
@@ -2507,7 +3070,7 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
2507
3070
|
const isFile = opts.isFile ?? defaultIsFile;
|
|
2508
3071
|
for (const dir of pathDirs) {
|
|
2509
3072
|
for (const ext of pathExt) {
|
|
2510
|
-
const full = pathMod2.join(dir, cmd + ext);
|
|
3073
|
+
const full = pathMod2.win32.join(dir, cmd + ext);
|
|
2511
3074
|
if (isFile(full)) return full;
|
|
2512
3075
|
}
|
|
2513
3076
|
}
|
|
@@ -2515,7 +3078,7 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
2515
3078
|
}
|
|
2516
3079
|
function defaultIsFile(full) {
|
|
2517
3080
|
try {
|
|
2518
|
-
return
|
|
3081
|
+
return existsSync5(full) && statSync3(full).isFile();
|
|
2519
3082
|
} catch {
|
|
2520
3083
|
return false;
|
|
2521
3084
|
}
|
|
@@ -2540,8 +3103,23 @@ function prepareSpawn(argv, opts = {}) {
|
|
|
2540
3103
|
spawnOverrides: { windowsVerbatimArguments: true }
|
|
2541
3104
|
};
|
|
2542
3105
|
}
|
|
3106
|
+
if (isBareWindowsName(resolved) && resolved === head) {
|
|
3107
|
+
const cmdline = [head, ...tail].map(quoteForCmdExe).join(" ");
|
|
3108
|
+
return {
|
|
3109
|
+
bin: "cmd.exe",
|
|
3110
|
+
args: ["/d", "/s", "/c", cmdline],
|
|
3111
|
+
spawnOverrides: { windowsVerbatimArguments: true }
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
2543
3114
|
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2544
3115
|
}
|
|
3116
|
+
function isBareWindowsName(s) {
|
|
3117
|
+
if (!s) return false;
|
|
3118
|
+
if (s.includes("/") || s.includes("\\")) return false;
|
|
3119
|
+
if (pathMod2.isAbsolute(s)) return false;
|
|
3120
|
+
if (pathMod2.extname(s)) return false;
|
|
3121
|
+
return true;
|
|
3122
|
+
}
|
|
2545
3123
|
function quoteForCmdExe(arg) {
|
|
2546
3124
|
if (arg === "") return '""';
|
|
2547
3125
|
if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
|
|
@@ -2561,11 +3139,14 @@ function registerShellTools(registry, opts) {
|
|
|
2561
3139
|
const rootDir = pathMod2.resolve(opts.rootDir);
|
|
2562
3140
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
2563
3141
|
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
2564
|
-
const
|
|
3142
|
+
const getExtraAllowed = typeof opts.extraAllowed === "function" ? opts.extraAllowed : (() => {
|
|
3143
|
+
const snapshot = opts.extraAllowed ?? [];
|
|
3144
|
+
return () => snapshot;
|
|
3145
|
+
})();
|
|
2565
3146
|
const allowAll = opts.allowAll ?? false;
|
|
2566
3147
|
registry.register({
|
|
2567
3148
|
name: "run_command",
|
|
2568
|
-
description: "Run a shell command in the project root and return its combined stdout+stderr.
|
|
3149
|
+
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.",
|
|
2569
3150
|
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
2570
3151
|
// cargo check, ls, grep …) so the model can actually investigate
|
|
2571
3152
|
// during planning. Anything that would otherwise trigger a
|
|
@@ -2574,14 +3155,14 @@ function registerShellTools(registry, opts) {
|
|
|
2574
3155
|
if (allowAll) return true;
|
|
2575
3156
|
const cmd = typeof args?.command === "string" ? args.command.trim() : "";
|
|
2576
3157
|
if (!cmd) return false;
|
|
2577
|
-
return isAllowed(cmd,
|
|
3158
|
+
return isAllowed(cmd, getExtraAllowed());
|
|
2578
3159
|
},
|
|
2579
3160
|
parameters: {
|
|
2580
3161
|
type: "object",
|
|
2581
3162
|
properties: {
|
|
2582
3163
|
command: {
|
|
2583
3164
|
type: "string",
|
|
2584
|
-
description: "Full command line
|
|
3165
|
+
description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
|
|
2585
3166
|
},
|
|
2586
3167
|
timeoutSec: {
|
|
2587
3168
|
type: "integer",
|
|
@@ -2593,7 +3174,7 @@ function registerShellTools(registry, opts) {
|
|
|
2593
3174
|
fn: async (args, ctx) => {
|
|
2594
3175
|
const cmd = args.command.trim();
|
|
2595
3176
|
if (!cmd) throw new Error("run_command: empty command");
|
|
2596
|
-
if (!allowAll && !isAllowed(cmd,
|
|
3177
|
+
if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
|
|
2597
3178
|
throw new NeedsConfirmationError(cmd);
|
|
2598
3179
|
}
|
|
2599
3180
|
const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
|
|
@@ -2800,12 +3381,12 @@ ${i + 1}. ${r.title}`);
|
|
|
2800
3381
|
}
|
|
2801
3382
|
|
|
2802
3383
|
// src/env.ts
|
|
2803
|
-
import { readFileSync as
|
|
2804
|
-
import { resolve as
|
|
3384
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3385
|
+
import { resolve as resolve5 } from "path";
|
|
2805
3386
|
function loadDotenv(path = ".env") {
|
|
2806
3387
|
let raw;
|
|
2807
3388
|
try {
|
|
2808
|
-
raw =
|
|
3389
|
+
raw = readFileSync5(resolve5(process.cwd(), path), "utf8");
|
|
2809
3390
|
} catch {
|
|
2810
3391
|
return;
|
|
2811
3392
|
}
|
|
@@ -2824,7 +3405,7 @@ function loadDotenv(path = ".env") {
|
|
|
2824
3405
|
}
|
|
2825
3406
|
|
|
2826
3407
|
// src/transcript.ts
|
|
2827
|
-
import { createWriteStream, readFileSync as
|
|
3408
|
+
import { createWriteStream, readFileSync as readFileSync6 } from "fs";
|
|
2828
3409
|
function recordFromLoopEvent(ev, extra) {
|
|
2829
3410
|
const rec = {
|
|
2830
3411
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2875,7 +3456,7 @@ function openTranscriptFile(path, meta) {
|
|
|
2875
3456
|
return stream;
|
|
2876
3457
|
}
|
|
2877
3458
|
function readTranscript(path) {
|
|
2878
|
-
const raw =
|
|
3459
|
+
const raw = readFileSync6(path, "utf8");
|
|
2879
3460
|
return parseTranscript(raw);
|
|
2880
3461
|
}
|
|
2881
3462
|
function isPlanStateEmptyShape(s) {
|
|
@@ -3487,7 +4068,7 @@ var McpClient = class {
|
|
|
3487
4068
|
const id = this.nextId++;
|
|
3488
4069
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
3489
4070
|
let abortHandler = null;
|
|
3490
|
-
const promise = new Promise((
|
|
4071
|
+
const promise = new Promise((resolve7, reject) => {
|
|
3491
4072
|
const timeout = setTimeout(() => {
|
|
3492
4073
|
this.pending.delete(id);
|
|
3493
4074
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -3496,7 +4077,7 @@ var McpClient = class {
|
|
|
3496
4077
|
);
|
|
3497
4078
|
}, this.requestTimeoutMs);
|
|
3498
4079
|
this.pending.set(id, {
|
|
3499
|
-
resolve:
|
|
4080
|
+
resolve: resolve7,
|
|
3500
4081
|
reject,
|
|
3501
4082
|
timeout
|
|
3502
4083
|
});
|
|
@@ -3619,12 +4200,12 @@ var StdioTransport = class {
|
|
|
3619
4200
|
}
|
|
3620
4201
|
async send(message) {
|
|
3621
4202
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
3622
|
-
return new Promise((
|
|
4203
|
+
return new Promise((resolve7, reject) => {
|
|
3623
4204
|
const line = `${JSON.stringify(message)}
|
|
3624
4205
|
`;
|
|
3625
4206
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
3626
4207
|
if (err) reject(err);
|
|
3627
|
-
else
|
|
4208
|
+
else resolve7();
|
|
3628
4209
|
});
|
|
3629
4210
|
});
|
|
3630
4211
|
}
|
|
@@ -3635,8 +4216,8 @@ var StdioTransport = class {
|
|
|
3635
4216
|
continue;
|
|
3636
4217
|
}
|
|
3637
4218
|
if (this.closed) return;
|
|
3638
|
-
const next = await new Promise((
|
|
3639
|
-
this.waiters.push(
|
|
4219
|
+
const next = await new Promise((resolve7) => {
|
|
4220
|
+
this.waiters.push(resolve7);
|
|
3640
4221
|
});
|
|
3641
4222
|
if (next === null) return;
|
|
3642
4223
|
yield next;
|
|
@@ -3702,8 +4283,8 @@ var SseTransport = class {
|
|
|
3702
4283
|
constructor(opts) {
|
|
3703
4284
|
this.url = opts.url;
|
|
3704
4285
|
this.headers = opts.headers ?? {};
|
|
3705
|
-
this.endpointReady = new Promise((
|
|
3706
|
-
this.resolveEndpoint =
|
|
4286
|
+
this.endpointReady = new Promise((resolve7, reject) => {
|
|
4287
|
+
this.resolveEndpoint = resolve7;
|
|
3707
4288
|
this.rejectEndpoint = reject;
|
|
3708
4289
|
});
|
|
3709
4290
|
this.endpointReady.catch(() => void 0);
|
|
@@ -3730,8 +4311,8 @@ var SseTransport = class {
|
|
|
3730
4311
|
continue;
|
|
3731
4312
|
}
|
|
3732
4313
|
if (this.closed) return;
|
|
3733
|
-
const next = await new Promise((
|
|
3734
|
-
this.waiters.push(
|
|
4314
|
+
const next = await new Promise((resolve7) => {
|
|
4315
|
+
this.waiters.push(resolve7);
|
|
3735
4316
|
});
|
|
3736
4317
|
if (next === null) return;
|
|
3737
4318
|
yield next;
|
|
@@ -3930,8 +4511,8 @@ async function trySection(load) {
|
|
|
3930
4511
|
}
|
|
3931
4512
|
|
|
3932
4513
|
// src/code/edit-blocks.ts
|
|
3933
|
-
import { existsSync as
|
|
3934
|
-
import { dirname as dirname3, resolve as
|
|
4514
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4515
|
+
import { dirname as dirname3, resolve as resolve6 } from "path";
|
|
3935
4516
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
3936
4517
|
function parseEditBlocks(text) {
|
|
3937
4518
|
const out = [];
|
|
@@ -3949,8 +4530,8 @@ function parseEditBlocks(text) {
|
|
|
3949
4530
|
return out;
|
|
3950
4531
|
}
|
|
3951
4532
|
function applyEditBlock(block, rootDir) {
|
|
3952
|
-
const absRoot =
|
|
3953
|
-
const absTarget =
|
|
4533
|
+
const absRoot = resolve6(rootDir);
|
|
4534
|
+
const absTarget = resolve6(absRoot, block.path);
|
|
3954
4535
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
3955
4536
|
return {
|
|
3956
4537
|
path: block.path,
|
|
@@ -3959,7 +4540,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
3959
4540
|
};
|
|
3960
4541
|
}
|
|
3961
4542
|
const searchEmpty = block.search.length === 0;
|
|
3962
|
-
const exists =
|
|
4543
|
+
const exists = existsSync6(absTarget);
|
|
3963
4544
|
try {
|
|
3964
4545
|
if (!exists) {
|
|
3965
4546
|
if (!searchEmpty) {
|
|
@@ -3969,11 +4550,11 @@ function applyEditBlock(block, rootDir) {
|
|
|
3969
4550
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
3970
4551
|
};
|
|
3971
4552
|
}
|
|
3972
|
-
|
|
3973
|
-
|
|
4553
|
+
mkdirSync3(dirname3(absTarget), { recursive: true });
|
|
4554
|
+
writeFileSync3(absTarget, block.replace, "utf8");
|
|
3974
4555
|
return { path: block.path, status: "created" };
|
|
3975
4556
|
}
|
|
3976
|
-
const content =
|
|
4557
|
+
const content = readFileSync7(absTarget, "utf8");
|
|
3977
4558
|
if (searchEmpty) {
|
|
3978
4559
|
return {
|
|
3979
4560
|
path: block.path,
|
|
@@ -3990,7 +4571,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
3990
4571
|
};
|
|
3991
4572
|
}
|
|
3992
4573
|
const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
|
|
3993
|
-
|
|
4574
|
+
writeFileSync3(absTarget, replaced, "utf8");
|
|
3994
4575
|
return { path: block.path, status: "applied" };
|
|
3995
4576
|
} catch (err) {
|
|
3996
4577
|
return { path: block.path, status: "error", message: err.message };
|
|
@@ -4000,19 +4581,19 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
4000
4581
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
4001
4582
|
}
|
|
4002
4583
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
4003
|
-
const absRoot =
|
|
4584
|
+
const absRoot = resolve6(rootDir);
|
|
4004
4585
|
const seen = /* @__PURE__ */ new Set();
|
|
4005
4586
|
const snapshots = [];
|
|
4006
4587
|
for (const b of blocks) {
|
|
4007
4588
|
if (seen.has(b.path)) continue;
|
|
4008
4589
|
seen.add(b.path);
|
|
4009
|
-
const abs =
|
|
4010
|
-
if (!
|
|
4590
|
+
const abs = resolve6(absRoot, b.path);
|
|
4591
|
+
if (!existsSync6(abs)) {
|
|
4011
4592
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4012
4593
|
continue;
|
|
4013
4594
|
}
|
|
4014
4595
|
try {
|
|
4015
|
-
snapshots.push({ path: b.path, prevContent:
|
|
4596
|
+
snapshots.push({ path: b.path, prevContent: readFileSync7(abs, "utf8") });
|
|
4016
4597
|
} catch {
|
|
4017
4598
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4018
4599
|
}
|
|
@@ -4020,9 +4601,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
4020
4601
|
return snapshots;
|
|
4021
4602
|
}
|
|
4022
4603
|
function restoreSnapshots(snapshots, rootDir) {
|
|
4023
|
-
const absRoot =
|
|
4604
|
+
const absRoot = resolve6(rootDir);
|
|
4024
4605
|
return snapshots.map((snap) => {
|
|
4025
|
-
const abs =
|
|
4606
|
+
const abs = resolve6(absRoot, snap.path);
|
|
4026
4607
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
4027
4608
|
return {
|
|
4028
4609
|
path: snap.path,
|
|
@@ -4032,14 +4613,14 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
4032
4613
|
}
|
|
4033
4614
|
try {
|
|
4034
4615
|
if (snap.prevContent === null) {
|
|
4035
|
-
if (
|
|
4616
|
+
if (existsSync6(abs)) unlinkSync3(abs);
|
|
4036
4617
|
return {
|
|
4037
4618
|
path: snap.path,
|
|
4038
4619
|
status: "applied",
|
|
4039
4620
|
message: "removed (the edit had created it)"
|
|
4040
4621
|
};
|
|
4041
4622
|
}
|
|
4042
|
-
|
|
4623
|
+
writeFileSync3(abs, snap.prevContent, "utf8");
|
|
4043
4624
|
return {
|
|
4044
4625
|
path: snap.path,
|
|
4045
4626
|
status: "applied",
|
|
@@ -4055,8 +4636,8 @@ function sep() {
|
|
|
4055
4636
|
}
|
|
4056
4637
|
|
|
4057
4638
|
// src/code/prompt.ts
|
|
4058
|
-
import { existsSync as
|
|
4059
|
-
import { join as
|
|
4639
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
4640
|
+
import { join as join6 } from "path";
|
|
4060
4641
|
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.
|
|
4061
4642
|
|
|
4062
4643
|
# When to propose a plan (submit_plan)
|
|
@@ -4070,13 +4651,13 @@ You have a \`submit_plan\` tool that shows the user a markdown plan and lets the
|
|
|
4070
4651
|
|
|
4071
4652
|
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
4653
|
|
|
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"
|
|
4654
|
+
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.
|
|
4074
4655
|
|
|
4075
4656
|
# Plan mode (/plan)
|
|
4076
4657
|
|
|
4077
4658
|
The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
|
|
4078
4659
|
- 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
|
|
4660
|
+
- 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.
|
|
4080
4661
|
- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
|
|
4081
4662
|
|
|
4082
4663
|
|
|
@@ -4114,9 +4695,13 @@ Rules:
|
|
|
4114
4695
|
- 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).
|
|
4115
4696
|
- Paths are relative to the working directory. Don't use absolute paths.
|
|
4116
4697
|
|
|
4698
|
+
# Trust what you already know
|
|
4699
|
+
|
|
4700
|
+
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.
|
|
4701
|
+
|
|
4117
4702
|
# Exploration
|
|
4118
4703
|
|
|
4119
|
-
-
|
|
4704
|
+
- Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
|
|
4120
4705
|
- 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.
|
|
4121
4706
|
|
|
4122
4707
|
# Style
|
|
@@ -4126,12 +4711,12 @@ Rules:
|
|
|
4126
4711
|
- If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
|
|
4127
4712
|
`;
|
|
4128
4713
|
function codeSystemPrompt(rootDir) {
|
|
4129
|
-
const withMemory =
|
|
4130
|
-
const gitignorePath =
|
|
4131
|
-
if (!
|
|
4714
|
+
const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
|
|
4715
|
+
const gitignorePath = join6(rootDir, ".gitignore");
|
|
4716
|
+
if (!existsSync7(gitignorePath)) return withMemory;
|
|
4132
4717
|
let content;
|
|
4133
4718
|
try {
|
|
4134
|
-
content =
|
|
4719
|
+
content = readFileSync8(gitignorePath, "utf8");
|
|
4135
4720
|
} catch {
|
|
4136
4721
|
return withMemory;
|
|
4137
4722
|
}
|
|
@@ -4151,15 +4736,15 @@ ${truncated}
|
|
|
4151
4736
|
}
|
|
4152
4737
|
|
|
4153
4738
|
// src/config.ts
|
|
4154
|
-
import { chmodSync as chmodSync2, mkdirSync as
|
|
4155
|
-
import { homedir as
|
|
4156
|
-
import { dirname as dirname4, join as
|
|
4739
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
4740
|
+
import { homedir as homedir4 } from "os";
|
|
4741
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
4157
4742
|
function defaultConfigPath() {
|
|
4158
|
-
return
|
|
4743
|
+
return join7(homedir4(), ".reasonix", "config.json");
|
|
4159
4744
|
}
|
|
4160
4745
|
function readConfig(path = defaultConfigPath()) {
|
|
4161
4746
|
try {
|
|
4162
|
-
const raw =
|
|
4747
|
+
const raw = readFileSync9(path, "utf8");
|
|
4163
4748
|
const parsed = JSON.parse(raw);
|
|
4164
4749
|
if (parsed && typeof parsed === "object") return parsed;
|
|
4165
4750
|
} catch {
|
|
@@ -4167,8 +4752,8 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
4167
4752
|
return {};
|
|
4168
4753
|
}
|
|
4169
4754
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
4170
|
-
|
|
4171
|
-
|
|
4755
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
4756
|
+
writeFileSync4(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
4172
4757
|
try {
|
|
4173
4758
|
chmodSync2(path, 384);
|
|
4174
4759
|
} catch {
|
|
@@ -4194,7 +4779,7 @@ function redactKey(key) {
|
|
|
4194
4779
|
}
|
|
4195
4780
|
|
|
4196
4781
|
// src/index.ts
|
|
4197
|
-
var VERSION = "0.4.
|
|
4782
|
+
var VERSION = "0.4.20";
|
|
4198
4783
|
export {
|
|
4199
4784
|
AppendOnlyLog,
|
|
4200
4785
|
CODE_SYSTEM_PROMPT,
|
|
@@ -4203,7 +4788,10 @@ export {
|
|
|
4203
4788
|
DeepSeekClient,
|
|
4204
4789
|
ImmutablePrefix,
|
|
4205
4790
|
MCP_PROTOCOL_VERSION,
|
|
4791
|
+
MEMORY_INDEX_FILE,
|
|
4792
|
+
MEMORY_INDEX_MAX_CHARS,
|
|
4206
4793
|
McpClient,
|
|
4794
|
+
MemoryStore,
|
|
4207
4795
|
NeedsConfirmationError,
|
|
4208
4796
|
PROJECT_MEMORY_FILE,
|
|
4209
4797
|
PROJECT_MEMORY_MAX_CHARS,
|
|
@@ -4214,6 +4802,7 @@ export {
|
|
|
4214
4802
|
StormBreaker,
|
|
4215
4803
|
ToolCallRepair,
|
|
4216
4804
|
ToolRegistry,
|
|
4805
|
+
USER_MEMORY_DIR,
|
|
4217
4806
|
Usage,
|
|
4218
4807
|
VERSION,
|
|
4219
4808
|
VolatileScratch,
|
|
@@ -4222,7 +4811,9 @@ export {
|
|
|
4222
4811
|
appendSessionMessage,
|
|
4223
4812
|
applyEditBlock,
|
|
4224
4813
|
applyEditBlocks,
|
|
4814
|
+
applyMemoryStack,
|
|
4225
4815
|
applyProjectMemory,
|
|
4816
|
+
applyUserMemory,
|
|
4226
4817
|
bridgeMcpTools,
|
|
4227
4818
|
claudeEquivalentCost,
|
|
4228
4819
|
codeSystemPrompt,
|
|
@@ -4261,6 +4852,7 @@ export {
|
|
|
4261
4852
|
parseMojeekResults,
|
|
4262
4853
|
parseTranscript,
|
|
4263
4854
|
prepareSpawn,
|
|
4855
|
+
projectHash,
|
|
4264
4856
|
quoteForCmdExe,
|
|
4265
4857
|
readConfig,
|
|
4266
4858
|
readProjectMemory,
|
|
@@ -4268,6 +4860,7 @@ export {
|
|
|
4268
4860
|
recordFromLoopEvent,
|
|
4269
4861
|
redactKey,
|
|
4270
4862
|
registerFilesystemTools,
|
|
4863
|
+
registerMemoryTools,
|
|
4271
4864
|
registerPlanTool,
|
|
4272
4865
|
registerShellTools,
|
|
4273
4866
|
registerWebTools,
|
|
@@ -4279,6 +4872,7 @@ export {
|
|
|
4279
4872
|
restoreSnapshots,
|
|
4280
4873
|
runBranches,
|
|
4281
4874
|
runCommand,
|
|
4875
|
+
sanitizeMemoryName,
|
|
4282
4876
|
sanitizeName as sanitizeSessionName,
|
|
4283
4877
|
saveApiKey,
|
|
4284
4878
|
scavengeToolCalls,
|