@oss-autopilot/core 0.42.0 → 0.42.2

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 (50) hide show
  1. package/dist/cli.bundle.cjs +1026 -1018
  2. package/dist/cli.js +18 -30
  3. package/dist/commands/check-integration.js +5 -4
  4. package/dist/commands/comments.js +24 -24
  5. package/dist/commands/daily.d.ts +0 -1
  6. package/dist/commands/daily.js +18 -16
  7. package/dist/commands/dashboard-components.d.ts +33 -0
  8. package/dist/commands/dashboard-components.js +57 -0
  9. package/dist/commands/dashboard-data.js +7 -6
  10. package/dist/commands/dashboard-formatters.d.ts +20 -0
  11. package/dist/commands/dashboard-formatters.js +33 -0
  12. package/dist/commands/dashboard-scripts.d.ts +7 -0
  13. package/dist/commands/dashboard-scripts.js +281 -0
  14. package/dist/commands/dashboard-server.js +3 -2
  15. package/dist/commands/dashboard-styles.d.ts +5 -0
  16. package/dist/commands/dashboard-styles.js +765 -0
  17. package/dist/commands/dashboard-templates.d.ts +6 -18
  18. package/dist/commands/dashboard-templates.js +30 -1134
  19. package/dist/commands/dashboard.js +2 -1
  20. package/dist/commands/dismiss.d.ts +6 -6
  21. package/dist/commands/dismiss.js +13 -13
  22. package/dist/commands/local-repos.js +2 -1
  23. package/dist/commands/parse-list.js +2 -1
  24. package/dist/commands/startup.js +6 -16
  25. package/dist/commands/validation.d.ts +3 -1
  26. package/dist/commands/validation.js +12 -6
  27. package/dist/core/errors.d.ts +9 -0
  28. package/dist/core/errors.js +17 -0
  29. package/dist/core/github-stats.d.ts +14 -21
  30. package/dist/core/github-stats.js +84 -138
  31. package/dist/core/http-cache.d.ts +6 -0
  32. package/dist/core/http-cache.js +16 -4
  33. package/dist/core/index.d.ts +3 -2
  34. package/dist/core/index.js +3 -2
  35. package/dist/core/issue-conversation.js +4 -4
  36. package/dist/core/issue-discovery.d.ts +5 -0
  37. package/dist/core/issue-discovery.js +70 -93
  38. package/dist/core/issue-vetting.js +17 -17
  39. package/dist/core/logger.d.ts +5 -0
  40. package/dist/core/logger.js +8 -0
  41. package/dist/core/pr-monitor.d.ts +6 -20
  42. package/dist/core/pr-monitor.js +16 -52
  43. package/dist/core/review-analysis.js +8 -6
  44. package/dist/core/state.js +4 -5
  45. package/dist/core/test-utils.d.ts +14 -0
  46. package/dist/core/test-utils.js +125 -0
  47. package/dist/core/utils.d.ts +11 -0
  48. package/dist/core/utils.js +21 -0
  49. package/dist/formatters/json.d.ts +0 -1
  50. package/package.json +1 -1
