agents.yaml 0.2.1 → 0.2.2

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/AGENTS.md CHANGED
@@ -19,3 +19,5 @@ Agents should treat paths listed in `documents` as promoted supplemental guidanc
19
19
  The CLI can help discover package and local `AGENTS.md` files, add selected paths to `agents.yaml`, remove paths, initialize the root breadcrumb, and validate that referenced files still exist.
20
20
 
21
21
  Discovery only considers direct dependencies under a project's `node_modules`; nested dependency `AGENTS.md` files are not automatically activated.
22
+
23
+ Discovery skips dot-prefixed directories by default. Use `agents discover --include-dot-directories` when hidden project directories should be scanned too.
package/README.md CHANGED
@@ -17,12 +17,30 @@ pnpm run build
17
17
  ```sh
18
18
  agents init
19
19
  agents discover
20
+ agents discover --include-dot-directories
20
21
  agents add ./node_modules/react/AGENTS.md
21
22
  agents validate
22
23
  ```
23
24
 
24
25
  Run `agents` with no command for the interactive flow.
25
26
 
27
+ Discovery skips dot-prefixed directories by default so local caches and tool
28
+ state do not dominate scan time. Use `--include-dot-directories` when you need
29
+ to search those directories too.
30
+
31
+ ## Benchmark
32
+
33
+ ```sh
34
+ pnpm --filter agents.yaml bench
35
+ ```
36
+
37
+ The benchmark creates a temporary discovery fixture, compares default discovery
38
+ against `--include-dot-directories`, prints median/min/max timings, and removes
39
+ the fixture when it exits. Fixture size can be tuned with
40
+ `AGENTS_BENCH_HIDDEN_DIRS`, `AGENTS_BENCH_FILES_PER_HIDDEN_DIR`,
41
+ `AGENTS_BENCH_VISIBLE_PACKAGES`, `AGENTS_BENCH_ITERATIONS`, and
42
+ `AGENTS_BENCH_WARMUPS`.
43
+
26
44
  ## File Format
27
45
 
28
46
  ```yaml
package/dist/index.mjs CHANGED
@@ -15,8 +15,8 @@ function resolveFromRoot(root, input) {
15
15
  }
16
16
  function formatProjectPath(root, target) {
17
17
  const relative = path.relative(root, target).split(path.sep).join(path.posix.sep);
18
- if (relative.startsWith("..")) return target;
19
- return relative.startsWith(".") ? relative : `./${relative}`;
18
+ if (relative === ".." || relative.startsWith("../")) return target;
19
+ return relative.startsWith("./") ? relative : `./${relative}`;
20
20
  }
21
21
  //#endregion
22
22
  //#region src/agents-file.ts
@@ -174,16 +174,16 @@ const skippedDirectories = new Set([
174
174
  "dist",
175
175
  "build"
176
176
  ]);
177
- async function discoverAgentDocuments(root) {
177
+ async function discoverAgentDocuments(root, options = {}) {
178
178
  const found = [];
179
- await walk(root, root, found);
179
+ await walk(root, root, found, options);
180
180
  return found.filter((document) => document.path !== "./AGENTS.md").sort((left, right) => left.path.localeCompare(right.path));
181
181
  }
182
182
  async function describeAgentDocument(root, agentsDocumentPath) {
183
183
  const absolutePath = resolveFromRoot(root, agentsDocumentPath);
184
184
  return documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath)));
185
185
  }
186
- async function walk(root, directory, found) {
186
+ async function walk(root, directory, found, options) {
187
187
  let handle;
188
188
  try {
189
189
  handle = await opendir(directory);
@@ -197,12 +197,16 @@ async function walk(root, directory, found) {
197
197
  await scanDirectNodeModules(root, absolutePath, found);
198
198
  continue;
199
199
  }
200
- if (!skippedDirectories.has(entry.name)) await walk(root, absolutePath, found);
200
+ if (!shouldSkipDirectory(entry.name, options)) await walk(root, absolutePath, found, options);
201
201
  continue;
202
202
  }
203
203
  if (entry.isFile() && entry.name === "AGENTS.md") found.push(await documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath))));
204
204
  }
205
205
  }
206
+ function shouldSkipDirectory(name, options) {
207
+ if (skippedDirectories.has(name)) return true;
208
+ return !options.includeDotDirectories && name.startsWith(".");
209
+ }
206
210
  async function scanDirectNodeModules(root, nodeModulesPath, found) {
207
211
  let handle;
208
212
  try {
@@ -263,7 +267,7 @@ const helpText = `agents
263
267
  Usage:
