compose-agentsmd 3.2.6 → 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,9 +32,13 @@ 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
 
40
+ When the output file is `AGENTS.md`, the CLI also prints a unified diff for `AGENTS.md` when it changes (and prints `AGENTS.md unchanged.` when it does not). This works even when the project is not under git. `--quiet` and `--json` suppress this output.
41
+
38
42
  ## Setup (init)
39
43
 
40
44
  For a project that does not have a ruleset yet, bootstrap one with `init`:
@@ -49,6 +53,7 @@ Defaults:
49
53
  - `domains`: empty
50
54
  - `extra`: empty
51
55
  - `global`: omitted (defaults to `true`)
56
+ - `claude`: `{ "enabled": true, "output": "CLAUDE.md" }`
52
57
  - `output`: `AGENTS.md`
53
58
 
54
59
  Use `--dry-run` to preview actions, `--force` to overwrite existing files, and `--compose` to generate `AGENTS.md` immediately.
@@ -82,6 +87,11 @@ Ruleset files accept JSON with `//` or `/* */` comments.
82
87
  "domains": ["node", "unreal"],
83
88
  // Additional local rule files to append.
84
89
  "extra": ["agent-rules-local/custom.md"],
90
+ // Optional Claude Code companion output.
91
+ "claude": {
92
+ "enabled": true,
93
+ "output": "CLAUDE.md"
94
+ },
85
95
  // Output file name.
86
96
  "output": "AGENTS.md"
87
97
  }
@@ -93,6 +103,9 @@ Ruleset keys:
93
103
  - `global` (optional): include `rules/global` (defaults to true). Omit this unless you want to disable globals.
94
104
  - `domains` (optional): domain folders under `rules/domains/<domain>`.
95
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`).
96
109
  - `output` (optional): output file name (defaults to `AGENTS.md`).
97
110
 
98
111
  ### Ruleset schema validation
@@ -119,7 +132,7 @@ Remote sources are cached under `~/.agentsmd/cache/<owner>/<repo>/<ref>/`. Use `
119
132
  - `--no-domains`: initialize with no domains
120
133
  - `--no-extra`: initialize without extra rule files
121
134
  - `--no-global`: initialize without global rules
122
- - `--compose`: compose `AGENTS.md` after `init`
135
+ - `--compose`: compose output file(s) after `init`
123
136
  - `--dry-run`: show init plan without writing files
124
137
  - `--yes`: skip init confirmation prompt
125
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": {
@@ -5,8 +5,10 @@ import os from "node:os";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import readline from "node:readline";
7
7
  import { Ajv } from "ajv";
8
+ import { createTwoFilesPatch } from "diff";
8
9
  const DEFAULT_RULESET_NAME = "agent-ruleset.json";
9
10
  const DEFAULT_OUTPUT = "AGENTS.md";
11
+ const DEFAULT_CLAUDE_OUTPUT = "CLAUDE.md";
10
12
  const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd", "cache");
11
13
  const DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), ".agentsmd", "workspace");
12
14
  const DEFAULT_INIT_SOURCE = "github:owner/repo@latest";
@@ -275,6 +277,15 @@ const readProjectRuleset = (rulesetPath) => {
275
277
  if (ruleset.output === undefined) {
276
278
  ruleset.output = DEFAULT_OUTPUT;
277
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
+ }
278
289
  if (ruleset.global === undefined) {
279
290
  ruleset.global = true;
280
291
  }
@@ -515,11 +526,25 @@ const formatRuleSourcePath = (rulePath, rulesRoot, rulesetDir, source, resolvedR
515
526
  const result = normalizePath(path.relative(rulesetDir, rulePath));
516
527
  return result;
517
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
+ };
518
543
  const composeRuleset = (rulesetPath, rootDir, options) => {
519
544
  const rulesetDir = path.dirname(rulesetPath);
520
545
  const projectRuleset = readProjectRuleset(rulesetPath);
521
- const outputFileName = projectRuleset.output ?? DEFAULT_OUTPUT;
522
- const outputPath = resolveFrom(rulesetDir, outputFileName);
546
+ const { primaryOutputPath, companionOutputPath } = resolveOutputPaths(rulesetDir, projectRuleset);
547
+ const composedOutputPath = normalizePath(path.relative(rootDir, primaryOutputPath));
523
548
  const { rulesRoot, resolvedRef } = resolveRulesRoot(rulesetDir, projectRuleset.source, options.refresh ?? false);
524
549
  const globalRoot = path.join(rulesRoot, "global");
525
550
  const domainsRoot = path.join(rulesRoot, "domains");
@@ -543,12 +568,63 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
543
568
  });
544
569
  const lintHeader = "<!-- markdownlint-disable MD025 -->";
545
570
  const toolRules = normalizeTrailingWhitespace(TOOL_RULES);
