@oss-scout/mcp 0.1.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(),
@@ -35762,6 +35805,7 @@ var ScoutPreferencesSchema = external_exports3.object({
35762
35805
  labels: external_exports3.array(external_exports3.string()).default(["good first issue", "help wanted"]),
35763
35806
  scope: external_exports3.array(IssueScopeSchema).optional(),
35764
35807
  excludeRepos: external_exports3.array(external_exports3.string()).default([]),
35808
+ excludeOrgs: external_exports3.array(external_exports3.string()).default([]),
35765
35809
  aiPolicyBlocklist: external_exports3.array(external_exports3.string()).default(["matplotlib/matplotlib"]),
35766
35810
  preferredOrgs: external_exports3.array(external_exports3.string()).default([]),
35767
35811
  projectCategories: external_exports3.array(ProjectCategorySchema).default([]),
@@ -35787,7 +35831,12 @@ var ScoutStateSchema = external_exports3.object({
35787
35831
  });
35788
35832
 
35789
35833
  // ../core/dist/core/issue-filtering.js
35790
- 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
+ ]);
35791
35840
  function isDocOnlyIssue(item) {
35792
35841
  if (!item.labels || !Array.isArray(item.labels) || item.labels.length === 0)
35793
35842
  return false;
@@ -35826,7 +35875,9 @@ function detectLabelFarmingRepos(items) {
35826
35875
  const spamRepos = /* @__PURE__ */ new Set();
35827
35876
  const repoSpamCounts = /* @__PURE__ */ new Map();
35828
35877
  for (const item of items) {
35829
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
35878
+ const repoFullName = extractRepoFromUrl(item.repository_url);
35879
+ if (!repoFullName)
35880
+ continue;
35830
35881
  if (isLabelFarming(item)) {
35831
35882
  spamRepos.add(repoFullName);
35832
35883
  continue;
@@ -35912,20 +35963,111 @@ function calculateViabilityScore(params) {
35912
35963
 
35913
35964
  // ../core/dist/core/category-mapping.js
35914
35965
  var CATEGORY_TOPICS = {
35915
- nonprofit: ["nonprofit", "social-good", "humanitarian", "charity", "social-impact", "civic-tech"],
35916
- devtools: ["developer-tools", "devtools", "cli", "sdk", "linter", "formatter", "build-tool"],
35917
- infrastructure: ["infrastructure", "cloud", "kubernetes", "docker", "devops", "monitoring", "observability"],
35918
- "web-frameworks": ["web-framework", "frontend", "backend", "fullstack", "nextjs", "react", "vue"],
35919
- "data-ml": ["machine-learning", "data-science", "deep-learning", "nlp", "data-pipeline", "analytics"],
35920
- 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
+ ]
35921
36017
  };
35922
36018
  var CATEGORY_ORGS = {
35923
- nonprofit: ["code-for-america", "opengovfoundation", "ushahidi", "hotosm", "openfn", "democracyearth"],
35924
- devtools: ["eslint", "prettier", "vitejs", "biomejs", "oxc-project", "ast-grep", "turbot"],
35925
- infrastructure: ["kubernetes", "hashicorp", "grafana", "prometheus", "open-telemetry", "envoyproxy", "cncf"],
35926
- "web-frameworks": ["vercel", "remix-run", "sveltejs", "nuxt", "astro", "redwoodjs", "blitz-js"],
35927
- "data-ml": ["huggingface", "mlflow", "apache", "dbt-labs", "dagster-io", "prefecthq", "langchain-ai"],
35928
- 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
+ ]
35929
36071
  };
35930
36072
  function repoBelongsToCategory(repoFullName, categories) {
35931
36073
  if (categories.length === 0)
@@ -36045,7 +36187,10 @@ var HttpCache = class {
36045
36187
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
36046
36188
  };
36047
36189
  try {
36048
- 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
+ });
36049
36194
  debug(MODULE4, `Cached response for ${url2}`);
36050
36195
  } catch (err) {
36051
36196
  warn(MODULE4, `Failed to write cache for ${url2}: ${errorMessage(err)}`);
@@ -36300,7 +36445,12 @@ function analyzeRequirements(body) {
36300
36445
  const hasSteps = /\d\.|[-*]\s/.test(body);
36301
36446
  const hasCodeBlock = /```/.test(body);
36302
36447
  const hasExpectedBehavior = /expect|should|must|want/i.test(body);
36303
- const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
36448
+ const indicators = [
36449
+ hasSteps,
36450
+ hasCodeBlock,
36451
+ hasExpectedBehavior,
36452
+ body.length > 200
36453
+ ];
36304
36454
  return indicators.filter(Boolean).length >= 2;
36305
36455
  }
36306
36456
 
@@ -36377,7 +36527,12 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
36377
36527
  if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
36378
36528
  return cached2.guidelines;
36379
36529
  }
36380
- 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
+ ];
36381
36536
  const results = await Promise.allSettled(filesToCheck.map((file2) => octokit.repos.getContent({ owner, repo, path: file2 }).then(({ data }) => {
36382
36537
  if ("content" in data) {
36383
36538
  return Buffer.from(data.content, "base64").toString("utf-8");
@@ -36399,7 +36554,10 @@ async function fetchContributionGuidelines(octokit, owner, repo) {
36399
36554
  }
36400
36555
  }
36401
36556
  }
36402
- guidelinesCache.set(cacheKey, { guidelines: void 0, fetchedAt: Date.now() });
36557
+ guidelinesCache.set(cacheKey, {
36558
+ guidelines: void 0,
36559
+ fetchedAt: Date.now()
36560
+ });
36403
36561
  pruneCache();
36404
36562
  return void 0;
36405
36563
  }
@@ -36661,7 +36819,11 @@ var IssueVetter = class {
36661
36819
  if (allFailed) {
36662
36820
  warn(MODULE7, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
36663
36821
  }
36664
- 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
+ };
36665
36827
  }
36666
36828
  };
36667
36829
 
@@ -36762,11 +36924,13 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
36762
36924
  async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
36763
36925
  const spamRepos = detectLabelFarmingRepos(items);
36764
36926
  if (spamRepos.size > 0) {
36765
- 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;
36766
36928
  debug(MODULE8, `[SPAM_FILTER] Filtered ${spamCount} issues from ${spamRepos.size} label-farming repos: ${[...spamRepos].join(", ")}`);
36767
36929
  }
36768
36930
  const itemsToVet = filterIssues(items).filter((item) => {
36769
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
36931
+ const repoFullName = extractRepoFromUrl(item.repository_url);
36932
+ if (!repoFullName)
36933
+ return false;
36770
36934
  if (spamRepos.has(repoFullName))
36771
36935
  return false;
36772
36936
  return excludedRepoSets.every((s) => !s.has(repoFullName));
@@ -36834,6 +36998,182 @@ var MODULE9 = "issue-discovery";
36834
36998
  var INTER_PHASE_DELAY_MS = 2e3;
36835
36999
  var LOW_BUDGET_THRESHOLD = 20;
36836
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
+ }
36837
37177
  var IssueDiscovery = class {
36838
37178
  preferences;
36839
37179
  stateReader;
@@ -36897,8 +37237,7 @@ var IssueDiscovery = class {
36897
37237
  const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
36898
37238
  const strategiesUsed = [];
36899
37239
  const allCandidates = [];
36900
- let phase0Error = null;
36901
- let phase1Error = null;
37240
+ const phaseErrors = {};
36902
37241
  let rateLimitHitDuringSearch = false;
36903
37242
  this.rateLimitWarning = null;
36904
37243
  const tracker = getSearchBudgetTracker();
@@ -36918,65 +37257,44 @@ var IssueDiscovery = class {
36918
37257
  info(MODULE9, `Search budget low (${searchBudget} remaining) \u2014 skipping heavy phases (2, 3)`);
36919
37258
  }
36920
37259
  } catch (error48) {
36921
- if (getHttpStatusCode(error48) === 401) {
37260
+ if (getHttpStatusCode(error48) === 401)
36922
37261
  throw error48;
36923
- }
36924
37262
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
36925
37263
  warn(MODULE9, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
36926
37264
  }
36927
37265
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
36928
37266
  const starredRepos = this.getStarredRepos();
36929
37267
  const starredRepoSet = new Set(starredRepos);
36930
- const minRepoScoreThreshold = config2.minRepoScoreThreshold;
36931
- const lowScoringRepos = new Set(this.deriveLowScoringRepos(minRepoScoreThreshold));
36932
- const excludedRepos = new Set(config2.excludeRepos);
36933
- const maxAgeDays = config2.maxIssueAgeDays || 90;
36934
- const now = /* @__PURE__ */ new Date();
37268
+ const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
36935
37269
  const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
36936
37270
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
36937
37271
  const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`.replace(/ +/g, " ").trim();
36938
- const includeDocIssues = config2.includeDocIssues ?? true;
36939
37272
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
36940
37273
  if (aiBlocklisted.size > 0) {
36941
37274
  debug(MODULE9, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
36942
37275
  }
36943
- const filterIssues = (items) => {
36944
- return items.filter((item) => {
36945
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
36946
- if (excludedRepos.has(repoFullName))
36947
- return false;
36948
- if (aiBlocklisted.has(repoFullName))
36949
- return false;
36950
- if (lowScoringRepos.has(repoFullName))
36951
- return false;
36952
- const updatedAt = new Date(item.updated_at);
36953
- const ageDays = daysBetween(updatedAt, now);
36954
- if (ageDays > maxAgeDays)
36955
- return false;
36956
- if (!includeDocIssues && isDocOnlyIssue(item))
36957
- return false;
36958
- return true;
36959
- });
36960
- };
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
+ });
36961
37285
  const phase0Repos = mergedPRRepos.slice(0, 10);
36962
37286
  const phase0RepoSet = new Set(phase0Repos);
36963
37287
  if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
36964
- info(MODULE9, `Phase 0: Searching issues in ${phase0Repos.length} merged-PR repos (no label filter)...`);
36965
- const remainingNeeded = maxResults - allCandidates.length;
36966
- if (remainingNeeded > 0) {
36967
- const { candidates: mergedCandidates, allBatchesFailed, rateLimitHit } = await searchInRepos(this.octokit, this.vetter, phase0Repos, baseQualifiers, [], remainingNeeded, "merged_pr", filterIssues);
36968
- allCandidates.push(...mergedCandidates);
36969
- if (allBatchesFailed) {
36970
- phase0Error = "All merged-PR repo batches failed";
36971
- }
36972
- 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)
36973
37294
  rateLimitHitDuringSearch = true;
36974
- }
36975
- info(MODULE9, `Found ${mergedCandidates.length} candidates from merged-PR repos`);
36976
37295
  }
36977
37296
  strategiesUsed.push("merged");
36978
37297
  }
