agents.yaml 0.2.0 → 0.2.1

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.
@@ -1,114 +1,153 @@
1
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
- import { tmpdir } from 'node:os'
3
- import path from 'node:path'
4
- import { afterEach, describe, expect, it } from 'vitest'
5
- import { addDocuments, loadAgentsFile } from './agents-file.ts'
6
- import { discoverAgentDocuments } from './discover.ts'
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import path from "node:path"
4
+ import { afterEach, describe, expect, it } from "vitest"
5
+ import { addDocuments, loadAgentsFile } from "./agents-file.ts"
6
+ import { discoverAgentDocuments } from "./discover.ts"
7
7
 
8
8
  const tempRoots: string[] = []
9
9
 
10
- describe('agents.yaml dependency discovery', () => {
11
- afterEach(async () => {
12
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
13
- })
10
+ describe("agents.yaml dependency discovery", () => {
11
+ afterEach(async () => {
12
+ await Promise.all(
13
+ tempRoots
14
+ .splice(0)
15
+ .map((root) => rm(root, { recursive: true, force: true })),
16
+ )
17
+ })
14
18
 
15
- it('discovers a direct dependency with an AGENTS.md and adds it to agents.yaml', async () => {
16
- const root = await createTempProject()
17
- const dependencyAgentsPath = path.join(root, 'node_modules', 'direct-lib', 'AGENTS.md')
18
- await mkdir(path.dirname(dependencyAgentsPath), { recursive: true })
19
- await writeFile(dependencyAgentsPath, '# Direct dependency guidance\n', 'utf8')
20
- await writeFile(
21
- path.join(root, 'node_modules', 'direct-lib', 'package.json'),
22
- JSON.stringify({
23
- name: 'direct-lib',
24
- description: 'Direct fixtures for testing agent guidance.',
25
- }),
26
- 'utf8',
27
- )
19
+ it("discovers a direct dependency with an AGENTS.md and adds it to agents.yaml", async () => {
20
+ const root = await createTempProject()
21
+ const dependencyAgentsPath = path.join(
22
+ root,
23
+ "node_modules",
24
+ "direct-lib",
25
+ "AGENTS.md",
26
+ )
27
+ await mkdir(path.dirname(dependencyAgentsPath), { recursive: true })
28
+ await writeFile(
29
+ dependencyAgentsPath,
30
+ "# Direct dependency guidance\n",
31
+ "utf8",
32
+ )
33
+ await writeFile(
34
+ path.join(root, "node_modules", "direct-lib", "package.json"),
35
+ JSON.stringify({
36
+ name: "direct-lib",
37
+ description: "Direct fixtures for testing agent guidance.",
38
+ }),
39
+ "utf8",
40
+ )
28
41
 
29
- const discovered = await discoverAgentDocuments(root)
30
- expect(discovered).toEqual([
31
- {
32
- path: './node_modules/direct-lib/AGENTS.md',
33
- description: 'Direct fixtures for testing agent guidance.',
34
- },
35
- ])
42
+ const discovered = await discoverAgentDocuments(root)
43
+ expect(discovered).toEqual([
44
+ {
45
+ path: "./node_modules/direct-lib/AGENTS.md",
46
+ description: "Direct fixtures for testing agent guidance.",
47
+ },
48
+ ])
36
49
 
37
- await addDocuments(root, [discovered[0]!])
50
+ await addDocuments(root, [discovered[0]!])
38
51
 
39
- await expect(loadAgentsFile(root)).resolves.toEqual({
40
- version: 1,
41
- documents: [
42
- {
43
- path: './node_modules/direct-lib/AGENTS.md',
44
- description: 'Direct fixtures for testing agent guidance.',
45
- },
46
- ],
47
- })
52
+ await expect(loadAgentsFile(root)).resolves.toEqual({
53
+ version: 1,
54
+ documents: [
55
+ {
56
+ path: "./node_modules/direct-lib/AGENTS.md",
57
+ description: "Direct fixtures for testing agent guidance.",
58
+ },
59
+ ],
60
+ })
48
61
 
49
- await addDocuments(root, [{ path: './node_modules/direct-lib/AGENTS.md' }])
50
- await expect(loadAgentsFile(root)).resolves.toEqual({
51
- version: 1,
52
- documents: [
53
- {
54
- path: './node_modules/direct-lib/AGENTS.md',
55
- description: 'Direct fixtures for testing agent guidance.',
56
- },
57
- ],
58
- })
59
- })
62
+ await addDocuments(root, [{ path: "./node_modules/direct-lib/AGENTS.md" }])
63
+ await expect(loadAgentsFile(root)).resolves.toEqual({
64
+ version: 1,
65
+ documents: [
66
+ {
67
+ path: "./node_modules/direct-lib/AGENTS.md",
68
+ description: "Direct fixtures for testing agent guidance.",
69
+ },
70
+ ],
71
+ })
72
+ })
60
73
 
61
- it('does not discover an indirect dependency that has an AGENTS.md', async () => {
62
- const root = await createTempProject()
63
- const directAgentsPath = path.join(root, 'node_modules', 'direct-lib', 'AGENTS.md')
64
- const indirectAgentsPath = path.join(
65
- root,
66
- 'node_modules',
67
- 'direct-lib',
68
- 'node_modules',
69
- 'indirect-lib',
70
- 'AGENTS.md',
71
- )
74
+ it("does not discover an indirect dependency that has an AGENTS.md", async () => {
75
+ const root = await createTempProject()
76
+ const directAgentsPath = path.join(
77
+ root,
78
+ "node_modules",
79
+ "direct-lib",
80
+ "AGENTS.md",
81
+ )
82
+ const indirectAgentsPath = path.join(
83
+ root,
84
+ "node_modules",
85
+ "direct-lib",
86
+ "node_modules",
87
+ "indirect-lib",
88
+ "AGENTS.md",
89
+ )
72
90
 
73
- await mkdir(path.dirname(directAgentsPath), { recursive: true })
74
- await mkdir(path.dirname(indirectAgentsPath), { recursive: true })
75
- await writeFile(directAgentsPath, '# Direct dependency guidance\n', 'utf8')
76
- await writeFile(indirectAgentsPath, '# Indirect dependency guidance\n', 'utf8')
91
+ await mkdir(path.dirname(directAgentsPath), { recursive: true })
92
+ await mkdir(path.dirname(indirectAgentsPath), { recursive: true })
93
+ await writeFile(directAgentsPath, "# Direct dependency guidance\n", "utf8")
94
+ await writeFile(
95
+ indirectAgentsPath,
96
+ "# Indirect dependency guidance\n",
97
+ "utf8",
98
+ )
77
99
 
78
- await expect(discoverAgentDocuments(root)).resolves.toEqual([
79
- { path: './node_modules/direct-lib/AGENTS.md' },
80
- ])
81
- })
100
+ await expect(discoverAgentDocuments(root)).resolves.toEqual([
101
+ { path: "./node_modules/direct-lib/AGENTS.md" },
102
+ ])
103
+ })
82
104
 
83
- it('includes scoped package descriptions when present', async () => {
84
- const root = await createTempProject()
85
- const dependencyPath = path.join(root, 'node_modules', '@scope', 'direct-lib')
86
- await mkdir(dependencyPath, { recursive: true })
87
- await writeFile(path.join(dependencyPath, 'AGENTS.md'), '# Scoped guidance\n', 'utf8')
88
- await writeFile(
89
- path.join(dependencyPath, 'package.json'),
90
- JSON.stringify({
91
- name: '@scope/direct-lib',
92
- description: 'Scoped package guidance breadcrumbs.',
93
- }),
94
- 'utf8',
95
- )
105
+ it("includes scoped package descriptions when present", async () => {
106
+ const root = await createTempProject()
107
+ const dependencyPath = path.join(
108
+ root,
109
+ "node_modules",
110
+ "@scope",
111
+ "direct-lib",
112
+ )
113
+ await mkdir(dependencyPath, { recursive: true })
114
+ await writeFile(
115
+ path.join(dependencyPath, "AGENTS.md"),
116
+ "# Scoped guidance\n",
117
+ "utf8",
118
+ )
119
+ await writeFile(
120
+ path.join(dependencyPath, "package.json"),
121
+ JSON.stringify({
122
+ name: "@scope/direct-lib",
123
+ description: "Scoped package guidance breadcrumbs.",
124
+ }),
125
+ "utf8",
126
+ )
96
127
 
97
- await expect(discoverAgentDocuments(root)).resolves.toEqual([
98
- {
99
- path: './node_modules/@scope/direct-lib/AGENTS.md',
100
- description: 'Scoped package guidance breadcrumbs.',
101
- },
102
- ])
103
- })
128
+ await expect(discoverAgentDocuments(root)).resolves.toEqual([
129
+ {
130
+ path: "./node_modules/@scope/direct-lib/AGENTS.md",
131
+ description: "Scoped package guidance breadcrumbs.",
132
+ },
133
+ ])
134
+ })
104
135
  })
105
136
 
106
137
  async function createTempProject(): Promise<string> {
107
- const root = await mkdtemp(path.join(tmpdir(), 'agents-yaml-'))
108
- tempRoots.push(root)
138
+ const root = await mkdtemp(path.join(tmpdir(), "agents-yaml-"))
139
+ tempRoots.push(root)
109
140
 
110
- await writeFile(path.join(root, 'agents.yaml'), 'version: 1\n\ndocuments: []\n', 'utf8')
111
- await writeFile(path.join(root, 'AGENTS.md'), 'Consult ./agents.yaml.\n', 'utf8')
141
+ await writeFile(
142
+ path.join(root, "agents.yaml"),
143
+ "version: 1\n\ndocuments: []\n",
144
+ "utf8",
145
+ )
146
+ await writeFile(
147
+ path.join(root, "AGENTS.md"),
148
+ "Consult ./agents.yaml.\n",
149
+ "utf8",
150
+ )
112
151
 
113
- return root
152
+ return root
114
153
  }
package/src/discover.ts CHANGED
@@ -1,158 +1,187 @@
1
- import { access, opendir, readFile } from 'node:fs/promises'
2
- import path from 'node:path'
3
- import { formatProjectPath, resolveFromRoot } from './paths.ts'
1
+ import { access, opendir, readFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import { formatProjectPath, resolveFromRoot } from "./paths.ts"
4
4
 
5
5
  export type DiscoveredDocument = {
6
- path: string
7
- description?: string
6
+ path: string
7
+ description?: string
8
8
  }
9
9
 
10
10
  const skippedDirectories = new Set([
11
- '.git',
12
- '.hg',
13
- '.svn',
14
- '.turbo',
15
- '.next',
16
- 'coverage',
17
- 'dist',
18
- 'build',
11
+ ".git",
12
+ ".hg",
13
+ ".svn",
14
+ ".turbo",
15
+ ".next",
16
+ "coverage",
17
+ "dist",
18
+ "build",
19
19
  ])
20
20
 
21
- export async function discoverAgentDocuments(root: string): Promise<DiscoveredDocument[]> {
22
- const found: DiscoveredDocument[] = []
23
- await walk(root, root, found)
24
- return found
25
- .filter((document) => document.path !== './AGENTS.md')
26
- .sort((left, right) => left.path.localeCompare(right.path))
21
+ export async function discoverAgentDocuments(
22
+ root: string,
23
+ ): Promise<DiscoveredDocument[]> {
24
+ const found: DiscoveredDocument[] = []
25
+ await walk(root, root, found)
26
+ return found
27
+ .filter((document) => document.path !== "./AGENTS.md")
28
+ .sort((left, right) => left.path.localeCompare(right.path))
27
29
  }
28
30
 
29
31
  export async function describeAgentDocument(
30
- root: string,
31
- agentsDocumentPath: string,
32
+ root: string,
33
+ agentsDocumentPath: string,
32
34
  ): Promise<DiscoveredDocument> {
33
- const absolutePath = resolveFromRoot(root, agentsDocumentPath)
34
- return documentEntry(root, absolutePath, await readPackageDescription(path.dirname(absolutePath)))
35
+ const absolutePath = resolveFromRoot(root, agentsDocumentPath)
36
+ return documentEntry(
37
+ root,
38
+ absolutePath,
39
+ await readPackageDescription(path.dirname(absolutePath)),
40
+ )
35
41
  }
36
42
 
37
- async function walk(root: string, directory: string, found: DiscoveredDocument[]): Promise<void> {
38
- let handle
39
- try {
40
- handle = await opendir(directory)
41
- } catch {
42
- return
43
- }
44
-
45
- for await (const entry of handle) {
46
- const absolutePath = path.join(directory, entry.name)
47
-
48
- if (entry.isDirectory()) {
49
- if (entry.name === 'node_modules') {
50
- await scanDirectNodeModules(root, absolutePath, found)
51
- continue
52
- }
53
-
54
- if (!skippedDirectories.has(entry.name)) {
55
- await walk(root, absolutePath, found)
56
- }
57
- continue
58
- }
59
-
60
- if (entry.isFile() && entry.name === 'AGENTS.md') {
61
- found.push(
62
- await documentEntry(
63
- root,
64
- absolutePath,
65
- await readPackageDescription(path.dirname(absolutePath)),
66
- ),
67
- )
68
- }
69
- }
43
+ async function walk(
44
+ root: string,
45
+ directory: string,
46
+ found: DiscoveredDocument[],
47
+ ): Promise<void> {
48
+ let handle
49
+ try {
50
+ handle = await opendir(directory)
51
+ } catch {
52
+ return
53
+ }
54
+
55
+ for await (const entry of handle) {
56
+ const absolutePath = path.join(directory, entry.name)
57
+
58
+ if (entry.isDirectory()) {
59
+ if (entry.name === "node_modules") {
60
+ await scanDirectNodeModules(root, absolutePath, found)
61
+ continue
62
+ }
63
+
64
+ if (!skippedDirectories.has(entry.name)) {
65
+ await walk(root, absolutePath, found)
66
+ }
67
+ continue
68
+ }
69
+
70
+ if (entry.isFile() && entry.name === "AGENTS.md") {
71
+ found.push(
72
+ await documentEntry(
73
+ root,
74
+ absolutePath,
75
+ await readPackageDescription(path.dirname(absolutePath)),
76
+ ),
77
+ )
78
+ }
79
+ }
70
80
  }
71
81
 
72
82
  async function scanDirectNodeModules(
73
- root: string,
74
- nodeModulesPath: string,
75
- found: DiscoveredDocument[],
83
+ root: string,
84
+ nodeModulesPath: string,
85
+ found: DiscoveredDocument[],
76
86
  ): Promise<void> {
77
- let handle
78
- try {
79
- handle = await opendir(nodeModulesPath)
80
- } catch {
81
- return
82
- }
83
-
84
- for await (const entry of handle) {
85
- if ((!entry.isDirectory() && !entry.isSymbolicLink()) || entry.name.startsWith('.')) {
86
- continue
87
- }
88
-
89
- const packagePath = path.join(nodeModulesPath, entry.name)
90
- if (entry.name.startsWith('@')) {
91
- await scanScopedPackages(root, packagePath, found)
92
- continue
93
- }
94
-
95
- await addPackageAgentsDocument(root, packagePath, found)
96
- }
87
+ let handle
88
+ try {
89
+ handle = await opendir(nodeModulesPath)
90
+ } catch {
91
+ return
92
+ }
93
+
94
+ for await (const entry of handle) {
95
+ if (
96
+ (!entry.isDirectory() && !entry.isSymbolicLink()) ||
97
+ entry.name.startsWith(".")
98
+ ) {
99
+ continue
100
+ }
101
+
102
+ const packagePath = path.join(nodeModulesPath, entry.name)
103
+ if (entry.name.startsWith("@")) {
104
+ await scanScopedPackages(root, packagePath, found)
105
+ continue
106
+ }
107
+
108
+ await addPackageAgentsDocument(root, packagePath, found)
109
+ }
97
110
  }
98
111
 
99
112
  async function scanScopedPackages(
100
- root: string,
101
- scopePath: string,
102
- found: DiscoveredDocument[],
113
+ root: string,
114
+ scopePath: string,
115
+ found: DiscoveredDocument[],
103
116
  ): Promise<void> {
104
- let handle
105
- try {
106
- handle = await opendir(scopePath)
107
- } catch {
108
- return
109
- }
110
-
111
- for await (const entry of handle) {
112
- if (entry.isDirectory() || entry.isSymbolicLink()) {
113
- await addPackageAgentsDocument(root, path.join(scopePath, entry.name), found)
114
- }
115
- }
117
+ let handle
118
+ try {
119
+ handle = await opendir(scopePath)
120
+ } catch {
121
+ return
122
+ }
123
+
124
+ for await (const entry of handle) {
125
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
126
+ await addPackageAgentsDocument(
127
+ root,
128
+ path.join(scopePath, entry.name),
129
+ found,
130
+ )
131
+ }
132
+ }
116
133
  }
117
134
 
118
135
  async function addPackageAgentsDocument(
119
- root: string,
120
- packagePath: string,
121
- found: DiscoveredDocument[],
136
+ root: string,
137
+ packagePath: string,
138
+ found: DiscoveredDocument[],
122
139
  ): Promise<void> {
123
- const agentsPath = path.join(packagePath, 'AGENTS.md')
124
- try {
125
- await access(agentsPath)
126
- found.push(await documentEntry(root, agentsPath, await readPackageDescription(packagePath)))
127
- } catch {
128
- // Packages without AGENTS.md are simply not candidates.
129
- }
140
+ const agentsPath = path.join(packagePath, "AGENTS.md")
141
+ try {
142
+ await access(agentsPath)
143
+ found.push(
144
+ await documentEntry(
145
+ root,
146
+ agentsPath,
147
+ await readPackageDescription(packagePath),
148
+ ),
149
+ )
150
+ } catch {
151
+ // Packages without AGENTS.md are simply not candidates.
152
+ }
130
153
  }
131
154
 
132
155
  async function documentEntry(
133
- root: string,
134
- agentsPath: string,
135
- description: string | undefined,
156
+ root: string,
157
+ agentsPath: string,
158
+ description: string | undefined,
136
159
  ): Promise<DiscoveredDocument> {
137
- return {
138
- path: formatProjectPath(root, agentsPath),
139
- ...(description ? { description } : {}),
140
- }
160
+ return {
161
+ path: formatProjectPath(root, agentsPath),
162
+ ...(description ? { description } : {}),
163
+ }
141
164
  }
142
165
 
143
- async function readPackageDescription(packagePath: string): Promise<string | undefined> {
144
- try {
145
- const source = await readFile(path.join(packagePath, 'package.json'), 'utf8')
146
- const parsed = JSON.parse(source) as unknown
147
- if (!isPackageJson(parsed) || typeof parsed.description !== 'string') return undefined
148
-
149
- const description = parsed.description.trim()
150
- return description && description.length > 0 ? description : undefined
151
- } catch {
152
- return undefined
153
- }
166
+ async function readPackageDescription(
167
+ packagePath: string,
168
+ ): Promise<string | undefined> {
169
+ try {
170
+ const source = await readFile(
171
+ path.join(packagePath, "package.json"),
172
+ "utf8",
173
+ )
174
+ const parsed = JSON.parse(source) as unknown
175
+ if (!isPackageJson(parsed) || typeof parsed.description !== "string")
176
+ return undefined
177
+
178
+ const description = parsed.description.trim()
179
+ return description && description.length > 0 ? description : undefined
180
+ } catch {
181
+ return undefined
182
+ }
154
183
  }
155
184
 
156
185
  function isPackageJson(value: unknown): value is Record<string, unknown> {
157
- return typeof value === 'object' && value !== null
186
+ return typeof value === "object" && value !== null
158
187
  }
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { run } from './run.ts'
3
+ import { run } from "./run.ts"
4
4
 
5
5
  run(process.argv.slice(2)).catch((error: unknown) => {
6
- const message = error instanceof Error ? error.message : String(error)
7
- console.error(`agents: ${message}`)
8
- process.exitCode = 1
6
+ const message = error instanceof Error ? error.message : String(error)
7
+ console.error(`agents: ${message}`)
8
+ process.exitCode = 1
9
9
  })
package/src/paths.ts CHANGED
@@ -1,18 +1,23 @@
1
- import path from 'node:path'
1
+ import path from "node:path"
2
2
 
3
3
  export function cwd(): string {
4
- return process.cwd()
4
+ return process.cwd()
5
5
  }
6
6
 
7
7
  export function resolveFromRoot(root: string, input: string): string {
8
- return path.isAbsolute(input) ? path.normalize(input) : path.resolve(root, input)
8
+ return path.isAbsolute(input)
9
+ ? path.normalize(input)
10
+ : path.resolve(root, input)
9
11
  }
10
12
 
11
13
  export function formatProjectPath(root: string, target: string): string {
12
- const relative = path.relative(root, target).split(path.sep).join(path.posix.sep)
13
- if (relative.startsWith('..')) {
14
- return target
15
- }
14
+ const relative = path
15
+ .relative(root, target)
16
+ .split(path.sep)
17
+ .join(path.posix.sep)
18
+ if (relative.startsWith("..")) {
19
+ return target
20
+ }
16
21
 
17
- return relative.startsWith('.') ? relative : `./${relative}`
22
+ return relative.startsWith(".") ? relative : `./${relative}`
18
23
  }