skill-tree 0.2.0 → 0.3.0

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.
Files changed (130) hide show
  1. package/README.md +75 -0
  2. package/dist/bowser-CQI7RKRA.mjs +2821 -0
  3. package/dist/bowser-XBWM4HVL.mjs +2821 -0
  4. package/dist/chunk-2NL4MXNX.mjs +3156 -0
  5. package/dist/chunk-2STDJU5Y.mjs +1174 -0
  6. package/dist/chunk-3BCRI4CA.mjs +101 -0
  7. package/dist/chunk-3MV4GQ3N.mjs +19 -0
  8. package/dist/chunk-3SRB47JW.mjs +8344 -0
  9. package/dist/chunk-43YOKLZP.mjs +6081 -0
  10. package/dist/chunk-4AGZU52D.mjs +7918 -0
  11. package/dist/chunk-4HXHCEFH.mjs +9157 -0
  12. package/dist/chunk-4OC5QFIF.mjs +11267 -0
  13. package/dist/chunk-4QGSDVGH.mjs +580 -0
  14. package/dist/chunk-4TFMKAVC.mjs +1225 -0
  15. package/dist/chunk-55SMGVTP.mjs +7126 -0
  16. package/dist/chunk-5C4MEQMR.mjs +125 -0
  17. package/dist/chunk-6AZMD3Q3.mjs +1243 -0
  18. package/dist/chunk-6FX4IK4Z.mjs +5368 -0
  19. package/dist/chunk-6UPDN5QM.mjs +163 -0
  20. package/dist/chunk-7EGDKOHV.mjs +9439 -0
  21. package/dist/chunk-7IHYDFWW.mjs +163 -0
  22. package/dist/chunk-7LMOQW5H.mjs +4893 -0
  23. package/dist/chunk-7QIQJVNP.mjs +14206 -0
  24. package/dist/chunk-7VB4ZRZO.mjs +7127 -0
  25. package/dist/chunk-A3SILZYX.mjs +8360 -0
  26. package/dist/chunk-BPVRW25O.mjs +6089 -0
  27. package/dist/chunk-BZ2JKJ54.mjs +1057 -0
  28. package/dist/chunk-CI4476KM.mjs +6607 -0
  29. package/dist/chunk-DCRKELD5.mjs +46 -0
  30. package/dist/chunk-DDXYQ74I.mjs +13969 -0
  31. package/dist/chunk-DQOFJXBX.mjs +6595 -0
  32. package/dist/chunk-E2CVK23F.mjs +8751 -0
  33. package/dist/chunk-F3YEUQAP.mjs +654 -0
  34. package/dist/chunk-FKJJ4RJG.mjs +13874 -0
  35. package/dist/chunk-GBIK7WMX.mjs +9293 -0
  36. package/dist/chunk-GFK5SZRJ.mjs +580 -0
  37. package/dist/chunk-GPN6UQVR.mjs +9238 -0
  38. package/dist/chunk-II7DECZQ.mjs +9111 -0
  39. package/dist/chunk-INKVOZXK.mjs +15898 -0
  40. package/dist/chunk-J2JM7HAK.mjs +8787 -0
  41. package/dist/chunk-K6NRCSAZ.mjs +4355 -0
  42. package/dist/chunk-LACI6YL4.mjs +1379 -0
  43. package/dist/chunk-M4RPUUZT.mjs +3156 -0
  44. package/dist/chunk-MBF2MJS5.mjs +1230 -0
  45. package/dist/chunk-MBIGW6KU.mjs +644 -0
  46. package/dist/chunk-MR4TVINH.mjs +1234 -0
  47. package/dist/chunk-OOECXYLU.mjs +1379 -0
  48. package/dist/chunk-OYHYXKXO.mjs +7297 -0
  49. package/dist/chunk-P5GJJ4JB.mjs +9237 -0
  50. package/dist/chunk-PDPN7FW7.mjs +1045 -0
  51. package/dist/chunk-PJJJQXJL.mjs +1174 -0
  52. package/dist/chunk-PK3BAIFW.mjs +9294 -0
  53. package/dist/chunk-QNK3WYNA.mjs +8971 -0
  54. package/dist/chunk-QZ7TP4HQ.mjs +7 -0
  55. package/dist/chunk-RJYJGJO3.mjs +349 -0
  56. package/dist/chunk-T4PVQW5O.mjs +124 -0
  57. package/dist/chunk-TENXZJB3.mjs +349 -0
  58. package/dist/chunk-TEUB6DZR.mjs +6453 -0
  59. package/dist/chunk-TWPEHDW4.mjs +1067 -0
  60. package/dist/chunk-VHFTX33A.mjs +6724 -0
  61. package/dist/chunk-VNZSS2WY.mjs +1057 -0
  62. package/dist/chunk-WJP5XYS7.mjs +2102 -0
  63. package/dist/chunk-WX6N7KNO.mjs +1239 -0
  64. package/dist/chunk-Y54UK2J3.mjs +13071 -0
  65. package/dist/chunk-YDNGMDXC.mjs +9294 -0
  66. package/dist/chunk-YDVZIFIU.mjs +2102 -0
  67. package/dist/chunk-YJ6NZQLT.mjs +9237 -0
  68. package/dist/chunk-YWRKGXK4.mjs +9300 -0
  69. package/dist/chunk-ZI4AIAWQ.mjs +46 -0
  70. package/dist/chunk-ZQVS7MQK.mjs +6081 -0
  71. package/dist/chunk-ZYKRDDFO.mjs +163 -0
  72. package/dist/cli/index.js +1173 -324
  73. package/dist/cli/index.mjs +202 -9164
  74. package/dist/dist-es-27NPMXP7.mjs +22 -0
  75. package/dist/dist-es-2JG6ZWFR.mjs +69 -0
  76. package/dist/dist-es-2JGXQKUP.mjs +6077 -0
  77. package/dist/dist-es-5QD5QJS2.mjs +495 -0
  78. package/dist/dist-es-5ZD454R2.mjs +317 -0
  79. package/dist/dist-es-644EP2LP.mjs +317 -0
  80. package/dist/dist-es-DSNCHWLJ.mjs +170 -0
  81. package/dist/dist-es-DYHMPEKZ.mjs +170 -0
  82. package/dist/dist-es-FIVW7BUZ.mjs +317 -0
  83. package/dist/dist-es-GXJAFBE5.mjs +22 -0
  84. package/dist/dist-es-HRBPKDMR.mjs +935 -0
  85. package/dist/dist-es-L5AMJHSY.mjs +935 -0
  86. package/dist/dist-es-LHPJ63IO.mjs +4437 -0
  87. package/dist/dist-es-LT2AQAG7.mjs +4437 -0
  88. package/dist/dist-es-OK2J7EV3.mjs +378 -0
  89. package/dist/dist-es-ORE4PQTL.mjs +87 -0
  90. package/dist/dist-es-TLCYJJ25.mjs +495 -0
  91. package/dist/dist-es-V4LHTSRG.mjs +69 -0
  92. package/dist/dist-es-XFAHNA2L.mjs +69 -0
  93. package/dist/dist-es-XHTU3ZU2.mjs +935 -0
  94. package/dist/dist-es-XPNJAJI7.mjs +4437 -0
  95. package/dist/dist-es-Y2MPJ6IO.mjs +378 -0
  96. package/dist/dist-es-Y4JPNLF3.mjs +6077 -0
  97. package/dist/dist-es-ZGPJUGVW.mjs +87 -0
  98. package/dist/dist-es-ZYHLY2E6.mjs +487 -0
  99. package/dist/event-streams-6MFHPNRF.mjs +42 -0
  100. package/dist/event-streams-KIAAAC7Z.mjs +42 -0
  101. package/dist/index.d.mts +1189 -13
  102. package/dist/index.d.ts +1189 -13
  103. package/dist/index.js +38737 -601
  104. package/dist/index.mjs +131 -9693
  105. package/dist/lib-B245IUXF.mjs +778 -0
  106. package/dist/loadSso-CAWKILED.mjs +579 -0
  107. package/dist/loadSso-NPRY7QRT.mjs +579 -0
  108. package/dist/loadSso-OYKG6ZRE.mjs +579 -0
  109. package/dist/signin-KUENA7ZD.mjs +743 -0
  110. package/dist/signin-LMFNL434.mjs +665 -0
  111. package/dist/signin-LUKXFXSI.mjs +743 -0
  112. package/dist/sqlite-5LHEQTBD.mjs +7 -0
  113. package/dist/sqlite-BZK5GF76.mjs +7 -0
  114. package/dist/sqlite-MG45OOTV.mjs +6 -0
  115. package/dist/sqlite-OLU72GHB.mjs +6 -0
  116. package/dist/sqlite-RR2SJ3SR.mjs +7 -0
  117. package/dist/sqlite-V6GFGHTD.mjs +7 -0
  118. package/dist/sqlite-XJRPMNAJ.mjs +6 -0
  119. package/dist/sqlite-ZKQKQKPT.mjs +7 -0
  120. package/dist/sso-oidc-3VGFPMFD.mjs +832 -0
  121. package/dist/sso-oidc-NNH6SQIH.mjs +832 -0
  122. package/dist/sso-oidc-STZH2XK2.mjs +832 -0
  123. package/dist/sts-EF755UBF.mjs +6290 -0
  124. package/dist/sts-QGXULWRT.mjs +6290 -0
  125. package/dist/sts-ZIS4G6FQ.mjs +6290 -0
  126. package/dist/sync-4DCV43GA.mjs +15 -0
  127. package/dist/sync-BSWMMDA6.mjs +14 -0
  128. package/dist/sync-WHIIDHML.mjs +14 -0
  129. package/dist/sync-XRWFQYBY.mjs +15 -0
  130. package/package.json +9 -2
