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