co-maintainer 0.4.13 → 0.5.0-beta.1
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/LICENSE +21 -21
- package/README.md +57 -38
- package/dist/main.js +2 -3
- package/dist/package.json +21 -15
- package/dist/src/ai/batch.d.ts +24 -1
- package/dist/src/ai/batch.js +75 -23
- package/dist/src/ai/estimate.d.ts +40 -0
- package/dist/src/ai/estimate.js +113 -0
- package/dist/src/ai/fake.d.ts +1 -1
- package/dist/src/ai/fake.js +1 -1
- package/dist/src/ai/hetzner.js +1 -1
- package/dist/src/ai/openrouter.d.ts +13 -0
- package/dist/src/ai/openrouter.js +66 -12
- package/dist/src/ai/pricing.d.ts +14 -0
- package/dist/src/ai/pricing.js +77 -0
- package/dist/src/ai/provider.js +8 -0
- package/dist/src/ai/verify.d.ts +25 -0
- package/dist/src/ai/verify.js +78 -0
- package/dist/src/cli/args.js +25 -29
- package/dist/src/cli/commands/config.d.ts +44 -0
- package/dist/src/cli/commands/config.js +220 -0
- package/dist/src/cli/commands/probe.js +70 -103
- package/dist/src/cli/commands/registry.d.ts +57 -0
- package/dist/src/cli/commands/registry.js +713 -0
- package/dist/src/cli/commands/review.js +66 -43
- package/dist/src/cli/commands/serve.d.ts +4 -2
- package/dist/src/cli/commands/serve.js +14 -10
- package/dist/src/cli/commands/set.d.ts +2 -1
- package/dist/src/cli/commands/set.js +63 -17
- package/dist/src/cli/commands/view.d.ts +1 -0
- package/dist/src/cli/commands/view.js +180 -0
- package/dist/src/cli/error.d.ts +39 -0
- package/dist/src/cli/error.js +64 -0
- package/dist/src/cli/main.d.ts +7 -0
- package/dist/src/cli/main.js +82 -1
- package/dist/src/cli/prompt.js +21 -7
- package/dist/src/cli/review_args.d.ts +4 -0
- package/dist/src/cli/review_args.js +22 -9
- package/dist/src/cli/review_output.d.ts +2 -2
- package/dist/src/cli/review_output.js +7 -6
- package/dist/src/cli/review_result.d.ts +59 -8
- package/dist/src/cli/review_result.js +145 -103
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +31 -0
- package/dist/src/github/app.d.ts +13 -0
- package/dist/src/github/app.js +23 -0
- package/dist/src/github/app_manifest.d.ts +50 -0
- package/dist/src/github/app_manifest.js +138 -0
- package/dist/src/github/client.js +7 -1
- package/dist/src/github/collect.js +1 -1
- package/dist/src/github/gh.js +75 -18
- package/dist/src/knowledge/facts.js +8 -3
- package/dist/src/knowledge/guide.d.ts +8 -0
- package/dist/src/knowledge/guide.js +32 -8
- package/dist/src/knowledge/probe.js +13 -3
- package/dist/src/knowledge/sections.d.ts +9 -0
- package/dist/src/knowledge/sections.js +16 -0
- package/dist/src/knowledge/skill.d.ts +5 -0
- package/dist/src/knowledge/skill.js +68 -21
- package/dist/src/knowledge/synthesis.d.ts +17 -1
- package/dist/src/knowledge/synthesis.js +70 -15
- package/dist/src/knowledge/types.d.ts +5 -0
- package/dist/src/local/codegraph_prepare.d.ts +3 -2
- package/dist/src/local/codegraph_prepare.js +4 -2
- package/dist/src/local/git_ops.d.ts +5 -4
- package/dist/src/local/git_ops.js +8 -9
- package/dist/src/local/review_local.js +36 -14
- package/dist/src/pr/checkout.js +14 -3
- package/dist/src/pr/codegraph_tools.js +3 -3
- package/dist/src/pr/diff_summary.js +3 -3
- package/dist/src/pr/findings.js +16 -5
- package/dist/src/pr/findings_json.d.ts +26 -0
- package/dist/src/pr/findings_json.js +187 -0
- package/dist/src/pr/review_copy.d.ts +20 -0
- package/dist/src/pr/review_copy.js +40 -0
- package/dist/src/pr/reviewer.d.ts +2 -1
- package/dist/src/pr/reviewer.js +84 -91
- package/dist/src/remote/client.js +42 -42
- package/dist/src/remote/http.d.ts +5 -0
- package/dist/src/remote/http.js +49 -0
- package/dist/src/remote/server/guides.d.ts +22 -0
- package/dist/src/remote/server/guides.js +81 -0
- package/dist/src/remote/server/routes.js +24 -0
- package/dist/src/review/blocking.d.ts +35 -0
- package/dist/src/review/blocking.js +45 -0
- package/dist/src/review/carry_over.d.ts +12 -1
- package/dist/src/review/carry_over.js +35 -9
- package/dist/src/review/engine.d.ts +1 -0
- package/dist/src/review/guides.js +28 -1
- package/dist/src/server/api/installations.js +1 -1
- package/dist/src/server/api/repos.js +86 -3
- package/dist/src/server/api/settings.js +3 -3
- package/dist/src/server/app.d.ts +3 -0
- package/dist/src/server/app.js +12 -5
- package/dist/src/server/pages/activity.d.ts +1 -0
- package/dist/src/server/pages/activity.js +2 -2
- package/dist/src/server/pages/add_repo.js +73 -5
- package/dist/src/server/pages/client.d.ts +1 -1
- package/dist/src/server/pages/client.js +1 -1
- package/dist/src/server/pages/home.d.ts +6 -1
- package/dist/src/server/pages/home.js +25 -2
- package/dist/src/server/pages/knowledge.js +1 -1
- package/dist/src/server/pages/layout.js +2 -2
- package/dist/src/server/pages/repo.d.ts +1 -1
- package/dist/src/server/pages/repo.js +21 -2
- package/dist/src/server/pages/repo_prs.d.ts +5 -1
- package/dist/src/server/pages/repo_prs.js +36 -1
- package/dist/src/server/pages/repo_remote.js +2 -2
- package/dist/src/server/pages/repo_settings.js +2 -2
- package/dist/src/server/pages/router.d.ts +6 -0
- package/dist/src/server/pages/router.js +129 -9
- package/dist/src/server/pages/settings.d.ts +1 -1
- package/dist/src/server/pages/settings.js +87 -9
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/probe.d.ts +28 -0
- package/dist/src/services/probe.js +133 -0
- package/dist/src/services/remake_cron.d.ts +1 -1
- package/dist/src/services/remake_cron.js +3 -3
- package/dist/src/services/remote_review.js +14 -4
- package/dist/src/services/review.d.ts +11 -0
- package/dist/src/services/review.js +39 -11
- package/dist/src/services/setup.js +37 -19
- package/dist/src/services/setup_checklist.d.ts +10 -0
- package/dist/src/services/setup_checklist.js +87 -0
- package/dist/src/store/app_db.js +5 -2
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +27 -4
- package/dist/src/store/deliveries.d.ts +7 -0
- package/dist/src/store/deliveries.js +15 -0
- package/dist/src/tools/codegraph.d.ts +30 -0
- package/dist/src/tools/codegraph.js +49 -12
- package/dist/src/types.d.ts +10 -0
- package/dist/src/util/log.d.ts +3 -0
- package/dist/src/util/log.js +12 -0
- package/dist/src/util/run_summary.d.ts +33 -0
- package/dist/src/util/run_summary.js +47 -0
- package/dist/src/util/webhook_reachability.d.ts +11 -0
- package/dist/src/util/webhook_reachability.js +64 -0
- package/package.json +21 -15
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/** The `config` command: read and edit the user config without opening the
|
|
2
|
+
* file (CORE-22, F12). `set` is `config set` under its own name, so both share
|
|
3
|
+
* the mapping and masking here.
|
|
4
|
+
*
|
|
5
|
+
* The key list is derived from `UserConfig` at runtime where possible: a
|
|
6
|
+
* kebab-case name maps to the camelCase JSON key, so a new field needs no
|
|
7
|
+
* second place to be registered. `secretFields` decides what is masked. */
|
|
8
|
+
import { configPath, getCacheDir, readConfig, reposDir } from "../../config.js";
|
|
9
|
+
import { getEnv } from "../../util/runtime.js";
|
|
10
|
+
import { die } from "../error.js";
|
|
11
|
+
import { renderCommandHelp } from "./registry.js";
|
|
12
|
+
/** Fields whose value must never be printed in full. */
|
|
13
|
+
export const SECRET_FIELDS = new Set([
|
|
14
|
+
"token",
|
|
15
|
+
"githubPat",
|
|
16
|
+
"githubAppPrivateKey",
|
|
17
|
+
"githubWebhookSecret",
|
|
18
|
+
"githubOAuthClientSecret",
|
|
19
|
+
"remoteToken",
|
|
20
|
+
"dashboardPasswordHash",
|
|
21
|
+
]);
|
|
22
|
+
/** `lowModel` -> `low-model`. The inverse of {@link configKey}. */
|
|
23
|
+
export function flagName(field) {
|
|
24
|
+
return field.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
25
|
+
}
|
|
26
|
+
/** `low-model` -> `lowModel`. */
|
|
27
|
+
export function configKey(name) {
|
|
28
|
+
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
29
|
+
}
|
|
30
|
+
/** Masks a secret for display: 12 characters or more keep the last four so a
|
|
31
|
+
* user can tell two keys apart, anything shorter is fully hidden. `password`
|
|
32
|
+
* is handled by the caller because it is a hash, not a key. */
|
|
33
|
+
export function maskSecret(value) {
|
|
34
|
+
if (value.length >= 12)
|
|
35
|
+
return `••••${value.slice(-4)}`;
|
|
36
|
+
return "••••";
|
|
37
|
+
}
|
|
38
|
+
/** The env var that supplies each field, mirroring args.ts. Only the fields
|
|
39
|
+
* that actually read env are listed. */
|
|
40
|
+
export const ENV_BY_FIELD = {
|
|
41
|
+
token: ["CO_MAINTAINER_TOKEN", "OPENROUTER_API_KEY", "HETZNER_API_KEY"],
|
|
42
|
+
githubPat: ["GITHUB_TOKEN", "GH_TOKEN"],
|
|
43
|
+
ai: ["CO_MAINTAINER_AI"],
|
|
44
|
+
auth: ["CO_MAINTAINER_AUTH"],
|
|
45
|
+
lowModel: ["OPENROUTER_LOW_MODEL", "HETZNER_LOW_MODEL", "LOW_MODEL"],
|
|
46
|
+
highModel: ["OPENROUTER_HIGH_MODEL", "HETZNER_HIGH_MODEL", "HIGH_MODEL"],
|
|
47
|
+
webhookUrl: ["CM_WEBHOOK_URL"],
|
|
48
|
+
reviewBlocking: ["CO_MAINTAINER_REVIEW_BLOCKING"],
|
|
49
|
+
};
|
|
50
|
+
/** Renders one field for people. A secret is masked; the password hash, which
|
|
51
|
+
* nobody can use, only reports whether it is set. */
|
|
52
|
+
export function displayValue(field, value) {
|
|
53
|
+
if (field === "dashboardPasswordHash") {
|
|
54
|
+
return value === undefined ? "not set" : "set";
|
|
55
|
+
}
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
return "";
|
|
58
|
+
if (SECRET_FIELDS.has(field))
|
|
59
|
+
return maskSecret(String(value));
|
|
60
|
+
if (Array.isArray(value))
|
|
61
|
+
return value.join(",");
|
|
62
|
+
if (typeof value === "object" && value !== null)
|
|
63
|
+
return JSON.stringify(value);
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
/** Values a field falls back to when neither env nor the file sets it, so
|
|
67
|
+
* `config list` can say a row is the default rather than leaving it out. */
|
|
68
|
+
export const DEFAULTS = {
|
|
69
|
+
reviewBlocking: "model",
|
|
70
|
+
};
|
|
71
|
+
/** Every top-level UserConfig key with a value and where it came from. */
|
|
72
|
+
export function configRows(config) {
|
|
73
|
+
const rows = [];
|
|
74
|
+
for (const field of KNOWN_FIELDS) {
|
|
75
|
+
const fromEnv = (ENV_BY_FIELD[field] ?? []).find((name) => getEnv(name) !== undefined);
|
|
76
|
+
const fileValue = config[field];
|
|
77
|
+
const value = fromEnv ? getEnv(fromEnv) : fileValue;
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
const fallback = DEFAULTS[field];
|
|
80
|
+
if (fallback === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
rows.push({
|
|
83
|
+
key: flagName(field),
|
|
84
|
+
field,
|
|
85
|
+
value: fallback,
|
|
86
|
+
source: "default",
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
rows.push({
|
|
91
|
+
key: flagName(field),
|
|
92
|
+
field,
|
|
93
|
+
value: displayValue(field, value),
|
|
94
|
+
source: fromEnv ? "env" : "file",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (config.repos && Object.keys(config.repos).length > 0) {
|
|
98
|
+
rows.push({
|
|
99
|
+
key: "repos",
|
|
100
|
+
field: "repos",
|
|
101
|
+
value: `${Object.keys(config.repos).length} repo(s)`,
|
|
102
|
+
source: "file",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return rows.sort((a, b) => a.key.localeCompare(b.key));
|
|
106
|
+
}
|
|
107
|
+
function printList(config) {
|
|
108
|
+
const rows = configRows(config);
|
|
109
|
+
const width = Math.max(...rows.map((row) => row.key.length));
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
console.log(`${row.key.padEnd(width)} ${row.value.padEnd(24)}${row.source}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Resolves a user-supplied key name to its field. Accepts kebab-case
|
|
115
|
+
* (`low-model`), camelCase (`lowModel`), and rejects anything that is not a
|
|
116
|
+
* `UserConfig` key, so a typo cannot silently read undefined. */
|
|
117
|
+
export function resolveField(name) {
|
|
118
|
+
const field = configKey(name);
|
|
119
|
+
const match = KNOWN_FIELDS.find((known) => known === field || known === name);
|
|
120
|
+
if (!match)
|
|
121
|
+
die(`Unknown config key: ${name}`);
|
|
122
|
+
return match;
|
|
123
|
+
}
|
|
124
|
+
/** The scalar `UserConfig` keys `config` can read and write. `repos` and
|
|
125
|
+
* `defaults` are nested maps, not settings, so they are not listed.
|
|
126
|
+
* `satisfies` makes this the type's own vocabulary: a name that is not a
|
|
127
|
+
* `UserConfig` key is a compile error rather than a silently undefined read. */
|
|
128
|
+
export const KNOWN_FIELDS = [
|
|
129
|
+
"auth",
|
|
130
|
+
"ai",
|
|
131
|
+
"lowModel",
|
|
132
|
+
"highModel",
|
|
133
|
+
"token",
|
|
134
|
+
"githubPat",
|
|
135
|
+
"githubAppId",
|
|
136
|
+
"githubAppPrivateKey",
|
|
137
|
+
"githubAppPrivateKeyPath",
|
|
138
|
+
"githubWebhookSecret",
|
|
139
|
+
"webhookUrl",
|
|
140
|
+
"githubOAuthClientId",
|
|
141
|
+
"githubOAuthClientSecret",
|
|
142
|
+
"githubOAuthAllowedUser",
|
|
143
|
+
"passwordAuthDisabled",
|
|
144
|
+
"githubAuthEnabled",
|
|
145
|
+
"dashboardPasswordHash",
|
|
146
|
+
"maxConcurrentJobs",
|
|
147
|
+
"reviewBlocking",
|
|
148
|
+
"remoteHost",
|
|
149
|
+
"remoteToken",
|
|
150
|
+
"remoteNoticeShownFor",
|
|
151
|
+
"remoteSyncTimeoutSeconds",
|
|
152
|
+
"maxConcurrentRemoteReviewsPerToken",
|
|
153
|
+
"remoteToolOutputMaxChars",
|
|
154
|
+
];
|
|
155
|
+
/** `co-maintainer config <sub> ...`. `set` is handled by runSet, which this
|
|
156
|
+
* command delegates to so there is one implementation of the write rules. */
|
|
157
|
+
export async function runConfig(args) {
|
|
158
|
+
const [sub, ...rest] = args;
|
|
159
|
+
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
|
160
|
+
console.log(renderCommandHelp("config") ??
|
|
161
|
+
"Usage: co-maintainer config <list|get|set|unset|path>");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const config = readConfig();
|
|
165
|
+
if (sub === "list") {
|
|
166
|
+
printList(config);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (sub === "path") {
|
|
170
|
+
console.log(configPath());
|
|
171
|
+
if (rest.includes("--all")) {
|
|
172
|
+
console.log(reposDir());
|
|
173
|
+
console.log(`${getCacheDir()}/co-maintainer`);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (sub === "get") {
|
|
178
|
+
const name = rest.find((arg) => !arg.startsWith("-"));
|
|
179
|
+
if (!name)
|
|
180
|
+
die("Usage: co-maintainer config get <key>");
|
|
181
|
+
const field = resolveField(name);
|
|
182
|
+
const fromEnv = (ENV_BY_FIELD[field] ?? []).find((envName) => getEnv(envName) !== undefined);
|
|
183
|
+
const effective = fromEnv
|
|
184
|
+
? getEnv(fromEnv)
|
|
185
|
+
: (config[field] ?? DEFAULTS[field]);
|
|
186
|
+
if (effective === undefined) {
|
|
187
|
+
die(`No value for ${name}.`);
|
|
188
|
+
}
|
|
189
|
+
// A machine reading this wants the real value, not a mask, which is why
|
|
190
|
+
// `get` prints it plainly and `list` is what a screen-share shows.
|
|
191
|
+
console.log(String(displayValue(field, effective)));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (sub === "unset") {
|
|
195
|
+
const name = rest.find((arg) => !arg.startsWith("-"));
|
|
196
|
+
if (!name)
|
|
197
|
+
die("Usage: co-maintainer config unset <key>");
|
|
198
|
+
const field = resolveField(name);
|
|
199
|
+
const { runSet } = await import("./set.js");
|
|
200
|
+
await runSet([`--unset=${field}`, "--no-verify"]);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (sub === "set") {
|
|
204
|
+
const { runSet } = await import("./set.js");
|
|
205
|
+
// `config set key value` and `config set --key=value` both work. The
|
|
206
|
+
// verification in runSet is left on: a key or model typed through this
|
|
207
|
+
// path deserves the same check as one typed through `set`.
|
|
208
|
+
const positional = rest.filter((arg) => !arg.startsWith("-"));
|
|
209
|
+
if (positional.length >= 2 && !rest[0]?.startsWith("-")) {
|
|
210
|
+
const [name, ...valueParts] = positional;
|
|
211
|
+
const field = resolveField(name);
|
|
212
|
+
const value = valueParts.join(" ");
|
|
213
|
+
await runSet([`--${flagName(field)}=${value}`]);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await runSet(rest);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
die(`Unknown config subcommand: ${sub}`);
|
|
220
|
+
}
|
|
@@ -1,112 +1,22 @@
|
|
|
1
|
-
import { clientFor } from "../../services/setup.js";
|
|
2
|
-
import {
|
|
1
|
+
import { clientFor, runInitOrRemake } from "../../services/setup.js";
|
|
2
|
+
import { probePlan } from "../../services/probe.js";
|
|
3
|
+
import { log, startHeartbeat } from "../../util/log.js";
|
|
4
|
+
import { estimateInit, readJobHistory } from "../../ai/estimate.js";
|
|
5
|
+
import { loadPrices } from "../../ai/pricing.js";
|
|
3
6
|
import { cacheSet } from "../../store/cache_db.js";
|
|
4
|
-
import { log, startHeartbeat, timed } from "../../util/log.js";
|
|
5
7
|
export async function runProbe(options) {
|
|
6
8
|
const operationStarted = performance.now();
|
|
7
9
|
const stopHeartbeat = startHeartbeat("probing repository");
|
|
8
10
|
const client = clientFor(options);
|
|
9
11
|
log("probe", `reading ${options.repo} via ${options.auth}`);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const releases = await timed("probe release metadata", options.logTime, () => client.pages(`repos/${options.repo}/releases?per_page=1`, 1));
|
|
15
|
-
latestReleaseAt = String(releases[0]?.published_at ?? releases[0]?.created_at ?? "");
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// Release metadata is an optional probe signal.
|
|
19
|
-
}
|
|
20
|
-
log("probe", "reading pull request list");
|
|
21
|
-
const pulls = await timed("probe pull request listing", options.logTime, () => client.pages(`repos/${options.repo}/pulls?state=all&sort=updated&direction=desc`, undefined, (page, fetched) => log("probe", `pull request listing · page ${page} · fetched ${fetched} · total unknown`)));
|
|
22
|
-
const yearBuckets = new Map();
|
|
23
|
-
for (const pull of pulls) {
|
|
24
|
-
const year = new Date(String(pull.updated_at)).getFullYear();
|
|
25
|
-
yearBuckets.set(year, [...(yearBuckets.get(year) ?? []), pull]);
|
|
26
|
-
}
|
|
27
|
-
const sampleTargets = new Map();
|
|
28
|
-
for (const pull of pulls.slice(0, 30)) {
|
|
29
|
-
sampleTargets.set(Number(pull.number), pull);
|
|
30
|
-
}
|
|
31
|
-
for (const yearPulls of yearBuckets.values()) {
|
|
32
|
-
for (const pull of yearPulls.slice(0, 3)) {
|
|
33
|
-
sampleTargets.set(Number(pull.number), pull);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const samplePulls = [...sampleTargets.values()];
|
|
37
|
-
const detailSamples = [];
|
|
38
|
-
log("probe", `sampling ${samplePulls.length} pull request details · concurrency=${options.ghConcurrent}`);
|
|
39
|
-
await timed("probe PR detail sampling", options.logTime, async () => {
|
|
40
|
-
const details = new Array(samplePulls.length);
|
|
41
|
-
let cursor = 0;
|
|
42
|
-
let completed = 0;
|
|
43
|
-
const worker = async () => {
|
|
44
|
-
while (cursor < samplePulls.length) {
|
|
45
|
-
const index = cursor++;
|
|
46
|
-
const pull = samplePulls[index];
|
|
47
|
-
try {
|
|
48
|
-
details[index] = await client.request(`repos/${options.repo}/pulls/${Number(pull.number)}`);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// A missing detail should not invalidate the rest of the probe.
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
completed++;
|
|
55
|
-
log("probe", `PR detail sampling · ${completed}/${samplePulls.length} · ${Math.round((completed / samplePulls.length) * 100)}%`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
await Promise.all(Array.from({ length: Math.min(options.ghConcurrent, samplePulls.length) }, worker));
|
|
60
|
-
detailSamples.push(...details.filter((detail) => !!detail));
|
|
12
|
+
const plan = await probePlan(client, options.repo, {
|
|
13
|
+
ghConcurrent: options.ghConcurrent,
|
|
14
|
+
logTime: options.logTime,
|
|
15
|
+
log: (message) => log("probe", message),
|
|
61
16
|
});
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const analysis = await timed("probe analysis", options.logTime, async () => analyzeProbe({ ...meta, latest_release_at: latestReleaseAt }, pulls, detailSamples, commits));
|
|
66
|
-
const recommendation = ["--include-codebase"];
|
|
67
|
-
if (analysis.includePullRequests) {
|
|
68
|
-
recommendation.push("--include-pull-requests");
|
|
69
|
-
}
|
|
70
|
-
if (analysis.includePullRequestChanges) {
|
|
71
|
-
recommendation.push("--include-pull-request-changes");
|
|
72
|
-
}
|
|
73
|
-
if (analysis.includeCommitHistory) {
|
|
74
|
-
recommendation.push("--include-commit-history");
|
|
75
|
-
}
|
|
76
|
-
if (pulls.length || Boolean(meta.has_issues)) {
|
|
77
|
-
recommendation.push("--include-how-repo-works");
|
|
78
|
-
}
|
|
79
|
-
if (analysis.maxPullRequestChangeLines) {
|
|
80
|
-
recommendation.push(`--max-pull-request-change-lines=${analysis.maxPullRequestChangeLines}`);
|
|
81
|
-
}
|
|
82
|
-
if (analysis.maxPrMonths) {
|
|
83
|
-
recommendation.push(`--max-pr-months=${analysis.maxPrMonths}`);
|
|
84
|
-
}
|
|
85
|
-
if (analysis.maxCommits) {
|
|
86
|
-
recommendation.push(`--max-commits=${analysis.maxCommits}`);
|
|
87
|
-
}
|
|
88
|
-
const command = [
|
|
89
|
-
"co-maintainer",
|
|
90
|
-
"init",
|
|
91
|
-
options.repo,
|
|
92
|
-
...recommendation,
|
|
93
|
-
].join(" ");
|
|
94
|
-
const report = {
|
|
95
|
-
repo: options.repo,
|
|
96
|
-
...analysis.report,
|
|
97
|
-
recommendations: {
|
|
98
|
-
includePullRequests: analysis.includePullRequests,
|
|
99
|
-
includePullRequestChanges: analysis.includePullRequestChanges,
|
|
100
|
-
includeCommitHistory: analysis.includeCommitHistory,
|
|
101
|
-
maxPrMonths: analysis.maxPrMonths ?? "all",
|
|
102
|
-
maxCommits: analysis.maxCommits ?? "all",
|
|
103
|
-
maxPullRequestChangeLines: analysis.maxPullRequestChangeLines ?? "all",
|
|
104
|
-
},
|
|
105
|
-
reasons: analysis.reasons,
|
|
106
|
-
recommendedCommand: command,
|
|
107
|
-
createdAt: new Date().toISOString(),
|
|
108
|
-
};
|
|
109
|
-
await timed("probe report write", options.logTime, () => cacheSet("probe", options.repo, JSON.stringify(report)));
|
|
17
|
+
const analysis = plan.analysis;
|
|
18
|
+
const command = plan.command;
|
|
19
|
+
await cacheSet("probe", options.repo, JSON.stringify(plan.report));
|
|
110
20
|
console.log(`\nrecommended\n ${command}`);
|
|
111
21
|
console.log("\nresearch");
|
|
112
22
|
console.log(` PRs: ${analysis.report.pullRequests} · sampled: ${analysis.report.sampledPullRequests}`);
|
|
@@ -114,9 +24,66 @@ export async function runProbe(options) {
|
|
|
114
24
|
console.log(` windows: PR months=${analysis.maxPrMonths ?? "all"} · commits=${analysis.maxCommits ?? "all"} · diff lines=${analysis.maxPullRequestChangeLines ?? "all"}`);
|
|
115
25
|
for (const reason of analysis.reasons)
|
|
116
26
|
console.log(` - ${reason}`);
|
|
117
|
-
|
|
27
|
+
// The estimate is separate from the recommendation: the flags say what to
|
|
28
|
+
// read, this says what that costs in time and dollars (CORE-24). Prices are
|
|
29
|
+
// only fetched when an OpenRouter model would actually be billed, so a
|
|
30
|
+
// plain `--ai=none` probe makes no network call.
|
|
31
|
+
const needsPrices = options.ai === "openrouter" &&
|
|
32
|
+
Boolean(options.lowModel && options.highModel);
|
|
33
|
+
const [prices, history] = await Promise.all([
|
|
34
|
+
needsPrices
|
|
35
|
+
? loadPrices()
|
|
36
|
+
: Promise.resolve({ prices: new Map(), source: "unavailable" }),
|
|
37
|
+
readJobHistory(options.repo),
|
|
38
|
+
]);
|
|
39
|
+
const estimate = estimateInit({
|
|
40
|
+
pullRequests: analysis.includePullRequests
|
|
41
|
+
? Number(analysis.report.pullRequests ?? 0)
|
|
42
|
+
: 0,
|
|
43
|
+
includeCodebase: analysis.includePullRequests,
|
|
44
|
+
lowModel: options.lowModel,
|
|
45
|
+
highModel: options.highModel,
|
|
46
|
+
prices: prices.prices,
|
|
47
|
+
history,
|
|
48
|
+
});
|
|
49
|
+
console.log("\nestimate");
|
|
50
|
+
console.log(` AI jobs: ${estimate.extract} extract + ${estimate.synth} synth`);
|
|
51
|
+
console.log(` tokens: ${range(estimate.tokensIn)} in · ${range(estimate.tokensOut)} out`);
|
|
52
|
+
console.log(` time: ${secondsRange(estimate.seconds)}s`);
|
|
53
|
+
if (estimate.usd) {
|
|
54
|
+
console.log(` cost: $${estimate.usd[0].toFixed(4)}-$${estimate.usd[1].toFixed(4)}`);
|
|
55
|
+
}
|
|
56
|
+
else if (needsPrices && prices.source === "unavailable") {
|
|
57
|
+
console.log(" cost: unknown (could not read OpenRouter prices)");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(" cost: set --ai=openrouter with both models to estimate dollars");
|
|
61
|
+
}
|
|
62
|
+
console.log(` basis: ${estimate.basis === "history"
|
|
63
|
+
? "this repository's recorded jobs"
|
|
64
|
+
: "the cm-dx-lab calibration"}; an estimate, not a bill`);
|
|
65
|
+
console.log("\nnext");
|
|
66
|
+
console.log(` ${command}`);
|
|
67
|
+
console.log(` or run it now: co-maintainer probe ${options.repo} --run`);
|
|
68
|
+
console.log(" Probe only reads. It does not write guides.");
|
|
118
69
|
if (options.logTime) {
|
|
119
70
|
console.log(` total time: ${((performance.now() - operationStarted) / 1000).toFixed(2)}s`);
|
|
120
71
|
}
|
|
121
72
|
stopHeartbeat();
|
|
73
|
+
if (options.run) {
|
|
74
|
+
// Same process, no re-spawn: the dashboard learned that lesson already
|
|
75
|
+
// (the second path had its own failure modes). `init` inherits the
|
|
76
|
+
// recommended flags, which is exactly the command printed above.
|
|
77
|
+
options.command = "init";
|
|
78
|
+
await runInitOrRemake(options);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** `120-190` for a token range, rounded to whole tokens. */
|
|
82
|
+
function range([low, high]) {
|
|
83
|
+
const round = (value) => Math.round(value).toLocaleString("en-US");
|
|
84
|
+
return `${round(low)}-${round(high)}`;
|
|
85
|
+
}
|
|
86
|
+
/** `8-14` for a seconds range, rounded to whole seconds. */
|
|
87
|
+
function secondsRange([low, high]) {
|
|
88
|
+
return `${Math.round(low)}-${Math.round(high)}`;
|
|
122
89
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** The command registry (CORE-20).
|
|
2
|
+
*
|
|
3
|
+
* One place that knows every command, its aliases, its usage lines, its flags
|
|
4
|
+
* and its examples. Everything the user reads as help is rendered from here,
|
|
5
|
+
* so a flag cannot drift out of the help and a command cannot be documented
|
|
6
|
+
* twice with different wording. It is also the source CORE-81 will export to
|
|
7
|
+
* the command reference, which is why it is data rather than template strings.
|
|
8
|
+
*/
|
|
9
|
+
export type FlagSpec = {
|
|
10
|
+
/** Without the leading dashes, e.g. `include-codebase`. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The value placeholder when the flag takes one, e.g. `N` in `--port=N`. */
|
|
13
|
+
value?: string;
|
|
14
|
+
default?: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
export type FlagGroup = {
|
|
18
|
+
title: string;
|
|
19
|
+
flags: FlagSpec[];
|
|
20
|
+
};
|
|
21
|
+
export type CommandSpec = {
|
|
22
|
+
name: string;
|
|
23
|
+
/** Other spellings that run the same handler. Hidden aliases stay out of the
|
|
24
|
+
* global help but still resolve. */
|
|
25
|
+
aliases?: string[];
|
|
26
|
+
/** A hidden command is resolvable but never listed. */
|
|
27
|
+
hidden?: boolean;
|
|
28
|
+
summary: string;
|
|
29
|
+
usage: string[];
|
|
30
|
+
groups: FlagGroup[];
|
|
31
|
+
examples: string[];
|
|
32
|
+
docs?: string;
|
|
33
|
+
};
|
|
34
|
+
export declare const COMMANDS: CommandSpec[];
|
|
35
|
+
/** Every name a command answers to, including hidden aliases. */
|
|
36
|
+
export declare function allNames(): string[];
|
|
37
|
+
export declare function findCommand(name: string): CommandSpec | undefined;
|
|
38
|
+
/** Commands listed in the global help. A hidden alias or command is resolvable
|
|
39
|
+
* but invisible. */
|
|
40
|
+
export declare function visibleCommands(): CommandSpec[];
|
|
41
|
+
/** Levenshtein distance, iterative and allocation-light: the help path runs it
|
|
42
|
+
* on every unknown token, and the strings are short. */
|
|
43
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
44
|
+
/** The closest candidate within `maxDistance`, or undefined. Ties keep the
|
|
45
|
+
* first candidate, which is the declaration order in `COMMANDS`. */
|
|
46
|
+
export declare function closest(input: string, candidates: string[], maxDistance?: number): string | undefined;
|
|
47
|
+
/** `prob` becomes `Unknown command: prob. Did you mean probe?`. */
|
|
48
|
+
export declare function unknownCommandMessage(input: string): string;
|
|
49
|
+
/** `--jsno` becomes `Unknown option: --jsno. Did you mean --json?`. */
|
|
50
|
+
export declare function unknownOptionMessage(input: string): string;
|
|
51
|
+
/** The global help: every visible command with its one-line summary. */
|
|
52
|
+
export declare function renderGlobalHelp(): string;
|
|
53
|
+
/** One command's help: usage, flags grouped, examples, docs link. */
|
|
54
|
+
export declare function renderCommandHelp(name: string): string | undefined;
|
|
55
|
+
/** The registry as markdown, so CORE-81 can build the command reference from
|
|
56
|
+
* the same data the runtime help uses. */
|
|
57
|
+
export declare function registryToMarkdown(): string;
|