@gaud_erp/paperclip-github-manager 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,523 +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);
9191
+ const envToken = process.env.GITHUB_TOKEN?.trim();
9192
+ if (envToken) return envToken;
9193
+ throw new Error("No GitHub token configured. Set a PAT or secret reference in Settings.");
8942
9194
  }
8943
- async function 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
- );
9195
+ async function saveGithubPAT(ctx, companyId, token) {
9196
+ await ctx.state.set({
9197
+ scopeKind: "company",
9198
+ scopeId: companyId,
9199
+ stateKey: GITHUB_PAT_KEY
9200
+ }, token);
8952
9201
  }
8953
- async function 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);
9202
+ async function saveGithubSecretRef(ctx, companyId, ref) {
9203
+ await ctx.state.set({
9204
+ scopeKind: "company",
9205
+ scopeId: companyId,
9206
+ stateKey: GITHUB_SECRET_REF_KEY
9207
+ }, ref);
8956
9208
  }
8957
- 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" };
9209
+ function getGithubApiBase() {
9210
+ const base = process.env.GITHUB_API_URL?.trim();
9211
+ return base ? base.replace(/\/+$/, "") : "https://api.github.com";
8967
9212
  }
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;
9213
+
9214
+ // src/github/api-client.ts
9215
+ async function githubFetch(ctx, companyId, path2, options = {}) {
9216
+ const token = await resolveGithubToken(ctx, companyId);
9217
+ const base = getGithubApiBase();
9218
+ const url = path2.startsWith("http") ? path2 : `${base}${path2}`;
9219
+ const headers = {
9220
+ Authorization: `Bearer ${token}`,
9221
+ Accept: options.accept ?? "application/vnd.github+json",
9222
+ "X-GitHub-Api-Version": "2022-11-28"
9223
+ };
9224
+ const resp = await ctx.http.fetch(url, {
9225
+ method: options.method ?? "GET",
9226
+ headers,
9227
+ body: options.body ? JSON.stringify(options.body) : void 0
9228
+ });
9229
+ const rateLimit = {
9230
+ remaining: Number(resp.headers.get("x-ratelimit-remaining") ?? 5e3),
9231
+ limit: Number(resp.headers.get("x-ratelimit-limit") ?? 5e3),
9232
+ resetAt: new Date(
9233
+ Number(resp.headers.get("x-ratelimit-reset") ?? 0) * 1e3
9234
+ ).toISOString()
9235
+ };
9236
+ if (!resp.ok) {
9237
+ const body = await resp.text();
9238
+ if (resp.status === 403 && rateLimit.remaining === 0) {
9239
+ throw new Error(`GitHub rate limit exceeded. Resets at ${rateLimit.resetAt}`);
9240
+ }
9241
+ throw new Error(`GitHub API ${resp.status}: ${body}`);
8981
9242
  }
9243
+ const data = await resp.json();
9244
+ return { data, rateLimit };
9245
+ }
9246
+ function isRateLimitSafe(rateLimit, threshold = 100) {
9247
+ return rateLimit.remaining > threshold;
8982
9248
  }
8983
9249
 
