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 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 clack.multiselect({
436
- message: "Choose documents to enable",
437
- options: candidates.map((doc) => ({
438
- value: doc.path,
439
- label: doc.path
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.0",
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"
@@ -1,209 +1,224 @@
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'
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
- path: string
9
- description?: string | undefined
8
+ path: string
9
+ description?: string | undefined
10
10
  }
11
11
 
12
12
  export type AgentsFile = {
13
- version: 1
14
- documents: AgentsDocumentEntry[]
13
+ version: 1
14
+ documents: AgentsDocumentEntry[]
15
15
  }
16
16
 
17
17
  export type ValidationResult = {
18
- ok: boolean
19
- errors: string[]
20
- warnings: string[]
18
+ ok: boolean
19
+ errors: string[]
20
+ warnings: string[]
21
21
  }
22
22
 
23
23
  const fileSchema = z.object({
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
- ),
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
- 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
- }
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(root: string, file: AgentsFile): Promise<void> {
58
- const normalized = {
59
- version: file.version,
60
- documents: file.documents.map((doc) => ({
61
- path: doc.path,
62
- ...(doc.description ? { description: doc.description } : {}),
63
- })),
64
- }
65
-
66
- await writeFile(agentsPath(root), YAML.stringify(normalized, { lineWidth: 0 }), 'utf8')
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
- root: string,
71
- documents: AgentsDocumentEntry[],
77
+ root: string,
78
+ documents: AgentsDocumentEntry[],
72
79
  ): Promise<AgentsFile> {
73
- const file = await loadAgentsFile(root)
74
- const byPath = new Map(file.documents.map((doc) => [doc.path, doc]))
75
-
76
- for (const document of documents) {
77
- const existing = byPath.get(document.path)
78
- byPath.set(document.path, {
79
- path: document.path,
80
- ...(existing?.description ? { description: existing.description } : {}),
81
- ...(document.description ? { description: document.description } : {}),
82
- })
83
- }
84
-
85
- const next = {
86
- version: 1 as const,
87
- documents: [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path)),
88
- }
89
-
90
- await saveAgentsFile(root, next)
91
- return next
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
- root: string,
96
- paths: string[],
104
+ root: string,
105
+ paths: string[],
97
106
  ): Promise<{ file: AgentsFile; removed: string[] }> {
98
- const file = await loadAgentsFile(root)
99
- const pathSet = new Set(paths)
100
- const removed: string[] = []
101
- const documents = file.documents.filter((doc) => {
102
- if (pathSet.has(doc.path)) {
103
- removed.push(doc.path)
104
- return false
105
- }
106
-
107
- return true
108
- })
109
-
110
- const next = { version: 1 as const, documents }
111
- await saveAgentsFile(root, next)
112
- return { file: next, removed }
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(root: string): Promise<ValidationResult> {
116
- const errors: string[] = []
117
- const warnings: string[] = []
118
- let file: AgentsFile
119
-
120
- try {
121
- file = await loadAgentsFile(root)
122
- } catch (error) {
123
- return {
124
- ok: false,
125
- errors: [error instanceof Error ? error.message : String(error)],
126
- warnings,
127
- }
128
- }
129
-
130
- const seen = new Set<string>()
131
- for (const [index, document] of file.documents.entries()) {
132
- const label = `documents[${index}] ${document.path}`
133
-
134
- if (seen.has(document.path)) {
135
- errors.push(`${label}: duplicate path`)
136
- }
137
- seen.add(document.path)
138
-
139
- if (path.basename(document.path) !== 'AGENTS.md') {
140
- warnings.push(`${label}: path does not end with AGENTS.md`)
141
- }
142
-
143
- try {
144
- await access(resolveFromRoot(root, document.path))
145
- } catch {
146
- errors.push(`${label}: file does not exist`)
147
- }
148
- }
149
-
150
- try {
151
- const source = await readFile(path.join(root, 'AGENTS.md'), 'utf8')
152
- if (!source.includes('./agents.yaml') && !source.includes('agents.yaml')) {
153
- warnings.push('AGENTS.md does not mention agents.yaml')
154
- }
155
- } catch {
156
- warnings.push('AGENTS.md is missing the agents.yaml breadcrumb')
157
- }
158
-
159
- return {
160
- ok: errors.length === 0,
161
- errors,
162
- warnings,
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
- root: string,
168
- options: { force: boolean },
178
+ root: string,
179
+ options: { force: boolean },
169
180
  ): Promise<{ messages: string[] }> {
170
- const messages: string[] = []
171
-
172
- try {
173
- await access(agentsPath(root))
174
- messages.push('agents.yaml already exists')
175
- } catch {
176
- await saveAgentsFile(root, { version: 1, documents: [] })
177
- messages.push('created agents.yaml')
178
- }
179
-
180
- const projectAgentsPath = path.join(root, 'AGENTS.md')
181
- try {
182
- const source = await readFile(projectAgentsPath, 'utf8')
183
- if (source.includes('agents.yaml') && !options.force) {
184
- messages.push('AGENTS.md already mentions agents.yaml')
185
- return { messages }
186
- }
187
-
188
- const next =
189
- source.trimEnd().length === 0
190
- ? `# Project Instructions\n\n${breadcrumb}\n`
191
- : `${source.trimEnd()}\n\n${breadcrumb}\n`
192
- await writeFile(projectAgentsPath, next, 'utf8')
193
- messages.push('updated AGENTS.md')
194
- } catch (error) {
195
- if (!isNotFound(error)) throw error
196
- await writeFile(projectAgentsPath, `# Project Instructions\n\n${breadcrumb}\n`, 'utf8')
197
- messages.push('created AGENTS.md')
198
- }
199
-
200
- return { messages }
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
- return path.join(root, 'agents.yaml')
219
+ return path.join(root, "agents.yaml")
205
220
  }
206
221
 
207
222
  function isNotFound(error: unknown): boolean {
208
- return error instanceof Error && 'code' in error && error.code === 'ENOENT'
223
+ return error instanceof Error && "code" in error && error.code === "ENOENT"
209
224
  }