UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

530 lines (529 loc) 15.5 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../../../core/monitoring/logger.js"; function getEnv(key, defaultValue) { const value = process.env[key]; if (value === void 0) { if (defaultValue !== void 0) return defaultValue; throw new Error(`Environment variable ${key} is required`); } return value; } function getOptionalEnv(key) { return process.env[key]; } import { getSkillStorage, getDefaultUserId } from "../../../core/skills/index.js"; class SkillHandlers { constructor(redisUrl, userId) { this.redisUrl = redisUrl; this.userId = userId || getDefaultUserId(); } skillStorage = null; userId; /** * Lazy initialization of skill storage */ getStorage() { if (!this.skillStorage) { const url = this.redisUrl || process.env["REDIS_URL"]; if (!url) { throw new Error("REDIS_URL not configured for skill storage"); } this.skillStorage = getSkillStorage({ redisUrl: url, userId: this.userId }); } return this.skillStorage; } /** * Get current user ID */ getUserId() { return this.userId; } /** * Check if skill storage is available */ isAvailable() { return !!(this.redisUrl || process.env["REDIS_URL"]); } // ============================================================ // SKILL OPERATIONS // ============================================================ /** * Record a new skill/learning */ async recordSkill(args, context) { try { const storage = this.getStorage(); const skill = await storage.createSkill({ content: args.content, category: args.category, priority: args.priority || "medium", tool: args.tool, tags: args.tags || [], source: args.source || "observation", sessionId: context.sessionId }); logger.info("Recorded skill via MCP", { skillId: skill.id, category: skill.category }); return { success: true, skill }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to record skill", { error: message }); return { success: false, error: message }; } } /** * Get relevant skills for current context */ async getRelevantSkills(args) { try { const storage = this.getStorage(); const skills = await storage.getRelevantSkills({ tool: args.tool, language: args.language, framework: args.framework, tags: args.tags }); const limited = args.limit ? skills.slice(0, args.limit) : skills; return { success: true, skills: limited }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to get relevant skills", { error: message }); return { success: false, error: message }; } } /** * Query skills with filters */ async querySkills(args) { try { const storage = this.getStorage(); const query = { categories: args.categories, priorities: args.priorities, tool: args.tool, tags: args.tags, minValidatedCount: args.minValidatedCount, limit: args.limit || 50, offset: 0, sortBy: args.sortBy || "priority", sortOrder: "desc" }; const skills = await storage.querySkills(query); return { success: true, skills, total: skills.length }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to query skills", { error: message }); return { success: false, error: message }; } } /** * Validate/reinforce a skill */ async validateSkill(args) { try { const storage = this.getStorage(); const skill = await storage.validateSkill(args.skill_id); if (!skill) { return { success: false, error: "Skill not found" }; } return { success: true, skill }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to validate skill", { error: message }); return { success: false, error: message }; } } /** * Update a skill */ async updateSkill(args) { try { const storage = this.getStorage(); const skill = await storage.updateSkill({ id: args.skill_id, content: args.content, priority: args.priority, tags: args.tags }); if (!skill) { return { success: false, error: "Skill not found" }; } return { success: true, skill }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to update skill", { error: message }); return { success: false, error: message }; } } /** * Delete a skill */ async deleteSkill(args) { try { const storage = this.getStorage(); const deleted = await storage.deleteSkill(args.skill_id); if (!deleted) { return { success: false, error: "Skill not found" }; } return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to delete skill", { error: message }); return { success: false, error: message }; } } // ============================================================ // SESSION JOURNAL OPERATIONS // ============================================================ /** * Record a journal entry */ async recordJournalEntry(args, context) { try { const storage = this.getStorage(); const sessionId = context.sessionId || "default"; const entry = await storage.createJournalEntry( sessionId, args.type, args.title, args.content, { tool: args.tool, file: args.file } ); return { success: true, entryId: entry.id }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to record journal entry", { error: message }); return { success: false, error: message }; } } /** * Get session journal */ async getSessionJournal(args, context) { try { const storage = this.getStorage(); const sessionId = args.session_id || context.sessionId || "default"; const entries = await storage.getSessionJournal(sessionId); return { success: true, entries }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to get session journal", { error: message }); return { success: false, error: message }; } } /** * Promote a journal entry to a skill */ async promoteToSkill(args) { try { const storage = this.getStorage(); const skill = await storage.promoteToSkill( args.entry_id, args.category, args.priority || "medium" ); if (!skill) { return { success: false, error: "Journal entry not found" }; } return { success: true, skill }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to promote journal entry", { error: message }); return { success: false, error: message }; } } // ============================================================ // SESSION MANAGEMENT // ============================================================ /** * Start session tracking */ async startSession(args) { try { const storage = this.getStorage(); await storage.startSession(args.session_id); return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to start session", { error: message }); return { success: false, error: message }; } } /** * End session and get summary */ async endSession(args) { try { const storage = this.getStorage(); const summary = await storage.endSession(args.session_id); if (!summary) { return { success: false, error: "Session not found" }; } return { success: true, summary }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to end session", { error: message }); return { success: false, error: message }; } } // ============================================================ // KNOWLEDGE MANAGEMENT // ============================================================ /** * Get promotion candidates */ async getPromotionCandidates() { try { const storage = this.getStorage(); const skills = await storage.getPromotionCandidates(); return { success: true, skills }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to get promotion candidates", { error: message }); return { success: false, error: message }; } } /** * Promote skill priority */ async promoteSkillPriority(args) { try { const storage = this.getStorage(); const skill = await storage.promoteSkill(args.skill_id); if (!skill) { return { success: false, error: "Skill not found" }; } return { success: true, skill }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to promote skill priority", { error: message }); return { success: false, error: message }; } } /** * Archive stale skills */ async archiveStaleSkills(args) { try { const storage = this.getStorage(); const count = await storage.archiveStaleSkills(args.days_threshold || 90); return { success: true, archivedCount: count }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to archive stale skills", { error: message }); return { success: false, error: message }; } } /** * Get skill storage metrics */ async getSkillMetrics() { try { const storage = this.getStorage(); const metrics = await storage.getMetrics(); return { success: true, metrics }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; logger.error("Failed to get skill metrics", { error: message }); return { success: false, error: message }; } } } const SKILL_TOOL_DEFINITIONS = [ { name: "record_skill", description: "Record a new learning, pattern, or skill to remember across sessions", inputSchema: { type: "object", properties: { content: { type: "string", description: "The skill/learning content to remember" }, category: { type: "string", enum: [ "tool", "workflow", "correction", "pattern", "preference", "pitfall", "optimization" ], description: "Category of the skill" }, priority: { type: "string", enum: ["critical", "high", "medium", "low"], default: "medium", description: "How important this skill is" }, tool: { type: "string", description: "Related tool name (if applicable)" }, tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" } }, required: ["content", "category"] } }, { name: "get_relevant_skills", description: "Get skills relevant to current context (tool, language, etc.)", inputSchema: { type: "object", properties: { tool: { type: "string", description: "Filter by tool name" }, language: { type: "string", description: "Filter by programming language" }, framework: { type: "string", description: "Filter by framework" }, limit: { type: "number", default: 20, description: "Maximum skills to return" } } } }, { name: "validate_skill", description: "Mark a skill as validated/reinforced (increases its importance)", inputSchema: { type: "object", properties: { skill_id: { type: "string", description: "ID of the skill to validate" } }, required: ["skill_id"] } }, { name: "record_correction", description: "Record a user correction to remember and apply in future sessions", inputSchema: { type: "object", properties: { title: { type: "string", description: "Brief title of the correction" }, content: { type: "string", description: "What was corrected and how to do it correctly" }, tool: { type: "string", description: "Related tool (if applicable)" } }, required: ["title", "content"] } }, { name: "record_decision", description: "Record an important decision made during this session", inputSchema: { type: "object", properties: { title: { type: "string", description: "Brief title of the decision" }, content: { type: "string", description: "The decision and its reasoning" }, file: { type: "string", description: "Related file path (if applicable)" } }, required: ["title", "content"] } }, { name: "get_session_learnings", description: "Get all learnings and corrections from current or specified session", inputSchema: { type: "object", properties: { session_id: { type: "string", description: "Session ID (defaults to current)" } } } }, { name: "promote_learning", description: "Promote a session learning to a permanent skill", inputSchema: { type: "object", properties: { entry_id: { type: "string", description: "Journal entry ID to promote" }, category: { type: "string", enum: [ "tool", "workflow", "correction", "pattern", "preference", "pitfall", "optimization" ], description: "Skill category" }, priority: { type: "string", enum: ["critical", "high", "medium", "low"], default: "medium", description: "Skill priority" } }, required: ["entry_id", "category"] } }, { name: "get_skill_metrics", description: "Get metrics about stored skills and learnings", inputSchema: { type: "object", properties: {} } } ]; export { SKILL_TOOL_DEFINITIONS, SkillHandlers }; //# sourceMappingURL=skill-handlers.js.map