@mastra/github-signals 0.2.2 → 0.2.3-alpha.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/index.cjs CHANGED
@@ -1,483 +1,601 @@
1
- 'use strict';
2
-
3
- var crypto = require('crypto');
4
- var promises = require('fs/promises');
5
- var os = require('os');
6
- var path = require('path');
7
- var util = require('util');
8
- var signals = require('@mastra/core/signals');
9
- var tools = require('@mastra/core/tools');
10
- var z = require('zod');
11
-
12
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
-
14
- var z__default = /*#__PURE__*/_interopDefault(z);
15
-
16
- // src/index.ts
17
- var _execFileAsync;
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let crypto = require("crypto");
25
+ let fs_promises = require("fs/promises");
26
+ let os = require("os");
27
+ let path = require("path");
28
+ let util = require("util");
29
+ let _mastra_core_signals = require("@mastra/core/signals");
30
+ let _mastra_core_tools = require("@mastra/core/tools");
31
+ let zod = require("zod");
32
+ zod = __toESM(zod, 1);
33
+ //#region src/index.ts
34
+ let _execFileAsync;
18
35
  async function execFileAsync(file, args, options) {
19
- if (!_execFileAsync) {
20
- const cp = await import('child_process');
21
- _execFileAsync = util.promisify(cp.execFile);
22
- }
23
- return _execFileAsync(file, args, options);
36
+ if (!_execFileAsync) _execFileAsync = (0, util.promisify)((await import("child_process")).execFile);
37
+ return _execFileAsync(file, args, options);
24
38
  }
25
- var GITHUB_SUBSCRIBE_PR_TAG = "github-subscribe-pr";
26
- var GITHUB_UNSUBSCRIBE_PR_TAG = "github-unsubscribe-pr";
27
- var GITHUB_SYNC_STATUS_TAG = "github-sync-status";
28
- var GITHUB_SIGNALS_METADATA_KEY = "githubSignals";
29
- var DEFAULT_AUTHORIZED_PERMISSIONS = ["admin", "maintain", "write"];
30
- var DEFAULT_AUTHORIZED_BOTS = ["coderabbitai[bot]", "devin-ai-integration[bot]"];
31
- var PERMISSION_CACHE_TTL_MS = 5 * 60 * 1e3;
32
- var AUTHOR_GATED_NOTIFICATION_KINDS = /* @__PURE__ */ new Set(["pull-request-activity", "pull-request-review-activity"]);
33
- var createGithubTool = tools.createTool;
39
+ const GITHUB_SUBSCRIBE_PR_TAG = "github-subscribe-pr";
40
+ const GITHUB_UNSUBSCRIBE_PR_TAG = "github-unsubscribe-pr";
41
+ const GITHUB_SYNC_STATUS_TAG = "github-sync-status";
42
+ const GITHUB_SIGNALS_METADATA_KEY = "githubSignals";
43
+ const DEFAULT_AUTHORIZED_PERMISSIONS = [
44
+ "admin",
45
+ "maintain",
46
+ "write"
47
+ ];
48
+ const DEFAULT_AUTHORIZED_BOTS = ["coderabbitai[bot]", "devin-ai-integration[bot]"];
49
+ const PERMISSION_CACHE_TTL_MS = 300 * 1e3;
50
+ /** Notification kinds driven by comment/review activity that should be gated by author permission. */
51
+ const AUTHOR_GATED_NOTIFICATION_KINDS = /* @__PURE__ */ new Set(["pull-request-activity", "pull-request-review-activity"]);
52
+ const createGithubTool = _mastra_core_tools.createTool;
34
53
  function isPlainObject(value) {
35
- return typeof value === "object" && value !== null && !Array.isArray(value);
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36
55
  }
37
56
  function readString(value) {
38
- return typeof value === "string" && value.length > 0 ? value : void 0;
57
+ return typeof value === "string" && value.length > 0 ? value : void 0;
39
58
  }
40
59
  function readNumber(value) {
41
- if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
42
- if (typeof value === "string" && /^\d+$/.test(value)) return Number(value);
43
- return void 0;
60
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
61
+ if (typeof value === "string" && /^\d+$/.test(value)) return Number(value);
44
62
  }
