agents.yaml 0.2.1 → 0.2.3

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.d.mts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}
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
@@ -147,7 +147,8 @@ async function initProject(root, options) {
147
147
  messages.push("AGENTS.md already mentions agents.yaml");
148
148
  return { messages };
149
149
  }
150
- await writeFile(projectAgentsPath, source.trimEnd().length === 0 ? `# Project Instructions\n\n${breadcrumb}\n` : `${source.trimEnd()}\n\n${breadcrumb}\n`, "utf8");
150
+ const next = source.trimEnd().length === 0 ? `# Project Instructions\n\n${breadcrumb}\n` : `${source.trimEnd()}\n\n${breadcrumb}\n`;
151
+ await writeFile(projectAgentsPath, next, "utf8");
151
152
  messages.push("updated AGENTS.md");
152
153
  } catch (error) {
153
154
  if (!isNotFound(error)) throw error;
@@ -164,7 +165,7 @@ function isNotFound(error) {
164
165
  }
165
166
  //#endregion
166
167
  //#region src/discover.ts
167
- const skippedDirectories = new Set([
168
+ const skippedDirectories = /* @__PURE__ */ new Set([
168
169
  ".git",
169
170
  ".hg",
170
171
  ".svn",
@@ -174,16 +175,16 @@ const skippedDirectories = new Set([
174
175
  "dist",
175
176
  "build"
176
177
  ]);
177
- async function discoverAgentDocuments(root) {
178
+ async function discoverAgentDocuments(root, options = {}) {
178
179
  const found = [];
179
- await walk(root, root, found);
180
+ await walk(root, root, found, options);
180
181
  return found.filter((document) => document.path !== "./AGENTS.md").sort((left, right) => left.path.localeCompare(right.path));
181
182
  }
182
183
  async function describeAgentDocument(root, agentsDocumentPath) {
183
184
  const absolutePath = resolveFromRoot(root, agentsDocumentPath);
184
185
  return documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath)));
185
186
  }
186
- async function walk(root, directory, found) {
187
+ async function walk(root, directory, found, options) {
187
188
  let handle;
188
189
  try {
189
190
  handle = await opendir(directory);
@@ -197,12 +198,16 @@ async function walk(root, directory, found) {
197
198
  await scanDirectNodeModules(root, absolutePath, found);
198
199
  continue;
199
200
  }
200
- if (!skippedDirectories.has(entry.name)) await walk(root, absolutePath, found);
201
+ if (!shouldSkipDirectory(entry.name, options)) await walk(root, absolutePath, found, options);
201
202
  continue;
202
203
  }
203
204
  if (entry.isFile() && entry.name === "AGENTS.md") found.push(await documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath))));
204
205
  }
205
206
  }
207
+ function shouldSkipDirectory(name, options) {
208
+ if (skippedDirectories.has(name)) return true;
209
+ return !options.includeDotDirectories && name.startsWith(".");
210
+ }
206
211
  async function scanDirectNodeModules(root, nodeModulesPath, found) {
207
212
  let handle;
208
213
  try {
@@ -263,7 +268,7 @@ const helpText = `agents
263
268
  Usage:
264
269
  agents
265
270
  agents init [--force]
266
- agents discover [--json]
271
+ agents discover [--json] [--include-dot-directories]
267
272
  agents add <path...>
268
273
  agents remove <path...>
269
274
  agents validate [--json]
@@ -286,7 +291,10 @@ async function run(argv) {
286
291
  await commandInit(root, parsed.flags.get("force") === true);
287
292
  return;
288
293
  case "discover":
289
- await commandDiscover(root, parsed.flags.get("json") === true);
294
+ await commandDiscover(root, {
295
+ json: parsed.flags.get("json") === true,
296
+ includeDotDirectories: parsed.flags.get("include-dot-directories") === true
297
+ });
290
298
  return;
291
299
  case "add":
292
300
  await commandAdd(root, parsed.values);
@@ -353,9 +361,9 @@ async function commandInit(root, force) {
353
361
  clack.note(result.messages.join("\n"), "Updated");
354
362
  clack.outro("Project breadcrumb is ready.");
355
363
  }
356
- async function commandDiscover(root, json) {
357
- const documents = await discoverAgentDocuments(root);
358
- if (json) {
364
+ async function commandDiscover(root, options) {
365
+ const documents = await discoverAgentDocuments(root, { includeDotDirectories: options.includeDotDirectories });
366
+ if (options.json) {
359
367
  console.log(JSON.stringify(documents, null, 2));
360
368
  return;
361
369
  }
@@ -444,8 +452,8 @@ async function interactive(root) {
444
452
  }
445
453
  await commandAdd(root, selected);
446
454
  }
447
- function chooseDocumentsToEnable(options) {
448
- return new MultiSelectPrompt({
455
+ async function chooseDocumentsToEnable(options) {
456
+ const selected = await new MultiSelectPrompt({
449
457
  options,
450
458
  required: false,
451
459
  render() {
@@ -467,6 +475,7 @@ ${styleText("cyan", clack.S_BAR_END)}
467
475
  `;
468
476
  }
469
477
  }).prompt();
478
+ return typeof selected === "symbol" ? clack.CANCEL_SYMBOL : selected;
470
479
  }
471
480
  function styleDocumentOption(option, state) {
472
481
  if (option.disabled) return `${styleText("gray", clack.S_CHECKBOX_INACTIVE)} ${styleText(["strikethrough", "gray"], option.label)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents.yaml",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A CLI for discovering and curating agent-readable documentation in agents.yaml.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,20 +17,21 @@
17
17
  ],
18
18
  "type": "module",
19
19
  "dependencies": {
20
- "@clack/core": "1.4.1",
21
- "@clack/prompts": "1.5.1",
22
- "yaml": "2.9.0",
23
- "zod": "4.4.3"
20
+ "@clack/core": "1.5.1",
21
+ "@clack/prompts": "1.8.1",
22
+ "yaml": "2.9.1",
23
+ "zod": "4.6.5"
24
24
  },
25
25
  "devDependencies": {
26
- "@types/node": "25.9.2",
27
- "typescript": "6.0.3"
26
+ "@types/node": "25.9.6",
27
+ "typescript": "7.0.2"
28
28
  },
29
29
  "engines": {
30
- "node": "26.3.0",
31
- "pnpm": "11.5.2"
30
+ "node": "26.8.2",
31
+ "pnpm": "12.4.1"
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
  }
@@ -281,10 +290,10 @@ async function interactive(root: string): Promise<void> {
281
290
  await commandAdd(root, selected)
282
291
  }
283
292
 
284
- function chooseDocumentsToEnable(
293
+ async function chooseDocumentsToEnable(
285
294
  options: DocumentOption[],
286
- ): Promise<string[] | symbol | undefined> {
287
- return new MultiSelectPrompt<DocumentOption>({
295
+ ): Promise<string[] | typeof clack.CANCEL_SYMBOL | undefined> {
296
+ const selected = await new MultiSelectPrompt<DocumentOption>({
288
297
  options,
289
298
  required: false,
290
299
  render() {
@@ -308,6 +317,8 @@ ${styleText("cyan", clack.S_BAR_END)}
308
317
  `
309
318
  },
310
319
  }).prompt()
320
+
321
+ return typeof selected === "symbol" ? clack.CANCEL_SYMBOL : selected
311
322
  }
312
323
 
313
324
  function styleDocumentOption(