@cruxy/cli 0.17.0 → 0.18.0

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.
@@ -40,6 +40,9 @@ export interface RunAgentArgs {
40
40
  } | null;
41
41
  /** Project instructions (e.g. from CRUXY.md) folded into the system prompt. */
42
42
  projectInstructions?: string | null;
43
+ /** Persistent memory (C.29): the pre-rendered recall block for the system
44
+ * prompt, or null. Reference data only — never influences the U.3 gate. */
45
+ recalledMemory?: string | null;
43
46
  /** Plan mode's propose phase (C.31): inject the plan-first system directive. */
44
47
  planMode?: boolean;
45
48
  /** Subagent runs (C.14): inject the bounded-subtask system directive. */
@@ -56,6 +56,7 @@ async function driveLoop(args, renderer, routed) {
56
56
  .map((tool) => ({ name: tool.name, description: tool.description })),
57
57
  git: args.git ?? null,
58
58
  projectInstructions: args.projectInstructions ?? null,
59
+ recalledMemory: args.recalledMemory ?? null,
59
60
  planMode: args.planMode ?? false,
60
61
  subagent: args.subagent ?? false,
61
62
  });
@@ -28,6 +28,13 @@ export interface PromptContext {
28
28
  } | null;
29
29
  /** Optional extra instructions (e.g. from a project CRUXY.md). */
30
30
  projectInstructions?: string | null;
31
+ /**
32
+ * Persistent memory (C.29): the pre-rendered, demarcated recall block injected
33
+ * at session start. It is REFERENCE DATA, not instructions — the block carries
34
+ * its own data-only framing (see memory/recall.ts) and is appended as an
35
+ * ordinary section; the U.3 gate never reads it. Null when nothing is recalled.
36
+ */
37
+ recalledMemory?: string | null;
31
38
  /** Plan mode's propose phase (C.31): inject the plan-first directive. */
32
39
  planMode?: boolean;
33
40
  /** Subagent run (C.14): inject the bounded-subtask directive. */
@@ -91,6 +91,12 @@ export function buildSystemPrompt(ctx) {
91
91
  if (ctx.projectInstructions?.trim()) {
92
92
  sections.push(`## Project instructions\nThe following came from this project's configuration; honor it unless it conflicts with the rules above:\n\n${ctx.projectInstructions.trim()}`);
93
93
  }
94
+ // Persistent memory (C.29). Appended last, as reference DATA — the block is
95
+ // pre-rendered with its own un-spoofable data-only demarcation, so it is added
96
+ // verbatim (never re-wrapped as an instruction). Absent when nothing recalled.
97
+ if (ctx.recalledMemory?.trim()) {
98
+ sections.push(ctx.recalledMemory.trim());
99
+ }
94
100
  return sections.join("\n\n");
95
101
  }
96
102
  /**
@@ -14,6 +14,7 @@ import { type AgentResult, type LifecycleHookRunner } from "./loop.js";
14
14
  export type PlanRunner = (args: {
15
15
  messages: Message[];
16
16
  projectInstructions: string | null;
17
+ recalledMemory: string | null;
17
18
  renderer?: StreamRenderer;
18
19
  }) => Promise<AgentResult>;
19
20
  export interface SessionArgs {
@@ -32,6 +33,13 @@ export interface SessionArgs {
32
33
  } | null;
33
34
  /** Project instructions (e.g. CRUXY.md) folded into every turn's system prompt. */
34
35
  projectInstructions?: string | null;
36
+ /**
37
+ * Persistent memory (C.29): the pre-rendered recall block, injected into every
38
+ * turn's system prompt as reference data. Built once at session start (user
39
+ * memory + trusted project memory); null when memory is off or empty. Fixed
40
+ * for the session — it does not re-read mid-session.
41
+ */
42
+ recalledMemory?: string | null;
35
43
  /** Start in plan mode (C.31). Toggleable at runtime via `setPlanMode`. */
36
44
  planMode?: boolean;
37
45
  /** The plan-mode turn runner; required for plan mode to actually engage. */
