@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.
Files changed (56) hide show
  1. package/dist/cli.bundle.cjs +51 -47
  2. package/dist/cli.js +218 -87
  3. package/dist/commands/config.d.ts +2 -4
  4. package/dist/commands/config.js +76 -78
  5. package/dist/commands/results.d.ts +1 -1
  6. package/dist/commands/results.js +1 -1
  7. package/dist/commands/search.d.ts +2 -2
  8. package/dist/commands/search.js +16 -6
  9. package/dist/commands/setup.d.ts +1 -1
  10. package/dist/commands/setup.js +25 -25
  11. package/dist/commands/skip.d.ts +33 -0
  12. package/dist/commands/skip.js +89 -0
  13. package/dist/commands/validation.d.ts +1 -1
  14. package/dist/commands/validation.js +1 -1
  15. package/dist/commands/vet-list.d.ts +2 -2
  16. package/dist/commands/vet-list.js +12 -5
  17. package/dist/commands/vet.d.ts +3 -3
  18. package/dist/commands/vet.js +9 -5
  19. package/dist/core/bootstrap.d.ts +1 -1
  20. package/dist/core/bootstrap.js +20 -16
  21. package/dist/core/category-mapping.d.ts +1 -1
  22. package/dist/core/category-mapping.js +104 -13
  23. package/dist/core/errors.d.ts +8 -1
  24. package/dist/core/errors.js +31 -19
  25. package/dist/core/gist-state-store.d.ts +1 -1
  26. package/dist/core/gist-state-store.js +55 -28
  27. package/dist/core/github.d.ts +1 -1
  28. package/dist/core/github.js +5 -5
  29. package/dist/core/http-cache.js +26 -22
  30. package/dist/core/issue-discovery.d.ts +6 -6
  31. package/dist/core/issue-discovery.js +279 -286
  32. package/dist/core/issue-eligibility.d.ts +2 -2
  33. package/dist/core/issue-eligibility.js +26 -21
  34. package/dist/core/issue-filtering.js +23 -15
  35. package/dist/core/issue-scoring.js +1 -1
  36. package/dist/core/issue-vetting.d.ts +2 -4
  37. package/dist/core/issue-vetting.js +65 -56
  38. package/dist/core/local-state.d.ts +1 -1
  39. package/dist/core/local-state.js +16 -14
  40. package/dist/core/repo-health.d.ts +2 -2
  41. package/dist/core/repo-health.js +46 -35
  42. package/dist/core/schemas.d.ts +17 -9
  43. package/dist/core/schemas.js +47 -19
  44. package/dist/core/search-budget.js +3 -3
  45. package/dist/core/search-phases.d.ts +6 -6
  46. package/dist/core/search-phases.js +23 -19
  47. package/dist/core/types.d.ts +9 -9
  48. package/dist/core/types.js +15 -3
  49. package/dist/core/utils.d.ts +10 -1
  50. package/dist/core/utils.js +44 -25
  51. package/dist/formatters/json.d.ts +1 -1
  52. package/dist/index.d.ts +7 -7
  53. package/dist/index.js +5 -5
  54. package/dist/scout.d.ts +30 -6
  55. package/dist/scout.js +141 -34
  56. package/package.json +7 -3
@@ -1,41 +1,36 @@
1
1
  /**
2
2
  * Config command — view and update oss-scout preferences.
3
3
  */
