@bretwardjames/ghp-core 0.7.0 → 0.8.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.
package/dist/index.cjs CHANGED
@@ -199,6 +199,8 @@ __export(queries_exports, {
199
199
  PROJECT_FIELDS_QUERY: () => PROJECT_FIELDS_QUERY,
200
200
  PROJECT_ITEMS_QUERY: () => PROJECT_ITEMS_QUERY,
201
201
  PROJECT_VIEWS_QUERY: () => PROJECT_VIEWS_QUERY,
202
+ PR_AUTHORED_SEARCH_QUERY: () => PR_AUTHORED_SEARCH_QUERY,
203
+ PR_REVIEWS_SEARCH_QUERY: () => PR_REVIEWS_SEARCH_QUERY,
202
204
  RECENT_ISSUES_QUERY: () => RECENT_ISSUES_QUERY,
203
205
  REMOVE_BLOCKED_BY_MUTATION: () => REMOVE_BLOCKED_BY_MUTATION,
204
206
  REMOVE_LABELS_MUTATION: () => REMOVE_LABELS_MUTATION,
@@ -761,6 +763,39 @@ var ISSUE_TIMELINE_QUERY = `
761
763
  ... on Issue { number title }
762
764
  }
763
765
  }
766
+ ... on PullRequestReview {
767
+ author { login }
768
+ createdAt
769
+ state
770
+ }
771
+ ... on ReviewRequestedEvent {
772
+ actor { login }
773
+ createdAt
774
+ requestedReviewer {
775
+ ... on User { login }
776
+ }
777
+ }
778
+ }
779
+ }
780
+ }
781
+ }
782
+ }
783
+ }
784
+ `;
785
+ var PR_REVIEWS_SEARCH_QUERY = `
786
+ query($searchQuery: String!) {
787
+ search(query: $searchQuery, type: ISSUE, first: 50) {
788
+ nodes {
789
+ ... on PullRequest {
790
+ number
791
+ title
792
+ url
793
+ author { login }
794
+ reviews(first: 20) {
795
+ nodes {
796
+ author { login }
797
+ state
798
+ submittedAt
764
799
  }
765
800
  }
766
801
  }
@@ -768,6 +803,23 @@ var ISSUE_TIMELINE_QUERY = `
768
803
  }
769
804
  }
770
805
  `;
806
+ var PR_AUTHORED_SEARCH_QUERY = `
807
+ query($searchQuery: String!) {
808
+ search(query: $searchQuery, type: ISSUE, first: 50) {
809
+ nodes {
810
+ ... on PullRequest {
811
+ number
812
+ title
813
+ url
814
+ state
815
+ createdAt
816
+ mergedAt
817
+ mergedBy { login }
818
+ }
819
+ }
820
+ }
821
+ }
822
+ `;
771
823
  var ISSUE_RELATIONSHIPS_QUERY = `
