@cruxy/cli 0.4.0 → 0.7.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 (105) hide show
  1. package/README.md +57 -16
  2. package/dist/agent/index.d.ts +0 -1
  3. package/dist/agent/index.js +0 -1
  4. package/dist/agent/loop.d.ts +2 -0
  5. package/dist/agent/loop.js +1 -1
  6. package/dist/agent/prompts.d.ts +2 -2
  7. package/dist/agent/prompts.js +9 -3
  8. package/dist/agent/session.d.ts +24 -0
  9. package/dist/agent/session.js +33 -7
  10. package/dist/approval/classify.d.ts +18 -0
  11. package/dist/approval/classify.js +162 -0
  12. package/dist/approval/index.d.ts +5 -0
  13. package/dist/approval/index.js +5 -0
  14. package/dist/approval/policy.d.ts +37 -0
  15. package/dist/approval/policy.js +81 -0
  16. package/dist/approval/prompt.d.ts +33 -0
  17. package/dist/approval/prompt.js +212 -0
  18. package/dist/approval/service.d.ts +36 -0
  19. package/dist/approval/service.js +37 -0
  20. package/dist/approval/types.d.ts +64 -0
  21. package/dist/approval/types.js +1 -0
  22. package/dist/cli/commands/init.d.ts +7 -0
  23. package/dist/cli/commands/init.js +40 -0
  24. package/dist/cli/commands/login.d.ts +8 -0
  25. package/dist/cli/commands/login.js +36 -0
  26. package/dist/cli/commands/pr.d.ts +8 -0
  27. package/dist/cli/commands/pr.js +87 -0
  28. package/dist/cli/commands/run.js +30 -52
  29. package/dist/cli/onboard.d.ts +25 -0
  30. package/dist/cli/onboard.js +54 -0
  31. package/dist/cli/program.js +21 -1
  32. package/dist/cli/repl.js +10 -1
  33. package/dist/cli/session-factory.d.ts +12 -0
  34. package/dist/cli/session-factory.js +88 -0
  35. package/dist/config/credentials.d.ts +10 -0
  36. package/dist/config/credentials.js +69 -0
  37. package/dist/config/index.d.ts +1 -0
  38. package/dist/config/index.js +1 -0
  39. package/dist/config/manager.d.ts +6 -1
  40. package/dist/config/manager.js +11 -1
  41. package/dist/config/schema.d.ts +48 -9
  42. package/dist/config/schema.js +15 -4
  43. package/dist/constants.d.ts +6 -0
  44. package/dist/constants.js +6 -0
  45. package/dist/errors/constructors.d.ts +35 -0
  46. package/dist/errors/constructors.js +129 -2
  47. package/dist/errors/types.d.ts +8 -0
  48. package/dist/errors/types.js +17 -0
  49. package/dist/onboarding/detect.d.ts +26 -0
  50. package/dist/onboarding/detect.js +56 -0
  51. package/dist/onboarding/flow.d.ts +28 -0
  52. package/dist/onboarding/flow.js +100 -0
  53. package/dist/onboarding/index.d.ts +5 -0
  54. package/dist/onboarding/index.js +5 -0
  55. package/dist/onboarding/io.d.ts +8 -0
  56. package/dist/onboarding/io.js +133 -0
  57. package/dist/onboarding/steps.d.ts +17 -0
  58. package/dist/onboarding/steps.js +100 -0
  59. package/dist/onboarding/types.d.ts +81 -0
  60. package/dist/onboarding/types.js +6 -0
  61. package/dist/plan/approve.d.ts +16 -0
  62. package/dist/plan/approve.js +46 -0
  63. package/dist/plan/execute.d.ts +20 -0
  64. package/dist/plan/execute.js +31 -0
  65. package/dist/plan/index.d.ts +7 -0
  66. package/dist/plan/index.js +7 -0
  67. package/dist/plan/policy.d.ts +26 -0
  68. package/dist/plan/policy.js +45 -0
  69. package/dist/plan/render.d.ts +5 -0
  70. package/dist/plan/render.js +47 -0
  71. package/dist/plan/service.d.ts +39 -0
  72. package/dist/plan/service.js +118 -0
  73. package/dist/plan/submit-plan.d.ts +33 -0
  74. package/dist/plan/submit-plan.js +57 -0
  75. package/dist/plan/types.d.ts +60 -0
  76. package/dist/plan/types.js +6 -0
  77. package/dist/tools/create-pull-request.d.ts +24 -0
  78. package/dist/tools/create-pull-request.js +83 -0
  79. package/dist/tools/file/apply-patch.js +3 -3
  80. package/dist/tools/file/edit-file.js +6 -3
  81. package/dist/tools/file/write-file.js +6 -3
  82. package/dist/tools/index.d.ts +1 -0
  83. package/dist/tools/index.js +1 -0
  84. package/dist/tools/registry.js +2 -0
  85. package/dist/tools/shell/run-command.js +11 -3
  86. package/dist/tools/types.d.ts +25 -6
  87. package/dist/vcs/auth.d.ts +22 -0
  88. package/dist/vcs/auth.js +29 -0
  89. package/dist/vcs/generate.d.ts +72 -0
  90. package/dist/vcs/generate.js +265 -0
  91. package/dist/vcs/git.d.ts +52 -0
  92. package/dist/vcs/git.js +152 -0
  93. package/dist/vcs/github.d.ts +44 -0
  94. package/dist/vcs/github.js +145 -0
  95. package/dist/vcs/guidance.d.ts +20 -0
  96. package/dist/vcs/guidance.js +76 -0
  97. package/dist/vcs/index.d.ts +7 -0
  98. package/dist/vcs/index.js +7 -0
  99. package/dist/vcs/service.d.ts +53 -0
  100. package/dist/vcs/service.js +79 -0
  101. package/dist/vcs/types.d.ts +57 -0
  102. package/dist/vcs/types.js +6 -0
  103. package/package.json +1 -1
  104. package/dist/agent/approval.d.ts +0 -41
  105. package/dist/agent/approval.js +0 -179
