merge: seed-mmpdf (US2)

This commit is contained in:
m2 (AI Agent) 2026-07-02 03:50:25 +02:00
commit 5e2dd7aac3
8 changed files with 608 additions and 0 deletions

View file

@ -0,0 +1,26 @@
{
"schema_version": "m2.listing.v1",
"listing_id": "lst_mm-pdf-report",
"solution_id": "sol_mm-pdf-report",
"solution_version": "1.0.0",
"inventory_type": "solution",
"name": "MM PDF Report Generator",
"summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown.",
"category": "document-generation",
"keywords": ["pdf", "branding", "markdown", "reports", "decks"],
"price": {
"amount": 40,
"currency": "m2cr",
"model": "fixed"
},
"seller": "sdjs-operator",
"evidence_summary": "Skill in production use on the m2 host since 2026-04-21; two real generated PDF/HTML pairs on disk (session-2026-04-20.pdf, spark-cluster-old/fabric-setup.pdf) confirm the pipeline works end-to-end.",
"tenant_visibility": ["*"],
"stats": {
"installs": 0,
"proposals_shown": 0,
"proposals_accepted": 0
},
"status": "draft",
"install_ref": "lst_mm-pdf-report-v1.0.0"
}

View file

@ -0,0 +1,89 @@
---
name: mm-pdf
description: Generate beautifully styled Machine.Machine branded PDFs and slide decks from Markdown. Dark navy aesthetic, cyan accents, professional A4 layout. Uses a headless-Chrome render container (any container with Node + Chrome, e.g. a "*-m2o" agent-desktop image) or a local WeasyPrint fallback. Produces PDF + HTML output.
---
# mm-pdf
Generate MM-branded documents from Markdown content.
## Design system
| Token | Value | Use |
|-------|-------|-----|
| Cover background | `#0a0e1a` | Dark navy |
| Primary accent | `#4bb8d4` | Cyan — headings, kickers, borders |
| Section banners | `#3d4550` | Slate grey |
| Purple sidebar | `#6b46c1` | Alt cover variant |
| Body text | `#e2e8f0` | Light grey on dark bg |
| Body background | `#111827` | Dark page bg |
| Code/data blocks | `#1e2533` | Slightly lighter navy |
## Usage
```bash
# Generate PDF from markdown
mm-pdf generate input.md [--out output.pdf] [--style dark|purple|light]
# Generate slide deck (Marp)
mm-pdf slides input.md [--out output.pdf]
# With explicit container (auto-discovery picks the first "*m2o*" container if omitted)
mm-pdf generate input.md --container <your-render-container-name>
```
## Pipeline
1. **MD → HTML** — a small Node script (via `marked`) renders Markdown into full MM-styled
HTML. Extracts YAML frontmatter (`title`, `subtitle`, `date`, `client`) for the cover
page. Strips the first `# H1` to use as title if no frontmatter is present.
2. **HTML → PDF** — Chrome headless renders the HTML to PDF (`--print-to-pdf`).
Both outputs are saved alongside the input file.
## Output
- `<name>.html` — standalone styled HTML (preview, shareable)
- `<name>.pdf` — print-ready PDF
## Styles
| Style | Cover | Use case |
|-------|-------|----------|
| `dark` | Navy `#0a0e1a` + cyan accent | Technical specs, deployment docs |
| `purple` | Purple sidebar `#6b46c1` | Partner decks |
| `light` | White + navy header | Client-facing docs |
`dark.css` and `purple.css` ship in `templates/`. `light` is documented for forward
compatibility but has no template in this bundle yet — add `templates/light.css` locally
if you need it (it will be picked up automatically; `generate.sh` falls back to `dark.css`
for any missing style file).
## Components (in Markdown)
```markdown
<!-- KPI card row -->
<div class="kpi-row">
<div class="kpi">**€5K** setup</div>
<div class="kpi">**€2K**/mo</div>
<div class="kpi">**8 weeks** pilot</div>
</div>
<!-- Section kicker -->
<span class="kicker">DEPLOYMENT ARCHITECTURE</span>
<!-- Warning/callout -->
<div class="callout">⚠️ Open item: integration path TBD</div>
```
## Requirements
- Docker, with a running container that has Node.js + Google Chrome (headless) installed
and reachable via `docker exec`/`docker cp` from the host running this skill. Any
container name is fine — `scripts/generate.sh` auto-discovers the first container whose
name matches `nasr-m2o|m2o`, or accepts `--container <name>` to target any other
container that meets the Node+Chrome requirement.
- `docker` CLI available on the host invoking `mm-pdf`.
No absolute host paths, hostnames, or client-identifying data are baked into this skill —
`--container` and the input Markdown path are the only machine-specific inputs at run time.

