@cursor/july 0.1.75 → 0.1.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/authored-alias-hooks.d.ts +12 -0
- package/dist/internal/authored-alias-hooks.d.ts.map +1 -1
- package/dist/internal/authored-alias-hooks.js +60 -8
- package/dist/internal/authored-loaders.d.ts +5 -3
- package/dist/internal/authored-loaders.d.ts.map +1 -1
- package/dist/internal/authored-loaders.js +13 -4
- package/dist/internal/deployment-scm-event-stream.js +4 -4
- package/dist/internal/review-comments.d.ts +62 -0
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +199 -0
- package/dist/playground/assets/index-Bfwz2yLN.css +1 -0
- package/dist/playground/index.html +2 -2
- package/package.json +1 -1
- package/src/internal/authored-alias-hooks.ts +75 -7
- package/src/internal/authored-loaders.ts +15 -4
- package/src/internal/deployment-scm-event-stream.ts +4 -4
- package/src/internal/review-comments.ts +286 -0
- package/dist/playground/assets/index-BrSd5vff.css +0 -1
- /package/dist/playground/assets/{index-BJy9UJrF.js → index-BWqjJcRo.js} +0 -0
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
10
|
<title>agent-serve playground</title>
|
|
11
|
-
<script type="module" crossorigin src="./assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
11
|
+
<script type="module" crossorigin src="./assets/index-BWqjJcRo.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Bfwz2yLN.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cursor/july",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.76",
|
|
4
4
|
"description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
|
@@ -14,16 +14,29 @@
|
|
|
14
14
|
* project's own node_modules; `fallbackAliases` only apply when that
|
|
15
15
|
* fails (npx/global installs, projects without a local dependency), so a
|
|
16
16
|
* real install in the user's project keeps precedence.
|
|
17
|
+
*
|
|
18
|
+
* A third case is first-party factory agents that relative-import this
|
|
19
|
+
* package's `src/` from a monorepo checkout. Hosted engines `npm install`
|
|
20
|
+
* only the agent directory, so files under `packages/agent-serve/src` have
|
|
21
|
+
* no `node_modules`. Bare specifiers from that tree (`zod`, octokit, …)
|
|
22
|
+
* retry against the running `@cursor/july` package.json, which does.
|
|
17
23
|
*/
|
|
18
24
|
|
|
19
25
|
// Null-prototype copies so specifiers like "constructor" or "__proto__"
|
|
20
26
|
// can never resolve to inherited Object.prototype members.
|
|
21
27
|
let directAliases = toNullProtoMap({});
|
|
22
28
|
let fallbackAliases = toNullProtoMap({});
|
|
29
|
+
let sourceTreeFallbackParentURL: string | undefined;
|
|
23
30
|
|
|
24
31
|
export interface AuthoredAliasHookData {
|
|
25
32
|
directAliases: Record<string, string>;
|
|
26
33
|
fallbackAliases: Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* `file:` URL of the running `@cursor/july` package.json. Bare imports
|
|
36
|
+
* from monorepo `packages/agent-serve/` source retry against this parent
|
|
37
|
+
* when the source tree has no `node_modules` (hosted factory engines).
|
|
38
|
+
*/
|
|
39
|
+
sourceTreeFallbackParentURL?: string;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
function toNullProtoMap(
|
|
@@ -35,6 +48,9 @@ function toNullProtoMap(
|
|
|
35
48
|
export function initialize(data: AuthoredAliasHookData): void {
|
|
36
49
|
directAliases = toNullProtoMap(data.directAliases);
|
|
37
50
|
fallbackAliases = toNullProtoMap(data.fallbackAliases);
|
|
51
|
+
const parent = data.sourceTreeFallbackParentURL;
|
|
52
|
+
sourceTreeFallbackParentURL =
|
|
53
|
+
typeof parent === "string" && parent !== "" ? parent : undefined;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
interface ResolveResult {
|
|
@@ -58,12 +74,64 @@ export function resolve(
|
|
|
58
74
|
return { url: direct, shortCircuit: true };
|
|
59
75
|
}
|
|
60
76
|
const fallback = fallbackAliases[specifier];
|
|
61
|
-
if (fallback
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
if (fallback !== undefined) {
|
|
78
|
+
return Promise.resolve()
|
|
79
|
+
.then(() => nextResolve(specifier, context))
|
|
80
|
+
.catch(() => ({ url: fallback, shortCircuit: true }));
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
sourceTreeFallbackParentURL !== undefined &&
|
|
84
|
+
isBarePackageSpecifier(specifier) &&
|
|
85
|
+
isMonorepoAgentServeSource(parentURLOf(context))
|
|
86
|
+
) {
|
|
87
|
+
const retryParent = sourceTreeFallbackParentURL;
|
|
88
|
+
return Promise.resolve()
|
|
89
|
+
.then(() => nextResolve(specifier, context))
|
|
90
|
+
.catch(() => nextResolve(specifier, withParentURL(context, retryParent)));
|
|
91
|
+
}
|
|
92
|
+
// Not ours: pass through with no promise/try-catch overhead. This hook
|
|
93
|
+
// runs on every module resolution in the process.
|
|
94
|
+
return nextResolve(specifier, context);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parentURLOf(context: unknown): string | undefined {
|
|
98
|
+
if (context === null || typeof context !== "object") {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const parentURL = (context as { parentURL?: unknown }).parentURL;
|
|
102
|
+
return typeof parentURL === "string" ? parentURL : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function withParentURL(context: unknown, parentURL: string): unknown {
|
|
106
|
+
if (context !== null && typeof context === "object") {
|
|
107
|
+
return { ...(context as Record<string, unknown>), parentURL };
|
|
108
|
+
}
|
|
109
|
+
return { parentURL };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isBarePackageSpecifier(specifier: string): boolean {
|
|
113
|
+
if (specifier.length === 0) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const first = specifier[0];
|
|
117
|
+
if (first === "." || first === "/" || first === "\\") {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return !(
|
|
121
|
+
specifier.startsWith("node:") ||
|
|
122
|
+
specifier.startsWith("file:") ||
|
|
123
|
+
specifier.startsWith("data:") ||
|
|
124
|
+
specifier.startsWith("http:") ||
|
|
125
|
+
specifier.startsWith("https:")
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isMonorepoAgentServeSource(parentURL: string | undefined): boolean {
|
|
130
|
+
if (parentURL === undefined) {
|
|
131
|
+
return false;
|
|
65
132
|
}
|
|
66
|
-
return
|
|
67
|
-
.
|
|
68
|
-
.
|
|
133
|
+
return (
|
|
134
|
+
parentURL.includes("/packages/agent-serve/") &&
|
|
135
|
+
!parentURL.includes("/node_modules/")
|
|
136
|
+
);
|
|
69
137
|
}
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* imports of the framework resolve against the running package: the
|
|
12
12
|
* historical `@anysphere/agent-serve[/x]` name always, and
|
|
13
13
|
* `@cursor/july[/x]` / `@cursor/agentkit[/x]` as fallbacks when the user's
|
|
14
|
-
* project has no local install (npx / global).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* project has no local install (npx / global). The same hooks retry bare
|
|
15
|
+
* npm specifiers from monorepo `packages/agent-serve/` source against this
|
|
16
|
+
* package (hosted factory engines install only the agent directory).
|
|
17
|
+
* Source runs (tsx from the monorepo) skip both — the host loader and
|
|
18
|
+
* tsconfig paths already cover them.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import { stat } from "node:fs/promises";
|
|
@@ -175,7 +177,16 @@ function registerFrameworkAliases(): void {
|
|
|
175
177
|
fallbackAliases[`${AGENTKIT_PACKAGE_NAME}${suffix}`] = target;
|
|
176
178
|
}
|
|
177
179
|
const hooksUrl = new URL("./authored-alias-hooks.js", import.meta.url);
|
|
178
|
-
const data: AuthoredAliasHookData = {
|
|
180
|
+
const data: AuthoredAliasHookData = {
|
|
181
|
+
directAliases,
|
|
182
|
+
fallbackAliases,
|
|
183
|
+
// Hosted factory agents relative-import this package's src/ from a
|
|
184
|
+
// monorepo checkout while npm install ran only in the agent directory.
|
|
185
|
+
// Bare specifiers from that tree retry against this package.json.
|
|
186
|
+
sourceTreeFallbackParentURL: pathToFileURL(
|
|
187
|
+
join(packageRootDir(), "package.json")
|
|
188
|
+
).href,
|
|
189
|
+
};
|
|
179
190
|
// `node:module`'s register() is synchronous: it blocks until the hooks
|
|
180
191
|
// thread has initialized, so the aliases are active when it returns.
|
|
181
192
|
registerModuleHooks(hooksUrl, { data });
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
* only.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
37
|
+
// Hosted engines install the consuming agent's directory alone and load this
|
|
38
|
+
// file as raw TypeScript source. Relative and `node:` imports resolve here;
|
|
39
|
+
// this package's own npm dependencies retry against the running `@cursor/july`
|
|
40
|
+
// install (authored-alias-hooks). Do not add deps that package does not have.
|
|
41
41
|
import { createHmac } from "node:crypto";
|
|
42
42
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
43
43
|
import { join } from "node:path";
|
|
@@ -110,6 +110,43 @@ export type ReviewCommentReaderImplementation = {
|
|
|
110
110
|
) => Promise<ExistingReviewComment[]>;
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
export type ResolveReviewThreadsArgs = {
|
|
114
|
+
target: ScmReviewTarget;
|
|
115
|
+
/** REST comment ids whose threads should be resolved. */
|
|
116
|
+
commentIds: readonly number[];
|
|
117
|
+
signal?: AbortSignal;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Outcome of resolving review threads for a set of comment ids. Callers
|
|
122
|
+
* treat this as best-effort actuation: a failed resolve must not fail the
|
|
123
|
+
* review that already posted.
|
|
124
|
+
*/
|
|
125
|
+
export type ResolveReviewThreadsResult = {
|
|
126
|
+
requested: number;
|
|
127
|
+
resolved: number;
|
|
128
|
+
alreadyResolved: number;
|
|
129
|
+
notFound: number;
|
|
130
|
+
failed: number;
|
|
131
|
+
detail: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write side of thread resolution, the counterpart of
|
|
136
|
+
* {@link ReviewCommentReaderImplementation}. Bugbot's equivalent is
|
|
137
|
+
* `IGitProvider.resolveReviewThreadForComment` / `resolveThreads`.
|
|
138
|
+
*
|
|
139
|
+
* GitHub only exposes resolve via GraphQL (`resolveReviewThread`). REST
|
|
140
|
+
* has no equivalent. The mutation runs as whatever identity minted the
|
|
141
|
+
* bound GitHub client — for `githubChannel({ cursorAccount })` that is
|
|
142
|
+
* the Cursor installation, the same identity that posted the comments.
|
|
143
|
+
*/
|
|
144
|
+
export type ReviewThreadResolverImplementation = {
|
|
145
|
+
resolveReviewThreadsForComments: (
|
|
146
|
+
args: ResolveReviewThreadsArgs
|
|
147
|
+
) => Promise<ResolveReviewThreadsResult>;
|
|
148
|
+
};
|
|
149
|
+
|
|
113
150
|
/** Parse a PR URL into the SCM target to read comments from. */
|
|
114
151
|
export function resolveScmReviewTarget(
|
|
115
152
|
prUrl: string
|
|
@@ -141,9 +178,65 @@ export function createReviewCommentReader(args: {
|
|
|
141
178
|
}
|
|
142
179
|
}
|
|
143
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Resolver for a resolved target. Returns undefined when the host cannot
|
|
183
|
+
* serve that provider, so callers degrade instead of throwing.
|
|
184
|
+
*/
|
|
185
|
+
export function createReviewThreadResolver(args: {
|
|
186
|
+
target: ScmReviewTarget;
|
|
187
|
+
github?: HostGitHubClient;
|
|
188
|
+
}): ReviewThreadResolverImplementation | undefined {
|
|
189
|
+
switch (args.target.provider) {
|
|
190
|
+
case "github":
|
|
191
|
+
return args.github === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: createHostGithubReviewThreadResolver(args.github);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the review threads that own `commentIds` on `prUrl`.
|
|
199
|
+
*
|
|
200
|
+
* No-ops (never throws) when the PR is on an SCM this host cannot write,
|
|
201
|
+
* no client is bound, or `commentIds` is empty. Same degrade-don't-fail
|
|
202
|
+
* posture as {@link createReviewCommentReader}.
|
|
203
|
+
*/
|
|
204
|
+
export async function resolveReviewThreadsForComments(args: {
|
|
205
|
+
prUrl: string;
|
|
206
|
+
commentIds: readonly number[];
|
|
207
|
+
github?: HostGitHubClient;
|
|
208
|
+
resolver?: ReviewThreadResolverImplementation;
|
|
209
|
+
signal?: AbortSignal;
|
|
210
|
+
}): Promise<ResolveReviewThreadsResult> {
|
|
211
|
+
if (args.commentIds.length === 0) {
|
|
212
|
+
return skippedResolve("no comments to resolve");
|
|
213
|
+
}
|
|
214
|
+
const target = resolveScmReviewTarget(args.prUrl);
|
|
215
|
+
if (target === undefined) {
|
|
216
|
+
return skippedResolve("no SCM resolver for this PR URL");
|
|
217
|
+
}
|
|
218
|
+
const resolver =
|
|
219
|
+
args.resolver ??
|
|
220
|
+
createReviewThreadResolver({
|
|
221
|
+
target,
|
|
222
|
+
github: args.github,
|
|
223
|
+
});
|
|
224
|
+
if (resolver === undefined) {
|
|
225
|
+
return skippedResolve("no GitHub client bound to resolve threads");
|
|
226
|
+
}
|
|
227
|
+
return resolver.resolveReviewThreadsForComments({
|
|
228
|
+
target,
|
|
229
|
+
commentIds: args.commentIds,
|
|
230
|
+
signal: args.signal,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
144
234
|
/** Cap on pages pulled per PR; GitHub serves 100 comments per page. */
|
|
145
235
|
const REVIEW_COMMENTS_MAX_PAGES = 5;
|
|
146
236
|
const REVIEW_COMMENTS_PAGE_SIZE = 100;
|
|
237
|
+
/** Cap on review-thread pages when mapping comment ids to GraphQL thread ids. */
|
|
238
|
+
const REVIEW_THREADS_MAX_PAGES = 20;
|
|
239
|
+
const REVIEW_THREADS_PAGE_SIZE = 100;
|
|
147
240
|
|
|
148
241
|
/**
|
|
149
242
|
* Reads inline review comments through the host GitHub client (Cursor
|
|
@@ -182,6 +275,188 @@ export function createHostGithubReviewCommentReader(
|
|
|
182
275
|
};
|
|
183
276
|
}
|
|
184
277
|
|
|
278
|
+
type GithubReviewThreadNode = {
|
|
279
|
+
id?: string;
|
|
280
|
+
isResolved?: boolean;
|
|
281
|
+
comments?: {
|
|
282
|
+
nodes?: Array<{ databaseId?: number | null } | null>;
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
type GithubReviewThreadsQuery = {
|
|
287
|
+
repository?: {
|
|
288
|
+
pullRequest?: {
|
|
289
|
+
reviewThreads?: {
|
|
290
|
+
nodes?: Array<GithubReviewThreadNode | null>;
|
|
291
|
+
pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
|
|
292
|
+
};
|
|
293
|
+
} | null;
|
|
294
|
+
} | null;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
type GithubResolveThreadMutation = {
|
|
298
|
+
resolveReviewThread?: {
|
|
299
|
+
thread?: { isResolved?: boolean };
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resolves inline review threads through the host GitHub client (Cursor
|
|
305
|
+
* account lease, App installation token, or PAT). GitHub GraphQL has no
|
|
306
|
+
* comment→thread field, so this pages the PR's review threads and matches
|
|
307
|
+
* each REST comment id by `databaseId` — the same approach as Bugbot's
|
|
308
|
+
* `resolveReviewThreadForComment`.
|
|
309
|
+
*/
|
|
310
|
+
export function createHostGithubReviewThreadResolver(
|
|
311
|
+
github: HostGitHubClient
|
|
312
|
+
): ReviewThreadResolverImplementation {
|
|
313
|
+
return {
|
|
314
|
+
async resolveReviewThreadsForComments({ target, commentIds, signal }) {
|
|
315
|
+
const uniqueIds = [...new Set(commentIds.filter((id) => id > 0))];
|
|
316
|
+
if (uniqueIds.length === 0) {
|
|
317
|
+
return skippedResolve("no comments to resolve");
|
|
318
|
+
}
|
|
319
|
+
const [owner, repo] = target.repo.split("/", 2);
|
|
320
|
+
if (owner === undefined || repo === undefined || repo === "") {
|
|
321
|
+
return skippedResolve("unparseable owner/repo from PR URL");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const octokit = await github.getOctokit();
|
|
326
|
+
const wanted = new Set(uniqueIds);
|
|
327
|
+
const matched = new Map<
|
|
328
|
+
number,
|
|
329
|
+
{ threadId: string; isResolved: boolean }
|
|
330
|
+
>();
|
|
331
|
+
|
|
332
|
+
let cursor: string | null = null;
|
|
333
|
+
for (let page = 0; page < REVIEW_THREADS_MAX_PAGES; page++) {
|
|
334
|
+
signal?.throwIfAborted();
|
|
335
|
+
const response = (await octokit.graphql(REVIEW_THREADS_FOR_PR_QUERY, {
|
|
336
|
+
owner,
|
|
337
|
+
repo,
|
|
338
|
+
number: target.prNumber,
|
|
339
|
+
cursor,
|
|
340
|
+
})) as GithubReviewThreadsQuery;
|
|
341
|
+
const reviewThreads = response.repository?.pullRequest?.reviewThreads;
|
|
342
|
+
for (const thread of reviewThreads?.nodes ?? []) {
|
|
343
|
+
if (thread?.id === undefined || thread.id === "") {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
347
|
+
const databaseId = comment?.databaseId;
|
|
348
|
+
if (
|
|
349
|
+
databaseId !== undefined &&
|
|
350
|
+
databaseId !== null &&
|
|
351
|
+
wanted.has(databaseId) &&
|
|
352
|
+
!matched.has(databaseId)
|
|
353
|
+
) {
|
|
354
|
+
matched.set(databaseId, {
|
|
355
|
+
threadId: thread.id,
|
|
356
|
+
isResolved: thread.isResolved === true,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (matched.size === wanted.size) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
const pageInfo = reviewThreads?.pageInfo;
|
|
365
|
+
if (pageInfo?.hasNextPage !== true || !pageInfo.endCursor) {
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
cursor = pageInfo.endCursor;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const toResolve = [
|
|
372
|
+
...new Map(
|
|
373
|
+
[...matched.values()]
|
|
374
|
+
.filter((thread) => !thread.isResolved)
|
|
375
|
+
.map((thread) => [thread.threadId, thread.threadId])
|
|
376
|
+
).values(),
|
|
377
|
+
];
|
|
378
|
+
let resolved = 0;
|
|
379
|
+
let failed = 0;
|
|
380
|
+
for (const threadId of toResolve) {
|
|
381
|
+
signal?.throwIfAborted();
|
|
382
|
+
try {
|
|
383
|
+
const mutation = (await octokit.graphql(
|
|
384
|
+
RESOLVE_REVIEW_THREAD_MUTATION,
|
|
385
|
+
{ threadId }
|
|
386
|
+
)) as GithubResolveThreadMutation;
|
|
387
|
+
if (mutation.resolveReviewThread?.thread?.isResolved === true) {
|
|
388
|
+
resolved += 1;
|
|
389
|
+
} else {
|
|
390
|
+
failed += 1;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
failed += 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const alreadyResolved = new Set(
|
|
398
|
+
[...matched.values()]
|
|
399
|
+
.filter((thread) => thread.isResolved)
|
|
400
|
+
.map((thread) => thread.threadId)
|
|
401
|
+
).size;
|
|
402
|
+
const notFound = wanted.size - matched.size;
|
|
403
|
+
return {
|
|
404
|
+
requested: uniqueIds.length,
|
|
405
|
+
resolved,
|
|
406
|
+
alreadyResolved,
|
|
407
|
+
notFound,
|
|
408
|
+
failed,
|
|
409
|
+
detail: `resolved ${resolved} of ${toResolve.length} open thread(s) for ${uniqueIds.length} comment(s) via host GitHub (${alreadyResolved} already resolved, ${notFound} not found, ${failed} failed)`,
|
|
410
|
+
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (signal?.aborted) {
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
requested: uniqueIds.length,
|
|
417
|
+
resolved: 0,
|
|
418
|
+
alreadyResolved: 0,
|
|
419
|
+
notFound: 0,
|
|
420
|
+
failed: uniqueIds.length,
|
|
421
|
+
detail: `host GitHub thread resolve failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const REVIEW_THREADS_FOR_PR_QUERY = `
|
|
429
|
+
query ReviewThreadsForPr(
|
|
430
|
+
$owner: String!
|
|
431
|
+
$repo: String!
|
|
432
|
+
$number: Int!
|
|
433
|
+
$cursor: String
|
|
434
|
+
) {
|
|
435
|
+
repository(owner: $owner, name: $repo) {
|
|
436
|
+
pullRequest(number: $number) {
|
|
437
|
+
reviewThreads(first: ${REVIEW_THREADS_PAGE_SIZE}, after: $cursor) {
|
|
438
|
+
nodes {
|
|
439
|
+
id
|
|
440
|
+
isResolved
|
|
441
|
+
comments(first: ${REVIEW_THREADS_PAGE_SIZE}) {
|
|
442
|
+
nodes { databaseId }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
pageInfo { hasNextPage endCursor }
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
`;
|
|
451
|
+
|
|
452
|
+
const RESOLVE_REVIEW_THREAD_MUTATION = `
|
|
453
|
+
mutation ResolveReviewThread($threadId: ID!) {
|
|
454
|
+
resolveReviewThread(input: { threadId: $threadId }) {
|
|
455
|
+
thread { isResolved }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
|
|
185
460
|
type GithubReviewCommentLike = {
|
|
186
461
|
id: number;
|
|
187
462
|
path: string;
|
|
@@ -504,6 +779,17 @@ function skippedHost(detail: string): ReviewCommentPostResult {
|
|
|
504
779
|
};
|
|
505
780
|
}
|
|
506
781
|
|
|
782
|
+
function skippedResolve(detail: string): ResolveReviewThreadsResult {
|
|
783
|
+
return {
|
|
784
|
+
requested: 0,
|
|
785
|
+
resolved: 0,
|
|
786
|
+
alreadyResolved: 0,
|
|
787
|
+
notFound: 0,
|
|
788
|
+
failed: 0,
|
|
789
|
+
detail,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
507
793
|
function parseGithubPrUrl(
|
|
508
794
|
prUrl: string
|
|
509
795
|
):
|