@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
@@ -0,0 +1,84 @@
1
+ import { MODEL_TIERS } from "../brand/voice.js";
2
+ import { routingTierUnavailable } from "../errors/index.js";
3
+ import { resolveModelId } from "./resolve.js";
4
+ /**
5
+ * The tier a config resolves to when nothing else pins one down — mirrors the
6
+ * gateway's `auto` fallback (`AUTO_FALLBACK_TIER` in the SDK), so an unrouted
7
+ * cruxy session lands on exactly the tier it does today.
8
+ */
9
+ export const DEFAULT_TIER = "vaani";
10
+ /**
11
+ * The config-driven {@link Router}: maps a declared task class to a tier from
12
+ * `{ default, map }`, and fails loud when the resolved tier is not offered. It
13
+ * NEVER inspects prompt content — selection is purely `map[taskClass] ?? default`.
14
+ */
15
+ export class ConfigRouter {
16
+ cfg;
17
+ offered;
18
+ /**
19
+ * @param cfg the resolved routing table (default tier + per-task map)
20
+ * @param offered the tiers this gateway/plan actually provides; a resolved
21
+ * tier outside this set fails loud. Defaults to all tiers — the
22
+ * seam a future entitlement check narrows (never a silent
23
+ * downgrade).
24
+ */
25
+ constructor(cfg, offered = MODEL_TIERS) {
26
+ this.cfg = cfg;
27
+ this.offered = new Set(offered);
28
+ }
29
+ select(taskClass) {
30
+ // Explicit override, else the default — an unmapped/unknown class is not an
31
+ // error, it just takes the default (never a crash, never the cheapest).
32
+ const tier = this.cfg.map[taskClass] ?? this.cfg.default;
33
+ // Fail loud: a configured tier the gateway does not offer is a usage error
34
+ // to fix, NOT a silent substitution to some other tier (a user who asked for
35
+ // mira reasoning must never be quietly handed kavi).
36
+ if (!this.offered.has(tier)) {
37
+ throw routingTierUnavailable(tier, taskClass, [...this.offered]);
38
+ }
39
+ return tier;
40
+ }
41
+ }
42
+ /**
43
+ * The base tier implied by the session's `model.model`: a real tier passes
44
+ * through; `auto` (and any non-tier value) falls back to {@link DEFAULT_TIER}.
45
+ * Used so that when a user has pinned a single tier, an opt-in routing table
46
+ * that omits `routing.default` still defaults to THEIR tier, not a fixed one.
47
+ */
48
+ function baseTierFromModel(model) {
49
+ return MODEL_TIERS.includes(model)
50
+ ? model
51
+ : DEFAULT_TIER;
52
+ }
53
+ /**
54
+ * Build a router from resolved config, or `null` when routing should stay
55
+ * inert. Routing is:
56
+ *
57
+ * - a cruxy-gateway concept — tiers do not apply to BYO providers, so non-cruxy
58
+ * providers get `null` (no override, their `model.model` is used unchanged);
59
+ * - opt-in — with no `routing.default` and an empty `routing.map`, this returns
60
+ * `null` so behavior (and the wire body, and the state line) is byte-identical
61
+ * to today. Multi-tier routing activates only once the user configures it.
62
+ */
63
+ export function routerForConfig(config) {
64
+ if (config.model.provider !== "cruxy")
65
+ return null;
66
+ const { default: def, map } = config.routing;
67
+ const configured = def !== undefined || Object.keys(map).length > 0;
68
+ if (!configured)
69
+ return null;
70
+ return new ConfigRouter({
71
+ default: def ?? baseTierFromModel(config.model.model),
72
+ map,
73
+ });
74
+ }
75
+ /**
76
+ * Resolve a declared task class to `{ tier, model }`: the tier for honest
77
+ * surfacing (the U.4 state line), the wire model id for the request. The model
78
+ * id comes from the internal {@link resolveModelId} — callers never touch that
79
+ * mapping directly, so it stays the single source of truth.
80
+ */
81
+ export function resolveTaskModel(router, taskClass) {
82
+ const tier = router.select(taskClass);
83
+ return { tier, model: resolveModelId(tier) };
84
+ }
@@ -0,0 +1,42 @@
1
+ import { MODEL_TIERS } from "../brand/voice.js";
2
+ /**
3
+ * Multi-model routing (C.30): route each unit of work to the right tier instead
4
+ * of running one model for everything. The caller DECLARES a {@link TaskClass}
5
+ * at the call site; a {@link Router} maps that class to a {@link Tier} via config
6
+ * — it never sniffs the prompt to guess difficulty. The tier→gateway model-id
7
+ * mapping is internal (see `resolve.ts`); only tier names ever appear in config,
8
+ * logs, errors, or the state line (the U.8 tier gag).
9
+ */
10
+ /**
11
+ * The unit-of-work classes a caller can declare. Each is an explicit intent —
12
+ * NOT a difficulty the router infers. Unknown/unset resolves to the router's
13
+ * default tier, never a crash.
14
+ */
15
+ export declare const TASK_CLASSES: readonly ["main-turn", "subagent", "plan", "commit-msg", "classify", "summarize"];
16
+ export type TaskClass = (typeof TASK_CLASSES)[number];
17
+ /** A routing tier — the ONLY model vocabulary the user ever sees (U.8). */
18
+ export type Tier = (typeof MODEL_TIERS)[number];
19
+ /**
20
+ * Selects a tier for a declared task class. Deliberately one method: the caller
21
+ * passes intent, the router returns a tier from its configured mapping. No
22
+ * prompt-content inspection, ever — difficulty detection is explicitly out of
23
+ * scope (a caller declares; the router does not guess).
24
+ */
25
+ export interface Router {
26
+ /**
27
+ * Map a task class to its tier. Falls back to the configured default when the
28
+ * class has no explicit mapping; throws `CRUXY_E_ROUTING_TIER_UNAVAILABLE`
29
+ * (fail loud, never a silent substitution) when the resolved tier is not
30
+ * offered.
31
+ */
32
+ select(taskClass: TaskClass): Tier;
33
+ }
34
+ /**
35
+ * The declarative routing table: a `default` tier plus per-task overrides. Lives
36
+ * in config (`routing.default`, `routing.map`). An empty map means every class
37
+ * resolves to `default` — a single tier, no traffic splitting.
38
+ */
39
+ export interface RoutingConfig {
40
+ default: Tier;
41
+ map: Partial<Record<TaskClass, Tier>>;
42
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Multi-model routing (C.30): route each unit of work to the right tier instead
3
+ * of running one model for everything. The caller DECLARES a {@link TaskClass}
4
+ * at the call site; a {@link Router} maps that class to a {@link Tier} via config
5
+ * — it never sniffs the prompt to guess difficulty. The tier→gateway model-id
6
+ * mapping is internal (see `resolve.ts`); only tier names ever appear in config,
7
+ * logs, errors, or the state line (the U.8 tier gag).
8
+ */
9
+ /**
10
+ * The unit-of-work classes a caller can declare. Each is an explicit intent —
11
+ * NOT a difficulty the router infers. Unknown/unset resolves to the router's
12
+ * default tier, never a crash.
13
+ */
14
+ export const TASK_CLASSES = [
15
+ /** An interactive main-agent turn. */
16
+ "main-turn",
17
+ /** A spawned subagent task (C.14). */
18
+ "subagent",
19
+ /** Plan proposal / revision (C.31). */
20
+ "plan",
21
+ /** Commit / pull-request text generation. */
22
+ "commit-msg",
23
+ /** A cheap one-shot classification. */
24
+ "classify",
25
+ /** Context compaction / summarization. */
26
+ "summarize",
27
+ ];
@@ -2,6 +2,7 @@ import type { Provider } from "@cruxy/sdk";
2
2
  import type { ApprovalDecision } from "../approval/types.js";
3
3
  import type { CruxyConfig } from "../config/index.js";
4
4
  import type { StreamRenderer } from "../render/index.js";
5
+ import type { Router } from "../routing/index.js";
5
6
  import type { ApproveAction, ToolContext, ToolRegistry } from "../tools/index.js";
6
7
  import type { SandboxService } from "../sandbox/index.js";
7
8
  import type { SubagentResult, SubagentSpec } from "./types.js";
@@ -15,6 +16,11 @@ import type { SubagentResult, SubagentSpec } from "./types.js";
15
16
  export interface SubagentOrchestratorDeps {
16
17
  provider: Provider;
17
18
  config: CruxyConfig;
19
+ /**
20
+ * Multi-model routing (C.30). When set, child runs route on their spec's task
21
+ * class (default `subagent`); omitted → the provider default (unchanged).
22
+ */
23
+ router?: Router;
18
24
  /** The parent's registry — the ceiling every child scope derives from. */
19
25
  parentRegistry: ToolRegistry;
20
26
  cwd: string;
@@ -91,6 +91,8 @@ export class SubagentOrchestrator {
91
91
  projectInstructions: deps.projectInstructions,
92
92
  subagent: true,
93
93
  budget,
94
+ router: deps.router,
95
+ taskClass: spec.taskClass ?? "subagent",
94
96
  });
95
97
  }
