@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,57 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The `submit_plan` tool (C.31): the agent's only way to propose a plan. It is a
4
+ * normal tool on the same loop seam as everything else — no hidden control flow.
5
+ * During the propose phase this is the *only* non-read-only tool registered, so
6
+ * the agent structurally cannot act before approval.
7
+ *
8
+ * Validation is fail-loud: a malformed or empty plan returns `{ok:false}` so the
9
+ * model sees the error and retries, exactly like any other tool.
10
+ */
11
+ const stepSchema = z.object({
12
+ title: z.string().min(1).describe("One-line imperative title for the step."),
13
+ rationale: z
14
+ .string()
15
+ .min(1)
16
+ .describe("Why this step exists / what it accomplishes."),
17
+ kind: z
18
+ .enum(["read", "mutate", "destructive"])
19
+ .describe("Risk estimate: read (no side effects), mutate (reversible file writes), " +
20
+ "destructive (shell, deletes, PR-open). Advisory — the approval gate re-checks each action."),
21
+ });
22
+ const parameters = z.object({
23
+ steps: z
24
+ .array(stepSchema)
25
+ .min(1)
26
+ .describe("Ordered steps that fully accomplish the task."),
27
+ });
28
+ /** Build a `submit_plan` tool bound to `holder`, which captures the last plan. */
29
+ export function makeSubmitPlanTool(holder) {
30
+ return {
31
+ name: "submit_plan",
32
+ description: "Propose an ordered, step-by-step plan for the user to approve before you act. " +
33
+ "Call this first (and only this) in plan mode; do not take any other action until the plan is approved.",
34
+ parameters,
35
+ async execute(input) {
36
+ // zod already guarantees ≥1 step with non-empty fields; this is the
37
+ // fail-loud belt-and-suspenders for an empty array slipping through.
38
+ if (input.steps.length === 0) {
39
+ return { ok: false, error: "a plan must have at least one step" };
40
+ }
41
+ const steps = input.steps.map((s, i) => ({
42
+ id: String(i + 1),
43
+ title: s.title.trim(),
44
+ rationale: s.rationale.trim(),
45
+ kind: s.kind,
46
+ status: "pending",
47
+ }));
48
+ const plan = { steps };
49
+ holder.plan = plan;
50
+ return {
51
+ ok: true,
52
+ output: `Plan received (${steps.length} step${steps.length === 1 ? "" : "s"}) and shown to the user for approval. ` +
53
+ "Do not take any further action now — end your turn.",
54
+ };
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Types for plan mode (C.31). A {@link Plan} is a first-class, typed artifact the
3
+ * agent produces via the `submit_plan` tool — never free text — so the user can
4
+ * review the shape of the work once, and execution can track per-step status.
5
+ */
6
+ /**
7
+ * The agent's risk *estimate* for a step, used only to render intent and shape
8
+ * the approval copy. It is NOT authoritative: the real U.3 gate re-classifies
9
+ * every concrete action at execution time (a step the agent calls "mutate" whose
10
+ * action turns out destructive still confirms).
11
+ */
12
+ export type PlanStepKind = "read" | "mutate" | "destructive";
13
+ /** Live execution status of a step. */
14
+ export type PlanStepStatus = "pending" | "running" | "done" | "failed";
15
+ export interface PlanStep {
16
+ /** Stable 1-based id assigned on submit (e.g. "1"). */
17
+ readonly id: string;
18
+ /** One-line imperative title. */
19
+ readonly title: string;
20
+ /** Why this step exists / what it accomplishes. */
21
+ readonly rationale: string;
22
+ /** The agent's risk estimate (advisory; see {@link PlanStepKind}). */
23
+ readonly kind: PlanStepKind;
24
+ /** Mutated in place as execution progresses. */
25
+ status: PlanStepStatus;
26
+ }
27
+ export interface Plan {
28
+ readonly steps: PlanStep[];
29
+ }
30
+ /**
31
+ * The user's decision at the plan-approval prompt.
32
+ * - `approve` — execute; every action still hits the U.3 gate.
33
+ * - `approve-grant` — execute AND auto-allow the safe (read/mutate, grantable)
34
+ * steps for this run; destructive/ungrantable actions still confirm.
35
+ * - `revise` — send `feedback` back to the agent for a revised plan.
36
+ * - `abort` — cancel; nothing executes (also the default-deny for EOF/Ctrl-C).
37
+ */
38
+ export type PlanDecision = {
39
+ readonly kind: "approve";
40
+ } | {
41
+ readonly kind: "approve-grant";
42
+ } | {
43
+ readonly kind: "revise";
44
+ readonly feedback: string;
45
+ } | {
46
+ readonly kind: "abort";
47
+ };
48
+ /** Outcome of executing an approved plan. */
49
+ export interface PlanExecutionResult {
50
+ /** True if every step reached `done`. */
51
+ readonly completed: boolean;
52
+ /** True if a step failed and the user chose to abort the run. */
53
+ readonly halted: boolean;
54
+ /** The id of the step that failed, when halted. */
55
+ readonly failedStepId?: string;
56
+ }
57
+ /** A mutable holder the `submit_plan` tool writes the captured plan into. */
58
+ export interface PlanHolder {
59
+ plan: Plan | null;
60
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Types for plan mode (C.31). A {@link Plan} is a first-class, typed artifact the
3
+ * agent produces via the `submit_plan` tool — never free text — so the user can
4
+ * review the shape of the work once, and execution can track per-step status.
5
+ */
6
+ export {};
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import type { Tool } from "./types.js";
3
+ /**
4
+ * Open a pull request from the agent's work (C.15): branch → conventional commit
5
+ * → push → open PR, all behind the one U.3 approval. The model authors `title`
6
+ * and `body`; anything omitted is filled deterministically (no second LLM call —
7
+ * the agent IS the LLM). The token is resolved from the environment / `gh`; it is
8
+ * never prompted for or stored.
9
+ */
10
+ declare const parameters: z.ZodObject<{
11
+ title: z.ZodOptional<z.ZodString>;
12
+ body: z.ZodOptional<z.ZodString>;
13
+ base: z.ZodOptional<z.ZodString>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ body?: string | undefined;
16
+ base?: string | undefined;
17
+ title?: string | undefined;
18
+ }, {
19
+ body?: string | undefined;
20
+ base?: string | undefined;
21
+ title?: string | undefined;
22
+ }>;
23
+ export declare const createPullRequestTool: Tool<typeof parameters>;
24
+ export {};
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ import { CruxyError, ErrorCode } from "../errors/index.js";
3
+ import { createForgeProvider, createPrService, fillContent, loadCommitGuidance, resolveForgeToken, } from "../vcs/index.js";
4
+ /**
5
+ * Open a pull request from the agent's work (C.15): branch → conventional commit
6
+ * → push → open PR, all behind the one U.3 approval. The model authors `title`
7
+ * and `body`; anything omitted is filled deterministically (no second LLM call —
8
+ * the agent IS the LLM). The token is resolved from the environment / `gh`; it is
9
+ * never prompted for or stored.
10
+ */
11
+ const parameters = z.object({
12
+ title: z
13
+ .string()
14
+ .optional()
15
+ .describe("PR title as a conventional-commit subject (e.g. `feat(cli): add X`). " +
16
+ "Lowercase subject; omit to auto-generate."),
17
+ body: z
18
+ .string()
19
+ .optional()
20
+ .describe("PR body in markdown (what changed · why · verification). Omit to auto-generate."),
21
+ base: z
22
+ .string()
23
+ .optional()
24
+ .describe("Base branch to merge into (defaults to the repo's default branch)."),
25
+ });
26
+ export const createPullRequestTool = {
27
+ name: "create_pull_request",
28
+ description: "Open a pull request for the current changes: create a feature branch, make a " +
29
+ "conventional commit, push, and open the PR. Branch/commit/PR are shown for your " +
30
+ "approval before anything runs. Never acts on a protected branch and never force-pushes.",
31
+ parameters,
32
+ async execute(input, ctx) {
33
+ try {
34
+ const token = resolveForgeToken();
35
+ const forge = createForgeProvider(token);
36
+ const guidance = await loadCommitGuidance(ctx.cwd);
37
+ const service = createPrService({
38
+ cwd: ctx.cwd,
39
+ config: ctx.config,
40
+ forge,
41
+ requestApproval: ctx.requestApproval,
42
+ generate: async (i) => fillContent({
43
+ ...i,
44
+ scopes: guidance.scopes,
45
+ skillBody: guidance.skillBody,
46
+ }),
47
+ });
48
+ const outcome = await service.openPullRequest({
49
+ title: input.title,
50
+ body: input.body,
51
+ base: input.base,
52
+ });
53
+ if (!outcome.approved) {
54
+ return {
55
+ ok: false,
56
+ error: outcome.feedback ?? "you declined to open the pull request",
57
+ };
58
+ }
59
+ const verb = outcome.alreadyExists
60
+ ? "a pull request already exists"
61
+ : "opened pull request";
62
+ return { ok: true, output: `${verb}: ${outcome.url}` };
63
+ }
64
+ catch (err) {
65
+ // The non-interactive approval error must reach the boundary (exit 10);
66
+ // every other coded failure is surfaced to the model so it can adapt.
67
+ if (err instanceof CruxyError &&
68
+ err.code === ErrorCode.ApprovalRequired) {
69
+ throw err;
70
+ }
71
+ if (err instanceof CruxyError) {
72
+ return { ok: false, error: formatCoded(err) };
73
+ }
74
+ throw err;
75
+ }
76
+ },
77
+ };
78
+ /** One-line, model-readable rendering of a coded error + its next steps. */
79
+ function formatCoded(err) {
80
+ const head = err.cause ? `${err.title} — ${err.cause}` : err.title;
81
+ const steps = err.nextSteps.length > 0 ? ` next steps: ${err.nextSteps.join("; ")}` : "";
82
+ return `${head} (${err.code}).${steps}`;
83
+ }
@@ -80,12 +80,12 @@ export const applyPatchTool = {
80
80
  planned.push(planResult.planned);
81
81
  }
82
82
  // One approval for the whole patch — denial writes nothing.
83
- const approved = await ctx.approve({
83
+ const decision = await ctx.requestApproval({
84
84
  kind: "patch",
85
85
  preview: { type: "patch", files: planned.map(toPreview) },
86
86
  });
87
- if (!approved) {
88
- return { ok: false, error: "patch denied" };
87
+ if (!decision.allow) {
88
+ return { ok: false, error: decision.feedback ?? "patch denied" };
89
89
  }
90
90
  // Validation passed and the user approved; apply everything. A mid-apply I/O
91
91
  // failure is rare but reported with what already landed.
@@ -57,13 +57,16 @@ export const editFileTool = {
57
57
  error: `old_str not unique (${matches} matches); add surrounding context to disambiguate`,
58
58
  };
59
59
  }
60
- const approved = await ctx.approve({
60
+ const decision = await ctx.requestApproval({
61
61
  kind: "edit",
62
62
  path: abs,
63
63
  preview: { type: "edit", oldStr: input.old_str, newStr: input.new_str },
64
64
  });
65
- if (!approved) {
66
- return { ok: false, error: `edit to ${input.path} denied` };
65
+ if (!decision.allow) {
66
+ return {
67
+ ok: false,
68
+ error: decision.feedback ?? `edit to ${input.path} denied`,
69
+ };
67
70
  }
68
71
  // Replace the single occurrence by index to avoid `$`-pattern interpretation.
69
72
  const idx = content.indexOf(input.old_str);
@@ -35,13 +35,16 @@ export const writeFileTool = {
35
35
  const lines = allLines.slice(0, PREVIEW_LINES);
36
36
  const omittedLines = Math.max(0, allLines.length - PREVIEW_LINES);
37
37
  // Approve BEFORE any mutation; a denial writes nothing.
38
- const approved = await ctx.approve({
38
+ const decision = await ctx.requestApproval({
39
39
  kind: "write",
40
40
  path: abs,
41
41
  preview: { type: "write", exists, lines, omittedLines },
42
42
  });
43
- if (!approved) {
44
- return { ok: false, error: `write to ${input.path} denied` };
43
+ if (!decision.allow) {
44
+ return {
45
+ ok: false,
46
+ error: decision.feedback ?? `write to ${input.path} denied`,
47
+ };
45
48
  }
46
49
  try {
47
50
  await fs.mkdir(path.dirname(abs), { recursive: true });
@@ -5,4 +5,5 @@ export * from "./git-status.js";
5
5
  export * from "./search-codebase.js";
6
6
  export * from "./list-skills.js";
7
7
  export * from "./load-skill.js";
8
+ export * from "./create-pull-request.js";
8
9
  export * from "./file/index.js";
@@ -5,4 +5,5 @@ export * from "./git-status.js";
5
5
  export * from "./search-codebase.js";
6
6
  export * from "./list-skills.js";
7
7
  export * from "./load-skill.js";
8
+ export * from "./create-pull-request.js";
8
9
  export * from "./file/index.js";
@@ -6,6 +6,7 @@ import { runCommandTool } from "./shell/index.js";
6
6
  import { searchCodebaseTool } from "./search-codebase.js";
7
7
  import { listSkillsTool } from "./list-skills.js";
8
8
  import { loadSkillTool } from "./load-skill.js";
9
+ import { createPullRequestTool } from "./create-pull-request.js";
9
10
  /**
10
11
  * In-memory catalogue of the tools available to the agent. Names are unique;
11
12
  * `toToolSpecs()` projects the catalogue into the `@cruxy/sdk` wire format.
@@ -65,5 +66,6 @@ export function buildDefaultRegistry() {
65
66
  registry.register(searchCodebaseTool);
66
67
  registry.register(listSkillsTool);
67
68
  registry.register(loadSkillTool);
69
+ registry.register(createPullRequestTool);
68
70
  return registry;
69
71
  }
@@ -14,9 +14,17 @@ export const runCommandTool = {
14
14
  .describe("The shell command to run (executed via the system shell)."),
15
15
  }),
16
16
  async execute(input, ctx) {
17
- // Approve BEFORE anything runs; a denial executes nothing.
18
- if (!(await ctx.approve({ kind: "shell", command: input.command }))) {
19
- return { ok: false, error: "denied by user" };
17
+ // Approve BEFORE anything runs; a denial executes nothing. A thrown
18
+ // CRUXY_E_APPROVAL_REQUIRED (non-interactive) propagates do not catch.
19
+ const decision = await ctx.requestApproval({
20
+ kind: "shell",
21
+ command: input.command,
22
+ });
23
+ if (!decision.allow) {
24
+ return {
25
+ ok: false,
26
+ error: decision.feedback ?? "command denied by the user",
27
+ };
20
28
  }
21
29
  return runBounded(input.command, ctx);
22
30
  },
@@ -1,5 +1,6 @@
1
1
  import type { z, ZodTypeAny } from "zod";
2
2
  import type { CruxyConfig } from "../config/index.js";
3
+ import type { ApprovalDecision } from "../approval/types.js";
3
4
  import type { logger } from "../utils/logger.js";
4
5
  /** The leveled logger instance shared across the CLI. */
5
6
  type Logger = typeof logger;
@@ -61,6 +62,20 @@ export type ActionPreview =
61
62
  | {
62
63
  type: "patch";
63
64
  files: PatchFilePreview[];
65
+ }
66
+ /**
67
+ * The whole publish plan for a `vcs` action (C.15): the feature branch, the
68
+ * conventional commit, and the PR title/body — shown as one block so the user
69
+ * approves the entire branch → commit → push → open-PR sequence at once.
70
+ */
71
+ | {
72
+ type: "pr";
73
+ branch: string;
74
+ base: string;
75
+ commitSubject: string;
76
+ commitBody: string;
77
+ prTitle: string;
78
+ prBody: string;
64
79
  };
65
80
  /**
66
81
  * A side-effecting action a tool wants to take, passed to `ctx.approve`. The
@@ -68,12 +83,12 @@ export type ActionPreview =
68
83
  */
69
84
  export interface ApproveAction {
70
85
  /** The category of side effect being requested. */
71
- kind: "write" | "edit" | "shell" | "patch";
86
+ kind: "write" | "edit" | "shell" | "patch" | "vcs";
72
87
  /** Absolute resolved path the action targets (write/edit). */
73
88
  path?: string;
74
89
  /** The command to run (shell). */
75
90
  command?: string;
76
- /** Exact-change preview rendered above the prompt (write/edit/patch). */
91
+ /** Exact-change preview rendered above the prompt (write/edit/patch/vcs). */
77
92
  preview?: ActionPreview;
78
93
  }
79
94
  /**
@@ -89,11 +104,15 @@ export interface ToolContext {
89
104
  /** Shared leveled logger (diagnostics to stderr, `print` to stdout). */
90
105
  logger: Logger;
91
106
  /**
92
- * Permission gate for side-effecting actions. Returns `true` when the action
93
- * is allowed. Backed by the interactive `Approver` (see agent/approval.ts);
94
- * tests may inject their own.
107
+ * The approval gate every side-effecting tool funnels through, **before** any
108
+ * mutation. Returns a decision: `{allow:true}` to proceed, or `{allow:false,
109
+ * feedback?}` (a clean rejection whose feedback is surfaced to the agent).
110
+ * Backed by the risk-tiered `ApprovalService` (see src/approval); it may
111
+ * *throw* `CRUXY_E_APPROVAL_REQUIRED` when it can't ask (non-interactive),
112
+ * which propagates to the boundary rather than being swallowed. Read-only
113
+ * tools never call this.
95
114
  */
96
- approve(action: ApproveAction): Promise<boolean>;
115
+ requestApproval(action: ApproveAction): Promise<ApprovalDecision>;
97
116
  }
98
117
  /**
99
118
  * The one interface every tool implements. `parameters` is a zod schema; it both
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Resolve a forge token (C.15). The chain is, in order:
3
+ * 1. `GITHUB_TOKEN`
4
+ * 2. `GH_TOKEN`
5
+ * 3. `gh auth token` (if the GitHub CLI is installed and logged in)
6
+ * 4. fail loud — {@link forgeAuth} (`CRUXY_E_FORGE_AUTH`)
7
+ *
8
+ * We **never** prompt for, store, or persist a token: it's read from the
9
+ * environment / `gh` on every run, exactly like provider API keys.
10
+ */
11
+ /** Injection seams so the chain is testable without real env / a real `gh`. */
12
+ export interface ForgeAuthDeps {
13
+ /** Environment to read tokens from (defaults to `process.env`). */
14
+ env?: NodeJS.ProcessEnv;
15
+ /** Run `gh auth token`, returning the token or `null`. Defaults to the CLI. */
16
+ ghToken?: () => string | null;
17
+ }
18
+ /**
19
+ * Resolve a forge token or throw {@link forgeAuth}. `host` is only used to make
20
+ * the error message specific.
21
+ */
22
+ export declare function resolveForgeToken(deps?: ForgeAuthDeps, host?: string): string;
@@ -0,0 +1,29 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { forgeAuth } from "../errors/index.js";
3
+ /** Invoke `gh auth token`; returns the trimmed token, or `null` on any failure. */
4
+ function ghAuthToken() {
5
+ const res = spawnSync("gh", ["auth", "token"], {
6
+ encoding: "utf8",
7
+ timeout: 5000,
8
+ windowsHide: true,
9
+ });
10
+ if (res.error || res.status !== 0 || typeof res.stdout !== "string") {
11
+ return null;
12
+ }
13
+ const token = res.stdout.trim();
14
+ return token === "" ? null : token;
15
+ }
16
+ /**
17
+ * Resolve a forge token or throw {@link forgeAuth}. `host` is only used to make
18
+ * the error message specific.
19
+ */
20
+ export function resolveForgeToken(deps = {}, host = "github.com") {
21
+ const env = deps.env ?? process.env;
22
+ const fromEnv = env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim();
23
+ if (fromEnv)
24
+ return fromEnv;
25
+ const fromGh = (deps.ghToken ?? ghAuthToken)();
26
+ if (fromGh && fromGh.trim() !== "")
27
+ return fromGh.trim();
28
+ throw forgeAuth(host);
29
+ }
@@ -0,0 +1,72 @@
1
+ import type { Provider } from "@cruxy/sdk";
2
+ import type { GeneratedContent } from "./types.js";
3
+ /**
4
+ * Turn a diff + session context into the PR publish content (C.15): a
5
+ * conventional-commit subject, a structured body, a branch name, and the PR
6
+ * title/body. The repo's commit rules (the C.18 git-commit skill + the
7
+ * commitlint scope list) are honored two ways: they're handed to the LLM in the
8
+ * prompt, **and** every field is run through deterministic normalizers so the
9
+ * lowercase-subject rule (and scope allow-list) hold even if the model slips.
10
+ *
11
+ * Secrets never leave: the diff is redacted before the LLM sees it, and the
12
+ * generated bodies are redacted again (defense-in-depth).
13
+ */
14
+ /** Conventional-commit types accepted by `@commitlint/config-conventional`. */
15
+ export declare const CONVENTIONAL_TYPES: readonly ["feat", "fix", "chore", "docs", "refactor", "test", "perf", "build", "ci", "style", "revert"];
16
+ export interface GenerateInput {
17
+ /** The change set to summarize (already from `diffAgainst`). */
18
+ diff: string;
19
+ /** The PR base branch. */
20
+ base: string;
21
+ /** The branch the change is on, if already a feature branch. */
22
+ currentBranch?: string;
23
+ /** A short summary of what the agent did this session, if available. */
24
+ sessionSummary?: string;
25
+ /** commitlint scope allow-list; empty ⇒ no scope constraint. */
26
+ scopes: string[];
27
+ /** The git-commit SKILL.md body, embedded into the LLM prompt verbatim. */
28
+ skillBody?: string;
29
+ /** Caller-supplied title (e.g. the agent's), normalized rather than generated. */
30
+ title?: string;
31
+ /** Caller-supplied body. */
32
+ body?: string;
33
+ }
34
+ /** Strip anything that looks like a credential from `text`. */
35
+ export declare function redactSecrets(text: string): string;
36
+ /**
37
+ * Force `raw` into a valid conventional-commit subject: a known `type`, an
38
+ * allow-listed scope (dropped if it isn't), a **lowercase** first word, no
39
+ * trailing period, clamped to 72 chars. This is the deterministic guarantee that
40
+ * the commitlint `subject-case` + `scope-enum` rules pass.
41
+ */
42
+ export declare function normalizeSubject(raw: string, scopes?: string[]): string;
43
+ /** A safe branch name `type/slug` derived from a conventional subject. */
44
+ export declare function slugifyBranch(subject: string): string;
45
+ /** Assemble a structured PR body: what changed · why · verification. */
46
+ export declare function assembleBody(parts: {
47
+ what: string;
48
+ why?: string;
49
+ verification?: string;
50
+ }): string;
51
+ /**
52
+ * Build the publish content deterministically (used by the `create_pull_request`
53
+ * tool, where the agent itself authored the title/body). Missing fields are
54
+ * derived; every field is still normalized + redacted.
55
+ */
56
+ export declare function fillContent(input: GenerateInput): GeneratedContent;
57
+ /**
58
+ * Generate publish content from the diff with the model, honoring the commit
59
+ * rules, then normalize + redact. Resilient: if the model's reply isn't the
60
+ * expected JSON, the first line becomes the subject and the rest the body.
61
+ */
62
+ export declare function generateWithLlm(provider: Provider, input: GenerateInput): Promise<GeneratedContent>;
63
+ interface ParsedGenerated {
64
+ branchName?: string;
65
+ commitSubject?: string;
66
+ commitBody?: string;
67
+ prTitle?: string;
68
+ prBody?: string;
69
+ }
70
+ /** Parse the model reply: a JSON object if present, else first-line/rest. */
71
+ export declare function parseGenerated(text: string): ParsedGenerated;
72
+ export {};