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.
Files changed (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +57 -38
  3. package/dist/main.js +2 -3
  4. package/dist/package.json +21 -15
  5. package/dist/src/ai/batch.d.ts +24 -1
  6. package/dist/src/ai/batch.js +75 -23
  7. package/dist/src/ai/estimate.d.ts +40 -0
  8. package/dist/src/ai/estimate.js +113 -0
  9. package/dist/src/ai/fake.d.ts +1 -1
  10. package/dist/src/ai/fake.js +1 -1
  11. package/dist/src/ai/hetzner.js +1 -1
  12. package/dist/src/ai/openrouter.d.ts +13 -0
  13. package/dist/src/ai/openrouter.js +66 -12
  14. package/dist/src/ai/pricing.d.ts +14 -0
  15. package/dist/src/ai/pricing.js +77 -0
  16. package/dist/src/ai/provider.js +8 -0
  17. package/dist/src/ai/verify.d.ts +25 -0
  18. package/dist/src/ai/verify.js +78 -0
  19. package/dist/src/cli/args.js +25 -29
  20. package/dist/src/cli/commands/config.d.ts +44 -0
  21. package/dist/src/cli/commands/config.js +220 -0
  22. package/dist/src/cli/commands/probe.js +70 -103
  23. package/dist/src/cli/commands/registry.d.ts +57 -0
  24. package/dist/src/cli/commands/registry.js +713 -0
  25. package/dist/src/cli/commands/review.js +66 -43
  26. package/dist/src/cli/commands/serve.d.ts +4 -2
  27. package/dist/src/cli/commands/serve.js +14 -10
  28. package/dist/src/cli/commands/set.d.ts +2 -1
  29. package/dist/src/cli/commands/set.js +63 -17
  30. package/dist/src/cli/commands/view.d.ts +1 -0
  31. package/dist/src/cli/commands/view.js +180 -0
  32. package/dist/src/cli/error.d.ts +39 -0
  33. package/dist/src/cli/error.js +64 -0
  34. package/dist/src/cli/main.d.ts +7 -0
  35. package/dist/src/cli/main.js +82 -1
  36. package/dist/src/cli/prompt.js +21 -7
  37. package/dist/src/cli/review_args.d.ts +4 -0
  38. package/dist/src/cli/review_args.js +22 -9
  39. package/dist/src/cli/review_output.d.ts +2 -2
  40. package/dist/src/cli/review_output.js +7 -6
  41. package/dist/src/cli/review_result.d.ts +59 -8
  42. package/dist/src/cli/review_result.js +145 -103
  43. package/dist/src/config.d.ts +15 -0
  44. package/dist/src/config.js +31 -0
  45. package/dist/src/github/app.d.ts +13 -0
  46. package/dist/src/github/app.js +23 -0
  47. package/dist/src/github/app_manifest.d.ts +50 -0
  48. package/dist/src/github/app_manifest.js +138 -0
  49. package/dist/src/github/client.js +7 -1
  50. package/dist/src/github/collect.js +1 -1
  51. package/dist/src/github/gh.js +75 -18
  52. package/dist/src/knowledge/facts.js +8 -3
  53. package/dist/src/knowledge/guide.d.ts +8 -0
  54. package/dist/src/knowledge/guide.js +32 -8
  55. package/dist/src/knowledge/probe.js +13 -3
  56. package/dist/src/knowledge/sections.d.ts +9 -0
  57. package/dist/src/knowledge/sections.js +16 -0
  58. package/dist/src/knowledge/skill.d.ts +5 -0
  59. package/dist/src/knowledge/skill.js +68 -21
  60. package/dist/src/knowledge/synthesis.d.ts +17 -1
  61. package/dist/src/knowledge/synthesis.js +70 -15
  62. package/dist/src/knowledge/types.d.ts +5 -0
  63. package/dist/src/local/codegraph_prepare.d.ts +3 -2
  64. package/dist/src/local/codegraph_prepare.js +4 -2
  65. package/dist/src/local/git_ops.d.ts +5 -4
  66. package/dist/src/local/git_ops.js +8 -9
  67. package/dist/src/local/review_local.js +36 -14
  68. package/dist/src/pr/checkout.js +14 -3
  69. package/dist/src/pr/codegraph_tools.js +3 -3
  70. package/dist/src/pr/diff_summary.js +3 -3
  71. package/dist/src/pr/findings.js +16 -5
  72. package/dist/src/pr/findings_json.d.ts +26 -0
  73. package/dist/src/pr/findings_json.js +187 -0
  74. package/dist/src/pr/review_copy.d.ts +20 -0
  75. package/dist/src/pr/review_copy.js +40 -0
  76. package/dist/src/pr/reviewer.d.ts +2 -1
  77. package/dist/src/pr/reviewer.js +84 -91
  78. package/dist/src/remote/client.js +42 -42
  79. package/dist/src/remote/http.d.ts +5 -0
  80. package/dist/src/remote/http.js +49 -0
  81. package/dist/src/remote/server/guides.d.ts +22 -0
  82. package/dist/src/remote/server/guides.js +81 -0
  83. package/dist/src/remote/server/routes.js +24 -0
  84. package/dist/src/review/blocking.d.ts +35 -0
  85. package/dist/src/review/blocking.js +45 -0
  86. package/dist/src/review/carry_over.d.ts +12 -1
  87. package/dist/src/review/carry_over.js +35 -9
  88. package/dist/src/review/engine.d.ts +1 -0
  89. package/dist/src/review/guides.js +28 -1
  90. package/dist/src/server/api/installations.js +1 -1
  91. package/dist/src/server/api/repos.js +86 -3
  92. package/dist/src/server/api/settings.js +3 -3
  93. package/dist/src/server/app.d.ts +3 -0
  94. package/dist/src/server/app.js +12 -5
  95. package/dist/src/server/pages/activity.d.ts +1 -0
  96. package/dist/src/server/pages/activity.js +2 -2
  97. package/dist/src/server/pages/add_repo.js +73 -5
  98. package/dist/src/server/pages/client.d.ts +1 -1
  99. package/dist/src/server/pages/client.js +1 -1
  100. package/dist/src/server/pages/home.d.ts +6 -1
  101. package/dist/src/server/pages/home.js +25 -2
  102. package/dist/src/server/pages/knowledge.js +1 -1
  103. package/dist/src/server/pages/layout.js +2 -2
  104. package/dist/src/server/pages/repo.d.ts +1 -1
  105. package/dist/src/server/pages/repo.js +21 -2
  106. package/dist/src/server/pages/repo_prs.d.ts +5 -1
  107. package/dist/src/server/pages/repo_prs.js +36 -1
  108. package/dist/src/server/pages/repo_remote.js +2 -2
  109. package/dist/src/server/pages/repo_settings.js +2 -2
  110. package/dist/src/server/pages/router.d.ts +6 -0
  111. package/dist/src/server/pages/router.js +129 -9
  112. package/dist/src/server/pages/settings.d.ts +1 -1
  113. package/dist/src/server/pages/settings.js +87 -9
  114. package/dist/src/server/pages/styles.d.ts +1 -1
  115. package/dist/src/server/pages/styles.js +1 -1
  116. package/dist/src/services/probe.d.ts +28 -0
  117. package/dist/src/services/probe.js +133 -0
  118. package/dist/src/services/remake_cron.d.ts +1 -1
  119. package/dist/src/services/remake_cron.js +3 -3
  120. package/dist/src/services/remote_review.js +14 -4
  121. package/dist/src/services/review.d.ts +11 -0
  122. package/dist/src/services/review.js +39 -11
  123. package/dist/src/services/setup.js +37 -19
  124. package/dist/src/services/setup_checklist.d.ts +10 -0
  125. package/dist/src/services/setup_checklist.js +87 -0
  126. package/dist/src/store/app_db.js +5 -2
  127. package/dist/src/store/cache_db.d.ts +4 -0
  128. package/dist/src/store/cache_db.js +27 -4
  129. package/dist/src/store/deliveries.d.ts +7 -0
  130. package/dist/src/store/deliveries.js +15 -0
  131. package/dist/src/tools/codegraph.d.ts +30 -0
  132. package/dist/src/tools/codegraph.js +49 -12
  133. package/dist/src/types.d.ts +10 -0
  134. package/dist/src/util/log.d.ts +3 -0
  135. package/dist/src/util/log.js +12 -0
  136. package/dist/src/util/run_summary.d.ts +33 -0
  137. package/dist/src/util/run_summary.js +47 -0
  138. package/dist/src/util/webhook_reachability.d.ts +11 -0
  139. package/dist/src/util/webhook_reachability.js +64 -0
  140. 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
- endpoint = "https://openrouter.ai/api/v1/chat/completions";
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
- const response = await fetch(this.endpoint, {
15
- method: "POST",
16
- headers: {
17
- Authorization: `Bearer ${this.apiKey}`,
18
- "Content-Type": "application/json",
19
- "HTTP-Referer": "https://github.com/GroophyLifefor/co-maintainer",
20
- "X-Title": "co-maintainer",
21
- },
22
- body: JSON.stringify(chatBody(this.model, request)),
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 new Error(`OpenRouter ${response.status}: ${await response.text()}`);
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
+ }
@@ -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
+ }
@@ -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
- function die(message) {
5
- throw new Error(message);
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("Usage: co-maintainer <probe|init|remake|review> owner/repo [options]");
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(`Unknown command: ${command}`);
49
+ die(unknownCommandMessage(command));
62
50
  }
63
51
  if (rest.includes("--help") || rest.includes("-h")) {
64
- console.log(`Usage: co-maintainer ${command} ...`);
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(`Usage: co-maintainer ${command} ...`);
70
- console.log("Run co-maintainer --help for all options.");
56
+ console.log(renderCommandHelp(command) ?? renderGlobalHelp());
71
57
  process.exit(0);
72
58
  }
73
- if (!repo || !/^[^/]+\/[^/]+$/.test(repo)) {
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?.[repo] ?? {};
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(`Unknown option: ${arg}`);
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; pass --github-pat=..., set GITHUB_TOKEN/GH_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 {};