agentloom 0.1.6 → 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.
package/dist/core/copy.js CHANGED
@@ -249,13 +249,6 @@ export function formatUsageError(input) {
249
249
  return lines.join("\n");
250
250
  }
251
251
  export function formatUnknownCommandError(command) {
252
- if (command === "skills") {
253
- return formatUsageError({
254
- issue: 'Command "skills" was removed.',
255
- usage: "agentloom skill <add|list|delete|find|update|sync> [options]",
256
- example: "agentloom skill find typescript",
257
- });
258
- }
259
252
  return formatUsageError({
260
253
  issue: `Unknown command "${command}".`,
261
254
  usage: "agentloom --help",
@@ -7,12 +7,12 @@ import YAML from "yaml";
7
7
  import { buildAgentMarkdown, parseAgentsDir, targetFileNameForAgent, } from "./agents.js";
8
8
  import { normalizeCommandArgumentsForCanonical, normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
9
9
  import { normalizeRuleSelector, parseRulesDir, resolveRuleSelections, stripRuleFileExtension, } from "./rules.js";
10
- import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
10
+ import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelector, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
11
11
  import { ensureDir, hashContent, isObject, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
12
12
  import { ALL_PROVIDERS } from "../types.js";
13
13
  import { readLockfile, upsertLockEntry, writeLockfile } from "./lockfile.js";
14
14
  import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
15
- import { discoverSourceAgentsDir, discoverSourceCommandsDir, discoverSourceCommandsDirs, discoverSourceMcpPath, discoverSourceRulesDir, discoverSourceSkillsDir, prepareSource, } from "./sources.js";
15
+ import { discoverPluginSourceRoots, discoverSourceAgentsDirs, discoverSourceCommandsDirs, discoverSourceMcpPaths, discoverSourceRulesDirs, discoverSourceSkillsDirs, prepareSource, } from "./sources.js";
16
16
  import { isProviderEntityFileName } from "./provider-entity-validation.js";
17
17
  export class NonInteractiveConflictError extends Error {
18
18
  constructor(message) {
@@ -51,67 +51,77 @@ export async function importSource(options) {
51
51
  ? `${options.source} (subdir: ${options.subdir})`
52
52
  : options.source;
53
53
  try {
54
- const sourceAgentsDir = shouldImportAgents
55
- ? discoverSourceAgentsDir(prepared.importRoot)
56
- : null;
54
+ const pluginSourceRoots = discoverPluginSourceRoots(prepared.importRoot);
55
+ const sourceAgentsDirs = shouldImportAgents
56
+ ? discoverSourceAgentsDirs(prepared.importRoot)
57
+ : [];
57
58
  const sourceCommandsDirs = shouldImportCommands
58
59
  ? discoverSourceCommandsDirs(prepared.importRoot)
59
60
  : [];
60
- const sourceCommandsDir = sourceCommandsDirs[0] ??
61
- (shouldImportCommands
62
- ? discoverSourceCommandsDir(prepared.importRoot)
63
- : null);
64
- const sourceMcpPath = shouldImportMcp
65
- ? discoverSourceMcpPath(prepared.importRoot)
66
- : null;
67
- const sourceRulesDir = shouldImportRules
68
- ? discoverSourceRulesDir(prepared.importRoot)
69
- : null;
70
- const sourceSkillsDir = shouldImportSkills
71
- ? discoverSourceSkillsDir(prepared.importRoot)
72
- : null;
73
- const sourceAgents = sourceAgentsDir
74
- ? parseSourceAgentsForImport(sourceAgentsDir)
61
+ const sourceMcpPaths = shouldImportMcp
62
+ ? discoverSourceMcpPaths(prepared.importRoot)
63
+ : [];
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)
75
72
  : [];
76
73
  const sourceCommands = sourceCommandsDirs.length > 0
77
- ? parseSourceCommandsForImport(sourceCommandsDirs)
74
+ ? parseSourceCommandsForImport(sourceCommandsDirs, pluginSourceRoots)
78
75
  : [];
79
- const sourceMcp = sourceMcpPath
80
- ? normalizeMcp(readJsonIfExists(sourceMcpPath))
76
+ const sourceMcp = sourceMcpPaths.length > 0
77
+ ? parseSourceMcpForImport(sourceMcpPaths, pluginSourceRoots)
81
78
  : null;
82
- const sourceRules = sourceRulesDir ? parseRulesDir(sourceRulesDir) : [];
83
- 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;
84
90
  const hasExplicitCommandSelection = (options.commandSelectors?.length ?? 0) > 0;
85
91
  const isAggregateImport = shouldImportAgents &&
86
92
  shouldImportCommands &&
87
93
  shouldImportMcp &&
88
94
  (options.importRules === undefined || shouldImportRules) &&
89
95
  shouldImportSkills;
90
- if (shouldImportAgents && requireAgents && !sourceAgentsDir) {
91
- throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/, .agents/agents/, or .github/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).`);
92
98
  }
93
99
  if (shouldImportAgents && requireAgents && sourceAgents.length === 0) {
94
100
  throw new Error(`No agent files found in ${sourceAgentsDir}.`);
95
101
  }
96
- if (shouldImportCommands && options.requireCommands && !sourceCommandsDir) {
97
- throw new Error(`No source commands directory found under ${prepared.importRoot} (expected .agents/commands/, commands/, prompts/, .gemini/commands/, or .github/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).`);
98
106
  }
99
107
  if (shouldImportCommands &&
100
108
  options.requireCommands &&
101
109
  sourceCommands.length === 0) {
102
110
  throw new Error(`No command files found in ${sourceCommandsDir}.`);
103
111
  }
104
- if (shouldImportMcp && options.requireMcp && !sourceMcpPath) {
105
- 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).`);
106
114
  }
107
- if (shouldImportRules && requireRules && !sourceRulesDir) {
108
- throw new Error(`No source rules directory found under ${prepared.importRoot} (expected .agents/rules/ or rules/).`);
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).`);
109
117
  }
110
118
  if (shouldImportRules && requireRules && sourceRules.length === 0) {
111
119
  throw new Error(`No rule files found in ${sourceRulesDir}.`);
112
120
  }
113
- if (shouldImportSkills && options.requireSkills && !sourceSkillsDir) {
114
- 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).`);
115
125
  }
116
126
  if (shouldImportSkills &&
117
127
  options.requireSkills &&
@@ -124,7 +134,7 @@ export async function importSource(options) {
124
134
  sourceRules.length === 0 &&
125
135
  sourceSkills.length === 0 &&
126
136
  Object.keys(sourceMcp?.mcpServers ?? {}).length === 0) {
127
- 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/, 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.`);
128
138
  }
129
139
  const shouldResolveAgents = shouldImportAgents &&
130
140
  (sourceAgents.length > 0 ||
@@ -198,8 +208,8 @@ export async function importSource(options) {
198
208
  selectionMode: options.selectionMode,
199
209
  });
200
210
  selectedSkills = skillSelection.selectedSkills;
211
+ selectedSourceSkills = skillSelection.selectedSourceSkills;
201
212
  skillSelectionMode = skillSelection.selectionMode;
202
- selectedSourceSkills = selectedSkills.map((skill) => skill.name);
203
213
  }
204
214
  const importedAgents = [];
205
215
  if (shouldImportAgents && selection.selectedAgents.length > 0) {
@@ -331,10 +341,16 @@ export async function importSource(options) {
331
341
  ensureDir(options.paths.skillsDir);
332
342
  }
333
343
  for (const [index, sourceSkill] of selectedSkills.entries()) {
334
- let targetSkillDirName = slugify(sourceSkill.name) || "skill";
335
- 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);
336
348
  if (mappedTargetSkillDirName) {
337
- targetSkillDirName = mappedTargetSkillDirName;
349
+ targetSkillDirName =
350
+ mappedTargetSkillDirName === legacySkillDirName &&
351
+ legacySkillDirName !== canonicalSkillDirName
352
+ ? canonicalSkillDirName
353
+ : mappedTargetSkillDirName;
338
354
  }
339
355
  else if (options.rename &&
340
356
  selectedSkills.length === 1 &&
@@ -347,6 +363,10 @@ export async function importSource(options) {
347
363
  const resolvedSkillDirName = await resolveSkillConflict({
348
364
  sourceSkill,
349
365
  targetSkillDirName,
366
+ legacySkillDirName: targetSkillDirName === canonicalSkillDirName
367
+ ? legacySkillDirName
368
+ : undefined,
369
+ canonicalSkillDirName,
350
370
  paths: options.paths,
351
371
  yes: !!options.yes,
352
372
  nonInteractive: !!options.nonInteractive,
@@ -355,10 +375,25 @@ export async function importSource(options) {
355
375
  if (!resolvedSkillDirName)
356
376
  continue;
357
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
+ }
358
386
  if (!skillContentMatchesTarget(sourceSkill, targetSkillDir)) {
359
387
  fs.rmSync(targetSkillDir, { recursive: true, force: true });
360
388
  copySkillArtifacts(sourceSkill, targetSkillDir);
361
389
  }
390
+ if (resolvedSkillDirName === canonicalSkillDirName) {
391
+ removeLegacySkillDirectory({
392
+ legacySkillDirName,
393
+ canonicalSkillDirName,
394
+ paths: options.paths,
395
+ });
396
+ }
362
397
  importedSkills.push(resolvedSkillDirName);
363
398
  telemetrySkills.push({
364
399
  name: sourceSkill.name,
@@ -429,6 +464,7 @@ export async function importSource(options) {
429
464
  selectedSourceMcpServers: selectedSourceMcpServersForLock,
430
465
  selectedSourceRules: selectedSourceRulesForLock,
431
466
  selectedSourceSkills: selectedSourceSkillsForLock,
467
+ selectedSkills,
432
468
  skillsProviders: skillsProvidersForLock,
433
469
  });
434
470
  const shouldMergeCommandOnlyEntry = isCommandOnlyImport &&
@@ -605,17 +641,164 @@ export async function importSource(options) {
605
641
  prepared.cleanup();
606
642
  }
607
643
  }
608
- function parseSourceAgentsForImport(sourceAgentsDir) {
609
- if (isGitHubAgentsDir(sourceAgentsDir)) {
610
- return parseGitHubAgentsDirForImport(sourceAgentsDir);
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
+ });
611
784
  }
612
- return parseAgentsDir(sourceAgentsDir);
613
785
  }
614
- function parseSourceCommandsForImport(sourceCommandsDir) {
615
- const sourceCommandsDirs = Array.isArray(sourceCommandsDir)
616
- ? sourceCommandsDir
617
- : [sourceCommandsDir];
618
- return mergeCanonicalCommandFiles(sourceCommandsDirs.flatMap((dirPath) => parseSourceCommandsFromDir(dirPath)));
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;
619
802
  }
620
803
  function parseSourceCommandsFromDir(sourceCommandsDir) {
621
804
  if (isGeminiCommandsDir(sourceCommandsDir)) {
@@ -1117,7 +1300,7 @@ function findMatchingLockEntry(entries, key) {
1117
1300
  sameStringSelectionForMatch(entry.selectedSourceCommands, key.selectedSourceCommands) &&
1118
1301
  sameStringSelectionForMatch(entry.selectedSourceMcpServers, key.selectedSourceMcpServers) &&
1119
1302
  sameStringSelectionForMatch(entry.selectedSourceRules, key.selectedSourceRules, { wildcardWhenRightIsUndefined: true }) &&
1120
- sameStringSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, { wildcardWhenRightIsUndefined: true }) &&
1303
+ sameSkillSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, key.selectedSkills, { wildcardWhenRightIsUndefined: true }) &&
1121
1304
  sameStringSelectionForMatch(entry.skillsProviders, key.skillsProviders, {
1122
1305
  wildcardWhenRightIsUndefined: true,
1123
1306
  }));
@@ -1161,6 +1344,39 @@ function sameStringSelectionForMatch(left, right, options = {}) {
1161
1344
  }
1162
1345
  return normalizedLeft.every((value, index) => value === normalizedRight[index]);
1163
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
+ }
1164
1380
  function uniqueStrings(values) {
1165
1381
  return [...new Set(values)];
1166
1382
  }
@@ -1279,20 +1495,65 @@ function normalizeSkillRenameMap(renameMap) {
1279
1495
  return undefined;
1280
1496
  return Object.fromEntries(normalizedEntries);
1281
1497
  }
1282
- function resolveMappedTargetSkillName(sourceSkillName, renameMap) {
1498
+ function resolveMappedTargetSkillName(sourceSkill, selectedSkills, renameMap) {
1283
1499
  if (!renameMap)
1284
1500
  return undefined;
1285
- const normalizedSourceName = normalizeSkillSelector(sourceSkillName);
1286
- if (!normalizedSourceName)
1287
- return undefined;
1288
1501
  for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
1289
- if (normalizeSkillSelector(sourceSelector) !== normalizedSourceName) {
1502
+ const matchedSkill = resolveSkillSelector(selectedSkills, sourceSelector);
1503
+ if (!matchedSkill || matchedSkill.sourcePath !== sourceSkill.sourcePath) {
1290
1504
  continue;
1291
1505
  }
1292
1506
  return slugify(path.basename(importedName.trim())) || "skill";
1293
1507
  }
1294
1508
  return undefined;
1295
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
+ }
1296
1557
  function normalizeSkillsProviders(providers) {
1297
1558
  if (!providers || providers.length === 0)
1298
1559
  return undefined;
@@ -1307,12 +1568,13 @@ function normalizeSkillsProviders(providers) {
1307
1568
  }
1308
1569
  async function resolveSkillConflict(options) {
1309
1570
  const targetPath = path.join(options.paths.skillsDir, options.targetSkillDirName);
1310
- if (!fs.existsSync(targetPath))
1571
+ const conflictPath = resolveExistingSkillConflictPath(options, targetPath);
1572
+ if (!conflictPath)
1311
1573
  return options.targetSkillDirName;
1312
- if (!fs.statSync(targetPath).isDirectory()) {
1313
- 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.`);
1314
1576
  }
1315
- if (skillContentMatchesTarget(options.sourceSkill, targetPath)) {
1577
+ if (skillContentMatchesTarget(options.sourceSkill, conflictPath)) {
1316
1578
  return options.targetSkillDirName;
1317
1579
  }
1318
1580
  if (options.yes) {
@@ -1359,6 +1621,17 @@ async function resolveSkillConflict(options) {
1359
1621
  }
1360
1622
  return options.targetSkillDirName;
1361
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
+ }
1362
1635
  async function resolveAgentConflict(options) {
1363
1636
  const targetPath = path.join(options.paths.agentsDir, options.targetFileName);
1364
1637
  if (!fs.existsSync(targetPath))
@@ -1700,6 +1973,7 @@ async function resolveSkillsToImport(options) {
1700
1973
  }
1701
1974
  return {
1702
1975
  selectedSkills: selected,
1976
+ selectedSourceSkills: selectors,
1703
1977
  selectionMode: "custom",
1704
1978
  };
1705
1979
  }
@@ -1713,6 +1987,7 @@ async function resolveSkillsToImport(options) {
1713
1987
  if (selectionResolution.skipImport) {
1714
1988
  return {
1715
1989
  selectedSkills: [],
1990
+ selectedSourceSkills: [],
1716
1991
  selectionMode: "custom",
1717
1992
  };
1718
1993
  }
@@ -1721,6 +1996,7 @@ async function resolveSkillsToImport(options) {
1721
1996
  options.nonInteractive) {
1722
1997
  return {
1723
1998
  selectedSkills: options.sourceSkills,
1999
+ selectedSourceSkills: options.sourceSkills.map((skill) => skill.name),
1724
2000
  selectionMode,
1725
2001
  };
1726
2002
  }
@@ -1741,6 +2017,9 @@ async function resolveSkillsToImport(options) {
1741
2017
  : new Set();
1742
2018
  return {
1743
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),
1744
2023
  selectionMode,
1745
2024
  };
1746
2025
  }
@@ -14,6 +14,13 @@ const ENTITY_NOUNS = new Set([
14
14
  "rule",
15
15
  "skill",
16
16
  ]);
17
+ const ENTITY_NOUN_ALIASES = {
18
+ agents: "agent",
19
+ commands: "command",
20
+ mcps: "mcp",
21
+ rules: "rule",
22
+ skills: "skill",
23
+ };
17
24
  const ENTITY_VERBS = new Set([
18
25
  "add",
19
26
  "list",
@@ -24,9 +31,10 @@ const ENTITY_VERBS = new Set([
24
31
  ]);
25
32
  const MCP_SERVER_VERBS = new Set(["add", "list", "delete"]);
26
33
  export function parseCommandRoute(argv) {
27
- const root = argv[0]?.trim().toLowerCase();
28
- if (!root)
34
+ const rawRoot = argv[0]?.trim().toLowerCase();
35
+ if (!rawRoot)
29
36
  return null;
37
+ const root = ENTITY_NOUN_ALIASES[rawRoot] ?? rawRoot;
30
38
  if (AGGREGATE_VERBS.has(root)) {
31
39
  return {
32
40
  mode: "aggregate",
@@ -1,6 +1,8 @@
1
1
  import type { Provider, ScopePaths } from "../types.js";
2
2
  export interface CanonicalSkill {
3
3
  name: string;
4
+ aliases: string[];
5
+ sourceDirName: string;
4
6
  sourcePath: string;
5
7
  skillPath: string;
6
8
  layout: "nested" | "root";
@@ -8,6 +10,7 @@ export interface CanonicalSkill {
8
10
  export declare const ROOT_SKILL_ARTIFACT_DIRS: readonly ["references", "assets", "scripts", "templates", "examples"];
9
11
  export declare function parseSkillsDir(skillsDir: string): CanonicalSkill[];
10
12
  export declare function normalizeSkillSelector(value: string): string;
13
+ export declare function resolveSkillSelector(skills: CanonicalSkill[], selector: string): CanonicalSkill | null;
11
14
  export declare function resolveSkillSelections(skills: CanonicalSkill[], selectors: string[]): {
12
15
  selected: CanonicalSkill[];
13
16
  unmatched: string[];
@@ -22,8 +22,12 @@ export function parseSkillsDir(skillsDir) {
22
22
  const skillFile = path.join(skillDir, "SKILL.md");
23
23
  if (!fs.existsSync(skillFile))
24
24
  continue;
25
+ const raw = fs.readFileSync(skillFile, "utf8");
26
+ const canonicalName = extractSkillName(raw) || entry.name;
25
27
  skills.push({
26
- name: entry.name,
28
+ name: canonicalName,
29
+ aliases: buildSkillAliases(canonicalName, entry.name),
30
+ sourceDirName: entry.name,
27
31
  sourcePath: skillDir,
28
32
  skillPath: skillFile,
29
33
  layout: "nested",
@@ -37,9 +41,12 @@ export function parseSkillsDir(skillsDir) {
37
41
  return [];
38
42
  }
39
43
  const raw = fs.readFileSync(rootSkillFile, "utf8");
44
+ const canonicalName = extractSkillName(raw) || path.basename(skillsDir);
40
45
  return [
41
46
  {
42
- name: extractSkillName(raw) || path.basename(skillsDir),
47
+ name: canonicalName,
48
+ aliases: buildSkillAliases(canonicalName, path.basename(skillsDir)),
49
+ sourceDirName: path.basename(skillsDir),
43
50
  sourcePath: skillsDir,
44
51
  skillPath: rootSkillFile,
45
52
  layout: "root",
@@ -61,6 +68,16 @@ function extractSkillName(raw) {
61
68
  export function normalizeSkillSelector(value) {
62
69
  return slugify(value.trim().replace(/\/+$/, "")).toLowerCase();
63
70
  }
71
+ export function resolveSkillSelector(skills, selector) {
72
+ const normalizedSelector = normalizeSkillSelector(selector);
73
+ if (!normalizedSelector)
74
+ return null;
75
+ const canonicalMatch = skills.find((skill) => normalizeSkillSelector(skill.name) === normalizedSelector) ?? null;
76
+ if (canonicalMatch) {
77
+ return canonicalMatch;
78
+ }
79
+ return (skills.find((skill) => normalizeSkillSelector(skill.sourceDirName) === normalizedSelector) ?? null);
80
+ }
64
81
  export function resolveSkillSelections(skills, selectors) {
65
82
  const normalizedSelectors = selectors
66
83
  .map((item) => item.trim())
@@ -70,14 +87,12 @@ export function resolveSkillSelections(skills, selectors) {
70
87
  const selectedMap = new Map();
71
88
  const unmatched = [];
72
89
  for (const selector of normalizedSelectors) {
73
- const matches = skills.filter((skill) => normalizeSkillSelector(skill.name) === selector);
74
- if (matches.length === 0) {
90
+ const match = resolveSkillSelector(skills, selector);
91
+ if (!match) {
75
92
  unmatched.push(selector);
76
93
  continue;
77
94
  }
78
- for (const match of matches) {
79
- selectedMap.set(match.name, match);
80
- }
95
+ selectedMap.set(match.name, match);
81
96
  }
82
97
  return {
83
98
  selected: [...selectedMap.values()],
@@ -195,9 +210,7 @@ function enforceProviderSkillsSymlink(options) {
195
210
  function migrateProviderSkillsIntoCanonical(options) {
196
211
  const providerSkills = parseSkillsDir(options.providerSkillsDir);
197
212
  for (const skill of providerSkills) {
198
- const targetSkillDirName = skill.layout === "nested"
199
- ? path.basename(skill.sourcePath)
200
- : slugify(skill.name) || "skill";
213
+ const targetSkillDirName = slugify(skill.name) || "skill";
201
214
  const targetSkillDir = path.join(options.canonicalSkillsDir, targetSkillDirName);
202
215
  if (fs.existsSync(targetSkillDir)) {
203
216
  const sameContent = skill.layout === "nested"
@@ -218,6 +231,9 @@ function migrateProviderSkillsIntoCanonical(options) {
218
231
  copyRootSkillArtifacts(skill.sourcePath, targetSkillDir);
219
232
  }
220
233
  }
234
+ function buildSkillAliases(name, sourceDirName) {
235
+ return [...new Set([name, sourceDirName].filter(Boolean))];
236
+ }
221
237
  function moveDirectory(sourceDir, targetDir) {
222
238
  ensureDir(path.dirname(targetDir));
223
239
  try {
@@ -16,9 +16,14 @@ export declare function prepareSource(options: {
16
16
  ref?: string;
17
17
  subdir?: string;
18
18
  }): PreparedSource;
19
+ export declare function discoverPluginSourceRoots(importRoot: string): string[];
19
20
  export declare function discoverSourceAgentsDir(importRoot: string): string | null;
21
+ export declare function discoverSourceAgentsDirs(importRoot: string): string[];
20
22
  export declare function discoverSourceMcpPath(importRoot: string): string | null;
23
+ export declare function discoverSourceMcpPaths(importRoot: string): string[];
21
24
  export declare function discoverSourceCommandsDirs(importRoot: string): string[];
22
25
  export declare function discoverSourceCommandsDir(importRoot: string): string | null;
23
26
  export declare function discoverSourceSkillsDir(importRoot: string): string | null;
27
+ export declare function discoverSourceSkillsDirs(importRoot: string): string[];
24
28
  export declare function discoverSourceRulesDir(importRoot: string): string | null;
29
+ export declare function discoverSourceRulesDirs(importRoot: string): string[];
@@ -56,31 +56,72 @@ export function prepareSource(options) {
56
56
  },
57
57
  };
58
58
  }
59
- export function discoverSourceAgentsDir(importRoot) {
60
- const direct = path.join(importRoot, "agents");
61
- if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
62
- return direct;
59
+ export function discoverPluginSourceRoots(importRoot) {
60
+ const marketplacePath = path.join(importRoot, ".claude-plugin", "marketplace.json");
61
+ if (!fs.existsSync(marketplacePath) ||
62
+ !fs.statSync(marketplacePath).isFile()) {
63
+ return [];
63
64
  }
64
- const nested = path.join(importRoot, ".agents", "agents");
65
- if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
66
- return nested;
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
67
68
  }
68
- const githubAgents = path.join(importRoot, ".github", "agents");
69
- if (fs.existsSync(githubAgents) && fs.statSync(githubAgents).isDirectory()) {
70
- return githubAgents;
69
+ catch {
70
+ return [];
71
+ }
72
+ const plugins = Array.isArray(parsed?.plugins)
73
+ ? parsed.plugins
74
+ : [];
75
+ const discoveredRoots = [];
76
+ for (const plugin of plugins) {
77
+ if (!plugin ||
78
+ typeof plugin !== "object" ||
79
+ Array.isArray(plugin) ||
80
+ typeof plugin.source !== "string") {
81
+ continue;
82
+ }
83
+ const source = plugin.source.trim();
84
+ if (!source)
85
+ continue;
86
+ const pluginRoot = path.resolve(importRoot, source);
87
+ if (!isPathWithinRoot(importRoot, pluginRoot)) {
88
+ continue;
89
+ }
90
+ if (!fs.existsSync(pluginRoot) || !fs.statSync(pluginRoot).isDirectory()) {
91
+ continue;
92
+ }
93
+ discoveredRoots.push(pluginRoot);
71
94
  }
72
- return null;
95
+ return dedupePaths(discoveredRoots);
96
+ }
97
+ export function discoverSourceAgentsDir(importRoot) {
98
+ return discoverSourceAgentsDirs(importRoot)[0] ?? null;
99
+ }
100
+ export function discoverSourceAgentsDirs(importRoot) {
101
+ const direct = discoverSourceAgentsDirsForRoot(importRoot);
102
+ if (direct.length > 0) {
103
+ return direct;
104
+ }
105
+ return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceAgentsDirsForRoot(pluginRoot)));
73
106
  }
74
107
  export function discoverSourceMcpPath(importRoot) {
75
- const nested = path.join(importRoot, ".agents", "mcp.json");
76
- if (fs.existsSync(nested))
77
- return nested;
78
- const direct = path.join(importRoot, "mcp.json");
79
- if (fs.existsSync(direct))
108
+ return discoverSourceMcpPaths(importRoot)[0] ?? null;
109
+ }
110
+ export function discoverSourceMcpPaths(importRoot) {
111
+ const direct = discoverSourceMcpPathsForRoot(importRoot);
112
+ if (direct.length > 0) {
80
113
  return direct;
81
- return null;
114
+ }
115
+ return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceMcpPathsForRoot(pluginRoot)));
82
116
  }
83
117
  export function discoverSourceCommandsDirs(importRoot) {
118
+ const direct = discoverSourceCommandsDirsForRoot(importRoot);
119
+ if (direct.length > 0) {
120
+ return direct;
121
+ }
122
+ return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceCommandsDirsForRoot(pluginRoot)));
123
+ }
124
+ function discoverSourceCommandsDirsForRoot(importRoot) {
84
125
  const nested = path.join(importRoot, ".agents", "commands");
85
126
  if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
86
127
  return [nested];
@@ -110,30 +151,84 @@ export function discoverSourceCommandsDir(importRoot) {
110
151
  return discoverSourceCommandsDirs(importRoot)[0] ?? null;
111
152
  }
112
153
  export function discoverSourceSkillsDir(importRoot) {
154
+ return discoverSourceSkillsDirs(importRoot)[0] ?? null;
155
+ }
156
+ export function discoverSourceSkillsDirs(importRoot) {
157
+ const direct = discoverSourceSkillsDirsForRoot(importRoot);
158
+ if (direct.length > 0) {
159
+ return direct;
160
+ }
161
+ return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceSkillsDirsForRoot(pluginRoot)));
162
+ }
163
+ export function discoverSourceRulesDir(importRoot) {
164
+ return discoverSourceRulesDirs(importRoot)[0] ?? null;
165
+ }
166
+ export function discoverSourceRulesDirs(importRoot) {
167
+ const direct = discoverSourceRulesDirsForRoot(importRoot);
168
+ if (direct.length > 0) {
169
+ return direct;
170
+ }
171
+ return dedupePaths(discoverPluginSourceRoots(importRoot).flatMap((pluginRoot) => discoverSourceRulesDirsForRoot(pluginRoot)));
172
+ }
173
+ function discoverSourceAgentsDirsForRoot(importRoot) {
174
+ const direct = path.join(importRoot, "agents");
175
+ if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
176
+ return [direct];
177
+ }
178
+ const nested = path.join(importRoot, ".agents", "agents");
179
+ if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
180
+ return [nested];
181
+ }
182
+ const githubAgents = path.join(importRoot, ".github", "agents");
183
+ if (fs.existsSync(githubAgents) && fs.statSync(githubAgents).isDirectory()) {
184
+ return [githubAgents];
185
+ }
186
+ return [];
187
+ }
188
+ function discoverSourceMcpPathsForRoot(importRoot) {
189
+ const nested = path.join(importRoot, ".agents", "mcp.json");
190
+ if (fs.existsSync(nested) && fs.statSync(nested).isFile()) {
191
+ return [nested];
192
+ }
193
+ const direct = path.join(importRoot, "mcp.json");
194
+ if (fs.existsSync(direct) && fs.statSync(direct).isFile()) {
195
+ return [direct];
196
+ }
197
+ return [];
198
+ }
199
+ function discoverSourceSkillsDirsForRoot(importRoot) {
113
200
  const nested = path.join(importRoot, ".agents", "skills");
114
201
  if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
115
- return nested;
202
+ return [nested];
116
203
  }
117
204
  const direct = path.join(importRoot, "skills");
118
205
  if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
119
- return direct;
206
+ return [direct];
120
207
  }
121
208
  const rootSkill = path.join(importRoot, "SKILL.md");
122
209
  if (fs.existsSync(rootSkill) && fs.statSync(rootSkill).isFile()) {
123
- return importRoot;
210
+ return [importRoot];
124
211
  }
125
- return null;
212
+ return [];
126
213
  }
127
- export function discoverSourceRulesDir(importRoot) {
214
+ function discoverSourceRulesDirsForRoot(importRoot) {
128
215
  const nested = path.join(importRoot, ".agents", "rules");
129
216
  if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
130
- return nested;
217
+ return [nested];
131
218
  }
132
219
  const direct = path.join(importRoot, "rules");
133
220
  if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
134
- return direct;
221
+ return [direct];
135
222
  }
136
- return null;
223
+ return [];
224
+ }
225
+ function dedupePaths(paths) {
226
+ return [...new Set(paths)];
227
+ }
228
+ function isPathWithinRoot(rootPath, targetPath) {
229
+ const relative = path.relative(rootPath, targetPath);
230
+ return (relative === "" ||
231
+ (!relative.startsWith("..") && !path.isAbsolute(relative)));
137
232
  }
138
233
  function resolveImportRoot(rootPath, subdir) {
139
234
  if (!subdir)
@@ -24,6 +24,7 @@ export async function syncFromCanonical(options) {
24
24
  const agents = parseAgentsDir(options.paths.agentsDir);
25
25
  const commands = parseCommandsDir(options.paths.commandsDir);
26
26
  const rules = parseRulesDir(options.paths.rulesDir);
27
+ const managedInstructionRules = rules.filter((rule) => rule.frontmatter.alwaysApply !== false);
27
28
  const mcp = readCanonicalMcp(options.paths);
28
29
  const manifest = readManifest(options.paths);
29
30
  const effectiveManifest = {
@@ -100,7 +101,7 @@ export async function syncFromCanonical(options) {
100
101
  syncManagedRuleInstructions({
101
102
  paths: options.paths,
102
103
  providers,
103
- rules,
104
+ rules: managedInstructionRules,
104
105
  generated: generatedRules,
105
106
  updated: updatedRuleInstructionFiles,
106
107
  retained: retainedRuleInstructionFiles,
@@ -507,7 +508,18 @@ function syncManagedRuleInstructions(options) {
507
508
  }
508
509
  const cleanupTargets = new Set(getRuleInstructionPaths(options.paths, cleanupProviders));
509
510
  for (const targetPath of cleanupTargets) {
510
- const existing = readTextIfExists(targetPath) ?? "";
511
+ if (!fs.existsSync(targetPath)) {
512
+ continue;
513
+ }
514
+ const stat = fs.lstatSync(targetPath);
515
+ if (!stat.isFile() || stat.isSymbolicLink()) {
516
+ options.retained.add(targetPath);
517
+ continue;
518
+ }
519
+ const existing = readTextIfExists(targetPath);
520
+ if (existing === null) {
521
+ continue;
522
+ }
511
523
  const next = upsertManagedRuleBlocks(existing, activeTargets.has(targetPath) ? options.rules : []);
512
524
  const shouldTrackGenerated = activeTargets.has(targetPath) &&
513
525
  options.rules.length > 0 &&
@@ -531,7 +543,6 @@ function syncManagedRuleInstructions(options) {
531
543
  }
532
544
  }
533
545
  else {
534
- ensureDir(path.dirname(targetPath));
535
546
  writeTextAtomic(targetPath, next);
536
547
  }
537
548
  }
@@ -550,7 +561,8 @@ function syncCopilotDiscoverySettings(options) {
550
561
  appendPathSetting(settings, "chat.agentFilesLocations", path.join(options.paths.homeDir, ".copilot", "agents"));
551
562
  }
552
563
  if (options.includeInstructionLocations) {
553
- appendPathSetting(settings, "chat.instructionsFilesLocations", path.join(options.paths.homeDir, ".copilot", "copilot-instructions.md"));
564
+ const instructionPath = path.join(options.paths.homeDir, ".copilot", "copilot-instructions.md");
565
+ setPathSettingEnabled(settings, "chat.instructionsFilesLocations", instructionPath, fs.existsSync(instructionPath));
554
566
  }
555
567
  maybeWriteJson(settingsPath, settings, options.dryRun);
556
568
  }
@@ -724,6 +736,22 @@ function appendPathSetting(settings, key, settingPath) {
724
736
  }
725
737
  settings[key] = existing;
726
738
  }
739
+ function setPathSettingEnabled(settings, key, settingPath, enabled) {
740
+ if (enabled) {
741
+ appendPathSetting(settings, key, settingPath);
742
+ return;
743
+ }
744
+ const existing = Array.isArray(settings[key])
745
+ ? settings[key].filter((value) => typeof value === "string" &&
746
+ value.trim() !== "" &&
747
+ value !== settingPath)
748
+ : [];
749
+ if (existing.length === 0) {
750
+ delete settings[key];
751
+ return;
752
+ }
753
+ settings[key] = existing;
754
+ }
727
755
  function syncCodex(options) {
728
756
  const codexDir = getCodexRootDir(options.paths);
729
757
  const codexConfigPath = getCodexConfigPath(options.paths);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentloom",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
5
5
  "type": "module",
6
6
  "bin": {