96
98
  catch (err) {
@@ -1,4 +1,5 @@
1
1
  import type { Usage } from "@cruxy/sdk";
2
+ import type { TaskClass } from "../routing/index.js";
2
3
  /**
3
4
  * Types for subagent orchestration (C.14): the main agent delegates a bounded
4
5
  * subtask to a child agent that runs the SAME loop with its own fresh history,
@@ -35,6 +36,11 @@ export interface SubagentSpec {
35
36
  * spawn can narrow its budget, never raise it past the configured ceilings.
36
37
  */
37
38
  budget?: Partial<BudgetLimits>;
39
+ /**
40
+ * Routing task class for this spawn (C.30); defaults to `subagent`. A
41
+ * declaration at the spawn call site — not something the router guesses.
42
+ */
43
+ taskClass?: TaskClass;
38
44
  }
39
45
  /**
40
46
  * What the parent gets back — compact structured data, never the transcript.
@@ -59,7 +59,9 @@ export declare function fillContent(input: GenerateInput): GeneratedContent;
59
59
  * rules, then normalize + redact. Resilient: if the model's reply isn't the
60
60
  * expected JSON, the first line becomes the subject and the rest the body.
61
61
  */
62
- export declare function generateWithLlm(provider: Provider, input: GenerateInput): Promise<GeneratedContent>;
62
+ export declare function generateWithLlm(provider: Provider, input: GenerateInput, opts?: {
63
+ model?: string;
64
+ }): Promise<GeneratedContent>;
63
65
  interface ParsedGenerated {
64
66
  branchName?: string;
65
67
  commitSubject?: string;
@@ -154,7 +154,7 @@ export function fillContent(input) {
154
154
  * rules, then normalize + redact. Resilient: if the model's reply isn't the
155
155
  * expected JSON, the first line becomes the subject and the rest the body.
156
156
  */
157
- export async function generateWithLlm(provider, input) {
157
+ export async function generateWithLlm(provider, input, opts = {}) {
158
158
  const redactedDiff = redactSecrets(input.diff);
159
159
  const system = buildSystemPrompt(input);
160
160
  const user = buildUserPrompt({ ...input, diff: redactedDiff });
@@ -162,6 +162,9 @@ export async function generateWithLlm(provider, input) {
162
162
  for await (const ev of provider.stream({
163
163
  system,
164
164
  messages: [{ role: "user", content: user }],
165
+ // The commit-msg tier's wire model (C.30), resolved by the caller; omitted →
166
+ // the provider default.
167
+ ...(opts.model ? { model: opts.model } : {}),
165
168
  })) {
166
169
  if (ev.type === "text_delta")
167
170
  text += ev.text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "tinyglobby": "^0.2.10",
37
37
  "zod": "^3.23.8",
38
38
  "zod-to-json-schema": "^3.23.5",
39
- "@cruxy/sdk": "0.1.0"
39
+ "@cruxy/sdk": "0.2.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/better-sqlite3": "^7.6.13",