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.
- package/AGENTS.md +3 -2
- package/README.md +4 -3
- package/dist/index.mjs +89 -24
- package/package.json +8 -10
- package/src/agents-file.ts +186 -163
- package/src/discover.test.ts +137 -55
- package/src/discover.ts +165 -93
- package/src/index.ts +4 -4
- package/src/paths.ts +13 -8
- package/src/run.ts +290 -191
package/src/discover.test.ts
CHANGED
|
@@ -1,71 +1,153 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from
|
|
2
|
-
import { tmpdir } from
|
|
3
|
-
import path from
|
|
4
|
-
import { afterEach, describe, expect, it } from
|
|
5
|
-
import { addDocuments, loadAgentsFile } from
|
|
6
|
-
import { discoverAgentDocuments } from
|
|
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(
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
{
|
|
26
|
-
path: discovered[0]!.path,
|
|
27
|
-
},
|
|
28
|
-
])
|
|
50
|
+
await addDocuments(root, [discovered[0]!])
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
138
|
+
const root = await mkdtemp(path.join(tmpdir(), "agents-yaml-"))
|
|
139
|
+
tempRoots.push(root)
|
|
66
140
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
152
|
+
return root
|
|
71
153
|
}
|
package/src/discover.ts
CHANGED
|
@@ -1,115 +1,187 @@
|
|
|
1
|
-
import { access, opendir } from
|
|
2
|
-
import path from
|
|
3
|
-
import { formatProjectPath } from
|
|
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
|
-
|
|
6
|
+
path: string
|
|
7
|
+
description?: string
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
const skippedDirectories = new Set([
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
root: string,
|
|
84
|
+
nodeModulesPath: string,
|
|
85
|
+
found: DiscoveredDocument[],
|
|
61
86
|
): Promise<void> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
root: string,
|
|
114
|
+
scopePath: string,
|
|
115
|
+
found: DiscoveredDocument[],
|
|
88
116
|
): Promise<void> {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
root: string,
|
|
137
|
+
packagePath: string,
|
|
138
|
+
found: DiscoveredDocument[],
|
|
107
139
|
): Promise<void> {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
3
|
+
import { run } from "./run.ts"
|
|
4
4
|
|
|
5
5
|
run(process.argv.slice(2)).catch((error: unknown) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
1
|
+
import path from "node:path"
|
|
2
2
|
|
|
3
3
|
export function cwd(): string {
|
|
4
|
-
|
|
4
|
+
return process.cwd()
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function resolveFromRoot(root: string, input: string): string {
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
22
|
+
return relative.startsWith(".") ? relative : `./${relative}`
|
|
18
23
|
}
|