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/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((resolve5, reject) => {
51
- const timer = setTimeout(resolve5, ms);
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((resolve5) => {
1541
- waiter = resolve5;
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. This flat-string shape replaces the `{oldText, newText}[]` JSON array form that previously triggered R1 DSML hallucinations.",
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 existsSync3, statSync as statSync2 } from "fs";
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((resolve5, reject) => {
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
- resolve5({ exitCode: code, output, timedOut });
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 existsSync3(full) && statSync2(full).isFile();
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 extraAllowed = opts.extraAllowed ?? [];
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. Read-only and test commands (git status, ls, npm test, pytest, cargo test, grep, etc.) run immediately. Anything that could mutate state (npm install, git commit, rm, chmod) is refused and the user has to confirm in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
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, extraAllowed);
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, e.g. 'npm test' or 'git diff src/foo.ts'. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
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, extraAllowed)) {
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 readFileSync3 } from "fs";
2804
- import { resolve as resolve3 } from "path";
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 = readFileSync3(resolve3(process.cwd(), path), "utf8");
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 readFileSync4 } from "fs";
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 = readFileSync4(path, "utf8");
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((resolve5, reject) => {
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: resolve5,
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((resolve5, reject) => {
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 resolve5();
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((resolve5) => {
3639
- this.waiters.push(resolve5);
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((resolve5, reject) => {
3706
- this.resolveEndpoint = resolve5;
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((resolve5) => {
3734
- this.waiters.push(resolve5);
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 existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
3934
- import { dirname as dirname3, resolve as resolve4 } from "path";
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 = resolve4(rootDir);
3953
- const absTarget = resolve4(absRoot, block.path);
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 = existsSync4(absTarget);
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
- mkdirSync2(dirname3(absTarget), { recursive: true });
3973
- writeFileSync2(absTarget, block.replace, "utf8");
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 = readFileSync5(absTarget, "utf8");
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
- writeFileSync2(absTarget, replaced, "utf8");
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 = resolve4(rootDir);
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 = resolve4(absRoot, b.path);
4010
- if (!existsSync4(abs)) {
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: readFileSync5(abs, "utf8") });
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 = resolve4(rootDir);
4604
+ const absRoot = resolve6(rootDir);
4024
4605
  return snapshots.map((snap) => {
4025
- const abs = resolve4(absRoot, snap.path);
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 (existsSync4(abs)) unlinkSync2(abs);
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
- writeFileSync2(abs, snap.prevContent, "utf8");
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 existsSync5, readFileSync as readFileSync6 } from "fs";
4059
- import { join as join5 } from "path";
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" or "\u5F85\u786E\u8BA4" section \u2014 the user sees the plan in a picker and has a text input to answer your questions before approving. Don't pretend certainty you don't have; flagged questions are how the user tells you what they care about. After calling submit_plan, STOP \u2014 don't call any more tools, wait for the user's verdict.
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 shell (git status/log/diff, ls, cat, grep, cargo check, npm test) still work \u2014 use them to investigate.
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
- - Avoid listing or reading inside these common dependency / build directories unless the user explicitly asks about them: node_modules, dist, build, out, .next, .nuxt, .svelte-kit, .git, .venv, venv, __pycache__, target, coverage, .turbo, .cache. They're expensive and usually irrelevant.
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 = applyProjectMemory(CODE_SYSTEM_PROMPT, rootDir);
4130
- const gitignorePath = join5(rootDir, ".gitignore");
4131
- if (!existsSync5(gitignorePath)) return withMemory;
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 = readFileSync6(gitignorePath, "utf8");
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 mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
4155
- import { homedir as homedir2 } from "os";
4156
- import { dirname as dirname4, join as join6 } from "path";
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 join6(homedir2(), ".reasonix", "config.json");
4743
+ return join7(homedir4(), ".reasonix", "config.json");
4159
4744
  }
4160
4745
  function readConfig(path = defaultConfigPath()) {
4161
4746
  try {
4162
- const raw = readFileSync7(path, "utf8");
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
- mkdirSync3(dirname4(path), { recursive: true });
4171
- writeFileSync3(path, JSON.stringify(cfg, null, 2), "utf8");
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.19";
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,