@open-cr-agent/cli 0.5.0 → 0.6.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 (139) hide show
  1. package/dist/cloud/account-salt.d.ts +6 -0
  2. package/dist/cloud/account-salt.js +33 -0
  3. package/dist/{review → cloud}/account-settings.js +5 -4
  4. package/dist/cloud/browser.d.ts +17 -0
  5. package/dist/cloud/browser.js +37 -0
  6. package/dist/cloud/client.d.ts +93 -0
  7. package/dist/cloud/client.js +221 -0
  8. package/dist/cloud/credentials.d.ts +18 -0
  9. package/dist/cloud/credentials.js +52 -0
  10. package/dist/cloud/deps.d.ts +14 -0
  11. package/dist/cloud/deps.js +34 -0
  12. package/dist/cloud/file-lock.d.ts +14 -0
  13. package/dist/cloud/file-lock.js +77 -0
  14. package/dist/cloud/findings.d.ts +8 -0
  15. package/dist/{review/cloud-findings.js → cloud/findings.js} +10 -32
  16. package/dist/cloud/gateway-token.d.ts +18 -0
  17. package/dist/cloud/gateway-token.js +63 -0
  18. package/dist/cloud/memory.d.ts +8 -0
  19. package/dist/cloud/memory.js +68 -0
  20. package/dist/cloud/private-file.d.ts +8 -0
  21. package/dist/cloud/private-file.js +46 -0
  22. package/dist/{review/cloud-providers.d.ts → cloud/providers.d.ts} +12 -6
  23. package/dist/cloud/providers.js +132 -0
  24. package/dist/cloud/review.d.ts +28 -0
  25. package/dist/cloud/review.js +76 -0
  26. package/dist/cloud/settings.d.ts +22 -0
  27. package/dist/cloud/settings.js +64 -0
  28. package/dist/cloud/upload.d.ts +20 -0
  29. package/dist/{review/cloud-upload.js → cloud/upload.js} +74 -71
  30. package/dist/commands/login.d.ts +5 -0
  31. package/dist/commands/login.js +164 -0
  32. package/dist/commands/memory.d.ts +3 -0
  33. package/dist/{memory.js → commands/memory.js} +21 -20
  34. package/dist/commands/metrics.d.ts +45 -0
  35. package/dist/{metrics.js → commands/metrics.js} +32 -45
  36. package/dist/{plugins-command.d.ts → commands/plugins.d.ts} +3 -9
  37. package/dist/{plugins-command.js → commands/plugins.js} +29 -53
  38. package/dist/{review → commands/review}/args.d.ts +7 -10
  39. package/dist/{review → commands/review}/args.js +11 -16
  40. package/dist/commands/review/deliver.d.ts +7 -0
  41. package/dist/commands/review/deliver.js +56 -0
  42. package/dist/commands/review/deps.d.ts +18 -0
  43. package/dist/commands/review/deps.js +2 -0
  44. package/dist/commands/review/execute-run.d.ts +13 -0
  45. package/dist/commands/review/execute-run.js +97 -0
  46. package/dist/commands/review/exit-code.d.ts +4 -0
  47. package/dist/commands/review/exit-code.js +29 -0
  48. package/dist/commands/review/plan-prices.d.ts +3 -0
  49. package/dist/commands/review/plan-prices.js +18 -0
  50. package/dist/{review → commands/review}/plan-render.js +22 -2
  51. package/dist/commands/review/plan-run.d.ts +5 -0
  52. package/dist/commands/review/plan-run.js +38 -0
  53. package/dist/commands/review/plan-schema.d.ts +97 -0
  54. package/dist/commands/review/plan-schema.js +34 -0
  55. package/dist/{review → commands/review}/progress.d.ts +3 -5
  56. package/dist/{review → commands/review}/progress.js +3 -3
  57. package/dist/{review → commands/review}/provenance.d.ts +4 -2
  58. package/dist/{review → commands/review}/provenance.js +2 -3
  59. package/dist/{review → commands/review}/render.js +1 -1
  60. package/dist/commands/review/resolve-run.d.ts +29 -0
  61. package/dist/commands/review/resolve-run.js +125 -0
  62. package/dist/commands/review/review-options.d.ts +6 -0
  63. package/dist/commands/review/review-options.js +35 -0
  64. package/dist/{review → commands/review}/runtimes.js +1 -1
  65. package/dist/{review → commands/review}/sarif-input.d.ts +0 -1
  66. package/dist/{review → commands/review}/sarif-input.js +5 -5
  67. package/dist/commands/review/sarif.d.ts +96 -0
  68. package/dist/{review → commands/review}/sarif.js +29 -7
  69. package/dist/commands/review/settings-sources.d.ts +17 -0
  70. package/dist/commands/review/settings-sources.js +30 -0
  71. package/dist/commands/review/target.d.ts +38 -0
  72. package/dist/commands/review/target.js +126 -0
  73. package/dist/commands/review.d.ts +7 -0
  74. package/dist/commands/review.js +34 -0
  75. package/dist/config/cli-config.d.ts +41 -0
  76. package/dist/config/cli-config.js +199 -0
  77. package/dist/{review/remote-config.d.ts → config/remote.d.ts} +3 -3
  78. package/dist/{review/remote-config.js → config/remote.js} +5 -23
  79. package/dist/{review/config.d.ts → config/schema.d.ts} +8 -31
  80. package/dist/config/schema.js +138 -0
  81. package/dist/config/settings.d.ts +37 -0
  82. package/dist/config/settings.js +133 -0
  83. package/dist/config/user-dir.d.ts +3 -0
  84. package/dist/config/user-dir.js +10 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +10 -1
  87. package/dist/internal.d.ts +6 -0
  88. package/dist/internal.js +7 -0
  89. package/dist/io/exit.d.ts +9 -0
  90. package/dist/io/exit.js +12 -0
  91. package/dist/io/output.d.ts +4 -0
  92. package/dist/io/output.js +2 -0
  93. package/dist/io/usage-error.d.ts +5 -0
  94. package/dist/io/usage-error.js +8 -0
  95. package/dist/main.js +2 -2
  96. package/dist/{review/account-plugins.d.ts → plugins/account.d.ts} +2 -2
  97. package/dist/{review/account-plugins.js → plugins/account.js} +7 -7
  98. package/dist/{review/plugins.d.ts → plugins/load.d.ts} +2 -2
  99. package/dist/{review/plugins.js → plugins/load.js} +6 -5
  100. package/dist/plugins/npm.d.ts +8 -0
  101. package/dist/plugins/npm.js +24 -0
  102. package/dist/{plugin-store.d.ts → plugins/store.d.ts} +1 -1
  103. package/dist/{plugin-store.js → plugins/store.js} +5 -4
  104. package/dist/repository-id.d.ts +12 -0
  105. package/dist/repository-id.js +72 -0
  106. package/dist/run.d.ts +2 -2
  107. package/dist/run.js +15 -12
  108. package/dist/session/store.d.ts +9 -0
  109. package/dist/session/store.js +31 -0
  110. package/package.json +17 -7
  111. package/dist/account-salt.d.ts +0 -7
  112. package/dist/account-salt.js +0 -43
  113. package/dist/cloud.d.ts +0 -34
  114. package/dist/cloud.js +0 -272
  115. package/dist/memory.d.ts +0 -4
  116. package/dist/metrics.d.ts +0 -47
  117. package/dist/review/cloud-findings.d.ts +0 -31
  118. package/dist/review/cloud-memory.d.ts +0 -6
  119. package/dist/review/cloud-memory.js +0 -58
  120. package/dist/review/cloud-providers.js +0 -60
  121. package/dist/review/cloud-review.d.ts +0 -19
  122. package/dist/review/cloud-review.js +0 -51
  123. package/dist/review/cloud-settings.d.ts +0 -16
  124. package/dist/review/cloud-settings.js +0 -118
  125. package/dist/review/cloud-upload.d.ts +0 -60
  126. package/dist/review/command.d.ts +0 -33
  127. package/dist/review/command.js +0 -293
  128. package/dist/review/config.js +0 -304
  129. package/dist/review/sarif.d.ts +0 -4
  130. package/dist/review/settings-sources.d.ts +0 -11
  131. package/dist/review/settings-sources.js +0 -58
  132. package/dist/review/target.d.ts +0 -18
  133. package/dist/review/target.js +0 -210
  134. /package/dist/{review → cloud}/account-settings.d.ts +0 -0
  135. /package/dist/{review → commands/review}/plan-render.d.ts +0 -0
  136. /package/dist/{review → commands/review}/render.d.ts +0 -0
  137. /package/dist/{review → commands/review}/runtimes.d.ts +0 -0
  138. /package/dist/{review → io}/terminal.d.ts +0 -0
  139. /package/dist/{review → io}/terminal.js +0 -0
