@oss-scout/mcp 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,26 @@ 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
+ "orgs",
35708
+ "starred",
35709
+ "broad",
35710
+ "maintained",
35711
+ "all"
35712
+ ]);
35713
+ var CONCRETE_STRATEGIES = [
35714
+ "merged",
35715
+ "orgs",
35716
+ "starred",
35717
+ "broad",
35718
+ "maintained"
35719
+ ];
35677
35720
  var RepoSignalsSchema = external_exports3.object({
35678
35721
  hasActiveMaintainers: external_exports3.boolean(),
35679
35722
  isResponsive: external_exports3.boolean(),
@@ -35788,7 +35831,12 @@ var ScoutStateSchema = external_exports3.object({
35788
35831
  });
35789
35832
 
35790
35833
  // ../core/dist/core/issue-filtering.js
35791
- var DOC_ONLY_LABELS = /* @__PURE__ */ new Set(["documentation", "docs", "typo", "spelling"]);
35834
+ var DOC_ONLY_LABELS = /* @__PURE__ */ new Set([
35835
+ "documentation",
35836
+ "docs",
35837
+ "typo",
35838
+ "spelling"
35839
+ ]);
35792
35840
  function isDocOnlyIssue(item) {
35793
35841
  if (!item.labels || !Array.isArray(item.labels) || item.labels.length === 0)
35794
35842
  return false;
@@ -35827,7 +35875,9 @@ function detectLabelFarmingRepos(items) {
35827
35875
  const spamRepos = /* @__PURE__ */ new Set();
35828
35876
  const repoSpamCounts = /* @__PURE__ */ new Map();
35829
35877
  for (const item of items) {
35830
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
35878
+ const repoFullName = extractRepoFromUrl(item.repository_url);
35879
+ if (!repoFullName)
35880
+ continue;
35831
35881
  if (isLabelFarming(item)) {
35832
35882
  spamRepos.add(repoFullName);
35833
35883
  continue;
@@ -35913,20 +35963,111 @@ function calculateViabilityScore(params) {
35913
35963
 
35914
35964
  // ../core/dist/core/category-mapping.js
35915
35965
  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"]
35966
+ nonprofit: [
35967
+ "nonprofit",
35968
+ "social-good",
35969
+ "humanitarian",
35970
+ "charity",
35971
+ "social-impact",
35972
+ "civic-tech"
35973
+ ],
35974
+ devtools: [
35975
+ "developer-tools",
35976
+ "devtools",
35977
+ "cli",
35978
+ "sdk",
35979
+ "linter",
35980
+ "formatter",
35981
+ "build-tool"
35982
+ ],
35983
+ infrastructure: [
35984
+ "infrastructure",
35985
+ "cloud",
35986
+ "kubernetes",
35987
+ "docker",
35988
+ "devops",
35989
+ "monitoring",
35990
+ "observability"
35991
+ ],
35992
+ "web-frameworks": [
35993
+ "web-framework",
35994
+ "frontend",
35995
+ "backend",
35996
+ "fullstack",
35997
+ "nextjs",
35998
+ "react",
35999
+ "vue"
36000
+ ],
36001
+ "data-ml": [
36002
+ "machine-learning",
36003
+ "data-science",
36004
+ "deep-learning",
36005
+ "nlp",
36006
+ "data-pipeline",
36007
+ "analytics"
36008
+ ],
36009
+ education: [
36010
+ "education",
36011
+ "learning",
36012
+ "tutorial",
36013
+ "courseware",
36014
+ "edtech",
36015
+ "teaching"
36016
+ ]
35922
36017
  };
35923
36018
  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"]
36019
+ nonprofit: [
36020
+ "code-for-america",
36021
+ "opengovfoundation",
36022
+ "ushahidi",
36023
+ "hotosm",
36024
+ "openfn",
36025
+ "democracyearth"
36026
+ ],
36027
+ devtools: [
36028
+ "eslint",
36029
+ "prettier",
36030
+ "vitejs",
36031
+ "biomejs",
36032
+ "oxc-project",
36033
+ "ast-grep",
36034
+ "turbot"
36035
+ ],
36036
+ infrastructure: [
36037
+ "kubernetes",
36038
+ "hashicorp",
36039
+ "grafana",
36040
+ "prometheus",
36041
+ "open-telemetry",
36042
+ "envoyproxy",
36043
+ "cncf"
36044
+ ],
36045
+ "web-frameworks": [
36046
+ "vercel",
36047
+ "remix-run",
36048
+ "sveltejs",
36049
+ "nuxt",
36050
+ "astro",
36051
+ "redwoodjs",
36052
+ "blitz-js"
36053
+ ],
36054
+ "data-ml": [
36055
+ "huggingface",
36056
+ "mlflow",
36057
+ "apache",
36058
+ "dbt-labs",
36059
+ "dagster-io",
36060
+ "prefecthq",
36061
+ "langchain-ai"
36062
+ ],
36063
+ education: [
36064
+ "freeCodeCamp",
36065
+ "TheOdinProject",
36066
+ "exercism",
36067
+ "codecademy",
36068
+ "oppia",
36069
+ "Khan"
36070
+ ]
35930
36071
  };
35931
36072
  function repoBelongsToCategory(repoFullName, categories) {
35932
36073
  if (categories.length === 0)
@@ -36046,7 +36187,10 @@ var HttpCache = class {
36046
36187
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
36047
36188
  };
36048
36189
  try {
36049
- fs2.writeFileSync(this.pathFor(url2), JSON.stringify(entry), { encoding: "utf-8", mode: 384 });
36190
+ fs2.writeFileSync(this.pathFor(url2), JSON.stringify(entry), {
36191
+ encoding: "utf-8",
36192
+ mode: 384
36193
+ });
36050
36194
  debug(MODULE4, `Cached response for ${url2}`);
36051
36195
  } catch (err) {
36052
36196
  warn(MODULE4, `Failed to write cache for ${url2}: ${errorMessage(err)}`);
@@ -36301,7 +36445,12 @@ function analyzeRequirements(body) {
36301
36445
  const hasSteps = /\d\.|[-*]\s/.test(body);
36302
36446
  const hasCodeBlock = /```/.test(body);
36303
36447
  const hasExpectedBehavior = /expect|should|must|want/i.test(body);
36304
- const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
36448
+ const indicators = [
36449
+ hasSteps,
36450
+ hasCodeBlock,
36451
+ hasExpectedBehavior,
36452
+ body.length > 200
36453
+ ];
36305
36454
  return indicators.filter(Boolean).length >= 2;
36306
36455
  }
36307
36456
 
@@ -36378,7 +36527,12 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
36378
36527
  if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
36379
36528
  return cached2.guidelines;
36380
36529
  }
36381
- const filesToCheck = ["CONTRIBUTING.md", ".github/CONTRIBUTING.md", "docs/CONTRIBUTING.md", "contributing.md"];
36530
+ const filesToCheck = [
36531
+ "CONTRIBUTING.md",
36532
+ ".github/CONTRIBUTING.md",
36533
+ "docs/CONTRIBUTING.md",
36534
+ "contributing.md"
36535
+ ];
36382
36536
  const results = await Promise.allSettled(filesToCheck.map((file2) => octokit.repos.getContent({ owner, repo, path: file2 }).then(({ data }) => {
36383
36537
  if ("content" in data) {
36384
36538
  return Buffer.from(data.content, "base64").toString("utf-8");
@@ -36400,7 +36554,10 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
36400
36554
  }
36401
36555
  }
36402
36556
  }
36403
- guidelinesCache.set(cacheKey, { guidelines: void 0, fetchedAt: Date.now() });
36557
+ guidelinesCache.set(cacheKey, {
36558
+ guidelines: void 0,
36559
+ fetchedAt: Date.now()
36560
+ });
36404
36561
  pruneCache();
36405
36562
  return void 0;
36406
36563
  }
@@ -36662,7 +36819,11 @@ var IssueVetter = class {
36662
36819
  if (allFailed) {
36663
36820
  warn(MODULE7, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
36664
36821
  }
36665
- return { candidates: candidates.slice(0, maxResults), allFailed, rateLimitHit: rateLimitFailures > 0 };
36822
+ return {
36823
+ candidates: candidates.slice(0, maxResults),
36824
+ allFailed,
36825
+ rateLimitHit: rateLimitFailures > 0
36826
+ };
36666
36827
  }
36667
36828
  };
36668
36829
 
@@ -36763,11 +36924,13 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
36763
36924
  async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
36764
36925
  const spamRepos = detectLabelFarmingRepos(items);
36765
36926
  if (spamRepos.size > 0) {
36766
- const spamCount = items.filter((i) => spamRepos.has(i.repository_url.split("/").slice(-2).join("/"))).length;
36927
+ const spamCount = items.filter((i) => spamRepos.has(extractRepoFromUrl(i.repository_url) ?? "")).length;
36767
36928
  debug(MODULE8, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
36768
36929
  }
36769
36930
  const itemsToVet = filterIssues(items).filter((item) => {
36770
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
36931
+ const repoFullName = extractRepoFromUrl(item.repository_url);
36932
+ if (!repoFullName)
36933
+ return false;
36771
36934
  if (spamRepos.has(repoFullName))
36772
36935
  return false;
36773
36936
  return excludedRepoSets.every((s) => !s.has(repoFullName));
@@ -36835,6 +36998,182 @@ var MODULE9 = "issue-discovery";
36835
36998
  var INTER_PHASE_DELAY_MS = 2e3;
36836
36999
  var LOW_BUDGET_THRESHOLD = 20;
36837
37000
  var CRITICAL_BUDGET_THRESHOLD = 10;
37001
+ function buildIssueFilter(config2) {
37002
+ return (items) => {
37003
+ return items.filter((item) => {
37004
+ const repoFullName = extractRepoFromUrl(item.repository_url);
37005
+ if (!repoFullName)
37006
+ return false;
37007
+ if (config2.excludedRepos.has(repoFullName))
37008
+ return false;
37009
+ if (config2.excludeOrgs.size > 0) {
37010
+ const orgName = repoFullName.split("/")[0]?.toLowerCase();
37011
+ if (orgName && config2.excludeOrgs.has(orgName))
37012
+ return false;
37013
+ }
37014
+ if (config2.aiBlocklisted.has(repoFullName))
37015
+ return false;
37016
+ if (config2.lowScoringRepos.has(repoFullName))
37017
+ return false;
37018
+ const updatedAt = new Date(item.updated_at);
37019
+ const ageDays = daysBetween(updatedAt, config2.now);
37020
+ if (ageDays > config2.maxAgeDays)
37021
+ return false;
37022
+ if (!config2.includeDocIssues && isDocOnlyIssue(item))
37023
+ return false;
37024
+ return true;
37025
+ });
37026
+ };
37027
+ }
37028
+ async function runPhase0(octokit, vetter, repos, baseQualifiers, maxResults, filterIssues) {
37029
+ info(MODULE9, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
37030
+ const { candidates, allBatchesFailed, rateLimitHit } = await searchInRepos(octokit, vetter, repos, baseQualifiers, [], maxResults, "merged_pr", filterIssues);
37031
+ info(MODULE9, `Found ${candidates.length} candidates from merged-PR repos`);
37032
+ return {
37033
+ candidates,
37034
+ error: allBatchesFailed ? "All merged-PR repo batches failed" : null,
37035
+ rateLimitHit
37036
+ };
37037
+ }
37038
+ async function runPhase05(octokit, vetter, orgsToSearch, baseQualifiers, labels, maxResults, phase0RepoSet, filterIssues) {
37039
+ info(MODULE9, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
37040
+ const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(" OR ");
37041
+ const orgOps = orgsToSearch.length - 1;
37042
+ try {
37043
+ const allItems = await searchWithChunkedLabels(octokit, labels, orgOps, (labelQ) => `${baseQualifiers} ${labelQ} (${orgRepoFilter})`.replace(/ +/g, " ").trim(), maxResults * 3);
37044
+ if (allItems.length === 0) {
37045
+ return { candidates: [], error: null, rateLimitHit: false };
37046
+ }
37047
+ const filtered = filterIssues(allItems).filter((item) => {
37048
+ const repoFullName = extractRepoFromUrl(item.repository_url);
37049
+ if (!repoFullName)
37050
+ return false;
37051
+ return !phase0RepoSet.has(repoFullName);
37052
+ });
37053
+ const { candidates, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(filtered.slice(0, maxResults * 2).map((i) => i.html_url), maxResults, "preferred_org");
37054
+ info(MODULE9, `Found ${candidates.length} candidates from preferred orgs`);
37055
+ return {
37056
+ candidates,
37057
+ error: allVetFailed ? "All preferred org issue vetting failed" : null,
37058
+ rateLimitHit
37059
+ };
37060
+ } catch (error48) {
37061
+ const errMsg = errorMessage(error48);
37062
+ warn(MODULE9, `Error searching preferred orgs: ${errMsg}`);
37063
+ return {
37064
+ candidates: [],
37065
+ error: errMsg,
37066
+ rateLimitHit: isRateLimitError(error48)
37067
+ };
37068
+ }
37069
+ }
37070
+ async function runPhase1(octokit, vetter, repos, baseQualifiers, labels, maxResults, filterIssues) {
37071
+ info(MODULE9, `Phase 1: Searching issues in ${repos.length} starred repos...`);
37072
+ const phase1Labels = labels.slice(0, 3);
37073
+ const { candidates, allBatchesFailed, rateLimitHit } = await searchInRepos(octokit, vetter, repos.slice(0, 10), baseQualifiers, phase1Labels, maxResults, "starred", filterIssues);
37074
+ info(MODULE9, `Found ${candidates.length} candidates from starred repos`);
37075
+ return {
37076
+ candidates,
37077
+ error: allBatchesFailed ? "All starred repo batches failed" : null,
37078
+ rateLimitHit
37079
+ };
37080
+ }
37081
+ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQualifiers, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37082
+ info(MODULE9, "Phase 2: General issue search...");
37083
+ const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37084
+ const tierLabelGroups = [];
37085
+ if (scopes && scopes.length > 1) {
37086
+ for (const scope of scopes) {
37087
+ const scopeLabels = SCOPE_LABELS[scope] ?? [];
37088
+ if (scopeLabels.length === 0) {
37089
+ warn(MODULE9, `Scope "${scope}" has no labels, skipping tier`);
37090
+ continue;
37091
+ }
37092
+ tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
37093
+ }
37094
+ const allScopeLabels = new Set(scopes.flatMap((s) => SCOPE_LABELS[s] ?? []));
37095
+ const customOnly = configLabels.filter((l) => !allScopeLabels.has(l));
37096
+ if (customOnly.length > 0) {
37097
+ tierLabelGroups.push({ tier: "custom", tierLabels: customOnly });
37098
+ }
37099
+ } else {
37100
+ tierLabelGroups.push({ tier: "general", tierLabels: labels });
37101
+ }
37102
+ const budgetPerTier = Math.ceil(maxResults / tierLabelGroups.length);
37103
+ const tierResults = [];
37104
+ let error48 = null;
37105
+ let rateLimitHit = false;
37106
+ for (const { tier, tierLabels } of tierLabelGroups) {
37107
+ try {
37108
+ const allItems = await searchWithChunkedLabels(octokit, tierLabels, 0, (labelQ) => `${baseQualifiers} ${labelQ}`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
37109
+ info(MODULE9, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
37110
+ const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
37111
+ tierResults.push(tierCandidates);
37112
+ for (const c of tierCandidates)
37113
+ seenRepos.add(c.issue.repo);
37114
+ if (allVetFailed) {
37115
+ error48 = (error48 ? error48 + "; " : "") + `${tier}: all vetting failed`;
37116
+ }
37117
+ if (vetRateLimitHit) {
37118
+ rateLimitHit = true;
37119
+ }
37120
+ info(MODULE9, `Found ${tierCandidates.length} candidates from ${tier} tier`);
37121
+ } catch (err) {
37122
+ if (getHttpStatusCode(err) === 401)
37123
+ throw err;
37124
+ const errMsg = errorMessage(err);
37125
+ error48 = (error48 ? error48 + "; " : "") + `${tier}: ${errMsg}`;
37126
+ if (isRateLimitError(err)) {
37127
+ rateLimitHit = true;
37128
+ }
37129
+ warn(MODULE9, `Error in ${tier} tier search: ${errMsg}`);
37130
+ tierResults.push([]);
37131
+ }
37132
+ }
37133
+ const interleaved = interleaveArrays(tierResults);
37134
+ if (interleaved.length === 0 && error48) {
37135
+ warn(MODULE9, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${error48}`);
37136
+ }
37137
+ return {
37138
+ candidates: interleaved.slice(0, maxResults),
37139
+ error: error48,
37140
+ rateLimitHit
37141
+ };
37142
+ }
37143
+ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37144
+ info(MODULE9, "Phase 3: Searching actively maintained repos...");
37145
+ const thirtyDaysAgo = /* @__PURE__ */ new Date();
37146
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
37147
+ const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
37148
+ const categoryTopics = getTopicsForCategories(projectCategories);
37149
+ const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : "";
37150
+ const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
37151
+ try {
37152
+ const data = await cachedSearchIssues(octokit, {
37153
+ q: phase3Query,
37154
+ sort: "updated",
37155
+ order: "desc",
37156
+ per_page: maxResults * 3
37157
+ });
37158
+ info(MODULE9, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
37159
+ const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37160
+ const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
37161
+ info(MODULE9, `Found ${candidates.length} candidates from maintained-repo search`);
37162
+ return {
37163
+ candidates,
37164
+ error: allVetFailed ? "all vetting failed" : null,
37165
+ rateLimitHit: vetRateLimitHit
37166
+ };
37167
+ } catch (error48) {
37168
+ const errMsg = errorMessage(error48);
37169
+ warn(MODULE9, `Error in maintained-repo search: ${errMsg}`);
37170
+ return {
37171
+ candidates: [],
37172
+ error: errMsg,
37173
+ rateLimitHit: isRateLimitError(error48)
37174
+ };
37175
+ }
37176
+ }
36838
37177
  var IssueDiscovery = class {
36839
37178
  preferences;
36840
37179
  stateReader;
@@ -36898,8 +37237,7 @@ var IssueDiscovery = class {
36898
37237
  const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
36899
37238
  const strategiesUsed = [];
36900
37239
  const allCandidates = [];
36901
- let phase0Error = null;
36902
- let phase1Error = null;
37240
+ const phaseErrors = {};
36903
37241
  let rateLimitHitDuringSearch = false;
36904
37242
  this.rateLimitWarning = null;
36905
37243
  const tracker = getSearchBudgetTracker();
@@ -36919,71 +37257,44 @@ var IssueDiscovery = class {
36919
37257
  info(MODULE9, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
36920
37258
  }
36921
37259
  } catch (error48) {
36922
- if (getHttpStatusCode(error48) === 401) {
37260
+ if (getHttpStatusCode(error48) === 401)
36923
37261
  throw error48;
36924
- }
36925
37262
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
36926
37263
  warn(MODULE9, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
36927
37264
  }
36928
37265
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
36929
37266
  const starredRepos = this.getStarredRepos();
36930
37267
  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();
37268
+ const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
36937
37269
  const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
36938
37270
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
36939
37271
  const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`.replace(/ +/g, " ").trim();
36940
- const includeDocIssues = config2.includeDocIssues ?? true;
36941
37272
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
36942
37273
  if (aiBlocklisted.size > 0) {
36943
37274
  debug(MODULE9, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
36944
37275
  }
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
- };
37276
+ const filterIssues = buildIssueFilter({
37277
+ excludedRepos: new Set(config2.excludeRepos),
37278
+ excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
37279
+ aiBlocklisted,
37280
+ lowScoringRepos,
37281
+ maxAgeDays: config2.maxIssueAgeDays || 90,
37282
+ now: /* @__PURE__ */ new Date(),
37283
+ includeDocIssues: config2.includeDocIssues ?? true
37284
+ });
36968
37285
  const phase0Repos = mergedPRRepos.slice(0, 10);
36969
37286
  const phase0RepoSet = new Set(phase0Repos);
36970
37287
  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) {
37288
+ const remaining = maxResults - allCandidates.length;
37289
+ if (remaining > 0) {
37290
+ const result = await runPhase0(this.octokit, this.vetter, phase0Repos, baseQualifiers, remaining, filterIssues);
37291
+ allCandidates.push(...result.candidates);
37292
+ phaseErrors["0"] = result.error;
37293
+ if (result.rateLimitHit)
36980
37294
  rateLimitHitDuringSearch = true;
36981
- }
36982
- info(MODULE9, `Found ${mergedCandidates.length} candidates from merged-PR repos`);
36983
37295
  }
36984
37296
  strategiesUsed.push("merged");
36985
37297
  }
36986
- let phase0_5Error = null;
36987
37298
  const preferredOrgs = config2.preferredOrgs ?? [];
36988
37299
  if (allCandidates.length < maxResults && preferredOrgs.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("orgs")) {
36989
37300
  if (phase0Repos.length > 0)
@@ -36991,35 +37302,12 @@ var IssueDiscovery = class {
36991
37302
  const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
36992
37303
  const orgsToSearch = preferredOrgs.filter((org) => !phase0Orgs.has(org.toLowerCase())).slice(0, 5);
36993
37304
  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
- }
37305
+ const remaining = maxResults - allCandidates.length;
37306
+ const result = await runPhase05(this.octokit, this.vetter, orgsToSearch, baseQualifiers, labels, remaining, phase0RepoSet, filterIssues);
37307
+ allCandidates.push(...result.candidates);
37308
+ phaseErrors["0.5"] = result.error;
37309
+ if (result.rateLimitHit)
37310
+ rateLimitHitDuringSearch = true;
37023
37311
  }
37024
37312
  strategiesUsed.push("orgs");
37025
37313
  }
@@ -37027,127 +37315,35 @@ var IssueDiscovery = class {
37027
37315
  await sleep(INTER_PHASE_DELAY_MS);
37028
37316
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
37029
37317
  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) {
37318
+ const remaining = maxResults - allCandidates.length;
37319
+ if (remaining > 0) {
37320
+ const result = await runPhase1(this.octokit, this.vetter, reposToSearch, baseQualifiers, labels, remaining, filterIssues);
37321
+ allCandidates.push(...result.candidates);
37322
+ phaseErrors["1"] = result.error;
37323
+ if (result.rateLimitHit)
37040
37324
  rateLimitHitDuringSearch = true;
37041
- }
37042
- info(MODULE9, `Found ${starredCandidates.length} candidates from starred repos`);
37043
37325
  }
37044
37326
  }
37045
37327
  strategiesUsed.push("starred");
37046
37328
  }
37047
- let phase2Error = null;
37048
37329
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
37049
37330
  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));
37331
+ const remaining = maxResults - allCandidates.length;
37332
+ const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37333
+ allCandidates.push(...result.candidates);
37334
+ phaseErrors["2"] = result.error;
37335
+ if (result.rateLimitHit)
37336
+ rateLimitHitDuringSearch = true;
37112
37337
  strategiesUsed.push("broad");
37113
37338
  }
37114
- let phase3Error = null;
37115
37339
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
37116
37340
  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
- }
37341
+ const remaining = maxResults - allCandidates.length;
37342
+ const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37343
+ allCandidates.push(...result.candidates);
37344
+ phaseErrors["3"] = result.error;
37345
+ if (result.rateLimitHit)
37346
+ rateLimitHitDuringSearch = true;
37151
37347
  strategiesUsed.push("maintained");
37152
37348
  }
37153
37349
  const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
@@ -37158,14 +37354,14 @@ var IssueDiscovery = class {
37158
37354
  budgetNote = ` Some search phases were skipped due to low API quota (${searchBudget} remaining).`;
37159
37355
  }
37160
37356
  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
37357
+ const errorDetails = [
37358
+ phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
37359
+ phaseErrors["0.5"] ? `Phase 0.5 (preferred orgs): ${phaseErrors["0.5"]}` : null,
37360
+ phaseErrors["1"] ? `Phase 1 (starred repos): ${phaseErrors["1"]}` : null,
37361
+ phaseErrors["2"] ? `Phase 2 (general): ${phaseErrors["2"]}` : null,
37362
+ phaseErrors["3"] ? `Phase 3 (maintained repos): ${phaseErrors["3"]}` : null
37167
37363
  ].filter(Boolean);
37168
- const details = phaseErrors.length > 0 ? ` ${phaseErrors.join(". ")}.` : "";
37364
+ const details = errorDetails.length > 0 ? ` ${errorDetails.join(". ")}.` : "";
37169
37365
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
37170
37366
  this.rateLimitWarning = `Search returned no results due to GitHub API rate limits.${details}${budgetNote} Try again after the rate limit resets.`;
37171
37367
  return { candidates: [], strategiesUsed };
@@ -37176,7 +37372,12 @@ var IssueDiscovery = class {
37176
37372
  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
37373
  }
37178
37374
  allCandidates.sort((a, b) => {
37179
- const priorityOrder = { merged_pr: 0, preferred_org: 1, starred: 2, normal: 3 };
37375
+ const priorityOrder = {
37376
+ merged_pr: 0,
37377
+ preferred_org: 1,
37378
+ starred: 2,
37379
+ normal: 3
37380
+ };
37180
37381
  const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
37181
37382
  if (priorityDiff !== 0)
37182
37383
  return priorityDiff;
@@ -37323,6 +37524,8 @@ var GistStateStore = class {
37323
37524
  this.writeCache(state);
37324
37525
  return { gistId: foundId, state, created: false };
37325
37526
  }
37527
+ warn(MODULE10, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
37528
+ return this.bootstrapFromCache();
37326
37529
  }
37327
37530
  debug(MODULE10, "No existing gist found, creating new one");
37328
37531
  const freshState = ScoutStateSchema.parse({ version: 1 });
@@ -37418,7 +37621,9 @@ var GistStateStore = class {
37418
37621
  }
37419
37622
  writeCache(state) {
37420
37623
  try {
37421
- fs3.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", { mode: 384 });
37624
+ fs3.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", {
37625
+ mode: 384
37626
+ });
37422
37627
  } catch (err) {
37423
37628
  warn(MODULE10, `Failed to write cache: ${errorMessage(err)}`);
37424
37629
  }
@@ -37521,13 +37726,50 @@ function loadLocalState() {
37521
37726
  }
37522
37727
 
37523
37728
  // ../core/dist/scout.js
37729
+ function toGistOctokit(octokit) {
37730
+ return {
37731
+ gists: {
37732
+ async get(params) {
37733
+ const { data } = await octokit.gists.get(params);
37734
+ if (!data.id)
37735
+ throw new Error("Gist get returned no id");
37736
+ const files = data.files ? Object.fromEntries(Object.entries(data.files).map(([k, v]) => [
37737
+ k,
37738
+ v ? { content: v.content } : void 0
37739
+ ])) : null;
37740
+ return { data: { id: data.id, files } };
37741
+ },
37742
+ async create(params) {
37743
+ const { data } = await octokit.gists.create(params);
37744
+ if (!data.id)
37745
+ throw new Error("Gist create returned no id");
37746
+ return { data: { id: data.id } };
37747
+ },
37748
+ async update(params) {
37749
+ const { data } = await octokit.gists.update(params);
37750
+ if (!data.id)
37751
+ throw new Error("Gist update returned no id");
37752
+ return { data: { id: data.id } };
37753
+ },
37754
+ async list(params) {
37755
+ const { data } = await octokit.gists.list(params);
37756
+ return {
37757
+ data: data.filter((g) => g.id).map((g) => ({
37758
+ id: g.id,
37759
+ description: g.description ?? null
37760
+ }))
37761
+ };
37762
+ }
37763
+ }
37764
+ };
37765
+ }
37524
37766
  async function createScout(config2) {
37525
37767
  let state;
37526
37768
  let gistStore = null;
37527
37769
  if (config2.persistence === "provided") {
37528
37770
  state = config2.initialState;
37529
37771
  } else if (config2.persistence === "gist") {
37530
- gistStore = new GistStateStore(getOctokit(config2.githubToken));
37772
+ gistStore = new GistStateStore(toGistOctokit(getOctokit(config2.githubToken)));
37531
37773
  const result = await gistStore.bootstrap();
37532
37774
  if (result.degraded) {
37533
37775
  warn("scout", "Gist sync unavailable \u2014 running in offline mode. Changes will only be saved locally.");
@@ -37653,7 +37895,7 @@ var OssScout = class {
37653
37895
  getReposWithMergedPRs() {
37654
37896
  const repoCounts = /* @__PURE__ */ new Map();
37655
37897
  for (const pr of this.state.mergedPRs ?? []) {
37656
- const repo = this.extractRepoFromUrl(pr.url);
37898
+ const repo = extractRepoFromUrl(pr.url);
37657
37899
  if (repo) {
37658
37900
  repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
37659
37901
  }
@@ -37826,17 +38068,13 @@ var OssScout = class {
37826
38068
  return this.state;
37827
38069
  }
37828
38070
  // ── Private helpers ─────────────────────────────────────────────────
37829
- extractRepoFromUrl(url2) {
37830
- const match = url2.match(/github\.com\/([^/]+\/[^/]+)\//);
37831
- return match ? match[1] : null;
37832
- }
37833
38071
  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;
38072
+ const mergedCount = (this.state.mergedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
38073
+ const closedCount = (this.state.closedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
37836
38074
  this.updateRepoScore(repo, {
37837
38075
  mergedPRCount: mergedCount,
37838
38076
  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
38077
+ 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
38078
  });
37841
38079
  }
37842
38080
  /**
@@ -37865,26 +38103,52 @@ function registerTools(server, scout) {
37865
38103
  "Search for open source issues matching your preferences",
37866
38104
  {
37867
38105
  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")
38106
+ strategies: external_exports3.string().optional().describe(
38107
+ "Comma-separated search strategies: merged, orgs, starred, broad, maintained, all"
38108
+ )
37869
38109
  },
37870
38110
  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) }] };
38111
+ try {
38112
+ const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
38113
+ const result = await scout.search({
38114
+ maxResults: maxResults ?? 10,
38115
+ strategies: parsedStrategies
38116
+ });
38117
+ scout.saveResults(result.candidates);
38118
+ await scout.checkpoint();
38119
+ return {
38120
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
38121
+ };
38122
+ } catch (err) {
38123
+ const msg = err instanceof Error ? err.message : String(err);
38124
+ return {
38125
+ content: [{ type: "text", text: `Error: ${msg}` }],
38126
+ isError: true
38127
+ };
38128
+ }
37877
38129
  }
37878
38130
  );
37879
38131
  server.tool(
37880
38132
  "vet",
37881
38133
  "Vet a specific GitHub issue for contribution viability",
37882
38134
  {
37883
- issueUrl: external_exports3.string().describe("Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)")
38135
+ issueUrl: external_exports3.string().describe(
38136
+ "Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)"
38137
+ )
37884
38138
  },
37885
38139
  async ({ issueUrl }) => {
37886
- const candidate = await scout.vetIssue(issueUrl);
37887
- return { content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }] };
38140
+ try {
38141
+ const candidate = await scout.vetIssue(issueUrl);
38142
+ return {
38143
+ content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }]
38144
+ };
38145
+ } catch (err) {
38146
+ const msg = err instanceof Error ? err.message : String(err);
38147
+ return {
38148
+ content: [{ type: "text", text: `Error: ${msg}` }],
38149
+ isError: true
38150
+ };
38151
+ }
37888
38152
  }
37889
38153
  );
37890
38154
  server.tool(
@@ -37892,7 +38156,9 @@ function registerTools(server, scout) {
37892
38156
  "Show current oss-scout configuration preferences",
37893
38157
  async () => {
37894
38158
  const preferences = scout.getPreferences();
37895
- return { content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }] };
38159
+ return {
38160
+ content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }]
38161
+ };
37896
38162
  }
37897
38163
  );
37898
38164
  const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
@@ -37902,23 +38168,37 @@ function registerTools(server, scout) {
37902
38168
  "preferredOrgs",
37903
38169
  "projectCategories",
37904
38170
  "excludeRepos",
38171
+ "excludeOrgs",
37905
38172
  "aiPolicyBlocklist",
37906
38173
  "scope",
37907
38174
  "defaultStrategy"
37908
38175
  ]);
37909
- const NUMBER_KEYS = /* @__PURE__ */ new Set(["minStars", "maxIssueAgeDays", "minRepoScoreThreshold"]);
38176
+ const NUMBER_KEYS = /* @__PURE__ */ new Set([
38177
+ "minStars",
38178
+ "maxIssueAgeDays",
38179
+ "minRepoScoreThreshold"
38180
+ ]);
37910
38181
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
37911
38182
  server.tool(
37912
38183
  "config-set",
37913
38184
  "Update an oss-scout configuration preference",
37914
38185
  {
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")
38186
+ key: external_exports3.string().describe(
38187
+ "Preference key to update (e.g. languages, minStars, excludeRepos)"
38188
+ ),
38189
+ value: external_exports3.string().describe(
38190
+ "New value \u2014 comma-separated for arrays, plain string for scalars"
38191
+ )
37917
38192
  },
37918
38193
  async ({ key, value }) => {
37919
38194
  if (!VALID_KEYS.includes(key)) {
37920
38195
  return {
37921
- content: [{ type: "text", text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}` }],
38196
+ content: [
38197
+ {
38198
+ type: "text",
38199
+ text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`
38200
+ }
38201
+ ],
37922
38202
  isError: true
37923
38203
  };
37924
38204
  }
@@ -37936,7 +38216,9 @@ function registerTools(server, scout) {
37936
38216
  const num = Number(value);
37937
38217
  if (isNaN(num)) {
37938
38218
  return {
37939
- content: [{ type: "text", text: `Invalid number for "${key}": "${value}"` }],
38219
+ content: [
38220
+ { type: "text", text: `Invalid number for "${key}": "${value}"` }
38221
+ ],
37940
38222
  isError: true
37941
38223
  };
37942
38224
  }
@@ -37947,7 +38229,12 @@ function registerTools(server, scout) {
37947
38229
  else if (lower === "false" || lower === "no") parsed = false;
37948
38230
  else {
37949
38231
  return {
37950
- content: [{ type: "text", text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.` }],
38232
+ content: [
38233
+ {
38234
+ type: "text",
38235
+ text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`
38236
+ }
38237
+ ],
37951
38238
  isError: true
37952
38239
  };
37953
38240
  }
@@ -37965,8 +38252,11 @@ function registerTools(server, scout) {
37965
38252
  };
37966
38253
  }
37967
38254
  scout.updatePreferences({ [key]: parsed });
38255
+ await scout.checkpoint();
37968
38256
  const updated = scout.getPreferences();
37969
- return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
38257
+ return {
38258
+ content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
38259
+ };
37970
38260
  }
37971
38261
  );
37972
38262
  }
@@ -37978,11 +38268,13 @@ function registerResources(server, scout) {
37978
38268
  "scout://config",
37979
38269
  { description: "Current oss-scout preferences" },
37980
38270
  async () => ({
37981
- contents: [{
37982
- uri: "scout://config",
37983
- mimeType: "application/json",
37984
- text: JSON.stringify(scout.getPreferences(), null, 2)
37985
- }]
38271
+ contents: [
38272
+ {
38273
+ uri: "scout://config",
38274
+ mimeType: "application/json",
38275
+ text: JSON.stringify(scout.getPreferences(), null, 2)
38276
+ }
38277
+ ]
37986
38278
  })
37987
38279
  );
37988
38280
  server.resource(
@@ -37990,11 +38282,13 @@ function registerResources(server, scout) {
37990
38282
  "scout://results",
37991
38283
  { description: "Saved search results from previous runs" },
37992
38284
  async () => ({
37993
- contents: [{
37994
- uri: "scout://results",
37995
- mimeType: "application/json",
37996
- text: JSON.stringify(scout.getSavedResults(), null, 2)
37997
- }]
38285
+ contents: [
38286
+ {
38287
+ uri: "scout://results",
38288
+ mimeType: "application/json",
38289
+ text: JSON.stringify(scout.getSavedResults(), null, 2)
38290
+ }
38291
+ ]
37998
38292
  })
37999
38293
  );
38000
38294
  server.resource(
@@ -38004,11 +38298,13 @@ function registerResources(server, scout) {
38004
38298
  async () => {
38005
38299
  const state = scout.getState();
38006
38300
  return {
38007
- contents: [{
38008
- uri: "scout://scores",
38009
- mimeType: "application/json",
38010
- text: JSON.stringify(state.repoScores, null, 2)
38011
- }]
38301
+ contents: [
38302
+ {
38303
+ uri: "scout://scores",
38304
+ mimeType: "application/json",
38305
+ text: JSON.stringify(state.repoScores, null, 2)
38306
+ }
38307
+ ]
38012
38308
  };
38013
38309
  }
38014
38310
  );
@@ -38034,8 +38330,10 @@ async function main() {
38034
38330
  await server.connect(transport);
38035
38331
  }
38036
38332
  main().catch((err) => {
38037
- process.stderr.write(`oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
38038
- `);
38333
+ process.stderr.write(
38334
+ `oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
38335
+ `
38336
+ );
38039
38337
  process.exit(1);
38040
38338
  });
38041
38339
  /*! Bundled license information: