compose-agentsmd 1.0.0 → 1.1.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 +10 -0
- package/agent-ruleset.schema.json +38 -0
- package/dist/compose-agents.js +58 -57
- package/package.json +13 -3
- package/tools/compose-agents.js +2 -0
- package/tools/compose-agents.cjs +0 -2
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
This repository contains CLI tooling for composing per-project `AGENTS.md` files from modular rule sets.
|
|
4
4
|
|
|
5
|
+
## Release notes
|
|
6
|
+
|
|
7
|
+
See `CHANGELOG.md` for release notes.
|
|
8
|
+
|
|
5
9
|
It is intended to be used together with shared rule modules such as:
|
|
6
10
|
|
|
7
11
|
- `agent-rules/` (public rule modules)
|
|
@@ -41,6 +45,8 @@ compose-agentsmd
|
|
|
41
45
|
|
|
42
46
|
The tool searches for `agent-ruleset.json` under the given root directory (default: current working directory), and writes output files as specified by each ruleset. If `output` is omitted, it defaults to `AGENTS.md`.
|
|
43
47
|
|
|
48
|
+
The tool prepends a small "Tool Rules" block to every generated `AGENTS.md` so agents know how to regenerate or update rules.
|
|
49
|
+
|
|
44
50
|
### Rules root resolution (important for global installs)
|
|
45
51
|
|
|
46
52
|
When installed globally, the rules directory is usually outside the project. You can point to it in either of the following ways:
|
|
@@ -78,6 +84,10 @@ Rules root resolution precedence is:
|
|
|
78
84
|
- `domains` selects domain folders under `agent-rules/rules/domains`.
|
|
79
85
|
- `rules` is optional and appends additional rule files.
|
|
80
86
|
|
|
87
|
+
### Ruleset schema validation
|
|
88
|
+
|
|
89
|
+
`compose-agentsmd` validates rulesets against `agent-ruleset.schema.json` on every run. If the ruleset does not conform to the schema, the tool exits with a schema error.
|
|
90
|
+
|
|
81
91
|
Optional path overrides:
|
|
82
92
|
|
|
83
93
|
- `rulesRoot`: override `agent-rules/rules`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Compose AGENTS.md ruleset",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"output": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"minLength": 1
|
|
10
|
+
},
|
|
11
|
+
"domains": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"minLength": 1
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"rules": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"rulesRoot": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1
|
|
28
|
+
},
|
|
29
|
+
"globalDir": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 1
|
|
32
|
+
},
|
|
33
|
+
"domainsDir": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/compose-agents.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Ajv } from "ajv";
|
|
9
5
|
const DEFAULT_RULESET_NAME = "agent-ruleset.json";
|
|
10
6
|
const DEFAULT_RULES_ROOT = "agent-rules/rules";
|
|
11
7
|
const DEFAULT_GLOBAL_DIR = "global";
|
|
12
8
|
const DEFAULT_DOMAINS_DIR = "domains";
|
|
13
9
|
const RULES_ROOT_ENV_VAR = "AGENT_RULES_ROOT";
|
|
14
10
|
const DEFAULT_OUTPUT = "AGENTS.md";
|
|
11
|
+
const RULESET_SCHEMA_PATH = new URL("../agent-ruleset.schema.json", import.meta.url);
|
|
12
|
+
const TOOL_RULES = [
|
|
13
|
+
"# Tool Rules (compose-agentsmd)",
|
|
14
|
+
"- Before starting any work, run `compose-agentsmd` from the project root.",
|
|
15
|
+
"- To update rules, update the source rule modules or ruleset, then rerun `compose-agentsmd`.",
|
|
16
|
+
"- Do not edit `AGENTS.md` directly; update the source rules and regenerate."
|
|
17
|
+
].join("\n");
|
|
15
18
|
const DEFAULT_IGNORE_DIRS = new Set([
|
|
16
19
|
".git",
|
|
17
20
|
"agent-rules",
|
|
@@ -86,59 +89,56 @@ const parseArgs = (argv) => {
|
|
|
86
89
|
const normalizeTrailingWhitespace = (content) => content.replace(/\s+$/u, "");
|
|
87
90
|
const normalizePath = (filePath) => filePath.replace(/\\/g, "/");
|
|
88
91
|
const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
|
|
92
|
+
const rulesetSchema = JSON.parse(fs.readFileSync(RULESET_SCHEMA_PATH, "utf8"));
|
|
93
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
94
|
+
const validateRulesetSchema = ajv.compile(rulesetSchema);
|
|
95
|
+
const formatSchemaErrors = (errors) => {
|
|
96
|
+
if (!errors || errors.length === 0) {
|
|
97
|
+
return "Unknown schema validation error";
|
|
98
|
+
}
|
|
99
|
+
return errors
|
|
100
|
+
.map((error) => {
|
|
101
|
+
const pathLabel = error.instancePath ? error.instancePath : "(root)";
|
|
102
|
+
return `${pathLabel} ${error.message ?? "is invalid"}`;
|
|
103
|
+
})
|
|
104
|
+
.join("; ");
|
|
105
|
+
};
|
|
89
106
|
const resolveFrom = (baseDir, targetPath) => {
|
|
90
|
-
if (
|
|
107
|
+
if (path.isAbsolute(targetPath)) {
|
|
91
108
|
return targetPath;
|
|
92
109
|
}
|
|
93
|
-
return
|
|
110
|
+
return path.resolve(baseDir, targetPath);
|
|
94
111
|
};
|
|
95
112
|
const ensureFileExists = (filePath) => {
|
|
96
|
-
if (!
|
|
113
|
+
if (!fs.existsSync(filePath)) {
|
|
97
114
|
throw new Error(`Missing file: ${filePath}`);
|
|
98
115
|
}
|
|
99
116
|
};
|
|
100
117
|
const ensureDirectoryExists = (dirPath) => {
|
|
101
|
-
if (!
|
|
118
|
+
if (!fs.existsSync(dirPath)) {
|
|
102
119
|
throw new Error(`Missing directory: ${dirPath}`);
|
|
103
120
|
}
|
|
104
|
-
const stat =
|
|
121
|
+
const stat = fs.statSync(dirPath);
|
|
105
122
|
if (!stat.isDirectory()) {
|
|
106
123
|
throw new Error(`Not a directory: ${dirPath}`);
|
|
107
124
|
}
|
|
108
125
|
};
|
|
109
126
|
const readJsonFile = (filePath) => {
|
|
110
|
-
const raw =
|
|
127
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
111
128
|
return JSON.parse(raw);
|
|
112
129
|
};
|
|
113
130
|
const readProjectRuleset = (rulesetPath) => {
|
|
114
131
|
const parsed = readJsonFile(rulesetPath);
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
const isValid = validateRulesetSchema(parsed);
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
const message = formatSchemaErrors(validateRulesetSchema.errors);
|
|
135
|
+
throw new Error(`Invalid ruleset schema in ${rulesetPath}: ${message}`);
|
|
117
136
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (parsed.domains !== undefined) {
|
|
122
|
-
if (!Array.isArray(parsed.domains)) {
|
|
123
|
-
throw new Error(`"domains" must be an array in ${rulesetPath}`);
|
|
124
|
-
}
|
|
125
|
-
for (const domain of parsed.domains) {
|
|
126
|
-
if (!isNonEmptyString(domain)) {
|
|
127
|
-
throw new Error(`"domains" entries must be non-empty strings in ${rulesetPath}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (parsed.rules !== undefined) {
|
|
132
|
-
if (!Array.isArray(parsed.rules)) {
|
|
133
|
-
throw new Error(`"rules" must be an array in ${rulesetPath}`);
|
|
134
|
-
}
|
|
135
|
-
for (const rule of parsed.rules) {
|
|
136
|
-
if (!isNonEmptyString(rule)) {
|
|
137
|
-
throw new Error(`"rules" entries must be non-empty strings in ${rulesetPath}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
137
|
+
const ruleset = parsed;
|
|
138
|
+
if (ruleset.output === undefined) {
|
|
139
|
+
ruleset.output = DEFAULT_OUTPUT;
|
|
140
140
|
}
|
|
141
|
-
return
|
|
141
|
+
return ruleset;
|
|
142
142
|
};
|
|
143
143
|
const resolveRulesRoot = (rulesetDir, projectRuleset, options) => {
|
|
144
144
|
if (isNonEmptyString(options.cliRulesRoot)) {
|
|
@@ -151,19 +151,19 @@ const resolveRulesRoot = (rulesetDir, projectRuleset, options) => {
|
|
|
151
151
|
if (isNonEmptyString(projectRuleset.rulesRoot)) {
|
|
152
152
|
return resolveFrom(rulesetDir, projectRuleset.rulesRoot);
|
|
153
153
|
}
|
|
154
|
-
return
|
|
154
|
+
return path.resolve(rulesetDir, DEFAULT_RULES_ROOT);
|
|
155
155
|
};
|
|
156
156
|
const resolveGlobalRoot = (rulesRoot, projectRuleset) => {
|
|
157
157
|
const globalDirName = isNonEmptyString(projectRuleset.globalDir)
|
|
158
158
|
? projectRuleset.globalDir
|
|
159
159
|
: DEFAULT_GLOBAL_DIR;
|
|
160
|
-
return
|
|
160
|
+
return path.resolve(rulesRoot, globalDirName);
|
|
161
161
|
};
|
|
162
162
|
const resolveDomainsRoot = (rulesRoot, projectRuleset) => {
|
|
163
163
|
const domainsDirName = isNonEmptyString(projectRuleset.domainsDir)
|
|
164
164
|
? projectRuleset.domainsDir
|
|
165
165
|
: DEFAULT_DOMAINS_DIR;
|
|
166
|
-
return
|
|
166
|
+
return path.resolve(rulesRoot, domainsDirName);
|
|
167
167
|
};
|
|
168
168
|
const collectMarkdownFiles = (rootDir) => {
|
|
169
169
|
ensureDirectoryExists(rootDir);
|
|
@@ -174,27 +174,27 @@ const collectMarkdownFiles = (rootDir) => {
|
|
|
174
174
|
if (!currentDir) {
|
|
175
175
|
continue;
|
|
176
176
|
}
|
|
177
|
-
const entries =
|
|
177
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
178
178
|
for (const entry of entries) {
|
|
179
|
-
const entryPath =
|
|
179
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
180
180
|
if (entry.isDirectory()) {
|
|
181
181
|
pending.push(entryPath);
|
|
182
182
|
continue;
|
|
183
183
|
}
|
|
184
|
-
if (entry.isFile() &&
|
|
184
|
+
if (entry.isFile() && path.extname(entry.name).toLowerCase() === ".md") {
|
|
185
185
|
results.push(entryPath);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
return results.sort((a, b) => {
|
|
190
|
-
const relA = normalizePath(
|
|
191
|
-
const relB = normalizePath(
|
|
190
|
+
const relA = normalizePath(path.relative(rootDir, a));
|
|
191
|
+
const relB = normalizePath(path.relative(rootDir, b));
|
|
192
192
|
return relA.localeCompare(relB);
|
|
193
193
|
});
|
|
194
194
|
};
|
|
195
195
|
const addRulePaths = (rulePaths, resolvedRules, seenRules) => {
|
|
196
196
|
for (const rulePath of rulePaths) {
|
|
197
|
-
const resolvedRulePath =
|
|
197
|
+
const resolvedRulePath = path.resolve(rulePath);
|
|
198
198
|
if (seenRules.has(resolvedRulePath)) {
|
|
199
199
|
continue;
|
|
200
200
|
}
|
|
@@ -204,7 +204,7 @@ const addRulePaths = (rulePaths, resolvedRules, seenRules) => {
|
|
|
204
204
|
}
|
|
205
205
|
};
|
|
206
206
|
const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
207
|
-
const rulesetDir =
|
|
207
|
+
const rulesetDir = path.dirname(rulesetPath);
|
|
208
208
|
const projectRuleset = readProjectRuleset(rulesetPath);
|
|
209
209
|
const outputFileName = projectRuleset.output ?? DEFAULT_OUTPUT;
|
|
210
210
|
const outputPath = resolveFrom(rulesetDir, outputFileName);
|
|
@@ -219,18 +219,19 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
|
219
219
|
addRulePaths(collectMarkdownFiles(globalRoot), resolvedRules, seenRules);
|
|
220
220
|
const domains = Array.isArray(projectRuleset.domains) ? projectRuleset.domains : [];
|
|
221
221
|
for (const domain of domains) {
|
|
222
|
-
const domainRoot =
|
|
222
|
+
const domainRoot = path.resolve(domainsRoot, domain);
|
|
223
223
|
addRulePaths(collectMarkdownFiles(domainRoot), resolvedRules, seenRules);
|
|
224
224
|
}
|
|
225
225
|
const directRules = Array.isArray(projectRuleset.rules) ? projectRuleset.rules : [];
|
|
226
226
|
const directRulePaths = directRules.map((rulePath) => resolveFrom(rulesetDir, rulePath));
|
|
227
227
|
addRulePaths(directRulePaths, resolvedRules, seenRules);
|
|
228
|
-
const parts = resolvedRules.map((rulePath) => normalizeTrailingWhitespace(
|
|
228
|
+
const parts = resolvedRules.map((rulePath) => normalizeTrailingWhitespace(fs.readFileSync(rulePath, "utf8")));
|
|
229
229
|
const lintHeader = "<!-- markdownlint-disable MD025 -->";
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
const toolRules = normalizeTrailingWhitespace(TOOL_RULES);
|
|
231
|
+
const output = `${lintHeader}\n${[toolRules, ...parts].join("\n\n")}\n`;
|
|
232
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
233
|
+
fs.writeFileSync(outputPath, output, "utf8");
|
|
234
|
+
return normalizePath(path.relative(rootDir, outputPath));
|
|
234
235
|
};
|
|
235
236
|
const findRulesetFiles = (rootDir, rulesetName) => {
|
|
236
237
|
const results = [];
|
|
@@ -240,9 +241,9 @@ const findRulesetFiles = (rootDir, rulesetName) => {
|
|
|
240
241
|
if (!currentDir) {
|
|
241
242
|
continue;
|
|
242
243
|
}
|
|
243
|
-
const entries =
|
|
244
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
244
245
|
for (const entry of entries) {
|
|
245
|
-
const entryPath =
|
|
246
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
246
247
|
if (entry.isDirectory()) {
|
|
247
248
|
if (DEFAULT_IGNORE_DIRS.has(entry.name)) {
|
|
248
249
|
continue;
|
|
@@ -271,7 +272,7 @@ const main = () => {
|
|
|
271
272
|
process.stdout.write(`${usage}\n`);
|
|
272
273
|
return;
|
|
273
274
|
}
|
|
274
|
-
const rootDir = args.root ?
|
|
275
|
+
const rootDir = args.root ? path.resolve(args.root) : process.cwd();
|
|
275
276
|
const rulesetName = args.rulesetName || DEFAULT_RULESET_NAME;
|
|
276
277
|
const rulesetFiles = getRulesetFiles(rootDir, args.ruleset, rulesetName);
|
|
277
278
|
if (rulesetFiles.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compose-agentsmd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI tools for composing per-project AGENTS.md files from modular rule sets",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,17 +18,24 @@
|
|
|
18
18
|
"cli",
|
|
19
19
|
"markdown"
|
|
20
20
|
],
|
|
21
|
-
"type": "
|
|
21
|
+
"type": "module",
|
|
22
22
|
"bin": {
|
|
23
23
|
"compose-agentsmd": "dist/compose-agents.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"
|
|
27
|
+
"agent-ruleset.schema.json",
|
|
28
|
+
"tools",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
28
31
|
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
29
35
|
"scripts": {
|
|
30
36
|
"build": "tsc -p tsconfig.json",
|
|
31
37
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"prepare": "npm run build",
|
|
32
39
|
"prepack": "npm run build",
|
|
33
40
|
"test": "npm run build && node --test",
|
|
34
41
|
"compose": "npm run build && node dist/compose-agents.js"
|
|
@@ -39,5 +46,8 @@
|
|
|
39
46
|
"devDependencies": {
|
|
40
47
|
"@types/node": "^25.0.10",
|
|
41
48
|
"typescript": "^5.7.3"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"ajv": "^8.17.1"
|
|
42
52
|
}
|
|
43
53
|
}
|
package/tools/compose-agents.cjs
DELETED