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/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/cli/index.js +1401 -1118
- package/dist/core/agent-registry.d.ts +6 -2
- package/dist/core/agent-registry.d.ts.map +1 -1
- package/dist/core/claude-3p-installer.d.ts +28 -0
- package/dist/core/claude-3p-installer.d.ts.map +1 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/installer.d.ts.map +1 -1
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1412 -1124
- package/package.json +1 -1
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
|
-
*
|
|
36
|
+
* Skill Parser - SKILL.md parser
|
|
37
37
|
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
51
|
-
* - YAML frontmatter
|
|
52
|
-
* -
|
|
53
|
-
* -
|
|
54
|
-
* -
|
|
55
|
-
|
|
56
|
-
*
|
|
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
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
//
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
//
|
|
115
|
-
|
|
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
|
-
|
|
154
|
+
// Save last accumulated value
|
|
155
|
+
flushCurrent();
|
|
156
|
+
return {
|
|
157
|
+
data,
|
|
158
|
+
content: markdownContent
|
|
159
|
+
};
|
|
120
160
|
}
|
|
121
|
-
/**
|
|
122
|
-
|
|
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
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
348
|
-
return
|
|
257
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
258
|
+
return parseSkillMd(content, options);
|
|
349
259
|
}
|
|
350
260
|
/**
|
|
351
|
-
*
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
*
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
*
|
|
415
|
-
*
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
*
|
|
379
|
+
* Filter skills by name (case-insensitive exact match).
|
|
427
380
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
|
|
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
|
-
*
|
|
433
|
-
*/
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
609
|
-
if (!
|
|
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
|
|
616
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1455
|
+
* Installer - Multi-Agent installer
|
|
1206
1456
|
*
|
|
1207
|
-
*
|
|
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
|
-
*
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
*
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
*
|
|
1225
|
-
|
|
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
|
-
*
|
|
1228
|
-
* -
|
|
1229
|
-
* -
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
*
|
|
1332
|
-
*/ function
|
|
1333
|
-
if (!
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
*
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
-
*
|
|
1527
|
+
* Copy directory with file exclusion
|
|
1364
1528
|
*
|
|
1365
|
-
*
|
|
1366
|
-
* -
|
|
1367
|
-
* -
|
|
1368
|
-
*/ function
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
*
|
|
1548
|
+
* Create symbolic link
|
|
1375
1549
|
*
|
|
1376
|
-
* @
|
|
1377
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
if (
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1841
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
2026
|
-
|
|
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
|
-
|
|
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 (!
|
|
2040
|
-
|
|
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
|
-
|
|
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 (
|
|
2076
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
2006
|
+
fs_remove(skillDir);
|
|
2117
2007
|
}
|
|
2118
2008
|
}
|
|
2119
2009
|
/**
|
|
2120
2010
|
* Clear all cache
|
|
2121
2011
|
*/ clearAll() {
|
|
2122
|
-
|
|
2012
|
+
fs_remove(this.cacheDir);
|
|
2123
2013
|
}
|
|
2124
2014
|
/**
|
|
2125
2015
|
* Get cache statistics
|
|
2126
2016
|
*/ getStats() {
|
|
2127
|
-
if (!
|
|
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
|
|
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
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
-
*
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
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
|
-
|
|
2873
|
+
passed: !hasHighRisk,
|
|
2874
|
+
findings
|
|
2591
2875
|
};
|
|
2592
2876
|
}
|
|
2593
2877
|
/**
|
|
2594
|
-
*
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
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
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
4554
|
-
if (
|
|
4833
|
+
fs_ensureDir(this.getInstallDir());
|
|
4834
|
+
if (fs_exists(skillPath)) fs_remove(skillPath);
|
|
4555
4835
|
copyDir(sourcePath, skillPath, {
|
|
4556
|
-
exclude:
|
|
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 (
|
|
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
|
-
|
|
4620
|
-
if (
|
|
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 (!
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
5363
|
-
await
|
|
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
|
|
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
|
|
5540
|
-
await
|
|
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
|
|
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 };
|