agentsmap 0.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 +132 -0
- package/dist/discoverer.d.ts +13 -0
- package/dist/discoverer.js +102 -0
- package/dist/generator.d.ts +15 -0
- package/dist/generator.js +58 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +264 -0
- package/dist/parser.d.ts +13 -0
- package/dist/parser.js +105 -0
- package/dist/resolver.d.ts +23 -0
- package/dist/resolver.js +88 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +4 -0
- package/dist/validator.d.ts +6 -0
- package/dist/validator.js +102 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# agentsmap
|
|
2
|
+
|
|
3
|
+
CLI tool for the [AGENTS.map](https://github.com/ygwyg/agents.map) specification — discover, validate, and resolve `AGENTS.md` instruction files.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g agentsmap
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with `npx`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx agentsmap init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `agentsmap init`
|
|
20
|
+
|
|
21
|
+
Scan your repo for `AGENTS.md` files and generate an `AGENTS.map.md` at the root.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
agentsmap init
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Interactive by default — prompts you for each file's purpose. Use `--non-interactive` to auto-infer purposes from file contents:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
agentsmap init --non-interactive
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `agentsmap validate`
|
|
34
|
+
|
|
35
|
+
Check that your `AGENTS.map.md` is valid: all listed paths exist, required fields are present, no duplicates, no path traversal.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
agentsmap validate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use this in CI to catch stale entries:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
# .github/workflows/agents-map.yml
|
|
45
|
+
- run: npx agentsmap validate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Exits with code 1 on errors. Warnings (like unlisted `AGENTS.md` files) don't fail the check.
|
|
49
|
+
|
|
50
|
+
### `agentsmap resolve <path>`
|
|
51
|
+
|
|
52
|
+
Show which `AGENTS.md` files apply to a given path, ranked by specificity.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
agentsmap resolve src/services/payments/checkout.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Output:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
2 AGENTS.md file(s) apply to src/services/payments/checkout.ts:
|
|
62
|
+
|
|
63
|
+
(most specific)
|
|
64
|
+
services/payments/AGENTS.md
|
|
65
|
+
Purpose: PCI rules, Stripe integration patterns.
|
|
66
|
+
Matched pattern: services/payments/**
|
|
67
|
+
Specificity: 6
|
|
68
|
+
Owners: @payments-team
|
|
69
|
+
|
|
70
|
+
(#2)
|
|
71
|
+
AGENTS.md
|
|
72
|
+
Purpose: Global repo conventions.
|
|
73
|
+
Matched pattern: **
|
|
74
|
+
Specificity: 0
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use `--json` for machine-readable output:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
agentsmap resolve src/payments/checkout.ts --json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `agentsmap discover`
|
|
84
|
+
|
|
85
|
+
Find all `AGENTS.md` files in your repo and show whether they're listed in the map.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
agentsmap discover
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Output shows listed files with `+` and unlisted with `?`, along with suggested purposes.
|
|
92
|
+
|
|
93
|
+
## Programmatic API
|
|
94
|
+
|
|
95
|
+
You can import the parser, resolver, and validator directly:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { parseMarkdown } from "agentsmap/parser";
|
|
99
|
+
import { resolveEntries } from "agentsmap/resolver";
|
|
100
|
+
import { validate } from "agentsmap/validator";
|
|
101
|
+
|
|
102
|
+
const map = parseMarkdown(markdownContent);
|
|
103
|
+
const matches = resolveEntries(map, "src/payments/checkout.ts");
|
|
104
|
+
const result = validate(map, "/path/to/repo");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## How it works
|
|
108
|
+
|
|
109
|
+
`AGENTS.map.md` is a plain Markdown file at your repo root that indexes all `AGENTS.md` files:
|
|
110
|
+
|
|
111
|
+
```markdown
|
|
112
|
+
# AGENTS.map
|
|
113
|
+
|
|
114
|
+
## Entries
|
|
115
|
+
|
|
116
|
+
- Path: /AGENTS.md
|
|
117
|
+
- Purpose: Global repo conventions.
|
|
118
|
+
- Applies to: /**
|
|
119
|
+
|
|
120
|
+
- Path: /services/payments/AGENTS.md
|
|
121
|
+
- Purpose: PCI rules, Stripe patterns.
|
|
122
|
+
- Applies to: /services/payments/**
|
|
123
|
+
- Owners: @payments-team
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
When an agent enters your repo, it reads this file, matches entries by glob pattern, and loads the most specific instructions. If the map is missing or stale, agents fall back to scanning — nothing breaks.
|
|
127
|
+
|
|
128
|
+
Full spec: [spec/v1.md](https://github.com/ygwyg/agents.map/blob/main/spec/v1.md)
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover AGENTS.md files by scanning the filesystem.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Recursively scan a directory for AGENTS.md files.
|
|
6
|
+
* Returns POSIX-style relative paths from rootDir.
|
|
7
|
+
*/
|
|
8
|
+
export declare function discoverAgentFiles(rootDir: string): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Read the first meaningful line from an AGENTS.md file to infer a purpose.
|
|
11
|
+
* Skips blank lines and heading markers.
|
|
12
|
+
*/
|
|
13
|
+
export declare function inferPurpose(filePath: string): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover AGENTS.md files by scanning the filesystem.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
/** Directories to always skip when scanning. */
|
|
7
|
+
const SKIP_DIRS = new Set([
|
|
8
|
+
"node_modules",
|
|
9
|
+
".git",
|
|
10
|
+
".hg",
|
|
11
|
+
".svn",
|
|
12
|
+
"dist",
|
|
13
|
+
"build",
|
|
14
|
+
".next",
|
|
15
|
+
".nuxt",
|
|
16
|
+
"__pycache__",
|
|
17
|
+
".venv",
|
|
18
|
+
"venv",
|
|
19
|
+
".tox",
|
|
20
|
+
"vendor",
|
|
21
|
+
".bundle",
|
|
22
|
+
"target",
|
|
23
|
+
"coverage",
|
|
24
|
+
".cache",
|
|
25
|
+
".turbo",
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* Recursively scan a directory for AGENTS.md files.
|
|
29
|
+
* Returns POSIX-style relative paths from rootDir.
|
|
30
|
+
*/
|
|
31
|
+
export function discoverAgentFiles(rootDir) {
|
|
32
|
+
const results = [];
|
|
33
|
+
scanDir(rootDir, rootDir, results);
|
|
34
|
+
return results.sort();
|
|
35
|
+
}
|
|
36
|
+
function scanDir(currentDir, rootDir, results) {
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Permission denied or other read error — skip
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) {
|
|
48
|
+
// Allow .github but skip other dot directories
|
|
49
|
+
if (entry.name !== ".github")
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
scanDir(path.join(currentDir, entry.name), rootDir, results);
|
|
53
|
+
}
|
|
54
|
+
else if (entry.isFile() && entry.name === "AGENTS.md") {
|
|
55
|
+
const relativePath = path
|
|
56
|
+
.relative(rootDir, path.join(currentDir, entry.name))
|
|
57
|
+
.replace(/\\/g, "/");
|
|
58
|
+
results.push(relativePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read the first meaningful line from an AGENTS.md file to infer a purpose.
|
|
64
|
+
* Skips blank lines and heading markers.
|
|
65
|
+
*/
|
|
66
|
+
export function inferPurpose(filePath) {
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
69
|
+
const lines = content.split("\n");
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
// Skip empty lines
|
|
73
|
+
if (!trimmed)
|
|
74
|
+
continue;
|
|
75
|
+
// If it's a heading, extract the heading text
|
|
76
|
+
const headingMatch = trimmed.match(/^#+\s+(.+)$/);
|
|
77
|
+
if (headingMatch) {
|
|
78
|
+
const heading = headingMatch[1].trim();
|
|
79
|
+
// Skip generic headings
|
|
80
|
+
if (/^agents(\.md)?$/i.test(heading))
|
|
81
|
+
continue;
|
|
82
|
+
return heading;
|
|
83
|
+
}
|
|
84
|
+
// Skip horizontal rules
|
|
85
|
+
if (/^[-=*]{3,}$/.test(trimmed))
|
|
86
|
+
continue;
|
|
87
|
+
// Skip HTML comments
|
|
88
|
+
if (trimmed.startsWith("<!--"))
|
|
89
|
+
continue;
|
|
90
|
+
// Use first meaningful text line (truncated)
|
|
91
|
+
const maxLen = 120;
|
|
92
|
+
if (trimmed.length > maxLen) {
|
|
93
|
+
return trimmed.slice(0, maxLen) + "...";
|
|
94
|
+
}
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Can't read file — fall through to placeholder
|
|
100
|
+
}
|
|
101
|
+
return "TODO: Describe this file's purpose.";
|
|
102
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate AGENTS.map.md files.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentsMap } from "./types.js";
|
|
5
|
+
/** Build a default scope from the path of an AGENTS.md file. */
|
|
6
|
+
export declare function defaultScope(agentsPath: string): string[];
|
|
7
|
+
/** Create a new AgentsMap structure from discovered files. */
|
|
8
|
+
export declare function createMap(entries: Array<{
|
|
9
|
+
path: string;
|
|
10
|
+
purpose: string;
|
|
11
|
+
owners?: string[];
|
|
12
|
+
tags?: string[];
|
|
13
|
+
}>): AgentsMap;
|
|
14
|
+
/** Serialize an AgentsMap to Markdown format. */
|
|
15
|
+
export declare function toMarkdown(map: AgentsMap): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate AGENTS.map.md files.
|
|
3
|
+
*/
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
/** Build a default scope from the path of an AGENTS.md file. */
|
|
6
|
+
export function defaultScope(agentsPath) {
|
|
7
|
+
const dir = path.posix.dirname(agentsPath);
|
|
8
|
+
if (dir === ".") {
|
|
9
|
+
// Root AGENTS.md applies to everything
|
|
10
|
+
return ["**"];
|
|
11
|
+
}
|
|
12
|
+
return [`${dir}/**`];
|
|
13
|
+
}
|
|
14
|
+
/** Create a new AgentsMap structure from discovered files. */
|
|
15
|
+
export function createMap(entries) {
|
|
16
|
+
return {
|
|
17
|
+
schema_version: 1,
|
|
18
|
+
entries: entries.map((e) => {
|
|
19
|
+
const entry = {
|
|
20
|
+
path: e.path,
|
|
21
|
+
scope: defaultScope(e.path),
|
|
22
|
+
purpose: e.purpose,
|
|
23
|
+
};
|
|
24
|
+
if (e.owners && e.owners.length > 0)
|
|
25
|
+
entry.owners = e.owners;
|
|
26
|
+
if (e.tags && e.tags.length > 0)
|
|
27
|
+
entry.tags = e.tags;
|
|
28
|
+
return entry;
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Serialize an AgentsMap to Markdown format. */
|
|
33
|
+
export function toMarkdown(map) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
lines.push("# AGENTS.map");
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push("This file lists where nested AGENTS.md files live and what they're for.");
|
|
38
|
+
lines.push("The AGENTS.md files themselves are authoritative for their subtrees.");
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("## Entries");
|
|
41
|
+
lines.push("");
|
|
42
|
+
for (const entry of map.entries) {
|
|
43
|
+
lines.push(`- Path: /${entry.path}`);
|
|
44
|
+
lines.push(` - Purpose: ${entry.purpose}`);
|
|
45
|
+
lines.push(` - Applies to: ${entry.scope.map((s) => `/${s}`).join(", ")}`);
|
|
46
|
+
if (entry.owners && entry.owners.length > 0) {
|
|
47
|
+
lines.push(` - Owners: ${entry.owners.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
50
|
+
lines.push(` - Tags: ${entry.tags.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
if (entry.last_reviewed) {
|
|
53
|
+
lines.push(` - Last reviewed: ${entry.last_reviewed}`);
|
|
54
|
+
}
|
|
55
|
+
lines.push("");
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agentsmap — CLI tool for the AGENTS.map specification.
|
|
4
|
+
*
|
|
5
|
+
* Discover, validate, and resolve AGENTS.md instruction files.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as readline from "node:readline";
|
|
12
|
+
import { parseMap, findMap } from "./parser.js";
|
|
13
|
+
import { validate } from "./validator.js";
|
|
14
|
+
import { resolveEntries, formatMatch } from "./resolver.js";
|
|
15
|
+
import { discoverAgentFiles, inferPurpose } from "./discoverer.js";
|
|
16
|
+
import { createMap, toMarkdown } from "./generator.js";
|
|
17
|
+
const program = new Command();
|
|
18
|
+
program
|
|
19
|
+
.name("agentsmap")
|
|
20
|
+
.description("CLI tool for the AGENTS.map specification — discover, validate, and resolve AGENTS.md files.")
|
|
21
|
+
.version("0.1.0");
|
|
22
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// init
|
|
24
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
program
|
|
26
|
+
.command("init")
|
|
27
|
+
.description("Scan for AGENTS.md files and generate an AGENTS.map.md file.")
|
|
28
|
+
.option("--non-interactive", "Skip interactive prompts; use inferred or placeholder purposes.")
|
|
29
|
+
.action(async (opts) => {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
console.log(chalk.blue("Scanning for AGENTS.md files..."));
|
|
32
|
+
const files = discoverAgentFiles(cwd);
|
|
33
|
+
if (files.length === 0) {
|
|
34
|
+
console.log(chalk.yellow("No AGENTS.md files found in this directory tree."));
|
|
35
|
+
console.log(chalk.dim("Create an AGENTS.md file and run this command again."));
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk.green(`Found ${files.length} AGENTS.md file(s):\n`));
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
console.log(` ${chalk.cyan(file)}`);
|
|
41
|
+
}
|
|
42
|
+
console.log();
|
|
43
|
+
const entries = [];
|
|
44
|
+
if (opts.nonInteractive) {
|
|
45
|
+
// Non-interactive: infer purpose from file content
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const fullPath = path.join(cwd, file);
|
|
48
|
+
const purpose = inferPurpose(fullPath);
|
|
49
|
+
entries.push({ path: file, purpose });
|
|
50
|
+
console.log(` ${chalk.cyan(file)} ${chalk.dim("->")} ${purpose}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Interactive: prompt for each purpose
|
|
55
|
+
const rl = readline.createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout,
|
|
58
|
+
});
|
|
59
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const fullPath = path.join(cwd, file);
|
|
62
|
+
const inferred = inferPurpose(fullPath);
|
|
63
|
+
const defaultText = inferred !== "TODO: Describe this file's purpose." ? inferred : "";
|
|
64
|
+
const hint = defaultText ? chalk.dim(` (default: "${defaultText}")`) : "";
|
|
65
|
+
const answer = await question(`${chalk.cyan(file)} - Purpose${hint}: `);
|
|
66
|
+
const purpose = answer.trim() || defaultText || "TODO: Describe this file's purpose.";
|
|
67
|
+
entries.push({ path: file, purpose });
|
|
68
|
+
}
|
|
69
|
+
rl.close();
|
|
70
|
+
}
|
|
71
|
+
const map = createMap(entries);
|
|
72
|
+
const content = toMarkdown(map);
|
|
73
|
+
const outputPath = path.join(cwd, "AGENTS.map.md");
|
|
74
|
+
// Check if file already exists
|
|
75
|
+
if (fs.existsSync(outputPath)) {
|
|
76
|
+
console.log(chalk.yellow(`\nAGENTS.map.md already exists. Overwriting.`));
|
|
77
|
+
}
|
|
78
|
+
fs.writeFileSync(outputPath, content, "utf-8");
|
|
79
|
+
console.log(chalk.green(`\nCreated ${chalk.bold("AGENTS.map.md")} with ${entries.length} entries.`));
|
|
80
|
+
console.log(chalk.dim("Run `agentsmap validate` to check the generated file."));
|
|
81
|
+
});
|
|
82
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// validate / check
|
|
84
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
const validateAction = () => {
|
|
86
|
+
const cwd = process.cwd();
|
|
87
|
+
const mapPath = findMap(cwd);
|
|
88
|
+
if (!mapPath) {
|
|
89
|
+
console.log(chalk.red("No AGENTS.map.md found."));
|
|
90
|
+
console.log(chalk.dim("Run `agentsmap init` to create one."));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
console.log(chalk.blue(`Validating ${chalk.bold("AGENTS.map.md")}...\n`));
|
|
94
|
+
let map;
|
|
95
|
+
try {
|
|
96
|
+
const result = parseMap(cwd);
|
|
97
|
+
map = result.map;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.log(chalk.red(`Parse error: ${err.message}`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const result = validate(map, cwd);
|
|
104
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
105
|
+
const warnings = result.diagnostics.filter((d) => d.severity === "warning");
|
|
106
|
+
for (const diag of errors) {
|
|
107
|
+
console.log(` ${chalk.red("error")} ${diag.message}`);
|
|
108
|
+
}
|
|
109
|
+
for (const diag of warnings) {
|
|
110
|
+
console.log(` ${chalk.yellow("warn")} ${diag.message}`);
|
|
111
|
+
}
|
|
112
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
113
|
+
console.log(chalk.green("All checks passed. No issues found."));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log();
|
|
117
|
+
if (errors.length > 0) {
|
|
118
|
+
console.log(chalk.red(`${errors.length} error(s)`));
|
|
119
|
+
}
|
|
120
|
+
if (warnings.length > 0) {
|
|
121
|
+
console.log(chalk.yellow(`${warnings.length} warning(s)`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!result.valid) {
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
program
|
|
129
|
+
.command("validate")
|
|
130
|
+
.description("Validate the AGENTS.map.md file in the current directory.")
|
|
131
|
+
.action(validateAction);
|
|
132
|
+
program
|
|
133
|
+
.command("check")
|
|
134
|
+
.description("Alias for validate.")
|
|
135
|
+
.action(validateAction);
|
|
136
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
// resolve
|
|
138
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
program
|
|
140
|
+
.command("resolve <target-path>")
|
|
141
|
+
.description("Show which AGENTS.md files apply to a given path.")
|
|
142
|
+
.option("--json", "Output in JSON format.")
|
|
143
|
+
.action((targetPath, opts) => {
|
|
144
|
+
const cwd = process.cwd();
|
|
145
|
+
let map;
|
|
146
|
+
try {
|
|
147
|
+
const result = parseMap(cwd);
|
|
148
|
+
map = result.map;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.log(chalk.red(err.message));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const matches = resolveEntries(map, targetPath);
|
|
155
|
+
if (opts.json) {
|
|
156
|
+
const output = matches.map((m) => ({
|
|
157
|
+
path: m.entry.path,
|
|
158
|
+
purpose: m.entry.purpose,
|
|
159
|
+
matchedPattern: m.matchedPattern,
|
|
160
|
+
specificity: m.specificity,
|
|
161
|
+
owners: m.entry.owners,
|
|
162
|
+
tags: m.entry.tags,
|
|
163
|
+
}));
|
|
164
|
+
console.log(JSON.stringify(output, null, 2));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (matches.length === 0) {
|
|
168
|
+
console.log(chalk.yellow(`No AGENTS.md files apply to "${targetPath}".`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.log(chalk.blue(`${matches.length} AGENTS.md file(s) apply to ${chalk.bold(targetPath)}:\n`));
|
|
172
|
+
for (let i = 0; i < matches.length; i++) {
|
|
173
|
+
const match = matches[i];
|
|
174
|
+
const rank = i + 1;
|
|
175
|
+
const label = i === 0 ? chalk.green("(most specific)") : chalk.dim(`(#${rank})`);
|
|
176
|
+
console.log(`${label}`);
|
|
177
|
+
console.log(formatMatch(match));
|
|
178
|
+
console.log();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
182
|
+
// discover
|
|
183
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
program
|
|
185
|
+
.command("discover")
|
|
186
|
+
.description("Scan for all AGENTS.md files and show their listing status.")
|
|
187
|
+
.action(() => {
|
|
188
|
+
const cwd = process.cwd();
|
|
189
|
+
console.log(chalk.blue("Scanning for AGENTS.md files...\n"));
|
|
190
|
+
const files = discoverAgentFiles(cwd);
|
|
191
|
+
if (files.length === 0) {
|
|
192
|
+
console.log(chalk.yellow("No AGENTS.md files found."));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Try to load existing map
|
|
196
|
+
let listedPaths = new Set();
|
|
197
|
+
let hasMap = false;
|
|
198
|
+
try {
|
|
199
|
+
const result = parseMap(cwd);
|
|
200
|
+
listedPaths = new Set(result.map.entries.map((e) => e.path));
|
|
201
|
+
hasMap = true;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// No map file or parse error — treat everything as unlisted
|
|
205
|
+
}
|
|
206
|
+
const listed = [];
|
|
207
|
+
const unlisted = [];
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
if (listedPaths.has(file)) {
|
|
210
|
+
listed.push(file);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
unlisted.push(file);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (hasMap) {
|
|
217
|
+
if (listed.length > 0) {
|
|
218
|
+
console.log(chalk.green(`Listed in AGENTS.map.md (${listed.length}):`));
|
|
219
|
+
for (const f of listed) {
|
|
220
|
+
console.log(` ${chalk.green("+")} ${f}`);
|
|
221
|
+
}
|
|
222
|
+
console.log();
|
|
223
|
+
}
|
|
224
|
+
if (unlisted.length > 0) {
|
|
225
|
+
console.log(chalk.yellow(`Not listed in AGENTS.map.md (${unlisted.length}):`));
|
|
226
|
+
for (const f of unlisted) {
|
|
227
|
+
const purpose = inferPurpose(path.join(cwd, f));
|
|
228
|
+
console.log(` ${chalk.yellow("?")} ${f}`);
|
|
229
|
+
console.log(` ${chalk.dim(`Suggested purpose: ${purpose}`)}`);
|
|
230
|
+
}
|
|
231
|
+
console.log();
|
|
232
|
+
console.log(chalk.dim('Run `agentsmap init` to regenerate the map, or manually add entries.'));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.log(chalk.green("All AGENTS.md files are listed in the map."));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(`Found ${files.length} AGENTS.md file(s):\n`);
|
|
240
|
+
for (const f of files) {
|
|
241
|
+
const purpose = inferPurpose(path.join(cwd, f));
|
|
242
|
+
console.log(` ${chalk.cyan(f)}`);
|
|
243
|
+
console.log(` ${chalk.dim(purpose)}`);
|
|
244
|
+
}
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(chalk.dim("No AGENTS.map.md found. Run `agentsmap init` to create one."));
|
|
247
|
+
}
|
|
248
|
+
// Also check for entries in the map that point to missing files
|
|
249
|
+
if (hasMap) {
|
|
250
|
+
const result = parseMap(cwd);
|
|
251
|
+
const missingEntries = result.map.entries.filter((e) => !files.includes(e.path));
|
|
252
|
+
if (missingEntries.length > 0) {
|
|
253
|
+
console.log();
|
|
254
|
+
console.log(chalk.red(`Stale entries in AGENTS.map.md (file missing):`));
|
|
255
|
+
for (const e of missingEntries) {
|
|
256
|
+
console.log(` ${chalk.red("x")} ${e.path}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
// Run
|
|
263
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
264
|
+
program.parse();
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse AGENTS.map.md files.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentsMap } from "./types.js";
|
|
5
|
+
/** Find the AGENTS.map.md file in the given directory. */
|
|
6
|
+
export declare function findMap(dir: string): string | null;
|
|
7
|
+
/** Parse an AGENTS.map.md file from a given directory. */
|
|
8
|
+
export declare function parseMap(dir: string): {
|
|
9
|
+
map: AgentsMap;
|
|
10
|
+
filePath: string;
|
|
11
|
+
};
|
|
12
|
+
/** Parse AGENTS.map.md content into an AgentsMap structure. */
|
|
13
|
+
export declare function parseMarkdown(content: string): AgentsMap;
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse AGENTS.map.md files.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
const MAP_FILENAME = "AGENTS.map.md";
|
|
7
|
+
/** Find the AGENTS.map.md file in the given directory. */
|
|
8
|
+
export function findMap(dir) {
|
|
9
|
+
const mapPath = path.join(dir, MAP_FILENAME);
|
|
10
|
+
if (fs.existsSync(mapPath)) {
|
|
11
|
+
return mapPath;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
/** Parse an AGENTS.map.md file from a given directory. */
|
|
16
|
+
export function parseMap(dir) {
|
|
17
|
+
const filePath = findMap(dir);
|
|
18
|
+
if (!filePath) {
|
|
19
|
+
throw new Error(`No AGENTS.map.md found in ${dir}. Run \`agentsmap init\` to create one.`);
|
|
20
|
+
}
|
|
21
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
22
|
+
const map = parseMarkdown(content);
|
|
23
|
+
return { map, filePath };
|
|
24
|
+
}
|
|
25
|
+
/** Parse AGENTS.map.md content into an AgentsMap structure. */
|
|
26
|
+
export function parseMarkdown(content) {
|
|
27
|
+
const entries = [];
|
|
28
|
+
const lines = content.split("\n");
|
|
29
|
+
let currentEntry = null;
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
// Match "- Path: /some/path" or "- Path: some/path"
|
|
33
|
+
const pathMatch = trimmed.match(/^-\s+Path:\s+\/?(.+)$/i);
|
|
34
|
+
if (pathMatch) {
|
|
35
|
+
// Save previous entry if complete
|
|
36
|
+
if (currentEntry && currentEntry.path) {
|
|
37
|
+
entries.push(finalizeEntry(currentEntry));
|
|
38
|
+
}
|
|
39
|
+
currentEntry = { path: pathMatch[1].trim() };
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!currentEntry)
|
|
43
|
+
continue;
|
|
44
|
+
// Match "- Purpose: ..."
|
|
45
|
+
const purposeMatch = trimmed.match(/^-\s+Purpose:\s+(.+)$/i);
|
|
46
|
+
if (purposeMatch) {
|
|
47
|
+
currentEntry.purpose = purposeMatch[1].trim();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Match "- Applies to: ..." (one or more comma-separated globs)
|
|
51
|
+
const scopeMatch = trimmed.match(/^-\s+Applies\s+to:\s+(.+)$/i);
|
|
52
|
+
if (scopeMatch) {
|
|
53
|
+
currentEntry.scope = scopeMatch[1]
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.map((s) => (s.startsWith("/") ? s.slice(1) : s));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Match "- Owners: ..."
|
|
60
|
+
const ownersMatch = trimmed.match(/^-\s+Owners?:\s+(.+)$/i);
|
|
61
|
+
if (ownersMatch) {
|
|
62
|
+
currentEntry.owners = ownersMatch[1]
|
|
63
|
+
.split(",")
|
|
64
|
+
.map((o) => o.trim());
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Match "- Tags: ..."
|
|
68
|
+
const tagsMatch = trimmed.match(/^-\s+Tags?:\s+(.+)$/i);
|
|
69
|
+
if (tagsMatch) {
|
|
70
|
+
currentEntry.tags = tagsMatch[1]
|
|
71
|
+
.split(",")
|
|
72
|
+
.map((t) => t.trim());
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Match "- Last reviewed: ..."
|
|
76
|
+
const reviewedMatch = trimmed.match(/^-\s+Last\s+reviewed:\s+(.+)$/i);
|
|
77
|
+
if (reviewedMatch) {
|
|
78
|
+
currentEntry.last_reviewed = reviewedMatch[1].trim();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Don't forget the last entry
|
|
83
|
+
if (currentEntry && currentEntry.path) {
|
|
84
|
+
entries.push(finalizeEntry(currentEntry));
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
schema_version: 1,
|
|
88
|
+
entries,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Finalize a partially-parsed entry with defaults for missing fields. */
|
|
92
|
+
function finalizeEntry(partial) {
|
|
93
|
+
const entry = {
|
|
94
|
+
path: partial.path,
|
|
95
|
+
scope: partial.scope ?? ["**"],
|
|
96
|
+
purpose: partial.purpose ?? "",
|
|
97
|
+
};
|
|
98
|
+
if (partial.owners)
|
|
99
|
+
entry.owners = partial.owners;
|
|
100
|
+
if (partial.tags)
|
|
101
|
+
entry.tags = partial.tags;
|
|
102
|
+
if (partial.last_reviewed)
|
|
103
|
+
entry.last_reviewed = partial.last_reviewed;
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution and scope matching for AGENTS.map entries.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentsMap, ResolveMatch } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Calculate the specificity of a glob pattern.
|
|
7
|
+
* More specific patterns have higher scores.
|
|
8
|
+
*
|
|
9
|
+
* Heuristic:
|
|
10
|
+
* - Count non-wildcard path segments
|
|
11
|
+
* - "**" alone is least specific
|
|
12
|
+
* - Longer literal prefixes are more specific
|
|
13
|
+
*/
|
|
14
|
+
export declare function calculateSpecificity(pattern: string): number;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve which AGENTS.md entries apply to a given target path.
|
|
17
|
+
* Returns matches sorted by specificity (most specific first).
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveEntries(map: AgentsMap, targetPath: string): ResolveMatch[];
|
|
20
|
+
/**
|
|
21
|
+
* Format a resolved match for human-readable display.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatMatch(match: ResolveMatch): string;
|
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution and scope matching for AGENTS.map entries.
|
|
3
|
+
*/
|
|
4
|
+
import picomatch from "picomatch";
|
|
5
|
+
/**
|
|
6
|
+
* Calculate the specificity of a glob pattern.
|
|
7
|
+
* More specific patterns have higher scores.
|
|
8
|
+
*
|
|
9
|
+
* Heuristic:
|
|
10
|
+
* - Count non-wildcard path segments
|
|
11
|
+
* - "**" alone is least specific
|
|
12
|
+
* - Longer literal prefixes are more specific
|
|
13
|
+
*/
|
|
14
|
+
export function calculateSpecificity(pattern) {
|
|
15
|
+
// Remove leading slash if present
|
|
16
|
+
const normalized = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
17
|
+
// "**" alone matches everything — lowest specificity
|
|
18
|
+
if (normalized === "**")
|
|
19
|
+
return 0;
|
|
20
|
+
const segments = normalized.split("/");
|
|
21
|
+
let score = 0;
|
|
22
|
+
for (const seg of segments) {
|
|
23
|
+
if (seg === "**") {
|
|
24
|
+
// Double wildcard doesn't add specificity
|
|
25
|
+
score += 0;
|
|
26
|
+
}
|
|
27
|
+
else if (seg === "*") {
|
|
28
|
+
// Single wildcard adds minimal specificity
|
|
29
|
+
score += 1;
|
|
30
|
+
}
|
|
31
|
+
else if (seg.includes("*") || seg.includes("?")) {
|
|
32
|
+
// Partial wildcard — somewhat specific
|
|
33
|
+
score += 2;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Literal segment — most specific
|
|
37
|
+
score += 3;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return score;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve which AGENTS.md entries apply to a given target path.
|
|
44
|
+
* Returns matches sorted by specificity (most specific first).
|
|
45
|
+
*/
|
|
46
|
+
export function resolveEntries(map, targetPath) {
|
|
47
|
+
// Normalize target path: strip leading slash and backslashes
|
|
48
|
+
const normalized = targetPath
|
|
49
|
+
.replace(/\\/g, "/")
|
|
50
|
+
.replace(/^\//, "");
|
|
51
|
+
const matches = [];
|
|
52
|
+
for (const entry of map.entries) {
|
|
53
|
+
for (const pattern of entry.scope) {
|
|
54
|
+
// Normalize pattern: strip leading slash
|
|
55
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
56
|
+
const isMatch = picomatch(normalizedPattern);
|
|
57
|
+
if (isMatch(normalized)) {
|
|
58
|
+
matches.push({
|
|
59
|
+
entry,
|
|
60
|
+
matchedPattern: pattern,
|
|
61
|
+
specificity: calculateSpecificity(pattern),
|
|
62
|
+
});
|
|
63
|
+
// Only match the first (most specific) pattern per entry
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Sort by specificity (most specific first)
|
|
69
|
+
matches.sort((a, b) => b.specificity - a.specificity);
|
|
70
|
+
return matches;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format a resolved match for human-readable display.
|
|
74
|
+
*/
|
|
75
|
+
export function formatMatch(match) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
lines.push(` ${match.entry.path}`);
|
|
78
|
+
lines.push(` Purpose: ${match.entry.purpose}`);
|
|
79
|
+
lines.push(` Matched pattern: ${match.matchedPattern}`);
|
|
80
|
+
lines.push(` Specificity: ${match.specificity}`);
|
|
81
|
+
if (match.entry.owners && match.entry.owners.length > 0) {
|
|
82
|
+
lines.push(` Owners: ${match.entry.owners.join(", ")}`);
|
|
83
|
+
}
|
|
84
|
+
if (match.entry.tags && match.entry.tags.length > 0) {
|
|
85
|
+
lines.push(` Tags: ${match.entry.tags.join(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGENTS.map type definitions (v1 spec).
|
|
3
|
+
*/
|
|
4
|
+
/** A single entry in the AGENTS.map.md file. */
|
|
5
|
+
export interface AgentsMapEntry {
|
|
6
|
+
/** POSIX path relative to repo root. Must not contain ".." or start with "/". */
|
|
7
|
+
path: string;
|
|
8
|
+
/** Glob patterns indicating which paths this entry applies to. */
|
|
9
|
+
scope: string[];
|
|
10
|
+
/** 1-3 sentences explaining why/when to use this file. */
|
|
11
|
+
purpose: string;
|
|
12
|
+
/** Optional team handles, CODEOWNERS aliases, etc. */
|
|
13
|
+
owners?: string[];
|
|
14
|
+
/** Optional categorical labels. */
|
|
15
|
+
tags?: string[];
|
|
16
|
+
/** Optional date in YYYY-MM-DD format. */
|
|
17
|
+
last_reviewed?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The parsed AGENTS.map structure. */
|
|
20
|
+
export interface AgentsMap {
|
|
21
|
+
/** Must be 1 for this version of the spec. */
|
|
22
|
+
schema_version: number;
|
|
23
|
+
/** Array of entry objects. */
|
|
24
|
+
entries: AgentsMapEntry[];
|
|
25
|
+
}
|
|
26
|
+
/** Validation severity levels. */
|
|
27
|
+
export type Severity = "error" | "warning";
|
|
28
|
+
/** A single validation diagnostic. */
|
|
29
|
+
export interface ValidationDiagnostic {
|
|
30
|
+
severity: Severity;
|
|
31
|
+
message: string;
|
|
32
|
+
/** The entry path this diagnostic relates to, if applicable. */
|
|
33
|
+
entryPath?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Result of validation. */
|
|
36
|
+
export interface ValidationResult {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
diagnostics: ValidationDiagnostic[];
|
|
39
|
+
}
|
|
40
|
+
/** Result of resolving a target path against the map. */
|
|
41
|
+
export interface ResolveMatch {
|
|
42
|
+
entry: AgentsMapEntry;
|
|
43
|
+
/** The specific scope pattern that matched. */
|
|
44
|
+
matchedPattern: string;
|
|
45
|
+
/** Specificity score (higher = more specific). */
|
|
46
|
+
specificity: number;
|
|
47
|
+
}
|
|
48
|
+
/** A discovered AGENTS.md file on disk. */
|
|
49
|
+
export interface DiscoveredFile {
|
|
50
|
+
/** Relative POSIX path from the repo root. */
|
|
51
|
+
path: string;
|
|
52
|
+
/** Whether this file is listed in the current AGENTS.map. */
|
|
53
|
+
listed: boolean;
|
|
54
|
+
/** First meaningful line content, used for purpose inference. */
|
|
55
|
+
firstLine?: string;
|
|
56
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation logic for AGENTS.map.md files.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentsMap, ValidationResult } from "./types.js";
|
|
5
|
+
/** Validate an AGENTS.map structure against the v1 spec rules. */
|
|
6
|
+
export declare function validate(map: AgentsMap, rootDir: string): ValidationResult;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation logic for AGENTS.map.md files.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { discoverAgentFiles } from "./discoverer.js";
|
|
7
|
+
/** Validate an AGENTS.map structure against the v1 spec rules. */
|
|
8
|
+
export function validate(map, rootDir) {
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
// Check that entries is present and is an array
|
|
11
|
+
if (!Array.isArray(map.entries)) {
|
|
12
|
+
diagnostics.push({
|
|
13
|
+
severity: "error",
|
|
14
|
+
message: '"entries" must be an array.',
|
|
15
|
+
});
|
|
16
|
+
return { valid: false, diagnostics };
|
|
17
|
+
}
|
|
18
|
+
// Track paths for duplicate detection
|
|
19
|
+
const seenPaths = new Set();
|
|
20
|
+
for (const entry of map.entries) {
|
|
21
|
+
// Check required fields
|
|
22
|
+
if (!entry.path || typeof entry.path !== "string") {
|
|
23
|
+
diagnostics.push({
|
|
24
|
+
severity: "error",
|
|
25
|
+
message: "Entry is missing required field \"path\".",
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!entry.scope || !Array.isArray(entry.scope) || entry.scope.length === 0) {
|
|
30
|
+
diagnostics.push({
|
|
31
|
+
severity: "error",
|
|
32
|
+
message: `Entry "${entry.path}": missing or empty required field "scope".`,
|
|
33
|
+
entryPath: entry.path,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (!entry.purpose || typeof entry.purpose !== "string") {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
severity: "error",
|
|
39
|
+
message: `Entry "${entry.path}": missing required field "purpose".`,
|
|
40
|
+
entryPath: entry.path,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Check path doesn't contain ".."
|
|
44
|
+
if (entry.path.includes("..")) {
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: `Entry "${entry.path}": path must not contain ".." segments.`,
|
|
48
|
+
entryPath: entry.path,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Check path doesn't start with "/"
|
|
52
|
+
if (entry.path.startsWith("/")) {
|
|
53
|
+
diagnostics.push({
|
|
54
|
+
severity: "error",
|
|
55
|
+
message: `Entry "${entry.path}": path must not start with "/". Use relative POSIX paths.`,
|
|
56
|
+
entryPath: entry.path,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Check for duplicate paths
|
|
60
|
+
const normalizedPath = entry.path.replace(/\\/g, "/");
|
|
61
|
+
if (seenPaths.has(normalizedPath)) {
|
|
62
|
+
diagnostics.push({
|
|
63
|
+
severity: "error",
|
|
64
|
+
message: `Duplicate entry for path "${entry.path}".`,
|
|
65
|
+
entryPath: entry.path,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
seenPaths.add(normalizedPath);
|
|
69
|
+
// Check that the file actually exists
|
|
70
|
+
const fullPath = path.join(rootDir, entry.path);
|
|
71
|
+
if (!fs.existsSync(fullPath)) {
|
|
72
|
+
diagnostics.push({
|
|
73
|
+
severity: "error",
|
|
74
|
+
message: `Entry "${entry.path}": file does not exist at ${fullPath}.`,
|
|
75
|
+
entryPath: entry.path,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Validate last_reviewed format if present
|
|
79
|
+
if (entry.last_reviewed) {
|
|
80
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.last_reviewed)) {
|
|
81
|
+
diagnostics.push({
|
|
82
|
+
severity: "warning",
|
|
83
|
+
message: `Entry "${entry.path}": last_reviewed "${entry.last_reviewed}" is not in YYYY-MM-DD format.`,
|
|
84
|
+
entryPath: entry.path,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Warn about AGENTS.md files that exist but aren't listed
|
|
90
|
+
const discoveredFiles = discoverAgentFiles(rootDir);
|
|
91
|
+
for (const file of discoveredFiles) {
|
|
92
|
+
if (!seenPaths.has(file)) {
|
|
93
|
+
diagnostics.push({
|
|
94
|
+
severity: "warning",
|
|
95
|
+
message: `Found AGENTS.md at "${file}" that is not listed in the map. Consider adding it.`,
|
|
96
|
+
entryPath: file,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
101
|
+
return { valid: !hasErrors, diagnostics };
|
|
102
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentsmap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for the AGENTS.map specification — discover, validate, and resolve AGENTS.md instruction files.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentsmap": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/types.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./parser": {
|
|
17
|
+
"types": "./dist/parser.d.ts",
|
|
18
|
+
"import": "./dist/parser.js"
|
|
19
|
+
},
|
|
20
|
+
"./resolver": {
|
|
21
|
+
"types": "./dist/resolver.d.ts",
|
|
22
|
+
"import": "./dist/resolver.js"
|
|
23
|
+
},
|
|
24
|
+
"./validator": {
|
|
25
|
+
"types": "./dist/validator.d.ts",
|
|
26
|
+
"import": "./dist/validator.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.build.json",
|
|
34
|
+
"prepublishOnly": "npm run build",
|
|
35
|
+
"start": "tsx src/index.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"agents",
|
|
42
|
+
"agents.md",
|
|
43
|
+
"agents.map",
|
|
44
|
+
"ai",
|
|
45
|
+
"llm",
|
|
46
|
+
"cli",
|
|
47
|
+
"monorepo",
|
|
48
|
+
"agent-instructions"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/ygwyg/agents.map.git",
|
|
54
|
+
"directory": "cli"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/ygwyg/agents.map#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/ygwyg/agents.map/issues"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"chalk": "^5.4.1",
|
|
65
|
+
"commander": "^13.1.0",
|
|
66
|
+
"picomatch": "^4.0.2"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "^22.13.4",
|
|
70
|
+
"@types/picomatch": "^3.0.2",
|
|
71
|
+
"tsx": "^4.19.3",
|
|
72
|
+
"typescript": "^5.7.3",
|
|
73
|
+
"vitest": "^3.0.6"
|
|
74
|
+
}
|
|
75
|
+
}
|