264
268
  agents
265
269
  agents init [--force]
266
- agents discover [--json]
270
+ agents discover [--json] [--include-dot-directories]
267
271
  agents add <path...>
268
272
  agents remove <path...>
269
273
  agents validate [--json]
@@ -286,7 +290,10 @@ async function run(argv) {
286
290
  await commandInit(root, parsed.flags.get("force") === true);
287
291
  return;
288
292
  case "discover":
289
- await commandDiscover(root, parsed.flags.get("json") === true);
293
+ await commandDiscover(root, {
294
+ json: parsed.flags.get("json") === true,
295
+ includeDotDirectories: parsed.flags.get("include-dot-directories") === true
296
+ });
290
297
  return;
291
298
  case "add":
292
299
  await commandAdd(root, parsed.values);
@@ -353,9 +360,9 @@ async function commandInit(root, force) {
353
360
  clack.note(result.messages.join("\n"), "Updated");
354
361
  clack.outro("Project breadcrumb is ready.");
355
362
  }
356
- async function commandDiscover(root, json) {
357
- const documents = await discoverAgentDocuments(root);
358
- if (json) {
363
+ async function commandDiscover(root, options) {
364
+ const documents = await discoverAgentDocuments(root, { includeDotDirectories: options.includeDotDirectories });
365
+ if (options.json) {
359
366
  console.log(JSON.stringify(documents, null, 2));
360
367
  return;
361
368
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents.yaml",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A CLI for discovering and curating agent-readable documentation in agents.yaml.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,6 +31,7 @@
31
31
  "pnpm": "11.5.2"
32
32
  },
33
33
  "scripts": {
34
+ "bench": "node src/discover.bench.ts",
34
35
  "build": "vp pack",
35
36
  "dev": "src/index.ts",
36
37
  "test": "vp test"
@@ -0,0 +1,219 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import path from "node:path"
4
+ import { performance } from "node:perf_hooks"
5
+ import { discoverAgentDocuments, type DiscoverOptions } from "./discover.ts"
6
+
7
+ type BenchmarkCase = {
8
+ name: string
9
+ options?: DiscoverOptions
10
+ }
11
+
12
+ type BenchmarkResult = {
13
+ name: string
14
+ docs: number
15
+ medianMs: number
16
+ minMs: number
17
+ maxMs: number
18
+ }
19
+
20
+ const hiddenDirectories = readPositiveInteger("AGENTS_BENCH_HIDDEN_DIRS", 250)
21
+ const filesPerHiddenDirectory = readPositiveInteger(
22
+ "AGENTS_BENCH_FILES_PER_HIDDEN_DIR",
23
+ 8,
24
+ )
25
+ const visiblePackages = readPositiveInteger("AGENTS_BENCH_VISIBLE_PACKAGES", 25)
26
+ const iterations = readPositiveInteger("AGENTS_BENCH_ITERATIONS", 7)
27
+ const warmups = readPositiveInteger("AGENTS_BENCH_WARMUPS", 1)
28
+
29
+ const cases: BenchmarkCase[] = [
30
+ { name: "default" },
31
+ {
32
+ name: "include dot directories",
33
+ options: { includeDotDirectories: true },
34
+ },
35
+ ]
36
+
37
+ const root = await mkdtemp(path.join(tmpdir(), "agents-yaml-bench-"))
38
+
39
+ try {
40
+ await createFixture(root)
41
+ const results: BenchmarkResult[] = []
42
+
43
+ for (const benchmarkCase of cases) {
44
+ for (let index = 0; index < warmups; index += 1) {
45
+ await discoverAgentDocuments(root, benchmarkCase.options)
46
+ }
47
+
48
+ results.push(await runCase(root, benchmarkCase))
49
+ }
50
+
51
+ printResults(results)
52
+ } finally {
53
+ await rm(root, { recursive: true, force: true })
54
+ }
55
+
56
+ async function createFixture(root: string): Promise<void> {
57
+ await writeFile(
58
+ path.join(root, "agents.yaml"),
59
+ "version: 1\n\ndocuments: []\n",
60
+ "utf8",
61
+ )
62
+ await writeFile(path.join(root, "AGENTS.md"), "# Root guidance\n", "utf8")
63
+
64
+ await createDirectDependency(root)
65
+ await createVisibleProjectDocuments(root)
66
+ await createHiddenCache(root)
67
+ }
68
+
69
+ async function createDirectDependency(root: string): Promise<void> {
70
+ const dependencyPath = path.join(root, "node_modules", "direct-lib")
71
+ await mkdir(dependencyPath, { recursive: true })
72
+ await writeFile(
73
+ path.join(dependencyPath, "AGENTS.md"),
74
+ "# Direct dependency guidance\n",
75
+ "utf8",
76
+ )
77
+ await writeFile(
78
+ path.join(dependencyPath, "package.json"),
79
+ JSON.stringify({
80
+ name: "direct-lib",
81
+ description: "Direct fixture dependency.",
82
+ }),
83
+ "utf8",
84
+ )
85
+ }
86
+
87
+ async function createVisibleProjectDocuments(root: string): Promise<void> {
88
+ for (let index = 0; index < visiblePackages; index += 1) {
89
+ const packagePath = path.join(root, "packages", `visible-${index}`)
90
+ await mkdir(packagePath, { recursive: true })
91
+ await writeFile(
92
+ path.join(packagePath, "AGENTS.md"),
93
+ "# Visible project guidance\n",
94
+ "utf8",
95
+ )
96
+ }
97
+ }
98
+
99
+ async function createHiddenCache(root: string): Promise<void> {
100
+ for (let index = 0; index < hiddenDirectories; index += 1) {
101
+ const cachePath = path.join(root, ".cache", `entry-${index}`, "nested")
102
+ await mkdir(cachePath, { recursive: true })
103
+ await writeFile(
104
+ path.join(cachePath, "AGENTS.md"),
105
+ "# Hidden cache guidance\n",
106
+ "utf8",
107
+ )
108
+
109
+ for (
110
+ let fileIndex = 0;
111
+ fileIndex < filesPerHiddenDirectory;
112
+ fileIndex += 1
113
+ ) {
114
+ await writeFile(
115
+ path.join(cachePath, `file-${fileIndex}.txt`),
116
+ "x".repeat(100),
117
+ "utf8",
118
+ )
119
+ }
120
+ }
121
+ }
122
+
123
+ async function runCase(
124
+ root: string,
125
+ benchmarkCase: BenchmarkCase,
126
+ ): Promise<BenchmarkResult> {
127
+ const durations: number[] = []
128
+ let docs = 0
129
+
130
+ for (let index = 0; index < iterations; index += 1) {
131
+ const start = performance.now()
132
+ const discovered = await discoverAgentDocuments(root, benchmarkCase.options)
133
+ const duration = performance.now() - start
134
+
135
+ docs = discovered.length
136
+ durations.push(duration)
137
+ }
138
+
139
+ const sorted = [...durations].sort((left, right) => left - right)
140
+ const medianMs = sorted[Math.floor(sorted.length / 2)]
141
+ const minMs = sorted[0]
142
+ const maxMs = sorted[sorted.length - 1]
143
+
144
+ if (medianMs === undefined || minMs === undefined || maxMs === undefined) {
145
+ throw new Error("Benchmark did not record any durations")
146
+ }
147
+
148
+ return {
149
+ name: benchmarkCase.name,
150
+ docs,
151
+ medianMs,
152
+ minMs,
153
+ maxMs,
154
+ }
155
+ }
156
+
157
+ function printResults(results: BenchmarkResult[]): void {
158
+ console.log("agents discover benchmark")
159
+ console.log(
160
+ [
161
+ `fixture: hiddenDirectories=${hiddenDirectories}`,
162
+ `filesPerHiddenDirectory=${filesPerHiddenDirectory}`,
163
+ `visiblePackages=${visiblePackages}`,
164
+ `iterations=${iterations}`,
165
+ `warmups=${warmups}`,
166
+ ].join(", "),
167
+ )
168
+ console.log("")
169
+ console.log(
170
+ [
171
+ pad("case", 24),
172
+ pad("docs", 8),
173
+ pad("median", 10),
174
+ pad("min", 10),
175
+ pad("max", 10),
176
+ ].join(""),
177
+ )
178
+ console.log("-".repeat(62))
179
+
180
+ for (const result of results) {
181
+ console.log(
182
+ [
183
+ pad(result.name, 24),
184
+ pad(String(result.docs), 8),
185
+ pad(formatMs(result.medianMs), 10),
186
+ pad(formatMs(result.minMs), 10),
187
+ pad(formatMs(result.maxMs), 10),
188
+ ].join(""),
189
+ )
190
+ }
191
+
192
+ const defaultResult = results.find((result) => result.name === "default")
193
+ const includeDotResult = results.find(
194
+ (result) => result.name === "include dot directories",
195
+ )
196
+ if (defaultResult && includeDotResult && defaultResult.medianMs > 0) {
197
+ const ratio = includeDotResult.medianMs / defaultResult.medianMs
198
+ console.log("")
199
+ console.log(`include dot directories median: ${ratio.toFixed(1)}x default`)
200
+ }
201
+ }
202
+
203
+ function readPositiveInteger(name: string, fallback: number): number {
204
+ const raw = process.env[name]
205
+ if (!raw) return fallback
206
+
207
+ const value = Number.parseInt(raw, 10)
208
+ if (Number.isInteger(value) && value > 0) return value
209
+
210
+ throw new Error(`${name} must be a positive integer`)
211
+ }
212
+
213
+ function formatMs(value: number): string {
214
+ return `${value.toFixed(1)}ms`
215
+ }
216
+
217
+ function pad(value: string, width: number): string {
218
+ return value.padEnd(width, " ")
219
+ }
@@ -132,6 +132,26 @@ describe("agents.yaml dependency discovery", () => {
132
132
  },
133
133
  ])
134
134
  })
135
+
136
+ it("skips dot-prefixed directories by default", async () => {
137
+ const root = await createTempProject()
138
+ const hiddenAgentsPath = path.join(root, ".cache", "AGENTS.md")
139
+ await mkdir(path.dirname(hiddenAgentsPath), { recursive: true })
140
+ await writeFile(hiddenAgentsPath, "# Hidden cache guidance\n", "utf8")
141
+
142
+ await expect(discoverAgentDocuments(root)).resolves.toEqual([])
143
+ })
144
+
145
+ it("can include dot-prefixed directories when requested", async () => {
146
+ const root = await createTempProject()
147
+ const hiddenAgentsPath = path.join(root, ".cache", "AGENTS.md")
148
+ await mkdir(path.dirname(hiddenAgentsPath), { recursive: true })
149
+ await writeFile(hiddenAgentsPath, "# Hidden cache guidance\n", "utf8")
150
+
151
+ await expect(
152
+ discoverAgentDocuments(root, { includeDotDirectories: true }),
153
+ ).resolves.toEqual([{ path: "./.cache/AGENTS.md" }])
154
+ })
135
155
  })