546
- 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
+ }
586
+ let agentsMdDiff;
587
+ if (options.emitAgentsMdDiff && path.basename(primaryOutputPath) === DEFAULT_OUTPUT) {
588
+ const before = fs.existsSync(primaryOutputPath) ? fs.readFileSync(primaryOutputPath, "utf8") : "";
589
+ if (before === primaryOutputContent) {
590
+ agentsMdDiff = { status: "unchanged" };
591
+ }
592
+ else {
593
+ agentsMdDiff = {
594
+ status: "updated",
595
+ patch: createTwoFilesPatch(`a/${composedOutputPath}`, `b/${composedOutputPath}`, before, primaryOutputContent, "", "", { context: 3 })
596
+ };
597
+ }
598
+ }
547
599
  if (!options.dryRun) {
548
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
549
- 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
+ }
550
604
  }
551
- return normalizePath(path.relative(rootDir, outputPath));
605
+ return {
606
+ output: composedOutputPath,
607
+ outputs: composedFiles.map((file) => file.relativePath),
608
+ agentsMdDiff
609
+ };
610
+ };
611
+ const printAgentsMdDiffIfPresent = (result) => {
612
+ if (!result.agentsMdDiff) {
613
+ return;
614
+ }
615
+ if (result.agentsMdDiff.status === "unchanged") {
616
+ process.stdout.write("AGENTS.md unchanged.\n");
617
+ return;
618
+ }
619
+ process.stdout.write("AGENTS.md updated. ACTION (agent): refresh rule recognition from the diff below.\n");
620
+ process.stdout.write("--- BEGIN DIFF ---\n");
621
+ if (result.agentsMdDiff.patch) {
622
+ process.stdout.write(result.agentsMdDiff.patch);
623
+ if (!result.agentsMdDiff.patch.endsWith("\n")) {
624
+ process.stdout.write("\n");
625
+ }
626
+ }
627
+ process.stdout.write("--- END DIFF ---\n");
552
628
  };
553
629
  const LOCAL_RULES_TEMPLATE = "# Local Rules\n\n- Add project-specific instructions here.\n";
