compose-agentsmd 2.0.1 → 3.0.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 +21 -2
- package/dist/compose-agents.js +89 -66
- package/package.json +1 -1
- package/tools/tool-rules.md +4 -0
- package/tools/usage.txt +8 -0
package/README.md
CHANGED
|
@@ -30,10 +30,27 @@ From each project root, run:
|
|
|
30
30
|
compose-agentsmd
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The tool
|
|
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
35
|
The tool prepends a small "Tool Rules" block to every generated `AGENTS.md` so agents know how to regenerate or update rules.
|
|
36
36
|
|
|
37
|
+
## Updating shared rules
|
|
38
|
+
|
|
39
|
+
For GitHub sources, the tool keeps two locations:
|
|
40
|
+
|
|
41
|
+
- Cache: `~/.agentsmd/cache/<owner>/<repo>/<ref>/` (read-only, used for compose)
|
|
42
|
+
- Workspace: `~/.agentsmd/workspace/<owner>/<repo>/` (editable)
|
|
43
|
+
|
|
44
|
+
Update flow:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
compose-agentsmd edit-rules
|
|
48
|
+
# edit files under rules/ in the workspace
|
|
49
|
+
compose-agentsmd apply-rules
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`edit-rules` clones the GitHub source into the workspace (or reuses it). `apply-rules` pushes the workspace (if clean) and regenerates `AGENTS.md` by refreshing the cache. If your `source` is a local path, `edit-rules` will just print that path and `apply-rules` will skip the push.
|
|
53
|
+
|
|
37
54
|
## Project ruleset format
|
|
38
55
|
|
|
39
56
|
```json
|
|
@@ -59,7 +76,7 @@ Ruleset keys:
|
|
|
59
76
|
|
|
60
77
|
### Cache
|
|
61
78
|
|
|
62
|
-
Remote sources are cached under `~/.agentsmd/<owner>/<repo>/<ref>/`. Use `--refresh` to re-fetch or `--clear-cache` to remove cached rules.
|
|
79
|
+
Remote sources are cached under `~/.agentsmd/cache/<owner>/<repo>/<ref>/`. Use `--refresh` to re-fetch or `--clear-cache` to remove cached rules.
|
|
63
80
|
|
|
64
81
|
### Optional arguments
|
|
65
82
|
|
|
@@ -68,6 +85,8 @@ Remote sources are cached under `~/.agentsmd/<owner>/<repo>/<ref>/`. Use `--refr
|
|
|
68
85
|
- `--ruleset-name <name>`: override the ruleset filename (default: `agent-ruleset.json`)
|
|
69
86
|
- `--refresh`: refresh cached remote rules
|
|
70
87
|
- `--clear-cache`: remove cached remote rules and exit
|
|
88
|
+
- `edit-rules`: prepare or locate a writable rules workspace
|
|
89
|
+
- `apply-rules`: push workspace changes (if GitHub source) and regenerate rules with refresh
|
|
71
90
|
|
|
72
91
|
## Development
|
|
73
92
|
|
package/dist/compose-agents.js
CHANGED
|
@@ -6,48 +6,26 @@ import { execFileSync } from "node:child_process";
|
|
|
6
6
|
import { Ajv } from "ajv";
|
|
7
7
|
const DEFAULT_RULESET_NAME = "agent-ruleset.json";
|
|
8
8
|
const DEFAULT_OUTPUT = "AGENTS.md";
|
|
9
|
-
const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd");
|
|
9
|
+
const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd", "cache");
|
|
10
|
+
const DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), ".agentsmd", "workspace");
|
|
10
11
|
const RULESET_SCHEMA_PATH = new URL("../agent-ruleset.schema.json", import.meta.url);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
"- Before starting any work, run `compose-agentsmd` from the project root.",
|
|
14
|
-
"- To update rules, update the source rule modules or ruleset, then rerun `compose-agentsmd`.",
|
|
15
|
-
"- Do not edit `AGENTS.md` directly; update the source rules and regenerate."
|
|
16
|
-
].join("\n");
|
|
17
|
-
const DEFAULT_IGNORE_DIRS = new Set([
|
|
18
|
-
".git",
|
|
19
|
-
"agent-rules",
|
|
20
|
-
"agent-rules-private",
|
|
21
|
-
"agent-rules-local",
|
|
22
|
-
"agent-rules-tools",
|
|
23
|
-
"compose-agentsmd",
|
|
24
|
-
"node_modules",
|
|
25
|
-
"dist",
|
|
26
|
-
"build",
|
|
27
|
-
"out",
|
|
28
|
-
".next",
|
|
29
|
-
".turbo",
|
|
30
|
-
"coverage"
|
|
31
|
-
]);
|
|
32
|
-
const usage = `Usage: compose-agentsmd [--root <path>] [--ruleset <path>] [--ruleset-name <name>] [--refresh] [--clear-cache]
|
|
33
|
-
|
|
34
|
-
Options:
|
|
35
|
-
--root <path> Project root directory (default: current working directory)
|
|
36
|
-
--ruleset <path> Only compose a single ruleset file
|
|
37
|
-
--ruleset-name <name> Ruleset filename to search for (default: agent-ruleset.json)
|
|
38
|
-
--refresh Refresh cached remote rules
|
|
39
|
-
--clear-cache Remove cached remote rules and exit
|
|
40
|
-
`;
|
|
12
|
+
const TOOL_RULES_PATH = new URL("../tools/tool-rules.md", import.meta.url);
|
|
13
|
+
const USAGE_PATH = new URL("../tools/usage.txt", import.meta.url);
|
|
41
14
|
const parseArgs = (argv) => {
|
|
42
15
|
const args = {};
|
|
43
|
-
|
|
44
|
-
|
|
16
|
+
const knownCommands = new Set(["edit-rules", "apply-rules"]);
|
|
17
|
+
const remaining = [...argv];
|
|
18
|
+
if (remaining.length > 0 && knownCommands.has(remaining[0])) {
|
|
19
|
+
args.command = remaining.shift();
|
|
20
|
+
}
|
|
21
|
+
for (let i = 0; i < remaining.length; i += 1) {
|
|
22
|
+
const arg = remaining[i];
|
|
45
23
|
if (arg === "--help" || arg === "-h") {
|
|
46
24
|
args.help = true;
|
|
47
25
|
continue;
|
|
48
26
|
}
|
|
49
27
|
if (arg === "--root") {
|
|
50
|
-
const value =
|
|
28
|
+
const value = remaining[i + 1];
|
|
51
29
|
if (!value) {
|
|
52
30
|
throw new Error("Missing value for --root");
|
|
53
31
|
}
|
|
@@ -56,7 +34,7 @@ const parseArgs = (argv) => {
|
|
|
56
34
|
continue;
|
|
57
35
|
}
|
|
58
36
|
if (arg === "--ruleset") {
|
|
59
|
-
const value =
|
|
37
|
+
const value = remaining[i + 1];
|
|
60
38
|
if (!value) {
|
|
61
39
|
throw new Error("Missing value for --ruleset");
|
|
62
40
|
}
|
|
@@ -65,7 +43,7 @@ const parseArgs = (argv) => {
|
|
|
65
43
|
continue;
|
|
66
44
|
}
|
|
67
45
|
if (arg === "--ruleset-name") {
|
|
68
|
-
const value =
|
|
46
|
+
const value = remaining[i + 1];
|
|
69
47
|
if (!value) {
|
|
70
48
|
throw new Error("Missing value for --ruleset-name");
|
|
71
49
|
}
|
|
@@ -88,7 +66,9 @@ const parseArgs = (argv) => {
|
|
|
88
66
|
const normalizeTrailingWhitespace = (content) => content.replace(/\s+$/u, "");
|
|
89
67
|
const normalizePath = (filePath) => filePath.replace(/\\/g, "/");
|
|
90
68
|
const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
|
|
69
|
+
const usage = normalizeTrailingWhitespace(fs.readFileSync(USAGE_PATH, "utf8"));
|
|
91
70
|
const rulesetSchema = JSON.parse(fs.readFileSync(RULESET_SCHEMA_PATH, "utf8"));
|
|
71
|
+
const TOOL_RULES = normalizeTrailingWhitespace(fs.readFileSync(TOOL_RULES_PATH, "utf8"));
|
|
92
72
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
93
73
|
const validateRulesetSchema = ajv.compile(rulesetSchema);
|
|
94
74
|
const formatSchemaErrors = (errors) => {
|
|
@@ -108,6 +88,9 @@ const resolveFrom = (baseDir, targetPath) => {
|
|
|
108
88
|
}
|
|
109
89
|
return path.resolve(baseDir, targetPath);
|
|
110
90
|
};
|
|
91
|
+
const ensureDir = (dirPath) => {
|
|
92
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
93
|
+
};
|
|
111
94
|
const clearCache = () => {
|
|
112
95
|
if (fs.existsSync(DEFAULT_CACHE_ROOT)) {
|
|
113
96
|
fs.rmSync(DEFAULT_CACHE_ROOT, { recursive: true, force: true });
|
|
@@ -270,9 +253,6 @@ const resolveRefHash = (repoUrl, ref) => {
|
|
|
270
253
|
const [hash] = raw.split(/\s+/u);
|
|
271
254
|
return hash ?? null;
|
|
272
255
|
};
|
|
273
|
-
const ensureDir = (dirPath) => {
|
|
274
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
275
|
-
};
|
|
276
256
|
const cloneAtRef = (repoUrl, ref, destination) => {
|
|
277
257
|
execGit(["clone", "--depth", "1", "--branch", ref, repoUrl, destination]);
|
|
278
258
|
};
|
|
@@ -329,6 +309,41 @@ const resolveLocalRulesRoot = (rulesetDir, source) => {
|
|
|
329
309
|
ensureDirectoryExists(candidate);
|
|
330
310
|
return candidate;
|
|
331
311
|
};
|
|
312
|
+
const resolveWorkspaceRoot = (rulesetDir, source) => {
|
|
313
|
+
if (source.startsWith("github:")) {
|
|
314
|
+
const parsed = parseGithubSource(source);
|
|
315
|
+
return path.join(DEFAULT_WORKSPACE_ROOT, parsed.owner, parsed.repo);
|
|
316
|
+
}
|
|
317
|
+
return resolveFrom(rulesetDir, source);
|
|
318
|
+
};
|
|
319
|
+
const ensureWorkspaceForGithubSource = (source) => {
|
|
320
|
+
const parsed = parseGithubSource(source);
|
|
321
|
+
const workspaceRoot = path.join(DEFAULT_WORKSPACE_ROOT, parsed.owner, parsed.repo);
|
|
322
|
+
if (!fs.existsSync(workspaceRoot)) {
|
|
323
|
+
ensureDir(path.dirname(workspaceRoot));
|
|
324
|
+
execGit(["clone", parsed.url, workspaceRoot]);
|
|
325
|
+
}
|
|
326
|
+
if (parsed.ref !== "latest") {
|
|
327
|
+
execGit(["fetch", "--all"], workspaceRoot);
|
|
328
|
+
execGit(["checkout", parsed.ref], workspaceRoot);
|
|
329
|
+
}
|
|
330
|
+
return workspaceRoot;
|
|
331
|
+
};
|
|
332
|
+
const applyRulesFromWorkspace = (rulesetDir, source) => {
|
|
333
|
+
if (!source.startsWith("github:")) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const workspaceRoot = ensureWorkspaceForGithubSource(source);
|
|
337
|
+
const status = execGit(["status", "--porcelain"], workspaceRoot);
|
|
338
|
+
if (status) {
|
|
339
|
+
throw new Error(`Workspace has uncommitted changes: ${workspaceRoot}`);
|
|
340
|
+
}
|
|
341
|
+
const branch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], workspaceRoot);
|
|
342
|
+
if (branch === "HEAD") {
|
|
343
|
+
throw new Error(`Workspace is in detached HEAD state: ${workspaceRoot}`);
|
|
344
|
+
}
|
|
345
|
+
execGit(["push"], workspaceRoot);
|
|
346
|
+
};
|
|
332
347
|
const resolveRulesRoot = (rulesetDir, source, refresh) => {
|
|
333
348
|
if (source.startsWith("github:")) {
|
|
334
349
|
return resolveGithubRulesRoot(source, refresh);
|
|
@@ -364,38 +379,24 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
|
364
379
|
fs.writeFileSync(outputPath, output, "utf8");
|
|
365
380
|
return normalizePath(path.relative(rootDir, outputPath));
|
|
366
381
|
};
|
|
367
|
-
const findRulesetFiles = (rootDir, rulesetName) => {
|
|
368
|
-
const results = [];
|
|
369
|
-
const pending = [rootDir];
|
|
370
|
-
while (pending.length > 0) {
|
|
371
|
-
const currentDir = pending.pop();
|
|
372
|
-
if (!currentDir) {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
376
|
-
for (const entry of entries) {
|
|
377
|
-
const entryPath = path.join(currentDir, entry.name);
|
|
378
|
-
if (entry.isDirectory()) {
|
|
379
|
-
if (DEFAULT_IGNORE_DIRS.has(entry.name)) {
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
pending.push(entryPath);
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (entry.isFile() && entry.name === rulesetName) {
|
|
386
|
-
results.push(entryPath);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return results;
|
|
391
|
-
};
|
|
392
382
|
const getRulesetFiles = (rootDir, specificRuleset, rulesetName) => {
|
|
393
383
|
if (specificRuleset) {
|
|
394
384
|
const resolved = resolveFrom(rootDir, specificRuleset);
|
|
395
385
|
ensureFileExists(resolved);
|
|
396
386
|
return [resolved];
|
|
397
387
|
}
|
|
398
|
-
|
|
388
|
+
const defaultRuleset = path.join(rootDir, rulesetName);
|
|
389
|
+
if (!fs.existsSync(defaultRuleset)) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
return [defaultRuleset];
|
|
393
|
+
};
|
|
394
|
+
const ensureSingleRuleset = (rulesetFiles, rootDir, rulesetName) => {
|
|
395
|
+
if (rulesetFiles.length === 0) {
|
|
396
|
+
const expectedPath = normalizePath(path.join(rootDir, rulesetName));
|
|
397
|
+
throw new Error(`Missing ruleset file: ${expectedPath}`);
|
|
398
|
+
}
|
|
399
|
+
return rulesetFiles[0];
|
|
399
400
|
};
|
|
400
401
|
const main = () => {
|
|
401
402
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -411,8 +412,30 @@ const main = () => {
|
|
|
411
412
|
const rootDir = args.root ? path.resolve(args.root) : process.cwd();
|
|
412
413
|
const rulesetName = args.rulesetName || DEFAULT_RULESET_NAME;
|
|
413
414
|
const rulesetFiles = getRulesetFiles(rootDir, args.ruleset, rulesetName);
|
|
415
|
+
const command = args.command ?? "compose";
|
|
416
|
+
if (command === "edit-rules") {
|
|
417
|
+
const rulesetPath = ensureSingleRuleset(rulesetFiles, rootDir, rulesetName);
|
|
418
|
+
const rulesetDir = path.dirname(rulesetPath);
|
|
419
|
+
const ruleset = readProjectRuleset(rulesetPath);
|
|
420
|
+
let workspaceRoot = resolveWorkspaceRoot(rulesetDir, ruleset.source);
|
|
421
|
+
if (ruleset.source.startsWith("github:")) {
|
|
422
|
+
workspaceRoot = ensureWorkspaceForGithubSource(ruleset.source);
|
|
423
|
+
}
|
|
424
|
+
process.stdout.write(`Rules workspace: ${workspaceRoot}\n`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (command === "apply-rules") {
|
|
428
|
+
const rulesetPath = ensureSingleRuleset(rulesetFiles, rootDir, rulesetName);
|
|
429
|
+
const rulesetDir = path.dirname(rulesetPath);
|
|
430
|
+
const ruleset = readProjectRuleset(rulesetPath);
|
|
431
|
+
applyRulesFromWorkspace(rulesetDir, ruleset.source);
|
|
432
|
+
const output = composeRuleset(rulesetPath, rootDir, { refresh: true });
|
|
433
|
+
process.stdout.write(`Composed AGENTS.md:\n- ${output}\n`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
414
436
|
if (rulesetFiles.length === 0) {
|
|
415
|
-
|
|
437
|
+
const expectedPath = normalizePath(path.join(rootDir, rulesetName));
|
|
438
|
+
throw new Error(`Missing ruleset file: ${expectedPath}`);
|
|
416
439
|
}
|
|
417
440
|
const outputs = rulesetFiles
|
|
418
441
|
.sort()
|
package/package.json
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Tool Rules (compose-agentsmd)
|
|
2
|
+
- Before starting any work, run `compose-agentsmd` from the project root.
|
|
3
|
+
- To update shared rules, run `compose-agentsmd edit-rules`, edit the workspace rules, then run `compose-agentsmd apply-rules`.
|
|
4
|
+
- Do not edit `AGENTS.md` directly; update the source rules and regenerate.
|
package/tools/usage.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Usage: compose-agentsmd [edit-rules|apply-rules] [--root <path>] [--ruleset <path>] [--ruleset-name <name>] [--refresh] [--clear-cache]
|
|
2
|
+
|
|
3
|
+
Options:
|
|
4
|
+
--root <path> Project root directory (default: current working directory)
|
|
5
|
+
--ruleset <path> Only compose a single ruleset file
|
|
6
|
+
--ruleset-name <name> Ruleset filename in the project root (default: agent-ruleset.json)
|
|
7
|
+
--refresh Refresh cached remote rules
|
|
8
|
+
--clear-cache Remove cached remote rules and exit
|