136
156
 
137
157
  async function createTempProject(): Promise<string> {
package/src/discover.ts CHANGED
@@ -7,6 +7,10 @@ export type DiscoveredDocument = {
7
7
  description?: string
8
8
  }
9
9
 
10
+ export type DiscoverOptions = {
11
+ includeDotDirectories?: boolean
12
+ }
13
+
10
14
  const skippedDirectories = new Set([
11
15
  ".git",
12
16
  ".hg",
@@ -20,9 +24,10 @@ const skippedDirectories = new Set([
20
24
 
21
25
  export async function discoverAgentDocuments(
22
26
  root: string,
27
+ options: DiscoverOptions = {},
23
28
  ): Promise<DiscoveredDocument[]> {
24
29
  const found: DiscoveredDocument[] = []
25
- await walk(root, root, found)
30
+ await walk(root, root, found, options)
26
31
  return found
27
32
  .filter((document) => document.path !== "./AGENTS.md")
28
33
  .sort((left, right) => left.path.localeCompare(right.path))
@@ -44,6 +49,7 @@ async function walk(
44
49
  root: string,
45
50
  directory: string,
46
51
  found: DiscoveredDocument[],
52
+ options: DiscoverOptions,
47
53
  ): Promise<void> {
48
54
  let handle
49
55
  try {
@@ -61,8 +67,8 @@ async function walk(
61
67
  continue
62
68
  }
63
69
 
64
- if (!skippedDirectories.has(entry.name)) {
65
- await walk(root, absolutePath, found)
70
+ if (!shouldSkipDirectory(entry.name, options)) {
71
+ await walk(root, absolutePath, found, options)
66
72
  }
67
73
  continue
68
74
  }
@@ -79,6 +85,11 @@ async function walk(
79
85
  }
80
86
  }
81
87
 
88
+ function shouldSkipDirectory(name: string, options: DiscoverOptions): boolean {
89
+ if (skippedDirectories.has(name)) return true
90
+ return !options.includeDotDirectories && name.startsWith(".")
91
+ }
92
+
82
93
  async function scanDirectNodeModules(
83
94
  root: string,
84
95
  nodeModulesPath: string,
package/src/paths.ts CHANGED
@@ -15,9 +15,9 @@ export function formatProjectPath(root: string, target: string): string {
15
15
  .relative(root, target)
16
16
  .split(path.sep)
17
17
  .join(path.posix.sep)
18
- if (relative.startsWith("..")) {
18
+ if (relative === ".." || relative.startsWith("../")) {
19
19
  return target
20
20
  }
21
21
 
22
- return relative.startsWith(".") ? relative : `./${relative}`
22
+ return relative.startsWith("./") ? relative : `./${relative}`
23
23
  }
package/src/run.ts CHANGED
@@ -37,7 +37,7 @@ const helpText = `agents
37
37
  Usage:
38
38
  agents
39
39
  agents init [--force]
40
- agents discover [--json]
40
+ agents discover [--json] [--include-dot-directories]
41
41
  agents add <path...>
42
42
  agents remove <path...>
43
43
  agents validate [--json]
@@ -62,7 +62,11 @@ export async function run(argv: string[]): Promise<void> {
62
62
  await commandInit(root, parsed.flags.get("force") === true)
63
63
  return
64
64
  case "discover":
65
- await commandDiscover(root, parsed.flags.get("json") === true)
65
+ await commandDiscover(root, {
66
+ json: parsed.flags.get("json") === true,
67
+ includeDotDirectories:
68
+ parsed.flags.get("include-dot-directories") === true,
69
+ })
66
70
  return
67
71
  case "add":
68
72
  await commandAdd(root, parsed.values)
@@ -137,9 +141,14 @@ async function commandInit(root: string, force: boolean): Promise<void> {
137
141
  clack.outro("Project breadcrumb is ready.")
138
142
  }
139
143
 
140
- async function commandDiscover(root: string, json: boolean): Promise<void> {
141
- const documents = await discoverAgentDocuments(root)
142
- if (json) {
144
+ async function commandDiscover(
145
+ root: string,
146
+ options: { json: boolean; includeDotDirectories: boolean },
147
+ ): Promise<void> {
148
+ const documents = await discoverAgentDocuments(root, {
149
+ includeDotDirectories: options.includeDotDirectories,
150
+ })
151
+ if (options.json) {
143
152
  console.log(JSON.stringify(documents, null, 2))
144
153
  return
145
154
  }