@magnolia/skill-loader 0.1.0-preview.1
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/LICENSE.txt +27 -0
- package/README.md +354 -0
- package/dist/filter.d.ts +26 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +160 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +81 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +192 -0
- package/dist/loader.js.map +1 -0
- package/dist/manager.d.ts +131 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +236 -0
- package/dist/manager.js.map +1 -0
- package/dist/parser.d.ts +25 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +110 -0
- package/dist/parser.js.map +1 -0
- package/dist/sources/directory.d.ts +18 -0
- package/dist/sources/directory.d.ts.map +1 -0
- package/dist/sources/directory.js +57 -0
- package/dist/sources/directory.js.map +1 -0
- package/dist/sources/file.d.ts +15 -0
- package/dist/sources/file.d.ts.map +1 -0
- package/dist/sources/file.js +30 -0
- package/dist/sources/file.js.map +1 -0
- package/dist/sources/inline.d.ts +15 -0
- package/dist/sources/inline.d.ts.map +1 -0
- package/dist/sources/inline.js +21 -0
- package/dist/sources/inline.js.map +1 -0
- package/dist/sources/url.d.ts +20 -0
- package/dist/sources/url.d.ts.map +1 -0
- package/dist/sources/url.js +65 -0
- package/dist/sources/url.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2026 Magnolia International Ltd.
|
|
2
|
+
(http://www.magnolia-cms.com). All rights reserved.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
The software is dual-licensed under both the Magnolia
|
|
6
|
+
Network Agreement and the GNU General Public License.
|
|
7
|
+
You may elect to use one or the other of these licenses.
|
|
8
|
+
|
|
9
|
+
The software is distributed in the hope that it will be
|
|
10
|
+
useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
|
|
11
|
+
implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
12
|
+
PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
|
|
13
|
+
Redistribution, except as permitted by whichever of the GPL
|
|
14
|
+
or MNA you select, is prohibited.
|
|
15
|
+
|
|
16
|
+
1. For the GPL license (GPL), you can redistribute and/or
|
|
17
|
+
modify this file under the terms of the GNU General
|
|
18
|
+
Public License, Version 3, as published by the Free Software
|
|
19
|
+
Foundation. You should have received a copy of the GNU
|
|
20
|
+
General Public License, Version 3 along with this program;
|
|
21
|
+
if not, write to the Free Software Foundation, Inc., 51
|
|
22
|
+
Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
23
|
+
|
|
24
|
+
2. For the Magnolia Network Agreement (MNA), this file
|
|
25
|
+
and the accompanying materials are made available under the
|
|
26
|
+
terms of the MNA which accompanies this distribution, and
|
|
27
|
+
is available at http://www.magnolia-cms.com/mna.html
|
package/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# @magnolia/skill-loader
|
|
2
|
+
|
|
3
|
+
A Node.js library for loading, filtering, and managing AI prompt "skills" for use with LLMs.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Skills are structured prompts with metadata that can be conditionally included based on context (target platform, version constraints, detected features, etc.). This library provides a flexible, type-safe way to manage these prompts across different tools and applications.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @magnolia/skill-loader
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Using SkillLoader (Low-level API)
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { SkillLoader } from '@magnolia/skill-loader';
|
|
21
|
+
|
|
22
|
+
// Create a loader with a directory of skill files
|
|
23
|
+
const loader = new SkillLoader()
|
|
24
|
+
.addDirectory('./skills');
|
|
25
|
+
|
|
26
|
+
// Load skills filtered by context
|
|
27
|
+
const skills = await loader.load({
|
|
28
|
+
target: 'freemarker',
|
|
29
|
+
version: '6.3.0',
|
|
30
|
+
detectedFieldTypes: ['textField', 'damLinkField'],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Get combined prompt content
|
|
34
|
+
const prompt = await loader.getPrompt({
|
|
35
|
+
target: 'freemarker',
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using SkillManager (High-level API with Priority Management)
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { SkillManager } from '@magnolia/skill-loader';
|
|
43
|
+
|
|
44
|
+
// Create a manager with priority-based directory loading
|
|
45
|
+
const manager = new SkillManager({
|
|
46
|
+
directories: [
|
|
47
|
+
{ path: './plugin-skills', priority: 1 }, // Lowest priority
|
|
48
|
+
{ path: '~/.app/skills', priority: 10 }, // Medium priority
|
|
49
|
+
{ path: './project/skills', priority: 100 }, // Highest priority
|
|
50
|
+
],
|
|
51
|
+
cacheTtl: 60000, // 1 minute cache
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Register additional directories dynamically
|
|
55
|
+
manager.registerSkillDirectory('./custom-skills', 50);
|
|
56
|
+
|
|
57
|
+
// Load and use skills
|
|
58
|
+
const prompt = await manager.getPrompt({ target: 'freemarker' });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Skill File Format
|
|
62
|
+
|
|
63
|
+
Skills are Markdown files with YAML frontmatter:
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
---
|
|
67
|
+
name: my-skill
|
|
68
|
+
description: A helpful skill for FreeMarker templates
|
|
69
|
+
targets: [freemarker]
|
|
70
|
+
version: ">=6.2"
|
|
71
|
+
tags: [ftl, generation]
|
|
72
|
+
requires: [damLinkField]
|
|
73
|
+
priority: 100
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
# My Skill
|
|
77
|
+
|
|
78
|
+
Your prompt content here...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Frontmatter Fields
|
|
82
|
+
|
|
83
|
+
| Field | Type | Description |
|
|
84
|
+
|---------------|----------|-------------------------------------------------------|
|
|
85
|
+
| `name` | string | Unique identifier (defaults to filename) |
|
|
86
|
+
| `description` | string | Human-readable description |
|
|
87
|
+
| `targets` | string[] | Target platforms (e.g., `freemarker`, `spa`, `react`) |
|
|
88
|
+
| `version` | string | Semver constraint (e.g., `>=6.2`, `^6.3.0`) |
|
|
89
|
+
| `tags` | string[] | Tags for categorization and filtering |
|
|
90
|
+
| `requires` | string[] | Field types that must be detected for inclusion |
|
|
91
|
+
| `priority` | number | Sort order (higher = earlier in output) |
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### SkillManager (Recommended)
|
|
96
|
+
|
|
97
|
+
The `SkillManager` provides a high-level API for managing skills from multiple directories with configurable priority-based loading. This is the recommended approach for applications that need to manage skills from multiple sources.
|
|
98
|
+
|
|
99
|
+
#### Constructor
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const manager = new SkillManager(options?)
|
|
103
|
+
|
|
104
|
+
interface SkillManagerOptions {
|
|
105
|
+
directories?: SkillDirectory[]; // Directories to load, sorted by priority
|
|
106
|
+
enabled?: boolean; // Enable/disable skill loading (default: true)
|
|
107
|
+
cacheTtl?: number; // Cache TTL in milliseconds (default: 60000)
|
|
108
|
+
customFilter?: (skill, context) => boolean; // Custom filter function
|
|
109
|
+
customSort?: (a, b) => number; // Custom sort function
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface SkillDirectory {
|
|
113
|
+
path: string; // Absolute or relative path to skills directory
|
|
114
|
+
priority?: number; // Priority (higher = loaded later = overrides earlier)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Methods
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Register a directory with optional priority
|
|
122
|
+
manager.registerSkillDirectory(path: string, priority?: number): void
|
|
123
|
+
|
|
124
|
+
// Load all skills without filtering
|
|
125
|
+
const all = await manager.loadAll(): Promise<Skill[]>
|
|
126
|
+
|
|
127
|
+
// Load skills filtered by context
|
|
128
|
+
const filtered = await manager.load(context: SkillContext): Promise<Skill[]>
|
|
129
|
+
|
|
130
|
+
// Get a single skill by name
|
|
131
|
+
const skill = await manager.get(name: string): Promise<Skill | undefined>
|
|
132
|
+
|
|
133
|
+
// Get combined prompt from filtered skills
|
|
134
|
+
const prompt = await manager.getPrompt(
|
|
135
|
+
context: SkillContext,
|
|
136
|
+
options?: PromptOptions
|
|
137
|
+
): Promise<string>
|
|
138
|
+
|
|
139
|
+
// List all skills with metadata
|
|
140
|
+
const list = await manager.listSkills(): Promise<SkillInfo[]>
|
|
141
|
+
|
|
142
|
+
// Get registered directories with priorities
|
|
143
|
+
const dirs = manager.getDirectories(): Array<{ path: string; priority: number }>
|
|
144
|
+
|
|
145
|
+
// Get statistics
|
|
146
|
+
const stats = manager.getStats(): {
|
|
147
|
+
totalDirectories: number;
|
|
148
|
+
enabledDirectories: number;
|
|
149
|
+
enabled: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Clear cache to force reload
|
|
153
|
+
manager.clearCache(): void
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Priority-Based Loading
|
|
157
|
+
|
|
158
|
+
Skills are loaded in priority order (lowest to highest). When multiple directories contain skills with the same name, the skill from the **higher priority** directory wins.
|
|
159
|
+
|
|
160
|
+
**Example:**
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const manager = new SkillManager({
|
|
164
|
+
directories: [
|
|
165
|
+
{ path: './built-in-skills', priority: 1 }, // Base skills
|
|
166
|
+
{ path: './plugin-skills', priority: 10 }, // Plugin overrides
|
|
167
|
+
{ path: './user-skills', priority: 100 }, // User overrides
|
|
168
|
+
{ path: './project-skills', priority: 1000 }, // Project-specific (highest)
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// If all directories have a skill named "dialog-generation.prompt.md",
|
|
173
|
+
// the one from './project-skills' will be used (highest priority)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Dynamic Registration
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const manager = new SkillManager();
|
|
180
|
+
|
|
181
|
+
// Register directories dynamically
|
|
182
|
+
manager.registerSkillDirectory('./core-skills', 1);
|
|
183
|
+
manager.registerSkillDirectory('./plugin-a-skills', 10);
|
|
184
|
+
manager.registerSkillDirectory('./plugin-b-skills', 11);
|
|
185
|
+
manager.registerSkillDirectory('./user-skills', 100);
|
|
186
|
+
|
|
187
|
+
// Re-registering the same path updates its priority
|
|
188
|
+
manager.registerSkillDirectory('./user-skills', 200);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Disabling Skill Loading
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const manager = new SkillManager({ enabled: false });
|
|
195
|
+
|
|
196
|
+
// All methods return empty results when disabled
|
|
197
|
+
await manager.loadAll(); // Returns []
|
|
198
|
+
await manager.getPrompt({}); // Returns ""
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### SkillLoader (Low-level API)
|
|
202
|
+
|
|
203
|
+
The `SkillLoader` provides direct control over skill sources and loading. Use this for simple use cases or when you need fine-grained control.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const loader = new SkillLoader(options?)
|
|
207
|
+
.addDirectory(path, pattern?) // Add a directory source
|
|
208
|
+
.addFile(path) // Add a single file
|
|
209
|
+
.addSkills(skills[]) // Add inline skills
|
|
210
|
+
.addUrl(url, headers?) // Add a remote URL source
|
|
211
|
+
|
|
212
|
+
// Load all skills
|
|
213
|
+
const all = await loader.loadAll();
|
|
214
|
+
|
|
215
|
+
// Load with filtering
|
|
216
|
+
const filtered = await loader.load(context);
|
|
217
|
+
|
|
218
|
+
// Get a single skill
|
|
219
|
+
const skill = await loader.get('skill-name');
|
|
220
|
+
|
|
221
|
+
// Get combined prompt
|
|
222
|
+
const prompt = await loader.getPrompt(context, options?);
|
|
223
|
+
|
|
224
|
+
// Clear cache
|
|
225
|
+
loader.clearCache();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Context Filtering
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
interface SkillContext {
|
|
232
|
+
target?: string; // Platform: 'freemarker', 'spa', 'react', etc.
|
|
233
|
+
version?: string; // Version to match against constraints
|
|
234
|
+
detectedFieldTypes?: string[]; // Field types present in current context
|
|
235
|
+
requiredTags?: string[]; // Skills must have at least one of these
|
|
236
|
+
excludedTags?: string[]; // Skills with these tags are excluded
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Standalone Functions
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import {
|
|
244
|
+
parseSkillFile,
|
|
245
|
+
createSkill,
|
|
246
|
+
filterSkills,
|
|
247
|
+
loadSkillsFromDirectory,
|
|
248
|
+
} from '@magnolia/skill-loader';
|
|
249
|
+
|
|
250
|
+
// Parse a skill file
|
|
251
|
+
const skill = parseSkillFile(content, filename);
|
|
252
|
+
|
|
253
|
+
// Create a skill programmatically
|
|
254
|
+
const skill = createSkill('name', 'content', { targets: ['spa'] });
|
|
255
|
+
|
|
256
|
+
// Filter skills
|
|
257
|
+
const filtered = filterSkills(skills, context);
|
|
258
|
+
|
|
259
|
+
// Load from directory
|
|
260
|
+
const skills = await loadSkillsFromDirectory('./skills', '*.prompt.md');
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Filtering Logic
|
|
264
|
+
|
|
265
|
+
### Target Matching
|
|
266
|
+
|
|
267
|
+
Skills with `targets` are included only if the context `target` matches (case-insensitive). Skills without `targets` are always included.
|
|
268
|
+
|
|
269
|
+
### Version Matching
|
|
270
|
+
|
|
271
|
+
Skills with `version` are included only if the context `version` satisfies the semver constraint. Uses the [semver](https://github.com/npm/node-semver) library.
|
|
272
|
+
|
|
273
|
+
### Requires Matching
|
|
274
|
+
|
|
275
|
+
Skills with `requires` are only included when at least one required field type is in `detectedFieldTypes`.
|
|
276
|
+
|
|
277
|
+
### Tag Filtering
|
|
278
|
+
|
|
279
|
+
- `requiredTags`: Skills must have at least one matching tag
|
|
280
|
+
- `excludedTags`: Skills with any matching tag are excluded
|
|
281
|
+
|
|
282
|
+
## Use Cases
|
|
283
|
+
|
|
284
|
+
### When to Use SkillManager
|
|
285
|
+
|
|
286
|
+
Use `SkillManager` when you need:
|
|
287
|
+
|
|
288
|
+
- **Multiple skill sources** with override capabilities (plugins, user configs, project-specific)
|
|
289
|
+
- **Priority-based loading** where later sources override earlier ones
|
|
290
|
+
- **Dynamic registration** of skill directories at runtime
|
|
291
|
+
- **Centralized management** of skills across an application
|
|
292
|
+
- **Easy enable/disable** of skill loading
|
|
293
|
+
|
|
294
|
+
**Example:** Plugin System
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
const manager = new SkillManager({
|
|
298
|
+
directories: [
|
|
299
|
+
{ path: '~/.myapp/skills', priority: 100 },
|
|
300
|
+
{ path: './project/skills', priority: 1000 },
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Plugins register their skills dynamically
|
|
305
|
+
plugins.forEach((plugin, index) => {
|
|
306
|
+
if (plugin.skillsDir) {
|
|
307
|
+
manager.registerSkillDirectory(plugin.skillsDir, 1 + index);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### When to Use SkillLoader
|
|
313
|
+
|
|
314
|
+
Use `SkillLoader` when you need:
|
|
315
|
+
|
|
316
|
+
- **Simple, single-source** skill loading
|
|
317
|
+
- **Fine-grained control** over sources (files, URLs, inline)
|
|
318
|
+
- **Custom loading logic** without priority management
|
|
319
|
+
- **Lightweight** skill loading without extra abstractions
|
|
320
|
+
|
|
321
|
+
**Example:** Simple CLI Tool
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const loader = new SkillLoader()
|
|
325
|
+
.addDirectory('./skills')
|
|
326
|
+
.addFile('./custom-skill.md');
|
|
327
|
+
|
|
328
|
+
const prompt = await loader.getPrompt({ target: 'react' });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Development
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# Install dependencies
|
|
335
|
+
npm install
|
|
336
|
+
|
|
337
|
+
# Build
|
|
338
|
+
npm run build
|
|
339
|
+
|
|
340
|
+
# Run tests
|
|
341
|
+
npm test
|
|
342
|
+
|
|
343
|
+
# Watch mode
|
|
344
|
+
npm run dev
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## License
|
|
348
|
+
|
|
349
|
+
See LICENSE.txt for license information.
|
|
350
|
+
|
|
351
|
+
## Support
|
|
352
|
+
|
|
353
|
+
- [GitLab Issues](https://gitlab.magnolia-platform.com/ps/extensions/ai/dev-mcp/magnolia-skill-loader/issues) **Currently Internal Access Only**
|
|
354
|
+
- [Magnolia Documentation](https://docs.magnolia-cms.com)
|
package/dist/filter.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @magnolia/skill-loader - Filtering Logic
|
|
3
|
+
*
|
|
4
|
+
* Implements skill filtering based on context (target, version, requires, tags).
|
|
5
|
+
*/
|
|
6
|
+
import { Skill, SkillContext } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Filter an array of skills by context.
|
|
9
|
+
*
|
|
10
|
+
* @param skills - Array of skills to filter
|
|
11
|
+
* @param context - Context to filter against
|
|
12
|
+
* @returns Filtered and sorted array of skills
|
|
13
|
+
*/
|
|
14
|
+
export declare function filterSkills(skills: Skill[], context: SkillContext): Skill[];
|
|
15
|
+
/**
|
|
16
|
+
* Check if a skill matches the given context.
|
|
17
|
+
*
|
|
18
|
+
* A skill is included if ALL of the following are true:
|
|
19
|
+
* 1. Target matches (if skill has targets)
|
|
20
|
+
* 2. Version satisfies constraint (if skill has version)
|
|
21
|
+
* 3. At least one required field type is detected (if skill has requires)
|
|
22
|
+
* 4. Has at least one required tag (if context has requiredTags)
|
|
23
|
+
* 5. Has none of the excluded tags (if context has excludedTags)
|
|
24
|
+
*/
|
|
25
|
+
export declare function matchesContext(skill: Skill, context: SkillContext): boolean;
|
|
26
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,KAAK,EAAE,CAc5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CA6B3E"}
|
package/dist/filter.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @magnolia/skill-loader - Filtering Logic
|
|
3
|
+
*
|
|
4
|
+
* Implements skill filtering based on context (target, version, requires, tags).
|
|
5
|
+
*/
|
|
6
|
+
import { satisfies, valid, coerce } from 'semver';
|
|
7
|
+
/**
|
|
8
|
+
* Filter an array of skills by context.
|
|
9
|
+
*
|
|
10
|
+
* @param skills - Array of skills to filter
|
|
11
|
+
* @param context - Context to filter against
|
|
12
|
+
* @returns Filtered and sorted array of skills
|
|
13
|
+
*/
|
|
14
|
+
export function filterSkills(skills, context) {
|
|
15
|
+
const filtered = skills.filter((skill) => matchesContext(skill, context));
|
|
16
|
+
// Sort by priority (higher first), then by name for stability
|
|
17
|
+
return filtered.sort((a, b) => {
|
|
18
|
+
const priorityA = a.metadata.priority ?? 0;
|
|
19
|
+
const priorityB = b.metadata.priority ?? 0;
|
|
20
|
+
if (priorityA !== priorityB) {
|
|
21
|
+
return priorityB - priorityA; // Higher priority first
|
|
22
|
+
}
|
|
23
|
+
return a.name.localeCompare(b.name);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if a skill matches the given context.
|
|
28
|
+
*
|
|
29
|
+
* A skill is included if ALL of the following are true:
|
|
30
|
+
* 1. Target matches (if skill has targets)
|
|
31
|
+
* 2. Version satisfies constraint (if skill has version)
|
|
32
|
+
* 3. At least one required field type is detected (if skill has requires)
|
|
33
|
+
* 4. Has at least one required tag (if context has requiredTags)
|
|
34
|
+
* 5. Has none of the excluded tags (if context has excludedTags)
|
|
35
|
+
*/
|
|
36
|
+
export function matchesContext(skill, context) {
|
|
37
|
+
const { metadata } = skill;
|
|
38
|
+
// Check target matching
|
|
39
|
+
if (!matchesTarget(metadata.targets, context.target)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
// Check version matching
|
|
43
|
+
if (!matchesVersion(metadata.version, context.version)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// Check requires matching
|
|
47
|
+
if (!matchesRequires(metadata.requires, context.detectedFieldTypes)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Check required tags
|
|
51
|
+
if (!matchesRequiredTags(metadata.tags, context.requiredTags)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// Check excluded tags
|
|
55
|
+
if (!matchesExcludedTags(metadata.tags, context.excludedTags)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if skill targets match context target.
|
|
62
|
+
* Skills with no targets are included for all targets.
|
|
63
|
+
*/
|
|
64
|
+
function matchesTarget(skillTargets, contextTarget) {
|
|
65
|
+
// No targets on skill = universal skill
|
|
66
|
+
if (!skillTargets || skillTargets.length === 0) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// No target in context = include all skills
|
|
70
|
+
if (!contextTarget) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// Check if context target is in skill targets (case-insensitive)
|
|
74
|
+
const normalizedContextTarget = contextTarget.toLowerCase();
|
|
75
|
+
return skillTargets.some((target) => target.toLowerCase() === normalizedContextTarget);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check if context version satisfies skill version constraint.
|
|
79
|
+
* Skills with no version constraint are included for all versions.
|
|
80
|
+
*/
|
|
81
|
+
function matchesVersion(skillVersion, contextVersion) {
|
|
82
|
+
// No version constraint on skill = include for all versions
|
|
83
|
+
if (!skillVersion) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
// No version in context = include skill (can't check constraint)
|
|
87
|
+
if (!contextVersion) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Try to coerce the context version to a valid semver
|
|
91
|
+
const coercedVersion = coerce(contextVersion);
|
|
92
|
+
if (!coercedVersion) {
|
|
93
|
+
// Can't parse context version, include the skill
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
// Check if version is a valid semver (might just be a simple string like "6.3")
|
|
97
|
+
const validVersion = valid(coercedVersion);
|
|
98
|
+
if (!validVersion) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return satisfies(validVersion, skillVersion);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Invalid semver range, include the skill
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if at least one required field type is detected.
|
|
111
|
+
* Skills with no requires are always included.
|
|
112
|
+
*/
|
|
113
|
+
function matchesRequires(skillRequires, detectedFieldTypes) {
|
|
114
|
+
// No requires on skill = always include
|
|
115
|
+
if (!skillRequires || skillRequires.length === 0) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// No detected field types = exclude skills with requires
|
|
119
|
+
if (!detectedFieldTypes || detectedFieldTypes.length === 0) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// Check if at least one required field type is detected
|
|
123
|
+
const normalizedDetected = new Set(detectedFieldTypes.map((f) => f.toLowerCase()));
|
|
124
|
+
return skillRequires.some((required) => normalizedDetected.has(required.toLowerCase()));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if skill has at least one of the required tags.
|
|
128
|
+
* If no required tags in context, all skills pass.
|
|
129
|
+
*/
|
|
130
|
+
function matchesRequiredTags(skillTags, requiredTags) {
|
|
131
|
+
// No required tags in context = all skills pass
|
|
132
|
+
if (!requiredTags || requiredTags.length === 0) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
// Required tags but skill has no tags = fail
|
|
136
|
+
if (!skillTags || skillTags.length === 0) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
// Check if skill has at least one required tag
|
|
140
|
+
const normalizedSkillTags = new Set(skillTags.map((t) => t.toLowerCase()));
|
|
141
|
+
return requiredTags.some((tag) => normalizedSkillTags.has(tag.toLowerCase()));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if skill has none of the excluded tags.
|
|
145
|
+
* If no excluded tags in context, all skills pass.
|
|
146
|
+
*/
|
|
147
|
+
function matchesExcludedTags(skillTags, excludedTags) {
|
|
148
|
+
// No excluded tags in context = all skills pass
|
|
149
|
+
if (!excludedTags || excludedTags.length === 0) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
// No tags on skill = can't match excluded tags, so pass
|
|
153
|
+
if (!skillTags || skillTags.length === 0) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// Check if skill has any excluded tag (if so, fail)
|
|
157
|
+
const normalizedSkillTags = new Set(skillTags.map((t) => t.toLowerCase()));
|
|
158
|
+
return !excludedTags.some((tag) => normalizedSkillTags.has(tag.toLowerCase()));
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @magnolia/skill-loader
|
|
3
|
+
*
|
|
4
|
+
* A Node.js library for loading, filtering, and managing AI prompt skills.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { SkillLoader, createSkill } from '@magnolia/skill-loader';
|
|
9
|
+
*
|
|
10
|
+
* const loader = new SkillLoader()
|
|
11
|
+
* .addDirectory('./skills');
|
|
12
|
+
*
|
|
13
|
+
* const skills = await loader.load({
|
|
14
|
+
* target: 'freemarker',
|
|
15
|
+
* version: '6.3.0',
|
|
16
|
+
* detectedFieldTypes: ['damLinkField'],
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const prompt = await loader.getPrompt({ target: 'freemarker' });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
export { SkillLoader, loadSkills } from './loader.js';
|
|
25
|
+
export { SkillManager } from './manager.js';
|
|
26
|
+
export type { Skill, SkillMetadata, SkillContext, SkillSource, SkillLoaderOptions, PromptOptions, } from './types.js';
|
|
27
|
+
export type { SkillManagerOptions, SkillDirectory, SkillInfo, SkillManagerLogger, } from './manager.js';
|
|
28
|
+
export { SkillLoaderError } from './types.js';
|
|
29
|
+
export { parseSkillFile, createSkill } from './parser.js';
|
|
30
|
+
export { filterSkills, matchesContext } from './filter.js';
|
|
31
|
+
export { loadSkillsFromDirectory } from './sources/directory.js';
|
|
32
|
+
export { loadSkillFromFile } from './sources/file.js';
|
|
33
|
+
export { loadSkillsFromUrl } from './sources/url.js';
|
|
34
|
+
export { processInlineSkills } from './sources/inline.js';
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,YAAY,EACV,KAAK,EACL,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,SAAS,EACT,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @magnolia/skill-loader
|
|
3
|
+
*
|
|
4
|
+
* A Node.js library for loading, filtering, and managing AI prompt skills.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { SkillLoader, createSkill } from '@magnolia/skill-loader';
|
|
9
|
+
*
|
|
10
|
+
* const loader = new SkillLoader()
|
|
11
|
+
* .addDirectory('./skills');
|
|
12
|
+
*
|
|
13
|
+
* const skills = await loader.load({
|
|
14
|
+
* target: 'freemarker',
|
|
15
|
+
* version: '6.3.0',
|
|
16
|
+
* detectedFieldTypes: ['damLinkField'],
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const prompt = await loader.getPrompt({ target: 'freemarker' });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
// Main classes
|
|
25
|
+
export { SkillLoader, loadSkills } from './loader.js';
|
|
26
|
+
export { SkillManager } from './manager.js';
|
|
27
|
+
// Error class (not a type-only export)
|
|
28
|
+
export { SkillLoaderError } from './types.js';
|
|
29
|
+
// Parser functions
|
|
30
|
+
export { parseSkillFile, createSkill } from './parser.js';
|
|
31
|
+
// Filter functions
|
|
32
|
+
export { filterSkills, matchesContext } from './filter.js';
|
|
33
|
+
// Source loaders (for advanced usage)
|
|
34
|
+
export { loadSkillsFromDirectory } from './sources/directory.js';
|
|
35
|
+
export { loadSkillFromFile } from './sources/file.js';
|
|
36
|
+
export { loadSkillsFromUrl } from './sources/url.js';
|
|
37
|
+
export { processInlineSkills } from './sources/inline.js';
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,eAAe;AACf,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAmB5C,uCAAuC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,mBAAmB;AACnB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D,mBAAmB;AACnB,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3D,sCAAsC;AACtC,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|