554
630
  const buildInitRuleset = (args) => {
@@ -556,7 +632,11 @@ const buildInitRuleset = (args) => {
556
632
  const extra = normalizeListOption(args.extra, "--extra");
557
633
  const ruleset = {
558
634
  source: args.source ?? DEFAULT_INIT_SOURCE,
559
- output: args.output ?? DEFAULT_OUTPUT
635
+ output: args.output ?? DEFAULT_OUTPUT,
636
+ claude: {
637
+ enabled: true,
638
+ output: DEFAULT_CLAUDE_OUTPUT
639
+ }
560
640
  };
561
641
  if (args.global === false) {
562
642
  ruleset.global = false;
@@ -574,6 +654,8 @@ const buildInitRuleset = (args) => {
574
654
  const formatInitRuleset = (ruleset) => {
575
655
  const domainsValue = JSON.stringify(ruleset.domains ?? []);
576
656
  const extraValue = JSON.stringify(ruleset.extra ?? []);
657
+ const claudeEnabled = ruleset.claude?.enabled ?? true;
658
+ const claudeOutput = ruleset.claude?.output ?? DEFAULT_CLAUDE_OUTPUT;
577
659
  const lines = [
578
660
  "{",
579
661
  ' // Rules source. Use github:owner/repo@ref or a local path.',
@@ -587,6 +669,11 @@ const formatInitRuleset = (ruleset) => {
587
669
  lines.push(' // Include rules/global from the source.');
588
670
  lines.push(' "global": false,');
589
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(" },");
590
677
  lines.push(' // Output file name.');
591
678
  lines.push(` "output": "${ruleset.output ?? DEFAULT_OUTPUT}"`);
592
679
  lines.push("}");
@@ -616,7 +703,7 @@ const initProject = async (args, rootDir, rulesetName) => {
616
703
  const rulesetPath = args.ruleset ? resolveFrom(rootDir, args.ruleset) : path.join(rootDir, rulesetName);
617
704
  const rulesetDir = path.dirname(rulesetPath);
618
705
  const ruleset = buildInitRuleset(args);
619
- const outputPath = resolveFrom(rulesetDir, ruleset.output ?? DEFAULT_OUTPUT);
706
+ const outputPaths = resolveOutputPaths(rulesetDir, ruleset);
620
707
  const plan = [];
621
708
  if (fs.existsSync(rulesetPath)) {
622
709
  if (!args.force) {
@@ -641,14 +728,20 @@ const initProject = async (args, rootDir, rulesetName) => {
641
728
  extraToWrite.push(extraPath);
642
729
  }
643
730
  if (args.compose) {
644
- if (fs.existsSync(outputPath)) {
645
- if (!args.force) {
646
- 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 });
647
744
  }
648
- plan.push({ action: "overwrite", path: outputPath });
649
- }
650
- else {
651
- plan.push({ action: "create", path: outputPath });
652
745
  }
653
746
  }
654
747
  if (!args.quiet && !args.json) {
@@ -678,13 +771,16 @@ const initProject = async (args, rootDir, rulesetName) => {
678
771
  }
679
772
  let composedOutput;
680
773
  if (args.compose) {
681
- composedOutput = composeRuleset(rulesetPath, rootDir, { refresh: args.refresh ?? false });
774
+ composedOutput = composeRuleset(rulesetPath, rootDir, {
775
+ refresh: args.refresh ?? false,
776
+ emitAgentsMdDiff: !args.quiet && !args.json
777
+ });
682
778
  }
683
779
  if (args.json) {
684
780
  process.stdout.write(JSON.stringify({
685
781
  initialized: [normalizePath(path.relative(rootDir, rulesetPath))],
686
782
  localRules: extraToWrite.map((filePath) => normalizePath(path.relative(rootDir, filePath))),
687
- composed: composedOutput ? [composedOutput] : [],
783
+ composed: composedOutput ? composedOutput.outputs : [],
688
784
  dryRun: false
689
785
  }, null, 2) + "\n");
690
786
  }
@@ -696,7 +792,8 @@ const initProject = async (args, rootDir, rulesetName) => {
696
792
  .join("\n")}\n`);
697
793
  }
698
794
  if (composedOutput) {
699
- process.stdout.write(`Composed AGENTS.md:\n- ${composedOutput}\n`);
795
+ process.stdout.write(`Composed AGENTS.md:\n${composedOutput.outputs.map((filePath) => `- ${filePath}`).join("\n")}\n`);
796
+ printAgentsMdDiffIfPresent(composedOutput);
700
797
  }
701
798
  }
702
799
  };
@@ -777,12 +874,17 @@ const main = async () => {
777
874
  const rulesetDir = path.dirname(rulesetPath);
778
875
  const ruleset = readProjectRuleset(rulesetPath);
779
876
  applyRulesFromWorkspace(rulesetDir, ruleset.source);
780
- const output = composeRuleset(rulesetPath, rootDir, { refresh: true, dryRun: args.dryRun });
877
+ const output = composeRuleset(rulesetPath, rootDir, {
878
+ refresh: true,
879
+ dryRun: args.dryRun,
880
+ emitAgentsMdDiff: !args.quiet && !args.json
881
+ });
781
882
  if (args.json) {
782
- process.stdout.write(JSON.stringify({ composed: [output], dryRun: !!args.dryRun }, null, 2) + "\n");
883
+ process.stdout.write(JSON.stringify({ composed: output.outputs, dryRun: !!args.dryRun }, null, 2) + "\n");
783
884
  }
784
885
  else if (!args.quiet) {
785
- process.stdout.write(`Composed AGENTS.md:\n- ${output}\n`);
886
+ process.stdout.write(`Composed AGENTS.md:\n${output.outputs.map((filePath) => `- ${filePath}`).join("\n")}\n`);
887
+ printAgentsMdDiffIfPresent(output);
786
888
  }
787
889
  return;
788
890
  }
@@ -792,12 +894,19 @@ const main = async () => {
792
894
  }
793
895
  const outputs = rulesetFiles
794
896
  .sort()
795
- .map((rulesetPath) => composeRuleset(rulesetPath, rootDir, { refresh: args.refresh, dryRun: args.dryRun }));
897
+ .map((rulesetPath) => composeRuleset(rulesetPath, rootDir, {
898
+ refresh: args.refresh,
899
+ dryRun: args.dryRun,
900
+ emitAgentsMdDiff: !args.quiet && !args.json
901
+ }));
796
902
  if (args.json) {
797
- process.stdout.write(JSON.stringify({ composed: outputs, dryRun: !!args.dryRun }, null, 2) + "\n");
903
+ process.stdout.write(JSON.stringify({ composed: outputs.flatMap((result) => result.outputs), dryRun: !!args.dryRun }, null, 2) + "\n");
798
904
  }
799
905
  else if (!args.quiet) {
800
- process.stdout.write(`Composed AGENTS.md:\n${outputs.map((file) => `- ${file}`).join("\n")}\n`);
906
+ process.stdout.write(`Composed AGENTS.md:\n${outputs.flatMap((result) => result.outputs).map((filePath) => `- ${filePath}`).join("\n")}\n`);
907
+ for (const result of outputs) {
908
+ printAgentsMdDiffIfPresent(result);
909
+ }
801
910
  }
802
911
  };
803
912
  const run = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compose-agentsmd",
3
- "version": "3.2.6",
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": {
@@ -48,6 +48,7 @@
48
48
  "typescript": "^5.7.3"
49
49
  },
50
50
  "dependencies": {
51
- "ajv": "^8.17.1"
51
+ "ajv": "^8.17.1",
52
+ "diff": "^8.0.3"
52
53
  }
53
54
  }
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