agents.yaml 0.1.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,71 +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')
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
+ )
20
41
 
21
- const discovered = await discoverAgentDocuments(root)
22
- expect(discovered).toEqual([{ path: './node_modules/direct-lib/AGENTS.md' }])
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
+ ])
23
49
 
24
- await addDocuments(root, [
25
- {
26
- path: discovered[0]!.path,
27
- },
28
- ])
50
+ await addDocuments(root, [discovered[0]!])
29
51
 
30
- await expect(loadAgentsFile(root)).resolves.toEqual({
31
- version: 1,
32
- documents: [
33
- {
34
- path: './node_modules/direct-lib/AGENTS.md',
35
- },
36
- ],
37
- })
38
- })
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
+ })
39
61
 
40
- it('does not discover an indirect dependency that has an AGENTS.md', async () => {
41
- const root = await createTempProject()
42
- const directAgentsPath = path.join(root, 'node_modules', 'direct-lib', 'AGENTS.md')
43
- const indirectAgentsPath = path.join(
44
- root,
45
- 'node_modules',
46
- 'direct-lib',
47
- 'node_modules',
48
- 'indirect-lib',
49
- 'AGENTS.md',
50
- )
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
+ })
51
73
 
52
- await mkdir(path.dirname(directAgentsPath), { recursive: true })
53
- await mkdir(path.dirname(indirectAgentsPath), { recursive: true })
54
- await writeFile(directAgentsPath, '# Direct dependency guidance\n', 'utf8')
55
- await writeFile(indirectAgentsPath, '# Indirect dependency guidance\n', 'utf8')
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
+ )
56
90
 
57
- await expect(discoverAgentDocuments(root)).resolves.toEqual([
58
- { path: './node_modules/direct-lib/AGENTS.md' },
59
- ])
60
- })
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
+ )
99
+
100
+ await expect(discoverAgentDocuments(root)).resolves.toEqual([
101
+ { path: "./node_modules/direct-lib/AGENTS.md" },
102
+ ])
103
+ })
104
+
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
+ )
127
+
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
+ })
61
135
  })
62
136
 
63
137
  async function createTempProject(): Promise<string> {
64
- const root = await mkdtemp(path.join(tmpdir(), 'agents-yaml-'))
65
- tempRoots.push(root)
138
+ const root = await mkdtemp(path.join(tmpdir(), "agents-yaml-"))
139
+ tempRoots.push(root)
66
140
 
67
- await writeFile(path.join(root, 'agents.yaml'), 'version: 1\n\ndocuments: []\n', 'utf8')
68
- 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
+ )
69
151
 
70
- return root
152
+ return root
71
153
  }
package/src/discover.ts CHANGED
@@ -1,115 +1,187 @@
1
- import { access, opendir } from 'node:fs/promises'
2
- import path from 'node:path'
3
- import { formatProjectPath } 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
6
+ path: string
7
+ description?: string
7
8
  }
8
9
 
9
10
  const skippedDirectories = new Set([
10
- '.git',
11
- '.hg',
12
- '.svn',
13
- '.turbo',
14
- '.next',
15
- 'coverage',
16
- 'dist',
17
- 'build',
11
+ ".git",
12
+ ".hg",
13
+ ".svn",
14
+ ".turbo",
15
+ ".next",
16
+ "coverage",
17
+ "dist",
18
+ "build",
18
19
  ])
19
20
 
20
- export async function discoverAgentDocuments(root: string): Promise<DiscoveredDocument[]> {
21
- const found: DiscoveredDocument[] = []
22
- await walk(root, root, found)
23
- return found
24
- .filter((document) => document.path !== './AGENTS.md')
25
- .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))
26
29
  }
27
30
 
