@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.
Files changed (132) hide show
  1. package/dist/cloud/account-salt.d.ts +6 -0
  2. package/dist/cloud/account-salt.js +33 -0
  3. package/dist/cloud/account-settings.d.ts +100 -0
  4. package/dist/cloud/account-settings.js +107 -0
  5. package/dist/cloud/browser.d.ts +17 -0
  6. package/dist/cloud/browser.js +37 -0
  7. package/dist/cloud/client.d.ts +93 -0
  8. package/dist/cloud/client.js +221 -0
  9. package/dist/cloud/credentials.d.ts +18 -0
  10. package/dist/cloud/credentials.js +52 -0
  11. package/dist/cloud/deps.d.ts +14 -0
  12. package/dist/cloud/deps.js +34 -0
  13. package/dist/cloud/file-lock.d.ts +14 -0
  14. package/dist/cloud/file-lock.js +77 -0
  15. package/dist/cloud/findings.d.ts +8 -0
  16. package/dist/cloud/findings.js +58 -0
  17. package/dist/cloud/gateway-token.d.ts +18 -0
  18. package/dist/cloud/gateway-token.js +63 -0
  19. package/dist/cloud/memory.d.ts +8 -0
  20. package/dist/cloud/memory.js +68 -0
  21. package/dist/cloud/private-file.d.ts +8 -0
  22. package/dist/cloud/private-file.js +46 -0
  23. package/dist/cloud/providers.d.ts +18 -0
  24. package/dist/cloud/providers.js +132 -0
  25. package/dist/cloud/review.d.ts +28 -0
  26. package/dist/cloud/review.js +76 -0
  27. package/dist/cloud/settings.d.ts +22 -0
  28. package/dist/cloud/settings.js +64 -0
  29. package/dist/cloud/upload.d.ts +20 -0
  30. package/dist/cloud/upload.js +183 -0
  31. package/dist/commands/login.d.ts +5 -0
  32. package/dist/commands/login.js +164 -0
  33. package/dist/commands/memory.d.ts +3 -0
  34. package/dist/{memory.js → commands/memory.js} +21 -20
  35. package/dist/commands/metrics.d.ts +45 -0
  36. package/dist/{metrics.js → commands/metrics.js} +32 -45
  37. package/dist/commands/plugins.d.ts +9 -0
  38. package/dist/commands/plugins.js +159 -0
  39. package/dist/{review → commands/review}/args.d.ts +7 -10
  40. package/dist/{review → commands/review}/args.js +11 -16
  41. package/dist/commands/review/deliver.d.ts +7 -0
  42. package/dist/commands/review/deliver.js +56 -0
  43. package/dist/commands/review/deps.d.ts +18 -0
  44. package/dist/commands/review/deps.js +2 -0
  45. package/dist/commands/review/execute-run.d.ts +13 -0
  46. package/dist/commands/review/execute-run.js +97 -0
  47. package/dist/commands/review/exit-code.d.ts +4 -0
  48. package/dist/commands/review/exit-code.js +29 -0
  49. package/dist/commands/review/plan-prices.d.ts +3 -0
  50. package/dist/commands/review/plan-prices.js +18 -0
  51. package/dist/{review → commands/review}/plan-render.js +22 -2
  52. package/dist/commands/review/plan-run.d.ts +5 -0
  53. package/dist/commands/review/plan-run.js +38 -0
  54. package/dist/commands/review/plan-schema.d.ts +97 -0
  55. package/dist/commands/review/plan-schema.js +34 -0
  56. package/dist/{review → commands/review}/progress.d.ts +3 -5
  57. package/dist/{review → commands/review}/progress.js +3 -3
  58. package/dist/{review → commands/review}/provenance.d.ts +4 -2
  59. package/dist/{review → commands/review}/provenance.js +11 -2
  60. package/dist/{review → commands/review}/render.js +8 -3
  61. package/dist/commands/review/resolve-run.d.ts +29 -0
  62. package/dist/commands/review/resolve-run.js +125 -0
  63. package/dist/commands/review/review-options.d.ts +6 -0
  64. package/dist/commands/review/review-options.js +35 -0
  65. package/dist/{review → commands/review}/runtimes.js +1 -1
  66. package/dist/{review → commands/review}/sarif-input.d.ts +0 -1
  67. package/dist/{review → commands/review}/sarif-input.js +5 -5
  68. package/dist/commands/review/sarif.d.ts +96 -0
  69. package/dist/{review → commands/review}/sarif.js +29 -7
  70. package/dist/commands/review/settings-sources.d.ts +17 -0
  71. package/dist/commands/review/settings-sources.js +30 -0
  72. package/dist/commands/review/target.d.ts +38 -0
  73. package/dist/commands/review/target.js +126 -0
  74. package/dist/commands/review.d.ts +7 -0
  75. package/dist/commands/review.js +34 -0
  76. package/dist/config/cli-config.d.ts +41 -0
  77. package/dist/config/cli-config.js +199 -0
  78. package/dist/{review/remote-config.d.ts → config/remote.d.ts} +3 -3
  79. package/dist/{review/remote-config.js → config/remote.js} +5 -23
  80. package/dist/{review/config.d.ts → config/schema.d.ts} +13 -25
  81. package/dist/config/schema.js +138 -0
  82. package/dist/config/settings.d.ts +37 -0
  83. package/dist/config/settings.js +133 -0
  84. package/dist/config/user-dir.d.ts +3 -0
  85. package/dist/config/user-dir.js +10 -0
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +10 -1
  88. package/dist/internal.d.ts +6 -0
  89. package/dist/internal.js +7 -0
  90. package/dist/io/exit.d.ts +9 -0
  91. package/dist/io/exit.js +12 -0
  92. package/dist/io/output.d.ts +4 -0
  93. package/dist/io/output.js +2 -0
  94. package/dist/io/usage-error.d.ts +5 -0
  95. package/dist/io/usage-error.js +8 -0
  96. package/dist/main.js +2 -2
  97. package/dist/plugins/account.d.ts +23 -0
  98. package/dist/plugins/account.js +54 -0
  99. package/dist/plugins/load.d.ts +13 -0
  100. package/dist/plugins/load.js +78 -0
  101. package/dist/plugins/npm.d.ts +8 -0
  102. package/dist/plugins/npm.js +24 -0
  103. package/dist/plugins/store.d.ts +20 -0
  104. package/dist/plugins/store.js +85 -0
  105. package/dist/repository-id.d.ts +12 -0
  106. package/dist/repository-id.js +72 -0
  107. package/dist/run.d.ts +2 -2
  108. package/dist/run.js +32 -10
  109. package/dist/session/store.d.ts +9 -0
  110. package/dist/session/store.js +31 -0
  111. package/package.json +17 -7
  112. package/dist/cloud.d.ts +0 -32
  113. package/dist/cloud.js +0 -262
  114. package/dist/memory.d.ts +0 -4
  115. package/dist/metrics.d.ts +0 -47
  116. package/dist/review/cloud-providers.d.ts +0 -12
  117. package/dist/review/cloud-providers.js +0 -59
  118. package/dist/review/cloud-upload.d.ts +0 -40
  119. package/dist/review/cloud-upload.js +0 -135
  120. package/dist/review/command.d.ts +0 -31
  121. package/dist/review/command.js +0 -240
  122. package/dist/review/config.js +0 -280
  123. package/dist/review/plugins.d.ts +0 -3
  124. package/dist/review/plugins.js +0 -32
  125. package/dist/review/sarif.d.ts +0 -4
  126. package/dist/review/target.d.ts +0 -17
  127. package/dist/review/target.js +0 -207
  128. /package/dist/{review → commands/review}/plan-render.d.ts +0 -0
  129. /package/dist/{review → commands/review}/render.d.ts +0 -0
  130. /package/dist/{review → commands/review}/runtimes.d.ts +0 -0
  131. /package/dist/{review → io}/terminal.d.ts +0 -0
  132. /package/dist/{review → io}/terminal.js +0 -0
