@oss-scout/core 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/dist/cli.js CHANGED
@@ -223,7 +223,7 @@ const configCmd = program
223
223
  console.log(formatJsonSuccess(getConfigData()));
224
224
  }
225
225
  else {
226
- runConfigShow(options);
226
+ runConfigShow();
227
227
  }
228
228
  }
229
229
  catch (err) {
@@ -322,6 +322,113 @@ program
322
322
  handleCommandError(err, options);
323
323
  }
324
324
  });
325
+ // ── skip command ───────────────────────────────────────────────────
326
+ const skipCmd = program
327
+ .command("skip")
328
+ .description("Manage the skip list — exclude issues from future searches");
329
+ skipCmd
330
+ .command("add <issue-url>")
331
+ .description("Skip an issue by URL")
332
+ .option("--json", "Output as JSON")
333
+ .action(async (issueUrl, options) => {
334
+ try {
335
+ const { runSkip } = await import("./commands/skip.js");
336
+ const state = loadLocalState();
337
+ const result = await runSkip({ issueUrl, state });
338
+ if (options.json) {
339
+ console.log(formatJsonSuccess(result));
340
+ }
341
+ else {
342
+ if (result.alreadySkipped) {
343
+ console.log("Issue already in skip list.");
344
+ }
345
+ else {
346
+ console.log(`Skipped: ${issueUrl}`);
347
+ }
348
+ }
349
+ }
350
+ catch (err) {
351
+ handleCommandError(err, options);
352
+ }
353
+ });
354
+ skipCmd
355
+ .command("list")
356
+ .description("Show all skipped issues")
357
+ .option("--json", "Output as JSON")
358
+ .action(async (options) => {
359
+ try {
360
+ const { runSkipList } = await import("./commands/skip.js");
361
+ const results = runSkipList();
362
+ if (options.json) {
363
+ console.log(formatJsonSuccess(results));
364
+ }
365
+ else {
366
+ if (results.length === 0) {
367
+ console.log("\nNo skipped issues.\n");
368
+ return;
369
+ }
370
+ console.log(`\nSkipped issues (${results.length}):\n`);
371
+ console.log(" Repo Issue Skipped Title");
372
+ console.log(" ──────────────────────────────── ────── ────────── ─────");
373
+ for (const s of results) {
374
+ const repo = (s.repo || "unknown").padEnd(32).slice(0, 32);
375
+ const issue = s.number ? `#${s.number}`.padEnd(6) : "—".padEnd(6);
376
+ const skippedDate = s.skippedAt.split("T")[0] ?? "";
377
+ const title = s.title.length > 50
378
+ ? s.title.slice(0, 47) + "..."
379
+ : s.title || s.url;
380
+ console.log(` ${repo} ${issue} ${skippedDate} ${title}`);
381
+ }
382
+ console.log();
383
+ }
384
+ }
385
+ catch (err) {
386
+ handleCommandError(err, options);
387
+ }
388
+ });
389
+ skipCmd
390
+ .command("remove <issue-url>")
391
+ .description("Remove an issue from the skip list (unskip)")
392
+ .option("--json", "Output as JSON")
393
+ .action(async (issueUrl, options) => {
394
+ try {
395
+ const { runSkipRemove } = await import("./commands/skip.js");
396
+ const result = await runSkipRemove({ issueUrl });
397
+ if (options.json) {
398
+ console.log(formatJsonSuccess(result));
399
+ }
400
+ else {
401
+ if (result.removed) {
402
+ console.log(`Removed from skip list: ${issueUrl}`);
403
+ }
404
+ else {
405
+ console.log("Issue was not in the skip list.");
406
+ }
407
+ }
408
+ }
409
+ catch (err) {
410
+ handleCommandError(err, options);
411
+ }
412
+ });
413
+ skipCmd
414
+ .command("clear")
415
+ .description("Clear all skipped issues")
416
+ .option("--json", "Output as JSON")
417
+ .action(async (options) => {
418
+ try {
419
+ const { runSkipClear } = await import("./commands/skip.js");
420
+ await runSkipClear();
421
+ if (options.json) {
422
+ console.log(formatJsonSuccess({ cleared: true }));
423
+ }
424
+ else {
425
+ console.log("Skip list cleared.");
426
+ }
427
+ }
428
+ catch (err) {
429
+ handleCommandError(err, options);
430
+ }
431
+ });
325
432
  program
326
433
  .command("vet <issue-url>")
327
434
  .description("Vet a specific GitHub issue for claimability and project health")
@@ -5,9 +5,7 @@ import type { ScoutPreferences } from "../core/schemas.js";
5
5
  /**
6
6
  * Display current preferences in human-readable format.
7
7
  */