36979
- let phase0_5Error = null;
36980
37298
  const preferredOrgs = config2.preferredOrgs ?? [];
36981
37299
  if (allCandidates.length < maxResults && preferredOrgs.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("orgs")) {
36982
37300
  if (phase0Repos.length > 0)
@@ -36984,35 +37302,12 @@ var IssueDiscovery = class {
36984
37302
  const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
36985
37303
  const orgsToSearch = preferredOrgs.filter((org) => !phase0Orgs.has(org.toLowerCase())).slice(0, 5);
36986
37304
  if (orgsToSearch.length > 0) {
36987
- info(MODULE9, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
36988
- const remainingNeeded = maxResults - allCandidates.length;
36989
- const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(" OR ");
36990
- const orgOps = orgsToSearch.length - 1;
36991
- try {
36992
- const allItems = await searchWithChunkedLabels(this.octokit, labels, orgOps, (labelQ) => `${baseQualifiers} ${labelQ} (${orgRepoFilter})`.replace(/ +/g, " ").trim(), remainingNeeded * 3);
36993
- if (allItems.length > 0) {
36994
- const filtered = filterIssues(allItems).filter((item) => {
36995
- const repoFullName = item.repository_url.split("/").slice(-2).join("/");
36996
- return !phase0RepoSet.has(repoFullName);
36997
- });
36998
- const { candidates: orgCandidates, allFailed: allVetFailed, rateLimitHit } = await this.vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((i) => i.html_url), remainingNeeded, "preferred_org");
36999
- allCandidates.push(...orgCandidates);
37000
- if (allVetFailed) {
37001
- phase0_5Error = "All preferred org issue vetting failed";
37002
- }
37003
- if (rateLimitHit) {
37004
- rateLimitHitDuringSearch = true;
37005
- }
37006
- info(MODULE9, `Found ${orgCandidates.length} candidates from preferred orgs`);
37007
- }
37008
- } catch (error48) {
37009
- const errMsg = errorMessage(error48);
37010
- phase0_5Error = errMsg;
37011
- if (isRateLimitError(error48)) {
37012
- rateLimitHitDuringSearch = true;
37013
- }
37014
- warn(MODULE9, `Error searching preferred orgs: ${errMsg}`);
37015
- }
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;
37016
37311
  }
37017
37312
  strategiesUsed.push("orgs");
37018
37313
  }
@@ -37020,127 +37315,35 @@ var IssueDiscovery = class {
37020
37315
  await sleep(INTER_PHASE_DELAY_MS);
37021
37316
  const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
37022
37317
  if (reposToSearch.length > 0) {
37023
- info(MODULE9, `Phase 1: Searching issues in ${reposToSearch.length} starred repos...`);
37024
- const remainingNeeded = maxResults - allCandidates.length;
37025
- if (remainingNeeded > 0) {
37026
- const phase1Labels = labels.slice(0, 3);
37027
- const { candidates: starredCandidates, allBatchesFailed, rateLimitHit } = await searchInRepos(this.octokit, this.vetter, reposToSearch.slice(0, 10), baseQualifiers, phase1Labels, remainingNeeded, "starred", filterIssues);
37028
- allCandidates.push(...starredCandidates);
37029
- if (allBatchesFailed) {
37030
- phase1Error = "All starred repo batches failed";
37031
- }
37032
- 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)
37033
37324
  rateLimitHitDuringSearch = true;
37034
- }
37035
- info(MODULE9, `Found ${starredCandidates.length} candidates from starred repos`);
37036
37325
  }
37037
37326
  }
