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.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/index.js +538 -0
- 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
|
+
}
|