diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6bc4cd6..e203893 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Skills shared by Baoyu for improving daily work efficiency", - "version": "0.6.0" + "version": "0.6.1" }, "plugins": [ { diff --git a/CLAUDE.md b/CLAUDE.md index 4ca8114..96d321c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,30 @@ npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md conte 2. Add TypeScript in `skills/baoyu-/scripts/` 3. Add prompt templates in `skills/baoyu-/prompts/` if needed 4. Register in `marketplace.json` plugins[0].skills array as `./skills/baoyu-` +5. **Add Script Directory section** to SKILL.md (see template below) + +### Script Directory Template + +Every SKILL.md with scripts MUST include this section after Usage: + +```markdown +## Script Directory + +**Important**: All scripts are located in the `scripts/` subdirectory of this skill. + +**Agent Execution Instructions**: +1. Determine this SKILL.md file's directory path as `SKILL_DIR` +2. Script path = `${SKILL_DIR}/scripts/.ts` +3. Replace all `${SKILL_DIR}` in this document with the actual path + +**Script Reference**: +| Script | Purpose | +|--------|---------| +| `scripts/main.ts` | Main entry point | +| `scripts/other.ts` | Other functionality | +``` + +When referencing scripts in workflow sections, use `${SKILL_DIR}/scripts/.ts` so agents can resolve the correct path. ## Code Style diff --git a/skills/baoyu-comic/SKILL.md b/skills/baoyu-comic/SKILL.md index 41ea6d1..6c9a6ce 100644 --- a/skills/baoyu-comic/SKILL.md +++ b/skills/baoyu-comic/SKILL.md @@ -36,6 +36,20 @@ Style × Layout can be freely combined. | Wine, food, business, lifestyle, professional | realistic | cinematic | | Biography, balanced | classic | mixed | +## Script Directory + +**Important**: All scripts are located in the `scripts/` subdirectory of this skill. + +**Agent Execution Instructions**: +1. Determine this SKILL.md file's directory path as `SKILL_DIR` +2. Script path = `${SKILL_DIR}/scripts/.ts` +3. Replace all `${SKILL_DIR}` in this document with the actual path + +**Script Reference**: +| Script | Purpose | +|--------|---------| +| `scripts/merge-to-pdf.ts` | Merge comic pages into PDF | + ## File Structure ``` @@ -48,7 +62,8 @@ Style × Layout can be freely combined. │ ├── 00-cover.md │ └── XX-page.md ├── 00-cover.png -└── XX-page.png +├── XX-page.png +└── {topic-slug}.pdf ``` **Target directory**: @@ -99,12 +114,13 @@ For each page (cover + pages): **Image Generation Skill Selection**: - Check available image generation skills in the environment -- Adapt parameters based on skill capabilities: - - If supports `--promptfiles`: pass prompt files - - If supports reference image: pass `characters/characters.png` - - If text-only: concatenate prompts into single text - If multiple skills available, ask user preference +**Character Reference Handling**: +- If skill supports reference image: pass `characters/characters.png` as reference image +- If skill does NOT support reference image: include `characters/characters.md` content in the prompt +- This ensures character visual consistency across all pages + **Session Management**: If the image generation skill supports `--sessionId`: 1. Generate a unique session ID at the start (e.g., `comic-{topic-slug}-{timestamp}`) @@ -113,7 +129,17 @@ If the image generation skill supports `--sessionId`: 3. Report progress after each generation -### Step 5: Completion Report +### Step 5: Merge to PDF + +After all images are generated, merge them into a PDF file: + +```bash +npx -y bun ${SKILL_DIR}/scripts/merge-to-pdf.ts +``` + +This creates `{topic-slug}.pdf` in the comic directory with all pages as full-page images. + +### Step 6: Completion Report ``` Comic Complete! @@ -121,6 +147,7 @@ Title: [title] | Style: [style] | Pages: [count] Location: [path] ✓ characters.png ✓ 00-cover.png ... XX-page.png +✓ {topic-slug}.pdf ``` ## Style-Specific Guidelines diff --git a/skills/baoyu-comic/scripts/merge-to-pdf.ts b/skills/baoyu-comic/scripts/merge-to-pdf.ts new file mode 100644 index 0000000..bea5dc9 --- /dev/null +++ b/skills/baoyu-comic/scripts/merge-to-pdf.ts @@ -0,0 +1,116 @@ +import { existsSync, readdirSync, readFileSync } from "fs"; +import { join, basename } from "path"; +import { PDFDocument } from "pdf-lib"; + +interface PageInfo { + filename: string; + path: string; + index: number; + promptPath?: string; +} + +function parseArgs(): { dir: string; output?: string } { + const args = process.argv.slice(2); + let dir = ""; + let output: string | undefined; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--output" || args[i] === "-o") { + output = args[++i]; + } else if (!args[i].startsWith("-")) { + dir = args[i]; + } + } + + if (!dir) { + console.error("Usage: bun merge-to-pdf.ts [--output filename.pdf]"); + process.exit(1); + } + + return { dir, output }; +} + +function findComicPages(dir: string): PageInfo[] { + if (!existsSync(dir)) { + console.error(`Directory not found: ${dir}`); + process.exit(1); + } + + const files = readdirSync(dir); + const pagePattern = /^(\d+)-(cover|page)\.(png|jpg|jpeg)$/i; + const promptsDir = join(dir, "prompts"); + const hasPrompts = existsSync(promptsDir); + + const pages: PageInfo[] = files + .filter((f) => pagePattern.test(f)) + .map((f) => { + const match = f.match(pagePattern); + const baseName = f.replace(/\.(png|jpg|jpeg)$/i, ""); + const promptPath = hasPrompts ? join(promptsDir, `${baseName}.md`) : undefined; + + return { + filename: f, + path: join(dir, f), + index: parseInt(match![1], 10), + promptPath: promptPath && existsSync(promptPath) ? promptPath : undefined, + }; + }) + .sort((a, b) => a.index - b.index); + + if (pages.length === 0) { + console.error(`No comic pages found in: ${dir}`); + console.error("Expected format: 00-cover.png, 01-page.png, etc."); + process.exit(1); + } + + return pages; +} + +async function createPdf(pages: PageInfo[], outputPath: string) { + const pdfDoc = await PDFDocument.create(); + pdfDoc.setAuthor("baoyu-comic"); + pdfDoc.setSubject("Generated Comic"); + + for (const page of pages) { + const imageData = readFileSync(page.path); + const ext = page.filename.toLowerCase(); + const image = ext.endsWith(".png") + ? await pdfDoc.embedPng(imageData) + : await pdfDoc.embedJpg(imageData); + + const { width, height } = image; + const pdfPage = pdfDoc.addPage([width, height]); + + pdfPage.drawImage(image, { + x: 0, + y: 0, + width, + height, + }); + + console.log(`Added: ${page.filename}${page.promptPath ? " (prompt available)" : ""}`); + } + + const pdfBytes = await pdfDoc.save(); + await Bun.write(outputPath, pdfBytes); + + console.log(`\nCreated: ${outputPath}`); + console.log(`Total pages: ${pages.length}`); +} + +async function main() { + const { dir, output } = parseArgs(); + const pages = findComicPages(dir); + + const dirName = basename(dir) === "comic" ? basename(join(dir, "..")) : basename(dir); + const outputPath = output || join(dir, `${dirName}.pdf`); + + console.log(`Found ${pages.length} pages in: ${dir}\n`); + + await createPdf(pages, outputPath); +} + +main().catch((err) => { + console.error("Error:", err.message); + process.exit(1); +}); diff --git a/skills/baoyu-gemini-web/SKILL.md b/skills/baoyu-gemini-web/SKILL.md index 7410c42..ff40c97 100644 --- a/skills/baoyu-gemini-web/SKILL.md +++ b/skills/baoyu-gemini-web/SKILL.md @@ -12,6 +12,21 @@ Supports: - Multi-turn conversations within the same executor instance (`keepSession`) - Experimental video generation (`generateVideo`) — Gemini may return an async placeholder; download might require Gemini web UI +## Script Directory + +**Important**: All scripts are located in the `scripts/` subdirectory of this skill. + +**Agent Execution Instructions**: +1. Determine this SKILL.md file's directory path as `SKILL_DIR` +2. Script path = `${SKILL_DIR}/scripts/.ts` +3. Replace all `${SKILL_DIR}` in this document with the actual path + +**Script Reference**: +| Script | Purpose | +|--------|---------| +| `scripts/main.ts` | CLI entry point for text/image generation | +| `scripts/executor.ts` | Programmatic Gemini executor API | + ## Quick start ```bash diff --git a/skills/baoyu-slide-deck/SKILL.md b/skills/baoyu-slide-deck/SKILL.md index 8ece24d..9b6586d 100644 --- a/skills/baoyu-slide-deck/SKILL.md +++ b/skills/baoyu-slide-deck/SKILL.md @@ -37,6 +37,21 @@ Transform content into professional slide deck images with flexible style option /baoyu-slide-deck path/to/content.md --style storytelling --audience experts --slides 15 ``` +## Script Directory + +**Important**: All scripts are located in the `scripts/` subdirectory of this skill. + +**Agent Execution Instructions**: +1. Determine this SKILL.md file's directory path as `SKILL_DIR` +2. Script path = `${SKILL_DIR}/scripts/.ts` +3. Replace all `${SKILL_DIR}` in this document with the actual path + +**Script Reference**: +| Script | Purpose | +|--------|---------| +| `scripts/merge-to-pptx.ts` | Merge slides into PowerPoint | +| `scripts/merge-to-pdf.ts` | Merge slides into PDF | + ## Options | Option | Description | @@ -105,7 +120,9 @@ content-dir/ │ └── ... ├── 01-slide-cover.png ├── 02-slide-{slug}.png - └── ... + ├── ... + ├── {topic-slug}.pptx + └── {topic-slug}.pdf ``` ### Without Content Path @@ -121,7 +138,9 @@ slide-outputs/ │ ├── 01-slide-cover.md │ └── ... ├── 01-slide-cover.png - └── ... + ├── ... + ├── ai-future-trends.pptx + └── ai-future-trends.pdf ``` ## Workflow @@ -282,17 +301,18 @@ If the image generation skill supports `--sessionId`: 3. Report progress: "Generated X/N" 4. Continue to next -### Step 6: Merge to PPTX +### Step 6: Merge to PPTX and PDF -After all images are generated, merge them into a PowerPoint file: +After all images are generated, merge them into PowerPoint and PDF files: ```bash -npx -y bun skills/baoyu-slide-deck/scripts/merge-to-pptx.ts +npx -y bun ${SKILL_DIR}/scripts/merge-to-pptx.ts +npx -y bun ${SKILL_DIR}/scripts/merge-to-pdf.ts ``` -This creates `{topic-slug}.pptx` in the slide deck directory with: -- All images as full-bleed 16:9 slides -- Prompt content added as speaker notes (from `prompts/` directory) +This creates: +- `{topic-slug}.pptx` - PowerPoint with all images as full-bleed 16:9 slides and prompt content as speaker notes +- `{topic-slug}.pdf` - PDF with all images as full-page slides ### Step 7: Output Summary @@ -313,6 +333,7 @@ Slides: N total Outline: outline.md PPTX: {topic-slug}.pptx +PDF: {topic-slug}.pdf ``` ## Content Rules diff --git a/skills/baoyu-slide-deck/scripts/merge-to-pdf.ts b/skills/baoyu-slide-deck/scripts/merge-to-pdf.ts new file mode 100644 index 0000000..6ba342c --- /dev/null +++ b/skills/baoyu-slide-deck/scripts/merge-to-pdf.ts @@ -0,0 +1,116 @@ +import { existsSync, readdirSync, readFileSync } from "fs"; +import { join, basename } from "path"; +import { PDFDocument, rgb } from "pdf-lib"; + +interface SlideInfo { + filename: string; + path: string; + index: number; + promptPath?: string; +} + +function parseArgs(): { dir: string; output?: string } { + const args = process.argv.slice(2); + let dir = ""; + let output: string | undefined; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--output" || args[i] === "-o") { + output = args[++i]; + } else if (!args[i].startsWith("-")) { + dir = args[i]; + } + } + + if (!dir) { + console.error("Usage: bun merge-to-pdf.ts [--output filename.pdf]"); + process.exit(1); + } + + return { dir, output }; +} + +function findSlideImages(dir: string): SlideInfo[] { + if (!existsSync(dir)) { + console.error(`Directory not found: ${dir}`); + process.exit(1); + } + + const files = readdirSync(dir); + const slidePattern = /^(\d+)-slide-.*\.(png|jpg|jpeg)$/i; + const promptsDir = join(dir, "prompts"); + const hasPrompts = existsSync(promptsDir); + + const slides: SlideInfo[] = files + .filter((f) => slidePattern.test(f)) + .map((f) => { + const match = f.match(slidePattern); + const baseName = f.replace(/\.(png|jpg|jpeg)$/i, ""); + const promptPath = hasPrompts ? join(promptsDir, `${baseName}.md`) : undefined; + + return { + filename: f, + path: join(dir, f), + index: parseInt(match![1], 10), + promptPath: promptPath && existsSync(promptPath) ? promptPath : undefined, + }; + }) + .sort((a, b) => a.index - b.index); + + if (slides.length === 0) { + console.error(`No slide images found in: ${dir}`); + console.error("Expected format: 01-slide-*.png, 02-slide-*.png, etc."); + process.exit(1); + } + + return slides; +} + +async function createPdf(slides: SlideInfo[], outputPath: string) { + const pdfDoc = await PDFDocument.create(); + pdfDoc.setAuthor("baoyu-slide-deck"); + pdfDoc.setSubject("Generated Slide Deck"); + + for (const slide of slides) { + const imageData = readFileSync(slide.path); + const ext = slide.filename.toLowerCase(); + const image = ext.endsWith(".png") + ? await pdfDoc.embedPng(imageData) + : await pdfDoc.embedJpg(imageData); + + const { width, height } = image; + const page = pdfDoc.addPage([width, height]); + + page.drawImage(image, { + x: 0, + y: 0, + width, + height, + }); + + console.log(`Added: ${slide.filename}${slide.promptPath ? " (prompt available)" : ""}`); + } + + const pdfBytes = await pdfDoc.save(); + await Bun.write(outputPath, pdfBytes); + + console.log(`\nCreated: ${outputPath}`); + console.log(`Total pages: ${slides.length}`); +} + +async function main() { + const { dir, output } = parseArgs(); + const slides = findSlideImages(dir); + + const dirName = basename(dir) === "slide-deck" ? basename(join(dir, "..")) : basename(dir); + const outputPath = output || join(dir, `${dirName}.pdf`); + + console.log(`Found ${slides.length} slides in: ${dir}\n`); + + await createPdf(slides, outputPath); +} + +main().catch((err) => { + console.error("Error:", err.message); + process.exit(1); +});