compose-agentsmd 3.2.7 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,8 @@ compose-agentsmd
32
32
 
33
33
  The tool reads `agent-ruleset.json` from the given root directory (default: current working directory), and writes the output file specified by the ruleset. If `output` is omitted, it defaults to `AGENTS.md`.
34
34
 
35
+ By default, compose also writes a `CLAUDE.md` companion file containing an `@...` import pointing to the primary output file. You can disable this with `claude.enabled: false` in the ruleset.
36
+
35
37
  The tool prepends a small "Tool Rules" block to every generated `AGENTS.md` so agents know how to regenerate or update rules.
36
38
  Each composed rule section is also prefixed with the source file path that produced it.
37
39
 
@@ -51,6 +53,7 @@ Defaults:
51
53
  - `domains`: empty
52
54
  - `extra`: empty
53
55
  - `global`: omitted (defaults to `true`)
56
+ - `claude`: `{ "enabled": true, "output": "CLAUDE.md" }`
54
57
  - `output`: `AGENTS.md`
55
58
 
56
59
  Use `--dry-run` to preview actions, `--force` to overwrite existing files, and `--compose` to generate `AGENTS.md` immediately.
@@ -84,6 +87,11 @@ Ruleset files accept JSON with `//` or `/* */` comments.
84
87
  "domains": ["node", "unreal"],
85
88
  // Additional local rule files to append.
86
89
  "extra": ["agent-rules-local/custom.md"],
90
+ // Optional Claude Code companion output.
91
+ "claude": {
92
+ "enabled": true,
93
+ "output": "CLAUDE.md"
94
+ },
87
95
  // Output file name.
88
96
  "output": "AGENTS.md"
89
97
  }
@@ -95,6 +103,9 @@ Ruleset keys:
95
103
  - `global` (optional): include `rules/global` (defaults to true). Omit this unless you want to disable globals.
96
104
  - `domains` (optional): domain folders under `rules/domains/<domain>`.
97
105
  - `extra` (optional): additional local rule files to append.
106
+ - `claude` (optional): companion settings for Claude Code.
107
+ - `claude.enabled` (optional): enable/disable companion generation (defaults to `true`).
108
+ - `claude.output` (optional): companion file path (defaults to `CLAUDE.md`).
98
109
  - `output` (optional): output file name (defaults to `AGENTS.md`).
99
110
 
100
111
  ### Ruleset schema validation
