@cruxy/cli 0.16.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.
Files changed (58) hide show
  1. package/dist/agent/loop.d.ts +15 -0
  2. package/dist/agent/loop.js +13 -2
  3. package/dist/agent/prompts.d.ts +7 -0
  4. package/dist/agent/prompts.js +6 -0
  5. package/dist/agent/session.d.ts +14 -0
  6. package/dist/agent/session.js +10 -1
  7. package/dist/brand/index.d.ts +1 -1
  8. package/dist/brand/index.js +1 -1
  9. package/dist/brand/voice.d.ts +20 -0
  10. package/dist/brand/voice.js +54 -0
  11. package/dist/cli/commands/memory.d.ts +8 -0
  12. package/dist/cli/commands/memory.js +98 -0
  13. package/dist/cli/commands/pr.js +9 -1
  14. package/dist/cli/program.js +2 -0
  15. package/dist/cli/session-factory.js +31 -1
  16. package/dist/config/schema.d.ts +114 -28
  17. package/dist/config/schema.js +38 -0
  18. package/dist/constants.d.ts +11 -0
  19. package/dist/constants.js +11 -0
  20. package/dist/errors/constructors.d.ts +23 -0
  21. package/dist/errors/constructors.js +86 -6
  22. package/dist/errors/types.d.ts +12 -0
  23. package/dist/errors/types.js +20 -0
  24. package/dist/hooks/types.d.ts +1 -1
  25. package/dist/memory/index.d.ts +7 -0
  26. package/dist/memory/index.js +7 -0
  27. package/dist/memory/recall.d.ts +32 -0
  28. package/dist/memory/recall.js +73 -0
  29. package/dist/memory/remember-tool.d.ts +25 -0
  30. package/dist/memory/remember-tool.js +56 -0
  31. package/dist/memory/secrets.d.ts +29 -0
  32. package/dist/memory/secrets.js +61 -0
  33. package/dist/memory/service.d.ts +92 -0
  34. package/dist/memory/service.js +164 -0
  35. package/dist/memory/store.d.ts +32 -0
  36. package/dist/memory/store.js +100 -0
  37. package/dist/memory/trust.d.ts +52 -0
  38. package/dist/memory/trust.js +106 -0
  39. package/dist/memory/types.d.ts +101 -0
  40. package/dist/memory/types.js +58 -0
  41. package/dist/plan/service.d.ts +9 -0
  42. package/dist/plan/service.js +6 -0
  43. package/dist/render/state.js +4 -1
  44. package/dist/render/types.d.ts +7 -1
  45. package/dist/routing/index.d.ts +2 -0
  46. package/dist/routing/index.js +5 -0
  47. package/dist/routing/resolve.d.ts +17 -0
  48. package/dist/routing/resolve.js +18 -0
  49. package/dist/routing/router.d.ts +47 -0
  50. package/dist/routing/router.js +84 -0
  51. package/dist/routing/types.d.ts +42 -0
  52. package/dist/routing/types.js +27 -0
  53. package/dist/subagent/orchestrator.d.ts +6 -0
  54. package/dist/subagent/orchestrator.js +2 -0
  55. package/dist/subagent/types.d.ts +6 -0
  56. package/dist/vcs/generate.d.ts +3 -1
  57. package/dist/vcs/generate.js +4 -1
  58. package/package.json +2 -2
@@ -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;
@@ -365,6 +365,49 @@ export declare const HooksConfigSchema: z.ZodObject<{
365
365
  trustPrompt?: boolean | undefined;
366
366
  }>;
367
367
  export type HooksConfig = z.infer<typeof HooksConfigSchema>;
