@gaud_erp/paperclip-github-manager 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,29 @@ Paperclip connector plugin for GitHub — repository listing, PR/issue sync, and
8
8
  - Sync open pull requests and issues (manual actions + scheduled job)
9
9
  - Register GitHub webhooks pointing at the Paperclip host inbound URL
10
10
  - Dashboard health widget and full **GitHub** page in the host UI
11
+ - **Agent tools** for autonomous PR code review (diff, inline comments, review verdict, file read, repo list, issue search)
12
+
13
+ ## Agent reviewer workflow
14
+
15
+ Tools are exposed to Paperclip agents as `cus.github-manager/github_*` (plugin-namespaced at runtime).
16
+
17
+ 1. **Collect** — `github_get_pull_request_diff` with `owner`, `repo`, `pr_number`
18
+ 2. **Analyze** — agent reads metadata + unified diff (truncated above ~120k chars with a changed-files index)
19
+ 3. **Context** (optional) — `github_read_file_content` when the diff is insufficient
20
+ 4. **Inline feedback** (optional) — `github_create_review_comment` per finding (`commit_id` = PR head SHA)
21
+ 5. **Verdict** — `github_submit_pr_review` with `APPROVE`, `REQUEST_CHANGES`, or `COMMENT`
22
+
23
+ Supporting tools: `github_list_repositories`, `github_search_issues`.
24
+
25
+ ### Configuration
26
+
27
+ | Variable | Purpose |
28
+ |----------|---------|
29
+ | `GITHUB_TOKEN` | Optional worker-level PAT fallback |
30
+ | `GITHUB_DEFAULT_OWNER` | Default org/user when `owner` is omitted in list tools |
31
+ | `GITHUB_API_URL` | API base (GitHub Enterprise), default `https://api.github.com` |
32
+
33
+ Per-company PAT in **GitHub → Configurações** is preferred for multi-tenant installs.
11
34
 
12
35
  ## Requirements
13
36
 
