@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
JavaScript
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