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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { readConfig } from "../config.js";
|
|
2
|
+
import { claimJob, getJob, getQueuedJobByKey, getRunningJobByKey, insertJob, listJobs, setJobStatus, } from "../store/jobs.js";
|
|
3
|
+
import { appendLog as storeAppendLog, listLogs } from "../store/job_logs.js";
|
|
4
|
+
import { redact } from "../util/redact.js";
|
|
5
|
+
import { abortQueuedRemoteReviewRow } from "./remote_review_abort.js";
|
|
6
|
+
import { getReviewByJobId } from "../store/reviews.js";
|
|
7
|
+
const handlers = new Map();
|
|
8
|
+
export function registerHandler(type, handler) {
|
|
9
|
+
handlers.set(type, handler);
|
|
10
|
+
}
|
|
11
|
+
const subscribers = new Map();
|
|
12
|
+
export function subscribeToLogs(jobId, fn) {
|
|
13
|
+
const set = subscribers.get(jobId) ?? new Set();
|
|
14
|
+
set.add(fn);
|
|
15
|
+
subscribers.set(jobId, set);
|
|
16
|
+
return () => set.delete(fn);
|
|
17
|
+
}
|
|
18
|
+
function log(jobId, level, message) {
|
|
19
|
+
const clean = redact(message);
|
|
20
|
+
const seq = storeAppendLog(jobId, level, clean);
|
|
21
|
+
const line = {
|
|
22
|
+
job_id: jobId,
|
|
23
|
+
seq,
|
|
24
|
+
at: new Date().toISOString(),
|
|
25
|
+
level,
|
|
26
|
+
message: clean,
|
|
27
|
+
};
|
|
28
|
+
for (const fn of subscribers.get(jobId) ?? [])
|
|
29
|
+
fn(line);
|
|
30
|
+
}
|
|
31
|
+
export function getLogsSince(jobId, fromSeq = 0) {
|
|
32
|
+
return listLogs(jobId, fromSeq);
|
|
33
|
+
}
|
|
34
|
+
export function enqueue(input) {
|
|
35
|
+
const queueKey = input.queueKey ??
|
|
36
|
+
(input.type === "review" && input.prNumber !== undefined
|
|
37
|
+
? `review:${input.repo}:${input.prNumber}`
|
|
38
|
+
: undefined);
|
|
39
|
+
if (queueKey) {
|
|
40
|
+
const existing = getQueuedJobByKey(queueKey);
|
|
41
|
+
if (existing) {
|
|
42
|
+
const age = Date.now() - Date.parse(existing.created_at);
|
|
43
|
+
if (input.debounceMs && age < input.debounceMs) {
|
|
44
|
+
return { id: existing.id, debounced: true };
|
|
45
|
+
}
|
|
46
|
+
const id = crypto.randomUUID();
|
|
47
|
+
setJobStatus(existing.id, "canceled", {
|
|
48
|
+
superseded_by: id,
|
|
49
|
+
cancel_reason: "superseded",
|
|
50
|
+
});
|
|
51
|
+
log(existing.id, "info", `superseded by ${id}`);
|
|
52
|
+
insertJob({ ...input, id, queueKey });
|
|
53
|
+
return { id, debounced: false };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const id = crypto.randomUUID();
|
|
57
|
+
insertJob({ ...input, id, queueKey });
|
|
58
|
+
return { id, debounced: false };
|
|
59
|
+
}
|
|
60
|
+
const running = new Map();
|
|
61
|
+
const pendingCancelReason = new Map();
|
|
62
|
+
async function runJob(job) {
|
|
63
|
+
const handler = handlers.get(job.type);
|
|
64
|
+
const logFn = (level, message) => log(job.id, level, message);
|
|
65
|
+
if (!handler) {
|
|
66
|
+
setJobStatus(job.id, "failed", {
|
|
67
|
+
error: `no handler registered for job type ${job.type}`,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
running.set(job.id, controller);
|
|
73
|
+
try {
|
|
74
|
+
await handler.run(job, logFn, controller.signal);
|
|
75
|
+
const status = controller.signal.aborted ? "canceled" : "done";
|
|
76
|
+
const cancelReason = pendingCancelReason.get(job.id);
|
|
77
|
+
setJobStatus(job.id, status, cancelReason ? { cancel_reason: cancelReason } : {});
|
|
78
|
+
log(job.id, "status", status);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const status = controller.signal.aborted ? "canceled" : "failed";
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
const cancelReason = pendingCancelReason.get(job.id);
|
|
84
|
+
setJobStatus(job.id, status, controller.signal.aborted
|
|
85
|
+
? cancelReason
|
|
86
|
+
? { cancel_reason: cancelReason }
|
|
87
|
+
: {}
|
|
88
|
+
: { error: String(error) });
|
|
89
|
+
if (status === "failed")
|
|
90
|
+
log(job.id, "error", message);
|
|
91
|
+
log(job.id, "status", status);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
running.delete(job.id);
|
|
95
|
+
pendingCancelReason.delete(job.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function runningRemoteReviewsPerToken() {
|
|
99
|
+
const counts = new Map();
|
|
100
|
+
for (const job of listJobs({ status: "running" })) {
|
|
101
|
+
if (job.type !== "remote_review")
|
|
102
|
+
continue;
|
|
103
|
+
const review = getReviewByJobId(job.id);
|
|
104
|
+
if (!review?.token_id)
|
|
105
|
+
continue;
|
|
106
|
+
counts.set(review.token_id, (counts.get(review.token_id) ?? 0) + 1);
|
|
107
|
+
}
|
|
108
|
+
return counts;
|
|
109
|
+
}
|
|
110
|
+
function claimNextEligible() {
|
|
111
|
+
const config = readConfig();
|
|
112
|
+
const perTokenLimit = config.maxConcurrentRemoteReviewsPerToken;
|
|
113
|
+
const remoteRunning = perTokenLimit
|
|
114
|
+
? runningRemoteReviewsPerToken()
|
|
115
|
+
: undefined;
|
|
116
|
+
const oldestFirst = listJobs({ status: "queued" }).slice().reverse();
|
|
117
|
+
for (const candidate of oldestFirst) {
|
|
118
|
+
if (!handlers.has(candidate.type))
|
|
119
|
+
continue;
|
|
120
|
+
if (candidate.queue_key) {
|
|
121
|
+
const busy = getRunningJobByKey(candidate.queue_key);
|
|
122
|
+
if (busy)
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (perTokenLimit && candidate.type === "remote_review") {
|
|
126
|
+
const review = getReviewByJobId(candidate.id);
|
|
127
|
+
const tokenId = review?.token_id;
|
|
128
|
+
if (tokenId && (remoteRunning.get(tokenId) ?? 0) >= perTokenLimit) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!claimJob(candidate.id))
|
|
133
|
+
continue;
|
|
134
|
+
if (perTokenLimit && candidate.type === "remote_review") {
|
|
135
|
+
const review = getReviewByJobId(candidate.id);
|
|
136
|
+
if (review?.token_id) {
|
|
137
|
+
remoteRunning.set(review.token_id, (remoteRunning.get(review.token_id) ?? 0) + 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return getJob(candidate.id);
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
export async function claimAndRun() {
|
|
145
|
+
const limit = readConfig().maxConcurrentJobs;
|
|
146
|
+
if (limit && inFlight.size >= limit)
|
|
147
|
+
return false;
|
|
148
|
+
const job = claimNextEligible();
|
|
149
|
+
if (!job)
|
|
150
|
+
return false;
|
|
151
|
+
const run = runJob(job).finally(() => inFlight.delete(run));
|
|
152
|
+
inFlight.add(run);
|
|
153
|
+
await run;
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
const inFlight = new Set();
|
|
157
|
+
let workerTimer;
|
|
158
|
+
function tick() {
|
|
159
|
+
const limit = readConfig().maxConcurrentJobs;
|
|
160
|
+
while (!limit || inFlight.size < limit) {
|
|
161
|
+
const job = claimNextEligible();
|
|
162
|
+
if (!job)
|
|
163
|
+
break;
|
|
164
|
+
const p = runJob(job).finally(() => {
|
|
165
|
+
inFlight.delete(p);
|
|
166
|
+
});
|
|
167
|
+
inFlight.add(p);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function startWorkerLoop(intervalMs = 1000) {
|
|
171
|
+
if (workerTimer !== undefined)
|
|
172
|
+
return;
|
|
173
|
+
workerTimer = setInterval(() => tick(), intervalMs);
|
|
174
|
+
tick();
|
|
175
|
+
}
|
|
176
|
+
export async function stopWorkerLoop() {
|
|
177
|
+
if (workerTimer !== undefined) {
|
|
178
|
+
clearInterval(workerTimer);
|
|
179
|
+
workerTimer = undefined;
|
|
180
|
+
}
|
|
181
|
+
await Promise.allSettled([...inFlight]);
|
|
182
|
+
}
|
|
183
|
+
/** Cooperative: aborts a running job's `AbortSignal` if this process is
|
|
184
|
+
* running it, or cancels it outright if it is merely queued. Returns false
|
|
185
|
+
* if neither applies (already finished, or running in another process). */
|
|
186
|
+
export function cancel(jobId, reason = "dashboard_canceled") {
|
|
187
|
+
const controller = running.get(jobId);
|
|
188
|
+
if (controller) {
|
|
189
|
+
pendingCancelReason.set(jobId, reason);
|
|
190
|
+
controller.abort();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const job = getJob(jobId);
|
|
194
|
+
if (job?.status === "queued") {
|
|
195
|
+
abortQueuedRemoteReviewRow(jobId);
|
|
196
|
+
setJobStatus(jobId, "canceled", { cancel_reason: reason });
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
export async function recoverOrphans() {
|
|
202
|
+
const orphans = listJobs({ status: "running" });
|
|
203
|
+
for (const job of orphans) {
|
|
204
|
+
const handler = handlers.get(job.type);
|
|
205
|
+
const logFn = (level, message) => log(job.id, level, message);
|
|
206
|
+
if (handler?.reconcile) {
|
|
207
|
+
logFn("info", "recovering after a restart: reconciling instead of rerunning");
|
|
208
|
+
try {
|
|
209
|
+
const outcome = await handler.reconcile(job, logFn);
|
|
210
|
+
setJobStatus(job.id, outcome === "canceled" ? "canceled" : "done", outcome === "canceled" ? { cancel_reason: "server_restarted" } : {});
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
setJobStatus(job.id, "failed", { error: String(error) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
logFn("info", "recovering after a restart: requeued from the top");
|
|
218
|
+
setJobStatus(job.id, "queued");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return orphans.length;
|
|
222
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RemoteTokenRow } from "../store/rows.ts";
|
|
2
|
+
export declare function submitRemoteReview(token: RemoteTokenRow, body: Record<string, unknown>): {
|
|
3
|
+
jobId: string;
|
|
4
|
+
reviewId: string;
|
|
5
|
+
} | {
|
|
6
|
+
error: string;
|
|
7
|
+
status: number;
|
|
8
|
+
code: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function registerRemoteReviewHandler(): void;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { VERSION } from "../version.js";
|
|
2
|
+
import { resolvedFromFirstReview, revisionStats, sortResolvedFindings, summaryCounts, toJsonFinding, } from "../cli/review_result.js";
|
|
3
|
+
import { parseFindings } from "../pr/findings.js";
|
|
4
|
+
import { codegraphToolHandlersForBridge } from "../pr/codegraph_tools.js";
|
|
5
|
+
import { reviewWorkspaceRevision } from "../pr/reviewer.js";
|
|
6
|
+
import { buildCarryPromptSection, classifyCarryItems, incrementalDiffPaths, } from "../review/carry_over.js";
|
|
7
|
+
import { remoteBridgeToolHandlers } from "../remote/tool_bridge.js";
|
|
8
|
+
import { revisionFromSubmitJson } from "../remote/revision_from_submit.js";
|
|
9
|
+
import { closeRemoteSession, openRemoteSession, setRemoteSyncResult, } from "../remote/server/sessions.js";
|
|
10
|
+
import { readConfig } from "../config.js";
|
|
11
|
+
import { withLogSink } from "../util/log.js";
|
|
12
|
+
import { cancel, registerHandler } from "./jobs.js";
|
|
13
|
+
import { recordAiCost } from "./setup.js";
|
|
14
|
+
import { findRepoByFullName } from "../store/repos.js";
|
|
15
|
+
import { deleteRemoteReviewInput, findRemoteReviewInputByRequest, getRemoteReviewInput, insertRemoteReviewInput, } from "../store/remote_review_inputs.js";
|
|
16
|
+
import { insertFinding, listCarryableFindingsForReview, } from "../store/findings.js";
|
|
17
|
+
import { deleteSubjectRevision, getOrCreateRemoteSubject, getSubjectRevision, parseRevisionFiles, parseVisiblePaths, saveSubjectRevision, } from "../store/subjects.js";
|
|
18
|
+
import { getReviewByJobId, insertRemoteReview, setReviewStatus, } from "../store/reviews.js";
|
|
19
|
+
import { insertJob, listJobsByQueueKey, setJobStatus } from "../store/jobs.js";
|
|
20
|
+
const REPO_UNAVAILABLE = "Sorry, we could not access this repository.";
|
|
21
|
+
function remoteQueueKey(repo, branch, tokenId) {
|
|
22
|
+
return `remote:${repo}:${branch}:${tokenId}`;
|
|
23
|
+
}
|
|
24
|
+
function reviewOptionsForRepo(repo, useCodegraph) {
|
|
25
|
+
const config = readConfig();
|
|
26
|
+
return {
|
|
27
|
+
command: "review",
|
|
28
|
+
repo,
|
|
29
|
+
debug: false,
|
|
30
|
+
logTime: false,
|
|
31
|
+
useCodegraph,
|
|
32
|
+
improveMatrix: 1,
|
|
33
|
+
ghConcurrent: 1,
|
|
34
|
+
aiConcurrent: 3,
|
|
35
|
+
auth: config.auth ?? "gh",
|
|
36
|
+
githubPat: config.githubPat,
|
|
37
|
+
ai: config.ai ?? "openrouter",
|
|
38
|
+
aiToken: config.token,
|
|
39
|
+
lowModel: config.lowModel,
|
|
40
|
+
highModel: config.highModel,
|
|
41
|
+
synthesisVersion: 16,
|
|
42
|
+
includeCodebase: true,
|
|
43
|
+
includePullRequests: true,
|
|
44
|
+
includePullRequestChanges: true,
|
|
45
|
+
includeCommitHistory: true,
|
|
46
|
+
includeHowRepoWorks: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function supersedeActiveRemoteJobs(queueKey, supersededBy) {
|
|
50
|
+
for (const job of listJobsByQueueKey(queueKey, ["queued", "running"])) {
|
|
51
|
+
if (job.id === supersededBy)
|
|
52
|
+
continue;
|
|
53
|
+
if (job.status === "running") {
|
|
54
|
+
cancel(job.id, "superseded");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
setJobStatus(job.id, "canceled", {
|
|
58
|
+
superseded_by: supersededBy,
|
|
59
|
+
cancel_reason: "superseded",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function submitRemoteReview(token, body) {
|
|
65
|
+
const requestId = String(body.requestId);
|
|
66
|
+
const existing = findRemoteReviewInputByRequest(token.id, requestId);
|
|
67
|
+
if (existing) {
|
|
68
|
+
const review = getReviewByJobId(existing.job_id);
|
|
69
|
+
if (review) {
|
|
70
|
+
return { jobId: existing.job_id, reviewId: review.id };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const repoRow = findRepoByFullName(String(body.repo));
|
|
74
|
+
if (!repoRow || repoRow.active !== 1) {
|
|
75
|
+
return { error: REPO_UNAVAILABLE, status: 404, code: "repo_unavailable" };
|
|
76
|
+
}
|
|
77
|
+
if (typeof body.branch !== "string") {
|
|
78
|
+
return {
|
|
79
|
+
error: "branch must be a string",
|
|
80
|
+
status: 400,
|
|
81
|
+
code: "bad_request",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const branch = body.branch;
|
|
85
|
+
const fresh = body.fresh === true;
|
|
86
|
+
const subject = getOrCreateRemoteSubject(repoRow.full_name, branch, token.id);
|
|
87
|
+
if (fresh)
|
|
88
|
+
deleteSubjectRevision(subject.id);
|
|
89
|
+
const jobId = crypto.randomUUID();
|
|
90
|
+
const reviewId = crypto.randomUUID();
|
|
91
|
+
const queueKey = remoteQueueKey(repoRow.full_name, branch, token.id);
|
|
92
|
+
supersedeActiveRemoteJobs(queueKey, jobId);
|
|
93
|
+
const revision = body.revision ?? {};
|
|
94
|
+
const capabilities = body.capabilities ?? { tools: [] };
|
|
95
|
+
insertRemoteReviewInput({
|
|
96
|
+
jobId,
|
|
97
|
+
revisionJson: JSON.stringify(revision),
|
|
98
|
+
capabilitiesJson: JSON.stringify(capabilities),
|
|
99
|
+
requestId,
|
|
100
|
+
tokenId: token.id,
|
|
101
|
+
});
|
|
102
|
+
const config = readConfig();
|
|
103
|
+
insertRemoteReview({
|
|
104
|
+
id: reviewId,
|
|
105
|
+
subjectId: subject.id,
|
|
106
|
+
repo: repoRow.full_name,
|
|
107
|
+
branch,
|
|
108
|
+
tokenId: token.id,
|
|
109
|
+
tokenName: token.name,
|
|
110
|
+
jobId,
|
|
111
|
+
scope: "remote",
|
|
112
|
+
model: config.highModel ?? "unknown",
|
|
113
|
+
});
|
|
114
|
+
insertJob({
|
|
115
|
+
id: jobId,
|
|
116
|
+
type: "remote_review",
|
|
117
|
+
repo: repoRow.full_name,
|
|
118
|
+
queueKey,
|
|
119
|
+
args: { subjectId: subject.id, requestId, reviewId },
|
|
120
|
+
});
|
|
121
|
+
openRemoteSession({
|
|
122
|
+
jobId,
|
|
123
|
+
reviewId,
|
|
124
|
+
tokenId: token.id,
|
|
125
|
+
subjectId: subject.id,
|
|
126
|
+
});
|
|
127
|
+
return { jobId, reviewId };
|
|
128
|
+
}
|
|
129
|
+
export function registerRemoteReviewHandler() {
|
|
130
|
+
registerHandler("remote_review", {
|
|
131
|
+
async run(job, log, signal) {
|
|
132
|
+
const args = JSON.parse(job.args);
|
|
133
|
+
const reviewId = args.reviewId;
|
|
134
|
+
const subjectId = args.subjectId;
|
|
135
|
+
if (!reviewId)
|
|
136
|
+
throw new Error("remote review missing reviewId");
|
|
137
|
+
const input = getRemoteReviewInput(job.id);
|
|
138
|
+
if (!input)
|
|
139
|
+
throw new Error("remote review input missing");
|
|
140
|
+
const review = getReviewByJobId(job.id);
|
|
141
|
+
if (!review)
|
|
142
|
+
throw new Error("remote review row missing");
|
|
143
|
+
setReviewStatus(reviewId, "running");
|
|
144
|
+
log("info", `remote review started for ${job.repo}`);
|
|
145
|
+
let completed = false;
|
|
146
|
+
try {
|
|
147
|
+
const revision = revisionFromSubmitJson(JSON.parse(input.revision_json));
|
|
148
|
+
const caps = JSON.parse(input.capabilities_json);
|
|
149
|
+
const allowed = new Set((caps.tools ?? [])
|
|
150
|
+
.map((tool) => tool.name)
|
|
151
|
+
.filter((name) => typeof name === "string" && name));
|
|
152
|
+
const useCodegraph = allowed.size > 0;
|
|
153
|
+
const options = reviewOptionsForRepo(job.repo, useCodegraph);
|
|
154
|
+
if (!options.aiToken || options.ai === "none") {
|
|
155
|
+
throw new Error("server AI is not configured for remote review");
|
|
156
|
+
}
|
|
157
|
+
const extras = {};
|
|
158
|
+
if (subjectId) {
|
|
159
|
+
const subjectRevision = getSubjectRevision(subjectId);
|
|
160
|
+
if (subjectRevision) {
|
|
161
|
+
const prevFiles = parseRevisionFiles(subjectRevision);
|
|
162
|
+
const { unchanged } = incrementalDiffPaths(revision, prevFiles);
|
|
163
|
+
extras.unchangedPaths = unchanged;
|
|
164
|
+
const storedRows = listCarryableFindingsForReview(subjectRevision.review_id);
|
|
165
|
+
const carryPrevious = {
|
|
166
|
+
files: prevFiles,
|
|
167
|
+
visiblePaths: parseVisiblePaths(subjectRevision),
|
|
168
|
+
findings: storedRows.map((row) => ({
|
|
169
|
+
id: row.id,
|
|
170
|
+
path: row.path,
|
|
171
|
+
lineFrom: row.line_from,
|
|
172
|
+
lineTo: row.line_to,
|
|
173
|
+
title: row.title,
|
|
174
|
+
bodyMd: row.body_md,
|
|
175
|
+
anchorText: row.anchor_text,
|
|
176
|
+
severity: row.severity,
|
|
177
|
+
firstSeenReviewId: row.first_seen_review_id ?? subjectRevision.review_id,
|
|
178
|
+
})),
|
|
179
|
+
guideBuiltAt: subjectRevision.guide_built_at,
|
|
180
|
+
};
|
|
181
|
+
const carryItems = classifyCarryItems(carryPrevious, revision, carryPrevious.visiblePaths, null);
|
|
182
|
+
extras.carryPrompt = buildCarryPromptSection(carryItems, revision);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (useCodegraph) {
|
|
186
|
+
const schemas = codegraphToolHandlersForBridge();
|
|
187
|
+
extras.prepareCodegraphTools = async () => remoteBridgeToolHandlers(job.id, signal, allowed, schemas);
|
|
188
|
+
}
|
|
189
|
+
const started = performance.now();
|
|
190
|
+
let tokensIn = 0;
|
|
191
|
+
let tokensOut = 0;
|
|
192
|
+
let costUsd = 0;
|
|
193
|
+
let costKnown = true;
|
|
194
|
+
const result = await withLogSink((phase, message) => log("info", `[${phase}] ${message}`), () => reviewWorkspaceRevision(revision, options, "remote", async (response) => {
|
|
195
|
+
tokensIn += response.tokensIn;
|
|
196
|
+
tokensOut += response.tokensOut;
|
|
197
|
+
if (response.cost === undefined)
|
|
198
|
+
costKnown = false;
|
|
199
|
+
else
|
|
200
|
+
costUsd = (costUsd ?? 0) + response.cost;
|
|
201
|
+
await recordAiCost(job.repo, "remote_review", response);
|
|
202
|
+
}, undefined, (message) => log("info", message), extras));
|
|
203
|
+
if (signal.aborted)
|
|
204
|
+
return;
|
|
205
|
+
const parsed = parseFindings(result.text);
|
|
206
|
+
const filesByPath = new Map(revision.files.map((file) => [file.path, { patch: file.patch }]));
|
|
207
|
+
const findings = sortResolvedFindings(resolvedFromFirstReview(parsed, filesByPath));
|
|
208
|
+
const durationMs = Math.round(performance.now() - started);
|
|
209
|
+
setRemoteSyncResult(job.id, {
|
|
210
|
+
subject: {
|
|
211
|
+
repo: job.repo,
|
|
212
|
+
branch: review.branch,
|
|
213
|
+
prNumber: null,
|
|
214
|
+
},
|
|
215
|
+
revision: revisionStats(revision),
|
|
216
|
+
guide: { builtAt: result.guideBuiltAt },
|
|
217
|
+
summary: summaryCounts(findings),
|
|
218
|
+
findings: findings.map(toJsonFinding),
|
|
219
|
+
usage: {
|
|
220
|
+
tokensIn,
|
|
221
|
+
tokensOut,
|
|
222
|
+
costUsd: costKnown ? costUsd : null,
|
|
223
|
+
},
|
|
224
|
+
remote: { jobId: job.id, reviewId, clientVersion: VERSION },
|
|
225
|
+
});
|
|
226
|
+
setReviewStatus(reviewId, "done", {
|
|
227
|
+
findings_count: findings.length,
|
|
228
|
+
guide_built_at: result.guideBuiltAt,
|
|
229
|
+
duration_ms: durationMs,
|
|
230
|
+
tokens_in: tokensIn,
|
|
231
|
+
tokens_out: tokensOut,
|
|
232
|
+
cost: costKnown ? costUsd : null,
|
|
233
|
+
});
|
|
234
|
+
if (subjectId) {
|
|
235
|
+
saveSubjectRevision({
|
|
236
|
+
subjectId,
|
|
237
|
+
reviewId,
|
|
238
|
+
files: revision.files,
|
|
239
|
+
visiblePaths: [...result.visiblePaths],
|
|
240
|
+
guideBuiltAt: result.guideBuiltAt,
|
|
241
|
+
});
|
|
242
|
+
for (const row of findings) {
|
|
243
|
+
insertFinding({
|
|
244
|
+
id: row.id,
|
|
245
|
+
reviewId,
|
|
246
|
+
severity: row.severity,
|
|
247
|
+
path: row.path ?? undefined,
|
|
248
|
+
lineFrom: row.lineFrom ?? undefined,
|
|
249
|
+
lineTo: row.lineTo ?? undefined,
|
|
250
|
+
title: row.title,
|
|
251
|
+
bodyMd: row.bodyMd,
|
|
252
|
+
anchorText: row.anchorText,
|
|
253
|
+
state: row.state,
|
|
254
|
+
closeReason: row.closeReason ?? null,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
deleteRemoteReviewInput(job.id);
|
|
259
|
+
closeRemoteSession(job.id);
|
|
260
|
+
completed = true;
|
|
261
|
+
log("info", `remote review finished (${findings.length} findings)`);
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
if (!completed) {
|
|
265
|
+
if (signal.aborted) {
|
|
266
|
+
setReviewStatus(reviewId, "aborted");
|
|
267
|
+
}
|
|
268
|
+
deleteRemoteReviewInput(job.id);
|
|
269
|
+
closeRemoteSession(job.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getReviewByJobId, setReviewStatus } from "../store/reviews.js";
|
|
2
|
+
/** Queued remote reviews never enter the handler; sync the review row when the
|
|
3
|
+
* job is canceled from the dashboard or token deactivation. */
|
|
4
|
+
export function abortQueuedRemoteReviewRow(jobId) {
|
|
5
|
+
const review = getReviewByJobId(jobId);
|
|
6
|
+
if (!review || review.kind !== "remote")
|
|
7
|
+
return;
|
|
8
|
+
if (review.status !== "queued")
|
|
9
|
+
return;
|
|
10
|
+
setReviewStatus(review.id, "aborted");
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cancelRemoteJobsForToken(tokenId: string, reason: "token_deactivated" | "token_deleted"): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cancel } from "./jobs.js";
|
|
2
|
+
import { abortQueuedRemoteReviewRow } from "./remote_review_abort.js";
|
|
3
|
+
import { listJobs, setJobStatus } from "../store/jobs.js";
|
|
4
|
+
import { getReviewByJobId } from "../store/reviews.js";
|
|
5
|
+
export function cancelRemoteJobsForToken(tokenId, reason) {
|
|
6
|
+
for (const job of listJobs()) {
|
|
7
|
+
if (job.type !== "remote_review")
|
|
8
|
+
continue;
|
|
9
|
+
const review = getReviewByJobId(job.id);
|
|
10
|
+
if (!review || review.token_id !== tokenId)
|
|
11
|
+
continue;
|
|
12
|
+
if (job.status === "queued") {
|
|
13
|
+
setJobStatus(job.id, "canceled", { cancel_reason: reason });
|
|
14
|
+
abortQueuedRemoteReviewRow(job.id);
|
|
15
|
+
}
|
|
16
|
+
else if (job.status === "running") {
|
|
17
|
+
cancel(job.id, reason);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { deleteRemoteToken, listRemoteTokens, setRemoteTokenActive, touchRemoteToken } from "../store/remote_tokens.ts";
|
|
2
|
+
import type { RemoteTokenRow } from "../store/rows.ts";
|
|
3
|
+
export declare function generateRemoteBearerToken(): string;
|
|
4
|
+
export declare function hashRemoteBearerToken(token: string): Promise<string>;
|
|
5
|
+
export declare function validateRemoteTokenName(name: string): string | null;
|
|
6
|
+
export declare function createRemoteToken(name: string): Promise<{
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
token: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function resolveRemoteToken(bearer: string): Promise<RemoteTokenRow | undefined>;
|
|
12
|
+
export declare function lookupRemoteBearer(bearer: string): Promise<RemoteTokenRow | "invalid" | "inactive">;
|
|
13
|
+
export { deleteRemoteToken, listRemoteTokens, setRemoteTokenActive, touchRemoteToken, };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { deleteRemoteToken, findRemoteTokenByHash, findRemoteTokenByName, insertRemoteToken, listRemoteTokens, setRemoteTokenActive, touchRemoteToken, } from "../store/remote_tokens.js";
|
|
2
|
+
const NAME_RE = /^[\w][\w .-]{0,63}$/;
|
|
3
|
+
async function sha256Hex(value) {
|
|
4
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
5
|
+
return [...new Uint8Array(bytes)]
|
|
6
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
7
|
+
.join("");
|
|
8
|
+
}
|
|
9
|
+
function base64Url(bytes) {
|
|
10
|
+
const bin = [...bytes].map((b) => String.fromCharCode(b)).join("");
|
|
11
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12
|
+
}
|
|
13
|
+
export function generateRemoteBearerToken() {
|
|
14
|
+
const bytes = new Uint8Array(32);
|
|
15
|
+
crypto.getRandomValues(bytes);
|
|
16
|
+
return `cmr_${base64Url(bytes)}`;
|
|
17
|
+
}
|
|
18
|
+
export async function hashRemoteBearerToken(token) {
|
|
19
|
+
return sha256Hex(token);
|
|
20
|
+
}
|
|
21
|
+
export function validateRemoteTokenName(name) {
|
|
22
|
+
const trimmed = name.trim();
|
|
23
|
+
if (!NAME_RE.test(trimmed)) {
|
|
24
|
+
return "name must be 1–64 characters (letters, numbers, spaces, . -)";
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export async function createRemoteToken(name) {
|
|
29
|
+
const err = validateRemoteTokenName(name);
|
|
30
|
+
if (err)
|
|
31
|
+
throw new Error(err);
|
|
32
|
+
const trimmed = name.trim();
|
|
33
|
+
if (findRemoteTokenByName(trimmed)) {
|
|
34
|
+
throw new Error("name_taken");
|
|
35
|
+
}
|
|
36
|
+
const id = crypto.randomUUID();
|
|
37
|
+
const token = generateRemoteBearerToken();
|
|
38
|
+
const tokenHash = await hashRemoteBearerToken(token);
|
|
39
|
+
insertRemoteToken(id, trimmed, tokenHash);
|
|
40
|
+
return { id, name: trimmed, token };
|
|
41
|
+
}
|
|
42
|
+
export async function resolveRemoteToken(bearer) {
|
|
43
|
+
const status = await lookupRemoteBearer(bearer);
|
|
44
|
+
return status === "invalid" || status === "inactive" ? undefined : status;
|
|
45
|
+
}
|
|
46
|
+
export async function lookupRemoteBearer(bearer) {
|
|
47
|
+
const token = bearer.startsWith("cmr_") ? bearer : `cmr_${bearer}`;
|
|
48
|
+
const hash = await hashRemoteBearerToken(token);
|
|
49
|
+
const row = findRemoteTokenByHash(hash);
|
|
50
|
+
if (!row)
|
|
51
|
+
return "invalid";
|
|
52
|
+
if (row.active !== 1)
|
|
53
|
+
return "inactive";
|
|
54
|
+
touchRemoteToken(row.id);
|
|
55
|
+
return row;
|
|
56
|
+
}
|
|
57
|
+
export { deleteRemoteToken, listRemoteTokens, setRemoteTokenActive, touchRemoteToken, };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AiProvider, GitHubClient } from "../types.ts";
|
|
2
|
+
import { type LogFn } from "./jobs.ts";
|
|
3
|
+
import type { JobRow } from "../store/rows.ts";
|
|
4
|
+
export declare function runReplyJob(job: JobRow, log: LogFn, client?: GitHubClient, ai?: AiProvider): Promise<void>;
|
|
5
|
+
export declare function reconcileReply(job: JobRow, log: LogFn): Promise<void>;
|
|
6
|
+
export declare function recoverReplyRequests(): number;
|
|
7
|
+
export declare function registerReplyJobHandler(): void;
|