@gaud_erp/paperclip-github-manager 0.3.0 → 1.0.0
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/README.md +106 -43
- package/dist/manifest.js +153 -68
- package/dist/manifest.js.map +3 -3
- package/dist/ui/index.js +671 -542
- package/dist/ui/index.js.map +4 -4
- package/dist/worker.js +1045 -450
- package/dist/worker.js.map +4 -4
- package/package.json +28 -40
- package/src/db/migrations/001_initial.sql +70 -0
package/dist/worker.js
CHANGED
|
@@ -1,9 +1,264 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
2
6
|
var __export = (target, all) => {
|
|
3
7
|
for (var name in all)
|
|
4
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
9
|
};
|
|
6
10
|
|
|
11
|
+
// src/db/queries.ts
|
|
12
|
+
var queries_exports = {};
|
|
13
|
+
__export(queries_exports, {
|
|
14
|
+
completeSyncLog: () => completeSyncLog,
|
|
15
|
+
createSyncLog: () => createSyncLog,
|
|
16
|
+
getLastSyncTime: () => getLastSyncTime,
|
|
17
|
+
getLinksForCard: () => getLinksForCard,
|
|
18
|
+
getLinksForPR: () => getLinksForPR,
|
|
19
|
+
getPRByRepoAndNumber: () => getPRByRepoAndNumber,
|
|
20
|
+
getRepoByFullName: () => getRepoByFullName,
|
|
21
|
+
linkPRToCard: () => linkPRToCard,
|
|
22
|
+
listPRs: () => listPRs,
|
|
23
|
+
listRepos: () => listRepos,
|
|
24
|
+
upsertIssue: () => upsertIssue,
|
|
25
|
+
upsertPR: () => upsertPR,
|
|
26
|
+
upsertRepo: () => upsertRepo
|
|
27
|
+
});
|
|
28
|
+
async function upsertRepo(db, repo) {
|
|
29
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
30
|
+
await db.execute(
|
|
31
|
+
`INSERT INTO ${S}.gh_repositories (id, full_name, owner, name, private, default_branch, html_url, description, language, topics, updated_at, synced_at)
|
|
32
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
33
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
34
|
+
full_name = EXCLUDED.full_name,
|
|
35
|
+
owner = EXCLUDED.owner,
|
|
36
|
+
name = EXCLUDED.name,
|
|
37
|
+
private = EXCLUDED.private,
|
|
38
|
+
default_branch = EXCLUDED.default_branch,
|
|
39
|
+
html_url = EXCLUDED.html_url,
|
|
40
|
+
description = EXCLUDED.description,
|
|
41
|
+
language = EXCLUDED.language,
|
|
42
|
+
topics = EXCLUDED.topics,
|
|
43
|
+
updated_at = EXCLUDED.updated_at,
|
|
44
|
+
synced_at = EXCLUDED.synced_at`,
|
|
45
|
+
[
|
|
46
|
+
repo.id,
|
|
47
|
+
repo.fullName,
|
|
48
|
+
repo.owner,
|
|
49
|
+
repo.name,
|
|
50
|
+
repo.private,
|
|
51
|
+
repo.defaultBranch,
|
|
52
|
+
repo.htmlUrl,
|
|
53
|
+
repo.description,
|
|
54
|
+
repo.language,
|
|
55
|
+
JSON.stringify(repo.topics),
|
|
56
|
+
repo.updatedAt,
|
|
57
|
+
now
|
|
58
|
+
]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
async function listRepos(db) {
|
|
62
|
+
const rows = await db.query(`SELECT * FROM ${S}.gh_repositories ORDER BY full_name`);
|
|
63
|
+
return rows.map(mapRepo);
|
|
64
|
+
}
|
|
65
|
+
async function getRepoByFullName(db, fullName) {
|
|
66
|
+
const rows = await db.query(`SELECT * FROM ${S}.gh_repositories WHERE full_name = $1`, [fullName]);
|
|
67
|
+
return rows.length > 0 ? mapRepo(rows[0]) : null;
|
|
68
|
+
}
|
|
69
|
+
function mapRepo(row) {
|
|
70
|
+
return {
|
|
71
|
+
id: row.id,
|
|
72
|
+
fullName: row.full_name,
|
|
73
|
+
owner: row.owner,
|
|
74
|
+
name: row.name,
|
|
75
|
+
private: row.private,
|
|
76
|
+
defaultBranch: row.default_branch,
|
|
77
|
+
htmlUrl: row.html_url,
|
|
78
|
+
description: row.description,
|
|
79
|
+
language: row.language,
|
|
80
|
+
topics: JSON.parse(row.topics || "[]"),
|
|
81
|
+
updatedAt: row.updated_at,
|
|
82
|
+
syncedAt: row.synced_at
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function upsertPR(db, pr) {
|
|
86
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
87
|
+
await db.execute(
|
|
88
|
+
`INSERT INTO ${S}.gh_pull_requests (id, repo_id, number, title, body, state, author, head_branch, base_branch, html_url, draft, mergeable, merged_at, created_at, updated_at, synced_at)
|
|
89
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
|
90
|
+
ON CONFLICT (repo_id, number) DO UPDATE SET
|
|
91
|
+
title = EXCLUDED.title, body = EXCLUDED.body, state = EXCLUDED.state,
|
|
92
|
+
author = EXCLUDED.author, head_branch = EXCLUDED.head_branch,
|
|
93
|
+
base_branch = EXCLUDED.base_branch, html_url = EXCLUDED.html_url,
|
|
94
|
+
draft = EXCLUDED.draft, mergeable = EXCLUDED.mergeable,
|
|
95
|
+
merged_at = EXCLUDED.merged_at, updated_at = EXCLUDED.updated_at,
|
|
96
|
+
synced_at = EXCLUDED.synced_at`,
|
|
97
|
+
[
|
|
98
|
+
pr.id,
|
|
99
|
+
pr.repoId,
|
|
100
|
+
pr.number,
|
|
101
|
+
pr.title,
|
|
102
|
+
pr.body,
|
|
103
|
+
pr.state,
|
|
104
|
+
pr.author,
|
|
105
|
+
pr.headBranch,
|
|
106
|
+
pr.baseBranch,
|
|
107
|
+
pr.htmlUrl,
|
|
108
|
+
pr.draft,
|
|
109
|
+
pr.mergeable,
|
|
110
|
+
pr.mergedAt,
|
|
111
|
+
pr.createdAt,
|
|
112
|
+
pr.updatedAt,
|
|
113
|
+
now
|
|
114
|
+
]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
async function listPRs(db, filters) {
|
|
118
|
+
let sql = `SELECT p.*, r.full_name AS repo_full_name
|
|
119
|
+
FROM ${S}.gh_pull_requests p
|
|
120
|
+
JOIN ${S}.gh_repositories r ON r.id = p.repo_id
|
|
121
|
+
WHERE 1=1`;
|
|
122
|
+
const params = [];
|
|
123
|
+
let idx = 1;
|
|
124
|
+
if (filters?.repoId) {
|
|
125
|
+
sql += ` AND p.repo_id = $${idx++}`;
|
|
126
|
+
params.push(filters.repoId);
|
|
127
|
+
}
|
|
128
|
+
if (filters?.state) {
|
|
129
|
+
sql += ` AND p.state = $${idx++}`;
|
|
130
|
+
params.push(filters.state);
|
|
131
|
+
}
|
|
132
|
+
if (filters?.author) {
|
|
133
|
+
sql += ` AND p.author = $${idx++}`;
|
|
134
|
+
params.push(filters.author);
|
|
135
|
+
}
|
|
136
|
+
sql += " ORDER BY p.updated_at DESC";
|
|
137
|
+
const rows = await db.query(sql, params);
|
|
138
|
+
return rows.map(mapPRWithRepo);
|
|
139
|
+
}
|
|
140
|
+
async function getPRByRepoAndNumber(db, repoId, number) {
|
|
141
|
+
const rows = await db.query(
|
|
142
|
+
`SELECT p.*, r.full_name AS repo_full_name
|
|
143
|
+
FROM ${S}.gh_pull_requests p
|
|
144
|
+
JOIN ${S}.gh_repositories r ON r.id = p.repo_id
|
|
145
|
+
WHERE p.repo_id = $1 AND p.number = $2`,
|
|
146
|
+
[repoId, number]
|
|
147
|
+
);
|
|
148
|
+
return rows.length > 0 ? mapPRWithRepo(rows[0]) : null;
|
|
149
|
+
}
|
|
150
|
+
function mapPRWithRepo(row) {
|
|
151
|
+
return {
|
|
152
|
+
id: row.id,
|
|
153
|
+
repoId: row.repo_id,
|
|
154
|
+
number: row.number,
|
|
155
|
+
title: row.title,
|
|
156
|
+
body: row.body,
|
|
157
|
+
state: row.state,
|
|
158
|
+
author: row.author,
|
|
159
|
+
headBranch: row.head_branch,
|
|
160
|
+
baseBranch: row.base_branch,
|
|
161
|
+
htmlUrl: row.html_url,
|
|
162
|
+
draft: row.draft,
|
|
163
|
+
mergeable: row.mergeable,
|
|
164
|
+
mergedAt: row.merged_at,
|
|
165
|
+
createdAt: row.created_at,
|
|
166
|
+
updatedAt: row.updated_at,
|
|
167
|
+
syncedAt: row.synced_at,
|
|
168
|
+
repoFullName: row.repo_full_name
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async function upsertIssue(db, issue) {
|
|
172
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
173
|
+
await db.execute(
|
|
174
|
+
`INSERT INTO ${S}.gh_issues (id, repo_id, number, title, body, state, author, labels, html_url, created_at, updated_at, synced_at)
|
|
175
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
|
176
|
+
ON CONFLICT (repo_id, number) DO UPDATE SET
|
|
177
|
+
title = EXCLUDED.title, body = EXCLUDED.body, state = EXCLUDED.state,
|
|
178
|
+
author = EXCLUDED.author, labels = EXCLUDED.labels,
|
|
179
|
+
html_url = EXCLUDED.html_url, updated_at = EXCLUDED.updated_at,
|
|
180
|
+
synced_at = EXCLUDED.synced_at`,
|
|
181
|
+
[
|
|
182
|
+
issue.id,
|
|
183
|
+
issue.repoId,
|
|
184
|
+
issue.number,
|
|
185
|
+
issue.title,
|
|
186
|
+
issue.body,
|
|
187
|
+
issue.state,
|
|
188
|
+
issue.author,
|
|
189
|
+
JSON.stringify(issue.labels),
|
|
190
|
+
issue.htmlUrl,
|
|
191
|
+
issue.createdAt,
|
|
192
|
+
issue.updatedAt,
|
|
193
|
+
now
|
|
194
|
+
]
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
async function linkPRToCard(db, prId, issueId, source) {
|
|
198
|
+
await db.execute(
|
|
199
|
+
`INSERT INTO ${S}.gh_pr_card_links (pr_id, issue_id, link_source, created_at)
|
|
200
|
+
VALUES ($1, $2, $3, $4)
|
|
201
|
+
ON CONFLICT (pr_id, issue_id) DO NOTHING`,
|
|
202
|
+
[prId, issueId, source, (/* @__PURE__ */ new Date()).toISOString()]
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
async function getLinksForCard(db, issueId) {
|
|
206
|
+
const rows = await db.query(
|
|
207
|
+
`SELECT p.*, r.full_name AS repo_full_name
|
|
208
|
+
FROM ${S}.gh_pr_card_links l
|
|
209
|
+
JOIN ${S}.gh_pull_requests p ON p.id = l.pr_id
|
|
210
|
+
JOIN ${S}.gh_repositories r ON r.id = p.repo_id
|
|
211
|
+
WHERE l.issue_id = $1
|
|
212
|
+
ORDER BY p.updated_at DESC`,
|
|
213
|
+
[issueId]
|
|
214
|
+
);
|
|
215
|
+
return rows.map(mapPRWithRepo);
|
|
216
|
+
}
|
|
217
|
+
async function getLinksForPR(db, prId) {
|
|
218
|
+
const rows = await db.query(
|
|
219
|
+
`SELECT * FROM ${S}.gh_pr_card_links WHERE pr_id = $1`,
|
|
220
|
+
[prId]
|
|
221
|
+
);
|
|
222
|
+
return rows.map((r) => ({
|
|
223
|
+
id: r.id,
|
|
224
|
+
prId: r.pr_id,
|
|
225
|
+
issueId: r.issue_id,
|
|
226
|
+
linkSource: r.link_source,
|
|
227
|
+
createdAt: r.created_at
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
async function createSyncLog(db, scope) {
|
|
231
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
232
|
+
await db.execute(
|
|
233
|
+
`INSERT INTO ${S}.gh_sync_log (scope, started_at) VALUES ($1, $2)`,
|
|
234
|
+
[scope, now]
|
|
235
|
+
);
|
|
236
|
+
const rows = await db.query(
|
|
237
|
+
`SELECT id FROM ${S}.gh_sync_log WHERE scope = $1 AND started_at = $2 ORDER BY id DESC LIMIT 1`,
|
|
238
|
+
[scope, now]
|
|
239
|
+
);
|
|
240
|
+
return rows[0].id;
|
|
241
|
+
}
|
|
242
|
+
async function completeSyncLog(db, id, stats) {
|
|
243
|
+
await db.execute(
|
|
244
|
+
`UPDATE ${S}.gh_sync_log SET repos_synced=$1, prs_synced=$2, issues_synced=$3, errors=$4, finished_at=$5 WHERE id=$6`,
|
|
245
|
+
[stats.reposSynced, stats.prsSynced, stats.issuesSynced, JSON.stringify(stats.errors), (/* @__PURE__ */ new Date()).toISOString(), id]
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
async function getLastSyncTime(db) {
|
|
249
|
+
const rows = await db.query(
|
|
250
|
+
`SELECT finished_at FROM ${S}.gh_sync_log WHERE finished_at IS NOT NULL ORDER BY finished_at DESC LIMIT 1`
|
|
251
|
+
);
|
|
252
|
+
return rows.length > 0 ? rows[0].finished_at : null;
|
|
253
|
+
}
|
|
254
|
+
var S;
|
|
255
|
+
var init_queries = __esm({
|
|
256
|
+
"src/db/queries.ts"() {
|
|
257
|
+
"use strict";
|
|
258
|
+
S = "plugin_cus_github_manager_d2300af002";
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
7
262
|
// node_modules/@paperclipai/plugin-sdk/dist/define-plugin.js
|
|
8
263
|
function definePlugin(definition) {
|
|
9
264
|
return Object.freeze({ definition });
|
|
@@ -3837,7 +4092,7 @@ ZodNaN.create = (params) => {
|
|
|
3837
4092
|
...processCreateParams(params)
|
|
3838
4093
|
});
|
|
3839
4094
|
};
|
|
3840
|
-
var BRAND =
|
|
4095
|
+
var BRAND = Symbol("zod_brand");
|
|
3841
4096
|
var ZodBranded = class extends ZodType {
|
|
3842
4097
|
_parse(input) {
|
|
3843
4098
|
const { ctx } = this._processInputParams(input);
|
|
@@ -8914,523 +9169,863 @@ function startWorkerRpcHost(options) {
|
|
|
8914
9169
|
};
|
|
8915
9170
|
}
|
|
8916
9171
|
|
|
8917
|
-
// src/github
|
|
8918
|
-
var
|
|
8919
|
-
var
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
const raw = await ctx.state.get({ ...companyScope(companyId), stateKey: GITHUB_PAT_STATE_KEY });
|
|
8926
|
-
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null;
|
|
8927
|
-
}
|
|
8928
|
-
async function loadGithubSecretRef(ctx, companyId) {
|
|
8929
|
-
const raw = await ctx.state.get({
|
|
8930
|
-
...companyScope(companyId),
|
|
8931
|
-
stateKey: GITHUB_SECRET_REF_STATE_KEY
|
|
9172
|
+
// src/github/config.ts
|
|
9173
|
+
var GITHUB_PAT_KEY = "github_pat";
|
|
9174
|
+
var GITHUB_SECRET_REF_KEY = "github_secret_ref";
|
|
9175
|
+
async function resolveGithubToken(ctx, companyId) {
|
|
9176
|
+
const pat = await ctx.state.get({
|
|
9177
|
+
scopeKind: "company",
|
|
9178
|
+
scopeId: companyId,
|
|
9179
|
+
stateKey: GITHUB_PAT_KEY
|
|
8932
9180
|
});
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
9181
|
+
if (pat && typeof pat === "string" && pat.trim()) return pat.trim();
|
|
9182
|
+
const secretRef = await ctx.state.get({
|
|
9183
|
+
scopeKind: "company",
|
|
9184
|
+
scopeId: companyId,
|
|
9185
|
+
stateKey: GITHUB_SECRET_REF_KEY
|
|
9186
|
+
});
|
|
9187
|
+
if (secretRef && typeof secretRef === "string" && secretRef.trim()) {
|
|
9188
|
+
const resolved = await ctx.secrets.resolve(secretRef.trim());
|
|
9189
|
+
if (resolved) return resolved;
|
|
8940
9190
|
}
|
|
8941
|
-
|
|
9191
|
+
const envToken = process.env.GITHUB_TOKEN?.trim();
|
|
9192
|
+
if (envToken) return envToken;
|
|
9193
|
+
throw new Error("No GitHub token configured. Set a PAT or secret reference in Settings.");
|
|
8942
9194
|
}
|
|
8943
|
-
async function
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
{ ...companyScope(companyId), stateKey: GITHUB_SECRET_REF_STATE_KEY },
|
|
8950
|
-
trimmed
|
|
8951
|
-
);
|
|
9195
|
+
async function saveGithubPAT(ctx, companyId, token) {
|
|
9196
|
+
await ctx.state.set({
|
|
9197
|
+
scopeKind: "company",
|
|
9198
|
+
scopeId: companyId,
|
|
9199
|
+
stateKey: GITHUB_PAT_KEY
|
|
9200
|
+
}, token);
|
|
8952
9201
|
}
|
|
8953
|
-
async function
|
|
8954
|
-
await ctx.state.set({
|
|
8955
|
-
|
|
9202
|
+
async function saveGithubSecretRef(ctx, companyId, ref) {
|
|
9203
|
+
await ctx.state.set({
|
|
9204
|
+
scopeKind: "company",
|
|
9205
|
+
scopeId: companyId,
|
|
9206
|
+
stateKey: GITHUB_SECRET_REF_KEY
|
|
9207
|
+
}, ref);
|
|
8956
9208
|
}
|
|
8957
|
-
|
|
8958
|
-
const
|
|
8959
|
-
|
|
8960
|
-
return { configured: true, mode: "pat" };
|
|
8961
|
-
}
|
|
8962
|
-
const secretRef = await loadGithubSecretRef(ctx, companyId);
|
|
8963
|
-
if (secretRef) {
|
|
8964
|
-
return { configured: true, mode: "secret-ref" };
|
|
8965
|
-
}
|
|
8966
|
-
return { configured: false, mode: "none" };
|
|
9209
|
+
function getGithubApiBase() {
|
|
9210
|
+
const base = process.env.GITHUB_API_URL?.trim();
|
|
9211
|
+
return base ? base.replace(/\/+$/, "") : "https://api.github.com";
|
|
8967
9212
|
}
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
const
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
9213
|
+
|
|
9214
|
+
// src/github/api-client.ts
|
|
9215
|
+
async function githubFetch(ctx, companyId, path2, options = {}) {
|
|
9216
|
+
const token = await resolveGithubToken(ctx, companyId);
|
|
9217
|
+
const base = getGithubApiBase();
|
|
9218
|
+
const url = path2.startsWith("http") ? path2 : `${base}${path2}`;
|
|
9219
|
+
const headers = {
|
|
9220
|
+
Authorization: `Bearer ${token}`,
|
|
9221
|
+
Accept: options.accept ?? "application/vnd.github+json",
|
|
9222
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
9223
|
+
};
|
|
9224
|
+
const resp = await ctx.http.fetch(url, {
|
|
9225
|
+
method: options.method ?? "GET",
|
|
9226
|
+
headers,
|
|
9227
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
9228
|
+
});
|
|
9229
|
+
const rateLimit = {
|
|
9230
|
+
remaining: Number(resp.headers.get("x-ratelimit-remaining") ?? 5e3),
|
|
9231
|
+
limit: Number(resp.headers.get("x-ratelimit-limit") ?? 5e3),
|
|
9232
|
+
resetAt: new Date(
|
|
9233
|
+
Number(resp.headers.get("x-ratelimit-reset") ?? 0) * 1e3
|
|
9234
|
+
).toISOString()
|
|
9235
|
+
};
|
|
9236
|
+
if (!resp.ok) {
|
|
9237
|
+
const body = await resp.text();
|
|
9238
|
+
if (resp.status === 403 && rateLimit.remaining === 0) {
|
|
9239
|
+
throw new Error(`GitHub rate limit exceeded. Resets at ${rateLimit.resetAt}`);
|
|
9240
|
+
}
|
|
9241
|
+
throw new Error(`GitHub API ${resp.status}: ${body}`);
|
|
8981
9242
|
}
|
|
9243
|
+
const data = await resp.json();
|
|
9244
|
+
return { data, rateLimit };
|
|
9245
|
+
}
|
|
9246
|
+
function isRateLimitSafe(rateLimit, threshold = 100) {
|
|
9247
|
+
return rateLimit.remaining > threshold;
|
|
8982
9248
|
}
|
|
8983
9249
|
|
|
8984
|
-
// src/
|
|
8985
|
-
var
|
|
8986
|
-
var
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
9250
|
+
// src/review/review-tools.ts
|
|
9251
|
+
var MAX_DIFF_CHARS = 12e4;
|
|
9252
|
+
var MAX_FILE_CHARS = 128e3;
|
|
9253
|
+
function registerReviewTools(ctx) {
|
|
9254
|
+
ctx.tools.register(
|
|
9255
|
+
"github_get_pull_request_diff",
|
|
9256
|
+
{
|
|
9257
|
+
displayName: "Get PR Diff",
|
|
9258
|
+
description: "Get the diff of a GitHub pull request for code review",
|
|
9259
|
+
parametersSchema: {
|
|
9260
|
+
type: "object",
|
|
9261
|
+
properties: {
|
|
9262
|
+
owner: { type: "string", description: "Repository owner" },
|
|
9263
|
+
repo: { type: "string", description: "Repository name" },
|
|
9264
|
+
pull_number: { type: "number", description: "PR number" }
|
|
9265
|
+
},
|
|
9266
|
+
required: ["owner", "repo", "pull_number"]
|
|
9267
|
+
}
|
|
9268
|
+
},
|
|
9269
|
+
async (params, runCtx) => {
|
|
9270
|
+
const { owner, repo, pull_number } = params;
|
|
9271
|
+
const companyId = runCtx.companyId;
|
|
9272
|
+
if (!companyId) return { error: "No company context" };
|
|
9273
|
+
const { data: prData } = await githubFetch(ctx, companyId, `/repos/${owner}/${repo}/pulls/${pull_number}`);
|
|
9274
|
+
const pr = prData;
|
|
9275
|
+
const { data: diffData } = await githubFetch(
|
|
9276
|
+
ctx,
|
|
9277
|
+
companyId,
|
|
9278
|
+
`/repos/${owner}/${repo}/pulls/${pull_number}`,
|
|
9279
|
+
{ accept: "application/vnd.github.v3.diff" }
|
|
9280
|
+
);
|
|
9281
|
+
let diff = String(diffData);
|
|
9282
|
+
let truncated = false;
|
|
9283
|
+
if (diff.length > MAX_DIFF_CHARS) {
|
|
9284
|
+
diff = diff.slice(0, MAX_DIFF_CHARS);
|
|
9285
|
+
truncated = true;
|
|
9286
|
+
}
|
|
9287
|
+
const { data: filesData } = await githubFetch(
|
|
9288
|
+
ctx,
|
|
9289
|
+
companyId,
|
|
9290
|
+
`/repos/${owner}/${repo}/pulls/${pull_number}/files?per_page=100`
|
|
9291
|
+
);
|
|
9292
|
+
const files = filesData.map((f) => ({
|
|
9293
|
+
filename: f.filename,
|
|
9294
|
+
status: f.status,
|
|
9295
|
+
additions: f.additions,
|
|
9296
|
+
deletions: f.deletions
|
|
9297
|
+
}));
|
|
9298
|
+
return {
|
|
9299
|
+
content: `PR #${pull_number}: ${pr.title}
|
|
9300
|
+
Author: ${pr.user.login}
|
|
9301
|
+
Files changed: ${files.length}${truncated ? "\n\u26A0\uFE0F Diff truncated" : ""}
|
|
9302
|
+
|
|
9303
|
+
${diff}`,
|
|
9304
|
+
data: { pr: { title: pr.title, number: pull_number, sha: pr.head.sha }, files }
|
|
9305
|
+
};
|
|
8996
9306
|
}
|
|
8997
|
-
|
|
9307
|
+
);
|
|
9308
|
+
ctx.tools.register(
|
|
9309
|
+
"github_read_file_content",
|
|
9310
|
+
{
|
|
9311
|
+
displayName: "Read File",
|
|
9312
|
+
description: "Read a file from a GitHub repository",
|
|
9313
|
+
parametersSchema: {
|
|
9314
|
+
type: "object",
|
|
9315
|
+
properties: {
|
|
9316
|
+
owner: { type: "string" },
|
|
9317
|
+
repo: { type: "string" },
|
|
9318
|
+
path: { type: "string", description: "File path in the repository" },
|
|
9319
|
+
ref: { type: "string", description: "Branch, tag, or commit SHA (optional)" }
|
|
9320
|
+
},
|
|
9321
|
+
required: ["owner", "repo", "path"]
|
|
9322
|
+
}
|
|
9323
|
+
},
|
|
9324
|
+
async (params, runCtx) => {
|
|
9325
|
+
const { owner, repo, path: path2, ref } = params;
|
|
9326
|
+
const companyId = runCtx.companyId;
|
|
9327
|
+
if (!companyId) return { error: "No company context" };
|
|
9328
|
+
const refParam = ref ? `?ref=${ref}` : "";
|
|
9329
|
+
const { data } = await githubFetch(
|
|
9330
|
+
ctx,
|
|
9331
|
+
companyId,
|
|
9332
|
+
`/repos/${owner}/${repo}/contents/${path2}${refParam}`
|
|
9333
|
+
);
|
|
9334
|
+
const file = data;
|
|
9335
|
+
const content = Buffer.from(file.content, "base64").toString("utf-8");
|
|
9336
|
+
const truncated = content.length > MAX_FILE_CHARS;
|
|
9337
|
+
return {
|
|
9338
|
+
content: truncated ? content.slice(0, MAX_FILE_CHARS) + "\n\u26A0\uFE0F Truncated" : content,
|
|
9339
|
+
data: { path: path2, size: file.size, sha: file.sha }
|
|
9340
|
+
};
|
|
9341
|
+
}
|
|
9342
|
+
);
|
|
9343
|
+
ctx.tools.register(
|
|
9344
|
+
"github_create_review_comment",
|
|
9345
|
+
{
|
|
9346
|
+
displayName: "Add Review Comment",
|
|
9347
|
+
description: "Add an inline review comment to a pull request",
|
|
9348
|
+
parametersSchema: {
|
|
9349
|
+
type: "object",
|
|
9350
|
+
properties: {
|
|
9351
|
+
owner: { type: "string" },
|
|
9352
|
+
repo: { type: "string" },
|
|
9353
|
+
pull_number: { type: "number" },
|
|
9354
|
+
commit_id: { type: "string", description: "The SHA of the PR head commit" },
|
|
9355
|
+
path: { type: "string", description: "File path relative to repo root" },
|
|
9356
|
+
line: { type: "number", description: "Line number in the diff" },
|
|
9357
|
+
body: { type: "string", description: "Comment text (markdown)" }
|
|
9358
|
+
},
|
|
9359
|
+
required: ["owner", "repo", "pull_number", "commit_id", "path", "line", "body"]
|
|
9360
|
+
}
|
|
9361
|
+
},
|
|
9362
|
+
async (params, runCtx) => {
|
|
9363
|
+
const { owner, repo, pull_number, commit_id, path: path2, line, body } = params;
|
|
9364
|
+
const companyId = runCtx.companyId;
|
|
9365
|
+
if (!companyId) return { error: "No company context" };
|
|
9366
|
+
await githubFetch(ctx, companyId, `/repos/${owner}/${repo}/pulls/${pull_number}/comments`, {
|
|
9367
|
+
method: "POST",
|
|
9368
|
+
body: { commit_id, path: path2, line, body, side: "RIGHT" }
|
|
9369
|
+
});
|
|
9370
|
+
return { content: `Comment added to ${path2}:${line}` };
|
|
9371
|
+
}
|
|
9372
|
+
);
|
|
9373
|
+
ctx.tools.register(
|
|
9374
|
+
"github_submit_pr_review",
|
|
9375
|
+
{
|
|
9376
|
+
displayName: "Submit PR Review",
|
|
9377
|
+
description: "Submit a pull request review with a verdict",
|
|
9378
|
+
parametersSchema: {
|
|
9379
|
+
type: "object",
|
|
9380
|
+
properties: {
|
|
9381
|
+
owner: { type: "string" },
|
|
9382
|
+
repo: { type: "string" },
|
|
9383
|
+
pull_number: { type: "number" },
|
|
9384
|
+
event: { type: "string", enum: ["APPROVE", "REQUEST_CHANGES", "COMMENT"] },
|
|
9385
|
+
body: { type: "string", description: "Review summary (markdown)" }
|
|
9386
|
+
},
|
|
9387
|
+
required: ["owner", "repo", "pull_number", "event", "body"]
|
|
9388
|
+
}
|
|
9389
|
+
},
|
|
9390
|
+
async (params, runCtx) => {
|
|
9391
|
+
const { owner, repo, pull_number, event, body } = params;
|
|
9392
|
+
const companyId = runCtx.companyId;
|
|
9393
|
+
if (!companyId) return { error: "No company context" };
|
|
9394
|
+
await githubFetch(ctx, companyId, `/repos/${owner}/${repo}/pulls/${pull_number}/reviews`, {
|
|
9395
|
+
method: "POST",
|
|
9396
|
+
body: { event, body }
|
|
9397
|
+
});
|
|
9398
|
+
return { content: `Review submitted: ${event}`, data: { event } };
|
|
9399
|
+
}
|
|
9400
|
+
);
|
|
9401
|
+
ctx.tools.register(
|
|
9402
|
+
"github_list_repositories",
|
|
9403
|
+
{
|
|
9404
|
+
displayName: "List Repositories",
|
|
9405
|
+
description: "List tracked GitHub repositories",
|
|
9406
|
+
parametersSchema: { type: "object", properties: {} }
|
|
9407
|
+
},
|
|
9408
|
+
async (_params, _runCtx) => {
|
|
9409
|
+
const { listRepos: listRepos2 } = await Promise.resolve().then(() => (init_queries(), queries_exports));
|
|
9410
|
+
const repos = await listRepos2(ctx.db);
|
|
9411
|
+
return {
|
|
9412
|
+
content: repos.map((r) => `${r.fullName} (${r.language ?? "unknown"})`).join("\n"),
|
|
9413
|
+
data: { repos }
|
|
9414
|
+
};
|
|
9415
|
+
}
|
|
9416
|
+
);
|
|
9417
|
+
ctx.tools.register(
|
|
9418
|
+
"github_search_issues",
|
|
9419
|
+
{
|
|
9420
|
+
displayName: "Search Issues",
|
|
9421
|
+
description: "Search GitHub issues and PRs using GitHub search syntax",
|
|
9422
|
+
parametersSchema: {
|
|
9423
|
+
type: "object",
|
|
9424
|
+
properties: {
|
|
9425
|
+
query: { type: "string", description: "GitHub search query (e.g. 'is:open label:bug')" }
|
|
9426
|
+
},
|
|
9427
|
+
required: ["query"]
|
|
9428
|
+
}
|
|
9429
|
+
},
|
|
9430
|
+
async (params, runCtx) => {
|
|
9431
|
+
const { query } = params;
|
|
9432
|
+
const companyId = runCtx.companyId;
|
|
9433
|
+
if (!companyId) return { error: "No company context" };
|
|
9434
|
+
const { data } = await githubFetch(
|
|
9435
|
+
ctx,
|
|
9436
|
+
companyId,
|
|
9437
|
+
`/search/issues?q=${encodeURIComponent(query)}&per_page=20`
|
|
9438
|
+
);
|
|
9439
|
+
const result = data;
|
|
9440
|
+
const items = result.items.map((i) => ({
|
|
9441
|
+
title: i.title,
|
|
9442
|
+
number: i.number,
|
|
9443
|
+
state: i.state,
|
|
9444
|
+
html_url: i.html_url
|
|
9445
|
+
}));
|
|
9446
|
+
return { content: items.map((i) => `#${i.number} ${i.title} [${i.state}]`).join("\n"), data: { items } };
|
|
9447
|
+
}
|
|
9448
|
+
);
|
|
8998
9449
|
}
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9450
|
+
|
|
9451
|
+
// src/sync/webhook-handler.ts
|
|
9452
|
+
init_queries();
|
|
9453
|
+
|
|
9454
|
+
// src/sync/link-detector.ts
|
|
9455
|
+
init_queries();
|
|
9456
|
+
var KEY_PATTERN = /\b([A-Z][A-Z0-9]+-\d+)\b/g;
|
|
9457
|
+
var HASH_PATTERN = /#(\d+)\b/g;
|
|
9458
|
+
function extractCardIds(branch, title) {
|
|
9459
|
+
const text = `${branch} ${title}`;
|
|
9460
|
+
const ids = /* @__PURE__ */ new Set();
|
|
9461
|
+
for (const match of text.matchAll(KEY_PATTERN)) {
|
|
9462
|
+
ids.add(match[1]);
|
|
9463
|
+
}
|
|
9464
|
+
for (const match of text.matchAll(HASH_PATTERN)) {
|
|
9465
|
+
ids.add(`#${match[1]}`);
|
|
9466
|
+
}
|
|
9467
|
+
return [...ids];
|
|
9005
9468
|
}
|
|
9006
|
-
function
|
|
9007
|
-
|
|
9469
|
+
async function detectAndLinkCards(ctx, prId, branch, title) {
|
|
9470
|
+
const cardIds = extractCardIds(branch, title);
|
|
9471
|
+
for (const cardId of cardIds) {
|
|
9472
|
+
await linkPRToCard(ctx.db, prId, cardId, "pattern");
|
|
9473
|
+
}
|
|
9474
|
+
return cardIds;
|
|
9008
9475
|
}
|
|
9009
9476
|
|
|
9010
|
-
// src/
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9477
|
+
// src/sync/webhook-handler.ts
|
|
9478
|
+
async function handleGithubWebhook(ctx, input) {
|
|
9479
|
+
const event = input.headers["x-github-event"];
|
|
9480
|
+
const payload = input.parsedBody;
|
|
9481
|
+
if (!payload || !event) {
|
|
9482
|
+
ctx.logger.warn("Webhook received with missing event header or body");
|
|
9483
|
+
return;
|
|
9484
|
+
}
|
|
9485
|
+
if (event === "pull_request") {
|
|
9486
|
+
await handlePullRequestEvent(ctx, payload);
|
|
9487
|
+
} else if (event === "issues") {
|
|
9488
|
+
await handleIssuesEvent(ctx, payload);
|
|
9489
|
+
} else {
|
|
9490
|
+
ctx.logger.info(`Ignoring GitHub event: ${event}`);
|
|
9491
|
+
}
|
|
9023
9492
|
}
|
|
9024
|
-
async function
|
|
9025
|
-
|
|
9493
|
+
async function handlePullRequestEvent(ctx, payload) {
|
|
9494
|
+
const prData = payload.pull_request;
|
|
9495
|
+
const repoData = payload.repository;
|
|
9496
|
+
if (!prData || !repoData) return;
|
|
9497
|
+
await upsertRepo(ctx.db, {
|
|
9498
|
+
id: repoData.id,
|
|
9499
|
+
fullName: repoData.full_name,
|
|
9500
|
+
owner: repoData.owner.login,
|
|
9501
|
+
name: repoData.name,
|
|
9502
|
+
private: repoData.private,
|
|
9503
|
+
defaultBranch: repoData.default_branch,
|
|
9504
|
+
htmlUrl: repoData.html_url,
|
|
9505
|
+
description: repoData.description,
|
|
9506
|
+
language: repoData.language,
|
|
9507
|
+
topics: repoData.topics ?? [],
|
|
9508
|
+
updatedAt: repoData.updated_at
|
|
9509
|
+
});
|
|
9510
|
+
const merged = prData.merged;
|
|
9511
|
+
const state = merged ? "merged" : prData.state;
|
|
9512
|
+
const pr = {
|
|
9513
|
+
id: prData.id,
|
|
9514
|
+
repoId: repoData.id,
|
|
9515
|
+
number: prData.number,
|
|
9516
|
+
title: prData.title,
|
|
9517
|
+
body: prData.body,
|
|
9518
|
+
state,
|
|
9519
|
+
author: prData.user.login,
|
|
9520
|
+
headBranch: prData.head.ref,
|
|
9521
|
+
baseBranch: prData.base.ref,
|
|
9522
|
+
htmlUrl: prData.html_url,
|
|
9523
|
+
draft: prData.draft,
|
|
9524
|
+
mergeable: prData.mergeable,
|
|
9525
|
+
mergedAt: prData.merged_at,
|
|
9526
|
+
createdAt: prData.created_at,
|
|
9527
|
+
updatedAt: prData.updated_at
|
|
9528
|
+
};
|
|
9529
|
+
await upsertPR(ctx.db, pr);
|
|
9530
|
+
await detectAndLinkCards(ctx, pr.id, pr.headBranch, pr.title);
|
|
9531
|
+
ctx.logger.info(`Webhook: upserted PR #${pr.number} from ${repoData.full_name}`);
|
|
9026
9532
|
}
|
|
9027
|
-
async function
|
|
9028
|
-
const
|
|
9029
|
-
|
|
9533
|
+
async function handleIssuesEvent(ctx, payload) {
|
|
9534
|
+
const issueData = payload.issue;
|
|
9535
|
+
const repoData = payload.repository;
|
|
9536
|
+
if (!issueData || !repoData) return;
|
|
9537
|
+
await upsertRepo(ctx.db, {
|
|
9538
|
+
id: repoData.id,
|
|
9539
|
+
fullName: repoData.full_name,
|
|
9540
|
+
owner: repoData.owner.login,
|
|
9541
|
+
name: repoData.name,
|
|
9542
|
+
private: repoData.private,
|
|
9543
|
+
defaultBranch: repoData.default_branch,
|
|
9544
|
+
htmlUrl: repoData.html_url,
|
|
9545
|
+
description: repoData.description,
|
|
9546
|
+
language: repoData.language,
|
|
9547
|
+
topics: repoData.topics ?? [],
|
|
9548
|
+
updatedAt: repoData.updated_at
|
|
9549
|
+
});
|
|
9550
|
+
const issue = {
|
|
9551
|
+
id: issueData.id,
|
|
9552
|
+
repoId: repoData.id,
|
|
9553
|
+
number: issueData.number,
|
|
9554
|
+
title: issueData.title,
|
|
9555
|
+
body: issueData.body,
|
|
9556
|
+
state: issueData.state,
|
|
9557
|
+
author: issueData.user.login,
|
|
9558
|
+
labels: (issueData.labels ?? []).map(
|
|
9559
|
+
(l) => l.name
|
|
9560
|
+
),
|
|
9561
|
+
htmlUrl: issueData.html_url,
|
|
9562
|
+
createdAt: issueData.created_at,
|
|
9563
|
+
updatedAt: issueData.updated_at
|
|
9564
|
+
};
|
|
9565
|
+
await upsertIssue(ctx.db, issue);
|
|
9566
|
+
ctx.logger.info(`Webhook: upserted issue #${issue.number} from ${repoData.full_name}`);
|
|
9030
9567
|
}
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9568
|
+
|
|
9569
|
+
// src/sync/incremental-sync.ts
|
|
9570
|
+
init_queries();
|
|
9571
|
+
async function runIncrementalSync(ctx, companyId) {
|
|
9572
|
+
const repos = await listRepos(ctx.db);
|
|
9573
|
+
if (repos.length === 0) return;
|
|
9574
|
+
const lastSync = await getLastSyncTime(ctx.db);
|
|
9575
|
+
const since = lastSync ?? new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
9576
|
+
const logId = await createSyncLog(ctx.db, "incremental");
|
|
9577
|
+
let reposSynced = 0;
|
|
9578
|
+
let prsSynced = 0;
|
|
9579
|
+
let issuesSynced = 0;
|
|
9580
|
+
const errors = [];
|
|
9581
|
+
for (const repo of repos) {
|
|
9582
|
+
try {
|
|
9583
|
+
const prResult = await syncRepoPRs(ctx, companyId, repo.id, repo.fullName, since);
|
|
9584
|
+
const issueResult = await syncRepoIssues(ctx, companyId, repo.id, repo.fullName, since);
|
|
9585
|
+
prsSynced += prResult;
|
|
9586
|
+
issuesSynced += issueResult;
|
|
9587
|
+
reposSynced++;
|
|
9588
|
+
} catch (err) {
|
|
9589
|
+
const msg = `${repo.fullName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
9590
|
+
errors.push(msg);
|
|
9591
|
+
ctx.logger.error(`Sync error: ${msg}`);
|
|
9592
|
+
}
|
|
9041
9593
|
}
|
|
9042
|
-
|
|
9594
|
+
await completeSyncLog(ctx.db, logId, { reposSynced, prsSynced, issuesSynced, errors });
|
|
9595
|
+
ctx.logger.info(`Incremental sync done: ${reposSynced} repos, ${prsSynced} PRs, ${issuesSynced} issues`);
|
|
9596
|
+
}
|
|
9597
|
+
async function syncRepoPRs(ctx, companyId, repoId, fullName, since) {
|
|
9598
|
+
const { data, rateLimit } = await githubFetch(
|
|
9043
9599
|
ctx,
|
|
9044
|
-
|
|
9045
|
-
`/
|
|
9600
|
+
companyId,
|
|
9601
|
+
`/repos/${fullName}/pulls?state=all&sort=updated&direction=desc&per_page=100&since=${since}`
|
|
9046
9602
|
);
|
|
9047
|
-
if (!
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9603
|
+
if (!isRateLimitSafe(rateLimit)) {
|
|
9604
|
+
ctx.logger.warn(`Rate limit low (${rateLimit.remaining}), skipping remaining repos`);
|
|
9605
|
+
}
|
|
9606
|
+
const items = data;
|
|
9607
|
+
for (const item of items) {
|
|
9608
|
+
const merged = item.merged_at !== null && item.merged_at !== void 0;
|
|
9609
|
+
const state = merged ? "merged" : item.state;
|
|
9610
|
+
const pr = {
|
|
9611
|
+
id: item.id,
|
|
9612
|
+
repoId,
|
|
9613
|
+
number: item.number,
|
|
9614
|
+
title: item.title,
|
|
9615
|
+
body: item.body,
|
|
9616
|
+
state,
|
|
9617
|
+
author: item.user.login,
|
|
9618
|
+
headBranch: item.head.ref,
|
|
9619
|
+
baseBranch: item.base.ref,
|
|
9620
|
+
htmlUrl: item.html_url,
|
|
9621
|
+
draft: item.draft,
|
|
9622
|
+
mergeable: item.mergeable,
|
|
9623
|
+
mergedAt: item.merged_at,
|
|
9624
|
+
createdAt: item.created_at,
|
|
9625
|
+
updatedAt: item.updated_at
|
|
9053
9626
|
};
|
|
9627
|
+
await upsertPR(ctx.db, pr);
|
|
9628
|
+
await detectAndLinkCards(ctx, pr.id, pr.headBranch, pr.title);
|
|
9054
9629
|
}
|
|
9055
|
-
|
|
9056
|
-
const repos = rows.map((r) => ({
|
|
9057
|
-
id: r.id,
|
|
9058
|
-
fullName: r.full_name,
|
|
9059
|
-
private: r.private,
|
|
9060
|
-
htmlUrl: r.html_url,
|
|
9061
|
-
updatedAt: r.updated_at,
|
|
9062
|
-
defaultBranch: r.default_branch
|
|
9063
|
-
}));
|
|
9064
|
-
return { status: "ok", checkedAt, repos };
|
|
9630
|
+
return items.length;
|
|
9065
9631
|
}
|
|
9066
|
-
async function
|
|
9067
|
-
const
|
|
9068
|
-
if (Array.isArray(tracked) && tracked.length > 0) {
|
|
9069
|
-
return tracked.filter((r) => typeof r === "string").slice(0, MAX_REPOS_PER_SYNC);
|
|
9070
|
-
}
|
|
9071
|
-
const reposData = await listRepos(ctx, companyId);
|
|
9072
|
-
return reposData.repos.slice(0, MAX_REPOS_PER_SYNC).map((r) => r.fullName);
|
|
9073
|
-
}
|
|
9074
|
-
async function fetchPullRequestsForRepo(ctx, token, repoFullName) {
|
|
9075
|
-
const { owner, repo } = parseRepoFullName(repoFullName);
|
|
9076
|
-
const res = await githubFetch(
|
|
9632
|
+
async function syncRepoIssues(ctx, companyId, repoId, fullName, since) {
|
|
9633
|
+
const { data } = await githubFetch(
|
|
9077
9634
|
ctx,
|
|
9078
|
-
|
|
9079
|
-
`/repos/${
|
|
9635
|
+
companyId,
|
|
9636
|
+
`/repos/${fullName}/issues?state=all&sort=updated&direction=desc&per_page=100&since=${since}&filter=all`
|
|
9080
9637
|
);
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
}
|
|
9084
|
-
const rows = await res.json();
|
|
9085
|
-
return rows.map((pr) => ({
|
|
9086
|
-
id: pr.id,
|
|
9087
|
-
number: pr.number,
|
|
9088
|
-
title: pr.title,
|
|
9089
|
-
state: pr.state,
|
|
9090
|
-
htmlUrl: pr.html_url,
|
|
9091
|
-
repoFullName,
|
|
9092
|
-
updatedAt: pr.updated_at
|
|
9093
|
-
}));
|
|
9094
|
-
}
|
|
9095
|
-
async function fetchIssuesForRepo(ctx, token, repoFullName) {
|
|
9096
|
-
const { owner, repo } = parseRepoFullName(repoFullName);
|
|
9097
|
-
const res = await githubFetch(
|
|
9098
|
-
ctx,
|
|
9099
|
-
token,
|
|
9100
|
-
`/repos/${owner}/${repo}/issues?state=open&per_page=${MAX_ITEMS_PER_REPO}&sort=updated&direction=desc`
|
|
9638
|
+
const items = data.filter(
|
|
9639
|
+
(item) => !item.pull_request
|
|
9101
9640
|
);
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9641
|
+
for (const item of items) {
|
|
9642
|
+
const issue = {
|
|
9643
|
+
id: item.id,
|
|
9644
|
+
repoId,
|
|
9645
|
+
number: item.number,
|
|
9646
|
+
title: item.title,
|
|
9647
|
+
body: item.body,
|
|
9648
|
+
state: item.state,
|
|
9649
|
+
author: item.user.login,
|
|
9650
|
+
labels: (item.labels ?? []).map(
|
|
9651
|
+
(l) => l.name
|
|
9652
|
+
),
|
|
9653
|
+
htmlUrl: item.html_url,
|
|
9654
|
+
createdAt: item.created_at,
|
|
9655
|
+
updatedAt: item.updated_at
|
|
9656
|
+
};
|
|
9657
|
+
await upsertIssue(ctx.db, issue);
|
|
9658
|
+
}
|
|
9659
|
+
return items.length;
|
|
9115
9660
|
}
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
const
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
errors: []
|
|
9127
|
-
};
|
|
9661
|
+
|
|
9662
|
+
// src/sync/full-sync.ts
|
|
9663
|
+
init_queries();
|
|
9664
|
+
async function runFullSync(ctx, companyId) {
|
|
9665
|
+
const repos = await listRepos(ctx.db);
|
|
9666
|
+
if (repos.length === 0) return;
|
|
9667
|
+
const logId = await createSyncLog(ctx.db, "full");
|
|
9668
|
+
let reposSynced = 0;
|
|
9669
|
+
let prsSynced = 0;
|
|
9670
|
+
let issuesSynced = 0;
|
|
9128
9671
|
const errors = [];
|
|
9129
|
-
|
|
9130
|
-
let issues = mode === "pullRequests" ? existing.issues : [];
|
|
9131
|
-
for (const repoFullName of repos) {
|
|
9672
|
+
for (const repo of repos) {
|
|
9132
9673
|
try {
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9674
|
+
const { data: repoData } = await githubFetch(ctx, companyId, `/repos/${repo.fullName}`);
|
|
9675
|
+
const rd = repoData;
|
|
9676
|
+
await upsertRepo(ctx.db, {
|
|
9677
|
+
id: rd.id,
|
|
9678
|
+
fullName: rd.full_name,
|
|
9679
|
+
owner: rd.owner.login,
|
|
9680
|
+
name: rd.name,
|
|
9681
|
+
private: rd.private,
|
|
9682
|
+
defaultBranch: rd.default_branch,
|
|
9683
|
+
htmlUrl: rd.html_url,
|
|
9684
|
+
description: rd.description,
|
|
9685
|
+
language: rd.language,
|
|
9686
|
+
topics: rd.topics ?? [],
|
|
9687
|
+
updatedAt: rd.updated_at
|
|
9688
|
+
});
|
|
9689
|
+
const { data: prs } = await githubFetch(
|
|
9690
|
+
ctx,
|
|
9691
|
+
companyId,
|
|
9692
|
+
`/repos/${repo.fullName}/pulls?state=open&per_page=100`
|
|
9693
|
+
);
|
|
9694
|
+
for (const item of prs) {
|
|
9695
|
+
const pr = {
|
|
9696
|
+
id: item.id,
|
|
9697
|
+
repoId: repo.id,
|
|
9698
|
+
number: item.number,
|
|
9699
|
+
title: item.title,
|
|
9700
|
+
body: item.body,
|
|
9701
|
+
state: "open",
|
|
9702
|
+
author: item.user.login,
|
|
9703
|
+
headBranch: item.head.ref,
|
|
9704
|
+
baseBranch: item.base.ref,
|
|
9705
|
+
htmlUrl: item.html_url,
|
|
9706
|
+
draft: item.draft,
|
|
9707
|
+
mergeable: item.mergeable,
|
|
9708
|
+
mergedAt: null,
|
|
9709
|
+
createdAt: item.created_at,
|
|
9710
|
+
updatedAt: item.updated_at
|
|
9711
|
+
};
|
|
9712
|
+
await upsertPR(ctx.db, pr);
|
|
9713
|
+
await detectAndLinkCards(ctx, pr.id, pr.headBranch, pr.title);
|
|
9714
|
+
prsSynced++;
|
|
9137
9715
|
}
|
|
9138
|
-
|
|
9139
|
-
|
|
9716
|
+
const { data: issues } = await githubFetch(
|
|
9717
|
+
ctx,
|
|
9718
|
+
companyId,
|
|
9719
|
+
`/repos/${repo.fullName}/issues?state=open&per_page=100&filter=all`
|
|
9720
|
+
);
|
|
9721
|
+
for (const item of issues.filter((i) => !i.pull_request)) {
|
|
9722
|
+
const issue = {
|
|
9723
|
+
id: item.id,
|
|
9724
|
+
repoId: repo.id,
|
|
9725
|
+
number: item.number,
|
|
9726
|
+
title: item.title,
|
|
9727
|
+
body: item.body,
|
|
9728
|
+
state: item.state,
|
|
9729
|
+
author: item.user.login,
|
|
9730
|
+
labels: (item.labels ?? []).map((l) => l.name),
|
|
9731
|
+
htmlUrl: item.html_url,
|
|
9732
|
+
createdAt: item.created_at,
|
|
9733
|
+
updatedAt: item.updated_at
|
|
9734
|
+
};
|
|
9735
|
+
await upsertIssue(ctx.db, issue);
|
|
9736
|
+
issuesSynced++;
|
|
9140
9737
|
}
|
|
9738
|
+
reposSynced++;
|
|
9141
9739
|
} catch (err) {
|
|
9142
|
-
|
|
9143
|
-
errors.push(message);
|
|
9144
|
-
ctx.logger.warn("GitHub sync repo failed", { repoFullName, message });
|
|
9740
|
+
errors.push(`${repo.fullName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9145
9741
|
}
|
|
9146
9742
|
}
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9743
|
+
await completeSyncLog(ctx.db, logId, { reposSynced, prsSynced, issuesSynced, errors });
|
|
9744
|
+
ctx.logger.info(`Full sync done: ${reposSynced} repos, ${prsSynced} PRs, ${issuesSynced} issues`);
|
|
9745
|
+
}
|
|
9746
|
+
|
|
9747
|
+
// src/review/quick-check.ts
|
|
9748
|
+
var SENSITIVE_PATTERNS = [
|
|
9749
|
+
/\.env$/i,
|
|
9750
|
+
/\.env\./i,
|
|
9751
|
+
/credentials/i,
|
|
9752
|
+
/secret/i,
|
|
9753
|
+
/\.pem$/i,
|
|
9754
|
+
/\.key$/i,
|
|
9755
|
+
/password/i,
|
|
9756
|
+
/token/i
|
|
9757
|
+
];
|
|
9758
|
+
var TEST_PATTERNS = [
|
|
9759
|
+
/\.test\./,
|
|
9760
|
+
/\.spec\./,
|
|
9761
|
+
/_test\./,
|
|
9762
|
+
/tests?\//,
|
|
9763
|
+
/__tests__\//
|
|
9764
|
+
];
|
|
9765
|
+
async function runQuickCheck(ctx, companyId, owner, repo, pullNumber) {
|
|
9766
|
+
const { data: prData } = await githubFetch(ctx, companyId, `/repos/${owner}/${repo}/pulls/${pullNumber}`);
|
|
9767
|
+
const pr = prData;
|
|
9768
|
+
const { data: filesData } = await githubFetch(
|
|
9769
|
+
ctx,
|
|
9770
|
+
companyId,
|
|
9771
|
+
`/repos/${owner}/${repo}/pulls/${pullNumber}/files?per_page=100`
|
|
9772
|
+
);
|
|
9773
|
+
const files = filesData;
|
|
9774
|
+
const filenames = files.map((f) => f.filename);
|
|
9775
|
+
const hasDescription = Boolean(pr.body && pr.body.trim().length > 10);
|
|
9776
|
+
const hasTests = filenames.some(
|
|
9777
|
+
(f) => TEST_PATTERNS.some((pattern) => pattern.test(f))
|
|
9778
|
+
);
|
|
9779
|
+
const sensitiveFiles = filenames.filter(
|
|
9780
|
+
(f) => SENSITIVE_PATTERNS.some((pattern) => pattern.test(f))
|
|
9781
|
+
);
|
|
9782
|
+
return {
|
|
9783
|
+
hasDescription,
|
|
9784
|
+
hasTests,
|
|
9785
|
+
sensitiveFiles,
|
|
9786
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9152
9787
|
};
|
|
9153
|
-
await saveSyncCache(ctx, companyId, cache);
|
|
9154
|
-
return cache;
|
|
9155
9788
|
}
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
status: "not_synced",
|
|
9176
|
-
checkedAt,
|
|
9177
|
-
message: "Run sync to fetch open PRs and issues",
|
|
9178
|
-
lastSyncedAt: null,
|
|
9179
|
-
pullRequestCount: 0,
|
|
9180
|
-
issueCount: 0,
|
|
9181
|
-
recentPullRequests: [],
|
|
9182
|
-
recentIssues: [],
|
|
9183
|
-
lastErrors: []
|
|
9789
|
+
|
|
9790
|
+
// src/graphify/graph-generator.ts
|
|
9791
|
+
init_queries();
|
|
9792
|
+
async function generateHighLevelGraph(ctx, companyId) {
|
|
9793
|
+
const repos = await listRepos(ctx.db);
|
|
9794
|
+
const prs = await listPRs(ctx.db, { state: "open" });
|
|
9795
|
+
const nodes = repos.map((r) => ({
|
|
9796
|
+
id: `repo:${r.fullName}`,
|
|
9797
|
+
label: r.fullName,
|
|
9798
|
+
type: "repo",
|
|
9799
|
+
metadata: { language: r.language, private: r.private, defaultBranch: r.defaultBranch }
|
|
9800
|
+
}));
|
|
9801
|
+
const edges = [];
|
|
9802
|
+
for (const pr of prs) {
|
|
9803
|
+
const prNode = {
|
|
9804
|
+
id: `pr:${pr.repoFullName}#${pr.number}`,
|
|
9805
|
+
label: `#${pr.number}: ${pr.title}`,
|
|
9806
|
+
type: "pr",
|
|
9807
|
+
metadata: { state: pr.state, author: pr.author, draft: pr.draft }
|
|
9184
9808
|
};
|
|
9809
|
+
nodes.push(prNode);
|
|
9810
|
+
edges.push({
|
|
9811
|
+
source: prNode.id,
|
|
9812
|
+
target: `repo:${pr.repoFullName}`,
|
|
9813
|
+
label: `${pr.headBranch} \u2192 ${pr.baseBranch}`,
|
|
9814
|
+
type: "pr_target"
|
|
9815
|
+
});
|
|
9185
9816
|
}
|
|
9186
9817
|
return {
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
issueCount: cache.issues.length,
|
|
9193
|
-
recentPullRequests: cache.pullRequests.slice(0, 10),
|
|
9194
|
-
recentIssues: cache.issues.slice(0, 10),
|
|
9195
|
-
lastErrors: cache.errors
|
|
9818
|
+
nodes,
|
|
9819
|
+
edges,
|
|
9820
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9821
|
+
repoFullName: "*",
|
|
9822
|
+
level: "high"
|
|
9196
9823
|
};
|
|
9197
9824
|
}
|
|
9198
|
-
async function
|
|
9199
|
-
const
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
const
|
|
9205
|
-
const
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9825
|
+
async function generateCodeGraph(ctx, companyId, repoFullName) {
|
|
9826
|
+
const { data } = await githubFetch(
|
|
9827
|
+
ctx,
|
|
9828
|
+
companyId,
|
|
9829
|
+
`/repos/${repoFullName}/git/trees/HEAD?recursive=1`
|
|
9830
|
+
);
|
|
9831
|
+
const tree = data.tree;
|
|
9832
|
+
const nodes = [{
|
|
9833
|
+
id: `repo:${repoFullName}`,
|
|
9834
|
+
label: repoFullName,
|
|
9835
|
+
type: "repo",
|
|
9836
|
+
metadata: {}
|
|
9837
|
+
}];
|
|
9838
|
+
const edges = [];
|
|
9839
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
9840
|
+
for (const entry of tree) {
|
|
9841
|
+
const path2 = entry.path;
|
|
9842
|
+
const type = entry.type;
|
|
9843
|
+
if (type === "tree") {
|
|
9844
|
+
const depth = path2.split("/").length;
|
|
9845
|
+
if (depth <= 3) {
|
|
9846
|
+
dirs.add(path2);
|
|
9847
|
+
nodes.push({
|
|
9848
|
+
id: `dir:${repoFullName}/${path2}`,
|
|
9849
|
+
label: path2,
|
|
9850
|
+
type: "module",
|
|
9851
|
+
metadata: { depth }
|
|
9852
|
+
});
|
|
9853
|
+
const parentDir = path2.split("/").slice(0, -1).join("/");
|
|
9854
|
+
const parentId = parentDir ? `dir:${repoFullName}/${parentDir}` : `repo:${repoFullName}`;
|
|
9855
|
+
edges.push({
|
|
9856
|
+
source: parentId,
|
|
9857
|
+
target: `dir:${repoFullName}/${path2}`,
|
|
9858
|
+
label: "contains",
|
|
9859
|
+
type: "contains"
|
|
9860
|
+
});
|
|
9861
|
+
}
|
|
9862
|
+
} else if (type === "blob") {
|
|
9863
|
+
const depth = path2.split("/").length;
|
|
9864
|
+
if (depth <= 2 || /\.(json|ya?ml|toml|lock)$/.test(path2)) {
|
|
9865
|
+
nodes.push({
|
|
9866
|
+
id: `file:${repoFullName}/${path2}`,
|
|
9867
|
+
label: path2.split("/").pop(),
|
|
9868
|
+
type: "file",
|
|
9869
|
+
metadata: { path: path2, size: entry.size }
|
|
9870
|
+
});
|
|
9871
|
+
const parentDir = path2.split("/").slice(0, -1).join("/");
|
|
9872
|
+
const parentId = parentDir ? `dir:${repoFullName}/${parentDir}` : `repo:${repoFullName}`;
|
|
9873
|
+
edges.push({
|
|
9874
|
+
source: parentId,
|
|
9875
|
+
target: `file:${repoFullName}/${path2}`,
|
|
9876
|
+
label: "contains",
|
|
9877
|
+
type: "contains"
|
|
9878
|
+
});
|
|
9879
|
+
}
|
|
9214
9880
|
}
|
|
9215
|
-
};
|
|
9216
|
-
const res = await githubFetch(ctx, token, `/repos/${owner}/${repo}/hooks`, {
|
|
9217
|
-
method: "POST",
|
|
9218
|
-
headers: { "Content-Type": "application/json" },
|
|
9219
|
-
body: JSON.stringify(payload)
|
|
9220
|
-
});
|
|
9221
|
-
if (!res.ok) {
|
|
9222
|
-
const body = await res.text();
|
|
9223
|
-
throw new Error(`GitHub webhook registration failed (${res.status}): ${body}`);
|
|
9224
9881
|
}
|
|
9225
|
-
|
|
9226
|
-
|
|
9882
|
+
return {
|
|
9883
|
+
nodes,
|
|
9884
|
+
edges,
|
|
9885
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9227
9886
|
repoFullName,
|
|
9228
|
-
|
|
9229
|
-
hookId: hook.id,
|
|
9230
|
-
configuredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9231
|
-
inboundUrl
|
|
9887
|
+
level: "code"
|
|
9232
9888
|
};
|
|
9233
|
-
await ctx.state.set({ ...companyScope2(companyId), stateKey: WEBHOOK_STATE_KEY }, config);
|
|
9234
|
-
return config;
|
|
9235
|
-
}
|
|
9236
|
-
function requireCompanyId(input) {
|
|
9237
|
-
const companyId = input?.companyId;
|
|
9238
|
-
if (!companyId) {
|
|
9239
|
-
throw new Error("companyId is required");
|
|
9240
|
-
}
|
|
9241
|
-
return companyId;
|
|
9242
|
-
}
|
|
9243
|
-
async function handleGithubWebhook(input) {
|
|
9244
|
-
const ctx = workerCtx;
|
|
9245
|
-
if (!ctx) {
|
|
9246
|
-
return;
|
|
9247
|
-
}
|
|
9248
|
-
if (input.endpointKey !== GITHUB_WEBHOOK_ENDPOINT) {
|
|
9249
|
-
return;
|
|
9250
|
-
}
|
|
9251
|
-
const payload = input.parsedBody ?? {};
|
|
9252
|
-
const repoFullName = payload.repository?.full_name;
|
|
9253
|
-
if (!repoFullName) {
|
|
9254
|
-
return;
|
|
9255
|
-
}
|
|
9256
|
-
const companies = await ctx.companies.list();
|
|
9257
|
-
for (const company of companies) {
|
|
9258
|
-
const webhook = await loadWebhookConfig(ctx, company.id);
|
|
9259
|
-
if (!webhook || webhook.repoFullName !== repoFullName) {
|
|
9260
|
-
continue;
|
|
9261
|
-
}
|
|
9262
|
-
try {
|
|
9263
|
-
await runSync(ctx, company.id, "all");
|
|
9264
|
-
ctx.logger.info("GitHub webhook triggered sync", {
|
|
9265
|
-
companyId: company.id,
|
|
9266
|
-
repoFullName,
|
|
9267
|
-
action: payload.action ?? "unknown",
|
|
9268
|
-
requestId: input.requestId
|
|
9269
|
-
});
|
|
9270
|
-
} catch (err) {
|
|
9271
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
9272
|
-
ctx.logger.warn("GitHub webhook sync failed", { companyId: company.id, message });
|
|
9273
|
-
}
|
|
9274
|
-
}
|
|
9275
9889
|
}
|
|
9890
|
+
|
|
9891
|
+
// src/worker.ts
|
|
9892
|
+
init_queries();
|
|
9893
|
+
var pluginCtx = null;
|
|
9276
9894
|
var plugin = definePlugin({
|
|
9277
9895
|
async setup(ctx) {
|
|
9278
|
-
|
|
9279
|
-
ctx.
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
ctx.logger.info("
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
const token = await resolveGithubToken(ctx, cid);
|
|
9291
|
-
if (!token) {
|
|
9292
|
-
return {
|
|
9293
|
-
status: "degraded",
|
|
9294
|
-
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9295
|
-
message: auth.mode === "secret-ref" ? "Secret ID salvo, mas o Paperclip ainda n\xE3o resolve secret refs em plugins \u2014 cole o PAT abaixo ou aguarde PAP-2394" : "Cole um Personal Access Token (PAT) em Configura\xE7\xF5es e clique em Salvar",
|
|
9296
|
-
auth
|
|
9297
|
-
};
|
|
9298
|
-
}
|
|
9299
|
-
const res = await githubFetch(ctx, token, "/user");
|
|
9300
|
-
if (!res.ok) {
|
|
9301
|
-
return {
|
|
9302
|
-
status: "error",
|
|
9303
|
-
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9304
|
-
message: `GitHub API retornou ${res.status} \u2014 verifique escopos do PAT`,
|
|
9305
|
-
auth
|
|
9306
|
-
};
|
|
9896
|
+
pluginCtx = ctx;
|
|
9897
|
+
ctx.logger.info("GitHub Manager v2 starting");
|
|
9898
|
+
registerReviewTools(ctx);
|
|
9899
|
+
ctx.jobs.register("sync-github", async (job) => {
|
|
9900
|
+
ctx.logger.info("Running scheduled incremental sync");
|
|
9901
|
+
const companies = await ctx.companies.list();
|
|
9902
|
+
for (const company of companies) {
|
|
9903
|
+
try {
|
|
9904
|
+
await runIncrementalSync(ctx, company.id);
|
|
9905
|
+
} catch (err) {
|
|
9906
|
+
ctx.logger.error(`Sync failed for company ${company.id}: ${err}`);
|
|
9907
|
+
}
|
|
9307
9908
|
}
|
|
9308
|
-
const user = await res.json();
|
|
9309
|
-
return {
|
|
9310
|
-
status: "ok",
|
|
9311
|
-
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9312
|
-
login: user.login ?? "unknown",
|
|
9313
|
-
auth
|
|
9314
|
-
};
|
|
9315
9909
|
});
|
|
9316
9910
|
ctx.data.register("repos", async ({ companyId }) => {
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
}
|
|
9320
|
-
|
|
9321
|
-
})
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
const
|
|
9911
|
+
const repos = await listRepos(ctx.db);
|
|
9912
|
+
const lastSync = await getLastSyncTime(ctx.db);
|
|
9913
|
+
return { repos, lastSync };
|
|
9914
|
+
});
|
|
9915
|
+
ctx.data.register("pull-requests", async ({ companyId, filters }) => {
|
|
9916
|
+
const f = filters;
|
|
9917
|
+
const prs = await listPRs(ctx.db, f);
|
|
9918
|
+
return { pullRequests: prs };
|
|
9919
|
+
});
|
|
9920
|
+
ctx.data.register("card-prs", async ({ companyId, issueId }) => {
|
|
9921
|
+
const prs = await getLinksForCard(ctx.db, issueId);
|
|
9922
|
+
return { pullRequests: prs };
|
|
9923
|
+
});
|
|
9924
|
+
ctx.data.register("sync-status", async () => {
|
|
9925
|
+
const lastSync = await getLastSyncTime(ctx.db);
|
|
9926
|
+
const repos = await listRepos(ctx.db);
|
|
9927
|
+
const openPRs = await listPRs(ctx.db, { state: "open" });
|
|
9333
9928
|
return {
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9929
|
+
lastSync,
|
|
9930
|
+
repoCount: repos.length,
|
|
9931
|
+
openPRCount: openPRs.length
|
|
9337
9932
|
};
|
|
9338
9933
|
});
|
|
9339
|
-
ctx.
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
ctx.actions.register("saveGithubToken", async (input) => {
|
|
9343
|
-
const companyId = requireCompanyId(input);
|
|
9344
|
-
const token = input?.token;
|
|
9345
|
-
if (typeof token !== "string") {
|
|
9346
|
-
throw new Error("token is required");
|
|
9347
|
-
}
|
|
9348
|
-
await saveGithubPat(ctx, companyId, token);
|
|
9349
|
-
return { saved: true, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9350
|
-
});
|
|
9351
|
-
ctx.actions.register("saveGithubSecretRef", async (input) => {
|
|
9352
|
-
const companyId = requireCompanyId(input);
|
|
9353
|
-
const secretRef = input?.secretRef;
|
|
9354
|
-
if (typeof secretRef !== "string") {
|
|
9355
|
-
throw new Error("secretRef is required");
|
|
9356
|
-
}
|
|
9357
|
-
await saveGithubSecretRef(ctx, companyId, secretRef);
|
|
9358
|
-
return { saved: true, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9359
|
-
});
|
|
9360
|
-
ctx.actions.register("clearGithubAuth", async (input) => {
|
|
9361
|
-
const companyId = requireCompanyId(input);
|
|
9362
|
-
await clearGithubAuth(ctx, companyId);
|
|
9363
|
-
return { cleared: true, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9364
|
-
});
|
|
9365
|
-
ctx.actions.register("setTrackedRepos", async (input) => {
|
|
9366
|
-
const companyId = requireCompanyId(input);
|
|
9367
|
-
const repos = input?.repos;
|
|
9368
|
-
if (!Array.isArray(repos)) {
|
|
9369
|
-
throw new Error("repos array is required");
|
|
9934
|
+
ctx.data.register("graph-data", async ({ companyId, repoFullName, level }) => {
|
|
9935
|
+
if (level === "high") {
|
|
9936
|
+
return await generateHighLevelGraph(ctx, companyId);
|
|
9370
9937
|
}
|
|
9371
|
-
|
|
9372
|
-
await ctx.state.set({ ...companyScope2(companyId), stateKey: TRACKED_REPOS_KEY }, normalized);
|
|
9373
|
-
return { saved: true, repos: normalized, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9938
|
+
return await generateCodeGraph(ctx, companyId, repoFullName);
|
|
9374
9939
|
});
|
|
9375
|
-
ctx.
|
|
9376
|
-
const
|
|
9377
|
-
|
|
9378
|
-
return {
|
|
9379
|
-
syncedAt: cache.syncedAt,
|
|
9380
|
-
pullRequestCount: cache.pullRequests.length,
|
|
9381
|
-
errors: cache.errors
|
|
9382
|
-
};
|
|
9940
|
+
ctx.data.register("available-agents", async ({ companyId }) => {
|
|
9941
|
+
const agents = await ctx.agents.list({ companyId });
|
|
9942
|
+
return { agents };
|
|
9383
9943
|
});
|
|
9384
|
-
ctx.actions.register("
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
return {
|
|
9388
|
-
syncedAt: cache.syncedAt,
|
|
9389
|
-
issueCount: cache.issues.length,
|
|
9390
|
-
errors: cache.errors
|
|
9391
|
-
};
|
|
9944
|
+
ctx.actions.register("save-token", async ({ companyId, token }) => {
|
|
9945
|
+
await saveGithubPAT(ctx, companyId, token);
|
|
9946
|
+
return { ok: true };
|
|
9392
9947
|
});
|
|
9393
|
-
ctx.actions.register("
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
return {
|
|
9397
|
-
syncedAt: cache.syncedAt,
|
|
9398
|
-
pullRequestCount: cache.pullRequests.length,
|
|
9399
|
-
issueCount: cache.issues.length,
|
|
9400
|
-
errors: cache.errors
|
|
9401
|
-
};
|
|
9948
|
+
ctx.actions.register("save-secret-ref", async ({ companyId, secretRef }) => {
|
|
9949
|
+
await saveGithubSecretRef(ctx, companyId, secretRef);
|
|
9950
|
+
return { ok: true };
|
|
9402
9951
|
});
|
|
9403
|
-
ctx.actions.register("
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9952
|
+
ctx.actions.register("test-connection", async ({ companyId }) => {
|
|
9953
|
+
try {
|
|
9954
|
+
const token = await resolveGithubToken(ctx, companyId);
|
|
9955
|
+
const { data } = await githubFetch(ctx, companyId, "/user");
|
|
9956
|
+
const user = data;
|
|
9957
|
+
return { ok: true, login: user.login };
|
|
9958
|
+
} catch (err) {
|
|
9959
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
9409
9960
|
}
|
|
9410
|
-
const config = await registerGithubWebhook(ctx, companyId, repoFullName, events);
|
|
9411
|
-
return { saved: true, config };
|
|
9412
9961
|
});
|
|
9413
|
-
ctx.
|
|
9414
|
-
const
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9962
|
+
ctx.actions.register("add-repo", async ({ companyId, fullName }) => {
|
|
9963
|
+
const { data } = await githubFetch(ctx, companyId, `/repos/${fullName}`);
|
|
9964
|
+
const rd = data;
|
|
9965
|
+
await upsertRepo(ctx.db, {
|
|
9966
|
+
id: rd.id,
|
|
9967
|
+
fullName: rd.full_name,
|
|
9968
|
+
owner: rd.owner.login,
|
|
9969
|
+
name: rd.name,
|
|
9970
|
+
private: rd.private,
|
|
9971
|
+
defaultBranch: rd.default_branch,
|
|
9972
|
+
htmlUrl: rd.html_url,
|
|
9973
|
+
description: rd.description,
|
|
9974
|
+
language: rd.language,
|
|
9975
|
+
topics: rd.topics ?? [],
|
|
9976
|
+
updatedAt: rd.updated_at
|
|
9977
|
+
});
|
|
9978
|
+
return { ok: true };
|
|
9979
|
+
});
|
|
9980
|
+
ctx.actions.register("sync-all", async ({ companyId }) => {
|
|
9981
|
+
await runFullSync(ctx, companyId);
|
|
9982
|
+
return { ok: true };
|
|
9983
|
+
});
|
|
9984
|
+
ctx.actions.register("sync-incremental", async ({ companyId }) => {
|
|
9985
|
+
await runIncrementalSync(ctx, companyId);
|
|
9986
|
+
return { ok: true };
|
|
9987
|
+
});
|
|
9988
|
+
ctx.actions.register("link-pr-to-card", async ({ prId, issueId }) => {
|
|
9989
|
+
await linkPRToCard(ctx.db, prId, issueId, "manual");
|
|
9990
|
+
return { ok: true };
|
|
9991
|
+
});
|
|
9992
|
+
ctx.actions.register("request-review", async ({ companyId, prId, repoFullName, prNumber, agentId }) => {
|
|
9993
|
+
const repo = await getRepoByFullName(ctx.db, repoFullName);
|
|
9994
|
+
if (!repo) throw new Error(`Repo ${repoFullName} not found`);
|
|
9995
|
+
const [owner, repoName] = repoFullName.split("/");
|
|
9996
|
+
await ctx.agents.invoke(
|
|
9997
|
+
agentId,
|
|
9998
|
+
companyId,
|
|
9999
|
+
{
|
|
10000
|
+
prompt: `Please review PR #${prNumber} in ${repoFullName}. Use the github_get_pull_request_diff tool with owner="${owner}", repo="${repoName}", pull_number=${prNumber} to get the diff, then provide a thorough code review. Post your findings as inline comments using github_create_review_comment and submit your final verdict using github_submit_pr_review.`
|
|
9425
10001
|
}
|
|
10002
|
+
);
|
|
10003
|
+
return { ok: true };
|
|
10004
|
+
});
|
|
10005
|
+
ctx.actions.register("run-quick-check", async ({ companyId, repoFullName, prNumber }) => {
|
|
10006
|
+
const [owner, repo] = repoFullName.split("/");
|
|
10007
|
+
const result = await runQuickCheck(ctx, companyId, owner, repo, prNumber);
|
|
10008
|
+
return result;
|
|
10009
|
+
});
|
|
10010
|
+
ctx.actions.register("generate-graph", async ({ companyId, repoFullName, level }) => {
|
|
10011
|
+
if (level === "high") {
|
|
10012
|
+
return await generateHighLevelGraph(ctx, companyId);
|
|
9426
10013
|
}
|
|
10014
|
+
return await generateCodeGraph(ctx, companyId, repoFullName);
|
|
10015
|
+
});
|
|
10016
|
+
ctx.events.on("company.created", async (event) => {
|
|
10017
|
+
await ctx.agents.managed.reconcile("github-reviewer", event.companyId);
|
|
9427
10018
|
});
|
|
9428
10019
|
},
|
|
9429
10020
|
async onHealth() {
|
|
9430
|
-
return { status: "ok", message: "GitHub Manager
|
|
10021
|
+
return { status: "ok", message: "GitHub Manager v2 running" };
|
|
9431
10022
|
},
|
|
9432
10023
|
async onWebhook(input) {
|
|
9433
|
-
|
|
10024
|
+
if (!pluginCtx) throw new Error("Plugin not initialized");
|
|
10025
|
+
await handleGithubWebhook(pluginCtx, input);
|
|
10026
|
+
},
|
|
10027
|
+
async onShutdown() {
|
|
10028
|
+
pluginCtx = null;
|
|
9434
10029
|
}
|
|
9435
10030
|
});
|
|
9436
10031
|
var worker_default = plugin;
|