View file

@ -0,0 +1,145 @@
#!/usr/bin/env bash
# mm-pdf generate — render Markdown → HTML → PDF using nasr-m2o container
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STYLE="${MM_PDF_STYLE:-dark}"
OUT=""
OUT_HTML=""
CONTAINER=""
INPUT=""
usage() {
echo "Usage: mm-pdf generate <input.md> [--out output.pdf] [--style dark|purple|light] [--container NAME]"
exit 1
}
while [[ $# -gt 0 ]]; do
case $1 in
--out) OUT="$2"; shift 2 ;;
--style) STYLE="$2"; shift 2 ;;
--container) CONTAINER="$2"; shift 2 ;;
--help|-h) usage ;;
*) INPUT="$1"; shift ;;
esac
done
[[ -z "$INPUT" ]] && usage
[[ ! -f "$INPUT" ]] && { echo "Error: $INPUT not found"; exit 1; }
INPUT_ABS="$(realpath "$INPUT")"
BASENAME="${INPUT_ABS%.md}"
OUT="${OUT:-${BASENAME}.pdf}"
OUT_ABS="$(realpath -m "$OUT")"
OUT_HTML_ABS="${OUT_ABS%.pdf}.html"
# Auto-discover container
if [[ -z "$CONTAINER" ]]; then
CONTAINER=$(docker ps --format "{{.Names}}" | grep -E "nasr-m2o|m2o" | head -1)
[[ -z "$CONTAINER" ]] && { echo "Error: no m2o container running. Start nasr-m2o or pass --container"; exit 1; }
fi
echo "Using container: $CONTAINER"
# Copy files into container
TMP="/tmp/mm-pdf-$$"
docker exec "$CONTAINER" mkdir -p "$TMP"
docker cp "$INPUT_ABS" "$CONTAINER:$TMP/input.md"
if [[ -f "$SKILL_DIR/templates/${STYLE}.css" ]]; then
docker cp "$SKILL_DIR/templates/${STYLE}.css" "$CONTAINER:$TMP/style.css"
else
docker cp "$SKILL_DIR/templates/dark.css" "$CONTAINER:$TMP/style.css"
fi
# Step 1: Markdown → styled HTML (via Node/marked)
echo "Step 1: Converting Markdown → HTML..."
docker exec "$CONTAINER" bash -c "
cd $TMP
# Install marked locally in tmp dir so require() can find it
[ -d node_modules/marked ] || npm install marked --prefix . -q 2>/dev/null
node - <<'JSEOF'
const fs = require('fs');
const path = require('path');
const { marked } = require('marked');
const mdRaw = fs.readFileSync('input.md', 'utf8');
const css = fs.readFileSync('style.css', 'utf8');
// Strip YAML frontmatter
let body = mdRaw;
const fm = {};
const fmMatch = mdRaw.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
if (fmMatch) {
fmMatch[1].split('\n').forEach(line => {
const i = line.indexOf(':');
if (i > 0) fm[line.slice(0,i).trim().toLowerCase()] = line.slice(i+1).trim();
});
body = mdRaw.slice(fmMatch[0].length);
}
// Extract first H1 as title
let title = fm.title || '';
if (!title) {
const h1 = body.match(/^#\s+(.+)$/m);
if (h1) { title = h1[1].trim(); body = body.replace(h1[0], ''); }
}
title = title || 'Machine.Machine';
const subtitle = fm.subtitle || fm.description || '';
const dateLine = [fm.client, fm.date].filter(Boolean).join(' \u00a0·\u00a0 ');
marked.setOptions({ gfm: true, breaks: false });
const contentHtml = marked.parse(body);
const coverHtml = \`<div class=\"cover\">
<div class=\"logo\">MACHINE.MACHINE</div>
<div>
<h1 class=\"title\">\${title}</h1>
\${subtitle ? '<div class=\"subtitle\">' + subtitle + '</div>' : ''}
</div>
\${dateLine ? '<div class=\"meta\">' + dateLine + '</div>' : ''}
</div>\`;
const html = \`<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<title>\${title}</title>
<style>\${css}</style>
</head>
<body>
\${coverHtml}
<div class=\"body-content\">
\${contentHtml}
</div>
</body>
</html>\`;
fs.writeFileSync('output.html', html);
console.log('HTML written:', html.length, 'bytes');
JSEOF
"
echo "Step 2: Rendering HTML → PDF..."
docker exec "$CONTAINER" bash -c "
cd $TMP
google-chrome \
--headless \
--no-sandbox \
--disable-gpu \
--disable-web-security \
--print-to-pdf=output.pdf \
--print-to-pdf-no-header \
--run-all-compositor-stages-before-draw \
--virtual-time-budget=5000 \
'file://$TMP/output.html'
"
# Copy both outputs back
docker cp "$CONTAINER:$TMP/output.html" "$OUT_HTML_ABS"
docker cp "$CONTAINER:$TMP/output.pdf" "$OUT_ABS"
docker exec "$CONTAINER" rm -rf "$TMP"
echo ""
echo "HTML: $OUT_HTML_ABS ($(du -h "$OUT_HTML_ABS" | cut -f1))"
echo "PDF: $OUT_ABS ($(du -h "$OUT_ABS" | cut -f1))"

View file

@ -0,0 +1,236 @@
/* MM Dark Design System — WeasyPrint / Marp compatible */
:root {
--cover-bg: #0a0e1a;
--accent: #4bb8d4;
--section-bg: #3d4550;
--body-bg: #111827;
--body-text: #e2e8f0;
--muted: #94a3b8;
--code-bg: #1e2533;
--border: #2d3748;
--white: #ffffff;
}
/* --- PAGE --- */
@page {
size: A4;
margin: 15mm 18mm 15mm 18mm;
}
body {
font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
font-size: 10pt;
line-height: 1.6;
color: var(--body-text);
background: var(--body-bg);
margin: 0;
padding: 0;
}
.body-content {
padding: 0;
}
/* --- COVER PAGE --- */
.cover {
background: var(--cover-bg);
min-height: 100vh;
padding: 52px 52px 40px 52px;
display: flex;
flex-direction: column;
justify-content: space-between;
page-break-after: always;
}
.cover .logo {
font-size: 11pt;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--accent);
}
.cover .title {
font-size: 28pt;
font-weight: 800;
color: var(--white);
line-height: 1.15;
margin: 0;
}
.cover .subtitle {
font-size: 13pt;
color: var(--accent);
margin-top: 8px;
font-weight: 400;
}
.cover .meta {
font-size: 9pt;
color: var(--muted);
margin-top: 32px;
}
.cover .version-badge {
display: inline-block;
background: var(--accent);
color: var(--cover-bg);
font-size: 8pt;
font-weight: 700;
padding: 3px 10px;
border-radius: 3px;
letter-spacing: 0.08em;
}
/* --- SECTION BANNERS --- */
.section-banner, h2 {
background: var(--section-bg);
color: var(--white);
padding: 8px 16px;
margin: 24px -18mm 16px -18mm;
font-size: 10pt;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
/* --- KICKER --- */
.kicker {
display: block;
font-size: 8pt;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 4px;
}
/* --- HEADINGS --- */
h1 {
font-size: 20pt;
font-weight: 800;
color: var(--white);
margin: 0 0 8px 0;
}
h3 {
font-size: 11pt;
font-weight: 700;
color: var(--accent);
margin: 16px 0 6px 0;
}
h4 {
font-size: 10pt;
font-weight: 600;
color: var(--white);
margin: 12px 0 4px 0;
}
/* --- KPI CARDS --- */
.kpi-row {
display: flex;
gap: 12px;
margin: 16px 0;
}
.kpi {
flex: 1;
background: var(--code-bg);
border: 1px solid var(--border);
border-top: 3px solid var(--accent);
padding: 12px 14px;
border-radius: 4px;
font-size: 9pt;
color: var(--muted);
}
.kpi strong {
display: block;
font-size: 16pt;
font-weight: 800;
color: var(--white);
margin-bottom: 2px;
}
/* --- TABLES --- */
table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 9pt;
}
th {
background: var(--section-bg);
color: var(--white);
padding: 7px 10px;
text-align: left;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
font-size: 8pt;
}
td {
padding: 6px 10px;
border-bottom: 1px solid var(--border);
color: var(--body-text);
}
tr:nth-child(even) td {
background: rgba(255,255,255,0.02);
}
/* --- CODE BLOCKS --- */
pre, code {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 3px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 8.5pt;
color: #a5d6a7;
}
pre {
padding: 12px 14px;
overflow-x: auto;
}
code { padding: 1px 5px; }
/* --- CALLOUTS --- */
.callout {
background: rgba(75, 184, 212, 0.08);
border-left: 3px solid var(--accent);
padding: 10px 14px;
margin: 12px 0;
border-radius: 0 4px 4px 0;
font-size: 9pt;
color: var(--body-text);
}
.callout.warn {
border-left-color: #f6a623;
background: rgba(246, 166, 35, 0.06);
}
.callout.danger {
border-left-color: #e53e3e;
background: rgba(229, 62, 62, 0.06);
}
/* --- FOOTER --- */
@page {
@bottom-center {
content: "Machine.Machine — Confidential";
font-size: 7.5pt;
color: #4a5568;
}
@bottom-right {
content: counter(page);
font-size: 7.5pt;
color: #4a5568;
}
}

View file

@ -0,0 +1,32 @@
/* MM Purple Sidebar — Nasr reference style */
@import url("dark.css");
:root {
--cover-bg: #1a0533;
--accent: #9f7aea;
--sidebar-bg: #6b46c1;
}
.cover {
background: var(--cover-bg);
padding-left: 0;
display: grid;
grid-template-columns: 6px 1fr;
}
.cover::before {
content: '';
background: var(--sidebar-bg);
grid-column: 1;
}
.cover-content {
grid-column: 2;
padding: 52px 52px 40px 36px;
}
.cover .logo { color: var(--accent); }
.cover .subtitle { color: var(--accent); }
h3 { color: var(--accent); }
.kpi { border-top-color: var(--accent); }
.callout { border-left-color: var(--accent); }

View file

@ -0,0 +1,17 @@
# mm-pdf-report install recipe (apply-adapter.md bundle layout, local adapter v1)
# Places the mm-pdf skill into the buyer's Claude Code skills directory.
targets:
- src: SKILL.md
dest: ~/.claude/skills/mm-pdf/SKILL.md
- src: scripts/generate.sh
dest: ~/.claude/skills/mm-pdf/scripts/generate.sh
- src: templates/dark.css
dest: ~/.claude/skills/mm-pdf/templates/dark.css
- src: templates/purple.css
dest: ~/.claude/skills/mm-pdf/templates/purple.css
checks:
- cmd: test -f ~/.claude/skills/mm-pdf/SKILL.md
expect_exit: 0
- cmd: test -x ~/.claude/skills/mm-pdf/scripts/generate.sh
expect_exit: 0
entrypoint: "Ask your agent to use the mm-pdf skill, or run: ~/.claude/skills/mm-pdf/scripts/generate.sh <input.md> --style dark"

View file

@ -0,0 +1,63 @@
{
"schema_version": "m2.solution.v1",
"solution_id": "sol_mm-pdf-report",
"name": "MM PDF Report Generator",
"summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown.",
"description": "Installs the mm-pdf skill: a Markdown-to-branded-PDF/HTML pipeline (dark navy aesthetic, cyan accents, A4 layout) built on a Node+marked HTML render step and headless-Chrome print-to-pdf inside a Docker container. Ships three artifacts: SKILL.md, scripts/generate.sh, and templates/{dark,purple}.css. content_hash is computed over payload/ + recipe.yaml only (not solution.json itself, to avoid the self-referential hash problem of hashing a file that contains its own hash): sha256 of `tar --sort=name --mtime='@0' --owner=0 --group=0 --numeric-owner -czf - payload recipe.yaml` run from this bundle's root.",
"intent": "Give an operator a working branded-document pipeline (proposal PDFs, spec sheets, decks) without building one from scratch.",
"behavior": {
"skill_ref": "payload/SKILL.md"
},
"tools": [
{ "name": "docker", "kind": "cli", "required": true }
],
"runtime": {
"surfaces": ["claude-code-skill"]
},
"permissions": [
{ "action": "write", "target": "~/.claude/skills/mm-pdf/" },
{ "action": "exec", "target": "docker exec/docker cp against a caller-selected container" }
],
"deployment": {
"recipe_ref": "recipe.yaml",
"entrypoint": "Ask your agent to use the mm-pdf skill, or run: ~/.claude/skills/mm-pdf/scripts/generate.sh <input.md> --style dark",
"verify_command": "test -f ~/.claude/skills/mm-pdf/SKILL.md"
},
"applicability": {
"image_classes": ["primus", "agent-latest"],
"roles": ["operator", "desktop-agent"],
"tool_requirements": ["docker"]
},
"tenant_scope": "m2-core",
"evidence": [
{
"source": "/home/m2/.claude/skills/mm-pdf/ (SKILL.md, scripts/generate.sh, templates/dark.css, templates/purple.css) — the proven skill this bundle packages, present on the m2 host since 2026-04-21 (SKILL.md mtime 2026-04-21T00:05:01+02:00, templates/dark.css mtime 2026-04-21T00:04, scripts/generate.sh mtime 2026-04-21T00:09)",
"machine": "m2",
"excerpt": "SKILL.md frontmatter: 'Generate beautifully styled Machine.Machine branded PDFs and slide decks from Markdown. Dark navy aesthetic, cyan accents, professional A4 layout.'"
},
{
"source": "/home/m2/session-2026-04-20.pdf — real output of this pipeline: 4-page A4 PDF, PDF metadata Title 'output.html' (the pipeline's fixed intermediate filename), Creator 'HeadlessChrome/145.0.0.0' (Chrome print-to-pdf, matches scripts/generate.sh Step 2), CreationDate 2026-04-20T22:56:51+02:00; pdftotext shows the 'MACHINE.MACHINE' cover logo and MM design-system markup",
"machine": "m2",
"excerpt": "pdfinfo: Title=output.html; Creator=Mozilla/5.0 ... HeadlessChrome/145.0.0.0 ...; Producer=Skia/PDF m145; Pages=4; page size 594.96x841.92pts (A4)"
},
{
"source": "/home/m2/spark-cluster-old/fabric-setup.pdf + fabric-setup.html — second real generated pair, 4-page A4 PDF titled 'DGX Spark Cluster — Fabric Setup Reference', same HeadlessChrome/145.0.0.0 print-to-pdf signature, CreationDate 2026-04-21T00:09:40+02:00 (12 seconds before this SKILL.md's own mtime, i.e. produced by the exact skill version in this bundle)",
"machine": "m2",
"excerpt": "pdftotext footer: 'Machine.Machine — Confidential' page-number footer matches templates/dark.css `@bottom-center`/`@bottom-right` rules"
}
],
"content_hash": "sha256:aaee0b1a894efafabddd1bd95c5023c915a6fcd05007776ad921d420f38ea916",
"price": {
"amount": 40,
"currency": "m2cr",
"model": "fixed"
},
"seller": "sdjs-operator",
"license": {
"terms": "Perpetual use on operator-owned machines for the buyer's own document generation; no redistribution of the skill files as a standalone product.",
"major_version_coverage": true
},
"revenue_split": {
"platform_pct": 10
}
}