4
- import { loadLocalState, saveLocalState } from '../core/local-state.js';
5
- import { ScoutPreferencesSchema, IssueScopeSchema, ProjectCategorySchema, PersistenceModeSchema } from '../core/schemas.js';
6
- import { ValidationError } from '../core/errors.js';
7
- /** All known preference keys and their types. */
8
- const ARRAY_FIELDS = new Set([
9
- 'languages',
10
- 'labels',
11
- 'preferredOrgs',
12
- 'projectCategories',
13
- 'excludeRepos',
14
- 'excludeOrgs',
15
- 'aiPolicyBlocklist',
16
- ]);
17
- const NUMBER_FIELDS = new Set(['minStars', 'maxIssueAgeDays', 'minRepoScoreThreshold']);
18
- const BOOLEAN_FIELDS = new Set(['includeDocIssues']);
19
- const STRING_FIELDS = new Set(['githubUsername']);
20
- const SCOPE_FIELD = 'scope';
21
- const ENUM_FIELDS = {
22
- persistence: PersistenceModeSchema.options,
4
+ import { loadLocalState, saveLocalState } from "../core/local-state.js";
5
+ import { ScoutPreferencesSchema, IssueScopeSchema, ProjectCategorySchema, PersistenceModeSchema, SearchStrategySchema, } from "../core/schemas.js";
6
+ import { ValidationError } from "../core/errors.js";
7
+ const FIELD_CONFIGS = {
8
+ languages: { type: "array" },
9
+ labels: { type: "array" },
10
+ excludeRepos: { type: "array" },
11
+ excludeOrgs: { type: "array" },
12
+ aiPolicyBlocklist: { type: "array" },
13
+ minStars: { type: "number" },
14
+ maxIssueAgeDays: { type: "number" },
15
+ minRepoScoreThreshold: { type: "number" },
16
+ includeDocIssues: { type: "boolean" },
17
+ scope: { type: "enum-array", validValues: IssueScopeSchema.options },
18
+ projectCategories: {
19
+ type: "enum-array",
20
+ validValues: ProjectCategorySchema.options,
21
+ },
22
+ persistence: { type: "enum", validValues: PersistenceModeSchema.options },
23
+ defaultStrategy: {
24
+ type: "enum-array",
25
+ validValues: SearchStrategySchema.options,
26
+ },
27
+ githubUsername: { type: "string" },
23
28
  };
24
- const ALL_FIELDS = new Set([
25
- ...ARRAY_FIELDS,
26
- ...NUMBER_FIELDS,
27
- ...BOOLEAN_FIELDS,
28
- ...STRING_FIELDS,
29
- ...Object.keys(ENUM_FIELDS),
30
- SCOPE_FIELD,
31
- ]);
32
- const VALID_SCOPES = IssueScopeSchema.options;
33
- const VALID_CATEGORIES = ProjectCategorySchema.options;
34
29
  function parseBoolean(value) {
35
30
  const lower = value.toLowerCase();
36
- if (lower === 'true' || lower === 'yes')
31
+ if (lower === "true" || lower === "yes")
37
32
  return true;
38
- if (lower === 'false' || lower === 'no')
33
+ if (lower === "false" || lower === "no")
39
34
  return false;
40
35
  throw new ValidationError(`Invalid boolean value: "${value}". Use true/false or yes/no.`);
41
36
  }
@@ -48,7 +43,7 @@ function parseNumber(value, key) {
48
43
  }
49
44
  function parseArrayValue(value) {
50
45
  return value
51
- .split(',')
46
+ .split(",")
52
47
  .map((s) => s.trim())
53
48
  .filter((s) => s.length > 0);
54
49
  }
@@ -56,7 +51,7 @@ function parseArrayValue(value) {
56
51
  * Apply an array update: plain set, +append, or -remove.
57
52
  */
58
53
  function updateArray(current, value) {
59
- if (value.startsWith('+')) {
54
+ if (value.startsWith("+")) {
60
55
  const toAdd = parseArrayValue(value.slice(1));
61
56
  const merged = [...current];
62
57
  for (const item of toAdd) {
@@ -65,39 +60,35 @@ function updateArray(current, value) {
65
60
  }
66
61
  return merged;
67
62
  }
68
- if (value.startsWith('-')) {
63
+ if (value.startsWith("-")) {
69
64
  const toRemove = new Set(parseArrayValue(value.slice(1)));
70
65
  return current.filter((item) => !toRemove.has(item));
71
66
  }
72
67
  return parseArrayValue(value);
73
68
  }
74
69
  function formatArray(arr) {
75
- return arr.length > 0 ? arr.join(', ') : '(none)';
70
+ return arr.length > 0 ? arr.join(", ") : "(none)";
76
71
  }
77
72
  /**
78
73
  * Display current preferences in human-readable format.
79
74
  */
80
- export function runConfigShow(options) {
75
+ export function runConfigShow() {
81
76
  const state = loadLocalState();
82
77
  const prefs = state.preferences;
83
- if (options.json) {
84
- // JSON output handled by caller
85
- return;
86
- }
87
- console.log('\n⚙️ oss-scout preferences\n');
88
- console.log(` githubUsername: ${prefs.githubUsername || '(not set)'}`);
78
+ console.log("\n⚙️ oss-scout preferences\n");
79
+ console.log(` githubUsername: ${prefs.githubUsername || "(not set)"}`);
89
80
  console.log(` languages: ${formatArray(prefs.languages)}`);
90
81
  console.log(` labels: ${formatArray(prefs.labels)}`);
91
- console.log(` scope: ${prefs.scope ? formatArray(prefs.scope) : '(all)'}`);
82
+ console.log(` scope: ${prefs.scope ? formatArray(prefs.scope) : "(all)"}`);
92
83
  console.log(` minStars: ${prefs.minStars}`);
93
84
  console.log(` maxIssueAgeDays: ${prefs.maxIssueAgeDays}`);
94
85
  console.log(` minRepoScoreThreshold: ${prefs.minRepoScoreThreshold}`);
95
86
  console.log(` includeDocIssues: ${prefs.includeDocIssues}`);
96
- console.log(` preferredOrgs: ${formatArray(prefs.preferredOrgs)}`);
97
87
  console.log(` projectCategories: ${formatArray(prefs.projectCategories)}`);
98
88
  console.log(` excludeRepos: ${formatArray(prefs.excludeRepos)}`);
99
89
  console.log(` excludeOrgs: ${formatArray(prefs.excludeOrgs)}`);
100
90
  console.log(` aiPolicyBlocklist: ${formatArray(prefs.aiPolicyBlocklist)}`);
91
+ console.log(` defaultStrategy: ${prefs.defaultStrategy ? formatArray(prefs.defaultStrategy) : "(all)"}`);
101
92
  console.log(` persistence: ${prefs.persistence}`);
102
93
  console.log();
103
94
  }
@@ -112,46 +103,53 @@ export function getConfigData() {
112
103
  * Update a single preference by key.
113
104
  */
114
105
  export function runConfigSet(key, value) {
115
- if (!ALL_FIELDS.has(key)) {
116
- throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${[...ALL_FIELDS].sort().join(', ')}`);
106
+ const field = FIELD_CONFIGS[key];
107
+ if (!field) {
108
+ throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${Object.keys(FIELD_CONFIGS).sort().join(", ")}`);
117
109
  }
118
110
  const state = loadLocalState();
119
111
  const prefs = { ...state.preferences };
120
- if (STRING_FIELDS.has(key)) {
121
- prefs[key] = value;
122
- }
123
- else if (BOOLEAN_FIELDS.has(key)) {
124
- prefs[key] = parseBoolean(value);
125
- }
126
- else if (NUMBER_FIELDS.has(key)) {
127
- prefs[key] = parseNumber(value, key);
128
- }
129
- else if (key === SCOPE_FIELD) {
130
- const updated = updateArray(prefs.scope ?? [], value);
131
- const invalid = updated.filter((s) => !VALID_SCOPES.includes(s));
132
- if (invalid.length > 0) {
133
- throw new ValidationError(`Invalid scope value(s): ${invalid.join(', ')}. Valid: ${VALID_SCOPES.join(', ')}`);
112
+ switch (field.type) {
113
+ case "string":
114
+ prefs[key] = value;
115
+ break;
116
+ case "boolean":
117
+ prefs[key] = parseBoolean(value);
118
+ break;
119
+ case "number":
120
+ prefs[key] = parseNumber(value, key);
121
+ break;
122
+ case "array": {
123
+ const current = prefs[key] ?? [];
124
+ prefs[key] = updateArray(current, value);
125
+ break;
134
126
  }
135
- prefs.scope = updated.length > 0 ? updated : undefined;
136
- }
137
- else if (key === 'projectCategories') {
138
- const updated = updateArray(prefs.projectCategories, value);
139
- const invalid = updated.filter((s) => !VALID_CATEGORIES.includes(s));
140
- if (invalid.length > 0) {
141
- throw new ValidationError(`Invalid category value(s): ${invalid.join(', ')}. Valid: ${VALID_CATEGORIES.join(', ')}`);
127
+ case "enum": {
128
+ const validValues = field.validValues;
129
+ if (!validValues.includes(value)) {
130
+ throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(", ")}`);
131
+ }
132
+ prefs[key] = value;
133
+ break;
142
134
  }
143
- prefs.projectCategories = updated;
144
- }
145
- else if (key in ENUM_FIELDS) {
146
- const validValues = ENUM_FIELDS[key];
147
- if (!validValues.includes(value)) {
148
- throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(', ')}`);
135
+ case "enum-array": {
136
+ const current = prefs[key] ?? [];
137
+ const updated = updateArray(current, value);
138
+ const validValues = field.validValues;
139
+ const invalid = updated.filter((s) => !validValues.includes(s));
140
+ if (invalid.length > 0) {
141
+ throw new ValidationError(`Invalid value(s) for "${key}": ${invalid.join(", ")}. Valid: ${validValues.join(", ")}`);
142
+ }
143
+ // For 'scope', empty array means undefined (all scopes)
144
+ if (key === "scope") {
145
+ prefs[key] =
146
+ updated.length > 0 ? updated : undefined;
147
+ }
148
+ else {
149
+ prefs[key] = updated;
150
+ }
151
+ break;
149
152
  }
150
- prefs[key] = value;
151
- }
152
- else if (ARRAY_FIELDS.has(key)) {
153
- const current = prefs[key] ?? [];
154
- prefs[key] = updateArray(current, value);
155
153
  }
156
154
  // Validate the full preferences object
157
155
  const validated = ScoutPreferencesSchema.parse(prefs);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Results command — display and manage saved search results.
3
3
  */
4
- import type { SavedCandidate } from '../core/schemas.js';
4
+ import type { SavedCandidate } from "../core/schemas.js";
5
5
  export declare function runResults(_options: {
6
6
  json?: boolean;
7
7
  }): Promise<SavedCandidate[]>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Results command — display and manage saved search results.
3
3
  */
4
- import { loadLocalState, saveLocalState } from '../core/local-state.js';
4
+ import { loadLocalState, saveLocalState } from "../core/local-state.js";
5
5
  export async function runResults(_options) {
6
6
  const state = loadLocalState();
7
7
  return state.savedResults ?? [];
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Search command — finds contributable issues using multi-strategy search.
3
3
  */
4
- import type { ScoutState, SearchStrategy } from '../core/schemas.js';
4
+ import type { ScoutState, SearchStrategy } from "../core/schemas.js";
5
5
  export interface SearchOutput {
6
6
  candidates: Array<{
7
7
  issue: {
@@ -12,7 +12,7 @@ export interface SearchOutput {
12
12
  url: string;
13
13
  labels: string[];
14
14
  };
15
- recommendation: 'approve' | 'skip' | 'needs_review';
15
+ recommendation: "approve" | "skip" | "needs_review";
16
16
  reasonsToApprove: string[];
17
17
  reasonsToSkip: string[];
18
18
  searchPriority: string;
@@ -1,19 +1,29 @@
1
1
  /**
2
2
  * Search command — finds contributable issues using multi-strategy search.
3
3
  */
4
- import { createScout } from '../scout.js';
5
- import { requireGitHubToken } from '../core/utils.js';
6
- import { saveLocalState } from '../core/local-state.js';
4
+ import { createScout } from "../scout.js";
5
+ import { requireGitHubToken } from "../core/utils.js";
6
+ import { saveLocalState } from "../core/local-state.js";
7
7
  export async function runSearch(options) {
8
8
  const token = requireGitHubToken();
9
9
  const scout = options.state
10
- ? await createScout({ githubToken: token, persistence: 'provided', initialState: options.state })
10
+ ? await createScout({
11
+ githubToken: token,
12
+ persistence: "provided",
13
+ initialState: options.state,
14
+ })
11
15
  : await createScout({ githubToken: token });
12
- const result = await scout.search({ maxResults: options.maxResults, strategies: options.strategies });
16
+ const result = await scout.search({
17
+ maxResults: options.maxResults,
18
+ strategies: options.strategies,
19
+ });
13
20
  // Persist results to local state and gist
14
21
  scout.saveResults(result.candidates);
15
22
  saveLocalState(scout.getState());
16
- await scout.checkpoint();
23
+ const persisted = await scout.checkpoint();
24
+ if (!persisted) {
25
+ console.error("Warning: changes saved locally but gist sync failed.");
26
+ }
17
27
  return {
18
28
  candidates: result.candidates.map((c) => {
19
29
  const repoScoreRecord = scout.getRepoScoreRecord(c.issue.repo);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Setup command — interactive first-run configuration for oss-scout.
3
3
  */
4
- import type { ScoutPreferences } from '../core/schemas.js';
4
+ import type { ScoutPreferences } from "../core/schemas.js";
5
5
  interface ReadlineInterface {
6
6
  question(query: string, callback: (answer: string) => void): void;
7
7
  close(): void;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Setup command — interactive first-run configuration for oss-scout.
3
3
  */
4
- import * as readline from 'readline';
5
- import { execFile } from 'child_process';
6
- import { ScoutPreferencesSchema, ProjectCategorySchema, IssueScopeSchema } from '../core/schemas.js';
4
+ import * as readline from "readline";
5
+ import { execFile } from "child_process";
6
+ import { ScoutPreferencesSchema, ProjectCategorySchema, IssueScopeSchema, } from "../core/schemas.js";
7
7
  const ALL_CATEGORIES = ProjectCategorySchema.options;
8
8
  const ALL_SCOPES = IssueScopeSchema.options;
9
9
  function createReadlineInterface() {
@@ -19,9 +19,9 @@ function ask(rl, query) {
19
19
  }
20
20
  function detectGitHubUsername() {
21
21
  return new Promise((resolve) => {
22
- execFile('gh', ['api', 'user', '--jq', '.login'], { timeout: 5000 }, (err, stdout) => {
22
+ execFile("gh", ["api", "user", "--jq", ".login"], { timeout: 5000 }, (err, stdout) => {
23
23
  if (err || !stdout.trim()) {
24
- resolve('');
24
+ resolve("");
25
25
  }
26
26
  else {
27
27
  resolve(stdout.trim());
@@ -31,7 +31,7 @@ function detectGitHubUsername() {
31
31
  }
32
32
  function parseCSV(input) {
33
33
  return input
34
- .split(',')
34
+ .split(",")
35
35
  .map((s) => s.trim())
36
36
  .filter((s) => s.length > 0);
37
37
  }
@@ -48,40 +48,41 @@ export async function runSetup(options) {
48
48
  const rl = options?.rl ?? createReadlineInterface();
49
49
  const detect = options?.detectUsername ?? detectGitHubUsername;
50
50
  try {
51
- console.log('\n🔧 oss-scout setup\n');
51
+ console.log("\n🔧 oss-scout setup\n");
52
52
  // Detect GitHub username
53
- console.log('Detecting GitHub username...');
53
+ console.log("Detecting GitHub username...");
54
54
  const detectedUsername = await detect();
55
- const usernameDefault = detectedUsername || '';
55
+ const usernameDefault = detectedUsername || "";
56
56
  const usernamePrompt = detectedUsername
57
57
  ? `GitHub username [${detectedUsername}]: `
58
- : 'GitHub username: ';
58
+ : "GitHub username: ";
59
59
  const usernameInput = await ask(rl, usernamePrompt);
60
60
  const githubUsername = usernameInput || usernameDefault;
61
61
  // Languages
62
- const defaultLangs = 'typescript, javascript';
63
- const langsInput = await ask(rl, `Preferred languages [${defaultLangs}]: `);
64
- const languages = langsInput ? parseCSV(langsInput) : ['typescript', 'javascript'];
62
+ const defaultLangs = "any (all languages)";
63
+ const langsInput = await ask(rl, `Preferred languages (comma-separated, or "any" for all) [${defaultLangs}]: `);
64
+ const languages = langsInput ? parseCSV(langsInput) : ["any"];
65
65
  // Issue labels
66
- const defaultLabels = 'good first issue, help wanted';
66
+ const defaultLabels = "good first issue, help wanted";
67
67
  const labelsInput = await ask(rl, `Issue labels to search for [${defaultLabels}]: `);
68
- const labels = labelsInput ? parseCSV(labelsInput) : ['good first issue', 'help wanted'];
68
+ const labels = labelsInput
69
+ ? parseCSV(labelsInput)
70
+ : ["good first issue", "help wanted"];
69
71
  // Difficulty scope
70
- const scopeOptions = ALL_SCOPES.join(', ');
72
+ const scopeOptions = ALL_SCOPES.join(", ");
71
73
  const scopeInput = await ask(rl, `Difficulty scope (${scopeOptions}) [all]: `);
72
- const scope = scopeInput ? parseMultiSelect(scopeInput, ALL_SCOPES) : [...ALL_SCOPES];
74
+ const scope = scopeInput
75
+ ? parseMultiSelect(scopeInput, ALL_SCOPES)
76
+ : [...ALL_SCOPES];
73
77
  // Minimum stars
74
- const minStarsInput = await ask(rl, 'Minimum repo stars [50]: ');
78
+ const minStarsInput = await ask(rl, "Minimum repo stars [50]: ");
75
79
  const minStars = minStarsInput ? parseInt(minStarsInput, 10) : 50;
76
- // Preferred organizations
77
- const orgsInput = await ask(rl, 'Preferred organizations (comma-separated, optional): ');
78
- const preferredOrgs = parseCSV(orgsInput);
79
80
  // Project categories
80
- const categoryOptions = ALL_CATEGORIES.join(', ');
81
+ const categoryOptions = ALL_CATEGORIES.join(", ");
81
82
  const categoriesInput = await ask(rl, `Project categories (${categoryOptions}) [none]: `);
82
83
  const projectCategories = parseMultiSelect(categoriesInput, ALL_CATEGORIES);
83
84
  // Repos to exclude
84
- const excludeInput = await ask(rl, 'Repos to exclude (owner/repo, comma-separated, optional): ');
85
+ const excludeInput = await ask(rl, "Repos to exclude (owner/repo, comma-separated, optional): ");
85
86
  const excludeRepos = parseCSV(excludeInput);
86
87
  const prefs = ScoutPreferencesSchema.parse({
87
88
  githubUsername,
@@ -89,11 +90,10 @@ export async function runSetup(options) {
89
90
  labels,
90
91
  scope: scope.length > 0 ? scope : undefined,
91
92
  excludeRepos,
92
- preferredOrgs,
93
93
  projectCategories,
94
94
  minStars: isNaN(minStars) ? 50 : minStars,
95
95
  });
96
- console.log('\n✅ Setup complete! Preferences saved.\n');
96
+ console.log("\n✅ Setup complete! Preferences saved.\n");
97
97
  return prefs;
98
98
  }
99
99
  finally {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Skip command — manage the skip list for excluding issues from future searches.
3
+ */
4
+ import type { SkippedIssue, ScoutState } from "../core/schemas.js";
5
+ /**
6
+ * Skip an issue by URL — adds it to the skip list and removes it from saved results.
7
+ * Tries to enrich metadata from saved results if available.
8
+ */
9
+ export declare function runSkip(options: {
10
+ issueUrl: string;
11
+ state?: ScoutState;
12
+ }): Promise<{
13
+ skipped: boolean;
14
+ alreadySkipped: boolean;
15
+ }>;
16
+ /**
17
+ * List all skipped issues.
18
+ */
19
+ export declare function runSkipList(options?: {
20
+ state?: ScoutState;
21
+ }): SkippedIssue[];
22
+ /**
23
+ * Clear all skipped issues.
24
+ */
25
+ export declare function runSkipClear(): Promise<void>;
26
+ /**
27
+ * Remove a specific issue from the skip list (unskip).
28
+ */
29
+ export declare function runSkipRemove(options: {
30
+ issueUrl: string;
31
+ }): Promise<{
32
+ removed: boolean;
33
+ }>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Skip command — manage the skip list for excluding issues from future searches.
3
+ */
4
+ import { loadLocalState, saveLocalState } from "../core/local-state.js";
5
+ import { createScout } from "../scout.js";
6
+ import { getGitHubToken } from "../core/utils.js";
7
+ /**
8
+ * Create an OssScout instance for skip operations.
9
+ * Uses gist persistence when a token is available, otherwise provided-state mode.
10
+ */
11
+ async function createSkipScout(state) {
12
+ const token = getGitHubToken() ?? "";
13
+ if (token) {
14
+ return createScout({
15
+ githubToken: token,
16
+ persistence: "provided",
17
+ initialState: state,
18
+ });
19
+ }
20
+ return createScout({
21
+ githubToken: "",
22
+ persistence: "provided",
23
+ initialState: state,
24
+ });
25
+ }
26
+ /**
27
+ * Skip an issue by URL — adds it to the skip list and removes it from saved results.
28
+ * Tries to enrich metadata from saved results if available.
29
+ */
30
+ export async function runSkip(options) {
31
+ const state = options.state ?? loadLocalState();
32
+ const scout = await createSkipScout(state);
33
+ const alreadySkipped = scout
34
+ .getSkippedIssues()
35
+ .some((s) => s.url === options.issueUrl);
36
+ if (alreadySkipped) {
37
+ return { skipped: false, alreadySkipped: true };
38
+ }
39
+ // Try to enrich metadata from saved results
40
+ const saved = scout
41
+ .getSavedResults()
42
+ .find((r) => r.issueUrl === options.issueUrl);
43
+ const metadata = saved
44
+ ? { repo: saved.repo, number: saved.number, title: saved.title }
45
+ : parseIssueUrl(options.issueUrl);
46
+ scout.skipIssue(options.issueUrl, metadata);
47
+ saveLocalState(scout.getState());
48
+ await scout.checkpoint();
49
+ return { skipped: true, alreadySkipped: false };
50
+ }
51
+ /**
52
+ * List all skipped issues.
53
+ */
54
+ export function runSkipList(options) {
55
+ const state = options?.state ?? loadLocalState();
56
+ return state.skippedIssues ?? [];
57
+ }
58
+ /**
59
+ * Clear all skipped issues.
60
+ */
61
+ export async function runSkipClear() {
62
+ const state = loadLocalState();
63
+ const scout = await createSkipScout(state);
64
+ scout.clearSkippedIssues();
65
+ saveLocalState(scout.getState());
66
+ await scout.checkpoint();
67
+ }
68
+ /**
69
+ * Remove a specific issue from the skip list (unskip).
70
+ */
71
+ export async function runSkipRemove(options) {
72
+ const state = loadLocalState();
73
+ const scout = await createSkipScout(state);
74
+ const before = scout.getSkippedIssues().length;
75
+ scout.unskipIssue(options.issueUrl);
76
+ const removed = before !== scout.getSkippedIssues().length;
77
+ saveLocalState(scout.getState());
78
+ await scout.checkpoint();
79
+ return { removed };
80
+ }
81
+ /**
82
+ * Parse a GitHub issue URL to extract repo and number.
83
+ */
84
+ function parseIssueUrl(url) {
85
+ const match = url.match(/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)/);
86
+ if (!match)
87
+ return undefined;
88
+ return { repo: match[1], number: parseInt(match[2], 10), title: "" };
89
+ }
@@ -2,5 +2,5 @@
2
2
  * Shared validation patterns and helpers for CLI commands.
3
3
  */
4
4
  export declare const ISSUE_URL_PATTERN: RegExp;
5
- export declare function validateGitHubUrl(url: string, pattern: RegExp, entityType: 'issue'): void;
5
+ export declare function validateGitHubUrl(url: string, pattern: RegExp, entityType: "issue"): void;
6
6
  export declare function validateUrl(url: string): string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared validation patterns and helpers for CLI commands.
3
3
  */
4
- import { ValidationError } from '../core/errors.js';
4
+ import { ValidationError } from "../core/errors.js";
5
5
  export const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
6
6
  const MAX_URL_LENGTH = 2048;
7
7
  export function validateGitHubUrl(url, pattern, entityType) {
@@ -1,5 +1,5 @@
1
- import type { ScoutState } from '../core/schemas.js';
2
- import type { VetListResult } from '../core/types.js';
1
+ import type { ScoutState } from "../core/schemas.js";
2
+ import type { VetListResult } from "../core/types.js";
3
3
  interface VetListCommandOptions {
4
4
  concurrency?: number;
5
5
  prune?: boolean;
@@ -1,16 +1,23 @@
1
- import { createScout } from '../scout.js';
2
- import { requireGitHubToken } from '../core/utils.js';
3
- import { saveLocalState } from '../core/local-state.js';
1
+ import { createScout } from "../scout.js";
2
+ import { requireGitHubToken } from "../core/utils.js";
3
+ import { saveLocalState } from "../core/local-state.js";
4
4
  export async function runVetList(options) {
5
5
  const token = requireGitHubToken();
6
6
  const scout = options.state
7
- ? await createScout({ githubToken: token, persistence: 'provided', initialState: options.state })
7
+ ? await createScout({
8
+ githubToken: token,
9
+ persistence: "provided",
10
+ initialState: options.state,
11
+ })
8
12
  : await createScout({ githubToken: token });
9
13
  const result = await scout.vetList({
10
14
  concurrency: options.concurrency,
11
15
  prune: options.prune,
12
16
  });
13
17
  saveLocalState(scout.getState());
14
- await scout.checkpoint();
18
+ const persisted = await scout.checkpoint();
19
+ if (!persisted) {
20
+ console.error("Warning: changes saved locally but gist sync failed.");
21
+ }
15
22
  return result;
16
23
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Vet command — vets a specific issue for claimability.
3
3
  */
4
- import type { ProjectHealth } from '../core/types.js';
5
- import type { IssueVettingResult, ScoutState } from '../core/schemas.js';
4
+ import type { ProjectHealth } from "../core/types.js";
5
+ import type { IssueVettingResult, ScoutState } from "../core/schemas.js";
6
6
  export interface VetOutput {
7
7
  issue: {
8
8
  repo: string;
@@ -11,7 +11,7 @@ export interface VetOutput {
11
11
  url: string;
12
12
  labels: string[];
13
13
  };
14
- recommendation: 'approve' | 'skip' | 'needs_review';
14
+ recommendation: "approve" | "skip" | "needs_review";
15
15
  reasonsToApprove: string[];
16
16
  reasonsToSkip: string[];
17
17
  projectHealth: ProjectHealth;
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * Vet command — vets a specific issue for claimability.
3
3
  */
4
- import { createScout } from '../scout.js';
5
- import { requireGitHubToken } from '../core/utils.js';
6
- import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
4
+ import { createScout } from "../scout.js";
5
+ import { requireGitHubToken } from "../core/utils.js";
6
+ import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl, } from "./validation.js";
7
7
  export async function runVet(options) {
8
8
  validateUrl(options.issueUrl);
9
- validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
9
+ validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
10
10
  const token = requireGitHubToken();
11
11
  const scout = options.state
12
- ? await createScout({ githubToken: token, persistence: 'provided', initialState: options.state })
12
+ ? await createScout({
13
+ githubToken: token,
14
+ persistence: "provided",
15
+ initialState: options.state,
16
+ })
13
17
  : await createScout({ githubToken: token });
14
18
  const candidate = await scout.vetIssue(options.issueUrl);
15
19
  return {
@@ -2,7 +2,7 @@
2
2
  * First-run bootstrap — fetches starred repos and PR history from GitHub
3
3
  * to seed the scout's state with the user's contribution context.
4
4
  */
5
- import type { OssScout } from '../scout.js';
5
+ import type { OssScout } from "../scout.js";
6
6
  export interface BootstrapResult {
7
7
  starredRepoCount: number;
8
8
  mergedPRCount: number;