@@ -46,7 +69,7 @@ Cada **release no GitHub** (tag `v*`, ex. `v0.3.0`) dispara o workflow [publish-
46
69
  ## Production install (npm)
47
70
 
48
71
  ```bash
49
- paperclipai plugin install @gaud_erp/paperclip-github-manager@0.3.0 --api-base http://127.0.0.1:3100
72
+ paperclipai plugin install @gaud_erp/paperclip-github-manager@0.4.0 --api-base http://127.0.0.1:3100
50
73
  paperclipai plugin inspect cus.github-manager --api-base http://127.0.0.1:3100
51
74
  ```
52
75
 
package/dist/manifest.js CHANGED
@@ -9,7 +9,7 @@ var ROUTES = {
9
9
  var manifest = {
10
10
  id: "cus.github-manager",
11
11
  apiVersion: 1,
12
- version: "0.3.0",
12
+ version: "0.4.0",
13
13
  displayName: "GitHub Manager",
14
14
  description: "Plugin Paperclip para gerenciar repositorios, PRs, issues e webhooks GitHub",
15
15
  author: "CUS",
@@ -20,6 +20,7 @@ var manifest = {
20
20
  "secrets.read-ref",
21
21
  "plugin.state.read",
22
22
  "plugin.state.write",
23
+ "agent.tools.register",
23
24
  "jobs.schedule",
24
25
  "webhooks.receive",
25
26
  "ui.sidebar.register",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/constants.ts", "../src/manifest.ts"],
4
- "sourcesContent": ["/** Plugin page route segments (manifest `routePath` \u2014 single slug, no slashes). */\nexport const ROUTES = {\n repos: \"github\",\n settings: \"github-settings\",\n pullRequests: \"github-pull-requests\"\n} as const;\n\n/** Host navigation paths (use with `linkProps`). */\nexport const PATHS = {\n repos: \"/github\",\n settings: \"/github-settings\",\n pullRequests: \"/github-pull-requests\",\n companySecrets: \"/company/settings\"\n} as const;\n\nexport const GITHUB_TOKEN_SECRET_KEY = \"github_token\";\n", "import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\nimport { ROUTES } from \"./constants.js\";\n\nconst manifest: PaperclipPluginManifestV1 = {\n id: \"cus.github-manager\",\n apiVersion: 1,\n version: \"0.3.0\",\n displayName: \"GitHub Manager\",\n description: \"Plugin Paperclip para gerenciar repositorios, PRs, issues e webhooks GitHub\",\n author: \"CUS\",\n categories: [\"connector\"],\n capabilities: [\n \"events.subscribe\",\n \"http.outbound\",\n \"secrets.read-ref\",\n \"plugin.state.read\",\n \"plugin.state.write\",\n \"jobs.schedule\",\n \"webhooks.receive\",\n \"ui.sidebar.register\",\n \"ui.dashboardWidget.register\",\n \"ui.page.register\"\n ],\n jobs: [\n {\n jobKey: \"sync-github\",\n displayName: \"Sync GitHub PRs and issues\",\n description: \"Pulls open PRs and issues for tracked repositories\",\n schedule: \"0 */6 * * *\"\n }\n ],\n webhooks: [\n {\n endpointKey: \"github-events\",\n displayName: \"GitHub repository events\",\n description: \"Receives pull_request and issues events from configured repositories\"\n }\n ],\n entrypoints: {\n worker: \"./dist/worker.js\",\n ui: \"./dist/ui\"\n },\n ui: {\n slots: [\n {\n type: \"sidebarPanel\",\n id: \"github-module\",\n displayName: \"GitHub\",\n exportName: \"GitHubSidebarModule\",\n order: 45\n },\n {\n type: \"routeSidebar\",\n id: \"github-route-nav\",\n displayName: \"GitHub\",\n routePath: ROUTES.repos,\n exportName: \"GitHubRouteSidebar\",\n order: 45\n },\n {\n type: \"routeSidebar\",\n id: \"github-settings-route-nav\",\n displayName: \"GitHub\",\n routePath: ROUTES.settings,\n exportName: \"GitHubRouteSidebar\",\n order: 45\n },\n {\n type: \"routeSidebar\",\n id: \"github-prs-route-nav\",\n displayName: \"GitHub\",\n routePath: ROUTES.pullRequests,\n exportName: \"GitHubRouteSidebar\",\n order: 45\n },\n {\n type: \"dashboardWidget\",\n id: \"github-health\",\n displayName: \"GitHub Manager Health\",\n exportName: \"DashboardWidget\"\n },\n {\n type: \"page\",\n id: \"github-settings-page\",\n displayName: \"GitHub \u2014 Configura\u00E7\u00F5es\",\n routePath: ROUTES.settings,\n exportName: \"GitHubSettingsPage\",\n order: 45\n },\n {\n type: \"page\",\n id: \"github-repos-page\",\n displayName: \"GitHub \u2014 Reposit\u00F3rios\",\n routePath: ROUTES.repos,\n exportName: \"GitHubReposPage\",\n order: 45\n },\n {\n type: \"page\",\n id: \"github-prs-page\",\n displayName: \"GitHub \u2014 Pull requests\",\n routePath: ROUTES.pullRequests,\n exportName: \"GitHubPullRequestsPage\",\n order: 45\n }\n ]\n }\n};\n\nexport default manifest;\n"],
5
- "mappings": ";AACO,IAAM,SAAS;AAAA,EACpB,OAAO;AAAA,EACP,UAAU;AAAA,EACV,cAAc;AAChB;;;ACFA,IAAM,WAAsC;AAAA,EAC1C,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,aAAa;AAAA,EACb,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,YAAY,CAAC,WAAW;AAAA,EACxB,cAAc;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ;AAAA,MACE,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,aAAa;AAAA,MACb,UAAU;AAAA,IACZ;AAAA,EACF;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,aAAa;AAAA,MACb,aAAa;AAAA,MACb,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,IAAI;AAAA,EACN;AAAA,EACA,IAAI;AAAA,IACF,OAAO;AAAA,MACL;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,YAAY;AAAA,MACd;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,mBAAQ;",
4
+ "sourcesContent": ["/** Plugin page route segments (manifest `routePath` \u2014 single slug, no slashes). */\nexport const ROUTES = {\n repos: \"github\",\n settings: \"github-settings\",\n pullRequests: \"github-pull-requests\"\n} as const;\n\n/** Host navigation paths (use with `linkProps`). */\nexport const PATHS = {\n repos: \"/github\",\n settings: \"/github-settings\",\n pullRequests: \"/github-pull-requests\",\n companySecrets: \"/company/settings\"\n} as const;\n\nexport const GITHUB_TOKEN_SECRET_KEY = \"github_token\";\n", "import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\nimport { ROUTES } from \"./constants.js\";\n\nconst manifest: PaperclipPluginManifestV1 = {\n id: \"cus.github-manager\",\n apiVersion: 1,\n version: \"0.4.0\",\n displayName: \"GitHub Manager\",\n description: \"Plugin Paperclip para gerenciar repositorios, PRs, issues e webhooks GitHub\",\n author: \"CUS\",\n categories: [\"connector\"],\n capabilities: [\n \"events.subscribe\",\n \"http.outbound\",\n \"secrets.read-ref\",\n \"plugin.state.read\",\n \"plugin.state.write\",\n \"agent.tools.register\",\n \"jobs.schedule\",\n \"webhooks.receive\",\n \"ui.sidebar.register\",\n \"ui.dashboardWidget.register\",\n \"ui.page.register\"\n ],\n jobs: [\n {\n jobKey: \"sync-github\",\n displayName: \"Sync GitHub PRs and issues\",\n description: \"Pulls open PRs and issues for tracked repositories\",\n schedule: \"0 */6 * * *\"\n }\n ],\n webhooks: [\n {\n endpointKey: \"github-events\",\n displayName: \"GitHub repository events\",\n description: \"Receives pull_request and issues events from configured repositories\"\n }\n ],\n entrypoints: {\n worker: \"./dist/worker.js\",\n ui: \"./dist/ui\"\n },\n ui: {\n slots: [\n {\n type: \"sidebarPanel\",\n id: \"github-module\",\n displayName: \"GitHub\",\n exportName: \"GitHubSidebarModule\",\n order: 45\n },\n {\n type: \"routeSidebar\",\n id: \"github-route-nav\",\n displayName: \"GitHub\",\n routePath: ROUTES.repos,\n exportName: \"GitHubRouteSidebar\",\n order: 45\n },\n {\n type: \"routeSidebar\",\n id: \"github-settings-route-nav\",\n displayName: \"GitHub\",\n routePath: ROUTES.settings,\n exportName: \"GitHubRouteSidebar\",\n order: 45\n },\n {\n type: \"routeSidebar\",\n id: \"github-prs-route-nav\",\n displayName: \"GitHub\",\n routePath: ROUTES.pullRequests,\n exportName: \"GitHubRouteSidebar\",\n order: 45\n },\n {\n type: \"dashboardWidget\",\n id: \"github-health\",\n displayName: \"GitHub Manager Health\",\n exportName: \"DashboardWidget\"\n },\n {\n type: \"page\",\n id: \"github-settings-page\",\n displayName: \"GitHub \u2014 Configura\u00E7\u00F5es\",\n routePath: ROUTES.settings,\n exportName: \"GitHubSettingsPage\",\n order: 45\n },\n {\n type: \"page\",\n id: \"github-repos-page\",\n displayName: \"GitHub \u2014 Reposit\u00F3rios\",\n routePath: ROUTES.repos,\n exportName: \"GitHubReposPage\",\n order: 45\n },\n {\n type: \"page\",\n id: \"github-prs-page\",\n displayName: \"GitHub \u2014 Pull requests\",\n routePath: ROUTES.pullRequests,\n exportName: \"GitHubPullRequestsPage\",\n order: 45\n }\n ]\n }\n};\n\nexport default manifest;\n"],
5
+ "mappings": ";AACO,IAAM,SAAS;AAAA,EACpB,OAAO;AAAA,EACP,UAAU;AAAA,EACV,cAAc;AAChB;;;ACFA,IAAM,WAAsC;AAAA,EAC1C,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,aAAa;AAAA,EACb,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,YAAY,CAAC,WAAW;AAAA,EACxB,cAAc;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ;AAAA,MACE,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,aAAa;AAAA,MACb,UAAU;AAAA,IACZ;AAAA,EACF;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,aAAa;AAAA,MACb,aAAa;AAAA,MACb,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,IAAI;AAAA,EACN;AAAA,EACA,IAAI;AAAA,IACF,OAAO;AAAA,MACL;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,YAAY;AAAA,MACd;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,WAAW,OAAO;AAAA,QAClB,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,mBAAQ;",
6
6
  "names": []
7
7
  }
package/dist/worker.js CHANGED
@@ -8981,11 +8981,21 @@ async function resolveGithubToken(ctx, companyId) {
8981
8981
  }
8982
8982
  }
8983
8983
 
8984
+ // src/github-env.ts
8985
+ 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;
8992
+ }
8993
+
8984
8994
  // src/github-api.ts
8985
- var GITHUB_API = "https://api.github.com";
8986
8995
  var GITHUB_WEBHOOK_ENDPOINT = "github-events";
8987
8996
  async function githubFetch(ctx, token, path2, init) {
8988
- const url = path2.startsWith("http") ? path2 : `${GITHUB_API}${path2}`;
8997
+ const apiBase = getGithubApiBase();
8998
+ const url = path2.startsWith("http") ? path2 : `${apiBase}${path2}`;
8989
8999
  return ctx.http.fetch(url, {
8990
9000
  ...init,
8991
9001
  headers: {
@@ -9006,6 +9016,421 @@ function parseRepoFullName(fullName) {
9006
9016
  function buildInboundWebhookUrl(pluginId, baseUrl = "http://127.0.0.1:3100") {
9007
9017
  return `${baseUrl.replace(/\/+$/, "")}/api/plugins/${pluginId}/webhooks/${GITHUB_WEBHOOK_ENDPOINT}`;
9008
9018
  }
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}`);
9036
+ }
9037
+
9038
+ // src/github-review-tools.ts
9039
+ 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) => ({
9121
+ filename: f.filename,
9122
+ status: f.status,
9123
+ additions: f.additions,
9124
+ deletions: f.deletions
9125
+ }));
9126
+ }
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
+ );
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",
9322
+ {
9323
+ displayName: "Get PR diff",
9324
+ description: "Returns pull request metadata and unified diff. Large diffs are truncated with a file list.",
9325
+ parametersSchema: {
9326
+ ...ownerRepoSchema,
9327
+ properties: {
9328
+ ...ownerRepoSchema.properties,
9329
+ pr_number: { type: "integer", description: "Pull request number" }
9330
+ },
9331
+ required: ["owner", "repo", "pr_number"]
9332
+ }
9333
+ },
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);
9338
+ }
9339
+ );
9340
+ wrap(
9341
+ "github_create_review_comment",
9342
+ {
9343
+ displayName: "Create PR review comment",
9344
+ description: "Adds an inline review comment on a specific line in a pull request diff.",
9345
+ parametersSchema: {
9346
+ ...ownerRepoSchema,
9347
+ 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" }
9354
+ },
9355
+ required: ["owner", "repo", "pr_number", "commit_id", "path", "line", "body"]
9356
+ }
9357
+ },
9358
+ async (ctx2, token, params) => createReviewComment(ctx2, token, params)
9359
+ );
9360
+ wrap(
9361
+ "github_submit_pr_review",
9362
+ {
9363
+ displayName: "Submit PR review",
9364
+ description: "Submits a pull request review with APPROVE, REQUEST_CHANGES, or COMMENT.",
9365
+ 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,
9389
+ properties: {
9390
+ ...ownerRepoSchema.properties,
9391
+ path: { type: "string", description: "Path to the file" },
9392
+ ref: { type: "string", description: "Branch name or commit SHA (optional)" }
9393
+ },
9394
+ required: ["owner", "repo", "path"]
9395
+ }
9396
+ },
9397
+ async (ctx2, token, params) => readFileContent(ctx2, token, params)
9398
+ );
9399
+ wrap(
9400
+ "github_list_repositories",
9401
+ {
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
+ }
9414
+ },
9415
+ async (ctx2, token, params) => listRepositories(ctx2, token, params)
9416
+ );
9417
+ wrap(
9418
+ "github_search_issues",
9419
+ {
9420
+ displayName: "Search GitHub issues",
9421
+ description: "Searches issues using GitHub issue search syntax (q parameter).",
9422
+ parametersSchema: {
9423
+ type: "object",
9424
+ properties: {
9425
+ q: { type: "string", description: "GitHub search query" },
9426
+ per_page: { type: "integer", description: "Results per page (max 100)" }
9427
+ },
9428
+ required: ["q"]
9429
+ }
9430
+ },
9431
+ async (ctx2, token, params) => searchIssues(ctx2, token, params)
9432
+ );
9433
+ }
9009
9434
 
9010
9435
  // src/worker.ts
9011
9436
  var SYNC_STATE_KEY = "github.sync.cache";
@@ -9276,6 +9701,7 @@ async function handleGithubWebhook(input) {
9276
9701
  var plugin = definePlugin({
9277
9702
  async setup(ctx) {
9278
9703
  workerCtx = ctx;
9704
+ registerGithubReviewTools(ctx);
9279
9705
  ctx.events.on("issue.created", async (event) => {
9280
9706
  const issueId = event.entityId ?? "unknown";
9281
9707
  await ctx.state.set({ scopeKind: "issue", scopeId: issueId, stateKey: "seen" }, true);