Files
claude-md-to-pdf/docs/superpowers/plans/2026-05-04-md-to-pdf-skill.md
2026-05-04 20:02:15 +02:00

20 KiB

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

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):

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:

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
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
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:

{
  "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:

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
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

cd ~/Development/claude-md-to-pdf
mkdir -p .claude-plugin
  • Step 2: Write the marketplace manifest

Create .claude-plugin/marketplace.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
cd ~/Development/claude-md-to-pdf
jq empty .claude-plugin/marketplace.json && echo "ok"

Expected: ok.

  • Step 4: Verify the relative source path resolves
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
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:

---
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
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
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:

# 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
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
  1. 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

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

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:

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
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
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
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.