@looplia/looplia-cli 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- });