@open-cr-agent/cli 0.4.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.
- package/dist/cloud/account-salt.d.ts +6 -0
- package/dist/cloud/account-salt.js +33 -0
- package/dist/cloud/account-settings.d.ts +100 -0
- package/dist/cloud/account-settings.js +107 -0
- package/dist/cloud/browser.d.ts +17 -0
- package/dist/cloud/browser.js +37 -0
- package/dist/cloud/client.d.ts +93 -0
- package/dist/cloud/client.js +221 -0
- package/dist/cloud/credentials.d.ts +18 -0
- package/dist/cloud/credentials.js +52 -0
- package/dist/cloud/deps.d.ts +14 -0
- package/dist/cloud/deps.js +34 -0
- package/dist/cloud/file-lock.d.ts +14 -0
- package/dist/cloud/file-lock.js +77 -0
- package/dist/cloud/findings.d.ts +8 -0
- package/dist/cloud/findings.js +58 -0
- package/dist/cloud/gateway-token.d.ts +18 -0
- package/dist/cloud/gateway-token.js +63 -0
- package/dist/cloud/memory.d.ts +8 -0
- package/dist/cloud/memory.js +68 -0
- package/dist/cloud/private-file.d.ts +8 -0
- package/dist/cloud/private-file.js +46 -0
- package/dist/cloud/providers.d.ts +18 -0
- package/dist/cloud/providers.js +132 -0
- package/dist/cloud/review.d.ts +28 -0
- package/dist/cloud/review.js +76 -0
- package/dist/cloud/settings.d.ts +22 -0
- package/dist/cloud/settings.js +64 -0
- package/dist/cloud/upload.d.ts +20 -0
- package/dist/cloud/upload.js +183 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +164 -0
- package/dist/commands/memory.d.ts +3 -0
- package/dist/{memory.js → commands/memory.js} +21 -20
- package/dist/commands/metrics.d.ts +45 -0
- package/dist/{metrics.js → commands/metrics.js} +32 -45
- package/dist/commands/plugins.d.ts +9 -0
- package/dist/commands/plugins.js +159 -0
- package/dist/{review → commands/review}/args.d.ts +7 -10
- package/dist/{review → commands/review}/args.js +11 -16
- package/dist/commands/review/deliver.d.ts +7 -0
- package/dist/commands/review/deliver.js +56 -0
- package/dist/commands/review/deps.d.ts +18 -0
- package/dist/commands/review/deps.js +2 -0
- package/dist/commands/review/execute-run.d.ts +13 -0
- package/dist/commands/review/execute-run.js +97 -0
- package/dist/commands/review/exit-code.d.ts +4 -0
- package/dist/commands/review/exit-code.js +29 -0
- package/dist/commands/review/plan-prices.d.ts +3 -0
- package/dist/commands/review/plan-prices.js +18 -0
- package/dist/{review → commands/review}/plan-render.js +22 -2
- package/dist/commands/review/plan-run.d.ts +5 -0
- package/dist/commands/review/plan-run.js +38 -0
- package/dist/commands/review/plan-schema.d.ts +97 -0
- package/dist/commands/review/plan-schema.js +34 -0
- package/dist/{review → commands/review}/progress.d.ts +3 -5
- package/dist/{review → commands/review}/progress.js +3 -3
- package/dist/{review → commands/review}/provenance.d.ts +4 -2
- package/dist/{review → commands/review}/provenance.js +11 -2
- package/dist/{review → commands/review}/render.js +8 -3
- package/dist/commands/review/resolve-run.d.ts +29 -0
- package/dist/commands/review/resolve-run.js +125 -0
- package/dist/commands/review/review-options.d.ts +6 -0
- package/dist/commands/review/review-options.js +35 -0
- package/dist/{review → commands/review}/runtimes.js +1 -1
- package/dist/{review → commands/review}/sarif-input.d.ts +0 -1
- package/dist/{review → commands/review}/sarif-input.js +5 -5
- package/dist/commands/review/sarif.d.ts +96 -0
- package/dist/{review → commands/review}/sarif.js +29 -7
- package/dist/commands/review/settings-sources.d.ts +17 -0
- package/dist/commands/review/settings-sources.js +30 -0
- package/dist/commands/review/target.d.ts +38 -0
- package/dist/commands/review/target.js +126 -0
- package/dist/commands/review.d.ts +7 -0
- package/dist/commands/review.js +34 -0
- package/dist/config/cli-config.d.ts +41 -0
- package/dist/config/cli-config.js +199 -0
- package/dist/{review/remote-config.d.ts → config/remote.d.ts} +3 -3
- package/dist/{review/remote-config.js → config/remote.js} +5 -23
- package/dist/{review/config.d.ts → config/schema.d.ts} +13 -25
- package/dist/config/schema.js +138 -0
- package/dist/config/settings.d.ts +37 -0
- package/dist/config/settings.js +133 -0
- package/dist/config/user-dir.d.ts +3 -0
- package/dist/config/user-dir.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.js +7 -0
- package/dist/io/exit.d.ts +9 -0
- package/dist/io/exit.js +12 -0
- package/dist/io/output.d.ts +4 -0
- package/dist/io/output.js +2 -0
- package/dist/io/usage-error.d.ts +5 -0
- package/dist/io/usage-error.js +8 -0
- package/dist/main.js +2 -2
- package/dist/plugins/account.d.ts +23 -0
- package/dist/plugins/account.js +54 -0
- package/dist/plugins/load.d.ts +13 -0
- package/dist/plugins/load.js +78 -0
- package/dist/plugins/npm.d.ts +8 -0
- package/dist/plugins/npm.js +24 -0
- package/dist/plugins/store.d.ts +20 -0
- package/dist/plugins/store.js +85 -0
- package/dist/repository-id.d.ts +12 -0
- package/dist/repository-id.js +72 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +32 -10
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +31 -0
- package/package.json +17 -7
- package/dist/cloud.d.ts +0 -32
- package/dist/cloud.js +0 -262
- package/dist/memory.d.ts +0 -4
- package/dist/metrics.d.ts +0 -47
- package/dist/review/cloud-providers.d.ts +0 -12
- package/dist/review/cloud-providers.js +0 -59
- package/dist/review/cloud-upload.d.ts +0 -40
- package/dist/review/cloud-upload.js +0 -135
- package/dist/review/command.d.ts +0 -31
- package/dist/review/command.js +0 -240
- package/dist/review/config.js +0 -280
- package/dist/review/plugins.d.ts +0 -3
- package/dist/review/plugins.js +0 -32
- package/dist/review/sarif.d.ts +0 -4
- package/dist/review/target.d.ts +0 -17
- package/dist/review/target.js +0 -207
- /package/dist/{review → commands/review}/plan-render.d.ts +0 -0
- /package/dist/{review → commands/review}/render.d.ts +0 -0
- /package/dist/{review → commands/review}/runtimes.d.ts +0 -0
- /package/dist/{review → io}/terminal.d.ts +0 -0
- /package/dist/{review → io}/terminal.js +0 -0
package/dist/review/command.js
DELETED
|
@@ -1,240 +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 { VERSION } from "../version.js";
|
|
9
|
-
import { withCloudProviders } from "./cloud-providers.js";
|
|
10
|
-
import { cloudEnabled, defaultModels, repoHash, uploadOf, uploadReview } from "./cloud-upload.js";
|
|
11
|
-
import { ConfigError } from "./config.js";
|
|
12
|
-
import { renderPlan } from "./plan-render.js";
|
|
13
|
-
import { ProgressPrinter } from "./progress.js";
|
|
14
|
-
import { configHash, requestedSampling } from "./provenance.js";
|
|
15
|
-
import { renderJson, renderText, safeJson } from "./render.js";
|
|
16
|
-
import { renderSarif } from "./sarif.js";
|
|
17
|
-
import { loadSarifLogs } from "./sarif-input.js";
|
|
18
|
-
import { localTarget, mergeRequestTarget, pullRequestTarget } from "./target.js";
|
|
19
|
-
import { forTerminal } from "./terminal.js";
|
|
20
|
-
export const SESSIONS_DIR = ".ocra/sessions";
|
|
21
|
-
export const EXIT = { ok: 0, blocking: 1, error: 2, incomplete: 3, interrupted: 130 };
|
|
22
|
-
export const BUILTIN_PLUGINS = [
|
|
23
|
-
localGitPlugin,
|
|
24
|
-
githubPlugin,
|
|
25
|
-
gitlabPlugin,
|
|
26
|
-
correctnessReviewerPlugin,
|
|
27
|
-
securityReviewerPlugin,
|
|
28
|
-
performanceReviewerPlugin,
|
|
29
|
-
docsReviewerPlugin,
|
|
30
|
-
agentsMdReviewerPlugin,
|
|
31
|
-
sessionJsonlPlugin,
|
|
32
|
-
];
|
|
33
|
-
export async function reviewCommand(args, io, deps) {
|
|
34
|
-
const root = await findRepositoryRoot(deps.cwd);
|
|
35
|
-
const warn = (message) => io.err.write(`[ocra] Warning: ${forTerminal(message)}\n`);
|
|
36
|
-
const ignoreRepoConfig = args.ignoreRepoConfig === true;
|
|
37
|
-
const configFile = args.configFile ? resolve(deps.cwd, args.configFile) : undefined;
|
|
38
|
-
const localArgs = configFile ? { ...args, configFile } : args;
|
|
39
|
-
const target = args.pullRequest
|
|
40
|
-
? await pullRequestTarget(args.pullRequest, deps.cwd, root, deps.env, warn, deps.fetch, ignoreRepoConfig, configFile)
|
|
41
|
-
: args.mergeRequest
|
|
42
|
-
? await mergeRequestTarget(args.mergeRequest, deps.cwd, root, deps.env, warn, deps.fetch, ignoreRepoConfig, configFile)
|
|
43
|
-
: await localTarget(localArgs, deps.cwd, root, deps.env, warn, deps.fetch);
|
|
44
|
-
const { config } = target;
|
|
45
|
-
const session = { dir: join(root, SESSIONS_DIR), id: newRunId() };
|
|
46
|
-
// A plan calls no model, writes no session log and imports no runtime: it
|
|
47
|
-
// works without the optional runtime-opencode.
|
|
48
|
-
const builtins = args.plan
|
|
49
|
-
? deps.builtinPlugins.filter((p) => p.name !== sessionJsonlPlugin.name)
|
|
50
|
-
: [...deps.builtinPlugins, ...(await builtinRuntime(deps.runtimes, config.runtime))];
|
|
51
|
-
const registry = await startPlugins([...builtins, ...target.plugins], {
|
|
52
|
-
settings: args.plan
|
|
53
|
-
? config.pluginSettings
|
|
54
|
-
: { ...config.pluginSettings, [sessionJsonlPlugin.name]: session },
|
|
55
|
-
env: deps.env,
|
|
56
|
-
warn,
|
|
57
|
-
});
|
|
58
|
-
const vcs = target.createVcs(registry);
|
|
59
|
-
const overrides = reviewerOverrides(config, args, registry.reviewers.map((r) => r.id));
|
|
60
|
-
if (args.plan) {
|
|
61
|
-
const preview = await previewReview({
|
|
62
|
-
vcs,
|
|
63
|
-
reviewers: registry.reviewers,
|
|
64
|
-
reviewerOverrides: overrides,
|
|
65
|
-
effort: config.effort,
|
|
66
|
-
roles: config.roles,
|
|
67
|
-
rules: [...registry.rules, ...config.rules],
|
|
68
|
-
selection: { ...defaultSelectionPolicy, include: config.include, exclude: config.exclude },
|
|
69
|
-
...(target.readTrusted ? { readTrusted: target.readTrusted } : {}),
|
|
70
|
-
...(args.ultra ? { ultra: true } : {}),
|
|
71
|
-
...(config.maxTasks !== undefined ? { maxTasks: config.maxTasks } : {}),
|
|
72
|
-
});
|
|
73
|
-
const rendered = args.format === "json" ? `${safeJson(toPlanOutput(preview))}\n` : renderPlan(preview);
|
|
74
|
-
if (args.output === undefined)
|
|
75
|
-
io.out.write(rendered);
|
|
76
|
-
else
|
|
77
|
-
await deps.writeFile(resolve(deps.cwd, args.output), rendered);
|
|
78
|
-
return EXIT.ok;
|
|
79
|
-
}
|
|
80
|
-
const sarif = await loadSarifLogs(args.importSarif ?? [], deps.cwd);
|
|
81
|
-
const sampling = requestedSampling(config, args);
|
|
82
|
-
const cloudDeps = deps.cloud;
|
|
83
|
-
const signedIn = cloudDeps !== undefined && (await cloudEnabled(cloudDeps));
|
|
84
|
-
let models = config.models;
|
|
85
|
-
if (cloudDeps && signedIn && Object.values(models).every((chain) => !chain?.length)) {
|
|
86
|
-
models = await defaultModels(cloudDeps);
|
|
87
|
-
if (Object.keys(models).length > 0) {
|
|
88
|
-
io.err.write("[ocra] No models configured: using your default models from ocra Cloud\n");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const cloud = await withCloudProviders(models, config.providers, deps.env, cloudDeps, warn);
|
|
92
|
-
const runtime = registry.createRuntime(config.runtime, {
|
|
93
|
-
models,
|
|
94
|
-
env: cloud.env,
|
|
95
|
-
providers: cloud.providers,
|
|
96
|
-
...(Object.keys(sampling).length > 0 ? { sampling } : {}),
|
|
97
|
-
});
|
|
98
|
-
const progress = new ProgressPrinter(io.err, { heartbeatMs: deps.heartbeatMs, now: deps.now });
|
|
99
|
-
const interrupt = new AbortController();
|
|
100
|
-
const stopListening = deps.onInterrupt?.(() => {
|
|
101
|
-
io.err.write("[ocra] Interrupted: stopping and writing a partial report (Ctrl-C again quits now)\n");
|
|
102
|
-
interrupt.abort();
|
|
103
|
-
});
|
|
104
|
-
let report;
|
|
105
|
-
const started = deps.now();
|
|
106
|
-
try {
|
|
107
|
-
report = await review({
|
|
108
|
-
runId: session.id,
|
|
109
|
-
signal: interrupt.signal,
|
|
110
|
-
...runOptions(config),
|
|
111
|
-
reviewerOverrides: overrides,
|
|
112
|
-
provenance: { ocraVersion: VERSION, configHash: configHash(config, args), sampling },
|
|
113
|
-
...(args.maxCostUsd !== undefined ? { maxCostUsd: args.maxCostUsd } : {}),
|
|
114
|
-
...(args.ultra ? { ultra: true } : {}),
|
|
115
|
-
...(args.full ? { fullReview: true } : {}),
|
|
116
|
-
...(sarif.length > 0 ? { sarif } : {}),
|
|
117
|
-
...(target.readTrusted ? { readTrusted: target.readTrusted } : {}),
|
|
118
|
-
vcs,
|
|
119
|
-
runtime,
|
|
120
|
-
reviewers: registry.reviewers,
|
|
121
|
-
rules: [...registry.rules, ...config.rules],
|
|
122
|
-
onEvent: (event) => {
|
|
123
|
-
registry.emit(event);
|
|
124
|
-
progress.onEvent(event);
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
stopListening?.();
|
|
130
|
-
progress.stop();
|
|
131
|
-
await runtime.dispose?.();
|
|
132
|
-
}
|
|
133
|
-
const sessionDir = relative(deps.cwd, join(session.dir, session.id)) || ".";
|
|
134
|
-
const rendered = args.format === "json"
|
|
135
|
-
? renderJson(report)
|
|
136
|
-
: args.format === "sarif"
|
|
137
|
-
? renderSarif(report, VERSION)
|
|
138
|
-
: renderText(report, sessionDir);
|
|
139
|
-
if (args.output === undefined) {
|
|
140
|
-
io.out.write(rendered);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
await deps.writeFile(resolve(deps.cwd, args.output), rendered);
|
|
144
|
-
io.err.write(`[ocra] Wrote ${args.output}\n`);
|
|
145
|
-
}
|
|
146
|
-
if (interrupt.signal.aborted)
|
|
147
|
-
return EXIT.interrupted;
|
|
148
|
-
if (target.publish) {
|
|
149
|
-
// Stopping halfway could post the inline comments without the summary
|
|
150
|
-
// that remembers them; a first Ctrl-C only warns.
|
|
151
|
-
const stopHolding = deps.onInterrupt?.(() => {
|
|
152
|
-
io.err.write("[ocra] Publishing; Ctrl-C again quits now and may leave the pull request half updated\n");
|
|
153
|
-
});
|
|
154
|
-
try {
|
|
155
|
-
const published = await vcs.publish(report);
|
|
156
|
-
for (const warning of published.warnings)
|
|
157
|
-
warn(warning);
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
stopHolding?.();
|
|
161
|
-
}
|
|
162
|
-
io.err.write(`[ocra] Published the review to the ${target.publishesTo ?? "change request"}\n`);
|
|
163
|
-
}
|
|
164
|
-
if (signedIn && cloudDeps && !args.noUpload) {
|
|
165
|
-
const source = args.pullRequest ? "github" : args.mergeRequest ? "gitlab" : "local";
|
|
166
|
-
const upload = uploadOf(report, source, await repoHash(root, cloudDeps.credentialsPath), deps.now() - started);
|
|
167
|
-
if (await uploadReview(upload, cloudDeps, warn)) {
|
|
168
|
-
io.err.write("[ocra] Sent this review's counts to ocra Cloud (--no-upload to skip)\n");
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return exitCode(report, io.err);
|
|
172
|
-
}
|
|
173
|
-
// A runtime not built in comes from a plugin of the configuration.
|
|
174
|
-
async function builtinRuntime(runtimes, name) {
|
|
175
|
-
const load = Object.hasOwn(runtimes, name) ? runtimes[name] : undefined;
|
|
176
|
-
return load ? [await load()] : [];
|
|
177
|
-
}
|
|
178
|
-
function runOptions(config) {
|
|
179
|
-
const options = {
|
|
180
|
-
selection: { ...defaultSelectionPolicy, include: config.include, exclude: config.exclude },
|
|
181
|
-
effort: config.effort,
|
|
182
|
-
roles: config.roles,
|
|
183
|
-
};
|
|
184
|
-
if (config.concurrency !== undefined)
|
|
185
|
-
options.concurrency = config.concurrency;
|
|
186
|
-
if (config.taskTimeoutMinutes !== undefined)
|
|
187
|
-
options.taskTimeoutMs = config.taskTimeoutMinutes * 60_000;
|
|
188
|
-
if (config.verify !== undefined)
|
|
189
|
-
options.verify = config.verify;
|
|
190
|
-
if (config.judge !== undefined)
|
|
191
|
-
options.judge = config.judge;
|
|
192
|
-
if (config.maxCostUsd !== undefined)
|
|
193
|
-
options.maxCostUsd = config.maxCostUsd;
|
|
194
|
-
if (config.maxTasks !== undefined)
|
|
195
|
-
options.maxTasks = config.maxTasks;
|
|
196
|
-
if (config.runTimeoutMinutes !== undefined)
|
|
197
|
-
options.runTimeoutMs = config.runTimeoutMinutes * 60_000;
|
|
198
|
-
return options;
|
|
199
|
-
}
|
|
200
|
-
// --reviewers narrows the run to the named reviewers on top of the config.
|
|
201
|
-
function reviewerOverrides(config, args, registered) {
|
|
202
|
-
if (args.reviewers === undefined)
|
|
203
|
-
return config.reviewers;
|
|
204
|
-
const unknown = args.reviewers.filter((id) => !registered.includes(id));
|
|
205
|
-
if (unknown.length > 0) {
|
|
206
|
-
throw new ConfigError(`Unknown reviewer(s): ${unknown.join(", ")} (available: ${registered.join(", ")})`);
|
|
207
|
-
}
|
|
208
|
-
const overrides = { ...config.reviewers };
|
|
209
|
-
for (const id of registered) {
|
|
210
|
-
if (!args.reviewers.includes(id))
|
|
211
|
-
overrides[id] = { ...overrides[id], enabled: false };
|
|
212
|
-
}
|
|
213
|
-
return overrides;
|
|
214
|
-
}
|
|
215
|
-
function exitCode(report, err) {
|
|
216
|
-
if (report.tasks.length > 0 && report.tasks.every((t) => t.status !== "completed")) {
|
|
217
|
-
err.write("[ocra] No review task completed; see the errors above.\n");
|
|
218
|
-
return EXIT.error;
|
|
219
|
-
}
|
|
220
|
-
// Incomplete comes first, even over a blocking verdict: the action lets
|
|
221
|
-
// exit 1 pass unless fail-on-concerns is set, and a review that missed
|
|
222
|
-
// files or could not check a critical finding must never pass.
|
|
223
|
-
const { notReviewed } = coverageGaps(report);
|
|
224
|
-
if (notReviewed > 0) {
|
|
225
|
-
err.write(`[ocra] ${notReviewed} selected file(s) were not reviewed; the review is incomplete.\n`);
|
|
226
|
-
return EXIT.incomplete;
|
|
227
|
-
}
|
|
228
|
-
if (report.unverifiedCriticals > 0) {
|
|
229
|
-
err.write(`[ocra] ${report.unverifiedCriticals} critical finding(s) could not be verified; the review is incomplete.\n`);
|
|
230
|
-
return EXIT.incomplete;
|
|
231
|
-
}
|
|
232
|
-
if (report.verdict !== "significant_concerns")
|
|
233
|
-
return EXIT.ok;
|
|
234
|
-
const override = report.changeRequest.override;
|
|
235
|
-
if (!override)
|
|
236
|
-
return EXIT.blocking;
|
|
237
|
-
err.write(`[ocra] The blocking verdict was overridden by ${forTerminal(override.by)}: ${forTerminal(override.reason)}\n`);
|
|
238
|
-
return EXIT.ok;
|
|
239
|
-
}
|
|
240
|
-
//# sourceMappingURL=command.js.map
|
package/dist/review/config.js
DELETED
|
@@ -1,280 +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
|
-
const configSchema = z
|
|
55
|
-
.object({
|
|
56
|
-
$schema: z.string().optional(),
|
|
57
|
-
models: z
|
|
58
|
-
.object({ top: modelChain, standard: modelChain, light: modelChain })
|
|
59
|
-
.partial()
|
|
60
|
-
.strict()
|
|
61
|
-
.default({}),
|
|
62
|
-
effort: z
|
|
63
|
-
.object({ top: effort, standard: effort, light: effort })
|
|
64
|
-
.partial()
|
|
65
|
-
.strict()
|
|
66
|
-
.default({}),
|
|
67
|
-
concurrency: z.number().int().min(1).max(32).optional(),
|
|
68
|
-
taskTimeoutMinutes: z.number().positive().optional(),
|
|
69
|
-
runTimeoutMinutes: z.number().positive().optional(),
|
|
70
|
-
verify: z.boolean().optional(),
|
|
71
|
-
judge: z.boolean().optional(),
|
|
72
|
-
maxCostUsd: z.number().positive().optional(),
|
|
73
|
-
maxTasks: z.number().int().min(1).max(1_000).optional(),
|
|
74
|
-
// Unset: each provider's default. A seed makes sampling repeatable only
|
|
75
|
-
// where the provider supports it.
|
|
76
|
-
sampling: z
|
|
77
|
-
.object({
|
|
78
|
-
temperature: z.number().min(0).max(2),
|
|
79
|
-
seed: z
|
|
80
|
-
.number()
|
|
81
|
-
.int()
|
|
82
|
-
.min(0)
|
|
83
|
-
.max(2 ** 31 - 1),
|
|
84
|
-
})
|
|
85
|
-
.partial()
|
|
86
|
-
.strict()
|
|
87
|
-
.optional(),
|
|
88
|
-
github: z
|
|
89
|
-
.object({ requestChanges: z.boolean(), botLogin: z.string().min(1) })
|
|
90
|
-
.partial()
|
|
91
|
-
.strict()
|
|
92
|
-
.default({}),
|
|
93
|
-
include: z.array(z.string().min(1)).default([]),
|
|
94
|
-
exclude: z.array(z.string().min(1)).default([]),
|
|
95
|
-
runtime: z.enum(BUILTIN_RUNTIMES).or(z.string().min(1)).default("opencode"),
|
|
96
|
-
plugins: z.array(z.string().min(1)).default([]),
|
|
97
|
-
reviewers: z
|
|
98
|
-
.record(z.string(), z
|
|
99
|
-
.object({
|
|
100
|
-
enabled: z.boolean(),
|
|
101
|
-
minTier: z.enum(RISK_TIERS),
|
|
102
|
-
effort,
|
|
103
|
-
})
|
|
104
|
-
.partial()
|
|
105
|
-
.strict())
|
|
106
|
-
.default({}),
|
|
107
|
-
roles: z
|
|
108
|
-
.partialRecord(z.enum(AGENT_ROLES), z.object({ effort }).partial().strict())
|
|
109
|
-
.default({}),
|
|
110
|
-
pluginSettings: z.record(z.string(), z.unknown()).default({}),
|
|
111
|
-
providers: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,39}$/), providerSchema).default({}),
|
|
112
|
-
extends: z.string().min(1).optional(),
|
|
113
|
-
})
|
|
114
|
-
.strict();
|
|
115
|
-
export const CONFIG_SCHEMA_ID = "https://ocra.majincheng.com/schema/config.v1.json";
|
|
116
|
-
// JSON Schema (draft 2020-12) of .ocra/config.json, for editors: what a
|
|
117
|
-
// file may say, so defaults are optional and model chains take either form.
|
|
118
|
-
export function configJsonSchema() {
|
|
119
|
-
const generated = z.toJSONSchema(configSchema, { target: "draft-2020-12", io: "input" });
|
|
120
|
-
const { $schema, ...rest } = generated;
|
|
121
|
-
return {
|
|
122
|
-
$schema,
|
|
123
|
-
$id: CONFIG_SCHEMA_ID,
|
|
124
|
-
title: "ocra configuration",
|
|
125
|
-
description: "The .ocra/config.json file of a repository, or a file passed with --config. Unknown keys are rejected.",
|
|
126
|
-
...rest,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
const MODEL_ENV = {
|
|
130
|
-
top: "OCRA_MODEL_TOP",
|
|
131
|
-
standard: "OCRA_MODEL_STANDARD",
|
|
132
|
-
light: "OCRA_MODEL_LIGHT",
|
|
133
|
-
};
|
|
134
|
-
const EFFORT_ENV = {
|
|
135
|
-
top: "OCRA_EFFORT_TOP",
|
|
136
|
-
standard: "OCRA_EFFORT_STANDARD",
|
|
137
|
-
light: "OCRA_EFFORT_LIGHT",
|
|
138
|
-
};
|
|
139
|
-
export class ConfigError extends OcraError {
|
|
140
|
-
constructor(message, options) {
|
|
141
|
-
super("CONFIG_INVALID", message, options);
|
|
142
|
-
this.name = "ConfigError";
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// Without the repository's file, only defaults and environment variables
|
|
146
|
-
// apply: that file can name plugins, and plugins run code.
|
|
147
|
-
export async function loadConfig(root, env, options = { repository: true }) {
|
|
148
|
-
const read = options.read ?? ((path) => readConfigFromDisk(root, path));
|
|
149
|
-
const { config: parsed, rules } = options.file
|
|
150
|
-
? await readConfigFile(ownFile(options.file), options, options.file)
|
|
151
|
-
: options.repository
|
|
152
|
-
? await readConfigFile(read, options, CONFIG_PATH)
|
|
153
|
-
: { config: configSchema.parse({}), rules: [] };
|
|
154
|
-
const models = {};
|
|
155
|
-
for (const [tier, chain] of Object.entries(parsed.models)) {
|
|
156
|
-
if (chain)
|
|
157
|
-
models[tier] = chain;
|
|
158
|
-
}
|
|
159
|
-
for (const [tier, name] of Object.entries(MODEL_ENV)) {
|
|
160
|
-
const chain = (env[name] ?? "")
|
|
161
|
-
.split(",")
|
|
162
|
-
.map((m) => m.trim())
|
|
163
|
-
.filter((m) => m !== "");
|
|
164
|
-
if (chain.length > 0)
|
|
165
|
-
models[tier] = chain;
|
|
166
|
-
}
|
|
167
|
-
const efforts = {
|
|
168
|
-
...parsed.effort,
|
|
169
|
-
};
|
|
170
|
-
for (const [tier, name] of Object.entries(EFFORT_ENV)) {
|
|
171
|
-
const value = env[name]?.trim();
|
|
172
|
-
if (!value)
|
|
173
|
-
continue;
|
|
174
|
-
const level = effort.safeParse(value);
|
|
175
|
-
if (!level.success) {
|
|
176
|
-
throw new ConfigError(`${name} must be one of ${EFFORT_LEVELS.join(", ")}`);
|
|
177
|
-
}
|
|
178
|
-
efforts[tier] = level.data;
|
|
179
|
-
}
|
|
180
|
-
for (const model of unpriced(parsed.providers, models)) {
|
|
181
|
-
options.warn?.(`${model} has a price of 0: reported cost and --max-cost-usd do not count its tokens`);
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
...parsed,
|
|
185
|
-
models,
|
|
186
|
-
effort: efforts,
|
|
187
|
-
providers: toProviders(parsed.providers),
|
|
188
|
-
rules,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
function toProviders(declared) {
|
|
192
|
-
return Object.fromEntries(Object.entries(declared).map(([id, p]) => [
|
|
193
|
-
id,
|
|
194
|
-
{
|
|
195
|
-
baseUrl: p.baseUrl,
|
|
196
|
-
...(p.apiKeyEnv ? { apiKeyEnv: p.apiKeyEnv } : {}),
|
|
197
|
-
...(p.effort ? { effort: p.effort } : {}),
|
|
198
|
-
models: Object.fromEntries(Object.entries(p.models).map(([model, price]) => [
|
|
199
|
-
model,
|
|
200
|
-
{
|
|
201
|
-
input: price.input,
|
|
202
|
-
output: price.output,
|
|
203
|
-
...(price.cachedInput === undefined ? {} : { cachedInput: price.cachedInput }),
|
|
204
|
-
},
|
|
205
|
-
])),
|
|
206
|
-
},
|
|
207
|
-
]));
|
|
208
|
-
}
|
|
209
|
-
// Models of declared providers, named in a chain, priced at 0 per token.
|
|
210
|
-
function unpriced(providers, models) {
|
|
211
|
-
const named = new Set(Object.values(models).flat());
|
|
212
|
-
return [...named].filter((model) => {
|
|
213
|
-
const slash = model.indexOf("/");
|
|
214
|
-
const price = providers[model.slice(0, slash)]?.models[model.slice(slash + 1)];
|
|
215
|
-
return price !== undefined && price.input === 0 && price.output === 0;
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
// The user's own checkout, read like any local file (links followed): unlike
|
|
219
|
-
// the review's reads, which never follow links (vcs-local), this is trusted
|
|
220
|
-
// configuration, and --no-repo-config skips it for code that is not.
|
|
221
|
-
function ownFile(file) {
|
|
222
|
-
return async () => {
|
|
223
|
-
try {
|
|
224
|
-
return await readFile(file, "utf8");
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
throw new ConfigError(`cannot read ${file}: ${error.message}`, { cause: error });
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
async function readConfigFromDisk(root, path) {
|
|
232
|
-
try {
|
|
233
|
-
return await readFile(join(root, path), "utf8");
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
if (error.code === "ENOENT")
|
|
237
|
-
return undefined;
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
async function readConfigFile(read, options, label) {
|
|
242
|
-
const text = await read(CONFIG_PATH);
|
|
243
|
-
if (text === undefined)
|
|
244
|
-
return { config: configSchema.parse({}), rules: [] };
|
|
245
|
-
let data;
|
|
246
|
-
try {
|
|
247
|
-
data = JSON.parse(text);
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
throw new ConfigError(`${label} is not valid JSON: ${error.message}`, {
|
|
251
|
-
cause: error,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
const local = configSchema.safeParse(data);
|
|
255
|
-
if (!local.success)
|
|
256
|
-
throw new ConfigError(`${label} is invalid: ${z.prettifyError(local.error)}`);
|
|
257
|
-
if (!local.data.extends)
|
|
258
|
-
return { config: local.data, rules: [] };
|
|
259
|
-
// A shared configuration that cannot be loaded costs its defaults, not the
|
|
260
|
-
// review: the repository's own settings still apply.
|
|
261
|
-
let remote;
|
|
262
|
-
try {
|
|
263
|
-
remote = await fetchRemoteConfig(local.data.extends, options.fetch);
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
// Its limits are lost with it; say so rather than run silently unbounded.
|
|
267
|
-
const limits = local.data.maxCostUsd === undefined
|
|
268
|
-
? "; none of its settings apply, including any spend or task limit"
|
|
269
|
-
: "; none of its settings apply";
|
|
270
|
-
options.warn?.(`could not load extends ${local.data.extends}: ${error.message}${limits}`);
|
|
271
|
-
return { config: local.data, rules: [] };
|
|
272
|
-
}
|
|
273
|
-
const merged = configSchema.safeParse(mergeConfig(remote, data));
|
|
274
|
-
if (!merged.success) {
|
|
275
|
-
options.warn?.(`ignoring extends ${local.data.extends}: its settings are invalid (${z.prettifyError(merged.error)})`);
|
|
276
|
-
return { config: local.data, rules: [] };
|
|
277
|
-
}
|
|
278
|
-
return { config: merged.data, rules: remote.rules ?? [] };
|
|
279
|
-
}
|
|
280
|
-
//# sourceMappingURL=config.js.map
|
package/dist/review/plugins.d.ts
DELETED
package/dist/review/plugins.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { ConfigError } from "./config.js";
|
|
5
|
-
// Plugins listed in the repository config execute code, so they are resolved
|
|
6
|
-
// only from the repository's own dependencies or relative paths inside it.
|
|
7
|
-
export async function loadExternalPlugins(specifiers, root) {
|
|
8
|
-
const plugins = [];
|
|
9
|
-
for (const specifier of specifiers) {
|
|
10
|
-
const module = await import(pathToFileURL(resolvePlugin(specifier, root)).href);
|
|
11
|
-
const plugin = module.default ?? module.plugin;
|
|
12
|
-
if (!isPlugin(plugin)) {
|
|
13
|
-
throw new ConfigError(`Plugin "${specifier}" must export an ocra plugin as default or "plugin"`);
|
|
14
|
-
}
|
|
15
|
-
plugins.push(plugin);
|
|
16
|
-
}
|
|
17
|
-
return plugins;
|
|
18
|
-
}
|
|
19
|
-
function resolvePlugin(specifier, root) {
|
|
20
|
-
if (specifier.startsWith(".") || isAbsolute(specifier))
|
|
21
|
-
return resolve(root, specifier);
|
|
22
|
-
try {
|
|
23
|
-
return createRequire(join(root, "package.json")).resolve(specifier);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
throw new ConfigError(`Cannot find plugin "${specifier}" from ${root}; install it as a dependency`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function isPlugin(value) {
|
|
30
|
-
return (typeof value === "object" && value !== null && typeof value.name === "string");
|
|
31
|
-
}
|
|
32
|
-
//# sourceMappingURL=plugins.js.map
|
package/dist/review/sarif.d.ts
DELETED
package/dist/review/target.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type OcraPlugin, type PluginRegistry, type VcsAdapter } from "@open-cr-agent/core";
|
|
2
|
-
import type { MergeRequestTarget, PullRequestTarget, ReviewArgs } from "./args.js";
|
|
3
|
-
import { type CliConfig } from "./config.js";
|
|
4
|
-
export interface ReviewTarget {
|
|
5
|
-
config: CliConfig;
|
|
6
|
-
plugins: OcraPlugin[];
|
|
7
|
-
createVcs(registry: PluginRegistry): VcsAdapter;
|
|
8
|
-
readTrusted?: (path: string) => Promise<string | undefined>;
|
|
9
|
-
publish: boolean;
|
|
10
|
-
publishesTo?: string;
|
|
11
|
-
}
|
|
12
|
-
type Env = Readonly<Record<string, string | undefined>>;
|
|
13
|
-
export declare function localTarget(args: ReviewArgs, cwd: string, root: string, env: Env, warn: (message: string) => void, fetchImpl?: typeof fetch): Promise<ReviewTarget>;
|
|
14
|
-
export declare function pullRequestTarget(pr: PullRequestTarget, cwd: string, root: string, env: Env, warn: (message: string) => void, fetchImpl?: typeof fetch, ignoreRepoConfig?: boolean, configFile?: string): Promise<ReviewTarget>;
|
|
15
|
-
export declare function mergeRequestTarget(mr: MergeRequestTarget, cwd: string, root: string, env: Env, warn: (message: string) => void, fetchImpl?: typeof fetch, ignoreRepoConfig?: boolean, configFile?: string): Promise<ReviewTarget>;
|
|
16
|
-
export {};
|
|
17
|
-
//# sourceMappingURL=target.d.ts.map
|