@oss-scout/mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35495,6 +35495,15 @@ function getCacheDir() {
35495
35495
  }
35496
35496
  return dir;
35497
35497
  }
35498
+ function extractRepoFromUrl(url2) {
35499
+ const apiMatch = url2.match(/api\.github\.com\/repos\/([^/]+\/[^/]+)/);
35500
+ if (apiMatch)
35501
+ return apiMatch[1];
35502
+ const webMatch = url2.match(/github\.com\/([^/]+\/[^/]+)/);
35503
+ if (webMatch)
35504
+ return webMatch[1];
35505
+ return null;
35506
+ }
35498
35507
  var OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
35499
35508
  var REPO_PATTERN = /^[a-zA-Z0-9_.-]+$/;
35500
35509
  function isValidOwnerRepo(owner, repo) {
@@ -35656,13 +35665,30 @@ function getSearchBudgetTracker() {
35656
35665
 
35657
35666
  // ../core/dist/core/types.js
35658
35667
  var SCOPE_LABELS = {
35659
- beginner: ["good first issue", "help wanted", "easy", "up-for-grabs", "first-timers-only", "beginner"],
35660
- intermediate: ["enhancement", "feature", "feature-request", "contributions welcome"],
35668
+ beginner: [
35669
+ "good first issue",
35670
+ "help wanted",
35671
+ "easy",
35672
+ "up-for-grabs",
35673
+ "first-timers-only",
35674
+ "beginner"
35675
+ ],
35676
+ intermediate: [
35677
+ "enhancement",
35678
+ "feature",
35679
+ "feature-request",
35680
+ "contributions welcome"
35681
+ ],
35661
35682
  advanced: ["proposal", "RFC", "accepted", "design"]
35662
35683
  };
35663
35684
 
35664
35685
  // ../core/dist/core/schemas.js
35665
- var IssueStatusSchema = external_exports3.enum(["candidate", "claimed", "in_progress", "pr_submitted"]);
35686
+ var IssueStatusSchema = external_exports3.enum([
35687
+ "candidate",
35688
+ "claimed",
35689
+ "in_progress",
35690
+ "pr_submitted"
35691
+ ]);
35666
35692
  var ProjectCategorySchema = external_exports3.enum([
35667
35693
  "nonprofit",
35668
35694
  "devtools",
@@ -35671,9 +35697,24 @@ var ProjectCategorySchema = external_exports3.enum([
35671
35697
  "data-ml",
35672
35698
  "education"
35673
35699
  ]);
35674
- var IssueScopeSchema = external_exports3.enum(["beginner", "intermediate", "advanced"]);
35675
- var SearchStrategySchema = external_exports3.enum(["merged", "orgs", "starred", "broad", "maintained", "all"]);
35676
- var CONCRETE_STRATEGIES = ["merged", "orgs", "starred", "broad", "maintained"];
35700
+ var IssueScopeSchema = external_exports3.enum([
35701
+ "beginner",
35702
+ "intermediate",
35703
+ "advanced"
35704
+ ]);
35705
+ var SearchStrategySchema = external_exports3.enum([
35706
+ "merged",
35707
+ "starred",
35708
+ "broad",
35709
+ "maintained",
35710
+ "all"
35711
+ ]);
35712
+ var CONCRETE_STRATEGIES = [
35713
+ "merged",
35714
+ "starred",
35715
+ "broad",
35716
+ "maintained"
35717
+ ];
35677
35718
  var RepoSignalsSchema = external_exports3.object({
35678
35719
  hasActiveMaintainers: external_exports3.boolean(),
35679
35720
  isResponsive: external_exports3.boolean(),
@@ -35742,6 +35783,13 @@ var TrackedIssueSchema = external_exports3.object({
35742
35783
  vetted: external_exports3.boolean(),
35743
35784
  vettingResult: IssueVettingResultSchema.optional()
35744
35785
  });
35786
+ var SkippedIssueSchema = external_exports3.object({
35787
+ url: external_exports3.string(),
35788
+ repo: external_exports3.string(),
35789
+ number: external_exports3.number(),
35790
+ title: external_exports3.string(),
35791
+ skippedAt: external_exports3.string()
35792
+ });
35745
35793
  var SavedCandidateSchema = external_exports3.object({
35746
35794
  issueUrl: external_exports3.string(),
35747
35795
  repo: external_exports3.string(),
@@ -35758,13 +35806,12 @@ var SavedCandidateSchema = external_exports3.object({
35758
35806
  var PersistenceModeSchema = external_exports3.enum(["local", "gist"]);
35759
35807
  var ScoutPreferencesSchema = external_exports3.object({
35760
35808
  githubUsername: external_exports3.string().default(""),
35761
- languages: external_exports3.array(external_exports3.string()).default(["typescript", "javascript"]),
35809
+ languages: external_exports3.array(external_exports3.string()).default(["any"]),
35762
35810
  labels: external_exports3.array(external_exports3.string()).default(["good first issue", "help wanted"]),
35763
35811
  scope: external_exports3.array(IssueScopeSchema).optional(),
35764
35812
  excludeRepos: external_exports3.array(external_exports3.string()).default([]),
35765
35813
  excludeOrgs: external_exports3.array(external_exports3.string()).default([]),
35766
35814
  aiPolicyBlocklist: external_exports3.array(external_exports3.string()).default(["matplotlib/matplotlib"]),
35767
- preferredOrgs: external_exports3.array(external_exports3.string()).default([]),
35768
35815
  projectCategories: external_exports3.array(ProjectCategorySchema).default([]),
35769
35816
  minStars: external_exports3.number().default(50),
35770
35817
  maxIssueAgeDays: external_exports3.number().default(90),
@@ -35782,13 +35829,19 @@ var ScoutStateSchema = external_exports3.object({
35782
35829
  mergedPRs: external_exports3.array(StoredMergedPRSchema).default([]),
35783
35830
  closedPRs: external_exports3.array(StoredClosedPRSchema).default([]),
35784
35831
  savedResults: external_exports3.array(SavedCandidateSchema).default([]),
35832
+ skippedIssues: external_exports3.array(SkippedIssueSchema).default([]),
35785
35833
  lastSearchAt: external_exports3.string().optional(),
35786
35834
  lastRunAt: external_exports3.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
35787
35835
  gistId: external_exports3.string().optional()
35788
35836
  });
35789
35837
 
35790
35838
  // ../core/dist/core/issue-filtering.js
35791
- var DOC_ONLY_LABELS = /* @__PURE__ */ new Set(["documentation", "docs", "typo", "spelling"]);
35839
+ var DOC_ONLY_LABELS = /* @__PURE__ */ new Set([
35840
+ "documentation",
35841
+ "docs",
35842
+ "typo",
35843
+ "spelling"
35844
+ ]);
35792
35845
  function isDocOnlyIssue(item) {
35793
35846
  if (!item.labels || !Array.isArray(item.labels) || item.labels.length === 0)
35794
35847
  return false;
@@ -35827,7 +35880,9 @@ function detectLabelFarmingRepos(items) {
35827
35880
  const spamRepos = /* @__PURE__ */ new Set();
35828
35881
  const repoSpamCounts = /* @__PURE__ */ new Map();
35829
35882
  for (const item of items) {
35830
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
35883
+ const repoFullName = extractRepoFromUrl(item.repository_url);
35884
+ if (!repoFullName)
35885
+ continue;
35831
35886
  if (isLabelFarming(item)) {
35832
35887
  spamRepos.add(repoFullName);
35833
35888
  continue;
@@ -35913,20 +35968,111 @@ function calculateViabilityScore(params) {
35913
35968
 
35914
35969
  // ../core/dist/core/category-mapping.js
35915
35970
  var CATEGORY_TOPICS = {
35916
- nonprofit: ["nonprofit", "social-good", "humanitarian", "charity", "social-impact", "civic-tech"],
35917
- devtools: ["developer-tools", "devtools", "cli", "sdk", "linter", "formatter", "build-tool"],
35918
- infrastructure: ["infrastructure", "cloud", "kubernetes", "docker", "devops", "monitoring", "observability"],
35919
- "web-frameworks": ["web-framework", "frontend", "backend", "fullstack", "nextjs", "react", "vue"],
35920
- "data-ml": ["machine-learning", "data-science", "deep-learning", "nlp", "data-pipeline", "analytics"],
35921
- education: ["education", "learning", "tutorial", "courseware", "edtech", "teaching"]
35971
+ nonprofit: [
35972
+ "nonprofit",
35973
+ "social-good",
35974
+ "humanitarian",
35975
+ "charity",
35976
+ "social-impact",
35977
+ "civic-tech"
35978
+ ],
35979
+ devtools: [
35980
+ "developer-tools",
35981
+ "devtools",
35982
+ "cli",
35983
+ "sdk",
35984
+ "linter",
35985
+ "formatter",
35986
+ "build-tool"
35987
+ ],
35988
+ infrastructure: [
35989
+ "infrastructure",
35990
+ "cloud",
35991
+ "kubernetes",
35992
+ "docker",
35993
+ "devops",
35994
+ "monitoring",
35995
+ "observability"
35996
+ ],
35997
+ "web-frameworks": [
35998
+ "web-framework",
35999
+ "frontend",
36000
+ "backend",
36001
+ "fullstack",
36002
+ "nextjs",
36003
+ "react",
36004
+ "vue"
36005
+ ],
36006
+ "data-ml": [
36007
+ "machine-learning",
36008
+ "data-science",
36009
+ "deep-learning",
36010
+ "nlp",
36011
+ "data-pipeline",
36012
+ "analytics"
36013
+ ],
36014
+ education: [
36015
+ "education",
36016
+ "learning",
36017
+ "tutorial",
36018
+ "courseware",
36019
+ "edtech",
36020
+ "teaching"
36021
+ ]
35922
36022
  };
35923
36023
  var CATEGORY_ORGS = {
35924
- nonprofit: ["code-for-america", "opengovfoundation", "ushahidi", "hotosm", "openfn", "democracyearth"],
35925
- devtools: ["eslint", "prettier", "vitejs", "biomejs", "oxc-project", "ast-grep", "turbot"],
35926
- infrastructure: ["kubernetes", "hashicorp", "grafana", "prometheus", "open-telemetry", "envoyproxy", "cncf"],
35927
- "web-frameworks": ["vercel", "remix-run", "sveltejs", "nuxt", "astro", "redwoodjs", "blitz-js"],
35928
- "data-ml": ["huggingface", "mlflow", "apache", "dbt-labs", "dagster-io", "prefecthq", "langchain-ai"],
35929
- education: ["freeCodeCamp", "TheOdinProject", "exercism", "codecademy", "oppia", "Khan"]
36024
+ nonprofit: [
36025
+ "code-for-america",
36026
+ "opengovfoundation",
36027
+ "ushahidi",
36028
+ "hotosm",
36029
+ "openfn",
36030
+ "democracyearth"
36031
+ ],
36032
+ devtools: [
36033
+ "eslint",
36034
+ "prettier",
36035
+ "vitejs",
36036
+ "biomejs",
36037
+ "oxc-project",
36038
+ "ast-grep",
36039
+ "turbot"
36040
+ ],
36041
+ infrastructure: [
36042
+ "kubernetes",
36043
+ "hashicorp",
36044
+ "grafana",
36045
+ "prometheus",
36046
+ "open-telemetry",
36047
+ "envoyproxy",
36048
+ "cncf"
36049
+ ],
36050
+ "web-frameworks": [
36051
+ "vercel",
36052
+ "remix-run",
36053
+ "sveltejs",
36054
+ "nuxt",
36055
+ "astro",
36056
+ "redwoodjs",
36057
+ "blitz-js"
36058
+ ],
36059
+ "data-ml": [
36060
+ "huggingface",
36061
+ "mlflow",
36062
+ "apache",
36063
+ "dbt-labs",
36064
+ "dagster-io",
36065
+ "prefecthq",
36066
+ "langchain-ai"
36067
+ ],
36068
+ education: [
36069
+ "freeCodeCamp",
36070
+ "TheOdinProject",
36071
+ "exercism",
36072
+ "codecademy",
36073
+ "oppia",
36074
+ "Khan"
36075
+ ]
35930
36076
  };
35931
36077
  function repoBelongsToCategory(repoFullName, categories) {
35932
36078
  if (categories.length === 0)
@@ -36046,7 +36192,10 @@ var HttpCache = class {
36046
36192
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
36047
36193
  };
36048
36194
  try {
36049
- fs2.writeFileSync(this.pathFor(url2), JSON.stringify(entry), { encoding: "utf-8", mode: 384 });
36195
+ fs2.writeFileSync(this.pathFor(url2), JSON.stringify(entry), {
36196
+ encoding: "utf-8",
36197
+ mode: 384
36198
+ });
36050
36199
  debug(MODULE4, `Cached response for ${url2}`);
36051
36200
  } catch (err) {
36052
36201
  warn(MODULE4, `Failed to write cache for ${url2}: ${errorMessage(err)}`);
@@ -36301,7 +36450,12 @@ function analyzeRequirements(body) {
36301
36450
  const hasSteps = /\d\.|[-*]\s/.test(body);
36302
36451
  const hasCodeBlock = /```/.test(body);
36303
36452
  const hasExpectedBehavior = /expect|should|must|want/i.test(body);
36304
- const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
36453
+ const indicators = [
36454
+ hasSteps,
36455
+ hasCodeBlock,
36456
+ hasExpectedBehavior,
36457
+ body.length > 200
36458
+ ];
36305
36459
  return indicators.filter(Boolean).length >= 2;
36306
36460
  }
36307
36461
 
@@ -36378,7 +36532,12 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
36378
36532
  if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
36379
36533
  return cached2.guidelines;
36380
36534
  }
36381
- const filesToCheck = ["CONTRIBUTING.md", ".github/CONTRIBUTING.md", "docs/CONTRIBUTING.md", "contributing.md"];
36535
+ const filesToCheck = [
36536
+ "CONTRIBUTING.md",
36537
+ ".github/CONTRIBUTING.md",
36538
+ "docs/CONTRIBUTING.md",
36539
+ "contributing.md"
36540
+ ];
36382
36541
  const results = await Promise.allSettled(filesToCheck.map((file2) => octokit.repos.getContent({ owner, repo, path: file2 }).then(({ data }) => {
36383
36542
  if ("content" in data) {
36384
36543
  return Buffer.from(data.content, "base64").toString("utf-8");
@@ -36400,7 +36559,10 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
36400
36559
  }
36401
36560
  }
36402
36561
  }
36403
- guidelinesCache.set(cacheKey, { guidelines: void 0, fetchedAt: Date.now() });
36562
+ guidelinesCache.set(cacheKey, {
36563
+ guidelines: void 0,
36564
+ fetchedAt: Date.now()
36565
+ });
36404
36566
  pruneCache();
36405
36567
  return void 0;
36406
36568
  }
@@ -36603,12 +36765,9 @@ var IssueVetter = class {
36603
36765
  matchesPreferredCategory: matchesCategory
36604
36766
  });
36605
36767
  const starredRepos = this.stateReader.getStarredRepos();
36606
- const preferredOrgs = this.stateReader.getPreferredOrgs();
36607
36768
  let searchPriority = "normal";
36608
36769
  if (effectiveMergedCount > 0) {
36609
36770
  searchPriority = "merged_pr";
36610
- } else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
36611
- searchPriority = "preferred_org";
36612
36771
  } else if (starredRepos.includes(repoFullName)) {
36613
36772
  searchPriority = "starred";
36614
36773
  }
@@ -36662,7 +36821,11 @@ var IssueVetter = class {
36662
36821
  if (allFailed) {
36663
36822
  warn(MODULE7, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
36664
36823
  }
36665
- return { candidates: candidates.slice(0, maxResults), allFailed, rateLimitHit: rateLimitFailures > 0 };
36824
+ return {
36825
+ candidates: candidates.slice(0, maxResults),
36826
+ allFailed,
36827
+ rateLimitHit: rateLimitFailures > 0
36828
+ };
36666
36829
  }
36667
36830
  };
36668
36831
 
@@ -36763,11 +36926,13 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
36763
36926
  async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
36764
36927
  const spamRepos = detectLabelFarmingRepos(items);
36765
36928
  if (spamRepos.size > 0) {
36766
- const spamCount = items.filter((i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))).length;
36929
+ const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
36767
36930
  debug(MODULE8, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
36768
36931
  }
36769
36932
  const itemsToVet = filterIssues(items).filter((item) => {
36770
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
36933
+ const repoFullName = extractRepoFromUrl(item.repository_url);
36934
+ if (!repoFullName)
36935
+ return false;
36771
36936
  if (spamRepos.has(repoFullName))
36772
36937
  return false;
36773
36938
  return excludedRepoSets.every((s) => !s.has(repoFullName));
@@ -36835,6 +37000,152 @@ var MODULE9 = "issue-discovery";
36835
37000
  var INTER_PHASE_DELAY_MS = 2e3;
36836
37001
  var LOW_BUDGET_THRESHOLD = 20;
36837
37002
  var CRITICAL_BUDGET_THRESHOLD = 10;
37003
+ function buildIssueFilter(config2) {
37004
+ return (items) => {
37005
+ return items.filter((item) => {
37006
+ const repoFullName = extractRepoFromUrl(item.repository_url);
37007
+ if (!repoFullName)
37008
+ return false;
37009
+ if (config2.excludedRepos.has(repoFullName))
37010
+ return false;
37011
+ if (config2.excludeOrgs.size > 0) {
37012
+ const orgName = repoFullName.split("/")[0]?.toLowerCase();
37013
+ if (orgName && config2.excludeOrgs.has(orgName))
37014
+ return false;
37015
+ }
37016
+ if (config2.aiBlocklisted.has(repoFullName))
37017
+ return false;
37018
+ if (config2.lowScoringRepos.has(repoFullName))
37019
+ return false;
37020
+ if (config2.skippedUrls.has(item.html_url))
37021
+ return false;
37022
+ const updatedAt = new Date(item.updated_at);
37023
+ const ageDays = daysBetween(updatedAt, config2.now);
37024
+ if (ageDays > config2.maxAgeDays)
37025
+ return false;
37026
+ if (!config2.includeDocIssues && isDocOnlyIssue(item))
37027
+ return false;
37028
+ return true;
37029
+ });
37030
+ };
37031
+ }
37032
+ async function runPhase0(octokit, vetter, repos, baseQualifiers, maxResults, filterIssues) {
37033
+ info(MODULE9, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
37034
+ const { candidates, allBatchesFailed, rateLimitHit } = await searchInRepos(octokit, vetter, repos, baseQualifiers, [], maxResults, "merged_pr", filterIssues);
37035
+ info(MODULE9, `Found ${candidates.length} candidates from merged-PR repos`);
37036
+ return {
37037
+ candidates,
37038
+ error: allBatchesFailed ? "All merged-PR repo batches failed" : null,
37039
+ rateLimitHit
37040
+ };
37041
+ }
37042
+ async function runPhase1(octokit, vetter, repos, baseQualifiers, labels, maxResults, filterIssues) {
37043
+ info(MODULE9, `Phase 1: Searching issues in ${repos.length} starred repos...`);
37044
+ const phase1Labels = labels.slice(0, 3);
37045
+ const { candidates, allBatchesFailed, rateLimitHit } = await searchInRepos(octokit, vetter, repos.slice(0, 10), baseQualifiers, phase1Labels, maxResults, "starred", filterIssues);
37046
+ info(MODULE9, `Found ${candidates.length} candidates from starred repos`);
37047
+ return {
37048
+ candidates,
37049
+ error: allBatchesFailed ? "All starred repo batches failed" : null,
37050
+ rateLimitHit
37051
+ };
37052
+ }
37053
+ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQualifiers, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37054
+ info(MODULE9, "Phase 2: General issue search...");
37055
+ const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37056
+ const tierLabelGroups = [];
37057
+ if (scopes && scopes.length > 1) {
37058
+ for (const scope of scopes) {
37059
+ const scopeLabels = SCOPE_LABELS[scope] ?? [];
37060
+ if (scopeLabels.length === 0) {
37061
+ warn(MODULE9, `Scope "${scope}" has no labels, skipping tier`);
37062
+ continue;
37063
+ }
37064
+ tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
37065
+ }
37066
+ const allScopeLabels = new Set(scopes.flatMap((s) => SCOPE_LABELS[s] ?? []));
37067
+ const customOnly = configLabels.filter((l) => !allScopeLabels.has(l));
37068
+ if (customOnly.length > 0) {
37069
+ tierLabelGroups.push({ tier: "custom", tierLabels: customOnly });
37070
+ }
37071
+ } else {
37072
+ tierLabelGroups.push({ tier: "general", tierLabels: labels });
37073
+ }
37074
+ const budgetPerTier = Math.ceil(maxResults / tierLabelGroups.length);
37075
+ const tierResults = [];
37076
+ let error48 = null;
37077
+ let rateLimitHit = false;
37078
+ for (const { tier, tierLabels } of tierLabelGroups) {
37079
+ try {
37080
+ const allItems = await searchWithChunkedLabels(octokit, tierLabels, 0, (labelQ) => `${baseQualifiers} ${labelQ}`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
37081
+ info(MODULE9, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
37082
+ const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
37083
+ tierResults.push(tierCandidates);
37084
+ for (const c of tierCandidates)
37085
+ seenRepos.add(c.issue.repo);
37086
+ if (allVetFailed) {
37087
+ error48 = (error48 ? error48 + "; " : "") + `${tier}: all vetting failed`;
37088
+ }
37089
+ if (vetRateLimitHit) {
37090
+ rateLimitHit = true;
37091
+ }
37092
+ info(MODULE9, `Found ${tierCandidates.length} candidates from ${tier} tier`);
37093
+ } catch (err) {
37094
+ if (getHttpStatusCode(err) === 401)
37095
+ throw err;
37096
+ const errMsg = errorMessage(err);
37097
+ error48 = (error48 ? error48 + "; " : "") + `${tier}: ${errMsg}`;
37098
+ if (isRateLimitError(err)) {
37099
+ rateLimitHit = true;
37100
+ }
37101
+ warn(MODULE9, `Error in ${tier} tier search: ${errMsg}`);
37102
+ tierResults.push([]);
37103
+ }
37104
+ }
37105
+ const interleaved = interleaveArrays(tierResults);
37106
+ if (interleaved.length === 0 && error48) {
37107
+ warn(MODULE9, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
37108
+ }
37109
+ return {
37110
+ candidates: interleaved.slice(0, maxResults),
37111
+ error: error48,
37112
+ rateLimitHit
37113
+ };
37114
+ }
37115
+ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37116
+ info(MODULE9, "Phase 3: Searching actively maintained repos...");
37117
+ const thirtyDaysAgo = /* @__PURE__ */ new Date();
37118
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
37119
+ const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
37120
+ const categoryTopics = getTopicsForCategories(projectCategories);
37121
+ const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : "";
37122
+ const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
37123
+ try {
37124
+ const data = await cachedSearchIssues(octokit, {
37125
+ q: phase3Query,
37126
+ sort: "updated",
37127
+ order: "desc",
37128
+ per_page: maxResults * 3
37129
+ });
37130
+ info(MODULE9, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
37131
+ const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37132
+ const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
37133
+ info(MODULE9, `Found ${candidates.length} candidates from maintained-repo search`);
37134
+ return {
37135
+ candidates,
37136
+ error: allVetFailed ? "all vetting failed" : null,
37137
+ rateLimitHit: vetRateLimitHit
37138
+ };
37139
+ } catch (error48) {
37140
+ const errMsg = errorMessage(error48);
37141
+ warn(MODULE9, `Error in maintained-repo search: ${errMsg}`);
37142
+ return {
37143
+ candidates: [],
37144
+ error: errMsg,
37145
+ rateLimitHit: isRateLimitError(error48)
37146
+ };
37147
+ }
37148
+ }
36838
37149
  var IssueDiscovery = class {
36839
37150
  preferences;
36840
37151
  stateReader;
@@ -36864,8 +37175,8 @@ var IssueDiscovery = class {
36864
37175
  }
36865
37176
  /**
36866
37177
  * Search for issues matching our criteria.
36867
- * Searches in priority order: merged-PR repos first (no label filter), then preferred
36868
- * organizations, then starred repos, then general search, then actively maintained repos.
37178
+ * Searches in priority order: merged-PR repos first (no label filter), then starred
37179
+ * repos, then general search, then actively maintained repos.
36869
37180
  * Filters out issues from low-scoring and excluded repos.
36870
37181
  *
36871
37182
  * @param options - Search configuration
@@ -36898,8 +37209,7 @@ var IssueDiscovery = class {
36898
37209
  const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
36899
37210
  const strategiesUsed = [];
36900
37211
  const allCandidates = [];
36901
- let phase0Error = null;
36902
- let phase1Error = null;
37212
+ const phaseErrors = {};
36903
37213
  let rateLimitHitDuringSearch = false;
36904
37214
  this.rateLimitWarning = null;
36905
37215
  const tracker = getSearchBudgetTracker();
@@ -36919,235 +37229,82 @@ var IssueDiscovery = class {
36919
37229
  info(MODULE9, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
36920
37230
  }
36921
37231
  } catch (error48) {
36922
- if (getHttpStatusCode(error48) === 401) {
37232
+ if (getHttpStatusCode(error48) === 401)
36923
37233
  throw error48;
36924
- }
36925
37234
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
36926
37235
  warn(MODULE9, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
36927
37236
  }
37237
+ if (searchBudget <= 0) {
37238
+ this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
37239
+ return { candidates: [], strategiesUsed: [] };
37240
+ }
36928
37241
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
36929
37242
  const starredRepos = this.getStarredRepos();
36930
37243
  const starredRepoSet = new Set(starredRepos);
36931
- const minRepoScoreThreshold = config2.minRepoScoreThreshold;
36932
- const lowScoringRepos = new Set(this.deriveLowScoringRepos(minRepoScoreThreshold));
36933
- const excludedRepos = new Set(config2.excludeRepos);
36934
- const excludeOrgs = new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase()));
36935
- const maxAgeDays = config2.maxIssueAgeDays || 90;
36936
- const now = /* @__PURE__ */ new Date();
37244
+ const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
36937
37245
  const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
36938
37246
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
36939
37247
  const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`.replace(/ +/g, " ").trim();
36940
- const includeDocIssues = config2.includeDocIssues ?? true;
36941
37248
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
36942
37249
  if (aiBlocklisted.size > 0) {
36943
37250
  debug(MODULE9, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
36944
37251
  }
36945
- const filterIssues = (items) => {
36946
- return items.filter((item) => {
36947
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
36948
- if (excludedRepos.has(repoFullName))
36949
- return false;
36950
- if (excludeOrgs.size > 0) {
36951
- const orgName = repoFullName.split("/")[0]?.toLowerCase();
36952
- if (orgName && excludeOrgs.has(orgName))
36953
- return false;
36954
- }
36955
- if (aiBlocklisted.has(repoFullName))
36956
- return false;
36957
- if (lowScoringRepos.has(repoFullName))
36958
- return false;
36959
- const updatedAt = new Date(item.updated_at);
36960
- const ageDays = daysBetween(updatedAt, now);
36961
- if (ageDays > maxAgeDays)
36962
- return false;
36963
- if (!includeDocIssues && isDocOnlyIssue(item))
36964
- return false;
36965
- return true;
36966
- });
36967
- };
37252
+ const filterIssues = buildIssueFilter({
37253
+ excludedRepos: new Set(config2.excludeRepos),
37254
+ excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
37255
+ aiBlocklisted,
37256
+ lowScoringRepos,
37257
+ skippedUrls: options.skippedUrls ?? /* @__PURE__ */ new Set(),
37258
+ maxAgeDays: config2.maxIssueAgeDays || 90,
37259
+ now: /* @__PURE__ */ new Date(),
37260
+ includeDocIssues: config2.includeDocIssues ?? true
37261
+ });
36968
37262
  const phase0Repos = mergedPRRepos.slice(0, 10);
36969
37263
  const phase0RepoSet = new Set(phase0Repos);
36970
37264
  if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
36971
- info(MODULE9, `Phase 0: Searching issues in ${phase0Repos.length} merged-PR repos (no label filter)...`);
36972
- const remainingNeeded = maxResults - allCandidates.length;
36973
- if (remainingNeeded > 0) {
36974
- const { candidates: mergedCandidates, allBatchesFailed, rateLimitHit } = await searchInRepos(this.octokit, this.vetter, phase0Repos, baseQualifiers, [], remainingNeeded, "merged_pr", filterIssues);
36975
- allCandidates.push(...mergedCandidates);
36976
- if (allBatchesFailed) {
36977
- phase0Error = "All merged-PR repo batches failed";
36978
- }
36979
- if (rateLimitHit) {
37265
+ const remaining = maxResults - allCandidates.length;
37266
+ if (remaining > 0) {
37267
+ const result = await runPhase0(this.octokit, this.vetter, phase0Repos, baseQualifiers, remaining, filterIssues);
37268
+ allCandidates.push(...result.candidates);
37269
+ phaseErrors["0"] = result.error;
37270
+ if (result.rateLimitHit)
36980
37271
  rateLimitHitDuringSearch = true;
36981
- }
36982
- info(MODULE9, `Found ${mergedCandidates.length} candidates from merged-PR repos`);
36983
37272
  }
36984
37273
  strategiesUsed.push("merged");
36985
37274
  }
36986
- let phase0_5Error = null;
36987
- const preferredOrgs = config2.preferredOrgs ?? [];
36988
- if (allCandidates.length < maxResults && preferredOrgs.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("orgs")) {
36989
- if (phase0Repos.length > 0)
36990
- await sleep(INTER_PHASE_DELAY_MS);
36991
- const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
36992
- const orgsToSearch = preferredOrgs.filter((org) => !phase0Orgs.has(org.toLowerCase())).slice(0, 5);
36993
- if (orgsToSearch.length > 0) {
36994
- info(MODULE9, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
36995
- const remainingNeeded = maxResults - allCandidates.length;
36996
- const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(" OR ");
36997
- const orgOps = orgsToSearch.length - 1;
36998
- try {
36999
- const allItems = await searchWithChunkedLabels(this.octokit, labels, orgOps, (labelQ) => `${baseQualifiers} ${labelQ} (${orgRepoFilter})`.replace(/ +/g, " ").trim(), remainingNeeded * 3);
37000
- if (allItems.length > 0) {
37001
- const filtered = filterIssues(allItems).filter((item) => {
37002
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
37003
- return !phase0RepoSet.has(repoFullName);
37004
- });
37005
- const { candidates: orgCandidates, allFailed: allVetFailed, rateLimitHit } = await this.vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((i) => i.html_url), remainingNeeded, "preferred_org");
37006
- allCandidates.push(...orgCandidates);
37007
- if (allVetFailed) {
37008
- phase0_5Error = "All preferred org issue vetting failed";
37009
- }
37010
- if (rateLimitHit) {
37011
- rateLimitHitDuringSearch = true;
37012
- }
37013
- info(MODULE9, `Found ${orgCandidates.length} candidates from preferred orgs`);
37014
- }
37015
- } catch (error48) {
37016
- const errMsg = errorMessage(error48);
37017
- phase0_5Error = errMsg;
37018
- if (isRateLimitError(error48)) {
37019
- rateLimitHitDuringSearch = true;
37020
- }
37021
- warn(MODULE9, `Error searching preferred orgs: ${errMsg}`);
37022
- }
37023
- }
37024
- strategiesUsed.push("orgs");
37025
- }
37026
37275
  if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
37027
37276
  await sleep(INTER_PHASE_DELAY_MS);
37028
37277
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
37029
37278
  if (reposToSearch.length > 0) {
37030
- info(MODULE9, `Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
37031
- const remainingNeeded = maxResults - allCandidates.length;
37032
- if (remainingNeeded > 0) {
37033
- const phase1Labels = labels.slice(0, 3);
37034
- const { candidates: starredCandidates, allBatchesFailed, rateLimitHit } = await searchInRepos(this.octokit, this.vetter, reposToSearch.slice(0, 10), baseQualifiers, phase1Labels, remainingNeeded, "starred", filterIssues);
37035
- allCandidates.push(...starredCandidates);
37036
- if (allBatchesFailed) {
37037
- phase1Error = "All starred repo batches failed";
37038
- }
37039
- if (rateLimitHit) {
37279
+ const remaining = maxResults - allCandidates.length;
37280
+ if (remaining > 0) {
37281
+ const result = await runPhase1(this.octokit, this.vetter, reposToSearch, baseQualifiers, labels, remaining, filterIssues);
37282
+ allCandidates.push(...result.candidates);
37283
+ phaseErrors["1"] = result.error;
37284
+ if (result.rateLimitHit)
37040
37285
  rateLimitHitDuringSearch = true;
37041
- }
37042
- info(MODULE9, `Found ${starredCandidates.length} candidates from starred repos`);
37043
37286
  }
37044
37287
  }
37045
37288
  strategiesUsed.push("starred");
37046
37289
  }
37047
- let phase2Error = null;
37048
37290
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
37049
37291
  await sleep(INTER_PHASE_DELAY_MS);
37050
- info(MODULE9, "Phase 2: General issue search...");
37051
- const remainingNeeded = maxResults - allCandidates.length;
37052
- const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
37053
- const tierLabelGroups = [];
37054
- if (scopes && scopes.length > 1) {
37055
- for (const scope of scopes) {
37056
- const scopeLabels = SCOPE_LABELS[scope] ?? [];
37057
- if (scopeLabels.length === 0) {
37058
- warn(MODULE9, `Scope "${scope}" has no labels, skipping tier`);
37059
- continue;
37060
- }
37061
- tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
37062
- }
37063
- const allScopeLabels = new Set(scopes.flatMap((s) => SCOPE_LABELS[s] ?? []));
37064
- const customOnly = config2.labels.filter((l) => !allScopeLabels.has(l));
37065
- if (customOnly.length > 0) {
37066
- tierLabelGroups.push({ tier: "custom", tierLabels: customOnly });
37067
- }
37068
- } else {
37069
- tierLabelGroups.push({ tier: "general", tierLabels: labels });
37070
- }
37071
- const budgetPerTier = Math.ceil(remainingNeeded / tierLabelGroups.length);
37072
- const tierResults = [];
37073
- for (const { tier, tierLabels } of tierLabelGroups) {
37074
- try {
37075
- const allItems = await searchWithChunkedLabels(
37076
- this.octokit,
37077
- tierLabels,
37078
- 0,
37079
- // no repo/org ORs in Phase 2
37080
- (labelQ) => `${baseQualifiers} ${labelQ}`.replace(/ +/g, " ").trim(),
37081
- budgetPerTier * 3
37082
- );
37083
- info(MODULE9, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
37084
- const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(this.vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
37085
- tierResults.push(tierCandidates);
37086
- for (const c of tierCandidates)
37087
- seenRepos.add(c.issue.repo);
37088
- if (allVetFailed) {
37089
- phase2Error = (phase2Error ? phase2Error + "; " : "") + `${tier}: all vetting failed`;
37090
- }
37091
- if (vetRateLimitHit) {
37092
- rateLimitHitDuringSearch = true;
37093
- }
37094
- info(MODULE9, `Found ${tierCandidates.length} candidates from ${tier} tier`);
37095
- } catch (error48) {
37096
- if (getHttpStatusCode(error48) === 401)
37097
- throw error48;
37098
- const errMsg = errorMessage(error48);
37099
- phase2Error = (phase2Error ? phase2Error + "; " : "") + `${tier}: ${errMsg}`;
37100
- if (isRateLimitError(error48)) {
37101
- rateLimitHitDuringSearch = true;
37102
- }
37103
- warn(MODULE9, `Error in ${tier} tier search: ${errMsg}`);
37104
- tierResults.push([]);
37105
- }
37106
- }
37107
- const interleaved = interleaveArrays(tierResults);
37108
- if (interleaved.length === 0 && phase2Error) {
37109
- warn(MODULE9, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${phase2Error}`);
37110
- }
37111
- allCandidates.push(...interleaved.slice(0, remainingNeeded));
37292
+ const remaining = maxResults - allCandidates.length;
37293
+ const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37294
+ allCandidates.push(...result.candidates);
37295
+ phaseErrors["2"] = result.error;
37296
+ if (result.rateLimitHit)
37297
+ rateLimitHitDuringSearch = true;
37112
37298
  strategiesUsed.push("broad");
37113
37299
  }
37114
- let phase3Error = null;
37115
37300
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
37116
37301
  await sleep(INTER_PHASE_DELAY_MS);
37117
- info(MODULE9, "Phase 3: Searching actively maintained repos...");
37118
- const remainingNeeded = maxResults - allCandidates.length;
37119
- const thirtyDaysAgo = /* @__PURE__ */ new Date();
37120
- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
37121
- const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
37122
- const categoryTopics = getTopicsForCategories(config2.projectCategories ?? []);
37123
- const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : "";
37124
- const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
37125
- try {
37126
- const data = await cachedSearchIssues(this.octokit, {
37127
- q: phase3Query,
37128
- sort: "updated",
37129
- order: "desc",
37130
- per_page: remainingNeeded * 3
37131
- });
37132
- info(MODULE9, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
37133
- const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
37134
- const { candidates: starFiltered, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(this.vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], remainingNeeded, minStars, "Phase 3");
37135
- allCandidates.push(...starFiltered);
37136
- if (allVetFailed) {
37137
- phase3Error = "all vetting failed";
37138
- }
37139
- if (vetRateLimitHit) {
37140
- rateLimitHitDuringSearch = true;
37141
- }
37142
- info(MODULE9, `Found ${starFiltered.length} candidates from maintained-repo search`);
37143
- } catch (error48) {
37144
- const errMsg = errorMessage(error48);
37145
- phase3Error = errMsg;
37146
- if (isRateLimitError(error48)) {
37147
- rateLimitHitDuringSearch = true;
37148
- }
37149
- warn(MODULE9, `Error in maintained-repo search: ${errMsg}`);
37150
- }
37302
+ const remaining = maxResults - allCandidates.length;
37303
+ const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37304
+ allCandidates.push(...result.candidates);
37305
+ phaseErrors["3"] = result.error;
37306
+ if (result.rateLimitHit)
37307
+ rateLimitHitDuringSearch = true;
37151
37308
  strategiesUsed.push("maintained");
37152
37309
  }
37153
37310
  const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
@@ -37158,14 +37315,13 @@ var IssueDiscovery = class {
37158
37315
  budgetNote = ` Some search phases were skipped due to low API quota (${searchBudget} remaining).`;
37159
37316
  }
37160
37317
  if (allCandidates.length === 0) {
37161
- const phaseErrors = [
37162
- phase0Error ? `Phase 0 (merged-PR repos): ${phase0Error}` : null,
37163
- phase0_5Error ? `Phase 0.5 (preferred orgs): ${phase0_5Error}` : null,
37164
- phase1Error ? `Phase 1 (starred repos): ${phase1Error}` : null,
37165
- phase2Error ? `Phase 2 (general): ${phase2Error}` : null,
37166
- phase3Error ? `Phase 3 (maintained repos): ${phase3Error}` : null
37318
+ const errorDetails = [
37319
+ phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
37320
+ phaseErrors["1"] ? `Phase 1 (starred repos): ${phaseErrors["1"]}` : null,
37321
+ phaseErrors["2"] ? `Phase 2 (general): ${phaseErrors["2"]}` : null,
37322
+ phaseErrors["3"] ? `Phase 3 (maintained repos): ${phaseErrors["3"]}` : null
37167
37323
  ].filter(Boolean);
37168
- const details = phaseErrors.length > 0 ? ` ${phaseErrors.join(". ")}.` : "";
37324
+ const details = errorDetails.length > 0 ? ` ${errorDetails.join(". ")}.` : "";
37169
37325
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
37170
37326
  this.rateLimitWarning = `Search returned no results due to GitHub API rate limits.${details}${budgetNote} Try again after the rate limit resets.`;
37171
37327
  return { candidates: [], strategiesUsed };
@@ -37176,7 +37332,11 @@ var IssueDiscovery = class {
37176
37332
  this.rateLimitWarning = `Search results may be incomplete: GitHub API rate limits were hit during search.${budgetNote} Found ${allCandidates.length} candidate${allCandidates.length === 1 ? "" : "s"} but some search phases were limited. Try again after the rate limit resets for complete results.`;
37177
37333
  }
37178
37334
  allCandidates.sort((a, b) => {
37179
- const priorityOrder = { merged_pr: 0, preferred_org: 1, starred: 2, normal: 3 };
37335
+ const priorityOrder = {
37336
+ merged_pr: 0,
37337
+ starred: 1,
37338
+ normal: 2
37339
+ };
37180
37340
  const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
37181
37341
  if (priorityDiff !== 0)
37182
37342
  return priorityDiff;
@@ -37261,11 +37421,16 @@ var GistStateStore = class {
37261
37421
  warn(MODULE10, "No gist ID \u2014 cannot push");
37262
37422
  return false;
37263
37423
  }
37424
+ const json2 = JSON.stringify(state, null, 2);
37425
+ if (json2.length > 9e5) {
37426
+ warn(MODULE10, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
37427
+ return false;
37428
+ }
37264
37429
  try {
37265
37430
  await this.octokit.gists.update({
37266
37431
  gist_id: this.gistId,
37267
37432
  files: {
37268
- [GIST_FILENAME]: { content: JSON.stringify(state, null, 2) }
37433
+ [GIST_FILENAME]: { content: json2 }
37269
37434
  }
37270
37435
  });
37271
37436
  debug(MODULE10, "State pushed to gist");
@@ -37323,6 +37488,8 @@ var GistStateStore = class {
37323
37488
  this.writeCache(state);
37324
37489
  return { gistId: foundId, state, created: false };
37325
37490
  }
37491
+ warn(MODULE10, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
37492
+ return this.bootstrapFromCache();
37326
37493
  }
37327
37494
  debug(MODULE10, "No existing gist found, creating new one");
37328
37495
  const freshState = ScoutStateSchema.parse({ version: 1 });
@@ -37418,7 +37585,9 @@ var GistStateStore = class {
37418
37585
  }
37419
37586
  writeCache(state) {
37420
37587
  try {
37421
- fs3.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", { mode: 384 });
37588
+ fs3.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", {
37589
+ mode: 384
37590
+ });
37422
37591
  } catch (err) {
37423
37592
  warn(MODULE10, `Failed to write cache: ${errorMessage(err)}`);
37424
37593
  }
@@ -37434,6 +37603,7 @@ function mergeStates(local, remote) {
37434
37603
  mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
37435
37604
  closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
37436
37605
  savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
37606
+ skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
37437
37607
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
37438
37608
  lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
37439
37609
  gistId: remote.gistId ?? local.gistId
@@ -37484,6 +37654,18 @@ function mergeSavedResults(local, remote) {
37484
37654
  }
37485
37655
  return [...merged.values()];
37486
37656
  }
37657
+ function mergeSkippedIssues(local, remote) {
37658
+ const merged = /* @__PURE__ */ new Map();
37659
+ for (const item of local)
37660
+ merged.set(item.url, item);
37661
+ for (const item of remote) {
37662
+ const existing = merged.get(item.url);
37663
+ if (!existing || item.skippedAt > existing.skippedAt) {
37664
+ merged.set(item.url, item);
37665
+ }
37666
+ }
37667
+ return [...merged.values()];
37668
+ }
37487
37669
  function pickFresherTimestamp(a, b) {
37488
37670
  if (!a)
37489
37671
  return b;
@@ -37514,20 +37696,58 @@ function loadLocalState() {
37514
37696
  const backupPath = `${statePath}.corrupt.${Date.now()}`;
37515
37697
  fs4.copyFileSync(statePath, backupPath);
37516
37698
  warn(MODULE11, `Corrupt state backed up to ${backupPath}`);
37517
- } catch {
37699
+ } catch (backupErr) {
37700
+ warn(MODULE11, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
37518
37701
  }
37519
37702
  return ScoutStateSchema.parse({ version: 1 });
37520
37703
  }
37521
37704
  }
37522
37705
 
37523
37706
  // ../core/dist/scout.js
37707
+ function toGistOctokit(octokit) {
37708
+ return {
37709
+ gists: {
37710
+ async get(params) {
37711
+ const { data } = await octokit.gists.get(params);
37712
+ if (!data.id)
37713
+ throw new Error("Gist get returned no id");
37714
+ const files = data.files ? Object.fromEntries(Object.entries(data.files).map(([k, v]) => [
37715
+ k,
37716
+ v ? { content: v.content } : void 0
37717
+ ])) : null;
37718
+ return { data: { id: data.id, files } };
37719
+ },
37720
+ async create(params) {
37721
+ const { data } = await octokit.gists.create(params);
37722
+ if (!data.id)
37723
+ throw new Error("Gist create returned no id");
37724
+ return { data: { id: data.id } };
37725
+ },
37726
+ async update(params) {
37727
+ const { data } = await octokit.gists.update(params);
37728
+ if (!data.id)
37729
+ throw new Error("Gist update returned no id");
37730
+ return { data: { id: data.id } };
37731
+ },
37732
+ async list(params) {
37733
+ const { data } = await octokit.gists.list(params);
37734
+ return {
37735
+ data: data.filter((g) => g.id).map((g) => ({
37736
+ id: g.id,
37737
+ description: g.description ?? null
37738
+ }))
37739
+ };
37740
+ }
37741
+ }
37742
+ };
37743
+ }
37524
37744
  async function createScout(config2) {
37525
37745
  let state;
37526
37746
  let gistStore = null;
37527
37747
  if (config2.persistence === "provided") {
37528
37748
  state = config2.initialState;
37529
37749
  } else if (config2.persistence === "gist") {
37530
- gistStore = new GistStateStore(getOctokit(config2.githubToken));
37750
+ gistStore = new GistStateStore(toGistOctokit(getOctokit(config2.githubToken)));
37531
37751
  const result = await gistStore.bootstrap();
37532
37752
  if (result.degraded) {
37533
37753
  warn("scout", "Gist sync unavailable \u2014 running in offline mode. Changes will only be saved locally.");
@@ -37557,12 +37777,16 @@ var OssScout = class {
37557
37777
  // ── Search ──────────────────────────────────────────────────────────
37558
37778
  /**
37559
37779
  * Multi-strategy issue search. Returns scored, sorted candidates.
37780
+ * Automatically culls expired skip entries and filters skipped issues.
37560
37781
  */
37561
37782
  async search(options) {
37783
+ this.cullExpiredSkips();
37784
+ const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
37562
37785
  const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
37563
37786
  const { candidates, strategiesUsed } = await discovery.searchIssues({
37564
37787
  maxResults: options?.maxResults,
37565
- strategies: options?.strategies
37788
+ strategies: options?.strategies,
37789
+ skippedUrls
37566
37790
  });
37567
37791
  this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
37568
37792
  this.dirty = true;
@@ -37653,7 +37877,7 @@ var OssScout = class {
37653
37877
  getReposWithMergedPRs() {
37654
37878
  const repoCounts = /* @__PURE__ */ new Map();
37655
37879
  for (const pr of this.state.mergedPRs ?? []) {
37656
- const repo = this.extractRepoFromUrl(pr.url);
37880
+ const repo = extractRepoFromUrl(pr.url);
37657
37881
  if (repo) {
37658
37882
  repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
37659
37883
  }
@@ -37663,9 +37887,6 @@ var OssScout = class {
37663
37887
  getStarredRepos() {
37664
37888
  return this.state.starredRepos;
37665
37889
  }
37666
- getPreferredOrgs() {
37667
- return this.state.preferences.preferredOrgs;
37668
- }
37669
37890
  getProjectCategories() {
37670
37891
  return this.state.preferences.projectCategories;
37671
37892
  }
@@ -37796,6 +38017,69 @@ var OssScout = class {
37796
38017
  this.state.savedResults = [];
37797
38018
  this.dirty = true;
37798
38019
  }
38020
+ // ── Skip List ───────────────────────────────────────────────────────
38021
+ /**
38022
+ * Skip an issue — excludes it from future searches. Auto-culled after 90 days.
38023
+ */
38024
+ skipIssue(url2, metadata) {
38025
+ const existing = this.state.skippedIssues ?? [];
38026
+ if (existing.some((s) => s.url === url2))
38027
+ return;
38028
+ this.state.skippedIssues = [
38029
+ ...existing,
38030
+ {
38031
+ url: url2,
38032
+ repo: metadata?.repo ?? "",
38033
+ number: metadata?.number ?? 0,
38034
+ title: metadata?.title ?? "",
38035
+ skippedAt: (/* @__PURE__ */ new Date()).toISOString()
38036
+ }
38037
+ ];
38038
+ if (this.state.savedResults) {
38039
+ this.state.savedResults = this.state.savedResults.filter((r) => r.issueUrl !== url2);
38040
+ }
38041
+ this.dirty = true;
38042
+ }
38043
+ /**
38044
+ * Get all skipped issues.
38045
+ */
38046
+ getSkippedIssues() {
38047
+ return this.state.skippedIssues ?? [];
38048
+ }
38049
+ /**
38050
+ * Remove a specific issue from the skip list.
38051
+ */
38052
+ unskipIssue(url2) {
38053
+ this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url2);
38054
+ this.dirty = true;
38055
+ }
38056
+ /**
38057
+ * Clear all skipped issues.
38058
+ */
38059
+ clearSkippedIssues() {
38060
+ this.state.skippedIssues = [];
38061
+ this.dirty = true;
38062
+ }
38063
+ /**
38064
+ * Remove skipped issues older than maxDays (default 90). Called automatically during search.
38065
+ * @returns The number of expired entries that were removed.
38066
+ */
38067
+ cullExpiredSkips(maxDays = 90) {
38068
+ const cutoff = /* @__PURE__ */ new Date();
38069
+ cutoff.setDate(cutoff.getDate() - maxDays);
38070
+ const before = (this.state.skippedIssues ?? []).length;
38071
+ this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => {
38072
+ const d = new Date(s.skippedAt);
38073
+ if (isNaN(d.getTime())) {
38074
+ return true;
38075
+ }
38076
+ return d >= cutoff;
38077
+ });
38078
+ const culled = before - this.state.skippedIssues.length;
38079
+ if (culled > 0)
38080
+ this.dirty = true;
38081
+ return culled;
38082
+ }
37799
38083
  // ── Persistence ─────────────────────────────────────────────────────
37800
38084
  /**
37801
38085
  * Check if state has uncommitted changes.
@@ -37826,17 +38110,13 @@ var OssScout = class {
37826
38110
  return this.state;
37827
38111
  }
37828
38112
  // ── Private helpers ─────────────────────────────────────────────────
37829
- extractRepoFromUrl(url2) {
37830
- const match = url2.match(/github\.com\/([^/]+\/[^/]+)\//);
37831
- return match ? match[1] : null;
37832
- }
37833
38113
  updateRepoScoreFromPRs(repo) {
37834
- const mergedCount = (this.state.mergedPRs ?? []).filter((p) => this.extractRepoFromUrl(p.url) === repo).length;
37835
- const closedCount = (this.state.closedPRs ?? []).filter((p) => this.extractRepoFromUrl(p.url) === repo).length;
38114
+ const mergedCount = (this.state.mergedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
38115
+ const closedCount = (this.state.closedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
37836
38116
  this.updateRepoScore(repo, {
37837
38117
  mergedPRCount: mergedCount,
37838
38118
  closedWithoutMergeCount: closedCount,
37839
- lastMergedAt: mergedCount > 0 ? (this.state.mergedPRs ?? []).filter((p) => this.extractRepoFromUrl(p.url) === repo).sort((a, b) => b.mergedAt.localeCompare(a.mergedAt))[0]?.mergedAt : void 0
38119
+ lastMergedAt: mergedCount > 0 ? (this.state.mergedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).sort((a, b) => b.mergedAt.localeCompare(a.mergedAt))[0]?.mergedAt : void 0
37840
38120
  });
37841
38121
  }
37842
38122
  /**
@@ -37859,32 +38139,71 @@ var OssScout = class {
37859
38139
  };
37860
38140
 
37861
38141
  // src/tools.ts
38142
+ var TOOL_TIMEOUT_MS = 6e4;
38143
+ function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
38144
+ let timeoutId;
38145
+ const timer = new Promise((_, reject) => {
38146
+ timeoutId = setTimeout(
38147
+ () => reject(new Error(`Request timed out after ${ms / 1e3}s`)),
38148
+ ms
38149
+ );
38150
+ });
38151
+ return Promise.race([promise2, timer]).finally(() => clearTimeout(timeoutId));
38152
+ }
37862
38153
  function registerTools(server, scout) {
37863
38154
  server.tool(
37864
38155
  "search",
37865
38156
  "Search for open source issues matching your preferences",
37866
38157
  {
37867
38158
  maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
37868
- strategies: external_exports3.string().optional().describe("Comma-separated search strategies: merged, orgs, starred, broad, maintained, all")
38159
+ strategies: external_exports3.string().optional().describe(
38160
+ "Comma-separated search strategies: merged, starred, broad, maintained, all"
38161
+ )
37869
38162
  },
37870
38163
  async ({ maxResults, strategies }) => {
37871
- const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
37872
- const result = await scout.search({
37873
- maxResults: maxResults ?? 10,
37874
- strategies: parsedStrategies
37875
- });
37876
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38164
+ try {
38165
+ const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
38166
+ const result = await withTimeout(
38167
+ scout.search({
38168
+ maxResults: maxResults ?? 10,
38169
+ strategies: parsedStrategies
38170
+ })
38171
+ );
38172
+ scout.saveResults(result.candidates);
38173
+ await scout.checkpoint();
38174
+ return {
38175
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
38176
+ };
38177
+ } catch (err) {
38178
+ const msg = err instanceof Error ? err.message : String(err);
38179
+ return {
38180
+ content: [{ type: "text", text: `Error: ${msg}` }],
38181
+ isError: true
38182
+ };
38183
+ }
37877
38184
  }
37878
38185
  );
37879
38186
  server.tool(
37880
38187
  "vet",
37881
38188
  "Vet a specific GitHub issue for contribution viability",
37882
38189
  {
37883
- issueUrl: external_exports3.string().describe("Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)")
38190
+ issueUrl: external_exports3.string().describe(
38191
+ "Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)"
38192
+ )
37884
38193
  },
37885
38194
  async ({ issueUrl }) => {
37886
- const candidate = await scout.vetIssue(issueUrl);
37887
- return { content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }] };
38195
+ try {
38196
+ const candidate = await withTimeout(scout.vetIssue(issueUrl));
38197
+ return {
38198
+ content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }]
38199
+ };
38200
+ } catch (err) {
38201
+ const msg = err instanceof Error ? err.message : String(err);
38202
+ return {
38203
+ content: [{ type: "text", text: `Error: ${msg}` }],
38204
+ isError: true
38205
+ };
38206
+ }
37888
38207
  }
37889
38208
  );
37890
38209
  server.tool(
@@ -37892,33 +38211,131 @@ function registerTools(server, scout) {
37892
38211
  "Show current oss-scout configuration preferences",
37893
38212
  async () => {
37894
38213
  const preferences = scout.getPreferences();
37895
- return { content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }] };
38214
+ return {
38215
+ content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }]
38216
+ };
38217
+ }
38218
+ );
38219
+ server.tool(
38220
+ "skip",
38221
+ "Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.",
38222
+ {
38223
+ action: external_exports3.enum(["add", "remove", "list", "clear"]).default("add").describe("Action to perform (default: add)"),
38224
+ issueUrl: external_exports3.string().optional().describe("GitHub issue URL (required for add/remove actions)")
38225
+ },
38226
+ async ({ action, issueUrl }) => {
38227
+ try {
38228
+ if ((action === "add" || action === "remove") && !issueUrl) {
38229
+ return {
38230
+ content: [
38231
+ {
38232
+ type: "text",
38233
+ text: `Error: issueUrl is required for the "${action}" action.`
38234
+ }
38235
+ ],
38236
+ isError: true
38237
+ };
38238
+ }
38239
+ if (action === "list") {
38240
+ const skipped = scout.getSkippedIssues();
38241
+ return {
38242
+ content: [{ type: "text", text: JSON.stringify(skipped, null, 2) }]
38243
+ };
38244
+ }
38245
+ if (action === "clear") {
38246
+ const count = scout.getSkippedIssues().length;
38247
+ scout.clearSkippedIssues();
38248
+ const synced2 = await scout.checkpoint();
38249
+ const syncNote2 = synced2 ? "" : " (saved locally, gist sync failed)";
38250
+ return {
38251
+ content: [
38252
+ {
38253
+ type: "text",
38254
+ text: count > 0 ? `Skip list cleared (${count} entries removed)${syncNote2}` : `Skip list already empty${syncNote2}`
38255
+ }
38256
+ ]
38257
+ };
38258
+ }
38259
+ if (action === "remove") {
38260
+ const wasPresent = scout.getSkippedIssues().some((s) => s.url === issueUrl);
38261
+ scout.unskipIssue(issueUrl);
38262
+ const synced2 = await scout.checkpoint();
38263
+ const syncNote2 = synced2 ? "" : " (saved locally, gist sync failed)";
38264
+ return {
38265
+ content: [
38266
+ {
38267
+ type: "text",
38268
+ text: wasPresent ? `Removed from skip list: ${issueUrl}${syncNote2}` : `Not in skip list: ${issueUrl}`
38269
+ }
38270
+ ]
38271
+ };
38272
+ }
38273
+ const alreadySkipped = scout.getSkippedIssues().some((s) => s.url === issueUrl);
38274
+ if (alreadySkipped) {
38275
+ return {
38276
+ content: [
38277
+ { type: "text", text: `Already in skip list: ${issueUrl}` }
38278
+ ]
38279
+ };
38280
+ }
38281
+ const saved = scout.getSavedResults().find((r) => r.issueUrl === issueUrl);
38282
+ const metadata = saved ? {
38283
+ repo: saved.repo,
38284
+ number: saved.number,
38285
+ title: saved.title
38286
+ } : void 0;
38287
+ scout.skipIssue(issueUrl, metadata);
38288
+ const synced = await scout.checkpoint();
38289
+ const syncNote = synced ? "" : " (saved locally, gist sync failed)";
38290
+ return {
38291
+ content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }]
38292
+ };
38293
+ } catch (err) {
38294
+ const msg = err instanceof Error ? err.message : String(err);
38295
+ return {
38296
+ content: [{ type: "text", text: `Error: ${msg}` }],
38297
+ isError: true
38298
+ };
38299
+ }
37896
38300
  }
37897
38301
  );
37898
38302
  const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
37899
38303
  const ARRAY_KEYS = /* @__PURE__ */ new Set([
37900
38304
  "languages",
37901
38305
  "labels",
37902
- "preferredOrgs",
37903
38306
  "projectCategories",
37904
38307
  "excludeRepos",
38308
+ "excludeOrgs",
37905
38309
  "aiPolicyBlocklist",
37906
38310
  "scope",
37907
38311
  "defaultStrategy"
37908
38312
  ]);
37909
- const NUMBER_KEYS = /* @__PURE__ */ new Set(["minStars", "maxIssueAgeDays", "minRepoScoreThreshold"]);
38313
+ const NUMBER_KEYS = /* @__PURE__ */ new Set([
38314
+ "minStars",
38315
+ "maxIssueAgeDays",
38316
+ "minRepoScoreThreshold"
38317
+ ]);
37910
38318
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
37911
38319
  server.tool(
37912
38320
  "config-set",
37913
38321
  "Update an oss-scout configuration preference",
37914
38322
  {
37915
- key: external_exports3.string().describe("Preference key to update (e.g. languages, minStars, excludeRepos)"),
37916
- value: external_exports3.string().describe("New value \u2014 comma-separated for arrays, plain string for scalars")
38323
+ key: external_exports3.string().describe(
38324
+ "Preference key to update (e.g. languages, minStars, excludeRepos)"
38325
+ ),
38326
+ value: external_exports3.string().describe(
38327
+ "New value \u2014 comma-separated for arrays, plain string for scalars"
38328
+ )
37917
38329
  },
37918
38330
  async ({ key, value }) => {
37919
38331
  if (!VALID_KEYS.includes(key)) {
37920
38332
  return {
37921
- content: [{ type: "text", text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}` }],
38333
+ content: [
38334
+ {
38335
+ type: "text",
38336
+ text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`
38337
+ }
38338
+ ],
37922
38339
  isError: true
37923
38340
  };
37924
38341
  }
@@ -37936,7 +38353,9 @@ function registerTools(server, scout) {
37936
38353
  const num = Number(value);
37937
38354
  if (isNaN(num)) {
37938
38355
  return {
37939
- content: [{ type: "text", text: `Invalid number for "${key}": "${value}"` }],
38356
+ content: [
38357
+ { type: "text", text: `Invalid number for "${key}": "${value}"` }
38358
+ ],
37940
38359
  isError: true
37941
38360
  };
37942
38361
  }
@@ -37947,7 +38366,12 @@ function registerTools(server, scout) {
37947
38366
  else if (lower === "false" || lower === "no") parsed = false;
37948
38367
  else {
37949
38368
  return {
37950
- content: [{ type: "text", text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.` }],
38369
+ content: [
38370
+ {
38371
+ type: "text",
38372
+ text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`
38373
+ }
38374
+ ],
37951
38375
  isError: true
37952
38376
  };
37953
38377
  }
@@ -37965,8 +38389,11 @@ function registerTools(server, scout) {
37965
38389
  };
37966
38390
  }
37967
38391
  scout.updatePreferences({ [key]: parsed });
38392
+ await scout.checkpoint();
37968
38393
  const updated = scout.getPreferences();
37969
- return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
38394
+ return {
38395
+ content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
38396
+ };
37970
38397
  }
37971
38398
  );
37972
38399
  }
@@ -37978,11 +38405,13 @@ function registerResources(server, scout) {
37978
38405
  "scout://config",
37979
38406
  { description: "Current oss-scout preferences" },
37980
38407
  async () => ({
37981
- contents: [{
37982
- uri: "scout://config",
37983
- mimeType: "application/json",
37984
- text: JSON.stringify(scout.getPreferences(), null, 2)
37985
- }]
38408
+ contents: [
38409
+ {
38410
+ uri: "scout://config",
38411
+ mimeType: "application/json",
38412
+ text: JSON.stringify(scout.getPreferences(), null, 2)
38413
+ }
38414
+ ]
37986
38415
  })
37987
38416
  );
37988
38417
  server.resource(
@@ -37990,11 +38419,13 @@ function registerResources(server, scout) {
37990
38419
  "scout://results",
37991
38420
  { description: "Saved search results from previous runs" },
37992
38421
  async () => ({
37993
- contents: [{
37994
- uri: "scout://results",
37995
- mimeType: "application/json",
37996
- text: JSON.stringify(scout.getSavedResults(), null, 2)
37997
- }]
38422
+ contents: [
38423
+ {
38424
+ uri: "scout://results",
38425
+ mimeType: "application/json",
38426
+ text: JSON.stringify(scout.getSavedResults(), null, 2)
38427
+ }
38428
+ ]
37998
38429
  })
37999
38430
  );
38000
38431
  server.resource(
@@ -38004,11 +38435,13 @@ function registerResources(server, scout) {
38004
38435
  async () => {
38005
38436
  const state = scout.getState();
38006
38437
  return {
38007
- contents: [{
38008
- uri: "scout://scores",
38009
- mimeType: "application/json",
38010
- text: JSON.stringify(state.repoScores, null, 2)
38011
- }]
38438
+ contents: [
38439
+ {
38440
+ uri: "scout://scores",
38441
+ mimeType: "application/json",
38442
+ text: JSON.stringify(state.repoScores, null, 2)
38443
+ }
38444
+ ]
38012
38445
  };
38013
38446
  }
38014
38447
  );
@@ -38025,7 +38458,7 @@ async function main() {
38025
38458
  }
38026
38459
  const scout = await createScout({ githubToken: token });
38027
38460
  const server = new McpServer(
38028
- { name: "@oss-scout/core", version: "0.1.0" },
38461
+ { name: "oss-scout-mcp", version: "0.2.1" },
38029
38462
  { capabilities: { resources: {}, tools: {} } }
38030
38463
  );
38031
38464
  registerTools(server, scout);
@@ -38034,8 +38467,10 @@ async function main() {
38034
38467
  await server.connect(transport);
38035
38468
  }
38036
38469
  main().catch((err) => {
38037
- process.stderr.write(`oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
38038
- `);
38470
+ process.stderr.write(
38471
+ `oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
38472
+ `
38473
+ );
38039
38474
  process.exit(1);
38040
38475
  });
38041
38476
  /*! Bundled license information: