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.
@@ -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,8 @@ 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);
27
+ const managedInstructionRules = rules.filter((rule) => rule.frontmatter.alwaysApply !== false);
25
28
  const mcp = readCanonicalMcp(options.paths);
26
29
  const manifest = readManifest(options.paths);
27
30
  const effectiveManifest = {
@@ -46,6 +49,9 @@ export async function syncFromCanonical(options) {
46
49
  const generatedAgents = new Set();
47
50
  const generatedCommands = new Set();
48
51
  const generatedMcp = new Set();
52
+ const generatedRules = new Set();
53
+ const updatedRuleInstructionFiles = new Set();
54
+ const retainedRuleInstructionFiles = new Set();
49
55
  if (target === "all" || target === "agent") {
50
56
  for (const provider of providers) {
51
57
  syncProviderAgents({
@@ -56,6 +62,13 @@ export async function syncFromCanonical(options) {
56
62
  dryRun: !!options.dryRun,
57
63
  });
58
64
  }
65
+ if (providers.includes("copilot") && options.paths.scope === "global") {
66
+ syncCopilotDiscoverySettings({
67
+ paths: options.paths,
68
+ dryRun: !!options.dryRun,
69
+ includeAgentLocations: true,
70
+ });
71
+ }
59
72
  }
60
73
  if (target === "all" || target === "command") {
61
74
  for (const provider of providers) {
@@ -67,6 +80,13 @@ export async function syncFromCanonical(options) {
67
80
  dryRun: !!options.dryRun,
68
81
  });
69
82
  }
83
+ if (providers.includes("copilot") && options.paths.scope === "global") {
84
+ syncCopilotDiscoverySettings({
85
+ paths: options.paths,
86
+ dryRun: !!options.dryRun,
87
+ includePromptLocations: true,
88
+ });
89
+ }
70
90
  }
71
91
  if (target === "all" || target === "mcp") {
72
92
  syncProviderMcp({
@@ -77,6 +97,33 @@ export async function syncFromCanonical(options) {
77
97
  dryRun: !!options.dryRun,
78
98
  });
79
99
  }
100
+ if (target === "all" || target === "rule") {
101
+ syncManagedRuleInstructions({
102
+ paths: options.paths,
103
+ providers,
104
+ rules: managedInstructionRules,
105
+ generated: generatedRules,
106
+ updated: updatedRuleInstructionFiles,
107
+ retained: retainedRuleInstructionFiles,
108
+ previouslyTracked: new Set(effectiveManifest.generatedByEntity?.rule ?? []),
109
+ dryRun: !!options.dryRun,
110
+ });
111
+ if (providers.includes("cursor") && options.paths.scope === "local") {
112
+ syncCursorRules({
113
+ paths: options.paths,
114
+ rules,
115
+ generated: generatedRules,
116
+ dryRun: !!options.dryRun,
117
+ });
118
+ }
119
+ if (providers.includes("copilot") && options.paths.scope === "global") {
120
+ syncCopilotDiscoverySettings({
121
+ paths: options.paths,
122
+ dryRun: !!options.dryRun,
123
+ includeInstructionLocations: true,
124
+ });
125
+ }
126
+ }
80
127
  if (providers.includes("codex")) {
81
128
  const includeRoles = target === "all" || target === "agent";
82
129
  const includeMcp = target === "all" || target === "mcp";
@@ -113,18 +160,23 @@ export async function syncFromCanonical(options) {
113
160
  if (target === "all" || target === "mcp") {
114
161
  nextByEntity.mcp = [...generatedMcp].sort();
115
162
  }
163
+ if (target === "all" || target === "rule") {
164
+ nextByEntity.rule = [...generatedRules].sort();
165
+ }
116
166
  nextManifest.generatedByEntity = pruneGeneratedByEntity(nextByEntity);
117
167
  nextManifest.generatedFiles = [
118
168
  ...new Set([
119
169
  ...(nextManifest.generatedByEntity.agent ?? []),
120
170
  ...(nextManifest.generatedByEntity.command ?? []),
121
171
  ...(nextManifest.generatedByEntity.mcp ?? []),
172
+ ...(nextManifest.generatedByEntity.rule ?? []),
122
173
  ...(nextManifest.generatedByEntity.skill ?? []),
123
174
  ]),
124
175
  ].sort();
125
176
  const removedFiles = await removeStaleGeneratedFiles({
126
177
  oldManifest: manifest,
127
178
  newManifest: nextManifest,
179
+ protectedFiles: retainedRuleInstructionFiles,
128
180
  dryRun: !!options.dryRun,
129
181
  yes: !!options.yes,
130
182
  nonInteractive: !!options.nonInteractive,
@@ -139,7 +191,12 @@ export async function syncFromCanonical(options) {
139
191
  }
140
192
  return {
141
193
  providers,
142
- generatedFiles: nextManifest.generatedFiles,
194
+ generatedFiles: [
195
+ ...new Set([
196
+ ...nextManifest.generatedFiles,
197
+ ...updatedRuleInstructionFiles,
198
+ ]),
199
+ ].sort(),
143
200
  removedFiles,
144
201
  };
145
202
  }
@@ -232,15 +289,36 @@ function syncProviderCommands(options) {
232
289
  for (const command of options.commands) {
233
290
  const fileName = mapProviderCommandFileName(options.provider, command.fileName);
234
291
  const outputPath = path.join(providerDir, fileName);
292
+ const content = renderCommandForProvider(command, options.provider);
293
+ if (content === null)
294
+ continue;
235
295
  if (!options.dryRun) {
236
296
  ensureDir(path.dirname(outputPath));
237
- writeTextAtomic(outputPath, command.content);
297
+ writeTextAtomic(outputPath, content);
238
298
  }
239
299
  options.generated.add(outputPath);
240
300
  }
241
301
  }
242
302
  function mapProviderCommandFileName(provider, fileName) {
243
303
  const lower = fileName.toLowerCase();
304
+ if (provider === "gemini") {
305
+ if (lower.endsWith(".toml"))
306
+ return fileName;
307
+ if (lower.endsWith(".prompt.md")) {
308
+ return `${fileName.slice(0, -".prompt.md".length)}.toml`;
309
+ }
310
+ if (lower.endsWith(".md")) {
311
+ return `${fileName.slice(0, -3)}.toml`;
312
+ }
313
+ if (lower.endsWith(".mdc")) {
314
+ return `${fileName.slice(0, -4)}.toml`;
315
+ }
316
+ const ext = path.extname(fileName);
317
+ if (ext) {
318
+ return `${fileName.slice(0, -ext.length)}.toml`;
319
+ }
320
+ return `${fileName}.toml`;
321
+ }
244
322
  if (provider === "copilot") {
245
323
  if (lower.endsWith(".prompt.md"))
246
324
  return fileName;
@@ -261,6 +339,21 @@ function mapProviderCommandFileName(provider, fileName) {
261
339
  }
262
340
  return fileName;
263
341
  }
342
+ function syncCursorRules(options) {
343
+ if (options.paths.scope !== "local") {
344
+ return;
345
+ }
346
+ const rulesDir = getCursorRulesDir(options.paths);
347
+ for (const rule of options.rules) {
348
+ const outputPath = path.join(rulesDir, `${rule.id}.mdc`);
349
+ const content = renderRuleForCursor(rule);
350
+ if (!options.dryRun) {
351
+ ensureDir(path.dirname(outputPath));
352
+ writeTextAtomic(outputPath, content);
353
+ }
354
+ options.generated.add(outputPath);
355
+ }
356
+ }
264
357
  function syncProviderMcp(options) {
265
358
  for (const provider of options.providers) {
266
359
  if (provider === "codex")
@@ -276,8 +369,18 @@ function syncProviderMcp(options) {
276
369
  continue;
277
370
  }
278
371
  if (provider === "claude") {
279
- const mcpPath = getClaudeMcpPath(options.paths);
280
372
  const settingsPath = getClaudeSettingsPath(options.paths);
373
+ const settings = readClaudeSettingsForSync(options.paths);
374
+ if (options.paths.scope === "global") {
375
+ maybeMigrateClaudeGlobalSettings({
376
+ paths: options.paths,
377
+ settingsPath,
378
+ settings,
379
+ dryRun: options.dryRun,
380
+ });
381
+ continue;
382
+ }
383
+ const mcpPath = getClaudeMcpPath(options.paths);
281
384
  const claudeServers = mapMcpServers(resolved, [
282
385
  "type",
283
386
  "url",
@@ -292,7 +395,6 @@ function syncProviderMcp(options) {
292
395
  }
293
396
  maybeWriteJson(mcpPath, { mcpServers: claudeServers }, options.dryRun);
294
397
  options.generated.add(mcpPath);
295
- const settings = readJsonIfExists(settingsPath) ?? {};
296
398
  settings.enabledMcpjsonServers = Object.keys(claudeServers).sort();
297
399
  maybeWriteJson(settingsPath, settings, options.dryRun);
298
400
  options.generated.add(settingsPath);
@@ -396,6 +498,260 @@ function syncProviderMcp(options) {
396
498
  }
397
499
  }
398
500
  }
501
+ function syncManagedRuleInstructions(options) {
502
+ const cleanupProviders = options.paths.scope === "global"
503
+ ? ["claude", "gemini", "copilot", "opencode"]
504
+ : ALL_PROVIDERS;
505
+ const activeTargets = new Set(getRuleInstructionPaths(options.paths, options.providers));
506
+ if (options.paths.scope === "global" && activeTargets.size === 0) {
507
+ return;
508
+ }
509
+ const cleanupTargets = new Set(getRuleInstructionPaths(options.paths, cleanupProviders));
510
+ for (const targetPath of cleanupTargets) {
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
+ }
523
+ const next = upsertManagedRuleBlocks(existing, activeTargets.has(targetPath) ? options.rules : []);
524
+ const shouldTrackGenerated = activeTargets.has(targetPath) &&
525
+ options.rules.length > 0 &&
526
+ next.trim().length > 0;
527
+ if (shouldTrackGenerated) {
528
+ options.generated.add(targetPath);
529
+ }
530
+ const shouldRetainOnDisk = !shouldTrackGenerated &&
531
+ next.trim().length > 0 &&
532
+ fs.existsSync(targetPath);
533
+ if (shouldRetainOnDisk) {
534
+ options.retained.add(targetPath);
535
+ }
536
+ if (next === existing)
537
+ continue;
538
+ options.updated.add(targetPath);
539
+ if (!options.dryRun) {
540
+ if (next.trim().length === 0) {
541
+ if (!options.previouslyTracked.has(targetPath)) {
542
+ removeFileIfExists(targetPath);
543
+ }
544
+ }
545
+ else {
546
+ writeTextAtomic(targetPath, next);
547
+ }
548
+ }
549
+ }
550
+ }
551
+ function syncCopilotDiscoverySettings(options) {
552
+ const settingsPath = getVsCodeSettingsPath(options.paths.homeDir);
553
+ const settings = readVsCodeSettings(settingsPath);
554
+ if (!settings) {
555
+ return;
556
+ }
557
+ if (options.includePromptLocations) {
558
+ appendPathSetting(settings, "chat.promptFilesLocations", path.join(options.paths.homeDir, ".copilot", "prompts"));
559
+ }
560
+ if (options.includeAgentLocations) {
561
+ appendPathSetting(settings, "chat.agentFilesLocations", path.join(options.paths.homeDir, ".copilot", "agents"));
562
+ }
563
+ if (options.includeInstructionLocations) {
564
+ const instructionPath = path.join(options.paths.homeDir, ".copilot", "copilot-instructions.md");
565
+ setPathSettingEnabled(settings, "chat.instructionsFilesLocations", instructionPath, fs.existsSync(instructionPath));
566
+ }
567
+ maybeWriteJson(settingsPath, settings, options.dryRun);
568
+ }
569
+ function readVsCodeSettings(settingsPath) {
570
+ const raw = readTextIfExists(settingsPath);
571
+ if (raw === null) {
572
+ return {};
573
+ }
574
+ const parsed = parseJsonOrJsonc(raw);
575
+ if (!isObject(parsed)) {
576
+ return null;
577
+ }
578
+ return parsed;
579
+ }
580
+ function readClaudeSettingsForSync(paths) {
581
+ const settingsPath = getClaudeSettingsPath(paths);
582
+ const settings = readJsonIfExists(settingsPath);
583
+ if (isObject(settings)) {
584
+ return { ...settings };
585
+ }
586
+ if (paths.scope === "global") {
587
+ const legacySettingsPath = path.join(paths.homeDir, ".claude.json");
588
+ const legacySettings = readJsonIfExists(legacySettingsPath);
589
+ if (isObject(legacySettings)) {
590
+ return { ...legacySettings };
591
+ }
592
+ }
593
+ return {};
594
+ }
595
+ function maybeMigrateClaudeGlobalSettings(options) {
596
+ const nextSettings = { ...options.settings };
597
+ delete nextSettings.enabledMcpjsonServers;
598
+ const legacySettingsPath = path.join(options.paths.homeDir, ".claude.json");
599
+ const hasCurrentSettings = fs.existsSync(options.settingsPath) &&
600
+ fs.statSync(options.settingsPath).isFile();
601
+ const hasLegacySettings = fs.existsSync(legacySettingsPath) &&
602
+ fs.statSync(legacySettingsPath).isFile();
603
+ const shouldWrite = hasCurrentSettings ||
604
+ (hasLegacySettings && Object.keys(nextSettings).length > 0);
605
+ if (shouldWrite) {
606
+ maybeWriteJson(options.settingsPath, nextSettings, options.dryRun);
607
+ }
608
+ }
609
+ function parseJsonOrJsonc(input) {
610
+ if (input.trim() === "") {
611
+ return {};
612
+ }
613
+ try {
614
+ return JSON.parse(input);
615
+ }
616
+ catch {
617
+ try {
618
+ const withoutComments = stripJsonComments(input);
619
+ const normalized = stripTrailingJsonCommas(withoutComments);
620
+ if (normalized.trim() === "") {
621
+ return {};
622
+ }
623
+ return JSON.parse(normalized);
624
+ }
625
+ catch {
626
+ return null;
627
+ }
628
+ }
629
+ }
630
+ function stripJsonComments(input) {
631
+ let result = "";
632
+ let inString = false;
633
+ let inLineComment = false;
634
+ let inBlockComment = false;
635
+ let escaped = false;
636
+ for (let i = 0; i < input.length; i += 1) {
637
+ const char = input[i];
638
+ const next = i + 1 < input.length ? input[i + 1] : "";
639
+ if (inLineComment) {
640
+ if (char === "\n") {
641
+ inLineComment = false;
642
+ result += char;
643
+ }
644
+ continue;
645
+ }
646
+ if (inBlockComment) {
647
+ if (char === "*" && next === "/") {
648
+ inBlockComment = false;
649
+ i += 1;
650
+ }
651
+ else if (char === "\n") {
652
+ result += char;
653
+ }
654
+ continue;
655
+ }
656
+ if (inString) {
657
+ result += char;
658
+ if (escaped) {
659
+ escaped = false;
660
+ continue;
661
+ }
662
+ if (char === "\\") {
663
+ escaped = true;
664
+ continue;
665
+ }
666
+ if (char === '"') {
667
+ inString = false;
668
+ }
669
+ continue;
670
+ }
671
+ if (char === '"') {
672
+ inString = true;
673
+ result += char;
674
+ continue;
675
+ }
676
+ if (char === "/" && next === "/") {
677
+ inLineComment = true;
678
+ i += 1;
679
+ continue;
680
+ }
681
+ if (char === "/" && next === "*") {
682
+ inBlockComment = true;
683
+ i += 1;
684
+ continue;
685
+ }
686
+ result += char;
687
+ }
688
+ return result;
689
+ }
690
+ function stripTrailingJsonCommas(input) {
691
+ let result = "";
692
+ let inString = false;
693
+ let escaped = false;
694
+ for (let i = 0; i < input.length; i += 1) {
695
+ const char = input[i];
696
+ if (inString) {
697
+ result += char;
698
+ if (escaped) {
699
+ escaped = false;
700
+ continue;
701
+ }
702
+ if (char === "\\") {
703
+ escaped = true;
704
+ continue;
705
+ }
706
+ if (char === '"') {
707
+ inString = false;
708
+ }
709
+ continue;
710
+ }
711
+ if (char === '"') {
712
+ inString = true;
713
+ result += char;
714
+ continue;
715
+ }
716
+ if (char === ",") {
717
+ let lookahead = i + 1;
718
+ while (lookahead < input.length && /\s/.test(input[lookahead] ?? "")) {
719
+ lookahead += 1;
720
+ }
721
+ const next = input[lookahead];
722
+ if (next === "}" || next === "]") {
723
+ continue;
724
+ }
725
+ }
726
+ result += char;
727
+ }
728
+ return result;
729
+ }
730
+ function appendPathSetting(settings, key, settingPath) {
731
+ const existing = Array.isArray(settings[key])
732
+ ? settings[key].filter((value) => typeof value === "string" && value.trim() !== "")
733
+ : [];
734
+ if (!existing.includes(settingPath)) {
735
+ existing.push(settingPath);
736
+ }
737
+ settings[key] = existing;
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
+ }
399
755
  function syncCodex(options) {
400
756
  const codexDir = getCodexRootDir(options.paths);
401
757
  const codexConfigPath = getCodexConfigPath(options.paths);
@@ -437,10 +793,11 @@ function syncCodex(options) {
437
793
  continue;
438
794
  const roleTomlPath = path.join(codexAgentsDir, `${role}.toml`);
439
795
  const roleInstructionsPath = path.join(codexAgentsDir, `${role}.instructions.md`);
440
- const roleToml = buildCodexRoleToml(roleInstructionsPath, codexConfig);
796
+ const developerInstructions = resolveCodexDeveloperInstructions(agent.body, codexConfig);
797
+ const roleToml = buildCodexRoleToml(developerInstructions, codexConfig);
441
798
  if (!options.dryRun) {
442
799
  ensureDir(codexAgentsDir);
443
- writeTextAtomic(roleInstructionsPath, `${agent.body.trimStart()}\n`);
800
+ writeTextAtomic(roleInstructionsPath, `${developerInstructions}\n`);
444
801
  writeTextAtomic(roleTomlPath, TOML.stringify(roleToml));
445
802
  }
446
803
  options.generated.add(roleTomlPath);
@@ -490,9 +847,16 @@ function resolveTrackedCodexEntries(trackedEntries, fallbackEntries) {
490
847
  const tracked = Array.isArray(trackedEntries) ? trackedEntries : [];
491
848
  return [...new Set([...tracked, ...fallbackEntries])].sort();
492
849
  }
493
- function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
850
+ function resolveCodexDeveloperInstructions(agentBody, providerConfig) {
851
+ if (typeof providerConfig.developerInstructions === "string" &&
852
+ providerConfig.developerInstructions.trim() !== "") {
853
+ return providerConfig.developerInstructions.trim();
854
+ }
855
+ return agentBody.trimStart().trimEnd();
856
+ }
857
+ function buildCodexRoleToml(developerInstructions, providerConfig) {
494
858
  const roleToml = {
495
- model_instructions_file: `./${path.basename(roleInstructionsPath)}`,
859
+ developer_instructions: developerInstructions,
496
860
  };
497
861
  if (typeof providerConfig.model === "string") {
498
862
  roleToml.model = providerConfig.model;
@@ -500,6 +864,12 @@ function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
500
864
  if (typeof providerConfig.reasoningEffort === "string") {
501
865
  roleToml.model_reasoning_effort = providerConfig.reasoningEffort;
502
866
  }
867
+ if (typeof providerConfig.reasoningSummary === "string") {
868
+ roleToml.model_reasoning_summary = providerConfig.reasoningSummary;
869
+ }
870
+ if (typeof providerConfig.verbosity === "string") {
871
+ roleToml.model_verbosity = providerConfig.verbosity;
872
+ }
503
873
  if (typeof providerConfig.approvalPolicy === "string") {
504
874
  roleToml.approval_policy = providerConfig.approvalPolicy;
505
875
  }
@@ -507,9 +877,7 @@ function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
507
877
  roleToml.sandbox_mode = providerConfig.sandboxMode;
508
878
  }
509
879
  if (typeof providerConfig.webSearch === "boolean") {
510
- roleToml.tools = {
511
- web_search: providerConfig.webSearch,
512
- };
880
+ roleToml.web_search = providerConfig.webSearch;
513
881
  }
514
882
  return roleToml;
515
883
  }
@@ -536,8 +904,10 @@ function maybeWriteJson(filePath, payload, dryRun) {
536
904
  async function removeStaleGeneratedFiles(options) {
537
905
  const oldSet = new Set(options.oldManifest.generatedFiles);
538
906
  const newSet = new Set(options.newManifest.generatedFiles);
907
+ const protectedSet = new Set(options.protectedFiles ?? []);
539
908
  const stale = [...oldSet]
540
909
  .filter((filePath) => !newSet.has(filePath))
910
+ .filter((filePath) => !protectedSet.has(filePath))
541
911
  .filter((filePath) => fs.existsSync(filePath));
542
912
  if (stale.length === 0)
543
913
  return [];
@@ -574,6 +944,7 @@ function normalizeGeneratedByEntity(manifest) {
574
944
  agent: Array.isArray(source.agent) ? [...source.agent] : [],
575
945
  command: Array.isArray(source.command) ? [...source.command] : [],
576
946
  mcp: Array.isArray(source.mcp) ? [...source.mcp] : [],
947
+ rule: Array.isArray(source.rule) ? [...source.rule] : [],
577
948
  skill: Array.isArray(source.skill) ? [...source.skill] : [],
578
949
  };
579
950
  }
@@ -582,6 +953,7 @@ function inferGeneratedByEntityFromLegacyFiles(generatedFiles) {
582
953
  agent: [],
583
954
  command: [],
584
955
  mcp: [],
956
+ rule: [],
585
957
  skill: [],
586
958
  };
587
959
  for (const filePath of generatedFiles) {
@@ -605,31 +977,45 @@ function classifyLegacyGeneratedFile(filePath) {
605
977
  if (isLegacyMcpOutputPath(normalized)) {
606
978
  return ["mcp"];
607
979
  }
980
+ if (isLegacyRuleOutputPath(normalized)) {
981
+ return ["agent", "rule"];
982
+ }
608
983
  // Preserve unknown generated paths during scoped syncs.
609
- return ["agent", "command", "mcp"];
984
+ return ["agent", "command", "mcp", "rule"];
610
985
  }
611
986
  function isLegacyCommandOutputPath(normalizedPath) {
612
987
  return (normalizedPath.includes("/.cursor/commands/") ||
613
988
  normalizedPath.includes("/.claude/commands/") ||
614
989
  normalizedPath.includes("/.opencode/commands/") ||
615
990
  normalizedPath.includes("/.gemini/commands/") ||
991
+ normalizedPath.includes("/.copilot/prompts/") ||
616
992
  normalizedPath.includes("/.github/prompts/") ||
617
993
  normalizedPath.includes("/.codex/prompts/") ||
618
994
  normalizedPath.includes("/.pi/prompts/"));
619
995
  }
620
996
  function isLegacyAgentOutputPath(normalizedPath) {
621
997
  return (normalizedPath.includes("/.cursor/agents/") ||
622
- normalizedPath.includes("/.cursor/rules/") ||
623
998
  normalizedPath.includes("/.claude/agents/") ||
624
999
  normalizedPath.includes("/.opencode/agents/") ||
625
1000
  normalizedPath.includes("/.gemini/agents/") ||
1001
+ normalizedPath.includes("/.copilot/agents/") ||
626
1002
  normalizedPath.includes("/.github/agents/") ||
627
1003
  normalizedPath.includes("/.codex/agents/") ||
628
1004
  normalizedPath.includes("/.pi/agents/"));
629
1005
  }
1006
+ function isLegacyRuleOutputPath(normalizedPath) {
1007
+ return (normalizedPath.includes("/.cursor/rules/") ||
1008
+ normalizedPath.endsWith("/agents.md") ||
1009
+ normalizedPath.endsWith("/claude.md") ||
1010
+ normalizedPath.endsWith("/gemini.md") ||
1011
+ normalizedPath.endsWith("/.github/copilot-instructions.md") ||
1012
+ normalizedPath.endsWith("/.copilot/copilot-instructions.md") ||
1013
+ normalizedPath.endsWith("/.config/opencode/agents.md"));
1014
+ }
630
1015
  function isLegacyMcpOutputPath(normalizedPath) {
631
1016
  return (normalizedPath.endsWith("/.cursor/mcp.json") ||
632
1017
  normalizedPath.endsWith("/.mcp.json") ||
1018
+ normalizedPath.endsWith("/.claude.json") ||
633
1019
  normalizedPath.endsWith("/.claude/settings.json") ||
634
1020
  normalizedPath.endsWith("/.opencode/opencode.json") ||
635
1021
  normalizedPath.endsWith("/.gemini/settings.json") ||
@@ -643,7 +1029,7 @@ function isLegacyCodexConfigPath(normalizedPath) {
643
1029
  }
644
1030
  function pruneGeneratedByEntity(value) {
645
1031
  const next = {};
646
- for (const entity of ["agent", "command", "mcp", "skill"]) {
1032
+ for (const entity of ["agent", "command", "mcp", "rule", "skill"]) {
647
1033
  const files = value[entity];
648
1034
  if (!files || files.length === 0)
649
1035
  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.5",
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": {