@bobfrankston/rmfmail 1.2.327 → 1.2.329

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1351,6 +1351,10 @@ export class MailxService implements MailxApi {
1351
1351
  live.add(`${base}.htm`);
1352
1352
  live.add(`~$${base.slice(2)}.docx`);
1353
1353
  live.add(`${base}-autosave.ps1`);
1354
+ // The sidecar's Word HTML mirror and the image folder Word writes
1355
+ // beside it (2026-09-13 Claude Code, Fable 5.1).
1356
+ live.add(`${base}-word.htm`);
1357
+ live.add(`${base}-word_files`);
1354
1358
  }
1355
1359
  let removed = 0, bytes = 0;
1356
1360
  const cutoff = Date.now() - maxAgeMs;
@@ -1533,10 +1537,23 @@ export class MailxService implements MailxApi {
1533
1537
  if (process.platform === "win32" && opener === "word") {
1534
1538
  try {
1535
1539
  const psPath = path.join(dir, `${fileBase}-autosave.ps1`);
1540
+ // 2026-09-13 20:10 EDT — Claude Code (Fable 5.1), at Bob's direction.
1541
+ // The sidecar now also mirrors the document as Word's own
1542
+ // filtered HTML via Range.ExportFragment (format 10 =
1543
+ // wdFormatFilteredHTML), which leaves the document's file and
1544
+ // format untouched. That export replaces mammoth as the way
1545
+ // edits come back (see word-html.ts for why). Export happens
1546
+ // BEFORE the save so the .htm is already fresh when the .docx
1547
+ // watcher fires; a manual Ctrl+S (document clean, file stamp
1548
+ // advanced) is mirrored on the next tick. The first attach
1549
+ // records the stamp without exporting so untouched content is
1550
+ // not re-rendered into compose before the user types anything.
1551
+ const wordHtmPath = path.join(dir, `${fileBase}-word.htm`);
1536
1552
  fs.writeFileSync(psPath, [
1537
- "param([string]$DocPath)",
1553
+ "param([string]$DocPath, [string]$HtmPath)",
1538
1554
  "$deadline = (Get-Date).AddHours(6)",
1539
1555
  "$attached = $false",
1556
+ "$lastStamp = $null",
1540
1557
  "while ((Get-Date) -lt $deadline) {",
1541
1558
  " Start-Sleep -Seconds 2",
1542
1559
  " $word = $null",
@@ -1546,11 +1563,23 @@ export class MailxService implements MailxApi {
1546
1563
  " try { foreach ($d in $word.Documents) { if ($d.FullName -ieq $DocPath) { $doc = $d; break } } } catch { continue }",
1547
1564
  " if ($null -eq $doc) { if ($attached) { exit 0 } else { continue } }",
1548
1565
  " $attached = $true",
1549
- " try { if (-not $doc.Saved) { $doc.Save() } } catch { }",
1566
+ " if ($null -eq $lastStamp) { try { $lastStamp = (Get-Item -LiteralPath $DocPath).LastWriteTimeUtc } catch { continue } }",
1567
+ " # A COM call can fail transiently while Word has a dialog up; the next",
1568
+ " # tick retries and the daemon logs any mirror that never arrives.",
1569
+ " try {",
1570
+ " if (-not $doc.Saved) {",
1571
+ " $doc.Content.ExportFragment($HtmPath, 10)",
1572
+ " $doc.Save()",
1573
+ " $lastStamp = (Get-Item -LiteralPath $DocPath).LastWriteTimeUtc",
1574
+ " } else {",
1575
+ " $stamp = (Get-Item -LiteralPath $DocPath).LastWriteTimeUtc",
1576
+ " if ($stamp -gt $lastStamp) { $doc.Content.ExportFragment($HtmPath, 10); $lastStamp = $stamp }",
1577
+ " }",
1578
+ " } catch { }",
1550
1579
  "}",
1551
1580
  "exit 1",
1552
1581
  ].join("\r\n"), "utf-8");
1553
- autosaveChild = spawn("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", psPath, filePath],
1582
+ autosaveChild = spawn("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", psPath, filePath, wordHtmPath],
1554
1583
  { detached: false, stdio: "ignore", windowsHide: true });
1555
1584
  autosaveChild.on("error", () => { autosaveChild = null; });
1556
1585
  autosaveChild.on("exit", (code) => {
@@ -1592,13 +1621,20 @@ export class MailxService implements MailxApi {
1592
1621
  // wordEditUpdated event. Mammoth is dynamically imported on first
1593
1622
  // save so the cold-start cost is paid only when the user actually
1594
1623
  // edits in Word — most sessions never hit it.
1595
- let debounce: ReturnType<typeof setTimeout> | null = null;
1624
+ // One debounce timer PER FILE. The sidecar writes the -word.htm and
1625
+ // then the .docx within a few hundred ms; a single shared timer let
1626
+ // the .docx event cancel the .htm one and the mirror was lost.
1627
+ // (2026-09-13 Claude Code, Fable 5.1)
1628
+ const debounces = new Map<string, ReturnType<typeof setTimeout>>();
1596
1629
  let lastSig = "";
1597
1630
  // Word saves the .docx in place on Ctrl+S, but Save-As-Web-Page
1598
1631
  // writes a SIBLING .htm/.html (same base name) — catch those too so
1599
1632
  // a web-page save doesn't vanish into an unwatched file (Bob
1600
1633
  // 2026-07-18, .html-fallback variant of the same report).
1601
- const watchNames = new Set([`${fileBase}.docx`, `${fileBase}.htm`, `${fileBase}.html`]);
1634
+ // `-word.htm` is the sidecar's Range.ExportFragment mirror (see the
1635
+ // sidecar script above and word-html.ts).
1636
+ const wordHtmName = `${fileBase}-word.htm`;
1637
+ const watchNames = new Set([`${fileBase}.docx`, `${fileBase}.htm`, `${fileBase}.html`, wordHtmName]);
1602
1638
  // Word's owner-lock file: "~$" + the filename with its first TWO
1603
1639
  // characters dropped. Word creates it while the document is open and
1604
1640
  // DELETES it on close — a filesystem fact the watcher already sees.
@@ -1630,17 +1666,33 @@ export class MailxService implements MailxApi {
1630
1666
  if (!name || !watchNames.has(name)) return;
1631
1667
  const savedPath = path.join(dir, name);
1632
1668
  const isDocx = name.endsWith(".docx");
1633
- if (debounce) clearTimeout(debounce);
1634
- debounce = setTimeout(async () => {
1669
+ const isWordHtm = name === wordHtmName;
1670
+ const pending = debounces.get(name);
1671
+ if (pending) clearTimeout(pending);
1672
+ debounces.set(name, setTimeout(async () => {
1673
+ debounces.delete(name);
1635
1674
  let stat: fs.Stats;
1636
1675
  try { stat = fs.statSync(savedPath); } catch { return; }
1637
1676
  const sig = `${savedPath}:${stat.size}:${stat.mtimeMs}`;
1638
1677
  if (sig === lastSig) return;
1639
1678
  lastSig = sig;
1679
+ // Once the sidecar has produced a Word HTML mirror, that file
1680
+ // is the source and every .docx save is ignored: the sidecar
1681
+ // exports before each autosave and mirrors a manual Ctrl+S on
1682
+ // its next tick, so the .docx never carries content the .htm
1683
+ // will not. mammoth stays only for sessions without the
1684
+ // sidecar (no Word, LibreOffice, COM attach failed).
1685
+ if (isDocx && fs.existsSync(path.join(dir, wordHtmName))) {
1686
+ console.log(` [word-edit] file changed: ${name} — Word HTML mirror is active, waiting for ${wordHtmName}`);
1687
+ return;
1688
+ }
1640
1689
  console.log(` [word-edit] file changed: ${name} (${stat.size} bytes) — converting`);
1641
1690
  try {
1642
1691
  let inner: string;
1643
- if (isDocx) {
1692
+ if (isWordHtm) {
1693
+ const { wordHtmlToFragment } = await import("./word-html.js");
1694
+ inner = wordHtmlToFragment(savedPath);
1695
+ } else if (isDocx) {
1644
1696
  const buf = fs.readFileSync(savedPath);
1645
1697
  const mammoth = (await import("mammoth")).default;
1646
1698
  const result = await mammoth.convertToHtml({ buffer: buf });
@@ -1675,11 +1727,12 @@ export class MailxService implements MailxApi {
1675
1727
  } catch (e: any) {
1676
1728
  console.error(` [word-edit] read after save failed: ${e.message}`);
1677
1729
  }
1678
- }, 300);
1730
+ }, 300));
1679
1731
  });
1680
1732
  const stop = () => {
1681
1733
  try { watcher.close(); } catch { /* */ }
1682
- if (debounce) clearTimeout(debounce);
1734
+ for (const t of debounces.values()) clearTimeout(t);
1735
+ debounces.clear();
1683
1736
  try { autosaveChild?.kill(); } catch { /* */ }
1684
1737
  };
1685
1738
  this.wordEdits.set(editId, { path: filePath, stop, opener, html });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-service",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Word's own HTML export → a body fragment compose can take.
3
+ *
4
+ * 2026-09-13 20:10 EDT — Claude Code (Fable 5.1), at Bob's direction ("go
5
+ * ahead and build the export fragment route").
6
+ *
7
+ * The Edit-in-Word autosave sidecar calls Word's `Range.ExportFragment(path,
8
+ * wdFormatFilteredHTML)` over COM on every change, which writes Word's
9
+ * filtered-HTML rendering of the document WITHOUT changing the document's
10
+ * own file or format. That HTML keeps colours, fonts, alignment, spacing and
11
+ * paragraph borders as inline styles — the things mammoth's docx→HTML throws
12
+ * away (measured on a real reply 2026-09-13: mammoth returned only <p>, <a>
13
+ * and <br>). This module turns that export into a fragment: body content
14
+ * only, Word's class and comment cruft removed, sidecar image files inlined.
15
+ */
16
+ /** Read a Word filtered-HTML export and return body-content HTML for compose. */
17
+ export declare function wordHtmlToFragment(htmPath: string): string;
18
+ //# sourceMappingURL=word-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"word-html.d.ts","sourceRoot":"","sources":["word-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA8CH,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgB1D"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Word's own HTML export → a body fragment compose can take.
3
+ *
4
+ * 2026-09-13 20:10 EDT — Claude Code (Fable 5.1), at Bob's direction ("go
5
+ * ahead and build the export fragment route").
6
+ *
7
+ * The Edit-in-Word autosave sidecar calls Word's `Range.ExportFragment(path,
8
+ * wdFormatFilteredHTML)` over COM on every change, which writes Word's
9
+ * filtered-HTML rendering of the document WITHOUT changing the document's
10
+ * own file or format. That HTML keeps colours, fonts, alignment, spacing and
11
+ * paragraph borders as inline styles — the things mammoth's docx→HTML throws
12
+ * away (measured on a real reply 2026-09-13: mammoth returned only <p>, <a>
13
+ * and <br>). This module turns that export into a fragment: body content
14
+ * only, Word's class and comment cruft removed, sidecar image files inlined.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ /** Decode Word's export honouring its own charset meta (utf-8 in every
19
+ * export seen so far, but Word has written windows-1252 in the past). */
20
+ function decodeWordHtml(bytes) {
21
+ const probe = bytes.subarray(0, 2048).toString("latin1");
22
+ const m = /charset=["']?([\w-]+)/i.exec(probe);
23
+ const charset = (m?.[1] || "utf-8").toLowerCase();
24
+ try {
25
+ return new TextDecoder(charset).decode(bytes);
26
+ }
27
+ catch (e) {
28
+ // An unknown label falls back to utf-8 rather than failing the
29
+ // mirror; the caller logs the fragment length either way.
30
+ console.warn(` [word-edit] unknown charset "${charset}" in Word export — decoding as utf-8 (${e?.message || e})`);
31
+ return bytes.toString("utf-8");
32
+ }
33
+ }
34
+ const IMAGE_MIME = {
35
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
36
+ ".bmp": "image/bmp", ".webp": "image/webp", ".svg": "image/svg+xml", ".emf": "image/emf", ".wmf": "image/wmf",
37
+ };
38
+ /** Turn `<img src="x_files/image001.png">` into a data: URI. Word writes
39
+ * images next to the export in a `<name>_files` folder; compose already
40
+ * converts data: images to cid parts on send. A missing file is left as
41
+ * is and logged — the text still mirrors. */
42
+ function inlineImages(html, baseDir) {
43
+ return html.replace(/<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)["']?[^>]*>/gi, (tag, src) => {
44
+ if (/^(data|https?|cid):/i.test(src))
45
+ return tag;
46
+ const file = path.resolve(baseDir, decodeURIComponent(src));
47
+ const mime = IMAGE_MIME[path.extname(file).toLowerCase()];
48
+ if (!mime)
49
+ return tag;
50
+ try {
51
+ const data = fs.readFileSync(file).toString("base64");
52
+ return tag.replace(src, `data:${mime};base64,${data}`);
53
+ }
54
+ catch (e) {
55
+ console.warn(` [word-edit] image ${src} from the Word export could not be read: ${e?.message || e}`);
56
+ return tag;
57
+ }
58
+ });
59
+ }
60
+ /** Read a Word filtered-HTML export and return body-content HTML for compose. */
61
+ export function wordHtmlToFragment(htmPath) {
62
+ const html = decodeWordHtml(fs.readFileSync(htmPath));
63
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
64
+ let inner = bodyMatch ? bodyMatch[1] : html;
65
+ inner = inner
66
+ // Conditional comments around fake-list bullets, and any other comment.
67
+ .replace(/<!--[\s\S]*?-->/g, "")
68
+ // Word's paragraph/run classes; their styles live in the export's
69
+ // <style> block, which does not travel with the fragment.
70
+ .replace(/\s+class\s*=\s*["']?Mso[\w-]*["']?/gi, "")
71
+ // Office namespace tags (<o:p></o:p> paragraph marks) and their content-free shells.
72
+ .replace(/<\/?o:[^>]*>/gi, "")
73
+ // Word wraps the whole export in a section div; compose has no use for it.
74
+ .replace(/^\s*<div\s+class\s*=\s*["']?WordSection\d*["']?[^>]*>([\s\S]*)<\/div>\s*$/i, "$1");
75
+ inner = inlineImages(inner, path.dirname(htmPath));
76
+ return inner.trim();
77
+ }
78
+ //# sourceMappingURL=word-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"word-html.js","sourceRoot":"","sources":["word-html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Word's own HTML export → a body fragment compose can take.
3
+ *
4
+ * 2026-09-13 20:10 EDT — Claude Code (Fable 5.1), at Bob's direction ("go
5
+ * ahead and build the export fragment route").
6
+ *
7
+ * The Edit-in-Word autosave sidecar calls Word's `Range.ExportFragment(path,
8
+ * wdFormatFilteredHTML)` over COM on every change, which writes Word's
9
+ * filtered-HTML rendering of the document WITHOUT changing the document's
10
+ * own file or format. That HTML keeps colours, fonts, alignment, spacing and
11
+ * paragraph borders as inline styles — the things mammoth's docx→HTML throws
12
+ * away (measured on a real reply 2026-09-13: mammoth returned only <p>, <a>
13
+ * and <br>). This module turns that export into a fragment: body content
14
+ * only, Word's class and comment cruft removed, sidecar image files inlined.
15
+ */
16
+
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+
20
+ /** Decode Word's export honouring its own charset meta (utf-8 in every
21
+ * export seen so far, but Word has written windows-1252 in the past). */
22
+ function decodeWordHtml(bytes: Buffer): string {
23
+ const probe = bytes.subarray(0, 2048).toString("latin1");
24
+ const m = /charset=["']?([\w-]+)/i.exec(probe);
25
+ const charset = (m?.[1] || "utf-8").toLowerCase();
26
+ try {
27
+ return new TextDecoder(charset).decode(bytes);
28
+ } catch (e: any) {
29
+ // An unknown label falls back to utf-8 rather than failing the
30
+ // mirror; the caller logs the fragment length either way.
31
+ console.warn(` [word-edit] unknown charset "${charset}" in Word export — decoding as utf-8 (${e?.message || e})`);
32
+ return bytes.toString("utf-8");
33
+ }
34
+ }
35
+
36
+ const IMAGE_MIME: Record<string, string> = {
37
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
38
+ ".bmp": "image/bmp", ".webp": "image/webp", ".svg": "image/svg+xml", ".emf": "image/emf", ".wmf": "image/wmf",
39
+ };
40
+
41
+ /** Turn `<img src="x_files/image001.png">` into a data: URI. Word writes
42
+ * images next to the export in a `<name>_files` folder; compose already
43
+ * converts data: images to cid parts on send. A missing file is left as
44
+ * is and logged — the text still mirrors. */
45
+ function inlineImages(html: string, baseDir: string): string {
46
+ return html.replace(/<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)["']?[^>]*>/gi, (tag, src: string) => {
47
+ if (/^(data|https?|cid):/i.test(src)) return tag;
48
+ const file = path.resolve(baseDir, decodeURIComponent(src));
49
+ const mime = IMAGE_MIME[path.extname(file).toLowerCase()];
50
+ if (!mime) return tag;
51
+ try {
52
+ const data = fs.readFileSync(file).toString("base64");
53
+ return tag.replace(src, `data:${mime};base64,${data}`);
54
+ } catch (e: any) {
55
+ console.warn(` [word-edit] image ${src} from the Word export could not be read: ${e?.message || e}`);
56
+ return tag;
57
+ }
58
+ });
59
+ }
60
+
61
+ /** Read a Word filtered-HTML export and return body-content HTML for compose. */
62
+ export function wordHtmlToFragment(htmPath: string): string {
63
+ const html = decodeWordHtml(fs.readFileSync(htmPath));
64
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
65
+ let inner = bodyMatch ? bodyMatch[1] : html;
66
+ inner = inner
67
+ // Conditional comments around fake-list bullets, and any other comment.
68
+ .replace(/<!--[\s\S]*?-->/g, "")
69
+ // Word's paragraph/run classes; their styles live in the export's
70
+ // <style> block, which does not travel with the fragment.
71
+ .replace(/\s+class\s*=\s*["']?Mso[\w-]*["']?/gi, "")
72
+ // Office namespace tags (<o:p></o:p> paragraph marks) and their content-free shells.
73
+ .replace(/<\/?o:[^>]*>/gi, "")
74
+ // Word wraps the whole export in a section div; compose has no use for it.
75
+ .replace(/^\s*<div\s+class\s*=\s*["']?WordSection\d*["']?[^>]*>([\s\S]*)<\/div>\s*$/i, "$1");
76
+ inner = inlineImages(inner, path.dirname(htmPath));
77
+ return inner.trim();
78
+ }