@bettsnation/paperclip-plugin-github 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @bettsnation/paperclip-plugin-github
2
+
3
+ Paperclip plugin for bidirectional GitHub Issues sync.
4
+
5
+ ## Inbound (GitHub → Paperclip)
6
+ - GitHub issue created/labeled with trigger label → plugin creates Paperclip issue
7
+ - Verifies `X-Hub-Signature-256` natively (no signing incompatibility)
8
+ - Routes to configured agent (Lead Engineer or Full Stack Developer)
9
+
10
+ ## Outbound (Paperclip → GitHub)
11
+ - Paperclip issue completed → closes linked GitHub issue with comment
12
+
13
+ ## Install
14
+
15
+ ```
16
+ npm install @bettsnation/paperclip-plugin-github
17
+ ```
18
+
19
+ Or: Instance Settings → Plugins → Install Plugin → `@bettsnation/paperclip-plugin-github`
20
+
21
+ ## Configuration
22
+
23
+ | Setting | Required | Description |
24
+ |---------|----------|-------------|
25
+ | `githubTokenRef` | Yes | Secret ref for GitHub API token |
26
+ | `webhookSecretRef` | Yes | Secret ref for webhook signature verification |
27
+ | `targetCompanyId` | Yes | Paperclip company for new issues |
28
+ | `defaultAgentId` | Yes | Agent to assign new issues to |
29
+ | `triggerLabel` | No | Only sync issues with this label (default: `paperclip`) |
30
+ | `syncClosedBack` | No | Close GitHub issues on completion (default: true) |
31
+
32
+ ## GitHub Webhook Setup
33
+
34
+ In each repo (or org-level): Settings → Webhooks → Add webhook
35
+ - **Payload URL:** Plugin webhook endpoint from Paperclip
36
+ - **Content type:** `application/json`
37
+ - **Secret:** Same secret stored in Paperclip secret ref
38
+ - **Events:** Select "Issues"
39
+
40
+ ## Label-Based Routing
41
+
42
+ Only GitHub issues with the configured `triggerLabel` (default: `paperclip`) are synced. This prevents every GitHub issue from flooding Paperclip — only explicitly tagged issues get picked up.
43
+
44
+ ## License
45
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
2
+ declare const manifest: PaperclipPluginManifestV1;
3
+ export default manifest;
@@ -0,0 +1,70 @@
1
+ const manifest = {
2
+ id: "bettsnation.github",
3
+ apiVersion: 1,
4
+ version: "0.1.0",
5
+ displayName: "GitHub Integration",
6
+ description: "Bidirectional GitHub Issues sync. Receives GitHub webhooks to create Paperclip issues, syncs status changes back to GitHub.",
7
+ author: "BettsNation",
8
+ categories: ["connector"],
9
+ capabilities: [
10
+ "events.subscribe",
11
+ "webhooks.receive",
12
+ "issues.create",
13
+ "issues.read",
14
+ "http.outbound",
15
+ "secrets.read-ref",
16
+ ],
17
+ entrypoints: {
18
+ worker: "./dist/worker.js",
19
+ },
20
+ webhooks: [
21
+ {
22
+ endpointKey: "github-events",
23
+ displayName: "GitHub Webhook",
24
+ description: "Receives issue and PR events from GitHub repositories",
25
+ },
26
+ ],
27
+ instanceConfigSchema: {
28
+ type: "object",
29
+ properties: {
30
+ githubTokenRef: {
31
+ type: "string",
32
+ title: "GitHub Token (Secret Ref)",
33
+ description: "Personal access token or GitHub App token for API calls",
34
+ },
35
+ webhookSecretRef: {
36
+ type: "string",
37
+ title: "Webhook Secret (Secret Ref)",
38
+ description: "Shared secret for verifying GitHub webhook signatures (X-Hub-Signature-256)",
39
+ },
40
+ targetCompanyId: {
41
+ type: "string",
42
+ title: "Target Company ID",
43
+ description: "Paperclip company to create issues in",
44
+ },
45
+ defaultAgentId: {
46
+ type: "string",
47
+ title: "Default Agent ID",
48
+ description: "Agent to assign new issues to (e.g. Lead Engineer or Full Stack Developer)",
49
+ },
50
+ triggerLabel: {
51
+ type: "string",
52
+ title: "Trigger Label",
53
+ description: "Only sync GitHub issues with this label (e.g. 'paperclip')",
54
+ default: "paperclip",
55
+ },
56
+ syncClosedBack: {
57
+ type: "boolean",
58
+ title: "Close GitHub issues when Paperclip issues complete",
59
+ default: true,
60
+ },
61
+ },
62
+ required: [
63
+ "githubTokenRef",
64
+ "webhookSecretRef",
65
+ "targetCompanyId",
66
+ "defaultAgentId",
67
+ ],
68
+ },
69
+ };
70
+ export default manifest;
@@ -0,0 +1,2 @@
1
+ declare const plugin: import("@paperclipai/plugin-sdk").PaperclipPlugin;
2
+ export default plugin;
package/dist/worker.js ADDED
@@ -0,0 +1,145 @@
1
+ import { definePlugin, runWorker, } from "@paperclipai/plugin-sdk";
2
+ import { createHmac, timingSafeEqual } from "crypto";
3
+ async function githubRequest(ctx, config, method, path, body) {
4
+ const token = await ctx.secrets.resolve(config.githubTokenRef);
5
+ return ctx.http.fetch(`https://api.github.com${path}`, {
6
+ method,
7
+ headers: {
8
+ Accept: "application/vnd.github.v3+json",
9
+ Authorization: `Bearer ${token}`,
10
+ "Content-Type": "application/json",
11
+ "User-Agent": "paperclip-plugin-github",
12
+ },
13
+ body: body ? JSON.stringify(body) : undefined,
14
+ });
15
+ }
16
+ function verifyGitHubSignature(secret, rawBody, signatureHeader) {
17
+ const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
18
+ try {
19
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ let _config = null;
26
+ let _ctx = null;
27
+ const plugin = definePlugin({
28
+ async setup(ctx) {
29
+ const config = (await ctx.config.get());
30
+ _config = config;
31
+ _ctx = ctx;
32
+ const triggerLabel = config.triggerLabel ?? "paperclip";
33
+ // =========================================================
34
+ // OUTBOUND: Paperclip issue done → close GitHub issue
35
+ // =========================================================
36
+ if (config.syncClosedBack !== false) {
37
+ ctx.events.on("issue.updated", async (event) => {
38
+ const payload = event.payload;
39
+ const changes = payload?.changes;
40
+ const statusChange = changes?.status;
41
+ const newStatus = statusChange?.to ?? payload?.status;
42
+ if (newStatus !== "done")
43
+ return;
44
+ // Check if this issue originated from GitHub (originId format: "github:<owner>/<repo>#<number>")
45
+ const originId = payload?.originId;
46
+ if (!originId?.startsWith("github:"))
47
+ return;
48
+ const match = originId.match(/^github:(.+)#(\d+)$/);
49
+ if (!match)
50
+ return;
51
+ const [, repoFullName, issueNumber] = match;
52
+ // Close the GitHub issue
53
+ const response = await githubRequest(ctx, config, "PATCH", `/repos/${repoFullName}/issues/${issueNumber}`, {
54
+ state: "closed",
55
+ state_reason: "completed",
56
+ });
57
+ if (response.ok) {
58
+ // Add a comment
59
+ await githubRequest(ctx, config, "POST", `/repos/${repoFullName}/issues/${issueNumber}/comments`, {
60
+ body: `Resolved by Paperclip agent. Issue: ${payload?.issueTag ?? event.entityId}`,
61
+ });
62
+ ctx.logger.info(`Closed GitHub issue ${repoFullName}#${issueNumber}`);
63
+ }
64
+ else {
65
+ ctx.logger.error(`Failed to close GitHub issue: ${response.status}`);
66
+ }
67
+ });
68
+ }
69
+ ctx.logger.info("GitHub plugin initialized", { triggerLabel });
70
+ },
71
+ // =========================================================
72
+ // INBOUND: GitHub webhook → create Paperclip issue
73
+ // =========================================================
74
+ async onWebhook(input) {
75
+ if (input.endpointKey !== "github-events")
76
+ return;
77
+ const ctx = _ctx;
78
+ const config = _config;
79
+ // Verify GitHub signature (X-Hub-Signature-256)
80
+ const signatureHeader = Array.isArray(input.headers["x-hub-signature-256"])
81
+ ? input.headers["x-hub-signature-256"][0]
82
+ : input.headers["x-hub-signature-256"];
83
+ if (!signatureHeader) {
84
+ ctx.logger.warn("GitHub webhook missing signature header");
85
+ return;
86
+ }
87
+ const secret = await ctx.secrets.resolve(config.webhookSecretRef);
88
+ if (!verifyGitHubSignature(secret, input.rawBody, signatureHeader)) {
89
+ ctx.logger.warn("GitHub webhook signature verification failed");
90
+ return;
91
+ }
92
+ // Check event type
93
+ const eventType = Array.isArray(input.headers["x-github-event"])
94
+ ? input.headers["x-github-event"][0]
95
+ : input.headers["x-github-event"];
96
+ if (eventType !== "issues") {
97
+ ctx.logger.info(`Ignoring GitHub event type: ${eventType}`);
98
+ return;
99
+ }
100
+ const event = input.parsedBody;
101
+ // Only process opened/labeled events
102
+ if (event.action !== "opened" && event.action !== "labeled")
103
+ return;
104
+ // Check for trigger label
105
+ const triggerLabel = config.triggerLabel ?? "paperclip";
106
+ const hasLabel = event.issue.labels.some((l) => l.name === triggerLabel);
107
+ if (!hasLabel)
108
+ return;
109
+ const repo = event.repository.full_name;
110
+ const num = event.issue.number;
111
+ const originId = `github:${repo}#${num}`;
112
+ // Create Paperclip issue
113
+ const description = [
114
+ `## GitHub Issue`,
115
+ ``,
116
+ `**Repo:** [${repo}](${event.repository.html_url})`,
117
+ `**Issue:** [#${num}](${event.issue.html_url})`,
118
+ `**Author:** ${event.issue.user.login}`,
119
+ `**Labels:** ${event.issue.labels.map((l) => l.name).join(", ")}`,
120
+ ``,
121
+ `### Description`,
122
+ event.issue.body || "_No description_",
123
+ ].join("\n");
124
+ await ctx.issues.create({
125
+ companyId: config.targetCompanyId,
126
+ title: `[${repo}#${num}] ${event.issue.title}`,
127
+ description,
128
+ status: "todo",
129
+ priority: "medium",
130
+ assigneeAgentId: config.defaultAgentId,
131
+ originKind: "routine_execution",
132
+ originId,
133
+ });
134
+ ctx.logger.info(`Created Paperclip issue for ${repo}#${num}`);
135
+ },
136
+ async onHealth() {
137
+ return { status: "ok" };
138
+ },
139
+ async onShutdown() {
140
+ _config = null;
141
+ _ctx = null;
142
+ },
143
+ });
144
+ export default plugin;
145
+ runWorker(plugin, import.meta.url);
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@bettsnation/paperclip-plugin-github",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Paperclip plugin for bidirectional GitHub Issues sync — webhook intake and status sync",
6
+ "paperclipPlugin": {
7
+ "manifest": "./dist/manifest.js",
8
+ "worker": "./dist/worker.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "peerDependencies": {
15
+ "@paperclipai/plugin-sdk": ">=0.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "@paperclipai/plugin-sdk": "^2026.403.0",
19
+ "@types/node": "^22.0.0",
20
+ "typescript": "^5.7.0"
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "README.md"
25
+ ],
26
+ "license": "MIT",
27
+ "author": "BettsNation",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/bettsnation/paperclip-plugin-github.git"
31
+ }
32
+ }