fraim 2.0.177 → 2.0.180

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.
Files changed (77) hide show
  1. package/dist/src/ai-hub/desktop-main.js +2 -2
  2. package/dist/src/ai-hub/server.js +50 -1
  3. package/dist/src/api/admin/payments.js +33 -0
  4. package/dist/src/api/admin/sales-leads.js +21 -0
  5. package/dist/src/api/payment/create-session.js +338 -0
  6. package/dist/src/api/payment/dashboard-link.js +149 -0
  7. package/dist/src/api/payment/session-details.js +31 -0
  8. package/dist/src/api/payment/webhook.js +587 -0
  9. package/dist/src/api/personas/me.js +29 -0
  10. package/dist/src/api/pricing/get-config.js +25 -0
  11. package/dist/src/api/sales/contact.js +44 -0
  12. package/dist/src/cli/commands/add-provider.js +74 -61
  13. package/dist/src/cli/commands/add-surface.js +128 -0
  14. package/dist/src/cli/commands/login.js +5 -69
  15. package/dist/src/cli/commands/setup.js +27 -347
  16. package/dist/src/cli/distribution/marketplace-bundles.js +580 -0
  17. package/dist/src/cli/fraim.js +2 -0
  18. package/dist/src/cli/mcp/ide-formats.js +5 -3
  19. package/dist/src/cli/mcp/mcp-server-registry.js +10 -3
  20. package/dist/src/cli/providers/local-provider-registry.js +2 -3
  21. package/dist/src/cli/setup/auto-mcp-setup.js +9 -32
  22. package/dist/src/cli/setup/ide-detector.js +34 -14
  23. package/dist/src/config/persona-capability-bundles.js +17 -13
  24. package/dist/src/db/payment-repository.js +61 -0
  25. package/dist/src/first-run/session-service.js +2 -2
  26. package/dist/src/fraim/config-loader.js +11 -0
  27. package/dist/src/fraim/db-service.js +2387 -0
  28. package/dist/src/fraim/issues.js +152 -0
  29. package/dist/src/fraim/template-processor.js +184 -0
  30. package/dist/src/fraim/utils/request-utils.js +23 -0
  31. package/dist/src/local-mcp-server/stdio-server.js +28 -4
  32. package/dist/src/local-mcp-server/usage-collector.js +24 -0
  33. package/dist/src/middleware/auth.js +266 -0
  34. package/dist/src/middleware/cors-config.js +111 -0
  35. package/dist/src/middleware/logger.js +116 -0
  36. package/dist/src/middleware/rate-limit.js +110 -0
  37. package/dist/src/middleware/reject-query-api-key.js +45 -0
  38. package/dist/src/middleware/security-headers.js +41 -0
  39. package/dist/src/middleware/telemetry.js +134 -0
  40. package/dist/src/models/payment.js +2 -0
  41. package/dist/src/routes/analytics.js +1447 -0
  42. package/dist/src/routes/app-routes.js +32 -0
  43. package/dist/src/routes/auth-routes.js +505 -0
  44. package/dist/src/routes/oauth-routes.js +325 -0
  45. package/dist/src/routes/payment-routes.js +186 -0
  46. package/dist/src/routes/persona-catalog-routes.js +84 -0
  47. package/dist/src/services/admin-service.js +229 -0
  48. package/dist/src/services/audit-log-persistence.js +60 -0
  49. package/dist/src/services/audit-log.js +69 -0
  50. package/dist/src/services/cookie-service.js +129 -0
  51. package/dist/src/services/dashboard-access.js +27 -0
  52. package/dist/src/services/demo-seed-service.js +139 -0
  53. package/dist/src/services/email-code.js +23 -0
  54. package/dist/src/services/email-service-clean.js +782 -0
  55. package/dist/src/services/email-service.js +951 -0
  56. package/dist/src/services/installer-service.js +131 -0
  57. package/dist/src/services/mcp-oauth-store.js +33 -0
  58. package/dist/src/services/mcp-service.js +823 -0
  59. package/dist/src/services/oauth-helpers.js +127 -0
  60. package/dist/src/services/org-service.js +89 -0
  61. package/dist/src/services/persona-entitlement-service.js +288 -0
  62. package/dist/src/services/provider-service.js +215 -0
  63. package/dist/src/services/registry-service.js +628 -0
  64. package/dist/src/services/session-service.js +86 -0
  65. package/dist/src/services/trial-reminder-service.js +120 -0
  66. package/dist/src/services/usage-analytics-service.js +419 -0
  67. package/dist/src/services/workspace-identity.js +21 -0
  68. package/dist/src/types/analytics.js +2 -0
  69. package/dist/src/utils/payment-calculator.js +52 -0
  70. package/extensions/office-word/favicon.ico +0 -0
  71. package/extensions/office-word/icon-64.png +0 -0
  72. package/extensions/office-word/manifest.xml +33 -0
  73. package/extensions/office-word/taskpane.html +242 -0
  74. package/package.json +14 -3
  75. package/public/ai-hub/index.html +14 -2
  76. package/public/ai-hub/script.js +340 -66
  77. package/public/ai-hub/styles.css +83 -0
