[object Object]

← back to George Gmail

initial scaffold (gitify-all 2026-05-06)

facbb3b2d281696a5ddd6704b07e5b57f7a11a25 · 2026-05-06 10:25:18 -0700 · Steve Abrams

Files touched

Diff

commit facbb3b2d281696a5ddd6704b07e5b57f7a11a25
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 6 10:25:18 2026 -0700

    initial scaffold (gitify-all 2026-05-06)
---
 .gitignore                    |   12 +
 check-body-regression.sh      |   72 ++
 package-lock.json             | 1656 +++++++++++++++++++++++++++++++
 package.json                  |   15 +
 public/favicon.svg            |    4 +
 public/index.html             |  193 ++++
 redfin-count.js               |   24 +
 server.js                     | 2181 +++++++++++++++++++++++++++++++++++++++++
 server.js.bak-20260421-094351 | 1153 ++++++++++++++++++++++
 trash-redfin-zillow.js        |   70 ++
 10 files changed, 5380 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e6a9c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.bak
diff --git a/check-body-regression.sh b/check-body-regression.sh
new file mode 100755
index 0000000..df6d5fa
--- /dev/null
+++ b/check-body-regression.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+# Weekly check: are George's recent sends arriving with non-empty bodies?
+# Regression target: buildRawMessage in server.js (fixed 2026-05-01).
+# If any of the 5 most-recent sends has body length 0, alert Steve via George.
+
+set -u
+GEORGE="http://localhost:9850"
+AUTH="admin:DWSecure2024!"
+LOG="${HOME}/Library/Logs/george-body-check.log"
+ts() { date '+%Y-%m-%d %H:%M:%S'; }
+log() { echo "[$(ts)] $*" >> "$LOG"; }
+
+log "=== body-regression check start ==="
+
+if ! curl -s -m 5 -u "$AUTH" "$GEORGE/api/health" | grep -q '"status":"ready"'; then
+  log "george unreachable or unhealthy — skipping (not a regression)"
+  exit 0
+fi
+
+# buildRawMessage was fixed at this UTC timestamp (2026-05-01 19:00 UTC).
+# Earlier sends had legacy empty-body bug — those are not regressions, skip.
+FIX_CUTOFF_MS=1777662000000
+
+ids=$(curl -s -u "$AUTH" "$GEORGE/api/search?q=in:sent&limit=10" \
+  | /usr/bin/python3 -c "import json,sys; d=json.load(sys.stdin); print('\n'.join(m['id'] for m in (d.get('messages') or [])[:10]))")
+
+if [ -z "$ids" ]; then
+  log "no recent sent messages found — exiting clean"
+  exit 0
+fi
+
+bad=()
+checked=0
+for id in $ids; do
+  read -r blen mdate <<< "$(curl -s -u "$AUTH" "$GEORGE/api/messages/$id" \
+    | /usr/bin/python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('body') or ''), d.get('internalDate') or 0)")"
+  if [ "$mdate" -lt "$FIX_CUTOFF_MS" ]; then
+    log "  $id  body=$blen  SKIP (pre-fix, $mdate)"
+    continue
+  fi
+  log "  $id  body=$blen"
+  checked=$((checked+1))
+  if [ "$blen" = "0" ]; then bad+=("$id"); fi
+done
+
+log "checked $checked post-fix messages"
+
+if [ "${#bad[@]}" -eq 0 ]; then
+  log "all healthy — no alert sent"
+  exit 0
+fi
+
+log "REGRESSION: empty-body messages: ${bad[*]}"
+
+body_html="<p>George body-empty regression detected — the <code>buildRawMessage</code> fix from 2026-05-01 may have been reverted.</p><p>Empty-body sends:</p><ul>"
+for id in "${bad[@]}"; do
+  body_html+="<li><code>$id</code></li>"
+done
+body_html+="</ul><p>Source: <code>~/Projects/george-gmail/server.js</code> (function <code>buildRawMessage</code>, ~line 789).</p><p>The <code>filter(Boolean)</code> bug was: it stripped the empty-string element that should be the blank line between MIME headers and the base64 body, so Gmail rendered no body.</p>"
+
+payload=$(/usr/bin/python3 -c "
+import json
+print(json.dumps({
+  'to': 'steveabramsdesigns@gmail.com',
+  'subject': 'George body-empty regression detected',
+  'body': '''$body_html'''
+}))
+")
+
+resp=$(curl -s -u "$AUTH" -X POST "$GEORGE/api/send" \
+  -H 'Content-Type: application/json' -d "$payload")
+log "alert sent: $resp"
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..0b7d625
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1656 @@
+{
+  "name": "george-gmail-agent",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "george-gmail-agent",
+      "version": "1.0.0",
+      "dependencies": {
+        "express": "^4.18.2",
+        "googleapis": "^164.1.0",
+        "helmet": "^8.1.0",
+        "nodemailer": "^8.0.5"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/bignumber.js": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.4",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+      "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "~1.2.0",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "on-finished": "~2.4.1",
+        "qs": "~6.14.0",
+        "raw-body": "~2.5.3",
+        "type-is": "~1.6.18",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+      "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.3",
+        "content-disposition": "~0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "~0.7.1",
+        "cookie-signature": "~1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.3.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "~0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "~6.14.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "~0.19.0",
+        "serve-static": "~1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/fetch-blob": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "node-domexception": "^1.0.0",
+        "web-streams-polyfill": "^3.0.3"
+      },
+      "engines": {
+        "node": "^12.20 || >= 14.13"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+      "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "~2.0.2",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/formdata-polyfill": {
+      "version": "4.0.10",
+      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+      "license": "MIT",
+      "dependencies": {
+        "fetch-blob": "^3.1.2"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gaxios": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
+      "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "node-fetch": "^3.3.2",
+        "rimraf": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
+      "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "google-logging-utils": "^1.0.0",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+      "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/google-auth-library": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
+      "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "base64-js": "^1.3.0",
+        "ecdsa-sig-formatter": "^1.0.11",
+        "gaxios": "^7.0.0",
+        "gcp-metadata": "^8.0.0",
+        "google-logging-utils": "^1.0.0",
+        "gtoken": "^8.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/google-logging-utils": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
+      "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/googleapis": {
+      "version": "164.1.0",
+      "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-164.1.0.tgz",
+      "integrity": "sha512-dIN768H8so9qGucFtjYPBZJ+OCEgDi/xYyvYQHniPL1ZCYvrRDBTmtbjVjKCPG1CuOhG4CKHZDXiFe6QZ2qBeQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "google-auth-library": "^10.2.0",
+        "googleapis-common": "^8.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/googleapis-common": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz",
+      "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "gaxios": "^7.0.0-rc.4",
+        "google-auth-library": "^10.1.0",
+        "qs": "^6.7.0",
+        "url-template": "^2.0.8"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gtoken": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
+      "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
+      "license": "MIT",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/helmet": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+      "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/https-proxy-agent/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "deprecated": "Use your platform's native DOMException instead",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+      "license": "MIT",
+      "dependencies": {
+        "data-uri-to-buffer": "^4.0.0",
+        "fetch-blob": "^3.1.4",
+        "formdata-polyfill": "^4.0.10"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/node-fetch"
+      }
+    },
+    "node_modules/nodemailer": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
+      "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+      "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+      "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "5.0.10",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
+      "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^10.3.7"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+      "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "~2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.3",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+      "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "~0.19.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/url-template": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
+      "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
+      "license": "BSD"
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/web-streams-polyfill": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1c7d84c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "george-gmail-agent",
+  "version": "1.0.0",
+  "description": "George the Gmail Agent — secure email access for DW-Agents",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js"
+  },
+  "dependencies": {
+    "express": "^4.18.2",
+    "googleapis": "^164.1.0",
+    "helmet": "^8.1.0",
+    "nodemailer": "^8.0.5"
+  }
+}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..5187fc3
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
+<rect width="32" height="32" rx="6" fill="#10b981"/>
+<text x="50%" y="55%" text-anchor="middle" dominant-baseline="middle" font-size="20" font-family="Apple Color Emoji, Segoe UI Emoji, sans-serif" fill="white">G</text>
+</svg>
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..f33cf45
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,193 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>George — Gmail Agent</title>
+<style>
+:root{--bg:#0f1117;--card:#1a1d27;--border:#2a2d3a;--text:#e4e4e7;--muted:#888;--accent:#EA4335;--green:#4ade80;--blue:#60a5fa;--red:#f87171}
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
+.header{padding:20px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px;background:var(--card)}
+.avatar{width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:#fff;background:linear-gradient(135deg,#EA4335,#FBBC04,#34A853,#4285F4)}
+.header-info h1{font-size:20px;font-weight:700}.header-info span{font-size:12px;color:var(--muted)}
+.status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
+.status-dot.online{background:var(--green)}.status-dot.error{background:var(--red)}
+.container{max-width:1200px;margin:0 auto;padding:20px}
+.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px}
+.stat{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;text-align:center}
+.stat .val{font-size:24px;font-weight:700;color:var(--accent)}.stat .label{font-size:11px;color:var(--muted);margin-top:4px}
+.tabs{display:flex;gap:4px;margin-bottom:16px;border-bottom:1px solid var(--border);padding-bottom:8px}
+.tab{background:none;border:none;color:var(--muted);padding:8px 16px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500}
+.tab.active{background:rgba(234,67,53,.12);color:var(--accent)}
+.tab:hover{color:var(--text)}
+.search-bar{display:flex;gap:8px;margin-bottom:16px}
+.search-bar input{flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 16px;color:var(--text);font-size:14px;outline:none}
+.search-bar input:focus{border-color:var(--accent)}
+.search-bar button{background:var(--accent);color:#fff;border:none;border-radius:10px;padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer}
+.search-bar button:hover{opacity:.9}
+.email-list{display:flex;flex-direction:column;gap:6px}
+.email-item{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px 16px;cursor:pointer;transition:all .15s}
+.email-item:hover{border-color:var(--accent);transform:translateY(-1px)}
+.email-item .subject{font-size:14px;font-weight:600;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.email-item .from{font-size:12px;color:var(--blue);margin-bottom:2px}
+.email-item .snippet{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.email-item .date{font-size:10px;color:var(--muted);float:right;margin-top:-20px}
+.email-detail{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px}
+.email-detail h2{font-size:16px;margin-bottom:8px}
+.email-detail .meta{font-size:12px;color:var(--muted);margin-bottom:16px;line-height:1.6}
+.email-detail .body{font-size:13px;line-height:1.7;white-space:pre-wrap;max-height:500px;overflow-y:auto;padding:12px;background:rgba(0,0,0,.2);border-radius:8px}
+.compose{display:flex;flex-direction:column;gap:10px}
+.compose input,.compose textarea{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 14px;color:var(--text);font-size:13px;outline:none;font-family:inherit}
+.compose textarea{min-height:200px;resize:vertical}
+.compose button{align-self:flex-end;background:var(--accent);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:600;cursor:pointer}
+.loading{text-align:center;padding:40px;color:var(--muted)}
+.empty{text-align:center;padding:30px;color:var(--muted);font-size:13px}
+.badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:10px;font-weight:600}
+.badge-green{background:rgba(74,222,128,.15);color:var(--green)}
+.badge-red{background:rgba(248,113,113,.15);color:var(--red)}
+.label-list{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
+.label-chip{padding:4px 10px;border-radius:8px;font-size:11px;background:rgba(96,165,250,.1);color:var(--blue);border:1px solid rgba(96,165,250,.2)}
+.back-btn{background:none;border:1px solid var(--border);color:var(--muted);padding:6px 14px;border-radius:8px;cursor:pointer;font-size:12px;margin-bottom:12px}
+.back-btn:hover{color:var(--text);border-color:var(--text)}
+</style>
+</head>
+<body>
+<div id="root"></div>
+<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
+function xfetch(url,opts){return new Promise(function(resolve,reject){var x=new XMLHttpRequest();x.open((opts&&opts.method)||"GET",url,true);x.setRequestHeader("Authorization","Basic "+btoa("admin:DWSecure2024!"));if(opts&&opts.body){x.setRequestHeader("Content-Type","application/json");}x.onload=function(){try{resolve({ok:x.status>=200&&x.status<300,status:x.status,json:function(){return Promise.resolve(JSON.parse(x.responseText))},text:function(){return Promise.resolve(x.responseText)}})}catch(e){reject(e)}};x.onerror=function(){reject(new Error("XHR failed"))};x.send(opts&&opts.body?JSON.stringify(opts.body):null)})};
+<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
+<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
+<script type="text/babel">
+const { useState, useEffect, useCallback } = React;
+
+const AUTH = 'Basic ' + btoa('admin:DWSecure2024!');
+async function api(path) {
+  const r = await xfetch(path, { headers: { Authorization: AUTH } });
+  return r.json();
+}
+async function apiPost(path, body) {
+  const r = await xfetch(path, { method: 'POST', headers: { Authorization: AUTH, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+  return r.json();
+}
+
+function App() {
+  const [status, setStatus] = useState(null);
+  const [profile, setProfile] = useState(null);
+  const [tab, setTab] = useState('inbox');
+  const [messages, setMessages] = useState([]);
+  const [labels, setLabels] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [search, setSearch] = useState('');
+  const [selectedMsg, setSelectedMsg] = useState(null);
+  const [msgDetail, setMsgDetail] = useState(null);
+
+  useEffect(() => {
+    Promise.all([api('/api/status'), api('/api/profile'), api('/api/messages?maxResults=25'), api('/api/labels')])
+      .then(([s, p, m, l]) => { setStatus(s); setProfile(p); setMessages(m.messages || []); setLabels(l); setLoading(false); })
+      .catch(() => setLoading(false));
+  }, []);
+
+  async function doSearch() {
+    if (!search.trim()) return;
+    setLoading(true);
+    const r = await api('/api/search?maxResults=20&q=' + encodeURIComponent(search));
+    setMessages(r.messages || []);
+    setTab('search');
+    setLoading(false);
+  }
+
+  async function loadInbox() {
+    setLoading(true);
+    setSelectedMsg(null); setMsgDetail(null);
+    const r = await api('/api/messages?maxResults=25');
+    setMessages(r.messages || []);
+    setTab('inbox');
+    setLoading(false);
+  }
+
+  async function openMessage(id) {
+    setSelectedMsg(id);
+    const r = await api('/api/messages/' + id);
+    setMsgDetail(r);
+  }
+
+  if (loading && !status) return <div className="loading">Loading George...</div>;
+
+  return (
+    <div>
+      <div className="header">
+        <div className="avatar">G</div>
+        <div className="header-info">
+          <h1>George <span style={{color:'var(--muted)',fontWeight:400,fontSize:14}}>Gmail Agent</span></h1>
+          <span>
+            <span className={'status-dot ' + (status?.status === 'online' ? 'online' : 'error')} />
+            {profile?.emailAddress || 'loading...'} — {profile?.messagesTotal?.toLocaleString()} messages
+          </span>
+        </div>
+        <div style={{marginLeft:'auto'}}>
+          <span className={'badge ' + (status?.status === 'online' ? 'badge-green' : 'badge-red')}>{status?.status || 'unknown'}</span>
+        </div>
+      </div>
+
+      <div className="container">
+        <div className="stats">
+          <div className="stat"><div className="val">{profile?.messagesTotal?.toLocaleString() || '—'}</div><div className="label">Total Messages</div></div>
+          <div className="stat"><div className="val">{profile?.threadsTotal?.toLocaleString() || '—'}</div><div className="label">Threads</div></div>
+          <div className="stat"><div className="val">{labels.length}</div><div className="label">Labels</div></div>
+          <div className="stat"><div className="val" style={{color:'var(--green)'}}>{status?.capabilities?.length || 0}</div><div className="label">Capabilities</div></div>
+        </div>
+
+        <div className="search-bar">
+          <input value={search} onChange={e => setSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && doSearch()} placeholder="Search emails (Gmail syntax: from:, subject:, has:attachment, newer_than:7d ...)" />
+          <button onClick={doSearch}>Search</button>
+        </div>
+
+        <div className="tabs">
+          <button className={'tab' + (tab === 'inbox' ? ' active' : '')} onClick={loadInbox}>Inbox</button>
+          <button className={'tab' + (tab === 'search' ? ' active' : '')} disabled={tab !== 'search'}>Search Results</button>
+          <button className={'tab' + (tab === 'labels' ? ' active' : '')} onClick={() => setTab('labels')}>Labels</button>
+        </div>
+
+        {selectedMsg && msgDetail ? (
+          <div>
+            <button className="back-btn" onClick={() => { setSelectedMsg(null); setMsgDetail(null); }}>← Back</button>
+            <div className="email-detail">
+              <h2>{msgDetail.subject || '(no subject)'}</h2>
+              <div className="meta">
+                <strong>From:</strong> {msgDetail.from}<br/>
+                <strong>To:</strong> {msgDetail.to}<br/>
+                {msgDetail.cc && <><strong>Cc:</strong> {msgDetail.cc}<br/></>}
+                <strong>Date:</strong> {msgDetail.date}
+                {msgDetail.attachments?.length > 0 && <><br/><strong>Attachments:</strong> {msgDetail.attachments.map(a => a.filename).join(', ')}</>}
+              </div>
+              <div className="body">{msgDetail.body || msgDetail.snippet || '(empty)'}</div>
+            </div>
+          </div>
+        ) : tab === 'labels' ? (
+          <div className="label-list">
+            {labels.map(l => <span key={l.id} className="label-chip">{l.name || l.id}</span>)}
+          </div>
+        ) : (
+          <div className="email-list">
+            {loading && <div className="loading">Loading...</div>}
+            {!loading && messages.length === 0 && <div className="empty">No messages found</div>}
+            {!loading && messages.map(m => (
+              <div key={m.id} className="email-item" onClick={() => openMessage(m.id)}>
+                <div className="date">{m.date ? new Date(m.date).toLocaleString('en-US', { timeZone: 'America/Los_Angeles', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : ''}</div>
+                <div className="from">{m.from}</div>
+                <div className="subject">{m.subject || '(no subject)'}</div>
+                <div className="snippet">{m.snippet}</div>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+</script>
+</body>
+</html>
diff --git a/redfin-count.js b/redfin-count.js
new file mode 100644
index 0000000..d578994
--- /dev/null
+++ b/redfin-count.js
@@ -0,0 +1,24 @@
+const fs=require("fs");const {google}=require("googleapis");
+const env={};fs.readFileSync("/root/Projects/Designer-Wallcoverings/DW-MCP/.env","utf8").split("\n").forEach(l=>{const m=l.match(/^([^#=]+)=(.+)/);if(m)env[m[1].trim()]=m[2].trim()});
+const accts=[
+  {k:"steve-office",l:"steve@designerwallcoverings.com",r:env.GMAIL_REFRESH_TOKEN},
+  {k:"info",l:"info@designerwallcoverings.com",r:env.INFO_REFRESH_TOKEN},
+  {k:"steve-personal",l:"steveabramsdesigns@gmail.com",r:env.PERSONAL_REFRESH_TOKEN},
+];
+(async()=>{
+  for(const a of accts){
+    const oc=new google.auth.OAuth2(env.GMAIL_CLIENT_ID,env.GMAIL_CLIENT_SECRET,"http://localhost:9850/oauth2callback");
+    oc.setCredentials({refresh_token:a.r});
+    const gm=google.gmail({version:"v1",auth:oc});
+    let count=0,pages=0,pageToken=null;
+    const start=Date.now();
+    do{
+      const r=await gm.users.messages.list({userId:"me",q:"redfin",maxResults:500,pageToken:pageToken||undefined});
+      count+=(r.data.messages||[]).length;
+      pageToken=r.data.nextPageToken||null;
+      pages++;
+      if(pages%20===0) process.stdout.write(`    ...${count} so far (${a.l})...\n`);
+    }while(pageToken);
+    console.log(`${a.l}: ${count} messages, ${pages} pages, ${((Date.now()-start)/1000).toFixed(1)}s`);
+  }
+})();
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..d82a3a7
--- /dev/null
+++ b/server.js
@@ -0,0 +1,2181 @@
+/**
+ * George the Gmail Agent — Port 9889
+ * Secure email access for DW-Agents ecosystem
+ * Auth: admin / DWSecure2024!
+ */
+const express = require('express');
+const helmet = require('helmet');
+const path = require('path');
+const fs = require('fs');
+const { google } = require('googleapis');
+const nodemailer = require('nodemailer');
+
+const PORT = process.env.PORT || 9850;
+const AGENT_NAME = 'George';
+const AGENT_CODENAME = 'Gmail';
+const AGENT_COLOR = '#EA4335';
+
+// ─── Gmail OAuth Credentials (loaded from DW-MCP .env) ───
+const ENV_PATH = '/root/Projects/Designer-Wallcoverings/DW-MCP/.env';
+const creds = {};
+try {
+  const envContent = fs.readFileSync(ENV_PATH, 'utf8');
+  envContent.split('\n').forEach(line => {
+    const match = line.match(/^([^#=]+)=(.+)/);
+    if (match) creds[match[1].trim()] = match[2].trim();
+  });
+  console.log(`[${AGENT_NAME}] Loaded credentials from ${ENV_PATH}`);
+} catch (e) {
+  console.error(`[${AGENT_NAME}] Failed to load env: ${e.message}`);
+}
+
+const GMAIL_CLIENT_ID = creds.GMAIL_CLIENT_ID || process.env.GMAIL_CLIENT_ID;
+const GMAIL_CLIENT_SECRET = creds.GMAIL_CLIENT_SECRET || process.env.GMAIL_CLIENT_SECRET;
+const GMAIL_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
+const GMAIL_REFRESH_TOKEN = creds.GMAIL_REFRESH_TOKEN || process.env.GMAIL_REFRESH_TOKEN;
+
+// ─── Info@ Account Credentials ───
+const INFO_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
+const INFO_REFRESH_TOKEN = creds.INFO_REFRESH_TOKEN || process.env.INFO_REFRESH_TOKEN;
+
+// ─── Steve Personal (steveabramsdesigns@gmail.com) Credentials ───
+// Reuses GMAIL_CLIENT_ID/SECRET (see PLAN-GSUITE-EXPANSION.md Q1=a).
+const PERSONAL_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
+const PERSONAL_REFRESH_TOKEN = creds.PERSONAL_REFRESH_TOKEN || process.env.PERSONAL_REFRESH_TOKEN;
+
+// ─── StevesClaude (stevesclaude@gmail.com) Credentials ───
+// Dedicated mailbox for emailing Claude. Reuses GMAIL_CLIENT_ID/SECRET.
+const STEVESCLAUDE_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
+const STEVESCLAUDE_REFRESH_TOKEN = creds.STEVESCLAUDE_REFRESH_TOKEN || process.env.STEVESCLAUDE_REFRESH_TOKEN;
+const STEVESCLAUDE_LOGIN_HINT = creds.STEVESCLAUDE_LOGIN_HINT || process.env.STEVESCLAUDE_LOGIN_HINT || 'stevesclaude@gmail.com';
+
+// ─── OAuth2 Scopes ───
+// FULL_WORKSPACE_SCOPES = unified scope list for all 3 accounts.
+// Steve approved including Forms (Q3=y) so we don't have to re-OAuth later.
+const FULL_WORKSPACE_SCOPES = [
+  'https://www.googleapis.com/auth/gmail.modify',
+  'https://www.googleapis.com/auth/gmail.settings.sharing',  // sendAs alias create/update
+  'https://www.googleapis.com/auth/gmail.settings.basic',    // filters / labels
+  'https://www.googleapis.com/auth/tasks',
+  'https://www.googleapis.com/auth/drive',
+  'https://www.googleapis.com/auth/documents',
+  'https://www.googleapis.com/auth/spreadsheets',
+  'https://www.googleapis.com/auth/presentations',
+  'https://www.googleapis.com/auth/calendar',
+  'https://www.googleapis.com/auth/forms.body',
+  'https://www.googleapis.com/auth/forms.responses.readonly',
+];
+// Kept as aliases so older references don't break mid-rollout.
+const SCOPES = FULL_WORKSPACE_SCOPES;
+const INFO_SCOPES = FULL_WORKSPACE_SCOPES;
+const PERSONAL_SCOPES = FULL_WORKSPACE_SCOPES;
+
+// Steve@ account (primary)
+let oauth2Client;
+let gmail;
+let tasksApi;
+try {
+  oauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URI);
+  oauth2Client.setCredentials({ refresh_token: GMAIL_REFRESH_TOKEN });
+  gmail = google.gmail({ version: 'v1', auth: oauth2Client });
+  tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
+  console.log(`[${AGENT_NAME}] Gmail + Tasks API client initialized (steve@)`);
+} catch (e) {
+  console.error(`[${AGENT_NAME}] API client error: ${e.message}`);
+}
+
+// Info@ account (secondary — now full Workspace)
+let infoOauth2Client;
+let infoGmail;
+let infoTasksApi;
+let infoDrive;
+let infoDocs;
+let infoSheets;
+let infoSlides;
+let infoCalendar;
+let infoForms;
+try {
+  infoOauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, INFO_REDIRECT_URI);
+  if (INFO_REFRESH_TOKEN) {
+    infoOauth2Client.setCredentials({ refresh_token: INFO_REFRESH_TOKEN });
+    infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
+    infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
+    infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
+    infoDocs = google.docs({ version: 'v1', auth: infoOauth2Client });
+    infoSheets = google.sheets({ version: 'v4', auth: infoOauth2Client });
+    infoSlides = google.slides({ version: 'v1', auth: infoOauth2Client });
+    infoCalendar = google.calendar({ version: 'v3', auth: infoOauth2Client });
+    infoForms = google.forms({ version: 'v1', auth: infoOauth2Client });
+    console.log(`[${AGENT_NAME}] Info@ account initialized (full Workspace)`);
+  } else {
+    console.log(`[${AGENT_NAME}] Info@ not configured — visit /auth/info to authorize`);
+  }
+} catch (e) {
+  console.error(`[${AGENT_NAME}] Info@ client error: ${e.message}`);
+}
+
+// ─── Steve@dw account — upgrade to full Workspace ───
+let docs, sheets, slides, drive, calendar, forms;
+try {
+  if (oauth2Client && GMAIL_REFRESH_TOKEN) {
+    docs = google.docs({ version: 'v1', auth: oauth2Client });
+    sheets = google.sheets({ version: 'v4', auth: oauth2Client });
+    slides = google.slides({ version: 'v1', auth: oauth2Client });
+    drive = google.drive({ version: 'v3', auth: oauth2Client });
+    calendar = google.calendar({ version: 'v3', auth: oauth2Client });
+    forms = google.forms({ version: 'v1', auth: oauth2Client });
+    console.log(`[${AGENT_NAME}] Steve Office account upgraded to full Workspace`);
+  }
+} catch (e) {
+  console.error(`[${AGENT_NAME}] Steve Office Workspace upgrade error: ${e.message}`);
+}
+
+// ─── Steve Personal (steveabramsdesigns@gmail.com) account ───
+let personalOauth2Client;
+let personalGmail, personalTasksApi, personalDrive, personalDocs, personalSheets, personalSlides, personalCalendar, personalForms;
+try {
+  personalOauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, PERSONAL_REDIRECT_URI);
+  if (PERSONAL_REFRESH_TOKEN) {
+    personalOauth2Client.setCredentials({ refresh_token: PERSONAL_REFRESH_TOKEN });
+    personalGmail = google.gmail({ version: 'v1', auth: personalOauth2Client });
+    personalTasksApi = google.tasks({ version: 'v1', auth: personalOauth2Client });
+    personalDrive = google.drive({ version: 'v3', auth: personalOauth2Client });
+    personalDocs = google.docs({ version: 'v1', auth: personalOauth2Client });
+    personalSheets = google.sheets({ version: 'v4', auth: personalOauth2Client });
+    personalSlides = google.slides({ version: 'v1', auth: personalOauth2Client });
+    personalCalendar = google.calendar({ version: 'v3', auth: personalOauth2Client });
+    personalForms = google.forms({ version: 'v1', auth: personalOauth2Client });
+    console.log(`[${AGENT_NAME}] Steve Personal account initialized (full Workspace)`);
+  } else {
+    console.log(`[${AGENT_NAME}] Steve Personal not configured — visit /auth/steve-personal to authorize`);
+  }
+} catch (e) {
+  console.error(`[${AGENT_NAME}] Steve Personal client error: ${e.message}`);
+}
+
+// ─── StevesClaude (stevesclaude@gmail.com) account ───
+let stevesclaudeOauth2Client;
+let stevesclaudeGmail, stevesclaudeTasksApi, stevesclaudeDrive, stevesclaudeDocs, stevesclaudeSheets, stevesclaudeSlides, stevesclaudeCalendar, stevesclaudeForms;
+try {
+  stevesclaudeOauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, STEVESCLAUDE_REDIRECT_URI);
+  if (STEVESCLAUDE_REFRESH_TOKEN) {
+    stevesclaudeOauth2Client.setCredentials({ refresh_token: STEVESCLAUDE_REFRESH_TOKEN });
+    stevesclaudeGmail    = google.gmail   ({ version: 'v1', auth: stevesclaudeOauth2Client });
+    stevesclaudeTasksApi = google.tasks   ({ version: 'v1', auth: stevesclaudeOauth2Client });
+    stevesclaudeDrive    = google.drive   ({ version: 'v3', auth: stevesclaudeOauth2Client });
+    stevesclaudeDocs     = google.docs    ({ version: 'v1', auth: stevesclaudeOauth2Client });
+    stevesclaudeSheets   = google.sheets  ({ version: 'v4', auth: stevesclaudeOauth2Client });
+    stevesclaudeSlides   = google.slides  ({ version: 'v1', auth: stevesclaudeOauth2Client });
+    stevesclaudeCalendar = google.calendar({ version: 'v3', auth: stevesclaudeOauth2Client });
+    stevesclaudeForms    = google.forms   ({ version: 'v1', auth: stevesclaudeOauth2Client });
+    console.log(`[${AGENT_NAME}] StevesClaude account initialized (full Workspace)`);
+  } else {
+    console.log(`[${AGENT_NAME}] StevesClaude not configured — visit /auth/stevesclaude to authorize`);
+  }
+} catch (e) {
+  console.error(`[${AGENT_NAME}] StevesClaude client error: ${e.message}`);
+}
+
+// ─── Account resolver for per-service API endpoints ───
+// account query/body param: "steve-office" (default), "info", "steve-personal", "stevesclaude"
+function resolveAccount(req) {
+  const key = (req.query.account || req.body?.account || 'steve-office').toLowerCase();
+  const map = {
+    'steve-office':   { label: 'steve@designerwallcoverings.com', oauth: oauth2Client, gmail, tasks: tasksApi, drive, docs, sheets, slides, calendar, forms, refreshToken: GMAIL_REFRESH_TOKEN },
+    'info':           { label: 'info@designerwallcoverings.com',  oauth: infoOauth2Client, gmail: infoGmail, tasks: infoTasksApi, drive: infoDrive, docs: infoDocs, sheets: infoSheets, slides: infoSlides, calendar: infoCalendar, forms: infoForms, refreshToken: INFO_REFRESH_TOKEN },
+    'steve-personal': { label: 'steveabramsdesigns@gmail.com',    oauth: personalOauth2Client, gmail: personalGmail, tasks: personalTasksApi, drive: personalDrive, docs: personalDocs, sheets: personalSheets, slides: personalSlides, calendar: personalCalendar, forms: personalForms, refreshToken: PERSONAL_REFRESH_TOKEN },
+    'stevesclaude':   { label: STEVESCLAUDE_LOGIN_HINT,           oauth: stevesclaudeOauth2Client, gmail: stevesclaudeGmail, tasks: stevesclaudeTasksApi, drive: stevesclaudeDrive, docs: stevesclaudeDocs, sheets: stevesclaudeSheets, slides: stevesclaudeSlides, calendar: stevesclaudeCalendar, forms: stevesclaudeForms, refreshToken: STEVESCLAUDE_REFRESH_TOKEN },
+  };
+  return { key, ...(map[key] || {}) };
+}
+
+// Audit log for every Workspace write operation
+const AUDIT_LOG = '/root/DW-Agents/logs/george-workspace-audit.log';
+function audit(account, action, details) {
+  try {
+    const line = `${new Date().toISOString()}\t${account}\t${action}\t${JSON.stringify(details)}\n`;
+    fs.appendFileSync(AUDIT_LOG, line);
+  } catch {}
+}
+
+// ─── Express App ───
+const app = express();
+// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
+app.use(helmet({ contentSecurityPolicy: false }));
+app.use(express.json({ limit: '50mb' }));
+
+// ─── Basic Auth (skip health + OAuth callbacks) ───
+app.use((req, res, next) => {
+  const publicPaths = ['/health', '/auth', '/auth/info', '/auth/steve-office', '/auth/steve-personal', '/auth/stevesclaude', '/oauth2callback'];
+  if (publicPaths.includes(req.path)) return next();
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) {
+    res.setHeader('WWW-Authenticate', 'Basic realm="George Gmail Agent"');
+    return res.status(401).json({ error: 'Authentication required' });
+  }
+  const decoded = Buffer.from(auth.slice(6), 'base64').toString();
+  const [user, pass] = decoded.split(':');
+  if (user === 'admin' && pass === 'DWSecure2024!') return next();
+  res.setHeader('WWW-Authenticate', 'Basic realm="George Gmail Agent"');
+  res.status(401).json({ error: 'Invalid credentials' });
+});
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+// ─── OAuth Re-Authorization Flow ───
+const OAUTH_REDIRECT = 'http://localhost:9850/oauth2callback';
+
+app.get('/auth', (req, res) => {
+  if (!oauth2Client) return res.status(500).send('OAuth client not configured');
+  const url = oauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: FULL_WORKSPACE_SCOPES,
+    prompt: 'consent',
+    redirect_uri: OAUTH_REDIRECT,
+    login_hint: 'steve@designerwallcoverings.com',
+  });
+  res.redirect(url);
+});
+
+// Alias: /auth/steve-office — same as /auth, clearer name
+app.get('/auth/steve-office', (req, res) => res.redirect('/auth'));
+
+app.get('/oauth2callback', async (req, res) => {
+  try {
+    const { code, state } = req.query;
+    if (!code) return res.status(400).send('No authorization code received');
+
+    // Route to stevesclaude handler if state=stevesclaude
+    if (state === 'stevesclaude') {
+      const { tokens } = await stevesclaudeOauth2Client.getToken({ code, redirect_uri: STEVESCLAUDE_REDIRECT_URI });
+      stevesclaudeOauth2Client.setCredentials(tokens);
+      stevesclaudeGmail    = google.gmail   ({ version: 'v1', auth: stevesclaudeOauth2Client });
+      stevesclaudeTasksApi = google.tasks   ({ version: 'v1', auth: stevesclaudeOauth2Client });
+      stevesclaudeDrive    = google.drive   ({ version: 'v3', auth: stevesclaudeOauth2Client });
+      stevesclaudeDocs     = google.docs    ({ version: 'v1', auth: stevesclaudeOauth2Client });
+      stevesclaudeSheets   = google.sheets  ({ version: 'v4', auth: stevesclaudeOauth2Client });
+      stevesclaudeSlides   = google.slides  ({ version: 'v1', auth: stevesclaudeOauth2Client });
+      stevesclaudeCalendar = google.calendar({ version: 'v3', auth: stevesclaudeOauth2Client });
+      stevesclaudeForms    = google.forms   ({ version: 'v1', auth: stevesclaudeOauth2Client });
+
+      let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
+      msg += '<h2 style="color:#4ade80">StevesClaude Authorized! (Full Google Workspace)</h2>';
+      if (tokens.refresh_token) {
+        msg += `<p><strong>New StevesClaude Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
+        console.log(`[${AGENT_NAME}] StevesClaude refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
+        try {
+          let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+          if (envContent.includes('STEVESCLAUDE_REFRESH_TOKEN=')) {
+            envContent = envContent.replace(/STEVESCLAUDE_REFRESH_TOKEN=.*/, `STEVESCLAUDE_REFRESH_TOKEN=${tokens.refresh_token}`);
+          } else {
+            envContent += `\n# StevesClaude (stevesclaude@gmail.com) OAuth Token\nSTEVESCLAUDE_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+          }
+          fs.writeFileSync(ENV_PATH, envContent);
+          msg += '<p style="color:#4ade80">Token auto-saved to central .env!</p>';
+          audit('stevesclaude', 'oauth-authorized', { scopes: FULL_WORKSPACE_SCOPES.length });
+        } catch (e) {
+          msg += `<p style="color:#f87171">Could not auto-save: ${e.message}</p>`;
+        }
+      }
+      try {
+        const profile = await stevesclaudeGmail.users.getProfile({ userId: 'me' });
+        msg += `<p style="color:#60a5fa">Connected as: <strong>${profile.data.emailAddress}</strong></p>`;
+      } catch {}
+      msg += '<p style="color:#a3a3a3">You can close this tab. Restart george-gmail to load the new token persistently.</p>';
+      msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
+      return res.send(msg);
+    }
+
+    // Route to steve-personal handler if state=personal
+    if (state === 'personal') {
+      const { tokens } = await personalOauth2Client.getToken({ code, redirect_uri: PERSONAL_REDIRECT_URI });
+      personalOauth2Client.setCredentials(tokens);
+      personalGmail = google.gmail({ version: 'v1', auth: personalOauth2Client });
+      personalTasksApi = google.tasks({ version: 'v1', auth: personalOauth2Client });
+      personalDrive = google.drive({ version: 'v3', auth: personalOauth2Client });
+      personalDocs = google.docs({ version: 'v1', auth: personalOauth2Client });
+      personalSheets = google.sheets({ version: 'v4', auth: personalOauth2Client });
+      personalSlides = google.slides({ version: 'v1', auth: personalOauth2Client });
+      personalCalendar = google.calendar({ version: 'v3', auth: personalOauth2Client });
+      personalForms = google.forms({ version: 'v1', auth: personalOauth2Client });
+
+      let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
+      msg += '<h2 style="color:#4ade80">Steve Personal Authorized! (Full Google Workspace)</h2>';
+      if (tokens.refresh_token) {
+        msg += `<p><strong>New Personal Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
+        console.log(`[${AGENT_NAME}] Personal refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
+        try {
+          let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+          if (envContent.includes('PERSONAL_REFRESH_TOKEN=')) {
+            envContent = envContent.replace(/PERSONAL_REFRESH_TOKEN=.*/, `PERSONAL_REFRESH_TOKEN=${tokens.refresh_token}`);
+          } else {
+            envContent += `\n# Steve Personal (steveabramsdesigns@gmail.com) OAuth Token\nPERSONAL_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+          }
+          fs.writeFileSync(ENV_PATH, envContent);
+          msg += '<p style="color:#4ade80">Token auto-saved to central .env!</p>';
+          audit('steve-personal', 'oauth-authorized', { scopes: PERSONAL_SCOPES.length });
+        } catch (e) {
+          msg += `<p style="color:#f87171">Could not auto-save: ${e.message}</p>`;
+        }
+      }
+      try {
+        const profile = await personalGmail.users.getProfile({ userId: 'me' });
+        msg += `<p style="color:#60a5fa">Connected as: <strong>${profile.data.emailAddress}</strong></p>`;
+      } catch {}
+      msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
+      return res.send(msg);
+    }
+
+    // Route to info@ handler if state=info
+    if (state === 'info') {
+      const { tokens } = await infoOauth2Client.getToken({ code, redirect_uri: INFO_REDIRECT_URI });
+      infoOauth2Client.setCredentials(tokens);
+      infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
+      infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
+      infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
+
+      let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
+      msg += '<h2 style="color:#4ade80">Info@ Authorization Successful! (Gmail + Tasks + Drive)</h2>';
+      if (tokens.refresh_token) {
+        msg += `<p><strong>New Info@ Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
+        msg += '<p style="color:#f59e0b">Save this token to your .env file as INFO_REFRESH_TOKEN</p>';
+        console.log(`[${AGENT_NAME}] Info@ refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
+        try {
+          let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+          if (envContent.includes('INFO_REFRESH_TOKEN=')) {
+            envContent = envContent.replace(/INFO_REFRESH_TOKEN=.*/, `INFO_REFRESH_TOKEN=${tokens.refresh_token}`);
+          } else {
+            envContent += `\n# Info@ Account OAuth Token\nINFO_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+          }
+          fs.writeFileSync(ENV_PATH, envContent);
+          msg += '<p style="color:#4ade80">Token auto-saved to .env!</p>';
+        } catch (e) {
+          msg += `<p style="color:#f87171">Could not auto-save: ${e.message}</p>`;
+        }
+      }
+      try {
+        const profile = await infoGmail.users.getProfile({ userId: 'me' });
+        msg += `<p style="color:#60a5fa">Connected as: <strong>${profile.data.emailAddress}</strong></p>`;
+      } catch {}
+      msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
+      return res.send(msg);
+    }
+
+    // Default: steve@ account (now full Workspace)
+    const { tokens } = await oauth2Client.getToken({ code, redirect_uri: OAUTH_REDIRECT });
+    oauth2Client.setCredentials(tokens);
+    gmail = google.gmail({ version: 'v1', auth: oauth2Client });
+    tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
+    drive = google.drive({ version: 'v3', auth: oauth2Client });
+    docs = google.docs({ version: 'v1', auth: oauth2Client });
+    sheets = google.sheets({ version: 'v4', auth: oauth2Client });
+    slides = google.slides({ version: 'v1', auth: oauth2Client });
+    calendar = google.calendar({ version: 'v3', auth: oauth2Client });
+    forms = google.forms({ version: 'v1', auth: oauth2Client });
+
+    let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
+    msg += '<h2 style="color:#4ade80">Authorization Successful! (Full Google Workspace)</h2>';
+    if (tokens.refresh_token) {
+      msg += `<p><strong>New Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
+      console.log(`[${AGENT_NAME}] New refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
+      try {
+        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+        if (envContent.includes('GMAIL_REFRESH_TOKEN=')) {
+          envContent = envContent.replace(/GMAIL_REFRESH_TOKEN=.*/, `GMAIL_REFRESH_TOKEN=${tokens.refresh_token}`);
+        } else {
+          envContent += `\nGMAIL_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+        }
+        fs.writeFileSync(ENV_PATH, envContent);
+        msg += '<p style="color:#4ade80">Token auto-saved to central .env!</p>';
+        audit('steve-office', 'oauth-authorized', { scopes: FULL_WORKSPACE_SCOPES.length });
+      } catch (e) {
+        msg += `<p style="color:#f87171">Could not auto-save: ${e.message}</p>`;
+      }
+    } else {
+      msg += '<p>Access token refreshed (existing refresh token reused).</p>';
+    }
+    msg += '<p style="color:#4ade80">Full Google Workspace is now ready for steve@.</p>';
+    msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
+    res.send(msg);
+  } catch (e) {
+    console.error(`[${AGENT_NAME}] OAuth callback error:`, e.message);
+    res.status(500).send(`<html><body style="font-family:sans-serif;background:#0f1117;color:#f87171;padding:40px"><h2>Authorization Failed</h2><p>${e.message}</p><p><a href="/auth" style="color:#60a5fa">Try Again</a></p></body></html>`);
+  }
+});
+
+// ─── Info@ OAuth Authorization Flow ───
+app.get('/auth/info', (req, res) => {
+  if (!infoOauth2Client) return res.status(500).send('OAuth client not configured');
+  const url = infoOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: INFO_SCOPES,
+    prompt: 'consent',
+    redirect_uri: INFO_REDIRECT_URI,
+    login_hint: 'info@designerwallcoverings.com',
+    state: 'info',
+  });
+  res.redirect(url);
+});
+
+app.get('/api/auth-url/info', (req, res) => {
+  if (!infoOauth2Client) return res.status(500).json({ error: 'OAuth client not configured' });
+  const url = infoOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: INFO_SCOPES,
+    prompt: 'consent',
+    redirect_uri: INFO_REDIRECT_URI,
+    login_hint: 'info@designerwallcoverings.com',
+    state: 'info',
+  });
+  res.json({ authUrl: url, account: 'info@designerwallcoverings.com', scopes: INFO_SCOPES, instructions: 'Visit this URL in a browser where you are logged into info@designerwallcoverings.com. Approve access, then copy the code from the redirect URL and pass to /api/exchange-code/info?code=YOUR_CODE' });
+});
+
+// oauth2callback-info is handled by the unified /oauth2callback via state=info
+
+// ─── StevesClaude OAuth Authorization Flow ───
+app.get('/auth/stevesclaude', (req, res) => {
+  if (!stevesclaudeOauth2Client) return res.status(500).send('StevesClaude OAuth client not configured');
+  const url = stevesclaudeOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: FULL_WORKSPACE_SCOPES,
+    prompt: 'consent',
+    redirect_uri: STEVESCLAUDE_REDIRECT_URI,
+    login_hint: STEVESCLAUDE_LOGIN_HINT,
+    state: 'stevesclaude',
+  });
+  res.redirect(url);
+});
+
+app.get('/api/auth-url/stevesclaude', (req, res) => {
+  if (!stevesclaudeOauth2Client) return res.status(500).json({ error: 'StevesClaude OAuth client not configured' });
+  const url = stevesclaudeOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: FULL_WORKSPACE_SCOPES,
+    prompt: 'consent',
+    redirect_uri: STEVESCLAUDE_REDIRECT_URI,
+    login_hint: STEVESCLAUDE_LOGIN_HINT,
+    state: 'stevesclaude',
+  });
+  res.json({ authUrl: url, account: STEVESCLAUDE_LOGIN_HINT, scopes: FULL_WORKSPACE_SCOPES });
+});
+
+// ─── Steve Personal OAuth Authorization Flow ───
+app.get('/auth/steve-personal', (req, res) => {
+  if (!personalOauth2Client) return res.status(500).send('Personal OAuth client not configured');
+  const url = personalOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: PERSONAL_SCOPES,
+    prompt: 'consent',
+    redirect_uri: PERSONAL_REDIRECT_URI,
+    login_hint: 'steveabramsdesigns@gmail.com',
+    state: 'personal',
+  });
+  res.redirect(url);
+});
+
+app.get('/api/auth-url/steve-personal', (req, res) => {
+  if (!personalOauth2Client) return res.status(500).json({ error: 'Personal OAuth client not configured' });
+  const url = personalOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: PERSONAL_SCOPES,
+    prompt: 'consent',
+    redirect_uri: PERSONAL_REDIRECT_URI,
+    login_hint: 'steveabramsdesigns@gmail.com',
+    state: 'personal',
+  });
+  res.json({ authUrl: url, account: 'steveabramsdesigns@gmail.com', scopes: PERSONAL_SCOPES });
+});
+
+app.get('/api/exchange-code/steve-personal', async (req, res) => {
+  try {
+    const { code } = req.query;
+    if (!code) return res.status(400).json({ error: 'code parameter required' });
+    const { tokens } = await personalOauth2Client.getToken({ code, redirect_uri: PERSONAL_REDIRECT_URI });
+    personalOauth2Client.setCredentials(tokens);
+    personalGmail = google.gmail({ version: 'v1', auth: personalOauth2Client });
+    personalTasksApi = google.tasks({ version: 'v1', auth: personalOauth2Client });
+    personalDrive = google.drive({ version: 'v3', auth: personalOauth2Client });
+    personalDocs = google.docs({ version: 'v1', auth: personalOauth2Client });
+    personalSheets = google.sheets({ version: 'v4', auth: personalOauth2Client });
+    personalSlides = google.slides({ version: 'v1', auth: personalOauth2Client });
+    personalCalendar = google.calendar({ version: 'v3', auth: personalOauth2Client });
+    personalForms = google.forms({ version: 'v1', auth: personalOauth2Client });
+
+    const result = { success: true, account: 'steve-personal', hasRefreshToken: !!tokens.refresh_token };
+    if (tokens.refresh_token) {
+      result.refreshToken = tokens.refresh_token;
+      try {
+        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+        if (envContent.includes('PERSONAL_REFRESH_TOKEN=')) {
+          envContent = envContent.replace(/PERSONAL_REFRESH_TOKEN=.*/, `PERSONAL_REFRESH_TOKEN=${tokens.refresh_token}`);
+        } else {
+          envContent += `\n# Steve Personal (steveabramsdesigns@gmail.com) OAuth Token\nPERSONAL_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+        }
+        fs.writeFileSync(ENV_PATH, envContent);
+        result.savedToEnv = true;
+        audit('steve-personal', 'oauth-authorized', { scopes: PERSONAL_SCOPES.length });
+      } catch (e) {
+        result.savedToEnv = false;
+        result.envError = e.message;
+      }
+    }
+    try {
+      const profile = await personalGmail.users.getProfile({ userId: 'me' });
+      result.email = profile.data.emailAddress;
+      result.ready = true;
+    } catch (e) {
+      result.ready = false;
+      result.testError = e.message;
+    }
+    res.json(result);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/exchange-code/info', async (req, res) => {
+  try {
+    const { code } = req.query;
+    if (!code) return res.status(400).json({ error: 'code parameter required' });
+    const { tokens } = await infoOauth2Client.getToken({ code, redirect_uri: INFO_REDIRECT_URI });
+    infoOauth2Client.setCredentials(tokens);
+    infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
+    infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
+    infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
+
+    const result = { success: true, account: 'info@', hasRefreshToken: !!tokens.refresh_token };
+    if (tokens.refresh_token) {
+      result.refreshToken = tokens.refresh_token;
+      try {
+        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+        if (envContent.includes('INFO_REFRESH_TOKEN=')) {
+          envContent = envContent.replace(/INFO_REFRESH_TOKEN=.*/, `INFO_REFRESH_TOKEN=${tokens.refresh_token}`);
+        } else {
+          envContent += `\n# Info@ Account OAuth Token\nINFO_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+        }
+        fs.writeFileSync(ENV_PATH, envContent);
+        result.savedToEnv = true;
+      } catch (e) {
+        result.savedToEnv = false;
+        result.envError = e.message;
+      }
+    }
+    try {
+      const profile = await infoGmail.users.getProfile({ userId: 'me' });
+      result.email = profile.data.emailAddress;
+      result.ready = true;
+    } catch (e) {
+      result.ready = false;
+      result.testError = e.message;
+    }
+    res.json(result);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Manual Code Exchange (for localhost redirect flow) ───
+app.get('/api/exchange-code', async (req, res) => {
+  try {
+    const { code } = req.query;
+    if (!code) return res.status(400).json({ error: 'code parameter required' });
+    const { tokens } = await oauth2Client.getToken({ code, redirect_uri: OAUTH_REDIRECT });
+    oauth2Client.setCredentials(tokens);
+    gmail = google.gmail({ version: 'v1', auth: oauth2Client });
+    tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
+
+    const result = { success: true, hasRefreshToken: !!tokens.refresh_token };
+    if (tokens.refresh_token) {
+      result.refreshToken = tokens.refresh_token;
+      // Auto-save refresh token to .env
+      try {
+        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+        if (envContent.includes('GMAIL_REFRESH_TOKEN=')) {
+          envContent = envContent.replace(/GMAIL_REFRESH_TOKEN=.*/, `GMAIL_REFRESH_TOKEN=${tokens.refresh_token}`);
+        } else {
+          envContent += `\nGMAIL_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+        }
+        fs.writeFileSync(ENV_PATH, envContent);
+        result.savedToEnv = true;
+        console.log(`[${AGENT_NAME}] Refresh token saved to ${ENV_PATH}`);
+      } catch (e) {
+        result.savedToEnv = false;
+        result.envError = e.message;
+      }
+    }
+    // Quick test
+    try {
+      const profile = await gmail.users.getProfile({ userId: 'me' });
+      result.email = profile.data.emailAddress;
+      result.gmailReady = true;
+    } catch (e) {
+      result.gmailReady = false;
+      result.testError = e.message;
+    }
+    res.json(result);
+  } catch (e) {
+    console.error(`[${AGENT_NAME}] Code exchange error:`, e.message);
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Auth URL Generator (returns URL instead of redirecting) ───
+app.get('/api/auth-url', (req, res) => {
+  if (!oauth2Client) return res.status(500).json({ error: 'OAuth client not configured' });
+  const url = oauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: SCOPES,
+    prompt: 'consent',
+    redirect_uri: OAUTH_REDIRECT,
+  });
+  res.json({ authUrl: url, instructions: 'Visit this URL, approve access, then copy the full redirect URL from your browser address bar and pass the code parameter to /api/exchange-code?code=YOUR_CODE' });
+});
+
+// ─── Basic Auth (exempt health + OAuth routes) ───
+app.use((req, res, next) => {
+  const publicApiPaths = [
+    '/health', '/api/health', '/auth', '/auth/info', '/auth/steve-office', '/auth/steve-personal', '/auth/stevesclaude',
+    '/oauth2callback', '/oauth2callback-info',
+    '/api/exchange-code', '/api/exchange-code/info', '/api/exchange-code/steve-personal',
+    '/api/auth-url', '/api/auth-url/info', '/api/auth-url/steve-personal', '/api/auth-url/stevesclaude'
+  ];
+  if (publicApiPaths.includes(req.path)) return next();
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) return res.status(401).json({ error: 'Auth required' });
+  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
+  if (user !== 'admin' || pass !== 'DWSecure2024!') return res.status(401).json({ error: 'Invalid credentials' });
+  next();
+});
+
+// ─── Helper: decode email parts ───
+function decodeBody(parts) {
+  if (!parts) return '';
+  for (const part of parts) {
+    if (part.mimeType === 'text/plain' && part.body?.data) {
+      return Buffer.from(part.body.data, 'base64url').toString('utf8');
+    }
+    if (part.parts) {
+      const nested = decodeBody(part.parts);
+      if (nested) return nested;
+    }
+  }
+  return '';
+}
+
+function getHeader(headers, name) {
+  const h = (headers || []).find(h => h.name.toLowerCase() === name.toLowerCase());
+  return h ? h.value : '';
+}
+
+// ─── API: Health Check ───
+app.get('/api/health', (req, res) => {
+  res.json({
+    agent: AGENT_NAME,
+    codename: AGENT_CODENAME,
+    status: gmail ? 'ready' : 'no-gmail-client',
+    port: PORT,
+    hasCredentials: !!(GMAIL_CLIENT_ID && GMAIL_REFRESH_TOKEN),
+    uptime: process.uptime()
+  });
+});
+
+// ─── API: Status + Profile ───
+app.get('/api/status', (req, res) => {
+  res.json({
+    agent: AGENT_NAME,
+    codename: AGENT_CODENAME,
+    color: AGENT_COLOR,
+    port: PORT,
+    status: gmail ? 'online' : 'error',
+    hasCredentials: !!(GMAIL_CLIENT_ID && GMAIL_REFRESH_TOKEN),
+    capabilities: ['list', 'read', 'search', 'send', 'labels', 'attachments'],
+    envSource: ENV_PATH
+  });
+});
+
+// ─── API: Get Profile (email address, history) ───
+app.get('/api/profile', async (req, res) => {
+  try {
+    const profile = await gmail.users.getProfile({ userId: 'me' });
+    res.json(profile.data);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Raw headers for a message (debug auth/SPF/DKIM/DMARC) ───
+app.get('/api/messages/:id/headers', async (req, res) => {
+  try {
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+    const r = await g.users.messages.get({ userId: 'me', id: req.params.id, format: 'metadata' });
+    const headers = (r.data.payload?.headers || []).reduce((acc, h) => {
+      const k = h.name;
+      acc[k] = acc[k] ? `${acc[k]}\n${h.value}` : h.value;
+      return acc;
+    }, {});
+    res.json({ id: r.data.id, labelIds: r.data.labelIds, headers });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Trash a message ───
+app.post('/api/messages/:id/trash', async (req, res) => {
+  try {
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+    const result = await g.users.messages.trash({ userId: 'me', id: req.params.id });
+    audit(account, 'trash', { messageId: req.params.id });
+    res.json({ success: true, messageId: result.data.id, account });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Batch trash by query ───
+app.post('/api/messages/trash-by-query', async (req, res) => {
+  try {
+    const { q } = req.body;
+    if (!q) return res.status(400).json({ error: 'q (search query) required' });
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+    const list = await g.users.messages.list({ userId: 'me', q, maxResults: 500 });
+    const ids = (list.data.messages || []).map(m => m.id);
+    if (ids.length === 0) return res.json({ success: true, trashed: 0, ids: [] });
+    await g.users.messages.batchModify({ userId: 'me', requestBody: { ids, addLabelIds: ['TRASH'], removeLabelIds: ['INBOX'] } });
+    audit(account, 'batch-trash', { q, count: ids.length });
+    res.json({ success: true, trashed: ids.length, ids, account });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: List Labels ───
+app.get('/api/labels', async (req, res) => {
+  try {
+    const result = await gmail.users.labels.list({ userId: 'me' });
+    res.json(result.data.labels || []);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: List Messages ───
+app.get('/api/messages', async (req, res) => {
+  try {
+    const { q, maxResults, labelIds, pageToken } = req.query;
+    const { gmail: gm, key: account } = resolveAccount(req);
+    if (!gm) return res.status(400).json({ error: `unknown account: ${account}` });
+    const params = { userId: 'me', maxResults: parseInt(maxResults) || 20 };
+    if (q) params.q = q;
+    if (labelIds) params.labelIds = labelIds.split(',');
+    if (pageToken) params.pageToken = pageToken;
+
+    const list = await gm.users.messages.list(params);
+    const messages = list.data.messages || [];
+
+    // Fetch headers for each message (batch)
+    const detailed = await Promise.all(
+      messages.slice(0, parseInt(maxResults) || 20).map(async m => {
+        try {
+          const full = await gm.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
+          return {
+            id: full.data.id,
+            threadId: full.data.threadId,
+            snippet: full.data.snippet,
+            labelIds: full.data.labelIds,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            to: getHeader(full.data.payload?.headers, 'To'),
+            date: getHeader(full.data.payload?.headers, 'Date'),
+            internalDate: full.data.internalDate
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+
+    res.json({
+      messages: detailed,
+      nextPageToken: list.data.nextPageToken,
+      resultSizeEstimate: list.data.resultSizeEstimate
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Read Single Message ───
+app.get('/api/messages/:id', async (req, res) => {
+  try {
+    const { gmail: gm, key: account } = resolveAccount(req);
+    if (!gm) return res.status(400).json({ error: `unknown account: ${account}` });
+    const msg = await gm.users.messages.get({ userId: 'me', id: req.params.id, format: 'full' });
+    const headers = msg.data.payload?.headers || [];
+    const body = msg.data.payload?.body?.data
+      ? Buffer.from(msg.data.payload.body.data, 'base64url').toString('utf8')
+      : decodeBody(msg.data.payload?.parts);
+
+    const attachments = [];
+    function findAttachments(parts) {
+      if (!parts) return;
+      for (const p of parts) {
+        if (p.filename && p.body?.attachmentId) {
+          attachments.push({ filename: p.filename, mimeType: p.mimeType, size: p.body.size, attachmentId: p.body.attachmentId });
+        }
+        if (p.parts) findAttachments(p.parts);
+      }
+    }
+    findAttachments(msg.data.payload?.parts);
+
+    res.json({
+      id: msg.data.id,
+      threadId: msg.data.threadId,
+      labelIds: msg.data.labelIds,
+      snippet: msg.data.snippet,
+      subject: getHeader(headers, 'Subject'),
+      from: getHeader(headers, 'From'),
+      to: getHeader(headers, 'To'),
+      cc: getHeader(headers, 'Cc'),
+      date: getHeader(headers, 'Date'),
+      body,
+      attachments,
+      internalDate: msg.data.internalDate
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Search Messages ───
+app.get('/api/search', async (req, res) => {
+  try {
+    const { q, maxResults } = req.query;
+    if (!q) return res.status(400).json({ error: 'Query (q) required' });
+    const { gmail: gm, key: account } = resolveAccount(req);
+    if (!gm) return res.status(400).json({ error: `unknown account: ${account}` });
+    const list = await gm.users.messages.list({ userId: 'me', q, maxResults: parseInt(maxResults) || 10 });
+    const messages = list.data.messages || [];
+
+    const detailed = await Promise.all(
+      messages.map(async m => {
+        try {
+          const full = await gm.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date'] });
+          return {
+            id: full.data.id,
+            snippet: full.data.snippet,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            date: getHeader(full.data.payload?.headers, 'Date')
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+
+    res.json({ query: q, messages: detailed, total: list.data.resultSizeEstimate });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Send Email ───
+// RFC 2047 encoded-word for any header containing non-ASCII (em-dash, curly quotes, accented chars, etc.)
+function encodeMimeHeader(s) {
+  if (!s) return s;
+  // eslint-disable-next-line no-control-regex
+  return /[^\x00-\x7F]/.test(s)
+    ? `=?UTF-8?B?${Buffer.from(s, 'utf8').toString('base64')}?=`
+    : s;
+}
+
+// Tag every outbound body with a "where-it's-from" header — so Steve can spot
+// at a glance which job/script/agent triggered the email. Source comes from
+// req.body.source (preferred), req.body.job, req.headers['x-george-source'],
+// or falls back to the user-agent string.
+function inferSource(body, req) {
+  const raw = body.source || body.job || body.from_job || body.origin
+           || req?.headers?.['x-george-source']
+           || req?.headers?.['user-agent']
+           || 'unspecified';
+  return String(raw).slice(0, 120);
+}
+function withSourceFooter(htmlBody, source) {
+  const ts = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
+  const tag = `<div style="font-family:ui-monospace,Menlo,monospace;font-size:10px;letter-spacing:.10em;text-transform:uppercase;color:#7a766f;padding:8px 12px;background:rgba(212,160,74,0.08);border-left:3px solid #d4a04a;margin-bottom:18px;border-radius:2px"><strong style="color:#d4a04a">From job:</strong> ${escapeHtml(source)} <span style="color:#a8a39c">·</span> ${ts}</div>`;
+  return tag + (htmlBody || '');
+}
+function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
+
+// Build a UTF-8-clean RFC 2822 message: encoded subject, MIME-Version, base64 body (76-char lines per RFC 2045).
+// inReplyTo / references are RFC Message-Id values (Gmail returns these from messages.get headers).
+function buildRawMessage({ from, to, cc, bcc, subject, body, inReplyTo, references }) {
+  const bodyB64 = Buffer.from(body, 'utf8').toString('base64').replace(/(.{76})/g, '$1\r\n');
+  const refsHeader = references && references.length ? references.join(' ') : (inReplyTo || null);
+  const headerLines = [
+    from ? `From: ${from}` : null,
+    to ? `To: ${to}` : null,
+    cc ? `Cc: ${cc}` : null,
+    bcc ? `Bcc: ${bcc}` : null,
+    `Subject: ${encodeMimeHeader(subject)}`,
+    inReplyTo ? `In-Reply-To: ${inReplyTo}` : null,
+    refsHeader ? `References: ${refsHeader}` : null,
+    'MIME-Version: 1.0',
+    'Content-Type: text/html; charset=UTF-8',
+    'Content-Transfer-Encoding: base64',
+  ].filter(Boolean);
+  const message = headerLines.join('\r\n') + '\r\n\r\n' + bodyB64;
+  return Buffer.from(message, 'utf8').toString('base64url');
+}
+
+// Look up RFC Message-Id + thread for a previously-sent message id, so callers
+// can reply in-thread by passing only the gmail message id (not the raw header).
+async function resolveReplyContext(gmail, replyToMessageId) {
+  const m = await gmail.users.messages.get({
+    userId: 'me', id: replyToMessageId, format: 'metadata',
+    metadataHeaders: ['Message-Id', 'References', 'Subject'],
+  });
+  const headers = m.data.payload?.headers || [];
+  const find = (n) => headers.find(h => h.name.toLowerCase() === n.toLowerCase())?.value || null;
+  const messageIdHdr = find('Message-Id');
+  const refs = find('References');
+  const subject = find('Subject') || '';
+  return {
+    threadId: m.data.threadId,
+    messageIdHeader: messageIdHdr,
+    referencesHeader: [refs, messageIdHdr].filter(Boolean).join(' ').trim() || null,
+    subject,
+  };
+}
+
+app.post('/api/send', async (req, res) => {
+  try {
+    const { from, to, subject, body, cc, bcc, threadId: bodyThreadId, inReplyTo: bodyInReplyTo, replyToMessageId, references } = req.body;
+    if (!to || !body) return res.status(400).json({ error: 'to and body required' });
+
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+
+    let threadId = bodyThreadId || null;
+    let inReplyTo = bodyInReplyTo || null;
+    let refsArr = Array.isArray(references) ? references.slice() : null;
+    let finalSubject = subject;
+
+    // Convenience: caller supplied just the gmail message id of what to reply to → we resolve everything.
+    if (replyToMessageId) {
+      const ctx = await resolveReplyContext(g, replyToMessageId);
+      threadId = threadId || ctx.threadId;
+      inReplyTo = inReplyTo || ctx.messageIdHeader;
+      if (!refsArr && ctx.referencesHeader) refsArr = ctx.referencesHeader.split(/\s+/);
+      if (!finalSubject) finalSubject = ctx.subject?.startsWith('Re:') ? ctx.subject : `Re: ${ctx.subject || ''}`.trim();
+    }
+    if (!finalSubject) return res.status(400).json({ error: 'subject required (or pass replyToMessageId)' });
+
+    const source = inferSource(req.body, req);
+    const taggedBody = withSourceFooter(body, source);
+    const encoded = buildRawMessage({ from, to, cc, bcc, subject: finalSubject, body: taggedBody, inReplyTo, references: refsArr });
+    const requestBody = { raw: encoded };
+    if (threadId) requestBody.threadId = threadId;
+    const result = await g.users.messages.send({ userId: 'me', requestBody });
+    audit(account, 'send', { from, to, subject: finalSubject, source, messageId: result.data.id, threadId: result.data.threadId, inReplyTo: inReplyTo || null });
+    res.json({ success: true, messageId: result.data.id, threadId: result.data.threadId, account, source, inThread: Boolean(threadId || inReplyTo) });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: List Send-As aliases ───
+app.get('/api/aliases', async (req, res) => {
+  try {
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+    const result = await g.users.settings.sendAs.list({ userId: 'me' });
+    res.json({ account, aliases: result.data.sendAs || [] });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Create Send-As alias (triggers verification email) ───
+// Body: { sendAsEmail, displayName?, treatAsAlias?, smtp?: {host,port,username,password,securityMode} }
+// Without smtp, Gmail relays through its own servers (sender shown as "via gmail.com" in some clients).
+// With smtp, relays through that SMTP server (cleaner DKIM-from-source-domain look).
+app.post('/api/aliases/create', async (req, res) => {
+  try {
+    const { sendAsEmail, displayName, treatAsAlias, smtp } = req.body || {};
+    if (!sendAsEmail) return res.status(400).json({ error: 'sendAsEmail required' });
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+    const requestBody = { sendAsEmail, treatAsAlias: treatAsAlias !== false };
+    if (displayName) requestBody.displayName = displayName;
+    if (smtp && smtp.host) {
+      requestBody.smtpMsa = {
+        host: smtp.host,
+        port: smtp.port || 465,
+        username: smtp.username,
+        password: smtp.password,
+        securityMode: smtp.securityMode || 'ssl',
+      };
+    }
+    const result = await g.users.settings.sendAs.create({ userId: 'me', requestBody });
+    audit(account, 'alias-create', { sendAsEmail, smtp: !!smtp });
+    res.json({ success: true, account, alias: { sendAsEmail: result.data.sendAsEmail, verificationStatus: result.data.verificationStatus, isPrimary: result.data.isPrimary, treatAsAlias: result.data.treatAsAlias } });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Verify Send-As alias by extracting verify link from inbox + fetching it ───
+// Body: { sendAsEmail, account: "main"|"info" } — searches for the most recent verification mail and clicks it
+app.post('/api/aliases/verify-from-inbox', async (req, res) => {
+  try {
+    const { sendAsEmail } = req.body || {};
+    if (!sendAsEmail) return res.status(400).json({ error: 'sendAsEmail required' });
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+    // Search for the most recent verification email related to this address
+    const q = `from:noreply-confirmation@google.com OR from:gmail-noreply@google.com to:${sendAsEmail} subject:Confirmation newer_than:1d`;
+    const list = await g.users.messages.list({ userId: 'me', q, maxResults: 5 });
+    const messages = list.data.messages || [];
+    if (messages.length === 0) {
+      // Fallback: any subaddress version of the from address
+      const m = sendAsEmail.match(/^([^@]+)@(.+)$/);
+      if (m) {
+        const tagged = `steve+${m[2].replace(/\./g, '-')}@designerwallcoverings.com`;
+        const q2 = `to:${tagged} subject:Confirmation newer_than:1d`;
+        const r2 = await g.users.messages.list({ userId: 'me', q: q2, maxResults: 5 });
+        if (r2.data.messages) messages.push(...r2.data.messages);
+      }
+    }
+    if (messages.length === 0) return res.status(404).json({ error: 'no verification email found in last 24h', sendAsEmail });
+    // Get the most recent message body
+    const msg = await g.users.messages.get({ userId: 'me', id: messages[0].id, format: 'full' });
+    function findUrl(payload) {
+      if (!payload) return null;
+      if (payload.body && payload.body.data) {
+        const text = Buffer.from(payload.body.data, 'base64url').toString('utf8');
+        const urls = text.match(/https?:\/\/[^\s)>"]*ConfirmAddress[^\s)>"]*/g) || text.match(/https?:\/\/mail-settings\.google\.com[^\s)>"]+/g);
+        if (urls) return urls[0];
+      }
+      if (payload.parts) for (const p of payload.parts) { const u = findUrl(p); if (u) return u; }
+      return null;
+    }
+    const url = findUrl(msg.data.payload);
+    if (!url) return res.status(404).json({ error: 'no verify URL in message', messageId: messages[0].id });
+    // Fetch URL = clicks verify
+    const r = await fetch(url, { redirect: 'follow' }).catch(e => ({ ok: false, status: 0, error: e.message }));
+    audit(account, 'alias-verify-clicked', { sendAsEmail, status: r.status });
+    res.json({ success: true, account, sendAsEmail, verifyUrl: url.slice(0, 80) + '...', clickStatus: r.status });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Configure Send-As alias smtpMsa to use PurelyMail SMTP ───
+// Body: { sendAsEmail: "info@<domain>" }
+// Uses relay@hospitalitywallcoverings.com PM SMTP creds (password from /root/.purelymail-relay-pw)
+app.post('/api/aliases/configure-smtp', async (req, res) => {
+  try {
+    const { sendAsEmail } = req.body;
+    if (!sendAsEmail) return res.status(400).json({ error: 'sendAsEmail required' });
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+    let pmPass;
+    try { pmPass = fs.readFileSync('/root/.purelymail-relay-pw', 'utf8').trim(); }
+    catch { return res.status(500).json({ error: 'PM relay password file missing on server' }); }
+    const result = await g.users.settings.sendAs.update({
+      userId: 'me',
+      sendAsEmail,
+      requestBody: {
+        sendAsEmail,
+        treatAsAlias: true,
+        smtpMsa: {
+          host: 'smtp.purelymail.com',
+          port: 465,
+          username: 'relay@hospitalitywallcoverings.com',
+          password: pmPass,
+          securityMode: 'ssl'
+        }
+      }
+    });
+    audit(account, 'configure-smtp', { sendAsEmail });
+    // Don't return the password back — Gmail's response also has it as null
+    res.json({ success: true, account, sendAsEmail, smtpMsa: result.data.smtpMsa, treatAsAlias: result.data.treatAsAlias });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Send with Attachments ───
+// Body: { to, cc?, bcc?, subject, body (html), attachments: [{ filename, content_base64, mime_type? }], account?: "main"|"info" }
+app.post('/api/send-with-attachment', async (req, res) => {
+  try {
+    const { to, cc, bcc, subject, body, attachments, account } = req.body;
+    if (!to || !subject || !body) return res.status(400).json({ error: 'to, subject, and body required' });
+    if (!attachments || !Array.isArray(attachments) || attachments.length === 0) {
+      return res.status(400).json({ error: 'attachments array required' });
+    }
+    const useInfo = account === 'info';
+    const authClient = useInfo ? infoOauth2Client : oauth2Client;
+    const gmailClient = useInfo ? infoGmail : gmail;
+    if (!authClient || !gmailClient) {
+      return res.status(500).json({ error: `${useInfo ? 'info' : 'main'} account not configured` });
+    }
+
+    // Build MIME with nodemailer (handles UTF-8 subjects, multipart, base64 chunking correctly)
+    const profile = await gmailClient.users.getProfile({ userId: 'me' });
+    const fromAddr = profile.data.emailAddress;
+    const source = inferSource(req.body, req);
+    const taggedBody = withSourceFooter(body, source);
+    const mailOpts = {
+      from: fromAddr,
+      to,
+      subject,
+      html: taggedBody,
+      attachments: attachments.map(a => ({
+        filename: a.filename || 'attachment.bin',
+        content: Buffer.from(a.content_base64, 'base64'),
+        contentType: a.mime_type || 'application/octet-stream',
+      })),
+    };
+    if (cc) mailOpts.cc = cc;
+    if (bcc) mailOpts.bcc = bcc;
+
+    const mime = await new Promise((resolve, reject) => {
+      const mail = nodemailer.createTransport({ streamTransport: true, buffer: true }).sendMail(
+        mailOpts,
+        (err, info) => (err ? reject(err) : resolve(info.message))
+      );
+    });
+    const raw = Buffer.from(mime).toString('base64url');
+    const result = await gmailClient.users.messages.send({ userId: 'me', requestBody: { raw } });
+    res.json({
+      success: true,
+      messageId: result.data.id,
+      threadId: result.data.threadId,
+      from: fromAddr,
+      account: useInfo ? 'info' : 'main',
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Create Draft ───
+app.post('/api/drafts', async (req, res) => {
+  try {
+    const { to, subject, body, cc, bcc } = req.body;
+    if (!subject || !body) return res.status(400).json({ error: 'subject and body required' });
+
+    const { gmail: g, key: account } = resolveAccount(req);
+    if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
+
+    const source = inferSource(req.body, req);
+    const taggedBody = withSourceFooter(body, source);
+    const encoded = buildRawMessage({ to, cc, bcc, subject, body: taggedBody });
+    const result = await g.users.drafts.create({ userId: 'me', requestBody: { message: { raw: encoded } } });
+    audit(account, 'draft', { to, subject, source, draftId: result.data.id });
+    res.json({ success: true, draftId: result.data.id, messageId: result.data.message?.id, account, source });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: List Drafts ───
+app.get('/api/drafts', async (req, res) => {
+  try {
+    const result = await gmail.users.drafts.list({ userId: 'me', maxResults: parseInt(req.query.maxResults) || 10 });
+    res.json(result.data.drafts || []);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Get Attachment ───
+app.get('/api/messages/:messageId/attachments/:attachmentId', async (req, res) => {
+  try {
+    const att = await gmail.users.messages.attachments.get({
+      userId: 'me', messageId: req.params.messageId, id: req.params.attachmentId
+    });
+    res.json({ data: att.data.data, size: att.data.size });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Slack Webhook for notifications ───
+const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL || '';
+function slackNotify(text) {
+  try {
+    const https = require('https');
+    const url = new URL(SLACK_WEBHOOK);
+    const payload = JSON.stringify({ text });
+    const req = https.request({ hostname: url.hostname, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } });
+    req.write(payload);
+    req.end();
+  } catch {}
+}
+
+// ─── Google Tasks API ───
+app.get('/api/tasks/lists', async (req, res) => {
+  try {
+    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
+    const result = await tasksApi.tasklists.list({ maxResults: 100 });
+    res.json({ lists: result.data.items || [] });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/tasks/:listId', async (req, res) => {
+  try {
+    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
+    const showCompleted = req.query.showCompleted !== 'false';
+    const showHidden = req.query.showHidden === 'true';
+    const result = await tasksApi.tasks.list({
+      tasklist: req.params.listId,
+      maxResults: parseInt(req.query.maxResults) || 100,
+      showCompleted,
+      showHidden,
+    });
+    const tasks = (result.data.items || []).map(t => ({
+      id: t.id,
+      title: t.title,
+      notes: t.notes || null,
+      status: t.status,
+      due: t.due || null,
+      completed: t.completed || null,
+      updated: t.updated,
+      parent: t.parent || null,
+      position: t.position,
+      links: t.links || [],
+    }));
+    res.json({ listId: req.params.listId, count: tasks.length, tasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Convenience: get all tasks across all lists
+app.get('/api/tasks', async (req, res) => {
+  try {
+    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
+    const listsResult = await tasksApi.tasklists.list({ maxResults: 100 });
+    const lists = listsResult.data.items || [];
+    const allTasks = [];
+    for (const list of lists) {
+      const tasksResult = await tasksApi.tasks.list({
+        tasklist: list.id,
+        maxResults: 100,
+        showCompleted: req.query.showCompleted !== 'false',
+      });
+      const items = (tasksResult.data.items || []).map(t => ({
+        id: t.id,
+        title: t.title,
+        notes: t.notes || null,
+        status: t.status,
+        due: t.due || null,
+        completed: t.completed || null,
+        updated: t.updated,
+        listId: list.id,
+        listTitle: list.title,
+      }));
+      allTasks.push(...items);
+    }
+    res.json({ totalLists: lists.length, totalTasks: allTasks.length, tasks: allTasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Tasks API ───
+app.get('/api/info/tasks/lists', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const result = await infoTasksApi.tasklists.list({ maxResults: 100 });
+    res.json({ account: 'info@designerwallcoverings.com', lists: result.data.items || [] });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Info@ tasks scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/info/tasks/:listId', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const showCompleted = req.query.showCompleted !== 'false';
+    const showHidden = req.query.showHidden === 'true';
+    const result = await infoTasksApi.tasks.list({
+      tasklist: req.params.listId,
+      maxResults: parseInt(req.query.maxResults) || 100,
+      showCompleted,
+      showHidden,
+    });
+    const tasks = (result.data.items || []).map(t => ({
+      id: t.id,
+      title: t.title,
+      notes: t.notes || null,
+      status: t.status,
+      due: t.due || null,
+      completed: t.completed || null,
+      updated: t.updated,
+      parent: t.parent || null,
+      position: t.position,
+      links: t.links || [],
+    }));
+    res.json({ account: 'info@designerwallcoverings.com', listId: req.params.listId, count: tasks.length, tasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Info@ tasks scope not authorized.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/info/tasks', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const listsResult = await infoTasksApi.tasklists.list({ maxResults: 100 });
+    const lists = listsResult.data.items || [];
+    const allTasks = [];
+    for (const list of lists) {
+      const tasksResult = await infoTasksApi.tasks.list({
+        tasklist: list.id,
+        maxResults: 100,
+        showCompleted: req.query.showCompleted !== 'false',
+      });
+      const items = (tasksResult.data.items || []).map(t => ({
+        id: t.id,
+        title: t.title,
+        notes: t.notes || null,
+        status: t.status,
+        due: t.due || null,
+        completed: t.completed || null,
+        updated: t.updated,
+        listId: list.id,
+        listTitle: list.title,
+      }));
+      allTasks.push(...items);
+    }
+    res.json({ account: 'info@designerwallcoverings.com', totalLists: lists.length, totalTasks: allTasks.length, tasks: allTasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Info@ tasks scope not authorized.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Tasks Write Endpoints ───
+// Create a new task list
+app.post('/api/info/tasks/lists/create', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { title } = req.body;
+    if (!title) return res.status(400).json({ error: 'title is required' });
+    const result = await infoTasksApi.tasklists.insert({ requestBody: { title } });
+    res.json({ success: true, list: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info to upgrade from tasks.readonly to tasks.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Find a task list by name, or create it if it doesn't exist
+app.post('/api/info/tasks/lists/find-or-create', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { title } = req.body;
+    if (!title) return res.status(400).json({ error: 'title is required' });
+
+    // Search existing lists
+    const listsResult = await infoTasksApi.tasklists.list({ maxResults: 100 });
+    const existing = (listsResult.data.items || []).find(l => l.title === title);
+    if (existing) {
+      return res.json({ success: true, created: false, list: existing });
+    }
+
+    // Create new list
+    const result = await infoTasksApi.tasklists.insert({ requestBody: { title } });
+    res.json({ success: true, created: true, list: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Move a task from one list to another (insert into dest + delete from source)
+// Google Tasks API has no cross-list move — we must copy then delete
+app.post('/api/info/tasks/move', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { sourceListId, taskId, destListId, markCompleted } = req.body;
+    if (!sourceListId || !taskId || !destListId) {
+      return res.status(400).json({ error: 'sourceListId, taskId, and destListId are required' });
+    }
+
+    // 1. Get the task from source list
+    const taskResult = await infoTasksApi.tasks.get({ tasklist: sourceListId, task: taskId });
+    const task = taskResult.data;
+
+    // 2. Insert into destination list (preserve title, notes, due, links)
+    const newTaskBody = {
+      title: task.title,
+      notes: task.notes || undefined,
+      due: task.due || undefined,
+      status: markCompleted ? 'completed' : 'needsAction',
+    };
+    const insertResult = await infoTasksApi.tasks.insert({ tasklist: destListId, requestBody: newTaskBody });
+
+    // 3. Delete from source list
+    await infoTasksApi.tasks.delete({ tasklist: sourceListId, task: taskId });
+
+    res.json({
+      success: true,
+      moved: true,
+      sourceListId,
+      destListId,
+      originalTaskId: taskId,
+      newTaskId: insertResult.data.id,
+      title: task.title,
+    });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Update a task's title (e.g., prepend "READY" status)
+app.post('/api/info/tasks/:listId/:taskId/update', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { listId, taskId } = req.params;
+    const { title, notes } = req.body;
+    const body = {};
+    if (title) body.title = title;
+    if (notes !== undefined) body.notes = notes;
+    if (!Object.keys(body).length) return res.status(400).json({ error: 'title or notes required' });
+    const result = await infoTasksApi.tasks.patch({
+      tasklist: listId,
+      task: taskId,
+      requestBody: body
+    });
+    res.json({ success: true, task: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Complete a task (mark as done without moving)
+app.post('/api/info/tasks/:listId/:taskId/complete', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { listId, taskId } = req.params;
+    const result = await infoTasksApi.tasks.patch({
+      tasklist: listId,
+      task: taskId,
+      requestBody: { status: 'completed' }
+    });
+    res.json({ success: true, task: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Gmail Endpoints ───
+app.get('/api/info/profile', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const profile = await infoGmail.users.getProfile({ userId: 'me' });
+    res.json({ account: 'info@designerwallcoverings.com', ...profile.data });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/info/messages', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const { q, maxResults, labelIds, pageToken } = req.query;
+    const params = { userId: 'me', maxResults: parseInt(maxResults) || 20 };
+    if (q) params.q = q;
+    if (labelIds) params.labelIds = labelIds.split(',');
+    if (pageToken) params.pageToken = pageToken;
+
+    const list = await infoGmail.users.messages.list(params);
+    const messages = list.data.messages || [];
+    const detailed = await Promise.all(
+      messages.slice(0, parseInt(maxResults) || 20).map(async m => {
+        try {
+          const full = await infoGmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
+          return {
+            id: full.data.id,
+            threadId: full.data.threadId,
+            snippet: full.data.snippet,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            to: getHeader(full.data.payload?.headers, 'To'),
+            date: getHeader(full.data.payload?.headers, 'Date'),
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+    res.json({ account: 'info@designerwallcoverings.com', messages: detailed, nextPageToken: list.data.nextPageToken });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Google Drive Endpoints ───
+const DRIVE_NOT_READY = { error: 'Info@ Drive not initialized — authorize at /auth/info (needs drive.readonly scope)', authUrl: '/auth/info' };
+
+// List files (supports Drive query syntax in ?q= param)
+app.get('/api/info/drive', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const { q, maxResults, pageToken, orderBy } = req.query;
+    const params = {
+      pageSize: parseInt(maxResults) || 25,
+      fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, webViewLink, iconLink, parents, shared)',
+      orderBy: orderBy || 'modifiedTime desc',
+    };
+    if (q) params.q = q;
+    if (pageToken) params.pageToken = pageToken;
+    const result = await infoDrive.files.list(params);
+    res.json({ account: 'info@', files: result.data.files || [], nextPageToken: result.data.nextPageToken });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Get file metadata by ID
+app.get('/api/info/drive/file/:fileId', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const result = await infoDrive.files.get({
+      fileId: req.params.fileId,
+      fields: 'id, name, mimeType, size, modifiedTime, createdTime, webViewLink, webContentLink, iconLink, parents, shared, description',
+    });
+    res.json(result.data);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Search files by name
+app.get('/api/info/drive/search/:query', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const searchQ = `name contains '${req.params.query.replace(/'/g, "\\'")}'`;
+    const result = await infoDrive.files.list({
+      q: searchQ,
+      pageSize: parseInt(req.query.maxResults) || 25,
+      fields: 'files(id, name, mimeType, size, modifiedTime, webViewLink, parents)',
+      orderBy: 'modifiedTime desc',
+    });
+    res.json({ account: 'info@', query: req.params.query, files: result.data.files || [] });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// List shared drives
+app.get('/api/info/drive/shared', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const result = await infoDrive.drives.list({ pageSize: 50 });
+    res.json({ account: 'info@', drives: result.data.drives || [] });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Single Message (with HTML body) ───
+function decodeHtmlBody(parts) {
+  if (!parts) return { html: '', text: '' };
+  let html = '', text = '';
+  for (const part of parts) {
+    if (part.mimeType === 'text/html' && part.body?.data) {
+      html = Buffer.from(part.body.data, 'base64url').toString('utf8');
+    }
+    if (part.mimeType === 'text/plain' && part.body?.data) {
+      text = Buffer.from(part.body.data, 'base64url').toString('utf8');
+    }
+    if (part.parts) {
+      const nested = decodeHtmlBody(part.parts);
+      if (nested.html && !html) html = nested.html;
+      if (nested.text && !text) text = nested.text;
+    }
+  }
+  return { html, text };
+}
+
+app.get('/api/info/messages/:id', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
+    const msg = await infoGmail.users.messages.get({ userId: 'me', id: req.params.id, format: 'full' });
+    const headers = msg.data.payload?.headers || [];
+
+    // Decode both HTML and text bodies
+    const topBody = msg.data.payload?.body?.data
+      ? Buffer.from(msg.data.payload.body.data, 'base64url').toString('utf8')
+      : null;
+    const { html, text } = decodeHtmlBody(msg.data.payload?.parts);
+
+    const attachments = [];
+    function findAttachments(parts) {
+      if (!parts) return;
+      for (const p of parts) {
+        if (p.filename && p.body?.attachmentId) {
+          attachments.push({ filename: p.filename, mimeType: p.mimeType, size: p.body.size, attachmentId: p.body.attachmentId });
+        }
+        if (p.parts) findAttachments(p.parts);
+      }
+    }
+    findAttachments(msg.data.payload?.parts);
+
+    res.json({
+      id: msg.data.id,
+      threadId: msg.data.threadId,
+      labelIds: msg.data.labelIds,
+      snippet: msg.data.snippet,
+      subject: getHeader(headers, 'Subject'),
+      from: getHeader(headers, 'From'),
+      to: getHeader(headers, 'To'),
+      cc: getHeader(headers, 'Cc'),
+      date: getHeader(headers, 'Date'),
+      bodyHtml: html || topBody || '',
+      bodyText: text || topBody || '',
+      attachments,
+      internalDate: msg.data.internalDate
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Search Messages ───
+app.get('/api/info/search', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
+    const { q, maxResults } = req.query;
+    if (!q) return res.status(400).json({ error: 'Query (q) required' });
+    const list = await infoGmail.users.messages.list({ userId: 'me', q, maxResults: parseInt(maxResults) || 10 });
+    const messages = list.data.messages || [];
+
+    const detailed = await Promise.all(
+      messages.map(async m => {
+        try {
+          const full = await infoGmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
+          return {
+            id: full.data.id,
+            snippet: full.data.snippet,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            to: getHeader(full.data.payload?.headers, 'To'),
+            date: getHeader(full.data.payload?.headers, 'Date')
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+
+    res.json({ account: 'info@designerwallcoverings.com', query: q, messages: detailed, total: list.data.resultSizeEstimate });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Download Attachment ───
+app.get('/api/info/messages/:messageId/attachments/:attachmentId', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
+    const att = await infoGmail.users.messages.attachments.get({
+      userId: 'me', messageId: req.params.messageId, id: req.params.attachmentId
+    });
+    const buf = Buffer.from(att.data.data, 'base64');
+    res.set('Content-Length', buf.length);
+    res.send(buf);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ══════════════════════════════════════════════════════════════
+// ─── Unified Google Workspace API (per-account) ───
+// Endpoints below accept ?account=steve-office|info|steve-personal
+// Default account = steve-office.
+// ══════════════════════════════════════════════════════════════
+
+function requireService(svc, svcName, account, res) {
+  if (!svc) {
+    res.status(503).json({ error: `${svcName} not ready for account '${account}' — authorize via /auth/${account}` });
+    return false;
+  }
+  return true;
+}
+
+// ─── Google Docs ───
+app.post('/api/docs/create', async (req, res) => {
+  try {
+    const { docs: d, key, label } = resolveAccount(req);
+    if (!requireService(d, 'Docs', key, res)) return;
+    const { title = 'Untitled' } = req.body || {};
+    const result = await d.documents.create({ requestBody: { title } });
+    audit(key, 'docs.create', { documentId: result.data.documentId, title });
+    res.json({ account: key, email: label, documentId: result.data.documentId, title, url: `https://docs.google.com/document/d/${result.data.documentId}/edit` });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/docs/:id', async (req, res) => {
+  try {
+    const { docs: d, key, label } = resolveAccount(req);
+    if (!requireService(d, 'Docs', key, res)) return;
+    const result = await d.documents.get({ documentId: req.params.id });
+    res.json({ account: key, email: label, document: result.data });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/docs/:id/append', async (req, res) => {
+  try {
+    const { docs: d, key } = resolveAccount(req);
+    if (!requireService(d, 'Docs', key, res)) return;
+    const { text = '' } = req.body || {};
+    // Get end index
+    const meta = await d.documents.get({ documentId: req.params.id });
+    const endIndex = meta.data.body?.content?.slice(-1)[0]?.endIndex || 1;
+    await d.documents.batchUpdate({
+      documentId: req.params.id,
+      requestBody: { requests: [{ insertText: { location: { index: Math.max(endIndex - 1, 1) }, text } }] }
+    });
+    audit(key, 'docs.append', { documentId: req.params.id, length: text.length });
+    res.json({ account: key, documentId: req.params.id, appendedChars: text.length });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/docs/:id/replace', async (req, res) => {
+  try {
+    const { docs: d, key } = resolveAccount(req);
+    if (!requireService(d, 'Docs', key, res)) return;
+    const { text = '' } = req.body || {};
+    // Wipe body then insert — delete range then insertText
+    const meta = await d.documents.get({ documentId: req.params.id });
+    const endIndex = meta.data.body?.content?.slice(-1)[0]?.endIndex || 1;
+    const requests = [];
+    if (endIndex > 2) {
+      requests.push({ deleteContentRange: { range: { startIndex: 1, endIndex: endIndex - 1 } } });
+    }
+    if (text) requests.push({ insertText: { location: { index: 1 }, text } });
+    if (requests.length) await d.documents.batchUpdate({ documentId: req.params.id, requestBody: { requests } });
+    audit(key, 'docs.replace', { documentId: req.params.id, length: text.length });
+    res.json({ account: key, documentId: req.params.id, length: text.length });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ─── Markdown → Google Docs formatter ───
+// Parses a subset of Markdown (# headings, - bullets, > blockquotes, **bold**,
+// `code`, [link](url), ---) into Google Docs batchUpdate requests.
+function renderMarkdownToDocs(md) {
+  const lines = md.split('\n');
+  let text = '';
+  const paraRanges = [];
+  const textRanges = [];
+  let cursor = 1; // Docs indexing starts at 1
+
+  for (const raw of lines) {
+    let content = raw;
+    let namedStyleType = null;
+    let isBullet = false;
+    let isQuote = false;
+
+    if (raw.startsWith('### ')) { content = raw.slice(4); namedStyleType = 'HEADING_3'; }
+    else if (raw.startsWith('## ')) { content = raw.slice(3); namedStyleType = 'HEADING_2'; }
+    else if (raw.startsWith('# ')) { content = raw.slice(2); namedStyleType = 'HEADING_1'; }
+    else if (raw.startsWith('- ') || raw.startsWith('* ')) { content = raw.slice(2); isBullet = true; }
+    else if (raw.startsWith('> ')) { content = raw.slice(2); isQuote = true; }
+    else if (raw === '---' || raw === '***' || raw === '___') { content = ''; }
+
+    // Inline parse: **bold**, `code`, [text](url)
+    let stripped = '';
+    let i = 0;
+    while (i < content.length) {
+      if (content.slice(i, i + 2) === '**') {
+        const end = content.indexOf('**', i + 2);
+        if (end > 0) {
+          const inner = content.slice(i + 2, end);
+          textRanges.push({ start: cursor + stripped.length, end: cursor + stripped.length + inner.length, bold: true });
+          stripped += inner; i = end + 2; continue;
+        }
+      }
+      if (content[i] === '`') {
+        const end = content.indexOf('`', i + 1);
+        if (end > i) {
+          const inner = content.slice(i + 1, end);
+          textRanges.push({ start: cursor + stripped.length, end: cursor + stripped.length + inner.length, code: true });
+          stripped += inner; i = end + 1; continue;
+        }
+      }
+      if (content[i] === '[') {
+        const rb = content.indexOf(']', i + 1);
+        if (rb > 0 && content[rb + 1] === '(') {
+          const rp = content.indexOf(')', rb + 1);
+          if (rp > 0) {
+            const linkText = content.slice(i + 1, rb);
+            const url = content.slice(rb + 2, rp);
+            textRanges.push({ start: cursor + stripped.length, end: cursor + stripped.length + linkText.length, link: url });
+            stripped += linkText; i = rp + 1; continue;
+          }
+        }
+      }
+      stripped += content[i]; i++;
+    }
+
+    const lineStart = cursor;
+    text += stripped + '\n';
+    const lineEnd = cursor + stripped.length + 1;
+
+    if (namedStyleType) paraRanges.push({ start: lineStart, end: lineEnd, namedStyleType });
+    else if (isBullet) paraRanges.push({ start: lineStart, end: lineEnd, isBullet: true });
+    else if (isQuote) paraRanges.push({ start: lineStart, end: lineEnd, isQuote: true });
+
+    cursor = lineEnd;
+  }
+
+  // Build batchUpdate request set (order matters: insert first, then styles)
+  const requests = [];
+  requests.push({ insertText: { location: { index: 1 }, text } });
+
+  // Paragraph styles: headings + quotes
+  for (const p of paraRanges) {
+    if (p.namedStyleType) {
+      requests.push({
+        updateParagraphStyle: {
+          range: { startIndex: p.start, endIndex: p.end },
+          paragraphStyle: { namedStyleType: p.namedStyleType },
+          fields: 'namedStyleType'
+        }
+      });
+    } else if (p.isQuote) {
+      requests.push({
+        updateParagraphStyle: {
+          range: { startIndex: p.start, endIndex: p.end },
+          paragraphStyle: { indentStart: { magnitude: 36, unit: 'PT' }, namedStyleType: 'NORMAL_TEXT' },
+          fields: 'indentStart,namedStyleType'
+        }
+      });
+      if (p.end - 1 > p.start) {
+        requests.push({
+          updateTextStyle: {
+            range: { startIndex: p.start, endIndex: p.end - 1 },
+            textStyle: { italic: true, foregroundColor: { color: { rgbColor: { red: 0.3, green: 0.3, blue: 0.3 } } } },
+            fields: 'italic,foregroundColor'
+          }
+        });
+      }
+    }
+  }
+
+  // Bullets: group contiguous bullet paragraphs into one createParagraphBullets call
+  const bullets = paraRanges.filter(p => p.isBullet);
+  for (let i = 0; i < bullets.length;) {
+    let j = i;
+    while (j < bullets.length - 1 && bullets[j + 1].start === bullets[j].end) j++;
+    requests.push({
+      createParagraphBullets: {
+        range: { startIndex: bullets[i].start, endIndex: bullets[j].end },
+        bulletPreset: 'BULLET_DISC_CIRCLE_SQUARE'
+      }
+    });
+    i = j + 1;
+  }
+
+  // Inline text styles: bold, code, links
+  for (const t of textRanges) {
+    if (t.end <= t.start) continue;
+    const textStyle = {};
+    const fields = [];
+    if (t.bold) { textStyle.bold = true; fields.push('bold'); }
+    if (t.code) {
+      textStyle.weightedFontFamily = { fontFamily: 'Roboto Mono', weight: 400 };
+      textStyle.backgroundColor = { color: { rgbColor: { red: 0.96, green: 0.96, blue: 0.96 } } };
+      fields.push('weightedFontFamily', 'backgroundColor');
+    }
+    if (t.link) {
+      textStyle.link = { url: t.link };
+      textStyle.foregroundColor = { color: { rgbColor: { red: 0.06, green: 0.43, blue: 0.56 } } };
+      textStyle.underline = true;
+      fields.push('link', 'foregroundColor', 'underline');
+    }
+    if (!fields.length) continue;
+    requests.push({
+      updateTextStyle: {
+        range: { startIndex: t.start, endIndex: t.end },
+        textStyle,
+        fields: fields.join(',')
+      }
+    });
+  }
+
+  return { text, requests };
+}
+
+app.post('/api/docs/:id/replace-markdown', async (req, res) => {
+  try {
+    const { docs: d, key } = resolveAccount(req);
+    if (!requireService(d, 'Docs', key, res)) return;
+    const { markdown = '' } = req.body || {};
+
+    // Step 1: wipe existing doc content
+    const meta = await d.documents.get({ documentId: req.params.id });
+    const endIndex = meta.data.body?.content?.slice(-1)[0]?.endIndex || 1;
+    if (endIndex > 2) {
+      await d.documents.batchUpdate({
+        documentId: req.params.id,
+        requestBody: { requests: [{ deleteContentRange: { range: { startIndex: 1, endIndex: endIndex - 1 } } }] }
+      });
+    }
+
+    // Step 2: render markdown → batchUpdate requests
+    const { text, requests } = renderMarkdownToDocs(markdown);
+
+    // Step 3: apply in chunks of 500 (Docs API limit is ~1000 per batch)
+    const CHUNK = 500;
+    let applied = 0;
+    for (let i = 0; i < requests.length; i += CHUNK) {
+      await d.documents.batchUpdate({
+        documentId: req.params.id,
+        requestBody: { requests: requests.slice(i, i + CHUNK) }
+      });
+      applied += Math.min(CHUNK, requests.length - i);
+    }
+
+    audit(key, 'docs.replace-markdown', { documentId: req.params.id, markdownLength: markdown.length, requests: requests.length });
+    res.json({
+      account: key,
+      documentId: req.params.id,
+      url: `https://docs.google.com/document/d/${req.params.id}/edit`,
+      markdownLength: markdown.length,
+      textLength: text.length,
+      requestsApplied: applied
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message, detail: (e.errors || []).slice(0, 3) });
+  }
+});
+
+// ─── Google Sheets ───
+app.post('/api/sheets/create', async (req, res) => {
+  try {
+    const { sheets: s, key, label } = resolveAccount(req);
+    if (!requireService(s, 'Sheets', key, res)) return;
+    const { title = 'Untitled' } = req.body || {};
+    const result = await s.spreadsheets.create({ requestBody: { properties: { title } } });
+    audit(key, 'sheets.create', { spreadsheetId: result.data.spreadsheetId, title });
+    res.json({ account: key, email: label, spreadsheetId: result.data.spreadsheetId, title, url: result.data.spreadsheetUrl });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/sheets/:id', async (req, res) => {
+  try {
+    const { sheets: s, key } = resolveAccount(req);
+    if (!requireService(s, 'Sheets', key, res)) return;
+    const result = await s.spreadsheets.get({ spreadsheetId: req.params.id });
+    res.json({ account: key, spreadsheet: result.data });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/sheets/:id/values', async (req, res) => {
+  try {
+    const { sheets: s, key } = resolveAccount(req);
+    if (!requireService(s, 'Sheets', key, res)) return;
+    const range = req.query.range || 'Sheet1';
+    const result = await s.spreadsheets.values.get({ spreadsheetId: req.params.id, range });
+    res.json({ account: key, range, values: result.data.values || [] });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/sheets/:id/append', async (req, res) => {
+  try {
+    const { sheets: s, key } = resolveAccount(req);
+    if (!requireService(s, 'Sheets', key, res)) return;
+    const { range = 'Sheet1', values = [] } = req.body || {};
+    const result = await s.spreadsheets.values.append({
+      spreadsheetId: req.params.id, range, valueInputOption: 'USER_ENTERED',
+      requestBody: { values }
+    });
+    audit(key, 'sheets.append', { spreadsheetId: req.params.id, rows: values.length });
+    res.json({ account: key, updates: result.data.updates });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ─── Google Slides ───
+app.post('/api/slides/create', async (req, res) => {
+  try {
+    const { slides: sl, key, label } = resolveAccount(req);
+    if (!requireService(sl, 'Slides', key, res)) return;
+    const { title = 'Untitled' } = req.body || {};
+    const result = await sl.presentations.create({ requestBody: { title } });
+    audit(key, 'slides.create', { presentationId: result.data.presentationId, title });
+    res.json({ account: key, email: label, presentationId: result.data.presentationId, title, url: `https://docs.google.com/presentation/d/${result.data.presentationId}/edit` });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/slides/:id', async (req, res) => {
+  try {
+    const { slides: sl, key } = resolveAccount(req);
+    if (!requireService(sl, 'Slides', key, res)) return;
+    const result = await sl.presentations.get({ presentationId: req.params.id });
+    res.json({ account: key, presentation: result.data });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ─── Google Drive (per-account) ───
+app.get('/api/drive/list', async (req, res) => {
+  try {
+    const { drive: dr, key } = resolveAccount(req);
+    if (!requireService(dr, 'Drive', key, res)) return;
+    const result = await dr.files.list({
+      pageSize: parseInt(req.query.pageSize) || 25,
+      fields: 'files(id, name, mimeType, size, modifiedTime, webViewLink)',
+      orderBy: 'modifiedTime desc',
+      q: req.query.q || undefined,
+    });
+    res.json({ account: key, files: result.data.files || [] });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/drive/file/:id', async (req, res) => {
+  try {
+    const { drive: dr, key } = resolveAccount(req);
+    if (!requireService(dr, 'Drive', key, res)) return;
+    const result = await dr.files.get({
+      fileId: req.params.id,
+      fields: 'id, name, mimeType, size, modifiedTime, createdTime, webViewLink, parents, shared'
+    });
+    res.json({ account: key, file: result.data });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Download raw file bytes (alt=media). Streams directly to the response.
+// Used by the AnimalsDirectory pet-photo importer to pull images without
+// having to expose the user's OAuth token externally.
+app.get('/api/drive/download/:id', async (req, res) => {
+  try {
+    const { drive: dr, key } = resolveAccount(req);
+    if (!requireService(dr, 'Drive', key, res)) return;
+    // First metadata call so we can set Content-Type + Content-Disposition correctly
+    const meta = await dr.files.get({ fileId: req.params.id, fields: 'name, mimeType, size' });
+    const stream = await dr.files.get(
+      { fileId: req.params.id, alt: 'media' },
+      { responseType: 'stream' }
+    );
+    res.setHeader('Content-Type', meta.data.mimeType || 'application/octet-stream');
+    if (meta.data.size) res.setHeader('Content-Length', meta.data.size);
+    res.setHeader('Content-Disposition', `inline; filename="${(meta.data.name || req.params.id).replace(/"/g, '')}"`);
+    stream.data.on('error', err => { audit(key, 'drive.download.error', { fileId: req.params.id, err: err.message }); });
+    stream.data.pipe(res);
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/drive/upload', async (req, res) => {
+  try {
+    const { drive: dr, key } = resolveAccount(req);
+    if (!requireService(dr, 'Drive', key, res)) return;
+    const { name, mimeType = 'text/plain', content = '', parents } = req.body || {};
+    if (!name) return res.status(400).json({ error: 'name required' });
+    const result = await dr.files.create({
+      requestBody: { name, parents },
+      media: { mimeType, body: content },
+      fields: 'id, name, mimeType, webViewLink'
+    });
+    audit(key, 'drive.upload', { fileId: result.data.id, name });
+    res.json({ account: key, file: result.data });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ─── Google Calendar (per-account) ───
+app.get('/api/calendar/events', async (req, res) => {
+  try {
+    const { calendar: c, key } = resolveAccount(req);
+    if (!requireService(c, 'Calendar', key, res)) return;
+    const calendarId = req.query.calendarId || 'primary';
+    const timeMin = req.query.from || new Date().toISOString();
+    const timeMax = req.query.to;
+    const result = await c.events.list({
+      calendarId, timeMin, timeMax, singleEvents: true, orderBy: 'startTime',
+      maxResults: parseInt(req.query.maxResults) || 50
+    });
+    res.json({ account: key, calendarId, events: result.data.items || [] });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/calendar/events', async (req, res) => {
+  try {
+    const { calendar: c, key } = resolveAccount(req);
+    if (!requireService(c, 'Calendar', key, res)) return;
+    const { calendarId = 'primary', event } = req.body || {};
+    if (!event) return res.status(400).json({ error: 'event body required' });
+    const result = await c.events.insert({ calendarId, requestBody: event });
+    audit(key, 'calendar.event.create', { eventId: result.data.id, summary: event.summary });
+    res.json({ account: key, event: result.data });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ─── Health Check (updated for 3 accounts) ───
+app.get('/health', (req, res) => {
+  res.json({
+    status: 'ok',
+    agent: 'George',
+    port: PORT,
+    uptime: process.uptime(),
+    scopes: FULL_WORKSPACE_SCOPES,
+    accounts: {
+      'steve-office': {
+        ready: !!gmail,
+        email: 'steve@designerwallcoverings.com',
+        services: { gmail: !!gmail, tasks: !!tasksApi, drive: !!drive, docs: !!docs, sheets: !!sheets, slides: !!slides, calendar: !!calendar, forms: !!forms },
+        authUrl: gmail ? null : '/auth/steve-office'
+      },
+      'info': {
+        ready: !!infoGmail,
+        email: 'info@designerwallcoverings.com',
+        services: { gmail: !!infoGmail, tasks: !!infoTasksApi, drive: !!infoDrive, docs: !!infoDocs, sheets: !!infoSheets, slides: !!infoSlides, calendar: !!infoCalendar, forms: !!infoForms },
+        authUrl: infoGmail ? null : '/auth/info'
+      },
+      'steve-personal': {
+        ready: !!personalGmail,
+        email: 'steveabramsdesigns@gmail.com',
+        services: { gmail: !!personalGmail, tasks: !!personalTasksApi, drive: !!personalDrive, docs: !!personalDocs, sheets: !!personalSheets, slides: !!personalSlides, calendar: !!personalCalendar, forms: !!personalForms },
+        authUrl: personalGmail ? null : '/auth/steve-personal'
+      }
+    }
+  });
+});
+
+// ─── Start ───
+app.listen(PORT, () => {
+  console.log(`[${AGENT_NAME}] Gmail Agent running on port ${PORT}`);
+  console.log(`[${AGENT_NAME}] Dashboard: http://45.61.58.125:${PORT}/`);
+  console.log(`[${AGENT_NAME}] API: /api/messages, /api/search, /api/send, /api/labels, /api/profile`);
+});
diff --git a/server.js.bak-20260421-094351 b/server.js.bak-20260421-094351
new file mode 100644
index 0000000..e609ad3
--- /dev/null
+++ b/server.js.bak-20260421-094351
@@ -0,0 +1,1153 @@
+/**
+ * George the Gmail Agent — Port 9889
+ * Secure email access for DW-Agents ecosystem
+ * Auth: admin / DWSecure2024!
+ */
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+const { google } = require('googleapis');
+
+const PORT = process.env.PORT || 9850;
+const AGENT_NAME = 'George';
+const AGENT_CODENAME = 'Gmail';
+const AGENT_COLOR = '#EA4335';
+
+// ─── Gmail OAuth Credentials (loaded from DW-MCP .env) ───
+const ENV_PATH = '/root/Projects/Designer-Wallcoverings/DW-MCP/.env';
+const creds = {};
+try {
+  const envContent = fs.readFileSync(ENV_PATH, 'utf8');
+  envContent.split('\n').forEach(line => {
+    const match = line.match(/^([^#=]+)=(.+)/);
+    if (match) creds[match[1].trim()] = match[2].trim();
+  });
+  console.log(`[${AGENT_NAME}] Loaded credentials from ${ENV_PATH}`);
+} catch (e) {
+  console.error(`[${AGENT_NAME}] Failed to load env: ${e.message}`);
+}
+
+const GMAIL_CLIENT_ID = creds.GMAIL_CLIENT_ID || process.env.GMAIL_CLIENT_ID;
+const GMAIL_CLIENT_SECRET = creds.GMAIL_CLIENT_SECRET || process.env.GMAIL_CLIENT_SECRET;
+const GMAIL_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
+const GMAIL_REFRESH_TOKEN = creds.GMAIL_REFRESH_TOKEN || process.env.GMAIL_REFRESH_TOKEN;
+
+// ─── Info@ Account Credentials ───
+const INFO_REDIRECT_URI = 'http://localhost:9850/oauth2callback';
+const INFO_REFRESH_TOKEN = creds.INFO_REFRESH_TOKEN || process.env.INFO_REFRESH_TOKEN;
+
+// ─── OAuth2 Clients ───
+const SCOPES = [
+  'https://www.googleapis.com/auth/gmail.modify',
+  'https://www.googleapis.com/auth/tasks'
+];
+
+// Steve@ account (primary)
+let oauth2Client;
+let gmail;
+let tasksApi;
+try {
+  oauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URI);
+  oauth2Client.setCredentials({ refresh_token: GMAIL_REFRESH_TOKEN });
+  gmail = google.gmail({ version: 'v1', auth: oauth2Client });
+  tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
+  console.log(`[${AGENT_NAME}] Gmail + Tasks API client initialized (steve@)`);
+} catch (e) {
+  console.error(`[${AGENT_NAME}] API client error: ${e.message}`);
+}
+
+// Info@ account (secondary — gmail + tasks + drive + docs)
+const INFO_SCOPES = [
+  'https://www.googleapis.com/auth/gmail.modify',
+  'https://www.googleapis.com/auth/tasks',
+  'https://www.googleapis.com/auth/drive',
+  'https://www.googleapis.com/auth/documents'
+];
+let infoOauth2Client;
+let infoGmail;
+let infoTasksApi;
+let infoDrive;
+try {
+  infoOauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, INFO_REDIRECT_URI);
+  if (INFO_REFRESH_TOKEN) {
+    infoOauth2Client.setCredentials({ refresh_token: INFO_REFRESH_TOKEN });
+    infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
+    infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
+    infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
+    console.log(`[${AGENT_NAME}] Info@ account initialized (Gmail + Tasks + Drive)`);
+  } else {
+    console.log(`[${AGENT_NAME}] Info@ not configured — visit /auth/info to authorize`);
+  }
+} catch (e) {
+  console.error(`[${AGENT_NAME}] Info@ client error: ${e.message}`);
+}
+
+// ─── Express App ───
+const app = express();
+app.use(express.json({ limit: '50mb' }));
+
+// ─── Basic Auth (skip health + OAuth callbacks) ───
+app.use((req, res, next) => {
+  if (req.path === '/health' || req.path === '/auth' || req.path === '/auth/info' || req.path === '/oauth2callback') return next();
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) {
+    res.setHeader('WWW-Authenticate', 'Basic realm="George Gmail Agent"');
+    return res.status(401).json({ error: 'Authentication required' });
+  }
+  const decoded = Buffer.from(auth.slice(6), 'base64').toString();
+  const [user, pass] = decoded.split(':');
+  if (user === 'admin' && pass === 'DWSecure2024!') return next();
+  res.setHeader('WWW-Authenticate', 'Basic realm="George Gmail Agent"');
+  res.status(401).json({ error: 'Invalid credentials' });
+});
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+// ─── OAuth Re-Authorization Flow ───
+const OAUTH_REDIRECT = 'http://localhost:9850/oauth2callback';
+
+app.get('/auth', (req, res) => {
+  if (!oauth2Client) return res.status(500).send('OAuth client not configured');
+  const url = oauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: SCOPES,
+    prompt: 'consent',
+    redirect_uri: OAUTH_REDIRECT,
+  });
+  res.redirect(url);
+});
+
+app.get('/oauth2callback', async (req, res) => {
+  try {
+    const { code, state } = req.query;
+    if (!code) return res.status(400).send('No authorization code received');
+
+    // Route to info@ handler if state=info
+    if (state === 'info') {
+      const { tokens } = await infoOauth2Client.getToken({ code, redirect_uri: INFO_REDIRECT_URI });
+      infoOauth2Client.setCredentials(tokens);
+      infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
+      infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
+      infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
+
+      let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
+      msg += '<h2 style="color:#4ade80">Info@ Authorization Successful! (Gmail + Tasks + Drive)</h2>';
+      if (tokens.refresh_token) {
+        msg += `<p><strong>New Info@ Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
+        msg += '<p style="color:#f59e0b">Save this token to your .env file as INFO_REFRESH_TOKEN</p>';
+        console.log(`[${AGENT_NAME}] Info@ refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
+        try {
+          let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+          if (envContent.includes('INFO_REFRESH_TOKEN=')) {
+            envContent = envContent.replace(/INFO_REFRESH_TOKEN=.*/, `INFO_REFRESH_TOKEN=${tokens.refresh_token}`);
+          } else {
+            envContent += `\n# Info@ Account OAuth Token\nINFO_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+          }
+          fs.writeFileSync(ENV_PATH, envContent);
+          msg += '<p style="color:#4ade80">Token auto-saved to .env!</p>';
+        } catch (e) {
+          msg += `<p style="color:#f87171">Could not auto-save: ${e.message}</p>`;
+        }
+      }
+      try {
+        const profile = await infoGmail.users.getProfile({ userId: 'me' });
+        msg += `<p style="color:#60a5fa">Connected as: <strong>${profile.data.emailAddress}</strong></p>`;
+      } catch {}
+      msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
+      return res.send(msg);
+    }
+
+    // Default: steve@ account
+    const { tokens } = await oauth2Client.getToken({ code, redirect_uri: OAUTH_REDIRECT });
+    oauth2Client.setCredentials(tokens);
+    gmail = google.gmail({ version: 'v1', auth: oauth2Client });
+    tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
+
+    let msg = '<html><body style="font-family:sans-serif;background:#0f1117;color:#e4e4e7;padding:40px">';
+    msg += '<h2 style="color:#4ade80">Authorization Successful!</h2>';
+    if (tokens.refresh_token) {
+      msg += `<p><strong>New Refresh Token:</strong></p><code style="background:#1a1d27;padding:10px;display:block;word-break:break-all;border-radius:8px">${tokens.refresh_token}</code>`;
+      msg += '<p style="color:#f59e0b">Save this token to your .env file as GMAIL_REFRESH_TOKEN</p>';
+      console.log(`[${AGENT_NAME}] New refresh token obtained: ${tokens.refresh_token.substring(0, 20)}...`);
+    } else {
+      msg += '<p>Access token refreshed (existing refresh token reused).</p>';
+    }
+    msg += '<p style="color:#4ade80">Gmail API is now ready.</p>';
+    msg += '<p><a href="/" style="color:#60a5fa">Go to Dashboard</a></p></body></html>';
+    res.send(msg);
+  } catch (e) {
+    console.error(`[${AGENT_NAME}] OAuth callback error:`, e.message);
+    res.status(500).send(`<html><body style="font-family:sans-serif;background:#0f1117;color:#f87171;padding:40px"><h2>Authorization Failed</h2><p>${e.message}</p><p><a href="/auth" style="color:#60a5fa">Try Again</a></p></body></html>`);
+  }
+});
+
+// ─── Info@ OAuth Authorization Flow ───
+app.get('/auth/info', (req, res) => {
+  if (!infoOauth2Client) return res.status(500).send('OAuth client not configured');
+  const url = infoOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: INFO_SCOPES,
+    prompt: 'consent',
+    redirect_uri: INFO_REDIRECT_URI,
+    login_hint: 'info@designerwallcoverings.com',
+    state: 'info',
+  });
+  res.redirect(url);
+});
+
+app.get('/api/auth-url/info', (req, res) => {
+  if (!infoOauth2Client) return res.status(500).json({ error: 'OAuth client not configured' });
+  const url = infoOauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: INFO_SCOPES,
+    prompt: 'consent',
+    redirect_uri: INFO_REDIRECT_URI,
+    login_hint: 'info@designerwallcoverings.com',
+    state: 'info',
+  });
+  res.json({ authUrl: url, account: 'info@designerwallcoverings.com', scopes: INFO_SCOPES, instructions: 'Visit this URL in a browser where you are logged into info@designerwallcoverings.com. Approve access, then copy the code from the redirect URL and pass to /api/exchange-code/info?code=YOUR_CODE' });
+});
+
+// oauth2callback-info is handled by the unified /oauth2callback via state=info
+
+app.get('/api/exchange-code/info', async (req, res) => {
+  try {
+    const { code } = req.query;
+    if (!code) return res.status(400).json({ error: 'code parameter required' });
+    const { tokens } = await infoOauth2Client.getToken({ code, redirect_uri: INFO_REDIRECT_URI });
+    infoOauth2Client.setCredentials(tokens);
+    infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
+    infoTasksApi = google.tasks({ version: 'v1', auth: infoOauth2Client });
+    infoDrive = google.drive({ version: 'v3', auth: infoOauth2Client });
+
+    const result = { success: true, account: 'info@', hasRefreshToken: !!tokens.refresh_token };
+    if (tokens.refresh_token) {
+      result.refreshToken = tokens.refresh_token;
+      try {
+        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+        if (envContent.includes('INFO_REFRESH_TOKEN=')) {
+          envContent = envContent.replace(/INFO_REFRESH_TOKEN=.*/, `INFO_REFRESH_TOKEN=${tokens.refresh_token}`);
+        } else {
+          envContent += `\n# Info@ Account OAuth Token\nINFO_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+        }
+        fs.writeFileSync(ENV_PATH, envContent);
+        result.savedToEnv = true;
+      } catch (e) {
+        result.savedToEnv = false;
+        result.envError = e.message;
+      }
+    }
+    try {
+      const profile = await infoGmail.users.getProfile({ userId: 'me' });
+      result.email = profile.data.emailAddress;
+      result.ready = true;
+    } catch (e) {
+      result.ready = false;
+      result.testError = e.message;
+    }
+    res.json(result);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Manual Code Exchange (for localhost redirect flow) ───
+app.get('/api/exchange-code', async (req, res) => {
+  try {
+    const { code } = req.query;
+    if (!code) return res.status(400).json({ error: 'code parameter required' });
+    const { tokens } = await oauth2Client.getToken({ code, redirect_uri: OAUTH_REDIRECT });
+    oauth2Client.setCredentials(tokens);
+    gmail = google.gmail({ version: 'v1', auth: oauth2Client });
+    tasksApi = google.tasks({ version: 'v1', auth: oauth2Client });
+
+    const result = { success: true, hasRefreshToken: !!tokens.refresh_token };
+    if (tokens.refresh_token) {
+      result.refreshToken = tokens.refresh_token;
+      // Auto-save refresh token to .env
+      try {
+        let envContent = fs.readFileSync(ENV_PATH, 'utf8');
+        if (envContent.includes('GMAIL_REFRESH_TOKEN=')) {
+          envContent = envContent.replace(/GMAIL_REFRESH_TOKEN=.*/, `GMAIL_REFRESH_TOKEN=${tokens.refresh_token}`);
+        } else {
+          envContent += `\nGMAIL_REFRESH_TOKEN=${tokens.refresh_token}\n`;
+        }
+        fs.writeFileSync(ENV_PATH, envContent);
+        result.savedToEnv = true;
+        console.log(`[${AGENT_NAME}] Refresh token saved to ${ENV_PATH}`);
+      } catch (e) {
+        result.savedToEnv = false;
+        result.envError = e.message;
+      }
+    }
+    // Quick test
+    try {
+      const profile = await gmail.users.getProfile({ userId: 'me' });
+      result.email = profile.data.emailAddress;
+      result.gmailReady = true;
+    } catch (e) {
+      result.gmailReady = false;
+      result.testError = e.message;
+    }
+    res.json(result);
+  } catch (e) {
+    console.error(`[${AGENT_NAME}] Code exchange error:`, e.message);
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Auth URL Generator (returns URL instead of redirecting) ───
+app.get('/api/auth-url', (req, res) => {
+  if (!oauth2Client) return res.status(500).json({ error: 'OAuth client not configured' });
+  const url = oauth2Client.generateAuthUrl({
+    access_type: 'offline',
+    scope: SCOPES,
+    prompt: 'consent',
+    redirect_uri: OAUTH_REDIRECT,
+  });
+  res.json({ authUrl: url, instructions: 'Visit this URL, approve access, then copy the full redirect URL from your browser address bar and pass the code parameter to /api/exchange-code?code=YOUR_CODE' });
+});
+
+// ─── Basic Auth (exempt health + OAuth routes) ───
+app.use((req, res, next) => {
+  if (req.path === '/api/health' || req.path === '/auth' || req.path === '/auth/info' || req.path === '/oauth2callback' || req.path === '/oauth2callback-info' || req.path === '/api/exchange-code' || req.path === '/api/exchange-code/info' || req.path === '/api/auth-url' || req.path === '/api/auth-url/info') return next();
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) return res.status(401).json({ error: 'Auth required' });
+  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
+  if (user !== 'admin' || pass !== 'DWSecure2024!') return res.status(401).json({ error: 'Invalid credentials' });
+  next();
+});
+
+// ─── Helper: decode email parts ───
+function decodeBody(parts) {
+  if (!parts) return '';
+  for (const part of parts) {
+    if (part.mimeType === 'text/plain' && part.body?.data) {
+      return Buffer.from(part.body.data, 'base64url').toString('utf8');
+    }
+    if (part.parts) {
+      const nested = decodeBody(part.parts);
+      if (nested) return nested;
+    }
+  }
+  return '';
+}
+
+function getHeader(headers, name) {
+  const h = (headers || []).find(h => h.name.toLowerCase() === name.toLowerCase());
+  return h ? h.value : '';
+}
+
+// ─── API: Health Check ───
+app.get('/api/health', (req, res) => {
+  res.json({
+    agent: AGENT_NAME,
+    codename: AGENT_CODENAME,
+    status: gmail ? 'ready' : 'no-gmail-client',
+    port: PORT,
+    hasCredentials: !!(GMAIL_CLIENT_ID && GMAIL_REFRESH_TOKEN),
+    uptime: process.uptime()
+  });
+});
+
+// ─── API: Status + Profile ───
+app.get('/api/status', (req, res) => {
+  res.json({
+    agent: AGENT_NAME,
+    codename: AGENT_CODENAME,
+    color: AGENT_COLOR,
+    port: PORT,
+    status: gmail ? 'online' : 'error',
+    hasCredentials: !!(GMAIL_CLIENT_ID && GMAIL_REFRESH_TOKEN),
+    capabilities: ['list', 'read', 'search', 'send', 'labels', 'attachments'],
+    envSource: ENV_PATH
+  });
+});
+
+// ─── API: Get Profile (email address, history) ───
+app.get('/api/profile', async (req, res) => {
+  try {
+    const profile = await gmail.users.getProfile({ userId: 'me' });
+    res.json(profile.data);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: List Labels ───
+app.get('/api/labels', async (req, res) => {
+  try {
+    const result = await gmail.users.labels.list({ userId: 'me' });
+    res.json(result.data.labels || []);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: List Messages ───
+app.get('/api/messages', async (req, res) => {
+  try {
+    const { q, maxResults, labelIds, pageToken } = req.query;
+    const params = { userId: 'me', maxResults: parseInt(maxResults) || 20 };
+    if (q) params.q = q;
+    if (labelIds) params.labelIds = labelIds.split(',');
+    if (pageToken) params.pageToken = pageToken;
+
+    const list = await gmail.users.messages.list(params);
+    const messages = list.data.messages || [];
+
+    // Fetch headers for each message (batch)
+    const detailed = await Promise.all(
+      messages.slice(0, parseInt(maxResults) || 20).map(async m => {
+        try {
+          const full = await gmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
+          return {
+            id: full.data.id,
+            threadId: full.data.threadId,
+            snippet: full.data.snippet,
+            labelIds: full.data.labelIds,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            to: getHeader(full.data.payload?.headers, 'To'),
+            date: getHeader(full.data.payload?.headers, 'Date'),
+            internalDate: full.data.internalDate
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+
+    res.json({
+      messages: detailed,
+      nextPageToken: list.data.nextPageToken,
+      resultSizeEstimate: list.data.resultSizeEstimate
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Read Single Message ───
+app.get('/api/messages/:id', async (req, res) => {
+  try {
+    const msg = await gmail.users.messages.get({ userId: 'me', id: req.params.id, format: 'full' });
+    const headers = msg.data.payload?.headers || [];
+    const body = msg.data.payload?.body?.data
+      ? Buffer.from(msg.data.payload.body.data, 'base64url').toString('utf8')
+      : decodeBody(msg.data.payload?.parts);
+
+    const attachments = [];
+    function findAttachments(parts) {
+      if (!parts) return;
+      for (const p of parts) {
+        if (p.filename && p.body?.attachmentId) {
+          attachments.push({ filename: p.filename, mimeType: p.mimeType, size: p.body.size, attachmentId: p.body.attachmentId });
+        }
+        if (p.parts) findAttachments(p.parts);
+      }
+    }
+    findAttachments(msg.data.payload?.parts);
+
+    res.json({
+      id: msg.data.id,
+      threadId: msg.data.threadId,
+      labelIds: msg.data.labelIds,
+      snippet: msg.data.snippet,
+      subject: getHeader(headers, 'Subject'),
+      from: getHeader(headers, 'From'),
+      to: getHeader(headers, 'To'),
+      cc: getHeader(headers, 'Cc'),
+      date: getHeader(headers, 'Date'),
+      body,
+      attachments,
+      internalDate: msg.data.internalDate
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Search Messages ───
+app.get('/api/search', async (req, res) => {
+  try {
+    const { q, maxResults } = req.query;
+    if (!q) return res.status(400).json({ error: 'Query (q) required' });
+    const list = await gmail.users.messages.list({ userId: 'me', q, maxResults: parseInt(maxResults) || 10 });
+    const messages = list.data.messages || [];
+
+    const detailed = await Promise.all(
+      messages.map(async m => {
+        try {
+          const full = await gmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date'] });
+          return {
+            id: full.data.id,
+            snippet: full.data.snippet,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            date: getHeader(full.data.payload?.headers, 'Date')
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+
+    res.json({ query: q, messages: detailed, total: list.data.resultSizeEstimate });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Send Email ───
+app.post('/api/send', async (req, res) => {
+  try {
+    const { to, subject, body, cc, bcc } = req.body;
+    if (!to || !subject || !body) return res.status(400).json({ error: 'to, subject, and body required' });
+
+    const headers = [
+      `To: ${to}`,
+      cc ? `Cc: ${cc}` : '',
+      bcc ? `Bcc: ${bcc}` : '',
+      `Subject: ${subject}`,
+      'Content-Type: text/html; charset=utf-8',
+      '',
+      body
+    ].filter(Boolean).join('\r\n');
+
+    const encoded = Buffer.from(headers).toString('base64url');
+    const result = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encoded } });
+    res.json({ success: true, messageId: result.data.id, threadId: result.data.threadId });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Send with Attachments ───
+// Body: { to, cc?, bcc?, subject, body (html), attachments: [{ filename, content_base64, mime_type? }] }
+app.post('/api/send-with-attachment', async (req, res) => {
+  try {
+    const { to, cc, bcc, subject, body, attachments } = req.body;
+    if (!to || !subject || !body) return res.status(400).json({ error: 'to, subject, and body required' });
+    if (!attachments || !Array.isArray(attachments) || attachments.length === 0) {
+      return res.status(400).json({ error: 'attachments array required' });
+    }
+    const boundary = `=_dw_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
+    const lines = [
+      `To: ${to}`,
+      cc ? `Cc: ${cc}` : '',
+      bcc ? `Bcc: ${bcc}` : '',
+      `Subject: ${subject}`,
+      'MIME-Version: 1.0',
+      `Content-Type: multipart/mixed; boundary="${boundary}"`,
+      '',
+      `--${boundary}`,
+      'Content-Type: text/html; charset=utf-8',
+      'Content-Transfer-Encoding: 7bit',
+      '',
+      body
+    ].filter(Boolean);
+    for (const att of attachments) {
+      const mime = att.mime_type || 'application/octet-stream';
+      const fname = att.filename || 'attachment.bin';
+      lines.push(
+        `--${boundary}`,
+        `Content-Type: ${mime}; name="${fname}"`,
+        'Content-Transfer-Encoding: base64',
+        `Content-Disposition: attachment; filename="${fname}"`,
+        '',
+        att.content_base64.replace(/(.{76})/g, '$1\r\n')
+      );
+    }
+    lines.push(`--${boundary}--`, '');
+    const raw = lines.join('\r\n');
+    const encoded = Buffer.from(raw).toString('base64url');
+    const result = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encoded } });
+    res.json({ success: true, messageId: result.data.id, threadId: result.data.threadId });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Create Draft ───
+app.post('/api/drafts', async (req, res) => {
+  try {
+    const { to, subject, body, cc, bcc } = req.body;
+    if (!subject || !body) return res.status(400).json({ error: 'subject and body required' });
+
+    const headers = [
+      to ? `To: ${to}` : '',
+      cc ? `Cc: ${cc}` : '',
+      bcc ? `Bcc: ${bcc}` : '',
+      `Subject: ${subject}`,
+      'Content-Type: text/html; charset=utf-8',
+      '',
+      body
+    ].filter(Boolean).join('\r\n');
+
+    const encoded = Buffer.from(headers).toString('base64url');
+    const result = await gmail.users.drafts.create({ userId: 'me', requestBody: { message: { raw: encoded } } });
+    res.json({ success: true, draftId: result.data.id, messageId: result.data.message?.id });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: List Drafts ───
+app.get('/api/drafts', async (req, res) => {
+  try {
+    const result = await gmail.users.drafts.list({ userId: 'me', maxResults: parseInt(req.query.maxResults) || 10 });
+    res.json(result.data.drafts || []);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── API: Get Attachment ───
+app.get('/api/messages/:messageId/attachments/:attachmentId', async (req, res) => {
+  try {
+    const att = await gmail.users.messages.attachments.get({
+      userId: 'me', messageId: req.params.messageId, id: req.params.attachmentId
+    });
+    res.json({ data: att.data.data, size: att.data.size });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Slack Webhook for notifications ───
+const SLACK_WEBHOOK = 'https://hooks.slack.com/services/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';
+function slackNotify(text) {
+  try {
+    const https = require('https');
+    const url = new URL(SLACK_WEBHOOK);
+    const payload = JSON.stringify({ text });
+    const req = https.request({ hostname: url.hostname, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } });
+    req.write(payload);
+    req.end();
+  } catch {}
+}
+
+// ─── Google Tasks API ───
+app.get('/api/tasks/lists', async (req, res) => {
+  try {
+    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
+    const result = await tasksApi.tasklists.list({ maxResults: 100 });
+    res.json({ lists: result.data.items || [] });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/tasks/:listId', async (req, res) => {
+  try {
+    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
+    const showCompleted = req.query.showCompleted !== 'false';
+    const showHidden = req.query.showHidden === 'true';
+    const result = await tasksApi.tasks.list({
+      tasklist: req.params.listId,
+      maxResults: parseInt(req.query.maxResults) || 100,
+      showCompleted,
+      showHidden,
+    });
+    const tasks = (result.data.items || []).map(t => ({
+      id: t.id,
+      title: t.title,
+      notes: t.notes || null,
+      status: t.status,
+      due: t.due || null,
+      completed: t.completed || null,
+      updated: t.updated,
+      parent: t.parent || null,
+      position: t.position,
+      links: t.links || [],
+    }));
+    res.json({ listId: req.params.listId, count: tasks.length, tasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Convenience: get all tasks across all lists
+app.get('/api/tasks', async (req, res) => {
+  try {
+    if (!tasksApi) return res.status(503).json({ error: 'Tasks API not initialized — re-authorize at /auth' });
+    const listsResult = await tasksApi.tasklists.list({ maxResults: 100 });
+    const lists = listsResult.data.items || [];
+    const allTasks = [];
+    for (const list of lists) {
+      const tasksResult = await tasksApi.tasks.list({
+        tasklist: list.id,
+        maxResults: 100,
+        showCompleted: req.query.showCompleted !== 'false',
+      });
+      const items = (tasksResult.data.items || []).map(t => ({
+        id: t.id,
+        title: t.title,
+        notes: t.notes || null,
+        status: t.status,
+        due: t.due || null,
+        completed: t.completed || null,
+        updated: t.updated,
+        listId: list.id,
+        listTitle: list.title,
+      }));
+      allTasks.push(...items);
+    }
+    res.json({ totalLists: lists.length, totalTasks: allTasks.length, tasks: allTasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks scope not authorized. Visit /auth to grant permission.', authUrl: '/auth' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Tasks API ───
+app.get('/api/info/tasks/lists', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const result = await infoTasksApi.tasklists.list({ maxResults: 100 });
+    res.json({ account: 'info@designerwallcoverings.com', lists: result.data.items || [] });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Info@ tasks scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/info/tasks/:listId', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const showCompleted = req.query.showCompleted !== 'false';
+    const showHidden = req.query.showHidden === 'true';
+    const result = await infoTasksApi.tasks.list({
+      tasklist: req.params.listId,
+      maxResults: parseInt(req.query.maxResults) || 100,
+      showCompleted,
+      showHidden,
+    });
+    const tasks = (result.data.items || []).map(t => ({
+      id: t.id,
+      title: t.title,
+      notes: t.notes || null,
+      status: t.status,
+      due: t.due || null,
+      completed: t.completed || null,
+      updated: t.updated,
+      parent: t.parent || null,
+      position: t.position,
+      links: t.links || [],
+    }));
+    res.json({ account: 'info@designerwallcoverings.com', listId: req.params.listId, count: tasks.length, tasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Info@ tasks scope not authorized.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/info/tasks', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const listsResult = await infoTasksApi.tasklists.list({ maxResults: 100 });
+    const lists = listsResult.data.items || [];
+    const allTasks = [];
+    for (const list of lists) {
+      const tasksResult = await infoTasksApi.tasks.list({
+        tasklist: list.id,
+        maxResults: 100,
+        showCompleted: req.query.showCompleted !== 'false',
+      });
+      const items = (tasksResult.data.items || []).map(t => ({
+        id: t.id,
+        title: t.title,
+        notes: t.notes || null,
+        status: t.status,
+        due: t.due || null,
+        completed: t.completed || null,
+        updated: t.updated,
+        listId: list.id,
+        listTitle: list.title,
+      }));
+      allTasks.push(...items);
+    }
+    res.json({ account: 'info@designerwallcoverings.com', totalLists: lists.length, totalTasks: allTasks.length, tasks: allTasks });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Info@ tasks scope not authorized.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Tasks Write Endpoints ───
+// Create a new task list
+app.post('/api/info/tasks/lists/create', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { title } = req.body;
+    if (!title) return res.status(400).json({ error: 'title is required' });
+    const result = await infoTasksApi.tasklists.insert({ requestBody: { title } });
+    res.json({ success: true, list: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info to upgrade from tasks.readonly to tasks.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Find a task list by name, or create it if it doesn't exist
+app.post('/api/info/tasks/lists/find-or-create', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { title } = req.body;
+    if (!title) return res.status(400).json({ error: 'title is required' });
+
+    // Search existing lists
+    const listsResult = await infoTasksApi.tasklists.list({ maxResults: 100 });
+    const existing = (listsResult.data.items || []).find(l => l.title === title);
+    if (existing) {
+      return res.json({ success: true, created: false, list: existing });
+    }
+
+    // Create new list
+    const result = await infoTasksApi.tasklists.insert({ requestBody: { title } });
+    res.json({ success: true, created: true, list: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Move a task from one list to another (insert into dest + delete from source)
+// Google Tasks API has no cross-list move — we must copy then delete
+app.post('/api/info/tasks/move', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { sourceListId, taskId, destListId, markCompleted } = req.body;
+    if (!sourceListId || !taskId || !destListId) {
+      return res.status(400).json({ error: 'sourceListId, taskId, and destListId are required' });
+    }
+
+    // 1. Get the task from source list
+    const taskResult = await infoTasksApi.tasks.get({ tasklist: sourceListId, task: taskId });
+    const task = taskResult.data;
+
+    // 2. Insert into destination list (preserve title, notes, due, links)
+    const newTaskBody = {
+      title: task.title,
+      notes: task.notes || undefined,
+      due: task.due || undefined,
+      status: markCompleted ? 'completed' : 'needsAction',
+    };
+    const insertResult = await infoTasksApi.tasks.insert({ tasklist: destListId, requestBody: newTaskBody });
+
+    // 3. Delete from source list
+    await infoTasksApi.tasks.delete({ tasklist: sourceListId, task: taskId });
+
+    res.json({
+      success: true,
+      moved: true,
+      sourceListId,
+      destListId,
+      originalTaskId: taskId,
+      newTaskId: insertResult.data.id,
+      title: task.title,
+    });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Update a task's title (e.g., prepend "READY" status)
+app.post('/api/info/tasks/:listId/:taskId/update', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { listId, taskId } = req.params;
+    const { title, notes } = req.body;
+    const body = {};
+    if (title) body.title = title;
+    if (notes !== undefined) body.notes = notes;
+    if (!Object.keys(body).length) return res.status(400).json({ error: 'title or notes required' });
+    const result = await infoTasksApi.tasks.patch({
+      tasklist: listId,
+      task: taskId,
+      requestBody: body
+    });
+    res.json({ success: true, task: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Complete a task (mark as done without moving)
+app.post('/api/info/tasks/:listId/:taskId/complete', async (req, res) => {
+  try {
+    if (!infoTasksApi) return res.status(503).json({ error: 'Info@ Tasks API not initialized', authUrl: '/auth/info' });
+    const { listId, taskId } = req.params;
+    const result = await infoTasksApi.tasks.patch({
+      tasklist: listId,
+      task: taskId,
+      requestBody: { status: 'completed' }
+    });
+    res.json({ success: true, task: result.data });
+  } catch (e) {
+    if (e.message.includes('insufficient')) {
+      return res.status(403).json({ error: 'Tasks write scope not authorized. Visit /auth/info.', authUrl: '/auth/info' });
+    }
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Gmail Endpoints ───
+app.get('/api/info/profile', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const profile = await infoGmail.users.getProfile({ userId: 'me' });
+    res.json({ account: 'info@designerwallcoverings.com', ...profile.data });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/info/messages', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized — authorize at /auth/info', authUrl: '/api/auth-url/info' });
+    const { q, maxResults, labelIds, pageToken } = req.query;
+    const params = { userId: 'me', maxResults: parseInt(maxResults) || 20 };
+    if (q) params.q = q;
+    if (labelIds) params.labelIds = labelIds.split(',');
+    if (pageToken) params.pageToken = pageToken;
+
+    const list = await infoGmail.users.messages.list(params);
+    const messages = list.data.messages || [];
+    const detailed = await Promise.all(
+      messages.slice(0, parseInt(maxResults) || 20).map(async m => {
+        try {
+          const full = await infoGmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
+          return {
+            id: full.data.id,
+            threadId: full.data.threadId,
+            snippet: full.data.snippet,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            to: getHeader(full.data.payload?.headers, 'To'),
+            date: getHeader(full.data.payload?.headers, 'Date'),
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+    res.json({ account: 'info@designerwallcoverings.com', messages: detailed, nextPageToken: list.data.nextPageToken });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Google Drive Endpoints ───
+const DRIVE_NOT_READY = { error: 'Info@ Drive not initialized — authorize at /auth/info (needs drive.readonly scope)', authUrl: '/auth/info' };
+
+// List files (supports Drive query syntax in ?q= param)
+app.get('/api/info/drive', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const { q, maxResults, pageToken, orderBy } = req.query;
+    const params = {
+      pageSize: parseInt(maxResults) || 25,
+      fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, webViewLink, iconLink, parents, shared)',
+      orderBy: orderBy || 'modifiedTime desc',
+    };
+    if (q) params.q = q;
+    if (pageToken) params.pageToken = pageToken;
+    const result = await infoDrive.files.list(params);
+    res.json({ account: 'info@', files: result.data.files || [], nextPageToken: result.data.nextPageToken });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Get file metadata by ID
+app.get('/api/info/drive/file/:fileId', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const result = await infoDrive.files.get({
+      fileId: req.params.fileId,
+      fields: 'id, name, mimeType, size, modifiedTime, createdTime, webViewLink, webContentLink, iconLink, parents, shared, description',
+    });
+    res.json(result.data);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Search files by name
+app.get('/api/info/drive/search/:query', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const searchQ = `name contains '${req.params.query.replace(/'/g, "\\'")}'`;
+    const result = await infoDrive.files.list({
+      q: searchQ,
+      pageSize: parseInt(req.query.maxResults) || 25,
+      fields: 'files(id, name, mimeType, size, modifiedTime, webViewLink, parents)',
+      orderBy: 'modifiedTime desc',
+    });
+    res.json({ account: 'info@', query: req.params.query, files: result.data.files || [] });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// List shared drives
+app.get('/api/info/drive/shared', async (req, res) => {
+  try {
+    if (!infoDrive) return res.status(503).json(DRIVE_NOT_READY);
+    const result = await infoDrive.drives.list({ pageSize: 50 });
+    res.json({ account: 'info@', drives: result.data.drives || [] });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Single Message (with HTML body) ───
+function decodeHtmlBody(parts) {
+  if (!parts) return { html: '', text: '' };
+  let html = '', text = '';
+  for (const part of parts) {
+    if (part.mimeType === 'text/html' && part.body?.data) {
+      html = Buffer.from(part.body.data, 'base64url').toString('utf8');
+    }
+    if (part.mimeType === 'text/plain' && part.body?.data) {
+      text = Buffer.from(part.body.data, 'base64url').toString('utf8');
+    }
+    if (part.parts) {
+      const nested = decodeHtmlBody(part.parts);
+      if (nested.html && !html) html = nested.html;
+      if (nested.text && !text) text = nested.text;
+    }
+  }
+  return { html, text };
+}
+
+app.get('/api/info/messages/:id', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
+    const msg = await infoGmail.users.messages.get({ userId: 'me', id: req.params.id, format: 'full' });
+    const headers = msg.data.payload?.headers || [];
+
+    // Decode both HTML and text bodies
+    const topBody = msg.data.payload?.body?.data
+      ? Buffer.from(msg.data.payload.body.data, 'base64url').toString('utf8')
+      : null;
+    const { html, text } = decodeHtmlBody(msg.data.payload?.parts);
+
+    const attachments = [];
+    function findAttachments(parts) {
+      if (!parts) return;
+      for (const p of parts) {
+        if (p.filename && p.body?.attachmentId) {
+          attachments.push({ filename: p.filename, mimeType: p.mimeType, size: p.body.size, attachmentId: p.body.attachmentId });
+        }
+        if (p.parts) findAttachments(p.parts);
+      }
+    }
+    findAttachments(msg.data.payload?.parts);
+
+    res.json({
+      id: msg.data.id,
+      threadId: msg.data.threadId,
+      labelIds: msg.data.labelIds,
+      snippet: msg.data.snippet,
+      subject: getHeader(headers, 'Subject'),
+      from: getHeader(headers, 'From'),
+      to: getHeader(headers, 'To'),
+      cc: getHeader(headers, 'Cc'),
+      date: getHeader(headers, 'Date'),
+      bodyHtml: html || topBody || '',
+      bodyText: text || topBody || '',
+      attachments,
+      internalDate: msg.data.internalDate
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Search Messages ───
+app.get('/api/info/search', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
+    const { q, maxResults } = req.query;
+    if (!q) return res.status(400).json({ error: 'Query (q) required' });
+    const list = await infoGmail.users.messages.list({ userId: 'me', q, maxResults: parseInt(maxResults) || 10 });
+    const messages = list.data.messages || [];
+
+    const detailed = await Promise.all(
+      messages.map(async m => {
+        try {
+          const full = await infoGmail.users.messages.get({ userId: 'me', id: m.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date', 'To'] });
+          return {
+            id: full.data.id,
+            snippet: full.data.snippet,
+            subject: getHeader(full.data.payload?.headers, 'Subject'),
+            from: getHeader(full.data.payload?.headers, 'From'),
+            to: getHeader(full.data.payload?.headers, 'To'),
+            date: getHeader(full.data.payload?.headers, 'Date')
+          };
+        } catch { return { id: m.id, error: 'fetch-failed' }; }
+      })
+    );
+
+    res.json({ account: 'info@designerwallcoverings.com', query: q, messages: detailed, total: list.data.resultSizeEstimate });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Info@ Download Attachment ───
+app.get('/api/info/messages/:messageId/attachments/:attachmentId', async (req, res) => {
+  try {
+    if (!infoGmail) return res.status(503).json({ error: 'Info@ Gmail not initialized', authUrl: '/auth/info' });
+    const att = await infoGmail.users.messages.attachments.get({
+      userId: 'me', messageId: req.params.messageId, id: req.params.attachmentId
+    });
+    const buf = Buffer.from(att.data.data, 'base64');
+    res.set('Content-Length', buf.length);
+    res.send(buf);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ─── Health Check ───
+app.get('/health', (req, res) => {
+  res.json({
+    status: 'ok',
+    agent: 'George',
+    port: PORT,
+    uptime: process.uptime(),
+    accounts: {
+      steve: { ready: !!gmail, email: 'steve@designerwallcoverings.com', scopes: SCOPES },
+      info: { ready: !!infoGmail, email: 'info@designerwallcoverings.com', scopes: INFO_SCOPES, drive: !!infoDrive, authUrl: infoGmail ? null : '/auth/info' },
+    }
+  });
+});
+
+// ─── Start ───
+app.listen(PORT, () => {
+  console.log(`[${AGENT_NAME}] Gmail Agent running on port ${PORT}`);
+  console.log(`[${AGENT_NAME}] Dashboard: http://45.61.58.125:${PORT}/`);
+  console.log(`[${AGENT_NAME}] API: /api/messages, /api/search, /api/send, /api/labels, /api/profile`);
+});
diff --git a/trash-redfin-zillow.js b/trash-redfin-zillow.js
new file mode 100644
index 0000000..ffafc83
--- /dev/null
+++ b/trash-redfin-zillow.js
@@ -0,0 +1,70 @@
+const fs=require("fs");const {google}=require("googleapis");
+const env={};fs.readFileSync("/root/Projects/Designer-Wallcoverings/DW-MCP/.env","utf8").split("\n").forEach(l=>{const m=l.match(/^([^#=]+)=(.+)/);if(m)env[m[1].trim()]=m[2].trim()});
+const accts=[
+  {k:"steve-office",l:"steve@designerwallcoverings.com",r:env.GMAIL_REFRESH_TOKEN},
+  {k:"info",l:"info@designerwallcoverings.com",r:env.INFO_REFRESH_TOKEN},
+  {k:"steve-personal",l:"steveabramsdesigns@gmail.com",r:env.PERSONAL_REFRESH_TOKEN},
+];
+const QUERY = "redfin OR zillow";
+
+const totals = { collected: 0, trashed: 0, byAccount: {} };
+const auditPath = "/root/DW-Agents/logs/george-workspace-audit.log";
+
+(async()=>{
+  for(const a of accts){
+    const oc=new google.auth.OAuth2(env.GMAIL_CLIENT_ID,env.GMAIL_CLIENT_SECRET,"http://localhost:9850/oauth2callback");
+    oc.setCredentials({refresh_token:a.r});
+    const gm=google.gmail({version:"v1",auth:oc});
+    console.log(`\n=== ${a.l} ===`);
+
+    // 1. Collect all message IDs matching the query
+    const ids=[];let pageToken=null,pages=0;
+    const t0=Date.now();
+    do{
+      const r=await gm.users.messages.list({userId:"me",q:QUERY,maxResults:500,pageToken:pageToken||undefined});
+      (r.data.messages||[]).forEach(m=>ids.push(m.id));
+      pageToken=r.data.nextPageToken||null;
+      pages++;
+      if(pages%20===0) process.stdout.write(`    collecting... ${ids.length} IDs so far\n`);
+    }while(pageToken);
+    console.log(`  collected ${ids.length} message IDs in ${((Date.now()-t0)/1000).toFixed(1)}s (${pages} pages)`);
+    totals.collected += ids.length;
+    totals.byAccount[a.l] = { collected: ids.length, trashed: 0 };
+
+    if(ids.length === 0) continue;
+
+    // 2. Batch-modify: add TRASH label (remove INBOX for cleanliness)
+    const BATCH = 1000;
+    const t1=Date.now();
+    for(let i=0; i<ids.length; i+=BATCH){
+      const chunk = ids.slice(i, i+BATCH);
+      try {
+        await gm.users.messages.batchModify({
+          userId:"me",
+          requestBody:{
+            ids: chunk,
+            addLabelIds: ["TRASH"],
+            removeLabelIds: ["INBOX"]
+          }
+        });
+        totals.trashed += chunk.length;
+        totals.byAccount[a.l].trashed += chunk.length;
+        process.stdout.write(`    trashed ${i+chunk.length}/${ids.length}\n`);
+      } catch(e) {
+        console.log(`    batch ${Math.floor(i/BATCH)+1} failed: ${e.message.slice(0,120)}`);
+      }
+    }
+    const elapsed = ((Date.now()-t1)/1000).toFixed(1);
+    console.log(`  trashed ${totals.byAccount[a.l].trashed} in ${elapsed}s`);
+    // Audit log
+    try {
+      fs.appendFileSync(auditPath, `${new Date().toISOString()}\t${a.k}\tgmail.batchTrash\t${JSON.stringify({query:QUERY, count: totals.byAccount[a.l].trashed})}\n`);
+    } catch {}
+  }
+
+  console.log("\n=== FINAL ===");
+  for(const [email,v] of Object.entries(totals.byAccount)){
+    console.log(`  ${email}: ${v.trashed}/${v.collected} trashed`);
+  }
+  console.log(`  TOTAL: ${totals.trashed}/${totals.collected} trashed`);
+})();

(oldest)  ·  back to George Gmail  ·  fix(george): ENV_PATH portable across Mac2/Kamatera a4d0448 →