skillfish 1.0.4 → 1.0.5
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/commands/add.js +55 -28
- package/dist/telemetry.d.ts +4 -2
- package/dist/telemetry.js +18 -13
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -16,6 +16,7 @@ import { EXIT_CODES, isValidName } from '../lib/constants.js';
|
|
|
16
16
|
export const addCommand = new Command('add')
|
|
17
17
|
.description('Install a skill from a GitHub repository')
|
|
18
18
|
.argument('<repo>', 'GitHub repository (owner/repo or owner/repo/plugin/skill)')
|
|
19
|
+
.argument('[skill-name]', 'Install a specific skill by name (from SKILL.md frontmatter)')
|
|
19
20
|
.option('--force', 'Overwrite existing skills without prompting')
|
|
20
21
|
.option('-y, --yes', 'Skip all confirmation prompts')
|
|
21
22
|
.option('--all', 'Install all skills found in the repository')
|
|
@@ -26,11 +27,12 @@ export const addCommand = new Command('add')
|
|
|
26
27
|
.addHelpText('after', `
|
|
27
28
|
Examples:
|
|
28
29
|
$ skillfish add owner/repo Install from a repository
|
|
30
|
+
$ skillfish add owner/repo my-skill Install skill by name
|
|
29
31
|
$ skillfish add owner/repo --all Install all skills in repo
|
|
30
|
-
$ skillfish add owner/repo/plugin/skill Install a specific skill
|
|
32
|
+
$ skillfish add owner/repo/plugin/skill Install a specific skill by path
|
|
31
33
|
$ skillfish add owner/repo --path path/to Install skill at specific path
|
|
32
34
|
$ skillfish add owner/repo --project Install to current project only`)
|
|
33
|
-
.action(async (repoArg, options, command) => {
|
|
35
|
+
.action(async (repoArg, skillNameArg, options, command) => {
|
|
34
36
|
const jsonMode = command.parent?.opts().json ?? false;
|
|
35
37
|
const jsonOutput = createJsonOutput();
|
|
36
38
|
const version = command.parent?.opts().version ?? '0.0.0';
|
|
@@ -84,29 +86,28 @@ Examples:
|
|
|
84
86
|
exitWithError('Invalid --path value. Path must be relative and contain only safe characters.', EXIT_CODES.INVALID_ARGS);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
// Parse repo format - supports
|
|
89
|
+
// Parse repo format - supports owner/repo and owner/repo/path/to/skill
|
|
88
90
|
const parts = repoArg.split('/');
|
|
89
91
|
let owner;
|
|
90
92
|
let repo;
|
|
91
|
-
if (parts.length
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Security: validate
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
if (parts.length < 2) {
|
|
94
|
+
exitWithError('Invalid format. Use: owner/repo or owner/repo/path/to/skill', EXIT_CODES.INVALID_ARGS);
|
|
95
|
+
}
|
|
96
|
+
[owner, repo] = parts;
|
|
97
|
+
// If path components exist after owner/repo, use them as the skill path
|
|
98
|
+
if (parts.length > 2) {
|
|
99
|
+
const pathParts = parts.slice(2);
|
|
100
|
+
// Security: validate each path component
|
|
101
|
+
for (const part of pathParts) {
|
|
102
|
+
if (!isValidName(part)) {
|
|
103
|
+
exitWithError('Invalid path component. Use only alphanumeric characters, dots, hyphens, and underscores.', EXIT_CODES.INVALID_ARGS);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
explicitPath = explicitPath || pathParts.join('/');
|
|
103
107
|
if (!jsonMode) {
|
|
104
|
-
console.log(`Installing skill from: ${
|
|
108
|
+
console.log(`Installing skill from: ${explicitPath}`);
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
|
-
else {
|
|
108
|
-
exitWithError('Invalid format. Use: owner/repo or owner/repo/plugin/skill', EXIT_CODES.INVALID_ARGS);
|
|
109
|
-
}
|
|
110
111
|
// Validate owner/repo (security: prevent injection)
|
|
111
112
|
if (!owner || !repo || !isValidName(owner) || !isValidName(repo)) {
|
|
112
113
|
exitWithError('Invalid repository format. Use: owner/repo', EXIT_CODES.INVALID_ARGS);
|
|
@@ -114,7 +115,7 @@ Examples:
|
|
|
114
115
|
// 1. Discover or select skills
|
|
115
116
|
const skillPaths = explicitPath
|
|
116
117
|
? [explicitPath]
|
|
117
|
-
: await discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput);
|
|
118
|
+
: await discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput, skillNameArg);
|
|
118
119
|
if (!skillPaths || skillPaths.length === 0) {
|
|
119
120
|
if (jsonMode) {
|
|
120
121
|
outputJsonAndExit(EXIT_CODES.NOT_FOUND);
|
|
@@ -224,10 +225,7 @@ Examples:
|
|
|
224
225
|
totalSkipped += result.skipped.length;
|
|
225
226
|
// Track successful installs (telemetry with timeout)
|
|
226
227
|
if (result.installed.length > 0) {
|
|
227
|
-
|
|
228
|
-
const skillDir = skillPath.replace(/\/?SKILL\.md$/i, '').replace(/^\.?\/?/, '');
|
|
229
|
-
const github = skillDir ? `${owner}/${repo}/${skillDir}` : `${owner}/${repo}`;
|
|
230
|
-
telemetryPromises.push(trackInstall(github));
|
|
228
|
+
telemetryPromises.push(trackInstall(owner, repo, skillName));
|
|
231
229
|
}
|
|
232
230
|
}
|
|
233
231
|
// Wait for telemetry to complete (with timeout built into trackInstall)
|
|
@@ -325,7 +323,7 @@ async function selectAgents(agents, isLocal, jsonMode) {
|
|
|
325
323
|
}
|
|
326
324
|
return agents.filter((a) => selected.includes(a.name));
|
|
327
325
|
}
|
|
328
|
-
async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput) {
|
|
326
|
+
async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput, targetSkillName) {
|
|
329
327
|
let skillPaths;
|
|
330
328
|
try {
|
|
331
329
|
skillPaths = await findAllSkillMdFiles(owner, repo);
|
|
@@ -397,6 +395,35 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput)
|
|
|
397
395
|
}
|
|
398
396
|
// Store found skills in JSON output
|
|
399
397
|
jsonOutput.skills_found = skills.map((s) => s.name);
|
|
398
|
+
// If a specific skill name was requested, find and return it
|
|
399
|
+
if (targetSkillName) {
|
|
400
|
+
const normalizedTarget = targetSkillName.toLowerCase();
|
|
401
|
+
const matchedSkill = skills.find((s) => s.name.toLowerCase() === normalizedTarget);
|
|
402
|
+
if (matchedSkill) {
|
|
403
|
+
const displayName = toTitleCase(matchedSkill.name);
|
|
404
|
+
const desc = matchedSkill.description ? truncate(matchedSkill.description, 60) : '';
|
|
405
|
+
if (!jsonMode) {
|
|
406
|
+
p.log.info(`${pc.bold(displayName)}${desc ? pc.dim(` - ${desc}`) : ''}`);
|
|
407
|
+
}
|
|
408
|
+
return [matchedSkill.dir];
|
|
409
|
+
}
|
|
410
|
+
// Skill not found - show available skills
|
|
411
|
+
const errorMsg = `Skill "${targetSkillName}" not found in repository`;
|
|
412
|
+
if (jsonMode) {
|
|
413
|
+
jsonOutput.errors.push(errorMsg);
|
|
414
|
+
jsonOutput.success = false;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
p.log.error(errorMsg);
|
|
418
|
+
console.log(`\nAvailable skills in ${owner}/${repo}:`);
|
|
419
|
+
for (const skill of skills) {
|
|
420
|
+
const displayName = toTitleCase(skill.name);
|
|
421
|
+
const desc = skill.description ? pc.dim(` - ${truncate(skill.description, 80)}`) : '';
|
|
422
|
+
console.log(` - ${pc.cyan(skill.name)} ${displayName}${desc}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
400
427
|
if (skills.length === 1) {
|
|
401
428
|
const skill = skills[0];
|
|
402
429
|
const displayName = toTitleCase(skill.name);
|
|
@@ -424,7 +451,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput)
|
|
|
424
451
|
}
|
|
425
452
|
// Otherwise, list skills and exit with guidance
|
|
426
453
|
if (jsonMode) {
|
|
427
|
-
jsonOutput.errors.push('Multiple skills found.
|
|
454
|
+
jsonOutput.errors.push('Multiple skills found. Specify skill name, use --path, or --all.');
|
|
428
455
|
jsonOutput.success = false;
|
|
429
456
|
}
|
|
430
457
|
else {
|
|
@@ -432,9 +459,9 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput)
|
|
|
432
459
|
for (const skill of skills) {
|
|
433
460
|
const displayName = toTitleCase(skill.name);
|
|
434
461
|
const desc = skill.description ? pc.dim(` - ${truncate(skill.description, 80)}`) : '';
|
|
435
|
-
console.log(` - ${displayName}${desc}`);
|
|
462
|
+
console.log(` - ${pc.cyan(skill.name)} ${displayName}${desc}`);
|
|
436
463
|
}
|
|
437
|
-
console.error('\nMultiple skills found.
|
|
464
|
+
console.error('\nMultiple skills found. Specify skill name, use --path, or --all (non-interactive mode).');
|
|
438
465
|
}
|
|
439
466
|
return null;
|
|
440
467
|
}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Track a skill install. Returns a promise that resolves when the request
|
|
3
3
|
* completes (or times out). Never rejects.
|
|
4
4
|
*
|
|
5
|
-
* @param
|
|
5
|
+
* @param owner GitHub repository owner
|
|
6
|
+
* @param repo GitHub repository name
|
|
7
|
+
* @param skillName Name of the skill being installed
|
|
6
8
|
* @returns Promise that resolves when telemetry is sent (or times out)
|
|
7
9
|
*/
|
|
8
|
-
export declare function trackInstall(
|
|
10
|
+
export declare function trackInstall(owner: string, repo: string, skillName: string): Promise<void>;
|
package/dist/telemetry.js
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
const TELEMETRY_URL = 'https://mcpmarket.com/api/telemetry';
|
|
2
2
|
/** Timeout for telemetry requests (ms) */
|
|
3
|
-
const TELEMETRY_TIMEOUT =
|
|
3
|
+
const TELEMETRY_TIMEOUT = 5000;
|
|
4
4
|
/**
|
|
5
5
|
* Track a skill install. Returns a promise that resolves when the request
|
|
6
6
|
* completes (or times out). Never rejects.
|
|
7
7
|
*
|
|
8
|
-
* @param
|
|
8
|
+
* @param owner GitHub repository owner
|
|
9
|
+
* @param repo GitHub repository name
|
|
10
|
+
* @param skillName Name of the skill being installed
|
|
9
11
|
* @returns Promise that resolves when telemetry is sent (or times out)
|
|
10
12
|
*/
|
|
11
|
-
export function trackInstall(
|
|
13
|
+
export function trackInstall(owner, repo, skillName) {
|
|
12
14
|
try {
|
|
13
15
|
if (process.env.DO_NOT_TRACK === '1' || process.env.CI === 'true') {
|
|
14
16
|
return Promise.resolve();
|
|
15
17
|
}
|
|
16
|
-
if (!
|
|
18
|
+
if (!owner || !repo || !skillName) {
|
|
17
19
|
return Promise.resolve();
|
|
18
20
|
}
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
+
// Use AbortController to properly timeout the request
|
|
22
|
+
// This ensures the fetch actually completes (or aborts) before we return
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeoutId = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT);
|
|
25
|
+
const body = JSON.stringify({ owner, repo, skillName });
|
|
26
|
+
return fetch(TELEMETRY_URL, {
|
|
21
27
|
method: 'POST',
|
|
22
28
|
headers: { 'Content-Type': 'application/json' },
|
|
23
|
-
body
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
body,
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
})
|
|
32
|
+
.then(() => { })
|
|
33
|
+
.catch(() => { })
|
|
34
|
+
.finally(() => clearTimeout(timeoutId));
|
|
29
35
|
}
|
|
30
36
|
catch {
|
|
31
|
-
// Telemetry should never throw
|
|
32
37
|
return Promise.resolve();
|
|
33
38
|
}
|
|
34
39
|
}
|