@@ -121,7 +132,7 @@ Remote sources are cached under `~/.agentsmd/cache/<owner>/<repo>/<ref>/`. Use `
121
132
  - `--no-domains`: initialize with no domains
122
133
  - `--no-extra`: initialize without extra rule files
123
134
  - `--no-global`: initialize without global rules
124
- - `--compose`: compose `AGENTS.md` after `init`
135
+ - `--compose`: compose output file(s) after `init`
125
136
  - `--dry-run`: show init plan without writing files
126
137
  - `--yes`: skip init confirmation prompt
127
138
  - `--force`: overwrite existing files during init
@@ -16,6 +16,19 @@
16
16
  "type": "string",
17
17
  "minLength": 1
18
18
  },
19
+ "claude": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "enabled": {
24
+ "type": "boolean"
25
+ },
26
+ "output": {
27
+ "type": "string",
28
+ "minLength": 1
29
+ }
30
+ }
31
+ },
19
32
  "domains": {
20
33
  "type": "array",
21
34
  "items": {
@@ -8,6 +8,7 @@ import { Ajv } from "ajv";
8
8
  import { createTwoFilesPatch } from "diff";
9
9
  const DEFAULT_RULESET_NAME = "agent-ruleset.json";
10
10
  const DEFAULT_OUTPUT = "AGENTS.md";
11
+ const DEFAULT_CLAUDE_OUTPUT = "CLAUDE.md";
11
12
  const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd", "cache");
12
13
  const DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), ".agentsmd", "workspace");
13
14
  const DEFAULT_INIT_SOURCE = "github:owner/repo@latest";
@@ -276,6 +277,15 @@ const readProjectRuleset = (rulesetPath) => {
276
277
  if (ruleset.output === undefined) {
277
278
  ruleset.output = DEFAULT_OUTPUT;
278
279
  }
280
+ if (ruleset.claude === undefined) {
281
+ ruleset.claude = {};
282
+ }
283
+ if (ruleset.claude.enabled === undefined) {
284
+ ruleset.claude.enabled = true;
285
+ }
286
+ if (ruleset.claude.output === undefined) {
287
+ ruleset.claude.output = DEFAULT_CLAUDE_OUTPUT;
288
+ }
279
289
  if (ruleset.global === undefined) {
280
290
  ruleset.global = true;
281
291
  }
@@ -516,12 +526,25 @@ const formatRuleSourcePath = (rulePath, rulesRoot, rulesetDir, source, resolvedR
516
526
  const result = normalizePath(path.relative(rulesetDir, rulePath));
517
527
  return result;
518
528
  };
529
+ const resolveOutputPaths = (rulesetDir, projectRuleset) => {
530
+ const primaryOutputPath = resolveFrom(rulesetDir, projectRuleset.output ?? DEFAULT_OUTPUT);
531
+ const claude = projectRuleset.claude ?? {};
532
+ const companionEnabled = claude.enabled !== false;
533
+ const configuredCompanionPath = resolveFrom(rulesetDir, claude.output ?? DEFAULT_CLAUDE_OUTPUT);
534
+ if (!companionEnabled || path.resolve(primaryOutputPath) === path.resolve(configuredCompanionPath)) {
535
+ return { primaryOutputPath };
536
+ }
537
+ return { primaryOutputPath, companionOutputPath: configuredCompanionPath };
538
+ };
539
+ const buildClaudeCompanionContent = (primaryOutputPath, companionOutputPath) => {
540
+ const relativeImportPath = normalizePath(path.relative(path.dirname(companionOutputPath), primaryOutputPath));
541
+ return `@${relativeImportPath}\n`;
542
+ };
519
543
  const composeRuleset = (rulesetPath, rootDir, options) => {
520
544
  const rulesetDir = path.dirname(rulesetPath);
521
545
  const projectRuleset = readProjectRuleset(rulesetPath);
522
- const outputFileName = projectRuleset.output ?? DEFAULT_OUTPUT;
523
- const outputPath = resolveFrom(rulesetDir, outputFileName);
524
- const composedOutputPath = normalizePath(path.relative(rootDir, outputPath));
546
+ const { primaryOutputPath, companionOutputPath } = resolveOutputPaths(rulesetDir, projectRuleset);
547
+ const composedOutputPath = normalizePath(path.relative(rootDir, primaryOutputPath));
525
548
  const { rulesRoot, resolvedRef } = resolveRulesRoot(rulesetDir, projectRuleset.source, options.refresh ?? false);
526
549
  const globalRoot = path.join(rulesRoot, "global");
527
550
  const domainsRoot = path.join(rulesRoot, "domains");
@@ -545,25 +568,45 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
545
568
  });
546
569
  const lintHeader = "<!-- markdownlint-disable MD025 -->";
547
570
  const toolRules = normalizeTrailingWhitespace(TOOL_RULES);
548
- const output = `${lintHeader}\n${[toolRules, ...parts].join("\n\n")}\n`;
571
+ const primaryOutputContent = `${lintHeader}\n${[toolRules, ...parts].join("\n\n")}\n`;
572
+ const composedFiles = [
573
+ {
574
+ absolutePath: primaryOutputPath,
575
+ relativePath: composedOutputPath,
576
+ content: primaryOutputContent
577
+ }
578
+ ];
579
+ if (companionOutputPath) {
580
+ composedFiles.push({
581
+ absolutePath: companionOutputPath,
582
+ relativePath: normalizePath(path.relative(rootDir, companionOutputPath)),
583
+ content: buildClaudeCompanionContent(primaryOutputPath, companionOutputPath)
584
+ });
585
+ }
549
586
  let agentsMdDiff;
550
- if (options.emitAgentsMdDiff && path.basename(outputPath) === DEFAULT_OUTPUT) {
551
- const before = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, "utf8") : "";
552
- if (before === output) {
587
+ if (options.emitAgentsMdDiff && path.basename(primaryOutputPath) === DEFAULT_OUTPUT) {
588
+ const before = fs.existsSync(primaryOutputPath) ? fs.readFileSync(primaryOutputPath, "utf8") : "";
589
+ if (before === primaryOutputContent) {
553
590
  agentsMdDiff = { status: "unchanged" };
554
591
  }
555
592
  else {
556
593
  agentsMdDiff = {
557
594
  status: "updated",
558
- patch: createTwoFilesPatch(`a/${composedOutputPath}`, `b/${composedOutputPath}`, before, output, "", "", { context: 3 })
595
+ patch: createTwoFilesPatch(`a/${composedOutputPath}`, `b/${composedOutputPath}`, before, primaryOutputContent, "", "", { context: 3 })
559
596
  };
560
597
  }
561
598
  }
562
599
  if (!options.dryRun) {
563
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
564
- fs.writeFileSync(outputPath, output, "utf8");
600
+ for (const file of composedFiles) {
601
+ fs.mkdirSync(path.dirname(file.absolutePath), { recursive: true });
602
+ fs.writeFileSync(file.absolutePath, file.content, "utf8");
603
+ }
565
604
  }
566
- return { output: composedOutputPath, agentsMdDiff };
605
+ return {
606
+ output: composedOutputPath,
607
+ outputs: composedFiles.map((file) => file.relativePath),
608
+ agentsMdDiff
609
+ };
567
610
  };
568
611
  const printAgentsMdDiffIfPresent = (result) => {
569
612
  if (!result.agentsMdDiff) {
@@ -589,7 +632,11 @@ const buildInitRuleset = (args) => {
589
632
  const extra = normalizeListOption(args.extra, "--extra");
590
633
  const ruleset = {
591
634
  source: args.source ?? DEFAULT_INIT_SOURCE,
592
- output: args.output ?? DEFAULT_OUTPUT
635
+ output: args.output ?? DEFAULT_OUTPUT,
636
+ claude: {
637
+ enabled: true,
638
+ output: DEFAULT_CLAUDE_OUTPUT
639
+ }
593
640
  };
594
641
  if (args.global === false) {
595
642
  ruleset.global = false;
@@ -607,6 +654,8 @@ const buildInitRuleset = (args) => {
607
654
  const formatInitRuleset = (ruleset) => {
608
655
  const domainsValue = JSON.stringify(ruleset.domains ?? []);
609
656
  const extraValue = JSON.stringify(ruleset.extra ?? []);
657
+ const claudeEnabled = ruleset.claude?.enabled ?? true;
658
+ const claudeOutput = ruleset.claude?.output ?? DEFAULT_CLAUDE_OUTPUT;
610
659
  const lines = [
611
660
  "{",
612
661
  ' // Rules source. Use github:owner/repo@ref or a local path.',
@@ -620,6 +669,11 @@ const formatInitRuleset = (ruleset) => {
620
669
  lines.push(' // Include rules/global from the source.');
621
670
  lines.push(' "global": false,');
622
671
  }
672
+ lines.push(' // Claude Code companion output settings.');
673
+ lines.push(' "claude": {');
674
+ lines.push(` "enabled": ${claudeEnabled ? "true" : "false"},`);
675
+ lines.push(` "output": "${claudeOutput}"`);
676
+ lines.push(" },");
623
677
  lines.push(' // Output file name.');
624
678
  lines.push(` "output": "${ruleset.output ?? DEFAULT_OUTPUT}"`);
625
679
  lines.push("}");
@@ -649,7 +703,7 @@ const initProject = async (args, rootDir, rulesetName) => {
649
703
  const rulesetPath = args.ruleset ? resolveFrom(rootDir, args.ruleset) : path.join(rootDir, rulesetName);
650
704
  const rulesetDir = path.dirname(rulesetPath);
651
705
  const ruleset = buildInitRuleset(args);
652
- const outputPath = resolveFrom(rulesetDir, ruleset.output ?? DEFAULT_OUTPUT);
706
+ const outputPaths = resolveOutputPaths(rulesetDir, ruleset);
653
707
  const plan = [];
654
708
  if (fs.existsSync(rulesetPath)) {
655
709
  if (!args.force) {
@@ -674,14 +728,20 @@ const initProject = async (args, rootDir, rulesetName) => {
674
728
  extraToWrite.push(extraPath);
675
729
  }
676
730
  if (args.compose) {
677
- if (fs.existsSync(outputPath)) {
678
- if (!args.force) {
679
- throw new Error(`Output already exists: ${normalizePath(outputPath)} (use --force to overwrite)`);
731
+ const composedTargets = [outputPaths.primaryOutputPath];
732
+ if (outputPaths.companionOutputPath) {
733
+ composedTargets.push(outputPaths.companionOutputPath);
734
+ }
735
+ for (const composedTarget of composedTargets) {
736
+ if (fs.existsSync(composedTarget)) {
737
+ if (!args.force) {
738
+ throw new Error(`Output already exists: ${normalizePath(composedTarget)} (use --force to overwrite)`);
739
+ }
740
+ plan.push({ action: "overwrite", path: composedTarget });
741
+ }
742
+ else {
743
+ plan.push({ action: "create", path: composedTarget });
680
744
  }
681
- plan.push({ action: "overwrite", path: outputPath });
682
- }
683
- else {
684
- plan.push({ action: "create", path: outputPath });
685
745
  }
686
746
  }
687
747
  if (!args.quiet && !args.json) {
@@ -720,7 +780,7 @@ const initProject = async (args, rootDir, rulesetName) => {
720
780
  process.stdout.write(JSON.stringify({
721
781
  initialized: [normalizePath(path.relative(rootDir, rulesetPath))],
722
782
  localRules: extraToWrite.map((filePath) => normalizePath(path.relative(rootDir, filePath))),
723
- composed: composedOutput ? [composedOutput.output] : [],
783
+ composed: composedOutput ? composedOutput.outputs : [],
724
784
  dryRun: false
725
785
  }, null, 2) + "\n");
726
786
  }
@@ -732,7 +792,7 @@ const initProject = async (args, rootDir, rulesetName) => {
732
792
  .join("\n")}\n`);
