setup
Set your languages, interests, and contribution goals for open source contributions. Use key=value pairs to configure non-interactively or reset to defaults.
Instructions
Run OSS Autopilot setup to configure preferences like languages, interests, and contribution goals.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| reset | No | If true, reset all preferences to defaults before running setup | |
| set | No | Set preferences non-interactively as key=value pairs (e.g. ["languages=typescript,rust"]) |
Implementation Reference
- The main `runSetup` function that implements the setup tool logic. Handles three modes: (1) `--set key=value` to apply settings directly via `stateManager.batch()`, (2) returns current config if setup is already complete, (3) returns interactive prompts for setup wizard. Supports settings like username, maxActivePRs, languages, labels, scope, persistence, etc.
export async function runSetup(options: SetupOptions): Promise<SetupOutput> { const stateManager = getStateManager(); const config = stateManager.getState().config; // Handle --set mode: apply settings directly if (options.set && options.set.length > 0) { const results: Record<string, string> = {}; const warnings: string[] = []; // Pre-validate every key before mutating state. `stateManager.batch()` only // defers the disk write — it does not snapshot in-memory state, so throwing // mid-loop would leave earlier successful updates applied in memory (a real // issue for long-running consumers like the MCP server and dashboard that // share the StateManager singleton across requests). const knownKeys = new Set(getSetupKeys()); for (const setting of options.set) { const [key] = setting.split('='); if (!knownKeys.has(key)) { throw new ValidationError(formatUnknownKeyError(key, 'setup')); } } stateManager.batch(() => { for (const setting of options.set!) { const [key, ...valueParts] = setting.split('='); const value = valueParts.join('='); switch (key) { case 'username': { validateGitHubUsername(value); stateManager.updateConfig({ githubUsername: value }); results[key] = value; break; } case 'maxActivePRs': { const maxPRs = parsePositiveInt(value, 'maxActivePRs'); stateManager.updateConfig({ maxActivePRs: maxPRs }); results[key] = String(maxPRs); break; } case 'dormantDays': { const dormant = parsePositiveInt(value, 'dormantDays'); stateManager.updateConfig({ dormantThresholdDays: dormant }); results[key] = String(dormant); break; } case 'approachingDays': { const approaching = parsePositiveInt(value, 'approachingDays'); stateManager.updateConfig({ approachingDormantDays: approaching }); results[key] = String(approaching); break; } case 'languages': { stateManager.updateConfig({ languages: value.split(',').map((l) => l.trim()) }); results[key] = value; break; } case 'labels': { stateManager.updateConfig({ labels: value.split(',').map((l) => l.trim()) }); results[key] = value; break; } case 'squashByDefault': { if (value === 'ask') { stateManager.updateConfig({ squashByDefault: 'ask' }); results[key] = 'ask'; } else { stateManager.updateConfig({ squashByDefault: value !== 'false' }); results[key] = value !== 'false' ? 'true' : 'false'; } break; } case 'minStars': { const stars = Number(value); if (!Number.isInteger(stars) || stars < 0) { throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`); } stateManager.updateConfig({ minStars: stars }); results[key] = String(stars); break; } case 'maxIssueAgeDays': { const days = parsePositiveInt(value, 'maxIssueAgeDays'); stateManager.updateConfig({ maxIssueAgeDays: days }); results[key] = String(days); break; } case 'minRepoScoreThreshold': { const threshold = Number(value); if (!Number.isInteger(threshold) || threshold < 0) { throw new ValidationError( `Invalid value for minRepoScoreThreshold: "${value}". Must be a non-negative integer.`, ); } stateManager.updateConfig({ minRepoScoreThreshold: threshold }); results[key] = String(threshold); break; } case 'skippedIssuesPath': { stateManager.updateConfig({ skippedIssuesPath: value || undefined }); results[key] = value || '(cleared)'; break; } case 'autoFormatBeforePush': { if (value !== 'true' && value !== 'false') { throw new ValidationError( `Invalid value for autoFormatBeforePush: "${value}". Must be "true" or "false".`, ); } const enabled = value === 'true'; stateManager.updateConfig({ autoFormatBeforePush: enabled }); results[key] = String(enabled); break; } case 'includeDocIssues': { stateManager.updateConfig({ includeDocIssues: value === 'true' }); results[key] = value === 'true' ? 'true' : 'false'; break; } case 'aiPolicyBlocklist': { const entries = value .split(',') .map((r) => r.trim()) .filter(Boolean); const valid: string[] = []; const invalid: string[] = []; for (const entry of entries) { const normalized = entry.replace(/\s+/g, ''); if (/^[\w.-]+\/[\w.-]+$/.test(normalized)) { valid.push(normalized); } else { invalid.push(entry); } } if (invalid.length > 0) { warnings.push(`Warning: Skipping invalid entries (expected "owner/repo" format): ${invalid.join(', ')}`); results['aiPolicyBlocklist_invalidEntries'] = invalid.join(', '); } if (valid.length === 0 && entries.length > 0) { warnings.push('Warning: All entries were invalid. Blocklist not updated.'); results[key] = '(all entries invalid)'; break; } stateManager.updateConfig({ aiPolicyBlocklist: valid }); results[key] = valid.length > 0 ? valid.join(', ') : '(empty)'; break; } case 'projectCategories': { const categories = value .split(',') .map((c) => c.trim()) .filter(Boolean); const validCategories: ProjectCategory[] = []; const invalidCategories: string[] = []; for (const cat of categories) { if ((PROJECT_CATEGORIES as readonly string[]).includes(cat)) { validCategories.push(cat as ProjectCategory); } else { invalidCategories.push(cat); } } if (invalidCategories.length > 0) { warnings.push( `Unknown project categories: ${invalidCategories.join(', ')}. Valid: ${PROJECT_CATEGORIES.join(', ')}`, ); } const dedupedCategories = [...new Set(validCategories)]; stateManager.updateConfig({ projectCategories: dedupedCategories }); results[key] = dedupedCategories.length > 0 ? dedupedCategories.join(', ') : '(empty)'; break; } case 'preferredOrgs': { const orgs = value .split(',') .map((o) => o.trim()) .filter(Boolean); const validOrgs: string[] = []; for (const org of orgs) { if (org.includes('/')) { warnings.push( `"${org}" looks like a repo path. Use org name only (e.g., "vercel" not "vercel/next.js").`, ); } else if (!/^[\da-z](?:[\da-z-]*[\da-z])?$/i.test(org)) { warnings.push(`"${org}" is not a valid GitHub organization name. Skipping.`); } else { validOrgs.push(org.toLowerCase()); } } const dedupedOrgs = [...new Set(validOrgs)]; stateManager.updateConfig({ preferredOrgs: dedupedOrgs }); results[key] = dedupedOrgs.length > 0 ? dedupedOrgs.join(', ') : '(empty)'; break; } case 'scope': { const scopeValues = value .split(',') .map((s) => s.trim()) .filter(Boolean); const validScopes: IssueScope[] = []; const invalidScopes: string[] = []; for (const s of scopeValues) { if ((ISSUE_SCOPES as readonly string[]).includes(s)) { validScopes.push(s as IssueScope); } else { invalidScopes.push(s); } } if (invalidScopes.length > 0) { warnings.push(`Unknown issue scopes: ${invalidScopes.join(', ')}. Valid: ${ISSUE_SCOPES.join(', ')}`); } const dedupedScopes = [...new Set(validScopes)]; stateManager.updateConfig({ scope: dedupedScopes.length > 0 ? dedupedScopes : undefined }); results[key] = dedupedScopes.length > 0 ? dedupedScopes.join(', ') : '(empty — using labels only)'; break; } case 'persistence': { if (value !== 'local' && value !== 'gist') { throw new ValidationError(`Invalid value for persistence: "${value}". Must be "local" or "gist".`); } stateManager.updateConfig({ persistence: value as 'local' | 'gist' }); results[key] = value; break; } case 'issueListPath': { stateManager.updateConfig({ issueListPath: value || undefined }); results[key] = value || '(cleared)'; break; } case 'diffTool': { if (!(DIFF_TOOLS as readonly string[]).includes(value)) { warnings.push(`Invalid diffTool "${value}". Valid: ${DIFF_TOOLS.join(', ')}`); break; } stateManager.updateConfig({ diffTool: value as DiffTool }); results[key] = value; break; } case 'diffToolCustomCommand': { stateManager.updateConfig({ diffToolCustomCommand: value || undefined }); results[key] = value || '(cleared)'; break; } case 'complete': { if (value === 'true') { stateManager.markSetupComplete(); results[key] = 'true'; } break; } default: { throw new ValidationError(formatUnknownKeyError(key, 'setup')); } } } }); return { success: true, settings: results, warnings: warnings.length > 0 ? warnings : undefined }; } // Show setup status if (config.setupComplete && !options.reset) { return { setupComplete: true, config: { githubUsername: config.githubUsername, maxActivePRs: config.maxActivePRs, dormantThresholdDays: config.dormantThresholdDays, approachingDormantDays: config.approachingDormantDays, languages: config.languages, labels: config.labels, projectCategories: config.projectCategories ?? [], preferredOrgs: config.preferredOrgs ?? [], scope: config.scope ?? [], persistence: config.persistence ?? 'local', }, }; } // Output setup prompts return { setupRequired: true, prompts: [ { setting: 'username', prompt: 'What is your GitHub username?', current: config.githubUsername || null, required: true, type: 'string', }, { setting: 'maxActivePRs', prompt: 'How many PRs do you want to work on at once?', current: config.maxActivePRs, default: 10, type: 'number', }, { setting: 'dormantDays', prompt: 'After how many days of inactivity should a PR be considered dormant?', current: config.dormantThresholdDays, default: 30, type: 'number', }, { setting: 'approachingDays', prompt: 'At how many days should we warn about approaching dormancy?', current: config.approachingDormantDays, default: 25, type: 'number', }, { setting: 'languages', prompt: 'What programming languages do you want to contribute to?', current: config.languages, default: ['typescript', 'javascript'], type: 'list', }, { setting: 'labels', prompt: 'What issue labels should we search for?', current: config.labels, default: ['good first issue', 'help wanted'], type: 'list', }, { setting: 'scope', prompt: 'What scope of issues do you want to discover? (beginner, intermediate, advanced — leave empty for default labels only)', current: config.scope ?? [], default: [], type: 'list', }, { setting: 'aiPolicyBlocklist', prompt: 'Repos with anti-AI contribution policies to block (owner/repo, comma-separated)?', current: config.aiPolicyBlocklist ?? DEFAULT_CONFIG.aiPolicyBlocklist ?? null, default: ['matplotlib/matplotlib'], type: 'list', }, { setting: 'projectCategories', prompt: 'What types of projects interest you? (nonprofit, devtools, infrastructure, web-frameworks, data-ml, education)', current: config.projectCategories ?? [], default: [], type: 'list', }, { setting: 'preferredOrgs', prompt: 'Any GitHub organizations to prioritize? (org names, comma-separated)', current: config.preferredOrgs ?? [], default: [], type: 'list', }, { setting: 'persistence', prompt: 'Where should state be stored? "local" for file only, "gist" for GitHub Gist (survives device loss)', current: config.persistence ?? 'local', default: 'local', type: 'string', }, ], }; } - The `runCheckSetup` helper function that checks whether initial setup has been completed, returning `setupComplete` boolean and the configured `username`.
export async function runCheckSetup(): Promise<CheckSetupOutput> { const stateManager = getStateManager(); return { setupComplete: stateManager.isSetupComplete(), username: stateManager.getState().config.githubUsername, }; } - Type definitions for the setup tool: `SetupOptions` (reset, set), `SetupSetOutput`, `SetupCompleteOutput`, `SetupPrompt`, `SetupRequiredOutput`, and the union type `SetupOutput`.
interface SetupOptions { reset?: boolean; set?: string[]; } export interface SetupSetOutput { success: true; settings: Record<string, string>; warnings?: string[]; } export interface SetupCompleteOutput { setupComplete: true; config: { githubUsername: string; maxActivePRs: number; dormantThresholdDays: number; approachingDormantDays: number; languages: string[]; labels: string[]; projectCategories: ProjectCategory[]; preferredOrgs: string[]; scope: IssueScope[]; persistence: 'local' | 'gist'; }; } export interface SetupPrompt { setting: string; prompt: string; current: string | number | string[] | null; required?: boolean; default?: string | number | string[]; type?: string; } export interface SetupRequiredOutput { setupRequired: true; prompts: SetupPrompt[]; } export type SetupOutput = SetupSetOutput | SetupCompleteOutput | SetupRequiredOutput; - packages/mcp-server/src/tools.ts:308-324 (registration)MCP tool registration for 'setup' – registers the tool with Zod input schema (reset: boolean, set: string[]) and wraps `runSetup` via `wrapTool()`. Also registers 'check-setup' at line 326-336.
// 13. setup — Interactive setup server.registerTool( 'setup', { description: 'Run OSS Autopilot setup to configure preferences like languages, interests, and contribution goals.', inputSchema: { reset: z.boolean().optional().describe('If true, reset all preferences to defaults before running setup'), set: z .array(z.string()) .optional() .describe('Set preferences non-interactively as key=value pairs (e.g. ["languages=typescript,rust"])'), }, annotations: { readOnlyHint: false, destructiveHint: false }, }, wrapTool(runSetup), ); - The `getSetupKeys()` function returns all config keys accepted by the `setup --set` command, filtering the registry for keys with `settableVia === 'setup' || 'both'`.
export function getSetupKeys(): readonly string[] { return CONFIG_KEY_REGISTRY.filter((d) => d.settableVia === 'setup' || d.settableVia === 'both').map((d) => d.key); }