← 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
M .gitignoreA package-lock.jsonA package.jsonM run-meeting.sh
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,"&").replace(/</g,"<").replace(/>/g,">");
+ 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 →