agentloom 0.1.4 → 0.1.6

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.
@@ -5,11 +5,12 @@ import TOML from "@iarna/toml";
5
5
  import YAML from "yaml";
6
6
  import { ALL_PROVIDERS } from "../types.js";
7
7
  import { getProviderConfig, isProviderEnabled, parseAgentsDir, } from "../core/agents.js";
8
- import { parseCommandsDir } from "../core/commands.js";
9
- import { ensureDir, isObject, readJsonIfExists, relativePosix, removeFileIfExists, slugify, toPosixPath, writeJsonAtomic, writeTextAtomic, } from "../core/fs.js";
8
+ import { parseCommandsDir, renderCommandForProvider, } from "../core/commands.js";
9
+ import { parseRulesDir, renderRuleForCursor, upsertManagedRuleBlocks, } from "../core/rules.js";
10
+ import { ensureDir, isObject, readJsonIfExists, readTextIfExists, relativePosix, removeFileIfExists, slugify, toPosixPath, writeJsonAtomic, writeTextAtomic, } from "../core/fs.js";
10
11
  import { readManifest, writeManifest } from "../core/manifest.js";
11
12
  import { readCanonicalMcp, resolveMcpForProvider } from "../core/mcp.js";
12
- import { getClaudeMcpPath, getClaudeSettingsPath, getCodexAgentsDir, getCodexConfigPath, getCodexRootDir, getCopilotMcpPath, getCursorMcpPath, getGeminiSettingsPath, getOpenCodeConfigPath, getPiMcpPath, getProviderAgentsDir, getProviderCommandsDir, getVsCodeSettingsPath, } from "../core/provider-paths.js";
13
+ import { getClaudeMcpPath, getClaudeSettingsPath, getCodexAgentsDir, getCodexConfigPath, getCodexRootDir, getCopilotMcpPath, getCursorRulesDir, getCursorMcpPath, getGeminiSettingsPath, getOpenCodeConfigPath, getPiMcpPath, getProviderAgentsDir, getProviderCommandsDir, getRuleInstructionPaths, getVsCodeSettingsPath, } from "../core/provider-paths.js";
13
14
  import { getGlobalSettingsPath, readSettings, updateLastScope, updateLastScopeBestEffort, } from "../core/settings.js";
