@blogic-cz/agent-tools 0.8.3 → 0.8.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -19,10 +19,10 @@ import { viewPR } from "./core";
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
21
  const REVIEW_THREADS_QUERY = `
22
- query($owner: String!, $name: String!, $pr: Int!) {
22
+ query($owner: String!, $name: String!, $pr: Int!, $after: String) {
23
23
  repository(owner: $owner, name: $name) {
24
24
  pullRequest(number: $pr) {
25
- reviewThreads(first: 100) {
25
+ reviewThreads(first: 100, after: $after) {
26
26
  nodes {
27
27
  id
28
28
  isResolved
@@ -37,6 +37,10 @@ const REVIEW_THREADS_QUERY = `
37
37
  }
38
38
  }
39
39
  }
40
+ pageInfo {
41
+ hasNextPage
42
+ endCursor
43
+ }
40
44
  }
41
45
  }
42
46
  }
@@ -100,6 +104,10 @@ type ThreadsQueryResult = {
100
104
  pullRequest: {
101
105
  reviewThreads: {
102
106
  nodes: ThreadNode[];
107
+ pageInfo: {
108
+ hasNextPage: boolean;
109
+ endCursor: string | null;
110
+ };
103
111
  };
104
112
  };
105
113
  };
@@ -156,6 +164,78 @@ type ReviewCommentById = {
156
164
  pull_request_url: string;
157
165
  };
158
166
 
167
+ const REST_PAGE_SIZE = 100;
168
+
169
+ const parseJson = <T>(
170
+ stdout: string,
171
+ command: string,
172
+ parseFailurePrefix: string,
173
+ ): Effect.Effect<T, GitHubCommandError> =>
174
+ Effect.try({
175
+ try: () => JSON.parse(stdout) as T,
176
+ catch: (error) =>
177
+ new GitHubCommandError({
178
+ command,
179
+ exitCode: 0,
180
+ stderr: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
181
+ message: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
182
+ }),
183
+ });
184
+
185
+ const fetchAllRestPages = Effect.fn("pr.fetchAllRestPages")(function* <T>(
186
+ endpoint: string,
187
+ command: string,
188
+ parseFailurePrefix: string,
189
+ ) {
190
+ const service = yield* GitHubService;
191
+
192
+ const results: T[] = [];
193
+ let page = 1;
194
+
195
+ while (true) {
196
+ const separator = endpoint.includes("?") ? "&" : "?";
197
+ const result = yield* service.runGh([
198
+ "api",
199
+ `${endpoint}${separator}per_page=${REST_PAGE_SIZE}&page=${page}`,
200
+ ]);
201
+
202
+ const rawPage = yield* parseJson<T[]>(result.stdout, command, parseFailurePrefix);
203
+ results.push(...rawPage);
204
+
205
+ if (rawPage.length < REST_PAGE_SIZE) {
206
+ return results;
207
+ }
208
+
209
+ page += 1;
210
+ }
211
+ });
212
+
213
+ const fetchAllThreadNodes = Effect.fn("pr.fetchAllThreadNodes")(function* (pr: number) {
214
+ const service = yield* GitHubService;
215
+ const repoInfo = yield* service.getRepoInfo();
216
+
217
+ const nodes: ThreadNode[] = [];
218
+ let after: string | null = null;
219
+
220
+ while (true) {
221
+ const response = (yield* service.runGraphQL(REVIEW_THREADS_QUERY, {
222
+ owner: repoInfo.owner,
223
+ name: repoInfo.name,
224
+ pr,
225
+ after,
226
+ })) as ThreadsQueryResult;
227
+
228
+ const page = response.repository.pullRequest.reviewThreads;
229
+ nodes.push(...page.nodes);
230
+
231
+ if (!page.pageInfo.hasNextPage || page.pageInfo.endCursor === null) {
232
+ return nodes;
233
+ }
234
+
235
+ after = page.pageInfo.endCursor;
236
+ }
237
+ });
238
+
159
239
  // ---------------------------------------------------------------------------
160
240
  // Handlers
161
241
  // ---------------------------------------------------------------------------
