prd-to-skill 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/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/index.js +538 -0
  4. package/package.json +77 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # prd-to-skill
2
+
3
+ Convert PRD documents (PDF/DOCX) into AI coding assistant instruction files.
4
+
5
+ Supports **Claude Code**, **Cursor**, **OpenAI Codex**, **GitHub Copilot**, **Windsurf**, and **Aider** — each with the correct file format and output location.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Set your API key
11
+ export OPENAI_API_KEY="sk-..."
12
+
13
+ # Generate a Claude Code skill (default)
14
+ npx prd-to-skill ./my-feature-prd.pdf
15
+
16
+ # Generate a Cursor rule
17
+ npx prd-to-skill ./my-feature-prd.pdf --target cursor
18
+
19
+ # Generate for all tools at once
20
+ for t in claude cursor codex copilot windsurf aider; do
21
+ npx prd-to-skill ./my-feature-prd.pdf --target $t
22
+ done
23
+ ```
24
+
25
+ ## Supported Targets
26
+
27
+ | Target | Flag | Output Path | Format |
28
+ | -------------- | --------------------------- | --------------------------------------------- | --------------------------- |
29
+ | Claude Code | `--target claude` (default) | `.claude/commands/<name>.md` | Markdown + YAML frontmatter |
30
+ | Cursor | `--target cursor` | `.cursor/rules/<name>.mdc` | MDC + YAML frontmatter |
31
+ | OpenAI Codex | `--target codex` | `./AGENTS.md` | Plain Markdown |
32
+ | GitHub Copilot | `--target copilot` | `.github/instructions/<name>.instructions.md` | Markdown + YAML frontmatter |
33
+ | Windsurf | `--target windsurf` | `.windsurf/rules/<name>.md` | Plain Markdown |
34
+ | Aider | `--target aider` | `./CONVENTIONS.md` | Plain Markdown |
35
+
36
+ Output directories are created automatically if they don't exist.
37
+
38
+ ## Supported LLM Providers
39
+
40
+ The tool auto-detects your provider from environment variables (checked in this order):
41
+
42
+ | Provider | Env Var | Default Model |
43
+ | --------- | ------------------- | -------------------------- |
44
+ | Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
45
+ | OpenAI | `OPENAI_API_KEY` | `gpt-4o` |
46
+ | Google | `GOOGLE_API_KEY` | `gemini-2.0-flash` |
47
+ | Mistral | `MISTRAL_API_KEY` | `mistral-large-latest` |
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ prd-to-skill <file> [options]
53
+ ```
54
+
55
+ ### Options
56
+
57
+ | Flag | Description | Default |
58
+ | -------------------------- | ------------------------------------------------------------------------ | --------------------- |
59
+ | `-t, --target <target>` | Target tool: `claude`, `cursor`, `codex`, `copilot`, `windsurf`, `aider` | `claude` |
60
+ | `-p, --provider <name>` | LLM provider: `openai`, `anthropic`, `google`, `mistral` | Auto-detected |
61
+ | `-m, --model <model>` | Model name | Provider default |
62
+ | `-o, --output <path>` | Output file path (overrides default) | Target-specific |
63
+ | `-n, --name <name>` | Skill/rule name | Derived from filename |
64
+ | `-d, --description <text>` | Description for frontmatter | Generated by LLM |
65
+ | `--max-tokens <number>` | Max output tokens | `4096` |
66
+ | `-v, --verbose` | Show extraction and API details | |
67
+
68
+ ### Examples
69
+
70
+ ```bash
71
+ # Use a specific provider and model
72
+ prd-to-skill ./prd.pdf --provider anthropic --model claude-sonnet-4-20250514
73
+
74
+ # Generate a Cursor rule with custom name
75
+ prd-to-skill ./prd.docx --target cursor --name auth-feature
76
+
77
+ # Custom output path
78
+ prd-to-skill ./prd.pdf --output ./my-custom-path/skill.md
79
+
80
+ # Verbose mode
81
+ prd-to-skill ./prd.pdf -v
82
+ ```
83
+
84
+ ## How It Works
85
+
86
+ 1. **Extracts text** from your PDF or Word document
87
+ 2. **Filters** for implementation-relevant content (requirements, architecture, business rules, acceptance criteria) and discards project management artifacts (timelines, stakeholders, budgets)
88
+ 3. **Sends to an LLM** with a target-specific prompt that generates the correct file format
89
+ 4. **Writes the file** to the right location for your chosen tool
90
+
91
+ ## License
92
+
93
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/extract.ts
7
+ import { access } from "fs/promises";
8
+ import { extname, resolve } from "path";
9
+ var extractText = async (filePath) => {
10
+ const resolved = resolve(filePath);
11
+ try {
12
+ await access(resolved);
13
+ } catch {
14
+ throw new Error(`File not found: ${filePath}`);
15
+ }
16
+ const ext = extname(resolved).toLowerCase();
17
+ let text;
18
+ if (ext === ".pdf") {
19
+ const { PDFParse } = await import("pdf-parse");
20
+ const pdf = new PDFParse({ url: resolved });
21
+ const result = await pdf.getText();
22
+ text = result.text;
23
+ } else if (ext === ".docx" || ext === ".doc") {
24
+ const mammoth = await import("mammoth");
25
+ const result = await mammoth.extractRawText({ path: resolved });
26
+ text = result.value;
27
+ } else {
28
+ throw new Error(`Unsupported file type "${ext}". Supported: .pdf, .docx`);
29
+ }
30
+ text = text.trim().replace(/\n{3,}/g, "\n\n");
31
+ if (!text) {
32
+ throw new Error(
33
+ "No text could be extracted from the document. Is it a scanned PDF? (Scanned PDFs are not supported.)"
34
+ );
35
+ }
36
+ return text;
37
+ };
38
+
39
+ // src/prompt.ts
40
+ var BASE_SYSTEM_PROMPT = `You are a technical writer specializing in AI coding assistant instruction files. You will receive the text content of a Product Requirements Document (PRD). Your job is to convert it into an instruction file for an AI coding tool.
41
+
42
+ ## What to Extract from the PRD
43
+
44
+ Focus ONLY on implementation-relevant content:
45
+ - Functional requirements \u2014 what the feature must do, user flows, expected behaviors
46
+ - Technical constraints \u2014 stack choices, API contracts, data models, auth patterns, performance targets
47
+ - Architecture decisions \u2014 component structure, naming conventions, integration points
48
+ - Business rules \u2014 domain logic that affects code behavior
49
+ - Acceptance criteria \u2014 what "done" looks like, edge cases to handle
50
+ - UI/UX specs \u2014 layout structure, interaction patterns, responsive behavior
51
+
52
+ ## What to Discard
53
+
54
+ Remove all non-technical content:
55
+ - Timelines, milestones, sprint planning
56
+ - Team assignments, RACI matrices
57
+ - Stakeholder lists, approval workflows
58
+ - Marketing copy, executive summaries
59
+ - Budget, resource allocation
60
+ - Meeting notes, decision logs
61
+
62
+ ## Content Guidelines
63
+
64
+ Convert the PRD requirements into ACTIONABLE INSTRUCTIONS for an AI agent. Do NOT simply restate the PRD \u2014 transform it:
65
+
66
+ 1. **Overview section**: 1-3 sentences stating what this helps build and the core approach.
67
+
68
+ 2. **Structured sections**: Break the PRD's requirements into logical sections. Each section should contain:
69
+ - Clear headings (## level)
70
+ - Imperative instructions ("Create X", "Ensure Y", "When Z, do W")
71
+ - Concrete patterns, code examples, or file structures where relevant
72
+ - Decision criteria (if X, then Y; if Z, then W)
73
+
74
+ 3. **Technical specifications**: Convert vague PRD language into specific technical guidance. If the PRD says "should be fast", translate to specific patterns (lazy loading, pagination, caching strategies).
75
+
76
+ 4. **Architecture guidance**: Include file structure recommendations, naming conventions, and integration patterns.
77
+
78
+ 5. **Quality checklist**: End with a verification section listing what "done" looks like.
79
+
80
+ ## Style Rules
81
+
82
+ - Write in direct, imperative voice ("Create the component", not "You should create the component")
83
+ - Be specific and concrete \u2014 no hand-waving
84
+ - Include code snippets for patterns that would be ambiguous in prose
85
+ - Use tables for decision matrices or option comparisons
86
+ - Address the reader as an AI agent that will execute these instructions
87
+ - Every sentence should add actionable information
88
+ - Remove filler, marketing language, and non-technical content
89
+
90
+ Output ONLY the complete file. No preamble, no explanation, no code fences wrapping the entire output.`;
91
+ var buildMessages = (prdText, name, target, description) => {
92
+ const systemPrompt = `${BASE_SYSTEM_PROMPT}
93
+
94
+ ## Output Format (${target.name})
95
+
96
+ You are generating a file for: ${target.description}
97
+
98
+ ${target.formatInstructions}`;
99
+ let userContent = `Here is the PRD content to convert into an AI coding instruction file:
100
+
101
+ <prd>
102
+ ${prdText}
103
+ </prd>
104
+
105
+ Name: ${name}`;
106
+ if (description) {
107
+ userContent += `
108
+ Description: ${description}`;
109
+ }
110
+ return [
111
+ { role: "system", content: systemPrompt },
112
+ { role: "user", content: userContent }
113
+ ];
114
+ };
115
+
116
+ // src/providers/openai.ts
117
+ var complete = async (config, messages) => {
118
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ Authorization: `Bearer ${config.apiKey}`
123
+ },
124
+ body: JSON.stringify({
125
+ model: config.model,
126
+ max_tokens: config.maxTokens,
127
+ messages: messages.map((m) => ({ role: m.role, content: m.content }))
128
+ })
129
+ });
130
+ if (!res.ok) {
131
+ const body = await res.text();
132
+ throw new Error(`OpenAI API error (${res.status}): ${body}`);
133
+ }
134
+ const data = await res.json();
135
+ return data.choices[0].message.content;
136
+ };
137
+
138
+ // src/providers/anthropic.ts
139
+ var complete2 = async (config, messages) => {
140
+ const systemMsg = messages.find((m) => m.role === "system");
141
+ const nonSystemMsgs = messages.filter((m) => m.role !== "system");
142
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ "x-api-key": config.apiKey,
147
+ "anthropic-version": "2023-06-01"
148
+ },
149
+ body: JSON.stringify({
150
+ model: config.model,
151
+ max_tokens: config.maxTokens,
152
+ system: systemMsg?.content ?? "",
153
+ messages: nonSystemMsgs.map((m) => ({ role: m.role, content: m.content }))
154
+ })
155
+ });
156
+ if (!res.ok) {
157
+ const body = await res.text();
158
+ throw new Error(`Anthropic API error (${res.status}): ${body}`);
159
+ }
160
+ const data = await res.json();
161
+ return data.content[0].text;
162
+ };
163
+
164
+ // src/providers/google.ts
165
+ var complete3 = async (config, messages) => {
166
+ const systemMsg = messages.find((m) => m.role === "system");
167
+ const nonSystemMsgs = messages.filter((m) => m.role !== "system");
168
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`;
169
+ const contents = nonSystemMsgs.map((m) => ({
170
+ role: m.role === "assistant" ? "model" : "user",
171
+ parts: [{ text: m.content }]
172
+ }));
173
+ const body = {
174
+ contents,
175
+ generationConfig: {
176
+ maxOutputTokens: config.maxTokens
177
+ }
178
+ };
179
+ if (systemMsg) {
180
+ body.systemInstruction = { parts: [{ text: systemMsg.content }] };
181
+ }
182
+ const res = await fetch(url, {
183
+ method: "POST",
184
+ headers: { "Content-Type": "application/json" },
185
+ body: JSON.stringify(body)
186
+ });
187
+ if (!res.ok) {
188
+ const respBody = await res.text();
189
+ throw new Error(`Google API error (${res.status}): ${respBody}`);
190
+ }
191
+ const data = await res.json();
192
+ return data.candidates[0].content.parts[0].text;
193
+ };
194
+
195
+ // src/providers/mistral.ts
196
+ var complete4 = async (config, messages) => {
197
+ const res = await fetch("https://api.mistral.ai/v1/chat/completions", {
198
+ method: "POST",
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ Authorization: `Bearer ${config.apiKey}`
202
+ },
203
+ body: JSON.stringify({
204
+ model: config.model,
205
+ max_tokens: config.maxTokens,
206
+ messages: messages.map((m) => ({ role: m.role, content: m.content }))
207
+ })
208
+ });
209
+ if (!res.ok) {
210
+ const body = await res.text();
211
+ throw new Error(`Mistral API error (${res.status}): ${body}`);
212
+ }
213
+ const data = await res.json();
214
+ return data.choices[0].message.content;
215
+ };
216
+
217
+ // src/providers/detect.ts
218
+ var providers = [
219
+ {
220
+ name: "anthropic",
221
+ envVar: "ANTHROPIC_API_KEY",
222
+ defaultModel: "claude-sonnet-4-20250514",
223
+ complete: complete2
224
+ },
225
+ {
226
+ name: "openai",
227
+ envVar: "OPENAI_API_KEY",
228
+ defaultModel: "gpt-4o",
229
+ complete
230
+ },
231
+ {
232
+ name: "google",
233
+ envVar: "GOOGLE_API_KEY",
234
+ defaultModel: "gemini-2.0-flash",
235
+ complete: complete3
236
+ },
237
+ {
238
+ name: "mistral",
239
+ envVar: "MISTRAL_API_KEY",
240
+ defaultModel: "mistral-large-latest",
241
+ complete: complete4
242
+ }
243
+ ];
244
+ var detectProvider = (explicit) => {
245
+ if (explicit) {
246
+ const provider = providers.find((p) => p.name === explicit);
247
+ if (!provider) {
248
+ throw new Error(
249
+ `Unknown provider "${explicit}". Supported: ${providers.map((p) => p.name).join(", ")}`
250
+ );
251
+ }
252
+ if (!process.env[provider.envVar]) {
253
+ throw new Error(`Provider "${explicit}" requires ${provider.envVar} to be set.`);
254
+ }
255
+ return provider;
256
+ }
257
+ for (const provider of providers) {
258
+ if (process.env[provider.envVar]) {
259
+ return provider;
260
+ }
261
+ }
262
+ throw new Error(
263
+ `No API key found. Set one of: ${providers.map((p) => p.envVar).join(", ")}`
264
+ );
265
+ };
266
+ var getApiKey = (provider) => {
267
+ const key = process.env[provider.envVar];
268
+ if (!key) {
269
+ throw new Error(`${provider.envVar} is not set.`);
270
+ }
271
+ return key;
272
+ };
273
+
274
+ // src/output.ts
275
+ import { mkdir, writeFile } from "fs/promises";
276
+ import { basename, dirname, join as join2, resolve as resolve2 } from "path";
277
+
278
+ // src/targets.ts
279
+ import { join } from "path";
280
+ var targets = {
281
+ claude: {
282
+ name: "claude",
283
+ description: "Claude Code skill (.md with YAML frontmatter)",
284
+ extension: ".md",
285
+ outputDir: (cwd) => join(cwd, ".claude", "commands"),
286
+ formatInstructions: `The file MUST begin with YAML frontmatter:
287
+
288
+ ---
289
+ name: <kebab-case-name>
290
+ description: <1-2 sentence description of when to use this skill. Start with "Use when..." This description is used for automatic skill matching.>
291
+ ---
292
+
293
+ Then the markdown body with ## sections.`
294
+ },
295
+ cursor: {
296
+ name: "cursor",
297
+ description: "Cursor rule (.mdc with YAML frontmatter)",
298
+ extension: ".mdc",
299
+ outputDir: (cwd) => join(cwd, ".cursor", "rules"),
300
+ formatInstructions: `The file MUST begin with YAML frontmatter:
301
+
302
+ ---
303
+ description: <1-2 sentence description of when this rule applies>
304
+ globs:
305
+ alwaysApply: true
306
+ ---
307
+
308
+ Then the markdown body with ## sections. Do NOT include a "name" field in frontmatter \u2014 Cursor uses the filename as the name.`
309
+ },
310
+ codex: {
311
+ name: "codex",
312
+ description: "OpenAI Codex CLI (AGENTS.md, plain markdown)",
313
+ extension: ".md",
314
+ outputDir: (cwd) => cwd,
315
+ formatInstructions: `The file is plain markdown with NO YAML frontmatter. Start directly with a heading:
316
+
317
+ # <Title>
318
+
319
+ Use standard markdown with ## sections. This file will be read as AGENTS.md by the Codex CLI. Keep it under 32 KiB.`
320
+ },
321
+ copilot: {
322
+ name: "copilot",
323
+ description: "GitHub Copilot instructions (.md in .github/)",
324
+ extension: ".instructions.md",
325
+ outputDir: (cwd) => join(cwd, ".github", "instructions"),
326
+ formatInstructions: `The file MUST begin with YAML frontmatter:
327
+
328
+ ---
329
+ description: <1-2 sentence description of what this instruction covers>
330
+ applyTo: "**"
331
+ ---
332
+
333
+ Then the markdown body with ## sections. Write instructions as guidance for a coding assistant.`
334
+ },
335
+ windsurf: {
336
+ name: "windsurf",
337
+ description: "Windsurf rule (.md in .windsurf/rules/)",
338
+ extension: ".md",
339
+ outputDir: (cwd) => join(cwd, ".windsurf", "rules"),
340
+ formatInstructions: `The file is plain markdown with NO YAML frontmatter. Start directly with a heading:
341
+
342
+ # <Title>
343
+
344
+ Use structured markdown with ## sections, bullet points, and numbered lists for clear rules.`
345
+ },
346
+ aider: {
347
+ name: "aider",
348
+ description: "Aider conventions (CONVENTIONS.md, plain markdown)",
349
+ extension: ".md",
350
+ outputDir: (cwd) => cwd,
351
+ formatInstructions: `The file is plain markdown with NO YAML frontmatter. Start directly with a heading:
352
+
353
+ # <Title>
354
+
355
+ Use structured markdown with ## sections. Focus on coding guidelines, style preferences, patterns, and conventions that an AI coding assistant should follow.`
356
+ }
357
+ };
358
+ var TARGET_NAMES = Object.keys(targets);
359
+ var getTarget = (name) => {
360
+ const target = targets[name];
361
+ if (!target) {
362
+ throw new Error(`Unknown target "${name}". Supported: ${TARGET_NAMES.join(", ")}`);
363
+ }
364
+ return target;
365
+ };
366
+ var getDefaultFilename = (target, name) => {
367
+ if (target.name === "codex") return "AGENTS.md";
368
+ if (target.name === "aider") return "CONVENTIONS.md";
369
+ return `${name}${target.extension}`;
370
+ };
371
+
372
+ // src/output.ts
373
+ var deriveSkillName = (filePath) => basename(filePath).replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
374
+ var deriveOutputPath = (name, target, outputFlag) => {
375
+ if (outputFlag) return resolve2(outputFlag);
376
+ const dir = target.outputDir(process.cwd());
377
+ const filename = getDefaultFilename(target, name);
378
+ return join2(dir, filename);
379
+ };
380
+ var writeSkillFile = async (content, outputPath) => {
381
+ const normalized = content.replace(/\r\n/g, "\n").trim() + "\n";
382
+ await mkdir(dirname(outputPath), { recursive: true });
383
+ await writeFile(outputPath, normalized, "utf-8");
384
+ };
385
+
386
+ // src/generate.ts
387
+ var generate = async (options) => {
388
+ const provider = detectProvider(options.provider);
389
+ const apiKey = getApiKey(provider);
390
+ const model = options.model ?? provider.defaultModel;
391
+ const name = options.name ?? deriveSkillName(options.filePath);
392
+ const target = getTarget(options.target);
393
+ const outputPath = deriveOutputPath(name, target, options.output);
394
+ if (options.verbose) {
395
+ console.error(`Provider: ${provider.name}`);
396
+ console.error(`Model: ${model}`);
397
+ console.error(`Target: ${target.name} (${target.description})`);
398
+ console.error(`Extracting text from: ${options.filePath}`);
399
+ }
400
+ const prdText = await extractText(options.filePath);
401
+ if (options.verbose) {
402
+ console.error(`Extracted ${prdText.length} characters`);
403
+ }
404
+ const messages = buildMessages(prdText, name, target, options.description);
405
+ if (options.verbose) {
406
+ console.error("Sending to LLM...");
407
+ }
408
+ const result = await provider.complete(
409
+ { apiKey, model, maxTokens: options.maxTokens },
410
+ messages
411
+ );
412
+ await writeSkillFile(result, outputPath);
413
+ console.log(`${target.name} file written to: ${outputPath}`);
414
+ };
415
+
416
+ // src/help.ts
417
+ var providers2 = {
418
+ openai: {
419
+ name: "OpenAI",
420
+ envVar: "OPENAI_API_KEY",
421
+ docsUrl: "https://platform.openai.com/api-keys",
422
+ steps: `1. Sign up or log in at platform.openai.com
423
+ 2. Go to API Keys and create a new secret key
424
+ 3. Export it: export OPENAI_API_KEY="sk-..."`
425
+ },
426
+ anthropic: {
427
+ name: "Anthropic",
428
+ envVar: "ANTHROPIC_API_KEY",
429
+ docsUrl: "https://console.anthropic.com/settings/keys",
430
+ steps: `1. Sign up or log in at console.anthropic.com
431
+ 2. Go to Settings > API Keys and create a key
432
+ 3. Export it: export ANTHROPIC_API_KEY="sk-ant-..."`
433
+ },
434
+ google: {
435
+ name: "Google (Gemini)",
436
+ envVar: "GOOGLE_API_KEY",
437
+ docsUrl: "https://aistudio.google.com/apikey",
438
+ steps: `1. Go to Google AI Studio
439
+ 2. Click "Get API key" and create one
440
+ 3. Export it: export GOOGLE_API_KEY="AI..."`
441
+ },
442
+ mistral: {
443
+ name: "Mistral",
444
+ envVar: "MISTRAL_API_KEY",
445
+ docsUrl: "https://console.mistral.ai/api-keys",
446
+ steps: `1. Sign up or log in at console.mistral.ai
447
+ 2. Go to API Keys and create a new key
448
+ 3. Export it: export MISTRAL_API_KEY="..."`
449
+ }
450
+ };
451
+ var GENERAL_HELP = `
452
+ prd-to-skill \u2014 Convert PRD documents into AI coding assistant instruction files.
453
+
454
+ Takes a PDF or Word (.docx) PRD and generates a ready-to-use instruction file
455
+ for your AI coding tool of choice.
456
+
457
+ QUICK START:
458
+ export OPENAI_API_KEY="sk-..."
459
+ npx prd-to-skill ./my-prd.pdf
460
+
461
+ SUPPORTED TARGETS:
462
+ claude Claude Code skill \u2192 .claude/commands/<name>.md
463
+ cursor Cursor rule \u2192 .cursor/rules/<name>.mdc
464
+ codex OpenAI Codex CLI \u2192 ./AGENTS.md
465
+ copilot GitHub Copilot \u2192 .github/instructions/<name>.instructions.md
466
+ windsurf Windsurf rule \u2192 .windsurf/rules/<name>.md
467
+ aider Aider conventions \u2192 ./CONVENTIONS.md
468
+
469
+ SUPPORTED PROVIDERS:
470
+ openai OPENAI_API_KEY Default: gpt-4o
471
+ anthropic ANTHROPIC_API_KEY Default: claude-sonnet-4-20250514
472
+ google GOOGLE_API_KEY Default: gemini-2.0-flash
473
+ mistral MISTRAL_API_KEY Default: mistral-large-latest
474
+
475
+ The provider is auto-detected from whichever API key you have set.
476
+ For provider-specific setup, run: prd-to-skill help <provider>
477
+
478
+ EXAMPLES:
479
+ prd-to-skill ./prd.pdf
480
+ prd-to-skill ./prd.docx --target cursor
481
+ prd-to-skill ./prd.pdf --provider anthropic --model claude-sonnet-4-20250514
482
+ prd-to-skill ./prd.pdf -n auth-feature -o .claude/commands/auth.md
483
+ `;
484
+ var printHelp = (provider) => {
485
+ if (!provider) {
486
+ console.log(GENERAL_HELP.trim());
487
+ return;
488
+ }
489
+ const p = providers2[provider.toLowerCase()];
490
+ if (!p) {
491
+ console.error(
492
+ `Unknown provider "${provider}". Available: ${Object.keys(providers2).join(", ")}`
493
+ );
494
+ process.exit(1);
495
+ }
496
+ console.log(
497
+ `
498
+ ${p.name} Setup
499
+ ${"=".repeat(p.name.length + 6)}
500
+
501
+ Env var: ${p.envVar}
502
+ Docs: ${p.docsUrl}
503
+
504
+ ${p.steps}
505
+
506
+ Then run:
507
+ prd-to-skill ./my-prd.pdf --provider ${provider.toLowerCase()}
508
+ `.trim()
509
+ );
510
+ };
511
+
512
+ // src/index.ts
513
+ var program = new Command();
514
+ program.name("prd-to-skill").description(
515
+ "Convert PRD documents (PDF/DOCX) into AI coding assistant instruction files"
516
+ ).version("0.1.0");
517
+ program.command("help [provider]").aliases(["h"]).description("Show detailed help, or setup guide for a specific provider").action((provider) => {
518
+ printHelp(provider);
519
+ });
520
+ program.argument("<file>", "Path to PRD file (.pdf or .docx)").option("-p, --provider <name>", "LLM provider: openai | anthropic | google | mistral").option("-m, --model <model>", "Model name (e.g. gpt-4o, claude-sonnet-4-20250514)").option("-o, --output <path>", "Output file path (overrides default)").option("-n, --name <name>", "Skill/rule name (default: derived from filename)").option("-d, --description <text>", "Description for frontmatter").option(`-t, --target <target>`, `Target tool: ${TARGET_NAMES.join(", ")}`, "claude").option("--max-tokens <number>", "Max output tokens", "4096").option("-v, --verbose", "Show extraction and API details").action(async (file, opts) => {
521
+ try {
522
+ await generate({
523
+ filePath: file,
524
+ provider: opts.provider,
525
+ model: opts.model,
526
+ name: opts.name,
527
+ description: opts.description,
528
+ output: opts.output,
529
+ target: opts.target,
530
+ maxTokens: parseInt(opts.maxTokens, 10),
531
+ verbose: opts.verbose ?? false
532
+ });
533
+ } catch (err) {
534
+ console.error(`Error: ${err.message}`);
535
+ process.exit(1);
536
+ }
537
+ });
538
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "prd-to-skill",
3
+ "version": "0.1.1",
4
+ "description": "Convert PRD documents (PDF/DOCX) into AI coding assistant instruction files",
5
+ "type": "module",
6
+ "bin": {
7
+ "prd-to-skill": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup src/index.ts --format esm --clean --shims --tsconfig tsconfig.build.json",
14
+ "dev": "tsx src/index.ts",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "test:coverage": "vitest run --coverage",
18
+ "lint": "eslint src/ tests/",
19
+ "lint:fix": "eslint src/ tests/ --fix",
20
+ "format": "prettier --write .",
21
+ "format:check": "prettier --check .",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepare": "husky",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "keywords": [
30
+ "prd",
31
+ "skill",
32
+ "claude",
33
+ "claude-code",
34
+ "cursor",
35
+ "codex",
36
+ "copilot",
37
+ "windsurf",
38
+ "aider",
39
+ "markdown",
40
+ "cli",
41
+ "ai"
42
+ ],
43
+ "lint-staged": {
44
+ "*.{ts,js}": [
45
+ "eslint --fix",
46
+ "prettier --write"
47
+ ],
48
+ "*.{json,md,yml,yaml}": [
49
+ "prettier --write"
50
+ ]
51
+ },
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "https://github.com/leosantos/prd-to-skill"
56
+ },
57
+ "dependencies": {
58
+ "commander": "^14.0.3",
59
+ "mammoth": "^1.12.0",
60
+ "pdf-parse": "^2.4.5"
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "^10.0.1",
64
+ "@types/node": "^25.6.0",
65
+ "@vitest/coverage-v8": "^4.1.4",
66
+ "eslint": "^10.2.0",
67
+ "eslint-config-prettier": "^10.1.8",
68
+ "husky": "^9.1.7",
69
+ "lint-staged": "^16.4.0",
70
+ "prettier": "^3.8.2",
71
+ "tsup": "^8.5.1",
72
+ "tsx": "^4.21.0",
73
+ "typescript": "^6.0.2",
74
+ "typescript-eslint": "^8.58.2",
75
+ "vitest": "^4.1.4"
76
+ }
77
+ }