@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.
357 lines (356 loc) • 11 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 { ConfigManager } from "../../../core/config/config-manager.js";
import { TraceDetector } from "../../../core/trace/trace-detector.js";
import { logger } from "../../../core/monitoring/logger.js";
import { v4 as uuidv4 } from "uuid";
class ToolScoringMiddleware {
configManager;
traceDetector;
metrics = [];
profileStats = /* @__PURE__ */ new Map();
db;
constructor(configManager, traceDetector, db) {
this.configManager = configManager || new ConfigManager();
this.traceDetector = traceDetector || new TraceDetector();
this.db = db;
this.initializeDatabase();
this.loadMetrics();
}
/**
* Initialize database tables for metrics
*/
initializeDatabase() {
if (!this.db) return;
this.db.exec(`
CREATE TABLE IF NOT EXISTS tool_scoring_metrics (
id TEXT PRIMARY KEY,
profile_name TEXT NOT NULL,
tool_name TEXT NOT NULL,
score REAL NOT NULL,
files_affected INTEGER DEFAULT 0,
is_permanent BOOLEAN DEFAULT FALSE,
reference_count INTEGER DEFAULT 0,
timestamp INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
this.db.exec(`
CREATE TABLE IF NOT EXISTS profile_usage_stats (
profile_name TEXT PRIMARY KEY,
usage_count INTEGER DEFAULT 0,
total_score REAL DEFAULT 0,
avg_score REAL DEFAULT 0,
high_score_tools TEXT,
last_used INTEGER,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_metrics_profile ON tool_scoring_metrics(profile_name);
CREATE INDEX IF NOT EXISTS idx_metrics_tool ON tool_scoring_metrics(tool_name);
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON tool_scoring_metrics(timestamp);
`);
}
/**
* Load existing metrics from database
*/
loadMetrics() {
if (!this.db) return;
try {
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
const stmt = this.db.prepare(`
SELECT * FROM tool_scoring_metrics
WHERE timestamp > ?
ORDER BY timestamp DESC
`);
const rows = stmt.all(cutoff);
this.metrics = rows.map((row) => ({
profileName: row.profile_name,
toolName: row.tool_name,
score: row.score,
factors: {
filesAffected: row.files_affected,
isPermanent: row.is_permanent === 1,
referenceCount: row.reference_count
},
timestamp: row.timestamp
}));
const statsStmt = this.db.prepare(`
SELECT * FROM profile_usage_stats
`);
const statsRows = statsStmt.all();
statsRows.forEach((row) => {
this.profileStats.set(row.profile_name, {
profileName: row.profile_name,
usageCount: row.usage_count,
totalScore: row.total_score,
avgScore: row.avg_score,
highScoreTools: row.high_score_tools ? JSON.parse(row.high_score_tools) : [],
lastUsed: row.last_used
});
});
logger.info("Loaded tool scoring metrics", {
metricsCount: this.metrics.length,
profilesCount: this.profileStats.size
});
} catch (error) {
logger.error("Failed to load metrics", error);
}
}
/**
* Score a tool call and track it
*/
async scoreToolCall(toolName, args, result, error) {
const factors = this.extractScoringFactors(toolName, args, result);
const score = this.configManager.calculateScore(toolName, factors);
const config = this.configManager.getConfig();
const profileName = config.profile || "default";
const metric = {
profileName,
toolName,
score,
factors,
timestamp: Date.now()
};
this.metrics.push(metric);
this.updateProfileStats(profileName, toolName, score);
this.saveMetric(metric);
if (score > 0.3) {
const toolCall = {
id: uuidv4(),
tool: toolName,
arguments: args,
timestamp: Date.now(),
result,
error,
filesAffected: factors.filesAffected > 0 ? this.getFilesFromArgs(args) : void 0
};
this.traceDetector.addToolCall(toolCall);
}
if (score > 0.8) {
logger.warn("High-importance tool call", {
tool: toolName,
score,
profile: profileName,
factors
});
}
return score;
}
/**
* Extract scoring factors from tool arguments and results
*/
extractScoringFactors(toolName, args, result) {
let filesAffected = 0;
let isPermanent = false;
let referenceCount = 0;
if (args.file || args.files || args.path || args.paths) {
filesAffected = 1;
if (args.files || args.paths) {
filesAffected = Array.isArray(args.files || args.paths) ? (args.files || args.paths).length : 1;
}
}
const permanentTools = [
"write",
"edit",
"create",
"delete",
"update",
"add_decision",
"create_task",
"linear_update_task"
];
if (permanentTools.some((t) => toolName.includes(t))) {
isPermanent = true;
}
if (result?.referenceCount !== void 0) {
referenceCount = result.referenceCount;
} else if (result?.references?.length) {
referenceCount = result.references.length;
}
return { filesAffected, isPermanent, referenceCount };
}
/**
* Get file paths from arguments
*/
getFilesFromArgs(args) {
const files = [];
if (args.file) files.push(args.file);
if (args.path) files.push(args.path);
if (args.files && Array.isArray(args.files)) files.push(...args.files);
if (args.paths && Array.isArray(args.paths)) files.push(...args.paths);
return files;
}
/**
* Update profile usage statistics
*/
updateProfileStats(profileName, toolName, score) {
let stats = this.profileStats.get(profileName);
if (!stats) {
stats = {
profileName,
usageCount: 0,
totalScore: 0,
avgScore: 0,
highScoreTools: [],
lastUsed: Date.now()
};
this.profileStats.set(profileName, stats);
}
stats.usageCount++;
stats.totalScore += score;
stats.avgScore = stats.totalScore / stats.usageCount;
stats.lastUsed = Date.now();
if (score > 0.7 && !stats.highScoreTools.includes(toolName)) {
stats.highScoreTools.push(toolName);
if (stats.highScoreTools.length > 10) {
stats.highScoreTools.shift();
}
}
this.saveProfileStats(stats);
}
/**
* Save metric to database
*/
saveMetric(metric) {
if (!this.db) return;
try {
const stmt = this.db.prepare(`
INSERT INTO tool_scoring_metrics (
id, profile_name, tool_name, score,
files_affected, is_permanent, reference_count,
timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
uuidv4(),
metric.profileName,
metric.toolName,
metric.score,
metric.factors.filesAffected,
metric.factors.isPermanent ? 1 : 0,
metric.factors.referenceCount,
metric.timestamp
);
} catch (error) {
logger.error("Failed to save metric", error);
}
}
/**
* Save profile stats to database
*/
saveProfileStats(stats) {
if (!this.db) return;
try {
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO profile_usage_stats (
profile_name, usage_count, total_score, avg_score,
high_score_tools, last_used
) VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run(
stats.profileName,
stats.usageCount,
stats.totalScore,
stats.avgScore,
JSON.stringify(stats.highScoreTools),
stats.lastUsed
);
} catch (error) {
logger.error("Failed to save profile stats", error);
}
}
/**
* Get profile effectiveness report
*/
getProfileReport(profileName) {
if (profileName) {
const stats = this.profileStats.get(profileName);
if (!stats) {
return { error: `Profile '${profileName}' not found or not used yet` };
}
return {
profile: profileName,
usage: stats.usageCount,
avgScore: stats.avgScore.toFixed(3),
highScoreTools: stats.highScoreTools,
lastUsed: new Date(stats.lastUsed).toISOString()
};
}
const profiles = Array.from(this.profileStats.values()).sort((a, b) => b.avgScore - a.avgScore);
return {
profileCount: profiles.length,
mostEffective: profiles[0]?.profileName || "none",
profiles: profiles.map((p) => ({
name: p.profileName,
usage: p.usageCount,
avgScore: p.avgScore.toFixed(3)
}))
};
}
/**
* Get tool scoring trends
*/
getToolTrends(toolName, hours = 24) {
const cutoff = Date.now() - hours * 60 * 60 * 1e3;
const relevantMetrics = this.metrics.filter(
(m) => m.toolName === toolName && m.timestamp > cutoff
);
if (relevantMetrics.length === 0) {
return { tool: toolName, message: "No recent data" };
}
const scores = relevantMetrics.map((m) => m.score);
const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;
const maxScore = Math.max(...scores);
const minScore = Math.min(...scores);
return {
tool: toolName,
period: `${hours}h`,
count: relevantMetrics.length,
avgScore: avgScore.toFixed(3),
maxScore: maxScore.toFixed(3),
minScore: minScore.toFixed(3),
trend: this.calculateTrend(relevantMetrics)
};
}
/**
* Calculate score trend
*/
calculateTrend(metrics) {
if (metrics.length < 2) return "stable";
const firstHalf = metrics.slice(0, Math.floor(metrics.length / 2));
const secondHalf = metrics.slice(Math.floor(metrics.length / 2));
const firstAvg = firstHalf.reduce((a, m) => a + m.score, 0) / firstHalf.length;
const secondAvg = secondHalf.reduce((a, m) => a + m.score, 0) / secondHalf.length;
const diff = secondAvg - firstAvg;
if (diff > 0.1) return "increasing";
if (diff < -0.1) return "decreasing";
return "stable";
}
/**
* Clean old metrics
*/
cleanOldMetrics(daysToKeep = 30) {
const cutoff = Date.now() - daysToKeep * 24 * 60 * 60 * 1e3;
const oldCount = this.metrics.length;
this.metrics = this.metrics.filter((m) => m.timestamp > cutoff);
if (this.db) {
try {
const stmt = this.db.prepare(`
DELETE FROM tool_scoring_metrics WHERE timestamp < ?
`);
stmt.run(cutoff);
} catch (error) {
logger.error("Failed to clean old metrics", error);
}
}
return oldCount - this.metrics.length;
}
}
export {
ToolScoringMiddleware
};
//# sourceMappingURL=tool-scoring.js.map