@oss-autopilot/core 0.41.0 → 0.42.1

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 (63) hide show
  1. package/dist/cli.bundle.cjs +1552 -1318
  2. package/dist/cli.js +593 -69
  3. package/dist/commands/check-integration.d.ts +3 -3
  4. package/dist/commands/check-integration.js +10 -43
  5. package/dist/commands/comments.d.ts +6 -9
  6. package/dist/commands/comments.js +102 -252
  7. package/dist/commands/config.d.ts +8 -2
  8. package/dist/commands/config.js +6 -28
  9. package/dist/commands/daily.d.ts +28 -4
  10. package/dist/commands/daily.js +33 -45
  11. package/dist/commands/dashboard-data.js +7 -6
  12. package/dist/commands/dashboard-server.d.ts +14 -0
  13. package/dist/commands/dashboard-server.js +362 -0
  14. package/dist/commands/dashboard.d.ts +5 -0
  15. package/dist/commands/dashboard.js +51 -1
  16. package/dist/commands/dismiss.d.ts +13 -5
  17. package/dist/commands/dismiss.js +4 -24
  18. package/dist/commands/index.d.ts +33 -0
  19. package/dist/commands/index.js +22 -0
  20. package/dist/commands/init.d.ts +5 -4
  21. package/dist/commands/init.js +4 -14
  22. package/dist/commands/local-repos.d.ts +4 -5
  23. package/dist/commands/local-repos.js +6 -33
  24. package/dist/commands/parse-list.d.ts +3 -4
  25. package/dist/commands/parse-list.js +8 -39
  26. package/dist/commands/read.d.ts +11 -5
  27. package/dist/commands/read.js +4 -18
  28. package/dist/commands/search.d.ts +3 -3
  29. package/dist/commands/search.js +39 -65
  30. package/dist/commands/setup.d.ts +34 -5
  31. package/dist/commands/setup.js +75 -166
  32. package/dist/commands/shelve.d.ts +13 -5
  33. package/dist/commands/shelve.js +4 -24
  34. package/dist/commands/snooze.d.ts +15 -9
  35. package/dist/commands/snooze.js +16 -59
  36. package/dist/commands/startup.d.ts +11 -6
  37. package/dist/commands/startup.js +44 -82
  38. package/dist/commands/status.d.ts +3 -3
  39. package/dist/commands/status.js +10 -29
  40. package/dist/commands/track.d.ts +10 -9
  41. package/dist/commands/track.js +17 -39
  42. package/dist/commands/validation.d.ts +2 -2
  43. package/dist/commands/validation.js +7 -15
  44. package/dist/commands/vet.d.ts +3 -3
  45. package/dist/commands/vet.js +16 -26
  46. package/dist/core/errors.d.ts +9 -0
  47. package/dist/core/errors.js +17 -0
  48. package/dist/core/github-stats.d.ts +14 -21
  49. package/dist/core/github-stats.js +84 -138
  50. package/dist/core/http-cache.d.ts +6 -0
  51. package/dist/core/http-cache.js +16 -4
  52. package/dist/core/index.d.ts +2 -1
  53. package/dist/core/index.js +2 -1
  54. package/dist/core/issue-conversation.js +4 -4
  55. package/dist/core/issue-discovery.js +14 -14
  56. package/dist/core/issue-vetting.js +17 -17
  57. package/dist/core/pr-monitor.d.ts +6 -20
  58. package/dist/core/pr-monitor.js +11 -52
  59. package/dist/core/state.js +4 -5
  60. package/dist/core/utils.d.ts +11 -0
  61. package/dist/core/utils.js +21 -0
  62. package/dist/formatters/json.d.ts +58 -0
  63. package/package.json +5 -1
@@ -2,19 +2,51 @@
2
2
  * GitHub Stats - Fetching merged/closed PR counts and repository star counts.
