@blogic-cz/agent-tools 0.14.48 → 0.14.50

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.14.48",
3
+ "version": "0.14.50",
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",
@@ -31,8 +31,10 @@ import {
31
31
  prIssueCommentsLatestCommand,
32
32
  prCommentCommand,
33
33
  prDiscussionSummaryCommand,
34
+ prFeedbackCommand,
34
35
  prReplyCommand,
35
36
  prResolveCommand,
37
+ prReviewsCommand,
36
38
  prSubmitReviewCommand,
37
39
  prChecksCommand,
38
40
  prChecksFailedCommand,
@@ -79,6 +81,8 @@ const prCommand = Command.make("pr", {}).pipe(
79
81
  prWaitMergeableCommand,
80
82
  prThreadsCommand,
81
83
  prCommentsCommand,
84
+ prReviewsCommand,
85
+ prFeedbackCommand,
82
86
  prIssueCommentsCommand,
83
87
  prIssueCommentsLatestCommand,
84
88
  prCommentCommand,
@@ -44,8 +44,10 @@ import {
44
44
  import {
45
45
  fetchComments,
46
46
  fetchDiscussionSummary,
47
+ fetchFeedback,
47
48
  fetchIssueComments,
48
49
  fetchLatestIssueComment,
50
+ fetchReviews,
49
51
  fetchThreads,
50
52
  postIssueComment,
51
53
  replyToComment,
@@ -101,13 +103,16 @@ export const parsePrNumbers = (input: string): readonly number[] =>
101
103
  export const fetchReviewTriage = Effect.fn("pr.fetchReviewTriage")(function* (
102
104
  prNumber: number | null,
103
105
  ) {
104
- const [info, unresolvedThreads, visibleOpenThreads, summary, checks] = yield* Effect.all([
105
- viewPR(prNumber),
106
- fetchThreads(prNumber, true),
107
- fetchThreads(prNumber, false, true),
108
- fetchDiscussionSummary(prNumber),
109
- fetchChecks(prNumber, false, false, 0),
110
- ]);
106
+ const [info, unresolvedThreads, visibleOpenThreads, summary, checks, reviews] = yield* Effect.all(
107
+ [
108
+ viewPR(prNumber),
109
+ fetchThreads(prNumber, true),
110
+ fetchThreads(prNumber, false, true),
111
+ fetchDiscussionSummary(prNumber),
112
+ fetchChecks(prNumber, false, false, 0),
113
+ fetchReviews(prNumber, null, null, null),
114
+ ],
115
+ );
111
116
  const classification = classifyReviewTriage(summary, checks);
112
117
 
113
118
  // Single merge-readiness verdict so agents stop re-stitching mergeable + checks + threads +
@@ -127,7 +132,16 @@ export const fetchReviewTriage = Effect.fn("pr.fetchReviewTriage")(function* (
127
132
  blocking,
128
133
  };
129
134
 
130
- return { ready, classification, info, unresolvedThreads, visibleOpenThreads, summary, checks };
135
+ return {
136
+ ready,
137
+ classification,
138
+ info,
139
+ unresolvedThreads,
140
+ visibleOpenThreads,
141
+ summary,
142
+ checks,
143
+ reviews,
144
+ };
131
145
  });
132
146
 
133
147
  export const prViewCommand = Command.make(
@@ -606,6 +620,73 @@ export const prCommentsCommand = Command.make(
606
620
  ),
607
621
  ).pipe(Command.withDescription("Fetch review comments for a PR (optionally filter by --since)"));
608
622
 
623
+ export const prReviewsCommand = Command.make(
624
+ "reviews",
625
+ {
626
+ author: Flag.string("author").pipe(
627
+ Flag.withDescription("Filter by author login substring"),
628
+ Flag.optional,
629
+ ),
630
+ bodyContains: Flag.string("body-contains").pipe(
631
+ Flag.withDescription("Filter reviews by body substring"),
632
+ Flag.optional,
633
+ ),
634
+ format: formatOption,
635
+ pr: Flag.integer("pr").pipe(
636
+ Flag.withDescription("PR number (default: current branch PR)"),
637
+ Flag.optional,
638
+ ),
639
+ repo: repoOption,
640
+ state: Flag.string("state").pipe(
641
+ Flag.withDescription(
642
+ "Filter by review state (APPROVED, CHANGES_REQUESTED, COMMENTED, DISMISSED)",
643
+ ),
644
+ Flag.optional,
645
+ ),
646
+ },
647
+ ({ author, bodyContains, format, pr, repo, state }) =>
648
+ withRepo(
649
+ repo,
650
+ Effect.gen(function* () {
651
+ const reviews = yield* fetchReviews(
652
+ Option.getOrNull(pr),
653
+ Option.getOrNull(author),
654
+ Option.getOrNull(bodyContains),
655
+ Option.getOrNull(state),
656
+ );
657
+ yield* logFormatted(reviews, format);
658
+ }),
659
+ ),
660
+ ).pipe(
661
+ Command.withDescription(
662
+ "Fetch submitted review summaries (state + body) for a PR — the review bodies that comments/issue-comments do not expose",
663
+ ),
664
+ );
665
+
666
+ export const prFeedbackCommand = Command.make(
667
+ "feedback",
668
+ {
669
+ format: formatOption,
670
+ pr: Flag.integer("pr").pipe(
671
+ Flag.withDescription("PR number (default: current branch PR)"),
672
+ Flag.optional,
673
+ ),
674
+ repo: repoOption,
675
+ },
676
+ ({ format, pr, repo }) =>
677
+ withRepo(
678
+ repo,
679
+ Effect.gen(function* () {
680
+ const feedback = yield* fetchFeedback(Option.getOrNull(pr));
681
+ yield* logFormatted(feedback, format);
682
+ }),
683
+ ),
684
+ ).pipe(
685
+ Command.withDescription(
686
+ "Full review-response inventory in one call: submitted reviews + threads (with state) + inline comments + issue comments",
687
+ ),
688
+ );
689
+
609
690
  export const prIssueCommentsCommand = Command.make(
610
691
  "issue-comments",
611
692
  {
@@ -7,6 +7,7 @@ export {
7
7
  prCreateCommand,
8
8
  prDiscussionSummaryCommand,
9
9
  prEditCommand,
10
+ prFeedbackCommand,
10
11
  prIssueCommentsCommand,
11
12
  prIssueCommentsLatestCommand,
12
13
  prListCommand,
@@ -15,6 +16,7 @@ export {
15
16
  prRerunChecksCommand,
16
17
  prReplyAndResolveCommand,
17
18
  prResolveCommand,
19
+ prReviewsCommand,
18
20
  prStatusCommand,
19
21
  prSubmitReviewCommand,
20
22
  prThreadsCommand,
@@ -5,6 +5,7 @@ import type {
5
5
  IssueComment,
6
6
  IssueCommentId,
7
7
  IsoTimestamp,
8
+ PullRequestReview,
8
9
  ReviewComment,
9
10
  ReviewThread,
10
11
  } from "#gh/types";
@@ -158,6 +159,15 @@ type RawIssueComment = {
158
159
  html_url: string;
159
160
  };
160
161
 
162
+ type RawPullRequestReview = {
163
+ id: number;
164
+ user: { login: string } | null;
165
+ state: string;
166
+ body: string | null;
167
+ submitted_at: string | null;
168
+ html_url: string;
169
+ };
170
+
161
171
  type ReviewCommentById = {
162
172
  id: number;
163
173
  in_reply_to_id: number | null;
@@ -405,6 +415,73 @@ export const fetchIssueComments = Effect.fn("pr.fetchIssueComments")(function* (
405
415
  return comments;
406
416
  });
407
417
 
418
+ /**
419
+ * Fetch submitted review summaries (state + top-level body) for a PR via REST API.
420
+ * Complements `comments` (inline review comments) and `issue-comments` (discussion),
421
+ * which never expose the submitted review body. Optional author/state/body filters.
422
+ */
423
+ export const fetchReviews = Effect.fn("pr.fetchReviews")(function* (
424
+ pr: number | null,
425
+ author: string | null,
426
+ bodyContains: string | null,
427
+ state: string | null,
428
+ ) {
429
+ const service = yield* GitHubService;
430
+ const repoInfo = yield* service.getRepoInfo();
431
+
432
+ const resolvedPr = pr ?? (yield* viewPR(null)).number;
433
+
434
+ const raw = yield* fetchAllRestPages<RawPullRequestReview>(
435
+ `repos/${repoInfo.owner}/${repoInfo.name}/pulls/${resolvedPr}/reviews`,
436
+ "gh-tool pr reviews",
437
+ "Failed to parse response",
438
+ );
439
+
440
+ let reviews: PullRequestReview[] = raw.map((review) => ({
441
+ id: review.id,
442
+ author: review.user?.login ?? "unknown",
443
+ state: review.state,
444
+ body: review.body ?? "",
445
+ submittedAt: (review.submitted_at ?? null) as IsoTimestamp | null,
446
+ url: review.html_url,
447
+ }));
448
+
449
+ if (author !== null) {
450
+ const authorFilter = author.toLowerCase();
451
+ reviews = reviews.filter((review) => review.author.toLowerCase().includes(authorFilter));
452
+ }
453
+
454
+ if (state !== null) {
455
+ const stateFilter = state.toLowerCase();
456
+ reviews = reviews.filter((review) => review.state.toLowerCase() === stateFilter);
457
+ }
458
+
459
+ if (bodyContains !== null) {
460
+ const bodyFilter = bodyContains.toLowerCase();
461
+ reviews = reviews.filter((review) => review.body.toLowerCase().includes(bodyFilter));
462
+ }
463
+
464
+ return reviews;
465
+ });
466
+
467
+ /**
468
+ * Full review-response inventory for a PR in one call: submitted review summaries,
469
+ * all review threads with resolution state, inline review comments, and issue
470
+ * (discussion) comments. Collapses the four separate fetches agents otherwise stitch.
471
+ */
472
+ export const fetchFeedback = Effect.fn("pr.fetchFeedback")(function* (pr: number | null) {
473
+ const resolvedPr = pr ?? (yield* viewPR(null)).number;
474
+
475
+ const [reviews, threads, inlineComments, issueComments] = yield* Effect.all([
476
+ fetchReviews(resolvedPr, null, null, null),
477
+ fetchThreads(resolvedPr, false),
478
+ fetchComments(resolvedPr, null),
479
+ fetchIssueComments(resolvedPr, null, null, null),
480
+ ]);
481
+
482
+ return { reviews, threads, inlineComments, issueComments };
483
+ });
484
+
408
485
  export const fetchLatestIssueComment = Effect.fn("pr.fetchLatestIssueComment")(function* (
409
486
  pr: number | null,
410
487
  author: string | null,
@@ -66,6 +66,15 @@ export type IssueComment = {
66
66
  url: GitHubIssueCommentUrl;
67
67
  };
68
68
 
69
+ export type PullRequestReview = {
70
+ id: number;
71
+ author: string;
72
+ state: string;
73
+ body: string;
74
+ submittedAt: IsoTimestamp | null;
75
+ url: string;
76
+ };
77
+
69
78
  export type CheckResult = {
70
79
  name: string;
71
80
  state: string;
@@ -245,10 +245,39 @@ export const dispatchWorkflow = Effect.fn("workflow.dispatchWorkflow")(function*
245
245
  // then fall back to a one-shot snapshot so a timeout never returns nothing.
246
246
  const DEFAULT_WATCH_RUN_TIMEOUT_SECONDS = CI_CHECK_WATCH_TIMEOUT_MS / 1000;
247
247
 
248
+ export const buildWatchResult = (
249
+ runId: number,
250
+ finalState: {
251
+ status: string;
252
+ conclusion: string | null;
253
+ jobs: ReadonlyArray<{ name: string; status: string; conclusion: string | null }>;
254
+ },
255
+ watchStdout: string | null,
256
+ frames: boolean,
257
+ timeoutSeconds: number,
258
+ ) => ({
259
+ runId,
260
+ status: finalState.status,
261
+ conclusion: finalState.conclusion,
262
+ jobs: finalState.jobs.map((job) => ({
263
+ name: job.name,
264
+ status: job.status,
265
+ conclusion: job.conclusion,
266
+ })),
267
+ ...(watchStdout === null
268
+ ? {
269
+ watchOutput: `(watch timed out after ${timeoutSeconds}s; status taken from snapshot — re-run to keep watching)`,
270
+ }
271
+ : frames
272
+ ? { watchOutput: watchStdout }
273
+ : {}),
274
+ });
275
+
248
276
  const watchRun = Effect.fn("workflow.watchRun")(function* (
249
277
  runId: number,
250
278
  repo: string | null,
251
279
  timeoutSeconds: number,
280
+ frames: boolean,
252
281
  ) {
253
282
  const gh = yield* GitHubService;
254
283
 
@@ -277,20 +306,13 @@ const watchRun = Effect.fn("workflow.watchRun")(function* (
277
306
 
278
307
  const finalState = yield* viewRun(runId, repo);
279
308
 
280
- return {
309
+ return buildWatchResult(
281
310
  runId,
282
- status: finalState.status,
283
- conclusion: finalState.conclusion,
284
- jobs: finalState.jobs.map((job) => ({
285
- name: job.name,
286
- status: job.status,
287
- conclusion: job.conclusion,
288
- })),
289
- watchOutput:
290
- result === null
291
- ? `(watch timed out after ${timeoutSeconds}s; status taken from snapshot — re-run to keep watching)`
292
- : result.stdout,
293
- };
311
+ finalState,
312
+ result === null ? null : result.stdout,
313
+ frames,
314
+ timeoutSeconds,
315
+ );
294
316
  });
295
317
 
296
318
  const fetchAnnotations = Effect.fn("workflow.fetchAnnotations")(function* (opts: {
@@ -712,6 +734,11 @@ export const workflowWatchCommand = Command.make(
712
734
  "watch",
713
735
  {
714
736
  format: formatOption,
737
+ frames: Flag.boolean("frames").pipe(
738
+ Flag.withDescription(
739
+ "Include the raw watch progress frames (large); omitted by default — final status/conclusion/jobs are always returned",
740
+ ),
741
+ ),
715
742
  repo: repoOption,
716
743
  run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID to watch")),
717
744
  timeout: Flag.integer("timeout").pipe(
@@ -725,10 +752,10 @@ export const workflowWatchCommand = Command.make(
725
752
  ),
726
753
  ),
727
754
  },
728
- ({ format, repo, run, timeout }) =>
755
+ ({ format, frames, repo, run, timeout }) =>
729
756
  Effect.gen(function* () {
730
757
  const resolvedRepo = yield* resolveRepoArg(repo);
731
- const result = yield* watchRun(run, resolvedRepo, timeout);
758
+ const result = yield* watchRun(run, resolvedRepo, timeout, frames);
732
759
  yield* logFormatted(result, format);
733
760
  }),
734
761
  ).pipe(Command.withDescription("Watch a workflow run until it completes, then show final status"));