@oss-scout/mcp 0.2.1 → 0.4.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 +1 -1
- package/dist/mcp-server.bundle.cjs +413 -154
- package/dist/tools.js +113 -5
- package/package.json +18 -13
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Costa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ async function main() {
|
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
13
|
const scout = await createScout({ githubToken: token });
|
|
14
|
-
const server = new McpServer({ name: "
|
|
14
|
+
const server = new McpServer({ name: "oss-scout-mcp", version: "0.2.1" }, { capabilities: { resources: {}, tools: {} } });
|
|
15
15
|
registerTools(server, scout);
|
|
16
16
|
registerResources(server, scout);
|
|
17
17
|
const transport = new StdioServerTransport();
|
|
@@ -35704,7 +35704,6 @@ var IssueScopeSchema = external_exports3.enum([
|
|
|
35704
35704
|
]);
|
|
35705
35705
|
var SearchStrategySchema = external_exports3.enum([
|
|
35706
35706
|
"merged",
|
|
35707
|
-
"orgs",
|
|
35708
35707
|
"starred",
|
|
35709
35708
|
"broad",
|
|
35710
35709
|
"maintained",
|
|
@@ -35712,7 +35711,6 @@ var SearchStrategySchema = external_exports3.enum([
|
|
|
35712
35711
|
]);
|
|
35713
35712
|
var CONCRETE_STRATEGIES = [
|
|
35714
35713
|
"merged",
|
|
35715
|
-
"orgs",
|
|
35716
35714
|
"starred",
|
|
35717
35715
|
"broad",
|
|
35718
35716
|
"maintained"
|
|
@@ -35785,6 +35783,13 @@ var TrackedIssueSchema = external_exports3.object({
|
|
|
35785
35783
|
vetted: external_exports3.boolean(),
|
|
35786
35784
|
vettingResult: IssueVettingResultSchema.optional()
|
|
35787
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
|
+
});
|
|
35788
35793
|
var SavedCandidateSchema = external_exports3.object({
|
|
35789
35794
|
issueUrl: external_exports3.string(),
|
|
35790
35795
|
repo: external_exports3.string(),
|
|
@@ -35801,20 +35806,22 @@ var SavedCandidateSchema = external_exports3.object({
|
|
|
35801
35806
|
var PersistenceModeSchema = external_exports3.enum(["local", "gist"]);
|
|
35802
35807
|
var ScoutPreferencesSchema = external_exports3.object({
|
|
35803
35808
|
githubUsername: external_exports3.string().default(""),
|
|
35804
|
-
languages: external_exports3.array(external_exports3.string()).default(["
|
|
35809
|
+
languages: external_exports3.array(external_exports3.string()).default(["any"]),
|
|
35805
35810
|
labels: external_exports3.array(external_exports3.string()).default(["good first issue", "help wanted"]),
|
|
35806
35811
|
scope: external_exports3.array(IssueScopeSchema).optional(),
|
|
35807
35812
|
excludeRepos: external_exports3.array(external_exports3.string()).default([]),
|
|
35808
35813
|
excludeOrgs: external_exports3.array(external_exports3.string()).default([]),
|
|
35809
35814
|
aiPolicyBlocklist: external_exports3.array(external_exports3.string()).default(["matplotlib/matplotlib"]),
|
|
35810
|
-
preferredOrgs: external_exports3.array(external_exports3.string()).default([]),
|
|
35811
35815
|
projectCategories: external_exports3.array(ProjectCategorySchema).default([]),
|
|
35812
35816
|
minStars: external_exports3.number().default(50),
|
|
35813
35817
|
maxIssueAgeDays: external_exports3.number().default(90),
|
|
35814
35818
|
includeDocIssues: external_exports3.boolean().default(true),
|
|
35815
35819
|
minRepoScoreThreshold: external_exports3.number().default(4),
|
|
35820
|
+
interPhaseDelayMs: external_exports3.number().min(0).max(12e4).default(3e4),
|
|
35816
35821
|
persistence: PersistenceModeSchema.default("local"),
|
|
35817
|
-
defaultStrategy: external_exports3.array(SearchStrategySchema).optional()
|
|
35822
|
+
defaultStrategy: external_exports3.array(SearchStrategySchema).optional(),
|
|
35823
|
+
broadPhaseDelayMs: external_exports3.number().min(0).max(3e5).default(9e4),
|
|
35824
|
+
skipBroadWhenSufficientResults: external_exports3.number().int().min(0).max(100).default(15)
|
|
35818
35825
|
});
|
|
35819
35826
|
var ScoutStateSchema = external_exports3.object({
|
|
35820
35827
|
version: external_exports3.literal(1),
|
|
@@ -35825,6 +35832,7 @@ var ScoutStateSchema = external_exports3.object({
|
|
|
35825
35832
|
mergedPRs: external_exports3.array(StoredMergedPRSchema).default([]),
|
|
35826
35833
|
closedPRs: external_exports3.array(StoredClosedPRSchema).default([]),
|
|
35827
35834
|
savedResults: external_exports3.array(SavedCandidateSchema).default([]),
|
|
35835
|
+
skippedIssues: external_exports3.array(SkippedIssueSchema).default([]),
|
|
35828
35836
|
lastSearchAt: external_exports3.string().optional(),
|
|
35829
35837
|
lastRunAt: external_exports3.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
35830
35838
|
gistId: external_exports3.string().optional()
|
|
@@ -36760,12 +36768,9 @@ var IssueVetter = class {
|
|
|
36760
36768
|
matchesPreferredCategory: matchesCategory
|
|
36761
36769
|
});
|
|
36762
36770
|
const starredRepos = this.stateReader.getStarredRepos();
|
|
36763
|
-
const preferredOrgs = this.stateReader.getPreferredOrgs();
|
|
36764
36771
|
let searchPriority = "normal";
|
|
36765
36772
|
if (effectiveMergedCount > 0) {
|
|
36766
36773
|
searchPriority = "merged_pr";
|
|
36767
|
-
} else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
|
|
36768
|
-
searchPriority = "preferred_org";
|
|
36769
36774
|
} else if (starredRepos.includes(repoFullName)) {
|
|
36770
36775
|
searchPriority = "starred";
|
|
36771
36776
|
}
|
|
@@ -36831,7 +36836,6 @@ var IssueVetter = class {
|
|
|
36831
36836
|
var MODULE8 = "search-phases";
|
|
36832
36837
|
var GITHUB_MAX_BOOLEAN_OPS = 5;
|
|
36833
36838
|
var INTER_QUERY_DELAY_MS = 2e3;
|
|
36834
|
-
var BATCH_SIZE = 3;
|
|
36835
36839
|
function chunkLabels(labels, reservedOps = 0) {
|
|
36836
36840
|
const maxPerChunk = GITHUB_MAX_BOOLEAN_OPS - reservedOps + 1;
|
|
36837
36841
|
if (maxPerChunk < 1) {
|
|
@@ -36877,26 +36881,138 @@ function interleaveArrays(arrays) {
|
|
|
36877
36881
|
}
|
|
36878
36882
|
return result;
|
|
36879
36883
|
}
|
|
36880
|
-
function batchRepos(repos, batchSize) {
|
|
36881
|
-
const batches = [];
|
|
36882
|
-
for (let i = 0; i < repos.length; i += batchSize) {
|
|
36883
|
-
batches.push(repos.slice(i, i + batchSize));
|
|
36884
|
-
}
|
|
36885
|
-
return batches;
|
|
36886
|
-
}
|
|
36887
36884
|
var SEARCH_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
36888
36885
|
async function cachedSearchIssues(octokit, params) {
|
|
36889
36886
|
const cacheKey = `search:${params.q}:${params.sort}:${params.order}:${params.per_page}`;
|
|
36890
|
-
|
|
36891
|
-
|
|
36892
|
-
|
|
36887
|
+
const cache = getHttpCache();
|
|
36888
|
+
const cached2 = cache.getIfFresh(cacheKey, SEARCH_CACHE_TTL_MS);
|
|
36889
|
+
if (cached2) {
|
|
36890
|
+
debug(MODULE8, `Search cache hit for query`);
|
|
36891
|
+
return cached2;
|
|
36892
|
+
}
|
|
36893
|
+
const tracker = getSearchBudgetTracker();
|
|
36894
|
+
await tracker.waitForBudget();
|
|
36895
|
+
let data;
|
|
36896
|
+
try {
|
|
36897
|
+
const response = await octokit.search.issuesAndPullRequests(params);
|
|
36898
|
+
data = response.data;
|
|
36899
|
+
} finally {
|
|
36900
|
+
tracker.recordCall();
|
|
36901
|
+
}
|
|
36902
|
+
if (data.items.length > 0) {
|
|
36903
|
+
cache.set(cacheKey, "", data);
|
|
36904
|
+
} else {
|
|
36905
|
+
debug(MODULE8, `Skipping cache for empty search result (possible rate limit artifact)`);
|
|
36906
|
+
}
|
|
36907
|
+
return data;
|
|
36908
|
+
}
|
|
36909
|
+
async function fetchIssuesFromMaintainedRepos(octokit, repos, minStars, maxResults) {
|
|
36910
|
+
const items = [];
|
|
36911
|
+
for (const repoFullName of repos) {
|
|
36912
|
+
if (items.length >= maxResults * 3)
|
|
36913
|
+
break;
|
|
36914
|
+
const [owner, repo] = repoFullName.split("/");
|
|
36915
|
+
if (!owner || !repo)
|
|
36916
|
+
continue;
|
|
36893
36917
|
try {
|
|
36894
|
-
const { data } = await octokit.
|
|
36895
|
-
|
|
36896
|
-
|
|
36897
|
-
|
|
36918
|
+
const { data: repoData } = await octokit.repos.get({ owner, repo });
|
|
36919
|
+
if (!repoData.pushed_at)
|
|
36920
|
+
continue;
|
|
36921
|
+
const pushedAt = new Date(repoData.pushed_at);
|
|
36922
|
+
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
36923
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
36924
|
+
if (pushedAt < thirtyDaysAgo)
|
|
36925
|
+
continue;
|
|
36926
|
+
if ((repoData.stargazers_count ?? 0) < minStars)
|
|
36927
|
+
continue;
|
|
36928
|
+
if (repoData.archived)
|
|
36929
|
+
continue;
|
|
36930
|
+
const { data: issues } = await octokit.issues.listForRepo({
|
|
36931
|
+
owner,
|
|
36932
|
+
repo,
|
|
36933
|
+
state: "open",
|
|
36934
|
+
sort: "created",
|
|
36935
|
+
direction: "desc",
|
|
36936
|
+
per_page: 5
|
|
36937
|
+
});
|
|
36938
|
+
const realIssues = issues.filter((i) => !i.pull_request && !i.assignee);
|
|
36939
|
+
for (const issue2 of realIssues) {
|
|
36940
|
+
items.push({
|
|
36941
|
+
html_url: issue2.html_url,
|
|
36942
|
+
repository_url: `https://api.github.com/repos/${repoFullName}`,
|
|
36943
|
+
updated_at: issue2.updated_at ?? "",
|
|
36944
|
+
title: issue2.title,
|
|
36945
|
+
labels: issue2.labels
|
|
36946
|
+
});
|
|
36947
|
+
}
|
|
36948
|
+
await sleep(INTER_QUERY_DELAY_MS);
|
|
36949
|
+
} catch (error48) {
|
|
36950
|
+
if (getHttpStatusCode(error48) === 401)
|
|
36951
|
+
throw error48;
|
|
36952
|
+
if (isRateLimitError(error48)) {
|
|
36953
|
+
warn(MODULE8, `Rate limit hit fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
36954
|
+
break;
|
|
36955
|
+
}
|
|
36956
|
+
warn(MODULE8, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
36898
36957
|
}
|
|
36899
|
-
}
|
|
36958
|
+
}
|
|
36959
|
+
return items;
|
|
36960
|
+
}
|
|
36961
|
+
async function fetchIssuesFromKnownRepos(octokit, vetter, repos, labels, maxResults, priority, filterFn) {
|
|
36962
|
+
const candidates = [];
|
|
36963
|
+
let failedRepos = 0;
|
|
36964
|
+
let rateLimitFailures = 0;
|
|
36965
|
+
for (let i = 0; i < repos.length; i++) {
|
|
36966
|
+
if (candidates.length >= maxResults)
|
|
36967
|
+
break;
|
|
36968
|
+
if (i > 0)
|
|
36969
|
+
await sleep(INTER_QUERY_DELAY_MS);
|
|
36970
|
+
const repoFullName = repos[i];
|
|
36971
|
+
const [owner, repo] = repoFullName.split("/");
|
|
36972
|
+
try {
|
|
36973
|
+
const response = await octokit.issues.listForRepo({
|
|
36974
|
+
owner,
|
|
36975
|
+
repo,
|
|
36976
|
+
state: "open",
|
|
36977
|
+
sort: "created",
|
|
36978
|
+
direction: "desc",
|
|
36979
|
+
per_page: 5,
|
|
36980
|
+
...labels.length > 0 ? { labels: labels.join(",") } : {}
|
|
36981
|
+
});
|
|
36982
|
+
const issuesOnly = response.data.filter((item) => !("pull_request" in item) && !item.assignee);
|
|
36983
|
+
const mapped = issuesOnly.map((issue2) => ({
|
|
36984
|
+
html_url: issue2.html_url,
|
|
36985
|
+
repository_url: `https://api.github.com/repos/${repoFullName}`,
|
|
36986
|
+
updated_at: issue2.updated_at ?? "",
|
|
36987
|
+
title: issue2.title,
|
|
36988
|
+
labels: issue2.labels
|
|
36989
|
+
}));
|
|
36990
|
+
if (mapped.length > 0) {
|
|
36991
|
+
const filtered = filterFn(mapped);
|
|
36992
|
+
if (filtered.length > 0) {
|
|
36993
|
+
const remainingNeeded = maxResults - candidates.length;
|
|
36994
|
+
const { candidates: vetted, rateLimitHit: vetRateLimitHit } = await vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((item) => item.html_url), remainingNeeded, priority);
|
|
36995
|
+
candidates.push(...vetted);
|
|
36996
|
+
if (vetRateLimitHit)
|
|
36997
|
+
rateLimitFailures++;
|
|
36998
|
+
}
|
|
36999
|
+
}
|
|
37000
|
+
} catch (error48) {
|
|
37001
|
+
if (getHttpStatusCode(error48) === 401)
|
|
37002
|
+
throw error48;
|
|
37003
|
+
failedRepos++;
|
|
37004
|
+
if (isRateLimitError(error48)) {
|
|
37005
|
+
rateLimitFailures++;
|
|
37006
|
+
}
|
|
37007
|
+
warn(MODULE8, `Error fetching issues from ${repoFullName}:`, errorMessage(error48));
|
|
37008
|
+
}
|
|
37009
|
+
}
|
|
37010
|
+
const allReposFailed = failedRepos === repos.length && repos.length > 0;
|
|
37011
|
+
const rateLimitHit = rateLimitFailures > 0;
|
|
37012
|
+
if (allReposFailed) {
|
|
37013
|
+
warn(MODULE8, `All ${repos.length} repo(s) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
37014
|
+
}
|
|
37015
|
+
return { candidates, allReposFailed, rateLimitHit };
|
|
36900
37016
|
}
|
|
36901
37017
|
async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery, perPage) {
|
|
36902
37018
|
const labelChunks = chunkLabels(labels, reservedOps);
|
|
@@ -36952,50 +37068,9 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
|
|
|
36952
37068
|
}
|
|
36953
37069
|
return { candidates: starFiltered, allVetFailed, rateLimitHit };
|
|
36954
37070
|
}
|
|
36955
|
-
async function searchInRepos(octokit, vetter, repos, baseQualifiers, labels, maxResults, priority, filterFn) {
|
|
36956
|
-
const candidates = [];
|
|
36957
|
-
const batches = batchRepos(repos, BATCH_SIZE);
|
|
36958
|
-
let failedBatches = 0;
|
|
36959
|
-
let rateLimitFailures = 0;
|
|
36960
|
-
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
36961
|
-
const batch = batches[batchIdx];
|
|
36962
|
-
if (candidates.length >= maxResults)
|
|
36963
|
-
break;
|
|
36964
|
-
if (batchIdx > 0)
|
|
36965
|
-
await sleep(INTER_QUERY_DELAY_MS);
|
|
36966
|
-
try {
|
|
36967
|
-
const repoFilter = batch.map((r) => `repo:${r}`).join(" OR ");
|
|
36968
|
-
const repoOps = batch.length - 1;
|
|
36969
|
-
const perPage = Math.min(30, (maxResults - candidates.length) * 3);
|
|
36970
|
-
const allItems = await searchWithChunkedLabels(octokit, labels, repoOps, (labelQ) => `${baseQualifiers} ${labelQ} (${repoFilter})`.replace(/ +/g, " ").trim(), perPage);
|
|
36971
|
-
if (allItems.length > 0) {
|
|
36972
|
-
const filtered = filterFn(allItems);
|
|
36973
|
-
const remainingNeeded = maxResults - candidates.length;
|
|
36974
|
-
const { candidates: vetted, rateLimitHit: vetRateLimitHit } = await vetter.vetIssuesParallel(filtered.slice(0, remainingNeeded * 2).map((i) => i.html_url), remainingNeeded, priority);
|
|
36975
|
-
candidates.push(...vetted);
|
|
36976
|
-
if (vetRateLimitHit)
|
|
36977
|
-
rateLimitFailures++;
|
|
36978
|
-
}
|
|
36979
|
-
} catch (error48) {
|
|
36980
|
-
failedBatches++;
|
|
36981
|
-
if (isRateLimitError(error48)) {
|
|
36982
|
-
rateLimitFailures++;
|
|
36983
|
-
}
|
|
36984
|
-
const batchReposStr = batch.join(", ");
|
|
36985
|
-
warn(MODULE8, `Error searching issues in batch [${batchReposStr}]:`, errorMessage(error48));
|
|
36986
|
-
}
|
|
36987
|
-
}
|
|
36988
|
-
const allBatchesFailed = failedBatches === batches.length && batches.length > 0;
|
|
36989
|
-
const rateLimitHit = rateLimitFailures > 0;
|
|
36990
|
-
if (allBatchesFailed) {
|
|
36991
|
-
warn(MODULE8, `All ${batches.length} batch(es) failed for ${priority} phase. This may indicate a systemic issue (rate limit, auth, network).`);
|
|
36992
|
-
}
|
|
36993
|
-
return { candidates, allBatchesFailed, rateLimitHit };
|
|
36994
|
-
}
|
|
36995
37071
|
|
|
36996
37072
|
// ../core/dist/core/issue-discovery.js
|
|
36997
37073
|
var MODULE9 = "issue-discovery";
|
|
36998
|
-
var INTER_PHASE_DELAY_MS = 2e3;
|
|
36999
37074
|
var LOW_BUDGET_THRESHOLD = 20;
|
|
37000
37075
|
var CRITICAL_BUDGET_THRESHOLD = 10;
|
|
37001
37076
|
function buildIssueFilter(config2) {
|
|
@@ -37015,6 +37090,8 @@ function buildIssueFilter(config2) {
|
|
|
37015
37090
|
return false;
|
|
37016
37091
|
if (config2.lowScoringRepos.has(repoFullName))
|
|
37017
37092
|
return false;
|
|
37093
|
+
if (config2.skippedUrls.has(item.html_url))
|
|
37094
|
+
return false;
|
|
37018
37095
|
const updatedAt = new Date(item.updated_at);
|
|
37019
37096
|
const ageDays = daysBetween(updatedAt, config2.now);
|
|
37020
37097
|
if (ageDays > config2.maxAgeDays)
|
|
@@ -37025,56 +37102,25 @@ function buildIssueFilter(config2) {
|
|
|
37025
37102
|
});
|
|
37026
37103
|
};
|
|
37027
37104
|
}
|
|
37028
|
-
async function runPhase0(octokit, vetter, repos,
|
|
37105
|
+
async function runPhase0(octokit, vetter, repos, maxResults, filterIssues) {
|
|
37029
37106
|
info(MODULE9, `Phase 0: Searching issues in ${repos.length} merged-PR repos (no label filter)...`);
|
|
37030
|
-
const { candidates,
|
|
37107
|
+
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, repos, [], maxResults, "merged_pr", filterIssues);
|
|
37031
37108
|
info(MODULE9, `Found ${candidates.length} candidates from merged-PR repos`);
|
|
37032
37109
|
return {
|
|
37033
37110
|
candidates,
|
|
37034
|
-
error:
|
|
37111
|
+
error: allReposFailed ? "All merged-PR repo fetches failed" : null,
|
|
37035
37112
|
rateLimitHit
|
|
37036
37113
|
};
|
|
37037
37114
|
}
|
|
37038
|
-
async function
|
|
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) {
|
|
37115
|
+
async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssues) {
|
|
37071
37116
|
info(MODULE9, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
37072
37117
|
const phase1Labels = labels.slice(0, 3);
|
|
37073
|
-
const
|
|
37118
|
+
const reposToSearch = repos.slice(0, 10);
|
|
37119
|
+
const { candidates, allReposFailed, rateLimitHit } = await fetchIssuesFromKnownRepos(octokit, vetter, reposToSearch, phase1Labels, maxResults, "starred", filterIssues);
|
|
37074
37120
|
info(MODULE9, `Found ${candidates.length} candidates from starred repos`);
|
|
37075
37121
|
return {
|
|
37076
37122
|
candidates,
|
|
37077
|
-
error:
|
|
37123
|
+
error: allReposFailed ? "All starred repo fetches failed" : null,
|
|
37078
37124
|
rateLimitHit
|
|
37079
37125
|
};
|
|
37080
37126
|
}
|
|
@@ -37140,8 +37186,32 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
|
|
|
37140
37186
|
rateLimitHit
|
|
37141
37187
|
};
|
|
37142
37188
|
}
|
|
37143
|
-
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
|
|
37189
|
+
async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories, maxResults, phase0RepoSet, starredRepoSet, starredRepos, existingCandidates, filterIssues) {
|
|
37144
37190
|
info(MODULE9, "Phase 3: Searching actively maintained repos...");
|
|
37191
|
+
const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
|
|
37192
|
+
const eligibleStarred = starredRepos.filter((r) => !phase0RepoSet.has(r) && !seenRepos.has(r));
|
|
37193
|
+
if (eligibleStarred.length > 0) {
|
|
37194
|
+
info(MODULE9, `Phase 3: Checking ${eligibleStarred.length} starred repos via REST API...`);
|
|
37195
|
+
const restItems = await fetchIssuesFromMaintainedRepos(octokit, eligibleStarred.slice(0, 15), minStars, maxResults);
|
|
37196
|
+
if (restItems.length > 0) {
|
|
37197
|
+
try {
|
|
37198
|
+
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, restItems, filterIssues, [phase0RepoSet, seenRepos], maxResults, minStars, "Phase 3 (REST)");
|
|
37199
|
+
if (candidates.length > 0) {
|
|
37200
|
+
info(MODULE9, `Found ${candidates.length} candidates from maintained-repo REST search`);
|
|
37201
|
+
return {
|
|
37202
|
+
candidates,
|
|
37203
|
+
error: allVetFailed ? "all vetting failed" : null,
|
|
37204
|
+
rateLimitHit: vetRateLimitHit
|
|
37205
|
+
};
|
|
37206
|
+
}
|
|
37207
|
+
} catch (error48) {
|
|
37208
|
+
if (getHttpStatusCode(error48) === 401)
|
|
37209
|
+
throw error48;
|
|
37210
|
+
warn(MODULE9, `Phase 3 REST vetting failed, falling back to Search API:`, errorMessage(error48));
|
|
37211
|
+
}
|
|
37212
|
+
}
|
|
37213
|
+
}
|
|
37214
|
+
info(MODULE9, "Phase 3: Falling back to Search API...");
|
|
37145
37215
|
const thirtyDaysAgo = /* @__PURE__ */ new Date();
|
|
37146
37216
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
37147
37217
|
const pushedSince = thirtyDaysAgo.toISOString().split("T")[0];
|
|
@@ -37156,7 +37226,6 @@ async function runPhase3(octokit, vetter, langQuery, minStars, projectCategories
|
|
|
37156
37226
|
per_page: maxResults * 3
|
|
37157
37227
|
});
|
|
37158
37228
|
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
37229
|
const { candidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, data.items, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], maxResults, minStars, "Phase 3");
|
|
37161
37230
|
info(MODULE9, `Found ${candidates.length} candidates from maintained-repo search`);
|
|
37162
37231
|
return {
|
|
@@ -37203,8 +37272,8 @@ var IssueDiscovery = class {
|
|
|
37203
37272
|
}
|
|
37204
37273
|
/**
|
|
37205
37274
|
* Search for issues matching our criteria.
|
|
37206
|
-
* Searches in priority order: merged-PR repos first (no label filter), then
|
|
37207
|
-
*
|
|
37275
|
+
* Searches in priority order: merged-PR repos first (no label filter), then starred
|
|
37276
|
+
* repos, then general search, then actively maintained repos.
|
|
37208
37277
|
* Filters out issues from low-scoring and excluded repos.
|
|
37209
37278
|
*
|
|
37210
37279
|
* @param options - Search configuration
|
|
@@ -37232,6 +37301,7 @@ var IssueDiscovery = class {
|
|
|
37232
37301
|
const labels = options.labels || (scopes ? buildEffectiveLabels(scopes, config2.labels) : config2.labels);
|
|
37233
37302
|
const maxResults = options.maxResults || 10;
|
|
37234
37303
|
const minStars = config2.minStars ?? 50;
|
|
37304
|
+
const interPhaseDelay = config2.interPhaseDelayMs ?? 3e4;
|
|
37235
37305
|
const ALL_STRATEGIES = CONCRETE_STRATEGIES;
|
|
37236
37306
|
const rawStrategies = options.strategies ?? config2.defaultStrategy ?? ["all"];
|
|
37237
37307
|
const enabledStrategies = new Set(rawStrategies.includes("all") ? ALL_STRATEGIES : rawStrategies);
|
|
@@ -37262,6 +37332,10 @@ var IssueDiscovery = class {
|
|
|
37262
37332
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
|
|
37263
37333
|
warn(MODULE9, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
|
|
37264
37334
|
}
|
|
37335
|
+
if (searchBudget <= 0) {
|
|
37336
|
+
this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
37337
|
+
return { candidates: [], strategiesUsed: [] };
|
|
37338
|
+
}
|
|
37265
37339
|
const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
|
|
37266
37340
|
const starredRepos = this.getStarredRepos();
|
|
37267
37341
|
const starredRepoSet = new Set(starredRepos);
|
|
@@ -37278,6 +37352,7 @@ var IssueDiscovery = class {
|
|
|
37278
37352
|
excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
37279
37353
|
aiBlocklisted,
|
|
37280
37354
|
lowScoringRepos,
|
|
37355
|
+
skippedUrls: options.skippedUrls ?? /* @__PURE__ */ new Set(),
|
|
37281
37356
|
maxAgeDays: config2.maxIssueAgeDays || 90,
|
|
37282
37357
|
now: /* @__PURE__ */ new Date(),
|
|
37283
37358
|
includeDocIssues: config2.includeDocIssues ?? true
|
|
@@ -37287,7 +37362,7 @@ var IssueDiscovery = class {
|
|
|
37287
37362
|
if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
|
|
37288
37363
|
const remaining = maxResults - allCandidates.length;
|
|
37289
37364
|
if (remaining > 0) {
|
|
37290
|
-
const result = await runPhase0(this.octokit, this.vetter, phase0Repos,
|
|
37365
|
+
const result = await runPhase0(this.octokit, this.vetter, phase0Repos, remaining, filterIssues);
|
|
37291
37366
|
allCandidates.push(...result.candidates);
|
|
37292
37367
|
phaseErrors["0"] = result.error;
|
|
37293
37368
|
if (result.rateLimitHit)
|
|
@@ -37295,29 +37370,16 @@ var IssueDiscovery = class {
|
|
|
37295
37370
|
}
|
|
37296
37371
|
strategiesUsed.push("merged");
|
|
37297
37372
|
}
|
|
37298
|
-
const preferredOrgs = config2.preferredOrgs ?? [];
|
|
37299
|
-
if (allCandidates.length < maxResults && preferredOrgs.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("orgs")) {
|
|
37300
|
-
if (phase0Repos.length > 0)
|
|
37301
|
-
await sleep(INTER_PHASE_DELAY_MS);
|
|
37302
|
-
const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
|
|
37303
|
-
const orgsToSearch = preferredOrgs.filter((org) => !phase0Orgs.has(org.toLowerCase())).slice(0, 5);
|
|
37304
|
-
if (orgsToSearch.length > 0) {
|
|
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;
|
|
37311
|
-
}
|
|
37312
|
-
strategiesUsed.push("orgs");
|
|
37313
|
-
}
|
|
37314
37373
|
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
|
|
37315
|
-
|
|
37374
|
+
if (interPhaseDelay > 0) {
|
|
37375
|
+
info(MODULE9, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37376
|
+
await sleep(interPhaseDelay);
|
|
37377
|
+
}
|
|
37316
37378
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
37317
37379
|
if (reposToSearch.length > 0) {
|
|
37318
37380
|
const remaining = maxResults - allCandidates.length;
|
|
37319
37381
|
if (remaining > 0) {
|
|
37320
|
-
const result = await runPhase1(this.octokit, this.vetter, reposToSearch,
|
|
37382
|
+
const result = await runPhase1(this.octokit, this.vetter, reposToSearch, labels, remaining, filterIssues);
|
|
37321
37383
|
allCandidates.push(...result.candidates);
|
|
37322
37384
|
phaseErrors["1"] = result.error;
|
|
37323
37385
|
if (result.rateLimitHit)
|
|
@@ -37326,20 +37388,38 @@ var IssueDiscovery = class {
|
|
|
37326
37388
|
}
|
|
37327
37389
|
strategiesUsed.push("starred");
|
|
37328
37390
|
}
|
|
37391
|
+
const broadDelay = config2.broadPhaseDelayMs ?? 9e4;
|
|
37392
|
+
const skipThreshold = config2.skipBroadWhenSufficientResults ?? 15;
|
|
37329
37393
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("broad")) {
|
|
37330
|
-
|
|
37331
|
-
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
37335
|
-
|
|
37336
|
-
|
|
37394
|
+
if (skipThreshold > 0 && allCandidates.length >= skipThreshold) {
|
|
37395
|
+
info(MODULE9, `Skipping broad search: already found ${allCandidates.length} candidates (threshold: ${skipThreshold})`);
|
|
37396
|
+
} else {
|
|
37397
|
+
if (interPhaseDelay > 0) {
|
|
37398
|
+
info(MODULE9, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37399
|
+
await sleep(interPhaseDelay);
|
|
37400
|
+
}
|
|
37401
|
+
if (allCandidates.length > 0 && broadDelay > 0) {
|
|
37402
|
+
info(MODULE9, `Waiting ${(broadDelay / 1e3).toFixed(0)}s for rate limit cooldown before broad search...`);
|
|
37403
|
+
await sleep(broadDelay);
|
|
37404
|
+
} else if (allCandidates.length === 0) {
|
|
37405
|
+
info(MODULE9, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
|
|
37406
|
+
}
|
|
37407
|
+
const remaining = maxResults - allCandidates.length;
|
|
37408
|
+
const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
37409
|
+
allCandidates.push(...result.candidates);
|
|
37410
|
+
phaseErrors["2"] = result.error;
|
|
37411
|
+
if (result.rateLimitHit)
|
|
37412
|
+
rateLimitHitDuringSearch = true;
|
|
37413
|
+
}
|
|
37337
37414
|
strategiesUsed.push("broad");
|
|
37338
37415
|
}
|
|
37339
37416
|
if (allCandidates.length < maxResults && searchBudget >= LOW_BUDGET_THRESHOLD && enabledStrategies.has("maintained")) {
|
|
37340
|
-
|
|
37417
|
+
if (interPhaseDelay > 0) {
|
|
37418
|
+
info(MODULE9, `Waiting ${(interPhaseDelay / 1e3).toFixed(0)}s between phases for rate limit management...`);
|
|
37419
|
+
await sleep(interPhaseDelay);
|
|
37420
|
+
}
|
|
37341
37421
|
const remaining = maxResults - allCandidates.length;
|
|
37342
|
-
const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
|
|
37422
|
+
const result = await runPhase3(this.octokit, this.vetter, langQuery, minStars, config2.projectCategories ?? [], remaining, phase0RepoSet, starredRepoSet, starredRepos, allCandidates, filterIssues);
|
|
37343
37423
|
allCandidates.push(...result.candidates);
|
|
37344
37424
|
phaseErrors["3"] = result.error;
|
|
37345
37425
|
if (result.rateLimitHit)
|
|
@@ -37356,7 +37436,6 @@ var IssueDiscovery = class {
|
|
|
37356
37436
|
if (allCandidates.length === 0) {
|
|
37357
37437
|
const errorDetails = [
|
|
37358
37438
|
phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
|
|
37359
|
-
phaseErrors["0.5"] ? `Phase 0.5 (preferred orgs): ${phaseErrors["0.5"]}` : null,
|
|
37360
37439
|
phaseErrors["1"] ? `Phase 1 (starred repos): ${phaseErrors["1"]}` : null,
|
|
37361
37440
|
phaseErrors["2"] ? `Phase 2 (general): ${phaseErrors["2"]}` : null,
|
|
37362
37441
|
phaseErrors["3"] ? `Phase 3 (maintained repos): ${phaseErrors["3"]}` : null
|
|
@@ -37374,9 +37453,8 @@ var IssueDiscovery = class {
|
|
|
37374
37453
|
allCandidates.sort((a, b) => {
|
|
37375
37454
|
const priorityOrder = {
|
|
37376
37455
|
merged_pr: 0,
|
|
37377
|
-
|
|
37378
|
-
|
|
37379
|
-
normal: 3
|
|
37456
|
+
starred: 1,
|
|
37457
|
+
normal: 2
|
|
37380
37458
|
};
|
|
37381
37459
|
const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
|
|
37382
37460
|
if (priorityDiff !== 0)
|
|
@@ -37462,11 +37540,16 @@ var GistStateStore = class {
|
|
|
37462
37540
|
warn(MODULE10, "No gist ID \u2014 cannot push");
|
|
37463
37541
|
return false;
|
|
37464
37542
|
}
|
|
37543
|
+
const json2 = JSON.stringify(state, null, 2);
|
|
37544
|
+
if (json2.length > 9e5) {
|
|
37545
|
+
warn(MODULE10, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
37546
|
+
return false;
|
|
37547
|
+
}
|
|
37465
37548
|
try {
|
|
37466
37549
|
await this.octokit.gists.update({
|
|
37467
37550
|
gist_id: this.gistId,
|
|
37468
37551
|
files: {
|
|
37469
|
-
[GIST_FILENAME]: { content:
|
|
37552
|
+
[GIST_FILENAME]: { content: json2 }
|
|
37470
37553
|
}
|
|
37471
37554
|
});
|
|
37472
37555
|
debug(MODULE10, "State pushed to gist");
|
|
@@ -37639,6 +37722,7 @@ function mergeStates(local, remote) {
|
|
|
37639
37722
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
37640
37723
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
37641
37724
|
savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
|
|
37725
|
+
skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
|
|
37642
37726
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
37643
37727
|
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
37644
37728
|
gistId: remote.gistId ?? local.gistId
|
|
@@ -37689,6 +37773,18 @@ function mergeSavedResults(local, remote) {
|
|
|
37689
37773
|
}
|
|
37690
37774
|
return [...merged.values()];
|
|
37691
37775
|
}
|
|
37776
|
+
function mergeSkippedIssues(local, remote) {
|
|
37777
|
+
const merged = /* @__PURE__ */ new Map();
|
|
37778
|
+
for (const item of local)
|
|
37779
|
+
merged.set(item.url, item);
|
|
37780
|
+
for (const item of remote) {
|
|
37781
|
+
const existing = merged.get(item.url);
|
|
37782
|
+
if (!existing || item.skippedAt > existing.skippedAt) {
|
|
37783
|
+
merged.set(item.url, item);
|
|
37784
|
+
}
|
|
37785
|
+
}
|
|
37786
|
+
return [...merged.values()];
|
|
37787
|
+
}
|
|
37692
37788
|
function pickFresherTimestamp(a, b) {
|
|
37693
37789
|
if (!a)
|
|
37694
37790
|
return b;
|
|
@@ -37719,7 +37815,8 @@ function loadLocalState() {
|
|
|
37719
37815
|
const backupPath = `${statePath}.corrupt.${Date.now()}`;
|
|
37720
37816
|
fs4.copyFileSync(statePath, backupPath);
|
|
37721
37817
|
warn(MODULE11, `Corrupt state backed up to ${backupPath}`);
|
|
37722
|
-
} catch {
|
|
37818
|
+
} catch (backupErr) {
|
|
37819
|
+
warn(MODULE11, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
37723
37820
|
}
|
|
37724
37821
|
return ScoutStateSchema.parse({ version: 1 });
|
|
37725
37822
|
}
|
|
@@ -37799,12 +37896,16 @@ var OssScout = class {
|
|
|
37799
37896
|
// ── Search ──────────────────────────────────────────────────────────
|
|
37800
37897
|
/**
|
|
37801
37898
|
* Multi-strategy issue search. Returns scored, sorted candidates.
|
|
37899
|
+
* Automatically culls expired skip entries and filters skipped issues.
|
|
37802
37900
|
*/
|
|
37803
37901
|
async search(options) {
|
|
37902
|
+
this.cullExpiredSkips();
|
|
37903
|
+
const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
37804
37904
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
37805
37905
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
37806
37906
|
maxResults: options?.maxResults,
|
|
37807
|
-
strategies: options?.strategies
|
|
37907
|
+
strategies: options?.strategies,
|
|
37908
|
+
skippedUrls
|
|
37808
37909
|
});
|
|
37809
37910
|
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37810
37911
|
this.dirty = true;
|
|
@@ -37905,9 +38006,6 @@ var OssScout = class {
|
|
|
37905
38006
|
getStarredRepos() {
|
|
37906
38007
|
return this.state.starredRepos;
|
|
37907
38008
|
}
|
|
37908
|
-
getPreferredOrgs() {
|
|
37909
|
-
return this.state.preferences.preferredOrgs;
|
|
37910
|
-
}
|
|
37911
38009
|
getProjectCategories() {
|
|
37912
38010
|
return this.state.preferences.projectCategories;
|
|
37913
38011
|
}
|
|
@@ -38038,6 +38136,69 @@ var OssScout = class {
|
|
|
38038
38136
|
this.state.savedResults = [];
|
|
38039
38137
|
this.dirty = true;
|
|
38040
38138
|
}
|
|
38139
|
+
// ── Skip List ───────────────────────────────────────────────────────
|
|
38140
|
+
/**
|
|
38141
|
+
* Skip an issue — excludes it from future searches. Auto-culled after 90 days.
|
|
38142
|
+
*/
|
|
38143
|
+
skipIssue(url2, metadata) {
|
|
38144
|
+
const existing = this.state.skippedIssues ?? [];
|
|
38145
|
+
if (existing.some((s) => s.url === url2))
|
|
38146
|
+
return;
|
|
38147
|
+
this.state.skippedIssues = [
|
|
38148
|
+
...existing,
|
|
38149
|
+
{
|
|
38150
|
+
url: url2,
|
|
38151
|
+
repo: metadata?.repo ?? "",
|
|
38152
|
+
number: metadata?.number ?? 0,
|
|
38153
|
+
title: metadata?.title ?? "",
|
|
38154
|
+
skippedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38155
|
+
}
|
|
38156
|
+
];
|
|
38157
|
+
if (this.state.savedResults) {
|
|
38158
|
+
this.state.savedResults = this.state.savedResults.filter((r) => r.issueUrl !== url2);
|
|
38159
|
+
}
|
|
38160
|
+
this.dirty = true;
|
|
38161
|
+
}
|
|
38162
|
+
/**
|
|
38163
|
+
* Get all skipped issues.
|
|
38164
|
+
*/
|
|
38165
|
+
getSkippedIssues() {
|
|
38166
|
+
return this.state.skippedIssues ?? [];
|
|
38167
|
+
}
|
|
38168
|
+
/**
|
|
38169
|
+
* Remove a specific issue from the skip list.
|
|
38170
|
+
*/
|
|
38171
|
+
unskipIssue(url2) {
|
|
38172
|
+
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url2);
|
|
38173
|
+
this.dirty = true;
|
|
38174
|
+
}
|
|
38175
|
+
/**
|
|
38176
|
+
* Clear all skipped issues.
|
|
38177
|
+
*/
|
|
38178
|
+
clearSkippedIssues() {
|
|
38179
|
+
this.state.skippedIssues = [];
|
|
38180
|
+
this.dirty = true;
|
|
38181
|
+
}
|
|
38182
|
+
/**
|
|
38183
|
+
* Remove skipped issues older than maxDays (default 90). Called automatically during search.
|
|
38184
|
+
* @returns The number of expired entries that were removed.
|
|
38185
|
+
*/
|
|
38186
|
+
cullExpiredSkips(maxDays = 90) {
|
|
38187
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
38188
|
+
cutoff.setDate(cutoff.getDate() - maxDays);
|
|
38189
|
+
const before = (this.state.skippedIssues ?? []).length;
|
|
38190
|
+
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => {
|
|
38191
|
+
const d = new Date(s.skippedAt);
|
|
38192
|
+
if (isNaN(d.getTime())) {
|
|
38193
|
+
return true;
|
|
38194
|
+
}
|
|
38195
|
+
return d >= cutoff;
|
|
38196
|
+
});
|
|
38197
|
+
const culled = before - this.state.skippedIssues.length;
|
|
38198
|
+
if (culled > 0)
|
|
38199
|
+
this.dirty = true;
|
|
38200
|
+
return culled;
|
|
38201
|
+
}
|
|
38041
38202
|
// ── Persistence ─────────────────────────────────────────────────────
|
|
38042
38203
|
/**
|
|
38043
38204
|
* Check if state has uncommitted changes.
|
|
@@ -38097,6 +38258,17 @@ var OssScout = class {
|
|
|
38097
38258
|
};
|
|
38098
38259
|
|
|
38099
38260
|
// src/tools.ts
|
|
38261
|
+
var TOOL_TIMEOUT_MS = 6e4;
|
|
38262
|
+
function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
|
|
38263
|
+
let timeoutId;
|
|
38264
|
+
const timer = new Promise((_, reject) => {
|
|
38265
|
+
timeoutId = setTimeout(
|
|
38266
|
+
() => reject(new Error(`Request timed out after ${ms / 1e3}s`)),
|
|
38267
|
+
ms
|
|
38268
|
+
);
|
|
38269
|
+
});
|
|
38270
|
+
return Promise.race([promise2, timer]).finally(() => clearTimeout(timeoutId));
|
|
38271
|
+
}
|
|
38100
38272
|
function registerTools(server, scout) {
|
|
38101
38273
|
server.tool(
|
|
38102
38274
|
"search",
|
|
@@ -38104,16 +38276,18 @@ function registerTools(server, scout) {
|
|
|
38104
38276
|
{
|
|
38105
38277
|
maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
|
|
38106
38278
|
strategies: external_exports3.string().optional().describe(
|
|
38107
|
-
"Comma-separated search strategies: merged,
|
|
38279
|
+
"Comma-separated search strategies: merged, starred, broad, maintained, all"
|
|
38108
38280
|
)
|
|
38109
38281
|
},
|
|
38110
38282
|
async ({ maxResults, strategies }) => {
|
|
38111
38283
|
try {
|
|
38112
38284
|
const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
|
|
38113
|
-
const result = await
|
|
38114
|
-
|
|
38115
|
-
|
|
38116
|
-
|
|
38285
|
+
const result = await withTimeout(
|
|
38286
|
+
scout.search({
|
|
38287
|
+
maxResults: maxResults ?? 10,
|
|
38288
|
+
strategies: parsedStrategies
|
|
38289
|
+
})
|
|
38290
|
+
);
|
|
38117
38291
|
scout.saveResults(result.candidates);
|
|
38118
38292
|
await scout.checkpoint();
|
|
38119
38293
|
return {
|
|
@@ -38138,7 +38312,7 @@ function registerTools(server, scout) {
|
|
|
38138
38312
|
},
|
|
38139
38313
|
async ({ issueUrl }) => {
|
|
38140
38314
|
try {
|
|
38141
|
-
const candidate = await scout.vetIssue(issueUrl);
|
|
38315
|
+
const candidate = await withTimeout(scout.vetIssue(issueUrl));
|
|
38142
38316
|
return {
|
|
38143
38317
|
content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }]
|
|
38144
38318
|
};
|
|
@@ -38161,11 +38335,93 @@ function registerTools(server, scout) {
|
|
|
38161
38335
|
};
|
|
38162
38336
|
}
|
|
38163
38337
|
);
|
|
38338
|
+
server.tool(
|
|
38339
|
+
"skip",
|
|
38340
|
+
"Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.",
|
|
38341
|
+
{
|
|
38342
|
+
action: external_exports3.enum(["add", "remove", "list", "clear"]).default("add").describe("Action to perform (default: add)"),
|
|
38343
|
+
issueUrl: external_exports3.string().optional().describe("GitHub issue URL (required for add/remove actions)")
|
|
38344
|
+
},
|
|
38345
|
+
async ({ action, issueUrl }) => {
|
|
38346
|
+
try {
|
|
38347
|
+
if ((action === "add" || action === "remove") && !issueUrl) {
|
|
38348
|
+
return {
|
|
38349
|
+
content: [
|
|
38350
|
+
{
|
|
38351
|
+
type: "text",
|
|
38352
|
+
text: `Error: issueUrl is required for the "${action}" action.`
|
|
38353
|
+
}
|
|
38354
|
+
],
|
|
38355
|
+
isError: true
|
|
38356
|
+
};
|
|
38357
|
+
}
|
|
38358
|
+
if (action === "list") {
|
|
38359
|
+
const skipped = scout.getSkippedIssues();
|
|
38360
|
+
return {
|
|
38361
|
+
content: [{ type: "text", text: JSON.stringify(skipped, null, 2) }]
|
|
38362
|
+
};
|
|
38363
|
+
}
|
|
38364
|
+
if (action === "clear") {
|
|
38365
|
+
const count = scout.getSkippedIssues().length;
|
|
38366
|
+
scout.clearSkippedIssues();
|
|
38367
|
+
const synced2 = await scout.checkpoint();
|
|
38368
|
+
const syncNote2 = synced2 ? "" : " (saved locally, gist sync failed)";
|
|
38369
|
+
return {
|
|
38370
|
+
content: [
|
|
38371
|
+
{
|
|
38372
|
+
type: "text",
|
|
38373
|
+
text: count > 0 ? `Skip list cleared (${count} entries removed)${syncNote2}` : `Skip list already empty${syncNote2}`
|
|
38374
|
+
}
|
|
38375
|
+
]
|
|
38376
|
+
};
|
|
38377
|
+
}
|
|
38378
|
+
if (action === "remove") {
|
|
38379
|
+
const wasPresent = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
38380
|
+
scout.unskipIssue(issueUrl);
|
|
38381
|
+
const synced2 = await scout.checkpoint();
|
|
38382
|
+
const syncNote2 = synced2 ? "" : " (saved locally, gist sync failed)";
|
|
38383
|
+
return {
|
|
38384
|
+
content: [
|
|
38385
|
+
{
|
|
38386
|
+
type: "text",
|
|
38387
|
+
text: wasPresent ? `Removed from skip list: ${issueUrl}${syncNote2}` : `Not in skip list: ${issueUrl}`
|
|
38388
|
+
}
|
|
38389
|
+
]
|
|
38390
|
+
};
|
|
38391
|
+
}
|
|
38392
|
+
const alreadySkipped = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
38393
|
+
if (alreadySkipped) {
|
|
38394
|
+
return {
|
|
38395
|
+
content: [
|
|
38396
|
+
{ type: "text", text: `Already in skip list: ${issueUrl}` }
|
|
38397
|
+
]
|
|
38398
|
+
};
|
|
38399
|
+
}
|
|
38400
|
+
const saved = scout.getSavedResults().find((r) => r.issueUrl === issueUrl);
|
|
38401
|
+
const metadata = saved ? {
|
|
38402
|
+
repo: saved.repo,
|
|
38403
|
+
number: saved.number,
|
|
38404
|
+
title: saved.title
|
|
38405
|
+
} : void 0;
|
|
38406
|
+
scout.skipIssue(issueUrl, metadata);
|
|
38407
|
+
const synced = await scout.checkpoint();
|
|
38408
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
38409
|
+
return {
|
|
38410
|
+
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }]
|
|
38411
|
+
};
|
|
38412
|
+
} catch (err) {
|
|
38413
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38414
|
+
return {
|
|
38415
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
38416
|
+
isError: true
|
|
38417
|
+
};
|
|
38418
|
+
}
|
|
38419
|
+
}
|
|
38420
|
+
);
|
|
38164
38421
|
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
38165
38422
|
const ARRAY_KEYS = /* @__PURE__ */ new Set([
|
|
38166
38423
|
"languages",
|
|
38167
38424
|
"labels",
|
|
38168
|
-
"preferredOrgs",
|
|
38169
38425
|
"projectCategories",
|
|
38170
38426
|
"excludeRepos",
|
|
38171
38427
|
"excludeOrgs",
|
|
@@ -38176,7 +38432,10 @@ function registerTools(server, scout) {
|
|
|
38176
38432
|
const NUMBER_KEYS = /* @__PURE__ */ new Set([
|
|
38177
38433
|
"minStars",
|
|
38178
38434
|
"maxIssueAgeDays",
|
|
38179
|
-
"minRepoScoreThreshold"
|
|
38435
|
+
"minRepoScoreThreshold",
|
|
38436
|
+
"interPhaseDelayMs",
|
|
38437
|
+
"broadPhaseDelayMs",
|
|
38438
|
+
"skipBroadWhenSufficientResults"
|
|
38180
38439
|
]);
|
|
38181
38440
|
const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["includeDocIssues"]);
|
|
38182
38441
|
server.tool(
|
|
@@ -38321,7 +38580,7 @@ async function main() {
|
|
|
38321
38580
|
}
|
|
38322
38581
|
const scout = await createScout({ githubToken: token });
|
|
38323
38582
|
const server = new McpServer(
|
|
38324
|
-
{ name: "
|
|
38583
|
+
{ name: "oss-scout-mcp", version: "0.2.1" },
|
|
38325
38584
|
{ capabilities: { resources: {}, tools: {} } }
|
|
38326
38585
|
);
|
|
38327
38586
|
registerTools(server, scout);
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SearchStrategySchema, ScoutPreferencesSchema } from "@oss-scout/core";
|
|
3
|
+
const TOOL_TIMEOUT_MS = 60000;
|
|
4
|
+
function withTimeout(promise, ms = TOOL_TIMEOUT_MS) {
|
|
5
|
+
let timeoutId;
|
|
6
|
+
const timer = new Promise((_, reject) => {
|
|
7
|
+
timeoutId = setTimeout(() => reject(new Error(`Request timed out after ${ms / 1000}s`)), ms);
|
|
8
|
+
});
|
|
9
|
+
return Promise.race([promise, timer]).finally(() => clearTimeout(timeoutId));
|
|
10
|
+
}
|
|
3
11
|
export function registerTools(server, scout) {
|
|
4
12
|
server.tool("search", "Search for open source issues matching your preferences", {
|
|
5
13
|
maxResults: z
|
|
@@ -9,7 +17,7 @@ export function registerTools(server, scout) {
|
|
|
9
17
|
strategies: z
|
|
10
18
|
.string()
|
|
11
19
|
.optional()
|
|
12
|
-
.describe("Comma-separated search strategies: merged,
|
|
20
|
+
.describe("Comma-separated search strategies: merged, starred, broad, maintained, all"),
|
|
13
21
|
}, async ({ maxResults, strategies }) => {
|
|
14
22
|
try {
|
|
15
23
|
const parsedStrategies = strategies
|
|
@@ -17,10 +25,10 @@ export function registerTools(server, scout) {
|
|
|
17
25
|
.split(",")
|
|
18
26
|
.map((s) => SearchStrategySchema.parse(s.trim()))
|
|
19
27
|
: undefined;
|
|
20
|
-
const result = await scout.search({
|
|
28
|
+
const result = await withTimeout(scout.search({
|
|
21
29
|
maxResults: maxResults ?? 10,
|
|
22
30
|
strategies: parsedStrategies,
|
|
23
|
-
});
|
|
31
|
+
}));
|
|
24
32
|
scout.saveResults(result.candidates);
|
|
25
33
|
await scout.checkpoint();
|
|
26
34
|
return {
|
|
@@ -41,7 +49,7 @@ export function registerTools(server, scout) {
|
|
|
41
49
|
.describe("Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)"),
|
|
42
50
|
}, async ({ issueUrl }) => {
|
|
43
51
|
try {
|
|
44
|
-
const candidate = await scout.vetIssue(issueUrl);
|
|
52
|
+
const candidate = await withTimeout(scout.vetIssue(issueUrl));
|
|
45
53
|
return {
|
|
46
54
|
content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }],
|
|
47
55
|
};
|
|
@@ -60,11 +68,108 @@ export function registerTools(server, scout) {
|
|
|
60
68
|
content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }],
|
|
61
69
|
};
|
|
62
70
|
});
|
|
71
|
+
server.tool("skip", "Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.", {
|
|
72
|
+
action: z
|
|
73
|
+
.enum(["add", "remove", "list", "clear"])
|
|
74
|
+
.default("add")
|
|
75
|
+
.describe("Action to perform (default: add)"),
|
|
76
|
+
issueUrl: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("GitHub issue URL (required for add/remove actions)"),
|
|
80
|
+
}, async ({ action, issueUrl }) => {
|
|
81
|
+
try {
|
|
82
|
+
if ((action === "add" || action === "remove") && !issueUrl) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Error: issueUrl is required for the "${action}" action.`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (action === "list") {
|
|
94
|
+
const skipped = scout.getSkippedIssues();
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: JSON.stringify(skipped, null, 2) }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (action === "clear") {
|
|
100
|
+
const count = scout.getSkippedIssues().length;
|
|
101
|
+
scout.clearSkippedIssues();
|
|
102
|
+
const synced = await scout.checkpoint();
|
|
103
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: count > 0
|
|
109
|
+
? `Skip list cleared (${count} entries removed)${syncNote}`
|
|
110
|
+
: `Skip list already empty${syncNote}`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (action === "remove") {
|
|
116
|
+
const wasPresent = scout
|
|
117
|
+
.getSkippedIssues()
|
|
118
|
+
.some((s) => s.url === issueUrl);
|
|
119
|
+
scout.unskipIssue(issueUrl);
|
|
120
|
+
const synced = await scout.checkpoint();
|
|
121
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: wasPresent
|
|
127
|
+
? `Removed from skip list: ${issueUrl}${syncNote}`
|
|
128
|
+
: `Not in skip list: ${issueUrl}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// action === "add"
|
|
134
|
+
const alreadySkipped = scout
|
|
135
|
+
.getSkippedIssues()
|
|
136
|
+
.some((s) => s.url === issueUrl);
|
|
137
|
+
if (alreadySkipped) {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{ type: "text", text: `Already in skip list: ${issueUrl}` },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const saved = scout
|
|
145
|
+
.getSavedResults()
|
|
146
|
+
.find((r) => r.issueUrl === issueUrl);
|
|
147
|
+
const metadata = saved
|
|
148
|
+
? {
|
|
149
|
+
repo: saved.repo,
|
|
150
|
+
number: saved.number,
|
|
151
|
+
title: saved.title,
|
|
152
|
+
}
|
|
153
|
+
: undefined;
|
|
154
|
+
scout.skipIssue(issueUrl, metadata);
|
|
155
|
+
const synced = await scout.checkpoint();
|
|
156
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
63
169
|
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
64
170
|
const ARRAY_KEYS = new Set([
|
|
65
171
|
"languages",
|
|
66
172
|
"labels",
|
|
67
|
-
"preferredOrgs",
|
|
68
173
|
"projectCategories",
|
|
69
174
|
"excludeRepos",
|
|
70
175
|
"excludeOrgs",
|
|
@@ -76,6 +181,9 @@ export function registerTools(server, scout) {
|
|
|
76
181
|
"minStars",
|
|
77
182
|
"maxIssueAgeDays",
|
|
78
183
|
"minRepoScoreThreshold",
|
|
184
|
+
"interPhaseDelayMs",
|
|
185
|
+
"broadPhaseDelayMs",
|
|
186
|
+
"skipBroadWhenSufficientResults",
|
|
79
187
|
]);
|
|
80
188
|
const BOOLEAN_KEYS = new Set(["includeDocIssues"]);
|
|
81
189
|
server.tool("config-set", "Update an oss-scout configuration preference", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MCP server for oss-scout — exposes issue discovery and vetting as MCP tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,19 +15,10 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist/"
|
|
17
17
|
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "tsc",
|
|
20
|
-
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/mcp-server.bundle.cjs",
|
|
21
|
-
"test": "vitest run",
|
|
22
|
-
"test:watch": "vitest",
|
|
23
|
-
"test:coverage": "vitest run --coverage",
|
|
24
|
-
"typecheck": "tsc --noEmit",
|
|
25
|
-
"prepublishOnly": "pnpm run build && pnpm run bundle"
|
|
26
|
-
},
|
|
27
18
|
"dependencies": {
|
|
28
19
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
20
|
+
"zod": "^4.3.6",
|
|
21
|
+
"@oss-scout/core": "^0.4.0"
|
|
31
22
|
},
|
|
32
23
|
"devDependencies": {
|
|
33
24
|
"@types/node": "^25.5.0",
|
|
@@ -37,6 +28,12 @@
|
|
|
37
28
|
"@vitest/coverage-v8": "^4.1.0",
|
|
38
29
|
"vitest": "^4.1.0"
|
|
39
30
|
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"mcp-server",
|
|
34
|
+
"claude",
|
|
35
|
+
"issue-discovery"
|
|
36
|
+
],
|
|
40
37
|
"engines": {
|
|
41
38
|
"node": ">=20"
|
|
42
39
|
},
|
|
@@ -45,5 +42,13 @@
|
|
|
45
42
|
"type": "git",
|
|
46
43
|
"url": "git+https://github.com/costajohnt/oss-scout.git",
|
|
47
44
|
"directory": "packages/mcp-server"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc",
|
|
48
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/mcp-server.bundle.cjs",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest",
|
|
51
|
+
"test:coverage": "vitest run --coverage",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
48
53
|
}
|
|
49
|
-
}
|
|
54
|
+
}
|