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 +14 -1
- package/agent-ruleset.schema.json +13 -0
- package/dist/compose-agents.js +133 -24
- package/package.json +3 -2
- package/tools/usage.txt +1 -1
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
|
|
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": {
|
package/dist/compose-agents.js
CHANGED
|
@@ -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
|
|
522
|
-
const
|
|
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
|
|
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
|
-
|
|
549
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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, {
|
|
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 ?
|
|
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
|
|
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, {
|
|
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:
|
|
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
|
|
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, {
|
|
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((
|
|
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.
|
|
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
|
|
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
|