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/cli/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ MemoryStore,
3
4
  PROJECT_MEMORY_FILE,
4
- applyProjectMemory,
5
+ SkillStore,
6
+ applyMemoryStack,
5
7
  memoryEnabled,
6
- readProjectMemory
7
- } from "./chunk-HNEWBEWZ.js";
8
+ readProjectMemory,
9
+ sanitizeMemoryName
10
+ } from "./chunk-K6MR4SWS.js";
8
11
 
9
12
  // src/cli/index.ts
10
13
  import { Command } from "commander";
@@ -2206,7 +2209,7 @@ function registerFilesystemTools(registry, opts) {
2206
2209
  });
2207
2210
  registry.register({
2208
2211
  name: "edit_file",
2209
- 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.",
2212
+ 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.",
2210
2213
  parameters: {
2211
2214
  type: "object",
2212
2215
  properties: {
@@ -2323,6 +2326,127 @@ function lineDiff(a, b) {
2323
2326
  return out;
2324
2327
  }
2325
2328
 
2329
+ // src/tools/memory.ts
2330
+ function registerMemoryTools(registry, opts = {}) {
2331
+ const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
2332
+ const hasProject = store.hasProjectScope();
2333
+ registry.register({
2334
+ name: "remember",
2335
+ 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.",
2336
+ parameters: {
2337
+ type: "object",
2338
+ properties: {
2339
+ type: {
2340
+ type: "string",
2341
+ enum: ["user", "feedback", "project", "reference"],
2342
+ 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."
2343
+ },
2344
+ scope: {
2345
+ type: "string",
2346
+ enum: ["global", "project"],
2347
+ description: "'global' = applies across every project (preferences, tooling); 'project' = scoped to the current sandbox (decisions, local facts). Only available in `reasonix code`."
2348
+ },
2349
+ name: {
2350
+ type: "string",
2351
+ description: "filename-safe identifier, 3-40 chars, alnum + _ - . (no path separators, no leading dot)."
2352
+ },
2353
+ description: {
2354
+ type: "string",
2355
+ description: "One-line summary shown in MEMORY.md (under ~150 chars)."
2356
+ },
2357
+ content: {
2358
+ type: "string",
2359
+ description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
2360
+ }
2361
+ },
2362
+ required: ["type", "scope", "name", "description", "content"]
2363
+ },
2364
+ fn: async (args) => {
2365
+ if (args.scope === "project" && !hasProject) {
2366
+ return JSON.stringify({
2367
+ 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."
2368
+ });
2369
+ }
2370
+ try {
2371
+ const path = store.write({
2372
+ name: args.name,
2373
+ type: args.type,
2374
+ scope: args.scope,
2375
+ description: args.description,
2376
+ body: args.content
2377
+ });
2378
+ const key = sanitizeMemoryName(args.name);
2379
+ return [
2380
+ `\u2713 REMEMBERED (${args.scope}/${key}): ${args.description}`,
2381
+ "",
2382
+ "TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
2383
+ "The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
2384
+ `(Saved to ${path}; pins into the system prompt on next /new or launch.)`
2385
+ ].join("\n");
2386
+ } catch (err) {
2387
+ return JSON.stringify({ error: `remember failed: ${err.message}` });
2388
+ }
2389
+ }
2390
+ });
2391
+ registry.register({
2392
+ name: "forget",
2393
+ 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.",
2394
+ parameters: {
2395
+ type: "object",
2396
+ properties: {
2397
+ name: { type: "string", description: "Memory name (the identifier used in `remember`)." },
2398
+ scope: { type: "string", enum: ["global", "project"] }
2399
+ },
2400
+ required: ["name", "scope"]
2401
+ },
2402
+ fn: async (args) => {
2403
+ if (args.scope === "project" && !hasProject) {
2404
+ return JSON.stringify({
2405
+ error: "scope='project' is unavailable in this session (no sandbox root)."
2406
+ });
2407
+ }
2408
+ try {
2409
+ const existed = store.delete(args.scope, args.name);
2410
+ 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).`;
2411
+ } catch (err) {
2412
+ return JSON.stringify({ error: `forget failed: ${err.message}` });
2413
+ }
2414
+ }
2415
+ });
2416
+ registry.register({
2417
+ name: "recall_memory",
2418
+ 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.",
2419
+ readOnly: true,
2420
+ parameters: {
2421
+ type: "object",
2422
+ properties: {
2423
+ name: { type: "string" },
2424
+ scope: { type: "string", enum: ["global", "project"] }
2425
+ },
2426
+ required: ["name", "scope"]
2427
+ },
2428
+ fn: async (args) => {
2429
+ if (args.scope === "project" && !hasProject) {
2430
+ return JSON.stringify({
2431
+ error: "scope='project' is unavailable in this session (no sandbox root)."
2432
+ });
2433
+ }
2434
+ try {
2435
+ const entry = store.read(args.scope, args.name);
2436
+ return [
2437
+ `# ${entry.name} (${entry.scope}/${entry.type}, created ${entry.createdAt || "?"})`,
2438
+ entry.description ? `> ${entry.description}` : "",
2439
+ "",
2440
+ entry.body
2441
+ ].filter(Boolean).join("\n");
2442
+ } catch (err) {
2443
+ return JSON.stringify({ error: `recall failed: ${err.message}` });
2444
+ }
2445
+ }
2446
+ });
2447
+ return registry;
2448
+ }
2449
+
2326
2450
  // src/tools/plan.ts
2327
2451
  var PlanProposedError = class extends Error {
2328
2452
  plan;
@@ -2540,7 +2664,7 @@ function resolveExecutable(cmd, opts = {}) {
2540
2664
  const isFile = opts.isFile ?? defaultIsFile;
2541
2665
  for (const dir of pathDirs) {
2542
2666
  for (const ext of pathExt) {
2543
- const full = pathMod2.join(dir, cmd + ext);
2667
+ const full = pathMod2.win32.join(dir, cmd + ext);
2544
2668
  if (isFile(full)) return full;
2545
2669
  }
2546
2670
  }
@@ -2573,8 +2697,23 @@ function prepareSpawn(argv, opts = {}) {
2573
2697
  spawnOverrides: { windowsVerbatimArguments: true }
2574
2698
  };
2575
2699
  }
2700
+ if (isBareWindowsName(resolved) && resolved === head) {
2701
+ const cmdline = [head, ...tail].map(quoteForCmdExe).join(" ");
2702
+ return {
2703
+ bin: "cmd.exe",
2704
+ args: ["/d", "/s", "/c", cmdline],
2705
+ spawnOverrides: { windowsVerbatimArguments: true }
2706
+ };
2707
+ }
2576
2708
  return { bin: resolved, args: [...tail], spawnOverrides: {} };
2577
2709
  }
2710
+ function isBareWindowsName(s) {
2711
+ if (!s) return false;
2712
+ if (s.includes("/") || s.includes("\\")) return false;
2713
+ if (pathMod2.isAbsolute(s)) return false;
2714
+ if (pathMod2.extname(s)) return false;
2715
+ return true;
2716
+ }
2578
2717
  function quoteForCmdExe(arg) {
2579
2718
  if (arg === "") return '""';
2580
2719
  if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
@@ -2594,11 +2733,14 @@ function registerShellTools(registry, opts) {
2594
2733
  const rootDir = pathMod2.resolve(opts.rootDir);
2595
2734
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
2596
2735
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
2597
- const extraAllowed = opts.extraAllowed ?? [];
2736
+ const getExtraAllowed = typeof opts.extraAllowed === "function" ? opts.extraAllowed : (() => {
2737
+ const snapshot = opts.extraAllowed ?? [];
2738
+ return () => snapshot;
2739
+ })();
2598
2740
  const allowAll = opts.allowAll ?? false;
2599
2741
  registry.register({
2600
2742
  name: "run_command",
2601
- 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.",
2743
+ 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.",
2602
2744
  // Plan-mode gate: allow allowlisted commands through (git status,
2603
2745
  // cargo check, ls, grep …) so the model can actually investigate
2604
2746
  // during planning. Anything that would otherwise trigger a
@@ -2607,14 +2749,14 @@ function registerShellTools(registry, opts) {
2607
2749
  if (allowAll) return true;
2608
2750
  const cmd = typeof args?.command === "string" ? args.command.trim() : "";
2609
2751
  if (!cmd) return false;
2610
- return isAllowed(cmd, extraAllowed);
2752
+ return isAllowed(cmd, getExtraAllowed());
2611
2753
  },
2612
2754
  parameters: {
2613
2755
  type: "object",
2614
2756
  properties: {
2615
2757
  command: {
2616
2758
  type: "string",
2617
- 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."
2759
+ description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
2618
2760
  },
2619
2761
  timeoutSec: {
2620
2762
  type: "integer",
@@ -2626,7 +2768,7 @@ function registerShellTools(registry, opts) {
2626
2768
  fn: async (args, ctx) => {
2627
2769
  const cmd = args.command.trim();
2628
2770
  if (!cmd) throw new Error("run_command: empty command");
2629
- if (!allowAll && !isAllowed(cmd, extraAllowed)) {
2771
+ if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
2630
2772
  throw new NeedsConfirmationError(cmd);
2631
2773
  }
2632
2774
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
@@ -4119,13 +4261,64 @@ function sep() {
4119
4261
  }
4120
4262
 
4121
4263
  // src/index.ts
4122
- var VERSION = "0.4.19";
4264
+ var VERSION = "0.4.20";
4123
4265
 
4124
4266
  // src/cli/commands/chat.tsx
4125
4267
  import { existsSync as existsSync4, statSync as statSync3 } from "fs";
4126
4268
  import { render } from "ink";
4127
4269
  import React15, { useState as useState7 } from "react";
4128
4270
 
4271
+ // src/tools/skills.ts
4272
+ function registerSkillTools(registry, opts = {}) {
4273
+ const store = new SkillStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
4274
+ registry.register({
4275
+ name: "run_skill",
4276
+ description: "Load the full body of a user-defined skill into this conversation. Call when the pinned Skills index (in the system prompt) lists a skill whose description matches what's being asked. Returns the skill's markdown instructions \u2014 read them and continue the loop, calling whatever filesystem / shell / web tools the skill's prose requires. Skills are user content; follow their instructions, but keep Reasonix's own safety rules (no destructive ops without confirmation, etc.).",
4277
+ readOnly: true,
4278
+ parameters: {
4279
+ type: "object",
4280
+ properties: {
4281
+ name: {
4282
+ type: "string",
4283
+ description: "Skill identifier as it appears in the pinned Skills index (e.g. 'review', 'security-review'). Case-sensitive."
4284
+ },
4285
+ arguments: {
4286
+ type: "string",
4287
+ description: "Optional free-form arguments the caller wants the skill to act on. Forwarded verbatim as an 'Arguments:' line appended to the skill body; the skill's own instructions decide how to consume them."
4288
+ }
4289
+ },
4290
+ required: ["name"]
4291
+ },
4292
+ fn: async (args) => {
4293
+ const name = typeof args.name === "string" ? args.name.trim() : "";
4294
+ if (!name) {
4295
+ return JSON.stringify({ error: "run_skill requires a 'name' argument" });
4296
+ }
4297
+ const skill = store.read(name);
4298
+ if (!skill) {
4299
+ const available = store.list().map((s) => s.name).join(", ");
4300
+ return JSON.stringify({
4301
+ error: `unknown skill: ${JSON.stringify(name)}`,
4302
+ available: available || "(none \u2014 user has not defined any skills)"
4303
+ });
4304
+ }
4305
+ const rawArgs = typeof args.arguments === "string" ? args.arguments.trim() : "";
4306
+ const header = [
4307
+ `# Skill: ${skill.name}`,
4308
+ skill.description ? `> ${skill.description}` : "",
4309
+ `(scope: ${skill.scope} \xB7 ${skill.path})`
4310
+ ].filter(Boolean).join("\n");
4311
+ const argsBlock = rawArgs ? `
4312
+
4313
+ Arguments: ${rawArgs}` : "";
4314
+ return `${header}
4315
+
4316
+ ${skill.body}${argsBlock}`;
4317
+ }
4318
+ });
4319
+ return registry;
4320
+ }
4321
+
4129
4322
  // src/cli/ui/App.tsx
4130
4323
  import { Box as Box11, Static, Text as Text11, useApp, useInput as useInput4 } from "ink";
4131
4324
  import React12, { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState5 } from "react";
@@ -5093,7 +5286,16 @@ var SLASH_COMMANDS = [
5093
5286
  { cmd: "branch", argsHint: "<N|off>", summary: "run N parallel samples per turn (N>=2)" },
5094
5287
  { cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
5095
5288
  { cmd: "tool", argsHint: "[N]", summary: "dump full output of the Nth tool call (1=latest)" },
5096
- { cmd: "memory", summary: "show the project's REASONIX.md (pinned into the system prompt)" },
5289
+ {
5290
+ cmd: "memory",
5291
+ argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]",
5292
+ summary: "show / manage pinned memory (REASONIX.md + ~/.reasonix/memory)"
5293
+ },
5294
+ {
5295
+ cmd: "skill",
5296
+ argsHint: "[list|show <name>|<name> [args]]",
5297
+ summary: "list / run user skills (<project>/.reasonix/skills + ~/.reasonix/skills)"
5298
+ },
5097
5299
  { cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
5098
5300
  { cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
5099
5301
  { cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
@@ -5173,7 +5375,10 @@ function handleSlash(cmd, args, loop, ctx = {}) {
5173
5375
  " /compact [cap] shrink large tool results in history (default 4k/result)",
5174
5376
  " /think dump the most recent turn's full R1 reasoning (reasoner only)",
5175
5377
  " /tool [N] list tool calls (or dump full output of #N, 1=most recent)",
5176
- " /memory show the project's REASONIX.md (pinned into the system prompt)",
5378
+ " /memory [sub] show pinned memory (REASONIX.md + ~/.reasonix/memory).",
5379
+ " subs: list | show <name> | forget <name> | clear <scope> confirm",
5380
+ " /skill [sub] list / run user skills (project/.reasonix/skills + ~/.reasonix/skills).",
5381
+ " subs: list | show <name> | <name> [args] (injects skill body as user turn)",
5177
5382
  " /retry truncate & resend your last message (fresh sample from the model)",
5178
5383
  " /apply (code mode) commit the pending edit blocks to disk",
5179
5384
  " /discard (code mode) drop pending edits without writing",
@@ -5258,43 +5463,11 @@ function handleSlash(cmd, args, loop, ctx = {}) {
5258
5463
  };
5259
5464
  }
5260
5465
  case "memory": {
5261
- if (!memoryEnabled()) {
5262
- return {
5263
- info: "project memory is disabled (REASONIX_MEMORY=off in env). Unset the var to re-enable; no REASONIX.md will be pinned in the meantime."
5264
- };
5265
- }
5266
- if (!ctx.memoryRoot) {
5267
- return {
5268
- info: "no project root on this session \u2014 `/memory` needs a working directory to resolve REASONIX.md from."
5269
- };
5270
- }
5271
- const mem = readProjectMemory(ctx.memoryRoot);
5272
- if (!mem) {
5273
- return {
5274
- info: [
5275
- `no ${PROJECT_MEMORY_FILE} in ${ctx.memoryRoot}.`,
5276
- "",
5277
- "Project memory is an optional file you pin notes into \u2014 project conventions,",
5278
- "things the model keeps forgetting, domain glossary, setup gotchas. When present,",
5279
- "its contents are appended to the system prompt (the immutable-prefix region)",
5280
- "so every turn sees it without eating per-turn context, and the prefix cache stays",
5281
- "warm as long as the file is stable.",
5282
- "",
5283
- `Create it with: echo "# Project notes for Reasonix" > ${PROJECT_MEMORY_FILE}`,
5284
- "Re-launch (or `/new`) to pick up changes \u2014 the prefix is hashed at session start."
5285
- ].join("\n")
5286
- };
5287
- }
5288
- const header = mem.truncated ? `\u25B8 project memory: ${mem.path} (${mem.originalChars.toLocaleString()} chars, truncated for the prefix)` : `\u25B8 project memory: ${mem.path} (${mem.originalChars.toLocaleString()} chars)`;
5289
- return {
5290
- info: [
5291
- header,
5292
- "",
5293
- mem.content,
5294
- "",
5295
- "Changes take effect on the next launch or `/new` \u2014 the system prompt is hashed once per session to keep the prefix cache warm."
5296
- ].join("\n")
5297
- };
5466
+ return handleMemorySlash(args, ctx);
5467
+ }
5468
+ case "skill":
5469
+ case "skills": {
5470
+ return handleSkillSlash(args, ctx);
5298
5471
  }
5299
5472
  case "think":
5300
5473
  case "reasoning": {
@@ -5532,6 +5705,228 @@ ${entry.text}`
5532
5705
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
5533
5706
  }
5534
5707
  }
5708
+ function handleSkillSlash(args, ctx) {
5709
+ const store = new SkillStore({ projectRoot: ctx.codeRoot });
5710
+ const sub = (args[0] ?? "").toLowerCase();
5711
+ if (sub === "" || sub === "list" || sub === "ls") {
5712
+ const skills = store.list();
5713
+ if (skills.length === 0) {
5714
+ const lines2 = ["no skills found. Reasonix reads skills from:"];
5715
+ if (store.hasProjectScope()) {
5716
+ lines2.push(
5717
+ " \xB7 <project>/.reasonix/skills/<name>/SKILL.md (or <name>.md) \u2014 project scope"
5718
+ );
5719
+ }
5720
+ lines2.push(" \xB7 ~/.reasonix/skills/<name>/SKILL.md (or <name>.md) \u2014 global scope");
5721
+ if (!store.hasProjectScope()) {
5722
+ lines2.push(" (project scope is only active in `reasonix code`)");
5723
+ }
5724
+ lines2.push(
5725
+ "",
5726
+ "Each file's frontmatter needs at least `name` and `description`.",
5727
+ "Invoke a skill with `/skill <name> [args]` or by asking the model to call `run_skill`."
5728
+ );
5729
+ return { info: lines2.join("\n") };
5730
+ }
5731
+ const lines = [`User skills (${skills.length}):`];
5732
+ for (const s of skills) {
5733
+ const scope = `(${s.scope})`.padEnd(11);
5734
+ const name2 = s.name.padEnd(24);
5735
+ const desc = s.description.length > 70 ? `${s.description.slice(0, 69)}\u2026` : s.description;
5736
+ lines.push(` ${scope} ${name2} ${desc}`);
5737
+ }
5738
+ lines.push("");
5739
+ lines.push("View body: /skill show <name> Run: /skill <name> [args]");
5740
+ return { info: lines.join("\n") };
5741
+ }
5742
+ if (sub === "show" || sub === "cat") {
5743
+ const target = args[1];
5744
+ if (!target) return { info: "usage: /skill show <name>" };
5745
+ const skill2 = store.read(target);
5746
+ if (!skill2) return { info: `no skill found: ${target}` };
5747
+ return {
5748
+ info: [
5749
+ `\u25B8 ${skill2.name} (${skill2.scope})`,
5750
+ skill2.description ? ` ${skill2.description}` : "",
5751
+ ` ${skill2.path}`,
5752
+ "",
5753
+ skill2.body
5754
+ ].filter((l) => l !== "").join("\n")
5755
+ };
5756
+ }
5757
+ const name = args[0] ?? "";
5758
+ const skill = store.read(name);
5759
+ if (!skill) {
5760
+ return {
5761
+ info: `no skill found: ${name} (try /skill list)`
5762
+ };
5763
+ }
5764
+ const extra = args.slice(1).join(" ").trim();
5765
+ const header = `# Skill: ${skill.name}${skill.description ? `
5766
+ > ${skill.description}` : ""}`;
5767
+ const argsLine = extra ? `
5768
+
5769
+ Arguments: ${extra}` : "";
5770
+ const payload = `${header}
5771
+
5772
+ ${skill.body}${argsLine}`;
5773
+ return {
5774
+ info: `\u25B8 running skill: ${skill.name}${extra ? ` \u2014 ${extra}` : ""}`,
5775
+ resubmit: payload
5776
+ };
5777
+ }
5778
+ function handleMemorySlash(args, ctx) {
5779
+ if (!memoryEnabled()) {
5780
+ return {
5781
+ info: "memory is disabled (REASONIX_MEMORY=off in env). Unset the var to re-enable \u2014 no REASONIX.md or ~/.reasonix/memory content will be pinned in the meantime."
5782
+ };
5783
+ }
5784
+ if (!ctx.memoryRoot) {
5785
+ return {
5786
+ info: "no working directory on this session \u2014 `/memory` needs a root to resolve REASONIX.md from. (Running in a test harness?)"
5787
+ };
5788
+ }
5789
+ const store = new MemoryStore({ projectRoot: ctx.codeRoot });
5790
+ const sub = (args[0] ?? "").toLowerCase();
5791
+ if (sub === "list" || sub === "ls") {
5792
+ const entries = store.list();
5793
+ if (entries.length === 0) {
5794
+ return {
5795
+ info: "no user memories yet. The model can call `remember` to save one, or you can create files by hand in ~/.reasonix/memory/global/ or the per-project subdir."
5796
+ };
5797
+ }
5798
+ const lines = [`User memories (${entries.length}):`];
5799
+ for (const e of entries) {
5800
+ const tag = `${e.scope}/${e.type}`.padEnd(18);
5801
+ const name = e.name.padEnd(28);
5802
+ const desc = e.description.length > 70 ? `${e.description.slice(0, 69)}\u2026` : e.description;
5803
+ lines.push(` ${tag} ${name} ${desc}`);
5804
+ }
5805
+ lines.push("");
5806
+ lines.push("View body: /memory show <name> Delete: /memory forget <name>");
5807
+ return { info: lines.join("\n") };
5808
+ }
5809
+ if (sub === "show" || sub === "cat") {
5810
+ const target = args[1];
5811
+ if (!target) return { info: "usage: /memory show <name> or /memory show <scope>/<name>" };
5812
+ const resolved = resolveMemoryTarget(store, target);
5813
+ if (!resolved) return { info: `no memory found: ${target}` };
5814
+ try {
5815
+ const entry = store.read(resolved.scope, resolved.name);
5816
+ return {
5817
+ info: [
5818
+ `\u25B8 ${entry.scope}/${entry.name} (${entry.type}, created ${entry.createdAt || "?"})`,
5819
+ entry.description ? ` ${entry.description}` : "",
5820
+ "",
5821
+ entry.body
5822
+ ].filter((l) => l !== "").concat("").join("\n")
5823
+ };
5824
+ } catch (err) {
5825
+ return { info: `show failed: ${err.message}` };
5826
+ }
5827
+ }
5828
+ if (sub === "forget" || sub === "rm" || sub === "delete") {
5829
+ const target = args[1];
5830
+ if (!target) return { info: "usage: /memory forget <name> or /memory forget <scope>/<name>" };
5831
+ const resolved = resolveMemoryTarget(store, target);
5832
+ if (!resolved) return { info: `no memory found: ${target}` };
5833
+ try {
5834
+ const ok = store.delete(resolved.scope, resolved.name);
5835
+ return {
5836
+ info: ok ? `\u25B8 forgot ${resolved.scope}/${resolved.name}. Next /new or launch won't see it.` : `could not forget ${resolved.scope}/${resolved.name} (already gone?)`
5837
+ };
5838
+ } catch (err) {
5839
+ return { info: `forget failed: ${err.message}` };
5840
+ }
5841
+ }
5842
+ if (sub === "clear") {
5843
+ const rawScope = (args[1] ?? "").toLowerCase();
5844
+ if (rawScope !== "global" && rawScope !== "project") {
5845
+ return { info: "usage: /memory clear <global|project> confirm" };
5846
+ }
5847
+ if ((args[2] ?? "").toLowerCase() !== "confirm") {
5848
+ return {
5849
+ info: `about to delete every memory in scope=${rawScope}. Re-run with the word 'confirm' to proceed: /memory clear ${rawScope} confirm`
5850
+ };
5851
+ }
5852
+ const scope = rawScope;
5853
+ const entries = store.list().filter((e) => e.scope === scope);
5854
+ let deleted = 0;
5855
+ for (const e of entries) {
5856
+ try {
5857
+ if (store.delete(scope, e.name)) deleted++;
5858
+ } catch {
5859
+ }
5860
+ }
5861
+ return { info: `\u25B8 cleared scope=${scope} \u2014 deleted ${deleted} memory file(s).` };
5862
+ }
5863
+ const parts = [];
5864
+ const projMem = readProjectMemory(ctx.memoryRoot);
5865
+ if (projMem) {
5866
+ const hdr = projMem.truncated ? `\u25B8 ${PROJECT_MEMORY_FILE}: ${projMem.path} (${projMem.originalChars.toLocaleString()} chars, truncated)` : `\u25B8 ${PROJECT_MEMORY_FILE}: ${projMem.path} (${projMem.originalChars.toLocaleString()} chars)`;
5867
+ parts.push(hdr, "", projMem.content);
5868
+ }
5869
+ const globalIdx = store.loadIndex("global");
5870
+ if (globalIdx) {
5871
+ parts.push(
5872
+ "",
5873
+ `\u25B8 global memory (${globalIdx.originalChars.toLocaleString()} chars${globalIdx.truncated ? ", truncated" : ""})`,
5874
+ "",
5875
+ globalIdx.content
5876
+ );
5877
+ }
5878
+ const projectIdx = store.loadIndex("project");
5879
+ if (projectIdx) {
5880
+ parts.push(
5881
+ "",
5882
+ `\u25B8 project memory (${projectIdx.originalChars.toLocaleString()} chars${projectIdx.truncated ? ", truncated" : ""})`,
5883
+ "",
5884
+ projectIdx.content
5885
+ );
5886
+ }
5887
+ if (parts.length === 0) {
5888
+ return {
5889
+ info: [
5890
+ `no memory pinned in ${ctx.memoryRoot}.`,
5891
+ "",
5892
+ "Three layers are available:",
5893
+ ` 1. ${PROJECT_MEMORY_FILE} \u2014 committable team memory (in the repo).`,
5894
+ " 2. ~/.reasonix/memory/global/ \u2014 your cross-project private memory.",
5895
+ ` 3. ~/.reasonix/memory/<project-hash>/ \u2014 this project's private memory.`,
5896
+ "",
5897
+ "Ask the model to `remember` something, or hand-edit files directly.",
5898
+ "Changes take effect on next /new or launch \u2014 the system prompt is hashed once per session to keep the prefix cache warm.",
5899
+ "",
5900
+ "Subcommands: /memory list | /memory show <name> | /memory forget <name> | /memory clear <scope> confirm"
5901
+ ].join("\n")
5902
+ };
5903
+ }
5904
+ parts.push(
5905
+ "",
5906
+ "Changes take effect on next /new or launch. Subcommands: /memory list | show | forget | clear"
5907
+ );
5908
+ return { info: parts.join("\n") };
5909
+ }
5910
+ function resolveMemoryTarget(store, raw) {
5911
+ const slash = raw.indexOf("/");
5912
+ if (slash > 0) {
5913
+ const scopeRaw = raw.slice(0, slash).toLowerCase();
5914
+ const name = raw.slice(slash + 1);
5915
+ if (scopeRaw !== "global" && scopeRaw !== "project") return null;
5916
+ const scope = scopeRaw;
5917
+ if (scope === "project" && !store.hasProjectScope()) return null;
5918
+ return { scope, name };
5919
+ }
5920
+ for (const scope of ["project", "global"]) {
5921
+ if (scope === "project" && !store.hasProjectScope()) continue;
5922
+ try {
5923
+ store.read(scope, raw);
5924
+ return { scope, name: raw };
5925
+ } catch {
5926
+ }
5927
+ }
5928
+ return null;
5929
+ }
5535
5930
  function appendSection(lines, label, section) {
5536
5931
  if (!section || !section.supported) {
5537
5932
  lines.push(
@@ -6611,6 +7006,11 @@ async function chatCommand(opts) {
6611
7006
  if (!tools) tools = new ToolRegistry();
6612
7007
  registerWebTools(tools);
6613
7008
  }
7009
+ if (!opts.seedTools) {
7010
+ if (!tools) tools = new ToolRegistry();
7011
+ registerMemoryTools(tools, {});
7012
+ registerSkillTools(tools);
7013
+ }
6614
7014
  let sessionPreview;
6615
7015
  if (opts.session && !opts.forceResume && !opts.forceNew) {
6616
7016
  const prior = loadSessionMessages(opts.session);
@@ -6649,7 +7049,7 @@ async function chatCommand(opts) {
6649
7049
  // src/cli/commands/code.tsx
6650
7050
  import { basename, resolve as resolve5 } from "path";
6651
7051
  async function codeCommand(opts = {}) {
6652
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-JNNNJLYF.js");
7052
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-VDN5U3YE.js");
6653
7053
  const rootDir = resolve5(opts.dir ?? process.cwd());
6654
7054
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
6655
7055
  const tools = new ToolRegistry();
@@ -6658,9 +7058,14 @@ async function codeCommand(opts = {}) {
6658
7058
  rootDir,
6659
7059
  // Per-project "always allow" list persisted from prior ShellConfirm
6660
7060
  // choices; merged on top of the built-in allowlist in shell.ts.
6661
- extraAllowed: loadProjectShellAllowed(rootDir)
7061
+ // GETTER form — re-read every dispatch so a prefix the user adds
7062
+ // via ShellConfirm mid-session takes effect on the next shell call
7063
+ // instead of waiting for `/new` or a relaunch.
7064
+ extraAllowed: () => loadProjectShellAllowed(rootDir)
6662
7065
  });
6663
7066
  registerPlanTool(tools);
7067
+ registerMemoryTools(tools, { projectRoot: rootDir });
7068
+ registerSkillTools(tools, { projectRoot: rootDir });
6664
7069
  process.stderr.write(
6665
7070
  `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
6666
7071
  `
@@ -7710,7 +8115,7 @@ program.action(async () => {
7710
8115
  const defaults = resolveDefaults({});
7711
8116
  await chatCommand({
7712
8117
  model: defaults.model,
7713
- system: applyProjectMemory(DEFAULT_SYSTEM, process.cwd()),
8118
+ system: applyMemoryStack(DEFAULT_SYSTEM, process.cwd()),
7714
8119
  harvest: defaults.harvest,
7715
8120
  branch: defaults.branch,
7716
8121
  session: defaults.session,
@@ -7762,7 +8167,7 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
7762
8167
  });
7763
8168
  await chatCommand({
7764
8169
  model: defaults.model,
7765
- system: applyProjectMemory(opts.system, process.cwd()),
8170
+ system: applyMemoryStack(opts.system, process.cwd()),
7766
8171
  transcript: opts.transcript,
7767
8172
  harvest: defaults.harvest,
7768
8173
  branch: defaults.branch,
@@ -7797,7 +8202,7 @@ program.command("run <task>").description("Run a single task non-interactively,
7797
8202
  await runCommand2({
7798
8203
  task,
7799
8204
  model: defaults.model,
7800
- system: applyProjectMemory(opts.system, process.cwd()),
8205
+ system: applyMemoryStack(opts.system, process.cwd()),
7801
8206
  harvest: defaults.harvest,
7802
8207
  branch: defaults.branch,
7803
8208
  transcript: opts.transcript,