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
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import { chatBody, parseChatResponse } from "./provider.js";
|
|
2
|
+
import { getEnv } from "../util/runtime.js";
|
|
3
|
+
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
4
|
+
/** Provider failures as actionable text (CORE-12, F24/F28). A 401 and a 400 for
|
|
5
|
+
* an unknown model are preconditions the user can fix, so they are usage
|
|
6
|
+
* errors; anything else is a runtime failure. Only the provider's own
|
|
7
|
+
* `error.message` is surfaced: the raw body can carry account metadata
|
|
8
|
+
* (`user_id`) that has no business in a terminal. */
|
|
9
|
+
export function openRouterError(model, status, body) {
|
|
10
|
+
let detail = "";
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(body);
|
|
13
|
+
detail = String(parsed.error?.message ?? "");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
detail = body.trim().slice(0, 200);
|
|
17
|
+
}
|
|
18
|
+
if (status === 401 || status === 403) {
|
|
19
|
+
return new CliError("openrouter_unauthorized", "OpenRouter rejected the API key.", "Set a new one with co-maintainer set --token=...", EXIT_USAGE);
|
|
20
|
+
}
|
|
21
|
+
if (status === 400 && /model/i.test(detail)) {
|
|
22
|
+
return new CliError("openrouter_unknown_model", `OpenRouter does not know the model ${model}.`, `Pick one at https://openrouter.ai/models and set it with co-maintainer set --high-model=...`, EXIT_USAGE);
|
|
23
|
+
}
|
|
24
|
+
return new CliError("openrouter_failed", `OpenRouter failed with ${status}${detail ? `: ${detail}` : "."}`, undefined, EXIT_RUNTIME);
|
|
25
|
+
}
|
|
2
26
|
export class OpenRouterProvider {
|
|
3
27
|
supportsTools = true;
|
|
4
|
-
|
|
28
|
+
/** `CM_OPENROUTER_URL` points the provider at a local fake in tests. It is
|
|
29
|
+
* only a default override: with the env unset the real endpoint is used, so
|
|
30
|
+
* nothing changes for users. */
|
|
31
|
+
endpoint = getEnv("CM_OPENROUTER_URL") ??
|
|
32
|
+
"https://openrouter.ai/api/v1/chat/completions";
|
|
5
33
|
apiKey;
|
|
6
34
|
model;
|
|
7
35
|
constructor(apiKey, model) {
|
|
@@ -10,19 +38,45 @@ export class OpenRouterProvider {
|
|
|
10
38
|
this.apiKey = apiKey;
|
|
11
39
|
this.model = model;
|
|
12
40
|
}
|
|
41
|
+
/** One POST. Returns the raw response so `complete` can decide whether the
|
|
42
|
+
* failure is worth a retry without the schema. */
|
|
43
|
+
async post(body) {
|
|
44
|
+
try {
|
|
45
|
+
return await fetch(this.endpoint, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"HTTP-Referer": "https://github.com/GroophyLifefor/co-maintainer",
|
|
51
|
+
"X-Title": "co-maintainer",
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
// `fetch failed` alone names neither the host nor the cause (CORE-12).
|
|
58
|
+
throw networkFailure(this.endpoint, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
13
61
|
async complete(request) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
62
|
+
let response = await this.post(chatBody(this.model, request));
|
|
63
|
+
// A provider may refuse `response_format` next to `tools`. The schema is
|
|
64
|
+
// only a hint — the prompt also asks for a fenced JSON block — so one
|
|
65
|
+
// retry without it is better than failing the review (CORE-40).
|
|
66
|
+
if (response.status === 400 && request.responseFormat) {
|
|
67
|
+
const detail = await response.text();
|
|
68
|
+
if (/response_format|json_schema|structured/i.test(detail)) {
|
|
69
|
+
const { responseFormat: _dropped, ...withoutSchema } = request;
|
|
70
|
+
response = await this.post(chatBody(this.model, withoutSchema));
|
|
71
|
+
if (response.ok) {
|
|
72
|
+
return parseChatResponse((await response.json()), "openrouter", this.model);
|
|
73
|
+
}
|
|
74
|
+
throw openRouterError(this.model, response.status, await response.text());
|
|
75
|
+
}
|
|
76
|
+
throw openRouterError(this.model, response.status, detail);
|
|
77
|
+
}
|
|
24
78
|
if (!response.ok) {
|
|
25
|
-
throw
|
|
79
|
+
throw openRouterError(this.model, response.status, await response.text());
|
|
26
80
|
}
|
|
27
81
|
return parseChatResponse((await response.json()), "openrouter", this.model);
|
|
28
82
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** USD per one million tokens. OpenRouter reports USD per single token. */
|
|
2
|
+
export type ModelPrice = {
|
|
3
|
+
usdPerMillionIn: number;
|
|
4
|
+
usdPerMillionOut: number;
|
|
5
|
+
};
|
|
6
|
+
export type PriceTable = {
|
|
7
|
+
/** Model id -> price. A model OpenRouter does not list is simply absent. */
|
|
8
|
+
prices: Map<string, ModelPrice>;
|
|
9
|
+
/** Where the table came from, so the estimate can be honest about it. */
|
|
10
|
+
source: "network" | "cache" | "unavailable";
|
|
11
|
+
};
|
|
12
|
+
/** Fetches or reads the cached OpenRouter price list. Never throws: a failure
|
|
13
|
+
* becomes `source: "unavailable"` so the caller degrades instead of failing. */
|
|
14
|
+
export declare function loadPrices(): Promise<PriceTable>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** OpenRouter model prices for the probe estimate (CORE-24).
|
|
2
|
+
*
|
|
3
|
+
* `/api/v1/models` is public: no key needed. The list is large and changes
|
|
4
|
+
* slowly, so it is cached for 24 hours in cache.db. A network failure is not
|
|
5
|
+
* fatal — the estimate then reports tokens and time but no dollars, which is
|
|
6
|
+
* still useful and never invents a price. */
|
|
7
|
+
import { openRouterBase } from "./verify.js";
|
|
8
|
+
import { cacheGet, cacheSet } from "../store/cache_db.js";
|
|
9
|
+
const CACHE_NAMESPACE = "pricing";
|
|
10
|
+
const CACHE_KEY = "openrouter/models";
|
|
11
|
+
const TTL_MS = 24 * 60 * 60 * 1_000;
|
|
12
|
+
function toPrice(row) {
|
|
13
|
+
const pricing = row
|
|
14
|
+
?.pricing;
|
|
15
|
+
if (!pricing)
|
|
16
|
+
return undefined;
|
|
17
|
+
const inUsd = Number(pricing.prompt);
|
|
18
|
+
const outUsd = Number(pricing.completion);
|
|
19
|
+
if (!Number.isFinite(inUsd) || !Number.isFinite(outUsd))
|
|
20
|
+
return undefined;
|
|
21
|
+
return {
|
|
22
|
+
usdPerMillionIn: inUsd * 1_000_000,
|
|
23
|
+
usdPerMillionOut: outUsd * 1_000_000,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Fetches or reads the cached OpenRouter price list. Never throws: a failure
|
|
27
|
+
* becomes `source: "unavailable"` so the caller degrades instead of failing. */
|
|
28
|
+
export async function loadPrices() {
|
|
29
|
+
let cached;
|
|
30
|
+
try {
|
|
31
|
+
cached = await cacheGet(CACHE_NAMESPACE, CACHE_KEY);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
cached = undefined;
|
|
35
|
+
}
|
|
36
|
+
if (cached) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(cached);
|
|
39
|
+
const age = Date.now() - new Date(parsed.at).getTime();
|
|
40
|
+
if (age < TTL_MS) {
|
|
41
|
+
return { prices: buildMap(parsed.rows), source: "cache" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// A corrupt cache entry falls through to the network.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(`${openRouterBase()}/models`);
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
return { prices: new Map(), source: "unavailable" };
|
|
52
|
+
const body = (await response.json());
|
|
53
|
+
const rows = Array.isArray(body.data) ? body.data : [];
|
|
54
|
+
try {
|
|
55
|
+
await cacheSet(CACHE_NAMESPACE, CACHE_KEY, JSON.stringify({ at: new Date().toISOString(), rows }));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A cache write failure must not lose the freshly fetched prices.
|
|
59
|
+
}
|
|
60
|
+
return { prices: buildMap(rows), source: "network" };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return { prices: new Map(), source: "unavailable" };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function buildMap(rows) {
|
|
67
|
+
const prices = new Map();
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const id = row?.id;
|
|
70
|
+
if (typeof id !== "string")
|
|
71
|
+
continue;
|
|
72
|
+
const price = toPrice(row);
|
|
73
|
+
if (price)
|
|
74
|
+
prices.set(id, price);
|
|
75
|
+
}
|
|
76
|
+
return prices;
|
|
77
|
+
}
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -59,5 +59,13 @@ export function chatBody(model, request) {
|
|
|
59
59
|
...(request.tools?.length
|
|
60
60
|
? { tools: request.tools, tool_choice: "auto" }
|
|
61
61
|
: {}),
|
|
62
|
+
// A review asks for structured output (CORE-40). The schema is sent even
|
|
63
|
+
// when tools are present, because most providers accept the pair; one that
|
|
64
|
+
// rejects it gets a single retry without the schema (see
|
|
65
|
+
// `OpenRouterProvider.complete`), and the prompt also asks for a fenced
|
|
66
|
+
// JSON block, so a plain-text provider still works.
|
|
67
|
+
...(request.responseFormat
|
|
68
|
+
? { response_format: request.responseFormat }
|
|
69
|
+
: {}),
|
|
62
70
|
};
|
|
63
71
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Pre-flight checks for `config set` (CORE-22, F35).
|
|
2
|
+
*
|
|
3
|
+
* A wrong API key or an unknown model used to be accepted by `set` and only
|
|
4
|
+
* surfaced during a review, after the pull request had been fetched and
|
|
5
|
+
* cloned. These call OpenRouter's own `/key` and `/models` endpoints so the
|
|
6
|
+
* mistake is caught the moment it is typed. A network failure is not the
|
|
7
|
+
* user's mistake, so it warns and saves instead of refusing. */
|
|
8
|
+
import { CliError } from "../cli/error.ts";
|
|
9
|
+
/** The chat endpoint the provider uses, with `/chat/completions` removed.
|
|
10
|
+
* `CM_OPENROUTER_URL` points at a local fake in tests, so the base follows it
|
|
11
|
+
* and the verification hits the same server the review would. */
|
|
12
|
+
export declare function openRouterBase(): string;
|
|
13
|
+
export type VerifyResult = {
|
|
14
|
+
status: "ok";
|
|
15
|
+
} | {
|
|
16
|
+
status: "unreachable";
|
|
17
|
+
reason: string;
|
|
18
|
+
} | {
|
|
19
|
+
status: "rejected";
|
|
20
|
+
error: CliError;
|
|
21
|
+
};
|
|
22
|
+
/** Checks the key against `/key` and, when `model` is given, that `/models`
|
|
23
|
+
* lists it. The first failure decides: a bad key is reported before a model
|
|
24
|
+
* question, since the model check would fail on the key too. */
|
|
25
|
+
export declare function verifyOpenRouter(apiKey: string, model?: string): Promise<VerifyResult>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Pre-flight checks for `config set` (CORE-22, F35).
|
|
2
|
+
*
|
|
3
|
+
* A wrong API key or an unknown model used to be accepted by `set` and only
|
|
4
|
+
* surfaced during a review, after the pull request had been fetched and
|
|
5
|
+
* cloned. These call OpenRouter's own `/key` and `/models` endpoints so the
|
|
6
|
+
* mistake is caught the moment it is typed. A network failure is not the
|
|
7
|
+
* user's mistake, so it warns and saves instead of refusing. */
|
|
8
|
+
import { CliError, EXIT_USAGE } from "../cli/error.js";
|
|
9
|
+
import { getEnv } from "../util/runtime.js";
|
|
10
|
+
/** The chat endpoint the provider uses, with `/chat/completions` removed.
|
|
11
|
+
* `CM_OPENROUTER_URL` points at a local fake in tests, so the base follows it
|
|
12
|
+
* and the verification hits the same server the review would. */
|
|
13
|
+
export function openRouterBase() {
|
|
14
|
+
const endpoint = getEnv("CM_OPENROUTER_URL") ??
|
|
15
|
+
"https://openrouter.ai/api/v1/chat/completions";
|
|
16
|
+
return endpoint.replace(/\/chat\/completions\/?$/, "");
|
|
17
|
+
}
|
|
18
|
+
async function getJson(url, apiKey) {
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(url, {
|
|
22
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const code = error.cause?.code;
|
|
27
|
+
return { unreachable: code ?? "network error" };
|
|
28
|
+
}
|
|
29
|
+
let body = undefined;
|
|
30
|
+
try {
|
|
31
|
+
body = await response.json();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
body = undefined;
|
|
35
|
+
}
|
|
36
|
+
return { status: response.status, body };
|
|
37
|
+
}
|
|
38
|
+
/** Checks the key against `/key` and, when `model` is given, that `/models`
|
|
39
|
+
* lists it. The first failure decides: a bad key is reported before a model
|
|
40
|
+
* question, since the model check would fail on the key too. */
|
|
41
|
+
export async function verifyOpenRouter(apiKey, model) {
|
|
42
|
+
const base = openRouterBase();
|
|
43
|
+
const key = await getJson(`${base}/key`, apiKey);
|
|
44
|
+
if ("unreachable" in key)
|
|
45
|
+
return { status: "unreachable", reason: key.unreachable };
|
|
46
|
+
if (key.status === 401 || key.status === 403) {
|
|
47
|
+
return {
|
|
48
|
+
status: "rejected",
|
|
49
|
+
error: new CliError("openrouter_unauthorized", "OpenRouter rejected the API key.", "Check the key at https://openrouter.ai/keys and pass it again.", EXIT_USAGE),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (key.status >= 400) {
|
|
53
|
+
return {
|
|
54
|
+
status: "unreachable",
|
|
55
|
+
reason: `OpenRouter returned ${key.status}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!model)
|
|
59
|
+
return { status: "ok" };
|
|
60
|
+
const models = await getJson(`${base}/models`, apiKey);
|
|
61
|
+
if ("unreachable" in models) {
|
|
62
|
+
return { status: "unreachable", reason: models.unreachable };
|
|
63
|
+
}
|
|
64
|
+
const rows = models.body?.data;
|
|
65
|
+
if (!Array.isArray(rows)) {
|
|
66
|
+
return {
|
|
67
|
+
status: "unreachable",
|
|
68
|
+
reason: "OpenRouter returned no model list",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!rows.some((row) => row?.id === model)) {
|
|
72
|
+
return {
|
|
73
|
+
status: "rejected",
|
|
74
|
+
error: new CliError("openrouter_unknown_model", `OpenRouter does not know the model ${model}.`, "Pick one at https://openrouter.ai/models and set it with co-maintainer set --high-model=...", EXIT_USAGE),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { status: "ok" };
|
|
78
|
+
}
|
package/dist/src/cli/args.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { prepareConfig } from "../config.js";
|
|
2
2
|
import { getEnv } from "../util/runtime.js";
|
|
3
3
|
import { askLine } from "./prompt.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
4
|
+
import { die } from "./error.js";
|
|
5
|
+
import { detectRemoteRepo } from "../local/git_ops.js";
|
|
6
|
+
import { reviewBlockingFrom } from "../review/blocking.js";
|
|
7
|
+
import { renderCommandHelp, renderGlobalHelp, unknownCommandMessage, unknownOptionMessage, } from "./commands/registry.js";
|
|
7
8
|
function numberOption(value, name) {
|
|
8
9
|
const number = Number(value);
|
|
9
10
|
if (!Number.isInteger(number) || number < 0) {
|
|
@@ -11,7 +12,7 @@ function numberOption(value, name) {
|
|
|
11
12
|
}
|
|
12
13
|
return number;
|
|
13
14
|
}
|
|
14
|
-
const commands = ["probe", "init", "remake", "review"];
|
|
15
|
+
const commands = ["probe", "init", "sync", "remake", "review"];
|
|
15
16
|
export const defaultLowModel = "openai/gpt-oss-120b";
|
|
16
17
|
const defaultHighModel = "openai/gpt-5.6-luna";
|
|
17
18
|
let cliInteractive = true;
|
|
@@ -41,36 +42,27 @@ export async function parseArgs(args) {
|
|
|
41
42
|
command === "-h" ||
|
|
42
43
|
repo === "--help" ||
|
|
43
44
|
repo === "-h") {
|
|
44
|
-
console.log(
|
|
45
|
-
console.log(" co-maintainer review [options]");
|
|
46
|
-
console.log(" co-maintainer review owner/repo PR_NUMBER [options]");
|
|
47
|
-
console.log(" co-maintainer set --token=... --ai=... --low-model=... --high-model=... --auth=...");
|
|
48
|
-
console.log(" co-maintainer serve --port=N");
|
|
49
|
-
console.log(" co-maintainer -v | --version");
|
|
50
|
-
console.log(" --webhook-url=https://host/github/webhook [or CM_WEBHOOK_URL]");
|
|
51
|
-
console.log(" --env=PATH --debug --log-time --gh-concurrent=N --ai-concurrent=N");
|
|
52
|
-
console.log("Options: --include-codebase --include-pull-requests --include-pull-request-changes");
|
|
53
|
-
console.log(" --include-commit-history --include-how-repo-works");
|
|
54
|
-
console.log(" --max-commits=N --max-pr-months=N --max-pull-request-change-lines=N --max-comment=N");
|
|
55
|
-
console.log(" --only-request-changed-pr");
|
|
56
|
-
console.log(" --pr-state=open,closed,merged");
|
|
57
|
-
console.log(" --auth=gh|pat --github-pat=... --ai=none|openrouter|hetzner --token=... --low-model=... --high-model=...");
|
|
45
|
+
console.log(renderGlobalHelp());
|
|
58
46
|
process.exit(0);
|
|
59
47
|
}
|
|
60
48
|
if (!commands.includes(command)) {
|
|
61
|
-
die(
|
|
49
|
+
die(unknownCommandMessage(command));
|
|
62
50
|
}
|
|
63
51
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
64
|
-
console.log(
|
|
65
|
-
console.log("Run co-maintainer --help for all options.");
|
|
52
|
+
console.log(renderCommandHelp(command) ?? renderGlobalHelp());
|
|
66
53
|
process.exit(0);
|
|
67
54
|
}
|
|
68
55
|
if (repo === "--help" || repo === "-h") {
|
|
69
|
-
console.log(
|
|
70
|
-
console.log("Run co-maintainer --help for all options.");
|
|
56
|
+
console.log(renderCommandHelp(command) ?? renderGlobalHelp());
|
|
71
57
|
process.exit(0);
|
|
72
58
|
}
|
|
73
|
-
|
|
59
|
+
// `probe` (and only probe, for now) may omit the repo and let the current
|
|
60
|
+
// directory's git remote name it (CORE-24).
|
|
61
|
+
let repoName = repo;
|
|
62
|
+
if ((!repoName || repoName.startsWith("-")) && command === "probe") {
|
|
63
|
+
repoName = await detectRemoteRepo(process.cwd());
|
|
64
|
+
}
|
|
65
|
+
if (!repoName || !/^[^/]+\/[^/]+$/.test(repoName)) {
|
|
74
66
|
die("Repository must look like owner/repo");
|
|
75
67
|
}
|
|
76
68
|
const { config, envPath } = prepareConfig(args);
|
|
@@ -78,7 +70,8 @@ export async function parseArgs(args) {
|
|
|
78
70
|
const configDefault = config.defaults ?? {};
|
|
79
71
|
// Remembered from a prior `init`/`remake` on this exact repo — never a
|
|
80
72
|
// secret, so it can safely fill in everything except the API token.
|
|
81
|
-
const repoConfig = config.repos?.[
|
|
73
|
+
const repoConfig = config.repos?.[repoName] ?? {};
|
|
74
|
+
const reviewBlocking = reviewBlockingFrom(env("CO_MAINTAINER_REVIEW_BLOCKING") ?? config.reviewBlocking);
|
|
82
75
|
let prNumber;
|
|
83
76
|
if (command === "review") {
|
|
84
77
|
const rawNumber = rest[0];
|
|
@@ -167,10 +160,11 @@ export async function parseArgs(args) {
|
|
|
167
160
|
arg === "--log-time" ||
|
|
168
161
|
arg === "--review-upstream" ||
|
|
169
162
|
arg === "--disable-codegraph" ||
|
|
163
|
+
arg === "--run" ||
|
|
170
164
|
arg === "--only-request-changed-pr")
|
|
171
165
|
continue;
|
|
172
166
|
if (arg.startsWith("--") && !known)
|
|
173
|
-
die(
|
|
167
|
+
die(unknownOptionMessage(arg));
|
|
174
168
|
}
|
|
175
169
|
const explicitAi = rest.some((arg) => arg.startsWith("--ai="));
|
|
176
170
|
const configuredAiRaw = env("CO_MAINTAINER_AI") ?? repoConfig.ai ?? config.ai;
|
|
@@ -190,7 +184,7 @@ export async function parseArgs(args) {
|
|
|
190
184
|
env("GH_TOKEN") ??
|
|
191
185
|
config.githubPat;
|
|
192
186
|
if (auth === "pat" && !githubPat) {
|
|
193
|
-
die("--auth=pat requires a GitHub token
|
|
187
|
+
die("--auth=pat requires a GitHub token. Pass --github-pat=..., set GITHUB_TOKEN/GH_TOKEN, " +
|
|
194
188
|
"or run: co-maintainer set --github-pat=...");
|
|
195
189
|
}
|
|
196
190
|
let ai = choice("ai", ["none", "openrouter", "hetzner"], configuredAi ?? "none");
|
|
@@ -240,12 +234,13 @@ export async function parseArgs(args) {
|
|
|
240
234
|
die("--token, --low-model, and --high-model are required when AI is enabled");
|
|
241
235
|
}
|
|
242
236
|
return {
|
|
243
|
-
command: command,
|
|
244
|
-
repo,
|
|
237
|
+
command: command === "sync" ? "remake" : command,
|
|
238
|
+
repo: repoName,
|
|
245
239
|
prNumber,
|
|
246
240
|
debug: rest.includes("--debug"),
|
|
247
241
|
logTime: rest.includes("--log-time"),
|
|
248
242
|
reviewUpstream: rest.includes("--review-upstream"),
|
|
243
|
+
run: rest.includes("--run"),
|
|
249
244
|
useCodegraph: command === "review" ? !rest.includes("--disable-codegraph") : false,
|
|
250
245
|
envPath,
|
|
251
246
|
improveMatrix: value("improve-matrix") ?? 1,
|
|
@@ -274,5 +269,6 @@ export async function parseArgs(args) {
|
|
|
274
269
|
repoConfig.maxPullRequestChangeLines ??
|
|
275
270
|
configDefault.maxPullRequestChangeLines,
|
|
276
271
|
maxComments: value("max-comment") ?? repoConfig.maxComments,
|
|
272
|
+
reviewBlocking,
|
|
277
273
|
};
|
|
278
274
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { UserConfig } from "../../config.ts";
|
|
2
|
+
/** Fields whose value must never be printed in full. */
|
|
3
|
+
export declare const SECRET_FIELDS: Set<string>;
|
|
4
|
+
/** `lowModel` -> `low-model`. The inverse of {@link configKey}. */
|
|
5
|
+
export declare function flagName(field: string): string;
|
|
6
|
+
/** `low-model` -> `lowModel`. */
|
|
7
|
+
export declare function configKey(name: string): string;
|
|
8
|
+
/** Masks a secret for display: 12 characters or more keep the last four so a
|
|
9
|
+
* user can tell two keys apart, anything shorter is fully hidden. `password`
|
|
10
|
+
* is handled by the caller because it is a hash, not a key. */
|
|
11
|
+
export declare function maskSecret(value: string): string;
|
|
12
|
+
/** Where a value came from, so `config list` can say why a field is what it
|
|
13
|
+
* is. Env beats the file because env overrides it at run time. */
|
|
14
|
+
export type Source = "env" | "file" | "default";
|
|
15
|
+
/** The env var that supplies each field, mirroring args.ts. Only the fields
|
|
16
|
+
* that actually read env are listed. */
|
|
17
|
+
export declare const ENV_BY_FIELD: Record<string, string[]>;
|
|
18
|
+
/** Renders one field for people. A secret is masked; the password hash, which
|
|
19
|
+
* nobody can use, only reports whether it is set. */
|
|
20
|
+
export declare function displayValue(field: string, value: unknown): string;
|
|
21
|
+
/** Values a field falls back to when neither env nor the file sets it, so
|
|
22
|
+
* `config list` can say a row is the default rather than leaving it out. */
|
|
23
|
+
export declare const DEFAULTS: Record<string, string>;
|
|
24
|
+
type Row = {
|
|
25
|
+
key: string;
|
|
26
|
+
field: string;
|
|
27
|
+
value: string;
|
|
28
|
+
source: Source;
|
|
29
|
+
};
|
|
30
|
+
/** Every top-level UserConfig key with a value and where it came from. */
|
|
31
|
+
export declare function configRows(config: UserConfig): Row[];
|
|
32
|
+
/** Resolves a user-supplied key name to its field. Accepts kebab-case
|
|
33
|
+
* (`low-model`), camelCase (`lowModel`), and rejects anything that is not a
|
|
34
|
+
* `UserConfig` key, so a typo cannot silently read undefined. */
|
|
35
|
+
export declare function resolveField(name: string): string;
|
|
36
|
+
/** The scalar `UserConfig` keys `config` can read and write. `repos` and
|
|
37
|
+
* `defaults` are nested maps, not settings, so they are not listed.
|
|
38
|
+
* `satisfies` makes this the type's own vocabulary: a name that is not a
|
|
39
|
+
* `UserConfig` key is a compile error rather than a silently undefined read. */
|
|
40
|
+
export declare const KNOWN_FIELDS: readonly ["auth", "ai", "lowModel", "highModel", "token", "githubPat", "githubAppId", "githubAppPrivateKey", "githubAppPrivateKeyPath", "githubWebhookSecret", "webhookUrl", "githubOAuthClientId", "githubOAuthClientSecret", "githubOAuthAllowedUser", "passwordAuthDisabled", "githubAuthEnabled", "dashboardPasswordHash", "maxConcurrentJobs", "reviewBlocking", "remoteHost", "remoteToken", "remoteNoticeShownFor", "remoteSyncTimeoutSeconds", "maxConcurrentRemoteReviewsPerToken", "remoteToolOutputMaxChars"];
|
|
41
|
+
/** `co-maintainer config <sub> ...`. `set` is handled by runSet, which this
|
|
42
|
+
* command delegates to so there is one implementation of the write rules. */
|
|
43
|
+
export declare function runConfig(args: string[]): Promise<void>;
|
|
44
|
+
export {};
|