772
824
  query($owner: String!, $name: String!, $number: Int!) {
773
825
  repository(owner: $owner, name: $name) {
@@ -1848,12 +1900,15 @@ var GitHubAPI = class {
1848
1900
  }
1849
1901
  /**
1850
1902
  * Get recent activity across all project items since a given time.
1851
- * Uses a two-pass approach for efficiency:
1903
+ * Multi-pass approach:
1852
1904
  * Pass 1: Fetch all project items, filter by updatedAt client-side
1853
1905
  * Pass 2: Fetch timeline events only for items that changed
1906
+ * Pass 3: Search for PRs reviewed by user (not captured in project items)
1907
+ * Pass 4: Search for PRs authored by user (created/merged)
1854
1908
  */
1855
1909
  async getRecentActivity(repo, since, options) {
1856
1910
  if (!this.graphqlWithAuth) throw new Error("Not authenticated");
1911
+ const filterUser = options?.user || (options?.mine ? this.username : null);
1857
1912
  const projects = await this.getProjects(repo);
1858
1913
  if (projects.length === 0) return [];
1859
1914
  const allItems = [];
@@ -1863,12 +1918,13 @@ var GitHubAPI = class {
1863
1918
  }
1864
1919
  const sinceMs = since.getTime();
1865
1920
  const sinceISO = since.toISOString();
1921
+ const sinceDate = sinceISO.split("T")[0];
1866
1922
  let recentItems = allItems.filter(
1867
1923
  (item) => item.updatedAt && new Date(item.updatedAt).getTime() >= sinceMs
1868
1924
  );
1869
- if (options?.mine && this.username) {
1925
+ if (filterUser) {
1870
1926
  recentItems = recentItems.filter(
1871
- (item) => item.assignees.includes(this.username)
1927
+ (item) => item.assignees.includes(filterUser)
1872
1928
  );
1873
1929
  }
1874
1930
  const seen = /* @__PURE__ */ new Set();
@@ -1912,6 +1968,32 @@ var GitHubAPI = class {
1912
1968
  }
1913
1969
  }
1914
1970
  }
1971
+ const activityNumbers = new Set(activities.map((a) => a.issue.number));
1972
+ const prSearchUser = filterUser || this.username;
1973
+ if (prSearchUser) {
1974
+ const reviewActivities = await this.fetchReviewedPRs(
1975
+ repo,
1976
+ prSearchUser,
1977
+ sinceDate,
1978
+ sinceMs,
1979
+ activityNumbers
1980
+ );
1981
+ for (const a of reviewActivities) {
1982
+ activityNumbers.add(a.issue.number);
1983
+ activities.push(a);
1984
+ }
1985
+ const authoredActivities = await this.fetchAuthoredPRs(
1986
+ repo,
1987
+ prSearchUser,
1988
+ sinceDate,
1989
+ sinceMs,
1990
+ activityNumbers
1991
+ );
1992
+ for (const a of authoredActivities) {
1993
+ activityNumbers.add(a.issue.number);
1994
+ activities.push(a);
1995
+ }
1996
+ }
1915
1997
  activities.sort((a, b) => {
1916
1998
  const aLatest = a.changes[a.changes.length - 1]?.timestamp || "";
1917
1999
  const bLatest = b.changes[b.changes.length - 1]?.timestamp || "";
@@ -1919,6 +2001,78 @@ var GitHubAPI = class {
1919
2001
  });
1920
2002
  return activities;
1921
2003
  }
2004
+ /**
2005
+ * Search for PRs the user reviewed that aren't already in the activity list.
2006
+ */
2007
+ async fetchReviewedPRs(repo, username, sinceDate, sinceMs, seen) {
2008
+ const searchQuery = `repo:${repo.owner}/${repo.name} reviewed-by:${username} updated:>=${sinceDate} is:pr`;
2009
+ const response = await this.graphqlWithRetry(
2010
+ PR_REVIEWS_SEARCH_QUERY,
2011
+ { searchQuery }
2012
+ );
2013
+ const activities = [];
2014
+ for (const pr of response.search.nodes) {
2015
+ if (seen.has(pr.number)) continue;
2016
+ const recentReviews = pr.reviews.nodes.filter(
2017
+ (r) => r.author?.login === username && new Date(r.submittedAt).getTime() >= sinceMs
2018
+ );
2019
+ if (recentReviews.length === 0) continue;
2020
+ const events = recentReviews.map((r) => {
2021
+ const stateLabel = r.state === "APPROVED" ? "Approved" : r.state === "CHANGES_REQUESTED" ? "Changes requested" : r.state === "COMMENTED" ? "Review comment" : r.state === "DISMISSED" ? "Review dismissed" : r.state;
2022
+ return {
2023
+ type: "review_submitted",
2024
+ actor: username,
2025
+ timestamp: r.submittedAt,
2026
+ details: stateLabel
2027
+ };
2028
+ });
2029
+ activities.push({
2030
+ issue: { number: pr.number, title: pr.title, url: pr.url },
2031
+ status: null,
2032
+ assignees: [],
2033
+ changes: events
2034
+ });
2035
+ }
2036
+ return activities;
2037
+ }
2038
+ /**
2039
+ * Search for PRs the user authored that aren't already in the activity list.
2040
+ * Captures PR creation and merges.
2041
+ */
2042
+ async fetchAuthoredPRs(repo, username, sinceDate, sinceMs, seen) {
2043
+ const searchQuery = `repo:${repo.owner}/${repo.name} author:${username} created:>=${sinceDate} is:pr`;
2044
+ const response = await this.graphqlWithRetry(
2045
+ PR_AUTHORED_SEARCH_QUERY,
2046
+ { searchQuery }
2047
+ );
2048
+ const activities = [];
2049
+ for (const pr of response.search.nodes) {
2050
+ if (seen.has(pr.number)) continue;
2051
+ const events = [];
2052
+ if (new Date(pr.createdAt).getTime() >= sinceMs) {
2053
+ events.push({
2054
+ type: "pr_created",
2055
+ actor: username,
2056
+ timestamp: pr.createdAt
2057
+ });
2058
+ }
2059
+ if (pr.mergedAt && new Date(pr.mergedAt).getTime() >= sinceMs) {
2060
+ events.push({
2061
+ type: "pr_merged",
2062
+ actor: pr.mergedBy?.login || username,
2063
+ timestamp: pr.mergedAt
2064
+ });
2065
+ }
2066
+ if (events.length === 0) continue;
2067
+ activities.push({
2068
+ issue: { number: pr.number, title: pr.title, url: pr.url },
2069
+ status: pr.state === "MERGED" ? "Merged" : pr.state === "OPEN" ? "Open" : "Closed",
2070
+ assignees: [username],
2071
+ changes: events
2072
+ });
2073
+ }
2074
+ return activities;
2075
+ }
1922
2076
  /**
1923
2077
  * Fetch and normalize timeline events for a single issue/PR
1924
2078
  */
@@ -1994,6 +2148,25 @@ var GitHubAPI = class {
1994
2148
  }
1995
2149
  break;
1996
2150
  }
2151
+ case "PullRequestReview": {
2152
+ const stateLabel = node.state === "APPROVED" ? "Approved" : node.state === "CHANGES_REQUESTED" ? "Changes requested" : node.state === "COMMENTED" ? "Review comment" : node.state === "DISMISSED" ? "Review dismissed" : node.state || "Reviewed";
2153
+ events.push({
2154
+ type: "review_submitted",
2155
+ actor,
2156
+ timestamp,
2157
+ details: stateLabel
2158
+ });
2159
+ break;
2160
+ }
2161
+ case "ReviewRequestedEvent": {
2162
+ events.push({
2163
+ type: "review_requested",
2164
+ actor,
2165
+ timestamp,
2166
+ details: node.requestedReviewer?.login
2167
+ });
2168
+ break;
2169
+ }
1997
2170
  }
1998
2171
  }
1999
2172
  return events;
