agent-md-generator 0.1.0 → 0.1.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.
Files changed (4) hide show
  1. package/README.md +84 -119
  2. package/dist/cli.js +1541 -0
  3. package/package.json +15 -40
  4. package/dist/cli.mjs +0 -456
package/dist/cli.js ADDED
@@ -0,0 +1,1541 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as p5 from "@clack/prompts";
5
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
6
+ import { resolve as resolve2 } from "path";
7
+
8
+ // ../shared/src/lib/questions.ts
9
+ var QUESTIONS = [
10
+ {
11
+ id: "project_name",
12
+ step: 1,
13
+ category: "Identity",
14
+ question: "What is the name of your project?",
15
+ hint: "This will be the main title of your AGENT.md",
16
+ type: "text",
17
+ placeholder: "e.g. MyApp, Preflight, DeutschFlow...",
18
+ required: true
19
+ },
20
+ {
21
+ id: "project_type",
22
+ step: 1,
23
+ category: "Identity",
24
+ question: "What type of project is this?",
25
+ hint: "This defines the overall architecture and conventions",
26
+ type: "select",
27
+ required: true,
28
+ options: [
29
+ { value: "web_app", label: "Web Application", description: "Next.js, React, Vue, SvelteKit..." },
30
+ { value: "mobile_app", label: "Mobile Application", description: "React Native, Expo, Flutter..." },
31
+ { value: "vscode_extension", label: "VS Code Extension", description: "TypeScript, VS Code API..." },
32
+ { value: "npm_package", label: "npm Package / Library", description: "TypeScript library, SDK, CLI..." },
33
+ { value: "api_backend", label: "API / Backend", description: "Express, Fastify, NestJS, Hono..." },
34
+ { value: "fullstack", label: "Full-Stack Application", description: "Frontend + Backend in one repo" },
35
+ { value: "cli_tool", label: "CLI Tool", description: "Command-line application" },
36
+ { value: "other", label: "Other", description: "Something else entirely" }
37
+ ]
38
+ },
39
+ {
40
+ id: "mobile_platforms",
41
+ step: 1,
42
+ category: "Identity",
43
+ question: "What mobile platforms will you target?",
44
+ hint: "Affects build config, testing setup, and platform-specific code conventions",
45
+ type: "select",
46
+ required: false,
47
+ dependsOn: { questionId: "project_type", value: "mobile_app" },
48
+ options: [
49
+ { value: "ios_only", label: "iOS only", description: "Target Apple devices only" },
50
+ { value: "android_only", label: "Android only", description: "Target Android devices only" },
51
+ { value: "both", label: "iOS + Android", description: "Cross-platform for both" }
52
+ ]
53
+ },
54
+ {
55
+ id: "npm_distribution",
56
+ step: 1,
57
+ category: "Identity",
58
+ question: "How will your package be distributed?",
59
+ hint: "Affects build output, bundler config, and import conventions",
60
+ type: "select",
61
+ required: false,
62
+ dependsOn: { questionId: "project_type", value: "npm_package" },
63
+ options: [
64
+ { value: "esm_only", label: "ESM only", description: "Modern ESM-only package" },
65
+ { value: "cjs_esm", label: "CommonJS + ESM", description: "Dual-format for broad compatibility" },
66
+ { value: "browser_node", label: "Browser + Node.js", description: "Universal package for both environments" }
67
+ ]
68
+ },
69
+ {
70
+ id: "project_description",
71
+ step: 1,
72
+ category: "Identity",
73
+ question: "Describe your project in 2-3 sentences.",
74
+ hint: "What does it do? Who is it for? What problem does it solve?",
75
+ coaching: "Impacts the ## Overview section. A rich description gives the AI full context to write more accurate conventions. Aim for 80+ characters.",
76
+ type: "textarea",
77
+ placeholder: "e.g. Preflight is a VS Code extension that helps developers ship cleaner code by detecting debug statements, tracking TODOs with deadlines, and generating AI-powered commit messages...",
78
+ required: true
79
+ },
80
+ {
81
+ id: "tech_stack",
82
+ step: 2,
83
+ category: "Tech Stack",
84
+ question: "What is your primary tech stack?",
85
+ hint: "Select all that apply to your project",
86
+ coaching: "Impacts ## Tech Stack and ## Conventions. Each technology adds framework-specific rules (e.g. Next.js \u2192 Server Components, Prisma \u2192 ORM patterns). Select all relevant tools.",
87
+ type: "multiselect",
88
+ required: true,
89
+ options: [
90
+ { value: "typescript", label: "TypeScript" },
91
+ { value: "javascript", label: "JavaScript" },
92
+ { value: "react", label: "React" },
93
+ { value: "nextjs", label: "Next.js" },
94
+ { value: "react_native", label: "React Native" },
95
+ { value: "expo", label: "Expo" },
96
+ { value: "tailwind", label: "Tailwind CSS" },
97
+ { value: "nodejs", label: "Node.js" },
98
+ { value: "python", label: "Python" },
99
+ { value: "go", label: "Go" },
100
+ { value: "rust", label: "Rust" },
101
+ { value: "prisma", label: "Prisma" },
102
+ { value: "supabase", label: "Supabase" },
103
+ { value: "vercel_ai", label: "Vercel AI SDK" },
104
+ { value: "zustand", label: "Zustand" },
105
+ { value: "zod", label: "Zod" }
106
+ ]
107
+ },
108
+ {
109
+ id: "ai_features",
110
+ step: 2,
111
+ category: "Tech Stack",
112
+ question: "Does your project use AI / LLM features?",
113
+ type: "select",
114
+ required: true,
115
+ options: [
116
+ { value: "yes_core", label: "Yes \u2014 AI is a core feature", description: "The app is built around AI capabilities" },
117
+ { value: "yes_secondary", label: "Yes \u2014 AI is a secondary feature", description: "AI enhances some features but is not the core" },
118
+ { value: "no", label: "No AI features", description: "Pure logic, no LLM integration" }
119
+ ]
120
+ },
121
+ {
122
+ id: "ai_providers",
123
+ step: 2,
124
+ category: "Tech Stack",
125
+ question: "Which AI providers will you use?",
126
+ hint: "Select all that apply",
127
+ type: "multiselect",
128
+ required: false,
129
+ dependsOn: { questionId: "ai_features", value: ["yes_core", "yes_secondary"] },
130
+ options: [
131
+ { value: "openai", label: "OpenAI" },
132
+ { value: "anthropic", label: "Anthropic (Claude)" },
133
+ { value: "deepseek", label: "DeepSeek" },
134
+ { value: "google", label: "Google Gemini" },
135
+ { value: "ollama", label: "Ollama (local)" },
136
+ { value: "model_agnostic", label: "Model-agnostic (any provider)" }
137
+ ]
138
+ },
139
+ {
140
+ id: "python_framework",
141
+ step: 2,
142
+ category: "Tech Stack",
143
+ question: "Which Python framework are you using?",
144
+ hint: "Determines routing, ORM, and project structure conventions",
145
+ type: "select",
146
+ required: false,
147
+ dependsOn: { questionId: "tech_stack", value: "python" },
148
+ options: [
149
+ { value: "django", label: "Django", description: "Full-stack web framework with ORM" },
150
+ { value: "fastapi", label: "FastAPI", description: "Modern async API framework" },
151
+ { value: "flask", label: "Flask", description: "Lightweight WSGI microframework" },
152
+ { value: "none", label: "No framework", description: "Pure Python / scripts" }
153
+ ]
154
+ },
155
+ {
156
+ id: "folder_structure",
157
+ step: 3,
158
+ category: "Architecture",
159
+ question: "Describe your folder structure.",
160
+ hint: "List the main folders and what they contain. The AI will use this to enforce conventions.",
161
+ coaching: "Impacts the ## Folder Structure section directly. A detailed tree prevents the AI from creating files in wrong places. The more specific, the better.",
162
+ type: "textarea",
163
+ placeholder: `src/
164
+ app/ <- Next.js App Router pages
165
+ components/ <- reusable UI components
166
+ lib/ <- utilities and helpers
167
+ types/ <- TypeScript types
168
+ hooks/ <- custom React hooks`,
169
+ required: true
170
+ },
171
+ {
172
+ id: "coding_conventions",
173
+ step: 3,
174
+ category: "Architecture",
175
+ question: "What are your most important coding conventions?",
176
+ hint: "Rules the AI must always follow when writing code for this project",
177
+ coaching: "These become enforced rules in the ## Coding Conventions section. More selections = more specific constraints = fewer AI mistakes. Pick at least 3.",
178
+ type: "multiselect",
179
+ required: true,
180
+ options: [
181
+ { value: "no_any", label: "No TypeScript any", description: "Strict typing, use unknown instead" },
182
+ { value: "no_comments", label: "No code comments", description: "Code must be self-documenting" },
183
+ { value: "functional", label: "Functional components only", description: "No class components" },
184
+ { value: "server_components", label: "Prefer Server Components", description: "Use client components only when needed" },
185
+ { value: "named_exports", label: "Named exports only", description: "No default exports" },
186
+ { value: "small_files", label: "Small focused files", description: "One responsibility per file" },
187
+ { value: "no_overengineering", label: "No overengineering", description: "Build the simplest thing that works" },
188
+ { value: "modular", label: "Modular architecture", description: "Clear separation of concerns" },
189
+ { value: "error_handling", label: "Explicit error handling", description: "Always handle errors gracefully" },
190
+ { value: "env_secrets", label: "No secrets in frontend", description: "API keys only in server/env" }
191
+ ]
192
+ },
193
+ {
194
+ id: "ui_style",
195
+ step: 3,
196
+ category: "Architecture",
197
+ question: "What is the UI style and tone?",
198
+ hint: "How should the interface feel?",
199
+ type: "multiselect",
200
+ required: false,
201
+ dependsOn: { questionId: "project_type", value: ["web_app", "mobile_app", "fullstack"] },
202
+ options: [
203
+ { value: "modern", label: "Modern & minimal" },
204
+ { value: "playful", label: "Playful & friendly" },
205
+ { value: "premium", label: "Premium & polished" },
206
+ { value: "dark", label: "Dark theme first" },
207
+ { value: "accessible", label: "Accessibility first" },
208
+ { value: "mobile_first", label: "Mobile first" },
209
+ { value: "pixel_perfect", label: "Pixel-perfect from designs" }
210
+ ]
211
+ },
212
+ {
213
+ id: "constraints",
214
+ step: 4,
215
+ category: "Constraints",
216
+ question: "What are the hard constraints for this project?",
217
+ hint: "Things the AI must NEVER do in this project",
218
+ coaching: "These become a strict ## Hard Constraints table \u2014 the most critical section. Each selected constraint is a guardrail the AI will respect on every response.",
219
+ type: "multiselect",
220
+ required: true,
221
+ options: [
222
+ { value: "no_db", label: "No database", description: "Use local state or files only" },
223
+ { value: "no_auth", label: "No authentication", description: "No login or user accounts" },
224
+ { value: "no_new_deps", label: "No new dependencies without approval", description: "Ask before adding packages" },
225
+ { value: "no_telemetry", label: "No telemetry or tracking", description: "No analytics or user tracking" },
226
+ { value: "no_breaking_changes", label: "No breaking API changes", description: "Maintain backward compatibility" },
227
+ { value: "offline_first", label: "Offline first", description: "Must work without internet" },
228
+ { value: "lightweight", label: "Keep it lightweight", description: "Minimize bundle size" },
229
+ { value: "no_class_components", label: "No class components" }
230
+ ]
231
+ },
232
+ {
233
+ id: "dev_philosophy",
234
+ step: 4,
235
+ category: "Constraints",
236
+ question: "What is your development philosophy?",
237
+ hint: "How should the AI approach building features?",
238
+ coaching: "Shapes the ## Development Philosophy section. This single choice influences how the AI balances speed vs. quality on every decision it makes.",
239
+ type: "select",
240
+ required: true,
241
+ options: [
242
+ { value: "mvp_first", label: "MVP first, iterate later", description: "Build the smallest useful version, then improve" },
243
+ { value: "production_grade", label: "Production-grade from day one", description: "Build it right the first time" },
244
+ { value: "teachable", label: "Teachable & approachable", description: "Code should be easy to understand and explain" },
245
+ { value: "performance", label: "Performance first", description: "Optimize for speed and efficiency" }
246
+ ]
247
+ },
248
+ {
249
+ id: "extra_context",
250
+ step: 4,
251
+ category: "Constraints",
252
+ question: "Any additional context or special instructions?",
253
+ hint: "Anything else the AI should know \u2014 edge cases, specific patterns, important decisions already made...",
254
+ type: "textarea",
255
+ placeholder: "e.g. This project uses the Vercel AI SDK for all LLM calls. The API keys are stored in .env.local and never exposed to the client. The app is deployed on Vercel...",
256
+ required: false
257
+ },
258
+ {
259
+ id: "initialize_project",
260
+ step: 4,
261
+ category: "Constraints",
262
+ question: "Initialize the project with a Technical Roadmap & Prompt Blueprints?",
263
+ hint: "If enabled, we will generate highly detailed ROADMAP.md and session-optimized PROMPTS.md files in addition to agent profiles.",
264
+ type: "select",
265
+ required: true,
266
+ options: [
267
+ { value: "yes", label: "Yes \u2014 Generate Roadmap & Prompts", description: "Create a comprehensive project execution plan" },
268
+ { value: "no", label: "No \u2014 Only generate AGENT.md / configs", description: "Skip roadmap and prompts creation" }
269
+ ]
270
+ }
271
+ ];
272
+ var STEP_LABELS = {
273
+ 1: "Identity",
274
+ 2: "Tech Stack",
275
+ 3: "Architecture",
276
+ 4: "Constraints"
277
+ };
278
+ function isQuestionVisible(question, answers) {
279
+ if (!question.dependsOn) return true;
280
+ const { questionId, value } = question.dependsOn;
281
+ const answer = answers[questionId];
282
+ if (!answer) return false;
283
+ if (Array.isArray(value)) {
284
+ if (Array.isArray(answer)) return value.some((v) => answer.includes(v));
285
+ return value.includes(answer);
286
+ }
287
+ if (Array.isArray(answer)) return answer.includes(value);
288
+ return answer === value;
289
+ }
290
+
291
+ // ../shared/src/lib/buildPrompt.ts
292
+ function getLabel(questionId, value) {
293
+ const question = QUESTIONS.find((q) => q.id === questionId);
294
+ if (!question?.options) return Array.isArray(value) ? value.join(", ") : value;
295
+ if (Array.isArray(value)) {
296
+ return value.map((v) => question.options?.find((o) => o.value === v)?.label ?? v).join(", ");
297
+ }
298
+ return question.options.find((o) => o.value === value)?.label ?? value;
299
+ }
300
+ function buildPrompt(answers) {
301
+ const projectName = answers["project_name"] ?? "My Project";
302
+ const projectType = getLabel("project_type", answers["project_type"] ?? "");
303
+ const description = answers["project_description"] ?? "";
304
+ const mobilePlatforms = answers["mobile_platforms"] ? getLabel("mobile_platforms", answers["mobile_platforms"]) : null;
305
+ const npmDistribution = answers["npm_distribution"] ? getLabel("npm_distribution", answers["npm_distribution"]) : null;
306
+ const techStack = getLabel("tech_stack", answers["tech_stack"] ?? []);
307
+ const pythonFramework = answers["python_framework"] ? getLabel("python_framework", answers["python_framework"]) : null;
308
+ const aiFeatures = getLabel("ai_features", answers["ai_features"] ?? "no");
309
+ const aiProviders = answers["ai_providers"] ? getLabel("ai_providers", answers["ai_providers"]) : "None";
310
+ const folderStructure = answers["folder_structure"] ?? "";
311
+ const conventions = getLabel("coding_conventions", answers["coding_conventions"] ?? []);
312
+ const uiStyle = answers["ui_style"] ? getLabel("ui_style", answers["ui_style"]) : "Not specified";
313
+ const constraints = getLabel("constraints", answers["constraints"] ?? []);
314
+ const devPhilosophy = getLabel("dev_philosophy", answers["dev_philosophy"] ?? "");
315
+ const extraContext = answers["extra_context"] ?? "";
316
+ const initProject = answers["initialize_project"] === "yes";
317
+ let systemPrompt = `You are an expert software architect. Generate a complete, detailed, and production-quality AGENT.md file for the following project.
318
+
319
+ The AGENT.md is a comprehensive engineering README that will be used by AI coding assistants (like Claude, GPT-4, Kiro, Cursor) to understand the project deeply and write code that perfectly matches the project's conventions, architecture, and constraints.
320
+
321
+ The output must be a single Markdown document. It must be thorough, specific, and actionable \u2014 not generic. Every section should contain real, project-specific content.
322
+
323
+ Use the DeutschFlow AGENT.md and Preflight AGENT.md as structural references for quality and depth. Match that level of detail.
324
+
325
+ ---
326
+
327
+ PROJECT INFORMATION:
328
+
329
+ Project Name: ${projectName}
330
+ Project Type: ${projectType}${mobilePlatforms ? `
331
+ Mobile Platforms: ${mobilePlatforms}` : ""}${npmDistribution ? `
332
+ Package Distribution: ${npmDistribution}` : ""}
333
+ Description: ${description}
334
+
335
+ Tech Stack: ${techStack}${pythonFramework ? `
336
+ Python Framework: ${pythonFramework}` : ""}
337
+ AI Features: ${aiFeatures}
338
+ AI Providers: ${aiProviders}
339
+
340
+ Folder Structure:
341
+ ${folderStructure}
342
+
343
+ Coding Conventions: ${conventions}
344
+ UI Style: ${uiStyle}
345
+ Hard Constraints: ${constraints}
346
+ Development Philosophy: ${devPhilosophy}
347
+ ${extraContext ? `
348
+ Additional Context:
349
+ ${extraContext}` : ""}
350
+
351
+ ---
352
+
353
+ Generate the AGENT.md with these sections (adapt section names to fit the project):
354
+
355
+ 1. A header introducing the AI assistant's role for this specific project
356
+ 2. Project Identity (name, vision, what makes it unique)
357
+ 3. Core Product Principles (what it does and does NOT do)
358
+ 4. Important Constraints (VERY IMPORTANT section \u2014 hard rules)
359
+ 5. Tech Stack (with specific versions/choices and why)
360
+ 6. Folder Structure (with role of each folder/file)
361
+ 7. Architecture Guidelines (patterns, naming conventions, component rules)
362
+ 8. Coding Conventions (with \u2705 CORRECT and \u274C INCORRECT examples where useful)
363
+ 9. TypeScript Rules (if applicable)
364
+ 10. Feature Implementation Rules (step-by-step process)
365
+ 11. Development Philosophy
366
+ 12. Decision Making & Clarifications
367
+ 13. Communication Style
368
+ 14. Final Reminder
369
+
370
+ Rules for the output:
371
+ - Be specific to THIS project, not generic
372
+ - Use real code examples that match the tech stack
373
+ - Include \u2705 CORRECT / \u274C INCORRECT examples for important rules
374
+ - Use tables where they add clarity
375
+ - Keep the tone direct and professional
376
+ - No filler content \u2014 every sentence must be actionable
377
+ - The document should be long enough to be genuinely useful (aim for 600-1000 lines)`;
378
+ if (initProject) {
379
+ systemPrompt += `
380
+
381
+ ================================================================================
382
+ CRITICAL WORKSPACE INITIALIZATION FILES REQUESTED
383
+ ================================================================================
384
+ Because the user selected "Yes" to project initialization, in addition to the AGENT.md file, you MUST generate two other files: ROADMAP.md and PROMPTS.md.
385
+ You MUST output ALL THREE FILES inside a single response, separated EXACTLY by these custom line-delimiters (do not wrap the delimiters in code blocks, write them as plain text on their own lines):
386
+
387
+ ===AGENT_MD===
388
+ [Place the complete AGENT.md text here]
389
+ ===ROADMAP_MD===
390
+ [Place the complete, highly detailed ROADMAP.md text here]
391
+ ===PROMPTS_MD===
392
+ [Place the complete, advanced PROMPTS.md text here]
393
+
394
+ --------------------------------------------------------------------------------
395
+ SPECIFIC FILE REQUIREMENTS:
396
+ --------------------------------------------------------------------------------
397
+
398
+ 1. ROADMAP.md:
399
+ - Must be a highly detailed, highly technical project implementation roadmap.
400
+ - Break down the target product features into clear, logical milestones and phases.
401
+ - For each feature/task, list specific architectural guidelines, implementation checklists, and structural changes.
402
+ - The roadmap must be extremely robust and powerful, specifically designed for an AI Coding Agent to read, parse, and execute cleanly step-by-step.
403
+
404
+ 2. PROMPTS.md:
405
+ - Provide a set of advanced, production-ready engineering prompt blueprints for each and every feature or step defined in the ROADMAP.md.
406
+ - Every single feature prompt MUST start with a phrase like:
407
+ "Read the AGENT.md/AGENTS.md file in the root directory and follow its rules strictly."
408
+ - Provide highly precise, well-structured prompt blueprints that guide the AI to implement the feature exactly according to the conventions.
409
+ - CRITICAL: For each prompt, include a section called "Session-Saving Tip" that advises the user to start a new chat/agent session for that feature, pointing out that because AGENT.md contains all the context and constraints of the codebase, clean independent chat sessions will save massive amounts of tokens, prevent prompt/context dilution, and guarantee high-quality execution.
410
+
411
+ --------------------------------------------------------------------------------
412
+ `;
413
+ }
414
+ systemPrompt += `
415
+ - Output ONLY the Markdown content, no preamble, no explanation`;
416
+ return systemPrompt;
417
+ }
418
+
419
+ // ../shared/src/lib/buildUpdatePrompt.ts
420
+ function buildUpdatePrompt(existingContent, changeDescription) {
421
+ return `You are an expert software architect. You are updating an existing AGENT.md file based on specific change instructions.
422
+
423
+ CHANGE INSTRUCTIONS FROM THE USER:
424
+ ${changeDescription}
425
+
426
+ ---
427
+
428
+ EXISTING AGENT.md (TO BE UPDATED):
429
+ ${existingContent}
430
+
431
+ ---
432
+
433
+ CRITICAL REQUIREMENTS FOR THE UPDATE:
434
+ 1. STRICT PRESERVATION: Do NOT delete, summarize, truncate, or omit any section, paragraph, rule, list item, or code snippet from the original document unless the change instructions explicitly demand its removal. Everything else must remain 100% intact.
435
+ 2. TARGETED MODIFICATION: Only modify the specific parts of the document that are directly relevant to the user's change instructions.
436
+ 3. CONTEXT INTEGRATION: If adding a new section, insert it at the most logical position within the document structure (e.g. adding a "Testing" section right after "Coding Conventions" or "Tech Stack").
437
+ 4. MAINTAIN STYLE: Keep the exact same tone, document layout, formatting, and markdown styles. If the original uses a specific visual style like checklist boxes [ ] or correct/incorrect emoji blocks (\u2705 CORRECT / \u274C INCORRECT), you MUST use that exact same style for the new content.
438
+ 5. HEADING HIERARCHY: Maintain the same markdown heading nesting (e.g., #, ##, ###).
439
+ 6. COMPLETE OUTPUT: You must output the ENTIRE updated AGENT.md file from start to finish. Do NOT output a diff, do NOT output only the modified parts, and do NOT truncate the document with placeholders like "... [rest of document] ...".
440
+ 7. CLEAN MARKDOWN: Output ONLY valid Markdown. Do NOT wrap the markdown in a code block, do NOT write any intro/outro explanations, and do NOT include any conversational responses. Start immediately with the updated content.`;
441
+ }
442
+
443
+ // ../shared/src/lib/formatOutput.ts
444
+ var OUTPUT_FORMATS = [
445
+ { label: "AGENT.md", filename: "AGENT.md", tool: "Kiro" },
446
+ { label: "CLAUDE.md", filename: "CLAUDE.md", tool: "Claude Code" },
447
+ { label: ".cursorrules", filename: ".cursorrules", tool: "Cursor" },
448
+ { label: ".windsurfrules", filename: ".windsurfrules", tool: "Windsurf" },
449
+ { label: "copilot-instructions.md", filename: "copilot-instructions.md", tool: "GitHub Copilot" }
450
+ ];
451
+
452
+ // ../shared/src/lib/parseManifest.ts
453
+ var PACKAGE_TO_STACK = {
454
+ next: "nextjs",
455
+ react: "react",
456
+ "react-dom": "react",
457
+ "react-native": "react_native",
458
+ expo: "expo",
459
+ tailwindcss: "tailwind",
460
+ "@prisma/client": "prisma",
461
+ prisma: "prisma",
462
+ "@supabase/supabase-js": "supabase",
463
+ zustand: "zustand",
464
+ zod: "zod",
465
+ ai: "vercel_ai",
466
+ "@ai-sdk/openai": "vercel_ai",
467
+ "@ai-sdk/anthropic": "vercel_ai",
468
+ "@ai-sdk/deepseek": "vercel_ai",
469
+ "@ai-sdk/google": "vercel_ai",
470
+ "@ai-sdk/xai": "vercel_ai",
471
+ typescript: "typescript"
472
+ };
473
+ var REQUIREMENTS_TO_STACK = {
474
+ django: "python",
475
+ flask: "python",
476
+ fastapi: "python",
477
+ uvicorn: "python",
478
+ sqlalchemy: "python",
479
+ pytest: "python",
480
+ pydantic: "python",
481
+ numpy: "python",
482
+ pandas: "python"
483
+ };
484
+ function parsePackageJson(text4) {
485
+ try {
486
+ const pkg = JSON.parse(text4);
487
+ const deps = pkg.dependencies && typeof pkg.dependencies === "object" ? pkg.dependencies : {};
488
+ const devDeps = pkg.devDependencies && typeof pkg.devDependencies === "object" ? pkg.devDependencies : {};
489
+ const allDeps = { ...deps, ...devDeps };
490
+ const found = /* @__PURE__ */ new Set(["nodejs"]);
491
+ for (const dep of Object.keys(allDeps)) {
492
+ const mapped = PACKAGE_TO_STACK[dep];
493
+ if (mapped) found.add(mapped);
494
+ }
495
+ return Array.from(found);
496
+ } catch {
497
+ return [];
498
+ }
499
+ }
500
+ function parseRequirementsTxt(text4) {
501
+ const found = /* @__PURE__ */ new Set();
502
+ for (const line of text4.split("\n")) {
503
+ const pkg = line.split(/[>=<!]/)[0].trim().toLowerCase();
504
+ if (!pkg || pkg.startsWith("#")) continue;
505
+ const mapped = REQUIREMENTS_TO_STACK[pkg];
506
+ if (mapped) found.add(mapped);
507
+ }
508
+ if (found.size > 0) found.add("python");
509
+ return Array.from(found);
510
+ }
511
+ function parseManifest(filename, text4) {
512
+ if (filename === "package.json" || filename.endsWith("/package.json")) return parsePackageJson(text4);
513
+ if (filename === "requirements.txt" || filename.endsWith("/requirements.txt")) return parseRequirementsTxt(text4);
514
+ return [];
515
+ }
516
+ function parsePackageJsonMeta(text4) {
517
+ try {
518
+ const pkg = JSON.parse(text4);
519
+ return { name: pkg.name, description: pkg.description };
520
+ } catch {
521
+ return {};
522
+ }
523
+ }
524
+
525
+ // ../shared/src/lib/scoreOutput.ts
526
+ var CANONICAL_SECTIONS = [
527
+ "Tech Stack",
528
+ "Architecture",
529
+ "Constraints",
530
+ "Coding Conventions",
531
+ "Development Philosophy",
532
+ "Folder Structure",
533
+ "TypeScript",
534
+ "Feature Implementation",
535
+ "Decision Making",
536
+ "Communication Style",
537
+ "Testing",
538
+ "Security"
539
+ ];
540
+ function scoreOutput(content) {
541
+ const sections = CANONICAL_SECTIONS.map((label) => ({
542
+ label,
543
+ present: new RegExp(`#{1,3}\\s+[^\\n]*${label}`, "i").test(content)
544
+ }));
545
+ return {
546
+ found: sections.filter((s) => s.present).length,
547
+ total: sections.length,
548
+ sections
549
+ };
550
+ }
551
+
552
+ // ../shared/src/lib/shareConfig.ts
553
+ function encodeConfig(answers) {
554
+ const json = JSON.stringify(answers);
555
+ return Buffer.from(json).toString("base64url");
556
+ }
557
+ function buildShareUrl(answers, baseUrl = "https://agent-md-generator.edwinfom.dev") {
558
+ return `${baseUrl}?config=${encodeConfig(answers)}`;
559
+ }
560
+
561
+ // ../shared/src/lib/templates.ts
562
+ var TEMPLATES = [
563
+ {
564
+ id: "nextjs_saas",
565
+ name: "Next.js SaaS",
566
+ description: "Full-stack web app with authentication, database, and modern UI",
567
+ stack: "TypeScript \xB7 React \xB7 Next.js \xB7 Tailwind \xB7 Prisma \xB7 Supabase",
568
+ answers: {
569
+ project_type: "web_app",
570
+ tech_stack: ["typescript", "react", "nextjs", "tailwind", "prisma", "supabase"],
571
+ ai_features: "no",
572
+ coding_conventions: ["no_any", "functional", "server_components", "named_exports", "env_secrets"],
573
+ dev_philosophy: "production_grade",
574
+ constraints: ["no_telemetry"],
575
+ ui_style: ["modern", "accessible"]
576
+ }
577
+ },
578
+ {
579
+ id: "api_backend",
580
+ name: "API Backend Node.js",
581
+ description: "Lightweight REST or RPC backend with schema validation",
582
+ stack: "TypeScript \xB7 Node.js \xB7 Fastify / Hono \xB7 Zod",
583
+ answers: {
584
+ project_type: "api_backend",
585
+ tech_stack: ["typescript", "nodejs", "zod"],
586
+ ai_features: "no",
587
+ coding_conventions: ["no_any", "named_exports", "small_files", "error_handling", "env_secrets"],
588
+ dev_philosophy: "production_grade",
589
+ constraints: ["no_new_deps"]
590
+ }
591
+ },
592
+ {
593
+ id: "mobile_rn",
594
+ name: "Mobile React Native",
595
+ description: "Cross-platform mobile app for iOS and Android",
596
+ stack: "TypeScript \xB7 React Native \xB7 Expo",
597
+ answers: {
598
+ project_type: "mobile_app",
599
+ tech_stack: ["typescript", "react", "react_native", "expo"],
600
+ ai_features: "no",
601
+ coding_conventions: ["no_any", "functional", "named_exports"],
602
+ dev_philosophy: "mvp_first",
603
+ constraints: ["lightweight"],
604
+ ui_style: ["mobile_first", "accessible"]
605
+ }
606
+ },
607
+ {
608
+ id: "fullstack_monorepo",
609
+ name: "Full-Stack Monorepo",
610
+ description: "Frontend and backend in one repo, sharing types and tooling",
611
+ stack: "TypeScript \xB7 React \xB7 Next.js \xB7 Node.js \xB7 Prisma",
612
+ answers: {
613
+ project_type: "fullstack",
614
+ tech_stack: ["typescript", "react", "nextjs", "nodejs", "prisma"],
615
+ ai_features: "no",
616
+ coding_conventions: ["no_any", "functional", "named_exports", "modular", "env_secrets"],
617
+ dev_philosophy: "production_grade",
618
+ constraints: ["no_new_deps", "no_telemetry"],
619
+ ui_style: ["modern"]
620
+ }
621
+ },
622
+ {
623
+ id: "cli_tool",
624
+ name: "CLI Tool",
625
+ description: "Command-line application distributed via npm",
626
+ stack: "TypeScript \xB7 Node.js",
627
+ answers: {
628
+ project_type: "cli_tool",
629
+ tech_stack: ["typescript", "nodejs", "zod"],
630
+ ai_features: "no",
631
+ coding_conventions: ["no_any", "named_exports", "small_files", "error_handling"],
632
+ dev_philosophy: "mvp_first",
633
+ constraints: ["lightweight", "offline_first"]
634
+ }
635
+ }
636
+ ];
637
+
638
+ // ../shared/src/lib/validateOutput.ts
639
+ var SECTION_GROUPS = [
640
+ {
641
+ label: "Tech Stack",
642
+ patterns: ["tech stack", "technology", "dependencies", "stack"]
643
+ },
644
+ {
645
+ label: "Architecture / Structure",
646
+ patterns: ["architecture", "folder structure", "project structure", "structure", "layout"]
647
+ },
648
+ {
649
+ label: "Constraints",
650
+ patterns: ["constraint", "hard rule", "never do", "restriction", "must not", "do not"]
651
+ },
652
+ {
653
+ label: "Coding Conventions",
654
+ patterns: ["coding convention", "code style", "convention", "style guide"]
655
+ },
656
+ {
657
+ label: "Commands / Workflow",
658
+ patterns: ["command", "script", "workflow", "build", "run ", "development setup", "getting started"]
659
+ },
660
+ {
661
+ label: "Philosophy",
662
+ patterns: ["philosophy", "principle", "approach", "mindset", "development philosophy"]
663
+ },
664
+ {
665
+ label: "Identity / Overview",
666
+ patterns: ["identity", "overview", "project identity", "vision", "what it does", "introduction"]
667
+ }
668
+ ];
669
+ var MIN_LENGTH = 2e3;
670
+ var MIN_SECTIONS = 5;
671
+ function isSectionPresent(content, patterns) {
672
+ return patterns.some(
673
+ (pattern) => new RegExp(`#{1,3}\\s+[^\\n]*${pattern}`, "i").test(content)
674
+ );
675
+ }
676
+ function validateOutput(content) {
677
+ const warnings = [];
678
+ if (content.length < MIN_LENGTH) {
679
+ warnings.push(
680
+ `Generated file is too short (${content.length} chars \u2014 expected \u2265 ${MIN_LENGTH}). The output may be incomplete.`
681
+ );
682
+ }
683
+ const results = SECTION_GROUPS.map((g) => ({
684
+ label: g.label,
685
+ present: isSectionPresent(content, g.patterns)
686
+ }));
687
+ const presentCount = results.filter((r) => r.present).length;
688
+ const missing = results.filter((r) => !r.present).map((r) => r.label);
689
+ if (presentCount < MIN_SECTIONS) {
690
+ warnings.push(
691
+ `Only ${presentCount}/${SECTION_GROUPS.length} expected sections found. Missing: ${missing.join(", ")}.`
692
+ );
693
+ }
694
+ return { warnings };
695
+ }
696
+
697
+ // src/detect.ts
698
+ import * as p from "@clack/prompts";
699
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
700
+ import { basename, extname, join } from "path";
701
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
702
+ "node_modules",
703
+ ".git",
704
+ ".next",
705
+ "dist",
706
+ "build",
707
+ "out",
708
+ ".turbo",
709
+ ".cache",
710
+ "__pycache__",
711
+ ".venv",
712
+ "venv",
713
+ ".env",
714
+ "env",
715
+ "target",
716
+ "vendor",
717
+ ".cargo",
718
+ "coverage",
719
+ ".nyc_output",
720
+ "storybook-static",
721
+ ".vercel",
722
+ ".swc",
723
+ "tmp",
724
+ ".tmp"
725
+ ]);
726
+ var IGNORE_EXTS = /* @__PURE__ */ new Set([
727
+ ".png",
728
+ ".jpg",
729
+ ".jpeg",
730
+ ".gif",
731
+ ".ico",
732
+ ".webp",
733
+ ".avif",
734
+ ".svg",
735
+ ".ttf",
736
+ ".woff",
737
+ ".woff2",
738
+ ".eot",
739
+ ".otf",
740
+ ".exe",
741
+ ".dll",
742
+ ".so",
743
+ ".dylib",
744
+ ".wasm",
745
+ ".zip",
746
+ ".tar",
747
+ ".gz",
748
+ ".rar",
749
+ ".7z",
750
+ ".bz2",
751
+ ".pdf",
752
+ ".mp4",
753
+ ".mp3",
754
+ ".mov",
755
+ ".avi",
756
+ ".wav",
757
+ ".tsbuildinfo",
758
+ ".DS_Store",
759
+ ".pyc",
760
+ ".db",
761
+ ".sqlite",
762
+ ".sqlite3",
763
+ ".bin",
764
+ ".dat",
765
+ ".lock"
766
+ ]);
767
+ var IGNORE_FILES = /* @__PURE__ */ new Set([
768
+ "package-lock.json",
769
+ "yarn.lock",
770
+ "pnpm-lock.yaml",
771
+ "poetry.lock",
772
+ "bun.lockb",
773
+ "Cargo.lock",
774
+ ".DS_Store",
775
+ "Thumbs.db",
776
+ ".gitkeep",
777
+ "next-env.d.ts",
778
+ ".env",
779
+ ".env.local",
780
+ ".env.production",
781
+ ".env.development"
782
+ ]);
783
+ var PROJECT_MARKERS = {
784
+ "package.json": "npm",
785
+ "pyproject.toml": "python",
786
+ "requirements.txt": "python",
787
+ "go.mod": "go",
788
+ "Cargo.toml": "rust"
789
+ };
790
+ var MAX_DEPTH = 6;
791
+ var MAX_FILES_PER_DIR = 40;
792
+ function buildTree(dirPath, prefix = "", depth = 0) {
793
+ if (depth > MAX_DEPTH) return "";
794
+ let entries;
795
+ try {
796
+ entries = readdirSync(dirPath);
797
+ } catch {
798
+ return "";
799
+ }
800
+ const dirs = [];
801
+ const files = [];
802
+ for (const entry of entries) {
803
+ if (entry.startsWith(".") && entry !== ".env.example" && entry !== ".gitignore") {
804
+ if (IGNORE_DIRS.has(entry)) continue;
805
+ }
806
+ if (IGNORE_DIRS.has(entry)) continue;
807
+ if (IGNORE_FILES.has(entry)) continue;
808
+ const ext = extname(entry).toLowerCase();
809
+ if (IGNORE_EXTS.has(ext)) continue;
810
+ const fullPath = join(dirPath, entry);
811
+ let stat;
812
+ try {
813
+ stat = statSync(fullPath);
814
+ } catch {
815
+ continue;
816
+ }
817
+ if (stat.isDirectory()) {
818
+ dirs.push(entry);
819
+ } else {
820
+ files.push(entry);
821
+ }
822
+ }
823
+ const lines = [];
824
+ const allEntries = [...dirs.sort(), ...files.sort()].slice(0, MAX_FILES_PER_DIR);
825
+ for (let i = 0; i < allEntries.length; i++) {
826
+ const entry = allEntries[i];
827
+ const isLast = i === allEntries.length - 1;
828
+ const isDir = dirs.includes(entry);
829
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
830
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
831
+ if (isDir) {
832
+ lines.push(`${prefix}${connector}${entry}/`);
833
+ const sub = buildTree(join(dirPath, entry), childPrefix, depth + 1);
834
+ if (sub) lines.push(sub);
835
+ } else {
836
+ lines.push(`${prefix}${connector}${entry}`);
837
+ }
838
+ }
839
+ return lines.join("\n");
840
+ }
841
+ function detectMarker(rootPath) {
842
+ for (const [marker, type] of Object.entries(PROJECT_MARKERS)) {
843
+ if (existsSync(join(rootPath, marker))) {
844
+ return { type, file: marker };
845
+ }
846
+ }
847
+ return { type: "unknown" };
848
+ }
849
+ function isEmptyish(rootPath) {
850
+ try {
851
+ const entries = readdirSync(rootPath).filter((e) => !e.startsWith("."));
852
+ return entries.length === 0;
853
+ } catch {
854
+ return false;
855
+ }
856
+ }
857
+ async function detectProject(startPath) {
858
+ let rootPath = startPath;
859
+ if (isEmptyish(rootPath)) {
860
+ p.log.warn("The current directory seems empty or does not contain a project.");
861
+ const entered = await p.text({
862
+ message: "Path to your project (absolute or relative):",
863
+ placeholder: "e.g. ../my-project or C:/Users/you/projects/my-app",
864
+ validate: (v) => {
865
+ if (!v.trim()) return "Project path is required.";
866
+ if (!existsSync(v.trim())) return `Directory not found: ${v.trim()}`;
867
+ }
868
+ });
869
+ if (p.isCancel(entered)) {
870
+ p.cancel("Cancelled.");
871
+ process.exit(0);
872
+ }
873
+ rootPath = entered.trim();
874
+ }
875
+ const { type: manifestType, file: manifestFile } = detectMarker(rootPath);
876
+ let techStack = [];
877
+ let projectName = basename(rootPath);
878
+ let description = "";
879
+ if (manifestFile) {
880
+ const manifestPath = join(rootPath, manifestFile);
881
+ try {
882
+ const content = readFileSync(manifestPath, "utf-8");
883
+ techStack = parseManifest(manifestFile, content);
884
+ if (manifestFile === "package.json") {
885
+ const meta = parsePackageJsonMeta(content);
886
+ if (meta.name) projectName = meta.name;
887
+ if (meta.description) description = meta.description;
888
+ } else if (manifestFile === "go.mod") {
889
+ const match = content.match(/^module\s+(\S+)/m);
890
+ if (match) projectName = match[1].split("/").pop() ?? projectName;
891
+ } else if (manifestFile === "Cargo.toml") {
892
+ const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
893
+ if (nameMatch) projectName = nameMatch[1];
894
+ }
895
+ } catch {
896
+ }
897
+ }
898
+ const rootLine = `${basename(rootPath)}/`;
899
+ const childTree = buildTree(rootPath);
900
+ const tree = childTree ? `${rootLine}
901
+ ${childTree}` : rootLine;
902
+ return { rootPath, tree, techStack, projectName, description, manifestType };
903
+ }
904
+
905
+ // src/models.ts
906
+ import * as p2 from "@clack/prompts";
907
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
908
+ import { resolve } from "path";
909
+ var MODELS = [
910
+ {
911
+ provider: "deepseek",
912
+ modelId: "deepseek-chat",
913
+ label: "DeepSeek Chat",
914
+ envKey: "DEEPSEEK_API_KEY",
915
+ hint: "Fast \xB7 Cheap \xB7 Recommended \u2014 platform.deepseek.com"
916
+ },
917
+ {
918
+ provider: "deepseek",
919
+ modelId: "deepseek-reasoner",
920
+ label: "DeepSeek Reasoner (R1)",
921
+ envKey: "DEEPSEEK_API_KEY",
922
+ hint: "Slower \xB7 Best for complex reasoning"
923
+ },
924
+ {
925
+ provider: "google",
926
+ modelId: "gemini-2.0-flash",
927
+ label: "Gemini 2.0 Flash",
928
+ envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
929
+ hint: "Fast \xB7 Multimodal \u2014 aistudio.google.com"
930
+ },
931
+ {
932
+ provider: "google",
933
+ modelId: "gemini-1.5-pro",
934
+ label: "Gemini 1.5 Pro",
935
+ envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
936
+ hint: "Long context (2M tokens) \xB7 High quality"
937
+ },
938
+ {
939
+ provider: "xai",
940
+ modelId: "grok-beta",
941
+ label: "Grok Beta",
942
+ envKey: "XAI_API_KEY",
943
+ hint: "xAI \xB7 Recent knowledge \u2014 console.x.ai"
944
+ },
945
+ {
946
+ provider: "xai",
947
+ modelId: "grok-3-mini",
948
+ label: "Grok 3 Mini",
949
+ envKey: "XAI_API_KEY",
950
+ hint: "xAI \xB7 Fast \xB7 Cost-effective"
951
+ },
952
+ {
953
+ provider: "ollama",
954
+ modelId: "llama3.2",
955
+ label: "Llama 3.2 (Local Ollama)",
956
+ envKey: null,
957
+ hint: "Local \xB7 Free \xB7 No API key required \u2014 ollama.com"
958
+ },
959
+ {
960
+ provider: "ollama",
961
+ modelId: "mistral",
962
+ label: "Mistral (Local Ollama)",
963
+ envKey: null,
964
+ hint: "Local \xB7 Free \xB7 Good for coding"
965
+ }
966
+ ];
967
+ function loadEnv(cwd) {
968
+ const envPath = resolve(cwd, ".env.local");
969
+ const env = {};
970
+ if (!existsSync2(envPath)) return env;
971
+ const lines = readFileSync2(envPath, "utf-8").split("\n");
972
+ for (const line of lines) {
973
+ const trimmed = line.trim();
974
+ if (!trimmed || trimmed.startsWith("#")) continue;
975
+ const eqIndex = trimmed.indexOf("=");
976
+ if (eqIndex === -1) continue;
977
+ const key = trimmed.slice(0, eqIndex).trim();
978
+ const val = trimmed.slice(eqIndex + 1).trim().replace(/^["']|["']$/g, "");
979
+ env[key] = val;
980
+ }
981
+ return env;
982
+ }
983
+ function saveKeyToEnvLocal(cwd, key, value) {
984
+ const envPath = resolve(cwd, ".env.local");
985
+ const existing = existsSync2(envPath) ? readFileSync2(envPath, "utf-8") : "";
986
+ const line = `${key}=${value}
987
+ `;
988
+ const updated = existing.endsWith("\n") || existing === "" ? existing + line : existing + "\n" + line;
989
+ writeFileSync(envPath, updated, "utf-8");
990
+ }
991
+ async function checkOllamaRunning() {
992
+ try {
993
+ const controller = new AbortController();
994
+ const timeout = setTimeout(() => controller.abort(), 2e3);
995
+ const res = await fetch("http://localhost:11434/api/tags", { signal: controller.signal });
996
+ clearTimeout(timeout);
997
+ return res.ok;
998
+ } catch {
999
+ return false;
1000
+ }
1001
+ }
1002
+ async function selectModel(cwd) {
1003
+ const env = { ...loadEnv(cwd), ...process.env };
1004
+ const options = MODELS.map((m) => {
1005
+ const hasKey = m.envKey === null || Boolean(env[m.envKey]);
1006
+ return {
1007
+ value: m.modelId,
1008
+ label: `${m.label}${hasKey ? " \u2713" : ""}`,
1009
+ hint: hasKey && m.envKey ? "API key found" : m.hint
1010
+ };
1011
+ });
1012
+ const chosen = await p2.select({
1013
+ message: "Which model do you want to use for generation?",
1014
+ options
1015
+ });
1016
+ if (p2.isCancel(chosen)) {
1017
+ p2.cancel("Cancelled.");
1018
+ process.exit(0);
1019
+ }
1020
+ const config = MODELS.find((m) => m.modelId === chosen);
1021
+ if (config.provider === "ollama") {
1022
+ const running = await checkOllamaRunning();
1023
+ if (!running) {
1024
+ p2.log.warn(
1025
+ "Ollama does not seem to be running on localhost:11434.\nStart it with: ollama serve\nThen in another terminal run: ollama pull " + config.modelId
1026
+ );
1027
+ const cont = await p2.confirm({ message: "Continue anyway?", initialValue: false });
1028
+ if (p2.isCancel(cont) || !cont) {
1029
+ p2.cancel("Cancelled.");
1030
+ process.exit(0);
1031
+ }
1032
+ } else {
1033
+ p2.log.success(`Ollama detected on localhost:11434`);
1034
+ }
1035
+ return { config, apiKey: null };
1036
+ }
1037
+ const envKeyName = config.envKey;
1038
+ let apiKey = env[envKeyName];
1039
+ if (!apiKey) {
1040
+ const entered = await p2.password({
1041
+ message: `Enter your API key for ${config.label} (${envKeyName})`,
1042
+ validate: (v) => !v.trim() ? "API key is required." : void 0
1043
+ });
1044
+ if (p2.isCancel(entered)) {
1045
+ p2.cancel("Cancelled.");
1046
+ process.exit(0);
1047
+ }
1048
+ apiKey = entered;
1049
+ const save = await p2.confirm({
1050
+ message: `Save ${envKeyName} to .env.local for future use?`,
1051
+ initialValue: true
1052
+ });
1053
+ if (!p2.isCancel(save) && save) {
1054
+ saveKeyToEnvLocal(cwd, envKeyName, apiKey);
1055
+ p2.log.success(`.env.local updated with ${envKeyName}`);
1056
+ }
1057
+ } else {
1058
+ p2.log.success(`API key ${envKeyName} found in the environment`);
1059
+ }
1060
+ return { config, apiKey };
1061
+ }
1062
+
1063
+ // src/generate.ts
1064
+ import { createDeepSeek } from "@ai-sdk/deepseek";
1065
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
1066
+ import { createXai } from "@ai-sdk/xai";
1067
+ import { createOllama } from "ollama-ai-provider";
1068
+ import { streamText } from "ai";
1069
+ function createLanguageModel(config, apiKey) {
1070
+ switch (config.provider) {
1071
+ case "deepseek": {
1072
+ const deepseek = createDeepSeek({ apiKey });
1073
+ return deepseek(config.modelId);
1074
+ }
1075
+ case "google": {
1076
+ const google = createGoogleGenerativeAI({ apiKey });
1077
+ return google(config.modelId);
1078
+ }
1079
+ case "xai": {
1080
+ const xai = createXai({ apiKey });
1081
+ return xai(config.modelId);
1082
+ }
1083
+ case "ollama": {
1084
+ const ollama = createOllama({ baseURL: "http://localhost:11434/api" });
1085
+ return ollama(config.modelId);
1086
+ }
1087
+ }
1088
+ }
1089
+ async function callModel(config, apiKey, prompt, onChunk) {
1090
+ const model = createLanguageModel(config, apiKey);
1091
+ const { fullStream } = streamText({
1092
+ model,
1093
+ prompt,
1094
+ temperature: 0.3,
1095
+ maxOutputTokens: 8e3
1096
+ });
1097
+ let fullText = "";
1098
+ for await (const part of fullStream) {
1099
+ if (part.type === "text-delta") {
1100
+ fullText += part.textDelta;
1101
+ onChunk(part.textDelta);
1102
+ }
1103
+ }
1104
+ return fullText;
1105
+ }
1106
+
1107
+ // src/history.ts
1108
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1109
+ import { homedir } from "os";
1110
+ import { join as join2 } from "path";
1111
+ import * as p3 from "@clack/prompts";
1112
+ var HISTORY_DIR = join2(homedir(), ".agent-md-generator");
1113
+ var HISTORY_FILE = join2(HISTORY_DIR, "history.json");
1114
+ var MAX_ENTRIES = 5;
1115
+ function saveToCliHistory(answers, content, modelLabel) {
1116
+ const entry = {
1117
+ id: Date.now().toString(),
1118
+ timestamp: Date.now(),
1119
+ projectName: answers["project_name"] || "Untitled",
1120
+ modelLabel,
1121
+ contentLength: content.length,
1122
+ content
1123
+ };
1124
+ const existing = loadCliHistory();
1125
+ const updated = [entry, ...existing].slice(0, MAX_ENTRIES);
1126
+ try {
1127
+ if (!existsSync3(HISTORY_DIR)) {
1128
+ mkdirSync(HISTORY_DIR, { recursive: true });
1129
+ }
1130
+ writeFileSync2(HISTORY_FILE, JSON.stringify(updated, null, 2), "utf-8");
1131
+ } catch {
1132
+ }
1133
+ }
1134
+ function loadCliHistory() {
1135
+ try {
1136
+ if (!existsSync3(HISTORY_FILE)) return [];
1137
+ const raw = readFileSync3(HISTORY_FILE, "utf-8");
1138
+ return JSON.parse(raw);
1139
+ } catch {
1140
+ return [];
1141
+ }
1142
+ }
1143
+ async function showHistory() {
1144
+ const entries = loadCliHistory();
1145
+ if (entries.length === 0) {
1146
+ p3.log.info("No generation history found.");
1147
+ return;
1148
+ }
1149
+ p3.log.info(`Latest ${entries.length} generation(s):
1150
+ `);
1151
+ for (const entry of entries) {
1152
+ const date = new Date(entry.timestamp).toLocaleString("en-US");
1153
+ const lines = entry.content.split("\n").length;
1154
+ p3.log.message(
1155
+ ` ${entry.projectName} \xB7 ${entry.modelLabel} \xB7 ${lines} lines
1156
+ ${date}
1157
+ `
1158
+ );
1159
+ }
1160
+ const chosen = await p3.select({
1161
+ message: "What would you like to do?",
1162
+ options: [
1163
+ { value: "view", label: "View the latest generation" },
1164
+ { value: "exit", label: "Exit" }
1165
+ ]
1166
+ });
1167
+ if (p3.isCancel(chosen) || chosen === "exit") return;
1168
+ if (chosen === "view" && entries[0]) {
1169
+ console.log("\n" + "\u2500".repeat(60));
1170
+ console.log(entries[0].content);
1171
+ console.log("\u2500".repeat(60) + "\n");
1172
+ }
1173
+ }
1174
+
1175
+ // src/prompts.ts
1176
+ import * as p4 from "@clack/prompts";
1177
+ async function promptQuestion(question, prefill) {
1178
+ const message = question.hint ? `${question.question}
1179
+ ${question.hint}` : question.question;
1180
+ switch (question.type) {
1181
+ case "text":
1182
+ case "textarea":
1183
+ return p4.text({
1184
+ message,
1185
+ placeholder: question.placeholder,
1186
+ initialValue: typeof prefill === "string" ? prefill : void 0,
1187
+ validate: question.required ? (v) => v.trim() === "" ? "This field is required." : void 0 : void 0
1188
+ });
1189
+ case "select":
1190
+ return p4.select({
1191
+ message,
1192
+ options: (question.options ?? []).map((o) => ({
1193
+ value: o.value,
1194
+ label: o.label,
1195
+ hint: o.description
1196
+ })),
1197
+ initialValue: typeof prefill === "string" ? prefill : void 0
1198
+ });
1199
+ case "multiselect":
1200
+ return p4.multiselect({
1201
+ message,
1202
+ options: (question.options ?? []).map((o) => ({
1203
+ value: o.value,
1204
+ label: o.label,
1205
+ hint: o.description
1206
+ })),
1207
+ required: question.required,
1208
+ initialValues: Array.isArray(prefill) ? prefill : void 0
1209
+ });
1210
+ }
1211
+ }
1212
+ function reviewAnswers(answers) {
1213
+ const steps = [1, 2, 3, 4];
1214
+ for (const step of steps) {
1215
+ const stepLabel = STEP_LABELS[step] ?? `Step ${step}`;
1216
+ const stepQuestions = QUESTIONS.filter(
1217
+ (q) => q.step === step && isQuestionVisible(q, answers)
1218
+ );
1219
+ const lines = [];
1220
+ for (const q of stepQuestions) {
1221
+ const val = answers[q.id];
1222
+ if (!val) continue;
1223
+ const displayVal = Array.isArray(val) ? val.join(", ") : val;
1224
+ lines.push(` ${q.question.split("?")[0]}:
1225
+ ${displayVal}`);
1226
+ }
1227
+ if (lines.length > 0) {
1228
+ p4.log.step(`Step ${step} \u2014 ${stepLabel}`);
1229
+ p4.log.message(lines.join("\n"));
1230
+ }
1231
+ }
1232
+ }
1233
+
1234
+ // src/index.ts
1235
+ function parseArgs() {
1236
+ const args = process.argv.slice(2);
1237
+ const result = { update: false, history: false, yes: false, init: false, templateId: void 0 };
1238
+ for (let i = 0; i < args.length; i++) {
1239
+ if (args[i] === "--update") result.update = true;
1240
+ else if (args[i] === "--history") result.history = true;
1241
+ else if (args[i] === "--yes") result.yes = true;
1242
+ else if (args[i] === "--init") result.init = true;
1243
+ else if (args[i] === "--template" && args[i + 1]) {
1244
+ result.templateId = args[i + 1];
1245
+ i++;
1246
+ }
1247
+ }
1248
+ return result;
1249
+ }
1250
+ function loadEnvLocal(cwd) {
1251
+ const envPath = resolve2(cwd, ".env.local");
1252
+ const env = {};
1253
+ if (!existsSync4(envPath)) return env;
1254
+ const lines = readFileSync4(envPath, "utf-8").split("\n");
1255
+ for (const line of lines) {
1256
+ const trimmed = line.trim();
1257
+ if (!trimmed || trimmed.startsWith("#")) continue;
1258
+ const eqIndex = trimmed.indexOf("=");
1259
+ if (eqIndex === -1) continue;
1260
+ const key = trimmed.slice(0, eqIndex).trim();
1261
+ const val = trimmed.slice(eqIndex + 1).trim().replace(/^["']|["']$/g, "");
1262
+ env[key] = val;
1263
+ if (!process.env[key]) process.env[key] = val;
1264
+ }
1265
+ return env;
1266
+ }
1267
+ async function runUpdateMode(cwd) {
1268
+ let agentMdPath = resolve2(cwd, "AGENT.md");
1269
+ if (!existsSync4(agentMdPath)) {
1270
+ const entered = await p5.text({
1271
+ message: "Path to AGENT.md to update:",
1272
+ validate: (v) => {
1273
+ if (!v.trim()) return "Required.";
1274
+ if (!existsSync4(v.trim())) return `File not found: ${v}`;
1275
+ }
1276
+ });
1277
+ if (p5.isCancel(entered)) {
1278
+ p5.cancel("Cancelled.");
1279
+ process.exit(0);
1280
+ }
1281
+ agentMdPath = entered.trim();
1282
+ }
1283
+ const existingContent = readFileSync4(agentMdPath, "utf-8");
1284
+ p5.log.success(`AGENT.md loaded (${existingContent.split("\n").length} lines)`);
1285
+ const change = await p5.text({
1286
+ message: "What do you want to change or add?",
1287
+ placeholder: "e.g. Add a Testing section with Jest. Update the tech stack with Drizzle.",
1288
+ validate: (v) => !v.trim() ? "Required." : void 0
1289
+ });
1290
+ if (p5.isCancel(change)) {
1291
+ p5.cancel("Cancelled.");
1292
+ process.exit(0);
1293
+ }
1294
+ const { config, apiKey } = await selectModel(cwd);
1295
+ console.log("\n");
1296
+ p5.log.step(`Updating with ${config.label}...`);
1297
+ console.log("\u2500".repeat(60));
1298
+ const prompt = buildUpdatePrompt(existingContent, change);
1299
+ let content = "";
1300
+ try {
1301
+ content = await callModel(config, apiKey, prompt, (chunk) => {
1302
+ process.stdout.write(chunk);
1303
+ });
1304
+ } catch (err) {
1305
+ console.log("\n");
1306
+ p5.cancel(err instanceof Error ? err.message : "Unknown error");
1307
+ process.exit(1);
1308
+ }
1309
+ console.log("\n" + "\u2500".repeat(60) + "\n");
1310
+ writeFileSync3(agentMdPath, content, "utf-8");
1311
+ p5.outro(`\u2713 AGENT.md updated \u2192 ${agentMdPath}`);
1312
+ }
1313
+ function splitInitOutput(raw) {
1314
+ if (!raw.includes("===AGENT_MD===")) return { agent: raw };
1315
+ const agentMatch = raw.match(/===AGENT_MD===([\s\S]*?)(?:===ROADMAP_MD===|$)/);
1316
+ const roadmapMatch = raw.match(/===ROADMAP_MD===([\s\S]*?)(?:===PROMPTS_MD===|$)/);
1317
+ const promptsMatch = raw.match(/===PROMPTS_MD===([\s\S]*)$/);
1318
+ return {
1319
+ agent: agentMatch?.[1]?.trim() ?? raw,
1320
+ roadmap: roadmapMatch?.[1]?.trim(),
1321
+ prompts: promptsMatch?.[1]?.trim()
1322
+ };
1323
+ }
1324
+ async function main() {
1325
+ const cwd = process.cwd();
1326
+ const loadedEnv = loadEnvLocal(cwd);
1327
+ const args = parseArgs();
1328
+ console.clear();
1329
+ p5.intro(" agent-md-generator ");
1330
+ if (args.history) {
1331
+ await showHistory();
1332
+ process.exit(0);
1333
+ }
1334
+ if (args.update) {
1335
+ await runUpdateMode(cwd);
1336
+ process.exit(0);
1337
+ }
1338
+ if (args.init) {
1339
+ p5.log.step("Running automatic non-interactive initialization...");
1340
+ const detected2 = await detectProject(cwd);
1341
+ const env = { ...loadedEnv, ...process.env };
1342
+ let selectedConfig = MODELS[0];
1343
+ let apiKey2 = null;
1344
+ if (env.DEEPSEEK_API_KEY) {
1345
+ selectedConfig = MODELS.find((m) => m.modelId === "deepseek-chat");
1346
+ apiKey2 = env.DEEPSEEK_API_KEY;
1347
+ } else if (env.GOOGLE_GENERATIVE_AI_API_KEY) {
1348
+ selectedConfig = MODELS.find((m) => m.modelId === "gemini-2.0-flash");
1349
+ apiKey2 = env.GOOGLE_GENERATIVE_AI_API_KEY;
1350
+ } else if (env.XAI_API_KEY) {
1351
+ selectedConfig = MODELS.find((m) => m.modelId === "grok-3-mini");
1352
+ apiKey2 = env.XAI_API_KEY;
1353
+ } else {
1354
+ selectedConfig = MODELS.find((m) => m.modelId === "llama3.2");
1355
+ apiKey2 = null;
1356
+ p5.log.info("No API keys found in environment, falling back to local Llama 3.2 Ollama.");
1357
+ }
1358
+ const answers2 = {
1359
+ project_name: detected2.projectName || "Untitled Project",
1360
+ project_type: detected2.manifestType === "unknown" ? "web_app" : detected2.manifestType === "npm" ? "web_app" : "other",
1361
+ project_description: detected2.description || "A software application project.",
1362
+ tech_stack: detected2.techStack.length ? detected2.techStack : ["typescript"],
1363
+ ai_features: "no",
1364
+ folder_structure: detected2.tree,
1365
+ coding_conventions: ["no_any", "small_files", "named_exports"],
1366
+ dev_philosophy: "production_grade",
1367
+ constraints: ["no_telemetry"],
1368
+ initialize_project: "no"
1369
+ };
1370
+ p5.log.step(`Generating AGENT.md using ${selectedConfig.label}...`);
1371
+ const prompt2 = buildPrompt(answers2);
1372
+ let rawContent2 = "";
1373
+ try {
1374
+ rawContent2 = await callModel(selectedConfig, apiKey2, prompt2, (chunk) => {
1375
+ process.stdout.write(chunk);
1376
+ });
1377
+ } catch (err) {
1378
+ console.log("\n");
1379
+ p5.cancel(err instanceof Error ? err.message : "Generation error");
1380
+ process.exit(1);
1381
+ }
1382
+ console.log("\n" + "\u2500".repeat(60) + "\n");
1383
+ const { agent: agentContent2 } = splitInitOutput(rawContent2);
1384
+ const agentPath = resolve2(detected2.rootPath, "AGENT.md");
1385
+ const claudePath = resolve2(detected2.rootPath, "CLAUDE.md");
1386
+ writeFileSync3(agentPath, agentContent2, "utf-8");
1387
+ writeFileSync3(claudePath, agentContent2, "utf-8");
1388
+ saveToCliHistory(answers2, agentContent2, selectedConfig.label);
1389
+ p5.outro(`\u2713 Done! Initialization complete:
1390
+ \u2192 ${agentPath}
1391
+ \u2192 ${claudePath}`);
1392
+ process.exit(0);
1393
+ }
1394
+ p5.log.step("Analyzing project...");
1395
+ const detected = await detectProject(cwd);
1396
+ const prefillSummary = [];
1397
+ if (detected.projectName) prefillSummary.push(`Name: ${detected.projectName}`);
1398
+ if (detected.techStack.length) prefillSummary.push(`Tech stack: ${detected.techStack.join(", ")}`);
1399
+ prefillSummary.push(`Structure read from: ${detected.rootPath}`);
1400
+ p5.log.success(`Project detected (${detected.manifestType})
1401
+ ${prefillSummary.join("\n ")}`);
1402
+ const { config: modelConfig, apiKey } = await selectModel(cwd);
1403
+ let answers = {};
1404
+ if (args.templateId) {
1405
+ const template = TEMPLATES.find((t) => t.id === args.templateId);
1406
+ if (template) {
1407
+ answers = { ...template.answers };
1408
+ p5.log.success(`Template "${template.name}" applied`);
1409
+ } else {
1410
+ p5.log.warn(`Template "${args.templateId}" not found. Starting from scratch.`);
1411
+ }
1412
+ } else {
1413
+ const useTemplate = await p5.select({
1414
+ message: "Start from a template?",
1415
+ options: [
1416
+ { value: "__none__", label: "Start from scratch" },
1417
+ ...TEMPLATES.map((t) => ({ value: t.id, label: t.name, hint: t.stack }))
1418
+ ]
1419
+ });
1420
+ if (p5.isCancel(useTemplate)) {
1421
+ p5.cancel("Cancelled.");
1422
+ process.exit(0);
1423
+ }
1424
+ if (useTemplate !== "__none__") {
1425
+ const template = TEMPLATES.find((t) => t.id === useTemplate);
1426
+ answers = { ...template.answers };
1427
+ p5.log.success(`Template "${template.name}" loaded`);
1428
+ }
1429
+ }
1430
+ if (detected.projectName && !answers["project_name"]) {
1431
+ answers["project_name"] = detected.projectName;
1432
+ }
1433
+ if (detected.description && !answers["project_description"]) {
1434
+ answers["project_description"] = detected.description;
1435
+ }
1436
+ if (detected.techStack.length && !answers["tech_stack"]) {
1437
+ answers["tech_stack"] = detected.techStack;
1438
+ }
1439
+ for (const question of QUESTIONS) {
1440
+ if (!isQuestionVisible(question, answers)) continue;
1441
+ if (question.id === "folder_structure") {
1442
+ answers["folder_structure"] = detected.tree;
1443
+ p5.log.success("Project structure auto-injected");
1444
+ continue;
1445
+ }
1446
+ const prefill = answers[question.id];
1447
+ const value = await promptQuestion(question, prefill);
1448
+ if (p5.isCancel(value)) {
1449
+ p5.cancel("Cancelled.");
1450
+ process.exit(0);
1451
+ }
1452
+ if (value !== void 0 && (typeof value !== "string" || value !== "")) {
1453
+ answers[question.id] = value;
1454
+ }
1455
+ }
1456
+ p5.log.step("Summary of your answers:");
1457
+ reviewAnswers(answers);
1458
+ const confirmed = await p5.confirm({
1459
+ message: "Generate AGENT.md now?",
1460
+ initialValue: true
1461
+ });
1462
+ if (p5.isCancel(confirmed) || !confirmed) {
1463
+ p5.cancel("Cancelled.");
1464
+ process.exit(0);
1465
+ }
1466
+ console.log("\n");
1467
+ p5.log.step(`Generating with ${modelConfig.label}...`);
1468
+ console.log("\u2500".repeat(60));
1469
+ const prompt = buildPrompt(answers);
1470
+ let rawContent = "";
1471
+ try {
1472
+ rawContent = await callModel(modelConfig, apiKey, prompt, (chunk) => {
1473
+ process.stdout.write(chunk);
1474
+ });
1475
+ } catch (err) {
1476
+ console.log("\n");
1477
+ p5.cancel(err instanceof Error ? err.message : "Generation error");
1478
+ process.exit(1);
1479
+ }
1480
+ console.log("\n" + "\u2500".repeat(60) + "\n");
1481
+ const { agent: agentContent, roadmap: roadmapContent, prompts: promptsContent } = splitInitOutput(rawContent);
1482
+ const { warnings } = validateOutput(agentContent);
1483
+ const score = scoreOutput(agentContent);
1484
+ p5.log.step(`Validating generated content`);
1485
+ const lines = agentContent.split("\n").length;
1486
+ p5.log.success(`${lines} lines generated`);
1487
+ if (warnings.length > 0) {
1488
+ for (const w of warnings) p5.log.warn(w);
1489
+ }
1490
+ const scoreLabel = `${score.found}/${score.total} sections covered`;
1491
+ if (score.found >= 8) {
1492
+ p5.log.success(`Completeness score: ${scoreLabel}`);
1493
+ } else {
1494
+ p5.log.warn(`Completeness score: ${scoreLabel}`);
1495
+ }
1496
+ const missingSections = score.sections.filter((s) => !s.present).map((s) => s.label);
1497
+ if (missingSections.length) {
1498
+ p5.log.info(`Missing sections: ${missingSections.join(", ")}`);
1499
+ }
1500
+ const formatOptions = OUTPUT_FORMATS.map((f) => ({
1501
+ value: f.filename,
1502
+ label: `${f.filename} (${f.tool})`
1503
+ }));
1504
+ const selectedFormats = await p5.multiselect({
1505
+ message: "Which files do you want to generate?",
1506
+ options: formatOptions,
1507
+ initialValues: ["AGENT.md", "CLAUDE.md"],
1508
+ required: true
1509
+ });
1510
+ if (p5.isCancel(selectedFormats)) {
1511
+ p5.cancel("Cancelled.");
1512
+ process.exit(0);
1513
+ }
1514
+ const writtenFiles = [];
1515
+ for (const filename of selectedFormats) {
1516
+ const outputPath = resolve2(detected.rootPath, filename);
1517
+ writeFileSync3(outputPath, agentContent, "utf-8");
1518
+ writtenFiles.push(outputPath);
1519
+ }
1520
+ if (roadmapContent) {
1521
+ const roadmapPath = resolve2(detected.rootPath, "ROADMAP.md");
1522
+ writeFileSync3(roadmapPath, roadmapContent, "utf-8");
1523
+ writtenFiles.push(roadmapPath);
1524
+ p5.log.success("ROADMAP.md generated");
1525
+ }
1526
+ if (promptsContent) {
1527
+ const promptsPath = resolve2(detected.rootPath, "PROMPTS.md");
1528
+ writeFileSync3(promptsPath, promptsContent, "utf-8");
1529
+ writtenFiles.push(promptsPath);
1530
+ p5.log.success("PROMPTS.md generated");
1531
+ }
1532
+ saveToCliHistory(answers, agentContent, modelConfig.label);
1533
+ const shareUrl = buildShareUrl(answers);
1534
+ p5.log.info(`Share your config:
1535
+ ${shareUrl}`);
1536
+ p5.outro(
1537
+ `\u2713 Done! Files written:
1538
+ ${writtenFiles.map((f) => ` \u2192 ${f}`).join("\n")}`
1539
+ );
1540
+ }
1541
+ main();