package/dist/cloud.d.ts DELETED
@@ -1,32 +0,0 @@
1
- import type { Output } from "./review/progress.js";
2
- /** ocra Cloud's address; OCRA_CLOUD_URL names another server. */
3
- export declare const DEFAULT_CLOUD_URL = "https://app.ocracloud.com";
4
- export declare const LOGIN_USAGE: string;
5
- export type Credentials = {
6
- server: string;
7
- login: string;
8
- access_token: string;
9
- refresh_token: string;
10
- /** Epoch milliseconds when the access token expires. */
11
- expires_at: number;
12
- };
13
- export type CloudDeps = {
14
- env: Readonly<Record<string, string | undefined>>;
15
- fetch: typeof fetch;
16
- now: () => number;
17
- sleep: (ms: number) => Promise<void>;
18
- openBrowser: (url: string) => void;
19
- credentialsPath: string;
20
- clientName: string;
21
- };
22
- export declare function defaultCloudDeps(env?: CloudDeps["env"]): CloudDeps;
23
- export declare function credentialsPath(env: CloudDeps["env"]): string;
24
- export declare function readCredentials(path: string): Promise<Credentials | undefined>;
25
- /**
26
- * The saved session with a live access token, refreshed (and saved) when
27
- * it has expired or is about to; undefined when not signed in or the
28
- * session was revoked.
29
- */
30
- export declare function cloudSession(deps: CloudDeps, minValidityMs?: number): Promise<Credentials | undefined>;
31
- export declare function cloudCommand(command: "login" | "logout" | "whoami", argv: string[], out: Output, err: Output, deps: CloudDeps): Promise<number>;
32
- //# sourceMappingURL=cloud.d.ts.map
package/dist/cloud.js DELETED
@@ -1,262 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { homedir, hostname } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { parseArgs } from "node:util";
6
- import { errorMessage } from "@open-cr-agent/core/internal";
7
- import { UsageError } from "./review/args.js";
8
- import { forTerminal } from "./review/terminal.js";
9
- import { VERSION } from "./version.js";
10
- // ocra Cloud sign-in (ADR-0024): `ocra login` runs the OAuth device flow
11
- // (RFC 8628) against ocra Cloud, `ocra whoami` shows the account, and
12
- // `ocra logout` ends the session. Nothing here runs unless one of these
13
- // commands is used: a self-hosted user never sees a login prompt.
14
- /** ocra Cloud's address; OCRA_CLOUD_URL names another server. */
15
- export const DEFAULT_CLOUD_URL = "https://app.ocracloud.com";
16
- export const LOGIN_USAGE = `Usage: ocra login [--no-browser]
17
- ocra logout
18
- ocra whoami
19
-
20
- Sign in to ocra Cloud with GitHub, so reviews can use the model keys stored
21
- there. ocra login shows a code, opens the browser to confirm it, and saves the
22
- session to ${credentialsHint()} (readable only by you).
23
-
24
- Environment:
25
- OCRA_CLOUD=off Disable ocra Cloud: these commands refuse to run
26
- OCRA_CLOUD_URL Another ocra Cloud server (default ${DEFAULT_CLOUD_URL})
27
- `;
28
- export function defaultCloudDeps(env = process.env) {
29
- return {
30
- env,
31
- fetch: globalThis.fetch,
32
- now: Date.now,
33
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
34
- openBrowser,
35
- credentialsPath: credentialsPath(env),
36
- clientName: `${hostname()} (ocra ${VERSION})`,
37
- };
38
- }
39
- function credentialsHint() {
40
- return process.platform === "win32"
41
- ? "%APPDATA%\\ocra\\credentials.json"
42
- : "~/.config/ocra/credentials.json";
43
- }
44
- export function credentialsPath(env) {
45
- const base = process.platform === "win32"
46
- ? (env.APPDATA ?? join(homedir(), "AppData", "Roaming"))
47
- : (env.XDG_CONFIG_HOME ?? join(homedir(), ".config"));
48
- return join(base, "ocra", "credentials.json");
49
- }
50
- function cloudUrl(env) {
51
- const named = env.OCRA_CLOUD_URL || DEFAULT_CLOUD_URL;
52
- let url;
53
- try {
54
- url = new URL(named);
55
- }
56
- catch {
57
- throw new UsageError(`OCRA_CLOUD_URL is not a URL: ${named}`);
58
- }
59
- if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
60
- throw new UsageError(`OCRA_CLOUD_URL must use https: ${url}`);
61
- }
62
- return url.origin;
63
- }
64
- function openBrowser(url) {
65
- const [cmd, args] = process.platform === "darwin"
66
- ? ["open", [url]]
67
- : process.platform === "win32"
68
- ? ["cmd", ["/c", "start", "", url]]
69
- : ["xdg-open", [url]];
70
- try {
71
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
72
- child.on("error", () => { });
73
- child.unref();
74
- }
75
- catch {
76
- // The URL is printed as well; a missing opener is not an error.
77
- }
78
- }
79
- export async function readCredentials(path) {
80
- let text;
81
- try {
82
- text = await readFile(path, "utf8");
83
- }
84
- catch {
85
- return undefined;
86
- }
87
- const c = JSON.parse(text);
88
- if (!c.server || !c.access_token || !c.refresh_token || typeof c.expires_at !== "number") {
89
- return undefined;
90
- }
91
- return c;
92
- }
93
- async function writeCredentials(path, c) {
94
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
95
- await writeFile(path, `${JSON.stringify(c, null, 2)}\n`, { mode: 0o600 });
96
- // writeFile keeps the mode of a file that already exists.
97
- await chmod(path, 0o600);
98
- }
99
- async function postJson(deps, url, body, token) {
100
- const res = await deps.fetch(url, {
101
- method: "POST",
102
- headers: {
103
- "content-type": "application/json",
104
- "user-agent": `ocra/${VERSION}`,
105
- ...(token ? { authorization: `Bearer ${token}` } : {}),
106
- },
107
- body: JSON.stringify(body),
108
- signal: AbortSignal.timeout(30_000),
109
- });
110
- return {
111
- status: res.status,
112
- body: (await res.json().catch(() => ({}))),
113
- };
114
- }
115
- async function fetchMe(deps, server, token) {
116
- const res = await deps.fetch(`${server}/api/me`, {
117
- headers: { authorization: `Bearer ${token}`, "user-agent": `ocra/${VERSION}` },
118
- signal: AbortSignal.timeout(30_000),
119
- });
120
- if (res.status === 401)
121
- return undefined;
122
- if (!res.ok)
123
- throw new Error(`ocra Cloud answered ${res.status}`);
124
- return (await res.json());
125
- }
126
- /**
127
- * The saved session with a live access token, refreshed (and saved) when
128
- * it has expired or is about to; undefined when not signed in or the
129
- * session was revoked.
130
- */
131
- export async function cloudSession(deps, minValidityMs = 60_000) {
132
- const saved = await readCredentials(deps.credentialsPath);
133
- if (!saved)
134
- return undefined;
135
- if (saved.expires_at - deps.now() > minValidityMs)
136
- return saved;
137
- const { status, body } = await postJson(deps, `${saved.server}/api/device/refresh`, {
138
- refresh_token: saved.refresh_token,
139
- });
140
- const t = body;
141
- if (status !== 200 || !t.access_token || !t.refresh_token || !t.expires_in)
142
- return undefined;
143
- const next = {
144
- ...saved,
145
- access_token: t.access_token,
146
- refresh_token: t.refresh_token,
147
- expires_at: deps.now() + t.expires_in * 1000,
148
- };
149
- await writeCredentials(deps.credentialsPath, next);
150
- return next;
151
- }
152
- export async function cloudCommand(command, argv, out, err, deps) {
153
- const { values } = parseArgs({
154
- args: argv,
155
- strict: true,
156
- options: { "no-browser": { type: "boolean" }, help: { type: "boolean", short: "h" } },
157
- });
158
- if (values.help) {
159
- out.write(LOGIN_USAGE);
160
- return 0;
161
- }
162
- if (deps.env.OCRA_CLOUD === "off") {
163
- err.write("ocra Cloud is off (OCRA_CLOUD=off).\n");
164
- return 2;
165
- }
166
- if (command === "whoami")
167
- return whoami(out, err, deps);
168
- if (command === "logout")
169
- return logout(out, deps);
170
- return login(out, err, deps, !values["no-browser"]);
171
- }
172
- async function whoami(out, err, deps) {
173
- const session = await cloudSession(deps);
174
- if (!session) {
175
- err.write("Not signed in. Run ocra login.\n");
176
- return 1;
177
- }
178
- const me = await fetchMe(deps, session.server, session.access_token);
179
- if (!me) {
180
- err.write("The saved session was revoked. Run ocra login.\n");
181
- return 1;
182
- }
183
- out.write(forTerminal(`${me.login} on ${session.server}\n`));
184
- return 0;
185
- }
186
- async function logout(out, deps) {
187
- const saved = await readCredentials(deps.credentialsPath);
188
- if (saved) {
189
- // Ends the session on the server too; the local file goes either way.
190
- const session = await cloudSession(deps).catch(() => undefined);
191
- if (session) {
192
- await postJson(deps, `${session.server}/api/auth/logout`, {}, session.access_token).catch(() => { });
193
- }
194
- }
195
- await rm(deps.credentialsPath, { force: true });
196
- out.write(saved ? "Signed out.\n" : "Not signed in.\n");
197
- return 0;
198
- }
199
- async function login(out, err, deps, browser) {
200
- const server = cloudUrl(deps.env);
201
- const start = await postJson(deps, `${server}/api/device/code`, { client_name: deps.clientName });
202
- const code = start.body;
203
- if (start.status !== 200 ||
204
- !code.device_code ||
205
- !code.user_code ||
206
- !code.verification_uri_complete) {
207
- err.write(`ocra Cloud did not start a login (HTTP ${start.status}). Try again in a minute.\n`);
208
- return 2;
209
- }
210
- out.write(forTerminal(`Confirm this code in your browser: ${code.user_code}\n` +
211
- ` ${code.verification_uri_complete}\n` +
212
- `(or open ${code.verification_uri} and type it)\n\nWaiting for approval...\n`));
213
- if (browser)
214
- deps.openBrowser(code.verification_uri_complete);
215
- let interval = (code.interval ?? 5) * 1000;
216
- const deadline = deps.now() + (code.expires_in ?? 600) * 1000;
217
- while (deps.now() < deadline) {
218
- await deps.sleep(interval);
219
- let answer;
220
- try {
221
- answer = await postJson(deps, `${server}/api/device/token`, {
222
- device_code: code.device_code,
223
- });
224
- }
225
- catch (error) {
226
- // A dropped connection is retried on the next tick.
227
- err.write(forTerminal(`(${errorMessage(error)}; retrying)\n`));
228
- continue;
229
- }
230
- const t = answer.body;
231
- if (answer.status === 200 && t.access_token && t.refresh_token && t.expires_in) {
232
- const me = await fetchMe(deps, server, t.access_token);
233
- const credentials = {
234
- server,
235
- login: me?.login ?? "",
236
- access_token: t.access_token,
237
- refresh_token: t.refresh_token,
238
- expires_at: deps.now() + t.expires_in * 1000,
239
- };
240
- await writeCredentials(deps.credentialsPath, credentials);
241
- out.write(forTerminal(`Signed in as ${credentials.login}.\n`));
242
- return 0;
243
- }
244
- if (t.error === "authorization_pending")
245
- continue;
246
- if (t.error === "slow_down") {
247
- interval = typeof t.interval === "number" ? t.interval * 1000 : interval + 5000;
248
- continue;
249
- }
250
- if (t.error === "access_denied") {
251
- err.write("Login denied in the browser.\n");
252
- return 1;
253
- }
254
- if (t.error === "expired_token")
255
- break;
256
- err.write(forTerminal(`Login failed: ${t.error ?? `HTTP ${answer.status}`}\n`));
257
- return 2;
258
- }
259
- err.write("The code expired before it was confirmed. Run ocra login again.\n");
260
- return 1;
261
- }
262
- //# sourceMappingURL=cloud.js.map
package/dist/memory.d.ts DELETED
@@ -1,4 +0,0 @@
1
- import type { Output } from "./review/progress.js";
2
- export declare const MEMORY_USAGE = "Usage: ocra memory <command>\n\nRemember findings the team accepts, so ocra stops reporting them.\nEntries live in .ocra/memory.json; commit it to share them.\n\nCommands:\n list Show remembered findings\n add <id> --reason <text> Remember a finding from the last review\n [--session <dir>] (default: the newest session in .ocra/sessions)\n";
3
- export declare function memoryCommand(argv: string[], out: Output, cwd: string, now?: Date): Promise<number>;
4
- //# sourceMappingURL=memory.d.ts.map
package/dist/metrics.d.ts DELETED
@@ -1,47 +0,0 @@
1
- import { type Severity, type Verification } from "@open-cr-agent/core";
2
- import type { Output } from "./review/progress.js";
3
- export declare const METRICS_VERSION = 1;
4
- export declare const METRICS_USAGE = "Usage: ocra metrics [options]\n\nCounts over the finished reviews in .ocra/sessions: runs, cost, findings,\nwhat reviewers later fixed or dismissed, per reviewer. Reads each session's\nreport.json (report version 1); nothing calls a model.\n\nOptions:\n --sessions <dir> Sessions directory (default: .ocra/sessions of this repository)\n --since <date> Only runs started at or after this ISO 8601 date\n --format <text|json> Output format (default: text); the JSON carries \"version\": 1\n -h, --help Show help\n";
5
- export interface ReviewerMetrics {
6
- tasks: number;
7
- failedTasks: number;
8
- findings: number;
9
- costUsd: number;
10
- fixed: number;
11
- dismissed: number;
12
- acceptanceRate: number | null;
13
- }
14
- export interface Metrics {
15
- version: typeof METRICS_VERSION;
16
- sessionsDir: string;
17
- since?: string;
18
- runs: {
19
- total: number;
20
- byVerdict: Record<string, number>;
21
- incomplete: number;
22
- unreadable: number;
23
- };
24
- cost: {
25
- usd: number;
26
- inputTokens: number;
27
- outputTokens: number;
28
- usdPerRun: number | null;
29
- };
30
- findings: {
31
- reported: number;
32
- unique: number;
33
- bySeverity: Record<Severity, number>;
34
- byVerification: Record<Verification, number>;
35
- };
36
- outcomes: {
37
- fixed: number;
38
- dismissed: number;
39
- acceptanceRate: number | null;
40
- };
41
- reviewers: Record<string, ReviewerMetrics>;
42
- }
43
- export declare function metricsCommand(argv: string[], out: Output, cwd: string): Promise<number>;
44
- export declare function sessionStart(id: string): Date | undefined;
45
- export declare function collectMetrics(sessionsDir: string, since?: Date): Promise<Metrics>;
46
- export declare function renderMetrics(m: Metrics): string;
47
- //# sourceMappingURL=metrics.d.ts.map
@@ -1,12 +0,0 @@
1
- import type { CustomProvider, ModelChains } from "@open-cr-agent/core";
2
- import { type CloudDeps } from "../cloud.js";
3
- export declare const CLOUD_PREFIX = "ocra-";
4
- /** Where the access token travels to the runtime; never a user's variable (OCRA_ is refused in config). */
5
- export declare const CLOUD_TOKEN_ENV = "OCRA_CLOUD_ACCESS_TOKEN";
6
- type Env = Readonly<Record<string, string | undefined>>;
7
- export declare function withCloudProviders(models: ModelChains, providers: Readonly<Record<string, CustomProvider>>, env: Env, deps: CloudDeps | undefined, warn: (message: string) => void): Promise<{
8
- providers: Record<string, CustomProvider>;
9
- env: Env;
10
- }>;
11
- export {};
12
- //# sourceMappingURL=cloud-providers.d.ts.map
@@ -1,59 +0,0 @@
1
- import { OcraError } from "@open-cr-agent/core";
2
- import { cloudSession } from "../cloud.js";
3
- // Models named ocra-<provider>/<model> go through the ocra Cloud gateway with
4
- // the key stored there for <provider> (ADR-0024). Each such provider becomes
5
- // an OpenAI-compatible endpoint declared for this run, authorized by the
6
- // signed-in session's access token. Without such a model nothing here reads
7
- // the session or touches the network.
8
- export const CLOUD_PREFIX = "ocra-";
9
- /** Where the access token travels to the runtime; never a user's variable (OCRA_ is refused in config). */
10
- export const CLOUD_TOKEN_ENV = "OCRA_CLOUD_ACCESS_TOKEN";
11
- // A review may run for half an hour; start it with a token that outlives it.
12
- const MIN_TOKEN_MS = 35 * 60_000;
13
- export async function withCloudProviders(models, providers, env, deps, warn) {
14
- const wanted = new Map();
15
- for (const model of Object.values(models).flat()) {
16
- const slash = model.indexOf("/");
17
- const id = model.slice(0, slash);
18
- if (slash < 0 || !id.startsWith(CLOUD_PREFIX) || providers[id])
19
- continue;
20
- const set = wanted.get(id) ?? new Set();
21
- set.add(model.slice(slash + 1));
22
- wanted.set(id, set);
23
- }
24
- if (wanted.size === 0)
25
- return { providers: { ...providers }, env };
26
- const names = [...wanted.keys()].join(", ");
27
- if (env.OCRA_CLOUD === "off") {
28
- throw new OcraError("CONFIG_INVALID", `Models name ocra Cloud (${names}) but OCRA_CLOUD=off`);
29
- }
30
- const session = deps && (await cloudSession(deps, MIN_TOKEN_MS));
31
- if (!session) {
32
- throw new OcraError("CONFIG_CREDENTIALS_MISSING", `Models name ocra Cloud (${names}): sign in with ocra login`);
33
- }
34
- const res = await deps.fetch(`${session.server}/api/providers`, {
35
- signal: AbortSignal.timeout(30_000),
36
- });
37
- if (!res.ok) {
38
- throw new OcraError("RUNTIME_START_FAILED", `ocra Cloud answered ${res.status} for its providers`);
39
- }
40
- const listed = (await res.json()).providers ?? [];
41
- const declared = { ...providers };
42
- for (const [id, modelIds] of wanted) {
43
- const name = id.slice(CLOUD_PREFIX.length);
44
- const chat = listed
45
- .find((p) => p.name === name)
46
- ?.paths.find((path) => path.endsWith("/chat/completions"));
47
- if (!chat) {
48
- throw new OcraError("CONFIG_INVALID", `ocra Cloud has no OpenAI-compatible chat endpoint for "${name}" (named by ${id})`);
49
- }
50
- declared[id] = {
51
- baseUrl: `${session.server}/api/gateway/${name}${chat.slice(0, -"/chat/completions".length)}`,
52
- apiKeyEnv: CLOUD_TOKEN_ENV,
53
- models: Object.fromEntries([...modelIds].map((m) => [m, { input: 0, output: 0 }])),
54
- };
55
- }
56
- warn(`${names}: models through ocra Cloud are not priced, so reported cost and --max-cost-usd do not count them`);
57
- return { providers: declared, env: { ...env, [CLOUD_TOKEN_ENV]: session.access_token } };
58
- }
59
- //# sourceMappingURL=cloud-providers.js.map
@@ -1,40 +0,0 @@
1
- import { type ModelChains, type ReviewReport } from "@open-cr-agent/core";
2
- import { type CloudDeps } from "../cloud.js";
3
- export type ReviewSource = "local" | "github" | "gitlab";
4
- export type ReviewUpload = {
5
- runId: string;
6
- repoHash: string;
7
- source: ReviewSource;
8
- tier: string;
9
- verdict: string;
10
- complete: boolean;
11
- findings: {
12
- critical: number;
13
- warning: number;
14
- suggestion: number;
15
- };
16
- files: {
17
- reviewed: number;
18
- notReviewed: number;
19
- };
20
- tasks: {
21
- completed: number;
22
- failed: number;
23
- };
24
- usage: {
25
- inputTokens: number;
26
- outputTokens: number;
27
- costUsd: number;
28
- };
29
- durationMs: number;
30
- ocraVersion: string;
31
- };
32
- export declare function uploadOf(report: ReviewReport, source: ReviewSource, repoHash: string, durationMs: number): ReviewUpload;
33
- export declare function repoHash(root: string, credentialsPath: string): Promise<string>;
34
- /** Whether this run talks to ocra Cloud at all: signed in, not turned off. */
35
- export declare function cloudEnabled(deps: CloudDeps): Promise<boolean>;
36
- /** Sends the counts; a failure is a warning, never a failed review. */
37
- export declare function uploadReview(upload: ReviewUpload, deps: CloudDeps, warn: (message: string) => void): Promise<boolean>;
38
- /** The default models chosen on the web, for a review that configures none. */
39
- export declare function defaultModels(deps: CloudDeps): Promise<ModelChains>;
40
- //# sourceMappingURL=cloud-upload.d.ts.map
@@ -1,135 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { createHash, randomBytes } from "node:crypto";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
5
- import { promisify } from "node:util";
6
- import { coverageGaps } from "@open-cr-agent/core";
7
- import { cloudSession, readCredentials } from "../cloud.js";
8
- import { VERSION } from "../version.js";
9
- // After a review, a signed-in CLI sends ocra Cloud its counts (ADR-0024):
10
- // the verdict, how many findings of each severity, files and tasks, tokens
11
- // and time. Never a path, a title, a finding's text or code. The repository
12
- // is a hash salted with a value that stays on this machine, so the server
13
- // can group reviews of one repository without learning which it is.
14
- const run = promisify(execFile);
15
- export function uploadOf(report, source, repoHash, durationMs) {
16
- const severity = (s) => report.findings.filter((f) => f.severity === s).length;
17
- const gaps = coverageGaps(report);
18
- return {
19
- runId: report.runId,
20
- repoHash,
21
- source,
22
- tier: report.tier,
23
- verdict: report.verdict,
24
- complete: gaps.notReviewed === 0,
25
- findings: {
26
- critical: severity("critical"),
27
- warning: severity("warning"),
28
- suggestion: severity("suggestion"),
29
- },
30
- files: {
31
- reviewed: report.coverage.filter((c) => c.status === "reviewed").length,
32
- notReviewed: gaps.notReviewed,
33
- },
34
- tasks: {
35
- completed: report.tasks.filter((t) => t.status === "completed").length,
36
- failed: report.tasks.filter((t) => t.status === "failed").length,
37
- },
38
- usage: {
39
- inputTokens: report.usage.inputTokens,
40
- outputTokens: report.usage.outputTokens,
41
- costUsd: report.usage.costUsd ?? 0,
42
- },
43
- durationMs: Math.round(durationMs),
44
- ocraVersion: VERSION,
45
- };
46
- }
47
- /** The repository's origin without credentials, or its root path when it has none. */
48
- async function repositoryId(root) {
49
- try {
50
- const { stdout } = await run("git", ["-C", root, "remote", "get-url", "origin"], {
51
- timeout: 10_000,
52
- });
53
- const url = stdout.trim();
54
- return url
55
- .replace(/^[a-z+]+:\/\/[^@/]*@/i, (m) => m.slice(0, m.indexOf("//") + 2))
56
- .replace(/\.git$/, "")
57
- .toLowerCase();
58
- }
59
- catch {
60
- return root;
61
- }
62
- }
63
- /** A random salt kept beside the credentials, made on first use. */
64
- async function salt(credentialsPath) {
65
- const path = join(dirname(credentialsPath), "upload-salt");
66
- try {
67
- const existing = (await readFile(path, "utf8")).trim();
68
- if (/^[0-9a-f]{64}$/.test(existing))
69
- return existing;
70
- }
71
- catch { }
72
- const fresh = randomBytes(32).toString("hex");
73
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
74
- await writeFile(path, `${fresh}\n`, { mode: 0o600 });
75
- return fresh;
76
- }
77
- export async function repoHash(root, credentialsPath) {
78
- return createHash("sha256")
79
- .update(`${await salt(credentialsPath)}:${await repositoryId(root)}`)
80
- .digest("hex");
81
- }
82
- /** Whether this run talks to ocra Cloud at all: signed in, not turned off. */
83
- export async function cloudEnabled(deps) {
84
- return (deps.env.OCRA_CLOUD !== "off" && (await readCredentials(deps.credentialsPath)) !== undefined);
85
- }
86
- /** Sends the counts; a failure is a warning, never a failed review. */
87
- export async function uploadReview(upload, deps, warn) {
88
- try {
89
- const session = await cloudSession(deps);
90
- if (!session)
91
- return false;
92
- const res = await deps.fetch(`${session.server}/api/reviews`, {
93
- method: "POST",
94
- headers: {
95
- "content-type": "application/json",
96
- authorization: `Bearer ${session.access_token}`,
97
- "user-agent": `ocra/${VERSION}`,
98
- },
99
- body: JSON.stringify(upload),
100
- signal: AbortSignal.timeout(15_000),
101
- });
102
- if (!res.ok) {
103
- warn(`ocra Cloud did not take the review's counts (HTTP ${res.status})`);
104
- return false;
105
- }
106
- return true;
107
- }
108
- catch (error) {
109
- warn(`could not send the review's counts to ocra Cloud: ${error instanceof Error ? error.name : "error"}`);
110
- return false;
111
- }
112
- }
113
- /** The default models chosen on the web, for a review that configures none. */
114
- export async function defaultModels(deps) {
115
- const session = await cloudSession(deps);
116
- if (!session)
117
- return {};
118
- const res = await deps.fetch(`${session.server}/api/preferences/models`, {
119
- headers: { authorization: `Bearer ${session.access_token}`, "user-agent": `ocra/${VERSION}` },
120
- signal: AbortSignal.timeout(15_000),
121
- });
122
- if (!res.ok)
123
- return {};
124
- const body = (await res.json());
125
- const chains = {};
126
- for (const tier of ["top", "standard", "light"]) {
127
- const list = body.models?.[tier];
128
- if (Array.isArray(list) && list.every((m) => typeof m === "string" && m.startsWith("ocra-"))) {
129
- if (list.length > 0)
130
- chains[tier] = list;
131
- }
132
- }
133
- return chains;
134
- }
135
- //# sourceMappingURL=cloud-upload.js.map
@@ -1,31 +0,0 @@
1
- import { type OcraPlugin } from "@open-cr-agent/core";
2
- import type { CloudDeps } from "../cloud.js";
3
- import type { ReviewArgs } from "./args.js";
4
- import { type Output } from "./progress.js";
5
- import type { RuntimeLoaders } from "./runtimes.js";
6
- export declare const SESSIONS_DIR = ".ocra/sessions";
7
- export declare const EXIT: {
8
- readonly ok: 0;
9
- readonly blocking: 1;
10
- readonly error: 2;
11
- readonly incomplete: 3;
12
- readonly interrupted: 130;
13
- };
14
- export declare const BUILTIN_PLUGINS: readonly OcraPlugin[];
15
- export interface ReviewDeps {
16
- cwd: string;
17
- env: Readonly<Record<string, string | undefined>>;
18
- builtinPlugins: readonly OcraPlugin[];
19
- runtimes: RuntimeLoaders;
20
- writeFile(path: string, content: string): Promise<void>;
21
- now(): number;
22
- heartbeatMs: number;
23
- fetch?: typeof fetch;
24
- cloud?: CloudDeps;
25
- onInterrupt?(handler: () => void): () => void;
26
- }
27
- export declare function reviewCommand(args: ReviewArgs, io: {
28
- out: Output;
29
- err: Output;
30
- }, deps: ReviewDeps): Promise<number>;
31
- //# sourceMappingURL=command.d.ts.map