@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.
- package/README.md +57 -16
- package/dist/agent/index.d.ts +0 -1
- package/dist/agent/index.js +0 -1
- package/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.js +1 -1
- package/dist/agent/prompts.d.ts +2 -2
- package/dist/agent/prompts.js +9 -3
- package/dist/agent/session.d.ts +24 -0
- package/dist/agent/session.js +33 -7
- package/dist/approval/classify.d.ts +18 -0
- package/dist/approval/classify.js +162 -0
- package/dist/approval/index.d.ts +5 -0
- package/dist/approval/index.js +5 -0
- package/dist/approval/policy.d.ts +37 -0
- package/dist/approval/policy.js +81 -0
- package/dist/approval/prompt.d.ts +33 -0
- package/dist/approval/prompt.js +212 -0
- package/dist/approval/service.d.ts +36 -0
- package/dist/approval/service.js +37 -0
- package/dist/approval/types.d.ts +64 -0
- package/dist/approval/types.js +1 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +40 -0
- package/dist/cli/commands/login.d.ts +8 -0
- package/dist/cli/commands/login.js +36 -0
- package/dist/cli/commands/pr.d.ts +8 -0
- package/dist/cli/commands/pr.js +87 -0
- package/dist/cli/commands/run.js +30 -52
- package/dist/cli/onboard.d.ts +25 -0
- package/dist/cli/onboard.js +54 -0
- package/dist/cli/program.js +21 -1
- package/dist/cli/repl.js +10 -1
- package/dist/cli/session-factory.d.ts +12 -0
- package/dist/cli/session-factory.js +88 -0
- package/dist/config/credentials.d.ts +10 -0
- package/dist/config/credentials.js +69 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/manager.d.ts +6 -1
- package/dist/config/manager.js +11 -1
- package/dist/config/schema.d.ts +48 -9
- package/dist/config/schema.js +15 -4
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +6 -0
- package/dist/errors/constructors.d.ts +35 -0
- package/dist/errors/constructors.js +129 -2
- package/dist/errors/types.d.ts +8 -0
- package/dist/errors/types.js +17 -0
- package/dist/onboarding/detect.d.ts +26 -0
- package/dist/onboarding/detect.js +56 -0
- package/dist/onboarding/flow.d.ts +28 -0
- package/dist/onboarding/flow.js +100 -0
- package/dist/onboarding/index.d.ts +5 -0
- package/dist/onboarding/index.js +5 -0
- package/dist/onboarding/io.d.ts +8 -0
- package/dist/onboarding/io.js +133 -0
- package/dist/onboarding/steps.d.ts +17 -0
- package/dist/onboarding/steps.js +100 -0
- package/dist/onboarding/types.d.ts +81 -0
- package/dist/onboarding/types.js +6 -0
- package/dist/plan/approve.d.ts +16 -0
- package/dist/plan/approve.js +46 -0
- package/dist/plan/execute.d.ts +20 -0
- package/dist/plan/execute.js +31 -0
- package/dist/plan/index.d.ts +7 -0
- package/dist/plan/index.js +7 -0
- package/dist/plan/policy.d.ts +26 -0
- package/dist/plan/policy.js +45 -0
- package/dist/plan/render.d.ts +5 -0
- package/dist/plan/render.js +47 -0
- package/dist/plan/service.d.ts +39 -0
- package/dist/plan/service.js +118 -0
- package/dist/plan/submit-plan.d.ts +33 -0
- package/dist/plan/submit-plan.js +57 -0
- package/dist/plan/types.d.ts +60 -0
- package/dist/plan/types.js +6 -0
- package/dist/tools/create-pull-request.d.ts +24 -0
- package/dist/tools/create-pull-request.js +83 -0
- package/dist/tools/file/apply-patch.js +3 -3
- package/dist/tools/file/edit-file.js +6 -3
- package/dist/tools/file/write-file.js +6 -3
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/registry.js +2 -0
- package/dist/tools/shell/run-command.js +11 -3
- package/dist/tools/types.d.ts +25 -6
- package/dist/vcs/auth.d.ts +22 -0
- package/dist/vcs/auth.js +29 -0
- package/dist/vcs/generate.d.ts +72 -0
- package/dist/vcs/generate.js +265 -0
- package/dist/vcs/git.d.ts +52 -0
- package/dist/vcs/git.js +152 -0
- package/dist/vcs/github.d.ts +44 -0
- package/dist/vcs/github.js +145 -0
- package/dist/vcs/guidance.d.ts +20 -0
- package/dist/vcs/guidance.js +76 -0
- package/dist/vcs/index.d.ts +7 -0
- package/dist/vcs/index.js +7 -0
- package/dist/vcs/service.d.ts +53 -0
- package/dist/vcs/service.js +79 -0
- package/dist/vcs/types.d.ts +57 -0
- package/dist/vcs/types.js +6 -0
- package/package.json +1 -1
- package/dist/agent/approval.d.ts +0 -41
- 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
|
+
}
|
package/dist/cli/program.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/config/index.d.ts
CHANGED
package/dist/config/index.js
CHANGED
package/dist/config/manager.d.ts
CHANGED
|
@@ -30,5 +30,10 @@ export declare function initConfig(file: string): {
|
|
|
30
30
|
path: string;
|
|
31
31
|
created: boolean;
|
|
32
32
|
};
|
|
33
|
-
/**
|
|
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;
|
package/dist/config/manager.js
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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"
|
|
113
|
+
mode: z.ZodDefault<z.ZodEnum<["prompt"]>>;
|
|
95
114
|
}, "strict", z.ZodTypeAny, {
|
|
96
|
-
mode: "
|
|
115
|
+
mode: "prompt";
|
|
97
116
|
}, {
|
|
98
|
-
mode?: "
|
|
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"
|
|
320
|
+
mode: z.ZodDefault<z.ZodEnum<["prompt"]>>;
|
|
288
321
|
}, "strict", z.ZodTypeAny, {
|
|
289
|
-
mode: "
|
|
322
|
+
mode: "prompt";
|
|
290
323
|
}, {
|
|
291
|
-
mode?: "
|
|
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: "
|
|
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?: "
|
|
515
|
+
mode?: "prompt" | undefined;
|
|
477
516
|
} | undefined;
|
|
478
517
|
index?: {
|
|
479
518
|
search?: {
|
package/dist/config/schema.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
/**
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|