28
- async function walk(root: string, directory: string, found: DiscoveredDocument[]): Promise<void> {
29
- let handle
30
- try {
31
- handle = await opendir(directory)
32
- } catch {
33
- return
34
- }
35
-
36
- for await (const entry of handle) {
37
- const absolutePath = path.join(directory, entry.name)
38
-
39
- if (entry.isDirectory()) {
40
- if (entry.name === 'node_modules') {
41
- await scanDirectNodeModules(root, absolutePath, found)
42
- continue
43
- }
44
-
45
- if (!skippedDirectories.has(entry.name)) {
46
- await walk(root, absolutePath, found)
47
- }
48
- continue
49
- }
50
-
51
- if (entry.isFile() && entry.name === 'AGENTS.md') {
52
- found.push({ path: formatProjectPath(root, absolutePath) })
53
- }
54
- }
31
+ export async function describeAgentDocument(
32
+ root: string,
33
+ agentsDocumentPath: string,
34
+ ): Promise<DiscoveredDocument> {
35
+ const absolutePath = resolveFromRoot(root, agentsDocumentPath)
36
+ return documentEntry(
37
+ root,
38
+ absolutePath,
39
+ await readPackageDescription(path.dirname(absolutePath)),
40
+ )
41
+ }
42
+
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
+ }
55
80
  }
56
81
 
57
82
  async function scanDirectNodeModules(
58
- root: string,
59
- nodeModulesPath: string,
60
- found: DiscoveredDocument[],
83
+ root: string,
84
+ nodeModulesPath: string,
85
+ found: DiscoveredDocument[],
61
86
  ): Promise<void> {
62
- let handle
63
- try {
64
- handle = await opendir(nodeModulesPath)
65
- } catch {
66
- return
67
- }
68
-
69
- for await (const entry of handle) {
70
- if ((!entry.isDirectory() && !entry.isSymbolicLink()) || entry.name.startsWith('.')) {
71
- continue
72
- }
73
-
74
- const packagePath = path.join(nodeModulesPath, entry.name)
75
- if (entry.name.startsWith('@')) {
76
- await scanScopedPackages(root, packagePath, found)
77
- continue
78
- }
79
-
80
- await addPackageAgentsDocument(root, packagePath, found)
81
- }
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
+ }
82
110
  }
83
111
 
84
112
  async function scanScopedPackages(
85
- root: string,
86
- scopePath: string,
87
- found: DiscoveredDocument[],
113
+ root: string,
114
+ scopePath: string,
115
+ found: DiscoveredDocument[],
88
116
  ): Promise<void> {
89
- let handle
90
- try {
91
- handle = await opendir(scopePath)
92
- } catch {
93
- return
94
- }
95
-
96
- for await (const entry of handle) {
97
- if (entry.isDirectory() || entry.isSymbolicLink()) {
98
- await addPackageAgentsDocument(root, path.join(scopePath, entry.name), found)
99
- }
100
- }
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
+ }
101
133
  }
102
134
 
103
135
  async function addPackageAgentsDocument(
104
- root: string,
105
- packagePath: string,
106
- found: DiscoveredDocument[],
136
+ root: string,
137
+ packagePath: string,
138
+ found: DiscoveredDocument[],
107
139
  ): Promise<void> {
108
- const agentsPath = path.join(packagePath, 'AGENTS.md')
109
- try {
110
- await access(agentsPath)
111
- found.push({ path: formatProjectPath(root, agentsPath) })
112
- } catch {
113
- // Packages without AGENTS.md are simply not candidates.
114
- }
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
+ }
153
+ }
154
+
155
+ async function documentEntry(
156
+ root: string,
157
+ agentsPath: string,
158
+ description: string | undefined,
159
+ ): Promise<DiscoveredDocument> {
160
+ return {
161
+ path: formatProjectPath(root, agentsPath),
162
+ ...(description ? { description } : {}),
163
+ }
164
+ }
165
+
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
+ }
183
+ }
184
+
185
+ function isPackageJson(value: unknown): value is Record<string, unknown> {
186
+ return typeof value === "object" && value !== null
115
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
  }