@looplia/looplia-cli 0.6.10 → 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-DN3RSIIJ.js +595 -0
- package/dist/chunk-HSZZVXV5.js +24154 -0
- package/dist/chunk-NUQVUYOZ.js +379 -0
- package/dist/chunk-VRBGWKZ6.js +54 -0
- package/dist/chunk-Y55L47HC.js +61 -0
- package/dist/claude-agent-sdk-BKJ5OHH6.js +68 -0
- package/dist/cli.js +32242 -29548
- package/dist/compiler-4VFX7JAN-K3XYU5VB.js +36 -0
- package/dist/devtools-MCPGSUKV.js +3713 -0
- package/dist/dist-5SEP7KKQ.js +65 -0
- 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 +175 -0
- package/plugins/looplia-core/skills/search/SKILL.md +1 -1
- package/plugins/looplia-core/skills/skill-capability-matcher/SKILL.md +76 -8
- package/plugins/looplia-core/skills/workflow-schema-composer/SKILL.md +27 -3
- 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 -256
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
init_esm_shims
|
|
4
|
+
} from "./chunk-Y55L47HC.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/sandbox.ts
|
|
7
|
+
init_esm_shims();
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
import { mkdirSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
var SANDBOX_DIRS = {
|
|
12
|
+
INPUTS: "inputs",
|
|
13
|
+
OUTPUTS: "outputs",
|
|
14
|
+
LOGS: "logs"
|
|
15
|
+
};
|
|
16
|
+
function generateRandomSuffix() {
|
|
17
|
+
return randomBytes(2).toString("hex");
|
|
18
|
+
}
|
|
19
|
+
function generateSlug(input, maxLength = 30) {
|
|
20
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, maxLength);
|
|
21
|
+
}
|
|
22
|
+
function generateSandboxId(slug) {
|
|
23
|
+
const normalizedSlug = generateSlug(slug);
|
|
24
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
25
|
+
const suffix = generateRandomSuffix();
|
|
26
|
+
return `${normalizedSlug}-${date}-${suffix}`;
|
|
27
|
+
}
|
|
28
|
+
function createSandboxDirectories(workspace, sandboxId) {
|
|
29
|
+
const sandboxDir = join(workspace, "sandbox", sandboxId);
|
|
30
|
+
mkdirSync(join(sandboxDir, SANDBOX_DIRS.INPUTS), { recursive: true });
|
|
31
|
+
mkdirSync(join(sandboxDir, SANDBOX_DIRS.OUTPUTS), { recursive: true });
|
|
32
|
+
mkdirSync(join(sandboxDir, SANDBOX_DIRS.LOGS), { recursive: true });
|
|
33
|
+
return sandboxDir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
SANDBOX_DIRS,
|
|
38
|
+
generateRandomSuffix,
|
|
39
|
+
generateSlug,
|
|
40
|
+
generateSandboxId,
|
|
41
|
+
createSandboxDirectories
|
|
42
|
+
};
|
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
pathExists
|
|
4
|
+
} from "./chunk-VRBGWKZ6.js";
|
|
5
|
+
import {
|
|
6
|
+
init_esm_shims
|
|
7
|
+
} from "./chunk-Y55L47HC.js";
|
|
8
|
+
|
|
9
|
+
// ../../packages/provider/dist/chunk-M7EZUTK7.js
|
|
10
|
+
init_esm_shims();
|
|
11
|
+
import { exec } from "child_process";
|
|
12
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { promisify } from "util";
|
|
16
|
+
function createProgress() {
|
|
17
|
+
let currentMessage = "";
|
|
18
|
+
let isActive = false;
|
|
19
|
+
return {
|
|
20
|
+
start(message) {
|
|
21
|
+
currentMessage = message;
|
|
22
|
+
isActive = true;
|
|
23
|
+
process.stdout.write(`\u23F3 ${message}...`);
|
|
24
|
+
},
|
|
25
|
+
succeed(message) {
|
|
26
|
+
if (!isActive) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
process.stdout.write("\r\x1B[K");
|
|
30
|
+
console.log(`\u2713 ${message ?? currentMessage}`);
|
|
31
|
+
isActive = false;
|
|
32
|
+
},
|
|
33
|
+
warn(message) {
|
|
34
|
+
if (!isActive) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
process.stdout.write("\r\x1B[K");
|
|
38
|
+
console.log(`\u26A0 ${message ?? currentMessage}`);
|
|
39
|
+
isActive = false;
|
|
40
|
+
},
|
|
41
|
+
fail(message) {
|
|
42
|
+
if (!isActive) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
process.stdout.write("\r\x1B[K");
|
|
46
|
+
console.log(`\u2717 ${message ?? currentMessage}`);
|
|
47
|
+
isActive = false;
|
|
48
|
+
},
|
|
49
|
+
update(message) {
|
|
50
|
+
if (!isActive) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
currentMessage = message;
|
|
54
|
+
process.stdout.write(`\r\x1B[K\u23F3 ${message}...`);
|
|
55
|
+
},
|
|
56
|
+
stop() {
|
|
57
|
+
if (!isActive) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
process.stdout.write("\r\x1B[K");
|
|
61
|
+
isActive = false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
var CAPABILITY_PATTERNS = [
|
|
66
|
+
{ pattern: /media|video|audio|image/, capability: "media-processing" },
|
|
67
|
+
{ pattern: /content|text|document/, capability: "content-analysis" },
|
|
68
|
+
{ pattern: /json|schema|structured/, capability: "structured-output" },
|
|
69
|
+
{ pattern: /workflow|orchestrat/, capability: "workflow-management" },
|
|
70
|
+
{ pattern: /search|find|discover/, capability: "search" },
|
|
71
|
+
{ pattern: /generat|creat|produc/, capability: "generation" },
|
|
72
|
+
{ pattern: /valid|check|verify/, capability: "validation" }
|
|
73
|
+
];
|
|
74
|
+
var FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
|
|
75
|
+
var PROTOCOL_REGEX = /^https?:\/\//;
|
|
76
|
+
var TRAILING_SLASH_REGEX = /\/$/;
|
|
77
|
+
var LEADING_DOT_SLASH_REGEX = /^\.\//;
|
|
78
|
+
function parseMultilineValue(lines, startIndex) {
|
|
79
|
+
const multilineLines = [];
|
|
80
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
81
|
+
const nextLine = lines[j];
|
|
82
|
+
if (nextLine === void 0) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (nextLine.startsWith(" ")) {
|
|
86
|
+
multilineLines.push(nextLine.trim());
|
|
87
|
+
} else if (nextLine.trim() !== "") {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return multilineLines.join(" ");
|
|
92
|
+
}
|
|
93
|
+
function parseYamlFrontmatter(frontmatter) {
|
|
94
|
+
const lines = frontmatter.split("\n");
|
|
95
|
+
const metadata = {};
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
if (line === void 0) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const colonIndex = line.indexOf(":");
|
|
102
|
+
if (colonIndex <= 0) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const key = line.slice(0, colonIndex).trim();
|
|
106
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
107
|
+
if (value === "|") {
|
|
108
|
+
value = parseMultilineValue(lines, i + 1);
|
|
109
|
+
}
|
|
110
|
+
metadata[key] = value;
|
|
111
|
+
}
|
|
112
|
+
return metadata;
|
|
113
|
+
}
|
|
114
|
+
function inferCategory(name, description) {
|
|
115
|
+
const text = `${name} ${description}`.toLowerCase();
|
|
116
|
+
if (text.includes("review") || text.includes("analyze") || text.includes("scan")) {
|
|
117
|
+
return "analysis";
|
|
118
|
+
}
|
|
119
|
+
if (text.includes("generate") || text.includes("synthesis") || text.includes("create")) {
|
|
120
|
+
return "generation";
|
|
121
|
+
}
|
|
122
|
+
if (text.includes("assemble") || text.includes("document") || text.includes("compile")) {
|
|
123
|
+
return "assembly";
|
|
124
|
+
}
|
|
125
|
+
if (text.includes("validate") || text.includes("check")) {
|
|
126
|
+
return "validation";
|
|
127
|
+
}
|
|
128
|
+
if (text.includes("search") || text.includes("find")) {
|
|
129
|
+
return "search";
|
|
130
|
+
}
|
|
131
|
+
if (text.includes("workflow") || text.includes("execute") || text.includes("orchestrat")) {
|
|
132
|
+
return "orchestration";
|
|
133
|
+
}
|
|
134
|
+
return "utility";
|
|
135
|
+
}
|
|
136
|
+
function inferCapabilities(description) {
|
|
137
|
+
const capabilities = [];
|
|
138
|
+
const text = description.toLowerCase();
|
|
139
|
+
for (const { pattern, capability } of CAPABILITY_PATTERNS) {
|
|
140
|
+
if (pattern.test(text)) {
|
|
141
|
+
capabilities.push(capability);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return capabilities;
|
|
145
|
+
}
|
|
146
|
+
function formatTitle(name) {
|
|
147
|
+
return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
148
|
+
}
|
|
149
|
+
var execAsync = promisify(exec);
|
|
150
|
+
var REGISTRY_VERSION = "1.0.0";
|
|
151
|
+
var DEFAULT_MARKETPLACE_SOURCES = [
|
|
152
|
+
{
|
|
153
|
+
name: "looplia-skills",
|
|
154
|
+
url: "https://github.com/memorysaver/looplia-skills",
|
|
155
|
+
description: "Looplia curated skills collection"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "anthropic-skills",
|
|
159
|
+
url: "https://github.com/anthropics/skills",
|
|
160
|
+
description: "Official Anthropic skills - xlsx, pdf, pptx, docx, frontend-design, and more"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "awesome-claude-skills",
|
|
164
|
+
url: "https://github.com/ComposioHQ/awesome-claude-skills",
|
|
165
|
+
description: "Community-curated Claude skills collection by ComposioHQ"
|
|
166
|
+
}
|
|
167
|
+
];
|
|
168
|
+
function getLoopliaHome() {
|
|
169
|
+
return process.env.LOOPLIA_HOME ?? join(homedir(), ".looplia");
|
|
170
|
+
}
|
|
171
|
+
function getRegistryPath() {
|
|
172
|
+
return join(getLoopliaHome(), "registry");
|
|
173
|
+
}
|
|
174
|
+
function getCompiledRegistryPath() {
|
|
175
|
+
return join(getRegistryPath(), "skill-catalog.json");
|
|
176
|
+
}
|
|
177
|
+
var getSkillCatalogPath = getCompiledRegistryPath;
|
|
178
|
+
function getSourcesPath() {
|
|
179
|
+
return join(getRegistryPath(), "sources.json");
|
|
180
|
+
}
|
|
181
|
+
async function initializeRegistry(force = false) {
|
|
182
|
+
const registryPath = getRegistryPath();
|
|
183
|
+
const sourcesPath = getSourcesPath();
|
|
184
|
+
await mkdir(registryPath, { recursive: true });
|
|
185
|
+
await mkdir(join(registryPath, "cache"), { recursive: true });
|
|
186
|
+
if (force || !await pathExists(sourcesPath)) {
|
|
187
|
+
const defaultSources = DEFAULT_MARKETPLACE_SOURCES.map(
|
|
188
|
+
(source, index) => ({
|
|
189
|
+
id: `github:${source.url.replace("https://github.com/", "")}`,
|
|
190
|
+
type: "github",
|
|
191
|
+
url: source.url,
|
|
192
|
+
enabled: true,
|
|
193
|
+
priority: 90 - index * 10,
|
|
194
|
+
// First source gets highest priority
|
|
195
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
await writeFile(sourcesPath, JSON.stringify(defaultSources, null, 2));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function loadSources() {
|
|
202
|
+
const sourcesPath = getSourcesPath();
|
|
203
|
+
if (!await pathExists(sourcesPath)) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const content = await readFile(sourcesPath, "utf-8");
|
|
207
|
+
return JSON.parse(content);
|
|
208
|
+
}
|
|
209
|
+
async function saveSources(sources) {
|
|
210
|
+
const sourcesPath = getSourcesPath();
|
|
211
|
+
await writeFile(sourcesPath, JSON.stringify(sources, null, 2));
|
|
212
|
+
}
|
|
213
|
+
async function addSource(url, type = "github") {
|
|
214
|
+
const sources = await loadSources();
|
|
215
|
+
const normalizedPath = url.replace(PROTOCOL_REGEX, "").replace("github.com/", "").replace(TRAILING_SLASH_REGEX, "");
|
|
216
|
+
const id = type === "github" ? `github:${normalizedPath}` : `local:${url}`;
|
|
217
|
+
if (sources.some((s) => s.id === id)) {
|
|
218
|
+
throw new Error(`Source already exists: ${id}`);
|
|
219
|
+
}
|
|
220
|
+
const newSource = {
|
|
221
|
+
id,
|
|
222
|
+
type,
|
|
223
|
+
url,
|
|
224
|
+
enabled: true,
|
|
225
|
+
priority: 50,
|
|
226
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
227
|
+
};
|
|
228
|
+
sources.push(newSource);
|
|
229
|
+
await saveSources(sources);
|
|
230
|
+
return newSource;
|
|
231
|
+
}
|
|
232
|
+
async function removeSource(sourceId) {
|
|
233
|
+
const sources = await loadSources();
|
|
234
|
+
const filtered = sources.filter((s) => s.id !== sourceId);
|
|
235
|
+
if (filtered.length === sources.length) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
await saveSources(filtered);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
async function tryFetchMarketplace(repoPath) {
|
|
242
|
+
const url = `https://raw.githubusercontent.com/${repoPath}/main/.claude-plugin/marketplace.json`;
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetch(url);
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const marketplace = await response.json();
|
|
249
|
+
const items = [];
|
|
250
|
+
for (const plugin of marketplace.plugins) {
|
|
251
|
+
for (const skillPath of plugin.skills) {
|
|
252
|
+
const skillName = skillPath.split("/").pop() ?? skillPath;
|
|
253
|
+
items.push({
|
|
254
|
+
name: skillName,
|
|
255
|
+
type: "registry:skill",
|
|
256
|
+
title: formatTitle(skillName),
|
|
257
|
+
description: `Skill from ${plugin.name}`,
|
|
258
|
+
author: plugin.name,
|
|
259
|
+
plugin: plugin.name,
|
|
260
|
+
category: inferCategory(skillName, plugin.description),
|
|
261
|
+
capabilities: inferCapabilities(plugin.description),
|
|
262
|
+
downloadUrl: `https://github.com/${repoPath}`,
|
|
263
|
+
files: [],
|
|
264
|
+
// Store skillPath for JIT installation
|
|
265
|
+
skillPath: skillPath.replace(LEADING_DOT_SLASH_REGEX, "")
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
name: marketplace.name,
|
|
271
|
+
homepage: `https://github.com/${repoPath}`,
|
|
272
|
+
version: marketplace.version ?? "1.0.0",
|
|
273
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
274
|
+
items
|
|
275
|
+
};
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function tryFetchRegistryJson(repoPath) {
|
|
281
|
+
const url = `https://github.com/${repoPath}/releases/latest/download/registry.json`;
|
|
282
|
+
try {
|
|
283
|
+
const response = await fetch(url);
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return await response.json();
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function fetchRemoteRegistry(source) {
|
|
293
|
+
const repoPath = source.url.replace(PROTOCOL_REGEX, "").replace("github.com/", "").replace(TRAILING_SLASH_REGEX, "");
|
|
294
|
+
const marketplaceManifest = await tryFetchMarketplace(repoPath);
|
|
295
|
+
if (marketplaceManifest) {
|
|
296
|
+
return { manifest: marketplaceManifest, format: "marketplace" };
|
|
297
|
+
}
|
|
298
|
+
const registryManifest = await tryFetchRegistryJson(repoPath);
|
|
299
|
+
if (registryManifest) {
|
|
300
|
+
return { manifest: registryManifest, format: "registry" };
|
|
301
|
+
}
|
|
302
|
+
console.warn(
|
|
303
|
+
`No registry found for ${source.url} (tried marketplace.json and registry.json)`
|
|
304
|
+
);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
function buildSkillFromMetadata(metadata) {
|
|
308
|
+
const category = inferCategory(
|
|
309
|
+
metadata.name ?? "",
|
|
310
|
+
metadata.description ?? ""
|
|
311
|
+
);
|
|
312
|
+
const capabilities = inferCapabilities(metadata.description ?? "");
|
|
313
|
+
return {
|
|
314
|
+
name: metadata.name,
|
|
315
|
+
title: formatTitle(metadata.name ?? ""),
|
|
316
|
+
description: metadata.description ?? "",
|
|
317
|
+
category,
|
|
318
|
+
capabilities,
|
|
319
|
+
model: metadata.model,
|
|
320
|
+
inputless: metadata.inputless === "true",
|
|
321
|
+
tools: metadata.tools?.split(",").map((t) => t.trim())
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async function parseSkillMetadata(skillPath) {
|
|
325
|
+
const skillMdPath = join(skillPath, "SKILL.md");
|
|
326
|
+
if (!await pathExists(skillMdPath)) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
331
|
+
const frontmatterMatch = content.match(FRONTMATTER_REGEX);
|
|
332
|
+
if (!frontmatterMatch?.[1]) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const metadata = parseYamlFrontmatter(frontmatterMatch[1]);
|
|
336
|
+
return buildSkillFromMetadata(metadata);
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function getGitRemoteUrl(repoPath) {
|
|
342
|
+
const pluginJsonPath = join(repoPath, ".claude-plugin", "plugin.json");
|
|
343
|
+
try {
|
|
344
|
+
if (await pathExists(pluginJsonPath)) {
|
|
345
|
+
const content = await readFile(pluginJsonPath, "utf-8");
|
|
346
|
+
const pluginJson = JSON.parse(content);
|
|
347
|
+
if (pluginJson.source?.url) {
|
|
348
|
+
return pluginJson.source.url;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const { stdout } = await execAsync("git remote get-url origin", {
|
|
355
|
+
cwd: repoPath
|
|
356
|
+
});
|
|
357
|
+
return stdout.trim() || void 0;
|
|
358
|
+
} catch {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function scanPluginDirectory(pluginPath, pluginName, sourceType) {
|
|
363
|
+
const skills = [];
|
|
364
|
+
const skillsPath = join(pluginPath, "skills");
|
|
365
|
+
if (!await pathExists(skillsPath)) {
|
|
366
|
+
return skills;
|
|
367
|
+
}
|
|
368
|
+
const gitUrl = sourceType === "thirdparty" ? await getGitRemoteUrl(pluginPath) : void 0;
|
|
369
|
+
try {
|
|
370
|
+
const skillEntries = await readdir(skillsPath, { withFileTypes: true });
|
|
371
|
+
for (const skillEntry of skillEntries) {
|
|
372
|
+
if (!skillEntry.isDirectory()) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const skillPath = join(skillsPath, skillEntry.name);
|
|
376
|
+
const metadata = await parseSkillMetadata(skillPath);
|
|
377
|
+
if (metadata) {
|
|
378
|
+
skills.push({
|
|
379
|
+
name: metadata.name ?? skillEntry.name,
|
|
380
|
+
title: metadata.title ?? formatTitle(skillEntry.name),
|
|
381
|
+
description: metadata.description ?? "",
|
|
382
|
+
plugin: pluginName,
|
|
383
|
+
category: metadata.category ?? "utility",
|
|
384
|
+
capabilities: metadata.capabilities ?? [],
|
|
385
|
+
tools: metadata.tools,
|
|
386
|
+
model: metadata.model,
|
|
387
|
+
inputless: metadata.inputless,
|
|
388
|
+
source: "local",
|
|
389
|
+
sourceType,
|
|
390
|
+
installed: true,
|
|
391
|
+
installedPath: skillPath,
|
|
392
|
+
gitUrl
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
return skills;
|
|
399
|
+
}
|
|
400
|
+
async function scanLocalPlugins(loopliaPath) {
|
|
401
|
+
const skills = [];
|
|
402
|
+
try {
|
|
403
|
+
const entries = await readdir(loopliaPath, { withFileTypes: true });
|
|
404
|
+
const builtinDirs = entries.filter(
|
|
405
|
+
(e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "sandbox" && e.name !== "workflows" && e.name !== "registry" && e.name !== "plugins"
|
|
406
|
+
);
|
|
407
|
+
for (const pluginDir of builtinDirs) {
|
|
408
|
+
const pluginPath = join(loopliaPath, pluginDir.name);
|
|
409
|
+
const pluginSkills = await scanPluginDirectory(
|
|
410
|
+
pluginPath,
|
|
411
|
+
pluginDir.name,
|
|
412
|
+
"builtin"
|
|
413
|
+
);
|
|
414
|
+
skills.push(...pluginSkills);
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
const pluginsDir = join(loopliaPath, "plugins");
|
|
419
|
+
if (await pathExists(pluginsDir)) {
|
|
420
|
+
try {
|
|
421
|
+
const entries = await readdir(pluginsDir, { withFileTypes: true });
|
|
422
|
+
const thirdPartyDirs = entries.filter(
|
|
423
|
+
(e) => e.isDirectory() && !e.name.startsWith(".")
|
|
424
|
+
);
|
|
425
|
+
for (const pluginDir of thirdPartyDirs) {
|
|
426
|
+
const pluginPath = join(pluginsDir, pluginDir.name);
|
|
427
|
+
const pluginSkills = await scanPluginDirectory(
|
|
428
|
+
pluginPath,
|
|
429
|
+
pluginDir.name,
|
|
430
|
+
"thirdparty"
|
|
431
|
+
);
|
|
432
|
+
skills.push(...pluginSkills);
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return skills;
|
|
438
|
+
}
|
|
439
|
+
async function processLocalSource(source, seenSkills, allSkills, progress) {
|
|
440
|
+
progress?.start(`Scanning local source: ${source.id}`);
|
|
441
|
+
try {
|
|
442
|
+
const localPath = source.url.startsWith("~") ? source.url.replace("~", homedir()) : source.url;
|
|
443
|
+
if (!await pathExists(localPath)) {
|
|
444
|
+
progress?.fail(`Local source not found: ${source.url}`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const skills = await scanPluginDirectory(
|
|
448
|
+
localPath,
|
|
449
|
+
source.id,
|
|
450
|
+
"thirdparty"
|
|
451
|
+
);
|
|
452
|
+
let addedCount = 0;
|
|
453
|
+
for (const skill of skills) {
|
|
454
|
+
if (seenSkills.has(skill.name)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
skill.source = source.id;
|
|
458
|
+
allSkills.push(skill);
|
|
459
|
+
seenSkills.add(skill.name);
|
|
460
|
+
addedCount += 1;
|
|
461
|
+
}
|
|
462
|
+
progress?.succeed(`Scanned ${source.id}: ${addedCount} skills`);
|
|
463
|
+
} catch {
|
|
464
|
+
progress?.fail(`Failed to scan: ${source.id}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function processRemoteSource(options) {
|
|
468
|
+
const { source, seenSkills, allSkills, installedSkillsMap, progress } = options;
|
|
469
|
+
progress?.start(`Fetching registry: ${source.id}`);
|
|
470
|
+
const result = await fetchRemoteRegistry(source);
|
|
471
|
+
if (!result) {
|
|
472
|
+
progress?.fail(`Failed to fetch: ${source.id}`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const { manifest, format } = result;
|
|
476
|
+
let addedCount = 0;
|
|
477
|
+
for (const item of manifest.items) {
|
|
478
|
+
if (seenSkills.has(item.name)) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const installed = installedSkillsMap.get(item.name);
|
|
482
|
+
const skill = {
|
|
483
|
+
name: item.name,
|
|
484
|
+
title: item.title,
|
|
485
|
+
description: item.description,
|
|
486
|
+
plugin: item.plugin,
|
|
487
|
+
category: item.category,
|
|
488
|
+
capabilities: item.capabilities,
|
|
489
|
+
tools: item.tools,
|
|
490
|
+
model: item.model,
|
|
491
|
+
inputless: item.inputless,
|
|
492
|
+
source: source.id,
|
|
493
|
+
sourceType: "thirdparty",
|
|
494
|
+
installed: installed !== void 0,
|
|
495
|
+
installedPath: installed?.installedPath,
|
|
496
|
+
gitUrl: item.downloadUrl,
|
|
497
|
+
skillPath: item.skillPath,
|
|
498
|
+
// Set by marketplace format, undefined for registry.json
|
|
499
|
+
checksum: item.checksum,
|
|
500
|
+
dependencies: item.registryDependencies
|
|
501
|
+
};
|
|
502
|
+
allSkills.push(skill);
|
|
503
|
+
seenSkills.add(item.name);
|
|
504
|
+
addedCount += 1;
|
|
505
|
+
}
|
|
506
|
+
const formatLabel = format === "marketplace" ? "marketplace" : "registry";
|
|
507
|
+
progress?.succeed(
|
|
508
|
+
`Fetched ${source.id} (${formatLabel}): ${addedCount} skills`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
function buildRegistrySummary(skills) {
|
|
512
|
+
const byCategory = {
|
|
513
|
+
analysis: 0,
|
|
514
|
+
generation: 0,
|
|
515
|
+
assembly: 0,
|
|
516
|
+
validation: 0,
|
|
517
|
+
search: 0,
|
|
518
|
+
orchestration: 0,
|
|
519
|
+
utility: 0
|
|
520
|
+
};
|
|
521
|
+
const bySource = {};
|
|
522
|
+
for (const skill of skills) {
|
|
523
|
+
byCategory[skill.category] += 1;
|
|
524
|
+
bySource[skill.source] = (bySource[skill.source] ?? 0) + 1;
|
|
525
|
+
}
|
|
526
|
+
return { byCategory, bySource };
|
|
527
|
+
}
|
|
528
|
+
async function compileRegistry(options) {
|
|
529
|
+
const { showProgress = false, localOnly = true } = options ?? {};
|
|
530
|
+
const progress = showProgress ? createProgress() : null;
|
|
531
|
+
const loopliaPath = getLoopliaHome();
|
|
532
|
+
const sources = await loadSources();
|
|
533
|
+
progress?.start("Scanning local plugins");
|
|
534
|
+
const localSkills = await scanLocalPlugins(loopliaPath);
|
|
535
|
+
const installedSkillsMap = new Map(localSkills.map((s) => [s.name, s]));
|
|
536
|
+
progress?.succeed(`Found ${localSkills.length} local skills`);
|
|
537
|
+
const allSkills = [...localSkills];
|
|
538
|
+
const seenSkills = new Set(localSkills.map((s) => s.name));
|
|
539
|
+
const localSources = sources.filter((s) => s.enabled && s.type === "local");
|
|
540
|
+
for (const source of localSources) {
|
|
541
|
+
await processLocalSource(source, seenSkills, allSkills, progress);
|
|
542
|
+
}
|
|
543
|
+
if (!localOnly) {
|
|
544
|
+
const remoteSources = sources.filter((s) => s.enabled && s.type !== "local").sort((a, b) => b.priority - a.priority);
|
|
545
|
+
for (const source of remoteSources) {
|
|
546
|
+
await processRemoteSource({
|
|
547
|
+
source,
|
|
548
|
+
seenSkills,
|
|
549
|
+
allSkills,
|
|
550
|
+
installedSkillsMap,
|
|
551
|
+
progress
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const { byCategory, bySource } = buildRegistrySummary(allSkills);
|
|
556
|
+
const compiled = {
|
|
557
|
+
compiledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
558
|
+
version: REGISTRY_VERSION,
|
|
559
|
+
sources,
|
|
560
|
+
skills: allSkills,
|
|
561
|
+
summary: { totalSkills: allSkills.length, byCategory, bySource }
|
|
562
|
+
};
|
|
563
|
+
const registryPath = getRegistryPath();
|
|
564
|
+
await mkdir(registryPath, { recursive: true });
|
|
565
|
+
await writeFile(getCompiledRegistryPath(), JSON.stringify(compiled, null, 2));
|
|
566
|
+
return compiled;
|
|
567
|
+
}
|
|
568
|
+
function createEmptyManifest() {
|
|
569
|
+
return {
|
|
570
|
+
name: "looplia",
|
|
571
|
+
homepage: "https://github.com/memorysaver/looplia-core",
|
|
572
|
+
version: "0.7.1",
|
|
573
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
574
|
+
items: []
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export {
|
|
579
|
+
createProgress,
|
|
580
|
+
FRONTMATTER_REGEX,
|
|
581
|
+
PROTOCOL_REGEX,
|
|
582
|
+
TRAILING_SLASH_REGEX,
|
|
583
|
+
DEFAULT_MARKETPLACE_SOURCES,
|
|
584
|
+
getRegistryPath,
|
|
585
|
+
getCompiledRegistryPath,
|
|
586
|
+
getSkillCatalogPath,
|
|
587
|
+
getSourcesPath,
|
|
588
|
+
initializeRegistry,
|
|
589
|
+
loadSources,
|
|
590
|
+
saveSources,
|
|
591
|
+
addSource,
|
|
592
|
+
removeSource,
|
|
593
|
+
compileRegistry,
|
|
594
|
+
createEmptyManifest
|
|
595
|
+
};
|