[object Object]

← back to Wallco Ai

trade-login: wire PureLyMail SMTP (smtp.purelymail.com:465 SSL, auth noreply@agentabrams.com — same pattern as george-gmail). nodemailer installed. MAIL_FROM='wallco.ai sign-in <noreply@agentabrams.com>' (change after adding wallco.ai to that mailbox's allowed-sender list). Test sent successfully — sent:true via smtp

54a2c780908573bd8e8d4afa4e9a7580cf2103df · 2026-05-28 09:49:05 -0700 · Steve Abrams

Files touched

Diff

commit 54a2c780908573bd8e8d4afa4e9a7580cf2103df
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 28 09:49:05 2026 -0700

    trade-login: wire PureLyMail SMTP (smtp.purelymail.com:465 SSL, auth noreply@agentabrams.com — same pattern as george-gmail). nodemailer installed. MAIL_FROM='wallco.ai sign-in <noreply@agentabrams.com>' (change after adding wallco.ai to that mailbox's allowed-sender list). Test sent successfully — sent:true via smtp
---
 data/cactus-decisions.jsonl | 19 +++++++++++++++++++
 package-lock.json           | 10 ++++++++++
 package.json                |  1 +
 scripts/vision-contains.py  | 43 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 73 insertions(+)

diff --git a/data/cactus-decisions.jsonl b/data/cactus-decisions.jsonl
index 09964f3..53bc7dc 100644
--- a/data/cactus-decisions.jsonl
+++ b/data/cactus-decisions.jsonl
@@ -3876,3 +3876,22 @@
 {"ts":"2026-05-28T16:39:59.805Z","id":27685,"action":"bad"}
 {"ts":"2026-05-28T16:40:00.979Z","id":27916,"action":"bad"}
 {"ts":"2026-05-28T16:40:10.233Z","id":24888,"action":"bad"}
+{"ts":"2026-05-28T16:40:16.381Z","id":27375,"action":"live"}
+{"ts":"2026-05-28T16:40:22.809Z","id":10727,"action":"bad"}
+{"ts":"2026-05-28T16:40:28.478Z","id":10778,"action":"bad"}
+{"ts":"2026-05-28T16:40:34.125Z","id":11662,"action":"bad"}
+{"ts":"2026-05-28T16:40:38.163Z","id":12210,"action":"bad"}
+{"ts":"2026-05-28T16:40:39.846Z","id":12234,"action":"bad"}
+{"ts":"2026-05-28T16:40:42.463Z","id":12452,"action":"bad"}
+{"ts":"2026-05-28T16:40:48.930Z","id":13439,"action":"bad"}
+{"ts":"2026-05-28T16:40:49.064Z","id":12698,"action":"bad"}
+{"ts":"2026-05-28T16:41:01.682Z","id":14885,"action":"bad"}
+{"ts":"2026-05-28T16:41:02.518Z","id":14690,"action":"bad"}
+{"ts":"2026-05-28T16:41:04.736Z","id":15395,"action":"bad"}
+{"ts":"2026-05-28T16:41:06.874Z","id":15692,"action":"bad"}
+{"ts":"2026-05-28T16:41:16.186Z","id":15512,"action":"live"}
+{"ts":"2026-05-28T16:41:20.485Z","id":16338,"action":"bad"}
+{"ts":"2026-05-28T16:41:22.590Z","id":16241,"action":"bad"}
+{"ts":"2026-05-28T16:41:23.595Z","id":16366,"action":"bad"}
+{"ts":"2026-05-28T16:41:24.548Z","id":17278,"action":"bad"}
+{"ts":"2026-05-28T16:41:27.589Z","id":20171,"action":"bad"}
diff --git a/package-lock.json b/package-lock.json
index 0ed869e..df99503 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "express-rate-limit": "^8.5.1",
         "jsonwebtoken": "^9.0.3",
         "multer": "^2.1.1",
+        "nodemailer": "^8.0.9",
         "pg": "^8.20.0"
       }
     },
@@ -830,6 +831,15 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/nodemailer": {
+      "version": "8.0.9",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.9.tgz",
+      "integrity": "sha512-5ofa7BUN8+C+Hckh5V2GjeeOGRQBx0CJQA6KxrvuZfC8iU4/q7sLn8XrtEEhJkjV6HdyIiQs7Bba6bTao8JhkA==",
+      "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",
diff --git a/package.json b/package.json
index 65a4891..31ba411 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
     "express-rate-limit": "^8.5.1",
     "jsonwebtoken": "^9.0.3",
     "multer": "^2.1.1",
+    "nodemailer": "^8.0.9",
     "pg": "^8.20.0"
   }
 }
diff --git a/scripts/vision-contains.py b/scripts/vision-contains.py
new file mode 100644
index 0000000..ca9d8df
--- /dev/null
+++ b/scripts/vision-contains.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+"""Yes/no vision gate via Gemini 2.5 flash. Used by the regen loop as the 3rd
+gate after seam-PASS + flat-color: rejects rolls that drifted off-concept
+(e.g. abstract botanical when the prompt asked for animal silhouettes).
+
+Usage: vision-contains.py <png_path> <concept-question>
+Prints: yes|no
+"""
+import os, sys, base64, json, urllib.request
+
+png, question = sys.argv[1], sys.argv[2]
+# Load GEMINI_API_KEY from project .env if not in env
+if not os.environ.get('GEMINI_API_KEY'):
+    try:
+        for line in open(os.path.join(os.path.dirname(__file__), '..', '.env')).read().splitlines():
+            if line.startswith('GEMINI_API_KEY='):
+                os.environ['GEMINI_API_KEY'] = line.split('=', 1)[1].strip().strip('"').strip("'")
+                break
+    except Exception: pass
+
+key = os.environ.get('GEMINI_API_KEY')
+if not key: print('no'); sys.exit(2)
+
+img_b64 = base64.b64encode(open(png, 'rb').read()).decode()
+body = {
+    'contents': [{'parts': [
+        {'text': question + ' Reply with exactly one word: yes or no.'},
+        {'inline_data': {'mime_type': 'image/png', 'data': img_b64}},
+    ]}],
+    'generation_config': {'temperature': 0, 'max_output_tokens': 8, 'thinking_config': {'thinking_budget': 0}},
+}
+req = urllib.request.Request(
+    f'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={key}',
+    data=json.dumps(body).encode(),
+    headers={'Content-Type': 'application/json'},
+)
+try:
+    with urllib.request.urlopen(req, timeout=20) as r:
+        j = json.load(r)
+    txt = (((j.get('candidates') or [{}])[0].get('content') or {}).get('parts') or [{}])[0].get('text', '').strip().lower()
+    print('yes' if txt.startswith('yes') else 'no')
+except Exception as e:
+    print('no', file=sys.stderr); print('no')

← 1f77520 cactus-curator: 📦 TIF badge on every card (green when archi  ·  back to Wallco Ai  ·  design page: sticky admin toolbar (Unpublish / Smart-fix / S 88d2c1f →