skill-tree 0.1.2 → 0.1.4
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/README.md +188 -140
- package/dist/chunk-7QIQJVNP.mjs +14206 -0
- package/dist/chunk-FKJJ4RJG.mjs +13874 -0
- package/dist/chunk-OYHYXKXO.mjs +7297 -0
- package/dist/chunk-PDPN7FW7.mjs +1045 -0
- package/dist/chunk-Y54UK2J3.mjs +13071 -0
- package/dist/cli/index.js +6257 -10937
- package/dist/cli/index.mjs +388 -107
- package/dist/index.d.mts +2197 -5682
- package/dist/index.d.ts +2197 -5682
- package/dist/index.js +7196 -14794
- package/dist/index.mjs +22 -140
- package/dist/sqlite-OLU72GHB.mjs +6 -0
- package/package.json +2 -3
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
// src/storage/sqlite.ts
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
|
|
6
|
+
// src/storage/base.ts
|
|
7
|
+
var BaseStorageAdapter = class {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.initialized = false;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Ensure storage is initialized before operations
|
|
13
|
+
*/
|
|
14
|
+
ensureInitialized() {
|
|
15
|
+
if (!this.initialized) {
|
|
16
|
+
throw new Error("Storage not initialized. Call initialize() first.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Apply filters to a list of skills
|
|
21
|
+
*/
|
|
22
|
+
applyFilter(skills, filter) {
|
|
23
|
+
if (!filter) return skills;
|
|
24
|
+
return skills.filter((skill) => {
|
|
25
|
+
if (filter.status && filter.status.length > 0) {
|
|
26
|
+
if (!filter.status.includes(skill.status)) return false;
|
|
27
|
+
}
|
|
28
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
29
|
+
const hasTag = filter.tags.some((tag) => skill.tags.includes(tag));
|
|
30
|
+
if (!hasTag) return false;
|
|
31
|
+
}
|
|
32
|
+
if (filter.author && skill.author !== filter.author) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (filter.minSuccessRate !== void 0 && skill.metrics.successRate < filter.minSuccessRate) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (filter.createdAfter && skill.createdAt < filter.createdAfter) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (filter.createdBefore && skill.createdAt > filter.createdBefore) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!this.applyNamespaceFilter(skill, filter)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Apply namespace-related filters to a skill
|
|
52
|
+
*/
|
|
53
|
+
applyNamespaceFilter(skill, filter) {
|
|
54
|
+
const namespace = skill.namespace;
|
|
55
|
+
if (filter.scope) {
|
|
56
|
+
const scopes = Array.isArray(filter.scope) ? filter.scope : [filter.scope];
|
|
57
|
+
const skillScope = namespace?.scope || "personal";
|
|
58
|
+
if (!scopes.includes(skillScope)) return false;
|
|
59
|
+
}
|
|
60
|
+
if (filter.owner) {
|
|
61
|
+
const skillOwner = namespace?.owner;
|
|
62
|
+
if (skillOwner !== filter.owner) return false;
|
|
63
|
+
}
|
|
64
|
+
if (filter.team) {
|
|
65
|
+
const skillTeam = namespace?.team;
|
|
66
|
+
if (skillTeam !== filter.team) return false;
|
|
67
|
+
}
|
|
68
|
+
if (filter.visibility) {
|
|
69
|
+
const visibilities = Array.isArray(filter.visibility) ? filter.visibility : [filter.visibility];
|
|
70
|
+
const skillVisibility = namespace?.visibility || "private";
|
|
71
|
+
if (!visibilities.includes(skillVisibility)) return false;
|
|
72
|
+
}
|
|
73
|
+
if (filter.accessibleBy) {
|
|
74
|
+
if (!this.canAgentAccessSkill(skill, filter.accessibleBy, filter.accessibleByTeam)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if an agent can access a skill based on namespace rules
|
|
82
|
+
*/
|
|
83
|
+
canAgentAccessSkill(skill, agentId, agentTeam) {
|
|
84
|
+
const namespace = skill.namespace;
|
|
85
|
+
if (!namespace) return true;
|
|
86
|
+
if (namespace.owner === agentId) return true;
|
|
87
|
+
switch (namespace.visibility) {
|
|
88
|
+
case "public":
|
|
89
|
+
return true;
|
|
90
|
+
case "team-only":
|
|
91
|
+
return agentTeam !== void 0 && agentTeam === namespace.team;
|
|
92
|
+
case "private":
|
|
93
|
+
return namespace.owner === agentId;
|
|
94
|
+
default:
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Simple text search across skill fields
|
|
100
|
+
*/
|
|
101
|
+
textSearch(skills, query) {
|
|
102
|
+
const lowerQuery = query.toLowerCase();
|
|
103
|
+
const terms = lowerQuery.split(/\s+/).filter((t) => t.length > 0);
|
|
104
|
+
return skills.filter((skill) => {
|
|
105
|
+
const searchText = [
|
|
106
|
+
skill.name,
|
|
107
|
+
skill.description,
|
|
108
|
+
skill.problem,
|
|
109
|
+
skill.solution,
|
|
110
|
+
...skill.tags,
|
|
111
|
+
skill.triggerConditions.map((t) => t.value).join(" ")
|
|
112
|
+
].join(" ").toLowerCase();
|
|
113
|
+
return terms.every((term) => searchText.includes(term));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var MemoryStorageAdapter = class extends BaseStorageAdapter {
|
|
118
|
+
constructor() {
|
|
119
|
+
super(...arguments);
|
|
120
|
+
this.skills = /* @__PURE__ */ new Map();
|
|
121
|
+
// skillId -> version -> skill
|
|
122
|
+
this.lineages = /* @__PURE__ */ new Map();
|
|
123
|
+
}
|
|
124
|
+
async initialize() {
|
|
125
|
+
this.initialized = true;
|
|
126
|
+
}
|
|
127
|
+
async saveSkill(skill) {
|
|
128
|
+
this.ensureInitialized();
|
|
129
|
+
if (!this.skills.has(skill.id)) {
|
|
130
|
+
this.skills.set(skill.id, /* @__PURE__ */ new Map());
|
|
131
|
+
}
|
|
132
|
+
const versionMap = this.skills.get(skill.id);
|
|
133
|
+
versionMap.set(skill.version, { ...skill });
|
|
134
|
+
this.updateLineage(skill);
|
|
135
|
+
}
|
|
136
|
+
async getSkill(id, version) {
|
|
137
|
+
this.ensureInitialized();
|
|
138
|
+
const versionMap = this.skills.get(id);
|
|
139
|
+
if (!versionMap) return null;
|
|
140
|
+
if (version) {
|
|
141
|
+
return versionMap.get(version) || null;
|
|
142
|
+
}
|
|
143
|
+
const versions = Array.from(versionMap.keys()).sort(this.compareVersions);
|
|
144
|
+
const latestVersion = versions[versions.length - 1];
|
|
145
|
+
return latestVersion ? versionMap.get(latestVersion) || null : null;
|
|
146
|
+
}
|
|
147
|
+
async listSkills(filter) {
|
|
148
|
+
this.ensureInitialized();
|
|
149
|
+
const skills = [];
|
|
150
|
+
for (const versionMap of this.skills.values()) {
|
|
151
|
+
const versions = Array.from(versionMap.keys()).sort(this.compareVersions);
|
|
152
|
+
const latestVersion = versions[versions.length - 1];
|
|
153
|
+
if (latestVersion) {
|
|
154
|
+
const skill = versionMap.get(latestVersion);
|
|
155
|
+
if (skill) skills.push(skill);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return this.applyFilter(skills, filter);
|
|
159
|
+
}
|
|
160
|
+
async deleteSkill(id, version) {
|
|
161
|
+
this.ensureInitialized();
|
|
162
|
+
if (version) {
|
|
163
|
+
const versionMap = this.skills.get(id);
|
|
164
|
+
if (!versionMap) return false;
|
|
165
|
+
return versionMap.delete(version);
|
|
166
|
+
}
|
|
167
|
+
const existed = this.skills.has(id);
|
|
168
|
+
this.skills.delete(id);
|
|
169
|
+
this.lineages.delete(id);
|
|
170
|
+
return existed;
|
|
171
|
+
}
|
|
172
|
+
async getVersionHistory(skillId) {
|
|
173
|
+
this.ensureInitialized();
|
|
174
|
+
const versionMap = this.skills.get(skillId);
|
|
175
|
+
if (!versionMap) return [];
|
|
176
|
+
const versions = [];
|
|
177
|
+
for (const [version, skill] of versionMap) {
|
|
178
|
+
versions.push({
|
|
179
|
+
skillId,
|
|
180
|
+
version,
|
|
181
|
+
skill,
|
|
182
|
+
changelog: "",
|
|
183
|
+
// Not tracked in memory
|
|
184
|
+
createdAt: skill.createdAt,
|
|
185
|
+
contentHash: this.hashSkill(skill)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return versions.sort((a, b) => this.compareVersions(a.version, b.version));
|
|
189
|
+
}
|
|
190
|
+
async getLineage(skillId) {
|
|
191
|
+
this.ensureInitialized();
|
|
192
|
+
return this.lineages.get(skillId) || null;
|
|
193
|
+
}
|
|
194
|
+
async searchSkills(query) {
|
|
195
|
+
this.ensureInitialized();
|
|
196
|
+
const allSkills = await this.listSkills();
|
|
197
|
+
return this.textSearch(allSkills, query);
|
|
198
|
+
}
|
|
199
|
+
updateLineage(skill) {
|
|
200
|
+
if (!this.lineages.has(skill.id)) {
|
|
201
|
+
this.lineages.set(skill.id, {
|
|
202
|
+
rootId: skill.id,
|
|
203
|
+
versions: [],
|
|
204
|
+
forks: []
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const lineage = this.lineages.get(skill.id);
|
|
208
|
+
const existingIndex = lineage.versions.findIndex((v) => v.version === skill.version);
|
|
209
|
+
const versionEntry = {
|
|
210
|
+
skillId: skill.id,
|
|
211
|
+
version: skill.version,
|
|
212
|
+
skill,
|
|
213
|
+
changelog: "",
|
|
214
|
+
createdAt: skill.createdAt,
|
|
215
|
+
contentHash: this.hashSkill(skill)
|
|
216
|
+
};
|
|
217
|
+
if (existingIndex >= 0) {
|
|
218
|
+
lineage.versions[existingIndex] = versionEntry;
|
|
219
|
+
} else {
|
|
220
|
+
lineage.versions.push(versionEntry);
|
|
221
|
+
lineage.versions.sort((a, b) => this.compareVersions(a.version, b.version));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
compareVersions(a, b) {
|
|
225
|
+
const partsA = a.split(".").map(Number);
|
|
226
|
+
const partsB = b.split(".").map(Number);
|
|
227
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
228
|
+
const numA = partsA[i] || 0;
|
|
229
|
+
const numB = partsB[i] || 0;
|
|
230
|
+
if (numA !== numB) return numA - numB;
|
|
231
|
+
}
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
hashSkill(skill) {
|
|
235
|
+
const content = JSON.stringify({
|
|
236
|
+
problem: skill.problem,
|
|
237
|
+
solution: skill.solution,
|
|
238
|
+
triggerConditions: skill.triggerConditions
|
|
239
|
+
});
|
|
240
|
+
let hash = 0;
|
|
241
|
+
for (let i = 0; i < content.length; i++) {
|
|
242
|
+
const char = content.charCodeAt(i);
|
|
243
|
+
hash = (hash << 5) - hash + char;
|
|
244
|
+
hash = hash & hash;
|
|
245
|
+
}
|
|
246
|
+
return Math.abs(hash).toString(16);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Clear all stored skills (for testing)
|
|
250
|
+
*/
|
|
251
|
+
clear() {
|
|
252
|
+
this.skills.clear();
|
|
253
|
+
this.lineages.clear();
|
|
254
|
+
}
|
|
255
|
+
// ==========================================================================
|
|
256
|
+
// Fork Tracking
|
|
257
|
+
// ==========================================================================
|
|
258
|
+
async recordFork(sourceSkillId, fork) {
|
|
259
|
+
this.ensureInitialized();
|
|
260
|
+
const lineage = this.lineages.get(sourceSkillId);
|
|
261
|
+
if (lineage) {
|
|
262
|
+
const exists = lineage.forks.some(
|
|
263
|
+
(f) => f.forkedSkillId === fork.forkedSkillId
|
|
264
|
+
);
|
|
265
|
+
if (!exists) {
|
|
266
|
+
lineage.forks.push(fork);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/storage/sqlite.ts
|
|
273
|
+
var SCHEMA_VERSION = 2;
|
|
274
|
+
var SQLiteStorageAdapter = class extends BaseStorageAdapter {
|
|
275
|
+
constructor(config) {
|
|
276
|
+
super();
|
|
277
|
+
this.db = null;
|
|
278
|
+
this.config = {
|
|
279
|
+
walMode: true,
|
|
280
|
+
enableFTS: true,
|
|
281
|
+
...config
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async initialize() {
|
|
285
|
+
const dir = path.dirname(this.config.dbPath);
|
|
286
|
+
if (dir && !fs.existsSync(dir)) {
|
|
287
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
this.db = new Database(this.config.dbPath);
|
|
290
|
+
if (this.config.walMode) {
|
|
291
|
+
this.db.pragma("journal_mode = WAL");
|
|
292
|
+
}
|
|
293
|
+
this.db.pragma("foreign_keys = ON");
|
|
294
|
+
this.createSchema();
|
|
295
|
+
this.runMigrations();
|
|
296
|
+
this.initialized = true;
|
|
297
|
+
}
|
|
298
|
+
createSchema() {
|
|
299
|
+
const db = this.getDb();
|
|
300
|
+
db.exec(`
|
|
301
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
302
|
+
version INTEGER PRIMARY KEY
|
|
303
|
+
)
|
|
304
|
+
`);
|
|
305
|
+
db.exec(`
|
|
306
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
307
|
+
id TEXT PRIMARY KEY,
|
|
308
|
+
version TEXT NOT NULL,
|
|
309
|
+
name TEXT NOT NULL,
|
|
310
|
+
description TEXT NOT NULL,
|
|
311
|
+
problem TEXT NOT NULL,
|
|
312
|
+
trigger_conditions TEXT NOT NULL,
|
|
313
|
+
solution TEXT NOT NULL,
|
|
314
|
+
verification TEXT NOT NULL,
|
|
315
|
+
examples TEXT NOT NULL,
|
|
316
|
+
notes TEXT,
|
|
317
|
+
author TEXT NOT NULL,
|
|
318
|
+
tags TEXT NOT NULL,
|
|
319
|
+
created_at TEXT NOT NULL,
|
|
320
|
+
updated_at TEXT NOT NULL,
|
|
321
|
+
status TEXT NOT NULL,
|
|
322
|
+
parent_version TEXT,
|
|
323
|
+
derived_from TEXT,
|
|
324
|
+
metrics TEXT NOT NULL,
|
|
325
|
+
source TEXT,
|
|
326
|
+
taxonomy TEXT,
|
|
327
|
+
external_source TEXT
|
|
328
|
+
)
|
|
329
|
+
`);
|
|
330
|
+
db.exec(`
|
|
331
|
+
CREATE TABLE IF NOT EXISTS skill_versions (
|
|
332
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
333
|
+
skill_id TEXT NOT NULL,
|
|
334
|
+
version TEXT NOT NULL,
|
|
335
|
+
skill_data TEXT NOT NULL,
|
|
336
|
+
changelog TEXT NOT NULL,
|
|
337
|
+
created_at TEXT NOT NULL,
|
|
338
|
+
content_hash TEXT NOT NULL,
|
|
339
|
+
UNIQUE(skill_id, version)
|
|
340
|
+
)
|
|
341
|
+
`);
|
|
342
|
+
db.exec(`
|
|
343
|
+
CREATE TABLE IF NOT EXISTS skill_lineage (
|
|
344
|
+
skill_id TEXT PRIMARY KEY,
|
|
345
|
+
root_id TEXT NOT NULL
|
|
346
|
+
)
|
|
347
|
+
`);
|
|
348
|
+
db.exec(`
|
|
349
|
+
CREATE TABLE IF NOT EXISTS skill_forks (
|
|
350
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
351
|
+
root_skill_id TEXT NOT NULL,
|
|
352
|
+
forked_skill_id TEXT NOT NULL,
|
|
353
|
+
from_version TEXT NOT NULL,
|
|
354
|
+
reason TEXT NOT NULL,
|
|
355
|
+
forked_at TEXT NOT NULL,
|
|
356
|
+
FOREIGN KEY (root_skill_id) REFERENCES skill_lineage(skill_id)
|
|
357
|
+
)
|
|
358
|
+
`);
|
|
359
|
+
db.exec(`
|
|
360
|
+
CREATE INDEX IF NOT EXISTS idx_skills_status ON skills(status);
|
|
361
|
+
CREATE INDEX IF NOT EXISTS idx_skills_author ON skills(author);
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_skills_updated ON skills(updated_at);
|
|
363
|
+
CREATE INDEX IF NOT EXISTS idx_versions_skill ON skill_versions(skill_id);
|
|
364
|
+
`);
|
|
365
|
+
if (this.config.enableFTS) {
|
|
366
|
+
db.exec(`
|
|
367
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
368
|
+
skill_id,
|
|
369
|
+
name,
|
|
370
|
+
description,
|
|
371
|
+
problem,
|
|
372
|
+
solution,
|
|
373
|
+
tags,
|
|
374
|
+
trigger_values
|
|
375
|
+
)
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
378
|
+
db.exec(`
|
|
379
|
+
CREATE TABLE IF NOT EXISTS taxonomy_nodes (
|
|
380
|
+
id TEXT PRIMARY KEY,
|
|
381
|
+
name TEXT NOT NULL,
|
|
382
|
+
description TEXT,
|
|
383
|
+
parent_id TEXT REFERENCES taxonomy_nodes(id),
|
|
384
|
+
path TEXT NOT NULL,
|
|
385
|
+
skill_count INTEGER DEFAULT 0,
|
|
386
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
387
|
+
)
|
|
388
|
+
`);
|
|
389
|
+
db.exec(`
|
|
390
|
+
CREATE TABLE IF NOT EXISTS skill_taxonomy_placements (
|
|
391
|
+
skill_id TEXT NOT NULL,
|
|
392
|
+
node_id TEXT NOT NULL,
|
|
393
|
+
is_primary INTEGER DEFAULT 0,
|
|
394
|
+
confidence REAL,
|
|
395
|
+
reasoning TEXT,
|
|
396
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
397
|
+
PRIMARY KEY (skill_id, node_id)
|
|
398
|
+
)
|
|
399
|
+
`);
|
|
400
|
+
db.exec(`
|
|
401
|
+
CREATE TABLE IF NOT EXISTS skill_relationships (
|
|
402
|
+
source_skill_id TEXT NOT NULL,
|
|
403
|
+
target_skill_id TEXT NOT NULL,
|
|
404
|
+
type TEXT NOT NULL,
|
|
405
|
+
confidence REAL NOT NULL,
|
|
406
|
+
reasoning TEXT,
|
|
407
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
408
|
+
PRIMARY KEY (source_skill_id, target_skill_id, type)
|
|
409
|
+
)
|
|
410
|
+
`);
|
|
411
|
+
db.exec(`
|
|
412
|
+
CREATE TABLE IF NOT EXISTS skill_sources (
|
|
413
|
+
id TEXT PRIMARY KEY,
|
|
414
|
+
type TEXT NOT NULL,
|
|
415
|
+
url TEXT,
|
|
416
|
+
last_scraped TEXT,
|
|
417
|
+
etag TEXT,
|
|
418
|
+
skill_count INTEGER DEFAULT 0
|
|
419
|
+
)
|
|
420
|
+
`);
|
|
421
|
+
db.exec(`
|
|
422
|
+
CREATE INDEX IF NOT EXISTS idx_taxonomy_parent ON taxonomy_nodes(parent_id);
|
|
423
|
+
CREATE INDEX IF NOT EXISTS idx_taxonomy_path ON taxonomy_nodes(path);
|
|
424
|
+
CREATE INDEX IF NOT EXISTS idx_placements_skill ON skill_taxonomy_placements(skill_id);
|
|
425
|
+
CREATE INDEX IF NOT EXISTS idx_placements_node ON skill_taxonomy_placements(node_id);
|
|
426
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_source ON skill_relationships(source_skill_id);
|
|
427
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_target ON skill_relationships(target_skill_id);
|
|
428
|
+
`);
|
|
429
|
+
const version = db.prepare("SELECT version FROM schema_version").get();
|
|
430
|
+
if (!version) {
|
|
431
|
+
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
runMigrations() {
|
|
435
|
+
const db = this.getDb();
|
|
436
|
+
const row = db.prepare("SELECT version FROM schema_version").get();
|
|
437
|
+
const currentVersion = row?.version || 0;
|
|
438
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
439
|
+
if (currentVersion < 2) {
|
|
440
|
+
try {
|
|
441
|
+
db.exec("ALTER TABLE skills ADD COLUMN taxonomy TEXT");
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
db.exec("ALTER TABLE skills ADD COLUMN external_source TEXT");
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
getDb() {
|
|
453
|
+
if (!this.db) {
|
|
454
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
455
|
+
}
|
|
456
|
+
return this.db;
|
|
457
|
+
}
|
|
458
|
+
async saveSkill(skill) {
|
|
459
|
+
this.ensureInitialized();
|
|
460
|
+
const db = this.getDb();
|
|
461
|
+
const stmt = db.prepare(`
|
|
462
|
+
INSERT OR REPLACE INTO skills (
|
|
463
|
+
id, version, name, description, problem, trigger_conditions, solution,
|
|
464
|
+
verification, examples, notes, author, tags, created_at, updated_at,
|
|
465
|
+
status, parent_version, derived_from, metrics, source, taxonomy, external_source
|
|
466
|
+
) VALUES (
|
|
467
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
468
|
+
)
|
|
469
|
+
`);
|
|
470
|
+
stmt.run(
|
|
471
|
+
skill.id,
|
|
472
|
+
skill.version,
|
|
473
|
+
skill.name,
|
|
474
|
+
skill.description,
|
|
475
|
+
skill.problem,
|
|
476
|
+
JSON.stringify(skill.triggerConditions),
|
|
477
|
+
skill.solution,
|
|
478
|
+
skill.verification,
|
|
479
|
+
JSON.stringify(skill.examples),
|
|
480
|
+
skill.notes || null,
|
|
481
|
+
skill.author,
|
|
482
|
+
JSON.stringify(skill.tags),
|
|
483
|
+
skill.createdAt.toISOString(),
|
|
484
|
+
skill.updatedAt.toISOString(),
|
|
485
|
+
skill.status,
|
|
486
|
+
skill.parentVersion || null,
|
|
487
|
+
skill.derivedFrom ? JSON.stringify(skill.derivedFrom) : null,
|
|
488
|
+
JSON.stringify(skill.metrics),
|
|
489
|
+
skill.source ? JSON.stringify(skill.source) : null,
|
|
490
|
+
skill.taxonomy ? JSON.stringify(skill.taxonomy) : null,
|
|
491
|
+
skill.externalSource ? JSON.stringify({
|
|
492
|
+
...skill.externalSource,
|
|
493
|
+
scrapedAt: skill.externalSource.scrapedAt.toISOString()
|
|
494
|
+
}) : null
|
|
495
|
+
);
|
|
496
|
+
if (skill.relationships && skill.relationships.length > 0) {
|
|
497
|
+
await this.saveSkillRelationships(skill.id, skill.relationships);
|
|
498
|
+
}
|
|
499
|
+
if (this.config.enableFTS) {
|
|
500
|
+
this.updateFTSIndex(skill);
|
|
501
|
+
}
|
|
502
|
+
await this.saveVersionSnapshot(skill);
|
|
503
|
+
this.updateLineage(skill);
|
|
504
|
+
}
|
|
505
|
+
updateFTSIndex(skill) {
|
|
506
|
+
const db = this.getDb();
|
|
507
|
+
db.prepare("DELETE FROM skills_fts WHERE skill_id = ?").run(skill.id);
|
|
508
|
+
const triggerValues = skill.triggerConditions.map((t) => t.value).join(" ");
|
|
509
|
+
const tags = skill.tags.join(" ");
|
|
510
|
+
db.prepare(`
|
|
511
|
+
INSERT INTO skills_fts (skill_id, name, description, problem, solution, tags, trigger_values)
|
|
512
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
513
|
+
`).run(skill.id, skill.name, skill.description, skill.problem, skill.solution, tags, triggerValues);
|
|
514
|
+
}
|
|
515
|
+
async saveVersionSnapshot(skill) {
|
|
516
|
+
const db = this.getDb();
|
|
517
|
+
const existing = db.prepare(
|
|
518
|
+
"SELECT id FROM skill_versions WHERE skill_id = ? AND version = ?"
|
|
519
|
+
).get(skill.id, skill.version);
|
|
520
|
+
if (existing) {
|
|
521
|
+
db.prepare(`
|
|
522
|
+
UPDATE skill_versions
|
|
523
|
+
SET skill_data = ?, content_hash = ?
|
|
524
|
+
WHERE skill_id = ? AND version = ?
|
|
525
|
+
`).run(
|
|
526
|
+
JSON.stringify(this.serializeSkill(skill)),
|
|
527
|
+
this.hashSkill(skill),
|
|
528
|
+
skill.id,
|
|
529
|
+
skill.version
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
db.prepare(`
|
|
533
|
+
INSERT INTO skill_versions (skill_id, version, skill_data, changelog, created_at, content_hash)
|
|
534
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
535
|
+
`).run(
|
|
536
|
+
skill.id,
|
|
537
|
+
skill.version,
|
|
538
|
+
JSON.stringify(this.serializeSkill(skill)),
|
|
539
|
+
"",
|
|
540
|
+
// Changelog can be added via separate method
|
|
541
|
+
skill.createdAt.toISOString(),
|
|
542
|
+
this.hashSkill(skill)
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
updateLineage(skill) {
|
|
547
|
+
const db = this.getDb();
|
|
548
|
+
const existing = db.prepare(
|
|
549
|
+
"SELECT skill_id FROM skill_lineage WHERE skill_id = ?"
|
|
550
|
+
).get(skill.id);
|
|
551
|
+
if (!existing) {
|
|
552
|
+
db.prepare(
|
|
553
|
+
"INSERT INTO skill_lineage (skill_id, root_id) VALUES (?, ?)"
|
|
554
|
+
).run(skill.id, skill.id);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async getSkill(id, version) {
|
|
558
|
+
this.ensureInitialized();
|
|
559
|
+
const db = this.getDb();
|
|
560
|
+
if (version) {
|
|
561
|
+
const row2 = db.prepare(
|
|
562
|
+
"SELECT skill_data FROM skill_versions WHERE skill_id = ? AND version = ?"
|
|
563
|
+
).get(id, version);
|
|
564
|
+
if (!row2) return null;
|
|
565
|
+
return this.deserializeSkill(JSON.parse(row2.skill_data));
|
|
566
|
+
}
|
|
567
|
+
const row = db.prepare("SELECT * FROM skills WHERE id = ?").get(id);
|
|
568
|
+
if (!row) return null;
|
|
569
|
+
return this.rowToSkill(row);
|
|
570
|
+
}
|
|
571
|
+
async listSkills(filter) {
|
|
572
|
+
this.ensureInitialized();
|
|
573
|
+
const db = this.getDb();
|
|
574
|
+
let sql = "SELECT * FROM skills WHERE 1=1";
|
|
575
|
+
const params = [];
|
|
576
|
+
if (filter?.status && filter.status.length > 0) {
|
|
577
|
+
sql += ` AND status IN (${filter.status.map(() => "?").join(",")})`;
|
|
578
|
+
params.push(...filter.status);
|
|
579
|
+
}
|
|
580
|
+
if (filter?.author) {
|
|
581
|
+
sql += " AND author = ?";
|
|
582
|
+
params.push(filter.author);
|
|
583
|
+
}
|
|
584
|
+
if (filter?.minSuccessRate !== void 0) {
|
|
585
|
+
sql += " AND json_extract(metrics, '$.successRate') >= ?";
|
|
586
|
+
params.push(filter.minSuccessRate);
|
|
587
|
+
}
|
|
588
|
+
if (filter?.createdAfter) {
|
|
589
|
+
sql += " AND created_at >= ?";
|
|
590
|
+
params.push(filter.createdAfter.toISOString());
|
|
591
|
+
}
|
|
592
|
+
if (filter?.createdBefore) {
|
|
593
|
+
sql += " AND created_at <= ?";
|
|
594
|
+
params.push(filter.createdBefore.toISOString());
|
|
595
|
+
}
|
|
596
|
+
sql += " ORDER BY updated_at DESC";
|
|
597
|
+
const rows = db.prepare(sql).all(...params);
|
|
598
|
+
let skills = rows.map((row) => this.rowToSkill(row));
|
|
599
|
+
if (filter?.tags && filter.tags.length > 0) {
|
|
600
|
+
skills = skills.filter(
|
|
601
|
+
(skill) => filter.tags.some((tag) => skill.tags.includes(tag))
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
return skills;
|
|
605
|
+
}
|
|
606
|
+
async deleteSkill(id, version) {
|
|
607
|
+
this.ensureInitialized();
|
|
608
|
+
const db = this.getDb();
|
|
609
|
+
if (version) {
|
|
610
|
+
const result = db.prepare(
|
|
611
|
+
"DELETE FROM skill_versions WHERE skill_id = ? AND version = ?"
|
|
612
|
+
).run(id, version);
|
|
613
|
+
return result.changes > 0;
|
|
614
|
+
}
|
|
615
|
+
const transaction = db.transaction(() => {
|
|
616
|
+
db.prepare("DELETE FROM skill_versions WHERE skill_id = ?").run(id);
|
|
617
|
+
db.prepare("DELETE FROM skill_forks WHERE root_skill_id = ? OR forked_skill_id = ?").run(id, id);
|
|
618
|
+
db.prepare("DELETE FROM skill_lineage WHERE skill_id = ?").run(id);
|
|
619
|
+
if (this.config.enableFTS) {
|
|
620
|
+
db.prepare("DELETE FROM skills_fts WHERE skill_id = ?").run(id);
|
|
621
|
+
}
|
|
622
|
+
db.prepare("DELETE FROM skill_taxonomy_placements WHERE skill_id = ?").run(id);
|
|
623
|
+
db.prepare("DELETE FROM skill_relationships WHERE source_skill_id = ? OR target_skill_id = ?").run(id, id);
|
|
624
|
+
const result = db.prepare("DELETE FROM skills WHERE id = ?").run(id);
|
|
625
|
+
return result.changes > 0;
|
|
626
|
+
});
|
|
627
|
+
return transaction();
|
|
628
|
+
}
|
|
629
|
+
async getVersionHistory(skillId) {
|
|
630
|
+
this.ensureInitialized();
|
|
631
|
+
const db = this.getDb();
|
|
632
|
+
const rows = db.prepare(`
|
|
633
|
+
SELECT skill_id, version, skill_data, changelog, created_at, content_hash
|
|
634
|
+
FROM skill_versions
|
|
635
|
+
WHERE skill_id = ?
|
|
636
|
+
ORDER BY created_at ASC
|
|
637
|
+
`).all(skillId);
|
|
638
|
+
return rows.map((row) => ({
|
|
639
|
+
skillId: row.skill_id,
|
|
640
|
+
version: row.version,
|
|
641
|
+
skill: this.deserializeSkill(JSON.parse(row.skill_data)),
|
|
642
|
+
changelog: row.changelog,
|
|
643
|
+
createdAt: new Date(row.created_at),
|
|
644
|
+
contentHash: row.content_hash
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
async getLineage(skillId) {
|
|
648
|
+
this.ensureInitialized();
|
|
649
|
+
const db = this.getDb();
|
|
650
|
+
const lineageRow = db.prepare(
|
|
651
|
+
"SELECT * FROM skill_lineage WHERE skill_id = ?"
|
|
652
|
+
).get(skillId);
|
|
653
|
+
if (!lineageRow) return null;
|
|
654
|
+
const versions = await this.getVersionHistory(skillId);
|
|
655
|
+
const forkRows = db.prepare(`
|
|
656
|
+
SELECT forked_skill_id, from_version, reason, forked_at
|
|
657
|
+
FROM skill_forks
|
|
658
|
+
WHERE root_skill_id = ?
|
|
659
|
+
`).all(skillId);
|
|
660
|
+
const forks = forkRows.map((row) => ({
|
|
661
|
+
forkedSkillId: row.forked_skill_id,
|
|
662
|
+
fromVersion: row.from_version,
|
|
663
|
+
reason: row.reason,
|
|
664
|
+
forkedAt: new Date(row.forked_at)
|
|
665
|
+
}));
|
|
666
|
+
return {
|
|
667
|
+
rootId: lineageRow.root_id,
|
|
668
|
+
versions,
|
|
669
|
+
forks
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async searchSkills(query) {
|
|
673
|
+
this.ensureInitialized();
|
|
674
|
+
const db = this.getDb();
|
|
675
|
+
if (this.config.enableFTS) {
|
|
676
|
+
const rows = db.prepare(`
|
|
677
|
+
SELECT s.* FROM skills s
|
|
678
|
+
JOIN skills_fts fts ON s.id = fts.skill_id
|
|
679
|
+
WHERE skills_fts MATCH ?
|
|
680
|
+
ORDER BY rank
|
|
681
|
+
`).all(query);
|
|
682
|
+
return rows.map((row) => this.rowToSkill(row));
|
|
683
|
+
}
|
|
684
|
+
const allSkills = await this.listSkills();
|
|
685
|
+
return this.textSearch(allSkills, query);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Add a fork record
|
|
689
|
+
*/
|
|
690
|
+
async addFork(rootSkillId, forkedSkillId, fromVersion, reason) {
|
|
691
|
+
this.ensureInitialized();
|
|
692
|
+
const db = this.getDb();
|
|
693
|
+
db.prepare(`
|
|
694
|
+
INSERT INTO skill_forks (root_skill_id, forked_skill_id, from_version, reason, forked_at)
|
|
695
|
+
VALUES (?, ?, ?, ?, ?)
|
|
696
|
+
`).run(rootSkillId, forkedSkillId, fromVersion, reason, (/* @__PURE__ */ new Date()).toISOString());
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Update changelog for a version
|
|
700
|
+
*/
|
|
701
|
+
async updateChangelog(skillId, version, changelog) {
|
|
702
|
+
this.ensureInitialized();
|
|
703
|
+
const db = this.getDb();
|
|
704
|
+
db.prepare(`
|
|
705
|
+
UPDATE skill_versions SET changelog = ? WHERE skill_id = ? AND version = ?
|
|
706
|
+
`).run(changelog, skillId, version);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get skills by tag
|
|
710
|
+
*/
|
|
711
|
+
async getSkillsByTag(tag) {
|
|
712
|
+
return this.listSkills({ tags: [tag] });
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Get all tags with counts
|
|
716
|
+
*/
|
|
717
|
+
async getTagCounts() {
|
|
718
|
+
this.ensureInitialized();
|
|
719
|
+
const db = this.getDb();
|
|
720
|
+
const rows = db.prepare("SELECT tags FROM skills").all();
|
|
721
|
+
const counts = /* @__PURE__ */ new Map();
|
|
722
|
+
for (const row of rows) {
|
|
723
|
+
const tags = JSON.parse(row.tags);
|
|
724
|
+
for (const tag of tags) {
|
|
725
|
+
counts.set(tag, (counts.get(tag) || 0) + 1);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return counts;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Get skill count by status
|
|
732
|
+
*/
|
|
733
|
+
async getStatusCounts() {
|
|
734
|
+
this.ensureInitialized();
|
|
735
|
+
const db = this.getDb();
|
|
736
|
+
const rows = db.prepare(`
|
|
737
|
+
SELECT status, COUNT(*) as count FROM skills GROUP BY status
|
|
738
|
+
`).all();
|
|
739
|
+
const counts = /* @__PURE__ */ new Map();
|
|
740
|
+
for (const row of rows) {
|
|
741
|
+
counts.set(row.status, row.count);
|
|
742
|
+
}
|
|
743
|
+
return counts;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Close the database connection
|
|
747
|
+
*/
|
|
748
|
+
close() {
|
|
749
|
+
if (this.db) {
|
|
750
|
+
this.db.close();
|
|
751
|
+
this.db = null;
|
|
752
|
+
this.initialized = false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Export all skills for backup
|
|
757
|
+
*/
|
|
758
|
+
async exportAll() {
|
|
759
|
+
return this.listSkills();
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Import skills from backup
|
|
763
|
+
*/
|
|
764
|
+
async importSkills(skills) {
|
|
765
|
+
let imported = 0;
|
|
766
|
+
let failed = 0;
|
|
767
|
+
for (const skill of skills) {
|
|
768
|
+
try {
|
|
769
|
+
await this.saveSkill(skill);
|
|
770
|
+
imported++;
|
|
771
|
+
} catch {
|
|
772
|
+
failed++;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return { imported, failed };
|
|
776
|
+
}
|
|
777
|
+
// =========================================================================
|
|
778
|
+
// TAXONOMY METHODS
|
|
779
|
+
// =========================================================================
|
|
780
|
+
/**
|
|
781
|
+
* Get or create a taxonomy node
|
|
782
|
+
*/
|
|
783
|
+
async ensureTaxonomyNode(path2) {
|
|
784
|
+
this.ensureInitialized();
|
|
785
|
+
const db = this.getDb();
|
|
786
|
+
const pathStr = path2.join("/");
|
|
787
|
+
const existing = db.prepare("SELECT id FROM taxonomy_nodes WHERE path = ?").get(pathStr);
|
|
788
|
+
if (existing) return existing.id;
|
|
789
|
+
const id = `node-${pathStr.replace(/\//g, "-").toLowerCase()}`;
|
|
790
|
+
const name = path2[path2.length - 1] || "Root";
|
|
791
|
+
const parentPath = path2.slice(0, -1);
|
|
792
|
+
let parentId = null;
|
|
793
|
+
if (parentPath.length > 0) {
|
|
794
|
+
parentId = await this.ensureTaxonomyNode(parentPath);
|
|
795
|
+
}
|
|
796
|
+
db.prepare(`
|
|
797
|
+
INSERT INTO taxonomy_nodes (id, name, parent_id, path, created_at)
|
|
798
|
+
VALUES (?, ?, ?, ?, ?)
|
|
799
|
+
`).run(id, name, parentId, pathStr, (/* @__PURE__ */ new Date()).toISOString());
|
|
800
|
+
return id;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Place a skill in the taxonomy
|
|
804
|
+
*/
|
|
805
|
+
async placeInTaxonomy(skillId, taxonomy) {
|
|
806
|
+
this.ensureInitialized();
|
|
807
|
+
const db = this.getDb();
|
|
808
|
+
const primaryNodeId = await this.ensureTaxonomyNode(taxonomy.primaryPath);
|
|
809
|
+
db.prepare(`
|
|
810
|
+
INSERT OR REPLACE INTO skill_taxonomy_placements
|
|
811
|
+
(skill_id, node_id, is_primary, confidence, created_at)
|
|
812
|
+
VALUES (?, ?, 1, ?, ?)
|
|
813
|
+
`).run(skillId, primaryNodeId, taxonomy.confidence || null, (/* @__PURE__ */ new Date()).toISOString());
|
|
814
|
+
db.prepare("UPDATE taxonomy_nodes SET skill_count = skill_count + 1 WHERE id = ?").run(primaryNodeId);
|
|
815
|
+
if (taxonomy.secondaryPaths) {
|
|
816
|
+
for (const secondaryPath of taxonomy.secondaryPaths) {
|
|
817
|
+
const secondaryNodeId = await this.ensureTaxonomyNode(secondaryPath);
|
|
818
|
+
db.prepare(`
|
|
819
|
+
INSERT OR IGNORE INTO skill_taxonomy_placements
|
|
820
|
+
(skill_id, node_id, is_primary, created_at)
|
|
821
|
+
VALUES (?, ?, 0, ?)
|
|
822
|
+
`).run(skillId, secondaryNodeId, (/* @__PURE__ */ new Date()).toISOString());
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Get taxonomy tree
|
|
828
|
+
*/
|
|
829
|
+
async getTaxonomyTree(rootPath) {
|
|
830
|
+
this.ensureInitialized();
|
|
831
|
+
const db = this.getDb();
|
|
832
|
+
let sql = "SELECT * FROM taxonomy_nodes";
|
|
833
|
+
const params = [];
|
|
834
|
+
if (rootPath && rootPath.length > 0) {
|
|
835
|
+
const pathPrefix = rootPath.join("/");
|
|
836
|
+
sql += " WHERE path LIKE ? OR path = ?";
|
|
837
|
+
params.push(`${pathPrefix}/%`, pathPrefix);
|
|
838
|
+
}
|
|
839
|
+
sql += " ORDER BY path";
|
|
840
|
+
const rows = db.prepare(sql).all(...params);
|
|
841
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
842
|
+
const roots = [];
|
|
843
|
+
for (const row of rows) {
|
|
844
|
+
const node = {
|
|
845
|
+
id: row.id,
|
|
846
|
+
name: row.name,
|
|
847
|
+
path: row.path.split("/"),
|
|
848
|
+
skillCount: row.skill_count,
|
|
849
|
+
children: []
|
|
850
|
+
};
|
|
851
|
+
nodeMap.set(row.id, node);
|
|
852
|
+
if (row.parent_id && nodeMap.has(row.parent_id)) {
|
|
853
|
+
nodeMap.get(row.parent_id).children.push(node);
|
|
854
|
+
} else {
|
|
855
|
+
roots.push(node);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return roots;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Get skills in a taxonomy node
|
|
862
|
+
*/
|
|
863
|
+
async getSkillsInTaxonomyNode(nodeId) {
|
|
864
|
+
this.ensureInitialized();
|
|
865
|
+
const db = this.getDb();
|
|
866
|
+
const rows = db.prepare(`
|
|
867
|
+
SELECT s.* FROM skills s
|
|
868
|
+
JOIN skill_taxonomy_placements p ON s.id = p.skill_id
|
|
869
|
+
WHERE p.node_id = ?
|
|
870
|
+
`).all(nodeId);
|
|
871
|
+
return rows.map((row) => this.rowToSkill(row));
|
|
872
|
+
}
|
|
873
|
+
// =========================================================================
|
|
874
|
+
// RELATIONSHIP METHODS
|
|
875
|
+
// =========================================================================
|
|
876
|
+
/**
|
|
877
|
+
* Save skill relationships
|
|
878
|
+
*/
|
|
879
|
+
async saveSkillRelationships(skillId, relationships) {
|
|
880
|
+
const db = this.getDb();
|
|
881
|
+
db.prepare("DELETE FROM skill_relationships WHERE source_skill_id = ?").run(skillId);
|
|
882
|
+
const stmt = db.prepare(`
|
|
883
|
+
INSERT OR REPLACE INTO skill_relationships
|
|
884
|
+
(source_skill_id, target_skill_id, type, confidence, reasoning, created_at)
|
|
885
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
886
|
+
`);
|
|
887
|
+
for (const rel of relationships) {
|
|
888
|
+
stmt.run(
|
|
889
|
+
skillId,
|
|
890
|
+
rel.targetSkillId,
|
|
891
|
+
rel.type,
|
|
892
|
+
rel.confidence,
|
|
893
|
+
rel.reasoning || null,
|
|
894
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Add a relationship between skills
|
|
900
|
+
*/
|
|
901
|
+
async addRelationship(sourceSkillId, targetSkillId, type, confidence, reasoning) {
|
|
902
|
+
this.ensureInitialized();
|
|
903
|
+
const db = this.getDb();
|
|
904
|
+
db.prepare(`
|
|
905
|
+
INSERT OR REPLACE INTO skill_relationships
|
|
906
|
+
(source_skill_id, target_skill_id, type, confidence, reasoning, created_at)
|
|
907
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
908
|
+
`).run(sourceSkillId, targetSkillId, type, confidence, reasoning || null, (/* @__PURE__ */ new Date()).toISOString());
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Get relationships for a skill
|
|
912
|
+
*/
|
|
913
|
+
async getRelationships(skillId) {
|
|
914
|
+
this.ensureInitialized();
|
|
915
|
+
const db = this.getDb();
|
|
916
|
+
const rows = db.prepare(`
|
|
917
|
+
SELECT target_skill_id, type, confidence, reasoning
|
|
918
|
+
FROM skill_relationships
|
|
919
|
+
WHERE source_skill_id = ?
|
|
920
|
+
`).all(skillId);
|
|
921
|
+
return rows.map((row) => ({
|
|
922
|
+
targetSkillId: row.target_skill_id,
|
|
923
|
+
type: row.type,
|
|
924
|
+
confidence: row.confidence,
|
|
925
|
+
reasoning: row.reasoning || void 0
|
|
926
|
+
}));
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Get skills that depend on a given skill
|
|
930
|
+
*/
|
|
931
|
+
async getDependentSkills(skillId) {
|
|
932
|
+
this.ensureInitialized();
|
|
933
|
+
const db = this.getDb();
|
|
934
|
+
const rows = db.prepare(`
|
|
935
|
+
SELECT s.* FROM skills s
|
|
936
|
+
JOIN skill_relationships r ON s.id = r.source_skill_id
|
|
937
|
+
WHERE r.target_skill_id = ? AND r.type = 'depends_on'
|
|
938
|
+
`).all(skillId);
|
|
939
|
+
return rows.map((row) => this.rowToSkill(row));
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Get related skills (any relationship type)
|
|
943
|
+
*/
|
|
944
|
+
async getRelatedSkills(skillId) {
|
|
945
|
+
this.ensureInitialized();
|
|
946
|
+
const db = this.getDb();
|
|
947
|
+
const rows = db.prepare(`
|
|
948
|
+
SELECT s.*, r.type, r.confidence, r.reasoning
|
|
949
|
+
FROM skills s
|
|
950
|
+
JOIN skill_relationships r ON s.id = r.target_skill_id
|
|
951
|
+
WHERE r.source_skill_id = ?
|
|
952
|
+
`).all(skillId);
|
|
953
|
+
return rows.map((row) => ({
|
|
954
|
+
skill: this.rowToSkill(row),
|
|
955
|
+
relationship: {
|
|
956
|
+
targetSkillId: row.id,
|
|
957
|
+
type: row.type,
|
|
958
|
+
confidence: row.confidence,
|
|
959
|
+
reasoning: row.reasoning || void 0
|
|
960
|
+
}
|
|
961
|
+
}));
|
|
962
|
+
}
|
|
963
|
+
// =========================================================================
|
|
964
|
+
// HELPER METHODS
|
|
965
|
+
// =========================================================================
|
|
966
|
+
rowToSkill(row) {
|
|
967
|
+
const externalSource = row.external_source ? JSON.parse(row.external_source) : void 0;
|
|
968
|
+
return {
|
|
969
|
+
id: row.id,
|
|
970
|
+
version: row.version,
|
|
971
|
+
name: row.name,
|
|
972
|
+
description: row.description,
|
|
973
|
+
problem: row.problem,
|
|
974
|
+
triggerConditions: JSON.parse(row.trigger_conditions),
|
|
975
|
+
solution: row.solution,
|
|
976
|
+
verification: row.verification,
|
|
977
|
+
examples: JSON.parse(row.examples),
|
|
978
|
+
notes: row.notes || void 0,
|
|
979
|
+
author: row.author,
|
|
980
|
+
tags: JSON.parse(row.tags),
|
|
981
|
+
createdAt: new Date(row.created_at),
|
|
982
|
+
updatedAt: new Date(row.updated_at),
|
|
983
|
+
status: row.status,
|
|
984
|
+
parentVersion: row.parent_version || void 0,
|
|
985
|
+
derivedFrom: row.derived_from ? JSON.parse(row.derived_from) : void 0,
|
|
986
|
+
metrics: JSON.parse(row.metrics),
|
|
987
|
+
source: row.source ? JSON.parse(row.source) : void 0,
|
|
988
|
+
taxonomy: row.taxonomy ? JSON.parse(row.taxonomy) : void 0,
|
|
989
|
+
externalSource: externalSource ? {
|
|
990
|
+
...externalSource,
|
|
991
|
+
scrapedAt: new Date(externalSource.scrapedAt)
|
|
992
|
+
} : void 0
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
serializeSkill(skill) {
|
|
996
|
+
return {
|
|
997
|
+
...skill,
|
|
998
|
+
createdAt: skill.createdAt.toISOString(),
|
|
999
|
+
updatedAt: skill.updatedAt.toISOString(),
|
|
1000
|
+
source: skill.source ? {
|
|
1001
|
+
...skill.source,
|
|
1002
|
+
importedAt: skill.source.importedAt.toISOString()
|
|
1003
|
+
} : void 0,
|
|
1004
|
+
metrics: {
|
|
1005
|
+
...skill.metrics,
|
|
1006
|
+
lastUsed: skill.metrics.lastUsed?.toISOString()
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
deserializeSkill(data) {
|
|
1011
|
+
return {
|
|
1012
|
+
...data,
|
|
1013
|
+
createdAt: new Date(data.createdAt),
|
|
1014
|
+
updatedAt: new Date(data.updatedAt),
|
|
1015
|
+
source: data.source ? {
|
|
1016
|
+
...data.source,
|
|
1017
|
+
importedAt: new Date(data.source.importedAt)
|
|
1018
|
+
} : void 0,
|
|
1019
|
+
metrics: {
|
|
1020
|
+
...data.metrics,
|
|
1021
|
+
lastUsed: data.metrics.lastUsed ? new Date(data.metrics.lastUsed) : void 0
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
hashSkill(skill) {
|
|
1026
|
+
const content = JSON.stringify({
|
|
1027
|
+
problem: skill.problem,
|
|
1028
|
+
solution: skill.solution,
|
|
1029
|
+
triggerConditions: skill.triggerConditions
|
|
1030
|
+
});
|
|
1031
|
+
let hash = 0;
|
|
1032
|
+
for (let i = 0; i < content.length; i++) {
|
|
1033
|
+
const char = content.charCodeAt(i);
|
|
1034
|
+
hash = (hash << 5) - hash + char;
|
|
1035
|
+
hash = hash & hash;
|
|
1036
|
+
}
|
|
1037
|
+
return Math.abs(hash).toString(16);
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
export {
|
|
1042
|
+
BaseStorageAdapter,
|
|
1043
|
+
MemoryStorageAdapter,
|
|
1044
|
+
SQLiteStorageAdapter
|
|
1045
|
+
};
|