@@ -0,0 +1,951 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmailService = void 0;
4
+ const resend_1 = require("resend");
5
+ class EmailService {
6
+ constructor() {
7
+ const apiKey = process.env.RESEND_API_KEY;
8
+ if (!apiKey) {
9
+ throw new Error('RESEND_API_KEY environment variable is required');
10
+ }
11
+ this.resend = new resend_1.Resend(apiKey);
12
+ // Use custom domain if configured, otherwise use Resend default
13
+ this.fromEmail = process.env.RESEND_FROM_EMAIL || 'FRAIM <onboarding@resend.dev>';
14
+ this.baseUrl = (process.env.BASE_URL || 'https://fraimworks.ai').replace(/\/$/, '');
15
+ }
16
+ /** Wrapper around resend.emails.send — no-ops in test mode to avoid burning real API quota. */
17
+ async sendEmail(payload) {
18
+ if (process.env.NODE_ENV === 'test') {
19
+ console.log(`[TEST] Email suppressed — would send to ${Array.isArray(payload.to) ? payload.to.join(', ') : payload.to}: "${payload.subject}"`);
20
+ return { data: { id: 'test-suppressed' }, error: null, headers: null };
21
+ }
22
+ return this.sendWithResend(payload);
23
+ }
24
+ async sendWithResend(payload) {
25
+ if (process.env.NODE_ENV === 'test' && process.env.ALLOW_REAL_EMAILS_IN_TEST !== 'true') {
26
+ console.log(`[TEST] Email suppressed - would send to ${Array.isArray(payload.to) ? payload.to.join(', ') : payload.to}: "${payload.subject}"`);
27
+ return { data: { id: 'test-suppressed' }, error: null, headers: null };
28
+ }
29
+ return this.resend.emails.send(payload);
30
+ }
31
+ async sendEmailCode(to, actionUrl, code, options = {}) {
32
+ const template = this.generateEmailCodeTemplate(actionUrl, code, options.purpose ?? 'sign-in');
33
+ try {
34
+ const result = await this.sendWithResend({
35
+ from: this.fromEmail,
36
+ to,
37
+ subject: template.subject,
38
+ html: template.html,
39
+ text: template.text
40
+ });
41
+ if (result.error) {
42
+ console.error(`❌ Resend API error (email code to ${to}):`, JSON.stringify(result.error));
43
+ throw new Error(`Resend error: ${result.error.message || JSON.stringify(result.error)}`);
44
+ }
45
+ console.log(`✅ Email code sent to ${to}`, { id: result.data?.id });
46
+ }
47
+ catch (error) {
48
+ console.error(`❌ Failed to send email code to ${to}:`, error);
49
+ throw error;
50
+ }
51
+ }
52
+ /**
53
+ * Send welcome email after trial key created
54
+ */
55
+ async sendWelcomeEmail(to, apiKey, expiresAt) {
56
+ const template = this.generateWelcomeTemplate(apiKey, expiresAt);
57
+ try {
58
+ const result = await this.sendWithResend({
59
+ from: this.fromEmail,
60
+ to,
61
+ subject: template.subject,
62
+ html: template.html,
63
+ text: template.text
64
+ });
65
+ if (result.error) {
66
+ console.error(`❌ Resend API error (welcome email to ${to}):`, JSON.stringify(result.error));
67
+ throw new Error(`Resend error: ${result.error.message || JSON.stringify(result.error)}`);
68
+ }
69
+ console.log(`✅ Welcome email sent to ${to}`, { id: result.data?.id });
70
+ }
71
+ catch (error) {
72
+ console.error(`❌ Failed to send welcome email to ${to}:`, error);
73
+ throw error;
74
+ }
75
+ }
76
+ /**
77
+ * Send trial check-in email (Day 7)
78
+ */
79
+ async sendTrialCheckIn(to, expiresAt) {
80
+ const template = this.generateTrialCheckInTemplate(expiresAt);
81
+ await this.sendWithResend({
82
+ from: this.fromEmail,
83
+ to,
84
+ subject: template.subject,
85
+ html: template.html,
86
+ text: template.text
87
+ });
88
+ console.log(`✅ Trial check-in email sent to ${to}`);
89
+ }
90
+ /**
91
+ * Send trial expiration warning (Day 12)
92
+ */
93
+ async sendTrialExpiring(to, expiresAt, daysRemaining) {
94
+ const template = this.generateTrialExpiringTemplate(expiresAt, daysRemaining);
95
+ await this.sendWithResend({
96
+ from: this.fromEmail,
97
+ to,
98
+ subject: template.subject,
99
+ html: template.html,
100
+ text: template.text
101
+ });
102
+ console.log(`✅ Trial expiring email sent to ${to}`);
103
+ }
104
+ /**
105
+ * Send trial expired notification
106
+ */
107
+ async sendTrialExpired(to, expiredAt) {
108
+ const template = this.generateTrialExpiredTemplate(expiredAt);
109
+ await this.sendWithResend({
110
+ from: this.fromEmail,
111
+ to,
112
+ subject: template.subject,
113
+ html: template.html,
114
+ text: template.text
115
+ });
116
+ console.log(`✅ Trial expired email sent to ${to}`);
117
+ }
118
+ /**
119
+ * Send subscription activated confirmation
120
+ */
121
+ async sendSubscriptionActivated(to, plan, amountCents, billingCycle, nextBillingDate, dashboardUrl, founderDiscount = false) {
122
+ const template = this.generateSubscriptionActivatedTemplate(plan, amountCents, billingCycle, nextBillingDate, dashboardUrl, founderDiscount);
123
+ try {
124
+ const result = await this.sendWithResend({
125
+ from: this.fromEmail,
126
+ to,
127
+ subject: template.subject,
128
+ html: template.html,
129
+ text: template.text
130
+ });
131
+ if (result.error) {
132
+ console.error(`❌ Resend API error (subscription activated to ${to}):`, JSON.stringify(result.error));
133
+ throw new Error(`Resend error: ${result.error.message || JSON.stringify(result.error)}`);
134
+ }
135
+ console.log(`✅ Subscription activated email sent to ${to}`, { id: result.data?.id });
136
+ }
137
+ catch (error) {
138
+ console.error(`❌ Failed to send subscription activated email to ${to}:`, error);
139
+ throw error;
140
+ }
141
+ }
142
+ /**
143
+ * Send monthly renewal receipt
144
+ */
145
+ async sendRenewalReceipt(to, amount, nextBillingDate, invoiceUrl) {
146
+ const template = this.generateRenewalReceiptTemplate(amount, nextBillingDate, invoiceUrl);
147
+ const result = await this.sendWithResend({
148
+ from: this.fromEmail,
149
+ to,
150
+ subject: template.subject,
151
+ html: template.html,
152
+ text: template.text
153
+ });
154
+ if (result.error) {
155
+ console.error(`❌ Resend API error (renewal receipt to ${to}):`, JSON.stringify(result.error));
156
+ throw new Error(`Resend error: ${result.error.message || JSON.stringify(result.error)}`);
157
+ }
158
+ console.log(`✅ Renewal receipt sent to ${to}`, { id: result.data?.id });
159
+ }
160
+ /**
161
+ * Send payment failed alert
162
+ */
163
+ async sendPaymentFailed(to, billingPortalUrl, gracePeriodEndsAt) {
164
+ const template = this.generatePaymentFailedTemplate(billingPortalUrl, gracePeriodEndsAt);
165
+ const result = await this.sendWithResend({
166
+ from: this.fromEmail,
167
+ to,
168
+ subject: template.subject,
169
+ html: template.html,
170
+ text: template.text
171
+ });
172
+ if (result.error) {
173
+ console.error(`❌ Resend API error (payment failed alert to ${to}):`, JSON.stringify(result.error));
174
+ throw new Error(`Resend error: ${result.error.message || JSON.stringify(result.error)}`);
175
+ }
176
+ console.log(`✅ Payment failed alert sent to ${to}`, { id: result.data?.id });
177
+ }
178
+ /**
179
+ * Send payment retry succeeded
180
+ */
181
+ async sendPaymentRestored(to, nextBillingDate) {
182
+ const template = this.generatePaymentRestoredTemplate(nextBillingDate);
183
+ const result = await this.sendWithResend({
184
+ from: this.fromEmail,
185
+ to,
186
+ subject: template.subject,
187
+ html: template.html,
188
+ text: template.text
189
+ });
190
+ if (result.error) {
191
+ console.error(`❌ Resend API error (payment restored to ${to}):`, JSON.stringify(result.error));
192
+ throw new Error(`Resend error: ${result.error.message || JSON.stringify(result.error)}`);
193
+ }
194
+ console.log(`✅ Payment restored email sent to ${to}`, { id: result.data?.id });
195
+ }
196
+ /**
197
+ * Send subscription cancelled notification
198
+ */
199
+ async sendSubscriptionCancelled(to, endsAt) {
200
+ const template = this.generateSubscriptionCancelledTemplate(endsAt);
201
+ const result = await this.sendWithResend({
202
+ from: this.fromEmail,
203
+ to,
204
+ subject: template.subject,
205
+ html: template.html,
206
+ text: template.text
207
+ });
208
+ if (result.error) {
209
+ console.error(`❌ Resend API error (subscription cancelled to ${to}):`, JSON.stringify(result.error));
210
+ throw new Error(`Resend error: ${result.error.message || JSON.stringify(result.error)}`);
211
+ }
212
+ console.log(`✅ Subscription cancelled email sent to ${to}`, { id: result.data?.id });
213
+ }
214
+ // ========== Email Template Generators ==========
215
+ generateEmailCodeTemplate(actionUrl, code, purpose) {
216
+ const expiresInMinutes = 15;
217
+ const isRequestAccess = purpose === 'request-access';
218
+ const heading = isRequestAccess ? 'Start your FRAIM trial' : 'Sign in to FRAIM';
219
+ const introText = isRequestAccess
220
+ ? 'Open FRAIM and enter this code to start your 14-day trial:'
221
+ : 'Open FRAIM and enter this code to sign in:';
222
+ const buttonText = isRequestAccess ? 'Open FRAIM Get Started' : 'Open FRAIM Sign-In';
223
+ const codeText = `Open ${actionUrl} and enter this code:\n\n${code}`;
224
+ return {
225
+ subject: isRequestAccess ? 'Your FRAIM verification code' : 'Sign in to FRAIM',
226
+ html: `
227
+ <!DOCTYPE html>
228
+ <html>
229
+ <head>
230
+ <style>
231
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
232
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
233
+ .button {
234
+ display: inline-block;
235
+ padding: 12px 24px;
236
+ background-color: #10b981;
237
+ color: #ffffff !important;
238
+ text-decoration: none;
239
+ border-radius: 5px;
240
+ margin: 20px 0;
241
+ }
242
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
243
+ </style>
244
+ </head>
245
+ <body>
246
+ <div class="container">
247
+ <h2>${heading}</h2>
248
+ <p>${introText}</p>
249
+ <div style="font-size:28px;letter-spacing:4px;font-weight:bold;background:#f3f4f6;border:1px solid #d1d5db;border-radius:6px;padding:12px 16px;display:inline-block;color:#111827">${code}</div>
250
+ <p><a href="${actionUrl}" style="display:inline-block;padding:12px 24px;background-color:#10b981;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">${buttonText}</a></p>
251
+ <p>This code expires in ${expiresInMinutes} minutes.</p>
252
+ <p>If you didn't request this, you can safely ignore this email.</p>
253
+ <div class="footer">
254
+ <p>FRAIM - Framework for Rigor-based AI Management<br>
255
+ <a href="${this.baseUrl}">fraimworks.ai</a></p>
256
+ </div>
257
+ </div>
258
+ </body>
259
+ </html>
260
+ `,
261
+ text: `${heading}\n\n${codeText}\n\nThis code expires in ${expiresInMinutes} minutes.\n\nIf you didn't request this, you can safely ignore this email.\n\nFRAIM - Framework for Rigor-based AI Management\n${this.baseUrl}`
262
+ };
263
+ }
264
+ generateWelcomeTemplate(apiKey, expiresAt) {
265
+ const daysRemaining = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
266
+ return {
267
+ subject: 'Welcome to FRAIM! Your 14-day trial starts now',
268
+ html: `
269
+ <!DOCTYPE html>
270
+ <html>
271
+ <head>
272
+ <style>
273
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
274
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
275
+ .api-key {
276
+ background: #f5f5f5;
277
+ padding: 15px;
278
+ border-radius: 5px;
279
+ font-family: monospace;
280
+ word-break: break-all;
281
+ margin: 20px 0;
282
+ }
283
+ .button {
284
+ display: inline-block;
285
+ padding: 12px 24px;
286
+ background-color: #10b981;
287
+ color: #ffffff !important;
288
+ text-decoration: none;
289
+ border-radius: 5px;
290
+ margin: 20px 0;
291
+ }
292
+ .highlight { background: #fff3cd; padding: 15px; border-left: 4px solid #ffc107; margin: 20px 0; }
293
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
294
+ </style>
295
+ </head>
296
+ <body>
297
+ <div class="container">
298
+ <h2>🎉 Welcome to FRAIM!</h2>
299
+ <p>Your 14-day free trial is now active. Here's your API key:</p>
300
+ <div class="api-key">${apiKey}</div>
301
+
302
+ <div class="highlight">
303
+ <strong>Trial expires:</strong> ${expiresAt.toLocaleDateString()} (${daysRemaining} days remaining)
304
+ </div>
305
+
306
+ <h3>Get Started:</h3>
307
+ <ol>
308
+ <li><a href="${this.baseUrl}/dashboard">Access your dashboard</a></li>
309
+ <li>Download the installer for your platform</li>
310
+ <li>Run your first AI-managed workflow</li>
311
+ </ol>
312
+
313
+ <a href="${this.baseUrl}/docs/quickstart" style="display:inline-block;padding:12px 24px;background-color:#10b981;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">View Quickstart Guide</a>
314
+ <p>Prefer a walkthrough first? Watch the onboarding videos on the FRAIM website:</p>
315
+ <p><a href="${this.baseUrl}/resources.html#videos">${this.baseUrl}/resources.html#videos</a></p>
316
+
317
+ <div class="footer">
318
+ <p>Questions? Reply to this email or visit <a href="${this.baseUrl}/docs">our docs</a>.</p>
319
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
320
+ </div>
321
+ </div>
322
+ </body>
323
+ </html>
324
+ `,
325
+ text: `Welcome to FRAIM!\n\nYour 14-day free trial is now active. Here's your API key:\n\n${apiKey}\n\nTrial expires: ${expiresAt.toLocaleDateString()} (${daysRemaining} days remaining)\n\nGet Started:\n1. Access your dashboard: ${this.baseUrl}/dashboard\n2. Download the installer for your platform\n3. Run your first AI-managed workflow\n\nView Quickstart Guide: ${this.baseUrl}/docs/quickstart\nWatch onboarding videos: ${this.baseUrl}/resources.html#videos\n\nQuestions? Reply to this email or visit our docs.`
326
+ };
327
+ }
328
+ generateTrialCheckInTemplate(expiresAt) {
329
+ const daysRemaining = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
330
+ return {
331
+ subject: "You've been using FRAIM for a week!",
332
+ html: `
333
+ <!DOCTYPE html>
334
+ <html>
335
+ <head>
336
+ <style>
337
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
338
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
339
+ .button {
340
+ display: inline-block;
341
+ padding: 12px 24px;
342
+ background-color: #10b981;
343
+ color: #ffffff !important;
344
+ text-decoration: none;
345
+ border-radius: 5px;
346
+ margin: 20px 0;
347
+ }
348
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
349
+ </style>
350
+ </head>
351
+ <body>
352
+ <div class="container">
353
+ <h2>How's FRAIM working for you?</h2>
354
+ <p>You've been using FRAIM for a week now. We hope it's helping you manage your AI agents more effectively!</p>
355
+
356
+ <p><strong>Your trial ends in ${daysRemaining} days</strong> (${expiresAt.toLocaleDateString()})</p>
357
+
358
+ <h3>Upgrade to Pro for:</h3>
359
+ <ul>
360
+ <li>Unlimited usage (no expiration)</li>
361
+ <li>Priority support</li>
362
+ <li>Advanced workflows</li>
363
+ <li>Team collaboration features</li>
364
+ </ul>
365
+
366
+ <p><strong>Special offer for early adopters:</strong> 90% off for pre-seed, pre-revenue founders!</p>
367
+
368
+ <a href="${this.baseUrl}/upgrade" style="display:inline-block;padding:12px 24px;background-color:#10b981;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">Upgrade to Pro</a>
369
+
370
+ <div class="footer">
371
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
372
+ </div>
373
+ </div>
374
+ </body>
375
+ </html>
376
+ `,
377
+ text: `How's FRAIM working for you?\n\nYou've been using FRAIM for a week now. We hope it's helping you manage your AI agents more effectively!\n\nYour trial ends in ${daysRemaining} days (${expiresAt.toLocaleDateString()})\n\nUpgrade to Pro for:\n- Unlimited usage (no expiration)\n- Priority support\n- Advanced workflows\n- Team collaboration features\n\nSpecial offer for early adopters: 90% off for pre-seed, pre-revenue founders!\n\nUpgrade: ${this.baseUrl}/upgrade`
378
+ };
379
+ }
380
+ generateTrialExpiringTemplate(expiresAt, daysRemaining) {
381
+ const urgency = daysRemaining <= 1 ? '🚨 Last day' : '⚠️ Trial ending soon';
382
+ return {
383
+ subject: `${urgency} - Your FRAIM trial expires in ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''}`,
384
+ html: `
385
+ <!DOCTYPE html>
386
+ <html>
387
+ <head>
388
+ <style>
389
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
390
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
391
+ .button {
392
+ display: inline-block;
393
+ padding: 12px 24px;
394
+ background-color: #dc3545;
395
+ color: #ffffff !important;
396
+ text-decoration: none;
397
+ border-radius: 5px;
398
+ margin: 20px 0;
399
+ }
400
+ .warning { background: #fff3cd; padding: 20px; border-left: 4px solid #ffc107; margin: 20px 0; }
401
+ .pricing { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; }
402
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <div class="container">
407
+ <h2>${urgency}</h2>
408
+
409
+ <div class="warning">
410
+ <strong>Your FRAIM trial expires on ${expiresAt.toLocaleDateString()}</strong><br>
411
+ (${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining)
412
+ </div>
413
+
414
+ <p><strong>What happens when your trial ends:</strong></p>
415
+ <ul>
416
+ <li>Your API key will stop working</li>
417
+ <li>You won't be able to run workflows</li>
418
+ <li>Your data is preserved - upgrade anytime to reactivate</li>
419
+ </ul>
420
+
421
+ <div class="pricing">
422
+ <h3>Choose your plan:</h3>
423
+ <p><strong>Regular pricing:</strong> $200/month or $2,000/year (save 17%)</p>
424
+ <p><strong>Founder discount (90% off):</strong> $20/month or $200/year<br>
425
+ <em>Available to pre-seed, pre-revenue founders with corporate email</em></p>
426
+ </div>
427
+
428
+ <a href="${this.baseUrl}/upgrade" style="display:inline-block;padding:12px 24px;background-color:#dc3545;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">Upgrade Now - Don't Lose Access</a>
429
+
430
+ <div class="footer">
431
+ <p>Questions? Reply to this email.</p>
432
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
433
+ </div>
434
+ </div>
435
+ </body>
436
+ </html>
437
+ `,
438
+ text: `${urgency}\n\nYour FRAIM trial expires on ${expiresAt.toLocaleDateString()} (${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining)\n\nWhat happens when your trial ends:\n- Your API key will stop working\n- You won't be able to run workflows\n- Your data is preserved - upgrade anytime to reactivate\n\nChoose your plan:\n\nRegular pricing: $200/month or $2,000/year (save 17%)\n\nFounder discount (90% off): $20/month or $200/year\nAvailable to pre-seed, pre-revenue founders with corporate email\n\nUpgrade now: ${this.baseUrl}/upgrade`
439
+ };
440
+ }
441
+ generateTrialExpiredTemplate(expiredAt) {
442
+ return {
443
+ subject: 'Your FRAIM trial has ended',
444
+ html: `
445
+ <!DOCTYPE html>
446
+ <html>
447
+ <head>
448
+ <style>
449
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
450
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
451
+ .button {
452
+ display: inline-block;
453
+ padding: 12px 24px;
454
+ background-color: #10b981;
455
+ color: #ffffff !important;
456
+ text-decoration: none;
457
+ border-radius: 5px;
458
+ margin: 20px 0;
459
+ }
460
+ .highlight { background: #e7f3ff; padding: 20px; border-left: 4px solid #0066cc; margin: 20px 0; }
461
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
462
+ </style>
463
+ </head>
464
+ <body>
465
+ <div class="container">
466
+ <h2>Your FRAIM trial has ended</h2>
467
+ <p>Your 14-day trial expired on ${expiredAt.toLocaleDateString()}.</p>
468
+
469
+ <div class="highlight">
470
+ <p><strong>Your data is safe and preserved.</strong></p>
471
+ <p>Upgrade now to reactivate your account and continue using FRAIM.</p>
472
+ </div>
473
+
474
+ <h3>Pricing plans:</h3>
475
+ <ul>
476
+ <li><strong>Regular:</strong> $200/month or $2,000/year</li>
477
+ <li><strong>Founders (90% off):</strong> $20/month or $200/year</li>
478
+ </ul>
479
+
480
+ <a href="${this.baseUrl}/upgrade" style="display:inline-block;padding:12px 24px;background-color:#10b981;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">Reactivate Your Account</a>
481
+
482
+ <div class="footer">
483
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
484
+ </div>
485
+ </div>
486
+ </body>
487
+ </html>
488
+ `,
489
+ text: `Your FRAIM trial has ended\n\nYour 14-day trial expired on ${expiredAt.toLocaleDateString()}.\n\nYour data is safe and preserved. Upgrade now to reactivate your account and continue using FRAIM.\n\nPricing plans:\n- Regular: $200/month or $2,000/year\n- Founders (90% off): $20/month or $200/year\n\nReactivate your account: ${this.baseUrl}/upgrade`
490
+ };
491
+ }
492
+ generateSubscriptionActivatedTemplate(plan, amountCents, billingCycle, nextBillingDate, dashboardUrl, founderDiscount) {
493
+ const formattedAmount = (amountCents / 100).toFixed(2);
494
+ const displayAmount = `$${formattedAmount}`;
495
+ const cycleText = billingCycle === 'monthly' ? 'month' : 'year';
496
+ const dashboardAccess = this.generateDashboardAccessSection(dashboardUrl, 'Access Your Dashboard');
497
+ return {
498
+ subject: '🎉 Welcome to FRAIM Pro!',
499
+ html: `
500
+ <!DOCTYPE html>
501
+ <html>
502
+ <head>
503
+ <style>
504
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
505
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
506
+ .success { background: #d4edda; padding: 20px; border-left: 4px solid #28a745; margin: 20px 0; }
507
+ .api-key {
508
+ background: #f5f5f5;
509
+ padding: 15px;
510
+ border-radius: 5px;
511
+ font-family: monospace;
512
+ word-break: break-all;
513
+ margin: 20px 0;
514
+ }
515
+ .button {
516
+ display: inline-block;
517
+ padding: 12px 24px;
518
+ background-color: #10b981;
519
+ color: #ffffff !important;
520
+ text-decoration: none;
521
+ border-radius: 5px;
522
+ margin: 20px 0;
523
+ }
524
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
525
+ </style>
526
+ </head>
527
+ <body>
528
+ <div class="container">
529
+ <h2>🎉 Welcome to FRAIM Pro!</h2>
530
+
531
+ <div class="success">
532
+ <strong>Payment confirmed!</strong><br>
533
+ Your subscription is now active.
534
+ </div>
535
+
536
+ <h3>Subscription Details:</h3>
537
+ <ul>
538
+ <li><strong>Plan:</strong> ${plan}${founderDiscount ? ' (Founder Discount - 90% off)' : ''}</li>
539
+ <li><strong>Amount:</strong> ${displayAmount}/${cycleText}</li>
540
+ <li><strong>Next billing date:</strong> ${nextBillingDate.toLocaleDateString()}</li>
541
+ </ul>
542
+
543
+ <p>${dashboardAccess.lead}</p>
544
+ ${dashboardAccess.buttonHtml}
545
+
546
+ <p>Need a walkthrough? Watch the onboarding videos on the FRAIM website:</p>
547
+ <p><a href="${this.baseUrl}/resources.html#videos">${this.baseUrl}/resources.html#videos</a></p>
548
+
549
+ <p>Manage your subscription (update payment method, change plan, etc.):</p>
550
+ <a href="${this.baseUrl}/billing">Billing Portal</a>
551
+
552
+ <div class="footer">
553
+ <p>Questions? Reply to this email.</p>
554
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
555
+ </div>
556
+ </div>
557
+ </body>
558
+ </html>
559
+ `,
560
+ text: `Welcome to FRAIM Pro!\n\nPayment confirmed! Your subscription is now active.\n\nSubscription Details:\n- Plan: ${plan}${founderDiscount ? ' (Founder Discount - 90% off)' : ''}\n- Amount: ${displayAmount}/${cycleText}\n- Next billing date: ${nextBillingDate.toLocaleDateString()}\n\n${dashboardAccess.lead}\n${dashboardAccess.buttonText}\nWatch onboarding videos: ${this.baseUrl}/resources.html#videos\n\nManage subscription: ${this.baseUrl}/billing`
561
+ };
562
+ }
563
+ generateDashboardAccessSection(dashboardUrl, ctaLabel) {
564
+ const lead = 'Your API key and install commands are available in Account.';
565
+ return {
566
+ lead,
567
+ buttonHtml: `<a href="${dashboardUrl}" style="display:inline-block;padding:12px 24px;background-color:#10b981;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">${ctaLabel}</a>`,
568
+ buttonText: `${ctaLabel}: ${dashboardUrl}`
569
+ };
570
+ }
571
+ generateRenewalReceiptTemplate(amount, nextBillingDate, invoiceUrl) {
572
+ const formattedAmount = (amount / 100).toFixed(2);
573
+ return {
574
+ subject: `Receipt for FRAIM Pro - ${formattedAmount}`,
575
+ html: `
576
+ <!DOCTYPE html>
577
+ <html>
578
+ <head>
579
+ <style>
580
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
581
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
582
+ .receipt { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; }
583
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
584
+ </style>
585
+ </head>
586
+ <body>
587
+ <div class="container">
588
+ <h2>Payment Receipt</h2>
589
+ <p>Thank you for your continued subscription to FRAIM Pro!</p>
590
+
591
+ <div class="receipt">
592
+ <h3>Receipt Details:</h3>
593
+ <p><strong>Amount charged:</strong> ${formattedAmount}</p>
594
+ <p><strong>Next billing date:</strong> ${nextBillingDate.toLocaleDateString()}</p>
595
+ ${invoiceUrl ? `<p><a href="${invoiceUrl}">Download Invoice</a></p>` : ''}
596
+ </div>
597
+
598
+ <p>Your subscription remains active with no changes.</p>
599
+
600
+ <div class="footer">
601
+ <p>Manage your subscription: <a href="${this.baseUrl}/billing">Billing Portal</a></p>
602
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
603
+ </div>
604
+ </div>
605
+ </body>
606
+ </html>
607
+ `,
608
+ text: `Payment Receipt\n\nThank you for your continued subscription to FRAIM Pro!\n\nReceipt Details:\n- Amount charged: ${formattedAmount}\n- Next billing date: ${nextBillingDate.toLocaleDateString()}\n${invoiceUrl ? `- Invoice: ${invoiceUrl}\n` : ''}\nYour subscription remains active with no changes.\n\nManage subscription: ${this.baseUrl}/billing`
609
+ };
610
+ }
611
+ generatePaymentFailedTemplate(billingPortalUrl, gracePeriodEndsAt) {
612
+ const daysRemaining = Math.ceil((gracePeriodEndsAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
613
+ return {
614
+ subject: '🚨 FRAIM: Payment Failed - Action Required',
615
+ html: `
616
+ <!DOCTYPE html>
617
+ <html>
618
+ <head>
619
+ <style>
620
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
621
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
622
+ .alert { background: #f8d7da; padding: 20px; border-left: 4px solid #dc3545; margin: 20px 0; }
623
+ .button {
624
+ display: inline-block;
625
+ padding: 12px 24px;
626
+ background-color: #dc3545;
627
+ color: #ffffff !important;
628
+ text-decoration: none;
629
+ border-radius: 5px;
630
+ margin: 20px 0;
631
+ }
632
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
633
+ </style>
634
+ </head>
635
+ <body>
636
+ <div class="container">
637
+ <h2>⚠️ Payment Failed</h2>
638
+
639
+ <div class="alert">
640
+ <strong>Your FRAIM subscription payment was declined.</strong><br>
641
+ Your access has been suspended.
642
+ </div>
643
+
644
+ <p><strong>What this means:</strong></p>
645
+ <ul>
646
+ <li>Your API key has been suspended and won't work</li>
647
+ <li>You have ${daysRemaining} days to update your payment method</li>
648
+ <li>After ${gracePeriodEndsAt.toLocaleDateString()}, your subscription will be cancelled</li>
649
+ </ul>
650
+
651
+ <p><strong>To restore access:</strong></p>
652
+ <ol>
653
+ <li>Click the button below to update your payment method</li>
654
+ <li>Your subscription will be automatically renewed</li>
655
+ <li>Your API key will be reactivated immediately</li>
656
+ </ol>
657
+
658
+ <a href="${billingPortalUrl}" style="display:inline-block;padding:12px 24px;background-color:#dc3545;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">Update Payment Method</a>
659
+
660
+ <div class="footer">
661
+ <p>Need help? Reply to this email.</p>
662
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
663
+ </div>
664
+ </div>
665
+ </body>
666
+ </html>
667
+ `,
668
+ text: `⚠️ Payment Failed\n\nYour FRAIM subscription payment was declined. Your access has been suspended.\n\nWhat this means:\n- Your API key has been suspended and won't work\n- You have ${daysRemaining} days to update your payment method\n- After ${gracePeriodEndsAt.toLocaleDateString()}, your subscription will be cancelled\n\nTo restore access:\n1. Update your payment method: ${billingPortalUrl}\n2. Your subscription will be automatically renewed\n3. Your API key will be reactivated immediately\n\nNeed help? Reply to this email.`
669
+ };
670
+ }
671
+ generatePaymentRestoredTemplate(nextBillingDate) {
672
+ return {
673
+ subject: '✅ Payment successful - FRAIM access restored',
674
+ html: `
675
+ <!DOCTYPE html>
676
+ <html>
677
+ <head>
678
+ <style>
679
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
680
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
681
+ .success { background: #d4edda; padding: 20px; border-left: 4px solid #28a745; margin: 20px 0; }
682
+ .button {
683
+ display: inline-block;
684
+ padding: 12px 24px;
685
+ background-color: #10b981;
686
+ color: #ffffff !important;
687
+ text-decoration: none;
688
+ border-radius: 5px;
689
+ margin: 20px 0;
690
+ }
691
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
692
+ </style>
693
+ </head>
694
+ <body>
695
+ <div class="container">
696
+ <h2>✅ Access Restored!</h2>
697
+
698
+ <div class="success">
699
+ <strong>Payment successful!</strong><br>
700
+ Your FRAIM subscription has been renewed.
701
+ </div>
702
+
703
+ <p>Your API key is now active again and you can continue using FRAIM.</p>
704
+ <p><strong>Next billing date:</strong> ${nextBillingDate.toLocaleDateString()}</p>
705
+
706
+ <a href="${this.baseUrl}/dashboard" style="display:inline-block;padding:12px 24px;background-color:#10b981;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">Continue Using FRAIM</a>
707
+
708
+ <div class="footer">
709
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
710
+ </div>
711
+ </div>
712
+ </body>
713
+ </html>
714
+ `,
715
+ text: `✅ Access Restored!\n\nPayment successful! Your FRAIM subscription has been renewed.\n\nYour API key is now active again and you can continue using FRAIM.\n\nNext billing date: ${nextBillingDate.toLocaleDateString()}\n\nContinue using FRAIM: ${this.baseUrl}/dashboard`
716
+ };
717
+ }
718
+ generateSubscriptionCancelledTemplate(endsAt) {
719
+ return {
720
+ subject: 'FRAIM subscription cancelled',
721
+ html: `
722
+ <!DOCTYPE html>
723
+ <html>
724
+ <head>
725
+ <style>
726
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
727
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
728
+ .info { background: #fff3cd; padding: 20px; border-left: 4px solid #ffc107; margin: 20px 0; }
729
+ .button {
730
+ display: inline-block;
731
+ padding: 12px 24px;
732
+ background-color: #10b981;
733
+ color: #ffffff !important;
734
+ text-decoration: none;
735
+ border-radius: 5px;
736
+ margin: 20px 0;
737
+ }
738
+ .footer { font-size: 12px; color: #666; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; }
739
+ </style>
740
+ </head>
741
+ <body>
742
+ <div class="container">
743
+ <h2>Subscription Cancelled</h2>
744
+
745
+ <div class="info">
746
+ <p><strong>Your subscription has been cancelled.</strong></p>
747
+ <p>Your access remains active until: ${endsAt.toLocaleDateString()}</p>
748
+ </div>
749
+
750
+ <p>After this date:</p>
751
+ <ul>
752
+ <li>Your API key will stop working</li>
753
+ <li>You won't be able to run workflows</li>
754
+ <li>Your data will be preserved for 30 days</li>
755
+ </ul>
756
+
757
+ <p><strong>Changed your mind?</strong> You can reactivate your subscription anytime before ${endsAt.toLocaleDateString()}.</p>
758
+
759
+ <a href="${this.baseUrl}/billing" style="display:inline-block;padding:12px 24px;background-color:#10b981;color:#ffffff !important;text-decoration:none;border-radius:5px;margin:20px 0;font-weight:bold">Reactivate Subscription</a>
760
+
761
+ <div class="footer">
762
+ <p>We're sorry to see you go. Reply to this email to let us know how we can improve.</p>
763
+ <p>FRAIM - Framework for Rigor-based AI Management</p>
764
+ </div>
765
+ </div>
766
+ </body>
767
+ </html>
768
+ `,
769
+ text: `Subscription Cancelled\n\nYour subscription has been cancelled.\n\nYour access remains active until: ${endsAt.toLocaleDateString()}\n\nAfter this date:\n- Your API key will stop working\n- You won't be able to run workflows\n- Your data will be preserved for 30 days\n\nChanged your mind? You can reactivate your subscription anytime before ${endsAt.toLocaleDateString()}.\n\nReactivate: ${this.baseUrl}/billing\n\nWe're sorry to see you go. Reply to this email to let us know how we can improve.`
770
+ };
771
+ }
772
+ generateThankYouTemplate(customerName, issueTitle, improvements) {
773
+ const improvementsList = improvements.map(item => `• ${item}`).join('\n');
774
+ return {
775
+ subject: `Thank you for making FRAIM better! 🎉`,
776
+ html: `
777
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
778
+ <div style="text-align: center; margin-bottom: 30px;">
779
+ <h1 style="color: #2563eb; margin: 0;">Thank you for making FRAIM better!</h1>
780
+ </div>
781
+
782
+ <p style="font-size: 16px; line-height: 1.6; color: #374151;">Hi ${customerName},</p>
783
+
784
+ <p style="font-size: 16px; line-height: 1.6; color: #374151;">
785
+ I wanted to personally thank you for reporting the issue: <strong>"${issueTitle}"</strong>
786
+ </p>
787
+
788
+ <p style="font-size: 16px; line-height: 1.6; color: #374151;">
789
+ Your feedback has been implemented and FRAIM is now better because of you! Here's what we improved:
790
+ </p>
791
+
792
+ <div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
793
+ <pre style="font-family: inherit; white-space: pre-wrap; margin: 0; color: #374151;">${improvementsList}</pre>
794
+ </div>
795
+
796
+ <p style="font-size: 16px; line-height: 1.6; color: #374151;">
797
+ Your input helps us build a better AI development framework for everyone. Please keep the feedback coming!
798
+ </p>
799
+
800
+ <div style="text-align: center; margin: 30px 0;">
801
+ <a href="https://github.com/mathursrus/FRAIM/issues/new"
802
+ style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
803
+ Report Another Issue
804
+ </a>
805
+ </div>
806
+
807
+ <p style="font-size: 14px; color: #6b7280; text-align: center; margin-top: 30px;">
808
+ Thank you for being part of the FRAIM community!<br>
809
+ — Sid Mathur, FRAIM Creator
810
+ </p>
811
+ </div>
812
+ `,
813
+ text: `Hi ${customerName},
814
+
815
+ Thank you for making FRAIM better!
816
+
817
+ I wanted to personally thank you for reporting the issue: "${issueTitle}"
818
+
819
+ Your feedback has been implemented and FRAIM is now better because of you! Here's what we improved:
820
+
821
+ ${improvementsList}
822
+
823
+ Your input helps us build a better AI development framework for everyone. Please keep the feedback coming!
824
+
825
+ Report another issue: https://github.com/mathursrus/FRAIM/issues/new
826
+
827
+ Thank you for being part of the FRAIM community!
828
+ — Sid Mathur, FRAIM Creator`
829
+ };
830
+ }
831
+ generateNewsletterTemplate(recipientName, features, bugFixes, weekRange) {
832
+ const featuresHtml = features.length > 0 ? features.map(f => `
833
+ <tr>
834
+ <td style="padding: 16px 18px; background-color: #f0f7ff; border-radius: 8px; border-left: 4px solid #4A90E2;">
835
+ <strong style="color: #1e3a5f; font-size: 15px;">${f.title}</strong>
836
+ <p style="margin: 6px 0 0 0; color: #4a5568; font-size: 14px; line-height: 1.6;">${f.description}</p>
837
+ </td>
838
+ </tr>
839
+ <tr><td style="height: 10px;"></td></tr>
840
+ `).join('') : '<tr><td style="color: #94a3b8; font-size: 14px; padding: 8px 0;">No new features this week.</td></tr>';
841
+ const bugFixesHtml = bugFixes.length > 0 ? bugFixes.map(b => `
842
+ <tr>
843
+ <td style="padding: 14px 18px; background-color: #fff8f0; border-radius: 8px; border-left: 4px solid #e08a2e;">
844
+ <strong style="color: #7c4a03; font-size: 15px;">${b.title}</strong>
845
+ <p style="margin: 5px 0 0 0; color: #4a5568; font-size: 14px; line-height: 1.5;">${b.description}</p>
846
+ </td>
847
+ </tr>
848
+ <tr><td style="height: 10px;"></td></tr>
849
+ `).join('') : '<tr><td style="color: #94a3b8; font-size: 14px; padding: 8px 0;">No bug fixes this week.</td></tr>';
850
+ return {
851
+ subject: `FRAIM Weekly Update - ${weekRange} 🚀`,
852
+ html: `
853
+ <!DOCTYPE html>
854
+ <html lang="en">
855
+ <head>
856
+ <meta charset="UTF-8">
857
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
858
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
859
+ <title>FRAIM Weekly Update - ${weekRange}</title>
860
+ <style>
861
+ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 14px; line-height: 1.6; color: #333; background-color: #f1f5f9; }
862
+ .email-container { max-width: 600px; margin: 0 auto; background-color: #ffffff; }
863
+ .header { background-color: #4A90E2; color: #ffffff; padding: 30px 40px; text-align: center; }
864
+ .header h1 { margin: 0 0 8px 0; font-size: 24px; font-weight: 700; }
865
+ .header p { margin: 0; font-size: 15px; opacity: 0.9; }
866
+ .greeting { padding: 28px 40px 20px 40px; }
867
+ .greeting p { margin: 0 0 10px 0; font-size: 15px; color: #334155; line-height: 1.7; }
868
+ .section-header { background-color: #2C3E50; color: #ffffff; padding: 12px 40px; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
869
+ .section-content { padding: 20px 40px; border-bottom: 1px solid #e2e8f0; }
870
+ .footer { padding: 24px 40px; text-align: center; background-color: #f8fafc; border-top: 1px solid #e2e8f0; }
871
+ .footer-btn { display: inline-block; margin: 0 5px 8px 5px; padding: 11px 22px; background-color: #4A90E2; color: #ffffff !important; text-decoration: none; border-radius: 5px; font-weight: 600; font-size: 14px; }
872
+ .footer-btn.secondary { background-color: #2C3E50; }
873
+ @media only screen and (max-width: 600px) {
874
+ .email-container { width: 100% !important; }
875
+ .header, .greeting, .section-header, .section-content, .footer { padding-left: 20px !important; padding-right: 20px !important; }
876
+ .footer-btn { display: block; margin: 8px 0; }
877
+ }
878
+ </style>
879
+ </head>
880
+ <body>
881
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
882
+ <tr>
883
+ <td align="center" style="padding: 30px 20px;">
884
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" class="email-container" style="max-width: 600px; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
885
+
886
+ <!-- Header -->
887
+ <tr><td class="header">
888
+ <h1>FRAIM Weekly Update</h1>
889
+ <p>${weekRange}</p>
890
+ </td></tr>
891
+
892
+ <!-- Greeting -->
893
+ <tr><td class="greeting">
894
+ <p>Hi ${recipientName},</p>
895
+ <p>Here's what's new in FRAIM this week — we've been working hard to make your AI development experience even better.</p>
896
+ </td></tr>
897
+
898
+ <!-- New Features Section -->
899
+ <tr><td class="section-header">🎉 NEW FEATURES</td></tr>
900
+ <tr><td class="section-content">
901
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
902
+ ${featuresHtml}
903
+ </table>
904
+ </td></tr>
905
+
906
+ <!-- Bug Fixes Section -->
907
+ <tr><td class="section-header">🐛 BUG FIXES</td></tr>
908
+ <tr><td class="section-content">
909
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
910
+ ${bugFixesHtml}
911
+ </table>
912
+ </td></tr>
913
+
914
+ <!-- Footer -->
915
+ <tr><td class="footer">
916
+ <p style="margin: 0 0 14px 0; font-weight: 600; color: #1e293b; font-size: 14px;">Want to explore or contribute?</p>
917
+ <a href="https://github.com/mathursrus/FRAIM" class="footer-btn">View on GitHub</a>
918
+ <a href="https://github.com/mathursrus/FRAIM/issues/new" class="footer-btn secondary">Report an Issue</a>
919
+ <p style="margin: 14px 0 0 0; font-size: 12px; color: #94a3b8;">────────────────────────</p>
920
+ <p style="margin: 5px 0 0 0; font-size: 12px; font-weight: 600; color: #64748b;">FRAIM — Framework for Rigor-based AI Management</p>
921
+ </td></tr>
922
+
923
+ </table>
924
+ </td>
925
+ </tr>
926
+ </table>
927
+ </body>
928
+ </html>
929
+ `,
930
+ text: `FRAIM Weekly Update - ${weekRange}
931
+
932
+ Hi ${recipientName},
933
+
934
+ Here's what's new in FRAIM this week!
935
+
936
+ NEW FEATURES:
937
+ ${features.length > 0 ? features.map(f => `• ${f.title}: ${f.description}`).join('\n') : '• No new features this week.'}
938
+
939
+ BUG FIXES:
940
+ ${bugFixes.length > 0 ? bugFixes.map(b => `• ${b.title}: ${b.description}`).join('\n') : '• No bug fixes this week.'}
941
+
942
+ ────────────────────────
943
+
944
+ View on GitHub: https://github.com/mathursrus/FRAIM
945
+ Report an Issue: https://github.com/mathursrus/FRAIM/issues/new
946
+
947
+ FRAIM — Framework for Rigor-based AI Management`
948
+ };
949
+ }
950
+ }
951
+ exports.EmailService = EmailService;