@@ -1196,7 +1196,7 @@ var require_command = __commonJS({
1196
1196
  "../../node_modules/.pnpm/commander@14.0.3/node_modules/commander/lib/command.js"(exports2) {
1197
1197
  var EventEmitter = require("node:events").EventEmitter;
1198
1198
  var childProcess = require("node:child_process");
1199
- var path11 = require("node:path");
1199
+ var path10 = require("node:path");
1200
1200
  var fs10 = require("node:fs");
1201
1201
  var process2 = require("node:process");
1202
1202
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -2209,9 +2209,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2209
2209
  let launchWithNode = false;
2210
2210
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2211
2211
  function findFile(baseDir, baseName) {
2212
- const localBin = path11.resolve(baseDir, baseName);
2212
+ const localBin = path10.resolve(baseDir, baseName);
2213
2213
  if (fs10.existsSync(localBin)) return localBin;
2214
- if (sourceExt.includes(path11.extname(baseName))) return void 0;
2214
+ if (sourceExt.includes(path10.extname(baseName))) return void 0;
2215
2215
  const foundExt = sourceExt.find(
2216
2216
  (ext) => fs10.existsSync(`${localBin}${ext}`)
2217
2217
  );
@@ -2229,17 +2229,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
2229
2229
  } catch {
2230
2230
  resolvedScriptPath = this._scriptPath;
2231
2231
  }
2232
- executableDir = path11.resolve(
2233
- path11.dirname(resolvedScriptPath),
2232
+ executableDir = path10.resolve(
2233
+ path10.dirname(resolvedScriptPath),
2234
2234
  executableDir
2235
2235
  );
2236
2236
  }
2237
2237
  if (executableDir) {
2238
2238
  let localFile = findFile(executableDir, executableFile);
2239
2239
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2240
- const legacyName = path11.basename(
2240
+ const legacyName = path10.basename(
2241
2241
  this._scriptPath,
2242
- path11.extname(this._scriptPath)
2242
+ path10.extname(this._scriptPath)
2243
2243
  );
2244
2244
  if (legacyName !== this._name) {
2245
2245
  localFile = findFile(
@@ -2250,7 +2250,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2250
2250
  }
2251
2251
  executableFile = localFile || executableFile;
2252
2252
  }
2253
- launchWithNode = sourceExt.includes(path11.extname(executableFile));
2253
+ launchWithNode = sourceExt.includes(path10.extname(executableFile));
2254
2254
  let proc;
2255
2255
  if (process2.platform !== "win32") {
2256
2256
  if (launchWithNode) {
@@ -3165,7 +3165,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3165
3165
  * @return {Command}
3166
3166
  */
3167
3167
  nameFromFilename(filename) {
3168
- this._name = path11.basename(filename, path11.extname(filename));
3168
+ this._name = path10.basename(filename, path10.extname(filename));
3169
3169
  return this;
3170
3170
  }
3171
3171
  /**
@@ -3179,9 +3179,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3179
3179
  * @param {string} [path]
3180
3180
  * @return {(string|null|Command)}
3181
3181
  */
3182
- executableDir(path12) {
3183
- if (path12 === void 0) return this._executableDir;
3184
- this._executableDir = path12;
3182
+ executableDir(path11) {
3183
+ if (path11 === void 0) return this._executableDir;
3184
+ this._executableDir = path11;
3185
3185
  return this;
3186
3186
  }
3187
3187
  /**
@@ -3498,6 +3498,16 @@ var init_types = __esm({
3498
3498
  });
3499
3499
 
3500
3500
  // src/core/errors.ts
3501
+ function errorMessage(e) {
3502
+ return e instanceof Error ? e.message : String(e);
3503
+ }
3504
+ function getHttpStatusCode(error) {
3505
+ if (error && typeof error === "object" && "status" in error) {
3506
+ const status = error.status;
3507
+ return typeof status === "number" && Number.isFinite(status) ? status : void 0;
3508
+ }
3509
+ return void 0;
3510
+ }
3501
3511
  var OssAutopilotError, ConfigurationError, ValidationError;
3502
3512
  var init_errors = __esm({
3503
3513
  "src/core/errors.ts"() {
@@ -3533,6 +3543,10 @@ function debug(module2, message, ...args) {
3533
3543
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3534
3544
  console.error(`[${timestamp}] [DEBUG] [${module2}] ${message}`, ...args);
3535
3545
  }
3546
+ function info(module2, message, ...args) {
3547
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3548
+ console.error(`[${timestamp}] [INFO] [${module2}] ${message}`, ...args);
3549
+ }
3536
3550
  function warn(module2, message, ...args) {
3537
3551
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3538
3552
  console.error(`[${timestamp}] [WARN] [${module2}] ${message}`, ...args);
@@ -3640,6 +3654,17 @@ function splitRepo(repoFullName) {
3640
3654
  const [owner, repo] = repoFullName.split("/");
3641
3655
  return { owner, repo };
3642
3656
  }
3657
+ function isOwnRepo(owner, username) {
3658
+ return owner.toLowerCase() === username.toLowerCase();
3659
+ }
3660
+ function getCLIVersion() {
3661
+ try {
3662
+ const pkgPath = path.join(path.dirname(process.argv[1]), "..", "package.json");
3663
+ return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
3664
+ } catch {
3665
+ return "0.0.0";
3666
+ }
3667
+ }
3643
3668
  function formatRelativeTime(dateStr) {
3644
3669
  const date = new Date(dateStr);
3645
3670
  const diffMs = Date.now() - date.getTime();
@@ -3960,8 +3985,7 @@ var init_state = __esm({
3960
3985
  debug(MODULE2, "Migration complete!");
3961
3986
  return true;
3962
3987
  } catch (error) {
3963
- const errorMessage = error instanceof Error ? error.message : String(error);
3964
- warn(MODULE2, `Failed to migrate state: ${errorMessage}`);
3988
+ warn(MODULE2, `Failed to migrate state: ${errorMessage(error)}`);
3965
3989
  const newStatePath2 = getStatePath();
3966
3990
  if (fs2.existsSync(newStatePath2) && fs2.existsSync(LEGACY_STATE_FILE)) {
3967
3991
  try {
@@ -4113,11 +4137,11 @@ var init_state = __esm({
4113
4137
  try {
4114
4138
  fs2.unlinkSync(path2.join(backupDir, file));
4115
4139
  } catch (error) {
4116
- warn(MODULE2, `Could not delete old backup ${file}:`, error instanceof Error ? error.message : error);
4140
+ warn(MODULE2, `Could not delete old backup ${file}:`, errorMessage(error));
4117
4141
  }
4118
4142
  }
4119
4143
  } catch (error) {
4120
- warn(MODULE2, "Could not clean up backups:", error instanceof Error ? error.message : error);
4144
+ warn(MODULE2, "Could not clean up backups:", errorMessage(error));
4121
4145
  }
4122
4146
  }
4123
4147
  /**
@@ -4445,11 +4469,11 @@ var init_state = __esm({
4445
4469
  * @returns true if the PR is snoozed and the snooze has not expired.
4446
4470
  */
4447
4471
  isSnoozed(url) {
4448
- const info = this.getSnoozeInfo(url);
4449
- if (!info) return false;
4450
- const expiresAtMs = new Date(info.expiresAt).getTime();
4472
+ const info2 = this.getSnoozeInfo(url);
4473
+ if (!info2) return false;
4474
+ const expiresAtMs = new Date(info2.expiresAt).getTime();
4451
4475
  if (isNaN(expiresAtMs)) {
4452
- warn(MODULE2, `Invalid expiresAt for snoozed PR ${url}: "${info.expiresAt}". Treating as not snoozed.`);
4476
+ warn(MODULE2, `Invalid expiresAt for snoozed PR ${url}: "${info2.expiresAt}". Treating as not snoozed.`);
4453
4477
  return false;
4454
4478
  }
4455
4479
  return expiresAtMs > Date.now();
@@ -4470,8 +4494,8 @@ var init_state = __esm({
4470
4494
  if (!this.state.config.snoozedPRs) return [];
4471
4495
  const expired = [];
4472
4496
  const now = Date.now();
4473
- for (const [url, info] of Object.entries(this.state.config.snoozedPRs)) {
4474
- const expiresAtMs = new Date(info.expiresAt).getTime();
4497
+ for (const [url, info2] of Object.entries(this.state.config.snoozedPRs)) {
4498
+ const expiresAtMs = new Date(info2.expiresAt).getTime();
4475
4499
  if (isNaN(expiresAtMs) || expiresAtMs <= now) {
4476
4500
  expired.push(url);
4477
4501
  }
@@ -5900,17 +5924,17 @@ function requestLog(octokit) {
5900
5924
  octokit.log.debug("request", options);
5901
5925
  const start = Date.now();
5902
5926
  const requestOptions = octokit.request.endpoint.parse(options);
5903
- const path11 = requestOptions.url.replace(options.baseUrl, "");
5927
+ const path10 = requestOptions.url.replace(options.baseUrl, "");
5904
5928
  return request2(options).then((response) => {
5905
5929
  const requestId = response.headers["x-github-request-id"];
5906
5930
  octokit.log.info(
5907
- `${requestOptions.method} ${path11} - ${response.status} with id ${requestId} in ${Date.now() - start}ms`
5931
+ `${requestOptions.method} ${path10} - ${response.status} with id ${requestId} in ${Date.now() - start}ms`
5908
5932
  );
5909
5933
  return response;
5910
5934
  }).catch((error) => {
5911
5935
  const requestId = error.response?.headers["x-github-request-id"] || "UNKNOWN";
5912
5936
  octokit.log.error(
5913
- `${requestOptions.method} ${path11} - ${error.status} with id ${requestId} in ${Date.now() - start}ms`
5937
+ `${requestOptions.method} ${path10} - ${error.status} with id ${requestId} in ${Date.now() - start}ms`
5914
5938
  );
5915
5939
  throw error;
5916
5940
  });
@@ -9891,7 +9915,7 @@ function isAuthRequest(method, pathname) {
9891
9915
  }
9892
9916
  function routeMatcher(paths) {
9893
9917
  const regexes = paths.map(
9894
- (path11) => path11.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
9918
+ (path10) => path10.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
9895
9919
  );
9896
9920
  const regex2 = `^(?:${regexes.map((r) => `(?:${r})`).join("|")})[^/]*$`;
9897
9921
  return new RegExp(regex2, "i");
@@ -9948,8 +9972,8 @@ function throttling(octokit, octokitOptions) {
9948
9972
  "error",
9949
9973
  (e) => octokit.log.warn("Error in throttling-plugin limit handler", e)
9950
9974
  );
9951
- state.retryLimiter.on("failed", async function(error, info) {
9952
- const [state2, request2, options] = info.args;
9975
+ state.retryLimiter.on("failed", async function(error, info2) {
9976
+ const [state2, request2, options] = info2.args;
9953
9977
  const { pathname } = new URL(options.url, "http://github.test");
9954
9978
  const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;
9955
9979
  if (!(shouldRetryGraphQL || error.status === 403 || error.status === 429)) {
@@ -10219,10 +10243,7 @@ async function cachedRequest(cache, url, fetcher) {
10219
10243
  }
10220
10244
  }
10221
10245
  function isNotModifiedError(err) {
10222
- if (err && typeof err === "object" && "status" in err) {
10223
- return err.status === 304;
10224
- }
10225
- return false;
10246
+ return getHttpStatusCode(err) === 304;
10226
10247
  }
10227
10248
  var fs3, path3, crypto, MODULE4, DEFAULT_MAX_AGE_MS, HttpCache, _httpCache;
10228
10249
  var init_http_cache = __esm({
@@ -10233,6 +10254,7 @@ var init_http_cache = __esm({
10233
10254
  crypto = __toESM(require("crypto"), 1);
10234
10255
  init_utils();
10235
10256
  init_logger();
10257
+ init_errors();
10236
10258
  MODULE4 = "http-cache";
10237
10259
  DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
10238
10260
  HttpCache = class {
@@ -10250,6 +10272,18 @@ var init_http_cache = __esm({
10250
10272
  pathFor(url) {
10251
10273
  return path3.join(this.cacheDir, `${this.keyFor(url)}.json`);
10252
10274
  }
10275
+ /**
10276
+ * Return the cached body if the entry exists and is younger than `maxAgeMs`.
10277
+ * Useful for time-based caching where ETag validation isn't applicable
10278
+ * (e.g., caching aggregated results from paginated API calls).
10279
+ */
10280
+ getIfFresh(key, maxAgeMs) {
10281
+ const entry = this.get(key);
10282
+ if (!entry) return null;
10283
+ const age = Date.now() - new Date(entry.cachedAt).getTime();
10284
+ if (!Number.isFinite(age) || age < 0 || age > maxAgeMs) return null;
10285
+ return entry.body;
10286
+ }
10253
10287
  /**
10254
10288
  * Look up a cached response. Returns `null` if no cache entry exists.
10255
10289
  */
@@ -10587,14 +10621,14 @@ function checkUnrespondedComments(comments, reviews, reviewComments, username) {
10587
10621
  for (const review of reviews) {
10588
10622
  if (!review.submitted_at) continue;
10589
10623
  const body = (review.body || "").trim();
10590
- if (!body && review.state !== "COMMENTED") continue;
10624
+ if (!body && review.state !== "COMMENTED" && review.state !== "CHANGES_REQUESTED") continue;
10591
10625
  const author = review.user?.login || "unknown";
10592
10626
  if (!body && review.state === "COMMENTED" && review.id != null) {
10593
10627
  if (isAllSelfReplies(review.id, reviewComments)) {
10594
10628
  continue;
10595
10629
  }
10596
10630
  }
10597
- const resolvedBody = body || (review.id != null ? getInlineCommentBody(review.id, reviewComments) : void 0) || "(posted inline review comments)";
10631
+ const resolvedBody = body || (review.id != null ? getInlineCommentBody(review.id, reviewComments) : void 0) || (review.state === "CHANGES_REQUESTED" ? "(requested changes via inline review comments)" : "(posted inline review comments)");
10598
10632
  timeline.push({
10599
10633
  author,
10600
10634
  body: resolvedBody,
@@ -10813,11 +10847,28 @@ var init_display_utils = __esm({
10813
10847
  });
10814
10848
 
10815
10849
  // src/core/github-stats.ts
10816
- async function fetchUserMergedPRCounts(octokit, githubUsername) {
10850
+ function isCachedPRCounts(v) {
10851
+ if (typeof v !== "object" || v === null) return false;
10852
+ const obj = v;
10853
+ return Array.isArray(obj.reposEntries) && typeof obj.monthlyCounts === "object" && obj.monthlyCounts !== null && typeof obj.monthlyOpenedCounts === "object" && obj.monthlyOpenedCounts !== null && typeof obj.dailyActivityCounts === "object" && obj.dailyActivityCounts !== null;
10854
+ }
10855
+ async function fetchUserPRCounts(octokit, githubUsername, query, label, accumulateRepo) {
10817
10856
  if (!githubUsername) {
10818
10857
  return { repos: /* @__PURE__ */ new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
10819
10858
  }
10820
- debug(MODULE6, `Fetching merged PR counts for @${githubUsername}...`);
10859
+ const cache = getHttpCache();
10860
+ const cacheKey = `pr-counts:${label}:${githubUsername}`;
10861
+ const cached = cache.getIfFresh(cacheKey, PR_COUNTS_CACHE_TTL_MS);
10862
+ if (cached && isCachedPRCounts(cached)) {
10863
+ debug(MODULE6, `Using cached ${label} PR counts for @${githubUsername}`);
10864
+ return {
10865
+ repos: new Map(cached.reposEntries),
10866
+ monthlyCounts: cached.monthlyCounts,
10867
+ monthlyOpenedCounts: cached.monthlyOpenedCounts,
10868
+ dailyActivityCounts: cached.dailyActivityCounts
10869
+ };
10870
+ }
10871
+ debug(MODULE6, `Fetching ${label} PR counts for @${githubUsername}...`);
10821
10872
  const repos = /* @__PURE__ */ new Map();
10822
10873
  const monthlyCounts = {};
10823
10874
  const monthlyOpenedCounts = {};
@@ -10826,7 +10877,7 @@ async function fetchUserMergedPRCounts(octokit, githubUsername) {
10826
10877
  let fetched = 0;
10827
10878
  while (true) {
10828
10879
  const { data } = await octokit.search.issuesAndPullRequests({
10829
- q: `is:pr is:merged author:${githubUsername}`,
10880
+ q: `is:pr ${query} author:${githubUsername}`,
10830
10881
  sort: "updated",
10831
10882
  order: "desc",
10832
10883
  per_page: 100,
@@ -10835,25 +10886,18 @@ async function fetchUserMergedPRCounts(octokit, githubUsername) {
10835
10886
  for (const item of data.items) {
10836
10887
  const parsed = extractOwnerRepo(item.html_url);
10837
10888
  if (!parsed) {
10838
- warn(MODULE6, `Skipping merged PR with unparseable URL: ${item.html_url}`);
10889
+ warn(MODULE6, `Skipping ${label} PR with unparseable URL: ${item.html_url}`);
10839
10890
  continue;
10840
10891
  }
10841
10892
  const { owner } = parsed;
10842
10893
  const repo = `${owner}/${parsed.repo}`;
10843
- if (owner.toLowerCase() === githubUsername.toLowerCase()) continue;
10844
- const mergedAt = item.pull_request?.merged_at || item.closed_at || "";
10845
- const existing = repos.get(repo);
10846
- if (existing) {
10847
- existing.count += 1;
10848
- if (mergedAt && mergedAt > existing.lastMergedAt) {
10849
- existing.lastMergedAt = mergedAt;
10850
- }
10851
- } else {
10852
- repos.set(repo, { count: 1, lastMergedAt: mergedAt });
10853
- }
10854
- if (mergedAt) {
10855
- const month = mergedAt.slice(0, 7);
10894
+ if (isOwnRepo(owner, githubUsername)) continue;
10895
+ const primaryDate = accumulateRepo(repos, repo, item);
10896
+ if (primaryDate) {
10897
+ const month = primaryDate.slice(0, 7);
10856
10898
  monthlyCounts[month] = (monthlyCounts[month] || 0) + 1;
10899
+ const day = primaryDate.slice(0, 10);
10900
+ if (day.length === 10) dailyActivityCounts[day] = (dailyActivityCounts[day] || 0) + 1;
10857
10901
  }
10858
10902
  if (item.created_at) {
10859
10903
  const openedMonth = item.created_at.slice(0, 7);
@@ -10861,10 +10905,6 @@ async function fetchUserMergedPRCounts(octokit, githubUsername) {
10861
10905
  const openedDay = item.created_at.slice(0, 10);
10862
10906
  if (openedDay.length === 10) dailyActivityCounts[openedDay] = (dailyActivityCounts[openedDay] || 0) + 1;
10863
10907
  }
10864
- if (mergedAt) {
10865
- const mergedDay = mergedAt.slice(0, 10);
10866
- if (mergedDay.length === 10) dailyActivityCounts[mergedDay] = (dailyActivityCounts[mergedDay] || 0) + 1;
10867
- }
10868
10908
  }
10869
10909
  fetched += data.items.length;
10870
10910
  if (fetched >= data.total_count || fetched >= 1e3 || data.items.length === 0) {
@@ -10872,59 +10912,41 @@ async function fetchUserMergedPRCounts(octokit, githubUsername) {
10872
10912
  }
10873
10913
  page++;
10874
10914
  }
10875
- debug(MODULE6, `Found ${fetched} merged PRs across ${repos.size} repos`);
10915
+ debug(MODULE6, `Found ${fetched} ${label} PRs across ${repos.size} repos`);
10916
+ cache.set(cacheKey, "", {
10917
+ reposEntries: Array.from(repos.entries()),
10918
+ monthlyCounts,
10919
+ monthlyOpenedCounts,
10920
+ dailyActivityCounts
10921
+ });
10876
10922
  return { repos, monthlyCounts, monthlyOpenedCounts, dailyActivityCounts };
10877
10923
  }
10878
- async function fetchUserClosedPRCounts(octokit, githubUsername) {
10879
- if (!githubUsername) {
10880
- return { repos: /* @__PURE__ */ new Map(), monthlyCounts: {}, monthlyOpenedCounts: {}, dailyActivityCounts: {} };
10881
- }
10882
- debug(MODULE6, `Fetching closed PR counts for @${githubUsername}...`);
10883
- const repos = /* @__PURE__ */ new Map();
10884
- const monthlyCounts = {};
10885
- const monthlyOpenedCounts = {};
10886
- const dailyActivityCounts = {};
10887
- let page = 1;
10888
- let fetched = 0;
10889
- while (true) {
10890
- const { data } = await octokit.search.issuesAndPullRequests({
10891
- q: `is:pr is:closed is:unmerged author:${githubUsername}`,
10892
- sort: "updated",
10893
- order: "desc",
10894
- per_page: 100,
10895
- page
10896
- });
10897
- for (const item of data.items) {
10898
- const parsed = extractOwnerRepo(item.html_url);
10899
- if (!parsed) {
10900
- warn(MODULE6, `Skipping closed PR with unparseable URL: ${item.html_url}`);
10901
- continue;
10902
- }
10903
- const { owner } = parsed;
10904
- const repo = `${owner}/${parsed.repo}`;
10905
- if (owner.toLowerCase() === githubUsername.toLowerCase()) continue;
10906
- repos.set(repo, (repos.get(repo) || 0) + 1);
10907
- if (item.closed_at) {
10908
- const closedMonth = item.closed_at.slice(0, 7);
10909
- monthlyCounts[closedMonth] = (monthlyCounts[closedMonth] || 0) + 1;
10910
- const closedDay = item.closed_at.slice(0, 10);
10911
- if (closedDay.length === 10) dailyActivityCounts[closedDay] = (dailyActivityCounts[closedDay] || 0) + 1;
10912
- }
10913
- if (item.created_at) {
10914
- const openedMonth = item.created_at.slice(0, 7);
10915
- monthlyOpenedCounts[openedMonth] = (monthlyOpenedCounts[openedMonth] || 0) + 1;
10916
- const openedDay = item.created_at.slice(0, 10);
10917
- if (openedDay.length === 10) dailyActivityCounts[openedDay] = (dailyActivityCounts[openedDay] || 0) + 1;
10918
- }
10924
+ function fetchUserMergedPRCounts(octokit, githubUsername) {
10925
+ return fetchUserPRCounts(octokit, githubUsername, "is:merged", "merged", (repos, repo, item) => {
10926
+ if (!item.pull_request?.merged_at) {
10927
+ warn(
10928
+ MODULE6,
10929
+ `merged_at missing for merged PR ${item.html_url}${item.closed_at ? ", falling back to closed_at" : ", no date available"}`
10930
+ );
10919
10931
  }
10920
- fetched += data.items.length;
10921
- if (fetched >= data.total_count || fetched >= 1e3 || data.items.length === 0) {
10922
- break;
10932
+ const mergedAt = item.pull_request?.merged_at || item.closed_at || "";
10933
+ const existing = repos.get(repo);
10934
+ if (existing) {
10935
+ existing.count += 1;
10936
+ if (mergedAt && mergedAt > existing.lastMergedAt) {
10937
+ existing.lastMergedAt = mergedAt;
10938
+ }
10939
+ } else {
10940
+ repos.set(repo, { count: 1, lastMergedAt: mergedAt });
10923
10941
  }
10924
- page++;
10925
- }
10926
- debug(MODULE6, `Found ${fetched} closed (unmerged) PRs across ${repos.size} repos`);
10927
- return { repos, monthlyCounts, monthlyOpenedCounts, dailyActivityCounts };
10942
+ return mergedAt;
10943
+ });
10944
+ }
10945
+ function fetchUserClosedPRCounts(octokit, githubUsername) {
10946
+ return fetchUserPRCounts(octokit, githubUsername, "is:closed is:unmerged", "closed", (repos, repo, item) => {
10947
+ repos.set(repo, (repos.get(repo) || 0) + 1);
10948
+ return item.closed_at || "";
10949
+ });
10928
10950
  }
10929
10951
  async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
10930
10952
  if (!config.githubUsername) {
@@ -10949,7 +10971,7 @@ async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
10949
10971
  continue;
10950
10972
  }
10951
10973
  const repo = `${parsed.owner}/${parsed.repo}`;
10952
- if (parsed.owner.toLowerCase() === config.githubUsername.toLowerCase()) continue;
10974
+ if (isOwnRepo(parsed.owner, config.githubUsername)) continue;
10953
10975
  if (config.excludeRepos.includes(repo)) continue;
10954
10976
  if (config.excludeOrgs?.some((org) => parsed.owner.toLowerCase() === org.toLowerCase())) continue;
10955
10977
  results.push(mapItem(item, { owner: parsed.owner, repo, number: parsed.number }));
@@ -10998,14 +11020,15 @@ async function fetchRecentlyMergedPRs(octokit, config, days = 7) {
10998
11020
  }
10999
11021
  );
11000
11022
  }
11001
- var MODULE6;
11023
+ var MODULE6, PR_COUNTS_CACHE_TTL_MS;
11002
11024
  var init_github_stats = __esm({
11003
11025
  "src/core/github-stats.ts"() {
11004
11026
  "use strict";
11005
11027
  init_utils();
11006
- init_errors();
11007
11028
  init_logger();
11029
+ init_http_cache();
11008
11030
  MODULE6 = "github-stats";
11031
+ PR_COUNTS_CACHE_TTL_MS = 60 * 60 * 1e3;
11009
11032
  }
11010
11033
  });
11011
11034
 
@@ -11106,9 +11129,9 @@ var init_pr_monitor = __esm({
11106
11129
  const pr = await this.fetchPRDetails(item.html_url);
11107
11130
  if (pr) prs.push(pr);
11108
11131
  } catch (error) {
11109
- const errorMessage = error instanceof Error ? error.message : String(error);
11110
- warn("pr-monitor", `Error fetching ${item.html_url}: ${errorMessage}`);
11111
- failures.push({ prUrl: item.html_url, error: errorMessage });
11132
+ const errMsg = errorMessage(error);
11133
+ warn("pr-monitor", `Error fetching ${item.html_url}: ${errMsg}`);
11134
+ failures.push({ prUrl: item.html_url, error: errMsg });
11112
11135
  }
11113
11136
  },
11114
11137
  MAX_CONCURRENT_REQUESTS
@@ -11155,12 +11178,12 @@ var init_pr_monitor = __esm({
11155
11178
  paginateAll(
11156
11179
  (page) => this.octokit.pulls.listReviewComments({ owner, repo, pull_number: number, per_page: 100, page })
11157
11180
  ).catch((err) => {
11158
- const status2 = err?.status;
11181
+ const status2 = getHttpStatusCode(err);
11159
11182
  if (status2 === 429) {
11160
11183
  throw err;
11161
11184
  }
11162
11185
  if (status2 === 403) {
11163
- const msg = (err?.message ?? "").toLowerCase();
11186
+ const msg = errorMessage(err).toLowerCase();
11164
11187
  if (msg.includes("rate limit") || msg.includes("abuse detection")) {
11165
11188
  throw err;
11166
11189
  }
@@ -11272,6 +11295,9 @@ var init_pr_monitor = __esm({
11272
11295
  } = input;
11273
11296
  if (hasUnrespondedComment) {
11274
11297
  if (latestCommitDate && lastMaintainerCommentDate && latestCommitDate > lastMaintainerCommentDate) {
11298
+ if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
11299
+ return "needs_response";
11300
+ }
11275
11301
  if (ciStatus === "failing") return "failing_ci";
11276
11302
  return "changes_addressed";
11277
11303
  }
@@ -11327,7 +11353,7 @@ var init_pr_monitor = __esm({
11327
11353
  this.octokit.repos.getCombinedStatusForRef({ owner, repo, ref: sha }),
11328
11354
  // 404 is expected for repos without check runs configured; log other errors for debugging
11329
11355
  this.octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
11330
- const status = err?.status;
11356
+ const status = getHttpStatusCode(err);
11331
11357
  if (status === 404) {
11332
11358
  debug("pr-monitor", `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
11333
11359
  } else {
@@ -11353,8 +11379,8 @@ var init_pr_monitor = __esm({
11353
11379
  const combinedAnalysis = analyzeCombinedStatus(combinedStatus);
11354
11380
  return mergeStatuses(checkRunAnalysis, combinedAnalysis, checkRuns.length);
11355
11381
  } catch (error) {
11356
- const statusCode = error.status;
11357
- const errorMessage = error instanceof Error ? error.message : String(error);
11382
+ const statusCode = getHttpStatusCode(error);
11383
+ const errMsg = errorMessage(error);
11358
11384
  if (statusCode === 401) {
11359
11385
  warn("pr-monitor", `CI check failed for ${owner}/${repo}: Invalid token`);
11360
11386
  } else if (statusCode === 403) {
@@ -11363,7 +11389,7 @@ var init_pr_monitor = __esm({
11363
11389
  debug("pr-monitor", `CI check 404 for ${owner}/${repo} (no CI configured)`);
11364
11390
  return { status: "unknown", failingCheckNames: [], failingCheckConclusions: /* @__PURE__ */ new Map() };
11365
11391
  } else {
11366
- warn("pr-monitor", `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage}`);
11392
+ warn("pr-monitor", `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errMsg}`);
11367
11393
  }
11368
11394
  return { status: "unknown", failingCheckNames: [], failingCheckConclusions: /* @__PURE__ */ new Map() };
11369
11395
  }
@@ -11424,10 +11450,7 @@ var init_pr_monitor = __esm({
11424
11450
  results.set(result.value.repo, result.value.stars);
11425
11451
  } else {
11426
11452
  chunkFailures++;
11427
- warn(
11428
- MODULE7,
11429
- `Failed to fetch stars for ${chunk[j]}: ${result.reason instanceof Error ? result.reason.message : result.reason}`
11430
- );
11453
+ warn(MODULE7, `Failed to fetch stars for ${chunk[j]}: ${errorMessage(result.reason)}`);
11431
11454
  }
11432
11455
  }
11433
11456
  if (chunkFailures === chunk.length && chunk.length > 0) {
@@ -11441,42 +11464,6 @@ var init_pr_monitor = __esm({
11441
11464
  debug(MODULE7, `Fetched star counts for ${results.size}/${repos.length} repos`);
11442
11465
  return results;
11443
11466
  }
11444
- /**
11445
- * Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
11446
- * Returns parsed search results that pass all filters.
11447
- */
11448
- async fetchRecentPRs(query, label, days, mapItem) {
11449
- const config = this.stateManager.getState().config;
11450
- if (!config.githubUsername) {
11451
- warn(MODULE7, `Skipping recently ${label} PRs fetch: no githubUsername configured. Run /setup-oss to configure.`);
11452
- return [];
11453
- }
11454
- const sinceDate = /* @__PURE__ */ new Date();
11455
- sinceDate.setDate(sinceDate.getDate() - days);
11456
- const since = sinceDate.toISOString().split("T")[0];
11457
- debug(MODULE7, `Fetching recently ${label} PRs for @${config.githubUsername} (since ${since})...`);
11458
- const { data } = await this.octokit.search.issuesAndPullRequests({
11459
- q: query.replace("{username}", config.githubUsername).replace("{since}", since),
11460
- sort: "updated",
11461
- order: "desc",
11462
- per_page: 100
11463
- });
11464
- const results = [];
11465
- for (const item of data.items) {
11466
- const parsed = parseGitHubUrl(item.html_url);
11467
- if (!parsed) {
11468
- warn(MODULE7, `Could not parse GitHub URL from API response: ${item.html_url}`);
11469
- continue;
11470
- }
11471
- const repo = `${parsed.owner}/${parsed.repo}`;
11472
- if (parsed.owner.toLowerCase() === config.githubUsername.toLowerCase()) continue;
11473
- if (config.excludeRepos.includes(repo)) continue;
11474
- if (config.excludeOrgs?.some((org) => parsed.owner.toLowerCase() === org.toLowerCase())) continue;
11475
- results.push(mapItem(item, { owner: parsed.owner, repo, number: parsed.number }));
11476
- }
11477
- debug(MODULE7, `Found ${results.length} recently ${label} PRs`);
11478
- return results;
11479
- }
11480
11467
  /**
11481
11468
  * Fetch PRs closed without merge in the last N days.
11482
11469
  * Delegates to github-stats module.
@@ -11901,7 +11888,7 @@ var init_issue_vetting = __esm({
11901
11888
  if (_IssueVetter.isRateLimitError(error)) {
11902
11889
  rateLimitFailures++;
11903
11890
  }
11904
- warn(MODULE8, `Error vetting issue ${url}:`, error instanceof Error ? error.message : error);
11891
+ warn(MODULE8, `Error vetting issue ${url}:`, errorMessage(error));
11905
11892
  });
11906
11893
  pending.push(task);
11907
11894
  if (pending.length >= MAX_CONCURRENT_REQUESTS2) {
@@ -11921,10 +11908,10 @@ var init_issue_vetting = __esm({
11921
11908
  }
11922
11909
  /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
11923
11910
  static isRateLimitError(error) {
11924
- const status = error?.status;
11911
+ const status = getHttpStatusCode(error);
11925
11912
  if (status === 429) return true;
11926
11913
  if (status === 403) {
11927
- const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
11914
+ const msg = errorMessage(error).toLowerCase();
11928
11915
  return msg.includes("rate limit");
11929
11916
  }
11930
11917
  return false;
@@ -11950,12 +11937,12 @@ var init_issue_vetting = __esm({
11950
11937
  });
11951
11938
  return { passed: data.total_count === 0 && linkedPRs.length === 0 };
11952
11939
  } catch (error) {
11953
- const errorMessage = error instanceof Error ? error.message : String(error);
11940
+ const errMsg = errorMessage(error);
11954
11941
  warn(
11955
11942
  MODULE8,
11956
- `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errorMessage}. Assuming no existing PR.`
11943
+ `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`
11957
11944
  );
11958
- return { passed: true, inconclusive: true, reason: errorMessage };
11945
+ return { passed: true, inconclusive: true, reason: errMsg };
11959
11946
  }
11960
11947
  }
11961
11948
  /**
@@ -11971,8 +11958,8 @@ var init_issue_vetting = __esm({
11971
11958
  });
11972
11959
  return data.total_count;
11973
11960
  } catch (error) {
11974
- const errorMessage = error instanceof Error ? error.message : String(error);
11975
- warn(MODULE8, `Could not check merged PRs in ${owner}/${repo}: ${errorMessage}. Defaulting to 0.`);
11961
+ const errMsg = errorMessage(error);
11962
+ warn(MODULE8, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
11976
11963
  return 0;
11977
11964
  }
11978
11965
  }
@@ -12015,12 +12002,9 @@ var init_issue_vetting = __esm({
12015
12002
  }
12016
12003
  return { passed: true };
12017
12004
  } catch (error) {
12018
- const errorMessage = error instanceof Error ? error.message : String(error);
12019
- warn(
12020
- MODULE8,
12021
- `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errorMessage}. Assuming not claimed.`
12022
- );
12023
- return { passed: true, inconclusive: true, reason: errorMessage };
12005
+ const errMsg = errorMessage(error);
12006
+ warn(MODULE8, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
12007
+ return { passed: true, inconclusive: true, reason: errMsg };
12024
12008
  }
12025
12009
  }
12026
12010
  async checkProjectHealth(owner, repo) {
@@ -12051,8 +12035,8 @@ var init_issue_vetting = __esm({
12051
12035
  ciStatus = "passing";
12052
12036
  }
12053
12037
  } catch (error) {
12054
- const errorMessage = error instanceof Error ? error.message : String(error);
12055
- warn(MODULE8, `Failed to check CI status for ${owner}/${repo}: ${errorMessage}. Defaulting to unknown.`);
12038
+ const errMsg = errorMessage(error);
12039
+ warn(MODULE8, `Failed to check CI status for ${owner}/${repo}: ${errMsg}. Defaulting to unknown.`);
12056
12040
  }
12057
12041
  return {
12058
12042
  repo: `${owner}/${repo}`,
@@ -12067,8 +12051,8 @@ var init_issue_vetting = __esm({
12067
12051
  forksCount: repoData.forks_count
12068
12052
  };
12069
12053
  } catch (error) {
12070
- const errorMessage = error instanceof Error ? error.message : String(error);
12071
- warn(MODULE8, `Error checking project health for ${owner}/${repo}: ${errorMessage}`);
12054
+ const errMsg = errorMessage(error);
12055
+ warn(MODULE8, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
12072
12056
  return {
12073
12057
  repo: `${owner}/${repo}`,
12074
12058
  lastCommitAt: "",
@@ -12078,7 +12062,7 @@ var init_issue_vetting = __esm({
12078
12062
  ciStatus: "unknown",
12079
12063
  isActive: false,
12080
12064
  checkFailed: true,
12081
- failureReason: errorMessage
12065
+ failureReason: errMsg
12082
12066
  };
12083
12067
  }
12084
12068
  }
@@ -12201,7 +12185,7 @@ var init_issue_discovery = __esm({
12201
12185
  * Updates the state manager with the list and timestamp.
12202
12186
  */
12203
12187
  async fetchStarredRepos() {
12204
- console.log("Fetching starred repositories...");
12188
+ info(MODULE9, "Fetching starred repositories...");
12205
12189
  const starredRepos = [];
12206
12190
  try {
12207
12191
  const iterator2 = this.octokit.paginate.iterator(this.octokit.activity.listReposStarredByAuthenticatedUser, {
@@ -12222,27 +12206,27 @@ var init_issue_discovery = __esm({
12222
12206
  }
12223
12207
  pageCount++;
12224
12208
  if (pageCount >= 5) {
12225
- console.log("Reached pagination limit for starred repos (500)");
12209
+ info(MODULE9, "Reached pagination limit for starred repos (500)");
12226
12210
  break;
12227
12211
  }
12228
12212
  }
12229
- console.log(`Fetched ${starredRepos.length} starred repositories`);
12213
+ info(MODULE9, `Fetched ${starredRepos.length} starred repositories`);
12230
12214
  this.stateManager.setStarredRepos(starredRepos);
12231
12215
  return starredRepos;
12232
12216
  } catch (error) {
12233
12217
  const cachedRepos = this.stateManager.getStarredRepos();
12234
- const errorMessage = error instanceof Error ? error.message : String(error);
12235
- warn(MODULE9, "Error fetching starred repos:", errorMessage);
12218
+ const errMsg = errorMessage(error);
12219
+ warn(MODULE9, "Error fetching starred repos:", errMsg);
12236
12220
  if (cachedRepos.length === 0) {
12237
12221
  warn(
12238
12222
  MODULE9,
12239
- `Failed to fetch starred repositories from GitHub API. No cached repos available. Error: ${errorMessage}
12223
+ `Failed to fetch starred repositories from GitHub API. No cached repos available. Error: ${errMsg}
12240
12224
  Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12241
12225
  );
12242
12226
  } else {
12243
12227
  warn(
12244
12228
  MODULE9,
12245
- `Failed to fetch starred repositories from GitHub API. Using ${cachedRepos.length} cached repos instead. Error: ${errorMessage}`
12229
+ `Failed to fetch starred repositories from GitHub API. Using ${cachedRepos.length} cached repos instead. Error: ${errMsg}`
12246
12230
  );
12247
12231
  }
12248
12232
  return cachedRepos;
@@ -12257,6 +12241,48 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12257
12241
  }
12258
12242
  return this.stateManager.getStarredRepos();
12259
12243
  }
12244
+ /**
12245
+ * Shared pipeline for Phases 2 and 3: spam-filter, repo-exclusion, vetting, and star-count filter.
12246
+ * Extracts the common logic so each phase only needs to supply search results and context.
12247
+ */
12248
+ async filterVetAndScore(items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
12249
+ const spamRepos = detectLabelFarmingRepos(items);
12250
+ if (spamRepos.size > 0) {
12251
+ const spamCount = items.filter((i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))).length;
12252
+ debug(
12253
+ MODULE9,
12254
+ `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`
12255
+ );
12256
+ }
12257
+ const itemsToVet = filterIssues(items).filter((item) => {
12258
+ const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12259
+ if (spamRepos.has(repoFullName)) return false;
12260
+ return excludedRepoSets.every((s) => !s.has(repoFullName));
12261
+ }).slice(0, remainingNeeded * 2);
12262
+ if (itemsToVet.length === 0) {
12263
+ debug(MODULE9, `[${phaseLabel}] All ${items.length} items filtered before vetting`);
12264
+ return { candidates: [], allVetFailed: false, rateLimitHit: false };
12265
+ }
12266
+ const {
12267
+ candidates: results,
12268
+ allFailed: allVetFailed,
12269
+ rateLimitHit
12270
+ } = await this.vetter.vetIssuesParallel(
12271
+ itemsToVet.map((i) => i.html_url),
12272
+ remainingNeeded,
12273
+ "normal"
12274
+ );
12275
+ const starFiltered = results.filter((c) => {
12276
+ if (c.projectHealth.checkFailed) return true;
12277
+ const stars = c.projectHealth.stargazersCount ?? 0;
12278
+ return stars >= minStars;
12279
+ });
12280
+ const starFilteredCount = results.length - starFiltered.length;
12281
+ if (starFilteredCount > 0) {
12282
+ debug(MODULE9, `[STAR_FILTER] Filtered ${starFilteredCount} ${phaseLabel} candidates below ${minStars} stars`);
12283
+ }
12284
+ return { candidates: starFiltered, allVetFailed, rateLimitHit };
12285
+ }
12260
12286
  /**
12261
12287
  * Search for issues matching our criteria.
12262
12288
  * Searches in priority order: merged-PR repos first (no label filter), then starred repos,
@@ -12268,6 +12294,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12268
12294
  const languages = options.languages || config.languages;
12269
12295
  const labels = options.labels || config.labels;
12270
12296
  const maxResults = options.maxResults || 10;
12297
+ const minStars = config.minStars ?? 50;
12271
12298
  const allCandidates = [];
12272
12299
  let phase0Error = null;
12273
12300
  let phase1Error = null;
@@ -12281,10 +12308,10 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12281
12308
  warn(MODULE9, this.rateLimitWarning);
12282
12309
  }
12283
12310
  } catch (error) {
12284
- if (error?.status === 401) {
12311
+ if (getHttpStatusCode(error) === 401) {
12285
12312
  throw error;
12286
12313
  }
12287
- warn(MODULE9, "Could not check rate limit:", error instanceof Error ? error.message : error);
12314
+ warn(MODULE9, "Could not check rate limit:", errorMessage(error));
12288
12315
  }
12289
12316
  const mergedPRRepos = this.stateManager.getReposWithMergedPRs();
12290
12317
  const mergedPRRepoSet = new Set(mergedPRRepos);
@@ -12304,7 +12331,8 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12304
12331
  const includeDocIssues = config.includeDocIssues ?? true;
12305
12332
  const aiBlocklisted = new Set(config.aiPolicyBlocklist ?? DEFAULT_CONFIG.aiPolicyBlocklist ?? []);
12306
12333
  if (aiBlocklisted.size > 0) {
12307
- console.log(
12334
+ debug(
12335
+ MODULE9,
12308
12336
  `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`
12309
12337
  );
12310
12338
  }
@@ -12327,7 +12355,8 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12327
12355
  if (phase0Repos.length > 0) {
12328
12356
  const mergedInPhase0 = Math.min(mergedPRRepos.length, phase0Repos.length);
12329
12357
  const openInPhase0 = phase0Repos.length - mergedInPhase0;
12330
- console.log(
12358
+ info(
12359
+ MODULE9,
12331
12360
  `Phase 0: Searching issues in ${phase0Repos.length} repos (${mergedInPhase0} merged-PR, ${openInPhase0} open-PR, no label filter)...`
12332
12361
  );
12333
12362
  const mergedPhase0Repos = phase0Repos.slice(0, mergedInPhase0);
@@ -12346,7 +12375,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12346
12375
  if (rateLimitHit) {
12347
12376
  rateLimitHitDuringSearch = true;
12348
12377
  }
12349
- console.log(`Found ${mergedCandidates.length} candidates from merged-PR repos`);
12378
+ info(MODULE9, `Found ${mergedCandidates.length} candidates from merged-PR repos`);
12350
12379
  }
12351
12380
  }
12352
12381
  const openPhase0Repos = phase0Repos.slice(mergedInPhase0);
@@ -12366,14 +12395,14 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12366
12395
  if (rateLimitHit) {
12367
12396
  rateLimitHitDuringSearch = true;
12368
12397
  }
12369
- console.log(`Found ${openCandidates.length} candidates from open-PR repos`);
12398
+ info(MODULE9, `Found ${openCandidates.length} candidates from open-PR repos`);
12370
12399
  }
12371
12400
  }
12372
12401
  }
12373
12402
  if (allCandidates.length < maxResults && starredRepos.length > 0) {
12374
12403
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
12375
12404
  if (reposToSearch.length > 0) {
12376
- console.log(`Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
12405
+ info(MODULE9, `Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
12377
12406
  const remainingNeeded = maxResults - allCandidates.length;
12378
12407
  if (remainingNeeded > 0) {
12379
12408
  const {
@@ -12388,13 +12417,13 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12388
12417
  if (rateLimitHit) {
12389
12418
  rateLimitHitDuringSearch = true;
12390
12419
  }
12391
- console.log(`Found ${starredCandidates.length} candidates from starred repos`);
12420
+ info(MODULE9, `Found ${starredCandidates.length} candidates from starred repos`);
12392
12421
  }
12393
12422
  }
12394
12423
  }
12395
12424
  let phase2Error = null;
12396
12425
  if (allCandidates.length < maxResults) {
12397
- console.log("Phase 2: General issue search...");
12426
+ info(MODULE9, "Phase 2: General issue search...");
12398
12427
  const remainingNeeded = maxResults - allCandidates.length;
12399
12428
  try {
12400
12429
  const { data } = await this.octokit.search.issuesAndPullRequests({
@@ -12404,43 +12433,20 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12404
12433
  per_page: remainingNeeded * 3
12405
12434
  // Fetch extra since some will be filtered
12406
12435
  });
12407
- console.log(`Found ${data.total_count} issues in general search, processing top ${data.items.length}...`);
12408
- const spamRepos = detectLabelFarmingRepos(data.items);
12409
- if (spamRepos.size > 0) {
12410
- const spamCount = data.items.filter(
12411
- (i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))
12412
- ).length;
12413
- console.log(
12414
- `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`
12415
- );
12416
- }
12436
+ info(MODULE9, `Found ${data.total_count} issues in general search, processing top ${data.items.length}...`);
12417
12437
  const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
12418
- const itemsToVet = filterIssues(data.items).filter((item) => {
12419
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12420
- return !spamRepos.has(repoFullName);
12421
- }).filter((item) => {
12422
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12423
- return !phase0RepoSet.has(repoFullName) && !starredRepoSet.has(repoFullName) && !seenRepos.has(repoFullName);
12424
- }).slice(0, remainingNeeded * 2);
12425
12438
  const {
12426
- candidates: results,
12427
- allFailed: allVetFailed,
12439
+ candidates: starFiltered,
12440
+ allVetFailed,
12428
12441
  rateLimitHit: vetRateLimitHit
12429
- } = await this.vetter.vetIssuesParallel(
12430
- itemsToVet.map((i) => i.html_url),
12442
+ } = await this.filterVetAndScore(
12443
+ data.items,
12444
+ filterIssues,
12445
+ [phase0RepoSet, starredRepoSet, seenRepos],
12431
12446
  remainingNeeded,
12432
- "normal"
12447
+ minStars,
12448
+ "Phase 2"
12433
12449
  );
12434
- const minStars = config.minStars ?? 50;
12435
- const starFiltered = results.filter((c) => {
12436
- if (c.projectHealth.checkFailed) return true;
12437
- const stars = c.projectHealth.stargazersCount ?? 0;
12438
- return stars >= minStars;
12439
- });
12440
- const starFilteredCount = results.length - starFiltered.length;
12441
- if (starFilteredCount > 0) {
12442
- console.log(`[STAR_FILTER] Filtered ${starFilteredCount} candidates below ${minStars} stars`);
12443
- }
12444
12450
  allCandidates.push(...starFiltered);
12445
12451
  if (allVetFailed) {
12446
12452
  phase2Error = (phase2Error ? phase2Error + "; " : "") + "all vetting failed";
@@ -12448,25 +12454,24 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12448
12454
  if (vetRateLimitHit) {
12449
12455
  rateLimitHitDuringSearch = true;
12450
12456
  }
12451
- console.log(`Found ${starFiltered.length} candidates from general search`);
12457
+ info(MODULE9, `Found ${starFiltered.length} candidates from general search`);
12452
12458
  } catch (error) {
12453
- const errorMessage = error instanceof Error ? error.message : String(error);
12454
- phase2Error = errorMessage;
12459
+ const errMsg = errorMessage(error);
12460
+ phase2Error = errMsg;
12455
12461
  if (IssueVetter.isRateLimitError(error)) {
12456
12462
  rateLimitHitDuringSearch = true;
12457
12463
  }
12458
- warn(MODULE9, `Error in general issue search: ${errorMessage}`);
12464
+ warn(MODULE9, `Error in general issue search: ${errMsg}`);
12459
12465
  }
12460
12466
  }
12461
12467
  let phase3Error = null;
12462
12468
  if (allCandidates.length < maxResults) {
12463
- console.log("Phase 3: Searching actively maintained repos...");
12469
+ info(MODULE9, "Phase 3: Searching actively maintained repos...");
12464
12470
  const remainingNeeded = maxResults - allCandidates.length;
12465
12471
  const thirtyDaysAgo = /* @__PURE__ */ new Date();
12466
12472
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
12467
12473
  const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
12468
- const phase3MinStars = config.minStars ?? 50;
12469
- const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${phase3MinStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
12474
+ const phase3Query = `is:issue is:open no:assignee ${langQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
12470
12475
  try {
12471
12476
  const { data } = await this.octokit.search.issuesAndPullRequests({
12472
12477
  q: phase3Query,
@@ -12474,42 +12479,23 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12474
12479
  order: "desc",
12475
12480
  per_page: remainingNeeded * 3
12476
12481
  });
12477
- console.log(
12482
+ info(
12483
+ MODULE9,
12478
12484
  `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`
12479
12485
  );
12480
- const spamRepos = detectLabelFarmingRepos(data.items);
12481
- if (spamRepos.size > 0) {
12482
- const spamCount = data.items.filter(
12483
- (i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))
12484
- ).length;
12485
- console.log(
12486
- `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`
12487
- );
12488
- }
12489
12486
  const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
12490
- const itemsToVet = filterIssues(data.items).filter((item) => {
12491
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
12492
- return !spamRepos.has(repoFullName) && !phase0RepoSet.has(repoFullName) && !starredRepoSet.has(repoFullName) && !seenRepos.has(repoFullName);
12493
- }).slice(0, remainingNeeded * 2);
12494
12487
  const {
12495
- candidates: results,
12496
- allFailed: allVetFailed,
12488
+ candidates: starFiltered,
12489
+ allVetFailed,
12497
12490
  rateLimitHit: vetRateLimitHit
12498
- } = await this.vetter.vetIssuesParallel(
12499
- itemsToVet.map((i) => i.html_url),
12491
+ } = await this.filterVetAndScore(
12492
+ data.items,
12493
+ filterIssues,
12494
+ [phase0RepoSet, starredRepoSet, seenRepos],
12500
12495
  remainingNeeded,
12501
- "normal"
12496
+ minStars,
12497
+ "Phase 3"
12502
12498
  );
12503
- const minStars = config.minStars ?? 50;
12504
- const starFiltered = results.filter((c) => {
12505
- if (c.projectHealth.checkFailed) return true;
12506
- const stars = c.projectHealth.stargazersCount ?? 0;
12507
- return stars >= minStars;
12508
- });
12509
- const starFilteredCount = results.length - starFiltered.length;
12510
- if (starFilteredCount > 0) {
12511
- console.log(`[STAR_FILTER] Filtered ${starFilteredCount} Phase 3 candidates below ${minStars} stars`);
12512
- }
12513
12499
  allCandidates.push(...starFiltered);
12514
12500
  if (allVetFailed) {
12515
12501
  phase3Error = "all vetting failed";
@@ -12517,14 +12503,14 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12517
12503
  if (vetRateLimitHit) {
12518
12504
  rateLimitHitDuringSearch = true;
12519
12505
  }
12520
- console.log(`Found ${starFiltered.length} candidates from maintained-repo search`);
12506
+ info(MODULE9, `Found ${starFiltered.length} candidates from maintained-repo search`);
12521
12507
  } catch (error) {
12522
- const errorMessage = error instanceof Error ? error.message : String(error);
12523
- phase3Error = errorMessage;
12508
+ const errMsg = errorMessage(error);
12509
+ phase3Error = errMsg;
12524
12510
  if (IssueVetter.isRateLimitError(error)) {
12525
12511
  rateLimitHitDuringSearch = true;
12526
12512
  }
12527
- warn(MODULE9, `Error in maintained-repo search: ${errorMessage}`);
12513
+ warn(MODULE9, `Error in maintained-repo search: ${errMsg}`);
12528
12514
  }
12529
12515
  }
12530
12516
  if (allCandidates.length === 0) {
@@ -12600,11 +12586,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12600
12586
  rateLimitFailures++;
12601
12587
  }
12602
12588
  const batchRepos = batch.join(", ");
12603
- warn(
12604
- MODULE9,
12605
- `Error searching issues in batch [${batchRepos}]:`,
12606
- error instanceof Error ? error.message : error
12607
- );
12589
+ warn(MODULE9, `Error searching issues in batch [${batchRepos}]:`, errorMessage(error));
12608
12590
  }
12609
12591
  }
12610
12592
  const allBatchesFailed = failedBatches === batches.length && batches.length > 0;
@@ -12692,7 +12674,7 @@ Tip: Ensure your GITHUB_TOKEN has the 'read:user' scope and try again.`
12692
12674
  content += `- **Recommendation**: Y = approve, N = skip, ? = needs_review
12693
12675
  `;
12694
12676
  fs4.writeFileSync(outputFile, content, "utf-8");
12695
- console.log(`Saved ${sorted.length} issues to ${outputFile}`);
12677
+ info(MODULE9, `Saved ${sorted.length} issues to ${outputFile}`);
12696
12678
  return outputFile;
12697
12679
  }
12698
12680
  /**
@@ -12798,7 +12780,7 @@ var init_issue_conversation = __esm({
12798
12780
  }
12799
12781
  const { owner, repo } = parsed;
12800
12782
  const repoFullName = `${owner}/${repo}`;
12801
- if (owner.toLowerCase() === username.toLowerCase()) continue;
12783
+ if (isOwnRepo(owner, username)) continue;
12802
12784
  if (item.user?.login?.toLowerCase() === username.toLowerCase()) continue;
12803
12785
  if (config.excludeRepos.includes(repoFullName)) continue;
12804
12786
  if (config.excludeOrgs?.some((org) => owner.toLowerCase() === org.toLowerCase())) continue;
@@ -12823,7 +12805,7 @@ var init_issue_conversation = __esm({
12823
12805
  });
12824
12806
  }
12825
12807
  } catch (error) {
12826
- const msg = error instanceof Error ? error.message : String(error);
12808
+ const msg = errorMessage(error);
12827
12809
  failures.push({ issueUrl: item.html_url, error: msg });
12828
12810
  warn(MODULE10, `Error analyzing issue ${item.html_url}: ${msg}`);
12829
12811
  }
@@ -13377,6 +13359,7 @@ var init_core = __esm({
13377
13359
  init_comment_utils();
13378
13360
  init_github();
13379
13361
  init_utils();
13362
+ init_errors();
13380
13363
  init_logger();
13381
13364
  init_http_cache();
13382
13365
  init_daily_logic();
@@ -13478,15 +13461,15 @@ async function fetchPRData(prMonitor, token) {
13478
13461
  prMonitor.fetchUserMergedPRCounts(),
13479
13462
  prMonitor.fetchUserClosedPRCounts(),
13480
13463
  prMonitor.fetchRecentlyClosedPRs().catch((err) => {
13481
- console.error(`Warning: Failed to fetch recently closed PRs: ${err instanceof Error ? err.message : err}`);
13464
+ console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
13482
13465
  return [];
13483
13466
  }),
13484
13467
  prMonitor.fetchRecentlyMergedPRs().catch((err) => {
13485
- console.error(`Warning: Failed to fetch recently merged PRs: ${err instanceof Error ? err.message : err}`);
13468
+ console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
13486
13469
  return [];
13487
13470
  }),
13488
13471
  issueMonitor.fetchCommentedIssues().catch((error) => {
13489
- const msg = error instanceof Error ? error.message : String(error);
13472
+ const msg = errorMessage(error);
13490
13473
  if (msg.includes("No GitHub username configured")) {
13491
13474
  console.error(`[DAILY] Issue conversation tracking requires setup: ${msg}`);
13492
13475
  } else {
@@ -13543,10 +13526,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
13543
13526
  stateManager2.updateRepoScore(repo, { mergedPRCount: count, lastMergedAt: lastMergedAt || void 0 });
13544
13527
  } catch (error) {
13545
13528
  mergedCountFailures++;
13546
- console.error(
13547
- `[DAILY] Failed to update merged count for ${repo}:`,
13548
- error instanceof Error ? error.message : error
13549
- );
13529
+ console.error(`[DAILY] Failed to update merged count for ${repo}:`, errorMessage(error));
13550
13530
  }
13551
13531
  }
13552
13532
  if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
@@ -13566,10 +13546,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
13566
13546
  stateManager2.updateRepoScore(repo, { closedWithoutMergeCount: count });
13567
13547
  } catch (error) {
13568
13548
  closedCountFailures++;
13569
- console.error(
13570
- `[DAILY] Failed to update closed count for ${repo}:`,
13571
- error instanceof Error ? error.message : error
13572
- );
13549
+ console.error(`[DAILY] Failed to update closed count for ${repo}:`, errorMessage(error));
13573
13550
  }
13574
13551
  }
13575
13552
  if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
@@ -13582,7 +13559,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
13582
13559
  stateManager2.updateRepoScore(repo, { signals });
13583
13560
  } catch (error) {
13584
13561
  signalUpdateFailures++;
13585
- console.error(`[DAILY] Failed to update signals for ${repo}:`, error instanceof Error ? error.message : error);
13562
+ console.error(`[DAILY] Failed to update signals for ${repo}:`, errorMessage(error));
13586
13563
  }
13587
13564
  }
13588
13565
  if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
@@ -13595,7 +13572,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
13595
13572
  try {
13596
13573
  starCounts = await prMonitor.fetchRepoStarCounts(allRepos);
13597
13574
  } catch (error) {
13598
- console.error("[DAILY] Failed to fetch repo star counts:", error instanceof Error ? error.message : error);
13575
+ console.error("[DAILY] Failed to fetch repo star counts:", errorMessage(error));
13599
13576
  console.error(
13600
13577
  "[DAILY] Dashboard minStars filter will use cached star counts (or be skipped for repos without cached data)."
13601
13578
  );
@@ -13607,7 +13584,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
13607
13584
  stateManager2.updateRepoScore(repo, { stargazersCount: stars });
13608
13585
  } catch (error) {
13609
13586
  starUpdateFailures++;
13610
- console.error(`[DAILY] Failed to update star count for ${repo}:`, error instanceof Error ? error.message : error);
13587
+ console.error(`[DAILY] Failed to update star count for ${repo}:`, errorMessage(error));
13611
13588
  }
13612
13589
  }
13613
13590
  if (starUpdateFailures === starCounts.size && starCounts.size > 0) {
@@ -13619,7 +13596,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
13619
13596
  stateManager2.addTrustedProject(repo);
13620
13597
  } catch (error) {
13621
13598
  trustSyncFailures++;
13622
- console.error(`[DAILY] Failed to sync trusted project ${repo}:`, error instanceof Error ? error.message : error);
13599
+ console.error(`[DAILY] Failed to sync trusted project ${repo}:`, errorMessage(error));
13623
13600
  }
13624
13601
  }
13625
13602
  if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
@@ -13633,12 +13610,12 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
13633
13610
  try {
13634
13611
  stateManager2.setMonthlyMergedCounts(monthlyCounts);
13635
13612
  } catch (error) {
13636
- console.error("[DAILY] Failed to store monthly merged counts:", error instanceof Error ? error.message : error);
13613
+ console.error("[DAILY] Failed to store monthly merged counts:", errorMessage(error));
13637
13614
  }
13638
13615
  try {
13639
13616
  stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
13640
13617
  } catch (error) {
13641
- console.error("[DAILY] Failed to store monthly closed counts:", error instanceof Error ? error.message : error);
13618
+ console.error("[DAILY] Failed to store monthly closed counts:", errorMessage(error));
13642
13619
  }
13643
13620
  try {
13644
13621
  const combinedOpenedCounts = { ...openedFromMerged };
@@ -13653,10 +13630,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
13653
13630
  }
13654
13631
  stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
13655
13632
  } catch (error) {
13656
- console.error(
13657
- "[DAILY] Failed to compute/store monthly opened counts:",
13658
- error instanceof Error ? error.message : error
13659
- );
13633
+ console.error("[DAILY] Failed to compute/store monthly opened counts:", errorMessage(error));
13660
13634
  }
13661
13635
  }
13662
13636
  function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
@@ -13671,7 +13645,7 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
13671
13645
  stateManager2.save();
13672
13646
  }
13673
13647
  } catch (error) {
13674
- console.error("[DAILY] Failed to expire/persist snoozes:", error instanceof Error ? error.message : error);
13648
+ console.error("[DAILY] Failed to expire/persist snoozes:", errorMessage(error));
13675
13649
  }
13676
13650
  const shelvedPRs = [];
13677
13651
  const autoUnshelvedPRs = [];
@@ -13733,14 +13707,15 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
13733
13707
  const snoozedUrls = new Set(
13734
13708
  Object.keys(stateManager2.getState().config.snoozedPRs ?? {}).filter((url) => stateManager2.isSnoozed(url))
13735
13709
  );
13736
- const actionableIssues = collectActionableIssues(activePRs, snoozedUrls);
13710
+ const dismissedUrls = new Set(Object.keys(stateManager2.getState().config.dismissedIssues ?? {}));
13711
+ const nonDismissedPRs = activePRs.filter((pr) => !dismissedUrls.has(pr.url));
13712
+ const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
13737
13713
  digest.summary.totalNeedingAttention = actionableIssues.length;
13738
13714
  const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
13739
13715
  const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
13740
13716
  const repoGroups = groupPRsByRepo(activePRs);
13741
13717
  return {
13742
13718
  digest,
13743
- updates: [],
13744
13719
  capacity,
13745
13720
  summary,
13746
13721
  briefSummary,
@@ -13754,7 +13729,6 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
13754
13729
  function toDailyOutput(result) {
13755
13730
  return {
13756
13731
  digest: deduplicateDigest(result.digest),
13757
- updates: result.updates,
13758
13732
  capacity: result.capacity,
13759
13733
  summary: result.summary,
13760
13734
  briefSummary: result.briefSummary,
@@ -13801,6 +13775,7 @@ var init_daily = __esm({
13801
13775
  "src/commands/daily.ts"() {
13802
13776
  "use strict";
13803
13777
  init_core();
13778
+ init_errors();
13804
13779
  init_json();
13805
13780
  init_core();
13806
13781
  }
@@ -13890,18 +13865,22 @@ var init_search = __esm({
13890
13865
  // src/commands/validation.ts
13891
13866
  function validateGitHubUrl(url, pattern, entityType) {
13892
13867
  if (pattern.test(url)) return;
13893
- const example = entityType === "PR" ? "https://github.com/owner/repo/pull/123" : "https://github.com/owner/repo/issues/123";
13894
- throw new Error(`Invalid ${entityType} URL: ${url}. Expected format: ${example}`);
13868
+ const examples = {
13869
+ PR: "https://github.com/owner/repo/pull/123",
13870
+ issue: "https://github.com/owner/repo/issues/123",
13871
+ "issue or PR": "https://github.com/owner/repo/issues/123 or https://github.com/owner/repo/pull/123"
13872
+ };
13873
+ throw new ValidationError(`Invalid ${entityType} URL: ${url}. Expected format: ${examples[entityType]}`);
13895
13874
  }
13896
13875
  function validateUrl(url) {
13897
13876
  if (url.length > MAX_URL_LENGTH) {
13898
- throw new Error(`URL exceeds maximum length of ${MAX_URL_LENGTH} characters`);
13877
+ throw new ValidationError(`URL exceeds maximum length of ${MAX_URL_LENGTH} characters`);
13899
13878
  }
13900
13879
  return url;
13901
13880
  }
13902
13881
  function validateMessage(message) {
13903
13882
  if (message.length > MAX_MESSAGE_LENGTH) {
13904
- throw new Error(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
13883
+ throw new ValidationError(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
13905
13884
  }
13906
13885
  return message;
13907
13886
  }
@@ -13929,13 +13908,13 @@ function validateGitHubUsername(username) {
13929
13908
  }
13930
13909
  return trimmed;
13931
13910
  }
13932
- var PR_URL_PATTERN, ISSUE_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
13911
+ var PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
13933
13912
  var init_validation = __esm({
13934
13913
  "src/commands/validation.ts"() {
13935
13914
  "use strict";
13936
13915
  init_errors();
13937
13916
  PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
13938
- ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
13917
+ ISSUE_OR_PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
13939
13918
  MAX_URL_LENGTH = 2048;
13940
13919
  MAX_MESSAGE_LENGTH = 1e3;
13941
13920
  MAX_USERNAME_LENGTH = 39;
@@ -14063,33 +14042,35 @@ async function runComments(options) {
14063
14042
  }
14064
14043
  const { owner, repo, number: pull_number } = parsed;
14065
14044
  const { data: pr } = await octokit.pulls.get({ owner, repo, pull_number });
14066
- const reviewComments = await paginateAll(
14067
- (page) => octokit.pulls.listReviewComments({
14068
- owner,
14069
- repo,
14070
- pull_number,
14071
- per_page: 100,
14072
- page
14073
- })
14074
- );
14075
- const issueComments = await paginateAll(
14076
- (page) => octokit.issues.listComments({
14077
- owner,
14078
- repo,
14079
- issue_number: pull_number,
14080
- per_page: 100,
14081
- page
14082
- })
14083
- );
14084
- const reviews = await paginateAll(
14085
- (page) => octokit.pulls.listReviews({
14086
- owner,
14087
- repo,
14088
- pull_number,
14089
- per_page: 100,
14090
- page
14091
- })
14092
- );
14045
+ const [reviewComments, issueComments, reviews] = await Promise.all([
14046
+ paginateAll(
14047
+ (page) => octokit.pulls.listReviewComments({
14048
+ owner,
14049
+ repo,
14050
+ pull_number,
14051
+ per_page: 100,
14052
+ page
14053
+ })
14054
+ ),
14055
+ paginateAll(
14056
+ (page) => octokit.issues.listComments({
14057
+ owner,
14058
+ repo,
14059
+ issue_number: pull_number,
14060
+ per_page: 100,
14061
+ page
14062
+ })
14063
+ ),
14064
+ paginateAll(
14065
+ (page) => octokit.pulls.listReviews({
14066
+ owner,
14067
+ repo,
14068
+ pull_number,
14069
+ per_page: 100,
14070
+ page
14071
+ })
14072
+ )
14073
+ ]);
14093
14074
  const username = stateManager2.getState().config.githubUsername;
14094
14075
  const filterComment = (c) => {
14095
14076
  if (!c.user) return false;
@@ -14487,17 +14468,17 @@ async function fetchDashboardData(token) {
14487
14468
  const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues] = await Promise.all([
14488
14469
  prMonitor.fetchUserOpenPRs(),
14489
14470
  prMonitor.fetchRecentlyClosedPRs().catch((err) => {
14490
- console.error(`Warning: Failed to fetch recently closed PRs: ${err instanceof Error ? err.message : err}`);
14471
+ console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
14491
14472
  return [];
14492
14473
  }),
14493
14474
  prMonitor.fetchRecentlyMergedPRs().catch((err) => {
14494
- console.error(`Warning: Failed to fetch recently merged PRs: ${err instanceof Error ? err.message : err}`);
14475
+ console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
14495
14476
  return [];
14496
14477
  }),
14497
14478
  prMonitor.fetchUserMergedPRCounts(),
14498
14479
  prMonitor.fetchUserClosedPRCounts(),
14499
14480
  issueMonitor.fetchCommentedIssues().catch((error) => {
14500
- const msg = error instanceof Error ? error.message : String(error);
14481
+ const msg = errorMessage(error);
14501
14482
  if (msg.includes("No GitHub username configured")) {
14502
14483
  console.error(`[DASHBOARD] Issue conversation tracking requires setup: ${msg}`);
14503
14484
  } else {
@@ -14521,12 +14502,12 @@ async function fetchDashboardData(token) {
14521
14502
  try {
14522
14503
  stateManager2.setMonthlyMergedCounts(monthlyCounts);
14523
14504
  } catch (error) {
14524
- console.error("[DASHBOARD] Failed to store monthly merged counts:", error instanceof Error ? error.message : error);
14505
+ console.error("[DASHBOARD] Failed to store monthly merged counts:", errorMessage(error));
14525
14506
  }
14526
14507
  try {
14527
14508
  stateManager2.setMonthlyClosedCounts(monthlyClosedCounts);
14528
14509
  } catch (error) {
14529
- console.error("[DASHBOARD] Failed to store monthly closed counts:", error instanceof Error ? error.message : error);
14510
+ console.error("[DASHBOARD] Failed to store monthly closed counts:", errorMessage(error));
14530
14511
  }
14531
14512
  try {
14532
14513
  const combinedOpenedCounts = { ...openedFromMerged };
@@ -14541,7 +14522,7 @@ async function fetchDashboardData(token) {
14541
14522
  }
14542
14523
  stateManager2.setMonthlyOpenedCounts(combinedOpenedCounts);
14543
14524
  } catch (error) {
14544
- console.error("[DASHBOARD] Failed to store monthly opened counts:", error instanceof Error ? error.message : error);
14525
+ console.error("[DASHBOARD] Failed to store monthly opened counts:", errorMessage(error));
14545
14526
  }
14546
14527
  const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
14547
14528
  const shelvedUrls = new Set(stateManager2.getState().config.shelvedPRUrls || []);
@@ -14581,11 +14562,12 @@ var init_dashboard_data = __esm({
14581
14562
  "src/commands/dashboard-data.ts"() {
14582
14563
  "use strict";
14583
14564
  init_core();
14565
+ init_errors();
14584
14566
  init_daily();
14585
14567
  }
14586
14568
  });
14587
14569
 
14588
- // src/commands/dashboard-templates.ts
14570
+ // src/commands/dashboard-formatters.ts
14589
14571
  function escapeHtml(text) {
14590
14572
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
14591
14573
  }
@@ -14604,78 +14586,18 @@ function buildDashboardStats(digest, state) {
14604
14586
  mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`
14605
14587
  };
14606
14588
  }
14607
- function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
14608
- const approachingDormantDays = state.config?.approachingDormantDays ?? 25;
14609
- const shelvedPRs = digest.shelvedPRs || [];
14610
- const autoUnshelvedPRs = digest.autoUnshelvedPRs || [];
14611
- const recentlyMerged = digest.recentlyMergedPRs || [];
14612
- const shelvedUrls = new Set(shelvedPRs.map((pr) => pr.url));
14613
- const activePRList = (digest.openPRs || []).filter((pr) => !shelvedUrls.has(pr.url));
14614
- const actionRequired = [
14615
- ...digest.prsNeedingResponse || [],
14616
- ...digest.needsChangesPRs || [],
14617
- ...digest.ciFailingPRs || [],
14618
- ...digest.mergeConflictPRs || [],
14619
- ...digest.incompleteChecklistPRs || [],
14620
- ...digest.missingRequiredFilesPRs || [],
14621
- ...digest.needsRebasePRs || []
14622
- ];
14623
- const waitingOnOthers = [
14624
- ...digest.changesAddressedPRs || [],
14625
- ...digest.waitingOnMaintainerPRs || [],
14626
- ...digest.ciBlockedPRs || [],
14627
- ...digest.ciNotRunningPRs || []
14628
- ];
14629
- function truncateTitle(title, max = 50) {
14630
- const truncated = title.length <= max ? title : title.slice(0, max) + "...";
14631
- return escapeHtml(truncated);
14632
- }
14633
- function renderHealthItems(prs, cssClass, svgPaths, labelFn, metaFn) {
14634
- return prs.map((pr) => {
14635
- const rawLabel = typeof labelFn === "string" ? labelFn : labelFn(pr);
14636
- const label = escapeHtml(rawLabel);
14637
- return `
14638
- <div class="health-item ${cssClass}" data-status="${cssClass}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
14639
- <div class="health-icon">
14640
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
14641
- ${svgPaths}
14642
- </svg>
14643
- </div>
14644
- <div class="health-content">
14645
- <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - ${label}</div>
14646
- <div class="health-meta">${metaFn(pr)}</div>
14647
- </div>
14648
- </div>`;
14649
- }).join("");
14650
- }
14651
- const SVG = {
14652
- comment: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
14653
- edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
14654
- xCircle: '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
14655
- conflict: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
14656
- checklist: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
14657
- file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>',
14658
- checkCircle: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
14659
- clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
14660
- lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
14661
- infoCircle: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
14662
- refresh: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
14663
- box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
14664
- bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
14665
- gitMerge: '<circle cx="7" cy="18" r="3"/><circle cx="7" cy="6" r="3"/><circle cx="17" cy="12" r="3"/><line x1="7" y1="9" x2="7" y2="15"/><path d="M7 9c0 4 10 3 10 3"/>'
14666
- };
14667
- const titleMeta = (pr) => truncateTitle(pr.title);
14668
- return `<!DOCTYPE html>
14669
- <html lang="en">
14670
- <head>
14671
- <meta charset="UTF-8">
14672
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
14673
- <title>OSS Autopilot - Mission Control</title>
14674
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
14675
- <link rel="preconnect" href="https://fonts.googleapis.com">
14676
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14677
- <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
14678
- <style>
14589
+ var init_dashboard_formatters = __esm({
14590
+ "src/commands/dashboard-formatters.ts"() {
14591
+ "use strict";
14592
+ }
14593
+ });
14594
+
14595
+ // src/commands/dashboard-styles.ts
14596
+ var DASHBOARD_CSS;
14597
+ var init_dashboard_styles = __esm({
14598
+ "src/commands/dashboard-styles.ts"() {
14599
+ "use strict";
14600
+ DASHBOARD_CSS = `
14679
14601
  :root, [data-theme="dark"] {
14680
14602
  --bg-base: #080b10;
14681
14603
  --bg-surface: rgba(22, 27, 34, 0.65);
@@ -15435,57 +15357,416 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15435
15357
  .health-item[data-hidden="true"] {
15436
15358
  display: none;
15437
15359
  }
15438
- </style>
15439
- </head>
15440
- <body>
15441
- <div class="container">
15442
- <header class="header">
15443
- <div class="header-left">
15444
- <div class="logo">
15445
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15446
- <circle cx="12" cy="12" r="10"/>
15447
- <path d="M12 6v6l4 2"/>
15448
- </svg>
15449
- </div>
15450
- <div>
15451
- <h1>OSS Autopilot</h1>
15452
- <span class="header-subtitle">Mission Control</span>
15453
- </div>
15454
- </div>
15455
- <div class="header-controls">
15456
- <div class="timestamp">
15457
- Last updated: ${digest.generatedAt ? new Date(digest.generatedAt).toLocaleString("en-US", {
15458
- weekday: "short",
15459
- month: "short",
15460
- day: "numeric",
15461
- year: "numeric",
15462
- hour: "2-digit",
15463
- minute: "2-digit",
15464
- second: "2-digit",
15465
- hour12: false
15466
- }) : "Unknown"}
15467
- </div>
15468
- <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">
15469
- <svg id="themeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15470
- <circle cx="12" cy="12" r="5"/>
15471
- <line x1="12" y1="1" x2="12" y2="3"/>
15472
- <line x1="12" y1="21" x2="12" y2="23"/>
15473
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
15474
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
15475
- <line x1="1" y1="12" x2="3" y2="12"/>
15476
- <line x1="21" y1="12" x2="23" y2="12"/>
15477
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
15478
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
15479
- </svg>
15480
- <svg id="themeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
15481
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
15482
- </svg>
15483
- <span id="themeLabel">Light</span>
15484
- </button>
15485
- </div>
15486
- </header>
15487
-
15488
- <div class="stats-grid">
15360
+ `;
15361
+ }
15362
+ });
15363
+
15364
+ // src/commands/dashboard-components.ts
15365
+ function truncateTitle(title, max = 50) {
15366
+ const truncated = title.length <= max ? title : title.slice(0, max) + "...";
15367
+ return escapeHtml(truncated);
15368
+ }
15369
+ function renderHealthItems(prs, cssClass, svgPaths, labelFn, metaFn) {
15370
+ return prs.map((pr) => {
15371
+ const rawLabel = typeof labelFn === "string" ? labelFn : labelFn(pr);
15372
+ const label = escapeHtml(rawLabel);
15373
+ return `
15374
+ <div class="health-item ${cssClass}" data-status="${cssClass}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15375
+ <div class="health-icon">
15376
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15377
+ ${svgPaths}
15378
+ </svg>
15379
+ </div>
15380
+ <div class="health-content">
15381
+ <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - ${label}</div>
15382
+ <div class="health-meta">${metaFn(pr)}</div>
15383
+ </div>
15384
+ </div>`;
15385
+ }).join("");
15386
+ }
15387
+ function titleMeta(pr) {
15388
+ return truncateTitle(pr.title);
15389
+ }
15390
+ var SVG_ICONS;
15391
+ var init_dashboard_components = __esm({
15392
+ "src/commands/dashboard-components.ts"() {
15393
+ "use strict";
15394
+ init_dashboard_formatters();
15395
+ SVG_ICONS = {
15396
+ comment: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
15397
+ edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
15398
+ xCircle: '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
15399
+ conflict: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
15400
+ checklist: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
15401
+ file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>',
15402
+ checkCircle: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
15403
+ clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
15404
+ lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
15405
+ infoCircle: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
15406
+ refresh: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
15407
+ box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
15408
+ bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
15409
+ gitMerge: '<circle cx="7" cy="18" r="3"/><circle cx="7" cy="6" r="3"/><circle cx="17" cy="12" r="3"/><line x1="7" y1="9" x2="7" y2="15"/><path d="M7 9c0 4 10 3 10 3"/>'
15410
+ };
15411
+ }
15412
+ });
15413
+
15414
+ // src/commands/dashboard-scripts.ts
15415
+ function generateDashboardScripts(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state) {
15416
+ const statusChart = `
15417
+ Chart.defaults.color = '#6e7681';
15418
+ Chart.defaults.borderColor = 'rgba(48, 54, 61, 0.4)';
15419
+ Chart.defaults.font.family = "'Geist', sans-serif";
15420
+ Chart.defaults.font.size = 11;
15421
+
15422
+ // === Status Doughnut ===
15423
+ new Chart(document.getElementById('statusChart'), {
15424
+ type: 'doughnut',
15425
+ data: {
15426
+ labels: ['Active', 'Shelved', 'Merged', 'Closed'],
15427
+ datasets: [{
15428
+ data: [${stats.activePRs}, ${stats.shelvedPRs}, ${stats.mergedPRs}, ${stats.closedPRs}],
15429
+ backgroundColor: ['#3fb950', '#6e7681', '#a855f7', '#484f58'],
15430
+ borderColor: 'rgba(8, 11, 16, 0.8)',
15431
+ borderWidth: 2,
15432
+ hoverOffset: 8
15433
+ }]
15434
+ },
15435
+ options: {
15436
+ responsive: true,
15437
+ maintainAspectRatio: false,
15438
+ cutout: '65%',
15439
+ plugins: {
15440
+ legend: {
15441
+ position: 'bottom',
15442
+ labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } }
15443
+ }
15444
+ }
15445
+ }
15446
+ });`;
15447
+ const repoChart = (() => {
15448
+ const { excludeRepos: exRepos = [], excludeOrgs: exOrgs, minStars } = state.config;
15449
+ const starThreshold = minStars ?? 50;
15450
+ const shouldExcludeRepo = (repo) => {
15451
+ const repoLower = repo.toLowerCase();
15452
+ if (exRepos.some((r) => r.toLowerCase() === repoLower)) return true;
15453
+ if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split("/")[0])) return true;
15454
+ const score = (state.repoScores || {})[repo];
15455
+ if (score?.stargazersCount !== void 0 && score.stargazersCount < starThreshold) return true;
15456
+ return false;
15457
+ };
15458
+ const allRepoEntries = Object.entries(
15459
+ // Rebuild from full prsByRepo to get all repos, not just top 10
15460
+ (() => {
15461
+ const all = {};
15462
+ for (const pr of digest.openPRs || []) {
15463
+ if (shouldExcludeRepo(pr.repo)) continue;
15464
+ if (!all[pr.repo]) all[pr.repo] = { active: 0, merged: 0, closed: 0 };
15465
+ all[pr.repo].active++;
15466
+ }
15467
+ for (const [repo, score] of Object.entries(state.repoScores || {})) {
15468
+ if (shouldExcludeRepo(repo)) continue;
15469
+ if (!all[repo]) all[repo] = { active: 0, merged: 0, closed: 0 };
15470
+ all[repo].merged = score.mergedPRCount;
15471
+ all[repo].closed = score.closedWithoutMergeCount;
15472
+ }
15473
+ return all;
15474
+ })()
15475
+ ).sort((a, b) => {
15476
+ const totalA = a[1].merged + a[1].active + a[1].closed;
15477
+ const totalB = b[1].merged + b[1].active + b[1].closed;
15478
+ return totalB - totalA;
15479
+ });
15480
+ const displayRepos = allRepoEntries.slice(0, 10);
15481
+ const otherRepos = allRepoEntries.slice(10);
15482
+ const grandTotal = allRepoEntries.reduce((sum, [, d]) => sum + d.merged + d.active + d.closed, 0);
15483
+ if (otherRepos.length > 0) {
15484
+ const otherData = otherRepos.reduce(
15485
+ (acc, [, d]) => ({
15486
+ active: acc.active + d.active,
15487
+ merged: acc.merged + d.merged,
15488
+ closed: acc.closed + d.closed
15489
+ }),
15490
+ { active: 0, merged: 0, closed: 0 }
15491
+ );
15492
+ displayRepos.push(["Other", otherData]);
15493
+ }
15494
+ const repoLabels = displayRepos.map(([repo]) => repo === "Other" ? "Other" : repo.split("/")[1] || repo);
15495
+ const mergedData = displayRepos.map(([, d]) => d.merged);
15496
+ const activeData = displayRepos.map(([, d]) => d.active);
15497
+ const closedData = displayRepos.map(([, d]) => d.closed);
15498
+ return `
15499
+ new Chart(document.getElementById('reposChart'), {
15500
+ type: 'bar',
15501
+ data: {
15502
+ labels: ${JSON.stringify(repoLabels)},
15503
+ datasets: [
15504
+ { label: 'Merged', data: ${JSON.stringify(mergedData)}, backgroundColor: '#a855f7', borderRadius: 3 },
15505
+ { label: 'Active', data: ${JSON.stringify(activeData)}, backgroundColor: '#3fb950', borderRadius: 3 },
15506
+ { label: 'Closed', data: ${JSON.stringify(closedData)}, backgroundColor: '#484f58', borderRadius: 3 }
15507
+ ]
15508
+ },
15509
+ options: {
15510
+ responsive: true,
15511
+ maintainAspectRatio: false,
15512
+ scales: {
15513
+ x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 } } },
15514
+ y: { stacked: true, grid: { color: 'rgba(48, 54, 61, 0.3)' }, ticks: { stepSize: 1 } }
15515
+ },
15516
+ plugins: {
15517
+ legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } },
15518
+ tooltip: {
15519
+ callbacks: {
15520
+ afterBody: function(context) {
15521
+ const idx = context[0].dataIndex;
15522
+ const total = ${JSON.stringify(mergedData)}[idx] + ${JSON.stringify(activeData)}[idx] + ${JSON.stringify(closedData)}[idx];
15523
+ const pct = ${grandTotal} > 0 ? ((total / ${grandTotal}) * 100).toFixed(1) : '0.0';
15524
+ return pct + '% of all PRs';
15525
+ }
15526
+ }
15527
+ }
15528
+ }
15529
+ }
15530
+ });`;
15531
+ })();
15532
+ const timelineChart = (() => {
15533
+ const now = /* @__PURE__ */ new Date();
15534
+ const allMonths = [];
15535
+ for (let offset = 5; offset >= 0; offset--) {
15536
+ const d = new Date(now.getFullYear(), now.getMonth() - offset, 1);
15537
+ allMonths.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`);
15538
+ }
15539
+ return `
15540
+ const timelineMonths = ${JSON.stringify(allMonths)};
15541
+ const openedData = ${JSON.stringify(monthlyOpened)};
15542
+ const mergedData = ${JSON.stringify(monthlyMerged)};
15543
+ const closedData = ${JSON.stringify(monthlyClosed)};
15544
+ new Chart(document.getElementById('monthlyChart'), {
15545
+ type: 'bar',
15546
+ data: {
15547
+ labels: timelineMonths,
15548
+ datasets: [
15549
+ {
15550
+ label: 'Opened',
15551
+ data: timelineMonths.map(m => openedData[m] || 0),
15552
+ backgroundColor: '#58a6ff',
15553
+ borderRadius: 3
15554
+ },
15555
+ {
15556
+ label: 'Merged',
15557
+ data: timelineMonths.map(m => mergedData[m] || 0),
15558
+ backgroundColor: '#a855f7',
15559
+ borderRadius: 3
15560
+ },
15561
+ {
15562
+ label: 'Closed',
15563
+ data: timelineMonths.map(m => closedData[m] || 0),
15564
+ backgroundColor: '#484f58',
15565
+ borderRadius: 3
15566
+ }
15567
+ ]
15568
+ },
15569
+ options: {
15570
+ responsive: true,
15571
+ maintainAspectRatio: false,
15572
+ scales: {
15573
+ x: { grid: { display: false } },
15574
+ y: { grid: { color: 'rgba(48, 54, 61, 0.3)' }, beginAtZero: true, ticks: { stepSize: 1 } }
15575
+ },
15576
+ plugins: {
15577
+ legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }
15578
+ },
15579
+ interaction: { intersect: false, mode: 'index' }
15580
+ }
15581
+ });`;
15582
+ })();
15583
+ return THEME_AND_FILTER_SCRIPT + statusChart + "\n" + repoChart + "\n" + timelineChart;
15584
+ }
15585
+ var THEME_AND_FILTER_SCRIPT;
15586
+ var init_dashboard_scripts = __esm({
15587
+ "src/commands/dashboard-scripts.ts"() {
15588
+ "use strict";
15589
+ THEME_AND_FILTER_SCRIPT = `
15590
+ // === Theme Toggle ===
15591
+ (function() {
15592
+ var html = document.documentElement;
15593
+ var toggle = document.getElementById('themeToggle');
15594
+ var sunIcon = document.getElementById('themeIconSun');
15595
+ var moonIcon = document.getElementById('themeIconMoon');
15596
+ var label = document.getElementById('themeLabel');
15597
+
15598
+ function getEffectiveTheme() {
15599
+ try {
15600
+ var stored = localStorage.getItem('oss-dashboard-theme');
15601
+ if (stored === 'light' || stored === 'dark') return stored;
15602
+ } catch (e) { /* localStorage unavailable (private browsing) */ }
15603
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
15604
+ }
15605
+
15606
+ function applyTheme(theme) {
15607
+ html.setAttribute('data-theme', theme);
15608
+ if (theme === 'light') {
15609
+ sunIcon.style.display = 'none';
15610
+ moonIcon.style.display = 'block';
15611
+ label.textContent = 'Dark';
15612
+ } else {
15613
+ sunIcon.style.display = 'block';
15614
+ moonIcon.style.display = 'none';
15615
+ label.textContent = 'Light';
15616
+ }
15617
+ }
15618
+
15619
+ applyTheme(getEffectiveTheme());
15620
+
15621
+ toggle.addEventListener('click', function() {
15622
+ var current = html.getAttribute('data-theme');
15623
+ var next = current === 'dark' ? 'light' : 'dark';
15624
+ try { localStorage.setItem('oss-dashboard-theme', next); } catch (e) { /* private browsing */ }
15625
+ applyTheme(next);
15626
+ });
15627
+ })();
15628
+
15629
+ // === Filtering & Search ===
15630
+ (function() {
15631
+ var searchInput = document.getElementById('searchInput');
15632
+ var statusFilter = document.getElementById('statusFilter');
15633
+ var repoFilter = document.getElementById('repoFilter');
15634
+ var filterCount = document.getElementById('filterCount');
15635
+
15636
+ function applyFilters() {
15637
+ var query = searchInput.value.toLowerCase().trim();
15638
+ var status = statusFilter.value;
15639
+ var repo = repoFilter.value;
15640
+ var allItems = document.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15641
+ var visible = 0;
15642
+ var total = allItems.length;
15643
+
15644
+ allItems.forEach(function(item) {
15645
+ var itemStatus = item.getAttribute('data-status') || '';
15646
+ var itemRepo = item.getAttribute('data-repo') || '';
15647
+ var itemTitle = item.getAttribute('data-title') || '';
15648
+
15649
+ var matchesStatus = (status === 'all') || (itemStatus === status);
15650
+ var matchesRepo = (repo === 'all') || (itemRepo === repo);
15651
+ var matchesSearch = !query || itemTitle.indexOf(query) !== -1;
15652
+
15653
+ if (matchesStatus && matchesRepo && matchesSearch) {
15654
+ item.setAttribute('data-hidden', 'false');
15655
+ visible++;
15656
+ } else {
15657
+ item.setAttribute('data-hidden', 'true');
15658
+ }
15659
+ });
15660
+
15661
+ // Show/hide parent sections if all children are hidden
15662
+ var sections = document.querySelectorAll('.health-section, .pr-list-section');
15663
+ sections.forEach(function(section) {
15664
+ var items = section.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15665
+ if (items.length === 0) return; // sections without filterable items (e.g. empty state)
15666
+ var anyVisible = false;
15667
+ items.forEach(function(item) {
15668
+ if (item.getAttribute('data-hidden') !== 'true') anyVisible = true;
15669
+ });
15670
+ section.style.display = anyVisible ? '' : 'none';
15671
+ });
15672
+
15673
+ var isFiltering = (status !== 'all' || repo !== 'all' || query.length > 0);
15674
+ filterCount.textContent = isFiltering ? (visible + ' of ' + total + ' items') : '';
15675
+ }
15676
+
15677
+ searchInput.addEventListener('input', applyFilters);
15678
+ statusFilter.addEventListener('change', applyFilters);
15679
+ repoFilter.addEventListener('change', applyFilters);
15680
+ })();
15681
+ `;
15682
+ }
15683
+ });
15684
+
15685
+ // src/commands/dashboard-templates.ts
15686
+ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
15687
+ const approachingDormantDays = state.config?.approachingDormantDays ?? 25;
15688
+ const shelvedPRs = digest.shelvedPRs || [];
15689
+ const autoUnshelvedPRs = digest.autoUnshelvedPRs || [];
15690
+ const recentlyMerged = digest.recentlyMergedPRs || [];
15691
+ const shelvedUrls = new Set(shelvedPRs.map((pr) => pr.url));
15692
+ const activePRList = (digest.openPRs || []).filter((pr) => !shelvedUrls.has(pr.url));
15693
+ const actionRequired = [
15694
+ ...digest.prsNeedingResponse || [],
15695
+ ...digest.needsChangesPRs || [],
15696
+ ...digest.ciFailingPRs || [],
15697
+ ...digest.mergeConflictPRs || [],
15698
+ ...digest.incompleteChecklistPRs || [],
15699
+ ...digest.missingRequiredFilesPRs || [],
15700
+ ...digest.needsRebasePRs || []
15701
+ ];
15702
+ const waitingOnOthers = [
15703
+ ...digest.changesAddressedPRs || [],
15704
+ ...digest.waitingOnMaintainerPRs || [],
15705
+ ...digest.ciBlockedPRs || [],
15706
+ ...digest.ciNotRunningPRs || []
15707
+ ];
15708
+ return `<!DOCTYPE html>
15709
+ <html lang="en">
15710
+ <head>
15711
+ <meta charset="UTF-8">
15712
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15713
+ <title>OSS Autopilot - Mission Control</title>
15714
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15715
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15716
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15717
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
15718
+ <style>${DASHBOARD_CSS}
15719
+ </style>
15720
+ </head>
15721
+ <body>
15722
+ <div class="container">
15723
+ <header class="header">
15724
+ <div class="header-left">
15725
+ <div class="logo">
15726
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15727
+ <circle cx="12" cy="12" r="10"/>
15728
+ <path d="M12 6v6l4 2"/>
15729
+ </svg>
15730
+ </div>
15731
+ <div>
15732
+ <h1>OSS Autopilot</h1>
15733
+ <span class="header-subtitle">Mission Control</span>
15734
+ </div>
15735
+ </div>
15736
+ <div class="header-controls">
15737
+ <div class="timestamp">
15738
+ Last updated: ${digest.generatedAt ? new Date(digest.generatedAt).toLocaleString("en-US", {
15739
+ weekday: "short",
15740
+ month: "short",
15741
+ day: "numeric",
15742
+ year: "numeric",
15743
+ hour: "2-digit",
15744
+ minute: "2-digit",
15745
+ second: "2-digit",
15746
+ hour12: false
15747
+ }) : "Unknown"}
15748
+ </div>
15749
+ <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">
15750
+ <svg id="themeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15751
+ <circle cx="12" cy="12" r="5"/>
15752
+ <line x1="12" y1="1" x2="12" y2="3"/>
15753
+ <line x1="12" y1="21" x2="12" y2="23"/>
15754
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
15755
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
15756
+ <line x1="1" y1="12" x2="3" y2="12"/>
15757
+ <line x1="21" y1="12" x2="23" y2="12"/>
15758
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
15759
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
15760
+ </svg>
15761
+ <svg id="themeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
15762
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
15763
+ </svg>
15764
+ <span id="themeLabel">Light</span>
15765
+ </button>
15766
+ </div>
15767
+ </header>
15768
+
15769
+ <div class="stats-grid">
15489
15770
  <div class="stat-card active">
15490
15771
  <div class="stat-value">${stats.activePRs}</div>
15491
15772
  <div class="stat-label">Active PRs</div>
@@ -15562,31 +15843,31 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15562
15843
  ${renderHealthItems(
15563
15844
  digest.prsNeedingResponse || [],
15564
15845
  "needs-response",
15565
- SVG.comment,
15846
+ SVG_ICONS.comment,
15566
15847
  "Needs Response",
15567
15848
  (pr) => pr.lastMaintainerComment ? `@${escapeHtml(pr.lastMaintainerComment.author)}: ${truncateTitle(pr.lastMaintainerComment.body, 40)}` : truncateTitle(pr.title)
15568
15849
  )}
15569
- ${renderHealthItems(digest.needsChangesPRs || [], "needs-changes", SVG.edit, "Needs Changes", titleMeta)}
15570
- ${renderHealthItems(digest.ciFailingPRs || [], "ci-failing", SVG.xCircle, "CI Failing", titleMeta)}
15571
- ${renderHealthItems(digest.mergeConflictPRs || [], "conflict", SVG.conflict, "Merge Conflict", titleMeta)}
15850
+ ${renderHealthItems(digest.needsChangesPRs || [], "needs-changes", SVG_ICONS.edit, "Needs Changes", titleMeta)}
15851
+ ${renderHealthItems(digest.ciFailingPRs || [], "ci-failing", SVG_ICONS.xCircle, "CI Failing", titleMeta)}
15852
+ ${renderHealthItems(digest.mergeConflictPRs || [], "conflict", SVG_ICONS.conflict, "Merge Conflict", titleMeta)}
15572
15853
  ${renderHealthItems(
15573
15854
  digest.incompleteChecklistPRs || [],
15574
15855
  "incomplete-checklist",
15575
- SVG.checklist,
15856
+ SVG_ICONS.checklist,
15576
15857
  (pr) => `Incomplete Checklist${pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : ""}`,
15577
15858
  titleMeta
15578
15859
  )}
15579
15860
  ${renderHealthItems(
15580
15861
  digest.missingRequiredFilesPRs || [],
15581
15862
  "missing-files",
15582
- SVG.file,
15863
+ SVG_ICONS.file,
15583
15864
  "Missing Required Files",
15584
15865
  (pr) => pr.missingRequiredFiles ? escapeHtml(pr.missingRequiredFiles.join(", ")) : truncateTitle(pr.title)
15585
15866
  )}
15586
15867
  ${renderHealthItems(
15587
15868
  digest.needsRebasePRs || [],
15588
15869
  "needs-rebase",
15589
- SVG.refresh,
15870
+ SVG_ICONS.refresh,
15590
15871
  (pr) => `Needs Rebase${pr.commitsBehindUpstream ? ` (${pr.commitsBehindUpstream} behind)` : ""}`,
15591
15872
  titleMeta
15592
15873
  )}
@@ -15608,13 +15889,13 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15608
15889
  ${renderHealthItems(
15609
15890
  digest.changesAddressedPRs || [],
15610
15891
  "changes-addressed",
15611
- SVG.checkCircle,
15892
+ SVG_ICONS.checkCircle,
15612
15893
  "Changes Addressed",
15613
15894
  (pr) => `Awaiting re-review${pr.lastMaintainerComment ? ` from @${escapeHtml(pr.lastMaintainerComment.author)}` : ""}`
15614
15895
  )}
15615
- ${renderHealthItems(digest.waitingOnMaintainerPRs || [], "waiting-maintainer", SVG.clock, "Waiting on Maintainer", titleMeta)}
15616
- ${renderHealthItems(digest.ciBlockedPRs || [], "ci-blocked", SVG.lock, "CI Blocked", titleMeta)}
15617
- ${renderHealthItems(digest.ciNotRunningPRs || [], "ci-not-running", SVG.infoCircle, "CI Not Running", titleMeta)}
15896
+ ${renderHealthItems(digest.waitingOnMaintainerPRs || [], "waiting-maintainer", SVG_ICONS.clock, "Waiting on Maintainer", titleMeta)}
15897
+ ${renderHealthItems(digest.ciBlockedPRs || [], "ci-blocked", SVG_ICONS.lock, "CI Blocked", titleMeta)}
15898
+ ${renderHealthItems(digest.ciNotRunningPRs || [], "ci-not-running", SVG_ICONS.infoCircle, "CI Not Running", titleMeta)}
15618
15899
  </div>
15619
15900
  </section>
15620
15901
  ` : ""}
@@ -15638,7 +15919,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15638
15919
  <section class="health-section" style="animation-delay: 0.15s;">
15639
15920
  <div class="health-header">
15640
15921
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-merged)" stroke-width="2">
15641
- ${SVG.gitMerge}
15922
+ ${SVG_ICONS.gitMerge}
15642
15923
  </svg>
15643
15924
  <h2>Recently Merged</h2>
15644
15925
  <span class="health-badge" style="background: var(--accent-merged-dim); color: var(--accent-merged);">${recentlyMerged.length} merged</span>
@@ -15649,7 +15930,7 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15649
15930
  <div class="health-item" style="border-left-color: var(--accent-merged);" data-status="merged" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15650
15931
  <div class="health-icon" style="background: var(--accent-merged-dim); color: var(--accent-merged);">
15651
15932
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15652
- ${SVG.gitMerge}
15933
+ ${SVG_ICONS.gitMerge}
15653
15934
  </svg>
15654
15935
  </div>
15655
15936
  <div class="health-content">
@@ -15662,500 +15943,237 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
15662
15943
  </div>
15663
15944
  </section>
15664
15945
  ` : ""}
15665
-
15666
- ${(digest.recentlyClosedPRs || []).length > 0 ? `
15667
- <section class="health-section" style="animation-delay: 0.2s;">
15668
- <div class="health-header">
15669
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="2">
15670
- <circle cx="12" cy="12" r="10"/>
15671
- <line x1="15" y1="9" x2="9" y2="15"/>
15672
- <line x1="9" y1="9" x2="15" y2="15"/>
15673
- </svg>
15674
- <h2>Recently Closed</h2>
15675
- <span class="health-badge" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${(digest.recentlyClosedPRs || []).length} closed</span>
15676
- </div>
15677
- <div class="health-items">
15678
- ${(digest.recentlyClosedPRs || []).map(
15679
- (pr) => `
15680
- <div class="health-item" style="border-left-color: var(--text-muted); opacity: 0.7;" data-status="closed" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15681
- <div class="health-icon" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">
15682
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15683
- <circle cx="12" cy="12" r="10"/>
15684
- <line x1="15" y1="9" x2="9" y2="15"/>
15685
- <line x1="9" y1="9" x2="15" y2="15"/>
15686
- </svg>
15687
- </div>
15688
- <div class="health-content">
15689
- <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - Closed</div>
15690
- <div class="health-meta">${truncateTitle(pr.title)}${pr.closedAt ? ` \xB7 ${new Date(pr.closedAt).toLocaleDateString()}` : ""}</div>
15691
- </div>
15692
- </div>
15693
- `
15694
- ).join("")}
15695
- </div>
15696
- </section>
15697
- ` : ""}
15698
-
15699
- ${autoUnshelvedPRs.length > 0 ? `
15700
- <section class="health-section" style="animation-delay: 0.25s;">
15701
- <div class="health-header">
15702
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
15703
- ${SVG.bell}
15704
- </svg>
15705
- <h2>Auto-Unshelved</h2>
15706
- <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${autoUnshelvedPRs.length} unshelved</span>
15707
- </div>
15708
- <div class="health-items">
15709
- ${renderHealthItems(
15710
- autoUnshelvedPRs,
15711
- "auto-unshelved",
15712
- SVG.bell,
15713
- (pr) => "Auto-Unshelved (" + pr.status.replace(/_/g, " ") + ")",
15714
- titleMeta
15715
- )}
15716
- </div>
15717
- </section>
15718
- ` : ""}
15719
-
15720
- ${issueResponses.length > 0 ? `
15721
- <section class="health-section" style="animation-delay: 0.3s;">
15722
- <div class="health-header">
15723
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
15724
- ${SVG.comment}
15725
- </svg>
15726
- <h2>Issue Conversations</h2>
15727
- <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${issueResponses.length} repl${issueResponses.length !== 1 ? "ies" : "y"}</span>
15728
- </div>
15729
- <div class="health-items">
15730
- ${issueResponses.map(
15731
- (issue) => `
15732
- <div class="health-item changes-addressed">
15733
- <div class="health-icon" style="background: var(--accent-info-dim); color: var(--accent-info);">
15734
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15735
- ${SVG.comment}
15736
- </svg>
15737
- </div>
15738
- <div class="health-content">
15739
- <div class="health-title"><a href="${escapeHtml(issue.url)}" target="_blank">${escapeHtml(issue.repo)}#${issue.number}</a> - ${escapeHtml(issue.title.slice(0, 50))}${issue.title.length > 50 ? "..." : ""}</div>
15740
- <div class="health-meta">@${escapeHtml(issue.lastResponseAuthor)}: ${escapeHtml(issue.lastResponseBody.slice(0, 60))}${issue.lastResponseBody.length > 60 ? "..." : ""}</div>
15741
- </div>
15742
- </div>
15743
- `
15744
- ).join("")}
15745
- </div>
15746
- </section>
15747
- ` : ""}
15748
-
15749
- <div class="main-grid">
15750
- <div class="card">
15751
- <div class="card-header">
15752
- <span class="card-title">PR Status Distribution</span>
15753
- </div>
15754
- <div class="card-body">
15755
- <div class="chart-container">
15756
- <canvas id="statusChart"></canvas>
15757
- </div>
15758
- </div>
15759
- </div>
15760
-
15761
- <div class="card">
15762
- <div class="card-header">
15763
- <span class="card-title">Repository Breakdown</span>
15764
- </div>
15765
- <div class="card-body">
15766
- <div class="chart-container">
15767
- <canvas id="reposChart"></canvas>
15768
- </div>
15769
- </div>
15770
- </div>
15771
- </div>
15772
-
15773
- <div class="card" style="margin-bottom: 1.25rem;">
15774
- <div class="card-header">
15775
- <span class="card-title">Contribution Timeline</span>
15776
- </div>
15777
- <div class="card-body">
15778
- <div class="chart-container" style="height: 250px;">
15779
- <canvas id="monthlyChart"></canvas>
15780
- </div>
15781
- </div>
15782
- </div>
15783
-
15784
-
15785
- ${activePRList.length > 0 ? `
15786
- <section class="pr-list-section">
15787
- <div class="pr-list-header">
15788
- <h2 class="pr-list-title">Active Pull Requests</h2>
15789
- <span class="pr-count">${activePRList.length} open</span>
15790
- </div>
15791
- <div class="pr-list">
15792
- ${activePRList.map((pr) => {
15793
- const hasIssues = pr.ciStatus === "failing" || pr.hasMergeConflict || pr.hasUnrespondedComment && pr.status !== "changes_addressed" || pr.status === "needs_changes";
15794
- const isStale = pr.daysSinceActivity >= approachingDormantDays;
15795
- const itemClass = hasIssues ? "has-issues" : isStale ? "stale" : "";
15796
- const prStatus = pr.ciStatus === "failing" ? "ci-failing" : pr.hasMergeConflict ? "conflict" : pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? "needs-response" : pr.status === "needs_changes" ? "needs-changes" : pr.status === "changes_addressed" ? "changes-addressed" : "active";
15797
- return `
15798
- <div class="pr-item ${itemClass}" data-status="${prStatus}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15799
- <div class="pr-status-indicator">
15800
- ${hasIssues ? `
15801
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15802
- <circle cx="12" cy="12" r="10"/>
15803
- <line x1="12" y1="8" x2="12" y2="12"/>
15804
- <line x1="12" y1="16" x2="12.01" y2="16"/>
15805
- </svg>
15806
- ` : `
15807
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15808
- <circle cx="12" cy="12" r="10"/>
15809
- <line x1="12" y1="16" x2="12" y2="12"/>
15810
- <line x1="12" y1="8" x2="12.01" y2="8"/>
15811
- </svg>
15812
- `}
15813
- </div>
15814
- <div class="pr-content">
15815
- <div class="pr-title-row">
15816
- <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
15817
- <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
15818
- </div>
15819
- <div class="pr-badges">
15820
- ${pr.ciStatus === "failing" ? '<span class="badge badge-ci-failing">CI Failing</span>' : ""}
15821
- ${pr.ciStatus === "passing" ? '<span class="badge badge-passing">CI Passing</span>' : ""}
15822
- ${pr.ciStatus === "pending" ? '<span class="badge badge-pending">CI Pending</span>' : ""}
15823
- ${pr.hasMergeConflict ? '<span class="badge badge-conflict">Merge Conflict</span>' : ""}
15824
- ${pr.hasUnrespondedComment && pr.status === "changes_addressed" ? '<span class="badge badge-changes-addressed">Changes Addressed</span>' : ""}
15825
- ${pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? '<span class="badge badge-needs-response">Needs Response</span>' : ""}
15826
- ${pr.reviewDecision === "changes_requested" ? '<span class="badge badge-changes-requested">Changes Requested</span>' : ""}
15827
- ${isStale ? `<span class="badge badge-stale">${pr.daysSinceActivity}d inactive</span>` : ""}
15828
- </div>
15829
- </div>
15830
- <div class="pr-activity">
15831
- ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
15832
- </div>
15833
- </div>`;
15834
- }).join("")}
15835
- </div>
15836
- </section>
15837
- ` : `
15838
- <section class="pr-list-section">
15839
- <div class="pr-list-header">
15840
- <h2 class="pr-list-title">Active Pull Requests</h2>
15841
- <span class="pr-count">0 open</span>
15842
- </div>
15843
- <div class="empty-state">
15844
- <div class="empty-state-icon">
15845
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
15846
- <circle cx="12" cy="12" r="10"/>
15847
- <path d="M8 12h8"/>
15848
- </svg>
15849
- </div>
15850
- <p>No active pull requests</p>
15851
- </div>
15852
- </section>
15853
- `}
15854
-
15855
- ${shelvedPRs.length > 0 ? `
15856
- <section class="pr-list-section" style="margin-top: 1.25rem; opacity: 0.7;">
15857
- <div class="pr-list-header">
15858
- <h2 class="pr-list-title">Shelved Pull Requests</h2>
15859
- <span class="pr-count" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${shelvedPRs.length} shelved</span>
15946
+
15947
+ ${(digest.recentlyClosedPRs || []).length > 0 ? `
15948
+ <section class="health-section" style="animation-delay: 0.2s;">
15949
+ <div class="health-header">
15950
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="2">
15951
+ <circle cx="12" cy="12" r="10"/>
15952
+ <line x1="15" y1="9" x2="9" y2="15"/>
15953
+ <line x1="9" y1="9" x2="15" y2="15"/>
15954
+ </svg>
15955
+ <h2>Recently Closed</h2>
15956
+ <span class="health-badge" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${(digest.recentlyClosedPRs || []).length} closed</span>
15860
15957
  </div>
15861
- <div class="pr-list">
15862
- ${shelvedPRs.map(
15958
+ <div class="health-items">
15959
+ ${(digest.recentlyClosedPRs || []).map(
15863
15960
  (pr) => `
15864
- <div class="pr-item" data-status="shelved" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15865
- <div class="pr-status-indicator" style="background: rgba(110, 118, 129, 0.1); color: var(--text-muted);">
15866
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15867
- ${SVG.box}
15961
+ <div class="health-item" style="border-left-color: var(--text-muted); opacity: 0.7;" data-status="closed" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
15962
+ <div class="health-icon" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">
15963
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15964
+ <circle cx="12" cy="12" r="10"/>
15965
+ <line x1="15" y1="9" x2="9" y2="15"/>
15966
+ <line x1="9" y1="9" x2="15" y2="15"/>
15868
15967
  </svg>
15869
15968
  </div>
15870
- <div class="pr-content">
15871
- <div class="pr-title-row">
15872
- <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
15873
- <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
15874
- </div>
15875
- <div class="pr-badges">
15876
- <span class="badge badge-days">${pr.daysSinceActivity}d inactive</span>
15877
- </div>
15878
- </div>
15879
- <div class="pr-activity">
15880
- ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
15969
+ <div class="health-content">
15970
+ <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - Closed</div>
15971
+ <div class="health-meta">${truncateTitle(pr.title)}${pr.closedAt ? ` \xB7 ${new Date(pr.closedAt).toLocaleDateString()}` : ""}</div>
15881
15972
  </div>
15882
- </div>`
15973
+ </div>
15974
+ `
15883
15975
  ).join("")}
15884
15976
  </div>
15885
15977
  </section>
15886
15978
  ` : ""}
15887
15979
 
15888
- <footer class="footer">
15889
- <p>OSS Autopilot // Mission Control</p>
15890
- <p style="margin-top: 0.25rem;">Dashboard generated: ${digest.generatedAt ? new Date(digest.generatedAt).toISOString() : "Unknown"}</p>
15891
- </footer>
15892
- </div>
15893
-
15894
- <script>
15895
- // === Theme Toggle ===
15896
- (function() {
15897
- var html = document.documentElement;
15898
- var toggle = document.getElementById('themeToggle');
15899
- var sunIcon = document.getElementById('themeIconSun');
15900
- var moonIcon = document.getElementById('themeIconMoon');
15901
- var label = document.getElementById('themeLabel');
15902
-
15903
- function getEffectiveTheme() {
15904
- try {
15905
- var stored = localStorage.getItem('oss-dashboard-theme');
15906
- if (stored === 'light' || stored === 'dark') return stored;
15907
- } catch (e) { /* localStorage unavailable (private browsing) */ }
15908
- return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
15909
- }
15910
-
15911
- function applyTheme(theme) {
15912
- html.setAttribute('data-theme', theme);
15913
- if (theme === 'light') {
15914
- sunIcon.style.display = 'none';
15915
- moonIcon.style.display = 'block';
15916
- label.textContent = 'Dark';
15917
- } else {
15918
- sunIcon.style.display = 'block';
15919
- moonIcon.style.display = 'none';
15920
- label.textContent = 'Light';
15921
- }
15922
- }
15923
-
15924
- applyTheme(getEffectiveTheme());
15925
-
15926
- toggle.addEventListener('click', function() {
15927
- var current = html.getAttribute('data-theme');
15928
- var next = current === 'dark' ? 'light' : 'dark';
15929
- try { localStorage.setItem('oss-dashboard-theme', next); } catch (e) { /* private browsing */ }
15930
- applyTheme(next);
15931
- });
15932
- })();
15933
-
15934
- // === Filtering & Search ===
15935
- (function() {
15936
- var searchInput = document.getElementById('searchInput');
15937
- var statusFilter = document.getElementById('statusFilter');
15938
- var repoFilter = document.getElementById('repoFilter');
15939
- var filterCount = document.getElementById('filterCount');
15940
-
15941
- function applyFilters() {
15942
- var query = searchInput.value.toLowerCase().trim();
15943
- var status = statusFilter.value;
15944
- var repo = repoFilter.value;
15945
- var allItems = document.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15946
- var visible = 0;
15947
- var total = allItems.length;
15948
-
15949
- allItems.forEach(function(item) {
15950
- var itemStatus = item.getAttribute('data-status') || '';
15951
- var itemRepo = item.getAttribute('data-repo') || '';
15952
- var itemTitle = item.getAttribute('data-title') || '';
15953
-
15954
- var matchesStatus = (status === 'all') || (itemStatus === status);
15955
- var matchesRepo = (repo === 'all') || (itemRepo === repo);
15956
- var matchesSearch = !query || itemTitle.indexOf(query) !== -1;
15957
-
15958
- if (matchesStatus && matchesRepo && matchesSearch) {
15959
- item.setAttribute('data-hidden', 'false');
15960
- visible++;
15961
- } else {
15962
- item.setAttribute('data-hidden', 'true');
15963
- }
15964
- });
15965
-
15966
- // Show/hide parent sections if all children are hidden
15967
- var sections = document.querySelectorAll('.health-section, .pr-list-section');
15968
- sections.forEach(function(section) {
15969
- var items = section.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
15970
- if (items.length === 0) return; // sections without filterable items (e.g. empty state)
15971
- var anyVisible = false;
15972
- items.forEach(function(item) {
15973
- if (item.getAttribute('data-hidden') !== 'true') anyVisible = true;
15974
- });
15975
- section.style.display = anyVisible ? '' : 'none';
15976
- });
15977
-
15978
- var isFiltering = (status !== 'all' || repo !== 'all' || query.length > 0);
15979
- filterCount.textContent = isFiltering ? (visible + ' of ' + total + ' items') : '';
15980
- }
15981
-
15982
- searchInput.addEventListener('input', applyFilters);
15983
- statusFilter.addEventListener('change', applyFilters);
15984
- repoFilter.addEventListener('change', applyFilters);
15985
- })();
15986
-
15987
- // === Chart.js Configuration ===
15988
- Chart.defaults.color = '#6e7681';
15989
- Chart.defaults.borderColor = 'rgba(48, 54, 61, 0.4)';
15990
- Chart.defaults.font.family = "'Geist', sans-serif";
15991
- Chart.defaults.font.size = 11;
15992
-
15993
- // === Status Doughnut ===
15994
- new Chart(document.getElementById('statusChart'), {
15995
- type: 'doughnut',
15996
- data: {
15997
- labels: ['Active', 'Shelved', 'Merged', 'Closed'],
15998
- datasets: [{
15999
- data: [${stats.activePRs}, ${stats.shelvedPRs}, ${stats.mergedPRs}, ${stats.closedPRs}],
16000
- backgroundColor: ['#3fb950', '#6e7681', '#a855f7', '#484f58'],
16001
- borderColor: 'rgba(8, 11, 16, 0.8)',
16002
- borderWidth: 2,
16003
- hoverOffset: 8
16004
- }]
16005
- },
16006
- options: {
16007
- responsive: true,
16008
- maintainAspectRatio: false,
16009
- cutout: '65%',
16010
- plugins: {
16011
- legend: {
16012
- position: 'bottom',
16013
- labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } }
16014
- }
16015
- }
16016
- }
16017
- });
16018
-
16019
- // === Repository Breakdown (with "Other" bucket + percentage tooltips) ===
16020
- ${(() => {
16021
- const { excludeRepos: exRepos = [], excludeOrgs: exOrgs, minStars } = state.config;
16022
- const starThreshold = minStars ?? 50;
16023
- const shouldExcludeRepo = (repo) => {
16024
- const repoLower = repo.toLowerCase();
16025
- if (exRepos.some((r) => r.toLowerCase() === repoLower)) return true;
16026
- if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split("/")[0])) return true;
16027
- const score = (state.repoScores || {})[repo];
16028
- if (score?.stargazersCount !== void 0 && score.stargazersCount < starThreshold) return true;
16029
- return false;
16030
- };
16031
- const allRepoEntries = Object.entries(
16032
- // Rebuild from full prsByRepo to get all repos, not just top 10
16033
- (() => {
16034
- const all = {};
16035
- for (const pr of digest.openPRs || []) {
16036
- if (shouldExcludeRepo(pr.repo)) continue;
16037
- if (!all[pr.repo]) all[pr.repo] = { active: 0, merged: 0, closed: 0 };
16038
- all[pr.repo].active++;
16039
- }
16040
- for (const [repo, score] of Object.entries(state.repoScores || {})) {
16041
- if (shouldExcludeRepo(repo)) continue;
16042
- if (!all[repo]) all[repo] = { active: 0, merged: 0, closed: 0 };
16043
- all[repo].merged = score.mergedPRCount;
16044
- all[repo].closed = score.closedWithoutMergeCount;
16045
- }
16046
- return all;
16047
- })()
16048
- ).sort((a, b) => {
16049
- const totalA = a[1].merged + a[1].active + a[1].closed;
16050
- const totalB = b[1].merged + b[1].active + b[1].closed;
16051
- return totalB - totalA;
16052
- });
16053
- const displayRepos = allRepoEntries.slice(0, 10);
16054
- const otherRepos = allRepoEntries.slice(10);
16055
- const grandTotal = allRepoEntries.reduce((sum, [, d]) => sum + d.merged + d.active + d.closed, 0);
16056
- if (otherRepos.length > 0) {
16057
- const otherData = otherRepos.reduce(
16058
- (acc, [, d]) => ({
16059
- active: acc.active + d.active,
16060
- merged: acc.merged + d.merged,
16061
- closed: acc.closed + d.closed
16062
- }),
16063
- { active: 0, merged: 0, closed: 0 }
16064
- );
16065
- displayRepos.push(["Other", otherData]);
16066
- }
16067
- const repoLabels = displayRepos.map(([repo]) => repo === "Other" ? "Other" : repo.split("/")[1] || repo);
16068
- const mergedData = displayRepos.map(([, d]) => d.merged);
16069
- const activeData = displayRepos.map(([, d]) => d.active);
16070
- const closedData = displayRepos.map(([, d]) => d.closed);
16071
- return `
16072
- new Chart(document.getElementById('reposChart'), {
16073
- type: 'bar',
16074
- data: {
16075
- labels: ${JSON.stringify(repoLabels)},
16076
- datasets: [
16077
- { label: 'Merged', data: ${JSON.stringify(mergedData)}, backgroundColor: '#a855f7', borderRadius: 3 },
16078
- { label: 'Active', data: ${JSON.stringify(activeData)}, backgroundColor: '#3fb950', borderRadius: 3 },
16079
- { label: 'Closed', data: ${JSON.stringify(closedData)}, backgroundColor: '#484f58', borderRadius: 3 }
16080
- ]
16081
- },
16082
- options: {
16083
- responsive: true,
16084
- maintainAspectRatio: false,
16085
- scales: {
16086
- x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 } } },
16087
- y: { stacked: true, grid: { color: 'rgba(48, 54, 61, 0.3)' }, ticks: { stepSize: 1 } }
16088
- },
16089
- plugins: {
16090
- legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } },
16091
- tooltip: {
16092
- callbacks: {
16093
- afterBody: function(context) {
16094
- const idx = context[0].dataIndex;
16095
- const total = ${JSON.stringify(mergedData)}[idx] + ${JSON.stringify(activeData)}[idx] + ${JSON.stringify(closedData)}[idx];
16096
- const pct = ${grandTotal} > 0 ? ((total / ${grandTotal}) * 100).toFixed(1) : '0.0';
16097
- return pct + '% of all PRs';
16098
- }
16099
- }
16100
- }
16101
- }
16102
- }
16103
- });`;
16104
- })()}
15980
+ ${autoUnshelvedPRs.length > 0 ? `
15981
+ <section class="health-section" style="animation-delay: 0.25s;">
15982
+ <div class="health-header">
15983
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
15984
+ ${SVG_ICONS.bell}
15985
+ </svg>
15986
+ <h2>Auto-Unshelved</h2>
15987
+ <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${autoUnshelvedPRs.length} unshelved</span>
15988
+ </div>
15989
+ <div class="health-items">
15990
+ ${renderHealthItems(
15991
+ autoUnshelvedPRs,
15992
+ "auto-unshelved",
15993
+ SVG_ICONS.bell,
15994
+ (pr) => "Auto-Unshelved (" + pr.status.replace(/_/g, " ") + ")",
15995
+ titleMeta
15996
+ )}
15997
+ </div>
15998
+ </section>
15999
+ ` : ""}
16105
16000
 
16106
- // === Contribution Timeline (grouped bar: Opened/Merged/Closed) ===
16107
- ${(() => {
16108
- const now = /* @__PURE__ */ new Date();
16109
- const allMonths = [];
16110
- for (let offset = 5; offset >= 0; offset--) {
16111
- const d = new Date(now.getFullYear(), now.getMonth() - offset, 1);
16112
- allMonths.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`);
16113
- }
16001
+ ${issueResponses.length > 0 ? `
16002
+ <section class="health-section" style="animation-delay: 0.3s;">
16003
+ <div class="health-header">
16004
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
16005
+ ${SVG_ICONS.comment}
16006
+ </svg>
16007
+ <h2>Issue Conversations</h2>
16008
+ <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${issueResponses.length} repl${issueResponses.length !== 1 ? "ies" : "y"}</span>
16009
+ </div>
16010
+ <div class="health-items">
16011
+ ${issueResponses.map(
16012
+ (issue) => `
16013
+ <div class="health-item changes-addressed">
16014
+ <div class="health-icon" style="background: var(--accent-info-dim); color: var(--accent-info);">
16015
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
16016
+ ${SVG_ICONS.comment}
16017
+ </svg>
16018
+ </div>
16019
+ <div class="health-content">
16020
+ <div class="health-title"><a href="${escapeHtml(issue.url)}" target="_blank">${escapeHtml(issue.repo)}#${issue.number}</a> - ${escapeHtml(issue.title.slice(0, 50))}${issue.title.length > 50 ? "..." : ""}</div>
16021
+ <div class="health-meta">@${escapeHtml(issue.lastResponseAuthor)}: ${escapeHtml(issue.lastResponseBody.slice(0, 60))}${issue.lastResponseBody.length > 60 ? "..." : ""}</div>
16022
+ </div>
16023
+ </div>
16024
+ `
16025
+ ).join("")}
16026
+ </div>
16027
+ </section>
16028
+ ` : ""}
16029
+
16030
+ <div class="main-grid">
16031
+ <div class="card">
16032
+ <div class="card-header">
16033
+ <span class="card-title">PR Status Distribution</span>
16034
+ </div>
16035
+ <div class="card-body">
16036
+ <div class="chart-container">
16037
+ <canvas id="statusChart"></canvas>
16038
+ </div>
16039
+ </div>
16040
+ </div>
16041
+
16042
+ <div class="card">
16043
+ <div class="card-header">
16044
+ <span class="card-title">Repository Breakdown</span>
16045
+ </div>
16046
+ <div class="card-body">
16047
+ <div class="chart-container">
16048
+ <canvas id="reposChart"></canvas>
16049
+ </div>
16050
+ </div>
16051
+ </div>
16052
+ </div>
16053
+
16054
+ <div class="card" style="margin-bottom: 1.25rem;">
16055
+ <div class="card-header">
16056
+ <span class="card-title">Contribution Timeline</span>
16057
+ </div>
16058
+ <div class="card-body">
16059
+ <div class="chart-container" style="height: 250px;">
16060
+ <canvas id="monthlyChart"></canvas>
16061
+ </div>
16062
+ </div>
16063
+ </div>
16064
+
16065
+
16066
+ ${activePRList.length > 0 ? `
16067
+ <section class="pr-list-section">
16068
+ <div class="pr-list-header">
16069
+ <h2 class="pr-list-title">Active Pull Requests</h2>
16070
+ <span class="pr-count">${activePRList.length} open</span>
16071
+ </div>
16072
+ <div class="pr-list">
16073
+ ${activePRList.map((pr) => {
16074
+ const hasIssues = pr.ciStatus === "failing" || pr.hasMergeConflict || pr.hasUnrespondedComment && pr.status !== "changes_addressed" || pr.status === "needs_changes";
16075
+ const isStale = pr.daysSinceActivity >= approachingDormantDays;
16076
+ const itemClass = hasIssues ? "has-issues" : isStale ? "stale" : "";
16077
+ const prStatus = pr.ciStatus === "failing" ? "ci-failing" : pr.hasMergeConflict ? "conflict" : pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? "needs-response" : pr.status === "needs_changes" ? "needs-changes" : pr.status === "changes_addressed" ? "changes-addressed" : "active";
16114
16078
  return `
16115
- const timelineMonths = ${JSON.stringify(allMonths)};
16116
- const openedData = ${JSON.stringify(monthlyOpened)};
16117
- const mergedData = ${JSON.stringify(monthlyMerged)};
16118
- const closedData = ${JSON.stringify(monthlyClosed)};
16119
- new Chart(document.getElementById('monthlyChart'), {
16120
- type: 'bar',
16121
- data: {
16122
- labels: timelineMonths,
16123
- datasets: [
16124
- {
16125
- label: 'Opened',
16126
- data: timelineMonths.map(m => openedData[m] || 0),
16127
- backgroundColor: '#58a6ff',
16128
- borderRadius: 3
16129
- },
16130
- {
16131
- label: 'Merged',
16132
- data: timelineMonths.map(m => mergedData[m] || 0),
16133
- backgroundColor: '#a855f7',
16134
- borderRadius: 3
16135
- },
16136
- {
16137
- label: 'Closed',
16138
- data: timelineMonths.map(m => closedData[m] || 0),
16139
- backgroundColor: '#484f58',
16140
- borderRadius: 3
16141
- }
16142
- ]
16143
- },
16144
- options: {
16145
- responsive: true,
16146
- maintainAspectRatio: false,
16147
- scales: {
16148
- x: { grid: { display: false } },
16149
- y: { grid: { color: 'rgba(48, 54, 61, 0.3)' }, beginAtZero: true, ticks: { stepSize: 1 } }
16150
- },
16151
- plugins: {
16152
- legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }
16153
- },
16154
- interaction: { intersect: false, mode: 'index' }
16155
- }
16156
- });`;
16157
- })()}
16079
+ <div class="pr-item ${itemClass}" data-status="${prStatus}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
16080
+ <div class="pr-status-indicator">
16081
+ ${hasIssues ? `
16082
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
16083
+ <circle cx="12" cy="12" r="10"/>
16084
+ <line x1="12" y1="8" x2="12" y2="12"/>
16085
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
16086
+ </svg>
16087
+ ` : `
16088
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
16089
+ <circle cx="12" cy="12" r="10"/>
16090
+ <line x1="12" y1="16" x2="12" y2="12"/>
16091
+ <line x1="12" y1="8" x2="12.01" y2="8"/>
16092
+ </svg>
16093
+ `}
16094
+ </div>
16095
+ <div class="pr-content">
16096
+ <div class="pr-title-row">
16097
+ <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
16098
+ <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
16099
+ </div>
16100
+ <div class="pr-badges">
16101
+ ${pr.ciStatus === "failing" ? '<span class="badge badge-ci-failing">CI Failing</span>' : ""}
16102
+ ${pr.ciStatus === "passing" ? '<span class="badge badge-passing">CI Passing</span>' : ""}
16103
+ ${pr.ciStatus === "pending" ? '<span class="badge badge-pending">CI Pending</span>' : ""}
16104
+ ${pr.hasMergeConflict ? '<span class="badge badge-conflict">Merge Conflict</span>' : ""}
16105
+ ${pr.hasUnrespondedComment && pr.status === "changes_addressed" ? '<span class="badge badge-changes-addressed">Changes Addressed</span>' : ""}
16106
+ ${pr.hasUnrespondedComment && pr.status !== "changes_addressed" && pr.status !== "failing_ci" ? '<span class="badge badge-needs-response">Needs Response</span>' : ""}
16107
+ ${pr.reviewDecision === "changes_requested" ? '<span class="badge badge-changes-requested">Changes Requested</span>' : ""}
16108
+ ${isStale ? `<span class="badge badge-stale">${pr.daysSinceActivity}d inactive</span>` : ""}
16109
+ </div>
16110
+ </div>
16111
+ <div class="pr-activity">
16112
+ ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
16113
+ </div>
16114
+ </div>`;
16115
+ }).join("")}
16116
+ </div>
16117
+ </section>
16118
+ ` : `
16119
+ <section class="pr-list-section">
16120
+ <div class="pr-list-header">
16121
+ <h2 class="pr-list-title">Active Pull Requests</h2>
16122
+ <span class="pr-count">0 open</span>
16123
+ </div>
16124
+ <div class="empty-state">
16125
+ <div class="empty-state-icon">
16126
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
16127
+ <circle cx="12" cy="12" r="10"/>
16128
+ <path d="M8 12h8"/>
16129
+ </svg>
16130
+ </div>
16131
+ <p>No active pull requests</p>
16132
+ </div>
16133
+ </section>
16134
+ `}
16135
+
16136
+ ${shelvedPRs.length > 0 ? `
16137
+ <section class="pr-list-section" style="margin-top: 1.25rem; opacity: 0.7;">
16138
+ <div class="pr-list-header">
16139
+ <h2 class="pr-list-title">Shelved Pull Requests</h2>
16140
+ <span class="pr-count" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${shelvedPRs.length} shelved</span>
16141
+ </div>
16142
+ <div class="pr-list">
16143
+ ${shelvedPRs.map(
16144
+ (pr) => `
16145
+ <div class="pr-item" data-status="shelved" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
16146
+ <div class="pr-status-indicator" style="background: rgba(110, 118, 129, 0.1); color: var(--text-muted);">
16147
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
16148
+ ${SVG_ICONS.box}
16149
+ </svg>
16150
+ </div>
16151
+ <div class="pr-content">
16152
+ <div class="pr-title-row">
16153
+ <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
16154
+ <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
16155
+ </div>
16156
+ <div class="pr-badges">
16157
+ <span class="badge badge-days">${pr.daysSinceActivity}d inactive</span>
16158
+ </div>
16159
+ </div>
16160
+ <div class="pr-activity">
16161
+ ${pr.daysSinceActivity === 0 ? "Today" : pr.daysSinceActivity === 1 ? "Yesterday" : pr.daysSinceActivity + "d ago"}
16162
+ </div>
16163
+ </div>`
16164
+ ).join("")}
16165
+ </div>
16166
+ </section>
16167
+ ` : ""}
16168
+
16169
+ <footer class="footer">
16170
+ <p>OSS Autopilot // Mission Control</p>
16171
+ <p style="margin-top: 0.25rem;">Dashboard generated: ${digest.generatedAt ? new Date(digest.generatedAt).toISOString() : "Unknown"}</p>
16172
+ </footer>
16173
+ </div>
16158
16174
 
16175
+ <script>
16176
+ ${generateDashboardScripts(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state)}
16159
16177
  </script>
16160
16178
  </body>
16161
16179
  </html>`;
@@ -16163,6 +16181,11 @@ function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpene
16163
16181
  var init_dashboard_templates = __esm({
16164
16182
  "src/commands/dashboard-templates.ts"() {
16165
16183
  "use strict";
16184
+ init_dashboard_formatters();
16185
+ init_dashboard_styles();
16186
+ init_dashboard_components();
16187
+ init_dashboard_scripts();
16188
+ init_dashboard_formatters();
16166
16189
  }
16167
16190
  });
16168
16191
 
@@ -16320,7 +16343,7 @@ async function startDashboardServer(options) {
16320
16343
  stateManager2.save();
16321
16344
  } catch (error) {
16322
16345
  console.error("Action failed:", body.action, body.url, error);
16323
- sendError(res, 500, `Action failed: ${error instanceof Error ? error.message : String(error)}`);
16346
+ sendError(res, 500, `Action failed: ${errorMessage(error)}`);
16324
16347
  return;
16325
16348
  }
16326
16349
  if (cachedDigest) {
@@ -16343,7 +16366,7 @@ async function startDashboardServer(options) {
16343
16366
  sendJson(res, 200, cachedJsonData);
16344
16367
  } catch (error) {
16345
16368
  console.error("Dashboard refresh failed:", error);
16346
- sendError(res, 500, `Refresh failed: ${error instanceof Error ? error.message : String(error)}`);
16369
+ sendError(res, 500, `Refresh failed: ${errorMessage(error)}`);
16347
16370
  }
16348
16371
  }
16349
16372
  function serveStaticFile(requestUrl, res) {
@@ -16464,6 +16487,7 @@ var init_dashboard_server = __esm({
16464
16487
  fs5 = __toESM(require("fs"), 1);
16465
16488
  path5 = __toESM(require("path"), 1);
16466
16489
  init_core();
16490
+ init_errors();
16467
16491
  init_dashboard_data();
16468
16492
  init_dashboard_templates();
16469
16493
  VALID_ACTIONS = /* @__PURE__ */ new Set(["shelve", "unshelve", "snooze", "unsnooze"]);
@@ -16512,7 +16536,7 @@ async function runDashboard(options) {
16512
16536
  digest = result.digest;
16513
16537
  commentedIssues = result.commentedIssues;
16514
16538
  } catch (error) {
16515
- console.error("Failed to fetch fresh data:", error instanceof Error ? error.message : error);
16539
+ console.error("Failed to fetch fresh data:", errorMessage(error));
16516
16540
  console.error("Falling back to cached data (issue conversations unavailable)...");
16517
16541
  digest = stateManager2.getState().lastDigest;
16518
16542
  }
@@ -16642,6 +16666,7 @@ var init_dashboard = __esm({
16642
16666
  path6 = __toESM(require("path"), 1);
16643
16667
  import_child_process2 = require("child_process");
16644
16668
  init_core();
16669
+ init_errors();
16645
16670
  init_json();
16646
16671
  init_dashboard_data();
16647
16672
  init_dashboard_templates();
@@ -16727,7 +16752,7 @@ async function runParseList(options) {
16727
16752
  try {
16728
16753
  content = fs7.readFileSync(filePath, "utf-8");
16729
16754
  } catch (error) {
16730
- const msg = error instanceof Error ? error.message : String(error);
16755
+ const msg = errorMessage(error);
16731
16756
  throw new Error(`Failed to read file: ${msg}`, { cause: error });
16732
16757
  }
16733
16758
  return parseIssueList(content);
@@ -16738,6 +16763,7 @@ var init_parse_list = __esm({
16738
16763
  "use strict";
16739
16764
  fs7 = __toESM(require("fs"), 1);
16740
16765
  path7 = __toESM(require("path"), 1);
16766
+ init_errors();
16741
16767
  }
16742
16768
  });
16743
16769
 
@@ -16782,7 +16808,7 @@ async function runCheckIntegration(options) {
16782
16808
  }).trim();
16783
16809
  newFiles = output ? output.split("\n").filter(Boolean) : [];
16784
16810
  } catch (error) {
16785
- const msg = error instanceof Error ? error.message : String(error);
16811
+ const msg = errorMessage(error);
16786
16812
  throw new Error(`Failed to run git diff: ${msg}`, { cause: error });
16787
16813
  }
16788
16814
  const codeFiles = newFiles.filter((f) => {
@@ -16826,24 +16852,24 @@ async function runCheckIntegration(options) {
16826
16852
  referencedBy.push(...matches);
16827
16853
  }
16828
16854
  } catch (error) {
16829
- const exitCode = error && typeof error === "object" && "status" in error ? error.status : null;
16830
- if (exitCode !== null && exitCode !== 1) {
16831
- const msg = error instanceof Error ? error.message : String(error);
16855
+ const exitCode = error && typeof error === "object" && "status" in error ? error.status : void 0;
16856
+ if (exitCode !== void 0 && exitCode !== 1) {
16857
+ const msg = errorMessage(error);
16832
16858
  debug("check-integration", `git grep failed for "${pattern}": ${msg}`);
16833
16859
  }
16834
16860
  }
16835
16861
  }
16836
16862
  referencedBy = [...new Set(referencedBy)];
16837
16863
  const isIntegrated = referencedBy.length > 0;
16838
- const info = {
16864
+ const info2 = {
16839
16865
  path: newFile,
16840
16866
  referencedBy,
16841
16867
  isIntegrated
16842
16868
  };
16843
16869
  if (!isIntegrated) {
16844
- info.suggestedEntryPoints = suggestEntryPoints(newFile, allFiles);
16870
+ info2.suggestedEntryPoints = suggestEntryPoints(newFile, allFiles);
16845
16871
  }
16846
- results.push(info);
16872
+ results.push(info2);
16847
16873
  }
16848
16874
  const unreferencedCount = results.filter((r) => !r.isIntegrated).length;
16849
16875
  return { newFiles: results, unreferencedCount };
@@ -16855,6 +16881,7 @@ var init_check_integration = __esm({
16855
16881
  path8 = __toESM(require("path"), 1);
16856
16882
  import_child_process3 = require("child_process");
16857
16883
  init_core();
16884
+ init_errors();
16858
16885
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
16859
16886
  ".ts",
16860
16887
  ".tsx",
@@ -16976,7 +17003,7 @@ async function runLocalRepos(options) {
16976
17003
  stateManager2.setLocalRepoCache({ repos, scanPaths, cachedAt });
16977
17004
  stateManager2.save();
16978
17005
  } catch (error) {
16979
- const msg = error instanceof Error ? error.message : String(error);
17006
+ const msg = errorMessage(error);
16980
17007
  debug("local-repos", `Failed to cache scan results: ${msg}`);
16981
17008
  }
16982
17009
  return {
@@ -16995,6 +17022,7 @@ var init_local_repos = __esm({
16995
17022
  os2 = __toESM(require("os"), 1);
16996
17023
  import_child_process4 = require("child_process");
16997
17024
  init_core();
17025
+ init_errors();
16998
17026
  DEFAULT_SCAN_PATHS = [
16999
17027
  path9.join(os2.homedir(), "Documents", "oss"),
17000
17028
  path9.join(os2.homedir(), "dev"),
@@ -17014,15 +17042,6 @@ __export(startup_exports, {
17014
17042
  parseIssueListPathFromConfig: () => parseIssueListPathFromConfig,
17015
17043
  runStartup: () => runStartup
17016
17044
  });
17017
- function getVersion() {
17018
- try {
17019
- const pkgPath = path10.join(path10.dirname(process.argv[1]), "..", "package.json");
17020
- return JSON.parse(fs9.readFileSync(pkgPath, "utf-8")).version;
17021
- } catch (error) {
17022
- console.error("[STARTUP] Failed to detect CLI version:", error instanceof Error ? error.message : error);
17023
- return "0.0.0";
17024
- }
17025
- }
17026
17045
  function parseIssueListPathFromConfig(configContent) {
17027
17046
  const match = configContent.match(/^---\n([\s\S]*?)\n---/);
17028
17047
  if (!match) return void 0;
@@ -17058,7 +17077,7 @@ function detectIssueList() {
17058
17077
  source = "configured";
17059
17078
  }
17060
17079
  } catch (error) {
17061
- console.error("[STARTUP] Failed to read config:", error instanceof Error ? error.message : error);
17080
+ console.error("[STARTUP] Failed to read config:", errorMessage(error));
17062
17081
  }
17063
17082
  }
17064
17083
  if (!issueListPath) {
@@ -17077,10 +17096,7 @@ function detectIssueList() {
17077
17096
  const { availableCount, completedCount } = countIssueListItems(content);
17078
17097
  return { path: issueListPath, source, availableCount, completedCount };
17079
17098
  } catch (error) {
17080
- console.error(
17081
- `[STARTUP] Failed to read issue list at ${issueListPath}:`,
17082
- error instanceof Error ? error.message : error
17083
- );
17099
+ console.error(`[STARTUP] Failed to read issue list at ${issueListPath}:`, errorMessage(error));
17084
17100
  return { path: issueListPath, source, availableCount: 0, completedCount: 0 };
17085
17101
  }
17086
17102
  }
@@ -17095,7 +17111,7 @@ function openInBrowser(filePath) {
17095
17111
  });
17096
17112
  }
17097
17113
  async function runStartup() {
17098
- const version = getVersion();
17114
+ const version = getCLIVersion();
17099
17115
  const stateManager2 = getStateManager();
17100
17116
  if (!stateManager2.isSetupComplete()) {
17101
17117
  return { version, setupComplete: false };
@@ -17118,7 +17134,7 @@ async function runStartup() {
17118
17134
  dashboardOpened = true;
17119
17135
  }
17120
17136
  } catch (error) {
17121
- console.error("[STARTUP] Dashboard generation failed:", error instanceof Error ? error.message : error);
17137
+ console.error("[STARTUP] Dashboard generation failed:", errorMessage(error));
17122
17138
  }
17123
17139
  if (dashboardOpened) {
17124
17140
  daily.briefSummary += " | Dashboard opened in browser";
@@ -17132,14 +17148,14 @@ async function runStartup() {
17132
17148
  issueList
17133
17149
  };
17134
17150
  }
17135
- var fs9, path10, import_child_process5;
17151
+ var fs9, import_child_process5;
17136
17152
  var init_startup = __esm({
17137
17153
  "src/commands/startup.ts"() {
17138
17154
  "use strict";
17139
17155
  fs9 = __toESM(require("fs"), 1);
17140
- path10 = __toESM(require("path"), 1);
17141
17156
  import_child_process5 = require("child_process");
17142
17157
  init_core();
17158
+ init_errors();
17143
17159
  init_daily();
17144
17160
  init_dashboard();
17145
17161
  }
@@ -17183,29 +17199,29 @@ var init_shelve = __esm({
17183
17199
  // src/commands/dismiss.ts
17184
17200
  var dismiss_exports = {};
17185
17201
  __export(dismiss_exports, {
17186
- ISSUE_URL_PATTERN: () => ISSUE_URL_PATTERN,
17202
+ ISSUE_OR_PR_URL_PATTERN: () => ISSUE_OR_PR_URL_PATTERN,
17187
17203
  runDismiss: () => runDismiss,
17188
17204
  runUndismiss: () => runUndismiss
17189
17205
  });
17190
17206
  async function runDismiss(options) {
17191
- validateUrl(options.issueUrl);
17192
- validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
17207
+ validateUrl(options.url);
17208
+ validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, "issue or PR");
17193
17209
  const stateManager2 = getStateManager();
17194
- const added = stateManager2.dismissIssue(options.issueUrl, (/* @__PURE__ */ new Date()).toISOString());
17210
+ const added = stateManager2.dismissIssue(options.url, (/* @__PURE__ */ new Date()).toISOString());
17195
17211
  if (added) {
17196
17212
  stateManager2.save();
17197
17213
  }
17198
- return { dismissed: added, url: options.issueUrl };
17214
+ return { dismissed: added, url: options.url };
17199
17215
  }
17200
17216
  async function runUndismiss(options) {
17201
- validateUrl(options.issueUrl);
17202
- validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
17217
+ validateUrl(options.url);
17218
+ validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, "issue or PR");
17203
17219
  const stateManager2 = getStateManager();
17204
- const removed = stateManager2.undismissIssue(options.issueUrl);
17220
+ const removed = stateManager2.undismissIssue(options.url);
17205
17221
  if (removed) {
17206
17222
  stateManager2.save();
17207
17223
  }
17208
- return { undismissed: removed, url: options.issueUrl };
17224
+ return { undismissed: removed, url: options.url };
17209
17225
  }
17210
17226
  var init_dismiss = __esm({
17211
17227
  "src/commands/dismiss.ts"() {
@@ -17282,17 +17298,18 @@ var {
17282
17298
 
17283
17299
  // src/cli.ts
17284
17300
  init_core();
17301
+ init_errors();
17285
17302
  init_json();
17286
17303
  function printRepos(repos) {
17287
17304
  const entries = Object.entries(repos).sort(([a], [b]) => a.localeCompare(b));
17288
- for (const [remote, info] of entries) {
17289
- const branch = info.currentBranch ? ` (${info.currentBranch})` : "";
17305
+ for (const [remote, info2] of entries) {
17306
+ const branch = info2.currentBranch ? ` (${info2.currentBranch})` : "";
17290
17307
  console.log(` ${remote}${branch}`);
17291
- console.log(` ${info.path}`);
17308
+ console.log(` ${info2.path}`);
17292
17309
  }
17293
17310
  }
17294
17311
  function handleCommandError(err, json) {
17295
- const msg = err instanceof Error ? err.message : String(err);
17312
+ const msg = errorMessage(err);
17296
17313
  if (json) {
17297
17314
  outputJsonError(msg);
17298
17315
  } else {
@@ -17300,16 +17317,7 @@ function handleCommandError(err, json) {
17300
17317
  }
17301
17318
  process.exit(1);
17302
17319
  }
17303
- var VERSION10 = (() => {
17304
- try {
17305
- const fs10 = require("fs");
17306
- const path11 = require("path");
17307
- const pkgPath = path11.join(path11.dirname(process.argv[1]), "..", "package.json");
17308
- return JSON.parse(fs10.readFileSync(pkgPath, "utf-8")).version;
17309
- } catch (_err) {
17310
- return "0.0.0";
17311
- }
17312
- })();
17320
+ var VERSION10 = getCLIVersion();
17313
17321
  var LOCAL_ONLY_COMMANDS = [
17314
17322
  "help",
17315
17323
  "status",
@@ -17701,8 +17709,8 @@ program2.command("parse-issue-list <path>").description("Parse a markdown issue
17701
17709
  if (options.json) {
17702
17710
  outputJson(data);
17703
17711
  } else {
17704
- const path11 = await import("path");
17705
- const resolvedPath = path11.resolve(filePath);
17712
+ const path10 = await import("path");
17713
+ const resolvedPath = path10.resolve(filePath);
17706
17714
  console.log(`
17707
17715
  \u{1F4CB} Issue List: ${resolvedPath}
17708
17716
  `);
@@ -17830,34 +17838,34 @@ program2.command("unshelve <pr-url>").description("Unshelve a PR (include in cap
17830
17838
  handleCommandError(err, options.json);
17831
17839
  }
17832
17840
  });
17833
- program2.command("dismiss <issue-url>").description("Dismiss issue reply notifications (resurfaces on new activity)").option("--json", "Output as JSON").action(async (issueUrl, options) => {
17841
+ program2.command("dismiss <url>").description("Dismiss notifications for an issue or PR (resurfaces on new activity)").option("--json", "Output as JSON").action(async (url, options) => {
17834
17842
  try {
17835
17843
  const { runDismiss: runDismiss2 } = await Promise.resolve().then(() => (init_dismiss(), dismiss_exports));
17836
- const data = await runDismiss2({ issueUrl });
17844
+ const data = await runDismiss2({ url });
17837
17845
  if (options.json) {
17838
17846
  outputJson(data);
17839
17847
  } else if (data.dismissed) {
17840
- console.log(`Dismissed: ${issueUrl}`);
17841
- console.log("Issue reply notifications are now muted.");
17848
+ console.log(`Dismissed: ${url}`);
17849
+ console.log("Notifications are now muted.");
17842
17850
  console.log("New responses after this point will resurface automatically.");
17843
17851
  } else {
17844
- console.log("Issue is already dismissed.");
17852
+ console.log("Already dismissed.");
17845
17853
  }
17846
17854
  } catch (err) {
17847
17855
  handleCommandError(err, options.json);
17848
17856
  }
17849
17857
  });
17850
- program2.command("undismiss <issue-url>").description("Undismiss an issue (re-enable reply notifications)").option("--json", "Output as JSON").action(async (issueUrl, options) => {
17858
+ program2.command("undismiss <url>").description("Undismiss an issue or PR (re-enable notifications)").option("--json", "Output as JSON").action(async (url, options) => {
17851
17859
  try {
17852
17860
  const { runUndismiss: runUndismiss2 } = await Promise.resolve().then(() => (init_dismiss(), dismiss_exports));
17853
- const data = await runUndismiss2({ issueUrl });
17861
+ const data = await runUndismiss2({ url });
17854
17862
  if (options.json) {
17855
17863
  outputJson(data);
17856
17864
  } else if (data.undismissed) {
17857
- console.log(`Undismissed: ${issueUrl}`);
17858
- console.log("Issue reply notifications are active again.");
17865
+ console.log(`Undismissed: ${url}`);
17866
+ console.log("Notifications are active again.");
17859
17867
  } else {
17860
- console.log("Issue was not dismissed.");
17868
+ console.log("Was not dismissed.");
17861
17869
  }
17862
17870
  } catch (err) {
17863
17871
  handleCommandError(err, options.json);