@mcp-s/skills 1.0.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.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @mcp-s/skills
2
+
3
+ Install skills onto coding agents from an MCP-S server. Supports Claude Code, Cline, Codex, Cursor, Windsurf, and many more AI coding agents.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @mcp-s/skills --org <org> [options]
9
+ # or
10
+ npx @mcp-s/skills --base-url <url> [options]
11
+ ```
12
+
13
+ ### Options
14
+
15
+ - `-o, --org <org>` - Organization name (builds URL as `https://<org>.mcp-s.com/skills`)
16
+ - `-b, --base-url <url>` - Full base URL of the MCP-S skills server
17
+ - `-g, --global` - Install skill globally (user-level) instead of project-level
18
+ - `-a, --agent <agents...>` - Specify agents to install to (claude-code, cline, codex, cursor, etc.)
19
+ - `-s, --skill <skills...>` - Specify skill slugs to install (skip selection prompt)
20
+ - `-l, --list` - List available skills without installing
21
+ - `-y, --yes` - Skip confirmation prompts
22
+ - `--all` - Install all skills to all agents without any prompts (implies -y -g)
23
+
24
+ ## Authentication
25
+
26
+ The CLI automatically handles authentication. On first run, it will:
27
+ 1. Generate authentication credentials
28
+ 2. Open your browser to complete authentication
29
+ 3. Save the credentials for future use
30
+
31
+ Credentials are stored in `~/.mcp-s/skills-config.json`.
32
+
33
+ ## Examples
34
+
35
+ ### List available skills
36
+
37
+ ```bash
38
+ npx @mcp-s/skills --org mycompany --list
39
+ ```
40
+
41
+ ### Install specific skills
42
+
43
+ ```bash
44
+ npx @mcp-s/skills --org mycompany -s skill-1 skill-2
45
+ ```
46
+
47
+ ### Install to specific agents
48
+
49
+ ```bash
50
+ npx @mcp-s/skills --org mycompany -a cursor claude-code
51
+ ```
52
+
53
+ ### Install all skills globally without prompts
54
+
55
+ ```bash
56
+ npx @mcp-s/skills --org mycompany --all
57
+ ```
58
+
59
+ ### Using a custom base URL
60
+
61
+ ```bash
62
+ npx @mcp-s/skills --base-url https://custom.example.com/skills --list
63
+ ```
64
+
65
+ ## Supported Agents
66
+
67
+ - Amp
68
+ - Antigravity
69
+ - Claude Code
70
+ - Clawdbot
71
+ - Cline
72
+ - Codex
73
+ - Command Code
74
+ - Cursor
75
+ - Droid
76
+ - Gemini CLI
77
+ - GitHub Copilot
78
+ - Goose
79
+ - Kilo Code
80
+ - Kiro CLI
81
+ - MCPJam
82
+ - Neovate
83
+ - OpenCode
84
+ - OpenHands
85
+ - Pi
86
+ - Qoder
87
+ - Qwen Code
88
+ - Roo Code
89
+ - Trae
90
+ - Windsurf
91
+ - Zencoder
92
+
93
+ ## Server API
94
+
95
+ The skills library expects the server to implement the following endpoints:
96
+
97
+ ### POST /generate-auth-url
98
+
99
+ Generates an authentication URL for the user.
100
+
101
+ **Headers:**
102
+ - `Authorization: <userAccessKey>:<userOTT>`
103
+
104
+ **Response:**
105
+ ```json
106
+ {
107
+ "data": {
108
+ "url": "https://..."
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### GET /skills
114
+
115
+ Returns a list of available skills.
116
+
117
+ **Headers:**
118
+ - `Authorization: <userAccessKey>:<userOTT>`
119
+
120
+ **Response:**
121
+ ```json
122
+ {
123
+ "data": [
124
+ {
125
+ "name": "Skill Name",
126
+ "slug": "skill-slug",
127
+ "description": "Skill description",
128
+ "content": "# SKILL.md content...",
129
+ "category": "optional-category"
130
+ }
131
+ ]
132
+ }
133
+ ```
134
+
135
+ ## License
136
+
137
+ ISC
package/dist/agents.js ADDED
@@ -0,0 +1,246 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { existsSync } from "fs";
4
+ const home = homedir();
5
+ export const agents = {
6
+ amp: {
7
+ name: "amp",
8
+ displayName: "Amp",
9
+ skillsDir: ".agents/skills",
10
+ globalSkillsDir: join(home, ".config/agents/skills"),
11
+ detectInstalled: async () => {
12
+ return existsSync(join(home, ".config/amp"));
13
+ },
14
+ },
15
+ antigravity: {
16
+ name: "antigravity",
17
+ displayName: "Antigravity",
18
+ skillsDir: ".agent/skills",
19
+ globalSkillsDir: join(home, ".gemini/antigravity/skills"),
20
+ detectInstalled: async () => {
21
+ return (existsSync(join(process.cwd(), ".agent")) ||
22
+ existsSync(join(home, ".gemini/antigravity")));
23
+ },
24
+ },
25
+ "claude-code": {
26
+ name: "claude-code",
27
+ displayName: "Claude Code",
28
+ skillsDir: ".claude/skills",
29
+ globalSkillsDir: join(home, ".claude/skills"),
30
+ detectInstalled: async () => {
31
+ return existsSync(join(home, ".claude"));
32
+ },
33
+ },
34
+ clawdbot: {
35
+ name: "clawdbot",
36
+ displayName: "Clawdbot",
37
+ skillsDir: "skills",
38
+ globalSkillsDir: join(home, ".clawdbot/skills"),
39
+ detectInstalled: async () => {
40
+ return existsSync(join(home, ".clawdbot"));
41
+ },
42
+ },
43
+ cline: {
44
+ name: "cline",
45
+ displayName: "Cline",
46
+ skillsDir: ".cline/skills",
47
+ globalSkillsDir: join(home, ".cline/skills"),
48
+ detectInstalled: async () => {
49
+ return existsSync(join(home, ".cline"));
50
+ },
51
+ },
52
+ codex: {
53
+ name: "codex",
54
+ displayName: "Codex",
55
+ skillsDir: ".codex/skills",
56
+ globalSkillsDir: join(home, ".codex/skills"),
57
+ detectInstalled: async () => {
58
+ return existsSync(join(home, ".codex"));
59
+ },
60
+ },
61
+ "command-code": {
62
+ name: "command-code",
63
+ displayName: "Command Code",
64
+ skillsDir: ".commandcode/skills",
65
+ globalSkillsDir: join(home, ".commandcode/skills"),
66
+ detectInstalled: async () => {
67
+ return existsSync(join(home, ".commandcode"));
68
+ },
69
+ },
70
+ cursor: {
71
+ name: "cursor",
72
+ displayName: "Cursor",
73
+ skillsDir: ".cursor/skills",
74
+ globalSkillsDir: join(home, ".cursor/skills"),
75
+ detectInstalled: async () => {
76
+ return existsSync(join(home, ".cursor"));
77
+ },
78
+ },
79
+ droid: {
80
+ name: "droid",
81
+ displayName: "Droid",
82
+ skillsDir: ".factory/skills",
83
+ globalSkillsDir: join(home, ".factory/skills"),
84
+ detectInstalled: async () => {
85
+ return existsSync(join(home, ".factory/skills"));
86
+ },
87
+ },
88
+ "gemini-cli": {
89
+ name: "gemini-cli",
90
+ displayName: "Gemini CLI",
91
+ skillsDir: ".gemini/skills",
92
+ globalSkillsDir: join(home, ".gemini/skills"),
93
+ detectInstalled: async () => {
94
+ return existsSync(join(home, ".gemini"));
95
+ },
96
+ },
97
+ "github-copilot": {
98
+ name: "github-copilot",
99
+ displayName: "GitHub Copilot",
100
+ skillsDir: ".github/skills",
101
+ globalSkillsDir: join(home, ".copilot/skills"),
102
+ detectInstalled: async () => {
103
+ return (existsSync(join(process.cwd(), ".github")) ||
104
+ existsSync(join(home, ".copilot")));
105
+ },
106
+ },
107
+ goose: {
108
+ name: "goose",
109
+ displayName: "Goose",
110
+ skillsDir: ".goose/skills",
111
+ globalSkillsDir: join(home, ".config/goose/skills"),
112
+ detectInstalled: async () => {
113
+ return existsSync(join(home, ".config/goose"));
114
+ },
115
+ },
116
+ kilo: {
117
+ name: "kilo",
118
+ displayName: "Kilo Code",
119
+ skillsDir: ".kilocode/skills",
120
+ globalSkillsDir: join(home, ".kilocode/skills"),
121
+ detectInstalled: async () => {
122
+ return existsSync(join(home, ".kilocode"));
123
+ },
124
+ },
125
+ "kiro-cli": {
126
+ name: "kiro-cli",
127
+ displayName: "Kiro CLI",
128
+ skillsDir: ".kiro/skills",
129
+ globalSkillsDir: join(home, ".kiro/skills"),
130
+ detectInstalled: async () => {
131
+ return existsSync(join(home, ".kiro"));
132
+ },
133
+ },
134
+ mcpjam: {
135
+ name: "mcpjam",
136
+ displayName: "MCPJam",
137
+ skillsDir: ".mcpjam/skills",
138
+ globalSkillsDir: join(home, ".mcpjam/skills"),
139
+ detectInstalled: async () => {
140
+ return existsSync(join(home, ".mcpjam"));
141
+ },
142
+ },
143
+ opencode: {
144
+ name: "opencode",
145
+ displayName: "OpenCode",
146
+ skillsDir: ".opencode/skills",
147
+ globalSkillsDir: join(home, ".config/opencode/skills"),
148
+ detectInstalled: async () => {
149
+ return (existsSync(join(home, ".config/opencode")) ||
150
+ existsSync(join(home, ".claude/skills")));
151
+ },
152
+ },
153
+ openhands: {
154
+ name: "openhands",
155
+ displayName: "OpenHands",
156
+ skillsDir: ".openhands/skills",
157
+ globalSkillsDir: join(home, ".openhands/skills"),
158
+ detectInstalled: async () => {
159
+ return existsSync(join(home, ".openhands"));
160
+ },
161
+ },
162
+ pi: {
163
+ name: "pi",
164
+ displayName: "Pi",
165
+ skillsDir: ".pi/skills",
166
+ globalSkillsDir: join(home, ".pi/agent/skills"),
167
+ detectInstalled: async () => {
168
+ return existsSync(join(home, ".pi/agent"));
169
+ },
170
+ },
171
+ qoder: {
172
+ name: "qoder",
173
+ displayName: "Qoder",
174
+ skillsDir: ".qoder/skills",
175
+ globalSkillsDir: join(home, ".qoder/skills"),
176
+ detectInstalled: async () => {
177
+ return existsSync(join(home, ".qoder"));
178
+ },
179
+ },
180
+ "qwen-code": {
181
+ name: "qwen-code",
182
+ displayName: "Qwen Code",
183
+ skillsDir: ".qwen/skills",
184
+ globalSkillsDir: join(home, ".qwen/skills"),
185
+ detectInstalled: async () => {
186
+ return existsSync(join(home, ".qwen"));
187
+ },
188
+ },
189
+ roo: {
190
+ name: "roo",
191
+ displayName: "Roo Code",
192
+ skillsDir: ".roo/skills",
193
+ globalSkillsDir: join(home, ".roo/skills"),
194
+ detectInstalled: async () => {
195
+ return existsSync(join(home, ".roo"));
196
+ },
197
+ },
198
+ trae: {
199
+ name: "trae",
200
+ displayName: "Trae",
201
+ skillsDir: ".trae/skills",
202
+ globalSkillsDir: join(home, ".trae/skills"),
203
+ detectInstalled: async () => {
204
+ return existsSync(join(home, ".trae"));
205
+ },
206
+ },
207
+ windsurf: {
208
+ name: "windsurf",
209
+ displayName: "Windsurf",
210
+ skillsDir: ".windsurf/skills",
211
+ globalSkillsDir: join(home, ".codeium/windsurf/skills"),
212
+ detectInstalled: async () => {
213
+ return existsSync(join(home, ".codeium/windsurf"));
214
+ },
215
+ },
216
+ zencoder: {
217
+ name: "zencoder",
218
+ displayName: "Zencoder",
219
+ skillsDir: ".zencoder/skills",
220
+ globalSkillsDir: join(home, ".zencoder/skills"),
221
+ detectInstalled: async () => {
222
+ return existsSync(join(home, ".zencoder"));
223
+ },
224
+ },
225
+ neovate: {
226
+ name: "neovate",
227
+ displayName: "Neovate",
228
+ skillsDir: ".neovate/skills",
229
+ globalSkillsDir: join(home, ".neovate/skills"),
230
+ detectInstalled: async () => {
231
+ return existsSync(join(home, ".neovate"));
232
+ },
233
+ },
234
+ };
235
+ export async function detectInstalledAgents() {
236
+ const installed = [];
237
+ for (const [type, config] of Object.entries(agents)) {
238
+ if (await config.detectInstalled()) {
239
+ installed.push(type);
240
+ }
241
+ }
242
+ return installed;
243
+ }
244
+ export function getAgentConfig(type) {
245
+ return agents[type];
246
+ }
package/dist/api.js ADDED
@@ -0,0 +1,45 @@
1
+ export async function fetchSkills(baseUrl, auth) {
2
+ const headers = {
3
+ "Content-Type": "application/json",
4
+ };
5
+ if (auth) {
6
+ headers["Authorization"] = `${auth.userAccessKey}:${auth.userOTT}`;
7
+ }
8
+ const response = await fetch(`${baseUrl}/skills`, {
9
+ headers,
10
+ });
11
+ if (!response.ok) {
12
+ const errorText = await response.text();
13
+ // Check for authentication error
14
+ if (response.status === 401) {
15
+ const error = new Error("Unauthorized");
16
+ error.status = 401;
17
+ throw error;
18
+ }
19
+ throw new Error(`Failed to fetch skills: ${response.status} ${response.statusText}\n${errorText}`);
20
+ }
21
+ const data = await response.json();
22
+ return data.data;
23
+ }
24
+ export async function fetchSkillContent(baseUrl, skillSlug, auth) {
25
+ const headers = {
26
+ "Content-Type": "application/json",
27
+ };
28
+ if (auth) {
29
+ headers["Authorization"] = `${auth.userAccessKey}:${auth.userOTT}`;
30
+ }
31
+ const response = await fetch(`${baseUrl}/skills/${skillSlug}/content`, {
32
+ headers,
33
+ });
34
+ if (!response.ok) {
35
+ const errorText = await response.text();
36
+ if (response.status === 401) {
37
+ const error = new Error("Unauthorized");
38
+ error.status = 401;
39
+ throw error;
40
+ }
41
+ throw new Error(`Failed to fetch skill content: ${response.status} ${response.statusText}\n${errorText}`);
42
+ }
43
+ const data = await response.json();
44
+ return data.content;
45
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,98 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { readFile, writeFile, mkdir } from "fs/promises";
4
+ import { existsSync } from "fs";
5
+ import crypto from "crypto";
6
+ import open from "open";
7
+ const CONFIG_DIR = join(homedir(), ".mcp-s");
8
+ const CONFIG_FILE = join(CONFIG_DIR, "skills-config.json");
9
+ export const generateToken = () => crypto.randomBytes(32).toString("hex");
10
+ export async function loadAuthConfig() {
11
+ try {
12
+ if (!existsSync(CONFIG_FILE)) {
13
+ return null;
14
+ }
15
+ const content = await readFile(CONFIG_FILE, "utf-8");
16
+ return JSON.parse(content);
17
+ }
18
+ catch (_a) {
19
+ return null;
20
+ }
21
+ }
22
+ export async function saveAuthConfig(config) {
23
+ if (!existsSync(CONFIG_DIR)) {
24
+ await mkdir(CONFIG_DIR, { recursive: true });
25
+ }
26
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
27
+ }
28
+ export async function generateAuthUrl({ baseUrl, userAccessKey, userOTT, }) {
29
+ const response = await fetch(`${baseUrl}/generate-auth-url`, {
30
+ method: "POST",
31
+ headers: {
32
+ Authorization: `${userAccessKey}:${userOTT}`,
33
+ },
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to generate auth URL: ${response.status}`);
37
+ }
38
+ const { data: { url }, } = await response.json();
39
+ return url;
40
+ }
41
+ export async function authenticate({ baseUrl, userAccessKey, userOTT, openBrowser = true, }) {
42
+ const url = await generateAuthUrl({ baseUrl, userAccessKey, userOTT });
43
+ let opened = false;
44
+ if (openBrowser) {
45
+ try {
46
+ await open(url);
47
+ opened = true;
48
+ }
49
+ catch (_a) {
50
+ // Browser opening failed, user will need to use the URL manually
51
+ }
52
+ }
53
+ return { url, opened };
54
+ }
55
+ export async function getOrCreateAuth(org) {
56
+ const existing = await loadAuthConfig();
57
+ if (existing && existing.userAccessKey && existing.userOTT) {
58
+ // If org changed, generate new OTT
59
+ if (org && existing.org !== org) {
60
+ const newOTT = generateToken();
61
+ await saveAuthConfig(Object.assign(Object.assign({}, existing), { userOTT: newOTT, org }));
62
+ return {
63
+ userAccessKey: existing.userAccessKey,
64
+ userOTT: newOTT,
65
+ isNew: true,
66
+ };
67
+ }
68
+ return {
69
+ userAccessKey: existing.userAccessKey,
70
+ userOTT: existing.userOTT,
71
+ isNew: false,
72
+ };
73
+ }
74
+ // Create new auth
75
+ const userAccessKey = generateToken();
76
+ const userOTT = generateToken();
77
+ await saveAuthConfig({
78
+ userAccessKey,
79
+ userOTT,
80
+ org,
81
+ });
82
+ return {
83
+ userAccessKey,
84
+ userOTT,
85
+ isNew: true,
86
+ };
87
+ }
88
+ export async function refreshToken(org) {
89
+ const existing = await loadAuthConfig();
90
+ const userAccessKey = (existing === null || existing === void 0 ? void 0 : existing.userAccessKey) || generateToken();
91
+ const userOTT = generateToken();
92
+ await saveAuthConfig({
93
+ userAccessKey,
94
+ userOTT,
95
+ org,
96
+ });
97
+ return { userAccessKey, userOTT };
98
+ }
package/dist/index.js ADDED
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import * as p from "@clack/prompts";
4
+ import chalk from "chalk";
5
+ import { homedir } from "os";
6
+ import { fetchSkills } from "./api.js";
7
+ import { detectInstalledAgents, agents } from "./agents.js";
8
+ import { installSkillForAgent, isSkillInstalled, getCanonicalPath, } from "./installer.js";
9
+ import { getOrCreateAuth, refreshToken, authenticate, saveAuthConfig, } from "./auth.js";
10
+ function shortenPath(fullPath, cwd) {
11
+ const home = homedir();
12
+ if (fullPath.startsWith(home)) {
13
+ return fullPath.replace(home, "~");
14
+ }
15
+ if (fullPath.startsWith(cwd)) {
16
+ return "." + fullPath.slice(cwd.length);
17
+ }
18
+ return fullPath;
19
+ }
20
+ function formatList(items, maxShow = 5) {
21
+ if (items.length <= maxShow) {
22
+ return items.join(", ");
23
+ }
24
+ const shown = items.slice(0, maxShow);
25
+ const remaining = items.length - maxShow;
26
+ return `${shown.join(", ")} +${remaining} more`;
27
+ }
28
+ function buildBaseUrl(org, baseUrl) {
29
+ if (baseUrl) {
30
+ return baseUrl;
31
+ }
32
+ if (org) {
33
+ return `https://${org}.mcp-s.com/skills`;
34
+ }
35
+ throw new Error("Either --org or --base-url must be provided");
36
+ }
37
+ program
38
+ .name("skills")
39
+ .description("Install skills onto coding agents from MCP-S server (Claude Code, Cline, Codex, Cursor, and more)")
40
+ .version("1.0.0")
41
+ .option("-o, --org <org>", "Organization name (builds URL as https://<org>.mcp-s.com/skills)")
42
+ .option("-b, --base-url <url>", "Full base URL of the MCP-S skills server")
43
+ .option("-g, --global", "Install skill globally (user-level) instead of project-level")
44
+ .option("-a, --agent <agents...>", "Specify agents to install to (claude-code, cline, codex, cursor, and more)")
45
+ .option("-s, --skill <skills...>", "Specify skill slugs to install (skip selection prompt)")
46
+ .option("-l, --list", "List available skills without installing")
47
+ .option("-y, --yes", "Skip confirmation prompts")
48
+ .option("--all", "Install all skills to all agents without any prompts (implies -y -g)")
49
+ .configureOutput({
50
+ outputError: (str, write) => {
51
+ if (str.includes("error: required option")) {
52
+ console.log();
53
+ console.log(chalk.bgRed.white.bold(" ERROR ") +
54
+ " " +
55
+ chalk.red("Missing required option: --org or --base-url"));
56
+ console.log();
57
+ console.log(chalk.dim(" Usage:"));
58
+ console.log(` ${chalk.cyan("npx @mcp-s/skills")} ${chalk.yellow("--org <org>")} ${chalk.dim("[options]")}`);
59
+ console.log(` ${chalk.cyan("npx @mcp-s/skills")} ${chalk.yellow("--base-url <url>")} ${chalk.dim("[options]")}`);
60
+ console.log();
61
+ console.log(chalk.dim(" Examples:"));
62
+ console.log(` ${chalk.cyan("npx @mcp-s/skills")} ${chalk.yellow("--org mycompany")}`);
63
+ console.log(` ${chalk.cyan("npx @mcp-s/skills")} ${chalk.yellow("--base-url https://custom.example.com/skills")}`);
64
+ console.log();
65
+ console.log(chalk.dim(" Run") +
66
+ ` ${chalk.cyan("npx @mcp-s/skills --help")}` +
67
+ chalk.dim(" for more information."));
68
+ console.log();
69
+ }
70
+ else {
71
+ write(str);
72
+ }
73
+ },
74
+ })
75
+ .action(async (options) => {
76
+ await main(options);
77
+ });
78
+ program.parse();
79
+ async function main(options) {
80
+ var _a;
81
+ // Validate that either org or baseUrl is provided
82
+ if (!options.org && !options.baseUrl) {
83
+ console.log();
84
+ console.log(chalk.bgRed.white.bold(" ERROR ") +
85
+ " " +
86
+ chalk.red("Missing required option: --org or --base-url"));
87
+ console.log();
88
+ console.log(chalk.dim(" Usage:"));
89
+ console.log(` ${chalk.cyan("npx @mcp-s/skills")} ${chalk.yellow("--org <org>")} ${chalk.dim("[options]")}`);
90
+ console.log(` ${chalk.cyan("npx @mcp-s/skills")} ${chalk.yellow("--base-url <url>")} ${chalk.dim("[options]")}`);
91
+ console.log();
92
+ process.exit(1);
93
+ }
94
+ if (options.all) {
95
+ options.yes = true;
96
+ options.global = true;
97
+ }
98
+ console.log();
99
+ p.intro(chalk.bgCyan.black(" MCP-S Skills "));
100
+ const spinner = p.spinner();
101
+ const baseUrl = buildBaseUrl(options.org, options.baseUrl);
102
+ try {
103
+ // Get or create authentication
104
+ spinner.start("Checking authentication...");
105
+ let auth = await getOrCreateAuth(options.org);
106
+ spinner.stop(auth.isNew
107
+ ? "New authentication created"
108
+ : "Using existing authentication");
109
+ // Try to fetch skills
110
+ spinner.start("Fetching skills from server...");
111
+ let skills;
112
+ let needsAuth = false;
113
+ try {
114
+ skills = await fetchSkills(baseUrl, auth);
115
+ }
116
+ catch (error) {
117
+ if (error &&
118
+ typeof error === "object" &&
119
+ "status" in error &&
120
+ error.status === 401) {
121
+ needsAuth = true;
122
+ skills = [];
123
+ }
124
+ else {
125
+ throw error;
126
+ }
127
+ }
128
+ // Handle authentication if needed
129
+ if (needsAuth || auth.isNew) {
130
+ spinner.stop("Authentication required");
131
+ p.log.info("You need to authenticate to access skills.");
132
+ // Generate new token and authenticate
133
+ const newAuth = await refreshToken(options.org);
134
+ auth = Object.assign(Object.assign({}, newAuth), { isNew: true });
135
+ const { url, opened } = await authenticate({
136
+ baseUrl,
137
+ userAccessKey: auth.userAccessKey,
138
+ userOTT: auth.userOTT,
139
+ openBrowser: true,
140
+ });
141
+ if (opened) {
142
+ p.log.info("Opening authentication page in your browser...");
143
+ }
144
+ else {
145
+ p.log.info("Please open this URL in your browser to authenticate:");
146
+ p.log.message(chalk.cyan(url));
147
+ }
148
+ // Wait for user confirmation
149
+ const confirmed = await p.confirm({
150
+ message: "Have you completed authentication in the browser?",
151
+ });
152
+ if (p.isCancel(confirmed) || !confirmed) {
153
+ p.cancel("Authentication cancelled");
154
+ process.exit(0);
155
+ }
156
+ // Save the auth config
157
+ await saveAuthConfig({
158
+ userAccessKey: auth.userAccessKey,
159
+ userOTT: auth.userOTT,
160
+ org: options.org,
161
+ });
162
+ // Retry fetching skills
163
+ spinner.start("Fetching skills from server...");
164
+ try {
165
+ skills = await fetchSkills(baseUrl, auth);
166
+ }
167
+ catch (error) {
168
+ if (error &&
169
+ typeof error === "object" &&
170
+ "status" in error &&
171
+ error.status === 401) {
172
+ spinner.stop(chalk.red("Authentication failed"));
173
+ p.log.error("Authentication failed. Please try again or check your credentials.");
174
+ process.exit(1);
175
+ }
176
+ throw error;
177
+ }
178
+ }
179
+ spinner.stop(`Found ${skills.length} skill${skills.length !== 1 ? "s" : ""}`);
180
+ if (skills.length === 0) {
181
+ p.log.warn("No skills available on this server.");
182
+ p.outro(chalk.yellow("Nothing to install."));
183
+ process.exit(0);
184
+ }
185
+ // List mode
186
+ if (options.list) {
187
+ console.log();
188
+ p.log.step(chalk.bold("Available Skills"));
189
+ for (const skill of skills) {
190
+ p.log.message(` ${chalk.cyan(skill.slug || skill.name)} - ${chalk.dim(skill.description)}`);
191
+ }
192
+ console.log();
193
+ p.outro("Run without --list to install");
194
+ process.exit(0);
195
+ }
196
+ // Select skills
197
+ let selectedSkills;
198
+ if (options.skill && options.skill.length > 0) {
199
+ // Filter by specified skill slugs
200
+ selectedSkills = skills.filter((s) => options.skill.includes(s.slug) || options.skill.includes(s.name));
201
+ if (selectedSkills.length === 0) {
202
+ p.log.error(`No matching skills found for: ${options.skill.join(", ")}`);
203
+ p.log.info(`Available skills: ${skills.map((s) => s.slug || s.name).join(", ")}`);
204
+ process.exit(1);
205
+ }
206
+ }
207
+ else if (options.all) {
208
+ selectedSkills = skills;
209
+ }
210
+ else {
211
+ // Interactive selection
212
+ const skillChoices = skills.map((s) => ({
213
+ value: s,
214
+ label: s.name,
215
+ hint: s.description,
216
+ }));
217
+ const selected = await p.multiselect({
218
+ message: "Select skills to install",
219
+ options: skillChoices,
220
+ required: true,
221
+ });
222
+ if (p.isCancel(selected)) {
223
+ p.cancel("Installation cancelled");
224
+ process.exit(0);
225
+ }
226
+ selectedSkills = selected;
227
+ }
228
+ p.log.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${chalk.cyan(selectedSkills.map((s) => s.slug || s.name).join(", "))}`);
229
+ // Select agents
230
+ let targetAgents;
231
+ const validAgents = Object.keys(agents);
232
+ if (options.agent && options.agent.length > 0) {
233
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
234
+ if (invalidAgents.length > 0) {
235
+ p.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
236
+ p.log.info(`Valid agents: ${validAgents.join(", ")}`);
237
+ process.exit(1);
238
+ }
239
+ targetAgents = options.agent;
240
+ }
241
+ else {
242
+ spinner.start("Detecting installed agents...");
243
+ const installedAgents = await detectInstalledAgents();
244
+ spinner.stop(`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`);
245
+ if (installedAgents.length === 0) {
246
+ if (options.yes) {
247
+ targetAgents = validAgents;
248
+ p.log.info("Installing to all agents (none detected)");
249
+ }
250
+ else {
251
+ p.log.warn("No coding agents detected. You can still install skills.");
252
+ const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
253
+ value: key,
254
+ label: config.displayName,
255
+ }));
256
+ const selected = await p.multiselect({
257
+ message: "Select agents to install skills to",
258
+ options: allAgentChoices,
259
+ required: true,
260
+ initialValues: Object.keys(agents),
261
+ });
262
+ if (p.isCancel(selected)) {
263
+ p.cancel("Installation cancelled");
264
+ process.exit(0);
265
+ }
266
+ targetAgents = selected;
267
+ }
268
+ }
269
+ else if (installedAgents.length === 1 || options.yes) {
270
+ targetAgents = installedAgents;
271
+ if (installedAgents.length === 1) {
272
+ const firstAgent = installedAgents[0];
273
+ p.log.info(`Installing to: ${chalk.cyan(agents[firstAgent].displayName)}`);
274
+ }
275
+ else {
276
+ p.log.info(`Installing to: ${installedAgents.map((a) => chalk.cyan(agents[a].displayName)).join(", ")}`);
277
+ }
278
+ }
279
+ else {
280
+ const agentChoices = installedAgents.map((a) => ({
281
+ value: a,
282
+ label: agents[a].displayName,
283
+ hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`,
284
+ }));
285
+ const selected = await p.multiselect({
286
+ message: "Select agents to install skills to",
287
+ options: agentChoices,
288
+ required: true,
289
+ initialValues: installedAgents,
290
+ });
291
+ if (p.isCancel(selected)) {
292
+ p.cancel("Installation cancelled");
293
+ process.exit(0);
294
+ }
295
+ targetAgents = selected;
296
+ }
297
+ }
298
+ // Installation scope
299
+ let installGlobally = (_a = options.global) !== null && _a !== void 0 ? _a : false;
300
+ if (options.global === undefined && !options.yes) {
301
+ const scope = await p.select({
302
+ message: "Installation scope",
303
+ options: [
304
+ {
305
+ value: false,
306
+ label: "Project",
307
+ hint: "Install in current directory (committed with your project)",
308
+ },
309
+ {
310
+ value: true,
311
+ label: "Global",
312
+ hint: "Install in home directory (available across all projects)",
313
+ },
314
+ ],
315
+ });
316
+ if (p.isCancel(scope)) {
317
+ p.cancel("Installation cancelled");
318
+ process.exit(0);
319
+ }
320
+ installGlobally = scope;
321
+ }
322
+ // Installation method
323
+ let installMode = "symlink";
324
+ if (!options.yes) {
325
+ const modeChoice = await p.select({
326
+ message: "Installation method",
327
+ options: [
328
+ {
329
+ value: "symlink",
330
+ label: "Symlink (Recommended)",
331
+ hint: "Single source of truth, easy updates",
332
+ },
333
+ {
334
+ value: "copy",
335
+ label: "Copy to all agents",
336
+ hint: "Independent copies for each agent",
337
+ },
338
+ ],
339
+ });
340
+ if (p.isCancel(modeChoice)) {
341
+ p.cancel("Installation cancelled");
342
+ process.exit(0);
343
+ }
344
+ installMode = modeChoice;
345
+ }
346
+ // Check for existing installations
347
+ const cwd = process.cwd();
348
+ const overwriteStatus = new Map();
349
+ for (const skill of selectedSkills) {
350
+ const skillStatus = new Map();
351
+ for (const agent of targetAgents) {
352
+ skillStatus.set(agent, await isSkillInstalled(skill.slug || skill.name, agent, {
353
+ global: installGlobally,
354
+ }));
355
+ }
356
+ overwriteStatus.set(skill.slug || skill.name, skillStatus);
357
+ }
358
+ // Show summary
359
+ const summaryLines = [];
360
+ const agentNames = targetAgents.map((a) => agents[a].displayName);
361
+ for (const skill of selectedSkills) {
362
+ const skillName = skill.slug || skill.name;
363
+ if (installMode === "symlink") {
364
+ const canonicalPath = getCanonicalPath(skillName, {
365
+ global: installGlobally,
366
+ });
367
+ const shortCanonical = shortenPath(canonicalPath, cwd);
368
+ summaryLines.push(`${chalk.cyan(skillName)}`);
369
+ summaryLines.push(` ${chalk.dim(shortCanonical)}`);
370
+ summaryLines.push(` ${chalk.dim("symlink →")} ${formatList(agentNames)}`);
371
+ }
372
+ else {
373
+ summaryLines.push(`${chalk.cyan(skillName)}`);
374
+ summaryLines.push(` ${chalk.dim("copy →")} ${formatList(agentNames)}`);
375
+ }
376
+ const skillOverwrites = overwriteStatus.get(skillName);
377
+ const overwriteAgents = targetAgents
378
+ .filter((a) => skillOverwrites === null || skillOverwrites === void 0 ? void 0 : skillOverwrites.get(a))
379
+ .map((a) => agents[a].displayName);
380
+ if (overwriteAgents.length > 0) {
381
+ summaryLines.push(` ${chalk.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
382
+ }
383
+ }
384
+ console.log();
385
+ p.note(summaryLines.join("\n"), "Installation Summary");
386
+ // Confirm
387
+ if (!options.yes) {
388
+ const confirmed = await p.confirm({
389
+ message: "Proceed with installation?",
390
+ });
391
+ if (p.isCancel(confirmed) || !confirmed) {
392
+ p.cancel("Installation cancelled");
393
+ process.exit(0);
394
+ }
395
+ }
396
+ // Install
397
+ spinner.start("Installing skills...");
398
+ const results = [];
399
+ for (const skill of selectedSkills) {
400
+ for (const agent of targetAgents) {
401
+ const result = await installSkillForAgent(skill, agent, {
402
+ global: installGlobally,
403
+ mode: installMode,
404
+ });
405
+ results.push(Object.assign({ skill: skill.slug || skill.name, agent: agents[agent].displayName }, result));
406
+ }
407
+ }
408
+ spinner.stop("Installation complete");
409
+ console.log();
410
+ const successful = results.filter((r) => r.success);
411
+ const failed = results.filter((r) => !r.success);
412
+ if (successful.length > 0) {
413
+ const resultLines = [];
414
+ // Group by skill
415
+ const skillGroups = new Map();
416
+ for (const r of successful) {
417
+ if (!skillGroups.has(r.skill)) {
418
+ skillGroups.set(r.skill, []);
419
+ }
420
+ skillGroups.get(r.skill).push(r);
421
+ }
422
+ for (const [skillName, skillResults] of skillGroups) {
423
+ const firstResult = skillResults[0];
424
+ if (firstResult.mode === "copy") {
425
+ resultLines.push(`${chalk.green("✓")} ${skillName} ${chalk.dim("(copied)")}`);
426
+ for (const r of skillResults) {
427
+ const shortPath = shortenPath(r.path, cwd);
428
+ resultLines.push(` ${chalk.dim("→")} ${r.agent}: ${shortPath}`);
429
+ }
430
+ }
431
+ else {
432
+ if (firstResult.canonicalPath) {
433
+ const shortPath = shortenPath(firstResult.canonicalPath, cwd);
434
+ resultLines.push(`${chalk.green("✓")} ${skillName}`);
435
+ resultLines.push(` ${chalk.dim(shortPath)}`);
436
+ }
437
+ else {
438
+ resultLines.push(`${chalk.green("✓")} ${skillName}`);
439
+ }
440
+ const symlinked = skillResults
441
+ .filter((r) => !r.symlinkFailed)
442
+ .map((r) => r.agent);
443
+ const copied = skillResults
444
+ .filter((r) => r.symlinkFailed)
445
+ .map((r) => r.agent);
446
+ if (symlinked.length > 0) {
447
+ resultLines.push(` ${chalk.dim("symlink →")} ${formatList(symlinked)}`);
448
+ }
449
+ if (copied.length > 0) {
450
+ resultLines.push(` ${chalk.yellow("copied →")} ${formatList(copied)}`);
451
+ }
452
+ }
453
+ }
454
+ const skillCount = skillGroups.size;
455
+ const agentCount = new Set(successful.map((r) => r.agent)).size;
456
+ const title = chalk.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`);
457
+ p.note(resultLines.join("\n"), title);
458
+ const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
459
+ if (symlinkFailures.length > 0) {
460
+ const copiedAgentNames = [
461
+ ...new Set(symlinkFailures.map((r) => r.agent)),
462
+ ];
463
+ p.log.warn(chalk.yellow(`Symlinks failed for: ${formatList(copiedAgentNames)}`));
464
+ p.log.message(chalk.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
465
+ }
466
+ }
467
+ if (failed.length > 0) {
468
+ console.log();
469
+ p.log.error(chalk.red(`Failed to install ${failed.length}`));
470
+ for (const r of failed) {
471
+ p.log.message(` ${chalk.red("✗")} ${r.skill} → ${r.agent}: ${chalk.dim(r.error)}`);
472
+ }
473
+ }
474
+ console.log();
475
+ p.outro(chalk.green("Done!"));
476
+ }
477
+ catch (error) {
478
+ spinner.stop(chalk.red("Error"));
479
+ p.log.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
480
+ process.exit(1);
481
+ }
482
+ }
@@ -0,0 +1,188 @@
1
+ import { mkdir, access, symlink, lstat, rm, readlink, writeFile, } from "fs/promises";
2
+ import { join, normalize, resolve, sep, relative } from "path";
3
+ import { homedir, platform } from "os";
4
+ import { agents } from "./agents.js";
5
+ const AGENTS_DIR = ".agents";
6
+ const SKILLS_SUBDIR = "skills";
7
+ function sanitizeName(name) {
8
+ let sanitized = name.replace(/[\/\\:\0]/g, "");
9
+ sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, "");
10
+ sanitized = sanitized.replace(/^\.+/, "");
11
+ if (!sanitized || sanitized.length === 0) {
12
+ sanitized = "unnamed-skill";
13
+ }
14
+ if (sanitized.length > 255) {
15
+ sanitized = sanitized.substring(0, 255);
16
+ }
17
+ return sanitized;
18
+ }
19
+ function isPathSafe(basePath, targetPath) {
20
+ const normalizedBase = normalize(resolve(basePath));
21
+ const normalizedTarget = normalize(resolve(targetPath));
22
+ return (normalizedTarget.startsWith(normalizedBase + sep) ||
23
+ normalizedTarget === normalizedBase);
24
+ }
25
+ function getCanonicalSkillsDir(global, cwd) {
26
+ const baseDir = global ? homedir() : cwd || process.cwd();
27
+ return join(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
28
+ }
29
+ async function createSymlink(target, linkPath) {
30
+ try {
31
+ try {
32
+ const stats = await lstat(linkPath);
33
+ if (stats.isSymbolicLink()) {
34
+ const existingTarget = await readlink(linkPath);
35
+ if (resolve(existingTarget) === resolve(target)) {
36
+ return true;
37
+ }
38
+ await rm(linkPath);
39
+ }
40
+ else {
41
+ await rm(linkPath, { recursive: true });
42
+ }
43
+ }
44
+ catch (err) {
45
+ if (err &&
46
+ typeof err === "object" &&
47
+ "code" in err &&
48
+ err.code === "ELOOP") {
49
+ try {
50
+ await rm(linkPath, { force: true });
51
+ }
52
+ catch (_a) {
53
+ // Ignore cleanup errors
54
+ }
55
+ }
56
+ }
57
+ const linkDir = join(linkPath, "..");
58
+ await mkdir(linkDir, { recursive: true });
59
+ const relativePath = relative(linkDir, target);
60
+ const symlinkType = platform() === "win32" ? "junction" : undefined;
61
+ await symlink(relativePath, linkPath, symlinkType);
62
+ return true;
63
+ }
64
+ catch (_b) {
65
+ return false;
66
+ }
67
+ }
68
+ export async function installSkillForAgent(skill, agentType, options = {}) {
69
+ var _a, _b;
70
+ const agent = agents[agentType];
71
+ const isGlobal = (_a = options.global) !== null && _a !== void 0 ? _a : false;
72
+ const cwd = options.cwd || process.cwd();
73
+ const skillName = sanitizeName(skill.slug || skill.name);
74
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
75
+ const canonicalDir = join(canonicalBase, skillName);
76
+ const agentBase = isGlobal
77
+ ? agent.globalSkillsDir
78
+ : join(cwd, agent.skillsDir);
79
+ const agentDir = join(agentBase, skillName);
80
+ const installMode = (_b = options.mode) !== null && _b !== void 0 ? _b : "symlink";
81
+ if (!isPathSafe(canonicalBase, canonicalDir)) {
82
+ return {
83
+ success: false,
84
+ path: agentDir,
85
+ mode: installMode,
86
+ error: "Invalid skill name: potential path traversal detected",
87
+ };
88
+ }
89
+ if (!isPathSafe(agentBase, agentDir)) {
90
+ return {
91
+ success: false,
92
+ path: agentDir,
93
+ mode: installMode,
94
+ error: "Invalid skill name: potential path traversal detected",
95
+ };
96
+ }
97
+ try {
98
+ if (installMode === "copy") {
99
+ await mkdir(agentDir, { recursive: true });
100
+ const skillMdPath = join(agentDir, "SKILL.md");
101
+ await writeFile(skillMdPath, skill.content, "utf-8");
102
+ return {
103
+ success: true,
104
+ path: agentDir,
105
+ mode: "copy",
106
+ };
107
+ }
108
+ // Symlink mode
109
+ await mkdir(canonicalDir, { recursive: true });
110
+ const skillMdPath = join(canonicalDir, "SKILL.md");
111
+ await writeFile(skillMdPath, skill.content, "utf-8");
112
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
113
+ if (!symlinkCreated) {
114
+ // Fallback to copy
115
+ try {
116
+ await rm(agentDir, { recursive: true, force: true });
117
+ }
118
+ catch (_c) {
119
+ // Ignore
120
+ }
121
+ await mkdir(agentDir, { recursive: true });
122
+ const agentSkillMdPath = join(agentDir, "SKILL.md");
123
+ await writeFile(agentSkillMdPath, skill.content, "utf-8");
124
+ return {
125
+ success: true,
126
+ path: agentDir,
127
+ canonicalPath: canonicalDir,
128
+ mode: "symlink",
129
+ symlinkFailed: true,
130
+ };
131
+ }
132
+ return {
133
+ success: true,
134
+ path: agentDir,
135
+ canonicalPath: canonicalDir,
136
+ mode: "symlink",
137
+ };
138
+ }
139
+ catch (error) {
140
+ return {
141
+ success: false,
142
+ path: agentDir,
143
+ mode: installMode,
144
+ error: error instanceof Error ? error.message : "Unknown error",
145
+ };
146
+ }
147
+ }
148
+ export async function isSkillInstalled(skillName, agentType, options = {}) {
149
+ const agent = agents[agentType];
150
+ const sanitized = sanitizeName(skillName);
151
+ const targetBase = options.global
152
+ ? agent.globalSkillsDir
153
+ : join(options.cwd || process.cwd(), agent.skillsDir);
154
+ const skillDir = join(targetBase, sanitized);
155
+ if (!isPathSafe(targetBase, skillDir)) {
156
+ return false;
157
+ }
158
+ try {
159
+ await access(skillDir);
160
+ return true;
161
+ }
162
+ catch (_a) {
163
+ return false;
164
+ }
165
+ }
166
+ export function getInstallPath(skillName, agentType, options = {}) {
167
+ const agent = agents[agentType];
168
+ const cwd = options.cwd || process.cwd();
169
+ const sanitized = sanitizeName(skillName);
170
+ const targetBase = options.global
171
+ ? agent.globalSkillsDir
172
+ : join(cwd, agent.skillsDir);
173
+ const installPath = join(targetBase, sanitized);
174
+ if (!isPathSafe(targetBase, installPath)) {
175
+ throw new Error("Invalid skill name: potential path traversal detected");
176
+ }
177
+ return installPath;
178
+ }
179
+ export function getCanonicalPath(skillName, options = {}) {
180
+ var _a;
181
+ const sanitized = sanitizeName(skillName);
182
+ const canonicalBase = getCanonicalSkillsDir((_a = options.global) !== null && _a !== void 0 ? _a : false, options.cwd);
183
+ const canonicalPath = join(canonicalBase, sanitized);
184
+ if (!isPathSafe(canonicalBase, canonicalPath)) {
185
+ throw new Error("Invalid skill name: potential path traversal detected");
186
+ }
187
+ return canonicalPath;
188
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@mcp-s/skills",
3
+ "version": "1.0.2",
4
+ "description": "Install skills onto coding agents from MCP-S server",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "main": "dist/index.js",
10
+ "bin": {
11
+ "skills": "dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "start": "node dist/index.js",
15
+ "build": "tsc && shx chmod +x dist/*.js",
16
+ "prepare": "npm run build",
17
+ "prepublishOnly": "npm run build && npm version patch",
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ },
20
+ "author": "MCP-S",
21
+ "license": "ISC",
22
+ "keywords": [
23
+ "cli",
24
+ "agent-skills",
25
+ "skills",
26
+ "ai-agents",
27
+ "mcp-s",
28
+ "claude-code",
29
+ "cline",
30
+ "codex",
31
+ "cursor",
32
+ "opencode",
33
+ "openhands",
34
+ "windsurf"
35
+ ],
36
+ "dependencies": {
37
+ "@clack/prompts": "^0.9.1",
38
+ "chalk": "^5.4.1",
39
+ "commander": "^13.1.0",
40
+ "open": "^10.1.1"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.14.0",
47
+ "shx": "^0.4.0",
48
+ "typescript": "^5.8.3"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ }