Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/skillkit/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
GuidelineDisableCommand,
GuidelineCreateCommand,
GuidelineRemoveCommand,
TreeCommand,
} from '@skillkit/cli';

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -217,4 +218,6 @@ cli.register(GuidelineDisableCommand);
cli.register(GuidelineCreateCommand);
cli.register(GuidelineRemoveCommand);

cli.register(TreeCommand);

cli.runExit(process.argv.slice(2));
3 changes: 3 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ export {
GuidelineCreateCommand,
GuidelineRemoveCommand,
} from './guideline.js';

// Tree command for hierarchical skill browsing (Phase 21)
export { TreeCommand } from './tree.js';
170 changes: 170 additions & 0 deletions packages/cli/src/commands/recommend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { resolve } from 'node:path';
import {
type ProjectProfile,
type ScoredSkill,
type ExplainedScoredSkill,
ContextManager,
RecommendationEngine,
ReasoningRecommendationEngine,
buildSkillIndex,
saveIndex,
loadIndex as loadIndexFromCache,
Expand Down Expand Up @@ -108,6 +110,21 @@ export class RecommendCommand extends Command {
description: 'Minimal output',
});

// Explain mode (reasoning-based recommendations)
explain = Option.Boolean('--explain,-e', false, {
description: 'Show detailed explanations for recommendations (uses reasoning engine)',
});

// Reasoning mode
reasoning = Option.Boolean('--reasoning,-r', false, {
description: 'Use LLM-based reasoning for recommendations',
});

// Show tree path
showPath = Option.Boolean('--show-path', false, {
description: 'Show category path for each recommendation',
});

