prd-to-skill 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/index.js +109 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,12 +39,12 @@ Output directories are created automatically if they don't exist.
|
|
|
39
39
|
|
|
40
40
|
The tool auto-detects your provider from environment variables (checked in this order):
|
|
41
41
|
|
|
42
|
-
| Provider | Env Var | Default Model
|
|
43
|
-
| --------- | ------------------- |
|
|
42
|
+
| Provider | Env Var | Default Model |
|
|
43
|
+
| --------- | ------------------- | ---------------------------- |
|
|
44
44
|
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-6-20250217` |
|
|
45
|
-
| OpenAI | `OPENAI_API_KEY` | `gpt-5.4`
|
|
46
|
-
| Google | `GOOGLE_API_KEY` | `gemini-2.5-flash`
|
|
47
|
-
| Mistral | `MISTRAL_API_KEY` | `mistral-large-latest`
|
|
45
|
+
| OpenAI | `OPENAI_API_KEY` | `gpt-5.4` |
|
|
46
|
+
| Google | `GOOGLE_API_KEY` | `gemini-2.5-flash` |
|
|
47
|
+
| Mistral | `MISTRAL_API_KEY` | `mistral-large-latest` |
|
|
48
48
|
|
|
49
49
|
## Usage
|
|
50
50
|
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,22 @@ var extractText = async (filePath) => {
|
|
|
35
35
|
}
|
|
36
36
|
return text;
|
|
37
37
|
};
|
|
38
|
+
var TRUNCATION_PATTERNS = [
|
|
39
|
+
/[a-z,]\s*$/,
|
|
40
|
+
// ends mid-word or after a comma
|
|
41
|
+
/\(\s*e\.g\.\s*,?\s*$/,
|
|
42
|
+
// ends mid-parenthetical like "(e.g.,"
|
|
43
|
+
/:\s*$/,
|
|
44
|
+
// ends with a colon expecting more
|
|
45
|
+
/\b(and|or|the|a|an|to|for|in|of|with|by|from|as|is|are|was|were|that|this|which|when|where|how|but|if|not|on|at|into|about|than|then|such|each|per|via)\s*$/i
|
|
46
|
+
];
|
|
47
|
+
var detectTruncation = (text) => {
|
|
48
|
+
const lastLine = text.split("\n").filter(Boolean).pop() ?? "";
|
|
49
|
+
const trimmed = lastLine.trim();
|
|
50
|
+
if (!trimmed) return false;
|
|
51
|
+
if (!/[.!?:)\]"']$/.test(trimmed)) return true;
|
|
52
|
+
return TRUNCATION_PATTERNS.some((p) => p.test(trimmed));
|
|
53
|
+
};
|
|
38
54
|
|
|
39
55
|
// src/prompt.ts
|
|
40
56
|
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.
|
|
@@ -88,14 +104,27 @@ Convert the PRD requirements into ACTIONABLE INSTRUCTIONS for an AI agent. Do NO
|
|
|
88
104
|
- Remove filler, marketing language, and non-technical content
|
|
89
105
|
|
|
90
106
|
Output ONLY the complete file. No preamble, no explanation, no code fences wrapping the entire output.`;
|
|
91
|
-
var buildMessages = (prdText, name, target, description) => {
|
|
92
|
-
|
|
107
|
+
var buildMessages = (prdText, name, target, description, truncated = false) => {
|
|
108
|
+
let systemPrompt = `${BASE_SYSTEM_PROMPT}
|
|
93
109
|
|
|
94
110
|
## Output Format (${target.name})
|
|
95
111
|
|
|
96
112
|
You are generating a file for: ${target.description}
|
|
97
113
|
|
|
98
114
|
${target.formatInstructions}`;
|
|
115
|
+
if (truncated) {
|
|
116
|
+
systemPrompt += `
|
|
117
|
+
|
|
118
|
+
## Important: Incomplete Document
|
|
119
|
+
|
|
120
|
+
The provided PRD appears to be truncated \u2014 it ends abruptly mid-sentence or mid-section. Handle this as follows:
|
|
121
|
+
- Generate the instruction file based ONLY on the content that is present. Do NOT invent or guess missing requirements.
|
|
122
|
+
- At the very end of the output, add a section:
|
|
123
|
+
|
|
124
|
+
## Incomplete Sections
|
|
125
|
+
|
|
126
|
+
Note which sections or topics appear to be cut off or missing based on context clues (e.g., a numbered list that stops, a heading with no content, a sentence that ends mid-thought). List them as bullet points so the user knows what to add.`;
|
|
127
|
+
}
|
|
99
128
|
let userContent = `Here is the PRD content to convert into an AI coding instruction file:
|
|
100
129
|
|
|
101
130
|
<prd>
|
|
@@ -383,6 +412,32 @@ var writeSkillFile = async (content, outputPath) => {
|
|
|
383
412
|
await writeFile(outputPath, normalized, "utf-8");
|
|
384
413
|
};
|
|
385
414
|
|
|
415
|
+
// src/spinner.ts
|
|
416
|
+
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
417
|
+
var createSpinner = (message) => {
|
|
418
|
+
let i = 0;
|
|
419
|
+
const stream = process.stderr;
|
|
420
|
+
const write = (text) => {
|
|
421
|
+
stream.write(`\r\x1B[K${text}`);
|
|
422
|
+
};
|
|
423
|
+
write(`${frames[0]} ${message}`);
|
|
424
|
+
const timer = setInterval(() => {
|
|
425
|
+
i = (i + 1) % frames.length;
|
|
426
|
+
write(`${frames[i]} ${message}`);
|
|
427
|
+
}, 80);
|
|
428
|
+
return {
|
|
429
|
+
update: (msg) => {
|
|
430
|
+
message = msg;
|
|
431
|
+
write(`${frames[i]} ${message}`);
|
|
432
|
+
},
|
|
433
|
+
stop: (msg) => {
|
|
434
|
+
clearInterval(timer);
|
|
435
|
+
write(`${msg}
|
|
436
|
+
`);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
|
|
386
441
|
// src/generate.ts
|
|
387
442
|
var generate = async (options) => {
|
|
388
443
|
const provider = detectProvider(options.provider);
|
|
@@ -395,22 +450,68 @@ var generate = async (options) => {
|
|
|
395
450
|
console.error(`Provider: ${provider.name}`);
|
|
396
451
|
console.error(`Model: ${model}`);
|
|
397
452
|
console.error(`Target: ${target.name} (${target.description})`);
|
|
398
|
-
console.error(`Extracting text from: ${options.filePath}`);
|
|
399
453
|
}
|
|
454
|
+
const spinner = createSpinner("Extracting text from document...");
|
|
400
455
|
const prdText = await extractText(options.filePath);
|
|
401
|
-
|
|
402
|
-
|
|
456
|
+
const isTruncated = detectTruncation(prdText);
|
|
457
|
+
if (isTruncated) {
|
|
458
|
+
spinner.stop(
|
|
459
|
+
`\u26A0 Extracted ${prdText.length.toLocaleString()} characters \u2014 document appears to be incomplete or cut off`
|
|
460
|
+
);
|
|
461
|
+
console.error(
|
|
462
|
+
" The document ends abruptly. The generated output will cover only what was provided."
|
|
463
|
+
);
|
|
464
|
+
} else {
|
|
465
|
+
spinner.stop(`\u2713 Extracted ${prdText.length.toLocaleString()} characters`);
|
|
403
466
|
}
|
|
404
|
-
const messages = buildMessages(prdText, name, target, options.description);
|
|
405
467
|
if (options.verbose) {
|
|
406
|
-
console.error(
|
|
468
|
+
console.error(` Source: ${options.filePath}`);
|
|
407
469
|
}
|
|
470
|
+
const messages = buildMessages(prdText, name, target, options.description, isTruncated);
|
|
471
|
+
const llmSpinner = createSpinner(
|
|
472
|
+
`Generating ${target.name} file via ${provider.name} (${model})...`
|
|
473
|
+
);
|
|
474
|
+
const startTime = Date.now();
|
|
475
|
+
const elapsed = setInterval(() => {
|
|
476
|
+
const secs = Math.floor((Date.now() - startTime) / 1e3);
|
|
477
|
+
llmSpinner.update(
|
|
478
|
+
`Generating ${target.name} file via ${provider.name} (${model})... ${secs}s`
|
|
479
|
+
);
|
|
480
|
+
}, 1e3);
|
|
408
481
|
const result = await provider.complete(
|
|
409
482
|
{ apiKey, model, maxTokens: options.maxTokens },
|
|
410
483
|
messages
|
|
411
484
|
);
|
|
485
|
+
clearInterval(elapsed);
|
|
486
|
+
const totalSecs = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
487
|
+
const outputTruncated = detectTruncation(result);
|
|
488
|
+
if (outputTruncated) {
|
|
489
|
+
llmSpinner.stop(`\u26A0 Generated in ${totalSecs}s \u2014 output appears cut off`);
|
|
490
|
+
} else {
|
|
491
|
+
llmSpinner.stop(`\u2713 Generated in ${totalSecs}s`);
|
|
492
|
+
}
|
|
493
|
+
const writeSpinner = createSpinner("Writing file...");
|
|
412
494
|
await writeSkillFile(result, outputPath);
|
|
413
|
-
|
|
495
|
+
writeSpinner.stop(`\u2713 Saved to ${outputPath}`);
|
|
496
|
+
if (outputTruncated) {
|
|
497
|
+
const nextMaxTokens = options.maxTokens * 2;
|
|
498
|
+
console.error("");
|
|
499
|
+
console.error(
|
|
500
|
+
"\u26A0 The generated output appears incomplete \u2014 the LLM likely hit the token limit."
|
|
501
|
+
);
|
|
502
|
+
console.error("");
|
|
503
|
+
console.error(" To fix this, try one of the following:");
|
|
504
|
+
console.error("");
|
|
505
|
+
console.error(
|
|
506
|
+
` 1. Increase max tokens: prd-to-skill ${options.filePath} --max-tokens ${nextMaxTokens}`
|
|
507
|
+
);
|
|
508
|
+
console.error(` 2. Use a higher-capacity model with a larger output window`);
|
|
509
|
+
console.error(
|
|
510
|
+
` 3. Split your PRD into smaller focused documents and generate a skill for each`
|
|
511
|
+
);
|
|
512
|
+
console.error("");
|
|
513
|
+
console.error(` Current --max-tokens: ${options.maxTokens}`);
|
|
514
|
+
}
|
|
414
515
|
};
|
|
415
516
|
|
|
416
517
|
// src/help.ts
|