8984
- // src/github-api.ts
8985
- var GITHUB_API = "https://api.github.com";
8986
- var GITHUB_WEBHOOK_ENDPOINT = "github-events";
8987
- async function githubFetch(ctx, token, path2, init) {
8988
- const url = path2.startsWith("http") ? path2 : `${GITHUB_API}${path2}`;
8989
- return ctx.http.fetch(url, {
8990
- ...init,
8991
- headers: {
8992
- Authorization: `Bearer ${token}`,
8993
- Accept: "application/vnd.github+json",
8994
- "X-GitHub-Api-Version": "2022-11-28",
8995
- ...init?.headers ?? {}
9250
+ // src/review/review-tools.ts
9251
+ var MAX_DIFF_CHARS = 12e4;
9252
+ var MAX_FILE_CHARS = 128e3;
9253
+ function registerReviewTools(ctx) {
9254
+ ctx.tools.register(
9255
+ "github_get_pull_request_diff",
9256
+ {
9257
+ displayName: "Get PR Diff",
9258
+ description: "Get the diff of a GitHub pull request for code review",
9259
+ parametersSchema: {
9260
+ type: "object",
9261
+ properties: {
9262
+ owner: { type: "string", description: "Repository owner" },
9263
+ repo: { type: "string", description: "Repository name" },
9264
+ pull_number: { type: "number", description: "PR number" }
9265
+ },
9266
+ required: ["owner", "repo", "pull_number"]
9267
+ }
9268
+ },
9269
+ async (params, runCtx) => {
9270
+ const { owner, repo, pull_number } = params;
9271
+ const companyId = runCtx.companyId;
9272
+ if (!companyId) return { error: "No company context" };
9273
+ const { data: prData } = await githubFetch(ctx, companyId, `/repos/${owner}/${repo}/pulls/${pull_number}`);
9274
+ const pr = prData;
9275
+ const { data: diffData } = await githubFetch(
9276
+ ctx,
9277
+ companyId,
9278
+ `/repos/${owner}/${repo}/pulls/${pull_number}`,
9279
+ { accept: "application/vnd.github.v3.diff" }
9280
+ );
9281
+ let diff = String(diffData);
9282
+ let truncated = false;
9283
+ if (diff.length > MAX_DIFF_CHARS) {
9284
+ diff = diff.slice(0, MAX_DIFF_CHARS);
9285
+ truncated = true;
9286
+ }
9287
+ const { data: filesData } = await githubFetch(
9288
+ ctx,
9289
+ companyId,
9290
+ `/repos/${owner}/${repo}/pulls/${pull_number}/files?per_page=100`
9291
+ );
9292
+ const files = filesData.map((f) => ({
9293
+ filename: f.filename,
9294
+ status: f.status,
9295
+ additions: f.additions,
9296
+ deletions: f.deletions
9297
+ }));
9298
+ return {
9299
+ content: `PR #${pull_number}: ${pr.title}
9300
+ Author: ${pr.user.login}
9301
+ Files changed: ${files.length}${truncated ? "\n\u26A0\uFE0F Diff truncated" : ""}
9302
+
9303
+ ${diff}`,
9304
+ data: { pr: { title: pr.title, number: pull_number, sha: pr.head.sha }, files }
9305
+ };
8996
9306
  }
8997
- });
9307
+ );
9308
+ ctx.tools.register(
9309
+ "github_read_file_content",
9310
+ {
9311
+ displayName: "Read File",
9312
+ description: "Read a file from a GitHub repository",
9313
+ parametersSchema: {
9314
+ type: "object",
9315
+ properties: {
9316
+ owner: { type: "string" },
9317
+ repo: { type: "string" },
9318
+ path: { type: "string", description: "File path in the repository" },
9319
+ ref: { type: "string", description: "Branch, tag, or commit SHA (optional)" }
9320
+ },
9321
+ required: ["owner", "repo", "path"]
9322
+ }
9323
+ },
9324
+ async (params, runCtx) => {
9325
+ const { owner, repo, path: path2, ref } = params;
9326
+ const companyId = runCtx.companyId;
9327
+ if (!companyId) return { error: "No company context" };
9328
+ const refParam = ref ? `?ref=${ref}` : "";
9329
+ const { data } = await githubFetch(
9330
+ ctx,
9331
+ companyId,
9332
+ `/repos/${owner}/${repo}/contents/${path2}${refParam}`
9333
+ );
9334
+ const file = data;
9335
+ const content = Buffer.from(file.content, "base64").toString("utf-8");
9336
+ const truncated = content.length > MAX_FILE_CHARS;
9337
+ return {
9338
+ content: truncated ? content.slice(0, MAX_FILE_CHARS) + "\n\u26A0\uFE0F Truncated" : content,
9339
+ data: { path: path2, size: file.size, sha: file.sha }
9340
+ };
9341
+ }
9342
+ );
9343
+ ctx.tools.register(
9344
+ "github_create_review_comment",
9345
+ {
9346
+ displayName: "Add Review Comment",
9347
+ description: "Add an inline review comment to a pull request",
9348
+ parametersSchema: {
9349
+ type: "object",
9350
+ properties: {
9351
+ owner: { type: "string" },
9352
+ repo: { type: "string" },
9353
+ pull_number: { type: "number" },
9354
+ commit_id: { type: "string", description: "The SHA of the PR head commit" },
9355
+ path: { type: "string", description: "File path relative to repo root" },
9356
+ line: { type: "number", description: "Line number in the diff" },
9357
+ body: { type: "string", description: "Comment text (markdown)" }
9358
+ },
9359
+ required: ["owner", "repo", "pull_number", "commit_id", "path", "line", "body"]
9360
+ }
9361
+ },
9362
+ async (params, runCtx) => {
9363
+ const { owner, repo, pull_number, commit_id, path: path2, line, body } = params;
9364
+ const companyId = runCtx.companyId;
9365
+ if (!companyId) return { error: "No company context" };
9366
+ await githubFetch(ctx, companyId, `/repos/${owner}/${repo}/pulls/${pull_number}/comments`, {
9367
+ method: "POST",
9368
+ body: { commit_id, path: path2, line, body, side: "RIGHT" }
9369
+ });
9370
+ return { content: `Comment added to ${path2}:${line}` };
9371
+ }
9372
+ );
9373
+ ctx.tools.register(
9374
+ "github_submit_pr_review",
9375
+ {
9376
+ displayName: "Submit PR Review",
9377
+ description: "Submit a pull request review with a verdict",
9378
+ parametersSchema: {
9379
+ type: "object",
9380
+ properties: {
9381
+ owner: { type: "string" },
9382
+ repo: { type: "string" },
9383
+ pull_number: { type: "number" },
9384
+ event: { type: "string", enum: ["APPROVE", "REQUEST_CHANGES", "COMMENT"] },
9385
+ body: { type: "string", description: "Review summary (markdown)" }
9386
+ },
9387
+ required: ["owner", "repo", "pull_number", "event", "body"]
9388
+ }
9389
+ },
9390
+ async (params, runCtx) => {
9391
+ const { owner, repo, pull_number, event, body } = params;
9392
+ const companyId = runCtx.companyId;
9393
+ if (!companyId) return { error: "No company context" };
9394
+ await githubFetch(ctx, companyId, `/repos/${owner}/${repo}/pulls/${pull_number}/reviews`, {
9395
+ method: "POST",
9396
+ body: { event, body }
9397
+ });
9398
+ return { content: `Review submitted: ${event}`, data: { event } };
9399
+ }
9400
+ );
9401
+ ctx.tools.register(
9402
+ "github_list_repositories",
9403
+ {
9404
+ displayName: "List Repositories",
9405
+ description: "List tracked GitHub repositories",
9406
+ parametersSchema: { type: "object", properties: {} }
9407
+ },
9408
+ async (_params, _runCtx) => {
9409
+ const { listRepos: listRepos2 } = await Promise.resolve().then(() => (init_queries(), queries_exports));
9410
+ const repos = await listRepos2(ctx.db);
9411
+ return {
9412
+ content: repos.map((r) => `${r.fullName} (${r.language ?? "unknown"})`).join("\n"),
9413
+ data: { repos }
9414
+ };
9415
+ }
9416
+ );
9417
+ ctx.tools.register(
9418
+ "github_search_issues",
9419
+ {
9420
+ displayName: "Search Issues",
9421
+ description: "Search GitHub issues and PRs using GitHub search syntax",
9422
+ parametersSchema: {
9423
+ type: "object",
9424
+ properties: {
9425
+ query: { type: "string", description: "GitHub search query (e.g. 'is:open label:bug')" }
9426
+ },
9427
+ required: ["query"]
9428
+ }
9429
+ },
9430
+ async (params, runCtx) => {
9431
+ const { query } = params;
9432
+ const companyId = runCtx.companyId;
9433
+ if (!companyId) return { error: "No company context" };
9434
+ const { data } = await githubFetch(
9435
+ ctx,
9436
+ companyId,
9437
+ `/search/issues?q=${encodeURIComponent(query)}&per_page=20`
9438
+ );
9439
+ const result = data;
9440
+ const items = result.items.map((i) => ({
9441
+ title: i.title,
9442
+ number: i.number,
9443
+ state: i.state,
9444
+ html_url: i.html_url
9445
+ }));
9446
+ return { content: items.map((i) => `#${i.number} ${i.title} [${i.state}]`).join("\n"), data: { items } };
9447
+ }
9448
+ );
8998
9449
  }
8999
- function parseRepoFullName(fullName) {
9000
- const [owner, repo] = fullName.split("/");
9001
- if (!owner || !repo) {
9002
- throw new Error(`Invalid repository full name: ${fullName}`);
9003
- }
9004
- return { owner, repo };
9450
+
9451
+ // src/sync/webhook-handler.ts
9452
+ init_queries();
9453
+
9454
+ // src/sync/link-detector.ts
9455
+ init_queries();
9456
+ var KEY_PATTERN = /\b([A-Z][A-Z0-9]+-\d+)\b/g;
9457
+ var HASH_PATTERN = /#(\d+)\b/g;
9458
+ function extractCardIds(branch, title) {
9459
+ const text = `${branch} ${title}`;
9460
+ const ids = /* @__PURE__ */ new Set();
9461
+ for (const match of text.matchAll(KEY_PATTERN)) {
9462
+ ids.add(match[1]);
9463
+ }
9464
+ for (const match of text.matchAll(HASH_PATTERN)) {
9465
+ ids.add(`#${match[1]}`);
9466
+ }
9467
+ return [...ids];
9005
9468
  }
9006
- function buildInboundWebhookUrl(pluginId, baseUrl = "http://127.0.0.1:3100") {
9007
- return `${baseUrl.replace(/\/+$/, "")}/api/plugins/${pluginId}/webhooks/${GITHUB_WEBHOOK_ENDPOINT}`;
9469
+ async function detectAndLinkCards(ctx, prId, branch, title) {
9470
+ const cardIds = extractCardIds(branch, title);
9471
+ for (const cardId of cardIds) {
9472
+ await linkPRToCard(ctx.db, prId, cardId, "pattern");
9473
+ }
9474
+ return cardIds;
9008
9475
  }
9009
9476
 
9010
- // src/worker.ts
9011
- var SYNC_STATE_KEY = "github.sync.cache";
9012
- var WEBHOOK_STATE_KEY = "github.webhook.config";
9013
- var TRACKED_REPOS_KEY = "github.tracked.repos";
9014
- var MAX_REPOS_PER_SYNC = 5;
9015
- var MAX_ITEMS_PER_REPO = 20;
9016
- var workerCtx = null;
9017
- function companyScope2(companyId) {
9018
- return { scopeKind: "company", scopeId: companyId };
9019
- }
9020
- async function loadSyncCache(ctx, companyId) {
9021
- const raw = await ctx.state.get({ ...companyScope2(companyId), stateKey: SYNC_STATE_KEY });
9022
- return raw ?? null;
9477
+ // src/sync/webhook-handler.ts
9478
+ async function handleGithubWebhook(ctx, input) {
9479
+ const event = input.headers["x-github-event"];
9480
+ const payload = input.parsedBody;
9481
+ if (!payload || !event) {
9482
+ ctx.logger.warn("Webhook received with missing event header or body");
9483
+ return;
9484
+ }
9485
+ if (event === "pull_request") {
9486
+ await handlePullRequestEvent(ctx, payload);
9487
+ } else if (event === "issues") {
9488
+ await handleIssuesEvent(ctx, payload);
9489
+ } else {
9490
+ ctx.logger.info(`Ignoring GitHub event: ${event}`);
9491
+ }
9023
9492
  }
9024
- async function saveSyncCache(ctx, companyId, cache) {
9025
- await ctx.state.set({ ...companyScope2(companyId), stateKey: SYNC_STATE_KEY }, cache);
9493
+ async function handlePullRequestEvent(ctx, payload) {
9494
+ const prData = payload.pull_request;
9495
+ const repoData = payload.repository;
9496
+ if (!prData || !repoData) return;
9497
+ await upsertRepo(ctx.db, {
9498
+ id: repoData.id,
9499
+ fullName: repoData.full_name,
9500
+ owner: repoData.owner.login,
9501
+ name: repoData.name,
9502
+ private: repoData.private,
9503
+ defaultBranch: repoData.default_branch,
9504
+ htmlUrl: repoData.html_url,
9505
+ description: repoData.description,
9506
+ language: repoData.language,
9507
+ topics: repoData.topics ?? [],
9508
+ updatedAt: repoData.updated_at
9509
+ });
9510
+ const merged = prData.merged;
9511
+ const state = merged ? "merged" : prData.state;
9512
+ const pr = {
9513
+ id: prData.id,
9514
+ repoId: repoData.id,
9515
+ number: prData.number,
9516
+ title: prData.title,
9517
+ body: prData.body,
9518
+ state,
9519
+ author: prData.user.login,
9520
+ headBranch: prData.head.ref,
9521
+ baseBranch: prData.base.ref,
9522
+ htmlUrl: prData.html_url,
9523
+ draft: prData.draft,
9524
+ mergeable: prData.mergeable,
9525
+ mergedAt: prData.merged_at,
9526
+ createdAt: prData.created_at,
9527
+ updatedAt: prData.updated_at
9528
+ };
9529
+ await upsertPR(ctx.db, pr);
9530
+ await detectAndLinkCards(ctx, pr.id, pr.headBranch, pr.title);
9531
+ ctx.logger.info(`Webhook: upserted PR #${pr.number} from ${repoData.full_name}`);
9026
9532
  }
9027
- async function loadWebhookConfig(ctx, companyId) {
9028
- const raw = await ctx.state.get({ ...companyScope2(companyId), stateKey: WEBHOOK_STATE_KEY });
9029
- return raw ?? null;
9533
+ async function handleIssuesEvent(ctx, payload) {
9534
+ const issueData = payload.issue;
9535
+ const repoData = payload.repository;
9536
+ if (!issueData || !repoData) return;
9537
+ await upsertRepo(ctx.db, {
9538
+ id: repoData.id,
9539
+ fullName: repoData.full_name,
9540
+ owner: repoData.owner.login,
9541
+ name: repoData.name,
9542
+ private: repoData.private,
9543
+ defaultBranch: repoData.default_branch,
9544
+ htmlUrl: repoData.html_url,
9545
+ description: repoData.description,
9546
+ language: repoData.language,
9547
+ topics: repoData.topics ?? [],
9548
+ updatedAt: repoData.updated_at
9549
+ });
9550
+ const issue = {
9551
+ id: issueData.id,
9552
+ repoId: repoData.id,
9553
+ number: issueData.number,
9554
+ title: issueData.title,
9555
+ body: issueData.body,
9556
+ state: issueData.state,
9557
+ author: issueData.user.login,
9558
+ labels: (issueData.labels ?? []).map(
9559
+ (l) => l.name
9560
+ ),
9561
+ htmlUrl: issueData.html_url,
9562
+ createdAt: issueData.created_at,
9563
+ updatedAt: issueData.updated_at
9564
+ };
9565
+ await upsertIssue(ctx.db, issue);
9566
+ ctx.logger.info(`Webhook: upserted issue #${issue.number} from ${repoData.full_name}`);
9030
9567
  }
9031
- async function listRepos(ctx, companyId) {
9032
- const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
9033
- const token = await resolveGithubToken(ctx, companyId);
9034
- if (!token) {
9035
- return {
9036
- status: "degraded",
9037
- checkedAt,
9038
- message: "Configure o token GitHub em Configura\xE7\xF5es (PAT ou Secret ID)",
9039
- repos: []
9040
- };
9568
+
9569
+ // src/sync/incremental-sync.ts
9570
+ init_queries();
9571
+ async function runIncrementalSync(ctx, companyId) {
9572
+ const repos = await listRepos(ctx.db);
9573
+ if (repos.length === 0) return;
9574
+ const lastSync = await getLastSyncTime(ctx.db);
9575
+ const since = lastSync ?? new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
9576
+ const logId = await createSyncLog(ctx.db, "incremental");
9577
+ let reposSynced = 0;
9578
+ let prsSynced = 0;
9579
+ let issuesSynced = 0;
9580
+ const errors = [];
9581
+ for (const repo of repos) {
9582
+ try {
9583
+ const prResult = await syncRepoPRs(ctx, companyId, repo.id, repo.fullName, since);
9584
+ const issueResult = await syncRepoIssues(ctx, companyId, repo.id, repo.fullName, since);
9585
+ prsSynced += prResult;
9586
+ issuesSynced += issueResult;
9587
+ reposSynced++;
9588
+ } catch (err) {
9589
+ const msg = `${repo.fullName}: ${err instanceof Error ? err.message : String(err)}`;
9590
+ errors.push(msg);
9591
+ ctx.logger.error(`Sync error: ${msg}`);
9592
+ }
9041
9593
  }
9042
- 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(
9043
9599
  ctx,
9044
- token,
9045
- `/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}`
9046
9602
  );
9047
- if (!res.ok) {
9048
- return {
9049
- status: "error",
9050
- checkedAt,
9051
- message: `GitHub API returned ${res.status}`,
9052
- 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
9053
9626
  };
9627
+ await upsertPR(ctx.db, pr);
9628
+ await detectAndLinkCards(ctx, pr.id, pr.headBranch, pr.title);
9054
9629
  }
9055
- const rows = await res.json();
9056
- const repos = rows.map((r) => ({
9057
- id: r.id,
9058
- fullName: r.full_name,
9059
- private: r.private,
9060
- htmlUrl: r.html_url,
9061
- updatedAt: r.updated_at,
9062
- defaultBranch: r.default_branch
9063
- }));
9064
- return { status: "ok", checkedAt, repos };
9630
+ return items.length;
9065
9631
  }
9066
- async function resolveTrackedRepos(ctx, companyId) {
9067
- const tracked = await ctx.state.get({ ...companyScope2(companyId), stateKey: TRACKED_REPOS_KEY });
9068
- if (Array.isArray(tracked) && tracked.length > 0) {
9069
- return tracked.filter((r) => typeof r === "string").slice(0, MAX_REPOS_PER_SYNC);
9070
- }
9071
- const reposData = await listRepos(ctx, companyId);
9072
- return reposData.repos.slice(0, MAX_REPOS_PER_SYNC).map((r) => r.fullName);
9073
- }
9074
- async function fetchPullRequestsForRepo(ctx, token, repoFullName) {
9075
- const { owner, repo } = parseRepoFullName(repoFullName);
9076
- const res = await githubFetch(
9632
+ async function syncRepoIssues(ctx, companyId, repoId, fullName, since) {
9633
+ const { data } = await githubFetch(
9077
9634
  ctx,
9078
- token,
9079
- `/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`
9080
9637
  );
9081
- if (!res.ok) {
9082
- throw new Error(`PR sync failed for ${repoFullName}: HTTP ${res.status}`);
9083
- }
9084
- const rows = await res.json();
9085
- return rows.map((pr) => ({
9086
- id: pr.id,
9087
- number: pr.number,
9088
- title: pr.title,
9089
- state: pr.state,
9090
- htmlUrl: pr.html_url,
9091
- repoFullName,
9092
- updatedAt: pr.updated_at
9093
- }));
9094
- }
9095
- async function fetchIssuesForRepo(ctx, token, repoFullName) {
9096
- const { owner, repo } = parseRepoFullName(repoFullName);
9097
- const res = await githubFetch(
9098
- ctx,
9099
- token,
9100
- `/repos/${owner}/${repo}/issues?state=open&per_page=${MAX_ITEMS_PER_REPO}&sort=updated&direction=desc`
9638
+ const items = data.filter(
9639
+ (item) => !item.pull_request
9101
9640
  );
9102
- if (!res.ok) {
9103
- throw new Error(`Issue sync failed for ${repoFullName}: HTTP ${res.status}`);
9104
- }
9105
- const rows = await res.json();
9106
- return rows.filter((row) => !row.pull_request).map((issue) => ({
9107
- id: issue.id,
9108
- number: issue.number,
9109
- title: issue.title,
9110
- state: issue.state,
9111
- htmlUrl: issue.html_url,
9112
- repoFullName,
9113
- updatedAt: issue.updated_at
9114
- }));
9641
+ for (const item of items) {
9642
+ const issue = {
9643
+ id: item.id,
9644
+ repoId,
9645
+ number: item.number,
9646
+ title: item.title,
9647
+ body: item.body,
9648
+ state: item.state,
9649
+ author: item.user.login,
9650
+ labels: (item.labels ?? []).map(
9651
+ (l) => l.name
9652
+ ),
9653
+ htmlUrl: item.html_url,
9654
+ createdAt: item.created_at,
9655
+ updatedAt: item.updated_at
9656
+ };
9657
+ await upsertIssue(ctx.db, issue);
9658
+ }
9659
+ return items.length;
9115
9660
  }
9116
- async function runSync(ctx, companyId, mode) {
9117
- const token = await resolveGithubToken(ctx, companyId);
9118
- if (!token) {
9119
- throw new Error("Configure o token GitHub em Configura\xE7\xF5es antes de sincronizar");
9120
- }
9121
- const repos = await resolveTrackedRepos(ctx, companyId);
9122
- const existing = await loadSyncCache(ctx, companyId) ?? {
9123
- syncedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
9124
- pullRequests: [],
9125
- issues: [],
9126
- errors: []
9127
- };
9661
+
9662
+ // src/sync/full-sync.ts
9663
+ init_queries();
9664
+ async function runFullSync(ctx, companyId) {
9665
+ const repos = await listRepos(ctx.db);
9666
+ if (repos.length === 0) return;
9667
+ const logId = await createSyncLog(ctx.db, "full");
9668
+ let reposSynced = 0;
9669
+ let prsSynced = 0;
9670
+ let issuesSynced = 0;
9128
9671
  const errors = [];
9129
- let pullRequests = mode === "issues" ? existing.pullRequests : [];
9130
- let issues = mode === "pullRequests" ? existing.issues : [];
9131
- for (const repoFullName of repos) {
9672
+ for (const repo of repos) {
9132
9673
  try {
9133
- if (mode === "pullRequests" || mode === "all") {
9134
- pullRequests = pullRequests.concat(
9135
- await fetchPullRequestsForRepo(ctx, token, repoFullName)
9136
- );
9674
+ const { data: repoData } = await githubFetch(ctx, companyId, `/repos/${repo.fullName}`);
9675
+ const rd = repoData;
9676
+ await upsertRepo(ctx.db, {
9677
+ id: rd.id,
9678
+ fullName: rd.full_name,
9679
+ owner: rd.owner.login,
9680
+ name: rd.name,
9681
+ private: rd.private,
9682
+ defaultBranch: rd.default_branch,
9683
+ htmlUrl: rd.html_url,
9684
+ description: rd.description,
9685
+ language: rd.language,
9686
+ topics: rd.topics ?? [],
9687
+ updatedAt: rd.updated_at
9688
+ });
9689
+ const { data: prs } = await githubFetch(
9690
+ ctx,
9691
+ companyId,
9692
+ `/repos/${repo.fullName}/pulls?state=open&per_page=100`
9693
+ );
9694
+ for (const item of prs) {
9695
+ const pr = {
9696
+ id: item.id,
9697
+ repoId: repo.id,
9698
+ number: item.number,
9699
+ title: item.title,
9700
+ body: item.body,
9701
+ state: "open",
9702
+ author: item.user.login,
9703
+ headBranch: item.head.ref,
9704
+ baseBranch: item.base.ref,
9705
+ htmlUrl: item.html_url,
9706
+ draft: item.draft,
9707
+ mergeable: item.mergeable,
9708
+ mergedAt: null,
9709
+ createdAt: item.created_at,
9710
+ updatedAt: item.updated_at
9711
+ };
9712
+ await upsertPR(ctx.db, pr);
9713
+ await detectAndLinkCards(ctx, pr.id, pr.headBranch, pr.title);
9714
+ prsSynced++;
9137
9715
  }
9138
- if (mode === "issues" || mode === "all") {
9139
- 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++;
9140
9737
  }
9738
+ reposSynced++;
9141
9739
  } catch (err) {
9142
- const message = err instanceof Error ? err.message : String(err);
9143
- errors.push(message);
9144
- ctx.logger.warn("GitHub sync repo failed", { repoFullName, message });
9740
+ errors.push(`${repo.fullName}: ${err instanceof Error ? err.message : String(err)}`);
9145
9741
  }
9146
9742
  }
9147
- const cache = {
9148
- syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
9149
- pullRequests: pullRequests.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, MAX_ITEMS_PER_REPO * MAX_REPOS_PER_SYNC),
9150
- issues: issues.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, MAX_ITEMS_PER_REPO * MAX_REPOS_PER_SYNC),
9151
- 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()
9152
9787
  };
9153
- await saveSyncCache(ctx, companyId, cache);
9154
- return cache;
9155
9788
  }
9156
- async function buildSyncOverview(ctx, companyId) {
9157
- const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
9158
- const token = await resolveGithubToken(ctx, companyId);
9159
- const cache = await loadSyncCache(ctx, companyId);
9160
- if (!token) {
9161
- return {
9162
- status: "degraded",
9163
- checkedAt,
9164
- message: "Configure o token GitHub em Configura\xE7\xF5es para sincronizar PRs e issues",
9165
- lastSyncedAt: cache?.syncedAt ?? null,
9166
- pullRequestCount: cache?.pullRequests.length ?? 0,
9167
- issueCount: cache?.issues.length ?? 0,
9168
- recentPullRequests: cache?.pullRequests.slice(0, 10) ?? [],
9169
- recentIssues: cache?.issues.slice(0, 10) ?? [],
9170
- lastErrors: cache?.errors ?? []
9171
- };
9172
- }
9173
- if (!cache) {
9174
- return {
9175
- status: "not_synced",
9176
- checkedAt,
9177
- message: "Run sync to fetch open PRs and issues",
9178
- lastSyncedAt: null,
9179
- pullRequestCount: 0,
9180
- issueCount: 0,
9181
- recentPullRequests: [],
9182
- recentIssues: [],
9183
- lastErrors: []
9789
+
9790
+ // src/graphify/graph-generator.ts
9791
+ init_queries();
9792
+ async function generateHighLevelGraph(ctx, companyId) {
9793
+ const repos = await listRepos(ctx.db);
9794
+ const prs = await listPRs(ctx.db, { state: "open" });
9795
+ const nodes = repos.map((r) => ({
9796
+ id: `repo:${r.fullName}`,
9797
+ label: r.fullName,
9798
+ type: "repo",
9799
+ metadata: { language: r.language, private: r.private, defaultBranch: r.defaultBranch }
9800
+ }));
9801
+ const edges = [];
9802
+ for (const pr of prs) {
9803
+ const prNode = {
9804
+ id: `pr:${pr.repoFullName}#${pr.number}`,
9805
+ label: `#${pr.number}: ${pr.title}`,
9806
+ type: "pr",
9807
+ metadata: { state: pr.state, author: pr.author, draft: pr.draft }
9184
9808
  };
9809
+ nodes.push(prNode);
9810
+ edges.push({
9811
+ source: prNode.id,
9812
+ target: `repo:${pr.repoFullName}`,
9813
+ label: `${pr.headBranch} \u2192 ${pr.baseBranch}`,
9814
+ type: "pr_target"
9815
+ });
9185
9816
  }
9186
9817
  return {
9187
- status: "ok",
9188
- checkedAt,
9189
- message: `Last sync ${cache.syncedAt}`,
9190
- lastSyncedAt: cache.syncedAt,
9191
- pullRequestCount: cache.pullRequests.length,
9192
- issueCount: cache.issues.length,
9193
- recentPullRequests: cache.pullRequests.slice(0, 10),
9194
- recentIssues: cache.issues.slice(0, 10),
9195
- lastErrors: cache.errors
9818
+ nodes,
9819
+ edges,
9820
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9821
+ repoFullName: "*",
9822
+ level: "high"
9196
9823
  };
9197
9824
  }
9198
- async function registerGithubWebhook(ctx, companyId, repoFullName, events) {
9199
- const token = await resolveGithubToken(ctx, companyId);
9200
- if (!token) {
9201
- throw new Error("Configure o token GitHub em Configura\xE7\xF5es antes de registrar webhooks");
9202
- }
9203
- const pluginId = ctx.manifest.id;
9204
- const inboundUrl = buildInboundWebhookUrl(pluginId);
9205
- const { owner, repo } = parseRepoFullName(repoFullName);
9206
- const payload = {
9207
- name: "web",
9208
- active: true,
9209
- events: events.length > 0 ? events : ["pull_request", "issues"],
9210
- config: {
9211
- url: inboundUrl,
9212
- content_type: "json",
9213
- 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
+ }
9214
9880
  }
9215
- };
9216
- const res = await githubFetch(ctx, token, `/repos/${owner}/${repo}/hooks`, {
9217
- method: "POST",
9218
- headers: { "Content-Type": "application/json" },
9219
- body: JSON.stringify(payload)
9220
- });
9221
- if (!res.ok) {
9222
- const body = await res.text();
9223
- throw new Error(`GitHub webhook registration failed (${res.status}): ${body}`);
9224
9881
  }
9225
- const hook = await res.json();
9226
- const config = {
9882
+ return {
9883
+ nodes,
9884
+ edges,
9885
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
9227
9886
  repoFullName,
9228
- events: payload.events,
9229
- hookId: hook.id,
9230
- configuredAt: (/* @__PURE__ */ new Date()).toISOString(),
9231
- inboundUrl
9887
+ level: "code"
9232
9888
  };
9233
- await ctx.state.set({ ...companyScope2(companyId), stateKey: WEBHOOK_STATE_KEY }, config);
9234
- return config;
9235
- }
9236
- function requireCompanyId(input) {
9237
- const companyId = input?.companyId;
9238
- if (!companyId) {
9239
- throw new Error("companyId is required");
9240
- }
9241
- return companyId;
9242
- }
9243
- async function handleGithubWebhook(input) {
9244
- const ctx = workerCtx;
9245
- if (!ctx) {
9246
- return;
9247
- }
9248
- if (input.endpointKey !== GITHUB_WEBHOOK_ENDPOINT) {
9249
- return;
9250
- }
9251
- const payload = input.parsedBody ?? {};
9252
- const repoFullName = payload.repository?.full_name;
9253
- if (!repoFullName) {
9254
- return;
9255
- }
9256
- const companies = await ctx.companies.list();
9257
- for (const company of companies) {
9258
- const webhook = await loadWebhookConfig(ctx, company.id);
9259
- if (!webhook || webhook.repoFullName !== repoFullName) {
9260
- continue;
9261
- }
9262
- try {
9263
- await runSync(ctx, company.id, "all");
9264
- ctx.logger.info("GitHub webhook triggered sync", {
9265
- companyId: company.id,
9266
- repoFullName,
9267
- action: payload.action ?? "unknown",
9268
- requestId: input.requestId
9269
- });
9270
- } catch (err) {
9271
- const message = err instanceof Error ? err.message : String(err);
9272
- ctx.logger.warn("GitHub webhook sync failed", { companyId: company.id, message });
9273
- }
9274
- }
9275
9889
  }
9890
+
9891
+ // src/worker.ts
9892
+ init_queries();
9893
+ var pluginCtx = null;
9276
9894
  var plugin = definePlugin({
9277
9895
  async setup(ctx) {
9278
- workerCtx = ctx;
9279
- ctx.events.on("issue.created", async (event) => {
9280
- const issueId = event.entityId ?? "unknown";
9281
- await ctx.state.set({ scopeKind: "issue", scopeId: issueId, stateKey: "seen" }, true);
9282
- ctx.logger.info("GitHub plugin observed issue.created", { issueId });
9283
- });
9284
- ctx.data.register("health", async ({ companyId }) => {
9285
- if (!companyId) {
9286
- throw new Error("companyId is required");
9287
- }
9288
- const cid = String(companyId);
9289
- const auth = await getGithubAuthStatus(ctx, cid);
9290
- const token = await resolveGithubToken(ctx, cid);
9291
- if (!token) {
9292
- return {
9293
- status: "degraded",
9294
- checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
9295
- message: auth.mode === "secret-ref" ? "Secret ID salvo, mas o Paperclip ainda n\xE3o resolve secret refs em plugins \u2014 cole o PAT abaixo ou aguarde PAP-2394" : "Cole um Personal Access Token (PAT) em Configura\xE7\xF5es e clique em Salvar",
9296
- auth
9297
- };
9298
- }
9299
- const res = await githubFetch(ctx, token, "/user");
9300
- if (!res.ok) {
9301
- return {
9302
- status: "error",
9303
- checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
9304
- message: `GitHub API retornou ${res.status} \u2014 verifique escopos do PAT`,
9305
- auth
9306
- };
9896
+ pluginCtx = ctx;
9897
+ ctx.logger.info("GitHub Manager v2 starting");
9898
+ registerReviewTools(ctx);
9899
+ ctx.jobs.register("sync-github", async (job) => {
9900
+ ctx.logger.info("Running scheduled incremental sync");
9901
+ const companies = await ctx.companies.list();
9902
+ for (const company of companies) {
9903
+ try {
9904
+ await runIncrementalSync(ctx, company.id);
9905
+ } catch (err) {
9906
+ ctx.logger.error(`Sync failed for company ${company.id}: ${err}`);
9907
+ }
9307
9908
  }
9308
- const user = await res.json();
9309
- return {
9310
- status: "ok",
9311
- checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
9312
- login: user.login ?? "unknown",
9313
- auth
9314
- };
9315
9909
  });
9316
9910
  ctx.data.register("repos", async ({ companyId }) => {
9317
- if (!companyId) {
9318
- throw new Error("companyId is required");
9319
- }
9320
- return listRepos(ctx, String(companyId));
9321
- });
9322
- ctx.data.register("syncOverview", async ({ companyId }) => {
9323
- if (!companyId) {
9324
- throw new Error("companyId is required");
9325
- }
9326
- return buildSyncOverview(ctx, String(companyId));
9327
- });
9328
- ctx.data.register("webhookConfig", async ({ companyId }) => {
9329
- if (!companyId) {
9330
- throw new Error("companyId is required");
9331
- }
9332
- 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" });
9333
9928
  return {
9334
- configured: Boolean(config),
9335
- config,
9336
- inboundUrl: buildInboundWebhookUrl(ctx.manifest.id)
9929
+ lastSync,
9930
+ repoCount: repos.length,
9931
+ openPRCount: openPRs.length
9337
9932
  };
9338
9933
  });
9339
- ctx.actions.register("ping", async () => {
9340
- return { pong: true, at: (/* @__PURE__ */ new Date()).toISOString() };
9341
- });
9342
- ctx.actions.register("saveGithubToken", async (input) => {
9343
- const companyId = requireCompanyId(input);
9344
- const token = input?.token;
9345
- if (typeof token !== "string") {
9346
- throw new Error("token is required");
9347
- }
9348
- await saveGithubPat(ctx, companyId, token);
9349
- return { saved: true, at: (/* @__PURE__ */ new Date()).toISOString() };
9350
- });
9351
- ctx.actions.register("saveGithubSecretRef", async (input) => {
9352
- const companyId = requireCompanyId(input);
9353
- const secretRef = input?.secretRef;
9354
- if (typeof secretRef !== "string") {
9355
- throw new Error("secretRef is required");
9356
- }
9357
- await saveGithubSecretRef(ctx, companyId, secretRef);
9358
- return { saved: true, at: (/* @__PURE__ */ new Date()).toISOString() };
9359
- });
9360
- ctx.actions.register("clearGithubAuth", async (input) => {
9361
- const companyId = requireCompanyId(input);
9362
- await clearGithubAuth(ctx, companyId);
9363
- return { cleared: true, at: (/* @__PURE__ */ new Date()).toISOString() };
9364
- });
9365
- ctx.actions.register("setTrackedRepos", async (input) => {
9366
- const companyId = requireCompanyId(input);
9367
- const repos = input?.repos;
9368
- if (!Array.isArray(repos)) {
9369
- throw new Error("repos array is required");
9934
+ ctx.data.register("graph-data", async ({ companyId, repoFullName, level }) => {
9935
+ if (level === "high") {
9936
+ return await generateHighLevelGraph(ctx, companyId);
9370
9937
  }
9371
- const normalized = repos.filter((r) => typeof r === "string" && r.includes("/")).slice(0, MAX_REPOS_PER_SYNC);
9372
- await ctx.state.set({ ...companyScope2(companyId), stateKey: TRACKED_REPOS_KEY }, normalized);
9373
- return { saved: true, repos: normalized, at: (/* @__PURE__ */ new Date()).toISOString() };
9938
+ return await generateCodeGraph(ctx, companyId, repoFullName);
9374
9939
  });
9375
- ctx.actions.register("syncPullRequests", async (input) => {
9376
- const companyId = requireCompanyId(input);
9377
- const cache = await runSync(ctx, companyId, "pullRequests");
9378
- return {
9379
- syncedAt: cache.syncedAt,
9380
- pullRequestCount: cache.pullRequests.length,
9381
- errors: cache.errors
9382
- };
9940
+ ctx.data.register("available-agents", async ({ companyId }) => {
9941
+ const agents = await ctx.agents.list({ companyId });
9942
+ return { agents };
9383
9943
  });
9384
- ctx.actions.register("syncIssues", async (input) => {
9385
- const companyId = requireCompanyId(input);
9386
- const cache = await runSync(ctx, companyId, "issues");
9387
- return {
9388
- syncedAt: cache.syncedAt,
9389
- issueCount: cache.issues.length,
9390
- errors: cache.errors
9391
- };
9944
+ ctx.actions.register("save-token", async ({ companyId, token }) => {
9945
+ await saveGithubPAT(ctx, companyId, token);
9946
+ return { ok: true };
9392
9947
  });
9393
- ctx.actions.register("syncAll", async (input) => {
9394
- const companyId = requireCompanyId(input);
9395
- const cache = await runSync(ctx, companyId, "all");
9396
- return {
9397
- syncedAt: cache.syncedAt,
9398
- pullRequestCount: cache.pullRequests.length,
9399
- issueCount: cache.issues.length,
9400
- errors: cache.errors
9401
- };
9948
+ ctx.actions.register("save-secret-ref", async ({ companyId, secretRef }) => {
9949
+ await saveGithubSecretRef(ctx, companyId, secretRef);
9950
+ return { ok: true };
9402
9951
  });
9403
- ctx.actions.register("configureWebhook", async (input) => {
9404
- const companyId = requireCompanyId(input);
9405
- const repoFullName = input?.repoFullName;
9406
- const events = input?.events ?? ["pull_request", "issues"];
9407
- if (!repoFullName) {
9408
- 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) };
9409
9960
  }
9410
- const config = await registerGithubWebhook(ctx, companyId, repoFullName, events);
9411
- return { saved: true, config };
9412
9961
  });
9413
- ctx.jobs.register("sync-github", async (job) => {
9414
- const companies = await ctx.companies.list();
9415
- for (const company of companies) {
9416
- try {
9417
- await runSync(ctx, company.id, "all");
9418
- ctx.logger.info("Scheduled GitHub sync completed", {
9419
- companyId: company.id,
9420
- runId: job.runId
9421
- });
9422
- } catch (err) {
9423
- const message = err instanceof Error ? err.message : String(err);
9424
- 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.`
9425
10001
  }
10002
+ );
10003
+ return { ok: true };
10004
+ });
10005
+ ctx.actions.register("run-quick-check", async ({ companyId, repoFullName, prNumber }) => {
10006
+ const [owner, repo] = repoFullName.split("/");
10007
+ const result = await runQuickCheck(ctx, companyId, owner, repo, prNumber);
10008
+ return result;
10009
+ });
10010
+ ctx.actions.register("generate-graph", async ({ companyId, repoFullName, level }) => {
10011
+ if (level === "high") {
10012
+ return await generateHighLevelGraph(ctx, companyId);
9426
10013
  }
10014
+ return await generateCodeGraph(ctx, companyId, repoFullName);
10015
+ });
10016
+ ctx.events.on("company.created", async (event) => {
10017
+ await ctx.agents.managed.reconcile("github-reviewer", event.companyId);
9427
10018
  });
9428
10019
  },
9429
10020
  async onHealth() {
9430
- return { status: "ok", message: "GitHub Manager worker is running" };
10021
+ return { status: "ok", message: "GitHub Manager v2 running" };
9431
10022
  },
9432
10023
  async onWebhook(input) {
9433
- await handleGithubWebhook(input);
10024
+ if (!pluginCtx) throw new Error("Plugin not initialized");
10025
+ await handleGithubWebhook(pluginCtx, input);
10026
+ },
10027
+ async onShutdown() {
10028
+ pluginCtx = null;
9434
10029
  }
9435
10030
  });
9436
10031
  var worker_default = plugin;