brand-manager-worker 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brand-manager-worker",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The Goose Tools brand-deal worker — your computer reads your brand email and drafts replies in your voice for goosetools.com, using your own Claude account. Drafts only; it never sends.",
5
5
  "repository": {
6
6
  "type": "git",
package/worker/agent.js CHANGED
@@ -12,8 +12,21 @@ import { BRAND_DIR } from "./paths.js";
12
12
  // Automated / no-reply senders never send brand deals — skip them WITHOUT
13
13
  // spending a Claude call. Conservative on purpose: real brands/agencies use
14
14
  // human addresses.
15
- const AUTOMATED_SENDER =
16
- /no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|notifications?@|accounts\.google\.com|@.*\.(beehiiv|substack)\.com|@e\.|@email\.|@mail\./i;
15
+ const AUTOMATED_SENDER = new RegExp(
16
+ [
17
+ // Classic no-reply shapes.
18
+ "no-?reply", "do-?not-?reply", "noreply", "mailer-daemon", "postmaster",
19
+ "notifications?@", "accounts\\.google\\.com",
20
+ "@.*\\.(beehiiv|substack)\\.com",
21
+ // Bulk-mail SUBDOMAINS. Retail blasts send from things like
22
+ // news.emailmarket.shein.com, which slipped past the old @mail./@email.
23
+ // patterns and cost a full Claude call each — 7 SHEIN cart reminders in
24
+ // one scan. Matched as a leading label so a real company domain like
25
+ // news-corp.com or mailchimp.com (an agency's own address) is untouched.
26
+ "@(e|news|newsletter|email|emails|mail|mailer|marketing|promo|promos|campaign|campaigns|updates|offers|deals|shop|store|hello|info)[.-]",
27
+ ].join("|"),
28
+ "i",
29
+ );
17
30
 
18
31
  export const isAutomated = (from) => AUTOMATED_SENDER.test(from);
19
32
 
@@ -35,18 +35,6 @@ const noGmail = () => ({
35
35
  error: "Gmail isn't connected — connect your email on goosetools.com/dashboard/brand-manager",
36
36
  });
37
37
 
38
- /** Thread metadata rows for the mirror, from fetched threads. */
39
- const threadMeta = (threads) =>
40
- threads.map((t) => ({
41
- threadId: t.id,
42
- accountEmail: t.account,
43
- fromName: t.last.from.replace(/\s*<[^>]*>\s*/, "").trim() || null,
44
- fromEmail: (t.last.from.match(/<([^>]+)>/)?.[1] ?? t.last.from).trim() || null,
45
- subject: t.subject,
46
- lastMessageAt: t.last.date,
47
- hasUnsentDraft: t.hasUnsentDraft,
48
- }));
49
-
50
38
  /** Resolve decision attachment paths ("stats/screenshots/x.png") under BRAND_DIR. */
51
39
  const resolveAttachments = (paths = []) =>
52
40
  paths.map((p) => (p.startsWith("/") ? p : join(BRAND_DIR, p))).filter((p) => existsSync(p));
@@ -73,7 +61,7 @@ async function draftForThread(job, thread) {
73
61
  return {
74
62
  ok: true,
75
63
  summary: `skip — ${decision.summary}`,
76
- mirror: buildMirror({ threads: threadMeta([thread]) }),
64
+ mirror: buildMirror({ threads: [thread] }),
77
65
  };
78
66
  }
79
67
 
@@ -105,7 +93,7 @@ async function draftForThread(job, thread) {
105
93
  summary: decision.summary,
106
94
  resultBody: decision.body ?? "",
107
95
  flags: decision.flags ?? [],
108
- mirror: buildMirror({ threads: threadMeta([thread]) }),
96
+ mirror: buildMirror({ threads: [thread] }),
109
97
  };
110
98
  }
111
99
 
@@ -177,7 +165,7 @@ export async function runScan(job) {
177
165
  ok: true,
178
166
  summary: lines.length ? lines.join(" · ").slice(0, 1900) : "No new brand mail needing a draft.",
179
167
  flags,
180
- mirror: buildMirror({ threads: threadMeta(threads) }),
168
+ mirror: buildMirror({ threads }),
181
169
  };
182
170
  }
183
171
 
package/worker/mirror.js CHANGED
@@ -74,15 +74,22 @@ export function collectKnowledge() {
74
74
  * machine is defined in exactly one place, right here.
75
75
  */
76
76
  export function collectThreads(threads) {
77
- return threads.map((t) => ({
78
- threadId: t.id,
79
- accountEmail: t.account ?? null,
80
- fromName: t.fromName ?? null,
81
- fromEmail: t.fromEmail ?? null,
82
- subject: t.subject ?? null,
83
- lastMessageAt: t.lastMessageAt ?? null,
84
- hasUnsentDraft: Boolean(t.hasUnsentDraft),
85
- }));
77
+ return threads.map((t) => {
78
+ // Raw Thread objects in, mirror rows out. This is the ONLY place that
79
+ // shape is decided — callers must not pre-map, or threadId silently
80
+ // becomes undefined and the server drops the row (which is exactly what
81
+ // happened: deals and knowledge mirrored, threads never did).
82
+ const from = t.last?.from ?? "";
83
+ return {
84
+ threadId: t.id,
85
+ accountEmail: t.account ?? null,
86
+ fromName: from.replace(/\s*<[^>]*>\s*/, "").replace(/^"|"$/g, "").trim() || null,
87
+ fromEmail: (from.match(/<([^>]+)>/)?.[1] ?? from).trim() || null,
88
+ subject: t.subject ?? null,
89
+ lastMessageAt: t.last?.date ?? null,
90
+ hasUnsentDraft: Boolean(t.hasUnsentDraft),
91
+ };
92
+ });
86
93
  }
87
94
 
88
95
  /** The full mirror payload for a complete() call. */