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