@@ -176,18 +256,8 @@ export const fetchThreads = Effect.fn("pr.fetchThreads")(function* (
176
256
  pr: number | null,
177
257
  unresolvedOnly: boolean,
178
258
  ) {
179
- const service = yield* GitHubService;
180
- const repoInfo = yield* service.getRepoInfo();
181
-
182
259
  const resolvedPr = pr ?? (yield* viewPR(null)).number;
183
-
184
- const response = (yield* service.runGraphQL(REVIEW_THREADS_QUERY, {
185
- owner: repoInfo.owner,
186
- name: repoInfo.name,
187
- pr: resolvedPr,
188
- })) as ThreadsQueryResult;
189
-
190
- const threads = response.repository.pullRequest.reviewThreads.nodes;
260
+ const threads = yield* fetchAllThreadNodes(resolvedPr);
191
261
 
192
262
  const mapped: ReviewThread[] = threads
193
263
  .map((node) => {
@@ -223,21 +293,11 @@ export const fetchComments = Effect.fn("pr.fetchComments")(function* (
223
293
 
224
294
  const resolvedPr = pr ?? (yield* viewPR(null)).number;
225
295
 
226
- const result = yield* service.runGh([
227
- "api",
296
+ const raw = yield* fetchAllRestPages<RawReviewComment>(
228
297
  `repos/${repoInfo.owner}/${repoInfo.name}/pulls/${resolvedPr}/comments`,
229
- ]);
230
-
231
- const raw = yield* Effect.try({
232
- try: () => JSON.parse(result.stdout) as RawReviewComment[],
233
- catch: (error) =>
234
- new GitHubCommandError({
235
- command: "gh-tool pr comments",
236
- exitCode: 0,
237
- stderr: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
238
- message: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
239
- }),
240
- });
298
+ "gh-tool pr comments",
299
+ "Failed to parse response",
300
+ );
241
301
 
242
302
  const comments: ReviewComment[] = raw.map((c) => ({
243
303
  id: c.id,
@@ -272,21 +332,11 @@ export const fetchIssueComments = Effect.fn("pr.fetchIssueComments")(function* (
272
332
 
273
333
  const resolvedPr = pr ?? (yield* viewPR(null)).number;
274
334
 
275
- const result = yield* service.runGh([
276
- "api",
335
+ const raw = yield* fetchAllRestPages<RawIssueComment>(
277
336
  `repos/${repoInfo.owner}/${repoInfo.name}/issues/${resolvedPr}/comments`,
278
- ]);
279
-
280
- const raw = yield* Effect.try({
281
- try: () => JSON.parse(result.stdout) as RawIssueComment[],
282
- catch: (error) =>
283
- new GitHubCommandError({
284
- command: "gh-tool pr issue-comments",
285
- exitCode: 0,
286
- stderr: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
287
- message: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
288
- }),
289
- });
337
+ "gh-tool pr issue-comments",
338
+ "Failed to parse response",
339
+ );
290
340
 
291
341
  let comments = raw.map(mapRawIssueComment);
292
342
 
@@ -388,12 +438,30 @@ export const fetchDiscussionSummary = Effect.fn("pr.fetchDiscussionSummary")(fun
388
438
  : current,
389
439
  );
390
440
 
441
+ const repliedThreadCommentIds = new Set(
442
+ reviewComments
443
+ .filter((comment) => comment.inReplyToId !== null)
444
+ .map((comment) => comment.inReplyToId),
445
+ );
446
+
447
+ const unrepliedReviewThreadsCount = threads.filter(
448
+ (thread) => !repliedThreadCommentIds.has(thread.commentId),
449
+ ).length;
450
+
391
451
  return {
392
452
  issueCommentsCount: issueComments.length,
393
453
  latestIssueComment,
394
454
  reviewCommentsCount: reviewComments.length,
395
455
  reviewThreadsCount: threads.length,
456
+ repliedReviewThreadsCount: threads.length - unrepliedReviewThreadsCount,
457
+ unrepliedReviewThreadsCount,
458
+ resolvedUnrepliedReviewThreadsCount: threads.filter(
459
+ (thread) => thread.isResolved && !repliedThreadCommentIds.has(thread.commentId),
460
+ ).length,
396
461
  unresolvedReviewThreadsCount: threads.filter((thread) => !thread.isResolved).length,
462
+ unresolvedUnrepliedReviewThreadsCount: threads.filter(
463
+ (thread) => !thread.isResolved && !repliedThreadCommentIds.has(thread.commentId),
464
+ ).length,
397
465
  };
398
466
  });
399
467
 
@@ -22,7 +22,7 @@ export class GitHubService extends ServiceMap.Service<
22
22
  readonly runGhJson: <T>(args: string[]) => Effect.Effect<T, GhError>;
23
23
  readonly runGraphQL: (
24
24
  query: string,
25
- variables: Record<string, string | number>,
25
+ variables: Record<string, string | number | null>,
26
26
  ) => Effect.Effect<unknown, GhError>;
27
27
  readonly getRepoInfo: () => Effect.Effect<RepoInfo, GhError>;
28
28
  }
@@ -135,11 +135,15 @@ export class GitHubService extends ServiceMap.Service<
135
135
 
136
136
  const runGraphQL = Effect.fn("GitHubService.runGraphQL")(function* (
137
137
  query: string,
138
- variables: Record<string, string | number>,
138
+ variables: Record<string, string | number | null>,
139
139
  ) {
140
140
  const args = ["api", "graphql", "-f", `query=${query}`];
141
141
 
142
142
  for (const [key, value] of Object.entries(variables)) {
143
+ if (value === null) {
144
+ continue;
145
+ }
146
+
143
147
  if (typeof value === "number") {
144
148
  args.push("-F", `${key}=${value}`);
145
149
  } else {