@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
|
@@ -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
|
-
|
|
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
|
|
35
|
-
//# sourceMappingURL=remote
|
|
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
|
-
|
|
10
|
+
const remoteConfigSchema = z
|
|
11
11
|
.object({
|
|
12
12
|
$schema: z.string().optional(),
|
|
13
|
-
models: z.record(z.enum(
|
|
14
|
-
effort: z.partialRecord(z.enum(
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
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.
|
|
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
|
-
|
|
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,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
|
|
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
|
-
|
|
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
|