@elizaos/plugin-agent-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +372 -0
- package/dist/index.js +3698 -0
- package/package.json +83 -0
- package/src/__tests__/clawhub.test.ts +722 -0
- package/src/__tests__/integration.test.ts +465 -0
- package/src/__tests__/parser.test.ts +304 -0
- package/src/__tests__/skill-eligibility.test.ts +575 -0
- package/src/__tests__/skill-precedence.test.ts +592 -0
- package/src/__tests__/storage.test.ts +549 -0
- package/src/actions/get-skill-details.ts +127 -0
- package/src/actions/get-skill-guidance.ts +388 -0
- package/src/actions/run-skill-script.ts +200 -0
- package/src/actions/search-skills.ts +106 -0
- package/src/actions/sync-catalog.ts +88 -0
- package/src/index.ts +124 -0
- package/src/parser.ts +478 -0
- package/src/plugin.ts +118 -0
- package/src/providers/skills.ts +443 -0
- package/src/services/install.ts +628 -0
- package/src/services/skills.ts +2363 -0
- package/src/storage.ts +544 -0
- package/src/types.ts +582 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +18 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3698 @@
|
|
|
1
|
+
// src/services/skills.ts
|
|
2
|
+
import { Service } from "@elizaos/core";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var SKILL_NAME_MAX_LENGTH = 64;
|
|
6
|
+
var SKILL_DESCRIPTION_MAX_LENGTH = 1024;
|
|
7
|
+
var SKILL_COMPATIBILITY_MAX_LENGTH = 500;
|
|
8
|
+
var SKILL_BODY_RECOMMENDED_TOKENS = 5e3;
|
|
9
|
+
var SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10
|
+
var SKILL_SOURCE_PRECEDENCE = {
|
|
11
|
+
workspace: 5,
|
|
12
|
+
managed: 4,
|
|
13
|
+
bundled: 3,
|
|
14
|
+
plugin: 2,
|
|
15
|
+
extra: 1
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/parser.ts
|
|
19
|
+
function parseFrontmatter(content) {
|
|
20
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
21
|
+
if (!match) {
|
|
22
|
+
return { frontmatter: null, body: content, raw: "" };
|
|
23
|
+
}
|
|
24
|
+
const raw = match[1];
|
|
25
|
+
const body = content.slice(match[0].length).trim();
|
|
26
|
+
try {
|
|
27
|
+
const frontmatter = parseYamlSubset(raw);
|
|
28
|
+
return { frontmatter, body, raw };
|
|
29
|
+
} catch {
|
|
30
|
+
return { frontmatter: null, body, raw };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function parseYamlSubset(yaml) {
|
|
34
|
+
const result = {};
|
|
35
|
+
const lines = yaml.split("\n");
|
|
36
|
+
let currentKey = "";
|
|
37
|
+
let currentIndent = 0;
|
|
38
|
+
const stack = [
|
|
39
|
+
{ obj: result, indent: -1 }
|
|
40
|
+
];
|
|
41
|
+
let collectingJson = false;
|
|
42
|
+
let jsonBuffer = "";
|
|
43
|
+
let jsonDepth = 0;
|
|
44
|
+
let jsonKey = "";
|
|
45
|
+
let jsonParent = null;
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (collectingJson) {
|
|
50
|
+
if (!trimmed) continue;
|
|
51
|
+
jsonBuffer += trimmed;
|
|
52
|
+
let inString = false;
|
|
53
|
+
let escape = false;
|
|
54
|
+
for (const char of trimmed) {
|
|
55
|
+
if (escape) {
|
|
56
|
+
escape = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (char === "\\") {
|
|
60
|
+
escape = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (char === '"') {
|
|
64
|
+
inString = !inString;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!inString) {
|
|
68
|
+
if (char === "{" || char === "[") jsonDepth++;
|
|
69
|
+
else if (char === "}" || char === "]") jsonDepth--;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (jsonDepth === 0) {
|
|
73
|
+
try {
|
|
74
|
+
const cleanedJson = jsonBuffer.replace(/,(\s*[}\]])/g, "$1");
|
|
75
|
+
if (jsonParent) {
|
|
76
|
+
jsonParent[jsonKey] = JSON.parse(cleanedJson);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
if (jsonParent) {
|
|
80
|
+
jsonParent[jsonKey] = jsonBuffer;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
collectingJson = false;
|
|
84
|
+
jsonBuffer = "";
|
|
85
|
+
jsonKey = "";
|
|
86
|
+
jsonParent = null;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
91
|
+
const indent = line.search(/\S/);
|
|
92
|
+
const kvMatch = trimmed.match(/^([a-zA-Z0-9_-]+):\s*(.*)/);
|
|
93
|
+
if (kvMatch) {
|
|
94
|
+
const [, key, valueStr] = kvMatch;
|
|
95
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
96
|
+
stack.pop();
|
|
97
|
+
}
|
|
98
|
+
const parent = stack[stack.length - 1].obj;
|
|
99
|
+
if (valueStr === "" || valueStr === "|" || valueStr === ">") {
|
|
100
|
+
let nextLineIdx = i + 1;
|
|
101
|
+
while (nextLineIdx < lines.length && !lines[nextLineIdx].trim()) {
|
|
102
|
+
nextLineIdx++;
|
|
103
|
+
}
|
|
104
|
+
const nextTrimmed = nextLineIdx < lines.length ? lines[nextLineIdx].trim() : "";
|
|
105
|
+
if (nextTrimmed.startsWith("{") || nextTrimmed.startsWith("[")) {
|
|
106
|
+
jsonKey = key;
|
|
107
|
+
jsonParent = parent;
|
|
108
|
+
jsonBuffer = "";
|
|
109
|
+
jsonDepth = 0;
|
|
110
|
+
collectingJson = true;
|
|
111
|
+
} else {
|
|
112
|
+
const childObj = {};
|
|
113
|
+
parent[key] = childObj;
|
|
114
|
+
stack.push({ obj: childObj, indent });
|
|
115
|
+
currentKey = key;
|
|
116
|
+
currentIndent = indent;
|
|
117
|
+
}
|
|
118
|
+
} else if (valueStr.startsWith("{") || valueStr.startsWith("[")) {
|
|
119
|
+
let depth = 0;
|
|
120
|
+
let inString = false;
|
|
121
|
+
let escape = false;
|
|
122
|
+
for (const char of valueStr) {
|
|
123
|
+
if (escape) {
|
|
124
|
+
escape = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (char === "\\") {
|
|
128
|
+
escape = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (char === '"') {
|
|
132
|
+
inString = !inString;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!inString) {
|
|
136
|
+
if (char === "{" || char === "[") depth++;
|
|
137
|
+
else if (char === "}" || char === "]") depth--;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (depth === 0) {
|
|
141
|
+
try {
|
|
142
|
+
const cleanedJson = valueStr.replace(/,(\s*[}\]])/g, "$1");
|
|
143
|
+
parent[key] = JSON.parse(cleanedJson);
|
|
144
|
+
} catch {
|
|
145
|
+
parent[key] = valueStr;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
jsonKey = key;
|
|
149
|
+
jsonParent = parent;
|
|
150
|
+
jsonBuffer = valueStr;
|
|
151
|
+
jsonDepth = depth;
|
|
152
|
+
collectingJson = true;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
parent[key] = parseYamlValue(valueStr);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function parseYamlValue(value) {
|
|
162
|
+
const trimmed = value.trim();
|
|
163
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
164
|
+
return trimmed.slice(1, -1);
|
|
165
|
+
}
|
|
166
|
+
if (trimmed === "true") return true;
|
|
167
|
+
if (trimmed === "false") return false;
|
|
168
|
+
if (trimmed === "null" || trimmed === "~") return null;
|
|
169
|
+
if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
|
|
170
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
|
|
171
|
+
return trimmed;
|
|
172
|
+
}
|
|
173
|
+
function validateFrontmatter(frontmatter, directoryName) {
|
|
174
|
+
const errors = [];
|
|
175
|
+
const warnings = [];
|
|
176
|
+
if (!frontmatter.name) {
|
|
177
|
+
errors.push({
|
|
178
|
+
field: "name",
|
|
179
|
+
message: "name is required",
|
|
180
|
+
code: "MISSING_NAME"
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
if (frontmatter.name.length > SKILL_NAME_MAX_LENGTH) {
|
|
184
|
+
errors.push({
|
|
185
|
+
field: "name",
|
|
186
|
+
message: `name must be ${SKILL_NAME_MAX_LENGTH} characters or less`,
|
|
187
|
+
code: "NAME_TOO_LONG"
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (!SKILL_NAME_PATTERN.test(frontmatter.name)) {
|
|
191
|
+
errors.push({
|
|
192
|
+
field: "name",
|
|
193
|
+
message: "name must contain only lowercase letters, numbers, and hyphens, cannot start/end with hyphen or have consecutive hyphens",
|
|
194
|
+
code: "INVALID_NAME_FORMAT"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (frontmatter.name.startsWith("-") || frontmatter.name.endsWith("-")) {
|
|
198
|
+
errors.push({
|
|
199
|
+
field: "name",
|
|
200
|
+
message: "name cannot start or end with a hyphen",
|
|
201
|
+
code: "NAME_INVALID_HYPHEN"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (frontmatter.name.includes("--")) {
|
|
205
|
+
errors.push({
|
|
206
|
+
field: "name",
|
|
207
|
+
message: "name cannot contain consecutive hyphens",
|
|
208
|
+
code: "NAME_CONSECUTIVE_HYPHENS"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (directoryName && directoryName !== frontmatter.name) {
|
|
212
|
+
errors.push({
|
|
213
|
+
field: "name",
|
|
214
|
+
message: `name "${frontmatter.name}" must match directory name "${directoryName}"`,
|
|
215
|
+
code: "NAME_MISMATCH"
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!frontmatter.description) {
|
|
220
|
+
errors.push({
|
|
221
|
+
field: "description",
|
|
222
|
+
message: "description is required",
|
|
223
|
+
code: "MISSING_DESCRIPTION"
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
if (frontmatter.description.length > SKILL_DESCRIPTION_MAX_LENGTH) {
|
|
227
|
+
errors.push({
|
|
228
|
+
field: "description",
|
|
229
|
+
message: `description must be ${SKILL_DESCRIPTION_MAX_LENGTH} characters or less`,
|
|
230
|
+
code: "DESCRIPTION_TOO_LONG"
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (frontmatter.description.length < 20) {
|
|
234
|
+
warnings.push({
|
|
235
|
+
field: "description",
|
|
236
|
+
message: "description is very short; consider adding more detail about when to use this skill",
|
|
237
|
+
code: "DESCRIPTION_TOO_SHORT"
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (frontmatter.compatibility) {
|
|
242
|
+
if (frontmatter.compatibility.length > SKILL_COMPATIBILITY_MAX_LENGTH) {
|
|
243
|
+
errors.push({
|
|
244
|
+
field: "compatibility",
|
|
245
|
+
message: `compatibility must be ${SKILL_COMPATIBILITY_MAX_LENGTH} characters or less`,
|
|
246
|
+
code: "COMPATIBILITY_TOO_LONG"
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
valid: errors.length === 0,
|
|
252
|
+
errors,
|
|
253
|
+
warnings
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function validateSkillDirectory(path2, content, directoryName) {
|
|
257
|
+
const errors = [];
|
|
258
|
+
const warnings = [];
|
|
259
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
260
|
+
if (!frontmatter) {
|
|
261
|
+
errors.push({
|
|
262
|
+
field: "frontmatter",
|
|
263
|
+
message: "SKILL.md must have valid YAML frontmatter",
|
|
264
|
+
code: "MISSING_FRONTMATTER"
|
|
265
|
+
});
|
|
266
|
+
return { valid: false, errors, warnings };
|
|
267
|
+
}
|
|
268
|
+
const fmResult = validateFrontmatter(frontmatter, directoryName);
|
|
269
|
+
errors.push(...fmResult.errors);
|
|
270
|
+
warnings.push(...fmResult.warnings);
|
|
271
|
+
return {
|
|
272
|
+
valid: errors.length === 0,
|
|
273
|
+
errors,
|
|
274
|
+
warnings
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function extractBody(content) {
|
|
278
|
+
const { body } = parseFrontmatter(content);
|
|
279
|
+
return body;
|
|
280
|
+
}
|
|
281
|
+
function estimateTokens(text) {
|
|
282
|
+
return Math.ceil(text.length / 4);
|
|
283
|
+
}
|
|
284
|
+
function generateSkillsXml(skills, options = {}) {
|
|
285
|
+
if (skills.length === 0) {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
const skillElements = skills.map((skill) => {
|
|
289
|
+
const locationTag = options.includeLocation && skill.location ? `
|
|
290
|
+
<location>${escapeXml(skill.location)}</location>` : "";
|
|
291
|
+
return ` <skill>
|
|
292
|
+
<name>${escapeXml(skill.name)}</name>
|
|
293
|
+
<description>${escapeXml(skill.description)}</description>${locationTag}
|
|
294
|
+
</skill>`;
|
|
295
|
+
}).join("\n");
|
|
296
|
+
return `<available_skills>
|
|
297
|
+
${skillElements}
|
|
298
|
+
</available_skills>`;
|
|
299
|
+
}
|
|
300
|
+
function escapeXml(str) {
|
|
301
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/storage.ts
|
|
305
|
+
var MemorySkillStore = class {
|
|
306
|
+
type = "memory";
|
|
307
|
+
skills = /* @__PURE__ */ new Map();
|
|
308
|
+
basePath;
|
|
309
|
+
constructor(basePath = "/virtual/skills") {
|
|
310
|
+
this.basePath = basePath;
|
|
311
|
+
}
|
|
312
|
+
async initialize() {
|
|
313
|
+
}
|
|
314
|
+
async listSkills() {
|
|
315
|
+
return Array.from(this.skills.keys());
|
|
316
|
+
}
|
|
317
|
+
async hasSkill(slug) {
|
|
318
|
+
return this.skills.has(slug);
|
|
319
|
+
}
|
|
320
|
+
async loadSkillContent(slug) {
|
|
321
|
+
const pkg = this.skills.get(slug);
|
|
322
|
+
if (!pkg) return null;
|
|
323
|
+
const skillMd = pkg.files.get("SKILL.md");
|
|
324
|
+
if (!skillMd || !skillMd.isText) return null;
|
|
325
|
+
return skillMd.content;
|
|
326
|
+
}
|
|
327
|
+
async loadFile(slug, relativePath) {
|
|
328
|
+
const pkg = this.skills.get(slug);
|
|
329
|
+
if (!pkg) return null;
|
|
330
|
+
const file = pkg.files.get(relativePath);
|
|
331
|
+
if (!file) return null;
|
|
332
|
+
return file.content;
|
|
333
|
+
}
|
|
334
|
+
async listFiles(slug, subdir) {
|
|
335
|
+
const pkg = this.skills.get(slug);
|
|
336
|
+
if (!pkg) return [];
|
|
337
|
+
const prefix = subdir ? `${subdir}/` : "";
|
|
338
|
+
const files = [];
|
|
339
|
+
for (const [path2] of pkg.files) {
|
|
340
|
+
if (subdir) {
|
|
341
|
+
if (path2.startsWith(prefix) && !path2.slice(prefix.length).includes("/")) {
|
|
342
|
+
files.push(path2.slice(prefix.length));
|
|
343
|
+
}
|
|
344
|
+
} else if (!path2.includes("/")) {
|
|
345
|
+
files.push(path2);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return files;
|
|
349
|
+
}
|
|
350
|
+
async saveSkill(pkg) {
|
|
351
|
+
this.skills.set(pkg.slug, pkg);
|
|
352
|
+
}
|
|
353
|
+
async deleteSkill(slug) {
|
|
354
|
+
return this.skills.delete(slug);
|
|
355
|
+
}
|
|
356
|
+
getSkillPath(slug) {
|
|
357
|
+
return `${this.basePath}/${slug}`;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Load a skill directly from content (no network/file needed).
|
|
361
|
+
*/
|
|
362
|
+
async loadFromContent(slug, skillMdContent, additionalFiles) {
|
|
363
|
+
const files = /* @__PURE__ */ new Map();
|
|
364
|
+
files.set("SKILL.md", {
|
|
365
|
+
path: "SKILL.md",
|
|
366
|
+
content: skillMdContent,
|
|
367
|
+
isText: true
|
|
368
|
+
});
|
|
369
|
+
if (additionalFiles) {
|
|
370
|
+
for (const [path2, content] of additionalFiles) {
|
|
371
|
+
files.set(path2, {
|
|
372
|
+
path: path2,
|
|
373
|
+
content,
|
|
374
|
+
isText: typeof content === "string"
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
await this.saveSkill({ slug, files });
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Load a skill from a zip buffer (for registry downloads).
|
|
382
|
+
*/
|
|
383
|
+
async loadFromZip(slug, zipBuffer) {
|
|
384
|
+
const { unzipSync } = await import("fflate");
|
|
385
|
+
const unzipped = unzipSync(zipBuffer);
|
|
386
|
+
const files = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const [fileName, data] of Object.entries(unzipped)) {
|
|
388
|
+
if (fileName.endsWith("/")) continue;
|
|
389
|
+
const parts = fileName.split("/").filter((p) => p && p !== ".." && p !== ".");
|
|
390
|
+
if (parts.length === 0) continue;
|
|
391
|
+
const relativePath = parts.join("/");
|
|
392
|
+
const isText = isTextFile(relativePath);
|
|
393
|
+
files.set(relativePath, {
|
|
394
|
+
path: relativePath,
|
|
395
|
+
content: isText ? new TextDecoder().decode(data) : data,
|
|
396
|
+
isText
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
await this.saveSkill({ slug, files });
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get the full skill package (for export/transfer).
|
|
403
|
+
*/
|
|
404
|
+
getPackage(slug) {
|
|
405
|
+
return this.skills.get(slug);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Save a skill package from simple file list format.
|
|
409
|
+
* Convenience method for use with GitHub/URL installs.
|
|
410
|
+
*/
|
|
411
|
+
async savePackage(pkg) {
|
|
412
|
+
const files = /* @__PURE__ */ new Map();
|
|
413
|
+
for (const file of pkg.files) {
|
|
414
|
+
const isText = typeof file.content === "string";
|
|
415
|
+
files.set(file.name, {
|
|
416
|
+
path: file.name,
|
|
417
|
+
content: file.content,
|
|
418
|
+
isText
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
await this.saveSkill({ slug: pkg.slug, files });
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get all skills in memory.
|
|
425
|
+
*/
|
|
426
|
+
getAllPackages() {
|
|
427
|
+
return new Map(this.skills);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
var FileSystemSkillStore = class {
|
|
431
|
+
type = "filesystem";
|
|
432
|
+
basePath;
|
|
433
|
+
fs = null;
|
|
434
|
+
path = null;
|
|
435
|
+
constructor(basePath = "./skills") {
|
|
436
|
+
this.basePath = basePath;
|
|
437
|
+
}
|
|
438
|
+
async initialize() {
|
|
439
|
+
try {
|
|
440
|
+
this.fs = await import("fs");
|
|
441
|
+
this.path = await import("path");
|
|
442
|
+
if (!this.fs.existsSync(this.basePath)) {
|
|
443
|
+
this.fs.mkdirSync(this.basePath, { recursive: true });
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
throw new Error("FileSystemSkillStore requires Node.js fs module");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async listSkills() {
|
|
450
|
+
if (!this.fs) await this.initialize();
|
|
451
|
+
const entries = this.fs.readdirSync(this.basePath, {
|
|
452
|
+
withFileTypes: true
|
|
453
|
+
});
|
|
454
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
455
|
+
}
|
|
456
|
+
async hasSkill(slug) {
|
|
457
|
+
if (!this.fs) await this.initialize();
|
|
458
|
+
const skillPath = this.path.join(this.basePath, slug, "SKILL.md");
|
|
459
|
+
return this.fs.existsSync(skillPath);
|
|
460
|
+
}
|
|
461
|
+
async loadSkillContent(slug) {
|
|
462
|
+
if (!this.fs) await this.initialize();
|
|
463
|
+
const skillPath = this.path.join(this.basePath, slug, "SKILL.md");
|
|
464
|
+
if (!this.fs.existsSync(skillPath)) return null;
|
|
465
|
+
return this.fs.readFileSync(skillPath, "utf-8");
|
|
466
|
+
}
|
|
467
|
+
async loadFile(slug, relativePath) {
|
|
468
|
+
if (!this.fs) await this.initialize();
|
|
469
|
+
const safePath = this.path.basename(relativePath);
|
|
470
|
+
const subdir = this.path.dirname(relativePath);
|
|
471
|
+
const fullPath = this.path.join(this.basePath, slug, subdir, safePath);
|
|
472
|
+
if (!this.fs.existsSync(fullPath)) return null;
|
|
473
|
+
if (isTextFile(relativePath)) {
|
|
474
|
+
return this.fs.readFileSync(fullPath, "utf-8");
|
|
475
|
+
} else {
|
|
476
|
+
return new Uint8Array(this.fs.readFileSync(fullPath));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async listFiles(slug, subdir) {
|
|
480
|
+
if (!this.fs) await this.initialize();
|
|
481
|
+
const dirPath = subdir ? this.path.join(this.basePath, slug, subdir) : this.path.join(this.basePath, slug);
|
|
482
|
+
if (!this.fs.existsSync(dirPath)) return [];
|
|
483
|
+
return this.fs.readdirSync(dirPath).filter((f) => !f.startsWith("."));
|
|
484
|
+
}
|
|
485
|
+
async saveSkill(pkg) {
|
|
486
|
+
if (!this.fs) await this.initialize();
|
|
487
|
+
const skillDir = this.path.join(this.basePath, pkg.slug);
|
|
488
|
+
if (!this.fs.existsSync(skillDir)) {
|
|
489
|
+
this.fs.mkdirSync(skillDir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
for (const [relativePath, file] of pkg.files) {
|
|
492
|
+
const fullPath = this.path.join(skillDir, relativePath);
|
|
493
|
+
const dir = this.path.dirname(fullPath);
|
|
494
|
+
if (!this.fs.existsSync(dir)) {
|
|
495
|
+
this.fs.mkdirSync(dir, { recursive: true });
|
|
496
|
+
}
|
|
497
|
+
if (file.isText) {
|
|
498
|
+
this.fs.writeFileSync(fullPath, file.content, "utf-8");
|
|
499
|
+
} else {
|
|
500
|
+
this.fs.writeFileSync(fullPath, file.content);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async deleteSkill(slug) {
|
|
505
|
+
if (!this.fs) await this.initialize();
|
|
506
|
+
const skillDir = this.path.join(this.basePath, slug);
|
|
507
|
+
if (!this.fs.existsSync(skillDir)) return false;
|
|
508
|
+
this.fs.rmSync(skillDir, { recursive: true, force: true });
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
getSkillPath(slug) {
|
|
512
|
+
return this.path ? this.path.resolve(this.basePath, slug) : `${this.basePath}/${slug}`;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Save a skill from a zip buffer.
|
|
516
|
+
*/
|
|
517
|
+
async saveFromZip(slug, zipBuffer) {
|
|
518
|
+
const { unzipSync } = await import("fflate");
|
|
519
|
+
const unzipped = unzipSync(zipBuffer);
|
|
520
|
+
const files = /* @__PURE__ */ new Map();
|
|
521
|
+
for (const [fileName, data] of Object.entries(unzipped)) {
|
|
522
|
+
if (fileName.endsWith("/")) continue;
|
|
523
|
+
const parts = fileName.split("/").filter((p) => p && p !== ".." && p !== ".");
|
|
524
|
+
if (parts.length === 0) continue;
|
|
525
|
+
const relativePath = parts.join("/");
|
|
526
|
+
const isText = isTextFile(relativePath);
|
|
527
|
+
files.set(relativePath, {
|
|
528
|
+
path: relativePath,
|
|
529
|
+
content: isText ? new TextDecoder().decode(data) : data,
|
|
530
|
+
isText
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
await this.saveSkill({ slug, files });
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
function isTextFile(filePath) {
|
|
537
|
+
const textExtensions = /* @__PURE__ */ new Set([
|
|
538
|
+
".md",
|
|
539
|
+
".txt",
|
|
540
|
+
".json",
|
|
541
|
+
".yaml",
|
|
542
|
+
".yml",
|
|
543
|
+
".toml",
|
|
544
|
+
".js",
|
|
545
|
+
".ts",
|
|
546
|
+
".py",
|
|
547
|
+
".rs",
|
|
548
|
+
".sh",
|
|
549
|
+
".bash",
|
|
550
|
+
".html",
|
|
551
|
+
".css",
|
|
552
|
+
".xml",
|
|
553
|
+
".svg",
|
|
554
|
+
".env",
|
|
555
|
+
".gitignore",
|
|
556
|
+
".dockerignore"
|
|
557
|
+
]);
|
|
558
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
559
|
+
return textExtensions.has(ext) || !filePath.includes(".");
|
|
560
|
+
}
|
|
561
|
+
function createStorage(options) {
|
|
562
|
+
const { type = "auto", basePath } = options;
|
|
563
|
+
if (type === "memory") {
|
|
564
|
+
return new MemorySkillStore(basePath);
|
|
565
|
+
}
|
|
566
|
+
if (type === "filesystem") {
|
|
567
|
+
return new FileSystemSkillStore(basePath);
|
|
568
|
+
}
|
|
569
|
+
if (typeof window !== "undefined" || typeof process === "undefined") {
|
|
570
|
+
return new MemorySkillStore(basePath);
|
|
571
|
+
}
|
|
572
|
+
return new FileSystemSkillStore(basePath);
|
|
573
|
+
}
|
|
574
|
+
async function loadSkillFromStorage(storage, slug, options = {}) {
|
|
575
|
+
const content = await storage.loadSkillContent(slug);
|
|
576
|
+
if (!content) return null;
|
|
577
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
578
|
+
if (!frontmatter) return null;
|
|
579
|
+
if (options.validate !== false) {
|
|
580
|
+
const result = validateFrontmatter(frontmatter, slug);
|
|
581
|
+
if (!result.valid) {
|
|
582
|
+
console.warn(`Skill ${slug} validation failed:`, result.errors);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const scripts = await storage.listFiles(slug, "scripts");
|
|
586
|
+
const references = await storage.listFiles(slug, "references");
|
|
587
|
+
const assets = await storage.listFiles(slug, "assets");
|
|
588
|
+
return {
|
|
589
|
+
slug,
|
|
590
|
+
name: frontmatter.name,
|
|
591
|
+
description: frontmatter.description,
|
|
592
|
+
version: frontmatter.metadata?.version?.toString() || "local",
|
|
593
|
+
content,
|
|
594
|
+
frontmatter,
|
|
595
|
+
path: storage.getSkillPath(slug),
|
|
596
|
+
scripts,
|
|
597
|
+
references,
|
|
598
|
+
assets,
|
|
599
|
+
loadedAt: Date.now()
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/services/skills.ts
|
|
604
|
+
var CLAWHUB_API = "https://clawhub.ai";
|
|
605
|
+
var CACHE_TTL = {
|
|
606
|
+
CATALOG: 1e3 * 60 * 60,
|
|
607
|
+
// 1 hour - list of all skills
|
|
608
|
+
SKILL_DETAILS: 1e3 * 60 * 30,
|
|
609
|
+
// 30 min - individual skill details
|
|
610
|
+
SEARCH: 1e3 * 60 * 5
|
|
611
|
+
// 5 min - search results
|
|
612
|
+
};
|
|
613
|
+
var MAX_PACKAGE_SIZE = 10 * 1024 * 1024;
|
|
614
|
+
var DEFAULT_AUTO_REFRESH_INTERVAL = 5e3;
|
|
615
|
+
var ELIGIBILITY_CACHE_TTL = 5 * 60 * 1e3;
|
|
616
|
+
function sanitizeSlug(slug) {
|
|
617
|
+
const sanitized = slug.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
618
|
+
if (sanitized !== slug || sanitized.length === 0 || sanitized.length > 100) {
|
|
619
|
+
throw new Error(`Invalid skill slug: ${slug}`);
|
|
620
|
+
}
|
|
621
|
+
return sanitized;
|
|
622
|
+
}
|
|
623
|
+
var AgentSkillsService = class _AgentSkillsService extends Service {
|
|
624
|
+
constructor(runtime, config) {
|
|
625
|
+
super(runtime);
|
|
626
|
+
this.runtime = runtime;
|
|
627
|
+
const skillsDirSetting = runtime.getSetting("SKILLS_DIR") ?? runtime.getSetting("CLAWHUB_SKILLS_DIR");
|
|
628
|
+
const skillsDir = config?.skillsDir || (typeof skillsDirSetting === "string" ? skillsDirSetting : null) || "./skills";
|
|
629
|
+
const storageTypeSetting = runtime.getSetting("SKILLS_STORAGE_TYPE");
|
|
630
|
+
const storageType = config?.storageType || (typeof storageTypeSetting === "string" ? storageTypeSetting : null) || "auto";
|
|
631
|
+
const registrySetting = runtime.getSetting("SKILLS_REGISTRY") ?? runtime.getSetting("CLAWHUB_REGISTRY");
|
|
632
|
+
this.apiBase = config?.registryUrl || (typeof registrySetting === "string" ? registrySetting : null) || CLAWHUB_API;
|
|
633
|
+
this.autoLoad = config?.autoLoad ?? (runtime.getSetting("SKILLS_AUTO_LOAD") !== "false" && runtime.getSetting("CLAWHUB_AUTO_LOAD") !== "false");
|
|
634
|
+
const bundledDirsConfig = config?.bundledSkillsDirs || runtime.getSetting("BUNDLED_SKILLS_DIRS") || runtime.getSetting("OTTO_BUNDLED_SKILLS_DIR");
|
|
635
|
+
if (Array.isArray(bundledDirsConfig)) {
|
|
636
|
+
this.bundledSkillsDirs = bundledDirsConfig.filter(Boolean);
|
|
637
|
+
} else if (typeof bundledDirsConfig === "string" && bundledDirsConfig.trim()) {
|
|
638
|
+
this.bundledSkillsDirs = bundledDirsConfig.split(",").map((d) => d.trim()).filter(Boolean);
|
|
639
|
+
} else {
|
|
640
|
+
this.bundledSkillsDirs = [];
|
|
641
|
+
}
|
|
642
|
+
const workspaceDirConfig = config?.workspaceSkillsDir || runtime.getSetting("WORKSPACE_SKILLS_DIR") || runtime.getSetting("OTTO_WORKSPACE_SKILLS_DIR");
|
|
643
|
+
if (typeof workspaceDirConfig === "string" && workspaceDirConfig.trim()) {
|
|
644
|
+
this.workspaceSkillsDir = workspaceDirConfig.trim();
|
|
645
|
+
}
|
|
646
|
+
const pluginDirsConfig = config?.pluginSkillsDirs || runtime.getSetting("PLUGIN_SKILLS_DIRS") || runtime.getSetting("OTTO_PLUGIN_SKILLS_DIRS");
|
|
647
|
+
this.pluginSkillsDirs = this.parseDirectoryList(pluginDirsConfig);
|
|
648
|
+
const extraDirsConfig = config?.extraDirs || runtime.getSetting("EXTRA_SKILLS_DIRS") || runtime.getSetting("OTTO_EXTRA_SKILLS_DIRS") || runtime.getSetting("skills.load.extraDirs");
|
|
649
|
+
this.extraDirs = this.parseDirectoryList(extraDirsConfig);
|
|
650
|
+
const allowlistConfig = config?.allowlist || runtime.getSetting("SKILLS_ALLOWLIST") || runtime.getSetting("skills.allowlist");
|
|
651
|
+
if (allowlistConfig) {
|
|
652
|
+
this.allowlist = new Set(this.parseStringList(allowlistConfig));
|
|
653
|
+
}
|
|
654
|
+
const denylistConfig = config?.denylist || runtime.getSetting("SKILLS_DENYLIST") || runtime.getSetting("skills.denylist");
|
|
655
|
+
if (denylistConfig) {
|
|
656
|
+
this.denylist = new Set(this.parseStringList(denylistConfig));
|
|
657
|
+
}
|
|
658
|
+
if (config?.skillEntries) {
|
|
659
|
+
for (const [slug, entry] of Object.entries(config.skillEntries)) {
|
|
660
|
+
this.skillEntries.set(slug, entry);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
this.autoRefreshEnabled = config?.autoRefresh ?? runtime.getSetting("SKILLS_AUTO_REFRESH") === "true";
|
|
664
|
+
this.autoRefreshInterval = config?.autoRefreshInterval ?? DEFAULT_AUTO_REFRESH_INTERVAL;
|
|
665
|
+
this.storage = config?.storage || createStorage({ type: storageType, basePath: skillsDir });
|
|
666
|
+
if (this.storage.type === "filesystem") {
|
|
667
|
+
this.catalogCachePath = `${skillsDir}/.cache/catalog.json`;
|
|
668
|
+
this.lockfilePath = `${skillsDir}/.cache/lock.json`;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
static serviceType = "AGENT_SKILLS_SERVICE";
|
|
672
|
+
capabilityDescription = "Agent Skills - discover, load, and execute modular agent capabilities";
|
|
673
|
+
storage;
|
|
674
|
+
apiBase;
|
|
675
|
+
autoLoad;
|
|
676
|
+
// Bundled skills configuration
|
|
677
|
+
bundledSkillsDirs;
|
|
678
|
+
bundledStorages = /* @__PURE__ */ new Map();
|
|
679
|
+
// Phase 4.1: Additional skill source directories
|
|
680
|
+
workspaceSkillsDir = null;
|
|
681
|
+
workspaceStorage = null;
|
|
682
|
+
pluginSkillsDirs = [];
|
|
683
|
+
pluginStorages = /* @__PURE__ */ new Map();
|
|
684
|
+
extraDirs = [];
|
|
685
|
+
extraStorages = /* @__PURE__ */ new Map();
|
|
686
|
+
// In-memory caches - now tracks LoadedSkill with source info
|
|
687
|
+
loadedSkills = /* @__PURE__ */ new Map();
|
|
688
|
+
catalogCache = null;
|
|
689
|
+
searchCache = /* @__PURE__ */ new Map();
|
|
690
|
+
detailsCache = /* @__PURE__ */ new Map();
|
|
691
|
+
// Phase 4.2: Eligibility cache
|
|
692
|
+
eligibilityCache = /* @__PURE__ */ new Map();
|
|
693
|
+
// Phase 4.4: Skill configuration
|
|
694
|
+
allowlist = null;
|
|
695
|
+
denylist = /* @__PURE__ */ new Set();
|
|
696
|
+
skillEntries = /* @__PURE__ */ new Map();
|
|
697
|
+
skillEnvOverrides = /* @__PURE__ */ new Map();
|
|
698
|
+
skillApiKeys = /* @__PURE__ */ new Map();
|
|
699
|
+
// Auto-refresh watcher
|
|
700
|
+
autoRefreshEnabled = false;
|
|
701
|
+
autoRefreshInterval = DEFAULT_AUTO_REFRESH_INTERVAL;
|
|
702
|
+
watcherCleanup = null;
|
|
703
|
+
// Catalog cache for disk persistence (filesystem mode only)
|
|
704
|
+
catalogCachePath = null;
|
|
705
|
+
lockfilePath = null;
|
|
706
|
+
/**
|
|
707
|
+
* Parse a directory list from config (string or array).
|
|
708
|
+
*/
|
|
709
|
+
parseDirectoryList(config) {
|
|
710
|
+
if (Array.isArray(config)) {
|
|
711
|
+
return config.filter((d) => typeof d === "string" && d.trim().length > 0);
|
|
712
|
+
}
|
|
713
|
+
if (typeof config === "string" && config.trim()) {
|
|
714
|
+
return config.split(",").map((d) => d.trim()).filter(Boolean);
|
|
715
|
+
}
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Parse a string list from config (string or array).
|
|
720
|
+
*/
|
|
721
|
+
parseStringList(config) {
|
|
722
|
+
if (Array.isArray(config)) {
|
|
723
|
+
return config.filter((s) => typeof s === "string");
|
|
724
|
+
}
|
|
725
|
+
if (typeof config === "string") {
|
|
726
|
+
return config.split(",").map((s) => s.trim()).filter(Boolean);
|
|
727
|
+
}
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
static async start(runtime, config) {
|
|
731
|
+
const service = new _AgentSkillsService(runtime, config);
|
|
732
|
+
await service.initialize();
|
|
733
|
+
return service;
|
|
734
|
+
}
|
|
735
|
+
static async stop(_runtime) {
|
|
736
|
+
}
|
|
737
|
+
async stop() {
|
|
738
|
+
this.runtime.logger.info("AgentSkills: Service stopping...");
|
|
739
|
+
if (this.watcherCleanup) {
|
|
740
|
+
this.watcherCleanup();
|
|
741
|
+
this.watcherCleanup = null;
|
|
742
|
+
}
|
|
743
|
+
this.loadedSkills.clear();
|
|
744
|
+
this.eligibilityCache.clear();
|
|
745
|
+
this.catalogCache = null;
|
|
746
|
+
this.searchCache.clear();
|
|
747
|
+
this.detailsCache.clear();
|
|
748
|
+
}
|
|
749
|
+
async initialize() {
|
|
750
|
+
this.runtime.logger.info(
|
|
751
|
+
`AgentSkills: Service initializing (storage: ${this.storage.type})...`
|
|
752
|
+
);
|
|
753
|
+
await this.storage.initialize();
|
|
754
|
+
await this.initializeSkillSources();
|
|
755
|
+
if (this.autoLoad) {
|
|
756
|
+
await this.loadSkillsFromSource(this.extraStorages, "extra");
|
|
757
|
+
await this.loadSkillsFromSource(this.pluginStorages, "plugin");
|
|
758
|
+
await this.loadBundledSkills();
|
|
759
|
+
await this.loadInstalledSkills();
|
|
760
|
+
await this.loadWorkspaceSkills();
|
|
761
|
+
}
|
|
762
|
+
if (this.storage.type === "filesystem") {
|
|
763
|
+
await this.loadCatalogFromDisk();
|
|
764
|
+
}
|
|
765
|
+
if (this.autoRefreshEnabled && this.storage.type === "filesystem") {
|
|
766
|
+
this.startAutoRefresh();
|
|
767
|
+
}
|
|
768
|
+
const counts = this.getSkillCountsBySource();
|
|
769
|
+
this.runtime.logger.info(
|
|
770
|
+
`AgentSkills: Initialized with ${this.loadedSkills.size} skills (workspace: ${counts.workspace}, managed: ${counts.managed}, bundled: ${counts.bundled}, plugin: ${counts.plugin}, extra: ${counts.extra})`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Initialize all skill source storages.
|
|
775
|
+
*/
|
|
776
|
+
async initializeSkillSources() {
|
|
777
|
+
if (this.workspaceSkillsDir) {
|
|
778
|
+
try {
|
|
779
|
+
this.workspaceStorage = new FileSystemSkillStore(this.workspaceSkillsDir);
|
|
780
|
+
await this.workspaceStorage.initialize();
|
|
781
|
+
this.runtime.logger.info(
|
|
782
|
+
`AgentSkills: Registered workspace skills directory: ${this.workspaceSkillsDir}`
|
|
783
|
+
);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
this.runtime.logger.debug(
|
|
786
|
+
`AgentSkills: Workspace skills directory not accessible: ${this.workspaceSkillsDir}`
|
|
787
|
+
);
|
|
788
|
+
this.workspaceStorage = null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
for (const bundledDir of this.bundledSkillsDirs) {
|
|
792
|
+
try {
|
|
793
|
+
const bundledStorage = new FileSystemSkillStore(bundledDir);
|
|
794
|
+
await bundledStorage.initialize();
|
|
795
|
+
this.bundledStorages.set(bundledDir, bundledStorage);
|
|
796
|
+
this.runtime.logger.info(
|
|
797
|
+
`AgentSkills: Registered bundled skills directory: ${bundledDir}`
|
|
798
|
+
);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
this.runtime.logger.warn(
|
|
801
|
+
`AgentSkills: Failed to initialize bundled skills directory: ${bundledDir}`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
for (const pluginDir of this.pluginSkillsDirs) {
|
|
806
|
+
try {
|
|
807
|
+
const pluginStorage = new FileSystemSkillStore(pluginDir);
|
|
808
|
+
await pluginStorage.initialize();
|
|
809
|
+
this.pluginStorages.set(pluginDir, pluginStorage);
|
|
810
|
+
this.runtime.logger.info(
|
|
811
|
+
`AgentSkills: Registered plugin skills directory: ${pluginDir}`
|
|
812
|
+
);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
this.runtime.logger.debug(
|
|
815
|
+
`AgentSkills: Plugin skills directory not accessible: ${pluginDir}`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (const extraDir of this.extraDirs) {
|
|
820
|
+
try {
|
|
821
|
+
const extraStorage = new FileSystemSkillStore(extraDir);
|
|
822
|
+
await extraStorage.initialize();
|
|
823
|
+
this.extraStorages.set(extraDir, extraStorage);
|
|
824
|
+
this.runtime.logger.info(
|
|
825
|
+
`AgentSkills: Registered extra skills directory: ${extraDir}`
|
|
826
|
+
);
|
|
827
|
+
} catch (error) {
|
|
828
|
+
this.runtime.logger.debug(
|
|
829
|
+
`AgentSkills: Extra skills directory not accessible: ${extraDir}`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Get skill counts by source type.
|
|
836
|
+
*/
|
|
837
|
+
getSkillCountsBySource() {
|
|
838
|
+
const counts = {
|
|
839
|
+
workspace: 0,
|
|
840
|
+
managed: 0,
|
|
841
|
+
bundled: 0,
|
|
842
|
+
plugin: 0,
|
|
843
|
+
extra: 0
|
|
844
|
+
};
|
|
845
|
+
for (const skill of this.loadedSkills.values()) {
|
|
846
|
+
counts[skill.source]++;
|
|
847
|
+
}
|
|
848
|
+
return counts;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Load skills from a set of storages with a specific source type.
|
|
852
|
+
*/
|
|
853
|
+
async loadSkillsFromSource(storages, source) {
|
|
854
|
+
for (const [dir, storage] of storages) {
|
|
855
|
+
const slugs = await storage.listSkills();
|
|
856
|
+
this.runtime.logger.debug(
|
|
857
|
+
`AgentSkills: Found ${slugs.length} ${source} skills in ${dir}`
|
|
858
|
+
);
|
|
859
|
+
for (const slug of slugs) {
|
|
860
|
+
if (!this.isSkillAllowed(slug)) {
|
|
861
|
+
this.runtime.logger.debug(
|
|
862
|
+
`AgentSkills: Skipping ${source} skill ${slug} (filtered by allow/denylist)`
|
|
863
|
+
);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const existing = this.loadedSkills.get(slug);
|
|
867
|
+
if (existing && SKILL_SOURCE_PRECEDENCE[existing.source] >= SKILL_SOURCE_PRECEDENCE[source]) {
|
|
868
|
+
this.runtime.logger.debug(
|
|
869
|
+
`AgentSkills: Skipping ${source} skill ${slug} (${existing.source} version takes precedence)`
|
|
870
|
+
);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
874
|
+
storage,
|
|
875
|
+
slug,
|
|
876
|
+
source,
|
|
877
|
+
dir
|
|
878
|
+
);
|
|
879
|
+
if (skill) {
|
|
880
|
+
if (existing) {
|
|
881
|
+
this.runtime.logger.info(
|
|
882
|
+
`AgentSkills: ${source} skill ${slug} overrides ${existing.source} version from ${existing.sourceDir}`
|
|
883
|
+
);
|
|
884
|
+
skill.overrides = `${existing.source}:${existing.sourceDir}`;
|
|
885
|
+
}
|
|
886
|
+
this.loadedSkills.set(slug, skill);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Load workspace skills (highest precedence).
|
|
893
|
+
*/
|
|
894
|
+
async loadWorkspaceSkills() {
|
|
895
|
+
if (!this.workspaceStorage) return;
|
|
896
|
+
const slugs = await this.workspaceStorage.listSkills();
|
|
897
|
+
this.runtime.logger.debug(
|
|
898
|
+
`AgentSkills: Found ${slugs.length} workspace skills`
|
|
899
|
+
);
|
|
900
|
+
for (const slug of slugs) {
|
|
901
|
+
if (!this.isSkillAllowed(slug)) {
|
|
902
|
+
this.runtime.logger.debug(
|
|
903
|
+
`AgentSkills: Skipping workspace skill ${slug} (filtered by allow/denylist)`
|
|
904
|
+
);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const existing = this.loadedSkills.get(slug);
|
|
908
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
909
|
+
this.workspaceStorage,
|
|
910
|
+
slug,
|
|
911
|
+
"workspace",
|
|
912
|
+
this.workspaceSkillsDir
|
|
913
|
+
);
|
|
914
|
+
if (skill) {
|
|
915
|
+
if (existing) {
|
|
916
|
+
this.runtime.logger.info(
|
|
917
|
+
`AgentSkills: Workspace skill ${slug} overrides ${existing.source} version`
|
|
918
|
+
);
|
|
919
|
+
skill.overrides = `${existing.source}:${existing.sourceDir}`;
|
|
920
|
+
}
|
|
921
|
+
this.loadedSkills.set(slug, skill);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Check if a skill is allowed based on allowlist/denylist.
|
|
927
|
+
*/
|
|
928
|
+
isSkillAllowed(slug) {
|
|
929
|
+
if (this.denylist.has(slug)) {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
if (this.allowlist !== null) {
|
|
933
|
+
return this.allowlist.has(slug);
|
|
934
|
+
}
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Start the auto-refresh watcher.
|
|
939
|
+
*/
|
|
940
|
+
startAutoRefresh() {
|
|
941
|
+
if (this.watcherCleanup) return;
|
|
942
|
+
const watchDirs = [];
|
|
943
|
+
if (this.workspaceSkillsDir) {
|
|
944
|
+
watchDirs.push(this.workspaceSkillsDir);
|
|
945
|
+
}
|
|
946
|
+
if (watchDirs.length === 0) {
|
|
947
|
+
this.runtime.logger.debug(
|
|
948
|
+
"AgentSkills: No directories to watch for auto-refresh"
|
|
949
|
+
);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
let lastCheck = Date.now();
|
|
953
|
+
const interval = setInterval(async () => {
|
|
954
|
+
try {
|
|
955
|
+
await this.refreshSkillsIfChanged(lastCheck);
|
|
956
|
+
lastCheck = Date.now();
|
|
957
|
+
} catch (error) {
|
|
958
|
+
this.runtime.logger.error(
|
|
959
|
+
`AgentSkills: Auto-refresh error: ${error}`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}, this.autoRefreshInterval);
|
|
963
|
+
this.watcherCleanup = () => {
|
|
964
|
+
clearInterval(interval);
|
|
965
|
+
};
|
|
966
|
+
this.runtime.logger.info(
|
|
967
|
+
`AgentSkills: Auto-refresh enabled (${this.autoRefreshInterval}ms interval)`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Refresh skills if any files have changed.
|
|
972
|
+
*/
|
|
973
|
+
async refreshSkillsIfChanged(since) {
|
|
974
|
+
if (this.workspaceStorage) {
|
|
975
|
+
const slugs = await this.workspaceStorage.listSkills();
|
|
976
|
+
for (const slug of slugs) {
|
|
977
|
+
const existing = this.loadedSkills.get(slug);
|
|
978
|
+
if (!existing || existing.source !== "workspace") {
|
|
979
|
+
await this.loadSkill(slug, { validate: true });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Load all skills from bundled directories.
|
|
986
|
+
* These are read-only and cannot be modified or uninstalled.
|
|
987
|
+
*/
|
|
988
|
+
async loadBundledSkills() {
|
|
989
|
+
for (const [bundledDir, storage] of this.bundledStorages) {
|
|
990
|
+
const slugs = await storage.listSkills();
|
|
991
|
+
this.runtime.logger.debug(
|
|
992
|
+
`AgentSkills: Found ${slugs.length} bundled skills in ${bundledDir}`
|
|
993
|
+
);
|
|
994
|
+
for (const slug of slugs) {
|
|
995
|
+
if (!this.isSkillAllowed(slug)) {
|
|
996
|
+
this.runtime.logger.debug(
|
|
997
|
+
`AgentSkills: Skipping bundled skill ${slug} (filtered by allow/denylist)`
|
|
998
|
+
);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const existing = this.loadedSkills.get(slug);
|
|
1002
|
+
if (existing && SKILL_SOURCE_PRECEDENCE[existing.source] >= SKILL_SOURCE_PRECEDENCE.bundled) {
|
|
1003
|
+
this.runtime.logger.debug(
|
|
1004
|
+
`AgentSkills: Skipping bundled skill ${slug} (${existing.source} version takes precedence)`
|
|
1005
|
+
);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
1009
|
+
storage,
|
|
1010
|
+
slug,
|
|
1011
|
+
"bundled",
|
|
1012
|
+
bundledDir
|
|
1013
|
+
);
|
|
1014
|
+
if (skill) {
|
|
1015
|
+
if (existing) {
|
|
1016
|
+
skill.overrides = `${existing.source}:${existing.sourceDir}`;
|
|
1017
|
+
}
|
|
1018
|
+
this.loadedSkills.set(slug, skill);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Internal helper to load a skill from any storage with source tracking.
|
|
1025
|
+
*/
|
|
1026
|
+
async loadSkillFromStorageWithSource(storage, slug, source, sourceDir) {
|
|
1027
|
+
const content = await storage.loadSkillContent(slug);
|
|
1028
|
+
if (!content) {
|
|
1029
|
+
this.runtime.logger.warn(`AgentSkills: No SKILL.md found for ${slug}`);
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
1033
|
+
if (!frontmatter) {
|
|
1034
|
+
this.runtime.logger.warn(`AgentSkills: ${slug} has invalid frontmatter`);
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
const validation = validateFrontmatter(frontmatter, slug);
|
|
1038
|
+
if (!validation.valid) {
|
|
1039
|
+
this.runtime.logger.warn(
|
|
1040
|
+
`AgentSkills: ${slug} validation failed: ${validation.errors.map((e) => e.message).join(", ")}`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
for (const warning of validation.warnings) {
|
|
1044
|
+
this.runtime.logger.debug(
|
|
1045
|
+
`AgentSkills: ${slug} warning: ${warning.message}`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const scripts = await storage.listFiles(slug, "scripts");
|
|
1049
|
+
const references = await storage.listFiles(slug, "references");
|
|
1050
|
+
const assets = await storage.listFiles(slug, "assets");
|
|
1051
|
+
const version = frontmatter.metadata?.version?.toString() || "local";
|
|
1052
|
+
return {
|
|
1053
|
+
slug,
|
|
1054
|
+
name: frontmatter.name,
|
|
1055
|
+
description: frontmatter.description,
|
|
1056
|
+
version,
|
|
1057
|
+
content,
|
|
1058
|
+
frontmatter,
|
|
1059
|
+
path: storage.getSkillPath(slug),
|
|
1060
|
+
scripts,
|
|
1061
|
+
references,
|
|
1062
|
+
assets,
|
|
1063
|
+
loadedAt: Date.now(),
|
|
1064
|
+
source,
|
|
1065
|
+
sourceDir,
|
|
1066
|
+
precedence: SKILL_SOURCE_PRECEDENCE[source],
|
|
1067
|
+
bundledDir: source === "bundled" ? sourceDir : void 0
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
// ============================================================
|
|
1071
|
+
// PHASE 4.2: SKILL ELIGIBILITY CHECKING
|
|
1072
|
+
// ============================================================
|
|
1073
|
+
/**
|
|
1074
|
+
* Check if a skill is eligible for use based on its requirements.
|
|
1075
|
+
* Checks required binaries, environment variables, and config.
|
|
1076
|
+
*
|
|
1077
|
+
* @param slug - Skill slug or loaded skill
|
|
1078
|
+
* @returns Eligibility status with reasons if ineligible
|
|
1079
|
+
*/
|
|
1080
|
+
async checkSkillEligibility(slugOrSkill) {
|
|
1081
|
+
const skill = typeof slugOrSkill === "string" ? this.loadedSkills.get(slugOrSkill) : slugOrSkill;
|
|
1082
|
+
if (!skill) {
|
|
1083
|
+
return {
|
|
1084
|
+
slug: typeof slugOrSkill === "string" ? slugOrSkill : "unknown",
|
|
1085
|
+
eligible: false,
|
|
1086
|
+
reasons: [{
|
|
1087
|
+
type: "config",
|
|
1088
|
+
missing: "skill",
|
|
1089
|
+
message: "Skill not found"
|
|
1090
|
+
}],
|
|
1091
|
+
checkedAt: Date.now()
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
const cached = this.eligibilityCache.get(skill.slug);
|
|
1095
|
+
if (cached && Date.now() - cached.checkedAt < ELIGIBILITY_CACHE_TTL) {
|
|
1096
|
+
return cached;
|
|
1097
|
+
}
|
|
1098
|
+
const reasons = [];
|
|
1099
|
+
const metadata = skill.frontmatter.metadata?.otto;
|
|
1100
|
+
const requires = metadata?.requires;
|
|
1101
|
+
if (requires) {
|
|
1102
|
+
if (requires.bins && requires.bins.length > 0) {
|
|
1103
|
+
const missingBins = await this.checkMissingBinaries(requires.bins);
|
|
1104
|
+
for (const bin of missingBins) {
|
|
1105
|
+
reasons.push({
|
|
1106
|
+
type: "bin",
|
|
1107
|
+
missing: bin,
|
|
1108
|
+
message: `Required binary '${bin}' not found in PATH`,
|
|
1109
|
+
suggestion: this.getSuggestionForBinary(bin, metadata?.install)
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (requires.env && requires.env.length > 0) {
|
|
1114
|
+
for (const envVar of requires.env) {
|
|
1115
|
+
const value = process.env[envVar] || this.runtime.getSetting(envVar);
|
|
1116
|
+
if (!value) {
|
|
1117
|
+
reasons.push({
|
|
1118
|
+
type: "env",
|
|
1119
|
+
missing: envVar,
|
|
1120
|
+
message: `Required environment variable '${envVar}' is not set`,
|
|
1121
|
+
suggestion: `Set ${envVar} in your environment or agent settings`
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (requires.config && requires.config.length > 0) {
|
|
1127
|
+
for (const configKey of requires.config) {
|
|
1128
|
+
const value = this.runtime.getSetting(configKey);
|
|
1129
|
+
if (!value) {
|
|
1130
|
+
reasons.push({
|
|
1131
|
+
type: "config",
|
|
1132
|
+
missing: configKey,
|
|
1133
|
+
message: `Required configuration '${configKey}' is not set`,
|
|
1134
|
+
suggestion: `Set ${configKey} in your agent configuration`
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const eligibility = {
|
|
1141
|
+
slug: skill.slug,
|
|
1142
|
+
eligible: reasons.length === 0,
|
|
1143
|
+
reasons,
|
|
1144
|
+
checkedAt: Date.now(),
|
|
1145
|
+
installOptions: metadata?.install
|
|
1146
|
+
};
|
|
1147
|
+
this.eligibilityCache.set(skill.slug, eligibility);
|
|
1148
|
+
return eligibility;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Check for missing binaries from a list.
|
|
1152
|
+
*/
|
|
1153
|
+
async checkMissingBinaries(bins) {
|
|
1154
|
+
const missing = [];
|
|
1155
|
+
for (const bin of bins) {
|
|
1156
|
+
const exists = await this.binaryExists(bin);
|
|
1157
|
+
if (!exists) {
|
|
1158
|
+
missing.push(bin);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return missing;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Check if a binary exists in PATH.
|
|
1165
|
+
*/
|
|
1166
|
+
async binaryExists(name) {
|
|
1167
|
+
try {
|
|
1168
|
+
const { execSync } = await import("node:child_process");
|
|
1169
|
+
const platform = process.platform;
|
|
1170
|
+
const command = platform === "win32" ? `where ${name}` : `which ${name}`;
|
|
1171
|
+
execSync(command, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1172
|
+
return true;
|
|
1173
|
+
} catch {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Get installation suggestion for a missing binary.
|
|
1179
|
+
*/
|
|
1180
|
+
getSuggestionForBinary(bin, installOptions) {
|
|
1181
|
+
if (!installOptions) return void 0;
|
|
1182
|
+
const options = installOptions.filter((opt) => opt.bins?.includes(bin));
|
|
1183
|
+
if (options.length === 0) return void 0;
|
|
1184
|
+
const platform = process.platform;
|
|
1185
|
+
const preferred = platform === "darwin" ? options.find((o) => o.kind === "brew") : options.find((o) => o.kind === "apt");
|
|
1186
|
+
const option = preferred || options[0];
|
|
1187
|
+
switch (option.kind) {
|
|
1188
|
+
case "brew":
|
|
1189
|
+
return `Install with Homebrew: brew install ${option.formula || option.package}`;
|
|
1190
|
+
case "apt":
|
|
1191
|
+
return `Install with apt: sudo apt-get install ${option.package}`;
|
|
1192
|
+
case "node":
|
|
1193
|
+
return `Install with npm/pnpm: npm install -g ${option.package}`;
|
|
1194
|
+
case "pip":
|
|
1195
|
+
return `Install with pip: pip install ${option.package}`;
|
|
1196
|
+
case "cargo":
|
|
1197
|
+
return `Install with cargo: cargo install ${option.package}`;
|
|
1198
|
+
default:
|
|
1199
|
+
return option.label;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Get eligibility status for all loaded skills.
|
|
1204
|
+
*/
|
|
1205
|
+
async getAllSkillEligibility() {
|
|
1206
|
+
const results = /* @__PURE__ */ new Map();
|
|
1207
|
+
for (const [slug, skill] of this.loadedSkills) {
|
|
1208
|
+
const eligibility = await this.checkSkillEligibility(skill);
|
|
1209
|
+
results.set(slug, eligibility);
|
|
1210
|
+
}
|
|
1211
|
+
return results;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Get only eligible skills.
|
|
1215
|
+
*/
|
|
1216
|
+
async getEligibleSkills() {
|
|
1217
|
+
const eligible = [];
|
|
1218
|
+
for (const skill of this.loadedSkills.values()) {
|
|
1219
|
+
const eligibility = await this.checkSkillEligibility(skill);
|
|
1220
|
+
if (eligibility.eligible) {
|
|
1221
|
+
eligible.push(skill);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return eligible;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Get ineligible skills with their reasons.
|
|
1228
|
+
*/
|
|
1229
|
+
async getIneligibleSkills() {
|
|
1230
|
+
const ineligible = [];
|
|
1231
|
+
for (const skill of this.loadedSkills.values()) {
|
|
1232
|
+
const eligibility = await this.checkSkillEligibility(skill);
|
|
1233
|
+
if (!eligibility.eligible) {
|
|
1234
|
+
ineligible.push({ skill, eligibility });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return ineligible;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Clear the eligibility cache.
|
|
1241
|
+
*/
|
|
1242
|
+
clearEligibilityCache() {
|
|
1243
|
+
this.eligibilityCache.clear();
|
|
1244
|
+
}
|
|
1245
|
+
// ============================================================
|
|
1246
|
+
// PHASE 4.4: SKILL CONFIGURATION
|
|
1247
|
+
// ============================================================
|
|
1248
|
+
/**
|
|
1249
|
+
* Set environment variables for a specific skill.
|
|
1250
|
+
* These will be injected when the skill is used.
|
|
1251
|
+
*
|
|
1252
|
+
* @param skillName - Skill slug
|
|
1253
|
+
* @param env - Environment variables to set
|
|
1254
|
+
*/
|
|
1255
|
+
setSkillEnv(skillName, env) {
|
|
1256
|
+
this.skillEnvOverrides.set(skillName, {
|
|
1257
|
+
...this.skillEnvOverrides.get(skillName),
|
|
1258
|
+
...env
|
|
1259
|
+
});
|
|
1260
|
+
this.runtime.logger.debug(
|
|
1261
|
+
`AgentSkills: Set env overrides for skill ${skillName}`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Get environment variables configured for a skill.
|
|
1266
|
+
*
|
|
1267
|
+
* @param skillName - Skill slug
|
|
1268
|
+
* @returns Merged environment variables
|
|
1269
|
+
*/
|
|
1270
|
+
getSkillEnv(skillName) {
|
|
1271
|
+
const skillEntry = this.skillEntries.get(skillName);
|
|
1272
|
+
const overrides = this.skillEnvOverrides.get(skillName);
|
|
1273
|
+
return {
|
|
1274
|
+
...skillEntry?.env,
|
|
1275
|
+
...overrides
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Set an API key for a specific skill.
|
|
1280
|
+
*
|
|
1281
|
+
* @param skillName - Skill slug
|
|
1282
|
+
* @param apiKey - API key value
|
|
1283
|
+
*/
|
|
1284
|
+
setSkillApiKey(skillName, apiKey) {
|
|
1285
|
+
this.skillApiKeys.set(skillName, apiKey);
|
|
1286
|
+
this.runtime.logger.debug(
|
|
1287
|
+
`AgentSkills: Set API key for skill ${skillName}`
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Get the API key for a skill.
|
|
1292
|
+
*
|
|
1293
|
+
* @param skillName - Skill slug
|
|
1294
|
+
* @returns API key if set
|
|
1295
|
+
*/
|
|
1296
|
+
getSkillApiKey(skillName) {
|
|
1297
|
+
const override = this.skillApiKeys.get(skillName);
|
|
1298
|
+
if (override) return override;
|
|
1299
|
+
const entry = this.skillEntries.get(skillName);
|
|
1300
|
+
return entry?.apiKey;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Update the allowlist of skills.
|
|
1304
|
+
*
|
|
1305
|
+
* @param slugs - Skill slugs to allow (null to disable allowlist)
|
|
1306
|
+
*/
|
|
1307
|
+
setAllowlist(slugs) {
|
|
1308
|
+
this.allowlist = slugs ? new Set(slugs) : null;
|
|
1309
|
+
this.runtime.logger.info(
|
|
1310
|
+
`AgentSkills: Updated allowlist (${slugs?.length ?? "disabled"} skills)`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Update the denylist of skills.
|
|
1315
|
+
*
|
|
1316
|
+
* @param slugs - Skill slugs to deny
|
|
1317
|
+
*/
|
|
1318
|
+
setDenylist(slugs) {
|
|
1319
|
+
this.denylist = new Set(slugs);
|
|
1320
|
+
this.runtime.logger.info(
|
|
1321
|
+
`AgentSkills: Updated denylist (${slugs.length} skills)`
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Get the current allowlist.
|
|
1326
|
+
*/
|
|
1327
|
+
getAllowlist() {
|
|
1328
|
+
return this.allowlist ? Array.from(this.allowlist) : null;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Get the current denylist.
|
|
1332
|
+
*/
|
|
1333
|
+
getDenylist() {
|
|
1334
|
+
return Array.from(this.denylist);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Set configuration for a skill.
|
|
1338
|
+
*
|
|
1339
|
+
* @param skillName - Skill slug
|
|
1340
|
+
* @param config - Configuration entry
|
|
1341
|
+
*/
|
|
1342
|
+
setSkillConfig(skillName, config) {
|
|
1343
|
+
this.skillEntries.set(skillName, {
|
|
1344
|
+
...this.skillEntries.get(skillName),
|
|
1345
|
+
...config
|
|
1346
|
+
});
|
|
1347
|
+
this.runtime.logger.debug(
|
|
1348
|
+
`AgentSkills: Updated config for skill ${skillName}`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Get configuration for a skill.
|
|
1353
|
+
*
|
|
1354
|
+
* @param skillName - Skill slug
|
|
1355
|
+
* @returns Skill configuration or undefined
|
|
1356
|
+
*/
|
|
1357
|
+
getSkillConfig(skillName) {
|
|
1358
|
+
return this.skillEntries.get(skillName);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Check if a skill is enabled.
|
|
1362
|
+
*
|
|
1363
|
+
* @param skillName - Skill slug
|
|
1364
|
+
* @returns True if enabled (default: true)
|
|
1365
|
+
*/
|
|
1366
|
+
isSkillEnabled(skillName) {
|
|
1367
|
+
const entry = this.skillEntries.get(skillName);
|
|
1368
|
+
return entry?.enabled !== false;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Enable or disable a skill.
|
|
1372
|
+
*
|
|
1373
|
+
* @param skillName - Skill slug
|
|
1374
|
+
* @param enabled - Whether to enable the skill
|
|
1375
|
+
*/
|
|
1376
|
+
setSkillEnabled(skillName, enabled) {
|
|
1377
|
+
this.setSkillConfig(skillName, { enabled });
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Add a plugin skills directory at runtime.
|
|
1381
|
+
*
|
|
1382
|
+
* @param dir - Directory path
|
|
1383
|
+
*/
|
|
1384
|
+
async addPluginSkillsDir(dir) {
|
|
1385
|
+
if (this.pluginStorages.has(dir)) return;
|
|
1386
|
+
try {
|
|
1387
|
+
const storage = new FileSystemSkillStore(dir);
|
|
1388
|
+
await storage.initialize();
|
|
1389
|
+
this.pluginStorages.set(dir, storage);
|
|
1390
|
+
this.pluginSkillsDirs.push(dir);
|
|
1391
|
+
await this.loadSkillsFromSource(
|
|
1392
|
+
/* @__PURE__ */ new Map([[dir, storage]]),
|
|
1393
|
+
"plugin"
|
|
1394
|
+
);
|
|
1395
|
+
this.runtime.logger.info(
|
|
1396
|
+
`AgentSkills: Added plugin skills directory: ${dir}`
|
|
1397
|
+
);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
this.runtime.logger.warn(
|
|
1400
|
+
`AgentSkills: Failed to add plugin skills directory: ${dir}`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// ============================================================
|
|
1405
|
+
// STORAGE ACCESS
|
|
1406
|
+
// ============================================================
|
|
1407
|
+
/**
|
|
1408
|
+
* Get the storage backend.
|
|
1409
|
+
*/
|
|
1410
|
+
getStorage() {
|
|
1411
|
+
return this.storage;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Get storage type.
|
|
1415
|
+
*/
|
|
1416
|
+
getStorageType() {
|
|
1417
|
+
return this.storage.type;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Check if running in memory mode.
|
|
1421
|
+
*/
|
|
1422
|
+
isMemoryMode() {
|
|
1423
|
+
return this.storage.type === "memory";
|
|
1424
|
+
}
|
|
1425
|
+
// ============================================================
|
|
1426
|
+
// SKILL DISCOVERY (Progressive Disclosure Level 1)
|
|
1427
|
+
// ============================================================
|
|
1428
|
+
/**
|
|
1429
|
+
* Get skill metadata for all loaded skills.
|
|
1430
|
+
* Returns minimal information suitable for system prompts.
|
|
1431
|
+
*/
|
|
1432
|
+
getSkillsMetadata() {
|
|
1433
|
+
return Array.from(this.loadedSkills.values()).map((skill) => ({
|
|
1434
|
+
name: skill.name,
|
|
1435
|
+
description: skill.description,
|
|
1436
|
+
location: `${skill.path}/SKILL.md`
|
|
1437
|
+
}));
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Generate XML for available skills (for system prompts).
|
|
1441
|
+
*/
|
|
1442
|
+
generateSkillsPromptXml(options = {}) {
|
|
1443
|
+
const metadata = this.getSkillsMetadata();
|
|
1444
|
+
const limited = options.maxSkills ? metadata.slice(0, options.maxSkills) : metadata;
|
|
1445
|
+
return generateSkillsXml(limited, {
|
|
1446
|
+
includeLocation: options.includeLocation ?? true
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
// ============================================================
|
|
1450
|
+
// SKILL LOADING (Progressive Disclosure Level 2)
|
|
1451
|
+
// ============================================================
|
|
1452
|
+
/**
|
|
1453
|
+
* Load all managed/installed skills from the main storage.
|
|
1454
|
+
* Respects skill source precedence ordering.
|
|
1455
|
+
*/
|
|
1456
|
+
async loadInstalledSkills() {
|
|
1457
|
+
const slugs = await this.storage.listSkills();
|
|
1458
|
+
for (const slug of slugs) {
|
|
1459
|
+
if (!this.isSkillAllowed(slug)) {
|
|
1460
|
+
this.runtime.logger.debug(
|
|
1461
|
+
`AgentSkills: Skipping managed skill ${slug} (filtered by allow/denylist)`
|
|
1462
|
+
);
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
const existing = this.loadedSkills.get(slug);
|
|
1466
|
+
if (existing && SKILL_SOURCE_PRECEDENCE[existing.source] >= SKILL_SOURCE_PRECEDENCE.managed) {
|
|
1467
|
+
this.runtime.logger.debug(
|
|
1468
|
+
`AgentSkills: Skipping managed skill ${slug} (${existing.source} version takes precedence)`
|
|
1469
|
+
);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
const skillsDir = this.storage.type === "filesystem" ? this.storage.basePath : "./skills";
|
|
1473
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
1474
|
+
this.storage,
|
|
1475
|
+
slug,
|
|
1476
|
+
"managed",
|
|
1477
|
+
skillsDir
|
|
1478
|
+
);
|
|
1479
|
+
if (skill) {
|
|
1480
|
+
if (existing) {
|
|
1481
|
+
this.runtime.logger.info(
|
|
1482
|
+
`AgentSkills: Managed skill ${slug} overrides ${existing.source} version`
|
|
1483
|
+
);
|
|
1484
|
+
skill.overrides = `${existing.source}:${existing.sourceDir}`;
|
|
1485
|
+
}
|
|
1486
|
+
this.loadedSkills.set(slug, skill);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Load a single skill by slug or path.
|
|
1492
|
+
* Checks all storage sources in precedence order.
|
|
1493
|
+
*/
|
|
1494
|
+
async loadSkill(slugOrPath, options = {}) {
|
|
1495
|
+
let slug;
|
|
1496
|
+
if (slugOrPath.includes("/")) {
|
|
1497
|
+
const parts = slugOrPath.split("/").filter(Boolean);
|
|
1498
|
+
slug = parts[parts.length - 1];
|
|
1499
|
+
} else {
|
|
1500
|
+
slug = sanitizeSlug(slugOrPath);
|
|
1501
|
+
}
|
|
1502
|
+
if (!this.isSkillAllowed(slug)) {
|
|
1503
|
+
this.runtime.logger.debug(
|
|
1504
|
+
`AgentSkills: Skill ${slug} not allowed by allow/denylist`
|
|
1505
|
+
);
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
const existing = this.loadedSkills.get(slug);
|
|
1509
|
+
if (existing) {
|
|
1510
|
+
return existing;
|
|
1511
|
+
}
|
|
1512
|
+
if (this.workspaceStorage && await this.workspaceStorage.hasSkill(slug)) {
|
|
1513
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
1514
|
+
this.workspaceStorage,
|
|
1515
|
+
slug,
|
|
1516
|
+
"workspace",
|
|
1517
|
+
this.workspaceSkillsDir
|
|
1518
|
+
);
|
|
1519
|
+
if (skill) {
|
|
1520
|
+
this.loadedSkills.set(slug, skill);
|
|
1521
|
+
return skill;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (await this.storage.hasSkill(slug)) {
|
|
1525
|
+
const skillsDir = this.storage.type === "filesystem" ? this.storage.basePath : "./skills";
|
|
1526
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
1527
|
+
this.storage,
|
|
1528
|
+
slug,
|
|
1529
|
+
"managed",
|
|
1530
|
+
skillsDir
|
|
1531
|
+
);
|
|
1532
|
+
if (skill) {
|
|
1533
|
+
this.loadedSkills.set(slug, skill);
|
|
1534
|
+
return skill;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
for (const [bundledDir, storage] of this.bundledStorages) {
|
|
1538
|
+
if (await storage.hasSkill(slug)) {
|
|
1539
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
1540
|
+
storage,
|
|
1541
|
+
slug,
|
|
1542
|
+
"bundled",
|
|
1543
|
+
bundledDir
|
|
1544
|
+
);
|
|
1545
|
+
if (skill) {
|
|
1546
|
+
this.loadedSkills.set(slug, skill);
|
|
1547
|
+
return skill;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
for (const [pluginDir, storage] of this.pluginStorages) {
|
|
1552
|
+
if (await storage.hasSkill(slug)) {
|
|
1553
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
1554
|
+
storage,
|
|
1555
|
+
slug,
|
|
1556
|
+
"plugin",
|
|
1557
|
+
pluginDir
|
|
1558
|
+
);
|
|
1559
|
+
if (skill) {
|
|
1560
|
+
this.loadedSkills.set(slug, skill);
|
|
1561
|
+
return skill;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
for (const [extraDir, storage] of this.extraStorages) {
|
|
1566
|
+
if (await storage.hasSkill(slug)) {
|
|
1567
|
+
const skill = await this.loadSkillFromStorageWithSource(
|
|
1568
|
+
storage,
|
|
1569
|
+
slug,
|
|
1570
|
+
"extra",
|
|
1571
|
+
extraDir
|
|
1572
|
+
);
|
|
1573
|
+
if (skill) {
|
|
1574
|
+
this.loadedSkills.set(slug, skill);
|
|
1575
|
+
return skill;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Load a skill directly from content (memory mode convenience).
|
|
1583
|
+
*/
|
|
1584
|
+
async loadSkillFromContent(slug, skillMdContent, additionalFiles) {
|
|
1585
|
+
if (!(this.storage instanceof MemorySkillStore)) {
|
|
1586
|
+
throw new Error("loadSkillFromContent requires memory storage mode");
|
|
1587
|
+
}
|
|
1588
|
+
await this.storage.loadFromContent(
|
|
1589
|
+
slug,
|
|
1590
|
+
skillMdContent,
|
|
1591
|
+
additionalFiles
|
|
1592
|
+
);
|
|
1593
|
+
return this.loadSkill(slug);
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Get skill instructions (body without frontmatter).
|
|
1597
|
+
*/
|
|
1598
|
+
getSkillInstructions(slug) {
|
|
1599
|
+
try {
|
|
1600
|
+
const skill = this.loadedSkills.get(sanitizeSlug(slug));
|
|
1601
|
+
if (!skill) return null;
|
|
1602
|
+
const body = extractBody(skill.content);
|
|
1603
|
+
return {
|
|
1604
|
+
slug: skill.slug,
|
|
1605
|
+
body,
|
|
1606
|
+
estimatedTokens: estimateTokens(body)
|
|
1607
|
+
};
|
|
1608
|
+
} catch {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
// ============================================================
|
|
1613
|
+
// RESOURCE ACCESS (Progressive Disclosure Level 3)
|
|
1614
|
+
// ============================================================
|
|
1615
|
+
/**
|
|
1616
|
+
* Get the appropriate storage for a skill based on its source.
|
|
1617
|
+
*/
|
|
1618
|
+
getStorageForSkill(skill) {
|
|
1619
|
+
switch (skill.source) {
|
|
1620
|
+
case "workspace":
|
|
1621
|
+
if (this.workspaceStorage) return this.workspaceStorage;
|
|
1622
|
+
break;
|
|
1623
|
+
case "bundled":
|
|
1624
|
+
if (skill.bundledDir) {
|
|
1625
|
+
const bundledStorage = this.bundledStorages.get(skill.bundledDir);
|
|
1626
|
+
if (bundledStorage) return bundledStorage;
|
|
1627
|
+
}
|
|
1628
|
+
break;
|
|
1629
|
+
case "plugin":
|
|
1630
|
+
if (skill.sourceDir) {
|
|
1631
|
+
const pluginStorage = this.pluginStorages.get(skill.sourceDir);
|
|
1632
|
+
if (pluginStorage) return pluginStorage;
|
|
1633
|
+
}
|
|
1634
|
+
break;
|
|
1635
|
+
case "extra":
|
|
1636
|
+
if (skill.sourceDir) {
|
|
1637
|
+
const extraStorage = this.extraStorages.get(skill.sourceDir);
|
|
1638
|
+
if (extraStorage) return extraStorage;
|
|
1639
|
+
}
|
|
1640
|
+
break;
|
|
1641
|
+
case "managed":
|
|
1642
|
+
default:
|
|
1643
|
+
return this.storage;
|
|
1644
|
+
}
|
|
1645
|
+
return this.storage;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Read a reference file from a skill.
|
|
1649
|
+
* Injects per-skill environment variables if configured.
|
|
1650
|
+
*/
|
|
1651
|
+
async readReference(slug, filename) {
|
|
1652
|
+
const safeSlug = sanitizeSlug(slug);
|
|
1653
|
+
const skill = this.loadedSkills.get(safeSlug);
|
|
1654
|
+
if (!skill) return null;
|
|
1655
|
+
const safeName = filename.split("/").pop() || filename;
|
|
1656
|
+
const storage = this.getStorageForSkill(skill);
|
|
1657
|
+
const content = await storage.loadFile(safeSlug, `references/${safeName}`);
|
|
1658
|
+
return typeof content === "string" ? content : null;
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Get the path to a script file.
|
|
1662
|
+
* Returns the actual filesystem path for all skill sources.
|
|
1663
|
+
*/
|
|
1664
|
+
getScriptPath(slug, filename) {
|
|
1665
|
+
const skill = this.loadedSkills.get(sanitizeSlug(slug));
|
|
1666
|
+
if (!skill) return null;
|
|
1667
|
+
const safeName = filename.split("/").pop() || filename;
|
|
1668
|
+
if (!skill.scripts.includes(safeName)) return null;
|
|
1669
|
+
return `${skill.path}/scripts/${safeName}`;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Read a script file content.
|
|
1673
|
+
*/
|
|
1674
|
+
async readScript(slug, filename) {
|
|
1675
|
+
const safeSlug = sanitizeSlug(slug);
|
|
1676
|
+
const skill = this.loadedSkills.get(safeSlug);
|
|
1677
|
+
if (!skill) return null;
|
|
1678
|
+
const safeName = filename.split("/").pop() || filename;
|
|
1679
|
+
const storage = this.getStorageForSkill(skill);
|
|
1680
|
+
const content = await storage.loadFile(safeSlug, `scripts/${safeName}`);
|
|
1681
|
+
return typeof content === "string" ? content : null;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Get the path to an asset file.
|
|
1685
|
+
*/
|
|
1686
|
+
getAssetPath(slug, filename) {
|
|
1687
|
+
const skill = this.loadedSkills.get(sanitizeSlug(slug));
|
|
1688
|
+
if (!skill) return null;
|
|
1689
|
+
const safeName = filename.split("/").pop() || filename;
|
|
1690
|
+
if (!skill.assets.includes(safeName)) return null;
|
|
1691
|
+
return `${skill.path}/assets/${safeName}`;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Read an asset file content.
|
|
1695
|
+
*/
|
|
1696
|
+
async readAsset(slug, filename) {
|
|
1697
|
+
const safeSlug = sanitizeSlug(slug);
|
|
1698
|
+
const skill = this.loadedSkills.get(safeSlug);
|
|
1699
|
+
if (!skill) return null;
|
|
1700
|
+
const safeName = filename.split("/").pop() || filename;
|
|
1701
|
+
const storage = this.getStorageForSkill(skill);
|
|
1702
|
+
const content = await storage.loadFile(safeSlug, `assets/${safeName}`);
|
|
1703
|
+
if (content instanceof Uint8Array) return content;
|
|
1704
|
+
if (typeof content === "string") return new TextEncoder().encode(content);
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Get the environment to use when executing a skill script.
|
|
1709
|
+
* Merges system env with skill-specific overrides.
|
|
1710
|
+
*/
|
|
1711
|
+
getSkillExecutionEnv(slug) {
|
|
1712
|
+
const skillEnv = this.getSkillEnv(slug);
|
|
1713
|
+
const apiKey = this.getSkillApiKey(slug);
|
|
1714
|
+
const env = {
|
|
1715
|
+
...process.env,
|
|
1716
|
+
...skillEnv
|
|
1717
|
+
};
|
|
1718
|
+
if (apiKey) {
|
|
1719
|
+
env.SKILL_API_KEY = apiKey;
|
|
1720
|
+
env[`${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`] = apiKey;
|
|
1721
|
+
}
|
|
1722
|
+
return env;
|
|
1723
|
+
}
|
|
1724
|
+
// ============================================================
|
|
1725
|
+
// SKILL RETRIEVAL
|
|
1726
|
+
// ============================================================
|
|
1727
|
+
/**
|
|
1728
|
+
* Get all loaded skills.
|
|
1729
|
+
*/
|
|
1730
|
+
getLoadedSkills() {
|
|
1731
|
+
return Array.from(this.loadedSkills.values());
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Get only bundled skills.
|
|
1735
|
+
*/
|
|
1736
|
+
getBundledSkills() {
|
|
1737
|
+
return Array.from(this.loadedSkills.values()).filter(
|
|
1738
|
+
(s) => s.source === "bundled"
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Get only managed/installed skills.
|
|
1743
|
+
*/
|
|
1744
|
+
getManagedSkills() {
|
|
1745
|
+
return Array.from(this.loadedSkills.values()).filter(
|
|
1746
|
+
(s) => s.source === "managed"
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Get only workspace skills.
|
|
1751
|
+
*/
|
|
1752
|
+
getWorkspaceSkills() {
|
|
1753
|
+
return Array.from(this.loadedSkills.values()).filter(
|
|
1754
|
+
(s) => s.source === "workspace"
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Get only plugin-contributed skills.
|
|
1759
|
+
*/
|
|
1760
|
+
getPluginSkills() {
|
|
1761
|
+
return Array.from(this.loadedSkills.values()).filter(
|
|
1762
|
+
(s) => s.source === "plugin"
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Get skills by source type.
|
|
1767
|
+
*/
|
|
1768
|
+
getSkillsBySource(source) {
|
|
1769
|
+
return Array.from(this.loadedSkills.values()).filter(
|
|
1770
|
+
(s) => s.source === source
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Get a specific loaded skill.
|
|
1775
|
+
*/
|
|
1776
|
+
getLoadedSkill(slug) {
|
|
1777
|
+
try {
|
|
1778
|
+
return this.loadedSkills.get(sanitizeSlug(slug));
|
|
1779
|
+
} catch {
|
|
1780
|
+
return void 0;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Check if a skill is loaded.
|
|
1785
|
+
*/
|
|
1786
|
+
isLoaded(slug) {
|
|
1787
|
+
try {
|
|
1788
|
+
return this.loadedSkills.has(sanitizeSlug(slug));
|
|
1789
|
+
} catch {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Check if a skill is bundled (read-only).
|
|
1795
|
+
*/
|
|
1796
|
+
isBundled(slug) {
|
|
1797
|
+
const skill = this.loadedSkills.get(slug);
|
|
1798
|
+
return skill?.source === "bundled";
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Check if a skill is installed (in managed storage, not bundled).
|
|
1802
|
+
*/
|
|
1803
|
+
async isInstalled(slug) {
|
|
1804
|
+
try {
|
|
1805
|
+
return await this.storage.hasSkill(sanitizeSlug(slug));
|
|
1806
|
+
} catch {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Check if a skill exists (either bundled or installed).
|
|
1812
|
+
*/
|
|
1813
|
+
async exists(slug) {
|
|
1814
|
+
const safeSlug = sanitizeSlug(slug);
|
|
1815
|
+
for (const storage of this.bundledStorages.values()) {
|
|
1816
|
+
if (await storage.hasSkill(safeSlug)) return true;
|
|
1817
|
+
}
|
|
1818
|
+
return this.storage.hasSkill(safeSlug);
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Unload a skill from memory (keeps in storage).
|
|
1822
|
+
*/
|
|
1823
|
+
unloadSkill(slug) {
|
|
1824
|
+
try {
|
|
1825
|
+
return this.loadedSkills.delete(sanitizeSlug(slug));
|
|
1826
|
+
} catch {
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Get the list of bundled skills directories.
|
|
1832
|
+
*/
|
|
1833
|
+
getBundledSkillsDirs() {
|
|
1834
|
+
return [...this.bundledSkillsDirs];
|
|
1835
|
+
}
|
|
1836
|
+
// ============================================================
|
|
1837
|
+
// REGISTRY OPERATIONS (ClawHub Integration)
|
|
1838
|
+
// ============================================================
|
|
1839
|
+
/**
|
|
1840
|
+
* Get the full skill catalog from ClawHub.
|
|
1841
|
+
*/
|
|
1842
|
+
async getCatalog(options = {}) {
|
|
1843
|
+
const ttl = options.notOlderThan ?? CACHE_TTL.CATALOG;
|
|
1844
|
+
if (!options.forceRefresh && this.catalogCache) {
|
|
1845
|
+
const age = Date.now() - this.catalogCache.cachedAt;
|
|
1846
|
+
if (age < ttl) {
|
|
1847
|
+
return this.catalogCache.data;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
const entries = [];
|
|
1852
|
+
let cursor;
|
|
1853
|
+
do {
|
|
1854
|
+
const url = `${this.apiBase}/api/v1/skills?limit=100${cursor ? `&cursor=${cursor}` : ""}`;
|
|
1855
|
+
const response = await fetch(url, {
|
|
1856
|
+
headers: { Accept: "application/json" }
|
|
1857
|
+
});
|
|
1858
|
+
if (!response.ok) {
|
|
1859
|
+
throw new Error(`Catalog fetch failed: ${response.status}`);
|
|
1860
|
+
}
|
|
1861
|
+
const data = await response.json();
|
|
1862
|
+
entries.push(...data.items);
|
|
1863
|
+
cursor = data.nextCursor;
|
|
1864
|
+
} while (cursor);
|
|
1865
|
+
this.catalogCache = { data: entries, cachedAt: Date.now() };
|
|
1866
|
+
if (this.storage.type === "filesystem") {
|
|
1867
|
+
await this.saveCatalogToDisk();
|
|
1868
|
+
}
|
|
1869
|
+
return entries;
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
this.runtime.logger.error(`AgentSkills: Catalog fetch error: ${error}`);
|
|
1872
|
+
return this.catalogCache?.data || [];
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Search ClawHub for skills.
|
|
1877
|
+
*/
|
|
1878
|
+
async search(query, limit = 10, options = {}) {
|
|
1879
|
+
const cacheKey = `${query}:${limit}`;
|
|
1880
|
+
const ttl = options.notOlderThan ?? CACHE_TTL.SEARCH;
|
|
1881
|
+
if (!options.forceRefresh) {
|
|
1882
|
+
const cached = this.searchCache.get(cacheKey);
|
|
1883
|
+
if (cached && Date.now() - cached.cachedAt < ttl) {
|
|
1884
|
+
return cached.data;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
try {
|
|
1888
|
+
const url = `${this.apiBase}/api/v1/search?q=${encodeURIComponent(query)}&limit=${limit}`;
|
|
1889
|
+
const response = await fetch(url, {
|
|
1890
|
+
headers: { Accept: "application/json" }
|
|
1891
|
+
});
|
|
1892
|
+
if (!response.ok) {
|
|
1893
|
+
throw new Error(`Search failed: ${response.status}`);
|
|
1894
|
+
}
|
|
1895
|
+
const data = await response.json();
|
|
1896
|
+
const results = data.results || [];
|
|
1897
|
+
this.searchCache.set(cacheKey, { data: results, cachedAt: Date.now() });
|
|
1898
|
+
return results;
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
this.runtime.logger.error(`AgentSkills: Search error: ${error}`);
|
|
1901
|
+
return this.searchCache.get(cacheKey)?.data || [];
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Get skill details from ClawHub.
|
|
1906
|
+
*/
|
|
1907
|
+
async getSkillDetails(slug, options = {}) {
|
|
1908
|
+
const safeSlug = sanitizeSlug(slug);
|
|
1909
|
+
const ttl = options.notOlderThan ?? CACHE_TTL.SKILL_DETAILS;
|
|
1910
|
+
if (!options.forceRefresh) {
|
|
1911
|
+
const cached = this.detailsCache.get(safeSlug);
|
|
1912
|
+
if (cached && Date.now() - cached.cachedAt < ttl) {
|
|
1913
|
+
return cached.data;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
try {
|
|
1917
|
+
const url = `${this.apiBase}/api/v1/skills/${safeSlug}`;
|
|
1918
|
+
const response = await fetch(url, {
|
|
1919
|
+
headers: { Accept: "application/json" }
|
|
1920
|
+
});
|
|
1921
|
+
if (!response.ok) {
|
|
1922
|
+
if (response.status === 404) return null;
|
|
1923
|
+
throw new Error(`Details fetch failed: ${response.status}`);
|
|
1924
|
+
}
|
|
1925
|
+
const details = await response.json();
|
|
1926
|
+
this.detailsCache.set(safeSlug, { data: details, cachedAt: Date.now() });
|
|
1927
|
+
return details;
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
this.runtime.logger.error(`AgentSkills: Details fetch error: ${error}`);
|
|
1930
|
+
return this.detailsCache.get(safeSlug)?.data || null;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
// ============================================================
|
|
1934
|
+
// INSTALLATION
|
|
1935
|
+
// ============================================================
|
|
1936
|
+
/**
|
|
1937
|
+
* Install a skill from ClawHub.
|
|
1938
|
+
*
|
|
1939
|
+
* In memory mode: Downloads and loads skill into memory.
|
|
1940
|
+
* In filesystem mode: Downloads, extracts to disk, and loads.
|
|
1941
|
+
*/
|
|
1942
|
+
async install(slug, options = {}) {
|
|
1943
|
+
try {
|
|
1944
|
+
const safeSlug = sanitizeSlug(slug);
|
|
1945
|
+
const version = options.version || "latest";
|
|
1946
|
+
if (!options.force && await this.isInstalled(safeSlug)) {
|
|
1947
|
+
this.runtime.logger.info(`AgentSkills: ${safeSlug} already installed`);
|
|
1948
|
+
return true;
|
|
1949
|
+
}
|
|
1950
|
+
this.runtime.logger.info(
|
|
1951
|
+
`AgentSkills: Installing ${safeSlug}@${version}...`
|
|
1952
|
+
);
|
|
1953
|
+
const details = await this.getSkillDetails(safeSlug);
|
|
1954
|
+
if (!details) {
|
|
1955
|
+
throw new Error(`Skill "${safeSlug}" not found`);
|
|
1956
|
+
}
|
|
1957
|
+
const resolvedVersion = version === "latest" ? details.latestVersion.version : version;
|
|
1958
|
+
const downloadUrl = `${this.apiBase}/api/v1/download?slug=${safeSlug}&version=${resolvedVersion}`;
|
|
1959
|
+
const response = await fetch(downloadUrl);
|
|
1960
|
+
if (!response.ok) {
|
|
1961
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
1962
|
+
}
|
|
1963
|
+
const zipBuffer = await response.arrayBuffer();
|
|
1964
|
+
if (zipBuffer.byteLength > MAX_PACKAGE_SIZE) {
|
|
1965
|
+
throw new Error(
|
|
1966
|
+
`Package too large (max ${MAX_PACKAGE_SIZE / 1024 / 1024}MB)`
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
if (this.storage instanceof MemorySkillStore) {
|
|
1970
|
+
await this.storage.loadFromZip(
|
|
1971
|
+
safeSlug,
|
|
1972
|
+
new Uint8Array(zipBuffer)
|
|
1973
|
+
);
|
|
1974
|
+
} else if (this.storage instanceof FileSystemSkillStore) {
|
|
1975
|
+
await this.storage.saveFromZip(
|
|
1976
|
+
safeSlug,
|
|
1977
|
+
new Uint8Array(zipBuffer)
|
|
1978
|
+
);
|
|
1979
|
+
await this.updateLockfile(safeSlug, resolvedVersion);
|
|
1980
|
+
}
|
|
1981
|
+
await this.loadSkill(safeSlug);
|
|
1982
|
+
this.runtime.logger.info(
|
|
1983
|
+
`AgentSkills: Installed ${safeSlug}@${resolvedVersion}`
|
|
1984
|
+
);
|
|
1985
|
+
return true;
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
this.runtime.logger.error(`AgentSkills: Install error: ${error}`);
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Install a skill from a GitHub repository.
|
|
1993
|
+
*
|
|
1994
|
+
* Supports both full repo paths and shorthand:
|
|
1995
|
+
* - "owner/repo" - Uses repo root
|
|
1996
|
+
* - "owner/repo/path/to/skill" - Uses specific subdirectory
|
|
1997
|
+
* - "https://github.com/owner/repo" - Full URL
|
|
1998
|
+
*
|
|
1999
|
+
* Downloads SKILL.md and any additional files in the skill directory.
|
|
2000
|
+
*/
|
|
2001
|
+
async installFromGitHub(repo, options = {}) {
|
|
2002
|
+
try {
|
|
2003
|
+
let owner;
|
|
2004
|
+
let repoName;
|
|
2005
|
+
let skillPath = options.path || "";
|
|
2006
|
+
const branch = options.branch || "main";
|
|
2007
|
+
if (repo.startsWith("http")) {
|
|
2008
|
+
const url = new URL(repo);
|
|
2009
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
2010
|
+
if (parts.length < 2) {
|
|
2011
|
+
throw new Error("Invalid GitHub URL");
|
|
2012
|
+
}
|
|
2013
|
+
owner = parts[0];
|
|
2014
|
+
repoName = parts[1];
|
|
2015
|
+
if (parts.length > 2) {
|
|
2016
|
+
const treeIdx = parts.indexOf("tree");
|
|
2017
|
+
if (treeIdx >= 0 && parts.length > treeIdx + 2) {
|
|
2018
|
+
skillPath = parts.slice(treeIdx + 2).join("/");
|
|
2019
|
+
} else if (parts.length > 2) {
|
|
2020
|
+
skillPath = parts.slice(2).join("/");
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
} else {
|
|
2024
|
+
const parts = repo.split("/");
|
|
2025
|
+
if (parts.length < 2) {
|
|
2026
|
+
throw new Error(
|
|
2027
|
+
"Invalid repo format. Use owner/repo or owner/repo/path"
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
owner = parts[0];
|
|
2031
|
+
repoName = parts[1];
|
|
2032
|
+
if (parts.length > 2) {
|
|
2033
|
+
skillPath = parts.slice(2).join("/");
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
const slug = skillPath ? skillPath.split("/").pop() || repoName : repoName;
|
|
2037
|
+
const safeSlug = sanitizeSlug(slug);
|
|
2038
|
+
if (!options.force && await this.isInstalled(safeSlug)) {
|
|
2039
|
+
this.runtime.logger.info(
|
|
2040
|
+
`AgentSkills: ${safeSlug} already installed from GitHub`
|
|
2041
|
+
);
|
|
2042
|
+
return true;
|
|
2043
|
+
}
|
|
2044
|
+
this.runtime.logger.info(
|
|
2045
|
+
`AgentSkills: Installing from GitHub ${owner}/${repoName}/${skillPath}...`
|
|
2046
|
+
);
|
|
2047
|
+
const basePath = skillPath ? `${skillPath}/` : "";
|
|
2048
|
+
const rawBase = `https://raw.githubusercontent.com/${owner}/${repoName}/${branch}/${basePath}`;
|
|
2049
|
+
const skillMdUrl = `${rawBase}SKILL.md`;
|
|
2050
|
+
const response = await fetch(skillMdUrl);
|
|
2051
|
+
if (!response.ok) {
|
|
2052
|
+
throw new Error(
|
|
2053
|
+
`Failed to fetch SKILL.md: ${response.status} from ${skillMdUrl}`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
const skillMdContent = await response.text();
|
|
2057
|
+
const files = [
|
|
2058
|
+
{ name: "SKILL.md", content: skillMdContent }
|
|
2059
|
+
];
|
|
2060
|
+
try {
|
|
2061
|
+
const readmeUrl = `${rawBase}README.md`;
|
|
2062
|
+
const readmeResponse = await fetch(readmeUrl);
|
|
2063
|
+
if (readmeResponse.ok) {
|
|
2064
|
+
const readmeContent = await readmeResponse.text();
|
|
2065
|
+
files.push({ name: "README.md", content: readmeContent });
|
|
2066
|
+
}
|
|
2067
|
+
} catch {
|
|
2068
|
+
}
|
|
2069
|
+
if (this.storage instanceof MemorySkillStore) {
|
|
2070
|
+
await this.storage.savePackage({
|
|
2071
|
+
slug: safeSlug,
|
|
2072
|
+
files,
|
|
2073
|
+
loadedAt: Date.now()
|
|
2074
|
+
});
|
|
2075
|
+
} else if (this.storage instanceof FileSystemSkillStore) {
|
|
2076
|
+
const fs = await import("fs/promises");
|
|
2077
|
+
const path2 = await import("path");
|
|
2078
|
+
const skillDir = path2.join(
|
|
2079
|
+
this.storage.basePath,
|
|
2080
|
+
safeSlug
|
|
2081
|
+
);
|
|
2082
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
2083
|
+
for (const file of files) {
|
|
2084
|
+
await fs.writeFile(path2.join(skillDir, file.name), file.content);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
await this.loadSkill(safeSlug);
|
|
2088
|
+
this.runtime.logger.info(
|
|
2089
|
+
`AgentSkills: Installed ${safeSlug} from GitHub`
|
|
2090
|
+
);
|
|
2091
|
+
return true;
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
this.runtime.logger.error(`AgentSkills: GitHub install error: ${error}`);
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Install a skill from a direct URL to a SKILL.md file or zip package.
|
|
2099
|
+
*/
|
|
2100
|
+
async installFromUrl(url, options = {}) {
|
|
2101
|
+
try {
|
|
2102
|
+
const response = await fetch(url);
|
|
2103
|
+
if (!response.ok) {
|
|
2104
|
+
throw new Error(`Failed to fetch: ${response.status}`);
|
|
2105
|
+
}
|
|
2106
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2107
|
+
const urlPath = new URL(url).pathname;
|
|
2108
|
+
const derivedSlug = options.slug || urlPath.split("/").filter(Boolean).pop()?.replace(/\.(md|zip)$/i, "") || "skill";
|
|
2109
|
+
const safeSlug = sanitizeSlug(derivedSlug);
|
|
2110
|
+
if (contentType.includes("application/zip") || url.endsWith(".zip")) {
|
|
2111
|
+
const zipBuffer = await response.arrayBuffer();
|
|
2112
|
+
if (zipBuffer.byteLength > MAX_PACKAGE_SIZE) {
|
|
2113
|
+
throw new Error(
|
|
2114
|
+
`Package too large (max ${MAX_PACKAGE_SIZE / 1024 / 1024}MB)`
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
if (this.storage instanceof MemorySkillStore) {
|
|
2118
|
+
await this.storage.loadFromZip(
|
|
2119
|
+
safeSlug,
|
|
2120
|
+
new Uint8Array(zipBuffer)
|
|
2121
|
+
);
|
|
2122
|
+
} else if (this.storage instanceof FileSystemSkillStore) {
|
|
2123
|
+
await this.storage.saveFromZip(
|
|
2124
|
+
safeSlug,
|
|
2125
|
+
new Uint8Array(zipBuffer)
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
} else {
|
|
2129
|
+
const content = await response.text();
|
|
2130
|
+
const files = [
|
|
2131
|
+
{ name: "SKILL.md", content }
|
|
2132
|
+
];
|
|
2133
|
+
if (this.storage instanceof MemorySkillStore) {
|
|
2134
|
+
await this.storage.savePackage({
|
|
2135
|
+
slug: safeSlug,
|
|
2136
|
+
files,
|
|
2137
|
+
loadedAt: Date.now()
|
|
2138
|
+
});
|
|
2139
|
+
} else if (this.storage instanceof FileSystemSkillStore) {
|
|
2140
|
+
const fs = await import("fs/promises");
|
|
2141
|
+
const path2 = await import("path");
|
|
2142
|
+
const skillDir = path2.join(
|
|
2143
|
+
this.storage.basePath,
|
|
2144
|
+
safeSlug
|
|
2145
|
+
);
|
|
2146
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
2147
|
+
for (const file of files) {
|
|
2148
|
+
await fs.writeFile(path2.join(skillDir, file.name), file.content);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
await this.loadSkill(safeSlug);
|
|
2153
|
+
this.runtime.logger.info(`AgentSkills: Installed ${safeSlug} from URL`);
|
|
2154
|
+
return true;
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
this.runtime.logger.error(`AgentSkills: URL install error: ${error}`);
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Uninstall a skill (remove from storage and memory).
|
|
2162
|
+
* Cannot uninstall bundled skills - they are read-only.
|
|
2163
|
+
*/
|
|
2164
|
+
async uninstall(slug) {
|
|
2165
|
+
const safeSlug = sanitizeSlug(slug);
|
|
2166
|
+
const existing = this.loadedSkills.get(safeSlug);
|
|
2167
|
+
if (existing?.source === "bundled") {
|
|
2168
|
+
this.runtime.logger.warn(
|
|
2169
|
+
`AgentSkills: Cannot uninstall bundled skill ${safeSlug}`
|
|
2170
|
+
);
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
this.loadedSkills.delete(safeSlug);
|
|
2174
|
+
const deleted = await this.storage.deleteSkill(safeSlug);
|
|
2175
|
+
if (deleted) {
|
|
2176
|
+
this.runtime.logger.info(`AgentSkills: Uninstalled ${safeSlug}`);
|
|
2177
|
+
}
|
|
2178
|
+
return deleted;
|
|
2179
|
+
}
|
|
2180
|
+
// ============================================================
|
|
2181
|
+
// SYNC OPERATIONS
|
|
2182
|
+
// ============================================================
|
|
2183
|
+
/**
|
|
2184
|
+
* Sync the skill catalog from ClawHub.
|
|
2185
|
+
*/
|
|
2186
|
+
async syncCatalog() {
|
|
2187
|
+
const oldCount = this.catalogCache?.data.length || 0;
|
|
2188
|
+
await this.getCatalog({ forceRefresh: true });
|
|
2189
|
+
const newCount = this.catalogCache?.data.length || 0;
|
|
2190
|
+
return {
|
|
2191
|
+
added: Math.max(0, newCount - oldCount),
|
|
2192
|
+
updated: newCount
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Get catalog stats for logging.
|
|
2197
|
+
*/
|
|
2198
|
+
getCatalogStats() {
|
|
2199
|
+
const categories = /* @__PURE__ */ new Set();
|
|
2200
|
+
if (this.catalogCache?.data) {
|
|
2201
|
+
for (const skill of this.catalogCache.data) {
|
|
2202
|
+
if (skill.tags) {
|
|
2203
|
+
for (const tag of Object.keys(skill.tags)) {
|
|
2204
|
+
if (tag !== "latest") categories.add(tag);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
total: this.catalogCache?.data.length || 0,
|
|
2211
|
+
installed: this.loadedSkills.size,
|
|
2212
|
+
// For backward compat
|
|
2213
|
+
loaded: this.loadedSkills.size,
|
|
2214
|
+
cachedAt: this.catalogCache?.cachedAt || null,
|
|
2215
|
+
storageType: this.storage.type,
|
|
2216
|
+
categories: Array.from(categories).slice(0, 20)
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
// ============================================================
|
|
2220
|
+
// PRIVATE HELPERS
|
|
2221
|
+
// ============================================================
|
|
2222
|
+
async getLockfileVersion(slug) {
|
|
2223
|
+
if (!this.lockfilePath || this.storage.type !== "filesystem") return null;
|
|
2224
|
+
try {
|
|
2225
|
+
const fs = await import("fs");
|
|
2226
|
+
if (!fs.existsSync(this.lockfilePath)) return null;
|
|
2227
|
+
const lockfile = JSON.parse(fs.readFileSync(this.lockfilePath, "utf-8"));
|
|
2228
|
+
return lockfile[slug]?.version || null;
|
|
2229
|
+
} catch {
|
|
2230
|
+
return null;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
async updateLockfile(slug, version) {
|
|
2234
|
+
if (!this.lockfilePath || this.storage.type !== "filesystem") return;
|
|
2235
|
+
try {
|
|
2236
|
+
const fs = await import("fs");
|
|
2237
|
+
const path2 = await import("path");
|
|
2238
|
+
const cacheDir = path2.dirname(this.lockfilePath);
|
|
2239
|
+
if (!fs.existsSync(cacheDir)) {
|
|
2240
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
2241
|
+
}
|
|
2242
|
+
let lockfile = {};
|
|
2243
|
+
if (fs.existsSync(this.lockfilePath)) {
|
|
2244
|
+
try {
|
|
2245
|
+
lockfile = JSON.parse(fs.readFileSync(this.lockfilePath, "utf-8"));
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
lockfile[slug] = { version, installedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2250
|
+
fs.writeFileSync(this.lockfilePath, JSON.stringify(lockfile, null, 2));
|
|
2251
|
+
} catch {
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
async loadCatalogFromDisk() {
|
|
2255
|
+
if (!this.catalogCachePath || this.storage.type !== "filesystem") return;
|
|
2256
|
+
try {
|
|
2257
|
+
const fs = await import("fs");
|
|
2258
|
+
if (!fs.existsSync(this.catalogCachePath)) return;
|
|
2259
|
+
const cached = JSON.parse(
|
|
2260
|
+
fs.readFileSync(this.catalogCachePath, "utf-8")
|
|
2261
|
+
);
|
|
2262
|
+
if (cached.data && cached.cachedAt) {
|
|
2263
|
+
this.catalogCache = cached;
|
|
2264
|
+
this.runtime.logger.debug(
|
|
2265
|
+
`AgentSkills: Loaded catalog cache (${cached.data.length} skills)`
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
} catch {
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
async saveCatalogToDisk() {
|
|
2272
|
+
if (!this.catalogCache || !this.catalogCachePath || this.storage.type !== "filesystem")
|
|
2273
|
+
return;
|
|
2274
|
+
try {
|
|
2275
|
+
const fs = await import("fs");
|
|
2276
|
+
const path2 = await import("path");
|
|
2277
|
+
const cacheDir = path2.dirname(this.catalogCachePath);
|
|
2278
|
+
if (!fs.existsSync(cacheDir)) {
|
|
2279
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
2280
|
+
}
|
|
2281
|
+
fs.writeFileSync(
|
|
2282
|
+
this.catalogCachePath,
|
|
2283
|
+
JSON.stringify(this.catalogCache, null, 2)
|
|
2284
|
+
);
|
|
2285
|
+
} catch {
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
// src/actions/search-skills.ts
|
|
2291
|
+
var searchSkillsAction = {
|
|
2292
|
+
name: "SEARCH_SKILLS",
|
|
2293
|
+
similes: ["BROWSE_SKILLS", "LIST_SKILLS", "FIND_SKILLS"],
|
|
2294
|
+
description: "Search the skill registry for available skills by keyword or category.",
|
|
2295
|
+
validate: async (runtime, _message) => {
|
|
2296
|
+
const service = runtime.getService(
|
|
2297
|
+
"AGENT_SKILLS_SERVICE"
|
|
2298
|
+
);
|
|
2299
|
+
return !!service;
|
|
2300
|
+
},
|
|
2301
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
2302
|
+
try {
|
|
2303
|
+
const service = runtime.getService(
|
|
2304
|
+
"AGENT_SKILLS_SERVICE"
|
|
2305
|
+
);
|
|
2306
|
+
if (!service) {
|
|
2307
|
+
throw new Error("AgentSkillsService not available");
|
|
2308
|
+
}
|
|
2309
|
+
const query = message.content?.text || "";
|
|
2310
|
+
const results = await service.search(query, 10);
|
|
2311
|
+
if (results.length === 0) {
|
|
2312
|
+
const text2 = `No skills found matching "${query}".`;
|
|
2313
|
+
if (callback) await callback({ text: text2 });
|
|
2314
|
+
return { success: true, text: text2, data: { results: [] } };
|
|
2315
|
+
}
|
|
2316
|
+
const skillList = results.map(
|
|
2317
|
+
(r, i) => `${i + 1}. **${r.displayName}** (\`${r.slug}\`)
|
|
2318
|
+
${r.summary}`
|
|
2319
|
+
).join("\n\n");
|
|
2320
|
+
const text = `## Skills matching "${query}"
|
|
2321
|
+
|
|
2322
|
+
${skillList}
|
|
2323
|
+
|
|
2324
|
+
Use GET_SKILL_GUIDANCE with a skill name to get detailed instructions.`;
|
|
2325
|
+
if (callback) await callback({ text });
|
|
2326
|
+
return {
|
|
2327
|
+
success: true,
|
|
2328
|
+
text,
|
|
2329
|
+
data: { results }
|
|
2330
|
+
};
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2333
|
+
if (callback) {
|
|
2334
|
+
await callback({ text: `Error searching skills: ${errorMsg}` });
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
success: false,
|
|
2338
|
+
error: error instanceof Error ? error : new Error(errorMsg)
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
},
|
|
2342
|
+
examples: [
|
|
2343
|
+
[
|
|
2344
|
+
{
|
|
2345
|
+
name: "{{userName}}",
|
|
2346
|
+
content: { text: "Search for skills about data analysis" }
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
name: "{{agentName}}",
|
|
2350
|
+
content: {
|
|
2351
|
+
text: '## Skills matching "data analysis"\n\n1. **Data Analysis** (`data-analysis`)\n Analyze datasets and generate insights...',
|
|
2352
|
+
actions: ["SEARCH_SKILLS"]
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
]
|
|
2356
|
+
]
|
|
2357
|
+
};
|
|
2358
|
+
|
|
2359
|
+
// src/actions/get-skill-details.ts
|
|
2360
|
+
var getSkillDetailsAction = {
|
|
2361
|
+
name: "GET_SKILL_DETAILS",
|
|
2362
|
+
similes: ["SKILL_INFO", "SKILL_DETAILS"],
|
|
2363
|
+
description: "Get detailed information about a specific skill including version, owner, and stats.",
|
|
2364
|
+
validate: async (runtime, _message) => {
|
|
2365
|
+
const service = runtime.getService(
|
|
2366
|
+
"AGENT_SKILLS_SERVICE"
|
|
2367
|
+
);
|
|
2368
|
+
return !!service;
|
|
2369
|
+
},
|
|
2370
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
2371
|
+
try {
|
|
2372
|
+
const service = runtime.getService(
|
|
2373
|
+
"AGENT_SKILLS_SERVICE"
|
|
2374
|
+
);
|
|
2375
|
+
if (!service) {
|
|
2376
|
+
throw new Error("AgentSkillsService not available");
|
|
2377
|
+
}
|
|
2378
|
+
const opts = options;
|
|
2379
|
+
const slug = opts?.slug || extractSlugFromText(message.content?.text || "");
|
|
2380
|
+
if (!slug) {
|
|
2381
|
+
return {
|
|
2382
|
+
success: false,
|
|
2383
|
+
error: new Error("Skill slug is required")
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
const details = await service.getSkillDetails(slug);
|
|
2387
|
+
if (!details) {
|
|
2388
|
+
const text2 = `Skill "${slug}" not found in the registry.`;
|
|
2389
|
+
if (callback) await callback({ text: text2 });
|
|
2390
|
+
return { success: false, error: new Error(text2) };
|
|
2391
|
+
}
|
|
2392
|
+
const isInstalled = await service.isInstalled(slug);
|
|
2393
|
+
const text = `## ${details.skill.displayName}
|
|
2394
|
+
|
|
2395
|
+
**Slug:** \`${details.skill.slug}\`
|
|
2396
|
+
**Version:** ${details.latestVersion.version}
|
|
2397
|
+
**Status:** ${isInstalled ? "\u2705 Installed" : "\u{1F4E6} Available"}
|
|
2398
|
+
|
|
2399
|
+
${details.skill.summary}
|
|
2400
|
+
|
|
2401
|
+
**Stats:**
|
|
2402
|
+
- Downloads: ${details.skill.stats.downloads}
|
|
2403
|
+
- Stars: ${details.skill.stats.stars}
|
|
2404
|
+
- Versions: ${details.skill.stats.versions}
|
|
2405
|
+
|
|
2406
|
+
${details.owner ? `**Author:** ${details.owner.displayName} (@${details.owner.handle})` : ""}
|
|
2407
|
+
|
|
2408
|
+
${details.latestVersion.changelog ? `**Changelog:** ${details.latestVersion.changelog}` : ""}`;
|
|
2409
|
+
if (callback) await callback({ text });
|
|
2410
|
+
return {
|
|
2411
|
+
success: true,
|
|
2412
|
+
text,
|
|
2413
|
+
data: { details, isInstalled }
|
|
2414
|
+
};
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2417
|
+
if (callback) {
|
|
2418
|
+
await callback({ text: `Error getting skill details: ${errorMsg}` });
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
success: false,
|
|
2422
|
+
error: error instanceof Error ? error : new Error(errorMsg)
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
},
|
|
2426
|
+
examples: [
|
|
2427
|
+
[
|
|
2428
|
+
{
|
|
2429
|
+
name: "{{userName}}",
|
|
2430
|
+
content: { text: "Tell me about the pdf-processing skill" }
|
|
2431
|
+
},
|
|
2432
|
+
{
|
|
2433
|
+
name: "{{agentName}}",
|
|
2434
|
+
content: {
|
|
2435
|
+
text: "## PDF Processing\n\n**Slug:** `pdf-processing`\n**Version:** 1.2.0\n**Status:** \u2705 Installed...",
|
|
2436
|
+
actions: ["GET_SKILL_DETAILS"]
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
]
|
|
2440
|
+
]
|
|
2441
|
+
};
|
|
2442
|
+
function extractSlugFromText(text) {
|
|
2443
|
+
const match = text.match(/\b([a-z][a-z0-9-]*[a-z0-9])\b/);
|
|
2444
|
+
return match ? match[1] : null;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// src/actions/get-skill-guidance.ts
|
|
2448
|
+
var getSkillGuidanceAction = {
|
|
2449
|
+
name: "GET_SKILL_GUIDANCE",
|
|
2450
|
+
similes: [
|
|
2451
|
+
"FIND_SKILL",
|
|
2452
|
+
"SEARCH_SKILLS",
|
|
2453
|
+
"SKILL_HELP",
|
|
2454
|
+
"HOW_TO",
|
|
2455
|
+
"GET_INSTRUCTIONS",
|
|
2456
|
+
"LEARN_SKILL",
|
|
2457
|
+
"LOOKUP_SKILL"
|
|
2458
|
+
],
|
|
2459
|
+
description: "Search for and get skill instructions. Use when user asks to find a skill or when you need instructions for a capability.",
|
|
2460
|
+
validate: async (runtime, _message) => {
|
|
2461
|
+
const service = runtime.getService(
|
|
2462
|
+
"AGENT_SKILLS_SERVICE"
|
|
2463
|
+
);
|
|
2464
|
+
return !!service;
|
|
2465
|
+
},
|
|
2466
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
2467
|
+
try {
|
|
2468
|
+
const service = runtime.getService(
|
|
2469
|
+
"AGENT_SKILLS_SERVICE"
|
|
2470
|
+
);
|
|
2471
|
+
if (!service) {
|
|
2472
|
+
throw new Error("AgentSkillsService not available");
|
|
2473
|
+
}
|
|
2474
|
+
const query = message.content?.text || "";
|
|
2475
|
+
if (!query || query.length < 3) {
|
|
2476
|
+
return { success: false, error: new Error("Query too short") };
|
|
2477
|
+
}
|
|
2478
|
+
const searchTerms = extractSearchTerms(query);
|
|
2479
|
+
runtime.logger.info(`AgentSkills: Searching for "${searchTerms}"`);
|
|
2480
|
+
const searchResults = await service.search(searchTerms, 5);
|
|
2481
|
+
const installedSkills = service.getLoadedSkills();
|
|
2482
|
+
const localMatch = findBestLocalMatch(installedSkills, searchTerms);
|
|
2483
|
+
runtime.logger.info(
|
|
2484
|
+
`AgentSkills: Found ${searchResults.length} remote results, local match: ${localMatch?.skill.slug || "none"} (score: ${localMatch?.score || 0})`
|
|
2485
|
+
);
|
|
2486
|
+
const bestRemote = searchResults.length > 0 ? searchResults[0] : null;
|
|
2487
|
+
const remoteScore = bestRemote ? bestRemote.score * 100 : 0;
|
|
2488
|
+
const localIsStrong = localMatch && localMatch.score >= 8;
|
|
2489
|
+
if (!bestRemote || bestRemote.score < 0.25 && !localIsStrong) {
|
|
2490
|
+
const text = `I couldn't find a specific skill for "${searchTerms}". I'll do my best with my general knowledge.`;
|
|
2491
|
+
if (callback) await callback({ text });
|
|
2492
|
+
return {
|
|
2493
|
+
success: true,
|
|
2494
|
+
text,
|
|
2495
|
+
data: { found: false, query: searchTerms }
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
const useLocal = localIsStrong && (!bestRemote || localMatch.score >= remoteScore);
|
|
2499
|
+
if (useLocal && localMatch) {
|
|
2500
|
+
runtime.logger.info(
|
|
2501
|
+
`AgentSkills: Using local skill "${localMatch.skill.slug}"`
|
|
2502
|
+
);
|
|
2503
|
+
const instructions2 = service.getSkillInstructions(
|
|
2504
|
+
localMatch.skill.slug
|
|
2505
|
+
);
|
|
2506
|
+
return buildSuccessResult(
|
|
2507
|
+
localMatch.skill,
|
|
2508
|
+
instructions2?.body || null,
|
|
2509
|
+
"local",
|
|
2510
|
+
callback
|
|
2511
|
+
);
|
|
2512
|
+
}
|
|
2513
|
+
if (!bestRemote) {
|
|
2514
|
+
const text = `I couldn't find a specific skill for "${searchTerms}".`;
|
|
2515
|
+
if (callback) await callback({ text });
|
|
2516
|
+
return { success: true, text, data: { found: false } };
|
|
2517
|
+
}
|
|
2518
|
+
const alreadyInstalled = service.getLoadedSkill(bestRemote.slug);
|
|
2519
|
+
if (!alreadyInstalled) {
|
|
2520
|
+
const installed = await service.install(bestRemote.slug);
|
|
2521
|
+
if (!installed) {
|
|
2522
|
+
if (localMatch) {
|
|
2523
|
+
const instructions2 = service.getSkillInstructions(
|
|
2524
|
+
localMatch.skill.slug
|
|
2525
|
+
);
|
|
2526
|
+
return buildSuccessResult(
|
|
2527
|
+
localMatch.skill,
|
|
2528
|
+
instructions2?.body || null,
|
|
2529
|
+
"local",
|
|
2530
|
+
callback
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
const text = `Found "${bestRemote.displayName}" skill but couldn't install it.`;
|
|
2534
|
+
if (callback) await callback({ text });
|
|
2535
|
+
return {
|
|
2536
|
+
success: true,
|
|
2537
|
+
text,
|
|
2538
|
+
data: { found: true, installed: false }
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
const skill = service.getLoadedSkill(bestRemote.slug);
|
|
2543
|
+
const instructions = skill ? service.getSkillInstructions(skill.slug) : null;
|
|
2544
|
+
return buildSuccessResult(
|
|
2545
|
+
skill || {
|
|
2546
|
+
slug: bestRemote.slug,
|
|
2547
|
+
name: bestRemote.displayName,
|
|
2548
|
+
description: bestRemote.summary,
|
|
2549
|
+
version: bestRemote.version,
|
|
2550
|
+
frontmatter: {
|
|
2551
|
+
name: bestRemote.slug,
|
|
2552
|
+
description: bestRemote.summary
|
|
2553
|
+
},
|
|
2554
|
+
content: "",
|
|
2555
|
+
path: "",
|
|
2556
|
+
scripts: [],
|
|
2557
|
+
references: [],
|
|
2558
|
+
assets: [],
|
|
2559
|
+
loadedAt: Date.now()
|
|
2560
|
+
},
|
|
2561
|
+
instructions?.body || null,
|
|
2562
|
+
alreadyInstalled ? "local" : "installed",
|
|
2563
|
+
callback
|
|
2564
|
+
);
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2567
|
+
if (callback) {
|
|
2568
|
+
await callback({ text: `Error finding skill guidance: ${errorMsg}` });
|
|
2569
|
+
}
|
|
2570
|
+
return {
|
|
2571
|
+
success: false,
|
|
2572
|
+
error: error instanceof Error ? error : new Error(errorMsg)
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
},
|
|
2576
|
+
examples: [
|
|
2577
|
+
[
|
|
2578
|
+
{ name: "{{userName}}", content: { text: "How do I work with PDFs?" } },
|
|
2579
|
+
{
|
|
2580
|
+
name: "{{agentName}}",
|
|
2581
|
+
content: {
|
|
2582
|
+
text: "I found the **PDF Processing** skill. Here's how to use it:\n\n# PDF Processing\n\nExtract text and tables from PDF files...",
|
|
2583
|
+
actions: ["GET_SKILL_GUIDANCE"]
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
],
|
|
2587
|
+
[
|
|
2588
|
+
{
|
|
2589
|
+
name: "{{userName}}",
|
|
2590
|
+
content: { text: "I need help with browser automation" }
|
|
2591
|
+
},
|
|
2592
|
+
{
|
|
2593
|
+
name: "{{agentName}}",
|
|
2594
|
+
content: {
|
|
2595
|
+
text: "I found the **Browser Automation** skill. Here's the guidance:\n\n# Browser Automation\n\nAutomate browser interactions...",
|
|
2596
|
+
actions: ["GET_SKILL_GUIDANCE"]
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
]
|
|
2600
|
+
]
|
|
2601
|
+
};
|
|
2602
|
+
function extractSearchTerms(query) {
|
|
2603
|
+
let cleaned = query.toLowerCase().replace(/\b(on|in|from|at)\s+(clawhub|registry)\b/g, "").replace(/\b(clawhub|registry)\s+(catalog|platform|site)\b/g, "");
|
|
2604
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
2605
|
+
"search",
|
|
2606
|
+
"find",
|
|
2607
|
+
"look",
|
|
2608
|
+
"for",
|
|
2609
|
+
"a",
|
|
2610
|
+
"an",
|
|
2611
|
+
"the",
|
|
2612
|
+
"skill",
|
|
2613
|
+
"skills",
|
|
2614
|
+
"please",
|
|
2615
|
+
"can",
|
|
2616
|
+
"you",
|
|
2617
|
+
"help",
|
|
2618
|
+
"me",
|
|
2619
|
+
"with",
|
|
2620
|
+
"how",
|
|
2621
|
+
"to",
|
|
2622
|
+
"do",
|
|
2623
|
+
"i",
|
|
2624
|
+
"need",
|
|
2625
|
+
"want",
|
|
2626
|
+
"get",
|
|
2627
|
+
"use",
|
|
2628
|
+
"using",
|
|
2629
|
+
"about",
|
|
2630
|
+
"is",
|
|
2631
|
+
"are",
|
|
2632
|
+
"there",
|
|
2633
|
+
"any",
|
|
2634
|
+
"some",
|
|
2635
|
+
"show",
|
|
2636
|
+
"list",
|
|
2637
|
+
"give",
|
|
2638
|
+
"tell",
|
|
2639
|
+
"what",
|
|
2640
|
+
"which"
|
|
2641
|
+
]);
|
|
2642
|
+
const words = cleaned.replace(/[^\w\s-]/g, " ").split(/\s+/).filter((w) => w.length > 1 && !stopWords.has(w));
|
|
2643
|
+
return words.join(" ") || query.toLowerCase();
|
|
2644
|
+
}
|
|
2645
|
+
function findBestLocalMatch(skills, query) {
|
|
2646
|
+
const queryLower = query.toLowerCase();
|
|
2647
|
+
const queryWords = queryLower.split(/\s+/).filter((w) => w.length > 2);
|
|
2648
|
+
let bestMatch = null;
|
|
2649
|
+
for (const skill of skills) {
|
|
2650
|
+
let score = 0;
|
|
2651
|
+
const slugLower = skill.slug.toLowerCase();
|
|
2652
|
+
const nameLower = skill.name.toLowerCase();
|
|
2653
|
+
if (queryLower.includes(slugLower) || queryWords.some((w) => slugLower.includes(w) && w.length > 3)) {
|
|
2654
|
+
score += 10;
|
|
2655
|
+
}
|
|
2656
|
+
if (queryLower.includes(nameLower) || queryWords.some((w) => nameLower.includes(w) && w.length > 3)) {
|
|
2657
|
+
score += 8;
|
|
2658
|
+
}
|
|
2659
|
+
const genericWords = /* @__PURE__ */ new Set([
|
|
2660
|
+
"skill",
|
|
2661
|
+
"agent",
|
|
2662
|
+
"search",
|
|
2663
|
+
"install",
|
|
2664
|
+
"use",
|
|
2665
|
+
"when",
|
|
2666
|
+
"with",
|
|
2667
|
+
"from",
|
|
2668
|
+
"your"
|
|
2669
|
+
]);
|
|
2670
|
+
const descWords = skill.description.toLowerCase().split(/\s+/);
|
|
2671
|
+
for (const word of descWords) {
|
|
2672
|
+
if (word.length > 5 && !genericWords.has(word) && queryWords.includes(word)) {
|
|
2673
|
+
score += 1;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
if (score > 0 && (!bestMatch || score > bestMatch.score)) {
|
|
2677
|
+
bestMatch = { skill, score };
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return bestMatch;
|
|
2681
|
+
}
|
|
2682
|
+
async function buildSuccessResult(skill, instructions, source, callback) {
|
|
2683
|
+
let text = `## ${skill.name}
|
|
2684
|
+
|
|
2685
|
+
`;
|
|
2686
|
+
if (source === "installed") {
|
|
2687
|
+
text += `*Skill installed from registry*
|
|
2688
|
+
|
|
2689
|
+
`;
|
|
2690
|
+
}
|
|
2691
|
+
text += `${skill.description}
|
|
2692
|
+
|
|
2693
|
+
`;
|
|
2694
|
+
if (instructions) {
|
|
2695
|
+
const maxLen = 3500;
|
|
2696
|
+
const truncated = instructions.length > maxLen ? instructions.substring(0, maxLen) + "\n\n...[truncated]" : instructions;
|
|
2697
|
+
text += `### Instructions
|
|
2698
|
+
|
|
2699
|
+
${truncated}`;
|
|
2700
|
+
}
|
|
2701
|
+
if (callback) {
|
|
2702
|
+
await callback({ text, actions: ["GET_SKILL_GUIDANCE"] });
|
|
2703
|
+
}
|
|
2704
|
+
return {
|
|
2705
|
+
success: true,
|
|
2706
|
+
text,
|
|
2707
|
+
values: {
|
|
2708
|
+
activeSkill: skill.slug,
|
|
2709
|
+
skillName: skill.name,
|
|
2710
|
+
skillSource: source
|
|
2711
|
+
},
|
|
2712
|
+
data: {
|
|
2713
|
+
skill: {
|
|
2714
|
+
slug: skill.slug,
|
|
2715
|
+
name: skill.name,
|
|
2716
|
+
description: skill.description
|
|
2717
|
+
},
|
|
2718
|
+
instructions,
|
|
2719
|
+
source
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// src/actions/sync-catalog.ts
|
|
2725
|
+
var syncCatalogAction = {
|
|
2726
|
+
name: "SYNC_SKILL_CATALOG",
|
|
2727
|
+
similes: ["REFRESH_SKILLS", "UPDATE_CATALOG"],
|
|
2728
|
+
description: "Sync the skill catalog from the registry to discover new skills.",
|
|
2729
|
+
validate: async (runtime, _message) => {
|
|
2730
|
+
const service = runtime.getService(
|
|
2731
|
+
"AGENT_SKILLS_SERVICE"
|
|
2732
|
+
);
|
|
2733
|
+
return !!service;
|
|
2734
|
+
},
|
|
2735
|
+
handler: async (runtime, _message, _state, _options, callback) => {
|
|
2736
|
+
try {
|
|
2737
|
+
const service = runtime.getService(
|
|
2738
|
+
"AGENT_SKILLS_SERVICE"
|
|
2739
|
+
);
|
|
2740
|
+
if (!service) {
|
|
2741
|
+
throw new Error("AgentSkillsService not available");
|
|
2742
|
+
}
|
|
2743
|
+
runtime.logger.info("AgentSkills: Manual catalog sync triggered");
|
|
2744
|
+
const result = await service.syncCatalog();
|
|
2745
|
+
const text = `Skill catalog synced successfully.
|
|
2746
|
+
- Total skills: ${result.updated}
|
|
2747
|
+
- New skills: ${result.added}`;
|
|
2748
|
+
if (callback) await callback({ text });
|
|
2749
|
+
return {
|
|
2750
|
+
success: true,
|
|
2751
|
+
text,
|
|
2752
|
+
data: result
|
|
2753
|
+
};
|
|
2754
|
+
} catch (error) {
|
|
2755
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2756
|
+
if (callback) {
|
|
2757
|
+
await callback({ text: `Error syncing catalog: ${errorMsg}` });
|
|
2758
|
+
}
|
|
2759
|
+
return {
|
|
2760
|
+
success: false,
|
|
2761
|
+
error: error instanceof Error ? error : new Error(errorMsg)
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
},
|
|
2765
|
+
examples: [
|
|
2766
|
+
[
|
|
2767
|
+
{ name: "{{userName}}", content: { text: "Refresh the skill catalog" } },
|
|
2768
|
+
{
|
|
2769
|
+
name: "{{agentName}}",
|
|
2770
|
+
content: {
|
|
2771
|
+
text: "Skill catalog synced successfully.\n- Total skills: 150\n- New skills: 5",
|
|
2772
|
+
actions: ["SYNC_SKILL_CATALOG"]
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
]
|
|
2776
|
+
]
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2779
|
+
// src/actions/run-skill-script.ts
|
|
2780
|
+
import { spawn } from "child_process";
|
|
2781
|
+
import * as path from "path";
|
|
2782
|
+
var runSkillScriptAction = {
|
|
2783
|
+
name: "RUN_SKILL_SCRIPT",
|
|
2784
|
+
similes: ["EXECUTE_SKILL_SCRIPT", "SKILL_SCRIPT"],
|
|
2785
|
+
description: "Execute a script bundled with an installed skill. Provide skill slug and script name.",
|
|
2786
|
+
validate: async (runtime, _message) => {
|
|
2787
|
+
const service = runtime.getService(
|
|
2788
|
+
"AGENT_SKILLS_SERVICE"
|
|
2789
|
+
);
|
|
2790
|
+
return !!service;
|
|
2791
|
+
},
|
|
2792
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
2793
|
+
try {
|
|
2794
|
+
const service = runtime.getService(
|
|
2795
|
+
"AGENT_SKILLS_SERVICE"
|
|
2796
|
+
);
|
|
2797
|
+
if (!service) {
|
|
2798
|
+
throw new Error("AgentSkillsService not available");
|
|
2799
|
+
}
|
|
2800
|
+
const opts = options;
|
|
2801
|
+
const skillSlug = opts?.skillSlug;
|
|
2802
|
+
const scriptName = opts?.script;
|
|
2803
|
+
const args = opts?.args || [];
|
|
2804
|
+
if (!skillSlug || !scriptName) {
|
|
2805
|
+
return {
|
|
2806
|
+
success: false,
|
|
2807
|
+
error: new Error("Both skillSlug and script are required")
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
const scriptPath = service.getScriptPath(skillSlug, scriptName);
|
|
2811
|
+
if (!scriptPath) {
|
|
2812
|
+
return {
|
|
2813
|
+
success: false,
|
|
2814
|
+
error: new Error(
|
|
2815
|
+
`Script "${scriptName}" not found in skill "${skillSlug}"`
|
|
2816
|
+
)
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
runtime.logger.info(`AgentSkills: Executing ${skillSlug}/${scriptName}`);
|
|
2820
|
+
const result = await executeScript(scriptPath, args);
|
|
2821
|
+
const text = result.success ? `Script executed successfully:
|
|
2822
|
+
\`\`\`
|
|
2823
|
+
${result.stdout}
|
|
2824
|
+
\`\`\`` : `Script failed:
|
|
2825
|
+
\`\`\`
|
|
2826
|
+
${result.stderr}
|
|
2827
|
+
\`\`\``;
|
|
2828
|
+
if (callback) {
|
|
2829
|
+
await callback({ text });
|
|
2830
|
+
}
|
|
2831
|
+
return {
|
|
2832
|
+
success: result.success,
|
|
2833
|
+
text,
|
|
2834
|
+
data: {
|
|
2835
|
+
scriptPath,
|
|
2836
|
+
exitCode: result.exitCode,
|
|
2837
|
+
stdout: result.stdout,
|
|
2838
|
+
stderr: result.stderr
|
|
2839
|
+
}
|
|
2840
|
+
};
|
|
2841
|
+
} catch (error) {
|
|
2842
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2843
|
+
if (callback) {
|
|
2844
|
+
await callback({ text: `Error executing script: ${errorMsg}` });
|
|
2845
|
+
}
|
|
2846
|
+
return {
|
|
2847
|
+
success: false,
|
|
2848
|
+
error: error instanceof Error ? error : new Error(errorMsg)
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
},
|
|
2852
|
+
examples: [
|
|
2853
|
+
[
|
|
2854
|
+
{
|
|
2855
|
+
name: "{{userName}}",
|
|
2856
|
+
content: { text: "Run the rotate script from pdf-skill" }
|
|
2857
|
+
},
|
|
2858
|
+
{
|
|
2859
|
+
name: "{{agentName}}",
|
|
2860
|
+
content: {
|
|
2861
|
+
text: "I'll run the rotate_pdf.py script from the pdf-skill.",
|
|
2862
|
+
actions: ["RUN_SKILL_SCRIPT"]
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
]
|
|
2866
|
+
]
|
|
2867
|
+
};
|
|
2868
|
+
async function executeScript(scriptPath, args) {
|
|
2869
|
+
return new Promise((resolve) => {
|
|
2870
|
+
const ext = path.extname(scriptPath).toLowerCase();
|
|
2871
|
+
let cmd;
|
|
2872
|
+
let cmdArgs;
|
|
2873
|
+
switch (ext) {
|
|
2874
|
+
case ".py":
|
|
2875
|
+
cmd = "python3";
|
|
2876
|
+
cmdArgs = [scriptPath, ...args];
|
|
2877
|
+
break;
|
|
2878
|
+
case ".sh":
|
|
2879
|
+
cmd = "bash";
|
|
2880
|
+
cmdArgs = [scriptPath, ...args];
|
|
2881
|
+
break;
|
|
2882
|
+
case ".js":
|
|
2883
|
+
cmd = "node";
|
|
2884
|
+
cmdArgs = [scriptPath, ...args];
|
|
2885
|
+
break;
|
|
2886
|
+
default:
|
|
2887
|
+
cmd = scriptPath;
|
|
2888
|
+
cmdArgs = args;
|
|
2889
|
+
}
|
|
2890
|
+
const child = spawn(cmd, cmdArgs, {
|
|
2891
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2892
|
+
timeout: 6e4
|
|
2893
|
+
// 1 minute timeout
|
|
2894
|
+
});
|
|
2895
|
+
let stdout = "";
|
|
2896
|
+
let stderr = "";
|
|
2897
|
+
child.stdout?.on("data", (data) => {
|
|
2898
|
+
stdout += data.toString();
|
|
2899
|
+
});
|
|
2900
|
+
child.stderr?.on("data", (data) => {
|
|
2901
|
+
stderr += data.toString();
|
|
2902
|
+
});
|
|
2903
|
+
child.on("close", (code) => {
|
|
2904
|
+
resolve({
|
|
2905
|
+
success: code === 0,
|
|
2906
|
+
exitCode: code || 0,
|
|
2907
|
+
stdout: stdout.trim(),
|
|
2908
|
+
stderr: stderr.trim()
|
|
2909
|
+
});
|
|
2910
|
+
});
|
|
2911
|
+
child.on("error", (error) => {
|
|
2912
|
+
resolve({
|
|
2913
|
+
success: false,
|
|
2914
|
+
exitCode: -1,
|
|
2915
|
+
stdout: "",
|
|
2916
|
+
stderr: error.message
|
|
2917
|
+
});
|
|
2918
|
+
});
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
// src/providers/skills.ts
|
|
2923
|
+
var skillsOverviewProvider = {
|
|
2924
|
+
name: "agent_skills_overview",
|
|
2925
|
+
description: "Low-res overview of available Agent Skills (names only)",
|
|
2926
|
+
position: -20,
|
|
2927
|
+
dynamic: true,
|
|
2928
|
+
get: async (runtime, _message, _state) => {
|
|
2929
|
+
const service = runtime.getService(
|
|
2930
|
+
"AGENT_SKILLS_SERVICE"
|
|
2931
|
+
);
|
|
2932
|
+
if (!service) return { text: "" };
|
|
2933
|
+
const stats = service.getCatalogStats();
|
|
2934
|
+
const installed = service.getLoadedSkills();
|
|
2935
|
+
const catalog = await service.getCatalog({ notOlderThan: Infinity });
|
|
2936
|
+
const availableCount = catalog.length;
|
|
2937
|
+
const examples = catalog.slice(0, 5).map((s) => s.displayName).join(", ");
|
|
2938
|
+
const text = `**Skills:** ${stats.installed} installed, ${availableCount} available
|
|
2939
|
+
Examples: ${examples}...
|
|
2940
|
+
Use GET_SKILL_GUIDANCE to find skills for specific tasks.`;
|
|
2941
|
+
return {
|
|
2942
|
+
text,
|
|
2943
|
+
values: {
|
|
2944
|
+
installedCount: stats.installed,
|
|
2945
|
+
availableCount
|
|
2946
|
+
},
|
|
2947
|
+
data: {
|
|
2948
|
+
installed: installed.map((s) => s.slug),
|
|
2949
|
+
catalogSize: availableCount
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
var skillsSummaryProvider = {
|
|
2955
|
+
name: "agent_skills",
|
|
2956
|
+
description: "Medium-res list of installed Agent Skills with descriptions",
|
|
2957
|
+
position: -10,
|
|
2958
|
+
get: async (runtime, _message, _state) => {
|
|
2959
|
+
const service = runtime.getService(
|
|
2960
|
+
"AGENT_SKILLS_SERVICE"
|
|
2961
|
+
);
|
|
2962
|
+
if (!service) return { text: "" };
|
|
2963
|
+
const skills = service.getLoadedSkills();
|
|
2964
|
+
if (skills.length === 0) {
|
|
2965
|
+
return {
|
|
2966
|
+
text: "**Skills:** None installed. Use GET_SKILL_GUIDANCE to find and install skills.",
|
|
2967
|
+
values: { skillCount: 0 },
|
|
2968
|
+
data: { skills: [] }
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
const xml = service.generateSkillsPromptXml({ includeLocation: true });
|
|
2972
|
+
const text = `## Installed Skills (${skills.length})
|
|
2973
|
+
|
|
2974
|
+
${xml}
|
|
2975
|
+
|
|
2976
|
+
*More skills available via GET_SKILL_GUIDANCE*`;
|
|
2977
|
+
return {
|
|
2978
|
+
text,
|
|
2979
|
+
values: {
|
|
2980
|
+
skillCount: skills.length,
|
|
2981
|
+
installedSkills: skills.map((s) => s.slug).join(", ")
|
|
2982
|
+
},
|
|
2983
|
+
data: {
|
|
2984
|
+
skills: skills.map((s) => ({
|
|
2985
|
+
slug: s.slug,
|
|
2986
|
+
name: s.name,
|
|
2987
|
+
description: s.description,
|
|
2988
|
+
version: s.version
|
|
2989
|
+
}))
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
var skillInstructionsProvider = {
|
|
2995
|
+
name: "agent_skill_instructions",
|
|
2996
|
+
description: "High-res instructions from the most relevant skill",
|
|
2997
|
+
position: 5,
|
|
2998
|
+
get: async (runtime, message, state) => {
|
|
2999
|
+
const service = runtime.getService(
|
|
3000
|
+
"AGENT_SKILLS_SERVICE"
|
|
3001
|
+
);
|
|
3002
|
+
if (!service) return { text: "" };
|
|
3003
|
+
const skills = service.getLoadedSkills();
|
|
3004
|
+
if (skills.length === 0) return { text: "" };
|
|
3005
|
+
const messageText = (message.content?.text || "").toLowerCase();
|
|
3006
|
+
const recentContext = getRecentContext(state);
|
|
3007
|
+
const fullContext = `${messageText} ${recentContext}`.toLowerCase();
|
|
3008
|
+
const scoredSkills = skills.map((skill) => ({
|
|
3009
|
+
skill,
|
|
3010
|
+
score: calculateSkillRelevance(skill, fullContext)
|
|
3011
|
+
})).filter((s) => s.score > 0).sort((a, b) => b.score - a.score);
|
|
3012
|
+
if (scoredSkills.length === 0 || scoredSkills[0].score < 3) {
|
|
3013
|
+
return { text: "" };
|
|
3014
|
+
}
|
|
3015
|
+
const topSkill = scoredSkills[0];
|
|
3016
|
+
const instructions = service.getSkillInstructions(topSkill.skill.slug);
|
|
3017
|
+
if (!instructions) return { text: "" };
|
|
3018
|
+
const maxChars = 4e3;
|
|
3019
|
+
const truncatedBody = instructions.body.length > maxChars ? instructions.body.substring(0, maxChars) + "\n\n...[truncated]" : instructions.body;
|
|
3020
|
+
const text = `## Active Skill: ${topSkill.skill.name}
|
|
3021
|
+
|
|
3022
|
+
${truncatedBody}`;
|
|
3023
|
+
return {
|
|
3024
|
+
text,
|
|
3025
|
+
values: {
|
|
3026
|
+
activeSkill: topSkill.skill.slug,
|
|
3027
|
+
skillName: topSkill.skill.name,
|
|
3028
|
+
relevanceScore: topSkill.score,
|
|
3029
|
+
estimatedTokens: instructions.estimatedTokens
|
|
3030
|
+
},
|
|
3031
|
+
data: {
|
|
3032
|
+
activeSkill: {
|
|
3033
|
+
slug: topSkill.skill.slug,
|
|
3034
|
+
name: topSkill.skill.name,
|
|
3035
|
+
score: topSkill.score
|
|
3036
|
+
},
|
|
3037
|
+
otherMatches: scoredSkills.slice(1, 3).map((s) => ({
|
|
3038
|
+
slug: s.skill.slug,
|
|
3039
|
+
score: s.score
|
|
3040
|
+
}))
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
};
|
|
3045
|
+
var catalogAwarenessProvider = {
|
|
3046
|
+
name: "agent_skills_catalog",
|
|
3047
|
+
description: "Awareness of skills available on the registry",
|
|
3048
|
+
position: 10,
|
|
3049
|
+
dynamic: true,
|
|
3050
|
+
private: true,
|
|
3051
|
+
get: async (runtime, message, _state) => {
|
|
3052
|
+
const service = runtime.getService(
|
|
3053
|
+
"AGENT_SKILLS_SERVICE"
|
|
3054
|
+
);
|
|
3055
|
+
if (!service) return { text: "" };
|
|
3056
|
+
const text = (message.content?.text || "").toLowerCase();
|
|
3057
|
+
const capabilityKeywords = [
|
|
3058
|
+
"what can you",
|
|
3059
|
+
"what skills",
|
|
3060
|
+
"capabilities",
|
|
3061
|
+
"what do you know",
|
|
3062
|
+
"help with"
|
|
3063
|
+
];
|
|
3064
|
+
if (!capabilityKeywords.some((kw) => text.includes(kw))) {
|
|
3065
|
+
return { text: "" };
|
|
3066
|
+
}
|
|
3067
|
+
const catalog = await service.getCatalog({ notOlderThan: Infinity });
|
|
3068
|
+
if (catalog.length === 0) return { text: "" };
|
|
3069
|
+
const categories = groupByCategory(catalog);
|
|
3070
|
+
let categoryText = "";
|
|
3071
|
+
for (const [category, skills] of Object.entries(categories).slice(0, 8)) {
|
|
3072
|
+
const skillNames = skills.slice(0, 3).map((s) => s.name).join(", ");
|
|
3073
|
+
const more = skills.length > 3 ? ` +${skills.length - 3} more` : "";
|
|
3074
|
+
categoryText += `- **${category}**: ${skillNames}${more}
|
|
3075
|
+
`;
|
|
3076
|
+
}
|
|
3077
|
+
return {
|
|
3078
|
+
text: `## Available Skill Categories
|
|
3079
|
+
|
|
3080
|
+
${categoryText}
|
|
3081
|
+
Use GET_SKILL_GUIDANCE to find and use any skill.`,
|
|
3082
|
+
data: { categories }
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
function getRecentContext(state) {
|
|
3087
|
+
const recentMessages = state.recentMessages || state.recentMessagesData || [];
|
|
3088
|
+
if (Array.isArray(recentMessages)) {
|
|
3089
|
+
return recentMessages.slice(-5).map(
|
|
3090
|
+
(m) => m.content?.text || ""
|
|
3091
|
+
).join(" ");
|
|
3092
|
+
}
|
|
3093
|
+
return "";
|
|
3094
|
+
}
|
|
3095
|
+
function calculateSkillRelevance(skill, context) {
|
|
3096
|
+
let score = 0;
|
|
3097
|
+
const contextLower = context.toLowerCase();
|
|
3098
|
+
if (contextLower.includes(skill.slug.toLowerCase())) score += 10;
|
|
3099
|
+
if (contextLower.includes(skill.name.toLowerCase())) score += 8;
|
|
3100
|
+
const nameWords = skill.name.split(/[\s-_]+/).filter((w) => w.length > 3);
|
|
3101
|
+
for (const word of nameWords) {
|
|
3102
|
+
if (contextLower.includes(word.toLowerCase())) score += 2;
|
|
3103
|
+
}
|
|
3104
|
+
const stopwords = /* @__PURE__ */ new Set([
|
|
3105
|
+
"the",
|
|
3106
|
+
"and",
|
|
3107
|
+
"for",
|
|
3108
|
+
"with",
|
|
3109
|
+
"this",
|
|
3110
|
+
"that",
|
|
3111
|
+
"from",
|
|
3112
|
+
"will",
|
|
3113
|
+
"can",
|
|
3114
|
+
"are",
|
|
3115
|
+
"use",
|
|
3116
|
+
"when",
|
|
3117
|
+
"how",
|
|
3118
|
+
"what",
|
|
3119
|
+
"your",
|
|
3120
|
+
"you",
|
|
3121
|
+
"our",
|
|
3122
|
+
"has",
|
|
3123
|
+
"have",
|
|
3124
|
+
"been",
|
|
3125
|
+
"skill",
|
|
3126
|
+
"agent",
|
|
3127
|
+
"search",
|
|
3128
|
+
"install"
|
|
3129
|
+
]);
|
|
3130
|
+
const descWords = skill.description.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 4 && !stopwords.has(w));
|
|
3131
|
+
for (const word of descWords) {
|
|
3132
|
+
if (contextLower.includes(word)) score += 1;
|
|
3133
|
+
}
|
|
3134
|
+
const triggerMatch = skill.description.match(
|
|
3135
|
+
/Use (?:when|for|to)\s+([^.]+)/i
|
|
3136
|
+
);
|
|
3137
|
+
if (triggerMatch) {
|
|
3138
|
+
const triggerWords = triggerMatch[1].split(/[,;]/).map((t) => t.trim().toLowerCase());
|
|
3139
|
+
for (const trigger of triggerWords) {
|
|
3140
|
+
if (trigger && contextLower.includes(trigger)) score += 3;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
return score;
|
|
3144
|
+
}
|
|
3145
|
+
function groupByCategory(skills) {
|
|
3146
|
+
const categories = {};
|
|
3147
|
+
const categoryKeywords = {
|
|
3148
|
+
"AI & Models": [
|
|
3149
|
+
"ai",
|
|
3150
|
+
"llm",
|
|
3151
|
+
"model",
|
|
3152
|
+
"gpt",
|
|
3153
|
+
"claude",
|
|
3154
|
+
"openai",
|
|
3155
|
+
"anthropic"
|
|
3156
|
+
],
|
|
3157
|
+
"Browser & Web": ["browser", "web", "scrape", "chrome", "selenium"],
|
|
3158
|
+
"Code & Dev": ["code", "python", "javascript", "typescript", "git", "dev"],
|
|
3159
|
+
"Data & Analytics": ["data", "analytics", "csv", "json", "database"],
|
|
3160
|
+
"Finance & Trading": [
|
|
3161
|
+
"trading",
|
|
3162
|
+
"finance",
|
|
3163
|
+
"crypto",
|
|
3164
|
+
"market",
|
|
3165
|
+
"prediction"
|
|
3166
|
+
],
|
|
3167
|
+
Communication: ["email", "slack", "discord", "telegram", "chat"],
|
|
3168
|
+
Productivity: ["calendar", "task", "todo", "note", "document"],
|
|
3169
|
+
Other: []
|
|
3170
|
+
};
|
|
3171
|
+
for (const skill of skills) {
|
|
3172
|
+
const text = `${skill.displayName} ${skill.summary || ""}`.toLowerCase();
|
|
3173
|
+
let assigned = false;
|
|
3174
|
+
for (const [category, keywords] of Object.entries(categoryKeywords)) {
|
|
3175
|
+
if (category === "Other") continue;
|
|
3176
|
+
if (keywords.some((kw) => text.includes(kw))) {
|
|
3177
|
+
if (!categories[category]) categories[category] = [];
|
|
3178
|
+
categories[category].push({
|
|
3179
|
+
slug: skill.slug,
|
|
3180
|
+
name: skill.displayName
|
|
3181
|
+
});
|
|
3182
|
+
assigned = true;
|
|
3183
|
+
break;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
if (!assigned) {
|
|
3187
|
+
if (!categories["Other"]) categories["Other"] = [];
|
|
3188
|
+
categories["Other"].push({ slug: skill.slug, name: skill.displayName });
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
return categories;
|
|
3192
|
+
}
|
|
3193
|
+
var skillsProvider = skillsSummaryProvider;
|
|
3194
|
+
|
|
3195
|
+
// src/tasks/sync-catalog.ts
|
|
3196
|
+
var SYNC_INTERVAL_MS = 1e3 * 60 * 60;
|
|
3197
|
+
var syncCatalogTask = {
|
|
3198
|
+
name: "agent-skills-sync",
|
|
3199
|
+
description: "Sync skill catalog from registry",
|
|
3200
|
+
execute: async (runtime) => {
|
|
3201
|
+
const service = runtime.getService(
|
|
3202
|
+
"AGENT_SKILLS_SERVICE"
|
|
3203
|
+
);
|
|
3204
|
+
if (!service) {
|
|
3205
|
+
runtime.logger.warn("AgentSkills: Sync task - service not available");
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
try {
|
|
3209
|
+
const result = await service.syncCatalog();
|
|
3210
|
+
runtime.logger.info(
|
|
3211
|
+
`AgentSkills: Catalog synced - ${result.updated} skills, ${result.added} new`
|
|
3212
|
+
);
|
|
3213
|
+
} catch (error) {
|
|
3214
|
+
runtime.logger.error(`AgentSkills: Sync failed: ${error}`);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
};
|
|
3218
|
+
function startSyncTask(runtime) {
|
|
3219
|
+
const initialTimeout = setTimeout(() => {
|
|
3220
|
+
syncCatalogTask.execute(runtime).catch((err) => {
|
|
3221
|
+
runtime.logger.error(`AgentSkills: Initial sync failed: ${err}`);
|
|
3222
|
+
});
|
|
3223
|
+
}, 5e3);
|
|
3224
|
+
const interval = setInterval(() => {
|
|
3225
|
+
syncCatalogTask.execute(runtime).catch((err) => {
|
|
3226
|
+
runtime.logger.error(`AgentSkills: Periodic sync failed: ${err}`);
|
|
3227
|
+
});
|
|
3228
|
+
}, SYNC_INTERVAL_MS);
|
|
3229
|
+
return () => {
|
|
3230
|
+
clearTimeout(initialTimeout);
|
|
3231
|
+
clearInterval(interval);
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
// src/plugin.ts
|
|
3236
|
+
var ALL_SERVICES = [
|
|
3237
|
+
AgentSkillsService
|
|
3238
|
+
];
|
|
3239
|
+
var ALL_ACTIONS = [
|
|
3240
|
+
searchSkillsAction,
|
|
3241
|
+
// Browse/search available skills
|
|
3242
|
+
getSkillDetailsAction,
|
|
3243
|
+
// Get info about a specific skill
|
|
3244
|
+
getSkillGuidanceAction,
|
|
3245
|
+
// Auto-finds, installs, returns skill instructions
|
|
3246
|
+
syncCatalogAction,
|
|
3247
|
+
// Manual catalog sync
|
|
3248
|
+
runSkillScriptAction
|
|
3249
|
+
// Execute scripts from installed skills
|
|
3250
|
+
];
|
|
3251
|
+
var ALL_PROVIDERS = [
|
|
3252
|
+
skillsSummaryProvider,
|
|
3253
|
+
// Medium-res (default) - installed skills
|
|
3254
|
+
skillInstructionsProvider,
|
|
3255
|
+
// High-res - active skill instructions
|
|
3256
|
+
catalogAwarenessProvider
|
|
3257
|
+
// Dynamic - catalog awareness
|
|
3258
|
+
];
|
|
3259
|
+
var cleanupSyncTask = null;
|
|
3260
|
+
var agentSkillsPlugin = {
|
|
3261
|
+
name: "@elizaos/plugin-agent-skills",
|
|
3262
|
+
description: "Agent Skills - modular capabilities with progressive disclosure",
|
|
3263
|
+
services: ALL_SERVICES,
|
|
3264
|
+
actions: ALL_ACTIONS,
|
|
3265
|
+
providers: ALL_PROVIDERS,
|
|
3266
|
+
evaluators: [],
|
|
3267
|
+
routes: [],
|
|
3268
|
+
// Initialize background task when plugin loads
|
|
3269
|
+
init: async (_config, runtime) => {
|
|
3270
|
+
cleanupSyncTask = startSyncTask(runtime);
|
|
3271
|
+
runtime.logger.info("AgentSkills: Background sync task started");
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3274
|
+
var clawHubPlugin = agentSkillsPlugin;
|
|
3275
|
+
var plugin_default = agentSkillsPlugin;
|
|
3276
|
+
|
|
3277
|
+
// src/services/install.ts
|
|
3278
|
+
var DEFAULT_TIMEOUT = 3e5;
|
|
3279
|
+
var NODE_MANAGERS = ["bun", "pnpm", "npm", "yarn"];
|
|
3280
|
+
function detectPlatform() {
|
|
3281
|
+
if (typeof process === "undefined") return "unknown";
|
|
3282
|
+
const platform = process.platform;
|
|
3283
|
+
if (platform === "darwin") return "darwin";
|
|
3284
|
+
if (platform === "linux") return "linux";
|
|
3285
|
+
if (platform === "win32") return "windows";
|
|
3286
|
+
return "unknown";
|
|
3287
|
+
}
|
|
3288
|
+
async function binaryExists(name) {
|
|
3289
|
+
try {
|
|
3290
|
+
const { execSync } = await import("node:child_process");
|
|
3291
|
+
const platform = detectPlatform();
|
|
3292
|
+
const command = platform === "windows" ? `where ${name}` : `which ${name}`;
|
|
3293
|
+
execSync(command, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3294
|
+
return true;
|
|
3295
|
+
} catch {
|
|
3296
|
+
return false;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
async function getPreferredNodeManager() {
|
|
3300
|
+
const preferred = process.env.OTTO_NODE_MANAGER;
|
|
3301
|
+
if (preferred && await binaryExists(preferred)) {
|
|
3302
|
+
return preferred;
|
|
3303
|
+
}
|
|
3304
|
+
for (const manager of NODE_MANAGERS) {
|
|
3305
|
+
if (await binaryExists(manager)) {
|
|
3306
|
+
return manager;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
return null;
|
|
3310
|
+
}
|
|
3311
|
+
async function isHomebrewAvailable() {
|
|
3312
|
+
return detectPlatform() === "darwin" && await binaryExists("brew");
|
|
3313
|
+
}
|
|
3314
|
+
async function isAptAvailable() {
|
|
3315
|
+
return detectPlatform() === "linux" && await binaryExists("apt-get");
|
|
3316
|
+
}
|
|
3317
|
+
async function isPipAvailable() {
|
|
3318
|
+
return await binaryExists("pip3") || await binaryExists("pip");
|
|
3319
|
+
}
|
|
3320
|
+
async function isCargoAvailable() {
|
|
3321
|
+
return binaryExists("cargo");
|
|
3322
|
+
}
|
|
3323
|
+
function buildInstallCommand(option) {
|
|
3324
|
+
switch (option.kind) {
|
|
3325
|
+
case "brew":
|
|
3326
|
+
return `brew install ${option.formula || option.package}`;
|
|
3327
|
+
case "apt":
|
|
3328
|
+
return `sudo apt-get install -y ${option.package}`;
|
|
3329
|
+
case "node": {
|
|
3330
|
+
const pkg = option.package;
|
|
3331
|
+
return `__NODE_MANAGER__ install -g ${pkg}`;
|
|
3332
|
+
}
|
|
3333
|
+
case "pip":
|
|
3334
|
+
return `pip3 install ${option.package}`;
|
|
3335
|
+
case "cargo":
|
|
3336
|
+
return `cargo install ${option.package}`;
|
|
3337
|
+
case "manual":
|
|
3338
|
+
return null;
|
|
3339
|
+
default:
|
|
3340
|
+
return null;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
async function resolveNodeManager(command) {
|
|
3344
|
+
if (!command.includes("__NODE_MANAGER__")) {
|
|
3345
|
+
return command;
|
|
3346
|
+
}
|
|
3347
|
+
const manager = await getPreferredNodeManager();
|
|
3348
|
+
if (!manager) {
|
|
3349
|
+
throw new Error("No Node.js package manager found (tried bun, pnpm, npm, yarn)");
|
|
3350
|
+
}
|
|
3351
|
+
return command.replace("__NODE_MANAGER__", manager);
|
|
3352
|
+
}
|
|
3353
|
+
async function executeInstall(command, options = {}) {
|
|
3354
|
+
const { timeout = DEFAULT_TIMEOUT, onProgress, dryRun } = options;
|
|
3355
|
+
const startTime = Date.now();
|
|
3356
|
+
if (dryRun) {
|
|
3357
|
+
onProgress?.({
|
|
3358
|
+
phase: "complete",
|
|
3359
|
+
progress: 100,
|
|
3360
|
+
message: `[DRY RUN] Would execute: ${command}`
|
|
3361
|
+
});
|
|
3362
|
+
return { success: true, duration: 0 };
|
|
3363
|
+
}
|
|
3364
|
+
try {
|
|
3365
|
+
const { spawn: spawn2 } = await import("node:child_process");
|
|
3366
|
+
onProgress?.({
|
|
3367
|
+
phase: "installing",
|
|
3368
|
+
progress: 10,
|
|
3369
|
+
message: `Executing: ${command}`
|
|
3370
|
+
});
|
|
3371
|
+
return await new Promise((resolve) => {
|
|
3372
|
+
const child = spawn2("sh", ["-c", command], {
|
|
3373
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3374
|
+
timeout
|
|
3375
|
+
});
|
|
3376
|
+
let stdout = "";
|
|
3377
|
+
let stderr = "";
|
|
3378
|
+
child.stdout?.on("data", (data) => {
|
|
3379
|
+
stdout += data.toString();
|
|
3380
|
+
onProgress?.({
|
|
3381
|
+
phase: "installing",
|
|
3382
|
+
progress: 50,
|
|
3383
|
+
message: data.toString().trim().slice(0, 200)
|
|
3384
|
+
});
|
|
3385
|
+
});
|
|
3386
|
+
child.stderr?.on("data", (data) => {
|
|
3387
|
+
stderr += data.toString();
|
|
3388
|
+
});
|
|
3389
|
+
child.on("close", (code) => {
|
|
3390
|
+
const duration = Date.now() - startTime;
|
|
3391
|
+
if (code === 0) {
|
|
3392
|
+
onProgress?.({
|
|
3393
|
+
phase: "complete",
|
|
3394
|
+
progress: 100,
|
|
3395
|
+
message: "Installation completed successfully"
|
|
3396
|
+
});
|
|
3397
|
+
resolve({ success: true, duration });
|
|
3398
|
+
} else {
|
|
3399
|
+
const error = stderr || stdout || `Process exited with code ${code}`;
|
|
3400
|
+
onProgress?.({
|
|
3401
|
+
phase: "error",
|
|
3402
|
+
message: "Installation failed",
|
|
3403
|
+
error
|
|
3404
|
+
});
|
|
3405
|
+
resolve({ success: false, error, duration });
|
|
3406
|
+
}
|
|
3407
|
+
});
|
|
3408
|
+
child.on("error", (err) => {
|
|
3409
|
+
const duration = Date.now() - startTime;
|
|
3410
|
+
const error = err.message;
|
|
3411
|
+
onProgress?.({
|
|
3412
|
+
phase: "error",
|
|
3413
|
+
message: "Installation failed",
|
|
3414
|
+
error
|
|
3415
|
+
});
|
|
3416
|
+
resolve({ success: false, error, duration });
|
|
3417
|
+
});
|
|
3418
|
+
});
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
const duration = Date.now() - startTime;
|
|
3421
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
3422
|
+
onProgress?.({
|
|
3423
|
+
phase: "error",
|
|
3424
|
+
message: "Installation failed",
|
|
3425
|
+
error: errorMessage
|
|
3426
|
+
});
|
|
3427
|
+
return { success: false, error: errorMessage, duration };
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
async function installSkillDependency(options) {
|
|
3431
|
+
const { option, onProgress, dryRun, timeout } = options;
|
|
3432
|
+
onProgress?.({
|
|
3433
|
+
phase: "installing",
|
|
3434
|
+
progress: 0,
|
|
3435
|
+
message: `Preparing to install via ${option.kind}...`
|
|
3436
|
+
});
|
|
3437
|
+
const platform = detectPlatform();
|
|
3438
|
+
switch (option.kind) {
|
|
3439
|
+
case "brew":
|
|
3440
|
+
if (!await isHomebrewAvailable()) {
|
|
3441
|
+
return {
|
|
3442
|
+
success: false,
|
|
3443
|
+
option,
|
|
3444
|
+
error: "Homebrew is not available (macOS only)"
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
break;
|
|
3448
|
+
case "apt":
|
|
3449
|
+
if (!await isAptAvailable()) {
|
|
3450
|
+
return {
|
|
3451
|
+
success: false,
|
|
3452
|
+
option,
|
|
3453
|
+
error: "apt-get is not available (Debian/Ubuntu only)"
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
break;
|
|
3457
|
+
case "node":
|
|
3458
|
+
if (!await getPreferredNodeManager()) {
|
|
3459
|
+
return {
|
|
3460
|
+
success: false,
|
|
3461
|
+
option,
|
|
3462
|
+
error: "No Node.js package manager found"
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
break;
|
|
3466
|
+
case "pip":
|
|
3467
|
+
if (!await isPipAvailable()) {
|
|
3468
|
+
return {
|
|
3469
|
+
success: false,
|
|
3470
|
+
option,
|
|
3471
|
+
error: "pip/pip3 is not available"
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
break;
|
|
3475
|
+
case "cargo":
|
|
3476
|
+
if (!await isCargoAvailable()) {
|
|
3477
|
+
return {
|
|
3478
|
+
success: false,
|
|
3479
|
+
option,
|
|
3480
|
+
error: "cargo is not available (Rust)"
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
break;
|
|
3484
|
+
case "manual":
|
|
3485
|
+
return {
|
|
3486
|
+
success: false,
|
|
3487
|
+
option,
|
|
3488
|
+
error: `Manual installation required: ${option.label || "See skill documentation"}`
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
let command = buildInstallCommand(option);
|
|
3492
|
+
if (!command) {
|
|
3493
|
+
return {
|
|
3494
|
+
success: false,
|
|
3495
|
+
option,
|
|
3496
|
+
error: `Cannot build command for install kind: ${option.kind}`
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
try {
|
|
3500
|
+
command = await resolveNodeManager(command);
|
|
3501
|
+
} catch (error) {
|
|
3502
|
+
return {
|
|
3503
|
+
success: false,
|
|
3504
|
+
option,
|
|
3505
|
+
error: error instanceof Error ? error.message : "Failed to resolve node manager"
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
const result = await executeInstall(command, {
|
|
3509
|
+
timeout,
|
|
3510
|
+
onProgress,
|
|
3511
|
+
dryRun
|
|
3512
|
+
});
|
|
3513
|
+
return {
|
|
3514
|
+
...result,
|
|
3515
|
+
option,
|
|
3516
|
+
command
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
async function findBestInstallOption(options) {
|
|
3520
|
+
const platform = detectPlatform();
|
|
3521
|
+
const preferenceOrder = [];
|
|
3522
|
+
if (platform === "darwin") {
|
|
3523
|
+
preferenceOrder.push("brew", "node", "pip", "cargo");
|
|
3524
|
+
} else if (platform === "linux") {
|
|
3525
|
+
preferenceOrder.push("apt", "node", "pip", "cargo");
|
|
3526
|
+
} else {
|
|
3527
|
+
preferenceOrder.push("node", "pip", "cargo");
|
|
3528
|
+
}
|
|
3529
|
+
for (const kind of preferenceOrder) {
|
|
3530
|
+
const option = options.find((o) => o.kind === kind);
|
|
3531
|
+
if (option) {
|
|
3532
|
+
switch (kind) {
|
|
3533
|
+
case "brew":
|
|
3534
|
+
if (await isHomebrewAvailable()) return option;
|
|
3535
|
+
break;
|
|
3536
|
+
case "apt":
|
|
3537
|
+
if (await isAptAvailable()) return option;
|
|
3538
|
+
break;
|
|
3539
|
+
case "node":
|
|
3540
|
+
if (await getPreferredNodeManager()) return option;
|
|
3541
|
+
break;
|
|
3542
|
+
case "pip":
|
|
3543
|
+
if (await isPipAvailable()) return option;
|
|
3544
|
+
break;
|
|
3545
|
+
case "cargo":
|
|
3546
|
+
if (await isCargoAvailable()) return option;
|
|
3547
|
+
break;
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
const manual = options.find((o) => o.kind === "manual");
|
|
3552
|
+
return manual || null;
|
|
3553
|
+
}
|
|
3554
|
+
async function getAvailableInstallOptions(options) {
|
|
3555
|
+
const available = [];
|
|
3556
|
+
for (const option of options) {
|
|
3557
|
+
switch (option.kind) {
|
|
3558
|
+
case "brew":
|
|
3559
|
+
if (await isHomebrewAvailable()) available.push(option);
|
|
3560
|
+
break;
|
|
3561
|
+
case "apt":
|
|
3562
|
+
if (await isAptAvailable()) available.push(option);
|
|
3563
|
+
break;
|
|
3564
|
+
case "node":
|
|
3565
|
+
if (await getPreferredNodeManager()) available.push(option);
|
|
3566
|
+
break;
|
|
3567
|
+
case "pip":
|
|
3568
|
+
if (await isPipAvailable()) available.push(option);
|
|
3569
|
+
break;
|
|
3570
|
+
case "cargo":
|
|
3571
|
+
if (await isCargoAvailable()) available.push(option);
|
|
3572
|
+
break;
|
|
3573
|
+
case "manual":
|
|
3574
|
+
available.push(option);
|
|
3575
|
+
break;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return available;
|
|
3579
|
+
}
|
|
3580
|
+
async function installSkillDependencies(skill, options = {}) {
|
|
3581
|
+
const metadata = skill.frontmatter.metadata?.otto;
|
|
3582
|
+
const installOptions = metadata?.install || [];
|
|
3583
|
+
if (installOptions.length === 0) {
|
|
3584
|
+
return [];
|
|
3585
|
+
}
|
|
3586
|
+
const results = [];
|
|
3587
|
+
const { onProgress, dryRun } = options;
|
|
3588
|
+
const binsByOption = /* @__PURE__ */ new Map();
|
|
3589
|
+
for (const option of installOptions) {
|
|
3590
|
+
const bins = option.bins || [];
|
|
3591
|
+
for (const bin of bins) {
|
|
3592
|
+
if (!binsByOption.has(bin)) {
|
|
3593
|
+
binsByOption.set(bin, []);
|
|
3594
|
+
}
|
|
3595
|
+
binsByOption.get(bin).push(option);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
for (const [bin, opts] of binsByOption) {
|
|
3599
|
+
if (await binaryExists(bin)) {
|
|
3600
|
+
onProgress?.({
|
|
3601
|
+
phase: "complete",
|
|
3602
|
+
message: `${bin} is already installed`
|
|
3603
|
+
});
|
|
3604
|
+
continue;
|
|
3605
|
+
}
|
|
3606
|
+
const bestOption = await findBestInstallOption(opts);
|
|
3607
|
+
if (!bestOption) {
|
|
3608
|
+
results.push({
|
|
3609
|
+
success: false,
|
|
3610
|
+
option: opts[0],
|
|
3611
|
+
error: `No available installation method for ${bin}`
|
|
3612
|
+
});
|
|
3613
|
+
continue;
|
|
3614
|
+
}
|
|
3615
|
+
const result = await installSkillDependency({
|
|
3616
|
+
option: bestOption,
|
|
3617
|
+
onProgress,
|
|
3618
|
+
dryRun
|
|
3619
|
+
});
|
|
3620
|
+
results.push(result);
|
|
3621
|
+
if (!result.success) {
|
|
3622
|
+
break;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return results;
|
|
3626
|
+
}
|
|
3627
|
+
async function getInstallPlan(skill) {
|
|
3628
|
+
const metadata = skill.frontmatter.metadata?.otto;
|
|
3629
|
+
const requiredBins = metadata?.requires?.bins || [];
|
|
3630
|
+
const installOptions = metadata?.install || [];
|
|
3631
|
+
const missingBins = [];
|
|
3632
|
+
for (const bin of requiredBins) {
|
|
3633
|
+
if (!await binaryExists(bin)) {
|
|
3634
|
+
missingBins.push(bin);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
const availableOptions = await getAvailableInstallOptions(installOptions);
|
|
3638
|
+
const recommendedOptions = [];
|
|
3639
|
+
for (const bin of missingBins) {
|
|
3640
|
+
const opts = installOptions.filter((o) => o.bins?.includes(bin));
|
|
3641
|
+
const best = await findBestInstallOption(opts);
|
|
3642
|
+
if (best && !recommendedOptions.includes(best)) {
|
|
3643
|
+
recommendedOptions.push(best);
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
return {
|
|
3647
|
+
requiredBins,
|
|
3648
|
+
missingBins,
|
|
3649
|
+
availableOptions,
|
|
3650
|
+
recommendedOptions
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
export {
|
|
3654
|
+
AgentSkillsService,
|
|
3655
|
+
AgentSkillsService as ClawHubService,
|
|
3656
|
+
FileSystemSkillStore,
|
|
3657
|
+
MemorySkillStore,
|
|
3658
|
+
SKILL_BODY_RECOMMENDED_TOKENS,
|
|
3659
|
+
SKILL_COMPATIBILITY_MAX_LENGTH,
|
|
3660
|
+
SKILL_DESCRIPTION_MAX_LENGTH,
|
|
3661
|
+
SKILL_NAME_MAX_LENGTH,
|
|
3662
|
+
SKILL_NAME_PATTERN,
|
|
3663
|
+
SKILL_SOURCE_PRECEDENCE,
|
|
3664
|
+
agentSkillsPlugin,
|
|
3665
|
+
catalogAwarenessProvider,
|
|
3666
|
+
clawHubPlugin,
|
|
3667
|
+
createStorage,
|
|
3668
|
+
plugin_default as default,
|
|
3669
|
+
estimateTokens,
|
|
3670
|
+
extractBody,
|
|
3671
|
+
findBestInstallOption,
|
|
3672
|
+
generateSkillsXml,
|
|
3673
|
+
getAvailableInstallOptions,
|
|
3674
|
+
getInstallPlan,
|
|
3675
|
+
getPreferredNodeManager,
|
|
3676
|
+
getSkillDetailsAction,
|
|
3677
|
+
getSkillGuidanceAction,
|
|
3678
|
+
installSkillDependencies,
|
|
3679
|
+
installSkillDependency,
|
|
3680
|
+
isAptAvailable,
|
|
3681
|
+
isCargoAvailable,
|
|
3682
|
+
isHomebrewAvailable,
|
|
3683
|
+
isPipAvailable,
|
|
3684
|
+
loadSkillFromStorage,
|
|
3685
|
+
parseFrontmatter,
|
|
3686
|
+
runSkillScriptAction,
|
|
3687
|
+
searchSkillsAction,
|
|
3688
|
+
skillInstructionsProvider,
|
|
3689
|
+
skillsOverviewProvider,
|
|
3690
|
+
skillsProvider,
|
|
3691
|
+
skillsSummaryProvider,
|
|
3692
|
+
startSyncTask,
|
|
3693
|
+
syncCatalogAction,
|
|
3694
|
+
syncCatalogTask,
|
|
3695
|
+
validateFrontmatter,
|
|
3696
|
+
validateSkillDirectory
|
|
3697
|
+
};
|
|
3698
|
+
//# sourceMappingURL=index.js.map
|