plan: md-to-pdf skill implementation plan

This commit is contained in:
Jean-Luc Makiola
2026-05-04 20:02:15 +02:00
parent ea49946927
commit 45eed700c0

View File

@@ -0,0 +1,602 @@
# md-to-pdf Skill Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a Claude Code plugin (publishable via Gitea) that exposes an `md-to-pdf` skill, triggered by natural language, which regenerates PDF from a Markdown file using the user's bundled style — with project-local config override.
**Architecture:** A Gitea-hosted repo in Claude Code's plugin-marketplace format. The marketplace exposes one plugin (`md-to-pdf`) containing a single skill. The skill is prompt-content that instructs Claude to (1) resolve the source `.md`, (2) walk upward for a project-local `.md-to-pdf.config.js`, falling back to the bundled `default.config.js` at `${CLAUDE_PLUGIN_ROOT}`, and (3) shell out to `npx --yes md-to-pdf`. No runtime code beyond the JS config.
**Tech Stack:** Claude Code plugin/marketplace format (`marketplace.json`, `plugin.json`), `md-to-pdf` (npm), Node.js, Gitea for hosting.
---
## File Structure
Working directory: `~/Development/claude-md-to-pdf` (already initialized as a git repo with the spec committed).
Files to create:
| Path | Responsibility |
|------|----------------|
| `.claude-plugin/marketplace.json` | Marketplace manifest. Declares the marketplace and lists the `md-to-pdf` plugin pointing to `./plugins/md-to-pdf`. |
| `plugins/md-to-pdf/.claude-plugin/plugin.json` | Plugin manifest. Name, description, author, version. |
| `plugins/md-to-pdf/skills/md-to-pdf/SKILL.md` | The skill itself: frontmatter (name, description that triggers on natural language), then step-by-step instructions for Claude. |
| `plugins/md-to-pdf/skills/md-to-pdf/default.config.js` | Verbatim copy of the user's existing `.md-to-pdf.config.js` (A4 / 5pt fixed-layout tables / system sans-serif). |
| `README.md` | Install instructions (`/plugin marketplace add …`), usage examples, project-local override docs. |
| `LICENSE` | MIT. |
Already in place: `docs/superpowers/specs/2026-05-04-md-to-pdf-skill-design.md` (committed at `ea49946`).
---
## Task 1: Bundled default config
**Files:**
- Create: `plugins/md-to-pdf/skills/md-to-pdf/default.config.js`
- [ ] **Step 1: Create the directory structure**
```bash
cd ~/Development/claude-md-to-pdf
mkdir -p plugins/md-to-pdf/.claude-plugin plugins/md-to-pdf/skills/md-to-pdf
```
- [ ] **Step 2: Write the bundled default config**
Create `plugins/md-to-pdf/skills/md-to-pdf/default.config.js` with this exact content (verbatim copy of the user's working config from the Databases project):
```js
module.exports = {
pdf_options: {
format: 'A4',
margin: '15mm 12mm',
printBackground: true,
},
css: `
@page { size: A4; margin: 15mm 12mm; }
html, body { width: 186mm; max-width: 186mm; margin: 0; padding: 0; }
body { font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 11pt; }
h1 { font-size: 18pt; }
h2 { font-size: 14pt; margin-top: 1.2em; }
h3 { font-size: 12pt; }
h4 { font-size: 11pt; }
table {
border-collapse: collapse;
font-size: 5pt;
margin: 0.6em 0;
table-layout: fixed;
width: 100%;
}
th, td {
border: 1px solid #888;
padding: 1px 3px;
white-space: nowrap;
vertical-align: top;
letter-spacing: -0.1px;
overflow: hidden;
}
code { font-size: 0.9em; background: #f4f4f4; padding: 0 2px; border-radius: 2px; }
td code, th code { font-size: 0.85em; padding: 0 1px; }
th { background: #f0f0f0; }
blockquote { border-left: 3px solid #ccc; margin: 0.5em 0; padding: 0.2em 0.8em; color: #444; }
`,
};
```
- [ ] **Step 3: Verify the config loads as valid CommonJS**
Run:
```bash
cd ~/Development/claude-md-to-pdf
node -e "const c = require('./plugins/md-to-pdf/skills/md-to-pdf/default.config.js'); if (!c.pdf_options || !c.css) throw new Error('config missing required fields'); console.log('ok')"
```
Expected output:
```
ok
```
If you see `SyntaxError` or "config missing required fields" — re-check the file content matches Step 2 exactly.
- [ ] **Step 4: Smoke-test md-to-pdf with this config on a temporary sample**
```bash
cd /tmp
printf '# Test\n\n| A | B |\n|---|---|\n| 1 | 2 |\n' > md2pdf-smoke.md
npx --yes md-to-pdf md2pdf-smoke.md --config-file ~/Development/claude-md-to-pdf/plugins/md-to-pdf/skills/md-to-pdf/default.config.js
ls -la md2pdf-smoke.pdf
rm md2pdf-smoke.md md2pdf-smoke.pdf
```
Expected: `md2pdf-smoke.pdf` is created with non-zero size before deletion.
- [ ] **Step 5: Commit**
```bash
cd ~/Development/claude-md-to-pdf
git add plugins/md-to-pdf/skills/md-to-pdf/default.config.js
git commit -m "feat: bundled default md-to-pdf config (homework style)"
```
---
## Task 2: Plugin manifest
**Files:**
- Create: `plugins/md-to-pdf/.claude-plugin/plugin.json`
- [ ] **Step 1: Write the plugin manifest**
Create `plugins/md-to-pdf/.claude-plugin/plugin.json`:
```json
{
"name": "md-to-pdf",
"description": "Regenerate a PDF from a Markdown file on natural-language request (e.g. 'exportier mal das PDF', 'regenerate the PDF'). Uses project-local .md-to-pdf.config.js when present, otherwise a bundled default styled for A4 with small tables.",
"version": "0.1.0",
"author": {
"name": "Jean-Luc Makiola",
"email": "mail@jeanlucmakiola.de"
}
}
```
- [ ] **Step 2: Validate JSON**
Run:
```bash
cd ~/Development/claude-md-to-pdf
jq empty plugins/md-to-pdf/.claude-plugin/plugin.json && echo "ok"
```
Expected: `ok`. If `jq` is missing, install it (`pacman -S jq` on Arch) before proceeding — the plan uses it for validation in later tasks too.
- [ ] **Step 3: Commit**
```bash
cd ~/Development/claude-md-to-pdf
git add plugins/md-to-pdf/.claude-plugin/plugin.json
git commit -m "feat: plugin manifest for md-to-pdf"
```
---
## Task 3: Marketplace manifest
**Files:**
- Create: `.claude-plugin/marketplace.json`
- [ ] **Step 1: Create the marketplace directory**
```bash
cd ~/Development/claude-md-to-pdf
mkdir -p .claude-plugin
```
- [ ] **Step 2: Write the marketplace manifest**
Create `.claude-plugin/marketplace.json`:
```json
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "claude-md-to-pdf",
"description": "Self-hosted marketplace for the md-to-pdf skill — natural-language Markdown to PDF conversion.",
"owner": {
"name": "Jean-Luc Makiola",
"email": "mail@jeanlucmakiola.de"
},
"plugins": [
{
"name": "md-to-pdf",
"description": "Regenerate a PDF from a Markdown file on natural-language request. Uses project-local .md-to-pdf.config.js when present, otherwise a bundled default.",
"category": "productivity",
"source": "./plugins/md-to-pdf"
}
]
}
```
- [ ] **Step 3: Validate JSON**
```bash
cd ~/Development/claude-md-to-pdf
jq empty .claude-plugin/marketplace.json && echo "ok"
```
Expected: `ok`.
- [ ] **Step 4: Verify the relative source path resolves**
```bash
cd ~/Development/claude-md-to-pdf
test -f plugins/md-to-pdf/.claude-plugin/plugin.json && echo "plugin source ok"
```
Expected: `plugin source ok`.
- [ ] **Step 5: Commit**
```bash
cd ~/Development/claude-md-to-pdf
git add .claude-plugin/marketplace.json
git commit -m "feat: marketplace manifest"
```
---
## Task 4: SKILL.md (the skill itself)
**Files:**
- Create: `plugins/md-to-pdf/skills/md-to-pdf/SKILL.md`
- [ ] **Step 1: Write the skill**
Create `plugins/md-to-pdf/skills/md-to-pdf/SKILL.md`:
````markdown
---
name: md-to-pdf
description: Use when the user asks to export, regenerate, build, or "make" a PDF from a Markdown file. Triggers on phrases like "exportier mal das PDF", "mach das als PDF", "regenerate the PDF", "PDF neu generieren", "convert this markdown to pdf". Do NOT trigger on requests to read, open, or display an existing PDF.
version: 0.1.0
---
# md-to-pdf
Regenerate a PDF from a Markdown file using the user's preferred styling. The skill walks upward for a project-local `.md-to-pdf.config.js`; if none is found it falls back to the bundled `default.config.js` shipped with this skill.
## When This Skill Applies
Trigger when the user wants a Markdown file converted to PDF — typical phrasings: "exportier mal", "mach mir das als PDF", "PDF regenerieren", "regenerate the PDF", "convert to pdf". Do **not** trigger when the user just wants to view, read, or open an existing PDF.
## Procedure
### 1. Identify the source `.md`
Resolve in this order — stop at the first that yields a single unambiguous file:
1. A `.md` path explicitly named in the user's most recent message.
2. The `.md` file the assistant most recently read or wrote in this session.
3. The single most-recently-modified `.md` file in the current working directory (`ls -t *.md | head -1` semantics).
If none of these resolves to one file, **ask** the user which file to convert. Do not guess.
### 2. Resolve the config
Walk from the source file's directory upward toward `/`, looking for `.md-to-pdf.config.js`. Use the first match.
If no project-local config is found, fall back to the bundled default:
```
${CLAUDE_PLUGIN_ROOT}/skills/md-to-pdf/default.config.js
```
`CLAUDE_PLUGIN_ROOT` is provided by Claude Code at runtime and points to this plugin's root directory.
### 3. Run the converter
```bash
npx --yes md-to-pdf <source.md> --config-file <resolved-config>
```
The PDF is written next to the source (`Loesung.md` → `Loesung.pdf`), overwriting any existing file.
### 4. Confirm
Tell the user:
- The output PDF path and file size (`ls -la` style).
- Which config was used: project-local path, or "bundled default".
## Error Handling
- **`npx` / Node not on PATH** → tell the user Node.js is required and stop. Don't try to install it.
- **`md-to-pdf` fetch or render fails** → relay the npx output verbatim. Don't retry silently.
- **Source `.md` ambiguous** → ask, don't guess.
- **Config file is syntactically broken** → relay the error from md-to-pdf. Don't try to "fix" the user's config.
## Examples
### Project-local config
User: *"exportier mal das PDF"* (after editing `Uebungen/03/Loesung.md`)
Skill:
1. Source: `Uebungen/03/Loesung.md` (most recently edited).
2. Walks up; finds `.md-to-pdf.config.js` at the repo root.
3. Runs `npx --yes md-to-pdf Uebungen/03/Loesung.md --config-file .md-to-pdf.config.js`.
4. Reports: `Uebungen/03/Loesung.pdf (280K) — using project config .md-to-pdf.config.js`.
### Bundled default
User: *"convert this markdown to pdf"* (in a fresh directory with one `.md` file and no config).
Skill:
1. Source: `notes.md` (only `.md` in CWD).
2. Walks up; no `.md-to-pdf.config.js` found.
3. Runs `npx --yes md-to-pdf notes.md --config-file ${CLAUDE_PLUGIN_ROOT}/skills/md-to-pdf/default.config.js`.
4. Reports: `notes.pdf (62K) — using bundled default`.
````
- [ ] **Step 2: Verify the SKILL.md frontmatter parses**
```bash
cd ~/Development/claude-md-to-pdf
head -5 plugins/md-to-pdf/skills/md-to-pdf/SKILL.md | grep -E '^(name|description|version):' | wc -l
```
Expected output: `3` (three frontmatter keys present).
- [ ] **Step 3: Commit**
```bash
cd ~/Development/claude-md-to-pdf
git add plugins/md-to-pdf/skills/md-to-pdf/SKILL.md
git commit -m "feat: SKILL.md with natural-language trigger and procedure"
```
---
## Task 5: README and LICENSE
**Files:**
- Create: `README.md`
- Create: `LICENSE`
- [ ] **Step 1: Write the README**
Create `README.md`:
````markdown
# claude-md-to-pdf
A Claude Code plugin marketplace exposing the **md-to-pdf** skill — natural-language Markdown→PDF conversion using [`md-to-pdf`](https://www.npmjs.com/package/md-to-pdf).
## What it does
When you say things like *"exportier mal das PDF"*, *"regenerate the PDF"*, or *"convert this markdown to pdf"*, the skill picks up the relevant `.md`, finds the right style config, and runs `npx md-to-pdf`. No slash command required.
## Install
In Claude Code, on each device:
```
/plugin marketplace add https://gitea.<your-domain>/jlmak/claude-md-to-pdf
/plugin install md-to-pdf@claude-md-to-pdf
```
(Replace `gitea.<your-domain>` with your Gitea host. The repo can also be installed from a local clone — see "Local install" below.)
## Requirements
- Node.js with `npx` on `PATH` (the skill calls `npx --yes md-to-pdf`).
## How style is chosen
The skill walks upward from the source `.md` file looking for a `.md-to-pdf.config.js`. If found, it's used. Otherwise the bundled default — A4, 15/12mm margins, 5pt fixed-layout tables, system sans-serif at 11pt — is used. The bundled default lives at:
```
plugins/md-to-pdf/skills/md-to-pdf/default.config.js
```
## Local install (no Gitea remote yet)
```
/plugin marketplace add ~/Development/claude-md-to-pdf
/plugin install md-to-pdf@claude-md-to-pdf
```
## License
MIT — see `LICENSE`.
````
- [ ] **Step 2: Write the LICENSE**
Create `LICENSE` with the standard MIT text:
```
MIT License
Copyright (c) 2026 Jean-Luc Makiola
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
- [ ] **Step 3: Commit**
```bash
cd ~/Development/claude-md-to-pdf
git add README.md LICENSE
git commit -m "docs: README + MIT LICENSE"
```
---
## Task 6: Local install + end-to-end smoke test
**Goal:** Install the plugin from the local repo path, then verify Claude actually triggers the skill on natural language and produces a PDF.
This task requires the human to drive a Claude Code session. Document the exact sequence so it's repeatable.
- [ ] **Step 1: Add the local marketplace**
In a Claude Code session (any working directory):
```
/plugin marketplace add ~/Development/claude-md-to-pdf
```
Expected: Claude Code reports the marketplace was added with one plugin available.
- [ ] **Step 2: Install the plugin**
```
/plugin install md-to-pdf@claude-md-to-pdf
```
Expected: Plugin installs without error. The skill `md-to-pdf` is now available.
- [ ] **Step 3: Test trigger and project-local config path**
In a Claude Code session with cwd `~/Nextcloud/Documents/THB/2.Semester/Databases`:
1. Touch a file to create a recency signal:
```
touch Uebungen/03/Loesung.md
```
2. Tell Claude: *"exportier mal das PDF"*
Expected:
- Claude invokes the `md-to-pdf` skill (visible in tool calls).
- Claude resolves the source as `Uebungen/03/Loesung.md`.
- Claude finds `.md-to-pdf.config.js` at the project root and uses it (not the bundled default).
- `Uebungen/03/Loesung.pdf` is regenerated (check `mtime`).
- Claude's confirmation mentions the project-local config.
- [ ] **Step 4: Test bundled-default fallback**
```bash
mkdir -p /tmp/md2pdf-bundled-test
cd /tmp/md2pdf-bundled-test
printf '# Hello\n\nWorld.\n' > notes.md
```
Open a Claude Code session in `/tmp/md2pdf-bundled-test` and say: *"convert this markdown to pdf"*
Expected:
- Skill triggers.
- No project-local config is found; bundled default is used.
- `notes.pdf` exists.
- Claude's confirmation mentions "bundled default".
- [ ] **Step 5: Cleanup**
```bash
rm -rf /tmp/md2pdf-bundled-test
```
- [ ] **Step 6: If anything failed, fix and re-commit**
If the skill didn't trigger, the description in `SKILL.md` likely needs more discriminative phrasing — edit it, reinstall the plugin (`/plugin uninstall md-to-pdf` then `/plugin install …`), and retry. Commit the fix:
```bash
cd ~/Development/claude-md-to-pdf
git add plugins/md-to-pdf/skills/md-to-pdf/SKILL.md
git commit -m "fix: tighten SKILL.md trigger description"
```
If file resolution misbehaved (e.g. picked the wrong `.md`), refine the resolution rules in the "Identify the source `.md`" section. Same commit cycle.
---
## Task 7: Push to Gitea
**Goal:** Make the marketplace installable from the user's Gitea host, so other devices can use the same `/plugin marketplace add` URL.
This task requires the user to have a Gitea instance reachable and an API token or SSH key configured. The plan documents the exact commands; the human runs them.
- [ ] **Step 1: Create the empty repo on Gitea**
In the Gitea web UI (or via the Gitea CLI if installed): create a new public repo named `claude-md-to-pdf` under the user's account. Do **not** initialize it with a README — the local repo already has commits.
- [ ] **Step 2: Add the remote**
```bash
cd ~/Development/claude-md-to-pdf
git remote add origin https://gitea.<your-domain>/jlmak/claude-md-to-pdf.git
git remote -v
```
Expected: `origin` lists the Gitea URL for both `(fetch)` and `(push)`.
- [ ] **Step 3: Push**
```bash
cd ~/Development/claude-md-to-pdf
git push -u origin main
```
Expected: All commits land on Gitea. The web UI shows the spec, plan, manifests, skill, README, and LICENSE.
- [ ] **Step 4: Verify install from Gitea URL**
In a fresh Claude Code session (or after removing the local marketplace):
```
/plugin marketplace remove claude-md-to-pdf
/plugin marketplace add https://gitea.<your-domain>/jlmak/claude-md-to-pdf
/plugin install md-to-pdf@claude-md-to-pdf
```
Then re-run the smoke test from Task 6, Step 3 in the Databases project. Expected behavior identical to the local-install run.
- [ ] **Step 5: Tag a release**
```bash
cd ~/Development/claude-md-to-pdf
git tag -a v0.1.0 -m "Initial release: md-to-pdf skill"
git push origin v0.1.0
```
Expected: Tag visible on Gitea.
---
## Self-Review
(To be performed by the implementing agent before declaring the plan done — kept here so the engineer can re-check.)
**1. Spec coverage:**
| Spec section | Implemented in |
|---|---|
| Distribution via Gitea marketplace | Tasks 3, 7 |
| Repo layout exactly as in spec | Tasks 1, 2, 3, 4 (file structure) |
| Bundled default config (homework style) | Task 1 |
| Natural-language trigger (DE+EN) | Task 4 (SKILL.md description) |
| Source `.md` resolution rules | Task 4 (Procedure §1) |
| Config resolution: project-local then bundled default | Task 4 (Procedure §2) |
| `npx md-to-pdf` invocation, output next to source | Task 4 (Procedure §3) |
| Confirmation with path/size/which config | Task 4 (Procedure §4) |
| Error handling (npx missing, fetch fails, ambiguous source, broken config) | Task 4 (Error Handling section) |
| Manual smoke test in Databases project | Task 6 Step 3 |
| Smoke test for bundled-default fallback | Task 6 Step 4 |
| Smoke test for ambiguous source | *Not in plan; out of v1 scope per spec — verifies "ask, don't guess" only via real-world use.* |
| README with install instructions | Task 5 |
The "ambiguous source" smoke test from the spec's Testing Strategy is left to ad-hoc verification rather than a scripted task — it depends on Claude's judgment, not a deterministic command. Acceptable for v1.
**2. Placeholder scan:** None present. All steps contain concrete commands or full file content.
**3. Type/name consistency:**
- Plugin name: `md-to-pdf` everywhere (manifest, marketplace entry, install command, README, frontmatter).
- Marketplace name: `claude-md-to-pdf` everywhere.
- Bundled config path: `plugins/md-to-pdf/skills/md-to-pdf/default.config.js` and `${CLAUDE_PLUGIN_ROOT}/skills/md-to-pdf/default.config.js` — consistent (the latter is just the runtime form of the former).
- Author identity (`Jean-Luc Makiola` / `mail@jeanlucmakiola.de`) consistent across plugin.json, marketplace.json, LICENSE.
No drift detected.