@@ -0,0 +1,25 @@
1
+ import type { CruxyConfig } from "../config/index.js";
2
+ import { type OnboardingResult } from "../onboarding/index.js";
3
+ /**
4
+ * CLI-layer glue between the entry points and the onboarding module (U.6). Keeps
5
+ * `run`/`program`/`login`/`init` thin: they decide *when* to onboard; this builds
6
+ * the production wiring (live validation, credential persistence, first-win run).
7
+ */
8
+ /** The env var that holds the key for a provider (for the fail-loud message). */
9
+ export declare function apiKeyEnvVar(provider: string): string;
10
+ /** Run one first-win task end-to-end against the just-saved key. */
11
+ export declare function runFirstWinTask(config: CruxyConfig, cwd: string, prompt: string): Promise<void>;
12
+ export interface MaybeOnboardOptions {
13
+ /** Whether stdin is a TTY (the gate). */
14
+ ttyInteractive: boolean;
15
+ /** Offer the first-win demo run (no real task queued). */
16
+ offerFirstWin: boolean;
17
+ cwd: string;
18
+ }
19
+ /**
20
+ * Run the guided first-run flow **iff** this is a first run; otherwise return
21
+ * `null` so the caller can fall back (fail loud, or show a banner). Never throws
22
+ * for an aborted setup — `result.completed` is `false` and the caller exits
23
+ * cleanly.
24
+ */
25
+ export declare function maybeRunOnboarding(config: CruxyConfig, opts: MaybeOnboardOptions): Promise<OnboardingResult> | null;
@@ -0,0 +1,54 @@
1
+ import { resolveApiKey } from "../config/index.js";
2
+ import { createDefaultDeps, defaultOnboardingIO, isFirstRun, runOnboarding, } from "../onboarding/index.js";
3
+ import { createStreamPrinter } from "./stream-print.js";
4
+ import { buildAgentSession } from "./session-factory.js";
5
+ /**
6
+ * CLI-layer glue between the entry points and the onboarding module (U.6). Keeps
7
+ * `run`/`program`/`login`/`init` thin: they decide *when* to onboard; this builds
8
+ * the production wiring (live validation, credential persistence, first-win run).
9
+ */
10
+ /** The env var that holds the key for a provider (for the fail-loud message). */
11
+ export function apiKeyEnvVar(provider) {
12
+ switch (provider) {
13
+ case "anthropic":
14
+ return "ANTHROPIC_API_KEY";
15
+ case "openai":
16
+ return "OPENAI_API_KEY";
17
+ default:
18
+ return "CRUXY_API_KEY";
19
+ }
20
+ }
21
+ /** Run one first-win task end-to-end against the just-saved key. */
22
+ export async function runFirstWinTask(config, cwd, prompt) {
23
+ const apiKey = resolveApiKey(config.model.provider);
24
+ if (!apiKey)
25
+ return; // defensive — the key was just persisted
26
+ const session = buildAgentSession(config, apiKey, cwd, true);
27
+ const print = createStreamPrinter((text) => process.stdout.write(text));
28
+ await session.send(prompt, print);
29
+ }
30
+ /**
31
+ * Run the guided first-run flow **iff** this is a first run; otherwise return
32
+ * `null` so the caller can fall back (fail loud, or show a banner). Never throws
33
+ * for an aborted setup — `result.completed` is `false` and the caller exits
34
+ * cleanly.
35
+ */
36
+ export function maybeRunOnboarding(config, opts) {
37
+ const provider = config.model.provider;
38
+ if (!isFirstRun({ provider, interactive: opts.ttyInteractive }))
39
+ return null;
40
+ const deps = createDefaultDeps({
41
+ config,
42
+ cwd: opts.cwd,
43
+ runTask: opts.offerFirstWin
44
+ ? (prompt) => runFirstWinTask(config, opts.cwd, prompt)
45
+ : undefined,
46
+ });
47
+ return runOnboarding({
48
+ provider,
49
+ mode: "first-run",
50
+ offerFirstWin: opts.offerFirstWin,
51
+ io: defaultOnboardingIO(),
52
+ deps,
53
+ });
54
+ }
@@ -7,6 +7,11 @@ import { runCommand } from "./commands/run.js";
7
7
  import { configCommand } from "./commands/config.js";
