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/dist/index.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { MultiSelectPrompt } from "@clack/core";
|
|
2
3
|
import * as clack from "@clack/prompts";
|
|
4
|
+
import { styleText } from "node:util";
|
|
3
5
|
import { access, opendir, readFile, writeFile } from "node:fs/promises";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import * as YAML from "yaml";
|
|
@@ -432,20 +434,45 @@ async function interactive(root) {
|
|
|
432
434
|
clack.outro("No unlisted supplemental AGENTS.md files found.");
|
|
433
435
|
return;
|
|
434
436
|
}
|
|
435
|
-
const selected = await
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
})),
|
|
441
|
-
required: false
|
|
442
|
-
});
|
|
443
|
-
if (clack.isCancel(selected) || selected.length === 0) {
|
|
437
|
+
const selected = await chooseDocumentsToEnable(candidates.map((doc) => ({
|
|
438
|
+
value: doc.path,
|
|
439
|
+
label: doc.path
|
|
440
|
+
})));
|
|
441
|
+
if (clack.isCancel(selected) || selected === void 0 || selected.length === 0) {
|
|
444
442
|
clack.cancel("No documents selected.");
|
|
445
443
|
return;
|
|
446
444
|
}
|
|
447
445
|
await commandAdd(root, selected);
|
|
448
446
|
}
|
|
447
|
+
function chooseDocumentsToEnable(options) {
|
|
448
|
+
return new MultiSelectPrompt({
|
|
449
|
+
options,
|
|
450
|
+
required: false,
|
|
451
|
+
render() {
|
|
452
|
+
const prefix = `${styleText("cyan", clack.S_BAR)} `;
|
|
453
|
+
const selected = this.value ?? [];
|
|
454
|
+
return `${styleText("gray", clack.S_BAR)}
|
|
455
|
+
${clack.symbol(this.state)} Choose documents to enable
|
|
456
|
+
${prefix}${clack.limitOptions({
|
|
457
|
+
options: this.options,
|
|
458
|
+
cursor: this.cursor,
|
|
459
|
+
columnPadding: prefix.length,
|
|
460
|
+
style: (option, active) => styleDocumentOption(option, {
|
|
461
|
+
active,
|
|
462
|
+
selected: selected.includes(option.value)
|
|
463
|
+
})
|
|
464
|
+
}).join(`
|
|
465
|
+
${prefix}`)}
|
|
466
|
+
${styleText("cyan", clack.S_BAR_END)}
|
|
467
|
+
`;
|
|
468
|
+
}
|
|
469
|
+
}).prompt();
|
|
470
|
+
}
|
|
471
|
+
function styleDocumentOption(option, state) {
|
|
472
|
+
if (option.disabled) return `${styleText("gray", clack.S_CHECKBOX_INACTIVE)} ${styleText(["strikethrough", "gray"], option.label)}`;
|
|
473
|
+
const checkbox = state.selected ? styleText("green", clack.S_CHECKBOX_SELECTED) : state.active ? styleText("cyan", clack.S_CHECKBOX_ACTIVE) : styleText("dim", clack.S_CHECKBOX_INACTIVE);
|
|
474
|
+
return `${state.active ? styleText("cyan", ">") : " "} ${checkbox} ${state.active ? option.label : styleText("dim", option.label)}`;
|
|
475
|
+
}
|
|
449
476
|
//#endregion
|
|
450
477
|
//#region src/index.ts
|
|
451
478
|
run(process.argv.slice(2)).catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents.yaml",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A CLI for discovering and curating agent-readable documentation in agents.yaml.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@clack/core": "1.4.1",
|
|
20
21
|
"@clack/prompts": "1.5.1",
|
|
21
22
|
"yaml": "2.9.0",
|
|
22
23
|
"zod": "4.4.3"
|
package/src/agents-file.ts
CHANGED
|
@@ -1,209 +1,224 @@
|
|
|
1
|
-
import { access, readFile, writeFile } from
|
|
2
|
-
import path from
|
|
3
|
-
import * as YAML from
|
|
4
|
-
import { z } from
|
|
5
|
-
import { resolveFromRoot } from
|
|
1
|
+
import { access, readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import * as YAML from "yaml"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import { resolveFromRoot } from "./paths.ts"
|
|
6
6
|
|
|
7
7
|
export type AgentsDocumentEntry = {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
path: string
|
|
9
|
+
description?: string | undefined
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export type AgentsFile = {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
version: 1
|
|
14
|
+
documents: AgentsDocumentEntry[]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export type ValidationResult = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
ok: boolean
|
|
19
|
+
errors: string[]
|
|
20
|
+
warnings: string[]
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const fileSchema = z.object({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
version: z.literal(1),
|
|
25
|
+
documents: z.array(
|
|
26
|
+
z.object({
|
|
27
|
+
path: z.string().min(1),
|
|
28
|
+
description: z.string().min(1).optional(),
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
const breadcrumb = `Consult \`./agents.yaml\` when working with outside dependencies.`
|
|
34
34
|
|
|
35
35
|
export async function loadAgentsFile(root: string): Promise<AgentsFile> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
const filePath = agentsPath(root)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const source = await readFile(filePath, "utf8")
|
|
40
|
+
const parsed = YAML.parse(source) as unknown
|
|
41
|
+
return fileSchema.parse(parsed)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isNotFound(error)) {
|
|
44
|
+
return { version: 1, documents: [] }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (error instanceof z.ZodError) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid agents.yaml: ${error.issues.map((issue) => issue.message).join(", ")}`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export async function saveAgentsFile(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
export async function saveAgentsFile(
|
|
58
|
+
root: string,
|
|
59
|
+
file: AgentsFile,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const normalized = {
|
|
62
|
+
version: file.version,
|
|
63
|
+
documents: file.documents.map((doc) => ({
|
|
64
|
+
path: doc.path,
|
|
65
|
+
...(doc.description ? { description: doc.description } : {}),
|
|
66
|
+
})),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await writeFile(
|
|
70
|
+
agentsPath(root),
|
|
71
|
+
YAML.stringify(normalized, { lineWidth: 0 }),
|
|
72
|
+
"utf8",
|
|
73
|
+
)
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
export async function addDocuments(
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
root: string,
|
|
78
|
+
documents: AgentsDocumentEntry[],
|
|
72
79
|
): Promise<AgentsFile> {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
const file = await loadAgentsFile(root)
|
|
81
|
+
const byPath = new Map(file.documents.map((doc) => [doc.path, doc]))
|
|
82
|
+
|
|
83
|
+
for (const document of documents) {
|
|
84
|
+
const existing = byPath.get(document.path)
|
|
85
|
+
byPath.set(document.path, {
|
|
86
|
+
path: document.path,
|
|
87
|
+
...(existing?.description ? { description: existing.description } : {}),
|
|
88
|
+
...(document.description ? { description: document.description } : {}),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const next = {
|
|
93
|
+
version: 1 as const,
|
|
94
|
+
documents: [...byPath.values()].sort((left, right) =>
|
|
95
|
+
left.path.localeCompare(right.path),
|
|
96
|
+
),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await saveAgentsFile(root, next)
|
|
100
|
+
return next
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export async function removeDocuments(
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
root: string,
|
|
105
|
+
paths: string[],
|
|
97
106
|
): Promise<{ file: AgentsFile; removed: string[] }> {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
const file = await loadAgentsFile(root)
|
|
108
|
+
const pathSet = new Set(paths)
|
|
109
|
+
const removed: string[] = []
|
|
110
|
+
const documents = file.documents.filter((doc) => {
|
|
111
|
+
if (pathSet.has(doc.path)) {
|
|
112
|
+
removed.push(doc.path)
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return true
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const next = { version: 1 as const, documents }
|
|
120
|
+
await saveAgentsFile(root, next)
|
|
121
|
+
return { file: next, removed }
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
export async function validateAgentsFile(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
124
|
+
export async function validateAgentsFile(
|
|
125
|
+
root: string,
|
|
126
|
+
): Promise<ValidationResult> {
|
|
127
|
+
const errors: string[] = []
|
|
128
|
+
const warnings: string[] = []
|
|
129
|
+
let file: AgentsFile
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
file = await loadAgentsFile(root)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
137
|
+
warnings,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const seen = new Set<string>()
|
|
142
|
+
for (const [index, document] of file.documents.entries()) {
|
|
143
|
+
const label = `documents[${index}] ${document.path}`
|
|
144
|
+
|
|
145
|
+
if (seen.has(document.path)) {
|
|
146
|
+
errors.push(`${label}: duplicate path`)
|
|
147
|
+
}
|
|
148
|
+
seen.add(document.path)
|
|
149
|
+
|
|
150
|
+
if (path.basename(document.path) !== "AGENTS.md") {
|
|
151
|
+
warnings.push(`${label}: path does not end with AGENTS.md`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await access(resolveFromRoot(root, document.path))
|
|
156
|
+
} catch {
|
|
157
|
+
errors.push(`${label}: file does not exist`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const source = await readFile(path.join(root, "AGENTS.md"), "utf8")
|
|
163
|
+
if (!source.includes("./agents.yaml") && !source.includes("agents.yaml")) {
|
|
164
|
+
warnings.push("AGENTS.md does not mention agents.yaml")
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
warnings.push("AGENTS.md is missing the agents.yaml breadcrumb")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
ok: errors.length === 0,
|
|
172
|
+
errors,
|
|
173
|
+
warnings,
|
|
174
|
+
}
|
|
164
175
|
}
|
|
165
176
|
|
|
166
177
|
export async function initProject(
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
root: string,
|
|
179
|
+
options: { force: boolean },
|
|
169
180
|
): Promise<{ messages: string[] }> {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
181
|
+
const messages: string[] = []
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await access(agentsPath(root))
|
|
185
|
+
messages.push("agents.yaml already exists")
|
|
186
|
+
} catch {
|
|
187
|
+
await saveAgentsFile(root, { version: 1, documents: [] })
|
|
188
|
+
messages.push("created agents.yaml")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const projectAgentsPath = path.join(root, "AGENTS.md")
|
|
192
|
+
try {
|
|
193
|
+
const source = await readFile(projectAgentsPath, "utf8")
|
|
194
|
+
if (source.includes("agents.yaml") && !options.force) {
|
|
195
|
+
messages.push("AGENTS.md already mentions agents.yaml")
|
|
196
|
+
return { messages }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const next =
|
|
200
|
+
source.trimEnd().length === 0
|
|
201
|
+
? `# Project Instructions\n\n${breadcrumb}\n`
|
|
202
|
+
: `${source.trimEnd()}\n\n${breadcrumb}\n`
|
|
203
|
+
await writeFile(projectAgentsPath, next, "utf8")
|
|
204
|
+
messages.push("updated AGENTS.md")
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!isNotFound(error)) throw error
|
|
207
|
+
await writeFile(
|
|
208
|
+
projectAgentsPath,
|
|
209
|
+
`# Project Instructions\n\n${breadcrumb}\n`,
|
|
210
|
+
"utf8",
|
|
211
|
+
)
|
|
212
|
+
messages.push("created AGENTS.md")
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { messages }
|
|
201
216
|
}
|
|
202
217
|
|
|
203
218
|
function agentsPath(root: string): string {
|
|
204
|
-
|
|
219
|
+
return path.join(root, "agents.yaml")
|
|
205
220
|
}
|
|
206
221
|
|
|
207
222
|
function isNotFound(error: unknown): boolean {
|
|
208
|
-
|
|
223
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
209
224
|
}
|