@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
@@ -1,293 +0,0 @@
1
- import { join, relative, resolve } from "node:path";
2
- import { agentsMdReviewerPlugin, correctnessReviewerPlugin, coverageGaps, docsReviewerPlugin, performanceReviewerPlugin, review, securityReviewerPlugin, sessionJsonlPlugin, startPlugins, } from "@open-cr-agent/core";
3
- import { defaultSelectionPolicy, newRunId, previewReview, toPlanOutput, } from "@open-cr-agent/core/internal";
4
- import { githubPlugin } from "@open-cr-agent/vcs-github";
5
- import { gitlabPlugin } from "@open-cr-agent/vcs-gitlab";
6
- import { localGitPlugin } from "@open-cr-agent/vcs-local";
7
- import { findRepositoryRoot } from "@open-cr-agent/vcs-local/internal";
8
- import { pluginsDir } from "../plugin-store.js";
9
- import { VERSION } from "../version.js";
10
- import { accountPlugins } from "./account-plugins.js";
11
- import { withCloudProviders } from "./cloud-providers.js";
12
- import { prepareCloudReview, sendToCloud } from "./cloud-review.js";
13
- import { fetchAccountSettings, layerAccountSettings } from "./cloud-settings.js";
14
- import { cloudEnabled } from "./cloud-upload.js";
15
- import { agentChains, ConfigError } from "./config.js";
16
- import { renderPlan } from "./plan-render.js";
17
- import { ProgressPrinter } from "./progress.js";
18
- import { configHash, requestedSampling } from "./provenance.js";
19
- import { renderJson, renderText, safeJson } from "./render.js";
20
- import { renderSarif } from "./sarif.js";
21
- import { loadSarifLogs } from "./sarif-input.js";
22
- import { effectiveSettings, renderSettings } from "./settings-sources.js";
23
- import { localTarget, mergeRequestTarget, pullRequestTarget } from "./target.js";
24
- import { forTerminal } from "./terminal.js";
25
- export const SESSIONS_DIR = ".ocra/sessions";
26
- export const EXIT = { ok: 0, blocking: 1, error: 2, incomplete: 3, interrupted: 130 };
27
- export const BUILTIN_PLUGINS = [
28
- localGitPlugin,
29
- githubPlugin,
30
- gitlabPlugin,
31
- correctnessReviewerPlugin,
32
- securityReviewerPlugin,
33
- performanceReviewerPlugin,
34
- docsReviewerPlugin,
35
- agentsMdReviewerPlugin,
36
- sessionJsonlPlugin,
37
- ];
38
- export async function reviewCommand(args, io, deps) {
39
- const root = await findRepositoryRoot(deps.cwd);
40
- const warn = (message) => io.err.write(`[ocra] Warning: ${forTerminal(message)}\n`);
41
- const ignoreRepoConfig = args.ignoreRepoConfig === true;
42
- const configFile = args.configFile ? resolve(deps.cwd, args.configFile) : undefined;
43
- const localArgs = configFile ? { ...args, configFile } : args;
44
- const target = args.pullRequest
45
- ? await pullRequestTarget(args.pullRequest, deps.cwd, root, deps.env, warn, deps.fetch, ignoreRepoConfig, configFile)
46
- : args.mergeRequest
47
- ? await mergeRequestTarget(args.mergeRequest, deps.cwd, root, deps.env, warn, deps.fetch, ignoreRepoConfig, configFile)
48
- : await localTarget(localArgs, deps.cwd, root, deps.env, warn, deps.fetch);
49
- // The account's settings fill what the repository leaves out (ADR-0027);
50
- // unreachable, they cost a warning, so a plan still works offline.
51
- const cloudDeps = deps.cloud;
52
- const signedIn = cloudDeps !== undefined && (await cloudEnabled(cloudDeps));
53
- let config = target.config;
54
- let filled = [];
55
- let fromAccount = { plugins: [], pluginSettings: {} };
56
- if (cloudDeps && signedIn) {
57
- const account = await fetchAccountSettings(cloudDeps, warn);
58
- if (account?.settings) {
59
- ({ config, filled } = layerAccountSettings(config, account.settings));
60
- // The account's default for --ultra; there is no --no-ultra to refuse it.
61
- if (account.settings.ultra === true && !args.ultra)
62
- filled.push("ultra");
63
- if (filled.length > 0) {
64
- io.err.write(`[ocra] From your ocra Cloud settings: ${filled.join(", ")}\n`);
65
- }
66
- }
67
- if (account)
68
- fromAccount = account.plugins;
69
- }
70
- const ultra = args.ultra === true || filled.includes("ultra");
71
- const session = { dir: join(root, SESSIONS_DIR), id: newRunId() };
72
- // A plan calls no model, writes no session log and imports no runtime: it
73
- // works without the optional runtime-opencode.
74
- const builtins = args.plan
75
- ? deps.builtinPlugins.filter((p) => p.name !== sessionJsonlPlugin.name)
76
- : [...deps.builtinPlugins, ...(await builtinRuntime(deps.runtimes, config.runtime))];
77
- const account = await accountPlugins(fromAccount, {
78
- allowed: target.accountPlugins,
79
- configured: config.plugins,
80
- taken: [...builtins, ...target.plugins],
81
- dir: pluginsDir(deps.env),
82
- warn,
83
- });
84
- // The repository's own settings win; a repository plugin's come from it alone.
85
- const pluginSettings = { ...account.settings, ...config.pluginSettings };
86
- const registry = await startPlugins([...builtins, ...target.plugins, ...account.plugins], {
87
- settings: args.plan
88
- ? pluginSettings
89
- : { ...pluginSettings, [sessionJsonlPlugin.name]: session },
90
- env: deps.env,
91
- warn,
92
- });
93
- const vcs = target.createVcs(registry);
94
- const rules = [
95
- ...registry.rules.map((rule) => ({ ...rule, source: "plugin" })),
96
- ...config.rules,
97
- ];
98
- const overrides = reviewerOverrides(config, args, registry.reviewers.map((r) => r.id));
99
- if (args.plan) {
100
- const preview = await previewReview({
101
- vcs,
102
- reviewers: registry.reviewers,
103
- reviewerOverrides: overrides,
104
- effort: config.effort,
105
- roles: config.roles,
106
- models: config.models,
107
- rules,
108
- selection: { ...defaultSelectionPolicy, include: config.include, exclude: config.exclude },
109
- ...(target.readTrusted ? { readTrusted: target.readTrusted } : {}),
110
- ...(ultra ? { ultra: true } : {}),
111
- ...(config.maxTasks !== undefined ? { maxTasks: config.maxTasks } : {}),
112
- });
113
- const settings = effectiveSettings(target.config, config, filled);
114
- const rendered = args.format === "json"
115
- ? `${safeJson({ ...toPlanOutput(preview), settings, ...accountOf(config) })}\n`
116
- : renderPlan(preview) + renderSettings(settings, config.accountSettings);
117
- if (args.output === undefined)
118
- io.out.write(rendered);
119
- else
120
- await deps.writeFile(resolve(deps.cwd, args.output), rendered);
121
- return EXIT.ok;
122
- }
123
- const sarif = await loadSarifLogs(args.importSarif ?? [], deps.cwd);
124
- // The repository's hash and the account's memory for it (ADR-0028).
125
- const cloudReview = cloudDeps && signedIn ? await prepareCloudReview(root, cloudDeps, warn) : undefined;
126
- const sampling = requestedSampling(config, args);
127
- const models = config.models;
128
- const agentModels = agentChains({ reviewers: overrides, roles: config.roles });
129
- const cloud = await withCloudProviders([...Object.values(models), ...Object.values(agentModels)], config.providers, deps.env, cloudDeps, warn);
130
- const runtime = registry.createRuntime(config.runtime, {
131
- models,
132
- ...(Object.keys(agentModels).length > 0 ? { agentModels } : {}),
133
- env: cloud.env,
134
- providers: cloud.providers,
135
- ...(Object.keys(sampling).length > 0 ? { sampling } : {}),
136
- });
137
- const progress = new ProgressPrinter(io.err, { heartbeatMs: deps.heartbeatMs, now: deps.now });
138
- const interrupt = new AbortController();
139
- const stopListening = deps.onInterrupt?.(() => {
140
- io.err.write("[ocra] Interrupted: stopping and writing a partial report (Ctrl-C again quits now)\n");
141
- interrupt.abort();
142
- });
143
- let report;
144
- const started = deps.now();
145
- try {
146
- report = await review({
147
- runId: session.id,
148
- signal: interrupt.signal,
149
- ...runOptions(config),
150
- models,
151
- reviewerOverrides: overrides,
152
- provenance: {
153
- ocraVersion: VERSION,
154
- configHash: configHash(config, ultra ? { ...args, ultra: true } : args),
155
- sampling,
156
- ...accountOf(config),
157
- },
158
- ...(args.maxCostUsd !== undefined ? { maxCostUsd: args.maxCostUsd } : {}),
159
- ...(ultra ? { ultra: true } : {}),
160
- ...(args.full ? { fullReview: true } : {}),
161
- ...(sarif.length > 0 ? { sarif } : {}),
162
- ...(target.readTrusted ? { readTrusted: target.readTrusted } : {}),
163
- vcs,
164
- runtime,
165
- reviewers: registry.reviewers,
166
- rules,
167
- ...(cloudReview?.memory.length ? { accountMemory: cloudReview.memory } : {}),
168
- onEvent: (event) => {
169
- registry.emit(event);
170
- progress.onEvent(event);
171
- },
172
- });
173
- }
174
- finally {
175
- stopListening?.();
176
- progress.stop();
177
- await runtime.dispose?.();
178
- }
179
- const sessionDir = relative(deps.cwd, join(session.dir, session.id)) || ".";
180
- const rendered = args.format === "json"
181
- ? renderJson(report)
182
- : args.format === "sarif"
183
- ? renderSarif(report, VERSION)
184
- : renderText(report, sessionDir);
185
- if (args.output === undefined) {
186
- io.out.write(rendered);
187
- }
188
- else {
189
- await deps.writeFile(resolve(deps.cwd, args.output), rendered);
190
- io.err.write(`[ocra] Wrote ${args.output}\n`);
191
- }
192
- const hidden = report.remembered.filter((e) => e.source === "account").length;
193
- if (hidden > 0)
194
- io.err.write(`[ocra] ${hidden} finding(s) hidden by your ocra Cloud memory\n`);
195
- if (interrupt.signal.aborted)
196
- return EXIT.interrupted;
197
- if (target.publish) {
198
- // Stopping halfway could post the inline comments without the summary
199
- // that remembers them; a first Ctrl-C only warns.
200
- const stopHolding = deps.onInterrupt?.(() => {
201
- io.err.write("[ocra] Publishing; Ctrl-C again quits now and may leave the pull request half updated\n");
202
- });
203
- try {
204
- const published = await vcs.publish(report);
205
- for (const warning of published.warnings)
206
- warn(warning);
207
- }
208
- finally {
209
- stopHolding?.();
210
- }
211
- io.err.write(`[ocra] Published the review to the ${target.publishesTo ?? "change request"}\n`);
212
- }
213
- if (cloudReview && cloudDeps && !args.noUpload) {
214
- await sendToCloud(report, cloudReview, cloudDeps, {
215
- source: args.pullRequest ? "github" : args.mergeRequest ? "gitlab" : "local",
216
- durationMs: deps.now() - started,
217
- err: io.err,
218
- warn,
219
- });
220
- }
221
- return exitCode(report, io.err);
222
- }
223
- function accountOf(config) {
224
- return config.accountSettings ? { accountSettings: config.accountSettings } : {};
225
- }
226
- // A runtime not built in comes from a plugin of the configuration.
227
- async function builtinRuntime(runtimes, name) {
228
- const load = Object.hasOwn(runtimes, name) ? runtimes[name] : undefined;
229
- return load ? [await load()] : [];
230
- }
231
- function runOptions(config) {
232
- const options = {
233
- selection: { ...defaultSelectionPolicy, include: config.include, exclude: config.exclude },
234
- effort: config.effort,
235
- roles: config.roles,
236
- };
237
- if (config.concurrency !== undefined)
238
- options.concurrency = config.concurrency;
239
- if (config.taskTimeoutMinutes !== undefined)
240
- options.taskTimeoutMs = config.taskTimeoutMinutes * 60_000;
241
- if (config.verify !== undefined)
242
- options.verify = config.verify;
243
- if (config.judge !== undefined)
244
- options.judge = config.judge;
245
- if (config.maxCostUsd !== undefined)
246
- options.maxCostUsd = config.maxCostUsd;
247
- if (config.maxTasks !== undefined)
248
- options.maxTasks = config.maxTasks;
249
- if (config.runTimeoutMinutes !== undefined)
250
- options.runTimeoutMs = config.runTimeoutMinutes * 60_000;
251
- return options;
252
- }
253
- // --reviewers narrows the run to the named reviewers on top of the config.
254
- function reviewerOverrides(config, args, registered) {
255
- if (args.reviewers === undefined)
256
- return config.reviewers;
257
- const unknown = args.reviewers.filter((id) => !registered.includes(id));
258
- if (unknown.length > 0) {
259
- throw new ConfigError(`Unknown reviewer(s): ${unknown.join(", ")} (available: ${registered.join(", ")})`);
260
- }
261
- const overrides = { ...config.reviewers };
262
- for (const id of registered) {
263
- if (!args.reviewers.includes(id))
264
- overrides[id] = { ...overrides[id], enabled: false };
265
- }
266
- return overrides;
267
- }
268
- function exitCode(report, err) {
269
- if (report.tasks.length > 0 && report.tasks.every((t) => t.status !== "completed")) {
270
- err.write("[ocra] No review task completed; see the errors above.\n");
271
- return EXIT.error;
272
- }
273
- // Incomplete comes first, even over a blocking verdict: the action lets
274
- // exit 1 pass unless fail-on-concerns is set, and a review that missed
275
- // files or could not check a critical finding must never pass.
276
- const { notReviewed } = coverageGaps(report);
277
- if (notReviewed > 0) {
278
- err.write(`[ocra] ${notReviewed} selected file(s) were not reviewed; the review is incomplete.\n`);
279
- return EXIT.incomplete;
280
- }
281
- if (report.unverifiedCriticals > 0) {
282
- err.write(`[ocra] ${report.unverifiedCriticals} critical finding(s) could not be verified; the review is incomplete.\n`);
283
- return EXIT.incomplete;
284
- }
285
- if (report.verdict !== "significant_concerns")
286
- return EXIT.ok;
287
- const override = report.changeRequest.override;
288
- if (!override)
289
- return EXIT.blocking;
290
- err.write(`[ocra] The blocking verdict was overridden by ${forTerminal(override.by)}: ${forTerminal(override.reason)}\n`);
291
- return EXIT.ok;
292
- }
293
- //# sourceMappingURL=command.js.map
@@ -1,304 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { OcraError, } from "@open-cr-agent/core";
4
- import { AGENT_ROLES, EFFORT_LEVELS, RISK_TIERS } from "@open-cr-agent/core/internal";
5
- import { z } from "zod";
6
- import { fetchRemoteConfig, mergeConfig } from "./remote-config.js";
7
- export const CONFIG_PATH = ".ocra/config.json";
8
- // How much a model reasons before answering (ADR-0025); unset leaves the
9
- // provider's default.
10
- const effort = z.enum(EFFORT_LEVELS);
11
- const modelChain = z
12
- .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
13
- .transform((value) => (typeof value === "string" ? [value] : value));
14
- // Where review code is sent: over https, or plain http only to this machine.
15
- // OpenCode replaces {env:NAME} and {file:path} anywhere in its configuration,
16
- // so an address with braces could carry a variable or a file (the checkout's
17
- // .git/config holds its token) to the endpoint.
18
- const endpoint = z
19
- .url()
20
- .refine((value) => {
21
- const url = new URL(value);
22
- if (url.protocol === "https:")
23
- return true;
24
- const host = url.hostname.replace(/^\[|\]$/g, "");
25
- return (url.protocol === "http:" &&
26
- (host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host)));
27
- }, "must be an https URL, or http on this machine (localhost, 127.0.0.1, ::1)")
28
- .refine((value) => !/[{}]/.test(value), "must not contain { or }");
29
- // The platforms' and clouds' credentials never go to a model endpoint; the
30
- // same prefixes are never passed to the runtime by prefix (server-env.ts).
31
- const PLATFORM_TOKENS = /^(GITHUB_|GH_|GITLAB_|CI_|ACTIONS_|RUNNER_|AWS_|AZURE_|NPM_|SSH_|OCRA_)/;
32
- const price = z.number().min(0).max(10_000);
33
- const providerSchema = z
34
- .object({
35
- type: z.literal("openai-compatible"),
36
- baseUrl: endpoint,
37
- apiKeyEnv: z
38
- .string()
39
- .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be an environment variable name")
40
- .refine((name) => !PLATFORM_TOKENS.test(name), "must not name a platform or cloud credential")
41
- .optional(),
42
- // Prices in US dollars per million tokens; 0 means unpriced.
43
- models: z
44
- .record(z.string().regex(/^[\w./:@-]{1,200}$/), z.object({ input: price, output: price, cachedInput: price.optional() }).strict())
45
- .refine((models) => Object.keys(models).length > 0, "must list at least one model"),
46
- // How the endpoint takes a reasoning effort: "openai" sends
47
- // reasoning_effort (the default), "openrouter" sends reasoning.effort.
48
- effort: z.enum(["openai", "openrouter"]).optional(),
49
- })
50
- .strict();
51
- // The runtimes ocra ships; a plugin may register another name, so the
52
- // schema lists these for editors and still accepts any other string.
53
- const BUILTIN_RUNTIMES = ["opencode", "direct"];
54
- export const DEFAULT_RUNTIME = "opencode";
55
- export const configSchema = z
56
- .object({
57
- $schema: z.string().optional(),
58
- models: z
59
- .object({ top: modelChain, standard: modelChain, light: modelChain })
60
- .partial()
61
- .strict()
62
- .default({}),
63
- effort: z
64
- .object({ top: effort, standard: effort, light: effort })
65
- .partial()
66
- .strict()
67
- .default({}),
68
- concurrency: z.number().int().min(1).max(32).optional(),
69
- taskTimeoutMinutes: z.number().positive().optional(),
70
- runTimeoutMinutes: z.number().positive().optional(),
71
- verify: z.boolean().optional(),
72
- judge: z.boolean().optional(),
73
- maxCostUsd: z.number().positive().optional(),
74
- maxTasks: z.number().int().min(1).max(1_000).optional(),
75
- // Unset: each provider's default. A seed makes sampling repeatable only
76
- // where the provider supports it.
77
- sampling: z
78
- .object({
79
- temperature: z.number().min(0).max(2),
80
- seed: z
81
- .number()
82
- .int()
83
- .min(0)
84
- .max(2 ** 31 - 1),
85
- })
86
- .partial()
87
- .strict()
88
- .optional(),
89
- github: z
90
- .object({ requestChanges: z.boolean(), botLogin: z.string().min(1) })
91
- .partial()
92
- .strict()
93
- .default({}),
94
- include: z.array(z.string().min(1)).default([]),
95
- exclude: z.array(z.string().min(1)).default([]),
96
- // Unset means "opencode"; loadConfig fills it in and records whether the
97
- // file chose, so account defaults (ADR-0025) apply only where it did not.
98
- runtime: z.enum(BUILTIN_RUNTIMES).or(z.string().min(1)).optional(),
99
- plugins: z.array(z.string().min(1)).default([]),
100
- reviewers: z
101
- .record(z.string(), z
102
- .object({
103
- enabled: z.boolean(),
104
- minTier: z.enum(RISK_TIERS),
105
- // The reviewer's own chain, for its review tasks and plan call.
106
- models: modelChain,
107
- effort,
108
- })
109
- .partial()
110
- .strict())
111
- .default({}),
112
- roles: z
113
- .partialRecord(z.enum(AGENT_ROLES), z.object({ models: modelChain, effort }).partial().strict())
114
- .default({}),
115
- pluginSettings: z.record(z.string(), z.unknown()).default({}),
116
- providers: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,39}$/), providerSchema).default({}),
117
- extends: z.string().min(1).optional(),
118
- })
119
- .strict();
120
- export const CONFIG_SCHEMA_ID = "https://ocra.majincheng.com/schema/config.v1.json";
121
- // JSON Schema (draft 2020-12) of .ocra/config.json, for editors: what a
122
- // file may say, so defaults are optional and model chains take either form.
123
- export function configJsonSchema() {
124
- const generated = z.toJSONSchema(configSchema, { target: "draft-2020-12", io: "input" });
125
- const { $schema, ...rest } = generated;
126
- return {
127
- $schema,
128
- $id: CONFIG_SCHEMA_ID,
129
- title: "ocra configuration",
130
- description: "The .ocra/config.json file of a repository, or a file passed with --config. Unknown keys are rejected.",
131
- ...rest,
132
- };
133
- }
134
- const MODEL_ENV = {
135
- top: "OCRA_MODEL_TOP",
136
- standard: "OCRA_MODEL_STANDARD",
137
- light: "OCRA_MODEL_LIGHT",
138
- };
139
- const EFFORT_ENV = {
140
- top: "OCRA_EFFORT_TOP",
141
- standard: "OCRA_EFFORT_STANDARD",
142
- light: "OCRA_EFFORT_LIGHT",
143
- };
144
- export class ConfigError extends OcraError {
145
- constructor(message, options) {
146
- super("CONFIG_INVALID", message, options);
147
- this.name = "ConfigError";
148
- }
149
- }
150
- // Without the repository's file, only defaults and environment variables
151
- // apply: that file can name plugins, and plugins run code.
152
- export async function loadConfig(root, env, options = { repository: true }) {
153
- const read = options.read ?? ((path) => readConfigFromDisk(root, path));
154
- const { config: parsed, rules } = options.file
155
- ? await readConfigFile(ownFile(options.file), options, options.file)
156
- : options.repository
157
- ? await readConfigFile(read, options, CONFIG_PATH)
158
- : { config: configSchema.parse({}), rules: [] };
159
- const models = {};
160
- for (const [tier, chain] of Object.entries(parsed.models)) {
161
- if (chain)
162
- models[tier] = chain;
163
- }
164
- for (const [tier, name] of Object.entries(MODEL_ENV)) {
165
- const chain = (env[name] ?? "")
166
- .split(",")
167
- .map((m) => m.trim())
168
- .filter((m) => m !== "");
169
- if (chain.length > 0)
170
- models[tier] = chain;
171
- }
172
- const efforts = {
173
- ...parsed.effort,
174
- };
175
- for (const [tier, name] of Object.entries(EFFORT_ENV)) {
176
- const value = env[name]?.trim();
177
- if (!value)
178
- continue;
179
- const level = effort.safeParse(value);
180
- if (!level.success) {
181
- throw new ConfigError(`${name} must be one of ${EFFORT_LEVELS.join(", ")}`);
182
- }
183
- efforts[tier] = level.data;
184
- }
185
- for (const model of unpriced(parsed.providers, [
186
- ...Object.values(models),
187
- ...Object.values(agentChains(parsed)),
188
- ])) {
189
- options.warn?.(`${model} has a price of 0: reported cost and --max-cost-usd do not count its tokens`);
190
- }
191
- return {
192
- ...parsed,
193
- runtime: parsed.runtime ?? DEFAULT_RUNTIME,
194
- runtimeSet: parsed.runtime !== undefined,
195
- models,
196
- effort: efforts,
197
- providers: toProviders(parsed.providers),
198
- rules: rules.map((rule) => ({ ...rule, source: "shared" })),
199
- };
200
- }
201
- function toProviders(declared) {
202
- return Object.fromEntries(Object.entries(declared).map(([id, p]) => [
203
- id,
204
- {
205
- baseUrl: p.baseUrl,
206
- ...(p.apiKeyEnv ? { apiKeyEnv: p.apiKeyEnv } : {}),
207
- ...(p.effort ? { effort: p.effort } : {}),
208
- models: Object.fromEntries(Object.entries(p.models).map(([model, price]) => [
209
- model,
210
- {
211
- input: price.input,
212
- output: price.output,
213
- ...(price.cachedInput === undefined ? {} : { cachedInput: price.cachedInput }),
214
- },
215
- ])),
216
- },
217
- ]));
218
- }
219
- // The reviewers' and roles' own chains, by agent id (ADR-0025); a disabled
220
- // reviewer's is left out, since it makes no call.
221
- export function agentChains(config) {
222
- const chains = {};
223
- for (const [id, setting] of Object.entries(config.reviewers)) {
224
- if (setting.models && setting.enabled !== false)
225
- chains[id] = setting.models;
226
- }
227
- for (const [role, setting] of Object.entries(config.roles)) {
228
- if (setting?.models)
229
- chains[role] = setting.models;
230
- }
231
- return chains;
232
- }
233
- // Models of declared providers, named in a chain, priced at 0 per token.
234
- function unpriced(providers, chains) {
235
- const named = new Set(chains.flatMap((chain) => chain ?? []));
236
- return [...named].filter((model) => {
237
- const slash = model.indexOf("/");
238
- const price = providers[model.slice(0, slash)]?.models[model.slice(slash + 1)];
239
- return price !== undefined && price.input === 0 && price.output === 0;
240
- });
241
- }
242
- // The user's own checkout, read like any local file (links followed): unlike
243
- // the review's reads, which never follow links (vcs-local), this is trusted
244
- // configuration, and --no-repo-config skips it for code that is not.
245
- function ownFile(file) {
246
- return async () => {
247
- try {
248
- return await readFile(file, "utf8");
249
- }
250
- catch (error) {
251
- throw new ConfigError(`cannot read ${file}: ${error.message}`, { cause: error });
252
- }
253
- };
254
- }
255
- async function readConfigFromDisk(root, path) {
256
- try {
257
- return await readFile(join(root, path), "utf8");
258
- }
259
- catch (error) {
260
- if (error.code === "ENOENT")
261
- return undefined;
262
- throw error;
263
- }
264
- }
265
- async function readConfigFile(read, options, label) {
266
- const text = await read(CONFIG_PATH);
267
- if (text === undefined)
268
- return { config: configSchema.parse({}), rules: [] };
269
- let data;
270
- try {
271
- data = JSON.parse(text);
272
- }
273
- catch (error) {
274
- throw new ConfigError(`${label} is not valid JSON: ${error.message}`, {
275
- cause: error,
276
- });
277
- }
278
- const local = configSchema.safeParse(data);
279
- if (!local.success)
280
- throw new ConfigError(`${label} is invalid: ${z.prettifyError(local.error)}`);
281
- if (!local.data.extends)
282
- return { config: local.data, rules: [] };
283
- // A shared configuration that cannot be loaded costs its defaults, not the
284
- // review: the repository's own settings still apply.
285
- let remote;
286
- try {
287
- remote = await fetchRemoteConfig(local.data.extends, options.fetch);
288
- }
289
- catch (error) {
290
- // Its limits are lost with it; say so rather than run silently unbounded.
291
- const limits = local.data.maxCostUsd === undefined
292
- ? "; none of its settings apply, including any spend or task limit"
293
- : "; none of its settings apply";
294
- options.warn?.(`could not load extends ${local.data.extends}: ${error.message}${limits}`);
295
- return { config: local.data, rules: [] };
296
- }
297
- const merged = configSchema.safeParse(mergeConfig(remote, data));
298
- if (!merged.success) {
299
- options.warn?.(`ignoring extends ${local.data.extends}: its settings are invalid (${z.prettifyError(merged.error)})`);
300
- return { config: local.data, rules: [] };
301
- }
302
- return { config: merged.data, rules: remote.rules ?? [] };
303
- }
304
- //# sourceMappingURL=config.js.map
@@ -1,4 +0,0 @@
1
- import { type ReviewReport } from "@open-cr-agent/core";
2
- export declare function renderSarif(report: ReviewReport, version: string): string;
3
- export declare function plainText(text: string): string;
4
- //# sourceMappingURL=sarif.d.ts.map
@@ -1,11 +0,0 @@
1
- import type { CliConfig } from "./config.js";
2
- export type SettingSource = "file" | "account" | "file+account" | "default";
3
- export interface EffectiveSetting {
4
- key: string;
5
- value?: unknown;
6
- source: SettingSource;
7
- }
8
- /** file: the configuration before the account's settings; effective: after; filled: what they filled. */
9
- export declare function effectiveSettings(file: CliConfig, effective: CliConfig, filled: readonly string[]): EffectiveSetting[];
10
- export declare function renderSettings(settings: readonly EffectiveSetting[], account: CliConfig["accountSettings"]): string;
11
- //# sourceMappingURL=settings-sources.d.ts.map
@@ -1,58 +0,0 @@
1
- import { ACCOUNT_SCALARS } from "./cloud-settings.js";
2
- import { forTerminal } from "./terminal.js";
3
- const TIERS = ["top", "standard", "light"];
4
- /** file: the configuration before the account's settings; effective: after; filled: what they filled. */
5
- export function effectiveSettings(file, effective, filled) {
6
- const settings = [];
7
- const add = (key, value, fromFile) => {
8
- const fromAccount = filled.includes(key);
9
- const source = fromFile && fromAccount
10
- ? "file+account"
11
- : fromAccount
12
- ? "account"
13
- : fromFile
14
- ? "file"
15
- : "default";
16
- settings.push(source === "default" ? { key, source } : { key, value, source });
17
- };
18
- add("runtime", effective.runtime, file.runtimeSet);
19
- for (const tier of TIERS) {
20
- add(`models.${tier}`, effective.models[tier], file.models[tier] !== undefined);
21
- }
22
- for (const tier of TIERS) {
23
- add(`effort.${tier}`, effective.effort[tier], file.effort[tier] !== undefined);
24
- }
25
- for (const [id, entry] of Object.entries(effective.reviewers)) {
26
- add(`reviewers.${id}`, entry, file.reviewers[id] !== undefined);
27
- }
28
- for (const [role, entry] of Object.entries(effective.roles)) {
29
- add(`roles.${role}`, entry, file.roles[role] !== undefined);
30
- }
31
- for (const key of ACCOUNT_SCALARS)
32
- add(key, effective[key], file[key] !== undefined);
33
- add("include", effective.include, file.include.length > 0);
34
- add("exclude", effective.exclude, file.exclude.length > 0);
35
- add("rules", effective.rules.map(({ path, source }) => ({ path, source })), file.rules.length > 0);
36
- // A flag rather than a key: listed only when the account turned it on.
37
- if (filled.includes("ultra"))
38
- settings.push({ key: "ultra", value: true, source: "account" });
39
- return settings;
40
- }
41
- export function renderSettings(settings, account) {
42
- const lines = [
43
- "",
44
- account
45
- ? `Settings (under your ocra Cloud settings, version ${account.version ?? "none"}):`
46
- : "Settings:",
47
- ];
48
- const set = settings.filter((s) => s.source !== "default");
49
- const width = Math.max(0, ...set.map((s) => s.key.length));
50
- for (const s of set) {
51
- lines.push(` ${s.key.padEnd(width)} ${JSON.stringify(s.value)} (${s.source})`);
52
- }
53
- const defaults = settings.filter((s) => s.source === "default").map((s) => s.key);
54
- if (defaults.length > 0)
55
- lines.push(` default: ${defaults.join(", ")}`);
56
- return forTerminal(`${lines.join("\n")}\n`);
57
- }
58
- //# sourceMappingURL=settings-sources.js.map