@oss-scout/core 0.1.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 (66) hide show
  1. package/dist/cli.bundle.cjs +114 -0
  2. package/dist/cli.d.ts +5 -0
  3. package/dist/cli.js +341 -0
  4. package/dist/commands/config.d.ts +22 -0
  5. package/dist/commands/config.js +169 -0
  6. package/dist/commands/results.d.ts +8 -0
  7. package/dist/commands/results.js +13 -0
  8. package/dist/commands/search.d.ts +39 -0
  9. package/dist/commands/search.js +50 -0
  10. package/dist/commands/setup.d.ts +17 -0
  11. package/dist/commands/setup.js +104 -0
  12. package/dist/commands/validation.d.ts +6 -0
  13. package/dist/commands/validation.js +17 -0
  14. package/dist/commands/vet-list.d.ts +9 -0
  15. package/dist/commands/vet-list.js +16 -0
  16. package/dist/commands/vet.d.ts +25 -0
  17. package/dist/commands/vet.js +29 -0
  18. package/dist/core/bootstrap.d.ts +14 -0
  19. package/dist/core/bootstrap.js +122 -0
  20. package/dist/core/category-mapping.d.ts +19 -0
  21. package/dist/core/category-mapping.js +58 -0
  22. package/dist/core/concurrency.d.ts +6 -0
  23. package/dist/core/concurrency.js +25 -0
  24. package/dist/core/errors.d.ts +22 -0
  25. package/dist/core/errors.js +69 -0
  26. package/dist/core/gist-state-store.d.ts +96 -0
  27. package/dist/core/gist-state-store.js +302 -0
  28. package/dist/core/github.d.ts +16 -0
  29. package/dist/core/github.js +58 -0
  30. package/dist/core/http-cache.d.ts +108 -0
  31. package/dist/core/http-cache.js +314 -0
  32. package/dist/core/issue-discovery.d.ts +93 -0
  33. package/dist/core/issue-discovery.js +475 -0
  34. package/dist/core/issue-eligibility.d.ts +33 -0
  35. package/dist/core/issue-eligibility.js +151 -0
  36. package/dist/core/issue-filtering.d.ts +51 -0
  37. package/dist/core/issue-filtering.js +103 -0
  38. package/dist/core/issue-scoring.d.ts +43 -0
  39. package/dist/core/issue-scoring.js +97 -0
  40. package/dist/core/issue-vetting.d.ts +44 -0
  41. package/dist/core/issue-vetting.js +270 -0
  42. package/dist/core/local-state.d.ts +16 -0
  43. package/dist/core/local-state.js +56 -0
  44. package/dist/core/logger.d.ts +11 -0
  45. package/dist/core/logger.js +25 -0
  46. package/dist/core/pagination.d.ts +7 -0
  47. package/dist/core/pagination.js +16 -0
  48. package/dist/core/repo-health.d.ts +19 -0
  49. package/dist/core/repo-health.js +179 -0
  50. package/dist/core/schemas.d.ts +315 -0
  51. package/dist/core/schemas.js +137 -0
  52. package/dist/core/search-budget.d.ts +62 -0
  53. package/dist/core/search-budget.js +129 -0
  54. package/dist/core/search-phases.d.ts +69 -0
  55. package/dist/core/search-phases.js +238 -0
  56. package/dist/core/types.d.ts +124 -0
  57. package/dist/core/types.js +9 -0
  58. package/dist/core/utils.d.ts +18 -0
  59. package/dist/core/utils.js +106 -0
  60. package/dist/formatters/json.d.ts +6 -0
  61. package/dist/formatters/json.js +20 -0
  62. package/dist/index.d.ts +23 -0
  63. package/dist/index.js +25 -0
  64. package/dist/scout.d.ts +125 -0
  65. package/dist/scout.js +391 -0
  66. package/package.json +70 -0
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * oss-scout CLI — Find open source issues personalized to your contribution history.
4
+ */
5
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * oss-scout CLI — Find open source issues personalized to your contribution history.
4
+ */
5
+ import { Command } from 'commander';
6
+ import { enableDebug } from './core/logger.js';
7
+ import { getCLIVersion } from './core/utils.js';
8
+ import { formatJsonSuccess, formatJsonError } from './formatters/json.js';
9
+ import { errorMessage, resolveErrorCode } from './core/errors.js';
10
+ import { hasLocalState, loadLocalState, saveLocalState } from './core/local-state.js';
11
+ import { CONCRETE_STRATEGIES, SearchStrategySchema } from './core/schemas.js';
12
+ function handleCommandError(err, options) {
13
+ if (options.json) {
14
+ console.log(formatJsonError(errorMessage(err), resolveErrorCode(err)));
15
+ }
16
+ else {
17
+ console.error('Error:', errorMessage(err));
18
+ }
19
+ process.exit(1);
20
+ }
21
+ const program = new Command();
22
+ program
23
+ .name('oss-scout')
24
+ .description('Find open source issues personalized to your contribution history')
25
+ .version(getCLIVersion())
26
+ .option('--debug', 'Enable debug output');
27
+ // Parse --debug early so it's available in preAction hooks
28
+ program.hook('preAction', (_thisCommand, _actionCommand) => {
29
+ const opts = program.opts();
30
+ if (opts.debug)
31
+ enableDebug();
32
+ });
33
+ program
34
+ .command('setup')
35
+ .description('Interactive first-run configuration')
36
+ .option('--json', 'Output as JSON')
37
+ .action(async (options) => {
38
+ try {
39
+ const { runSetup } = await import('./commands/setup.js');
40
+ const prefs = await runSetup();
41
+ const state = loadLocalState();
42
+ state.preferences = prefs;
43
+ saveLocalState(state);
44
+ if (options.json) {
45
+ console.log(formatJsonSuccess(prefs));
46
+ }
47
+ }
48
+ catch (err) {
49
+ handleCommandError(err, options);
50
+ }
51
+ });
52
+ program
53
+ .command('bootstrap')
54
+ .description('Import starred repos and PR history from GitHub')
55
+ .option('--json', 'Output as JSON')
56
+ .action(async (options) => {
57
+ try {
58
+ const { bootstrapScout } = await import('./core/bootstrap.js');
59
+ const { createScout } = await import('./scout.js');
60
+ const { requireGitHubToken } = await import('./core/utils.js');
61
+ const token = requireGitHubToken();
62
+ const state = loadLocalState();
63
+ const scout = await createScout({ githubToken: token, persistence: 'provided', initialState: state });
64
+ const result = await bootstrapScout(scout, token);
65
+ saveLocalState(scout.getState());
66
+ if (options.json) {
67
+ console.log(formatJsonSuccess(result));
68
+ }
69
+ else {
70
+ if (result.skippedDueToRateLimit) {
71
+ console.log('Skipped: GitHub API rate limit too low. Try again later.');
72
+ }
73
+ else {
74
+ console.log(`Imported ${result.mergedPRCount} merged PRs, ${result.closedPRCount} closed PRs, ${result.starredRepoCount} starred repos`);
75
+ console.log(`Scored ${result.reposScoredCount} repositories`);
76
+ }
77
+ }
78
+ }
79
+ catch (err) {
80
+ handleCommandError(err, options);
81
+ }
82
+ });
83
+ program
84
+ .command('search [count]')
85
+ .description('Search for contributable issues using multi-strategy discovery')
86
+ .option('--json', 'Output as JSON')
87
+ .option('--strategy <strategies>', `Search strategies (${CONCRETE_STRATEGIES.join(',')},all)`, 'all')
88
+ .action(async (count, options) => {
89
+ try {
90
+ if (!hasLocalState()) {
91
+ console.log('💡 Run `oss-scout setup` to configure your preferences for personalized search results.\n');
92
+ }
93
+ const { runSearch } = await import('./commands/search.js');
94
+ const maxResults = count ? parseInt(count, 10) : 10;
95
+ if (isNaN(maxResults) || maxResults < 1) {
96
+ console.error('Error: count must be a positive integer');
97
+ process.exit(1);
98
+ }
99
+ const state = loadLocalState();
100
+ if (state.mergedPRs.length === 0 &&
101
+ state.starredRepos.length === 0 &&
102
+ state.preferences.githubUsername) {
103
+ console.log('Run `oss-scout bootstrap` to import your starred repos and PR history for better results.\n');
104
+ }
105
+ // Parse --strategy option
106
+ const strategyTokens = (options.strategy ?? 'all').split(',').map(s => s.trim()).filter(Boolean);
107
+ const strategies = [];
108
+ for (const token of strategyTokens) {
109
+ const parsed = SearchStrategySchema.safeParse(token);
110
+ if (!parsed.success) {
111
+ const valid = [...CONCRETE_STRATEGIES, 'all'].join(', ');
112
+ console.error('Error: unknown strategy "' + token + '". Valid strategies: ' + valid);
113
+ process.exit(1);
114
+ }
115
+ strategies.push(parsed.data);
116
+ }
117
+ const results = await runSearch({ maxResults, state, strategies });
118
+ if (options.json) {
119
+ console.log(formatJsonSuccess(results));
120
+ }
121
+ else {
122
+ // Human-readable output
123
+ console.log(`\nFound ${results.candidates.length} issue candidates:\n`);
124
+ for (const c of results.candidates) {
125
+ const icon = c.recommendation === 'approve' ? '✅' : c.recommendation === 'skip' ? '❌' : '⚠️';
126
+ console.log(` ${icon} ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100]`);
127
+ console.log(` ${c.issue.title}`);
128
+ console.log(` ${c.issue.url}`);
129
+ if (c.repoScore) {
130
+ console.log(` Repo: ${c.repoScore.score}/10, ${c.repoScore.mergedPRCount} merged PRs`);
131
+ }
132
+ console.log();
133
+ }
134
+ if (results.rateLimitWarning) {
135
+ console.error(`\n⚠️ ${results.rateLimitWarning}`);
136
+ }
137
+ }
138
+ }
139
+ catch (err) {
140
+ handleCommandError(err, options);
141
+ }
142
+ });
143
+ // ── results command ────────────────────────────────────────────────
144
+ const resultsCmd = program
145
+ .command('results')
146
+ .description('Show saved search results');
147
+ resultsCmd
148
+ .command('show', { isDefault: true })
149
+ .description('Display saved search results')
150
+ .option('--json', 'Output as JSON')
151
+ .action(async (options) => {
152
+ try {
153
+ const { runResults } = await import('./commands/results.js');
154
+ const results = await runResults(options);
155
+ if (options.json) {
156
+ console.log(formatJsonSuccess(results));
157
+ }
158
+ else {
159
+ if (results.length === 0) {
160
+ console.log('\nNo saved results. Run `oss-scout search` to find issues.\n');
161
+ return;
162
+ }
163
+ console.log(`\nSaved results (${results.length}):\n`);
164
+ console.log(' Score Repo Issue Recommendation Title');
165
+ console.log(' ───── ──────────────────────────────── ────── ────────────── ─────');
166
+ for (const r of results) {
167
+ const score = String(r.viabilityScore).padStart(3);
168
+ const repo = r.repo.padEnd(32).slice(0, 32);
169
+ const issue = `#${r.number}`.padEnd(6);
170
+ const rec = r.recommendation.padEnd(14);
171
+ const title = r.title.length > 50 ? r.title.slice(0, 47) + '...' : r.title;
172
+ console.log(` ${score} ${repo} ${issue} ${rec} ${title}`);
173
+ }
174
+ console.log();
175
+ }
176
+ }
177
+ catch (err) {
178
+ handleCommandError(err, options);
179
+ }
180
+ });
181
+ resultsCmd
182
+ .command('clear')
183
+ .description('Clear all saved results')
184
+ .option('--json', 'Output as JSON')
185
+ .action(async (options) => {
186
+ try {
187
+ const { runResultsClear } = await import('./commands/results.js');
188
+ await runResultsClear();
189
+ if (options.json) {
190
+ console.log(formatJsonSuccess({ cleared: true }));
191
+ }
192
+ else {
193
+ console.log('Saved results cleared.');
194
+ }
195
+ }
196
+ catch (err) {
197
+ handleCommandError(err, options);
198
+ }
199
+ });
200
+ // ── config command ──────────────────────────────────────────────────
201
+ const configCmd = program
202
+ .command('config')
203
+ .description('View and update preferences')
204
+ .option('--json', 'Output as JSON')
205
+ .action(async (options) => {
206
+ try {
207
+ const { runConfigShow, getConfigData } = await import('./commands/config.js');
208
+ if (options.json) {
209
+ console.log(formatJsonSuccess(getConfigData()));
210
+ }
211
+ else {
212
+ runConfigShow(options);
213
+ }
214
+ }
215
+ catch (err) {
216
+ handleCommandError(err, options);
217
+ }
218
+ });
219
+ configCmd
220
+ .command('set <key> <value>')
221
+ .description('Update a single preference (e.g. config set minStars 100)')
222
+ .option('--json', 'Output as JSON')
223
+ .action(async (key, value, options) => {
224
+ try {
225
+ const { runConfigSet } = await import('./commands/config.js');
226
+ const updated = runConfigSet(key, value);
227
+ if (options.json) {
228
+ console.log(formatJsonSuccess(updated));
229
+ }
230
+ else {
231
+ console.log(`✅ Updated "${key}" successfully.`);
232
+ }
233
+ }
234
+ catch (err) {
235
+ handleCommandError(err, options);
236
+ }
237
+ });
238
+ configCmd
239
+ .command('reset')
240
+ .description('Reset all preferences to defaults')
241
+ .option('--json', 'Output as JSON')
242
+ .action(async (options) => {
243
+ try {
244
+ const { runConfigReset } = await import('./commands/config.js');
245
+ const defaults = runConfigReset();
246
+ if (options.json) {
247
+ console.log(formatJsonSuccess(defaults));
248
+ }
249
+ else {
250
+ console.log('✅ Preferences reset to defaults.');
251
+ }
252
+ }
253
+ catch (err) {
254
+ handleCommandError(err, options);
255
+ }
256
+ });
257
+ program
258
+ .command('vet-list')
259
+ .description('Re-vet all saved search results and classify their current status')
260
+ .option('--prune', 'Remove unavailable issues from saved results')
261
+ .option('--concurrency <n>', 'Max concurrent API requests (default: 5)', parseInt)
262
+ .option('--json', 'Output as JSON')
263
+ .action(async (options) => {
264
+ try {
265
+ if (options.concurrency !== undefined && (isNaN(options.concurrency) || options.concurrency < 1)) {
266
+ console.error('Error: --concurrency must be a positive integer');
267
+ process.exit(1);
268
+ }
269
+ const { runVetList } = await import('./commands/vet-list.js');
270
+ const state = loadLocalState();
271
+ const result = await runVetList({
272
+ state,
273
+ prune: options.prune,
274
+ concurrency: options.concurrency,
275
+ });
276
+ if (options.json) {
277
+ console.log(formatJsonSuccess(result));
278
+ }
279
+ else {
280
+ if (result.results.length === 0) {
281
+ console.log('\nNo saved results to vet. Run `oss-scout search` first.\n');
282
+ return;
283
+ }
284
+ console.log(`\nVet-list results (${result.summary.total}):\n`);
285
+ for (const r of result.results) {
286
+ const icon = r.status === 'still_available' ? '✅' :
287
+ r.status === 'claimed' ? '🔒' :
288
+ r.status === 'has_pr' ? '🔀' :
289
+ r.status === 'closed' ? '🚫' : '❌';
290
+ const score = r.viabilityScore != null ? ` [${r.viabilityScore}/100]` : '';
291
+ console.log(` ${icon} ${r.repo}#${r.number} — ${r.status}${score}`);
292
+ console.log(` ${r.title}`);
293
+ }
294
+ console.log(`\nSummary: ${result.summary.stillAvailable} available, ${result.summary.claimed} claimed, ${result.summary.hasPR} has PR, ${result.summary.closed} closed, ${result.summary.errors} errors`);
295
+ if (result.prunedCount != null) {
296
+ console.log(`Pruned ${result.prunedCount} unavailable issues from saved results.`);
297
+ }
298
+ console.log();
299
+ }
300
+ }
301
+ catch (err) {
302
+ handleCommandError(err, options);
303
+ }
304
+ });
305
+ program
306
+ .command('vet <issue-url>')
307
+ .description('Vet a specific GitHub issue for claimability and project health')
308
+ .option('--json', 'Output as JSON')
309
+ .action(async (issueUrl, options) => {
310
+ try {
311
+ const { runVet } = await import('./commands/vet.js');
312
+ const state = loadLocalState();
313
+ const result = await runVet({ issueUrl, state });
314
+ if (options.json) {
315
+ console.log(formatJsonSuccess(result));
316
+ }
317
+ else {
318
+ const icon = result.recommendation === 'approve' ? '✅' : result.recommendation === 'skip' ? '❌' : '⚠️';
319
+ console.log(`\n${icon} ${result.issue.repo}#${result.issue.number}: ${result.recommendation.toUpperCase()}`);
320
+ console.log(` ${result.issue.title}`);
321
+ console.log(` ${result.issue.url}\n`);
322
+ if (result.reasonsToApprove.length > 0) {
323
+ console.log('Reasons to approve:');
324
+ for (const r of result.reasonsToApprove)
325
+ console.log(` + ${r}`);
326
+ }
327
+ if (result.reasonsToSkip.length > 0) {
328
+ console.log('Reasons to skip:');
329
+ for (const r of result.reasonsToSkip)
330
+ console.log(` - ${r}`);
331
+ }
332
+ console.log(`\nProject health: ${result.projectHealth.isActive ? 'Active' : 'Inactive'}`);
333
+ console.log(` Last commit: ${result.projectHealth.daysSinceLastCommit} days ago`);
334
+ console.log(` CI status: ${result.projectHealth.ciStatus}`);
335
+ }
336
+ }
337
+ catch (err) {
338
+ handleCommandError(err, options);
339
+ }
340
+ });
341
+ program.parse();
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Config command — view and update oss-scout preferences.
3
+ */
4
+ import type { ScoutPreferences } from '../core/schemas.js';
5
+ /**
6
+ * Display current preferences in human-readable format.
7
+ */
8
+ export declare function runConfigShow(options: {
9
+ json?: boolean;
10
+ }): void;
11
+ /**
12
+ * Get current preferences for JSON output.
13
+ */
14
+ export declare function getConfigData(): ScoutPreferences;
15
+ /**
16
+ * Update a single preference by key.
17
+ */
18
+ export declare function runConfigSet(key: string, value: string): ScoutPreferences;
19
+ /**
20
+ * Reset preferences to defaults.
21
+ */
22
+ export declare function runConfigReset(): ScoutPreferences;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Config command — view and update oss-scout preferences.
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
+ 'aiPolicyBlocklist',
15
+ ]);
16
+ const NUMBER_FIELDS = new Set(['minStars', 'maxIssueAgeDays', 'minRepoScoreThreshold']);
17
+ const BOOLEAN_FIELDS = new Set(['includeDocIssues']);
18
+ const STRING_FIELDS = new Set(['githubUsername']);
19
+ const SCOPE_FIELD = 'scope';
20
+ const ENUM_FIELDS = {
21
+ persistence: PersistenceModeSchema.options,
22
+ };
23
+ const ALL_FIELDS = new Set([
24
+ ...ARRAY_FIELDS,
25
+ ...NUMBER_FIELDS,
26
+ ...BOOLEAN_FIELDS,
27
+ ...STRING_FIELDS,
28
+ ...Object.keys(ENUM_FIELDS),
29
+ SCOPE_FIELD,
30
+ ]);
31
+ const VALID_SCOPES = IssueScopeSchema.options;
32
+ const VALID_CATEGORIES = ProjectCategorySchema.options;
33
+ function parseBoolean(value) {
34
+ const lower = value.toLowerCase();
35
+ if (lower === 'true' || lower === 'yes')
36
+ return true;
37
+ if (lower === 'false' || lower === 'no')
38
+ return false;
39
+ throw new ValidationError(`Invalid boolean value: "${value}". Use true/false or yes/no.`);
40
+ }
41
+ function parseNumber(value, key) {
42
+ const num = parseInt(value, 10);
43
+ if (isNaN(num)) {
44
+ throw new ValidationError(`Invalid number for "${key}": "${value}"`);
45
+ }
46
+ return num;
47
+ }
48
+ function parseArrayValue(value) {
49
+ return value
50
+ .split(',')
51
+ .map((s) => s.trim())
52
+ .filter((s) => s.length > 0);
53
+ }
54
+ /**
55
+ * Apply an array update: plain set, +append, or -remove.
56
+ */
57
+ function updateArray(current, value) {
58
+ if (value.startsWith('+')) {
59
+ const toAdd = parseArrayValue(value.slice(1));
60
+ const merged = [...current];
61
+ for (const item of toAdd) {
62
+ if (!merged.includes(item))
63
+ merged.push(item);
64
+ }
65
+ return merged;
66
+ }
67
+ if (value.startsWith('-')) {
68
+ const toRemove = new Set(parseArrayValue(value.slice(1)));
69
+ return current.filter((item) => !toRemove.has(item));
70
+ }
71
+ return parseArrayValue(value);
72
+ }
73
+ function formatArray(arr) {
74
+ return arr.length > 0 ? arr.join(', ') : '(none)';
75
+ }
76
+ /**
77
+ * Display current preferences in human-readable format.
78
+ */
79
+ export function runConfigShow(options) {
80
+ const state = loadLocalState();
81
+ const prefs = state.preferences;
82
+ if (options.json) {
83
+ // JSON output handled by caller
84
+ return;
85
+ }
86
+ console.log('\n⚙️ oss-scout preferences\n');
87
+ console.log(` githubUsername: ${prefs.githubUsername || '(not set)'}`);
88
+ console.log(` languages: ${formatArray(prefs.languages)}`);
89
+ console.log(` labels: ${formatArray(prefs.labels)}`);
90
+ console.log(` scope: ${prefs.scope ? formatArray(prefs.scope) : '(all)'}`);
91
+ console.log(` minStars: ${prefs.minStars}`);
92
+ console.log(` maxIssueAgeDays: ${prefs.maxIssueAgeDays}`);
93
+ console.log(` minRepoScoreThreshold: ${prefs.minRepoScoreThreshold}`);
94
+ console.log(` includeDocIssues: ${prefs.includeDocIssues}`);
95
+ console.log(` preferredOrgs: ${formatArray(prefs.preferredOrgs)}`);
96
+ console.log(` projectCategories: ${formatArray(prefs.projectCategories)}`);
97
+ console.log(` excludeRepos: ${formatArray(prefs.excludeRepos)}`);
98
+ console.log(` aiPolicyBlocklist: ${formatArray(prefs.aiPolicyBlocklist)}`);
99
+ console.log(` persistence: ${prefs.persistence}`);
100
+ console.log();
101
+ }
102
+ /**
103
+ * Get current preferences for JSON output.
104
+ */
105
+ export function getConfigData() {
106
+ const state = loadLocalState();
107
+ return state.preferences;
108
+ }
109
+ /**
110
+ * Update a single preference by key.
111
+ */
112
+ export function runConfigSet(key, value) {
113
+ if (!ALL_FIELDS.has(key)) {
114
+ throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${[...ALL_FIELDS].sort().join(', ')}`);
115
+ }
116
+ const state = loadLocalState();
117
+ const prefs = { ...state.preferences };
118
+ if (STRING_FIELDS.has(key)) {
119
+ prefs[key] = value;
120
+ }
121
+ else if (BOOLEAN_FIELDS.has(key)) {
122
+ prefs[key] = parseBoolean(value);
123
+ }
124
+ else if (NUMBER_FIELDS.has(key)) {
125
+ prefs[key] = parseNumber(value, key);
126
+ }
127
+ else if (key === SCOPE_FIELD) {
128
+ const updated = updateArray(prefs.scope ?? [], value);
129
+ const invalid = updated.filter((s) => !VALID_SCOPES.includes(s));
130
+ if (invalid.length > 0) {
131
+ throw new ValidationError(`Invalid scope value(s): ${invalid.join(', ')}. Valid: ${VALID_SCOPES.join(', ')}`);
132
+ }
133
+ prefs.scope = updated.length > 0 ? updated : undefined;
134
+ }
135
+ else if (key === 'projectCategories') {
136
+ const updated = updateArray(prefs.projectCategories, value);
137
+ const invalid = updated.filter((s) => !VALID_CATEGORIES.includes(s));
138
+ if (invalid.length > 0) {
139
+ throw new ValidationError(`Invalid category value(s): ${invalid.join(', ')}. Valid: ${VALID_CATEGORIES.join(', ')}`);
140
+ }
141
+ prefs.projectCategories = updated;
142
+ }
143
+ else if (key in ENUM_FIELDS) {
144
+ const validValues = ENUM_FIELDS[key];
145
+ if (!validValues.includes(value)) {
146
+ throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(', ')}`);
147
+ }
148
+ prefs[key] = value;
149
+ }
150
+ else if (ARRAY_FIELDS.has(key)) {
151
+ const current = prefs[key] ?? [];
152
+ prefs[key] = updateArray(current, value);
153
+ }
154
+ // Validate the full preferences object
155
+ const validated = ScoutPreferencesSchema.parse(prefs);
156
+ state.preferences = validated;
157
+ saveLocalState(state);
158
+ return validated;
159
+ }
160
+ /**
161
+ * Reset preferences to defaults.
162
+ */
163
+ export function runConfigReset() {
164
+ const state = loadLocalState();
165
+ const defaults = ScoutPreferencesSchema.parse({});
166
+ state.preferences = defaults;
167
+ saveLocalState(state);
168
+ return defaults;
169
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Results command — display and manage saved search results.
3
+ */
4
+ import type { SavedCandidate } from '../core/schemas.js';
5
+ export declare function runResults(_options: {
6
+ json?: boolean;
7
+ }): Promise<SavedCandidate[]>;
8
+ export declare function runResultsClear(): Promise<void>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Results command — display and manage saved search results.
3
+ */
4
+ import { loadLocalState, saveLocalState } from '../core/local-state.js';
5
+ export async function runResults(_options) {
6
+ const state = loadLocalState();
7
+ return state.savedResults ?? [];
8
+ }
9
+ export async function runResultsClear() {
10
+ const state = loadLocalState();
11
+ state.savedResults = [];
12
+ saveLocalState(state);
13
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Search command — finds contributable issues using multi-strategy search.
3
+ */
4
+ import type { ScoutState, SearchStrategy } from '../core/schemas.js';
5
+ export interface SearchOutput {
6
+ candidates: Array<{
7
+ issue: {
8
+ repo: string;
9
+ repoUrl: string;
10
+ number: number;
11
+ title: string;
12
+ url: string;
13
+ labels: string[];
14
+ };
15
+ recommendation: 'approve' | 'skip' | 'needs_review';
16
+ reasonsToApprove: string[];
17
+ reasonsToSkip: string[];
18
+ searchPriority: string;
19
+ viabilityScore: number;
20
+ repoScore?: {
21
+ score: number;
22
+ mergedPRCount: number;
23
+ closedWithoutMergeCount: number;
24
+ isResponsive: boolean;
25
+ lastMergedAt?: string;
26
+ };
27
+ }>;
28
+ excludedRepos: string[];
29
+ aiPolicyBlocklist: string[];
30
+ rateLimitWarning?: string;
31
+ strategiesUsed: SearchStrategy[];
32
+ }
33
+ interface SearchCommandOptions {
34
+ maxResults: number;
35
+ state?: ScoutState;
36
+ strategies?: SearchStrategy[];
37
+ }
38
+ export declare function runSearch(options: SearchCommandOptions): Promise<SearchOutput>;
39
+ export {};
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Search command — finds contributable issues using multi-strategy search.
3
+ */
4
+ import { createScout } from '../scout.js';
5
+ import { requireGitHubToken } from '../core/utils.js';
6
+ import { saveLocalState } from '../core/local-state.js';
7
+ export async function runSearch(options) {
8
+ const token = requireGitHubToken();
9
+ const scout = options.state
10
+ ? await createScout({ githubToken: token, persistence: 'provided', initialState: options.state })
11
+ : await createScout({ githubToken: token });
12
+ const result = await scout.search({ maxResults: options.maxResults, strategies: options.strategies });
13
+ // Persist results to local state and gist
14
+ scout.saveResults(result.candidates);
15
+ saveLocalState(scout.getState());
16
+ await scout.checkpoint();
17
+ return {
18
+ candidates: result.candidates.map((c) => {
19
+ const repoScoreRecord = scout.getRepoScoreRecord(c.issue.repo);
20
+ return {
21
+ issue: {
22
+ repo: c.issue.repo,
23
+ repoUrl: `https://github.com/${c.issue.repo}`,
24
+ number: c.issue.number,
25
+ title: c.issue.title,
26
+ url: c.issue.url,
27
+ labels: c.issue.labels,
28
+ },
29
+ recommendation: c.recommendation,
30
+ reasonsToApprove: c.reasonsToApprove,
31
+ reasonsToSkip: c.reasonsToSkip,
32
+ searchPriority: c.searchPriority,
33
+ viabilityScore: c.viabilityScore,
34
+ repoScore: repoScoreRecord
35
+ ? {
36
+ score: repoScoreRecord.score,
37
+ mergedPRCount: repoScoreRecord.mergedPRCount,
38
+ closedWithoutMergeCount: repoScoreRecord.closedWithoutMergeCount,
39
+ isResponsive: repoScoreRecord.signals?.isResponsive ?? false,
40
+ lastMergedAt: repoScoreRecord.lastMergedAt,
41
+ }
42
+ : undefined,
43
+ };
44
+ }),
45
+ excludedRepos: result.excludedRepos,
46
+ aiPolicyBlocklist: result.aiPolicyBlocklist,
47
+ rateLimitWarning: result.rateLimitWarning,
48
+ strategiesUsed: result.strategiesUsed,
49
+ };
50
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Setup command — interactive first-run configuration for oss-scout.
3
+ */
4
+ import type { ScoutPreferences } from '../core/schemas.js';
5
+ interface ReadlineInterface {
6
+ question(query: string, callback: (answer: string) => void): void;
7
+ close(): void;
8
+ }
9
+ export interface SetupOptions {
10
+ rl?: ReadlineInterface;
11
+ detectUsername?: () => Promise<string>;
12
+ }
13
+ /**
14
+ * Run the interactive setup flow and return the configured preferences.
15
+ */
16
+ export declare function runSetup(options?: SetupOptions): Promise<ScoutPreferences>;
17
+ export {};