co-maintainer 0.5.1-beta.2 → 0.5.1-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.5.1-beta.2",
3
+ "version": "0.5.1-beta.3",
4
4
  "description": "An imitator of yours, A PR review tool like co-maintainer.",
5
5
  "keywords": [
6
6
  "ai",
@@ -3,9 +3,7 @@ import type { AiProvider, AiRequest, AiResponse, Json, Options } from "../types.
3
3
  * added or removed in a single place instead of the half dozen copies
4
4
  * Hetzner's removal had to chase down. */
5
5
  export declare const AI_PROVIDERS: readonly ["none", "openrouter", "openai", "anthropic", "opencode-zen", "opencode-go"];
6
- /** The provider's own key variable. It is read only when that provider runs,
7
- * so a key meant for one provider is never sent to another. */
8
- export declare function providerKeyEnv(ai: string, env: (name: string) => string | undefined): string | undefined;
6
+ export { providerKeyEnv } from "../config.ts";
9
7
  /** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
10
8
  export declare function rejectRetiredProvider(value: string | undefined): void;
11
9
  export declare function createAiProvider(options: Options, model: string): AiProvider | undefined;
@@ -14,16 +14,7 @@ export const AI_PROVIDERS = [
14
14
  "opencode-zen",
15
15
  "opencode-go",
16
16
  ];
17
- /** The provider's own key variable. It is read only when that provider runs,
18
- * so a key meant for one provider is never sent to another. */
19
- export function providerKeyEnv(ai, env) {
20
- if (ai === "openrouter")
21
- return env("OPENROUTER_API_KEY");
22
- if (ai === "opencode-zen" || ai === "opencode-go") {
23
- return env("OPENCODE_API_KEY");
24
- }
25
- return undefined;
26
- }
17
+ export { providerKeyEnv } from "../config.js";
27
18
  /** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
28
19
  export function rejectRetiredProvider(value) {
29
20
  if (value === "hetzner") {
@@ -97,6 +97,16 @@ export type ByokPolicy = "off" | "allow" | "require";
97
97
  * overrides the saved config, so a hosted server cannot be talked out of its
98
98
  * policy by a config edit. An unset or unreadable value falls back to the
99
99
  * config, then to `off`. */
100
+ /** The provider's own key variable. It is read only when that provider runs,
101
+ * so a key meant for one provider is never sent to another. */
102
+ export declare function providerKeyEnv(ai: string, env?: (name: string) => string | undefined): string | undefined;
103
+ /** The AI key `serve` uses for its jobs and its setup checks, in the order the
104
+ * CLI resolves it: `CO_MAINTAINER_TOKEN`, then the provider's own variable,
105
+ * then the saved key. Without this a server started with the key in its
106
+ * environment reported AI as not configured and failed every job. */
107
+ export declare function resolvedAiToken(config: Pick<UserConfig, "ai" | "token">, ai?: string | undefined): string | undefined;
108
+ /** True when a provider is chosen and a key reaches it from anywhere. */
109
+ export declare function aiConfigured(config: Pick<UserConfig, "ai" | "token">): boolean;
100
110
  export declare function remoteByokPolicy(config?: Pick<UserConfig, "remoteByokPolicy">): ByokPolicy;
101
111
  /** Platform/env injection keeps the OS branches testable: the
102
112
  * migration's `"windows"` vs `"win32"` trap is invisible otherwise. */
@@ -3,6 +3,29 @@ import { chmod, currentPlatform, getEnv, isNotFound, mkdir, readTextFileSync, se
3
3
  * overrides the saved config, so a hosted server cannot be talked out of its
4
4
  * policy by a config edit. An unset or unreadable value falls back to the
5
5
  * config, then to `off`. */
6
+ /** The provider's own key variable. It is read only when that provider runs,
7
+ * so a key meant for one provider is never sent to another. */
8
+ export function providerKeyEnv(ai, env = getEnv) {
9
+ if (ai === "openrouter")
10
+ return env("OPENROUTER_API_KEY");
11
+ if (ai === "opencode-zen" || ai === "opencode-go") {
12
+ return env("OPENCODE_API_KEY");
13
+ }
14
+ return undefined;
15
+ }
16
+ /** The AI key `serve` uses for its jobs and its setup checks, in the order the
17
+ * CLI resolves it: `CO_MAINTAINER_TOKEN`, then the provider's own variable,
18
+ * then the saved key. Without this a server started with the key in its
19
+ * environment reported AI as not configured and failed every job. */
20
+ export function resolvedAiToken(config, ai = config.ai) {
21
+ return (getEnv("CO_MAINTAINER_TOKEN") ??
22
+ (ai ? providerKeyEnv(ai) : undefined) ??
23
+ config.token);
24
+ }
25
+ /** True when a provider is chosen and a key reaches it from anywhere. */
26
+ export function aiConfigured(config) {
27
+ return Boolean(config.ai && config.ai !== "none" && resolvedAiToken(config));
28
+ }
6
29
  export function remoteByokPolicy(config = readConfig()) {
7
30
  // An env value that is not one of the three known policies is treated as
8
31
  // unreadable and falls back to the config, so a typo cannot silently turn a
@@ -1,5 +1,6 @@
1
1
  import { errorResponse } from "../errors.js";
2
2
  import { readConfig, resolveAppPrivateKey, writeUserConfig, } from "../../config.js";
3
+ import { aiConfigured } from "../../config.js";
3
4
  import { testAppAccess, testGithubAccess } from "../../services/credentials.js";
4
5
  import { checkPassword, readSessionToken, revokeOtherSessions, } from "../auth.js";
5
6
  import { passwordProblem } from "../../util/password.js";
@@ -231,7 +232,7 @@ export async function handleSettingsRoute(request, url, webhookUrl, passwords, i
231
232
  privateKeyPem: resolveAppPrivateKey(config) ?? "",
232
233
  });
233
234
  return Response.json({
234
- ai: { ok: Boolean(config.ai && config.ai !== "none" && config.token) },
235
+ ai: { ok: aiConfigured(config) },
235
236
  github: github.ok
236
237
  ? { ok: true, login: github.login }
237
238
  : { ok: false, message: github.message },
@@ -5,7 +5,7 @@ import { errorResponse } from "./errors.js";
5
5
  import { hasCsrfHeader, login, logout, readSessionToken, sessionCookieHeader, verifySession, } from "./auth.js";
6
6
  import { memoryPasswordStore } from "./auth.js";
7
7
  import { clientAddress, forwardedHttps } from "./proxy_headers.js";
8
- import { readConfig, resolveAppPrivateKey } from "../config.js";
8
+ import { aiConfigured, readConfig, resolveAppPrivateKey } from "../config.js";
9
9
  import { listJobs } from "../store/jobs.js";
10
10
  import { handleJobsRoute } from "./api/jobs.js";
11
11
  import { handleReposRoute } from "./api/repos.js";
@@ -19,7 +19,7 @@ import { handleWebhookRequest } from "./webhook/index.js";
19
19
  import { handlePageRequest } from "./pages/router.js";
20
20
  function setupStatus() {
21
21
  const config = readConfig();
22
- const ai = Boolean(config.ai && config.ai !== "none" && config.token);
22
+ const ai = aiConfigured(config);
23
23
  const github = Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat));
24
24
  const app = Boolean(config.githubAppId && resolveAppPrivateKey(config));
25
25
  const missing = [
@@ -5,6 +5,7 @@ import { activityFeed, listReposForHome, prDetail, repoKnowledge, RepoNotFound,
5
5
  import { escapeHtml, html } from "./layout.js";
6
6
  import { formatCostTotal } from "./cost.js";
7
7
  import { readConfig, resolveAppPrivateKey, writeUserConfig, } from "../../config.js";
8
+ import { aiConfigured } from "../../config.js";
8
9
  import { appNameProblem, beginManifestState, buildAppManifest, consumeManifestState, convertManifest, } from "../../github/app_manifest.js";
9
10
  import { renderHome, renderLogin } from "./home.js";
10
11
  import { renderSetup } from "./setup.js";
@@ -113,7 +114,7 @@ export async function handlePageRequest(request, deps, ip) {
113
114
  if (url.pathname === "/setup" && request.method === "GET") {
114
115
  const config = readConfig();
115
116
  return renderSetup(username, {
116
- ai: Boolean(config.ai && config.ai !== "none" && config.token),
117
+ ai: aiConfigured(config),
117
118
  github: Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat)),
118
119
  app: Boolean(config.githubAppId && resolveAppPrivateKey(config)),
119
120
  });
@@ -1,11 +1,22 @@
1
1
  import { html, layout, skSlot, text } from "./layout.js";
2
- import { resolveAppPrivateKey } from "../../config.js";
2
+ import { aiConfigured, resolveAppPrivateKey, resolvedAiToken, } from "../../config.js";
3
3
  import { VERSION } from "../../version.js";
4
4
  import { TEMPLATE_INFO } from "../../services/review_policy.js";
5
5
  import { defaultAppName, manifestBlockedReason, } from "../../github/app_manifest.js";
6
6
  const SOURCE_URL = "https://github.com/GroophyLifefor/co-maintainer";
7
+ /** Which key the jobs will use. The environment wins over the saved key, the
8
+ * same order `resolvedAiToken` follows, so the hint must check it first. */
9
+ function keyHint(config) {
10
+ const fromEnv = resolvedAiToken({ ai: config.ai });
11
+ if (fromEnv && config.token) {
12
+ return "Using the key from the server's environment, which overrides the saved key";
13
+ }
14
+ if (fromEnv)
15
+ return "Using the key from the server's environment";
16
+ return config.token ? "A key is saved" : "No key saved";
17
+ }
7
18
  export function renderSettings(username, config, webhookUrl, baseUrl) {
8
- const aiOk = Boolean(config.ai && config.ai !== "none" && config.token);
19
+ const aiOk = aiConfigured(config);
9
20
  const ghOk = Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat));
10
21
  const appOk = Boolean(config.githubAppId && resolveAppPrivateKey(config));
11
22
  let host = "localhost";
@@ -53,7 +64,7 @@ export function renderSettings(username, config, webhookUrl, baseUrl) {
53
64
  </select></div>
54
65
  <div class="field"><label>API key</label>
55
66
  <input id="token" type="password" placeholder="Leave blank to keep the current key">
56
- <div class="hint">${config.token ? "A key is saved" : "No key saved"}</div></div>
67
+ <div class="hint">${keyHint(config)}</div></div>
57
68
  <div class="two" style="max-width:none">
58
69
  <div class="field"><label>High model</label>
59
70
  <input id="high" value="${text(config.highModel ?? "")}">
@@ -85,7 +85,9 @@ async function runJob(job) {
85
85
  ? cancelReason
86
86
  ? { cancel_reason: cancelReason }
87
87
  : {}
88
- : { error: String(error) });
88
+ : // The message only: `String(error)` would show the class name, and
89
+ // a remote client prints this text to its user as is.
90
+ { error: message });
89
91
  if (status === "failed")
90
92
  log(job.id, "error", message);
91
93
  log(job.id, "status", status);
@@ -210,7 +212,9 @@ export async function recoverOrphans() {
210
212
  setJobStatus(job.id, outcome === "canceled" ? "canceled" : "done", outcome === "canceled" ? { cancel_reason: "server_restarted" } : {});
211
213
  }
212
214
  catch (error) {
213
- setJobStatus(job.id, "failed", { error: String(error) });
215
+ setJobStatus(job.id, "failed", {
216
+ error: error instanceof Error ? error.message : String(error),
217
+ });
214
218
  }
215
219
  }
216
220
  else {
@@ -8,7 +8,7 @@ import { buildCarryPromptSection, classifyCarryItems, guideRebuiltSince, increme
8
8
  import { remoteBridgeToolHandlers } from "../remote/tool_bridge.js";
9
9
  import { revisionFromSubmitJson } from "../remote/revision_from_submit.js";
10
10
  import { closeRemoteSession, openRemoteSession, setRemoteSyncResult, } from "../remote/server/sessions.js";
11
- import { readConfig, remoteByokPolicy } from "../config.js";
11
+ import { readConfig, remoteByokPolicy, resolvedAiToken } from "../config.js";
12
12
  import { dropByokKey, getByokKey, hasByokKey, registerByokKey, } from "../remote/server/byok_keys.js";
13
13
  import { reviewBlockingFrom } from "../review/blocking.js";
14
14
  import { loadGuides } from "../review/guides.js";
@@ -66,7 +66,7 @@ function reviewOptionsForRepo(repo, useCodegraph, aiToken) {
66
66
  auth: config.auth ?? "gh",
67
67
  githubPat: config.githubPat,
68
68
  ai: config.ai ?? "openrouter",
69
- aiToken: aiToken ?? config.token,
69
+ aiToken: aiToken ?? resolvedAiToken(config, config.ai ?? "openrouter"),
70
70
  lowModel: config.lowModel,
71
71
  highModel: config.highModel,
72
72
  synthesisVersion: 16,
@@ -14,7 +14,7 @@ import { addResponseCost, costColumns, emptyTally, settle, } from "../util/cost.
14
14
  import { loadGuides } from "../review/guides.js";
15
15
  import { reviewBlockingFrom, isBlocking, DEFAULT_REVIEW_BLOCKING, } from "../review/blocking.js";
16
16
  import { matchRepeat } from "../pr/rounds.js";
17
- import { readConfig, resolveAppPrivateKey } from "../config.js";
17
+ import { readConfig, resolveAppPrivateKey, resolvedAiToken, } from "../config.js";
18
18
  import { outsideCode, redact } from "../util/redact.js";
19
19
  import { getRepo, setInstallationId } from "../store/repos.js";
20
20
  import { getReviewByJobId, insertReview, latestPostedReview, setReviewScope, setReviewStatus, } from "../store/reviews.js";
@@ -143,7 +143,7 @@ export function reviewOptions(repo, prNumber) {
143
143
  // With no provider saved the App review stays on OpenRouter, the same
144
144
  // fallback the CLI review uses.
145
145
  ai: config.ai && config.ai !== "none" ? config.ai : "openrouter",
146
- aiToken: config.token,
146
+ aiToken: resolvedAiToken(config, config.ai && config.ai !== "none" ? config.ai : "openrouter"),
147
147
  highModel: config.highModel,
148
148
  lowModel: config.lowModel,
149
149
  synthesisVersion: 16,
@@ -10,7 +10,7 @@ import { extractFacts } from "../knowledge/facts.js";
10
10
  import { buildReviewDocuments, reviewSignalCount } from "../knowledge/guide.js";
11
11
  import { assembleSkill, codebaseBody, factSectionHashes, } from "../knowledge/skill.js";
12
12
  import { validateSkill } from "../knowledge/validate.js";
13
- import { readConfig, reposDir, writeRepoConfig } from "../config.js";
13
+ import { readConfig, reposDir, resolvedAiToken, writeRepoConfig, } from "../config.js";
14
14
  import { readState, writeState } from "../store/skill_state.js";
15
15
  import { cacheSet } from "../store/cache_db.js";
16
16
  import { log, timed, withLogSink } from "../util/log.js";
@@ -318,7 +318,8 @@ export function optionsFromConfig(repo, command) {
318
318
  const ai = repoConfig.ai ?? config.ai ?? "none";
319
319
  const lowModel = repoConfig.lowModel ?? config.lowModel;
320
320
  const highModel = repoConfig.highModel ?? config.highModel;
321
- if (ai !== "none" && (!config.token || !lowModel || !highModel)) {
321
+ const aiToken = resolvedAiToken(config, ai);
322
+ if (ai !== "none" && (!aiToken || !lowModel || !highModel)) {
322
323
  throw new Error("AI is enabled but token/low-model/high-model are not fully configured. Run: " +
323
324
  "co-maintainer set --token=... --low-model=... --high-model=...");
324
325
  }
@@ -333,7 +334,7 @@ export function optionsFromConfig(repo, command) {
333
334
  auth,
334
335
  githubPat: config.githubPat,
335
336
  ai,
336
- aiToken: config.token,
337
+ aiToken,
337
338
  lowModel,
338
339
  highModel,
339
340
  synthesisVersion: 16,
@@ -3,7 +3,7 @@
3
3
  * Each item is derived from state that already exists, so the card reflects
4
4
  * what serve can actually do rather than a parallel bookkeeping of it. The
5
5
  * webhook item reuses the existing reachability check. */
6
- import { readConfig, resolveAppPrivateKey } from "../config.js";
6
+ import { aiConfigured, readConfig, resolveAppPrivateKey } from "../config.js";
7
7
  import { countReviews } from "../store/reviews.js";
8
8
  import { listRemoteTokens } from "../store/remote_tokens.js";
9
9
  import { listActiveRepos } from "../store/repos.js";
@@ -11,7 +11,7 @@ import { lastDelivery } from "../store/deliveries.js";
11
11
  import { webhookReachabilityProblem } from "../util/webhook_reachability.js";
12
12
  export function setupChecklist(webhookUrl) {
13
13
  const config = readConfig();
14
- const aiDone = Boolean(config.ai && config.ai !== "none" && config.token);
14
+ const aiDone = aiConfigured(config);
15
15
  const appDone = Boolean(config.githubAppId && resolveAppPrivateKey(config));
16
16
  const repoCount = listActiveRepos().length;
17
17
  const reviewCount = countReviews();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "co-maintainer",
3
- "version": "0.5.1-beta.2",
3
+ "version": "0.5.1-beta.3",
4
4
  "description": "An imitator of yours, A PR review tool like co-maintainer.",
5
5
  "keywords": [
6
6
  "ai",