733
793
  }
734
794
  if (composedOutput) {
735
- process.stdout.write(`Composed AGENTS.md:\n- ${composedOutput.output}\n`);
795
+ process.stdout.write(`Composed AGENTS.md:\n${composedOutput.outputs.map((filePath) => `- ${filePath}`).join("\n")}\n`);
736
796
  printAgentsMdDiffIfPresent(composedOutput);
737
797
  }
738
798
  }
@@ -820,10 +880,10 @@ const main = async () => {
820
880
  emitAgentsMdDiff: !args.quiet && !args.json
821
881
  });
822
882
  if (args.json) {
823
- process.stdout.write(JSON.stringify({ composed: [output.output], dryRun: !!args.dryRun }, null, 2) + "\n");
883
+ process.stdout.write(JSON.stringify({ composed: output.outputs, dryRun: !!args.dryRun }, null, 2) + "\n");
824
884
  }
825
885
  else if (!args.quiet) {
826
- process.stdout.write(`Composed AGENTS.md:\n- ${output.output}\n`);
886
+ process.stdout.write(`Composed AGENTS.md:\n${output.outputs.map((filePath) => `- ${filePath}`).join("\n")}\n`);
827
887
  printAgentsMdDiffIfPresent(output);
828
888
  }
829
889
  return;
