opencode-forgekeeper 2.1.0 → 2.2.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 (4) hide show
  1. package/README.md +39 -14
  2. package/dist/index.js +543 -193
  3. package/dist/tui.js +263 -29
  4. package/package.json +1 -1
package/dist/tui.js CHANGED
@@ -73,14 +73,182 @@ class ForgeError extends Error {
73
73
  var query = (text) => text;
74
74
  var typed = (transport) => async (host, query2, variables) => await transport(host, query2, variables);
75
75
 
76
- // packages/forgekeeper/src/gitlab-feedback.ts
76
+ // packages/forgekeeper/src/github-feedback.ts
77
77
  var PAGE_SIZE = 100;
78
78
  var MAX_COMMENT_PAGES = 20;
79
+ var MAX_FETCH_REQUESTS = 200;
80
+ var author = "author { __typename login ... on User { id } ... on Bot { id } ... on Mannequin { id } }";
81
+ var page = "pageInfo { hasPreviousPage startCursor }";
82
+ var INLINE_FIELDS = `id url state createdAt publishedAt lastEditedAt isMinimized ${author}`;
83
+ var listQuery = (field, connection) => query(`query($owner: String!, $repo: String!, $number: Int!, $before: String) {
84
+ viewer { id }
85
+ repository(owner: $owner, name: $repo) {
86
+ pullRequest(number: $number) {
87
+ ${field}(last: ${PAGE_SIZE}, before: $before) { ${page} nodes { ${connection} } }
88
+ }
89
+ }
90
+ }`);
91
+ var ISSUE_COMMENTS_QUERY = listQuery("comments", `id url createdAt publishedAt lastEditedAt isMinimized ${author}`);
92
+ var REVIEWS_QUERY = listQuery("reviews", `id url state body submittedAt lastEditedAt isMinimized ${author}`);
93
+ var THREADS_QUERY = listQuery("reviewThreads", `id isResolved comments(last: ${PAGE_SIZE}) { ${page} nodes { ${INLINE_FIELDS} } }`);
94
+ var THREAD_COMMENTS_QUERY = query(`query($id: ID!, $before: String) {
95
+ node(id: $id) {
96
+ ... on PullRequestReviewThread {
97
+ comments(last: ${PAGE_SIZE}, before: $before) { ${page} nodes { ${INLINE_FIELDS} } }
98
+ }
99
+ }
100
+ }`);
101
+ var REVIEW_STATES = new Set(["COMMENTED", "CHANGES_REQUESTED", "APPROVED"]);
102
+ var time = (...values) => {
103
+ const value = values.find((candidate) => candidate);
104
+ return value ? Date.parse(value) : NaN;
105
+ };
106
+ var activity = (lastEditedAt, postedAt) => {
107
+ const edited = lastEditedAt ? Date.parse(lastEditedAt) : NaN;
108
+ return Number.isFinite(edited) ? Math.max(edited, postedAt) : postedAt;
109
+ };
110
+ function build({ node, postedAt, editedAt, resolved }) {
111
+ if (node.isMinimized || !node.author?.id || !Number.isFinite(postedAt) || !Number.isFinite(editedAt))
112
+ return;
113
+ const { __typename, login } = node.author;
114
+ return {
115
+ id: node.id,
116
+ order: postedAt,
117
+ url: node.url ?? "",
118
+ editedAt,
119
+ resolved,
120
+ bot: __typename === "Bot" || login.endsWith("[bot]"),
121
+ authorID: node.author.id,
122
+ username: login
123
+ };
124
+ }
125
+ function toIssueComment(node) {
126
+ const postedAt = time(node.publishedAt, node.createdAt);
127
+ return build({
128
+ node,
129
+ postedAt,
130
+ editedAt: activity(node.lastEditedAt, postedAt),
131
+ resolved: false
132
+ });
133
+ }
134
+ function toReview(node) {
135
+ if (!REVIEW_STATES.has(node.state) || !node.body.trim())
136
+ return;
137
+ const postedAt = time(node.submittedAt);
138
+ return build({
139
+ node,
140
+ postedAt,
141
+ editedAt: activity(node.lastEditedAt, postedAt),
142
+ resolved: false
143
+ });
144
+ }
145
+ function toInlineComment(node, resolved) {
146
+ if (node.state === "PENDING")
147
+ return;
148
+ const postedAt = time(node.publishedAt, node.createdAt);
149
+ return build({
150
+ node,
151
+ postedAt,
152
+ editedAt: activity(node.lastEditedAt, postedAt),
153
+ resolved
154
+ });
155
+ }
156
+ var commentWindow = { pages: MAX_COMMENT_PAGES, completeAtLimit: true };
157
+ var allThreads = { pages: MAX_FETCH_REQUESTS, completeAtLimit: false };
158
+ async function readList(graphql, host, base, text, field, limit = commentWindow) {
159
+ const pages = [];
160
+ let viewer;
161
+ let before;
162
+ const result = (complete) => ({
163
+ items: pages.toReversed().flat(),
164
+ viewer,
165
+ complete
166
+ });
167
+ for (let count = 0;count < limit.pages; count++) {
168
+ const variables = before ? { ...base, before } : base;
169
+ const data = await graphql(host, text, variables);
170
+ viewer ??= data.viewer?.id ?? undefined;
171
+ const connection = data.repository?.pullRequest?.[field];
172
+ if (!connection)
173
+ return count === 0 ? { viewer, complete: false } : result(false);
174
+ pages.push(connection.nodes);
175
+ if (!connection.pageInfo.hasPreviousPage)
176
+ return result(true);
177
+ before = connection.pageInfo.startCursor ?? undefined;
178
+ if (!before)
179
+ return result(false);
180
+ }
181
+ return result(limit.completeAtLimit);
182
+ }
183
+ async function readOlderComments(graphql, host, thread) {
184
+ if (!thread.comments)
185
+ return { comments: [], complete: false };
186
+ const pages = [];
187
+ let before = thread.comments?.pageInfo.hasPreviousPage ? thread.comments.pageInfo.startCursor ?? undefined : undefined;
188
+ if (thread.comments?.pageInfo.hasPreviousPage && !before)
189
+ return { comments: [], complete: false };
190
+ for (let count = 1;before && count < MAX_COMMENT_PAGES; count++) {
191
+ const data = await graphql(host, THREAD_COMMENTS_QUERY, { id: thread.id, before });
192
+ const connection = data.node?.comments;
193
+ if (!connection)
194
+ return { comments: pages.toReversed().flat(), complete: false };
195
+ pages.push(connection.nodes);
196
+ before = connection.pageInfo.hasPreviousPage ? connection.pageInfo.startCursor ?? undefined : undefined;
197
+ if (connection.pageInfo.hasPreviousPage && !before)
198
+ return { comments: pages.toReversed().flat(), complete: false };
199
+ }
200
+ return { comments: pages.toReversed().flat(), complete: true };
201
+ }
202
+ async function fetchComments(transport, host, project, number) {
203
+ let requests = 0;
204
+ const graphql = typed(async (...args) => {
205
+ if (requests++ >= MAX_FETCH_REQUESTS)
206
+ return {};
207
+ return transport(...args);
208
+ });
209
+ const [owner, repo] = project.split("/");
210
+ const base = { owner, repo, number: Number(number) };
211
+ const [conversation, reviews, threads] = await Promise.all([
212
+ readList(graphql, host, base, ISSUE_COMMENTS_QUERY, "comments"),
213
+ readList(graphql, host, base, REVIEWS_QUERY, "reviews"),
214
+ readList(graphql, host, base, THREADS_QUERY, "reviewThreads", allThreads)
215
+ ]);
216
+ if (!conversation.items || !reviews.items || !threads.items)
217
+ return;
218
+ const viewerID = conversation.viewer ?? reviews.viewer ?? threads.viewer;
219
+ let threadsComplete = threads.complete;
220
+ const inline = [];
221
+ for (const thread of threads.items) {
222
+ const older = await readOlderComments(graphql, host, thread);
223
+ threadsComplete &&= older.complete;
224
+ for (const node of [...older.comments, ...thread.comments?.nodes ?? []]) {
225
+ const comment = toInlineComment(node, thread.isResolved);
226
+ if (comment)
227
+ inline.push(comment);
228
+ }
229
+ inline.sort((a, b) => b.order - a.order);
230
+ inline.splice(PAGE_SIZE * MAX_COMMENT_PAGES);
231
+ }
232
+ const comments = [
233
+ ...conversation.items.map(toIssueComment),
234
+ ...reviews.items.map(toReview),
235
+ ...inline.sort((a, b) => a.order - b.order).slice(-PAGE_SIZE * MAX_COMMENT_PAGES)
236
+ ].flatMap((comment) => comment ?? []);
237
+ return {
238
+ complete: conversation.complete && reviews.complete && threadsComplete && viewerID !== undefined,
239
+ viewerID,
240
+ comments: comments.sort((a, b) => a.order - b.order)
241
+ };
242
+ }
243
+
244
+ // packages/forgekeeper/src/gitlab-feedback.ts
245
+ var PAGE_SIZE2 = 100;
246
+ var MAX_COMMENT_PAGES2 = 20;
79
247
  var COMMENTS_QUERY = query(`query($project: ID!, $iid: String!, $before: String) {
80
248
  currentUser { id }
81
249
  project(fullPath: $project) {
82
250
  mergeRequest(iid: $iid) {
83
- notes(last: ${PAGE_SIZE}, before: $before, filter: ONLY_COMMENTS) {
251
+ notes(last: ${PAGE_SIZE2}, before: $before, filter: ONLY_COMMENTS) {
84
252
  pageInfo { hasPreviousPage startCursor }
85
253
  nodes {
86
254
  id url createdAt updatedAt lastEditedAt system
@@ -109,7 +277,7 @@ function toComment(node) {
109
277
  username: node.author.username
110
278
  };
111
279
  }
112
- async function fetchComments(transport, host, project, iid) {
280
+ async function fetchComments2(transport, host, project, iid) {
113
281
  const graphql = typed(transport);
114
282
  const pages = [];
115
283
  let viewerID;
@@ -119,7 +287,7 @@ async function fetchComments(transport, host, project, iid) {
119
287
  viewerID,
120
288
  comments: pages.toReversed().flat()
121
289
  });
122
- for (let page = 0;page < MAX_COMMENT_PAGES; page++) {
290
+ for (let page2 = 0;page2 < MAX_COMMENT_PAGES2; page2++) {
123
291
  const variables = { project, iid };
124
292
  if (before)
125
293
  variables.before = before;
@@ -127,7 +295,7 @@ async function fetchComments(transport, host, project, iid) {
127
295
  viewerID ??= data.currentUser?.id ?? undefined;
128
296
  const connection = data.project?.mergeRequest?.notes;
129
297
  if (!connection)
130
- return page === 0 ? undefined : snapshot(false);
298
+ return page2 === 0 ? undefined : snapshot(false);
131
299
  pages.push(connection.nodes.flatMap((node) => toComment(node) ?? []));
132
300
  if (!connection.pageInfo.hasPreviousPage)
133
301
  return snapshot(true);
@@ -140,7 +308,7 @@ async function fetchComments(transport, host, project, iid) {
140
308
 
141
309
  // packages/forgekeeper/src/gitlab.ts
142
310
  var DISCUSSIONS = "pageInfo { hasNextPage endCursor } nodes { resolvable resolved }";
143
- var PAGE_SIZE2 = 100;
311
+ var PAGE_SIZE3 = 100;
144
312
  var MAX_DISCUSSION_PAGES = 50;
145
313
  var MERGE_REQUEST_FIELDS = `
146
314
  iid title webUrl state draft conflicts approved detailedMergeStatus diffHeadSha updatedAt targetBranch
@@ -149,7 +317,7 @@ var MERGE_REQUEST_FIELDS = `
149
317
  targetProject { fullPath }
150
318
  headPipeline { status path detailedStatus { label } }
151
319
  reviewers(first: 100) { nodes { type username bot mergeRequestInteraction { reviewState approved } } }
152
- discussions(first: ${PAGE_SIZE2}) { ${DISCUSSIONS} }
320
+ discussions(first: ${PAGE_SIZE3}) { ${DISCUSSIONS} }
153
321
  `;
154
322
  function mergeRequestsQuery(projectCount) {
155
323
  const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
@@ -184,7 +352,7 @@ var SOURCE_BRANCH_QUERY = query(`query($project: ID!, $iid: String!) {
184
352
  }`);
185
353
  var DISCUSSIONS_QUERY = query(`query($project: ID!, $iid: String!, $after: String) {
186
354
  project(fullPath: $project) {
187
- mergeRequest(iid: $iid) { discussions(first: ${PAGE_SIZE2}, after: $after) { ${DISCUSSIONS} } }
355
+ mergeRequest(iid: $iid) { discussions(first: ${PAGE_SIZE3}, after: $after) { ${DISCUSSIONS} } }
188
356
  }
189
357
  }`);
190
358
  function classifyError(message, code) {
@@ -223,7 +391,7 @@ function glabGraphQL(run, cwd) {
223
391
  return body.data;
224
392
  };
225
393
  }
226
- var countUnresolved = (page) => page.nodes.filter((discussion) => discussion.resolvable && !discussion.resolved).length;
394
+ var countUnresolved = (page2) => page2.nodes.filter((discussion) => discussion.resolvable && !discussion.resolved).length;
227
395
  var gitlabTraits = {
228
396
  kind: "gitlab",
229
397
  noun: "MR",
@@ -258,7 +426,7 @@ var gitlabTraits = {
258
426
  feedback: {
259
427
  async fetch(run, directory, request) {
260
428
  const ref = gitlabTraits.parseUrl(request.url);
261
- return ref ? fetchComments(glabGraphQL(run, directory), ref.host, ref.project, ref.iid) : undefined;
429
+ return ref ? fetchComments2(glabGraphQL(run, directory), ref.host, ref.project, ref.iid) : undefined;
262
430
  }
263
431
  }
264
432
  };
@@ -330,22 +498,22 @@ class GitLabForge {
330
498
  }
331
499
  async unresolvedThreads(host, node) {
332
500
  let count = countUnresolved(node.discussions);
333
- let page = node.discussions.pageInfo;
501
+ let page2 = node.discussions.pageInfo;
334
502
  let pages = 1;
335
- while (page.hasNextPage && page.endCursor && pages < MAX_DISCUSSION_PAGES) {
503
+ while (page2.hasNextPage && page2.endCursor && pages < MAX_DISCUSSION_PAGES) {
336
504
  const data = await this.graphql(host, DISCUSSIONS_QUERY, {
337
505
  project: node.targetProject.fullPath,
338
506
  iid: node.iid,
339
- after: page.endCursor
507
+ after: page2.endCursor
340
508
  });
341
509
  const next = data.project?.mergeRequest?.discussions;
342
510
  if (!next)
343
511
  break;
344
512
  count += countUnresolved(next);
345
- page = next.pageInfo;
513
+ page2 = next.pageInfo;
346
514
  pages++;
347
515
  }
348
- return { count, complete: !page.hasNextPage };
516
+ return { count, complete: !page2.hasNextPage };
349
517
  }
350
518
  async normalize(host, node) {
351
519
  const threads = await this.unresolvedThreads(host, node);
@@ -384,10 +552,12 @@ class GitLabForge {
384
552
  }
385
553
 
386
554
  // packages/forgekeeper/src/github.ts
555
+ var COPILOT_AUTHOR = "copilot-pull-request-reviewer[bot]";
387
556
  var fields = `number title url state isDraft mergeable reviewDecision headRefOid headRefName
388
557
  headRepository { nameWithOwner } baseRepository { nameWithOwner } baseRefName updatedAt
389
558
  reviewThreads(first:100) { nodes { isResolved } pageInfo { hasNextPage endCursor } }
390
- reviewRequests(first:100) { nodes { requestedReviewer { ... on User { login } ... on Team { name } } } pageInfo { hasNextPage } }
559
+ reviewRequests(first:100) { nodes { requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { name } } } pageInfo { hasNextPage endCursor } }
560
+ copilotReviews: reviews(last:1, author:"${COPILOT_AUTHOR}", states:[COMMENTED,CHANGES_REQUESTED,APPROVED,DISMISSED]) { nodes { id state comments { totalCount } } }
391
561
  commits(last:1) { nodes { commit { statusCheckRollup { contexts(first:100) {
392
562
  nodes { __typename ... on CheckRun { status conclusion detailsUrl } ... on StatusContext { state targetUrl } }
393
563
  pageInfo { hasNextPage endCursor }
@@ -481,8 +651,43 @@ var githubTraits = {
481
651
  indicators: (request) => request.reviewDecision === "CHANGES_REQUESTED" ? [{ text: "changes requested", tone: "warning", essential: true }] : [],
482
652
  details: (request) => [
483
653
  `Review decision: ${request.reviewDecision ? humanize(request.reviewDecision) : "not reported"}`
484
- ]
654
+ ],
655
+ automatedReview: {
656
+ name: "GitHub Copilot",
657
+ status: (request) => copilotStatus(request.copilotReview)
658
+ },
659
+ feedback: {
660
+ async fetch(run, directory, request) {
661
+ const ref = githubTraits.parseUrl(request.url);
662
+ return ref ? fetchComments(ghGraphQL(run, directory), ref.host, ref.project, ref.iid) : undefined;
663
+ }
664
+ }
485
665
  };
666
+ function copilotStatus(review) {
667
+ if (!review)
668
+ return;
669
+ const feedback = review.comments > 0 && !review.dismissed;
670
+ const label = review.dismissed ? "dismissed" : feedback ? "commented" : "no comments";
671
+ const completed = review.reviewID ? { id: review.reviewID, feedback, label } : undefined;
672
+ if (review.requested)
673
+ return { state: "running", label: "reviewing", completed };
674
+ if (!completed)
675
+ return;
676
+ return { state: feedback ? "feedback" : "settled", label, completed };
677
+ }
678
+ var isCopilotReviewer = (login) => /^copilot-pull-request-reviewer(\[bot\])?$/i.test(login ?? "");
679
+ function copilotReview(node) {
680
+ const requested = node.reviewRequests.nodes.some((request) => isCopilotReviewer(request.requestedReviewer?.login));
681
+ const latest = node.copilotReviews?.nodes?.find((review) => review);
682
+ if (!requested && !latest)
683
+ return;
684
+ return {
685
+ requested,
686
+ reviewID: latest?.id,
687
+ comments: latest?.comments.totalCount ?? 0,
688
+ ...latest?.state === "DISMISSED" ? { dismissed: true } : {}
689
+ };
690
+ }
486
691
 
487
692
  class GitHubForge {
488
693
  traits = githubTraits;
@@ -533,7 +738,7 @@ class GitHubForge {
533
738
  const [owner, repo] = target.path.split("/");
534
739
  let after;
535
740
  let complete = false;
536
- for (let page = 0;page < MAX_PAGES; page++) {
741
+ for (let page2 = 0;page2 < MAX_PAGES; page2++) {
537
742
  const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
538
743
  repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch${after ? ",after:$after" : ""}) {
539
744
  nodes { ${fields} } pageInfo { hasNextPage endCursor }
@@ -563,21 +768,21 @@ class GitHubForge {
563
768
  async threads(host, node) {
564
769
  const [owner, repo] = node.baseRepository.nameWithOwner.split("/");
565
770
  const threads = [...node.reviewThreads.nodes];
566
- let page = node.reviewThreads.pageInfo;
567
- for (let count = 1;page?.hasNextPage && page.endCursor && count < MAX_PAGES; count++) {
771
+ let page2 = node.reviewThreads.pageInfo;
772
+ for (let count = 1;page2?.hasNextPage && page2.endCursor && count < MAX_PAGES; count++) {
568
773
  const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
569
774
  repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
570
775
  reviewThreads(first:100,after:$after) { nodes { isResolved } pageInfo { hasNextPage endCursor } }
571
- } } }`), { owner, repo, number: node.number, after: page.endCursor });
776
+ } } }`), { owner, repo, number: node.number, after: page2.endCursor });
572
777
  const next = data.repository?.pullRequest?.reviewThreads;
573
778
  if (!next)
574
779
  break;
575
780
  threads.push(...next.nodes);
576
- page = next.pageInfo;
781
+ page2 = next.pageInfo;
577
782
  }
578
783
  return {
579
784
  unresolved: threads.filter((thread) => !thread.isResolved).length,
580
- complete: !page?.hasNextPage
785
+ complete: !page2?.hasNextPage
581
786
  };
582
787
  }
583
788
  async contexts(host, node) {
@@ -598,7 +803,33 @@ class GitHubForge {
598
803
  }
599
804
  return { nodes: contexts, complete: !cursor?.hasNextPage };
600
805
  }
806
+ async reviewers(host, node) {
807
+ const nodes = [...node.reviewRequests.nodes];
808
+ let reviewerPage = node.reviewRequests.pageInfo;
809
+ for (let count = 1;reviewerPage?.hasNextPage && count < MAX_PAGES; count++) {
810
+ if (!reviewerPage.endCursor)
811
+ throw new ForgeError("request", "GitHub reviewer pagination cursor missing");
812
+ const [owner, repo] = node.baseRepository.nameWithOwner.split("/");
813
+ const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
814
+ repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
815
+ reviewRequests(first:100,after:$after) {
816
+ nodes { requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { name } } }
817
+ pageInfo { hasNextPage endCursor }
818
+ }
819
+ } }
820
+ }`), { owner, repo, number: node.number, after: reviewerPage.endCursor });
821
+ const next = data.repository?.pullRequest?.reviewRequests;
822
+ if (!next)
823
+ throw new ForgeError("request", "GitHub reviewers unavailable");
824
+ nodes.push(...next.nodes);
825
+ reviewerPage = next.pageInfo;
826
+ }
827
+ if (reviewerPage?.hasNextPage)
828
+ throw new ForgeError("request", "GitHub reviewers exceeded the pagination limit");
829
+ return { nodes, pageInfo: reviewerPage };
830
+ }
601
831
  async normalize(host, node) {
832
+ const reviewers = await this.reviewers(host, node);
602
833
  const [threads, contexts] = [await this.threads(host, node), await this.contexts(host, node)];
603
834
  return {
604
835
  forge: "github",
@@ -611,11 +842,14 @@ class GitHubForge {
611
842
  conflictsKnown: node.mergeable !== "UNKNOWN",
612
843
  approved: node.reviewDecision === "APPROVED",
613
844
  reviewDecision: node.reviewDecision ?? undefined,
614
- awaitingReviewers: node.reviewRequests.nodes.flatMap((request) => {
845
+ copilotReview: copilotReview({ ...node, reviewRequests: reviewers }),
846
+ awaitingReviewers: reviewers.nodes.flatMap((request) => {
615
847
  const reviewer = request.requestedReviewer;
848
+ if (reviewer?.__typename === "Bot")
849
+ return [];
616
850
  return reviewer?.login ? [reviewer.login] : reviewer?.name ? [reviewer.name] : [];
617
851
  }),
618
- reviewersComplete: !node.reviewRequests.pageInfo?.hasNextPage,
852
+ reviewersComplete: !reviewers.pageInfo?.hasNextPage,
619
853
  targetProject: node.baseRepository.nameWithOwner,
620
854
  targetBranch: node.baseRefName,
621
855
  headSha: node.headRefOid,
@@ -731,7 +965,7 @@ function footerSegments(snapshot) {
731
965
  const { lookup, error } = snapshot;
732
966
  const stale = error ? [{ text: "stale", tone: "warning", essential: true }] : [];
733
967
  if (!lookup || lookup.kind === "none" || lookup.requests.length === 0) {
734
- return error ? [{ text: "PR/MR status unavailable", tone: "warning" }] : [];
968
+ return snapshot.targeted && (error || lookup?.explicitTarget) ? [{ text: "PR/MR status unavailable", tone: "warning" }] : [];
735
969
  }
736
970
  if (lookup.failed?.length) {
737
971
  stale.unshift({
@@ -808,7 +1042,7 @@ function responsiveFooterSegments(snapshot, width) {
808
1042
  }
809
1043
  return fitted;
810
1044
  }
811
- var time = (value) => new Date(value).toLocaleTimeString();
1045
+ var time2 = (value) => new Date(value).toLocaleTimeString();
812
1046
  function describe(request, head) {
813
1047
  const traits = traitsOf(request);
814
1048
  const lines = [
@@ -856,9 +1090,9 @@ function detailsMessage(snapshot) {
856
1090
  lines.push("", ...describe(request, repository?.head ?? ""));
857
1091
  }
858
1092
  if (error)
859
- lines.push("", `Last refresh failed at ${time(error.at)}: ${error.message}`);
1093
+ lines.push("", `Last refresh failed at ${time2(error.at)}: ${error.message}`);
860
1094
  if (fetchedAt !== undefined)
861
- lines.push("", `Last fetched: ${time(fetchedAt)}`);
1095
+ lines.push("", `Last fetched: ${time2(fetchedAt)}`);
862
1096
  return lines.join(`
863
1097
  `);
864
1098
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-forgekeeper",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "OpenCode plugin that tracks each session's GitHub and GitLab issues and PRs/MRs: title prefixes, a PR/MR status footer, and review notifications",
5
5
  "author": {
6
6
  "name": "Pedro Pombeiro",