@gaud_erp/paperclip-github-manager 0.4.0 → 1.1.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 +133 -61
- package/dist/manifest.js +159 -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 +998 -829
- 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,949 +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
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
const trimmed = secretRef.trim();
|
|
8945
|
-
if (!UUID_RE.test(trimmed)) {
|
|
8946
|
-
throw new Error("Secret ID must be a UUID from Company \u2192 Settings \u2192 Secrets");
|
|
8947
|
-
}
|
|
8948
|
-
await ctx.state.set(
|
|
8949
|
-
{ ...companyScope(companyId), stateKey: GITHUB_SECRET_REF_STATE_KEY },
|
|
8950
|
-
trimmed
|
|
8951
|
-
);
|
|
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.");
|
|
8952
9194
|
}
|
|
8953
|
-
async function
|
|
8954
|
-
await ctx.state.set({
|
|
8955
|
-
|
|
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);
|
|
8956
9201
|
}
|
|
8957
|
-
async function
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
if (secretRef) {
|
|
8964
|
-
return { configured: true, mode: "secret-ref" };
|
|
8965
|
-
}
|
|
8966
|
-
return { configured: false, mode: "none" };
|
|
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);
|
|
8967
9208
|
}
|
|
8968
|
-
async function resolveGithubToken(ctx, companyId) {
|
|
8969
|
-
const pat = await loadGithubPat(ctx, companyId);
|
|
8970
|
-
if (pat) {
|
|
8971
|
-
return pat;
|
|
8972
|
-
}
|
|
8973
|
-
const secretRef = await loadGithubSecretRef(ctx, companyId);
|
|
8974
|
-
if (!secretRef) {
|
|
8975
|
-
return null;
|
|
8976
|
-
}
|
|
8977
|
-
try {
|
|
8978
|
-
return await ctx.secrets.resolve(secretRef);
|
|
8979
|
-
} catch {
|
|
8980
|
-
return null;
|
|
8981
|
-
}
|
|
8982
|
-
}
|
|
8983
|
-
|
|
8984
|
-
// src/github-env.ts
|
|
8985
9209
|
function getGithubApiBase() {
|
|
8986
|
-
const
|
|
8987
|
-
return
|
|
8988
|
-
}
|
|
8989
|
-
function getGithubDefaultOwner() {
|
|
8990
|
-
const raw = process.env.GITHUB_DEFAULT_OWNER?.trim();
|
|
8991
|
-
return raw && raw.length > 0 ? raw : null;
|
|
9210
|
+
const base = process.env.GITHUB_API_URL?.trim();
|
|
9211
|
+
return base ? base.replace(/\/+$/, "") : "https://api.github.com";
|
|
8992
9212
|
}
|
|
8993
9213
|
|
|
8994
|
-
// src/github-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
const
|
|
8998
|
-
const url = path2.startsWith("http") ? path2 : `${
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
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
|
|
9007
9228
|
});
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
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}`);
|
|
9013
9242
|
}
|
|
9014
|
-
|
|
9015
|
-
}
|
|
9016
|
-
function buildInboundWebhookUrl(pluginId, baseUrl = "http://127.0.0.1:3100") {
|
|
9017
|
-
return `${baseUrl.replace(/\/+$/, "")}/api/plugins/${pluginId}/webhooks/${GITHUB_WEBHOOK_ENDPOINT}`;
|
|
9243
|
+
const data = await resp.json();
|
|
9244
|
+
return { data, rateLimit };
|
|
9018
9245
|
}
|
|
9019
|
-
function
|
|
9020
|
-
|
|
9021
|
-
const reset = res.headers.get("x-ratelimit-reset");
|
|
9022
|
-
if (res.status !== 403 || remaining !== "0") {
|
|
9023
|
-
return null;
|
|
9024
|
-
}
|
|
9025
|
-
const resetAt = reset && Number.isFinite(Number(reset)) ? new Date(Number(reset) * 1e3).toISOString() : "unknown";
|
|
9026
|
-
return `GitHub API rate limit exceeded. Retry after ${resetAt}.`;
|
|
9027
|
-
}
|
|
9028
|
-
async function assertGithubResponse(res, action) {
|
|
9029
|
-
if (res.ok) {
|
|
9030
|
-
return;
|
|
9031
|
-
}
|
|
9032
|
-
const rateLimit = formatRateLimitMessage(res);
|
|
9033
|
-
const body = await res.text().catch(() => "");
|
|
9034
|
-
const detail = body.length > 0 && body.length < 500 ? `: ${body}` : "";
|
|
9035
|
-
throw new Error(rateLimit ?? `GitHub ${action} failed (HTTP ${res.status})${detail}`);
|
|
9246
|
+
function isRateLimitSafe(rateLimit, threshold = 100) {
|
|
9247
|
+
return rateLimit.remaining > threshold;
|
|
9036
9248
|
}
|
|
9037
9249
|
|
|
9038
|
-
// src/
|
|
9250
|
+
// src/review/review-tools.ts
|
|
9039
9251
|
var MAX_DIFF_CHARS = 12e4;
|
|
9040
|
-
var
|
|
9041
|
-
function
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
}
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
}
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
}
|
|
9082
|
-
const head = diff.slice(0, MAX_DIFF_CHARS);
|
|
9083
|
-
const notice = `
|
|
9084
|
-
|
|
9085
|
-
\u2026 [diff truncated: ${diff.length} chars total, showing first ${MAX_DIFF_CHARS}] \u2026
|
|
9086
|
-
`;
|
|
9087
|
-
return {
|
|
9088
|
-
diff: head + notice,
|
|
9089
|
-
truncated: true,
|
|
9090
|
-
originalLength: diff.length
|
|
9091
|
-
};
|
|
9092
|
-
}
|
|
9093
|
-
async function fetchPullRequestDiff(ctx, token, { owner, repo }, prNumber) {
|
|
9094
|
-
const metaRes = await githubFetch(
|
|
9095
|
-
ctx,
|
|
9096
|
-
token,
|
|
9097
|
-
`/repos/${owner}/${repo}/pulls/${prNumber}`
|
|
9098
|
-
);
|
|
9099
|
-
await assertGithubResponse(metaRes, "fetch pull request");
|
|
9100
|
-
const pr = await metaRes.json();
|
|
9101
|
-
const diffRes = await githubFetch(
|
|
9102
|
-
ctx,
|
|
9103
|
-
token,
|
|
9104
|
-
`/repos/${owner}/${repo}/pulls/${prNumber}`,
|
|
9105
|
-
{ headers: { Accept: "application/vnd.github.diff" } }
|
|
9106
|
-
);
|
|
9107
|
-
await assertGithubResponse(diffRes, "fetch pull request diff");
|
|
9108
|
-
let diffText = await diffRes.text();
|
|
9109
|
-
const { diff, truncated, originalLength } = truncateDiff(diffText);
|
|
9110
|
-
diffText = diff;
|
|
9111
|
-
let files = [];
|
|
9112
|
-
if (truncated) {
|
|
9113
|
-
const filesRes = await githubFetch(
|
|
9114
|
-
ctx,
|
|
9115
|
-
token,
|
|
9116
|
-
`/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`
|
|
9117
|
-
);
|
|
9118
|
-
if (filesRes.ok) {
|
|
9119
|
-
const rows = await filesRes.json();
|
|
9120
|
-
files = rows.map((f) => ({
|
|
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) => ({
|
|
9121
9293
|
filename: f.filename,
|
|
9122
9294
|
status: f.status,
|
|
9123
9295
|
additions: f.additions,
|
|
9124
9296
|
deletions: f.deletions
|
|
9125
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
|
+
};
|
|
9126
9306
|
}
|
|
9127
|
-
}
|
|
9128
|
-
const payload = {
|
|
9129
|
-
pullRequest: {
|
|
9130
|
-
number: pr.number,
|
|
9131
|
-
title: pr.title,
|
|
9132
|
-
state: pr.state,
|
|
9133
|
-
htmlUrl: pr.html_url,
|
|
9134
|
-
author: pr.user?.login ?? "unknown",
|
|
9135
|
-
headSha: pr.head?.sha ?? null,
|
|
9136
|
-
headRef: pr.head?.ref ?? null,
|
|
9137
|
-
baseRef: pr.base?.ref ?? null,
|
|
9138
|
-
draft: pr.draft ?? false,
|
|
9139
|
-
mergedAt: pr.merged_at ?? null
|
|
9140
|
-
},
|
|
9141
|
-
diff: diffText,
|
|
9142
|
-
truncated,
|
|
9143
|
-
diffOriginalLength: originalLength,
|
|
9144
|
-
changedFiles: files
|
|
9145
|
-
};
|
|
9146
|
-
return toolJson(
|
|
9147
|
-
payload,
|
|
9148
|
-
truncated ? `PR #${prNumber} "${pr.title}" \u2014 diff truncated (${originalLength} chars)` : `PR #${prNumber} "${pr.title}" \u2014 full diff (${originalLength} chars)`
|
|
9149
|
-
);
|
|
9150
|
-
}
|
|
9151
|
-
async function createReviewComment(ctx, token, params) {
|
|
9152
|
-
const { owner, repo } = resolveOwnerRepo(params);
|
|
9153
|
-
const prNumber = requireNumber(params.pr_number, "pr_number");
|
|
9154
|
-
const commitId = requireString(params.commit_id, "commit_id");
|
|
9155
|
-
const path2 = requireString(params.path, "path");
|
|
9156
|
-
const line = requireNumber(params.line, "line");
|
|
9157
|
-
const body = requireString(params.body, "body");
|
|
9158
|
-
const res = await githubFetch(
|
|
9159
|
-
ctx,
|
|
9160
|
-
token,
|
|
9161
|
-
`/repos/${owner}/${repo}/pulls/${prNumber}/comments`,
|
|
9162
|
-
{
|
|
9163
|
-
method: "POST",
|
|
9164
|
-
headers: { "Content-Type": "application/json" },
|
|
9165
|
-
body: JSON.stringify({
|
|
9166
|
-
body,
|
|
9167
|
-
commit_id: commitId,
|
|
9168
|
-
path: path2,
|
|
9169
|
-
line,
|
|
9170
|
-
side: "RIGHT"
|
|
9171
|
-
})
|
|
9172
|
-
}
|
|
9173
|
-
);
|
|
9174
|
-
await assertGithubResponse(res, "create review comment");
|
|
9175
|
-
const comment = await res.json();
|
|
9176
|
-
return toolJson(
|
|
9177
|
-
{ id: comment.id, htmlUrl: comment.html_url ?? null },
|
|
9178
|
-
`Review comment created on ${path2}:${line}`
|
|
9179
|
-
);
|
|
9180
|
-
}
|
|
9181
|
-
async function submitPrReview(ctx, token, params) {
|
|
9182
|
-
const { owner, repo } = resolveOwnerRepo(params);
|
|
9183
|
-
const prNumber = requireNumber(params.pr_number, "pr_number");
|
|
9184
|
-
const event = requireString(params.event, "event").toUpperCase();
|
|
9185
|
-
const body = requireString(params.body, "body");
|
|
9186
|
-
if (!["APPROVE", "REQUEST_CHANGES", "COMMENT"].includes(event)) {
|
|
9187
|
-
throw new Error("event must be APPROVE, REQUEST_CHANGES, or COMMENT");
|
|
9188
|
-
}
|
|
9189
|
-
const res = await githubFetch(
|
|
9190
|
-
ctx,
|
|
9191
|
-
token,
|
|
9192
|
-
`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,
|
|
9193
|
-
{
|
|
9194
|
-
method: "POST",
|
|
9195
|
-
headers: { "Content-Type": "application/json" },
|
|
9196
|
-
body: JSON.stringify({ event, body })
|
|
9197
|
-
}
|
|
9198
|
-
);
|
|
9199
|
-
await assertGithubResponse(res, "submit pull request review");
|
|
9200
|
-
const review = await res.json();
|
|
9201
|
-
return toolJson(
|
|
9202
|
-
{ id: review.id, state: review.state, htmlUrl: review.html_url ?? null },
|
|
9203
|
-
`Review submitted: ${review.state}`
|
|
9204
|
-
);
|
|
9205
|
-
}
|
|
9206
|
-
async function readFileContent(ctx, token, params) {
|
|
9207
|
-
const { owner, repo } = resolveOwnerRepo(params);
|
|
9208
|
-
const path2 = requireString(params.path, "path").replace(/^\/+/, "");
|
|
9209
|
-
const ref = typeof params.ref === "string" && params.ref.trim().length > 0 ? params.ref.trim() : void 0;
|
|
9210
|
-
const query = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
|
9211
|
-
const res = await githubFetch(
|
|
9212
|
-
ctx,
|
|
9213
|
-
token,
|
|
9214
|
-
`/repos/${owner}/${repo}/contents/${path2.split("/").map(encodeURIComponent).join("/")}${query}`
|
|
9215
|
-
);
|
|
9216
|
-
await assertGithubResponse(res, "read file content");
|
|
9217
|
-
const file = await res.json();
|
|
9218
|
-
if (file.type !== "file" || !file.content) {
|
|
9219
|
-
throw new Error(`Path is not a file or is too large for API: ${path2}`);
|
|
9220
|
-
}
|
|
9221
|
-
const decoded = Buffer.from(file.content.replace(/\n/g, ""), "base64").toString("utf8");
|
|
9222
|
-
if (decoded.length > MAX_FILE_PATCH_CHARS * 4) {
|
|
9223
|
-
return toolJson(
|
|
9224
|
-
{
|
|
9225
|
-
path: path2,
|
|
9226
|
-
ref: ref ?? "default",
|
|
9227
|
-
truncated: true,
|
|
9228
|
-
size: file.size ?? decoded.length,
|
|
9229
|
-
content: decoded.slice(0, MAX_FILE_PATCH_CHARS * 4),
|
|
9230
|
-
message: `File content truncated to ${MAX_FILE_PATCH_CHARS * 4} characters`
|
|
9231
|
-
},
|
|
9232
|
-
`File ${path2} (truncated)`
|
|
9233
|
-
);
|
|
9234
|
-
}
|
|
9235
|
-
return toolJson(
|
|
9236
|
-
{ path: path2, ref: ref ?? "default", sha: file.sha ?? null, size: file.size ?? decoded.length, content: decoded },
|
|
9237
|
-
`File ${path2} (${decoded.length} chars)`
|
|
9238
|
-
);
|
|
9239
|
-
}
|
|
9240
|
-
async function listRepositories(ctx, token, params) {
|
|
9241
|
-
const defaultOwner = getGithubDefaultOwner();
|
|
9242
|
-
const owner = typeof params.owner === "string" && params.owner.trim().length > 0 ? params.owner.trim() : defaultOwner;
|
|
9243
|
-
const perPage = Math.min(
|
|
9244
|
-
100,
|
|
9245
|
-
typeof params.per_page === "number" ? params.per_page : Number(params.per_page) || 30
|
|
9246
9307
|
);
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
const orgProbe = await githubFetch(ctx, token, `/orgs/${owner}`);
|
|
9250
|
-
path2 = orgProbe.ok ? `/orgs/${owner}/repos?per_page=${perPage}&sort=updated` : `/users/${owner}/repos?per_page=${perPage}&sort=updated`;
|
|
9251
|
-
} else {
|
|
9252
|
-
path2 = `/user/repos?per_page=${perPage}&sort=updated&affiliation=owner,organization_member`;
|
|
9253
|
-
}
|
|
9254
|
-
const res = await githubFetch(ctx, token, path2);
|
|
9255
|
-
await assertGithubResponse(res, "list repositories");
|
|
9256
|
-
const rows = await res.json();
|
|
9257
|
-
const repos = rows.map((r) => ({
|
|
9258
|
-
id: r.id,
|
|
9259
|
-
fullName: r.full_name,
|
|
9260
|
-
private: r.private,
|
|
9261
|
-
htmlUrl: r.html_url,
|
|
9262
|
-
defaultBranch: r.default_branch,
|
|
9263
|
-
updatedAt: r.updated_at
|
|
9264
|
-
}));
|
|
9265
|
-
return toolJson({ owner: owner ?? null, count: repos.length, repos });
|
|
9266
|
-
}
|
|
9267
|
-
async function searchIssues(ctx, token, params) {
|
|
9268
|
-
const query = requireString(params.q ?? params.query, "q");
|
|
9269
|
-
const perPage = Math.min(
|
|
9270
|
-
100,
|
|
9271
|
-
typeof params.per_page === "number" ? params.per_page : Number(params.per_page) || 20
|
|
9272
|
-
);
|
|
9273
|
-
const res = await githubFetch(
|
|
9274
|
-
ctx,
|
|
9275
|
-
token,
|
|
9276
|
-
`/search/issues?q=${encodeURIComponent(query)}&per_page=${perPage}`
|
|
9277
|
-
);
|
|
9278
|
-
await assertGithubResponse(res, "search issues");
|
|
9279
|
-
const body = await res.json();
|
|
9280
|
-
const issues = body.items.map((item) => {
|
|
9281
|
-
const repoMatch = item.repository_url.match(/repos\/([^/]+)\/([^/]+)$/);
|
|
9282
|
-
const repoFullName = repoMatch && repoMatch[1] && repoMatch[2] ? `${repoMatch[1]}/${repoMatch[2]}` : null;
|
|
9283
|
-
return {
|
|
9284
|
-
id: item.id,
|
|
9285
|
-
number: item.number,
|
|
9286
|
-
title: item.title,
|
|
9287
|
-
state: item.state,
|
|
9288
|
-
htmlUrl: item.html_url,
|
|
9289
|
-
repoFullName,
|
|
9290
|
-
updatedAt: item.updated_at
|
|
9291
|
-
};
|
|
9292
|
-
});
|
|
9293
|
-
return toolJson({
|
|
9294
|
-
totalCount: body.total_count,
|
|
9295
|
-
incompleteResults: body.incomplete_results ?? false,
|
|
9296
|
-
count: issues.length,
|
|
9297
|
-
issues
|
|
9298
|
-
});
|
|
9299
|
-
}
|
|
9300
|
-
var ownerRepoSchema = {
|
|
9301
|
-
type: "object",
|
|
9302
|
-
properties: {
|
|
9303
|
-
owner: { type: "string", description: "Repository owner (user or org)" },
|
|
9304
|
-
repo: { type: "string", description: "Repository name" }
|
|
9305
|
-
},
|
|
9306
|
-
required: ["owner", "repo"]
|
|
9307
|
-
};
|
|
9308
|
-
function registerGithubReviewTools(ctx) {
|
|
9309
|
-
const wrap = (name, declaration, handler) => {
|
|
9310
|
-
ctx.tools.register(name, declaration, async (params, runCtx) => {
|
|
9311
|
-
try {
|
|
9312
|
-
const token = await resolveTokenForRun(ctx, runCtx);
|
|
9313
|
-
return await handler(ctx, token, params ?? {});
|
|
9314
|
-
} catch (err) {
|
|
9315
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
9316
|
-
return { error: message, content: message };
|
|
9317
|
-
}
|
|
9318
|
-
});
|
|
9319
|
-
};
|
|
9320
|
-
wrap(
|
|
9321
|
-
"github_get_pull_request_diff",
|
|
9308
|
+
ctx.tools.register(
|
|
9309
|
+
"github_read_file_content",
|
|
9322
9310
|
{
|
|
9323
|
-
displayName: "
|
|
9324
|
-
description: "
|
|
9311
|
+
displayName: "Read File",
|
|
9312
|
+
description: "Read a file from a GitHub repository",
|
|
9325
9313
|
parametersSchema: {
|
|
9326
|
-
|
|
9314
|
+
type: "object",
|
|
9327
9315
|
properties: {
|
|
9328
|
-
|
|
9329
|
-
|
|
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)" }
|
|
9330
9320
|
},
|
|
9331
|
-
required: ["owner", "repo", "
|
|
9321
|
+
required: ["owner", "repo", "path"]
|
|
9332
9322
|
}
|
|
9333
9323
|
},
|
|
9334
|
-
async (
|
|
9335
|
-
const { owner, repo } =
|
|
9336
|
-
const
|
|
9337
|
-
|
|
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
|
+
};
|
|
9338
9341
|
}
|
|
9339
9342
|
);
|
|
9340
|
-
|
|
9343
|
+
ctx.tools.register(
|
|
9341
9344
|
"github_create_review_comment",
|
|
9342
9345
|
{
|
|
9343
|
-
displayName: "
|
|
9344
|
-
description: "
|
|
9346
|
+
displayName: "Add Review Comment",
|
|
9347
|
+
description: "Add an inline review comment to a pull request",
|
|
9345
9348
|
parametersSchema: {
|
|
9346
|
-
|
|
9349
|
+
type: "object",
|
|
9347
9350
|
properties: {
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
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)" }
|
|
9354
9358
|
},
|
|
9355
|
-
required: ["owner", "repo", "
|
|
9359
|
+
required: ["owner", "repo", "pull_number", "commit_id", "path", "line", "body"]
|
|
9356
9360
|
}
|
|
9357
9361
|
},
|
|
9358
|
-
async (
|
|
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
|
+
}
|
|
9359
9372
|
);
|
|
9360
|
-
|
|
9373
|
+
ctx.tools.register(
|
|
9361
9374
|
"github_submit_pr_review",
|
|
9362
9375
|
{
|
|
9363
|
-
displayName: "Submit PR
|
|
9364
|
-
description: "
|
|
9376
|
+
displayName: "Submit PR Review",
|
|
9377
|
+
description: "Submit a pull request review with a verdict",
|
|
9365
9378
|
parametersSchema: {
|
|
9366
|
-
|
|
9367
|
-
properties: {
|
|
9368
|
-
...ownerRepoSchema.properties,
|
|
9369
|
-
pr_number: { type: "integer" },
|
|
9370
|
-
event: {
|
|
9371
|
-
type: "string",
|
|
9372
|
-
enum: ["APPROVE", "REQUEST_CHANGES", "COMMENT"],
|
|
9373
|
-
description: "Review verdict"
|
|
9374
|
-
},
|
|
9375
|
-
body: { type: "string", description: "Summary comment for the review" }
|
|
9376
|
-
},
|
|
9377
|
-
required: ["owner", "repo", "pr_number", "event", "body"]
|
|
9378
|
-
}
|
|
9379
|
-
},
|
|
9380
|
-
async (ctx2, token, params) => submitPrReview(ctx2, token, params)
|
|
9381
|
-
);
|
|
9382
|
-
wrap(
|
|
9383
|
-
"github_read_file_content",
|
|
9384
|
-
{
|
|
9385
|
-
displayName: "Read repository file",
|
|
9386
|
-
description: "Reads full file content from a repository at an optional ref (branch or SHA).",
|
|
9387
|
-
parametersSchema: {
|
|
9388
|
-
...ownerRepoSchema,
|
|
9379
|
+
type: "object",
|
|
9389
9380
|
properties: {
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
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)" }
|
|
9393
9386
|
},
|
|
9394
|
-
required: ["owner", "repo", "
|
|
9387
|
+
required: ["owner", "repo", "pull_number", "event", "body"]
|
|
9395
9388
|
}
|
|
9396
9389
|
},
|
|
9397
|
-
async (
|
|
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
|
+
}
|
|
9398
9400
|
);
|
|
9399
|
-
|
|
9401
|
+
ctx.tools.register(
|
|
9400
9402
|
"github_list_repositories",
|
|
9401
9403
|
{
|
|
9402
|
-
displayName: "List
|
|
9403
|
-
description: "
|
|
9404
|
-
parametersSchema: {
|
|
9405
|
-
type: "object",
|
|
9406
|
-
properties: {
|
|
9407
|
-
owner: {
|
|
9408
|
-
type: "string",
|
|
9409
|
-
description: "Org or user (defaults to GITHUB_DEFAULT_OWNER or authenticated user)"
|
|
9410
|
-
},
|
|
9411
|
-
per_page: { type: "integer", description: "Page size (max 100)" }
|
|
9412
|
-
}
|
|
9413
|
-
}
|
|
9404
|
+
displayName: "List Repositories",
|
|
9405
|
+
description: "List tracked GitHub repositories",
|
|
9406
|
+
parametersSchema: { type: "object", properties: {} }
|
|
9414
9407
|
},
|
|
9415
|
-
async (
|
|
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
9416
|
);
|
|
9417
|
-
|
|
9417
|
+
ctx.tools.register(
|
|
9418
9418
|
"github_search_issues",
|
|
9419
9419
|
{
|
|
9420
|
-
displayName: "Search
|
|
9421
|
-
description: "
|
|
9420
|
+
displayName: "Search Issues",
|
|
9421
|
+
description: "Search GitHub issues and PRs using GitHub search syntax",
|
|
9422
9422
|
parametersSchema: {
|
|
9423
9423
|
type: "object",
|
|
9424
9424
|
properties: {
|
|
9425
|
-
|
|
9426
|
-
per_page: { type: "integer", description: "Results per page (max 100)" }
|
|
9425
|
+
query: { type: "string", description: "GitHub search query (e.g. 'is:open label:bug')" }
|
|
9427
9426
|
},
|
|
9428
|
-
required: ["
|
|
9427
|
+
required: ["query"]
|
|
9429
9428
|
}
|
|
9430
9429
|
},
|
|
9431
|
-
async (
|
|
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
|
+
}
|
|
9432
9448
|
);
|
|
9433
9449
|
}
|
|
9434
9450
|
|
|
9435
|
-
// src/
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
var
|
|
9441
|
-
var
|
|
9442
|
-
function
|
|
9443
|
-
|
|
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];
|
|
9444
9468
|
}
|
|
9445
|
-
async function
|
|
9446
|
-
const
|
|
9447
|
-
|
|
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;
|
|
9448
9475
|
}
|
|
9449
|
-
|
|
9450
|
-
|
|
9476
|
+
|
|
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
|
+
}
|
|
9451
9492
|
}
|
|
9452
|
-
async function
|
|
9453
|
-
const
|
|
9454
|
-
|
|
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}`);
|
|
9455
9532
|
}
|
|
9456
|
-
async function
|
|
9457
|
-
const
|
|
9458
|
-
const
|
|
9459
|
-
if (!
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
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}`);
|
|
9567
|
+
}
|
|
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
|
+
}
|
|
9466
9593
|
}
|
|
9467
|
-
|
|
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(
|
|
9468
9599
|
ctx,
|
|
9469
|
-
|
|
9470
|
-
`/
|
|
9600
|
+
companyId,
|
|
9601
|
+
`/repos/${fullName}/pulls?state=all&sort=updated&direction=desc&per_page=100&since=${since}`
|
|
9471
9602
|
);
|
|
9472
|
-
if (!
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
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
|
|
9478
9626
|
};
|
|
9627
|
+
await upsertPR(ctx.db, pr);
|
|
9628
|
+
await detectAndLinkCards(ctx, pr.id, pr.headBranch, pr.title);
|
|
9479
9629
|
}
|
|
9480
|
-
|
|
9481
|
-
const repos = rows.map((r) => ({
|
|
9482
|
-
id: r.id,
|
|
9483
|
-
fullName: r.full_name,
|
|
9484
|
-
private: r.private,
|
|
9485
|
-
htmlUrl: r.html_url,
|
|
9486
|
-
updatedAt: r.updated_at,
|
|
9487
|
-
defaultBranch: r.default_branch
|
|
9488
|
-
}));
|
|
9489
|
-
return { status: "ok", checkedAt, repos };
|
|
9490
|
-
}
|
|
9491
|
-
async function resolveTrackedRepos(ctx, companyId) {
|
|
9492
|
-
const tracked = await ctx.state.get({ ...companyScope2(companyId), stateKey: TRACKED_REPOS_KEY });
|
|
9493
|
-
if (Array.isArray(tracked) && tracked.length > 0) {
|
|
9494
|
-
return tracked.filter((r) => typeof r === "string").slice(0, MAX_REPOS_PER_SYNC);
|
|
9495
|
-
}
|
|
9496
|
-
const reposData = await listRepos(ctx, companyId);
|
|
9497
|
-
return reposData.repos.slice(0, MAX_REPOS_PER_SYNC).map((r) => r.fullName);
|
|
9630
|
+
return items.length;
|
|
9498
9631
|
}
|
|
9499
|
-
async function
|
|
9500
|
-
const {
|
|
9501
|
-
const res = await githubFetch(
|
|
9632
|
+
async function syncRepoIssues(ctx, companyId, repoId, fullName, since) {
|
|
9633
|
+
const { data } = await githubFetch(
|
|
9502
9634
|
ctx,
|
|
9503
|
-
|
|
9504
|
-
`/repos/${
|
|
9635
|
+
companyId,
|
|
9636
|
+
`/repos/${fullName}/issues?state=all&sort=updated&direction=desc&per_page=100&since=${since}&filter=all`
|
|
9505
9637
|
);
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
}
|
|
9509
|
-
const rows = await res.json();
|
|
9510
|
-
return rows.map((pr) => ({
|
|
9511
|
-
id: pr.id,
|
|
9512
|
-
number: pr.number,
|
|
9513
|
-
title: pr.title,
|
|
9514
|
-
state: pr.state,
|
|
9515
|
-
htmlUrl: pr.html_url,
|
|
9516
|
-
repoFullName,
|
|
9517
|
-
updatedAt: pr.updated_at
|
|
9518
|
-
}));
|
|
9519
|
-
}
|
|
9520
|
-
async function fetchIssuesForRepo(ctx, token, repoFullName) {
|
|
9521
|
-
const { owner, repo } = parseRepoFullName(repoFullName);
|
|
9522
|
-
const res = await githubFetch(
|
|
9523
|
-
ctx,
|
|
9524
|
-
token,
|
|
9525
|
-
`/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
|
|
9526
9640
|
);
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
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;
|
|
9540
9660
|
}
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
const
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
errors: []
|
|
9552
|
-
};
|
|
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;
|
|
9553
9671
|
const errors = [];
|
|
9554
|
-
|
|
9555
|
-
let issues = mode === "pullRequests" ? existing.issues : [];
|
|
9556
|
-
for (const repoFullName of repos) {
|
|
9672
|
+
for (const repo of repos) {
|
|
9557
9673
|
try {
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
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++;
|
|
9562
9715
|
}
|
|
9563
|
-
|
|
9564
|
-
|
|
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++;
|
|
9565
9737
|
}
|
|
9738
|
+
reposSynced++;
|
|
9566
9739
|
} catch (err) {
|
|
9567
|
-
|
|
9568
|
-
errors.push(message);
|
|
9569
|
-
ctx.logger.warn("GitHub sync repo failed", { repoFullName, message });
|
|
9740
|
+
errors.push(`${repo.fullName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9570
9741
|
}
|
|
9571
9742
|
}
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
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()
|
|
9577
9787
|
};
|
|
9578
|
-
await saveSyncCache(ctx, companyId, cache);
|
|
9579
|
-
return cache;
|
|
9580
9788
|
}
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
status: "not_synced",
|
|
9601
|
-
checkedAt,
|
|
9602
|
-
message: "Run sync to fetch open PRs and issues",
|
|
9603
|
-
lastSyncedAt: null,
|
|
9604
|
-
pullRequestCount: 0,
|
|
9605
|
-
issueCount: 0,
|
|
9606
|
-
recentPullRequests: [],
|
|
9607
|
-
recentIssues: [],
|
|
9608
|
-
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 }
|
|
9609
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
|
+
});
|
|
9610
9816
|
}
|
|
9611
9817
|
return {
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
issueCount: cache.issues.length,
|
|
9618
|
-
recentPullRequests: cache.pullRequests.slice(0, 10),
|
|
9619
|
-
recentIssues: cache.issues.slice(0, 10),
|
|
9620
|
-
lastErrors: cache.errors
|
|
9818
|
+
nodes,
|
|
9819
|
+
edges,
|
|
9820
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9821
|
+
repoFullName: "*",
|
|
9822
|
+
level: "high"
|
|
9621
9823
|
};
|
|
9622
9824
|
}
|
|
9623
|
-
async function
|
|
9624
|
-
const
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
const
|
|
9630
|
-
const
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
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
|
+
}
|
|
9639
9880
|
}
|
|
9640
|
-
};
|
|
9641
|
-
const res = await githubFetch(ctx, token, `/repos/${owner}/${repo}/hooks`, {
|
|
9642
|
-
method: "POST",
|
|
9643
|
-
headers: { "Content-Type": "application/json" },
|
|
9644
|
-
body: JSON.stringify(payload)
|
|
9645
|
-
});
|
|
9646
|
-
if (!res.ok) {
|
|
9647
|
-
const body = await res.text();
|
|
9648
|
-
throw new Error(`GitHub webhook registration failed (${res.status}): ${body}`);
|
|
9649
9881
|
}
|
|
9650
|
-
|
|
9651
|
-
|
|
9882
|
+
return {
|
|
9883
|
+
nodes,
|
|
9884
|
+
edges,
|
|
9885
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9652
9886
|
repoFullName,
|
|
9653
|
-
|
|
9654
|
-
hookId: hook.id,
|
|
9655
|
-
configuredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9656
|
-
inboundUrl
|
|
9887
|
+
level: "code"
|
|
9657
9888
|
};
|
|
9658
|
-
await ctx.state.set({ ...companyScope2(companyId), stateKey: WEBHOOK_STATE_KEY }, config);
|
|
9659
|
-
return config;
|
|
9660
|
-
}
|
|
9661
|
-
function requireCompanyId(input) {
|
|
9662
|
-
const companyId = input?.companyId;
|
|
9663
|
-
if (!companyId) {
|
|
9664
|
-
throw new Error("companyId is required");
|
|
9665
|
-
}
|
|
9666
|
-
return companyId;
|
|
9667
|
-
}
|
|
9668
|
-
async function handleGithubWebhook(input) {
|
|
9669
|
-
const ctx = workerCtx;
|
|
9670
|
-
if (!ctx) {
|
|
9671
|
-
return;
|
|
9672
|
-
}
|
|
9673
|
-
if (input.endpointKey !== GITHUB_WEBHOOK_ENDPOINT) {
|
|
9674
|
-
return;
|
|
9675
|
-
}
|
|
9676
|
-
const payload = input.parsedBody ?? {};
|
|
9677
|
-
const repoFullName = payload.repository?.full_name;
|
|
9678
|
-
if (!repoFullName) {
|
|
9679
|
-
return;
|
|
9680
|
-
}
|
|
9681
|
-
const companies = await ctx.companies.list();
|
|
9682
|
-
for (const company of companies) {
|
|
9683
|
-
const webhook = await loadWebhookConfig(ctx, company.id);
|
|
9684
|
-
if (!webhook || webhook.repoFullName !== repoFullName) {
|
|
9685
|
-
continue;
|
|
9686
|
-
}
|
|
9687
|
-
try {
|
|
9688
|
-
await runSync(ctx, company.id, "all");
|
|
9689
|
-
ctx.logger.info("GitHub webhook triggered sync", {
|
|
9690
|
-
companyId: company.id,
|
|
9691
|
-
repoFullName,
|
|
9692
|
-
action: payload.action ?? "unknown",
|
|
9693
|
-
requestId: input.requestId
|
|
9694
|
-
});
|
|
9695
|
-
} catch (err) {
|
|
9696
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
9697
|
-
ctx.logger.warn("GitHub webhook sync failed", { companyId: company.id, message });
|
|
9698
|
-
}
|
|
9699
|
-
}
|
|
9700
9889
|
}
|
|
9890
|
+
|
|
9891
|
+
// src/worker.ts
|
|
9892
|
+
init_queries();
|
|
9893
|
+
var pluginCtx = null;
|
|
9701
9894
|
var plugin = definePlugin({
|
|
9702
9895
|
async setup(ctx) {
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
ctx
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
ctx.
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
const auth = await getGithubAuthStatus(ctx, cid);
|
|
9716
|
-
const token = await resolveGithubToken(ctx, cid);
|
|
9717
|
-
if (!token) {
|
|
9718
|
-
return {
|
|
9719
|
-
status: "degraded",
|
|
9720
|
-
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9721
|
-
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",
|
|
9722
|
-
auth
|
|
9723
|
-
};
|
|
9724
|
-
}
|
|
9725
|
-
const res = await githubFetch(ctx, token, "/user");
|
|
9726
|
-
if (!res.ok) {
|
|
9727
|
-
return {
|
|
9728
|
-
status: "error",
|
|
9729
|
-
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9730
|
-
message: `GitHub API retornou ${res.status} \u2014 verifique escopos do PAT`,
|
|
9731
|
-
auth
|
|
9732
|
-
};
|
|
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
|
+
}
|
|
9733
9908
|
}
|
|
9734
|
-
const user = await res.json();
|
|
9735
|
-
return {
|
|
9736
|
-
status: "ok",
|
|
9737
|
-
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9738
|
-
login: user.login ?? "unknown",
|
|
9739
|
-
auth
|
|
9740
|
-
};
|
|
9741
9909
|
});
|
|
9742
9910
|
ctx.data.register("repos", async ({ companyId }) => {
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
}
|
|
9746
|
-
|
|
9747
|
-
})
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
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" });
|
|
9759
9928
|
return {
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9929
|
+
lastSync,
|
|
9930
|
+
repoCount: repos.length,
|
|
9931
|
+
openPRCount: openPRs.length
|
|
9763
9932
|
};
|
|
9764
9933
|
});
|
|
9765
|
-
ctx.
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
ctx.actions.register("saveGithubToken", async (input) => {
|
|
9769
|
-
const companyId = requireCompanyId(input);
|
|
9770
|
-
const token = input?.token;
|
|
9771
|
-
if (typeof token !== "string") {
|
|
9772
|
-
throw new Error("token is required");
|
|
9773
|
-
}
|
|
9774
|
-
await saveGithubPat(ctx, companyId, token);
|
|
9775
|
-
return { saved: true, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9776
|
-
});
|
|
9777
|
-
ctx.actions.register("saveGithubSecretRef", async (input) => {
|
|
9778
|
-
const companyId = requireCompanyId(input);
|
|
9779
|
-
const secretRef = input?.secretRef;
|
|
9780
|
-
if (typeof secretRef !== "string") {
|
|
9781
|
-
throw new Error("secretRef is required");
|
|
9934
|
+
ctx.data.register("graph-data", async ({ companyId, repoFullName, level }) => {
|
|
9935
|
+
if (level === "high") {
|
|
9936
|
+
return await generateHighLevelGraph(ctx, companyId);
|
|
9782
9937
|
}
|
|
9783
|
-
await
|
|
9784
|
-
return { saved: true, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9785
|
-
});
|
|
9786
|
-
ctx.actions.register("clearGithubAuth", async (input) => {
|
|
9787
|
-
const companyId = requireCompanyId(input);
|
|
9788
|
-
await clearGithubAuth(ctx, companyId);
|
|
9789
|
-
return { cleared: true, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9790
|
-
});
|
|
9791
|
-
ctx.actions.register("setTrackedRepos", async (input) => {
|
|
9792
|
-
const companyId = requireCompanyId(input);
|
|
9793
|
-
const repos = input?.repos;
|
|
9794
|
-
if (!Array.isArray(repos)) {
|
|
9795
|
-
throw new Error("repos array is required");
|
|
9796
|
-
}
|
|
9797
|
-
const normalized = repos.filter((r) => typeof r === "string" && r.includes("/")).slice(0, MAX_REPOS_PER_SYNC);
|
|
9798
|
-
await ctx.state.set({ ...companyScope2(companyId), stateKey: TRACKED_REPOS_KEY }, normalized);
|
|
9799
|
-
return { saved: true, repos: normalized, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9938
|
+
return await generateCodeGraph(ctx, companyId, repoFullName);
|
|
9800
9939
|
});
|
|
9801
|
-
ctx.
|
|
9802
|
-
const
|
|
9803
|
-
|
|
9804
|
-
return {
|
|
9805
|
-
syncedAt: cache.syncedAt,
|
|
9806
|
-
pullRequestCount: cache.pullRequests.length,
|
|
9807
|
-
errors: cache.errors
|
|
9808
|
-
};
|
|
9940
|
+
ctx.data.register("available-agents", async ({ companyId }) => {
|
|
9941
|
+
const agents = await ctx.agents.list({ companyId });
|
|
9942
|
+
return { agents };
|
|
9809
9943
|
});
|
|
9810
|
-
ctx.actions.register("
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
return {
|
|
9814
|
-
syncedAt: cache.syncedAt,
|
|
9815
|
-
issueCount: cache.issues.length,
|
|
9816
|
-
errors: cache.errors
|
|
9817
|
-
};
|
|
9944
|
+
ctx.actions.register("save-token", async ({ companyId, token }) => {
|
|
9945
|
+
await saveGithubPAT(ctx, companyId, token);
|
|
9946
|
+
return { ok: true };
|
|
9818
9947
|
});
|
|
9819
|
-
ctx.actions.register("
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
return {
|
|
9823
|
-
syncedAt: cache.syncedAt,
|
|
9824
|
-
pullRequestCount: cache.pullRequests.length,
|
|
9825
|
-
issueCount: cache.issues.length,
|
|
9826
|
-
errors: cache.errors
|
|
9827
|
-
};
|
|
9948
|
+
ctx.actions.register("save-secret-ref", async ({ companyId, secretRef }) => {
|
|
9949
|
+
await saveGithubSecretRef(ctx, companyId, secretRef);
|
|
9950
|
+
return { ok: true };
|
|
9828
9951
|
});
|
|
9829
|
-
ctx.actions.register("
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
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) };
|
|
9835
9960
|
}
|
|
9836
|
-
const config = await registerGithubWebhook(ctx, companyId, repoFullName, events);
|
|
9837
|
-
return { saved: true, config };
|
|
9838
9961
|
});
|
|
9839
|
-
ctx.
|
|
9840
|
-
const
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
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.`
|
|
9851
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);
|
|
9852
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);
|
|
9853
10018
|
});
|
|
9854
10019
|
},
|
|
9855
10020
|
async onHealth() {
|
|
9856
|
-
return { status: "ok", message: "GitHub Manager
|
|
10021
|
+
return { status: "ok", message: "GitHub Manager v2 running" };
|
|
9857
10022
|
},
|
|
9858
10023
|
async onWebhook(input) {
|
|
9859
|
-
|
|
10024
|
+
if (!pluginCtx) throw new Error("Plugin not initialized");
|
|
10025
|
+
await handleGithubWebhook(pluginCtx, input);
|
|
10026
|
+
},
|
|
10027
|
+
async onShutdown() {
|
|
10028
|
+
pluginCtx = null;
|
|
9860
10029
|
}
|
|
9861
10030
|
});
|
|
9862
10031
|
var worker_default = plugin;
|