@blogic-cz/agent-tools 1.7.5 → 1.9.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.
Files changed (142) hide show
  1. package/README.md +2 -1
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/types.d.ts +1 -0
  4. package/dist/config/types.d.ts.map +1 -1
  5. package/dist/credential-guard/index.d.ts.map +1 -1
  6. package/dist/db-tool/service.d.ts.map +1 -1
  7. package/dist/gh-tool/branch.d.ts +2 -3
  8. package/dist/gh-tool/branch.d.ts.map +1 -1
  9. package/dist/gh-tool/environment.d.ts +1 -1
  10. package/dist/gh-tool/environment.d.ts.map +1 -1
  11. package/dist/gh-tool/gist.d.ts +8 -28
  12. package/dist/gh-tool/gist.d.ts.map +1 -1
  13. package/dist/gh-tool/issue/commands.d.ts +11 -9
  14. package/dist/gh-tool/issue/commands.d.ts.map +1 -1
  15. package/dist/gh-tool/issue/core.d.ts +116 -9
  16. package/dist/gh-tool/issue/core.d.ts.map +1 -1
  17. package/dist/gh-tool/issue/triage.d.ts +6 -6
  18. package/dist/gh-tool/issue/triage.d.ts.map +1 -1
  19. package/dist/gh-tool/label.d.ts +20 -0
  20. package/dist/gh-tool/label.d.ts.map +1 -0
  21. package/dist/gh-tool/pr/commands.d.ts +53 -46
  22. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  23. package/dist/gh-tool/pr/core.d.ts +156 -86
  24. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  25. package/dist/gh-tool/pr/review.d.ts +16 -16
  26. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  27. package/dist/gh-tool/pr/snapshot.d.ts +9 -0
  28. package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
  29. package/dist/gh-tool/pr/stack-read.d.ts +1 -6
  30. package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
  31. package/dist/gh-tool/pr/stack.d.ts +3 -3
  32. package/dist/gh-tool/pr/stack.d.ts.map +1 -1
  33. package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
  34. package/dist/gh-tool/read/api.d.ts.map +1 -0
  35. package/dist/gh-tool/read/cache.d.ts +44 -0
  36. package/dist/gh-tool/read/cache.d.ts.map +1 -0
  37. package/dist/gh-tool/read/catalog.d.ts +170 -0
  38. package/dist/gh-tool/read/catalog.d.ts.map +1 -0
  39. package/dist/gh-tool/read/classes.d.ts +98 -0
  40. package/dist/gh-tool/read/classes.d.ts.map +1 -0
  41. package/dist/gh-tool/read/commands.d.ts +3 -0
  42. package/dist/gh-tool/read/commands.d.ts.map +1 -0
  43. package/dist/gh-tool/read/commits.d.ts +10 -0
  44. package/dist/gh-tool/read/commits.d.ts.map +1 -0
  45. package/dist/gh-tool/read/gated.d.ts +14 -0
  46. package/dist/gh-tool/read/gated.d.ts.map +1 -0
  47. package/dist/gh-tool/read/index.d.ts +5 -0
  48. package/dist/gh-tool/read/index.d.ts.map +1 -0
  49. package/dist/gh-tool/read/issues.d.ts +80 -0
  50. package/dist/gh-tool/read/issues.d.ts.map +1 -0
  51. package/dist/gh-tool/read/keys.d.ts +64 -0
  52. package/dist/gh-tool/read/keys.d.ts.map +1 -0
  53. package/dist/gh-tool/read/porcelain.d.ts +68 -0
  54. package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
  55. package/dist/gh-tool/read/pulls.d.ts +137 -0
  56. package/dist/gh-tool/read/pulls.d.ts.map +1 -0
  57. package/dist/gh-tool/read/reads.d.ts +220 -0
  58. package/dist/gh-tool/read/reads.d.ts.map +1 -0
  59. package/dist/gh-tool/read/rest.d.ts +49 -0
  60. package/dist/gh-tool/read/rest.d.ts.map +1 -0
  61. package/dist/gh-tool/read/review-guards.d.ts +20 -0
  62. package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
  63. package/dist/gh-tool/read/review-model.d.ts +190 -0
  64. package/dist/gh-tool/read/review-model.d.ts.map +1 -0
  65. package/dist/gh-tool/read/reviews.d.ts +9 -0
  66. package/dist/gh-tool/read/reviews.d.ts.map +1 -0
  67. package/dist/gh-tool/read/stacks.d.ts +7 -0
  68. package/dist/gh-tool/read/stacks.d.ts.map +1 -0
  69. package/dist/gh-tool/read/store.d.ts +70 -0
  70. package/dist/gh-tool/read/store.d.ts.map +1 -0
  71. package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
  72. package/dist/gh-tool/read/transport.d.ts.map +1 -0
  73. package/dist/gh-tool/read/waits.d.ts +22 -0
  74. package/dist/gh-tool/read/waits.d.ts.map +1 -0
  75. package/dist/gh-tool/read/writes.d.ts +16 -0
  76. package/dist/gh-tool/read/writes.d.ts.map +1 -0
  77. package/dist/gh-tool/release.d.ts +10 -26
  78. package/dist/gh-tool/release.d.ts.map +1 -1
  79. package/dist/gh-tool/repo.d.ts +9 -4
  80. package/dist/gh-tool/repo.d.ts.map +1 -1
  81. package/dist/gh-tool/types.d.ts +1 -1
  82. package/dist/gh-tool/types.d.ts.map +1 -1
  83. package/dist/gh-tool/workflow.d.ts +43 -21
  84. package/dist/gh-tool/workflow.d.ts.map +1 -1
  85. package/dist/observability-tool/shared.d.ts.map +1 -1
  86. package/dist/shared/content-security.d.ts +2 -0
  87. package/dist/shared/content-security.d.ts.map +1 -1
  88. package/package.json +7 -6
  89. package/schemas/agent-tools.schema.json +5 -0
  90. package/src/api-tool/proxy-entry.ts +3 -1
  91. package/src/config/loader.ts +1 -0
  92. package/src/config/types.ts +1 -0
  93. package/src/credential-guard/index.ts +169 -1
  94. package/src/db-tool/service.ts +4 -1
  95. package/src/gh-tool/branch.ts +5 -8
  96. package/src/gh-tool/environment.ts +2 -1
  97. package/src/gh-tool/gist.ts +8 -35
  98. package/src/gh-tool/issue/commands.ts +12 -2
  99. package/src/gh-tool/issue/core.ts +58 -122
  100. package/src/gh-tool/issue/triage.ts +4 -26
  101. package/src/gh-tool/label.ts +57 -0
  102. package/src/gh-tool/main.ts +24 -4
  103. package/src/gh-tool/pr/commands.ts +17 -15
  104. package/src/gh-tool/pr/core.ts +486 -582
  105. package/src/gh-tool/pr/review.ts +106 -454
  106. package/src/gh-tool/pr/snapshot.ts +23 -0
  107. package/src/gh-tool/pr/stack-read.ts +1 -73
  108. package/src/gh-tool/pr/stack.ts +37 -36
  109. package/src/gh-tool/{api.ts → read/api.ts} +40 -7
  110. package/src/gh-tool/read/cache.ts +192 -0
  111. package/src/gh-tool/read/catalog.ts +310 -0
  112. package/src/gh-tool/read/classes.ts +31 -0
  113. package/src/gh-tool/read/commands.ts +25 -0
  114. package/src/gh-tool/read/commits.ts +28 -0
  115. package/src/gh-tool/read/gated.ts +56 -0
  116. package/src/gh-tool/read/index.ts +4 -0
  117. package/src/gh-tool/read/issues.ts +124 -0
  118. package/src/gh-tool/read/keys.ts +104 -0
  119. package/src/gh-tool/read/porcelain.ts +58 -0
  120. package/src/gh-tool/read/pulls.ts +266 -0
  121. package/src/gh-tool/read/reads.ts +306 -0
  122. package/src/gh-tool/read/rest.ts +111 -0
  123. package/src/gh-tool/read/review-guards.ts +61 -0
  124. package/src/gh-tool/read/review-model.ts +286 -0
  125. package/src/gh-tool/read/reviews.ts +114 -0
  126. package/src/gh-tool/read/stacks.ts +73 -0
  127. package/src/gh-tool/read/store.ts +504 -0
  128. package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
  129. package/src/gh-tool/read/waits.ts +52 -0
  130. package/src/gh-tool/read/writes.ts +37 -0
  131. package/src/gh-tool/release.ts +15 -168
  132. package/src/gh-tool/repo.ts +26 -78
  133. package/src/gh-tool/types.ts +1 -1
  134. package/src/gh-tool/workflow.ts +123 -214
  135. package/src/observability-tool/shared.ts +2 -1
  136. package/src/shared/content-security.ts +10 -1
  137. package/src/shared/credential-patterns.ts +1 -1
  138. package/dist/gh-tool/api.d.ts.map +0 -1
  139. package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
  140. package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
  141. package/dist/gh-tool/service.d.ts.map +0 -1
  142. package/src/gh-tool/pr/check-status-cache.ts +0 -142
