@looplia/looplia-cli 0.7.0 → 0.7.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/dist/chunk-2TWYHMFD.js +1148 -0
- package/dist/chunk-5WPEFJ5J.js +42 -0
- package/dist/chunk-APZNHRV3.js +7 -0
- package/dist/{chunk-GN6D7YWI.js → chunk-DN3RSIIJ.js} +139 -124
- package/dist/{chunk-APETX7TD.js → chunk-HSZZVXV5.js} +2298 -8048
- package/dist/chunk-NUQVUYOZ.js +379 -0
- package/dist/{chunk-FCL2HRTX.js → chunk-VRBGWKZ6.js} +9 -19
- package/dist/chunk-Y55L47HC.js +61 -0
- package/dist/claude-agent-sdk-BKJ5OHH6.js +68 -0
- package/dist/cli.js +33319 -1750
- package/dist/{compiler-J4DARL4X-2TXNVYEY.js → compiler-4VFX7JAN-K3XYU5VB.js} +11 -3
- package/dist/devtools-MCPGSUKV.js +3713 -0
- package/dist/{dist-2ZDF6EID.js → dist-5SEP7KKQ.js} +16 -10
- package/dist/sandbox-HAMJNBZ6.js +16 -0
- package/dist/sync-MXQ4NJWI-KGAZYCPW.js +17 -0
- package/package.json +1 -1
- package/plugins/looplia-core/skills/registry-loader/SKILL.md +1 -1
- package/plugins/looplia-core/skills/search/SKILL.md +1 -1
- package/plugins/looplia-core/skills/skill-capability-matcher/SKILL.md +1 -1
- package/plugins/looplia-core/skills/workflow-schema-composer/SKILL.md +1 -1
- package/dist/chunk-IVTVHH75.js +0 -523
- package/dist/skill-installer-GJYXIKXE-VS4MWW3V.js +0 -18
- package/plugins/looplia-core/skills/plugin-registry-scanner/SKILL.md +0 -108
- package/plugins/looplia-core/skills/plugin-registry-scanner/scripts/scan-plugins.ts +0 -221
- package/plugins/looplia-core/skills/plugin-registry-scanner/test/scan-plugins.test.ts +0 -260
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plugin-registry-scanner
|
|
3
|
-
description: |
|
|
4
|
-
This skill should be used when the user wants to discover available looplia skills,
|
|
5
|
-
scan installed plugins, or list what capabilities are available. Use when someone says
|
|
6
|
-
"what looplia skills are installed", "list available skills", "scan plugins", "/build",
|
|
7
|
-
"what can looplia do", or "show me all looplia capabilities".
|
|
8
|
-
|
|
9
|
-
First step in looplia workflow building: scans plugins/*/skills/*/SKILL.md to build
|
|
10
|
-
a registry of available skills. Part of the skills-first architecture where one workflow
|
|
11
|
-
step invokes one skill-executor to orchestrate multiple skills.
|
|
12
|
-
tools: Bash, Read, Glob
|
|
13
|
-
model: claude-haiku-4-5-20251001
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
# Plugin Registry Scanner
|
|
17
|
-
|
|
18
|
-
Discover and catalog all skills from installed looplia plugins.
|
|
19
|
-
|
|
20
|
-
## Purpose
|
|
21
|
-
|
|
22
|
-
Scan the `plugins/*/skills/*/SKILL.md` directory structure to build a registry of available skills with their capabilities.
|
|
23
|
-
|
|
24
|
-
## Process
|
|
25
|
-
|
|
26
|
-
### 1. Scan Plugin Directories
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
bun plugins/looplia-core/skills/plugin-registry-scanner/scripts/scan-plugins.ts
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
This deterministic script returns a JSON registry of all discovered skills.
|
|
33
|
-
|
|
34
|
-
### 2. Parse the Registry
|
|
35
|
-
|
|
36
|
-
The script output contains:
|
|
37
|
-
|
|
38
|
-
```json
|
|
39
|
-
{
|
|
40
|
-
"plugins": [
|
|
41
|
-
{
|
|
42
|
-
"name": "looplia-writer",
|
|
43
|
-
"path": "plugins/looplia-writer",
|
|
44
|
-
"skills": [
|
|
45
|
-
{
|
|
46
|
-
"name": "media-reviewer",
|
|
47
|
-
"description": "Deep content analysis (structure, themes, narrative)",
|
|
48
|
-
"tools": ["Read", "Grep", "Glob"],
|
|
49
|
-
"model": "haiku",
|
|
50
|
-
"capabilities": ["content analysis", "theme extraction", "quote identification"]
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
|
-
"summary": {
|
|
56
|
-
"totalPlugins": 2,
|
|
57
|
-
"totalSkills": 7
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 3. Infer Capabilities (Optional LLM Step)
|
|
63
|
-
|
|
64
|
-
If capability inference is needed beyond what the script provides, analyze skill descriptions to extract:
|
|
65
|
-
- Input types handled (video, audio, text, etc.)
|
|
66
|
-
- Processing capabilities (analyze, transform, generate, etc.)
|
|
67
|
-
- Output formats (JSON, markdown, structured data)
|
|
68
|
-
|
|
69
|
-
## Output Schema
|
|
70
|
-
|
|
71
|
-
```json
|
|
72
|
-
{
|
|
73
|
-
"plugins": [
|
|
74
|
-
{
|
|
75
|
-
"name": "string",
|
|
76
|
-
"path": "string",
|
|
77
|
-
"skills": [
|
|
78
|
-
{
|
|
79
|
-
"name": "string",
|
|
80
|
-
"description": "string",
|
|
81
|
-
"tools": ["string"],
|
|
82
|
-
"model": "string",
|
|
83
|
-
"capabilities": ["string"]
|
|
84
|
-
}
|
|
85
|
-
]
|
|
86
|
-
}
|
|
87
|
-
],
|
|
88
|
-
"summary": {
|
|
89
|
-
"totalPlugins": "number",
|
|
90
|
-
"totalSkills": "number"
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Usage
|
|
96
|
-
|
|
97
|
-
This skill is typically invoked as the first step in workflow building:
|
|
98
|
-
|
|
99
|
-
1. Scan available plugins and skills
|
|
100
|
-
2. Pass registry to skill-capability-matcher
|
|
101
|
-
3. Use matched skills in workflow-schema-composer
|
|
102
|
-
|
|
103
|
-
## Notes
|
|
104
|
-
|
|
105
|
-
- The scan script is deterministic (no LLM tokens)
|
|
106
|
-
- Capabilities can be inferred from descriptions if not explicitly declared
|
|
107
|
-
- Skills without SKILL.md files are skipped
|
|
108
|
-
- Invalid frontmatter generates warnings but doesn't halt scanning
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Plugin Registry Scanner
|
|
4
|
-
*
|
|
5
|
-
* Deterministic script to scan installed plugins and catalog available skills.
|
|
6
|
-
* Outputs a JSON registry to stdout.
|
|
7
|
-
*
|
|
8
|
-
* Usage: bun plugins/looplia-core/skills/plugin-registry-scanner/scripts/scan-plugins.ts [plugins-dir]
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
12
|
-
import { basename, join } from "node:path";
|
|
13
|
-
import { parse as parseYaml } from "yaml";
|
|
14
|
-
|
|
15
|
-
// Top-level regex for frontmatter extraction
|
|
16
|
-
export const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
|
|
17
|
-
|
|
18
|
-
// Capability inference lookup table
|
|
19
|
-
export const CAPABILITY_PATTERNS: [string, string][] = [
|
|
20
|
-
["analy", "content analysis"],
|
|
21
|
-
["review", "content review"],
|
|
22
|
-
["theme", "theme extraction"],
|
|
23
|
-
["extract", "theme extraction"],
|
|
24
|
-
["quote", "quote identification"],
|
|
25
|
-
["generat", "content generation"],
|
|
26
|
-
["idea", "idea generation"],
|
|
27
|
-
["hook", "idea generation"],
|
|
28
|
-
["outline", "outline creation"],
|
|
29
|
-
["transform", "content transformation"],
|
|
30
|
-
["document", "structured output"],
|
|
31
|
-
["structur", "structured output"],
|
|
32
|
-
["assembl", "content assembly"],
|
|
33
|
-
["combin", "content assembly"],
|
|
34
|
-
["profile", "personalization"],
|
|
35
|
-
["user", "personalization"],
|
|
36
|
-
["valid", "validation"],
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
export type SkillInfo = {
|
|
40
|
-
name: string;
|
|
41
|
-
description: string;
|
|
42
|
-
tools?: string[];
|
|
43
|
-
model?: string;
|
|
44
|
-
capabilities: string[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type PluginInfo = {
|
|
48
|
-
name: string;
|
|
49
|
-
path: string;
|
|
50
|
-
skills: SkillInfo[];
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export type Registry = {
|
|
54
|
-
plugins: PluginInfo[];
|
|
55
|
-
summary: {
|
|
56
|
-
totalPlugins: number;
|
|
57
|
-
totalSkills: number;
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Extract YAML frontmatter from markdown content
|
|
63
|
-
*/
|
|
64
|
-
export function extractFrontmatter(
|
|
65
|
-
content: string
|
|
66
|
-
): Record<string, unknown> | null {
|
|
67
|
-
const match = content.match(FRONTMATTER_REGEX);
|
|
68
|
-
if (!match) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
return parseYaml(match[1]) as Record<string, unknown>;
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Infer capabilities from skill description using pattern matching
|
|
81
|
-
*/
|
|
82
|
-
export function inferCapabilities(description: string): string[] {
|
|
83
|
-
const descLower = description.toLowerCase();
|
|
84
|
-
const capabilities = new Set<string>();
|
|
85
|
-
|
|
86
|
-
for (const [pattern, capability] of CAPABILITY_PATTERNS) {
|
|
87
|
-
if (descLower.includes(pattern)) {
|
|
88
|
-
capabilities.add(capability);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (capabilities.size === 0) {
|
|
93
|
-
capabilities.add("general processing");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return [...capabilities];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Scan a single skill directory
|
|
101
|
-
*/
|
|
102
|
-
async function scanSkill(skillPath: string): Promise<SkillInfo | null> {
|
|
103
|
-
const skillMdPath = join(skillPath, "SKILL.md");
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const content = await readFile(skillMdPath, "utf-8");
|
|
107
|
-
const frontmatter = extractFrontmatter(content);
|
|
108
|
-
|
|
109
|
-
if (!(frontmatter?.name && frontmatter.description)) {
|
|
110
|
-
console.error(`Warning: Invalid frontmatter in ${skillMdPath}`);
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const description =
|
|
115
|
-
typeof frontmatter.description === "string"
|
|
116
|
-
? frontmatter.description.trim()
|
|
117
|
-
: String(frontmatter.description);
|
|
118
|
-
|
|
119
|
-
let tools: string[] | undefined;
|
|
120
|
-
if (Array.isArray(frontmatter.tools)) {
|
|
121
|
-
tools = frontmatter.tools.map(String);
|
|
122
|
-
} else if (typeof frontmatter.tools === "string") {
|
|
123
|
-
tools = frontmatter.tools.split(",").map((t: string) => t.trim());
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
name: String(frontmatter.name),
|
|
128
|
-
description,
|
|
129
|
-
tools,
|
|
130
|
-
model: frontmatter.model ? String(frontmatter.model) : undefined,
|
|
131
|
-
capabilities: inferCapabilities(description),
|
|
132
|
-
};
|
|
133
|
-
} catch {
|
|
134
|
-
// SKILL.md doesn't exist or can't be read
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Scan a single plugin directory
|
|
141
|
-
*/
|
|
142
|
-
async function scanPlugin(pluginPath: string): Promise<PluginInfo | null> {
|
|
143
|
-
const skillsDir = join(pluginPath, "skills");
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const skillsStat = await stat(skillsDir);
|
|
147
|
-
if (!skillsStat.isDirectory()) {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
} catch {
|
|
151
|
-
// No skills directory
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const skillDirs = await readdir(skillsDir);
|
|
156
|
-
const skills: SkillInfo[] = [];
|
|
157
|
-
|
|
158
|
-
for (const skillDir of skillDirs) {
|
|
159
|
-
const skillPath = join(skillsDir, skillDir);
|
|
160
|
-
const skillStat = await stat(skillPath);
|
|
161
|
-
|
|
162
|
-
if (skillStat.isDirectory()) {
|
|
163
|
-
const skill = await scanSkill(skillPath);
|
|
164
|
-
if (skill) {
|
|
165
|
-
skills.push(skill);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (skills.length === 0) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
name: basename(pluginPath),
|
|
176
|
-
path: pluginPath,
|
|
177
|
-
skills,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Main scanner function
|
|
183
|
-
*/
|
|
184
|
-
export async function scanPlugins(pluginsPath: string): Promise<Registry> {
|
|
185
|
-
const plugins: PluginInfo[] = [];
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const entries = await readdir(pluginsPath);
|
|
189
|
-
|
|
190
|
-
for (const entry of entries) {
|
|
191
|
-
const pluginPath = join(pluginsPath, entry);
|
|
192
|
-
const entryStat = await stat(pluginPath);
|
|
193
|
-
|
|
194
|
-
if (entryStat.isDirectory() && !entry.startsWith(".")) {
|
|
195
|
-
const plugin = await scanPlugin(pluginPath);
|
|
196
|
-
if (plugin) {
|
|
197
|
-
plugins.push(plugin);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.error(`Error scanning plugins directory: ${error}`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const totalSkills = plugins.reduce((sum, p) => sum + p.skills.length, 0);
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
plugins,
|
|
209
|
-
summary: {
|
|
210
|
-
totalPlugins: plugins.length,
|
|
211
|
-
totalSkills,
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Main execution (only when run directly, not when imported)
|
|
217
|
-
if (import.meta.main) {
|
|
218
|
-
const pluginsDir = process.argv[2] || "plugins";
|
|
219
|
-
const registry = await scanPlugins(pluginsDir);
|
|
220
|
-
console.log(JSON.stringify(registry, null, 2));
|
|
221
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
CAPABILITY_PATTERNS,
|
|
5
|
-
extractFrontmatter,
|
|
6
|
-
inferCapabilities,
|
|
7
|
-
scanPlugins,
|
|
8
|
-
} from "../scripts/scan-plugins";
|
|
9
|
-
|
|
10
|
-
// Use absolute path to ensure tests work from any directory
|
|
11
|
-
const PLUGINS_DIR = join(import.meta.dir, "../../../..");
|
|
12
|
-
|
|
13
|
-
describe("scan-plugins", () => {
|
|
14
|
-
describe("extractFrontmatter", () => {
|
|
15
|
-
it("should extract valid YAML frontmatter", () => {
|
|
16
|
-
const content = `---
|
|
17
|
-
name: test-skill
|
|
18
|
-
description: A test skill
|
|
19
|
-
tools: Read, Write
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
# Test Skill
|
|
23
|
-
|
|
24
|
-
This is the body.`;
|
|
25
|
-
|
|
26
|
-
const result = extractFrontmatter(content);
|
|
27
|
-
expect(result).not.toBeNull();
|
|
28
|
-
expect(result?.name).toBe("test-skill");
|
|
29
|
-
expect(result?.description).toBe("A test skill");
|
|
30
|
-
expect(result?.tools).toBe("Read, Write");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("should extract multiline description with pipe syntax", () => {
|
|
34
|
-
const content = `---
|
|
35
|
-
name: multi-line
|
|
36
|
-
description: |
|
|
37
|
-
This is a multiline
|
|
38
|
-
description with details.
|
|
39
|
-
model: claude-haiku-4-5-20251001
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
# Skill`;
|
|
43
|
-
|
|
44
|
-
const result = extractFrontmatter(content);
|
|
45
|
-
expect(result).not.toBeNull();
|
|
46
|
-
expect(result?.name).toBe("multi-line");
|
|
47
|
-
expect(result?.description).toContain("multiline");
|
|
48
|
-
expect(result?.model).toBe("claude-haiku-4-5-20251001");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("should return null for content without frontmatter", () => {
|
|
52
|
-
const content = `# Just a heading
|
|
53
|
-
|
|
54
|
-
No frontmatter here.`;
|
|
55
|
-
|
|
56
|
-
const result = extractFrontmatter(content);
|
|
57
|
-
expect(result).toBeNull();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("should return null for invalid YAML", () => {
|
|
61
|
-
const content = `---
|
|
62
|
-
name: test
|
|
63
|
-
invalid: yaml: syntax: here
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
# Test`;
|
|
67
|
-
|
|
68
|
-
const result = extractFrontmatter(content);
|
|
69
|
-
expect(result).toBeNull();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("should return null for unclosed frontmatter", () => {
|
|
73
|
-
const content = `---
|
|
74
|
-
name: unclosed
|
|
75
|
-
description: Missing closing delimiter
|
|
76
|
-
|
|
77
|
-
# Content`;
|
|
78
|
-
|
|
79
|
-
const result = extractFrontmatter(content);
|
|
80
|
-
expect(result).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("inferCapabilities", () => {
|
|
85
|
-
it("should infer content analysis capability", () => {
|
|
86
|
-
const result = inferCapabilities("Deep content analysis for videos");
|
|
87
|
-
expect(result).toContain("content analysis");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("should infer theme extraction capability", () => {
|
|
91
|
-
const result = inferCapabilities("Extract key themes from documents");
|
|
92
|
-
expect(result).toContain("theme extraction");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("should infer idea generation capability", () => {
|
|
96
|
-
const result = inferCapabilities("Generate creative ideas and hooks");
|
|
97
|
-
expect(result).toContain("idea generation");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("should infer multiple capabilities from description", () => {
|
|
101
|
-
const result = inferCapabilities(
|
|
102
|
-
"Analyze content, generate ideas, and create outlines"
|
|
103
|
-
);
|
|
104
|
-
expect(result).toContain("content analysis");
|
|
105
|
-
expect(result).toContain("idea generation");
|
|
106
|
-
expect(result).toContain("outline creation");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("should infer validation capability", () => {
|
|
110
|
-
const result = inferCapabilities("Validate workflow outputs");
|
|
111
|
-
expect(result).toContain("validation");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("should infer personalization capability", () => {
|
|
115
|
-
const result = inferCapabilities("Read user profile for context");
|
|
116
|
-
expect(result).toContain("personalization");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("should infer structured output capability", () => {
|
|
120
|
-
const result = inferCapabilities("Generate structured JSON documents");
|
|
121
|
-
expect(result).toContain("structured output");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("should infer content assembly capability", () => {
|
|
125
|
-
const result = inferCapabilities("Assemble and combine content pieces");
|
|
126
|
-
expect(result).toContain("content assembly");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("should return general processing for unknown descriptions", () => {
|
|
130
|
-
const result = inferCapabilities("Something completely different");
|
|
131
|
-
expect(result).toContain("general processing");
|
|
132
|
-
expect(result.length).toBe(1);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("should be case-insensitive", () => {
|
|
136
|
-
const result = inferCapabilities("ANALYZE CONTENT");
|
|
137
|
-
expect(result).toContain("content analysis");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("should not duplicate capabilities", () => {
|
|
141
|
-
// "analyze" and "analysis" should not create duplicate entries
|
|
142
|
-
const result = inferCapabilities("Analyze using analysis techniques");
|
|
143
|
-
const analysisCount = result.filter(
|
|
144
|
-
(c) => c === "content analysis"
|
|
145
|
-
).length;
|
|
146
|
-
expect(analysisCount).toBe(1);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe("CAPABILITY_PATTERNS", () => {
|
|
151
|
-
it("should have expected patterns defined", () => {
|
|
152
|
-
const patterns = CAPABILITY_PATTERNS.map(([p]) => p);
|
|
153
|
-
expect(patterns).toContain("analy");
|
|
154
|
-
expect(patterns).toContain("valid");
|
|
155
|
-
expect(patterns).toContain("generat");
|
|
156
|
-
expect(patterns).toContain("theme");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("should map patterns to capabilities", () => {
|
|
160
|
-
const patternMap = Object.fromEntries(CAPABILITY_PATTERNS);
|
|
161
|
-
expect(patternMap.analy).toBe("content analysis");
|
|
162
|
-
expect(patternMap.valid).toBe("validation");
|
|
163
|
-
expect(patternMap.outline).toBe("outline creation");
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe("scanPlugins", () => {
|
|
168
|
-
it("should discover looplia-core skills", async () => {
|
|
169
|
-
const result = await scanPlugins(PLUGINS_DIR);
|
|
170
|
-
const corePlugin = result.plugins.find((p) => p.name === "looplia-core");
|
|
171
|
-
|
|
172
|
-
expect(corePlugin).toBeDefined();
|
|
173
|
-
expect(corePlugin?.skills.length).toBeGreaterThan(0);
|
|
174
|
-
|
|
175
|
-
const skillNames = corePlugin?.skills.map((s) => s.name) ?? [];
|
|
176
|
-
expect(skillNames).toContain("plugin-registry-scanner");
|
|
177
|
-
expect(skillNames).toContain("workflow-executor");
|
|
178
|
-
expect(skillNames).toContain("skill-capability-matcher");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("should discover looplia-writer skills", async () => {
|
|
182
|
-
const result = await scanPlugins(PLUGINS_DIR);
|
|
183
|
-
const writerPlugin = result.plugins.find(
|
|
184
|
-
(p) => p.name === "looplia-writer"
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
expect(writerPlugin).toBeDefined();
|
|
188
|
-
expect(writerPlugin?.skills.length).toBeGreaterThan(0);
|
|
189
|
-
|
|
190
|
-
const skillNames = writerPlugin?.skills.map((s) => s.name) ?? [];
|
|
191
|
-
expect(skillNames).toContain("media-reviewer");
|
|
192
|
-
expect(skillNames).toContain("idea-synthesis");
|
|
193
|
-
expect(skillNames).toContain("writing-kit-assembler");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("should return valid registry JSON schema", async () => {
|
|
197
|
-
const result = await scanPlugins(PLUGINS_DIR);
|
|
198
|
-
|
|
199
|
-
// Check top-level structure
|
|
200
|
-
expect(result).toHaveProperty("plugins");
|
|
201
|
-
expect(result).toHaveProperty("summary");
|
|
202
|
-
expect(Array.isArray(result.plugins)).toBe(true);
|
|
203
|
-
|
|
204
|
-
// Check summary
|
|
205
|
-
expect(result.summary).toHaveProperty("totalPlugins");
|
|
206
|
-
expect(result.summary).toHaveProperty("totalSkills");
|
|
207
|
-
expect(typeof result.summary.totalPlugins).toBe("number");
|
|
208
|
-
expect(typeof result.summary.totalSkills).toBe("number");
|
|
209
|
-
|
|
210
|
-
// Check skill structure
|
|
211
|
-
const firstSkill = result.plugins[0]?.skills[0];
|
|
212
|
-
if (firstSkill) {
|
|
213
|
-
expect(firstSkill).toHaveProperty("name");
|
|
214
|
-
expect(firstSkill).toHaveProperty("description");
|
|
215
|
-
expect(firstSkill).toHaveProperty("capabilities");
|
|
216
|
-
expect(Array.isArray(firstSkill.capabilities)).toBe(true);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("should include tools and model when specified", async () => {
|
|
221
|
-
const result = await scanPlugins(PLUGINS_DIR);
|
|
222
|
-
const corePlugin = result.plugins.find((p) => p.name === "looplia-core");
|
|
223
|
-
const scanner = corePlugin?.skills.find(
|
|
224
|
-
(s) => s.name === "plugin-registry-scanner"
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
expect(scanner?.tools).toBeDefined();
|
|
228
|
-
expect(scanner?.tools).toContain("Bash");
|
|
229
|
-
expect(scanner?.model).toBe("claude-haiku-4-5-20251001");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("should handle non-existent plugins directory gracefully", async () => {
|
|
233
|
-
const result = await scanPlugins("non-existent-dir");
|
|
234
|
-
|
|
235
|
-
expect(result.plugins).toEqual([]);
|
|
236
|
-
expect(result.summary.totalPlugins).toBe(0);
|
|
237
|
-
expect(result.summary.totalSkills).toBe(0);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("should calculate correct totals", async () => {
|
|
241
|
-
const result = await scanPlugins(PLUGINS_DIR);
|
|
242
|
-
|
|
243
|
-
const calculatedTotal = result.plugins.reduce(
|
|
244
|
-
(sum, p) => sum + p.skills.length,
|
|
245
|
-
0
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
expect(result.summary.totalSkills).toBe(calculatedTotal);
|
|
249
|
-
expect(result.summary.totalPlugins).toBe(result.plugins.length);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("should discover exactly 15 skills after adding registry-loader", async () => {
|
|
253
|
-
const result = await scanPlugins(PLUGINS_DIR);
|
|
254
|
-
|
|
255
|
-
// After adding registry-loader skill, we should have all 15 skills
|
|
256
|
-
expect(result.summary.totalSkills).toBe(15);
|
|
257
|
-
expect(result.summary.totalPlugins).toBe(2);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
});
|