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 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
+ }
@@ -0,0 +1,7 @@
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
+ export {};
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();
@@ -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;
@@ -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
+ }
@@ -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,4 @@
1
+ /**
2
+ * AGENTS.map type definitions (v1 spec).
3
+ */
4
+ export {};
@@ -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
+ }