@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/src/storage.ts
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Storage Abstraction
|
|
3
|
+
*
|
|
4
|
+
* Provides two storage backends:
|
|
5
|
+
* - MemorySkillStore: For browser/virtual FS environments (skills in memory)
|
|
6
|
+
* - FileSystemSkillStore: For Node.js/native environments (skills on disk)
|
|
7
|
+
*
|
|
8
|
+
* Both implement the same interface for seamless switching.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Skill, SkillFrontmatter } from "./types";
|
|
12
|
+
import { parseFrontmatter, validateFrontmatter } from "./parser";
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// STORAGE INTERFACE
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Skill file representation for in-memory storage.
|
|
20
|
+
*/
|
|
21
|
+
export interface SkillFile {
|
|
22
|
+
path: string;
|
|
23
|
+
content: string | Uint8Array;
|
|
24
|
+
isText: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Skill package - all files for a skill.
|
|
29
|
+
*/
|
|
30
|
+
export interface SkillPackage {
|
|
31
|
+
slug: string;
|
|
32
|
+
files: Map<string, SkillFile>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Storage interface for skill management.
|
|
37
|
+
*/
|
|
38
|
+
export interface ISkillStorage {
|
|
39
|
+
/** Storage type identifier */
|
|
40
|
+
readonly type: "memory" | "filesystem";
|
|
41
|
+
|
|
42
|
+
/** Initialize storage */
|
|
43
|
+
initialize(): Promise<void>;
|
|
44
|
+
|
|
45
|
+
/** List all installed skill slugs */
|
|
46
|
+
listSkills(): Promise<string[]>;
|
|
47
|
+
|
|
48
|
+
/** Check if a skill exists */
|
|
49
|
+
hasSkill(slug: string): Promise<boolean>;
|
|
50
|
+
|
|
51
|
+
/** Load a skill's SKILL.md content */
|
|
52
|
+
loadSkillContent(slug: string): Promise<string | null>;
|
|
53
|
+
|
|
54
|
+
/** Load a specific file from a skill */
|
|
55
|
+
loadFile(
|
|
56
|
+
slug: string,
|
|
57
|
+
relativePath: string,
|
|
58
|
+
): Promise<string | Uint8Array | null>;
|
|
59
|
+
|
|
60
|
+
/** List files in a skill directory */
|
|
61
|
+
listFiles(slug: string, subdir?: string): Promise<string[]>;
|
|
62
|
+
|
|
63
|
+
/** Save a complete skill package */
|
|
64
|
+
saveSkill(pkg: SkillPackage): Promise<void>;
|
|
65
|
+
|
|
66
|
+
/** Delete a skill */
|
|
67
|
+
deleteSkill(slug: string): Promise<boolean>;
|
|
68
|
+
|
|
69
|
+
/** Get skill directory path (filesystem) or virtual path (memory) */
|
|
70
|
+
getSkillPath(slug: string): string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================
|
|
74
|
+
// MEMORY STORAGE (Browser/Virtual FS)
|
|
75
|
+
// ============================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* In-memory skill storage for browser environments.
|
|
79
|
+
*
|
|
80
|
+
* Skills are stored entirely in memory, making this suitable for:
|
|
81
|
+
* - Browser environments without filesystem access
|
|
82
|
+
* - Virtual FS scenarios
|
|
83
|
+
* - Testing
|
|
84
|
+
* - Ephemeral skill loading
|
|
85
|
+
*/
|
|
86
|
+
export class MemorySkillStore implements ISkillStorage {
|
|
87
|
+
readonly type = "memory" as const;
|
|
88
|
+
|
|
89
|
+
private skills: Map<string, SkillPackage> = new Map();
|
|
90
|
+
private basePath: string;
|
|
91
|
+
|
|
92
|
+
constructor(basePath = "/virtual/skills") {
|
|
93
|
+
this.basePath = basePath;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async initialize(): Promise<void> {
|
|
97
|
+
// No-op for memory storage
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async listSkills(): Promise<string[]> {
|
|
101
|
+
return Array.from(this.skills.keys());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async hasSkill(slug: string): Promise<boolean> {
|
|
105
|
+
return this.skills.has(slug);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async loadSkillContent(slug: string): Promise<string | null> {
|
|
109
|
+
const pkg = this.skills.get(slug);
|
|
110
|
+
if (!pkg) return null;
|
|
111
|
+
|
|
112
|
+
const skillMd = pkg.files.get("SKILL.md");
|
|
113
|
+
if (!skillMd || !skillMd.isText) return null;
|
|
114
|
+
|
|
115
|
+
return skillMd.content as string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async loadFile(
|
|
119
|
+
slug: string,
|
|
120
|
+
relativePath: string,
|
|
121
|
+
): Promise<string | Uint8Array | null> {
|
|
122
|
+
const pkg = this.skills.get(slug);
|
|
123
|
+
if (!pkg) return null;
|
|
124
|
+
|
|
125
|
+
const file = pkg.files.get(relativePath);
|
|
126
|
+
if (!file) return null;
|
|
127
|
+
|
|
128
|
+
return file.content;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async listFiles(slug: string, subdir?: string): Promise<string[]> {
|
|
132
|
+
const pkg = this.skills.get(slug);
|
|
133
|
+
if (!pkg) return [];
|
|
134
|
+
|
|
135
|
+
const prefix = subdir ? `${subdir}/` : "";
|
|
136
|
+
const files: string[] = [];
|
|
137
|
+
|
|
138
|
+
for (const [path] of pkg.files) {
|
|
139
|
+
if (subdir) {
|
|
140
|
+
if (
|
|
141
|
+
path.startsWith(prefix) &&
|
|
142
|
+
!path.slice(prefix.length).includes("/")
|
|
143
|
+
) {
|
|
144
|
+
files.push(path.slice(prefix.length));
|
|
145
|
+
}
|
|
146
|
+
} else if (!path.includes("/")) {
|
|
147
|
+
files.push(path);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return files;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async saveSkill(pkg: SkillPackage): Promise<void> {
|
|
155
|
+
this.skills.set(pkg.slug, pkg);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async deleteSkill(slug: string): Promise<boolean> {
|
|
159
|
+
return this.skills.delete(slug);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getSkillPath(slug: string): string {
|
|
163
|
+
return `${this.basePath}/${slug}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Load a skill directly from content (no network/file needed).
|
|
168
|
+
*/
|
|
169
|
+
async loadFromContent(
|
|
170
|
+
slug: string,
|
|
171
|
+
skillMdContent: string,
|
|
172
|
+
additionalFiles?: Map<string, string | Uint8Array>,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const files = new Map<string, SkillFile>();
|
|
175
|
+
|
|
176
|
+
// Add SKILL.md
|
|
177
|
+
files.set("SKILL.md", {
|
|
178
|
+
path: "SKILL.md",
|
|
179
|
+
content: skillMdContent,
|
|
180
|
+
isText: true,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Add any additional files
|
|
184
|
+
if (additionalFiles) {
|
|
185
|
+
for (const [path, content] of additionalFiles) {
|
|
186
|
+
files.set(path, {
|
|
187
|
+
path,
|
|
188
|
+
content,
|
|
189
|
+
isText: typeof content === "string",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await this.saveSkill({ slug, files });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Load a skill from a zip buffer (for registry downloads).
|
|
199
|
+
*/
|
|
200
|
+
async loadFromZip(slug: string, zipBuffer: Uint8Array): Promise<void> {
|
|
201
|
+
// Dynamic import for browser compatibility
|
|
202
|
+
const { unzipSync } = await import("fflate");
|
|
203
|
+
const unzipped = unzipSync(zipBuffer);
|
|
204
|
+
|
|
205
|
+
const files = new Map<string, SkillFile>();
|
|
206
|
+
|
|
207
|
+
for (const [fileName, data] of Object.entries(unzipped)) {
|
|
208
|
+
if (fileName.endsWith("/")) continue;
|
|
209
|
+
|
|
210
|
+
// Sanitize path
|
|
211
|
+
const parts = fileName
|
|
212
|
+
.split("/")
|
|
213
|
+
.filter((p) => p && p !== ".." && p !== ".");
|
|
214
|
+
if (parts.length === 0) continue;
|
|
215
|
+
|
|
216
|
+
const relativePath = parts.join("/");
|
|
217
|
+
const isText = isTextFile(relativePath);
|
|
218
|
+
|
|
219
|
+
files.set(relativePath, {
|
|
220
|
+
path: relativePath,
|
|
221
|
+
content: isText ? new TextDecoder().decode(data) : data,
|
|
222
|
+
isText,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await this.saveSkill({ slug, files });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get the full skill package (for export/transfer).
|
|
231
|
+
*/
|
|
232
|
+
getPackage(slug: string): SkillPackage | undefined {
|
|
233
|
+
return this.skills.get(slug);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Save a skill package from simple file list format.
|
|
238
|
+
* Convenience method for use with GitHub/URL installs.
|
|
239
|
+
*/
|
|
240
|
+
async savePackage(pkg: {
|
|
241
|
+
slug: string;
|
|
242
|
+
files: Array<{ name: string; content: string | Uint8Array }>;
|
|
243
|
+
loadedAt?: number;
|
|
244
|
+
}): Promise<void> {
|
|
245
|
+
const files = new Map<string, SkillFile>();
|
|
246
|
+
|
|
247
|
+
for (const file of pkg.files) {
|
|
248
|
+
const isText = typeof file.content === "string";
|
|
249
|
+
files.set(file.name, {
|
|
250
|
+
path: file.name,
|
|
251
|
+
content: file.content,
|
|
252
|
+
isText,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await this.saveSkill({ slug: pkg.slug, files });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get all skills in memory.
|
|
261
|
+
*/
|
|
262
|
+
getAllPackages(): Map<string, SkillPackage> {
|
|
263
|
+
return new Map(this.skills);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ============================================================
|
|
268
|
+
// FILESYSTEM STORAGE (Node.js/Native)
|
|
269
|
+
// ============================================================
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Filesystem-based skill storage for Node.js environments.
|
|
273
|
+
*
|
|
274
|
+
* Skills are stored on disk, making this suitable for:
|
|
275
|
+
* - Node.js server environments
|
|
276
|
+
* - CLI tools
|
|
277
|
+
* - Persistent skill installations
|
|
278
|
+
*/
|
|
279
|
+
export class FileSystemSkillStore implements ISkillStorage {
|
|
280
|
+
readonly type = "filesystem" as const;
|
|
281
|
+
|
|
282
|
+
readonly basePath: string;
|
|
283
|
+
private fs: typeof import("fs") | null = null;
|
|
284
|
+
private path: typeof import("path") | null = null;
|
|
285
|
+
|
|
286
|
+
constructor(basePath = "./skills") {
|
|
287
|
+
this.basePath = basePath;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async initialize(): Promise<void> {
|
|
291
|
+
// Dynamic imports for Node.js
|
|
292
|
+
try {
|
|
293
|
+
this.fs = await import("fs");
|
|
294
|
+
this.path = await import("path");
|
|
295
|
+
|
|
296
|
+
// Ensure base directory exists
|
|
297
|
+
if (!this.fs.existsSync(this.basePath)) {
|
|
298
|
+
this.fs.mkdirSync(this.basePath, { recursive: true });
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
throw new Error("FileSystemSkillStore requires Node.js fs module");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async listSkills(): Promise<string[]> {
|
|
306
|
+
if (!this.fs) await this.initialize();
|
|
307
|
+
|
|
308
|
+
const entries = this.fs!.readdirSync(this.basePath, {
|
|
309
|
+
withFileTypes: true,
|
|
310
|
+
});
|
|
311
|
+
return entries
|
|
312
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
313
|
+
.map((e) => e.name);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async hasSkill(slug: string): Promise<boolean> {
|
|
317
|
+
if (!this.fs) await this.initialize();
|
|
318
|
+
|
|
319
|
+
const skillPath = this.path!.join(this.basePath, slug, "SKILL.md");
|
|
320
|
+
return this.fs!.existsSync(skillPath);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async loadSkillContent(slug: string): Promise<string | null> {
|
|
324
|
+
if (!this.fs) await this.initialize();
|
|
325
|
+
|
|
326
|
+
const skillPath = this.path!.join(this.basePath, slug, "SKILL.md");
|
|
327
|
+
if (!this.fs!.existsSync(skillPath)) return null;
|
|
328
|
+
|
|
329
|
+
return this.fs!.readFileSync(skillPath, "utf-8");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async loadFile(
|
|
333
|
+
slug: string,
|
|
334
|
+
relativePath: string,
|
|
335
|
+
): Promise<string | Uint8Array | null> {
|
|
336
|
+
if (!this.fs) await this.initialize();
|
|
337
|
+
|
|
338
|
+
// Sanitize path to prevent directory traversal
|
|
339
|
+
const safePath = this.path!.basename(relativePath);
|
|
340
|
+
const subdir = this.path!.dirname(relativePath);
|
|
341
|
+
const fullPath = this.path!.join(this.basePath, slug, subdir, safePath);
|
|
342
|
+
|
|
343
|
+
if (!this.fs!.existsSync(fullPath)) return null;
|
|
344
|
+
|
|
345
|
+
if (isTextFile(relativePath)) {
|
|
346
|
+
return this.fs!.readFileSync(fullPath, "utf-8");
|
|
347
|
+
} else {
|
|
348
|
+
return new Uint8Array(this.fs!.readFileSync(fullPath));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async listFiles(slug: string, subdir?: string): Promise<string[]> {
|
|
353
|
+
if (!this.fs) await this.initialize();
|
|
354
|
+
|
|
355
|
+
const dirPath = subdir
|
|
356
|
+
? this.path!.join(this.basePath, slug, subdir)
|
|
357
|
+
: this.path!.join(this.basePath, slug);
|
|
358
|
+
|
|
359
|
+
if (!this.fs!.existsSync(dirPath)) return [];
|
|
360
|
+
|
|
361
|
+
return this.fs!.readdirSync(dirPath).filter((f) => !f.startsWith("."));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async saveSkill(pkg: SkillPackage): Promise<void> {
|
|
365
|
+
if (!this.fs) await this.initialize();
|
|
366
|
+
|
|
367
|
+
const skillDir = this.path!.join(this.basePath, pkg.slug);
|
|
368
|
+
|
|
369
|
+
// Create skill directory
|
|
370
|
+
if (!this.fs!.existsSync(skillDir)) {
|
|
371
|
+
this.fs!.mkdirSync(skillDir, { recursive: true });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Write all files
|
|
375
|
+
for (const [relativePath, file] of pkg.files) {
|
|
376
|
+
const fullPath = this.path!.join(skillDir, relativePath);
|
|
377
|
+
const dir = this.path!.dirname(fullPath);
|
|
378
|
+
|
|
379
|
+
// Ensure directory exists
|
|
380
|
+
if (!this.fs!.existsSync(dir)) {
|
|
381
|
+
this.fs!.mkdirSync(dir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Write file
|
|
385
|
+
if (file.isText) {
|
|
386
|
+
this.fs!.writeFileSync(fullPath, file.content as string, "utf-8");
|
|
387
|
+
} else {
|
|
388
|
+
this.fs!.writeFileSync(fullPath, file.content as Uint8Array);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async deleteSkill(slug: string): Promise<boolean> {
|
|
394
|
+
if (!this.fs) await this.initialize();
|
|
395
|
+
|
|
396
|
+
const skillDir = this.path!.join(this.basePath, slug);
|
|
397
|
+
if (!this.fs!.existsSync(skillDir)) return false;
|
|
398
|
+
|
|
399
|
+
// Recursive delete
|
|
400
|
+
this.fs!.rmSync(skillDir, { recursive: true, force: true });
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getSkillPath(slug: string): string {
|
|
405
|
+
return this.path
|
|
406
|
+
? this.path.resolve(this.basePath, slug)
|
|
407
|
+
: `${this.basePath}/${slug}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Save a skill from a zip buffer.
|
|
412
|
+
*/
|
|
413
|
+
async saveFromZip(slug: string, zipBuffer: Uint8Array): Promise<void> {
|
|
414
|
+
const { unzipSync } = await import("fflate");
|
|
415
|
+
const unzipped = unzipSync(zipBuffer);
|
|
416
|
+
|
|
417
|
+
const files = new Map<string, SkillFile>();
|
|
418
|
+
|
|
419
|
+
for (const [fileName, data] of Object.entries(unzipped)) {
|
|
420
|
+
if (fileName.endsWith("/")) continue;
|
|
421
|
+
|
|
422
|
+
const parts = fileName
|
|
423
|
+
.split("/")
|
|
424
|
+
.filter((p) => p && p !== ".." && p !== ".");
|
|
425
|
+
if (parts.length === 0) continue;
|
|
426
|
+
|
|
427
|
+
const relativePath = parts.join("/");
|
|
428
|
+
const isText = isTextFile(relativePath);
|
|
429
|
+
|
|
430
|
+
files.set(relativePath, {
|
|
431
|
+
path: relativePath,
|
|
432
|
+
content: isText ? new TextDecoder().decode(data) : data,
|
|
433
|
+
isText,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
await this.saveSkill({ slug, files });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ============================================================
|
|
442
|
+
// HELPER FUNCTIONS
|
|
443
|
+
// ============================================================
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Determine if a file is text-based by extension.
|
|
447
|
+
*/
|
|
448
|
+
function isTextFile(filePath: string): boolean {
|
|
449
|
+
const textExtensions = new Set([
|
|
450
|
+
".md",
|
|
451
|
+
".txt",
|
|
452
|
+
".json",
|
|
453
|
+
".yaml",
|
|
454
|
+
".yml",
|
|
455
|
+
".toml",
|
|
456
|
+
".js",
|
|
457
|
+
".ts",
|
|
458
|
+
".py",
|
|
459
|
+
".rs",
|
|
460
|
+
".sh",
|
|
461
|
+
".bash",
|
|
462
|
+
".html",
|
|
463
|
+
".css",
|
|
464
|
+
".xml",
|
|
465
|
+
".svg",
|
|
466
|
+
".env",
|
|
467
|
+
".gitignore",
|
|
468
|
+
".dockerignore",
|
|
469
|
+
]);
|
|
470
|
+
|
|
471
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
472
|
+
return textExtensions.has(ext) || !filePath.includes(".");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Create the appropriate storage based on environment.
|
|
477
|
+
*/
|
|
478
|
+
export function createStorage(options: {
|
|
479
|
+
type?: "memory" | "filesystem" | "auto";
|
|
480
|
+
basePath?: string;
|
|
481
|
+
}): ISkillStorage {
|
|
482
|
+
const { type = "auto", basePath } = options;
|
|
483
|
+
|
|
484
|
+
if (type === "memory") {
|
|
485
|
+
return new MemorySkillStore(basePath);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (type === "filesystem") {
|
|
489
|
+
return new FileSystemSkillStore(basePath);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Auto-detect: use memory in browser, filesystem in Node.js
|
|
493
|
+
if (typeof window !== "undefined" || typeof process === "undefined") {
|
|
494
|
+
return new MemorySkillStore(basePath);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return new FileSystemSkillStore(basePath);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ============================================================
|
|
501
|
+
// SKILL LOADER (Works with any storage)
|
|
502
|
+
// ============================================================
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Load a skill from storage into a Skill object.
|
|
506
|
+
*/
|
|
507
|
+
export async function loadSkillFromStorage(
|
|
508
|
+
storage: ISkillStorage,
|
|
509
|
+
slug: string,
|
|
510
|
+
options: { validate?: boolean } = {},
|
|
511
|
+
): Promise<Skill | null> {
|
|
512
|
+
const content = await storage.loadSkillContent(slug);
|
|
513
|
+
if (!content) return null;
|
|
514
|
+
|
|
515
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
516
|
+
if (!frontmatter) return null;
|
|
517
|
+
|
|
518
|
+
// Validate if requested
|
|
519
|
+
if (options.validate !== false) {
|
|
520
|
+
const result = validateFrontmatter(frontmatter, slug);
|
|
521
|
+
if (!result.valid) {
|
|
522
|
+
console.warn(`Skill ${slug} validation failed:`, result.errors);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// List resource files
|
|
527
|
+
const scripts = await storage.listFiles(slug, "scripts");
|
|
528
|
+
const references = await storage.listFiles(slug, "references");
|
|
529
|
+
const assets = await storage.listFiles(slug, "assets");
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
slug,
|
|
533
|
+
name: frontmatter.name,
|
|
534
|
+
description: frontmatter.description,
|
|
535
|
+
version: frontmatter.metadata?.version?.toString() || "local",
|
|
536
|
+
content,
|
|
537
|
+
frontmatter,
|
|
538
|
+
path: storage.getSkillPath(slug),
|
|
539
|
+
scripts,
|
|
540
|
+
references,
|
|
541
|
+
assets,
|
|
542
|
+
loadedAt: Date.now(),
|
|
543
|
+
};
|
|
544
|
+
}
|