37038
37327
  strategiesUsed.push("starred");
37039
37328
  }
37040
- let phase2Error = null;
37041
37329
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
37042
37330
  await sleep(INTER_PHASE_DELAY_MS);
37043
- info(MODULE9, "Phase 2: General issue search...");
37044
- const remainingNeeded = maxResults - allCandidates.length;
37045
- const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
37046
- const tierLabelGroups = [];
37047
- if (scopes && scopes.length > 1) {
37048
- for (const scope of scopes) {
37049
- const scopeLabels = SCOPE_LABELS[scope] ?? [];
37050
- if (scopeLabels.length === 0) {
37051
- warn(MODULE9, `Scope "${scope}" has no labels, skipping tier`);
37052
- continue;
37053
- }
37054
- tierLabelGroups.push({ tier: scope, tierLabels: scopeLabels });
37055
- }
37056
- const allScopeLabels = new Set(scopes.flatMap((s) => SCOPE_LABELS[s] ?? []));
37057
- const customOnly = config2.labels.filter((l) => !allScopeLabels.has(l));
37058
- if (customOnly.length > 0) {
37059
- tierLabelGroups.push({ tier: "custom", tierLabels: customOnly });
37060
- }
37061
- } else {
37062
- tierLabelGroups.push({ tier: "general", tierLabels: labels });
37063
- }
37064
- const budgetPerTier = Math.ceil(remainingNeeded / tierLabelGroups.length);
37065
- const tierResults = [];
37066
- for (const { tier, tierLabels } of tierLabelGroups) {
37067
- try {
37068
- const allItems = await searchWithChunkedLabels(
37069
- this.octokit,
37070
- tierLabels,
37071
- 0,
37072
- // no repo/org ORs in Phase 2
37073
- (labelQ) => `${baseQualifiers} ${labelQ}`.replace(/ +/g, " ").trim(),
37074
- budgetPerTier * 3
37075
- );
37076
- info(MODULE9, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
37077
- const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(this.vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
37078
- tierResults.push(tierCandidates);
37079
- for (const c of tierCandidates)
37080
- seenRepos.add(c.issue.repo);
37081
- if (allVetFailed) {
37082
- phase2Error = (phase2Error ? phase2Error + "; " : "") + `${tier}: all vetting failed`;
37083
- }
37084
- if (vetRateLimitHit) {
37085
- rateLimitHitDuringSearch = true;
37086
- }
37087
- info(MODULE9, `Found ${tierCandidates.length} candidates from ${tier} tier`);
37088
- } catch (error48) {
37089
- if (getHttpStatusCode(error48) === 401)
37090
- throw error48;
37091
- const errMsg = errorMessage(error48);
37092
- phase2Error = (phase2Error ? phase2Error + "; " : "") + `${tier}: ${errMsg}`;
37093
- if (isRateLimitError(error48)) {
37094
- rateLimitHitDuringSearch = true;
37095
- }
37096
- warn(MODULE9, `Error in ${tier} tier search: ${errMsg}`);
37097
- tierResults.push([]);
37098
- }
37099
- }
37100
- const interleaved = interleaveArrays(tierResults);
37101
- if (interleaved.length === 0 && phase2Error) {
37102
- warn(MODULE9, `All ${tierLabelGroups.length} scope tiers failed in Phase 2: ${phase2Error}`);
37103
- }
37104
- 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;
37105
37337
  strategiesUsed.push("broad");
37106
37338
  }
37107
- let phase3Error = null;
37108
37339
  if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
37109
37340
  await sleep(INTER_PHASE_DELAY_MS);
37110
- info(MODULE9, "Phase 3: Searching actively maintained repos...");
37111
- const remainingNeeded = maxResults - allCandidates.length;
37112
- const thirtyDaysAgo = /* @__PURE__ */ new Date();
37113
- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
37114
- const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
37115
- const categoryTopics = getTopicsForCategories(config2.projectCategories ?? []);
37116
- const topicQuery = categoryTopics.length > 0 ? `topic:${categoryTopics[0]}` : "";
37117
- const phase3Query = `is:issue is:open no:assignee ${langQuery} ${topicQuery} stars:>=${minStars} pushed:>=${pushedSince} archived:false`.replace(/ +/g, " ").trim();
37118
- try {
37119
- const data = await cachedSearchIssues(this.octokit, {
37120
- q: phase3Query,
37121
- sort: "updated",
37122
- order: "desc",
37123
- per_page: remainingNeeded * 3
37124
- });
37125
- info(MODULE9, `Found ${data.total_count} issues in maintained-repo search, processing top ${data.items.length}...`);
37126
- const seenRepos = new Set(allCandidates.map((c) => c.issue.repo));
37127
- const { candidates: starFiltered, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(this.vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], remainingNeeded, minStars, "Phase 3");
37128
- allCandidates.push(...starFiltered);
37129
- if (allVetFailed) {
37130
- phase3Error = "all vetting failed";
37131
- }
37132
- if (vetRateLimitHit) {
37133
- rateLimitHitDuringSearch = true;
37134
- }
37135
- info(MODULE9, `Found ${starFiltered.length} candidates from maintained-repo search`);
37136
- } catch (error48) {
37137
- const errMsg = errorMessage(error48);
37138
- phase3Error = errMsg;
37139
- if (isRateLimitError(error48)) {
37140
- rateLimitHitDuringSearch = true;
37141
- }
37142
- warn(MODULE9, `Error in maintained-repo search: ${errMsg}`);
37143
- }
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;
37144
37347
  strategiesUsed.push("maintained");
37145
37348
  }
