@oss-scout/core 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/dist/cli.bundle.cjs +51 -47
- package/dist/cli.js +218 -87
- package/dist/commands/config.d.ts +2 -4
- package/dist/commands/config.js +76 -78
- package/dist/commands/results.d.ts +1 -1
- package/dist/commands/results.js +1 -1
- package/dist/commands/search.d.ts +2 -2
- package/dist/commands/search.js +16 -6
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +25 -25
- package/dist/commands/skip.d.ts +33 -0
- package/dist/commands/skip.js +89 -0
- package/dist/commands/validation.d.ts +1 -1
- package/dist/commands/validation.js +1 -1
- package/dist/commands/vet-list.d.ts +2 -2
- package/dist/commands/vet-list.js +12 -5
- package/dist/commands/vet.d.ts +3 -3
- package/dist/commands/vet.js +9 -5
- package/dist/core/bootstrap.d.ts +1 -1
- package/dist/core/bootstrap.js +20 -16
- package/dist/core/category-mapping.d.ts +1 -1
- package/dist/core/category-mapping.js +104 -13
- package/dist/core/errors.d.ts +8 -1
- package/dist/core/errors.js +31 -19
- package/dist/core/gist-state-store.d.ts +1 -1
- package/dist/core/gist-state-store.js +55 -28
- package/dist/core/github.d.ts +1 -1
- package/dist/core/github.js +5 -5
- package/dist/core/http-cache.js +26 -22
- package/dist/core/issue-discovery.d.ts +6 -6
- package/dist/core/issue-discovery.js +279 -286
- package/dist/core/issue-eligibility.d.ts +2 -2
- package/dist/core/issue-eligibility.js +26 -21
- package/dist/core/issue-filtering.js +23 -15
- package/dist/core/issue-scoring.js +1 -1
- package/dist/core/issue-vetting.d.ts +2 -4
- package/dist/core/issue-vetting.js +65 -56
- package/dist/core/local-state.d.ts +1 -1
- package/dist/core/local-state.js +16 -14
- package/dist/core/repo-health.d.ts +2 -2
- package/dist/core/repo-health.js +46 -35
- package/dist/core/schemas.d.ts +17 -9
- package/dist/core/schemas.js +47 -19
- package/dist/core/search-budget.js +3 -3
- package/dist/core/search-phases.d.ts +6 -6
- package/dist/core/search-phases.js +23 -19
- package/dist/core/types.d.ts +9 -9
- package/dist/core/types.js +15 -3
- package/dist/core/utils.d.ts +10 -1
- package/dist/core/utils.js +44 -25
- package/dist/formatters/json.d.ts +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +5 -5
- package/dist/scout.d.ts +30 -6
- package/dist/scout.js +141 -34
- package/package.json +7 -3
package/dist/core/utils.js
CHANGED
|
@@ -1,39 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared utility functions for oss-scout.
|
|
3
3
|
*/
|
|
4
|
-
import * as fs from
|
|
5
|
-
import * as path from
|
|
6
|
-
import * as os from
|
|
7
|
-
import { execFileSync } from
|
|
8
|
-
import { ConfigurationError, errorMessage } from
|
|
9
|
-
import { debug } from
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
import { ConfigurationError, errorMessage } from "./errors.js";
|
|
9
|
+
import { debug } from "./logger.js";
|
|
10
10
|
export function sleep(ms) {
|
|
11
11
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
12
|
}
|
|
13
|
-
const MODULE =
|
|
13
|
+
const MODULE = "utils";
|
|
14
14
|
let cachedGitHubToken = null;
|
|
15
15
|
let tokenFetchAttempted = false;
|
|
16
16
|
export function getDataDir() {
|
|
17
|
-
const dir = path.join(os.homedir(),
|
|
17
|
+
const dir = path.join(os.homedir(), ".oss-scout");
|
|
18
18
|
if (!fs.existsSync(dir)) {
|
|
19
19
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
20
20
|
}
|
|
21
21
|
return dir;
|
|
22
22
|
}
|
|
23
23
|
export function getCacheDir() {
|
|
24
|
-
const dir = path.join(getDataDir(),
|
|
24
|
+
const dir = path.join(getDataDir(), "cache");
|
|
25
25
|
if (!fs.existsSync(dir)) {
|
|
26
26
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
27
27
|
}
|
|
28
28
|
return dir;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract "owner/repo" from any GitHub URL format:
|
|
32
|
+
* - https://github.com/owner/repo
|
|
33
|
+
* - https://github.com/owner/repo/pull/123
|
|
34
|
+
* - https://github.com/owner/repo/issues/123
|
|
35
|
+
* - https://api.github.com/repos/owner/repo
|
|
36
|
+
* - https://api.github.com/repos/owner/repo/...
|
|
37
|
+
*/
|
|
38
|
+
export function extractRepoFromUrl(url) {
|
|
39
|
+
// API URLs: https://api.github.com/repos/owner/repo[/...]
|
|
40
|
+
const apiMatch = url.match(/api\.github\.com\/repos\/([^/]+\/[^/]+)/);
|
|
41
|
+
if (apiMatch)
|
|
42
|
+
return apiMatch[1];
|
|
43
|
+
// Web URLs: https://github.com/owner/repo[/...]
|
|
44
|
+
const webMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
45
|
+
if (webMatch)
|
|
46
|
+
return webMatch[1];
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
30
49
|
const OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
31
50
|
const REPO_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
32
51
|
function isValidOwnerRepo(owner, repo) {
|
|
33
52
|
return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
|
|
34
53
|
}
|
|
35
54
|
export function parseGitHubUrl(url) {
|
|
36
|
-
if (!url.startsWith(
|
|
55
|
+
if (!url.startsWith("https://github.com/"))
|
|
37
56
|
return null;
|
|
38
57
|
const prMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
39
58
|
if (prMatch) {
|
|
@@ -41,7 +60,7 @@ export function parseGitHubUrl(url) {
|
|
|
41
60
|
const repo = prMatch[2];
|
|
42
61
|
if (!isValidOwnerRepo(owner, repo))
|
|
43
62
|
return null;
|
|
44
|
-
return { owner, repo, number: parseInt(prMatch[3], 10), type:
|
|
63
|
+
return { owner, repo, number: parseInt(prMatch[3], 10), type: "pull" };
|
|
45
64
|
}
|
|
46
65
|
const issueMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
47
66
|
if (issueMatch) {
|
|
@@ -49,7 +68,7 @@ export function parseGitHubUrl(url) {
|
|
|
49
68
|
const repo = issueMatch[2];
|
|
50
69
|
if (!isValidOwnerRepo(owner, repo))
|
|
51
70
|
return null;
|
|
52
|
-
return { owner, repo, number: parseInt(issueMatch[3], 10), type:
|
|
71
|
+
return { owner, repo, number: parseInt(issueMatch[3], 10), type: "issues" };
|
|
53
72
|
}
|
|
54
73
|
return null;
|
|
55
74
|
}
|
|
@@ -58,12 +77,12 @@ export function daysBetween(from, to = new Date()) {
|
|
|
58
77
|
}
|
|
59
78
|
export function getCLIVersion() {
|
|
60
79
|
try {
|
|
61
|
-
const pkgPath = path.join(path.dirname(process.argv[1]),
|
|
62
|
-
return JSON.parse(fs.readFileSync(pkgPath,
|
|
80
|
+
const pkgPath = path.join(path.dirname(process.argv[1]), "..", "package.json");
|
|
81
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
63
82
|
}
|
|
64
83
|
catch (err) {
|
|
65
84
|
debug(MODULE, `Could not read CLI version: ${errorMessage(err)}`);
|
|
66
|
-
return
|
|
85
|
+
return "unknown";
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
88
|
export function getGitHubToken() {
|
|
@@ -77,30 +96,30 @@ export function getGitHubToken() {
|
|
|
77
96
|
return cachedGitHubToken;
|
|
78
97
|
}
|
|
79
98
|
try {
|
|
80
|
-
const token = execFileSync(
|
|
81
|
-
encoding:
|
|
82
|
-
stdio: [
|
|
99
|
+
const token = execFileSync("gh", ["auth", "token"], {
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
83
102
|
timeout: 2000,
|
|
84
103
|
}).trim();
|
|
85
104
|
if (token && token.length > 0) {
|
|
86
105
|
cachedGitHubToken = token;
|
|
87
|
-
debug(MODULE,
|
|
106
|
+
debug(MODULE, "Using GitHub token from gh CLI");
|
|
88
107
|
return cachedGitHubToken;
|
|
89
108
|
}
|
|
90
109
|
}
|
|
91
110
|
catch (err) {
|
|
92
|
-
debug(MODULE,
|
|
111
|
+
debug(MODULE, "gh auth token failed", err);
|
|
93
112
|
}
|
|
94
113
|
return null;
|
|
95
114
|
}
|
|
96
115
|
export function requireGitHubToken() {
|
|
97
116
|
const token = getGitHubToken();
|
|
98
117
|
if (!token) {
|
|
99
|
-
throw new ConfigurationError(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
throw new ConfigurationError("GitHub authentication required.\n\n" +
|
|
119
|
+
"Options:\n" +
|
|
120
|
+
" 1. Use gh CLI: gh auth login\n" +
|
|
121
|
+
" 2. Set GITHUB_TOKEN environment variable\n\n" +
|
|
122
|
+
"The gh CLI is recommended - install from https://cli.github.com");
|
|
104
123
|
}
|
|
105
124
|
return token;
|
|
106
125
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JSON output formatter for oss-scout CLI.
|
|
3
3
|
*/
|
|
4
|
-
import type { ErrorCode } from
|
|
4
|
+
import type { ErrorCode } from "../core/errors.js";
|
|
5
5
|
export declare function formatJsonSuccess<T>(data: T): string;
|
|
6
6
|
export declare function formatJsonError(error: string, errorCode?: ErrorCode): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @packageDocumentation
|
|
16
16
|
*/
|
|
17
|
-
export { createScout, OssScout } from
|
|
18
|
-
export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from
|
|
19
|
-
export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, SearchStrategy, } from
|
|
20
|
-
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, } from
|
|
21
|
-
export { requireGitHubToken, getGitHubToken } from
|
|
22
|
-
export { IssueDiscovery } from
|
|
23
|
-
export { IssueVetter, type ScoutStateReader } from
|
|
17
|
+
export { createScout, OssScout } from "./scout.js";
|
|
18
|
+
export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
|
|
19
|
+
export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, SearchStrategy, SkippedIssue, } from "./core/schemas.js";
|
|
20
|
+
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, } from "./core/schemas.js";
|
|
21
|
+
export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
|
|
22
|
+
export { IssueDiscovery } from "./core/issue-discovery.js";
|
|
23
|
+
export { IssueVetter, type ScoutStateReader } from "./core/issue-vetting.js";
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* @packageDocumentation
|
|
16
16
|
*/
|
|
17
17
|
// Main API
|
|
18
|
-
export { createScout, OssScout } from
|
|
18
|
+
export { createScout, OssScout } from "./scout.js";
|
|
19
19
|
// Schemas (for consumers who need runtime validation)
|
|
20
|
-
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, } from
|
|
20
|
+
export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, } from "./core/schemas.js";
|
|
21
21
|
// Utilities
|
|
22
|
-
export { requireGitHubToken, getGitHubToken } from
|
|
22
|
+
export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
|
|
23
23
|
// Internal classes (for advanced use)
|
|
24
|
-
export { IssueDiscovery } from
|
|
25
|
-
export { IssueVetter } from
|
|
24
|
+
export { IssueDiscovery } from "./core/issue-discovery.js";
|
|
25
|
+
export { IssueVetter } from "./core/issue-vetting.js";
|
package/dist/scout.d.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Provides personalized issue discovery, vetting, and scoring.
|
|
5
5
|
* Implements ScoutStateReader to bridge state with the search engine.
|
|
6
6
|
*/
|
|
7
|
-
import type { ScoutStateReader } from
|
|
8
|
-
import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate } from
|
|
9
|
-
import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from
|
|
10
|
-
import { GistStateStore } from
|
|
7
|
+
import type { ScoutStateReader } from "./core/issue-vetting.js";
|
|
8
|
+
import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate, SkippedIssue } from "./core/schemas.js";
|
|
9
|
+
import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from "./core/types.js";
|
|
10
|
+
import { GistStateStore } from "./core/gist-state-store.js";
|
|
11
11
|
/**
|
|
12
12
|
* Create an OssScout instance.
|
|
13
13
|
*
|
|
@@ -44,6 +44,7 @@ export declare class OssScout implements ScoutStateReader {
|
|
|
44
44
|
constructor(githubToken: string, initialState: ScoutState, gistStore?: GistStateStore | null);
|
|
45
45
|
/**
|
|
46
46
|
* Multi-strategy issue search. Returns scored, sorted candidates.
|
|
47
|
+
* Automatically culls expired skip entries and filters skipped issues.
|
|
47
48
|
*/
|
|
48
49
|
search(options?: SearchOptions): Promise<SearchResult>;
|
|
49
50
|
/**
|
|
@@ -59,7 +60,6 @@ export declare class OssScout implements ScoutStateReader {
|
|
|
59
60
|
private classifyVetResult;
|
|
60
61
|
getReposWithMergedPRs(): string[];
|
|
61
62
|
getStarredRepos(): string[];
|
|
62
|
-
getPreferredOrgs(): string[];
|
|
63
63
|
getProjectCategories(): ProjectCategory[];
|
|
64
64
|
getRepoScore(repo: string): number | null;
|
|
65
65
|
/** Get current preferences (read-only). */
|
|
@@ -101,6 +101,31 @@ export declare class OssScout implements ScoutStateReader {
|
|
|
101
101
|
* Clear all saved results.
|
|
102
102
|
*/
|
|
103
103
|
clearResults(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Skip an issue — excludes it from future searches. Auto-culled after 90 days.
|
|
106
|
+
*/
|
|
107
|
+
skipIssue(url: string, metadata?: {
|
|
108
|
+
repo?: string;
|
|
109
|
+
number?: number;
|
|
110
|
+
title?: string;
|
|
111
|
+
}): void;
|
|
112
|
+
/**
|
|
113
|
+
* Get all skipped issues.
|
|
114
|
+
*/
|
|
115
|
+
getSkippedIssues(): SkippedIssue[];
|
|
116
|
+
/**
|
|
117
|
+
* Remove a specific issue from the skip list.
|
|
118
|
+
*/
|
|
119
|
+
unskipIssue(url: string): void;
|
|
120
|
+
/**
|
|
121
|
+
* Clear all skipped issues.
|
|
122
|
+
*/
|
|
123
|
+
clearSkippedIssues(): void;
|
|
124
|
+
/**
|
|
125
|
+
* Remove skipped issues older than maxDays (default 90). Called automatically during search.
|
|
126
|
+
* @returns The number of expired entries that were removed.
|
|
127
|
+
*/
|
|
128
|
+
cullExpiredSkips(maxDays?: number): number;
|
|
104
129
|
/**
|
|
105
130
|
* Check if state has uncommitted changes.
|
|
106
131
|
*/
|
|
@@ -114,7 +139,6 @@ export declare class OssScout implements ScoutStateReader {
|
|
|
114
139
|
* Get the full state snapshot for serialization or external consumption.
|
|
115
140
|
*/
|
|
116
141
|
getState(): Readonly<ScoutState>;
|
|
117
|
-
private extractRepoFromUrl;
|
|
118
142
|
private updateRepoScoreFromPRs;
|
|
119
143
|
/**
|
|
120
144
|
* Calculate repo score (1-10) from observed data.
|
package/dist/scout.js
CHANGED
|
@@ -4,12 +4,55 @@
|
|
|
4
4
|
* Provides personalized issue discovery, vetting, and scoring.
|
|
5
5
|
* Implements ScoutStateReader to bridge state with the search engine.
|
|
6
6
|
*/
|
|
7
|
-
import { IssueDiscovery } from
|
|
8
|
-
import { ScoutStateSchema } from
|
|
9
|
-
import { GistStateStore, mergeStates } from
|
|
10
|
-
import { getOctokit } from
|
|
11
|
-
import { loadLocalState } from
|
|
12
|
-
import { warn } from
|
|
7
|
+
import { IssueDiscovery } from "./core/issue-discovery.js";
|
|
8
|
+
import { ScoutStateSchema } from "./core/schemas.js";
|
|
9
|
+
import { GistStateStore, mergeStates } from "./core/gist-state-store.js";
|
|
10
|
+
import { getOctokit } from "./core/github.js";
|
|
11
|
+
import { loadLocalState } from "./core/local-state.js";
|
|
12
|
+
import { warn } from "./core/logger.js";
|
|
13
|
+
import { extractRepoFromUrl } from "./core/utils.js";
|
|
14
|
+
/** Wrap a real Octokit instance as GistOctokitLike without unsafe double casts. */
|
|
15
|
+
function toGistOctokit(octokit) {
|
|
16
|
+
return {
|
|
17
|
+
gists: {
|
|
18
|
+
async get(params) {
|
|
19
|
+
const { data } = await octokit.gists.get(params);
|
|
20
|
+
if (!data.id)
|
|
21
|
+
throw new Error("Gist get returned no id");
|
|
22
|
+
const files = data.files
|
|
23
|
+
? Object.fromEntries(Object.entries(data.files).map(([k, v]) => [
|
|
24
|
+
k,
|
|
25
|
+
v ? { content: v.content } : undefined,
|
|
26
|
+
]))
|
|
27
|
+
: null;
|
|
28
|
+
return { data: { id: data.id, files } };
|
|
29
|
+
},
|
|
30
|
+
async create(params) {
|
|
31
|
+
const { data } = await octokit.gists.create(params);
|
|
32
|
+
if (!data.id)
|
|
33
|
+
throw new Error("Gist create returned no id");
|
|
34
|
+
return { data: { id: data.id } };
|
|
35
|
+
},
|
|
36
|
+
async update(params) {
|
|
37
|
+
const { data } = await octokit.gists.update(params);
|
|
38
|
+
if (!data.id)
|
|
39
|
+
throw new Error("Gist update returned no id");
|
|
40
|
+
return { data: { id: data.id } };
|
|
41
|
+
},
|
|
42
|
+
async list(params) {
|
|
43
|
+
const { data } = await octokit.gists.list(params);
|
|
44
|
+
return {
|
|
45
|
+
data: data
|
|
46
|
+
.filter((g) => g.id)
|
|
47
|
+
.map((g) => ({
|
|
48
|
+
id: g.id,
|
|
49
|
+
description: g.description ?? null,
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
13
56
|
/**
|
|
14
57
|
* Create an OssScout instance.
|
|
15
58
|
*
|
|
@@ -34,14 +77,14 @@ import { warn } from './core/logger.js';
|
|
|
34
77
|
export async function createScout(config) {
|
|
35
78
|
let state;
|
|
36
79
|
let gistStore = null;
|
|
37
|
-
if (config.persistence ===
|
|
80
|
+
if (config.persistence === "provided") {
|
|
38
81
|
state = config.initialState;
|
|
39
82
|
}
|
|
40
|
-
else if (config.persistence ===
|
|
41
|
-
gistStore = new GistStateStore(getOctokit(config.githubToken));
|
|
83
|
+
else if (config.persistence === "gist") {
|
|
84
|
+
gistStore = new GistStateStore(toGistOctokit(getOctokit(config.githubToken)));
|
|
42
85
|
const result = await gistStore.bootstrap();
|
|
43
86
|
if (result.degraded) {
|
|
44
|
-
warn(
|
|
87
|
+
warn("scout", "Gist sync unavailable — running in offline mode. Changes will only be saved locally.");
|
|
45
88
|
}
|
|
46
89
|
const localState = loadLocalState();
|
|
47
90
|
state = mergeStates(localState, result.state);
|
|
@@ -76,12 +119,17 @@ export class OssScout {
|
|
|
76
119
|
// ── Search ──────────────────────────────────────────────────────────
|
|
77
120
|
/**
|
|
78
121
|
* Multi-strategy issue search. Returns scored, sorted candidates.
|
|
122
|
+
* Automatically culls expired skip entries and filters skipped issues.
|
|
79
123
|
*/
|
|
80
124
|
async search(options) {
|
|
125
|
+
// Auto-cull expired skips before searching
|
|
126
|
+
this.cullExpiredSkips();
|
|
127
|
+
const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
81
128
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
82
129
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
83
130
|
maxResults: options?.maxResults,
|
|
84
131
|
strategies: options?.strategies,
|
|
132
|
+
skippedUrls,
|
|
85
133
|
});
|
|
86
134
|
this.state.lastSearchAt = new Date().toISOString();
|
|
87
135
|
this.dirty = true;
|
|
@@ -126,13 +174,13 @@ export class OssScout {
|
|
|
126
174
|
})
|
|
127
175
|
.catch((error) => {
|
|
128
176
|
const msg = error instanceof Error ? error.message : String(error);
|
|
129
|
-
const isGone = msg.includes(
|
|
177
|
+
const isGone = msg.includes("Not Found") || msg.includes("410");
|
|
130
178
|
results.push({
|
|
131
179
|
issueUrl: item.issueUrl,
|
|
132
180
|
repo: item.repo,
|
|
133
181
|
number: item.number,
|
|
134
182
|
title: item.title,
|
|
135
|
-
status: isGone ?
|
|
183
|
+
status: isGone ? "closed" : "error",
|
|
136
184
|
errorMessage: msg,
|
|
137
185
|
});
|
|
138
186
|
})
|
|
@@ -147,15 +195,18 @@ export class OssScout {
|
|
|
147
195
|
await Promise.allSettled(pending.values());
|
|
148
196
|
const summary = {
|
|
149
197
|
total: results.length,
|
|
150
|
-
stillAvailable: results.filter((r) => r.status ===
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
198
|
+
stillAvailable: results.filter((r) => r.status === "still_available")
|
|
199
|
+
.length,
|
|
200
|
+
claimed: results.filter((r) => r.status === "claimed").length,
|
|
201
|
+
closed: results.filter((r) => r.status === "closed").length,
|
|
202
|
+
hasPR: results.filter((r) => r.status === "has_pr").length,
|
|
203
|
+
errors: results.filter((r) => r.status === "error").length,
|
|
155
204
|
};
|
|
156
205
|
let prunedCount;
|
|
157
206
|
if (options?.prune) {
|
|
158
|
-
const unavailableUrls = new Set(results
|
|
207
|
+
const unavailableUrls = new Set(results
|
|
208
|
+
.filter((r) => r.status !== "still_available")
|
|
209
|
+
.map((r) => r.issueUrl));
|
|
159
210
|
const before = (this.state.savedResults ?? []).length;
|
|
160
211
|
this.state.savedResults = (this.state.savedResults ?? []).filter((r) => !unavailableUrls.has(r.issueUrl));
|
|
161
212
|
prunedCount = before - (this.state.savedResults?.length ?? 0);
|
|
@@ -166,16 +217,16 @@ export class OssScout {
|
|
|
166
217
|
classifyVetResult(candidate) {
|
|
167
218
|
const checks = candidate.vettingResult.checks;
|
|
168
219
|
if (!checks.noExistingPR)
|
|
169
|
-
return
|
|
220
|
+
return "has_pr";
|
|
170
221
|
if (!checks.notClaimed)
|
|
171
|
-
return
|
|
172
|
-
return
|
|
222
|
+
return "claimed";
|
|
223
|
+
return "still_available";
|
|
173
224
|
}
|
|
174
225
|
// ── State Reads (ScoutStateReader implementation) ───────────────────
|
|
175
226
|
getReposWithMergedPRs() {
|
|
176
227
|
const repoCounts = new Map();
|
|
177
228
|
for (const pr of this.state.mergedPRs ?? []) {
|
|
178
|
-
const repo =
|
|
229
|
+
const repo = extractRepoFromUrl(pr.url);
|
|
179
230
|
if (repo) {
|
|
180
231
|
repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
|
|
181
232
|
}
|
|
@@ -188,9 +239,6 @@ export class OssScout {
|
|
|
188
239
|
getStarredRepos() {
|
|
189
240
|
return this.state.starredRepos;
|
|
190
241
|
}
|
|
191
|
-
getPreferredOrgs() {
|
|
192
|
-
return this.state.preferences.preferredOrgs;
|
|
193
|
-
}
|
|
194
242
|
getProjectCategories() {
|
|
195
243
|
return this.state.preferences.projectCategories;
|
|
196
244
|
}
|
|
@@ -323,6 +371,70 @@ export class OssScout {
|
|
|
323
371
|
this.state.savedResults = [];
|
|
324
372
|
this.dirty = true;
|
|
325
373
|
}
|
|
374
|
+
// ── Skip List ───────────────────────────────────────────────────────
|
|
375
|
+
/**
|
|
376
|
+
* Skip an issue — excludes it from future searches. Auto-culled after 90 days.
|
|
377
|
+
*/
|
|
378
|
+
skipIssue(url, metadata) {
|
|
379
|
+
const existing = this.state.skippedIssues ?? [];
|
|
380
|
+
if (existing.some((s) => s.url === url))
|
|
381
|
+
return; // already skipped
|
|
382
|
+
this.state.skippedIssues = [
|
|
383
|
+
...existing,
|
|
384
|
+
{
|
|
385
|
+
url,
|
|
386
|
+
repo: metadata?.repo ?? "",
|
|
387
|
+
number: metadata?.number ?? 0,
|
|
388
|
+
title: metadata?.title ?? "",
|
|
389
|
+
skippedAt: new Date().toISOString(),
|
|
390
|
+
},
|
|
391
|
+
];
|
|
392
|
+
// Also remove from saved results if present
|
|
393
|
+
if (this.state.savedResults) {
|
|
394
|
+
this.state.savedResults = this.state.savedResults.filter((r) => r.issueUrl !== url);
|
|
395
|
+
}
|
|
396
|
+
this.dirty = true;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get all skipped issues.
|
|
400
|
+
*/
|
|
401
|
+
getSkippedIssues() {
|
|
402
|
+
return this.state.skippedIssues ?? [];
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Remove a specific issue from the skip list.
|
|
406
|
+
*/
|
|
407
|
+
unskipIssue(url) {
|
|
408
|
+
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url);
|
|
409
|
+
this.dirty = true;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Clear all skipped issues.
|
|
413
|
+
*/
|
|
414
|
+
clearSkippedIssues() {
|
|
415
|
+
this.state.skippedIssues = [];
|
|
416
|
+
this.dirty = true;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Remove skipped issues older than maxDays (default 90). Called automatically during search.
|
|
420
|
+
* @returns The number of expired entries that were removed.
|
|
421
|
+
*/
|
|
422
|
+
cullExpiredSkips(maxDays = 90) {
|
|
423
|
+
const cutoff = new Date();
|
|
424
|
+
cutoff.setDate(cutoff.getDate() - maxDays);
|
|
425
|
+
const before = (this.state.skippedIssues ?? []).length;
|
|
426
|
+
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => {
|
|
427
|
+
const d = new Date(s.skippedAt);
|
|
428
|
+
if (isNaN(d.getTime())) {
|
|
429
|
+
return true; // keep entries with invalid dates rather than silently dropping
|
|
430
|
+
}
|
|
431
|
+
return d >= cutoff;
|
|
432
|
+
});
|
|
433
|
+
const culled = before - this.state.skippedIssues.length;
|
|
434
|
+
if (culled > 0)
|
|
435
|
+
this.dirty = true;
|
|
436
|
+
return culled;
|
|
437
|
+
}
|
|
326
438
|
// ── Persistence ─────────────────────────────────────────────────────
|
|
327
439
|
/**
|
|
328
440
|
* Check if state has uncommitted changes.
|
|
@@ -353,21 +465,16 @@ export class OssScout {
|
|
|
353
465
|
return this.state;
|
|
354
466
|
}
|
|
355
467
|
// ── Private helpers ─────────────────────────────────────────────────
|
|
356
|
-
extractRepoFromUrl(url) {
|
|
357
|
-
const match = url.match(/github\.com\/([^/]+\/[^/]+)\//);
|
|
358
|
-
return match ? match[1] : null;
|
|
359
|
-
}
|
|
360
468
|
updateRepoScoreFromPRs(repo) {
|
|
361
|
-
const mergedCount = (this.state.mergedPRs ?? []).filter((p) =>
|
|
362
|
-
const closedCount = (this.state.closedPRs ?? []).filter((p) =>
|
|
469
|
+
const mergedCount = (this.state.mergedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
|
|
470
|
+
const closedCount = (this.state.closedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
|
|
363
471
|
this.updateRepoScore(repo, {
|
|
364
472
|
mergedPRCount: mergedCount,
|
|
365
473
|
closedWithoutMergeCount: closedCount,
|
|
366
474
|
lastMergedAt: mergedCount > 0
|
|
367
475
|
? (this.state.mergedPRs ?? [])
|
|
368
|
-
.filter((p) =>
|
|
369
|
-
.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt))[0]
|
|
370
|
-
?.mergedAt
|
|
476
|
+
.filter((p) => extractRepoFromUrl(p.url) === repo)
|
|
477
|
+
.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt))[0]?.mergedAt
|
|
371
478
|
: undefined,
|
|
372
479
|
});
|
|
373
480
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Personalized GitHub issue finder with multi-strategy search, deep vetting, and viability scoring — CLI, library, MCP server, and Claude Code plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"oss-scout": "./dist/cli.bundle.cjs"
|
|
@@ -37,7 +37,11 @@
|
|
|
37
37
|
"issue-discovery",
|
|
38
38
|
"cli",
|
|
39
39
|
"vetting",
|
|
40
|
-
"contributions"
|
|
40
|
+
"contributions",
|
|
41
|
+
"claude",
|
|
42
|
+
"mcp-server",
|
|
43
|
+
"contribution-finder",
|
|
44
|
+
"personalized"
|
|
41
45
|
],
|
|
42
46
|
"author": "John Costa",
|
|
43
47
|
"license": "MIT",
|