agentloom 0.1.5 → 0.1.7

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.
@@ -1,14 +1,19 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { cancel, isCancel, multiselect, select, text as promptText, } from "@clack/prompts";
4
+ import TOML from "@iarna/toml";
5
+ import matter from "gray-matter";
6
+ import YAML from "yaml";
4
7
  import { buildAgentMarkdown, parseAgentsDir, targetFileNameForAgent, } from "./agents.js";
5
- import { normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
6
- import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
7
- import { ensureDir, hashContent, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
8
+ import { normalizeCommandArgumentsForCanonical, normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
9
+ import { normalizeRuleSelector, parseRulesDir, resolveRuleSelections, stripRuleFileExtension, } from "./rules.js";
10
+ import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelector, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
11
+ import { ensureDir, hashContent, isObject, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
8
12
  import { ALL_PROVIDERS } from "../types.js";
9
13
  import { readLockfile, upsertLockEntry, writeLockfile } from "./lockfile.js";
10
14
  import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
11
- import { discoverSourceAgentsDir, discoverSourceCommandsDir, discoverSourceMcpPath, discoverSourceSkillsDir, prepareSource, } from "./sources.js";
15
+ import { discoverPluginSourceRoots, discoverSourceAgentsDirs, discoverSourceCommandsDirs, discoverSourceMcpPaths, discoverSourceRulesDirs, discoverSourceSkillsDirs, prepareSource, } from "./sources.js";
16
+ import { isProviderEntityFileName } from "./provider-entity-validation.js";
12
17
  export class NonInteractiveConflictError extends Error {
13
18
  constructor(message) {
14
19
  super(message);
@@ -24,10 +29,13 @@ export async function importSource(options) {
24
29
  const requireAgents = options.requireAgents ?? shouldImportAgents;
25
30
  const shouldImportCommands = options.importCommands ?? true;
26
31
  const shouldImportMcp = options.importMcp ?? true;
32
+ const shouldImportRules = options.importRules ?? false;
33
+ const requireRules = options.requireRules ?? shouldImportRules;
27
34
  const shouldImportSkills = options.importSkills ?? false;
28
35
  if (!shouldImportAgents &&
29
36
  !shouldImportCommands &&
30
37
  !shouldImportMcp &&
38
+ !shouldImportRules &&
31
39
  !shouldImportSkills) {
32
40
  throw new Error("No import targets selected.");
33
41
  }
@@ -43,50 +51,77 @@ export async function importSource(options) {
43
51
  ? `${options.source} (subdir: ${options.subdir})`
44
52
  : options.source;
45
53
  try {
46
- const sourceAgentsDir = shouldImportAgents
47
- ? discoverSourceAgentsDir(prepared.importRoot)
48
- : null;
49
- const sourceCommandsDir = shouldImportCommands
50
- ? discoverSourceCommandsDir(prepared.importRoot)
51
- : null;
52
- const sourceMcpPath = shouldImportMcp
53
- ? discoverSourceMcpPath(prepared.importRoot)
54
- : null;
55
- const sourceSkillsDir = shouldImportSkills
56
- ? discoverSourceSkillsDir(prepared.importRoot)
57
- : null;
58
- const sourceAgents = sourceAgentsDir ? parseAgentsDir(sourceAgentsDir) : [];
59
- const sourceCommands = sourceCommandsDir
60
- ? parseCommandsDir(sourceCommandsDir)
54
+ const pluginSourceRoots = discoverPluginSourceRoots(prepared.importRoot);
55
+ const sourceAgentsDirs = shouldImportAgents
56
+ ? discoverSourceAgentsDirs(prepared.importRoot)
57
+ : [];
58
+ const sourceCommandsDirs = shouldImportCommands
59
+ ? discoverSourceCommandsDirs(prepared.importRoot)
60
+ : [];
61
+ const sourceMcpPaths = shouldImportMcp
62
+ ? discoverSourceMcpPaths(prepared.importRoot)
61
63
  : [];
62
- const sourceMcp = sourceMcpPath
63
- ? normalizeMcp(readJsonIfExists(sourceMcpPath))
64
+ const sourceRulesDirs = shouldImportRules
65
+ ? discoverSourceRulesDirs(prepared.importRoot)
66
+ : [];
67
+ const sourceSkillsDirs = shouldImportSkills
68
+ ? discoverSourceSkillsDirs(prepared.importRoot)
69
+ : [];
70
+ const sourceAgents = sourceAgentsDirs.length > 0
71
+ ? parseSourceAgentsForImport(sourceAgentsDirs, pluginSourceRoots)
72
+ : [];
73
+ const sourceCommands = sourceCommandsDirs.length > 0
74
+ ? parseSourceCommandsForImport(sourceCommandsDirs, pluginSourceRoots)
75
+ : [];
76
+ const sourceMcp = sourceMcpPaths.length > 0
77
+ ? parseSourceMcpForImport(sourceMcpPaths, pluginSourceRoots)
64
78
  : null;
65
- const sourceSkills = sourceSkillsDir ? parseSkillsDir(sourceSkillsDir) : [];
79
+ const sourceRules = sourceRulesDirs.length > 0
80
+ ? parseSourceRulesForImport(sourceRulesDirs, pluginSourceRoots)
81
+ : [];
82
+ const sourceSkills = sourceSkillsDirs.length > 0
83
+ ? parseSourceSkillsForImport(sourceSkillsDirs, pluginSourceRoots)
84
+ : [];
85
+ const sourceAgentsDir = sourceAgentsDirs[0] ?? null;
86
+ const sourceCommandsDir = sourceCommandsDirs[0] ?? null;
87
+ const sourceMcpPath = sourceMcpPaths[0] ?? null;
88
+ const sourceRulesDir = sourceRulesDirs[0] ?? null;
89
+ const sourceSkillsDir = sourceSkillsDirs[0] ?? null;
66
90
  const hasExplicitCommandSelection = (options.commandSelectors?.length ?? 0) > 0;
67
91
  const isAggregateImport = shouldImportAgents &&
68
92
  shouldImportCommands &&
69
93
  shouldImportMcp &&
94
+ (options.importRules === undefined || shouldImportRules) &&
70
95
  shouldImportSkills;
71
- if (shouldImportAgents && requireAgents && !sourceAgentsDir) {
72
- throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/ or .agents/agents/).`);
96
+ if (shouldImportAgents && requireAgents && sourceAgentsDirs.length === 0) {
97
+ throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/, .agents/agents/, or .github/agents/, including plugin sources declared in .claude-plugin/marketplace.json).`);
73
98
  }
74
99
  if (shouldImportAgents && requireAgents && sourceAgents.length === 0) {
75
100
  throw new Error(`No agent files found in ${sourceAgentsDir}.`);
76
101
  }
77
- if (shouldImportCommands && options.requireCommands && !sourceCommandsDir) {
78
- throw new Error(`No source commands directory found under ${prepared.importRoot} (expected .agents/commands/, commands/, or prompts/).`);
102
+ if (shouldImportCommands &&
103
+ options.requireCommands &&
104
+ sourceCommandsDirs.length === 0) {
105
+ throw new Error(`No source commands directory found under ${prepared.importRoot} (expected .agents/commands/, commands/, prompts/, .gemini/commands/, or .github/prompts/, including plugin sources declared in .claude-plugin/marketplace.json).`);
79
106
  }
80
107
  if (shouldImportCommands &&
81
108
  options.requireCommands &&
82
109
  sourceCommands.length === 0) {
83
110
  throw new Error(`No command files found in ${sourceCommandsDir}.`);
84
111
  }
85
- if (shouldImportMcp && options.requireMcp && !sourceMcpPath) {
86
- throw new Error(`No source mcp.json found under ${prepared.importRoot} (expected mcp.json or .agents/mcp.json).`);
112
+ if (shouldImportMcp && options.requireMcp && sourceMcpPaths.length === 0) {
113
+ throw new Error(`No source mcp.json found under ${prepared.importRoot} (expected mcp.json or .agents/mcp.json, including plugin sources declared in .claude-plugin/marketplace.json).`);
114
+ }
115
+ if (shouldImportRules && requireRules && sourceRulesDirs.length === 0) {
116
+ throw new Error(`No source rules directory found under ${prepared.importRoot} (expected .agents/rules/ or rules/, including plugin sources declared in .claude-plugin/marketplace.json).`);
117
+ }
118
+ if (shouldImportRules && requireRules && sourceRules.length === 0) {
119
+ throw new Error(`No rule files found in ${sourceRulesDir}.`);
87
120
  }
88
- if (shouldImportSkills && options.requireSkills && !sourceSkillsDir) {
89
- throw new Error(`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md).`);
121
+ if (shouldImportSkills &&
122
+ options.requireSkills &&
123
+ sourceSkillsDirs.length === 0) {
124
+ throw new Error(`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md, including plugin sources declared in .claude-plugin/marketplace.json).`);
90
125
  }
91
126
  if (shouldImportSkills &&
92
127
  options.requireSkills &&
@@ -96,9 +131,10 @@ export async function importSource(options) {
96
131
  if (isAggregateImport &&
97
132
  sourceAgents.length === 0 &&
98
133
  sourceCommands.length === 0 &&
134
+ sourceRules.length === 0 &&
99
135
  sourceSkills.length === 0 &&
100
136
  Object.keys(sourceMcp?.mcpServers ?? {}).length === 0) {
101
- throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, commands/, .agents/commands/, prompts/, mcp.json/.agents/mcp.json, skills/, .agents/skills/, or root SKILL.md.`);
137
+ throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, or plugin sources from .claude-plugin/marketplace.json.`);
102
138
  }
103
139
  const shouldResolveAgents = shouldImportAgents &&
104
140
  (sourceAgents.length > 0 ||
@@ -145,6 +181,21 @@ export async function importSource(options) {
145
181
  selectedSourceMcpServers = mcpSelection.selectedServerNames;
146
182
  mcpSelectionMode = mcpSelection.selectionMode;
147
183
  }
184
+ let selectedRules = [];
185
+ let selectedSourceRules = [];
186
+ let ruleSelectionMode = "all";
187
+ if (shouldImportRules && sourceRulesDir) {
188
+ const ruleSelection = await resolveRulesToImport({
189
+ sourceRules,
190
+ selectors: options.ruleSelectors ?? [],
191
+ promptForRules: options.promptForRules ?? true,
192
+ nonInteractive: Boolean(options.nonInteractive),
193
+ selectionMode: options.selectionMode,
194
+ });
195
+ selectedRules = ruleSelection.selectedRules;
196
+ ruleSelectionMode = ruleSelection.selectionMode;
197
+ selectedSourceRules = selectedRules.map((rule) => rule.fileName);
198
+ }
148
199
  let selectedSkills = [];
149
200
  let selectedSourceSkills = [];
150
201
  let skillSelectionMode = "all";
@@ -157,8 +208,8 @@ export async function importSource(options) {
157
208
  selectionMode: options.selectionMode,
158
209
  });
159
210
  selectedSkills = skillSelection.selectedSkills;
211
+ selectedSourceSkills = skillSelection.selectedSourceSkills;
160
212
  skillSelectionMode = skillSelection.selectionMode;
161
- selectedSourceSkills = selectedSkills.map((skill) => skill.name);
162
213
  }
163
214
  const importedAgents = [];
164
215
  if (shouldImportAgents && selection.selectedAgents.length > 0) {
@@ -240,6 +291,47 @@ export async function importSource(options) {
240
291
  }
241
292
  importedMcpServers.push(...selectedSourceMcpServers);
242
293
  }
294
+ const importedRules = [];
295
+ const telemetryRules = [];
296
+ const importedRuleRenameMap = {};
297
+ if (shouldImportRules && sourceRulesDir) {
298
+ if (selectedRules.length > 0) {
299
+ ensureDir(options.paths.rulesDir);
300
+ }
301
+ for (const [index, rule] of selectedRules.entries()) {
302
+ let targetFileName = rule.fileName;
303
+ const mappedTargetFileName = resolveMappedTargetRuleFileName(rule.fileName, options.ruleRenameMap);
304
+ if (mappedTargetFileName) {
305
+ targetFileName = mappedTargetFileName;
306
+ }
307
+ else if (options.rename &&
308
+ selectedRules.length === 1 &&
309
+ importedAgents.length === 0 &&
310
+ importedCommands.length === 0 &&
311
+ importedMcpServers.length === 0 &&
312
+ selectedSkills.length === 0) {
313
+ targetFileName = `${slugify(options.rename) || "rule"}.md`;
314
+ }
315
+ const resolvedFileName = await resolveRuleConflict({
316
+ targetFileName,
317
+ ruleContent: rule.content,
318
+ paths: options.paths,
319
+ yes: !!options.yes,
320
+ nonInteractive: !!options.nonInteractive,
321
+ promptLabel: `${rule.fileName} (${index + 1}/${selectedRules.length})`,
322
+ });
323
+ if (!resolvedFileName)
324
+ continue;
325
+ const targetPath = path.join(options.paths.rulesDir, resolvedFileName);
326
+ writeTextAtomic(targetPath, rule.content);
327
+ importedRules.push(relativePosix(options.paths.agentsRoot, targetPath));
328
+ telemetryRules.push({
329
+ name: stripRuleFileExtension(rule.fileName),
330
+ filePath: relativePosix(prepared.rootPath, rule.sourcePath),
331
+ });
332
+ importedRuleRenameMap[rule.fileName] = resolvedFileName;
333
+ }
334
+ }
243
335
  const importedSkills = [];
244
336
  const telemetrySkills = [];
245
337
  const importedSkillRenameMap = {};
@@ -249,21 +341,32 @@ export async function importSource(options) {
249
341
  ensureDir(options.paths.skillsDir);
250
342
  }
251
343
  for (const [index, sourceSkill] of selectedSkills.entries()) {
252
- let targetSkillDirName = slugify(sourceSkill.name) || "skill";
253
- const mappedTargetSkillDirName = resolveMappedTargetSkillName(sourceSkill.name, options.skillRenameMap);
344
+ const canonicalSkillDirName = slugify(sourceSkill.name) || "skill";
345
+ const legacySkillDirName = slugify(sourceSkill.sourceDirName) || "skill";
346
+ let targetSkillDirName = canonicalSkillDirName;
347
+ const mappedTargetSkillDirName = resolveMappedTargetSkillName(sourceSkill, selectedSkills, options.skillRenameMap);
254
348
  if (mappedTargetSkillDirName) {
255
- targetSkillDirName = mappedTargetSkillDirName;
349
+ targetSkillDirName =
350
+ mappedTargetSkillDirName === legacySkillDirName &&
351
+ legacySkillDirName !== canonicalSkillDirName
352
+ ? canonicalSkillDirName
353
+ : mappedTargetSkillDirName;
256
354
  }
257
355
  else if (options.rename &&
258
356
  selectedSkills.length === 1 &&
259
357
  importedAgents.length === 0 &&
260
358
  importedCommands.length === 0 &&
261
- importedMcpServers.length === 0) {
359
+ importedMcpServers.length === 0 &&
360
+ importedRules.length === 0) {
262
361
  targetSkillDirName = slugify(options.rename) || "skill";
263
362
  }
264
363
  const resolvedSkillDirName = await resolveSkillConflict({
265
364
  sourceSkill,
266
365
  targetSkillDirName,
366
+ legacySkillDirName: targetSkillDirName === canonicalSkillDirName
367
+ ? legacySkillDirName
368
+ : undefined,
369
+ canonicalSkillDirName,
267
370
  paths: options.paths,
268
371
  yes: !!options.yes,
269
372
  nonInteractive: !!options.nonInteractive,
@@ -272,10 +375,25 @@ export async function importSource(options) {
272
375
  if (!resolvedSkillDirName)
273
376
  continue;
274
377
  const targetSkillDir = path.join(options.paths.skillsDir, resolvedSkillDirName);
378
+ if (resolvedSkillDirName === canonicalSkillDirName) {
379
+ moveLegacySkillDirectoryToCanonicalIfUnchanged({
380
+ sourceSkill,
381
+ legacySkillDirName,
382
+ canonicalSkillDirName,
383
+ paths: options.paths,
384
+ });
385
+ }
275
386
  if (!skillContentMatchesTarget(sourceSkill, targetSkillDir)) {
276
387
  fs.rmSync(targetSkillDir, { recursive: true, force: true });
277
388
  copySkillArtifacts(sourceSkill, targetSkillDir);
278
389
  }
390
+ if (resolvedSkillDirName === canonicalSkillDirName) {
391
+ removeLegacySkillDirectory({
392
+ legacySkillDirName,
393
+ canonicalSkillDirName,
394
+ paths: options.paths,
395
+ });
396
+ }
279
397
  importedSkills.push(resolvedSkillDirName);
280
398
  telemetrySkills.push({
281
399
  name: sourceSkill.name,
@@ -303,10 +421,12 @@ export async function importSource(options) {
303
421
  selectedSourceCommandFiles.length < sourceCommands.length;
304
422
  const selectedSubsetOfSourceMcp = sourceMcpServerNames.length > 0 &&
305
423
  selectedSourceMcpServers.length < sourceMcpServerNames.length;
424
+ const selectedSubsetOfSourceRules = sourceRules.length > 0 && selectedSourceRules.length < sourceRules.length;
306
425
  const selectedSubsetOfSourceSkills = sourceSkills.length > 0 &&
307
426
  selectedSourceSkills.length < sourceSkills.length;
308
427
  const shouldPersistCommandSelection = shouldImportCommands && commandSelectionMode === "custom";
309
428
  const shouldPersistMcpSelection = shouldImportMcp && mcpSelectionMode === "custom";
429
+ const shouldPersistRuleSelection = shouldImportRules && ruleSelectionMode === "custom";
310
430
  const shouldPersistSkillSelection = shouldImportSkills && skillSelectionMode === "custom";
311
431
  const selectedSourceCommandsForLock = shouldPersistCommandSelection || selectedSubsetOfSourceCommands
312
432
  ? selectedSourceCommandFiles
@@ -314,6 +434,9 @@ export async function importSource(options) {
314
434
  const selectedSourceMcpServersForLock = shouldPersistMcpSelection || selectedSubsetOfSourceMcp
315
435
  ? selectedSourceMcpServers
316
436
  : undefined;
437
+ const selectedSourceRulesForLock = shouldPersistRuleSelection || selectedSubsetOfSourceRules
438
+ ? selectedSourceRules
439
+ : undefined;
317
440
  const selectedSourceSkillsForLock = shouldPersistSkillSelection || selectedSubsetOfSourceSkills
318
441
  ? selectedSourceSkills
319
442
  : undefined;
@@ -321,6 +444,7 @@ export async function importSource(options) {
321
444
  const isCommandOnlyImport = !shouldImportAgents &&
322
445
  shouldImportCommands &&
323
446
  !shouldImportMcp &&
447
+ !shouldImportRules &&
324
448
  !shouldImportSkills;
325
449
  const existingEntry = isCommandOnlyImport
326
450
  ? findRelaxedCommandEntry(lockfile.entries, {
@@ -338,7 +462,9 @@ export async function importSource(options) {
338
462
  : options.agents,
339
463
  selectedSourceCommands: selectedSourceCommandsForLock,
340
464
  selectedSourceMcpServers: selectedSourceMcpServersForLock,
465
+ selectedSourceRules: selectedSourceRulesForLock,
341
466
  selectedSourceSkills: selectedSourceSkillsForLock,
467
+ selectedSkills,
342
468
  skillsProviders: skillsProvidersForLock,
343
469
  });
344
470
  const shouldMergeCommandOnlyEntry = isCommandOnlyImport &&
@@ -360,6 +486,9 @@ export async function importSource(options) {
360
486
  const lockImportedMcpServers = shouldImportMcp
361
487
  ? importedMcpServers
362
488
  : (existingEntry?.importedMcpServers ?? []);
489
+ const lockImportedRules = shouldImportRules
490
+ ? importedRules
491
+ : (existingEntry?.importedRules ?? []);
363
492
  const lockImportedSkills = shouldImportSkills
364
493
  ? importedSkills
365
494
  : (existingEntry?.importedSkills ?? []);
@@ -392,11 +521,19 @@ export async function importSource(options) {
392
521
  ? [...selectedSourceMcpServers]
393
522
  : undefined
394
523
  : existingEntry?.selectedSourceMcpServers;
524
+ const lockSelectedSourceRules = shouldImportRules
525
+ ? shouldPersistRuleSelection || selectedSubsetOfSourceRules
526
+ ? [...selectedSourceRules]
527
+ : undefined
528
+ : existingEntry?.selectedSourceRules;
395
529
  const lockSelectedSourceSkills = shouldImportSkills
396
530
  ? shouldPersistSkillSelection || selectedSubsetOfSourceSkills
397
531
  ? [...selectedSourceSkills]
398
532
  : undefined
399
533
  : existingEntry?.selectedSourceSkills;
534
+ const lockRuleRenameMap = shouldImportRules
535
+ ? normalizeRuleRenameMap(importedRuleRenameMap)
536
+ : existingEntry?.ruleRenameMap;
400
537
  const lockSkillsProviders = shouldImportSkills
401
538
  ? (skillsProvidersForLock ?? existingEntry?.skillsProviders)
402
539
  : existingEntry?.skillsProviders;
@@ -426,6 +563,9 @@ export async function importSource(options) {
426
563
  commandRenameMap: lockCommandRenameMap,
427
564
  importedMcpServers: lockImportedMcpServers,
428
565
  selectedSourceMcpServers: lockSelectedSourceMcpServers,
566
+ importedRules: lockImportedRules,
567
+ selectedSourceRules: lockSelectedSourceRules,
568
+ ruleRenameMap: lockRuleRenameMap,
429
569
  importedSkills: lockImportedSkills,
430
570
  selectedSourceSkills: lockSelectedSourceSkills,
431
571
  skillsProviders: lockSkillsProviders,
@@ -438,6 +578,9 @@ export async function importSource(options) {
438
578
  commandRenameMap: lockCommandRenameMap ?? {},
439
579
  mcp: lockImportedMcpServers,
440
580
  selectedSourceMcpServers: lockSelectedSourceMcpServers ?? [],
581
+ rules: lockImportedRules,
582
+ selectedSourceRules: lockSelectedSourceRules ?? [],
583
+ ruleRenameMap: lockRuleRenameMap ?? {},
441
584
  skills: lockImportedSkills,
442
585
  selectedSourceSkills: lockSelectedSourceSkills ?? [],
443
586
  skillsProviders: lockSkillsProviders ?? [],
@@ -458,6 +601,9 @@ export async function importSource(options) {
458
601
  commandRenameMap: lockCommandRenameMap,
459
602
  importedMcpServers: lockImportedMcpServers,
460
603
  selectedSourceMcpServers: lockSelectedSourceMcpServers,
604
+ importedRules: lockImportedRules,
605
+ selectedSourceRules: lockSelectedSourceRules,
606
+ ruleRenameMap: lockRuleRenameMap,
461
607
  importedSkills: lockImportedSkills,
462
608
  selectedSourceSkills: lockSelectedSourceSkills,
463
609
  skillsProviders: lockSkillsProviders,
@@ -484,7 +630,9 @@ export async function importSource(options) {
484
630
  importedAgents,
485
631
  importedCommands,
486
632
  importedMcpServers,
633
+ importedRules,
487
634
  importedSkills,
635
+ telemetryRules: telemetryRules.length > 0 ? telemetryRules : undefined,
488
636
  telemetrySkills: telemetrySkills.length > 0 ? telemetrySkills : undefined,
489
637
  resolvedCommit: prepared.resolvedCommit,
490
638
  };
@@ -493,6 +641,504 @@ export async function importSource(options) {
493
641
  prepared.cleanup();
494
642
  }
495
643
  }
644
+ function parseSourceAgentsForImport(sourceAgentsDirs, pluginSourceRoots) {
645
+ const sourceAgents = sourceAgentsDirs.flatMap((sourceAgentsDir) => {
646
+ if (isGitHubAgentsDir(sourceAgentsDir)) {
647
+ return parseGitHubAgentsDirForImport(sourceAgentsDir);
648
+ }
649
+ return parseAgentsDir(sourceAgentsDir);
650
+ });
651
+ assertNoPluginSourceCollisions({
652
+ entityLabel: "agent",
653
+ pluginSourceRoots,
654
+ entries: sourceAgents.map((agent) => ({
655
+ key: targetFileNameForAgent(agent),
656
+ sourcePath: agent.sourcePath,
657
+ })),
658
+ });
659
+ return sourceAgents;
660
+ }
661
+ function parseSourceCommandsForImport(sourceCommandsDirs, pluginSourceRoots) {
662
+ const sourceCommands = sourceCommandsDirs.flatMap((dirPath) => parseSourceCommandsFromDir(dirPath));
663
+ assertNoPluginSourceCollisions({
664
+ entityLabel: "command",
665
+ pluginSourceRoots,
666
+ entries: sourceCommands.map((command) => ({
667
+ key: toCanonicalCommandFileName(command.fileName),
668
+ sourcePath: command.sourcePath,
669
+ })),
670
+ });
671
+ return mergeCanonicalCommandFiles(sourceCommands);
672
+ }
673
+ function parseSourceMcpForImport(sourceMcpPaths, pluginSourceRoots) {
674
+ const mergedMcpServers = {};
675
+ const seenServerSource = new Map();
676
+ for (const sourceMcpPath of sourceMcpPaths) {
677
+ const sourceMcp = normalizeMcp(readJsonIfExists(sourceMcpPath));
678
+ for (const [serverName, serverConfig] of Object.entries(sourceMcp.mcpServers)) {
679
+ const pluginSourceRoot = resolvePluginSourceRootForPath(sourceMcpPath, pluginSourceRoots);
680
+ const existing = seenServerSource.get(serverName);
681
+ if (existing &&
682
+ existing.pluginSourceRoot &&
683
+ pluginSourceRoot &&
684
+ existing.pluginSourceRoot !== pluginSourceRoot) {
685
+ throw buildPluginCollisionError({
686
+ entityLabel: "mcp server",
687
+ key: serverName,
688
+ sourcePaths: [existing.sourcePath, sourceMcpPath],
689
+ });
690
+ }
691
+ mergedMcpServers[serverName] = serverConfig;
692
+ seenServerSource.set(serverName, {
693
+ sourcePath: sourceMcpPath,
694
+ pluginSourceRoot,
695
+ });
696
+ }
697
+ }
698
+ return {
699
+ version: 1,
700
+ mcpServers: mergedMcpServers,
701
+ };
702
+ }
703
+ function parseSourceRulesForImport(sourceRulesDirs, pluginSourceRoots) {
704
+ const sourceRules = sourceRulesDirs.flatMap((sourceRulesDir) => parseRulesDir(sourceRulesDir));
705
+ assertNoPluginSourceCollisions({
706
+ entityLabel: "rule",
707
+ pluginSourceRoots,
708
+ entries: sourceRules.map((rule) => ({
709
+ key: rule.id,
710
+ sourcePath: rule.sourcePath,
711
+ })),
712
+ });
713
+ return sourceRules;
714
+ }
715
+ function parseSourceSkillsForImport(sourceSkillsDirs, pluginSourceRoots) {
716
+ const sourceSkills = sourceSkillsDirs.flatMap((sourceSkillsDir) => parseSkillsDir(sourceSkillsDir));
717
+ assertNoPluginSourceCollisions({
718
+ entityLabel: "skill",
719
+ pluginSourceRoots,
720
+ entries: sourceSkills.map((skill) => ({
721
+ key: normalizeSkillSelector(skill.name),
722
+ sourcePath: skill.skillPath,
723
+ })),
724
+ });
725
+ assertNoDuplicateSkillNames(sourceSkills);
726
+ return sourceSkills;
727
+ }
728
+ function assertNoDuplicateSkillNames(sourceSkills) {
729
+ const byName = new Map();
730
+ for (const skill of sourceSkills) {
731
+ const normalizedName = normalizeSkillSelector(skill.name);
732
+ if (!normalizedName)
733
+ continue;
734
+ const matches = byName.get(normalizedName) ?? [];
735
+ matches.push({
736
+ name: skill.name,
737
+ sourcePath: skill.skillPath,
738
+ });
739
+ byName.set(normalizedName, matches);
740
+ }
741
+ for (const matches of byName.values()) {
742
+ const sourcePaths = [...new Set(matches.map((item) => item.sourcePath))];
743
+ if (sourcePaths.length < 2) {
744
+ continue;
745
+ }
746
+ const locations = sourcePaths
747
+ .map((sourcePath) => `- ${sourcePath}`)
748
+ .join("\n");
749
+ throw new Error(`Conflicting skill "${matches[0]?.name ?? "unknown"}" found in source:\n${locations}\nEnsure each SKILL.md frontmatter name is unique.`);
750
+ }
751
+ }
752
+ function assertNoPluginSourceCollisions(options) {
753
+ if (options.pluginSourceRoots.length === 0 || options.entries.length === 0) {
754
+ return;
755
+ }
756
+ const byKey = new Map();
757
+ for (const entry of options.entries) {
758
+ const normalizedKey = entry.key.trim().toLowerCase();
759
+ if (!normalizedKey)
760
+ continue;
761
+ const pluginSourceRoot = resolvePluginSourceRootForPath(entry.sourcePath, options.pluginSourceRoots);
762
+ const group = byKey.get(normalizedKey) ?? [];
763
+ group.push({
764
+ key: entry.key,
765
+ sourcePath: entry.sourcePath,
766
+ pluginSourceRoot,
767
+ });
768
+ byKey.set(normalizedKey, group);
769
+ }
770
+ for (const matches of byKey.values()) {
771
+ const pluginRoots = [
772
+ ...new Set(matches
773
+ .map((item) => item.pluginSourceRoot)
774
+ .filter((item) => Boolean(item))),
775
+ ];
776
+ if (pluginRoots.length < 2) {
777
+ continue;
778
+ }
779
+ throw buildPluginCollisionError({
780
+ entityLabel: options.entityLabel,
781
+ key: matches[0]?.key ?? "unknown",
782
+ sourcePaths: matches.map((item) => item.sourcePath),
783
+ });
784
+ }
785
+ }
786
+ function buildPluginCollisionError(options) {
787
+ const locations = [...new Set(options.sourcePaths)]
788
+ .map((sourcePath) => `- ${sourcePath}`)
789
+ .join("\n");
790
+ return new Error(`Conflicting ${options.entityLabel} "${options.key}" found across plugin sources declared in .claude-plugin/marketplace.json:\n${locations}\nUse --subdir to import a single plugin source.`);
791
+ }
792
+ function resolvePluginSourceRootForPath(sourcePath, pluginSourceRoots) {
793
+ for (const pluginSourceRoot of [...pluginSourceRoots].sort((left, right) => right.length - left.length)) {
794
+ const normalizedRoot = path.resolve(pluginSourceRoot);
795
+ const normalizedPath = path.resolve(sourcePath);
796
+ if (normalizedPath === normalizedRoot ||
797
+ normalizedPath.startsWith(`${normalizedRoot}${path.sep}`)) {
798
+ return normalizedRoot;
799
+ }
800
+ }
801
+ return null;
802
+ }
803
+ function parseSourceCommandsFromDir(sourceCommandsDir) {
804
+ if (isGeminiCommandsDir(sourceCommandsDir)) {
805
+ const geminiMarkdownCommands = parseCommandsDir(sourceCommandsDir).filter((command) => isProviderEntityFileName({
806
+ provider: "gemini",
807
+ entity: "command",
808
+ fileName: command.fileName,
809
+ }));
810
+ const parsedCommands = mergeCommandsByCanonicalFileName([
811
+ ...parseGeminiTomlCommandsForImport(sourceCommandsDir),
812
+ ...geminiMarkdownCommands,
813
+ ]);
814
+ return parsedCommands.map((command) => normalizeGeminiCommandForImport(command));
815
+ }
816
+ const commands = parseCommandsDir(sourceCommandsDir);
817
+ if (!isGitHubPromptsDir(sourceCommandsDir)) {
818
+ return commands;
819
+ }
820
+ return commands
821
+ .filter((command) => isProviderEntityFileName({
822
+ provider: "copilot",
823
+ entity: "command",
824
+ fileName: command.fileName,
825
+ }))
826
+ .map((command) => normalizeGitHubPromptForImport(command));
827
+ }
828
+ function mergeCommandsByCanonicalFileName(commands) {
829
+ const byFileName = new Map();
830
+ for (const command of commands) {
831
+ if (!byFileName.has(command.fileName)) {
832
+ byFileName.set(command.fileName, command);
833
+ }
834
+ }
835
+ return [...byFileName.values()];
836
+ }
837
+ function isGitHubAgentsDir(sourceAgentsDir) {
838
+ return (path.basename(sourceAgentsDir).toLowerCase() === "agents" &&
839
+ path.basename(path.dirname(sourceAgentsDir)).toLowerCase() === ".github");
840
+ }
841
+ function isGitHubPromptsDir(sourceCommandsDir) {
842
+ return (path.basename(sourceCommandsDir).toLowerCase() === "prompts" &&
843
+ path.basename(path.dirname(sourceCommandsDir)).toLowerCase() === ".github");
844
+ }
845
+ function isGeminiCommandsDir(sourceCommandsDir) {
846
+ return (path.basename(sourceCommandsDir).toLowerCase() === "commands" &&
847
+ path.basename(path.dirname(sourceCommandsDir)).toLowerCase() === ".gemini");
848
+ }
849
+ function parseGeminiTomlCommandsForImport(sourceCommandsDir) {
850
+ return fs
851
+ .readdirSync(sourceCommandsDir, { withFileTypes: true })
852
+ .filter((entry) => entry.isFile())
853
+ .map((entry) => entry.name)
854
+ .filter((fileName) => isProviderEntityFileName({
855
+ provider: "gemini",
856
+ entity: "command",
857
+ fileName,
858
+ }))
859
+ .filter((fileName) => fileName.toLowerCase().endsWith(".toml"))
860
+ .sort((a, b) => a.localeCompare(b))
861
+ .map((fileName) => parseGeminiTomlCommandForImport(path.join(sourceCommandsDir, fileName)))
862
+ .filter((command) => command !== null);
863
+ }
864
+ function mergeCanonicalCommandFiles(commands) {
865
+ const merged = new Map();
866
+ for (const command of commands) {
867
+ const fileName = toCanonicalCommandFileName(command.fileName);
868
+ const normalizedCommand = fileName === command.fileName ? command : { ...command, fileName };
869
+ const existing = merged.get(fileName);
870
+ if (!existing) {
871
+ merged.set(fileName, normalizedCommand);
872
+ continue;
873
+ }
874
+ const existingHasBody = normalizeImportedCommandBody(existing.body).length > 0;
875
+ const incomingHasBody = normalizeImportedCommandBody(normalizedCommand.body).length > 0;
876
+ if (existingHasBody &&
877
+ incomingHasBody &&
878
+ !sameNormalizedImportedCommandBody(existing.body, normalizedCommand.body)) {
879
+ throw new Error(`Conflicting command bodies found for "${fileName}" in ${existing.sourcePath} and ${normalizedCommand.sourcePath}. Align the provider-specific prompts before importing, or import a single provider directory with --subdir.`);
880
+ }
881
+ const body = existingHasBody ? existing.body : normalizedCommand.body;
882
+ const frontmatter = mergeCommandFrontmatterForImport(existing.frontmatter, normalizedCommand.frontmatter);
883
+ merged.set(fileName, {
884
+ ...existing,
885
+ fileName,
886
+ body,
887
+ frontmatter,
888
+ content: buildCommandMarkdownForImport(frontmatter, body),
889
+ });
890
+ }
891
+ return [...merged.values()];
892
+ }
893
+ function sameNormalizedImportedCommandBody(left, right) {
894
+ return (normalizeImportedCommandBody(left) === normalizeImportedCommandBody(right));
895
+ }
896
+ function normalizeImportedCommandBody(value) {
897
+ return value.trim().replace(/\r\n/g, "\n");
898
+ }
899
+ function mergeCommandFrontmatterForImport(existing, incoming) {
900
+ if (!existing && !incoming) {
901
+ return undefined;
902
+ }
903
+ const merged = existing ? cloneUnknown(existing) : {};
904
+ for (const [key, value] of Object.entries(incoming ?? {})) {
905
+ const current = merged[key];
906
+ if (current === undefined) {
907
+ merged[key] = cloneUnknown(value);
908
+ continue;
909
+ }
910
+ if (isObject(current) && isObject(value)) {
911
+ merged[key] = {
912
+ ...cloneUnknown(value),
913
+ ...cloneUnknown(current),
914
+ };
915
+ }
916
+ }
917
+ return Object.keys(merged).length > 0 ? merged : undefined;
918
+ }
919
+ function parseGeminiTomlCommandForImport(sourcePath) {
920
+ const raw = fs.readFileSync(sourcePath, "utf8");
921
+ let parsed;
922
+ try {
923
+ parsed = TOML.parse(raw);
924
+ }
925
+ catch {
926
+ return null;
927
+ }
928
+ if (!isObject(parsed) || typeof parsed.prompt !== "string") {
929
+ return null;
930
+ }
931
+ const body = normalizeCommandArgumentsForCanonical(parsed.prompt, "gemini");
932
+ const frontmatter = cloneUnknown(parsed);
933
+ delete frontmatter.prompt;
934
+ const normalizedFrontmatter = Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
935
+ const fileName = toCanonicalCommandFileName(path.basename(sourcePath));
936
+ const content = buildCommandMarkdownForImport(normalizedFrontmatter, body);
937
+ return {
938
+ fileName,
939
+ sourcePath,
940
+ content,
941
+ body,
942
+ frontmatter: normalizedFrontmatter,
943
+ };
944
+ }
945
+ function parseGitHubAgentsDirForImport(sourceAgentsDir) {
946
+ return fs
947
+ .readdirSync(sourceAgentsDir)
948
+ .filter((entry) => isProviderEntityFileName({
949
+ provider: "copilot",
950
+ entity: "agent",
951
+ fileName: entry,
952
+ }))
953
+ .sort((a, b) => a.localeCompare(b))
954
+ .map((entry) => parseGitHubAgentForImport(path.join(sourceAgentsDir, entry)));
955
+ }
956
+ function parseGitHubAgentForImport(sourcePath) {
957
+ const raw = fs.readFileSync(sourcePath, "utf8");
958
+ const parsed = matter(raw);
959
+ const data = isObject(parsed.data)
960
+ ? parsed.data
961
+ : {};
962
+ const fileName = path.basename(sourcePath);
963
+ const fallbackName = inferAgentNameFromFile(fileName);
964
+ const name = typeof data.name === "string" && data.name.trim().length > 0
965
+ ? data.name.trim()
966
+ : fallbackName;
967
+ const description = typeof data.description === "string" && data.description.trim().length > 0
968
+ ? data.description.trim()
969
+ : `Imported from Copilot agent "${name}".`;
970
+ const frontmatter = {
971
+ name,
972
+ description,
973
+ };
974
+ for (const provider of ALL_PROVIDERS) {
975
+ if (provider === "copilot")
976
+ continue;
977
+ const value = data[provider];
978
+ if (value === false || isObject(value)) {
979
+ frontmatter[provider] = cloneUnknown(value);
980
+ }
981
+ }
982
+ const inferredCopilotConfig = {};
983
+ for (const [key, value] of Object.entries(data)) {
984
+ if (key === "name" || key === "description")
985
+ continue;
986
+ if (ALL_PROVIDERS.includes(key))
987
+ continue;
988
+ inferredCopilotConfig[key] = cloneUnknown(value);
989
+ }
990
+ const explicitCopilot = data.copilot;
991
+ if (explicitCopilot === false) {
992
+ frontmatter.copilot = false;
993
+ }
994
+ else {
995
+ const copilotConfig = isObject(explicitCopilot)
996
+ ? cloneUnknown(explicitCopilot)
997
+ : {};
998
+ for (const [key, value] of Object.entries(inferredCopilotConfig)) {
999
+ if (!(key in copilotConfig)) {
1000
+ copilotConfig[key] = value;
1001
+ }
1002
+ }
1003
+ if (Object.keys(copilotConfig).length > 0) {
1004
+ frontmatter.copilot = copilotConfig;
1005
+ }
1006
+ }
1007
+ return {
1008
+ name,
1009
+ description,
1010
+ body: parsed.content.trimStart(),
1011
+ frontmatter: frontmatter,
1012
+ sourcePath,
1013
+ fileName,
1014
+ };
1015
+ }
1016
+ function normalizeGitHubPromptForImport(command) {
1017
+ const fileName = toCanonicalCommandFileName(command.fileName);
1018
+ if (!command.frontmatter) {
1019
+ return {
1020
+ ...command,
1021
+ fileName,
1022
+ };
1023
+ }
1024
+ const nextFrontmatter = {};
1025
+ for (const provider of ALL_PROVIDERS) {
1026
+ if (provider === "copilot")
1027
+ continue;
1028
+ const value = command.frontmatter[provider];
1029
+ if (value === false || isObject(value)) {
1030
+ nextFrontmatter[provider] = cloneUnknown(value);
1031
+ }
1032
+ }
1033
+ const explicitCopilot = command.frontmatter.copilot;
1034
+ if (explicitCopilot === false) {
1035
+ nextFrontmatter.copilot = false;
1036
+ }
1037
+ else {
1038
+ const copilotConfig = isObject(explicitCopilot)
1039
+ ? cloneUnknown(explicitCopilot)
1040
+ : {};
1041
+ for (const [key, value] of Object.entries(command.frontmatter)) {
1042
+ if (ALL_PROVIDERS.includes(key))
1043
+ continue;
1044
+ if (!(key in copilotConfig)) {
1045
+ copilotConfig[key] = cloneUnknown(value);
1046
+ }
1047
+ }
1048
+ if (Object.keys(copilotConfig).length > 0) {
1049
+ nextFrontmatter.copilot = copilotConfig;
1050
+ }
1051
+ }
1052
+ const frontmatter = Object.keys(nextFrontmatter).length > 0 ? nextFrontmatter : undefined;
1053
+ const body = normalizeCommandArgumentsForCanonical(command.body, "copilot").trimStart();
1054
+ return {
1055
+ ...command,
1056
+ fileName,
1057
+ body,
1058
+ frontmatter,
1059
+ content: buildCommandMarkdownForImport(frontmatter, body),
1060
+ };
1061
+ }
1062
+ function normalizeGeminiCommandForImport(command) {
1063
+ const fileName = toCanonicalCommandFileName(command.fileName);
1064
+ const body = normalizeCommandArgumentsForCanonical(command.body, "gemini");
1065
+ if (!command.frontmatter) {
1066
+ return {
1067
+ ...command,
1068
+ fileName,
1069
+ body,
1070
+ content: buildCommandMarkdownForImport(undefined, body),
1071
+ };
1072
+ }
1073
+ const nextFrontmatter = {};
1074
+ for (const provider of ALL_PROVIDERS) {
1075
+ if (provider === "gemini")
1076
+ continue;
1077
+ const value = command.frontmatter[provider];
1078
+ if (value === false || isObject(value)) {
1079
+ nextFrontmatter[provider] = cloneUnknown(value);
1080
+ }
1081
+ }
1082
+ if (typeof command.frontmatter.description === "string") {
1083
+ nextFrontmatter.description = command.frontmatter.description;
1084
+ }
1085
+ const explicitGemini = command.frontmatter.gemini;
1086
+ if (explicitGemini === false) {
1087
+ nextFrontmatter.gemini = false;
1088
+ }
1089
+ else {
1090
+ const geminiConfig = isObject(explicitGemini)
1091
+ ? cloneUnknown(explicitGemini)
1092
+ : {};
1093
+ for (const [key, value] of Object.entries(command.frontmatter)) {
1094
+ if (key === "description")
1095
+ continue;
1096
+ if (ALL_PROVIDERS.includes(key))
1097
+ continue;
1098
+ if (!(key in geminiConfig)) {
1099
+ geminiConfig[key] = cloneUnknown(value);
1100
+ }
1101
+ }
1102
+ if (Object.keys(geminiConfig).length > 0) {
1103
+ nextFrontmatter.gemini = geminiConfig;
1104
+ }
1105
+ }
1106
+ const frontmatter = Object.keys(nextFrontmatter).length > 0 ? nextFrontmatter : undefined;
1107
+ return {
1108
+ ...command,
1109
+ fileName,
1110
+ body,
1111
+ frontmatter,
1112
+ content: buildCommandMarkdownForImport(frontmatter, body),
1113
+ };
1114
+ }
1115
+ function buildCommandMarkdownForImport(frontmatter, body) {
1116
+ if (!frontmatter) {
1117
+ return body.endsWith("\n") ? body : `${body}\n`;
1118
+ }
1119
+ const fm = YAML.stringify(frontmatter, { lineWidth: 0 }).trimEnd();
1120
+ return `---\n${fm}\n---\n\n${body}${body.endsWith("\n") ? "" : "\n"}`;
1121
+ }
1122
+ function inferAgentNameFromFile(fileName) {
1123
+ const base = fileName
1124
+ .replace(/\.agent\.md$/i, "")
1125
+ .replace(/\.md$/i, "")
1126
+ .trim();
1127
+ return base || "agent";
1128
+ }
1129
+ function toCanonicalCommandFileName(fileName) {
1130
+ const lower = fileName.toLowerCase();
1131
+ if (lower.endsWith(".prompt.md")) {
1132
+ return `${fileName.slice(0, -".prompt.md".length)}.md`;
1133
+ }
1134
+ if (lower.endsWith(".toml")) {
1135
+ return `${fileName.slice(0, -".toml".length)}.md`;
1136
+ }
1137
+ return fileName;
1138
+ }
1139
+ function cloneUnknown(value) {
1140
+ return JSON.parse(JSON.stringify(value));
1141
+ }
496
1142
  async function resolveAgentsToImport(options) {
497
1143
  const requestedAgents = normalizeRequestedAgents(options.requestedAgents);
498
1144
  if (requestedAgents && requestedAgents.length > 0) {
@@ -653,7 +1299,8 @@ function findMatchingLockEntry(entries, key) {
653
1299
  sameRequestedAgentsForMatch(entry.requestedAgents, key.requestedAgents) &&
654
1300
  sameStringSelectionForMatch(entry.selectedSourceCommands, key.selectedSourceCommands) &&
655
1301
  sameStringSelectionForMatch(entry.selectedSourceMcpServers, key.selectedSourceMcpServers) &&
656
- sameStringSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, { wildcardWhenRightIsUndefined: true }) &&
1302
+ sameStringSelectionForMatch(entry.selectedSourceRules, key.selectedSourceRules, { wildcardWhenRightIsUndefined: true }) &&
1303
+ sameSkillSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, key.selectedSkills, { wildcardWhenRightIsUndefined: true }) &&
657
1304
  sameStringSelectionForMatch(entry.skillsProviders, key.skillsProviders, {
658
1305
  wildcardWhenRightIsUndefined: true,
659
1306
  }));
@@ -667,6 +1314,7 @@ function findRelaxedCommandEntry(entries, key) {
667
1314
  return undefined;
668
1315
  const mixed = matches.find((entry) => entry.importedAgents.length > 0 ||
669
1316
  entry.importedMcpServers.length > 0 ||
1317
+ entry.importedRules.length > 0 ||
670
1318
  entry.importedSkills.length > 0);
671
1319
  return mixed ?? matches[0];
672
1320
  }
@@ -696,6 +1344,39 @@ function sameStringSelectionForMatch(left, right, options = {}) {
696
1344
  }
697
1345
  return normalizedLeft.every((value, index) => value === normalizedRight[index]);
698
1346
  }
1347
+ function sameSkillSelectionForMatch(left, right, selectedSkills, options = {}) {
1348
+ if (options.wildcardWhenRightIsUndefined && right === undefined) {
1349
+ return true;
1350
+ }
1351
+ const normalizedLeft = normalizeSkillSelectionsForMatch(left);
1352
+ const normalizedRight = normalizeSkillSelectionsForMatch(right);
1353
+ if (normalizedLeft.length === normalizedRight.length &&
1354
+ normalizedLeft.every((value, index) => value === normalizedRight[index])) {
1355
+ return true;
1356
+ }
1357
+ if (!selectedSkills || normalizedLeft.length !== selectedSkills.length) {
1358
+ return false;
1359
+ }
1360
+ const remainingSelectors = new Set(normalizedLeft);
1361
+ for (const skill of selectedSkills) {
1362
+ const matchedSelector = [
1363
+ normalizeSkillSelector(skill.name),
1364
+ normalizeSkillSelector(skill.sourceDirName),
1365
+ ].find((selector) => selector && remainingSelectors.has(selector));
1366
+ if (!matchedSelector) {
1367
+ return false;
1368
+ }
1369
+ remainingSelectors.delete(matchedSelector);
1370
+ }
1371
+ return remainingSelectors.size === 0;
1372
+ }
1373
+ function normalizeSkillSelectionsForMatch(value) {
1374
+ if (!Array.isArray(value) || value.length === 0)
1375
+ return [];
1376
+ return [
1377
+ ...new Set(value.map((item) => normalizeSkillSelector(item)).filter(Boolean)),
1378
+ ].sort();
1379
+ }
699
1380
  function uniqueStrings(values) {
700
1381
  return [...new Set(values)];
701
1382
  }
@@ -714,6 +1395,11 @@ function computeTrackedEntitiesForLock(options) {
714
1395
  (options.selectedSourceMcpServers?.length ?? 0) > 0) {
715
1396
  tracked.push("mcp");
716
1397
  }
1398
+ if (options.importedRules.length > 0 ||
1399
+ (options.selectedSourceRules?.length ?? 0) > 0 ||
1400
+ Object.keys(options.ruleRenameMap ?? {}).length > 0) {
1401
+ tracked.push("rule");
1402
+ }
717
1403
  if (options.importedSkills.length > 0 ||
718
1404
  (options.selectedSourceSkills?.length ?? 0) > 0 ||
719
1405
  (options.skillsProviders?.length ?? 0) > 0 ||
@@ -754,6 +1440,44 @@ function resolveMappedTargetFileName(sourceFileName, renameMap) {
754
1440
  }
755
1441
  return undefined;
756
1442
  }
1443
+ function normalizeRuleRenameMap(renameMap) {
1444
+ if (!renameMap)
1445
+ return undefined;
1446
+ const normalizedEntries = Object.entries(renameMap)
1447
+ .map(([sourceSelector, importedName]) => {
1448
+ const normalizedSourceSelector = normalizeRuleSelector(sourceSelector);
1449
+ const importedBaseName = path.basename(importedName.trim());
1450
+ if (!normalizedSourceSelector || !importedBaseName) {
1451
+ return null;
1452
+ }
1453
+ const ext = path.extname(importedBaseName);
1454
+ const stem = stripRuleFileExtension(importedBaseName);
1455
+ const normalizedTarget = `${slugify(stem) || "rule"}${ext || ".md"}`;
1456
+ return [normalizedSourceSelector, normalizedTarget];
1457
+ })
1458
+ .filter((entry) => entry !== null);
1459
+ if (normalizedEntries.length === 0)
1460
+ return undefined;
1461
+ return Object.fromEntries(normalizedEntries);
1462
+ }
1463
+ function resolveMappedTargetRuleFileName(sourceFileName, renameMap) {
1464
+ if (!renameMap)
1465
+ return undefined;
1466
+ const normalizedSourceName = normalizeRuleSelector(sourceFileName);
1467
+ for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
1468
+ if (normalizeRuleSelector(sourceSelector) !== normalizedSourceName) {
1469
+ continue;
1470
+ }
1471
+ const importedBaseName = path.basename(importedName.trim());
1472
+ if (!importedBaseName)
1473
+ return undefined;
1474
+ const ext = path.extname(importedBaseName);
1475
+ if (ext)
1476
+ return importedBaseName;
1477
+ return `${slugify(importedBaseName) || "rule"}.md`;
1478
+ }
1479
+ return undefined;
1480
+ }
757
1481
  function normalizeSkillRenameMap(renameMap) {
758
1482
  if (!renameMap)
759
1483
  return undefined;
@@ -771,20 +1495,65 @@ function normalizeSkillRenameMap(renameMap) {
771
1495
  return undefined;
772
1496
  return Object.fromEntries(normalizedEntries);
773
1497
  }
774
- function resolveMappedTargetSkillName(sourceSkillName, renameMap) {
1498
+ function resolveMappedTargetSkillName(sourceSkill, selectedSkills, renameMap) {
775
1499
  if (!renameMap)
776
1500
  return undefined;
777
- const normalizedSourceName = normalizeSkillSelector(sourceSkillName);
778
- if (!normalizedSourceName)
779
- return undefined;
780
1501
  for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
781
- if (normalizeSkillSelector(sourceSelector) !== normalizedSourceName) {
1502
+ const matchedSkill = resolveSkillSelector(selectedSkills, sourceSelector);
1503
+ if (!matchedSkill || matchedSkill.sourcePath !== sourceSkill.sourcePath) {
782
1504
  continue;
783
1505
  }
784
1506
  return slugify(path.basename(importedName.trim())) || "skill";
785
1507
  }
786
1508
  return undefined;
787
1509
  }
1510
+ function moveLegacySkillDirectoryToCanonicalIfUnchanged(options) {
1511
+ if (options.legacySkillDirName === options.canonicalSkillDirName) {
1512
+ return;
1513
+ }
1514
+ const legacySkillDir = path.join(options.paths.skillsDir, options.legacySkillDirName);
1515
+ if (!fs.existsSync(legacySkillDir) ||
1516
+ !fs.statSync(legacySkillDir).isDirectory()) {
1517
+ return;
1518
+ }
1519
+ const canonicalSkillDir = path.join(options.paths.skillsDir, options.canonicalSkillDirName);
1520
+ if (fs.existsSync(canonicalSkillDir)) {
1521
+ return;
1522
+ }
1523
+ if (!skillContentMatchesTarget(options.sourceSkill, legacySkillDir)) {
1524
+ return;
1525
+ }
1526
+ moveDirectory(legacySkillDir, canonicalSkillDir);
1527
+ }
1528
+ function removeLegacySkillDirectory(options) {
1529
+ if (options.legacySkillDirName === options.canonicalSkillDirName) {
1530
+ return;
1531
+ }
1532
+ const legacySkillDir = path.join(options.paths.skillsDir, options.legacySkillDirName);
1533
+ if (!fs.existsSync(legacySkillDir)) {
1534
+ return;
1535
+ }
1536
+ const stat = fs.lstatSync(legacySkillDir);
1537
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
1538
+ return;
1539
+ }
1540
+ fs.rmSync(legacySkillDir, { recursive: true, force: true });
1541
+ }
1542
+ function moveDirectory(sourceDir, targetDir) {
1543
+ ensureDir(path.dirname(targetDir));
1544
+ try {
1545
+ fs.renameSync(sourceDir, targetDir);
1546
+ return;
1547
+ }
1548
+ catch (error) {
1549
+ const code = error?.code;
1550
+ if (code !== "EXDEV") {
1551
+ throw error;
1552
+ }
1553
+ }
1554
+ fs.cpSync(sourceDir, targetDir, { recursive: true, force: true });
1555
+ fs.rmSync(sourceDir, { recursive: true, force: true });
1556
+ }
788
1557
  function normalizeSkillsProviders(providers) {
789
1558
  if (!providers || providers.length === 0)
790
1559
  return undefined;
@@ -799,12 +1568,13 @@ function normalizeSkillsProviders(providers) {
799
1568
  }
800
1569
  async function resolveSkillConflict(options) {
801
1570
  const targetPath = path.join(options.paths.skillsDir, options.targetSkillDirName);
802
- if (!fs.existsSync(targetPath))
1571
+ const conflictPath = resolveExistingSkillConflictPath(options, targetPath);
1572
+ if (!conflictPath)
803
1573
  return options.targetSkillDirName;
804
- if (!fs.statSync(targetPath).isDirectory()) {
805
- throw new Error(`Cannot import skill ${options.promptLabel}: ${targetPath} exists and is not a directory.`);
1574
+ if (!fs.statSync(conflictPath).isDirectory()) {
1575
+ throw new Error(`Cannot import skill ${options.promptLabel}: ${conflictPath} exists and is not a directory.`);
806
1576
  }
807
- if (skillContentMatchesTarget(options.sourceSkill, targetPath)) {
1577
+ if (skillContentMatchesTarget(options.sourceSkill, conflictPath)) {
808
1578
  return options.targetSkillDirName;
809
1579
  }
810
1580
  if (options.yes) {
@@ -851,6 +1621,17 @@ async function resolveSkillConflict(options) {
851
1621
  }
852
1622
  return options.targetSkillDirName;
853
1623
  }
1624
+ function resolveExistingSkillConflictPath(options, targetPath) {
1625
+ if (fs.existsSync(targetPath)) {
1626
+ return targetPath;
1627
+ }
1628
+ if (!options.legacySkillDirName ||
1629
+ options.legacySkillDirName === options.canonicalSkillDirName) {
1630
+ return null;
1631
+ }
1632
+ const legacyPath = path.join(options.paths.skillsDir, options.legacySkillDirName);
1633
+ return fs.existsSync(legacyPath) ? legacyPath : null;
1634
+ }
854
1635
  async function resolveAgentConflict(options) {
855
1636
  const targetPath = path.join(options.paths.agentsDir, options.targetFileName);
856
1637
  if (!fs.existsSync(targetPath))
@@ -954,6 +1735,58 @@ async function resolveCommandConflict(options) {
954
1735
  }
955
1736
  return options.targetFileName;
956
1737
  }
1738
+ async function resolveRuleConflict(options) {
1739
+ const targetPath = path.join(options.paths.rulesDir, options.targetFileName);
1740
+ if (!fs.existsSync(targetPath))
1741
+ return options.targetFileName;
1742
+ const existing = fs.readFileSync(targetPath, "utf8");
1743
+ if (existing === options.ruleContent)
1744
+ return options.targetFileName;
1745
+ if (options.yes) {
1746
+ return options.targetFileName;
1747
+ }
1748
+ if (options.nonInteractive) {
1749
+ throw new NonInteractiveConflictError(`Conflict for ${options.targetFileName}. Use --yes or run interactively.`);
1750
+ }
1751
+ const choice = await select({
1752
+ message: `Rule conflict for ${options.promptLabel}`,
1753
+ options: [
1754
+ { value: "overwrite", label: `Overwrite ${options.targetFileName}` },
1755
+ { value: "skip", label: "Skip this rule" },
1756
+ { value: "rename", label: "Rename imported rule" },
1757
+ ],
1758
+ });
1759
+ if (isCancel(choice)) {
1760
+ cancel("Operation cancelled.");
1761
+ process.exit(1);
1762
+ }
1763
+ if (choice === "skip")
1764
+ return null;
1765
+ if (choice === "rename") {
1766
+ const entered = await promptText({
1767
+ message: `New filename (without extension) for ${options.promptLabel}`,
1768
+ placeholder: options.targetFileName.replace(/\.(md|mdc)$/i, ""),
1769
+ validate(value) {
1770
+ if (!value.trim())
1771
+ return "Name is required.";
1772
+ if (/[\\/]/.test(value))
1773
+ return "Use a simple filename.";
1774
+ return undefined;
1775
+ },
1776
+ });
1777
+ if (isCancel(entered)) {
1778
+ cancel("Operation cancelled.");
1779
+ process.exit(1);
1780
+ }
1781
+ const extension = path.extname(options.targetFileName) || ".md";
1782
+ const renamedFileName = `${slugify(String(entered)) || "rule"}${extension}`;
1783
+ return resolveRuleConflict({
1784
+ ...options,
1785
+ targetFileName: renamedFileName,
1786
+ });
1787
+ }
1788
+ return options.targetFileName;
1789
+ }
957
1790
  async function resolveCommandsToImport(options) {
958
1791
  const selectors = options.selectors
959
1792
  .map((selector) => selector.trim())
@@ -1073,6 +1906,62 @@ async function resolveMcpServersToImport(options) {
1073
1906
  selectionMode,
1074
1907
  };
1075
1908
  }
1909
+ async function resolveRulesToImport(options) {
1910
+ const selectors = options.selectors
1911
+ .map((selector) => selector.trim())
1912
+ .filter(Boolean);
1913
+ if (selectors.length > 0) {
1914
+ const { selected, unmatched } = resolveRuleSelections(options.sourceRules, selectors);
1915
+ if (unmatched.length > 0) {
1916
+ throw new Error(`Rule(s) not found in source: ${unmatched.join(", ")}. Available: ${options.sourceRules.map((item) => item.fileName).join(", ")}`);
1917
+ }
1918
+ return {
1919
+ selectedRules: selected,
1920
+ selectionMode: "custom",
1921
+ };
1922
+ }
1923
+ const selectionResolution = await resolveSelectionModeWithSkip({
1924
+ entityLabel: "rules",
1925
+ selectionMode: options.selectionMode,
1926
+ promptForSelection: options.promptForRules,
1927
+ nonInteractive: options.nonInteractive,
1928
+ });
1929
+ const selectionMode = selectionResolution.selectionMode;
1930
+ if (selectionResolution.skipImport) {
1931
+ return {
1932
+ selectedRules: [],
1933
+ selectionMode: "custom",
1934
+ };
1935
+ }
1936
+ if (selectionMode === "all" ||
1937
+ !options.promptForRules ||
1938
+ options.nonInteractive) {
1939
+ return {
1940
+ selectedRules: options.sourceRules,
1941
+ selectionMode,
1942
+ };
1943
+ }
1944
+ const selected = await multiselect({
1945
+ message: withMultiselectHelp("Select rules to import"),
1946
+ options: options.sourceRules.map((item) => ({
1947
+ value: item.fileName,
1948
+ label: item.fileName,
1949
+ hint: item.name,
1950
+ })),
1951
+ initialValues: options.sourceRules.map((item) => item.fileName),
1952
+ });
1953
+ if (isCancel(selected)) {
1954
+ cancel("Operation cancelled.");
1955
+ process.exit(1);
1956
+ }
1957
+ const selectedNames = Array.isArray(selected)
1958
+ ? new Set(selected.map((value) => String(value)))
1959
+ : new Set();
1960
+ return {
1961
+ selectedRules: options.sourceRules.filter((item) => selectedNames.has(item.fileName)),
1962
+ selectionMode,
1963
+ };
1964
+ }
1076
1965
  async function resolveSkillsToImport(options) {
1077
1966
  const selectors = options.selectors
1078
1967
  .map((item) => item.trim())
@@ -1084,6 +1973,7 @@ async function resolveSkillsToImport(options) {
1084
1973
  }
1085
1974
  return {
1086
1975
  selectedSkills: selected,
1976
+ selectedSourceSkills: selectors,
1087
1977
  selectionMode: "custom",
1088
1978
  };
1089
1979
  }
@@ -1097,6 +1987,7 @@ async function resolveSkillsToImport(options) {
1097
1987
  if (selectionResolution.skipImport) {
1098
1988
  return {
1099
1989
  selectedSkills: [],
1990
+ selectedSourceSkills: [],
1100
1991
  selectionMode: "custom",
1101
1992
  };
1102
1993
  }
@@ -1105,6 +1996,7 @@ async function resolveSkillsToImport(options) {
1105
1996
  options.nonInteractive) {
1106
1997
  return {
1107
1998
  selectedSkills: options.sourceSkills,
1999
+ selectedSourceSkills: options.sourceSkills.map((skill) => skill.name),
1108
2000
  selectionMode,
1109
2001
  };
1110
2002
  }
@@ -1125,6 +2017,9 @@ async function resolveSkillsToImport(options) {
1125
2017
  : new Set();
1126
2018
  return {
1127
2019
  selectedSkills: options.sourceSkills.filter((skill) => selectedNames.has(skill.name)),
2020
+ selectedSourceSkills: options.sourceSkills
2021
+ .filter((skill) => selectedNames.has(skill.name))
2022
+ .map((skill) => skill.name),
1128
2023
  selectionMode,
1129
2024
  };
1130
2025
  }