claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.
364 lines (313 loc) • 10.1 kB
text/typescript
/**
* CFN Skill Loader
* High-performance contextual skill loading with LRU caching and SHA256 validation
*/
import { createHash } from 'crypto';
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
import { join, dirname, basename } from 'path';
import type { DatabaseService } from '../lib/database-service.js';
import type { Logger } from '../../lib/propagation/src/types.js';
export interface SkillLoadOptions {
agentType: string;
taskContext: string[];
maxSkills?: number;
includeBootstrap?: boolean;
phase?: 'loop1' | 'loop2' | 'loop3';
cacheKey?: string;
}
export interface SkillLoadResult {
skills: Array<{
name: string;
content: string;
metadata: SkillMetadata;
hash: string;
}>;
totalSkills: number;
loadTimeMs: number;
cacheHitCount: number;
cacheMissCount: number;
skippedCount: number;
}
export interface SkillMetadata {
name: string;
description: string;
version: string;
tags: string[];
status: 'active' | 'deprecated' | 'experimental';
namespace?: string;
priority?: number;
dependencies?: string[];
agentTypes?: string[];
phases?: string[];
lastModified: string;
contentHash?: string;
size: number;
}
export interface CacheEntry {
skill: any;
hash: string;
lastAccessed: number;
loadCount: number;
}
export class SkillLoader {
private cache = new Map<string, CacheEntry>();
private readonly maxCacheSize = 100;
private readonly skillsPath = '.claude/skills';
constructor(
private db: DatabaseService,
private logger: Logger
) {}
/**
* Load skills based on agent type and task context
*/
async loadContextualSkills(options: SkillLoadOptions): Promise<SkillLoadResult> {
const startTime = Date.now();
const result: SkillLoadResult = {
skills: [],
totalSkills: 0,
loadTimeMs: 0,
cacheHitCount: 0,
cacheMissCount: 0,
skippedCount: 0
};
try {
// Get available skills from database
const availableSkills = await this.db.getSkillsForAgent(options.agentType);
result.totalSkills = availableSkills.length;
// Filter and load relevant skills
for (const skillMeta of availableSkills) {
// Check relevance based on task context and phase
if (!this.isSkillRelevant(skillMeta, options)) {
result.skippedCount++;
continue;
}
// Try cache first
const cacheKey = `${skillMeta.name}:${skillMeta.version}`;
const cached = this.cache.get(cacheKey);
if (cached && this.validateSkillHash(skillMeta, cached.hash)) {
result.skills.push(cached.skill);
result.cacheHitCount++;
this.updateCacheAccess(cacheKey);
} else {
// Load from disk
const skill = await this.loadSkillFromDisk(skillMeta);
if (skill) {
result.skills.push(skill);
result.cacheMissCount++;
this.cache.set(cacheKey, {
skill,
hash: skill.hash,
lastAccessed: Date.now(),
loadCount: 1
});
}
}
// Respect maxSkills limit
if (options.maxSkills && result.skills.length >= options.maxSkills) {
break;
}
}
// Clean up old cache entries
this.cleanupCache();
result.loadTimeMs = Date.now() - startTime;
this.logger.debug(`Loaded ${result.skills.length} skills for ${options.agentType} in ${result.loadTimeMs}ms`);
return result;
} catch (error) {
this.logger.error('Failed to load contextual skills', error as Error);
throw error;
}
}
/**
* Load a specific skill by name
*/
async loadSkillByName(skillName: string): Promise<any | null> {
try {
const skillMeta = await this.db.getSkillByName(skillName);
if (!skillMeta) {
return null;
}
const cacheKey = `${skillMeta.name}:${skillMeta.version}`;
const cached = this.cache.get(cacheKey);
if (cached && this.validateSkillHash(skillMeta, cached.hash)) {
this.updateCacheAccess(cacheKey);
return cached.skill;
}
const skill = await this.loadSkillFromDisk(skillMeta);
if (skill) {
this.cache.set(cacheKey, {
skill,
hash: skill.hash,
lastAccessed: Date.now(),
loadCount: 1
});
}
return skill;
} catch (error) {
this.logger.error(`Failed to load skill: ${skillName}`, error as Error);
return null;
}
}
/**
* Validate skill content integrity using SHA256 hash
*/
validateSkillHash(metadata: SkillMetadata, expectedHash: string): boolean {
try {
if (!metadata.contentHash) {
return false;
}
return metadata.contentHash === expectedHash;
} catch (error) {
this.logger.warn(`Hash validation failed for ${metadata.name}`, error as Error);
return false;
}
}
/**
* Clear the skill cache
*/
clearCache(): void {
this.cache.clear();
this.logger.debug('Skill cache cleared');
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
size: this.cache.size,
maxSize: this.maxCacheSize,
entries: Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
loadCount: entry.loadCount,
lastAccessed: entry.lastAccessed
}))
};
}
private isSkillRelevant(skill: SkillMetadata, options: SkillLoadOptions): boolean {
// Check agent type compatibility
if (skill.agentTypes && !skill.agentTypes.includes(options.agentType)) {
return false;
}
// Check phase compatibility
if (options.phase && skill.phases && !skill.phases.includes(options.phase)) {
return false;
}
// Check task context relevance
if (options.taskContext.length > 0 && skill.tags.length > 0) {
const hasRelevantTag = skill.tags.some(tag =>
options.taskContext.some(context =>
context.toLowerCase().includes(tag.toLowerCase()) ||
tag.toLowerCase().includes(context.toLowerCase())
)
);
if (!hasRelevantTag) {
return false;
}
}
// Check status
if (skill.status === 'deprecated' && !options.includeBootstrap) {
return false;
}
return true;
}
private async loadSkillFromDisk(metadata: SkillMetadata): Promise<any | null> {
try {
const skillPath = this.findSkillPath(metadata.name);
if (!skillPath || !existsSync(skillPath)) {
this.logger.warn(`Skill file not found: ${metadata.name}`);
return null;
}
const content = readFileSync(skillPath, 'utf8');
const hash = this.calculateHash(content);
// Extract skill content from markdown
const skillContent = this.extractSkillContent(content);
return {
name: metadata.name,
content: skillContent,
metadata,
hash
};
} catch (error) {
this.logger.error(`Failed to load skill from disk: ${metadata.name}`, error as Error);
return null;
}
}
private findSkillPath(skillName: string): string | null {
const possiblePaths = [
join(this.skillsPath, skillName, 'SKILL.md'),
join(this.skillsPath, `cfn-${skillName}`, 'SKILL.md'),
join(this.skillsPath, skillName + '.md'),
join(this.skillsPath, `cfn-${skillName}.md`)
];
for (const path of possiblePaths) {
if (existsSync(path)) {
return path;
}
}
return null;
}
private extractSkillContent(markdownContent: string): string {
// Remove frontmatter
const withoutFrontmatter = markdownContent.replace(/^---\n.*?\n---\n/s, '');
// Extract only the main content, remove metadata sections
const lines = withoutFrontmatter.split('\n');
const contentLines: string[] = [];
let skipSection = false;
for (const line of lines) {
// Skip common metadata sections
if (line.startsWith('## ') && (
line.includes('Version History') ||
line.includes('Dependencies') ||
line.includes('Related Skills') ||
line.includes('Tags')
)) {
skipSection = true;
continue;
}
if (skipSection && line.startsWith('## ')) {
skipSection = false;
}
if (!skipSection) {
contentLines.push(line);
}
}
return contentLines.join('\n').trim();
}
private calculateHash(content: string): string {
return createHash('sha256').update(content).digest('hex');
}
private updateCacheAccess(key: string): void {
const entry = this.cache.get(key);
if (entry) {
entry.lastAccessed = Date.now();
entry.loadCount++;
}
}
private cleanupCache(): void {
if (this.cache.size <= this.maxCacheSize) {
return;
}
// Sort by last accessed time (LRU)
const entries = Array.from(this.cache.entries())
.sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed);
// Remove oldest entries
const toRemove = entries.slice(0, entries.length - this.maxCacheSize);
for (const [key] of toRemove) {
this.cache.delete(key);
}
this.logger.debug(`Cache cleanup: removed ${toRemove.length} entries`);
}
}
// Global instance for convenience
let globalLoader: SkillLoader | null = null;
export function getGlobalLoader(db?: DatabaseService, logger?: Logger): SkillLoader {
if (!globalLoader) {
if (!db || !logger) {
throw new Error('Database service and logger required for first-time initialization');
}
globalLoader = new SkillLoader(db, logger);
}
return globalLoader;
}
export function resetGlobalLoader(): void {
globalLoader = null;
}