8
- export declare function runConfigShow(options: {
9
- json?: boolean;
10
- }): void;
8
+ export declare function runConfigShow(): void;
11
9
  /**
12
10
  * Get current preferences for JSON output.
13
11
  */
@@ -10,10 +10,10 @@ const FIELD_CONFIGS = {
10
10
  excludeRepos: { type: "array" },
11
11
  excludeOrgs: { type: "array" },
12
12
  aiPolicyBlocklist: { type: "array" },
13
- preferredOrgs: { type: "array" },
14
13
  minStars: { type: "number" },
15
14
  maxIssueAgeDays: { type: "number" },
16
15
  minRepoScoreThreshold: { type: "number" },
16
+ interPhaseDelayMs: { type: "number" },
17
17
  includeDocIssues: { type: "boolean" },
18
18
  scope: { type: "enum-array", validValues: IssueScopeSchema.options },
19
19
  projectCategories: {
@@ -26,6 +26,8 @@ const FIELD_CONFIGS = {
26
26
  validValues: SearchStrategySchema.options,
27
27
  },
28
28
  githubUsername: { type: "string" },
29
+ broadPhaseDelayMs: { type: "number" },
30
+ skipBroadWhenSufficientResults: { type: "number" },
29
31
  };
30
32
  function parseBoolean(value) {
31
33
  const lower = value.toLowerCase();
@@ -73,13 +75,9 @@ function formatArray(arr) {
73
75
  /**
74
76
  * Display current preferences in human-readable format.
75
77
  */
76
- export function runConfigShow(options) {
78
+ export function runConfigShow() {
77
79
  const state = loadLocalState();
78
80
  const prefs = state.preferences;
79
- if (options.json) {
80
- // JSON output handled by caller
81
- return;
82
- }
83
81
  console.log("\n⚙️ oss-scout preferences\n");
84
82
  console.log(` githubUsername: ${prefs.githubUsername || "(not set)"}`);
85
83
  console.log(` languages: ${formatArray(prefs.languages)}`);
@@ -88,14 +86,16 @@ export function runConfigShow(options) {
88
86
  console.log(` minStars: ${prefs.minStars}`);
89
87
  console.log(` maxIssueAgeDays: ${prefs.maxIssueAgeDays}`);
90
88
  console.log(` minRepoScoreThreshold: ${prefs.minRepoScoreThreshold}`);
89
+ console.log(` interPhaseDelayMs: ${prefs.interPhaseDelayMs}ms (${(prefs.interPhaseDelayMs / 1000).toFixed(0)}s)`);
91
90
  console.log(` includeDocIssues: ${prefs.includeDocIssues}`);
92
- console.log(` preferredOrgs: ${formatArray(prefs.preferredOrgs)}`);
93
91
  console.log(` projectCategories: ${formatArray(prefs.projectCategories)}`);
94
92
  console.log(` excludeRepos: ${formatArray(prefs.excludeRepos)}`);
95
93
  console.log(` excludeOrgs: ${formatArray(prefs.excludeOrgs)}`);
96
94
  console.log(` aiPolicyBlocklist: ${formatArray(prefs.aiPolicyBlocklist)}`);
97
95
  console.log(` defaultStrategy: ${prefs.defaultStrategy ? formatArray(prefs.defaultStrategy) : "(all)"}`);
98
96
  console.log(` persistence: ${prefs.persistence}`);
97
+ console.log(` broadPhaseDelayMs: ${prefs.broadPhaseDelayMs}ms (${(prefs.broadPhaseDelayMs / 1000).toFixed(0)}s)`);
98
+ console.log(` skipBroadWhenSufficientResults: ${prefs.skipBroadWhenSufficientResults}`);
99
99
  console.log();
100
100
  }
101
101
  /**
@@ -59,11 +59,9 @@ export async function runSetup(options) {
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
65
- ? parseCSV(langsInput)
66
- : ["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"];
67
65
  // Issue labels
68
66
  const defaultLabels = "good first issue, help wanted";
69
67
  const labelsInput = await ask(rl, `Issue labels to search for [${defaultLabels}]: `);
@@ -79,9 +77,6 @@ export async function runSetup(options) {
79
77
  // Minimum stars
80
78
  const minStarsInput = await ask(rl, "Minimum repo stars [50]: ");
81
79
  const minStars = minStarsInput ? parseInt(minStarsInput, 10) : 50;
82
- // Preferred organizations
83
- const orgsInput = await ask(rl, "Preferred organizations (comma-separated, optional): ");
84
- const preferredOrgs = parseCSV(orgsInput);
85
80
  // Project categories
86
81
  const categoryOptions = ALL_CATEGORIES.join(", ");
87
82
  const categoriesInput = await ask(rl, `Project categories (${categoryOptions}) [none]: `);
@@ -95,7 +90,6 @@ export async function runSetup(options) {
95
90
  labels,
96
91
  scope: scope.length > 0 ? scope : undefined,
97
92
  excludeRepos,
98
- preferredOrgs,
99
93
  projectCategories,
100
94
  minStars: isNaN(minStars) ? 50 : minStars,
101
95
  });
@@ -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
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { getOctokit, checkRateLimit } from "./github.js";
6
6
  import { debug, warn } from "./logger.js";
7
- import { errorMessage } from "./errors.js";
7
+ import { ConfigurationError, errorMessage } from "./errors.js";
8
8
  import { extractRepoFromUrl } from "./utils.js";
9
9
  const MODULE = "bootstrap";
10
10
  const STARRED_MAX_PAGES = 5;
@@ -13,7 +13,7 @@ const PER_PAGE = 100;
13
13
  export async function bootstrapScout(scout, token) {
14
14
  const username = scout.getPreferences().githubUsername;
15
15
  if (!username) {
16
- throw new Error("GitHub username not configured. Run `oss-scout setup` first.");
16
+ throw new ConfigurationError("GitHub username not configured. Run `oss-scout setup` first.");
17
17
  }
18
18
  const rateLimit = await checkRateLimit(token);
19
19
  debug(MODULE, `Rate limit: ${rateLimit.remaining}/${rateLimit.limit}, resets at ${rateLimit.resetAt}`);
@@ -50,11 +50,16 @@ export class GistStateStore {
50
50
  warn(MODULE, "No gist ID — cannot push");
51
51
  return false;
52
52
  }
53
+ const json = JSON.stringify(state, null, 2);
54
+ if (json.length > 900000) {
55
+ warn(MODULE, `State too large for gist (${Math.round(json.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
56
+ return false;
57
+ }
53
58
  try {
54
59
  await this.octokit.gists.update({
55
60
  gist_id: this.gistId,
56
61
  files: {
57
- [GIST_FILENAME]: { content: JSON.stringify(state, null, 2) },
62
+ [GIST_FILENAME]: { content: json },
58
63
  },
59
64
  });
60
65
  debug(MODULE, "State pushed to gist");
@@ -248,6 +253,7 @@ export function mergeStates(local, remote) {
248
253
  mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
249
254
  closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
250
255
  savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
256
+ skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
251
257
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
252
258
  lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
253
259
  new Date().toISOString(),
@@ -302,6 +308,18 @@ function mergeSavedResults(local, remote) {
302
308
  }
303
309
  return [...merged.values()];
304
310
  }
311
+ function mergeSkippedIssues(local, remote) {
312
+ const merged = new Map();
313
+ for (const item of local)
314
+ merged.set(item.url, item);
315
+ for (const item of remote) {
316
+ const existing = merged.get(item.url);
317
+ if (!existing || item.skippedAt > existing.skippedAt) {
318
+ merged.set(item.url, item);
319
+ }
320
+ }
321
+ return [...merged.values()];
322
+ }
305
323
  function pickFresherTimestamp(a, b) {
306
324
  if (!a)
307
325
  return b;
@@ -19,7 +19,6 @@ import { type ScoutStateReader } from "./issue-vetting.js";
19
19
  *
20
20
  * Search phases (in priority order):
21
21
  * 0. Repos where user has merged PRs (highest merge probability)
22
- * 0.5. Preferred organizations
23
22
  * 1. Starred repos
24
23
  * 2. General label-filtered search
25
24
  * 3. Actively maintained repos
@@ -47,8 +46,8 @@ export declare class IssueDiscovery {
47
46
  getStarredRepos(): string[];
48
47
  /**
49
48
  * Search for issues matching our criteria.
50
- * Searches in priority order: merged-PR repos first (no label filter), then preferred
51
- * organizations, then starred repos, then general search, then actively maintained repos.
49
+ * Searches in priority order: merged-PR repos first (no label filter), then starred
50
+ * repos, then general search, then actively maintained repos.
52
51
  * Filters out issues from low-scoring and excluded repos.
53
52
  *
54
53
  * @param options - Search configuration
@@ -74,6 +73,7 @@ export declare class IssueDiscovery {
74
73
  labels?: string[];
75
74
  maxResults?: number;
76
75
  strategies?: SearchStrategy[];
76
+ skippedUrls?: Set<string>;
77
77
  }): Promise<{
78
78
  candidates: IssueCandidate[];
79
79
  strategiesUsed: SearchStrategy[];