@cruxy/cli 0.14.0 → 0.17.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 +26 -0
- package/dist/agent/loop.js +59 -3
- package/dist/agent/session.d.ts +17 -1
- package/dist/agent/session.js +23 -2
- package/dist/brand/index.d.ts +1 -0
- package/dist/brand/index.js +1 -0
- package/dist/brand/voice.d.ts +94 -0
- package/dist/brand/voice.js +127 -0
- package/dist/cli/commands/checkpoint.js +1 -1
- package/dist/cli/commands/hooks.d.ts +8 -0
- package/dist/cli/commands/hooks.js +83 -0
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/pr.js +10 -2
- package/dist/cli/commands/rollback.js +1 -1
- package/dist/cli/commands/run.js +13 -3
- package/dist/cli/commands/skills.js +2 -2
- package/dist/cli/program.js +5 -2
- package/dist/cli/repl.d.ts +2 -1
- package/dist/cli/repl.js +54 -3
- package/dist/cli/session-factory.d.ts +2 -2
- package/dist/cli/session-factory.js +13 -2
- package/dist/config/schema.d.ts +139 -46
- package/dist/config/schema.js +42 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +9 -0
- package/dist/errors/constructors.d.ts +25 -0
- package/dist/errors/constructors.js +98 -6
- package/dist/errors/types.d.ts +14 -0
- package/dist/errors/types.js +23 -0
- package/dist/hooks/config.d.ts +21 -0
- package/dist/hooks/config.js +253 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/runner.d.ts +76 -0
- package/dist/hooks/runner.js +114 -0
- package/dist/hooks/service.d.ts +38 -0
- package/dist/hooks/service.js +49 -0
- package/dist/hooks/slash.d.ts +48 -0
- package/dist/hooks/slash.js +58 -0
- package/dist/hooks/trust.d.ts +46 -0
- package/dist/hooks/trust.js +106 -0
- package/dist/hooks/types.d.ts +147 -0
- package/dist/hooks/types.js +61 -0
- package/dist/onboarding/steps.js +1 -1
- package/dist/plan/service.d.ts +6 -0
- package/dist/plan/service.js +4 -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/tools/shell/exec.d.ts +53 -0
- package/dist/tools/shell/exec.js +128 -0
- package/dist/tools/shell/run-command.d.ts +4 -0
- package/dist/tools/shell/run-command.js +26 -116
- package/dist/vcs/generate.d.ts +3 -1
- package/dist/vcs/generate.js +4 -1
- package/package.json +2 -2
package/dist/cli/commands/run.js
CHANGED
|
@@ -6,12 +6,13 @@ import { createRenderer } from "../../render/index.js";
|
|
|
6
6
|
import { themeForColor } from "../../theme/index.js";
|
|
7
7
|
import { CheckpointService } from "../../checkpoint/index.js";
|
|
8
8
|
import { SandboxService } from "../../sandbox/index.js";
|
|
9
|
+
import { buildHooksService } from "../../hooks/index.js";
|
|
9
10
|
import { runInteractive } from "../repl.js";
|
|
10
11
|
import { buildAgentSession } from "../session-factory.js";
|
|
11
12
|
import { apiKeyEnvVar, maybeRunOnboarding } from "../onboard.js";
|
|
12
13
|
export function runCommand() {
|
|
13
14
|
return new Command("run")
|
|
14
|
-
.description("run
|
|
15
|
+
.description("run a task once, or start an interactive session")
|
|
15
16
|
.argument("[prompt...]", "the task for cruxy to perform (omit for interactive)")
|
|
16
17
|
.option("--plan", "plan mode: propose a step-by-step plan for approval before executing")
|
|
17
18
|
.option("--sandbox", "run shell + test commands inside an isolated container (fails loud if no runtime)")
|
|
@@ -79,9 +80,18 @@ export function runCommand() {
|
|
|
79
80
|
if (sandbox) {
|
|
80
81
|
logger.info(t.muted(`sandbox: ${sandbox.runtimeName} (network ${config.sandbox.network})`));
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
+
// Hooks + custom slash commands (C.19). Built once per run: loads the
|
|
84
|
+
// layered catalog and yields the lifecycle runner (threaded into the
|
|
85
|
+
// session) + the resolved custom slash commands (given to the REPL).
|
|
86
|
+
const hooksService = await buildHooksService({
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
config,
|
|
89
|
+
interactive: Boolean(process.stdin.isTTY),
|
|
90
|
+
logger,
|
|
91
|
+
});
|
|
92
|
+
const session = buildAgentSession(config, apiKey, process.cwd(), Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox, hooksService.runner);
|
|
83
93
|
if (interactive) {
|
|
84
|
-
await runInteractive(session, undefined, renderer, checkpoints);
|
|
94
|
+
await runInteractive(session, undefined, renderer, checkpoints, hooksService.commands);
|
|
85
95
|
return;
|
|
86
96
|
}
|
|
87
97
|
checkpoints?.beginRun(prompt);
|
|
@@ -33,9 +33,9 @@ export function skillsCommand() {
|
|
|
33
33
|
resetSkillServices();
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
logger.print(`\n${t.
|
|
36
|
+
logger.print(`\n${t.heading("sources")} ${t.muted("(precedence, high to low)")}`);
|
|
37
37
|
for (const { source, dir } of status.sources) {
|
|
38
|
-
logger.print(` ${
|
|
38
|
+
logger.print(` ${t.kv(source, t.muted(dir), 8)}`);
|
|
39
39
|
}
|
|
40
40
|
logger.print("");
|
|
41
41
|
if (status.errors.length === 0) {
|
package/dist/cli/program.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { APP_NAME, APP_VERSION
|
|
2
|
+
import { APP_NAME, APP_VERSION } from "../constants.js";
|
|
3
|
+
import { PRODUCT_MASTHEAD } from "../brand/index.js";
|
|
3
4
|
import { logger } from "../utils/logger.js";
|
|
4
5
|
import { shouldUseColor, usageError } from "../errors/index.js";
|
|
5
6
|
import { themeForColor } from "../theme/index.js";
|
|
@@ -13,13 +14,14 @@ import { initCommand } from "./commands/init.js";
|
|
|
13
14
|
import { checkpointCommand } from "./commands/checkpoint.js";
|
|
14
15
|
import { rollbackCommand } from "./commands/rollback.js";
|
|
15
16
|
import { testCommand } from "./commands/test.js";
|
|
17
|
+
import { hooksCommand } from "./commands/hooks.js";
|
|
16
18
|
import { loadConfig } from "../config/index.js";
|
|
17
19
|
import { maybeRunOnboarding } from "./onboard.js";
|
|
18
20
|
export function buildProgram() {
|
|
19
21
|
const program = new Command();
|
|
20
22
|
program
|
|
21
23
|
.name(APP_NAME)
|
|
22
|
-
.description(
|
|
24
|
+
.description(PRODUCT_MASTHEAD)
|
|
23
25
|
.version(APP_VERSION, "-v, --version", "print the cruxy version")
|
|
24
26
|
.option("-c, --config <path>", "use a specific config file")
|
|
25
27
|
.option("--log-level <level>", "debug | info | warn | error | silent")
|
|
@@ -42,6 +44,7 @@ export function buildProgram() {
|
|
|
42
44
|
program.addCommand(checkpointCommand());
|
|
43
45
|
program.addCommand(rollbackCommand());
|
|
44
46
|
program.addCommand(testCommand());
|
|
47
|
+
program.addCommand(hooksCommand());
|
|
45
48
|
// Default action: bare `cruxy` -> entrypoint. An unrecognized first operand
|
|
46
49
|
// means an unknown command (Commander runs the default action with it as an
|
|
47
50
|
// operand rather than erroring), so reject it as a usage error.
|
package/dist/cli/repl.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
2
|
import type { Session } from "../agent/index.js";
|
|
3
3
|
import type { CheckpointService } from "../checkpoint/index.js";
|
|
4
|
+
import { type SlashCommandSpec } from "../hooks/index.js";
|
|
4
5
|
import { type StreamRenderer } from "../render/index.js";
|
|
5
6
|
/**
|
|
6
7
|
* The REPL's slash commands — the autocomplete candidate set (U.7). Keep in
|
|
@@ -23,4 +24,4 @@ export interface ReplIO {
|
|
|
23
24
|
* live region, anything else the plain append-only renderer); `cruxy run`
|
|
24
25
|
* passes its own so the approval prompt's status-suspend hook shares it.
|
|
25
26
|
*/
|
|
26
|
-
export declare function runInteractive(session: Session, io?: ReplIO, renderer?: StreamRenderer, checkpoints?: CheckpointService): Promise<void>;
|
|
27
|
+
export declare function runInteractive(session: Session, io?: ReplIO, renderer?: StreamRenderer, checkpoints?: CheckpointService, slashCommands?: readonly SlashCommandSpec[]): Promise<void>;
|
package/dist/cli/repl.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
import { makeReplCompleter } from "../components/index.js";
|
|
3
|
+
import { resolveSlash } from "../hooks/index.js";
|
|
4
|
+
import { runGatedShell } from "../tools/shell/exec.js";
|
|
3
5
|
import { themeForColor } from "../theme/index.js";
|
|
4
6
|
import { formatError, fromUnknown, isVerbose, shouldUseColor, } from "../errors/index.js";
|
|
5
7
|
import { createRenderer } from "../render/index.js";
|
|
@@ -70,6 +72,35 @@ function readLine(io, prompt) {
|
|
|
70
72
|
});
|
|
71
73
|
});
|
|
72
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Run a shell-bound custom slash command (C.19) through the SAME gated +
|
|
77
|
+
* sandboxed path as `run_command` (`runGatedShell` over the session's tool
|
|
78
|
+
* context) — never a privileged route. Prints the result like a shell run; a
|
|
79
|
+
* gate rejection or a thrown coded error (e.g. sandbox) is surfaced, not fatal.
|
|
80
|
+
*/
|
|
81
|
+
async function runSlashShell(spec, session) {
|
|
82
|
+
logger.print(theme.muted(`running /${spec.name}${theme.glyph.ellipsis}`));
|
|
83
|
+
try {
|
|
84
|
+
const outcome = await runGatedShell(spec.command ?? "", session.toolContext);
|
|
85
|
+
if (!outcome.approved) {
|
|
86
|
+
logger.print(theme.muted(outcome.rejection ?? "command denied by the user"));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const e = outcome.exec;
|
|
90
|
+
if (e.timedOut) {
|
|
91
|
+
logger.print(theme.warning("timed out"));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (e.spawnError !== undefined) {
|
|
95
|
+
logger.print(theme.danger(e.spawnError));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
logger.print(`exit code ${e.exitCode ?? e.signal ?? "unknown"}\n${e.output}`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
printReplError(err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
73
104
|
/**
|
|
74
105
|
* Render a non-fatal error inline (classified + formatted, the same 4-part
|
|
75
106
|
* shape as the fatal boundary) and return to the prompt — the REPL must survive
|
|
@@ -93,16 +124,16 @@ function printReplError(err) {
|
|
|
93
124
|
* live region, anything else the plain append-only renderer); `cruxy run`
|
|
94
125
|
* passes its own so the approval prompt's status-suspend hook shares it.
|
|
95
126
|
*/
|
|
96
|
-
export async function runInteractive(session, io = defaultIO(), renderer = createRenderer(io.output, process.stderr), checkpoints) {
|
|
127
|
+
export async function runInteractive(session, io = defaultIO(), renderer = createRenderer(io.output, process.stderr), checkpoints, slashCommands = []) {
|
|
97
128
|
logger.print(theme.muted("interactive session — /help for commands, /exit or Ctrl+D to quit"));
|
|
98
129
|
try {
|
|
99
|
-
await replLoop(session, io, renderer, checkpoints);
|
|
130
|
+
await replLoop(session, io, renderer, checkpoints, slashCommands);
|
|
100
131
|
}
|
|
101
132
|
finally {
|
|
102
133
|
renderer.close();
|
|
103
134
|
}
|
|
104
135
|
}
|
|
105
|
-
async function replLoop(session, io, renderer, checkpoints) {
|
|
136
|
+
async function replLoop(session, io, renderer, checkpoints, slashCommands = []) {
|
|
106
137
|
for (;;) {
|
|
107
138
|
const line = await readLine(io, PROMPT);
|
|
108
139
|
// EOF / Ctrl+D.
|
|
@@ -151,6 +182,26 @@ async function replLoop(session, io, renderer, checkpoints) {
|
|
|
151
182
|
logger.print(HELP);
|
|
152
183
|
continue;
|
|
153
184
|
}
|
|
185
|
+
// Custom slash commands (C.19) — consulted AFTER builtins, so a custom
|
|
186
|
+
// command can never shadow /help, /exit, etc. A `prompt` command expands to
|
|
187
|
+
// text fed to the agent (safe); a `shell` command runs through the SAME
|
|
188
|
+
// gate + sandbox as any command (never a bypass). Unknown "/…" input falls
|
|
189
|
+
// through to a normal turn, preserving prior behavior.
|
|
190
|
+
const slash = resolveSlash(trimmed, slashCommands);
|
|
191
|
+
if (slash.kind === "prompt") {
|
|
192
|
+
try {
|
|
193
|
+
checkpoints?.beginRun(slash.prompt);
|
|
194
|
+
await session.send(slash.prompt, renderer);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
printReplError(err);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (slash.kind === "shell") {
|
|
202
|
+
await runSlashShell(slash.spec, session);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
154
205
|
// A real turn. Assistant text streams through the renderer delta by delta
|
|
155
206
|
// (leading blank lines trimmed, code fences highlighted); the agent loop
|
|
156
207
|
// closes each segment with one newline, so the next prompt lands on its own
|
|
@@ -4,7 +4,7 @@ import type { CheckpointService } from "../checkpoint/index.js";
|
|
|
4
4
|
import type { SandboxService } from "../sandbox/index.js";
|
|
5
5
|
import type { StreamRenderer } from "../render/index.js";
|
|
6
6
|
import { type ApproveAction } from "../tools/index.js";
|
|
7
|
-
import { Session } from "../agent/index.js";
|
|
7
|
+
import { Session, type LifecycleHookRunner } from "../agent/index.js";
|
|
8
8
|
/**
|
|
9
9
|
* Wrap the approval gate with the C.32 auto-checkpoint hook. Ordering is the
|
|
10
10
|
* whole point: a tool mutates only *after* `requestApproval` resolves, so
|
|
@@ -24,4 +24,4 @@ export declare function withCheckpointGate(requestApproval: (action: ApproveActi
|
|
|
24
24
|
* over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
|
|
25
25
|
* approves → executes. Plan mode is fully opt-in; the default path is unchanged.
|
|
26
26
|
*/
|
|
27
|
-
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService, sandbox?: SandboxService): Session;
|
|
27
|
+
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService, sandbox?: SandboxService, hooks?: LifecycleHookRunner): Session;
|
|
@@ -5,8 +5,9 @@ import { getGitInfo } from "../utils/git.js";
|
|
|
5
5
|
import { ApprovalService, InteractivePolicy, SessionAllowlist, classify, defaultPromptIO, } from "../approval/index.js";
|
|
6
6
|
import { shouldUseColor } from "../errors/index.js";
|
|
7
7
|
import { buildDefaultRegistry } from "../tools/index.js";
|
|
8
|
-
import { Session } from "../agent/index.js";
|
|
8
|
+
import { Session, } from "../agent/index.js";
|
|
9
9
|
import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
|
|
10
|
+
import { routerForConfig } from "../routing/index.js";
|
|
10
11
|
import { SubagentOrchestrator, makeSpawnSubagentTool, } from "../subagent/index.js";
|
|
11
12
|
/**
|
|
12
13
|
* Wrap a PromptIO so the live region yields before any prompt text lands
|
|
@@ -87,7 +88,7 @@ export function withCheckpointGate(requestApproval, checkpoints, cwd) {
|
|
|
87
88
|
* over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
|
|
88
89
|
* approves → executes. Plan mode is fully opt-in; the default path is unchanged.
|
|
89
90
|
*/
|
|
90
|
-
export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false, renderer, checkpoints, sandbox) {
|
|
91
|
+
export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false, renderer, checkpoints, sandbox, hooks) {
|
|
91
92
|
const provider = createProvider({
|
|
92
93
|
provider: config.model.provider,
|
|
93
94
|
apiKey,
|
|
@@ -96,6 +97,10 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
96
97
|
temperature: config.model.temperature,
|
|
97
98
|
gatewayUrl: config.cruxy.gatewayUrl,
|
|
98
99
|
});
|
|
100
|
+
// Multi-model routing (C.30): null unless routing is configured on a cruxy
|
|
101
|
+
// session, so the default path threads `undefined` and behaves exactly as
|
|
102
|
+
// before. One router is shared by the main loop, subagents, and plan mode.
|
|
103
|
+
const router = routerForConfig(config) ?? undefined;
|
|
99
104
|
const execRegistry = buildDefaultRegistry();
|
|
100
105
|
const git = getGitInfo(cwd);
|
|
101
106
|
const projectInstructions = loadProjectInstructions(cwd);
|
|
@@ -114,6 +119,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
114
119
|
const orchestrator = new SubagentOrchestrator({
|
|
115
120
|
provider,
|
|
116
121
|
config,
|
|
122
|
+
router,
|
|
117
123
|
parentRegistry: execRegistry,
|
|
118
124
|
cwd,
|
|
119
125
|
logger,
|
|
@@ -156,6 +162,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
156
162
|
git,
|
|
157
163
|
projectInstructions,
|
|
158
164
|
renderer: turnRenderer,
|
|
165
|
+
router,
|
|
159
166
|
});
|
|
160
167
|
return new Session({
|
|
161
168
|
provider,
|
|
@@ -166,6 +173,8 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
166
173
|
projectInstructions,
|
|
167
174
|
planMode: true,
|
|
168
175
|
planRunner,
|
|
176
|
+
hooks,
|
|
177
|
+
router,
|
|
169
178
|
});
|
|
170
179
|
}
|
|
171
180
|
const approval = new ApprovalService({
|
|
@@ -181,5 +190,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
181
190
|
ctx,
|
|
182
191
|
git,
|
|
183
192
|
projectInstructions,
|
|
193
|
+
hooks,
|
|
194
|
+
router,
|
|
184
195
|
});
|
|
185
196
|
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -339,6 +339,53 @@ export declare const SandboxConfigSchema: z.ZodObject<{
|
|
|
339
339
|
mounts?: string[] | undefined;
|
|
340
340
|
}>;
|
|
341
341
|
export type SandboxConfig = z.infer<typeof SandboxConfigSchema>;
|
|
342
|
+
/**
|
|
343
|
+
* Hooks + custom slash commands (C.19). Hooks run user-authored shell commands
|
|
344
|
+
* on lifecycle events — a real execution surface — so they are OFF by default
|
|
345
|
+
* and, when on, still funnel through the SAME U.3 gate + C.16 sandbox as any
|
|
346
|
+
* agent command (never a bypass). The hook *definitions* live in layered
|
|
347
|
+
* `hooks.json` files (project > user, source-tracked for the trust model); this
|
|
348
|
+
* schema holds only the global toggles.
|
|
349
|
+
*/
|
|
350
|
+
export declare const HooksConfigSchema: z.ZodObject<{
|
|
351
|
+
/** Master switch. When false, NO hook ever fires (config with hooks stays
|
|
352
|
+
* inert until explicitly enabled). */
|
|
353
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
354
|
+
/**
|
|
355
|
+
* Whether to interactively prompt to trust a project's hooks the first time
|
|
356
|
+
* they would run. When false, untrusted project hooks fail loud
|
|
357
|
+
* (CRUXY_E_HOOK_UNTRUSTED) instead of prompting — never auto-trust.
|
|
358
|
+
*/
|
|
359
|
+
trustPrompt: z.ZodDefault<z.ZodBoolean>;
|
|
360
|
+
}, "strict", z.ZodTypeAny, {
|
|
361
|
+
enabled: boolean;
|
|
362
|
+
trustPrompt: boolean;
|
|
363
|
+
}, {
|
|
364
|
+
enabled?: boolean | undefined;
|
|
365
|
+
trustPrompt?: boolean | undefined;
|
|
366
|
+
}>;
|
|
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
|
+
}>;
|
|
342
389
|
/** MCP server entry — stdio or URL transport (wired up in a later phase). */
|
|
343
390
|
export declare const McpServerSchema: z.ZodObject<{
|
|
344
391
|
command: z.ZodOptional<z.ZodString>;
|
|
@@ -654,6 +701,36 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
654
701
|
timeout?: number | undefined;
|
|
655
702
|
mounts?: string[] | undefined;
|
|
656
703
|
}>>;
|
|
704
|
+
hooks: z.ZodDefault<z.ZodObject<{
|
|
705
|
+
/** Master switch. When false, NO hook ever fires (config with hooks stays
|
|
706
|
+
* inert until explicitly enabled). */
|
|
707
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
708
|
+
/**
|
|
709
|
+
* Whether to interactively prompt to trust a project's hooks the first time
|
|
710
|
+
* they would run. When false, untrusted project hooks fail loud
|
|
711
|
+
* (CRUXY_E_HOOK_UNTRUSTED) instead of prompting — never auto-trust.
|
|
712
|
+
*/
|
|
713
|
+
trustPrompt: z.ZodDefault<z.ZodBoolean>;
|
|
714
|
+
}, "strict", z.ZodTypeAny, {
|
|
715
|
+
enabled: boolean;
|
|
716
|
+
trustPrompt: boolean;
|
|
717
|
+
}, {
|
|
718
|
+
enabled?: boolean | undefined;
|
|
719
|
+
trustPrompt?: boolean | undefined;
|
|
720
|
+
}>>;
|
|
721
|
+
routing: z.ZodDefault<z.ZodObject<{
|
|
722
|
+
/** Tier for any task class not in `map`. Unset → the tier implied by
|
|
723
|
+
* `model.model` (a real tier, or the auto-fallback tier). */
|
|
724
|
+
default: z.ZodOptional<z.ZodEnum<["kavi", "vaani", "mira"]>>;
|
|
725
|
+
/** Per-task-class tier overrides; anything omitted takes `default`. */
|
|
726
|
+
map: z.ZodDefault<z.ZodRecord<z.ZodEnum<["main-turn", "subagent", "plan", "commit-msg", "classify", "summarize"]>, z.ZodEnum<["kavi", "vaani", "mira"]>>>;
|
|
727
|
+
}, "strict", z.ZodTypeAny, {
|
|
728
|
+
map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
|
|
729
|
+
default?: "kavi" | "vaani" | "mira" | undefined;
|
|
730
|
+
}, {
|
|
731
|
+
map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
|
|
732
|
+
default?: "kavi" | "vaani" | "mira" | undefined;
|
|
733
|
+
}>>;
|
|
657
734
|
mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
658
735
|
command: z.ZodOptional<z.ZodString>;
|
|
659
736
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -672,6 +749,31 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
672
749
|
cruxy: {
|
|
673
750
|
gatewayUrl: string;
|
|
674
751
|
};
|
|
752
|
+
checkpoint: {
|
|
753
|
+
enabled: boolean;
|
|
754
|
+
retention: number;
|
|
755
|
+
};
|
|
756
|
+
sandbox: {
|
|
757
|
+
image: string;
|
|
758
|
+
enabled: boolean;
|
|
759
|
+
memory: string;
|
|
760
|
+
network: "none" | "host-loopback" | "full";
|
|
761
|
+
pids: number;
|
|
762
|
+
cpus: number;
|
|
763
|
+
mounts: string[];
|
|
764
|
+
timeout?: number | undefined;
|
|
765
|
+
};
|
|
766
|
+
approval: {
|
|
767
|
+
mode: "prompt";
|
|
768
|
+
};
|
|
769
|
+
subagent: {
|
|
770
|
+
maxDepth: number;
|
|
771
|
+
defaultBudget: {
|
|
772
|
+
maxTokens: number;
|
|
773
|
+
maxIterations: number;
|
|
774
|
+
timeoutMs?: number | undefined;
|
|
775
|
+
};
|
|
776
|
+
};
|
|
675
777
|
model: {
|
|
676
778
|
provider: "cruxy" | "anthropic" | "openai" | "custom";
|
|
677
779
|
model: string;
|
|
@@ -702,9 +804,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
702
804
|
compactThreshold: number;
|
|
703
805
|
keepRecentMessages: number;
|
|
704
806
|
};
|
|
705
|
-
approval: {
|
|
706
|
-
mode: "prompt";
|
|
707
|
-
};
|
|
708
807
|
index: {
|
|
709
808
|
search: {
|
|
710
809
|
defaultK: number;
|
|
@@ -720,32 +819,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
720
819
|
overlapLines: number;
|
|
721
820
|
};
|
|
722
821
|
};
|
|
723
|
-
checkpoint: {
|
|
724
|
-
enabled: boolean;
|
|
725
|
-
retention: number;
|
|
726
|
-
};
|
|
727
|
-
subagent: {
|
|
728
|
-
maxDepth: number;
|
|
729
|
-
defaultBudget: {
|
|
730
|
-
maxTokens: number;
|
|
731
|
-
maxIterations: number;
|
|
732
|
-
timeoutMs?: number | undefined;
|
|
733
|
-
};
|
|
734
|
-
};
|
|
735
822
|
test: {
|
|
736
823
|
maxIterations: number;
|
|
737
824
|
captureBytes: number;
|
|
738
825
|
command?: string | undefined;
|
|
739
826
|
};
|
|
740
|
-
|
|
741
|
-
image: string;
|
|
827
|
+
hooks: {
|
|
742
828
|
enabled: boolean;
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
timeout?: number | undefined;
|
|
829
|
+
trustPrompt: boolean;
|
|
830
|
+
};
|
|
831
|
+
routing: {
|
|
832
|
+
map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
|
|
833
|
+
default?: "kavi" | "vaani" | "mira" | undefined;
|
|
749
834
|
};
|
|
750
835
|
mcpServers: Record<string, {
|
|
751
836
|
command?: string | undefined;
|
|
@@ -757,6 +842,31 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
757
842
|
cruxy?: {
|
|
758
843
|
gatewayUrl?: string | undefined;
|
|
759
844
|
} | undefined;
|
|
845
|
+
checkpoint?: {
|
|
846
|
+
enabled?: boolean | undefined;
|
|
847
|
+
retention?: number | undefined;
|
|
848
|
+
} | undefined;
|
|
849
|
+
sandbox?: {
|
|
850
|
+
image?: string | undefined;
|
|
851
|
+
enabled?: boolean | undefined;
|
|
852
|
+
memory?: string | undefined;
|
|
853
|
+
network?: "none" | "host-loopback" | "full" | undefined;
|
|
854
|
+
pids?: number | undefined;
|
|
855
|
+
cpus?: number | undefined;
|
|
856
|
+
timeout?: number | undefined;
|
|
857
|
+
mounts?: string[] | undefined;
|
|
858
|
+
} | undefined;
|
|
859
|
+
approval?: {
|
|
860
|
+
mode?: "prompt" | undefined;
|
|
861
|
+
} | undefined;
|
|
862
|
+
subagent?: {
|
|
863
|
+
maxDepth?: number | undefined;
|
|
864
|
+
defaultBudget?: {
|
|
865
|
+
maxTokens?: number | undefined;
|
|
866
|
+
maxIterations?: number | undefined;
|
|
867
|
+
timeoutMs?: number | undefined;
|
|
868
|
+
} | undefined;
|
|
869
|
+
} | undefined;
|
|
760
870
|
model?: {
|
|
761
871
|
provider?: "cruxy" | "anthropic" | "openai" | "custom" | undefined;
|
|
762
872
|
model?: string | undefined;
|
|
@@ -787,9 +897,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
787
897
|
compactThreshold?: number | undefined;
|
|
788
898
|
keepRecentMessages?: number | undefined;
|
|
789
899
|
} | undefined;
|
|
790
|
-
approval?: {
|
|
791
|
-
mode?: "prompt" | undefined;
|
|
792
|
-
} | undefined;
|
|
793
900
|
index?: {
|
|
794
901
|
search?: {
|
|
795
902
|
defaultK?: number | undefined;
|
|
@@ -805,32 +912,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
805
912
|
overlapLines?: number | undefined;
|
|
806
913
|
} | undefined;
|
|
807
914
|
} | undefined;
|
|
808
|
-
checkpoint?: {
|
|
809
|
-
enabled?: boolean | undefined;
|
|
810
|
-
retention?: number | undefined;
|
|
811
|
-
} | undefined;
|
|
812
|
-
subagent?: {
|
|
813
|
-
maxDepth?: number | undefined;
|
|
814
|
-
defaultBudget?: {
|
|
815
|
-
maxTokens?: number | undefined;
|
|
816
|
-
maxIterations?: number | undefined;
|
|
817
|
-
timeoutMs?: number | undefined;
|
|
818
|
-
} | undefined;
|
|
819
|
-
} | undefined;
|
|
820
915
|
test?: {
|
|
821
916
|
maxIterations?: number | undefined;
|
|
822
917
|
command?: string | undefined;
|
|
823
918
|
captureBytes?: number | undefined;
|
|
824
919
|
} | undefined;
|
|
825
|
-
|
|
826
|
-
image?: string | undefined;
|
|
920
|
+
hooks?: {
|
|
827
921
|
enabled?: boolean | undefined;
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
mounts?: string[] | undefined;
|
|
922
|
+
trustPrompt?: boolean | undefined;
|
|
923
|
+
} | undefined;
|
|
924
|
+
routing?: {
|
|
925
|
+
map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
|
|
926
|
+
default?: "kavi" | "vaani" | "mira" | undefined;
|
|
834
927
|
} | undefined;
|
|
835
928
|
mcpServers?: Record<string, {
|
|
836
929
|
command?: string | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -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",
|
|
@@ -233,6 +235,44 @@ export const SandboxConfigSchema = z
|
|
|
233
235
|
mounts: z.array(z.string().min(1)).default([]),
|
|
234
236
|
})
|
|
235
237
|
.strict();
|
|
238
|
+
/**
|
|
239
|
+
* Hooks + custom slash commands (C.19). Hooks run user-authored shell commands
|
|
240
|
+
* on lifecycle events — a real execution surface — so they are OFF by default
|
|
241
|
+
* and, when on, still funnel through the SAME U.3 gate + C.16 sandbox as any
|
|
242
|
+
* agent command (never a bypass). The hook *definitions* live in layered
|
|
243
|
+
* `hooks.json` files (project > user, source-tracked for the trust model); this
|
|
244
|
+
* schema holds only the global toggles.
|
|
245
|
+
*/
|
|
246
|
+
export const HooksConfigSchema = z
|
|
247
|
+
.object({
|
|
248
|
+
/** Master switch. When false, NO hook ever fires (config with hooks stays
|
|
249
|
+
* inert until explicitly enabled). */
|
|
250
|
+
enabled: z.boolean().default(false),
|
|
251
|
+
/**
|
|
252
|
+
* Whether to interactively prompt to trust a project's hooks the first time
|
|
253
|
+
* they would run. When false, untrusted project hooks fail loud
|
|
254
|
+
* (CRUXY_E_HOOK_UNTRUSTED) instead of prompting — never auto-trust.
|
|
255
|
+
*/
|
|
256
|
+
trustPrompt: z.boolean().default(true),
|
|
257
|
+
})
|
|
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();
|
|
236
276
|
/** MCP server entry — stdio or URL transport (wired up in a later phase). */
|
|
237
277
|
export const McpServerSchema = z
|
|
238
278
|
.object({
|
|
@@ -256,6 +296,8 @@ export const CruxyConfigSchema = z
|
|
|
256
296
|
subagent: SubagentConfigSchema.default({}),
|
|
257
297
|
test: TestConfigSchema.default({}),
|
|
258
298
|
sandbox: SandboxConfigSchema.default({}),
|
|
299
|
+
hooks: HooksConfigSchema.default({}),
|
|
300
|
+
routing: RoutingConfigSchema.default({}),
|
|
259
301
|
mcpServers: z.record(z.string(), McpServerSchema).default({}),
|
|
260
302
|
logLevel: z.enum(LOG_LEVELS).default("info"),
|
|
261
303
|
})
|
package/dist/constants.d.ts
CHANGED
|
@@ -21,6 +21,15 @@ export declare const PROJECT_INSTRUCTION_FILENAMES: string[];
|
|
|
21
21
|
* root for shipped builtins.
|
|
22
22
|
*/
|
|
23
23
|
export declare const SKILLS_DIR_NAME = "skills";
|
|
24
|
+
/** Hook definitions file, under the project dir (`<cwd>/.cruxy/hooks.json`) and
|
|
25
|
+
* the global dir (`~/.cruxy/hooks.json`). Pure data, validated, never eval'd (C.19). */
|
|
26
|
+
export declare const HOOKS_FILE_NAME = "hooks.json";
|
|
27
|
+
/** Custom slash-command directory (`<cwd>/.cruxy/commands`, `~/.cruxy/commands`);
|
|
28
|
+
* one `<name>.md` per command (frontmatter + template body), mirroring skills. */
|
|
29
|
+
export declare const COMMANDS_DIR_NAME = "commands";
|
|
30
|
+
/** Per-repo hook-trust record, in the GLOBAL dir only (`~/.cruxy/trust.json`) —
|
|
31
|
+
* never in a repo, so cloning carries no trust (C.19 supply-chain safety). */
|
|
32
|
+
export declare const TRUST_FILE_NAME = "trust.json";
|
|
24
33
|
/**
|
|
25
34
|
* Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
|
|
26
35
|
* Anchored the same way as the package.json lookup above: both `dist/` and
|
package/dist/constants.js
CHANGED
|
@@ -41,6 +41,15 @@ export const PROJECT_INSTRUCTION_FILENAMES = ["CRUXY.md", "AGENTS.md"];
|
|
|
41
41
|
* root for shipped builtins.
|
|
42
42
|
*/
|
|
43
43
|
export const SKILLS_DIR_NAME = "skills";
|
|
44
|
+
/** Hook definitions file, under the project dir (`<cwd>/.cruxy/hooks.json`) and
|
|
45
|
+
* the global dir (`~/.cruxy/hooks.json`). Pure data, validated, never eval'd (C.19). */
|
|
46
|
+
export const HOOKS_FILE_NAME = "hooks.json";
|
|
47
|
+
/** Custom slash-command directory (`<cwd>/.cruxy/commands`, `~/.cruxy/commands`);
|
|
48
|
+
* one `<name>.md` per command (frontmatter + template body), mirroring skills. */
|
|
49
|
+
export const COMMANDS_DIR_NAME = "commands";
|
|
50
|
+
/** Per-repo hook-trust record, in the GLOBAL dir only (`~/.cruxy/trust.json`) —
|
|
51
|
+
* never in a repo, so cloning carries no trust (C.19 supply-chain safety). */
|
|
52
|
+
export const TRUST_FILE_NAME = "trust.json";
|
|
44
53
|
/**
|
|
45
54
|
* Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
|
|
46
55
|
* 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;
|
|
@@ -46,6 +55,22 @@ export declare function sandboxImage(image: string, underlying?: unknown): Cruxy
|
|
|
46
55
|
* can't launch is never papered over with a host run.
|
|
47
56
|
*/
|
|
48
57
|
export declare function sandboxExec(underlying?: unknown): CruxyError;
|
|
58
|
+
/**
|
|
59
|
+
* A blocking hook failed (non-zero exit, gate-declined, or errored). The action
|
|
60
|
+
* that fired it is aborted fail-closed — a failing pre-check must stop, never
|
|
61
|
+
* proceed. Advisory hooks report but never raise this.
|
|
62
|
+
*/
|
|
63
|
+
export declare function hookFailed(name: string, reason: string, underlying?: unknown): CruxyError;
|
|
64
|
+
/**
|
|
65
|
+
* A project (a cloned/opened repo) defines hooks that have not been trusted for
|
|
66
|
+
* this machine. Cruxy NEVER runs another author's hooks silently — trust is an
|
|
67
|
+
* explicit, per-repo decision recorded in your home, not in the repo.
|
|
68
|
+
*/
|
|
69
|
+
export declare function hookUntrusted(root: string, count: number): CruxyError;
|
|
70
|
+
/** A malformed hook definition — excluded from the catalog, never eval'd. */
|
|
71
|
+
export declare function hookInvalid(reason: string, source?: string): CruxyError;
|
|
72
|
+
/** A malformed custom slash-command definition — excluded and surfaced. */
|
|
73
|
+
export declare function slashInvalid(reason: string, source?: string): CruxyError;
|
|
49
74
|
/**
|
|
50
75
|
* A side-effecting action needs approval but cruxy can't ask (non-interactive,
|
|
51
76
|
* no policy). Default-deny — never auto-approve. A distinct exit code (10) so CI
|