@open-cr-agent/cli 0.1.2 → 0.3.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.
@@ -0,0 +1,153 @@
1
+ import { coverageGaps, } from "@open-cr-agent/core";
2
+ import { serializeOutput } from "@open-cr-agent/core/internal";
3
+ // `--format sarif`: the review as a SARIF 2.1.0 log, for code scanning and
4
+ // security dashboards. One rule per reviewer category, one result per
5
+ // finding. Findings an earlier review reported in files this run did not
6
+ // review again stay open, so they are results too, marked unchanged.
7
+ // https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
8
+ const SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
9
+ const LEVEL = {
10
+ critical: "error",
11
+ warning: "warning",
12
+ suggestion: "note",
13
+ };
14
+ export function renderSarif(report, version) {
15
+ const open = [
16
+ ...(report.rereview?.unchanged ?? []),
17
+ ...(report.rereview?.notReproduced ?? []),
18
+ ...(report.rereview?.notRechecked ?? []),
19
+ ];
20
+ const categories = [
21
+ ...new Set([...report.findings.map((f) => f.category), ...(open.length > 0 ? [CARRIED] : [])]),
22
+ ].sort();
23
+ const index = new Map(categories.map((c, i) => [c, i]));
24
+ const { notReviewed } = coverageGaps(report);
25
+ const log = {
26
+ $schema: SCHEMA,
27
+ version: "2.1.0",
28
+ runs: [
29
+ {
30
+ automationDetails: { id: `ocra/${report.runId}` },
31
+ tool: {
32
+ driver: {
33
+ name: "ocra",
34
+ informationUri: "https://ocra.majincheng.com",
35
+ version,
36
+ semanticVersion: version,
37
+ rules: categories.map((id) => ({
38
+ id,
39
+ name: id,
40
+ shortDescription: {
41
+ text: id === CARRIED
42
+ ? CARRIED_DESCRIPTION
43
+ : `Findings of the ${id} reviewer or analyzer`,
44
+ },
45
+ properties: { tags: [id] },
46
+ })),
47
+ },
48
+ },
49
+ invocations: [
50
+ {
51
+ executionSuccessful: notReviewed === 0 && report.unverifiedCriticals === 0,
52
+ toolExecutionNotifications: [
53
+ ...(notReviewed > 0
54
+ ? [
55
+ {
56
+ level: "warning",
57
+ message: { text: `${notReviewed} selected file(s) were not reviewed` },
58
+ },
59
+ ]
60
+ : []),
61
+ ...report.warnings.map((w) => ({
62
+ level: "warning",
63
+ message: { text: plainText(w) },
64
+ })),
65
+ ],
66
+ },
67
+ ],
68
+ results: [
69
+ ...report.findings.map((f) => fromFinding(f, index)),
70
+ ...open.map((f) => fromPrior(f, index)),
71
+ ],
72
+ properties: {
73
+ verdict: report.verdict,
74
+ tier: report.tier,
75
+ baseSha: report.changeRequest.baseSha,
76
+ headSha: report.changeRequest.headSha,
77
+ notReviewed: report.coverage
78
+ .filter((c) => c.status === "failed" || c.status === "unreviewed")
79
+ .map((c) => c.path),
80
+ unverifiedCriticals: report.unverifiedCriticals,
81
+ costUsd: report.usage.costUsd,
82
+ },
83
+ },
84
+ ],
85
+ };
86
+ return `${serializeOutput(log)}\n`;
87
+ }
88
+ // Open findings an earlier review reported, carried over under their own
89
+ // rule: their reviewer category is not recorded in the review state.
90
+ const CARRIED = "ocra-carried-over";
91
+ const CARRIED_DESCRIPTION = "Findings an earlier review reported that are still open in files this run did not review again";
92
+ function fromFinding(f, index) {
93
+ const text = [f.title, f.body, ...(f.suggestion ? [`Suggestion: ${f.suggestion}`] : [])].join("\n\n");
94
+ return {
95
+ ruleId: f.category,
96
+ ruleIndex: index.get(f.category) ?? 0,
97
+ level: LEVEL[f.severity],
98
+ message: { text: plainText(text) },
99
+ locations: [location(f.file, f.lineRange)],
100
+ partialFingerprints: { "ocra/v1": f.fingerprint },
101
+ properties: {
102
+ reviewer: f.reviewer,
103
+ severity: f.severity,
104
+ verification: f.verification ?? "unchecked",
105
+ status: f.status === "unfixed" ? "unfixed" : "new",
106
+ ...(f.lowConfidence ? { lowConfidence: true } : {}),
107
+ task: f.provenance.task,
108
+ ...(f.provenance.model === undefined ? {} : { model: f.provenance.model }),
109
+ },
110
+ };
111
+ }
112
+ function fromPrior(f, index) {
113
+ return {
114
+ ruleId: CARRIED,
115
+ ruleIndex: index.get(CARRIED) ?? 0,
116
+ level: LEVEL[f.severity],
117
+ message: { text: plainText(f.title) },
118
+ // The review state keeps no lines for them, so they apply to the file.
119
+ locations: [location(f.file)],
120
+ partialFingerprints: { "ocra/v1": f.fingerprint },
121
+ baselineState: "unchanged",
122
+ properties: { severity: f.severity, verification: f.verification ?? "unchecked" },
123
+ };
124
+ }
125
+ function location(file, lines) {
126
+ return {
127
+ 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
+ },
134
+ ...(lines ? { region: { startLine: lines.start, endLine: lines.end } } : {}),
135
+ },
136
+ };
137
+ }
138
+ // SARIF viewers turn "[text](target)" in a message into a link (SARIF
139
+ // 3.11.6), and some turn bare web addresses into links too. Text from a
140
+ // reviewed change or a model must do neither: every backslash and bracket is
141
+ // escaped, backslashes first so a planted "\[" cannot undo the escape, and
142
+ // web addresses get a zero-width space in their scheme or after "www", as in
143
+ // pull request comments (vcs-platform's safeMarkdown, which also handles
144
+ // markup SARIF text does not have). Braces are doubled, since "{0}" is a
145
+ // placeholder (SARIF 3.11.5); GitHub shows "{{" as "{".
146
+ export function plainText(text) {
147
+ return text
148
+ .replace(/[\\[\]]/g, (c) => `\\${c}`)
149
+ .replace(/[{}]/g, (c) => c + c)
150
+ .replace(/\b(https?|ftp)(?=:\/\/)/gi, "$1​")
151
+ .replace(/\bwww(?=\.)/gi, "www​");
152
+ }
153
+ //# sourceMappingURL=sarif.js.map
@@ -1,5 +1,5 @@
1
- import type { OcraPlugin, PluginRegistry, VcsAdapter } from "@open-cr-agent/core";
2
- import type { PullRequestTarget, ReviewArgs } from "./args.js";
1
+ import { type OcraPlugin, type PluginRegistry, type VcsAdapter } from "@open-cr-agent/core";
2
+ import type { MergeRequestTarget, PullRequestTarget, ReviewArgs } from "./args.js";
3
3
  import { type CliConfig } from "./config.js";
