co-maintainer 0.4.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 (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/main.d.ts +2 -0
  4. package/dist/main.js +9 -0
  5. package/dist/mod.d.ts +4 -0
  6. package/dist/mod.js +2 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/ai/batch.d.ts +16 -0
  9. package/dist/src/ai/batch.js +117 -0
  10. package/dist/src/ai/fake.d.ts +7 -0
  11. package/dist/src/ai/fake.js +40 -0
  12. package/dist/src/ai/hetzner.d.ts +16 -0
  13. package/dist/src/ai/hetzner.js +69 -0
  14. package/dist/src/ai/mermaid.d.ts +27 -0
  15. package/dist/src/ai/mermaid.js +457 -0
  16. package/dist/src/ai/mermaid_loop.d.ts +12 -0
  17. package/dist/src/ai/mermaid_loop.js +100 -0
  18. package/dist/src/ai/openrouter.d.ts +9 -0
  19. package/dist/src/ai/openrouter.js +29 -0
  20. package/dist/src/ai/provider.d.ts +4 -0
  21. package/dist/src/ai/provider.js +63 -0
  22. package/dist/src/cli/args.d.ts +5 -0
  23. package/dist/src/cli/args.js +252 -0
  24. package/dist/src/cli/commands/probe.d.ts +2 -0
  25. package/dist/src/cli/commands/probe.js +122 -0
  26. package/dist/src/cli/commands/review.d.ts +3 -0
  27. package/dist/src/cli/commands/review.js +84 -0
  28. package/dist/src/cli/commands/serve.d.ts +18 -0
  29. package/dist/src/cli/commands/serve.js +183 -0
  30. package/dist/src/cli/commands/set.d.ts +7 -0
  31. package/dist/src/cli/commands/set.js +171 -0
  32. package/dist/src/cli/main.d.ts +1 -0
  33. package/dist/src/cli/main.js +27 -0
  34. package/dist/src/cli/prompt.d.ts +4 -0
  35. package/dist/src/cli/prompt.js +26 -0
  36. package/dist/src/cli/review_args.d.ts +28 -0
  37. package/dist/src/cli/review_args.js +84 -0
  38. package/dist/src/cli/review_output.d.ts +2 -0
  39. package/dist/src/cli/review_output.js +10 -0
  40. package/dist/src/cli/review_result.d.ts +81 -0
  41. package/dist/src/cli/review_result.js +281 -0
  42. package/dist/src/config.d.ts +108 -0
  43. package/dist/src/config.js +138 -0
  44. package/dist/src/git/default_branch.d.ts +5 -0
  45. package/dist/src/git/default_branch.js +22 -0
  46. package/dist/src/github/app.d.ts +43 -0
  47. package/dist/src/github/app.js +120 -0
  48. package/dist/src/github/client.d.ts +17 -0
  49. package/dist/src/github/client.js +66 -0
  50. package/dist/src/github/collect.d.ts +7 -0
  51. package/dist/src/github/collect.js +368 -0
  52. package/dist/src/github/gh.d.ts +8 -0
  53. package/dist/src/github/gh.js +63 -0
  54. package/dist/src/github/jwt.d.ts +12 -0
  55. package/dist/src/github/jwt.js +84 -0
  56. package/dist/src/github/oauth.d.ts +9 -0
  57. package/dist/src/github/oauth.js +51 -0
  58. package/dist/src/github/pat.d.ts +10 -0
  59. package/dist/src/github/pat.js +60 -0
  60. package/dist/src/knowledge/facts.d.ts +3 -0
  61. package/dist/src/knowledge/facts.js +325 -0
  62. package/dist/src/knowledge/guide.d.ts +6 -0
  63. package/dist/src/knowledge/guide.js +82 -0
  64. package/dist/src/knowledge/probe.d.ts +12 -0
  65. package/dist/src/knowledge/probe.js +144 -0
  66. package/dist/src/knowledge/sections.d.ts +2 -0
  67. package/dist/src/knowledge/sections.js +13 -0
  68. package/dist/src/knowledge/skill.d.ts +10 -0
  69. package/dist/src/knowledge/skill.js +106 -0
  70. package/dist/src/knowledge/synthesis.d.ts +7 -0
  71. package/dist/src/knowledge/synthesis.js +315 -0
  72. package/dist/src/knowledge/types.d.ts +49 -0
  73. package/dist/src/knowledge/types.js +1 -0
  74. package/dist/src/knowledge/validate.d.ts +6 -0
  75. package/dist/src/knowledge/validate.js +134 -0
  76. package/dist/src/local/carry_over_store.d.ts +18 -0
  77. package/dist/src/local/carry_over_store.js +40 -0
  78. package/dist/src/local/codegraph_exclude.d.ts +3 -0
  79. package/dist/src/local/codegraph_exclude.js +26 -0
  80. package/dist/src/local/codegraph_prepare.d.ts +17 -0
  81. package/dist/src/local/codegraph_prepare.js +51 -0
  82. package/dist/src/local/git_ops.d.ts +12 -0
  83. package/dist/src/local/git_ops.js +98 -0
  84. package/dist/src/local/git_parse.d.ts +27 -0
  85. package/dist/src/local/git_parse.js +131 -0
  86. package/dist/src/local/git_revision.d.ts +15 -0
  87. package/dist/src/local/git_revision.js +141 -0
  88. package/dist/src/local/git_untracked.d.ts +21 -0
  89. package/dist/src/local/git_untracked.js +79 -0
  90. package/dist/src/local/review_local.d.ts +4 -0
  91. package/dist/src/local/review_local.js +273 -0
  92. package/dist/src/local/review_lock.d.ts +7 -0
  93. package/dist/src/local/review_lock.js +45 -0
  94. package/dist/src/pr/checkout.d.ts +25 -0
  95. package/dist/src/pr/checkout.js +136 -0
  96. package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
  97. package/dist/src/pr/codegraph_tool_args.js +34 -0
  98. package/dist/src/pr/codegraph_tools.d.ts +15 -0
  99. package/dist/src/pr/codegraph_tools.js +408 -0
  100. package/dist/src/pr/diff_summary.d.ts +6 -0
  101. package/dist/src/pr/diff_summary.js +52 -0
  102. package/dist/src/pr/findings.d.ts +14 -0
  103. package/dist/src/pr/findings.js +94 -0
  104. package/dist/src/pr/github_revision.d.ts +4 -0
  105. package/dist/src/pr/github_revision.js +39 -0
  106. package/dist/src/pr/hunks.d.ts +18 -0
  107. package/dist/src/pr/hunks.js +73 -0
  108. package/dist/src/pr/reviewer.d.ts +40 -0
  109. package/dist/src/pr/reviewer.js +582 -0
  110. package/dist/src/pr/rounds.d.ts +17 -0
  111. package/dist/src/pr/rounds.js +43 -0
  112. package/dist/src/pr/scope.d.ts +35 -0
  113. package/dist/src/pr/scope.js +84 -0
  114. package/dist/src/pr/snapshot.d.ts +11 -0
  115. package/dist/src/pr/snapshot.js +1 -0
  116. package/dist/src/pr/suggestion.d.ts +17 -0
  117. package/dist/src/pr/suggestion.js +70 -0
  118. package/dist/src/remote/client.d.ts +4 -0
  119. package/dist/src/remote/client.js +199 -0
  120. package/dist/src/remote/client_tools.d.ts +13 -0
  121. package/dist/src/remote/client_tools.js +30 -0
  122. package/dist/src/remote/fixtures.d.ts +1 -0
  123. package/dist/src/remote/fixtures.js +30 -0
  124. package/dist/src/remote/revision_from_submit.d.ts +2 -0
  125. package/dist/src/remote/revision_from_submit.js +26 -0
  126. package/dist/src/remote/schema.d.ts +14 -0
  127. package/dist/src/remote/schema.js +30 -0
  128. package/dist/src/remote/server/body.d.ts +18 -0
  129. package/dist/src/remote/server/body.js +73 -0
  130. package/dist/src/remote/server/routes.d.ts +1 -0
  131. package/dist/src/remote/server/routes.js +150 -0
  132. package/dist/src/remote/server/sessions.d.ts +16 -0
  133. package/dist/src/remote/server/sessions.js +48 -0
  134. package/dist/src/remote/server/sync.d.ts +3 -0
  135. package/dist/src/remote/server/sync.js +90 -0
  136. package/dist/src/remote/tool_bridge.d.ts +15 -0
  137. package/dist/src/remote/tool_bridge.js +84 -0
  138. package/dist/src/remote/validate.d.ts +8 -0
  139. package/dist/src/remote/validate.js +248 -0
  140. package/dist/src/review/carry_over.d.ts +63 -0
  141. package/dist/src/review/carry_over.js +372 -0
  142. package/dist/src/review/engine.d.ts +13 -0
  143. package/dist/src/review/engine.js +4 -0
  144. package/dist/src/review/guides.d.ts +12 -0
  145. package/dist/src/review/guides.js +44 -0
  146. package/dist/src/review/revision.d.ts +26 -0
  147. package/dist/src/review/revision.js +57 -0
  148. package/dist/src/server/api/activity.d.ts +1 -0
  149. package/dist/src/server/api/activity.js +12 -0
  150. package/dist/src/server/api/analytics.d.ts +1 -0
  151. package/dist/src/server/api/analytics.js +9 -0
  152. package/dist/src/server/api/installations.d.ts +4 -0
  153. package/dist/src/server/api/installations.js +24 -0
  154. package/dist/src/server/api/jobs.d.ts +1 -0
  155. package/dist/src/server/api/jobs.js +86 -0
  156. package/dist/src/server/api/json_body.d.ts +1 -0
  157. package/dist/src/server/api/json_body.js +14 -0
  158. package/dist/src/server/api/remote_tokens.d.ts +1 -0
  159. package/dist/src/server/api/remote_tokens.js +71 -0
  160. package/dist/src/server/api/repos.d.ts +4 -0
  161. package/dist/src/server/api/repos.js +136 -0
  162. package/dist/src/server/api/settings.d.ts +1 -0
  163. package/dist/src/server/api/settings.js +210 -0
  164. package/dist/src/server/app.d.ts +27 -0
  165. package/dist/src/server/app.js +147 -0
  166. package/dist/src/server/assets.d.ts +3 -0
  167. package/dist/src/server/assets.js +21 -0
  168. package/dist/src/server/auth.d.ts +44 -0
  169. package/dist/src/server/auth.js +188 -0
  170. package/dist/src/server/errors.d.ts +8 -0
  171. package/dist/src/server/errors.js +10 -0
  172. package/dist/src/server/http.d.ts +8 -0
  173. package/dist/src/server/http.js +117 -0
  174. package/dist/src/server/logo.d.ts +1 -0
  175. package/dist/src/server/logo.js +2 -0
  176. package/dist/src/server/pages/activity.d.ts +4 -0
  177. package/dist/src/server/pages/activity.js +200 -0
  178. package/dist/src/server/pages/add_repo.d.ts +9 -0
  179. package/dist/src/server/pages/add_repo.js +56 -0
  180. package/dist/src/server/pages/analytics.d.ts +2 -0
  181. package/dist/src/server/pages/analytics.js +193 -0
  182. package/dist/src/server/pages/client.d.ts +2 -0
  183. package/dist/src/server/pages/client.js +2 -0
  184. package/dist/src/server/pages/home.d.ts +16 -0
  185. package/dist/src/server/pages/home.js +87 -0
  186. package/dist/src/server/pages/knowledge.d.ts +2 -0
  187. package/dist/src/server/pages/knowledge.js +64 -0
  188. package/dist/src/server/pages/layout.d.ts +32 -0
  189. package/dist/src/server/pages/layout.js +230 -0
  190. package/dist/src/server/pages/pr.d.ts +2 -0
  191. package/dist/src/server/pages/pr.js +109 -0
  192. package/dist/src/server/pages/repo.d.ts +2 -0
  193. package/dist/src/server/pages/repo.js +92 -0
  194. package/dist/src/server/pages/repo_prs.d.ts +2 -0
  195. package/dist/src/server/pages/repo_prs.js +75 -0
  196. package/dist/src/server/pages/repo_remote.d.ts +2 -0
  197. package/dist/src/server/pages/repo_remote.js +38 -0
  198. package/dist/src/server/pages/repo_settings.d.ts +3 -0
  199. package/dist/src/server/pages/repo_settings.js +127 -0
  200. package/dist/src/server/pages/router.d.ts +17 -0
  201. package/dist/src/server/pages/router.js +312 -0
  202. package/dist/src/server/pages/settings.d.ts +2 -0
  203. package/dist/src/server/pages/settings.js +337 -0
  204. package/dist/src/server/pages/setup.d.ts +5 -0
  205. package/dist/src/server/pages/setup.js +38 -0
  206. package/dist/src/server/pages/styles.d.ts +2 -0
  207. package/dist/src/server/pages/styles.js +2 -0
  208. package/dist/src/server/webhook/index.d.ts +1 -0
  209. package/dist/src/server/webhook/index.js +64 -0
  210. package/dist/src/server/webhook/signature.d.ts +7 -0
  211. package/dist/src/server/webhook/signature.js +41 -0
  212. package/dist/src/services/credentials.d.ts +19 -0
  213. package/dist/src/services/credentials.js +146 -0
  214. package/dist/src/services/dashboard.d.ts +165 -0
  215. package/dist/src/services/dashboard.js +200 -0
  216. package/dist/src/services/drift.d.ts +13 -0
  217. package/dist/src/services/drift.js +76 -0
  218. package/dist/src/services/jobs.d.ts +34 -0
  219. package/dist/src/services/jobs.js +222 -0
  220. package/dist/src/services/remote_review.d.ts +10 -0
  221. package/dist/src/services/remote_review.js +274 -0
  222. package/dist/src/services/remote_review_abort.d.ts +3 -0
  223. package/dist/src/services/remote_review_abort.js +11 -0
  224. package/dist/src/services/remote_token_jobs.d.ts +1 -0
  225. package/dist/src/services/remote_token_jobs.js +20 -0
  226. package/dist/src/services/remote_tokens.d.ts +13 -0
  227. package/dist/src/services/remote_tokens.js +57 -0
  228. package/dist/src/services/replies.d.ts +7 -0
  229. package/dist/src/services/replies.js +248 -0
  230. package/dist/src/services/review.d.ts +35 -0
  231. package/dist/src/services/review.js +671 -0
  232. package/dist/src/services/setup.d.ts +29 -0
  233. package/dist/src/services/setup.js +364 -0
  234. package/dist/src/services/webhook.d.ts +22 -0
  235. package/dist/src/services/webhook.js +401 -0
  236. package/dist/src/store/app_db.d.ts +10 -0
  237. package/dist/src/store/app_db.js +85 -0
  238. package/dist/src/store/cache_db.d.ts +4 -0
  239. package/dist/src/store/cache_db.js +63 -0
  240. package/dist/src/store/deliveries.d.ts +12 -0
  241. package/dist/src/store/deliveries.js +23 -0
  242. package/dist/src/store/drift.d.ts +4 -0
  243. package/dist/src/store/drift.js +22 -0
  244. package/dist/src/store/findings.d.ts +31 -0
  245. package/dist/src/store/findings.js +64 -0
  246. package/dist/src/store/installations.d.ts +9 -0
  247. package/dist/src/store/installations.js +35 -0
  248. package/dist/src/store/job_logs.d.ts +5 -0
  249. package/dist/src/store/job_logs.js +17 -0
  250. package/dist/src/store/jobs.d.ts +27 -0
  251. package/dist/src/store/jobs.js +86 -0
  252. package/dist/src/store/migrations.d.ts +5 -0
  253. package/dist/src/store/migrations.js +258 -0
  254. package/dist/src/store/remote_review_inputs.d.ts +11 -0
  255. package/dist/src/store/remote_review_inputs.js +25 -0
  256. package/dist/src/store/remote_tokens.d.ts +10 -0
  257. package/dist/src/store/remote_tokens.js +45 -0
  258. package/dist/src/store/replies.d.ts +24 -0
  259. package/dist/src/store/replies.js +79 -0
  260. package/dist/src/store/repos.d.ts +11 -0
  261. package/dist/src/store/repos.js +59 -0
  262. package/dist/src/store/reviews.d.ts +86 -0
  263. package/dist/src/store/reviews.js +220 -0
  264. package/dist/src/store/rows.d.ts +179 -0
  265. package/dist/src/store/rows.js +4 -0
  266. package/dist/src/store/sessions.d.ts +8 -0
  267. package/dist/src/store/sessions.js +34 -0
  268. package/dist/src/store/skill_state.d.ts +3 -0
  269. package/dist/src/store/skill_state.js +24 -0
  270. package/dist/src/store/sqlite.d.ts +39 -0
  271. package/dist/src/store/sqlite.js +77 -0
  272. package/dist/src/store/subjects.d.ts +15 -0
  273. package/dist/src/store/subjects.js +78 -0
  274. package/dist/src/tools/codegraph.d.ts +58 -0
  275. package/dist/src/tools/codegraph.js +165 -0
  276. package/dist/src/tools/codegraph_exec.d.ts +13 -0
  277. package/dist/src/tools/codegraph_exec.js +66 -0
  278. package/dist/src/types.d.ts +89 -0
  279. package/dist/src/types.js +1 -0
  280. package/dist/src/util/atomic.d.ts +4 -0
  281. package/dist/src/util/atomic.js +78 -0
  282. package/dist/src/util/keyed_lock.d.ts +3 -0
  283. package/dist/src/util/keyed_lock.js +33 -0
  284. package/dist/src/util/log.d.ts +11 -0
  285. package/dist/src/util/log.js +42 -0
  286. package/dist/src/util/redact.d.ts +5 -0
  287. package/dist/src/util/redact.js +30 -0
  288. package/dist/src/util/runtime.d.ts +90 -0
  289. package/dist/src/util/runtime.js +206 -0
  290. package/dist/src/util/time.d.ts +9 -0
  291. package/dist/src/util/time.js +36 -0
  292. package/dist/src/version.d.ts +1 -0
  293. package/dist/src/version.js +5 -0
  294. package/package.json +54 -0
@@ -0,0 +1,86 @@
1
+ /** The log stream is meant to be consumed with `fetch()` and a manual
2
+ * reader, not the native `EventSource` API — `EventSource` cannot set an
3
+ * `Authorization` header, and every other `/api/*` route needs one. */
4
+ import { errorResponse } from "../errors.js";
5
+ import { cancel, getLogsSince, subscribeToLogs } from "../../services/jobs.js";
6
+ import { getJob, listJobs } from "../../store/jobs.js";
7
+ const ROUTE = /^\/api\/jobs(?:\/([^/]+)(?:\/(logs\/stream|cancel))?)?$/;
8
+ export function handleJobsRoute(request, url) {
9
+ const match = ROUTE.exec(url.pathname);
10
+ if (!match) {
11
+ return errorResponse(404, "not_found", `no route for ${url.pathname}`);
12
+ }
13
+ const [, jobId, sub] = match;
14
+ if (!jobId && request.method === "GET") {
15
+ return Response.json({
16
+ items: listJobs({
17
+ status: url.searchParams.get("status") ?? undefined,
18
+ repo: url.searchParams.get("repo") ?? undefined,
19
+ }),
20
+ });
21
+ }
22
+ if (jobId && !sub && request.method === "GET") {
23
+ const job = getJob(jobId);
24
+ if (!job)
25
+ return errorResponse(404, "not_found", "no such job");
26
+ return Response.json({ ...job, logs: getLogsSince(jobId).slice(-50) });
27
+ }
28
+ if (jobId && sub === "cancel" && request.method === "POST") {
29
+ if (!getJob(jobId))
30
+ return errorResponse(404, "not_found", "no such job");
31
+ return Response.json({ ok: cancel(jobId, "dashboard_canceled") });
32
+ }
33
+ if (jobId && sub === "logs/stream" && request.method === "GET") {
34
+ const from = Number(url.searchParams.get("from") ?? 0);
35
+ return streamLogs(jobId, Number.isFinite(from) ? from : 0);
36
+ }
37
+ return errorResponse(404, "not_found", `no route for ${url.pathname}`);
38
+ }
39
+ function streamLogs(jobId, fromSeq) {
40
+ const job = getJob(jobId);
41
+ if (!job)
42
+ return errorResponse(404, "not_found", "no such job");
43
+ const encoder = new TextEncoder();
44
+ let closed = false;
45
+ let unsubscribe;
46
+ const body = new ReadableStream({
47
+ start(controller) {
48
+ const send = (line) => {
49
+ if (closed)
50
+ return;
51
+ try {
52
+ controller.enqueue(encoder.encode(`id: ${line.seq}\ndata: ${JSON.stringify(line)}\n\n`));
53
+ }
54
+ catch {
55
+ closed = true;
56
+ }
57
+ };
58
+ for (const line of getLogsSince(jobId, fromSeq))
59
+ send(line);
60
+ const current = getJob(jobId);
61
+ if (current && ["done", "failed", "canceled"].includes(current.status)) {
62
+ closed = true;
63
+ try {
64
+ controller.close();
65
+ }
66
+ catch {
67
+ // already closed by the client disconnecting
68
+ }
69
+ return;
70
+ }
71
+ unsubscribe = subscribeToLogs(jobId, send);
72
+ },
73
+ cancel() {
74
+ // The client disconnected — stop pushing to it.
75
+ closed = true;
76
+ unsubscribe?.();
77
+ },
78
+ });
79
+ return new Response(body, {
80
+ headers: {
81
+ "content-type": "text/event-stream",
82
+ "cache-control": "no-cache",
83
+ connection: "keep-alive",
84
+ },
85
+ });
86
+ }
@@ -0,0 +1 @@
1
+ export declare function readJsonObject(request: Request): Promise<Record<string, unknown> | Response>;
@@ -0,0 +1,14 @@
1
+ import { errorResponse } from "../errors.js";
2
+ export async function readJsonObject(request) {
3
+ let value;
4
+ try {
5
+ value = await request.json();
6
+ }
7
+ catch {
8
+ return errorResponse(400, "bad_request", "expected a JSON body");
9
+ }
10
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
11
+ return errorResponse(400, "bad_request", "expected a JSON object");
12
+ }
13
+ return value;
14
+ }
@@ -0,0 +1 @@
1
+ export declare function handleRemoteTokensRoute(request: Request, url: URL): Promise<Response>;
@@ -0,0 +1,71 @@
1
+ import { readJsonObject } from "./json_body.js";
2
+ import { errorResponse } from "../errors.js";
3
+ import { createRemoteToken, deleteRemoteToken, listRemoteTokens, setRemoteTokenActive, } from "../../services/remote_tokens.js";
4
+ import { cancelRemoteJobsForToken } from "../../services/remote_token_jobs.js";
5
+ import { remoteTokenUsageSince } from "../../store/reviews.js";
6
+ import { getRemoteToken } from "../../store/remote_tokens.js";
7
+ import { daysAgoIso } from "../../util/time.js";
8
+ export async function handleRemoteTokensRoute(request, url) {
9
+ if (url.pathname === "/api/remote-tokens" && request.method === "GET") {
10
+ const since = daysAgoIso(30);
11
+ const items = listRemoteTokens().map((row) => {
12
+ const usage = remoteTokenUsageSince(row.id, since);
13
+ return {
14
+ id: row.id,
15
+ name: row.name,
16
+ active: row.active === 1,
17
+ createdAt: row.created_at,
18
+ lastUsedAt: row.last_used_at,
19
+ reviews30d: usage.reviews,
20
+ cost30d: usage.cost,
21
+ };
22
+ });
23
+ return Response.json(items);
24
+ }
25
+ if (url.pathname === "/api/remote-tokens" && request.method === "POST") {
26
+ const body = await readJsonObject(request);
27
+ if (body instanceof Response)
28
+ return body;
29
+ if (typeof body.name !== "string") {
30
+ return errorResponse(400, "bad_request", "name is required");
31
+ }
32
+ try {
33
+ const created = await createRemoteToken(body.name);
34
+ return Response.json(created, { status: 201 });
35
+ }
36
+ catch (error) {
37
+ const message = String(error);
38
+ if (message.includes("name_taken")) {
39
+ return errorResponse(409, "name_taken", "token name is already in use");
40
+ }
41
+ return errorResponse(400, "bad_request", message.replace(/^Error: /, ""));
42
+ }
43
+ }
44
+ const itemMatch = /^\/api\/remote-tokens\/([^/]+)$/.exec(url.pathname);
45
+ if (!itemMatch) {
46
+ return errorResponse(404, "not_found", `no route for ${url.pathname}`);
47
+ }
48
+ const id = itemMatch[1];
49
+ const row = getRemoteToken(id);
50
+ if (!row) {
51
+ return errorResponse(404, "not_found", "token not found");
52
+ }
53
+ if (request.method === "PATCH") {
54
+ const body = await readJsonObject(request);
55
+ if (body instanceof Response)
56
+ return body;
57
+ if (typeof body.active !== "boolean") {
58
+ return errorResponse(400, "bad_request", "active must be a boolean");
59
+ }
60
+ setRemoteTokenActive(id, body.active);
61
+ if (!body.active)
62
+ cancelRemoteJobsForToken(id, "token_deactivated");
63
+ return Response.json({ ok: true });
64
+ }
65
+ if (request.method === "DELETE") {
66
+ cancelRemoteJobsForToken(id, "token_deleted");
67
+ deleteRemoteToken(id);
68
+ return new Response(null, { status: 204 });
69
+ }
70
+ return errorResponse(404, "not_found", `no route for ${url.pathname}`);
71
+ }
@@ -0,0 +1,4 @@
1
+ export declare function handleReposRoute(request: Request, url: URL, githubApp?: {
2
+ appId: string;
3
+ privateKeyPem: string;
4
+ }): Promise<Response>;
@@ -0,0 +1,136 @@
1
+ import { errorResponse } from "../errors.js";
2
+ import { findInstallationForRepo } from "../../github/app.js";
3
+ import { activateRepo, deactivateRepo, listActiveRepos, updateRepoSettings, } from "../../store/repos.js";
4
+ import { writeRepoConfig } from "../../config.js";
5
+ import { enqueueSetup } from "../../services/setup.js";
6
+ import { enqueueManualReview } from "../../services/review.js";
7
+ import { prDetail, repoKnowledge, RepoNotFound, repoOverview, repoPulls, requireActiveRepo, } from "../../services/dashboard.js";
8
+ const REPO = /^\/api\/repos\/([^/]+)\/([^/]+)(?:\/(remake|knowledge|pulls)(?:\/(\d+)(?:\/(review))?)?)?$/;
9
+ export async function handleReposRoute(request, url, githubApp) {
10
+ if (url.pathname === "/api/repos" && request.method === "GET") {
11
+ return Response.json({ items: listActiveRepos() });
12
+ }
13
+ if (url.pathname === "/api/repos" && request.method === "POST") {
14
+ let body;
15
+ try {
16
+ body = await request.json();
17
+ }
18
+ catch {
19
+ return errorResponse(400, "bad_request", "expected a JSON body");
20
+ }
21
+ const repo = String(body.repo ?? "");
22
+ if (!/^[^/]+\/[^/]+$/.test(repo)) {
23
+ return errorResponse(400, "bad_request", "repo must look like owner/repo");
24
+ }
25
+ let installationId;
26
+ if (githubApp) {
27
+ try {
28
+ installationId = await findInstallationForRepo(githubApp.appId, githubApp.privateKeyPem, repo);
29
+ }
30
+ catch {
31
+ return errorResponse(422, "app_lookup_failed", "Could not verify the GitHub App's repository access.");
32
+ }
33
+ if (installationId === undefined) {
34
+ return errorResponse(422, "app_access_denied", `The GitHub App cannot access ${repo}.`);
35
+ }
36
+ }
37
+ activateRepo(repo, installationId);
38
+ try {
39
+ const { id } = enqueueSetup(repo, "init");
40
+ return Response.json({ jobId: id });
41
+ }
42
+ catch (error) {
43
+ return errorResponse(422, "setup_incomplete", String(error));
44
+ }
45
+ }
46
+ const match = REPO.exec(url.pathname);
47
+ if (!match) {
48
+ return errorResponse(404, "not_found", `no route for ${url.pathname}`);
49
+ }
50
+ const fullName = `${decodeURIComponent(match[1])}/${decodeURIComponent(match[2])}`;
51
+ const sub = match[3];
52
+ const pr = match[4] ? Number(match[4]) : undefined;
53
+ const action = match[5];
54
+ try {
55
+ if (!sub && request.method === "GET") {
56
+ return Response.json(repoOverview(fullName));
57
+ }
58
+ if (!sub && request.method === "PATCH") {
59
+ requireActiveRepo(fullName);
60
+ let body;
61
+ try {
62
+ body = await request.json();
63
+ }
64
+ catch {
65
+ return errorResponse(400, "bad_request", "expected a JSON body");
66
+ }
67
+ const patch = {};
68
+ if (typeof body.autoReview === "boolean") {
69
+ patch.auto_review = body.autoReview ? 1 : 0;
70
+ }
71
+ if (typeof body.skipDrafts === "boolean") {
72
+ patch.skip_drafts = body.skipDrafts ? 1 : 0;
73
+ }
74
+ if (typeof body.skipBots === "boolean") {
75
+ patch.skip_bots = body.skipBots ? 1 : 0;
76
+ }
77
+ if (typeof body.useCodegraph === "boolean") {
78
+ patch.use_codegraph = body.useCodegraph ? 1 : 0;
79
+ }
80
+ if (body.reviewScope === "whole-pr" ||
81
+ body.reviewScope === "incremental") {
82
+ patch.review_scope = body.reviewScope;
83
+ }
84
+ updateRepoSettings(fullName, patch);
85
+ const init = {};
86
+ for (const key of [
87
+ "maxPrMonths",
88
+ "maxCommits",
89
+ "maxPullRequestChangeLines",
90
+ "maxComments",
91
+ ]) {
92
+ if (typeof body[key] === "number" && Number.isFinite(body[key])) {
93
+ init[key] = body[key];
94
+ }
95
+ }
96
+ if (Object.keys(init).length > 0)
97
+ await writeRepoConfig(fullName, init);
98
+ return Response.json({ ok: true });
99
+ }
100
+ if (!sub && request.method === "DELETE") {
101
+ requireActiveRepo(fullName);
102
+ deactivateRepo(fullName);
103
+ return Response.json({ ok: true });
104
+ }
105
+ if (sub === "remake" && request.method === "POST") {
106
+ const { id } = enqueueSetup(fullName, "remake");
107
+ return Response.json({ jobId: id });
108
+ }
109
+ if (sub === "knowledge" && request.method === "GET") {
110
+ return Response.json(await repoKnowledge(fullName));
111
+ }
112
+ if (sub === "pulls" &&
113
+ pr &&
114
+ action === "review" &&
115
+ request.method === "POST") {
116
+ requireActiveRepo(fullName);
117
+ const { id } = enqueueManualReview(fullName, pr);
118
+ return Response.json({ jobId: id });
119
+ }
120
+ if (sub === "pulls" && pr && request.method === "GET") {
121
+ return Response.json(prDetail(fullName, pr));
122
+ }
123
+ if (sub === "pulls" && request.method === "GET") {
124
+ const page = Number(url.searchParams.get("page") ?? 1);
125
+ const per = Number(url.searchParams.get("per") ?? 20);
126
+ return Response.json(repoPulls(fullName, page, per));
127
+ }
128
+ }
129
+ catch (error) {
130
+ if (error instanceof RepoNotFound) {
131
+ return errorResponse(404, "not_found", error.message);
132
+ }
133
+ return errorResponse(422, "setup_incomplete", String(error));
134
+ }
135
+ return errorResponse(404, "not_found", `no route for ${url.pathname}`);
136
+ }
@@ -0,0 +1 @@
1
+ export declare function handleSettingsRoute(request: Request, url: URL, webhookUrl: string): Promise<Response>;
@@ -0,0 +1,210 @@
1
+ import { errorResponse } from "../errors.js";
2
+ import { readConfig, writeUserConfig } from "../../config.js";
3
+ import { testAppAccess, testGithubAccess } from "../../services/credentials.js";
4
+ function validateWebhookUrl(value) {
5
+ if (typeof value !== "string" || !value.trim()) {
6
+ return "webhook URL is required";
7
+ }
8
+ try {
9
+ const url = new URL(value.trim());
10
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
11
+ return "webhook URL must use http or https";
12
+ }
13
+ }
14
+ catch {
15
+ return "webhook URL must be an absolute http(s) URL";
16
+ }
17
+ return undefined;
18
+ }
19
+ export async function handleSettingsRoute(request, url, webhookUrl) {
20
+ if (url.pathname === "/api/settings" && request.method === "GET") {
21
+ const config = readConfig();
22
+ return Response.json({
23
+ auth: config.auth ?? "gh",
24
+ ai: config.ai ?? "none",
25
+ lowModel: config.lowModel ?? "",
26
+ highModel: config.highModel ?? "",
27
+ githubAppId: config.githubAppId ?? "",
28
+ defaults: config.defaults ?? {},
29
+ hasAiToken: Boolean(config.token),
30
+ hasPat: Boolean(config.githubPat),
31
+ hasAppKey: Boolean(config.githubAppPrivateKey),
32
+ hasWebhookSecret: Boolean(config.githubWebhookSecret),
33
+ webhookUrl: config.webhookUrl || webhookUrl,
34
+ githubOAuthClientId: config.githubOAuthClientId ?? "",
35
+ hasOAuthClientSecret: Boolean(config.githubOAuthClientSecret),
36
+ githubOAuthAllowedUser: config.githubOAuthAllowedUser ?? "",
37
+ passwordAuthDisabled: Boolean(config.passwordAuthDisabled),
38
+ githubAuthEnabled: Boolean(config.githubAuthEnabled),
39
+ maxConcurrentJobs: config.maxConcurrentJobs ?? null,
40
+ remoteSyncTimeoutSeconds: config.remoteSyncTimeoutSeconds ?? null,
41
+ maxConcurrentRemoteReviewsPerToken: config.maxConcurrentRemoteReviewsPerToken ?? null,
42
+ remoteToolOutputMaxChars: config.remoteToolOutputMaxChars ?? null,
43
+ });
44
+ }
45
+ if (url.pathname === "/api/settings" && request.method === "PUT") {
46
+ let body;
47
+ try {
48
+ body = await request.json();
49
+ }
50
+ catch {
51
+ return errorResponse(400, "bad_request", "expected a JSON body");
52
+ }
53
+ const current = readConfig();
54
+ const patch = {};
55
+ const text = (key) => {
56
+ const value = body[key];
57
+ if (typeof value !== "string")
58
+ return;
59
+ if (value === "")
60
+ return;
61
+ patch[key] = value;
62
+ };
63
+ text("auth");
64
+ text("ai");
65
+ text("lowModel");
66
+ text("highModel");
67
+ text("token");
68
+ text("githubPat");
69
+ text("githubAppId");
70
+ text("githubAppPrivateKey");
71
+ text("githubWebhookSecret");
72
+ text("githubOAuthClientId");
73
+ text("githubOAuthClientSecret");
74
+ text("githubOAuthAllowedUser");
75
+ if (typeof body.passwordAuthDisabled === "boolean") {
76
+ patch.passwordAuthDisabled = body.passwordAuthDisabled;
77
+ }
78
+ if (typeof body.githubAuthEnabled === "boolean") {
79
+ patch.githubAuthEnabled = body.githubAuthEnabled;
80
+ }
81
+ if ("webhookUrl" in body) {
82
+ const webhookError = validateWebhookUrl(body.webhookUrl);
83
+ if (webhookError) {
84
+ return errorResponse(400, "invalid_webhook_url", webhookError);
85
+ }
86
+ patch.webhookUrl = body.webhookUrl.trim();
87
+ }
88
+ if ("maxConcurrentJobs" in body) {
89
+ const raw = body.maxConcurrentJobs;
90
+ if (raw === null || raw === "") {
91
+ patch.maxConcurrentJobs = undefined;
92
+ }
93
+ else if (typeof raw === "number" && Number.isInteger(raw) && raw > 0) {
94
+ patch.maxConcurrentJobs = raw;
95
+ }
96
+ else {
97
+ return errorResponse(422, "invalid_setting", "maxConcurrentJobs must be a positive integer or null");
98
+ }
99
+ }
100
+ const positiveIntOrNull = (key, label) => {
101
+ if (!(key in body))
102
+ return;
103
+ const raw = body[key];
104
+ if (raw === null || raw === "") {
105
+ patch[key] = undefined;
106
+ return;
107
+ }
108
+ if (typeof raw === "number" && Number.isInteger(raw) && raw > 0) {
109
+ patch[key] = raw;
110
+ return;
111
+ }
112
+ return errorResponse(422, "invalid_setting", `${label} must be a positive integer or null`);
113
+ };
114
+ const remoteTimeout = positiveIntOrNull("remoteSyncTimeoutSeconds", "remoteSyncTimeoutSeconds");
115
+ if (remoteTimeout)
116
+ return remoteTimeout;
117
+ const remotePerToken = positiveIntOrNull("maxConcurrentRemoteReviewsPerToken", "maxConcurrentRemoteReviewsPerToken");
118
+ if (remotePerToken)
119
+ return remotePerToken;
120
+ const remoteToolOut = positiveIntOrNull("remoteToolOutputMaxChars", "remoteToolOutputMaxChars");
121
+ if (remoteToolOut)
122
+ return remoteToolOut;
123
+ if (body.defaults && typeof body.defaults === "object") {
124
+ const defaults = body.defaults;
125
+ const next = { ...current.defaults };
126
+ const intKeys = [
127
+ "maxPrMonths",
128
+ "maxCommits",
129
+ "maxPullRequestChangeLines",
130
+ ];
131
+ const intKeySet = new Set(intKeys);
132
+ for (const key of intKeys) {
133
+ if (!(key in defaults))
134
+ continue;
135
+ const raw = defaults[key];
136
+ if (raw === null || raw === "") {
137
+ delete next[key];
138
+ continue;
139
+ }
140
+ if (typeof raw === "number" && Number.isInteger(raw) && raw > 0) {
141
+ next[key] = raw;
142
+ continue;
143
+ }
144
+ return errorResponse(422, "invalid_setting", `${key} must be a positive integer or null`);
145
+ }
146
+ for (const [key, value] of Object.entries(defaults)) {
147
+ if (intKeySet.has(key))
148
+ continue;
149
+ if (value === "" || value === null || value === undefined)
150
+ continue;
151
+ next[key] = value;
152
+ }
153
+ patch.defaults = next;
154
+ }
155
+ const merged = { ...current, ...patch };
156
+ if (merged.passwordAuthDisabled && !merged.githubAuthEnabled) {
157
+ return errorResponse(422, "no_auth_method", "at least one sign-in method is required");
158
+ }
159
+ if (merged.githubAuthEnabled &&
160
+ (!merged.githubOAuthClientId ||
161
+ !merged.githubOAuthClientSecret ||
162
+ !merged.githubOAuthAllowedUser)) {
163
+ return errorResponse(422, "oauth_incomplete", "GitHub sign-in needs a client ID, client secret, and allowed username");
164
+ }
165
+ const touchingGithub = patch.auth !== undefined || patch.githubPat !== undefined;
166
+ const touchingApp = patch.githubAppId !== undefined ||
167
+ patch.githubAppPrivateKey !== undefined;
168
+ if (touchingGithub) {
169
+ const github = await testGithubAccess({
170
+ auth: merged.auth === "pat" ? "pat" : "gh",
171
+ githubPat: merged.githubPat,
172
+ });
173
+ if (!github.ok) {
174
+ return errorResponse(422, "github_access", github.message);
175
+ }
176
+ }
177
+ if (touchingApp) {
178
+ const app = await testAppAccess({
179
+ appId: merged.githubAppId ?? "",
180
+ privateKeyPem: merged.githubAppPrivateKey ?? "",
181
+ });
182
+ if (!app.ok) {
183
+ return errorResponse(422, "app_access", app.message);
184
+ }
185
+ }
186
+ await writeUserConfig(patch);
187
+ return Response.json({ ok: true });
188
+ }
189
+ if (url.pathname === "/api/settings/test" && request.method === "POST") {
190
+ const config = readConfig();
191
+ const github = await testGithubAccess({
192
+ auth: config.auth === "pat" ? "pat" : "gh",
193
+ githubPat: config.githubPat,
194
+ });
195
+ const app = await testAppAccess({
196
+ appId: config.githubAppId ?? "",
197
+ privateKeyPem: config.githubAppPrivateKey ?? "",
198
+ });
199
+ return Response.json({
200
+ ai: { ok: Boolean(config.ai && config.ai !== "none" && config.token) },
201
+ github: github.ok
202
+ ? { ok: true, login: github.login }
203
+ : { ok: false, message: github.message },
204
+ app: app.ok
205
+ ? { ok: true, installations: app.installations }
206
+ : { ok: false, message: app.message },
207
+ });
208
+ }
209
+ return errorResponse(404, "not_found", `no route for ${url.pathname}`);
210
+ }
@@ -0,0 +1,27 @@
1
+ import type { AuthMethods } from "./auth.ts";
2
+ /** `password` is the dashboard password `serve` generates or takes via
3
+ * `--password`. `githubApp` is unset until `co-maintainer set` has both
4
+ * the App ID and private key. */
5
+ export type AppDeps = {
6
+ password: string;
7
+ githubApp?: {
8
+ appId: string;
9
+ privateKeyPem: string;
10
+ };
11
+ webhookSecret?: string;
12
+ webhookUrl?: string;
13
+ secureCookie?: boolean;
14
+ inject500?: boolean;
15
+ /** Defaults to password-only when omitted, matching every caller that
16
+ * pre-dates GitHub sign-in. */
17
+ auth?: AuthMethods;
18
+ githubOAuth?: {
19
+ clientId: string;
20
+ clientSecret: string;
21
+ allowedUser: string;
22
+ };
23
+ };
24
+ export type App = {
25
+ fetch(request: Request, remoteAddr?: string): Promise<Response>;
26
+ };
27
+ export declare function createApp(deps: AppDeps): App;