368
+ /**
369
+ * Multi-model routing (C.30): map declared task classes to tiers so mechanical
370
+ * work runs on a cheap tier and hard reasoning on a strong one. Fully opt-in —
371
+ * with an empty `map` and no `default`, every task class routes to the session's
372
+ * single tier and behavior is unchanged (routing stays inert until configured).
373
+ * Only tier names appear here; upstream model names never do (U.8). Keys are the
374
+ * fixed {@link TASK_CLASSES}, so a mistyped class is rejected at config load.
375
+ */
376
+ export declare const RoutingConfigSchema: z.ZodObject<{
377
+ /** Tier for any task class not in `map`. Unset → the tier implied by
378
+ * `model.model` (a real tier, or the auto-fallback tier). */
379
+ default: z.ZodOptional<z.ZodEnum<["kavi", "vaani", "mira"]>>;
380
+ /** Per-task-class tier overrides; anything omitted takes `default`. */
381
+ map: z.ZodDefault<z.ZodRecord<z.ZodEnum<["main-turn", "subagent", "plan", "commit-msg", "classify", "summarize"]>, z.ZodEnum<["kavi", "vaani", "mira"]>>>;
382
+ }, "strict", z.ZodTypeAny, {
383
+ map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
384
+ default?: "kavi" | "vaani" | "mira" | undefined;
385
+ }, {
386
+ map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
387
+ default?: "kavi" | "vaani" | "mira" | undefined;
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>;
368
411
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
369
412
  export declare const McpServerSchema: z.ZodObject<{
370
413
  command: z.ZodOptional<z.ZodString>;
@@ -546,7 +589,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
546
589
  };
547
590
  enabled: boolean;
548
591
  embedder: "fastembed";
549
- store: "auto" | "sqlite" | "memory";
592
+ store: "memory" | "auto" | "sqlite";
550
593
  maxFileBytes: number;
551
594
  chunk: {
552
595
  windowLines: number;
@@ -560,7 +603,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
560
603
  } | undefined;
561
604
  enabled?: boolean | undefined;
562
605
  embedder?: "fastembed" | undefined;
563
- store?: "auto" | "sqlite" | "memory" | undefined;
606
+ store?: "memory" | "auto" | "sqlite" | undefined;
564
607
  maxFileBytes?: number | undefined;
565
608
  chunk?: {
566
609
  windowLines?: number | undefined;
@@ -662,18 +705,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
662
705
  */
663
706
  mounts: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
664
707
  }, "strict", z.ZodTypeAny, {
708
+ memory: string;
665
709
  image: string;
666
710
  enabled: boolean;
667
- memory: string;
668
711
  network: "none" | "host-loopback" | "full";
669
712
  pids: number;
670
713
  cpus: number;
671
714
  mounts: string[];
672
715
  timeout?: number | undefined;
673
716
  }, {
717
+ memory?: string | undefined;
674
718
  image?: string | undefined;
675
719
  enabled?: boolean | undefined;
676
- memory?: string | undefined;
677
720
  network?: "none" | "host-loopback" | "full" | undefined;
678
721
  pids?: number | undefined;
679
722
  cpus?: number | undefined;
@@ -697,6 +740,33 @@ export declare const CruxyConfigSchema: z.ZodObject<{
697
740
  enabled?: boolean | undefined;
698
741
  trustPrompt?: boolean | undefined;
699
742
  }>>;
743
+ routing: z.ZodDefault<z.ZodObject<{
744
+ /** Tier for any task class not in `map`. Unset → the tier implied by
745
+ * `model.model` (a real tier, or the auto-fallback tier). */
746
+ default: z.ZodOptional<z.ZodEnum<["kavi", "vaani", "mira"]>>;
747
+ /** Per-task-class tier overrides; anything omitted takes `default`. */
748
+ map: z.ZodDefault<z.ZodRecord<z.ZodEnum<["main-turn", "subagent", "plan", "commit-msg", "classify", "summarize"]>, z.ZodEnum<["kavi", "vaani", "mira"]>>>;
749
+ }, "strict", z.ZodTypeAny, {
750
+ map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
751
+ default?: "kavi" | "vaani" | "mira" | undefined;
752
+ }, {
753
+ map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
754
+ default?: "kavi" | "vaani" | "mira" | undefined;
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
+ }>>;
700
770
  mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
701
771
  command: z.ZodOptional<z.ZodString>;
702
772
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -715,14 +785,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
715
785
  cruxy: {
716
786
  gatewayUrl: string;
717
787
  };
788
+ memory: {
789
+ enabled: boolean;
790
+ maxRecallTokens: number;
791
+ };
718
792
  checkpoint: {
719
793
  enabled: boolean;
720
794
  retention: number;
721
795
  };
722
796
  sandbox: {
797
+ memory: string;
723
798
  image: string;
724
799
  enabled: boolean;
725
- memory: string;
726
800
  network: "none" | "host-loopback" | "full";
727
801
  pids: number;
728
802
  cpus: number;
@@ -732,6 +806,14 @@ export declare const CruxyConfigSchema: z.ZodObject<{
732
806
  approval: {
733
807
  mode: "prompt";
734
808
  };
809
+ subagent: {
810
+ maxDepth: number;
811
+ defaultBudget: {
812
+ maxTokens: number;
813
+ maxIterations: number;
814
+ timeoutMs?: number | undefined;
815
+ };
816
+ };
735
817
  model: {
736
818
  provider: "cruxy" | "anthropic" | "openai" | "custom";
737
819
  model: string;
@@ -770,21 +852,13 @@ export declare const CruxyConfigSchema: z.ZodObject<{
770
852
  };
771
853
  enabled: boolean;
772
854
  embedder: "fastembed";
773
- store: "auto" | "sqlite" | "memory";
855
+ store: "memory" | "auto" | "sqlite";
774
856
  maxFileBytes: number;
775
857
  chunk: {
776
858
  windowLines: number;
777
859
  overlapLines: number;
778
860
  };
779
861
  };
780
- subagent: {
781
- maxDepth: number;
782
- defaultBudget: {
783
- maxTokens: number;
784
- maxIterations: number;
785
- timeoutMs?: number | undefined;
786
- };
787
- };
788
862
  test: {
789
863
  maxIterations: number;
790
864
  captureBytes: number;
@@ -794,6 +868,10 @@ export declare const CruxyConfigSchema: z.ZodObject<{
794
868
  enabled: boolean;
795
869
  trustPrompt: boolean;
796
870
  };
871
+ routing: {
872
+ map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
873
+ default?: "kavi" | "vaani" | "mira" | undefined;
874
+ };
797
875
  mcpServers: Record<string, {
798
876
  command?: string | undefined;
799
877
  args?: string[] | undefined;
@@ -804,14 +882,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
804
882
  cruxy?: {
805
883
  gatewayUrl?: string | undefined;
806
884
  } | undefined;
885
+ memory?: {
886
+ enabled?: boolean | undefined;
887
+ maxRecallTokens?: number | undefined;
888
+ } | undefined;
807
889
  checkpoint?: {
808
890
  enabled?: boolean | undefined;
809
891
  retention?: number | undefined;
810
892
  } | undefined;
811
893
  sandbox?: {
894
+ memory?: string | undefined;
812
895
  image?: string | undefined;
813
896
  enabled?: boolean | undefined;
814
- memory?: string | undefined;
815
897
  network?: "none" | "host-loopback" | "full" | undefined;
816
898
  pids?: number | undefined;
817
899
  cpus?: number | undefined;
@@ -821,6 +903,14 @@ export declare const CruxyConfigSchema: z.ZodObject<{
821
903
  approval?: {
822
904
  mode?: "prompt" | undefined;
823
905
  } | undefined;
906
+ subagent?: {
907
+ maxDepth?: number | undefined;
908
+ defaultBudget?: {
909
+ maxTokens?: number | undefined;
910
+ maxIterations?: number | undefined;
911
+ timeoutMs?: number | undefined;
912
+ } | undefined;
913
+ } | undefined;
824
914
  model?: {
825
915
  provider?: "cruxy" | "anthropic" | "openai" | "custom" | undefined;
826
916
  model?: string | undefined;
@@ -859,21 +949,13 @@ export declare const CruxyConfigSchema: z.ZodObject<{
859
949
  } | undefined;
860
950
  enabled?: boolean | undefined;
861
951
  embedder?: "fastembed" | undefined;
862
- store?: "auto" | "sqlite" | "memory" | undefined;
952
+ store?: "memory" | "auto" | "sqlite" | undefined;
863
953
  maxFileBytes?: number | undefined;
864
954
  chunk?: {
865
955
  windowLines?: number | undefined;
866
956
  overlapLines?: number | undefined;
867
957
  } | undefined;
868
958
  } | undefined;
869
- subagent?: {
870
- maxDepth?: number | undefined;
871
- defaultBudget?: {
872
- maxTokens?: number | undefined;
873
- maxIterations?: number | undefined;
874
- timeoutMs?: number | undefined;
875
- } | undefined;
876
- } | undefined;
877
959
  test?: {
878
960
  maxIterations?: number | undefined;
879
961
  command?: string | undefined;
@@ -883,6 +965,10 @@ export declare const CruxyConfigSchema: z.ZodObject<{
883
965
  enabled?: boolean | undefined;
884
966
  trustPrompt?: boolean | undefined;
885
967
  } | undefined;
968
+ routing?: {
969
+ map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
970
+ default?: "kavi" | "vaani" | "mira" | undefined;
971
+ } | undefined;
886
972
  mcpServers?: Record<string, {
887
973
  command?: string | undefined;
888
974
  args?: string[] | undefined;
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { LOG_LEVELS } from "../utils/logger.js";
3
+ import { MODEL_TIERS } from "../brand/voice.js";
4
+ import { TASK_CLASSES } from "../routing/types.js";
3
5
  export const ProviderSchema = z.enum([
4
6
  "cruxy",
5
7
  "anthropic",
@@ -254,6 +256,40 @@ export const HooksConfigSchema = z
254
256
  trustPrompt: z.boolean().default(true),
255
257
  })
256
258
  .strict();
259
+ /**
260
+ * Multi-model routing (C.30): map declared task classes to tiers so mechanical
261
+ * work runs on a cheap tier and hard reasoning on a strong one. Fully opt-in —
262
+ * with an empty `map` and no `default`, every task class routes to the session's
263
+ * single tier and behavior is unchanged (routing stays inert until configured).
264
+ * Only tier names appear here; upstream model names never do (U.8). Keys are the
265
+ * fixed {@link TASK_CLASSES}, so a mistyped class is rejected at config load.
266
+ */
267
+ export const RoutingConfigSchema = z
268
+ .object({
269
+ /** Tier for any task class not in `map`. Unset → the tier implied by
270
+ * `model.model` (a real tier, or the auto-fallback tier). */
271
+ default: z.enum(MODEL_TIERS).optional(),
272
+ /** Per-task-class tier overrides; anything omitted takes `default`. */
273
+ map: z.record(z.enum(TASK_CLASSES), z.enum(MODEL_TIERS)).default({}),
274
+ })
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();
257
293
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
258
294
  export const McpServerSchema = z
259
295
  .object({
@@ -278,6 +314,8 @@ export const CruxyConfigSchema = z
278
314
  test: TestConfigSchema.default({}),
279
315
  sandbox: SandboxConfigSchema.default({}),
280
316
  hooks: HooksConfigSchema.default({}),
317
+ routing: RoutingConfigSchema.default({}),
318
+ memory: MemoryConfigSchema.default({}),
281
319
  mcpServers: z.record(z.string(), McpServerSchema).default({}),
282
320
  logLevel: z.enum(LOG_LEVELS).default("info"),
283
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
@@ -11,6 +11,15 @@ export declare function usageError(title: string, nextSteps?: string[]): CruxyEr
11
11
  export declare function interactiveRequired(what: string, alternatives?: string[]): CruxyError;
12
12
  export declare function configKeyUnknown(key: string): CruxyError;
13
13
  export declare function providerUnsupported(provider: string): CruxyError;
14
+ /**
15
+ * A task class is routed (C.30) to a tier the gateway does not offer. A usage
16
+ * error the user fixes in config — cruxy fails loud here rather than silently
17
+ * substituting a different tier (which would hand a user a model they never
18
+ * asked for). Distinct from runtime unavailability (overload/budget), which
19
+ * stays on the U.5 api codes. Params are tier/class NAMES only — never an
20
+ * upstream model id — so the message is gag-safe by construction (U.8).
21
+ */
22
+ export declare function routingTierUnavailable(tier: string, taskClass: string, offered: string[]): CruxyError;
14
23
  export declare function configParse(path: string, underlying?: unknown): CruxyError;
15
24
  export declare function configInvalid(issues: string, path?: string): CruxyError;
16
25
  export declare function authMissingKey(provider: string, envVar: string): CruxyError;
@@ -127,6 +136,20 @@ export declare function subagentFailed(underlying?: unknown): CruxyError;
127
136
  * invents a test command — the fix is always to declare one.
128
137
  */
129
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;
130
153
  export declare function internal(underlying?: unknown): CruxyError;
131
154
  /**
132
155
  * Map a known provider/transport error (from `@cruxy/sdk`) to a typed
@@ -1,4 +1,5 @@
1
1
  import { ApiError, AuthError, NetworkError, OverloadedError, RateLimitError, } from "@cruxy/sdk";
2
+ import { scrubModelNames } from "../brand/index.js";
2
3
  import { CruxyError, ErrorCode } from "./types.js";
3
4
  /**
4
5
  * Helper constructors for {@link CruxyError}. Each encodes the title, the human
@@ -15,6 +16,18 @@ export function messageOf(underlying) {
15
16
  return undefined;
16
17
  return String(underlying);
17
18
  }
19
+ /**
20
+ * {@link messageOf}, gag-scrubbed (U.8) for a gateway/provider-originated error.
21
+ * A gateway error body is an arbitrary EXTERNAL string, so it bypasses the
22
+ * structural tier gag — this is the boundary that scrubs any upstream model name
23
+ * out of it before it can reach a user-facing `cause`. Used by the provider-error
24
+ * constructors below (everything `classifyProviderError` routes through). The raw
25
+ * message stays on `underlying`, shown verbatim only under `--verbose`.
26
+ */
27
+ function scrubbedMessageOf(underlying) {
28
+ const msg = messageOf(underlying);
29
+ return msg === undefined ? undefined : scrubModelNames(msg);
30
+ }
18
31
  // ── usage (exit 2) ────────────────────────────────────────────────────────────
19
32
  export function usageError(title, nextSteps) {
20
33
  return new CruxyError({
@@ -57,6 +70,28 @@ export function providerUnsupported(provider) {
57
70
  meta: { provider },
58
71
  });
59
72
  }
73
+ /**
74
+ * A task class is routed (C.30) to a tier the gateway does not offer. A usage
75
+ * error the user fixes in config — cruxy fails loud here rather than silently
76
+ * substituting a different tier (which would hand a user a model they never
77
+ * asked for). Distinct from runtime unavailability (overload/budget), which
78
+ * stays on the U.5 api codes. Params are tier/class NAMES only — never an
79
+ * upstream model id — so the message is gag-safe by construction (U.8).
80
+ */
81
+ export function routingTierUnavailable(tier, taskClass, offered) {
82
+ return new CruxyError({
83
+ code: ErrorCode.RoutingTierUnavailable,
84
+ title: `the "${tier}" tier is not available for the "${taskClass}" task`,
85
+ cause: `routing maps "${taskClass}" to the "${tier}" tier, which this gateway does not currently offer`,
86
+ nextSteps: [
87
+ `route it to an available tier, e.g. \`cruxy config set routing.map.${taskClass} ${offered[0] ?? "vaani"}\``,
88
+ offered.length
89
+ ? `available tiers: ${offered.join(", ")}`
90
+ : "no tiers are currently available",
91
+ ],
92
+ meta: { tier, taskClass, offered },
93
+ });
94
+ }
60
95
  // ── config (exit 3) ───────────────────────────────────────────────────────────
61
96
  export function configParse(path, underlying) {
62
97
  return new CruxyError({
@@ -99,7 +134,7 @@ export function authInvalid(underlying) {
99
134
  return new CruxyError({
100
135
  code: ErrorCode.AuthInvalid,
101
136
  title: "the provider rejected your credentials",
102
- cause: messageOf(underlying),
137
+ cause: scrubbedMessageOf(underlying),
103
138
  nextSteps: [
104
139
  "verify your API key is correct and active",
105
140
  "re-export the key and try again",
@@ -112,7 +147,7 @@ export function gatewayUnreachable(underlying) {
112
147
  return new CruxyError({
113
148
  code: ErrorCode.GatewayUnreachable,
114
149
  title: "could not reach the model gateway",
115
- cause: messageOf(underlying),
150
+ cause: scrubbedMessageOf(underlying),
116
151
  nextSteps: [
117
152
  "check your internet connection",
118
153
  "verify the gateway URL with `cruxy config get cruxy.gatewayUrl`",
@@ -129,7 +164,7 @@ export function apiError(underlying) {
129
164
  title: status
130
165
  ? `the model provider returned an error (HTTP ${status})`
131
166
  : "the model provider returned an error",
132
- cause: messageOf(underlying),
167
+ cause: scrubbedMessageOf(underlying),
133
168
  nextSteps: [
134
169
  "retry in a moment; if it persists, check the provider's status",
135
170
  ],
@@ -142,7 +177,7 @@ export function apiRateLimit(underlying) {
142
177
  return new CruxyError({
143
178
  code: ErrorCode.ApiRateLimit,
144
179
  title: "rate limited by the model provider",
145
- cause: messageOf(underlying),
180
+ cause: scrubbedMessageOf(underlying),
146
181
  nextSteps: [
147
182
  retryAfterMs
148
183
  ? `wait ~${Math.ceil(retryAfterMs / 1000)}s and retry`
@@ -156,7 +191,7 @@ export function apiOverloaded(underlying) {
156
191
  return new CruxyError({
157
192
  code: ErrorCode.ApiOverloaded,
158
193
  title: "the model provider is overloaded",
159
- cause: messageOf(underlying),
194
+ cause: scrubbedMessageOf(underlying),
160
195
  nextSteps: ["retry in a few moments"],
161
196
  underlying,
162
197
  });
@@ -165,7 +200,7 @@ export function budgetExhausted(underlying) {
165
200
  return new CruxyError({
166
201
  code: ErrorCode.BudgetExhausted,
167
202
  title: "your Cruxy budget is exhausted",
168
- cause: messageOf(underlying),
203
+ cause: scrubbedMessageOf(underlying),
169
204
  nextSteps: ["top up or raise your budget, then retry"],
170
205
  underlying,
171
206
  });
@@ -567,6 +602,51 @@ export function testCommandNotFound() {
567
602
  ],
568
603
  });
569
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
+ }
570
650
  // ── internal (exit 1) ─────────────────────────────────────────────────────────
571
651
  export function internal(underlying) {
572
652
  return new CruxyError({
@@ -21,6 +21,9 @@ export declare const ErrorCode: {
21
21
  readonly PlanInvalid: "CRUXY_E_PLAN_INVALID";
22
22
  readonly PlanRevisionLimit: "CRUXY_E_PLAN_REVISION_LIMIT";
23
23
  readonly CheckpointNotFound: "CRUXY_E_CHECKPOINT_NOT_FOUND";
24
+ /** Routing (C.30): a task class is mapped to a tier the gateway does not
25
+ * offer — fix the config, NEVER a silent substitution to another tier. */
26
+ readonly RoutingTierUnavailable: "CRUXY_E_ROUTING_TIER_UNAVAILABLE";
24
27
  readonly ConfigParse: "CRUXY_E_CONFIG_PARSE";
25
28
  readonly ConfigInvalid: "CRUXY_E_CONFIG_INVALID";
26
29
  readonly AuthMissingKey: "CRUXY_E_AUTH_MISSING_KEY";
@@ -68,6 +71,15 @@ export declare const ErrorCode: {
68
71
  readonly HookInvalid: "CRUXY_E_HOOK_INVALID";
69
72
  /** A malformed custom slash-command definition — excluded and surfaced. */
70
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";
71
83
  };
72
84
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
73
85
  /** The process exit code for an error code (defaults to 1 for safety). */
@@ -23,6 +23,9 @@ export const ErrorCode = {
23
23
  PlanInvalid: "CRUXY_E_PLAN_INVALID",
24
24
  PlanRevisionLimit: "CRUXY_E_PLAN_REVISION_LIMIT",
25
25
  CheckpointNotFound: "CRUXY_E_CHECKPOINT_NOT_FOUND",
26
+ /** Routing (C.30): a task class is mapped to a tier the gateway does not
27
+ * offer — fix the config, NEVER a silent substitution to another tier. */
28
+ RoutingTierUnavailable: "CRUXY_E_ROUTING_TIER_UNAVAILABLE",
26
29
  // config (exit 3)
27
30
  ConfigParse: "CRUXY_E_CONFIG_PARSE",
28
31
  ConfigInvalid: "CRUXY_E_CONFIG_INVALID",
@@ -82,6 +85,16 @@ export const ErrorCode = {
82
85
  HookInvalid: "CRUXY_E_HOOK_INVALID",
83
86
  /** A malformed custom slash-command definition — excluded and surfaced. */
84
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",
85
98
  };
86
99
  /**
87
100
  * Category exit codes. Distinct per category so a caller (CI, a script) can
@@ -97,6 +110,7 @@ const EXIT_CODES = {
97
110
  [ErrorCode.PlanInvalid]: 2,
98
111
  [ErrorCode.PlanRevisionLimit]: 2,
99
112
  [ErrorCode.CheckpointNotFound]: 2,
113
+ [ErrorCode.RoutingTierUnavailable]: 2,
100
114
  [ErrorCode.ConfigParse]: 3,
101
115
  [ErrorCode.ConfigInvalid]: 3,
102
116
  [ErrorCode.AuthMissingKey]: 4,
@@ -142,6 +156,12 @@ const EXIT_CODES = {
142
156
  [ErrorCode.HookUntrusted]: 13,
143
157
  [ErrorCode.HookInvalid]: 13,
144
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,
145
165
  };
146
166
  /** The process exit code for an error code (defaults to 1 for safety). */
147
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. */