@@ -0,0 +1,111 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { GitHubService } from "#gh/read/transport";
3
+ import { GitHubCommandError } from "#gh/errors";
4
+ import { cachedRead, ReadCache } from "./cache";
5
+ import { OPS } from "./classes";
6
+ import { digest } from "./keys";
7
+ import type { ReadKey } from "./keys";
8
+ import { parsePreserving } from "./porcelain";
9
+
10
+ const Page = Schema.Struct({
11
+ path: Schema.String,
12
+ etag: Schema.NullOr(Schema.String),
13
+ body: Schema.Unknown,
14
+ });
15
+ const Snapshot = Schema.Struct({ pages: Schema.Array(Page) });
16
+ export type RestSnapshot = typeof Snapshot.Type;
17
+ export type RestSource =
18
+ | { readonly path: string; readonly kind: "single" }
19
+ | {
20
+ readonly path: string;
21
+ readonly kind: "pages";
22
+ readonly items: (body: unknown) => readonly unknown[];
23
+ };
24
+
25
+ export const restSnapshot = Effect.fn("reads.restSnapshot")(function* (
26
+ key: ReadKey & { op: "prHead" | "checkSignal" | "issueComments" | "prComments" | "prReviews" },
27
+ sources: readonly RestSource[],
28
+ consistency: "cached" | "origin" = "cached",
29
+ ) {
30
+ const gh = yield* GitHubService;
31
+ const session = yield* ReadCache;
32
+ return yield* cachedRead({
33
+ key,
34
+ policy: OPS[key.op],
35
+ consistency,
36
+ validateOrigin: true,
37
+ decode: parsePreserving(Snapshot),
38
+ fetch: (previous: { value: RestSnapshot } | null) =>
39
+ Effect.gen(function* () {
40
+ const pages: Array<typeof Page.Type> = [];
41
+ let unchanged = previous !== null;
42
+ for (const source of sources) {
43
+ let page = 1;
44
+ while (true) {
45
+ const path =
46
+ source.kind === "single"
47
+ ? source.path
48
+ : `${source.path}${source.path.includes("?") ? "&" : "?"}per_page=100&page=${page}`;
49
+ const prior = previous?.value.pages.find((item) => item.path === path);
50
+ const request = {
51
+ path,
52
+ ...(prior?.etag === null || prior?.etag === undefined
53
+ ? {}
54
+ : { ifNoneMatch: prior.etag }),
55
+ };
56
+ const response = yield* session?.store === null || session === null
57
+ ? gh.runGh(["api", path]).pipe(
58
+ Effect.flatMap((result) =>
59
+ Effect.try({
60
+ try: () => ({
61
+ status: 200,
62
+ body: JSON.parse(result.stdout) as unknown,
63
+ etag: undefined,
64
+ }),
65
+ catch: (error) =>
66
+ new GitHubCommandError({
67
+ command: `GET ${path}`,
68
+ exitCode: 0,
69
+ message: `Failed to parse response: ${String(error)}`,
70
+ stderr: `Failed to parse response: ${String(error)}`,
71
+ }),
72
+ }),
73
+ ),
74
+ )
75
+ : gh.apiRequest<unknown>(request);
76
+ if (response.status === 304 && prior === undefined)
77
+ return yield* new GitHubCommandError({
78
+ command: `GET ${path}`,
79
+ exitCode: 304,
80
+ message: "304 without a cached page",
81
+ stderr: "304 without a cached page",
82
+ });
83
+ const body =
84
+ response.status === 304 && prior !== undefined ? prior.body : response.body;
85
+ const etag = response.etag ?? (response.status === 304 ? (prior?.etag ?? null) : null);
86
+ unchanged = unchanged && response.status === 304;
87
+ pages.push({ path, etag, body });
88
+ if (source.kind === "single") break;
89
+ const count = yield* Effect.try({
90
+ try: () => source.items(body).length,
91
+ catch: (error) =>
92
+ new GitHubCommandError({
93
+ command: `GET ${path}`,
94
+ exitCode: 0,
95
+ message: `Invalid paginated response: ${String(error)}`,
96
+ stderr: `Invalid paginated response: ${String(error)}`,
97
+ }),
98
+ });
99
+ if (count < 100) break;
100
+ page += 1;
101
+ }
102
+ }
103
+ return unchanged
104
+ ? { kind: "unchanged" as const }
105
+ : { kind: "fresh" as const, value: { pages } };
106
+ }),
107
+ });
108
+ });
109
+
110
+ export const snapshotDigest = (snapshot: RestSnapshot): string =>
111
+ digest(JSON.stringify(snapshot.pages.map((page) => page.body)));
@@ -0,0 +1,61 @@
1
+ import { Effect } from "effect";
2
+ import { GitHubService } from "#gh/read/transport";
3
+ import type { RepoRef } from "./keys";
4
+ import { repoSlug } from "./keys";
5
+ import { PENDING_REVIEWS_QUERY, PENDING_REVIEW_CONTENT_QUERY } from "./review-model";
6
+ import type { EditableComment, ReviewCommentById } from "./review-model";
7
+
8
+ export const reviewGuards = {
9
+ requestedReviewers: (repo: RepoRef, pr: number) =>
10
+ Effect.gen(function* () {
11
+ const gh = yield* GitHubService;
12
+ return yield* gh.runGhJson<{ requested_reviewers?: unknown }>([
13
+ "api",
14
+ `repos/${repoSlug(repo)}/pulls/${pr}`,
15
+ ]);
16
+ }),
17
+ commentById: (commentId: number) =>
18
+ Effect.gen(function* () {
19
+ const gh = yield* GitHubService;
20
+ return yield* gh.runGhJson<ReviewCommentById>([
21
+ "api",
22
+ `repos/{owner}/{repo}/pulls/comments/${commentId}`,
23
+ ]);
24
+ }),
25
+ editableComment: (repo: RepoRef, kind: "review" | "issue", commentId: number) =>
26
+ Effect.gen(function* () {
27
+ const gh = yield* GitHubService;
28
+ return yield* gh.runGhJson<EditableComment>([
29
+ "api",
30
+ `repos/${repoSlug(repo)}/${kind === "review" ? "pulls" : "issues"}/comments/${commentId}`,
31
+ ]);
32
+ }),
33
+ viewer: () =>
34
+ Effect.gen(function* () {
35
+ const gh = yield* GitHubService;
36
+ return yield* gh.runGhJson<{ login: string }>(["api", "user"]);
37
+ }),
38
+ pullNumber: (repo: RepoRef, pr: number) =>
39
+ Effect.gen(function* () {
40
+ const gh = yield* GitHubService;
41
+ return yield* gh.runGhJson<{ number: number }>([
42
+ "api",
43
+ `repos/${repoSlug(repo)}/pulls/${pr}`,
44
+ ]);
45
+ }),
46
+ pendingReviews: (repo: RepoRef, pr: number, after: string | null) =>
47
+ Effect.gen(function* () {
48
+ const gh = yield* GitHubService;
49
+ return yield* gh.runGraphQL(PENDING_REVIEWS_QUERY, {
50
+ owner: repo.owner,
51
+ name: repo.name,
52
+ pr,
53
+ after,
54
+ });
55
+ }),
56
+ pendingReviewContent: (reviewId: string, after: string | null) =>
57
+ Effect.gen(function* () {
58
+ const gh = yield* GitHubService;
59
+ return yield* gh.runGraphQL(PENDING_REVIEW_CONTENT_QUERY, { reviewId, after });
60
+ }),
61
+ };
@@ -0,0 +1,286 @@
1
+ export const REVIEW_THREADS_QUERY = `
2
+ query($owner: String!, $name: String!, $pr: Int!, $after: String) {
3
+ repository(owner: $owner, name: $name) {
4
+ pullRequest(number: $pr) {
5
+ reviewThreads(first: 100, after: $after) {
6
+ nodes {
7
+ id
8
+ isResolved
9
+ comments(first: 100) {
10
+ nodes {
11
+ id
12
+ databaseId
13
+ path
14
+ line
15
+ body
16
+ author { login }
17
+ commit { oid }
18
+ }
19
+ pageInfo { hasNextPage endCursor }
20
+ }
21
+ }
22
+ pageInfo {
23
+ hasNextPage
24
+ endCursor
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ `;
31
+
32
+ export const REVIEW_THREAD_COMMENTS_QUERY = `
33
+ query($threadId: ID!, $after: String) {
34
+ node(id: $threadId) {
35
+ ... on PullRequestReviewThread {
36
+ comments(first: 100, after: $after) {
37
+ nodes {
38
+ id
39
+ databaseId
40
+ path
41
+ line
42
+ body
43
+ author { login }
44
+ commit { oid }
45
+ }
46
+ pageInfo { hasNextPage endCursor }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ `;
52
+
53
+ export const RESOLVE_THREAD_MUTATION = `
54
+ mutation($threadId: ID!) {
55
+ resolveReviewThread(input: {threadId: $threadId}) {
56
+ thread { id isResolved }
57
+ }
58
+ }
59
+ `;
60
+
61
+ export const PENDING_REVIEWS_QUERY = `
62
+ query($owner: String!, $name: String!, $pr: Int!, $after: String) {
63
+ viewer { login }
64
+ repository(owner: $owner, name: $name) {
65
+ pullRequest(number: $pr) {
66
+ reviews(first: 100, after: $after, states: [PENDING]) {
67
+ nodes {
68
+ id
69
+ state
70
+ author { login }
71
+ }
72
+ pageInfo { hasNextPage endCursor }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ `;
78
+
79
+ export const PENDING_REVIEW_CONTENT_QUERY = `
80
+ query($reviewId: ID!, $after: String) {
81
+ viewer { login }
82
+ node(id: $reviewId) {
83
+ ... on PullRequestReview {
84
+ id
85
+ state
86
+ body
87
+ author { login }
88
+ pullRequest {
89
+ number
90
+ repository { nameWithOwner }
91
+ }
92
+ comments(first: 100, after: $after) {
93
+ nodes { body }
94
+ pageInfo { hasNextPage endCursor }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ `;
100
+
101
+ export const SUBMIT_REVIEW_MUTATION = `
102
+ mutation($reviewId: ID!, $event: PullRequestReviewEvent!, $body: String) {
103
+ submitPullRequestReview(input: { pullRequestReviewId: $reviewId, event: $event, body: $body }) {
104
+ pullRequestReview { id state }
105
+ }
106
+ }
107
+ `;
108
+
109
+ export const LAST_HUMAN_REVIEWER_QUERY = `
110
+ query($owner: String!, $name: String!, $pr: Int!) {
111
+ repository(owner: $owner, name: $name) {
112
+ pullRequest(number: $pr) {
113
+ reviewRequests(first: 100) {
114
+ nodes {
115
+ requestedReviewer {
116
+ __typename
117
+ ... on User { login }
118
+ ... on Team { name }
119
+ ... on Bot { login }
120
+ }
121
+ }
122
+ }
123
+ reviews(last: 100) {
124
+ nodes {
125
+ author { login }
126
+ state
127
+ submittedAt
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ `;
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Internal types
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export type ThreadNode = {
140
+ id: string;
141
+ isResolved: boolean;
142
+ comments: {
143
+ nodes: Array<{
144
+ id: string;
145
+ databaseId: number;
146
+ path: string;
147
+ line: number;
148
+ body: string;
149
+ author: { login: string };
150
+ commit: { oid: string } | null;
151
+ }>;
152
+ pageInfo?: { hasNextPage: boolean; endCursor: string | null };
153
+ };
154
+ };
155
+
156
+ export type ThreadCommentsQueryResult = {
157
+ node: { comments: ThreadNode["comments"] } | null;
158
+ };
159
+
160
+ export type ThreadsQueryResult = {
161
+ repository: {
162
+ pullRequest: {
163
+ reviewThreads: {
164
+ nodes: ThreadNode[];
165
+ pageInfo: {
166
+ hasNextPage: boolean;
167
+ endCursor: string | null;
168
+ };
169
+ };
170
+ };
171
+ };
172
+ };
173
+
174
+ export type ResolveThreadResult = {
175
+ resolveReviewThread: {
176
+ thread: { id: string; isResolved: boolean };
177
+ };
178
+ };
179
+
180
+ export type PendingReviewsQueryResult = {
181
+ viewer: { login: string };
182
+ repository: {
183
+ pullRequest: {
184
+ reviews: {
185
+ nodes: Array<{
186
+ id: string;
187
+ state: string;
188
+ author: { login: string };
189
+ }>;
190
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
191
+ };
192
+ };
193
+ };
194
+ };
195
+
196
+ export type PendingReviewContentQueryResult = {
197
+ viewer: { login: string };
198
+ node: {
199
+ id: string;
200
+ state: string;
201
+ body: string | null;
202
+ author: { login: string };
203
+ pullRequest: { number: number; repository: { nameWithOwner: string } };
204
+ comments: {
205
+ nodes: Array<{ body: string }>;
206
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
207
+ };
208
+ } | null;
209
+ };
210
+
211
+ export type SubmitReviewResult = {
212
+ submitPullRequestReview: {
213
+ pullRequestReview: { id: string; state: string };
214
+ };
215
+ };
216
+
217
+ export type RequestedReviewerNode = {
218
+ __typename: string;
219
+ login?: string;
220
+ name?: string;
221
+ } | null;
222
+
223
+ export type LastHumanReviewerQueryResult = {
224
+ repository: {
225
+ pullRequest: {
226
+ reviewRequests: {
227
+ nodes: Array<{ requestedReviewer: RequestedReviewerNode }>;
228
+ };
229
+ reviews: {
230
+ nodes: Array<{
231
+ author: { login: string } | null;
232
+ state: string;
233
+ submittedAt: string | null;
234
+ }>;
235
+ };
236
+ };
237
+ };
238
+ };
239
+
240
+ export type RawReviewComment = {
241
+ id: number;
242
+ in_reply_to_id: number | null;
243
+ user: { login: string };
244
+ body: string;
245
+ path: string;
246
+ line: number;
247
+ created_at: string;
248
+ updated_at: string;
249
+ pull_request_review_id?: number | null;
250
+ commit_id?: string | null;
251
+ };
252
+
253
+ export type RawIssueComment = {
254
+ id: number;
255
+ user: { login: string };
256
+ body: string;
257
+ created_at: string;
258
+ html_url: string;
259
+ };
260
+
261
+ export type RawPullRequestReview = {
262
+ id: number;
263
+ user: { login: string } | null;
264
+ state: string;
265
+ body: string | null;
266
+ submitted_at: string | null;
267
+ html_url: string;
268
+ commit_id?: string | null;
269
+ };
270
+
271
+ export type ReviewCommentById = {
272
+ id: number;
273
+ in_reply_to_id: number | null;
274
+ pull_request_url: string;
275
+ };
276
+
277
+ export type EditableComment = {
278
+ id: number;
279
+ user: { login: string };
280
+ html_url: string;
281
+ updated_at: string;
282
+ pull_request_url?: string;
283
+ issue_url?: string;
284
+ };
285
+
286
+ export const REST_PAGE_SIZE = 100;
@@ -0,0 +1,114 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { GitHubService } from "#gh/read/transport";
3
+ import { cachedRead } from "./cache";
4
+ import { gatedRead } from "./gated";
5
+ import { restSnapshot, snapshotDigest } from "./rest";
6
+ import { OPS } from "./classes";
7
+ import { repoSlug } from "./keys";
8
+ import type { RepoRef } from "./keys";
9
+ import {
10
+ REVIEW_THREADS_QUERY,
11
+ REVIEW_THREAD_COMMENTS_QUERY,
12
+ LAST_HUMAN_REVIEWER_QUERY,
13
+ } from "./review-model";
14
+ import type {
15
+ ThreadNode,
16
+ ThreadsQueryResult,
17
+ ThreadCommentsQueryResult,
18
+ LastHumanReviewerQueryResult,
19
+ RawReviewComment,
20
+ RawPullRequestReview,
21
+ } from "./review-model";
22
+
23
+ const originThreads = Effect.fn("pr.fetchAllThreadNodes")(function* (
24
+ repoInfo: RepoRef,
25
+ pr: number,
26
+ ) {
27
+ const service = yield* GitHubService;
28
+
29
+ const nodes: ThreadNode[] = [];
30
+ let after: string | null = null;
31
+
32
+ while (true) {
33
+ const response = (yield* service.runGraphQL(REVIEW_THREADS_QUERY, {
34
+ owner: repoInfo.owner,
35
+ name: repoInfo.name,
36
+ pr,
37
+ after,
38
+ })) as ThreadsQueryResult;
39
+
40
+ const page = response.repository.pullRequest.reviewThreads;
41
+ for (const node of page.nodes) {
42
+ let commentsAfter = node.comments.pageInfo?.endCursor ?? null;
43
+ while (node.comments.pageInfo?.hasNextPage && commentsAfter !== null) {
44
+ const commentsResponse = (yield* service.runGraphQL(REVIEW_THREAD_COMMENTS_QUERY, {
45
+ threadId: node.id,
46
+ after: commentsAfter,
47
+ })) as ThreadCommentsQueryResult;
48
+ if (commentsResponse.node === null) break;
49
+ node.comments.nodes.push(...commentsResponse.node.comments.nodes);
50
+ node.comments.pageInfo = commentsResponse.node.comments.pageInfo;
51
+ commentsAfter = commentsResponse.node.comments.pageInfo?.endCursor ?? null;
52
+ }
53
+ nodes.push(node);
54
+ }
55
+
56
+ if (!page.pageInfo.hasNextPage || page.pageInfo.endCursor === null) {
57
+ return nodes;
58
+ }
59
+
60
+ after = page.pageInfo.endCursor;
61
+ }
62
+ });
63
+
64
+ const reviewPages = (repo: RepoRef, pr: number, op: "prComments" | "prReviews") =>
65
+ restSnapshot({ op, repo, pr }, [
66
+ {
67
+ kind: "pages",
68
+ path: `repos/${repoSlug(repo)}/pulls/${pr}/${op === "prComments" ? "comments" : "reviews"}`,
69
+ items: Schema.decodeUnknownSync(Schema.Array(Schema.Unknown)),
70
+ },
71
+ ]);
72
+
73
+ export const prComments = Effect.fn("reads.prComments")(function* (repo: RepoRef, pr: number) {
74
+ const snapshot = yield* reviewPages(repo, pr, "prComments");
75
+ return snapshot.pages.flatMap((page) => page.body as RawReviewComment[]);
76
+ });
77
+ export const prReviews = Effect.fn("reads.prReviews")(function* (repo: RepoRef, pr: number) {
78
+ const snapshot = yield* reviewPages(repo, pr, "prReviews");
79
+ return snapshot.pages.flatMap((page) => page.body as RawPullRequestReview[]);
80
+ });
81
+ export const reviewThreads = Effect.fn("reads.reviewThreads")(function* (
82
+ repo: RepoRef,
83
+ pr: number,
84
+ consistency: "cached" | "origin" = "cached",
85
+ ) {
86
+ return yield* gatedRead({
87
+ key: { op: "reviewThreads", repo, pr },
88
+ consistency,
89
+ decode: (raw) => Schema.decodeUnknownSync(Schema.Array(Schema.Unknown))(raw) as ThreadNode[],
90
+ fetch: originThreads(repo, pr),
91
+ signal: reviewPages(repo, pr, "prComments").pipe(Effect.map(snapshotDigest)),
92
+ maxAgeMs: () => OPS.reviewThreads.maxAgeMs,
93
+ });
94
+ });
95
+ export const lastHumanReviewer = Effect.fn("reads.lastHumanReviewer")(function* (
96
+ repo: RepoRef,
97
+ pr: number,
98
+ ) {
99
+ const service = yield* GitHubService;
100
+ return yield* cachedRead({
101
+ key: { op: "lastHumanReviewer", repo, pr },
102
+ policy: OPS.lastHumanReviewer,
103
+ decode: (raw) => raw as LastHumanReviewerQueryResult,
104
+ fetch: () =>
105
+ service
106
+ .runGraphQL(LAST_HUMAN_REVIEWER_QUERY, { owner: repo.owner, name: repo.name, pr })
107
+ .pipe(
108
+ Effect.map((value) => ({
109
+ kind: "fresh" as const,
110
+ value: value as LastHumanReviewerQueryResult,
111
+ })),
112
+ ),
113
+ });
114
+ });
@@ -0,0 +1,73 @@
1
+ import { Effect } from "effect";
2
+
3
+ import { GitHubService } from "#gh/read/transport";
4
+
5
+ import type { StackMember, StackView } from "#gh/types";
6
+
7
+ type StacksListResponse = Array<{ number: number }>;
8
+
9
+ type StackResponse = {
10
+ number: number;
11
+ base: { ref: string };
12
+ open: boolean;
13
+ pull_requests: Array<{
14
+ number: number;
15
+ title: string;
16
+ state: "open" | "closed";
17
+ merged_at: string | null;
18
+ draft: boolean;
19
+ html_url: string;
20
+ head: { ref: string };
21
+ base: { ref: string };
22
+ }>;
23
+ };
24
+
25
+ export const readStack = Effect.fn("pr.readStack")(function* (opts: { pr: number }) {
26
+ const gh = yield* GitHubService;
27
+ const repo = yield* gh.getRepoInfo();
28
+ const base = `repos/${repo.owner}/${repo.name}`;
29
+
30
+ const unstacked: StackView = {
31
+ pr: opts.pr,
32
+ isStacked: false,
33
+ stackNumber: null,
34
+ baseRef: null,
35
+ members: [],
36
+ };
37
+
38
+ // A 404 is the repository having no stacks surface at all, which reads the same as a PR
39
+ // that belongs to no stack. Every caller gets that reading from here, not its own.
40
+ const stacks = yield* gh
41
+ .apiRequest<StacksListResponse>({ path: `${base}/stacks?pull_request=${opts.pr}` })
42
+ .pipe(Effect.catchTag("GitHubNotFoundError", () => Effect.succeed(null)));
43
+
44
+ if (stacks === null) {
45
+ return unstacked;
46
+ }
47
+
48
+ const stackNumber = stacks.body?.[0]?.number;
49
+ if (stackNumber === undefined) {
50
+ return unstacked;
51
+ }
52
+
53
+ const stack = yield* gh.apiRequest<StackResponse>({ path: `${base}/stacks/${stackNumber}` });
54
+
55
+ const members: StackMember[] = stack.body.pull_requests.map((member, index) => ({
56
+ position: index + 1,
57
+ number: member.number,
58
+ title: member.title,
59
+ headRefName: member.head.ref,
60
+ baseRefName: member.base.ref,
61
+ state: member.merged_at === null ? member.state : "merged",
62
+ isDraft: member.draft,
63
+ url: member.html_url,
64
+ }));
65
+
66
+ return {
67
+ pr: opts.pr,
68
+ isStacked: true,
69
+ stackNumber: stack.body.number,
70
+ baseRef: stack.body.base.ref,
71
+ members,
72
+ } satisfies StackView;
73
+ });