@@ -840,10 +900,10 @@ const main = async () => {
840
900
  emitAgentsMdDiff: !args.quiet && !args.json
841
901
  }));
842
902
  if (args.json) {
843
- process.stdout.write(JSON.stringify({ composed: outputs.map((result) => result.output), dryRun: !!args.dryRun }, null, 2) + "\n");
903
+ process.stdout.write(JSON.stringify({ composed: outputs.flatMap((result) => result.outputs), dryRun: !!args.dryRun }, null, 2) + "\n");
844
904
  }
845
905
  else if (!args.quiet) {
846
- process.stdout.write(`Composed AGENTS.md:\n${outputs.map((result) => `- ${result.output}`).join("\n")}\n`);
906
+ process.stdout.write(`Composed AGENTS.md:\n${outputs.flatMap((result) => result.outputs).map((filePath) => `- ${filePath}`).join("\n")}\n`);
847
907
  for (const result of outputs) {
848
908
  printAgentsMdDiffIfPresent(result);
849
909
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compose-agentsmd",
3
- "version": "3.2.7",
3
+ "version": "3.3.0",
4
4
  "description": "CLI tools for composing per-project AGENTS.md files from modular rule sets",
5
5
  "license": "MIT",
6
6
  "repository": {
package/tools/usage.txt CHANGED
@@ -16,7 +16,7 @@ Options:
16
16
  --no-domains Initialize with no domains
17
17
  --no-extra Initialize without extra rule files
18
18
  --no-global Initialize without global rules
19
- --compose Compose AGENTS.md after init
19
+ --compose Compose output file(s) after init
20
20
  --dry-run Show init plan without writing files
21
21
  --yes Skip init confirmation prompt
22
22
  --force Overwrite existing files during init