@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
@@ -0,0 +1,199 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { EFFORT_LEVELS, errorMessage, MODEL_TIERS, OcraError, } from "@open-cr-agent/core";
4
+ import { isNotFound } from "@open-cr-agent/core/internal";
5
+ import { z } from "zod";
6
+ import { fetchRemoteConfig } from "./remote.js";
7
+ import { DEFAULT_RUNTIME, effort } from "./schema.js";
8
+ import { layerSchema, listSettings, resolveSettings, } from "./settings.js";
9
+ const CONFIG_PATH = ".ocra/config.json";
10
+ // OCRA_MODEL_TOP, OCRA_EFFORT_STANDARD and so on.
11
+ const tierEnv = (prefix) => Object.fromEntries(MODEL_TIERS.map((tier) => [tier, `${prefix}_${tier.toUpperCase()}`]));
12
+ const MODEL_ENV = tierEnv("OCRA_MODEL");
13
+ const EFFORT_ENV = tierEnv("OCRA_EFFORT");
14
+ export class ConfigError extends OcraError {
15
+ constructor(message, options) {
16
+ super("CONFIG_INVALID", message, options);
17
+ this.name = "ConfigError";
18
+ }
19
+ }
20
+ // Without the repository's file, only defaults and environment variables
21
+ // apply: that file can name plugins, and plugins run code.
22
+ export async function loadConfig(root, env, options = { repository: true }) {
23
+ return (await loadConfigLayers(root, env, options)).config;
24
+ }
25
+ /** The configuration's layers, and what they resolve to on their own. */
26
+ export async function loadConfigLayers(root, env, options = { repository: true }) {
27
+ const layers = await configLayers(root, env, options);
28
+ const { config } = resolveConfig(layers);
29
+ for (const model of unpriced(config.providers, [
30
+ ...Object.values(config.models),
31
+ ...Object.values(agentChains(config)),
32
+ ])) {
33
+ options.warn?.(`${model} has a price of 0: reported cost and --max-cost-usd do not count its tokens`);
34
+ }
35
+ return { config, layers };
36
+ }
37
+ export function resolveConfig(layers) {
38
+ const { settings, sources } = resolveSettings(layers);
39
+ const { ultra, providers, ...rest } = settings;
40
+ return {
41
+ config: {
42
+ ...rest,
43
+ runtime: settings.runtime ?? DEFAULT_RUNTIME,
44
+ models: defined(settings.models),
45
+ effort: defined(settings.effort),
46
+ providers: toProviders(providers),
47
+ },
48
+ sources,
49
+ ultra,
50
+ listed: listSettings(settings, sources),
51
+ };
52
+ }
53
+ function defined(map) {
54
+ return Object.fromEntries(Object.entries(map).filter(([, value]) => value !== undefined));
55
+ }
56
+ // The configuration's layers, earliest first: a shared configuration it
57
+ // extends, the file, and OCRA_MODEL_<TIER> and OCRA_EFFORT_<TIER> on top.
58
+ async function configLayers(root, env, options) {
59
+ const read = options.read ?? ((path) => readConfigFromDisk(root, path));
60
+ const files = options.file
61
+ ? await fileLayers(ownFile(options.file), options, options.file)
62
+ : options.repository
63
+ ? await fileLayers(read, options, CONFIG_PATH)
64
+ : [];
65
+ return [...files, envLayer(env)];
66
+ }
67
+ function envLayer(env) {
68
+ const models = {};
69
+ const efforts = {};
70
+ for (const tier of MODEL_TIERS) {
71
+ const chain = (env[MODEL_ENV[tier]] ?? "")
72
+ .split(",")
73
+ .map((m) => m.trim())
74
+ .filter((m) => m !== "");
75
+ if (chain.length > 0)
76
+ models[tier] = chain;
77
+ }
78
+ for (const tier of MODEL_TIERS) {
79
+ const name = EFFORT_ENV[tier];
80
+ const value = env[name]?.trim();
81
+ if (!value)
82
+ continue;
83
+ const level = effort.safeParse(value);
84
+ if (!level.success) {
85
+ throw new ConfigError(`${name} must be one of ${EFFORT_LEVELS.join(", ")}`);
86
+ }
87
+ efforts[tier] = level.data;
88
+ }
89
+ return { source: "env", settings: { models, effort: efforts } };
90
+ }
91
+ function toProviders(declared) {
92
+ return Object.fromEntries(Object.entries(declared).map(([id, p]) => [
93
+ id,
94
+ {
95
+ baseUrl: p.baseUrl,
96
+ ...(p.apiKeyEnv ? { apiKeyEnv: p.apiKeyEnv } : {}),
97
+ ...(p.effort ? { effort: p.effort } : {}),
98
+ models: Object.fromEntries(Object.entries(p.models).map(([model, price]) => [
99
+ model,
100
+ {
101
+ input: price.input,
102
+ output: price.output,
103
+ ...(price.cachedInput === undefined ? {} : { cachedInput: price.cachedInput }),
104
+ },
105
+ ])),
106
+ },
107
+ ]));
108
+ }
109
+ // The reviewers' and roles' own chains, by agent id (ADR-0025); a disabled
110
+ // reviewer's is left out, since it makes no call.
111
+ export function agentChains(config) {
112
+ const chains = {};
113
+ for (const [id, setting] of Object.entries(config.reviewers)) {
114
+ if (setting.models && setting.enabled !== false)
115
+ chains[id] = setting.models;
116
+ }
117
+ for (const [role, setting] of Object.entries(config.roles)) {
118
+ if (setting?.models)
119
+ chains[role] = setting.models;
120
+ }
121
+ return chains;
122
+ }
123
+ // Models of declared providers, named in a chain, priced at 0 per token.
124
+ function unpriced(providers, chains) {
125
+ const named = new Set(chains.flatMap((chain) => chain ?? []));
126
+ return [...named].filter((model) => {
127
+ const slash = model.indexOf("/");
128
+ const price = providers[model.slice(0, slash)]?.models[model.slice(slash + 1)];
129
+ return price !== undefined && price.input === 0 && price.output === 0;
130
+ });
131
+ }
132
+ // The user's own checkout, read like any local file (links followed): unlike
133
+ // the review's reads, which never follow links (vcs-local), this is trusted
134
+ // configuration, and --no-repo-config skips it for code that is not.
135
+ function ownFile(file) {
136
+ return async () => {
137
+ try {
138
+ return await readFile(file, "utf8");
139
+ }
140
+ catch (error) {
141
+ throw new ConfigError(`cannot read ${file}: ${errorMessage(error)}`, { cause: error });
142
+ }
143
+ };
144
+ }
145
+ async function readConfigFromDisk(root, path) {
146
+ try {
147
+ return await readFile(join(root, path), "utf8");
148
+ }
149
+ catch (error) {
150
+ if (isNotFound(error))
151
+ return undefined;
152
+ throw error;
153
+ }
154
+ }
155
+ // The file, and the shared configuration it extends under it.
156
+ async function fileLayers(read, options, label) {
157
+ const text = await read(CONFIG_PATH);
158
+ if (text === undefined)
159
+ return [];
160
+ let data;
161
+ try {
162
+ data = JSON.parse(text);
163
+ }
164
+ catch (error) {
165
+ throw new ConfigError(`${label} is not valid JSON: ${errorMessage(error)}`, {
166
+ cause: error,
167
+ });
168
+ }
169
+ const local = layerSchema.safeParse(data);
170
+ if (!local.success)
171
+ throw new ConfigError(`${label} is invalid: ${z.prettifyError(local.error)}`);
172
+ const file = { source: "file", settings: local.data };
173
+ const shared = local.data.extends;
174
+ if (!shared)
175
+ return [file];
176
+ // A shared configuration that cannot be loaded costs its defaults, not the
177
+ // review: the repository's own settings still apply.
178
+ let remote;
179
+ try {
180
+ remote = await fetchRemoteConfig(shared, options.fetch);
181
+ }
182
+ catch (error) {
183
+ // Its limits are lost with it; say so rather than run silently unbounded.
184
+ const limits = local.data.maxCostUsd === undefined
185
+ ? "; none of its settings apply, including any spend or task limit"
186
+ : "; none of its settings apply";
187
+ options.warn?.(`could not load extends ${shared}: ${errorMessage(error)}${limits}`);
188
+ return [file];
189
+ }
190
+ const { rules = [], ...rest } = remote;
191
+ const settings = layerSchema.safeParse(rest);
192
+ if (!settings.success) {
193
+ options.warn?.(`ignoring extends ${shared}: its settings are invalid (${z.prettifyError(settings.error)})`);
194
+ return [file];
195
+ }
196
+ const sourced = rules.map((rule) => ({ ...rule, source: "shared" }));
197
+ return [{ source: "shared", settings: { ...settings.data, rules: sourced } }, file];
198
+ }
199
+ //# sourceMappingURL=cli-config.js.map
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export declare const remoteConfigSchema: z.ZodObject<{
2
+ declare const remoteConfigSchema: z.ZodObject<{
3
3
  $schema: z.ZodOptional<z.ZodString>;
4
4
  models: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
5
5
  light: "light";
@@ -31,5 +31,5 @@ export declare const remoteConfigSchema: z.ZodObject<{
31
31
  }, z.core.$strict>;
32
32
  export type RemoteConfig = z.infer<typeof remoteConfigSchema>;
33
33
  export declare function fetchRemoteConfig(spec: string, fetchImpl?: typeof fetch): Promise<RemoteConfig>;
34
- export declare function mergeConfig(remote: RemoteConfig, local: Record<string, unknown>): Record<string, unknown>;
35
- //# sourceMappingURL=remote-config.d.ts.map
34
+ export {};
35
+ //# sourceMappingURL=remote.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { OcraError } from "@open-cr-agent/core";
2
+ import { MODEL_TIERS, OcraError } from "@open-cr-agent/core";
3
3
  import { repoRuleSchema } from "@open-cr-agent/core/internal";
4
4
  import { z } from "zod";
5
5
  const MAX_BYTES = 256 * 1024;
@@ -7,11 +7,11 @@ const TIMEOUT_MS = 10_000;
7
7
  // What a shared configuration may set: settings, rules and model providers
8
8
  // (a company gateway, for example), never plugins or a runtime, because it
9
9
  // is fetched from outside the repository.
10
- export const remoteConfigSchema = z
10
+ const remoteConfigSchema = z
11
11
  .object({
12
12
  $schema: z.string().optional(),
13
- models: z.record(z.enum(["top", "standard", "light"]), z.unknown()).optional(),
14
- effort: z.partialRecord(z.enum(["top", "standard", "light"]), z.unknown()).optional(),
13
+ models: z.record(z.enum(MODEL_TIERS), z.unknown()).optional(),
14
+ effort: z.partialRecord(z.enum(MODEL_TIERS), z.unknown()).optional(),
15
15
  concurrency: z.unknown().optional(),
16
16
  taskTimeoutMinutes: z.unknown().optional(),
17
17
  runTimeoutMinutes: z.unknown().optional(),
@@ -81,22 +81,4 @@ async function readLimited(response, limit, name) {
81
81
  }
82
82
  return Buffer.concat(chunks).toString("utf8");
83
83
  }
84
- // The repository's own values win; lists and maps are combined.
85
- export function mergeConfig(remote, local) {
86
- const { rules: _rules, ...settings } = remote;
87
- const merged = { ...settings, ...local };
88
- for (const key of ["models", "effort", "reviewers", "roles", "github", "providers"]) {
89
- const a = settings[key];
90
- const b = local[key];
91
- if (a !== undefined || b !== undefined)
92
- merged[key] = { ...(a ?? {}), ...(b ?? {}) };
93
- }
94
- for (const key of ["include", "exclude"]) {
95
- const a = settings[key] ?? [];
96
- const b = local[key] ?? [];
97
- if (a.length + b.length > 0)
98
- merged[key] = [...a, ...b];
99
- }
100
- return merged;
101
- }
102
- //# sourceMappingURL=remote-config.js.map
84
+ //# sourceMappingURL=remote.js.map
@@ -1,7 +1,13 @@
1
- import { type CustomProvider, type ModelChains, OcraError, type RepoRule, type TierEfforts } from "@open-cr-agent/core";
2
1
  import { z } from "zod";
3
- export declare const CONFIG_PATH = ".ocra/config.json";
4
- declare const configSchema: z.ZodObject<{
2
+ export declare const effort: z.ZodEnum<{
3
+ high: "high";
4
+ low: "low";
5
+ medium: "medium";
6
+ minimal: "minimal";
7
+ none: "none";
8
+ }>;
9
+ export declare const DEFAULT_RUNTIME = "opencode";
10
+ export declare const configSchema: z.ZodObject<{
5
11
  $schema: z.ZodOptional<z.ZodString>;
6
12
  models: z.ZodDefault<z.ZodObject<{
7
13
  top: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
@@ -48,7 +54,7 @@ declare const configSchema: z.ZodObject<{
48
54
  }, z.core.$strict>>;
49
55
  include: z.ZodDefault<z.ZodArray<z.ZodString>>;
50
56
  exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
51
- runtime: z.ZodDefault<z.ZodUnion<[z.ZodEnum<{
57
+ runtime: z.ZodOptional<z.ZodUnion<[z.ZodEnum<{
52
58
  direct: "direct";
53
59
  opencode: "opencode";
54
60
  }>, z.ZodString]>>;
@@ -60,6 +66,7 @@ declare const configSchema: z.ZodObject<{
60
66
  lite: "lite";
61
67
  trivial: "trivial";
62
68
  }>>;
69
+ models: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
63
70
  effort: z.ZodOptional<z.ZodEnum<{
64
71
  high: "high";
65
72
  low: "low";
@@ -73,6 +80,7 @@ declare const configSchema: z.ZodObject<{
73
80
  judge: "judge";
74
81
  verifier: "verifier";
75
82
  }> & z.core.$partial, z.ZodObject<{
83
+ models: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
76
84
  effort: z.ZodOptional<z.ZodEnum<{
77
85
  high: "high";
78
86
  low: "low";
@@ -100,24 +108,4 @@ declare const configSchema: z.ZodObject<{
100
108
  }, z.core.$strict>;
101
109
  export declare const CONFIG_SCHEMA_ID = "https://ocra.majincheng.com/schema/config.v1.json";
102
110
  export declare function configJsonSchema(): Record<string, unknown>;
103
- export type CliConfig = Omit<z.infer<typeof configSchema>, "models" | "effort" | "providers"> & {
104
- models: ModelChains;
105
- effort: TierEfforts;
106
- providers: Record<string, CustomProvider>;
107
- rules: RepoRule[];
108
- };
109
- export interface LoadOptions {
110
- repository: boolean;
111
- read?: (path: string) => Promise<string | undefined>;
112
- file?: string;
113
- fetch?: typeof fetch;
114
- warn?: (message: string) => void;
115
- }
116
- export declare class ConfigError extends OcraError {
117
- constructor(message: string, options?: {
118
- cause?: unknown;
119
- });
120
- }
121
- export declare function loadConfig(root: string, env: Readonly<Record<string, string | undefined>>, options?: LoadOptions): Promise<CliConfig>;
122
- export {};
123
- //# sourceMappingURL=config.d.ts.map
111
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { EFFORT_LEVELS } from "@open-cr-agent/core";
2
+ import { AGENT_ROLES, MAX_TIMER_MS, RISK_TIERS } from "@open-cr-agent/core/internal";
3
+ import { z } from "zod";
4
+ // What .ocra/config.json may say. Every layer of settings (settings.ts) is
5
+ // checked against it.
6
+ // How much a model reasons before answering (ADR-0025); unset leaves the
7
+ // provider's default.
8
+ export const effort = z.enum(EFFORT_LEVELS);
9
+ // The longest timeout a timer keeps, about 24.8 days; a longer one would fire at once.
10
+ const timeoutMinutes = z
11
+ .number()
12
+ .positive()
13
+ .max(Math.floor(MAX_TIMER_MS / 60_000))
14
+ .optional();
15
+ const modelChain = z
16
+ .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
17
+ .transform((value) => (typeof value === "string" ? [value] : value));
18
+ // Where review code is sent: over https, or plain http only to this machine.
19
+ // OpenCode replaces {env:NAME} and {file:path} anywhere in its configuration,
20
+ // so an address with braces could carry a variable or a file (the checkout's
21
+ // .git/config holds its token) to the endpoint.
22
+ const endpoint = z
23
+ .url()
24
+ .refine((value) => {
25
+ const url = new URL(value);
26
+ if (url.protocol === "https:")
27
+ return true;
28
+ const host = url.hostname.replace(/^\[|\]$/g, "");
29
+ return (url.protocol === "http:" &&
30
+ (host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host)));
31
+ }, "must be an https URL, or http on this machine (localhost, 127.0.0.1, ::1)")
32
+ .refine((value) => !/[{}]/.test(value), "must not contain { or }");
33
+ // The platforms' and clouds' credentials never go to a model endpoint; the
34
+ // same prefixes are never passed to the runtime by prefix (server-env.ts).
35
+ const PLATFORM_TOKENS = /^(GITHUB_|GH_|GITLAB_|CI_|ACTIONS_|RUNNER_|AWS_|AZURE_|NPM_|SSH_|OCRA_)/;
36
+ const price = z.number().min(0).max(10_000);
37
+ const providerSchema = z
38
+ .object({
39
+ type: z.literal("openai-compatible"),
40
+ baseUrl: endpoint,
41
+ apiKeyEnv: z
42
+ .string()
43
+ .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be an environment variable name")
44
+ .refine((name) => !PLATFORM_TOKENS.test(name), "must not name a platform or cloud credential")
45
+ .optional(),
46
+ // Prices in US dollars per million tokens; 0 means unpriced.
47
+ models: z
48
+ .record(z.string().regex(/^[\w./:@-]{1,200}$/), z.object({ input: price, output: price, cachedInput: price.optional() }).strict())
49
+ .refine((models) => Object.keys(models).length > 0, "must list at least one model"),
50
+ // How the endpoint takes a reasoning effort: "openai" sends
51
+ // reasoning_effort (the default), "openrouter" sends reasoning.effort.
52
+ effort: z.enum(["openai", "openrouter"]).optional(),
53
+ })
54
+ .strict();
55
+ // The runtimes ocra ships; a plugin may register another name, so the
56
+ // schema lists these for editors and still accepts any other string.
57
+ const BUILTIN_RUNTIMES = ["opencode", "direct"];
58
+ export const DEFAULT_RUNTIME = "opencode";
59
+ export const configSchema = z
60
+ .object({
61
+ $schema: z.string().optional(),
62
+ models: z
63
+ .object({ top: modelChain, standard: modelChain, light: modelChain })
64
+ .partial()
65
+ .strict()
66
+ .default({}),
67
+ effort: z
68
+ .object({ top: effort, standard: effort, light: effort })
69
+ .partial()
70
+ .strict()
71
+ .default({}),
72
+ concurrency: z.number().int().min(1).max(32).optional(),
73
+ taskTimeoutMinutes: timeoutMinutes,
74
+ runTimeoutMinutes: timeoutMinutes,
75
+ verify: z.boolean().optional(),
76
+ judge: z.boolean().optional(),
77
+ maxCostUsd: z.number().positive().optional(),
78
+ maxTasks: z.number().int().min(1).max(1_000).optional(),
79
+ // Unset: each provider's default. A seed makes sampling repeatable only
80
+ // where the provider supports it.
81
+ sampling: z
82
+ .object({
83
+ temperature: z.number().min(0).max(2),
84
+ seed: z
85
+ .number()
86
+ .int()
87
+ .min(0)
88
+ .max(2 ** 31 - 1),
89
+ })
90
+ .partial()
91
+ .strict()
92
+ .optional(),
93
+ github: z
94
+ .object({ requestChanges: z.boolean(), botLogin: z.string().min(1) })
95
+ .partial()
96
+ .strict()
97
+ .default({}),
98
+ include: z.array(z.string().min(1)).default([]),
99
+ exclude: z.array(z.string().min(1)).default([]),
100
+ // Unset means "opencode"; loadConfig fills it in and records whether the
101
+ // file chose, so account defaults (ADR-0025) apply only where it did not.
102
+ runtime: z.enum(BUILTIN_RUNTIMES).or(z.string().min(1)).optional(),
103
+ plugins: z.array(z.string().min(1)).default([]),
104
+ reviewers: z
105
+ .record(z.string(), z
106
+ .object({
107
+ enabled: z.boolean(),
108
+ minTier: z.enum(RISK_TIERS),
109
+ // The reviewer's own chain, for its review tasks and plan call.
110
+ models: modelChain,
111
+ effort,
112
+ })
113
+ .partial()
114
+ .strict())
115
+ .default({}),
116
+ roles: z
117
+ .partialRecord(z.enum(AGENT_ROLES), z.object({ models: modelChain, effort }).partial().strict())
118
+ .default({}),
119
+ pluginSettings: z.record(z.string(), z.unknown()).default({}),
120
+ providers: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,39}$/), providerSchema).default({}),
121
+ extends: z.string().min(1).optional(),
122
+ })
123
+ .strict();
124
+ export const CONFIG_SCHEMA_ID = "https://ocra.majincheng.com/schema/config.v1.json";
125
+ // JSON Schema (draft 2020-12) of .ocra/config.json, for editors: what a
126
+ // file may say, so defaults are optional and model chains take either form.
127
+ export function configJsonSchema() {
128
+ const generated = z.toJSONSchema(configSchema, { target: "draft-2020-12", io: "input" });
129
+ const { $schema, ...rest } = generated;
130
+ return {
131
+ $schema,
132
+ $id: CONFIG_SCHEMA_ID,
133
+ title: "ocra configuration",
134
+ description: "The .ocra/config.json file of a repository, or a file passed with --config. Unknown keys are rejected.",
135
+ ...rest,
136
+ };
137
+ }
138
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,37 @@
1
+ import { type SourcedRule } from "@open-cr-agent/core";
2
+ import { z } from "zod";
3
+ import { configSchema } from "./schema.js";
4
+ type FileSettings = z.output<typeof configSchema>;
5
+ export type Settings = FileSettings & {
6
+ rules: SourcedRule[];
7
+ ultra: boolean;
8
+ };
9
+ type SettingSource = "shared" | "file" | "env" | "account" | "flag";
10
+ export type LayerSettings = {
11
+ [K in keyof Settings]?: Settings[K];
12
+ };
13
+ export interface SettingsLayer {
14
+ source: SettingSource;
15
+ settings: LayerSettings;
16
+ under?: boolean;
17
+ }
18
+ export type SettingSources = Record<string, SettingSource[]>;
19
+ export declare const layerSchema: z.ZodType<Omit<LayerSettings, "rules" | "ultra">>;
20
+ /** The layers, earliest first, over ocra's defaults, and where each setting came from. */
21
+ export declare function resolveSettings(layers: readonly SettingsLayer[]): {
22
+ settings: Settings;
23
+ sources: SettingSources;
24
+ };
25
+ export interface ListedSetting {
26
+ key: string;
27
+ value?: unknown;
28
+ sources: SettingSource[];
29
+ }
30
+ /**
31
+ * The settings --plan lists, in its order, with their sources. A setting
32
+ * only a command-line flag set is the flag's, not a setting, and is left
33
+ * out.
34
+ */
35
+ export declare function listSettings(settings: Settings, sources: SettingSources): ListedSetting[];
36
+ export {};
37
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { MODEL_TIERS, OcraError } from "@open-cr-agent/core";
3
+ import { z } from "zod";
4
+ import { configSchema } from "./schema.js";
5
+ const ALL = ["shared", "file", "env", "account"];
6
+ const NOT_ENV = ["shared", "file", "account"];
7
+ const ALWAYS = { list: "always" };
8
+ // How each setting merges, which layers may set it, and how --plan lists
9
+ // it, in --plan's order. A shared configuration is fetched from outside the
10
+ // repository, so it never names plugins or a runtime; the account never
11
+ // moves code or keys (providers, extends) or decides whose comments are
12
+ // trusted (github). The layers' parsers refuse these with a warning; here
13
+ // they are a broken invariant.
14
+ const MERGE = {
15
+ runtime: { merge: "replace", from: ["file", "account"], plan: ALWAYS },
16
+ models: { merge: "entries", from: ALL, plan: { list: "always", entries: MODEL_TIERS } },
17
+ effort: { merge: "entries", from: ALL, plan: { list: "always", entries: MODEL_TIERS } },
18
+ reviewers: { merge: "entries", from: NOT_ENV, plan: { list: "set" } },
19
+ roles: { merge: "entries", from: NOT_ENV, plan: { list: "set" } },
20
+ concurrency: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
21
+ taskTimeoutMinutes: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
22
+ runTimeoutMinutes: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
23
+ maxCostUsd: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
24
+ maxTasks: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
25
+ verify: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
26
+ judge: { merge: "replace", from: NOT_ENV, plan: ALWAYS },
27
+ sampling: { merge: "replace", from: ["file", "account"], plan: ALWAYS },
28
+ include: { merge: "concat", from: NOT_ENV, plan: ALWAYS },
29
+ exclude: { merge: "concat", from: NOT_ENV, plan: ALWAYS },
30
+ rules: {
31
+ merge: "concat",
32
+ from: ["shared", "account"],
33
+ // Where each rule applies and whose it is; the rule's text can be long.
34
+ plan: {
35
+ list: "always",
36
+ show: (rules) => rules.map(({ path, source }) => ({ path, source })),
37
+ },
38
+ },
39
+ ultra: { merge: "replace", from: ["account", "flag"], plan: { list: "set" } },
40
+ $schema: { merge: "replace", from: ["shared", "file"] },
41
+ github: { merge: "entries", from: ["shared", "file"] },
42
+ plugins: { merge: "replace", from: ["file"] },
43
+ pluginSettings: { merge: "replace", from: ["file"] },
44
+ providers: { merge: "entries", from: ["shared", "file"] },
45
+ extends: { merge: "replace", from: ["file"] },
46
+ };
47
+ // The configuration file's schema without its defaults, so a layer holds
48
+ // only what it sets; unknown keys are refused as in the file.
49
+ export const layerSchema = z
50
+ .object(Object.fromEntries(Object.entries(configSchema.shape).map(([key, schema]) => [
51
+ key,
52
+ (schema instanceof z.ZodDefault ? schema.unwrap() : schema).optional(),
53
+ ])))
54
+ .strict();
55
+ /** The layers, earliest first, over ocra's defaults, and where each setting came from. */
56
+ export function resolveSettings(layers) {
57
+ const merged = { ...configSchema.parse({}), rules: [], ultra: false };
58
+ const sources = {};
59
+ const record = (name, source) => {
60
+ const from = sources[name] ?? [];
61
+ if (!from.includes(source))
62
+ sources[name] = [...from, source];
63
+ };
64
+ for (const { source, settings, under = false } of layers) {
65
+ for (const [key, value] of Object.entries(settings)) {
66
+ if (value === undefined)
67
+ continue;
68
+ const rule = MERGE[key];
69
+ if (!rule.from.includes(source)) {
70
+ throw new OcraError("INTERNAL", `the ${source} settings may not set ${key}`);
71
+ }
72
+ if (rule.merge === "replace") {
73
+ if (under && sources[key])
74
+ continue;
75
+ merged[key] = value;
76
+ sources[key] = [source];
77
+ }
78
+ else if (rule.merge === "entries") {
79
+ const entries = { ...merged[key] };
80
+ for (const [entry, setting] of Object.entries(value)) {
81
+ const name = `${key}.${entry}`;
82
+ if (setting === undefined || (under && sources[name]))
83
+ continue;
84
+ entries[entry] = setting;
85
+ sources[name] = [source];
86
+ }
87
+ merged[key] = entries;
88
+ }
89
+ else {
90
+ const current = merged[key];
91
+ const added = under
92
+ ? value.filter((v) => !current.some((c) => isDeepStrictEqual(c, v)))
93
+ : value;
94
+ if (added.length === 0)
95
+ continue;
96
+ merged[key] = [...current, ...added];
97
+ record(key, source);
98
+ }
99
+ }
100
+ }
101
+ return { settings: merged, sources };
102
+ }
103
+ /**
104
+ * The settings --plan lists, in its order, with their sources. A setting
105
+ * only a command-line flag set is the flag's, not a setting, and is left
106
+ * out.
107
+ */
108
+ export function listSettings(settings, sources) {
109
+ const listed = [];
110
+ for (const [key, rule] of Object.entries(MERGE)) {
111
+ const plan = rule.plan;
112
+ if (!plan)
113
+ continue;
114
+ const value = settings[key];
115
+ const add = (name, shown, always) => {
116
+ const from = (sources[name] ?? []).filter((source) => source !== "flag");
117
+ if (from.length === 0 && !always)
118
+ return;
119
+ listed.push(from.length === 0 ? { key: name, sources: [] } : { key: name, value: shown, sources: from });
120
+ };
121
+ if (rule.merge === "entries") {
122
+ const map = value;
123
+ for (const entry of plan.entries ?? Object.keys(map)) {
124
+ add(`${key}.${entry}`, map[entry], plan.list === "always");
125
+ }
126
+ }
127
+ else {
128
+ add(key, plan.show ? plan.show(value) : value, plan.list === "always");
129
+ }
130
+ }
131
+ return listed;
132
+ }
133
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1,3 @@
1
+ /** ~/.config/ocra ($XDG_CONFIG_HOME/ocra), or %APPDATA%\ocra on Windows: this machine's own. */
2
+ export declare function ocraConfigDir(env: Readonly<Record<string, string | undefined>>): string;
3
+ //# sourceMappingURL=user-dir.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ /** ~/.config/ocra ($XDG_CONFIG_HOME/ocra), or %APPDATA%\ocra on Windows: this machine's own. */
4
+ export function ocraConfigDir(env) {
5
+ const base = process.platform === "win32"
6
+ ? (env.APPDATA ?? join(homedir(), "AppData", "Roaming"))
7
+ : (env.XDG_CONFIG_HOME ?? join(homedir(), ".config"));
8
+ return join(base, "ocra");
9
+ }
10
+ //# sourceMappingURL=user-dir.js.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { run } from "./run.js";
1
+ export declare function run(argv: readonly string[]): Promise<number>;
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,11 @@
1
- export { run } from "./run.js";
1
+ // The public API of @open-cr-agent/cli, a contract under the 0.x rule
2
+ // (manual: Embedding, Stability); etc/cli.api.md records it. What the
3
+ // other workspace packages need beyond this comes from
4
+ // "@open-cr-agent/cli/internal", which is not a contract.
5
+ import { defaultDeps, run as runWith } from "./run.js";
6
+ // The `ocra` command with its arguments (no node or script path), writing
7
+ // to stdout and stderr; resolves to the exit code.
8
+ export function run(argv) {
9
+ return runWith([...argv], process.stdout, process.stderr, defaultDeps());
10
+ }
2
11
  //# sourceMappingURL=index.js.map