45
63
  function stableJson(value) {
46
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
47
- if (isPlainObject(value)) {
48
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
49
- }
50
- return JSON.stringify(value);
64
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
65
+ if (isPlainObject(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
66
+ return JSON.stringify(value);
51
67
  }
52
68
  function snapshotHash(value) {
53
- return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
69
+ return (0, crypto.createHash)("sha256").update(stableJson(value)).digest("hex");
54
70
  }
55
71
  function resolveHomePath(path$1) {
56
- return path$1.startsWith("~/") ? path.join(os.homedir(), path$1.slice(2)) : path$1;
72
+ return path$1.startsWith("~/") ? (0, path.join)((0, os.homedir)(), path$1.slice(2)) : path$1;
73
+ }
74
+ async function readDbPathFromGitcrawlConfig(configPath) {
75
+ try {
76
+ const config = await (0, fs_promises.readFile)(resolveHomePath(configPath), "utf8");
77
+ const match = /^\s*db_path\s*=\s*['\"]([^'\"]+)['\"]/m.exec(config);
78
+ if (match?.[1]) return resolveHomePath(match[1]);
79
+ } catch {}
57
80
  }
58
- async function getGitcrawlDbPath() {
59
- if (process.env.GITCRAWL_DB_PATH) return resolveHomePath(process.env.GITCRAWL_DB_PATH);
60
- const configPath = process.env.GITCRAWL_CONFIG_PATH ?? path.join(os.homedir(), ".config", "gitcrawl", "config.toml");
61
- try {
62
- const config = await promises.readFile(resolveHomePath(configPath), "utf8");
63
- const match = /^\s*db_path\s*=\s*['\"]([^'\"]+)['\"]/m.exec(config);
64
- if (match?.[1]) return resolveHomePath(match[1]);
65
- } catch {
66
- }
67
- return path.join(os.homedir(), ".config", "gitcrawl", "gitcrawl.db");
81
+ async function fileExists(path$2) {
82
+ try {
83
+ await (0, fs_promises.stat)(path$2);
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
68
88
  }
69
- async function queryGitcrawlDb(sql) {
70
- const dbPath = await getGitcrawlDbPath();
71
- const { stdout } = await execFileAsync("sqlite3", ["-json", dbPath, sql], { maxBuffer: 10 * 1024 * 1024 });
72
- return JSON.parse(stdout || "[]");
89
+ /**
90
+ * Directories gitcrawl uses for its config and database by default. Linux
91
+ * uses the XDG path, but macOS uses ~/Library/Application Support, so probing
92
+ * only ~/.config silently misses the database on macOS.
93
+ */
94
+ function gitcrawlDefaultDirs() {
95
+ return [(0, path.join)((0, os.homedir)(), ".config", "gitcrawl"), ...process.platform === "darwin" ? [(0, path.join)((0, os.homedir)(), "Library", "Application Support", "gitcrawl")] : []];
73
96
  }
74
97
  function sqlString(value) {
75
- return `'${value.replaceAll("'", "''")}'`;
98
+ return `'${value.replaceAll("'", "''")}'`;
76
99
  }
77
100
  function getSignalMetadata(message) {
78
- if (message.role !== "signal") return void 0;
79
- const signal = message.content.metadata?.signal;
80
- return isPlainObject(signal) ? signal : void 0;
101
+ if (message.role !== "signal") return void 0;
102
+ const signal = message.content.metadata?.signal;
103
+ return isPlainObject(signal) ? signal : void 0;
81
104
  }
82
105
  function getGithubMetadata(threadMetadata) {
83
- const mastra = isPlainObject(threadMetadata?.mastra) ? threadMetadata.mastra : {};
84
- const githubSignals = isPlainObject(mastra[GITHUB_SIGNALS_METADATA_KEY]) ? mastra[GITHUB_SIGNALS_METADATA_KEY] : {};
85
- const rawSubscriptions = Array.isArray(githubSignals.subscriptions) ? githubSignals.subscriptions : [];
86
- const subscriptions = [];
87
- for (const rawSubscription of rawSubscriptions) {
88
- if (!isPlainObject(rawSubscription)) continue;
89
- const owner = readString(rawSubscription.owner);
90
- const repo = readString(rawSubscription.repo);
91
- const number = readNumber(rawSubscription.number);
92
- const subscribedAt = readString(rawSubscription.subscribedAt);
93
- const updatedAt = readString(rawSubscription.updatedAt);
94
- const lastSubscribeSignalId = readString(rawSubscription.lastSubscribeSignalId);
95
- if (!owner || !repo || !number || !subscribedAt || !updatedAt || !lastSubscribeSignalId) continue;
96
- subscriptions.push({
97
- owner,
98
- repo,
99
- number,
100
- subscribedAt,
101
- updatedAt,
102
- lastSubscribeSignalId,
103
- ...readString(rawSubscription.lastSyncAt) ? { lastSyncAt: readString(rawSubscription.lastSyncAt) } : {},
104
- ...rawSubscription.lastSyncStatus === "success" || rawSubscription.lastSyncStatus === "error" || rawSubscription.lastSyncStatus === "skipped" ? { lastSyncStatus: rawSubscription.lastSyncStatus } : {},
105
- ...readString(rawSubscription.lastSyncError) ? { lastSyncError: readString(rawSubscription.lastSyncError) } : {},
106
- ...readString(rawSubscription.lastObservedGithubUpdatedAt) ? { lastObservedGithubUpdatedAt: readString(rawSubscription.lastObservedGithubUpdatedAt) } : {},
107
- ...readString(rawSubscription.lastObservedContentHash) ? { lastObservedContentHash: readString(rawSubscription.lastObservedContentHash) } : {},
108
- ...readString(rawSubscription.lastObservedThreadContentHash) ? { lastObservedThreadContentHash: readString(rawSubscription.lastObservedThreadContentHash) } : {},
109
- ...readString(rawSubscription.lastObservedHeadSha) ? { lastObservedHeadSha: readString(rawSubscription.lastObservedHeadSha) } : {},
110
- ...readString(rawSubscription.lastObservedState) ? { lastObservedState: readString(rawSubscription.lastObservedState) } : {},
111
- ...readString(rawSubscription.lastObservedMergeableState) ? { lastObservedMergeableState: readString(rawSubscription.lastObservedMergeableState) } : {},
112
- ...readString(rawSubscription.lastObservedCiState) ? { lastObservedCiState: readString(rawSubscription.lastObservedCiState) } : {},
113
- ...readString(rawSubscription.lastObservedReviewStateHash) ? { lastObservedReviewStateHash: readString(rawSubscription.lastObservedReviewStateHash) } : {},
114
- ...readString(rawSubscription.lastNotificationAt) ? { lastNotificationAt: readString(rawSubscription.lastNotificationAt) } : {},
115
- ...readString(rawSubscription.lastNotificationKind) ? { lastNotificationKind: readString(rawSubscription.lastNotificationKind) } : {},
116
- ...rawSubscription.lastNotificationPriority === "medium" || rawSubscription.lastNotificationPriority === "high" ? { lastNotificationPriority: rawSubscription.lastNotificationPriority } : {},
117
- ...readString(rawSubscription.lastNotificationSummary) ? { lastNotificationSummary: readString(rawSubscription.lastNotificationSummary) } : {}
118
- });
119
- }
120
- return {
121
- subscriptions,
122
- ...githubSignals.subscriptionHintShown === true ? { subscriptionHintShown: true } : {}
123
- };
106
+ const mastra = isPlainObject(threadMetadata?.mastra) ? threadMetadata.mastra : {};
107
+ const githubSignals = isPlainObject(mastra["githubSignals"]) ? mastra[GITHUB_SIGNALS_METADATA_KEY] : {};
108
+ const rawSubscriptions = Array.isArray(githubSignals.subscriptions) ? githubSignals.subscriptions : [];
109
+ const subscriptions = [];
110
+ for (const rawSubscription of rawSubscriptions) {
111
+ if (!isPlainObject(rawSubscription)) continue;
112
+ const owner = readString(rawSubscription.owner);
113
+ const repo = readString(rawSubscription.repo);
114
+ const number = readNumber(rawSubscription.number);
115
+ const subscribedAt = readString(rawSubscription.subscribedAt);
116
+ const updatedAt = readString(rawSubscription.updatedAt);
117
+ const lastSubscribeSignalId = readString(rawSubscription.lastSubscribeSignalId);
118
+ if (!owner || !repo || !number || !subscribedAt || !updatedAt || !lastSubscribeSignalId) continue;
119
+ subscriptions.push({
120
+ owner,
121
+ repo,
122
+ number,
123
+ subscribedAt,
124
+ updatedAt,
125
+ lastSubscribeSignalId,
126
+ ...readString(rawSubscription.lastSyncAt) ? { lastSyncAt: readString(rawSubscription.lastSyncAt) } : {},
127
+ ...rawSubscription.lastSyncStatus === "success" || rawSubscription.lastSyncStatus === "error" || rawSubscription.lastSyncStatus === "skipped" ? { lastSyncStatus: rawSubscription.lastSyncStatus } : {},
128
+ ...readString(rawSubscription.lastSyncError) ? { lastSyncError: readString(rawSubscription.lastSyncError) } : {},
129
+ ...readString(rawSubscription.lastSnapshotError) ? { lastSnapshotError: readString(rawSubscription.lastSnapshotError) } : {},
130
+ ...readString(rawSubscription.lastObservedGithubUpdatedAt) ? { lastObservedGithubUpdatedAt: readString(rawSubscription.lastObservedGithubUpdatedAt) } : {},
131
+ ...readString(rawSubscription.lastObservedContentHash) ? { lastObservedContentHash: readString(rawSubscription.lastObservedContentHash) } : {},
132
+ ...readString(rawSubscription.lastObservedThreadContentHash) ? { lastObservedThreadContentHash: readString(rawSubscription.lastObservedThreadContentHash) } : {},
133
+ ...readString(rawSubscription.lastObservedHeadSha) ? { lastObservedHeadSha: readString(rawSubscription.lastObservedHeadSha) } : {},
134
+ ...readString(rawSubscription.lastObservedState) ? { lastObservedState: readString(rawSubscription.lastObservedState) } : {},
135
+ ...readString(rawSubscription.lastObservedMergeableState) ? { lastObservedMergeableState: readString(rawSubscription.lastObservedMergeableState) } : {},
136
+ ...readString(rawSubscription.lastObservedCiState) ? { lastObservedCiState: readString(rawSubscription.lastObservedCiState) } : {},
137
+ ...readString(rawSubscription.lastObservedReviewStateHash) ? { lastObservedReviewStateHash: readString(rawSubscription.lastObservedReviewStateHash) } : {},
138
+ ...readString(rawSubscription.lastNotificationAt) ? { lastNotificationAt: readString(rawSubscription.lastNotificationAt) } : {},
139
+ ...readString(rawSubscription.lastNotificationKind) ? { lastNotificationKind: readString(rawSubscription.lastNotificationKind) } : {},
140
+ ...rawSubscription.lastNotificationPriority === "medium" || rawSubscription.lastNotificationPriority === "high" ? { lastNotificationPriority: rawSubscription.lastNotificationPriority } : {},
141
+ ...readString(rawSubscription.lastNotificationSummary) ? { lastNotificationSummary: readString(rawSubscription.lastNotificationSummary) } : {}
142
+ });
143
+ }
144
+ return {
145
+ subscriptions,
146
+ ...githubSignals.subscriptionHintShown === true ? { subscriptionHintShown: true } : {}
147
+ };
124
148
  }
125
149
  function setGithubMetadata(threadMetadata, githubSignals) {
126
- const existing = threadMetadata ?? {};
127
- const mastra = isPlainObject(existing.mastra) ? existing.mastra : {};
128
- const existingGithubSignals = isPlainObject(mastra[GITHUB_SIGNALS_METADATA_KEY]) ? mastra[GITHUB_SIGNALS_METADATA_KEY] : {};
129
- return {
130
- ...existing,
131
- mastra: {
132
- ...mastra,
133
- [GITHUB_SIGNALS_METADATA_KEY]: {
134
- ...existingGithubSignals,
135
- ...githubSignals
136
- }
137
- }
138
- };
150
+ const existing = threadMetadata ?? {};
151
+ const mastra = isPlainObject(existing.mastra) ? existing.mastra : {};
152
+ const existingGithubSignals = isPlainObject(mastra["githubSignals"]) ? mastra[GITHUB_SIGNALS_METADATA_KEY] : {};
153
+ return {
154
+ ...existing,
155
+ mastra: {
156
+ ...mastra,
157
+ [GITHUB_SIGNALS_METADATA_KEY]: {
158
+ ...existingGithubSignals,
159
+ ...githubSignals
160
+ }
161
+ }
162
+ };
139
163
  }
140
164
  function getFailingChecks(snapshot) {
141
- return (snapshot.checks ?? []).filter((check) => check.conclusion === "failure" || check.conclusion === "timed_out");
165
+ return (snapshot.checks ?? []).filter((check) => check.conclusion === "failure" || check.conclusion === "timed_out");
142
166
  }
143
167
  function getPendingChecks(snapshot) {
144
- return (snapshot.checks ?? []).filter((check) => check.status && check.status !== "completed");
168
+ return (snapshot.checks ?? []).filter((check) => check.status && check.status !== "completed");
145
169
  }
146
170
  function getPrLabel(subscription, snapshot) {
147
- const pr = `${subscription.owner}/${subscription.repo}#${subscription.number}`;
148
- return snapshot?.title ? `${pr}: ${snapshot.title}` : pr;
171
+ const pr = `${subscription.owner}/${subscription.repo}#${subscription.number}`;
172
+ return snapshot?.title ? `${pr}: ${snapshot.title}` : pr;
149
173
  }
150
174
  function getMergedNotificationSummary(label) {
151
- return `${label} was merged. This thread has been automatically unsubscribed from this PR. Resubscribe if you still need updates.`;
175
+ return `${label} was merged. This thread has been automatically unsubscribed from this PR. Resubscribe if you still need updates.`;
152
176
  }
177
+ /**
178
+ * Removes a hidden block (its delimiters *and* its content) starting at every `open` marker.
179
+ *
180
+ * For each `open` occurrence the whole region up to and including the matching `close` is dropped.
181
+ * When `close` is missing the block is treated as unterminated and removed through end-of-string,
182
+ * so large payloads can't survive by omitting their closing marker. Matching is case-insensitive on
183
+ * the markers and uses plain `indexOf` scanning, so there is no regex backtracking (ReDoS-safe).
184
+ */
153
185
  function stripBlocks(text, open, close) {
154
- const haystack = text.toLowerCase();
155
- const openLower = open.toLowerCase();
156
- const closeLower = close.toLowerCase();
157
- let result = "";
158
- let cursor = 0;
159
- for (; ; ) {
160
- const start = haystack.indexOf(openLower, cursor);
161
- if (start === -1) {
162
- result += text.slice(cursor);
163
- return result;
164
- }
165
- result += text.slice(cursor, start);
166
- const end = haystack.indexOf(closeLower, start + openLower.length);
167
- if (end === -1) return result;
168
- cursor = end + closeLower.length;
169
- }
186
+ const haystack = text.toLowerCase();
187
+ const openLower = open.toLowerCase();
188
+ const closeLower = close.toLowerCase();
189
+ let result = "";
190
+ let cursor = 0;
191
+ for (;;) {
192
+ const start = haystack.indexOf(openLower, cursor);
193
+ if (start === -1) {
194
+ result += text.slice(cursor);
195
+ return result;
196
+ }
197
+ result += text.slice(cursor, start);
198
+ const end = haystack.indexOf(closeLower, start + openLower.length);
199
+ if (end === -1) return result;
200
+ cursor = end + closeLower.length;
201
+ }
170
202
  }
171
- var CODE_TOKEN_PREFIX = "\0CODE";
172
- var CODE_TOKEN_SUFFIX = "\0";
203
+ /** Sentinel marker wrapping a stashed Markdown code region; `\u0000` cannot appear in GitHub text. */
204
+ const CODE_TOKEN_PREFIX = "\0CODE";
205
+ const CODE_TOKEN_SUFFIX = "\0";
206
+ /**
207
+ * Temporarily removes Markdown code spans and fenced code blocks so tag stripping can't damage
208
+ * human-authored code examples.
209
+ *
210
+ * GitHub renders Markdown, so legitimate code like `` `<Component>` ``, generic type examples, or
211
+ * fenced JSX/TSX must survive sanitization. Each code region is replaced with an opaque token and
212
+ * pushed onto a stash; {@link restore} swaps the tokens back after the surrounding prose has been
213
+ * stripped of markup. Fenced blocks are matched before inline spans so backtick runs inside a fence
214
+ * are not mistaken for inline code.
215
+ */
173
216
  function preserveMarkdownCode(text) {
174
- const preserved = [];
175
- const stash = (match) => {
176
- const token = `${CODE_TOKEN_PREFIX}${preserved.length}${CODE_TOKEN_SUFFIX}`;
177
- preserved.push(match);
178
- return token;
179
- };
180
- const protectedText = text.replace(/```[\s\S]*?```/g, stash).replace(/(`{2,})(?!`)[\s\S]*?[^`]\1(?!`)/g, stash).replace(/`[^`\n]*`/g, stash);
181
- return {
182
- text: protectedText,
183
- restore: (sanitized) => sanitized.replace(/\u0000CODE(\d+)\u0000/g, (_, index) => preserved[Number(index)] ?? "")
184
- };
217
+ const preserved = [];
218
+ const stash = (match) => {
219
+ const token = `${CODE_TOKEN_PREFIX}${preserved.length}${CODE_TOKEN_SUFFIX}`;
220
+ preserved.push(match);
221
+ return token;
222
+ };
223
+ return {
224
+ text: text.replace(/```[\s\S]*?```/g, stash).replace(/(`{2,})(?!`)[\s\S]*?[^`]\1(?!`)/g, stash).replace(/`[^`\n]*`/g, stash),
225
+ restore: (sanitized) => sanitized.replace(/\u0000CODE(\d+)\u0000/g, (_, index) => preserved[Number(index)] ?? "")
226
+ };
185
227
  }
228
+ /**
229
+ * Removes XML/HTML-like markup — and the content it hides — from a PR comment body, leaving only
230
+ * human-readable text while preserving Markdown code examples.
231
+ *
232
+ * Review bots (e.g. CodeRabbit) embed large machine-only payloads in comments: base64 state blobs
233
+ * inside `<!-- ... -->` comments (often >100KB) and verbose collapsed `<details>` sections. Rather
234
+ * than targeting specific bot markers, we strip hidden blocks and tags generically, since none of
235
+ * that markup is useful to downstream consumers and persisting it balloons notification payloads and
236
+ * can overflow agent context windows.
237
+ *
238
+ * Markdown code spans/fenced blocks are stashed first and restored last, so legitimate code such as
239
+ * `` `<Component>` `` or fenced JSX is kept intact while bot markup elsewhere is removed. Block
240
+ * removal (comments, `<details>`) drops the *entire* section including its inner content, and any
241
+ * unterminated block is removed through end-of-string so a missing closing marker can't smuggle the
242
+ * payload through. All scanning is `indexOf`-based and the only regex used is a non-backtracking
243
+ * single-tag matcher, so adversarial input cannot trigger catastrophic backtracking (ReDoS). Any
244
+ * unterminated markup fragment (e.g. a dangling `<script` with no `>`) is dropped through end-of-
245
+ * string, and finally every remaining lone `<` is removed so no partial markup survives — while
246
+ * ordinary prose like `coverage < 80%` keeps its text intact.
247
+ */
186
248
  function sanitizeCommentText(body) {
187
- const { text: protectedBody, restore } = preserveMarkdownCode(body);
188
- let text = stripBlocks(protectedBody, "<!--", "-->");
189
- text = stripBlocks(text, "<details", "</details>");
190
- const stripped = text.replace(/<\/?[a-zA-Z][^<>]*>/g, "").replace(/<[!/a-zA-Z][\s\S]*$/g, "").replace(/</g, "").replace(/\n{3,}/g, "\n\n").trim();
191
- return restore(stripped);
249
+ const { text: protectedBody, restore } = preserveMarkdownCode(body);
250
+ let text = stripBlocks(protectedBody, "<!--", "-->");
251
+ text = stripBlocks(text, "<details", "</details>");
252
+ return restore(text.replace(/<\/?[a-zA-Z][^<>]*>/g, "").replace(/<[!/a-zA-Z][\s\S]*$/g, "").replace(/</g, "").replace(/\n{3,}/g, "\n\n").trim());
192
253
  }
254
+ /** Applies {@link sanitizeCommentText} to an optional comment body, preserving `undefined`. */
193
255
  function sanitizeCommentBody(body) {
194
- if (body === void 0) return void 0;
195
- const sanitized = sanitizeCommentText(body);
196
- return sanitized.length > 0 ? sanitized : void 0;
256
+ if (body === void 0) return void 0;
257
+ const sanitized = sanitizeCommentText(body);
258
+ return sanitized.length > 0 ? sanitized : void 0;
197
259
  }
198
260
  function getCommentExcerpt(body) {
199
- const excerpt = sanitizeCommentText(body).replace(/\s+/g, " ").trim();
200
- return excerpt.length > 240 ? `${excerpt.slice(0, 237)}...` : excerpt;
261
+ const excerpt = sanitizeCommentText(body).replace(/\s+/g, " ").trim();
262
+ return excerpt.length > 240 ? `${excerpt.slice(0, 237)}...` : excerpt;
201
263
  }
202
264
  function getCommentNotificationSummary(pr, snapshot) {
203
- if (!snapshot.latestCommentAuthor || !snapshot.latestCommentBody) return void 0;
204
- return `${snapshot.latestCommentAuthor} commented on ${pr}: ${getCommentExcerpt(snapshot.latestCommentBody)}`;
265
+ if (!snapshot.latestCommentAuthor || !snapshot.latestCommentBody) return void 0;
266
+ return `${snapshot.latestCommentAuthor} commented on ${pr}: ${getCommentExcerpt(snapshot.latestCommentBody)}`;
205
267
  }
206
- var githubActivityNotificationPriority = {
207
- high: 0,
208
- medium: 1
268
+ const githubActivityNotificationPriority = {
269
+ high: 0,
270
+ medium: 1
209
271
  };
210
272
  function getGithubActivityNotificationRank(notification) {
211
- return notification.kind === "pull-request-activity" ? 0 : 1;
273
+ return notification.kind === "pull-request-activity" ? 0 : 1;
212
274
  }
213
275
  function compareGithubActivityNotifications(a, b) {
214
- if (!a && !b) return 0;
215
- if (!a) return 1;
216
- if (!b) return -1;
217
- const priorityComparison = githubActivityNotificationPriority[a.priority] - githubActivityNotificationPriority[b.priority];
218
- if (priorityComparison !== 0) return priorityComparison;
219
- return getGithubActivityNotificationRank(a) - getGithubActivityNotificationRank(b);
276
+ if (!a && !b) return 0;
277
+ if (!a) return 1;
278
+ if (!b) return -1;
279
+ const priorityComparison = githubActivityNotificationPriority[a.priority] - githubActivityNotificationPriority[b.priority];
280
+ if (priorityComparison !== 0) return priorityComparison;
281
+ return getGithubActivityNotificationRank(a) - getGithubActivityNotificationRank(b);
220
282
  }
221
283
  function classifyGithubCommentActivityNotification(input) {
222
- if (isBotOnlyActivity(input.snapshot)) return void 0;
223
- const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
224
- const summary = getCommentNotificationSummary(pr, input.snapshot);
225
- if (!summary) return void 0;
226
- return { kind: "pull-request-activity", priority: "high", summary };
284
+ if (isBotOnlyActivity(input.snapshot)) return void 0;
285
+ const summary = getCommentNotificationSummary(`${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`, input.snapshot);
286
+ if (!summary) return void 0;
287
+ return {
288
+ kind: "pull-request-activity",
289
+ priority: "high",
290
+ summary
291
+ };
227
292
  }
228
293
  function getCheckUpdatedTime(check) {
229
- const value = check.updatedAt ? Date.parse(check.updatedAt) : Number.NaN;
230
- return Number.isFinite(value) ? value : 0;
294
+ const value = check.updatedAt ? Date.parse(check.updatedAt) : NaN;
295
+ return Number.isFinite(value) ? value : 0;
231
296
  }
232
297
  function getCheckKey(check) {
233
- return `${check.name || "check"}:${check.detailsUrl || check.workflowName || ""}`;
298
+ return `${check.name || "check"}:${check.detailsUrl || check.workflowName || ""}`;
234
299
  }
235
300
  function normalizeGithubChecksForSnapshot(input) {
236
- const latestCheckUpdatedAt = input.checkRows.reduce(
237
- (latest, check) => Math.max(latest, getCheckUpdatedTime(check)),
238
- 0
239
- );
240
- const rows = [
241
- ...input.checkRows,
242
- ...input.workflowRows.filter(
243
- (workflow) => input.checkRows.length === 0 || getCheckUpdatedTime(workflow) >= latestCheckUpdatedAt
244
- )
245
- ];
246
- const byKey = /* @__PURE__ */ new Map();
247
- for (const row of rows) {
248
- const key = getCheckKey(row);
249
- const existing = byKey.get(key);
250
- if (!existing) {
251
- byKey.set(key, row);
252
- continue;
253
- }
254
- const rowTime = getCheckUpdatedTime(row);
255
- const existingTime = getCheckUpdatedTime(existing);
256
- if (rowTime > existingTime || rowTime === existingTime && existing.source === "workflow" && row.source === "check") {
257
- byKey.set(key, row);
258
- }
259
- }
260
- return [...byKey.values()].map(({ source: _source, ...check }) => check).sort((a, b) => `${a.name}:${a.detailsUrl ?? ""}`.localeCompare(`${b.name}:${b.detailsUrl ?? ""}`));
301
+ const latestCheckUpdatedAt = input.checkRows.reduce((latest, check) => Math.max(latest, getCheckUpdatedTime(check)), 0);
302
+ const rows = [...input.checkRows, ...input.workflowRows.filter((workflow) => input.checkRows.length === 0 || getCheckUpdatedTime(workflow) >= latestCheckUpdatedAt)];
303
+ const byKey = /* @__PURE__ */ new Map();
304
+ for (const row of rows) {
305
+ const key = getCheckKey(row);
306
+ const existing = byKey.get(key);
307
+ if (!existing) {
308
+ byKey.set(key, row);
309
+ continue;
310
+ }
311
+ const rowTime = getCheckUpdatedTime(row);
312
+ const existingTime = getCheckUpdatedTime(existing);
313
+ if (rowTime > existingTime || rowTime === existingTime && existing.source === "workflow" && row.source === "check") byKey.set(key, row);
314
+ }
315
+ return [...byKey.values()].map(({ source: _source, ...check }) => check).sort((a, b) => `${a.name}:${a.detailsUrl ?? ""}`.localeCompare(`${b.name}:${b.detailsUrl ?? ""}`));
261
316
  }
262
317
  function isBotOnlyActivity(snapshot) {
263
- return snapshot.latestCommentIsBot === true && (!snapshot.ciState || snapshot.ciState === "unknown");
318
+ return snapshot.latestCommentIsBot === true && (!snapshot.ciState || snapshot.ciState === "unknown");
264
319
  }
265
320
  function stringifyEvidence(value) {
266
- if (typeof value === "string") return value;
267
- try {
268
- return JSON.stringify(value);
269
- } catch {
270
- return "";
271
- }
321
+ if (typeof value === "string") return value;
322
+ try {
323
+ return JSON.stringify(value);
324
+ } catch {
325
+ return "";
326
+ }
272
327
  }
273
328
  function detectPrWorkEvidence(input) {
274
- const evidence = [
275
- input.text ?? "",
276
- ...(input.toolCalls ?? []).map((toolCall) => `${toolCall.toolName} ${stringifyEvidence(toolCall.args)}`)
277
- ].join("\n");
278
- if (!evidence.trim()) return void 0;
279
- const url = /github\.com\/([^\s/#]+)\/([^\s/#]+)\/pull\/(\d+)/i.exec(evidence);
280
- if (url?.[1] && url[2] && url[3]) return { owner: url[1], repo: url[2], number: Number(url[3]) };
281
- const repoRef = /\b([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)\b/.exec(evidence);
282
- if (repoRef?.[1] && repoRef[2] && repoRef[3])
283
- return { owner: repoRef[1], repo: repoRef[2], number: Number(repoRef[3]) };
284
- const ghCommand = /\bgh\s+(?:pr\s+(?:view|checks|status|comment|diff|checkout)|run\s+(?:rerun|view))\b/i.test(
285
- evidence
286
- );
287
- if (!ghCommand) return void 0;
288
- const numberMatch = /(?:^|\s)#?(\d{2,})(?:\s|$)/.exec(evidence);
289
- return numberMatch?.[1] ? { number: Number(numberMatch[1]) } : void 0;
329
+ const evidence = [input.text ?? "", ...(input.toolCalls ?? []).map((toolCall) => `${toolCall.toolName} ${stringifyEvidence(toolCall.args)}`)].join("\n");
330
+ if (!evidence.trim()) return void 0;
331
+ const url = /github\.com\/([^\s/#]+)\/([^\s/#]+)\/pull\/(\d+)/i.exec(evidence);
332
+ if (url?.[1] && url[2] && url[3]) return {
333
+ owner: url[1],
334
+ repo: url[2],
335
+ number: Number(url[3])
336
+ };
337
+ const repoRef = /\b([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)\b/.exec(evidence);
338
+ if (repoRef?.[1] && repoRef[2] && repoRef[3]) return {
339
+ owner: repoRef[1],
340
+ repo: repoRef[2],
341
+ number: Number(repoRef[3])
342
+ };
343
+ if (!/\bgh\s+(?:pr\s+(?:view|checks|status|comment|diff|checkout)|run\s+(?:rerun|view))\b/i.test(evidence)) return void 0;
344
+ const numberMatch = /(?:^|\s)#?(\d{2,})(?:\s|$)/.exec(evidence);
345
+ return numberMatch?.[1] ? { number: Number(numberMatch[1]) } : void 0;
290
346
  }
291
347
  function classifyGithubActivityNotification(input) {
292
- const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
293
- const label = getPrLabel(input.subscription, input.snapshot);
294
- if (input.snapshot.state && input.subscription.lastObservedState !== input.snapshot.state) {
295
- if (input.snapshot.state === "merged")
296
- return {
297
- kind: "pull-request-merged",
298
- priority: "high",
299
- summary: getMergedNotificationSummary(label)
300
- };
301
- if (input.snapshot.state === "closed")
302
- return { kind: "pull-request-closed", priority: "high", summary: `${label} was closed` };
303
- if (input.subscription.lastObservedState && input.snapshot.state === "open")
304
- return { kind: "pull-request-reopened", priority: "medium", summary: `${label} was reopened` };
305
- }
306
- const failingChecks = getFailingChecks(input.snapshot);
307
- if (input.snapshot.ciState === "failure" && input.subscription.lastObservedCiState !== "failure") {
308
- const names = failingChecks.slice(0, 3).map((check) => check.name).join(", ");
309
- return {
310
- kind: "pull-request-ci-failure",
311
- priority: "high",
312
- summary: `${pr} has failing CI${names ? `: ${names}` : ""}`
313
- };
314
- }
315
- if (input.snapshot.mergeableState === "dirty" && input.subscription.lastObservedMergeableState !== "dirty") {
316
- return {
317
- kind: "pull-request-conflict",
318
- priority: "high",
319
- summary: `${pr} has merge conflicts${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
320
- };
321
- }
322
- if (input.snapshot.mergeableState && input.subscription.lastObservedMergeableState === "dirty" && input.snapshot.mergeableState !== "dirty") {
323
- return {
324
- kind: "pull-request-conflict-resolved",
325
- priority: "medium",
326
- summary: `${pr} merge conflicts were resolved`
327
- };
328
- }
329
- if (input.snapshot.mergeableState === "dirty") return void 0;
330
- if (input.snapshot.ciState === "success" && input.subscription.lastObservedCiState && input.subscription.lastObservedCiState !== "success") {
331
- return { kind: "pull-request-ci-recovered", priority: "medium", summary: `${pr} CI recovered` };
332
- }
333
- if (input.snapshot.reviewStateHash && input.subscription.lastObservedReviewStateHash && input.snapshot.reviewStateHash !== input.subscription.lastObservedReviewStateHash && (input.snapshot.unresolvedReviewThreads ?? 0) > 0) {
334
- return {
335
- kind: "pull-request-review-activity",
336
- priority: "medium",
337
- summary: `${pr} has ${input.snapshot.unresolvedReviewThreads} unresolved review thread${input.snapshot.unresolvedReviewThreads === 1 ? "" : "s"}`
338
- };
339
- }
340
- const pendingChecks = getPendingChecks(input.snapshot);
341
- if (input.snapshot.ciState === "pending" && input.subscription.lastObservedCiState !== "pending" && pendingChecks.length > 0) {
342
- const names = pendingChecks.slice(0, 3).map((check) => check.name).join(", ");
343
- return {
344
- kind: "pull-request-ci-pending",
345
- priority: "medium",
346
- summary: `${pr} has CI still running${names ? `: ${names}` : ""}`
347
- };
348
- }
349
- if (input.snapshot.ciState === "pending" && input.subscription.lastObservedCiState === "pending") return void 0;
350
- if (isBotOnlyActivity(input.snapshot)) return void 0;
351
- const commentSummary = getCommentNotificationSummary(pr, input.snapshot);
352
- return {
353
- kind: "pull-request-activity",
354
- priority: commentSummary ? "high" : "medium",
355
- summary: commentSummary ?? `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
356
- };
348
+ const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
349
+ const label = getPrLabel(input.subscription, input.snapshot);
350
+ if (input.snapshot.state && input.subscription.lastObservedState !== input.snapshot.state) {
351
+ if (input.snapshot.state === "merged") return {
352
+ kind: "pull-request-merged",
353
+ priority: "high",
354
+ summary: getMergedNotificationSummary(label)
355
+ };
356
+ if (input.snapshot.state === "closed") return {
357
+ kind: "pull-request-closed",
358
+ priority: "high",
359
+ summary: `${label} was closed`
360
+ };
361
+ if (input.subscription.lastObservedState && input.snapshot.state === "open") return {
362
+ kind: "pull-request-reopened",
363
+ priority: "medium",
364
+ summary: `${label} was reopened`
365
+ };
366
+ }
367
+ const failingChecks = getFailingChecks(input.snapshot);
368
+ if (input.snapshot.ciState === "failure" && input.subscription.lastObservedCiState !== "failure") {
369
+ const names = failingChecks.slice(0, 3).map((check) => check.name).join(", ");
370
+ return {
371
+ kind: "pull-request-ci-failure",
372
+ priority: "high",
373
+ summary: `${pr} has failing CI${names ? `: ${names}` : ""}`
374
+ };
375
+ }
376
+ if (input.snapshot.mergeableState === "dirty" && input.subscription.lastObservedMergeableState !== "dirty") return {
377
+ kind: "pull-request-conflict",
378
+ priority: "high",
379
+ summary: `${pr} has merge conflicts${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
380
+ };
381
+ if (input.snapshot.mergeableState && input.subscription.lastObservedMergeableState === "dirty" && input.snapshot.mergeableState !== "dirty") return {
382
+ kind: "pull-request-conflict-resolved",
383
+ priority: "medium",
384
+ summary: `${pr} merge conflicts were resolved`
385
+ };
386
+ if (input.snapshot.mergeableState === "dirty") return void 0;
387
+ if (input.snapshot.ciState === "success" && input.subscription.lastObservedCiState && input.subscription.lastObservedCiState !== "success") return {
388
+ kind: "pull-request-ci-recovered",
389
+ priority: "medium",
390
+ summary: `${pr} CI recovered`
391
+ };
392
+ if (input.snapshot.reviewStateHash && input.subscription.lastObservedReviewStateHash && input.snapshot.reviewStateHash !== input.subscription.lastObservedReviewStateHash && (input.snapshot.unresolvedReviewThreads ?? 0) > 0) return {
393
+ kind: "pull-request-review-activity",
394
+ priority: "medium",
395
+ summary: `${pr} has ${input.snapshot.unresolvedReviewThreads} unresolved review thread${input.snapshot.unresolvedReviewThreads === 1 ? "" : "s"}`
396
+ };
397
+ const pendingChecks = getPendingChecks(input.snapshot);
398
+ if (input.snapshot.ciState === "pending" && input.subscription.lastObservedCiState !== "pending" && pendingChecks.length > 0) {
399
+ const names = pendingChecks.slice(0, 3).map((check) => check.name).join(", ");
400
+ return {
401
+ kind: "pull-request-ci-pending",
402
+ priority: "medium",
403
+ summary: `${pr} has CI still running${names ? `: ${names}` : ""}`
404
+ };
405
+ }
406
+ if (input.snapshot.ciState === "pending" && input.subscription.lastObservedCiState === "pending") return void 0;
407
+ if (isBotOnlyActivity(input.snapshot)) return void 0;
408
+ const commentSummary = getCommentNotificationSummary(pr, input.snapshot);
409
+ return {
410
+ kind: "pull-request-activity",
411
+ priority: commentSummary ? "high" : "medium",
412
+ summary: commentSummary ?? `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
413
+ };
357
414
  }
358
415
  function classifyGithubBaselineNotification(input) {
359
- const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
360
- const failingChecks = getFailingChecks(input.snapshot);
361
- const reviewCount = input.snapshot.unresolvedReviewThreads ?? 0;
362
- const high = input.snapshot.ciState === "failure" || input.snapshot.mergeableState === "dirty";
363
- const details = [
364
- input.snapshot.state ? `state: ${input.snapshot.state}` : void 0,
365
- input.snapshot.ciState && input.snapshot.ciState !== "unknown" ? `CI: ${input.snapshot.ciState}` : void 0,
366
- input.snapshot.mergeableState ? `mergeability: ${input.snapshot.mergeableState}` : void 0,
367
- reviewCount > 0 ? `${reviewCount} unresolved review thread${reviewCount === 1 ? "" : "s"}` : void 0,
368
- failingChecks.length > 0 ? `failing: ${failingChecks.slice(0, 3).map((check) => check.name).join(", ")}` : void 0
369
- ].filter(Boolean);
370
- return {
371
- kind: "pull-request-baseline",
372
- priority: high ? "high" : "medium",
373
- summary: `${pr} subscribed${input.snapshot.title ? `: ${input.snapshot.title}` : ""}${details.length ? ` (${details.join("; ")})` : ""}`
374
- };
416
+ const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
417
+ const failingChecks = getFailingChecks(input.snapshot);
418
+ const reviewCount = input.snapshot.unresolvedReviewThreads ?? 0;
419
+ const high = input.snapshot.ciState === "failure" || input.snapshot.mergeableState === "dirty";
420
+ const details = [
421
+ input.snapshot.state ? `state: ${input.snapshot.state}` : void 0,
422
+ input.snapshot.ciState && input.snapshot.ciState !== "unknown" ? `CI: ${input.snapshot.ciState}` : void 0,
423
+ input.snapshot.mergeableState ? `mergeability: ${input.snapshot.mergeableState}` : void 0,
424
+ reviewCount > 0 ? `${reviewCount} unresolved review thread${reviewCount === 1 ? "" : "s"}` : void 0,
425
+ failingChecks.length > 0 ? `failing: ${failingChecks.slice(0, 3).map((check) => check.name).join(", ")}` : void 0
426
+ ].filter(Boolean);
427
+ return {
428
+ kind: "pull-request-baseline",
429
+ priority: high ? "high" : "medium",
430
+ summary: `${pr} subscribed${input.snapshot.title ? `: ${input.snapshot.title}` : ""}${details.length ? ` (${details.join("; ")})` : ""}`
431
+ };
375
432
  }
376
433
  function applySnapshotCursor(subscription, snapshot) {
377
- if (snapshot.githubUpdatedAt) subscription.lastObservedGithubUpdatedAt = snapshot.githubUpdatedAt;
378
- if (snapshot.contentHash) subscription.lastObservedContentHash = snapshot.contentHash;
379
- if (snapshot.threadContentHash) subscription.lastObservedThreadContentHash = snapshot.threadContentHash;
380
- if (snapshot.headSha) subscription.lastObservedHeadSha = snapshot.headSha;
381
- if (snapshot.state) subscription.lastObservedState = snapshot.state;
382
- if (snapshot.mergeableState) subscription.lastObservedMergeableState = snapshot.mergeableState;
383
- if (snapshot.ciState) subscription.lastObservedCiState = snapshot.ciState;
384
- if (snapshot.reviewStateHash) subscription.lastObservedReviewStateHash = snapshot.reviewStateHash;
434
+ if (snapshot.githubUpdatedAt) subscription.lastObservedGithubUpdatedAt = snapshot.githubUpdatedAt;
435
+ if (snapshot.contentHash) subscription.lastObservedContentHash = snapshot.contentHash;
436
+ if (snapshot.threadContentHash) subscription.lastObservedThreadContentHash = snapshot.threadContentHash;
437
+ if (snapshot.headSha) subscription.lastObservedHeadSha = snapshot.headSha;
438
+ if (snapshot.state) subscription.lastObservedState = snapshot.state;
439
+ if (snapshot.mergeableState) subscription.lastObservedMergeableState = snapshot.mergeableState;
440
+ if (snapshot.ciState) subscription.lastObservedCiState = snapshot.ciState;
441
+ if (snapshot.reviewStateHash) subscription.lastObservedReviewStateHash = snapshot.reviewStateHash;
385
442
  }
386
443
  function parseGitHubRemoteUrl(remoteUrl) {
387
- const trimmed = remoteUrl.trim().replace(/\.git$/, "");
388
- const httpsMatch = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/.exec(trimmed);
389
- if (httpsMatch?.[1] && httpsMatch[2]) return { owner: httpsMatch[1], repo: httpsMatch[2] };
390
- const sshMatch = /^git@github\.com:([^/]+)\/([^/]+)$/.exec(trimmed);
391
- if (sshMatch?.[1] && sshMatch[2]) return { owner: sshMatch[1], repo: sshMatch[2] };
392
- return void 0;
444
+ const trimmed = remoteUrl.trim().replace(/\.git$/, "");
445
+ const httpsMatch = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/.exec(trimmed);
446
+ if (httpsMatch?.[1] && httpsMatch[2]) return {
447
+ owner: httpsMatch[1],
448
+ repo: httpsMatch[2]
449
+ };
450
+ const sshMatch = /^git@github\.com:([^/]+)\/([^/]+)$/.exec(trimmed);
451
+ if (sshMatch?.[1] && sshMatch[2]) return {
452
+ owner: sshMatch[1],
453
+ repo: sshMatch[2]
454
+ };
393
455
  }
394
456
  var GitRemoteRepositoryResolver = class {
395
- async resolveRepository(input) {
396
- try {
397
- const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], {
398
- cwd: input.cwd,
399
- signal: input.abortSignal
400
- });
401
- return parseGitHubRemoteUrl(stdout);
402
- } catch {
403
- return void 0;
404
- }
405
- }
457
+ async resolveRepository(input) {
458
+ try {
459
+ const { stdout } = await execFileAsync("git", [
460
+ "remote",
461
+ "get-url",
462
+ "origin"
463
+ ], {
464
+ cwd: input.cwd,
465
+ signal: input.abortSignal
466
+ });
467
+ return parseGitHubRemoteUrl(stdout);
468
+ } catch {
469
+ return;
470
+ }
471
+ }
406
472
  };
407
473
  var GitcrawlSyncClient = class {
408
- #command;
409
- constructor(options = {}) {
410
- this.#command = options.command ?? "gitcrawl";
411
- }
412
- async syncPullRequest(input) {
413
- try {
414
- const args = [
415
- "sync",
416
- `${input.owner}/${input.repo}`,
417
- "--numbers",
418
- String(input.number),
419
- ...input.includeComments === false ? [] : ["--include-comments"],
420
- "--with",
421
- "pr-details",
422
- "--json"
423
- ];
424
- const { stdout, stderr } = await execFileAsync(this.#command, args, {
425
- cwd: input.cwd,
426
- signal: input.abortSignal,
427
- maxBuffer: 10 * 1024 * 1024
428
- });
429
- return { ok: true, stdout, stderr };
430
- } catch (error) {
431
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
432
- }
433
- }
434
- async getPullRequestSnapshot(input) {
435
- try {
436
- const { stdout } = await execFileAsync(
437
- this.#command,
438
- ["threads", `${input.owner}/${input.repo}`, "--numbers", String(input.number), "--json"],
439
- {
440
- cwd: input.cwd,
441
- signal: input.abortSignal,
442
- maxBuffer: 10 * 1024 * 1024
443
- }
444
- );
445
- const parsed = JSON.parse(stdout);
446
- const thread = parsed.threads?.find((item) => readNumber(item.number) === input.number);
447
- if (!thread) return void 0;
448
- const owner = sqlString(input.owner);
449
- const repo = sqlString(input.repo);
450
- const number = input.number;
451
- const [threadDetails] = await queryGitcrawlDb(`select t.state, t.closed_at_gh, t.merged_at_gh
474
+ #command;
475
+ #dbPathPromise;
476
+ constructor(options = {}) {
477
+ this.#command = options.command ?? "gitcrawl";
478
+ }
479
+ /**
480
+ * Resolve the gitcrawl SQLite database path. Explicit env overrides win,
481
+ * then gitcrawl itself is asked (authoritative across platforms and
482
+ * versions), then known default locations are probed.
483
+ */
484
+ async #resolveDbPath() {
485
+ if (process.env.GITCRAWL_DB_PATH) return resolveHomePath(process.env.GITCRAWL_DB_PATH);
486
+ if (process.env.GITCRAWL_CONFIG_PATH) {
487
+ const fromEnvConfig = await readDbPathFromGitcrawlConfig(process.env.GITCRAWL_CONFIG_PATH);
488
+ if (fromEnvConfig) return fromEnvConfig;
489
+ }
490
+ try {
491
+ const { stdout } = await execFileAsync(this.#command, ["status", "--json"], { maxBuffer: 10 * 1024 * 1024 });
492
+ const status = JSON.parse(stdout);
493
+ const reported = readString(status.database_path) ?? readString(status.db_path);
494
+ if (reported) return resolveHomePath(reported);
495
+ } catch {}
496
+ const defaultDirs = gitcrawlDefaultDirs();
497
+ for (const dir of defaultDirs) {
498
+ const fromConfig = await readDbPathFromGitcrawlConfig((0, path.join)(dir, "config.toml"));
499
+ if (fromConfig) return fromConfig;
500
+ }
501
+ for (const dir of defaultDirs) {
502
+ const candidate = (0, path.join)(dir, "gitcrawl.db");
503
+ if (await fileExists(candidate)) return candidate;
504
+ }
505
+ return (0, path.join)(defaultDirs[0], "gitcrawl.db");
506
+ }
507
+ async #queryDb(sql) {
508
+ this.#dbPathPromise ??= this.#resolveDbPath();
509
+ const dbPath = await this.#dbPathPromise;
510
+ try {
511
+ const { stdout } = await execFileAsync("sqlite3", [
512
+ "-json",
513
+ dbPath,
514
+ sql
515
+ ], { maxBuffer: 10 * 1024 * 1024 });
516
+ return JSON.parse(stdout || "[]");
517
+ } catch (error) {
518
+ this.#dbPathPromise = void 0;
519
+ const message = error instanceof Error ? error.message : String(error);
520
+ throw new Error(`gitcrawl database query failed (db: ${dbPath}): ${message}`);
521
+ }
522
+ }
523
+ async syncPullRequest(input) {
524
+ try {
525
+ const args = [
526
+ "sync",
527
+ `${input.owner}/${input.repo}`,
528
+ "--numbers",
529
+ String(input.number),
530
+ ...input.includeComments === false ? [] : ["--include-comments"],
531
+ "--with",
532
+ "pr-details",
533
+ "--json"
534
+ ];
535
+ const { stdout, stderr } = await execFileAsync(this.#command, args, {
536
+ cwd: input.cwd,
537
+ signal: input.abortSignal,
538
+ maxBuffer: 10 * 1024 * 1024
539
+ });
540
+ return {
541
+ ok: true,
542
+ stdout,
543
+ stderr
544
+ };
545
+ } catch (error) {
546
+ return {
547
+ ok: false,
548
+ error: error instanceof Error ? error.message : String(error)
549
+ };
550
+ }
551
+ }
552
+ async getPullRequestSnapshot(input) {
553
+ const { stdout } = await execFileAsync(this.#command, [
554
+ "threads",
555
+ `${input.owner}/${input.repo}`,
556
+ "--numbers",
557
+ String(input.number),
558
+ "--json"
559
+ ], {
560
+ cwd: input.cwd,
561
+ signal: input.abortSignal,
562
+ maxBuffer: 10 * 1024 * 1024
563
+ });
564
+ const thread = JSON.parse(stdout).threads?.find((item) => readNumber(item.number) === input.number);
565
+ if (!thread) return void 0;
566
+ const owner = sqlString(input.owner);
567
+ const repo = sqlString(input.repo);
568
+ const number = input.number;
569
+ const [threadDetails] = await this.#queryDb(`select t.state, t.closed_at_gh, t.merged_at_gh
452
570
  from threads t
453
571
  join repositories r on r.id=t.repo_id
454
572
  where r.owner=${owner} and r.name=${repo} and t.number=${number}
455
573
  limit 1`);
456
- const [details] = await queryGitcrawlDb(`select d.head_sha, d.head_ref, d.mergeable_state,
574
+ const [details] = await this.#queryDb(`select d.head_sha, d.head_ref, d.mergeable_state,
457
575
  json_extract(d.raw_json, '$.merged_at') as merged_at
458
576
  from pull_request_details d
459
577
  join threads t on t.id=d.thread_id
460
578
  join repositories r on r.id=t.repo_id
461
579
  where r.owner=${owner} and r.name=${repo} and t.number=${number}
462
580
  limit 1`);
463
- const headSha = readString(details?.head_sha);
464
- const checkRows = await queryGitcrawlDb(`select c.name, c.status, c.conclusion, c.workflow_name, c.details_url,
581
+ const headSha = readString(details?.head_sha);
582
+ const checkRows = await this.#queryDb(`select c.name, c.status, c.conclusion, c.workflow_name, c.details_url,
465
583
  coalesce(c.completed_at, c.started_at, c.fetched_at) as updated_at
466
584
  from pull_request_checks c
467
585
  join threads t on t.id=c.thread_id
468
586
  join repositories r on r.id=t.repo_id
469
587
  where r.owner=${owner} and r.name=${repo} and t.number=${number}${headSha ? ` and json_extract(c.raw_json, '$.head_sha')=${sqlString(headSha)}` : ""}`);
470
- const workflowRows = details?.head_sha ? await queryGitcrawlDb(`select workflow_name, status, conclusion, html_url, updated_at_gh
588
+ const workflowRows = details?.head_sha ? await this.#queryDb(`select workflow_name, status, conclusion, html_url, updated_at_gh
471
589
  from github_workflow_runs w
472
590
  join repositories r on r.id=w.repo_id
473
591
  where r.owner=${owner} and r.name=${repo} and w.head_sha=${sqlString(details.head_sha)}`) : [];
474
- const [reviewState] = await queryGitcrawlDb(`select count(*) as unresolved_count,
592
+ const [reviewState] = await this.#queryDb(`select count(*) as unresolved_count,
475
593
  max(coalesce(first_comment_updated_at, first_comment_created_at, fetched_at)) as latest_review_thread_at
476
594
  from pull_request_review_threads rt
477
595
  join threads t on t.id=rt.thread_id
478
596
  join repositories r on r.id=t.repo_id
479
597
  where r.owner=${owner} and r.name=${repo} and t.number=${number} and rt.is_resolved=0`);
480
- const latestComments = await queryGitcrawlDb(`select c.author_login, c.author_type, c.is_bot, c.body, json_extract(c.raw_json, '$.html_url') as html_url,
598
+ const latestComments = await this.#queryDb(`select c.author_login, c.author_type, c.is_bot, c.body, json_extract(c.raw_json, '$.html_url') as html_url,
481
599
  coalesce(c.updated_at_gh, c.created_at_gh) as updated_at
482
600
  from comments c
483
601
  join threads t on t.id=c.thread_id
@@ -485,893 +603,927 @@ var GitcrawlSyncClient = class {
485
603
  where r.owner=${owner} and r.name=${repo} and t.number=${number}
486
604
  order by coalesce(c.updated_at_gh, c.created_at_gh) desc
487
605
  limit 20`);
488
- const latestComment = latestComments[0];
489
- const checks = normalizeGithubChecksForSnapshot({
490
- checkRows: checkRows.map((row) => ({
491
- source: "check",
492
- name: readString(row.name) ?? "check",
493
- status: readString(row.status),
494
- conclusion: readString(row.conclusion),
495
- workflowName: readString(row.workflow_name),
496
- detailsUrl: readString(row.details_url),
497
- updatedAt: readString(row.updated_at)
498
- })),
499
- workflowRows: workflowRows.map((row) => ({
500
- source: "workflow",
501
- name: readString(row.workflow_name) ?? "workflow",
502
- status: readString(row.status),
503
- conclusion: readString(row.conclusion),
504
- workflowName: readString(row.workflow_name),
505
- detailsUrl: readString(row.html_url),
506
- updatedAt: readString(row.updated_at_gh)
507
- }))
508
- });
509
- const ciState = checks.some((check) => check.conclusion === "failure" || check.conclusion === "timed_out") ? "failure" : checks.some((check) => check.status && check.status !== "completed") ? "pending" : checks.length > 0 ? "success" : "unknown";
510
- const threadContentHash = readString(thread.content_hash);
511
- const unresolvedReviewThreads = Number(reviewState?.unresolved_count ?? 0);
512
- const reviewStateHash = snapshotHash({
513
- unresolvedReviewThreads,
514
- latestReviewThreadAt: reviewState?.latest_review_thread_at
515
- });
516
- const contentHash = snapshotHash({
517
- threadContentHash,
518
- state: thread.state,
519
- headSha: details?.head_sha,
520
- mergeableState: details?.mergeable_state,
521
- ciState,
522
- reviewStateHash,
523
- checks: checks.map((check) => ({
524
- name: check.name,
525
- status: check.status,
526
- conclusion: check.conclusion,
527
- detailsUrl: check.detailsUrl,
528
- updatedAt: check.updatedAt
529
- }))
530
- });
531
- return {
532
- title: readString(thread.title),
533
- state: readString(details?.merged_at) || readString(threadDetails?.merged_at_gh) ? "merged" : readString(threadDetails?.state) ?? readString(thread.state),
534
- htmlUrl: readString(thread.html_url),
535
- githubUpdatedAt: readString(thread.updated_at_gh),
536
- closedAt: readString(threadDetails?.closed_at_gh),
537
- mergedAt: readString(details?.merged_at) ?? readString(threadDetails?.merged_at_gh),
538
- threadContentHash,
539
- contentHash,
540
- headSha: readString(details?.head_sha),
541
- headRef: readString(details?.head_ref),
542
- mergeableState: readString(details?.mergeable_state),
543
- checks,
544
- ciState,
545
- unresolvedReviewThreads,
546
- reviewStateHash,
547
- latestReviewThreadAt: readString(reviewState?.latest_review_thread_at),
548
- latestCommentAuthor: readString(latestComment?.author_login),
549
- latestCommentAuthorType: readString(latestComment?.author_type),
550
- latestCommentIsBot: latestComment?.is_bot === 1,
551
- latestCommentBody: sanitizeCommentBody(readString(latestComment?.body)),
552
- latestCommentUrl: readString(latestComment?.html_url),
553
- latestCommentUpdatedAt: readString(latestComment?.updated_at),
554
- latestComments: latestComments.map((comment) => ({
555
- author: readString(comment.author_login),
556
- authorType: readString(comment.author_type),
557
- isBot: comment.is_bot === 1,
558
- body: sanitizeCommentBody(readString(comment.body)),
559
- url: readString(comment.html_url),
560
- updatedAt: readString(comment.updated_at)
561
- }))
562
- };
563
- } catch {
564
- return void 0;
565
- }
566
- }
606
+ const latestComment = latestComments[0];
607
+ const checks = normalizeGithubChecksForSnapshot({
608
+ checkRows: checkRows.map((row) => ({
609
+ source: "check",
610
+ name: readString(row.name) ?? "check",
611
+ status: readString(row.status),
612
+ conclusion: readString(row.conclusion),
613
+ workflowName: readString(row.workflow_name),
614
+ detailsUrl: readString(row.details_url),
615
+ updatedAt: readString(row.updated_at)
616
+ })),
617
+ workflowRows: workflowRows.map((row) => ({
618
+ source: "workflow",
619
+ name: readString(row.workflow_name) ?? "workflow",
620
+ status: readString(row.status),
621
+ conclusion: readString(row.conclusion),
622
+ workflowName: readString(row.workflow_name),
623
+ detailsUrl: readString(row.html_url),
624
+ updatedAt: readString(row.updated_at_gh)
625
+ }))
626
+ });
627
+ const ciState = checks.some((check) => check.conclusion === "failure" || check.conclusion === "timed_out") ? "failure" : checks.some((check) => check.status && check.status !== "completed") ? "pending" : checks.length > 0 ? "success" : "unknown";
628
+ const threadContentHash = readString(thread.content_hash);
629
+ const unresolvedReviewThreads = Number(reviewState?.unresolved_count ?? 0);
630
+ const reviewStateHash = snapshotHash({
631
+ unresolvedReviewThreads,
632
+ latestReviewThreadAt: reviewState?.latest_review_thread_at
633
+ });
634
+ const contentHash = snapshotHash({
635
+ threadContentHash,
636
+ state: thread.state,
637
+ headSha: details?.head_sha,
638
+ mergeableState: details?.mergeable_state,
639
+ ciState,
640
+ reviewStateHash,
641
+ checks: checks.map((check) => ({
642
+ name: check.name,
643
+ status: check.status,
644
+ conclusion: check.conclusion,
645
+ detailsUrl: check.detailsUrl,
646
+ updatedAt: check.updatedAt
647
+ }))
648
+ });
649
+ return {
650
+ title: readString(thread.title),
651
+ state: readString(details?.merged_at) || readString(threadDetails?.merged_at_gh) ? "merged" : readString(threadDetails?.state) ?? readString(thread.state),
652
+ htmlUrl: readString(thread.html_url),
653
+ githubUpdatedAt: readString(thread.updated_at_gh),
654
+ closedAt: readString(threadDetails?.closed_at_gh),
655
+ mergedAt: readString(details?.merged_at) ?? readString(threadDetails?.merged_at_gh),
656
+ threadContentHash,
657
+ contentHash,
658
+ headSha: readString(details?.head_sha),
659
+ headRef: readString(details?.head_ref),
660
+ mergeableState: readString(details?.mergeable_state),
661
+ checks,
662
+ ciState,
663
+ unresolvedReviewThreads,
664
+ reviewStateHash,
665
+ latestReviewThreadAt: readString(reviewState?.latest_review_thread_at),
666
+ latestCommentAuthor: readString(latestComment?.author_login),
667
+ latestCommentAuthorType: readString(latestComment?.author_type),
668
+ latestCommentIsBot: latestComment?.is_bot === 1,
669
+ latestCommentBody: sanitizeCommentBody(readString(latestComment?.body)),
670
+ latestCommentUrl: readString(latestComment?.html_url),
671
+ latestCommentUpdatedAt: readString(latestComment?.updated_at),
672
+ latestComments: latestComments.map((comment) => ({
673
+ author: readString(comment.author_login),
674
+ authorType: readString(comment.author_type),
675
+ isBot: comment.is_bot === 1,
676
+ body: sanitizeCommentBody(readString(comment.body)),
677
+ url: readString(comment.html_url),
678
+ updatedAt: readString(comment.updated_at)
679
+ }))
680
+ };
681
+ }
567
682
  };
568
- var GithubSignals = class extends signals.SignalProvider {
569
- id = "github-signals";
570
- name = "GitHub Signals";
571
- #ghMastra;
572
- static signals = {
573
- subscribeToPR(input) {
574
- const normalized = typeof input === "number" ? { number: input } : input;
575
- return {
576
- type: "reactive",
577
- tagName: GITHUB_SUBSCRIBE_PR_TAG,
578
- contents: `Subscribe to GitHub PR #${normalized.number}`,
579
- attributes: {
580
- ...normalized.owner ? { owner: normalized.owner } : {},
581
- ...normalized.repo ? { repo: normalized.repo } : {},
582
- number: normalized.number
583
- },
584
- metadata: {
585
- github: {
586
- action: "subscribeToPR",
587
- ...normalized
588
- }
589
- }
590
- };
591
- },
592
- unsubscribeFromPR(input) {
593
- const normalized = typeof input === "number" ? { number: input } : input;
594
- return {
595
- type: "reactive",
596
- tagName: GITHUB_UNSUBSCRIBE_PR_TAG,
597
- contents: `Unsubscribe from GitHub PR #${normalized.number}`,
598
- attributes: {
599
- ...normalized.owner ? { owner: normalized.owner } : {},
600
- ...normalized.repo ? { repo: normalized.repo } : {},
601
- number: normalized.number
602
- },
603
- metadata: {
604
- github: {
605
- action: "unsubscribeFromPR",
606
- ...normalized
607
- }
608
- }
609
- };
610
- }
611
- };
612
- #options;
613
- #syncClient;
614
- #repositoryResolver;
615
- #polling = /* @__PURE__ */ new Map();
616
- #permissionCache = /* @__PURE__ */ new Map();
617
- #agent;
618
- #agentOptions = {};
619
- #subscriptionsChangedHandler;
620
- #pollingChangedHandler;
621
- constructor(options = {}) {
622
- super();
623
- this.#options = options;
624
- this.#syncClient = options.syncClient ?? new GitcrawlSyncClient({ command: options.gitcrawlCommand });
625
- this.#repositoryResolver = options.repositoryResolver ?? new GitRemoteRepositoryResolver();
626
- if (options.getNotificationStreamOptions) {
627
- this.#agentOptions = { getNotificationStreamOptions: options.getNotificationStreamOptions };
628
- }
629
- }
630
- /**
631
- * @deprecated Use `Agent({ signals: [githubSignals] })` instead.
632
- * Kept for backward compatibility.
633
- */
634
- addAgent(agent, options = {}) {
635
- this.#agent = agent;
636
- this.#agentOptions = options;
637
- }
638
- /**
639
- * Called by the Agent constructor when this provider is passed via `signals: [...]`.
640
- * Sets the bidirectional link so the provider can send signals back to the agent.
641
- */
642
- connect(agent) {
643
- super.connect(agent);
644
- this.#agent = agent;
645
- }
646
- getInputProcessors() {
647
- return [this];
648
- }
649
- getOutputProcessors() {
650
- return [this];
651
- }
652
- onSubscriptionsChanged(handler) {
653
- this.#subscriptionsChangedHandler = handler;
654
- }
655
- onPollingChanged(handler) {
656
- this.#pollingChangedHandler = handler;
657
- }
658
- __registerMastra(mastra) {
659
- super.__registerMastra(mastra);
660
- this.#ghMastra = mastra;
661
- }
662
- async syncThreadNow(input) {
663
- return this.#pollThread(input, { includeComments: true });
664
- }
665
- async subscribeThreadToPR(input) {
666
- const pr = typeof input.pr === "number" ? { number: input.pr } : input.pr;
667
- return this.#subscribe({
668
- id: `github-command-subscribe-${crypto.randomUUID()}`,
669
- ...pr,
670
- threadId: input.threadId,
671
- resourceId: input.resourceId
672
- });
673
- }
674
- async unsubscribeThreadFromPR(input) {
675
- const pr = typeof input.pr === "number" ? { number: input.pr } : input.pr;
676
- return this.#unsubscribe({
677
- id: `github-command-unsubscribe-${crypto.randomUUID()}`,
678
- ...pr,
679
- threadId: input.threadId,
680
- resourceId: input.resourceId
681
- });
682
- }
683
- async startPollingForThread(input, options = {}) {
684
- const subscriptions = await this.#getThreadSubscriptions(input);
685
- if (subscriptions.length === 0) {
686
- this.stopPollingForThread(input);
687
- return false;
688
- }
689
- const key = this.#pollingKey(input);
690
- for (const [pollingKey, state] of this.#polling.entries()) {
691
- if (pollingKey === key) continue;
692
- clearInterval(state.timer);
693
- this.#polling.delete(pollingKey);
694
- }
695
- if (this.#polling.has(key)) return true;
696
- const runPoll = (pollOptions = {}) => {
697
- void this.#pollThread(input, pollOptions).catch((error) => {
698
- console.warn("GitHub PR polling failed:", error);
699
- });
700
- };
701
- const timer = setInterval(() => {
702
- runPoll({ includeComments: true });
703
- }, this.#options.pollIntervalMs ?? 3e5);
704
- if (options.pollImmediately) runPoll({ includeComments: true });
705
- timer.unref?.();
706
- this.#polling.set(key, { ...input, timer, running: false });
707
- return true;
708
- }
709
- stopPollingForThread(input) {
710
- const key = this.#pollingKey(input);
711
- const state = this.#polling.get(key);
712
- if (!state) return;
713
- clearInterval(state.timer);
714
- this.#polling.delete(key);
715
- }
716
- isPollingThread(input) {
717
- return this.#polling.has(this.#pollingKey(input));
718
- }
719
- isPollingThreadRunning(input) {
720
- return this.#polling.get(this.#pollingKey(input))?.running ?? false;
721
- }
722
- getPollIntervalMs() {
723
- return this.#options.pollIntervalMs ?? 3e5;
724
- }
725
- stopAllPolling() {
726
- for (const state of this.#polling.values()) clearInterval(state.timer);
727
- this.#polling.clear();
728
- }
729
- async pollThreadNow(input) {
730
- return this.#pollThread(input, { includeComments: true });
731
- }
732
- async processInputStep(args) {
733
- const tools = this.#createTools(args);
734
- if (args.stepNumber !== 0) return { tools };
735
- const signal = this.#findLatestGithubSignal(args.messages);
736
- if (!signal) return { tools };
737
- const threadContext = this.#getThreadContext(args);
738
- if (signal.tagName === GITHUB_UNSUBSCRIBE_PR_TAG) {
739
- const result2 = await this.#unsubscribe({ ...signal, ...threadContext, abortSignal: args.abortSignal });
740
- await this.#sendStatus(args, result2, {
741
- status: result2.removed ? "unsubscribed" : "not_subscribed",
742
- action: "unsubscribeFromPR",
743
- message: result2.removed ? `Unsubscribed from ${result2.owner}/${result2.repo}#${result2.number}.` : `No GitHub subscription found for ${result2.owner}/${result2.repo}#${result2.number}.`
744
- });
745
- return { tools };
746
- }
747
- const result = await this.#subscribe({ ...signal, ...threadContext, abortSignal: args.abortSignal });
748
- if (result.alreadyProcessed) return { tools };
749
- await this.#sendStatus(args, result, {
750
- status: result.syncResult?.ok === false ? "sync_error" : "subscribed",
751
- action: "subscribeToPR",
752
- message: result.syncResult?.ok === false ? `Subscribed to ${result.owner}/${result.repo}#${result.number}, but gitcrawl sync failed: ${result.syncResult.error}` : `Subscribed to ${result.owner}/${result.repo}#${result.number}.`
753
- });
754
- return { tools };
755
- }
756
- async processOutputStep(args) {
757
- const evidence = detectPrWorkEvidence({ text: args.text, toolCalls: args.toolCalls });
758
- if (!evidence) return;
759
- const threadContext = this.#getThreadContext(args);
760
- if (!threadContext.threadId || !threadContext.resourceId) return;
761
- const { threadStore, loadedThread } = await this.#loadThread(threadContext);
762
- const githubMetadata = getGithubMetadata(loadedThread.metadata);
763
- if (githubMetadata.subscriptionHintShown || githubMetadata.subscriptions.length > 0) return;
764
- let repository;
765
- try {
766
- repository = await this.#resolveRepository({
767
- id: "github-subscription-hint",
768
- owner: evidence.owner,
769
- repo: evidence.repo,
770
- number: evidence.number
771
- });
772
- } catch {
773
- return;
774
- }
775
- await threadStore.saveThread({
776
- thread: {
777
- ...loadedThread,
778
- id: threadContext.threadId,
779
- resourceId: threadContext.resourceId,
780
- createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
781
- updatedAt: /* @__PURE__ */ new Date(),
782
- metadata: setGithubMetadata(loadedThread.metadata, { ...githubMetadata, subscriptionHintShown: true })
783
- }
784
- });
785
- await args.sendSignal?.({
786
- type: "reactive",
787
- tagName: "system-reminder",
788
- contents: `Looks like you're working with ${repository.owner}/${repository.repo}#${evidence.number}. Use /github subscribe ${evidence.number} or the github_subscribe_pr tool to follow updates.`,
789
- attributes: { type: "github-subscription-hint" },
790
- metadata: {
791
- github: {
792
- action: "subscriptionHint",
793
- owner: repository.owner,
794
- repo: repository.repo,
795
- number: evidence.number
796
- }
797
- }
798
- });
799
- }
800
- async #resolveThreadStore() {
801
- if (this.#options.threadStore) return this.#options.threadStore;
802
- const storage = this.#ghMastra?.getStorage?.();
803
- const memoryStore = storage?.getStore ? await storage.getStore("memory") : void 0;
804
- return memoryStore;
805
- }
806
- #getThreadContext(args) {
807
- const memoryContext = args.requestContext?.get("MastraMemory");
808
- return { threadId: memoryContext?.thread?.id, resourceId: memoryContext?.resourceId };
809
- }
810
- #createTools(args) {
811
- const threadContext = this.#getThreadContext(args);
812
- const getExecutionThreadContext = (context) => ({
813
- threadId: context?.agent?.threadId ?? threadContext.threadId,
814
- resourceId: context?.agent?.resourceId ?? threadContext.resourceId
815
- });
816
- return {
817
- ...args.tools,
818
- github_subscribe_pr: createGithubTool({
819
- id: "github_subscribe_pr",
820
- description: "Subscribe this thread to a GitHub pull request. Syncs only the requested PR with gitcrawl and stores the subscription on the thread.",
821
- inputSchema: z__default.default.object({
822
- number: z__default.default.number().int().positive(),
823
- owner: z__default.default.string().optional(),
824
- repo: z__default.default.string().optional()
825
- }),
826
- execute: async (input, context) => {
827
- const executionThreadContext = getExecutionThreadContext(context);
828
- const result = await this.#subscribe({
829
- id: `github-tool-subscribe-${crypto.randomUUID()}`,
830
- owner: input.owner,
831
- repo: input.repo,
832
- number: input.number,
833
- threadId: executionThreadContext.threadId,
834
- resourceId: executionThreadContext.resourceId
835
- });
836
- return {
837
- subscribed: true,
838
- owner: result.owner,
839
- repo: result.repo,
840
- number: result.number,
841
- syncStatus: result.syncResult?.ok === false ? "error" : result.syncResult ? "success" : void 0,
842
- message: result.syncResult?.ok === false ? `Subscribed to ${result.owner}/${result.repo}#${result.number}, but gitcrawl sync failed: ${result.syncResult.error}` : `Subscribed to ${result.owner}/${result.repo}#${result.number}.`
843
- };
844
- }
845
- }),
846
- github_unsubscribe_pr: createGithubTool({
847
- id: "github_unsubscribe_pr",
848
- description: "Unsubscribe this thread from a GitHub pull request.",
849
- inputSchema: z__default.default.object({
850
- number: z__default.default.number().int().positive(),
851
- owner: z__default.default.string().optional(),
852
- repo: z__default.default.string().optional()
853
- }),
854
- execute: async (input, context) => {
855
- const executionThreadContext = getExecutionThreadContext(context);
856
- const result = await this.#unsubscribe({
857
- id: `github-tool-unsubscribe-${crypto.randomUUID()}`,
858
- owner: input.owner,
859
- repo: input.repo,
860
- number: input.number,
861
- threadId: executionThreadContext.threadId,
862
- resourceId: executionThreadContext.resourceId
863
- });
864
- return {
865
- unsubscribed: result.removed ?? false,
866
- owner: result.owner,
867
- repo: result.repo,
868
- number: result.number,
869
- remainingSubscriptions: result.remainingSubscriptions,
870
- message: result.removed ? `Unsubscribed from ${result.owner}/${result.repo}#${result.number}.` : `No GitHub subscription found for ${result.owner}/${result.repo}#${result.number}.`
871
- };
872
- }
873
- })
874
- };
875
- }
876
- async #resolveRepository(input) {
877
- const resolvedRepository = input.owner && input.repo ? { owner: input.owner, repo: input.repo } : this.#options.owner && this.#options.repo ? { owner: this.#options.owner, repo: this.#options.repo } : await this.#repositoryResolver.resolveRepository({
878
- cwd: this.#options.cwd,
879
- abortSignal: input.abortSignal
880
- });
881
- if (!resolvedRepository?.owner || !resolvedRepository.repo) {
882
- throw new Error(
883
- "GitHub PR subscription requires owner and repo. Run inside a GitHub repo or pass owner and repo."
884
- );
885
- }
886
- return resolvedRepository;
887
- }
888
- async #loadThread(input) {
889
- const threadStore = await this.#resolveThreadStore();
890
- if (!threadStore) throw new Error("GitHub PR subscription requires memory-backed thread storage.");
891
- if (!input.threadId || !input.resourceId)
892
- throw new Error("GitHub PR subscription requires threadId and resourceId.");
893
- const loadedThread = await threadStore.getThreadById({ threadId: input.threadId, resourceId: input.resourceId }) ?? void 0;
894
- if (!loadedThread) throw new Error(`Could not load thread ${input.threadId}.`);
895
- return { threadStore, loadedThread };
896
- }
897
- #pollingKey(input) {
898
- return `${input.resourceId}:${input.threadId}`;
899
- }
900
- #getNotificationAgent(_input) {
901
- if (this.#agent) return this.#agent;
902
- const agentId = _input?.agentId ?? this.#options.agentId;
903
- return agentId ? this.#ghMastra?.getAgentById?.(agentId) : void 0;
904
- }
905
- async #getThreadSubscriptions(input) {
906
- const { loadedThread } = await this.#loadThread(input);
907
- return getGithubMetadata(loadedThread.metadata).subscriptions;
908
- }
909
- #notifySubscriptionsChanged(input) {
910
- this.#subscriptionsChangedHandler?.(input);
911
- }
912
- #notifyPollingChanged(input) {
913
- this.#pollingChangedHandler?.(input);
914
- }
915
- async #pollThread(input, options = {}) {
916
- const key = this.#pollingKey(input);
917
- const state = this.#polling.get(key);
918
- if (state?.running) {
919
- return 0;
920
- }
921
- if (state) state.running = true;
922
- this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: true });
923
- try {
924
- const { threadStore, loadedThread } = await this.#loadThread(input);
925
- const githubMetadata = getGithubMetadata(loadedThread.metadata);
926
- if (githubMetadata.subscriptions.length === 0) {
927
- this.stopPollingForThread(input);
928
- return 0;
929
- }
930
- const now = (/* @__PURE__ */ new Date()).toISOString();
931
- const subscriptions = [];
932
- for (const subscription of githubMetadata.subscriptions) {
933
- const syncInput = {
934
- owner: subscription.owner,
935
- repo: subscription.repo,
936
- number: subscription.number,
937
- cwd: this.#options.cwd,
938
- includeComments: options.includeComments
939
- };
940
- const syncResult = await this.#syncClient.syncPullRequest(syncInput);
941
- let snapshot = syncResult.ok ? await this.#syncClient.getPullRequestSnapshot?.(syncInput) : void 0;
942
- if (snapshot)
943
- snapshot = await this.#filterUnauthorizedLatestComment(subscription.owner, subscription.repo, snapshot);
944
- const nextSubscription = {
945
- ...subscription,
946
- updatedAt: now,
947
- lastSyncAt: now,
948
- lastSyncStatus: syncResult.ok ? "success" : "error"
949
- };
950
- if (syncResult.error) nextSubscription.lastSyncError = syncResult.error;
951
- else delete nextSubscription.lastSyncError;
952
- const previousGithubUpdatedAt = subscription.lastObservedGithubUpdatedAt;
953
- const previousContentHash = subscription.lastObservedContentHash;
954
- const previousThreadContentHash = subscription.lastObservedThreadContentHash;
955
- const previousHeadSha = subscription.lastObservedHeadSha;
956
- const latestCommentChanged = !!previousGithubUpdatedAt && !!snapshot?.latestCommentUpdatedAt && Date.parse(snapshot.latestCommentUpdatedAt) > Date.parse(previousGithubUpdatedAt);
957
- if (snapshot) applySnapshotCursor(nextSubscription, snapshot);
958
- const isFirstObservation = syncResult.ok && snapshot && !previousGithubUpdatedAt && !previousContentHash;
959
- const legacyAggregateChanged = previousContentHash && snapshot?.contentHash && previousContentHash !== snapshot.contentHash && !previousThreadContentHash && !previousHeadSha;
960
- const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || latestCommentChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
961
- let shouldKeepSubscription = true;
962
- if (changed && snapshot) {
963
- const notifications = await this.#sendActivityNotifications({
964
- polling: input,
965
- subscription,
966
- snapshot,
967
- previousGithubUpdatedAt,
968
- previousContentHash,
969
- latestCommentChanged
970
- });
971
- const primaryNotification = notifications[0];
972
- if (primaryNotification) {
973
- nextSubscription.lastNotificationAt = now;
974
- nextSubscription.lastNotificationKind = primaryNotification.kind;
975
- nextSubscription.lastNotificationPriority = primaryNotification.priority;
976
- nextSubscription.lastNotificationSummary = primaryNotification.summary;
977
- shouldKeepSubscription = notifications.every((notification) => notification.kind !== "pull-request-merged");
978
- }
979
- }
980
- if (shouldKeepSubscription) subscriptions.push(nextSubscription);
981
- }
982
- await threadStore.saveThread({
983
- thread: {
984
- ...loadedThread,
985
- id: input.threadId,
986
- resourceId: input.resourceId,
987
- createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
988
- updatedAt: /* @__PURE__ */ new Date(),
989
- metadata: setGithubMetadata(loadedThread.metadata, { subscriptions })
990
- }
991
- });
992
- this.#notifySubscriptionsChanged({ threadId: input.threadId, resourceId: input.resourceId, subscriptions });
993
- if (subscriptions.length === 0) this.stopPollingForThread(input);
994
- return subscriptions.length;
995
- } catch (error) {
996
- throw error;
997
- } finally {
998
- const latestState = this.#polling.get(key);
999
- if (latestState) latestState.running = false;
1000
- this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: false });
1001
- }
1002
- }
1003
- #createGithubNotificationInput(input) {
1004
- const failingChecks = getFailingChecks(input.snapshot);
1005
- const pendingChecks = getPendingChecks(input.snapshot);
1006
- const latestCommentExcerpt = input.snapshot.latestCommentBody ? getCommentExcerpt(input.snapshot.latestCommentBody) : void 0;
1007
- const latestCommentDedupeSuffix = input.notification.kind === "pull-request-activity" && input.snapshot.latestCommentUrl ? `comment:${input.snapshot.latestCommentUrl}:${input.snapshot.latestCommentUpdatedAt ?? ""}` : input.dedupeSuffix;
1008
- const notificationInput = {
1009
- source: "github",
1010
- kind: input.notification.kind,
1011
- priority: input.notification.priority,
1012
- summary: input.notification.summary,
1013
- dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${latestCommentDedupeSuffix}`,
1014
- coalesceKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${input.notification.kind}`,
1015
- attributes: {
1016
- owner: input.subscription.owner,
1017
- repo: input.subscription.repo,
1018
- number: input.subscription.number,
1019
- ...input.snapshot.title ? { title: input.snapshot.title } : {},
1020
- ...input.snapshot.state ? { state: input.snapshot.state } : {},
1021
- ...input.snapshot.htmlUrl ? { url: input.snapshot.htmlUrl } : {},
1022
- ...input.snapshot.githubUpdatedAt ? { githubUpdatedAt: input.snapshot.githubUpdatedAt } : {},
1023
- ...input.previousGithubUpdatedAt ? { previousGithubUpdatedAt: input.previousGithubUpdatedAt } : {},
1024
- ...input.snapshot.mergeableState ? { mergeableState: input.snapshot.mergeableState } : {},
1025
- ...input.snapshot.ciState ? { ciState: input.snapshot.ciState } : {},
1026
- ...input.snapshot.unresolvedReviewThreads !== void 0 ? { unresolvedReviewThreads: input.snapshot.unresolvedReviewThreads } : {},
1027
- ...input.snapshot.latestCommentAuthor ? { latestCommentAuthor: input.snapshot.latestCommentAuthor } : {},
1028
- ...latestCommentExcerpt ? { latestCommentExcerpt } : {},
1029
- ...input.snapshot.latestCommentUrl ? { latestCommentUrl: input.snapshot.latestCommentUrl } : {},
1030
- ...input.snapshot.latestCommentUpdatedAt ? { latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt } : {},
1031
- ...failingChecks.length > 0 ? { failingChecks: failingChecks.map((check) => check.name).join(", ") } : {},
1032
- ...pendingChecks.length > 0 ? { pendingChecks: pendingChecks.map((check) => check.name).join(", ") } : {}
1033
- },
1034
- metadata: {
1035
- github: {
1036
- owner: input.subscription.owner,
1037
- repo: input.subscription.repo,
1038
- number: input.subscription.number,
1039
- title: input.snapshot.title,
1040
- state: input.snapshot.state,
1041
- htmlUrl: input.snapshot.htmlUrl,
1042
- githubUpdatedAt: input.snapshot.githubUpdatedAt,
1043
- previousGithubUpdatedAt: input.previousGithubUpdatedAt,
1044
- contentHash: input.snapshot.contentHash,
1045
- previousContentHash: input.previousContentHash,
1046
- threadContentHash: input.snapshot.threadContentHash,
1047
- headSha: input.snapshot.headSha,
1048
- headRef: input.snapshot.headRef,
1049
- mergeableState: input.snapshot.mergeableState,
1050
- ciState: input.snapshot.ciState,
1051
- closedAt: input.snapshot.closedAt,
1052
- mergedAt: input.snapshot.mergedAt,
1053
- unresolvedReviewThreads: input.snapshot.unresolvedReviewThreads,
1054
- reviewStateHash: input.snapshot.reviewStateHash,
1055
- latestReviewThreadAt: input.snapshot.latestReviewThreadAt,
1056
- latestCommentAuthor: input.snapshot.latestCommentAuthor,
1057
- latestCommentAuthorType: input.snapshot.latestCommentAuthorType,
1058
- latestCommentIsBot: input.snapshot.latestCommentIsBot,
1059
- // Intentionally omit the full latestCommentBody here: persisting it verbatim bloats
1060
- // notification payloads (a single CodeRabbit comment can exceed 100KB) and can overflow
1061
- // agent context windows when listed. The 240-char latestCommentExcerpt is stored instead.
1062
- latestCommentExcerpt,
1063
- latestCommentUrl: input.snapshot.latestCommentUrl,
1064
- latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt,
1065
- failingChecks,
1066
- pendingChecks
1067
- }
1068
- }
1069
- };
1070
- return notificationInput;
1071
- }
1072
- async #sendGithubNotification(input) {
1073
- const notificationInput = this.#createGithubNotificationInput(input);
1074
- const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(input.target);
1075
- await input.agent?.sendNotificationSignal?.(
1076
- notificationInput,
1077
- streamOptions ? { ...input.target, ifIdle: { streamOptions } } : input.target
1078
- );
1079
- }
1080
- async #sendBaselineNotification(input) {
1081
- const agent = this.#getNotificationAgent({});
1082
- if (!agent?.sendNotificationSignal) return;
1083
- await this.#sendGithubNotification({
1084
- agent,
1085
- subscription: input.subscription,
1086
- snapshot: input.snapshot,
1087
- notification: classifyGithubBaselineNotification({ subscription: input.subscription, snapshot: input.snapshot }),
1088
- target: { resourceId: input.resourceId, threadId: input.threadId },
1089
- dedupeSuffix: `baseline:${input.subscription.lastSubscribeSignalId}`
1090
- });
1091
- }
1092
- async #isAuthorizedAuthor(owner, repo, user, metadata = {}) {
1093
- if (!user) return false;
1094
- const normalizedUser = user.toLowerCase();
1095
- const isBot = metadata.isBot === true || metadata.authorType?.toLowerCase() === "bot" || normalizedUser.endsWith("[bot]");
1096
- if (isBot) {
1097
- const ignoredBots = this.#options.ignoredBots ?? [];
1098
- if (ignoredBots.some((bot) => bot.toLowerCase() === normalizedUser)) return false;
1099
- const authorizedBots = this.#options.authorizedBots ?? DEFAULT_AUTHORIZED_BOTS;
1100
- return authorizedBots.some((bot) => bot.toLowerCase() === normalizedUser);
1101
- }
1102
- const permission = await this.#loadAuthorPermission(owner, repo, user);
1103
- const authorizedPermissions = this.#options.authorizedPermissions ?? DEFAULT_AUTHORIZED_PERMISSIONS;
1104
- return !!permission && authorizedPermissions.includes(permission);
1105
- }
1106
- async #filterUnauthorizedLatestComment(owner, repo, snapshot) {
1107
- const comments = snapshot.latestComments?.length ? snapshot.latestComments : [
1108
- {
1109
- author: snapshot.latestCommentAuthor,
1110
- authorType: snapshot.latestCommentAuthorType,
1111
- isBot: snapshot.latestCommentIsBot,
1112
- body: snapshot.latestCommentBody,
1113
- url: snapshot.latestCommentUrl,
1114
- updatedAt: snapshot.latestCommentUpdatedAt
1115
- }
1116
- ];
1117
- if (!comments.some((comment) => comment.author)) return snapshot;
1118
- if (!comments.some((comment) => comment.body || comment.url || comment.updatedAt)) return snapshot;
1119
- for (const comment of comments) {
1120
- if (!await this.#isAuthorizedAuthor(owner, repo, comment.author, {
1121
- authorType: comment.authorType,
1122
- isBot: comment.isBot
1123
- })) {
1124
- continue;
1125
- }
1126
- return {
1127
- ...snapshot,
1128
- latestCommentAuthor: comment.author,
1129
- latestCommentAuthorType: comment.authorType,
1130
- latestCommentIsBot: comment.isBot,
1131
- latestCommentBody: comment.body,
1132
- latestCommentUrl: comment.url,
1133
- latestCommentUpdatedAt: comment.updatedAt
1134
- };
1135
- }
1136
- return {
1137
- ...snapshot,
1138
- latestCommentAuthor: void 0,
1139
- latestCommentAuthorType: void 0,
1140
- latestCommentIsBot: void 0,
1141
- latestCommentBody: void 0,
1142
- latestCommentUrl: void 0,
1143
- latestCommentUpdatedAt: void 0
1144
- };
1145
- }
1146
- async #loadAuthorPermission(owner, repo, user) {
1147
- const cacheKey = `${owner}/${repo}:${user.toLowerCase()}`;
1148
- const cached = this.#permissionCache.get(cacheKey);
1149
- if (cached && cached.expiresAt > Date.now()) return cached.permission;
1150
- if (cached) this.#permissionCache.delete(cacheKey);
1151
- try {
1152
- let permission;
1153
- if (this.#options.permissionResolver) {
1154
- permission = await this.#options.permissionResolver.getPermission(owner, repo, user);
1155
- } else {
1156
- const { stdout } = await execFileAsync("gh", [
1157
- "api",
1158
- `repos/${owner}/${repo}/collaborators/${user}/permission`,
1159
- "--jq",
1160
- ".permission"
1161
- ]);
1162
- const raw = stdout.trim();
1163
- permission = ["admin", "maintain", "write", "triage", "read", "none"].includes(
1164
- raw
1165
- ) ? raw : void 0;
1166
- }
1167
- if (permission) {
1168
- this.#permissionCache.set(cacheKey, { permission, expiresAt: Date.now() + PERMISSION_CACHE_TTL_MS });
1169
- }
1170
- return permission;
1171
- } catch {
1172
- this.#permissionCache.delete(cacheKey);
1173
- return void 0;
1174
- }
1175
- }
1176
- async #sendActivityNotifications(input) {
1177
- const agent = this.#getNotificationAgent(input.polling);
1178
- if (!agent?.sendNotificationSignal) return [];
1179
- const notifications = [
1180
- classifyGithubActivityNotification({
1181
- subscription: input.subscription,
1182
- snapshot: input.snapshot
1183
- })
1184
- ];
1185
- if (input.latestCommentChanged && notifications[0]?.kind !== "pull-request-activity") {
1186
- notifications.push(
1187
- classifyGithubCommentActivityNotification({
1188
- subscription: input.subscription,
1189
- snapshot: input.snapshot
1190
- })
1191
- );
1192
- }
1193
- const sent = [];
1194
- const notificationInputs = [];
1195
- for (const notification of notifications.sort(compareGithubActivityNotifications)) {
1196
- if (!notification) continue;
1197
- if (AUTHOR_GATED_NOTIFICATION_KINDS.has(notification.kind)) {
1198
- const authorized = await this.#isAuthorizedAuthor(
1199
- input.subscription.owner,
1200
- input.subscription.repo,
1201
- input.snapshot.latestCommentAuthor,
1202
- {
1203
- authorType: input.snapshot.latestCommentAuthorType,
1204
- isBot: input.snapshot.latestCommentIsBot
1205
- }
1206
- );
1207
- if (!authorized) continue;
1208
- }
1209
- notificationInputs.push(
1210
- this.#createGithubNotificationInput({
1211
- subscription: input.subscription,
1212
- snapshot: input.snapshot,
1213
- notification,
1214
- dedupeSuffix: input.snapshot.contentHash ?? input.snapshot.githubUpdatedAt ?? String(Date.now()),
1215
- previousGithubUpdatedAt: input.previousGithubUpdatedAt,
1216
- previousContentHash: input.previousContentHash
1217
- })
1218
- );
1219
- sent.push(notification);
1220
- }
1221
- if (notificationInputs.length > 0) {
1222
- const target = { resourceId: input.polling.resourceId, threadId: input.polling.threadId };
1223
- const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(target);
1224
- await agent.sendNotificationSignal(
1225
- notificationInputs,
1226
- streamOptions ? { ...target, ifIdle: { streamOptions } } : target
1227
- );
1228
- }
1229
- return sent;
1230
- }
1231
- async #subscribe(input) {
1232
- const { owner, repo } = await this.#resolveRepository(input);
1233
- const { threadStore, loadedThread } = await this.#loadThread(input);
1234
- const githubMetadata = getGithubMetadata(loadedThread.metadata);
1235
- const existingIndex = githubMetadata.subscriptions.findIndex(
1236
- (subscription2) => subscription2.owner === owner && subscription2.repo === repo && subscription2.number === input.number
1237
- );
1238
- const existing = existingIndex >= 0 ? githubMetadata.subscriptions[existingIndex] : void 0;
1239
- if (existing?.lastSubscribeSignalId === input.id) {
1240
- return { owner, repo, number: input.number, subscription: existing, alreadyProcessed: true };
1241
- }
1242
- const now = (/* @__PURE__ */ new Date()).toISOString();
1243
- const subscription = {
1244
- owner,
1245
- repo,
1246
- number: input.number,
1247
- subscribedAt: existing?.subscribedAt ?? now,
1248
- updatedAt: now,
1249
- lastSubscribeSignalId: input.id,
1250
- ...existing?.lastSyncAt ? { lastSyncAt: existing.lastSyncAt } : {},
1251
- ...existing?.lastSyncStatus ? { lastSyncStatus: existing.lastSyncStatus } : {},
1252
- ...existing?.lastSyncError ? { lastSyncError: existing.lastSyncError } : {},
1253
- ...existing?.lastObservedGithubUpdatedAt ? { lastObservedGithubUpdatedAt: existing.lastObservedGithubUpdatedAt } : {},
1254
- ...existing?.lastObservedContentHash ? { lastObservedContentHash: existing.lastObservedContentHash } : {},
1255
- ...existing?.lastObservedThreadContentHash ? { lastObservedThreadContentHash: existing.lastObservedThreadContentHash } : {},
1256
- ...existing?.lastObservedHeadSha ? { lastObservedHeadSha: existing.lastObservedHeadSha } : {},
1257
- ...existing?.lastObservedState ? { lastObservedState: existing.lastObservedState } : {},
1258
- ...existing?.lastObservedMergeableState ? { lastObservedMergeableState: existing.lastObservedMergeableState } : {},
1259
- ...existing?.lastObservedCiState ? { lastObservedCiState: existing.lastObservedCiState } : {},
1260
- ...existing?.lastObservedReviewStateHash ? { lastObservedReviewStateHash: existing.lastObservedReviewStateHash } : {}
1261
- };
1262
- let syncResult;
1263
- let baselineSnapshot;
1264
- if (this.#options.syncOnSubscribe !== false) {
1265
- const syncInput = {
1266
- owner,
1267
- repo,
1268
- number: input.number,
1269
- cwd: this.#options.cwd,
1270
- abortSignal: input.abortSignal
1271
- };
1272
- syncResult = await this.#syncClient.syncPullRequest(syncInput);
1273
- subscription.lastSyncAt = (/* @__PURE__ */ new Date()).toISOString();
1274
- subscription.lastSyncStatus = syncResult.ok ? "success" : "error";
1275
- if (syncResult.error) subscription.lastSyncError = syncResult.error;
1276
- else delete subscription.lastSyncError;
1277
- const snapshot = syncResult.ok ? await this.#syncClient.getPullRequestSnapshot?.(syncInput) : void 0;
1278
- baselineSnapshot = snapshot;
1279
- if (snapshot) applySnapshotCursor(subscription, snapshot);
1280
- } else {
1281
- subscription.lastSyncStatus = "skipped";
1282
- }
1283
- const subscriptions = [subscription];
1284
- await threadStore.saveThread({
1285
- thread: {
1286
- ...loadedThread,
1287
- id: input.threadId,
1288
- resourceId: input.resourceId,
1289
- createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
1290
- updatedAt: /* @__PURE__ */ new Date(),
1291
- metadata: setGithubMetadata(loadedThread.metadata, { subscriptions })
1292
- }
1293
- });
1294
- this.#notifySubscriptionsChanged({ threadId: input.threadId, resourceId: input.resourceId, subscriptions });
1295
- if (baselineSnapshot) {
1296
- await this.#sendBaselineNotification({
1297
- threadId: input.threadId,
1298
- resourceId: input.resourceId,
1299
- subscription,
1300
- snapshot: baselineSnapshot
1301
- });
1302
- }
1303
- await this.startPollingForThread({ threadId: input.threadId, resourceId: input.resourceId });
1304
- return { owner, repo, number: input.number, subscription, syncResult };
1305
- }
1306
- async #unsubscribe(input) {
1307
- const { owner, repo } = await this.#resolveRepository(input);
1308
- const { threadStore, loadedThread } = await this.#loadThread(input);
1309
- const githubMetadata = getGithubMetadata(loadedThread.metadata);
1310
- const subscriptions = githubMetadata.subscriptions.filter(
1311
- (subscription) => !(subscription.owner === owner && subscription.repo === repo && subscription.number === input.number)
1312
- );
1313
- const removed = subscriptions.length !== githubMetadata.subscriptions.length;
1314
- if (removed) {
1315
- await threadStore.saveThread({
1316
- thread: {
1317
- ...loadedThread,
1318
- id: input.threadId,
1319
- resourceId: input.resourceId,
1320
- createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
1321
- updatedAt: /* @__PURE__ */ new Date(),
1322
- metadata: setGithubMetadata(loadedThread.metadata, { subscriptions })
1323
- }
1324
- });
1325
- this.#notifySubscriptionsChanged({ threadId: input.threadId, resourceId: input.resourceId, subscriptions });
1326
- if (subscriptions.length === 0)
1327
- this.stopPollingForThread({ threadId: input.threadId, resourceId: input.resourceId });
1328
- }
1329
- return { owner, repo, number: input.number, removed, remainingSubscriptions: subscriptions.length };
1330
- }
1331
- #findLatestGithubSignal(messages) {
1332
- const message = messages.at(-1);
1333
- if (!message) return void 0;
1334
- const signal = getSignalMetadata(message);
1335
- if (!signal || signal.tagName !== GITHUB_SUBSCRIBE_PR_TAG && signal.tagName !== GITHUB_UNSUBSCRIBE_PR_TAG) {
1336
- return void 0;
1337
- }
1338
- const attributes = isPlainObject(signal.attributes) ? signal.attributes : {};
1339
- const metadata = isPlainObject(signal.metadata) ? signal.metadata : {};
1340
- const github = isPlainObject(metadata.github) ? metadata.github : {};
1341
- const number = readNumber(attributes.number) ?? readNumber(github.number);
1342
- if (!number) return void 0;
1343
- return {
1344
- tagName: String(signal.tagName),
1345
- id: readString(signal.id) ?? message.id,
1346
- owner: readString(attributes.owner) ?? readString(github.owner),
1347
- repo: readString(attributes.repo) ?? readString(github.repo),
1348
- number
1349
- };
1350
- }
1351
- async #sendStatus(args, signal, status) {
1352
- await args.sendSignal?.({
1353
- type: "reactive",
1354
- tagName: GITHUB_SYNC_STATUS_TAG,
1355
- contents: status.message,
1356
- attributes: {
1357
- status: status.status,
1358
- owner: signal.owner,
1359
- repo: signal.repo,
1360
- number: signal.number
1361
- },
1362
- metadata: {
1363
- github: {
1364
- action: status.action,
1365
- status: status.status,
1366
- owner: signal.owner,
1367
- repo: signal.repo,
1368
- number: signal.number
1369
- }
1370
- }
1371
- });
1372
- }
683
+ var GithubSignals = class extends _mastra_core_signals.SignalProvider {
684
+ id = "github-signals";
685
+ name = "GitHub Signals";
686
+ #ghMastra;
687
+ static signals = {
688
+ subscribeToPR(input) {
689
+ const normalized = typeof input === "number" ? { number: input } : input;
690
+ return {
691
+ type: "reactive",
692
+ tagName: GITHUB_SUBSCRIBE_PR_TAG,
693
+ contents: `Subscribe to GitHub PR #${normalized.number}`,
694
+ attributes: {
695
+ ...normalized.owner ? { owner: normalized.owner } : {},
696
+ ...normalized.repo ? { repo: normalized.repo } : {},
697
+ number: normalized.number
698
+ },
699
+ metadata: { github: {
700
+ action: "subscribeToPR",
701
+ ...normalized
702
+ } }
703
+ };
704
+ },
705
+ unsubscribeFromPR(input) {
706
+ const normalized = typeof input === "number" ? { number: input } : input;
707
+ return {
708
+ type: "reactive",
709
+ tagName: GITHUB_UNSUBSCRIBE_PR_TAG,
710
+ contents: `Unsubscribe from GitHub PR #${normalized.number}`,
711
+ attributes: {
712
+ ...normalized.owner ? { owner: normalized.owner } : {},
713
+ ...normalized.repo ? { repo: normalized.repo } : {},
714
+ number: normalized.number
715
+ },
716
+ metadata: { github: {
717
+ action: "unsubscribeFromPR",
718
+ ...normalized
719
+ } }
720
+ };
721
+ }
722
+ };
723
+ #options;
724
+ #syncClient;
725
+ #repositoryResolver;
726
+ #polling = /* @__PURE__ */ new Map();
727
+ #permissionCache = /* @__PURE__ */ new Map();
728
+ #agent;
729
+ #agentOptions = {};
730
+ #subscriptionsChangedHandler;
731
+ #pollingChangedHandler;
732
+ constructor(options = {}) {
733
+ super();
734
+ this.#options = options;
735
+ this.#syncClient = options.syncClient ?? new GitcrawlSyncClient({ command: options.gitcrawlCommand });
736
+ this.#repositoryResolver = options.repositoryResolver ?? new GitRemoteRepositoryResolver();
737
+ if (options.getNotificationStreamOptions) this.#agentOptions = { getNotificationStreamOptions: options.getNotificationStreamOptions };
738
+ }
739
+ /**
740
+ * @deprecated Use `Agent({ signals: [githubSignals] })` instead.
741
+ * Kept for backward compatibility.
742
+ */
743
+ addAgent(agent, options = {}) {
744
+ this.#agent = agent;
745
+ this.#agentOptions = options;
746
+ }
747
+ /**
748
+ * Called by the Agent constructor when this provider is passed via `signals: [...]`.
749
+ * Sets the bidirectional link so the provider can send signals back to the agent.
750
+ */
751
+ connect(agent) {
752
+ super.connect(agent);
753
+ this.#agent = agent;
754
+ }
755
+ getInputProcessors() {
756
+ return [this];
757
+ }
758
+ getOutputProcessors() {
759
+ return [this];
760
+ }
761
+ onSubscriptionsChanged(handler) {
762
+ this.#subscriptionsChangedHandler = handler;
763
+ }
764
+ onPollingChanged(handler) {
765
+ this.#pollingChangedHandler = handler;
766
+ }
767
+ __registerMastra(mastra) {
768
+ super.__registerMastra(mastra);
769
+ this.#ghMastra = mastra;
770
+ }
771
+ async syncThreadNow(input) {
772
+ return this.#pollThread(input, { includeComments: true });
773
+ }
774
+ async subscribeThreadToPR(input) {
775
+ const pr = typeof input.pr === "number" ? { number: input.pr } : input.pr;
776
+ return this.#subscribe({
777
+ id: `github-command-subscribe-${(0, crypto.randomUUID)()}`,
778
+ ...pr,
779
+ threadId: input.threadId,
780
+ resourceId: input.resourceId
781
+ });
782
+ }
783
+ async unsubscribeThreadFromPR(input) {
784
+ const pr = typeof input.pr === "number" ? { number: input.pr } : input.pr;
785
+ return this.#unsubscribe({
786
+ id: `github-command-unsubscribe-${(0, crypto.randomUUID)()}`,
787
+ ...pr,
788
+ threadId: input.threadId,
789
+ resourceId: input.resourceId
790
+ });
791
+ }
792
+ async startPollingForThread(input, options = {}) {
793
+ if ((await this.#getThreadSubscriptions(input)).length === 0) {
794
+ this.stopPollingForThread(input);
795
+ return false;
796
+ }
797
+ const key = this.#pollingKey(input);
798
+ for (const [pollingKey, state] of this.#polling.entries()) {
799
+ if (pollingKey === key) continue;
800
+ clearInterval(state.timer);
801
+ this.#polling.delete(pollingKey);
802
+ }
803
+ if (this.#polling.has(key)) return true;
804
+ const runPoll = (pollOptions = {}) => {
805
+ this.#pollThread(input, pollOptions).catch((error) => {
806
+ console.warn("GitHub PR polling failed:", error);
807
+ });
808
+ };
809
+ const timer = setInterval(() => {
810
+ runPoll({ includeComments: true });
811
+ }, this.#options.pollIntervalMs ?? 3e5);
812
+ if (options.pollImmediately) runPoll({ includeComments: true });
813
+ timer.unref?.();
814
+ this.#polling.set(key, {
815
+ ...input,
816
+ timer,
817
+ running: false
818
+ });
819
+ return true;
820
+ }
821
+ stopPollingForThread(input) {
822
+ const key = this.#pollingKey(input);
823
+ const state = this.#polling.get(key);
824
+ if (!state) return;
825
+ clearInterval(state.timer);
826
+ this.#polling.delete(key);
827
+ }
828
+ isPollingThread(input) {
829
+ return this.#polling.has(this.#pollingKey(input));
830
+ }
831
+ isPollingThreadRunning(input) {
832
+ return this.#polling.get(this.#pollingKey(input))?.running ?? false;
833
+ }
834
+ getPollIntervalMs() {
835
+ return this.#options.pollIntervalMs ?? 3e5;
836
+ }
837
+ stopAllPolling() {
838
+ for (const state of this.#polling.values()) clearInterval(state.timer);
839
+ this.#polling.clear();
840
+ }
841
+ async pollThreadNow(input) {
842
+ return this.#pollThread(input, { includeComments: true });
843
+ }
844
+ async processInputStep(args) {
845
+ const tools = this.#createTools(args);
846
+ if (args.stepNumber !== 0) return { tools };
847
+ const signal = this.#findLatestGithubSignal(args.messages);
848
+ if (!signal) return { tools };
849
+ const threadContext = this.#getThreadContext(args);
850
+ if (signal.tagName === "github-unsubscribe-pr") {
851
+ const result = await this.#unsubscribe({
852
+ ...signal,
853
+ ...threadContext,
854
+ abortSignal: args.abortSignal
855
+ });
856
+ await this.#sendStatus(args, result, {
857
+ status: result.removed ? "unsubscribed" : "not_subscribed",
858
+ action: "unsubscribeFromPR",
859
+ message: result.removed ? `Unsubscribed from ${result.owner}/${result.repo}#${result.number}.` : `No GitHub subscription found for ${result.owner}/${result.repo}#${result.number}.`
860
+ });
861
+ return { tools };
862
+ }
863
+ const result = await this.#subscribe({
864
+ ...signal,
865
+ ...threadContext,
866
+ abortSignal: args.abortSignal
867
+ });
868
+ if (result.alreadyProcessed) return { tools };
869
+ await this.#sendStatus(args, result, {
870
+ status: result.syncResult?.ok === false ? "sync_error" : "subscribed",
871
+ action: "subscribeToPR",
872
+ message: result.syncResult?.ok === false ? `Subscribed to ${result.owner}/${result.repo}#${result.number}, but gitcrawl sync failed: ${result.syncResult.error}` : `Subscribed to ${result.owner}/${result.repo}#${result.number}.`
873
+ });
874
+ return { tools };
875
+ }
876
+ async processOutputStep(args) {
877
+ const evidence = detectPrWorkEvidence({
878
+ text: args.text,
879
+ toolCalls: args.toolCalls
880
+ });
881
+ if (!evidence) return;
882
+ const threadContext = this.#getThreadContext(args);
883
+ if (!threadContext.threadId || !threadContext.resourceId) return;
884
+ const { threadStore, loadedThread } = await this.#loadThread(threadContext);
885
+ const githubMetadata = getGithubMetadata(loadedThread.metadata);
886
+ if (githubMetadata.subscriptionHintShown || githubMetadata.subscriptions.length > 0) return;
887
+ let repository;
888
+ try {
889
+ repository = await this.#resolveRepository({
890
+ id: "github-subscription-hint",
891
+ owner: evidence.owner,
892
+ repo: evidence.repo,
893
+ number: evidence.number
894
+ });
895
+ } catch {
896
+ return;
897
+ }
898
+ await threadStore.saveThread({ thread: {
899
+ ...loadedThread,
900
+ id: threadContext.threadId,
901
+ resourceId: threadContext.resourceId,
902
+ createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
903
+ updatedAt: /* @__PURE__ */ new Date(),
904
+ metadata: setGithubMetadata(loadedThread.metadata, {
905
+ ...githubMetadata,
906
+ subscriptionHintShown: true
907
+ })
908
+ } });
909
+ await args.sendSignal?.({
910
+ type: "reactive",
911
+ tagName: "system-reminder",
912
+ contents: `Looks like you're working with ${repository.owner}/${repository.repo}#${evidence.number}. Use /github subscribe ${evidence.number} or the github_subscribe_pr tool to follow updates.`,
913
+ attributes: { type: "github-subscription-hint" },
914
+ metadata: { github: {
915
+ action: "subscriptionHint",
916
+ owner: repository.owner,
917
+ repo: repository.repo,
918
+ number: evidence.number
919
+ } }
920
+ });
921
+ }
922
+ async #resolveThreadStore() {
923
+ if (this.#options.threadStore) return this.#options.threadStore;
924
+ const storage = this.#ghMastra?.getStorage?.();
925
+ return storage?.getStore ? await storage.getStore("memory") : void 0;
926
+ }
927
+ #getThreadContext(args) {
928
+ const memoryContext = args.requestContext?.get("MastraMemory");
929
+ return {
930
+ threadId: memoryContext?.thread?.id,
931
+ resourceId: memoryContext?.resourceId
932
+ };
933
+ }
934
+ #createTools(args) {
935
+ const threadContext = this.#getThreadContext(args);
936
+ const getExecutionThreadContext = (context) => ({
937
+ threadId: context?.agent?.threadId ?? threadContext.threadId,
938
+ resourceId: context?.agent?.resourceId ?? threadContext.resourceId
939
+ });
940
+ return {
941
+ ...args.tools,
942
+ github_subscribe_pr: createGithubTool({
943
+ id: "github_subscribe_pr",
944
+ description: "Subscribe this thread to a GitHub pull request. Syncs only the requested PR with gitcrawl and stores the subscription on the thread.",
945
+ inputSchema: zod.default.object({
946
+ number: zod.default.number().int().positive(),
947
+ owner: zod.default.string().optional(),
948
+ repo: zod.default.string().optional()
949
+ }),
950
+ execute: async (input, context) => {
951
+ const executionThreadContext = getExecutionThreadContext(context);
952
+ const result = await this.#subscribe({
953
+ id: `github-tool-subscribe-${(0, crypto.randomUUID)()}`,
954
+ owner: input.owner,
955
+ repo: input.repo,
956
+ number: input.number,
957
+ threadId: executionThreadContext.threadId,
958
+ resourceId: executionThreadContext.resourceId
959
+ });
960
+ return {
961
+ subscribed: true,
962
+ owner: result.owner,
963
+ repo: result.repo,
964
+ number: result.number,
965
+ syncStatus: result.syncResult?.ok === false ? "error" : result.syncResult ? "success" : void 0,
966
+ message: result.syncResult?.ok === false ? `Subscribed to ${result.owner}/${result.repo}#${result.number}, but gitcrawl sync failed: ${result.syncResult.error}` : `Subscribed to ${result.owner}/${result.repo}#${result.number}.`
967
+ };
968
+ }
969
+ }),
970
+ github_unsubscribe_pr: createGithubTool({
971
+ id: "github_unsubscribe_pr",
972
+ description: "Unsubscribe this thread from a GitHub pull request.",
973
+ inputSchema: zod.default.object({
974
+ number: zod.default.number().int().positive(),
975
+ owner: zod.default.string().optional(),
976
+ repo: zod.default.string().optional()
977
+ }),
978
+ execute: async (input, context) => {
979
+ const executionThreadContext = getExecutionThreadContext(context);
980
+ const result = await this.#unsubscribe({
981
+ id: `github-tool-unsubscribe-${(0, crypto.randomUUID)()}`,
982
+ owner: input.owner,
983
+ repo: input.repo,
984
+ number: input.number,
985
+ threadId: executionThreadContext.threadId,
986
+ resourceId: executionThreadContext.resourceId
987
+ });
988
+ return {
989
+ unsubscribed: result.removed ?? false,
990
+ owner: result.owner,
991
+ repo: result.repo,
992
+ number: result.number,
993
+ remainingSubscriptions: result.remainingSubscriptions,
994
+ message: result.removed ? `Unsubscribed from ${result.owner}/${result.repo}#${result.number}.` : `No GitHub subscription found for ${result.owner}/${result.repo}#${result.number}.`
995
+ };
996
+ }
997
+ })
998
+ };
999
+ }
1000
+ async #resolveRepository(input) {
1001
+ const resolvedRepository = input.owner && input.repo ? {
1002
+ owner: input.owner,
1003
+ repo: input.repo
1004
+ } : this.#options.owner && this.#options.repo ? {
1005
+ owner: this.#options.owner,
1006
+ repo: this.#options.repo
1007
+ } : await this.#repositoryResolver.resolveRepository({
1008
+ cwd: this.#options.cwd,
1009
+ abortSignal: input.abortSignal
1010
+ });
1011
+ if (!resolvedRepository?.owner || !resolvedRepository.repo) throw new Error("GitHub PR subscription requires owner and repo. Run inside a GitHub repo or pass owner and repo.");
1012
+ return resolvedRepository;
1013
+ }
1014
+ async #loadThread(input) {
1015
+ const threadStore = await this.#resolveThreadStore();
1016
+ if (!threadStore) throw new Error("GitHub PR subscription requires memory-backed thread storage.");
1017
+ if (!input.threadId || !input.resourceId) throw new Error("GitHub PR subscription requires threadId and resourceId.");
1018
+ const loadedThread = await threadStore.getThreadById({
1019
+ threadId: input.threadId,
1020
+ resourceId: input.resourceId
1021
+ }) ?? void 0;
1022
+ if (!loadedThread) throw new Error(`Could not load thread ${input.threadId}.`);
1023
+ return {
1024
+ threadStore,
1025
+ loadedThread
1026
+ };
1027
+ }
1028
+ #pollingKey(input) {
1029
+ return `${input.resourceId}:${input.threadId}`;
1030
+ }
1031
+ #getNotificationAgent(_input) {
1032
+ if (this.#agent) return this.#agent;
1033
+ const agentId = _input?.agentId ?? this.#options.agentId;
1034
+ return agentId ? this.#ghMastra?.getAgentById?.(agentId) : void 0;
1035
+ }
1036
+ async #getThreadSubscriptions(input) {
1037
+ const { loadedThread } = await this.#loadThread(input);
1038
+ return getGithubMetadata(loadedThread.metadata).subscriptions;
1039
+ }
1040
+ #notifySubscriptionsChanged(input) {
1041
+ this.#subscriptionsChangedHandler?.(input);
1042
+ }
1043
+ #notifyPollingChanged(input) {
1044
+ this.#pollingChangedHandler?.(input);
1045
+ }
1046
+ async #pollThread(input, options = {}) {
1047
+ const key = this.#pollingKey(input);
1048
+ const state = this.#polling.get(key);
1049
+ if (state?.running) return 0;
1050
+ if (state) state.running = true;
1051
+ this.#notifyPollingChanged({
1052
+ threadId: input.threadId,
1053
+ resourceId: input.resourceId,
1054
+ running: true
1055
+ });
1056
+ try {
1057
+ const { threadStore, loadedThread } = await this.#loadThread(input);
1058
+ const githubMetadata = getGithubMetadata(loadedThread.metadata);
1059
+ if (githubMetadata.subscriptions.length === 0) {
1060
+ this.stopPollingForThread(input);
1061
+ return 0;
1062
+ }
1063
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1064
+ const subscriptions = [];
1065
+ for (const subscription of githubMetadata.subscriptions) {
1066
+ const syncInput = {
1067
+ owner: subscription.owner,
1068
+ repo: subscription.repo,
1069
+ number: subscription.number,
1070
+ cwd: this.#options.cwd,
1071
+ includeComments: options.includeComments
1072
+ };
1073
+ const syncResult = await this.#syncClient.syncPullRequest(syncInput);
1074
+ let snapshot;
1075
+ let snapshotError;
1076
+ if (syncResult.ok) try {
1077
+ snapshot = await this.#syncClient.getPullRequestSnapshot?.(syncInput);
1078
+ } catch (error) {
1079
+ snapshotError = error instanceof Error ? error.message : String(error);
1080
+ }
1081
+ if (snapshot) snapshot = await this.#filterUnauthorizedLatestComment(subscription.owner, subscription.repo, snapshot);
1082
+ const nextSubscription = {
1083
+ ...subscription,
1084
+ updatedAt: now,
1085
+ lastSyncAt: now,
1086
+ lastSyncStatus: syncResult.ok ? "success" : "error"
1087
+ };
1088
+ if (syncResult.error) nextSubscription.lastSyncError = syncResult.error;
1089
+ else delete nextSubscription.lastSyncError;
1090
+ if (snapshotError) nextSubscription.lastSnapshotError = snapshotError;
1091
+ else delete nextSubscription.lastSnapshotError;
1092
+ const previousGithubUpdatedAt = subscription.lastObservedGithubUpdatedAt;
1093
+ const previousContentHash = subscription.lastObservedContentHash;
1094
+ const previousThreadContentHash = subscription.lastObservedThreadContentHash;
1095
+ const previousHeadSha = subscription.lastObservedHeadSha;
1096
+ const latestCommentChanged = !!previousGithubUpdatedAt && !!snapshot?.latestCommentUpdatedAt && Date.parse(snapshot.latestCommentUpdatedAt) > Date.parse(previousGithubUpdatedAt);
1097
+ if (snapshot) applySnapshotCursor(nextSubscription, snapshot);
1098
+ const isFirstObservation = syncResult.ok && snapshot && !previousGithubUpdatedAt && !previousContentHash;
1099
+ const legacyAggregateChanged = previousContentHash && snapshot?.contentHash && previousContentHash !== snapshot.contentHash && !previousThreadContentHash && !previousHeadSha;
1100
+ const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || latestCommentChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
1101
+ let shouldKeepSubscription = true;
1102
+ if (changed && snapshot) {
1103
+ const notifications = await this.#sendActivityNotifications({
1104
+ polling: input,
1105
+ subscription,
1106
+ snapshot,
1107
+ previousGithubUpdatedAt,
1108
+ previousContentHash,
1109
+ latestCommentChanged
1110
+ });
1111
+ const primaryNotification = notifications[0];
1112
+ if (primaryNotification) {
1113
+ nextSubscription.lastNotificationAt = now;
1114
+ nextSubscription.lastNotificationKind = primaryNotification.kind;
1115
+ nextSubscription.lastNotificationPriority = primaryNotification.priority;
1116
+ nextSubscription.lastNotificationSummary = primaryNotification.summary;
1117
+ shouldKeepSubscription = notifications.every((notification) => notification.kind !== "pull-request-merged");
1118
+ }
1119
+ }
1120
+ if (shouldKeepSubscription) subscriptions.push(nextSubscription);
1121
+ }
1122
+ await threadStore.saveThread({ thread: {
1123
+ ...loadedThread,
1124
+ id: input.threadId,
1125
+ resourceId: input.resourceId,
1126
+ createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
1127
+ updatedAt: /* @__PURE__ */ new Date(),
1128
+ metadata: setGithubMetadata(loadedThread.metadata, { subscriptions })
1129
+ } });
1130
+ this.#notifySubscriptionsChanged({
1131
+ threadId: input.threadId,
1132
+ resourceId: input.resourceId,
1133
+ subscriptions
1134
+ });
1135
+ if (subscriptions.length === 0) this.stopPollingForThread(input);
1136
+ return subscriptions.length;
1137
+ } catch (error) {
1138
+ throw error;
1139
+ } finally {
1140
+ const latestState = this.#polling.get(key);
1141
+ if (latestState) latestState.running = false;
1142
+ this.#notifyPollingChanged({
1143
+ threadId: input.threadId,
1144
+ resourceId: input.resourceId,
1145
+ running: false
1146
+ });
1147
+ }
1148
+ }
1149
+ #createGithubNotificationInput(input) {
1150
+ const failingChecks = getFailingChecks(input.snapshot);
1151
+ const pendingChecks = getPendingChecks(input.snapshot);
1152
+ const latestCommentExcerpt = input.snapshot.latestCommentBody ? getCommentExcerpt(input.snapshot.latestCommentBody) : void 0;
1153
+ const latestCommentDedupeSuffix = input.notification.kind === "pull-request-activity" && input.snapshot.latestCommentUrl ? `comment:${input.snapshot.latestCommentUrl}:${input.snapshot.latestCommentUpdatedAt ?? ""}` : input.dedupeSuffix;
1154
+ return {
1155
+ source: "github",
1156
+ kind: input.notification.kind,
1157
+ priority: input.notification.priority,
1158
+ summary: input.notification.summary,
1159
+ dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${latestCommentDedupeSuffix}`,
1160
+ coalesceKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${input.notification.kind}`,
1161
+ attributes: {
1162
+ owner: input.subscription.owner,
1163
+ repo: input.subscription.repo,
1164
+ number: input.subscription.number,
1165
+ ...input.snapshot.title ? { title: input.snapshot.title } : {},
1166
+ ...input.snapshot.state ? { state: input.snapshot.state } : {},
1167
+ ...input.snapshot.htmlUrl ? { url: input.snapshot.htmlUrl } : {},
1168
+ ...input.snapshot.githubUpdatedAt ? { githubUpdatedAt: input.snapshot.githubUpdatedAt } : {},
1169
+ ...input.previousGithubUpdatedAt ? { previousGithubUpdatedAt: input.previousGithubUpdatedAt } : {},
1170
+ ...input.snapshot.mergeableState ? { mergeableState: input.snapshot.mergeableState } : {},
1171
+ ...input.snapshot.ciState ? { ciState: input.snapshot.ciState } : {},
1172
+ ...input.snapshot.unresolvedReviewThreads !== void 0 ? { unresolvedReviewThreads: input.snapshot.unresolvedReviewThreads } : {},
1173
+ ...input.snapshot.latestCommentAuthor ? { latestCommentAuthor: input.snapshot.latestCommentAuthor } : {},
1174
+ ...latestCommentExcerpt ? { latestCommentExcerpt } : {},
1175
+ ...input.snapshot.latestCommentUrl ? { latestCommentUrl: input.snapshot.latestCommentUrl } : {},
1176
+ ...input.snapshot.latestCommentUpdatedAt ? { latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt } : {},
1177
+ ...failingChecks.length > 0 ? { failingChecks: failingChecks.map((check) => check.name).join(", ") } : {},
1178
+ ...pendingChecks.length > 0 ? { pendingChecks: pendingChecks.map((check) => check.name).join(", ") } : {}
1179
+ },
1180
+ metadata: { github: {
1181
+ owner: input.subscription.owner,
1182
+ repo: input.subscription.repo,
1183
+ number: input.subscription.number,
1184
+ title: input.snapshot.title,
1185
+ state: input.snapshot.state,
1186
+ htmlUrl: input.snapshot.htmlUrl,
1187
+ githubUpdatedAt: input.snapshot.githubUpdatedAt,
1188
+ previousGithubUpdatedAt: input.previousGithubUpdatedAt,
1189
+ contentHash: input.snapshot.contentHash,
1190
+ previousContentHash: input.previousContentHash,
1191
+ threadContentHash: input.snapshot.threadContentHash,
1192
+ headSha: input.snapshot.headSha,
1193
+ headRef: input.snapshot.headRef,
1194
+ mergeableState: input.snapshot.mergeableState,
1195
+ ciState: input.snapshot.ciState,
1196
+ closedAt: input.snapshot.closedAt,
1197
+ mergedAt: input.snapshot.mergedAt,
1198
+ unresolvedReviewThreads: input.snapshot.unresolvedReviewThreads,
1199
+ reviewStateHash: input.snapshot.reviewStateHash,
1200
+ latestReviewThreadAt: input.snapshot.latestReviewThreadAt,
1201
+ latestCommentAuthor: input.snapshot.latestCommentAuthor,
1202
+ latestCommentAuthorType: input.snapshot.latestCommentAuthorType,
1203
+ latestCommentIsBot: input.snapshot.latestCommentIsBot,
1204
+ latestCommentExcerpt,
1205
+ latestCommentUrl: input.snapshot.latestCommentUrl,
1206
+ latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt,
1207
+ failingChecks,
1208
+ pendingChecks
1209
+ } }
1210
+ };
1211
+ }
1212
+ async #sendGithubNotification(input) {
1213
+ const notificationInput = this.#createGithubNotificationInput(input);
1214
+ const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(input.target);
1215
+ await input.agent?.sendNotificationSignal?.(notificationInput, streamOptions ? {
1216
+ ...input.target,
1217
+ ifIdle: { streamOptions }
1218
+ } : input.target);
1219
+ }
1220
+ async #sendBaselineNotification(input) {
1221
+ const agent = this.#getNotificationAgent({});
1222
+ if (!agent?.sendNotificationSignal) return;
1223
+ await this.#sendGithubNotification({
1224
+ agent,
1225
+ subscription: input.subscription,
1226
+ snapshot: input.snapshot,
1227
+ notification: classifyGithubBaselineNotification({
1228
+ subscription: input.subscription,
1229
+ snapshot: input.snapshot
1230
+ }),
1231
+ target: {
1232
+ resourceId: input.resourceId,
1233
+ threadId: input.threadId
1234
+ },
1235
+ dedupeSuffix: `baseline:${input.subscription.lastSubscribeSignalId}`
1236
+ });
1237
+ }
1238
+ async #isAuthorizedAuthor(owner, repo, user, metadata = {}) {
1239
+ if (!user) return false;
1240
+ const normalizedUser = user.toLowerCase();
1241
+ if (metadata.isBot === true || metadata.authorType?.toLowerCase() === "bot" || normalizedUser.endsWith("[bot]")) {
1242
+ if ((this.#options.ignoredBots ?? []).some((bot) => bot.toLowerCase() === normalizedUser)) return false;
1243
+ return (this.#options.authorizedBots ?? DEFAULT_AUTHORIZED_BOTS).some((bot) => bot.toLowerCase() === normalizedUser);
1244
+ }
1245
+ const permission = await this.#loadAuthorPermission(owner, repo, user);
1246
+ const authorizedPermissions = this.#options.authorizedPermissions ?? DEFAULT_AUTHORIZED_PERMISSIONS;
1247
+ return !!permission && authorizedPermissions.includes(permission);
1248
+ }
1249
+ async #filterUnauthorizedLatestComment(owner, repo, snapshot) {
1250
+ const comments = snapshot.latestComments?.length ? snapshot.latestComments : [{
1251
+ author: snapshot.latestCommentAuthor,
1252
+ authorType: snapshot.latestCommentAuthorType,
1253
+ isBot: snapshot.latestCommentIsBot,
1254
+ body: snapshot.latestCommentBody,
1255
+ url: snapshot.latestCommentUrl,
1256
+ updatedAt: snapshot.latestCommentUpdatedAt
1257
+ }];
1258
+ if (!comments.some((comment) => comment.author)) return snapshot;
1259
+ if (!comments.some((comment) => comment.body || comment.url || comment.updatedAt)) return snapshot;
1260
+ for (const comment of comments) {
1261
+ if (!await this.#isAuthorizedAuthor(owner, repo, comment.author, {
1262
+ authorType: comment.authorType,
1263
+ isBot: comment.isBot
1264
+ })) continue;
1265
+ return {
1266
+ ...snapshot,
1267
+ latestCommentAuthor: comment.author,
1268
+ latestCommentAuthorType: comment.authorType,
1269
+ latestCommentIsBot: comment.isBot,
1270
+ latestCommentBody: comment.body,
1271
+ latestCommentUrl: comment.url,
1272
+ latestCommentUpdatedAt: comment.updatedAt
1273
+ };
1274
+ }
1275
+ return {
1276
+ ...snapshot,
1277
+ latestCommentAuthor: void 0,
1278
+ latestCommentAuthorType: void 0,
1279
+ latestCommentIsBot: void 0,
1280
+ latestCommentBody: void 0,
1281
+ latestCommentUrl: void 0,
1282
+ latestCommentUpdatedAt: void 0
1283
+ };
1284
+ }
1285
+ async #loadAuthorPermission(owner, repo, user) {
1286
+ const cacheKey = `${owner}/${repo}:${user.toLowerCase()}`;
1287
+ const cached = this.#permissionCache.get(cacheKey);
1288
+ if (cached && cached.expiresAt > Date.now()) return cached.permission;
1289
+ if (cached) this.#permissionCache.delete(cacheKey);
1290
+ try {
1291
+ let permission;
1292
+ if (this.#options.permissionResolver) permission = await this.#options.permissionResolver.getPermission(owner, repo, user);
1293
+ else {
1294
+ const { stdout } = await execFileAsync("gh", [
1295
+ "api",
1296
+ `repos/${owner}/${repo}/collaborators/${user}/permission`,
1297
+ "--jq",
1298
+ ".permission"
1299
+ ]);
1300
+ const raw = stdout.trim();
1301
+ permission = [
1302
+ "admin",
1303
+ "maintain",
1304
+ "write",
1305
+ "triage",
1306
+ "read",
1307
+ "none"
1308
+ ].includes(raw) ? raw : void 0;
1309
+ }
1310
+ if (permission) this.#permissionCache.set(cacheKey, {
1311
+ permission,
1312
+ expiresAt: Date.now() + PERMISSION_CACHE_TTL_MS
1313
+ });
1314
+ return permission;
1315
+ } catch {
1316
+ this.#permissionCache.delete(cacheKey);
1317
+ return;
1318
+ }
1319
+ }
1320
+ async #sendActivityNotifications(input) {
1321
+ const agent = this.#getNotificationAgent(input.polling);
1322
+ if (!agent?.sendNotificationSignal) return [];
1323
+ const notifications = [classifyGithubActivityNotification({
1324
+ subscription: input.subscription,
1325
+ snapshot: input.snapshot
1326
+ })];
1327
+ if (input.latestCommentChanged && notifications[0]?.kind !== "pull-request-activity") notifications.push(classifyGithubCommentActivityNotification({
1328
+ subscription: input.subscription,
1329
+ snapshot: input.snapshot
1330
+ }));
1331
+ const sent = [];
1332
+ const notificationInputs = [];
1333
+ for (const notification of notifications.sort(compareGithubActivityNotifications)) {
1334
+ if (!notification) continue;
1335
+ if (AUTHOR_GATED_NOTIFICATION_KINDS.has(notification.kind)) {
1336
+ if (!await this.#isAuthorizedAuthor(input.subscription.owner, input.subscription.repo, input.snapshot.latestCommentAuthor, {
1337
+ authorType: input.snapshot.latestCommentAuthorType,
1338
+ isBot: input.snapshot.latestCommentIsBot
1339
+ })) continue;
1340
+ }
1341
+ notificationInputs.push(this.#createGithubNotificationInput({
1342
+ subscription: input.subscription,
1343
+ snapshot: input.snapshot,
1344
+ notification,
1345
+ dedupeSuffix: input.snapshot.contentHash ?? input.snapshot.githubUpdatedAt ?? String(Date.now()),
1346
+ previousGithubUpdatedAt: input.previousGithubUpdatedAt,
1347
+ previousContentHash: input.previousContentHash
1348
+ }));
1349
+ sent.push(notification);
1350
+ }
1351
+ if (notificationInputs.length > 0) {
1352
+ const target = {
1353
+ resourceId: input.polling.resourceId,
1354
+ threadId: input.polling.threadId
1355
+ };
1356
+ const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(target);
1357
+ await agent.sendNotificationSignal(notificationInputs, streamOptions ? {
1358
+ ...target,
1359
+ ifIdle: { streamOptions }
1360
+ } : target);
1361
+ }
1362
+ return sent;
1363
+ }
1364
+ async #subscribe(input) {
1365
+ const { owner, repo } = await this.#resolveRepository(input);
1366
+ const { threadStore, loadedThread } = await this.#loadThread(input);
1367
+ const githubMetadata = getGithubMetadata(loadedThread.metadata);
1368
+ const existingIndex = githubMetadata.subscriptions.findIndex((subscription) => subscription.owner === owner && subscription.repo === repo && subscription.number === input.number);
1369
+ const existing = existingIndex >= 0 ? githubMetadata.subscriptions[existingIndex] : void 0;
1370
+ if (existing?.lastSubscribeSignalId === input.id) return {
1371
+ owner,
1372
+ repo,
1373
+ number: input.number,
1374
+ subscription: existing,
1375
+ alreadyProcessed: true
1376
+ };
1377
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1378
+ const subscription = {
1379
+ owner,
1380
+ repo,
1381
+ number: input.number,
1382
+ subscribedAt: existing?.subscribedAt ?? now,
1383
+ updatedAt: now,
1384
+ lastSubscribeSignalId: input.id,
1385
+ ...existing?.lastSyncAt ? { lastSyncAt: existing.lastSyncAt } : {},
1386
+ ...existing?.lastSyncStatus ? { lastSyncStatus: existing.lastSyncStatus } : {},
1387
+ ...existing?.lastSyncError ? { lastSyncError: existing.lastSyncError } : {},
1388
+ ...existing?.lastObservedGithubUpdatedAt ? { lastObservedGithubUpdatedAt: existing.lastObservedGithubUpdatedAt } : {},
1389
+ ...existing?.lastObservedContentHash ? { lastObservedContentHash: existing.lastObservedContentHash } : {},
1390
+ ...existing?.lastObservedThreadContentHash ? { lastObservedThreadContentHash: existing.lastObservedThreadContentHash } : {},
1391
+ ...existing?.lastObservedHeadSha ? { lastObservedHeadSha: existing.lastObservedHeadSha } : {},
1392
+ ...existing?.lastObservedState ? { lastObservedState: existing.lastObservedState } : {},
1393
+ ...existing?.lastObservedMergeableState ? { lastObservedMergeableState: existing.lastObservedMergeableState } : {},
1394
+ ...existing?.lastObservedCiState ? { lastObservedCiState: existing.lastObservedCiState } : {},
1395
+ ...existing?.lastObservedReviewStateHash ? { lastObservedReviewStateHash: existing.lastObservedReviewStateHash } : {}
1396
+ };
1397
+ let syncResult;
1398
+ let baselineSnapshot;
1399
+ if (this.#options.syncOnSubscribe !== false) {
1400
+ const syncInput = {
1401
+ owner,
1402
+ repo,
1403
+ number: input.number,
1404
+ cwd: this.#options.cwd,
1405
+ abortSignal: input.abortSignal
1406
+ };
1407
+ syncResult = await this.#syncClient.syncPullRequest(syncInput);
1408
+ subscription.lastSyncAt = (/* @__PURE__ */ new Date()).toISOString();
1409
+ subscription.lastSyncStatus = syncResult.ok ? "success" : "error";
1410
+ if (syncResult.error) subscription.lastSyncError = syncResult.error;
1411
+ else delete subscription.lastSyncError;
1412
+ let snapshot;
1413
+ if (syncResult.ok) try {
1414
+ snapshot = await this.#syncClient.getPullRequestSnapshot?.(syncInput);
1415
+ } catch (error) {
1416
+ subscription.lastSnapshotError = error instanceof Error ? error.message : String(error);
1417
+ }
1418
+ if (snapshot) delete subscription.lastSnapshotError;
1419
+ baselineSnapshot = snapshot;
1420
+ if (snapshot) applySnapshotCursor(subscription, snapshot);
1421
+ } else subscription.lastSyncStatus = "skipped";
1422
+ const subscriptions = [subscription];
1423
+ await threadStore.saveThread({ thread: {
1424
+ ...loadedThread,
1425
+ id: input.threadId,
1426
+ resourceId: input.resourceId,
1427
+ createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
1428
+ updatedAt: /* @__PURE__ */ new Date(),
1429
+ metadata: setGithubMetadata(loadedThread.metadata, { subscriptions })
1430
+ } });
1431
+ this.#notifySubscriptionsChanged({
1432
+ threadId: input.threadId,
1433
+ resourceId: input.resourceId,
1434
+ subscriptions
1435
+ });
1436
+ if (baselineSnapshot) await this.#sendBaselineNotification({
1437
+ threadId: input.threadId,
1438
+ resourceId: input.resourceId,
1439
+ subscription,
1440
+ snapshot: baselineSnapshot
1441
+ });
1442
+ await this.startPollingForThread({
1443
+ threadId: input.threadId,
1444
+ resourceId: input.resourceId
1445
+ });
1446
+ return {
1447
+ owner,
1448
+ repo,
1449
+ number: input.number,
1450
+ subscription,
1451
+ syncResult
1452
+ };
1453
+ }
1454
+ async #unsubscribe(input) {
1455
+ const { owner, repo } = await this.#resolveRepository(input);
1456
+ const { threadStore, loadedThread } = await this.#loadThread(input);
1457
+ const githubMetadata = getGithubMetadata(loadedThread.metadata);
1458
+ const subscriptions = githubMetadata.subscriptions.filter((subscription) => !(subscription.owner === owner && subscription.repo === repo && subscription.number === input.number));
1459
+ const removed = subscriptions.length !== githubMetadata.subscriptions.length;
1460
+ if (removed) {
1461
+ await threadStore.saveThread({ thread: {
1462
+ ...loadedThread,
1463
+ id: input.threadId,
1464
+ resourceId: input.resourceId,
1465
+ createdAt: loadedThread.createdAt ?? /* @__PURE__ */ new Date(),
1466
+ updatedAt: /* @__PURE__ */ new Date(),
1467
+ metadata: setGithubMetadata(loadedThread.metadata, { subscriptions })
1468
+ } });
1469
+ this.#notifySubscriptionsChanged({
1470
+ threadId: input.threadId,
1471
+ resourceId: input.resourceId,
1472
+ subscriptions
1473
+ });
1474
+ if (subscriptions.length === 0) this.stopPollingForThread({
1475
+ threadId: input.threadId,
1476
+ resourceId: input.resourceId
1477
+ });
1478
+ }
1479
+ return {
1480
+ owner,
1481
+ repo,
1482
+ number: input.number,
1483
+ removed,
1484
+ remainingSubscriptions: subscriptions.length
1485
+ };
1486
+ }
1487
+ #findLatestGithubSignal(messages) {
1488
+ const message = messages.at(-1);
1489
+ if (!message) return void 0;
1490
+ const signal = getSignalMetadata(message);
1491
+ if (!signal || signal.tagName !== "github-subscribe-pr" && signal.tagName !== "github-unsubscribe-pr") return;
1492
+ const attributes = isPlainObject(signal.attributes) ? signal.attributes : {};
1493
+ const metadata = isPlainObject(signal.metadata) ? signal.metadata : {};
1494
+ const github = isPlainObject(metadata.github) ? metadata.github : {};
1495
+ const number = readNumber(attributes.number) ?? readNumber(github.number);
1496
+ if (!number) return void 0;
1497
+ return {
1498
+ tagName: String(signal.tagName),
1499
+ id: readString(signal.id) ?? message.id,
1500
+ owner: readString(attributes.owner) ?? readString(github.owner),
1501
+ repo: readString(attributes.repo) ?? readString(github.repo),
1502
+ number
1503
+ };
1504
+ }
1505
+ async #sendStatus(args, signal, status) {
1506
+ await args.sendSignal?.({
1507
+ type: "reactive",
1508
+ tagName: GITHUB_SYNC_STATUS_TAG,
1509
+ contents: status.message,
1510
+ attributes: {
1511
+ status: status.status,
1512
+ owner: signal.owner,
1513
+ repo: signal.repo,
1514
+ number: signal.number
1515
+ },
1516
+ metadata: { github: {
1517
+ action: status.action,
1518
+ status: status.status,
1519
+ owner: signal.owner,
1520
+ repo: signal.repo,
1521
+ number: signal.number
1522
+ } }
1523
+ });
1524
+ }
1373
1525
  };
1374
-
1526
+ //#endregion
1375
1527
  exports.GITHUB_SIGNALS_METADATA_KEY = GITHUB_SIGNALS_METADATA_KEY;
1376
1528
  exports.GITHUB_SUBSCRIBE_PR_TAG = GITHUB_SUBSCRIBE_PR_TAG;
1377
1529
  exports.GITHUB_SYNC_STATUS_TAG = GITHUB_SYNC_STATUS_TAG;
@@ -1381,5 +1533,5 @@ exports.GitcrawlSyncClient = GitcrawlSyncClient;
1381
1533
  exports.GithubSignals = GithubSignals;
1382
1534
  exports.normalizeGithubChecksForSnapshot = normalizeGithubChecksForSnapshot;
1383
1535
  exports.sanitizeCommentText = sanitizeCommentText;
1384
- //# sourceMappingURL=index.cjs.map
1536
+
1385
1537
  //# sourceMappingURL=index.cjs.map