@@ -0,0 +1,1234 @@
1
+ // src/storage/sqlite.ts
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+
5
+ // src/storage/base.ts
6
+ var _BaseStorageAdapter = class _BaseStorageAdapter {
7
+ constructor() {
8
+ this.initialized = false;
9
+ }
10
+ /**
11
+ * Ensure storage is initialized before operations
12
+ */
13
+ ensureInitialized() {
14
+ if (!this.initialized) {
15
+ throw new Error("Storage not initialized. Call initialize() first.");
16
+ }
17
+ }
18
+ /**
19
+ * Apply filters to a list of skills
20
+ */
21
+ applyFilter(skills, filter) {
22
+ if (!filter) return skills;
23
+ return skills.filter((skill) => {
24
+ if (filter.status && filter.status.length > 0) {
25
+ if (!filter.status.includes(skill.status)) return false;
26
+ }
27
+ if (filter.tags && filter.tags.length > 0) {
28
+ const hasTag = filter.tags.some((tag) => skill.tags.includes(tag));
29
+ if (!hasTag) return false;
30
+ }
31
+ if (filter.author && skill.author !== filter.author) {
32
+ return false;
33
+ }
34
+ if (filter.createdAfter && skill.createdAt < filter.createdAfter) {
35
+ return false;
36
+ }
37
+ if (filter.createdBefore && skill.createdAt > filter.createdBefore) {
38
+ return false;
39
+ }
40
+ if (!this.applyNamespaceFilter(skill, filter)) {
41
+ return false;
42
+ }
43
+ return true;
44
+ });
45
+ }
46
+ /**
47
+ * Apply namespace-related filters to a skill
48
+ */
49
+ applyNamespaceFilter(skill, filter) {
50
+ const namespace = skill.namespace;
51
+ if (filter.scope) {
52
+ const scopes = Array.isArray(filter.scope) ? filter.scope : [filter.scope];
53
+ const skillScope = namespace?.scope || "personal";
54
+ if (!scopes.includes(skillScope)) return false;
55
+ }
56
+ if (filter.owner) {
57
+ const skillOwner = namespace?.owner;
58
+ if (skillOwner !== filter.owner) return false;
59
+ }
60
+ if (filter.team) {
61
+ const skillTeam = namespace?.team;
62
+ if (skillTeam !== filter.team) return false;
63
+ }
64
+ if (filter.visibility) {
65
+ const visibilities = Array.isArray(filter.visibility) ? filter.visibility : [filter.visibility];
66
+ const skillVisibility = namespace?.visibility || "private";
67
+ if (!visibilities.includes(skillVisibility)) return false;
68
+ }
69
+ if (filter.accessibleBy) {
70
+ if (!this.canAgentAccessSkill(skill, filter.accessibleBy, filter.accessibleByTeam)) {
71
+ return false;
72
+ }
73
+ }
74
+ return true;
75
+ }
76
+ /**
77
+ * Check if an agent can access a skill based on namespace rules
78
+ */
79
+ canAgentAccessSkill(skill, agentId, agentTeam) {
80
+ const namespace = skill.namespace;
81
+ if (!namespace) return true;
82
+ if (namespace.owner === agentId) return true;
83
+ switch (namespace.visibility) {
84
+ case "public":
85
+ return true;
86
+ case "team-only":
87
+ return agentTeam !== void 0 && agentTeam === namespace.team;
88
+ case "private":
89
+ return namespace.owner === agentId;
90
+ default:
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * Simple text search across skill fields
96
+ */
97
+ textSearch(skills, query) {
98
+ const queryTerms = this.tokenize(query);
99
+ if (queryTerms.length === 0) return [];
100
+ const N = skills.length;
101
+ const k1 = 1.5;
102
+ const b = 0.75;
103
+ const docs = skills.map((skill) => ({
104
+ skill,
105
+ terms: this.tokenize(
106
+ [skill.name, skill.description, ...skill.tags].join(" ")
107
+ )
108
+ }));
109
+ const avgDl = docs.reduce((sum, d) => sum + d.terms.length, 0) / (N || 1);
110
+ const df = /* @__PURE__ */ new Map();
111
+ for (const doc of docs) {
112
+ const unique = new Set(doc.terms);
113
+ for (const t of unique) {
114
+ df.set(t, (df.get(t) ?? 0) + 1);
115
+ }
116
+ }
117
+ const scored = docs.map((doc) => {
118
+ let score = 0;
119
+ const tf = /* @__PURE__ */ new Map();
120
+ for (const t of doc.terms) {
121
+ tf.set(t, (tf.get(t) ?? 0) + 1);
122
+ }
123
+ for (const qt of queryTerms) {
124
+ const termDf = df.get(qt) ?? 0;
125
+ if (termDf === 0) continue;
126
+ const idf = Math.log((N - termDf + 0.5) / (termDf + 0.5) + 1);
127
+ const termTf = tf.get(qt) ?? 0;
128
+ const tfNorm = termTf * (k1 + 1) / (termTf + k1 * (1 - b + b * doc.terms.length / avgDl));
129
+ score += idf * tfNorm;
130
+ }
131
+ return { skill: doc.skill, score };
132
+ });
133
+ return scored.filter((s) => s.score > 0).sort((a, b2) => b2.score - a.score).map((s) => s.skill);
134
+ }
135
+ tokenize(text) {
136
+ return text.toLowerCase().split(/\W+/).filter((t) => t.length > 1 && !_BaseStorageAdapter.STOP_WORDS.has(t)).map((t) => _BaseStorageAdapter.stem(t));
137
+ }
138
+ static stem(word) {
139
+ if (word.length <= 3) return word;
140
+ let w = word;
141
+ w = w.replace(/ies$/, "y");
142
+ w = w.replace(/(ation|tion)$/, "t");
143
+ w = w.replace(/sion$/, "s");
144
+ w = w.replace(/(ing|ment|ness|able|ible|ous|ive|ful|less|ize|ise|ance|ence)$/, "");
145
+ w = w.replace(/([^aeiou])ed$/, "$1");
146
+ w = w.replace(/es$/, "");
147
+ w = w.replace(/([^s])s$/, "$1");
148
+ w = w.replace(/(.)\1$/, "$1");
149
+ if (w.length <= 1) return word;
150
+ return w;
151
+ }
152
+ };
153
+ _BaseStorageAdapter.STOP_WORDS = /* @__PURE__ */ new Set([
154
+ "the",
155
+ "a",
156
+ "an",
157
+ "is",
158
+ "are",
159
+ "was",
160
+ "were",
161
+ "be",
162
+ "been",
163
+ "being",
164
+ "have",
165
+ "has",
166
+ "had",
167
+ "do",
168
+ "does",
169
+ "did",
170
+ "will",
171
+ "would",
172
+ "could",
173
+ "should",
174
+ "may",
175
+ "might",
176
+ "shall",
177
+ "can",
178
+ "to",
179
+ "of",
180
+ "in",
181
+ "for",
182
+ "on",
183
+ "with",
184
+ "at",
185
+ "by",
186
+ "from",
187
+ "as",
188
+ "into",
189
+ "through",
190
+ "and",
191
+ "but",
192
+ "or",
193
+ "not",
194
+ "so",
195
+ "yet",
196
+ "if",
197
+ "when",
198
+ "that",
199
+ "this",
200
+ "it",
201
+ "its",
202
+ "also",
203
+ "which",
204
+ "what",
205
+ "how",
206
+ "why",
207
+ "where"
208
+ ]);
209
+ var BaseStorageAdapter = _BaseStorageAdapter;
210
+ var MemoryStorageAdapter = class extends BaseStorageAdapter {
211
+ constructor() {
212
+ super(...arguments);
213
+ this.skills = /* @__PURE__ */ new Map();
214
+ // skillId -> version -> skill
215
+ this.lineages = /* @__PURE__ */ new Map();
216
+ }
217
+ async initialize() {
218
+ this.initialized = true;
219
+ }
220
+ async saveSkill(skill) {
221
+ this.ensureInitialized();
222
+ if (!this.skills.has(skill.id)) {
223
+ this.skills.set(skill.id, /* @__PURE__ */ new Map());
224
+ }
225
+ const versionMap = this.skills.get(skill.id);
226
+ versionMap.set(skill.version, { ...skill });
227
+ this.updateLineage(skill);
228
+ }
229
+ async getSkill(id, version) {
230
+ this.ensureInitialized();
231
+ const versionMap = this.skills.get(id);
232
+ if (!versionMap) return null;
233
+ if (version) {
234
+ return versionMap.get(version) || null;
235
+ }
236
+ const versions = Array.from(versionMap.keys()).sort(this.compareVersions);
237
+ const latestVersion = versions[versions.length - 1];
238
+ return latestVersion ? versionMap.get(latestVersion) || null : null;
239
+ }
240
+ async listSkills(filter) {
241
+ this.ensureInitialized();
242
+ const skills = [];
243
+ for (const versionMap of this.skills.values()) {
244
+ const versions = Array.from(versionMap.keys()).sort(this.compareVersions);
245
+ const latestVersion = versions[versions.length - 1];
246
+ if (latestVersion) {
247
+ const skill = versionMap.get(latestVersion);
248
+ if (skill) skills.push(skill);
249
+ }
250
+ }
251
+ return this.applyFilter(skills, filter);
252
+ }
253
+ async deleteSkill(id, version) {
254
+ this.ensureInitialized();
255
+ if (version) {
256
+ const versionMap = this.skills.get(id);
257
+ if (!versionMap) return false;
258
+ return versionMap.delete(version);
259
+ }
260
+ const existed = this.skills.has(id);
261
+ this.skills.delete(id);
262
+ this.lineages.delete(id);
263
+ return existed;
264
+ }
265
+ async getVersionHistory(skillId) {
266
+ this.ensureInitialized();
267
+ const versionMap = this.skills.get(skillId);
268
+ if (!versionMap) return [];
269
+ const versions = [];
270
+ for (const [version, skill] of versionMap) {
271
+ versions.push({
272
+ skillId,
273
+ version,
274
+ skill,
275
+ changelog: "",
276
+ // Not tracked in memory
277
+ createdAt: skill.createdAt,
278
+ contentHash: this.hashSkill(skill)
279
+ });
280
+ }
281
+ return versions.sort((a, b) => this.compareVersions(a.version, b.version));
282
+ }
283
+ async getLineage(skillId) {
284
+ this.ensureInitialized();
285
+ return this.lineages.get(skillId) || null;
286
+ }
287
+ async searchSkills(query) {
288
+ this.ensureInitialized();
289
+ const allSkills = await this.listSkills();
290
+ return this.textSearch(allSkills, query);
291
+ }
292
+ updateLineage(skill) {
293
+ if (!this.lineages.has(skill.id)) {
294
+ this.lineages.set(skill.id, {
295
+ rootId: skill.id,
296
+ versions: [],
297
+ forks: []
298
+ });
299
+ }
300
+ const lineage = this.lineages.get(skill.id);
301
+ const existingIndex = lineage.versions.findIndex((v) => v.version === skill.version);
302
+ const versionEntry = {
303
+ skillId: skill.id,
304
+ version: skill.version,
305
+ skill,
306
+ changelog: "",
307
+ createdAt: skill.createdAt,
308
+ contentHash: this.hashSkill(skill)
309
+ };
310
+ if (existingIndex >= 0) {
311
+ lineage.versions[existingIndex] = versionEntry;
312
+ } else {
313
+ lineage.versions.push(versionEntry);
314
+ lineage.versions.sort((a, b) => this.compareVersions(a.version, b.version));
315
+ }
316
+ }
317
+ compareVersions(a, b) {
318
+ const partsA = a.split(".").map(Number);
319
+ const partsB = b.split(".").map(Number);
320
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
321
+ const numA = partsA[i] || 0;
322
+ const numB = partsB[i] || 0;
323
+ if (numA !== numB) return numA - numB;
324
+ }
325
+ return 0;
326
+ }
327
+ hashSkill(skill) {
328
+ const content = JSON.stringify({
329
+ instructions: skill.instructions
330
+ });
331
+ let hash = 0;
332
+ for (let i = 0; i < content.length; i++) {
333
+ const char = content.charCodeAt(i);
334
+ hash = (hash << 5) - hash + char;
335
+ hash = hash & hash;
336
+ }
337
+ return Math.abs(hash).toString(16);
338
+ }
339
+ /**
340
+ * Clear all stored skills (for testing)
341
+ */
342
+ clear() {
343
+ this.skills.clear();
344
+ this.lineages.clear();
345
+ }
346
+ // ==========================================================================
347
+ // Fork Tracking
348
+ // ==========================================================================
349
+ async recordFork(sourceSkillId, fork) {
350
+ this.ensureInitialized();
351
+ const lineage = this.lineages.get(sourceSkillId);
352
+ if (lineage) {
353
+ const exists = lineage.forks.some(
354
+ (f) => f.forkedSkillId === fork.forkedSkillId
355
+ );
356
+ if (!exists) {
357
+ lineage.forks.push(fork);
358
+ }
359
+ }
360
+ }
361
+ };
362
+
363
+ // src/storage/sqlite.ts
364
+ var SCHEMA_VERSION = 3;
365
+ var SQLiteStorageAdapter = class extends BaseStorageAdapter {
366
+ constructor(config) {
367
+ super();
368
+ this.db = null;
369
+ this.config = {
370
+ walMode: true,
371
+ enableFTS: true,
372
+ ...config
373
+ };
374
+ }
375
+ async initialize() {
376
+ const dir = path.dirname(this.config.dbPath);
377
+ if (dir && !fs.existsSync(dir)) {
378
+ fs.mkdirSync(dir, { recursive: true });
379
+ }
380
+ this.db = await this.openDatabase(this.config.dbPath);
381
+ if (this.config.walMode) {
382
+ this.db.pragma("journal_mode = WAL");
383
+ }
384
+ this.db.pragma("foreign_keys = ON");
385
+ this.createSchema();
386
+ this.runMigrations();
387
+ this.initialized = true;
388
+ }
389
+ /**
390
+ * Open the underlying SQLite database. Defaults to better-sqlite3, loaded
391
+ * lazily so the native addon is only required when this adapter is actually
392
+ * used. Subclasses (e.g. {@link NodeSqliteStorageAdapter}) override this to
393
+ * supply a different, cross-runtime driver.
394
+ */
395
+ async openDatabase(dbPath) {
396
+ const { default: Database } = await import("./lib-B245IUXF.mjs");
397
+ return new Database(dbPath);
398
+ }
399
+ createSchema() {
400
+ const db = this.getDb();
401
+ db.exec(`
402
+ CREATE TABLE IF NOT EXISTS schema_version (
403
+ version INTEGER PRIMARY KEY
404
+ )
405
+ `);
406
+ db.exec(`
407
+ CREATE TABLE IF NOT EXISTS skills (
408
+ id TEXT PRIMARY KEY,
409
+ version TEXT NOT NULL,
410
+ name TEXT NOT NULL,
411
+ description TEXT NOT NULL,
412
+ instructions TEXT NOT NULL DEFAULT '',
413
+ related TEXT,
414
+ author TEXT NOT NULL,
415
+ tags TEXT NOT NULL,
416
+ created_at TEXT NOT NULL,
417
+ updated_at TEXT NOT NULL,
418
+ status TEXT NOT NULL,
419
+ parent_version TEXT,
420
+ derived_from TEXT,
421
+ source TEXT,
422
+ taxonomy TEXT,
423
+ external_source TEXT
424
+ )
425
+ `);
426
+ try {
427
+ db.exec("ALTER TABLE skills DROP COLUMN metrics");
428
+ } catch {
429
+ const cols = db.prepare("PRAGMA table_info(skills)").all();
430
+ if (cols.some((c) => c.name === "metrics")) {
431
+ this.rebuildSkillsTableWithoutMetrics(db, cols);
432
+ }
433
+ }
434
+ db.exec(`
435
+ CREATE TABLE IF NOT EXISTS skill_versions (
436
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
437
+ skill_id TEXT NOT NULL,
438
+ version TEXT NOT NULL,
439
+ skill_data TEXT NOT NULL,
440
+ changelog TEXT NOT NULL,
441
+ created_at TEXT NOT NULL,
442
+ content_hash TEXT NOT NULL,
443
+ UNIQUE(skill_id, version)
444
+ )
445
+ `);
446
+ db.exec(`
447
+ CREATE TABLE IF NOT EXISTS skill_lineage (
448
+ skill_id TEXT PRIMARY KEY,
449
+ root_id TEXT NOT NULL
450
+ )
451
+ `);
452
+ db.exec(`
453
+ CREATE TABLE IF NOT EXISTS skill_forks (
454
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
455
+ root_skill_id TEXT NOT NULL,
456
+ forked_skill_id TEXT NOT NULL,
457
+ from_version TEXT NOT NULL,
458
+ reason TEXT NOT NULL,
459
+ forked_at TEXT NOT NULL,
460
+ FOREIGN KEY (root_skill_id) REFERENCES skill_lineage(skill_id)
461
+ )
462
+ `);
463
+ db.exec(`
464
+ CREATE INDEX IF NOT EXISTS idx_skills_status ON skills(status);
465
+ CREATE INDEX IF NOT EXISTS idx_skills_author ON skills(author);
466
+ CREATE INDEX IF NOT EXISTS idx_skills_updated ON skills(updated_at);
467
+ CREATE INDEX IF NOT EXISTS idx_versions_skill ON skill_versions(skill_id);
468
+ `);
469
+ if (this.config.enableFTS) {
470
+ db.exec(`
471
+ CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
472
+ skill_id,
473
+ name,
474
+ description,
475
+ instructions,
476
+ tags
477
+ )
478
+ `);
479
+ }
480
+ db.exec(`
481
+ CREATE TABLE IF NOT EXISTS taxonomy_nodes (
482
+ id TEXT PRIMARY KEY,
483
+ name TEXT NOT NULL,
484
+ description TEXT,
485
+ parent_id TEXT REFERENCES taxonomy_nodes(id),
486
+ path TEXT NOT NULL,
487
+ skill_count INTEGER DEFAULT 0,
488
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
489
+ )
490
+ `);
491
+ db.exec(`
492
+ CREATE TABLE IF NOT EXISTS skill_taxonomy_placements (
493
+ skill_id TEXT NOT NULL,
494
+ node_id TEXT NOT NULL,
495
+ is_primary INTEGER DEFAULT 0,
496
+ confidence REAL,
497
+ reasoning TEXT,
498
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
499
+ PRIMARY KEY (skill_id, node_id)
500
+ )
501
+ `);
502
+ db.exec(`
503
+ CREATE TABLE IF NOT EXISTS skill_relationships (
504
+ source_skill_id TEXT NOT NULL,
505
+ target_skill_id TEXT NOT NULL,
506
+ type TEXT NOT NULL,
507
+ confidence REAL NOT NULL,
508
+ reasoning TEXT,
509
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
510
+ PRIMARY KEY (source_skill_id, target_skill_id, type)
511
+ )
512
+ `);
513
+ db.exec(`
514
+ CREATE TABLE IF NOT EXISTS skill_sources (
515
+ id TEXT PRIMARY KEY,
516
+ type TEXT NOT NULL,
517
+ url TEXT,
518
+ last_scraped TEXT,
519
+ etag TEXT,
520
+ skill_count INTEGER DEFAULT 0
521
+ )
522
+ `);
523
+ db.exec(`
524
+ CREATE INDEX IF NOT EXISTS idx_taxonomy_parent ON taxonomy_nodes(parent_id);
525
+ CREATE INDEX IF NOT EXISTS idx_taxonomy_path ON taxonomy_nodes(path);
526
+ CREATE INDEX IF NOT EXISTS idx_placements_skill ON skill_taxonomy_placements(skill_id);
527
+ CREATE INDEX IF NOT EXISTS idx_placements_node ON skill_taxonomy_placements(node_id);
528
+ CREATE INDEX IF NOT EXISTS idx_relationships_source ON skill_relationships(source_skill_id);
529
+ CREATE INDEX IF NOT EXISTS idx_relationships_target ON skill_relationships(target_skill_id);
530
+ `);
531
+ const version = db.prepare("SELECT version FROM schema_version").get();
532
+ if (!version) {
533
+ db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
534
+ }
535
+ }
536
+ runMigrations() {
537
+ const db = this.getDb();
538
+ const row = db.prepare("SELECT version FROM schema_version").get();
539
+ const currentVersion = row?.version || 0;
540
+ if (currentVersion < SCHEMA_VERSION) {
541
+ if (currentVersion < 2) {
542
+ try {
543
+ db.exec("ALTER TABLE skills ADD COLUMN taxonomy TEXT");
544
+ } catch {
545
+ }
546
+ try {
547
+ db.exec("ALTER TABLE skills ADD COLUMN external_source TEXT");
548
+ } catch {
549
+ }
550
+ }
551
+ if (currentVersion < 3) {
552
+ try {
553
+ db.exec("ALTER TABLE skills ADD COLUMN instructions TEXT NOT NULL DEFAULT ''");
554
+ } catch {
555
+ }
556
+ try {
557
+ db.exec("ALTER TABLE skills ADD COLUMN related TEXT");
558
+ } catch {
559
+ }
560
+ try {
561
+ db.exec(`
562
+ UPDATE skills SET instructions =
563
+ COALESCE(problem, '') || CHAR(10) || CHAR(10) ||
564
+ COALESCE(solution, '') || CHAR(10) || CHAR(10) ||
565
+ COALESCE(verification, '')
566
+ WHERE instructions = ''
567
+ `);
568
+ } catch {
569
+ }
570
+ if (this.config.enableFTS) {
571
+ try {
572
+ db.exec("DROP TABLE IF EXISTS skills_fts");
573
+ db.exec(`
574
+ CREATE VIRTUAL TABLE skills_fts USING fts5(
575
+ skill_id, name, description, instructions, tags
576
+ )
577
+ `);
578
+ const rows = db.prepare("SELECT id, name, description, instructions, tags FROM skills").all();
579
+ const insertFts = db.prepare("INSERT INTO skills_fts (skill_id, name, description, instructions, tags) VALUES (?, ?, ?, ?, ?)");
580
+ for (const row2 of rows) {
581
+ const tags = (() => {
582
+ try {
583
+ return JSON.parse(row2.tags).join(" ");
584
+ } catch {
585
+ return row2.tags;
586
+ }
587
+ })();
588
+ insertFts.run(row2.id, row2.name, row2.description, row2.instructions, tags);
589
+ }
590
+ } catch {
591
+ }
592
+ }
593
+ }
594
+ db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
595
+ }
596
+ }
597
+ /**
598
+ * Rebuild the `skills` table without the legacy `metrics` column.
599
+ *
600
+ * Used as the fallback path on SQLite < 3.35 where ALTER TABLE DROP
601
+ * COLUMN isn't supported. Uses the standard "table dance" pattern:
602
+ * CREATE NEW → INSERT FROM OLD → DROP OLD → RENAME, inside a single
603
+ * transaction so a partial state can't outlive a crash.
604
+ *
605
+ * The `existingCols` arg is the full PRAGMA table_info output for the
606
+ * legacy table — we use it to figure out which columns to copy. Some
607
+ * legacy installs may have a subset of the current schema's columns
608
+ * (e.g., taxonomy/external_source were added in v2, related in v3),
609
+ * so we only copy columns that exist on both sides.
610
+ */
611
+ rebuildSkillsTableWithoutMetrics(db, existingCols) {
612
+ const NEW_SCHEMA_COLUMNS = [
613
+ "id",
614
+ "version",
615
+ "name",
616
+ "description",
617
+ "instructions",
618
+ "related",
619
+ "author",
620
+ "tags",
621
+ "created_at",
622
+ "updated_at",
623
+ "status",
624
+ "parent_version",
625
+ "derived_from",
626
+ "source",
627
+ "taxonomy",
628
+ "external_source"
629
+ ];
630
+ const sourceNames = new Set(existingCols.map((c) => c.name));
631
+ const copyable = NEW_SCHEMA_COLUMNS.filter((c) => sourceNames.has(c));
632
+ const copyList = copyable.join(", ");
633
+ db.exec("BEGIN");
634
+ try {
635
+ db.exec(`
636
+ CREATE TABLE skills_new (
637
+ id TEXT PRIMARY KEY,
638
+ version TEXT NOT NULL,
639
+ name TEXT NOT NULL,
640
+ description TEXT,
641
+ instructions TEXT NOT NULL DEFAULT '',
642
+ related TEXT,
643
+ author TEXT NOT NULL,
644
+ tags TEXT NOT NULL,
645
+ created_at TEXT NOT NULL,
646
+ updated_at TEXT NOT NULL,
647
+ status TEXT NOT NULL,
648
+ parent_version TEXT,
649
+ derived_from TEXT,
650
+ source TEXT,
651
+ taxonomy TEXT,
652
+ external_source TEXT
653
+ )
654
+ `);
655
+ db.exec(
656
+ `INSERT INTO skills_new (${copyList}) SELECT ${copyList} FROM skills`
657
+ );
658
+ db.exec("DROP TABLE skills");
659
+ db.exec("ALTER TABLE skills_new RENAME TO skills");
660
+ db.exec("COMMIT");
661
+ } catch (err) {
662
+ db.exec("ROLLBACK");
663
+ throw err;
664
+ }
665
+ }
666
+ getDb() {
667
+ if (!this.db) {
668
+ throw new Error("Database not initialized. Call initialize() first.");
669
+ }
670
+ return this.db;
671
+ }
672
+ async saveSkill(skill) {
673
+ this.ensureInitialized();
674
+ const db = this.getDb();
675
+ const stmt = db.prepare(`
676
+ INSERT OR REPLACE INTO skills (
677
+ id, version, name, description, instructions, related, author, tags,
678
+ created_at, updated_at, status, parent_version, derived_from,
679
+ source, taxonomy, external_source
680
+ ) VALUES (
681
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
682
+ )
683
+ `);
684
+ stmt.run(
685
+ skill.id,
686
+ skill.version,
687
+ skill.name,
688
+ skill.description,
689
+ skill.instructions,
690
+ skill.related ? JSON.stringify(skill.related) : null,
691
+ skill.author,
692
+ JSON.stringify(skill.tags),
693
+ skill.createdAt.toISOString(),
694
+ skill.updatedAt.toISOString(),
695
+ skill.status,
696
+ skill.parentVersion || null,
697
+ skill.derivedFrom ? JSON.stringify(skill.derivedFrom) : null,
698
+ skill.source ? JSON.stringify(skill.source) : null,
699
+ skill.taxonomy ? JSON.stringify(skill.taxonomy) : null,
700
+ skill.externalSource ? JSON.stringify({
701
+ ...skill.externalSource,
702
+ scrapedAt: skill.externalSource.scrapedAt.toISOString()
703
+ }) : null
704
+ );
705
+ if (skill.relationships && skill.relationships.length > 0) {
706
+ await this.saveSkillRelationships(skill.id, skill.relationships);
707
+ }
708
+ if (this.config.enableFTS) {
709
+ this.updateFTSIndex(skill);
710
+ }
711
+ await this.saveVersionSnapshot(skill);
712
+ this.updateLineage(skill);
713
+ }
714
+ updateFTSIndex(skill) {
715
+ const db = this.getDb();
716
+ db.prepare("DELETE FROM skills_fts WHERE skill_id = ?").run(skill.id);
717
+ const tags = skill.tags.join(" ");
718
+ db.prepare(`
719
+ INSERT INTO skills_fts (skill_id, name, description, instructions, tags)
720
+ VALUES (?, ?, ?, ?, ?)
721
+ `).run(skill.id, skill.name, skill.description, skill.instructions, tags);
722
+ }
723
+ async saveVersionSnapshot(skill) {
724
+ const db = this.getDb();
725
+ const existing = db.prepare(
726
+ "SELECT id FROM skill_versions WHERE skill_id = ? AND version = ?"
727
+ ).get(skill.id, skill.version);
728
+ if (existing) {
729
+ db.prepare(`
730
+ UPDATE skill_versions
731
+ SET skill_data = ?, content_hash = ?
732
+ WHERE skill_id = ? AND version = ?
733
+ `).run(
734
+ JSON.stringify(this.serializeSkill(skill)),
735
+ this.hashSkill(skill),
736
+ skill.id,
737
+ skill.version
738
+ );
739
+ } else {
740
+ db.prepare(`
741
+ INSERT INTO skill_versions (skill_id, version, skill_data, changelog, created_at, content_hash)
742
+ VALUES (?, ?, ?, ?, ?, ?)
743
+ `).run(
744
+ skill.id,
745
+ skill.version,
746
+ JSON.stringify(this.serializeSkill(skill)),
747
+ "",
748
+ // Changelog can be added via separate method
749
+ skill.createdAt.toISOString(),
750
+ this.hashSkill(skill)
751
+ );
752
+ }
753
+ }
754
+ updateLineage(skill) {
755
+ const db = this.getDb();
756
+ const existing = db.prepare(
757
+ "SELECT skill_id FROM skill_lineage WHERE skill_id = ?"
758
+ ).get(skill.id);
759
+ if (!existing) {
760
+ db.prepare(
761
+ "INSERT INTO skill_lineage (skill_id, root_id) VALUES (?, ?)"
762
+ ).run(skill.id, skill.id);
763
+ }
764
+ }
765
+ async getSkill(id, version) {
766
+ this.ensureInitialized();
767
+ const db = this.getDb();
768
+ if (version) {
769
+ const row2 = db.prepare(
770
+ "SELECT skill_data FROM skill_versions WHERE skill_id = ? AND version = ?"
771
+ ).get(id, version);
772
+ if (!row2) return null;
773
+ return this.deserializeSkill(JSON.parse(row2.skill_data));
774
+ }
775
+ const row = db.prepare("SELECT * FROM skills WHERE id = ?").get(id);
776
+ if (!row) return null;
777
+ return this.rowToSkill(row);
778
+ }
779
+ async listSkills(filter) {
780
+ this.ensureInitialized();
781
+ const db = this.getDb();
782
+ let sql = "SELECT * FROM skills WHERE 1=1";
783
+ const params = [];
784
+ if (filter?.status && filter.status.length > 0) {
785
+ sql += ` AND status IN (${filter.status.map(() => "?").join(",")})`;
786
+ params.push(...filter.status);
787
+ }
788
+ if (filter?.author) {
789
+ sql += " AND author = ?";
790
+ params.push(filter.author);
791
+ }
792
+ if (filter?.createdAfter) {
793
+ sql += " AND created_at >= ?";
794
+ params.push(filter.createdAfter.toISOString());
795
+ }
796
+ if (filter?.createdBefore) {
797
+ sql += " AND created_at <= ?";
798
+ params.push(filter.createdBefore.toISOString());
799
+ }
800
+ sql += " ORDER BY updated_at DESC";
801
+ const rows = db.prepare(sql).all(...params);
802
+ let skills = rows.map((row) => this.rowToSkill(row));
803
+ if (filter?.tags && filter.tags.length > 0) {
804
+ skills = skills.filter(
805
+ (skill) => filter.tags.some((tag) => skill.tags.includes(tag))
806
+ );
807
+ }
808
+ return skills;
809
+ }
810
+ async deleteSkill(id, version) {
811
+ this.ensureInitialized();
812
+ const db = this.getDb();
813
+ if (version) {
814
+ const result = db.prepare(
815
+ "DELETE FROM skill_versions WHERE skill_id = ? AND version = ?"
816
+ ).run(id, version);
817
+ return result.changes > 0;
818
+ }
819
+ const transaction = db.transaction(() => {
820
+ db.prepare("DELETE FROM skill_versions WHERE skill_id = ?").run(id);
821
+ db.prepare("DELETE FROM skill_forks WHERE root_skill_id = ? OR forked_skill_id = ?").run(id, id);
822
+ db.prepare("DELETE FROM skill_lineage WHERE skill_id = ?").run(id);
823
+ if (this.config.enableFTS) {
824
+ db.prepare("DELETE FROM skills_fts WHERE skill_id = ?").run(id);
825
+ }
826
+ db.prepare("DELETE FROM skill_taxonomy_placements WHERE skill_id = ?").run(id);
827
+ db.prepare("DELETE FROM skill_relationships WHERE source_skill_id = ? OR target_skill_id = ?").run(id, id);
828
+ const result = db.prepare("DELETE FROM skills WHERE id = ?").run(id);
829
+ return result.changes > 0;
830
+ });
831
+ return transaction();
832
+ }
833
+ async getVersionHistory(skillId) {
834
+ this.ensureInitialized();
835
+ const db = this.getDb();
836
+ const rows = db.prepare(`
837
+ SELECT skill_id, version, skill_data, changelog, created_at, content_hash
838
+ FROM skill_versions
839
+ WHERE skill_id = ?
840
+ ORDER BY created_at ASC
841
+ `).all(skillId);
842
+ return rows.map((row) => ({
843
+ skillId: row.skill_id,
844
+ version: row.version,
845
+ skill: this.deserializeSkill(JSON.parse(row.skill_data)),
846
+ changelog: row.changelog,
847
+ createdAt: new Date(row.created_at),
848
+ contentHash: row.content_hash
849
+ }));
850
+ }
851
+ async getLineage(skillId) {
852
+ this.ensureInitialized();
853
+ const db = this.getDb();
854
+ const lineageRow = db.prepare(
855
+ "SELECT * FROM skill_lineage WHERE skill_id = ?"
856
+ ).get(skillId);
857
+ if (!lineageRow) return null;
858
+ const versions = await this.getVersionHistory(skillId);
859
+ const forkRows = db.prepare(`
860
+ SELECT forked_skill_id, from_version, reason, forked_at
861
+ FROM skill_forks
862
+ WHERE root_skill_id = ?
863
+ `).all(skillId);
864
+ const forks = forkRows.map((row) => ({
865
+ forkedSkillId: row.forked_skill_id,
866
+ fromVersion: row.from_version,
867
+ reason: row.reason,
868
+ forkedAt: new Date(row.forked_at)
869
+ }));
870
+ return {
871
+ rootId: lineageRow.root_id,
872
+ versions,
873
+ forks
874
+ };
875
+ }
876
+ async searchSkills(query) {
877
+ this.ensureInitialized();
878
+ const db = this.getDb();
879
+ if (this.config.enableFTS) {
880
+ const rows = db.prepare(`
881
+ SELECT s.* FROM skills s
882
+ JOIN skills_fts fts ON s.id = fts.skill_id
883
+ WHERE skills_fts MATCH ?
884
+ ORDER BY rank
885
+ `).all(query);
886
+ return rows.map((row) => this.rowToSkill(row));
887
+ }
888
+ const allSkills = await this.listSkills();
889
+ return this.textSearch(allSkills, query);
890
+ }
891
+ /**
892
+ * Add a fork record
893
+ */
894
+ async addFork(rootSkillId, forkedSkillId, fromVersion, reason) {
895
+ this.ensureInitialized();
896
+ const db = this.getDb();
897
+ db.prepare(`
898
+ INSERT INTO skill_forks (root_skill_id, forked_skill_id, from_version, reason, forked_at)
899
+ VALUES (?, ?, ?, ?, ?)
900
+ `).run(rootSkillId, forkedSkillId, fromVersion, reason, (/* @__PURE__ */ new Date()).toISOString());
901
+ }
902
+ /**
903
+ * Update changelog for a version
904
+ */
905
+ async updateChangelog(skillId, version, changelog) {
906
+ this.ensureInitialized();
907
+ const db = this.getDb();
908
+ db.prepare(`
909
+ UPDATE skill_versions SET changelog = ? WHERE skill_id = ? AND version = ?
910
+ `).run(changelog, skillId, version);
911
+ }
912
+ /**
913
+ * Get skills by tag
914
+ */
915
+ async getSkillsByTag(tag) {
916
+ return this.listSkills({ tags: [tag] });
917
+ }
918
+ /**
919
+ * Get all tags with counts
920
+ */
921
+ async getTagCounts() {
922
+ this.ensureInitialized();
923
+ const db = this.getDb();
924
+ const rows = db.prepare("SELECT tags FROM skills").all();
925
+ const counts = /* @__PURE__ */ new Map();
926
+ for (const row of rows) {
927
+ const tags = JSON.parse(row.tags);
928
+ for (const tag of tags) {
929
+ counts.set(tag, (counts.get(tag) || 0) + 1);
930
+ }
931
+ }
932
+ return counts;
933
+ }
934
+ /**
935
+ * Get skill count by status
936
+ */
937
+ async getStatusCounts() {
938
+ this.ensureInitialized();
939
+ const db = this.getDb();
940
+ const rows = db.prepare(`
941
+ SELECT status, COUNT(*) as count FROM skills GROUP BY status
942
+ `).all();
943
+ const counts = /* @__PURE__ */ new Map();
944
+ for (const row of rows) {
945
+ counts.set(row.status, row.count);
946
+ }
947
+ return counts;
948
+ }
949
+ /**
950
+ * Close the database connection
951
+ */
952
+ close() {
953
+ if (this.db) {
954
+ this.db.close();
955
+ this.db = null;
956
+ this.initialized = false;
957
+ }
958
+ }
959
+ /**
960
+ * Export all skills for backup
961
+ */
962
+ async exportAll() {
963
+ return this.listSkills();
964
+ }
965
+ /**
966
+ * Import skills from backup
967
+ */
968
+ async importSkills(skills) {
969
+ let imported = 0;
970
+ let failed = 0;
971
+ for (const skill of skills) {
972
+ try {
973
+ await this.saveSkill(skill);
974
+ imported++;
975
+ } catch {
976
+ failed++;
977
+ }
978
+ }
979
+ return { imported, failed };
980
+ }
981
+ // =========================================================================
982
+ // TAXONOMY METHODS
983
+ // =========================================================================
984
+ /**
985
+ * Get or create a taxonomy node
986
+ */
987
+ async ensureTaxonomyNode(path2) {
988
+ this.ensureInitialized();
989
+ const db = this.getDb();
990
+ const pathStr = path2.join("/");
991
+ const existing = db.prepare("SELECT id FROM taxonomy_nodes WHERE path = ?").get(pathStr);
992
+ if (existing) return existing.id;
993
+ const id = `node-${pathStr.replace(/\//g, "-").toLowerCase()}`;
994
+ const name = path2[path2.length - 1] || "Root";
995
+ const parentPath = path2.slice(0, -1);
996
+ let parentId = null;
997
+ if (parentPath.length > 0) {
998
+ parentId = await this.ensureTaxonomyNode(parentPath);
999
+ }
1000
+ db.prepare(`
1001
+ INSERT INTO taxonomy_nodes (id, name, parent_id, path, created_at)
1002
+ VALUES (?, ?, ?, ?, ?)
1003
+ `).run(id, name, parentId, pathStr, (/* @__PURE__ */ new Date()).toISOString());
1004
+ return id;
1005
+ }
1006
+ /**
1007
+ * Place a skill in the taxonomy
1008
+ */
1009
+ async placeInTaxonomy(skillId, taxonomy) {
1010
+ this.ensureInitialized();
1011
+ const db = this.getDb();
1012
+ const primaryNodeId = await this.ensureTaxonomyNode(taxonomy.primaryPath);
1013
+ db.prepare(`
1014
+ INSERT OR REPLACE INTO skill_taxonomy_placements
1015
+ (skill_id, node_id, is_primary, confidence, created_at)
1016
+ VALUES (?, ?, 1, ?, ?)
1017
+ `).run(skillId, primaryNodeId, taxonomy.confidence || null, (/* @__PURE__ */ new Date()).toISOString());
1018
+ db.prepare("UPDATE taxonomy_nodes SET skill_count = skill_count + 1 WHERE id = ?").run(primaryNodeId);
1019
+ if (taxonomy.secondaryPaths) {
1020
+ for (const secondaryPath of taxonomy.secondaryPaths) {
1021
+ const secondaryNodeId = await this.ensureTaxonomyNode(secondaryPath);
1022
+ db.prepare(`
1023
+ INSERT OR IGNORE INTO skill_taxonomy_placements
1024
+ (skill_id, node_id, is_primary, created_at)
1025
+ VALUES (?, ?, 0, ?)
1026
+ `).run(skillId, secondaryNodeId, (/* @__PURE__ */ new Date()).toISOString());
1027
+ }
1028
+ }
1029
+ }
1030
+ /**
1031
+ * Get taxonomy tree
1032
+ */
1033
+ async getTaxonomyTree(rootPath) {
1034
+ this.ensureInitialized();
1035
+ const db = this.getDb();
1036
+ let sql = "SELECT * FROM taxonomy_nodes";
1037
+ const params = [];
1038
+ if (rootPath && rootPath.length > 0) {
1039
+ const pathPrefix = rootPath.join("/");
1040
+ sql += " WHERE path LIKE ? OR path = ?";
1041
+ params.push(`${pathPrefix}/%`, pathPrefix);
1042
+ }
1043
+ sql += " ORDER BY path";
1044
+ const rows = db.prepare(sql).all(...params);
1045
+ const nodeMap = /* @__PURE__ */ new Map();
1046
+ const roots = [];
1047
+ for (const row of rows) {
1048
+ const node = {
1049
+ id: row.id,
1050
+ name: row.name,
1051
+ path: row.path.split("/"),
1052
+ skillCount: row.skill_count,
1053
+ children: []
1054
+ };
1055
+ nodeMap.set(row.id, node);
1056
+ if (row.parent_id && nodeMap.has(row.parent_id)) {
1057
+ nodeMap.get(row.parent_id).children.push(node);
1058
+ } else {
1059
+ roots.push(node);
1060
+ }
1061
+ }
1062
+ return roots;
1063
+ }
1064
+ /**
1065
+ * Get skills in a taxonomy node
1066
+ */
1067
+ async getSkillsInTaxonomyNode(nodeId) {
1068
+ this.ensureInitialized();
1069
+ const db = this.getDb();
1070
+ const rows = db.prepare(`
1071
+ SELECT s.* FROM skills s
1072
+ JOIN skill_taxonomy_placements p ON s.id = p.skill_id
1073
+ WHERE p.node_id = ?
1074
+ `).all(nodeId);
1075
+ return rows.map((row) => this.rowToSkill(row));
1076
+ }
1077
+ // =========================================================================
1078
+ // RELATIONSHIP METHODS
1079
+ // =========================================================================
1080
+ /**
1081
+ * Save skill relationships
1082
+ */
1083
+ async saveSkillRelationships(skillId, relationships) {
1084
+ const db = this.getDb();
1085
+ db.prepare("DELETE FROM skill_relationships WHERE source_skill_id = ?").run(skillId);
1086
+ const stmt = db.prepare(`
1087
+ INSERT OR REPLACE INTO skill_relationships
1088
+ (source_skill_id, target_skill_id, type, confidence, reasoning, created_at)
1089
+ VALUES (?, ?, ?, ?, ?, ?)
1090
+ `);
1091
+ for (const rel of relationships) {
1092
+ stmt.run(
1093
+ skillId,
1094
+ rel.targetSkillId,
1095
+ rel.type,
1096
+ rel.confidence,
1097
+ rel.reasoning || null,
1098
+ (/* @__PURE__ */ new Date()).toISOString()
1099
+ );
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Add a relationship between skills
1104
+ */
1105
+ async addRelationship(sourceSkillId, targetSkillId, type, confidence, reasoning) {
1106
+ this.ensureInitialized();
1107
+ const db = this.getDb();
1108
+ db.prepare(`
1109
+ INSERT OR REPLACE INTO skill_relationships
1110
+ (source_skill_id, target_skill_id, type, confidence, reasoning, created_at)
1111
+ VALUES (?, ?, ?, ?, ?, ?)
1112
+ `).run(sourceSkillId, targetSkillId, type, confidence, reasoning || null, (/* @__PURE__ */ new Date()).toISOString());
1113
+ }
1114
+ /**
1115
+ * Get relationships for a skill
1116
+ */
1117
+ async getRelationships(skillId) {
1118
+ this.ensureInitialized();
1119
+ const db = this.getDb();
1120
+ const rows = db.prepare(`
1121
+ SELECT target_skill_id, type, confidence, reasoning
1122
+ FROM skill_relationships
1123
+ WHERE source_skill_id = ?
1124
+ `).all(skillId);
1125
+ return rows.map((row) => ({
1126
+ targetSkillId: row.target_skill_id,
1127
+ type: row.type,
1128
+ confidence: row.confidence,
1129
+ reasoning: row.reasoning || void 0
1130
+ }));
1131
+ }
1132
+ /**
1133
+ * Get skills that depend on a given skill
1134
+ */
1135
+ async getDependentSkills(skillId) {
1136
+ this.ensureInitialized();
1137
+ const db = this.getDb();
1138
+ const rows = db.prepare(`
1139
+ SELECT s.* FROM skills s
1140
+ JOIN skill_relationships r ON s.id = r.source_skill_id
1141
+ WHERE r.target_skill_id = ? AND r.type = 'depends_on'
1142
+ `).all(skillId);
1143
+ return rows.map((row) => this.rowToSkill(row));
1144
+ }
1145
+ /**
1146
+ * Get related skills (any relationship type)
1147
+ */
1148
+ async getRelatedSkills(skillId) {
1149
+ this.ensureInitialized();
1150
+ const db = this.getDb();
1151
+ const rows = db.prepare(`
1152
+ SELECT s.*, r.type, r.confidence, r.reasoning
1153
+ FROM skills s
1154
+ JOIN skill_relationships r ON s.id = r.target_skill_id
1155
+ WHERE r.source_skill_id = ?
1156
+ `).all(skillId);
1157
+ return rows.map((row) => ({
1158
+ skill: this.rowToSkill(row),
1159
+ relationship: {
1160
+ targetSkillId: row.id,
1161
+ type: row.type,
1162
+ confidence: row.confidence,
1163
+ reasoning: row.reasoning || void 0
1164
+ }
1165
+ }));
1166
+ }
1167
+ // =========================================================================
1168
+ // HELPER METHODS
1169
+ // =========================================================================
1170
+ rowToSkill(row) {
1171
+ const externalSource = row.external_source ? JSON.parse(row.external_source) : void 0;
1172
+ return {
1173
+ id: row.id,
1174
+ version: row.version,
1175
+ name: row.name,
1176
+ description: row.description,
1177
+ instructions: row.instructions,
1178
+ related: row.related ? JSON.parse(row.related) : void 0,
1179
+ author: row.author,
1180
+ tags: JSON.parse(row.tags),
1181
+ createdAt: new Date(row.created_at),
1182
+ updatedAt: new Date(row.updated_at),
1183
+ status: row.status,
1184
+ parentVersion: row.parent_version || void 0,
1185
+ derivedFrom: row.derived_from ? JSON.parse(row.derived_from) : void 0,
1186
+ source: row.source ? JSON.parse(row.source) : void 0,
1187
+ taxonomy: row.taxonomy ? JSON.parse(row.taxonomy) : void 0,
1188
+ externalSource: externalSource ? {
1189
+ ...externalSource,
1190
+ scrapedAt: new Date(externalSource.scrapedAt)
1191
+ } : void 0
1192
+ };
1193
+ }
1194
+ serializeSkill(skill) {
1195
+ return {
1196
+ ...skill,
1197
+ createdAt: skill.createdAt.toISOString(),
1198
+ updatedAt: skill.updatedAt.toISOString(),
1199
+ source: skill.source ? {
1200
+ ...skill.source,
1201
+ importedAt: skill.source.importedAt.toISOString()
1202
+ } : void 0
1203
+ };
1204
+ }
1205
+ deserializeSkill(data) {
1206
+ return {
1207
+ ...data,
1208
+ createdAt: new Date(data.createdAt),
1209
+ updatedAt: new Date(data.updatedAt),
1210
+ source: data.source ? {
1211
+ ...data.source,
1212
+ importedAt: new Date(data.source.importedAt)
1213
+ } : void 0
1214
+ };
1215
+ }
1216
+ hashSkill(skill) {
1217
+ const content = JSON.stringify({
1218
+ instructions: skill.instructions
1219
+ });
1220
+ let hash = 0;
1221
+ for (let i = 0; i < content.length; i++) {
1222
+ const char = content.charCodeAt(i);
1223
+ hash = (hash << 5) - hash + char;
1224
+ hash = hash & hash;
1225
+ }
1226
+ return Math.abs(hash).toString(16);
1227
+ }
1228
+ };
1229
+
1230
+ export {
1231
+ BaseStorageAdapter,
1232
+ MemoryStorageAdapter,
1233
+ SQLiteStorageAdapter
1234
+ };