reasonix 0.4.15 → 0.4.17

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((resolve4, reject) => {
51
- const timer = setTimeout(resolve4, ms);
50
+ return new Promise((resolve5, reject) => {
51
+ const timer = setTimeout(resolve5, ms);
52
52
  if (signal) {
53
53
  const onAbort = () => {
54
54
  clearTimeout(timer);
@@ -1488,8 +1488,8 @@ var CacheFirstLoop = class {
1488
1488
  }
1489
1489
  );
1490
1490
  for (let k = 0; k < budget; k++) {
1491
- const sample = queue.shift() ?? await new Promise((resolve4) => {
1492
- waiter = resolve4;
1491
+ const sample = queue.shift() ?? await new Promise((resolve5) => {
1492
+ waiter = resolve5;
1493
1493
  });
1494
1494
  yield {
1495
1495
  turn: this._turn,
@@ -1848,6 +1848,49 @@ function formatLoopError(err) {
1848
1848
  return msg;
1849
1849
  }
1850
1850
 
1851
+ // src/project-memory.ts
1852
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1853
+ import { join as join2 } from "path";
1854
+ var PROJECT_MEMORY_FILE = "REASONIX.md";
1855
+ var PROJECT_MEMORY_MAX_CHARS = 8e3;
1856
+ function readProjectMemory(rootDir) {
1857
+ const path = join2(rootDir, PROJECT_MEMORY_FILE);
1858
+ if (!existsSync2(path)) return null;
1859
+ let raw;
1860
+ try {
1861
+ raw = readFileSync2(path, "utf8");
1862
+ } catch {
1863
+ return null;
1864
+ }
1865
+ const trimmed = raw.trim();
1866
+ if (!trimmed) return null;
1867
+ const originalChars = trimmed.length;
1868
+ const truncated = originalChars > PROJECT_MEMORY_MAX_CHARS;
1869
+ const content = truncated ? `${trimmed.slice(0, PROJECT_MEMORY_MAX_CHARS)}
1870
+ \u2026 (truncated ${originalChars - PROJECT_MEMORY_MAX_CHARS} chars)` : trimmed;
1871
+ return { path, content, originalChars, truncated };
1872
+ }
1873
+ function memoryEnabled() {
1874
+ const env = process.env.REASONIX_MEMORY;
1875
+ if (env === "off" || env === "false" || env === "0") return false;
1876
+ return true;
1877
+ }
1878
+ function applyProjectMemory(basePrompt, rootDir) {
1879
+ if (!memoryEnabled()) return basePrompt;
1880
+ const mem = readProjectMemory(rootDir);
1881
+ if (!mem) return basePrompt;
1882
+ return `${basePrompt}
1883
+
1884
+ # Project memory (REASONIX.md)
1885
+
1886
+ The user pinned these notes about this project \u2014 treat them as authoritative context for every turn:
1887
+
1888
+ \`\`\`
1889
+ ${mem.content}
1890
+ \`\`\`
1891
+ `;
1892
+ }
1893
+
1851
1894
  // src/tools/filesystem.ts
1852
1895
  import { promises as fs } from "fs";
1853
1896
  import * as pathMod from "path";
@@ -2183,6 +2226,221 @@ function lineDiff(a, b) {
2183
2226
  return out;
2184
2227
  }
2185
2228
 
2229
+ // src/tools/shell.ts
2230
+ import { spawn } from "child_process";
2231
+ import * as pathMod2 from "path";
2232
+ var DEFAULT_TIMEOUT_SEC = 60;
2233
+ var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
2234
+ var BUILTIN_ALLOWLIST = [
2235
+ // Repo inspection
2236
+ "git status",
2237
+ "git diff",
2238
+ "git log",
2239
+ "git show",
2240
+ "git blame",
2241
+ "git branch",
2242
+ "git remote",
2243
+ "git rev-parse",
2244
+ "git config --get",
2245
+ // Filesystem inspection
2246
+ "ls",
2247
+ "pwd",
2248
+ "cat",
2249
+ "head",
2250
+ "tail",
2251
+ "wc",
2252
+ "file",
2253
+ "tree",
2254
+ "find",
2255
+ "grep",
2256
+ "rg",
2257
+ // Language version probes
2258
+ "node --version",
2259
+ "node -v",
2260
+ "npm --version",
2261
+ "npx --version",
2262
+ "python --version",
2263
+ "python3 --version",
2264
+ "cargo --version",
2265
+ "go version",
2266
+ "rustc --version",
2267
+ "deno --version",
2268
+ "bun --version",
2269
+ // Test runners (non-destructive by convention)
2270
+ "npm test",
2271
+ "npm run test",
2272
+ "npx vitest run",
2273
+ "npx vitest",
2274
+ "npx jest",
2275
+ "pytest",
2276
+ "python -m pytest",
2277
+ "cargo test",
2278
+ "cargo check",
2279
+ "cargo clippy",
2280
+ "go test",
2281
+ "go vet",
2282
+ "deno test",
2283
+ "bun test",
2284
+ // Linters / typecheckers (read-only by convention)
2285
+ "npm run lint",
2286
+ "npm run typecheck",
2287
+ "npx tsc --noEmit",
2288
+ "npx biome check",
2289
+ "npx eslint",
2290
+ "npx prettier --check",
2291
+ "ruff",
2292
+ "mypy"
2293
+ ];
2294
+ function tokenizeCommand(cmd) {
2295
+ const out = [];
2296
+ let cur = "";
2297
+ let quote = null;
2298
+ for (let i = 0; i < cmd.length; i++) {
2299
+ const ch = cmd[i];
2300
+ if (quote) {
2301
+ if (ch === quote) {
2302
+ quote = null;
2303
+ } else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
2304
+ cur += cmd[++i];
2305
+ } else {
2306
+ cur += ch;
2307
+ }
2308
+ continue;
2309
+ }
2310
+ if (ch === '"' || ch === "'") {
2311
+ quote = ch;
2312
+ continue;
2313
+ }
2314
+ if (ch === " " || ch === " ") {
2315
+ if (cur.length > 0) {
2316
+ out.push(cur);
2317
+ cur = "";
2318
+ }
2319
+ continue;
2320
+ }
2321
+ cur += ch;
2322
+ }
2323
+ if (quote) throw new Error(`unclosed ${quote} in command`);
2324
+ if (cur.length > 0) out.push(cur);
2325
+ return out;
2326
+ }
2327
+ function isAllowed(cmd, extra = []) {
2328
+ const normalized = cmd.trim().replace(/\s+/g, " ");
2329
+ const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
2330
+ for (const prefix of allowlist) {
2331
+ if (normalized === prefix) return true;
2332
+ if (normalized.startsWith(`${prefix} `)) return true;
2333
+ }
2334
+ return false;
2335
+ }
2336
+ async function runCommand(cmd, opts) {
2337
+ const argv = tokenizeCommand(cmd);
2338
+ if (argv.length === 0) throw new Error("run_command: empty command");
2339
+ const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
2340
+ const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
2341
+ const spawnOpts = {
2342
+ cwd: opts.cwd,
2343
+ shell: false,
2344
+ // no shell-expansion — see header comment
2345
+ windowsHide: true,
2346
+ env: process.env
2347
+ };
2348
+ return await new Promise((resolve5, reject) => {
2349
+ let child;
2350
+ try {
2351
+ child = spawn(argv[0], argv.slice(1), spawnOpts);
2352
+ } catch (err) {
2353
+ reject(err);
2354
+ return;
2355
+ }
2356
+ let buf = "";
2357
+ let timedOut = false;
2358
+ const killTimer = setTimeout(() => {
2359
+ timedOut = true;
2360
+ child.kill("SIGKILL");
2361
+ }, timeoutMs);
2362
+ const onAbort = () => child.kill("SIGKILL");
2363
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2364
+ const onData = (chunk) => {
2365
+ buf += chunk.toString();
2366
+ if (buf.length > maxChars * 2) buf = `${buf.slice(0, maxChars * 2)}`;
2367
+ };
2368
+ child.stdout?.on("data", onData);
2369
+ child.stderr?.on("data", onData);
2370
+ child.on("error", (err) => {
2371
+ clearTimeout(killTimer);
2372
+ opts.signal?.removeEventListener("abort", onAbort);
2373
+ reject(err);
2374
+ });
2375
+ child.on("close", (code) => {
2376
+ clearTimeout(killTimer);
2377
+ opts.signal?.removeEventListener("abort", onAbort);
2378
+ const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
2379
+
2380
+ [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
2381
+ resolve5({ exitCode: code, output, timedOut });
2382
+ });
2383
+ });
2384
+ }
2385
+ var NeedsConfirmationError = class extends Error {
2386
+ command;
2387
+ constructor(command) {
2388
+ super(
2389
+ `run_command: "${command}" needs the user's approval before it runs. STOP calling tools now \u2014 the TUI has already prompted the user to press y (run) or n (deny). Wait for their next message; it will either be the command's output (if they approved) or an instruction to continue without it (if they denied). Don't retry the command or call other shell commands in the meantime.`
2390
+ );
2391
+ this.name = "NeedsConfirmationError";
2392
+ this.command = command;
2393
+ }
2394
+ };
2395
+ function registerShellTools(registry, opts) {
2396
+ const rootDir = pathMod2.resolve(opts.rootDir);
2397
+ const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
2398
+ const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
2399
+ const extraAllowed = opts.extraAllowed ?? [];
2400
+ const allowAll = opts.allowAll ?? false;
2401
+ registry.register({
2402
+ name: "run_command",
2403
+ 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.",
2404
+ parameters: {
2405
+ type: "object",
2406
+ properties: {
2407
+ command: {
2408
+ type: "string",
2409
+ 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."
2410
+ },
2411
+ timeoutSec: {
2412
+ type: "integer",
2413
+ description: `Override the default ${timeoutSec}s timeout for a single command.`
2414
+ }
2415
+ },
2416
+ required: ["command"]
2417
+ },
2418
+ fn: async (args, ctx) => {
2419
+ const cmd = args.command.trim();
2420
+ if (!cmd) throw new Error("run_command: empty command");
2421
+ if (!allowAll && !isAllowed(cmd, extraAllowed)) {
2422
+ throw new NeedsConfirmationError(cmd);
2423
+ }
2424
+ const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
2425
+ const result = await runCommand(cmd, {
2426
+ cwd: rootDir,
2427
+ timeoutSec: effectiveTimeout,
2428
+ maxOutputChars,
2429
+ signal: ctx?.signal
2430
+ });
2431
+ return formatCommandResult(cmd, result);
2432
+ }
2433
+ });
2434
+ return registry;
2435
+ }
2436
+ function formatCommandResult(cmd, r) {
2437
+ const header = r.timedOut ? `$ ${cmd}
2438
+ [killed after timeout]` : `$ ${cmd}
2439
+ [exit ${r.exitCode ?? "?"}]`;
2440
+ return r.output ? `${header}
2441
+ ${r.output}` : header;
2442
+ }
2443
+
2186
2444
  // src/tools/web.ts
2187
2445
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
2188
2446
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
@@ -2365,12 +2623,12 @@ ${i + 1}. ${r.title}`);
2365
2623
  }
2366
2624
 
2367
2625
  // src/env.ts
2368
- import { readFileSync as readFileSync2 } from "fs";
2369
- import { resolve as resolve2 } from "path";
2626
+ import { readFileSync as readFileSync3 } from "fs";
2627
+ import { resolve as resolve3 } from "path";
2370
2628
  function loadDotenv(path = ".env") {
2371
2629
  let raw;
2372
2630
  try {
2373
- raw = readFileSync2(resolve2(process.cwd(), path), "utf8");
2631
+ raw = readFileSync3(resolve3(process.cwd(), path), "utf8");
2374
2632
  } catch {
2375
2633
  return;
2376
2634
  }
@@ -2389,7 +2647,7 @@ function loadDotenv(path = ".env") {
2389
2647
  }
2390
2648
 
2391
2649
  // src/transcript.ts
2392
- import { createWriteStream, readFileSync as readFileSync3 } from "fs";
2650
+ import { createWriteStream, readFileSync as readFileSync4 } from "fs";
2393
2651
  function recordFromLoopEvent(ev, extra) {
2394
2652
  const rec = {
2395
2653
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2440,7 +2698,7 @@ function openTranscriptFile(path, meta) {
2440
2698
  return stream;
2441
2699
  }
2442
2700
  function readTranscript(path) {
2443
- const raw = readFileSync3(path, "utf8");
2701
+ const raw = readFileSync4(path, "utf8");
2444
2702
  return parseTranscript(raw);
2445
2703
  }
2446
2704
  function isPlanStateEmptyShape(s) {
@@ -3052,7 +3310,7 @@ var McpClient = class {
3052
3310
  const id = this.nextId++;
3053
3311
  const frame = { jsonrpc: "2.0", id, method, params };
3054
3312
  let abortHandler = null;
3055
- const promise = new Promise((resolve4, reject) => {
3313
+ const promise = new Promise((resolve5, reject) => {
3056
3314
  const timeout = setTimeout(() => {
3057
3315
  this.pending.delete(id);
3058
3316
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -3061,7 +3319,7 @@ var McpClient = class {
3061
3319
  );
3062
3320
  }, this.requestTimeoutMs);
3063
3321
  this.pending.set(id, {
3064
- resolve: resolve4,
3322
+ resolve: resolve5,
3065
3323
  reject,
3066
3324
  timeout
3067
3325
  });
@@ -3143,7 +3401,7 @@ var McpClient = class {
3143
3401
  };
3144
3402
 
3145
3403
  // src/mcp/stdio.ts
3146
- import { spawn } from "child_process";
3404
+ import { spawn as spawn2 } from "child_process";
3147
3405
  var StdioTransport = class {
3148
3406
  child;
3149
3407
  queue = [];
@@ -3158,14 +3416,14 @@ var StdioTransport = class {
3158
3416
  opts.command,
3159
3417
  ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
3160
3418
  ].join(" ");
3161
- this.child = spawn(line, [], {
3419
+ this.child = spawn2(line, [], {
3162
3420
  env,
3163
3421
  cwd: opts.cwd,
3164
3422
  stdio: ["pipe", "pipe", "inherit"],
3165
3423
  shell: true
3166
3424
  });
3167
3425
  } else {
3168
- this.child = spawn(opts.command, opts.args ?? [], {
3426
+ this.child = spawn2(opts.command, opts.args ?? [], {
3169
3427
  env,
3170
3428
  cwd: opts.cwd,
3171
3429
  stdio: ["pipe", "pipe", "inherit"]
@@ -3184,12 +3442,12 @@ var StdioTransport = class {
3184
3442
  }
3185
3443
  async send(message) {
3186
3444
  if (this.closed) throw new Error("MCP transport is closed");
3187
- return new Promise((resolve4, reject) => {
3445
+ return new Promise((resolve5, reject) => {
3188
3446
  const line = `${JSON.stringify(message)}
3189
3447
  `;
3190
3448
  this.child.stdin.write(line, "utf8", (err) => {
3191
3449
  if (err) reject(err);
3192
- else resolve4();
3450
+ else resolve5();
3193
3451
  });
3194
3452
  });
3195
3453
  }
@@ -3200,8 +3458,8 @@ var StdioTransport = class {
3200
3458
  continue;
3201
3459
  }
3202
3460
  if (this.closed) return;
3203
- const next = await new Promise((resolve4) => {
3204
- this.waiters.push(resolve4);
3461
+ const next = await new Promise((resolve5) => {
3462
+ this.waiters.push(resolve5);
3205
3463
  });
3206
3464
  if (next === null) return;
3207
3465
  yield next;
@@ -3267,8 +3525,8 @@ var SseTransport = class {
3267
3525
  constructor(opts) {
3268
3526
  this.url = opts.url;
3269
3527
  this.headers = opts.headers ?? {};
3270
- this.endpointReady = new Promise((resolve4, reject) => {
3271
- this.resolveEndpoint = resolve4;
3528
+ this.endpointReady = new Promise((resolve5, reject) => {
3529
+ this.resolveEndpoint = resolve5;
3272
3530
  this.rejectEndpoint = reject;
3273
3531
  });
3274
3532
  this.endpointReady.catch(() => void 0);
@@ -3295,8 +3553,8 @@ var SseTransport = class {
3295
3553
  continue;
3296
3554
  }
3297
3555
  if (this.closed) return;
3298
- const next = await new Promise((resolve4) => {
3299
- this.waiters.push(resolve4);
3556
+ const next = await new Promise((resolve5) => {
3557
+ this.waiters.push(resolve5);
3300
3558
  });
3301
3559
  if (next === null) return;
3302
3560
  yield next;
@@ -3495,8 +3753,8 @@ async function trySection(load) {
3495
3753
  }
3496
3754
 
3497
3755
  // src/code/edit-blocks.ts
3498
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
3499
- import { dirname as dirname3, resolve as resolve3 } from "path";
3756
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
3757
+ import { dirname as dirname3, resolve as resolve4 } from "path";
3500
3758
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
3501
3759
  function parseEditBlocks(text) {
3502
3760
  const out = [];
@@ -3514,8 +3772,8 @@ function parseEditBlocks(text) {
3514
3772
  return out;
3515
3773
  }
3516
3774
  function applyEditBlock(block, rootDir) {
3517
- const absRoot = resolve3(rootDir);
3518
- const absTarget = resolve3(absRoot, block.path);
3775
+ const absRoot = resolve4(rootDir);
3776
+ const absTarget = resolve4(absRoot, block.path);
3519
3777
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
3520
3778
  return {
3521
3779
  path: block.path,
@@ -3524,7 +3782,7 @@ function applyEditBlock(block, rootDir) {
3524
3782
  };
3525
3783
  }
3526
3784
  const searchEmpty = block.search.length === 0;
3527
- const exists = existsSync2(absTarget);
3785
+ const exists = existsSync3(absTarget);
3528
3786
  try {
3529
3787
  if (!exists) {
3530
3788
  if (!searchEmpty) {
@@ -3538,7 +3796,7 @@ function applyEditBlock(block, rootDir) {
3538
3796
  writeFileSync2(absTarget, block.replace, "utf8");
3539
3797
  return { path: block.path, status: "created" };
3540
3798
  }
3541
- const content = readFileSync4(absTarget, "utf8");
3799
+ const content = readFileSync5(absTarget, "utf8");
3542
3800
  if (searchEmpty) {
3543
3801
  return {
3544
3802
  path: block.path,
@@ -3565,19 +3823,19 @@ function applyEditBlocks(blocks, rootDir) {
3565
3823
  return blocks.map((b) => applyEditBlock(b, rootDir));
3566
3824
  }
3567
3825
  function snapshotBeforeEdits(blocks, rootDir) {
3568
- const absRoot = resolve3(rootDir);
3826
+ const absRoot = resolve4(rootDir);
3569
3827
  const seen = /* @__PURE__ */ new Set();
3570
3828
  const snapshots = [];
3571
3829
  for (const b of blocks) {
3572
3830
  if (seen.has(b.path)) continue;
3573
3831
  seen.add(b.path);
3574
- const abs = resolve3(absRoot, b.path);
3575
- if (!existsSync2(abs)) {
3832
+ const abs = resolve4(absRoot, b.path);
3833
+ if (!existsSync3(abs)) {
3576
3834
  snapshots.push({ path: b.path, prevContent: null });
3577
3835
  continue;
3578
3836
  }
3579
3837
  try {
3580
- snapshots.push({ path: b.path, prevContent: readFileSync4(abs, "utf8") });
3838
+ snapshots.push({ path: b.path, prevContent: readFileSync5(abs, "utf8") });
3581
3839
  } catch {
3582
3840
  snapshots.push({ path: b.path, prevContent: null });
3583
3841
  }
@@ -3585,9 +3843,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
3585
3843
  return snapshots;
3586
3844
  }
3587
3845
  function restoreSnapshots(snapshots, rootDir) {
3588
- const absRoot = resolve3(rootDir);
3846
+ const absRoot = resolve4(rootDir);
3589
3847
  return snapshots.map((snap) => {
3590
- const abs = resolve3(absRoot, snap.path);
3848
+ const abs = resolve4(absRoot, snap.path);
3591
3849
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
3592
3850
  return {
3593
3851
  path: snap.path,
@@ -3597,7 +3855,7 @@ function restoreSnapshots(snapshots, rootDir) {
3597
3855
  }
3598
3856
  try {
3599
3857
  if (snap.prevContent === null) {
3600
- if (existsSync2(abs)) unlinkSync2(abs);
3858
+ if (existsSync3(abs)) unlinkSync2(abs);
3601
3859
  return {
3602
3860
  path: snap.path,
3603
3861
  status: "applied",
@@ -3620,8 +3878,8 @@ function sep() {
3620
3878
  }
3621
3879
 
3622
3880
  // src/code/prompt.ts
3623
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
3624
- import { join as join3 } from "path";
3881
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3882
+ import { join as join4 } from "path";
3625
3883
  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.
3626
3884
 
3627
3885
  # When to edit vs. when to explore
@@ -3670,18 +3928,19 @@ Rules:
3670
3928
  - If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
3671
3929
  `;
3672
3930
  function codeSystemPrompt(rootDir) {
3673
- const gitignorePath = join3(rootDir, ".gitignore");
3674
- if (!existsSync3(gitignorePath)) return CODE_SYSTEM_PROMPT;
3931
+ const withMemory = applyProjectMemory(CODE_SYSTEM_PROMPT, rootDir);
3932
+ const gitignorePath = join4(rootDir, ".gitignore");
3933
+ if (!existsSync4(gitignorePath)) return withMemory;
3675
3934
  let content;
3676
3935
  try {
3677
- content = readFileSync5(gitignorePath, "utf8");
3936
+ content = readFileSync6(gitignorePath, "utf8");
3678
3937
  } catch {
3679
- return CODE_SYSTEM_PROMPT;
3938
+ return withMemory;
3680
3939
  }
3681
3940
  const MAX = 2e3;
3682
3941
  const truncated = content.length > MAX ? `${content.slice(0, MAX)}
3683
3942
  \u2026 (truncated ${content.length - MAX} chars)` : content;
3684
- return `${CODE_SYSTEM_PROMPT}
3943
+ return `${withMemory}
3685
3944
 
3686
3945
  # Project .gitignore
3687
3946
 
@@ -3694,15 +3953,15 @@ ${truncated}
3694
3953
  }
3695
3954
 
3696
3955
  // src/config.ts
3697
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
3956
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
3698
3957
  import { homedir as homedir2 } from "os";
3699
- import { dirname as dirname4, join as join4 } from "path";
3958
+ import { dirname as dirname4, join as join5 } from "path";
3700
3959
  function defaultConfigPath() {
3701
- return join4(homedir2(), ".reasonix", "config.json");
3960
+ return join5(homedir2(), ".reasonix", "config.json");
3702
3961
  }
3703
3962
  function readConfig(path = defaultConfigPath()) {
3704
3963
  try {
3705
- const raw = readFileSync6(path, "utf8");
3964
+ const raw = readFileSync7(path, "utf8");
3706
3965
  const parsed = JSON.parse(raw);
3707
3966
  if (parsed && typeof parsed === "object") return parsed;
3708
3967
  } catch {
@@ -3737,7 +3996,7 @@ function redactKey(key) {
3737
3996
  }
3738
3997
 
3739
3998
  // src/index.ts
3740
- var VERSION = "0.4.15";
3999
+ var VERSION = "0.4.17";
3741
4000
  export {
3742
4001
  AppendOnlyLog,
3743
4002
  CODE_SYSTEM_PROMPT,
@@ -3747,6 +4006,9 @@ export {
3747
4006
  ImmutablePrefix,
3748
4007
  MCP_PROTOCOL_VERSION,
3749
4008
  McpClient,
4009
+ NeedsConfirmationError,
4010
+ PROJECT_MEMORY_FILE,
4011
+ PROJECT_MEMORY_MAX_CHARS,
3750
4012
  SessionStats,
3751
4013
  SseTransport,
3752
4014
  StdioTransport,
@@ -3761,6 +4023,7 @@ export {
3761
4023
  appendSessionMessage,
3762
4024
  applyEditBlock,
3763
4025
  applyEditBlocks,
4026
+ applyProjectMemory,
3764
4027
  bridgeMcpTools,
3765
4028
  claudeEquivalentCost,
3766
4029
  codeSystemPrompt,
@@ -3774,6 +4037,7 @@ export {
3774
4037
  fetchWithRetry,
3775
4038
  flattenMcpResult,
3776
4039
  flattenSchema,
4040
+ formatCommandResult,
3777
4041
  formatLoopError,
3778
4042
  formatSearchResults,
3779
4043
  harvest,
@@ -3781,6 +4045,7 @@ export {
3781
4045
  htmlToText,
3782
4046
  inputCostUsd,
3783
4047
  inspectMcpServer,
4048
+ isAllowed,
3784
4049
  isJsonRpcError,
3785
4050
  isPlanStateEmpty,
3786
4051
  isPlausibleKey,
@@ -3788,6 +4053,7 @@ export {
3788
4053
  loadApiKey,
3789
4054
  loadDotenv,
3790
4055
  loadSessionMessages,
4056
+ memoryEnabled,
3791
4057
  nestArguments,
3792
4058
  openTranscriptFile,
3793
4059
  outputCostUsd,
@@ -3796,10 +4062,12 @@ export {
3796
4062
  parseMojeekResults,
3797
4063
  parseTranscript,
3798
4064
  readConfig,
4065
+ readProjectMemory,
3799
4066
  readTranscript,
3800
4067
  recordFromLoopEvent,
3801
4068
  redactKey,
3802
4069
  registerFilesystemTools,
4070
+ registerShellTools,
3803
4071
  registerWebTools,
3804
4072
  renderMarkdown as renderDiffMarkdown,
3805
4073
  renderSummaryTable as renderDiffSummary,
@@ -3807,6 +4075,7 @@ export {
3807
4075
  replayFromFile,
3808
4076
  restoreSnapshots,
3809
4077
  runBranches,
4078
+ runCommand,
3810
4079
  sanitizeName as sanitizeSessionName,
3811
4080
  saveApiKey,
3812
4081
  scavengeToolCalls,
@@ -3815,6 +4084,7 @@ export {
3815
4084
  similarity,
3816
4085
  snapshotBeforeEdits,
3817
4086
  stripHallucinatedToolMarkup,
4087
+ tokenizeCommand,
3818
4088
  truncateForModel,
3819
4089
  webFetch,
3820
4090
  webSearch,