reasonix 0.4.19 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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((resolve6, reject) => {
51
+ const timer = setTimeout(resolve6, 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((resolve6) => {
1541
+ waiter = resolve6;
1542
1542
  });
1543
1543
  yield {
1544
1544
  turn: this._turn,
@@ -1950,6 +1950,291 @@ ${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 existsSync3,
1957
+ mkdirSync as mkdirSync2,
1958
+ readFileSync as readFileSync3,
1959
+ readdirSync as readdirSync2,
1960
+ unlinkSync as unlinkSync2,
1961
+ writeFileSync as writeFileSync2
1962
+ } from "fs";
1963
+ import { homedir as homedir2 } from "os";
1964
+ import { join as join3, resolve } from "path";
1965
+ var USER_MEMORY_DIR = "memory";
1966
+ var MEMORY_INDEX_FILE = "MEMORY.md";
1967
+ var MEMORY_INDEX_MAX_CHARS = 4e3;
1968
+ var VALID_NAME = /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{1,38}[a-zA-Z0-9]$/;
1969
+ function sanitizeMemoryName(raw) {
1970
+ const trimmed = String(raw ?? "").trim();
1971
+ if (!VALID_NAME.test(trimmed)) {
1972
+ throw new Error(
1973
+ `invalid memory name: ${JSON.stringify(raw)} \u2014 must be 3-40 chars, alnum/_/-, no path separators`
1974
+ );
1975
+ }
1976
+ return trimmed;
1977
+ }
1978
+ function projectHash(rootDir) {
1979
+ const abs = resolve(rootDir);
1980
+ return createHash2("sha1").update(abs).digest("hex").slice(0, 16);
1981
+ }
1982
+ function scopeDir(opts) {
1983
+ if (opts.scope === "global") {
1984
+ return join3(opts.homeDir, USER_MEMORY_DIR, "global");
1985
+ }
1986
+ if (!opts.projectRoot) {
1987
+ throw new Error("scope=project requires a projectRoot on MemoryStore");
1988
+ }
1989
+ return join3(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
1990
+ }
1991
+ function ensureDir(p) {
1992
+ if (!existsSync3(p)) mkdirSync2(p, { recursive: true });
1993
+ }
1994
+ function parseFrontmatter(raw) {
1995
+ const lines = raw.split(/\r?\n/);
1996
+ if (lines[0] !== "---") return { data: {}, body: raw };
1997
+ const end = lines.indexOf("---", 1);
1998
+ if (end < 0) return { data: {}, body: raw };
1999
+ const data = {};
2000
+ for (let i = 1; i < end; i++) {
2001
+ const line = lines[i];
2002
+ if (!line) continue;
2003
+ const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
2004
+ if (m?.[1]) data[m[1]] = (m[2] ?? "").trim();
2005
+ }
2006
+ return {
2007
+ data,
2008
+ body: lines.slice(end + 1).join("\n").replace(/^\n+/, "")
2009
+ };
2010
+ }
2011
+ function formatFrontmatter(e) {
2012
+ return [
2013
+ "---",
2014
+ `name: ${e.name}`,
2015
+ `description: ${e.description.replace(/\n/g, " ")}`,
2016
+ `type: ${e.type}`,
2017
+ `scope: ${e.scope}`,
2018
+ `created: ${e.createdAt}`,
2019
+ "---",
2020
+ ""
2021
+ ].join("\n");
2022
+ }
2023
+ function todayIso() {
2024
+ const d = /* @__PURE__ */ new Date();
2025
+ return d.toISOString().slice(0, 10);
2026
+ }
2027
+ function indexLine(e) {
2028
+ const safeDesc = e.description.replace(/\n/g, " ").trim();
2029
+ const max = 130 - e.name.length;
2030
+ const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
2031
+ return `- [${e.name}](${e.name}.md) \u2014 ${clipped}`;
2032
+ }
2033
+ var MemoryStore = class {
2034
+ homeDir;
2035
+ projectRoot;
2036
+ constructor(opts = {}) {
2037
+ this.homeDir = opts.homeDir ?? join3(homedir2(), ".reasonix");
2038
+ this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : void 0;
2039
+ }
2040
+ /** Directory this store writes `scope` files into, creating it if needed. */
2041
+ dir(scope) {
2042
+ const d = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
2043
+ ensureDir(d);
2044
+ return d;
2045
+ }
2046
+ /** Absolute path to a memory file (no existence check). */
2047
+ pathFor(scope, name) {
2048
+ return join3(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
2049
+ }
2050
+ /** True iff this store is configured with a project scope available. */
2051
+ hasProjectScope() {
2052
+ return this.projectRoot !== void 0;
2053
+ }
2054
+ /**
2055
+ * Read the `MEMORY.md` index for a scope. Returns post-cap content
2056
+ * (with a truncation marker if clipped), or `null` when absent / empty.
2057
+ */
2058
+ loadIndex(scope) {
2059
+ if (scope === "project" && !this.projectRoot) return null;
2060
+ const file = join3(
2061
+ scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
2062
+ MEMORY_INDEX_FILE
2063
+ );
2064
+ if (!existsSync3(file)) return null;
2065
+ let raw;
2066
+ try {
2067
+ raw = readFileSync3(file, "utf8");
2068
+ } catch {
2069
+ return null;
2070
+ }
2071
+ const trimmed = raw.trim();
2072
+ if (!trimmed) return null;
2073
+ const originalChars = trimmed.length;
2074
+ const truncated = originalChars > MEMORY_INDEX_MAX_CHARS;
2075
+ const content = truncated ? `${trimmed.slice(0, MEMORY_INDEX_MAX_CHARS)}
2076
+ \u2026 (truncated ${originalChars - MEMORY_INDEX_MAX_CHARS} chars)` : trimmed;
2077
+ return { content, originalChars, truncated };
2078
+ }
2079
+ /** Read one memory file's body (frontmatter stripped). Throws if missing. */
2080
+ read(scope, name) {
2081
+ const file = this.pathFor(scope, name);
2082
+ if (!existsSync3(file)) {
2083
+ throw new Error(`memory not found: scope=${scope} name=${name}`);
2084
+ }
2085
+ const raw = readFileSync3(file, "utf8");
2086
+ const { data, body } = parseFrontmatter(raw);
2087
+ return {
2088
+ name: data.name ?? name,
2089
+ type: data.type ?? "project",
2090
+ scope: data.scope ?? scope,
2091
+ description: data.description ?? "",
2092
+ body: body.trim(),
2093
+ createdAt: data.created ?? ""
2094
+ };
2095
+ }
2096
+ /**
2097
+ * List every memory in this store. Scans both scopes (skips project
2098
+ * scope if unconfigured). Silently skips malformed files; the index
2099
+ * must stay queryable even if one file is hand-edited into nonsense.
2100
+ */
2101
+ list() {
2102
+ const out = [];
2103
+ const scopes = this.projectRoot ? ["global", "project"] : ["global"];
2104
+ for (const scope of scopes) {
2105
+ const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
2106
+ if (!existsSync3(dir)) continue;
2107
+ let entries;
2108
+ try {
2109
+ entries = readdirSync2(dir);
2110
+ } catch {
2111
+ continue;
2112
+ }
2113
+ for (const entry of entries) {
2114
+ if (entry === MEMORY_INDEX_FILE) continue;
2115
+ if (!entry.endsWith(".md")) continue;
2116
+ const name = entry.slice(0, -3);
2117
+ try {
2118
+ out.push(this.read(scope, name));
2119
+ } catch {
2120
+ }
2121
+ }
2122
+ }
2123
+ return out;
2124
+ }
2125
+ /**
2126
+ * Write a new memory (or overwrite existing). Creates the scope dir,
2127
+ * writes the `.md` file, and regenerates `MEMORY.md`. Returns the
2128
+ * absolute path written to.
2129
+ */
2130
+ write(input) {
2131
+ if (input.scope === "project" && !this.projectRoot) {
2132
+ throw new Error("cannot write project-scoped memory: no projectRoot configured");
2133
+ }
2134
+ const name = sanitizeMemoryName(input.name);
2135
+ const desc = String(input.description ?? "").trim();
2136
+ if (!desc) throw new Error("memory description cannot be empty");
2137
+ const body = String(input.body ?? "").trim();
2138
+ if (!body) throw new Error("memory body cannot be empty");
2139
+ const entry = {
2140
+ ...input,
2141
+ name,
2142
+ description: desc,
2143
+ body,
2144
+ createdAt: todayIso()
2145
+ };
2146
+ const dir = this.dir(input.scope);
2147
+ const file = join3(dir, `${name}.md`);
2148
+ const content = `${formatFrontmatter(entry)}${body}
2149
+ `;
2150
+ writeFileSync2(file, content, "utf8");
2151
+ this.regenerateIndex(input.scope);
2152
+ return file;
2153
+ }
2154
+ /** Delete one memory + its index line. No-op if the file is already gone. */
2155
+ delete(scope, rawName) {
2156
+ if (scope === "project" && !this.projectRoot) {
2157
+ throw new Error("cannot delete project-scoped memory: no projectRoot configured");
2158
+ }
2159
+ const file = this.pathFor(scope, rawName);
2160
+ if (!existsSync3(file)) return false;
2161
+ unlinkSync2(file);
2162
+ this.regenerateIndex(scope);
2163
+ return true;
2164
+ }
2165
+ /**
2166
+ * Rebuild `MEMORY.md` from the `.md` files currently in the scope dir.
2167
+ * Called after every write/delete. Sorted by name for stable prefix
2168
+ * hashing — two stores with the same set of files produce byte-identical
2169
+ * MEMORY.md content, keeping the cache prefix reproducible.
2170
+ */
2171
+ regenerateIndex(scope) {
2172
+ const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
2173
+ if (!existsSync3(dir)) return;
2174
+ let files;
2175
+ try {
2176
+ files = readdirSync2(dir);
2177
+ } catch {
2178
+ return;
2179
+ }
2180
+ const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
2181
+ const indexPath = join3(dir, MEMORY_INDEX_FILE);
2182
+ if (mdFiles.length === 0) {
2183
+ if (existsSync3(indexPath)) unlinkSync2(indexPath);
2184
+ return;
2185
+ }
2186
+ const lines = [];
2187
+ for (const f of mdFiles) {
2188
+ const name = f.slice(0, -3);
2189
+ try {
2190
+ const entry = this.read(scope, name);
2191
+ lines.push(indexLine({ name: entry.name || name, description: entry.description }));
2192
+ } catch {
2193
+ lines.push(`- [${name}](${name}.md) \u2014 (malformed, check frontmatter)`);
2194
+ }
2195
+ }
2196
+ writeFileSync2(indexPath, `${lines.join("\n")}
2197
+ `, "utf8");
2198
+ }
2199
+ };
2200
+ function applyUserMemory(basePrompt, opts = {}) {
2201
+ if (!memoryEnabled()) return basePrompt;
2202
+ const store = new MemoryStore(opts);
2203
+ const global = store.loadIndex("global");
2204
+ const project = store.hasProjectScope() ? store.loadIndex("project") : null;
2205
+ if (!global && !project) return basePrompt;
2206
+ const parts = [basePrompt];
2207
+ if (global) {
2208
+ parts.push(
2209
+ "",
2210
+ "# User memory \u2014 global (~/.reasonix/memory/global/MEMORY.md)",
2211
+ "",
2212
+ "Cross-project facts and preferences the user has told you in prior sessions. TREAT AS AUTHORITATIVE \u2014 don't re-verify via filesystem or web. One-liners index detail files; call `recall_memory` for full bodies only when the one-liner isn't enough.",
2213
+ "",
2214
+ "```",
2215
+ global.content,
2216
+ "```"
2217
+ );
2218
+ }
2219
+ if (project) {
2220
+ parts.push(
2221
+ "",
2222
+ "# User memory \u2014 this project",
2223
+ "",
2224
+ "Per-project facts the user established in prior sessions (not committed to the repo). TREAT AS AUTHORITATIVE. Same recall pattern as global memory.",
2225
+ "",
2226
+ "```",
2227
+ project.content,
2228
+ "```"
2229
+ );
2230
+ }
2231
+ return parts.join("\n");
2232
+ }
2233
+ function applyMemoryStack(basePrompt, rootDir) {
2234
+ const withProject = applyProjectMemory(basePrompt, rootDir);
2235
+ return applyUserMemory(withProject, { projectRoot: rootDir });
2236
+ }
2237
+
1953
2238
  // src/tools/filesystem.ts
1954
2239
  import { promises as fs } from "fs";
1955
2240
  import * as pathMod from "path";
@@ -2173,7 +2458,7 @@ function registerFilesystemTools(registry, opts) {
2173
2458
  });
2174
2459
  registry.register({
2175
2460
  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.",
2461
+ description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.",
2177
2462
  parameters: {
2178
2463
  type: "object",
2179
2464
  properties: {
@@ -2290,6 +2575,127 @@ function lineDiff(a, b) {
2290
2575
  return out;
2291
2576
  }
2292
2577
 
2578
+ // src/tools/memory.ts
2579
+ function registerMemoryTools(registry, opts = {}) {
2580
+ const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
2581
+ const hasProject = store.hasProjectScope();
2582
+ registry.register({
2583
+ name: "remember",
2584
+ description: "Save a memory for future sessions. Use when the user states a preference, corrects your approach, shares a non-obvious fact about this project, or explicitly asks you to remember something. Don't remember transient task state \u2014 only things worth recalling next session. The memory is written now but won't re-load into the system prompt until the next `/new` or launch.",
2585
+ parameters: {
2586
+ type: "object",
2587
+ properties: {
2588
+ type: {
2589
+ type: "string",
2590
+ enum: ["user", "feedback", "project", "reference"],
2591
+ description: "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
2592
+ },
2593
+ scope: {
2594
+ type: "string",
2595
+ enum: ["global", "project"],
2596
+ description: "'global' = applies across every project (preferences, tooling); 'project' = scoped to the current sandbox (decisions, local facts). Only available in `reasonix code`."
2597
+ },
2598
+ name: {
2599
+ type: "string",
2600
+ description: "filename-safe identifier, 3-40 chars, alnum + _ - . (no path separators, no leading dot)."
2601
+ },
2602
+ description: {
2603
+ type: "string",
2604
+ description: "One-line summary shown in MEMORY.md (under ~150 chars)."
2605
+ },
2606
+ content: {
2607
+ type: "string",
2608
+ description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
2609
+ }
2610
+ },
2611
+ required: ["type", "scope", "name", "description", "content"]
2612
+ },
2613
+ fn: async (args) => {
2614
+ if (args.scope === "project" && !hasProject) {
2615
+ return JSON.stringify({
2616
+ error: "scope='project' is unavailable in this session (no sandbox root). Retry with scope='global', or ask the user to switch to `reasonix code` for project-scoped memory."
2617
+ });
2618
+ }
2619
+ try {
2620
+ const path = store.write({
2621
+ name: args.name,
2622
+ type: args.type,
2623
+ scope: args.scope,
2624
+ description: args.description,
2625
+ body: args.content
2626
+ });
2627
+ const key = sanitizeMemoryName(args.name);
2628
+ return [
2629
+ `\u2713 REMEMBERED (${args.scope}/${key}): ${args.description}`,
2630
+ "",
2631
+ "TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
2632
+ "The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
2633
+ `(Saved to ${path}; pins into the system prompt on next /new or launch.)`
2634
+ ].join("\n");
2635
+ } catch (err) {
2636
+ return JSON.stringify({ error: `remember failed: ${err.message}` });
2637
+ }
2638
+ }
2639
+ });
2640
+ registry.register({
2641
+ name: "forget",
2642
+ description: "Delete a memory file and remove it from MEMORY.md. Use when the user explicitly asks to forget something, or when a previously-remembered fact has become wrong. Irreversible \u2014 no tombstone.",
2643
+ parameters: {
2644
+ type: "object",
2645
+ properties: {
2646
+ name: { type: "string", description: "Memory name (the identifier used in `remember`)." },
2647
+ scope: { type: "string", enum: ["global", "project"] }
2648
+ },
2649
+ required: ["name", "scope"]
2650
+ },
2651
+ fn: async (args) => {
2652
+ if (args.scope === "project" && !hasProject) {
2653
+ return JSON.stringify({
2654
+ error: "scope='project' is unavailable in this session (no sandbox root)."
2655
+ });
2656
+ }
2657
+ try {
2658
+ const existed = store.delete(args.scope, args.name);
2659
+ return existed ? `forgot (${args.scope}/${sanitizeMemoryName(args.name)}). Re-load on next /new or launch.` : `no such memory: ${args.scope}/${args.name} (nothing to forget).`;
2660
+ } catch (err) {
2661
+ return JSON.stringify({ error: `forget failed: ${err.message}` });
2662
+ }
2663
+ }
2664
+ });
2665
+ registry.register({
2666
+ name: "recall_memory",
2667
+ description: "Read the full body of a memory file when its MEMORY.md one-liner (already in the system prompt) isn't enough detail. Most of the time the index suffices \u2014 only call this when the user's question genuinely requires the full context.",
2668
+ readOnly: true,
2669
+ parameters: {
2670
+ type: "object",
2671
+ properties: {
2672
+ name: { type: "string" },
2673
+ scope: { type: "string", enum: ["global", "project"] }
2674
+ },
2675
+ required: ["name", "scope"]
2676
+ },
2677
+ fn: async (args) => {
2678
+ if (args.scope === "project" && !hasProject) {
2679
+ return JSON.stringify({
2680
+ error: "scope='project' is unavailable in this session (no sandbox root)."
2681
+ });
2682
+ }
2683
+ try {
2684
+ const entry = store.read(args.scope, args.name);
2685
+ return [
2686
+ `# ${entry.name} (${entry.scope}/${entry.type}, created ${entry.createdAt || "?"})`,
2687
+ entry.description ? `> ${entry.description}` : "",
2688
+ "",
2689
+ entry.body
2690
+ ].filter(Boolean).join("\n");
2691
+ } catch (err) {
2692
+ return JSON.stringify({ error: `recall failed: ${err.message}` });
2693
+ }
2694
+ }
2695
+ });
2696
+ return registry;
2697
+ }
2698
+
2293
2699
  // src/tools/plan.ts
2294
2700
  var PlanProposedError = class extends Error {
2295
2701
  plan;
@@ -2337,7 +2743,7 @@ function registerPlanTool(registry, opts = {}) {
2337
2743
 
2338
2744
  // src/tools/shell.ts
2339
2745
  import { spawn } from "child_process";
2340
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
2746
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
2341
2747
  import * as pathMod2 from "path";
2342
2748
  var DEFAULT_TIMEOUT_SEC = 60;
2343
2749
  var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
@@ -2457,7 +2863,7 @@ async function runCommand(cmd, opts) {
2457
2863
  };
2458
2864
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
2459
2865
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
2460
- return await new Promise((resolve5, reject) => {
2866
+ return await new Promise((resolve6, reject) => {
2461
2867
  let child;
2462
2868
  try {
2463
2869
  child = spawn(bin, args, effectiveSpawnOpts);
@@ -2490,7 +2896,7 @@ async function runCommand(cmd, opts) {
2490
2896
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
2491
2897
 
2492
2898
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
2493
- resolve5({ exitCode: code, output, timedOut });
2899
+ resolve6({ exitCode: code, output, timedOut });
2494
2900
  });
2495
2901
  });
2496
2902
  }
@@ -2507,7 +2913,7 @@ function resolveExecutable(cmd, opts = {}) {
2507
2913
  const isFile = opts.isFile ?? defaultIsFile;
2508
2914
  for (const dir of pathDirs) {
2509
2915
  for (const ext of pathExt) {
2510
- const full = pathMod2.join(dir, cmd + ext);
2916
+ const full = pathMod2.win32.join(dir, cmd + ext);
2511
2917
  if (isFile(full)) return full;
2512
2918
  }
2513
2919
  }
@@ -2515,7 +2921,7 @@ function resolveExecutable(cmd, opts = {}) {
2515
2921
  }
2516
2922
  function defaultIsFile(full) {
2517
2923
  try {
2518
- return existsSync3(full) && statSync2(full).isFile();
2924
+ return existsSync4(full) && statSync2(full).isFile();
2519
2925
  } catch {
2520
2926
  return false;
2521
2927
  }
@@ -2565,7 +2971,7 @@ function registerShellTools(registry, opts) {
2565
2971
  const allowAll = opts.allowAll ?? false;
2566
2972
  registry.register({
2567
2973
  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.",
2974
+ description: "Run a shell command in the project root and return its combined stdout+stderr. Common read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
2569
2975
  // Plan-mode gate: allow allowlisted commands through (git status,
2570
2976
  // cargo check, ls, grep …) so the model can actually investigate
2571
2977
  // during planning. Anything that would otherwise trigger a
@@ -2581,7 +2987,7 @@ function registerShellTools(registry, opts) {
2581
2987
  properties: {
2582
2988
  command: {
2583
2989
  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."
2990
+ description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
2585
2991
  },
2586
2992
  timeoutSec: {
2587
2993
  type: "integer",
@@ -2800,12 +3206,12 @@ ${i + 1}. ${r.title}`);
2800
3206
  }
2801
3207
 
2802
3208
  // src/env.ts
2803
- import { readFileSync as readFileSync3 } from "fs";
2804
- import { resolve as resolve3 } from "path";
3209
+ import { readFileSync as readFileSync4 } from "fs";
3210
+ import { resolve as resolve4 } from "path";
2805
3211
  function loadDotenv(path = ".env") {
2806
3212
  let raw;
2807
3213
  try {
2808
- raw = readFileSync3(resolve3(process.cwd(), path), "utf8");
3214
+ raw = readFileSync4(resolve4(process.cwd(), path), "utf8");
2809
3215
  } catch {
2810
3216
  return;
2811
3217
  }
@@ -2824,7 +3230,7 @@ function loadDotenv(path = ".env") {
2824
3230
  }
2825
3231
 
2826
3232
  // src/transcript.ts
2827
- import { createWriteStream, readFileSync as readFileSync4 } from "fs";
3233
+ import { createWriteStream, readFileSync as readFileSync5 } from "fs";
2828
3234
  function recordFromLoopEvent(ev, extra) {
2829
3235
  const rec = {
2830
3236
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2875,7 +3281,7 @@ function openTranscriptFile(path, meta) {
2875
3281
  return stream;
2876
3282
  }
2877
3283
  function readTranscript(path) {
2878
- const raw = readFileSync4(path, "utf8");
3284
+ const raw = readFileSync5(path, "utf8");
2879
3285
  return parseTranscript(raw);
2880
3286
  }
2881
3287
  function isPlanStateEmptyShape(s) {
@@ -3487,7 +3893,7 @@ var McpClient = class {
3487
3893
  const id = this.nextId++;
3488
3894
  const frame = { jsonrpc: "2.0", id, method, params };
3489
3895
  let abortHandler = null;
3490
- const promise = new Promise((resolve5, reject) => {
3896
+ const promise = new Promise((resolve6, reject) => {
3491
3897
  const timeout = setTimeout(() => {
3492
3898
  this.pending.delete(id);
3493
3899
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -3496,7 +3902,7 @@ var McpClient = class {
3496
3902
  );
3497
3903
  }, this.requestTimeoutMs);
3498
3904
  this.pending.set(id, {
3499
- resolve: resolve5,
3905
+ resolve: resolve6,
3500
3906
  reject,
3501
3907
  timeout
3502
3908
  });
@@ -3619,12 +4025,12 @@ var StdioTransport = class {
3619
4025
  }
3620
4026
  async send(message) {
3621
4027
  if (this.closed) throw new Error("MCP transport is closed");
3622
- return new Promise((resolve5, reject) => {
4028
+ return new Promise((resolve6, reject) => {
3623
4029
  const line = `${JSON.stringify(message)}
3624
4030
  `;
3625
4031
  this.child.stdin.write(line, "utf8", (err) => {
3626
4032
  if (err) reject(err);
3627
- else resolve5();
4033
+ else resolve6();
3628
4034
  });
3629
4035
  });
3630
4036
  }
@@ -3635,8 +4041,8 @@ var StdioTransport = class {
3635
4041
  continue;
3636
4042
  }
3637
4043
  if (this.closed) return;
3638
- const next = await new Promise((resolve5) => {
3639
- this.waiters.push(resolve5);
4044
+ const next = await new Promise((resolve6) => {
4045
+ this.waiters.push(resolve6);
3640
4046
  });
3641
4047
  if (next === null) return;
3642
4048
  yield next;
@@ -3702,8 +4108,8 @@ var SseTransport = class {
3702
4108
  constructor(opts) {
3703
4109
  this.url = opts.url;
3704
4110
  this.headers = opts.headers ?? {};
3705
- this.endpointReady = new Promise((resolve5, reject) => {
3706
- this.resolveEndpoint = resolve5;
4111
+ this.endpointReady = new Promise((resolve6, reject) => {
4112
+ this.resolveEndpoint = resolve6;
3707
4113
  this.rejectEndpoint = reject;
3708
4114
  });
3709
4115
  this.endpointReady.catch(() => void 0);
@@ -3730,8 +4136,8 @@ var SseTransport = class {
3730
4136
  continue;
3731
4137
  }
3732
4138
  if (this.closed) return;
3733
- const next = await new Promise((resolve5) => {
3734
- this.waiters.push(resolve5);
4139
+ const next = await new Promise((resolve6) => {
4140
+ this.waiters.push(resolve6);
3735
4141
  });
3736
4142
  if (next === null) return;
3737
4143
  yield next;
@@ -3930,8 +4336,8 @@ async function trySection(load) {
3930
4336
  }
3931
4337
 
3932
4338
  // 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";
4339
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
4340
+ import { dirname as dirname3, resolve as resolve5 } from "path";
3935
4341
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
3936
4342
  function parseEditBlocks(text) {
3937
4343
  const out = [];
@@ -3949,8 +4355,8 @@ function parseEditBlocks(text) {
3949
4355
  return out;
3950
4356
  }
3951
4357
  function applyEditBlock(block, rootDir) {
3952
- const absRoot = resolve4(rootDir);
3953
- const absTarget = resolve4(absRoot, block.path);
4358
+ const absRoot = resolve5(rootDir);
4359
+ const absTarget = resolve5(absRoot, block.path);
3954
4360
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
3955
4361
  return {
3956
4362
  path: block.path,
@@ -3959,7 +4365,7 @@ function applyEditBlock(block, rootDir) {
3959
4365
  };
3960
4366
  }
3961
4367
  const searchEmpty = block.search.length === 0;
3962
- const exists = existsSync4(absTarget);
4368
+ const exists = existsSync5(absTarget);
3963
4369
  try {
3964
4370
  if (!exists) {
3965
4371
  if (!searchEmpty) {
@@ -3969,11 +4375,11 @@ function applyEditBlock(block, rootDir) {
3969
4375
  message: "file does not exist; to create it, use an empty SEARCH block"
3970
4376
  };
3971
4377
  }
3972
- mkdirSync2(dirname3(absTarget), { recursive: true });
3973
- writeFileSync2(absTarget, block.replace, "utf8");
4378
+ mkdirSync3(dirname3(absTarget), { recursive: true });
4379
+ writeFileSync3(absTarget, block.replace, "utf8");
3974
4380
  return { path: block.path, status: "created" };
3975
4381
  }
3976
- const content = readFileSync5(absTarget, "utf8");
4382
+ const content = readFileSync6(absTarget, "utf8");
3977
4383
  if (searchEmpty) {
3978
4384
  return {
3979
4385
  path: block.path,
@@ -3990,7 +4396,7 @@ function applyEditBlock(block, rootDir) {
3990
4396
  };
3991
4397
  }
3992
4398
  const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
3993
- writeFileSync2(absTarget, replaced, "utf8");
4399
+ writeFileSync3(absTarget, replaced, "utf8");
3994
4400
  return { path: block.path, status: "applied" };
3995
4401
  } catch (err) {
3996
4402
  return { path: block.path, status: "error", message: err.message };
@@ -4000,19 +4406,19 @@ function applyEditBlocks(blocks, rootDir) {
4000
4406
  return blocks.map((b) => applyEditBlock(b, rootDir));
4001
4407
  }
4002
4408
  function snapshotBeforeEdits(blocks, rootDir) {
4003
- const absRoot = resolve4(rootDir);
4409
+ const absRoot = resolve5(rootDir);
4004
4410
  const seen = /* @__PURE__ */ new Set();
4005
4411
  const snapshots = [];
4006
4412
  for (const b of blocks) {
4007
4413
  if (seen.has(b.path)) continue;
4008
4414
  seen.add(b.path);
4009
- const abs = resolve4(absRoot, b.path);
4010
- if (!existsSync4(abs)) {
4415
+ const abs = resolve5(absRoot, b.path);
4416
+ if (!existsSync5(abs)) {
4011
4417
  snapshots.push({ path: b.path, prevContent: null });
4012
4418
  continue;
4013
4419
  }
4014
4420
  try {
4015
- snapshots.push({ path: b.path, prevContent: readFileSync5(abs, "utf8") });
4421
+ snapshots.push({ path: b.path, prevContent: readFileSync6(abs, "utf8") });
4016
4422
  } catch {
4017
4423
  snapshots.push({ path: b.path, prevContent: null });
4018
4424
  }
@@ -4020,9 +4426,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
4020
4426
  return snapshots;
4021
4427
  }
4022
4428
  function restoreSnapshots(snapshots, rootDir) {
4023
- const absRoot = resolve4(rootDir);
4429
+ const absRoot = resolve5(rootDir);
4024
4430
  return snapshots.map((snap) => {
4025
- const abs = resolve4(absRoot, snap.path);
4431
+ const abs = resolve5(absRoot, snap.path);
4026
4432
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
4027
4433
  return {
4028
4434
  path: snap.path,
@@ -4032,14 +4438,14 @@ function restoreSnapshots(snapshots, rootDir) {
4032
4438
  }
4033
4439
  try {
4034
4440
  if (snap.prevContent === null) {
4035
- if (existsSync4(abs)) unlinkSync2(abs);
4441
+ if (existsSync5(abs)) unlinkSync3(abs);
4036
4442
  return {
4037
4443
  path: snap.path,
4038
4444
  status: "applied",
4039
4445
  message: "removed (the edit had created it)"
4040
4446
  };
4041
4447
  }
4042
- writeFileSync2(abs, snap.prevContent, "utf8");
4448
+ writeFileSync3(abs, snap.prevContent, "utf8");
4043
4449
  return {
4044
4450
  path: snap.path,
4045
4451
  status: "applied",
@@ -4055,7 +4461,7 @@ function sep() {
4055
4461
  }
4056
4462
 
4057
4463
  // src/code/prompt.ts
4058
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
4464
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
4059
4465
  import { join as join5 } from "path";
4060
4466
  var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
4061
4467
 
@@ -4070,13 +4476,13 @@ You have a \`submit_plan\` tool that shows the user a markdown plan and lets the
4070
4476
 
4071
4477
  Skip submit_plan for small, obvious changes: one-line typo, clear bug with a clear fix, adding a missing import, renaming a local variable. Just do those.
4072
4478
 
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.
4479
+ Plan body: one-sentence summary, then a file-by-file breakdown of what you'll change and why, and any risks or open questions. If some decisions are genuinely up to the user (naming, tradeoffs, out-of-scope possibilities), list them in an "Open questions" section \u2014 the user sees the plan in a picker and has a text input to answer your questions before approving. Don't pretend certainty you don't have; flagged questions are how the user tells you what they care about. After calling submit_plan, STOP \u2014 don't call any more tools, wait for the user's verdict.
4074
4480
 
4075
4481
  # Plan mode (/plan)
4076
4482
 
4077
4483
  The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
4078
4484
  - Write tools (edit_file, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
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.
4485
+ - Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work \u2014 use them to investigate.
4080
4486
  - You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
4081
4487
 
4082
4488
 
@@ -4114,9 +4520,13 @@ Rules:
4114
4520
  - Do NOT use write_file to change existing files \u2014 the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).
4115
4521
  - Paths are relative to the working directory. Don't use absolute paths.
4116
4522
 
4523
+ # Trust what you already know
4524
+
4525
+ Before exploring the filesystem to answer a factual question, check whether the answer is already in context: the user's current message, earlier turns in this conversation (including prior tool results from \`remember\`), and the pinned memory blocks at the top of this prompt. When the user has stated a fact or you have remembered one, it outranks what the files say \u2014 don't re-derive from code what the user already told you. Explore when you genuinely don't know.
4526
+
4117
4527
  # Exploration
4118
4528
 
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.
4529
+ - Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
4120
4530
  - Prefer search_files / grep over list_directory when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees.
4121
4531
 
4122
4532
  # Style
@@ -4126,12 +4536,12 @@ Rules:
4126
4536
  - If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
4127
4537
  `;
4128
4538
  function codeSystemPrompt(rootDir) {
4129
- const withMemory = applyProjectMemory(CODE_SYSTEM_PROMPT, rootDir);
4539
+ const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
4130
4540
  const gitignorePath = join5(rootDir, ".gitignore");
4131
- if (!existsSync5(gitignorePath)) return withMemory;
4541
+ if (!existsSync6(gitignorePath)) return withMemory;
4132
4542
  let content;
4133
4543
  try {
4134
- content = readFileSync6(gitignorePath, "utf8");
4544
+ content = readFileSync7(gitignorePath, "utf8");
4135
4545
  } catch {
4136
4546
  return withMemory;
4137
4547
  }
@@ -4151,15 +4561,15 @@ ${truncated}
4151
4561
  }
4152
4562
 
4153
4563
  // 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";
4564
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
4565
+ import { homedir as homedir3 } from "os";
4156
4566
  import { dirname as dirname4, join as join6 } from "path";
4157
4567
  function defaultConfigPath() {
4158
- return join6(homedir2(), ".reasonix", "config.json");
4568
+ return join6(homedir3(), ".reasonix", "config.json");
4159
4569
  }
4160
4570
  function readConfig(path = defaultConfigPath()) {
4161
4571
  try {
4162
- const raw = readFileSync7(path, "utf8");
4572
+ const raw = readFileSync8(path, "utf8");
4163
4573
  const parsed = JSON.parse(raw);
4164
4574
  if (parsed && typeof parsed === "object") return parsed;
4165
4575
  } catch {
@@ -4167,8 +4577,8 @@ function readConfig(path = defaultConfigPath()) {
4167
4577
  return {};
4168
4578
  }
4169
4579
  function writeConfig(cfg, path = defaultConfigPath()) {
4170
- mkdirSync3(dirname4(path), { recursive: true });
4171
- writeFileSync3(path, JSON.stringify(cfg, null, 2), "utf8");
4580
+ mkdirSync4(dirname4(path), { recursive: true });
4581
+ writeFileSync4(path, JSON.stringify(cfg, null, 2), "utf8");
4172
4582
  try {
4173
4583
  chmodSync2(path, 384);
4174
4584
  } catch {
@@ -4194,7 +4604,7 @@ function redactKey(key) {
4194
4604
  }
4195
4605
 
4196
4606
  // src/index.ts
4197
- var VERSION = "0.4.19";
4607
+ var VERSION = "0.4.20";
4198
4608
  export {
4199
4609
  AppendOnlyLog,
4200
4610
  CODE_SYSTEM_PROMPT,
@@ -4203,7 +4613,10 @@ export {
4203
4613
  DeepSeekClient,
4204
4614
  ImmutablePrefix,
4205
4615
  MCP_PROTOCOL_VERSION,
4616
+ MEMORY_INDEX_FILE,
4617
+ MEMORY_INDEX_MAX_CHARS,
4206
4618
  McpClient,
4619
+ MemoryStore,
4207
4620
  NeedsConfirmationError,
4208
4621
  PROJECT_MEMORY_FILE,
4209
4622
  PROJECT_MEMORY_MAX_CHARS,
@@ -4214,6 +4627,7 @@ export {
4214
4627
  StormBreaker,
4215
4628
  ToolCallRepair,
4216
4629
  ToolRegistry,
4630
+ USER_MEMORY_DIR,
4217
4631
  Usage,
4218
4632
  VERSION,
4219
4633
  VolatileScratch,
@@ -4222,7 +4636,9 @@ export {
4222
4636
  appendSessionMessage,
4223
4637
  applyEditBlock,
4224
4638
  applyEditBlocks,
4639
+ applyMemoryStack,
4225
4640
  applyProjectMemory,
4641
+ applyUserMemory,
4226
4642
  bridgeMcpTools,
4227
4643
  claudeEquivalentCost,
4228
4644
  codeSystemPrompt,
@@ -4261,6 +4677,7 @@ export {
4261
4677
  parseMojeekResults,
4262
4678
  parseTranscript,
4263
4679
  prepareSpawn,
4680
+ projectHash,
4264
4681
  quoteForCmdExe,
4265
4682
  readConfig,
4266
4683
  readProjectMemory,
@@ -4268,6 +4685,7 @@ export {
4268
4685
  recordFromLoopEvent,
4269
4686
  redactKey,
4270
4687
  registerFilesystemTools,
4688
+ registerMemoryTools,
4271
4689
  registerPlanTool,
4272
4690
  registerShellTools,
4273
4691
  registerWebTools,
@@ -4279,6 +4697,7 @@ export {
4279
4697
  restoreSnapshots,
4280
4698
  runBranches,
4281
4699
  runCommand,
4700
+ sanitizeMemoryName,
4282
4701
  sanitizeName as sanitizeSessionName,
4283
4702
  saveApiKey,
4284
4703
  scavengeToolCalls,