8
8
  import { indexCommand } from "./commands/index.js";
9
9
  import { skillsCommand } from "./commands/skills.js";
10
+ import { prCommand } from "./commands/pr.js";
11
+ import { loginCommand } from "./commands/login.js";
12
+ import { initCommand } from "./commands/init.js";
13
+ import { loadConfig } from "../config/index.js";
14
+ import { maybeRunOnboarding } from "./onboard.js";
10
15
  export function buildProgram() {
11
16
  const program = new Command();
12
17
  program
@@ -28,15 +33,30 @@ export function buildProgram() {
28
33
  program.addCommand(configCommand());
29
34
  program.addCommand(indexCommand());
30
35
  program.addCommand(skillsCommand());
36
+ program.addCommand(prCommand());
37
+ program.addCommand(loginCommand());
38
+ program.addCommand(initCommand());
31
39
  // Default action: bare `cruxy` -> entrypoint. An unrecognized first operand
32
40
  // means an unknown command (Commander runs the default action with it as an
33
41
  // operand rather than erroring), so reject it as a usage error.
34
- program.action((_opts, command) => {
42
+ program.action(async (_opts, command) => {
35
43
  if (command.args.length > 0) {
36
44
  throw usageError(`unknown command: ${command.args[0]}`, [
37
45
  "run `cruxy --help` to see available commands",
38
46
  ]);
39
47
  }
48
+ // First-run with no key (and a TTY) → guided setup, with the first-win demo.
49
+ // Otherwise fall through to the banner (an already-set-up user, or non-TTY).
50
+ const { config } = loadConfig();
51
+ const onboarding = maybeRunOnboarding(config, {
52
+ ttyInteractive: Boolean(process.stdin.isTTY),
53
+ offerFirstWin: true,
54
+ cwd: process.cwd(),
55
+ });
56
+ if (onboarding) {
57
+ await onboarding;
58
+ return;
59
+ }
40
60
  logger.print(pc.cyan(`${APP_NAME} v${APP_VERSION}`));
41
61
  logger.print(pc.dim("an agentic coding CLI\n"));
42
62
  logger.print("The interactive REPL lands in C.3 (terminal UI).");
package/dist/cli/repl.js CHANGED
@@ -9,6 +9,7 @@ const HELP = `Commands:
9
9
  /clear clear the conversation history (keep the session)
10
10
  /compact summarize older history to free up context now
11
11
  /reload re-read project instructions (CRUXY.md)
12
+ /plan toggle plan mode (propose a plan before executing)
12
13
  /exit, /quit leave cruxy
13
14
  Ctrl+D leave cruxy`;
14
15
  const defaultIO = () => ({
@@ -18,7 +19,7 @@ const defaultIO = () => ({
18
19
  /**
19
20
  * Read one line using a readline interface that is created and then **closed
20
21
  * before this resolves**. This is the crux of the stdin coordination: while a
21
- * turn runs (`session.send`), approvals grab stdin in raw mode via the Approver
22
+ * turn runs (`session.send`), approvals grab stdin in raw mode via the approval prompt
22
23
  * — so no readline interface may be live at that moment. Creating a fresh
23
24
  * interface per line, and closing it the instant we have input, guarantees the
24
25
  * two never contend.
@@ -107,6 +108,14 @@ export async function runInteractive(session, io = defaultIO()) {
107
108
  : "no project instructions found"));
108
109
  continue;
109
110
  }
111
+ if (trimmed === "/plan") {
112
+ session.setPlanMode(!session.getPlanMode());
113
+ const on = session.getPlanMode();
114
+ logger.print(pc.dim(on
115
+ ? "plan mode on — the next prompt proposes a plan for approval"
116
+ : "plan mode off"));
117
+ continue;
118
+ }
110
119
  if (trimmed === "/help") {
111
120
  logger.print(HELP);
112
121
  continue;
@@ -0,0 +1,12 @@
1
+ import type { CruxyConfig } from "../config/index.js";
2
+ import { Session } from "../agent/index.js";
3
+ /**
4
+ * Build a ready-to-run agent {@link Session} from a resolved key — the wiring
5
+ * shared by `cruxy run` and the onboarding first-win task (so they can't drift).
6
+ * The approval gate's interactivity tracks the TTY, exactly as in `run`.
7
+ *
8
+ * When `planMode` is on (C.31), it additionally wires a {@link PlanExecutionPolicy}
9
+ * over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
10
+ * approves → executes. Plan mode is fully opt-in; the default path is unchanged.
11
+ */
12
+ export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean): Session;
@@ -0,0 +1,88 @@
1
+ import { createProvider } from "@cruxy/sdk";
2
+ import { loadProjectInstructions } from "../config/index.js";
3
+ import { logger } from "../utils/logger.js";
4
+ import { getGitInfo } from "../utils/git.js";
5
+ import { ApprovalService, InteractivePolicy, SessionAllowlist, defaultPromptIO, } from "../approval/index.js";
6
+ import { shouldUseColor } from "../errors/index.js";
7
+ import { buildDefaultRegistry } from "../tools/index.js";
8
+ import { Session } from "../agent/index.js";
9
+ import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
10
+ /**
11
+ * Build a ready-to-run agent {@link Session} from a resolved key — the wiring
12
+ * shared by `cruxy run` and the onboarding first-win task (so they can't drift).
13
+ * The approval gate's interactivity tracks the TTY, exactly as in `run`.
14
+ *
15
+ * When `planMode` is on (C.31), it additionally wires a {@link PlanExecutionPolicy}
16
+ * over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
17
+ * approves → executes. Plan mode is fully opt-in; the default path is unchanged.
18
+ */
19
+ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false) {
20
+ const provider = createProvider({
21
+ provider: config.model.provider,
22
+ apiKey,
23
+ model: config.model.model,
24
+ maxTokens: config.model.maxTokens,
25
+ temperature: config.model.temperature,
26
+ gatewayUrl: config.cruxy.gatewayUrl,
27
+ });
28
+ const execRegistry = buildDefaultRegistry();
29
+ const git = getGitInfo(cwd);
30
+ const projectInstructions = loadProjectInstructions(cwd);
31
+ if (planMode) {
32
+ // One io + allowlist shared by the plan-approval prompt and the per-action
33
+ // gate, so a grant recorded during execution is honored by U.3's own check.
34
+ const io = defaultPromptIO(shouldUseColor());
35
+ const allowlist = new SessionAllowlist();
36
+ const planPolicy = new PlanExecutionPolicy(allowlist, new InteractivePolicy(allowlist, io));
37
+ const approval = new ApprovalService({
38
+ cwd,
39
+ interactive: ttyInteractive,
40
+ policy: planPolicy,
41
+ io,
42
+ });
43
+ const ctx = {
44
+ cwd,
45
+ config,
46
+ logger,
47
+ requestApproval: (action) => approval.requestApproval(action),
48
+ };
49
+ const planRunner = ({ messages, projectInstructions, onText, }) => runPlanSession({
50
+ provider,
51
+ config,
52
+ ctx,
53
+ execRegistry,
54
+ planPolicy,
55
+ io,
56
+ interactive: ttyInteractive,
57
+ messages,
58
+ git,
59
+ projectInstructions,
60
+ onText,
61
+ });
62
+ return new Session({
63
+ provider,
64
+ registry: execRegistry,
65
+ config,
66
+ ctx,
67
+ git,
68
+ projectInstructions,
69
+ planMode: true,
70
+ planRunner,
71
+ });
72
+ }
73
+ const approval = new ApprovalService({ cwd, interactive: ttyInteractive });
74
+ const ctx = {
75
+ cwd,
76
+ config,
77
+ logger,
78
+ requestApproval: (action) => approval.requestApproval(action),
79
+ };
80
+ return new Session({
81
+ provider,
82
+ registry: execRegistry,
83
+ config,
84
+ ctx,
85
+ git,
86
+ projectInstructions,
87
+ });
88
+ }
@@ -0,0 +1,10 @@
1
+ /** `~/.cruxy/credentials.json` */
2
+ export declare function credentialsPath(): string;
3
+ /** The stored key for `provider`, or `undefined`. Never throws. */
4
+ export declare function readCredential(provider: string, file?: string): string | undefined;
5
+ /**
6
+ * Persist `key` for `provider`, merging into any existing store. The file is
7
+ * written `0600` and its directory `0700` so the secret is owner-only — enforced
8
+ * with an explicit `chmod` after write (mkdir/write modes are umask-masked).
9
+ */
10
+ export declare function writeCredential(provider: string, key: string, file?: string): void;
@@ -0,0 +1,69 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { CREDENTIALS_FILE_NAME } from "../constants.js";
4
+ import { globalDir } from "./paths.js";
5
+ /**
6
+ * The credentials store (U.6) — the one place a provider API key is persisted.
7
+ * It lives **outside** `config.json` on purpose: config is secret-free by design
8
+ * ("the API key comes from env"), so secrets get their own file with restrictive
9
+ * permissions (`0600`, dir `0700`), the same shape as gh/aws/npm. `resolveApiKey`
10
+ * reads it as a fallback after the environment.
11
+ */
12
+ /** Bumped if the on-disk shape ever changes. */
13
+ const CREDENTIALS_VERSION = 1;
14
+ /** `~/.cruxy/credentials.json` */
15
+ export function credentialsPath() {
16
+ return join(globalDir(), CREDENTIALS_FILE_NAME);
17
+ }
18
+ /** Parse the store at `file`, or `null` if absent/unreadable/malformed. */
19
+ function readStore(file) {
20
+ if (!existsSync(file))
21
+ return null;
22
+ try {
23
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
24
+ if (parsed &&
25
+ typeof parsed === "object" &&
26
+ "keys" in parsed &&
27
+ typeof parsed.keys === "object") {
28
+ return parsed;
29
+ }
30
+ }
31
+ catch {
32
+ // A corrupt store is treated as absent — onboarding can rewrite it.
33
+ }
34
+ return null;
35
+ }
36
+ /** The stored key for `provider`, or `undefined`. Never throws. */
37
+ export function readCredential(provider, file = credentialsPath()) {
38
+ const store = readStore(file);
39
+ const key = store?.keys[provider];
40
+ return typeof key === "string" && key !== "" ? key : undefined;
41
+ }
42
+ /**
43
+ * Persist `key` for `provider`, merging into any existing store. The file is
44
+ * written `0600` and its directory `0700` so the secret is owner-only — enforced
45
+ * with an explicit `chmod` after write (mkdir/write modes are umask-masked).
46
+ */
47
+ export function writeCredential(provider, key, file = credentialsPath()) {
48
+ const dir = dirname(file);
49
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
50
+ try {
51
+ chmodSync(dir, 0o700);
52
+ }
53
+ catch {
54
+ // Best-effort on platforms without POSIX modes (e.g. Windows).
55
+ }
56
+ const store = readStore(file) ?? { version: CREDENTIALS_VERSION, keys: {} };
57
+ store.version = CREDENTIALS_VERSION;
58
+ store.keys[provider] = key;
59
+ writeFileSync(file, JSON.stringify(store, null, 2) + "\n", {
60
+ encoding: "utf8",
61
+ mode: 0o600,
62
+ });
63
+ try {
64
+ chmodSync(file, 0o600);
65
+ }
66
+ catch {
67
+ // Best-effort (see above).
68
+ }
69
+ }
@@ -2,3 +2,4 @@ export * from "./schema.js";
2
2
  export * from "./paths.js";
3
3
  export * from "./manager.js";
4
4
  export * from "./project.js";
5
+ export * from "./credentials.js";
@@ -2,3 +2,4 @@ export * from "./schema.js";
2
2
  export * from "./paths.js";
3
3
  export * from "./manager.js";
4
4
  export * from "./project.js";
5
+ export * from "./credentials.js";
@@ -30,5 +30,10 @@ export declare function initConfig(file: string): {
30
30
  path: string;
31
31
  created: boolean;
32
32
  };
33
- /** Provider API key, read from the environment only (never persisted). */
33
+ /**
34
+ * Provider API key, resolved **env → credentials store → undefined**. The
35
+ * environment always wins (CI / one-off overrides); the credentials store
36
+ * (`~/.cruxy/credentials.json`, written by onboarding / `cruxy login`) is the
37
+ * persistent fallback. The key is never read from `config.json`.
38
+ */
34
39
  export declare function resolveApiKey(provider: string): string | undefined;
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
3
3
  import { configInvalid, configParse } from "../errors/index.js";
4
4
  import { CruxyConfigSchema } from "./schema.js";
5
5
  import { globalConfigPath, findProjectConfig } from "./paths.js";
6
+ import { readCredential } from "./credentials.js";
6
7
  function isPlainObject(v) {
7
8
  return typeof v === "object" && v !== null && !Array.isArray(v);
8
9
  }
@@ -137,8 +138,17 @@ export function initConfig(file) {
137
138
  writeFileSync(file, JSON.stringify(defaults, null, 2) + "\n", "utf8");
138
139
  return { path: file, created: true };
139
140
  }
140
- /** Provider API key, read from the environment only (never persisted). */
141
+ /**
142
+ * Provider API key, resolved **env → credentials store → undefined**. The
143
+ * environment always wins (CI / one-off overrides); the credentials store
144
+ * (`~/.cruxy/credentials.json`, written by onboarding / `cruxy login`) is the
145
+ * persistent fallback. The key is never read from `config.json`.
146
+ */
141
147
  export function resolveApiKey(provider) {
148
+ return envApiKey(provider) ?? readCredential(provider);
149
+ }
150
+ /** The API key from the environment for `provider`, or `undefined`. */
151
+ function envApiKey(provider) {
142
152
  switch (provider) {
143
153
  case "cruxy":
144
154
  return process.env.CRUXY_API_KEY;
@@ -30,12 +30,16 @@ export declare const AgentConfigSchema: z.ZodObject<{
30
30
  maxIterations: z.ZodDefault<z.ZodNumber>;
31
31
  /** Skip per-action confirmation prompts. */
32
32
  autoApprove: z.ZodDefault<z.ZodBoolean>;
33
+ /** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
34
+ planMode: z.ZodDefault<z.ZodBoolean>;
33
35
  }, "strict", z.ZodTypeAny, {
34
36
  maxIterations: number;
35
37
  autoApprove: boolean;
38
+ planMode: boolean;
36
39
  }, {
37
40
  maxIterations?: number | undefined;
38
41
  autoApprove?: boolean | undefined;
42
+ planMode?: boolean | undefined;
39
43
  }>;
40
44
  export declare const ToolsConfigSchema: z.ZodObject<{
41
45
  fileEdit: z.ZodDefault<z.ZodBoolean>;
@@ -52,10 +56,20 @@ export declare const ToolsConfigSchema: z.ZodObject<{
52
56
  }>;
53
57
  export declare const GitConfigSchema: z.ZodObject<{
54
58
  autoCommit: z.ZodDefault<z.ZodBoolean>;
59
+ /** Branches cruxy never commits/pushes to directly (PR flow branches off
60
+ * first). `main` and `master` are always protected; these add to them. */
61
+ protectedBranches: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
62
+ /** Default base branch for generated pull requests; falls back to the
63
+ * repo's default branch, then `main`, when unset. */
64
+ defaultBase: z.ZodOptional<z.ZodString>;
55
65
  }, "strict", z.ZodTypeAny, {
56
66
  autoCommit: boolean;
67
+ protectedBranches: string[];
68
+ defaultBase?: string | undefined;
57
69
  }, {
58
70
  autoCommit?: boolean | undefined;
71
+ protectedBranches?: string[] | undefined;
72
+ defaultBase?: string | undefined;
59
73
  }>;
60
74
  /** Execution bounds for the `run_command` shell tool (distinct from the
61
75
  * `tools.shell` enable flag above). */
@@ -89,13 +103,18 @@ export declare const ContextConfigSchema: z.ZodObject<{
89
103
  compactThreshold?: number | undefined;
90
104
  keepRecentMessages?: number | undefined;
91
105
  }>;
92
- /** How tool-action approval is resolved. */
106
+ /**
107
+ * How tool-action approval is resolved. Only `prompt` exists: ask interactively
108
+ * and **deny by default** when non-interactive. There is deliberately no
109
+ * auto-approve / skip mode — the policy seam for a future CI mode lives in
110
+ * `src/approval` (see U.3), not behind a footgun flag.
111
+ */
93
112
  export declare const ApprovalConfigSchema: z.ZodObject<{
94
- mode: z.ZodDefault<z.ZodEnum<["prompt", "auto"]>>;
113
+ mode: z.ZodDefault<z.ZodEnum<["prompt"]>>;
95
114
  }, "strict", z.ZodTypeAny, {
96
- mode: "auto" | "prompt";
115
+ mode: "prompt";
97
116
  }, {
98
- mode?: "auto" | "prompt" | undefined;
117
+ mode?: "prompt" | undefined;
99
118
  }>;
100
119
  /**
101
120
  * Codebase semantic index (C.17): the local embedding index that backs the
@@ -227,12 +246,16 @@ export declare const CruxyConfigSchema: z.ZodObject<{
227
246
  maxIterations: z.ZodDefault<z.ZodNumber>;
228
247
  /** Skip per-action confirmation prompts. */
229
248
  autoApprove: z.ZodDefault<z.ZodBoolean>;
249
+ /** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
250
+ planMode: z.ZodDefault<z.ZodBoolean>;
230
251
  }, "strict", z.ZodTypeAny, {
231
252
  maxIterations: number;
232
253
  autoApprove: boolean;
254
+ planMode: boolean;
233
255
  }, {
234
256
  maxIterations?: number | undefined;
235
257
  autoApprove?: boolean | undefined;
258
+ planMode?: boolean | undefined;
236
259
  }>>;
237
260
  tools: z.ZodDefault<z.ZodObject<{
238
261
  fileEdit: z.ZodDefault<z.ZodBoolean>;
@@ -249,10 +272,20 @@ export declare const CruxyConfigSchema: z.ZodObject<{
249
272
  }>>;
250
273
  git: z.ZodDefault<z.ZodObject<{
251
274
  autoCommit: z.ZodDefault<z.ZodBoolean>;
275
+ /** Branches cruxy never commits/pushes to directly (PR flow branches off
276
+ * first). `main` and `master` are always protected; these add to them. */
277
+ protectedBranches: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
278
+ /** Default base branch for generated pull requests; falls back to the
279
+ * repo's default branch, then `main`, when unset. */
280
+ defaultBase: z.ZodOptional<z.ZodString>;
252
281
  }, "strict", z.ZodTypeAny, {
253
282
  autoCommit: boolean;
283
+ protectedBranches: string[];
284
+ defaultBase?: string | undefined;
254
285
  }, {
255
286
  autoCommit?: boolean | undefined;
287
+ protectedBranches?: string[] | undefined;
288
+ defaultBase?: string | undefined;
256
289
  }>>;
257
290
  shell: z.ZodDefault<z.ZodObject<{
258
291
  /** Kill the command (and its process tree) after this many ms. */
@@ -284,11 +317,11 @@ export declare const CruxyConfigSchema: z.ZodObject<{
284
317
  keepRecentMessages?: number | undefined;
285
318
  }>>;
286
319
  approval: z.ZodDefault<z.ZodObject<{
287
- mode: z.ZodDefault<z.ZodEnum<["prompt", "auto"]>>;
320
+ mode: z.ZodDefault<z.ZodEnum<["prompt"]>>;
288
321
  }, "strict", z.ZodTypeAny, {
289
- mode: "auto" | "prompt";
322
+ mode: "prompt";
290
323
  }, {
291
- mode?: "auto" | "prompt" | undefined;
324
+ mode?: "prompt" | undefined;
292
325
  }>>;
293
326
  index: z.ZodDefault<z.ZodObject<{
294
327
  /** Master switch for `search_codebase` and `cruxy index`. */
@@ -403,6 +436,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
403
436
  agent: {
404
437
  maxIterations: number;
405
438
  autoApprove: boolean;
439
+ planMode: boolean;
406
440
  };
407
441
  tools: {
408
442
  fileEdit: boolean;
@@ -411,6 +445,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
411
445
  };
412
446
  git: {
413
447
  autoCommit: boolean;
448
+ protectedBranches: string[];
449
+ defaultBase?: string | undefined;
414
450
  };
415
451
  context: {
416
452
  maxTokens: number;
@@ -418,7 +454,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
418
454
  keepRecentMessages: number;
419
455
  };
420
456
  approval: {
421
- mode: "auto" | "prompt";
457
+ mode: "prompt";
422
458
  };
423
459
  index: {
424
460
  search: {
@@ -458,6 +494,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
458
494
  agent?: {
459
495
  maxIterations?: number | undefined;
460
496
  autoApprove?: boolean | undefined;
497
+ planMode?: boolean | undefined;
461
498
  } | undefined;
462
499
  tools?: {
463
500
  fileEdit?: boolean | undefined;
@@ -466,6 +503,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
466
503
  } | undefined;
467
504
  git?: {
468
505
  autoCommit?: boolean | undefined;
506
+ protectedBranches?: string[] | undefined;
507
+ defaultBase?: string | undefined;
469
508
  } | undefined;
470
509
  context?: {
471
510
  maxTokens?: number | undefined;
@@ -473,7 +512,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
473
512
  keepRecentMessages?: number | undefined;
474
513
  } | undefined;
475
514
  approval?: {
476
- mode?: "auto" | "prompt" | undefined;
515
+ mode?: "prompt" | undefined;
477
516
  } | undefined;
478
517
  index?: {
479
518
  search?: {
@@ -28,6 +28,8 @@ export const AgentConfigSchema = z
28
28
  maxIterations: z.number().int().positive().default(25),
29
29
  /** Skip per-action confirmation prompts. */
30
30
  autoApprove: z.boolean().default(false),
31
+ /** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
32
+ planMode: z.boolean().default(false),
31
33
  })
32
34
  .strict();
33
35
  export const ToolsConfigSchema = z
@@ -40,6 +42,12 @@ export const ToolsConfigSchema = z
40
42
  export const GitConfigSchema = z
41
43
  .object({
42
44
  autoCommit: z.boolean().default(false),
45
+ /** Branches cruxy never commits/pushes to directly (PR flow branches off
46
+ * first). `main` and `master` are always protected; these add to them. */
47
+ protectedBranches: z.array(z.string()).default([]),
48
+ /** Default base branch for generated pull requests; falls back to the
49
+ * repo's default branch, then `main`, when unset. */
50
+ defaultBase: z.string().optional(),
43
51
  })
44
52
  .strict();
45
53
  /** Execution bounds for the `run_command` shell tool (distinct from the
@@ -64,12 +72,15 @@ export const ContextConfigSchema = z
64
72
  keepRecentMessages: z.number().int().positive().default(6),
65
73
  })
66
74
  .strict();
67
- /** How tool-action approval is resolved. */
75
+ /**
76
+ * How tool-action approval is resolved. Only `prompt` exists: ask interactively
77
+ * and **deny by default** when non-interactive. There is deliberately no
78
+ * auto-approve / skip mode — the policy seam for a future CI mode lives in
79
+ * `src/approval` (see U.3), not behind a footgun flag.
80
+ */
68
81
  export const ApprovalConfigSchema = z
69
82
  .object({
70
- // "prompt": ask interactively (deny when non-interactive). "auto": approve
71
- // every action unattended (CI / explicit opt-in).
72
- mode: z.enum(["prompt", "auto"]).default("prompt"),
83
+ mode: z.enum(["prompt"]).default("prompt"),
73
84
  })
74
85
  .strict();
75
86
  /**
@@ -5,6 +5,12 @@ export declare const APP_DESCRIPTION: string;
5
5
  /** Directory/file names cruxy looks for. */
6
6
  export declare const GLOBAL_DIR_NAME = ".cruxy";
7
7
  export declare const CONFIG_FILE_NAME = "config.json";
8
+ /** Secret store under the global dir — `0600`, never holds non-secret config. */
9
+ export declare const CREDENTIALS_FILE_NAME = "credentials.json";
10
+ /** Onboarding state + completion marker under the global dir (U.6). */
11
+ export declare const ONBOARDING_FILE_NAME = "onboarding.json";
12
+ /** Where a user creates a Cruxy gateway key (printed during onboarding). */
13
+ export declare const CREATE_KEY_URL = "https://app.cruxy.in";
8
14
  /** Project-level config filenames, checked in order. */
9
15
  export declare const PROJECT_CONFIG_FILENAMES: string[];
10
16
  /** Project-instruction filenames, checked in order (first match wins). */
package/dist/constants.js CHANGED
@@ -22,6 +22,12 @@ export const APP_DESCRIPTION = pkg.description ?? "an agentic coding CLI";
22
22
  /** Directory/file names cruxy looks for. */
23
23
  export const GLOBAL_DIR_NAME = ".cruxy";
24
24
  export const CONFIG_FILE_NAME = "config.json";
25
+ /** Secret store under the global dir — `0600`, never holds non-secret config. */
26
+ export const CREDENTIALS_FILE_NAME = "credentials.json";
27
+ /** Onboarding state + completion marker under the global dir (U.6). */
28
+ export const ONBOARDING_FILE_NAME = "onboarding.json";
29
+ /** Where a user creates a Cruxy gateway key (printed during onboarding). */
30
+ export const CREATE_KEY_URL = "https://app.cruxy.in";
25
31
  /** Project-level config filenames, checked in order. */
26
32
  export const PROJECT_CONFIG_FILENAMES = [
27
33
  "cruxy.config.json",
@@ -18,6 +18,41 @@ export declare function permissionDenied(path: string, underlying?: unknown): Cr
18
18
  export declare function indexEmbedderUnavailable(underlying?: unknown): CruxyError;
19
19
  export declare function indexStoreUnavailable(underlying?: unknown): CruxyError;
20
20
  export declare function indexFailed(underlying?: unknown): CruxyError;
21
+ /**
22
+ * A side-effecting action needs approval but cruxy can't ask (non-interactive,
23
+ * no policy). Default-deny — never auto-approve. A distinct exit code (10) so CI
24
+ * can tell "needed approval" apart from a usage error.
25
+ */
26
+ export declare function approvalRequired(summary: string): CruxyError;
27
+ /**
28
+ * No forge token could be resolved (PR generation, C.15). The chain is env →
29
+ * `gh auth token` → fail. We never prompt for, store, or persist a token, so the
30
+ * fix is always to provide one in the environment.
31
+ */
32
+ export declare function forgeAuth(host?: string): CruxyError;
33
+ /**
34
+ * A commit/push was attempted on a protected branch (`main`/`master`/configured).
35
+ * The PR flow must branch off first; this is the last-line guard.
36
+ */
37
+ export declare function gitProtectedBranch(branch: string): CruxyError;
38
+ /** The forge REST API returned an error (non-auth) while opening a PR. */
39
+ export declare function forgeApi(title: string, underlying?: unknown, meta?: Record<string, unknown>): CruxyError;
40
+ /**
41
+ * `git push` failed — most often the husky `pre-push` verify hook (build ·
42
+ * typecheck · lint · test) or a rejected non-fast-forward. We never `--force` or
43
+ * `--no-verify`, so the underlying reason is surfaced verbatim.
44
+ */
45
+ export declare function gitPushFailed(branch: string, stderr?: string): CruxyError;
46
+ /** The agent's proposed plan was missing or malformed (plan mode, C.31). */
47
+ export declare function planInvalid(reason: string): CruxyError;
48
+ /** The plan was rejected too many times without converging (plan mode, C.31). */
49
+ export declare function planRevisionLimit(limit: number): CruxyError;
50
+ /**
51
+ * Plan mode needs interactive approval but cruxy is running non-interactively.
52
+ * Default-deny — a plan is never auto-approved. Distinct code (exit 10) so CI can
53
+ * tell it apart from a per-action approval requirement.
54
+ */
55
+ export declare function planApprovalRequired(): CruxyError;
21
56
  export declare function internal(underlying?: unknown): CruxyError;
22
57
  /**
23
58
  * Map a known provider/transport error (from `@cruxy/sdk`) to a typed