[object Object]

← back to Claude Codex Meeting

fix(email): convert markdown→HTML so Gmail renders properly + codex P2 fallback

724a66e81cbcd745990bf73c1346b5b9661d028d · 2026-05-01 18:40:01 -0700 · Steve

Steve flagged that the emailed meeting body was rendering as one collapsed
paragraph in Gmail — George sends with Content-Type: text/html, but we
were posting raw markdown. Switched the node payload builder to:

  1. Try to require `marked` (newly added dep) and convert md→HTML with
     breaks:true + gfm:true so headings, bullets, bold, line breaks all
     render as expected.
  2. Wrap the result in a <div> with a sane font + line-height for
     readable rendering on desktop and mobile.
  3. CODEX P2 FALLBACK — if `marked` is missing (fresh checkout, wiped
     node_modules), HTML-escape the markdown body and wrap it in <pre>
     with monospace + white-space:pre-wrap so the email still goes out
     readable instead of POSTing an empty/invalid JSON payload. Verified
     end-to-end with marked temporarily renamed: msg 19de656af0808d2e.
  4. Belt-and-suspenders: if node fails entirely and POST_BODY is empty,
     skip the curl rather than send garbage.

Adds .gitignore for node_modules/ (was tracked by accident on the first
npm i).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 724a66e81cbcd745990bf73c1346b5b9661d028d
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri May 1 18:40:01 2026 -0700

    fix(email): convert markdown→HTML so Gmail renders properly + codex P2 fallback
    
    Steve flagged that the emailed meeting body was rendering as one collapsed
    paragraph in Gmail — George sends with Content-Type: text/html, but we
    were posting raw markdown. Switched the node payload builder to:
    
      1. Try to require `marked` (newly added dep) and convert md→HTML with
         breaks:true + gfm:true so headings, bullets, bold, line breaks all
         render as expected.
      2. Wrap the result in a <div> with a sane font + line-height for
         readable rendering on desktop and mobile.
      3. CODEX P2 FALLBACK — if `marked` is missing (fresh checkout, wiped
         node_modules), HTML-escape the markdown body and wrap it in <pre>
         with monospace + white-space:pre-wrap so the email still goes out
         readable instead of POSTing an empty/invalid JSON payload. Verified
         end-to-end with marked temporarily renamed: msg 19de656af0808d2e.
      4. Belt-and-suspenders: if node fails entirely and POST_BODY is empty,
         skip the curl rather than send garbage.
    
    Adds .gitignore for node_modules/ (was tracked by accident on the first
    npm i).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .gitignore        |  1 +
 package-lock.json | 28 ++++++++++++++++++++++++++++
 package.json      | 15 +++++++++++++++
 run-meeting.sh    | 38 +++++++++++++++++++++++++++++++++++++-
 4 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 276b53d..124bfee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ logs/launchd.*.out
 logs/launchd.*.err
 logs/claude.err
 .symlink.lock
+node_modules/
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..74acc0e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,28 @@
+{
+  "name": "claude-codex-meeting",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "claude-codex-meeting",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "marked": "^18.0.3"
+      }
+    },
+    "node_modules/marked": {
+      "version": "18.0.3",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz",
+      "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..a37ca3b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "claude-codex-meeting",
+  "version": "1.0.0",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "description": "",
+  "dependencies": {
+    "marked": "^18.0.3"
+  }
+}
diff --git a/run-meeting.sh b/run-meeting.sh
index 8ae40d8..705e640 100755
--- a/run-meeting.sh
+++ b/run-meeting.sh
@@ -226,7 +226,43 @@ HEALTH_CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$GEORGE/healt
 if [[ "$HEALTH_CODE" == "200" || "$HEALTH_CODE" == "401" ]]; then
   # Capture both response body and HTTP status — round-8 H1 caught that
   # we were logging "email queued" even on a POST 401/500.
-  POST_BODY=$(node -e 'const fs=require("fs");const b=fs.readFileSync(process.argv[1],"utf8");process.stdout.write(JSON.stringify({to:"steve@designerwallcoverings.com",subject:process.argv[2],body:b}))' "$OUT" "$SUBJECT")
+  # Body is markdown — convert to HTML so Gmail renders headings/bullets/line
+  # breaks instead of one collapsed paragraph (Steve flagged 2026-05-01).
+  # Codex P2 (same day): if `marked` is missing (fresh checkout, wiped
+  # node_modules), fall back to a <pre>-wrapped plaintext body so we still
+  # email something rather than POSTing an empty/invalid JSON payload.
+  POST_BODY=$(node -e '
+    const fs = require("fs");
+    const path = require("path");
+    const md = fs.readFileSync(process.argv[1], "utf8");
+    let html;
+    try {
+      const { marked } = require(path.join(process.env.HOME, "Projects/claude-codex-meeting/node_modules/marked"));
+      html = marked.parse(md, { breaks: true, gfm: true });
+    } catch (e) {
+      // Plaintext fallback: HTML-escape the markdown body and wrap in <pre>.
+      const esc = md.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
+      html = "<pre style=\"font-family:ui-monospace,Menlo,monospace;white-space:pre-wrap;\">" + esc + "</pre>";
+      process.stderr.write("[meeting] marked unavailable, falling back to <pre> plaintext: " + e.message + "\n");
+    }
+    const wrapped =
+      "<div style=\"font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;" +
+      "font-size:14px;line-height:1.55;color:#1a1a1a;max-width:720px;\">" +
+      html +
+      "</div>";
+    process.stdout.write(JSON.stringify({
+      to: "steve@designerwallcoverings.com",
+      subject: process.argv[2],
+      body: wrapped,
+    }));
+  ' "$OUT" "$SUBJECT")
+  # Belt-and-suspenders: if node failed entirely (POST_BODY empty), skip the
+  # email rather than POST garbage.
+  if [[ -z "$POST_BODY" ]]; then
+    log "email payload generation failed — skipping email send"
+    log "===== meeting end ====="
+    exit 0
+  fi
   RESP_AND_CODE=$(curl -s --max-time 30 -w '\n__HTTP_STATUS__%{http_code}' \
     -H "Authorization: $GEORGE_AUTH" \
     -H "Content-Type: application/json" \

← 666f05d fix(run-meeting): apply round-8 debate findings (regressions  ·  back to Claude Codex Meeting  ·  tighten .gitignore: add missing standing-rule patterns (.env 387033c →