3
3
  * Extracted from PRMonitor to isolate statistics-gathering API calls (#263).
4
4
  */
5
- import { extractOwnerRepo, parseGitHubUrl } from './utils.js';
6
- import { ValidationError } from './errors.js';
5
+ import { extractOwnerRepo, parseGitHubUrl, isOwnRepo } from './utils.js';
7
6
  import { debug, warn } from './logger.js';
7
+ import { getHttpCache } from './http-cache.js';
8
8
  const MODULE = 'github-stats';
9
+ /** TTL for cached PR count results (1 hour). */
10
+ export const PR_COUNTS_CACHE_TTL_MS = 60 * 60 * 1000;
11
+ /** Type guard for deserialized cache data — prevents crashes on corrupt/stale cache. */
12
+ function isCachedPRCounts(v) {
13
+ if (typeof v !== 'object' || v === null)
14
+ return false;
15
+ const obj = v;
16
+ return (Array.isArray(obj.reposEntries) &&
17
+ typeof obj.monthlyCounts === 'object' &&
18
+ obj.monthlyCounts !== null &&
19
+ typeof obj.monthlyOpenedCounts === 'object' &&
20
+ obj.monthlyOpenedCounts !== null &&
21
+ typeof obj.dailyActivityCounts === 'object' &&
22
+ obj.dailyActivityCounts !== null);
23
+ }
9
24
  /**
10
- * Fetch merged PR counts and latest merge dates per repository for the configured user.
11
- * Also builds a monthly histogram of all merges for the contribution timeline.
25
+ * Shared paginated search for user PR counts with histogram tracking.
26
+ *
27
+ * Handles: pagination, owner extraction, skip-own-repos, monthly/daily histograms.
28
+ * The `accumulateRepo` callback handles per-repo data and returns the primary date
29
+ * string (e.g. mergedAt or closedAt) used for monthly counts and daily activity.
30
+ * Return an empty string to skip histogram tracking for that item.
12
31
  */
13
- export async function fetchUserMergedPRCounts(octokit, githubUsername) {
32
+ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumulateRepo) {
14
33
  if (!githubUsername) {
15
34
  return { repos: new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
16
35
  }
17
- debug(MODULE, `Fetching merged PR counts for @${githubUsername}...`);
36
+ // Check for a fresh cached result (avoids 10-20 paginated API calls)
37
+ const cache = getHttpCache();
38
+ const cacheKey = `pr-counts:${label}:${githubUsername}`;
39
+ const cached = cache.getIfFresh(cacheKey, PR_COUNTS_CACHE_TTL_MS);
40
+ if (cached && isCachedPRCounts(cached)) {
41
+ debug(MODULE, `Using cached ${label} PR counts for @${githubUsername}`);
42
+ return {
43
+ repos: new Map(cached.reposEntries),
44
+ monthlyCounts: cached.monthlyCounts,
45
+ monthlyOpenedCounts: cached.monthlyOpenedCounts,
46
+ dailyActivityCounts: cached.dailyActivityCounts,
47
+ };
48
+ }
49
+ debug(MODULE, `Fetching ${label} PR counts for @${githubUsername}...`);
18
50
  const repos = new Map();
19
51
  const monthlyCounts = {};
20
52
  const monthlyOpenedCounts = {};
@@ -23,7 +55,7 @@ export async function fetchUserMergedPRCounts(octokit, githubUsername) {
23
55
  let fetched = 0;
24
56
  while (true) {
25
57
  const { data } = await octokit.search.issuesAndPullRequests({
26
- q: `is:pr is:merged author:${githubUsername}`,
58
+ q: `is:pr ${query} author:${githubUsername}`,
27
59
  sort: 'updated',
28
60
  order: 'desc',
29
61
  per_page: 100,
@@ -32,49 +64,35 @@ export async function fetchUserMergedPRCounts(octokit, githubUsername) {
32
64
  for (const item of data.items) {
33
65
  const parsed = extractOwnerRepo(item.html_url);
34
66
  if (!parsed) {
35
- warn(MODULE, `Skipping merged PR with unparseable URL: ${item.html_url}`);
67
+ warn(MODULE, `Skipping ${label} PR with unparseable URL: ${item.html_url}`);
36
68
  continue;
37
69
  }
38
70
  const { owner } = parsed;
39
71
  const repo = `${owner}/${parsed.repo}`;
40
72
  // Skip own repos (PRs to your own repos aren't OSS contributions)
41
- if (owner.toLowerCase() === githubUsername.toLowerCase())
73
+ if (isOwnRepo(owner, githubUsername))
42
74
  continue;
43
75
  // Note: excludeRepos/excludeOrgs are intentionally NOT filtered here.
44
76
  // Those filters control issue discovery/search, not historical statistics.
45
- // A merged PR is a merged PR regardless of current tracking preferences.
46
- const mergedAt = item.pull_request?.merged_at || item.closed_at || '';
47
- // Per-repo tracking
48
- const existing = repos.get(repo);
49
- if (existing) {
50
- existing.count += 1;
51
- if (mergedAt && mergedAt > existing.lastMergedAt) {
52
- existing.lastMergedAt = mergedAt;
53
- }
54
- }
55
- else {
56
- repos.set(repo, { count: 1, lastMergedAt: mergedAt });
57
- }
58
- // Monthly histogram (every PR counted individually)
59
- if (mergedAt) {
60
- const month = mergedAt.slice(0, 7); // "YYYY-MM"
77
+ // Per-repo accumulation + get primary date for histograms
78
+ const primaryDate = accumulateRepo(repos, repo, item);
79
+ // Monthly histogram for primary date (merged/closed)
80
+ if (primaryDate) {
81
+ const month = primaryDate.slice(0, 7); // "YYYY-MM"
61
82
  monthlyCounts[month] = (monthlyCounts[month] || 0) + 1;
83
+ // Daily activity for primary date
84
+ const day = primaryDate.slice(0, 10);
85
+ if (day.length === 10)
86
+ dailyActivityCounts[day] = (dailyActivityCounts[day] || 0) + 1;
62
87
  }
63
- // Track when this PR was opened (for monthly opened histogram)
88
+ // Track when this PR was opened (for monthly opened histogram + daily activity)
64
89
  if (item.created_at) {
65
90
  const openedMonth = item.created_at.slice(0, 7); // "YYYY-MM"
66
91
  monthlyOpenedCounts[openedMonth] = (monthlyOpenedCounts[openedMonth] || 0) + 1;
67
- // Daily activity: PR opened
68
92
  const openedDay = item.created_at.slice(0, 10);
69
93
  if (openedDay.length === 10)
70
94
  dailyActivityCounts[openedDay] = (dailyActivityCounts[openedDay] || 0) + 1;
71
95
  }
72
- // Daily activity: PR merged
73
- if (mergedAt) {
74
- const mergedDay = mergedAt.slice(0, 10);
75
- if (mergedDay.length === 10)
76
- dailyActivityCounts[mergedDay] = (dailyActivityCounts[mergedDay] || 0) + 1;
77
- }
78
96
  }
79
97
  fetched += data.items.length;
80
98
  // Stop if we've fetched all results or hit the API limit (1000)
@@ -83,120 +101,48 @@ export async function fetchUserMergedPRCounts(octokit, githubUsername) {
83
101
  }
84
102
  page++;
85
103
  }
86
- debug(MODULE, `Found ${fetched} merged PRs across ${repos.size} repos`);
104
+ debug(MODULE, `Found ${fetched} ${label} PRs across ${repos.size} repos`);
105
+ // Cache the aggregated result (Map → entries array for JSON serialization)
106
+ cache.set(cacheKey, '', {
107
+ reposEntries: Array.from(repos.entries()),
108
+ monthlyCounts,
109
+ monthlyOpenedCounts,
110
+ dailyActivityCounts,
111
+ });
87
112
  return { repos, monthlyCounts, monthlyOpenedCounts, dailyActivityCounts };
88
113
  }
89
114
  /**
90
- * Fetch closed-without-merge PR counts per repository for the configured user.
91
- * Used to populate closedWithoutMergeCount in repo scores for accurate merge rate.
115
+ * Fetch merged PR counts and latest merge dates per repository for the configured user.
116
+ * Also builds a monthly histogram of all merges for the contribution timeline.
92
117
  */
93
- export async function fetchUserClosedPRCounts(octokit, githubUsername) {
94
- if (!githubUsername) {
95
- return { repos: new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
96
- }
97
- debug(MODULE, `Fetching closed PR counts for @${githubUsername}...`);
98
- const repos = new Map();
99
- const monthlyCounts = {};
100
- const monthlyOpenedCounts = {};
101
- const dailyActivityCounts = {};
102
- let page = 1;
103
- let fetched = 0;
104
- while (true) {
105
- const { data } = await octokit.search.issuesAndPullRequests({
106
- q: `is:pr is:closed is:unmerged author:${githubUsername}`,
107
- sort: 'updated',
108
- order: 'desc',
109
- per_page: 100,
110
- page,
111
- });
112
- for (const item of data.items) {
113
- const parsed = extractOwnerRepo(item.html_url);
114
- if (!parsed) {
115
- warn(MODULE, `Skipping closed PR with unparseable URL: ${item.html_url}`);
116
- continue;
117
- }
118
- const { owner } = parsed;
119
- const repo = `${owner}/${parsed.repo}`;
120
- // Skip own repos
121
- if (owner.toLowerCase() === githubUsername.toLowerCase())
122
- continue;
123
- // Note: excludeRepos/excludeOrgs are intentionally NOT filtered here.
124
- // Those filters control issue discovery/search, not historical statistics.
125
- // A closed PR is a closed PR regardless of current tracking preferences.
126
- repos.set(repo, (repos.get(repo) || 0) + 1);
127
- // Track when this PR was closed (for monthly closed histogram)
128
- if (item.closed_at) {
129
- const closedMonth = item.closed_at.slice(0, 7); // "YYYY-MM"
130
- monthlyCounts[closedMonth] = (monthlyCounts[closedMonth] || 0) + 1;
131
- // Daily activity: PR closed
132
- const closedDay = item.closed_at.slice(0, 10);
133
- if (closedDay.length === 10)
134
- dailyActivityCounts[closedDay] = (dailyActivityCounts[closedDay] || 0) + 1;
135
- }
136
- // Track when this PR was opened (for monthly opened histogram)
137
- if (item.created_at) {
138
- const openedMonth = item.created_at.slice(0, 7); // "YYYY-MM"
139
- monthlyOpenedCounts[openedMonth] = (monthlyOpenedCounts[openedMonth] || 0) + 1;
140
- // Daily activity: PR opened
141
- const openedDay = item.created_at.slice(0, 10);
142
- if (openedDay.length === 10)
143
- dailyActivityCounts[openedDay] = (dailyActivityCounts[openedDay] || 0) + 1;
118
+ export function fetchUserMergedPRCounts(octokit, githubUsername) {
119
+ return fetchUserPRCounts(octokit, githubUsername, 'is:merged', 'merged', (repos, repo, item) => {
120
+ if (!item.pull_request?.merged_at) {
121
+ warn(MODULE, `merged_at missing for merged PR ${item.html_url}${item.closed_at ? ', falling back to closed_at' : ', no date available'}`);
122
+ }
123
+ const mergedAt = item.pull_request?.merged_at || item.closed_at || '';
124
+ const existing = repos.get(repo);
125
+ if (existing) {
126
+ existing.count += 1;
127
+ if (mergedAt && mergedAt > existing.lastMergedAt) {
128
+ existing.lastMergedAt = mergedAt;
144
129
  }
145
130
  }
146
- fetched += data.items.length;
147
- if (fetched >= data.total_count || fetched >= 1000 || data.items.length === 0) {
148
- break;
131
+ else {
132
+ repos.set(repo, { count: 1, lastMergedAt: mergedAt });
149
133
  }
150
- page++;
151
- }
152
- debug(MODULE, `Found ${fetched} closed (unmerged) PRs across ${repos.size} repos`);
153
- return { repos, monthlyCounts, monthlyOpenedCounts, dailyActivityCounts };
134
+ return mergedAt;
135
+ });
154
136
  }
155
137
  /**
156
- * Fetch GitHub star counts for a list of repositories.
157
- * Used to populate stargazersCount in repo scores for dashboard filtering by minStars.
158
- * Fetches concurrently with per-repo error isolation (missing/private repos are skipped).
138
+ * Fetch closed-without-merge PR counts per repository for the configured user.
139
+ * Used to populate closedWithoutMergeCount in repo scores for accurate merge rate.
159
140
  */
160
- export async function fetchRepoStarCounts(octokit, repos) {
161
- if (repos.length === 0)
162
- return new Map();
163
- debug(MODULE, `Fetching star counts for ${repos.length} repos...`);
164
- const results = new Map();
165
- // Fetch in parallel chunks to avoid overwhelming the API
166
- const chunkSize = 10;
167
- for (let i = 0; i < repos.length; i += chunkSize) {
168
- const chunk = repos.slice(i, i + chunkSize);
169
- const settled = await Promise.allSettled(chunk.map(async (repo) => {
170
- const parts = repo.split('/');
171
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
172
- throw new ValidationError(`Malformed repo identifier: "${repo}"`);
173
- }
174
- const [owner, name] = parts;
175
- const { data } = await octokit.repos.get({ owner, repo: name });
176
- return { repo, stars: data.stargazers_count };
177
- }));
178
- let chunkFailures = 0;
179
- for (let j = 0; j < settled.length; j++) {
180
- const result = settled[j];
181
- if (result.status === 'fulfilled') {
182
- results.set(result.value.repo, result.value.stars);
183
- }
184
- else {
185
- chunkFailures++;
186
- warn(MODULE, `Failed to fetch stars for ${chunk[j]}: ${result.reason instanceof Error ? result.reason.message : result.reason}`);
187
- }
188
- }
189
- // If entire chunk failed, likely a systemic issue (rate limit, auth, outage) — abort remaining
190
- if (chunkFailures === chunk.length && chunk.length > 0) {
191
- const remaining = repos.length - i - chunkSize;
192
- if (remaining > 0) {
193
- warn(MODULE, `Entire chunk failed, aborting remaining ${remaining} repos`);
194
- }
195
- break;
196
- }
197
- }
198
- debug(MODULE, `Fetched star counts for ${results.size}/${repos.length} repos`);
199
- return results;
141
+ export function fetchUserClosedPRCounts(octokit, githubUsername) {
142
+ return fetchUserPRCounts(octokit, githubUsername, 'is:closed is:unmerged', 'closed', (repos, repo, item) => {
143
+ repos.set(repo, (repos.get(repo) || 0) + 1);
144
+ return item.closed_at || '';
145
+ });
200
146
  }
201
147
  /**
202
148
  * Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
@@ -226,7 +172,7 @@ export async function fetchRecentPRs(octokit, config, query, label, days, mapIte
226
172
  }
227
173
  const repo = `${parsed.owner}/${parsed.repo}`;
228
174
  // Skip own repos
229
- if (parsed.owner.toLowerCase() === config.githubUsername.toLowerCase())
175
+ if (isOwnRepo(parsed.owner, config.githubUsername))
230
176
  continue;
231
177
  // Skip excluded repos and orgs
232
178
  if (config.excludeRepos.includes(repo))
@@ -32,6 +32,12 @@ export declare class HttpCache {
32
32
  private keyFor;
33
33
  /** Full path to the cache file for a given URL. */
34
34
  private pathFor;
35
+ /**
36
+ * Return the cached body if the entry exists and is younger than `maxAgeMs`.
37
+ * Useful for time-based caching where ETag validation isn't applicable
38
+ * (e.g., caching aggregated results from paginated API calls).
39
+ */
40
+ getIfFresh(key: string, maxAgeMs: number): unknown | null;
35
41
  /**
36
42
  * Look up a cached response. Returns `null` if no cache entry exists.
37
43
  */
@@ -14,6 +14,7 @@ import * as path from 'path';
14
14
  import * as crypto from 'crypto';
15
15
  import { getCacheDir } from './utils.js';
16
16
  import { debug } from './logger.js';
17
+ import { getHttpStatusCode } from './errors.js';
17
18
  const MODULE = 'http-cache';
18
19
  /**
19
20
  * Maximum age (in ms) before a cache entry is considered stale and eligible for
@@ -44,6 +45,20 @@ export class HttpCache {
44
45
  pathFor(url) {
45
46
  return path.join(this.cacheDir, `${this.keyFor(url)}.json`);
46
47
  }
48
+ /**
49
+ * Return the cached body if the entry exists and is younger than `maxAgeMs`.
50
+ * Useful for time-based caching where ETag validation isn't applicable
51
+ * (e.g., caching aggregated results from paginated API calls).
52
+ */
53
+ getIfFresh(key, maxAgeMs) {
54
+ const entry = this.get(key);
55
+ if (!entry)
56
+ return null;
57
+ const age = Date.now() - new Date(entry.cachedAt).getTime();
58
+ if (!Number.isFinite(age) || age < 0 || age > maxAgeMs)
59
+ return null;
60
+ return entry.body;
61
+ }
47
62
  /**
48
63
  * Look up a cached response. Returns `null` if no cache entry exists.
49
64
  */
@@ -262,8 +277,5 @@ export async function cachedRequest(cache, url, fetcher) {
262
277
  * Octokit throws a RequestError with status 304 for conditional requests.
263
278
  */
264
279
  function isNotModifiedError(err) {
265
- if (err && typeof err === 'object' && 'status' in err) {
266
- return err.status === 304;
267
- }
268
- return false;
280
+ return getHttpStatusCode(err) === 304;
269
281
  }
@@ -8,7 +8,8 @@ export { IssueDiscovery, type IssueCandidate, type SearchPriority, isDocOnlyIssu
8
8
  export { IssueConversationMonitor } from './issue-conversation.js';
9
9
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
11
- export { parseGitHubUrl, daysBetween, splitRepo, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
11
+ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
12
+ export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
12
13
  export { enableDebug, isDebugEnabled, debug, warn, timed } from './logger.js';
13
14
  export { HttpCache, getHttpCache, resetHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
14
15
  export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
@@ -8,7 +8,8 @@ export { IssueDiscovery, isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS, } fro
8
8
  export { IssueConversationMonitor } from './issue-conversation.js';
9
9
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  export { getOctokit, checkRateLimit } from './github.js';
11
- export { parseGitHubUrl, daysBetween, splitRepo, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
11
+ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
12
+ export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
12
13
  export { enableDebug, isDebugEnabled, debug, warn, timed } from './logger.js';
13
14
  export { HttpCache, getHttpCache, resetHttpCache, cachedRequest } from './http-cache.js';
14
15
  export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
@@ -9,9 +9,9 @@ import { getOctokit } from './github.js';
9
9
  import { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  import { paginateAll } from './pagination.js';
11
11
  import { getStateManager } from './state.js';
12
- import { daysBetween, splitRepo, extractOwnerRepo } from './utils.js';
12
+ import { daysBetween, splitRepo, extractOwnerRepo, isOwnRepo } from './utils.js';
13
13
  import { runWorkerPool } from './concurrency.js';
14
- import { ConfigurationError } from './errors.js';
14
+ import { ConfigurationError, errorMessage } from './errors.js';
15
15
  import { debug, warn } from './logger.js';
16
16
  const MODULE = 'issue-conversation';
17
17
  const MAX_CONCURRENT_REQUESTS = 5;
@@ -71,7 +71,7 @@ export class IssueConversationMonitor {
71
71
  const { owner, repo } = parsed;
72
72
  const repoFullName = `${owner}/${repo}`;
73
73
  // Skip issues in user-owned repos (we only care about contributing to others' projects)
74
- if (owner.toLowerCase() === username.toLowerCase())
74
+ if (isOwnRepo(owner, username))
75
75
  continue;
76
76
  // Skip user-authored issues
77
77
  if (item.user?.login?.toLowerCase() === username.toLowerCase())
@@ -107,7 +107,7 @@ export class IssueConversationMonitor {
107
107
  }
108
108
  }
109
109
  catch (error) {
110
- const msg = error instanceof Error ? error.message : String(error);
110
+ const msg = errorMessage(error);
111
111
  failures.push({ issueUrl: item.html_url, error: msg });
112
112
  warn(MODULE, `Error analyzing issue ${item.html_url}: ${msg}`);
113
113
  }
@@ -12,7 +12,7 @@ import { getOctokit, checkRateLimit } from './github.js';
12
12
  import { getStateManager } from './state.js';
13
13
  import { daysBetween, getDataDir } from './utils.js';
14
14
  import { DEFAULT_CONFIG } from './types.js';
15
- import { ValidationError } from './errors.js';
15
+ import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
16
16
  import { warn } from './logger.js';
17
17
  import { isDocOnlyIssue, detectLabelFarmingRepos, applyPerRepoCap } from './issue-filtering.js';
18
18
  import { IssueVetter } from './issue-vetting.js';
@@ -78,16 +78,16 @@ export class IssueDiscovery {
78
78
  }
79
79
  catch (error) {
80
80
  const cachedRepos = this.stateManager.getStarredRepos();
81
- const errorMessage = error instanceof Error ? error.message : String(error);
82
- warn(MODULE, 'Error fetching starred repos:', errorMessage);
81
+ const errMsg = errorMessage(error);
82
+ warn(MODULE, 'Error fetching starred repos:', errMsg);
83
83
  if (cachedRepos.length === 0) {
84
84
  warn(MODULE, `Failed to fetch starred repositories from GitHub API. ` +
85
- `No cached repos available. Error: ${errorMessage}\n` +
85
+ `No cached repos available. Error: ${errMsg}\n` +
86
86
  `Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`);
87
87
  }
88
88
  else {
89
89
  warn(MODULE, `Failed to fetch starred repositories from GitHub API. ` +
90
- `Using ${cachedRepos.length} cached repos instead. Error: ${errorMessage}`);
90
+ `Using ${cachedRepos.length} cached repos instead. Error: ${errMsg}`);
91
91
  }
92
92
  return cachedRepos;
93
93
  }
@@ -128,11 +128,11 @@ export class IssueDiscovery {
128
128
  }
129
129
  catch (error) {
130
130
  // Fail fast on auth errors — no point searching with a bad token
131
- if (error?.status === 401) {
131
+ if (getHttpStatusCode(error) === 401) {
132
132
  throw error;
133
133
  }
134
134
  // Non-fatal: proceed with search for transient/network errors
135
- warn(MODULE, 'Could not check rate limit:', error instanceof Error ? error.message : error);
135
+ warn(MODULE, 'Could not check rate limit:', errorMessage(error));
136
136
  }
137
137
  // Get merged-PR repos (highest merge probability)
138
138
  const mergedPRRepos = this.stateManager.getReposWithMergedPRs();
@@ -306,12 +306,12 @@ export class IssueDiscovery {
306
306
  console.log(`Found ${starFiltered.length} candidates from general search`);
307
307
  }
308
308
  catch (error) {
309
- const errorMessage = error instanceof Error ? error.message : String(error);
310
- phase2Error = errorMessage;
309
+ const errMsg = errorMessage(error);
310
+ phase2Error = errMsg;
311
311
  if (IssueVetter.isRateLimitError(error)) {
312
312
  rateLimitHitDuringSearch = true;
313
313
  }
314
- warn(MODULE, `Error in general issue search: ${errorMessage}`);
314
+ warn(MODULE, `Error in general issue search: ${errMsg}`);
315
315
  }
316
316
  }
317
317
  // Phase 3: Actively maintained repos (#349)
@@ -376,12 +376,12 @@ export class IssueDiscovery {
376
376
  console.log(`Found ${starFiltered.length} candidates from maintained-repo search`);
377
377
  }
378
378
  catch (error) {
379
- const errorMessage = error instanceof Error ? error.message : String(error);
380
- phase3Error = errorMessage;
379
+ const errMsg = errorMessage(error);
380
+ phase3Error = errMsg;
381
381
  if (IssueVetter.isRateLimitError(error)) {
382
382
  rateLimitHitDuringSearch = true;
383
383
  }
384
- warn(MODULE, `Error in maintained-repo search: ${errorMessage}`);
384
+ warn(MODULE, `Error in maintained-repo search: ${errMsg}`);
385
385
  }
386
386
  }
387
387
  if (allCandidates.length === 0) {
@@ -476,7 +476,7 @@ export class IssueDiscovery {
476
476
  rateLimitFailures++;
477
477
  }
478
478
  const batchRepos = batch.join(', ');
479
- warn(MODULE, `Error searching issues in batch [${batchRepos}]:`, error instanceof Error ? error.message : error);
479
+ warn(MODULE, `Error searching issues in batch [${batchRepos}]:`, errorMessage(error));
480
480
  }
481
481
  }
482
482
  const allBatchesFailed = failedBatches === batches.length && batches.length > 0;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { paginateAll } from './pagination.js';
8
8
  import { parseGitHubUrl, daysBetween } from './utils.js';
9
- import { ValidationError } from './errors.js';
9
+ import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
10
10
  import { warn } from './logger.js';
11
11
  import { getHttpCache, cachedRequest } from './http-cache.js';
12
12
  import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
@@ -254,7 +254,7 @@ export class IssueVetter {
254
254
  if (IssueVetter.isRateLimitError(error)) {
255
255
  rateLimitFailures++;
256
256
  }
257
- warn(MODULE, `Error vetting issue ${url}:`, error instanceof Error ? error.message : error);
257
+ warn(MODULE, `Error vetting issue ${url}:`, errorMessage(error));
258
258
  });
259
259
  pending.push(task);
260
260
  // Limit concurrency
@@ -275,11 +275,11 @@ export class IssueVetter {
275
275
  }
276
276
  /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
277
277
  static isRateLimitError(error) {
278
- const status = error?.status;
278
+ const status = getHttpStatusCode(error);
279
279
  if (status === 429)
280
280
  return true;
281
281
  if (status === 403) {
282
- const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
282
+ const msg = errorMessage(error).toLowerCase();
283
283
  return msg.includes('rate limit');
284
284
  }
285
285
  return false;
@@ -306,9 +306,9 @@ export class IssueVetter {
306
306
  return { passed: data.total_count === 0 && linkedPRs.length === 0 };
307
307
  }
308
308
  catch (error) {
309
- const errorMessage = error instanceof Error ? error.message : String(error);
310
- warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errorMessage}. Assuming no existing PR.`);
311
- return { passed: true, inconclusive: true, reason: errorMessage };
309
+ const errMsg = errorMessage(error);
310
+ warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
311
+ return { passed: true, inconclusive: true, reason: errMsg };
312
312
  }
313
313
  }
314
314
  /**
@@ -325,8 +325,8 @@ export class IssueVetter {
325
325
  return data.total_count;
326
326
  }
327
327
  catch (error) {
328
- const errorMessage = error instanceof Error ? error.message : String(error);
329
- warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${errorMessage}. Defaulting to 0.`);
328
+ const errMsg = errorMessage(error);
329
+ warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
330
330
  return 0;
331
331
  }
332
332
  }
@@ -370,9 +370,9 @@ export class IssueVetter {
370
370
  return { passed: true };
371
371
  }
372
372
  catch (error) {
373
- const errorMessage = error instanceof Error ? error.message : String(error);
374
- warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errorMessage}. Assuming not claimed.`);
375
- return { passed: true, inconclusive: true, reason: errorMessage };
373
+ const errMsg = errorMessage(error);
374
+ warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
375
+ return { passed: true, inconclusive: true, reason: errMsg };
376
376
  }
377
377
  }
378
378
  async checkProjectHealth(owner, repo) {
@@ -403,8 +403,8 @@ export class IssueVetter {
403
403
  }
404
404
  }
405
405
  catch (error) {
406
- const errorMessage = error instanceof Error ? error.message : String(error);
407
- warn(MODULE, `Failed to check CI status for ${owner}/${repo}: ${errorMessage}. Defaulting to unknown.`);
406
+ const errMsg = errorMessage(error);
407
+ warn(MODULE, `Failed to check CI status for ${owner}/${repo}: ${errMsg}. Defaulting to unknown.`);
408
408
  }
409
409
  return {
410
410
  repo: `${owner}/${repo}`,
@@ -419,8 +419,8 @@ export class IssueVetter {
419
419
  };
420
420
  }
421
421
  catch (error) {
422
- const errorMessage = error instanceof Error ? error.message : String(error);
423
- warn(MODULE, `Error checking project health for ${owner}/${repo}: ${errorMessage}`);
422
+ const errMsg = errorMessage(error);
423
+ warn(MODULE, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
424
424
  return {
425
425
  repo: `${owner}/${repo}`,
426
426
  lastCommitAt: '',
@@ -430,7 +430,7 @@ export class IssueVetter {
430
430
  ciStatus: 'unknown',
431
431
  isActive: false,
432
432
  checkFailed: true,
433
- failureReason: errorMessage,
433
+ failureReason: errMsg,
434
434
  };
435
435
  }
436
436
  }
@@ -12,6 +12,7 @@
12
12
  * - github-stats.ts: Merged/closed PR counts and star fetching
13
13
  */
14
14
  import { FetchedPR, DailyDigest, ClosedPR, MergedPR } from './types.js';
15
+ import { type PRCountsResult } from './github-stats.js';
15
16
  export { computeDisplayLabel } from './display-utils.js';
16
17
  export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
17
18
  export { isConditionalChecklistItem } from './checklist-analysis.js';
@@ -59,35 +60,20 @@ export declare class PRMonitor {
59
60
  * Fetch merged PR counts and latest merge dates per repository for the configured user.
60
61
  * Delegates to github-stats module.
61
62
  */
62
- fetchUserMergedPRCounts(): Promise<{
63
- repos: Map<string, {
64
- count: number;
65
- lastMergedAt: string;
66
- }>;
67
- monthlyCounts: Record<string, number>;
68
- monthlyOpenedCounts: Record<string, number>;
69
- dailyActivityCounts: Record<string, number>;
70
- }>;
63
+ fetchUserMergedPRCounts(): Promise<PRCountsResult<{
64
+ count: number;
65
+ lastMergedAt: string;
66
+ }>>;
71
67
  /**
72
68
  * Fetch closed-without-merge PR counts per repository for the configured user.
73
69
  * Delegates to github-stats module.
74
70
  */
75
- fetchUserClosedPRCounts(): Promise<{
76
- repos: Map<string, number>;
77
- monthlyCounts: Record<string, number>;
78
- monthlyOpenedCounts: Record<string, number>;
79
- dailyActivityCounts: Record<string, number>;
80
- }>;
71
+ fetchUserClosedPRCounts(): Promise<PRCountsResult<number>>;
81
72
  /**
82
73
  * Fetch GitHub star counts for a list of repositories.
83
74
  * Delegates to github-stats module.
84
75
  */
85
76
  fetchRepoStarCounts(repos: string[]): Promise<Map<string, number>>;
86
- /**
87
- * Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
88
- * Returns parsed search results that pass all filters.
89
- */
90
- private fetchRecentPRs;
91
77
  /**
92
78
  * Fetch PRs closed without merge in the last N days.
93
79
  * Delegates to github-stats module.