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.
313 lines (277 loc) • 9.32 kB
text/typescript
/**
* CFN Database Service
* Provides database operations for skill management
*/
import { Database } from 'sqlite3';
import { open } from 'sqlite';
import type { Logger } from '../propagation/src/types.js';
export interface SkillRecord {
id: number;
name: string;
version: string;
namespace?: string;
description: string;
status: 'active' | 'deprecated' | 'experimental';
tags: string;
agent_types?: string;
phases?: string;
priority?: number;
dependencies?: string;
content_hash?: string;
file_path: string;
file_size: number;
last_modified: string;
created_at: string;
updated_at: string;
}
export interface SkillFilter {
agentType?: string;
phase?: string;
tags?: string[];
status?: string;
namespace?: string;
}
export class DatabaseService {
private db: any = null;
private initialized = false;
constructor(
private dbPath: string,
private logger: Logger
) {}
/**
* Initialize database connection and create tables if needed
*/
async initialize(): Promise<void> {
if (this.initialized) {
return;
}
try {
this.db = await open({
filename: this.dbPath,
driver: Database
});
await this.createTables();
this.initialized = true;
this.logger.info(`Database initialized: ${this.dbPath}`);
} catch (error) {
this.logger.error('Failed to initialize database', error as Error);
throw error;
}
}
/**
* Close database connection
*/
async close(): Promise<void> {
if (this.db) {
await this.db.close();
this.db = null;
this.initialized = false;
this.logger.info('Database connection closed');
}
}
/**
* Get all skills suitable for a specific agent type
*/
async getSkillsForAgent(agentType: string, filters?: SkillFilter): Promise<SkillRecord[]> {
await this.ensureInitialized();
try {
let query = `
SELECT * FROM skills
WHERE status = 'active'
AND (agent_types IS NULL OR agent_types LIKE '%' || ? || '%')
`;
const params: any[] = [agentType];
if (filters?.phase) {
query += ` AND (phases IS NULL OR phases LIKE '%' || ? || '%')`;
params.push(filters.phase);
}
if (filters?.tags && filters.tags.length > 0) {
const tagConditions = filters.tags.map(() => `tags LIKE '%' || ? || '%'`).join(' OR ');
query += ` AND (${tagConditions})`;
params.push(...filters.tags);
}
if (filters?.namespace) {
query += ` AND namespace = ?`;
params.push(filters.namespace);
}
query += ` ORDER BY priority DESC, name ASC`;
const skills = await this.db.all(query, params);
return skills.map(this.mapRowToSkill);
} catch (error) {
this.logger.error(`Failed to get skills for agent: ${agentType}`, error as Error);
return [];
}
}
/**
* Get a specific skill by name
*/
async getSkillByName(name: string): Promise<SkillRecord | null> {
await this.ensureInitialized();
try {
const row = await this.db.get(
'SELECT * FROM skills WHERE name = ? ORDER BY version DESC LIMIT 1',
[name]
);
return row ? this.mapRowToSkill(row) : null;
} catch (error) {
this.logger.error(`Failed to get skill by name: ${name}`, error as Error);
return null;
}
}
/**
* Update or insert a skill record
*/
async upsertSkill(skill: Partial<SkillRecord>): Promise<void> {
await this.ensureInitialized();
try {
const existing = await this.db.get(
'SELECT id FROM skills WHERE name = ? AND version = ?',
[skill.name, skill.version]
);
if (existing) {
await this.db.run(`
UPDATE skills SET
description = ?, status = ?, tags = ?, agent_types = ?, phases = ?,
priority = ?, dependencies = ?, content_hash = ?, file_path = ?,
file_size = ?, last_modified = ?, updated_at = datetime('now')
WHERE name = ? AND version = ?
`, [
skill.description, skill.status, skill.tags, skill.agent_types, skill.phases,
skill.priority, skill.dependencies, skill.content_hash, skill.file_path,
skill.file_size, skill.last_modified, skill.name, skill.version
]);
} else {
await this.db.run(`
INSERT INTO skills (
name, version, namespace, description, status, tags, agent_types, phases,
priority, dependencies, content_hash, file_path, file_size, last_modified,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, [
skill.name, skill.version, skill.namespace, skill.description, skill.status,
skill.tags, skill.agent_types, skill.phases, skill.priority, skill.dependencies,
skill.content_hash, skill.file_path, skill.file_size, skill.last_modified
]);
}
this.logger.debug(`Skill upserted: ${skill.name} v${skill.version}`);
} catch (error) {
this.logger.error(`Failed to upsert skill: ${skill.name}`, error as Error);
throw error;
}
}
/**
* Delete a skill by name and version
*/
async deleteSkill(name: string, version?: string): Promise<boolean> {
await this.ensureInitialized();
try {
let query = 'DELETE FROM skills WHERE name = ?';
const params: any[] = [name];
if (version) {
query += ' AND version = ?';
params.push(version);
}
const result = await this.db.run(query, params);
const deleted = result.changes > 0;
if (deleted) {
this.logger.debug(`Skill deleted: ${name}${version ? ` v${version}` : ''}`);
}
return deleted;
} catch (error) {
this.logger.error(`Failed to delete skill: ${name}`, error as Error);
return false;
}
}
/**
* Get skill statistics
*/
async getStatistics(): Promise<{
totalSkills: number;
activeSkills: number;
deprecatedSkills: number;
experimentalSkills: number;
namespaces: string[];
}> {
await this.ensureInitialized();
try {
const [total, active, deprecated, experimental, namespaces] = await Promise.all([
this.db.get('SELECT COUNT(*) as count FROM skills'),
this.db.get('SELECT COUNT(*) as count FROM skills WHERE status = ?', ['active']),
this.db.get('SELECT COUNT(*) as count FROM skills WHERE status = ?', ['deprecated']),
this.db.get('SELECT COUNT(*) as count FROM skills WHERE status = ?', ['experimental']),
this.db.all('SELECT DISTINCT namespace FROM skills WHERE namespace IS NOT NULL')
]);
return {
totalSkills: total.count,
activeSkills: active.count,
deprecatedSkills: deprecated.count,
experimentalSkills: experimental.count,
namespaces: namespaces.map((row: any) => row.namespace)
};
} catch (error) {
this.logger.error('Failed to get statistics', error as Error);
return {
totalSkills: 0,
activeSkills: 0,
deprecatedSkills: 0,
experimentalSkills: 0,
namespaces: []
};
}
}
private async ensureInitialized(): Promise<void> {
if (!this.initialized) {
await this.initialize();
}
}
private async createTables(): Promise<void> {
await this.db.exec(`
CREATE TABLE IF NOT EXISTS skills (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
version TEXT NOT NULL,
namespace TEXT,
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
tags TEXT,
agent_types TEXT,
phases TEXT,
priority INTEGER DEFAULT 0,
dependencies TEXT,
content_hash TEXT,
file_path TEXT,
file_size INTEGER,
last_modified TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(name, version)
);
CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
CREATE INDEX IF NOT EXISTS idx_skills_status ON skills(status);
CREATE INDEX IF NOT EXISTS idx_skills_namespace ON skills(namespace);
CREATE INDEX IF NOT EXISTS idx_skills_agent_types ON skills(agent_types);
CREATE INDEX IF NOT EXISTS idx_skills_priority ON skills(priority);
`);
}
private mapRowToSkill(row: any): SkillRecord {
return {
id: row.id,
name: row.name,
version: row.version,
namespace: row.namespace,
description: row.description,
status: row.status,
tags: row.tags || '',
agent_types: row.agent_types,
phases: row.phases,
priority: row.priority,
dependencies: row.dependencies,
content_hash: row.content_hash,
file_path: row.file_path,
file_size: row.file_size,
last_modified: row.last_modified,
created_at: row.created_at,
updated_at: row.updated_at
};
}
}