@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 +24 -1
- package/dist/manifest.js +2 -1
- package/dist/manifest.js.map +2 -2
- package/dist/worker.js +428 -2
- package/dist/worker.js.map +4 -4
- package/package.json +1 -1
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.
|
|
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.
|
|
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",
|
package/dist/manifest.js.map
CHANGED
|
@@ -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.
|
|
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
|
|
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);
|