@@ -0,0 +1,96 @@
1
+ import { type ReviewReport } from "@open-cr-agent/core";
2
+ type SarifLevel = "error" | "warning" | "note";
3
+ interface SarifMessage {
4
+ text: string;
5
+ }
6
+ interface SarifResult {
7
+ ruleId: string;
8
+ ruleIndex: number;
9
+ level: SarifLevel;
10
+ message: SarifMessage;
11
+ locations: {
12
+ physicalLocation: {
13
+ artifactLocation: ArtifactLocation;
14
+ region?: {
15
+ startLine: number;
16
+ endLine: number;
17
+ };
18
+ };
19
+ }[];
20
+ partialFingerprints: Record<string, string>;
21
+ baselineState?: "new" | "unchanged";
22
+ fixes?: SarifFix[];
23
+ properties: Record<string, unknown>;
24
+ }
25
+ interface SarifRegion {
26
+ startLine: number;
27
+ startColumn?: number;
28
+ endLine: number;
29
+ endColumn?: number;
30
+ }
31
+ interface SarifFix {
32
+ description: SarifMessage;
33
+ artifactChanges: {
34
+ artifactLocation: ArtifactLocation;
35
+ replacements: {
36
+ deletedRegion: SarifRegion;
37
+ insertedContent: {
38
+ text: string;
39
+ };
40
+ }[];
41
+ }[];
42
+ }
43
+ interface ArtifactLocation {
44
+ uri: string;
45
+ uriBaseId: "%SRCROOT%";
46
+ }
47
+ export declare function renderSarif(report: ReviewReport, version: string): string;
48
+ export declare function sarifLog(report: ReviewReport, version: string): {
49
+ $schema: string;
50
+ version: string;
51
+ runs: {
52
+ automationDetails: {
53
+ id: string;
54
+ };
55
+ tool: {
56
+ driver: {
57
+ name: string;
58
+ informationUri: string;
59
+ version: string;
60
+ semanticVersion: string;
61
+ rules: {
62
+ id: string;
63
+ name: string;
64
+ shortDescription: {
65
+ text: string;
66
+ };
67
+ properties: {
68
+ tags: string[];
69
+ };
70
+ }[];
71
+ };
72
+ };
73
+ invocations: {
74
+ executionSuccessful: boolean;
75
+ toolExecutionNotifications: {
76
+ level: string;
77
+ message: {
78
+ text: string;
79
+ };
80
+ }[];
81
+ }[];
82
+ results: SarifResult[];
83
+ properties: {
84
+ verdict: "approved" | "approved_with_comments" | "minor_issues" | "significant_concerns";
85
+ tier: "full" | "lite" | "trivial";
86
+ baseSha: string;
87
+ headSha: string;
88
+ notReviewed: string[];
89
+ unverifiedCriticals: number;
90
+ costUsd: number;
91
+ };
92
+ }[];
93
+ };
94
+ export declare function plainText(text: string): string;
95
+ export {};
96
+ //# sourceMappingURL=sarif.d.ts.map
@@ -12,6 +12,9 @@ const LEVEL = {
12
12
  suggestion: "note",
13
13
  };