@@ -4071,6 +4244,12 @@ function validateUrl(url) {
4071
4244
 
4072
4245
  // src/standup.ts
4073
4246
  function formatStandupText(activities, options) {
4247
+ if (options.timeline) {
4248
+ return formatTimeline(activities, options);
4249
+ }
4250
+ return formatGrouped(activities, options);
4251
+ }
4252
+ function formatGrouped(activities, options) {
4074
4253
  const { since } = options;
4075
4254
  const lines = [];
4076
4255
  const sinceStr = formatRelativeDate(since);
@@ -4091,6 +4270,35 @@ function formatStandupText(activities, options) {
4091
4270
  }
4092
4271
  return lines.join("\n").trimEnd();
4093
4272
  }
4273
+ function formatTimeline(activities, options) {
4274
+ const { since } = options;
4275
+ const lines = [];
4276
+ const allEvents = [];
4277
+ for (const activity of activities) {
4278
+ for (const event of activity.changes) {
4279
+ allEvents.push({ event, issue: activity.issue });
4280
+ }
4281
+ }
4282
+ allEvents.sort(
4283
+ (a, b) => new Date(b.event.timestamp).getTime() - new Date(a.event.timestamp).getTime()
4284
+ );
4285
+ const sinceStr = formatRelativeDate(since);
4286
+ const eventCount = allEvents.length;
4287
+ const issueCount = activities.length;
4288
+ lines.push(`Since ${sinceStr} \u2014 ${eventCount} event${eventCount !== 1 ? "s" : ""} across ${issueCount} issue${issueCount !== 1 ? "s" : ""}`);
4289
+ lines.push("");
4290
+ if (allEvents.length === 0) {
4291
+ lines.push("No activity found in this time window.");
4292
+ return lines.join("\n");
4293
+ }
4294
+ for (const { event, issue } of allEvents) {
4295
+ const time = formatShortTimestamp(event.timestamp);
4296
+ const desc = formatEventDescription(event);
4297
+ const issueRef = `#${issue.number} ${truncate(issue.title, 50)}`;
4298
+ lines.push(`${time} ${desc} (${issueRef})`);
4299
+ }
4300
+ return lines.join("\n").trimEnd();
4301
+ }
4094
4302
  function parseSince(input) {
4095
4303
  const isoDate = new Date(input);
4096
4304
  if (!isNaN(isoDate.getTime()) && input.includes("-")) {
@@ -4135,10 +4343,53 @@ function formatEventLine(event) {
4135
4343
  return `${arrow} Reopened by ${actor} (${timestamp})`;
4136
4344
  case "referenced":
4137
4345
  return `${arrow} ${event.details} linked by ${actor} (${timestamp})`;
4346
+ case "review_submitted":
4347
+ return `${arrow} ${event.details} by ${actor} (${timestamp})`;
4348
+ case "review_requested":
4349
+ return `${arrow} Review requested from ${event.details || "team"} by ${actor} (${timestamp})`;
4350
+ case "pr_created":
4351
+ return `${arrow} PR created by ${actor} (${timestamp})`;
4352
+ case "pr_merged":
4353
+ return `${arrow} PR merged by ${actor} (${timestamp})`;
4138
4354
  default:
4139
4355
  return `${arrow} ${event.type} by ${actor} (${timestamp})`;
4140
4356
  }
4141
4357
  }
4358
+ function formatEventDescription(event) {
4359
+ const actor = event.actor;
4360
+ switch (event.type) {
4361
+ case "comment":
4362
+ return `Comment by ${actor}${event.details ? ": " + event.details : ""}`;
4363
+ case "labeled":
4364
+ return `Labeled "${event.details}" by ${actor}`;
4365
+ case "unlabeled":
4366
+ return `Unlabeled "${event.details}" by ${actor}`;
4367
+ case "assigned":
4368
+ return `Assigned to ${event.details || actor}`;
4369
+ case "unassigned":
4370
+ return `Unassigned ${event.details || ""} by ${actor}`;
4371
+ case "closed":
4372
+ return `Closed by ${actor}`;
4373
+ case "reopened":
4374
+ return `Reopened by ${actor}`;
4375
+ case "referenced":
4376
+ return `${event.details} linked by ${actor}`;
4377
+ case "review_submitted":
4378
+ return `${event.details} by ${actor}`;
4379
+ case "review_requested":
4380
+ return `Review requested from ${event.details || "team"} by ${actor}`;
4381
+ case "pr_created":
4382
+ return `PR created by ${actor}`;
4383
+ case "pr_merged":
4384
+ return `PR merged by ${actor}`;
4385
+ default:
4386
+ return `${event.type} by ${actor}`;
4387
+ }
4388
+ }
4389
+ function truncate(str, maxLen) {
4390
+ if (str.length <= maxLen) return str;
4391
+ return str.slice(0, maxLen - 1) + "\u2026";
4392
+ }
4142
4393
  function formatRelativeDate(date) {
4143
4394
  const now = /* @__PURE__ */ new Date();
4144
4395
  const diffMs = now.getTime() - date.getTime();
package/dist/index.d.cts CHANGED
@@ -442,7 +442,7 @@ interface IssueActivity {
442
442
  * A single activity event on an issue
443
443
  */
444
444
  interface ActivityEvent {
445
- type: 'comment' | 'assigned' | 'unassigned' | 'labeled' | 'unlabeled' | 'closed' | 'reopened' | 'referenced';
445
+ type: 'comment' | 'assigned' | 'unassigned' | 'labeled' | 'unlabeled' | 'closed' | 'reopened' | 'referenced' | 'review_submitted' | 'review_requested' | 'pr_created' | 'pr_merged';
446
446
  actor: string;
447
447
  timestamp: string;
448
448
  details?: string;
@@ -683,13 +683,25 @@ declare class GitHubAPI {
683
683
  getIssueRelationships(repo: RepoInfo, issueNumber: number): Promise<IssueRelationships | null>;
684
684
  /**
685
685
  * Get recent activity across all project items since a given time.
686
- * Uses a two-pass approach for efficiency:
686
+ * Multi-pass approach:
687
687
  * Pass 1: Fetch all project items, filter by updatedAt client-side
688
688
  * Pass 2: Fetch timeline events only for items that changed
689
+ * Pass 3: Search for PRs reviewed by user (not captured in project items)
690
+ * Pass 4: Search for PRs authored by user (created/merged)
689
691
  */
690
692
  getRecentActivity(repo: RepoInfo, since: Date, options?: {
691
693
  mine?: boolean;
694
+ user?: string;
692
695
  }): Promise<IssueActivity[]>;
696
+ /**
697
+ * Search for PRs the user reviewed that aren't already in the activity list.
698
+ */
699
+ private fetchReviewedPRs;
700
+ /**
701
+ * Search for PRs the user authored that aren't already in the activity list.
702
+ * Captures PR creation and merges.
703
+ */
704
+ private fetchAuthoredPRs;
693
705
  /**
694
706
  * Fetch and normalize timeline events for a single issue/PR
695
707
  */
@@ -1255,7 +1267,15 @@ declare const ISSUE_WITH_PROJECT_ITEMS_QUERY = "\n query($owner: String!, $na
1255
1267
  * Query to get timeline events for an issue since a given time.
1256
1268
  * Used by the standup command to fetch specific activity details.
1257
1269
  */
1258
- declare const ISSUE_TIMELINE_QUERY = "\n query($owner: String!, $name: String!, $number: Int!, $since: DateTime!) {\n repository(owner: $owner, name: $name) {\n issueOrPullRequest(number: $number) {\n ... on Issue {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n }\n }\n }\n ... on PullRequest {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n }\n }\n }\n }\n }\n }\n";
1270
+ declare const ISSUE_TIMELINE_QUERY = "\n query($owner: String!, $name: String!, $number: Int!, $since: DateTime!) {\n repository(owner: $owner, name: $name) {\n issueOrPullRequest(number: $number) {\n ... on Issue {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n }\n }\n }\n ... on PullRequest {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n ... on PullRequestReview {\n author { login }\n createdAt\n state\n }\n ... on ReviewRequestedEvent {\n actor { login }\n createdAt\n requestedReviewer {\n ... on User { login }\n }\n }\n }\n }\n }\n }\n }\n }\n";
1271
+ /**
1272
+ * Search for PRs reviewed by a user in a given repo since a date.
1273
+ */
1274
+ declare const PR_REVIEWS_SEARCH_QUERY = "\n query($searchQuery: String!) {\n search(query: $searchQuery, type: ISSUE, first: 50) {\n nodes {\n ... on PullRequest {\n number\n title\n url\n author { login }\n reviews(first: 20) {\n nodes {\n author { login }\n state\n submittedAt\n }\n }\n }\n }\n }\n }\n";
1275
+ /**
1276
+ * Search for PRs authored by a user in a given repo since a date.
1277
+ */
1278
+ declare const PR_AUTHORED_SEARCH_QUERY = "\n query($searchQuery: String!) {\n search(query: $searchQuery, type: ISSUE, first: 50) {\n nodes {\n ... on PullRequest {\n number\n title\n url\n state\n createdAt\n mergedAt\n mergedBy { login }\n }\n }\n }\n }\n";
1259
1279
  /**
1260
1280
  * Query to get issue relationships (parent and sub-issues)
1261
1281
  */
@@ -1281,6 +1301,8 @@ declare const queries_LABEL_EXISTS_QUERY: typeof LABEL_EXISTS_QUERY;
1281
1301
  declare const queries_PROJECT_FIELDS_QUERY: typeof PROJECT_FIELDS_QUERY;
1282
1302
  declare const queries_PROJECT_ITEMS_QUERY: typeof PROJECT_ITEMS_QUERY;
1283
1303
  declare const queries_PROJECT_VIEWS_QUERY: typeof PROJECT_VIEWS_QUERY;
1304
+ declare const queries_PR_AUTHORED_SEARCH_QUERY: typeof PR_AUTHORED_SEARCH_QUERY;
1305
+ declare const queries_PR_REVIEWS_SEARCH_QUERY: typeof PR_REVIEWS_SEARCH_QUERY;
1284
1306
  declare const queries_RECENT_ISSUES_QUERY: typeof RECENT_ISSUES_QUERY;
1285
1307
  declare const queries_REMOVE_BLOCKED_BY_MUTATION: typeof REMOVE_BLOCKED_BY_MUTATION;
1286
1308
  declare const queries_REMOVE_LABELS_MUTATION: typeof REMOVE_LABELS_MUTATION;
@@ -1294,7 +1316,7 @@ declare const queries_UPDATE_ITEM_FIELD_MUTATION: typeof UPDATE_ITEM_FIELD_MUTAT
1294
1316
  declare const queries_UPDATE_ITEM_STATUS_MUTATION: typeof UPDATE_ITEM_STATUS_MUTATION;
1295
1317
  declare const queries_VIEWER_QUERY: typeof VIEWER_QUERY;
1296
1318
  declare namespace queries {
1297
- export { queries_ADD_BLOCKED_BY_MUTATION as ADD_BLOCKED_BY_MUTATION, queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_SUB_ISSUE_MUTATION as ADD_SUB_ISSUE_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_RELATIONSHIPS_QUERY as ISSUE_RELATIONSHIPS_QUERY, queries_ISSUE_TIMELINE_QUERY as ISSUE_TIMELINE_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_ISSUE_WITH_PROJECT_ITEMS_QUERY as ISSUE_WITH_PROJECT_ITEMS_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_BLOCKED_BY_MUTATION as REMOVE_BLOCKED_BY_MUTATION, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REMOVE_SUB_ISSUE_MUTATION as REMOVE_SUB_ISSUE_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_MUTATION as UPDATE_ISSUE_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
1319
+ export { queries_ADD_BLOCKED_BY_MUTATION as ADD_BLOCKED_BY_MUTATION, queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_SUB_ISSUE_MUTATION as ADD_SUB_ISSUE_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_RELATIONSHIPS_QUERY as ISSUE_RELATIONSHIPS_QUERY, queries_ISSUE_TIMELINE_QUERY as ISSUE_TIMELINE_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_ISSUE_WITH_PROJECT_ITEMS_QUERY as ISSUE_WITH_PROJECT_ITEMS_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_PR_AUTHORED_SEARCH_QUERY as PR_AUTHORED_SEARCH_QUERY, queries_PR_REVIEWS_SEARCH_QUERY as PR_REVIEWS_SEARCH_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_BLOCKED_BY_MUTATION as REMOVE_BLOCKED_BY_MUTATION, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REMOVE_SUB_ISSUE_MUTATION as REMOVE_SUB_ISSUE_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_MUTATION as UPDATE_ISSUE_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
1298
1320
  }
1299
1321
 
1300
1322
  /**
@@ -2262,6 +2284,8 @@ declare function validateUrl(url: string): string;
2262
2284
  interface FormatStandupOptions {
2263
2285
  since: Date;
2264
2286
  colorize?: boolean;
2287
+ /** If true, show a flat chronological timeline instead of grouping by issue */
2288
+ timeline?: boolean;
2265
2289
  }
2266
2290
  /**
2267
2291
  * Format a standup summary as human-readable text.
package/dist/index.d.ts CHANGED
@@ -442,7 +442,7 @@ interface IssueActivity {
442
442
  * A single activity event on an issue
443
443
  */
444
444
  interface ActivityEvent {
445
- type: 'comment' | 'assigned' | 'unassigned' | 'labeled' | 'unlabeled' | 'closed' | 'reopened' | 'referenced';
445
+ type: 'comment' | 'assigned' | 'unassigned' | 'labeled' | 'unlabeled' | 'closed' | 'reopened' | 'referenced' | 'review_submitted' | 'review_requested' | 'pr_created' | 'pr_merged';
446
446
  actor: string;
447
447
  timestamp: string;
448
448
  details?: string;
@@ -683,13 +683,25 @@ declare class GitHubAPI {
683
683
  getIssueRelationships(repo: RepoInfo, issueNumber: number): Promise<IssueRelationships | null>;
684
684
  /**
685
685
  * Get recent activity across all project items since a given time.
686
- * Uses a two-pass approach for efficiency:
686
+ * Multi-pass approach:
687
687
  * Pass 1: Fetch all project items, filter by updatedAt client-side
688
688
  * Pass 2: Fetch timeline events only for items that changed
689
+ * Pass 3: Search for PRs reviewed by user (not captured in project items)
690
+ * Pass 4: Search for PRs authored by user (created/merged)
689
691
  */
690
692
  getRecentActivity(repo: RepoInfo, since: Date, options?: {
691
693
  mine?: boolean;
694
+ user?: string;
692
695
  }): Promise<IssueActivity[]>;
696
+ /**
697
+ * Search for PRs the user reviewed that aren't already in the activity list.
698
+ */
699
+ private fetchReviewedPRs;
700
+ /**
701
+ * Search for PRs the user authored that aren't already in the activity list.
702
+ * Captures PR creation and merges.
703
+ */
704
+ private fetchAuthoredPRs;
693
705
  /**
694
706
  * Fetch and normalize timeline events for a single issue/PR
695
707
  */
@@ -1255,7 +1267,15 @@ declare const ISSUE_WITH_PROJECT_ITEMS_QUERY = "\n query($owner: String!, $na
1255
1267
  * Query to get timeline events for an issue since a given time.
1256
1268
  * Used by the standup command to fetch specific activity details.
1257
1269
  */
1258
- declare const ISSUE_TIMELINE_QUERY = "\n query($owner: String!, $name: String!, $number: Int!, $since: DateTime!) {\n repository(owner: $owner, name: $name) {\n issueOrPullRequest(number: $number) {\n ... on Issue {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n }\n }\n }\n ... on PullRequest {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n }\n }\n }\n }\n }\n }\n";
1270
+ declare const ISSUE_TIMELINE_QUERY = "\n query($owner: String!, $name: String!, $number: Int!, $since: DateTime!) {\n repository(owner: $owner, name: $name) {\n issueOrPullRequest(number: $number) {\n ... on Issue {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n }\n }\n }\n ... on PullRequest {\n timelineItems(first: 100, since: $since) {\n nodes {\n __typename\n ... on IssueComment {\n author { login }\n createdAt\n body\n }\n ... on LabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on UnlabeledEvent {\n actor { login }\n createdAt\n label { name }\n }\n ... on AssignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on UnassignedEvent {\n actor { login }\n createdAt\n assignee { ... on User { login } }\n }\n ... on ClosedEvent {\n actor { login }\n createdAt\n }\n ... on ReopenedEvent {\n actor { login }\n createdAt\n }\n ... on CrossReferencedEvent {\n actor { login }\n createdAt\n source {\n __typename\n ... on PullRequest { number title url }\n ... on Issue { number title }\n }\n }\n ... on PullRequestReview {\n author { login }\n createdAt\n state\n }\n ... on ReviewRequestedEvent {\n actor { login }\n createdAt\n requestedReviewer {\n ... on User { login }\n }\n }\n }\n }\n }\n }\n }\n }\n";
1271
+ /**
1272
+ * Search for PRs reviewed by a user in a given repo since a date.
1273
+ */
1274
+ declare const PR_REVIEWS_SEARCH_QUERY = "\n query($searchQuery: String!) {\n search(query: $searchQuery, type: ISSUE, first: 50) {\n nodes {\n ... on PullRequest {\n number\n title\n url\n author { login }\n reviews(first: 20) {\n nodes {\n author { login }\n state\n submittedAt\n }\n }\n }\n }\n }\n }\n";
1275
+ /**
1276
+ * Search for PRs authored by a user in a given repo since a date.
1277
+ */
1278
+ declare const PR_AUTHORED_SEARCH_QUERY = "\n query($searchQuery: String!) {\n search(query: $searchQuery, type: ISSUE, first: 50) {\n nodes {\n ... on PullRequest {\n number\n title\n url\n state\n createdAt\n mergedAt\n mergedBy { login }\n }\n }\n }\n }\n";
1259
1279
  /**
1260
1280
  * Query to get issue relationships (parent and sub-issues)
1261
1281
  */
@@ -1281,6 +1301,8 @@ declare const queries_LABEL_EXISTS_QUERY: typeof LABEL_EXISTS_QUERY;
1281
1301
  declare const queries_PROJECT_FIELDS_QUERY: typeof PROJECT_FIELDS_QUERY;
1282
1302
  declare const queries_PROJECT_ITEMS_QUERY: typeof PROJECT_ITEMS_QUERY;
1283
1303
  declare const queries_PROJECT_VIEWS_QUERY: typeof PROJECT_VIEWS_QUERY;
1304
+ declare const queries_PR_AUTHORED_SEARCH_QUERY: typeof PR_AUTHORED_SEARCH_QUERY;
1305
+ declare const queries_PR_REVIEWS_SEARCH_QUERY: typeof PR_REVIEWS_SEARCH_QUERY;
1284
1306
  declare const queries_RECENT_ISSUES_QUERY: typeof RECENT_ISSUES_QUERY;
1285
1307
  declare const queries_REMOVE_BLOCKED_BY_MUTATION: typeof REMOVE_BLOCKED_BY_MUTATION;
1286
1308
  declare const queries_REMOVE_LABELS_MUTATION: typeof REMOVE_LABELS_MUTATION;
@@ -1294,7 +1316,7 @@ declare const queries_UPDATE_ITEM_FIELD_MUTATION: typeof UPDATE_ITEM_FIELD_MUTAT
1294
1316
  declare const queries_UPDATE_ITEM_STATUS_MUTATION: typeof UPDATE_ITEM_STATUS_MUTATION;
1295
1317
  declare const queries_VIEWER_QUERY: typeof VIEWER_QUERY;
1296
1318
  declare namespace queries {
1297
- export { queries_ADD_BLOCKED_BY_MUTATION as ADD_BLOCKED_BY_MUTATION, queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_SUB_ISSUE_MUTATION as ADD_SUB_ISSUE_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_RELATIONSHIPS_QUERY as ISSUE_RELATIONSHIPS_QUERY, queries_ISSUE_TIMELINE_QUERY as ISSUE_TIMELINE_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_ISSUE_WITH_PROJECT_ITEMS_QUERY as ISSUE_WITH_PROJECT_ITEMS_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_BLOCKED_BY_MUTATION as REMOVE_BLOCKED_BY_MUTATION, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REMOVE_SUB_ISSUE_MUTATION as REMOVE_SUB_ISSUE_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_MUTATION as UPDATE_ISSUE_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
1319
+ export { queries_ADD_BLOCKED_BY_MUTATION as ADD_BLOCKED_BY_MUTATION, queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_SUB_ISSUE_MUTATION as ADD_SUB_ISSUE_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_RELATIONSHIPS_QUERY as ISSUE_RELATIONSHIPS_QUERY, queries_ISSUE_TIMELINE_QUERY as ISSUE_TIMELINE_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_ISSUE_WITH_PROJECT_ITEMS_QUERY as ISSUE_WITH_PROJECT_ITEMS_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_PR_AUTHORED_SEARCH_QUERY as PR_AUTHORED_SEARCH_QUERY, queries_PR_REVIEWS_SEARCH_QUERY as PR_REVIEWS_SEARCH_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_BLOCKED_BY_MUTATION as REMOVE_BLOCKED_BY_MUTATION, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REMOVE_SUB_ISSUE_MUTATION as REMOVE_SUB_ISSUE_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_MUTATION as UPDATE_ISSUE_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
1298
1320
  }
1299
1321
 
1300
1322
  /**
@@ -2262,6 +2284,8 @@ declare function validateUrl(url: string): string;
2262
2284
  interface FormatStandupOptions {
2263
2285
  since: Date;
2264
2286
  colorize?: boolean;
2287
+ /** If true, show a flat chronological timeline instead of grouping by issue */
2288
+ timeline?: boolean;
2265
2289
  }
2266
2290
  /**
2267
2291
  * Format a standup summary as human-readable text.
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ __export(queries_exports, {
30
30
  PROJECT_FIELDS_QUERY: () => PROJECT_FIELDS_QUERY,
31
31
  PROJECT_ITEMS_QUERY: () => PROJECT_ITEMS_QUERY,
32
32
  PROJECT_VIEWS_QUERY: () => PROJECT_VIEWS_QUERY,
33
+ PR_AUTHORED_SEARCH_QUERY: () => PR_AUTHORED_SEARCH_QUERY,
34
+ PR_REVIEWS_SEARCH_QUERY: () => PR_REVIEWS_SEARCH_QUERY,
33
35
  RECENT_ISSUES_QUERY: () => RECENT_ISSUES_QUERY,
34
36
  REMOVE_BLOCKED_BY_MUTATION: () => REMOVE_BLOCKED_BY_MUTATION,
35
37
  REMOVE_LABELS_MUTATION: () => REMOVE_LABELS_MUTATION,
@@ -592,6 +594,39 @@ var ISSUE_TIMELINE_QUERY = `
592
594
  ... on Issue { number title }
593
595
  }
594
596
  }
597
+ ... on PullRequestReview {
598
+ author { login }
599
+ createdAt
600
+ state
601
+ }
602
+ ... on ReviewRequestedEvent {
603
+ actor { login }
604
+ createdAt
605
+ requestedReviewer {
606
+ ... on User { login }
607
+ }
608
+ }
609
+ }
610
+ }
611
+ }
612
+ }
613
+ }
614
+ }
615
+ `;
616
+ var PR_REVIEWS_SEARCH_QUERY = `
617
+ query($searchQuery: String!) {
618
+ search(query: $searchQuery, type: ISSUE, first: 50) {
619
+ nodes {
620
+ ... on PullRequest {
621
+ number
622
+ title
623
+ url
624
+ author { login }
625
+ reviews(first: 20) {
626
+ nodes {
627
+ author { login }
628
+ state
629
+ submittedAt
595
630
  }
596
631
  }
597
632
  }
@@ -599,6 +634,23 @@ var ISSUE_TIMELINE_QUERY = `
599
634
  }
600
635
  }
601
636
  `;
637
+ var PR_AUTHORED_SEARCH_QUERY = `
638
+ query($searchQuery: String!) {
639
+ search(query: $searchQuery, type: ISSUE, first: 50) {
640
+ nodes {
641
+ ... on PullRequest {
642
+ number
643
+ title
644
+ url
645
+ state
646
+ createdAt
647
+ mergedAt
648
+ mergedBy { login }
649
+ }
650
+ }
651
+ }
652
+ }
653
+ `;
602
654
  var ISSUE_RELATIONSHIPS_QUERY = `
603
655
  query($owner: String!, $name: String!, $number: Int!) {
604
656
  repository(owner: $owner, name: $name) {
@@ -1679,12 +1731,15 @@ var GitHubAPI = class {
1679
1731
  }
1680
1732
  /**
1681
1733
  * Get recent activity across all project items since a given time.
1682
- * Uses a two-pass approach for efficiency:
1734
+ * Multi-pass approach:
1683
1735
  * Pass 1: Fetch all project items, filter by updatedAt client-side
1684
1736
  * Pass 2: Fetch timeline events only for items that changed
1737
+ * Pass 3: Search for PRs reviewed by user (not captured in project items)
1738
+ * Pass 4: Search for PRs authored by user (created/merged)
1685
1739
  */
1686
1740
  async getRecentActivity(repo, since, options) {
1687
1741
  if (!this.graphqlWithAuth) throw new Error("Not authenticated");
1742
+ const filterUser = options?.user || (options?.mine ? this.username : null);
1688
1743
  const projects = await this.getProjects(repo);
1689
1744
  if (projects.length === 0) return [];
1690
1745
  const allItems = [];
@@ -1694,12 +1749,13 @@ var GitHubAPI = class {
1694
1749
  }
1695
1750
  const sinceMs = since.getTime();
1696
1751
  const sinceISO = since.toISOString();
1752
+ const sinceDate = sinceISO.split("T")[0];
1697
1753
  let recentItems = allItems.filter(
1698
1754
  (item) => item.updatedAt && new Date(item.updatedAt).getTime() >= sinceMs
1699
1755
  );
1700
- if (options?.mine && this.username) {
1756
+ if (filterUser) {
1701
1757
  recentItems = recentItems.filter(
1702
- (item) => item.assignees.includes(this.username)
1758
+ (item) => item.assignees.includes(filterUser)
1703
1759
  );
1704
1760
  }
1705
1761
  const seen = /* @__PURE__ */ new Set();
@@ -1743,6 +1799,32 @@ var GitHubAPI = class {
1743
1799
  }
1744
1800
  }
1745
1801
  }
1802
+ const activityNumbers = new Set(activities.map((a) => a.issue.number));
1803
+ const prSearchUser = filterUser || this.username;
1804
+ if (prSearchUser) {
1805
+ const reviewActivities = await this.fetchReviewedPRs(
1806
+ repo,
1807
+ prSearchUser,
1808
+ sinceDate,
1809
+ sinceMs,
1810
+ activityNumbers
1811
+ );
1812
+ for (const a of reviewActivities) {
1813
+ activityNumbers.add(a.issue.number);
1814
+ activities.push(a);
1815
+ }
1816
+ const authoredActivities = await this.fetchAuthoredPRs(
1817
+ repo,
1818
+ prSearchUser,
1819
+ sinceDate,
1820
+ sinceMs,
1821
+ activityNumbers
1822
+ );
1823
+ for (const a of authoredActivities) {
1824
+ activityNumbers.add(a.issue.number);
1825
+ activities.push(a);
1826
+ }
1827
+ }
1746
1828
  activities.sort((a, b) => {
1747
1829
  const aLatest = a.changes[a.changes.length - 1]?.timestamp || "";
1748
1830
  const bLatest = b.changes[b.changes.length - 1]?.timestamp || "";
@@ -1750,6 +1832,78 @@ var GitHubAPI = class {
1750
1832
  });
1751
1833
  return activities;
1752
1834
  }
1835
+ /**
1836
+ * Search for PRs the user reviewed that aren't already in the activity list.
1837
+ */
1838
+ async fetchReviewedPRs(repo, username, sinceDate, sinceMs, seen) {
1839
+ const searchQuery = `repo:${repo.owner}/${repo.name} reviewed-by:${username} updated:>=${sinceDate} is:pr`;
1840
+ const response = await this.graphqlWithRetry(
1841
+ PR_REVIEWS_SEARCH_QUERY,
1842
+ { searchQuery }
1843
+ );
1844
+ const activities = [];
1845
+ for (const pr of response.search.nodes) {
1846
+ if (seen.has(pr.number)) continue;
1847
+ const recentReviews = pr.reviews.nodes.filter(
1848
+ (r) => r.author?.login === username && new Date(r.submittedAt).getTime() >= sinceMs
1849
+ );
1850
+ if (recentReviews.length === 0) continue;
1851
+ const events = recentReviews.map((r) => {
1852
+ const stateLabel = r.state === "APPROVED" ? "Approved" : r.state === "CHANGES_REQUESTED" ? "Changes requested" : r.state === "COMMENTED" ? "Review comment" : r.state === "DISMISSED" ? "Review dismissed" : r.state;
1853
+ return {
1854
+ type: "review_submitted",
1855
+ actor: username,
1856
+ timestamp: r.submittedAt,
1857
+ details: stateLabel
1858
+ };
1859
+ });
1860
+ activities.push({
1861
+ issue: { number: pr.number, title: pr.title, url: pr.url },
1862
+ status: null,
1863
+ assignees: [],
1864
+ changes: events
1865
+ });
1866
+ }
1867
+ return activities;
1868
+ }
1869
+ /**
1870
+ * Search for PRs the user authored that aren't already in the activity list.
1871
+ * Captures PR creation and merges.
1872
+ */
1873
+ async fetchAuthoredPRs(repo, username, sinceDate, sinceMs, seen) {
1874
+ const searchQuery = `repo:${repo.owner}/${repo.name} author:${username} created:>=${sinceDate} is:pr`;
1875
+ const response = await this.graphqlWithRetry(
1876
+ PR_AUTHORED_SEARCH_QUERY,
1877
+ { searchQuery }
1878
+ );
1879
+ const activities = [];
1880
+ for (const pr of response.search.nodes) {
1881
+ if (seen.has(pr.number)) continue;
1882
+ const events = [];
1883
+ if (new Date(pr.createdAt).getTime() >= sinceMs) {
1884
+ events.push({
1885
+ type: "pr_created",
1886
+ actor: username,
1887
+ timestamp: pr.createdAt
1888
+ });
1889
+ }
1890
+ if (pr.mergedAt && new Date(pr.mergedAt).getTime() >= sinceMs) {
1891
+ events.push({
1892
+ type: "pr_merged",
1893
+ actor: pr.mergedBy?.login || username,
1894
+ timestamp: pr.mergedAt
1895
+ });
1896
+ }
1897
+ if (events.length === 0) continue;
1898
+ activities.push({
1899
+ issue: { number: pr.number, title: pr.title, url: pr.url },
1900
+ status: pr.state === "MERGED" ? "Merged" : pr.state === "OPEN" ? "Open" : "Closed",
1901
+ assignees: [username],
1902
+ changes: events
1903
+ });
1904
+ }
1905
+ return activities;
1906
+ }
1753
1907
  /**
1754
1908
  * Fetch and normalize timeline events for a single issue/PR
1755
1909
  */
@@ -1825,6 +1979,25 @@ var GitHubAPI = class {
1825
1979
  }
1826
1980
  break;
1827
1981
  }
1982
+ case "PullRequestReview": {
1983
+ const stateLabel = node.state === "APPROVED" ? "Approved" : node.state === "CHANGES_REQUESTED" ? "Changes requested" : node.state === "COMMENTED" ? "Review comment" : node.state === "DISMISSED" ? "Review dismissed" : node.state || "Reviewed";
1984
+ events.push({
1985
+ type: "review_submitted",
1986
+ actor,
1987
+ timestamp,
1988
+ details: stateLabel
1989
+ });
1990
+ break;
1991
+ }
1992
+ case "ReviewRequestedEvent": {
1993
+ events.push({
1994
+ type: "review_requested",
1995
+ actor,
1996
+ timestamp,
1997
+ details: node.requestedReviewer?.login
1998
+ });
1999
+ break;
2000
+ }
1828
2001
  }
1829
2002
  }
1830
2003
  return events;
@@ -3902,6 +4075,12 @@ function validateUrl(url) {
3902
4075
 
3903
4076
  // src/standup.ts
3904
4077
  function formatStandupText(activities, options) {
4078
+ if (options.timeline) {
4079
+ return formatTimeline(activities, options);
4080
+ }
4081
+ return formatGrouped(activities, options);
4082
+ }
4083
+ function formatGrouped(activities, options) {
3905
4084
  const { since } = options;
3906
4085
  const lines = [];
3907
4086
  const sinceStr = formatRelativeDate(since);
@@ -3922,6 +4101,35 @@ function formatStandupText(activities, options) {
3922
4101
  }
3923
4102
  return lines.join("\n").trimEnd();
3924
4103
  }
4104
+ function formatTimeline(activities, options) {
4105
+ const { since } = options;
4106
+ const lines = [];
4107
+ const allEvents = [];
4108
+ for (const activity of activities) {
4109
+ for (const event of activity.changes) {
4110
+ allEvents.push({ event, issue: activity.issue });
4111
+ }
4112
+ }
4113
+ allEvents.sort(
4114
+ (a, b) => new Date(b.event.timestamp).getTime() - new Date(a.event.timestamp).getTime()
4115
+ );
4116
+ const sinceStr = formatRelativeDate(since);
4117
+ const eventCount = allEvents.length;
4118
+ const issueCount = activities.length;
4119
+ lines.push(`Since ${sinceStr} \u2014 ${eventCount} event${eventCount !== 1 ? "s" : ""} across ${issueCount} issue${issueCount !== 1 ? "s" : ""}`);
4120
+ lines.push("");
4121
+ if (allEvents.length === 0) {
4122
+ lines.push("No activity found in this time window.");
4123
+ return lines.join("\n");
4124
+ }
4125
+ for (const { event, issue } of allEvents) {
4126
+ const time = formatShortTimestamp(event.timestamp);
4127
+ const desc = formatEventDescription(event);
4128
+ const issueRef = `#${issue.number} ${truncate(issue.title, 50)}`;
4129
+ lines.push(`${time} ${desc} (${issueRef})`);
4130
+ }
4131
+ return lines.join("\n").trimEnd();
4132
+ }
3925
4133
  function parseSince(input) {
3926
4134
  const isoDate = new Date(input);
3927
4135
  if (!isNaN(isoDate.getTime()) && input.includes("-")) {
@@ -3966,10 +4174,53 @@ function formatEventLine(event) {
3966
4174
  return `${arrow} Reopened by ${actor} (${timestamp})`;
3967
4175
  case "referenced":
3968
4176
  return `${arrow} ${event.details} linked by ${actor} (${timestamp})`;
4177
+ case "review_submitted":
4178
+ return `${arrow} ${event.details} by ${actor} (${timestamp})`;
4179
+ case "review_requested":
4180
+ return `${arrow} Review requested from ${event.details || "team"} by ${actor} (${timestamp})`;
4181
+ case "pr_created":
4182
+ return `${arrow} PR created by ${actor} (${timestamp})`;
4183
+ case "pr_merged":
4184
+ return `${arrow} PR merged by ${actor} (${timestamp})`;
3969
4185
  default:
3970
4186
  return `${arrow} ${event.type} by ${actor} (${timestamp})`;
3971
4187
  }
3972
4188
  }
4189
+ function formatEventDescription(event) {
4190
+ const actor = event.actor;
4191
+ switch (event.type) {
4192
+ case "comment":
4193
+ return `Comment by ${actor}${event.details ? ": " + event.details : ""}`;
4194
+ case "labeled":
4195
+ return `Labeled "${event.details}" by ${actor}`;
4196
+ case "unlabeled":
4197
+ return `Unlabeled "${event.details}" by ${actor}`;
4198
+ case "assigned":
4199
+ return `Assigned to ${event.details || actor}`;
4200
+ case "unassigned":
4201
+ return `Unassigned ${event.details || ""} by ${actor}`;
4202
+ case "closed":
4203
+ return `Closed by ${actor}`;
4204
+ case "reopened":
4205
+ return `Reopened by ${actor}`;
4206
+ case "referenced":
4207
+ return `${event.details} linked by ${actor}`;
4208
+ case "review_submitted":
4209
+ return `${event.details} by ${actor}`;
4210
+ case "review_requested":
4211
+ return `Review requested from ${event.details || "team"} by ${actor}`;
4212
+ case "pr_created":
4213
+ return `PR created by ${actor}`;
4214
+ case "pr_merged":
4215
+ return `PR merged by ${actor}`;
4216
+ default:
4217
+ return `${event.type} by ${actor}`;
4218
+ }
4219
+ }
4220
+ function truncate(str, maxLen) {
4221
+ if (str.length <= maxLen) return str;
4222
+ return str.slice(0, maxLen - 1) + "\u2026";
4223
+ }
3973
4224
  function formatRelativeDate(date) {
3974
4225
  const now = /* @__PURE__ */ new Date();
3975
4226
  const diffMs = now.getTime() - date.getTime();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bretwardjames/ghp-core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Shared core library for GitHub Projects tools",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",