@@ -99,6 +99,7 @@ export class Session {
99
99
  ? await this.args.planRunner({
100
100
  messages: this.messages,
101
101
  projectInstructions: this.projectInstructions,
102
+ recalledMemory: this.args.recalledMemory ?? null,
102
103
  renderer,
103
104
  })
104
105
  : await runAgent({
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * `cruxy memory` — inspect and control persistent memory (C.29). `list` shows
4
+ * user + project entries and whether project memory is trusted (recalled);
5
+ * `forget`/`clear` remove entries; `trust` records the explicit decision to
6
+ * recall a repo's project memory, bound to its current fingerprint.
7
+ */
8
+ export declare function memoryCommand(): Command;
@@ -0,0 +1,98 @@
1
+ import path from "node:path";
2
+ import { Command } from "commander";
3
+ import { loadConfig } from "../../config/index.js";
4
+ import { shouldUseColor } from "../../errors/index.js";
5
+ import { themeForColor } from "../../theme/index.js";
6
+ import { MemoryService } from "../../memory/index.js";
7
+ import { logger } from "../../utils/logger.js";
8
+ /**
9
+ * `cruxy memory` — inspect and control persistent memory (C.29). `list` shows
10
+ * user + project entries and whether project memory is trusted (recalled);
11
+ * `forget`/`clear` remove entries; `trust` records the explicit decision to
12
+ * recall a repo's project memory, bound to its current fingerprint.
13
+ */
14
+ export function memoryCommand() {
15
+ const cmd = new Command("memory").description("inspect and control persistent memory recalled across sessions");
16
+ const service = () => {
17
+ const { config } = loadConfig();
18
+ return new MemoryService({ cwd: process.cwd(), config: config.memory });
19
+ };
20
+ cmd
21
+ .command("list", { isDefault: true })
22
+ .description("list saved memory entries and project-memory trust status")
23
+ .action(() => {
24
+ const t = themeForColor(shouldUseColor(process.stdout));
25
+ const { config } = loadConfig();
26
+ const status = service().status();
27
+ logger.print(`${t.strong("memory:")} ${config.memory.enabled ? t.success("enabled") : t.warning("disabled (memory.enabled = false)")}`);
28
+ logger.print(`\n${t.heading("user memory")} ${t.muted("(trusted)")}`);
29
+ printEntries(status.user, t);
30
+ const trustLabel = status.projectTrusted
31
+ ? t.success("trusted — recalled")
32
+ : t.danger("NOT trusted — not recalled; run `cruxy memory trust .`");
33
+ logger.print(`\n${t.heading("project memory")} ${trustLabel}`);
34
+ printEntries(status.project, t);
35
+ if (status.errors.length > 0) {
36
+ logger.print(`\n${t.danger(t.heading("excluded (invalid or secret — never recalled):"))}`);
37
+ for (const e of status.errors) {
38
+ logger.print(` ${t.muted(`[${e.scope}]`)} ${t.strong(e.id)} — ${e.message}`);
39
+ }
40
+ }
41
+ });
42
+ cmd
43
+ .command("forget <id>")
44
+ .description("forget one saved memory entry by id")
45
+ .action((id) => {
46
+ const t = themeForColor(shouldUseColor(process.stdout));
47
+ const removed = service().forget(id);
48
+ logger.print(removed
49
+ ? `${t.success("forgotten")} — entry ${t.strong(id)} removed`
50
+ : t.warning(`no memory entry with id ${id}`));
51
+ });
52
+ cmd
53
+ .command("clear")
54
+ .description("clear saved memory entries in a scope")
55
+ .option("--scope <scope>", "which scope to clear: user | project | all", "all")
56
+ .action((opts) => {
57
+ const t = themeForColor(shouldUseColor(process.stdout));
58
+ const scope = opts.scope;
59
+ if (scope !== "user" && scope !== "project" && scope !== "all") {
60
+ logger.print(t.danger(`invalid --scope ${scope} (use user | project | all)`));
61
+ process.exitCode = 2;
62
+ return;
63
+ }
64
+ const removed = service().clear(scope);
65
+ const where = scope === "all" ? "across all scopes" : `from ${scope} memory`;
66
+ logger.print(`${t.success("cleared")} — removed ${removed} ` +
67
+ `entr${removed === 1 ? "y" : "ies"} ${where}`);
68
+ });
69
+ cmd
70
+ .command("trust [path]")
71
+ .description("trust this repo's project memory after reviewing it (records the decision)")
72
+ .action((target) => {
73
+ const t = themeForColor(shouldUseColor(process.stdout));
74
+ const { config } = loadConfig();
75
+ const root = path.resolve(target ?? process.cwd());
76
+ const svc = new MemoryService({ cwd: root, config: config.memory });
77
+ const status = svc.status();
78
+ if (status.project.length === 0) {
79
+ logger.print(t.muted(`no project memory found under ${root}`));
80
+ return;
81
+ }
82
+ logger.print(t.strong(`trusting ${status.project.length} project entr${status.project.length === 1 ? "y" : "ies"}:`));
83
+ printEntries(status.project, t);
84
+ const count = svc.trustProject();
85
+ logger.print(`${t.success("trusted")} — ${count} project entr${count === 1 ? "y" : "ies"} will now be recalled for ${root}. ` +
86
+ t.muted("changing them will require re-trusting."));
87
+ });
88
+ return cmd;
89
+ }
90
+ function printEntries(entries, t) {
91
+ if (entries.length === 0) {
92
+ logger.print(t.muted(" (none)"));
93
+ return;
94
+ }
95
+ for (const e of entries) {
96
+ logger.print(` ${t.muted(e.id.slice(0, 8))} ${t.strong(`(${e.kind})`)} ${e.content}`);
97
+ }
98
+ }
@@ -15,6 +15,7 @@ import { checkpointCommand } from "./commands/checkpoint.js";
15
15
  import { rollbackCommand } from "./commands/rollback.js";
16
16
  import { testCommand } from "./commands/test.js";
17
17
  import { hooksCommand } from "./commands/hooks.js";
18
+ import { memoryCommand } from "./commands/memory.js";
18
19
  import { loadConfig } from "../config/index.js";
19
20
  import { maybeRunOnboarding } from "./onboard.js";
20
21
  export function buildProgram() {
@@ -45,6 +46,7 @@ export function buildProgram() {
45
46
  program.addCommand(rollbackCommand());
46
47
  program.addCommand(testCommand());
47
48
  program.addCommand(hooksCommand());
49
+ program.addCommand(memoryCommand());
48
50
  // Default action: bare `cruxy` -> entrypoint. An unrecognized first operand
49
51
  // means an unknown command (Commander runs the default action with it as an
50
52
  // operand rather than erroring), so reject it as a usage error.
@@ -8,6 +8,7 @@ import { buildDefaultRegistry } from "../tools/index.js";
8
8
  import { Session, } from "../agent/index.js";
9
9
  import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
10
10
  import { routerForConfig } from "../routing/index.js";
11
+ import { MemoryService, rememberTool } from "../memory/index.js";
11
12
  import { SubagentOrchestrator, makeSpawnSubagentTool, } from "../subagent/index.js";
12
13
  /**
13
14
  * Wrap a PromptIO so the live region yields before any prompt text lands
@@ -104,6 +105,23 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
104
105
  const execRegistry = buildDefaultRegistry();
105
106
  const git = getGitInfo(cwd);
106
107
  const projectInstructions = loadProjectInstructions(cwd);
108
+ // Persistent memory (C.29): register the write tool and build the recall block
109
+ // ONCE at session start. User memory is always eligible; project memory is
110
+ // recalled only when trusted (a cloned repo's notes never inject silently).
111
+ // Fully inert when disabled. Recall is best-effort — a load problem degrades to
112
+ // "no memory", never a hard failure at session start.
113
+ let recalledMemory = null;
114
+ if (config.memory.enabled) {
115
+ execRegistry.register(rememberTool);
116
+ const recall = new MemoryService({ cwd, config: config.memory }).recall();
117
+ recalledMemory = recall.block;
118
+ if (recall.projectPresentButUntrusted) {
119
+ logger.info("project memory found but not trusted — run `cruxy memory trust .` to recall it");
120
+ }
121
+ for (const e of recall.errors) {
122
+ logger.warn(`memory: excluded ${e.scope} entry ${e.id} — ${e.message}`);
123
+ }
124
+ }
107
125
  // One io shared by every prompt in the session (plan approval, the U.3 gate,
108
126
  // and any gate inside a subagent), so they all coordinate with the same live
109
127
  // region. The full wrapper stack around an ApprovalService is factored here
@@ -150,7 +168,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
150
168
  requestApproval: gate(approval),
151
169
  sandbox,
152
170
  };
153
- const planRunner = ({ messages, projectInstructions, renderer: turnRenderer, }) => runPlanSession({
171
+ const planRunner = ({ messages, projectInstructions, recalledMemory: turnMemory, renderer: turnRenderer, }) => runPlanSession({
154
172
  provider,
155
173
  config,
156
174
  ctx,
@@ -161,6 +179,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
161
179
  messages,
162
180
  git,
163
181
  projectInstructions,
182
+ recalledMemory: turnMemory,
164
183
  renderer: turnRenderer,
165
184
  router,
166
185
  });
@@ -171,6 +190,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
171
190
  ctx,
172
191
  git,
173
192
  projectInstructions,
193
+ recalledMemory,
174
194
  planMode: true,
175
195
  planRunner,
176
196
  hooks,
@@ -190,6 +210,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
190
210
  ctx,
191
211
  git,
192
212
  projectInstructions,
213
+ recalledMemory,
193
214
  hooks,
194
215
  router,
195
216
  });
@@ -182,7 +182,7 @@ export declare const IndexConfigSchema: z.ZodObject<{
182
182
  };
183
183
  enabled: boolean;
184
184
  embedder: "fastembed";
185
- store: "auto" | "sqlite" | "memory";
185
+ store: "memory" | "auto" | "sqlite";
186
186
  maxFileBytes: number;
187
187
  chunk: {
188
188
  windowLines: number;
@@ -196,7 +196,7 @@ export declare const IndexConfigSchema: z.ZodObject<{
196
196
  } | undefined;
197
197
  enabled?: boolean | undefined;
198
198
  embedder?: "fastembed" | undefined;
199
- store?: "auto" | "sqlite" | "memory" | undefined;
199
+ store?: "memory" | "auto" | "sqlite" | undefined;
200
200
  maxFileBytes?: number | undefined;
201
201
  chunk?: {
202
202
  windowLines?: number | undefined;
@@ -320,18 +320,18 @@ export declare const SandboxConfigSchema: z.ZodObject<{
320
320
  */
321
321
  mounts: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
322
322
  }, "strict", z.ZodTypeAny, {
323
+ memory: string;
323
324
  image: string;
324
325
  enabled: boolean;
325
- memory: string;
326
326
  network: "none" | "host-loopback" | "full";
327
327
  pids: number;
328
328
  cpus: number;
329
329
  mounts: string[];
330
330
  timeout?: number | undefined;
331
331
  }, {
332
+ memory?: string | undefined;
332
333
  image?: string | undefined;
333
334
  enabled?: boolean | undefined;
334
- memory?: string | undefined;
335
335
  network?: "none" | "host-loopback" | "full" | undefined;
336
336
  pids?: number | undefined;
337
337
  cpus?: number | undefined;
@@ -386,6 +386,28 @@ export declare const RoutingConfigSchema: z.ZodObject<{
386
386
  map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
387
387
  default?: "kavi" | "vaani" | "mira" | undefined;
388
388
  }>;
389
+ /**
390
+ * Persistent memory (C.29): structured notes recalled across sessions. ON by
391
+ * default and safe to be so — user memory is self-authored (nothing you didn't
392
+ * write is injected) and project memory still requires an explicit
393
+ * `cruxy memory trust`, so a cloned repo never auto-injects. Recall is bounded
394
+ * by a token budget; the `remember` tool refuses to persist secrets.
395
+ */
396
+ export declare const MemoryConfigSchema: z.ZodObject<{
397
+ /** Master switch. When false, nothing is recalled and the `remember` tool
398
+ * is not registered (memory stays fully inert). */
399
+ enabled: z.ZodDefault<z.ZodBoolean>;
400
+ /** Token budget for the recalled block injected at session start; entries
401
+ * beyond it are pruned oldest-first (the omission is stated, never silent). */
402
+ maxRecallTokens: z.ZodDefault<z.ZodNumber>;
403
+ }, "strict", z.ZodTypeAny, {
404
+ enabled: boolean;
405
+ maxRecallTokens: number;
406
+ }, {
407
+ enabled?: boolean | undefined;
408
+ maxRecallTokens?: number | undefined;
409
+ }>;
410
+ export type MemoryConfig = z.infer<typeof MemoryConfigSchema>;
389
411
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
390
412
  export declare const McpServerSchema: z.ZodObject<{
391
413
  command: z.ZodOptional<z.ZodString>;
@@ -567,7 +589,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
567
589
  };
568
590
  enabled: boolean;
569
591
  embedder: "fastembed";
570
- store: "auto" | "sqlite" | "memory";
592
+ store: "memory" | "auto" | "sqlite";
571
593
  maxFileBytes: number;
572
594
  chunk: {
573
595
  windowLines: number;
@@ -581,7 +603,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
581
603
  } | undefined;
582
604
  enabled?: boolean | undefined;
583
605
  embedder?: "fastembed" | undefined;
584
- store?: "auto" | "sqlite" | "memory" | undefined;
606
+ store?: "memory" | "auto" | "sqlite" | undefined;
585
607
  maxFileBytes?: number | undefined;
586
608
  chunk?: {
587
609
  windowLines?: number | undefined;
@@ -683,18 +705,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
683
705
  */
684
706
  mounts: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
685
707
  }, "strict", z.ZodTypeAny, {
708
+ memory: string;
686
709
  image: string;
687
710
  enabled: boolean;
688
- memory: string;
689
711
  network: "none" | "host-loopback" | "full";
690
712
  pids: number;
691
713
  cpus: number;
692
714
  mounts: string[];
693
715
  timeout?: number | undefined;
694
716
  }, {
717
+ memory?: string | undefined;
695
718
  image?: string | undefined;
696
719
  enabled?: boolean | undefined;
697
- memory?: string | undefined;
698
720
  network?: "none" | "host-loopback" | "full" | undefined;
699
721
  pids?: number | undefined;
700
722
  cpus?: number | undefined;
@@ -731,6 +753,20 @@ export declare const CruxyConfigSchema: z.ZodObject<{
731
753
  map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
732
754
  default?: "kavi" | "vaani" | "mira" | undefined;
733
755
  }>>;
756
+ memory: z.ZodDefault<z.ZodObject<{
757
+ /** Master switch. When false, nothing is recalled and the `remember` tool
758
+ * is not registered (memory stays fully inert). */
759
+ enabled: z.ZodDefault<z.ZodBoolean>;
760
+ /** Token budget for the recalled block injected at session start; entries
761
+ * beyond it are pruned oldest-first (the omission is stated, never silent). */
762
+ maxRecallTokens: z.ZodDefault<z.ZodNumber>;
763
+ }, "strict", z.ZodTypeAny, {
764
+ enabled: boolean;
765
+ maxRecallTokens: number;
766
+ }, {
767
+ enabled?: boolean | undefined;
768
+ maxRecallTokens?: number | undefined;
769
+ }>>;
734
770
  mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
735
771
  command: z.ZodOptional<z.ZodString>;
736
772
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -749,14 +785,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
749
785
  cruxy: {
750
786
  gatewayUrl: string;
751
787
  };
788
+ memory: {
789
+ enabled: boolean;
790
+ maxRecallTokens: number;
791
+ };
752
792
  checkpoint: {
753
793
  enabled: boolean;
754
794
  retention: number;
755
795
  };
756
796
  sandbox: {
797
+ memory: string;
757
798
  image: string;
758
799
  enabled: boolean;
759
- memory: string;
760
800
  network: "none" | "host-loopback" | "full";
761
801
  pids: number;
762
802
  cpus: number;
@@ -812,7 +852,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
812
852
  };
813
853
  enabled: boolean;
814
854
  embedder: "fastembed";
815
- store: "auto" | "sqlite" | "memory";
855
+ store: "memory" | "auto" | "sqlite";
816
856
  maxFileBytes: number;
817
857
  chunk: {
818
858
  windowLines: number;
@@ -842,14 +882,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
842
882
  cruxy?: {
843
883
  gatewayUrl?: string | undefined;
844
884
  } | undefined;
885
+ memory?: {
886
+ enabled?: boolean | undefined;
887
+ maxRecallTokens?: number | undefined;
888
+ } | undefined;
845
889
  checkpoint?: {
846
890
  enabled?: boolean | undefined;
847
891
  retention?: number | undefined;
848
892
  } | undefined;
849
893
  sandbox?: {
894
+ memory?: string | undefined;
850
895
  image?: string | undefined;
851
896
  enabled?: boolean | undefined;
852
- memory?: string | undefined;
853
897
  network?: "none" | "host-loopback" | "full" | undefined;
854
898
  pids?: number | undefined;
855
899
  cpus?: number | undefined;
@@ -905,7 +949,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
905
949
  } | undefined;
906
950
  enabled?: boolean | undefined;
907
951
  embedder?: "fastembed" | undefined;
908
- store?: "auto" | "sqlite" | "memory" | undefined;
952
+ store?: "memory" | "auto" | "sqlite" | undefined;
909
953
  maxFileBytes?: number | undefined;
910
954
  chunk?: {
911
955
  windowLines?: number | undefined;
@@ -273,6 +273,23 @@ export const RoutingConfigSchema = z
273
273
  map: z.record(z.enum(TASK_CLASSES), z.enum(MODEL_TIERS)).default({}),
274
274
  })
275
275
  .strict();
276
+ /**
277
+ * Persistent memory (C.29): structured notes recalled across sessions. ON by
278
+ * default and safe to be so — user memory is self-authored (nothing you didn't
279
+ * write is injected) and project memory still requires an explicit
280
+ * `cruxy memory trust`, so a cloned repo never auto-injects. Recall is bounded
281
+ * by a token budget; the `remember` tool refuses to persist secrets.
282
+ */
283
+ export const MemoryConfigSchema = z
284
+ .object({
285
+ /** Master switch. When false, nothing is recalled and the `remember` tool
286
+ * is not registered (memory stays fully inert). */
287
+ enabled: z.boolean().default(true),
288
+ /** Token budget for the recalled block injected at session start; entries
289
+ * beyond it are pruned oldest-first (the omission is stated, never silent). */
290
+ maxRecallTokens: z.number().int().positive().default(1000),
291
+ })
292
+ .strict();
276
293
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
277
294
  export const McpServerSchema = z
278
295
  .object({
@@ -298,6 +315,7 @@ export const CruxyConfigSchema = z
298
315
  sandbox: SandboxConfigSchema.default({}),
299
316
  hooks: HooksConfigSchema.default({}),
300
317
  routing: RoutingConfigSchema.default({}),
318
+ memory: MemoryConfigSchema.default({}),
301
319
  mcpServers: z.record(z.string(), McpServerSchema).default({}),
302
320
  logLevel: z.enum(LOG_LEVELS).default("info"),
303
321
  })
@@ -30,6 +30,17 @@ export declare const COMMANDS_DIR_NAME = "commands";
30
30
  /** Per-repo hook-trust record, in the GLOBAL dir only (`~/.cruxy/trust.json`) —
31
31
  * never in a repo, so cloning carries no trust (C.19 supply-chain safety). */
32
32
  export declare const TRUST_FILE_NAME = "trust.json";
33
+ /**
34
+ * Persistent memory (C.29). Structured entries live under the memory subdir of
35
+ * the project dir (`<cwd>/.cruxy/memory`) and the global dir (`~/.cruxy/memory`),
36
+ * one JSON file per scope — pure data, validated, never eval'd.
37
+ */
38
+ export declare const MEMORY_DIR_NAME = "memory";
39
+ export declare const MEMORY_FILE_NAME = "entries.json";
40
+ /** Per-repo PROJECT-memory trust record, in the GLOBAL dir only
41
+ * (`~/.cruxy/memory-trust.json`) — its own file, independent of hook trust, so
42
+ * cloning a repo carries zero memory trust (C.29 supply-chain safety). */
43
+ export declare const MEMORY_TRUST_FILE_NAME = "memory-trust.json";
33
44
  /**
34
45
  * Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
35
46
  * Anchored the same way as the package.json lookup above: both `dist/` and
package/dist/constants.js CHANGED
@@ -50,6 +50,17 @@ export const COMMANDS_DIR_NAME = "commands";
50
50
  /** Per-repo hook-trust record, in the GLOBAL dir only (`~/.cruxy/trust.json`) —
51
51
  * never in a repo, so cloning carries no trust (C.19 supply-chain safety). */
52
52
  export const TRUST_FILE_NAME = "trust.json";
53
+ /**
54
+ * Persistent memory (C.29). Structured entries live under the memory subdir of
55
+ * the project dir (`<cwd>/.cruxy/memory`) and the global dir (`~/.cruxy/memory`),
56
+ * one JSON file per scope — pure data, validated, never eval'd.
57
+ */
58
+ export const MEMORY_DIR_NAME = "memory";
59
+ export const MEMORY_FILE_NAME = "entries.json";
60
+ /** Per-repo PROJECT-memory trust record, in the GLOBAL dir only
61
+ * (`~/.cruxy/memory-trust.json`) — its own file, independent of hook trust, so
62
+ * cloning a repo carries zero memory trust (C.29 supply-chain safety). */
63
+ export const MEMORY_TRUST_FILE_NAME = "memory-trust.json";
53
64
  /**
54
65
  * Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
55
66
  * Anchored the same way as the package.json lookup above: both `dist/` and
@@ -136,6 +136,20 @@ export declare function subagentFailed(underlying?: unknown): CruxyError;
136
136
  * invents a test command — the fix is always to declare one.
137
137
  */
138
138
  export declare function testCommandNotFound(): CruxyError;
139
+ /**
140
+ * A memory write was refused because the content matched a known secret shape
141
+ * (C.29). Secrets are NEVER persisted to memory — the fix is to remember a
142
+ * non-secret description, not the secret itself.
143
+ */
144
+ export declare function memorySecretRefused(kind: string): CruxyError;
145
+ /**
146
+ * A repo's project memory is present but has not been trusted on this machine
147
+ * (C.29). It is never recalled into the model's context silently — a cloned repo
148
+ * cannot inject notes until you review and trust them.
149
+ */
150
+ export declare function memoryUntrusted(root: string): CruxyError;
151
+ /** A malformed memory entry was rejected (C.29) — excluded, never eval'd. */
152
+ export declare function memoryInvalid(detail: string): CruxyError;
139
153
  export declare function internal(underlying?: unknown): CruxyError;
140
154
  /**
141
155
  * Map a known provider/transport error (from `@cruxy/sdk`) to a typed
@@ -602,6 +602,51 @@ export function testCommandNotFound() {
602
602
  ],
603
603
  });
604
604
  }
605
+ // ── persistent memory (exit 14) — C.29 ────────────────────────────────────────
606
+ /**
607
+ * A memory write was refused because the content matched a known secret shape
608
+ * (C.29). Secrets are NEVER persisted to memory — the fix is to remember a
609
+ * non-secret description, not the secret itself.
610
+ */
611
+ export function memorySecretRefused(kind) {
612
+ return new CruxyError({
613
+ code: ErrorCode.MemorySecret,
614
+ title: "refused to save that note — it looks like it contains a secret",
615
+ cause: `the content matched a ${kind}; secrets are never written to memory`,
616
+ nextSteps: [
617
+ "remember a non-secret description instead (e.g. “the deploy key lives in 1Password”, not the key)",
618
+ ],
619
+ meta: { kind },
620
+ });
621
+ }
622
+ /**
623
+ * A repo's project memory is present but has not been trusted on this machine
624
+ * (C.29). It is never recalled into the model's context silently — a cloned repo
625
+ * cannot inject notes until you review and trust them.
626
+ */
627
+ export function memoryUntrusted(root) {
628
+ return new CruxyError({
629
+ code: ErrorCode.MemoryUntrusted,
630
+ title: "this project's memory has not been trusted",
631
+ cause: "project memory can arrive with a cloned repo (another author), so it is not recalled until you trust it",
632
+ nextSteps: [
633
+ "review the entries with `cruxy memory list`",
634
+ "then trust them with `cruxy memory trust .` (re-trust is required if they change)",
635
+ ],
636
+ meta: { root },
637
+ });
638
+ }
639
+ /** A malformed memory entry was rejected (C.29) — excluded, never eval'd. */
640
+ export function memoryInvalid(detail) {
641
+ return new CruxyError({
642
+ code: ErrorCode.MemoryInvalid,
643
+ title: "a memory entry is malformed and was excluded",
644
+ cause: detail,
645
+ nextSteps: [
646
+ "inspect the file with `cruxy memory list`, or clear it with `cruxy memory clear`",
647
+ ],
648
+ });
649
+ }
605
650
  // ── internal (exit 1) ─────────────────────────────────────────────────────────
606
651
  export function internal(underlying) {
607
652
  return new CruxyError({
@@ -71,6 +71,15 @@ export declare const ErrorCode: {
71
71
  readonly HookInvalid: "CRUXY_E_HOOK_INVALID";
72
72
  /** A malformed custom slash-command definition — excluded and surfaced. */
73
73
  readonly SlashInvalid: "CRUXY_E_SLASH_INVALID";
74
+ /** A malformed memory entry — excluded from recall/writes and surfaced,
75
+ * never eval'd. */
76
+ readonly MemoryInvalid: "CRUXY_E_MEMORY_INVALID";
77
+ /** A repo's project memory has not been trusted — it is never recalled into
78
+ * context silently (supply-chain safety). */
79
+ readonly MemoryUntrusted: "CRUXY_E_MEMORY_UNTRUSTED";
80
+ /** A memory write was refused because the content matched a secret shape —
81
+ * secrets are never persisted (defense in depth over C.17). */
82
+ readonly MemorySecret: "CRUXY_E_MEMORY_SECRET";
74
83
  };
75
84
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
76
85
  /** The process exit code for an error code (defaults to 1 for safety). */
@@ -85,6 +85,16 @@ export const ErrorCode = {
85
85
  HookInvalid: "CRUXY_E_HOOK_INVALID",
86
86
  /** A malformed custom slash-command definition — excluded and surfaced. */
87
87
  SlashInvalid: "CRUXY_E_SLASH_INVALID",
88
+ // persistent memory (exit 14) — C.29
89
+ /** A malformed memory entry — excluded from recall/writes and surfaced,
90
+ * never eval'd. */
91
+ MemoryInvalid: "CRUXY_E_MEMORY_INVALID",
92
+ /** A repo's project memory has not been trusted — it is never recalled into
93
+ * context silently (supply-chain safety). */
94
+ MemoryUntrusted: "CRUXY_E_MEMORY_UNTRUSTED",
95
+ /** A memory write was refused because the content matched a secret shape —
96
+ * secrets are never persisted (defense in depth over C.17). */
97
+ MemorySecret: "CRUXY_E_MEMORY_SECRET",
88
98
  };
89
99
  /**
90
100
  * Category exit codes. Distinct per category so a caller (CI, a script) can
@@ -146,6 +156,12 @@ const EXIT_CODES = {
146
156
  [ErrorCode.HookUntrusted]: 13,
147
157
  [ErrorCode.HookInvalid]: 13,
148
158
  [ErrorCode.SlashInvalid]: 13,
159
+ // Persistent memory (C.29). A malformed entry and an untrusted project are
160
+ // data-safety problems; a refused secret write is a security stop — grouped
161
+ // for a greppable exit code.
162
+ [ErrorCode.MemoryInvalid]: 14,
163
+ [ErrorCode.MemoryUntrusted]: 14,
164
+ [ErrorCode.MemorySecret]: 14,
149
165
  };
150
166
  /** The process exit code for an error code (defaults to 1 for safety). */
151
167
  export function exitCodeFor(code) {
@@ -103,8 +103,8 @@ export declare const SlashFrontmatterSchema: z.ZodObject<{
103
103
  command?: string | undefined;
104
104
  }, {
105
105
  description: string;
106
- command?: string | undefined;
107
106
  kind?: "shell" | "prompt" | undefined;
107
+ command?: string | undefined;
108
108
  }>;
109
109
  export type SlashFrontmatter = z.infer<typeof SlashFrontmatterSchema>;
110
110
  /** A validated custom slash command, tagged with source. */