nuxt-agent-md 0.0.1-alpha.1 → 0.2.0

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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # nuxt-agent-md
2
+
3
+ Generate `AGENTS.md` with Nuxt documentation for AI coding agents.
4
+
5
+ Inspired by [Vercel's blog post](https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals) showing that `AGENTS.md` with a documentation index achieves 100% success rate on agent evaluations.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ # Run directly with bunx
11
+ bunx nuxt-agent-md
12
+
13
+ # Or install globally
14
+ bun add -g nuxt-agent-md
15
+ nuxt-agent-md
16
+ ```
17
+
18
+ ## What it does
19
+
20
+ 1. Detects your Nuxt version from `package.json`
21
+ 2. Downloads the corresponding `@nuxt/docs` documentation
22
+ 3. Generates a minified index of all documentation files
23
+ 4. Creates/updates `AGENTS.md` with the index and key Nuxt patterns
24
+
25
+ ## Options
26
+
27
+ ```
28
+ -d, --docs-dir <path> Directory to store docs (default: .nuxt-docs)
29
+ -o, --output <path> Output AGENTS.md path (default: AGENTS.md)
30
+ -v, --nuxt-version <ver> Nuxt docs version (auto-detected from package.json)
31
+ --minify Generate minified index (default: true)
32
+ --no-minify Generate full index instead of minified
33
+ --dry-run Show what would be done without making changes
34
+ -h, --help Show this help message
35
+ ```
36
+
37
+ ## Examples
38
+
39
+ ```bash
40
+ # Auto-detect Nuxt version from package.json
41
+ nuxt-agent-md
42
+
43
+ # Use specific Nuxt version
44
+ nuxt-agent-md -v 4.0.0
45
+
46
+ # Generate full (non-minified) index
47
+ nuxt-agent-md --no-minify
48
+
49
+ # Custom output paths
50
+ nuxt-agent-md -d .docs -o CLAUDE.md
51
+
52
+ # Preview changes without writing
53
+ nuxt-agent-md --dry-run
54
+ ```
55
+
56
+ ## Output
57
+
58
+ The tool generates:
59
+
60
+ 1. `.nuxt-docs/` - Directory containing raw markdown documentation
61
+ 2. `AGENTS.md` - File with minified index pointing to the docs
62
+
63
+ The index format is pipe-delimited for minimal token usage:
64
+
65
+ ```
66
+ CATEGORY|path/to/file.md|keyword1,keyword2,keyword3
67
+ ```
68
+
69
+ ## Why this approach?
70
+
71
+ From Vercel's research:
72
+
73
+ | Approach | Success Rate |
74
+ |----------|--------------|
75
+ | Baseline (no docs) | 53% |
76
+ | Skills | 53% |
77
+ | Skills with explicit instructions | 79% |
78
+ | **AGENTS.md with docs index** | **100%** |
79
+
80
+ The key insight: providing a compressed index in `AGENTS.md` that points to detailed documentation files gives agents immediate access to accurate API information without decision overhead.
81
+
82
+ ## License
83
+
84
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "node:util";
5
+
6
+ // src/index.ts
7
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, appendFileSync } from "node:fs";
8
+
9
+ // src/detect.ts
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ async function detectNuxtVersion(cwd = process.cwd()) {
13
+ const packageJsonPath = join(cwd, "package.json");
14
+ if (!existsSync(packageJsonPath)) {
15
+ throw new Error("No package.json found. Run this command in a Nuxt project root.");
16
+ }
17
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
18
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
19
+ const nuxtVersion = deps.nuxt;
20
+ if (!nuxtVersion) {
21
+ throw new Error("Nuxt is not installed in this project. Add nuxt to your dependencies.");
22
+ }
23
+ const cleanVersion = nuxtVersion.replace(/[\^~>=<]/g, "").split(" ")[0];
24
+ if (cleanVersion === "latest" || !cleanVersion.match(/^\d/)) {
25
+ return "4.0.0";
26
+ }
27
+ return cleanVersion;
28
+ }
29
+ function mapToDocsVersion(nuxtVersion) {
30
+ const major = parseInt(nuxtVersion.split(".")[0]);
31
+ if (major >= 4) {
32
+ return "4.3.0";
33
+ } else if (major === 3) {
34
+ return "3.21.0";
35
+ }
36
+ throw new Error(`Unsupported Nuxt version: ${nuxtVersion}. Only Nuxt 3.x and 4.x are supported.`);
37
+ }
38
+
39
+ // src/download.ts
40
+ import { spawnSync } from "node:child_process";
41
+ import { existsSync as existsSync2, mkdirSync, rmSync, renameSync } from "node:fs";
42
+ import { join as join2 } from "node:path";
43
+ async function downloadDocs(version, targetDir) {
44
+ const tempDir = ".nuxt-docs-temp";
45
+ if (existsSync2(targetDir))
46
+ rmSync(targetDir, { recursive: true });
47
+ if (existsSync2(tempDir))
48
+ rmSync(tempDir, { recursive: true });
49
+ mkdirSync(tempDir, { recursive: true });
50
+ try {
51
+ const pack = spawnSync("npm", ["pack", `@nuxt/docs@${version}`, "--pack-destination", tempDir], {
52
+ encoding: "utf-8"
53
+ });
54
+ if (pack.status !== 0)
55
+ throw new Error(pack.stderr);
56
+ const tarball = pack.stdout.trim().split(`
57
+ `).pop();
58
+ if (!tarball)
59
+ throw new Error("No tarball");
60
+ const extract = spawnSync("tar", ["-xf", join2(tempDir, tarball), "-C", tempDir], {
61
+ encoding: "utf-8"
62
+ });
63
+ if (extract.status !== 0)
64
+ throw new Error(extract.stderr);
65
+ renameSync(join2(tempDir, "package"), targetDir);
66
+ } finally {
67
+ if (existsSync2(tempDir))
68
+ rmSync(tempDir, { recursive: true });
69
+ }
70
+ }
71
+
72
+ // src/generate.ts
73
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
74
+ import { join as join3, relative } from "node:path";
75
+ async function generateIndex(docsDir) {
76
+ const entries = [];
77
+ walkDir(docsDir, docsDir, entries);
78
+ return {
79
+ entries,
80
+ minified: generateMinifiedIndex(entries, docsDir),
81
+ full: generateFullIndex(entries, docsDir)
82
+ };
83
+ }
84
+ function walkDir(dir, baseDir, entries) {
85
+ let items;
86
+ try {
87
+ items = readdirSync(dir);
88
+ } catch {
89
+ return;
90
+ }
91
+ for (const item of items) {
92
+ const fullPath = join3(dir, item);
93
+ let stat;
94
+ try {
95
+ stat = statSync(fullPath);
96
+ } catch {
97
+ continue;
98
+ }
99
+ if (stat.isDirectory()) {
100
+ if (!item.startsWith(".") && item !== "node_modules") {
101
+ walkDir(fullPath, baseDir, entries);
102
+ }
103
+ } else if (item.endsWith(".md") && !item.startsWith(".")) {
104
+ const relativePath = relative(baseDir, fullPath);
105
+ const content = readFileSync2(fullPath, "utf-8");
106
+ const pathParts = relativePath.split("/");
107
+ const category = pathParts[0].replace(/^\d+\./, "").toUpperCase().replace(/-/g, "_");
108
+ entries.push({
109
+ path: relativePath,
110
+ title: extractTitle(content),
111
+ keywords: extractKeywords(content, relativePath),
112
+ category
113
+ });
114
+ }
115
+ }
116
+ }
117
+ function extractTitle(content) {
118
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
119
+ if (frontmatterMatch) {
120
+ const titleMatch = frontmatterMatch[1].match(/^title:\s*['"]?([^'"\n]+)['"]?/m);
121
+ if (titleMatch) {
122
+ return titleMatch[1].trim();
123
+ }
124
+ }
125
+ const h1Match = content.match(/^#\s+(.+)$/m);
126
+ return h1Match ? h1Match[1].trim() : "";
127
+ }
128
+ function extractKeywords(content, path) {
129
+ const keywords = new Set;
130
+ const pathParts = path.replace(/\.md$/, "").split("/");
131
+ for (const part of pathParts) {
132
+ const clean = part.replace(/^\d+\./, "").replace(/-/g, " ");
133
+ if (clean.length > 2) {
134
+ keywords.add(clean);
135
+ }
136
+ }
137
+ const codeMatches = content.match(/`([a-zA-Z$][a-zA-Z0-9$]*(?:\(\))?)`/g) || [];
138
+ for (const match of codeMatches) {
139
+ const clean = match.replace(/`/g, "").replace(/\(\)$/, "");
140
+ if (clean.length > 2 && !["true", "false", "null", "undefined", "string", "number", "boolean", "object", "array"].includes(clean.toLowerCase())) {
141
+ keywords.add(clean);
142
+ }
143
+ }
144
+ const nuxtTerms = [
145
+ "useFetch",
146
+ "useAsyncData",
147
+ "useState",
148
+ "useCookie",
149
+ "useRuntimeConfig",
150
+ "useRoute",
151
+ "useRouter",
152
+ "useHead",
153
+ "useSeoMeta",
154
+ "useNuxtApp",
155
+ "definePageMeta",
156
+ "defineNuxtConfig",
157
+ "defineEventHandler",
158
+ "defineNuxtPlugin",
159
+ "defineNuxtRouteMiddleware",
160
+ "defineNuxtModule",
161
+ "NuxtLink",
162
+ "NuxtPage",
163
+ "NuxtLayout",
164
+ "ClientOnly",
165
+ "navigateTo",
166
+ "abortNavigation",
167
+ "createError",
168
+ "$fetch",
169
+ "nitro",
170
+ "h3",
171
+ "server/api",
172
+ "server/middleware"
173
+ ];
174
+ for (const term of nuxtTerms) {
175
+ if (content.includes(term)) {
176
+ keywords.add(term);
177
+ }
178
+ }
179
+ return [...keywords].slice(0, 15);
180
+ }
181
+ function generateMinifiedIndex(entries, docsDir) {
182
+ const sorted = [...entries].sort((a, b) => {
183
+ if (a.category !== b.category)
184
+ return a.category.localeCompare(b.category);
185
+ return a.path.localeCompare(b.path);
186
+ });
187
+ const lines = [];
188
+ for (const entry of sorted) {
189
+ const keywords = entry.keywords.slice(0, 6).join(",");
190
+ lines.push(`${entry.category}|${docsDir}/${entry.path}|${keywords}`);
191
+ }
192
+ return lines.join(`
193
+ `);
194
+ }
195
+ function generateFullIndex(entries, docsDir) {
196
+ const categories = new Map;
197
+ for (const entry of entries) {
198
+ if (!categories.has(entry.category)) {
199
+ categories.set(entry.category, []);
200
+ }
201
+ categories.get(entry.category).push(entry);
202
+ }
203
+ let output = "";
204
+ const sortedCategories = [...categories.entries()].sort((a, b) => a[0].localeCompare(b[0]));
205
+ for (const [category, files] of sortedCategories) {
206
+ output += `
207
+ ### ${category.toLowerCase().replace(/_/g, " ")}
208
+ `;
209
+ const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
210
+ for (const file of sortedFiles) {
211
+ const title = file.title || file.path.split("/").pop()?.replace(/\.md$/, "") || file.path;
212
+ output += `- [${title}](${docsDir}/${file.path})`;
213
+ if (file.keywords.length > 0) {
214
+ output += ` - \`${file.keywords.slice(0, 5).join("`, `")}\``;
215
+ }
216
+ output += `
217
+ `;
218
+ }
219
+ }
220
+ return output;
221
+ }
222
+
223
+ // src/inject.ts
224
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "node:fs";
225
+ var START_MARKER = "<!-- NUXT_DOCS_START -->";
226
+ var END_MARKER = "<!-- NUXT_DOCS_END -->";
227
+ async function injectAgentsMd(outputPath, index, nuxtVersion, docsDir, minify) {
228
+ let content = "";
229
+ if (existsSync3(outputPath)) {
230
+ content = readFileSync3(outputPath, "utf-8");
231
+ }
232
+ const nuxtSection = generateNuxtSection(index, nuxtVersion, docsDir, minify);
233
+ if (content.includes(START_MARKER) && content.includes(END_MARKER)) {
234
+ const regex = new RegExp(`${START_MARKER}[\\s\\S]*${END_MARKER}`, "m");
235
+ content = content.replace(regex, nuxtSection);
236
+ } else if (content.length > 0) {
237
+ content = content.trimEnd() + `
238
+
239
+ ` + nuxtSection;
240
+ } else {
241
+ content = generateFullAgentsMd(nuxtSection);
242
+ }
243
+ writeFileSync(outputPath, content);
244
+ }
245
+ function generateNuxtSection(index, nuxtVersion, docsDir, minify) {
246
+ const majorVersion = nuxtVersion.split(".")[0];
247
+ return `${START_MARKER}
248
+ ## Nuxt Documentation
249
+
250
+ This project uses **Nuxt ${majorVersion}** (v${nuxtVersion}).
251
+
252
+ When working with Nuxt APIs, ALWAYS read the referenced documentation files before making changes.
253
+
254
+ ### Quick Reference Index
255
+
256
+ \`\`\`
257
+ ${minify ? index.minified : index.full}
258
+ \`\`\`
259
+
260
+ ### Key Nuxt ${majorVersion} Patterns
261
+
262
+ #### Data Fetching
263
+ - Use \`useFetch\` for component-level data fetching (auto-deduped, SSR-safe)
264
+ - Use \`useAsyncData\` when you need more control over the key/handler
265
+ - Use \`$fetch\` in event handlers and server routes (NOT in setup for SSR)
266
+ - Always handle \`pending\` and \`error\` states
267
+
268
+ #### Server Routes
269
+ - Files in \`server/api/\` become API endpoints
270
+ - Use \`defineEventHandler\` for all handlers
271
+ - Access body with \`readBody(event)\`
272
+ - Access query with \`getQuery(event)\`
273
+ - Access params with \`event.context.params\`
274
+
275
+ #### State Management
276
+ - Use \`useState\` for SSR-friendly reactive state
277
+ - Use \`useCookie\` for cookie-based state
278
+ - Use \`useRuntimeConfig\` for environment variables
279
+
280
+ #### Routing & Navigation
281
+ - Use \`definePageMeta\` for page-level config
282
+ - Use \`navigateTo\` for programmatic navigation
283
+ - Use \`useRoute\` and \`useRouter\` for route info
284
+
285
+ #### Configuration
286
+ - \`nuxt.config.ts\` for build-time config
287
+ - \`runtimeConfig\` for environment variables (private/public)
288
+ - \`app.config.ts\` for public runtime config
289
+
290
+ ${END_MARKER}`;
291
+ }
292
+ function generateFullAgentsMd(nuxtSection) {
293
+ return `# AGENTS.md
294
+
295
+ This file provides documentation references for AI coding agents.
296
+
297
+ ${nuxtSection}
298
+ `;
299
+ }
300
+
301
+ // src/index.ts
302
+ function updateGitignore(entry) {
303
+ const path = ".gitignore";
304
+ if (!existsSync4(path)) {
305
+ writeFileSync2(path, entry + `
306
+ `);
307
+ return true;
308
+ }
309
+ const content = readFileSync4(path, "utf-8");
310
+ if (content.split(`
311
+ `).some((l) => l.trim() === entry))
312
+ return false;
313
+ appendFileSync(path, (content.endsWith(`
314
+ `) ? "" : `
315
+ `) + entry + `
316
+ `);
317
+ return true;
318
+ }
319
+ async function generateAgentsMd(options = {}) {
320
+ const {
321
+ docsDir = ".nuxt-docs",
322
+ outputPath = "AGENTS.md",
323
+ nuxtVersion,
324
+ minify = true,
325
+ dryRun = false
326
+ } = options;
327
+ const version = nuxtVersion || await detectNuxtVersion();
328
+ const docsVersion = mapToDocsVersion(version);
329
+ if (dryRun) {
330
+ console.log(`[dry-run] Would download @nuxt/docs@${docsVersion} to ${docsDir}`);
331
+ console.log(`[dry-run] Would generate index and inject into ${outputPath}`);
332
+ return;
333
+ }
334
+ console.log(`Downloading @nuxt/docs@${docsVersion}...`);
335
+ await downloadDocs(docsVersion, docsDir);
336
+ const index = await generateIndex(docsDir);
337
+ console.log(`Indexed ${index.entries.length} files`);
338
+ await injectAgentsMd(outputPath, index, version, docsDir, minify);
339
+ if (updateGitignore(docsDir)) {
340
+ console.log(`Added ${docsDir} to .gitignore`);
341
+ }
342
+ console.log(`Generated ${outputPath}`);
343
+ }
344
+
345
+ // src/cli.ts
346
+ var { values } = parseArgs({
347
+ args: process.argv.slice(2),
348
+ options: {
349
+ "docs-dir": {
350
+ type: "string",
351
+ short: "d",
352
+ default: ".nuxt-docs"
353
+ },
354
+ output: {
355
+ type: "string",
356
+ short: "o",
357
+ default: "AGENTS.md"
358
+ },
359
+ "nuxt-version": {
360
+ type: "string",
361
+ short: "v"
362
+ },
363
+ minify: {
364
+ type: "boolean",
365
+ default: true
366
+ },
367
+ "no-minify": {
368
+ type: "boolean",
369
+ default: false
370
+ },
371
+ "dry-run": {
372
+ type: "boolean",
373
+ default: false
374
+ },
375
+ help: {
376
+ type: "boolean",
377
+ short: "h",
378
+ default: false
379
+ }
380
+ },
381
+ strict: true,
382
+ allowPositionals: false
383
+ });
384
+ if (values.help) {
385
+ console.log(`
386
+ nuxt-agent-md - Generate AGENTS.md with Nuxt documentation for AI coding agents
387
+
388
+ Usage:
389
+ nuxt-agent-md [options]
390
+
391
+ Options:
392
+ -d, --docs-dir <path> Directory to store docs (default: .nuxt-docs)
393
+ -o, --output <path> Output AGENTS.md path (default: AGENTS.md)
394
+ -v, --nuxt-version <ver> Nuxt docs version (auto-detected from package.json)
395
+ --minify Generate minified index (default: true)
396
+ --no-minify Generate full index instead of minified
397
+ --dry-run Show what would be done without making changes
398
+ -h, --help Show this help message
399
+
400
+ Examples:
401
+ nuxt-agent-md # Auto-detect Nuxt version
402
+ nuxt-agent-md -v 4.0.0 # Use specific version
403
+ nuxt-agent-md --no-minify # Generate full index
404
+ nuxt-agent-md -d .docs -o CLAUDE.md # Custom paths
405
+ `);
406
+ process.exit(0);
407
+ }
408
+ try {
409
+ await generateAgentsMd({
410
+ docsDir: values["docs-dir"],
411
+ outputPath: values.output,
412
+ nuxtVersion: values["nuxt-version"],
413
+ minify: values["no-minify"] ? false : values.minify,
414
+ dryRun: values["dry-run"]
415
+ });
416
+ } catch (error) {
417
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
418
+ process.exit(1);
419
+ }
@@ -0,0 +1,2 @@
1
+ export declare function detectNuxtVersion(cwd?: string): Promise<string>;
2
+ export declare function mapToDocsVersion(nuxtVersion: string): string;
@@ -0,0 +1 @@
1
+ export declare function downloadDocs(version: string, targetDir: string): Promise<void>;
@@ -0,0 +1,2 @@
1
+ import type { IndexResult } from './types';
2
+ export declare function generateIndex(docsDir: string): Promise<IndexResult>;
@@ -0,0 +1,7 @@
1
+ import type { Options } from './types';
2
+ export { detectNuxtVersion, mapToDocsVersion } from './detect';
3
+ export { downloadDocs } from './download';
4
+ export { generateIndex } from './generate';
5
+ export { injectAgentsMd } from './inject';
6
+ export type * from './types';
7
+ export declare function generateAgentsMd(options?: Partial<Options>): Promise<void>;
@@ -0,0 +1,2 @@
1
+ import type { IndexResult } from './types';
2
+ export declare function injectAgentsMd(outputPath: string, index: IndexResult, nuxtVersion: string, docsDir: string, minify: boolean): Promise<void>;
@@ -0,0 +1,18 @@
1
+ export interface Options {
2
+ docsDir: string;
3
+ outputPath: string;
4
+ nuxtVersion?: string;
5
+ minify: boolean;
6
+ dryRun: boolean;
7
+ }
8
+ export interface DocEntry {
9
+ path: string;
10
+ title: string;
11
+ keywords: string[];
12
+ category: string;
13
+ }
14
+ export interface IndexResult {
15
+ entries: DocEntry[];
16
+ minified: string;
17
+ full: string;
18
+ }
package/package.json CHANGED
@@ -1,7 +1,41 @@
1
1
  {
2
2
  "name": "nuxt-agent-md",
3
- "version": "0.0.1-alpha.1",
4
- "description": "Coming soon - stay tuned!",
5
- "main": "index.js",
3
+ "version": "0.2.0",
4
+ "description": "Generate AGENTS.md with Nuxt documentation for AI coding agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "nuxt-agent-md": "./dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "bun build ./src/cli.ts --outdir ./dist --target node && bun run build:types",
21
+ "build:types": "tsc --emitDeclarationOnly",
22
+ "dev": "bun run ./src/cli.ts",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "bun run build"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "^1.2.0",
28
+ "@types/node": "^22.0.0",
29
+ "typescript": "^5.7.3"
30
+ },
31
+ "keywords": [
32
+ "nuxt",
33
+ "agents",
34
+ "ai",
35
+ "documentation",
36
+ "cursor",
37
+ "copilot",
38
+ "claude"
39
+ ],
6
40
  "license": "MIT"
7
- }
41
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- // placeholder