async execute(): Promise<number> {
const targetPath = resolve(this.projectPath || process.cwd());

Expand Down Expand Up @@ -139,6 +156,11 @@ export class RecommendCommand extends Command {
return 0;
}

// Use reasoning engine if explain/reasoning flags are set
if (this.explain || this.reasoning || this.showPath) {
return await this.handleReasoningRecommendations(profile, index);
}

// Create recommendation engine
const engine = new RecommendationEngine();
engine.loadIndex(index);
Expand Down Expand Up @@ -168,6 +190,63 @@ export class RecommendCommand extends Command {
return 0;
}

private async handleReasoningRecommendations(
profile: ProjectProfile,
index: { skills: ScoredSkill['skill'][]; sources: { name: string; url: string; lastFetched: string; skillCount: number }[]; version: number; lastUpdated: string }
): Promise<number> {
const s = !this.quiet && !this.json ? spinner() : null;
s?.start('Analyzing with reasoning engine...');

try {
const engine = new ReasoningRecommendationEngine();
engine.loadIndex(index);
await engine.initReasoning();

const result = await engine.recommendWithReasoning(profile, {
limit: this.limit ? parseInt(this.limit, 10) : 10,
minScore: this.minScore ? parseInt(this.minScore, 10) : 30,
categories: this.category,
excludeInstalled: !this.includeInstalled,
includeReasons: this.verbose,
reasoning: this.reasoning,
explainResults: this.explain,
useTree: true,
});

s?.stop('Analysis complete');

if (this.json) {
console.log(JSON.stringify(result, null, 2));
return 0;
}

this.displayExplainedRecommendations(
result.recommendations,
profile,
result.totalSkillsScanned,
result.reasoningSummary
);
return 0;
} catch (err) {
s?.stop(colors.error('Reasoning analysis failed'));
console.log(colors.muted(err instanceof Error ? err.message : String(err)));
console.log(colors.muted('Falling back to standard recommendations...'));
console.log('');

const engine = new RecommendationEngine();
engine.loadIndex(index);
const result = engine.recommend(profile, {
limit: this.limit ? parseInt(this.limit, 10) : 10,
minScore: this.minScore ? parseInt(this.minScore, 10) : 30,
categories: this.category,
excludeInstalled: !this.includeInstalled,
includeReasons: this.verbose,
});
this.displayRecommendations(result.recommendations, profile, result.totalSkillsScanned);
return 0;
}
}

private async getProjectProfile(projectPath: string): Promise<ProjectProfile | null> {
const manager = new ContextManager(projectPath);
let context = manager.get();
Expand Down Expand Up @@ -276,6 +355,97 @@ export class RecommendCommand extends Command {
console.log(colors.muted('Install with: skillkit install <source>'));
}

private displayExplainedRecommendations(
recommendations: ExplainedScoredSkill[],
profile: ProjectProfile,
totalScanned: number,
reasoningSummary?: string
): void {
this.showProjectProfile(profile);

if (reasoningSummary && !this.quiet) {
console.log(colors.dim('Reasoning: ') + colors.muted(reasoningSummary));
console.log('');
}

if (recommendations.length === 0) {
warn('No matching skills found.');
console.log(colors.muted('Try lowering the minimum score with --min-score'));
return;
}

console.log(colors.bold(`Explained Recommendations (${recommendations.length} of ${totalScanned} scanned):`));
console.log('');

for (const rec of recommendations) {
let scoreColor: (text: string) => string;
if (rec.score >= 70) {
scoreColor = colors.success;
} else if (rec.score >= 50) {
scoreColor = colors.warning;
} else {
scoreColor = colors.muted;
}
const scoreBar = progressBar(rec.score, 100, 10);
const qualityScore = rec.skill.quality ?? null;
const qualityDisplay = qualityScore !== null && qualityScore !== undefined
? ` ${formatQualityBadge(qualityScore)}`
: '';

console.log(` ${scoreColor(`${rec.score}%`)} ${colors.dim(scoreBar)} ${colors.bold(rec.skill.name)}${qualityDisplay}`);

if (this.showPath && rec.treePath && rec.treePath.length > 0) {
console.log(` ${colors.accent('Path:')} ${rec.treePath.join(' > ')}`);
}

if (rec.skill.description) {
console.log(` ${colors.muted(truncate(rec.skill.description, 70))}`);
}

if (rec.skill.source) {
console.log(` ${colors.dim('Source:')} ${rec.skill.source}`);
}

if (this.explain && rec.explanation) {
console.log(colors.dim(' Why this skill:'));
if (rec.explanation.matchedBecause.length > 0) {
console.log(` ${colors.success('├─')} Matched: ${rec.explanation.matchedBecause.join(', ')}`);
}
if (rec.explanation.relevantFor.length > 0) {
console.log(` ${colors.accent('├─')} Relevant for: ${rec.explanation.relevantFor.join(', ')}`);
}
if (rec.explanation.confidence) {
const confidenceColor = rec.explanation.confidence === 'high' ? colors.success :
rec.explanation.confidence === 'medium' ? colors.warning :
colors.muted;
console.log(` ${colors.dim('└─')} Confidence: ${confidenceColor(rec.explanation.confidence)}`);
}
}

if (this.verbose && rec.reasons.length > 0) {
console.log(colors.dim(' Score breakdown:'));
for (const reason of rec.reasons.filter(r => r.weight > 0)) {
console.log(` ${colors.muted(symbols.stepActive)} ${reason.description} (+${reason.weight})`);
}
if (qualityScore !== null && qualityScore !== undefined) {
const grade = getQualityGradeFromScore(qualityScore);
console.log(` ${colors.muted(symbols.stepActive)} Quality: ${qualityScore}/100 (${grade})`);
}
}

if (rec.warnings.length > 0) {
for (const warning of rec.warnings) {
console.log(` ${colors.warning(symbols.warning)} ${warning}`);
}
}

console.log('');
}

console.log(colors.muted('Install with: skillkit install <source>'));
console.log(colors.muted('More details: skillkit recommend --explain --verbose'));
}

private handleSearch(engine: RecommendationEngine, query: string): number {
if (!this.quiet && !this.json) {
header(`Search: "${query}"`);
Expand Down
Loading