@stackmemoryai/stackmemory 1.8.1 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/orchestrate.js +8 -1
- package/dist/src/cli/commands/orchestrator.js +27 -6
- package/dist/src/cli/commands/ralph.js +43 -0
- package/dist/src/cli/commands/rules.js +250 -0
- package/dist/src/cli/commands/skill.js +406 -0
- package/dist/src/cli/index.js +43 -0
- package/dist/src/core/rules/built-in-rules.js +289 -0
- package/dist/src/core/rules/pr-review-rule.js +87 -0
- package/dist/src/core/rules/rule-engine.js +85 -0
- package/dist/src/core/rules/rule-store.js +99 -0
- package/dist/src/core/rules/types.js +4 -0
- package/dist/src/core/skills/index.js +21 -0
- package/dist/src/core/skills/skill-matcher.js +178 -0
- package/dist/src/core/skills/skill-registry.js +646 -0
- package/dist/src/core/skills/types.js +1 -47
- package/dist/src/integrations/linear/client.js +66 -0
- package/dist/src/integrations/mcp/handlers/index.js +43 -1
- package/dist/src/integrations/mcp/handlers/linear-handlers.js +102 -0
- package/dist/src/integrations/mcp/handlers/provenant-handlers.js +398 -0
- package/dist/src/integrations/mcp/handlers/skill-handlers.js +67 -167
- package/dist/src/integrations/mcp/server.js +117 -6
- package/dist/src/integrations/mcp/tool-definitions.js +152 -1
- package/dist/src/integrations/ralph/loopmax.js +488 -0
- package/package.json +2 -2
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { v4 as uuidv4 } from "uuid";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import { logger } from "../monitoring/logger.js";
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
const SCHEMA_SQL = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
13
|
+
version INTEGER PRIMARY KEY
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
content TEXT NOT NULL,
|
|
19
|
+
summary TEXT,
|
|
20
|
+
category TEXT NOT NULL,
|
|
21
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
22
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
23
|
+
tool TEXT,
|
|
24
|
+
project TEXT,
|
|
25
|
+
language TEXT,
|
|
26
|
+
framework TEXT,
|
|
27
|
+
validated_count INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
last_validated TEXT,
|
|
29
|
+
source TEXT NOT NULL,
|
|
30
|
+
session_id TEXT,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
updated_at TEXT NOT NULL,
|
|
33
|
+
expires_at TEXT
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_skills_category ON skills(category);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_skills_priority ON skills(priority);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_skills_tool ON skills(tool);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_skills_created ON skills(created_at);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS skill_rules (
|
|
42
|
+
name TEXT PRIMARY KEY,
|
|
43
|
+
description TEXT NOT NULL,
|
|
44
|
+
priority INTEGER NOT NULL DEFAULT 5,
|
|
45
|
+
triggers TEXT NOT NULL DEFAULT '{}',
|
|
46
|
+
exclude_patterns TEXT NOT NULL DEFAULT '[]',
|
|
47
|
+
related_skills TEXT NOT NULL DEFAULT '[]',
|
|
48
|
+
suggestion TEXT,
|
|
49
|
+
created_at TEXT NOT NULL,
|
|
50
|
+
updated_at TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS directory_mappings (
|
|
54
|
+
directory TEXT PRIMARY KEY,
|
|
55
|
+
skill_name TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS matcher_config (
|
|
59
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
60
|
+
min_confidence_score INTEGER NOT NULL DEFAULT 3,
|
|
61
|
+
show_match_reasons INTEGER NOT NULL DEFAULT 1,
|
|
62
|
+
max_skills_to_show INTEGER NOT NULL DEFAULT 5,
|
|
63
|
+
scoring TEXT NOT NULL DEFAULT '{}'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS journal_entries (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
session_id TEXT NOT NULL,
|
|
69
|
+
type TEXT NOT NULL,
|
|
70
|
+
title TEXT NOT NULL,
|
|
71
|
+
content TEXT NOT NULL,
|
|
72
|
+
context_file TEXT,
|
|
73
|
+
context_tool TEXT,
|
|
74
|
+
context_command TEXT,
|
|
75
|
+
outcome TEXT,
|
|
76
|
+
promoted_to_skill_id TEXT,
|
|
77
|
+
created_at TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_journal_session ON journal_entries(session_id);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_journal_type ON journal_entries(type);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
84
|
+
session_id TEXT PRIMARY KEY,
|
|
85
|
+
started_at TEXT NOT NULL,
|
|
86
|
+
ended_at TEXT,
|
|
87
|
+
entries_count INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
corrections_count INTEGER NOT NULL DEFAULT 0,
|
|
89
|
+
decisions_count INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
key_learnings TEXT NOT NULL DEFAULT '[]',
|
|
91
|
+
promoted_skill_ids TEXT NOT NULL DEFAULT '[]'
|
|
92
|
+
);
|
|
93
|
+
`;
|
|
94
|
+
function getDefaultDbPath() {
|
|
95
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "/tmp";
|
|
96
|
+
return path.join(home, ".stackmemory", "skills.db");
|
|
97
|
+
}
|
|
98
|
+
function priorityScore(priority) {
|
|
99
|
+
const scores = {
|
|
100
|
+
critical: 1e3,
|
|
101
|
+
high: 100,
|
|
102
|
+
medium: 10,
|
|
103
|
+
low: 1
|
|
104
|
+
};
|
|
105
|
+
return scores[priority] ?? 10;
|
|
106
|
+
}
|
|
107
|
+
function rowToSkill(row) {
|
|
108
|
+
return {
|
|
109
|
+
id: row["id"],
|
|
110
|
+
content: row["content"],
|
|
111
|
+
summary: row["summary"] || void 0,
|
|
112
|
+
category: row["category"],
|
|
113
|
+
priority: row["priority"],
|
|
114
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
115
|
+
tool: row["tool"] || void 0,
|
|
116
|
+
project: row["project"] || void 0,
|
|
117
|
+
language: row["language"] || void 0,
|
|
118
|
+
framework: row["framework"] || void 0,
|
|
119
|
+
validatedCount: row["validated_count"] || 0,
|
|
120
|
+
lastValidated: row["last_validated"] || void 0,
|
|
121
|
+
source: row["source"],
|
|
122
|
+
sessionId: row["session_id"] || void 0,
|
|
123
|
+
createdAt: row["created_at"],
|
|
124
|
+
updatedAt: row["updated_at"],
|
|
125
|
+
expiresAt: row["expires_at"] || void 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function rowToJournalEntry(row) {
|
|
129
|
+
const context = row["context_file"] || row["context_tool"] || row["context_command"] ? {
|
|
130
|
+
file: row["context_file"] || void 0,
|
|
131
|
+
tool: row["context_tool"] || void 0,
|
|
132
|
+
command: row["context_command"] || void 0
|
|
133
|
+
} : void 0;
|
|
134
|
+
return {
|
|
135
|
+
id: row["id"],
|
|
136
|
+
sessionId: row["session_id"],
|
|
137
|
+
type: row["type"],
|
|
138
|
+
title: row["title"],
|
|
139
|
+
content: row["content"],
|
|
140
|
+
context,
|
|
141
|
+
outcome: row["outcome"] || void 0,
|
|
142
|
+
createdAt: row["created_at"],
|
|
143
|
+
promotedToSkillId: row["promoted_to_skill_id"] || void 0
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
class SkillRegistry {
|
|
147
|
+
db;
|
|
148
|
+
dbPath;
|
|
149
|
+
constructor(dbPath) {
|
|
150
|
+
this.dbPath = dbPath || getDefaultDbPath();
|
|
151
|
+
const dir = path.dirname(this.dbPath);
|
|
152
|
+
if (!fs.existsSync(dir)) {
|
|
153
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
this.db = new Database(this.dbPath);
|
|
156
|
+
this.db.pragma("journal_mode = WAL");
|
|
157
|
+
this.db.pragma("busy_timeout = 5000");
|
|
158
|
+
this.db.pragma("foreign_keys = ON");
|
|
159
|
+
this.initSchema();
|
|
160
|
+
}
|
|
161
|
+
initSchema() {
|
|
162
|
+
const versionRow = (() => {
|
|
163
|
+
try {
|
|
164
|
+
return this.db.prepare(
|
|
165
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'"
|
|
166
|
+
).get();
|
|
167
|
+
} catch {
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
171
|
+
if (!versionRow) {
|
|
172
|
+
this.db.exec(SCHEMA_SQL);
|
|
173
|
+
this.db.prepare("INSERT OR REPLACE INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
|
|
174
|
+
logger.debug("SkillRegistry: created schema v" + SCHEMA_VERSION);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ============================================================
|
|
178
|
+
// SKILL CRUD
|
|
179
|
+
// ============================================================
|
|
180
|
+
createSkill(input) {
|
|
181
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
182
|
+
const id = uuidv4();
|
|
183
|
+
this.db.prepare(
|
|
184
|
+
`INSERT INTO skills (id, content, summary, category, priority, tags, tool, project, language, framework,
|
|
185
|
+
validated_count, source, session_id, created_at, updated_at, expires_at)
|
|
186
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)`
|
|
187
|
+
).run(
|
|
188
|
+
id,
|
|
189
|
+
input.content,
|
|
190
|
+
input.summary ?? null,
|
|
191
|
+
input.category,
|
|
192
|
+
input.priority ?? "medium",
|
|
193
|
+
JSON.stringify(input.tags ?? []),
|
|
194
|
+
input.tool ?? null,
|
|
195
|
+
input.project ?? null,
|
|
196
|
+
input.language ?? null,
|
|
197
|
+
input.framework ?? null,
|
|
198
|
+
input.source,
|
|
199
|
+
input.sessionId ?? null,
|
|
200
|
+
now,
|
|
201
|
+
now,
|
|
202
|
+
input.expiresAt ?? null
|
|
203
|
+
);
|
|
204
|
+
const skill = this.getSkill(id);
|
|
205
|
+
if (!skill) throw new Error(`Skill not found after creation: ${id}`);
|
|
206
|
+
return skill;
|
|
207
|
+
}
|
|
208
|
+
getSkill(id) {
|
|
209
|
+
const row = this.db.prepare("SELECT * FROM skills WHERE id = ?").get(id);
|
|
210
|
+
return row ? rowToSkill(row) : void 0;
|
|
211
|
+
}
|
|
212
|
+
updateSkill(input) {
|
|
213
|
+
const existing = this.getSkill(input.id);
|
|
214
|
+
if (!existing) return void 0;
|
|
215
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
216
|
+
const updates = ["updated_at = ?"];
|
|
217
|
+
const params = [now];
|
|
218
|
+
if (input.content !== void 0) {
|
|
219
|
+
updates.push("content = ?");
|
|
220
|
+
params.push(input.content);
|
|
221
|
+
}
|
|
222
|
+
if (input.summary !== void 0) {
|
|
223
|
+
updates.push("summary = ?");
|
|
224
|
+
params.push(input.summary);
|
|
225
|
+
}
|
|
226
|
+
if (input.category !== void 0) {
|
|
227
|
+
updates.push("category = ?");
|
|
228
|
+
params.push(input.category);
|
|
229
|
+
}
|
|
230
|
+
if (input.priority !== void 0) {
|
|
231
|
+
updates.push("priority = ?");
|
|
232
|
+
params.push(input.priority);
|
|
233
|
+
}
|
|
234
|
+
if (input.tags !== void 0) {
|
|
235
|
+
updates.push("tags = ?");
|
|
236
|
+
params.push(JSON.stringify(input.tags));
|
|
237
|
+
}
|
|
238
|
+
if (input.tool !== void 0) {
|
|
239
|
+
updates.push("tool = ?");
|
|
240
|
+
params.push(input.tool);
|
|
241
|
+
}
|
|
242
|
+
params.push(input.id);
|
|
243
|
+
this.db.prepare(`UPDATE skills SET ${updates.join(", ")} WHERE id = ?`).run(...params);
|
|
244
|
+
return this.getSkill(input.id);
|
|
245
|
+
}
|
|
246
|
+
validateSkill(id) {
|
|
247
|
+
const skill = this.getSkill(id);
|
|
248
|
+
if (!skill) return void 0;
|
|
249
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
250
|
+
this.db.prepare(
|
|
251
|
+
"UPDATE skills SET validated_count = validated_count + 1, last_validated = ?, updated_at = ? WHERE id = ?"
|
|
252
|
+
).run(now, now, id);
|
|
253
|
+
return this.getSkill(id);
|
|
254
|
+
}
|
|
255
|
+
deleteSkill(id) {
|
|
256
|
+
const result = this.db.prepare("DELETE FROM skills WHERE id = ?").run(id);
|
|
257
|
+
return result.changes > 0;
|
|
258
|
+
}
|
|
259
|
+
querySkills(query) {
|
|
260
|
+
const conditions = [];
|
|
261
|
+
const params = [];
|
|
262
|
+
if (query.categories?.length) {
|
|
263
|
+
conditions.push(
|
|
264
|
+
`category IN (${query.categories.map(() => "?").join(",")})`
|
|
265
|
+
);
|
|
266
|
+
params.push(...query.categories);
|
|
267
|
+
}
|
|
268
|
+
if (query.priorities?.length) {
|
|
269
|
+
conditions.push(
|
|
270
|
+
`priority IN (${query.priorities.map(() => "?").join(",")})`
|
|
271
|
+
);
|
|
272
|
+
params.push(...query.priorities);
|
|
273
|
+
}
|
|
274
|
+
if (query.tool) {
|
|
275
|
+
conditions.push("tool = ?");
|
|
276
|
+
params.push(query.tool);
|
|
277
|
+
}
|
|
278
|
+
if (query.language) {
|
|
279
|
+
conditions.push("language = ?");
|
|
280
|
+
params.push(query.language);
|
|
281
|
+
}
|
|
282
|
+
if (query.framework) {
|
|
283
|
+
conditions.push("framework = ?");
|
|
284
|
+
params.push(query.framework);
|
|
285
|
+
}
|
|
286
|
+
if (query.minValidatedCount !== void 0) {
|
|
287
|
+
conditions.push("validated_count >= ?");
|
|
288
|
+
params.push(query.minValidatedCount);
|
|
289
|
+
}
|
|
290
|
+
const where = conditions.length ? "WHERE " + conditions.join(" AND ") : "";
|
|
291
|
+
const sortColMap = {
|
|
292
|
+
priority: "priority",
|
|
293
|
+
validatedCount: "validated_count",
|
|
294
|
+
createdAt: "created_at",
|
|
295
|
+
updatedAt: "updated_at"
|
|
296
|
+
};
|
|
297
|
+
const sortCol = sortColMap[query.sortBy ?? "priority"] ?? "priority";
|
|
298
|
+
const sortDir = query.sortOrder === "asc" ? "ASC" : "DESC";
|
|
299
|
+
const sql = `SELECT * FROM skills ${where} ORDER BY ${sortCol} ${sortDir} LIMIT ? OFFSET ?`;
|
|
300
|
+
params.push(query.limit ?? 50, query.offset ?? 0);
|
|
301
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
302
|
+
let skills = rows.map(rowToSkill);
|
|
303
|
+
if (sortCol === "priority") {
|
|
304
|
+
skills.sort((a, b) => {
|
|
305
|
+
const diff = priorityScore(b.priority) - priorityScore(a.priority);
|
|
306
|
+
return sortDir === "DESC" ? diff : -diff;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (query.tags?.length) {
|
|
310
|
+
const tags = query.tags;
|
|
311
|
+
skills = skills.filter((s) => tags.some((t) => s.tags.includes(t)));
|
|
312
|
+
}
|
|
313
|
+
return skills;
|
|
314
|
+
}
|
|
315
|
+
getRelevantSkills(context) {
|
|
316
|
+
const skills = [];
|
|
317
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
318
|
+
const critical = this.db.prepare("SELECT * FROM skills WHERE priority = 'critical'").all();
|
|
319
|
+
for (const row of critical) {
|
|
320
|
+
const skill = rowToSkill(row);
|
|
321
|
+
if (!seenIds.has(skill.id)) {
|
|
322
|
+
skills.push(skill);
|
|
323
|
+
seenIds.add(skill.id);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (context.tool) {
|
|
327
|
+
const toolRows = this.db.prepare(
|
|
328
|
+
"SELECT * FROM skills WHERE tool = ? ORDER BY validated_count DESC LIMIT 20"
|
|
329
|
+
).all(context.tool);
|
|
330
|
+
for (const row of toolRows) {
|
|
331
|
+
const skill = rowToSkill(row);
|
|
332
|
+
if (!seenIds.has(skill.id)) {
|
|
333
|
+
skills.push(skill);
|
|
334
|
+
seenIds.add(skill.id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const validated = this.db.prepare("SELECT * FROM skills ORDER BY validated_count DESC LIMIT 10").all();
|
|
339
|
+
for (const row of validated) {
|
|
340
|
+
const skill = rowToSkill(row);
|
|
341
|
+
if (!seenIds.has(skill.id)) {
|
|
342
|
+
skills.push(skill);
|
|
343
|
+
seenIds.add(skill.id);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return skills.slice(0, 50);
|
|
347
|
+
}
|
|
348
|
+
// ============================================================
|
|
349
|
+
// SKILL RULES CRUD
|
|
350
|
+
// ============================================================
|
|
351
|
+
upsertRule(name, rule) {
|
|
352
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
353
|
+
this.db.prepare(
|
|
354
|
+
`INSERT INTO skill_rules (name, description, priority, triggers, exclude_patterns, related_skills, suggestion, created_at, updated_at)
|
|
355
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
356
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
357
|
+
description = excluded.description,
|
|
358
|
+
priority = excluded.priority,
|
|
359
|
+
triggers = excluded.triggers,
|
|
360
|
+
exclude_patterns = excluded.exclude_patterns,
|
|
361
|
+
related_skills = excluded.related_skills,
|
|
362
|
+
suggestion = excluded.suggestion,
|
|
363
|
+
updated_at = excluded.updated_at`
|
|
364
|
+
).run(
|
|
365
|
+
name,
|
|
366
|
+
rule.description,
|
|
367
|
+
rule.priority,
|
|
368
|
+
JSON.stringify(rule.triggers),
|
|
369
|
+
JSON.stringify(rule.excludePatterns ?? []),
|
|
370
|
+
JSON.stringify(rule.relatedSkills ?? []),
|
|
371
|
+
rule.suggestion ?? null,
|
|
372
|
+
now,
|
|
373
|
+
now
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
getRule(name) {
|
|
377
|
+
const row = this.db.prepare("SELECT * FROM skill_rules WHERE name = ?").get(name);
|
|
378
|
+
if (!row) return void 0;
|
|
379
|
+
return {
|
|
380
|
+
description: row["description"],
|
|
381
|
+
priority: row["priority"],
|
|
382
|
+
triggers: JSON.parse(row["triggers"]),
|
|
383
|
+
excludePatterns: JSON.parse(row["exclude_patterns"]),
|
|
384
|
+
relatedSkills: JSON.parse(row["related_skills"]),
|
|
385
|
+
suggestion: row["suggestion"] || void 0
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
getAllRules() {
|
|
389
|
+
const rows = this.db.prepare("SELECT * FROM skill_rules").all();
|
|
390
|
+
const result = {};
|
|
391
|
+
for (const row of rows) {
|
|
392
|
+
result[row["name"]] = {
|
|
393
|
+
description: row["description"],
|
|
394
|
+
priority: row["priority"],
|
|
395
|
+
triggers: JSON.parse(row["triggers"]),
|
|
396
|
+
excludePatterns: JSON.parse(row["exclude_patterns"]),
|
|
397
|
+
relatedSkills: JSON.parse(row["related_skills"]),
|
|
398
|
+
suggestion: row["suggestion"] || void 0
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
deleteRule(name) {
|
|
404
|
+
return this.db.prepare("DELETE FROM skill_rules WHERE name = ?").run(name).changes > 0;
|
|
405
|
+
}
|
|
406
|
+
// ============================================================
|
|
407
|
+
// DIRECTORY MAPPINGS
|
|
408
|
+
// ============================================================
|
|
409
|
+
setDirectoryMapping(directory, skillName) {
|
|
410
|
+
this.db.prepare(
|
|
411
|
+
"INSERT OR REPLACE INTO directory_mappings (directory, skill_name) VALUES (?, ?)"
|
|
412
|
+
).run(directory, skillName);
|
|
413
|
+
}
|
|
414
|
+
getDirectoryMappings() {
|
|
415
|
+
const rows = this.db.prepare("SELECT * FROM directory_mappings").all();
|
|
416
|
+
const result = {};
|
|
417
|
+
for (const row of rows) {
|
|
418
|
+
result[row["directory"]] = row["skill_name"];
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
// ============================================================
|
|
423
|
+
// MATCHER CONFIG
|
|
424
|
+
// ============================================================
|
|
425
|
+
getMatcherConfig() {
|
|
426
|
+
const row = this.db.prepare("SELECT * FROM matcher_config WHERE id = 1").get();
|
|
427
|
+
if (!row) {
|
|
428
|
+
return {
|
|
429
|
+
config: {
|
|
430
|
+
minConfidenceScore: 3,
|
|
431
|
+
showMatchReasons: true,
|
|
432
|
+
maxSkillsToShow: 5
|
|
433
|
+
},
|
|
434
|
+
scoring: {
|
|
435
|
+
keyword: 2,
|
|
436
|
+
keywordPattern: 3,
|
|
437
|
+
pathPattern: 4,
|
|
438
|
+
directoryMatch: 5,
|
|
439
|
+
intentPattern: 4,
|
|
440
|
+
contentPattern: 3,
|
|
441
|
+
contextPattern: 2
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
config: {
|
|
447
|
+
minConfidenceScore: row["min_confidence_score"],
|
|
448
|
+
showMatchReasons: !!row["show_match_reasons"],
|
|
449
|
+
maxSkillsToShow: row["max_skills_to_show"]
|
|
450
|
+
},
|
|
451
|
+
scoring: JSON.parse(row["scoring"])
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
setMatcherConfig(config, scoring) {
|
|
455
|
+
this.db.prepare(
|
|
456
|
+
`INSERT OR REPLACE INTO matcher_config (id, min_confidence_score, show_match_reasons, max_skills_to_show, scoring)
|
|
457
|
+
VALUES (1, ?, ?, ?, ?)`
|
|
458
|
+
).run(
|
|
459
|
+
config.minConfidenceScore,
|
|
460
|
+
config.showMatchReasons ? 1 : 0,
|
|
461
|
+
config.maxSkillsToShow,
|
|
462
|
+
JSON.stringify(scoring)
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
// ============================================================
|
|
466
|
+
// JOURNAL
|
|
467
|
+
// ============================================================
|
|
468
|
+
createJournalEntry(sessionId, type, title, content, context) {
|
|
469
|
+
const id = uuidv4();
|
|
470
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
471
|
+
this.db.prepare(
|
|
472
|
+
`INSERT INTO journal_entries (id, session_id, type, title, content, context_file, context_tool, context_command, created_at)
|
|
473
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
474
|
+
).run(
|
|
475
|
+
id,
|
|
476
|
+
sessionId,
|
|
477
|
+
type,
|
|
478
|
+
title,
|
|
479
|
+
content,
|
|
480
|
+
context?.file ?? null,
|
|
481
|
+
context?.tool ?? null,
|
|
482
|
+
context?.command ?? null,
|
|
483
|
+
now
|
|
484
|
+
);
|
|
485
|
+
return {
|
|
486
|
+
id,
|
|
487
|
+
sessionId,
|
|
488
|
+
type,
|
|
489
|
+
title,
|
|
490
|
+
content,
|
|
491
|
+
context,
|
|
492
|
+
createdAt: now
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
getSessionJournal(sessionId) {
|
|
496
|
+
const rows = this.db.prepare(
|
|
497
|
+
"SELECT * FROM journal_entries WHERE session_id = ? ORDER BY created_at DESC"
|
|
498
|
+
).all(sessionId);
|
|
499
|
+
return rows.map(rowToJournalEntry);
|
|
500
|
+
}
|
|
501
|
+
promoteToSkill(entryId, category, priority = "medium") {
|
|
502
|
+
const row = this.db.prepare("SELECT * FROM journal_entries WHERE id = ?").get(entryId);
|
|
503
|
+
if (!row) return void 0;
|
|
504
|
+
const entry = rowToJournalEntry(row);
|
|
505
|
+
const skill = this.createSkill({
|
|
506
|
+
content: entry.content,
|
|
507
|
+
summary: entry.title,
|
|
508
|
+
category,
|
|
509
|
+
priority,
|
|
510
|
+
tags: [],
|
|
511
|
+
tool: entry.context?.tool,
|
|
512
|
+
source: "observation",
|
|
513
|
+
sessionId: entry.sessionId
|
|
514
|
+
});
|
|
515
|
+
this.db.prepare(
|
|
516
|
+
"UPDATE journal_entries SET promoted_to_skill_id = ? WHERE id = ?"
|
|
517
|
+
).run(skill.id, entryId);
|
|
518
|
+
return skill;
|
|
519
|
+
}
|
|
520
|
+
// ============================================================
|
|
521
|
+
// SESSION MANAGEMENT
|
|
522
|
+
// ============================================================
|
|
523
|
+
startSession(sessionId) {
|
|
524
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
525
|
+
this.db.prepare(
|
|
526
|
+
`INSERT OR REPLACE INTO session_summaries (session_id, started_at, entries_count, corrections_count, decisions_count)
|
|
527
|
+
VALUES (?, ?, 0, 0, 0)`
|
|
528
|
+
).run(sessionId, now);
|
|
529
|
+
}
|
|
530
|
+
endSession(sessionId) {
|
|
531
|
+
const row = this.db.prepare("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId);
|
|
532
|
+
if (!row) return void 0;
|
|
533
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
534
|
+
const entries = this.getSessionJournal(sessionId);
|
|
535
|
+
const corrections = entries.filter((e) => e.type === "correction").length;
|
|
536
|
+
const decisions = entries.filter((e) => e.type === "decision").length;
|
|
537
|
+
const keyLearnings = entries.filter((e) => e.type === "correction" || e.type === "resolution").slice(0, 5).map((e) => e.title);
|
|
538
|
+
const promotedSkillIds = entries.filter((e) => e.promotedToSkillId != null).map((e) => e.promotedToSkillId);
|
|
539
|
+
this.db.prepare(
|
|
540
|
+
`UPDATE session_summaries SET
|
|
541
|
+
ended_at = ?, entries_count = ?, corrections_count = ?,
|
|
542
|
+
decisions_count = ?, key_learnings = ?, promoted_skill_ids = ?
|
|
543
|
+
WHERE session_id = ?`
|
|
544
|
+
).run(
|
|
545
|
+
now,
|
|
546
|
+
entries.length,
|
|
547
|
+
corrections,
|
|
548
|
+
decisions,
|
|
549
|
+
JSON.stringify(keyLearnings),
|
|
550
|
+
JSON.stringify(promotedSkillIds),
|
|
551
|
+
sessionId
|
|
552
|
+
);
|
|
553
|
+
return {
|
|
554
|
+
sessionId,
|
|
555
|
+
startedAt: row["started_at"],
|
|
556
|
+
endedAt: now,
|
|
557
|
+
entriesCount: entries.length,
|
|
558
|
+
correctionsCount: corrections,
|
|
559
|
+
decisionsCount: decisions,
|
|
560
|
+
keyLearnings,
|
|
561
|
+
promotedSkillIds
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
getSessionSummary(sessionId) {
|
|
565
|
+
const row = this.db.prepare("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId);
|
|
566
|
+
if (!row) return void 0;
|
|
567
|
+
return {
|
|
568
|
+
sessionId: row["session_id"],
|
|
569
|
+
startedAt: row["started_at"],
|
|
570
|
+
endedAt: row["ended_at"] || void 0,
|
|
571
|
+
entriesCount: row["entries_count"] || 0,
|
|
572
|
+
correctionsCount: row["corrections_count"] || 0,
|
|
573
|
+
decisionsCount: row["decisions_count"] || 0,
|
|
574
|
+
keyLearnings: JSON.parse(row["key_learnings"] || "[]"),
|
|
575
|
+
promotedSkillIds: JSON.parse(
|
|
576
|
+
row["promoted_skill_ids"] || "[]"
|
|
577
|
+
)
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
// ============================================================
|
|
581
|
+
// METRICS
|
|
582
|
+
// ============================================================
|
|
583
|
+
getMetrics() {
|
|
584
|
+
const skillsTotal = this.db.prepare("SELECT COUNT(*) as c FROM skills").get().c;
|
|
585
|
+
const catRows = this.db.prepare("SELECT category, COUNT(*) as c FROM skills GROUP BY category").all();
|
|
586
|
+
const skillsByCategory = {};
|
|
587
|
+
for (const row of catRows) {
|
|
588
|
+
skillsByCategory[row.category] = row.c;
|
|
589
|
+
}
|
|
590
|
+
const rulesTotal = this.db.prepare("SELECT COUNT(*) as c FROM skill_rules").get().c;
|
|
591
|
+
const journalEntriesTotal = this.db.prepare("SELECT COUNT(*) as c FROM journal_entries").get().c;
|
|
592
|
+
const sessionsTotal = this.db.prepare("SELECT COUNT(*) as c FROM session_summaries").get().c;
|
|
593
|
+
return {
|
|
594
|
+
skillsTotal,
|
|
595
|
+
skillsByCategory,
|
|
596
|
+
rulesTotal,
|
|
597
|
+
journalEntriesTotal,
|
|
598
|
+
sessionsTotal
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// ============================================================
|
|
602
|
+
// SEED FROM RULES JSON
|
|
603
|
+
// ============================================================
|
|
604
|
+
seedFromRulesJson(rulesFile) {
|
|
605
|
+
const tx = this.db.transaction(() => {
|
|
606
|
+
this.setMatcherConfig(rulesFile.config, rulesFile.scoring);
|
|
607
|
+
for (const [dir, skill] of Object.entries(
|
|
608
|
+
rulesFile.directoryMappings || {}
|
|
609
|
+
)) {
|
|
610
|
+
this.setDirectoryMapping(dir, skill);
|
|
611
|
+
}
|
|
612
|
+
for (const [name, rule] of Object.entries(rulesFile.skills)) {
|
|
613
|
+
this.upsertRule(name, rule);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
tx();
|
|
617
|
+
logger.info("SkillRegistry: seeded from rules JSON", {
|
|
618
|
+
rules: Object.keys(rulesFile.skills).length,
|
|
619
|
+
mappings: Object.keys(rulesFile.directoryMappings || {}).length
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
// ============================================================
|
|
623
|
+
// LIFECYCLE
|
|
624
|
+
// ============================================================
|
|
625
|
+
close() {
|
|
626
|
+
this.db.close();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
let registryInstance;
|
|
630
|
+
function getSkillRegistry(dbPath) {
|
|
631
|
+
if (!registryInstance) {
|
|
632
|
+
registryInstance = new SkillRegistry(dbPath);
|
|
633
|
+
}
|
|
634
|
+
return registryInstance;
|
|
635
|
+
}
|
|
636
|
+
function resetSkillRegistry() {
|
|
637
|
+
if (registryInstance) {
|
|
638
|
+
registryInstance.close();
|
|
639
|
+
registryInstance = void 0;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
export {
|
|
643
|
+
SkillRegistry,
|
|
644
|
+
getSkillRegistry,
|
|
645
|
+
resetSkillRegistry
|
|
646
|
+
};
|
|
@@ -133,60 +133,14 @@ const SkillQuerySchema = z.object({
|
|
|
133
133
|
sortBy: z.enum(["priority", "validatedCount", "createdAt", "updatedAt"]).default("priority"),
|
|
134
134
|
sortOrder: z.enum(["asc", "desc"]).default("desc")
|
|
135
135
|
});
|
|
136
|
-
const REDIS_KEYS = {
|
|
137
|
-
// Skills (namespaced by user)
|
|
138
|
-
skill: (userId, id) => `user:${userId}:skill:${id}`,
|
|
139
|
-
skillsByTool: (userId, tool) => `user:${userId}:skills:tool:${tool}`,
|
|
140
|
-
skillsByCategory: (userId, category) => `user:${userId}:skills:category:${category}`,
|
|
141
|
-
skillsByTag: (userId, tag) => `user:${userId}:skills:tag:${tag}`,
|
|
142
|
-
skillsRecent: (userId) => `user:${userId}:skills:recent`,
|
|
143
|
-
skillsValidated: (userId) => `user:${userId}:skills:validated`,
|
|
144
|
-
// Session journal (namespaced by user)
|
|
145
|
-
journalEntry: (userId, id) => `user:${userId}:journal:entry:${id}`,
|
|
146
|
-
journalSession: (userId, sessionId) => `user:${userId}:journal:session:${sessionId}`,
|
|
147
|
-
journalRecent: (userId) => `user:${userId}:journal:recent`,
|
|
148
|
-
// Session tracking (namespaced by user)
|
|
149
|
-
sessionSummary: (userId, sessionId) => `user:${userId}:session:summary:${sessionId}`,
|
|
150
|
-
sessionsActive: (userId) => `user:${userId}:sessions:active`,
|
|
151
|
-
// Promotion tracking (namespaced by user)
|
|
152
|
-
promotionCandidates: (userId) => `user:${userId}:skills:promotion:candidates`,
|
|
153
|
-
// Locks (global)
|
|
154
|
-
syncLock: (resource) => `lock:skill:${resource}`
|
|
155
|
-
};
|
|
156
|
-
const CACHE_TTL = {
|
|
157
|
-
// Base skill TTL: 7 days minimum
|
|
158
|
-
skillBase: 604800,
|
|
159
|
-
// 7 days
|
|
160
|
-
// Max skill TTL: 90 days for frequently used skills
|
|
161
|
-
skillMax: 7776e3,
|
|
162
|
-
// 90 days
|
|
163
|
-
// TTL increment per validation/use: +7 days
|
|
164
|
-
skillIncrement: 604800,
|
|
165
|
-
// 7 days
|
|
166
|
-
skillIndex: 86400,
|
|
167
|
-
// 1 day (was 1 hour)
|
|
168
|
-
session: 604800,
|
|
169
|
-
// 7 days
|
|
170
|
-
journal: 2592e3,
|
|
171
|
-
// 30 days
|
|
172
|
-
lock: 30
|
|
173
|
-
// 30 seconds
|
|
174
|
-
};
|
|
175
|
-
function calculateSkillTTL(validatedCount) {
|
|
176
|
-
const ttl = CACHE_TTL.skillBase + validatedCount * CACHE_TTL.skillIncrement;
|
|
177
|
-
return Math.min(ttl, CACHE_TTL.skillMax);
|
|
178
|
-
}
|
|
179
136
|
export {
|
|
180
|
-
CACHE_TTL,
|
|
181
137
|
CreateSkillSchema,
|
|
182
138
|
JournalEntrySchema,
|
|
183
139
|
JournalEntryTypeSchema,
|
|
184
|
-
REDIS_KEYS,
|
|
185
140
|
SessionSummarySchema,
|
|
186
141
|
SkillCategorySchema,
|
|
187
142
|
SkillPrioritySchema,
|
|
188
143
|
SkillQuerySchema,
|
|
189
144
|
SkillSchema,
|
|
190
|
-
UpdateSkillSchema
|
|
191
|
-
calculateSkillTTL
|
|
145
|
+
UpdateSkillSchema
|
|
192
146
|
};
|