37146
37349
  const phasesSkippedForBudget = searchBudget < LOW_BUDGET_THRESHOLD;
@@ -37151,14 +37354,14 @@ var IssueDiscovery = class {
37151
37354
  budgetNote = ` Some search phases were skipped due to low API quota (${searchBudget} remaining).`;
37152
37355
  }
37153
37356
  if (allCandidates.length === 0) {
37154
- const phaseErrors = [
37155
- phase0Error ? `Phase 0 (merged-PR repos): ${phase0Error}` : null,
37156
- phase0_5Error ? `Phase 0.5 (preferred orgs): ${phase0_5Error}` : null,
37157
- phase1Error ? `Phase 1 (starred repos): ${phase1Error}` : null,
37158
- phase2Error ? `Phase 2 (general): ${phase2Error}` : null,
37159
- 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
37160
37363
  ].filter(Boolean);
37161
- const details = phaseErrors.length > 0 ? ` ${phaseErrors.join(". ")}.` : "";
37364
+ const details = errorDetails.length > 0 ? ` ${errorDetails.join(". ")}.` : "";
37162
37365
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
37163
37366
  this.rateLimitWarning = `Search returned no results due to GitHub API rate limits.${details}${budgetNote} Try again after the rate limit resets.`;
37164
37367
  return { candidates: [], strategiesUsed };
@@ -37169,7 +37372,12 @@ var IssueDiscovery = class {
37169
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.`;
37170
37373
  }
37171
37374
  allCandidates.sort((a, b) => {
37172
- 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
+ };
37173
37381
  const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
37174
37382
  if (priorityDiff !== 0)
37175
37383
  return priorityDiff;
@@ -37316,6 +37524,8 @@ var GistStateStore = class {
37316
37524
  this.writeCache(state);
37317
37525
  return { gistId: foundId, state, created: false };
37318
37526
  }
37527
+ warn(MODULE10, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
37528
+ return this.bootstrapFromCache();
37319
37529
  }
37320
37530
  debug(MODULE10, "No existing gist found, creating new one");
37321
37531
  const freshState = ScoutStateSchema.parse({ version: 1 });
@@ -37411,7 +37621,9 @@ var GistStateStore = class {
37411
37621
  }
37412
37622
  writeCache(state) {
37413
37623
  try {
37414
- 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
+ });
37415
37627
  } catch (err) {
37416
37628
  warn(MODULE10, `Failed to write cache: ${errorMessage(err)}`);
37417
37629
  }
@@ -37514,13 +37726,50 @@ function loadLocalState() {
37514
37726
  }
37515
37727
 
37516
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
+ }
37517
37766
  async function createScout(config2) {
37518
37767
  let state;
37519
37768
  let gistStore = null;
37520
37769
  if (config2.persistence === "provided") {
37521
37770
  state = config2.initialState;
37522
37771
  } else if (config2.persistence === "gist") {
37523
- gistStore = new GistStateStore(getOctokit(config2.githubToken));
37772
+ gistStore = new GistStateStore(toGistOctokit(getOctokit(config2.githubToken)));
37524
37773
  const result = await gistStore.bootstrap();
37525
37774
  if (result.degraded) {
37526
37775
  warn("scout", "Gist sync unavailable \u2014 running in offline mode. Changes will only be saved locally.");
@@ -37646,7 +37895,7 @@ var OssScout = class {
37646
37895
  getReposWithMergedPRs() {
37647
37896
  const repoCounts = /* @__PURE__ */ new Map();
37648
37897
  for (const pr of this.state.mergedPRs ?? []) {
37649
- const repo = this.extractRepoFromUrl(pr.url);
37898
+ const repo = extractRepoFromUrl(pr.url);
37650
37899
  if (repo) {
37651
37900
  repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
37652
37901
  }
@@ -37819,17 +38068,13 @@ var OssScout = class {
37819
38068
  return this.state;
37820
38069
  }
37821
38070
  // ── Private helpers ─────────────────────────────────────────────────
37822
- extractRepoFromUrl(url2) {
37823
- const match = url2.match(/github\.com\/([^/]+\/[^/]+)\//);
37824
- return match ? match[1] : null;
37825
- }
37826
38071
  updateRepoScoreFromPRs(repo) {
37827
- const mergedCount = (this.state.mergedPRs ?? []).filter((p) => this.extractRepoFromUrl(p.url) === repo).length;
37828
- 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;
37829
38074
  this.updateRepoScore(repo, {
37830
38075
  mergedPRCount: mergedCount,
37831
38076
  closedWithoutMergeCount: closedCount,
37832
- 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
37833
38078
  });
37834
38079
  }
37835
38080
  /**
@@ -37858,26 +38103,52 @@ function registerTools(server, scout) {
37858
38103
  "Search for open source issues matching your preferences",
37859
38104
  {
37860
38105
  maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
37861
- 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
+ )
37862
38109
  },
37863
38110
  async ({ maxResults, strategies }) => {
37864
- const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
37865
- const result = await scout.search({
37866
- maxResults: maxResults ?? 10,
37867
- strategies: parsedStrategies
37868
- });
37869
- 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
+ }
37870
38129
  }
37871
38130
  );
37872
38131
  server.tool(
37873
38132
  "vet",
37874
38133
  "Vet a specific GitHub issue for contribution viability",
37875
38134
  {
37876
- 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
+ )
37877
38138
  },
37878
38139
  async ({ issueUrl }) => {
37879
- const candidate = await scout.vetIssue(issueUrl);
37880
- 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
+ }
37881
38152
  }
37882
38153
  );
37883
38154
  server.tool(
@@ -37885,7 +38156,9 @@ function registerTools(server, scout) {
37885
38156
  "Show current oss-scout configuration preferences",
37886
38157
  async () => {
37887
38158
  const preferences = scout.getPreferences();
37888
- return { content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }] };
38159
+ return {
38160
+ content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }]
38161
+ };
37889
38162
  }
37890
38163
  );
37891
38164
  const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
@@ -37895,23 +38168,37 @@ function registerTools(server, scout) {
37895
38168
  "preferredOrgs",
37896
38169
  "projectCategories",
37897
38170
  "excludeRepos",
38171
+ "excludeOrgs",
37898
38172
  "aiPolicyBlocklist",
37899
38173
  "scope",
37900
38174
  "defaultStrategy"
37901
38175
  ]);
37902
- const NUMBER_KEYS = /* @__PURE__ */ new Set(["minStars", "maxIssueAgeDays", "minRepoScoreThreshold"]);
38176
+ const NUMBER_KEYS = /* @__PURE__ */ new Set([
38177
+ "minStars",
38178
+ "maxIssueAgeDays",
38179
+ "minRepoScoreThreshold"
38180
+ ]);
37903
38181
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
37904
38182
  server.tool(
37905
38183
  "config-set",
37906
38184
  "Update an oss-scout configuration preference",
37907
38185
  {
37908
- key: external_exports3.string().describe("Preference key to update (e.g. languages, minStars, excludeRepos)"),
37909
- 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
+ )
37910
38192
  },
37911
38193
  async ({ key, value }) => {
37912
38194
  if (!VALID_KEYS.includes(key)) {
37913
38195
  return {
37914
- 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
+ ],
37915
38202
  isError: true
37916
38203
  };
37917
38204
  }
@@ -37929,7 +38216,9 @@ function registerTools(server, scout) {
37929
38216
  const num = Number(value);
37930
38217
  if (isNaN(num)) {
37931
38218
  return {
37932
- content: [{ type: "text", text: `Invalid number for "${key}": "${value}"` }],
38219
+ content: [
38220
+ { type: "text", text: `Invalid number for "${key}": "${value}"` }
38221
+ ],
37933
38222
  isError: true
37934
38223
  };
37935
38224
  }
@@ -37940,7 +38229,12 @@ function registerTools(server, scout) {
37940
38229
  else if (lower === "false" || lower === "no") parsed = false;
37941
38230
  else {
37942
38231
  return {
37943
- 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
+ ],
37944
38238
  isError: true
37945
38239
  };
37946
38240
  }
@@ -37958,8 +38252,11 @@ function registerTools(server, scout) {
37958
38252
  };
37959
38253
  }
37960
38254
  scout.updatePreferences({ [key]: parsed });
38255
+ await scout.checkpoint();
37961
38256
  const updated = scout.getPreferences();
37962
- return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
38257
+ return {
38258
+ content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
38259
+ };
37963
38260
  }
37964
38261
  );
37965
38262
  }
@@ -37971,11 +38268,13 @@ function registerResources(server, scout) {
37971
38268
  "scout://config",
37972
38269
  { description: "Current oss-scout preferences" },
37973
38270
  async () => ({
37974
- contents: [{
37975
- uri: "scout://config",
37976
- mimeType: "application/json",
37977
- text: JSON.stringify(scout.getPreferences(), null, 2)
37978
- }]
38271
+ contents: [
38272
+ {
38273
+ uri: "scout://config",
38274
+ mimeType: "application/json",
38275
+ text: JSON.stringify(scout.getPreferences(), null, 2)
38276
+ }
38277
+ ]
37979
38278
  })
37980
38279
  );
37981
38280
  server.resource(
@@ -37983,11 +38282,13 @@ function registerResources(server, scout) {
37983
38282
  "scout://results",
37984
38283
  { description: "Saved search results from previous runs" },
37985
38284
  async () => ({
37986
- contents: [{
37987
- uri: "scout://results",
37988
- mimeType: "application/json",
37989
- text: JSON.stringify(scout.getSavedResults(), null, 2)
37990
- }]
38285
+ contents: [
38286
+ {
38287
+ uri: "scout://results",
38288
+ mimeType: "application/json",
38289
+ text: JSON.stringify(scout.getSavedResults(), null, 2)
38290
+ }
38291
+ ]
37991
38292
  })
37992
38293
  );
37993
38294
  server.resource(
@@ -37997,11 +38298,13 @@ function registerResources(server, scout) {
37997
38298
  async () => {
37998
38299
  const state = scout.getState();
37999
38300
  return {
38000
- contents: [{
38001
- uri: "scout://scores",
38002
- mimeType: "application/json",
38003
- text: JSON.stringify(state.repoScores, null, 2)
38004
- }]
38301
+ contents: [
38302
+ {
38303
+ uri: "scout://scores",
38304
+ mimeType: "application/json",
38305
+ text: JSON.stringify(state.repoScores, null, 2)
38306
+ }
38307
+ ]
38005
38308
  };
38006
38309
  }
38007
38310
  );
@@ -38027,8 +38330,10 @@ async function main() {
38027
38330
  await server.connect(transport);
38028
38331
  }
38029
38332
  main().catch((err) => {
38030
- process.stderr.write(`oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
38031
- `);
38333
+ process.stderr.write(
38334
+ `oss-scout-mcp fatal: ${err instanceof Error ? err.message : String(err)}
38335
+ `
38336
+ );
38032
38337
  process.exit(1);
38033
38338
  });
38034
38339
  /*! Bundled license information: