reskill 1.20.3 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -156,10 +156,616 @@ var external_node_fs_ = __webpack_require__("node:fs");
156
156
  └────────────────────────────────────────────────────┘
157
157
  `;
158
158
  }
159
+ /**
160
+ * Skill Parser - SKILL.md parser
161
+ *
162
+ * Following agentskills.io specification: https://agentskills.io/specification
163
+ *
164
+ * SKILL.md format requirements:
165
+ * - YAML frontmatter containing name and description (required)
166
+ * - name: max 64 characters, lowercase letters, numbers, hyphens
167
+ * - description: max 1024 characters
168
+ * - Optional fields: license, compatibility, metadata, allowed-tools
169
+ */ /**
170
+ * Skill validation error
171
+ */ class SkillValidationError extends Error {
172
+ field;
173
+ constructor(message, field){
174
+ super(message), this.field = field;
175
+ this.name = 'SkillValidationError';
176
+ }
177
+ }
178
+ /**
179
+ * Simple YAML frontmatter parser
180
+ * Parses --- delimited YAML header
181
+ *
182
+ * Supports:
183
+ * - Basic key: value pairs
184
+ * - Multiline strings (| and >)
185
+ * - Nested objects (one level deep, for metadata field)
186
+ */ function parseFrontmatter(content) {
187
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
188
+ const match = content.match(frontmatterRegex);
189
+ if (!match) return {
190
+ data: {},
191
+ content
192
+ };
193
+ const yamlContent = match[1];
194
+ const markdownContent = match[2];
195
+ // Simple YAML parsing (supports basic key: value, one level of nesting,
196
+ // block scalars (| and >), and plain scalars spanning multiple indented lines)
197
+ const data = {};
198
+ const lines = yamlContent.split('\n');
199
+ let currentKey = '';
200
+ let currentValue = '';
201
+ let inMultiline = false;
202
+ let inNestedObject = false;
203
+ let inPlainScalar = false;
204
+ let nestedObject = {};
205
+ /**
206
+ * Save the current key/value accumulated so far, then reset state.
207
+ */ function flushCurrent() {
208
+ if (!currentKey) return;
209
+ if (inNestedObject) {
210
+ data[currentKey] = nestedObject;
211
+ nestedObject = {};
212
+ inNestedObject = false;
213
+ } else if (inPlainScalar || inMultiline) {
214
+ data[currentKey] = currentValue.trim();
215
+ inPlainScalar = false;
216
+ inMultiline = false;
217
+ } else data[currentKey] = parseYamlValue(currentValue.trim());
218
+ currentKey = '';
219
+ currentValue = '';
220
+ }
221
+ for (const line of lines){
222
+ const trimmedLine = line.trim();
223
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
224
+ const isIndented = line.startsWith(' ');
225
+ // ---- Inside a block scalar (| or >) ----
226
+ if (inMultiline) {
227
+ if (isIndented) {
228
+ currentValue += (currentValue ? '\n' : '') + line.slice(2);
229
+ continue;
230
+ }
231
+ // Unindented line ends the block scalar — fall through to top-level parsing
232
+ flushCurrent();
233
+ }
234
+ // ---- Inside a plain scalar (multiline value without | or >) ----
235
+ if (inPlainScalar) {
236
+ if (isIndented) {
237
+ // Continuation line: join with a space (YAML plain scalar folding)
238
+ currentValue += ` ${trimmedLine}`;
239
+ continue;
240
+ }
241
+ // Unindented line ends the plain scalar — fall through to top-level parsing
242
+ flushCurrent();
243
+ }
244
+ // ---- Inside a nested object ----
245
+ if (inNestedObject && isIndented) {
246
+ const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
247
+ if (nestedMatch) {
248
+ const [, nestedKey, nestedValue] = nestedMatch;
249
+ nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
250
+ continue;
251
+ }
252
+ // Indented line that isn't a nested key:value — this key was actually
253
+ // a plain scalar, not a nested object. Switch modes.
254
+ inNestedObject = false;
255
+ inPlainScalar = true;
256
+ currentValue = trimmedLine;
257
+ continue;
258
+ }
259
+ // ---- Top-level key: value ----
260
+ const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
261
+ if (keyValueMatch) {
262
+ flushCurrent();
263
+ currentKey = keyValueMatch[1];
264
+ currentValue = keyValueMatch[2];
265
+ if ('|' === currentValue || '>' === currentValue) {
266
+ inMultiline = true;
267
+ currentValue = '';
268
+ } else if ('' === currentValue) {
269
+ // Empty value — could be nested object or plain scalar; peek at next lines
270
+ inNestedObject = true;
271
+ nestedObject = {};
272
+ }
273
+ continue;
274
+ }
275
+ // ---- Unindented line that isn't key:value while in nested object ----
276
+ if (inNestedObject) flushCurrent();
277
+ }
278
+ // Save last accumulated value
279
+ flushCurrent();
280
+ return {
281
+ data,
282
+ content: markdownContent
283
+ };
284
+ }
285
+ /**
286
+ * Parse YAML value
287
+ */ function parseYamlValue(value) {
288
+ if (!value) return '';
289
+ // Boolean value
290
+ if ('true' === value) return true;
291
+ if ('false' === value) return false;
292
+ // Number
293
+ if (/^-?\d+$/.test(value)) return parseInt(value, 10);
294
+ if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
295
+ // Remove quotes
296
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
297
+ return value;
298
+ }
299
+ /**
300
+ * Validate skill name format
301
+ *
302
+ * Specification requirements:
303
+ * - Max 64 characters
304
+ * - Only lowercase letters, numbers, hyphens allowed
305
+ * - Cannot start or end with hyphen
306
+ * - Cannot contain consecutive hyphens
307
+ */ function validateSkillName(name) {
308
+ if (!name) throw new SkillValidationError('Skill name is required', 'name');
309
+ if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
310
+ if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
311
+ if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
312
+ if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
313
+ 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');
314
+ // Single character name
315
+ if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
316
+ }
317
+ /**
318
+ * Validate skill description
319
+ *
320
+ * Specification requirements:
321
+ * - Max 1024 characters
322
+ * - Angle brackets are allowed per agentskills.io spec
323
+ */ function validateSkillDescription(description) {
324
+ if (!description) throw new SkillValidationError('Skill description is required', 'description');
325
+ if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
326
+ // Note: angle brackets are allowed per agentskills.io spec
327
+ }
328
+ /**
329
+ * Parse SKILL.md content
330
+ *
331
+ * @param content - SKILL.md file content
332
+ * @param options - Parse options
333
+ * @returns Parsed skill info, or null if format is invalid
334
+ * @throws SkillValidationError if validation fails in strict mode
335
+ */ function parseSkillMd(content, options = {}) {
336
+ const { strict = false } = options;
337
+ try {
338
+ const { data, content: body } = parseFrontmatter(content);
339
+ // Check required fields
340
+ if (!data.name || !data.description) {
341
+ if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
342
+ return null;
343
+ }
344
+ const name = String(data.name);
345
+ const description = String(data.description);
346
+ // Validate field format
347
+ if (strict) {
348
+ validateSkillName(name);
349
+ validateSkillDescription(description);
350
+ }
351
+ // Parse allowed-tools
352
+ let allowedTools;
353
+ if (data['allowed-tools']) {
354
+ const toolsStr = String(data['allowed-tools']);
355
+ allowedTools = toolsStr.split(/\s+/).filter(Boolean);
356
+ }
357
+ return {
358
+ name,
359
+ description,
360
+ version: data.version ? String(data.version) : void 0,
361
+ license: data.license ? String(data.license) : void 0,
362
+ compatibility: data.compatibility ? String(data.compatibility) : void 0,
363
+ metadata: data.metadata,
364
+ allowedTools,
365
+ content: body,
366
+ rawContent: content
367
+ };
368
+ } catch (error) {
369
+ if (error instanceof SkillValidationError) throw error;
370
+ if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
371
+ return null;
372
+ }
373
+ }
374
+ /**
375
+ * Parse SKILL.md from file path
376
+ */ function parseSkillMdFile(filePath, options = {}) {
377
+ if (!external_node_fs_.existsSync(filePath)) {
378
+ if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
379
+ return null;
380
+ }
381
+ const content = external_node_fs_.readFileSync(filePath, 'utf-8');
382
+ return parseSkillMd(content, options);
383
+ }
384
+ /**
385
+ * Parse SKILL.md from skill directory
386
+ */ function parseSkillFromDir(dirPath, options = {}) {
387
+ const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
388
+ return parseSkillMdFile(skillMdPath, options);
389
+ }
390
+ /**
391
+ * Check if directory contains valid SKILL.md
392
+ */ function hasValidSkillMd(dirPath) {
393
+ const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
394
+ if (!external_node_fs_.existsSync(skillMdPath)) return false;
395
+ try {
396
+ const skill = parseSkillMdFile(skillMdPath);
397
+ return null !== skill;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+ const SKIP_DIRS = [
403
+ 'node_modules',
404
+ '.git',
405
+ 'dist',
406
+ 'build',
407
+ '__pycache__'
408
+ ];
409
+ const MAX_DISCOVER_DEPTH = 5;
410
+ const PRIORITY_SKILL_DIRS = [
411
+ 'skills',
412
+ '.agents/skills',
413
+ '.cursor/skills',
414
+ '.claude/skills',
415
+ '.windsurf/skills',
416
+ '.github/skills'
417
+ ];
418
+ function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
419
+ if (depth > maxDepth) return [];
420
+ const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
421
+ if (visitedDirs.has(resolvedDir)) return [];
422
+ if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
423
+ visitedDirs.add(resolvedDir);
424
+ const results = [];
425
+ let entries;
426
+ try {
427
+ entries = external_node_fs_.readdirSync(dir);
428
+ } catch {
429
+ return [];
430
+ }
431
+ for (const entry of entries){
432
+ if (SKIP_DIRS.includes(entry)) continue;
433
+ const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
434
+ const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
435
+ if (visitedDirs.has(resolvedFull)) continue;
436
+ let stat;
437
+ try {
438
+ stat = external_node_fs_.statSync(fullPath);
439
+ } catch {
440
+ continue;
441
+ }
442
+ if (!!stat.isDirectory()) {
443
+ if (hasValidSkillMd(fullPath)) results.push(fullPath);
444
+ results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
445
+ }
446
+ }
447
+ return results;
448
+ }
449
+ /**
450
+ * Discover all skills in a directory by scanning for SKILL.md files.
451
+ *
452
+ * Strategy:
453
+ * 1. Check root for SKILL.md
454
+ * 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
455
+ * 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
456
+ *
457
+ * @param basePath - Root directory to search
458
+ * @returns List of parsed skills with their directory paths (absolute)
459
+ */ function discoverSkillsInDir(basePath) {
460
+ const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
461
+ const results = [];
462
+ const seenNames = new Set();
463
+ function addSkill(dirPath) {
464
+ const skill = parseSkillFromDir(dirPath);
465
+ if (skill && !seenNames.has(skill.name)) {
466
+ seenNames.add(skill.name);
467
+ results.push({
468
+ ...skill,
469
+ dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
470
+ });
471
+ }
472
+ }
473
+ if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
474
+ // Track visited directories to avoid redundant I/O during recursive scan
475
+ const visitedDirs = new Set();
476
+ for (const sub of PRIORITY_SKILL_DIRS){
477
+ const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
478
+ if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
479
+ visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
480
+ try {
481
+ const entries = external_node_fs_.readdirSync(dir);
482
+ for (const entry of entries){
483
+ const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
484
+ try {
485
+ if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
486
+ addSkill(skillDir);
487
+ visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
488
+ }
489
+ } catch {
490
+ // Skip entries that can't be stat'd (race condition, permission, etc.)
491
+ }
492
+ }
493
+ } catch {
494
+ // Skip if unreadable
495
+ }
496
+ }
497
+ }
498
+ const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
499
+ for (const skillDir of recursiveDirs)addSkill(skillDir);
500
+ return results;
501
+ }
502
+ /**
503
+ * Filter skills by name (case-insensitive exact match).
504
+ *
505
+ * Note: an empty `names` array returns an empty result (not all skills).
506
+ * Callers should check `names.length` before calling if "no filter = all" is desired.
507
+ *
508
+ * @param skills - List of discovered skills
509
+ * @param names - Skill names to match (e.g. from --skill pdf commit)
510
+ * @returns Skills whose name matches any of the given names
511
+ */ function filterSkillsByName(skills, names) {
512
+ const normalized = names.map((n)=>n.toLowerCase());
513
+ return skills.filter((skill)=>{
514
+ // Match against SKILL.md name field
515
+ if (normalized.includes(skill.name.toLowerCase())) return true;
516
+ // Also match against the directory name (basename of dirPath)
517
+ // Users naturally refer to skills by their directory name
518
+ const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
519
+ return normalized.includes(dirName);
520
+ });
521
+ }
522
+ const claude_3p_installer_CLAUDE_COWORK_3P_AGENT = 'claude-cowork-3p';
523
+ const CLAUDE_3P_SKILLS_ROOT_ENV = 'CLAUDE_3P_SKILLS_ROOT';
524
+ const CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV = 'CLAUDE_3P_SKILLS_PLUGIN_BASE';
525
+ const DEFAULT_EXCLUDE_FILES = [
526
+ 'README.md',
527
+ 'metadata.json',
528
+ '.reskill-commit'
529
+ ];
530
+ const EXCLUDE_PREFIX = '_';
531
+ const MAX_MANIFEST_BACKUPS = 10;
532
+ function exists(targetPath) {
533
+ return external_node_fs_.existsSync(targetPath);
534
+ }
535
+ function ensureDir(dirPath) {
536
+ if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
537
+ recursive: true
538
+ });
539
+ }
540
+ function remove(targetPath) {
541
+ if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
542
+ recursive: true,
543
+ force: true
544
+ });
545
+ }
546
+ function isSafeSkillId(name) {
547
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && '.' !== name && '..' !== name;
548
+ }
549
+ function isPathSafe(basePath, targetPath) {
550
+ const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
551
+ const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
552
+ return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep);
553
+ }
554
+ function getSafeSkillPath(root, skillName) {
555
+ if (!isSafeSkillId(skillName)) throw new Error(`Skill name is not safe for Claude Cowork 3P: ${skillName}`);
556
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
557
+ const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, skillName);
558
+ if (!isPathSafe(skillsDir, skillPath)) throw new Error(`Skill path escapes Claude Cowork 3P skills directory: ${skillName}`);
559
+ return skillPath;
560
+ }
561
+ function isSkillsRoot(root) {
562
+ return exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json')) && exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills'));
563
+ }
564
+ function copyDirectory(src, dest) {
565
+ ensureDir(dest);
566
+ for (const entry of external_node_fs_.readdirSync(src, {
567
+ withFileTypes: true
568
+ })){
569
+ if (DEFAULT_EXCLUDE_FILES.includes(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
570
+ const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
571
+ const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
572
+ if (entry.isDirectory()) copyDirectory(srcPath, destPath);
573
+ else if (entry.isFile()) external_node_fs_.copyFileSync(srcPath, destPath);
574
+ }
575
+ }
576
+ function getClaude3pSkillsPluginBase(options = {}) {
577
+ const env = options.env ?? process.env;
578
+ const explicitBase = env[CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV];
579
+ if (explicitBase) return explicitBase;
580
+ const homeDir = options.homeDir ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
581
+ const currentPlatform = options.platform ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.platform)();
582
+ if ('darwin' === currentPlatform) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, 'Library', 'Application Support', 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
583
+ 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');
584
+ 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');
585
+ }
586
+ function findClaude3pSkillsRoots(options = {}) {
587
+ const env = options.env ?? process.env;
588
+ const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
589
+ if (explicitRoot) return isSkillsRoot(explicitRoot) ? [
590
+ explicitRoot
591
+ ] : [];
592
+ const base = getClaude3pSkillsPluginBase(options);
593
+ if (!exists(base)) return [];
594
+ const roots = [];
595
+ for (const organization of external_node_fs_.readdirSync(base, {
596
+ withFileTypes: true
597
+ })){
598
+ if (!organization.isDirectory()) continue;
599
+ const organizationPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(base, organization.name);
600
+ for (const account of external_node_fs_.readdirSync(organizationPath, {
601
+ withFileTypes: true
602
+ })){
603
+ if (!account.isDirectory()) continue;
604
+ const candidate = __WEBPACK_EXTERNAL_MODULE_node_path__.join(organizationPath, account.name);
605
+ if (isSkillsRoot(candidate)) roots.push(candidate);
606
+ }
607
+ }
608
+ return roots;
609
+ }
610
+ function claude_3p_installer_resolveClaude3pSkillsRoot(options = {}) {
611
+ const env = options.env ?? process.env;
612
+ const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
613
+ if (explicitRoot) {
614
+ if (!isSkillsRoot(explicitRoot)) throw new Error(`${CLAUDE_3P_SKILLS_ROOT_ENV} must contain manifest.json and skills/: ${explicitRoot}`);
615
+ return explicitRoot;
616
+ }
617
+ const roots = findClaude3pSkillsRoots(options);
618
+ if (1 === roots.length) return roots[0];
619
+ const base = getClaude3pSkillsPluginBase(options);
620
+ 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/.`);
621
+ 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')}`);
622
+ }
623
+ function getClaude3pSkillPath(skillName) {
624
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
625
+ return getSafeSkillPath(root, skillName);
626
+ }
627
+ function readManifest(manifestPath) {
628
+ const content = external_node_fs_.readFileSync(manifestPath, 'utf-8');
629
+ return JSON.parse(content);
630
+ }
631
+ function writeManifest(manifestPath, manifest) {
632
+ external_node_fs_.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
633
+ }
634
+ function createManifestBackup(manifestPath) {
635
+ const backupPath = `${manifestPath}.bak.${Date.now()}`;
636
+ external_node_fs_.copyFileSync(manifestPath, backupPath);
637
+ return backupPath;
638
+ }
639
+ function pruneManifestBackups(manifestPath) {
640
+ const manifestDir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(manifestPath);
641
+ const backupPrefix = `${__WEBPACK_EXTERNAL_MODULE_node_path__.basename(manifestPath)}.bak.`;
642
+ const backups = external_node_fs_.readdirSync(manifestDir, {
643
+ withFileTypes: true
644
+ }).filter((entry)=>entry.isFile() && entry.name.startsWith(backupPrefix)).map((entry)=>({
645
+ name: entry.name,
646
+ path: __WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name),
647
+ mtimeMs: external_node_fs_.statSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name)).mtimeMs
648
+ })).sort((a, b)=>b.mtimeMs - a.mtimeMs);
649
+ for (const backup of backups.slice(MAX_MANIFEST_BACKUPS))remove(backup.path);
650
+ }
651
+ function updateManifest(manifestPath, skillId, name, description) {
652
+ const manifest = readManifest(manifestPath);
653
+ if (!Array.isArray(manifest.skills)) manifest.skills = [];
654
+ const entry = {
655
+ skillId,
656
+ name,
657
+ description,
658
+ creatorType: 'user',
659
+ syncManaged: false,
660
+ updatedAt: new Date().toISOString(),
661
+ enabled: true
662
+ };
663
+ const index = manifest.skills.findIndex((skill)=>skill.skillId === skillId || skill.name === name);
664
+ if (index >= 0) manifest.skills[index] = {
665
+ ...manifest.skills[index],
666
+ ...entry
667
+ };
668
+ else manifest.skills.push(entry);
669
+ manifest.lastUpdated = Date.now();
670
+ writeManifest(manifestPath, manifest);
671
+ }
672
+ function removeFromManifest(manifestPath, skillId) {
673
+ const manifest = readManifest(manifestPath);
674
+ if (!Array.isArray(manifest.skills)) return;
675
+ const before = manifest.skills.length;
676
+ manifest.skills = manifest.skills.filter((skill)=>skill.skillId !== skillId && skill.name !== skillId);
677
+ if (manifest.skills.length !== before) {
678
+ manifest.lastUpdated = Date.now();
679
+ writeManifest(manifestPath, manifest);
680
+ }
681
+ }
682
+ function installClaude3pSkill(sourcePath, fallbackName, _options = {}) {
683
+ const installMode = 'copy';
684
+ let manifestPath;
685
+ let manifestBackupPath;
686
+ let targetPath;
687
+ let tmpTarget;
688
+ let rollbackTarget;
689
+ try {
690
+ const metadata = parseSkillFromDir(sourcePath);
691
+ const skillId = metadata?.name ?? fallbackName;
692
+ const description = metadata?.description ?? '';
693
+ if (!isSafeSkillId(skillId)) return {
694
+ success: false,
695
+ path: '',
696
+ mode: installMode,
697
+ error: `SKILL.md name is not safe for Claude Cowork 3P: ${skillId}`
698
+ };
699
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
700
+ manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
701
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
702
+ targetPath = getSafeSkillPath(root, skillId);
703
+ tmpTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.installing.${process.pid}`);
704
+ rollbackTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.rollback.${process.pid}`);
705
+ if (!isPathSafe(skillsDir, tmpTarget)) throw new Error(`Temporary skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
706
+ if (!isPathSafe(skillsDir, rollbackTarget)) throw new Error(`Rollback skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
707
+ manifestBackupPath = createManifestBackup(manifestPath);
708
+ remove(tmpTarget);
709
+ remove(rollbackTarget);
710
+ copyDirectory(sourcePath, tmpTarget);
711
+ if (exists(targetPath)) external_node_fs_.renameSync(targetPath, rollbackTarget);
712
+ external_node_fs_.renameSync(tmpTarget, targetPath);
713
+ try {
714
+ updateManifest(manifestPath, skillId, skillId, description);
715
+ } catch (error) {
716
+ remove(targetPath);
717
+ if (rollbackTarget && exists(rollbackTarget)) external_node_fs_.renameSync(rollbackTarget, targetPath);
718
+ if (manifestBackupPath && manifestPath && exists(manifestBackupPath)) external_node_fs_.copyFileSync(manifestBackupPath, manifestPath);
719
+ throw error;
720
+ }
721
+ if (rollbackTarget) remove(rollbackTarget);
722
+ pruneManifestBackups(manifestPath);
723
+ return {
724
+ success: true,
725
+ path: targetPath,
726
+ mode: installMode
727
+ };
728
+ } catch (error) {
729
+ if (tmpTarget) remove(tmpTarget);
730
+ if (targetPath && rollbackTarget && exists(rollbackTarget) && !exists(targetPath)) try {
731
+ external_node_fs_.renameSync(rollbackTarget, targetPath);
732
+ } catch {
733
+ // Ignore rollback errors while reporting the original installation failure.
734
+ }
735
+ if (manifestPath) try {
736
+ pruneManifestBackups(manifestPath);
737
+ } catch {
738
+ // Ignore cleanup errors while reporting the original installation failure.
739
+ }
740
+ return {
741
+ success: false,
742
+ path: '',
743
+ mode: installMode,
744
+ error: error instanceof Error ? error.message : 'Unknown error'
745
+ };
746
+ }
747
+ }
748
+ function uninstallClaude3pSkill(skillName) {
749
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
750
+ const targetPath = getSafeSkillPath(root, skillName);
751
+ const manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
752
+ const existed = exists(targetPath);
753
+ if (existed) remove(targetPath);
754
+ removeFromManifest(manifestPath, skillName);
755
+ return existed;
756
+ }
757
+ function listClaude3pSkills() {
758
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
759
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
760
+ if (!exists(skillsDir)) return [];
761
+ return external_node_fs_.readdirSync(skillsDir, {
762
+ withFileTypes: true
763
+ }).filter((entry)=>entry.isDirectory() && isSafeSkillId(entry.name)).map((entry)=>entry.name);
764
+ }
159
765
  /**
160
766
  * Agent Registry - Multi-Agent configuration definitions
161
767
  *
162
- * Supports global and project-level installation for 17 coding agents
768
+ * Supports global and project-level installation for 18 coding agents
163
769
  * Reference: https://github.com/vercel-labs/add-skill
164
770
  */ const agent_registry_home = (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
165
771
  /**
@@ -186,6 +792,20 @@ var external_node_fs_ = __webpack_require__("node:fs");
186
792
  globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude/skills'),
187
793
  detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude'))
188
794
  },
795
+ [claude_3p_installer_CLAUDE_COWORK_3P_AGENT]: {
796
+ name: claude_3p_installer_CLAUDE_COWORK_3P_AGENT,
797
+ displayName: 'Claude Cowork 3P',
798
+ skillsDir: '.claude-3p/skills',
799
+ globalSkillsDir: getClaude3pSkillsPluginBase(),
800
+ detectInstalled: async ()=>{
801
+ try {
802
+ claude_3p_installer_resolveClaude3pSkillsRoot();
803
+ return true;
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+ },
189
809
  clawdbot: {
190
810
  name: 'clawdbot',
191
811
  displayName: 'Clawdbot',
@@ -306,1151 +926,788 @@ var external_node_fs_ = __webpack_require__("node:fs");
306
926
  * File system utilities
307
927
  */ /**
308
928
  * Check if a file or directory exists
309
- */ function exists(filePath) {
929
+ */ function fs_exists(filePath) {
310
930
  return external_node_fs_.existsSync(filePath);
311
931
  }
312
932
  /**
313
933
  * Read JSON file
314
934
  */ function readJson(filePath) {
315
- const content = external_node_fs_.readFileSync(filePath, 'utf-8');
316
- return JSON.parse(content);
317
- }
318
- /**
319
- * Write JSON file
320
- */ function writeJson(filePath, data, indent = 2) {
321
- const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
322
- if (!exists(dir)) external_node_fs_.mkdirSync(dir, {
323
- recursive: true
324
- });
325
- external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
326
- }
327
- /**
328
- * Create directory recursively
329
- */ function ensureDir(dirPath) {
330
- if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
331
- recursive: true
332
- });
333
- }
334
- /**
335
- * Remove file or directory
336
- */ function remove(targetPath) {
337
- if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
338
- recursive: true,
339
- force: true
340
- });
341
- }
342
- /**
343
- * Copy directory recursively
344
- *
345
- * @param src - Source directory
346
- * @param dest - Destination directory
347
- * @param options.exclude - Array of filenames to exclude
348
- * @param options.excludePrefix - Prefix for files to exclude (e.g., '_' to exclude _private.md)
349
- */ function copyDir(src, dest, options) {
350
- const exclude = options?.exclude || [];
351
- const excludePrefix = options?.excludePrefix || '_';
352
- ensureDir(dest);
353
- const entries = external_node_fs_.readdirSync(src, {
354
- withFileTypes: true
355
- });
356
- for (const entry of entries){
357
- // Skip files in exclude list or starting with excludePrefix
358
- if (exclude.includes(entry.name) || entry.name.startsWith(excludePrefix)) continue;
359
- const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
360
- const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
361
- if (entry.isDirectory()) copyDir(srcPath, destPath, options);
362
- else external_node_fs_.copyFileSync(srcPath, destPath);
363
- }
364
- }
365
- /**
366
- * List directory contents
367
- */ function listDir(dirPath) {
368
- if (!exists(dirPath)) return [];
369
- return external_node_fs_.readdirSync(dirPath);
370
- }
371
- /**
372
- * Check if path is a directory
373
- */ function isDirectory(targetPath) {
374
- if (!exists(targetPath)) return false;
375
- return external_node_fs_.statSync(targetPath).isDirectory();
376
- }
377
- /**
378
- * Check if path is a symbolic link
379
- */ function isSymlink(targetPath) {
380
- if (!exists(targetPath)) return false;
381
- return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
382
- }
383
- /**
384
- * Get real path of symbolic link
385
- */ function getRealPath(linkPath) {
386
- return external_node_fs_.realpathSync(linkPath);
387
- }
388
- /**
389
- * Get skills.json path for current project
390
- */ function getSkillsJsonPath(projectRoot) {
391
- const root = projectRoot || process.cwd();
392
- return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.json');
393
- }
394
- /**
395
- * Get skills.lock path for current project
396
- */ function getSkillsLockPath(projectRoot) {
397
- const root = projectRoot || process.cwd();
398
- return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.lock');
399
- }
400
- /**
401
- * Get global cache directory
402
- */ function getCacheDir() {
403
- const home = process.env.HOME || process.env.USERPROFILE || '';
404
- return process.env.RESKILL_CACHE_DIR || __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.reskill-cache');
405
- }
406
- /**
407
- * Get home directory
408
- */ function getHomeDir() {
409
- return process.env.HOME || process.env.USERPROFILE || '';
410
- }
411
- /**
412
- * Shorten path display (replace home directory with ~)
413
- */ function shortenPath(fullPath, cwd) {
414
- const home = getHomeDir();
415
- const currentDir = cwd || process.cwd();
416
- if (fullPath.startsWith(home)) return fullPath.replace(home, '~');
417
- if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
418
- return fullPath;
419
- }
420
- const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
421
- /**
422
- * Git utilities
423
- */ /**
424
- * SSH command with auto-accept for new host keys
425
- * Uses StrictHostKeyChecking=accept-new which:
426
- * - Automatically accepts keys for hosts not in known_hosts
427
- * - Still rejects connections if a known host's key has changed (security)
428
- */ const GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes';
429
- /**
430
- * Get environment variables for git commands that access remote repositories
431
- * Configures SSH to auto-accept new host keys and disables interactive prompts
432
- */ function getGitEnv() {
433
- return {
434
- ...process.env,
435
- GIT_SSH_COMMAND,
436
- // Disable interactive prompts for HTTPS as well
437
- GIT_TERMINAL_PROMPT: '0'
438
- };
439
- }
440
- /**
441
- * Custom error class for Git clone failures
442
- * Provides helpful tips for private repository authentication
443
- */ class GitCloneError extends Error {
444
- repoUrl;
445
- originalError;
446
- isAuthError;
447
- urlType;
448
- constructor(repoUrl, originalError){
449
- const isAuthError = GitCloneError.isAuthenticationError(originalError.message);
450
- const urlType = GitCloneError.detectUrlType(repoUrl);
451
- let message = `Failed to clone repository: ${repoUrl}`;
452
- if (isAuthError) {
453
- message += '\n\nTip: For private repos, ensure git credentials are configured:';
454
- if ('ssh' === urlType) {
455
- message += '\n - Check ~/.ssh/id_rsa or ~/.ssh/id_ed25519';
456
- message += '\n - Ensure SSH key is added to your Git hosting service';
457
- } else {
458
- // HTTPS or unknown
459
- message += "\n - Run 'git config --global credential.helper store'";
460
- message += '\n - Or use a personal access token in the URL';
461
- }
462
- }
463
- super(message);
464
- this.name = 'GitCloneError';
465
- this.repoUrl = repoUrl;
466
- this.originalError = originalError;
467
- this.isAuthError = isAuthError;
468
- this.urlType = urlType;
469
- }
470
- /**
471
- * Detect URL type from repository URL
472
- */ static detectUrlType(url) {
473
- if (url.startsWith('git@') || url.startsWith('ssh://')) return 'ssh';
474
- if (url.startsWith('http://') || url.startsWith('https://')) return 'https';
475
- return 'unknown';
476
- }
477
- /**
478
- * Check if an error message indicates an authentication problem
479
- */ static isAuthenticationError(message) {
480
- const authPatterns = [
481
- /permission denied/i,
482
- /could not read from remote/i,
483
- /authentication failed/i,
484
- /fatal: repository.*not found/i,
485
- /host key verification failed/i,
486
- /access denied/i,
487
- /unauthorized/i,
488
- /403/,
489
- /401/
490
- ];
491
- return authPatterns.some((pattern)=>pattern.test(message));
492
- }
493
- }
494
- /**
495
- * Execute git command asynchronously
496
- */ async function git(args, cwd) {
497
- const { stdout } = await git_execAsync(`git ${args.join(' ')}`, {
498
- cwd,
499
- encoding: 'utf-8',
500
- env: getGitEnv()
935
+ const content = external_node_fs_.readFileSync(filePath, 'utf-8');
936
+ return JSON.parse(content);
937
+ }
938
+ /**
939
+ * Write JSON file
940
+ */ function writeJson(filePath, data, indent = 2) {
941
+ const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
942
+ if (!fs_exists(dir)) external_node_fs_.mkdirSync(dir, {
943
+ recursive: true
501
944
  });
502
- return stdout.trim();
945
+ external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
503
946
  }
504
947
  /**
505
- * Get remote tags for a repository
506
- */ async function getRemoteTags(repoUrl) {
507
- try {
508
- const output = await git([
509
- 'ls-remote',
510
- '--tags',
511
- '--refs',
512
- repoUrl
513
- ]);
514
- if (!output) return [];
515
- const tags = [];
516
- const lines = output.split('\n');
517
- for (const line of lines){
518
- const [commit, ref] = line.split('\t');
519
- if (commit && ref) {
520
- // Extract tag name from refs/tags/v1.0.0
521
- const tagName = ref.replace('refs/tags/', '');
522
- tags.push({
523
- name: tagName,
524
- commit
525
- });
526
- }
527
- }
528
- return tags;
529
- } catch {
530
- return [];
531
- }
948
+ * Create directory recursively
949
+ */ function fs_ensureDir(dirPath) {
950
+ if (!fs_exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
951
+ recursive: true
952
+ });
532
953
  }
533
954
  /**
534
- * Get latest tag from repository
535
- */ async function getLatestTag(repoUrl) {
536
- const tags = await getRemoteTags(repoUrl);
537
- if (0 === tags.length) return null;
538
- // Sort by semver (simple version sort)
539
- const sortedTags = tags.sort((a, b)=>{
540
- const aVer = a.name.replace(/^v/, '');
541
- const bVer = b.name.replace(/^v/, '');
542
- return compareVersions(bVer, aVer);
955
+ * Remove file or directory
956
+ */ function fs_remove(targetPath) {
957
+ if (fs_exists(targetPath)) external_node_fs_.rmSync(targetPath, {
958
+ recursive: true,
959
+ force: true
543
960
  });
544
- return sortedTags[0];
545
961
  }
546
962
  /**
547
- * Clone a repository with shallow clone
963
+ * Copy directory recursively
548
964
  *
549
- * @throws {GitCloneError} When clone fails, with helpful tips for authentication issues
550
- */ async function clone(repoUrl, destPath, options) {
551
- const args = [
552
- 'clone'
553
- ];
554
- if (options?.depth) args.push('--depth', options.depth.toString());
555
- if (options?.branch) args.push('--branch', options.branch);
556
- args.push(repoUrl, destPath);
557
- try {
558
- await git(args);
559
- } catch (error) {
560
- throw new GitCloneError(repoUrl, error);
965
+ * @param src - Source directory
966
+ * @param dest - Destination directory
967
+ * @param options.exclude - Array of filenames to exclude
968
+ * @param options.excludePrefix - Prefix for files to exclude (e.g., '_' to exclude _private.md)
969
+ */ function copyDir(src, dest, options) {
970
+ const exclude = options?.exclude || [];
971
+ const excludePrefix = options?.excludePrefix || '_';
972
+ fs_ensureDir(dest);
973
+ const entries = external_node_fs_.readdirSync(src, {
974
+ withFileTypes: true
975
+ });
976
+ for (const entry of entries){
977
+ // Skip files in exclude list or starting with excludePrefix
978
+ if (exclude.includes(entry.name) || entry.name.startsWith(excludePrefix)) continue;
979
+ const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
980
+ const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
981
+ if (entry.isDirectory()) copyDir(srcPath, destPath, options);
982
+ else external_node_fs_.copyFileSync(srcPath, destPath);
561
983
  }
562
984
  }
563
985
  /**
564
- * Get current commit hash
565
- */ async function getCurrentCommit(cwd) {
566
- return git([
567
- 'rev-parse',
568
- 'HEAD'
569
- ], cwd);
986
+ * List directory contents
987
+ */ function listDir(dirPath) {
988
+ if (!fs_exists(dirPath)) return [];
989
+ return external_node_fs_.readdirSync(dirPath);
570
990
  }
571
991
  /**
572
- * Get default branch name
573
- */ async function getDefaultBranch(repoUrl) {
574
- try {
575
- const output = await git([
576
- 'ls-remote',
577
- '--symref',
578
- repoUrl,
579
- 'HEAD'
580
- ]);
581
- const match = output.match(/ref: refs\/heads\/(\S+)/);
582
- return match ? match[1] : 'main';
583
- } catch {
584
- return 'main';
585
- }
992
+ * Check if path is a directory
993
+ */ function isDirectory(targetPath) {
994
+ if (!fs_exists(targetPath)) return false;
995
+ return external_node_fs_.statSync(targetPath).isDirectory();
586
996
  }
587
997
  /**
588
- * Simple version comparison (for sorting)
589
- */ function compareVersions(a, b) {
590
- const aParts = a.split('.').map((p)=>parseInt(p, 10) || 0);
591
- const bParts = b.split('.').map((p)=>parseInt(p, 10) || 0);
592
- const maxLength = Math.max(aParts.length, bParts.length);
593
- for(let i = 0; i < maxLength; i++){
594
- const aPart = aParts[i] || 0;
595
- const bPart = bParts[i] || 0;
596
- if (aPart > bPart) return 1;
597
- if (aPart < bPart) return -1;
598
- }
599
- return 0;
998
+ * Check if path is a symbolic link
999
+ */ function isSymlink(targetPath) {
1000
+ if (!fs_exists(targetPath)) return false;
1001
+ return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
600
1002
  }
601
1003
  /**
602
- * Check if a source string is a complete Git URL (SSH, HTTPS, git://, or file://)
603
- *
604
- * Supported formats:
605
- * - SSH: git@github.com:user/repo.git
606
- * - HTTPS: https://github.com/user/repo.git
607
- * - Git protocol: git://github.com/user/repo.git
608
- * - File protocol: file:///path/to/repo (for local testing)
609
- * - URLs ending with .git
610
- */ function isGitUrl(source) {
611
- return source.startsWith('git@') || source.startsWith('git://') || source.startsWith('http://') || source.startsWith('https://') || source.startsWith('file://') || source.endsWith('.git');
1004
+ * Get real path of symbolic link
1005
+ */ function getRealPath(linkPath) {
1006
+ return external_node_fs_.realpathSync(linkPath);
612
1007
  }
613
1008
  /**
614
- * Parse a Git URL and extract host, owner, and repo information
615
- *
616
- * Supports:
617
- * - SSH: git@github.com:user/repo.git
618
- * - HTTPS: https://github.com/user/repo.git
619
- * - Git protocol: git://github.com/user/repo.git
620
- *
621
- * Note: GitHub/GitLab web URLs (with /tree/, /blob/, etc.) are handled
622
- * at a higher level in GitResolver.parseGitUrlRef() before calling this function.
623
- *
624
- * @param url The Git URL to parse
625
- * @returns Parsed URL information or null if parsing fails
626
- */ function parseGitUrl(url) {
627
- // Remove trailing .git if present
628
- const cleanUrl = url.replace(/\.git$/, '');
629
- // SSH format: git@github.com:user/repo
630
- const sshMatch = cleanUrl.match(/^git@([^:]+):(.+)$/);
631
- if (sshMatch) {
632
- const [, host, path] = sshMatch;
633
- const parts = path.split('/');
634
- if (parts.length >= 2) {
635
- // Handle nested paths like org/sub/repo
636
- const owner = parts.slice(0, -1).join('/');
637
- const repo = parts[parts.length - 1];
638
- return {
639
- host,
640
- owner,
641
- repo,
642
- url,
643
- type: 'ssh'
644
- };
645
- }
646
- }
647
- // HTTPS/Git protocol format: https://github.com/user/repo or git://github.com/user/repo
648
- const httpMatch = cleanUrl.match(/^(https?|git):\/\/([^/]+)\/(.+)$/);
649
- if (httpMatch) {
650
- const [, protocol, host, path] = httpMatch;
651
- const parts = path.split('/');
652
- if (parts.length >= 2) {
653
- const owner = parts.slice(0, -1).join('/');
654
- const repo = parts[parts.length - 1];
655
- return {
656
- host,
657
- owner,
658
- repo,
659
- url,
660
- type: 'git' === protocol ? 'git' : 'https'
661
- };
662
- }
663
- }
664
- // File protocol format: file:///path/to/repo
665
- // Used for local testing and development
666
- const fileMatch = cleanUrl.match(/^file:\/\/(.+)$/);
667
- if (fileMatch) {
668
- const [, filePath] = fileMatch;
669
- const parts = filePath.split('/').filter(Boolean);
670
- if (parts.length >= 1) {
671
- // Use 'local' as host, path components as owner/repo
672
- const repo = parts[parts.length - 1];
673
- const owner = parts.length > 1 ? parts[parts.length - 2] : 'local';
674
- return {
675
- host: 'local',
676
- owner,
677
- repo,
678
- url,
679
- type: 'file'
680
- };
681
- }
682
- }
683
- return null;
1009
+ * Get skills.json path for current project
1010
+ */ function getSkillsJsonPath(projectRoot) {
1011
+ const root = projectRoot || process.cwd();
1012
+ return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.json');
684
1013
  }
685
1014
  /**
686
- * Registry-Scope Mapping Utilities
687
- *
688
- * Maps registry URLs to their corresponding scopes.
689
- * Currently hardcoded; TODO: fetch from /api/registry/info in the future.
690
- */ /**
691
- * Public Registry URL
692
- * Used for installing skills without a scope
693
- */ const PUBLIC_REGISTRY = 'https://reskill.info/';
694
- /**
695
- * Hardcoded registry to scope mapping
696
- * TODO: Replace with dynamic fetching from /api/registry/info
697
- */ const REGISTRY_SCOPE_MAP = {
698
- // rush-app (private registry, new)
699
- 'https://rush-test.zhenguanyu.com': '@kanyun-test',
700
- 'https://rush.zhenguanyu.com': '@kanyun',
701
- // reskill-app (private registry, legacy)
702
- 'https://reskill-test.zhenguanyu.com': '@kanyun-test',
703
- // Local development
704
- 'http://localhost:3000': '@kanyun-test'
705
- };
1015
+ * Get skills.lock path for current project
1016
+ */ function getSkillsLockPath(projectRoot) {
1017
+ const root = projectRoot || process.cwd();
1018
+ return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.lock');
1019
+ }
706
1020
  /**
707
- * Get the scope for a given registry URL
708
- *
709
- * @param registry - Registry URL
710
- * @returns Scope string (e.g., "@kanyun") or null if not found
711
- *
712
- * @example
713
- * getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
714
- * getScopeForRegistry('https://unknown.com') // null
715
- */ function getScopeForRegistry(registry) {
716
- if (!registry) return null;
717
- // Try exact match first
718
- if (REGISTRY_SCOPE_MAP[registry]) return REGISTRY_SCOPE_MAP[registry];
719
- // Try with/without trailing slash
720
- const normalized = registry.endsWith('/') ? registry.slice(0, -1) : `${registry}/`;
721
- return REGISTRY_SCOPE_MAP[normalized] || null;
1021
+ * Get global cache directory
1022
+ */ function getCacheDir() {
1023
+ const home = process.env.HOME || process.env.USERPROFILE || '';
1024
+ return process.env.RESKILL_CACHE_DIR || __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.reskill-cache');
722
1025
  }
723
1026
  /**
724
- * Get the registry URL for a given scope (reverse lookup)
725
- *
726
- * @param scope - Scope string (with or without @ prefix), e.g., "@kanyun" or "kanyun"
727
- * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
728
- * @returns Registry URL (with trailing slash) or null if not found
729
- *
730
- * @example
731
- * getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
732
- * getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
733
- * getRegistryForScope('@unknown') // null
734
- * getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
735
- */ function getRegistryForScope(scope, customRegistries) {
736
- if (!scope) return null;
737
- // Normalize scope: ensure @ prefix
738
- const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
739
- // 1. First check custom scopeRegistries (from skills.json)
740
- if (customRegistries?.[normalizedScope]) {
741
- const url = customRegistries[normalizedScope];
742
- // Normalize trailing slash
743
- return url.endsWith('/') ? url : `${url}/`;
744
- }
745
- // 2. Fall back to hardcoded defaults
746
- for (const [registry, registryScope] of Object.entries(REGISTRY_SCOPE_MAP))if (registryScope === normalizedScope) // Return URL with trailing slash (normalized format)
747
- return registry.endsWith('/') ? registry : `${registry}/`;
748
- return null;
1027
+ * Get home directory
1028
+ */ function getHomeDir() {
1029
+ return process.env.HOME || process.env.USERPROFILE || '';
749
1030
  }
750
1031
  /**
751
- * Get the registry URL for a given scope
752
- *
753
- * - With scope → lookup private Registry (throws if not found)
754
- * - Without scope (null/undefined/'') → returns public Registry
755
- *
756
- * @param scope - Scope string (with or without @ prefix), null, undefined, or empty string
757
- * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
758
- * @returns Registry URL (with trailing slash)
759
- * @throws Error if scope is provided but not found in the registry map
760
- *
761
- * @example
762
- * getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
763
- * getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
764
- * getRegistryUrl(null) // 'https://reskill.info/'
765
- * getRegistryUrl('') // 'https://reskill.info/'
766
- * getRegistryUrl('@unknown') // throws Error
767
- * getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
768
- */ function getRegistryUrl(scope, customRegistries) {
769
- // No scope → return public Registry
770
- if (!scope) return PUBLIC_REGISTRY;
771
- // With scope → lookup private Registry
772
- const registry = getRegistryForScope(scope, customRegistries);
773
- if (!registry) {
774
- // Normalize scope for error message
775
- const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
776
- throw new Error(`Unknown scope ${normalizedScope}. No registry configured for this scope.`);
777
- }
778
- return registry;
1032
+ * Shorten path display (replace home directory with ~)
1033
+ */ function shortenPath(fullPath, cwd) {
1034
+ const home = getHomeDir();
1035
+ const currentDir = cwd || process.cwd();
1036
+ if (fullPath.startsWith(home)) return fullPath.replace(home, '~');
1037
+ if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
1038
+ return fullPath;
779
1039
  }
1040
+ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
780
1041
  /**
781
- * Parse a skill name into its components
782
- *
783
- * @param skillName - Full or short skill name
784
- * @returns Parsed skill name with scope and name
785
- *
786
- * @example
787
- * parseSkillName('@kanyun/planning-with-files')
788
- * // { scope: '@kanyun', name: 'planning-with-files', fullName: '@kanyun/planning-with-files' }
789
- *
790
- * parseSkillName('planning-with-files')
791
- * // { scope: null, name: 'planning-with-files', fullName: 'planning-with-files' }
792
- */ function parseSkillName(skillName) {
793
- // Match @scope/name pattern
794
- const match = skillName.match(/^(@[^/]+)\/(.+)$/);
795
- if (match) return {
796
- scope: match[1],
797
- name: match[2],
798
- fullName: skillName
799
- };
1042
+ * Git utilities
1043
+ */ /**
1044
+ * SSH command with auto-accept for new host keys
1045
+ * Uses StrictHostKeyChecking=accept-new which:
1046
+ * - Automatically accepts keys for hosts not in known_hosts
1047
+ * - Still rejects connections if a known host's key has changed (security)
1048
+ */ const GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes';
1049
+ /**
1050
+ * Get environment variables for git commands that access remote repositories
1051
+ * Configures SSH to auto-accept new host keys and disables interactive prompts
1052
+ */ function getGitEnv() {
800
1053
  return {
801
- scope: null,
802
- name: skillName,
803
- fullName: skillName
1054
+ ...process.env,
1055
+ GIT_SSH_COMMAND,
1056
+ // Disable interactive prompts for HTTPS as well
1057
+ GIT_TERMINAL_PROMPT: '0'
804
1058
  };
805
1059
  }
806
1060
  /**
807
- * Build full skill name from scope and name
808
- *
809
- * @param scope - Scope (with or without @ prefix), or null
810
- * @param name - Short skill name
811
- * @returns Full skill name (e.g., "@kanyun/planning-with-files")
812
- *
813
- * @example
814
- * buildFullSkillName('@kanyun', 'planning-with-files') // '@kanyun/planning-with-files'
815
- * buildFullSkillName('kanyun', 'my-skill') // '@kanyun/my-skill'
816
- * buildFullSkillName(null, 'my-skill') // 'my-skill'
817
- */ function buildFullSkillName(scope, name) {
818
- if (!scope) return name;
819
- // Ensure scope starts with @
820
- const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
821
- return `${normalizedScope}/${name}`;
1061
+ * Custom error class for Git clone failures
1062
+ * Provides helpful tips for private repository authentication
1063
+ */ class GitCloneError extends Error {
1064
+ repoUrl;
1065
+ originalError;
1066
+ isAuthError;
1067
+ urlType;
1068
+ constructor(repoUrl, originalError){
1069
+ const isAuthError = GitCloneError.isAuthenticationError(originalError.message);
1070
+ const urlType = GitCloneError.detectUrlType(repoUrl);
1071
+ let message = `Failed to clone repository: ${repoUrl}`;
1072
+ if (isAuthError) {
1073
+ message += '\n\nTip: For private repos, ensure git credentials are configured:';
1074
+ if ('ssh' === urlType) {
1075
+ message += '\n - Check ~/.ssh/id_rsa or ~/.ssh/id_ed25519';
1076
+ message += '\n - Ensure SSH key is added to your Git hosting service';
1077
+ } else {
1078
+ // HTTPS or unknown
1079
+ message += "\n - Run 'git config --global credential.helper store'";
1080
+ message += '\n - Or use a personal access token in the URL';
1081
+ }
1082
+ }
1083
+ super(message);
1084
+ this.name = 'GitCloneError';
1085
+ this.repoUrl = repoUrl;
1086
+ this.originalError = originalError;
1087
+ this.isAuthError = isAuthError;
1088
+ this.urlType = urlType;
1089
+ }
1090
+ /**
1091
+ * Detect URL type from repository URL
1092
+ */ static detectUrlType(url) {
1093
+ if (url.startsWith('git@') || url.startsWith('ssh://')) return 'ssh';
1094
+ if (url.startsWith('http://') || url.startsWith('https://')) return 'https';
1095
+ return 'unknown';
1096
+ }
1097
+ /**
1098
+ * Check if an error message indicates an authentication problem
1099
+ */ static isAuthenticationError(message) {
1100
+ const authPatterns = [
1101
+ /permission denied/i,
1102
+ /could not read from remote/i,
1103
+ /authentication failed/i,
1104
+ /fatal: repository.*not found/i,
1105
+ /host key verification failed/i,
1106
+ /access denied/i,
1107
+ /unauthorized/i,
1108
+ /403/,
1109
+ /401/
1110
+ ];
1111
+ return authPatterns.some((pattern)=>pattern.test(message));
1112
+ }
822
1113
  }
823
1114
  /**
824
- * Get short name from a skill name (removes scope if present)
825
- *
826
- * @param skillName - Full or short skill name
827
- * @returns Short name without scope
828
- *
829
- * @example
830
- * getShortName('@kanyun/planning-with-files') // 'planning-with-files'
831
- * getShortName('planning-with-files') // 'planning-with-files'
832
- */ function getShortName(skillName) {
833
- return parseSkillName(skillName).name;
1115
+ * Execute git command asynchronously
1116
+ */ async function git(args, cwd) {
1117
+ const { stdout } = await git_execAsync(`git ${args.join(' ')}`, {
1118
+ cwd,
1119
+ encoding: 'utf-8',
1120
+ env: getGitEnv()
1121
+ });
1122
+ return stdout.trim();
834
1123
  }
835
1124
  /**
836
- * Parse a skill identifier into its components (with version support)
837
- *
838
- * Supports both private registry (with @scope) and public registry (without scope) formats.
839
- *
840
- * @param identifier - Skill identifier string
841
- * @returns Parsed skill identifier with scope, name, version, and fullName
842
- * @throws Error if identifier is invalid
843
- *
844
- * @example
845
- * // Private registry
846
- * parseSkillIdentifier('@kanyun/planning-with-files')
847
- * // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
848
- *
849
- * parseSkillIdentifier('@kanyun/skill@2.4.5')
850
- * // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
851
- *
852
- * // Public registry
853
- * parseSkillIdentifier('planning-with-files')
854
- * // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
855
- *
856
- * parseSkillIdentifier('skill@latest')
857
- * // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
858
- */ function parseSkillIdentifier(identifier) {
859
- const trimmed = identifier.trim();
860
- // Empty string or whitespace only
861
- if (!trimmed) throw new Error('Invalid skill identifier: empty string');
862
- // Starting with @@ is invalid
863
- if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
864
- // Bare @ is invalid
865
- if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
866
- // Scoped format: @scope/name[@version]
867
- if (trimmed.startsWith('@')) {
868
- // Regex: @scope/name[@version]
869
- // scope: starts with @, followed by alphanumeric, hyphens, underscores
870
- // name: alphanumeric, hyphens, underscores
871
- // version: optional, @ followed by any non-empty string
872
- const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
873
- if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
874
- const [, scope, name, version] = scopedMatch;
875
- return {
876
- scope,
877
- name,
878
- version: version || void 0,
879
- fullName: `${scope}/${name}`
880
- };
1125
+ * Get remote tags for a repository
1126
+ */ async function getRemoteTags(repoUrl) {
1127
+ try {
1128
+ const output = await git([
1129
+ 'ls-remote',
1130
+ '--tags',
1131
+ '--refs',
1132
+ repoUrl
1133
+ ]);
1134
+ if (!output) return [];
1135
+ const tags = [];
1136
+ const lines = output.split('\n');
1137
+ for (const line of lines){
1138
+ const [commit, ref] = line.split('\t');
1139
+ if (commit && ref) {
1140
+ // Extract tag name from refs/tags/v1.0.0
1141
+ const tagName = ref.replace('refs/tags/', '');
1142
+ tags.push({
1143
+ name: tagName,
1144
+ commit
1145
+ });
1146
+ }
1147
+ }
1148
+ return tags;
1149
+ } catch {
1150
+ return [];
881
1151
  }
882
- // Unscoped format: name[@version] (public registry)
883
- // name must not contain / (otherwise it might be a git shorthand)
884
- const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
885
- if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
886
- const [, name, version] = unscopedMatch;
887
- return {
888
- scope: null,
889
- name,
890
- version: version || void 0,
891
- fullName: name
892
- };
893
1152
  }
894
1153
  /**
895
- * HTTP utilities for downloading and extracting skill archives
896
- */ /**
897
- * Custom error class for HTTP download failures
898
- */ class HttpDownloadError extends Error {
899
- url;
900
- statusCode;
901
- originalError;
902
- constructor(url, message, statusCode, originalError){
903
- super(message);
904
- this.name = 'HttpDownloadError';
905
- this.url = url;
906
- this.statusCode = statusCode;
907
- this.originalError = originalError;
908
- }
1154
+ * Get latest tag from repository
1155
+ */ async function getLatestTag(repoUrl) {
1156
+ const tags = await getRemoteTags(repoUrl);
1157
+ if (0 === tags.length) return null;
1158
+ // Sort by semver (simple version sort)
1159
+ const sortedTags = tags.sort((a, b)=>{
1160
+ const aVer = a.name.replace(/^v/, '');
1161
+ const bVer = b.name.replace(/^v/, '');
1162
+ return compareVersions(bVer, aVer);
1163
+ });
1164
+ return sortedTags[0];
909
1165
  }
910
1166
  /**
911
- * Download a file from HTTP/HTTPS URL
1167
+ * Clone a repository with shallow clone
912
1168
  *
913
- * @param url - URL to download from
914
- * @param destPath - Destination file path
915
- * @param options - Download options
916
- */ async function downloadFile(url, destPath, options = {}) {
917
- const { timeout = 60000, headers = {} } = options;
918
- // Ensure destination directory exists
919
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
1169
+ * @throws {GitCloneError} When clone fails, with helpful tips for authentication issues
1170
+ */ async function clone(repoUrl, destPath, options) {
1171
+ const args = [
1172
+ 'clone'
1173
+ ];
1174
+ if (options?.depth) args.push('--depth', options.depth.toString());
1175
+ if (options?.branch) args.push('--branch', options.branch);
1176
+ args.push(repoUrl, destPath);
920
1177
  try {
921
- // Use native fetch for HTTP/HTTPS
922
- const controller = new AbortController();
923
- const timeoutId = setTimeout(()=>controller.abort(), timeout);
924
- const response = await fetch(url, {
925
- signal: controller.signal,
926
- headers: {
927
- 'User-Agent': 'reskill/1.0',
928
- ...headers
929
- }
930
- });
931
- clearTimeout(timeoutId);
932
- if (!response.ok) throw new HttpDownloadError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
933
- // Stream response to file
934
- const fileStream = external_node_fs_.createWriteStream(destPath);
935
- const body = response.body;
936
- if (!body) throw new HttpDownloadError(url, 'Response body is empty');
937
- // Convert Web ReadableStream to Node.js Readable
938
- const { Readable } = await import("node:stream");
939
- const nodeStream = Readable.fromWeb(body);
940
- await (0, __WEBPACK_EXTERNAL_MODULE_node_stream_promises__.pipeline)(nodeStream, fileStream);
1178
+ await git(args);
941
1179
  } catch (error) {
942
- // Clean up partial download
943
- if (external_node_fs_.existsSync(destPath)) external_node_fs_.unlinkSync(destPath);
944
- if (error instanceof HttpDownloadError) throw error;
945
- const err = error;
946
- if ('AbortError' === err.name) throw new HttpDownloadError(url, `Download timeout after ${timeout}ms`);
947
- throw new HttpDownloadError(url, `Download failed: ${err.message}`, void 0, err);
1180
+ throw new GitCloneError(repoUrl, error);
948
1181
  }
949
1182
  }
950
1183
  /**
951
- * Extract an archive to a directory
952
- *
953
- * @param archivePath - Path to the archive file
954
- * @param destDir - Destination directory
955
- * @param format - Archive format (auto-detected from extension if not provided)
956
- */ async function extractArchive(archivePath, destDir, format) {
957
- // Auto-detect format from extension
958
- const detectedFormat = format || detectArchiveFormat(archivePath);
959
- if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
960
- // Ensure destination directory exists
961
- ensureDir(destDir);
962
- switch(detectedFormat){
963
- case 'tar.gz':
964
- case 'tgz':
965
- case 'tar':
966
- await extractTar(archivePath, destDir, 'tar.gz' === detectedFormat || 'tgz' === detectedFormat);
967
- break;
968
- case 'zip':
969
- await extractZip(archivePath, destDir);
970
- break;
971
- default:
972
- throw new Error(`Unsupported archive format: ${detectedFormat}`);
973
- }
1184
+ * Get current commit hash
1185
+ */ async function getCurrentCommit(cwd) {
1186
+ return git([
1187
+ 'rev-parse',
1188
+ 'HEAD'
1189
+ ], cwd);
974
1190
  }
975
1191
  /**
976
- * Extract tar archive using native tar command
977
- */ async function extractTar(archivePath, destDir, gzipped) {
978
- const { exec } = await import("node:child_process");
979
- const { promisify } = await import("node:util");
980
- const execAsync = promisify(exec);
981
- const flags = gzipped ? '-xzf' : '-xf';
1192
+ * Get default branch name
1193
+ */ async function getDefaultBranch(repoUrl) {
982
1194
  try {
983
- // Extract to a temp directory first to handle single-folder archives
984
- const tempExtractDir = `${destDir}.extract-temp`;
985
- ensureDir(tempExtractDir);
986
- await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
987
- encoding: 'utf-8'
988
- });
989
- // Check if archive contains a single root directory
990
- const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
991
- if (1 === extractedItems.length) {
992
- const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
993
- if (external_node_fs_.statSync(singleItem).isDirectory()) {
994
- // Move contents of single directory to destination
995
- const contents = external_node_fs_.readdirSync(singleItem);
996
- for (const item of contents){
997
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
998
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
999
- external_node_fs_.renameSync(src, dest);
1000
- }
1001
- remove(tempExtractDir);
1002
- return;
1003
- }
1004
- }
1005
- // Move all items to destination
1006
- for (const item of extractedItems){
1007
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1008
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1009
- external_node_fs_.renameSync(src, dest);
1010
- }
1011
- remove(tempExtractDir);
1012
- } catch (error) {
1013
- throw new Error(`Failed to extract tar archive: ${error.message}`);
1195
+ const output = await git([
1196
+ 'ls-remote',
1197
+ '--symref',
1198
+ repoUrl,
1199
+ 'HEAD'
1200
+ ]);
1201
+ const match = output.match(/ref: refs\/heads\/(\S+)/);
1202
+ return match ? match[1] : 'main';
1203
+ } catch {
1204
+ return 'main';
1014
1205
  }
1015
1206
  }
1016
1207
  /**
1017
- * Extract zip archive using native unzip command or Node.js
1018
- */ async function extractZip(archivePath, destDir) {
1019
- const { exec } = await import("node:child_process");
1020
- const { promisify } = await import("node:util");
1021
- const execAsync = promisify(exec);
1022
- try {
1023
- // Extract to a temp directory first
1024
- const tempExtractDir = `${destDir}.extract-temp`;
1025
- ensureDir(tempExtractDir);
1026
- // Try using unzip command (available on most systems)
1027
- await execAsync(`unzip -q "${archivePath}" -d "${tempExtractDir}"`, {
1028
- encoding: 'utf-8'
1029
- });
1030
- // Check if archive contains a single root directory
1031
- const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
1032
- if (1 === extractedItems.length) {
1033
- const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
1034
- if (external_node_fs_.statSync(singleItem).isDirectory()) {
1035
- // Move contents of single directory to destination
1036
- const contents = external_node_fs_.readdirSync(singleItem);
1037
- for (const item of contents){
1038
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
1039
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1040
- external_node_fs_.renameSync(src, dest);
1041
- }
1042
- remove(tempExtractDir);
1043
- return;
1044
- }
1208
+ * Simple version comparison (for sorting)
1209
+ */ function compareVersions(a, b) {
1210
+ const aParts = a.split('.').map((p)=>parseInt(p, 10) || 0);
1211
+ const bParts = b.split('.').map((p)=>parseInt(p, 10) || 0);
1212
+ const maxLength = Math.max(aParts.length, bParts.length);
1213
+ for(let i = 0; i < maxLength; i++){
1214
+ const aPart = aParts[i] || 0;
1215
+ const bPart = bParts[i] || 0;
1216
+ if (aPart > bPart) return 1;
1217
+ if (aPart < bPart) return -1;
1218
+ }
1219
+ return 0;
1220
+ }
1221
+ /**
1222
+ * Check if a source string is a complete Git URL (SSH, HTTPS, git://, or file://)
1223
+ *
1224
+ * Supported formats:
1225
+ * - SSH: git@github.com:user/repo.git
1226
+ * - HTTPS: https://github.com/user/repo.git
1227
+ * - Git protocol: git://github.com/user/repo.git
1228
+ * - File protocol: file:///path/to/repo (for local testing)
1229
+ * - URLs ending with .git
1230
+ */ function isGitUrl(source) {
1231
+ return source.startsWith('git@') || source.startsWith('git://') || source.startsWith('http://') || source.startsWith('https://') || source.startsWith('file://') || source.endsWith('.git');
1232
+ }
1233
+ /**
1234
+ * Parse a Git URL and extract host, owner, and repo information
1235
+ *
1236
+ * Supports:
1237
+ * - SSH: git@github.com:user/repo.git
1238
+ * - HTTPS: https://github.com/user/repo.git
1239
+ * - Git protocol: git://github.com/user/repo.git
1240
+ *
1241
+ * Note: GitHub/GitLab web URLs (with /tree/, /blob/, etc.) are handled
1242
+ * at a higher level in GitResolver.parseGitUrlRef() before calling this function.
1243
+ *
1244
+ * @param url The Git URL to parse
1245
+ * @returns Parsed URL information or null if parsing fails
1246
+ */ function parseGitUrl(url) {
1247
+ // Remove trailing .git if present
1248
+ const cleanUrl = url.replace(/\.git$/, '');
1249
+ // SSH format: git@github.com:user/repo
1250
+ const sshMatch = cleanUrl.match(/^git@([^:]+):(.+)$/);
1251
+ if (sshMatch) {
1252
+ const [, host, path] = sshMatch;
1253
+ const parts = path.split('/');
1254
+ if (parts.length >= 2) {
1255
+ // Handle nested paths like org/sub/repo
1256
+ const owner = parts.slice(0, -1).join('/');
1257
+ const repo = parts[parts.length - 1];
1258
+ return {
1259
+ host,
1260
+ owner,
1261
+ repo,
1262
+ url,
1263
+ type: 'ssh'
1264
+ };
1045
1265
  }
1046
- // Move all items to destination
1047
- for (const item of extractedItems){
1048
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1049
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1050
- external_node_fs_.renameSync(src, dest);
1266
+ }
1267
+ // HTTPS/Git protocol format: https://github.com/user/repo or git://github.com/user/repo
1268
+ const httpMatch = cleanUrl.match(/^(https?|git):\/\/([^/]+)\/(.+)$/);
1269
+ if (httpMatch) {
1270
+ const [, protocol, host, path] = httpMatch;
1271
+ const parts = path.split('/');
1272
+ if (parts.length >= 2) {
1273
+ const owner = parts.slice(0, -1).join('/');
1274
+ const repo = parts[parts.length - 1];
1275
+ return {
1276
+ host,
1277
+ owner,
1278
+ repo,
1279
+ url,
1280
+ type: 'git' === protocol ? 'git' : 'https'
1281
+ };
1282
+ }
1283
+ }
1284
+ // File protocol format: file:///path/to/repo
1285
+ // Used for local testing and development
1286
+ const fileMatch = cleanUrl.match(/^file:\/\/(.+)$/);
1287
+ if (fileMatch) {
1288
+ const [, filePath] = fileMatch;
1289
+ const parts = filePath.split('/').filter(Boolean);
1290
+ if (parts.length >= 1) {
1291
+ // Use 'local' as host, path components as owner/repo
1292
+ const repo = parts[parts.length - 1];
1293
+ const owner = parts.length > 1 ? parts[parts.length - 2] : 'local';
1294
+ return {
1295
+ host: 'local',
1296
+ owner,
1297
+ repo,
1298
+ url,
1299
+ type: 'file'
1300
+ };
1051
1301
  }
1052
- remove(tempExtractDir);
1053
- } catch (error) {
1054
- throw new Error(`Failed to extract zip archive: ${error.message}`);
1055
1302
  }
1303
+ return null;
1056
1304
  }
1057
1305
  /**
1058
- * Detect archive format from file path
1059
- */ function detectArchiveFormat(filePath) {
1060
- const lower = filePath.toLowerCase();
1061
- if (lower.endsWith('.tar.gz')) return 'tar.gz';
1062
- if (lower.endsWith('.tgz')) return 'tgz';
1063
- if (lower.endsWith('.zip')) return 'zip';
1064
- if (lower.endsWith('.tar')) return 'tar';
1306
+ * Registry-Scope Mapping Utilities
1307
+ *
1308
+ * Maps registry URLs to their corresponding scopes.
1309
+ * Currently hardcoded; TODO: fetch from /api/registry/info in the future.
1310
+ */ /**
1311
+ * Public Registry URL
1312
+ * Used for installing skills without a scope
1313
+ */ const PUBLIC_REGISTRY = 'https://reskill.info/';
1314
+ /**
1315
+ * Hardcoded registry to scope mapping
1316
+ * TODO: Replace with dynamic fetching from /api/registry/info
1317
+ */ const REGISTRY_SCOPE_MAP = {
1318
+ // rush-app (private registry, new)
1319
+ 'https://rush-test.zhenguanyu.com': '@kanyun-test',
1320
+ 'https://rush.zhenguanyu.com': '@kanyun',
1321
+ // reskill-app (private registry, legacy)
1322
+ 'https://reskill-test.zhenguanyu.com': '@kanyun-test',
1323
+ // Local development
1324
+ 'http://localhost:3000': '@kanyun-test'
1325
+ };
1326
+ /**
1327
+ * Get the scope for a given registry URL
1328
+ *
1329
+ * @param registry - Registry URL
1330
+ * @returns Scope string (e.g., "@kanyun") or null if not found
1331
+ *
1332
+ * @example
1333
+ * getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
1334
+ * getScopeForRegistry('https://unknown.com') // null
1335
+ */ function getScopeForRegistry(registry) {
1336
+ if (!registry) return null;
1337
+ // Try exact match first
1338
+ if (REGISTRY_SCOPE_MAP[registry]) return REGISTRY_SCOPE_MAP[registry];
1339
+ // Try with/without trailing slash
1340
+ const normalized = registry.endsWith('/') ? registry.slice(0, -1) : `${registry}/`;
1341
+ return REGISTRY_SCOPE_MAP[normalized] || null;
1065
1342
  }
1066
1343
  /**
1067
- * Download and extract an archive in one operation
1344
+ * Get the registry URL for a given scope (reverse lookup)
1068
1345
  *
1069
- * @param url - URL to download from
1070
- * @param destDir - Destination directory for extracted contents
1071
- * @param options - Download options
1072
- * @returns Path to extracted contents
1073
- */ async function downloadAndExtract(url, destDir, options = {}) {
1074
- // Determine archive filename from URL
1075
- const urlObj = new URL(url);
1076
- const filename = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(urlObj.pathname);
1077
- // Detect format from original filename before adding .download suffix
1078
- const format = detectArchiveFormat(filename);
1079
- if (!format) throw new Error(`Unable to detect archive format from URL: ${url}`);
1080
- const tempArchive = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, `../${filename}.download`);
1081
- try {
1082
- // Download archive
1083
- await downloadFile(url, tempArchive, options);
1084
- // Extract archive with explicit format
1085
- await extractArchive(tempArchive, destDir, format);
1086
- return destDir;
1087
- } finally{
1088
- // Clean up temp archive
1089
- if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
1346
+ * @param scope - Scope string (with or without @ prefix), e.g., "@kanyun" or "kanyun"
1347
+ * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
1348
+ * @returns Registry URL (with trailing slash) or null if not found
1349
+ *
1350
+ * @example
1351
+ * getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
1352
+ * getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
1353
+ * getRegistryForScope('@unknown') // null
1354
+ * getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
1355
+ */ function getRegistryForScope(scope, customRegistries) {
1356
+ if (!scope) return null;
1357
+ // Normalize scope: ensure @ prefix
1358
+ const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
1359
+ // 1. First check custom scopeRegistries (from skills.json)
1360
+ if (customRegistries?.[normalizedScope]) {
1361
+ const url = customRegistries[normalizedScope];
1362
+ // Normalize trailing slash
1363
+ return url.endsWith('/') ? url : `${url}/`;
1090
1364
  }
1365
+ // 2. Fall back to hardcoded defaults
1366
+ for (const [registry, registryScope] of Object.entries(REGISTRY_SCOPE_MAP))if (registryScope === normalizedScope) // Return URL with trailing slash (normalized format)
1367
+ return registry.endsWith('/') ? registry : `${registry}/`;
1368
+ return null;
1091
1369
  }
1092
1370
  /**
1093
- * Skill Parser - SKILL.md parser
1371
+ * Get the registry URL for a given scope
1094
1372
  *
1095
- * Following agentskills.io specification: https://agentskills.io/specification
1373
+ * - With scope → lookup private Registry (throws if not found)
1374
+ * - Without scope (null/undefined/'') → returns public Registry
1096
1375
  *
1097
- * SKILL.md format requirements:
1098
- * - YAML frontmatter containing name and description (required)
1099
- * - name: max 64 characters, lowercase letters, numbers, hyphens
1100
- * - description: max 1024 characters
1101
- * - Optional fields: license, compatibility, metadata, allowed-tools
1102
- */ /**
1103
- * Skill validation error
1104
- */ class SkillValidationError extends Error {
1105
- field;
1106
- constructor(message, field){
1107
- super(message), this.field = field;
1108
- this.name = 'SkillValidationError';
1376
+ * @param scope - Scope string (with or without @ prefix), null, undefined, or empty string
1377
+ * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
1378
+ * @returns Registry URL (with trailing slash)
1379
+ * @throws Error if scope is provided but not found in the registry map
1380
+ *
1381
+ * @example
1382
+ * getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
1383
+ * getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
1384
+ * getRegistryUrl(null) // 'https://reskill.info/'
1385
+ * getRegistryUrl('') // 'https://reskill.info/'
1386
+ * getRegistryUrl('@unknown') // throws Error
1387
+ * getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
1388
+ */ function getRegistryUrl(scope, customRegistries) {
1389
+ // No scope → return public Registry
1390
+ if (!scope) return PUBLIC_REGISTRY;
1391
+ // With scope → lookup private Registry
1392
+ const registry = getRegistryForScope(scope, customRegistries);
1393
+ if (!registry) {
1394
+ // Normalize scope for error message
1395
+ const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
1396
+ throw new Error(`Unknown scope ${normalizedScope}. No registry configured for this scope.`);
1109
1397
  }
1398
+ return registry;
1110
1399
  }
1111
1400
  /**
1112
- * Simple YAML frontmatter parser
1113
- * Parses --- delimited YAML header
1401
+ * Parse a skill name into its components
1114
1402
  *
1115
- * Supports:
1116
- * - Basic key: value pairs
1117
- * - Multiline strings (| and >)
1118
- * - Nested objects (one level deep, for metadata field)
1119
- */ function parseFrontmatter(content) {
1120
- const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
1121
- const match = content.match(frontmatterRegex);
1122
- if (!match) return {
1123
- data: {},
1124
- content
1125
- };
1126
- const yamlContent = match[1];
1127
- const markdownContent = match[2];
1128
- // Simple YAML parsing (supports basic key: value, one level of nesting,
1129
- // block scalars (| and >), and plain scalars spanning multiple indented lines)
1130
- const data = {};
1131
- const lines = yamlContent.split('\n');
1132
- let currentKey = '';
1133
- let currentValue = '';
1134
- let inMultiline = false;
1135
- let inNestedObject = false;
1136
- let inPlainScalar = false;
1137
- let nestedObject = {};
1138
- /**
1139
- * Save the current key/value accumulated so far, then reset state.
1140
- */ function flushCurrent() {
1141
- if (!currentKey) return;
1142
- if (inNestedObject) {
1143
- data[currentKey] = nestedObject;
1144
- nestedObject = {};
1145
- inNestedObject = false;
1146
- } else if (inPlainScalar || inMultiline) {
1147
- data[currentKey] = currentValue.trim();
1148
- inPlainScalar = false;
1149
- inMultiline = false;
1150
- } else data[currentKey] = parseYamlValue(currentValue.trim());
1151
- currentKey = '';
1152
- currentValue = '';
1153
- }
1154
- for (const line of lines){
1155
- const trimmedLine = line.trim();
1156
- if (!trimmedLine || trimmedLine.startsWith('#')) continue;
1157
- const isIndented = line.startsWith(' ');
1158
- // ---- Inside a block scalar (| or >) ----
1159
- if (inMultiline) {
1160
- if (isIndented) {
1161
- currentValue += (currentValue ? '\n' : '') + line.slice(2);
1162
- continue;
1163
- }
1164
- // Unindented line ends the block scalar — fall through to top-level parsing
1165
- flushCurrent();
1166
- }
1167
- // ---- Inside a plain scalar (multiline value without | or >) ----
1168
- if (inPlainScalar) {
1169
- if (isIndented) {
1170
- // Continuation line: join with a space (YAML plain scalar folding)
1171
- currentValue += ` ${trimmedLine}`;
1172
- continue;
1173
- }
1174
- // Unindented line ends the plain scalar — fall through to top-level parsing
1175
- flushCurrent();
1176
- }
1177
- // ---- Inside a nested object ----
1178
- if (inNestedObject && isIndented) {
1179
- const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
1180
- if (nestedMatch) {
1181
- const [, nestedKey, nestedValue] = nestedMatch;
1182
- nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
1183
- continue;
1184
- }
1185
- // Indented line that isn't a nested key:value — this key was actually
1186
- // a plain scalar, not a nested object. Switch modes.
1187
- inNestedObject = false;
1188
- inPlainScalar = true;
1189
- currentValue = trimmedLine;
1190
- continue;
1191
- }
1192
- // ---- Top-level key: value ----
1193
- const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
1194
- if (keyValueMatch) {
1195
- flushCurrent();
1196
- currentKey = keyValueMatch[1];
1197
- currentValue = keyValueMatch[2];
1198
- if ('|' === currentValue || '>' === currentValue) {
1199
- inMultiline = true;
1200
- currentValue = '';
1201
- } else if ('' === currentValue) {
1202
- // Empty value — could be nested object or plain scalar; peek at next lines
1203
- inNestedObject = true;
1204
- nestedObject = {};
1205
- }
1206
- continue;
1207
- }
1208
- // ---- Unindented line that isn't key:value while in nested object ----
1209
- if (inNestedObject) flushCurrent();
1210
- }
1211
- // Save last accumulated value
1212
- flushCurrent();
1403
+ * @param skillName - Full or short skill name
1404
+ * @returns Parsed skill name with scope and name
1405
+ *
1406
+ * @example
1407
+ * parseSkillName('@kanyun/planning-with-files')
1408
+ * // { scope: '@kanyun', name: 'planning-with-files', fullName: '@kanyun/planning-with-files' }
1409
+ *
1410
+ * parseSkillName('planning-with-files')
1411
+ * // { scope: null, name: 'planning-with-files', fullName: 'planning-with-files' }
1412
+ */ function parseSkillName(skillName) {
1413
+ // Match @scope/name pattern
1414
+ const match = skillName.match(/^(@[^/]+)\/(.+)$/);
1415
+ if (match) return {
1416
+ scope: match[1],
1417
+ name: match[2],
1418
+ fullName: skillName
1419
+ };
1213
1420
  return {
1214
- data,
1215
- content: markdownContent
1421
+ scope: null,
1422
+ name: skillName,
1423
+ fullName: skillName
1216
1424
  };
1217
1425
  }
1218
1426
  /**
1219
- * Parse YAML value
1220
- */ function parseYamlValue(value) {
1221
- if (!value) return '';
1222
- // Boolean value
1223
- if ('true' === value) return true;
1224
- if ('false' === value) return false;
1225
- // Number
1226
- if (/^-?\d+$/.test(value)) return parseInt(value, 10);
1227
- if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
1228
- // Remove quotes
1229
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
1230
- return value;
1231
- }
1232
- /**
1233
- * Validate skill name format
1427
+ * Build full skill name from scope and name
1234
1428
  *
1235
- * Specification requirements:
1236
- * - Max 64 characters
1237
- * - Only lowercase letters, numbers, hyphens allowed
1238
- * - Cannot start or end with hyphen
1239
- * - Cannot contain consecutive hyphens
1240
- */ function validateSkillName(name) {
1241
- if (!name) throw new SkillValidationError('Skill name is required', 'name');
1242
- if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
1243
- if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
1244
- if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
1245
- if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
1246
- 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');
1247
- // Single character name
1248
- if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
1429
+ * @param scope - Scope (with or without @ prefix), or null
1430
+ * @param name - Short skill name
1431
+ * @returns Full skill name (e.g., "@kanyun/planning-with-files")
1432
+ *
1433
+ * @example
1434
+ * buildFullSkillName('@kanyun', 'planning-with-files') // '@kanyun/planning-with-files'
1435
+ * buildFullSkillName('kanyun', 'my-skill') // '@kanyun/my-skill'
1436
+ * buildFullSkillName(null, 'my-skill') // 'my-skill'
1437
+ */ function buildFullSkillName(scope, name) {
1438
+ if (!scope) return name;
1439
+ // Ensure scope starts with @
1440
+ const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
1441
+ return `${normalizedScope}/${name}`;
1249
1442
  }
1250
1443
  /**
1251
- * Validate skill description
1444
+ * Get short name from a skill name (removes scope if present)
1252
1445
  *
1253
- * Specification requirements:
1254
- * - Max 1024 characters
1255
- * - Angle brackets are allowed per agentskills.io spec
1256
- */ function validateSkillDescription(description) {
1257
- if (!description) throw new SkillValidationError('Skill description is required', 'description');
1258
- if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
1259
- // Note: angle brackets are allowed per agentskills.io spec
1446
+ * @param skillName - Full or short skill name
1447
+ * @returns Short name without scope
1448
+ *
1449
+ * @example
1450
+ * getShortName('@kanyun/planning-with-files') // 'planning-with-files'
1451
+ * getShortName('planning-with-files') // 'planning-with-files'
1452
+ */ function getShortName(skillName) {
1453
+ return parseSkillName(skillName).name;
1260
1454
  }
1261
1455
  /**
1262
- * Parse SKILL.md content
1456
+ * Parse a skill identifier into its components (with version support)
1263
1457
  *
1264
- * @param content - SKILL.md file content
1265
- * @param options - Parse options
1266
- * @returns Parsed skill info, or null if format is invalid
1267
- * @throws SkillValidationError if validation fails in strict mode
1268
- */ function parseSkillMd(content, options = {}) {
1269
- const { strict = false } = options;
1270
- try {
1271
- const { data, content: body } = parseFrontmatter(content);
1272
- // Check required fields
1273
- if (!data.name || !data.description) {
1274
- if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
1275
- return null;
1276
- }
1277
- const name = String(data.name);
1278
- const description = String(data.description);
1279
- // Validate field format
1280
- if (strict) {
1281
- validateSkillName(name);
1282
- validateSkillDescription(description);
1283
- }
1284
- // Parse allowed-tools
1285
- let allowedTools;
1286
- if (data['allowed-tools']) {
1287
- const toolsStr = String(data['allowed-tools']);
1288
- allowedTools = toolsStr.split(/\s+/).filter(Boolean);
1289
- }
1458
+ * Supports both private registry (with @scope) and public registry (without scope) formats.
1459
+ *
1460
+ * @param identifier - Skill identifier string
1461
+ * @returns Parsed skill identifier with scope, name, version, and fullName
1462
+ * @throws Error if identifier is invalid
1463
+ *
1464
+ * @example
1465
+ * // Private registry
1466
+ * parseSkillIdentifier('@kanyun/planning-with-files')
1467
+ * // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
1468
+ *
1469
+ * parseSkillIdentifier('@kanyun/skill@2.4.5')
1470
+ * // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
1471
+ *
1472
+ * // Public registry
1473
+ * parseSkillIdentifier('planning-with-files')
1474
+ * // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
1475
+ *
1476
+ * parseSkillIdentifier('skill@latest')
1477
+ * // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
1478
+ */ function parseSkillIdentifier(identifier) {
1479
+ const trimmed = identifier.trim();
1480
+ // Empty string or whitespace only
1481
+ if (!trimmed) throw new Error('Invalid skill identifier: empty string');
1482
+ // Starting with @@ is invalid
1483
+ if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
1484
+ // Bare @ is invalid
1485
+ if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
1486
+ // Scoped format: @scope/name[@version]
1487
+ if (trimmed.startsWith('@')) {
1488
+ // Regex: @scope/name[@version]
1489
+ // scope: starts with @, followed by alphanumeric, hyphens, underscores
1490
+ // name: alphanumeric, hyphens, underscores
1491
+ // version: optional, @ followed by any non-empty string
1492
+ const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
1493
+ if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
1494
+ const [, scope, name, version] = scopedMatch;
1290
1495
  return {
1496
+ scope,
1291
1497
  name,
1292
- description,
1293
- version: data.version ? String(data.version) : void 0,
1294
- license: data.license ? String(data.license) : void 0,
1295
- compatibility: data.compatibility ? String(data.compatibility) : void 0,
1296
- metadata: data.metadata,
1297
- allowedTools,
1298
- content: body,
1299
- rawContent: content
1498
+ version: version || void 0,
1499
+ fullName: `${scope}/${name}`
1300
1500
  };
1301
- } catch (error) {
1302
- if (error instanceof SkillValidationError) throw error;
1303
- if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
1304
- return null;
1305
1501
  }
1502
+ // Unscoped format: name[@version] (public registry)
1503
+ // name must not contain / (otherwise it might be a git shorthand)
1504
+ const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
1505
+ if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
1506
+ const [, name, version] = unscopedMatch;
1507
+ return {
1508
+ scope: null,
1509
+ name,
1510
+ version: version || void 0,
1511
+ fullName: name
1512
+ };
1306
1513
  }
1307
1514
  /**
1308
- * Parse SKILL.md from file path
1309
- */ function parseSkillMdFile(filePath, options = {}) {
1310
- if (!external_node_fs_.existsSync(filePath)) {
1311
- if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
1312
- return null;
1515
+ * HTTP utilities for downloading and extracting skill archives
1516
+ */ /**
1517
+ * Custom error class for HTTP download failures
1518
+ */ class HttpDownloadError extends Error {
1519
+ url;
1520
+ statusCode;
1521
+ originalError;
1522
+ constructor(url, message, statusCode, originalError){
1523
+ super(message);
1524
+ this.name = 'HttpDownloadError';
1525
+ this.url = url;
1526
+ this.statusCode = statusCode;
1527
+ this.originalError = originalError;
1313
1528
  }
1314
- const content = external_node_fs_.readFileSync(filePath, 'utf-8');
1315
- return parseSkillMd(content, options);
1316
1529
  }
1317
1530
  /**
1318
- * Parse SKILL.md from skill directory
1319
- */ function parseSkillFromDir(dirPath, options = {}) {
1320
- const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
1321
- return parseSkillMdFile(skillMdPath, options);
1531
+ * Download a file from HTTP/HTTPS URL
1532
+ *
1533
+ * @param url - URL to download from
1534
+ * @param destPath - Destination file path
1535
+ * @param options - Download options
1536
+ */ async function downloadFile(url, destPath, options = {}) {
1537
+ const { timeout = 60000, headers = {} } = options;
1538
+ // Ensure destination directory exists
1539
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
1540
+ try {
1541
+ // Use native fetch for HTTP/HTTPS
1542
+ const controller = new AbortController();
1543
+ const timeoutId = setTimeout(()=>controller.abort(), timeout);
1544
+ const response = await fetch(url, {
1545
+ signal: controller.signal,
1546
+ headers: {
1547
+ 'User-Agent': 'reskill/1.0',
1548
+ ...headers
1549
+ }
1550
+ });
1551
+ clearTimeout(timeoutId);
1552
+ if (!response.ok) throw new HttpDownloadError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
1553
+ // Stream response to file
1554
+ const fileStream = external_node_fs_.createWriteStream(destPath);
1555
+ const body = response.body;
1556
+ if (!body) throw new HttpDownloadError(url, 'Response body is empty');
1557
+ // Convert Web ReadableStream to Node.js Readable
1558
+ const { Readable } = await import("node:stream");
1559
+ const nodeStream = Readable.fromWeb(body);
1560
+ await (0, __WEBPACK_EXTERNAL_MODULE_node_stream_promises__.pipeline)(nodeStream, fileStream);
1561
+ } catch (error) {
1562
+ // Clean up partial download
1563
+ if (external_node_fs_.existsSync(destPath)) external_node_fs_.unlinkSync(destPath);
1564
+ if (error instanceof HttpDownloadError) throw error;
1565
+ const err = error;
1566
+ if ('AbortError' === err.name) throw new HttpDownloadError(url, `Download timeout after ${timeout}ms`);
1567
+ throw new HttpDownloadError(url, `Download failed: ${err.message}`, void 0, err);
1568
+ }
1322
1569
  }
1323
1570
  /**
1324
- * Check if directory contains valid SKILL.md
1325
- */ function hasValidSkillMd(dirPath) {
1326
- const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
1327
- if (!external_node_fs_.existsSync(skillMdPath)) return false;
1328
- try {
1329
- const skill = parseSkillMdFile(skillMdPath);
1330
- return null !== skill;
1331
- } catch {
1332
- return false;
1571
+ * Extract an archive to a directory
1572
+ *
1573
+ * @param archivePath - Path to the archive file
1574
+ * @param destDir - Destination directory
1575
+ * @param format - Archive format (auto-detected from extension if not provided)
1576
+ */ async function extractArchive(archivePath, destDir, format) {
1577
+ // Auto-detect format from extension
1578
+ const detectedFormat = format || detectArchiveFormat(archivePath);
1579
+ if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
1580
+ // Ensure destination directory exists
1581
+ fs_ensureDir(destDir);
1582
+ switch(detectedFormat){
1583
+ case 'tar.gz':
1584
+ case 'tgz':
1585
+ case 'tar':
1586
+ await extractTar(archivePath, destDir, 'tar.gz' === detectedFormat || 'tgz' === detectedFormat);
1587
+ break;
1588
+ case 'zip':
1589
+ await extractZip(archivePath, destDir);
1590
+ break;
1591
+ default:
1592
+ throw new Error(`Unsupported archive format: ${detectedFormat}`);
1333
1593
  }
1334
1594
  }
1335
- const SKIP_DIRS = [
1336
- 'node_modules',
1337
- '.git',
1338
- 'dist',
1339
- 'build',
1340
- '__pycache__'
1341
- ];
1342
- const MAX_DISCOVER_DEPTH = 5;
1343
- const PRIORITY_SKILL_DIRS = [
1344
- 'skills',
1345
- '.agents/skills',
1346
- '.cursor/skills',
1347
- '.claude/skills',
1348
- '.windsurf/skills',
1349
- '.github/skills'
1350
- ];
1351
- function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
1352
- if (depth > maxDepth) return [];
1353
- const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
1354
- if (visitedDirs.has(resolvedDir)) return [];
1355
- if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
1356
- visitedDirs.add(resolvedDir);
1357
- const results = [];
1358
- let entries;
1595
+ /**
1596
+ * Extract tar archive using native tar command
1597
+ */ async function extractTar(archivePath, destDir, gzipped) {
1598
+ const { exec } = await import("node:child_process");
1599
+ const { promisify } = await import("node:util");
1600
+ const execAsync = promisify(exec);
1601
+ const flags = gzipped ? '-xzf' : '-xf';
1359
1602
  try {
1360
- entries = external_node_fs_.readdirSync(dir);
1361
- } catch {
1362
- return [];
1363
- }
1364
- for (const entry of entries){
1365
- if (SKIP_DIRS.includes(entry)) continue;
1366
- const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
1367
- const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
1368
- if (visitedDirs.has(resolvedFull)) continue;
1369
- let stat;
1370
- try {
1371
- stat = external_node_fs_.statSync(fullPath);
1372
- } catch {
1373
- continue;
1603
+ // Extract to a temp directory first to handle single-folder archives
1604
+ const tempExtractDir = `${destDir}.extract-temp`;
1605
+ fs_ensureDir(tempExtractDir);
1606
+ await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
1607
+ encoding: 'utf-8'
1608
+ });
1609
+ // Check if archive contains a single root directory
1610
+ const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
1611
+ if (1 === extractedItems.length) {
1612
+ const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
1613
+ if (external_node_fs_.statSync(singleItem).isDirectory()) {
1614
+ // Move contents of single directory to destination
1615
+ const contents = external_node_fs_.readdirSync(singleItem);
1616
+ for (const item of contents){
1617
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
1618
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1619
+ external_node_fs_.renameSync(src, dest);
1620
+ }
1621
+ fs_remove(tempExtractDir);
1622
+ return;
1623
+ }
1374
1624
  }
1375
- if (!!stat.isDirectory()) {
1376
- if (hasValidSkillMd(fullPath)) results.push(fullPath);
1377
- results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
1625
+ // Move all items to destination
1626
+ for (const item of extractedItems){
1627
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1628
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1629
+ external_node_fs_.renameSync(src, dest);
1378
1630
  }
1631
+ fs_remove(tempExtractDir);
1632
+ } catch (error) {
1633
+ throw new Error(`Failed to extract tar archive: ${error.message}`);
1379
1634
  }
1380
- return results;
1381
1635
  }
1382
1636
  /**
1383
- * Discover all skills in a directory by scanning for SKILL.md files.
1384
- *
1385
- * Strategy:
1386
- * 1. Check root for SKILL.md
1387
- * 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
1388
- * 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
1389
- *
1390
- * @param basePath - Root directory to search
1391
- * @returns List of parsed skills with their directory paths (absolute)
1392
- */ function discoverSkillsInDir(basePath) {
1393
- const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
1394
- const results = [];
1395
- const seenNames = new Set();
1396
- function addSkill(dirPath) {
1397
- const skill = parseSkillFromDir(dirPath);
1398
- if (skill && !seenNames.has(skill.name)) {
1399
- seenNames.add(skill.name);
1400
- results.push({
1401
- ...skill,
1402
- dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
1403
- });
1404
- }
1405
- }
1406
- if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
1407
- // Track visited directories to avoid redundant I/O during recursive scan
1408
- const visitedDirs = new Set();
1409
- for (const sub of PRIORITY_SKILL_DIRS){
1410
- const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
1411
- if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
1412
- visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
1413
- try {
1414
- const entries = external_node_fs_.readdirSync(dir);
1415
- for (const entry of entries){
1416
- const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
1417
- try {
1418
- if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
1419
- addSkill(skillDir);
1420
- visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
1421
- }
1422
- } catch {
1423
- // Skip entries that can't be stat'd (race condition, permission, etc.)
1424
- }
1637
+ * Extract zip archive using native unzip command or Node.js
1638
+ */ async function extractZip(archivePath, destDir) {
1639
+ const { exec } = await import("node:child_process");
1640
+ const { promisify } = await import("node:util");
1641
+ const execAsync = promisify(exec);
1642
+ try {
1643
+ // Extract to a temp directory first
1644
+ const tempExtractDir = `${destDir}.extract-temp`;
1645
+ fs_ensureDir(tempExtractDir);
1646
+ // Try using unzip command (available on most systems)
1647
+ await execAsync(`unzip -q "${archivePath}" -d "${tempExtractDir}"`, {
1648
+ encoding: 'utf-8'
1649
+ });
1650
+ // Check if archive contains a single root directory
1651
+ const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
1652
+ if (1 === extractedItems.length) {
1653
+ const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
1654
+ if (external_node_fs_.statSync(singleItem).isDirectory()) {
1655
+ // Move contents of single directory to destination
1656
+ const contents = external_node_fs_.readdirSync(singleItem);
1657
+ for (const item of contents){
1658
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
1659
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1660
+ external_node_fs_.renameSync(src, dest);
1425
1661
  }
1426
- } catch {
1427
- // Skip if unreadable
1662
+ fs_remove(tempExtractDir);
1663
+ return;
1428
1664
  }
1429
1665
  }
1666
+ // Move all items to destination
1667
+ for (const item of extractedItems){
1668
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1669
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1670
+ external_node_fs_.renameSync(src, dest);
1671
+ }
1672
+ fs_remove(tempExtractDir);
1673
+ } catch (error) {
1674
+ throw new Error(`Failed to extract zip archive: ${error.message}`);
1430
1675
  }
1431
- const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
1432
- for (const skillDir of recursiveDirs)addSkill(skillDir);
1433
- return results;
1434
1676
  }
1435
1677
  /**
1436
- * Filter skills by name (case-insensitive exact match).
1437
- *
1438
- * Note: an empty `names` array returns an empty result (not all skills).
1439
- * Callers should check `names.length` before calling if "no filter = all" is desired.
1678
+ * Detect archive format from file path
1679
+ */ function detectArchiveFormat(filePath) {
1680
+ const lower = filePath.toLowerCase();
1681
+ if (lower.endsWith('.tar.gz')) return 'tar.gz';
1682
+ if (lower.endsWith('.tgz')) return 'tgz';
1683
+ if (lower.endsWith('.zip')) return 'zip';
1684
+ if (lower.endsWith('.tar')) return 'tar';
1685
+ }
1686
+ /**
1687
+ * Download and extract an archive in one operation
1440
1688
  *
1441
- * @param skills - List of discovered skills
1442
- * @param names - Skill names to match (e.g. from --skill pdf commit)
1443
- * @returns Skills whose name matches any of the given names
1444
- */ function filterSkillsByName(skills, names) {
1445
- const normalized = names.map((n)=>n.toLowerCase());
1446
- return skills.filter((skill)=>{
1447
- // Match against SKILL.md name field
1448
- if (normalized.includes(skill.name.toLowerCase())) return true;
1449
- // Also match against the directory name (basename of dirPath)
1450
- // Users naturally refer to skills by their directory name
1451
- const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
1452
- return normalized.includes(dirName);
1453
- });
1689
+ * @param url - URL to download from
1690
+ * @param destDir - Destination directory for extracted contents
1691
+ * @param options - Download options
1692
+ * @returns Path to extracted contents
1693
+ */ async function downloadAndExtract(url, destDir, options = {}) {
1694
+ // Determine archive filename from URL
1695
+ const urlObj = new URL(url);
1696
+ const filename = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(urlObj.pathname);
1697
+ // Detect format from original filename before adding .download suffix
1698
+ const format = detectArchiveFormat(filename);
1699
+ if (!format) throw new Error(`Unable to detect archive format from URL: ${url}`);
1700
+ const tempArchive = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, `../${filename}.download`);
1701
+ try {
1702
+ // Download archive
1703
+ await downloadFile(url, tempArchive, options);
1704
+ // Extract archive with explicit format
1705
+ await extractArchive(tempArchive, destDir, format);
1706
+ return destDir;
1707
+ } finally{
1708
+ // Clean up temp archive
1709
+ if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
1710
+ }
1454
1711
  }
1455
1712
  /**
1456
1713
  * Installer - Multi-Agent installer
@@ -1469,14 +1726,14 @@ const installer_SKILLS_SUBDIR = 'skills';
1469
1726
  /**
1470
1727
  * Default files to exclude when copying skills
1471
1728
  * These files are typically used for repository metadata and should not be copied to agent directories
1472
- */ const DEFAULT_EXCLUDE_FILES = [
1729
+ */ const installer_DEFAULT_EXCLUDE_FILES = [
1473
1730
  'README.md',
1474
1731
  'metadata.json',
1475
1732
  '.reskill-commit'
1476
1733
  ];
1477
1734
  /**
1478
1735
  * Prefix for files that should be excluded (internal/private files)
1479
- */ const EXCLUDE_PREFIX = '_';
1736
+ */ const installer_EXCLUDE_PREFIX = '_';
1480
1737
  /**
1481
1738
  * Sanitize filename to prevent path traversal attacks
1482
1739
  */ function installer_sanitizeName(name) {
@@ -1530,18 +1787,18 @@ const installer_SKILLS_SUBDIR = 'skills';
1530
1787
  * By default excludes:
1531
1788
  * - Files in DEFAULT_EXCLUDE_FILES (README.md, metadata.json, .reskill-commit)
1532
1789
  * - Files starting with EXCLUDE_PREFIX ('_')
1533
- */ function copyDirectory(src, dest, options) {
1534
- const exclude = new Set(options?.exclude || DEFAULT_EXCLUDE_FILES);
1790
+ */ function installer_copyDirectory(src, dest, options) {
1791
+ const exclude = new Set(options?.exclude || installer_DEFAULT_EXCLUDE_FILES);
1535
1792
  installer_ensureDir(dest);
1536
1793
  const entries = external_node_fs_.readdirSync(src, {
1537
1794
  withFileTypes: true
1538
1795
  });
1539
1796
  for (const entry of entries){
1540
1797
  // Skip files starting with EXCLUDE_PREFIX and files in exclude list
1541
- if (exclude.has(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
1798
+ if (exclude.has(entry.name) || entry.name.startsWith(installer_EXCLUDE_PREFIX)) continue;
1542
1799
  const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
1543
1800
  const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
1544
- if (entry.isDirectory()) copyDirectory(srcPath, destPath, options);
1801
+ if (entry.isDirectory()) installer_copyDirectory(srcPath, destPath, options);
1545
1802
  else external_node_fs_.copyFileSync(srcPath, destPath);
1546
1803
  }
1547
1804
  }
@@ -1608,6 +1865,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1608
1865
  /**
1609
1866
  * Get agent's skill installation path
1610
1867
  */ getAgentSkillPath(skillName, agentType) {
1868
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) return getClaude3pSkillPath(skillName);
1611
1869
  const agent = getAgentConfig(agentType);
1612
1870
  const sanitized = installer_sanitizeName(skillName);
1613
1871
  const agentBase = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
@@ -1621,6 +1879,9 @@ const installer_SKILLS_SUBDIR = 'skills';
1621
1879
  * @param agentType - Target agent type
1622
1880
  * @param options - Installation options
1623
1881
  */ async installForAgent(sourcePath, skillName, agentType, options = {}) {
1882
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) return installClaude3pSkill(sourcePath, skillName, {
1883
+ mode: options.mode
1884
+ });
1624
1885
  const agent = getAgentConfig(agentType);
1625
1886
  const installMode = options.mode || 'symlink';
1626
1887
  const sanitized = installer_sanitizeName(skillName);
@@ -1649,7 +1910,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1649
1910
  if ('copy' === installMode) {
1650
1911
  installer_ensureDir(agentDir);
1651
1912
  installer_remove(agentDir);
1652
- copyDirectory(sourcePath, agentDir);
1913
+ installer_copyDirectory(sourcePath, agentDir);
1653
1914
  result = {
1654
1915
  success: true,
1655
1916
  path: agentDir,
@@ -1659,7 +1920,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1659
1920
  // Symlink mode: copy to canonical location, then create symlink
1660
1921
  installer_ensureDir(canonicalDir);
1661
1922
  installer_remove(canonicalDir);
1662
- copyDirectory(sourcePath, canonicalDir);
1923
+ installer_copyDirectory(sourcePath, canonicalDir);
1663
1924
  const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
1664
1925
  if (symlinkCreated) result = {
1665
1926
  success: true,
@@ -1675,7 +1936,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1675
1936
  // Ignore cleanup errors
1676
1937
  }
1677
1938
  installer_ensureDir(agentDir);
1678
- copyDirectory(sourcePath, agentDir);
1939
+ installer_copyDirectory(sourcePath, agentDir);
1679
1940
  result = {
1680
1941
  success: true,
1681
1942
  path: agentDir,
@@ -1710,8 +1971,12 @@ const installer_SKILLS_SUBDIR = 'skills';
1710
1971
  /**
1711
1972
  * Check if skill is installed to specified agent
1712
1973
  */ isInstalled(skillName, agentType) {
1713
- const skillPath = this.getAgentSkillPath(skillName, agentType);
1714
- return external_node_fs_.existsSync(skillPath);
1974
+ try {
1975
+ const skillPath = this.getAgentSkillPath(skillName, agentType);
1976
+ return external_node_fs_.existsSync(skillPath);
1977
+ } catch {
1978
+ return false;
1979
+ }
1715
1980
  }
1716
1981
  /**
1717
1982
  * Check if skill is installed in canonical location
@@ -1722,6 +1987,11 @@ const installer_SKILLS_SUBDIR = 'skills';
1722
1987
  /**
1723
1988
  * Uninstall skill from specified agent
1724
1989
  */ uninstallFromAgent(skillName, agentType) {
1990
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) try {
1991
+ return uninstallClaude3pSkill(skillName);
1992
+ } catch {
1993
+ return false;
1994
+ }
1725
1995
  const skillPath = this.getAgentSkillPath(skillName, agentType);
1726
1996
  if (!external_node_fs_.existsSync(skillPath)) return false;
1727
1997
  installer_remove(skillPath);
@@ -1742,6 +2012,11 @@ const installer_SKILLS_SUBDIR = 'skills';
1742
2012
  /**
1743
2013
  * Get all skills installed to specified agent
1744
2014
  */ listInstalledSkills(agentType) {
2015
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) try {
2016
+ return listClaude3pSkills();
2017
+ } catch {
2018
+ return [];
2019
+ }
1745
2020
  const agent = getAgentConfig(agentType);
1746
2021
  const skillsDir = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
1747
2022
  if (!external_node_fs_.existsSync(skillsDir)) return [];
@@ -1869,7 +2144,7 @@ ${CURSOR_BRIDGE_MARKER}
1869
2144
  * Check if skill is cached
1870
2145
  */ isCached(parsed, version) {
1871
2146
  const cachePath = this.getSkillCachePath(parsed, version);
1872
- return exists(cachePath) && isDirectory(cachePath);
2147
+ return fs_exists(cachePath) && isDirectory(cachePath);
1873
2148
  }
1874
2149
  /**
1875
2150
  * Get cached skill
@@ -1881,7 +2156,7 @@ ${CURSOR_BRIDGE_MARKER}
1881
2156
  let commit = '';
1882
2157
  try {
1883
2158
  const fs = await import("node:fs");
1884
- if (exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
2159
+ if (fs_exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
1885
2160
  } catch {
1886
2161
  // Ignore read errors
1887
2162
  }
@@ -1895,11 +2170,11 @@ ${CURSOR_BRIDGE_MARKER}
1895
2170
  */ async cache(repoUrl, parsed, ref, version) {
1896
2171
  const cachePath = this.getSkillCachePath(parsed, version);
1897
2172
  // If exists, delete first
1898
- if (exists(cachePath)) remove(cachePath);
1899
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
2173
+ if (fs_exists(cachePath)) fs_remove(cachePath);
2174
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
1900
2175
  // Clone repository
1901
2176
  const tempPath = `${cachePath}.tmp`;
1902
- remove(tempPath);
2177
+ fs_remove(tempPath);
1903
2178
  await clone(repoUrl, tempPath, {
1904
2179
  depth: 1,
1905
2180
  branch: ref
@@ -1909,8 +2184,8 @@ ${CURSOR_BRIDGE_MARKER}
1909
2184
  // If has subPath, only keep subdirectory
1910
2185
  if (parsed.subPath) {
1911
2186
  const subDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempPath, parsed.subPath);
1912
- if (!exists(subDir)) {
1913
- remove(tempPath);
2187
+ if (!fs_exists(subDir)) {
2188
+ fs_remove(tempPath);
1914
2189
  throw new Error(`Subpath ${parsed.subPath} not found in repository`);
1915
2190
  }
1916
2191
  copyDir(subDir, cachePath, {
@@ -1926,7 +2201,7 @@ ${CURSOR_BRIDGE_MARKER}
1926
2201
  // Save commit info
1927
2202
  external_node_fs_.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit'), commit);
1928
2203
  // Clean up temp directory
1929
- remove(tempPath);
2204
+ fs_remove(tempPath);
1930
2205
  return {
1931
2206
  path: cachePath,
1932
2207
  commit
@@ -1945,8 +2220,8 @@ ${CURSOR_BRIDGE_MARKER}
1945
2220
  */ async cacheFromHttp(url, parsed, version) {
1946
2221
  const cachePath = this.getSkillCachePath(parsed, version);
1947
2222
  // If exists, delete first
1948
- if (exists(cachePath)) remove(cachePath);
1949
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
2223
+ if (fs_exists(cachePath)) fs_remove(cachePath);
2224
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
1950
2225
  // Download and extract to cache path
1951
2226
  await downloadAndExtract(url, cachePath);
1952
2227
  // Generate a commit-like identifier from URL and version
@@ -1971,10 +2246,10 @@ ${CURSOR_BRIDGE_MARKER}
1971
2246
  const cached = await this.get(parsed, version);
1972
2247
  if (!cached) throw new Error(`Skill ${parsed.raw} version ${version} not found in cache`);
1973
2248
  // If target exists, delete first
1974
- if (exists(destPath)) remove(destPath);
2249
+ if (fs_exists(destPath)) fs_remove(destPath);
1975
2250
  // Use same exclude rules as Installer for consistency
1976
2251
  copyDir(cached.path, destPath, {
1977
- exclude: DEFAULT_EXCLUDE_FILES
2252
+ exclude: installer_DEFAULT_EXCLUDE_FILES
1978
2253
  });
1979
2254
  }
1980
2255
  /**
@@ -1982,22 +2257,22 @@ ${CURSOR_BRIDGE_MARKER}
1982
2257
  */ clearSkill(parsed, version) {
1983
2258
  if (version) {
1984
2259
  const cachePath = this.getSkillCachePath(parsed, version);
1985
- remove(cachePath);
2260
+ fs_remove(cachePath);
1986
2261
  } else {
1987
2262
  // Clear all versions
1988
2263
  const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cacheDir, parsed.registry, parsed.owner, parsed.repo);
1989
- remove(skillDir);
2264
+ fs_remove(skillDir);
1990
2265
  }
1991
2266
  }
1992
2267
  /**
1993
2268
  * Clear all cache
1994
2269
  */ clearAll() {
1995
- remove(this.cacheDir);
2270
+ fs_remove(this.cacheDir);
1996
2271
  }
1997
2272
  /**
1998
2273
  * Get cache statistics
1999
2274
  */ getStats() {
2000
- if (!exists(this.cacheDir)) return {
2275
+ if (!fs_exists(this.cacheDir)) return {
2001
2276
  totalSkills: 0,
2002
2277
  registries: []
2003
2278
  };
@@ -2151,7 +2426,7 @@ ${CURSOR_BRIDGE_MARKER}
2151
2426
  /**
2152
2427
  * Check if configuration file exists
2153
2428
  */ exists() {
2154
- return exists(this.configPath);
2429
+ return fs_exists(this.configPath);
2155
2430
  }
2156
2431
  /**
2157
2432
  * Load configuration from file
@@ -3073,7 +3348,7 @@ ${CURSOR_BRIDGE_MARKER}
3073
3348
  /**
3074
3349
  * Check if lock file exists
3075
3350
  */ exists() {
3076
- return exists(this.lockPath);
3351
+ return fs_exists(this.lockPath);
3077
3352
  }
3078
3353
  /**
3079
3354
  * Load lock file
@@ -4033,6 +4308,16 @@ class RegistryResolver {
4033
4308
  return this.isGlobal;
4034
4309
  }
4035
4310
  /**
4311
+ * Determine if the installation is effectively global.
4312
+ *
4313
+ * Claude Cowork 3P always installs to a global app-managed directory regardless
4314
+ * of the isGlobal flag. When all target agents are claude-cowork-3p, the
4315
+ * installation should be treated as global (skip skills.json/skills.lock writes).
4316
+ */ isEffectivelyGlobal(targetAgents) {
4317
+ if (this.isGlobal) return true;
4318
+ return targetAgents.length > 0 && targetAgents.every((a)=>a === claude_3p_installer_CLAUDE_COWORK_3P_AGENT);
4319
+ }
4320
+ /**
4036
4321
  * Get project root directory
4037
4322
  */ getProjectRoot() {
4038
4323
  return this.projectRoot;
@@ -4064,11 +4349,11 @@ class RegistryResolver {
4064
4349
  */ getSkillPath(name) {
4065
4350
  // Check canonical location first (.agents/skills/)
4066
4351
  const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
4067
- if (exists(canonicalPath)) return canonicalPath;
4352
+ if (fs_exists(canonicalPath)) return canonicalPath;
4068
4353
  // Check configured installation directory (.skills/ or custom)
4069
4354
  const installDir = this.getInstallDir();
4070
4355
  const installPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(installDir, name);
4071
- if (exists(installPath)) return installPath;
4356
+ if (fs_exists(installPath)) return installPath;
4072
4357
  // Default to configured installation directory for new installations
4073
4358
  // if it's not the default .skills, otherwise use canonical location.
4074
4359
  // This respects "installDir" in skills.json.
@@ -4148,7 +4433,7 @@ class RegistryResolver {
4148
4433
  const semanticVersion = metadata?.version ?? gitRef;
4149
4434
  const skillPath = this.getSkillPath(skillName);
4150
4435
  // Check if already installed (using the real name from SKILL.md)
4151
- if (exists(skillPath) && !force) {
4436
+ if (fs_exists(skillPath) && !force) {
4152
4437
  const locked = this.lockManager.get(skillName);
4153
4438
  // Compare ref if available, fallback to version for backward compatibility
4154
4439
  const lockedRef = locked?.ref || locked?.version;
@@ -4165,10 +4450,10 @@ class RegistryResolver {
4165
4450
  }
4166
4451
  logger_logger["package"](`Installing ${skillName}@${gitRef}...`);
4167
4452
  // Copy to installation directory
4168
- ensureDir(this.getInstallDir());
4169
- if (exists(skillPath)) remove(skillPath);
4453
+ fs_ensureDir(this.getInstallDir());
4454
+ if (fs_exists(skillPath)) fs_remove(skillPath);
4170
4455
  copyDir(sourcePath, skillPath, {
4171
- exclude: DEFAULT_EXCLUDE_FILES
4456
+ exclude: installer_DEFAULT_EXCLUDE_FILES
4172
4457
  });
4173
4458
  // Update lock file (project mode only)
4174
4459
  if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
@@ -4215,7 +4500,7 @@ class RegistryResolver {
4215
4500
  const semanticVersion = metadata?.version ?? version;
4216
4501
  const skillPath = this.getSkillPath(skillName);
4217
4502
  // Check if already installed (using the real name from SKILL.md)
4218
- if (exists(skillPath) && !force) {
4503
+ if (fs_exists(skillPath) && !force) {
4219
4504
  const locked = this.lockManager.get(skillName);
4220
4505
  const lockedRef = locked?.ref || locked?.version;
4221
4506
  if (locked && lockedRef === version) {
@@ -4231,8 +4516,8 @@ class RegistryResolver {
4231
4516
  }
4232
4517
  logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host}...`);
4233
4518
  // Copy to installation directory
4234
- ensureDir(this.getInstallDir());
4235
- if (exists(skillPath)) remove(skillPath);
4519
+ fs_ensureDir(this.getInstallDir());
4520
+ if (fs_exists(skillPath)) fs_remove(skillPath);
4236
4521
  await this.cache.copyTo(parsed, version, skillPath);
4237
4522
  // Update lock file (project mode only)
4238
4523
  if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
@@ -4277,13 +4562,13 @@ class RegistryResolver {
4277
4562
  * Uninstall skill
4278
4563
  */ uninstall(name) {
4279
4564
  const skillPath = this.getSkillPath(name);
4280
- if (!exists(skillPath)) {
4565
+ if (!fs_exists(skillPath)) {
4281
4566
  const location = this.isGlobal ? '(global)' : '';
4282
4567
  logger_logger.warn(`Skill ${name} is not installed ${location}`.trim());
4283
4568
  return false;
4284
4569
  }
4285
4570
  // Remove installation directory
4286
- remove(skillPath);
4571
+ fs_remove(skillPath);
4287
4572
  // Remove from lock file (project mode only)
4288
4573
  if (!this.isGlobal) this.lockManager.remove(name);
4289
4574
  // Remove from skills.json (project mode only)
@@ -4442,13 +4727,15 @@ class RegistryResolver {
4442
4727
  /**
4443
4728
  * List installed skills
4444
4729
  *
4445
- * Checks both canonical (.agents/skills/) and legacy (.skills/) locations.
4446
- */ list() {
4730
+ * When `agent` is specified, lists skills installed to that specific agent.
4731
+ * Otherwise checks both canonical (.agents/skills/) and legacy (.skills/) locations.
4732
+ */ list(options) {
4733
+ if (options?.agent) return this.listByAgent(options.agent);
4447
4734
  const skills = [];
4448
4735
  const seenNames = new Set();
4449
4736
  // Check canonical location first (.agents/skills/)
4450
4737
  const canonicalDir = this.getCanonicalSkillsDir();
4451
- if (exists(canonicalDir)) for (const name of listDir(canonicalDir)){
4738
+ if (fs_exists(canonicalDir)) for (const name of listDir(canonicalDir)){
4452
4739
  const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalDir, name);
4453
4740
  if (!isDirectory(skillPath)) continue;
4454
4741
  const skill = this.getInstalledSkillFromPath(name, skillPath);
@@ -4459,7 +4746,7 @@ class RegistryResolver {
4459
4746
  }
4460
4747
  // Check legacy location (.skills/)
4461
4748
  const legacyDir = this.getInstallDir();
4462
- if (exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
4749
+ if (fs_exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
4463
4750
  // Skip if already found in canonical location
4464
4751
  if (seenNames.has(name)) continue;
4465
4752
  const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(legacyDir, name);
@@ -4477,6 +4764,38 @@ class RegistryResolver {
4477
4764
  seenNames.add(name);
4478
4765
  }
4479
4766
  }
4767
+ // In global mode, also include claude-cowork-3p skills (always global)
4768
+ if (this.isGlobal) try {
4769
+ for (const name of listClaude3pSkills()){
4770
+ if (seenNames.has(name)) continue;
4771
+ const skillPath = getClaude3pSkillPath(name);
4772
+ const skill = this.getInstalledSkillFromPath(name, skillPath);
4773
+ if (skill) {
4774
+ skills.push(skill);
4775
+ seenNames.add(name);
4776
+ }
4777
+ }
4778
+ } catch {
4779
+ // Claude Cowork 3P not configured or accessible — skip silently
4780
+ }
4781
+ return skills;
4782
+ }
4783
+ /**
4784
+ * List skills installed to a specific agent
4785
+ */ listByAgent(agent) {
4786
+ const installer = new Installer({
4787
+ cwd: this.projectRoot,
4788
+ global: this.isGlobal
4789
+ });
4790
+ const skillNames = installer.listInstalledSkills(agent);
4791
+ const skills = [];
4792
+ for (const name of skillNames)try {
4793
+ const skillPath = installer.getAgentSkillPath(name, agent);
4794
+ const skill = this.getInstalledSkillFromPath(name, skillPath);
4795
+ if (skill) skills.push(skill);
4796
+ } catch {
4797
+ // Skip skills whose paths cannot be resolved (e.g. claude-3p not configured)
4798
+ }
4480
4799
  return skills;
4481
4800
  }
4482
4801
  /**
@@ -4489,18 +4808,26 @@ class RegistryResolver {
4489
4808
  const canonicalDir = this.getCanonicalSkillsDir();
4490
4809
  const installed = [];
4491
4810
  for (const [type, config] of Object.entries(agents)){
4811
+ if (type === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) {
4812
+ try {
4813
+ if (fs_exists(getClaude3pSkillPath(skillName))) installed.push(type);
4814
+ } catch {
4815
+ // Claude Cowork 3P keeps skills under an app-managed account directory.
4816
+ }
4817
+ continue;
4818
+ }
4492
4819
  const agentBase = this.isGlobal ? config.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.projectRoot, config.skillsDir);
4493
4820
  // Skip agents whose skillsDir is the canonical directory itself
4494
4821
  if (__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(agentBase) === __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(canonicalDir)) continue;
4495
4822
  const agentSkillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(agentBase, skillName);
4496
- if (exists(agentSkillDir)) installed.push(type);
4823
+ if (fs_exists(agentSkillDir)) installed.push(type);
4497
4824
  }
4498
4825
  return installed;
4499
4826
  }
4500
4827
  /**
4501
4828
  * Get installed skill information from a specific path
4502
4829
  */ getInstalledSkillFromPath(name, skillPath) {
4503
- if (!exists(skillPath)) return null;
4830
+ if (!fs_exists(skillPath)) return null;
4504
4831
  const isLinked = isSymlink(skillPath);
4505
4832
  const locked = this.lockManager.get(name);
4506
4833
  // Read metadata from SKILL.md (sole source per agentskills.io spec)
@@ -4523,10 +4850,10 @@ class RegistryResolver {
4523
4850
  */ getInstalledSkill(name) {
4524
4851
  // Check canonical location first (.agents/skills/)
4525
4852
  const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
4526
- if (exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
4853
+ if (fs_exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
4527
4854
  // Check legacy location (.skills/)
4528
4855
  const legacyPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getInstallDir(), name);
4529
- if (exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
4856
+ if (fs_exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
4530
4857
  return null;
4531
4858
  }
4532
4859
  /**
@@ -4722,14 +5049,15 @@ class RegistryResolver {
4722
5049
  const results = await installer.installToAgents(skillInfo.dirPath, skillInfo.name, targetAgents, {
4723
5050
  mode: mode
4724
5051
  });
4725
- if (!this.isGlobal) this.lockManager.lockSkill(skillInfo.name, {
5052
+ const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
5053
+ if (!effectivelyGlobal) this.lockManager.lockSkill(skillInfo.name, {
4726
5054
  source: skillSource,
4727
5055
  version: semanticVersion,
4728
5056
  ref: gitRef,
4729
5057
  resolved: repoUrl,
4730
5058
  commit: cacheResult.commit
4731
5059
  });
4732
- if (!this.isGlobal && save) {
5060
+ if (!effectivelyGlobal && save) {
4733
5061
  this.config.ensureExists();
4734
5062
  this.config.addSkill(skillInfo.name, `${baseRefForSave}#${skillInfo.name}`);
4735
5063
  }
@@ -4787,8 +5115,9 @@ class RegistryResolver {
4787
5115
  const results = await installer.installToAgents(sourcePath, skillName, targetAgents, {
4788
5116
  mode: mode
4789
5117
  });
4790
- // Update lock file (project mode only)
4791
- if (!this.isGlobal) {
5118
+ // Update lock file (project mode only, skip for effectively-global installs)
5119
+ const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
5120
+ if (!effectivelyGlobal) {
4792
5121
  const lockSource = registryContext?.lockSource ?? `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`;
4793
5122
  this.lockManager.lockSkill(skillName, {
4794
5123
  source: lockSource,
@@ -4799,8 +5128,8 @@ class RegistryResolver {
4799
5128
  registry: registryContext?.registryUrl
4800
5129
  });
4801
5130
  }
4802
- // Update skills.json (project mode only)
4803
- if (!this.isGlobal && save) {
5131
+ // Update skills.json (project mode only, skip for effectively-global installs)
5132
+ if (!effectivelyGlobal && save) {
4804
5133
  this.config.ensureExists();
4805
5134
  const configRef = registryContext?.configRef ?? this.config.normalizeSkillRef(ref);
4806
5135
  this.config.addSkill(skillName, configRef);
@@ -4858,8 +5187,9 @@ class RegistryResolver {
4858
5187
  const results = await installer.installToAgents(sourcePath, skillName, targetAgents, {
4859
5188
  mode: mode
4860
5189
  });
4861
- // Update lock file (project mode only)
4862
- if (!this.isGlobal) {
5190
+ // Update lock file (project mode only, skip for effectively-global installs)
5191
+ const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
5192
+ if (!effectivelyGlobal) {
4863
5193
  const lockSource = registryContext?.lockSource ?? `http:${httpInfo.host}/${skillName}`;
4864
5194
  this.lockManager.lockSkill(skillName, {
4865
5195
  source: lockSource,
@@ -4870,8 +5200,8 @@ class RegistryResolver {
4870
5200
  registry: registryContext?.registryUrl
4871
5201
  });
4872
5202
  }
4873
- // Update skills.json (project mode only)
4874
- if (!this.isGlobal && save) {
5203
+ // Update skills.json (project mode only, skip for effectively-global installs)
5204
+ if (!effectivelyGlobal && save) {
4875
5205
  this.config.ensureExists();
4876
5206
  const configRef = registryContext?.configRef ?? ref;
4877
5207
  this.config.addSkill(skillName, configRef);
@@ -4937,7 +5267,7 @@ class RegistryResolver {
4937
5267
  const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
4938
5268
  // 2. Check if already installed (skip if --force)
4939
5269
  const skillPath = this.getSkillPath(shortName);
4940
- if (exists(skillPath) && !force) {
5270
+ if (fs_exists(skillPath) && !force) {
4941
5271
  const locked = this.lockManager.get(shortName);
4942
5272
  const lockedVersion = locked?.version;
4943
5273
  // Same version already installed
@@ -4974,8 +5304,8 @@ class RegistryResolver {
4974
5304
  logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
4975
5305
  // 3. Create temp directory for extraction (clean stale files first)
4976
5306
  const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
4977
- await remove(tempDir);
4978
- await ensureDir(tempDir);
5307
+ await fs_remove(tempDir);
5308
+ await fs_ensureDir(tempDir);
4979
5309
  try {
4980
5310
  // 4. Extract tarball
4981
5311
  const extractedPath = await this.registryResolver.extract(tarball, tempDir);
@@ -4991,8 +5321,9 @@ class RegistryResolver {
4991
5321
  const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
4992
5322
  mode: mode
4993
5323
  });
4994
- // 7. Update lock file (project mode only)
4995
- if (!this.isGlobal) this.lockManager.lockSkill(shortName, {
5324
+ // 7. Update lock file (project mode only, skip for effectively-global installs)
5325
+ const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
5326
+ if (!effectivelyGlobal) this.lockManager.lockSkill(shortName, {
4996
5327
  source: `registry:${resolvedParsed.fullName}`,
4997
5328
  version,
4998
5329
  ref: version,
@@ -5000,8 +5331,8 @@ class RegistryResolver {
5000
5331
  commit: resolved.integrity,
5001
5332
  registry: resolvedRegistryUrl
5002
5333
  });
5003
- // 8. Update skills.json (project mode only)
5004
- if (!this.isGlobal && save) {
5334
+ // 8. Update skills.json (project mode only, skip for effectively-global installs)
5335
+ if (!effectivelyGlobal && save) {
5005
5336
  this.config.ensureExists();
5006
5337
  // Save with full name for registry skills
5007
5338
  this.config.addSkill(shortName, ref);
@@ -5032,7 +5363,7 @@ class RegistryResolver {
5032
5363
  };
5033
5364
  } finally{
5034
5365
  // Clean up temp directory after installation
5035
- await remove(tempDir);
5366
+ await fs_remove(tempDir);
5036
5367
  }
5037
5368
  }
5038
5369
  // ============================================================================
@@ -5067,7 +5398,8 @@ class RegistryResolver {
5067
5398
  registryContext
5068
5399
  };
5069
5400
  // Save custom registry to skills.json.registries (for reinstall without lock file)
5070
- if (!this.isGlobal && options.registry) {
5401
+ const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
5402
+ if (!effectivelyGlobal && options.registry) {
5071
5403
  const registryName = this.deriveRegistryName(options.registry);
5072
5404
  if (registryName) {
5073
5405
  this.config.ensureExists();
@@ -5151,8 +5483,8 @@ class RegistryResolver {
5151
5483
  logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
5152
5484
  // Extract tarball to temp directory (clean stale files first)
5153
5485
  const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
5154
- await remove(tempDir);
5155
- await ensureDir(tempDir);
5486
+ await fs_remove(tempDir);
5487
+ await fs_ensureDir(tempDir);
5156
5488
  try {
5157
5489
  const extractedPath = await this.registryResolver.extract(tarball, tempDir);
5158
5490
  logger_logger.debug(`Extracted to ${extractedPath}`);
@@ -5170,8 +5502,9 @@ class RegistryResolver {
5170
5502
  const metadata = this.getSkillMetadataFromDir(extractedPath);
5171
5503
  const skillName = metadata?.name ?? shortName;
5172
5504
  const semanticVersion = metadata?.version ?? version;
5173
- // Update lock file (project mode only)
5174
- if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
5505
+ // Update lock file (project mode only, skip for effectively-global installs)
5506
+ const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
5507
+ if (!effectivelyGlobal) this.lockManager.lockSkill(skillName, {
5175
5508
  source: `registry:${parsed.fullName}`,
5176
5509
  version: semanticVersion,
5177
5510
  ref: version,
@@ -5179,8 +5512,8 @@ class RegistryResolver {
5179
5512
  commit: '',
5180
5513
  registry: registryUrl
5181
5514
  });
5182
- // Update skills.json (project mode only)
5183
- if (!this.isGlobal && save) {
5515
+ // Update skills.json (project mode only, skip for effectively-global installs)
5516
+ if (!effectivelyGlobal && save) {
5184
5517
  this.config.ensureExists();
5185
5518
  this.config.addSkill(skillName, parsed.fullName);
5186
5519
  // Save custom registry to skills.json.registries (for reinstall without lock file)
@@ -5203,7 +5536,7 @@ class RegistryResolver {
5203
5536
  };
5204
5537
  } finally{
5205
5538
  // Clean up temp directory after installation
5206
- await remove(tempDir);
5539
+ await fs_remove(tempDir);
5207
5540
  }
5208
5541
  }
5209
5542
  /**
@@ -5254,10 +5587,11 @@ class RegistryResolver {
5254
5587
  installDir: defaults.installDir
5255
5588
  });
5256
5589
  const results = installer.uninstallFromAgents(name, targetAgents);
5257
- // Remove from lock file (project mode only)
5258
- if (!this.isGlobal) this.lockManager.remove(name);
5259
- // Remove from skills.json (project mode only)
5260
- if (!this.isGlobal && this.config.exists()) this.config.removeSkill(name);
5590
+ // Remove from lock file (project mode only, skip for effectively-global installs)
5591
+ const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
5592
+ if (!effectivelyGlobal) this.lockManager.remove(name);
5593
+ // Remove from skills.json (project mode only, skip for effectively-global installs)
5594
+ if (!effectivelyGlobal && this.config.exists()) this.config.removeSkill(name);
5261
5595
  const successCount = Array.from(results.values()).filter((r)=>r).length;
5262
5596
  logger_logger.success(`Uninstalled ${name} from ${successCount} agent(s)`);
5263
5597
  return results;
@@ -7326,12 +7660,19 @@ const DEFAULT_INSTALL_DIR = '.skills';
7326
7660
  // ============================================================================
7327
7661
  /**
7328
7662
  * Resolve installation scope (global vs project)
7329
- */ async function resolveInstallScope(ctx) {
7663
+ */ async function resolveInstallScope(ctx, targetAgents) {
7330
7664
  const { options, isReinstallAll, skipConfirm } = ctx;
7331
7665
  // Explicit --global flag
7332
7666
  if (void 0 !== options.global) return options.global;
7333
- // Skip prompt for reinstall-all (always project scope)
7667
+ // Skip prompt for reinstall-all (always project scope).
7668
+ // Must be checked before the claude-cowork-3p override to avoid
7669
+ // "Cannot install all skills globally" when 3p is the only detected agent.
7334
7670
  if (isReinstallAll) return false;
7671
+ // claude-cowork-3p always installs globally — skip prompt
7672
+ if (targetAgents.length > 0 && targetAgents.every((a)=>a === claude_3p_installer_CLAUDE_COWORK_3P_AGENT)) {
7673
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.info('Using global scope (claude-cowork-3p is always global)');
7674
+ return true;
7675
+ }
7335
7676
  // Skip prompt if --yes
7336
7677
  if (skipConfirm) return false;
7337
7678
  // Prompt user
@@ -7361,10 +7702,12 @@ const DEFAULT_INSTALL_DIR = '.skills';
7361
7702
  // ============================================================================
7362
7703
  /**
7363
7704
  * Resolve installation mode (symlink vs copy)
7364
- */ async function resolveInstallMode(ctx) {
7705
+ */ async function resolveInstallMode(ctx, targetAgents) {
7365
7706
  const { options, storedMode, isReinstallAll, skipConfirm } = ctx;
7366
7707
  // Priority 1: CLI --mode option
7367
7708
  if (options.mode) return options.mode;
7709
+ // claude-cowork-3p always uses copy mode — skip prompt
7710
+ if (targetAgents.length > 0 && targetAgents.every((a)=>a === claude_3p_installer_CLAUDE_COWORK_3P_AGENT)) return 'copy';
7368
7711
  // Priority 2: Reinstall all with stored mode
7369
7712
  if (isReinstallAll && storedMode) {
7370
7713
  __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.info(`Using saved install mode: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(storedMode)}`);
@@ -7846,14 +8189,14 @@ const DEFAULT_INSTALL_DIR = '.skills';
7846
8189
  // Step 1: Resolve target agents
7847
8190
  targetAgents = await resolveTargetAgents(ctx, spinner);
7848
8191
  // Step 2: Resolve installation scope
7849
- installGlobally = await resolveInstallScope(ctx);
8192
+ installGlobally = await resolveInstallScope(ctx, targetAgents);
7850
8193
  // Validate: Cannot install all skills globally
7851
8194
  if (ctx.isReinstallAll && installGlobally) {
7852
8195
  __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error('Cannot install all skills globally. Please specify a skill to install.');
7853
8196
  process.exit(1);
7854
8197
  }
7855
8198
  // Step 3: Resolve installation mode
7856
- installMode = await resolveInstallMode(ctx);
8199
+ installMode = await resolveInstallMode(ctx, targetAgents);
7857
8200
  }
7858
8201
  // Step 4: Execute installation
7859
8202
  if (ctx.isReinstallAll) await installAllSkills(ctx, targetAgents, installMode, spinner);
@@ -7871,14 +8214,23 @@ const DEFAULT_INSTALL_DIR = '.skills';
7871
8214
  });
7872
8215
  /**
7873
8216
  * list command - List installed skills
7874
- */ const listCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('list').alias('ls').description('List installed skills').option('-j, --json', 'Output as JSON').option('-g, --global', 'List globally installed skills').action((options)=>{
7875
- const isGlobal = options.global || false;
8217
+ */ const listCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('list').alias('ls').description('List installed skills').option('-j, --json', 'Output as JSON').option('-g, --global', 'List globally installed skills').option('-a, --agent <agent>', 'List skills installed to a specific agent').action((options)=>{
8218
+ const agentInput = options.agent;
8219
+ if (void 0 !== agentInput && !isValidAgentType(agentInput)) {
8220
+ logger_logger.error(`Invalid agent: ${agentInput}`);
8221
+ process.exit(1);
8222
+ }
8223
+ const agent = agentInput;
8224
+ // claude-cowork-3p is always global
8225
+ const isGlobal = options.global || agent === claude_3p_installer_CLAUDE_COWORK_3P_AGENT;
7876
8226
  const skillManager = new SkillManager(void 0, {
7877
8227
  global: isGlobal
7878
8228
  });
7879
- const skills = skillManager.list();
8229
+ const skills = skillManager.list(agent ? {
8230
+ agent
8231
+ } : void 0);
7880
8232
  if (0 === skills.length) {
7881
- const location = isGlobal ? 'globally' : 'in this project';
8233
+ const location = agent ? `for ${getAgentConfig(agent).displayName}` : isGlobal ? 'globally' : 'in this project';
7882
8234
  logger_logger.info(`No skills installed ${location}`);
7883
8235
  return;
7884
8236
  }
@@ -7886,7 +8238,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
7886
8238
  console.log(JSON.stringify(skills, null, 2));
7887
8239
  return;
7888
8240
  }
7889
- const scopeLabel = isGlobal ? 'global' : 'project';
8241
+ const scopeLabel = agent ? getAgentConfig(agent).displayName : isGlobal ? 'global' : 'project';
7890
8242
  logger_logger.log(`Installed Skills (${scopeLabel}):`);
7891
8243
  logger_logger.newline();
7892
8244
  const headers = [
@@ -9504,7 +9856,7 @@ async function publishAction(skillPath, options) {
9504
9856
  const validation = validator.validate(absolutePath);
9505
9857
  // 3.5. Content security scan
9506
9858
  const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
9507
- if (exists(skillMdPath)) {
9859
+ if (fs_exists(skillMdPath)) {
9508
9860
  const scanner = new ContentScanner();
9509
9861
  const scanResult = scanner.scanFile(skillMdPath);
9510
9862
  displayScanFindings(scanResult);