@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.
- package/dist/agent/loop.d.ts +15 -0
- package/dist/agent/loop.js +13 -2
- package/dist/agent/prompts.d.ts +7 -0
- package/dist/agent/prompts.js +6 -0
- package/dist/agent/session.d.ts +14 -0
- package/dist/agent/session.js +10 -1
- package/dist/brand/index.d.ts +1 -1
- package/dist/brand/index.js +1 -1
- package/dist/brand/voice.d.ts +20 -0
- package/dist/brand/voice.js +54 -0
- package/dist/cli/commands/memory.d.ts +8 -0
- package/dist/cli/commands/memory.js +98 -0
- package/dist/cli/commands/pr.js +9 -1
- package/dist/cli/program.js +2 -0
- package/dist/cli/session-factory.js +31 -1
- package/dist/config/schema.d.ts +114 -28
- package/dist/config/schema.js +38 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.js +11 -0
- package/dist/errors/constructors.d.ts +23 -0
- package/dist/errors/constructors.js +86 -6
- package/dist/errors/types.d.ts +12 -0
- package/dist/errors/types.js +20 -0
- package/dist/hooks/types.d.ts +1 -1
- package/dist/memory/index.d.ts +7 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/recall.d.ts +32 -0
- package/dist/memory/recall.js +73 -0
- package/dist/memory/remember-tool.d.ts +25 -0
- package/dist/memory/remember-tool.js +56 -0
- package/dist/memory/secrets.d.ts +29 -0
- package/dist/memory/secrets.js +61 -0
- package/dist/memory/service.d.ts +92 -0
- package/dist/memory/service.js +164 -0
- package/dist/memory/store.d.ts +32 -0
- package/dist/memory/store.js +100 -0
- package/dist/memory/trust.d.ts +52 -0
- package/dist/memory/trust.js +106 -0
- package/dist/memory/types.d.ts +101 -0
- package/dist/memory/types.js +58 -0
- package/dist/plan/service.d.ts +9 -0
- package/dist/plan/service.js +6 -0
- package/dist/render/state.js +4 -1
- package/dist/render/types.d.ts +7 -1
- package/dist/routing/index.d.ts +2 -0
- package/dist/routing/index.js +5 -0
- package/dist/routing/resolve.d.ts +17 -0
- package/dist/routing/resolve.js +18 -0
- package/dist/routing/router.d.ts +47 -0
- package/dist/routing/router.js +84 -0
- package/dist/routing/types.d.ts +42 -0
- package/dist/routing/types.js +27 -0
- package/dist/subagent/orchestrator.d.ts +6 -0
- package/dist/subagent/orchestrator.js +2 -0
- package/dist/subagent/types.d.ts +6 -0
- package/dist/vcs/generate.d.ts +3 -1
- package/dist/vcs/generate.js +4 -1
- package/package.json +2 -2
package/dist/agent/loop.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Message, Provider, Usage } from "@cruxy/sdk";
|
|
|
2
2
|
import type { CruxyConfig } from "../config/index.js";
|
|
3
3
|
import type { HookEvent } from "../hooks/index.js";
|
|
4
4
|
import type { StreamRenderer } from "../render/index.js";
|
|
5
|
+
import { type Router, type TaskClass } from "../routing/index.js";
|
|
5
6
|
import type { ToolContext } from "../tools/index.js";
|
|
6
7
|
import { ToolRegistry } from "../tools/index.js";
|
|
7
8
|
/** The lifecycle-hook firing seam (C.19). Structural so the loop stays
|
|
@@ -39,6 +40,9 @@ export interface RunAgentArgs {
|
|
|
39
40
|
} | null;
|
|
40
41
|
/** Project instructions (e.g. from CRUXY.md) folded into the system prompt. */
|
|
41
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;
|
|
42
46
|
/** Plan mode's propose phase (C.31): inject the plan-first system directive. */
|
|
43
47
|
planMode?: boolean;
|
|
44
48
|
/** Subagent runs (C.14): inject the bounded-subtask system directive. */
|
|
@@ -57,6 +61,17 @@ export interface RunAgentArgs {
|
|
|
57
61
|
* no-hooks path, so their tools never fire hooks.
|
|
58
62
|
*/
|
|
59
63
|
hooks?: LifecycleHookRunner;
|
|
64
|
+
/**
|
|
65
|
+
* Multi-model routing (C.30): when set, the tier for `taskClass` is resolved
|
|
66
|
+
* ONCE up front (fail loud before any model turn) and its wire model overrides
|
|
67
|
+
* the provider default for every turn in this run; the tier is surfaced on the
|
|
68
|
+
* live line. Omitted → no override, the provider's default model is used
|
|
69
|
+
* (unchanged behavior).
|
|
70
|
+
*/
|
|
71
|
+
router?: Router;
|
|
72
|
+
/** The declared task class for routing; defaults to `main-turn`. Ignored
|
|
73
|
+
* unless `router` is set. */
|
|
74
|
+
taskClass?: TaskClass;
|
|
60
75
|
}
|
|
61
76
|
/**
|
|
62
77
|
* The budget seam for {@link runAgent}: implementations track their own caps
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CruxyError, providerUnsupported } from "../errors/index.js";
|
|
2
|
+
import { resolveTaskModel, } from "../routing/index.js";
|
|
2
3
|
import { buildSystemPrompt } from "./prompts.js";
|
|
3
4
|
/** Tools whose successful call is a file change (drives the `on-file-change`
|
|
4
5
|
* hook). Kept in sync with the file-mutating tool set. */
|
|
@@ -17,9 +18,16 @@ export async function runAgent(args) {
|
|
|
17
18
|
if (!provider.supportsTools) {
|
|
18
19
|
throw providerUnsupported(config.model.provider);
|
|
19
20
|
}
|
|
21
|
+
// Resolve the routing tier ONCE, before any model turn (C.30). A tier the
|
|
22
|
+
// gateway does not offer throws CRUXY_E_ROUTING_TIER_UNAVAILABLE here — so a
|
|
23
|
+
// misrouted run never reaches provider.stream (no request sent with the wrong
|
|
24
|
+
// tier), and never silently falls back to a different one.
|
|
25
|
+
const routed = args.router
|
|
26
|
+
? resolveTaskModel(args.router, args.taskClass ?? "main-turn")
|
|
27
|
+
: null;
|
|
20
28
|
renderer?.beginTurn();
|
|
21
29
|
try {
|
|
22
|
-
return await driveLoop(args, renderer);
|
|
30
|
+
return await driveLoop(args, renderer, routed);
|
|
23
31
|
}
|
|
24
32
|
finally {
|
|
25
33
|
// Always leave the terminal clean: no orphaned status line, no held text —
|
|
@@ -28,7 +36,7 @@ export async function runAgent(args) {
|
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
/** The body of {@link runAgent}, split out so turn cleanup lives in one finally. */
|
|
31
|
-
async function driveLoop(args, renderer) {
|
|
39
|
+
async function driveLoop(args, renderer, routed) {
|
|
32
40
|
const { provider, registry, config, ctx } = args;
|
|
33
41
|
const { logger } = ctx;
|
|
34
42
|
// Work on a copy so we never mutate the caller's array as a side effect; the
|
|
@@ -48,6 +56,7 @@ async function driveLoop(args, renderer) {
|
|
|
48
56
|
.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
49
57
|
git: args.git ?? null,
|
|
50
58
|
projectInstructions: args.projectInstructions ?? null,
|
|
59
|
+
recalledMemory: args.recalledMemory ?? null,
|
|
51
60
|
planMode: args.planMode ?? false,
|
|
52
61
|
subagent: args.subagent ?? false,
|
|
53
62
|
});
|
|
@@ -80,11 +89,13 @@ async function driveLoop(args, renderer) {
|
|
|
80
89
|
tokens: usage.input_tokens + usage.output_tokens > 0
|
|
81
90
|
? { input: usage.input_tokens, output: usage.output_tokens }
|
|
82
91
|
: undefined,
|
|
92
|
+
tier: routed?.tier,
|
|
83
93
|
});
|
|
84
94
|
for await (const ev of provider.stream({
|
|
85
95
|
system,
|
|
86
96
|
messages,
|
|
87
97
|
tools,
|
|
98
|
+
...(routed ? { model: routed.model } : {}),
|
|
88
99
|
})) {
|
|
89
100
|
switch (ev.type) {
|
|
90
101
|
case "text_delta":
|
package/dist/agent/prompts.d.ts
CHANGED
|
@@ -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. */
|
package/dist/agent/prompts.js
CHANGED
|
@@ -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
|
/**
|
package/dist/agent/session.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Message, Provider, Usage } from "@cruxy/sdk";
|
|
2
2
|
import type { CruxyConfig } from "../config/index.js";
|
|
3
3
|
import type { StreamRenderer } from "../render/index.js";
|
|
4
|
+
import { type Router } from "../routing/index.js";
|
|
4
5
|
import type { ToolContext } from "../tools/index.js";
|
|
5
6
|
import type { ToolRegistry } from "../tools/index.js";
|
|
6
7
|
import { type AgentResult, type LifecycleHookRunner } from "./loop.js";
|
|
@@ -13,6 +14,7 @@ import { type AgentResult, type LifecycleHookRunner } from "./loop.js";
|
|
|
13
14
|
export type PlanRunner = (args: {
|
|
14
15
|
messages: Message[];
|
|
15
16
|
projectInstructions: string | null;
|
|
17
|
+
recalledMemory: string | null;
|
|
16
18
|
renderer?: StreamRenderer;
|
|
17
19
|
}) => Promise<AgentResult>;
|
|
18
20
|
export interface SessionArgs {
|
|
@@ -31,6 +33,13 @@ export interface SessionArgs {
|
|
|
31
33
|
} | null;
|
|
32
34
|
/** Project instructions (e.g. CRUXY.md) folded into every turn's system prompt. */
|
|
33
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;
|
|
34
43
|
/** Start in plan mode (C.31). Toggleable at runtime via `setPlanMode`. */
|
|
35
44
|
planMode?: boolean;
|
|
36
45
|
/** The plan-mode turn runner; required for plan mode to actually engage. */
|
|
@@ -42,6 +51,11 @@ export interface SessionArgs {
|
|
|
42
51
|
* `before-tool`/`after-tool`/`on-file-change`.
|
|
43
52
|
*/
|
|
44
53
|
hooks?: LifecycleHookRunner;
|
|
54
|
+
/**
|
|
55
|
+
* Multi-model routing (C.30). When set, main turns route on `main-turn` and
|
|
56
|
+
* context compaction on `summarize`; omitted → the provider default (unchanged).
|
|
57
|
+
*/
|
|
58
|
+
router?: Router;
|
|
45
59
|
}
|
|
46
60
|
/**
|
|
47
61
|
* Estimate the token footprint of a message list with a cheap chars/4 heuristic
|
package/dist/agent/session.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadProjectInstructions } from "../config/index.js";
|
|
2
|
+
import { resolveTaskModel } from "../routing/index.js";
|
|
2
3
|
import { runAgent, } from "./loop.js";
|
|
3
4
|
import { SUMMARY_SYSTEM, COMPACTION_MARKER } from "./prompts.js";
|
|
4
5
|
/**
|
|
@@ -98,11 +99,13 @@ export class Session {
|
|
|
98
99
|
? await this.args.planRunner({
|
|
99
100
|
messages: this.messages,
|
|
100
101
|
projectInstructions: this.projectInstructions,
|
|
102
|
+
recalledMemory: this.args.recalledMemory ?? null,
|
|
101
103
|
renderer,
|
|
102
104
|
})
|
|
103
105
|
: await runAgent({
|
|
104
106
|
messages: this.messages,
|
|
105
|
-
...this.args,
|
|
107
|
+
...this.args, // carries `router` through to the loop
|
|
108
|
+
taskClass: "main-turn",
|
|
106
109
|
// After the spread so a mid-session `/reload` wins over the initial value.
|
|
107
110
|
projectInstructions: this.projectInstructions,
|
|
108
111
|
planMode: false, // the plan directive belongs only to the runner's propose phase
|
|
@@ -226,9 +229,15 @@ export class Session {
|
|
|
226
229
|
const transcript = renderTranscript(prefix);
|
|
227
230
|
const usage = { input_tokens: 0, output_tokens: 0 };
|
|
228
231
|
let text = "";
|
|
232
|
+
// Compaction is mechanical work — route it on `summarize` (C.30) when routing
|
|
233
|
+
// is active; omitted → the provider default, unchanged.
|
|
234
|
+
const routed = this.args.router
|
|
235
|
+
? resolveTaskModel(this.args.router, "summarize")
|
|
236
|
+
: null;
|
|
229
237
|
for await (const ev of this.args.provider.stream({
|
|
230
238
|
system: SUMMARY_SYSTEM,
|
|
231
239
|
messages: [{ role: "user", content: transcript }],
|
|
240
|
+
...(routed ? { model: routed.model } : {}),
|
|
232
241
|
})) {
|
|
233
242
|
switch (ev.type) {
|
|
234
243
|
case "text_delta":
|
package/dist/brand/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { CANONICAL_TERMS, FORBIDDEN_MODEL_NAMES, FORBIDDEN_TERMS, MODEL_TIERS, PRODUCT_MASTHEAD, PRODUCT_NAME, PRODUCT_TAGLINE, scanForbidden, type ForbiddenTerm, type LexiconViolation, } from "./voice.js";
|
|
1
|
+
export { CANONICAL_TERMS, FORBIDDEN_MODEL_NAMES, FORBIDDEN_TERMS, MODEL_NAME_PLACEHOLDER, MODEL_TIERS, PRODUCT_MASTHEAD, PRODUCT_NAME, PRODUCT_TAGLINE, scanForbidden, scrubModelNames, type ForbiddenTerm, type LexiconViolation, } from "./voice.js";
|
package/dist/brand/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { CANONICAL_TERMS, FORBIDDEN_MODEL_NAMES, FORBIDDEN_TERMS, MODEL_TIERS, PRODUCT_MASTHEAD, PRODUCT_NAME, PRODUCT_TAGLINE, scanForbidden, } from "./voice.js";
|
|
1
|
+
export { CANONICAL_TERMS, FORBIDDEN_MODEL_NAMES, FORBIDDEN_TERMS, MODEL_NAME_PLACEHOLDER, MODEL_TIERS, PRODUCT_MASTHEAD, PRODUCT_NAME, PRODUCT_TAGLINE, scanForbidden, scrubModelNames, } from "./voice.js";
|
package/dist/brand/voice.d.ts
CHANGED
|
@@ -59,6 +59,26 @@ export declare const FORBIDDEN_TERMS: readonly ForbiddenTerm[];
|
|
|
59
59
|
* absent: they are real bring-your-own-provider config values, not model names.
|
|
60
60
|
*/
|
|
61
61
|
export declare const FORBIDDEN_MODEL_NAMES: RegExp;
|
|
62
|
+
/** The neutral token an upstream model name is replaced with in user-facing copy. */
|
|
63
|
+
export declare const MODEL_NAME_PLACEHOLDER = "\u00ABmodel\u00BB";
|
|
64
|
+
/**
|
|
65
|
+
* Replace every upstream model IDENTIFIER in an arbitrary, externally-sourced
|
|
66
|
+
* string with {@link MODEL_NAME_PLACEHOLDER}, collapsing a whole `name-name-vers`
|
|
67
|
+
* run to a single token.
|
|
68
|
+
*
|
|
69
|
+
* The U.8 gag is structural *inside the process* (the routing package can't
|
|
70
|
+
* produce an upstream id — see `resolveModelId`), but a gateway/provider error
|
|
71
|
+
* body is an arbitrary external string that bypasses that guarantee. This is the
|
|
72
|
+
* scrub applied at the error-construction boundary so no upstream id reaches a
|
|
73
|
+
* user-facing `CruxyError.cause`.
|
|
74
|
+
*
|
|
75
|
+
* Scrubs model ids as whole TOKENS, never a bare vendor word or a substring
|
|
76
|
+
* inside an unrelated word/path — so legitimate output (`/home/claude`, the
|
|
77
|
+
* `opus` codec, a `claude-bot` username) is left intact (see {@link isModelId}).
|
|
78
|
+
* Provider names (`anthropic`/`openai`) are absent from the set, so bring-your-own
|
|
79
|
+
* provider config values survive too.
|
|
80
|
+
*/
|
|
81
|
+
export declare function scrubModelNames(text: string): string;
|
|
62
82
|
/** One lexicon violation found in a scanned string. */
|
|
63
83
|
export interface LexiconViolation {
|
|
64
84
|
/** The forbidden term (or "model-name" for a gag violation). */
|
package/dist/brand/voice.js
CHANGED
|
@@ -55,6 +55,60 @@ export const FORBIDDEN_TERMS = [
|
|
|
55
55
|
* absent: they are real bring-your-own-provider config values, not model names.
|
|
56
56
|
*/
|
|
57
57
|
export const FORBIDDEN_MODEL_NAMES = /\b(claude|sonnet|opus|haiku|gpt-?\d|gemini|llama|mistral)\b/i;
|
|
58
|
+
/** The neutral token an upstream model name is replaced with in user-facing copy. */
|
|
59
|
+
export const MODEL_NAME_PLACEHOLDER = "«model»";
|
|
60
|
+
/**
|
|
61
|
+
* A model-id-SHAPED run in free text: a self-identifying `gpt-4`/`gpt-4o`, or a
|
|
62
|
+
* model word carrying a hyphen/dot tail (`claude-sonnet-4-6`, `gemini-1.5-pro`).
|
|
63
|
+
* Deliberately GENEROUS — it also matches bare words and non-model hyphenated
|
|
64
|
+
* tokens; {@link isModelId} does the real filtering in the scrub callback, and
|
|
65
|
+
* matching the whole run (not each word) is what collapses `claude-sonnet-4-6`
|
|
66
|
+
* to ONE placeholder instead of `«model»-«model»-4-6`. The leading
|
|
67
|
+
* `(?<![/\\\w])` stops it firing inside a longer word (`gptools`) or on a path
|
|
68
|
+
* segment (`/home/claude/…`) — both of which the bare `\b` form corrupts.
|
|
69
|
+
*/
|
|
70
|
+
const MODEL_ID_CANDIDATE = /(?<![/\\\w])(?:gpt-?\d[a-z0-9]*(?:[-.][a-z0-9]+)*|(?:claude|sonnet|opus|haiku|gemini|llama|mistral|gpt)(?:[-.][a-z0-9]+)*)/gi;
|
|
71
|
+
/** The bare model words, for the digitless-compound test in {@link isModelId}. */
|
|
72
|
+
const MODEL_WORD = /(?:claude|sonnet|opus|haiku|gemini|llama|mistral|gpt)/gi;
|
|
73
|
+
/**
|
|
74
|
+
* Is a {@link MODEL_ID_CANDIDATE} run an actual upstream model reference (scrub),
|
|
75
|
+
* or a legitimate token that merely contains a model word (keep)? A reference is:
|
|
76
|
+
* a self-identifying `gpt-<n>`; OR a hyphen/dot run carrying a version digit
|
|
77
|
+
* (`claude-sonnet-4-6`, `gemini-1.5-pro`); OR a digitless compound of ≥2 distinct
|
|
78
|
+
* model words (`claude-sonnet`). A bare word (`opus`, `haiku`), a path segment
|
|
79
|
+
* (`/home/claude`), or a plain hyphenated token (`claude-bot`) is none of these,
|
|
80
|
+
* so it survives verbatim — the gag scrubs model IDS, never legitimate output.
|
|
81
|
+
*/
|
|
82
|
+
function isModelId(run) {
|
|
83
|
+
if (/^gpt-?\d/i.test(run))
|
|
84
|
+
return true; // gpt-4, gpt4, gpt-4o
|
|
85
|
+
if (!/[-.]/.test(run))
|
|
86
|
+
return false; // bare word — never a model reference here
|
|
87
|
+
if (/\d/.test(run))
|
|
88
|
+
return true; // version digit ⇒ an id
|
|
89
|
+
const words = run.match(MODEL_WORD) ?? [];
|
|
90
|
+
return new Set(words.map((w) => w.toLowerCase())).size >= 2; // claude-sonnet
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Replace every upstream model IDENTIFIER in an arbitrary, externally-sourced
|
|
94
|
+
* string with {@link MODEL_NAME_PLACEHOLDER}, collapsing a whole `name-name-vers`
|
|
95
|
+
* run to a single token.
|
|
96
|
+
*
|
|
97
|
+
* The U.8 gag is structural *inside the process* (the routing package can't
|
|
98
|
+
* produce an upstream id — see `resolveModelId`), but a gateway/provider error
|
|
99
|
+
* body is an arbitrary external string that bypasses that guarantee. This is the
|
|
100
|
+
* scrub applied at the error-construction boundary so no upstream id reaches a
|
|
101
|
+
* user-facing `CruxyError.cause`.
|
|
102
|
+
*
|
|
103
|
+
* Scrubs model ids as whole TOKENS, never a bare vendor word or a substring
|
|
104
|
+
* inside an unrelated word/path — so legitimate output (`/home/claude`, the
|
|
105
|
+
* `opus` codec, a `claude-bot` username) is left intact (see {@link isModelId}).
|
|
106
|
+
* Provider names (`anthropic`/`openai`) are absent from the set, so bring-your-own
|
|
107
|
+
* provider config values survive too.
|
|
108
|
+
*/
|
|
109
|
+
export function scrubModelNames(text) {
|
|
110
|
+
return text.replace(MODEL_ID_CANDIDATE, (run) => isModelId(run) ? MODEL_NAME_PLACEHOLDER : run);
|
|
111
|
+
}
|
|
58
112
|
/**
|
|
59
113
|
* Scan one user-facing string for lexicon violations — deprecated synonyms and
|
|
60
114
|
* upstream model names. Returns every violation (empty ⇒ on-voice). Pure; the
|
|
@@ -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
|
+
}
|
package/dist/cli/commands/pr.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loadConfig, resolveApiKey } from "../../config/index.js";
|
|
|
5
5
|
import { authMissingKey, shouldUseColor } from "../../errors/index.js";
|
|
6
6
|
import { themeForColor } from "../../theme/index.js";
|
|
7
7
|
import { ApprovalService } from "../../approval/index.js";
|
|
8
|
+
import { resolveTaskModel, routerForConfig } from "../../routing/index.js";
|
|
8
9
|
import { createForgeProvider, createPrService, generateWithLlm, loadCommitGuidance, resolveForgeToken, } from "../../vcs/index.js";
|
|
9
10
|
/**
|
|
10
11
|
* `cruxy pr` — turn the current changes into a pull request (C.15). Generates the
|
|
@@ -37,6 +38,13 @@ export function prCommand() {
|
|
|
37
38
|
temperature: config.model.temperature,
|
|
38
39
|
gatewayUrl: config.cruxy.gatewayUrl,
|
|
39
40
|
});
|
|
41
|
+
// Multi-model routing (C.30): PR/commit text is `commit-msg` work. Null
|
|
42
|
+
// unless routing is configured → the provider default, unchanged. A
|
|
43
|
+
// misrouted tier fails loud here before any request.
|
|
44
|
+
const router = routerForConfig(config);
|
|
45
|
+
const genModel = router
|
|
46
|
+
? resolveTaskModel(router, "commit-msg").model
|
|
47
|
+
: undefined;
|
|
40
48
|
// resolveForgeToken throws CRUXY_E_FORGE_AUTH (exit 4) if none is found.
|
|
41
49
|
const forge = createForgeProvider(resolveForgeToken());
|
|
42
50
|
const guidance = await loadCommitGuidance(cwd);
|
|
@@ -53,7 +61,7 @@ export function prCommand() {
|
|
|
53
61
|
...i,
|
|
54
62
|
scopes: guidance.scopes,
|
|
55
63
|
skillBody: guidance.skillBody,
|
|
56
|
-
}),
|
|
64
|
+
}, { model: genModel }),
|
|
57
65
|
});
|
|
58
66
|
logger.info(t.muted("generating pull request content…"));
|
|
59
67
|
const outcome = await service.openPullRequest({
|
package/dist/cli/program.js
CHANGED
|
@@ -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.
|
|
@@ -7,6 +7,8 @@ import { shouldUseColor } from "../errors/index.js";
|
|
|
7
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
|
+
import { routerForConfig } from "../routing/index.js";
|
|
11
|
+
import { MemoryService, rememberTool } from "../memory/index.js";
|
|
10
12
|
import { SubagentOrchestrator, makeSpawnSubagentTool, } from "../subagent/index.js";
|
|
11
13
|
/**
|
|
12
14
|
* Wrap a PromptIO so the live region yields before any prompt text lands
|
|
@@ -96,9 +98,30 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
96
98
|
temperature: config.model.temperature,
|
|
97
99
|
gatewayUrl: config.cruxy.gatewayUrl,
|
|
98
100
|
});
|
|
101
|
+
// Multi-model routing (C.30): null unless routing is configured on a cruxy
|
|
102
|
+
// session, so the default path threads `undefined` and behaves exactly as
|
|
103
|
+
// before. One router is shared by the main loop, subagents, and plan mode.
|
|
104
|
+
const router = routerForConfig(config) ?? undefined;
|
|
99
105
|
const execRegistry = buildDefaultRegistry();
|
|
100
106
|
const git = getGitInfo(cwd);
|
|
101
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
|
+
}
|
|
102
125
|
// One io shared by every prompt in the session (plan approval, the U.3 gate,
|
|
103
126
|
// and any gate inside a subagent), so they all coordinate with the same live
|
|
104
127
|
// region. The full wrapper stack around an ApprovalService is factored here
|
|
@@ -114,6 +137,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
114
137
|
const orchestrator = new SubagentOrchestrator({
|
|
115
138
|
provider,
|
|
116
139
|
config,
|
|
140
|
+
router,
|
|
117
141
|
parentRegistry: execRegistry,
|
|
118
142
|
cwd,
|
|
119
143
|
logger,
|
|
@@ -144,7 +168,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
144
168
|
requestApproval: gate(approval),
|
|
145
169
|
sandbox,
|
|
146
170
|
};
|
|
147
|
-
const planRunner = ({ messages, projectInstructions, renderer: turnRenderer, }) => runPlanSession({
|
|
171
|
+
const planRunner = ({ messages, projectInstructions, recalledMemory: turnMemory, renderer: turnRenderer, }) => runPlanSession({
|
|
148
172
|
provider,
|
|
149
173
|
config,
|
|
150
174
|
ctx,
|
|
@@ -155,7 +179,9 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
155
179
|
messages,
|
|
156
180
|
git,
|
|
157
181
|
projectInstructions,
|
|
182
|
+
recalledMemory: turnMemory,
|
|
158
183
|
renderer: turnRenderer,
|
|
184
|
+
router,
|
|
159
185
|
});
|
|
160
186
|
return new Session({
|
|
161
187
|
provider,
|
|
@@ -164,9 +190,11 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
164
190
|
ctx,
|
|
165
191
|
git,
|
|
166
192
|
projectInstructions,
|
|
193
|
+
recalledMemory,
|
|
167
194
|
planMode: true,
|
|
168
195
|
planRunner,
|
|
169
196
|
hooks,
|
|
197
|
+
router,
|
|
170
198
|
});
|
|
171
199
|
}
|
|
172
200
|
const approval = new ApprovalService({
|
|
@@ -182,6 +210,8 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
182
210
|
ctx,
|
|
183
211
|
git,
|
|
184
212
|
projectInstructions,
|
|
213
|
+
recalledMemory,
|
|
185
214
|
hooks,
|
|
215
|
+
router,
|
|
186
216
|
});
|
|
187
217
|
}
|