4
4
  export interface ReviewTarget {
5
5
  config: CliConfig;
@@ -7,9 +7,11 @@ export interface ReviewTarget {
7
7
  createVcs(registry: PluginRegistry): VcsAdapter;
8
8
  readTrusted?: (path: string) => Promise<string | undefined>;
9
9
  publish: boolean;
10
+ publishesTo?: string;
10
11
  }
11
12
  type Env = Readonly<Record<string, string | undefined>>;
12
13
  export declare function localTarget(args: ReviewArgs, cwd: string, root: string, env: Env, warn: (message: string) => void, fetchImpl?: typeof fetch): Promise<ReviewTarget>;
13
- export declare function pullRequestTarget(pr: PullRequestTarget, cwd: string, root: string, env: Env, warn: (message: string) => void, fetchImpl?: typeof fetch, ignoreRepoConfig?: boolean): 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>;
14
16
  export {};
15
17
  //# sourceMappingURL=target.d.ts.map
@@ -1,16 +1,21 @@
1
- import { DEFAULT_BOT_LOGIN, GitHubApi } from "@open-cr-agent/vcs-github";
2
- import { ensureCommits, filesChangedSince, LocalGitAdapter } from "@open-cr-agent/vcs-local";
1
+ import { dirname } from "node:path";
2
+ import { OcraError, } from "@open-cr-agent/core";
3
+ import { DEFAULT_BOT_LOGIN, GitHubApi } from "@open-cr-agent/vcs-github/internal";
4
+ import { GitLabApi } from "@open-cr-agent/vcs-gitlab/internal";
5
+ import { ensureCommits, filesChangedSince, LocalGitAdapter, } from "@open-cr-agent/vcs-local/internal";
3
6
  import { ConfigError, loadConfig } from "./config.js";
4
7
  import { loadExternalPlugins } from "./plugins.js";
5
8
  export async function localTarget(args, cwd, root, env, warn, fetchImpl) {
6
9
  const config = await loadConfig(root, env, {
7
10
  repository: !args.ignoreRepoConfig,
11
+ ...(args.configFile ? { file: args.configFile } : {}),
8
12
  warn,
9
13
  ...(fetchImpl ? { fetch: fetchImpl } : {}),
10
14
  });
11
15
  return {
12
16
  config,
13
- plugins: await loadExternalPlugins(config.plugins, root),
17
+ // Plugins named by the user's own file are resolved from where it is.
18
+ plugins: await loadExternalPlugins(config.plugins, args.configFile ? dirname(args.configFile) : root),
14
19
  createVcs: (registry) => registry.createVcs("local", { cwd, target: args.target }),
15
20
  ...(args.ignoreRepoConfig ? { readTrusted: untrustedTreeReader(args, cwd) } : {}),
16
21
  publish: false,
@@ -28,10 +33,11 @@ function untrustedTreeReader(args, cwd) {
28
33
  }
29
34
  // The pull request's own files are untrusted: configuration, guidelines and
30
35
  // rules come from its base commit, and repository plugins never load.
31
- export async function pullRequestTarget(pr, cwd, root, env, warn, fetchImpl, ignoreRepoConfig = false) {
36
+ export async function pullRequestTarget(pr, cwd, root, env, warn, fetchImpl, ignoreRepoConfig = false, configFile) {
32
37
  const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
33
- if (!token)
34
- throw new ConfigError("--pr needs a GitHub token in GITHUB_TOKEN or GH_TOKEN");
38
+ if (!token) {
39
+ throw new OcraError("CONFIG_CREDENTIALS_MISSING", "--pr needs a GitHub token in GITHUB_TOKEN or GH_TOKEN");
40
+ }
35
41
  const [owner = "", repo = ""] = (pr.repo ?? (await repositoryName(root, env))).split("/");
36
42
  const apiOptions = { token, ...(env.GITHUB_API_URL ? { baseUrl: env.GITHUB_API_URL } : {}) };
37
43
  const api = new GitHubApi({ owner, repo }, fetchImpl ? { ...apiOptions, fetch: fetchImpl } : apiOptions);
@@ -42,11 +48,12 @@ export async function pullRequestTarget(pr, cwd, root, env, warn, fetchImpl, ign
42
48
  const config = await loadConfig(root, env, {
43
49
  repository: !ignoreRepoConfig,
44
50
  read: readTrusted,
51
+ ...(configFile ? { file: configFile } : {}),
45
52
  warn,
46
53
  ...(fetchImpl ? { fetch: fetchImpl } : {}),
47
54
  });
48
55
  if (config.plugins.length > 0) {
49
- warn("plugins in .ocra/config.json are not loaded for pull requests");
56
+ warn("plugins in the configuration are not loaded for pull requests");
50
57
  }
51
58
  const code = new LocalGitAdapter({
52
59
  cwd,
@@ -72,8 +79,119 @@ export async function pullRequestTarget(pr, cwd, root, env, warn, fetchImpl, ign
72
79
  }),
73
80
  readTrusted,
74
81
  publish: pr.publish,
82
+ publishesTo: "pull request",
75
83
  };
76
84
  }
85
+ const GITLAB_API = "https://gitlab.com/api/v4";
86
+ // Like a pull request: configuration, guidelines and rules come from the
87
+ // target branch the merge request was based on, and repository plugins never
88
+ // load. GitLab CI sets CI_API_V4_URL and CI_PROJECT_ID; GITLAB_TOKEN must be
89
+ // a token that may write notes, which CI_JOB_TOKEN may not.
90
+ export async function mergeRequestTarget(mr, cwd, root, env, warn, fetchImpl, ignoreRepoConfig = false, configFile) {
91
+ const token = env.GITLAB_TOKEN;
92
+ if (!token) {
93
+ throw new OcraError("CONFIG_CREDENTIALS_MISSING", "--mr needs a GitLab token in GITLAB_TOKEN: a project access token with the api scope and the Developer role (CI_JOB_TOKEN cannot post comments)");
94
+ }
95
+ const origin = await originUrl(root);
96
+ const project = mr.project ?? env.CI_PROJECT_ID ?? gitlabProject(origin);
97
+ const apiUrl = gitlabApi(env.CI_API_V4_URL, remoteHost(origin), warn);
98
+ const api = new GitLabApi(project, {
99
+ token,
100
+ baseUrl: apiUrl,
101
+ ...(fetchImpl ? { fetch: fetchImpl } : {}),
102
+ });
103
+ const merge = await api.getMergeRequest(mr.iid);
104
+ const refs = merge.diff_refs;
105
+ if (!refs) {
106
+ throw new OcraError("VCS_NOT_READY", `merge request !${mr.iid} has no diff yet; try again shortly`);
107
+ }
108
+ await ensureCommits(root, [refs.start_sha, refs.head_sha], [`refs/merge-requests/${mr.iid}/head`]);
109
+ const base = new LocalGitAdapter({ cwd, target: { mode: "commit", commit: refs.start_sha } });
110
+ const readTrusted = (path) => base.readFile(path);
111
+ const config = await loadConfig(root, env, {
112
+ repository: !ignoreRepoConfig,
113
+ read: readTrusted,
114
+ ...(configFile ? { file: configFile } : {}),
115
+ warn,
116
+ ...(fetchImpl ? { fetch: fetchImpl } : {}),
117
+ });
118
+ if (config.plugins.length > 0) {
119
+ warn("plugins in the configuration are not loaded for merge requests");
120
+ }
121
+ const code = new LocalGitAdapter({
122
+ cwd,
123
+ target: { mode: "range", from: refs.start_sha, to: refs.head_sha },
124
+ });
125
+ return {
126
+ config,
127
+ plugins: [],
128
+ createVcs: (registry) => registry.createVcs("gitlab", {
129
+ project: /^\d+$/.test(project) ? Number(project) : project,
130
+ iid: mr.iid,
131
+ token,
132
+ apiUrl,
133
+ code,
134
+ snapshot: merge,
135
+ history: {
136
+ filesChangedSince: (from, to) => filesChangedSince(root, from, to),
137
+ },
138
+ ...(fetchImpl ? { fetch: fetchImpl } : {}),
139
+ }),
140
+ readTrusted,
141
+ publish: mr.publish,
142
+ publishesTo: "merge request",
143
+ };
144
+ }
145
+ function originUrl(root) {
146
+ return new LocalGitAdapter({ cwd: root, target: { mode: "workspace" } })
147
+ .remoteUrl("origin")
148
+ .catch(() => undefined);
149
+ }
150
+ // Where GITLAB_TOKEN goes: to CI_API_V4_URL, which GitLab CI sets, or else
151
+ // to gitlab.com, but only when origin is there too or is no remote host. A
152
+ // token for a self-managed instance must not travel to gitlab.com because
153
+ // the address was left out.
154
+ function gitlabApi(configured, origin, warn) {
155
+ if (configured) {
156
+ const api = remoteHost(configured);
157
+ if (api && origin && site(api) !== site(origin)) {
158
+ warn(`CI_API_V4_URL is on ${api} but origin is on ${origin}; GITLAB_TOKEN goes to ${api}`);
159
+ }
160
+ return configured;
161
+ }
162
+ if (origin && site(origin) !== "gitlab.com") {
163
+ throw new ConfigError(`origin is on ${origin}, not gitlab.com: set CI_API_V4_URL to its API, such as https://${origin}/api/v4, so that GITLAB_TOKEN goes only there`);
164
+ }
165
+ return GITLAB_API;
166
+ }
167
+ // GitLab.com answers on subdomains too, such as altssh.gitlab.com for SSH.
168
+ function site(host) {
169
+ return host === "gitlab.com" || host.endsWith(".gitlab.com") ? "gitlab.com" : host;
170
+ }
171
+ // The host of https://host/…, ssh://user@host:port/… or user@host:path; none
172
+ // for a local path, including C:\ on Windows.
173
+ function remoteHost(url) {
174
+ if (!url)
175
+ return undefined;
176
+ if (/^[\w+.-]+:\/\//.test(url)) {
177
+ try {
178
+ const parsed = new URL(url);
179
+ return parsed.protocol === "file:" ? undefined : parsed.hostname.toLowerCase() || undefined;
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ }
185
+ return /^(?:[^@\s/]+@)?([^:/\s\\]+):(?![\\/])/.exec(url)?.[1]?.toLowerCase();
186
+ }
187
+ // The project behind the origin remote: any host, since GitLab runs on many.
188
+ function gitlabProject(url) {
189
+ const match = url && /^(?:[\w+.-]+:\/\/[^/]+\/|[^@\s]+@[^:]+:)(.+?)(?:\.git)?\/?$/.exec(url);
190
+ if (!match?.[1]) {
191
+ throw new ConfigError("Cannot tell which GitLab project this is; pass --project <id|path>");
192
+ }
193
+ return match[1];
194
+ }
77
195
  async function repositoryName(root, env) {
78
196
  if (env.GITHUB_REPOSITORY)
79
197
  return env.GITHUB_REPOSITORY;
@@ -1,4 +1,4 @@
1
- import { isUnsafeCodePoint } from "@open-cr-agent/core";
1
+ import { isUnsafeCodePoint } from "@open-cr-agent/core/internal";
2
2
  // Findings, commit messages and errors can carry text a diff planted; none of
3
3
  // it may drive the user's terminal. C0 and C1 controls (ANSI and OSC escapes
4
4
  // start with ESC or a C1 code) except tab and newline, carriage returns, and
package/dist/run.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { type ReviewDeps } from "./review/command.js";
2
2
  import type { Output } from "./review/progress.js";
3
- export declare const VERSION: string;
4
3
  export declare function defaultDeps(): ReviewDeps;
5
4
  export declare function run(argv: string[], out: Output, err: Output, deps?: ReviewDeps): Promise<number>;
6
5
  //# sourceMappingURL=run.d.ts.map
package/dist/run.js CHANGED
@@ -1,18 +1,20 @@
1
1
  import { writeFile } from "node:fs/promises";
2
- import { createRequire } from "node:module";
3
2
  import { parseArgs } from "node:util";
4
- import { errorMessage } from "@open-cr-agent/core";
3
+ import { isOcraError } from "@open-cr-agent/core";
4
+ import { errorMessage } from "@open-cr-agent/core/internal";
5
5
  import { memoryCommand } from "./memory.js";
6
+ import { metricsCommand } from "./metrics.js";
6
7
  import { parseReviewArgs, REVIEW_USAGE, UsageError } from "./review/args.js";
7
8
  import { BUILTIN_PLUGINS, EXIT, reviewCommand } from "./review/command.js";
9
+ import { BUILTIN_RUNTIMES } from "./review/runtimes.js";
8
10
  import { forTerminal } from "./review/terminal.js";
9
- // src/ and dist/ both sit one level below the package root.
10
- export const VERSION = createRequire(import.meta.url)("../package.json").version;
11
+ import { VERSION } from "./version.js";
11
12
  const USAGE = `Usage: ocra <command> [options]
12
13
 
13
14
  Commands:
14
15
  review Review code changes (run "ocra review --help" for options)
15
16
  memory Remember findings the team accepts (run "ocra memory --help")
17
+ metrics Counts over past reviews: runs, cost, findings, per reviewer (run "ocra metrics --help")
16
18
 
17
19
  Options:
18
20
  -h, --help Show help
@@ -23,6 +25,7 @@ export function defaultDeps() {
23
25
  cwd: process.cwd(),
24
26
  env: process.env,
25
27
  builtinPlugins: BUILTIN_PLUGINS,
28
+ runtimes: BUILTIN_RUNTIMES,
26
29
  writeFile: (path, content) => writeFile(path, content, "utf8"),
27
30
  now: Date.now,
28
31
  heartbeatMs: 30_000,
@@ -47,12 +50,14 @@ export async function run(argv, out, err, deps = defaultDeps()) {
47
50
  const [command, ...rest] = argv;
48
51
  if (command === "review")
49
52
  return review(rest, out, err, deps);
50
- if (command === "memory") {
53
+ if (command === "memory" || command === "metrics") {
51
54
  try {
52
- return await memoryCommand(rest, out, deps.cwd);
55
+ return command === "memory"
56
+ ? await memoryCommand(rest, out, deps.cwd)
57
+ : await metricsCommand(rest, out, deps.cwd);
53
58
  }
54
59
  catch (error) {
55
- err.write(`ocra: ${forTerminal(errorMessage(error))}\n`);
60
+ err.write(failure(error));
56
61
  return EXIT.error;
57
62
  }
58
63
  }
@@ -93,9 +98,14 @@ async function review(argv, out, err, deps) {
93
98
  err.write(`${error.message}\n\n${REVIEW_USAGE}`);
94
99
  }
95
100
  else {
96
- err.write(`ocra: ${forTerminal(errorMessage(error))}\n`);
101
+ err.write(failure(error));
97
102
  }
98
103
  return EXIT.error;
99
104
  }
100
105
  }
106
+ // The code is what a script may match on; the message is for people.
107
+ function failure(error) {
108
+ const code = isOcraError(error) ? ` [${error.code}]` : "";
109
+ return `ocra${code}: ${forTerminal(errorMessage(error))}\n`;
110
+ }
101
111
  //# sourceMappingURL=run.js.map
@@ -0,0 +1,2 @@
1
+ export declare const VERSION: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+ // src/ and dist/ both sit one level below the package root.
3
+ export const VERSION = createRequire(import.meta.url)("../package.json").version;
4
+ //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-cr-agent/cli",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "ocra: open-source multi-agent code review for local changes and GitHub pull requests",
5
5
  "keywords": [
6
6
  "code-review",
@@ -23,9 +23,11 @@
23
23
  "type": "module",
24
24
  "exports": {
25
25
  ".": {
26
+ "@open-cr-agent/source": "./src/index.ts",
26
27
  "types": "./dist/index.d.ts",
27
28
  "default": "./dist/index.js"
28
- }
29
+ },
30
+ "./package.json": "./package.json"
29
31
  },
30
32
  "bin": {
31
33
  "ocra": "./dist/main.js"
@@ -35,12 +37,16 @@
35
37
  "!dist/**/*.map"
36
38
  ],
37
39
  "dependencies": {
38
- "@open-cr-agent/core": "0.1.2",
39
- "@open-cr-agent/runtime-opencode": "0.1.2",
40
- "@open-cr-agent/vcs-github": "0.1.2",
41
- "@open-cr-agent/vcs-local": "0.1.2",
40
+ "@open-cr-agent/core": "0.3.0",
41
+ "@open-cr-agent/runtime-direct": "0.3.0",
42
+ "@open-cr-agent/vcs-github": "0.3.0",
43
+ "@open-cr-agent/vcs-gitlab": "0.3.0",
44
+ "@open-cr-agent/vcs-local": "0.3.0",
42
45
  "zod": "^4.6.5"
43
46
  },
47
+ "optionalDependencies": {
48
+ "@open-cr-agent/runtime-opencode": "0.3.0"
49
+ },
44
50
  "publishConfig": {
45
51
  "access": "public"
46
52
  }