@toolr/seedr 0.1.2 → 0.1.3
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-LA6GTMJY.js → chunk-7GIETJRD.js} +224 -182
- package/dist/cli.js +789 -204
- package/dist/index.d.ts +27 -9
- package/dist/index.js +104 -3
- package/package.json +10 -3
|
@@ -102,77 +102,159 @@ function getItemSourcePath(item) {
|
|
|
102
102
|
import { homedir } from "os";
|
|
103
103
|
import { join as join2 } from "path";
|
|
104
104
|
var home = homedir();
|
|
105
|
-
var SKILL_FORMAT_MARKDOWN = "markdown";
|
|
106
|
-
var SKILL_EXTENSION_MD = ".md";
|
|
107
|
-
var COPILOT_PATH = ".github/copilot-instructions";
|
|
108
|
-
var OPENCODE_PATH = ".opencode/agents";
|
|
109
105
|
var AI_TOOLS = {
|
|
110
106
|
claude: {
|
|
111
107
|
name: "Claude Code",
|
|
112
108
|
shortName: "claude",
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
projectRoot: ".claude",
|
|
110
|
+
userRoot: join2(home, ".claude"),
|
|
111
|
+
contentTypes: {
|
|
112
|
+
skill: {
|
|
113
|
+
path: "skills",
|
|
114
|
+
extension: ".md",
|
|
115
|
+
structure: "directory",
|
|
116
|
+
mainFile: "SKILL.md"
|
|
117
|
+
},
|
|
118
|
+
command: {
|
|
119
|
+
path: "commands",
|
|
120
|
+
extension: ".md",
|
|
121
|
+
structure: "directory",
|
|
122
|
+
mainFile: "COMMAND.md"
|
|
123
|
+
},
|
|
124
|
+
agent: {
|
|
125
|
+
path: "agents",
|
|
126
|
+
extension: ".md",
|
|
127
|
+
structure: "file"
|
|
128
|
+
},
|
|
129
|
+
hook: {
|
|
130
|
+
path: "",
|
|
131
|
+
extension: ".json",
|
|
132
|
+
structure: "json-merge",
|
|
133
|
+
mergeTarget: "settings.json",
|
|
134
|
+
mergeField: "hooks"
|
|
135
|
+
},
|
|
136
|
+
plugin: {
|
|
137
|
+
path: "plugins/cache",
|
|
138
|
+
extension: "",
|
|
139
|
+
structure: "plugin"
|
|
140
|
+
},
|
|
141
|
+
settings: {
|
|
142
|
+
path: "",
|
|
143
|
+
extension: ".json",
|
|
144
|
+
structure: "json-merge",
|
|
145
|
+
mergeTarget: "settings.json"
|
|
146
|
+
},
|
|
147
|
+
mcp: {
|
|
148
|
+
path: "",
|
|
149
|
+
extension: ".json",
|
|
150
|
+
structure: "json-merge",
|
|
151
|
+
mergeTarget: ".mcp.json",
|
|
152
|
+
mergeField: "mcpServers"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
119
155
|
},
|
|
120
156
|
copilot: {
|
|
121
157
|
name: "GitHub Copilot",
|
|
122
158
|
shortName: "copilot",
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
projectRoot: ".github",
|
|
160
|
+
userRoot: join2(home, ".github"),
|
|
161
|
+
contentTypes: {
|
|
162
|
+
skill: {
|
|
163
|
+
path: "skills",
|
|
164
|
+
extension: ".md",
|
|
165
|
+
structure: "directory",
|
|
166
|
+
mainFile: "SKILL.md"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
129
169
|
},
|
|
130
170
|
gemini: {
|
|
131
171
|
name: "Gemini Code Assist",
|
|
132
172
|
shortName: "gemini",
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
173
|
+
projectRoot: ".gemini",
|
|
174
|
+
userRoot: join2(home, ".gemini"),
|
|
175
|
+
contentTypes: {
|
|
176
|
+
skill: {
|
|
177
|
+
path: "skills",
|
|
178
|
+
extension: ".md",
|
|
179
|
+
structure: "directory",
|
|
180
|
+
mainFile: "SKILL.md"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
139
183
|
},
|
|
140
184
|
codex: {
|
|
141
|
-
name: "OpenAI Codex",
|
|
185
|
+
name: "OpenAI Codex CLI",
|
|
142
186
|
shortName: "codex",
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
187
|
+
projectRoot: ".codex",
|
|
188
|
+
userRoot: join2(home, ".codex"),
|
|
189
|
+
contentTypes: {
|
|
190
|
+
skill: {
|
|
191
|
+
path: "skills",
|
|
192
|
+
extension: ".md",
|
|
193
|
+
structure: "directory",
|
|
194
|
+
mainFile: "SKILL.md"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
149
197
|
},
|
|
150
198
|
opencode: {
|
|
151
199
|
name: "OpenCode",
|
|
152
200
|
shortName: "opencode",
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
201
|
+
projectRoot: ".opencode",
|
|
202
|
+
userRoot: join2(home, ".opencode"),
|
|
203
|
+
contentTypes: {
|
|
204
|
+
skill: {
|
|
205
|
+
path: "skills",
|
|
206
|
+
extension: ".md",
|
|
207
|
+
structure: "directory",
|
|
208
|
+
mainFile: "SKILL.md"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
159
211
|
}
|
|
160
212
|
};
|
|
161
213
|
var ALL_TOOLS = Object.keys(AI_TOOLS);
|
|
162
214
|
function getToolConfig(tool) {
|
|
163
215
|
return AI_TOOLS[tool];
|
|
164
216
|
}
|
|
165
|
-
function
|
|
217
|
+
function getContentTypeConfig(tool, type) {
|
|
218
|
+
return AI_TOOLS[tool].contentTypes[type];
|
|
219
|
+
}
|
|
220
|
+
function getToolRoot(tool, scope, cwd = process.cwd()) {
|
|
166
221
|
const config = AI_TOOLS[tool];
|
|
167
222
|
switch (scope) {
|
|
168
223
|
case "project":
|
|
169
|
-
|
|
224
|
+
case "local":
|
|
225
|
+
return join2(cwd, config.projectRoot);
|
|
170
226
|
case "user":
|
|
171
|
-
return config.
|
|
172
|
-
case "global":
|
|
173
|
-
return config.globalPath;
|
|
227
|
+
return config.userRoot;
|
|
174
228
|
}
|
|
175
229
|
}
|
|
230
|
+
function getContentPath(tool, type, scope, cwd = process.cwd()) {
|
|
231
|
+
const typeConfig = getContentTypeConfig(tool, type);
|
|
232
|
+
if (!typeConfig) return void 0;
|
|
233
|
+
const root = getToolRoot(tool, scope, cwd);
|
|
234
|
+
return typeConfig.path ? join2(root, typeConfig.path) : root;
|
|
235
|
+
}
|
|
236
|
+
function getSettingsPath(scope, cwd = process.cwd()) {
|
|
237
|
+
switch (scope) {
|
|
238
|
+
case "project":
|
|
239
|
+
return join2(cwd, ".claude/settings.json");
|
|
240
|
+
case "user":
|
|
241
|
+
return join2(home, ".claude/settings.json");
|
|
242
|
+
case "local":
|
|
243
|
+
return join2(cwd, ".claude/settings.local.json");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function getMcpPath(scope, cwd = process.cwd()) {
|
|
247
|
+
switch (scope) {
|
|
248
|
+
case "project":
|
|
249
|
+
case "local":
|
|
250
|
+
return join2(cwd, ".mcp.json");
|
|
251
|
+
case "user":
|
|
252
|
+
return join2(home, ".claude.json");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function getToolPath(tool, scope, cwd = process.cwd()) {
|
|
256
|
+
return getContentPath(tool, "skill", scope, cwd) || "";
|
|
257
|
+
}
|
|
176
258
|
|
|
177
259
|
// src/utils/fs.ts
|
|
178
260
|
import {
|
|
@@ -222,6 +304,26 @@ async function writeTextFile(path, content) {
|
|
|
222
304
|
await ensureDir(dirname2(path));
|
|
223
305
|
await writeFile(path, content, "utf-8");
|
|
224
306
|
}
|
|
307
|
+
function getAgentsPath(contentType, slug, cwd) {
|
|
308
|
+
return join3(cwd, ".agents", contentType + "s", slug);
|
|
309
|
+
}
|
|
310
|
+
async function copyDirectory(source, destination) {
|
|
311
|
+
const { cp } = await import("fs/promises");
|
|
312
|
+
await cp(source, destination, { recursive: true });
|
|
313
|
+
}
|
|
314
|
+
async function installDirectory(source, destination, method) {
|
|
315
|
+
await ensureDir(dirname2(destination));
|
|
316
|
+
if (await exists(destination)) {
|
|
317
|
+
const { rm } = await import("fs/promises");
|
|
318
|
+
await rm(destination, { recursive: true });
|
|
319
|
+
}
|
|
320
|
+
if (method === "symlink") {
|
|
321
|
+
const relPath = relative(dirname2(destination), source);
|
|
322
|
+
await symlink(relPath, destination);
|
|
323
|
+
} else {
|
|
324
|
+
await copyDirectory(source, destination);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
225
327
|
|
|
226
328
|
// src/utils/detection.ts
|
|
227
329
|
async function detectInstalledTools(cwd = process.cwd()) {
|
|
@@ -249,7 +351,8 @@ async function detectProjectTools(cwd = process.cwd()) {
|
|
|
249
351
|
return detected;
|
|
250
352
|
}
|
|
251
353
|
async function isToolInstalled(tool, scope, cwd = process.cwd()) {
|
|
252
|
-
const
|
|
354
|
+
const effectiveScope = scope === "local" ? "project" : scope;
|
|
355
|
+
const path = getToolPath(tool, effectiveScope, cwd);
|
|
253
356
|
return exists(path);
|
|
254
357
|
}
|
|
255
358
|
function parseToolArg(arg) {
|
|
@@ -277,143 +380,47 @@ function parseToolsArg(agents, allTools) {
|
|
|
277
380
|
return agents.split(",").map((t) => parseToolArg(t.trim())).filter((t) => t !== null);
|
|
278
381
|
}
|
|
279
382
|
|
|
280
|
-
// src/converters/index.ts
|
|
281
|
-
import matter from "gray-matter";
|
|
282
|
-
|
|
283
|
-
// src/converters/claude.ts
|
|
284
|
-
var claudeConverter = {
|
|
285
|
-
convert(skill) {
|
|
286
|
-
return skill.raw;
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
// src/converters/copilot.ts
|
|
291
|
-
var copilotConverter = {
|
|
292
|
-
convert(skill) {
|
|
293
|
-
const { frontmatter, body } = skill;
|
|
294
|
-
const lines = [];
|
|
295
|
-
if (frontmatter.name) {
|
|
296
|
-
lines.push(`# ${frontmatter.name}`, "");
|
|
297
|
-
}
|
|
298
|
-
if (frontmatter.description) {
|
|
299
|
-
lines.push(frontmatter.description, "");
|
|
300
|
-
}
|
|
301
|
-
lines.push(body);
|
|
302
|
-
return lines.join("\n");
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// src/converters/gemini.ts
|
|
307
|
-
var geminiConverter = {
|
|
308
|
-
convert(skill) {
|
|
309
|
-
const { frontmatter, body } = skill;
|
|
310
|
-
const lines = [];
|
|
311
|
-
if (frontmatter.name) {
|
|
312
|
-
lines.push(`# ${frontmatter.name}`, "");
|
|
313
|
-
}
|
|
314
|
-
if (frontmatter.description) {
|
|
315
|
-
lines.push(`> ${frontmatter.description}`, "");
|
|
316
|
-
}
|
|
317
|
-
lines.push("## Instructions", "", body);
|
|
318
|
-
return lines.join("\n");
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// src/converters/codex.ts
|
|
323
|
-
var codexConverter = {
|
|
324
|
-
convert(skill) {
|
|
325
|
-
const { frontmatter, body } = skill;
|
|
326
|
-
const lines = [];
|
|
327
|
-
lines.push("# Agent Instructions", "");
|
|
328
|
-
if (frontmatter.name) {
|
|
329
|
-
lines.push(`**Name:** ${frontmatter.name}`);
|
|
330
|
-
}
|
|
331
|
-
if (frontmatter.description) {
|
|
332
|
-
lines.push(`**Description:** ${frontmatter.description}`);
|
|
333
|
-
}
|
|
334
|
-
if (frontmatter.name || frontmatter.description) {
|
|
335
|
-
lines.push("");
|
|
336
|
-
}
|
|
337
|
-
lines.push("## Behavior", "", body);
|
|
338
|
-
return lines.join("\n");
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// src/converters/opencode.ts
|
|
343
|
-
var opencodeConverter = {
|
|
344
|
-
convert(skill) {
|
|
345
|
-
const { frontmatter, body } = skill;
|
|
346
|
-
const lines = [];
|
|
347
|
-
if (frontmatter.name) {
|
|
348
|
-
lines.push(`# ${frontmatter.name}`, "");
|
|
349
|
-
}
|
|
350
|
-
if (frontmatter.description) {
|
|
351
|
-
lines.push(frontmatter.description, "");
|
|
352
|
-
}
|
|
353
|
-
lines.push("---", "", body);
|
|
354
|
-
return lines.join("\n");
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
// src/converters/index.ts
|
|
359
|
-
var converters = {
|
|
360
|
-
claude: claudeConverter,
|
|
361
|
-
copilot: copilotConverter,
|
|
362
|
-
gemini: geminiConverter,
|
|
363
|
-
codex: codexConverter,
|
|
364
|
-
opencode: opencodeConverter
|
|
365
|
-
};
|
|
366
|
-
function parseSkillMarkdown(content) {
|
|
367
|
-
const { data, content: body } = matter(content);
|
|
368
|
-
return {
|
|
369
|
-
frontmatter: data,
|
|
370
|
-
body: body.trim(),
|
|
371
|
-
raw: content
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
function convertSkillToTool(content, targetTool) {
|
|
375
|
-
const converter = converters[targetTool];
|
|
376
|
-
if (!converter) {
|
|
377
|
-
return content;
|
|
378
|
-
}
|
|
379
|
-
const skill = parseSkillMarkdown(content);
|
|
380
|
-
return converter.convert(skill);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// src/utils/convert.ts
|
|
384
|
-
function getOutputFilename(slug, tool) {
|
|
385
|
-
const config = AI_TOOLS[tool];
|
|
386
|
-
return `${slug}${config.skillExtension}`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
383
|
// src/handlers/skill.ts
|
|
390
|
-
import { join as join4, dirname as dirname3 } from "path";
|
|
384
|
+
import { join as join4, relative as relative2, dirname as dirname3 } from "path";
|
|
385
|
+
import { readdir as readdir2, symlink as symlink2 } from "fs/promises";
|
|
391
386
|
import chalk from "chalk";
|
|
392
387
|
import ora from "ora";
|
|
393
|
-
async function
|
|
394
|
-
const
|
|
395
|
-
|
|
388
|
+
async function installToCentralLocation(item, sourcePath, cwd) {
|
|
389
|
+
const centralPath = getAgentsPath("skill", item.slug, cwd);
|
|
390
|
+
if (await exists(centralPath)) {
|
|
391
|
+
const { rm } = await import("fs/promises");
|
|
392
|
+
await rm(centralPath, { recursive: true });
|
|
393
|
+
}
|
|
394
|
+
await copyDirectory(sourcePath, centralPath);
|
|
395
|
+
return centralPath;
|
|
396
396
|
}
|
|
397
|
-
async function
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
await
|
|
402
|
-
} else {
|
|
403
|
-
await ensureDir(dirname3(destPath));
|
|
404
|
-
await writeTextFile(destPath, content);
|
|
397
|
+
async function createToolSymlink(centralPath, destPath) {
|
|
398
|
+
await ensureDir(dirname3(destPath));
|
|
399
|
+
if (await exists(destPath)) {
|
|
400
|
+
const { rm } = await import("fs/promises");
|
|
401
|
+
await rm(destPath, { recursive: true });
|
|
405
402
|
}
|
|
403
|
+
const relPath = relative2(dirname3(destPath), centralPath);
|
|
404
|
+
await symlink2(relPath, destPath);
|
|
406
405
|
}
|
|
407
|
-
async function installSkillForTool(item, tool, scope, method, cwd) {
|
|
406
|
+
async function installSkillForTool(item, tool, scope, method, cwd, centralPath) {
|
|
408
407
|
const spinner = ora(
|
|
409
408
|
`Installing ${item.name} for ${AI_TOOLS[tool].name}...`
|
|
410
409
|
).start();
|
|
411
410
|
try {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
411
|
+
const destDir = getContentPath(tool, "skill", scope, cwd);
|
|
412
|
+
if (!destDir) {
|
|
413
|
+
throw new Error(`${AI_TOOLS[tool].name} does not support skills`);
|
|
414
|
+
}
|
|
415
|
+
const destPath = join4(destDir, item.slug);
|
|
416
|
+
const sourcePath = getItemSourcePath(item);
|
|
417
|
+
if (method === "symlink" && centralPath) {
|
|
418
|
+
await createToolSymlink(centralPath, destPath);
|
|
419
|
+
} else if (sourcePath) {
|
|
420
|
+
await installDirectory(sourcePath, destPath, "copy");
|
|
421
|
+
} else {
|
|
422
|
+
throw new Error("External skill installation not yet implemented");
|
|
423
|
+
}
|
|
417
424
|
spinner.succeed(
|
|
418
425
|
chalk.green(`Installed ${item.name} for ${AI_TOOLS[tool].name}`)
|
|
419
426
|
);
|
|
@@ -428,31 +435,59 @@ async function installSkillForTool(item, tool, scope, method, cwd) {
|
|
|
428
435
|
}
|
|
429
436
|
async function installSkill(item, tools, scope, method, cwd = process.cwd()) {
|
|
430
437
|
const results = [];
|
|
438
|
+
const sourcePath = getItemSourcePath(item);
|
|
439
|
+
let centralPath;
|
|
440
|
+
if (method === "symlink" && sourcePath) {
|
|
441
|
+
centralPath = await installToCentralLocation(item, sourcePath, cwd);
|
|
442
|
+
}
|
|
431
443
|
for (const tool of tools) {
|
|
432
|
-
const result = await installSkillForTool(
|
|
444
|
+
const result = await installSkillForTool(
|
|
445
|
+
item,
|
|
446
|
+
tool,
|
|
447
|
+
scope,
|
|
448
|
+
method,
|
|
449
|
+
cwd,
|
|
450
|
+
centralPath
|
|
451
|
+
);
|
|
433
452
|
results.push(result);
|
|
434
453
|
}
|
|
435
454
|
return results;
|
|
436
455
|
}
|
|
437
456
|
async function uninstallSkill(slug, tool, scope, cwd = process.cwd()) {
|
|
438
|
-
const destDir =
|
|
439
|
-
|
|
440
|
-
const destPath = join4(destDir,
|
|
457
|
+
const destDir = getContentPath(tool, "skill", scope, cwd);
|
|
458
|
+
if (!destDir) return false;
|
|
459
|
+
const destPath = join4(destDir, slug);
|
|
441
460
|
if (!await exists(destPath)) {
|
|
442
461
|
return false;
|
|
443
462
|
}
|
|
444
|
-
|
|
463
|
+
const { rm } = await import("fs/promises");
|
|
464
|
+
try {
|
|
465
|
+
await rm(destPath, { recursive: true });
|
|
466
|
+
return true;
|
|
467
|
+
} catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
445
470
|
}
|
|
446
471
|
async function getInstalledSkills(tool, scope, cwd = process.cwd()) {
|
|
447
|
-
const destDir =
|
|
448
|
-
if (!await exists(destDir)) {
|
|
472
|
+
const destDir = getContentPath(tool, "skill", scope, cwd);
|
|
473
|
+
if (!destDir || !await exists(destDir)) {
|
|
449
474
|
return [];
|
|
450
475
|
}
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
const config = AI_TOOLS[tool];
|
|
454
|
-
return files.filter((f) => f.endsWith(config.skillExtension)).map((f) => f.replace(config.skillExtension, ""));
|
|
476
|
+
const entries = await readdir2(destDir, { withFileTypes: true });
|
|
477
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
455
478
|
}
|
|
479
|
+
var skillHandler = {
|
|
480
|
+
type: "skill",
|
|
481
|
+
async install(item, tools, scope, method, cwd) {
|
|
482
|
+
return installSkill(item, tools, scope, method, cwd);
|
|
483
|
+
},
|
|
484
|
+
async uninstall(slug, tool, scope, cwd) {
|
|
485
|
+
return uninstallSkill(slug, tool, scope, cwd);
|
|
486
|
+
},
|
|
487
|
+
async listInstalled(tool, scope, cwd) {
|
|
488
|
+
return getInstalledSkills(tool, scope, cwd);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
456
491
|
|
|
457
492
|
export {
|
|
458
493
|
loadManifest,
|
|
@@ -460,20 +495,27 @@ export {
|
|
|
460
495
|
listItems,
|
|
461
496
|
searchItems,
|
|
462
497
|
getItemContent,
|
|
498
|
+
getItemSourcePath,
|
|
463
499
|
exists,
|
|
464
500
|
ensureDir,
|
|
501
|
+
installFile,
|
|
502
|
+
removeFile,
|
|
465
503
|
writeTextFile,
|
|
504
|
+
getAgentsPath,
|
|
505
|
+
installDirectory,
|
|
466
506
|
AI_TOOLS,
|
|
467
507
|
ALL_TOOLS,
|
|
468
508
|
getToolConfig,
|
|
509
|
+
getContentPath,
|
|
510
|
+
getSettingsPath,
|
|
511
|
+
getMcpPath,
|
|
469
512
|
getToolPath,
|
|
470
513
|
detectInstalledTools,
|
|
471
514
|
detectProjectTools,
|
|
472
515
|
isToolInstalled,
|
|
473
516
|
parseToolsArg,
|
|
474
|
-
parseSkillMarkdown,
|
|
475
|
-
convertSkillToTool,
|
|
476
517
|
installSkill,
|
|
477
518
|
uninstallSkill,
|
|
478
|
-
getInstalledSkills
|
|
519
|
+
getInstalledSkills,
|
|
520
|
+
skillHandler
|
|
479
521
|
};
|