14
14
  export function renderSarif(report, version) {
15
+ return `${serializeOutput(sarifLog(report, version))}\n`;
16
+ }
17
+ export function sarifLog(report, version) {
15
18
  const open = [
16
19
  ...(report.rereview?.unchanged ?? []),
17
20
  ...(report.rereview?.notReproduced ?? []),
@@ -83,7 +86,7 @@ export function renderSarif(report, version) {
83
86
  },
84
87
  ],
85
88
  };
86
- return `${serializeOutput(log)}\n`;
89
+ return log;
87
90
  }
88
91
  // Open findings an earlier review reported, carried over under their own
89
92
  // rule: their reviewer category is not recorded in the review state.
@@ -98,6 +101,7 @@ function fromFinding(f, index) {
98
101
  message: { text: plainText(text) },
99
102
  locations: [location(f.file, f.lineRange)],
100
103
  partialFingerprints: { "ocra/v1": f.fingerprint },
104
+ ...(f.fix ? { fixes: [fix(f.file, f.fix)] } : {}),
101
105
  properties: {
102
106
  reviewer: f.reviewer,
103
107
  severity: f.severity,
@@ -125,16 +129,34 @@ function fromPrior(f, index) {
125
129
  function location(file, lines) {
126
130
  return {
127
131
  physicalLocation: {
128
- // Each segment percent-encoded: a path may hold "#", "?" or spaces,
129
- // which a URI would read as a fragment, a query or an error.
130
- artifactLocation: {
131
- uri: file.split("/").map(encodeURIComponent).join("/"),
132
- uriBaseId: "%SRCROOT%",
133
- },
132
+ artifactLocation: artifactLocation(file),
134
133
  ...(lines ? { region: { startLine: lines.start, endLine: lines.end } } : {}),
135
134
  },
136
135
  };
137
136
  }
137
+ // Each segment percent-encoded: a path may hold "#", "?" or spaces, which a
138
+ // URI would read as a fragment, a query or an error.
139
+ function artifactLocation(file) {
140
+ return { uri: file.split("/").map(encodeURIComponent).join("/"), uriBaseId: "%SRCROOT%" };
141
+ }
142
+ // The inserted text is code and goes in as written: it is content to apply,
143
+ // not a message a viewer renders. A region without columns ends before the
144
+ // last line's newline (SARIF 3.30), which a replacement keeps; a deletion
145
+ // runs to the start of the next line instead, so no empty line is left.
146
+ function fix(file, f) {
147
+ const deletedRegion = f.replacement === ""
148
+ ? { startLine: f.startLine, startColumn: 1, endLine: f.endLine + 1, endColumn: 1 }
149
+ : { startLine: f.startLine, endLine: f.endLine };
150
+ return {
151
+ description: { text: "Replace the lines with the suggested code" },
152
+ artifactChanges: [
153
+ {
154
+ artifactLocation: artifactLocation(file),
155
+ replacements: [{ deletedRegion, insertedContent: { text: f.replacement } }],
156
+ },
157
+ ],
158
+ };
159
+ }
138
160
  // SARIF viewers turn "[text](target)" in a message into a link (SARIF
139
161
  // 3.11.6), and some turn bare web addresses into links too. Text from a
140
162
  // reviewed change or a model must do neither: every backslash and bracket is
@@ -0,0 +1,17 @@
1
+ import type { ListedSetting } from "../../config/settings.js";
2
+ export interface EffectiveSetting {
3
+ key: string;
4
+ value?: unknown;
5
+ source: string;
6
+ }
7
+ export interface AccountVersion {
8
+ version: string | null;
9
+ }
10
+ export declare function effectiveSettings(listed: readonly ListedSetting[]): EffectiveSetting[];
11
+ /** The settings the account set, alone or with other layers. */
12
+ export declare function filledByAccount(listed: readonly ListedSetting[]): string[];
13
+ export declare function renderSettings(settings: readonly EffectiveSetting[], account: AccountVersion | undefined): string;
14
+ export declare function accountOf(account: AccountVersion | undefined): {
15
+ accountSettings?: AccountVersion;
16
+ };
17
+ //# sourceMappingURL=settings-sources.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { forTerminal } from "../../io/terminal.js";
2
+ export function effectiveSettings(listed) {
3
+ return listed.map(({ key, value, sources }) => sources.length === 0 ? { key, source: "default" } : { key, value, source: sources.join("+") });
4
+ }
5
+ /** The settings the account set, alone or with other layers. */
6
+ export function filledByAccount(listed) {
7
+ return listed.filter((s) => s.sources.includes("account")).map((s) => s.key);
8
+ }
9
+ export function renderSettings(settings, account) {
10
+ const lines = [
11
+ "",
12
+ account
13
+ ? `Settings (under your ocra Cloud settings, version ${account.version ?? "none"}):`
14
+ : "Settings:",
15
+ ];
16
+ const set = settings.filter((s) => s.source !== "default");
17
+ const width = Math.max(0, ...set.map((s) => s.key.length));
18
+ for (const s of set) {
19
+ lines.push(` ${s.key.padEnd(width)} ${JSON.stringify(s.value)} (${s.source})`);
20
+ }
21
+ const defaults = settings.filter((s) => s.source === "default").map((s) => s.key);
22
+ if (defaults.length > 0)
23
+ lines.push(` default: ${defaults.join(", ")}`);
24
+ return forTerminal(`${lines.join("\n")}\n`);
25
+ }
26
+ // The account's settings version, for the plan and the provenance.
27
+ export function accountOf(account) {
28
+ return account ? { accountSettings: account } : {};
29
+ }
30
+ //# sourceMappingURL=settings-sources.js.map
@@ -0,0 +1,38 @@
1
+ import type { OcraPlugin, PluginRegistry, VcsAdapter } from "@open-cr-agent/core";
2
+ import type { LocalCode, PlatformTarget, ResolveTargetOptions } from "@open-cr-agent/vcs-platform";
3
+ import { type CliConfig } from "../../config/cli-config.js";
4
+ import type { SettingsLayer } from "../../config/settings.js";
5
+ import type { ReviewArgs } from "./args.js";
6
+ type FoundChangeRequest = Omit<PlatformTarget<unknown>, "createVcs"> & {
7
+ createVcs(registry: PluginRegistry, local: LocalCode, config: CliConfig): VcsAdapter;
8
+ };
9
+ type FindChangeRequest = (options: ResolveTargetOptions) => Promise<FoundChangeRequest>;
10
+ declare const PLATFORMS: {
11
+ github: FindChangeRequest;
12
+ gitlab: FindChangeRequest;
13
+ };
14
+ type Platform = "local" | keyof typeof PLATFORMS;
15
+ export declare const CHANGE_REQUEST: {
16
+ readonly github: "pull request";
17
+ readonly gitlab: "merge request";
18
+ };
19
+ export interface ReviewTarget {
20
+ platform: Platform;
21
+ layers: SettingsLayer[];
22
+ plugins: OcraPlugin[];
23
+ accountPlugins: boolean;
24
+ createVcs(registry: PluginRegistry): VcsAdapter;
25
+ readTrusted?: (path: string) => Promise<string | undefined>;
26
+ publish: boolean;
27
+ repository?: string;
28
+ }
29
+ export interface TargetOptions {
30
+ cwd: string;
31
+ root: string;
32
+ env: Readonly<Record<string, string | undefined>>;
33
+ warn: (message: string) => void;
34
+ fetch?: typeof fetch;
35
+ }
36
+ export declare function resolveReviewTarget(args: ReviewArgs, options: TargetOptions): Promise<ReviewTarget>;
37
+ export {};
38
+ //# sourceMappingURL=target.d.ts.map
@@ -0,0 +1,126 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { resolveGitHubTarget } from "@open-cr-agent/vcs-github";
3
+ import { resolveGitLabTarget } from "@open-cr-agent/vcs-gitlab";
4
+ import { ensureCommits, filesChangedSince, LocalGitAdapter, } from "@open-cr-agent/vcs-local/internal";
5
+ import { loadConfigLayers } from "../../config/cli-config.js";
6
+ import { loadExternalPlugins } from "../../plugins/load.js";
7
+ import { repositoryOfWebUrl } from "../../repository-id.js";
8
+ function platform(resolveTarget, settings) {
9
+ return async (options) => {
10
+ const target = await resolveTarget(options);
11
+ return {
12
+ ...target,
13
+ createVcs: (registry, local, config) => target.createVcs(registry, local, settings(config)),
14
+ };
15
+ };
16
+ }
17
+ // The code review platforms ocra publishes to.
18
+ const PLATFORMS = {
19
+ github: platform(resolveGitHubTarget, (config) => config.github),
20
+ gitlab: platform(resolveGitLabTarget, () => ({})),
21
+ };
22
+ // What a review published to a platform lands on, as the output names it.
23
+ export const CHANGE_REQUEST = {
24
+ github: "pull request",
25
+ gitlab: "merge request",
26
+ };
27
+ export async function resolveReviewTarget(args, options) {
28
+ const inputs = {
29
+ ...options,
30
+ ignoreRepoConfig: args.ignoreRepoConfig === true,
31
+ ...(args.configFile ? { configFile: resolve(options.cwd, args.configFile) } : {}),
32
+ };
33
+ const change = changeRequestOf(args);
34
+ return change ? changeRequestTarget(change, inputs) : localTarget(args, inputs);
35
+ }
36
+ function changeRequestOf(args) {
37
+ const pr = args.pullRequest;
38
+ if (pr) {
39
+ const ref = { number: pr.number, ...(pr.repo ? { repository: pr.repo } : {}) };
40
+ return { platform: "github", ref, publish: pr.publish };
41
+ }
42
+ const mr = args.mergeRequest;
43
+ if (mr) {
44
+ const ref = { number: mr.iid, ...(mr.project ? { repository: mr.project } : {}) };
45
+ return { platform: "gitlab", ref, publish: mr.publish };
46
+ }
47
+ return undefined;
48
+ }
49
+ async function localTarget(args, inputs) {
50
+ const { cwd, root, configFile, ignoreRepoConfig } = inputs;
51
+ const { config, layers } = await loadConfigLayers(root, inputs.env, {
52
+ repository: !ignoreRepoConfig,
53
+ ...(configFile ? { file: configFile } : {}),
54
+ warn: inputs.warn,
55
+ ...(inputs.fetch ? { fetch: inputs.fetch } : {}),
56
+ });
57
+ return {
58
+ platform: "local",
59
+ layers,
60
+ // Plugins named by the user's own file are resolved from where it is.
61
+ plugins: await loadExternalPlugins(config.plugins, configFile ? dirname(configFile) : root),
62
+ accountPlugins: !ignoreRepoConfig,
63
+ createVcs: (registry) => registry.createVcs("local", { cwd, target: args.target }),
64
+ ...(ignoreRepoConfig ? { readTrusted: untrustedTreeReader(args, cwd) } : {}),
65
+ publish: false,
66
+ };
67
+ }
68
+ // --no-repo-config is for code you do not trust, so the files that steer a
69
+ // review (.ocra/memory.json can silence findings, rules and AGENTS.md shape
70
+ // the prompts) are not taken from it either: from the base of a range, which
71
+ // you chose, and otherwise not at all.
72
+ function untrustedTreeReader(args, cwd) {
73
+ if (args.target.mode !== "range")
74
+ return async () => undefined;
75
+ const base = new LocalGitAdapter({ cwd, target: { mode: "commit", commit: args.target.from } });
76
+ return (path) => base.readFile(path);
77
+ }
78
+ // The change request's own files are untrusted: configuration, guidelines
79
+ // and rules come from its base commit, and repository plugins never load.
80
+ async function changeRequestTarget(change, inputs) {
81
+ const { cwd, root, configFile, warn } = inputs;
82
+ const found = await PLATFORMS[change.platform]({
83
+ ref: change.ref,
84
+ env: inputs.env,
85
+ origin: () => originUrl(root),
86
+ warn,
87
+ ...(inputs.fetch ? { fetch: inputs.fetch } : {}),
88
+ });
89
+ await ensureCommits(root, [found.baseSha, found.headSha], found.headRefs);
90
+ const base = new LocalGitAdapter({ cwd, target: { mode: "commit", commit: found.baseSha } });
91
+ const readTrusted = (path) => base.readFile(path);
92
+ const { config, layers } = await loadConfigLayers(root, inputs.env, {
93
+ repository: !inputs.ignoreRepoConfig,
94
+ read: readTrusted,
95
+ ...(configFile ? { file: configFile } : {}),
96
+ warn,
97
+ ...(inputs.fetch ? { fetch: inputs.fetch } : {}),
98
+ });
99
+ if (config.plugins.length > 0) {
100
+ warn(`plugins in the configuration are not loaded for ${CHANGE_REQUEST[change.platform]}s`);
101
+ }
102
+ const local = {
103
+ code: new LocalGitAdapter({
104
+ cwd,
105
+ target: { mode: "range", from: found.baseSha, to: found.headSha },
106
+ }),
107
+ history: { filesChangedSince: (from, to) => filesChangedSince(root, from, to) },
108
+ };
109
+ const repository = repositoryOfWebUrl(found.webUrl);
110
+ return {
111
+ platform: change.platform,
112
+ layers,
113
+ plugins: [],
114
+ accountPlugins: false,
115
+ createVcs: (registry) => found.createVcs(registry, local, config),
116
+ readTrusted,
117
+ publish: change.publish,
118
+ ...(repository ? { repository } : {}),
119
+ };
120
+ }
121
+ function originUrl(root) {
122
+ return new LocalGitAdapter({ cwd: root, target: { mode: "workspace" } })
123
+ .remoteUrl("origin")
124
+ .catch(() => undefined);
125
+ }
126
+ //# sourceMappingURL=target.js.map
@@ -0,0 +1,7 @@
1
+ import { type OcraPlugin } from "@open-cr-agent/core";
2
+ import type { ReviewArgs } from "./review/args.js";
3
+ import type { ReviewDeps } from "./review/deps.js";
4
+ import { type ReviewIo } from "./review/resolve-run.js";
5
+ export declare const BUILTIN_PLUGINS: readonly OcraPlugin[];
6
+ export declare function reviewCommand(args: ReviewArgs, io: ReviewIo, deps: ReviewDeps): Promise<number>;
7
+ //# sourceMappingURL=review.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { agentsMdReviewerPlugin, correctnessReviewerPlugin, docsReviewerPlugin, performanceReviewerPlugin, securityReviewerPlugin, sessionJsonlPlugin, } from "@open-cr-agent/core";
2
+ import { githubPlugin } from "@open-cr-agent/vcs-github";
3
+ import { gitlabPlugin } from "@open-cr-agent/vcs-gitlab";
4
+ import { localGitPlugin } from "@open-cr-agent/vcs-local";
5
+ import { EXIT } from "../io/exit.js";
6
+ import { deliver, writeReport } from "./review/deliver.js";
7
+ import { executeRun } from "./review/execute-run.js";
8
+ import { exitCode } from "./review/exit-code.js";
9
+ import { planRun } from "./review/plan-run.js";
10
+ import { resolveRun } from "./review/resolve-run.js";
11
+ export const BUILTIN_PLUGINS = [
12
+ localGitPlugin,
13
+ githubPlugin,
14
+ gitlabPlugin,
15
+ correctnessReviewerPlugin,
16
+ securityReviewerPlugin,
17
+ performanceReviewerPlugin,
18
+ docsReviewerPlugin,
19
+ agentsMdReviewerPlugin,
20
+ sessionJsonlPlugin,
21
+ ];
22
+ // resolveRun → plan | execute → write → deliver (publish, upload) → exit code.
23
+ export async function reviewCommand(args, io, deps) {
24
+ const run = await resolveRun(args, io, deps);
25
+ if (args.plan)
26
+ return planRun(run, args, io, deps);
27
+ const executed = await executeRun(run, args, io, deps);
28
+ await writeReport(run, executed, args, io, deps);
29
+ if (executed.interrupted)
30
+ return EXIT.interrupted;
31
+ await deliver(run, executed, args, io, deps);
32
+ return exitCode(executed.report, io.err);
33
+ }
34
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1,41 @@
1
+ import { type CustomProvider, type ModelChains, OcraError, type ReviewerOverrides, type RoleSettings, type SourcedRule, type TierEfforts } from "@open-cr-agent/core";
2
+ import { z } from "zod";
3
+ import { type configSchema } from "./schema.js";
4
+ import { type ListedSetting, type SettingSources, type SettingsLayer } from "./settings.js";
5
+ export type CliConfig = Omit<z.infer<typeof configSchema>, "models" | "effort" | "providers" | "runtime"> & {
6
+ runtime: string;
7
+ models: ModelChains;
8
+ effort: TierEfforts;
9
+ providers: Record<string, CustomProvider>;
10
+ rules: SourcedRule[];
11
+ };
12
+ export interface ResolvedConfig {
13
+ config: CliConfig;
14
+ sources: SettingSources;
15
+ ultra: boolean;
16
+ listed: ListedSetting[];
17
+ }
18
+ export interface LoadOptions {
19
+ repository: boolean;
20
+ read?: (path: string) => Promise<string | undefined>;
21
+ file?: string;
22
+ fetch?: typeof fetch;
23
+ warn?: (message: string) => void;
24
+ }
25
+ export declare class ConfigError extends OcraError {
26
+ constructor(message: string, options?: {
27
+ cause?: unknown;
28
+ });
29
+ }
30
+ export declare function loadConfig(root: string, env: Readonly<Record<string, string | undefined>>, options?: LoadOptions): Promise<CliConfig>;
31
+ /** The configuration's layers, and what they resolve to on their own. */
32
+ export declare function loadConfigLayers(root: string, env: Readonly<Record<string, string | undefined>>, options?: LoadOptions): Promise<{
33
+ config: CliConfig;
34
+ layers: SettingsLayer[];
35
+ }>;
36
+ export declare function resolveConfig(layers: readonly SettingsLayer[]): ResolvedConfig;
37
+ export declare function agentChains(config: {
38
+ reviewers: ReviewerOverrides;
39
+ roles: RoleSettings;
40
+ }): Record<string, readonly string[]>;
41
+ //# sourceMappingURL=cli-config.d.ts.map
@@ -0,0 +1,199 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { EFFORT_LEVELS, errorMessage, MODEL_TIERS, OcraError, } from "@open-cr-agent/core";
4
+ import { isNotFound } from "@open-cr-agent/core/internal";
5
+ import { z } from "zod";
6
+ import { fetchRemoteConfig } from "./remote.js";
7
+ import { DEFAULT_RUNTIME, effort } from "./schema.js";
8
+ import { layerSchema, listSettings, resolveSettings, } from "./settings.js";
9
+ const CONFIG_PATH = ".ocra/config.json";
10
+ // OCRA_MODEL_TOP, OCRA_EFFORT_STANDARD and so on.
11
+ const tierEnv = (prefix) => Object.fromEntries(MODEL_TIERS.map((tier) => [tier, `${prefix}_${tier.toUpperCase()}`]));
12
+ const MODEL_ENV = tierEnv("OCRA_MODEL");
13
+ const EFFORT_ENV = tierEnv("OCRA_EFFORT");
14
+ export class ConfigError extends OcraError {
15
+ constructor(message, options) {
16
+ super("CONFIG_INVALID", message, options);
17
+ this.name = "ConfigError";
18
+ }
19
+ }
20
+ // Without the repository's file, only defaults and environment variables
21
+ // apply: that file can name plugins, and plugins run code.
22
+ export async function loadConfig(root, env, options = { repository: true }) {
23
+ return (await loadConfigLayers(root, env, options)).config;
24
+ }
25
+ /** The configuration's layers, and what they resolve to on their own. */
26
+ export async function loadConfigLayers(root, env, options = { repository: true }) {
27
+ const layers = await configLayers(root, env, options);
28
+ const { config } = resolveConfig(layers);
29
+ for (const model of unpriced(config.providers, [
30
+ ...Object.values(config.models),
31
+ ...Object.values(agentChains(config)),
32
+ ])) {
33
+ options.warn?.(`${model} has a price of 0: reported cost and --max-cost-usd do not count its tokens`);
34
+ }
35
+ return { config, layers };
36
+ }
37
+ export function resolveConfig(layers) {
38
+ const { settings, sources } = resolveSettings(layers);
39
+ const { ultra, providers, ...rest } = settings;
40
+ return {
41
+ config: {
42
+ ...rest,
43
+ runtime: settings.runtime ?? DEFAULT_RUNTIME,
44
+ models: defined(settings.models),
45
+ effort: defined(settings.effort),
46
+ providers: toProviders(providers),
47
+ },
48
+ sources,
49
+ ultra,
50
+ listed: listSettings(settings, sources),
51
+ };
52
+ }
53
+ function defined(map) {
54
+ return Object.fromEntries(Object.entries(map).filter(([, value]) => value !== undefined));
55
+ }
56
+ // The configuration's layers, earliest first: a shared configuration it
57
+ // extends, the file, and OCRA_MODEL_<TIER> and OCRA_EFFORT_<TIER> on top.
58
+ async function configLayers(root, env, options) {
59
+ const read = options.read ?? ((path) => readConfigFromDisk(root, path));
60
+ const files = options.file
61
+ ? await fileLayers(ownFile(options.file), options, options.file)
62
+ : options.repository
63
+ ? await fileLayers(read, options, CONFIG_PATH)
64
+ : [];
65
+ return [...files, envLayer(env)];
66
+ }
67
+ function envLayer(env) {
68
+ const models = {};
69
+ const efforts = {};
70
+ for (const tier of MODEL_TIERS) {
71
+ const chain = (env[MODEL_ENV[tier]] ?? "")
72
+ .split(",")
73
+ .map((m) => m.trim())
74
+ .filter((m) => m !== "");
75
+ if (chain.length > 0)
76
+ models[tier] = chain;
77
+ }
78
+ for (const tier of MODEL_TIERS) {
79
+ const name = EFFORT_ENV[tier];
80
+ const value = env[name]?.trim();
81
+ if (!value)
82
+ continue;
83
+ const level = effort.safeParse(value);
84
+ if (!level.success) {
85
+ throw new ConfigError(`${name} must be one of ${EFFORT_LEVELS.join(", ")}`);
86
+ }
87
+ efforts[tier] = level.data;
88
+ }
89
+ return { source: "env", settings: { models, effort: efforts } };
90
+ }
91
+ function toProviders(declared) {
92
+ return Object.fromEntries(Object.entries(declared).map(([id, p]) => [
93
+ id,
94
+ {
95
+ baseUrl: p.baseUrl,
96
+ ...(p.apiKeyEnv ? { apiKeyEnv: p.apiKeyEnv } : {}),
97
+ ...(p.effort ? { effort: p.effort } : {}),
98
+ models: Object.fromEntries(Object.entries(p.models).map(([model, price]) => [
99
+ model,
100
+ {
101
+ input: price.input,
102
+ output: price.output,
103
+ ...(price.cachedInput === undefined ? {} : { cachedInput: price.cachedInput }),
104
+ },
105
+ ])),
106
+ },
107
+ ]));
108
+ }
109
+ // The reviewers' and roles' own chains, by agent id (ADR-0025); a disabled
110
+ // reviewer's is left out, since it makes no call.
111
+ export function agentChains(config) {
112
+ const chains = {};
113
+ for (const [id, setting] of Object.entries(config.reviewers)) {
114
+ if (setting.models && setting.enabled !== false)
115
+ chains[id] = setting.models;
116
+ }
117
+ for (const [role, setting] of Object.entries(config.roles)) {
118
+ if (setting?.models)
119
+ chains[role] = setting.models;
120
+ }
121
+ return chains;
122
+ }
123
+ // Models of declared providers, named in a chain, priced at 0 per token.
124
+ function unpriced(providers, chains) {
125
+ const named = new Set(chains.flatMap((chain) => chain ?? []));
126
+ return [...named].filter((model) => {
127
+ const slash = model.indexOf("/");
128
+ const price = providers[model.slice(0, slash)]?.models[model.slice(slash + 1)];
129
+ return price !== undefined && price.input === 0 && price.output === 0;
130
+ });
131
+ }
132
+ // The user's own checkout, read like any local file (links followed): unlike
133
+ // the review's reads, which never follow links (vcs-local), this is trusted
134
+ // configuration, and --no-repo-config skips it for code that is not.
135
+ function ownFile(file) {
136
+ return async () => {
137
+ try {
138
+ return await readFile(file, "utf8");
139
+ }
140
+ catch (error) {
141
+ throw new ConfigError(`cannot read ${file}: ${errorMessage(error)}`, { cause: error });
142
+ }
143
+ };
144
+ }
145
+ async function readConfigFromDisk(root, path) {
146
+ try {
147
+ return await readFile(join(root, path), "utf8");
148
+ }
149
+ catch (error) {
150
+ if (isNotFound(error))
151
+ return undefined;
152
+ throw error;
153
+ }
154
+ }
155
+ // The file, and the shared configuration it extends under it.
156
+ async function fileLayers(read, options, label) {
157
+ const text = await read(CONFIG_PATH);
158
+ if (text === undefined)
159
+ return [];
160
+ let data;
161
+ try {
162
+ data = JSON.parse(text);
163
+ }
164
+ catch (error) {
165
+ throw new ConfigError(`${label} is not valid JSON: ${errorMessage(error)}`, {
166
+ cause: error,
167
+ });
168
+ }
169
+ const local = layerSchema.safeParse(data);
170
+ if (!local.success)
171
+ throw new ConfigError(`${label} is invalid: ${z.prettifyError(local.error)}`);
172
+ const file = { source: "file", settings: local.data };
173
+ const shared = local.data.extends;
174
+ if (!shared)
175
+ return [file];
176
+ // A shared configuration that cannot be loaded costs its defaults, not the
177
+ // review: the repository's own settings still apply.
178
+ let remote;
179
+ try {
180
+ remote = await fetchRemoteConfig(shared, options.fetch);
181
+ }
182
+ catch (error) {
183
+ // Its limits are lost with it; say so rather than run silently unbounded.
184
+ const limits = local.data.maxCostUsd === undefined
185
+ ? "; none of its settings apply, including any spend or task limit"
186
+ : "; none of its settings apply";
187
+ options.warn?.(`could not load extends ${shared}: ${errorMessage(error)}${limits}`);
188
+ return [file];
189
+ }
190
+ const { rules = [], ...rest } = remote;
191
+ const settings = layerSchema.safeParse(rest);
192
+ if (!settings.success) {
193
+ options.warn?.(`ignoring extends ${shared}: its settings are invalid (${z.prettifyError(settings.error)})`);
194
+ return [file];
195
+ }
196
+ const sourced = rules.map((rule) => ({ ...rule, source: "shared" }));
197
+ return [{ source: "shared", settings: { ...settings.data, rules: sourced } }, file];
198
+ }
199
+ //# sourceMappingURL=cli-config.js.map
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export declare const remoteConfigSchema: z.ZodObject<{
2
+ declare const remoteConfigSchema: z.ZodObject<{
3
3
  $schema: z.ZodOptional<z.ZodString>;
4
4
  models: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
5
5
  light: "light";
@@ -31,5 +31,5 @@ export declare const remoteConfigSchema: z.ZodObject<{
31
31
  }, z.core.$strict>;
32
32
  export type RemoteConfig = z.infer<typeof remoteConfigSchema>;
33
33
  export declare function fetchRemoteConfig(spec: string, fetchImpl?: typeof fetch): Promise<RemoteConfig>;
34
- export declare function mergeConfig(remote: RemoteConfig, local: Record<string, unknown>): Record<string, unknown>;
35
- //# sourceMappingURL=remote-config.d.ts.map
34
+ export {};
35
+ //# sourceMappingURL=remote.d.ts.map