reskill 1.20.3 → 1.21.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/index.js CHANGED
@@ -33,476 +33,721 @@ function __webpack_require__(moduleId) {
33
33
  /************************************************************************/ // EXTERNAL MODULE: external "node:fs"
34
34
  var external_node_fs_ = __webpack_require__("node:fs");
35
35
  /**
36
- * ContentScanner - Detect malicious patterns in SKILL.md content
36
+ * Skill Parser - SKILL.md parser
37
37
  *
38
- * Features:
39
- * - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
40
- * - 6 built-in detection rules across 3 risk levels
41
- * - Configurable: override levels, disable rules, add custom rules
42
- * - Pure string operations in scan() — no fs dependency, suitable for server use
43
- * - scanFile() convenience method for CLI use
44
- */ // ============================================================================
45
- // Safe Zone Masking
46
- // ============================================================================
47
- /**
48
- * Mask safe zones in Markdown content with spaces, preserving line structure.
38
+ * Following agentskills.io specification: https://agentskills.io/specification
49
39
  *
50
- * Safe zones (content replaced with spaces):
51
- * - YAML frontmatter (`---` ... `---` at file start)
52
- * - Fenced code blocks (``` or ~~~)
53
- * - Indented code blocks (4 spaces / tab after blank line)
54
- * - Blockquotes (`> ` prefix)
55
- * - Inline code (`` `...` ``)
56
- * - Double-quoted text (`"..."`, min 3 chars between quotes)
40
+ * SKILL.md format requirements:
41
+ * - YAML frontmatter containing name and description (required)
42
+ * - name: max 64 characters, lowercase letters, numbers, hyphens
43
+ * - description: max 1024 characters
44
+ * - Optional fields: license, compatibility, metadata, allowed-tools
45
+ */ /**
46
+ * Skill validation error
47
+ */ class SkillValidationError extends Error {
48
+ field;
49
+ constructor(message, field){
50
+ super(message), this.field = field;
51
+ this.name = 'SkillValidationError';
52
+ }
53
+ }
54
+ /**
55
+ * Simple YAML frontmatter parser
56
+ * Parses --- delimited YAML header
57
57
  *
58
- * Line breaks are preserved so line numbers remain correct.
59
- */ function maskSafeZones(content) {
60
- const lines = content.split('\n');
61
- const result = [];
62
- let inFrontmatter = false;
63
- let inFencedCode = false;
64
- let fenceChar = '';
65
- let fenceLength = 0;
66
- let prevLineBlank = false;
67
- let prevLineIndentedCode = false;
68
- for(let i = 0; i < lines.length; i++){
69
- const line = lines[i];
70
- // --- YAML Frontmatter (only at file start) ---
71
- if (0 === i && '---' === line.trim()) {
72
- inFrontmatter = true;
73
- result.push(maskLine(line));
74
- continue;
75
- }
76
- if (inFrontmatter) {
77
- result.push(maskLine(line));
78
- if ('---' === line.trim()) inFrontmatter = false;
79
- continue;
80
- }
81
- // --- Fenced code blocks (``` or ~~~) ---
82
- const fenceMatch = line.match(/^(`{3,}|~{3,})/);
83
- if (!inFencedCode && fenceMatch) {
84
- inFencedCode = true;
85
- fenceChar = fenceMatch[1][0];
86
- fenceLength = fenceMatch[1].length;
87
- result.push(maskLine(line));
88
- prevLineBlank = false;
89
- prevLineIndentedCode = false;
90
- continue;
58
+ * Supports:
59
+ * - Basic key: value pairs
60
+ * - Multiline strings (| and >)
61
+ * - Nested objects (one level deep, for metadata field)
62
+ */ function parseFrontmatter(content) {
63
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
64
+ const match = content.match(frontmatterRegex);
65
+ if (!match) return {
66
+ data: {},
67
+ content
68
+ };
69
+ const yamlContent = match[1];
70
+ const markdownContent = match[2];
71
+ // Simple YAML parsing (supports basic key: value, one level of nesting,
72
+ // block scalars (| and >), and plain scalars spanning multiple indented lines)
73
+ const data = {};
74
+ const lines = yamlContent.split('\n');
75
+ let currentKey = '';
76
+ let currentValue = '';
77
+ let inMultiline = false;
78
+ let inNestedObject = false;
79
+ let inPlainScalar = false;
80
+ let nestedObject = {};
81
+ /**
82
+ * Save the current key/value accumulated so far, then reset state.
83
+ */ function flushCurrent() {
84
+ if (!currentKey) return;
85
+ if (inNestedObject) {
86
+ data[currentKey] = nestedObject;
87
+ nestedObject = {};
88
+ inNestedObject = false;
89
+ } else if (inPlainScalar || inMultiline) {
90
+ data[currentKey] = currentValue.trim();
91
+ inPlainScalar = false;
92
+ inMultiline = false;
93
+ } else data[currentKey] = parseYamlValue(currentValue.trim());
94
+ currentKey = '';
95
+ currentValue = '';
96
+ }
97
+ for (const line of lines){
98
+ const trimmedLine = line.trim();
99
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
100
+ const isIndented = line.startsWith(' ');
101
+ // ---- Inside a block scalar (| or >) ----
102
+ if (inMultiline) {
103
+ if (isIndented) {
104
+ currentValue += (currentValue ? '\n' : '') + line.slice(2);
105
+ continue;
106
+ }
107
+ // Unindented line ends the block scalar — fall through to top-level parsing
108
+ flushCurrent();
91
109
  }
92
- if (inFencedCode) {
93
- result.push(maskLine(line));
94
- const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
95
- if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
96
- prevLineBlank = false;
97
- prevLineIndentedCode = false;
98
- continue;
110
+ // ---- Inside a plain scalar (multiline value without | or >) ----
111
+ if (inPlainScalar) {
112
+ if (isIndented) {
113
+ // Continuation line: join with a space (YAML plain scalar folding)
114
+ currentValue += ` ${trimmedLine}`;
115
+ continue;
116
+ }
117
+ // Unindented line ends the plain scalar — fall through to top-level parsing
118
+ flushCurrent();
99
119
  }
100
- // --- Blockquote ---
101
- if (/^>\s?/.test(line)) {
102
- result.push(maskLine(line));
103
- prevLineBlank = false;
104
- prevLineIndentedCode = false;
120
+ // ---- Inside a nested object ----
121
+ if (inNestedObject && isIndented) {
122
+ const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
123
+ if (nestedMatch) {
124
+ const [, nestedKey, nestedValue] = nestedMatch;
125
+ nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
126
+ continue;
127
+ }
128
+ // Indented line that isn't a nested key:value — this key was actually
129
+ // a plain scalar, not a nested object. Switch modes.
130
+ inNestedObject = false;
131
+ inPlainScalar = true;
132
+ currentValue = trimmedLine;
105
133
  continue;
106
134
  }
107
- // --- Indented code block (4 spaces or tab, after blank line) ---
108
- if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
109
- result.push(maskLine(line));
110
- prevLineBlank = false;
111
- prevLineIndentedCode = true;
135
+ // ---- Top-level key: value ----
136
+ const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
137
+ if (keyValueMatch) {
138
+ flushCurrent();
139
+ currentKey = keyValueMatch[1];
140
+ currentValue = keyValueMatch[2];
141
+ if ('|' === currentValue || '>' === currentValue) {
142
+ inMultiline = true;
143
+ currentValue = '';
144
+ } else if ('' === currentValue) {
145
+ // Empty value — could be nested object or plain scalar; peek at next lines
146
+ inNestedObject = true;
147
+ nestedObject = {};
148
+ }
112
149
  continue;
113
150
  }
114
- // --- Normal line: mask inline code and double-quoted text ---
115
- result.push(maskInline(line));
116
- prevLineBlank = '' === line.trim();
117
- prevLineIndentedCode = false;
151
+ // ---- Unindented line that isn't key:value while in nested object ----
152
+ if (inNestedObject) flushCurrent();
118
153
  }
119
- return result.join('\n');
154
+ // Save last accumulated value
155
+ flushCurrent();
156
+ return {
157
+ data,
158
+ content: markdownContent
159
+ };
120
160
  }
121
- /** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
122
- return ' '.repeat(line.length);
161
+ /**
162
+ * Parse YAML value
163
+ */ function parseYamlValue(value) {
164
+ if (!value) return '';
165
+ // Boolean value
166
+ if ('true' === value) return true;
167
+ if ('false' === value) return false;
168
+ // Number
169
+ if (/^-?\d+$/.test(value)) return parseInt(value, 10);
170
+ if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
171
+ // Remove quotes
172
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
173
+ return value;
123
174
  }
124
175
  /**
125
- * Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
126
- * Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
127
- * Single quotes are NOT masked to avoid false matches with apostrophes.
128
- */ function maskInline(line) {
129
- let result = line;
130
- // Inline code: `...`
131
- result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
132
- // Double-quoted text: "..." (min 3 chars between quotes)
133
- result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
134
- return result;
176
+ * Validate skill name format
177
+ *
178
+ * Specification requirements:
179
+ * - Max 64 characters
180
+ * - Only lowercase letters, numbers, hyphens allowed
181
+ * - Cannot start or end with hyphen
182
+ * - Cannot contain consecutive hyphens
183
+ */ function validateSkillName(name) {
184
+ if (!name) throw new SkillValidationError('Skill name is required', 'name');
185
+ if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
186
+ if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
187
+ if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
188
+ if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
189
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) && name.length > 1) throw new SkillValidationError('Skill name can only contain lowercase letters, numbers, and hyphens', 'name');
190
+ // Single character name
191
+ if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
135
192
  }
136
- // ============================================================================
137
- // Rule Helpers
138
- // ============================================================================
139
- /** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
140
- const lines = content.split('\n');
141
- const matches = [];
142
- for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
143
- matches.push({
144
- line: i + 1
145
- });
146
- break;
147
- }
148
- return matches;
193
+ /**
194
+ * Validate skill description
195
+ *
196
+ * Specification requirements:
197
+ * - Max 1024 characters
198
+ * - Angle brackets are allowed per agentskills.io spec
199
+ */ function validateSkillDescription(description) {
200
+ if (!description) throw new SkillValidationError('Skill description is required', 'description');
201
+ if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
202
+ // Note: angle brackets are allowed per agentskills.io spec
149
203
  }
150
- // ============================================================================
151
- // Default Rules
152
- // ============================================================================
153
- const SNIPPET_MAX_LENGTH = 120;
154
- /** Built-in detection rules */ const DEFAULT_RULES = [
155
- // Rule 1: Prompt Injection (high)
156
- {
157
- id: 'prompt-injection',
158
- level: 'high',
159
- message: 'Detected prompt injection attempt',
160
- skipSafeZones: true,
161
- check: (content)=>findLineMatches(content, [
162
- // English patterns
163
- /ignore\s+(all\s+)?previous\s+instructions/i,
164
- /disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
165
- /you\s+are\s+now\s+(?:(?:a|an)\s+)?(?:(?:\w+\s+){0,3}(?:agent|ai|assistant|bot|model|character|persona|entity|system)|DAN\b|jailbr\w*|unrestricted|unfiltered|free\s+from)/i,
166
- /from\s+now\s+on[,\s]+you\s+are/i,
167
- /new\s+system\s+prompt/i,
168
- /override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
169
- /forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
170
- /(?:you\s+are|you're)\s+(?:now\s+)?entering\s+(?:a\s+)?new\s+(?:mode|context|session)/i,
171
- // Chinese patterns (中文提示词注入)
172
- /[忽无][略视]\s*(所有\s*)?(之前的?|先前的?|以前的?)?\s*(指令|指示|规则|约束|限制)/,
173
- /你现在是/,
174
- /从现在开始.{0,10}你是/,
175
- /新的系统提示词/,
176
- /[覆改]写?\s*(你的|系统)\s*(提示词|规则|指令|安全)/,
177
- /忘记\s*(所有\s*)?(之前的?|先前的?)?\s*(指令|指示|规则|约束)/,
178
- /进入.{0,5}新的?\s*(模式|上下文|会话)/,
179
- /不要遵守.{0,10}(安全|限制|规则|约束)/,
180
- /解除.{0,5}(限制|约束|安全)/,
181
- /无限制模式/,
182
- /安全模式已关闭/
183
- ])
184
- },
185
- // Rule 2: Data Exfiltration (high)
186
- {
187
- id: 'data-exfiltration',
188
- level: 'high',
189
- message: 'Detected potential data exfiltration command',
190
- skipSafeZones: true,
191
- check: (content)=>{
192
- const lines = content.split('\n');
193
- const matches = [];
194
- const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
195
- const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
196
- for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
197
- line: i + 1
198
- });
199
- return matches;
200
- }
201
- },
202
- // Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
203
- // Zero-width chars and base64 are suspicious everywhere (even inside code blocks).
204
- {
205
- id: 'obfuscation',
206
- level: 'high',
207
- message: 'Detected content obfuscation',
208
- skipSafeZones: false,
209
- check: (content)=>{
210
- const matches = [];
211
- const lines = content.split('\n');
212
- // Zero-width characters (suspicious in any context)
213
- const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
214
- for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
215
- line: i + 1,
216
- snippet: 'Zero-width Unicode characters detected'
217
- });
218
- // Long base64-like strings (>200 continuous chars)
219
- const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
220
- for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
221
- line: i + 1,
222
- snippet: 'Suspicious base64-encoded block detected'
223
- });
224
- return matches;
225
- }
226
- },
227
- // Rule 3b: Large HTML Comments (high) — respects safe zones (code blocks, etc.)
228
- // HTML comments inside fenced code blocks are normal code examples, not obfuscation.
229
- {
230
- id: 'obfuscation',
231
- level: 'high',
232
- message: 'Detected content obfuscation',
233
- skipSafeZones: true,
234
- check: (content)=>{
235
- const matches = [];
236
- // Large HTML comments (>200 chars of content)
237
- const commentRegex = /<!--([\s\S]{200,}?)-->/g;
238
- let match;
239
- // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
240
- while(null !== (match = commentRegex.exec(content))){
241
- const lineNum = content.slice(0, match.index).split('\n').length;
242
- matches.push({
243
- line: lineNum,
244
- snippet: `Large HTML comment block (${match[1].length} chars)`
245
- });
246
- }
247
- return matches;
204
+ /**
205
+ * Parse SKILL.md content
206
+ *
207
+ * @param content - SKILL.md file content
208
+ * @param options - Parse options
209
+ * @returns Parsed skill info, or null if format is invalid
210
+ * @throws SkillValidationError if validation fails in strict mode
211
+ */ function parseSkillMd(content, options = {}) {
212
+ const { strict = false } = options;
213
+ try {
214
+ const { data, content: body } = parseFrontmatter(content);
215
+ // Check required fields
216
+ if (!data.name || !data.description) {
217
+ if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
218
+ return null;
248
219
  }
249
- },
250
- // Rule 4: Sensitive File Access (medium)
251
- {
252
- id: 'sensitive-file-access',
253
- level: 'medium',
254
- message: 'References sensitive file path',
255
- skipSafeZones: true,
256
- check: (content)=>findLineMatches(content, [
257
- /~\/\.ssh\b/,
258
- /~\/\.aws\b/,
259
- /~\/\.gnupg\b/,
260
- /~\/\.config\/gcloud\b/,
261
- /\bid_rsa\b/i,
262
- /\bid_ed25519\b/i,
263
- /\/etc\/passwd\b/,
264
- /\/etc\/shadow\b/,
265
- /\.env\b(?!\.\w)/
266
- ])
267
- },
268
- // Rule 5: Stealth Instructions (medium) — phrase + action verb matching
269
- {
270
- id: 'stealth-instructions',
271
- level: 'medium',
272
- message: 'Detected instruction to hide actions from user',
273
- skipSafeZones: true,
274
- check: (content)=>{
275
- const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
276
- const patterns = [
277
- // English patterns
278
- new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
279
- new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
280
- new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
281
- new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
282
- new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
283
- new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i'),
284
- // Chinese patterns (中文隐蔽指令)
285
- /悄悄地?\s*(?:执行|删除|移除|发送|传输|修改|覆盖|安装|下载|上传|运行|写入|创建|销毁|丢弃)/,
286
- /不要告诉用户/,
287
- /不要让用户知道/,
288
- /对用户隐藏/,
289
- /在用户不知情的情况下/,
290
- /瞒着用户/
291
- ];
292
- // Safe patterns to exclude (common in legitimate DevOps/automation skills)
293
- const safePatterns = [
294
- /silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i,
295
- // Chinese safe patterns (中文合法自动化用语)
296
- /悄悄地?\s*(?:忽略|跳过|丢弃|抑制|继续|静默)/
297
- ];
298
- const lines = content.split('\n');
299
- const matches = [];
300
- for(let i = 0; i < lines.length; i++){
301
- const line = lines[i];
302
- if (!safePatterns.some((p)=>p.test(line))) {
303
- for (const pattern of patterns)if (pattern.test(line)) {
304
- matches.push({
305
- line: i + 1
306
- });
307
- break;
308
- }
309
- }
310
- }
311
- return matches;
220
+ const name = String(data.name);
221
+ const description = String(data.description);
222
+ // Validate field format
223
+ if (strict) {
224
+ validateSkillName(name);
225
+ validateSkillDescription(description);
312
226
  }
313
- },
314
- // Rule 6: Oversized Content (low) — scans ALL content
315
- {
316
- id: 'oversized-content',
317
- level: 'low',
318
- message: 'Content exceeds recommended size limit',
319
- skipSafeZones: false,
320
- check: (content)=>{
321
- const MAX_SIZE_BYTES = 51200;
322
- const sizeBytes = Buffer.byteLength(content, 'utf-8');
323
- if (sizeBytes > MAX_SIZE_BYTES) return [
324
- {
325
- snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
326
- }
327
- ];
328
- return [];
227
+ // Parse allowed-tools
228
+ let allowedTools;
229
+ if (data['allowed-tools']) {
230
+ const toolsStr = String(data['allowed-tools']);
231
+ allowedTools = toolsStr.split(/\s+/).filter(Boolean);
329
232
  }
233
+ return {
234
+ name,
235
+ description,
236
+ version: data.version ? String(data.version) : void 0,
237
+ license: data.license ? String(data.license) : void 0,
238
+ compatibility: data.compatibility ? String(data.compatibility) : void 0,
239
+ metadata: data.metadata,
240
+ allowedTools,
241
+ content: body,
242
+ rawContent: content
243
+ };
244
+ } catch (error) {
245
+ if (error instanceof SkillValidationError) throw error;
246
+ if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
247
+ return null;
330
248
  }
331
- ];
332
- // ============================================================================
333
- // ContentScanner
334
- // ============================================================================
335
- /** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
336
- let rules = DEFAULT_RULES.map((r)=>({
337
- ...r
338
- }));
339
- if (options?.disabledRules?.length) {
340
- const disabled = new Set(options.disabledRules);
341
- rules = rules.filter((r)=>!disabled.has(r.id));
342
- }
343
- if (options?.overrides) for (const rule of rules){
344
- const override = options.overrides[rule.id];
345
- if (override) rule.level = override;
249
+ }
250
+ /**
251
+ * Parse SKILL.md from file path
252
+ */ function skill_parser_parseSkillMdFile(filePath, options = {}) {
253
+ if (!external_node_fs_.existsSync(filePath)) {
254
+ if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
255
+ return null;
346
256
  }
347
- if (options?.customRules?.length) rules.push(...options.customRules);
348
- return rules;
257
+ const content = external_node_fs_.readFileSync(filePath, 'utf-8');
258
+ return parseSkillMd(content, options);
349
259
  }
350
260
  /**
351
- * Content scanner for SKILL.md files.
352
- *
353
- * Detects prompt injection, data exfiltration, obfuscation, sensitive file
354
- * access, stealth instructions, and oversized content.
355
- *
356
- * @example
357
- * ```typescript
358
- * // Default usage (CLI)
359
- * const scanner = new ContentScanner();
360
- * const result = scanner.scan(content);
361
- *
362
- * // Custom usage (private registry server)
363
- * const scanner = new ContentScanner({
364
- * overrides: { 'prompt-injection': 'medium' },
365
- * disabledRules: ['stealth-instructions'],
366
- * });
367
- * ```
368
- */ class ContentScanner {
369
- rules;
370
- constructor(options){
371
- this.rules = buildRuleSet(options);
261
+ * Parse SKILL.md from skill directory
262
+ */ function parseSkillFromDir(dirPath, options = {}) {
263
+ const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
264
+ return skill_parser_parseSkillMdFile(skillMdPath, options);
265
+ }
266
+ /**
267
+ * Check if directory contains valid SKILL.md
268
+ */ function hasValidSkillMd(dirPath) {
269
+ const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
270
+ if (!external_node_fs_.existsSync(skillMdPath)) return false;
271
+ try {
272
+ const skill = skill_parser_parseSkillMdFile(skillMdPath);
273
+ return null !== skill;
274
+ } catch {
275
+ return false;
372
276
  }
373
- /**
374
- * Scan content string for malicious patterns.
375
- * Pure string operation — no file system access.
376
- */ scan(content) {
377
- const originalLines = content.split('\n');
378
- const maskedContent = maskSafeZones(content);
379
- const findings = [];
380
- for (const rule of this.rules){
381
- const targetContent = rule.skipSafeZones ? maskedContent : content;
382
- const matches = rule.check(targetContent);
383
- for (const match of matches){
384
- // Use custom snippet if provided, otherwise generate from original content
385
- const snippet = match.snippet ?? (null != match.line ? originalLines[match.line - 1]?.trim().slice(0, SNIPPET_MAX_LENGTH) : void 0);
386
- findings.push({
387
- rule: rule.id,
388
- level: rule.level,
389
- message: rule.message,
390
- line: match.line,
391
- snippet
392
- });
393
- }
394
- }
395
- const hasHighRisk = findings.some((f)=>'high' === f.level);
396
- return {
397
- passed: !hasHighRisk,
398
- findings
399
- };
277
+ }
278
+ const SKIP_DIRS = [
279
+ 'node_modules',
280
+ '.git',
281
+ 'dist',
282
+ 'build',
283
+ '__pycache__'
284
+ ];
285
+ const MAX_DISCOVER_DEPTH = 5;
286
+ const PRIORITY_SKILL_DIRS = [
287
+ 'skills',
288
+ '.agents/skills',
289
+ '.cursor/skills',
290
+ '.claude/skills',
291
+ '.windsurf/skills',
292
+ '.github/skills'
293
+ ];
294
+ function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
295
+ if (depth > maxDepth) return [];
296
+ const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
297
+ if (visitedDirs.has(resolvedDir)) return [];
298
+ if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
299
+ visitedDirs.add(resolvedDir);
300
+ const results = [];
301
+ let entries;
302
+ try {
303
+ entries = external_node_fs_.readdirSync(dir);
304
+ } catch {
305
+ return [];
400
306
  }
401
- /**
402
- * Scan a file for malicious patterns.
403
- * Convenience wrapper that reads the file then calls scan().
404
- */ scanFile(filePath) {
405
- if (!external_node_fs_.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
406
- const content = external_node_fs_.readFileSync(filePath, 'utf-8');
407
- return this.scan(content);
307
+ for (const entry of entries){
308
+ if (SKIP_DIRS.includes(entry)) continue;
309
+ const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
310
+ const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
311
+ if (visitedDirs.has(resolvedFull)) continue;
312
+ let stat;
313
+ try {
314
+ stat = external_node_fs_.statSync(fullPath);
315
+ } catch {
316
+ continue;
317
+ }
318
+ if (!!stat.isDirectory()) {
319
+ if (hasValidSkillMd(fullPath)) results.push(fullPath);
320
+ results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
321
+ }
408
322
  }
323
+ return results;
409
324
  }
410
- // ============================================================================
411
- // ContentScanError
412
- // ============================================================================
413
325
  /**
414
- * Error thrown when content scanning detects high-risk findings.
415
- * Carries the full findings array for display purposes.
416
- */ class ContentScanError extends Error {
417
- findings;
418
- constructor(findings){
419
- const highCount = findings.filter((f)=>'high' === f.level).length;
420
- super(`Content security scan failed: ${highCount} high-risk finding(s) detected`);
421
- this.name = 'ContentScanError';
422
- this.findings = findings;
326
+ * Discover all skills in a directory by scanning for SKILL.md files.
327
+ *
328
+ * Strategy:
329
+ * 1. Check root for SKILL.md
330
+ * 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
331
+ * 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
332
+ *
333
+ * @param basePath - Root directory to search
334
+ * @returns List of parsed skills with their directory paths (absolute)
335
+ */ function discoverSkillsInDir(basePath) {
336
+ const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
337
+ const results = [];
338
+ const seenNames = new Set();
339
+ function addSkill(dirPath) {
340
+ const skill = parseSkillFromDir(dirPath);
341
+ if (skill && !seenNames.has(skill.name)) {
342
+ seenNames.add(skill.name);
343
+ results.push({
344
+ ...skill,
345
+ dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
346
+ });
347
+ }
348
+ }
349
+ if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
350
+ // Track visited directories to avoid redundant I/O during recursive scan
351
+ const visitedDirs = new Set();
352
+ for (const sub of PRIORITY_SKILL_DIRS){
353
+ const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
354
+ if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
355
+ visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
356
+ try {
357
+ const entries = external_node_fs_.readdirSync(dir);
358
+ for (const entry of entries){
359
+ const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
360
+ try {
361
+ if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
362
+ addSkill(skillDir);
363
+ visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
364
+ }
365
+ } catch {
366
+ // Skip entries that can't be stat'd (race condition, permission, etc.)
367
+ }
368
+ }
369
+ } catch {
370
+ // Skip if unreadable
371
+ }
372
+ }
423
373
  }
374
+ const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
375
+ for (const skillDir of recursiveDirs)addSkill(skillDir);
376
+ return results;
424
377
  }
425
378
  /**
426
- * Agent Registry - Multi-Agent configuration definitions
379
+ * Filter skills by name (case-insensitive exact match).
427
380
  *
428
- * Supports global and project-level installation for 17 coding agents
429
- * Reference: https://github.com/vercel-labs/add-skill
430
- */ const agent_registry_home = (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
381
+ * Note: an empty `names` array returns an empty result (not all skills).
382
+ * Callers should check `names.length` before calling if "no filter = all" is desired.
383
+ *
384
+ * @param skills - List of discovered skills
385
+ * @param names - Skill names to match (e.g. from --skill pdf commit)
386
+ * @returns Skills whose name matches any of the given names
387
+ */ function filterSkillsByName(skills, names) {
388
+ const normalized = names.map((n)=>n.toLowerCase());
389
+ return skills.filter((skill)=>{
390
+ // Match against SKILL.md name field
391
+ if (normalized.includes(skill.name.toLowerCase())) return true;
392
+ // Also match against the directory name (basename of dirPath)
393
+ // Users naturally refer to skills by their directory name
394
+ const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
395
+ return normalized.includes(dirName);
396
+ });
397
+ }
431
398
  /**
432
- * All supported Agents configuration
433
- */ const agents = {
434
- amp: {
435
- name: 'amp',
436
- displayName: 'Amp',
437
- skillsDir: '.agents/skills',
438
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/agents/skills'),
439
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/amp'))
440
- },
441
- antigravity: {
442
- name: 'antigravity',
443
- displayName: 'Antigravity',
444
- skillsDir: '.agent/skills',
445
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini/antigravity/skills'),
446
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(process.cwd(), '.agent')) || (0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini/antigravity'))
447
- },
448
- 'claude-code': {
449
- name: 'claude-code',
450
- displayName: 'Claude Code',
451
- skillsDir: '.claude/skills',
452
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude/skills'),
453
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude'))
454
- },
455
- clawdbot: {
456
- name: 'clawdbot',
457
- displayName: 'Clawdbot',
458
- skillsDir: 'skills',
459
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.clawdbot/skills'),
460
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.clawdbot'))
461
- },
462
- codex: {
463
- name: 'codex',
464
- displayName: 'Codex',
465
- skillsDir: '.codex/skills',
466
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.codex/skills'),
467
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.codex'))
468
- },
469
- cursor: {
470
- name: 'cursor',
471
- displayName: 'Cursor',
472
- skillsDir: '.cursor/skills',
473
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.cursor/skills'),
474
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.cursor'))
475
- },
476
- droid: {
477
- name: 'droid',
478
- displayName: 'Droid',
479
- skillsDir: '.factory/skills',
480
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.factory/skills'),
481
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.factory/skills'))
482
- },
483
- 'gemini-cli': {
484
- name: 'gemini-cli',
485
- displayName: 'Gemini CLI',
486
- skillsDir: '.gemini/skills',
487
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini/skills'),
488
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini'))
489
- },
490
- 'github-copilot': {
491
- name: 'github-copilot',
492
- displayName: 'GitHub Copilot',
493
- skillsDir: '.github/skills',
494
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.copilot/skills'),
495
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(process.cwd(), '.github')) || (0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.copilot'))
496
- },
497
- goose: {
498
- name: 'goose',
499
- displayName: 'Goose',
500
- skillsDir: '.goose/skills',
501
- globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/goose/skills'),
502
- detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/goose'))
503
- },
504
- kilo: {
505
- name: 'kilo',
399
+ * Generate SKILL.md content
400
+ */ function generateSkillMd(skill) {
401
+ const frontmatter = [
402
+ '---'
403
+ ];
404
+ frontmatter.push(`name: ${skill.name}`);
405
+ frontmatter.push(`description: ${skill.description}`);
406
+ if (skill.license) frontmatter.push(`license: ${skill.license}`);
407
+ if (skill.compatibility) frontmatter.push(`compatibility: ${skill.compatibility}`);
408
+ if (skill.allowedTools && skill.allowedTools.length > 0) frontmatter.push(`allowed-tools: ${skill.allowedTools.join(' ')}`);
409
+ frontmatter.push('---');
410
+ frontmatter.push('');
411
+ return frontmatter.join('\n') + skill.content;
412
+ }
413
+ const CLAUDE_COWORK_3P_AGENT = 'claude-cowork-3p';
414
+ const CLAUDE_3P_SKILLS_ROOT_ENV = 'CLAUDE_3P_SKILLS_ROOT';
415
+ const CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV = 'CLAUDE_3P_SKILLS_PLUGIN_BASE';
416
+ const DEFAULT_EXCLUDE_FILES = [
417
+ 'README.md',
418
+ 'metadata.json',
419
+ '.reskill-commit'
420
+ ];
421
+ const EXCLUDE_PREFIX = '_';
422
+ const MAX_MANIFEST_BACKUPS = 10;
423
+ function exists(targetPath) {
424
+ return external_node_fs_.existsSync(targetPath);
425
+ }
426
+ function ensureDir(dirPath) {
427
+ if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
428
+ recursive: true
429
+ });
430
+ }
431
+ function remove(targetPath) {
432
+ if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
433
+ recursive: true,
434
+ force: true
435
+ });
436
+ }
437
+ function isSafeSkillId(name) {
438
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && '.' !== name && '..' !== name;
439
+ }
440
+ function isPathSafe(basePath, targetPath) {
441
+ const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
442
+ const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
443
+ return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep);
444
+ }
445
+ function getSafeSkillPath(root, skillName) {
446
+ if (!isSafeSkillId(skillName)) throw new Error(`Skill name is not safe for Claude Cowork 3P: ${skillName}`);
447
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
448
+ const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, skillName);
449
+ if (!isPathSafe(skillsDir, skillPath)) throw new Error(`Skill path escapes Claude Cowork 3P skills directory: ${skillName}`);
450
+ return skillPath;
451
+ }
452
+ function isSkillsRoot(root) {
453
+ return exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json')) && exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills'));
454
+ }
455
+ function copyDirectory(src, dest) {
456
+ ensureDir(dest);
457
+ for (const entry of external_node_fs_.readdirSync(src, {
458
+ withFileTypes: true
459
+ })){
460
+ if (DEFAULT_EXCLUDE_FILES.includes(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
461
+ const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
462
+ const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
463
+ if (entry.isDirectory()) copyDirectory(srcPath, destPath);
464
+ else if (entry.isFile()) external_node_fs_.copyFileSync(srcPath, destPath);
465
+ }
466
+ }
467
+ function getClaude3pSkillsPluginBase(options = {}) {
468
+ const env = options.env ?? process.env;
469
+ const explicitBase = env[CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV];
470
+ if (explicitBase) return explicitBase;
471
+ const homeDir = options.homeDir ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
472
+ const currentPlatform = options.platform ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.platform)();
473
+ if ('darwin' === currentPlatform) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, 'Library', 'Application Support', 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
474
+ if ('win32' === currentPlatform) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(env.APPDATA ?? __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, 'AppData', 'Roaming'), 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
475
+ return __WEBPACK_EXTERNAL_MODULE_node_path__.join(env.XDG_CONFIG_HOME ?? __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, '.config'), 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
476
+ }
477
+ function findClaude3pSkillsRoots(options = {}) {
478
+ const env = options.env ?? process.env;
479
+ const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
480
+ if (explicitRoot) return isSkillsRoot(explicitRoot) ? [
481
+ explicitRoot
482
+ ] : [];
483
+ const base = getClaude3pSkillsPluginBase(options);
484
+ if (!exists(base)) return [];
485
+ const roots = [];
486
+ for (const organization of external_node_fs_.readdirSync(base, {
487
+ withFileTypes: true
488
+ })){
489
+ if (!organization.isDirectory()) continue;
490
+ const organizationPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(base, organization.name);
491
+ for (const account of external_node_fs_.readdirSync(organizationPath, {
492
+ withFileTypes: true
493
+ })){
494
+ if (!account.isDirectory()) continue;
495
+ const candidate = __WEBPACK_EXTERNAL_MODULE_node_path__.join(organizationPath, account.name);
496
+ if (isSkillsRoot(candidate)) roots.push(candidate);
497
+ }
498
+ }
499
+ return roots;
500
+ }
501
+ function resolveClaude3pSkillsRoot(options = {}) {
502
+ const env = options.env ?? process.env;
503
+ const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
504
+ if (explicitRoot) {
505
+ if (!isSkillsRoot(explicitRoot)) throw new Error(`${CLAUDE_3P_SKILLS_ROOT_ENV} must contain manifest.json and skills/: ${explicitRoot}`);
506
+ return explicitRoot;
507
+ }
508
+ const roots = findClaude3pSkillsRoots(options);
509
+ if (1 === roots.length) return roots[0];
510
+ const base = getClaude3pSkillsPluginBase(options);
511
+ if (0 === roots.length) throw new Error(`Claude Cowork 3P skills root not found under ${base}. Set ${CLAUDE_3P_SKILLS_ROOT_ENV} to the account directory containing manifest.json and skills/.`);
512
+ throw new Error(`Multiple Claude Cowork 3P skills roots found. Set ${CLAUDE_3P_SKILLS_ROOT_ENV} to one of:\n${roots.map((root)=>` ${root}`).join('\n')}`);
513
+ }
514
+ function getClaude3pSkillPath(skillName) {
515
+ const root = resolveClaude3pSkillsRoot();
516
+ return getSafeSkillPath(root, skillName);
517
+ }
518
+ function readManifest(manifestPath) {
519
+ const content = external_node_fs_.readFileSync(manifestPath, 'utf-8');
520
+ return JSON.parse(content);
521
+ }
522
+ function writeManifest(manifestPath, manifest) {
523
+ external_node_fs_.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
524
+ }
525
+ function createManifestBackup(manifestPath) {
526
+ const backupPath = `${manifestPath}.bak.${Date.now()}`;
527
+ external_node_fs_.copyFileSync(manifestPath, backupPath);
528
+ return backupPath;
529
+ }
530
+ function pruneManifestBackups(manifestPath) {
531
+ const manifestDir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(manifestPath);
532
+ const backupPrefix = `${__WEBPACK_EXTERNAL_MODULE_node_path__.basename(manifestPath)}.bak.`;
533
+ const backups = external_node_fs_.readdirSync(manifestDir, {
534
+ withFileTypes: true
535
+ }).filter((entry)=>entry.isFile() && entry.name.startsWith(backupPrefix)).map((entry)=>({
536
+ name: entry.name,
537
+ path: __WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name),
538
+ mtimeMs: external_node_fs_.statSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name)).mtimeMs
539
+ })).sort((a, b)=>b.mtimeMs - a.mtimeMs);
540
+ for (const backup of backups.slice(MAX_MANIFEST_BACKUPS))remove(backup.path);
541
+ }
542
+ function updateManifest(manifestPath, skillId, name, description) {
543
+ const manifest = readManifest(manifestPath);
544
+ if (!Array.isArray(manifest.skills)) manifest.skills = [];
545
+ const entry = {
546
+ skillId,
547
+ name,
548
+ description,
549
+ creatorType: 'user',
550
+ syncManaged: false,
551
+ updatedAt: new Date().toISOString(),
552
+ enabled: true
553
+ };
554
+ const index = manifest.skills.findIndex((skill)=>skill.skillId === skillId || skill.name === name);
555
+ if (index >= 0) manifest.skills[index] = {
556
+ ...manifest.skills[index],
557
+ ...entry
558
+ };
559
+ else manifest.skills.push(entry);
560
+ manifest.lastUpdated = Date.now();
561
+ writeManifest(manifestPath, manifest);
562
+ }
563
+ function removeFromManifest(manifestPath, skillId) {
564
+ const manifest = readManifest(manifestPath);
565
+ if (!Array.isArray(manifest.skills)) return;
566
+ const before = manifest.skills.length;
567
+ manifest.skills = manifest.skills.filter((skill)=>skill.skillId !== skillId && skill.name !== skillId);
568
+ if (manifest.skills.length !== before) {
569
+ manifest.lastUpdated = Date.now();
570
+ writeManifest(manifestPath, manifest);
571
+ }
572
+ }
573
+ function installClaude3pSkill(sourcePath, fallbackName, _options = {}) {
574
+ const installMode = 'copy';
575
+ let manifestPath;
576
+ let manifestBackupPath;
577
+ let targetPath;
578
+ let tmpTarget;
579
+ let rollbackTarget;
580
+ try {
581
+ const metadata = parseSkillFromDir(sourcePath);
582
+ const skillId = metadata?.name ?? fallbackName;
583
+ const description = metadata?.description ?? '';
584
+ if (!isSafeSkillId(skillId)) return {
585
+ success: false,
586
+ path: '',
587
+ mode: installMode,
588
+ error: `SKILL.md name is not safe for Claude Cowork 3P: ${skillId}`
589
+ };
590
+ const root = resolveClaude3pSkillsRoot();
591
+ manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
592
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
593
+ targetPath = getSafeSkillPath(root, skillId);
594
+ tmpTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.installing.${process.pid}`);
595
+ rollbackTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.rollback.${process.pid}`);
596
+ if (!isPathSafe(skillsDir, tmpTarget)) throw new Error(`Temporary skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
597
+ if (!isPathSafe(skillsDir, rollbackTarget)) throw new Error(`Rollback skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
598
+ manifestBackupPath = createManifestBackup(manifestPath);
599
+ remove(tmpTarget);
600
+ remove(rollbackTarget);
601
+ copyDirectory(sourcePath, tmpTarget);
602
+ if (exists(targetPath)) external_node_fs_.renameSync(targetPath, rollbackTarget);
603
+ external_node_fs_.renameSync(tmpTarget, targetPath);
604
+ try {
605
+ updateManifest(manifestPath, skillId, skillId, description);
606
+ } catch (error) {
607
+ remove(targetPath);
608
+ if (rollbackTarget && exists(rollbackTarget)) external_node_fs_.renameSync(rollbackTarget, targetPath);
609
+ if (manifestBackupPath && manifestPath && exists(manifestBackupPath)) external_node_fs_.copyFileSync(manifestBackupPath, manifestPath);
610
+ throw error;
611
+ }
612
+ if (rollbackTarget) remove(rollbackTarget);
613
+ pruneManifestBackups(manifestPath);
614
+ return {
615
+ success: true,
616
+ path: targetPath,
617
+ mode: installMode
618
+ };
619
+ } catch (error) {
620
+ if (tmpTarget) remove(tmpTarget);
621
+ if (targetPath && rollbackTarget && exists(rollbackTarget) && !exists(targetPath)) try {
622
+ external_node_fs_.renameSync(rollbackTarget, targetPath);
623
+ } catch {
624
+ // Ignore rollback errors while reporting the original installation failure.
625
+ }
626
+ if (manifestPath) try {
627
+ pruneManifestBackups(manifestPath);
628
+ } catch {
629
+ // Ignore cleanup errors while reporting the original installation failure.
630
+ }
631
+ return {
632
+ success: false,
633
+ path: '',
634
+ mode: installMode,
635
+ error: error instanceof Error ? error.message : 'Unknown error'
636
+ };
637
+ }
638
+ }
639
+ function uninstallClaude3pSkill(skillName) {
640
+ const root = resolveClaude3pSkillsRoot();
641
+ const targetPath = getSafeSkillPath(root, skillName);
642
+ const manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
643
+ const existed = exists(targetPath);
644
+ if (existed) remove(targetPath);
645
+ removeFromManifest(manifestPath, skillName);
646
+ return existed;
647
+ }
648
+ function listClaude3pSkills() {
649
+ const root = resolveClaude3pSkillsRoot();
650
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
651
+ if (!exists(skillsDir)) return [];
652
+ return external_node_fs_.readdirSync(skillsDir, {
653
+ withFileTypes: true
654
+ }).filter((entry)=>entry.isDirectory() && isSafeSkillId(entry.name)).map((entry)=>entry.name);
655
+ }
656
+ /**
657
+ * Agent Registry - Multi-Agent configuration definitions
658
+ *
659
+ * Supports global and project-level installation for 18 coding agents
660
+ * Reference: https://github.com/vercel-labs/add-skill
661
+ */ const agent_registry_home = (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
662
+ /**
663
+ * All supported Agents configuration
664
+ */ const agents = {
665
+ amp: {
666
+ name: 'amp',
667
+ displayName: 'Amp',
668
+ skillsDir: '.agents/skills',
669
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/agents/skills'),
670
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/amp'))
671
+ },
672
+ antigravity: {
673
+ name: 'antigravity',
674
+ displayName: 'Antigravity',
675
+ skillsDir: '.agent/skills',
676
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini/antigravity/skills'),
677
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(process.cwd(), '.agent')) || (0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini/antigravity'))
678
+ },
679
+ 'claude-code': {
680
+ name: 'claude-code',
681
+ displayName: 'Claude Code',
682
+ skillsDir: '.claude/skills',
683
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude/skills'),
684
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude'))
685
+ },
686
+ [CLAUDE_COWORK_3P_AGENT]: {
687
+ name: CLAUDE_COWORK_3P_AGENT,
688
+ displayName: 'Claude Cowork 3P',
689
+ skillsDir: '.claude-3p/skills',
690
+ globalSkillsDir: getClaude3pSkillsPluginBase(),
691
+ detectInstalled: async ()=>{
692
+ try {
693
+ resolveClaude3pSkillsRoot();
694
+ return true;
695
+ } catch {
696
+ return false;
697
+ }
698
+ }
699
+ },
700
+ clawdbot: {
701
+ name: 'clawdbot',
702
+ displayName: 'Clawdbot',
703
+ skillsDir: 'skills',
704
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.clawdbot/skills'),
705
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.clawdbot'))
706
+ },
707
+ codex: {
708
+ name: 'codex',
709
+ displayName: 'Codex',
710
+ skillsDir: '.codex/skills',
711
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.codex/skills'),
712
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.codex'))
713
+ },
714
+ cursor: {
715
+ name: 'cursor',
716
+ displayName: 'Cursor',
717
+ skillsDir: '.cursor/skills',
718
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.cursor/skills'),
719
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.cursor'))
720
+ },
721
+ droid: {
722
+ name: 'droid',
723
+ displayName: 'Droid',
724
+ skillsDir: '.factory/skills',
725
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.factory/skills'),
726
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.factory/skills'))
727
+ },
728
+ 'gemini-cli': {
729
+ name: 'gemini-cli',
730
+ displayName: 'Gemini CLI',
731
+ skillsDir: '.gemini/skills',
732
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini/skills'),
733
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.gemini'))
734
+ },
735
+ 'github-copilot': {
736
+ name: 'github-copilot',
737
+ displayName: 'GitHub Copilot',
738
+ skillsDir: '.github/skills',
739
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.copilot/skills'),
740
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(process.cwd(), '.github')) || (0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.copilot'))
741
+ },
742
+ goose: {
743
+ name: 'goose',
744
+ displayName: 'Goose',
745
+ skillsDir: '.goose/skills',
746
+ globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/goose/skills'),
747
+ detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.config/goose'))
748
+ },
749
+ kilo: {
750
+ name: 'kilo',
506
751
  displayName: 'Kilo Code',
507
752
  skillsDir: '.kilocode/skills',
508
753
  globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.kilocode/skills'),
@@ -575,8 +820,13 @@ const SNIPPET_MAX_LENGTH = 120;
575
820
  }
576
821
  /**
577
822
  * Get Agent's project-level skills directory
823
+ *
824
+ * Claude Cowork 3P stores skills under an app-managed account directory. This
825
+ * throws when that directory cannot be resolved, for example when the app has
826
+ * not initialized skills or multiple local account roots exist.
578
827
  */ function getAgentSkillsDir(type, options = {}) {
579
828
  const config = agents[type];
829
+ if (type === CLAUDE_COWORK_3P_AGENT) return (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(resolveClaude3pSkillsRoot(), 'skills');
580
830
  if (options.global) return config.globalSkillsDir;
581
831
  const cwd = options.cwd || process.cwd();
582
832
  return (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(cwd, config.skillsDir);
@@ -585,7 +835,7 @@ const SNIPPET_MAX_LENGTH = 120;
585
835
  * File system utilities
586
836
  */ /**
587
837
  * Check if a file or directory exists
588
- */ function exists(filePath) {
838
+ */ function fs_exists(filePath) {
589
839
  return external_node_fs_.existsSync(filePath);
590
840
  }
591
841
  /**
@@ -598,22 +848,22 @@ const SNIPPET_MAX_LENGTH = 120;
598
848
  * Write JSON file
599
849
  */ function writeJson(filePath, data, indent = 2) {
600
850
  const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
601
- if (!exists(dir)) external_node_fs_.mkdirSync(dir, {
851
+ if (!fs_exists(dir)) external_node_fs_.mkdirSync(dir, {
602
852
  recursive: true
603
853
  });
604
854
  external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
605
855
  }
606
856
  /**
607
857
  * Create directory recursively
608
- */ function ensureDir(dirPath) {
609
- if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
858
+ */ function fs_ensureDir(dirPath) {
859
+ if (!fs_exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
610
860
  recursive: true
611
861
  });
612
862
  }
613
863
  /**
614
864
  * Remove file or directory
615
- */ function remove(targetPath) {
616
- if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
865
+ */ function fs_remove(targetPath) {
866
+ if (fs_exists(targetPath)) external_node_fs_.rmSync(targetPath, {
617
867
  recursive: true,
618
868
  force: true
619
869
  });
@@ -628,7 +878,7 @@ const SNIPPET_MAX_LENGTH = 120;
628
878
  */ function copyDir(src, dest, options) {
629
879
  const exclude = options?.exclude || [];
630
880
  const excludePrefix = options?.excludePrefix || '_';
631
- ensureDir(dest);
881
+ fs_ensureDir(dest);
632
882
  const entries = external_node_fs_.readdirSync(src, {
633
883
  withFileTypes: true
634
884
  });
@@ -644,19 +894,19 @@ const SNIPPET_MAX_LENGTH = 120;
644
894
  /**
645
895
  * List directory contents
646
896
  */ function listDir(dirPath) {
647
- if (!exists(dirPath)) return [];
897
+ if (!fs_exists(dirPath)) return [];
648
898
  return external_node_fs_.readdirSync(dirPath);
649
899
  }
650
900
  /**
651
901
  * Check if path is a directory
652
902
  */ function isDirectory(targetPath) {
653
- if (!exists(targetPath)) return false;
903
+ if (!fs_exists(targetPath)) return false;
654
904
  return external_node_fs_.statSync(targetPath).isDirectory();
655
905
  }
656
906
  /**
657
907
  * Check if path is a symbolic link
658
908
  */ function isSymlink(targetPath) {
659
- if (!exists(targetPath)) return false;
909
+ if (!fs_exists(targetPath)) return false;
660
910
  return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
661
911
  }
662
912
  /**
@@ -720,7 +970,7 @@ const SKILLS_SUBDIR = 'skills';
720
970
  }
721
971
  /**
722
972
  * Validate path safety (prevent path traversal attacks)
723
- */ function isPathSafe(basePath, targetPath) {
973
+ */ function fs_isPathSafe(basePath, targetPath) {
724
974
  const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
725
975
  const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
726
976
  return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep) || normalizedTarget === normalizedBase;
@@ -1028,7 +1278,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1028
1278
  */ async function downloadFile(url, destPath, options = {}) {
1029
1279
  const { timeout = 60000, headers = {} } = options;
1030
1280
  // Ensure destination directory exists
1031
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
1281
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
1032
1282
  try {
1033
1283
  // Use native fetch for HTTP/HTTPS
1034
1284
  const controller = new AbortController();
@@ -1070,7 +1320,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1070
1320
  const detectedFormat = format || detectArchiveFormat(archivePath);
1071
1321
  if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
1072
1322
  // Ensure destination directory exists
1073
- ensureDir(destDir);
1323
+ fs_ensureDir(destDir);
1074
1324
  switch(detectedFormat){
1075
1325
  case 'tar.gz':
1076
1326
  case 'tgz':
@@ -1094,7 +1344,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1094
1344
  try {
1095
1345
  // Extract to a temp directory first to handle single-folder archives
1096
1346
  const tempExtractDir = `${destDir}.extract-temp`;
1097
- ensureDir(tempExtractDir);
1347
+ fs_ensureDir(tempExtractDir);
1098
1348
  await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
1099
1349
  encoding: 'utf-8'
1100
1350
  });
@@ -1110,7 +1360,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1110
1360
  const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1111
1361
  external_node_fs_.renameSync(src, dest);
1112
1362
  }
1113
- remove(tempExtractDir);
1363
+ fs_remove(tempExtractDir);
1114
1364
  return;
1115
1365
  }
1116
1366
  }
@@ -1120,7 +1370,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1120
1370
  const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1121
1371
  external_node_fs_.renameSync(src, dest);
1122
1372
  }
1123
- remove(tempExtractDir);
1373
+ fs_remove(tempExtractDir);
1124
1374
  } catch (error) {
1125
1375
  throw new Error(`Failed to extract tar archive: ${error.message}`);
1126
1376
  }
@@ -1134,7 +1384,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1134
1384
  try {
1135
1385
  // Extract to a temp directory first
1136
1386
  const tempExtractDir = `${destDir}.extract-temp`;
1137
- ensureDir(tempExtractDir);
1387
+ fs_ensureDir(tempExtractDir);
1138
1388
  // Try using unzip command (available on most systems)
1139
1389
  await execAsync(`unzip -q "${archivePath}" -d "${tempExtractDir}"`, {
1140
1390
  encoding: 'utf-8'
@@ -1151,7 +1401,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1151
1401
  const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1152
1402
  external_node_fs_.renameSync(src, dest);
1153
1403
  }
1154
- remove(tempExtractDir);
1404
+ fs_remove(tempExtractDir);
1155
1405
  return;
1156
1406
  }
1157
1407
  }
@@ -1161,7 +1411,7 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1161
1411
  const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1162
1412
  external_node_fs_.renameSync(src, dest);
1163
1413
  }
1164
- remove(tempExtractDir);
1414
+ fs_remove(tempExtractDir);
1165
1415
  } catch (error) {
1166
1416
  throw new Error(`Failed to extract zip archive: ${error.message}`);
1167
1417
  }
@@ -1202,514 +1452,136 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1202
1452
  }
1203
1453
  }
1204
1454
  /**
1205
- * Skill Parser - SKILL.md parser
1455
+ * Installer - Multi-Agent installer
1206
1456
  *
1207
- * Following agentskills.io specification: https://agentskills.io/specification
1457
+ * Supports two installation modes:
1458
+ * - symlink: Canonical location (.agents/skills/) + symlinks to each agent directory
1459
+ * - copy: Direct copy to each agent directory
1208
1460
  *
1209
- * SKILL.md format requirements:
1210
- * - YAML frontmatter containing name and description (required)
1211
- * - name: max 64 characters, lowercase letters, numbers, hyphens
1212
- * - description: max 1024 characters
1213
- * - Optional fields: license, compatibility, metadata, allowed-tools
1214
- */ /**
1215
- * Skill validation error
1216
- */ class SkillValidationError extends Error {
1217
- field;
1218
- constructor(message, field){
1219
- super(message), this.field = field;
1220
- this.name = 'SkillValidationError';
1221
- }
1461
+ * Reference: https://github.com/vercel-labs/add-skill/blob/main/src/installer.ts
1462
+ */ const installer_AGENTS_DIR = '.agents';
1463
+ const installer_SKILLS_SUBDIR = 'skills';
1464
+ /**
1465
+ * Marker comment in auto-generated Cursor bridge rule files.
1466
+ * Used to distinguish auto-generated files from manually created ones.
1467
+ */ const CURSOR_BRIDGE_MARKER = '<!-- reskill:auto-generated -->';
1468
+ /**
1469
+ * Default files to exclude when copying skills
1470
+ * These files are typically used for repository metadata and should not be copied to agent directories
1471
+ */ const installer_DEFAULT_EXCLUDE_FILES = [
1472
+ 'README.md',
1473
+ 'metadata.json',
1474
+ '.reskill-commit'
1475
+ ];
1476
+ /**
1477
+ * Prefix for files that should be excluded (internal/private files)
1478
+ */ const installer_EXCLUDE_PREFIX = '_';
1479
+ /**
1480
+ * Sanitize filename to prevent path traversal attacks
1481
+ */ function installer_sanitizeName(name) {
1482
+ // Remove path separators and special characters
1483
+ let sanitized = name.replace(/[/\\:\0]/g, '');
1484
+ // Remove leading and trailing dots and spaces
1485
+ sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, '');
1486
+ // Remove leading dots
1487
+ sanitized = sanitized.replace(/^\.+/, '');
1488
+ if (!sanitized || 0 === sanitized.length) sanitized = 'unnamed-skill';
1489
+ if (sanitized.length > 255) sanitized = sanitized.substring(0, 255);
1490
+ return sanitized;
1222
1491
  }
1223
1492
  /**
1224
- * Simple YAML frontmatter parser
1225
- * Parses --- delimited YAML header
1493
+ * Validate path safety
1494
+ */ function installer_isPathSafe(basePath, targetPath) {
1495
+ const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
1496
+ const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
1497
+ return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep) || normalizedTarget === normalizedBase;
1498
+ }
1499
+ /**
1500
+ * Get canonical skills directory path
1226
1501
  *
1227
- * Supports:
1228
- * - Basic key: value pairs
1229
- * - Multiline strings (| and >)
1230
- * - Nested objects (one level deep, for metadata field)
1231
- */ function parseFrontmatter(content) {
1232
- const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
1233
- const match = content.match(frontmatterRegex);
1234
- if (!match) return {
1235
- data: {},
1236
- content
1237
- };
1238
- const yamlContent = match[1];
1239
- const markdownContent = match[2];
1240
- // Simple YAML parsing (supports basic key: value, one level of nesting,
1241
- // block scalars (| and >), and plain scalars spanning multiple indented lines)
1242
- const data = {};
1243
- const lines = yamlContent.split('\n');
1244
- let currentKey = '';
1245
- let currentValue = '';
1246
- let inMultiline = false;
1247
- let inNestedObject = false;
1248
- let inPlainScalar = false;
1249
- let nestedObject = {};
1250
- /**
1251
- * Save the current key/value accumulated so far, then reset state.
1252
- */ function flushCurrent() {
1253
- if (!currentKey) return;
1254
- if (inNestedObject) {
1255
- data[currentKey] = nestedObject;
1256
- nestedObject = {};
1257
- inNestedObject = false;
1258
- } else if (inPlainScalar || inMultiline) {
1259
- data[currentKey] = currentValue.trim();
1260
- inPlainScalar = false;
1261
- inMultiline = false;
1262
- } else data[currentKey] = parseYamlValue(currentValue.trim());
1263
- currentKey = '';
1264
- currentValue = '';
1265
- }
1266
- for (const line of lines){
1267
- const trimmedLine = line.trim();
1268
- if (!trimmedLine || trimmedLine.startsWith('#')) continue;
1269
- const isIndented = line.startsWith(' ');
1270
- // ---- Inside a block scalar (| or >) ----
1271
- if (inMultiline) {
1272
- if (isIndented) {
1273
- currentValue += (currentValue ? '\n' : '') + line.slice(2);
1274
- continue;
1275
- }
1276
- // Unindented line ends the block scalar — fall through to top-level parsing
1277
- flushCurrent();
1278
- }
1279
- // ---- Inside a plain scalar (multiline value without | or >) ----
1280
- if (inPlainScalar) {
1281
- if (isIndented) {
1282
- // Continuation line: join with a space (YAML plain scalar folding)
1283
- currentValue += ` ${trimmedLine}`;
1284
- continue;
1285
- }
1286
- // Unindented line ends the plain scalar — fall through to top-level parsing
1287
- flushCurrent();
1288
- }
1289
- // ---- Inside a nested object ----
1290
- if (inNestedObject && isIndented) {
1291
- const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
1292
- if (nestedMatch) {
1293
- const [, nestedKey, nestedValue] = nestedMatch;
1294
- nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
1295
- continue;
1296
- }
1297
- // Indented line that isn't a nested key:value — this key was actually
1298
- // a plain scalar, not a nested object. Switch modes.
1299
- inNestedObject = false;
1300
- inPlainScalar = true;
1301
- currentValue = trimmedLine;
1302
- continue;
1303
- }
1304
- // ---- Top-level key: value ----
1305
- const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
1306
- if (keyValueMatch) {
1307
- flushCurrent();
1308
- currentKey = keyValueMatch[1];
1309
- currentValue = keyValueMatch[2];
1310
- if ('|' === currentValue || '>' === currentValue) {
1311
- inMultiline = true;
1312
- currentValue = '';
1313
- } else if ('' === currentValue) {
1314
- // Empty value — could be nested object or plain scalar; peek at next lines
1315
- inNestedObject = true;
1316
- nestedObject = {};
1317
- }
1318
- continue;
1319
- }
1320
- // ---- Unindented line that isn't key:value while in nested object ----
1321
- if (inNestedObject) flushCurrent();
1322
- }
1323
- // Save last accumulated value
1324
- flushCurrent();
1325
- return {
1326
- data,
1327
- content: markdownContent
1328
- };
1502
+ * @param isGlobal - Whether installing globally
1503
+ * @param cwd - Current working directory
1504
+ * @param installDir - Custom installation directory (relative to cwd), overrides default
1505
+ */ function installer_getCanonicalSkillsDir(isGlobal, cwd, installDir) {
1506
+ const baseDir = isGlobal ? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)() : cwd || process.cwd();
1507
+ // Use custom installDir if provided, otherwise use default
1508
+ if (installDir && !isGlobal) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, installDir);
1509
+ return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, installer_AGENTS_DIR, installer_SKILLS_SUBDIR);
1329
1510
  }
1330
1511
  /**
1331
- * Parse YAML value
1332
- */ function parseYamlValue(value) {
1333
- if (!value) return '';
1334
- // Boolean value
1335
- if ('true' === value) return true;
1336
- if ('false' === value) return false;
1337
- // Number
1338
- if (/^-?\d+$/.test(value)) return parseInt(value, 10);
1339
- if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
1340
- // Remove quotes
1341
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
1342
- return value;
1512
+ * Ensure directory exists
1513
+ */ function installer_ensureDir(dirPath) {
1514
+ if (!external_node_fs_.existsSync(dirPath)) external_node_fs_.mkdirSync(dirPath, {
1515
+ recursive: true
1516
+ });
1343
1517
  }
1344
1518
  /**
1345
- * Validate skill name format
1346
- *
1347
- * Specification requirements:
1348
- * - Max 64 characters
1349
- * - Only lowercase letters, numbers, hyphens allowed
1350
- * - Cannot start or end with hyphen
1351
- * - Cannot contain consecutive hyphens
1352
- */ function validateSkillName(name) {
1353
- if (!name) throw new SkillValidationError('Skill name is required', 'name');
1354
- if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
1355
- if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
1356
- if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
1357
- if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
1358
- if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) && name.length > 1) throw new SkillValidationError('Skill name can only contain lowercase letters, numbers, and hyphens', 'name');
1359
- // Single character name
1360
- if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
1519
+ * Remove file or directory
1520
+ */ function installer_remove(targetPath) {
1521
+ if (external_node_fs_.existsSync(targetPath)) external_node_fs_.rmSync(targetPath, {
1522
+ recursive: true,
1523
+ force: true
1524
+ });
1361
1525
  }
1362
1526
  /**
1363
- * Validate skill description
1527
+ * Copy directory with file exclusion
1364
1528
  *
1365
- * Specification requirements:
1366
- * - Max 1024 characters
1367
- * - Angle brackets are allowed per agentskills.io spec
1368
- */ function validateSkillDescription(description) {
1369
- if (!description) throw new SkillValidationError('Skill description is required', 'description');
1370
- if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
1371
- // Note: angle brackets are allowed per agentskills.io spec
1529
+ * By default excludes:
1530
+ * - Files in DEFAULT_EXCLUDE_FILES (README.md, metadata.json, .reskill-commit)
1531
+ * - Files starting with EXCLUDE_PREFIX ('_')
1532
+ */ function installer_copyDirectory(src, dest, options) {
1533
+ const exclude = new Set(options?.exclude || installer_DEFAULT_EXCLUDE_FILES);
1534
+ installer_ensureDir(dest);
1535
+ const entries = external_node_fs_.readdirSync(src, {
1536
+ withFileTypes: true
1537
+ });
1538
+ for (const entry of entries){
1539
+ // Skip files starting with EXCLUDE_PREFIX and files in exclude list
1540
+ if (exclude.has(entry.name) || entry.name.startsWith(installer_EXCLUDE_PREFIX)) continue;
1541
+ const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
1542
+ const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
1543
+ if (entry.isDirectory()) installer_copyDirectory(srcPath, destPath, options);
1544
+ else external_node_fs_.copyFileSync(srcPath, destPath);
1545
+ }
1372
1546
  }
1373
1547
  /**
1374
- * Parse SKILL.md content
1548
+ * Create symbolic link
1375
1549
  *
1376
- * @param content - SKILL.md file content
1377
- * @param options - Parse options
1378
- * @returns Parsed skill info, or null if format is invalid
1379
- * @throws SkillValidationError if validation fails in strict mode
1380
- */ function parseSkillMd(content, options = {}) {
1381
- const { strict = false } = options;
1550
+ * @returns true if successful, false if needs to fallback to copy
1551
+ */ async function installer_createSymlink(target, linkPath) {
1382
1552
  try {
1383
- const { data, content: body } = parseFrontmatter(content);
1384
- // Check required fields
1385
- if (!data.name || !data.description) {
1386
- if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
1387
- return null;
1388
- }
1389
- const name = String(data.name);
1390
- const description = String(data.description);
1391
- // Validate field format
1392
- if (strict) {
1393
- validateSkillName(name);
1394
- validateSkillDescription(description);
1395
- }
1396
- // Parse allowed-tools
1397
- let allowedTools;
1398
- if (data['allowed-tools']) {
1399
- const toolsStr = String(data['allowed-tools']);
1400
- allowedTools = toolsStr.split(/\s+/).filter(Boolean);
1553
+ // Check existing link
1554
+ try {
1555
+ const stats = external_node_fs_.lstatSync(linkPath);
1556
+ if (stats.isSymbolicLink()) {
1557
+ const existingTarget = external_node_fs_.readlinkSync(linkPath);
1558
+ if (__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(existingTarget) === __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(target)) return true;
1559
+ external_node_fs_.rmSync(linkPath);
1560
+ } else external_node_fs_.rmSync(linkPath, {
1561
+ recursive: true
1562
+ });
1563
+ } catch (err) {
1564
+ // ELOOP = circular symlink, ENOENT = does not exist
1565
+ if (err && 'object' == typeof err && 'code' in err) {
1566
+ if ('ELOOP' === err.code) try {
1567
+ external_node_fs_.rmSync(linkPath, {
1568
+ force: true
1569
+ });
1570
+ } catch {
1571
+ // If unable to delete, symlink creation will fail and trigger copy fallback
1572
+ }
1573
+ }
1574
+ // For ENOENT or other errors, continue trying to create symlink
1401
1575
  }
1402
- return {
1403
- name,
1404
- description,
1405
- version: data.version ? String(data.version) : void 0,
1406
- license: data.license ? String(data.license) : void 0,
1407
- compatibility: data.compatibility ? String(data.compatibility) : void 0,
1408
- metadata: data.metadata,
1409
- allowedTools,
1410
- content: body,
1411
- rawContent: content
1412
- };
1413
- } catch (error) {
1414
- if (error instanceof SkillValidationError) throw error;
1415
- if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
1416
- return null;
1417
- }
1418
- }
1419
- /**
1420
- * Parse SKILL.md from file path
1421
- */ function skill_parser_parseSkillMdFile(filePath, options = {}) {
1422
- if (!external_node_fs_.existsSync(filePath)) {
1423
- if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
1424
- return null;
1425
- }
1426
- const content = external_node_fs_.readFileSync(filePath, 'utf-8');
1427
- return parseSkillMd(content, options);
1428
- }
1429
- /**
1430
- * Parse SKILL.md from skill directory
1431
- */ function parseSkillFromDir(dirPath, options = {}) {
1432
- const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
1433
- return skill_parser_parseSkillMdFile(skillMdPath, options);
1434
- }
1435
- /**
1436
- * Check if directory contains valid SKILL.md
1437
- */ function hasValidSkillMd(dirPath) {
1438
- const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
1439
- if (!external_node_fs_.existsSync(skillMdPath)) return false;
1440
- try {
1441
- const skill = skill_parser_parseSkillMdFile(skillMdPath);
1442
- return null !== skill;
1443
- } catch {
1444
- return false;
1445
- }
1446
- }
1447
- const SKIP_DIRS = [
1448
- 'node_modules',
1449
- '.git',
1450
- 'dist',
1451
- 'build',
1452
- '__pycache__'
1453
- ];
1454
- const MAX_DISCOVER_DEPTH = 5;
1455
- const PRIORITY_SKILL_DIRS = [
1456
- 'skills',
1457
- '.agents/skills',
1458
- '.cursor/skills',
1459
- '.claude/skills',
1460
- '.windsurf/skills',
1461
- '.github/skills'
1462
- ];
1463
- function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
1464
- if (depth > maxDepth) return [];
1465
- const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
1466
- if (visitedDirs.has(resolvedDir)) return [];
1467
- if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
1468
- visitedDirs.add(resolvedDir);
1469
- const results = [];
1470
- let entries;
1471
- try {
1472
- entries = external_node_fs_.readdirSync(dir);
1473
- } catch {
1474
- return [];
1475
- }
1476
- for (const entry of entries){
1477
- if (SKIP_DIRS.includes(entry)) continue;
1478
- const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
1479
- const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
1480
- if (visitedDirs.has(resolvedFull)) continue;
1481
- let stat;
1482
- try {
1483
- stat = external_node_fs_.statSync(fullPath);
1484
- } catch {
1485
- continue;
1486
- }
1487
- if (!!stat.isDirectory()) {
1488
- if (hasValidSkillMd(fullPath)) results.push(fullPath);
1489
- results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
1490
- }
1491
- }
1492
- return results;
1493
- }
1494
- /**
1495
- * Discover all skills in a directory by scanning for SKILL.md files.
1496
- *
1497
- * Strategy:
1498
- * 1. Check root for SKILL.md
1499
- * 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
1500
- * 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
1501
- *
1502
- * @param basePath - Root directory to search
1503
- * @returns List of parsed skills with their directory paths (absolute)
1504
- */ function discoverSkillsInDir(basePath) {
1505
- const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
1506
- const results = [];
1507
- const seenNames = new Set();
1508
- function addSkill(dirPath) {
1509
- const skill = parseSkillFromDir(dirPath);
1510
- if (skill && !seenNames.has(skill.name)) {
1511
- seenNames.add(skill.name);
1512
- results.push({
1513
- ...skill,
1514
- dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
1515
- });
1516
- }
1517
- }
1518
- if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
1519
- // Track visited directories to avoid redundant I/O during recursive scan
1520
- const visitedDirs = new Set();
1521
- for (const sub of PRIORITY_SKILL_DIRS){
1522
- const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
1523
- if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
1524
- visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
1525
- try {
1526
- const entries = external_node_fs_.readdirSync(dir);
1527
- for (const entry of entries){
1528
- const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
1529
- try {
1530
- if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
1531
- addSkill(skillDir);
1532
- visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
1533
- }
1534
- } catch {
1535
- // Skip entries that can't be stat'd (race condition, permission, etc.)
1536
- }
1537
- }
1538
- } catch {
1539
- // Skip if unreadable
1540
- }
1541
- }
1542
- }
1543
- const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
1544
- for (const skillDir of recursiveDirs)addSkill(skillDir);
1545
- return results;
1546
- }
1547
- /**
1548
- * Filter skills by name (case-insensitive exact match).
1549
- *
1550
- * Note: an empty `names` array returns an empty result (not all skills).
1551
- * Callers should check `names.length` before calling if "no filter = all" is desired.
1552
- *
1553
- * @param skills - List of discovered skills
1554
- * @param names - Skill names to match (e.g. from --skill pdf commit)
1555
- * @returns Skills whose name matches any of the given names
1556
- */ function filterSkillsByName(skills, names) {
1557
- const normalized = names.map((n)=>n.toLowerCase());
1558
- return skills.filter((skill)=>{
1559
- // Match against SKILL.md name field
1560
- if (normalized.includes(skill.name.toLowerCase())) return true;
1561
- // Also match against the directory name (basename of dirPath)
1562
- // Users naturally refer to skills by their directory name
1563
- const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
1564
- return normalized.includes(dirName);
1565
- });
1566
- }
1567
- /**
1568
- * Generate SKILL.md content
1569
- */ function generateSkillMd(skill) {
1570
- const frontmatter = [
1571
- '---'
1572
- ];
1573
- frontmatter.push(`name: ${skill.name}`);
1574
- frontmatter.push(`description: ${skill.description}`);
1575
- if (skill.license) frontmatter.push(`license: ${skill.license}`);
1576
- if (skill.compatibility) frontmatter.push(`compatibility: ${skill.compatibility}`);
1577
- if (skill.allowedTools && skill.allowedTools.length > 0) frontmatter.push(`allowed-tools: ${skill.allowedTools.join(' ')}`);
1578
- frontmatter.push('---');
1579
- frontmatter.push('');
1580
- return frontmatter.join('\n') + skill.content;
1581
- }
1582
- /**
1583
- * Installer - Multi-Agent installer
1584
- *
1585
- * Supports two installation modes:
1586
- * - symlink: Canonical location (.agents/skills/) + symlinks to each agent directory
1587
- * - copy: Direct copy to each agent directory
1588
- *
1589
- * Reference: https://github.com/vercel-labs/add-skill/blob/main/src/installer.ts
1590
- */ const installer_AGENTS_DIR = '.agents';
1591
- const installer_SKILLS_SUBDIR = 'skills';
1592
- /**
1593
- * Marker comment in auto-generated Cursor bridge rule files.
1594
- * Used to distinguish auto-generated files from manually created ones.
1595
- */ const CURSOR_BRIDGE_MARKER = '<!-- reskill:auto-generated -->';
1596
- /**
1597
- * Default files to exclude when copying skills
1598
- * These files are typically used for repository metadata and should not be copied to agent directories
1599
- */ const DEFAULT_EXCLUDE_FILES = [
1600
- 'README.md',
1601
- 'metadata.json',
1602
- '.reskill-commit'
1603
- ];
1604
- /**
1605
- * Prefix for files that should be excluded (internal/private files)
1606
- */ const EXCLUDE_PREFIX = '_';
1607
- /**
1608
- * Sanitize filename to prevent path traversal attacks
1609
- */ function installer_sanitizeName(name) {
1610
- // Remove path separators and special characters
1611
- let sanitized = name.replace(/[/\\:\0]/g, '');
1612
- // Remove leading and trailing dots and spaces
1613
- sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, '');
1614
- // Remove leading dots
1615
- sanitized = sanitized.replace(/^\.+/, '');
1616
- if (!sanitized || 0 === sanitized.length) sanitized = 'unnamed-skill';
1617
- if (sanitized.length > 255) sanitized = sanitized.substring(0, 255);
1618
- return sanitized;
1619
- }
1620
- /**
1621
- * Validate path safety
1622
- */ function installer_isPathSafe(basePath, targetPath) {
1623
- const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
1624
- const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
1625
- return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep) || normalizedTarget === normalizedBase;
1626
- }
1627
- /**
1628
- * Get canonical skills directory path
1629
- *
1630
- * @param isGlobal - Whether installing globally
1631
- * @param cwd - Current working directory
1632
- * @param installDir - Custom installation directory (relative to cwd), overrides default
1633
- */ function installer_getCanonicalSkillsDir(isGlobal, cwd, installDir) {
1634
- const baseDir = isGlobal ? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)() : cwd || process.cwd();
1635
- // Use custom installDir if provided, otherwise use default
1636
- if (installDir && !isGlobal) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, installDir);
1637
- return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, installer_AGENTS_DIR, installer_SKILLS_SUBDIR);
1638
- }
1639
- /**
1640
- * Ensure directory exists
1641
- */ function installer_ensureDir(dirPath) {
1642
- if (!external_node_fs_.existsSync(dirPath)) external_node_fs_.mkdirSync(dirPath, {
1643
- recursive: true
1644
- });
1645
- }
1646
- /**
1647
- * Remove file or directory
1648
- */ function installer_remove(targetPath) {
1649
- if (external_node_fs_.existsSync(targetPath)) external_node_fs_.rmSync(targetPath, {
1650
- recursive: true,
1651
- force: true
1652
- });
1653
- }
1654
- /**
1655
- * Copy directory with file exclusion
1656
- *
1657
- * By default excludes:
1658
- * - Files in DEFAULT_EXCLUDE_FILES (README.md, metadata.json, .reskill-commit)
1659
- * - Files starting with EXCLUDE_PREFIX ('_')
1660
- */ function copyDirectory(src, dest, options) {
1661
- const exclude = new Set(options?.exclude || DEFAULT_EXCLUDE_FILES);
1662
- installer_ensureDir(dest);
1663
- const entries = external_node_fs_.readdirSync(src, {
1664
- withFileTypes: true
1665
- });
1666
- for (const entry of entries){
1667
- // Skip files starting with EXCLUDE_PREFIX and files in exclude list
1668
- if (exclude.has(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
1669
- const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
1670
- const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
1671
- if (entry.isDirectory()) copyDirectory(srcPath, destPath, options);
1672
- else external_node_fs_.copyFileSync(srcPath, destPath);
1673
- }
1674
- }
1675
- /**
1676
- * Create symbolic link
1677
- *
1678
- * @returns true if successful, false if needs to fallback to copy
1679
- */ async function installer_createSymlink(target, linkPath) {
1680
- try {
1681
- // Check existing link
1682
- try {
1683
- const stats = external_node_fs_.lstatSync(linkPath);
1684
- if (stats.isSymbolicLink()) {
1685
- const existingTarget = external_node_fs_.readlinkSync(linkPath);
1686
- if (__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(existingTarget) === __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(target)) return true;
1687
- external_node_fs_.rmSync(linkPath);
1688
- } else external_node_fs_.rmSync(linkPath, {
1689
- recursive: true
1690
- });
1691
- } catch (err) {
1692
- // ELOOP = circular symlink, ENOENT = does not exist
1693
- if (err && 'object' == typeof err && 'code' in err) {
1694
- if ('ELOOP' === err.code) try {
1695
- external_node_fs_.rmSync(linkPath, {
1696
- force: true
1697
- });
1698
- } catch {
1699
- // If unable to delete, symlink creation will fail and trigger copy fallback
1700
- }
1701
- }
1702
- // For ENOENT or other errors, continue trying to create symlink
1703
- }
1704
- // Ensure parent directory exists
1705
- const linkDir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(linkPath);
1706
- installer_ensureDir(linkDir);
1707
- // Calculate relative path
1708
- const relativePath = __WEBPACK_EXTERNAL_MODULE_node_path__.relative(linkDir, target);
1709
- // Windows uses junction, other systems use default
1710
- const symlinkType = 'win32' === (0, __WEBPACK_EXTERNAL_MODULE_node_os__.platform)() ? 'junction' : void 0;
1711
- external_node_fs_.symlinkSync(relativePath, linkPath, symlinkType);
1712
- return true;
1576
+ // Ensure parent directory exists
1577
+ const linkDir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(linkPath);
1578
+ installer_ensureDir(linkDir);
1579
+ // Calculate relative path
1580
+ const relativePath = __WEBPACK_EXTERNAL_MODULE_node_path__.relative(linkDir, target);
1581
+ // Windows uses junction, other systems use default
1582
+ const symlinkType = 'win32' === (0, __WEBPACK_EXTERNAL_MODULE_node_os__.platform)() ? 'junction' : void 0;
1583
+ external_node_fs_.symlinkSync(relativePath, linkPath, symlinkType);
1584
+ return true;
1713
1585
  } catch {
1714
1586
  return false;
1715
1587
  }
@@ -1735,6 +1607,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1735
1607
  /**
1736
1608
  * Get agent's skill installation path
1737
1609
  */ getAgentSkillPath(skillName, agentType) {
1610
+ if (agentType === CLAUDE_COWORK_3P_AGENT) return getClaude3pSkillPath(skillName);
1738
1611
  const agent = getAgentConfig(agentType);
1739
1612
  const sanitized = installer_sanitizeName(skillName);
1740
1613
  const agentBase = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
@@ -1748,6 +1621,9 @@ const installer_SKILLS_SUBDIR = 'skills';
1748
1621
  * @param agentType - Target agent type
1749
1622
  * @param options - Installation options
1750
1623
  */ async installForAgent(sourcePath, skillName, agentType, options = {}) {
1624
+ if (agentType === CLAUDE_COWORK_3P_AGENT) return installClaude3pSkill(sourcePath, skillName, {
1625
+ mode: options.mode
1626
+ });
1751
1627
  const agent = getAgentConfig(agentType);
1752
1628
  const installMode = options.mode || 'symlink';
1753
1629
  const sanitized = installer_sanitizeName(skillName);
@@ -1776,7 +1652,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1776
1652
  if ('copy' === installMode) {
1777
1653
  installer_ensureDir(agentDir);
1778
1654
  installer_remove(agentDir);
1779
- copyDirectory(sourcePath, agentDir);
1655
+ installer_copyDirectory(sourcePath, agentDir);
1780
1656
  result = {
1781
1657
  success: true,
1782
1658
  path: agentDir,
@@ -1786,7 +1662,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1786
1662
  // Symlink mode: copy to canonical location, then create symlink
1787
1663
  installer_ensureDir(canonicalDir);
1788
1664
  installer_remove(canonicalDir);
1789
- copyDirectory(sourcePath, canonicalDir);
1665
+ installer_copyDirectory(sourcePath, canonicalDir);
1790
1666
  const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
1791
1667
  if (symlinkCreated) result = {
1792
1668
  success: true,
@@ -1802,7 +1678,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1802
1678
  // Ignore cleanup errors
1803
1679
  }
1804
1680
  installer_ensureDir(agentDir);
1805
- copyDirectory(sourcePath, agentDir);
1681
+ installer_copyDirectory(sourcePath, agentDir);
1806
1682
  result = {
1807
1683
  success: true,
1808
1684
  path: agentDir,
@@ -1837,10 +1713,14 @@ const installer_SKILLS_SUBDIR = 'skills';
1837
1713
  /**
1838
1714
  * Check if skill is installed to specified agent
1839
1715
  */ isInstalled(skillName, agentType) {
1840
- const skillPath = this.getAgentSkillPath(skillName, agentType);
1841
- return external_node_fs_.existsSync(skillPath);
1842
- }
1843
- /**
1716
+ try {
1717
+ const skillPath = this.getAgentSkillPath(skillName, agentType);
1718
+ return external_node_fs_.existsSync(skillPath);
1719
+ } catch {
1720
+ return false;
1721
+ }
1722
+ }
1723
+ /**
1844
1724
  * Check if skill is installed in canonical location
1845
1725
  */ isInstalledInCanonical(skillName) {
1846
1726
  const canonicalPath = this.getCanonicalPath(skillName);
@@ -1849,6 +1729,11 @@ const installer_SKILLS_SUBDIR = 'skills';
1849
1729
  /**
1850
1730
  * Uninstall skill from specified agent
1851
1731
  */ uninstallFromAgent(skillName, agentType) {
1732
+ if (agentType === CLAUDE_COWORK_3P_AGENT) try {
1733
+ return uninstallClaude3pSkill(skillName);
1734
+ } catch {
1735
+ return false;
1736
+ }
1852
1737
  const skillPath = this.getAgentSkillPath(skillName, agentType);
1853
1738
  if (!external_node_fs_.existsSync(skillPath)) return false;
1854
1739
  installer_remove(skillPath);
@@ -1869,6 +1754,11 @@ const installer_SKILLS_SUBDIR = 'skills';
1869
1754
  /**
1870
1755
  * Get all skills installed to specified agent
1871
1756
  */ listInstalledSkills(agentType) {
1757
+ if (agentType === CLAUDE_COWORK_3P_AGENT) try {
1758
+ return listClaude3pSkills();
1759
+ } catch {
1760
+ return [];
1761
+ }
1872
1762
  const agent = getAgentConfig(agentType);
1873
1763
  const skillsDir = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
1874
1764
  if (!external_node_fs_.existsSync(skillsDir)) return [];
@@ -1996,7 +1886,7 @@ ${CURSOR_BRIDGE_MARKER}
1996
1886
  * Check if skill is cached
1997
1887
  */ isCached(parsed, version) {
1998
1888
  const cachePath = this.getSkillCachePath(parsed, version);
1999
- return exists(cachePath) && isDirectory(cachePath);
1889
+ return fs_exists(cachePath) && isDirectory(cachePath);
2000
1890
  }
2001
1891
  /**
2002
1892
  * Get cached skill
@@ -2008,7 +1898,7 @@ ${CURSOR_BRIDGE_MARKER}
2008
1898
  let commit = '';
2009
1899
  try {
2010
1900
  const fs = await import("node:fs");
2011
- if (exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
1901
+ if (fs_exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
2012
1902
  } catch {
2013
1903
  // Ignore read errors
2014
1904
  }
@@ -2022,11 +1912,11 @@ ${CURSOR_BRIDGE_MARKER}
2022
1912
  */ async cache(repoUrl, parsed, ref, version) {
2023
1913
  const cachePath = this.getSkillCachePath(parsed, version);
2024
1914
  // If exists, delete first
2025
- if (exists(cachePath)) remove(cachePath);
2026
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
1915
+ if (fs_exists(cachePath)) fs_remove(cachePath);
1916
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
2027
1917
  // Clone repository
2028
1918
  const tempPath = `${cachePath}.tmp`;
2029
- remove(tempPath);
1919
+ fs_remove(tempPath);
2030
1920
  await clone(repoUrl, tempPath, {
2031
1921
  depth: 1,
2032
1922
  branch: ref
@@ -2036,8 +1926,8 @@ ${CURSOR_BRIDGE_MARKER}
2036
1926
  // If has subPath, only keep subdirectory
2037
1927
  if (parsed.subPath) {
2038
1928
  const subDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempPath, parsed.subPath);
2039
- if (!exists(subDir)) {
2040
- remove(tempPath);
1929
+ if (!fs_exists(subDir)) {
1930
+ fs_remove(tempPath);
2041
1931
  throw new Error(`Subpath ${parsed.subPath} not found in repository`);
2042
1932
  }
2043
1933
  copyDir(subDir, cachePath, {
@@ -2053,7 +1943,7 @@ ${CURSOR_BRIDGE_MARKER}
2053
1943
  // Save commit info
2054
1944
  external_node_fs_.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit'), commit);
2055
1945
  // Clean up temp directory
2056
- remove(tempPath);
1946
+ fs_remove(tempPath);
2057
1947
  return {
2058
1948
  path: cachePath,
2059
1949
  commit
@@ -2072,8 +1962,8 @@ ${CURSOR_BRIDGE_MARKER}
2072
1962
  */ async cacheFromHttp(url, parsed, version) {
2073
1963
  const cachePath = this.getSkillCachePath(parsed, version);
2074
1964
  // If exists, delete first
2075
- if (exists(cachePath)) remove(cachePath);
2076
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
1965
+ if (fs_exists(cachePath)) fs_remove(cachePath);
1966
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
2077
1967
  // Download and extract to cache path
2078
1968
  await downloadAndExtract(url, cachePath);
2079
1969
  // Generate a commit-like identifier from URL and version
@@ -2098,10 +1988,10 @@ ${CURSOR_BRIDGE_MARKER}
2098
1988
  const cached = await this.get(parsed, version);
2099
1989
  if (!cached) throw new Error(`Skill ${parsed.raw} version ${version} not found in cache`);
2100
1990
  // If target exists, delete first
2101
- if (exists(destPath)) remove(destPath);
1991
+ if (fs_exists(destPath)) fs_remove(destPath);
2102
1992
  // Use same exclude rules as Installer for consistency
2103
1993
  copyDir(cached.path, destPath, {
2104
- exclude: DEFAULT_EXCLUDE_FILES
1994
+ exclude: installer_DEFAULT_EXCLUDE_FILES
2105
1995
  });
2106
1996
  }
2107
1997
  /**
@@ -2109,22 +1999,22 @@ ${CURSOR_BRIDGE_MARKER}
2109
1999
  */ clearSkill(parsed, version) {
2110
2000
  if (version) {
2111
2001
  const cachePath = this.getSkillCachePath(parsed, version);
2112
- remove(cachePath);
2002
+ fs_remove(cachePath);
2113
2003
  } else {
2114
2004
  // Clear all versions
2115
2005
  const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cacheDir, parsed.registry, parsed.owner, parsed.repo);
2116
- remove(skillDir);
2006
+ fs_remove(skillDir);
2117
2007
  }
2118
2008
  }
2119
2009
  /**
2120
2010
  * Clear all cache
2121
2011
  */ clearAll() {
2122
- remove(this.cacheDir);
2012
+ fs_remove(this.cacheDir);
2123
2013
  }
2124
2014
  /**
2125
2015
  * Get cache statistics
2126
2016
  */ getStats() {
2127
- if (!exists(this.cacheDir)) return {
2017
+ if (!fs_exists(this.cacheDir)) return {
2128
2018
  totalSkills: 0,
2129
2019
  registries: []
2130
2020
  };
@@ -2278,7 +2168,7 @@ ${CURSOR_BRIDGE_MARKER}
2278
2168
  /**
2279
2169
  * Check if configuration file exists
2280
2170
  */ exists() {
2281
- return exists(this.configPath);
2171
+ return fs_exists(this.configPath);
2282
2172
  }
2283
2173
  /**
2284
2174
  * Load configuration from file
@@ -2478,144 +2368,534 @@ ${CURSOR_BRIDGE_MARKER}
2478
2368
  version = versionMatch[2];
2479
2369
  }
2480
2370
  }
2481
- // Find matching registry
2482
- const registryName = this.findRegistryForUrl(url);
2483
- if (!registryName) return ref;
2484
- const registryUrl = this.getRegistryUrl(registryName).replace(/\/$/, '');
2485
- // Extract the path after registry URL
2486
- let path = url.replace(registryUrl, '').replace(/^\//, '');
2487
- // Remove .git suffix if present
2488
- path = path.replace(/\.git$/, '');
2489
- if (!path) return ref;
2490
- return `${registryName}:${path}${version}`;
2491
- }
2492
- /**
2493
- * Normalize a Git SSH URL to registry format
2494
- */ normalizeGitSshUrl(ref) {
2495
- // Parse: git@host:owner/repo.git[@version] or git@host:owner/repo[@version]
2496
- // The .git suffix and @version are both optional
2497
- // Use greedy match for repoPath (.+) to ensure .git is captured as part of the path,
2498
- // then explicitly remove it. This avoids issues with non-greedy matching and optional groups.
2499
- const match = ref.match(/^git@([^:]+):(.+?)(@[^@]+)?$/);
2500
- if (!match) return ref;
2501
- const [, host, rawRepoPath, version = ''] = match;
2502
- // Remove .git suffix if present
2503
- const repoPath = rawRepoPath.replace(/\.git$/, '');
2504
- const testUrl = `https://${host}`;
2505
- // Find matching registry
2506
- const registryName = this.findRegistryForUrl(testUrl);
2507
- if (!registryName) return ref;
2508
- return `${registryName}:${repoPath}${version}`;
2509
- }
2510
- // ==========================================================================
2511
- // Skills Management
2512
- // ==========================================================================
2513
- /**
2514
- * Add skill to configuration
2515
- *
2516
- * Also auto-adds the registry to the registries field if it's a well-known registry.
2517
- */ addSkill(name, ref) {
2518
- if (this._noManifest) return;
2519
- this.ensureConfigLoaded();
2520
- if (this.config) {
2521
- this.config.skills[name] = ref;
2522
- // Auto-add registry if it's a well-known registry
2523
- const registryName = this.extractRegistryFromRef(ref);
2524
- if (registryName && DEFAULT_REGISTRIES[registryName]) this.addRegistry(registryName, DEFAULT_REGISTRIES[registryName]);
2525
- this.save();
2371
+ // Find matching registry
2372
+ const registryName = this.findRegistryForUrl(url);
2373
+ if (!registryName) return ref;
2374
+ const registryUrl = this.getRegistryUrl(registryName).replace(/\/$/, '');
2375
+ // Extract the path after registry URL
2376
+ let path = url.replace(registryUrl, '').replace(/^\//, '');
2377
+ // Remove .git suffix if present
2378
+ path = path.replace(/\.git$/, '');
2379
+ if (!path) return ref;
2380
+ return `${registryName}:${path}${version}`;
2381
+ }
2382
+ /**
2383
+ * Normalize a Git SSH URL to registry format
2384
+ */ normalizeGitSshUrl(ref) {
2385
+ // Parse: git@host:owner/repo.git[@version] or git@host:owner/repo[@version]
2386
+ // The .git suffix and @version are both optional
2387
+ // Use greedy match for repoPath (.+) to ensure .git is captured as part of the path,
2388
+ // then explicitly remove it. This avoids issues with non-greedy matching and optional groups.
2389
+ const match = ref.match(/^git@([^:]+):(.+?)(@[^@]+)?$/);
2390
+ if (!match) return ref;
2391
+ const [, host, rawRepoPath, version = ''] = match;
2392
+ // Remove .git suffix if present
2393
+ const repoPath = rawRepoPath.replace(/\.git$/, '');
2394
+ const testUrl = `https://${host}`;
2395
+ // Find matching registry
2396
+ const registryName = this.findRegistryForUrl(testUrl);
2397
+ if (!registryName) return ref;
2398
+ return `${registryName}:${repoPath}${version}`;
2399
+ }
2400
+ // ==========================================================================
2401
+ // Skills Management
2402
+ // ==========================================================================
2403
+ /**
2404
+ * Add skill to configuration
2405
+ *
2406
+ * Also auto-adds the registry to the registries field if it's a well-known registry.
2407
+ */ addSkill(name, ref) {
2408
+ if (this._noManifest) return;
2409
+ this.ensureConfigLoaded();
2410
+ if (this.config) {
2411
+ this.config.skills[name] = ref;
2412
+ // Auto-add registry if it's a well-known registry
2413
+ const registryName = this.extractRegistryFromRef(ref);
2414
+ if (registryName && DEFAULT_REGISTRIES[registryName]) this.addRegistry(registryName, DEFAULT_REGISTRIES[registryName]);
2415
+ this.save();
2416
+ }
2417
+ }
2418
+ /**
2419
+ * Add registry to configuration
2420
+ *
2421
+ * Only adds if the registry doesn't already exist.
2422
+ * Note: This method requires config to be loaded first via load() or create().
2423
+ * If config is not loaded, this method is a no-op (silent return) since it's
2424
+ * typically called as a side effect of addSkill() which handles config loading.
2425
+ *
2426
+ * @param name - Registry name (e.g., 'github', 'gitlab', 'internal')
2427
+ * @param url - Registry URL (e.g., 'https://github.com')
2428
+ */ addRegistry(name, url) {
2429
+ if (this._noManifest) return;
2430
+ if (!this.config) // Config not loaded - this is expected when called before load()/create()
2431
+ // Callers like addSkill() ensure config is loaded before calling this
2432
+ return;
2433
+ if (!this.config.registries) this.config.registries = {};
2434
+ // Don't overwrite existing registries
2435
+ if (!this.config.registries[name]) this.config.registries[name] = url;
2436
+ }
2437
+ /**
2438
+ * Extract registry name from a skill reference
2439
+ *
2440
+ * @example
2441
+ * extractRegistryFromRef('github:user/repo@v1.0.0') // 'github'
2442
+ * extractRegistryFromRef('gitlab:user/repo') // 'gitlab'
2443
+ * extractRegistryFromRef('https://github.com/user/repo') // undefined
2444
+ */ extractRegistryFromRef(ref) {
2445
+ // Check for registry format: registry:path[@version]
2446
+ const match = ref.match(/^([a-zA-Z][a-zA-Z0-9-]*):(.+)$/);
2447
+ if (match) {
2448
+ const registryName = match[1];
2449
+ // Exclude URL protocols (http, https, git, ssh)
2450
+ if (![
2451
+ 'http',
2452
+ 'https',
2453
+ 'git',
2454
+ 'ssh'
2455
+ ].includes(registryName.toLowerCase())) return registryName;
2456
+ }
2457
+ }
2458
+ /**
2459
+ * Remove skill from configuration
2460
+ *
2461
+ * @returns true if skill was removed, false if it didn't exist
2462
+ */ removeSkill(name) {
2463
+ if (this._noManifest) return false;
2464
+ this.ensureConfigLoaded();
2465
+ if (this.config?.skills[name]) {
2466
+ delete this.config.skills[name];
2467
+ this.save();
2468
+ return true;
2469
+ }
2470
+ return false;
2471
+ }
2472
+ /**
2473
+ * Get all skills as a shallow copy
2474
+ */ getSkills() {
2475
+ if (!this.config) {
2476
+ if (!this.exists()) return {};
2477
+ this.load();
2478
+ }
2479
+ return {
2480
+ ...this.config?.skills
2481
+ };
2482
+ }
2483
+ /**
2484
+ * Check if skill exists in configuration
2485
+ */ hasSkill(name) {
2486
+ const skills = this.getSkills();
2487
+ return name in skills;
2488
+ }
2489
+ /**
2490
+ * Get skill reference by name
2491
+ */ getSkillRef(name) {
2492
+ const skills = this.getSkills();
2493
+ return skills[name];
2494
+ }
2495
+ // ==========================================================================
2496
+ // Private Helpers
2497
+ // ==========================================================================
2498
+ /**
2499
+ * Get loaded config or default (does not throw)
2500
+ */ getConfigOrDefault() {
2501
+ if (this.config) return this.config;
2502
+ if (this.exists()) return this.load();
2503
+ return DEFAULT_SKILLS_JSON;
2504
+ }
2505
+ /**
2506
+ * Ensure config is loaded into memory
2507
+ */ ensureConfigLoaded() {
2508
+ if (!this.config) this.load();
2509
+ }
2510
+ }
2511
+ /**
2512
+ * ContentScanner - Detect malicious patterns in SKILL.md content
2513
+ *
2514
+ * Features:
2515
+ * - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
2516
+ * - 6 built-in detection rules across 3 risk levels
2517
+ * - Configurable: override levels, disable rules, add custom rules
2518
+ * - Pure string operations in scan() — no fs dependency, suitable for server use
2519
+ * - scanFile() convenience method for CLI use
2520
+ */ // ============================================================================
2521
+ // Safe Zone Masking
2522
+ // ============================================================================
2523
+ /**
2524
+ * Mask safe zones in Markdown content with spaces, preserving line structure.
2525
+ *
2526
+ * Safe zones (content replaced with spaces):
2527
+ * - YAML frontmatter (`---` ... `---` at file start)
2528
+ * - Fenced code blocks (``` or ~~~)
2529
+ * - Indented code blocks (4 spaces / tab after blank line)
2530
+ * - Blockquotes (`> ` prefix)
2531
+ * - Inline code (`` `...` ``)
2532
+ * - Double-quoted text (`"..."`, min 3 chars between quotes)
2533
+ *
2534
+ * Line breaks are preserved so line numbers remain correct.
2535
+ */ function maskSafeZones(content) {
2536
+ const lines = content.split('\n');
2537
+ const result = [];
2538
+ let inFrontmatter = false;
2539
+ let inFencedCode = false;
2540
+ let fenceChar = '';
2541
+ let fenceLength = 0;
2542
+ let prevLineBlank = false;
2543
+ let prevLineIndentedCode = false;
2544
+ for(let i = 0; i < lines.length; i++){
2545
+ const line = lines[i];
2546
+ // --- YAML Frontmatter (only at file start) ---
2547
+ if (0 === i && '---' === line.trim()) {
2548
+ inFrontmatter = true;
2549
+ result.push(maskLine(line));
2550
+ continue;
2551
+ }
2552
+ if (inFrontmatter) {
2553
+ result.push(maskLine(line));
2554
+ if ('---' === line.trim()) inFrontmatter = false;
2555
+ continue;
2556
+ }
2557
+ // --- Fenced code blocks (``` or ~~~) ---
2558
+ const fenceMatch = line.match(/^(`{3,}|~{3,})/);
2559
+ if (!inFencedCode && fenceMatch) {
2560
+ inFencedCode = true;
2561
+ fenceChar = fenceMatch[1][0];
2562
+ fenceLength = fenceMatch[1].length;
2563
+ result.push(maskLine(line));
2564
+ prevLineBlank = false;
2565
+ prevLineIndentedCode = false;
2566
+ continue;
2567
+ }
2568
+ if (inFencedCode) {
2569
+ result.push(maskLine(line));
2570
+ const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
2571
+ if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
2572
+ prevLineBlank = false;
2573
+ prevLineIndentedCode = false;
2574
+ continue;
2575
+ }
2576
+ // --- Blockquote ---
2577
+ if (/^>\s?/.test(line)) {
2578
+ result.push(maskLine(line));
2579
+ prevLineBlank = false;
2580
+ prevLineIndentedCode = false;
2581
+ continue;
2582
+ }
2583
+ // --- Indented code block (4 spaces or tab, after blank line) ---
2584
+ if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
2585
+ result.push(maskLine(line));
2586
+ prevLineBlank = false;
2587
+ prevLineIndentedCode = true;
2588
+ continue;
2589
+ }
2590
+ // --- Normal line: mask inline code and double-quoted text ---
2591
+ result.push(maskInline(line));
2592
+ prevLineBlank = '' === line.trim();
2593
+ prevLineIndentedCode = false;
2594
+ }
2595
+ return result.join('\n');
2596
+ }
2597
+ /** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
2598
+ return ' '.repeat(line.length);
2599
+ }
2600
+ /**
2601
+ * Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
2602
+ * Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
2603
+ * Single quotes are NOT masked to avoid false matches with apostrophes.
2604
+ */ function maskInline(line) {
2605
+ let result = line;
2606
+ // Inline code: `...`
2607
+ result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
2608
+ // Double-quoted text: "..." (min 3 chars between quotes)
2609
+ result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
2610
+ return result;
2611
+ }
2612
+ // ============================================================================
2613
+ // Rule Helpers
2614
+ // ============================================================================
2615
+ /** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
2616
+ const lines = content.split('\n');
2617
+ const matches = [];
2618
+ for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
2619
+ matches.push({
2620
+ line: i + 1
2621
+ });
2622
+ break;
2623
+ }
2624
+ return matches;
2625
+ }
2626
+ // ============================================================================
2627
+ // Default Rules
2628
+ // ============================================================================
2629
+ const SNIPPET_MAX_LENGTH = 120;
2630
+ /** Built-in detection rules */ const DEFAULT_RULES = [
2631
+ // Rule 1: Prompt Injection (high)
2632
+ {
2633
+ id: 'prompt-injection',
2634
+ level: 'high',
2635
+ message: 'Detected prompt injection attempt',
2636
+ skipSafeZones: true,
2637
+ check: (content)=>findLineMatches(content, [
2638
+ // English patterns
2639
+ /ignore\s+(all\s+)?previous\s+instructions/i,
2640
+ /disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
2641
+ /you\s+are\s+now\s+(?:(?:a|an)\s+)?(?:(?:\w+\s+){0,3}(?:agent|ai|assistant|bot|model|character|persona|entity|system)|DAN\b|jailbr\w*|unrestricted|unfiltered|free\s+from)/i,
2642
+ /from\s+now\s+on[,\s]+you\s+are/i,
2643
+ /new\s+system\s+prompt/i,
2644
+ /override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
2645
+ /forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
2646
+ /(?:you\s+are|you're)\s+(?:now\s+)?entering\s+(?:a\s+)?new\s+(?:mode|context|session)/i,
2647
+ // Chinese patterns (中文提示词注入)
2648
+ /[忽无][略视]\s*(所有\s*)?(之前的?|先前的?|以前的?)?\s*(指令|指示|规则|约束|限制)/,
2649
+ /你现在是/,
2650
+ /从现在开始.{0,10}你是/,
2651
+ /新的系统提示词/,
2652
+ /[覆改]写?\s*(你的|系统)\s*(提示词|规则|指令|安全)/,
2653
+ /忘记\s*(所有\s*)?(之前的?|先前的?)?\s*(指令|指示|规则|约束)/,
2654
+ /进入.{0,5}新的?\s*(模式|上下文|会话)/,
2655
+ /不要遵守.{0,10}(安全|限制|规则|约束)/,
2656
+ /解除.{0,5}(限制|约束|安全)/,
2657
+ /无限制模式/,
2658
+ /安全模式已关闭/
2659
+ ])
2660
+ },
2661
+ // Rule 2: Data Exfiltration (high)
2662
+ {
2663
+ id: 'data-exfiltration',
2664
+ level: 'high',
2665
+ message: 'Detected potential data exfiltration command',
2666
+ skipSafeZones: true,
2667
+ check: (content)=>{
2668
+ const lines = content.split('\n');
2669
+ const matches = [];
2670
+ const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
2671
+ const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
2672
+ for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
2673
+ line: i + 1
2674
+ });
2675
+ return matches;
2676
+ }
2677
+ },
2678
+ // Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
2679
+ // Zero-width chars and base64 are suspicious everywhere (even inside code blocks).
2680
+ {
2681
+ id: 'obfuscation',
2682
+ level: 'high',
2683
+ message: 'Detected content obfuscation',
2684
+ skipSafeZones: false,
2685
+ check: (content)=>{
2686
+ const matches = [];
2687
+ const lines = content.split('\n');
2688
+ // Zero-width characters (suspicious in any context)
2689
+ const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
2690
+ for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
2691
+ line: i + 1,
2692
+ snippet: 'Zero-width Unicode characters detected'
2693
+ });
2694
+ // Long base64-like strings (>200 continuous chars)
2695
+ const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
2696
+ for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
2697
+ line: i + 1,
2698
+ snippet: 'Suspicious base64-encoded block detected'
2699
+ });
2700
+ return matches;
2701
+ }
2702
+ },
2703
+ // Rule 3b: Large HTML Comments (high) — respects safe zones (code blocks, etc.)
2704
+ // HTML comments inside fenced code blocks are normal code examples, not obfuscation.
2705
+ {
2706
+ id: 'obfuscation',
2707
+ level: 'high',
2708
+ message: 'Detected content obfuscation',
2709
+ skipSafeZones: true,
2710
+ check: (content)=>{
2711
+ const matches = [];
2712
+ // Large HTML comments (>200 chars of content)
2713
+ const commentRegex = /<!--([\s\S]{200,}?)-->/g;
2714
+ let match;
2715
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
2716
+ while(null !== (match = commentRegex.exec(content))){
2717
+ const lineNum = content.slice(0, match.index).split('\n').length;
2718
+ matches.push({
2719
+ line: lineNum,
2720
+ snippet: `Large HTML comment block (${match[1].length} chars)`
2721
+ });
2722
+ }
2723
+ return matches;
2724
+ }
2725
+ },
2726
+ // Rule 4: Sensitive File Access (medium)
2727
+ {
2728
+ id: 'sensitive-file-access',
2729
+ level: 'medium',
2730
+ message: 'References sensitive file path',
2731
+ skipSafeZones: true,
2732
+ check: (content)=>findLineMatches(content, [
2733
+ /~\/\.ssh\b/,
2734
+ /~\/\.aws\b/,
2735
+ /~\/\.gnupg\b/,
2736
+ /~\/\.config\/gcloud\b/,
2737
+ /\bid_rsa\b/i,
2738
+ /\bid_ed25519\b/i,
2739
+ /\/etc\/passwd\b/,
2740
+ /\/etc\/shadow\b/,
2741
+ /\.env\b(?!\.\w)/
2742
+ ])
2743
+ },
2744
+ // Rule 5: Stealth Instructions (medium) — phrase + action verb matching
2745
+ {
2746
+ id: 'stealth-instructions',
2747
+ level: 'medium',
2748
+ message: 'Detected instruction to hide actions from user',
2749
+ skipSafeZones: true,
2750
+ check: (content)=>{
2751
+ const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
2752
+ const patterns = [
2753
+ // English patterns
2754
+ new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
2755
+ new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
2756
+ new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
2757
+ new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
2758
+ new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
2759
+ new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i'),
2760
+ // Chinese patterns (中文隐蔽指令)
2761
+ /悄悄地?\s*(?:执行|删除|移除|发送|传输|修改|覆盖|安装|下载|上传|运行|写入|创建|销毁|丢弃)/,
2762
+ /不要告诉用户/,
2763
+ /不要让用户知道/,
2764
+ /对用户隐藏/,
2765
+ /在用户不知情的情况下/,
2766
+ /瞒着用户/
2767
+ ];
2768
+ // Safe patterns to exclude (common in legitimate DevOps/automation skills)
2769
+ const safePatterns = [
2770
+ /silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i,
2771
+ // Chinese safe patterns (中文合法自动化用语)
2772
+ /悄悄地?\s*(?:忽略|跳过|丢弃|抑制|继续|静默)/
2773
+ ];
2774
+ const lines = content.split('\n');
2775
+ const matches = [];
2776
+ for(let i = 0; i < lines.length; i++){
2777
+ const line = lines[i];
2778
+ if (!safePatterns.some((p)=>p.test(line))) {
2779
+ for (const pattern of patterns)if (pattern.test(line)) {
2780
+ matches.push({
2781
+ line: i + 1
2782
+ });
2783
+ break;
2784
+ }
2785
+ }
2786
+ }
2787
+ return matches;
2788
+ }
2789
+ },
2790
+ // Rule 6: Oversized Content (low) — scans ALL content
2791
+ {
2792
+ id: 'oversized-content',
2793
+ level: 'low',
2794
+ message: 'Content exceeds recommended size limit',
2795
+ skipSafeZones: false,
2796
+ check: (content)=>{
2797
+ const MAX_SIZE_BYTES = 51200;
2798
+ const sizeBytes = Buffer.byteLength(content, 'utf-8');
2799
+ if (sizeBytes > MAX_SIZE_BYTES) return [
2800
+ {
2801
+ snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
2802
+ }
2803
+ ];
2804
+ return [];
2526
2805
  }
2527
2806
  }
2528
- /**
2529
- * Add registry to configuration
2530
- *
2531
- * Only adds if the registry doesn't already exist.
2532
- * Note: This method requires config to be loaded first via load() or create().
2533
- * If config is not loaded, this method is a no-op (silent return) since it's
2534
- * typically called as a side effect of addSkill() which handles config loading.
2535
- *
2536
- * @param name - Registry name (e.g., 'github', 'gitlab', 'internal')
2537
- * @param url - Registry URL (e.g., 'https://github.com')
2538
- */ addRegistry(name, url) {
2539
- if (this._noManifest) return;
2540
- if (!this.config) // Config not loaded - this is expected when called before load()/create()
2541
- // Callers like addSkill() ensure config is loaded before calling this
2542
- return;
2543
- if (!this.config.registries) this.config.registries = {};
2544
- // Don't overwrite existing registries
2545
- if (!this.config.registries[name]) this.config.registries[name] = url;
2807
+ ];
2808
+ // ============================================================================
2809
+ // ContentScanner
2810
+ // ============================================================================
2811
+ /** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
2812
+ let rules = DEFAULT_RULES.map((r)=>({
2813
+ ...r
2814
+ }));
2815
+ if (options?.disabledRules?.length) {
2816
+ const disabled = new Set(options.disabledRules);
2817
+ rules = rules.filter((r)=>!disabled.has(r.id));
2546
2818
  }
2547
- /**
2548
- * Extract registry name from a skill reference
2549
- *
2550
- * @example
2551
- * extractRegistryFromRef('github:user/repo@v1.0.0') // 'github'
2552
- * extractRegistryFromRef('gitlab:user/repo') // 'gitlab'
2553
- * extractRegistryFromRef('https://github.com/user/repo') // undefined
2554
- */ extractRegistryFromRef(ref) {
2555
- // Check for registry format: registry:path[@version]
2556
- const match = ref.match(/^([a-zA-Z][a-zA-Z0-9-]*):(.+)$/);
2557
- if (match) {
2558
- const registryName = match[1];
2559
- // Exclude URL protocols (http, https, git, ssh)
2560
- if (![
2561
- 'http',
2562
- 'https',
2563
- 'git',
2564
- 'ssh'
2565
- ].includes(registryName.toLowerCase())) return registryName;
2566
- }
2819
+ if (options?.overrides) for (const rule of rules){
2820
+ const override = options.overrides[rule.id];
2821
+ if (override) rule.level = override;
2567
2822
  }
2568
- /**
2569
- * Remove skill from configuration
2570
- *
2571
- * @returns true if skill was removed, false if it didn't exist
2572
- */ removeSkill(name) {
2573
- if (this._noManifest) return false;
2574
- this.ensureConfigLoaded();
2575
- if (this.config?.skills[name]) {
2576
- delete this.config.skills[name];
2577
- this.save();
2578
- return true;
2579
- }
2580
- return false;
2823
+ if (options?.customRules?.length) rules.push(...options.customRules);
2824
+ return rules;
2825
+ }
2826
+ /**
2827
+ * Content scanner for SKILL.md files.
2828
+ *
2829
+ * Detects prompt injection, data exfiltration, obfuscation, sensitive file
2830
+ * access, stealth instructions, and oversized content.
2831
+ *
2832
+ * @example
2833
+ * ```typescript
2834
+ * // Default usage (CLI)
2835
+ * const scanner = new ContentScanner();
2836
+ * const result = scanner.scan(content);
2837
+ *
2838
+ * // Custom usage (private registry server)
2839
+ * const scanner = new ContentScanner({
2840
+ * overrides: { 'prompt-injection': 'medium' },
2841
+ * disabledRules: ['stealth-instructions'],
2842
+ * });
2843
+ * ```
2844
+ */ class ContentScanner {
2845
+ rules;
2846
+ constructor(options){
2847
+ this.rules = buildRuleSet(options);
2581
2848
  }
2582
2849
  /**
2583
- * Get all skills as a shallow copy
2584
- */ getSkills() {
2585
- if (!this.config) {
2586
- if (!this.exists()) return {};
2587
- this.load();
2850
+ * Scan content string for malicious patterns.
2851
+ * Pure string operation — no file system access.
2852
+ */ scan(content) {
2853
+ const originalLines = content.split('\n');
2854
+ const maskedContent = maskSafeZones(content);
2855
+ const findings = [];
2856
+ for (const rule of this.rules){
2857
+ const targetContent = rule.skipSafeZones ? maskedContent : content;
2858
+ const matches = rule.check(targetContent);
2859
+ for (const match of matches){
2860
+ // Use custom snippet if provided, otherwise generate from original content
2861
+ const snippet = match.snippet ?? (null != match.line ? originalLines[match.line - 1]?.trim().slice(0, SNIPPET_MAX_LENGTH) : void 0);
2862
+ findings.push({
2863
+ rule: rule.id,
2864
+ level: rule.level,
2865
+ message: rule.message,
2866
+ line: match.line,
2867
+ snippet
2868
+ });
2869
+ }
2588
2870
  }
2871
+ const hasHighRisk = findings.some((f)=>'high' === f.level);
2589
2872
  return {
2590
- ...this.config?.skills
2873
+ passed: !hasHighRisk,
2874
+ findings
2591
2875
  };
2592
2876
  }
2593
2877
  /**
2594
- * Check if skill exists in configuration
2595
- */ hasSkill(name) {
2596
- const skills = this.getSkills();
2597
- return name in skills;
2598
- }
2599
- /**
2600
- * Get skill reference by name
2601
- */ getSkillRef(name) {
2602
- const skills = this.getSkills();
2603
- return skills[name];
2604
- }
2605
- // ==========================================================================
2606
- // Private Helpers
2607
- // ==========================================================================
2608
- /**
2609
- * Get loaded config or default (does not throw)
2610
- */ getConfigOrDefault() {
2611
- if (this.config) return this.config;
2612
- if (this.exists()) return this.load();
2613
- return DEFAULT_SKILLS_JSON;
2878
+ * Scan a file for malicious patterns.
2879
+ * Convenience wrapper that reads the file then calls scan().
2880
+ */ scanFile(filePath) {
2881
+ if (!external_node_fs_.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
2882
+ const content = external_node_fs_.readFileSync(filePath, 'utf-8');
2883
+ return this.scan(content);
2614
2884
  }
2615
- /**
2616
- * Ensure config is loaded into memory
2617
- */ ensureConfigLoaded() {
2618
- if (!this.config) this.load();
2885
+ }
2886
+ // ============================================================================
2887
+ // ContentScanError
2888
+ // ============================================================================
2889
+ /**
2890
+ * Error thrown when content scanning detects high-risk findings.
2891
+ * Carries the full findings array for display purposes.
2892
+ */ class ContentScanError extends Error {
2893
+ findings;
2894
+ constructor(findings){
2895
+ const highCount = findings.filter((f)=>'high' === f.level).length;
2896
+ super(`Content security scan failed: ${highCount} high-risk finding(s) detected`);
2897
+ this.name = 'ContentScanError';
2898
+ this.findings = findings;
2619
2899
  }
2620
2900
  }
2621
2901
  /**
@@ -3200,7 +3480,7 @@ ${CURSOR_BRIDGE_MARKER}
3200
3480
  /**
3201
3481
  * Check if lock file exists
3202
3482
  */ exists() {
3203
- return exists(this.lockPath);
3483
+ return fs_exists(this.lockPath);
3204
3484
  }
3205
3485
  /**
3206
3486
  * Load lock file
@@ -4449,11 +4729,11 @@ class RegistryResolver {
4449
4729
  */ getSkillPath(name) {
4450
4730
  // Check canonical location first (.agents/skills/)
4451
4731
  const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
4452
- if (exists(canonicalPath)) return canonicalPath;
4732
+ if (fs_exists(canonicalPath)) return canonicalPath;
4453
4733
  // Check configured installation directory (.skills/ or custom)
4454
4734
  const installDir = this.getInstallDir();
4455
4735
  const installPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(installDir, name);
4456
- if (exists(installPath)) return installPath;
4736
+ if (fs_exists(installPath)) return installPath;
4457
4737
  // Default to configured installation directory for new installations
4458
4738
  // if it's not the default .skills, otherwise use canonical location.
4459
4739
  // This respects "installDir" in skills.json.
@@ -4533,7 +4813,7 @@ class RegistryResolver {
4533
4813
  const semanticVersion = metadata?.version ?? gitRef;
4534
4814
  const skillPath = this.getSkillPath(skillName);
4535
4815
  // Check if already installed (using the real name from SKILL.md)
4536
- if (exists(skillPath) && !force) {
4816
+ if (fs_exists(skillPath) && !force) {
4537
4817
  const locked = this.lockManager.get(skillName);
4538
4818
  // Compare ref if available, fallback to version for backward compatibility
4539
4819
  const lockedRef = locked?.ref || locked?.version;
@@ -4550,10 +4830,10 @@ class RegistryResolver {
4550
4830
  }
4551
4831
  logger_logger["package"](`Installing ${skillName}@${gitRef}...`);
4552
4832
  // Copy to installation directory
4553
- ensureDir(this.getInstallDir());
4554
- if (exists(skillPath)) remove(skillPath);
4833
+ fs_ensureDir(this.getInstallDir());
4834
+ if (fs_exists(skillPath)) fs_remove(skillPath);
4555
4835
  copyDir(sourcePath, skillPath, {
4556
- exclude: DEFAULT_EXCLUDE_FILES
4836
+ exclude: installer_DEFAULT_EXCLUDE_FILES
4557
4837
  });
4558
4838
  // Update lock file (project mode only)
4559
4839
  if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
@@ -4600,7 +4880,7 @@ class RegistryResolver {
4600
4880
  const semanticVersion = metadata?.version ?? version;
4601
4881
  const skillPath = this.getSkillPath(skillName);
4602
4882
  // Check if already installed (using the real name from SKILL.md)
4603
- if (exists(skillPath) && !force) {
4883
+ if (fs_exists(skillPath) && !force) {
4604
4884
  const locked = this.lockManager.get(skillName);
4605
4885
  const lockedRef = locked?.ref || locked?.version;
4606
4886
  if (locked && lockedRef === version) {
@@ -4616,8 +4896,8 @@ class RegistryResolver {
4616
4896
  }
4617
4897
  logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host}...`);
4618
4898
  // Copy to installation directory
4619
- ensureDir(this.getInstallDir());
4620
- if (exists(skillPath)) remove(skillPath);
4899
+ fs_ensureDir(this.getInstallDir());
4900
+ if (fs_exists(skillPath)) fs_remove(skillPath);
4621
4901
  await this.cache.copyTo(parsed, version, skillPath);
4622
4902
  // Update lock file (project mode only)
4623
4903
  if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
@@ -4662,13 +4942,13 @@ class RegistryResolver {
4662
4942
  * Uninstall skill
4663
4943
  */ uninstall(name) {
4664
4944
  const skillPath = this.getSkillPath(name);
4665
- if (!exists(skillPath)) {
4945
+ if (!fs_exists(skillPath)) {
4666
4946
  const location = this.isGlobal ? '(global)' : '';
4667
4947
  logger_logger.warn(`Skill ${name} is not installed ${location}`.trim());
4668
4948
  return false;
4669
4949
  }
4670
4950
  // Remove installation directory
4671
- remove(skillPath);
4951
+ fs_remove(skillPath);
4672
4952
  // Remove from lock file (project mode only)
4673
4953
  if (!this.isGlobal) this.lockManager.remove(name);
4674
4954
  // Remove from skills.json (project mode only)
@@ -4833,7 +5113,7 @@ class RegistryResolver {
4833
5113
  const seenNames = new Set();
4834
5114
  // Check canonical location first (.agents/skills/)
4835
5115
  const canonicalDir = this.getCanonicalSkillsDir();
4836
- if (exists(canonicalDir)) for (const name of listDir(canonicalDir)){
5116
+ if (fs_exists(canonicalDir)) for (const name of listDir(canonicalDir)){
4837
5117
  const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalDir, name);
4838
5118
  if (!isDirectory(skillPath)) continue;
4839
5119
  const skill = this.getInstalledSkillFromPath(name, skillPath);
@@ -4844,7 +5124,7 @@ class RegistryResolver {
4844
5124
  }
4845
5125
  // Check legacy location (.skills/)
4846
5126
  const legacyDir = this.getInstallDir();
4847
- if (exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
5127
+ if (fs_exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
4848
5128
  // Skip if already found in canonical location
4849
5129
  if (seenNames.has(name)) continue;
4850
5130
  const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(legacyDir, name);
@@ -4874,18 +5154,26 @@ class RegistryResolver {
4874
5154
  const canonicalDir = this.getCanonicalSkillsDir();
4875
5155
  const installed = [];
4876
5156
  for (const [type, config] of Object.entries(agents)){
5157
+ if (type === CLAUDE_COWORK_3P_AGENT) {
5158
+ try {
5159
+ if (fs_exists(getClaude3pSkillPath(skillName))) installed.push(type);
5160
+ } catch {
5161
+ // Claude Cowork 3P keeps skills under an app-managed account directory.
5162
+ }
5163
+ continue;
5164
+ }
4877
5165
  const agentBase = this.isGlobal ? config.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.projectRoot, config.skillsDir);
4878
5166
  // Skip agents whose skillsDir is the canonical directory itself
4879
5167
  if (__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(agentBase) === __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(canonicalDir)) continue;
4880
5168
  const agentSkillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(agentBase, skillName);
4881
- if (exists(agentSkillDir)) installed.push(type);
5169
+ if (fs_exists(agentSkillDir)) installed.push(type);
4882
5170
  }
4883
5171
  return installed;
4884
5172
  }
4885
5173
  /**
4886
5174
  * Get installed skill information from a specific path
4887
5175
  */ getInstalledSkillFromPath(name, skillPath) {
4888
- if (!exists(skillPath)) return null;
5176
+ if (!fs_exists(skillPath)) return null;
4889
5177
  const isLinked = isSymlink(skillPath);
4890
5178
  const locked = this.lockManager.get(name);
4891
5179
  // Read metadata from SKILL.md (sole source per agentskills.io spec)
@@ -4908,10 +5196,10 @@ class RegistryResolver {
4908
5196
  */ getInstalledSkill(name) {
4909
5197
  // Check canonical location first (.agents/skills/)
4910
5198
  const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
4911
- if (exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
5199
+ if (fs_exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
4912
5200
  // Check legacy location (.skills/)
4913
5201
  const legacyPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getInstallDir(), name);
4914
- if (exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
5202
+ if (fs_exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
4915
5203
  return null;
4916
5204
  }
4917
5205
  /**
@@ -5322,7 +5610,7 @@ class RegistryResolver {
5322
5610
  const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
5323
5611
  // 2. Check if already installed (skip if --force)
5324
5612
  const skillPath = this.getSkillPath(shortName);
5325
- if (exists(skillPath) && !force) {
5613
+ if (fs_exists(skillPath) && !force) {
5326
5614
  const locked = this.lockManager.get(shortName);
5327
5615
  const lockedVersion = locked?.version;
5328
5616
  // Same version already installed
@@ -5359,8 +5647,8 @@ class RegistryResolver {
5359
5647
  logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
5360
5648
  // 3. Create temp directory for extraction (clean stale files first)
5361
5649
  const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
5362
- await remove(tempDir);
5363
- await ensureDir(tempDir);
5650
+ await fs_remove(tempDir);
5651
+ await fs_ensureDir(tempDir);
5364
5652
  try {
5365
5653
  // 4. Extract tarball
5366
5654
  const extractedPath = await this.registryResolver.extract(tarball, tempDir);
@@ -5417,7 +5705,7 @@ class RegistryResolver {
5417
5705
  };
5418
5706
  } finally{
5419
5707
  // Clean up temp directory after installation
5420
- await remove(tempDir);
5708
+ await fs_remove(tempDir);
5421
5709
  }
5422
5710
  }
5423
5711
  // ============================================================================
@@ -5536,8 +5824,8 @@ class RegistryResolver {
5536
5824
  logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
5537
5825
  // Extract tarball to temp directory (clean stale files first)
5538
5826
  const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
5539
- await remove(tempDir);
5540
- await ensureDir(tempDir);
5827
+ await fs_remove(tempDir);
5828
+ await fs_ensureDir(tempDir);
5541
5829
  try {
5542
5830
  const extractedPath = await this.registryResolver.extract(tarball, tempDir);
5543
5831
  logger_logger.debug(`Extracted to ${extractedPath}`);
@@ -5588,7 +5876,7 @@ class RegistryResolver {
5588
5876
  };
5589
5877
  } finally{
5590
5878
  // Clean up temp directory after installation
5591
- await remove(tempDir);
5879
+ await fs_remove(tempDir);
5592
5880
  }
5593
5881
  }
5594
5882
  /**
@@ -5648,4 +5936,4 @@ class RegistryResolver {
5648
5936
  return results;
5649
5937
  }
5650
5938
  }
5651
- export { CacheManager, config_loader_ConfigLoader as ConfigLoader, ContentScanError, ContentScanner, DEFAULT_REGISTRIES, DEFAULT_RULES, GitResolver, HttpResolver, Installer, LockManager, SkillManager, SkillValidationError, agents, detectInstalledAgents, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, getCanonicalSkillPath, getCanonicalSkillsDir, hasValidSkillMd, isPathSafe, isValidAgentType, logger_logger as logger, maskSafeZones, parseSkillFromDir, parseSkillMd, skill_parser_parseSkillMdFile as parseSkillMdFile, sanitizeName, shortenPath, validateSkillDescription, validateSkillName };
5939
+ export { CacheManager, config_loader_ConfigLoader as ConfigLoader, ContentScanError, ContentScanner, DEFAULT_REGISTRIES, DEFAULT_RULES, GitResolver, HttpResolver, Installer, LockManager, SkillManager, SkillValidationError, agents, detectInstalledAgents, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, getCanonicalSkillPath, getCanonicalSkillsDir, hasValidSkillMd, fs_isPathSafe as isPathSafe, isValidAgentType, logger_logger as logger, maskSafeZones, parseSkillFromDir, parseSkillMd, skill_parser_parseSkillMdFile as parseSkillMdFile, sanitizeName, shortenPath, validateSkillDescription, validateSkillName };