skill-tree 0.1.0 → 0.1.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/chunk-3SRB47JW.mjs +8344 -0
- package/dist/chunk-43YOKLZP.mjs +6081 -0
- package/dist/chunk-4AGZU52D.mjs +7918 -0
- package/dist/chunk-4OC5QFIF.mjs +11267 -0
- package/dist/chunk-55SMGVTP.mjs +7126 -0
- package/dist/chunk-6FX4IK4Z.mjs +5368 -0
- package/dist/chunk-7EGDKOHV.mjs +9439 -0
- package/dist/chunk-7LMOQW5H.mjs +4893 -0
- package/dist/chunk-7VB4ZRZO.mjs +7127 -0
- package/dist/chunk-BPVRW25O.mjs +6089 -0
- package/dist/chunk-CI4476KM.mjs +6607 -0
- package/dist/chunk-DDXYQ74I.mjs +13969 -0
- package/dist/chunk-DQOFJXBX.mjs +6595 -0
- package/dist/chunk-E2CVK23F.mjs +8751 -0
- package/dist/chunk-II7DECZQ.mjs +9111 -0
- package/dist/{chunk-5QGJJCVX.mjs → chunk-INKVOZXK.mjs} +506 -0
- package/dist/chunk-K6NRCSAZ.mjs +4355 -0
- package/dist/chunk-ZQVS7MQK.mjs +6081 -0
- package/dist/cli/index.mjs +1 -1
- package/dist/index.d.mts +326 -1
- package/dist/index.d.ts +326 -1
- package/dist/index.js +509 -0
- package/dist/index.mjs +7 -1
- package/package.json +7 -3
|
@@ -0,0 +1,4893 @@
|
|
|
1
|
+
// src/adapters/base.ts
|
|
2
|
+
var BaseSessionAdapter = class {
|
|
3
|
+
/**
|
|
4
|
+
* Parse multiple sessions from a directory or concatenated file
|
|
5
|
+
*/
|
|
6
|
+
async parseMany(input) {
|
|
7
|
+
const trajectory = await this.parse(input);
|
|
8
|
+
return [trajectory];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generate a unique session ID if none provided
|
|
12
|
+
*/
|
|
13
|
+
generateSessionId() {
|
|
14
|
+
return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Safely parse JSON with error handling
|
|
18
|
+
*/
|
|
19
|
+
safeJsonParse(input) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(input);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var AdapterRegistry = class {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.adapters = /* @__PURE__ */ new Map();
|
|
30
|
+
}
|
|
31
|
+
register(adapter) {
|
|
32
|
+
this.adapters.set(adapter.name, adapter);
|
|
33
|
+
}
|
|
34
|
+
unregister(name) {
|
|
35
|
+
return this.adapters.delete(name);
|
|
36
|
+
}
|
|
37
|
+
get(name) {
|
|
38
|
+
return this.adapters.get(name);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find an adapter that can handle the given input
|
|
42
|
+
*/
|
|
43
|
+
findAdapter(input) {
|
|
44
|
+
for (const adapter of this.adapters.values()) {
|
|
45
|
+
if (adapter.canHandle(input)) {
|
|
46
|
+
return adapter;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Find adapter by file extension
|
|
53
|
+
*/
|
|
54
|
+
findByExtension(extension) {
|
|
55
|
+
const ext = extension.startsWith(".") ? extension : `.${extension}`;
|
|
56
|
+
for (const adapter of this.adapters.values()) {
|
|
57
|
+
if (adapter.supportedExtensions.includes(ext)) {
|
|
58
|
+
return adapter;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
listAdapters() {
|
|
64
|
+
return Array.from(this.adapters.values());
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var adapterRegistry = new AdapterRegistry();
|
|
68
|
+
|
|
69
|
+
// src/adapters/claude-code.ts
|
|
70
|
+
var ClaudeCodeAdapter = class extends BaseSessionAdapter {
|
|
71
|
+
constructor() {
|
|
72
|
+
super(...arguments);
|
|
73
|
+
this.name = "claude-code";
|
|
74
|
+
this.supportedExtensions = [".jsonl", ".json"];
|
|
75
|
+
}
|
|
76
|
+
canHandle(input) {
|
|
77
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
78
|
+
const firstLine = str.split("\n")[0]?.trim();
|
|
79
|
+
if (!firstLine) return false;
|
|
80
|
+
const parsed = this.safeJsonParse(firstLine);
|
|
81
|
+
if (!parsed) return false;
|
|
82
|
+
return parsed.type !== void 0 || parsed.message?.role !== void 0 || parsed.sessionId !== void 0;
|
|
83
|
+
}
|
|
84
|
+
async parse(input) {
|
|
85
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
86
|
+
const lines = str.split("\n").filter((line) => line.trim());
|
|
87
|
+
const messages = [];
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const parsed = this.safeJsonParse(line);
|
|
90
|
+
if (parsed) {
|
|
91
|
+
messages.push(parsed);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return this.convertToTrajectory(messages);
|
|
95
|
+
}
|
|
96
|
+
async parseMany(input) {
|
|
97
|
+
const trajectory = await this.parse(input);
|
|
98
|
+
return [trajectory];
|
|
99
|
+
}
|
|
100
|
+
convertToTrajectory(messages) {
|
|
101
|
+
const turns = [];
|
|
102
|
+
let sessionId = this.generateSessionId();
|
|
103
|
+
let startedAt = /* @__PURE__ */ new Date();
|
|
104
|
+
let endedAt;
|
|
105
|
+
const metadata = {
|
|
106
|
+
agentType: "claude-code",
|
|
107
|
+
custom: {}
|
|
108
|
+
};
|
|
109
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
110
|
+
for (let i = 0; i < messages.length; i++) {
|
|
111
|
+
const msg = messages[i];
|
|
112
|
+
if (msg.sessionId) sessionId = msg.sessionId;
|
|
113
|
+
if (msg.timestamp) {
|
|
114
|
+
const ts = new Date(msg.timestamp);
|
|
115
|
+
if (i === 0) startedAt = ts;
|
|
116
|
+
endedAt = ts;
|
|
117
|
+
}
|
|
118
|
+
if (msg.cwd) metadata.workingDirectory = msg.cwd;
|
|
119
|
+
if (msg.model) metadata.model = msg.model;
|
|
120
|
+
if (msg.summary) continue;
|
|
121
|
+
if (msg.type === "result" && msg.toolUseId) {
|
|
122
|
+
const lastAssistantTurn = [...turns].reverse().find((t) => t.role === "assistant");
|
|
123
|
+
if (lastAssistantTurn) {
|
|
124
|
+
if (!lastAssistantTurn.toolResults) {
|
|
125
|
+
lastAssistantTurn.toolResults = [];
|
|
126
|
+
}
|
|
127
|
+
lastAssistantTurn.toolResults.push({
|
|
128
|
+
callId: msg.toolUseId,
|
|
129
|
+
name: msg.toolName || "unknown",
|
|
130
|
+
output: msg.result || "",
|
|
131
|
+
success: true
|
|
132
|
+
// Claude Code doesn't always indicate errors explicitly
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (msg.message) {
|
|
138
|
+
const turn = this.convertMessageToTurn(msg, turns.length, pendingToolCalls);
|
|
139
|
+
if (turn) {
|
|
140
|
+
turns.push(turn);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const outcome = this.inferOutcome(turns);
|
|
145
|
+
return {
|
|
146
|
+
sessionId,
|
|
147
|
+
startedAt,
|
|
148
|
+
endedAt,
|
|
149
|
+
turns,
|
|
150
|
+
metadata,
|
|
151
|
+
outcome
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
convertMessageToTurn(msg, index, pendingToolCalls) {
|
|
155
|
+
if (!msg.message) return null;
|
|
156
|
+
const { role, content } = msg.message;
|
|
157
|
+
const turn = {
|
|
158
|
+
index,
|
|
159
|
+
role,
|
|
160
|
+
content: "",
|
|
161
|
+
timestamp: msg.timestamp ? new Date(msg.timestamp) : void 0
|
|
162
|
+
};
|
|
163
|
+
if (typeof content === "string") {
|
|
164
|
+
turn.content = content;
|
|
165
|
+
} else if (Array.isArray(content)) {
|
|
166
|
+
const textParts = [];
|
|
167
|
+
const toolCalls = [];
|
|
168
|
+
const toolResults = [];
|
|
169
|
+
for (const block of content) {
|
|
170
|
+
if (block.type === "text" && block.text) {
|
|
171
|
+
textParts.push(block.text);
|
|
172
|
+
} else if (block.type === "tool_use" && block.name) {
|
|
173
|
+
const toolCall = {
|
|
174
|
+
name: block.name,
|
|
175
|
+
input: block.input || {},
|
|
176
|
+
callId: block.id
|
|
177
|
+
};
|
|
178
|
+
toolCalls.push(toolCall);
|
|
179
|
+
if (block.id) {
|
|
180
|
+
pendingToolCalls.set(block.id, toolCall);
|
|
181
|
+
}
|
|
182
|
+
} else if (block.type === "tool_result") {
|
|
183
|
+
const resultContent = this.extractToolResultContent(block.content);
|
|
184
|
+
toolResults.push({
|
|
185
|
+
callId: block.tool_use_id,
|
|
186
|
+
name: pendingToolCalls.get(block.tool_use_id || "")?.name || "unknown",
|
|
187
|
+
output: resultContent,
|
|
188
|
+
success: !block.is_error,
|
|
189
|
+
error: block.is_error ? resultContent : void 0
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
turn.content = textParts.join("\n");
|
|
194
|
+
if (toolCalls.length > 0) turn.toolCalls = toolCalls;
|
|
195
|
+
if (toolResults.length > 0) turn.toolResults = toolResults;
|
|
196
|
+
}
|
|
197
|
+
return turn;
|
|
198
|
+
}
|
|
199
|
+
extractToolResultContent(content) {
|
|
200
|
+
if (!content) return "";
|
|
201
|
+
if (typeof content === "string") return content;
|
|
202
|
+
if (Array.isArray(content)) {
|
|
203
|
+
return content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
204
|
+
}
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
inferOutcome(turns) {
|
|
208
|
+
const outcome = {
|
|
209
|
+
success: true,
|
|
210
|
+
// Default to success, look for failure signals
|
|
211
|
+
filesModified: [],
|
|
212
|
+
errors: []
|
|
213
|
+
};
|
|
214
|
+
for (const turn of turns) {
|
|
215
|
+
if (turn.content) {
|
|
216
|
+
const lowerContent = turn.content.toLowerCase();
|
|
217
|
+
if (lowerContent.includes("error") || lowerContent.includes("failed") || lowerContent.includes("couldn't") || lowerContent.includes("can't complete")) {
|
|
218
|
+
outcome.errors?.push(turn.content.substring(0, 200));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (turn.toolResults) {
|
|
222
|
+
for (const result of turn.toolResults) {
|
|
223
|
+
if (!result.success && result.error) {
|
|
224
|
+
outcome.errors?.push(result.error.substring(0, 200));
|
|
225
|
+
outcome.success = false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (turn.toolCalls) {
|
|
230
|
+
for (const call of turn.toolCalls) {
|
|
231
|
+
if (["Write", "Edit", "MultiEdit"].includes(call.name)) {
|
|
232
|
+
const filePath = call.input.file_path || call.input.filePath;
|
|
233
|
+
if (typeof filePath === "string") {
|
|
234
|
+
outcome.filesModified?.push(filePath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
outcome.filesModified = [...new Set(outcome.filesModified)];
|
|
241
|
+
if (outcome.errors?.length === 0) {
|
|
242
|
+
outcome.success = true;
|
|
243
|
+
}
|
|
244
|
+
return outcome;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var claudeCodeAdapter = new ClaudeCodeAdapter();
|
|
248
|
+
|
|
249
|
+
// src/adapters/openai.ts
|
|
250
|
+
var OpenAIAdapter = class extends BaseSessionAdapter {
|
|
251
|
+
constructor() {
|
|
252
|
+
super(...arguments);
|
|
253
|
+
this.name = "openai";
|
|
254
|
+
this.supportedExtensions = [".json", ".jsonl"];
|
|
255
|
+
}
|
|
256
|
+
canHandle(input) {
|
|
257
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(str);
|
|
260
|
+
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
261
|
+
return this.looksLikeOpenAIMessages(parsed.messages);
|
|
262
|
+
}
|
|
263
|
+
if (Array.isArray(parsed)) {
|
|
264
|
+
return this.looksLikeOpenAIMessages(parsed);
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
} catch {
|
|
268
|
+
const firstLine = str.split("\n")[0]?.trim();
|
|
269
|
+
if (!firstLine) return false;
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(firstLine);
|
|
272
|
+
return parsed.role !== void 0 && ["system", "user", "assistant", "tool"].includes(parsed.role);
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async parse(input) {
|
|
279
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
280
|
+
let conversation;
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(str);
|
|
283
|
+
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
284
|
+
conversation = parsed;
|
|
285
|
+
} else if (Array.isArray(parsed)) {
|
|
286
|
+
conversation = { messages: parsed };
|
|
287
|
+
} else {
|
|
288
|
+
throw new Error("Invalid OpenAI format");
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
const lines = str.split("\n").filter((l) => l.trim());
|
|
292
|
+
const messages = [];
|
|
293
|
+
for (const line of lines) {
|
|
294
|
+
try {
|
|
295
|
+
messages.push(JSON.parse(line));
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
conversation = { messages };
|
|
300
|
+
}
|
|
301
|
+
return this.convertToTrajectory(conversation);
|
|
302
|
+
}
|
|
303
|
+
async parseMany(input) {
|
|
304
|
+
const sections = input.split(/\n\n+/);
|
|
305
|
+
const trajectories = [];
|
|
306
|
+
for (const section of sections) {
|
|
307
|
+
if (section.trim()) {
|
|
308
|
+
try {
|
|
309
|
+
trajectories.push(await this.parse(section));
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return trajectories.length > 0 ? trajectories : [await this.parse(input)];
|
|
315
|
+
}
|
|
316
|
+
looksLikeOpenAIMessages(messages) {
|
|
317
|
+
if (messages.length === 0) return false;
|
|
318
|
+
const first = messages[0];
|
|
319
|
+
return first.role !== void 0 && ["system", "user", "assistant", "tool"].includes(first.role);
|
|
320
|
+
}
|
|
321
|
+
convertToTrajectory(conversation) {
|
|
322
|
+
const turns = [];
|
|
323
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
324
|
+
const metadata = {
|
|
325
|
+
agentType: "openai",
|
|
326
|
+
model: conversation.model,
|
|
327
|
+
custom: conversation.metadata
|
|
328
|
+
};
|
|
329
|
+
for (let i = 0; i < conversation.messages.length; i++) {
|
|
330
|
+
const msg = conversation.messages[i];
|
|
331
|
+
if (msg.role === "tool" && msg.tool_call_id) {
|
|
332
|
+
const lastAssistantTurn = [...turns].reverse().find((t) => t.role === "assistant");
|
|
333
|
+
if (lastAssistantTurn) {
|
|
334
|
+
if (!lastAssistantTurn.toolResults) {
|
|
335
|
+
lastAssistantTurn.toolResults = [];
|
|
336
|
+
}
|
|
337
|
+
const pendingCall = pendingToolCalls.get(msg.tool_call_id);
|
|
338
|
+
lastAssistantTurn.toolResults.push({
|
|
339
|
+
callId: msg.tool_call_id,
|
|
340
|
+
name: pendingCall?.name || msg.name || "unknown",
|
|
341
|
+
output: msg.content || "",
|
|
342
|
+
success: true
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const turn = {
|
|
348
|
+
index: turns.length,
|
|
349
|
+
role: msg.role,
|
|
350
|
+
content: msg.content || ""
|
|
351
|
+
};
|
|
352
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
353
|
+
turn.toolCalls = msg.tool_calls.map((tc) => {
|
|
354
|
+
let parsedArgs = {};
|
|
355
|
+
try {
|
|
356
|
+
parsedArgs = JSON.parse(tc.function.arguments);
|
|
357
|
+
} catch {
|
|
358
|
+
parsedArgs = { raw: tc.function.arguments };
|
|
359
|
+
}
|
|
360
|
+
const toolCall = {
|
|
361
|
+
callId: tc.id,
|
|
362
|
+
name: tc.function.name,
|
|
363
|
+
input: parsedArgs
|
|
364
|
+
};
|
|
365
|
+
pendingToolCalls.set(tc.id, toolCall);
|
|
366
|
+
return toolCall;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
turns.push(turn);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
sessionId: conversation.id || this.generateSessionId(),
|
|
373
|
+
startedAt: conversation.created ? new Date(conversation.created * 1e3) : /* @__PURE__ */ new Date(),
|
|
374
|
+
turns,
|
|
375
|
+
metadata
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
var openAIAdapter = new OpenAIAdapter();
|
|
380
|
+
|
|
381
|
+
// src/adapters/anthropic.ts
|
|
382
|
+
var AnthropicAdapter = class extends BaseSessionAdapter {
|
|
383
|
+
constructor() {
|
|
384
|
+
super(...arguments);
|
|
385
|
+
this.name = "anthropic";
|
|
386
|
+
this.supportedExtensions = [".json", ".jsonl"];
|
|
387
|
+
}
|
|
388
|
+
canHandle(input) {
|
|
389
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
390
|
+
try {
|
|
391
|
+
const parsed = JSON.parse(str);
|
|
392
|
+
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
393
|
+
return this.looksLikeAnthropicMessages(parsed.messages);
|
|
394
|
+
}
|
|
395
|
+
if (Array.isArray(parsed)) {
|
|
396
|
+
return this.looksLikeAnthropicMessages(parsed);
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
} catch {
|
|
400
|
+
const firstLine = str.split("\n")[0]?.trim();
|
|
401
|
+
if (!firstLine) return false;
|
|
402
|
+
try {
|
|
403
|
+
const parsed = JSON.parse(firstLine);
|
|
404
|
+
return parsed.role !== void 0 && ["user", "assistant"].includes(parsed.role) && (typeof parsed.content === "string" || Array.isArray(parsed.content));
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async parse(input) {
|
|
411
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
412
|
+
let conversation;
|
|
413
|
+
try {
|
|
414
|
+
const parsed = JSON.parse(str);
|
|
415
|
+
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
416
|
+
conversation = parsed;
|
|
417
|
+
} else if (Array.isArray(parsed)) {
|
|
418
|
+
conversation = { messages: parsed };
|
|
419
|
+
} else {
|
|
420
|
+
throw new Error("Invalid Anthropic format");
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
const lines = str.split("\n").filter((l) => l.trim());
|
|
424
|
+
const messages = [];
|
|
425
|
+
for (const line of lines) {
|
|
426
|
+
try {
|
|
427
|
+
messages.push(JSON.parse(line));
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
conversation = { messages };
|
|
432
|
+
}
|
|
433
|
+
return this.convertToTrajectory(conversation);
|
|
434
|
+
}
|
|
435
|
+
async parseMany(input) {
|
|
436
|
+
const sections = input.split(/\n\n+/);
|
|
437
|
+
const trajectories = [];
|
|
438
|
+
for (const section of sections) {
|
|
439
|
+
if (section.trim()) {
|
|
440
|
+
try {
|
|
441
|
+
trajectories.push(await this.parse(section));
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return trajectories.length > 0 ? trajectories : [await this.parse(input)];
|
|
447
|
+
}
|
|
448
|
+
looksLikeAnthropicMessages(messages) {
|
|
449
|
+
if (messages.length === 0) return false;
|
|
450
|
+
const first = messages[0];
|
|
451
|
+
return first.role !== void 0 && ["user", "assistant"].includes(first.role) && first.content !== void 0 && (typeof first.content === "string" || Array.isArray(first.content));
|
|
452
|
+
}
|
|
453
|
+
convertToTrajectory(conversation) {
|
|
454
|
+
const turns = [];
|
|
455
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
456
|
+
const metadata = {
|
|
457
|
+
agentType: "anthropic",
|
|
458
|
+
model: conversation.model,
|
|
459
|
+
custom: conversation.metadata
|
|
460
|
+
};
|
|
461
|
+
if (conversation.system) {
|
|
462
|
+
const systemContent = typeof conversation.system === "string" ? conversation.system : this.extractTextContent(conversation.system);
|
|
463
|
+
turns.push({
|
|
464
|
+
index: 0,
|
|
465
|
+
role: "system",
|
|
466
|
+
content: systemContent
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
for (const msg of conversation.messages) {
|
|
470
|
+
const turn = {
|
|
471
|
+
index: turns.length,
|
|
472
|
+
role: msg.role,
|
|
473
|
+
content: ""
|
|
474
|
+
};
|
|
475
|
+
if (typeof msg.content === "string") {
|
|
476
|
+
turn.content = msg.content;
|
|
477
|
+
} else if (Array.isArray(msg.content)) {
|
|
478
|
+
const textParts = [];
|
|
479
|
+
const toolCalls = [];
|
|
480
|
+
const toolResults = [];
|
|
481
|
+
for (const block of msg.content) {
|
|
482
|
+
switch (block.type) {
|
|
483
|
+
case "text":
|
|
484
|
+
textParts.push(block.text);
|
|
485
|
+
break;
|
|
486
|
+
case "tool_use": {
|
|
487
|
+
const toolCall = {
|
|
488
|
+
callId: block.id,
|
|
489
|
+
name: block.name,
|
|
490
|
+
input: block.input
|
|
491
|
+
};
|
|
492
|
+
toolCalls.push(toolCall);
|
|
493
|
+
pendingToolCalls.set(block.id, toolCall);
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
case "tool_result": {
|
|
497
|
+
const pendingCall = pendingToolCalls.get(block.tool_use_id);
|
|
498
|
+
const resultContent = typeof block.content === "string" ? block.content : this.extractTextContent(block.content);
|
|
499
|
+
toolResults.push({
|
|
500
|
+
callId: block.tool_use_id,
|
|
501
|
+
name: pendingCall?.name || "unknown",
|
|
502
|
+
output: resultContent,
|
|
503
|
+
success: !block.is_error,
|
|
504
|
+
error: block.is_error ? resultContent : void 0
|
|
505
|
+
});
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
turn.content = textParts.join("\n");
|
|
511
|
+
if (toolCalls.length > 0) {
|
|
512
|
+
turn.toolCalls = toolCalls;
|
|
513
|
+
}
|
|
514
|
+
if (toolResults.length > 0) {
|
|
515
|
+
turn.toolResults = toolResults;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
turns.push(turn);
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
sessionId: conversation.id || this.generateSessionId(),
|
|
522
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
523
|
+
turns,
|
|
524
|
+
metadata
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
extractTextContent(blocks) {
|
|
528
|
+
return blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
var anthropicAdapter = new AnthropicAdapter();
|
|
532
|
+
|
|
533
|
+
// src/adapters/generic.ts
|
|
534
|
+
var DEFAULT_CONFIG = {
|
|
535
|
+
messagesPath: "messages",
|
|
536
|
+
roleField: "role",
|
|
537
|
+
contentField: "content",
|
|
538
|
+
roleMapping: {
|
|
539
|
+
user: "user",
|
|
540
|
+
assistant: "assistant",
|
|
541
|
+
system: "system",
|
|
542
|
+
tool: "tool",
|
|
543
|
+
human: "user",
|
|
544
|
+
ai: "assistant",
|
|
545
|
+
bot: "assistant"
|
|
546
|
+
},
|
|
547
|
+
timestampField: "timestamp",
|
|
548
|
+
sessionIdField: "id",
|
|
549
|
+
toolCallsPath: "tool_calls",
|
|
550
|
+
toolCallFields: {
|
|
551
|
+
name: "name",
|
|
552
|
+
input: "arguments",
|
|
553
|
+
id: "id"
|
|
554
|
+
},
|
|
555
|
+
metadataPath: "metadata"
|
|
556
|
+
};
|
|
557
|
+
var GenericAdapter = class extends BaseSessionAdapter {
|
|
558
|
+
constructor(config = {}) {
|
|
559
|
+
super();
|
|
560
|
+
this.name = "generic";
|
|
561
|
+
this.supportedExtensions = [".json", ".jsonl"];
|
|
562
|
+
this.config = {
|
|
563
|
+
...DEFAULT_CONFIG,
|
|
564
|
+
...config,
|
|
565
|
+
roleMapping: { ...DEFAULT_CONFIG.roleMapping, ...config.roleMapping },
|
|
566
|
+
toolCallFields: { ...DEFAULT_CONFIG.toolCallFields, ...config.toolCallFields }
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Update configuration
|
|
571
|
+
*/
|
|
572
|
+
setConfig(config) {
|
|
573
|
+
this.config = {
|
|
574
|
+
...this.config,
|
|
575
|
+
...config,
|
|
576
|
+
roleMapping: { ...this.config.roleMapping, ...config.roleMapping },
|
|
577
|
+
toolCallFields: { ...this.config.toolCallFields, ...config.toolCallFields }
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get current configuration
|
|
582
|
+
*/
|
|
583
|
+
getConfig() {
|
|
584
|
+
return { ...this.config };
|
|
585
|
+
}
|
|
586
|
+
canHandle(input) {
|
|
587
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(str);
|
|
590
|
+
return this.canExtractMessages(parsed);
|
|
591
|
+
} catch {
|
|
592
|
+
const firstLine = str.split("\n")[0]?.trim();
|
|
593
|
+
if (!firstLine) return false;
|
|
594
|
+
try {
|
|
595
|
+
const parsed = JSON.parse(firstLine);
|
|
596
|
+
return this.looksLikeMessage(parsed);
|
|
597
|
+
} catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async parse(input) {
|
|
603
|
+
const str = typeof input === "string" ? input : input.toString("utf-8");
|
|
604
|
+
try {
|
|
605
|
+
const parsed = JSON.parse(str);
|
|
606
|
+
return this.convertToTrajectory(parsed);
|
|
607
|
+
} catch {
|
|
608
|
+
const lines = str.split("\n").filter((l) => l.trim());
|
|
609
|
+
const messages = [];
|
|
610
|
+
for (const line of lines) {
|
|
611
|
+
try {
|
|
612
|
+
messages.push(JSON.parse(line));
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return this.convertToTrajectory({ messages });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async parseMany(input) {
|
|
620
|
+
const sections = input.split(/\n\n+/);
|
|
621
|
+
const trajectories = [];
|
|
622
|
+
for (const section of sections) {
|
|
623
|
+
if (section.trim()) {
|
|
624
|
+
try {
|
|
625
|
+
trajectories.push(await this.parse(section));
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return trajectories.length > 0 ? trajectories : [await this.parse(input)];
|
|
631
|
+
}
|
|
632
|
+
canExtractMessages(obj) {
|
|
633
|
+
if (!obj || typeof obj !== "object") return false;
|
|
634
|
+
const messages = this.getNestedValue(obj, this.config.messagesPath);
|
|
635
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
636
|
+
return this.looksLikeMessage(messages[0]);
|
|
637
|
+
}
|
|
638
|
+
if (Array.isArray(obj) && obj.length > 0) {
|
|
639
|
+
return this.looksLikeMessage(obj[0]);
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
looksLikeMessage(obj) {
|
|
644
|
+
if (!obj || typeof obj !== "object") return false;
|
|
645
|
+
const record = obj;
|
|
646
|
+
const role = record[this.config.roleField];
|
|
647
|
+
if (role === void 0) return false;
|
|
648
|
+
const content = record[this.config.contentField];
|
|
649
|
+
if (content === void 0) return false;
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
convertToTrajectory(data) {
|
|
653
|
+
const obj = data;
|
|
654
|
+
let messages;
|
|
655
|
+
const messagesFromPath = this.getNestedValue(obj, this.config.messagesPath);
|
|
656
|
+
if (Array.isArray(messagesFromPath)) {
|
|
657
|
+
messages = messagesFromPath;
|
|
658
|
+
} else if (Array.isArray(data)) {
|
|
659
|
+
messages = data;
|
|
660
|
+
} else {
|
|
661
|
+
throw new Error("Could not find messages array");
|
|
662
|
+
}
|
|
663
|
+
const sessionId = this.getNestedValue(obj, this.config.sessionIdField) || this.generateSessionId();
|
|
664
|
+
const customMetadata = this.getNestedValue(obj, this.config.metadataPath);
|
|
665
|
+
const metadata = {
|
|
666
|
+
agentType: "generic",
|
|
667
|
+
custom: customMetadata
|
|
668
|
+
};
|
|
669
|
+
const turns = [];
|
|
670
|
+
for (const msg of messages) {
|
|
671
|
+
const turn = this.convertMessageToTurn(msg, turns.length);
|
|
672
|
+
if (turn) {
|
|
673
|
+
turns.push(turn);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
sessionId,
|
|
678
|
+
startedAt: this.extractTimestamp(messages[0]) || /* @__PURE__ */ new Date(),
|
|
679
|
+
endedAt: this.extractTimestamp(messages[messages.length - 1]),
|
|
680
|
+
turns,
|
|
681
|
+
metadata
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
convertMessageToTurn(msg, index) {
|
|
685
|
+
const rawRole = msg[this.config.roleField];
|
|
686
|
+
if (rawRole === void 0) return null;
|
|
687
|
+
const role = this.mapRole(String(rawRole));
|
|
688
|
+
let content = "";
|
|
689
|
+
const rawContent = msg[this.config.contentField];
|
|
690
|
+
if (typeof rawContent === "string") {
|
|
691
|
+
content = rawContent;
|
|
692
|
+
} else if (rawContent !== null && rawContent !== void 0) {
|
|
693
|
+
content = JSON.stringify(rawContent);
|
|
694
|
+
}
|
|
695
|
+
const turn = {
|
|
696
|
+
index,
|
|
697
|
+
role,
|
|
698
|
+
content,
|
|
699
|
+
timestamp: this.extractTimestamp(msg)
|
|
700
|
+
};
|
|
701
|
+
const toolCallsData = this.getNestedValue(msg, this.config.toolCallsPath);
|
|
702
|
+
if (Array.isArray(toolCallsData) && toolCallsData.length > 0) {
|
|
703
|
+
turn.toolCalls = toolCallsData.map(
|
|
704
|
+
(tc) => this.convertToolCall(tc)
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (this.config.customExtractors) {
|
|
708
|
+
for (const [key, extractor] of Object.entries(this.config.customExtractors)) {
|
|
709
|
+
try {
|
|
710
|
+
const value = extractor(msg);
|
|
711
|
+
if (value !== void 0) {
|
|
712
|
+
turn[key] = value;
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return turn;
|
|
719
|
+
}
|
|
720
|
+
convertToolCall(tc) {
|
|
721
|
+
const fields = this.config.toolCallFields;
|
|
722
|
+
let input = {};
|
|
723
|
+
const rawInput = tc[fields.input || "arguments"];
|
|
724
|
+
if (typeof rawInput === "string") {
|
|
725
|
+
try {
|
|
726
|
+
input = JSON.parse(rawInput);
|
|
727
|
+
} catch {
|
|
728
|
+
input = { raw: rawInput };
|
|
729
|
+
}
|
|
730
|
+
} else if (rawInput && typeof rawInput === "object") {
|
|
731
|
+
input = rawInput;
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
callId: tc[fields.id || "id"],
|
|
735
|
+
name: tc[fields.name || "name"] || "unknown",
|
|
736
|
+
input
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
mapRole(rawRole) {
|
|
740
|
+
const normalized = rawRole.toLowerCase();
|
|
741
|
+
return this.config.roleMapping[normalized] || "user";
|
|
742
|
+
}
|
|
743
|
+
extractTimestamp(msg) {
|
|
744
|
+
if (!msg || typeof msg !== "object") return void 0;
|
|
745
|
+
const record = msg;
|
|
746
|
+
const tsValue = record[this.config.timestampField];
|
|
747
|
+
if (!tsValue) return void 0;
|
|
748
|
+
if (typeof tsValue === "number") {
|
|
749
|
+
return new Date(tsValue > 1e12 ? tsValue : tsValue * 1e3);
|
|
750
|
+
}
|
|
751
|
+
if (typeof tsValue === "string") {
|
|
752
|
+
const parsed = new Date(tsValue);
|
|
753
|
+
return isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
754
|
+
}
|
|
755
|
+
return void 0;
|
|
756
|
+
}
|
|
757
|
+
getNestedValue(obj, path2) {
|
|
758
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
759
|
+
const parts = path2.split(".");
|
|
760
|
+
let current = obj;
|
|
761
|
+
for (const part of parts) {
|
|
762
|
+
if (current === null || current === void 0) return void 0;
|
|
763
|
+
if (typeof current !== "object") return void 0;
|
|
764
|
+
current = current[part];
|
|
765
|
+
}
|
|
766
|
+
return current;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
function createGenericAdapter(config) {
|
|
770
|
+
return new GenericAdapter(config);
|
|
771
|
+
}
|
|
772
|
+
var genericAdapter = new GenericAdapter();
|
|
773
|
+
|
|
774
|
+
// src/adapters/index.ts
|
|
775
|
+
adapterRegistry.register(claudeCodeAdapter);
|
|
776
|
+
adapterRegistry.register(openAIAdapter);
|
|
777
|
+
adapterRegistry.register(anthropicAdapter);
|
|
778
|
+
adapterRegistry.register(genericAdapter);
|
|
779
|
+
|
|
780
|
+
// src/extraction/quality-gates.ts
|
|
781
|
+
var DEFAULT_QUALITY_GATES = [
|
|
782
|
+
{
|
|
783
|
+
name: "reusability",
|
|
784
|
+
type: "reusability",
|
|
785
|
+
description: "Skill should be applicable across multiple future tasks, not one-off",
|
|
786
|
+
required: true
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
name: "non-trivial",
|
|
790
|
+
type: "non-trivial",
|
|
791
|
+
description: "Skill should involve discovery beyond basic documentation lookup",
|
|
792
|
+
required: true
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "specific-triggers",
|
|
796
|
+
type: "specific",
|
|
797
|
+
description: "Skill should have clear, specific trigger conditions",
|
|
798
|
+
required: true
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "verified",
|
|
802
|
+
type: "verified",
|
|
803
|
+
description: "Skill solution should be verified to actually work",
|
|
804
|
+
required: false
|
|
805
|
+
}
|
|
806
|
+
];
|
|
807
|
+
var QualityGateEvaluator = class {
|
|
808
|
+
constructor(gates = DEFAULT_QUALITY_GATES) {
|
|
809
|
+
this.gates = gates;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Evaluate all quality gates for a skill candidate
|
|
813
|
+
*/
|
|
814
|
+
async evaluate(skill, trajectory, turnRange) {
|
|
815
|
+
const results = [];
|
|
816
|
+
for (const gate of this.gates) {
|
|
817
|
+
const result = await this.evaluateGate(gate, skill, trajectory, turnRange);
|
|
818
|
+
results.push(result);
|
|
819
|
+
}
|
|
820
|
+
const passed = results.every((r) => {
|
|
821
|
+
const gate = this.gates.find((g) => g.name === r.gateName);
|
|
822
|
+
return !gate?.required || r.passed;
|
|
823
|
+
});
|
|
824
|
+
return { passed, results };
|
|
825
|
+
}
|
|
826
|
+
async evaluateGate(gate, skill, trajectory, turnRange) {
|
|
827
|
+
const turns = trajectory.turns.slice(turnRange[0], turnRange[1] + 1);
|
|
828
|
+
switch (gate.type) {
|
|
829
|
+
case "reusability":
|
|
830
|
+
return this.evaluateReusability(gate, skill, turns);
|
|
831
|
+
case "non-trivial":
|
|
832
|
+
return this.evaluateNonTrivial(gate, skill, turns);
|
|
833
|
+
case "specific":
|
|
834
|
+
return this.evaluateSpecificTriggers(gate, skill);
|
|
835
|
+
case "verified":
|
|
836
|
+
return this.evaluateVerified(gate, trajectory);
|
|
837
|
+
case "custom":
|
|
838
|
+
return this.evaluateCustom(gate, skill, trajectory, turns);
|
|
839
|
+
default:
|
|
840
|
+
return {
|
|
841
|
+
gateName: gate.name,
|
|
842
|
+
passed: true,
|
|
843
|
+
score: 1,
|
|
844
|
+
explanation: "Unknown gate type, passing by default"
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
evaluateReusability(gate, skill, turns) {
|
|
849
|
+
let score = 0;
|
|
850
|
+
const reasons = [];
|
|
851
|
+
const description = skill.description || "";
|
|
852
|
+
const problem = skill.problem || "";
|
|
853
|
+
const combined = `${description} ${problem}`.toLowerCase();
|
|
854
|
+
const reusabilitySignals = [
|
|
855
|
+
"error",
|
|
856
|
+
"bug",
|
|
857
|
+
"fix",
|
|
858
|
+
"pattern",
|
|
859
|
+
"when",
|
|
860
|
+
"if",
|
|
861
|
+
"always",
|
|
862
|
+
"common",
|
|
863
|
+
"typical",
|
|
864
|
+
"usually"
|
|
865
|
+
];
|
|
866
|
+
const signalCount = reusabilitySignals.filter((s) => combined.includes(s)).length;
|
|
867
|
+
score += Math.min(signalCount * 0.15, 0.45);
|
|
868
|
+
const specificSignals = [
|
|
869
|
+
"this project",
|
|
870
|
+
"this repo",
|
|
871
|
+
"our codebase",
|
|
872
|
+
"specific to",
|
|
873
|
+
"only in"
|
|
874
|
+
];
|
|
875
|
+
const specificCount = specificSignals.filter((s) => combined.includes(s)).length;
|
|
876
|
+
score -= specificCount * 0.2;
|
|
877
|
+
if (skill.triggerConditions && skill.triggerConditions.length > 0) {
|
|
878
|
+
const hasGenericTrigger = skill.triggerConditions.some(
|
|
879
|
+
(t) => t.type === "error" || t.type === "pattern"
|
|
880
|
+
);
|
|
881
|
+
if (hasGenericTrigger) {
|
|
882
|
+
score += 0.25;
|
|
883
|
+
reasons.push("Has generic trigger conditions");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const toolNames = /* @__PURE__ */ new Set();
|
|
887
|
+
for (const turn of turns) {
|
|
888
|
+
turn.toolCalls?.forEach((tc) => toolNames.add(tc.name));
|
|
889
|
+
}
|
|
890
|
+
if (toolNames.size > 2) {
|
|
891
|
+
score += 0.15;
|
|
892
|
+
reasons.push(`Uses ${toolNames.size} different tools`);
|
|
893
|
+
}
|
|
894
|
+
score = Math.max(0, Math.min(1, score + 0.3));
|
|
895
|
+
return {
|
|
896
|
+
gateName: gate.name,
|
|
897
|
+
passed: score >= 0.5,
|
|
898
|
+
score,
|
|
899
|
+
explanation: reasons.length > 0 ? reasons.join("; ") : score >= 0.5 ? "Skill appears reusable" : "Skill may be too project-specific"
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
evaluateNonTrivial(gate, skill, turns) {
|
|
903
|
+
let score = 0;
|
|
904
|
+
const reasons = [];
|
|
905
|
+
if (turns.length >= 5) {
|
|
906
|
+
score += 0.2;
|
|
907
|
+
reasons.push(`${turns.length} turns of investigation`);
|
|
908
|
+
} else if (turns.length >= 3) {
|
|
909
|
+
score += 0.1;
|
|
910
|
+
}
|
|
911
|
+
const hasErrors = turns.some(
|
|
912
|
+
(t) => t.toolResults?.some((r) => !r.success) || t.content?.toLowerCase().includes("error")
|
|
913
|
+
);
|
|
914
|
+
if (hasErrors) {
|
|
915
|
+
score += 0.25;
|
|
916
|
+
reasons.push("Involves error diagnosis/recovery");
|
|
917
|
+
}
|
|
918
|
+
const toolCallCounts = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const turn of turns) {
|
|
920
|
+
turn.toolCalls?.forEach((tc) => {
|
|
921
|
+
toolCallCounts.set(tc.name, (toolCallCounts.get(tc.name) || 0) + 1);
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
const hasIteration = Array.from(toolCallCounts.values()).some((count) => count >= 3);
|
|
925
|
+
if (hasIteration) {
|
|
926
|
+
score += 0.2;
|
|
927
|
+
reasons.push("Shows iterative refinement");
|
|
928
|
+
}
|
|
929
|
+
const solutionLength = (skill.solution || "").length;
|
|
930
|
+
if (solutionLength > 500) {
|
|
931
|
+
score += 0.2;
|
|
932
|
+
reasons.push("Detailed solution");
|
|
933
|
+
} else if (solutionLength > 200) {
|
|
934
|
+
score += 0.1;
|
|
935
|
+
}
|
|
936
|
+
const nonObviousSignals = [
|
|
937
|
+
"workaround",
|
|
938
|
+
"trick",
|
|
939
|
+
"gotcha",
|
|
940
|
+
"subtle",
|
|
941
|
+
"hidden",
|
|
942
|
+
"unexpected",
|
|
943
|
+
"actually",
|
|
944
|
+
"turns out",
|
|
945
|
+
"discovered",
|
|
946
|
+
"realized"
|
|
947
|
+
];
|
|
948
|
+
const content = `${skill.problem || ""} ${skill.solution || ""}`.toLowerCase();
|
|
949
|
+
const nonObviousCount = nonObviousSignals.filter((s) => content.includes(s)).length;
|
|
950
|
+
score += Math.min(nonObviousCount * 0.1, 0.25);
|
|
951
|
+
if (nonObviousCount > 0) {
|
|
952
|
+
reasons.push("Contains non-obvious discoveries");
|
|
953
|
+
}
|
|
954
|
+
score = Math.max(0, Math.min(1, score));
|
|
955
|
+
return {
|
|
956
|
+
gateName: gate.name,
|
|
957
|
+
passed: score >= 0.4,
|
|
958
|
+
score,
|
|
959
|
+
explanation: reasons.length > 0 ? reasons.join("; ") : score >= 0.4 ? "Skill involves non-trivial discovery" : "Skill may be too trivial (basic documentation lookup)"
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
evaluateSpecificTriggers(gate, skill) {
|
|
963
|
+
const triggers = skill.triggerConditions || [];
|
|
964
|
+
if (triggers.length === 0) {
|
|
965
|
+
return {
|
|
966
|
+
gateName: gate.name,
|
|
967
|
+
passed: false,
|
|
968
|
+
score: 0,
|
|
969
|
+
explanation: "No trigger conditions defined"
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
let score = 0;
|
|
973
|
+
const reasons = [];
|
|
974
|
+
for (const trigger of triggers) {
|
|
975
|
+
if (trigger.value && trigger.value.length > 10) {
|
|
976
|
+
score += 0.2;
|
|
977
|
+
}
|
|
978
|
+
if (trigger.description) {
|
|
979
|
+
score += 0.1;
|
|
980
|
+
}
|
|
981
|
+
if (trigger.type === "error" && trigger.value.length > 20) {
|
|
982
|
+
score += 0.15;
|
|
983
|
+
reasons.push("Has specific error trigger");
|
|
984
|
+
}
|
|
985
|
+
if (trigger.type === "pattern") {
|
|
986
|
+
score += 0.15;
|
|
987
|
+
reasons.push("Has pattern-based trigger");
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
score = Math.min(1, score);
|
|
991
|
+
return {
|
|
992
|
+
gateName: gate.name,
|
|
993
|
+
passed: score >= 0.4,
|
|
994
|
+
score,
|
|
995
|
+
explanation: reasons.length > 0 ? reasons.join("; ") : score >= 0.4 ? "Has specific trigger conditions" : "Trigger conditions need more specificity"
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
evaluateVerified(gate, trajectory) {
|
|
999
|
+
const success = trajectory.outcome?.success ?? false;
|
|
1000
|
+
const hasVerification = trajectory.turns.some((t) => {
|
|
1001
|
+
const content = t.content?.toLowerCase() || "";
|
|
1002
|
+
return content.includes("works") || content.includes("fixed") || content.includes("solved") || content.includes("success");
|
|
1003
|
+
});
|
|
1004
|
+
const score = success ? 0.7 : hasVerification ? 0.5 : 0.2;
|
|
1005
|
+
return {
|
|
1006
|
+
gateName: gate.name,
|
|
1007
|
+
passed: score >= 0.5,
|
|
1008
|
+
score,
|
|
1009
|
+
explanation: success ? "Session completed successfully" : hasVerification ? "Solution appears verified in conversation" : "Solution verification unclear"
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
evaluateCustom(gate, _skill, _trajectory, _turns) {
|
|
1013
|
+
return {
|
|
1014
|
+
gateName: gate.name,
|
|
1015
|
+
passed: true,
|
|
1016
|
+
score: 1,
|
|
1017
|
+
explanation: "Custom gate - no implementation provided"
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Add a custom quality gate
|
|
1022
|
+
*/
|
|
1023
|
+
addGate(gate) {
|
|
1024
|
+
this.gates.push(gate);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Remove a quality gate by name
|
|
1028
|
+
*/
|
|
1029
|
+
removeGate(name) {
|
|
1030
|
+
const index = this.gates.findIndex((g) => g.name === name);
|
|
1031
|
+
if (index >= 0) {
|
|
1032
|
+
this.gates.splice(index, 1);
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get all configured gates
|
|
1039
|
+
*/
|
|
1040
|
+
getGates() {
|
|
1041
|
+
return [...this.gates];
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
// src/extraction/base.ts
|
|
1046
|
+
var BaseExtractor = class {
|
|
1047
|
+
constructor(config) {
|
|
1048
|
+
this.config = {
|
|
1049
|
+
mode: config?.mode || "manual",
|
|
1050
|
+
qualityGates: config?.qualityGates || DEFAULT_QUALITY_GATES,
|
|
1051
|
+
minConfidence: config?.minConfidence ?? 0.6,
|
|
1052
|
+
llmProvider: config?.llmProvider
|
|
1053
|
+
};
|
|
1054
|
+
this.gateEvaluator = new QualityGateEvaluator(this.config.qualityGates);
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Create initial skill metrics
|
|
1058
|
+
*/
|
|
1059
|
+
createInitialMetrics() {
|
|
1060
|
+
return {
|
|
1061
|
+
usageCount: 0,
|
|
1062
|
+
successRate: 0,
|
|
1063
|
+
feedbackScores: []
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Create skill source info for extracted skills
|
|
1068
|
+
*/
|
|
1069
|
+
createSource(trajectory, type) {
|
|
1070
|
+
return {
|
|
1071
|
+
type,
|
|
1072
|
+
sessionId: trajectory.sessionId,
|
|
1073
|
+
importedAt: /* @__PURE__ */ new Date()
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Generate a skill ID from name
|
|
1078
|
+
*/
|
|
1079
|
+
generateSkillId(name) {
|
|
1080
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Validate and run quality gates on extracted skill
|
|
1084
|
+
*/
|
|
1085
|
+
async validateWithGates(skill, trajectory, turnRange) {
|
|
1086
|
+
return this.gateEvaluator.evaluate(skill, trajectory, turnRange);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Add or update quality gates
|
|
1090
|
+
*/
|
|
1091
|
+
setQualityGates(gates) {
|
|
1092
|
+
this.config.qualityGates = gates;
|
|
1093
|
+
this.gateEvaluator = new QualityGateEvaluator(gates);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Get current configuration
|
|
1097
|
+
*/
|
|
1098
|
+
getConfig() {
|
|
1099
|
+
return { ...this.config };
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// src/extraction/manual.ts
|
|
1104
|
+
var ManualExtractor = class extends BaseExtractor {
|
|
1105
|
+
constructor(config) {
|
|
1106
|
+
super({ mode: "manual", qualityGates: config });
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Extract a skill from a trajectory with manual guidance
|
|
1110
|
+
*/
|
|
1111
|
+
async extract(trajectory, options) {
|
|
1112
|
+
const turnRange = options?.turnRange || [
|
|
1113
|
+
0,
|
|
1114
|
+
trajectory.turns.length - 1
|
|
1115
|
+
];
|
|
1116
|
+
const turns = trajectory.turns.slice(turnRange[0], turnRange[1] + 1);
|
|
1117
|
+
const skill = this.buildSkillFromTurns(trajectory, turns, options);
|
|
1118
|
+
if (!options?.skipValidation) {
|
|
1119
|
+
const validation = await this.validateWithGates(skill, trajectory, turnRange);
|
|
1120
|
+
if (!validation.passed) {
|
|
1121
|
+
return [
|
|
1122
|
+
{
|
|
1123
|
+
success: false,
|
|
1124
|
+
confidence: this.calculateConfidence(validation.results),
|
|
1125
|
+
gateResults: validation.results,
|
|
1126
|
+
failureReason: "Quality gates not passed",
|
|
1127
|
+
sourceTrajectory: {
|
|
1128
|
+
sessionId: trajectory.sessionId,
|
|
1129
|
+
turnRange
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
];
|
|
1133
|
+
}
|
|
1134
|
+
return [
|
|
1135
|
+
{
|
|
1136
|
+
success: true,
|
|
1137
|
+
skill,
|
|
1138
|
+
confidence: this.calculateConfidence(validation.results),
|
|
1139
|
+
gateResults: validation.results,
|
|
1140
|
+
sourceTrajectory: {
|
|
1141
|
+
sessionId: trajectory.sessionId,
|
|
1142
|
+
turnRange
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
];
|
|
1146
|
+
}
|
|
1147
|
+
return [
|
|
1148
|
+
{
|
|
1149
|
+
success: true,
|
|
1150
|
+
skill,
|
|
1151
|
+
confidence: 0.8,
|
|
1152
|
+
gateResults: [],
|
|
1153
|
+
sourceTrajectory: {
|
|
1154
|
+
sessionId: trajectory.sessionId,
|
|
1155
|
+
turnRange
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
];
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Build a skill object from trajectory turns
|
|
1162
|
+
*/
|
|
1163
|
+
buildSkillFromTurns(trajectory, turns, options) {
|
|
1164
|
+
const firstUserTurn = turns.find((t) => t.role === "user");
|
|
1165
|
+
const problem = firstUserTurn?.content || "Problem not specified";
|
|
1166
|
+
const assistantTurns = turns.filter((t) => t.role === "assistant");
|
|
1167
|
+
const solution = this.buildSolutionFromTurns(assistantTurns);
|
|
1168
|
+
const triggerConditions = this.inferTriggerConditions(turns);
|
|
1169
|
+
const example = this.buildExampleFromTurns(turns);
|
|
1170
|
+
const name = options?.suggestedName || this.inferSkillName(problem, solution);
|
|
1171
|
+
const id = this.generateSkillId(name);
|
|
1172
|
+
const description = options?.description || this.generateDescription(problem, triggerConditions);
|
|
1173
|
+
return {
|
|
1174
|
+
id,
|
|
1175
|
+
name,
|
|
1176
|
+
version: "1.0.0",
|
|
1177
|
+
description,
|
|
1178
|
+
problem: this.truncate(problem, 1e3),
|
|
1179
|
+
triggerConditions,
|
|
1180
|
+
solution,
|
|
1181
|
+
verification: this.inferVerification(trajectory),
|
|
1182
|
+
examples: example ? [example] : [],
|
|
1183
|
+
notes: this.extractNotes(turns),
|
|
1184
|
+
author: "extracted",
|
|
1185
|
+
tags: options?.tags || this.inferTags(problem, solution),
|
|
1186
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1187
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1188
|
+
status: "draft",
|
|
1189
|
+
metrics: this.createInitialMetrics(),
|
|
1190
|
+
source: this.createSource(trajectory, "extracted")
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Build solution text from assistant turns
|
|
1195
|
+
*/
|
|
1196
|
+
buildSolutionFromTurns(assistantTurns) {
|
|
1197
|
+
const steps = [];
|
|
1198
|
+
for (let i = 0; i < assistantTurns.length; i++) {
|
|
1199
|
+
const turn = assistantTurns[i];
|
|
1200
|
+
if (turn.content && turn.content.trim()) {
|
|
1201
|
+
steps.push(turn.content.trim());
|
|
1202
|
+
}
|
|
1203
|
+
if (turn.toolCalls) {
|
|
1204
|
+
for (const call of turn.toolCalls) {
|
|
1205
|
+
const toolStep = this.formatToolCallAsStep(call);
|
|
1206
|
+
if (toolStep) {
|
|
1207
|
+
steps.push(toolStep);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return steps.filter((s) => s.length > 10).slice(0, 10).map((step, i) => `${i + 1}. ${step}`).join("\n\n");
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Format a tool call as a solution step
|
|
1216
|
+
*/
|
|
1217
|
+
formatToolCallAsStep(call) {
|
|
1218
|
+
const { name, input } = call;
|
|
1219
|
+
switch (name) {
|
|
1220
|
+
case "Read":
|
|
1221
|
+
return `Read file: \`${input.file_path || input.path}\``;
|
|
1222
|
+
case "Write":
|
|
1223
|
+
return `Create/write file: \`${input.file_path || input.path}\``;
|
|
1224
|
+
case "Edit":
|
|
1225
|
+
return `Edit file: \`${input.file_path || input.path}\``;
|
|
1226
|
+
case "Bash":
|
|
1227
|
+
return `Run command: \`${this.truncate(String(input.command || ""), 100)}\``;
|
|
1228
|
+
case "Grep":
|
|
1229
|
+
return `Search for: \`${input.pattern}\``;
|
|
1230
|
+
case "Glob":
|
|
1231
|
+
return `Find files matching: \`${input.pattern}\``;
|
|
1232
|
+
default:
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Infer trigger conditions from turns
|
|
1238
|
+
*/
|
|
1239
|
+
inferTriggerConditions(turns) {
|
|
1240
|
+
const conditions = [];
|
|
1241
|
+
for (const turn of turns) {
|
|
1242
|
+
if (turn.toolResults) {
|
|
1243
|
+
for (const result of turn.toolResults) {
|
|
1244
|
+
if (!result.success && result.error) {
|
|
1245
|
+
conditions.push({
|
|
1246
|
+
type: "error",
|
|
1247
|
+
value: this.extractErrorSignature(result.error),
|
|
1248
|
+
description: `Error from ${result.name}`
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const content = turn.content || "";
|
|
1254
|
+
const errorMatch = content.match(/error[:\s]+([^\n]+)/i);
|
|
1255
|
+
if (errorMatch) {
|
|
1256
|
+
conditions.push({
|
|
1257
|
+
type: "error",
|
|
1258
|
+
value: errorMatch[1].trim()
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
const firstUserTurn = turns.find((t) => t.role === "user");
|
|
1263
|
+
if (firstUserTurn?.content) {
|
|
1264
|
+
const keywords = this.extractKeywords(firstUserTurn.content);
|
|
1265
|
+
if (keywords.length > 0) {
|
|
1266
|
+
conditions.push({
|
|
1267
|
+
type: "keyword",
|
|
1268
|
+
value: keywords.join(", "),
|
|
1269
|
+
description: "Keywords from original problem"
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return this.deduplicateTriggers(conditions);
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Extract a representative error signature
|
|
1277
|
+
*/
|
|
1278
|
+
extractErrorSignature(error) {
|
|
1279
|
+
const lines = error.split("\n");
|
|
1280
|
+
const errorLine = lines.find(
|
|
1281
|
+
(l) => l.toLowerCase().includes("error") || l.toLowerCase().includes("exception") || l.toLowerCase().includes("failed")
|
|
1282
|
+
);
|
|
1283
|
+
return this.truncate(errorLine || lines[0] || error, 200);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Extract meaningful keywords from text
|
|
1287
|
+
*/
|
|
1288
|
+
extractKeywords(text) {
|
|
1289
|
+
const words = text.toLowerCase().split(/\W+/);
|
|
1290
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
1291
|
+
"the",
|
|
1292
|
+
"a",
|
|
1293
|
+
"an",
|
|
1294
|
+
"is",
|
|
1295
|
+
"are",
|
|
1296
|
+
"was",
|
|
1297
|
+
"were",
|
|
1298
|
+
"be",
|
|
1299
|
+
"been",
|
|
1300
|
+
"being",
|
|
1301
|
+
"have",
|
|
1302
|
+
"has",
|
|
1303
|
+
"had",
|
|
1304
|
+
"do",
|
|
1305
|
+
"does",
|
|
1306
|
+
"did",
|
|
1307
|
+
"will",
|
|
1308
|
+
"would",
|
|
1309
|
+
"could",
|
|
1310
|
+
"should",
|
|
1311
|
+
"may",
|
|
1312
|
+
"might",
|
|
1313
|
+
"can",
|
|
1314
|
+
"this",
|
|
1315
|
+
"that",
|
|
1316
|
+
"these",
|
|
1317
|
+
"those",
|
|
1318
|
+
"i",
|
|
1319
|
+
"you",
|
|
1320
|
+
"he",
|
|
1321
|
+
"she",
|
|
1322
|
+
"it",
|
|
1323
|
+
"we",
|
|
1324
|
+
"they",
|
|
1325
|
+
"what",
|
|
1326
|
+
"which",
|
|
1327
|
+
"who",
|
|
1328
|
+
"when",
|
|
1329
|
+
"where",
|
|
1330
|
+
"how",
|
|
1331
|
+
"to",
|
|
1332
|
+
"from",
|
|
1333
|
+
"in",
|
|
1334
|
+
"on",
|
|
1335
|
+
"at",
|
|
1336
|
+
"by",
|
|
1337
|
+
"for",
|
|
1338
|
+
"with",
|
|
1339
|
+
"about",
|
|
1340
|
+
"into",
|
|
1341
|
+
"through",
|
|
1342
|
+
"during",
|
|
1343
|
+
"before",
|
|
1344
|
+
"after",
|
|
1345
|
+
"above",
|
|
1346
|
+
"below",
|
|
1347
|
+
"and",
|
|
1348
|
+
"or",
|
|
1349
|
+
"but",
|
|
1350
|
+
"if",
|
|
1351
|
+
"then",
|
|
1352
|
+
"else",
|
|
1353
|
+
"so",
|
|
1354
|
+
"because",
|
|
1355
|
+
"as",
|
|
1356
|
+
"until",
|
|
1357
|
+
"while",
|
|
1358
|
+
"of",
|
|
1359
|
+
"not",
|
|
1360
|
+
"no",
|
|
1361
|
+
"just",
|
|
1362
|
+
"only"
|
|
1363
|
+
]);
|
|
1364
|
+
const keywords = words.filter((w) => w.length > 3 && !stopWords.has(w)).slice(0, 5);
|
|
1365
|
+
return [...new Set(keywords)];
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Deduplicate trigger conditions
|
|
1369
|
+
*/
|
|
1370
|
+
deduplicateTriggers(triggers) {
|
|
1371
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1372
|
+
return triggers.filter((t) => {
|
|
1373
|
+
const key = `${t.type}:${t.value}`;
|
|
1374
|
+
if (seen.has(key)) return false;
|
|
1375
|
+
seen.add(key);
|
|
1376
|
+
return true;
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Build an example from the trajectory
|
|
1381
|
+
*/
|
|
1382
|
+
buildExampleFromTurns(turns) {
|
|
1383
|
+
const firstUserTurn = turns.find((t) => t.role === "user");
|
|
1384
|
+
const lastAssistantTurn = [...turns].reverse().find((t) => t.role === "assistant");
|
|
1385
|
+
if (!firstUserTurn || !lastAssistantTurn) return null;
|
|
1386
|
+
return {
|
|
1387
|
+
scenario: "Original extraction context",
|
|
1388
|
+
before: this.truncate(firstUserTurn.content || "", 500),
|
|
1389
|
+
after: this.truncate(lastAssistantTurn.content || "", 500)
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Infer skill name from problem and solution
|
|
1394
|
+
*/
|
|
1395
|
+
inferSkillName(problem, solution) {
|
|
1396
|
+
const combined = `${problem} ${solution}`.toLowerCase();
|
|
1397
|
+
const patterns = [
|
|
1398
|
+
/fix(?:ing|ed)?\s+(\w+(?:\s+\w+)?)/i,
|
|
1399
|
+
/solv(?:e|ing|ed)\s+(\w+(?:\s+\w+)?)/i,
|
|
1400
|
+
/handl(?:e|ing|ed)\s+(\w+(?:\s+\w+)?)/i,
|
|
1401
|
+
/(\w+)\s+error/i,
|
|
1402
|
+
/(\w+)\s+issue/i,
|
|
1403
|
+
/(\w+)\s+problem/i
|
|
1404
|
+
];
|
|
1405
|
+
for (const pattern of patterns) {
|
|
1406
|
+
const match = combined.match(pattern);
|
|
1407
|
+
if (match && match[1]) {
|
|
1408
|
+
return `${match[1].trim()}-fix`;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const words = problem.split(/\s+/).slice(0, 4).join("-");
|
|
1412
|
+
return words.toLowerCase().replace(/[^a-z0-9-]/g, "") || "extracted-skill";
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Generate a description optimized for semantic matching
|
|
1416
|
+
*/
|
|
1417
|
+
generateDescription(problem, triggers) {
|
|
1418
|
+
let desc = this.truncate(problem, 150);
|
|
1419
|
+
const errorTriggers = triggers.filter((t) => t.type === "error");
|
|
1420
|
+
if (errorTriggers.length > 0) {
|
|
1421
|
+
desc += ` Triggered by: ${errorTriggers[0].value}`;
|
|
1422
|
+
}
|
|
1423
|
+
return desc;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Infer verification steps
|
|
1427
|
+
*/
|
|
1428
|
+
inferVerification(trajectory) {
|
|
1429
|
+
if (trajectory.outcome?.success) {
|
|
1430
|
+
return "Verified: Session completed successfully.";
|
|
1431
|
+
}
|
|
1432
|
+
return "Verify by checking that the original error no longer occurs.";
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Extract notes from turns (warnings, caveats, etc.)
|
|
1436
|
+
*/
|
|
1437
|
+
extractNotes(turns) {
|
|
1438
|
+
const notes = [];
|
|
1439
|
+
for (const turn of turns) {
|
|
1440
|
+
const content = turn.content || "";
|
|
1441
|
+
if (content.toLowerCase().includes("note:") || content.toLowerCase().includes("warning:") || content.toLowerCase().includes("caveat:") || content.toLowerCase().includes("important:")) {
|
|
1442
|
+
const match = content.match(/(note|warning|caveat|important):\s*([^\n]+)/i);
|
|
1443
|
+
if (match) {
|
|
1444
|
+
notes.push(match[0]);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return notes.length > 0 ? notes.join("\n") : void 0;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Infer tags from content
|
|
1452
|
+
*/
|
|
1453
|
+
inferTags(problem, solution) {
|
|
1454
|
+
const tags = [];
|
|
1455
|
+
const content = `${problem} ${solution}`.toLowerCase();
|
|
1456
|
+
const techPatterns = [
|
|
1457
|
+
[/typescript|\.ts\b/i, "typescript"],
|
|
1458
|
+
[/javascript|\.js\b/i, "javascript"],
|
|
1459
|
+
[/python|\.py\b/i, "python"],
|
|
1460
|
+
[/react/i, "react"],
|
|
1461
|
+
[/node\.?js/i, "nodejs"],
|
|
1462
|
+
[/docker/i, "docker"],
|
|
1463
|
+
[/git\b/i, "git"],
|
|
1464
|
+
[/npm|yarn|pnpm/i, "package-manager"],
|
|
1465
|
+
[/test|jest|vitest|mocha/i, "testing"],
|
|
1466
|
+
[/database|sql|postgres|mysql|mongo/i, "database"],
|
|
1467
|
+
[/api|rest|graphql/i, "api"]
|
|
1468
|
+
];
|
|
1469
|
+
for (const [pattern, tag] of techPatterns) {
|
|
1470
|
+
if (pattern.test(content)) {
|
|
1471
|
+
tags.push(tag);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (content.includes("error") || content.includes("fix")) {
|
|
1475
|
+
tags.push("debugging");
|
|
1476
|
+
}
|
|
1477
|
+
if (content.includes("performance") || content.includes("slow")) {
|
|
1478
|
+
tags.push("performance");
|
|
1479
|
+
}
|
|
1480
|
+
if (content.includes("security") || content.includes("auth")) {
|
|
1481
|
+
tags.push("security");
|
|
1482
|
+
}
|
|
1483
|
+
return [...new Set(tags)].slice(0, 5);
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Calculate confidence from gate results
|
|
1487
|
+
*/
|
|
1488
|
+
calculateConfidence(gateResults) {
|
|
1489
|
+
if (gateResults.length === 0) return 0.5;
|
|
1490
|
+
const avgScore = gateResults.reduce((sum, r) => sum + r.score, 0) / gateResults.length;
|
|
1491
|
+
return Math.round(avgScore * 100) / 100;
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Truncate text to max length
|
|
1495
|
+
*/
|
|
1496
|
+
truncate(text, maxLength) {
|
|
1497
|
+
if (text.length <= maxLength) return text;
|
|
1498
|
+
return text.substring(0, maxLength - 3) + "...";
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
// src/extraction/automatic.ts
|
|
1503
|
+
var AutomaticExtractor = class extends BaseExtractor {
|
|
1504
|
+
constructor(config) {
|
|
1505
|
+
super({
|
|
1506
|
+
mode: "automatic",
|
|
1507
|
+
qualityGates: config?.qualityGates,
|
|
1508
|
+
minConfidence: config?.minConfidence,
|
|
1509
|
+
llmProvider: config?.llmProvider
|
|
1510
|
+
});
|
|
1511
|
+
this.llmProvider = config?.llmProvider || null;
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Set the LLM provider for automatic extraction
|
|
1515
|
+
*/
|
|
1516
|
+
setLLMProvider(provider) {
|
|
1517
|
+
this.llmProvider = provider;
|
|
1518
|
+
this.config.llmProvider = provider;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Extract skills automatically from a trajectory
|
|
1522
|
+
*/
|
|
1523
|
+
async extract(trajectory, options) {
|
|
1524
|
+
if (!this.llmProvider) {
|
|
1525
|
+
return [
|
|
1526
|
+
{
|
|
1527
|
+
success: false,
|
|
1528
|
+
confidence: 0,
|
|
1529
|
+
gateResults: [],
|
|
1530
|
+
failureReason: "No LLM provider configured for automatic extraction",
|
|
1531
|
+
sourceTrajectory: {
|
|
1532
|
+
sessionId: trajectory.sessionId,
|
|
1533
|
+
turnRange: options?.turnRange || [0, trajectory.turns.length - 1]
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
];
|
|
1537
|
+
}
|
|
1538
|
+
const candidates = await this.identifySkillCandidates(trajectory, options);
|
|
1539
|
+
if (candidates.length === 0) {
|
|
1540
|
+
return [
|
|
1541
|
+
{
|
|
1542
|
+
success: false,
|
|
1543
|
+
confidence: 0,
|
|
1544
|
+
gateResults: [],
|
|
1545
|
+
failureReason: "No extractable skills identified in trajectory",
|
|
1546
|
+
sourceTrajectory: {
|
|
1547
|
+
sessionId: trajectory.sessionId,
|
|
1548
|
+
turnRange: options?.turnRange || [0, trajectory.turns.length - 1]
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
];
|
|
1552
|
+
}
|
|
1553
|
+
const results = [];
|
|
1554
|
+
const minConfidence = options?.minConfidence ?? this.config.minConfidence ?? 0.6;
|
|
1555
|
+
for (const candidate of candidates) {
|
|
1556
|
+
if (candidate.confidence < minConfidence) {
|
|
1557
|
+
results.push({
|
|
1558
|
+
success: false,
|
|
1559
|
+
confidence: candidate.confidence,
|
|
1560
|
+
gateResults: [],
|
|
1561
|
+
failureReason: `Confidence ${candidate.confidence} below threshold ${minConfidence}`,
|
|
1562
|
+
sourceTrajectory: {
|
|
1563
|
+
sessionId: trajectory.sessionId,
|
|
1564
|
+
turnRange: candidate.turnRange
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
const skill = this.buildSkillFromCandidate(candidate, trajectory);
|
|
1570
|
+
if (!options?.skipValidation) {
|
|
1571
|
+
const validation = await this.validateWithGates(
|
|
1572
|
+
skill,
|
|
1573
|
+
trajectory,
|
|
1574
|
+
candidate.turnRange
|
|
1575
|
+
);
|
|
1576
|
+
if (!validation.passed) {
|
|
1577
|
+
results.push({
|
|
1578
|
+
success: false,
|
|
1579
|
+
skill,
|
|
1580
|
+
confidence: candidate.confidence,
|
|
1581
|
+
gateResults: validation.results,
|
|
1582
|
+
failureReason: "Quality gates not passed",
|
|
1583
|
+
sourceTrajectory: {
|
|
1584
|
+
sessionId: trajectory.sessionId,
|
|
1585
|
+
turnRange: candidate.turnRange
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
results.push({
|
|
1591
|
+
success: true,
|
|
1592
|
+
skill,
|
|
1593
|
+
confidence: candidate.confidence,
|
|
1594
|
+
gateResults: validation.results,
|
|
1595
|
+
sourceTrajectory: {
|
|
1596
|
+
sessionId: trajectory.sessionId,
|
|
1597
|
+
turnRange: candidate.turnRange
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
} else {
|
|
1601
|
+
results.push({
|
|
1602
|
+
success: true,
|
|
1603
|
+
skill,
|
|
1604
|
+
confidence: candidate.confidence,
|
|
1605
|
+
gateResults: [],
|
|
1606
|
+
sourceTrajectory: {
|
|
1607
|
+
sessionId: trajectory.sessionId,
|
|
1608
|
+
turnRange: candidate.turnRange
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return results;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Identify skill candidates using LLM analysis
|
|
1617
|
+
*/
|
|
1618
|
+
async identifySkillCandidates(trajectory, options) {
|
|
1619
|
+
const prompt = this.buildAnalysisPrompt(trajectory, options);
|
|
1620
|
+
try {
|
|
1621
|
+
const response = await this.llmProvider.completeStructured(prompt, SKILL_CANDIDATE_SCHEMA, { temperature: 0.3 });
|
|
1622
|
+
return response.candidates || [];
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
try {
|
|
1625
|
+
const textResponse = await this.llmProvider.complete(prompt, {
|
|
1626
|
+
temperature: 0.3
|
|
1627
|
+
});
|
|
1628
|
+
return this.parseTextResponse(textResponse);
|
|
1629
|
+
} catch {
|
|
1630
|
+
return [];
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Build the analysis prompt for skill identification
|
|
1636
|
+
*/
|
|
1637
|
+
buildAnalysisPrompt(trajectory, options) {
|
|
1638
|
+
const turns = options?.turnRange ? trajectory.turns.slice(options.turnRange[0], options.turnRange[1] + 1) : trajectory.turns;
|
|
1639
|
+
const trajectoryText = this.formatTrajectoryForPrompt(turns);
|
|
1640
|
+
return `Analyze the following agent conversation trajectory and identify any reusable skills that could be extracted.
|
|
1641
|
+
|
|
1642
|
+
A skill is extractable if it meets these criteria:
|
|
1643
|
+
1. **Reusability**: The solution applies to multiple future tasks, not just this specific case
|
|
1644
|
+
2. **Non-trivial**: It involves discovery beyond basic documentation lookup
|
|
1645
|
+
3. **Specific triggers**: Has clear conditions for when to apply (error messages, patterns, contexts)
|
|
1646
|
+
4. **Verified**: The solution demonstrably worked in this trajectory
|
|
1647
|
+
|
|
1648
|
+
For each skill you identify, provide:
|
|
1649
|
+
- name: A descriptive kebab-case name
|
|
1650
|
+
- description: 1-2 sentences optimized for semantic search/matching
|
|
1651
|
+
- problem: What problem does this skill solve?
|
|
1652
|
+
- solution: Step-by-step instructions to apply this skill
|
|
1653
|
+
- triggerConditions: Array of {type, value, description} for when to use this skill
|
|
1654
|
+
- verification: How to verify the skill worked
|
|
1655
|
+
- turnRange: [startTurn, endTurn] indices in the trajectory
|
|
1656
|
+
- confidence: 0-1 score for how extractable this skill is
|
|
1657
|
+
- reasoning: Why this is or isn't a good skill candidate
|
|
1658
|
+
|
|
1659
|
+
If no skills are extractable, return an empty candidates array.
|
|
1660
|
+
|
|
1661
|
+
TRAJECTORY:
|
|
1662
|
+
${trajectoryText}
|
|
1663
|
+
|
|
1664
|
+
${options?.context ? `ADDITIONAL CONTEXT: ${options.context}` : ""}
|
|
1665
|
+
|
|
1666
|
+
Respond with a JSON object containing a "candidates" array.`;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Format trajectory turns for the prompt
|
|
1670
|
+
*/
|
|
1671
|
+
formatTrajectoryForPrompt(turns) {
|
|
1672
|
+
return turns.map((turn, i) => {
|
|
1673
|
+
let text = `[Turn ${i}] ${turn.role.toUpperCase()}:
|
|
1674
|
+
${turn.content || "(no content)"}`;
|
|
1675
|
+
if (turn.toolCalls && turn.toolCalls.length > 0) {
|
|
1676
|
+
text += "\nTool calls:";
|
|
1677
|
+
for (const call of turn.toolCalls) {
|
|
1678
|
+
text += `
|
|
1679
|
+
- ${call.name}: ${JSON.stringify(call.input).substring(0, 200)}`;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (turn.toolResults && turn.toolResults.length > 0) {
|
|
1683
|
+
text += "\nTool results:";
|
|
1684
|
+
for (const result of turn.toolResults) {
|
|
1685
|
+
const status = result.success ? "SUCCESS" : "ERROR";
|
|
1686
|
+
text += `
|
|
1687
|
+
- ${result.name} [${status}]: ${result.output.substring(0, 200)}`;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return text;
|
|
1691
|
+
}).join("\n\n---\n\n");
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Parse text response into skill candidates (fallback)
|
|
1695
|
+
*/
|
|
1696
|
+
parseTextResponse(response) {
|
|
1697
|
+
try {
|
|
1698
|
+
const jsonMatch = response.match(/\{[\s\S]*"candidates"[\s\S]*\}/);
|
|
1699
|
+
if (jsonMatch) {
|
|
1700
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1701
|
+
return parsed.candidates || [];
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
return [];
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Build a Skill from a candidate
|
|
1709
|
+
*/
|
|
1710
|
+
buildSkillFromCandidate(candidate, trajectory) {
|
|
1711
|
+
const id = candidate.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1712
|
+
const turns = trajectory.turns.slice(candidate.turnRange[0], candidate.turnRange[1] + 1);
|
|
1713
|
+
const example = this.buildExample(turns);
|
|
1714
|
+
return {
|
|
1715
|
+
id,
|
|
1716
|
+
name: candidate.name,
|
|
1717
|
+
version: "1.0.0",
|
|
1718
|
+
description: candidate.description,
|
|
1719
|
+
problem: candidate.problem,
|
|
1720
|
+
triggerConditions: candidate.triggerConditions,
|
|
1721
|
+
solution: candidate.solution,
|
|
1722
|
+
verification: candidate.verification,
|
|
1723
|
+
examples: example ? [example] : [],
|
|
1724
|
+
author: "auto-extracted",
|
|
1725
|
+
tags: this.inferTags(candidate),
|
|
1726
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1727
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1728
|
+
status: "draft",
|
|
1729
|
+
metrics: this.createInitialMetrics(),
|
|
1730
|
+
source: {
|
|
1731
|
+
type: "extracted",
|
|
1732
|
+
sessionId: trajectory.sessionId,
|
|
1733
|
+
importedAt: /* @__PURE__ */ new Date()
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Build an example from turns
|
|
1739
|
+
*/
|
|
1740
|
+
buildExample(turns) {
|
|
1741
|
+
const firstUser = turns.find((t) => t.role === "user");
|
|
1742
|
+
const lastAssistant = [...turns].reverse().find((t) => t.role === "assistant");
|
|
1743
|
+
if (!firstUser || !lastAssistant) return null;
|
|
1744
|
+
return {
|
|
1745
|
+
scenario: "Extracted from conversation",
|
|
1746
|
+
before: (firstUser.content || "").substring(0, 500),
|
|
1747
|
+
after: (lastAssistant.content || "").substring(0, 500)
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Infer tags from candidate
|
|
1752
|
+
*/
|
|
1753
|
+
inferTags(candidate) {
|
|
1754
|
+
const tags = [];
|
|
1755
|
+
const content = `${candidate.problem} ${candidate.solution}`.toLowerCase();
|
|
1756
|
+
const techPatterns = [
|
|
1757
|
+
[/typescript/i, "typescript"],
|
|
1758
|
+
[/javascript/i, "javascript"],
|
|
1759
|
+
[/python/i, "python"],
|
|
1760
|
+
[/react/i, "react"],
|
|
1761
|
+
[/node/i, "nodejs"],
|
|
1762
|
+
[/docker/i, "docker"],
|
|
1763
|
+
[/git\b/i, "git"],
|
|
1764
|
+
[/test/i, "testing"],
|
|
1765
|
+
[/error/i, "debugging"]
|
|
1766
|
+
];
|
|
1767
|
+
for (const [pattern, tag] of techPatterns) {
|
|
1768
|
+
if (pattern.test(content)) {
|
|
1769
|
+
tags.push(tag);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return [...new Set(tags)];
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
var SKILL_CANDIDATE_SCHEMA = {
|
|
1776
|
+
type: "object",
|
|
1777
|
+
properties: {
|
|
1778
|
+
candidates: {
|
|
1779
|
+
type: "array",
|
|
1780
|
+
items: {
|
|
1781
|
+
type: "object",
|
|
1782
|
+
properties: {
|
|
1783
|
+
name: { type: "string" },
|
|
1784
|
+
description: { type: "string" },
|
|
1785
|
+
problem: { type: "string" },
|
|
1786
|
+
solution: { type: "string" },
|
|
1787
|
+
triggerConditions: {
|
|
1788
|
+
type: "array",
|
|
1789
|
+
items: {
|
|
1790
|
+
type: "object",
|
|
1791
|
+
properties: {
|
|
1792
|
+
type: { type: "string", enum: ["error", "pattern", "context", "keyword", "custom"] },
|
|
1793
|
+
value: { type: "string" },
|
|
1794
|
+
description: { type: "string" }
|
|
1795
|
+
},
|
|
1796
|
+
required: ["type", "value"]
|
|
1797
|
+
}
|
|
1798
|
+
},
|
|
1799
|
+
verification: { type: "string" },
|
|
1800
|
+
turnRange: {
|
|
1801
|
+
type: "array",
|
|
1802
|
+
items: { type: "number" },
|
|
1803
|
+
minItems: 2,
|
|
1804
|
+
maxItems: 2
|
|
1805
|
+
},
|
|
1806
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
1807
|
+
reasoning: { type: "string" }
|
|
1808
|
+
},
|
|
1809
|
+
required: ["name", "description", "problem", "solution", "turnRange", "confidence"]
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
},
|
|
1813
|
+
required: ["candidates"]
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
// src/storage/base.ts
|
|
1817
|
+
var BaseStorageAdapter = class {
|
|
1818
|
+
constructor() {
|
|
1819
|
+
this.initialized = false;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Ensure storage is initialized before operations
|
|
1823
|
+
*/
|
|
1824
|
+
ensureInitialized() {
|
|
1825
|
+
if (!this.initialized) {
|
|
1826
|
+
throw new Error("Storage not initialized. Call initialize() first.");
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Apply filters to a list of skills
|
|
1831
|
+
*/
|
|
1832
|
+
applyFilter(skills, filter) {
|
|
1833
|
+
if (!filter) return skills;
|
|
1834
|
+
return skills.filter((skill) => {
|
|
1835
|
+
if (filter.status && filter.status.length > 0) {
|
|
1836
|
+
if (!filter.status.includes(skill.status)) return false;
|
|
1837
|
+
}
|
|
1838
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
1839
|
+
const hasTag = filter.tags.some((tag) => skill.tags.includes(tag));
|
|
1840
|
+
if (!hasTag) return false;
|
|
1841
|
+
}
|
|
1842
|
+
if (filter.author && skill.author !== filter.author) {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
if (filter.minSuccessRate !== void 0 && skill.metrics.successRate < filter.minSuccessRate) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
if (filter.createdAfter && skill.createdAt < filter.createdAfter) {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
if (filter.createdBefore && skill.createdAt > filter.createdBefore) {
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
return true;
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Simple text search across skill fields
|
|
1859
|
+
*/
|
|
1860
|
+
textSearch(skills, query) {
|
|
1861
|
+
const lowerQuery = query.toLowerCase();
|
|
1862
|
+
const terms = lowerQuery.split(/\s+/).filter((t) => t.length > 0);
|
|
1863
|
+
return skills.filter((skill) => {
|
|
1864
|
+
const searchText = [
|
|
1865
|
+
skill.name,
|
|
1866
|
+
skill.description,
|
|
1867
|
+
skill.problem,
|
|
1868
|
+
skill.solution,
|
|
1869
|
+
...skill.tags,
|
|
1870
|
+
skill.triggerConditions.map((t) => t.value).join(" ")
|
|
1871
|
+
].join(" ").toLowerCase();
|
|
1872
|
+
return terms.every((term) => searchText.includes(term));
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
var MemoryStorageAdapter = class extends BaseStorageAdapter {
|
|
1877
|
+
constructor() {
|
|
1878
|
+
super(...arguments);
|
|
1879
|
+
this.skills = /* @__PURE__ */ new Map();
|
|
1880
|
+
// skillId -> version -> skill
|
|
1881
|
+
this.lineages = /* @__PURE__ */ new Map();
|
|
1882
|
+
}
|
|
1883
|
+
async initialize() {
|
|
1884
|
+
this.initialized = true;
|
|
1885
|
+
}
|
|
1886
|
+
async saveSkill(skill) {
|
|
1887
|
+
this.ensureInitialized();
|
|
1888
|
+
if (!this.skills.has(skill.id)) {
|
|
1889
|
+
this.skills.set(skill.id, /* @__PURE__ */ new Map());
|
|
1890
|
+
}
|
|
1891
|
+
const versionMap = this.skills.get(skill.id);
|
|
1892
|
+
versionMap.set(skill.version, { ...skill });
|
|
1893
|
+
this.updateLineage(skill);
|
|
1894
|
+
}
|
|
1895
|
+
async getSkill(id, version) {
|
|
1896
|
+
this.ensureInitialized();
|
|
1897
|
+
const versionMap = this.skills.get(id);
|
|
1898
|
+
if (!versionMap) return null;
|
|
1899
|
+
if (version) {
|
|
1900
|
+
return versionMap.get(version) || null;
|
|
1901
|
+
}
|
|
1902
|
+
const versions = Array.from(versionMap.keys()).sort(this.compareVersions);
|
|
1903
|
+
const latestVersion = versions[versions.length - 1];
|
|
1904
|
+
return latestVersion ? versionMap.get(latestVersion) || null : null;
|
|
1905
|
+
}
|
|
1906
|
+
async listSkills(filter) {
|
|
1907
|
+
this.ensureInitialized();
|
|
1908
|
+
const skills = [];
|
|
1909
|
+
for (const versionMap of this.skills.values()) {
|
|
1910
|
+
const versions = Array.from(versionMap.keys()).sort(this.compareVersions);
|
|
1911
|
+
const latestVersion = versions[versions.length - 1];
|
|
1912
|
+
if (latestVersion) {
|
|
1913
|
+
const skill = versionMap.get(latestVersion);
|
|
1914
|
+
if (skill) skills.push(skill);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return this.applyFilter(skills, filter);
|
|
1918
|
+
}
|
|
1919
|
+
async deleteSkill(id, version) {
|
|
1920
|
+
this.ensureInitialized();
|
|
1921
|
+
if (version) {
|
|
1922
|
+
const versionMap = this.skills.get(id);
|
|
1923
|
+
if (!versionMap) return false;
|
|
1924
|
+
return versionMap.delete(version);
|
|
1925
|
+
}
|
|
1926
|
+
const existed = this.skills.has(id);
|
|
1927
|
+
this.skills.delete(id);
|
|
1928
|
+
this.lineages.delete(id);
|
|
1929
|
+
return existed;
|
|
1930
|
+
}
|
|
1931
|
+
async getVersionHistory(skillId) {
|
|
1932
|
+
this.ensureInitialized();
|
|
1933
|
+
const versionMap = this.skills.get(skillId);
|
|
1934
|
+
if (!versionMap) return [];
|
|
1935
|
+
const versions = [];
|
|
1936
|
+
for (const [version, skill] of versionMap) {
|
|
1937
|
+
versions.push({
|
|
1938
|
+
skillId,
|
|
1939
|
+
version,
|
|
1940
|
+
skill,
|
|
1941
|
+
changelog: "",
|
|
1942
|
+
// Not tracked in memory
|
|
1943
|
+
createdAt: skill.createdAt,
|
|
1944
|
+
contentHash: this.hashSkill(skill)
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
return versions.sort((a, b) => this.compareVersions(a.version, b.version));
|
|
1948
|
+
}
|
|
1949
|
+
async getLineage(skillId) {
|
|
1950
|
+
this.ensureInitialized();
|
|
1951
|
+
return this.lineages.get(skillId) || null;
|
|
1952
|
+
}
|
|
1953
|
+
async searchSkills(query) {
|
|
1954
|
+
this.ensureInitialized();
|
|
1955
|
+
const allSkills = await this.listSkills();
|
|
1956
|
+
return this.textSearch(allSkills, query);
|
|
1957
|
+
}
|
|
1958
|
+
updateLineage(skill) {
|
|
1959
|
+
if (!this.lineages.has(skill.id)) {
|
|
1960
|
+
this.lineages.set(skill.id, {
|
|
1961
|
+
rootId: skill.id,
|
|
1962
|
+
versions: [],
|
|
1963
|
+
forks: []
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
const lineage = this.lineages.get(skill.id);
|
|
1967
|
+
const existingIndex = lineage.versions.findIndex((v) => v.version === skill.version);
|
|
1968
|
+
const versionEntry = {
|
|
1969
|
+
skillId: skill.id,
|
|
1970
|
+
version: skill.version,
|
|
1971
|
+
skill,
|
|
1972
|
+
changelog: "",
|
|
1973
|
+
createdAt: skill.createdAt,
|
|
1974
|
+
contentHash: this.hashSkill(skill)
|
|
1975
|
+
};
|
|
1976
|
+
if (existingIndex >= 0) {
|
|
1977
|
+
lineage.versions[existingIndex] = versionEntry;
|
|
1978
|
+
} else {
|
|
1979
|
+
lineage.versions.push(versionEntry);
|
|
1980
|
+
lineage.versions.sort((a, b) => this.compareVersions(a.version, b.version));
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
compareVersions(a, b) {
|
|
1984
|
+
const partsA = a.split(".").map(Number);
|
|
1985
|
+
const partsB = b.split(".").map(Number);
|
|
1986
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
1987
|
+
const numA = partsA[i] || 0;
|
|
1988
|
+
const numB = partsB[i] || 0;
|
|
1989
|
+
if (numA !== numB) return numA - numB;
|
|
1990
|
+
}
|
|
1991
|
+
return 0;
|
|
1992
|
+
}
|
|
1993
|
+
hashSkill(skill) {
|
|
1994
|
+
const content = JSON.stringify({
|
|
1995
|
+
problem: skill.problem,
|
|
1996
|
+
solution: skill.solution,
|
|
1997
|
+
triggerConditions: skill.triggerConditions
|
|
1998
|
+
});
|
|
1999
|
+
let hash = 0;
|
|
2000
|
+
for (let i = 0; i < content.length; i++) {
|
|
2001
|
+
const char = content.charCodeAt(i);
|
|
2002
|
+
hash = (hash << 5) - hash + char;
|
|
2003
|
+
hash = hash & hash;
|
|
2004
|
+
}
|
|
2005
|
+
return Math.abs(hash).toString(16);
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Clear all stored skills (for testing)
|
|
2009
|
+
*/
|
|
2010
|
+
clear() {
|
|
2011
|
+
this.skills.clear();
|
|
2012
|
+
this.lineages.clear();
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
// src/storage/filesystem.ts
|
|
2017
|
+
import * as fs from "fs/promises";
|
|
2018
|
+
import * as path from "path";
|
|
2019
|
+
var FilesystemStorageAdapter = class extends BaseStorageAdapter {
|
|
2020
|
+
constructor(config) {
|
|
2021
|
+
super();
|
|
2022
|
+
this.config = {
|
|
2023
|
+
basePath: config.basePath,
|
|
2024
|
+
openSkillsCompatible: config.openSkillsCompatible ?? true,
|
|
2025
|
+
trackVersions: config.trackVersions ?? true
|
|
2026
|
+
};
|
|
2027
|
+
this.versionsDir = path.join(this.config.basePath, ".versions");
|
|
2028
|
+
}
|
|
2029
|
+
async initialize() {
|
|
2030
|
+
await fs.mkdir(this.config.basePath, { recursive: true });
|
|
2031
|
+
if (this.config.trackVersions) {
|
|
2032
|
+
await fs.mkdir(this.versionsDir, { recursive: true });
|
|
2033
|
+
}
|
|
2034
|
+
this.initialized = true;
|
|
2035
|
+
}
|
|
2036
|
+
async saveSkill(skill) {
|
|
2037
|
+
this.ensureInitialized();
|
|
2038
|
+
const skillDir = path.join(this.config.basePath, skill.id);
|
|
2039
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
2040
|
+
const skillContent = this.serializeSkill(skill);
|
|
2041
|
+
const skillFileName = this.config.openSkillsCompatible ? "SKILL.md" : "skill.md";
|
|
2042
|
+
await fs.writeFile(path.join(skillDir, skillFileName), skillContent, "utf-8");
|
|
2043
|
+
const metadata = this.createMetadata(skill);
|
|
2044
|
+
await fs.writeFile(
|
|
2045
|
+
path.join(skillDir, ".skilltree.json"),
|
|
2046
|
+
JSON.stringify(metadata, null, 2),
|
|
2047
|
+
"utf-8"
|
|
2048
|
+
);
|
|
2049
|
+
if (this.config.trackVersions) {
|
|
2050
|
+
await this.saveVersionSnapshot(skill);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
async getSkill(id, version) {
|
|
2054
|
+
this.ensureInitialized();
|
|
2055
|
+
if (version && this.config.trackVersions) {
|
|
2056
|
+
return this.getVersionedSkill(id, version);
|
|
2057
|
+
}
|
|
2058
|
+
const skillDir = path.join(this.config.basePath, id);
|
|
2059
|
+
const skillFileName = this.config.openSkillsCompatible ? "SKILL.md" : "skill.md";
|
|
2060
|
+
const skillPath = path.join(skillDir, skillFileName);
|
|
2061
|
+
try {
|
|
2062
|
+
const content = await fs.readFile(skillPath, "utf-8");
|
|
2063
|
+
const metadata = await this.loadMetadata(skillDir);
|
|
2064
|
+
return this.parseSkill(id, content, metadata);
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
if (error.code === "ENOENT") {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
throw error;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
async listSkills(filter) {
|
|
2073
|
+
this.ensureInitialized();
|
|
2074
|
+
const skills = [];
|
|
2075
|
+
try {
|
|
2076
|
+
const entries = await fs.readdir(this.config.basePath, { withFileTypes: true });
|
|
2077
|
+
for (const entry of entries) {
|
|
2078
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
const skill = await this.getSkill(entry.name);
|
|
2082
|
+
if (skill) {
|
|
2083
|
+
skills.push(skill);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
if (error.code !== "ENOENT") {
|
|
2088
|
+
throw error;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return this.applyFilter(skills, filter);
|
|
2092
|
+
}
|
|
2093
|
+
async deleteSkill(id, version) {
|
|
2094
|
+
this.ensureInitialized();
|
|
2095
|
+
if (version && this.config.trackVersions) {
|
|
2096
|
+
return this.deleteVersion(id, version);
|
|
2097
|
+
}
|
|
2098
|
+
const skillDir = path.join(this.config.basePath, id);
|
|
2099
|
+
try {
|
|
2100
|
+
await fs.rm(skillDir, { recursive: true });
|
|
2101
|
+
if (this.config.trackVersions) {
|
|
2102
|
+
const versionDir = path.join(this.versionsDir, id);
|
|
2103
|
+
await fs.rm(versionDir, { recursive: true }).catch(() => {
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
return true;
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
if (error.code === "ENOENT") {
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
throw error;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
async getVersionHistory(skillId) {
|
|
2115
|
+
this.ensureInitialized();
|
|
2116
|
+
if (!this.config.trackVersions) {
|
|
2117
|
+
const skill = await this.getSkill(skillId);
|
|
2118
|
+
if (!skill) return [];
|
|
2119
|
+
return [
|
|
2120
|
+
{
|
|
2121
|
+
skillId,
|
|
2122
|
+
version: skill.version,
|
|
2123
|
+
skill,
|
|
2124
|
+
changelog: "",
|
|
2125
|
+
createdAt: skill.createdAt,
|
|
2126
|
+
contentHash: this.hashContent(skill)
|
|
2127
|
+
}
|
|
2128
|
+
];
|
|
2129
|
+
}
|
|
2130
|
+
const versionDir = path.join(this.versionsDir, skillId);
|
|
2131
|
+
const versions = [];
|
|
2132
|
+
try {
|
|
2133
|
+
const entries = await fs.readdir(versionDir);
|
|
2134
|
+
for (const entry of entries) {
|
|
2135
|
+
if (!entry.endsWith(".json")) continue;
|
|
2136
|
+
const versionPath = path.join(versionDir, entry);
|
|
2137
|
+
const content = await fs.readFile(versionPath, "utf-8");
|
|
2138
|
+
const versionData = JSON.parse(content);
|
|
2139
|
+
versionData.createdAt = new Date(versionData.createdAt);
|
|
2140
|
+
if (versionData.skill) {
|
|
2141
|
+
const skill = versionData.skill;
|
|
2142
|
+
skill.createdAt = new Date(skill.createdAt);
|
|
2143
|
+
skill.updatedAt = new Date(skill.updatedAt);
|
|
2144
|
+
}
|
|
2145
|
+
versions.push(versionData);
|
|
2146
|
+
}
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
if (error.code !== "ENOENT") {
|
|
2149
|
+
throw error;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return versions.sort((a, b) => this.compareVersions(a.version, b.version));
|
|
2153
|
+
}
|
|
2154
|
+
async getLineage(skillId) {
|
|
2155
|
+
this.ensureInitialized();
|
|
2156
|
+
const versions = await this.getVersionHistory(skillId);
|
|
2157
|
+
if (versions.length === 0) return null;
|
|
2158
|
+
const skillDir = path.join(this.config.basePath, skillId);
|
|
2159
|
+
const metadata = await this.loadMetadata(skillDir);
|
|
2160
|
+
return {
|
|
2161
|
+
rootId: metadata?.lineage?.rootId || skillId,
|
|
2162
|
+
versions,
|
|
2163
|
+
forks: metadata?.lineage?.forks || []
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
async searchSkills(query) {
|
|
2167
|
+
this.ensureInitialized();
|
|
2168
|
+
const allSkills = await this.listSkills();
|
|
2169
|
+
return this.textSearch(allSkills, query);
|
|
2170
|
+
}
|
|
2171
|
+
// ==========================================================================
|
|
2172
|
+
// Serialization
|
|
2173
|
+
// ==========================================================================
|
|
2174
|
+
/**
|
|
2175
|
+
* Serialize skill to YAML frontmatter + Markdown format
|
|
2176
|
+
*/
|
|
2177
|
+
serializeSkill(skill) {
|
|
2178
|
+
const frontmatter = this.buildFrontmatter(skill);
|
|
2179
|
+
const body = this.buildBody(skill);
|
|
2180
|
+
return `---
|
|
2181
|
+
${frontmatter}---
|
|
2182
|
+
|
|
2183
|
+
${body}`;
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Build YAML frontmatter
|
|
2187
|
+
*/
|
|
2188
|
+
buildFrontmatter(skill) {
|
|
2189
|
+
const lines = [];
|
|
2190
|
+
lines.push(`name: ${skill.name}`);
|
|
2191
|
+
lines.push(`description: |`);
|
|
2192
|
+
lines.push(` ${skill.description}`);
|
|
2193
|
+
lines.push(`version: ${skill.version}`);
|
|
2194
|
+
lines.push(`author: ${skill.author}`);
|
|
2195
|
+
lines.push(`status: ${skill.status}`);
|
|
2196
|
+
lines.push(`date: ${skill.updatedAt.toISOString().split("T")[0]}`);
|
|
2197
|
+
if (skill.tags.length > 0) {
|
|
2198
|
+
lines.push(`tags:`);
|
|
2199
|
+
for (const tag of skill.tags) {
|
|
2200
|
+
lines.push(` - ${tag}`);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if (skill.parentVersion) {
|
|
2204
|
+
lines.push(`parentVersion: ${skill.parentVersion}`);
|
|
2205
|
+
}
|
|
2206
|
+
if (skill.derivedFrom && skill.derivedFrom.length > 0) {
|
|
2207
|
+
lines.push(`derivedFrom:`);
|
|
2208
|
+
for (const id of skill.derivedFrom) {
|
|
2209
|
+
lines.push(` - ${id}`);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return lines.join("\n") + "\n";
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Build Markdown body (Claudeception-inspired structure)
|
|
2216
|
+
*/
|
|
2217
|
+
buildBody(skill) {
|
|
2218
|
+
const sections = [];
|
|
2219
|
+
sections.push("## Problem\n");
|
|
2220
|
+
sections.push(skill.problem);
|
|
2221
|
+
sections.push("");
|
|
2222
|
+
if (skill.triggerConditions.length > 0) {
|
|
2223
|
+
sections.push("## Trigger Conditions\n");
|
|
2224
|
+
for (const trigger of skill.triggerConditions) {
|
|
2225
|
+
const desc = trigger.description ? ` - ${trigger.description}` : "";
|
|
2226
|
+
sections.push(`- **${trigger.type}**: \`${trigger.value}\`${desc}`);
|
|
2227
|
+
}
|
|
2228
|
+
sections.push("");
|
|
2229
|
+
}
|
|
2230
|
+
sections.push("## Solution\n");
|
|
2231
|
+
sections.push(skill.solution);
|
|
2232
|
+
sections.push("");
|
|
2233
|
+
sections.push("## Verification\n");
|
|
2234
|
+
sections.push(skill.verification);
|
|
2235
|
+
sections.push("");
|
|
2236
|
+
if (skill.examples.length > 0) {
|
|
2237
|
+
sections.push("## Examples\n");
|
|
2238
|
+
for (const example of skill.examples) {
|
|
2239
|
+
sections.push(`### ${example.scenario}
|
|
2240
|
+
`);
|
|
2241
|
+
sections.push("**Before:**");
|
|
2242
|
+
sections.push("```");
|
|
2243
|
+
sections.push(example.before);
|
|
2244
|
+
sections.push("```\n");
|
|
2245
|
+
sections.push("**After:**");
|
|
2246
|
+
sections.push("```");
|
|
2247
|
+
sections.push(example.after);
|
|
2248
|
+
sections.push("```");
|
|
2249
|
+
sections.push("");
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
if (skill.notes) {
|
|
2253
|
+
sections.push("## Notes\n");
|
|
2254
|
+
sections.push(skill.notes);
|
|
2255
|
+
sections.push("");
|
|
2256
|
+
}
|
|
2257
|
+
return sections.join("\n");
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Parse skill from YAML frontmatter + Markdown content
|
|
2261
|
+
*/
|
|
2262
|
+
parseSkill(id, content, metadata) {
|
|
2263
|
+
const { frontmatter, body } = this.parseFrontmatterAndBody(content);
|
|
2264
|
+
const sections = this.parseBodySections(body);
|
|
2265
|
+
const name = this.extractYamlField(frontmatter, "name") || id;
|
|
2266
|
+
const description = this.extractYamlMultiline(frontmatter, "description") || "";
|
|
2267
|
+
const version = this.extractYamlField(frontmatter, "version") || "1.0.0";
|
|
2268
|
+
const author = this.extractYamlField(frontmatter, "author") || "unknown";
|
|
2269
|
+
const status = this.extractYamlField(frontmatter, "status") || "active";
|
|
2270
|
+
const dateStr = this.extractYamlField(frontmatter, "date");
|
|
2271
|
+
const tags = this.extractYamlList(frontmatter, "tags");
|
|
2272
|
+
const parentVersion = this.extractYamlField(frontmatter, "parentVersion");
|
|
2273
|
+
const derivedFrom = this.extractYamlList(frontmatter, "derivedFrom");
|
|
2274
|
+
const problem = sections["problem"] || "";
|
|
2275
|
+
const solution = sections["solution"] || "";
|
|
2276
|
+
const verification = sections["verification"] || "";
|
|
2277
|
+
const notes = sections["notes"];
|
|
2278
|
+
const triggerConditions = this.parseTriggerConditions(sections["trigger conditions"] || "");
|
|
2279
|
+
const examples = this.parseExamples(sections["examples"] || "");
|
|
2280
|
+
const date = dateStr ? new Date(dateStr) : /* @__PURE__ */ new Date();
|
|
2281
|
+
return {
|
|
2282
|
+
id,
|
|
2283
|
+
name,
|
|
2284
|
+
version,
|
|
2285
|
+
description,
|
|
2286
|
+
problem,
|
|
2287
|
+
triggerConditions,
|
|
2288
|
+
solution,
|
|
2289
|
+
verification,
|
|
2290
|
+
examples,
|
|
2291
|
+
notes,
|
|
2292
|
+
author,
|
|
2293
|
+
tags,
|
|
2294
|
+
createdAt: date,
|
|
2295
|
+
updatedAt: date,
|
|
2296
|
+
status,
|
|
2297
|
+
parentVersion: parentVersion || void 0,
|
|
2298
|
+
derivedFrom: derivedFrom.length > 0 ? derivedFrom : void 0,
|
|
2299
|
+
metrics: {
|
|
2300
|
+
usageCount: 0,
|
|
2301
|
+
successRate: 0,
|
|
2302
|
+
feedbackScores: []
|
|
2303
|
+
},
|
|
2304
|
+
source: metadata?.source
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Split content into frontmatter and body
|
|
2309
|
+
*/
|
|
2310
|
+
parseFrontmatterAndBody(content) {
|
|
2311
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
2312
|
+
if (match) {
|
|
2313
|
+
return { frontmatter: match[1], body: match[2] };
|
|
2314
|
+
}
|
|
2315
|
+
return { frontmatter: "", body: content };
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Parse body into sections by ## headers
|
|
2319
|
+
*/
|
|
2320
|
+
parseBodySections(body) {
|
|
2321
|
+
const sections = {};
|
|
2322
|
+
const parts = body.split(/^## /m);
|
|
2323
|
+
for (const part of parts) {
|
|
2324
|
+
if (!part.trim()) continue;
|
|
2325
|
+
const lines = part.split("\n");
|
|
2326
|
+
const header = lines[0]?.trim().toLowerCase();
|
|
2327
|
+
const content = lines.slice(1).join("\n").trim();
|
|
2328
|
+
if (header) {
|
|
2329
|
+
sections[header] = content;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return sections;
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Extract a single-line YAML field
|
|
2336
|
+
*/
|
|
2337
|
+
extractYamlField(yaml, field) {
|
|
2338
|
+
const match = yaml.match(new RegExp(`^${field}:\\s*(.+)$`, "m"));
|
|
2339
|
+
return match ? match[1].trim() : null;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Extract a multiline YAML field (using |)
|
|
2343
|
+
*/
|
|
2344
|
+
extractYamlMultiline(yaml, field) {
|
|
2345
|
+
const match = yaml.match(new RegExp(`^${field}:\\s*\\|\\n((?:\\s{2}.+\\n?)+)`, "m"));
|
|
2346
|
+
if (match) {
|
|
2347
|
+
return match[1].split("\n").map((line) => line.replace(/^\s{2}/, "")).join("\n").trim();
|
|
2348
|
+
}
|
|
2349
|
+
return this.extractYamlField(yaml, field);
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Extract a YAML list
|
|
2353
|
+
*/
|
|
2354
|
+
extractYamlList(yaml, field) {
|
|
2355
|
+
const match = yaml.match(new RegExp(`^${field}:\\n((?:\\s+-\\s+.+\\n?)+)`, "m"));
|
|
2356
|
+
if (match) {
|
|
2357
|
+
return match[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter((line) => line.length > 0);
|
|
2358
|
+
}
|
|
2359
|
+
return [];
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Parse trigger conditions from markdown
|
|
2363
|
+
*/
|
|
2364
|
+
parseTriggerConditions(content) {
|
|
2365
|
+
const conditions = [];
|
|
2366
|
+
const lines = content.split("\n");
|
|
2367
|
+
for (const line of lines) {
|
|
2368
|
+
const match = line.match(/^-\s+\*\*(\w+)\*\*:\s*`([^`]+)`(?:\s*-\s*(.+))?/);
|
|
2369
|
+
if (match) {
|
|
2370
|
+
conditions.push({
|
|
2371
|
+
type: match[1],
|
|
2372
|
+
value: match[2],
|
|
2373
|
+
description: match[3]?.trim()
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return conditions;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Parse examples from markdown
|
|
2381
|
+
*/
|
|
2382
|
+
parseExamples(content) {
|
|
2383
|
+
const examples = [];
|
|
2384
|
+
const parts = content.split(/^### /m);
|
|
2385
|
+
for (const part of parts) {
|
|
2386
|
+
if (!part.trim()) continue;
|
|
2387
|
+
const lines = part.split("\n");
|
|
2388
|
+
const scenario = lines[0]?.trim() || "Example";
|
|
2389
|
+
const rest = lines.slice(1).join("\n");
|
|
2390
|
+
const beforeMatch = rest.match(/\*\*Before:\*\*\n```\n?([\s\S]*?)```/);
|
|
2391
|
+
const afterMatch = rest.match(/\*\*After:\*\*\n```\n?([\s\S]*?)```/);
|
|
2392
|
+
if (beforeMatch || afterMatch) {
|
|
2393
|
+
examples.push({
|
|
2394
|
+
scenario,
|
|
2395
|
+
before: beforeMatch?.[1]?.trim() || "",
|
|
2396
|
+
after: afterMatch?.[1]?.trim() || ""
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return examples;
|
|
2401
|
+
}
|
|
2402
|
+
// ==========================================================================
|
|
2403
|
+
// Version Management
|
|
2404
|
+
// ==========================================================================
|
|
2405
|
+
/**
|
|
2406
|
+
* Save a version snapshot
|
|
2407
|
+
*/
|
|
2408
|
+
async saveVersionSnapshot(skill) {
|
|
2409
|
+
const versionDir = path.join(this.versionsDir, skill.id);
|
|
2410
|
+
await fs.mkdir(versionDir, { recursive: true });
|
|
2411
|
+
const versionFile = path.join(versionDir, `${skill.version}.json`);
|
|
2412
|
+
const versionData = {
|
|
2413
|
+
skillId: skill.id,
|
|
2414
|
+
version: skill.version,
|
|
2415
|
+
skill,
|
|
2416
|
+
changelog: "",
|
|
2417
|
+
// Could be enhanced to track changes
|
|
2418
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2419
|
+
contentHash: this.hashContent(skill)
|
|
2420
|
+
};
|
|
2421
|
+
await fs.writeFile(versionFile, JSON.stringify(versionData, null, 2), "utf-8");
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Get a specific version of a skill
|
|
2425
|
+
*/
|
|
2426
|
+
async getVersionedSkill(id, version) {
|
|
2427
|
+
const versionFile = path.join(this.versionsDir, id, `${version}.json`);
|
|
2428
|
+
try {
|
|
2429
|
+
const content = await fs.readFile(versionFile, "utf-8");
|
|
2430
|
+
const versionData = JSON.parse(content);
|
|
2431
|
+
const skill = versionData.skill;
|
|
2432
|
+
skill.createdAt = new Date(skill.createdAt);
|
|
2433
|
+
skill.updatedAt = new Date(skill.updatedAt);
|
|
2434
|
+
return skill;
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
if (error.code === "ENOENT") {
|
|
2437
|
+
return null;
|
|
2438
|
+
}
|
|
2439
|
+
throw error;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Delete a specific version
|
|
2444
|
+
*/
|
|
2445
|
+
async deleteVersion(id, version) {
|
|
2446
|
+
const versionFile = path.join(this.versionsDir, id, `${version}.json`);
|
|
2447
|
+
try {
|
|
2448
|
+
await fs.unlink(versionFile);
|
|
2449
|
+
return true;
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
if (error.code === "ENOENT") {
|
|
2452
|
+
return false;
|
|
2453
|
+
}
|
|
2454
|
+
throw error;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
// ==========================================================================
|
|
2458
|
+
// Metadata
|
|
2459
|
+
// ==========================================================================
|
|
2460
|
+
/**
|
|
2461
|
+
* Create metadata for a skill
|
|
2462
|
+
*/
|
|
2463
|
+
createMetadata(skill) {
|
|
2464
|
+
return {
|
|
2465
|
+
source: skill.source,
|
|
2466
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2467
|
+
lineage: {
|
|
2468
|
+
rootId: skill.id,
|
|
2469
|
+
parentVersion: skill.parentVersion,
|
|
2470
|
+
derivedFrom: skill.derivedFrom
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* Load metadata for a skill directory
|
|
2476
|
+
*/
|
|
2477
|
+
async loadMetadata(skillDir) {
|
|
2478
|
+
const metadataPath = path.join(skillDir, ".skilltree.json");
|
|
2479
|
+
try {
|
|
2480
|
+
const content = await fs.readFile(metadataPath, "utf-8");
|
|
2481
|
+
return JSON.parse(content);
|
|
2482
|
+
} catch {
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
// ==========================================================================
|
|
2487
|
+
// Utilities
|
|
2488
|
+
// ==========================================================================
|
|
2489
|
+
/**
|
|
2490
|
+
* Compare semantic versions
|
|
2491
|
+
*/
|
|
2492
|
+
compareVersions(a, b) {
|
|
2493
|
+
const partsA = a.split(".").map(Number);
|
|
2494
|
+
const partsB = b.split(".").map(Number);
|
|
2495
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
2496
|
+
const numA = partsA[i] || 0;
|
|
2497
|
+
const numB = partsB[i] || 0;
|
|
2498
|
+
if (numA !== numB) return numA - numB;
|
|
2499
|
+
}
|
|
2500
|
+
return 0;
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Generate content hash
|
|
2504
|
+
*/
|
|
2505
|
+
hashContent(skill) {
|
|
2506
|
+
const content = JSON.stringify({
|
|
2507
|
+
problem: skill.problem,
|
|
2508
|
+
solution: skill.solution,
|
|
2509
|
+
triggerConditions: skill.triggerConditions
|
|
2510
|
+
});
|
|
2511
|
+
let hash = 0;
|
|
2512
|
+
for (let i = 0; i < content.length; i++) {
|
|
2513
|
+
const char = content.charCodeAt(i);
|
|
2514
|
+
hash = (hash << 5) - hash + char;
|
|
2515
|
+
hash = hash & hash;
|
|
2516
|
+
}
|
|
2517
|
+
return Math.abs(hash).toString(16);
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
// src/versioning/semver.ts
|
|
2522
|
+
function parseVersion(version) {
|
|
2523
|
+
const match = version.match(
|
|
2524
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/
|
|
2525
|
+
);
|
|
2526
|
+
if (!match) {
|
|
2527
|
+
throw new Error(`Invalid version format: ${version}`);
|
|
2528
|
+
}
|
|
2529
|
+
return {
|
|
2530
|
+
major: parseInt(match[1], 10),
|
|
2531
|
+
minor: parseInt(match[2], 10),
|
|
2532
|
+
patch: parseInt(match[3], 10),
|
|
2533
|
+
prerelease: match[4],
|
|
2534
|
+
build: match[5]
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
function formatVersion(parsed) {
|
|
2538
|
+
let version = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
2539
|
+
if (parsed.prerelease) {
|
|
2540
|
+
version += `-${parsed.prerelease}`;
|
|
2541
|
+
}
|
|
2542
|
+
if (parsed.build) {
|
|
2543
|
+
version += `+${parsed.build}`;
|
|
2544
|
+
}
|
|
2545
|
+
return version;
|
|
2546
|
+
}
|
|
2547
|
+
function isValidVersion(version) {
|
|
2548
|
+
try {
|
|
2549
|
+
parseVersion(version);
|
|
2550
|
+
return true;
|
|
2551
|
+
} catch {
|
|
2552
|
+
return false;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
function compareVersions(a, b) {
|
|
2556
|
+
const parsedA = parseVersion(a);
|
|
2557
|
+
const parsedB = parseVersion(b);
|
|
2558
|
+
if (parsedA.major !== parsedB.major) {
|
|
2559
|
+
return parsedA.major < parsedB.major ? -1 : 1;
|
|
2560
|
+
}
|
|
2561
|
+
if (parsedA.minor !== parsedB.minor) {
|
|
2562
|
+
return parsedA.minor < parsedB.minor ? -1 : 1;
|
|
2563
|
+
}
|
|
2564
|
+
if (parsedA.patch !== parsedB.patch) {
|
|
2565
|
+
return parsedA.patch < parsedB.patch ? -1 : 1;
|
|
2566
|
+
}
|
|
2567
|
+
if (!parsedA.prerelease && parsedB.prerelease) return 1;
|
|
2568
|
+
if (parsedA.prerelease && !parsedB.prerelease) return -1;
|
|
2569
|
+
if (parsedA.prerelease && parsedB.prerelease) {
|
|
2570
|
+
return parsedA.prerelease < parsedB.prerelease ? -1 : parsedA.prerelease > parsedB.prerelease ? 1 : 0;
|
|
2571
|
+
}
|
|
2572
|
+
return 0;
|
|
2573
|
+
}
|
|
2574
|
+
function bumpVersion(version, type) {
|
|
2575
|
+
const parsed = parseVersion(version);
|
|
2576
|
+
switch (type) {
|
|
2577
|
+
case "major":
|
|
2578
|
+
return formatVersion({
|
|
2579
|
+
major: parsed.major + 1,
|
|
2580
|
+
minor: 0,
|
|
2581
|
+
patch: 0
|
|
2582
|
+
});
|
|
2583
|
+
case "minor":
|
|
2584
|
+
return formatVersion({
|
|
2585
|
+
major: parsed.major,
|
|
2586
|
+
minor: parsed.minor + 1,
|
|
2587
|
+
patch: 0
|
|
2588
|
+
});
|
|
2589
|
+
case "patch":
|
|
2590
|
+
return formatVersion({
|
|
2591
|
+
major: parsed.major,
|
|
2592
|
+
minor: parsed.minor,
|
|
2593
|
+
patch: parsed.patch + 1
|
|
2594
|
+
});
|
|
2595
|
+
case "prerelease":
|
|
2596
|
+
if (parsed.prerelease) {
|
|
2597
|
+
const match = parsed.prerelease.match(/^(.+?)(\d+)$/);
|
|
2598
|
+
if (match) {
|
|
2599
|
+
return formatVersion({
|
|
2600
|
+
...parsed,
|
|
2601
|
+
prerelease: `${match[1]}${parseInt(match[2], 10) + 1}`
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
return formatVersion({
|
|
2605
|
+
...parsed,
|
|
2606
|
+
prerelease: `${parsed.prerelease}.1`
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
return formatVersion({
|
|
2610
|
+
...parsed,
|
|
2611
|
+
prerelease: "alpha.1"
|
|
2612
|
+
});
|
|
2613
|
+
default:
|
|
2614
|
+
throw new Error(`Unknown bump type: ${type}`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function satisfiesRange(version, range) {
|
|
2618
|
+
const parsed = parseVersion(version);
|
|
2619
|
+
if (isValidVersion(range)) {
|
|
2620
|
+
return compareVersions(version, range) === 0;
|
|
2621
|
+
}
|
|
2622
|
+
if (range.startsWith("^")) {
|
|
2623
|
+
const rangeVersion = parseVersion(range.slice(1));
|
|
2624
|
+
if (parsed.major !== rangeVersion.major) return false;
|
|
2625
|
+
if (rangeVersion.major === 0) {
|
|
2626
|
+
if (parsed.minor !== rangeVersion.minor) return false;
|
|
2627
|
+
if (rangeVersion.minor === 0) {
|
|
2628
|
+
return parsed.patch >= rangeVersion.patch;
|
|
2629
|
+
}
|
|
2630
|
+
return parsed.patch >= rangeVersion.patch;
|
|
2631
|
+
}
|
|
2632
|
+
return compareVersions(version, range.slice(1)) >= 0;
|
|
2633
|
+
}
|
|
2634
|
+
if (range.startsWith("~")) {
|
|
2635
|
+
const rangeVersion = parseVersion(range.slice(1));
|
|
2636
|
+
return parsed.major === rangeVersion.major && parsed.minor === rangeVersion.minor && parsed.patch >= rangeVersion.patch;
|
|
2637
|
+
}
|
|
2638
|
+
if (range.startsWith(">=")) {
|
|
2639
|
+
return compareVersions(version, range.slice(2)) >= 0;
|
|
2640
|
+
}
|
|
2641
|
+
if (range.startsWith(">")) {
|
|
2642
|
+
return compareVersions(version, range.slice(1)) > 0;
|
|
2643
|
+
}
|
|
2644
|
+
if (range.startsWith("<=")) {
|
|
2645
|
+
return compareVersions(version, range.slice(2)) <= 0;
|
|
2646
|
+
}
|
|
2647
|
+
if (range.startsWith("<")) {
|
|
2648
|
+
return compareVersions(version, range.slice(1)) < 0;
|
|
2649
|
+
}
|
|
2650
|
+
if (range.includes("x") || range.includes("*")) {
|
|
2651
|
+
const parts = range.split(".");
|
|
2652
|
+
if (parts[0] === "x" || parts[0] === "*") return true;
|
|
2653
|
+
if (parsed.major !== parseInt(parts[0], 10)) return false;
|
|
2654
|
+
if (parts[1] === "x" || parts[1] === "*" || parts.length === 1) return true;
|
|
2655
|
+
if (parsed.minor !== parseInt(parts[1], 10)) return false;
|
|
2656
|
+
if (parts[2] === "x" || parts[2] === "*" || parts.length === 2) return true;
|
|
2657
|
+
return parsed.patch === parseInt(parts[2], 10);
|
|
2658
|
+
}
|
|
2659
|
+
return false;
|
|
2660
|
+
}
|
|
2661
|
+
function getLatestVersion(versions) {
|
|
2662
|
+
if (versions.length === 0) return null;
|
|
2663
|
+
return versions.reduce((latest, current) => {
|
|
2664
|
+
return compareVersions(current, latest) > 0 ? current : latest;
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
function sortVersions(versions) {
|
|
2668
|
+
return [...versions].sort((a, b) => compareVersions(a, b));
|
|
2669
|
+
}
|
|
2670
|
+
function inferBumpType(changes) {
|
|
2671
|
+
if (changes.breakingChanges && changes.breakingChanges.length > 0) {
|
|
2672
|
+
return "major";
|
|
2673
|
+
}
|
|
2674
|
+
if (changes.newFeatures && changes.newFeatures.length > 0) {
|
|
2675
|
+
return "minor";
|
|
2676
|
+
}
|
|
2677
|
+
return "patch";
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// src/versioning/lineage.ts
|
|
2681
|
+
var LineageTracker = class {
|
|
2682
|
+
constructor(storage) {
|
|
2683
|
+
this.storage = storage;
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Create a new version of a skill
|
|
2687
|
+
*/
|
|
2688
|
+
async createVersion(skillId, updates, options = {}) {
|
|
2689
|
+
const currentSkill = await this.storage.getSkill(skillId);
|
|
2690
|
+
if (!currentSkill) {
|
|
2691
|
+
throw new Error(`Skill not found: ${skillId}`);
|
|
2692
|
+
}
|
|
2693
|
+
const bumpType = options.bumpType || inferBumpType(options.changes || {});
|
|
2694
|
+
const newVersion = bumpVersion(currentSkill.version, bumpType);
|
|
2695
|
+
const updatedSkill = {
|
|
2696
|
+
...currentSkill,
|
|
2697
|
+
...updates,
|
|
2698
|
+
id: skillId,
|
|
2699
|
+
version: newVersion,
|
|
2700
|
+
parentVersion: currentSkill.version,
|
|
2701
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2702
|
+
};
|
|
2703
|
+
await this.storage.saveSkill(updatedSkill);
|
|
2704
|
+
return updatedSkill;
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Fork a skill to create a new, related skill
|
|
2708
|
+
*/
|
|
2709
|
+
async forkSkill(skillId, options) {
|
|
2710
|
+
const sourceSkill = await this.storage.getSkill(skillId, options.fromVersion);
|
|
2711
|
+
if (!sourceSkill) {
|
|
2712
|
+
throw new Error(`Skill not found: ${skillId}${options.fromVersion ? `@${options.fromVersion}` : ""}`);
|
|
2713
|
+
}
|
|
2714
|
+
const forkedSkill = {
|
|
2715
|
+
...sourceSkill,
|
|
2716
|
+
id: options.newId,
|
|
2717
|
+
name: options.newName || `${sourceSkill.name}-fork`,
|
|
2718
|
+
version: "1.0.0",
|
|
2719
|
+
parentVersion: void 0,
|
|
2720
|
+
derivedFrom: [skillId],
|
|
2721
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2722
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2723
|
+
status: "draft",
|
|
2724
|
+
metrics: {
|
|
2725
|
+
usageCount: 0,
|
|
2726
|
+
successRate: 0,
|
|
2727
|
+
feedbackScores: []
|
|
2728
|
+
},
|
|
2729
|
+
source: {
|
|
2730
|
+
type: "composed",
|
|
2731
|
+
importedAt: /* @__PURE__ */ new Date()
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
await this.storage.saveSkill(forkedSkill);
|
|
2735
|
+
return forkedSkill;
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Merge changes from one skill into another
|
|
2739
|
+
*/
|
|
2740
|
+
async mergeSkill(targetId, sourceId, options = {}) {
|
|
2741
|
+
const targetSkill = await this.storage.getSkill(targetId);
|
|
2742
|
+
const sourceSkill = await this.storage.getSkill(sourceId);
|
|
2743
|
+
if (!targetSkill) throw new Error(`Target skill not found: ${targetId}`);
|
|
2744
|
+
if (!sourceSkill) throw new Error(`Source skill not found: ${sourceId}`);
|
|
2745
|
+
const fields = options.fields || [
|
|
2746
|
+
"solution",
|
|
2747
|
+
"triggerConditions",
|
|
2748
|
+
"examples",
|
|
2749
|
+
"notes"
|
|
2750
|
+
];
|
|
2751
|
+
const strategy = options.conflictStrategy || "combine";
|
|
2752
|
+
const updates = {};
|
|
2753
|
+
for (const field of fields) {
|
|
2754
|
+
if (field === "triggerConditions") {
|
|
2755
|
+
updates.triggerConditions = this.mergeTriggerConditions(
|
|
2756
|
+
targetSkill.triggerConditions,
|
|
2757
|
+
sourceSkill.triggerConditions
|
|
2758
|
+
);
|
|
2759
|
+
} else if (field === "examples") {
|
|
2760
|
+
updates.examples = this.mergeExamples(
|
|
2761
|
+
targetSkill.examples,
|
|
2762
|
+
sourceSkill.examples
|
|
2763
|
+
);
|
|
2764
|
+
} else if (field === "tags") {
|
|
2765
|
+
updates.tags = [.../* @__PURE__ */ new Set([...targetSkill.tags, ...sourceSkill.tags])];
|
|
2766
|
+
} else if (field === "solution" || field === "notes") {
|
|
2767
|
+
updates[field] = this.mergeText(
|
|
2768
|
+
targetSkill[field] || "",
|
|
2769
|
+
sourceSkill[field] || "",
|
|
2770
|
+
strategy
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
updates.derivedFrom = [
|
|
2775
|
+
...targetSkill.derivedFrom || [],
|
|
2776
|
+
sourceId
|
|
2777
|
+
].filter((id, i, arr) => arr.indexOf(id) === i);
|
|
2778
|
+
return this.createVersion(targetId, updates, {
|
|
2779
|
+
bumpType: "minor",
|
|
2780
|
+
changelog: options.changelog || `Merged from ${sourceId}`,
|
|
2781
|
+
changes: { newFeatures: [`Merged content from ${sourceId}`] }
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Get the full lineage tree for a skill
|
|
2786
|
+
*/
|
|
2787
|
+
async getLineageTree(skillId) {
|
|
2788
|
+
const lineage = await this.storage.getLineage(skillId);
|
|
2789
|
+
if (!lineage) return null;
|
|
2790
|
+
const tree = {
|
|
2791
|
+
skillId,
|
|
2792
|
+
versions: lineage.versions,
|
|
2793
|
+
forks: [],
|
|
2794
|
+
ancestors: []
|
|
2795
|
+
};
|
|
2796
|
+
for (const fork of lineage.forks) {
|
|
2797
|
+
const forkedSkill = await this.storage.getSkill(fork.forkedSkillId);
|
|
2798
|
+
if (forkedSkill) {
|
|
2799
|
+
tree.forks.push({
|
|
2800
|
+
skill: forkedSkill,
|
|
2801
|
+
fork
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
const currentSkill = await this.storage.getSkill(skillId);
|
|
2806
|
+
if (currentSkill?.derivedFrom) {
|
|
2807
|
+
for (const ancestorId of currentSkill.derivedFrom) {
|
|
2808
|
+
const ancestor = await this.storage.getSkill(ancestorId);
|
|
2809
|
+
if (ancestor) {
|
|
2810
|
+
tree.ancestors.push(ancestor);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return tree;
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Rollback a skill to a previous version
|
|
2818
|
+
*/
|
|
2819
|
+
async rollback(skillId, toVersion) {
|
|
2820
|
+
const targetSkill = await this.storage.getSkill(skillId, toVersion);
|
|
2821
|
+
if (!targetSkill) {
|
|
2822
|
+
throw new Error(`Version not found: ${skillId}@${toVersion}`);
|
|
2823
|
+
}
|
|
2824
|
+
const currentSkill = await this.storage.getSkill(skillId);
|
|
2825
|
+
if (!currentSkill) {
|
|
2826
|
+
throw new Error(`Skill not found: ${skillId}`);
|
|
2827
|
+
}
|
|
2828
|
+
const newVersion = bumpVersion(currentSkill.version, "patch");
|
|
2829
|
+
const rolledBackSkill = {
|
|
2830
|
+
...targetSkill,
|
|
2831
|
+
version: newVersion,
|
|
2832
|
+
parentVersion: currentSkill.version,
|
|
2833
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2834
|
+
};
|
|
2835
|
+
await this.storage.saveSkill(rolledBackSkill);
|
|
2836
|
+
return rolledBackSkill;
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Compare two versions of a skill
|
|
2840
|
+
*/
|
|
2841
|
+
async compareVersions(skillId, versionA, versionB) {
|
|
2842
|
+
const skillA = await this.storage.getSkill(skillId, versionA);
|
|
2843
|
+
const skillB = await this.storage.getSkill(skillId, versionB);
|
|
2844
|
+
if (!skillA) throw new Error(`Version not found: ${skillId}@${versionA}`);
|
|
2845
|
+
if (!skillB) throw new Error(`Version not found: ${skillId}@${versionB}`);
|
|
2846
|
+
return {
|
|
2847
|
+
versionA,
|
|
2848
|
+
versionB,
|
|
2849
|
+
changes: this.diffSkills(skillA, skillB)
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Get suggested next version based on changes
|
|
2854
|
+
*/
|
|
2855
|
+
async suggestNextVersion(skillId, changes) {
|
|
2856
|
+
const skill = await this.storage.getSkill(skillId);
|
|
2857
|
+
if (!skill) throw new Error(`Skill not found: ${skillId}`);
|
|
2858
|
+
const bumpType = inferBumpType(changes);
|
|
2859
|
+
return bumpVersion(skill.version, bumpType);
|
|
2860
|
+
}
|
|
2861
|
+
// ==========================================================================
|
|
2862
|
+
// Private helpers
|
|
2863
|
+
// ==========================================================================
|
|
2864
|
+
mergeTriggerConditions(target, source) {
|
|
2865
|
+
const seen = new Set(target.map((t) => `${t.type}:${t.value}`));
|
|
2866
|
+
const merged = [...target];
|
|
2867
|
+
for (const trigger of source) {
|
|
2868
|
+
const key = `${trigger.type}:${trigger.value}`;
|
|
2869
|
+
if (!seen.has(key)) {
|
|
2870
|
+
merged.push(trigger);
|
|
2871
|
+
seen.add(key);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
return merged;
|
|
2875
|
+
}
|
|
2876
|
+
mergeExamples(target, source) {
|
|
2877
|
+
const seen = new Set(target.map((e) => e.scenario));
|
|
2878
|
+
const merged = [...target];
|
|
2879
|
+
for (const example of source) {
|
|
2880
|
+
if (!seen.has(example.scenario)) {
|
|
2881
|
+
merged.push(example);
|
|
2882
|
+
seen.add(example.scenario);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
return merged;
|
|
2886
|
+
}
|
|
2887
|
+
mergeText(target, source, strategy) {
|
|
2888
|
+
switch (strategy) {
|
|
2889
|
+
case "source":
|
|
2890
|
+
return source;
|
|
2891
|
+
case "target":
|
|
2892
|
+
return target;
|
|
2893
|
+
case "combine":
|
|
2894
|
+
if (!target) return source;
|
|
2895
|
+
if (!source) return target;
|
|
2896
|
+
return `${target}
|
|
2897
|
+
|
|
2898
|
+
---
|
|
2899
|
+
|
|
2900
|
+
${source}`;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
diffSkills(skillA, skillB) {
|
|
2904
|
+
const changes = {
|
|
2905
|
+
modified: [],
|
|
2906
|
+
added: [],
|
|
2907
|
+
removed: []
|
|
2908
|
+
};
|
|
2909
|
+
const textFields = [
|
|
2910
|
+
"name",
|
|
2911
|
+
"description",
|
|
2912
|
+
"problem",
|
|
2913
|
+
"solution",
|
|
2914
|
+
"verification",
|
|
2915
|
+
"notes"
|
|
2916
|
+
];
|
|
2917
|
+
for (const field of textFields) {
|
|
2918
|
+
const valueA = skillA[field];
|
|
2919
|
+
const valueB = skillB[field];
|
|
2920
|
+
if (valueA !== valueB) {
|
|
2921
|
+
changes.modified.push({
|
|
2922
|
+
field,
|
|
2923
|
+
oldValue: valueA,
|
|
2924
|
+
newValue: valueB
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
const triggersA = new Set(skillA.triggerConditions.map((t) => `${t.type}:${t.value}`));
|
|
2929
|
+
const triggersB = new Set(skillB.triggerConditions.map((t) => `${t.type}:${t.value}`));
|
|
2930
|
+
for (const trigger of triggersB) {
|
|
2931
|
+
if (!triggersA.has(trigger)) {
|
|
2932
|
+
changes.added.push({ type: "triggerCondition", value: trigger });
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
for (const trigger of triggersA) {
|
|
2936
|
+
if (!triggersB.has(trigger)) {
|
|
2937
|
+
changes.removed.push({ type: "triggerCondition", value: trigger });
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
const tagsA = new Set(skillA.tags);
|
|
2941
|
+
const tagsB = new Set(skillB.tags);
|
|
2942
|
+
for (const tag of tagsB) {
|
|
2943
|
+
if (!tagsA.has(tag)) {
|
|
2944
|
+
changes.added.push({ type: "tag", value: tag });
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
for (const tag of tagsA) {
|
|
2948
|
+
if (!tagsB.has(tag)) {
|
|
2949
|
+
changes.removed.push({ type: "tag", value: tag });
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
return changes;
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
|
|
2956
|
+
// src/matching/embeddings.ts
|
|
2957
|
+
var SimpleHashEmbedding = class {
|
|
2958
|
+
constructor() {
|
|
2959
|
+
this.name = "simple-hash";
|
|
2960
|
+
this.dimensions = 64;
|
|
2961
|
+
}
|
|
2962
|
+
async embed(text) {
|
|
2963
|
+
return this.hashToVector(text);
|
|
2964
|
+
}
|
|
2965
|
+
async embedBatch(texts) {
|
|
2966
|
+
return texts.map((text) => this.hashToVector(text));
|
|
2967
|
+
}
|
|
2968
|
+
hashToVector(text) {
|
|
2969
|
+
const normalized = text.toLowerCase().trim();
|
|
2970
|
+
const vector = new Array(this.dimensions).fill(0);
|
|
2971
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
2972
|
+
const charCode = normalized.charCodeAt(i);
|
|
2973
|
+
const idx = (charCode + i) % this.dimensions;
|
|
2974
|
+
vector[idx] += Math.sin(charCode * (i + 1)) * 0.1;
|
|
2975
|
+
}
|
|
2976
|
+
const words = normalized.split(/\s+/);
|
|
2977
|
+
for (let i = 0; i < words.length; i++) {
|
|
2978
|
+
const word = words[i];
|
|
2979
|
+
const hash = this.simpleHash(word);
|
|
2980
|
+
const idx = hash % this.dimensions;
|
|
2981
|
+
vector[idx] += 0.5;
|
|
2982
|
+
}
|
|
2983
|
+
return this.normalize(vector);
|
|
2984
|
+
}
|
|
2985
|
+
simpleHash(str) {
|
|
2986
|
+
let hash = 0;
|
|
2987
|
+
for (let i = 0; i < str.length; i++) {
|
|
2988
|
+
const char = str.charCodeAt(i);
|
|
2989
|
+
hash = (hash << 5) - hash + char;
|
|
2990
|
+
hash = hash & hash;
|
|
2991
|
+
}
|
|
2992
|
+
return Math.abs(hash);
|
|
2993
|
+
}
|
|
2994
|
+
normalize(vector) {
|
|
2995
|
+
const magnitude = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
|
|
2996
|
+
if (magnitude === 0) return vector;
|
|
2997
|
+
return vector.map((v) => v / magnitude);
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
var OpenAIEmbedding = class {
|
|
3001
|
+
constructor(options) {
|
|
3002
|
+
this.name = "openai";
|
|
3003
|
+
this.dimensions = 1536;
|
|
3004
|
+
if (!options.apiKey) {
|
|
3005
|
+
throw new Error("OpenAI API key is required");
|
|
3006
|
+
}
|
|
3007
|
+
this.apiKey = options.apiKey;
|
|
3008
|
+
this.model = options.model || "text-embedding-3-small";
|
|
3009
|
+
this.baseUrl = options.baseUrl || "https://api.openai.com/v1";
|
|
3010
|
+
if (this.model === "text-embedding-3-large") {
|
|
3011
|
+
this.dimensions = 3072;
|
|
3012
|
+
} else if (this.model === "text-embedding-ada-002") {
|
|
3013
|
+
this.dimensions = 1536;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
async embed(text) {
|
|
3017
|
+
const embeddings = await this.embedBatch([text]);
|
|
3018
|
+
return embeddings[0];
|
|
3019
|
+
}
|
|
3020
|
+
async embedBatch(texts) {
|
|
3021
|
+
const response = await fetch(`${this.baseUrl}/embeddings`, {
|
|
3022
|
+
method: "POST",
|
|
3023
|
+
headers: {
|
|
3024
|
+
"Content-Type": "application/json",
|
|
3025
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
3026
|
+
},
|
|
3027
|
+
body: JSON.stringify({
|
|
3028
|
+
model: this.model,
|
|
3029
|
+
input: texts
|
|
3030
|
+
})
|
|
3031
|
+
});
|
|
3032
|
+
if (!response.ok) {
|
|
3033
|
+
const error = await response.text();
|
|
3034
|
+
throw new Error(`OpenAI API error: ${error}`);
|
|
3035
|
+
}
|
|
3036
|
+
const data = await response.json();
|
|
3037
|
+
return data.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
|
|
3038
|
+
}
|
|
3039
|
+
};
|
|
3040
|
+
var VoyageEmbedding = class {
|
|
3041
|
+
constructor(options) {
|
|
3042
|
+
this.name = "voyage";
|
|
3043
|
+
this.dimensions = 1024;
|
|
3044
|
+
if (!options.apiKey) {
|
|
3045
|
+
throw new Error("Voyage API key is required");
|
|
3046
|
+
}
|
|
3047
|
+
this.apiKey = options.apiKey;
|
|
3048
|
+
this.model = options.model || "voyage-2";
|
|
3049
|
+
}
|
|
3050
|
+
async embed(text) {
|
|
3051
|
+
const embeddings = await this.embedBatch([text]);
|
|
3052
|
+
return embeddings[0];
|
|
3053
|
+
}
|
|
3054
|
+
async embedBatch(texts) {
|
|
3055
|
+
const response = await fetch("https://api.voyageai.com/v1/embeddings", {
|
|
3056
|
+
method: "POST",
|
|
3057
|
+
headers: {
|
|
3058
|
+
"Content-Type": "application/json",
|
|
3059
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
3060
|
+
},
|
|
3061
|
+
body: JSON.stringify({
|
|
3062
|
+
model: this.model,
|
|
3063
|
+
input: texts
|
|
3064
|
+
})
|
|
3065
|
+
});
|
|
3066
|
+
if (!response.ok) {
|
|
3067
|
+
const error = await response.text();
|
|
3068
|
+
throw new Error(`Voyage API error: ${error}`);
|
|
3069
|
+
}
|
|
3070
|
+
const data = await response.json();
|
|
3071
|
+
return data.data.map((item) => item.embedding);
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
function cosineSimilarity(a, b) {
|
|
3075
|
+
if (a.length !== b.length) {
|
|
3076
|
+
throw new Error("Vectors must have the same length");
|
|
3077
|
+
}
|
|
3078
|
+
let dotProduct = 0;
|
|
3079
|
+
let normA = 0;
|
|
3080
|
+
let normB = 0;
|
|
3081
|
+
for (let i = 0; i < a.length; i++) {
|
|
3082
|
+
dotProduct += a[i] * b[i];
|
|
3083
|
+
normA += a[i] * a[i];
|
|
3084
|
+
normB += b[i] * b[i];
|
|
3085
|
+
}
|
|
3086
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
3087
|
+
if (magnitude === 0) return 0;
|
|
3088
|
+
return dotProduct / magnitude;
|
|
3089
|
+
}
|
|
3090
|
+
function euclideanDistance(a, b) {
|
|
3091
|
+
if (a.length !== b.length) {
|
|
3092
|
+
throw new Error("Vectors must have the same length");
|
|
3093
|
+
}
|
|
3094
|
+
let sum = 0;
|
|
3095
|
+
for (let i = 0; i < a.length; i++) {
|
|
3096
|
+
const diff = a[i] - b[i];
|
|
3097
|
+
sum += diff * diff;
|
|
3098
|
+
}
|
|
3099
|
+
return Math.sqrt(sum);
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// src/matching/matcher.ts
|
|
3103
|
+
var SemanticMatcher = class {
|
|
3104
|
+
constructor(config = {}) {
|
|
3105
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
3106
|
+
this.provider = config.embeddingProvider || new SimpleHashEmbedding();
|
|
3107
|
+
this.config = {
|
|
3108
|
+
embeddingProvider: this.provider,
|
|
3109
|
+
similarityThreshold: config.similarityThreshold ?? 0.5,
|
|
3110
|
+
maxResults: config.maxResults ?? 10,
|
|
3111
|
+
cacheEmbeddings: config.cacheEmbeddings ?? true,
|
|
3112
|
+
embeddingFields: config.embeddingFields ?? [
|
|
3113
|
+
"name",
|
|
3114
|
+
"description",
|
|
3115
|
+
"problem",
|
|
3116
|
+
"triggerConditions",
|
|
3117
|
+
"tags"
|
|
3118
|
+
]
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Find skills matching a query string
|
|
3123
|
+
*/
|
|
3124
|
+
async findMatches(query, skills, options) {
|
|
3125
|
+
const threshold = options?.threshold ?? this.config.similarityThreshold;
|
|
3126
|
+
const maxResults = options?.maxResults ?? this.config.maxResults;
|
|
3127
|
+
const queryEmbedding = await this.provider.embed(query);
|
|
3128
|
+
const results = [];
|
|
3129
|
+
for (const skill of skills) {
|
|
3130
|
+
const skillEmbedding = await this.getSkillEmbedding(skill);
|
|
3131
|
+
const similarity = cosineSimilarity(queryEmbedding, skillEmbedding);
|
|
3132
|
+
if (similarity >= threshold) {
|
|
3133
|
+
results.push({
|
|
3134
|
+
skill,
|
|
3135
|
+
similarity,
|
|
3136
|
+
matchedFields: this.identifyMatchedFields(query, skill)
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
return results.sort((a, b) => b.similarity - a.similarity).slice(0, maxResults);
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Find skills matching a context (problem description, error message, etc.)
|
|
3144
|
+
*/
|
|
3145
|
+
async findByContext(context, skills, options) {
|
|
3146
|
+
const queryParts = [];
|
|
3147
|
+
if (context.problemDescription) {
|
|
3148
|
+
queryParts.push(context.problemDescription);
|
|
3149
|
+
}
|
|
3150
|
+
if (context.errorMessage) {
|
|
3151
|
+
queryParts.push(`error: ${context.errorMessage}`);
|
|
3152
|
+
}
|
|
3153
|
+
if (context.keywords) {
|
|
3154
|
+
queryParts.push(context.keywords.join(" "));
|
|
3155
|
+
}
|
|
3156
|
+
if (context.technologies) {
|
|
3157
|
+
queryParts.push(context.technologies.join(" "));
|
|
3158
|
+
}
|
|
3159
|
+
if (context.codeSnippet) {
|
|
3160
|
+
const identifiers = this.extractIdentifiers(context.codeSnippet);
|
|
3161
|
+
queryParts.push(identifiers.join(" "));
|
|
3162
|
+
}
|
|
3163
|
+
const query = queryParts.join(" ");
|
|
3164
|
+
return this.findMatches(query, skills, options);
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Find skills similar to a given skill
|
|
3168
|
+
*/
|
|
3169
|
+
async findSimilar(skill, allSkills, options) {
|
|
3170
|
+
const skillText = this.skillToText(skill);
|
|
3171
|
+
const excludeSelf = options?.excludeSelf ?? true;
|
|
3172
|
+
const candidates = excludeSelf ? allSkills.filter((s) => s.id !== skill.id) : allSkills;
|
|
3173
|
+
return this.findMatches(skillText, candidates, options);
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Find skills that might apply to a trajectory
|
|
3177
|
+
*/
|
|
3178
|
+
async findForTrajectory(trajectory, skills, options) {
|
|
3179
|
+
const contextParts = [];
|
|
3180
|
+
for (const turn of trajectory.turns) {
|
|
3181
|
+
if (turn.content) {
|
|
3182
|
+
const errorMatch = turn.content.match(/error[:\s]+([^\n]+)/i);
|
|
3183
|
+
if (errorMatch) {
|
|
3184
|
+
contextParts.push(errorMatch[1]);
|
|
3185
|
+
}
|
|
3186
|
+
if (turn.role === "user") {
|
|
3187
|
+
contextParts.push(turn.content.substring(0, 500));
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
const query = contextParts.join(" ");
|
|
3192
|
+
return this.findMatches(query, skills, options);
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Pre-compute and cache embeddings for all skills
|
|
3196
|
+
*/
|
|
3197
|
+
async indexSkills(skills) {
|
|
3198
|
+
const texts = skills.map((s) => this.skillToText(s));
|
|
3199
|
+
const embeddings = await this.provider.embedBatch(texts);
|
|
3200
|
+
for (let i = 0; i < skills.length; i++) {
|
|
3201
|
+
const skill = skills[i];
|
|
3202
|
+
const cacheKey = this.getCacheKey(skill);
|
|
3203
|
+
this.cache.set(cacheKey, {
|
|
3204
|
+
skillId: skill.id,
|
|
3205
|
+
version: skill.version,
|
|
3206
|
+
embedding: embeddings[i],
|
|
3207
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Index skills from a storage adapter
|
|
3213
|
+
*/
|
|
3214
|
+
async indexFromStorage(storage) {
|
|
3215
|
+
const skills = await storage.listSkills();
|
|
3216
|
+
await this.indexSkills(skills);
|
|
3217
|
+
return skills.length;
|
|
3218
|
+
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Clear the embedding cache
|
|
3221
|
+
*/
|
|
3222
|
+
clearCache() {
|
|
3223
|
+
this.cache.clear();
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Get cache statistics
|
|
3227
|
+
*/
|
|
3228
|
+
getCacheStats() {
|
|
3229
|
+
return {
|
|
3230
|
+
size: this.cache.size,
|
|
3231
|
+
skills: Array.from(this.cache.values()).map((e) => `${e.skillId}@${e.version}`)
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Update configuration
|
|
3236
|
+
*/
|
|
3237
|
+
setConfig(config) {
|
|
3238
|
+
if (config.embeddingProvider) {
|
|
3239
|
+
this.provider = config.embeddingProvider;
|
|
3240
|
+
this.config.embeddingProvider = config.embeddingProvider;
|
|
3241
|
+
this.clearCache();
|
|
3242
|
+
}
|
|
3243
|
+
if (config.similarityThreshold !== void 0) {
|
|
3244
|
+
this.config.similarityThreshold = config.similarityThreshold;
|
|
3245
|
+
}
|
|
3246
|
+
if (config.maxResults !== void 0) {
|
|
3247
|
+
this.config.maxResults = config.maxResults;
|
|
3248
|
+
}
|
|
3249
|
+
if (config.cacheEmbeddings !== void 0) {
|
|
3250
|
+
this.config.cacheEmbeddings = config.cacheEmbeddings;
|
|
3251
|
+
}
|
|
3252
|
+
if (config.embeddingFields) {
|
|
3253
|
+
this.config.embeddingFields = config.embeddingFields;
|
|
3254
|
+
this.clearCache();
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
// ===========================================================================
|
|
3258
|
+
// Private helpers
|
|
3259
|
+
// ===========================================================================
|
|
3260
|
+
/**
|
|
3261
|
+
* Get or compute embedding for a skill
|
|
3262
|
+
*/
|
|
3263
|
+
async getSkillEmbedding(skill) {
|
|
3264
|
+
if (this.config.cacheEmbeddings) {
|
|
3265
|
+
const cacheKey = this.getCacheKey(skill);
|
|
3266
|
+
const cached = this.cache.get(cacheKey);
|
|
3267
|
+
if (cached) {
|
|
3268
|
+
return cached.embedding;
|
|
3269
|
+
}
|
|
3270
|
+
const embedding = await this.provider.embed(this.skillToText(skill));
|
|
3271
|
+
this.cache.set(cacheKey, {
|
|
3272
|
+
skillId: skill.id,
|
|
3273
|
+
version: skill.version,
|
|
3274
|
+
embedding,
|
|
3275
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3276
|
+
});
|
|
3277
|
+
return embedding;
|
|
3278
|
+
}
|
|
3279
|
+
return this.provider.embed(this.skillToText(skill));
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Convert skill to text for embedding
|
|
3283
|
+
*/
|
|
3284
|
+
skillToText(skill) {
|
|
3285
|
+
const parts = [];
|
|
3286
|
+
for (const field of this.config.embeddingFields) {
|
|
3287
|
+
switch (field) {
|
|
3288
|
+
case "name":
|
|
3289
|
+
parts.push(skill.name);
|
|
3290
|
+
break;
|
|
3291
|
+
case "description":
|
|
3292
|
+
parts.push(skill.description);
|
|
3293
|
+
break;
|
|
3294
|
+
case "problem":
|
|
3295
|
+
parts.push(skill.problem);
|
|
3296
|
+
break;
|
|
3297
|
+
case "solution":
|
|
3298
|
+
parts.push(skill.solution);
|
|
3299
|
+
break;
|
|
3300
|
+
case "triggerConditions":
|
|
3301
|
+
parts.push(
|
|
3302
|
+
skill.triggerConditions.map((t) => `${t.type}: ${t.value}`).join(" ")
|
|
3303
|
+
);
|
|
3304
|
+
break;
|
|
3305
|
+
case "tags":
|
|
3306
|
+
parts.push(skill.tags.join(" "));
|
|
3307
|
+
break;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
return parts.filter(Boolean).join(" ");
|
|
3311
|
+
}
|
|
3312
|
+
/**
|
|
3313
|
+
* Get cache key for a skill
|
|
3314
|
+
*/
|
|
3315
|
+
getCacheKey(skill) {
|
|
3316
|
+
return `${skill.id}:${skill.version}`;
|
|
3317
|
+
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Identify which fields in the skill matched the query
|
|
3320
|
+
*/
|
|
3321
|
+
identifyMatchedFields(query, skill) {
|
|
3322
|
+
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
3323
|
+
const matchedFields = [];
|
|
3324
|
+
const checkField = (fieldName, value) => {
|
|
3325
|
+
const lowerValue = value.toLowerCase();
|
|
3326
|
+
if (queryTerms.some((term) => lowerValue.includes(term))) {
|
|
3327
|
+
matchedFields.push(fieldName);
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
checkField("name", skill.name);
|
|
3331
|
+
checkField("description", skill.description);
|
|
3332
|
+
checkField("problem", skill.problem);
|
|
3333
|
+
checkField("solution", skill.solution);
|
|
3334
|
+
checkField("tags", skill.tags.join(" "));
|
|
3335
|
+
checkField(
|
|
3336
|
+
"triggerConditions",
|
|
3337
|
+
skill.triggerConditions.map((t) => t.value).join(" ")
|
|
3338
|
+
);
|
|
3339
|
+
return matchedFields;
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Extract identifiers from code snippet
|
|
3343
|
+
*/
|
|
3344
|
+
extractIdentifiers(code) {
|
|
3345
|
+
const identifiers = code.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];
|
|
3346
|
+
return [...new Set(identifiers)].filter((id) => id.length > 2);
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
|
|
3350
|
+
// src/skill-bank.ts
|
|
3351
|
+
var SkillBank = class {
|
|
3352
|
+
constructor(config = {}) {
|
|
3353
|
+
this.eventHandlers = [];
|
|
3354
|
+
this.initialized = false;
|
|
3355
|
+
this.autoIndexOnInit = false;
|
|
3356
|
+
if (config.storage?.type === "filesystem") {
|
|
3357
|
+
this.storage = new FilesystemStorageAdapter({
|
|
3358
|
+
basePath: config.storage.basePath,
|
|
3359
|
+
openSkillsCompatible: config.storage.openSkillsCompatible ?? true
|
|
3360
|
+
});
|
|
3361
|
+
} else {
|
|
3362
|
+
this.storage = new MemoryStorageAdapter();
|
|
3363
|
+
}
|
|
3364
|
+
this.adapterRegistry = new AdapterRegistry();
|
|
3365
|
+
this.adapterRegistry.register(claudeCodeAdapter);
|
|
3366
|
+
if (config.adapters) {
|
|
3367
|
+
for (const adapter of config.adapters) {
|
|
3368
|
+
this.adapterRegistry.register(adapter);
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
this.manualExtractor = new ManualExtractor();
|
|
3372
|
+
this.automaticExtractor = new AutomaticExtractor({
|
|
3373
|
+
llmProvider: config.llmProvider,
|
|
3374
|
+
minConfidence: config.minExtractionConfidence
|
|
3375
|
+
});
|
|
3376
|
+
this.lineageTracker = new LineageTracker(this.storage);
|
|
3377
|
+
this.semanticMatcher = new SemanticMatcher({
|
|
3378
|
+
embeddingProvider: config.matching?.embeddingProvider,
|
|
3379
|
+
similarityThreshold: config.matching?.similarityThreshold
|
|
3380
|
+
});
|
|
3381
|
+
this.autoIndexOnInit = config.matching?.autoIndex ?? false;
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Initialize the skill bank (required before use)
|
|
3385
|
+
*/
|
|
3386
|
+
async initialize() {
|
|
3387
|
+
await this.storage.initialize();
|
|
3388
|
+
this.initialized = true;
|
|
3389
|
+
if (this.autoIndexOnInit) {
|
|
3390
|
+
await this.indexSkills();
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Ensure initialized before operations
|
|
3395
|
+
*/
|
|
3396
|
+
ensureInitialized() {
|
|
3397
|
+
if (!this.initialized) {
|
|
3398
|
+
throw new Error("SkillBank not initialized. Call initialize() first.");
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
// ==========================================================================
|
|
3402
|
+
// Session Parsing
|
|
3403
|
+
// ==========================================================================
|
|
3404
|
+
/**
|
|
3405
|
+
* Parse a session from raw content
|
|
3406
|
+
*/
|
|
3407
|
+
async parseSession(content, adapterName) {
|
|
3408
|
+
let adapter;
|
|
3409
|
+
if (adapterName) {
|
|
3410
|
+
adapter = this.adapterRegistry.get(adapterName);
|
|
3411
|
+
if (!adapter) {
|
|
3412
|
+
throw new Error(`Adapter not found: ${adapterName}`);
|
|
3413
|
+
}
|
|
3414
|
+
} else {
|
|
3415
|
+
adapter = this.adapterRegistry.findAdapter(content);
|
|
3416
|
+
if (!adapter) {
|
|
3417
|
+
throw new Error("No suitable adapter found for this content");
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
return adapter.parse(content);
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* Parse multiple sessions
|
|
3424
|
+
*/
|
|
3425
|
+
async parseSessions(input, adapterName) {
|
|
3426
|
+
let adapter;
|
|
3427
|
+
if (adapterName) {
|
|
3428
|
+
adapter = this.adapterRegistry.get(adapterName);
|
|
3429
|
+
} else {
|
|
3430
|
+
adapter = this.adapterRegistry.findAdapter(input);
|
|
3431
|
+
}
|
|
3432
|
+
if (!adapter) {
|
|
3433
|
+
throw new Error("No suitable adapter found");
|
|
3434
|
+
}
|
|
3435
|
+
return adapter.parseMany(input);
|
|
3436
|
+
}
|
|
3437
|
+
/**
|
|
3438
|
+
* Register a custom session adapter
|
|
3439
|
+
*/
|
|
3440
|
+
registerAdapter(adapter) {
|
|
3441
|
+
this.adapterRegistry.register(adapter);
|
|
3442
|
+
}
|
|
3443
|
+
// ==========================================================================
|
|
3444
|
+
// Skill Extraction
|
|
3445
|
+
// ==========================================================================
|
|
3446
|
+
/**
|
|
3447
|
+
* Extract a skill manually from a trajectory
|
|
3448
|
+
*/
|
|
3449
|
+
async extractManual(trajectory, request) {
|
|
3450
|
+
this.ensureInitialized();
|
|
3451
|
+
this.emit({ type: "extraction:started", sessionId: trajectory.sessionId });
|
|
3452
|
+
const results = await this.manualExtractor.extract(trajectory, {
|
|
3453
|
+
turnRange: request?.turnRange,
|
|
3454
|
+
suggestedName: request?.suggestedName,
|
|
3455
|
+
skipValidation: request?.skipValidation
|
|
3456
|
+
});
|
|
3457
|
+
const result = results[0];
|
|
3458
|
+
if (!result) {
|
|
3459
|
+
const failedResult = {
|
|
3460
|
+
success: false,
|
|
3461
|
+
confidence: 0,
|
|
3462
|
+
gateResults: [],
|
|
3463
|
+
failureReason: "No extraction result",
|
|
3464
|
+
sourceTrajectory: {
|
|
3465
|
+
sessionId: trajectory.sessionId,
|
|
3466
|
+
turnRange: request?.turnRange || [0, trajectory.turns.length - 1]
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
this.emit({ type: "extraction:failed", error: "No extraction result" });
|
|
3470
|
+
return failedResult;
|
|
3471
|
+
}
|
|
3472
|
+
if (result.success && result.skill) {
|
|
3473
|
+
await this.storage.saveSkill(result.skill);
|
|
3474
|
+
this.emit({ type: "skill:created", skill: result.skill });
|
|
3475
|
+
}
|
|
3476
|
+
this.emit({ type: "extraction:completed", result });
|
|
3477
|
+
return result;
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Extract skills automatically from a trajectory
|
|
3481
|
+
*/
|
|
3482
|
+
async extractAutomatic(trajectory, options) {
|
|
3483
|
+
this.ensureInitialized();
|
|
3484
|
+
this.emit({ type: "extraction:started", sessionId: trajectory.sessionId });
|
|
3485
|
+
const results = await this.automaticExtractor.extract(trajectory, options);
|
|
3486
|
+
for (const result of results) {
|
|
3487
|
+
if (result.success && result.skill) {
|
|
3488
|
+
await this.storage.saveSkill(result.skill);
|
|
3489
|
+
this.emit({ type: "skill:created", skill: result.skill });
|
|
3490
|
+
}
|
|
3491
|
+
this.emit({ type: "extraction:completed", result });
|
|
3492
|
+
}
|
|
3493
|
+
return results;
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Extract from raw session content (convenience method)
|
|
3497
|
+
*/
|
|
3498
|
+
async extractFromSession(content, options) {
|
|
3499
|
+
const trajectory = await this.parseSession(content, options?.adapterName);
|
|
3500
|
+
if (options?.mode === "automatic") {
|
|
3501
|
+
return this.extractAutomatic(trajectory, options);
|
|
3502
|
+
} else {
|
|
3503
|
+
const result = await this.extractManual(trajectory, options);
|
|
3504
|
+
return [result];
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Set LLM provider for automatic extraction
|
|
3509
|
+
*/
|
|
3510
|
+
setLLMProvider(provider) {
|
|
3511
|
+
this.automaticExtractor.setLLMProvider(provider);
|
|
3512
|
+
}
|
|
3513
|
+
// ==========================================================================
|
|
3514
|
+
// Semantic Matching
|
|
3515
|
+
// ==========================================================================
|
|
3516
|
+
/**
|
|
3517
|
+
* Find skills matching a query using semantic similarity
|
|
3518
|
+
*/
|
|
3519
|
+
async findMatchingSkills(query, options) {
|
|
3520
|
+
this.ensureInitialized();
|
|
3521
|
+
const skills = await this.storage.listSkills({ status: ["active"] });
|
|
3522
|
+
return this.semanticMatcher.findMatches(query, skills, options);
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Find skills matching a context (problem, error, keywords)
|
|
3526
|
+
*/
|
|
3527
|
+
async findSkillsByContext(context, options) {
|
|
3528
|
+
this.ensureInitialized();
|
|
3529
|
+
const skills = await this.storage.listSkills({ status: ["active"] });
|
|
3530
|
+
return this.semanticMatcher.findByContext(context, skills, options);
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* Find skills similar to a given skill
|
|
3534
|
+
*/
|
|
3535
|
+
async findSimilarSkills(skillId, options) {
|
|
3536
|
+
this.ensureInitialized();
|
|
3537
|
+
const skill = await this.storage.getSkill(skillId);
|
|
3538
|
+
if (!skill) {
|
|
3539
|
+
throw new Error(`Skill not found: ${skillId}`);
|
|
3540
|
+
}
|
|
3541
|
+
const allSkills = await this.storage.listSkills({ status: ["active"] });
|
|
3542
|
+
return this.semanticMatcher.findSimilar(skill, allSkills, {
|
|
3543
|
+
...options,
|
|
3544
|
+
excludeSelf: true
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Find skills that might apply to a trajectory
|
|
3549
|
+
*/
|
|
3550
|
+
async findSkillsForTrajectory(trajectory, options) {
|
|
3551
|
+
this.ensureInitialized();
|
|
3552
|
+
const skills = await this.storage.listSkills({ status: ["active"] });
|
|
3553
|
+
return this.semanticMatcher.findForTrajectory(trajectory, skills, options);
|
|
3554
|
+
}
|
|
3555
|
+
/**
|
|
3556
|
+
* Index all skills for faster semantic matching
|
|
3557
|
+
*/
|
|
3558
|
+
async indexSkills() {
|
|
3559
|
+
this.ensureInitialized();
|
|
3560
|
+
return this.semanticMatcher.indexFromStorage(this.storage);
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Set embedding provider for semantic matching
|
|
3564
|
+
*/
|
|
3565
|
+
setEmbeddingProvider(provider) {
|
|
3566
|
+
this.semanticMatcher.setConfig({ embeddingProvider: provider });
|
|
3567
|
+
}
|
|
3568
|
+
// ==========================================================================
|
|
3569
|
+
// Skill CRUD Operations
|
|
3570
|
+
// ==========================================================================
|
|
3571
|
+
/**
|
|
3572
|
+
* Get a skill by ID
|
|
3573
|
+
*/
|
|
3574
|
+
async getSkill(id, version) {
|
|
3575
|
+
this.ensureInitialized();
|
|
3576
|
+
return this.storage.getSkill(id, version);
|
|
3577
|
+
}
|
|
3578
|
+
/**
|
|
3579
|
+
* List all skills with optional filtering
|
|
3580
|
+
*/
|
|
3581
|
+
async listSkills(filter) {
|
|
3582
|
+
this.ensureInitialized();
|
|
3583
|
+
return this.storage.listSkills(filter);
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Search skills by query
|
|
3587
|
+
*/
|
|
3588
|
+
async searchSkills(query) {
|
|
3589
|
+
this.ensureInitialized();
|
|
3590
|
+
return this.storage.searchSkills(query);
|
|
3591
|
+
}
|
|
3592
|
+
/**
|
|
3593
|
+
* Save or update a skill
|
|
3594
|
+
*/
|
|
3595
|
+
async saveSkill(skill) {
|
|
3596
|
+
this.ensureInitialized();
|
|
3597
|
+
const existing = await this.storage.getSkill(skill.id);
|
|
3598
|
+
await this.storage.saveSkill(skill);
|
|
3599
|
+
if (existing) {
|
|
3600
|
+
this.emit({ type: "skill:updated", skill, previousVersion: existing.version });
|
|
3601
|
+
} else {
|
|
3602
|
+
this.emit({ type: "skill:created", skill });
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Delete a skill
|
|
3607
|
+
*/
|
|
3608
|
+
async deleteSkill(id, version) {
|
|
3609
|
+
this.ensureInitialized();
|
|
3610
|
+
const deleted = await this.storage.deleteSkill(id, version);
|
|
3611
|
+
if (deleted) {
|
|
3612
|
+
this.emit({ type: "skill:deleted", skillId: id });
|
|
3613
|
+
}
|
|
3614
|
+
return deleted;
|
|
3615
|
+
}
|
|
3616
|
+
/**
|
|
3617
|
+
* Deprecate a skill
|
|
3618
|
+
*/
|
|
3619
|
+
async deprecateSkill(id) {
|
|
3620
|
+
this.ensureInitialized();
|
|
3621
|
+
const skill = await this.storage.getSkill(id);
|
|
3622
|
+
if (!skill) {
|
|
3623
|
+
throw new Error(`Skill not found: ${id}`);
|
|
3624
|
+
}
|
|
3625
|
+
const deprecated = {
|
|
3626
|
+
...skill,
|
|
3627
|
+
status: "deprecated",
|
|
3628
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3629
|
+
};
|
|
3630
|
+
await this.storage.saveSkill(deprecated);
|
|
3631
|
+
this.emit({ type: "skill:deprecated", skillId: id });
|
|
3632
|
+
return deprecated;
|
|
3633
|
+
}
|
|
3634
|
+
// ==========================================================================
|
|
3635
|
+
// Version Management
|
|
3636
|
+
// ==========================================================================
|
|
3637
|
+
/**
|
|
3638
|
+
* Create a new version of a skill
|
|
3639
|
+
*/
|
|
3640
|
+
async createVersion(skillId, updates, options) {
|
|
3641
|
+
this.ensureInitialized();
|
|
3642
|
+
const newSkill = await this.lineageTracker.createVersion(skillId, updates, options);
|
|
3643
|
+
this.emit({ type: "skill:updated", skill: newSkill, previousVersion: newSkill.parentVersion || "" });
|
|
3644
|
+
return newSkill;
|
|
3645
|
+
}
|
|
3646
|
+
/**
|
|
3647
|
+
* Fork a skill to create a new variant
|
|
3648
|
+
*/
|
|
3649
|
+
async forkSkill(skillId, options) {
|
|
3650
|
+
this.ensureInitialized();
|
|
3651
|
+
const forked = await this.lineageTracker.forkSkill(skillId, options);
|
|
3652
|
+
this.emit({ type: "skill:created", skill: forked });
|
|
3653
|
+
return forked;
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Get version history for a skill
|
|
3657
|
+
*/
|
|
3658
|
+
async getVersionHistory(skillId) {
|
|
3659
|
+
this.ensureInitialized();
|
|
3660
|
+
return this.storage.getVersionHistory(skillId);
|
|
3661
|
+
}
|
|
3662
|
+
/**
|
|
3663
|
+
* Get full lineage for a skill
|
|
3664
|
+
*/
|
|
3665
|
+
async getLineage(skillId) {
|
|
3666
|
+
this.ensureInitialized();
|
|
3667
|
+
return this.storage.getLineage(skillId);
|
|
3668
|
+
}
|
|
3669
|
+
/**
|
|
3670
|
+
* Rollback a skill to a previous version
|
|
3671
|
+
*/
|
|
3672
|
+
async rollbackSkill(skillId, toVersion) {
|
|
3673
|
+
this.ensureInitialized();
|
|
3674
|
+
return this.lineageTracker.rollback(skillId, toVersion);
|
|
3675
|
+
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Compare two versions of a skill
|
|
3678
|
+
*/
|
|
3679
|
+
async compareVersions(skillId, versionA, versionB) {
|
|
3680
|
+
this.ensureInitialized();
|
|
3681
|
+
return this.lineageTracker.compareVersions(skillId, versionA, versionB);
|
|
3682
|
+
}
|
|
3683
|
+
// ==========================================================================
|
|
3684
|
+
// Events
|
|
3685
|
+
// ==========================================================================
|
|
3686
|
+
/**
|
|
3687
|
+
* Subscribe to skill bank events
|
|
3688
|
+
*/
|
|
3689
|
+
on(handler) {
|
|
3690
|
+
this.eventHandlers.push(handler);
|
|
3691
|
+
return () => {
|
|
3692
|
+
const index = this.eventHandlers.indexOf(handler);
|
|
3693
|
+
if (index >= 0) {
|
|
3694
|
+
this.eventHandlers.splice(index, 1);
|
|
3695
|
+
}
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Emit an event to all handlers
|
|
3700
|
+
*/
|
|
3701
|
+
emit(event) {
|
|
3702
|
+
for (const handler of this.eventHandlers) {
|
|
3703
|
+
try {
|
|
3704
|
+
handler(event);
|
|
3705
|
+
} catch (error) {
|
|
3706
|
+
console.error("Event handler error:", error);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
// ==========================================================================
|
|
3711
|
+
// Utilities
|
|
3712
|
+
// ==========================================================================
|
|
3713
|
+
/**
|
|
3714
|
+
* Get statistics about the skill bank
|
|
3715
|
+
*/
|
|
3716
|
+
async getStats() {
|
|
3717
|
+
this.ensureInitialized();
|
|
3718
|
+
const skills = await this.storage.listSkills();
|
|
3719
|
+
const stats = {
|
|
3720
|
+
totalSkills: skills.length,
|
|
3721
|
+
byStatus: {
|
|
3722
|
+
draft: 0,
|
|
3723
|
+
active: 0,
|
|
3724
|
+
deprecated: 0,
|
|
3725
|
+
experimental: 0
|
|
3726
|
+
},
|
|
3727
|
+
byTag: {},
|
|
3728
|
+
avgSuccessRate: 0,
|
|
3729
|
+
totalUsage: 0
|
|
3730
|
+
};
|
|
3731
|
+
let successRateSum = 0;
|
|
3732
|
+
let successRateCount = 0;
|
|
3733
|
+
for (const skill of skills) {
|
|
3734
|
+
stats.byStatus[skill.status]++;
|
|
3735
|
+
for (const tag of skill.tags) {
|
|
3736
|
+
stats.byTag[tag] = (stats.byTag[tag] || 0) + 1;
|
|
3737
|
+
}
|
|
3738
|
+
stats.totalUsage += skill.metrics.usageCount;
|
|
3739
|
+
if (skill.metrics.successRate > 0) {
|
|
3740
|
+
successRateSum += skill.metrics.successRate;
|
|
3741
|
+
successRateCount++;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
stats.avgSuccessRate = successRateCount > 0 ? successRateSum / successRateCount : 0;
|
|
3745
|
+
return stats;
|
|
3746
|
+
}
|
|
3747
|
+
/**
|
|
3748
|
+
* Export all skills (for backup or migration)
|
|
3749
|
+
*/
|
|
3750
|
+
async exportAll() {
|
|
3751
|
+
this.ensureInitialized();
|
|
3752
|
+
return this.storage.listSkills();
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Import skills (for restore or migration)
|
|
3756
|
+
*/
|
|
3757
|
+
async importSkills(skills) {
|
|
3758
|
+
this.ensureInitialized();
|
|
3759
|
+
let imported = 0;
|
|
3760
|
+
let failed = 0;
|
|
3761
|
+
for (const skill of skills) {
|
|
3762
|
+
try {
|
|
3763
|
+
await this.storage.saveSkill(skill);
|
|
3764
|
+
this.emit({ type: "skill:created", skill });
|
|
3765
|
+
imported++;
|
|
3766
|
+
} catch {
|
|
3767
|
+
failed++;
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
return { imported, failed };
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
function createSkillBank(config) {
|
|
3774
|
+
return new SkillBank(config);
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
// src/batch/processor.ts
|
|
3778
|
+
var BatchProcessor = class {
|
|
3779
|
+
constructor(config = {}) {
|
|
3780
|
+
this.config = {
|
|
3781
|
+
concurrency: config.concurrency ?? 5,
|
|
3782
|
+
minConfidence: config.minConfidence ?? 0.6,
|
|
3783
|
+
deduplication: config.deduplication ?? "semantic",
|
|
3784
|
+
deduplicationThreshold: config.deduplicationThreshold ?? 0.85,
|
|
3785
|
+
skipQualityGates: config.skipQualityGates ?? false,
|
|
3786
|
+
extractionMode: config.extractionMode ?? "manual",
|
|
3787
|
+
llmProvider: config.llmProvider,
|
|
3788
|
+
embeddingProvider: config.embeddingProvider,
|
|
3789
|
+
onProgress: config.onProgress
|
|
3790
|
+
};
|
|
3791
|
+
this.manualExtractor = new ManualExtractor();
|
|
3792
|
+
this.automaticExtractor = new AutomaticExtractor({
|
|
3793
|
+
minConfidence: this.config.minConfidence
|
|
3794
|
+
});
|
|
3795
|
+
if (this.config.llmProvider) {
|
|
3796
|
+
this.automaticExtractor.setLLMProvider(this.config.llmProvider);
|
|
3797
|
+
}
|
|
3798
|
+
this.matcher = new SemanticMatcher({
|
|
3799
|
+
embeddingProvider: this.config.embeddingProvider || new SimpleHashEmbedding(),
|
|
3800
|
+
similarityThreshold: this.config.deduplicationThreshold
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Process multiple sessions and extract skills
|
|
3805
|
+
*/
|
|
3806
|
+
async process(sessions, parseSession) {
|
|
3807
|
+
const startTime = Date.now();
|
|
3808
|
+
const allExtractions = [];
|
|
3809
|
+
const failedSessions = [];
|
|
3810
|
+
const progress = {
|
|
3811
|
+
total: sessions.length,
|
|
3812
|
+
processed: 0,
|
|
3813
|
+
skillsExtracted: 0,
|
|
3814
|
+
duplicatesFound: 0,
|
|
3815
|
+
phase: "parsing",
|
|
3816
|
+
percentage: 0
|
|
3817
|
+
};
|
|
3818
|
+
this.reportProgress(progress);
|
|
3819
|
+
const batches = this.createBatches(sessions, this.config.concurrency);
|
|
3820
|
+
for (const batch of batches) {
|
|
3821
|
+
progress.phase = "extracting";
|
|
3822
|
+
const batchPromises = batch.map(async (session) => {
|
|
3823
|
+
progress.currentSession = session.source;
|
|
3824
|
+
this.reportProgress(progress);
|
|
3825
|
+
try {
|
|
3826
|
+
const trajectory = session.trajectory || await parseSession(session.content);
|
|
3827
|
+
let results = [];
|
|
3828
|
+
if (this.config.extractionMode === "automatic" && this.config.llmProvider) {
|
|
3829
|
+
results = await this.automaticExtractor.extract(trajectory);
|
|
3830
|
+
} else if (this.config.extractionMode === "manual") {
|
|
3831
|
+
results = await this.manualExtractor.extract(trajectory, {
|
|
3832
|
+
suggestedName: this.generateSkillName(trajectory, session.source),
|
|
3833
|
+
skipValidation: this.config.skipQualityGates
|
|
3834
|
+
});
|
|
3835
|
+
} else if (this.config.extractionMode === "candidates-only") {
|
|
3836
|
+
results = [this.createCandidateExtraction(trajectory, session.source)];
|
|
3837
|
+
}
|
|
3838
|
+
const successful = results.filter((r) => r.success && r.skill);
|
|
3839
|
+
allExtractions.push(...successful);
|
|
3840
|
+
progress.skillsExtracted += successful.length;
|
|
3841
|
+
} catch (error) {
|
|
3842
|
+
failedSessions.push({
|
|
3843
|
+
source: session.source,
|
|
3844
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
progress.processed++;
|
|
3848
|
+
progress.percentage = Math.round(progress.processed / progress.total * 100);
|
|
3849
|
+
this.reportProgress(progress);
|
|
3850
|
+
});
|
|
3851
|
+
await Promise.all(batchPromises);
|
|
3852
|
+
}
|
|
3853
|
+
progress.phase = "deduplicating";
|
|
3854
|
+
this.reportProgress(progress);
|
|
3855
|
+
const { unique, duplicates } = await this.deduplicate(allExtractions);
|
|
3856
|
+
progress.duplicatesFound = duplicates.length;
|
|
3857
|
+
progress.phase = "complete";
|
|
3858
|
+
progress.percentage = 100;
|
|
3859
|
+
this.reportProgress(progress);
|
|
3860
|
+
const avgConfidence = unique.length > 0 ? unique.reduce((sum, e) => sum + e.confidence, 0) / unique.length : 0;
|
|
3861
|
+
return {
|
|
3862
|
+
skills: unique,
|
|
3863
|
+
totalSessions: sessions.length,
|
|
3864
|
+
failedSessions,
|
|
3865
|
+
duplicates,
|
|
3866
|
+
stats: {
|
|
3867
|
+
totalExtractions: allExtractions.length,
|
|
3868
|
+
uniqueSkills: unique.length,
|
|
3869
|
+
duplicatesRemoved: duplicates.length,
|
|
3870
|
+
averageConfidence: avgConfidence,
|
|
3871
|
+
processingTimeMs: Date.now() - startTime
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
/**
|
|
3876
|
+
* Process session files from file paths
|
|
3877
|
+
*/
|
|
3878
|
+
async processFiles(filePaths, readFile2, parseSession) {
|
|
3879
|
+
const sessions = [];
|
|
3880
|
+
for (const path2 of filePaths) {
|
|
3881
|
+
const content = await readFile2(path2);
|
|
3882
|
+
sessions.push({
|
|
3883
|
+
content,
|
|
3884
|
+
source: path2
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
return this.process(sessions, parseSession);
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Set LLM provider for automatic extraction
|
|
3891
|
+
*/
|
|
3892
|
+
setLLMProvider(provider) {
|
|
3893
|
+
this.config.llmProvider = provider;
|
|
3894
|
+
this.automaticExtractor.setLLMProvider(provider);
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Set embedding provider for semantic deduplication
|
|
3898
|
+
*/
|
|
3899
|
+
setEmbeddingProvider(provider) {
|
|
3900
|
+
this.config.embeddingProvider = provider;
|
|
3901
|
+
this.matcher = new SemanticMatcher({
|
|
3902
|
+
embeddingProvider: provider,
|
|
3903
|
+
similarityThreshold: this.config.deduplicationThreshold
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* Update configuration
|
|
3908
|
+
*/
|
|
3909
|
+
setConfig(config) {
|
|
3910
|
+
if (config.concurrency !== void 0) {
|
|
3911
|
+
this.config.concurrency = config.concurrency;
|
|
3912
|
+
}
|
|
3913
|
+
if (config.minConfidence !== void 0) {
|
|
3914
|
+
this.config.minConfidence = config.minConfidence;
|
|
3915
|
+
this.automaticExtractor = new AutomaticExtractor({
|
|
3916
|
+
minConfidence: config.minConfidence
|
|
3917
|
+
});
|
|
3918
|
+
if (this.config.llmProvider) {
|
|
3919
|
+
this.automaticExtractor.setLLMProvider(this.config.llmProvider);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
if (config.deduplication !== void 0) {
|
|
3923
|
+
this.config.deduplication = config.deduplication;
|
|
3924
|
+
}
|
|
3925
|
+
if (config.deduplicationThreshold !== void 0) {
|
|
3926
|
+
this.config.deduplicationThreshold = config.deduplicationThreshold;
|
|
3927
|
+
this.matcher.setConfig({ similarityThreshold: config.deduplicationThreshold });
|
|
3928
|
+
}
|
|
3929
|
+
if (config.skipQualityGates !== void 0) {
|
|
3930
|
+
this.config.skipQualityGates = config.skipQualityGates;
|
|
3931
|
+
}
|
|
3932
|
+
if (config.extractionMode !== void 0) {
|
|
3933
|
+
this.config.extractionMode = config.extractionMode;
|
|
3934
|
+
}
|
|
3935
|
+
if (config.onProgress !== void 0) {
|
|
3936
|
+
this.config.onProgress = config.onProgress;
|
|
3937
|
+
}
|
|
3938
|
+
if (config.llmProvider !== void 0) {
|
|
3939
|
+
this.setLLMProvider(config.llmProvider);
|
|
3940
|
+
}
|
|
3941
|
+
if (config.embeddingProvider !== void 0) {
|
|
3942
|
+
this.setEmbeddingProvider(config.embeddingProvider);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
// ===========================================================================
|
|
3946
|
+
// Private helpers
|
|
3947
|
+
// ===========================================================================
|
|
3948
|
+
/**
|
|
3949
|
+
* Create batches for parallel processing
|
|
3950
|
+
*/
|
|
3951
|
+
createBatches(items, batchSize) {
|
|
3952
|
+
const batches = [];
|
|
3953
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
3954
|
+
batches.push(items.slice(i, i + batchSize));
|
|
3955
|
+
}
|
|
3956
|
+
return batches;
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* Deduplicate extracted skills
|
|
3960
|
+
*/
|
|
3961
|
+
async deduplicate(extractions) {
|
|
3962
|
+
if (this.config.deduplication === "none" || extractions.length === 0) {
|
|
3963
|
+
return { unique: extractions, duplicates: [] };
|
|
3964
|
+
}
|
|
3965
|
+
const unique = [];
|
|
3966
|
+
const duplicates = [];
|
|
3967
|
+
for (const extraction of extractions) {
|
|
3968
|
+
if (unique.length === 0) {
|
|
3969
|
+
unique.push(extraction);
|
|
3970
|
+
continue;
|
|
3971
|
+
}
|
|
3972
|
+
const isDuplicate = await this.checkDuplicate(extraction, unique);
|
|
3973
|
+
if (isDuplicate) {
|
|
3974
|
+
duplicates.push(isDuplicate);
|
|
3975
|
+
} else {
|
|
3976
|
+
unique.push(extraction);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
return { unique, duplicates };
|
|
3980
|
+
}
|
|
3981
|
+
/**
|
|
3982
|
+
* Check if an extraction is a duplicate of existing ones
|
|
3983
|
+
*/
|
|
3984
|
+
async checkDuplicate(extraction, existing) {
|
|
3985
|
+
const extractionSkill = extraction.skill;
|
|
3986
|
+
if (!extractionSkill) return null;
|
|
3987
|
+
if (this.config.deduplication === "exact") {
|
|
3988
|
+
for (const e of existing) {
|
|
3989
|
+
const existingSkill = e.skill;
|
|
3990
|
+
if (!existingSkill) continue;
|
|
3991
|
+
if (existingSkill.name === extractionSkill.name || existingSkill.problem === extractionSkill.problem) {
|
|
3992
|
+
return {
|
|
3993
|
+
skill: extraction,
|
|
3994
|
+
duplicateOf: existingSkill.id
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
} else if (this.config.deduplication === "semantic") {
|
|
3999
|
+
const existingSkills = existing.filter((e) => e.skill !== void 0).map((e) => e.skill);
|
|
4000
|
+
const matches = await this.matcher.findMatches(
|
|
4001
|
+
`${extractionSkill.name} ${extractionSkill.problem} ${extractionSkill.description}`,
|
|
4002
|
+
existingSkills,
|
|
4003
|
+
{ threshold: this.config.deduplicationThreshold, maxResults: 1 }
|
|
4004
|
+
);
|
|
4005
|
+
if (matches.length > 0) {
|
|
4006
|
+
return {
|
|
4007
|
+
skill: extraction,
|
|
4008
|
+
duplicateOf: matches[0].skill.id,
|
|
4009
|
+
similarity: matches[0].similarity
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
return null;
|
|
4014
|
+
}
|
|
4015
|
+
/**
|
|
4016
|
+
* Generate skill name from trajectory
|
|
4017
|
+
*/
|
|
4018
|
+
generateSkillName(trajectory, source) {
|
|
4019
|
+
const firstUserTurn = trajectory.turns.find((t) => t.role === "user");
|
|
4020
|
+
if (firstUserTurn?.content) {
|
|
4021
|
+
const firstLine = firstUserTurn.content.split(/[.\n]/)[0].trim();
|
|
4022
|
+
if (firstLine.length > 0 && firstLine.length < 60) {
|
|
4023
|
+
return this.slugify(firstLine);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
if (source) {
|
|
4027
|
+
const baseName = source.split("/").pop()?.replace(/\.[^.]+$/, "");
|
|
4028
|
+
if (baseName) {
|
|
4029
|
+
return `skill-from-${this.slugify(baseName)}`;
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
return `skill-${Date.now().toString(36)}`;
|
|
4033
|
+
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Convert string to slug
|
|
4036
|
+
*/
|
|
4037
|
+
slugify(text) {
|
|
4038
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 50);
|
|
4039
|
+
}
|
|
4040
|
+
/**
|
|
4041
|
+
* Create a candidate extraction without full processing
|
|
4042
|
+
*/
|
|
4043
|
+
createCandidateExtraction(trajectory, source) {
|
|
4044
|
+
const name = this.generateSkillName(trajectory, source);
|
|
4045
|
+
const firstUserTurn = trajectory.turns.find((t) => t.role === "user");
|
|
4046
|
+
return {
|
|
4047
|
+
success: true,
|
|
4048
|
+
skill: {
|
|
4049
|
+
id: name,
|
|
4050
|
+
name: name.replace(/-/g, " "),
|
|
4051
|
+
description: "Candidate skill - needs review",
|
|
4052
|
+
problem: firstUserTurn?.content?.substring(0, 500) || "Unknown problem",
|
|
4053
|
+
solution: "Solution pending extraction",
|
|
4054
|
+
verification: "Verification pending",
|
|
4055
|
+
triggerConditions: [],
|
|
4056
|
+
tags: [],
|
|
4057
|
+
version: "0.1.0",
|
|
4058
|
+
status: "draft",
|
|
4059
|
+
examples: [],
|
|
4060
|
+
author: "batch-processor",
|
|
4061
|
+
metrics: {
|
|
4062
|
+
usageCount: 0,
|
|
4063
|
+
successRate: 0,
|
|
4064
|
+
feedbackScores: []
|
|
4065
|
+
},
|
|
4066
|
+
source: {
|
|
4067
|
+
type: "extracted",
|
|
4068
|
+
sessionId: trajectory.sessionId,
|
|
4069
|
+
importedAt: /* @__PURE__ */ new Date()
|
|
4070
|
+
},
|
|
4071
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
4072
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4073
|
+
},
|
|
4074
|
+
confidence: 0.3,
|
|
4075
|
+
gateResults: [],
|
|
4076
|
+
sourceTrajectory: {
|
|
4077
|
+
sessionId: trajectory.sessionId,
|
|
4078
|
+
turnRange: [0, trajectory.turns.length - 1]
|
|
4079
|
+
}
|
|
4080
|
+
};
|
|
4081
|
+
}
|
|
4082
|
+
/**
|
|
4083
|
+
* Report progress to callback
|
|
4084
|
+
*/
|
|
4085
|
+
reportProgress(progress) {
|
|
4086
|
+
if (this.config.onProgress) {
|
|
4087
|
+
this.config.onProgress({ ...progress });
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
|
|
4092
|
+
// src/composer/dependency-graph.ts
|
|
4093
|
+
var DependencyGraph = class {
|
|
4094
|
+
constructor() {
|
|
4095
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
4096
|
+
this.edges = [];
|
|
4097
|
+
this.adjacencyList = /* @__PURE__ */ new Map();
|
|
4098
|
+
this.reverseAdjacencyList = /* @__PURE__ */ new Map();
|
|
4099
|
+
}
|
|
4100
|
+
/**
|
|
4101
|
+
* Add a skill to the graph
|
|
4102
|
+
*/
|
|
4103
|
+
addSkill(skill) {
|
|
4104
|
+
const key = this.getKey(skill.id, skill.version);
|
|
4105
|
+
this.nodes.set(key, {
|
|
4106
|
+
skillId: skill.id,
|
|
4107
|
+
version: skill.version,
|
|
4108
|
+
skill
|
|
4109
|
+
});
|
|
4110
|
+
if (!this.adjacencyList.has(key)) {
|
|
4111
|
+
this.adjacencyList.set(key, /* @__PURE__ */ new Set());
|
|
4112
|
+
}
|
|
4113
|
+
if (!this.reverseAdjacencyList.has(key)) {
|
|
4114
|
+
this.reverseAdjacencyList.set(key, /* @__PURE__ */ new Set());
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* Remove a skill from the graph
|
|
4119
|
+
*/
|
|
4120
|
+
removeSkill(skillId, version) {
|
|
4121
|
+
const keysToRemove = version ? [this.getKey(skillId, version)] : Array.from(this.nodes.keys()).filter((k) => k.startsWith(`${skillId}@`));
|
|
4122
|
+
for (const key of keysToRemove) {
|
|
4123
|
+
this.nodes.delete(key);
|
|
4124
|
+
this.adjacencyList.delete(key);
|
|
4125
|
+
this.reverseAdjacencyList.delete(key);
|
|
4126
|
+
this.edges = this.edges.filter(
|
|
4127
|
+
(e) => this.getKey(e.from) !== key && this.getKey(e.to) !== key
|
|
4128
|
+
);
|
|
4129
|
+
for (const adj of this.adjacencyList.values()) {
|
|
4130
|
+
adj.delete(key);
|
|
4131
|
+
}
|
|
4132
|
+
for (const adj of this.reverseAdjacencyList.values()) {
|
|
4133
|
+
adj.delete(key);
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Add a dependency edge
|
|
4139
|
+
*/
|
|
4140
|
+
addDependency(fromId, toId, type, options) {
|
|
4141
|
+
const edge = {
|
|
4142
|
+
from: fromId,
|
|
4143
|
+
to: toId,
|
|
4144
|
+
type,
|
|
4145
|
+
required: options?.required ?? true,
|
|
4146
|
+
description: options?.description
|
|
4147
|
+
};
|
|
4148
|
+
this.edges.push(edge);
|
|
4149
|
+
const fromKey = options?.fromVersion ? this.getKey(fromId, options.fromVersion) : fromId;
|
|
4150
|
+
const toKey = options?.toVersion ? this.getKey(toId, options.toVersion) : toId;
|
|
4151
|
+
if (!this.adjacencyList.has(fromKey)) {
|
|
4152
|
+
this.adjacencyList.set(fromKey, /* @__PURE__ */ new Set());
|
|
4153
|
+
}
|
|
4154
|
+
this.adjacencyList.get(fromKey).add(toKey);
|
|
4155
|
+
if (!this.reverseAdjacencyList.has(toKey)) {
|
|
4156
|
+
this.reverseAdjacencyList.set(toKey, /* @__PURE__ */ new Set());
|
|
4157
|
+
}
|
|
4158
|
+
this.reverseAdjacencyList.get(toKey).add(fromKey);
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Remove a dependency edge
|
|
4162
|
+
*/
|
|
4163
|
+
removeDependency(fromId, toId, type) {
|
|
4164
|
+
this.edges = this.edges.filter(
|
|
4165
|
+
(e) => !(e.from === fromId && e.to === toId && (type === void 0 || e.type === type))
|
|
4166
|
+
);
|
|
4167
|
+
const remainingEdges = this.edges.filter(
|
|
4168
|
+
(e) => e.from === fromId && e.to === toId
|
|
4169
|
+
);
|
|
4170
|
+
if (remainingEdges.length === 0) {
|
|
4171
|
+
this.adjacencyList.get(fromId)?.delete(toId);
|
|
4172
|
+
this.reverseAdjacencyList.get(toId)?.delete(fromId);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* Get all dependencies of a skill
|
|
4177
|
+
*/
|
|
4178
|
+
getDependencies(skillId) {
|
|
4179
|
+
return this.edges.filter((e) => e.from === skillId);
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Get all skills that depend on this skill
|
|
4183
|
+
*/
|
|
4184
|
+
getDependents(skillId) {
|
|
4185
|
+
return this.edges.filter((e) => e.to === skillId);
|
|
4186
|
+
}
|
|
4187
|
+
/**
|
|
4188
|
+
* Get conflicts for a skill
|
|
4189
|
+
*/
|
|
4190
|
+
getConflicts(skillId) {
|
|
4191
|
+
return this.edges.filter(
|
|
4192
|
+
(e) => e.type === "conflicts" && (e.from === skillId || e.to === skillId)
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Check if two skills conflict
|
|
4197
|
+
*/
|
|
4198
|
+
hasConflict(skillId1, skillId2) {
|
|
4199
|
+
return this.edges.some(
|
|
4200
|
+
(e) => e.type === "conflicts" && (e.from === skillId1 && e.to === skillId2 || e.from === skillId2 && e.to === skillId1)
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
4203
|
+
/**
|
|
4204
|
+
* Get topological sort of skills (for execution order)
|
|
4205
|
+
*/
|
|
4206
|
+
getTopologicalOrder(skillIds) {
|
|
4207
|
+
const targetIds = skillIds || Array.from(new Set(this.edges.flatMap((e) => [e.from, e.to])));
|
|
4208
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4209
|
+
const result = [];
|
|
4210
|
+
const visit = (id) => {
|
|
4211
|
+
if (visited.has(id)) return;
|
|
4212
|
+
visited.add(id);
|
|
4213
|
+
const deps = this.edges.filter(
|
|
4214
|
+
(e) => e.from === id && (e.type === "requires" || e.type === "precedes" || e.type === "extends")
|
|
4215
|
+
);
|
|
4216
|
+
for (const dep of deps) {
|
|
4217
|
+
visit(dep.to);
|
|
4218
|
+
}
|
|
4219
|
+
result.push(id);
|
|
4220
|
+
};
|
|
4221
|
+
for (const id of targetIds) {
|
|
4222
|
+
visit(id);
|
|
4223
|
+
}
|
|
4224
|
+
return result;
|
|
4225
|
+
}
|
|
4226
|
+
/**
|
|
4227
|
+
* Detect cycles in the graph
|
|
4228
|
+
*/
|
|
4229
|
+
detectCycle() {
|
|
4230
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4231
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
4232
|
+
const path2 = [];
|
|
4233
|
+
const dfs = (node) => {
|
|
4234
|
+
visited.add(node);
|
|
4235
|
+
recStack.add(node);
|
|
4236
|
+
path2.push(node);
|
|
4237
|
+
const neighbors = this.adjacencyList.get(node) || /* @__PURE__ */ new Set();
|
|
4238
|
+
for (const neighbor of neighbors) {
|
|
4239
|
+
if (!visited.has(neighbor)) {
|
|
4240
|
+
const cycle = dfs(neighbor);
|
|
4241
|
+
if (cycle) return cycle;
|
|
4242
|
+
} else if (recStack.has(neighbor)) {
|
|
4243
|
+
const cycleStart = path2.indexOf(neighbor);
|
|
4244
|
+
return path2.slice(cycleStart).concat(neighbor);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
path2.pop();
|
|
4248
|
+
recStack.delete(node);
|
|
4249
|
+
return null;
|
|
4250
|
+
};
|
|
4251
|
+
const allNodes = /* @__PURE__ */ new Set([
|
|
4252
|
+
...this.adjacencyList.keys(),
|
|
4253
|
+
...this.reverseAdjacencyList.keys()
|
|
4254
|
+
]);
|
|
4255
|
+
for (const node of allNodes) {
|
|
4256
|
+
if (!visited.has(node)) {
|
|
4257
|
+
const cycle = dfs(node);
|
|
4258
|
+
if (cycle) {
|
|
4259
|
+
return { hasCycle: true, cycle };
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
return { hasCycle: false };
|
|
4264
|
+
}
|
|
4265
|
+
/**
|
|
4266
|
+
* Get all required dependencies (transitive)
|
|
4267
|
+
*/
|
|
4268
|
+
getTransitiveDependencies(skillId) {
|
|
4269
|
+
const result = /* @__PURE__ */ new Set();
|
|
4270
|
+
const queue = [skillId];
|
|
4271
|
+
while (queue.length > 0) {
|
|
4272
|
+
const current = queue.shift();
|
|
4273
|
+
const deps = this.edges.filter(
|
|
4274
|
+
(e) => e.from === current && e.type === "requires" && e.required
|
|
4275
|
+
);
|
|
4276
|
+
for (const dep of deps) {
|
|
4277
|
+
if (!result.has(dep.to)) {
|
|
4278
|
+
result.add(dep.to);
|
|
4279
|
+
queue.push(dep.to);
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
return Array.from(result);
|
|
4284
|
+
}
|
|
4285
|
+
/**
|
|
4286
|
+
* Check if all required dependencies are satisfied
|
|
4287
|
+
*/
|
|
4288
|
+
checkDependenciesSatisfied(skillIds) {
|
|
4289
|
+
const available = new Set(skillIds);
|
|
4290
|
+
const missing = [];
|
|
4291
|
+
for (const id of skillIds) {
|
|
4292
|
+
const requiredDeps = this.edges.filter(
|
|
4293
|
+
(e) => e.from === id && e.type === "requires" && e.required
|
|
4294
|
+
);
|
|
4295
|
+
for (const dep of requiredDeps) {
|
|
4296
|
+
if (!available.has(dep.to)) {
|
|
4297
|
+
missing.push(dep.to);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
return {
|
|
4302
|
+
satisfied: missing.length === 0,
|
|
4303
|
+
missing: [...new Set(missing)]
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
4306
|
+
/**
|
|
4307
|
+
* Get all skills in the graph
|
|
4308
|
+
*/
|
|
4309
|
+
getSkills() {
|
|
4310
|
+
return Array.from(this.nodes.values()).map((n) => n.skill);
|
|
4311
|
+
}
|
|
4312
|
+
/**
|
|
4313
|
+
* Get all edges in the graph
|
|
4314
|
+
*/
|
|
4315
|
+
getEdges() {
|
|
4316
|
+
return [...this.edges];
|
|
4317
|
+
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Get graph statistics
|
|
4320
|
+
*/
|
|
4321
|
+
getStats() {
|
|
4322
|
+
const edgesByType = {
|
|
4323
|
+
requires: 0,
|
|
4324
|
+
extends: 0,
|
|
4325
|
+
conflicts: 0,
|
|
4326
|
+
enhances: 0,
|
|
4327
|
+
precedes: 0,
|
|
4328
|
+
follows: 0
|
|
4329
|
+
};
|
|
4330
|
+
for (const edge of this.edges) {
|
|
4331
|
+
edgesByType[edge.type]++;
|
|
4332
|
+
}
|
|
4333
|
+
return {
|
|
4334
|
+
nodeCount: this.nodes.size,
|
|
4335
|
+
edgeCount: this.edges.length,
|
|
4336
|
+
edgesByType
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Clear the graph
|
|
4341
|
+
*/
|
|
4342
|
+
clear() {
|
|
4343
|
+
this.nodes.clear();
|
|
4344
|
+
this.edges = [];
|
|
4345
|
+
this.adjacencyList.clear();
|
|
4346
|
+
this.reverseAdjacencyList.clear();
|
|
4347
|
+
}
|
|
4348
|
+
/**
|
|
4349
|
+
* Export graph to JSON
|
|
4350
|
+
*/
|
|
4351
|
+
toJSON() {
|
|
4352
|
+
return {
|
|
4353
|
+
nodes: Array.from(this.nodes.values()).map((n) => ({
|
|
4354
|
+
skillId: n.skillId,
|
|
4355
|
+
version: n.version
|
|
4356
|
+
})),
|
|
4357
|
+
edges: this.edges
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4360
|
+
/**
|
|
4361
|
+
* Import graph from JSON (requires skills to be added separately)
|
|
4362
|
+
*/
|
|
4363
|
+
fromJSON(data) {
|
|
4364
|
+
for (const edge of data.edges) {
|
|
4365
|
+
this.addDependency(edge.from, edge.to, edge.type, {
|
|
4366
|
+
required: edge.required,
|
|
4367
|
+
description: edge.description
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
// ===========================================================================
|
|
4372
|
+
// Private helpers
|
|
4373
|
+
// ===========================================================================
|
|
4374
|
+
getKey(skillId, version) {
|
|
4375
|
+
return version ? `${skillId}@${version}` : skillId;
|
|
4376
|
+
}
|
|
4377
|
+
};
|
|
4378
|
+
|
|
4379
|
+
// src/composer/composer.ts
|
|
4380
|
+
var SkillComposer = class {
|
|
4381
|
+
constructor(config = {}) {
|
|
4382
|
+
this.skillCache = /* @__PURE__ */ new Map();
|
|
4383
|
+
this.config = {
|
|
4384
|
+
storage: config.storage,
|
|
4385
|
+
embeddingProvider: config.embeddingProvider || new SimpleHashEmbedding(),
|
|
4386
|
+
suggestionThreshold: config.suggestionThreshold ?? 0.6,
|
|
4387
|
+
maxDependencyDepth: config.maxDependencyDepth ?? 10
|
|
4388
|
+
};
|
|
4389
|
+
this.graph = new DependencyGraph();
|
|
4390
|
+
this.matcher = new SemanticMatcher({
|
|
4391
|
+
embeddingProvider: this.config.embeddingProvider,
|
|
4392
|
+
similarityThreshold: this.config.suggestionThreshold
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
/**
|
|
4396
|
+
* Compose multiple skills into a compound skill
|
|
4397
|
+
*/
|
|
4398
|
+
async compose(skillIds, options) {
|
|
4399
|
+
const conflicts = [];
|
|
4400
|
+
const warnings = [];
|
|
4401
|
+
const missingDependencies = [];
|
|
4402
|
+
const skills = [];
|
|
4403
|
+
for (const id of skillIds) {
|
|
4404
|
+
const skill = await this.getSkill(id);
|
|
4405
|
+
if (!skill) {
|
|
4406
|
+
missingDependencies.push(id);
|
|
4407
|
+
continue;
|
|
4408
|
+
}
|
|
4409
|
+
skills.push(skill);
|
|
4410
|
+
}
|
|
4411
|
+
if (missingDependencies.length > 0) {
|
|
4412
|
+
return {
|
|
4413
|
+
success: false,
|
|
4414
|
+
conflicts,
|
|
4415
|
+
warnings,
|
|
4416
|
+
missingDependencies
|
|
4417
|
+
};
|
|
4418
|
+
}
|
|
4419
|
+
const detectedConflicts = this.detectConflicts(skills);
|
|
4420
|
+
conflicts.push(...detectedConflicts);
|
|
4421
|
+
const blockingConflicts = conflicts.filter((c) => c.severity === "error");
|
|
4422
|
+
if (blockingConflicts.length > 0) {
|
|
4423
|
+
return {
|
|
4424
|
+
success: false,
|
|
4425
|
+
conflicts,
|
|
4426
|
+
warnings,
|
|
4427
|
+
missingDependencies
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
for (const conflict of conflicts.filter((c) => c.severity === "warning")) {
|
|
4431
|
+
warnings.push(conflict.description);
|
|
4432
|
+
}
|
|
4433
|
+
const depCheck = this.graph.checkDependenciesSatisfied(skillIds);
|
|
4434
|
+
if (!depCheck.satisfied) {
|
|
4435
|
+
warnings.push(
|
|
4436
|
+
`Missing optional dependencies: ${depCheck.missing.join(", ")}`
|
|
4437
|
+
);
|
|
4438
|
+
}
|
|
4439
|
+
const orderedIds = options.mode === "sequential" ? this.getExecutionOrder(skillIds, options.executionOrder) : skillIds;
|
|
4440
|
+
const components = orderedIds.map((id, index) => ({
|
|
4441
|
+
skillId: id,
|
|
4442
|
+
priority: index
|
|
4443
|
+
}));
|
|
4444
|
+
const compoundSkill = {
|
|
4445
|
+
id: this.generateId(options.name),
|
|
4446
|
+
name: options.name,
|
|
4447
|
+
description: options.description || this.generateDescription(skills),
|
|
4448
|
+
problem: this.mergeProblemStatements(skills),
|
|
4449
|
+
triggerConditions: this.mergeTriggerConditions(skills),
|
|
4450
|
+
solution: this.generateSolutionSteps(skills, options.mode || "sequential"),
|
|
4451
|
+
verification: this.mergeVerifications(skills),
|
|
4452
|
+
examples: [],
|
|
4453
|
+
notes: `Composed from: ${skillIds.join(", ")}`,
|
|
4454
|
+
author: "skill-composer",
|
|
4455
|
+
tags: this.mergeTags(skills),
|
|
4456
|
+
version: "1.0.0",
|
|
4457
|
+
status: "draft",
|
|
4458
|
+
metrics: {
|
|
4459
|
+
usageCount: 0,
|
|
4460
|
+
successRate: 0,
|
|
4461
|
+
feedbackScores: []
|
|
4462
|
+
},
|
|
4463
|
+
source: {
|
|
4464
|
+
type: "composed",
|
|
4465
|
+
importedAt: /* @__PURE__ */ new Date()
|
|
4466
|
+
},
|
|
4467
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
4468
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
4469
|
+
derivedFrom: skillIds,
|
|
4470
|
+
isCompound: true,
|
|
4471
|
+
composition: {
|
|
4472
|
+
mode: options.mode || "sequential",
|
|
4473
|
+
executionOrder: options.executionOrder,
|
|
4474
|
+
components,
|
|
4475
|
+
stopOnFailure: options.stopOnFailure ?? true
|
|
4476
|
+
}
|
|
4477
|
+
};
|
|
4478
|
+
return {
|
|
4479
|
+
success: true,
|
|
4480
|
+
skill: compoundSkill,
|
|
4481
|
+
conflicts,
|
|
4482
|
+
warnings,
|
|
4483
|
+
missingDependencies
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* Decompose a compound skill into its components
|
|
4488
|
+
*/
|
|
4489
|
+
async decompose(skill) {
|
|
4490
|
+
const skills = [];
|
|
4491
|
+
for (const component of skill.composition.components) {
|
|
4492
|
+
const componentSkill = await this.getSkill(
|
|
4493
|
+
component.skillId,
|
|
4494
|
+
component.version
|
|
4495
|
+
);
|
|
4496
|
+
if (componentSkill) {
|
|
4497
|
+
skills.push(componentSkill);
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
return skills;
|
|
4501
|
+
}
|
|
4502
|
+
/**
|
|
4503
|
+
* Add a dependency relationship between skills
|
|
4504
|
+
*/
|
|
4505
|
+
addDependency(fromId, toId, type, options) {
|
|
4506
|
+
this.graph.addDependency(fromId, toId, type, options);
|
|
4507
|
+
}
|
|
4508
|
+
/**
|
|
4509
|
+
* Remove a dependency relationship
|
|
4510
|
+
*/
|
|
4511
|
+
removeDependency(fromId, toId, type) {
|
|
4512
|
+
this.graph.removeDependency(fromId, toId, type);
|
|
4513
|
+
}
|
|
4514
|
+
/**
|
|
4515
|
+
* Get dependencies for a skill
|
|
4516
|
+
*/
|
|
4517
|
+
getDependencies(skillId) {
|
|
4518
|
+
return this.graph.getDependencies(skillId);
|
|
4519
|
+
}
|
|
4520
|
+
/**
|
|
4521
|
+
* Get skills that depend on this skill
|
|
4522
|
+
*/
|
|
4523
|
+
getDependents(skillId) {
|
|
4524
|
+
return this.graph.getDependents(skillId);
|
|
4525
|
+
}
|
|
4526
|
+
/**
|
|
4527
|
+
* Detect conflicts between skills
|
|
4528
|
+
*/
|
|
4529
|
+
detectConflicts(skills) {
|
|
4530
|
+
const conflicts = [];
|
|
4531
|
+
for (let i = 0; i < skills.length; i++) {
|
|
4532
|
+
for (let j = i + 1; j < skills.length; j++) {
|
|
4533
|
+
const skill1 = skills[i];
|
|
4534
|
+
const skill2 = skills[j];
|
|
4535
|
+
if (this.graph.hasConflict(skill1.id, skill2.id)) {
|
|
4536
|
+
conflicts.push({
|
|
4537
|
+
type: "direct",
|
|
4538
|
+
skillId1: skill1.id,
|
|
4539
|
+
skillId2: skill2.id,
|
|
4540
|
+
description: `Skills "${skill1.name}" and "${skill2.name}" have declared conflicts`,
|
|
4541
|
+
severity: "error"
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
const triggerOverlap = this.checkTriggerOverlap(skill1, skill2);
|
|
4545
|
+
if (triggerOverlap) {
|
|
4546
|
+
conflicts.push({
|
|
4547
|
+
type: "trigger-overlap",
|
|
4548
|
+
skillId1: skill1.id,
|
|
4549
|
+
skillId2: skill2.id,
|
|
4550
|
+
description: `Skills have overlapping trigger conditions: ${triggerOverlap}`,
|
|
4551
|
+
severity: "warning"
|
|
4552
|
+
});
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
return conflicts;
|
|
4557
|
+
}
|
|
4558
|
+
/**
|
|
4559
|
+
* Validate a composition without creating it
|
|
4560
|
+
*/
|
|
4561
|
+
async validateComposition(skillIds) {
|
|
4562
|
+
const skills = [];
|
|
4563
|
+
const missingDependencies = [];
|
|
4564
|
+
const warnings = [];
|
|
4565
|
+
for (const id of skillIds) {
|
|
4566
|
+
const skill = await this.getSkill(id);
|
|
4567
|
+
if (!skill) {
|
|
4568
|
+
missingDependencies.push(id);
|
|
4569
|
+
} else {
|
|
4570
|
+
skills.push(skill);
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
const conflicts = this.detectConflicts(skills);
|
|
4574
|
+
const blockingConflicts = conflicts.filter((c) => c.severity === "error");
|
|
4575
|
+
const cycleResult = this.graph.detectCycle();
|
|
4576
|
+
if (cycleResult.hasCycle) {
|
|
4577
|
+
warnings.push(
|
|
4578
|
+
`Circular dependency detected: ${cycleResult.cycle?.join(" -> ")}`
|
|
4579
|
+
);
|
|
4580
|
+
}
|
|
4581
|
+
return {
|
|
4582
|
+
valid: blockingConflicts.length === 0 && missingDependencies.length === 0,
|
|
4583
|
+
conflicts,
|
|
4584
|
+
missingDependencies,
|
|
4585
|
+
warnings
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
/**
|
|
4589
|
+
* Suggest skill compositions based on usage patterns and similarity
|
|
4590
|
+
*/
|
|
4591
|
+
async suggestCompositions(skills, options) {
|
|
4592
|
+
const suggestions = [];
|
|
4593
|
+
const maxSuggestions = options?.maxSuggestions ?? 5;
|
|
4594
|
+
const minConfidence = options?.minConfidence ?? this.config.suggestionThreshold;
|
|
4595
|
+
for (let i = 0; i < skills.length; i++) {
|
|
4596
|
+
const similar = await this.matcher.findSimilar(skills[i], skills, {
|
|
4597
|
+
threshold: minConfidence,
|
|
4598
|
+
maxResults: 3
|
|
4599
|
+
});
|
|
4600
|
+
for (const match of similar) {
|
|
4601
|
+
if (match.skill.id === skills[i].id) continue;
|
|
4602
|
+
const existing = suggestions.find(
|
|
4603
|
+
(s) => s.skillIds.includes(skills[i].id) && s.skillIds.includes(match.skill.id)
|
|
4604
|
+
);
|
|
4605
|
+
if (existing) continue;
|
|
4606
|
+
const mode = this.suggestCompositionMode(skills[i], match.skill);
|
|
4607
|
+
suggestions.push({
|
|
4608
|
+
skillIds: [skills[i].id, match.skill.id],
|
|
4609
|
+
mode,
|
|
4610
|
+
confidence: match.similarity,
|
|
4611
|
+
reason: `Similar ${this.getSimilarityReason(skills[i], match.skill)}`,
|
|
4612
|
+
benefit: this.estimateBenefit(skills[i], match.skill, mode)
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
const dependencyPairs = this.findCommonDependencyPairs(skills);
|
|
4617
|
+
for (const pair of dependencyPairs) {
|
|
4618
|
+
const existing = suggestions.find(
|
|
4619
|
+
(s) => s.skillIds.includes(pair[0]) && s.skillIds.includes(pair[1])
|
|
4620
|
+
);
|
|
4621
|
+
if (!existing) {
|
|
4622
|
+
suggestions.push({
|
|
4623
|
+
skillIds: pair,
|
|
4624
|
+
mode: "sequential",
|
|
4625
|
+
confidence: 0.7,
|
|
4626
|
+
reason: "Frequently used together based on dependency patterns",
|
|
4627
|
+
benefit: "Streamlined workflow"
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
return suggestions.sort((a, b) => b.confidence - a.confidence).slice(0, maxSuggestions);
|
|
4632
|
+
}
|
|
4633
|
+
/**
|
|
4634
|
+
* Index skills from storage for composition suggestions
|
|
4635
|
+
*/
|
|
4636
|
+
async indexFromStorage(storage) {
|
|
4637
|
+
const skills = await storage.listSkills();
|
|
4638
|
+
for (const skill of skills) {
|
|
4639
|
+
this.graph.addSkill(skill);
|
|
4640
|
+
this.skillCache.set(skill.id, skill);
|
|
4641
|
+
if (skill.derivedFrom) {
|
|
4642
|
+
for (const parentId of skill.derivedFrom) {
|
|
4643
|
+
this.graph.addDependency(skill.id, parentId, "extends");
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
await this.matcher.indexSkills(skills);
|
|
4648
|
+
return skills.length;
|
|
4649
|
+
}
|
|
4650
|
+
/**
|
|
4651
|
+
* Get dependency graph
|
|
4652
|
+
*/
|
|
4653
|
+
getGraph() {
|
|
4654
|
+
return this.graph;
|
|
4655
|
+
}
|
|
4656
|
+
/**
|
|
4657
|
+
* Clear caches and graph
|
|
4658
|
+
*/
|
|
4659
|
+
clear() {
|
|
4660
|
+
this.graph.clear();
|
|
4661
|
+
this.skillCache.clear();
|
|
4662
|
+
this.matcher.clearCache();
|
|
4663
|
+
}
|
|
4664
|
+
// ===========================================================================
|
|
4665
|
+
// Private helpers
|
|
4666
|
+
// ===========================================================================
|
|
4667
|
+
async getSkill(id, version) {
|
|
4668
|
+
const cacheKey = version ? `${id}@${version}` : id;
|
|
4669
|
+
if (this.skillCache.has(cacheKey)) {
|
|
4670
|
+
return this.skillCache.get(cacheKey);
|
|
4671
|
+
}
|
|
4672
|
+
if (this.config.storage) {
|
|
4673
|
+
const skill = await this.config.storage.getSkill(id, version);
|
|
4674
|
+
if (skill) {
|
|
4675
|
+
this.skillCache.set(cacheKey, skill);
|
|
4676
|
+
return skill;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
return null;
|
|
4680
|
+
}
|
|
4681
|
+
generateId(name) {
|
|
4682
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4683
|
+
}
|
|
4684
|
+
generateDescription(skills) {
|
|
4685
|
+
return `Combines: ${skills.map((s) => s.name).join(", ")}`;
|
|
4686
|
+
}
|
|
4687
|
+
mergeProblemStatements(skills) {
|
|
4688
|
+
const problems = skills.map((s) => `- ${s.problem}`);
|
|
4689
|
+
return `This compound skill addresses:
|
|
4690
|
+
${problems.join("\n")}`;
|
|
4691
|
+
}
|
|
4692
|
+
mergeTriggerConditions(skills) {
|
|
4693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4694
|
+
const triggers = [];
|
|
4695
|
+
for (const skill of skills) {
|
|
4696
|
+
for (const trigger of skill.triggerConditions) {
|
|
4697
|
+
const key = `${trigger.type}:${trigger.value}`;
|
|
4698
|
+
if (!seen.has(key)) {
|
|
4699
|
+
seen.add(key);
|
|
4700
|
+
triggers.push(trigger);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
return triggers;
|
|
4705
|
+
}
|
|
4706
|
+
generateSolutionSteps(skills, mode) {
|
|
4707
|
+
const steps = [];
|
|
4708
|
+
switch (mode) {
|
|
4709
|
+
case "sequential":
|
|
4710
|
+
steps.push("Execute the following skills in order:");
|
|
4711
|
+
skills.forEach((s, i) => {
|
|
4712
|
+
steps.push(`
|
|
4713
|
+
## Step ${i + 1}: ${s.name}
|
|
4714
|
+
${s.solution}`);
|
|
4715
|
+
});
|
|
4716
|
+
break;
|
|
4717
|
+
case "parallel":
|
|
4718
|
+
steps.push("Execute the following skills (can be done in parallel):");
|
|
4719
|
+
skills.forEach((s) => {
|
|
4720
|
+
steps.push(`
|
|
4721
|
+
## ${s.name}
|
|
4722
|
+
${s.solution}`);
|
|
4723
|
+
});
|
|
4724
|
+
break;
|
|
4725
|
+
case "conditional":
|
|
4726
|
+
steps.push("Execute skills based on conditions:");
|
|
4727
|
+
skills.forEach((s) => {
|
|
4728
|
+
const triggers = s.triggerConditions.map((t) => t.value).join(" OR ");
|
|
4729
|
+
steps.push(`
|
|
4730
|
+
## If ${triggers}:
|
|
4731
|
+
${s.solution}`);
|
|
4732
|
+
});
|
|
4733
|
+
break;
|
|
4734
|
+
case "fallback":
|
|
4735
|
+
steps.push("Try skills in order until one succeeds:");
|
|
4736
|
+
skills.forEach((s, i) => {
|
|
4737
|
+
steps.push(`
|
|
4738
|
+
## Option ${i + 1}: ${s.name}
|
|
4739
|
+
${s.solution}`);
|
|
4740
|
+
});
|
|
4741
|
+
break;
|
|
4742
|
+
}
|
|
4743
|
+
return steps.join("\n");
|
|
4744
|
+
}
|
|
4745
|
+
mergeVerifications(skills) {
|
|
4746
|
+
const verifications = skills.map(
|
|
4747
|
+
(s) => `- ${s.name}: ${s.verification}`
|
|
4748
|
+
);
|
|
4749
|
+
return `Verify all components:
|
|
4750
|
+
${verifications.join("\n")}`;
|
|
4751
|
+
}
|
|
4752
|
+
mergeTags(skills) {
|
|
4753
|
+
const tags = /* @__PURE__ */ new Set();
|
|
4754
|
+
for (const skill of skills) {
|
|
4755
|
+
for (const tag of skill.tags) {
|
|
4756
|
+
tags.add(tag);
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
tags.add("compound");
|
|
4760
|
+
return Array.from(tags);
|
|
4761
|
+
}
|
|
4762
|
+
getExecutionOrder(skillIds, order) {
|
|
4763
|
+
switch (order) {
|
|
4764
|
+
case "dependency":
|
|
4765
|
+
return this.graph.getTopologicalOrder(skillIds);
|
|
4766
|
+
case "priority":
|
|
4767
|
+
return skillIds;
|
|
4768
|
+
case "fixed":
|
|
4769
|
+
default:
|
|
4770
|
+
return skillIds;
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
checkTriggerOverlap(skill1, skill2) {
|
|
4774
|
+
const triggers1 = skill1.triggerConditions.map((t) => t.value.toLowerCase());
|
|
4775
|
+
const triggers2 = skill2.triggerConditions.map((t) => t.value.toLowerCase());
|
|
4776
|
+
for (const t1 of triggers1) {
|
|
4777
|
+
for (const t2 of triggers2) {
|
|
4778
|
+
if (t1 === t2 || t1.includes(t2) || t2.includes(t1)) {
|
|
4779
|
+
return t1;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
return null;
|
|
4784
|
+
}
|
|
4785
|
+
suggestCompositionMode(skill1, skill2) {
|
|
4786
|
+
if (skill1.solution.toLowerCase().includes(skill2.problem.toLowerCase().slice(0, 20)) || skill2.solution.toLowerCase().includes(skill1.problem.toLowerCase().slice(0, 20))) {
|
|
4787
|
+
return "sequential";
|
|
4788
|
+
}
|
|
4789
|
+
const overlap = this.checkTriggerOverlap(skill1, skill2);
|
|
4790
|
+
if (!overlap) {
|
|
4791
|
+
return "conditional";
|
|
4792
|
+
}
|
|
4793
|
+
return "fallback";
|
|
4794
|
+
}
|
|
4795
|
+
getSimilarityReason(skill1, skill2) {
|
|
4796
|
+
const reasons = [];
|
|
4797
|
+
const problem1Words = new Set(skill1.problem.toLowerCase().split(/\s+/));
|
|
4798
|
+
const problem2Words = new Set(skill2.problem.toLowerCase().split(/\s+/));
|
|
4799
|
+
const problemOverlap = [...problem1Words].filter((w) => problem2Words.has(w));
|
|
4800
|
+
if (problemOverlap.length > 3) {
|
|
4801
|
+
reasons.push("problem statements");
|
|
4802
|
+
}
|
|
4803
|
+
const tagOverlap = skill1.tags.filter((t) => skill2.tags.includes(t));
|
|
4804
|
+
if (tagOverlap.length > 0) {
|
|
4805
|
+
reasons.push(`tags (${tagOverlap.join(", ")})`);
|
|
4806
|
+
}
|
|
4807
|
+
if (this.checkTriggerOverlap(skill1, skill2)) {
|
|
4808
|
+
reasons.push("trigger conditions");
|
|
4809
|
+
}
|
|
4810
|
+
return reasons.length > 0 ? reasons.join(", ") : "content";
|
|
4811
|
+
}
|
|
4812
|
+
estimateBenefit(skill1, skill2, mode) {
|
|
4813
|
+
switch (mode) {
|
|
4814
|
+
case "sequential":
|
|
4815
|
+
return "Combined workflow reduces manual steps";
|
|
4816
|
+
case "parallel":
|
|
4817
|
+
return "Parallel execution saves time";
|
|
4818
|
+
case "conditional":
|
|
4819
|
+
return "Smart routing to appropriate skill";
|
|
4820
|
+
case "fallback":
|
|
4821
|
+
return "Increased reliability with backup options";
|
|
4822
|
+
default:
|
|
4823
|
+
return "Unified skill management";
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
findCommonDependencyPairs(skills) {
|
|
4827
|
+
const pairs = [];
|
|
4828
|
+
const skillIds = new Set(skills.map((s) => s.id));
|
|
4829
|
+
for (const skill of skills) {
|
|
4830
|
+
const deps = this.graph.getDependencies(skill.id);
|
|
4831
|
+
for (const dep of deps) {
|
|
4832
|
+
if (skillIds.has(dep.to) && dep.type !== "conflicts") {
|
|
4833
|
+
pairs.push([skill.id, dep.to]);
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
return pairs;
|
|
4838
|
+
}
|
|
4839
|
+
};
|
|
4840
|
+
|
|
4841
|
+
// src/composer/types.ts
|
|
4842
|
+
function isCompoundSkill(skill) {
|
|
4843
|
+
return "isCompound" in skill && skill.isCompound === true;
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4846
|
+
// src/index.ts
|
|
4847
|
+
var VERSION = "0.1.0";
|
|
4848
|
+
|
|
4849
|
+
export {
|
|
4850
|
+
BaseSessionAdapter,
|
|
4851
|
+
AdapterRegistry,
|
|
4852
|
+
adapterRegistry,
|
|
4853
|
+
ClaudeCodeAdapter,
|
|
4854
|
+
claudeCodeAdapter,
|
|
4855
|
+
OpenAIAdapter,
|
|
4856
|
+
openAIAdapter,
|
|
4857
|
+
AnthropicAdapter,
|
|
4858
|
+
anthropicAdapter,
|
|
4859
|
+
GenericAdapter,
|
|
4860
|
+
createGenericAdapter,
|
|
4861
|
+
genericAdapter,
|
|
4862
|
+
DEFAULT_QUALITY_GATES,
|
|
4863
|
+
QualityGateEvaluator,
|
|
4864
|
+
BaseExtractor,
|
|
4865
|
+
ManualExtractor,
|
|
4866
|
+
AutomaticExtractor,
|
|
4867
|
+
BaseStorageAdapter,
|
|
4868
|
+
MemoryStorageAdapter,
|
|
4869
|
+
FilesystemStorageAdapter,
|
|
4870
|
+
parseVersion,
|
|
4871
|
+
formatVersion,
|
|
4872
|
+
isValidVersion,
|
|
4873
|
+
compareVersions,
|
|
4874
|
+
bumpVersion,
|
|
4875
|
+
satisfiesRange,
|
|
4876
|
+
getLatestVersion,
|
|
4877
|
+
sortVersions,
|
|
4878
|
+
inferBumpType,
|
|
4879
|
+
LineageTracker,
|
|
4880
|
+
SimpleHashEmbedding,
|
|
4881
|
+
OpenAIEmbedding,
|
|
4882
|
+
VoyageEmbedding,
|
|
4883
|
+
cosineSimilarity,
|
|
4884
|
+
euclideanDistance,
|
|
4885
|
+
SemanticMatcher,
|
|
4886
|
+
SkillBank,
|
|
4887
|
+
createSkillBank,
|
|
4888
|
+
BatchProcessor,
|
|
4889
|
+
DependencyGraph,
|
|
4890
|
+
SkillComposer,
|
|
4891
|
+
isCompoundSkill,
|
|
4892
|
+
VERSION
|
|
4893
|
+
};
|