14
15
  export async function resolveProvidersForSync(options) {
15
16
  const settings = readSettings(options.paths.settingsPath);
@@ -22,6 +23,7 @@ export async function resolveProvidersForSync(options) {
22
23
  export async function syncFromCanonical(options) {
23
24
  const agents = parseAgentsDir(options.paths.agentsDir);
24
25
  const commands = parseCommandsDir(options.paths.commandsDir);
26
+ const rules = parseRulesDir(options.paths.rulesDir);
25
27
  const mcp = readCanonicalMcp(options.paths);
26
28
  const manifest = readManifest(options.paths);
27
29
  const effectiveManifest = {
@@ -46,6 +48,9 @@ export async function syncFromCanonical(options) {
46
48
  const generatedAgents = new Set();
47
49
  const generatedCommands = new Set();
48
50
  const generatedMcp = new Set();
51
+ const generatedRules = new Set();
52
+ const updatedRuleInstructionFiles = new Set();
53
+ const retainedRuleInstructionFiles = new Set();
49
54
  if (target === "all" || target === "agent") {
50
55
  for (const provider of providers) {
51
56
  syncProviderAgents({
@@ -56,6 +61,13 @@ export async function syncFromCanonical(options) {
56
61
  dryRun: !!options.dryRun,
57
62
  });
58
63
  }
64
+ if (providers.includes("copilot") && options.paths.scope === "global") {
65
+ syncCopilotDiscoverySettings({
66
+ paths: options.paths,
67
+ dryRun: !!options.dryRun,
68
+ includeAgentLocations: true,
69
+ });
70
+ }
59
71
  }
60
72
  if (target === "all" || target === "command") {
61
73
  for (const provider of providers) {
@@ -67,6 +79,13 @@ export async function syncFromCanonical(options) {
67
79
  dryRun: !!options.dryRun,
68
80
  });
69
81
  }
82
+ if (providers.includes("copilot") && options.paths.scope === "global") {
83
+ syncCopilotDiscoverySettings({
84
+ paths: options.paths,
85
+ dryRun: !!options.dryRun,
86
+ includePromptLocations: true,
87
+ });
88
+ }
70
89
  }
71
90
  if (target === "all" || target === "mcp") {
72
91
  syncProviderMcp({
@@ -77,6 +96,33 @@ export async function syncFromCanonical(options) {
77
96
  dryRun: !!options.dryRun,
78
97
  });
79
98
  }
99
+ if (target === "all" || target === "rule") {
100
+ syncManagedRuleInstructions({
101
+ paths: options.paths,
102
+ providers,
103
+ rules,
104
+ generated: generatedRules,
105
+ updated: updatedRuleInstructionFiles,
106
+ retained: retainedRuleInstructionFiles,
107
+ previouslyTracked: new Set(effectiveManifest.generatedByEntity?.rule ?? []),
108
+ dryRun: !!options.dryRun,
109
+ });
110
+ if (providers.includes("cursor") && options.paths.scope === "local") {
111
+ syncCursorRules({
112
+ paths: options.paths,
113
+ rules,
114
+ generated: generatedRules,
115
+ dryRun: !!options.dryRun,
116
+ });
117
+ }
118
+ if (providers.includes("copilot") && options.paths.scope === "global") {
119
+ syncCopilotDiscoverySettings({
120
+ paths: options.paths,
121
+ dryRun: !!options.dryRun,
122
+ includeInstructionLocations: true,
123
+ });
124
+ }
125
+ }
80
126
  if (providers.includes("codex")) {
81
127
  const includeRoles = target === "all" || target === "agent";
82
128
  const includeMcp = target === "all" || target === "mcp";
@@ -113,18 +159,23 @@ export async function syncFromCanonical(options) {
113
159
  if (target === "all" || target === "mcp") {
114
160
  nextByEntity.mcp = [...generatedMcp].sort();
115
161
  }
162
+ if (target === "all" || target === "rule") {
163
+ nextByEntity.rule = [...generatedRules].sort();
164
+ }
116
165
  nextManifest.generatedByEntity = pruneGeneratedByEntity(nextByEntity);
117
166
  nextManifest.generatedFiles = [
118
167
  ...new Set([
119
168
  ...(nextManifest.generatedByEntity.agent ?? []),
120
169
  ...(nextManifest.generatedByEntity.command ?? []),
121
170
  ...(nextManifest.generatedByEntity.mcp ?? []),
171
+ ...(nextManifest.generatedByEntity.rule ?? []),
122
172
  ...(nextManifest.generatedByEntity.skill ?? []),
123
173
  ]),
124
174
  ].sort();
125
175
  const removedFiles = await removeStaleGeneratedFiles({
126
176
  oldManifest: manifest,
127
177
  newManifest: nextManifest,
178
+ protectedFiles: retainedRuleInstructionFiles,
128
179
  dryRun: !!options.dryRun,
129
180
  yes: !!options.yes,
130
181
  nonInteractive: !!options.nonInteractive,
@@ -139,7 +190,12 @@ export async function syncFromCanonical(options) {
139
190
  }
140
191
  return {
141
192
  providers,
142
- generatedFiles: nextManifest.generatedFiles,
193
+ generatedFiles: [
194
+ ...new Set([
195
+ ...nextManifest.generatedFiles,
196
+ ...updatedRuleInstructionFiles,
197
+ ]),
198
+ ].sort(),
143
199
  removedFiles,
144
200
  };
145
201
  }
@@ -224,7 +280,7 @@ function buildProviderAgentContent(provider, agent, providerConfig) {
224
280
  description: agent.description,
225
281
  ...providerConfig,
226
282
  };
227
- const fm = YAML.stringify(frontmatter).trimEnd();
283
+ const fm = YAML.stringify(frontmatter, { lineWidth: 0 }).trimEnd();
228
284
  return `---\n${fm}\n---\n\n${agent.body.trimStart()}${agent.body.endsWith("\n") ? "" : "\n"}`;
229
285
  }
230
286
  function syncProviderCommands(options) {
@@ -232,15 +288,36 @@ function syncProviderCommands(options) {
232
288
  for (const command of options.commands) {
233
289
  const fileName = mapProviderCommandFileName(options.provider, command.fileName);
234
290
  const outputPath = path.join(providerDir, fileName);
291
+ const content = renderCommandForProvider(command, options.provider);
292
+ if (content === null)
293
+ continue;
235
294
  if (!options.dryRun) {
236
295
  ensureDir(path.dirname(outputPath));
237
- writeTextAtomic(outputPath, command.content);
296
+ writeTextAtomic(outputPath, content);
238
297
  }
239
298
  options.generated.add(outputPath);
240
299
  }
241
300
  }
242
301
  function mapProviderCommandFileName(provider, fileName) {
243
302
  const lower = fileName.toLowerCase();
303
+ if (provider === "gemini") {
304
+ if (lower.endsWith(".toml"))
305
+ return fileName;
306
+ if (lower.endsWith(".prompt.md")) {
307
+ return `${fileName.slice(0, -".prompt.md".length)}.toml`;
308
+ }
309
+ if (lower.endsWith(".md")) {
310
+ return `${fileName.slice(0, -3)}.toml`;
311
+ }
312
+ if (lower.endsWith(".mdc")) {
313
+ return `${fileName.slice(0, -4)}.toml`;
314
+ }
315
+ const ext = path.extname(fileName);
316
+ if (ext) {
317
+ return `${fileName.slice(0, -ext.length)}.toml`;
318
+ }
319
+ return `${fileName}.toml`;
320
+ }
244
321
  if (provider === "copilot") {
245
322
  if (lower.endsWith(".prompt.md"))
246
323
  return fileName;
@@ -261,6 +338,21 @@ function mapProviderCommandFileName(provider, fileName) {
261
338
  }
262
339
  return fileName;
263
340
  }
341
+ function syncCursorRules(options) {
342
+ if (options.paths.scope !== "local") {
343
+ return;
344
+ }
345
+ const rulesDir = getCursorRulesDir(options.paths);
346
+ for (const rule of options.rules) {
347
+ const outputPath = path.join(rulesDir, `${rule.id}.mdc`);
348
+ const content = renderRuleForCursor(rule);
349
+ if (!options.dryRun) {
350
+ ensureDir(path.dirname(outputPath));
351
+ writeTextAtomic(outputPath, content);
352
+ }
353
+ options.generated.add(outputPath);
354
+ }
355
+ }
264
356
  function syncProviderMcp(options) {
265
357
  for (const provider of options.providers) {
266
358
  if (provider === "codex")
@@ -276,8 +368,18 @@ function syncProviderMcp(options) {
276
368
  continue;
277
369
  }
278
370
  if (provider === "claude") {
279
- const mcpPath = getClaudeMcpPath(options.paths);
280
371
  const settingsPath = getClaudeSettingsPath(options.paths);
372
+ const settings = readClaudeSettingsForSync(options.paths);
373
+ if (options.paths.scope === "global") {
374
+ maybeMigrateClaudeGlobalSettings({
375
+ paths: options.paths,
376
+ settingsPath,
377
+ settings,
378
+ dryRun: options.dryRun,
379
+ });
380
+ continue;
381
+ }
382
+ const mcpPath = getClaudeMcpPath(options.paths);
281
383
  const claudeServers = mapMcpServers(resolved, [
282
384
  "type",
283
385
  "url",
@@ -292,7 +394,6 @@ function syncProviderMcp(options) {
292
394
  }
293
395
  maybeWriteJson(mcpPath, { mcpServers: claudeServers }, options.dryRun);
294
396
  options.generated.add(mcpPath);
295
- const settings = readJsonIfExists(settingsPath) ?? {};
296
397
  settings.enabledMcpjsonServers = Object.keys(claudeServers).sort();
297
398
  maybeWriteJson(settingsPath, settings, options.dryRun);
298
399
  options.generated.add(settingsPath);
@@ -396,6 +497,233 @@ function syncProviderMcp(options) {
396
497
  }
397
498
  }
398
499
  }
500
+ function syncManagedRuleInstructions(options) {
501
+ const cleanupProviders = options.paths.scope === "global"
502
+ ? ["claude", "gemini", "copilot", "opencode"]
503
+ : ALL_PROVIDERS;
504
+ const activeTargets = new Set(getRuleInstructionPaths(options.paths, options.providers));
505
+ if (options.paths.scope === "global" && activeTargets.size === 0) {
506
+ return;
507
+ }
508
+ const cleanupTargets = new Set(getRuleInstructionPaths(options.paths, cleanupProviders));
509
+ for (const targetPath of cleanupTargets) {
510
+ const existing = readTextIfExists(targetPath) ?? "";
511
+ const next = upsertManagedRuleBlocks(existing, activeTargets.has(targetPath) ? options.rules : []);
512
+ const shouldTrackGenerated = activeTargets.has(targetPath) &&
513
+ options.rules.length > 0 &&
514
+ next.trim().length > 0;
515
+ if (shouldTrackGenerated) {
516
+ options.generated.add(targetPath);
517
+ }
518
+ const shouldRetainOnDisk = !shouldTrackGenerated &&
519
+ next.trim().length > 0 &&
520
+ fs.existsSync(targetPath);
521
+ if (shouldRetainOnDisk) {
522
+ options.retained.add(targetPath);
523
+ }
524
+ if (next === existing)
525
+ continue;
526
+ options.updated.add(targetPath);
527
+ if (!options.dryRun) {
528
+ if (next.trim().length === 0) {
529
+ if (!options.previouslyTracked.has(targetPath)) {
530
+ removeFileIfExists(targetPath);
531
+ }
532
+ }
533
+ else {
534
+ ensureDir(path.dirname(targetPath));
535
+ writeTextAtomic(targetPath, next);
536
+ }
537
+ }
538
+ }
539
+ }
540
+ function syncCopilotDiscoverySettings(options) {
541
+ const settingsPath = getVsCodeSettingsPath(options.paths.homeDir);
542
+ const settings = readVsCodeSettings(settingsPath);
543
+ if (!settings) {
544
+ return;
545
+ }
546
+ if (options.includePromptLocations) {
547
+ appendPathSetting(settings, "chat.promptFilesLocations", path.join(options.paths.homeDir, ".copilot", "prompts"));
548
+ }
549
+ if (options.includeAgentLocations) {
550
+ appendPathSetting(settings, "chat.agentFilesLocations", path.join(options.paths.homeDir, ".copilot", "agents"));
551
+ }
552
+ if (options.includeInstructionLocations) {
553
+ appendPathSetting(settings, "chat.instructionsFilesLocations", path.join(options.paths.homeDir, ".copilot", "copilot-instructions.md"));
554
+ }
555
+ maybeWriteJson(settingsPath, settings, options.dryRun);
556
+ }
557
+ function readVsCodeSettings(settingsPath) {
558
+ const raw = readTextIfExists(settingsPath);
559
+ if (raw === null) {
560
+ return {};
561
+ }
562
+ const parsed = parseJsonOrJsonc(raw);
563
+ if (!isObject(parsed)) {
564
+ return null;
565
+ }
566
+ return parsed;
567
+ }
568
+ function readClaudeSettingsForSync(paths) {
569
+ const settingsPath = getClaudeSettingsPath(paths);
570
+ const settings = readJsonIfExists(settingsPath);
571
+ if (isObject(settings)) {
572
+ return { ...settings };
573
+ }
574
+ if (paths.scope === "global") {
575
+ const legacySettingsPath = path.join(paths.homeDir, ".claude.json");
576
+ const legacySettings = readJsonIfExists(legacySettingsPath);
577
+ if (isObject(legacySettings)) {
578
+ return { ...legacySettings };
579
+ }
580
+ }
581
+ return {};
582
+ }
583
+ function maybeMigrateClaudeGlobalSettings(options) {
584
+ const nextSettings = { ...options.settings };
585
+ delete nextSettings.enabledMcpjsonServers;
586
+ const legacySettingsPath = path.join(options.paths.homeDir, ".claude.json");
587
+ const hasCurrentSettings = fs.existsSync(options.settingsPath) &&
588
+ fs.statSync(options.settingsPath).isFile();
589
+ const hasLegacySettings = fs.existsSync(legacySettingsPath) &&
590
+ fs.statSync(legacySettingsPath).isFile();
591
+ const shouldWrite = hasCurrentSettings ||
592
+ (hasLegacySettings && Object.keys(nextSettings).length > 0);
593
+ if (shouldWrite) {
594
+ maybeWriteJson(options.settingsPath, nextSettings, options.dryRun);
595
+ }
596
+ }
597
+ function parseJsonOrJsonc(input) {
598
+ if (input.trim() === "") {
599
+ return {};
600
+ }
601
+ try {
602
+ return JSON.parse(input);
603
+ }
604
+ catch {
605
+ try {
606
+ const withoutComments = stripJsonComments(input);
607
+ const normalized = stripTrailingJsonCommas(withoutComments);
608
+ if (normalized.trim() === "") {
609
+ return {};
610
+ }
611
+ return JSON.parse(normalized);
612
+ }
613
+ catch {
614
+ return null;
615
+ }
616
+ }
617
+ }
618
+ function stripJsonComments(input) {
619
+ let result = "";
620
+ let inString = false;
621
+ let inLineComment = false;
622
+ let inBlockComment = false;
623
+ let escaped = false;
624
+ for (let i = 0; i < input.length; i += 1) {
625
+ const char = input[i];
626
+ const next = i + 1 < input.length ? input[i + 1] : "";
627
+ if (inLineComment) {
628
+ if (char === "\n") {
629
+ inLineComment = false;
630
+ result += char;
631
+ }
632
+ continue;
633
+ }
634
+ if (inBlockComment) {
635
+ if (char === "*" && next === "/") {
636
+ inBlockComment = false;
637
+ i += 1;
638
+ }
639
+ else if (char === "\n") {
640
+ result += char;
641
+ }
642
+ continue;
643
+ }
644
+ if (inString) {
645
+ result += char;
646
+ if (escaped) {
647
+ escaped = false;
648
+ continue;
649
+ }
650
+ if (char === "\\") {
651
+ escaped = true;
652
+ continue;
653
+ }
654
+ if (char === '"') {
655
+ inString = false;
656
+ }
657
+ continue;
658
+ }
659
+ if (char === '"') {
660
+ inString = true;
661
+ result += char;
662
+ continue;
663
+ }
664
+ if (char === "/" && next === "/") {
665
+ inLineComment = true;
666
+ i += 1;
667
+ continue;
668
+ }
669
+ if (char === "/" && next === "*") {
670
+ inBlockComment = true;
671
+ i += 1;
672
+ continue;
673
+ }
674
+ result += char;
675
+ }
676
+ return result;
677
+ }
678
+ function stripTrailingJsonCommas(input) {
679
+ let result = "";
680
+ let inString = false;
681
+ let escaped = false;
682
+ for (let i = 0; i < input.length; i += 1) {
683
+ const char = input[i];
684
+ if (inString) {
685
+ result += char;
686
+ if (escaped) {
687
+ escaped = false;
688
+ continue;
689
+ }
690
+ if (char === "\\") {
691
+ escaped = true;
692
+ continue;
693
+ }
694
+ if (char === '"') {
695
+ inString = false;
696
+ }
697
+ continue;
698
+ }
699
+ if (char === '"') {
700
+ inString = true;
701
+ result += char;
702
+ continue;
703
+ }
704
+ if (char === ",") {
705
+ let lookahead = i + 1;
706
+ while (lookahead < input.length && /\s/.test(input[lookahead] ?? "")) {
707
+ lookahead += 1;
708
+ }
709
+ const next = input[lookahead];
710
+ if (next === "}" || next === "]") {
711
+ continue;
712
+ }
713
+ }
714
+ result += char;
715
+ }
716
+ return result;
717
+ }
718
+ function appendPathSetting(settings, key, settingPath) {
719
+ const existing = Array.isArray(settings[key])
720
+ ? settings[key].filter((value) => typeof value === "string" && value.trim() !== "")
721
+ : [];
722
+ if (!existing.includes(settingPath)) {
723
+ existing.push(settingPath);
724
+ }
725
+ settings[key] = existing;
726
+ }
399
727
  function syncCodex(options) {
400
728
  const codexDir = getCodexRootDir(options.paths);
401
729
  const codexConfigPath = getCodexConfigPath(options.paths);
@@ -437,10 +765,11 @@ function syncCodex(options) {
437
765
  continue;
438
766
  const roleTomlPath = path.join(codexAgentsDir, `${role}.toml`);
439
767
  const roleInstructionsPath = path.join(codexAgentsDir, `${role}.instructions.md`);
440
- const roleToml = buildCodexRoleToml(roleInstructionsPath, codexConfig);
768
+ const developerInstructions = resolveCodexDeveloperInstructions(agent.body, codexConfig);
769
+ const roleToml = buildCodexRoleToml(developerInstructions, codexConfig);
441
770
  if (!options.dryRun) {
442
771
  ensureDir(codexAgentsDir);
443
- writeTextAtomic(roleInstructionsPath, `${agent.body.trimStart()}\n`);
772
+ writeTextAtomic(roleInstructionsPath, `${developerInstructions}\n`);
444
773
  writeTextAtomic(roleTomlPath, TOML.stringify(roleToml));
445
774
  }
446
775
  options.generated.add(roleTomlPath);
@@ -490,9 +819,16 @@ function resolveTrackedCodexEntries(trackedEntries, fallbackEntries) {
490
819
  const tracked = Array.isArray(trackedEntries) ? trackedEntries : [];
491
820
  return [...new Set([...tracked, ...fallbackEntries])].sort();
492
821
  }
493
- function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
822
+ function resolveCodexDeveloperInstructions(agentBody, providerConfig) {
823
+ if (typeof providerConfig.developerInstructions === "string" &&
824
+ providerConfig.developerInstructions.trim() !== "") {
825
+ return providerConfig.developerInstructions.trim();
826
+ }
827
+ return agentBody.trimStart().trimEnd();
828
+ }
829
+ function buildCodexRoleToml(developerInstructions, providerConfig) {
494
830
  const roleToml = {
495
- model_instructions_file: `./${path.basename(roleInstructionsPath)}`,
831
+ developer_instructions: developerInstructions,
496
832
  };
497
833
  if (typeof providerConfig.model === "string") {
498
834
  roleToml.model = providerConfig.model;
@@ -500,6 +836,12 @@ function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
500
836
  if (typeof providerConfig.reasoningEffort === "string") {
501
837
  roleToml.model_reasoning_effort = providerConfig.reasoningEffort;
502
838
  }
839
+ if (typeof providerConfig.reasoningSummary === "string") {
840
+ roleToml.model_reasoning_summary = providerConfig.reasoningSummary;
841
+ }
842
+ if (typeof providerConfig.verbosity === "string") {
843
+ roleToml.model_verbosity = providerConfig.verbosity;
844
+ }
503
845
  if (typeof providerConfig.approvalPolicy === "string") {
504
846
  roleToml.approval_policy = providerConfig.approvalPolicy;
505
847
  }
@@ -507,9 +849,7 @@ function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
507
849
  roleToml.sandbox_mode = providerConfig.sandboxMode;
508
850
  }
509
851
  if (typeof providerConfig.webSearch === "boolean") {
510
- roleToml.tools = {
511
- web_search: providerConfig.webSearch,
512
- };
852
+ roleToml.web_search = providerConfig.webSearch;
513
853
  }
514
854
  return roleToml;
515
855
  }
@@ -536,8 +876,10 @@ function maybeWriteJson(filePath, payload, dryRun) {
536
876
  async function removeStaleGeneratedFiles(options) {
537
877
  const oldSet = new Set(options.oldManifest.generatedFiles);
538
878
  const newSet = new Set(options.newManifest.generatedFiles);
879
+ const protectedSet = new Set(options.protectedFiles ?? []);
539
880
  const stale = [...oldSet]
540
881
  .filter((filePath) => !newSet.has(filePath))
882
+ .filter((filePath) => !protectedSet.has(filePath))
541
883
  .filter((filePath) => fs.existsSync(filePath));
542
884
  if (stale.length === 0)
543
885
  return [];
@@ -574,6 +916,7 @@ function normalizeGeneratedByEntity(manifest) {
574
916
  agent: Array.isArray(source.agent) ? [...source.agent] : [],
575
917
  command: Array.isArray(source.command) ? [...source.command] : [],
576
918
  mcp: Array.isArray(source.mcp) ? [...source.mcp] : [],
919
+ rule: Array.isArray(source.rule) ? [...source.rule] : [],
577
920
  skill: Array.isArray(source.skill) ? [...source.skill] : [],
578
921
  };
579
922
  }
@@ -582,6 +925,7 @@ function inferGeneratedByEntityFromLegacyFiles(generatedFiles) {
582
925
  agent: [],
583
926
  command: [],
584
927
  mcp: [],
928
+ rule: [],
585
929
  skill: [],
586
930
  };
587
931
  for (const filePath of generatedFiles) {
@@ -605,31 +949,45 @@ function classifyLegacyGeneratedFile(filePath) {
605
949
  if (isLegacyMcpOutputPath(normalized)) {
606
950
  return ["mcp"];
607
951
  }
952
+ if (isLegacyRuleOutputPath(normalized)) {
953
+ return ["agent", "rule"];
954
+ }
608
955
  // Preserve unknown generated paths during scoped syncs.
609
- return ["agent", "command", "mcp"];
956
+ return ["agent", "command", "mcp", "rule"];
610
957
  }
611
958
  function isLegacyCommandOutputPath(normalizedPath) {
612
959
  return (normalizedPath.includes("/.cursor/commands/") ||
613
960
  normalizedPath.includes("/.claude/commands/") ||
614
961
  normalizedPath.includes("/.opencode/commands/") ||
615
962
  normalizedPath.includes("/.gemini/commands/") ||
963
+ normalizedPath.includes("/.copilot/prompts/") ||
616
964
  normalizedPath.includes("/.github/prompts/") ||
617
965
  normalizedPath.includes("/.codex/prompts/") ||
618
966
  normalizedPath.includes("/.pi/prompts/"));
619
967
  }
620
968
  function isLegacyAgentOutputPath(normalizedPath) {
621
969
  return (normalizedPath.includes("/.cursor/agents/") ||
622
- normalizedPath.includes("/.cursor/rules/") ||
623
970
  normalizedPath.includes("/.claude/agents/") ||
624
971
  normalizedPath.includes("/.opencode/agents/") ||
625
972
  normalizedPath.includes("/.gemini/agents/") ||
973
+ normalizedPath.includes("/.copilot/agents/") ||
626
974
  normalizedPath.includes("/.github/agents/") ||
627
975
  normalizedPath.includes("/.codex/agents/") ||
628
976
  normalizedPath.includes("/.pi/agents/"));
629
977
  }
978
+ function isLegacyRuleOutputPath(normalizedPath) {
979
+ return (normalizedPath.includes("/.cursor/rules/") ||
980
+ normalizedPath.endsWith("/agents.md") ||
981
+ normalizedPath.endsWith("/claude.md") ||
982
+ normalizedPath.endsWith("/gemini.md") ||
983
+ normalizedPath.endsWith("/.github/copilot-instructions.md") ||
984
+ normalizedPath.endsWith("/.copilot/copilot-instructions.md") ||
985
+ normalizedPath.endsWith("/.config/opencode/agents.md"));
986
+ }
630
987
  function isLegacyMcpOutputPath(normalizedPath) {
631
988
  return (normalizedPath.endsWith("/.cursor/mcp.json") ||
632
989
  normalizedPath.endsWith("/.mcp.json") ||
990
+ normalizedPath.endsWith("/.claude.json") ||
633
991
  normalizedPath.endsWith("/.claude/settings.json") ||
634
992
  normalizedPath.endsWith("/.opencode/opencode.json") ||
635
993
  normalizedPath.endsWith("/.gemini/settings.json") ||
@@ -643,7 +1001,7 @@ function isLegacyCodexConfigPath(normalizedPath) {
643
1001
  }
644
1002
  function pruneGeneratedByEntity(value) {
645
1003
  const next = {};
646
- for (const entity of ["agent", "command", "mcp", "skill"]) {
1004
+ for (const entity of ["agent", "command", "mcp", "rule", "skill"]) {
647
1005
  const files = value[entity];
648
1006
  if (!files || files.length === 0)
649
1007
  continue;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export declare const ALL_PROVIDERS: readonly ["cursor", "claude", "codex", "opencode", "gemini", "copilot", "pi"];
2
2
  export type Provider = (typeof ALL_PROVIDERS)[number];
3
3
  export type Scope = "local" | "global";
4
- export type EntityType = "agent" | "command" | "mcp" | "skill";
4
+ export type EntityType = "agent" | "command" | "mcp" | "rule" | "skill";
5
5
  export type SelectionMode = "all" | "custom";
6
6
  export interface AgentFrontmatter {
7
7
  name: string;
@@ -37,6 +37,9 @@ export interface LockEntry {
37
37
  commandRenameMap?: Record<string, string>;
38
38
  importedMcpServers: string[];
39
39
  selectedSourceMcpServers?: string[];
40
+ importedRules: string[];
41
+ selectedSourceRules?: string[];
42
+ ruleRenameMap?: Record<string, string>;
40
43
  importedSkills: string[];
41
44
  selectedSourceSkills?: string[];
42
45
  skillsProviders?: Provider[];
@@ -64,6 +67,7 @@ export interface ScopePaths {
64
67
  agentsRoot: string;
65
68
  agentsDir: string;
66
69
  commandsDir: string;
70
+ rulesDir: string;
67
71
  skillsDir: string;
68
72
  mcpPath: string;
69
73
  lockPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentloom",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
5
5
  "type": "module",
6
6
  "bin": {