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.
- package/dist/index.mjs +36 -9
- package/package.json +2 -1
- package/src/agents-file.ts +185 -170
- package/src/discover.test.ts +134 -95
- package/src/discover.ts +152 -123
- package/src/index.ts +4 -4
- package/src/paths.ts +13 -8
- package/src/run.ts +285 -196
package/src/discover.test.ts
CHANGED
|
@@ -1,114 +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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
50
|
+
await addDocuments(root, [discovered[0]!])
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
await expect(discoverAgentDocuments(root)).resolves.toEqual([
|
|
101
|
+
{ path: "./node_modules/direct-lib/AGENTS.md" },
|
|
102
|
+
])
|
|
103
|
+
})
|
|
82
104
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
108
|
-
|
|
138
|
+
const root = await mkdtemp(path.join(tmpdir(), "agents-yaml-"))
|
|
139
|
+
tempRoots.push(root)
|
|
109
140
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
152
|
+
return root
|
|
114
153
|
}
|
package/src/discover.ts
CHANGED
|
@@ -1,158 +1,187 @@
|
|
|
1
|
-
import { access, opendir, readFile } from
|
|
2
|
-
import path from
|
|
3
|
-
import { formatProjectPath, resolveFromRoot } 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
|
-
|
|
7
|
-
|
|
6
|
+
path: string
|
|
7
|
+
description?: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const skippedDirectories = new Set([
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
+
root: string,
|
|
33
|
+
agentsDocumentPath: string,
|
|
32
34
|
): Promise<DiscoveredDocument> {
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
root: string,
|
|
84
|
+
nodeModulesPath: string,
|
|
85
|
+
found: DiscoveredDocument[],
|
|
76
86
|
): Promise<void> {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
root: string,
|
|
114
|
+
scopePath: string,
|
|
115
|
+
found: DiscoveredDocument[],
|
|
103
116
|
): Promise<void> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
root: string,
|
|
137
|
+
packagePath: string,
|
|
138
|
+
found: DiscoveredDocument[],
|
|
122
139
|
): Promise<void> {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
root: string,
|
|
157
|
+
agentsPath: string,
|
|
158
|
+
description: string | undefined,
|
|
136
159
|
): Promise<DiscoveredDocument> {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
return {
|
|
161
|
+
path: formatProjectPath(root, agentsPath),
|
|
162
|
+
...(description ? { description } : {}),
|
|
163
|
+
}
|
|
141
164
|
}
|
|
142
165
|
|
|
143
|
-
async function readPackageDescription(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|