diff --git a/solutions/mm-pdf-report/.gitkeep b/solutions/mm-pdf-report/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/solutions/mm-pdf-report/listing.json b/solutions/mm-pdf-report/listing.json new file mode 100644 index 0000000..d875994 --- /dev/null +++ b/solutions/mm-pdf-report/listing.json @@ -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" +} diff --git a/solutions/mm-pdf-report/payload/SKILL.md b/solutions/mm-pdf-report/payload/SKILL.md new file mode 100644 index 0000000..359e84b --- /dev/null +++ b/solutions/mm-pdf-report/payload/SKILL.md @@ -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 +``` + +## 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 + +- `.html` — standalone styled HTML (preview, shareable) +- `.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 + +
+
**€5K** setup
+
**€2K**/mo
+
**8 weeks** pilot
+
+ + +DEPLOYMENT ARCHITECTURE + + +
⚠️ Open item: integration path TBD
+``` + +## 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 ` 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. diff --git a/solutions/mm-pdf-report/payload/scripts/generate.sh b/solutions/mm-pdf-report/payload/scripts/generate.sh new file mode 100755 index 0000000..ea47465 --- /dev/null +++ b/solutions/mm-pdf-report/payload/scripts/generate.sh @@ -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 [--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 = \`
+
MACHINE.MACHINE
+
+

\${title}

+ \${subtitle ? '
' + subtitle + '
' : ''} +
+ \${dateLine ? '
' + dateLine + '
' : ''} +
\`; + +const html = \` + + + + \${title} + + + +\${coverHtml} +
+\${contentHtml} +
+ +\`; + +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))" diff --git a/solutions/mm-pdf-report/payload/templates/dark.css b/solutions/mm-pdf-report/payload/templates/dark.css new file mode 100644 index 0000000..9982b08 --- /dev/null +++ b/solutions/mm-pdf-report/payload/templates/dark.css @@ -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; + } +} diff --git a/solutions/mm-pdf-report/payload/templates/purple.css b/solutions/mm-pdf-report/payload/templates/purple.css new file mode 100644 index 0000000..e4572f7 --- /dev/null +++ b/solutions/mm-pdf-report/payload/templates/purple.css @@ -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); } diff --git a/solutions/mm-pdf-report/recipe.yaml b/solutions/mm-pdf-report/recipe.yaml new file mode 100644 index 0000000..b56e026 --- /dev/null +++ b/solutions/mm-pdf-report/recipe.yaml @@ -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 --style dark" diff --git a/solutions/mm-pdf-report/solution.json b/solutions/mm-pdf-report/solution.json new file mode 100644 index 0000000..61af85c --- /dev/null +++ b/solutions/mm-pdf-report/solution.json @@ -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 --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 + } +}