@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/CHANGELOG.md +9 -0
- package/dist/index.cjs +1421 -1269
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1396 -1262
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
package/dist/index.cjs
CHANGED
|
@@ -1,483 +1,601 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
55
|
}
|
|
37
56
|
function readString(value) {
|
|
38
|
-
|
|
57
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
39
58
|
}
|
|
40
59
|
function readNumber(value) {
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
69
|
+
return (0, crypto.createHash)("sha256").update(stableJson(value)).digest("hex");
|
|
54
70
|
}
|
|
55
71
|
function resolveHomePath(path$1) {
|
|
56
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
98
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
76
99
|
}
|
|
77
100
|
function getSignalMetadata(message) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
165
|
+
return (snapshot.checks ?? []).filter((check) => check.conclusion === "failure" || check.conclusion === "timed_out");
|
|
142
166
|
}
|
|
143
167
|
function getPendingChecks(snapshot) {
|
|
144
|
-
|
|
168
|
+
return (snapshot.checks ?? []).filter((check) => check.status && check.status !== "completed");
|
|
145
169
|
}
|
|
146
170
|
function getPrLabel(subscription, snapshot) {
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
265
|
+
if (!snapshot.latestCommentAuthor || !snapshot.latestCommentBody) return void 0;
|
|
266
|
+
return `${snapshot.latestCommentAuthor} commented on ${pr}: ${getCommentExcerpt(snapshot.latestCommentBody)}`;
|
|
205
267
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
268
|
+
const githubActivityNotificationPriority = {
|
|
269
|
+
high: 0,
|
|
270
|
+
medium: 1
|
|
209
271
|
};
|
|
210
272
|
function getGithubActivityNotificationRank(notification) {
|
|
211
|
-
|
|
273
|
+
return notification.kind === "pull-request-activity" ? 0 : 1;
|
|
212
274
|
}
|
|
213
275
|
function compareGithubActivityNotifications(a, b) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
298
|
+
return `${check.name || "check"}:${check.detailsUrl || check.workflowName || ""}`;
|
|
234
299
|
}
|
|
235
300
|
function normalizeGithubChecksForSnapshot(input) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
318
|
+
return snapshot.latestCommentIsBot === true && (!snapshot.ciState || snapshot.ciState === "unknown");
|
|
264
319
|
}
|
|
265
320
|
function stringifyEvidence(value) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
1536
|
+
|
|
1385
1537
|
//# sourceMappingURL=index.cjs.map
|