@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/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 = /* @__PURE__ */ Symbol("zod_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-config.ts
8918
- var GITHUB_PAT_STATE_KEY = "github.token.pat";
8919
- var GITHUB_SECRET_REF_STATE_KEY = "github.token.secretRef";
8920
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8921
- function companyScope(companyId) {
8922
- return { scopeKind: "company", scopeId: companyId };
8923
- }
8924
- async function loadGithubPat(ctx, companyId) {
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
- const ref = typeof raw === "string" ? raw.trim() : "";
8934
- return UUID_RE.test(ref) ? ref : null;
8935
- }
8936
- async function saveGithubPat(ctx, companyId, token) {
8937
- const trimmed = token.trim();
8938
- if (!trimmed) {
8939
- throw new Error("GitHub token is required");
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
- await ctx.state.set({ ...companyScope(companyId), stateKey: GITHUB_PAT_STATE_KEY }, trimmed);
8942
- }
8943
- async function saveGithubSecretRef(ctx, companyId, secretRef) {
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 clearGithubAuth(ctx, companyId) {
8954
- await ctx.state.set({ ...companyScope(companyId), stateKey: GITHUB_PAT_STATE_KEY }, null);
8955
- await ctx.state.set({ ...companyScope(companyId), stateKey: GITHUB_SECRET_REF_STATE_KEY }, null);
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 getGithubAuthStatus(ctx, companyId) {
8958
- const pat = await loadGithubPat(ctx, companyId);
8959
- if (pat) {
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" };
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 raw = process.env.GITHUB_API_URL?.trim();
8987
- return raw ? raw.replace(/\/+$/, "") : "https://api.github.com";
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-api.ts
8995
- var GITHUB_WEBHOOK_ENDPOINT = "github-events";
8996
- async function githubFetch(ctx, token, path2, init) {
8997
- const apiBase = getGithubApiBase();
8998
- const url = path2.startsWith("http") ? path2 : `${apiBase}${path2}`;
8999
- return ctx.http.fetch(url, {
9000
- ...init,
9001
- headers: {
9002
- Authorization: `Bearer ${token}`,
9003
- Accept: "application/vnd.github+json",
9004
- "X-GitHub-Api-Version": "2022-11-28",
9005
- ...init?.headers ?? {}
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
- function parseRepoFullName(fullName) {
9010
- const [owner, repo] = fullName.split("/");
9011
- if (!owner || !repo) {
9012
- throw new Error(`Invalid repository full name: ${fullName}`);
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
- return { owner, repo };
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 formatRateLimitMessage(res) {
9020
- const remaining = res.headers.get("x-ratelimit-remaining");
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/github-review-tools.ts
9250
+ // src/review/review-tools.ts
9039
9251
  var MAX_DIFF_CHARS = 12e4;
9040
- var MAX_FILE_PATCH_CHARS = 32e3;
9041
- function requireString(value, field) {
9042
- if (typeof value !== "string" || value.trim().length === 0) {
9043
- throw new Error(`${field} is required`);
9044
- }
9045
- return value.trim();
9046
- }
9047
- function requireNumber(value, field) {
9048
- const n = typeof value === "number" ? value : Number(value);
9049
- if (!Number.isFinite(n) || n < 1) {
9050
- throw new Error(`${field} must be a positive number`);
9051
- }
9052
- return Math.floor(n);
9053
- }
9054
- function resolveOwnerRepo(params) {
9055
- const owner = requireString(params.owner, "owner");
9056
- const repo = requireString(params.repo, "repo");
9057
- return { owner, repo };
9058
- }
9059
- async function resolveTokenForRun(ctx, runCtx) {
9060
- const companyToken = await resolveGithubToken(ctx, runCtx.companyId);
9061
- if (companyToken) {
9062
- return companyToken;
9063
- }
9064
- const envToken = process.env.GITHUB_TOKEN?.trim();
9065
- if (envToken) {
9066
- return envToken;
9067
- }
9068
- throw new Error(
9069
- "GitHub token not configured \u2014 save a PAT in GitHub \u2192 Configura\xE7\xF5es or set GITHUB_TOKEN"
9070
- );
9071
- }
9072
- function toolJson(data, summary) {
9073
- return {
9074
- content: summary ?? JSON.stringify(data, null, 2),
9075
- data
9076
- };
9077
- }
9078
- function truncateDiff(diff) {
9079
- if (diff.length <= MAX_DIFF_CHARS) {
9080
- return { diff, truncated: false, originalLength: diff.length };
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
- let path2;
9248
- if (owner) {
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: "Get PR diff",
9324
- description: "Returns pull request metadata and unified diff. Large diffs are truncated with a file list.",
9311
+ displayName: "Read File",
9312
+ description: "Read a file from a GitHub repository",
9325
9313
  parametersSchema: {
9326
- ...ownerRepoSchema,
9314
+ type: "object",
9327
9315
  properties: {
9328
- ...ownerRepoSchema.properties,
9329
- pr_number: { type: "integer", description: "Pull request number" }
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", "pr_number"]
9321
+ required: ["owner", "repo", "path"]
9332
9322
  }
9333
9323
  },
9334
- async (ctx2, token, params) => {
9335
- const { owner, repo } = resolveOwnerRepo(params);
9336
- const prNumber = requireNumber(params.pr_number, "pr_number");
9337
- return fetchPullRequestDiff(ctx2, token, { owner, repo }, prNumber);
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
- wrap(
9343
+ ctx.tools.register(
9341
9344
  "github_create_review_comment",
9342
9345
  {
9343
- displayName: "Create PR review comment",
9344
- description: "Adds an inline review comment on a specific line in a pull request diff.",
9346
+ displayName: "Add Review Comment",
9347
+ description: "Add an inline review comment to a pull request",
9345
9348
  parametersSchema: {
9346
- ...ownerRepoSchema,
9349
+ type: "object",
9347
9350
  properties: {
9348
- ...ownerRepoSchema.properties,
9349
- pr_number: { type: "integer" },
9350
- commit_id: { type: "string", description: "Head commit SHA" },
9351
- path: { type: "string", description: "File path in the repo" },
9352
- line: { type: "integer", description: "Line number in the modified file" },
9353
- body: { type: "string", description: "Comment text" }
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", "pr_number", "commit_id", "path", "line", "body"]
9359
+ required: ["owner", "repo", "pull_number", "commit_id", "path", "line", "body"]
9356
9360
  }
9357
9361
  },
9358
- async (ctx2, token, params) => createReviewComment(ctx2, token, params)
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
- wrap(
9373
+ ctx.tools.register(
9361
9374
  "github_submit_pr_review",
9362
9375
  {
9363
- displayName: "Submit PR review",
9364
- description: "Submits a pull request review with APPROVE, REQUEST_CHANGES, or COMMENT.",
9376
+ displayName: "Submit PR Review",
9377
+ description: "Submit a pull request review with a verdict",
9365
9378
  parametersSchema: {
9366
- ...ownerRepoSchema,
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
- ...ownerRepoSchema.properties,
9391
- path: { type: "string", description: "Path to the file" },
9392
- ref: { type: "string", description: "Branch name or commit SHA (optional)" }
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", "path"]
9387
+ required: ["owner", "repo", "pull_number", "event", "body"]
9395
9388
  }
9396
9389
  },
9397
- async (ctx2, token, params) => readFileContent(ctx2, token, params)
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
- wrap(
9401
+ ctx.tools.register(
9400
9402
  "github_list_repositories",
9401
9403
  {
9402
- displayName: "List GitHub repositories",
9403
- description: "Lists repositories for an owner/org, or the authenticated user when owner is omitted.",
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 (ctx2, token, params) => listRepositories(ctx2, token, params)
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
- wrap(
9417
+ ctx.tools.register(
9418
9418
  "github_search_issues",
9419
9419
  {
9420
- displayName: "Search GitHub issues",
9421
- description: "Searches issues using GitHub issue search syntax (q parameter).",
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
- q: { type: "string", description: "GitHub search query" },
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: ["q"]
9427
+ required: ["query"]
9429
9428
  }
9430
9429
  },
9431
- async (ctx2, token, params) => searchIssues(ctx2, token, params)
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/worker.ts
9436
- var SYNC_STATE_KEY = "github.sync.cache";
9437
- var WEBHOOK_STATE_KEY = "github.webhook.config";
9438
- var TRACKED_REPOS_KEY = "github.tracked.repos";
9439
- var MAX_REPOS_PER_SYNC = 5;
9440
- var MAX_ITEMS_PER_REPO = 20;
9441
- var workerCtx = null;
9442
- function companyScope2(companyId) {
9443
- return { scopeKind: "company", scopeId: companyId };
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 loadSyncCache(ctx, companyId) {
9446
- const raw = await ctx.state.get({ ...companyScope2(companyId), stateKey: SYNC_STATE_KEY });
9447
- return raw ?? null;
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
- async function saveSyncCache(ctx, companyId, cache) {
9450
- await ctx.state.set({ ...companyScope2(companyId), stateKey: SYNC_STATE_KEY }, cache);
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 loadWebhookConfig(ctx, companyId) {
9453
- const raw = await ctx.state.get({ ...companyScope2(companyId), stateKey: WEBHOOK_STATE_KEY });
9454
- return raw ?? null;
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 listRepos(ctx, companyId) {
9457
- const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
9458
- const token = await resolveGithubToken(ctx, companyId);
9459
- if (!token) {
9460
- return {
9461
- status: "degraded",
9462
- checkedAt,
9463
- message: "Configure o token GitHub em Configura\xE7\xF5es (PAT ou Secret ID)",
9464
- repos: []
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
- const res = await githubFetch(
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
- token,
9470
- `/user/repos?per_page=30&sort=updated&affiliation=owner,organization_member`
9600
+ companyId,
9601
+ `/repos/${fullName}/pulls?state=all&sort=updated&direction=desc&per_page=100&since=${since}`
9471
9602
  );
9472
- if (!res.ok) {
9473
- return {
9474
- status: "error",
9475
- checkedAt,
9476
- message: `GitHub API returned ${res.status}`,
9477
- repos: []
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
- const rows = await res.json();
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 fetchPullRequestsForRepo(ctx, token, repoFullName) {
9500
- const { owner, repo } = parseRepoFullName(repoFullName);
9501
- const res = await githubFetch(
9632
+ async function syncRepoIssues(ctx, companyId, repoId, fullName, since) {
9633
+ const { data } = await githubFetch(
9502
9634
  ctx,
9503
- token,
9504
- `/repos/${owner}/${repo}/pulls?state=open&per_page=${MAX_ITEMS_PER_REPO}&sort=updated&direction=desc`
9635
+ companyId,
9636
+ `/repos/${fullName}/issues?state=all&sort=updated&direction=desc&per_page=100&since=${since}&filter=all`
9505
9637
  );
9506
- if (!res.ok) {
9507
- throw new Error(`PR sync failed for ${repoFullName}: HTTP ${res.status}`);
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
- if (!res.ok) {
9528
- throw new Error(`Issue sync failed for ${repoFullName}: HTTP ${res.status}`);
9529
- }
9530
- const rows = await res.json();
9531
- return rows.filter((row) => !row.pull_request).map((issue) => ({
9532
- id: issue.id,
9533
- number: issue.number,
9534
- title: issue.title,
9535
- state: issue.state,
9536
- htmlUrl: issue.html_url,
9537
- repoFullName,
9538
- updatedAt: issue.updated_at
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
- async function runSync(ctx, companyId, mode) {
9542
- const token = await resolveGithubToken(ctx, companyId);
9543
- if (!token) {
9544
- throw new Error("Configure o token GitHub em Configura\xE7\xF5es antes de sincronizar");
9545
- }
9546
- const repos = await resolveTrackedRepos(ctx, companyId);
9547
- const existing = await loadSyncCache(ctx, companyId) ?? {
9548
- syncedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
9549
- pullRequests: [],
9550
- issues: [],
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
- let pullRequests = mode === "issues" ? existing.pullRequests : [];
9555
- let issues = mode === "pullRequests" ? existing.issues : [];
9556
- for (const repoFullName of repos) {
9672
+ for (const repo of repos) {
9557
9673
  try {
9558
- if (mode === "pullRequests" || mode === "all") {
9559
- pullRequests = pullRequests.concat(
9560
- await fetchPullRequestsForRepo(ctx, token, repoFullName)
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
- if (mode === "issues" || mode === "all") {
9564
- issues = issues.concat(await fetchIssuesForRepo(ctx, token, repoFullName));
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
- const message = err instanceof Error ? err.message : String(err);
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
- const cache = {
9573
- syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
9574
- pullRequests: pullRequests.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, MAX_ITEMS_PER_REPO * MAX_REPOS_PER_SYNC),
9575
- issues: issues.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, MAX_ITEMS_PER_REPO * MAX_REPOS_PER_SYNC),
9576
- errors
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
- async function buildSyncOverview(ctx, companyId) {
9582
- const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
9583
- const token = await resolveGithubToken(ctx, companyId);
9584
- const cache = await loadSyncCache(ctx, companyId);
9585
- if (!token) {
9586
- return {
9587
- status: "degraded",
9588
- checkedAt,
9589
- message: "Configure o token GitHub em Configura\xE7\xF5es para sincronizar PRs e issues",
9590
- lastSyncedAt: cache?.syncedAt ?? null,
9591
- pullRequestCount: cache?.pullRequests.length ?? 0,
9592
- issueCount: cache?.issues.length ?? 0,
9593
- recentPullRequests: cache?.pullRequests.slice(0, 10) ?? [],
9594
- recentIssues: cache?.issues.slice(0, 10) ?? [],
9595
- lastErrors: cache?.errors ?? []
9596
- };
9597
- }
9598
- if (!cache) {
9599
- return {
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
- status: "ok",
9613
- checkedAt,
9614
- message: `Last sync ${cache.syncedAt}`,
9615
- lastSyncedAt: cache.syncedAt,
9616
- pullRequestCount: cache.pullRequests.length,
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 registerGithubWebhook(ctx, companyId, repoFullName, events) {
9624
- const token = await resolveGithubToken(ctx, companyId);
9625
- if (!token) {
9626
- throw new Error("Configure o token GitHub em Configura\xE7\xF5es antes de registrar webhooks");
9627
- }
9628
- const pluginId = ctx.manifest.id;
9629
- const inboundUrl = buildInboundWebhookUrl(pluginId);
9630
- const { owner, repo } = parseRepoFullName(repoFullName);
9631
- const payload = {
9632
- name: "web",
9633
- active: true,
9634
- events: events.length > 0 ? events : ["pull_request", "issues"],
9635
- config: {
9636
- url: inboundUrl,
9637
- content_type: "json",
9638
- insecure_ssl: "0"
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
- const hook = await res.json();
9651
- const config = {
9882
+ return {
9883
+ nodes,
9884
+ edges,
9885
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9652
9886
  repoFullName,
9653
- events: payload.events,
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
- workerCtx = ctx;
9704
- registerGithubReviewTools(ctx);
9705
- ctx.events.on("issue.created", async (event) => {
9706
- const issueId = event.entityId ?? "unknown";
9707
- await ctx.state.set({ scopeKind: "issue", scopeId: issueId, stateKey: "seen" }, true);
9708
- ctx.logger.info("GitHub plugin observed issue.created", { issueId });
9709
- });
9710
- ctx.data.register("health", async ({ companyId }) => {
9711
- if (!companyId) {
9712
- throw new Error("companyId is required");
9713
- }
9714
- const cid = String(companyId);
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
- if (!companyId) {
9744
- throw new Error("companyId is required");
9745
- }
9746
- return listRepos(ctx, String(companyId));
9747
- });
9748
- ctx.data.register("syncOverview", async ({ companyId }) => {
9749
- if (!companyId) {
9750
- throw new Error("companyId is required");
9751
- }
9752
- return buildSyncOverview(ctx, String(companyId));
9753
- });
9754
- ctx.data.register("webhookConfig", async ({ companyId }) => {
9755
- if (!companyId) {
9756
- throw new Error("companyId is required");
9757
- }
9758
- const config = await loadWebhookConfig(ctx, String(companyId));
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
- configured: Boolean(config),
9761
- config,
9762
- inboundUrl: buildInboundWebhookUrl(ctx.manifest.id)
9929
+ lastSync,
9930
+ repoCount: repos.length,
9931
+ openPRCount: openPRs.length
9763
9932
  };
9764
9933
  });
9765
- ctx.actions.register("ping", async () => {
9766
- return { pong: true, at: (/* @__PURE__ */ new Date()).toISOString() };
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 saveGithubSecretRef(ctx, companyId, secretRef);
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.actions.register("syncPullRequests", async (input) => {
9802
- const companyId = requireCompanyId(input);
9803
- const cache = await runSync(ctx, companyId, "pullRequests");
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("syncIssues", async (input) => {
9811
- const companyId = requireCompanyId(input);
9812
- const cache = await runSync(ctx, companyId, "issues");
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("syncAll", async (input) => {
9820
- const companyId = requireCompanyId(input);
9821
- const cache = await runSync(ctx, companyId, "all");
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("configureWebhook", async (input) => {
9830
- const companyId = requireCompanyId(input);
9831
- const repoFullName = input?.repoFullName;
9832
- const events = input?.events ?? ["pull_request", "issues"];
9833
- if (!repoFullName) {
9834
- throw new Error("repoFullName is required");
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.jobs.register("sync-github", async (job) => {
9840
- const companies = await ctx.companies.list();
9841
- for (const company of companies) {
9842
- try {
9843
- await runSync(ctx, company.id, "all");
9844
- ctx.logger.info("Scheduled GitHub sync completed", {
9845
- companyId: company.id,
9846
- runId: job.runId
9847
- });
9848
- } catch (err) {
9849
- const message = err instanceof Error ? err.message : String(err);
9850
- ctx.logger.warn("Scheduled GitHub sync skipped", { companyId: company.id, message });
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 worker is running" };
10021
+ return { status: "ok", message: "GitHub Manager v2 running" };
9857
10022
  },
9858
10023
  async onWebhook(input) {
9859
- await handleGithubWebhook(input);
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;