Reload skills
skills__reloadRescan all configured skill folders and return added/removed skills along with per-file errors. Optionally validate a specific folder is configured.
Instructions
Force a full rescan of all configured folders, returning the diff (added/removed) and any per-file errors. Pass an optional folder name to validate it is currently configured (the rescan itself remains global).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| folder | No |
Implementation Reference
- src/tools/reload.ts:21-58 (handler)The handleReload function that implements the reload logic: validates optional folder argument, captures before/after registry state, invalidates metadata cache, calls rebuildRegistry, and returns diff (added/removed) plus errors.
export async function handleReload( deps: ServerDeps, args: { folder?: string }, ): Promise<ReloadResult> { try { if (args.folder !== undefined) { const absolute = resolvePath(args.folder); // Why: reload always rebuilds the FULL registry (every configured folder), not just // the named one. Validating presence preserves the API contract for callers without // forcing a partial-scan code path. Partial reload deferred — would require splitting // the conflict-resolver logic, not worth the complexity in v0.x. if (!deps.folders.includes(absolute)) { throw new Error(`reload: folder "${args.folder}" is not currently configured`); } } const before = new Set(deps.registry.getAll().map((s) => s.name)); deps.metadataCache.invalidate(); const errorSink: Array<{ path: string; message: string }> = []; const stats = await rebuildRegistry(deps, { errorSink }); const added = stats.skills.filter((n) => !before.has(n)); const removed = [...before].filter((n) => !stats.skills.includes(n)).sort(); return { loaded: stats.skills.length, added, removed, errors: stats.errors, }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (msg.startsWith('reload: ')) throw err; throw new Error(`reload: ${msg}`); } } - src/tools/reload.ts:6-8 (schema)Input schema for the reload tool: an optional 'folder' string parameter defined using Zod.
export const reloadInputSchema = { folder: z.string().optional(), } as const; - src/tools/reload.ts:10-19 (schema)ReloadResult interface defining the shape of the reload response: loaded count, added skill names, removed skill names, and per-file errors.
export interface ReloadResult { /** Total skills in the registry after reload. */ loaded: number; /** Skill names present after reload but not before. */ added: string[]; /** Skill names present before reload but not after. */ removed: string[]; /** Per-file errors collected during the rebuild. */ errors: Array<{ path: string; message: string }>; } - src/tools/loader.ts:20-103 (helper)The rebuildRegistry helper function called by handleReload. Clears the registry and content cache, rescans all configured folders, parses files, applies blacklist filter, resolves conflicts, and returns the list of skills and any errors.
export async function rebuildRegistry(deps: ServerDeps, opts?: RebuildOptions): Promise<RebuildStats> { const errorSink = opts?.errorSink; deps.registry.clear(); deps.contentCache.clear(); // Collect all candidates grouped by skill name for conflict resolution. const candidates = new Map<string, SkillMetadata[]>(); for (const folder of deps.folders) { let filePaths: string[]; try { filePaths = await deps.scanner.scan(folder); } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (errorSink !== undefined) { errorSink.push({ path: folder, message: msg }); } else { console.error(`[skillforge] skipped folder ${folder}: ${msg}`); } continue; } for (const filePath of filePaths) { let content; try { content = await deps.parser.parseFile(filePath, folder); } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (errorSink !== undefined) { errorSink.push({ path: filePath, message: msg }); } else { console.error(`[skillforge] skipped ${filePath}: ${msg}`); } continue; } // Derive metadata snapshot (strip body + raw). const { body: _body, raw: _raw, ...metadata } = content; const meta: SkillMetadata = metadata; const verdict = deps.blacklistFilter.evaluate(content); if (!verdict.allowed) { const detail = verdict.reason === 'manual' ? 'blacklisted by name' : `audit hit: ${verdict.pattern}`; // Blacklist rejections are routine exclusions — always log to stderr, never sink. console.error(`[skillforge] excluded "${meta.name}" from ${filePath} — ${detail}`); continue; } const existing = candidates.get(meta.name) ?? []; existing.push(meta); candidates.set(meta.name, existing); // Store full content so resolve winner can be cached below. // We keep all candidates' content; winner selection happens after. deps.contentCache.set(meta.name + '\x00' + folder, content); } } // Resolve conflicts and register winners. for (const [name, group] of candidates) { const winner = deps.resolver.resolve(group, deps.folders); deps.registry.register(winner); // Retrieve the full content for the winner from the temporary keyed cache. const tempKey = name + '\x00' + winner.folder; const winnerContent = deps.contentCache.get(tempKey); if (winnerContent !== undefined) { deps.contentCache.set(name, winnerContent); } // Clean up temp keys (all candidates for this name). for (const candidate of group) { deps.contentCache.invalidate(name + '\x00' + candidate.folder); } } deps.metadataCache.markFresh(); const skills = deps.registry.getAll().map((s) => s.name).sort(); const errors = errorSink ?? []; return { skills, errors }; }