Skip to content

Commit 8121c2b

Browse files
authored
feat(core): add PageIndex-inspired patterns for skill discovery (#37)
* feat(core): add PageIndex-inspired patterns for skill discovery Implements patterns from anthropics/knowledge-work-plugins: Tree Module (packages/core/src/tree/): - Hierarchical skill taxonomy with 12 categories - Tree generator from skill tags and metadata - Tree serializer for JSON/Markdown export - Related skills graph with 4 relation types Reasoning Engine (packages/core/src/reasoning/): - LLM-based tree traversal for skill discovery - Explainable recommendations with reasoning chains - Search planning with category relevance scoring - Result caching with 5-minute TTL Connectors (packages/core/src/connectors/): - Tool-agnostic ~~ placeholder system - 13 connector categories (crm, chat, email, etc.) - Auto-suggest mappings from MCP config - Placeholder detection and replacement utilities Execution Flow (packages/core/src/execution/): - Step-by-step execution tracking with metrics - Automatic retry with exponential backoff - Standalone vs Enhanced mode detection - Capability-based feature gating CLI Integration: - New `skillkit tree` command with --generate, --stats flags - Enhanced `skillkit recommend` with --explain, --reasoning flags TUI Integration: - Tree view mode in Marketplace (toggle with 'v') - Arrow key navigation for tree hierarchy * fix: address CodeRabbit and Devin review feedback - Use os.homedir() instead of process.env.HOME || '~' fallback (tree.ts, tree.service.ts, mode.ts) - Add guard for division by zero in tree stats percentage - Validate parseInt result for --depth flag - Fix average stats calculation to use cacheMisses count - Extract category from data in validateCategoryScore - Remove unused skillMap field from TreeGenerator - Make reasoning provider configurable instead of hardcoded - Add try/catch for tree loading errors in Marketplace - Handle missing tree nodes during traversal * fix(execution): clear timeout timer to prevent resource leak Store setTimeout ID and clear it in finally block when step completes successfully before timeout. This prevents orphaned timers from accumulating during long-running applications.
1 parent 3565daa commit 8121c2b

File tree

28 files changed

+4285
-88
lines changed

28 files changed

+4285
-88
lines changed

apps/skillkit/src/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import {
9797
GuidelineDisableCommand,
9898
GuidelineCreateCommand,
9999
GuidelineRemoveCommand,
100+
TreeCommand,
100101
} from '@skillkit/cli';
101102

102103
const __filename = fileURLToPath(import.meta.url);
@@ -217,4 +218,6 @@ cli.register(GuidelineDisableCommand);
217218
cli.register(GuidelineCreateCommand);
218219
cli.register(GuidelineRemoveCommand);
219220

221+
cli.register(TreeCommand);
222+
220223
cli.runExit(process.argv.slice(2));

packages/cli/src/commands/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,6 @@ export {
105105
GuidelineCreateCommand,
106106
GuidelineRemoveCommand,
107107
} from './guideline.js';
108+
109+
// Tree command for hierarchical skill browsing (Phase 21)
110+
export { TreeCommand } from './tree.js';

packages/cli/src/commands/recommend.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { resolve } from 'node:path';
33
import {
44
type ProjectProfile,
55
type ScoredSkill,
6+
type ExplainedScoredSkill,
67
ContextManager,
78
RecommendationEngine,
9+
ReasoningRecommendationEngine,
810
buildSkillIndex,
911
saveIndex,
1012
loadIndex as loadIndexFromCache,
@@ -108,6 +110,21 @@ export class RecommendCommand extends Command {
108110
description: 'Minimal output',
109111
});
110112

113+
// Explain mode (reasoning-based recommendations)
114+
explain = Option.Boolean('--explain,-e', false, {
115+
description: 'Show detailed explanations for recommendations (uses reasoning engine)',
116+
});
117+
118+
// Reasoning mode
119+
reasoning = Option.Boolean('--reasoning,-r', false, {
120+
description: 'Use LLM-based reasoning for recommendations',
121+
});
122+
123+
// Show tree path
124+
showPath = Option.Boolean('--show-path', false, {
125+
description: 'Show category path for each recommendation',
126+
});
127+
111128
async execute(): Promise<number> {
112129
const targetPath = resolve(this.projectPath || process.cwd());
113130

@@ -139,6 +156,11 @@ export class RecommendCommand extends Command {
139156
return 0;
140157
}
141158

159+
// Use reasoning engine if explain/reasoning flags are set
160+
if (this.explain || this.reasoning || this.showPath) {
161+
return await this.handleReasoningRecommendations(profile, index);
162+
}
163+
142164
// Create recommendation engine
143165
const engine = new RecommendationEngine();
144166
engine.loadIndex(index);
@@ -168,6 +190,63 @@ export class RecommendCommand extends Command {
168190
return 0;
169191
}
170192

193+
private async handleReasoningRecommendations(
194+
profile: ProjectProfile,
195+
index: { skills: ScoredSkill['skill'][]; sources: { name: string; url: string; lastFetched: string; skillCount: number }[]; version: number; lastUpdated: string }
196+
): Promise<number> {
197+
const s = !this.quiet && !this.json ? spinner() : null;
198+
s?.start('Analyzing with reasoning engine...');
199+
200+
try {
201+
const engine = new ReasoningRecommendationEngine();
202+
engine.loadIndex(index);
203+
await engine.initReasoning();
204+
205+
const result = await engine.recommendWithReasoning(profile, {
206+
limit: this.limit ? parseInt(this.limit, 10) : 10,
207+
minScore: this.minScore ? parseInt(this.minScore, 10) : 30,
208+
categories: this.category,
209+
excludeInstalled: !this.includeInstalled,
210+
includeReasons: this.verbose,
211+
reasoning: this.reasoning,
212+
explainResults: this.explain,
213+
useTree: true,
214+
});
215+
216+
s?.stop('Analysis complete');
217+
218+
if (this.json) {
219+
console.log(JSON.stringify(result, null, 2));
220+
return 0;
221+
}
222+
223+
this.displayExplainedRecommendations(
224+
result.recommendations,
225+
profile,
226+
result.totalSkillsScanned,
227+
result.reasoningSummary
228+
);
229+
return 0;
230+
} catch (err) {
231+
s?.stop(colors.error('Reasoning analysis failed'));
232+
console.log(colors.muted(err instanceof Error ? err.message : String(err)));
233+
console.log(colors.muted('Falling back to standard recommendations...'));
234+
console.log('');
235+
236+
const engine = new RecommendationEngine();
237+
engine.loadIndex(index);
238+
const result = engine.recommend(profile, {
239+
limit: this.limit ? parseInt(this.limit, 10) : 10,
240+
minScore: this.minScore ? parseInt(this.minScore, 10) : 30,
241+
categories: this.category,
242+
excludeInstalled: !this.includeInstalled,
243+
includeReasons: this.verbose,
244+
});
245+
this.displayRecommendations(result.recommendations, profile, result.totalSkillsScanned);
246+
return 0;
247+
}
248+
}
249+
171250
private async getProjectProfile(projectPath: string): Promise<ProjectProfile | null> {
172251
const manager = new ContextManager(projectPath);
173252
let context = manager.get();
@@ -276,6 +355,97 @@ export class RecommendCommand extends Command {
276355
console.log(colors.muted('Install with: skillkit install <source>'));
277356
}
278357

358+
private displayExplainedRecommendations(
359+
recommendations: ExplainedScoredSkill[],
360+
profile: ProjectProfile,
361+
totalScanned: number,
362+
reasoningSummary?: string
363+
): void {
364+
this.showProjectProfile(profile);
365+
366+
if (reasoningSummary && !this.quiet) {
367+
console.log(colors.dim('Reasoning: ') + colors.muted(reasoningSummary));
368+
console.log('');
369+
}
370+
371+
if (recommendations.length === 0) {
372+
warn('No matching skills found.');
373+
console.log(colors.muted('Try lowering the minimum score with --min-score'));
374+
return;
375+
}
376+
377+
console.log(colors.bold(`Explained Recommendations (${recommendations.length} of ${totalScanned} scanned):`));
378+
console.log('');
379+
380+
for (const rec of recommendations) {
381+
let scoreColor: (text: string) => string;
382+
if (rec.score >= 70) {
383+
scoreColor = colors.success;
384+
} else if (rec.score >= 50) {
385+
scoreColor = colors.warning;
386+
} else {
387+
scoreColor = colors.muted;
388+
}
389+
const scoreBar = progressBar(rec.score, 100, 10);
390+
const qualityScore = rec.skill.quality ?? null;
391+
const qualityDisplay = qualityScore !== null && qualityScore !== undefined
392+
? ` ${formatQualityBadge(qualityScore)}`
393+
: '';
394+
395+
console.log(` ${scoreColor(`${rec.score}%`)} ${colors.dim(scoreBar)} ${colors.bold(rec.skill.name)}${qualityDisplay}`);
396+
397+
if (this.showPath && rec.treePath && rec.treePath.length > 0) {
398+
console.log(` ${colors.accent('Path:')} ${rec.treePath.join(' > ')}`);
399+
}
400+
401+
if (rec.skill.description) {
402+
console.log(` ${colors.muted(truncate(rec.skill.description, 70))}`);
403+
}
404+
405+
if (rec.skill.source) {
406+
console.log(` ${colors.dim('Source:')} ${rec.skill.source}`);
407+
}
408+
409+
if (this.explain && rec.explanation) {
410+
console.log(colors.dim(' Why this skill:'));
411+
if (rec.explanation.matchedBecause.length > 0) {
412+
console.log(` ${colors.success('├─')} Matched: ${rec.explanation.matchedBecause.join(', ')}`);
413+
}
414+
if (rec.explanation.relevantFor.length > 0) {
415+
console.log(` ${colors.accent('├─')} Relevant for: ${rec.explanation.relevantFor.join(', ')}`);
416+
}
417+
if (rec.explanation.confidence) {
418+
const confidenceColor = rec.explanation.confidence === 'high' ? colors.success :
419+
rec.explanation.confidence === 'medium' ? colors.warning :
420+
colors.muted;
421+
console.log(` ${colors.dim('└─')} Confidence: ${confidenceColor(rec.explanation.confidence)}`);
422+
}
423+
}
424+
425+
if (this.verbose && rec.reasons.length > 0) {
426+
console.log(colors.dim(' Score breakdown:'));
427+
for (const reason of rec.reasons.filter(r => r.weight > 0)) {
428+
console.log(` ${colors.muted(symbols.stepActive)} ${reason.description} (+${reason.weight})`);
429+
}
430+
if (qualityScore !== null && qualityScore !== undefined) {
431+
const grade = getQualityGradeFromScore(qualityScore);
432+
console.log(` ${colors.muted(symbols.stepActive)} Quality: ${qualityScore}/100 (${grade})`);
433+
}
434+
}
435+
436+
if (rec.warnings.length > 0) {
437+
for (const warning of rec.warnings) {
438+
console.log(` ${colors.warning(symbols.warning)} ${warning}`);
439+
}
440+
}
441+
442+
console.log('');
443+
}
444+
445+
console.log(colors.muted('Install with: skillkit install <source>'));
446+
console.log(colors.muted('More details: skillkit recommend --explain --verbose'));
447+
}
448+
279449
private handleSearch(engine: RecommendationEngine, query: string): number {
280450
if (!this.quiet && !this.json) {
281451
header(`Search: "${query}"`);

0 commit comments

Comments
 (0)