fraim-framework 2.0.35 → 2.0.36

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 (51) hide show
  1. package/bin/fraim.js +52 -5
  2. package/dist/registry/scripts/cleanup-branch.js +62 -33
  3. package/dist/registry/scripts/generate-engagement-emails.js +119 -44
  4. package/dist/registry/scripts/newsletter-helpers.js +208 -268
  5. package/dist/registry/scripts/profile-server.js +387 -0
  6. package/dist/tests/test-chalk-regression.js +18 -2
  7. package/dist/tests/test-client-scripts-validation.js +133 -0
  8. package/dist/tests/test-prep-issue.js +1 -34
  9. package/dist/tests/test-script-location-independence.js +76 -28
  10. package/package.json +2 -2
  11. package/registry/agent-guardrails.md +62 -62
  12. package/registry/rules/communication.md +121 -121
  13. package/registry/rules/continuous-learning.md +54 -54
  14. package/registry/rules/hitl-ppe-record-analysis.md +302 -302
  15. package/registry/rules/software-development-lifecycle.md +104 -104
  16. package/registry/scripts/cleanup-branch.ts +341 -0
  17. package/registry/scripts/code-quality-check.sh +559 -559
  18. package/registry/scripts/detect-tautological-tests.sh +38 -38
  19. package/registry/scripts/generate-engagement-emails.ts +830 -0
  20. package/registry/scripts/markdown-to-pdf.js +7 -3
  21. package/registry/scripts/newsletter-helpers.ts +777 -0
  22. package/registry/scripts/prep-issue.sh +30 -61
  23. package/registry/scripts/profile-server.ts +424 -0
  24. package/registry/scripts/run-thank-you-workflow.ts +122 -0
  25. package/registry/scripts/send-newsletter-simple.ts +102 -0
  26. package/registry/scripts/send-thank-you-emails.ts +57 -0
  27. package/registry/scripts/validate-openapi-limits.ts +366 -366
  28. package/registry/scripts/validate-test-coverage.ts +280 -280
  29. package/registry/scripts/verify-pr-comments.sh +70 -70
  30. package/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +53 -53
  31. package/registry/templates/evidence/Implementation-BugEvidence.md +85 -85
  32. package/registry/templates/evidence/Implementation-FeatureEvidence.md +120 -120
  33. package/registry/workflows/customer-development/insight-analysis.md +156 -156
  34. package/registry/workflows/customer-development/interview-preparation.md +421 -421
  35. package/registry/workflows/customer-development/strategic-brainstorming.md +146 -146
  36. package/registry/workflows/quality-assurance/iterative-improvement-cycle.md +562 -562
  37. package/registry/workflows/reviewer/review-implementation-vs-feature-spec.md +669 -669
  38. package/dist/registry/scripts/build-scripts-generator.js +0 -205
  39. package/dist/registry/scripts/fraim-config.js +0 -61
  40. package/dist/registry/scripts/generic-issues-api.js +0 -100
  41. package/dist/registry/scripts/openapi-generator.js +0 -664
  42. package/dist/registry/scripts/performance/profile-server.js +0 -390
  43. package/dist/test-utils.js +0 -96
  44. package/dist/tests/esm-compat.js +0 -11
  45. package/dist/tests/test-chalk-esm-issue.js +0 -159
  46. package/dist/tests/test-chalk-real-world.js +0 -265
  47. package/dist/tests/test-chalk-resolution-issue.js +0 -304
  48. package/dist/tests/test-fraim-install-chalk-issue.js +0 -254
  49. package/dist/tests/test-npm-resolution-diagnostic.js +0 -140
  50. package/registry/templates/marketing/STORYTELLING-TEMPLATE.md +0 -130
  51. package/registry/workflows/marketing/storytelling.md +0 -65
@@ -7,22 +7,197 @@
7
7
  * AI agents do the CREATIVE work (categorization, content writing).
8
8
  * These functions provide TOOLS (data fetching, email sending).
9
9
  */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
10
43
  Object.defineProperty(exports, "__esModule", { value: true });
11
44
  exports.getLatestNewsletterDate = getLatestNewsletterDate;
12
45
  exports.getResolvedIssuesForNewsletter = getResolvedIssuesForNewsletter;
13
46
  exports.getAllActiveExecutives = getAllActiveExecutives;
14
47
  exports.getPotentialCustomers = getPotentialCustomers;
15
- exports.generateNewsletterHTML = generateNewsletterHTML;
16
- exports.saveNewsletter = saveNewsletter;
17
48
  exports.sendNewsletterToExecutives = sendNewsletterToExecutives;
18
49
  const child_process_1 = require("child_process");
19
50
  const fs_1 = require("fs");
20
51
  const mongodb_1 = require("mongodb");
21
52
  const path_1 = require("path");
22
- const git_utils_1 = require("../../src/utils/git-utils");
23
- const fraim_config_1 = require("./fraim-config");
24
- // Reuse existing email infrastructure
25
- const generate_engagement_emails_1 = require("./generate-engagement-emails");
53
+ // Self-contained utility functions (no FRAIM internal imports)
54
+ function determineDatabaseName() {
55
+ const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
56
+ // If explicitly set to ppe/staging, use that; otherwise default to prod
57
+ if (env === 'ppe' || env === 'staging') {
58
+ return 'fraim_ppe';
59
+ }
60
+ // Default to production
61
+ return process.env.MONGO_DB_NAME || 'fraim_prod';
62
+ }
63
+ function determineSchema(branchName) {
64
+ if (branchName.includes('ppe') || branchName.includes('staging'))
65
+ return 'ppe';
66
+ return 'prod';
67
+ }
68
+ function getCurrentGitBranch() {
69
+ try {
70
+ return (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
71
+ }
72
+ catch (error) {
73
+ return 'master';
74
+ }
75
+ }
76
+ function loadClientConfig() {
77
+ const configPath = (0, path_1.join)(process.cwd(), '.fraim', 'config.json');
78
+ if (!(0, fs_1.existsSync)(configPath)) {
79
+ throw new Error('.fraim/config.json not found. Run fraim init first.');
80
+ }
81
+ return JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
82
+ }
83
+ function getEnvOr(keys, fallback) {
84
+ for (const key of keys) {
85
+ const value = process.env[key];
86
+ if (value && value.length)
87
+ return value;
88
+ }
89
+ return fallback;
90
+ }
91
+ // Load configuration
92
+ const config = loadClientConfig();
93
+ const fraimConfig = {
94
+ repoOwner: config.git.repoOwner || 'mathursrus',
95
+ repoName: config.git.repoName || 'fraim-repo',
96
+ personaName: config.persona.name,
97
+ identityCollection: config.database?.identityCollection || 'Identity',
98
+ executiveCollection: config.database?.executiveCollection || 'Executive',
99
+ defaultEmail: getEnvOr(['FRAIM_DEFAULT_EMAIL'], 'agent@example.com'),
100
+ prodDefaultEmail: getEnvOr(['PROD_FRAIM_DEFAULT_EMAIL'], 'agent@example.com'),
101
+ defaultAccessToken: getEnvOr(['FRAIM_DEFAULT_ACCESS_TOKEN'], ''),
102
+ defaultRefreshToken: getEnvOr(['FRAIM_DEFAULT_REFRESH_TOKEN'], ''),
103
+ prodAccessToken: getEnvOr(['PROD_FRAIM_DEFAULT_ACCESS_TOKEN'], ''),
104
+ prodRefreshToken: getEnvOr(['PROD_FRAIM_DEFAULT_REFRESH_TOKEN'], ''),
105
+ defaultOAuthClientId: getEnvOr(['FRAIM_DEFAULT_OAUTH_CLIENT_ID'], ''),
106
+ defaultOAuthClientSecret: getEnvOr(['FRAIM_DEFAULT_OAUTH_CLIENT_SECRET'], ''),
107
+ prodOAuthClientId: getEnvOr(['PROD_FRAIM_DEFAULT_OAUTH_CLIENT_ID'], ''),
108
+ prodOAuthClientSecret: getEnvOr(['PROD_FRAIM_DEFAULT_OAUTH_CLIENT_SECRET'], ''),
109
+ webAppUrl: config.marketing?.websiteUrl || getEnvOr(['FRAIM_WEB_APP_URL'], 'http://localhost:3000'),
110
+ chatUrl: config.marketing?.chatUrl || getEnvOr(['FRAIM_CHAT_URL'], ''),
111
+ };
112
+ // Helper functions for database operations
113
+ function getProductionDatabase() {
114
+ const mongoUrl = process.env.PROD_MONGO_DATABASE_URL || process.env.MONGO_DATABASE_URL;
115
+ if (!mongoUrl) {
116
+ throw new Error('PROD_MONGO_DATABASE_URL or MONGO_DATABASE_URL environment variable is required');
117
+ }
118
+ return new mongodb_1.MongoClient(mongoUrl);
119
+ }
120
+ /**
121
+ * Get database name, defaulting to production
122
+ */
123
+ function getDatabaseName() {
124
+ const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
125
+ // If explicitly set to ppe/staging, use that; otherwise default to prod
126
+ if (env === 'ppe' || env === 'staging') {
127
+ return determineDatabaseName();
128
+ }
129
+ // Default to production
130
+ return process.env.MONGO_DB_NAME || 'fraim_prod';
131
+ }
132
+ /**
133
+ * Get collection name with schema prefix (defaults to prod schema)
134
+ */
135
+ function getCollectionName(baseName) {
136
+ const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
137
+ // If explicitly set to ppe/staging, use that schema; otherwise default to prod
138
+ if (env === 'ppe' || env === 'staging') {
139
+ const schema = determineSchema(getCurrentGitBranch());
140
+ return `${schema}_${baseName}`;
141
+ }
142
+ // Default to prod schema
143
+ return `prod_${baseName}`;
144
+ }
145
+ // Reuse existing email infrastructure - implemented inline to avoid FRAIM internal imports
146
+ /**
147
+ * Find executive by email in database (defaults to production)
148
+ */
149
+ async function findExecutiveByEmail(email) {
150
+ const client = getProductionDatabase();
151
+ try {
152
+ await client.connect();
153
+ const dbName = getDatabaseName();
154
+ const db = client.db(dbName);
155
+ const executive = await db.collection(getCollectionName('Executive')).findOne({ email: email.toLowerCase() });
156
+ return executive;
157
+ }
158
+ finally {
159
+ await client.close();
160
+ }
161
+ }
162
+ /**
163
+ * Get the persona email for an executive from the identity collection
164
+ */
165
+ async function getPersonaEmailForExecutive(executiveId) {
166
+ const { MongoClient } = await Promise.resolve().then(() => __importStar(require('mongodb')));
167
+ const mongoUrl = process.env.PROD_MONGO_DATABASE_URL || process.env.MONGO_DATABASE_URL;
168
+ if (!mongoUrl) {
169
+ console.warn(`⚠️ PROD_MONGO_DATABASE_URL not set, using default ${fraimConfig.personaName} email`);
170
+ return fraimConfig.defaultEmail;
171
+ }
172
+ const client = new MongoClient(mongoUrl);
173
+ try {
174
+ await client.connect();
175
+ const dbName = getDatabaseName();
176
+ const db = client.db(dbName);
177
+ const collectionName = getCollectionName(fraimConfig.identityCollection);
178
+ // Query the identity collection (defaults to prod)
179
+ let identity = await db.collection(collectionName).findOne({
180
+ executive_id: executiveId,
181
+ status: 'active'
182
+ });
183
+ if (!identity) {
184
+ identity = await db.collection(collectionName).findOne({
185
+ executive_id: executiveId
186
+ });
187
+ }
188
+ if (identity && identity.email) {
189
+ return identity.email;
190
+ }
191
+ return fraimConfig.defaultEmail;
192
+ }
193
+ catch (error) {
194
+ console.warn(`⚠️ Could not get ${fraimConfig.personaName} email for executive ${executiveId}:`, error);
195
+ return fraimConfig.defaultEmail;
196
+ }
197
+ finally {
198
+ await client.close();
199
+ }
200
+ }
26
201
  /**
27
202
  * Get the latest date from existing newsletter files
28
203
  * Returns the most recent date when newsletter was sent, or null if no files exist
@@ -79,37 +254,12 @@ async function getResolvedIssuesForNewsletter(date) {
79
254
  }
80
255
  console.log(`📋 Fetching issues resolved since ${date}...`);
81
256
  // Get all closed issues from the specified date (not just user-reported)
82
- const command = `gh search issues --repo=${fraim_config_1.fraimConfig.repoOwner}/${fraim_config_1.fraimConfig.repoName} --state=closed --closed=">${date}" --json number,title,body,labels,closedAt,author --limit 100`;
257
+ const command = `gh search issues --repo=${fraimConfig.repoOwner}/${fraimConfig.repoName} --state=closed --closed=">${date}" --json number,title,body,labels,closedAt,author --limit 100`;
83
258
  const output = (0, child_process_1.execSync)(command, { encoding: 'utf-8' });
84
259
  const issues = JSON.parse(output);
85
260
  console.log(`✅ Found ${issues.length} resolved issues`);
86
261
  return issues;
87
262
  }
88
- /**
89
- * Get database name, defaulting to production
90
- */
91
- function getDatabaseName() {
92
- const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
93
- // If explicitly set to ppe/staging, use that; otherwise default to prod
94
- if (env === 'ppe' || env === 'staging') {
95
- return (0, git_utils_1.determineDatabaseName)();
96
- }
97
- // Default to production
98
- return process.env.MONGO_DB_NAME || 'fraim_prod';
99
- }
100
- /**
101
- * Get collection name with schema prefix (defaults to prod schema)
102
- */
103
- function getCollectionName(baseName) {
104
- const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
105
- // If explicitly set to ppe/staging, use that schema; otherwise default to prod
106
- if (env === 'ppe' || env === 'staging') {
107
- const schema = (0, git_utils_1.determineSchema)((0, git_utils_1.getCurrentGitBranch)());
108
- return `${schema}_${baseName}`;
109
- }
110
- // Default to prod schema
111
- return `prod_${baseName}`;
112
- }
113
263
  /**
114
264
  * Get all active executives (defaults to production database)
115
265
  * DETERMINISTIC: Just fetches executive list
@@ -164,218 +314,6 @@ function getPotentialCustomers() {
164
314
  return [];
165
315
  }
166
316
  }
167
- /**
168
- * Get default Persona email from database (defaults to production)
169
- * Extracts the base email (part before the +) from existing executive Persona emails
170
- * DETERMINISTIC: Reads from database, extracts pattern
171
- */
172
- async function getDefaultPersonaEmailFromDatabase() {
173
- const mongoUrl = process.env.PROD_MONGO_DATABASE_URL || process.env.MONGO_DATABASE_URL;
174
- if (!mongoUrl) {
175
- return null;
176
- }
177
- const client = new mongodb_1.MongoClient(mongoUrl);
178
- try {
179
- await client.connect();
180
- const dbName = getDatabaseName();
181
- const db = client.db(dbName);
182
- const collectionName = getCollectionName(fraim_config_1.fraimConfig.identityCollection);
183
- // Get any active Persona identity from the database (defaults to prod)
184
- let identity = await db.collection(collectionName)
185
- .findOne({ status: 'active' });
186
- // Try any identity if no active one found or no email
187
- if (!identity || !identity.email) {
188
- identity = await db.collection(collectionName)
189
- .findOne({ email: { $exists: true } });
190
- }
191
- if (!identity || !identity.email) {
192
- return null;
193
- }
194
- const email = identity.email;
195
- // Extract base email: if email is "persona+username@domain.com", extract "persona@domain.com"
196
- if (email.includes('+')) {
197
- const [localPart, rest] = email.split('+');
198
- const domain = rest.split('@')[1];
199
- return `${localPart}@${domain}`;
200
- }
201
- // If no plus sign, return as-is (it's already the base email)
202
- return email;
203
- }
204
- catch (error) {
205
- console.warn(`⚠️ Could not get default ${fraim_config_1.fraimConfig.personaName} email from database:`, error);
206
- return null;
207
- }
208
- finally {
209
- await client.close();
210
- }
211
- }
212
- /**
213
- * Generate HTML from newsletter JSON
214
- * DETERMINISTIC: Just applies template, no creative decisions
215
- */
216
- function generateNewsletterHTML(newsletter) {
217
- const templatePath = (0, path_1.join)(process.cwd(), 'registry', 'templates', 'customer-development', 'weekly-newsletter-template.html');
218
- let html = (0, fs_1.readFileSync)(templatePath, 'utf-8');
219
- const { content } = newsletter;
220
- // Replace simple variables
221
- html = html.replace(/\{\{weekTitle\}\}/g, escapeHtml(content.weekTitle));
222
- html = html.replace(/\{\{weekSubtitle\}\}/g, escapeHtml(content.weekSubtitle));
223
- html = html.replace(/\{\{weekDate\}\}/g, escapeHtml(content.weekDate));
224
- html = html.replace(/\{\{personaName\}\}/g, escapeHtml(fraim_config_1.fraimConfig.personaName));
225
- html = html.replace(/\{\{chatUrl\}\}/g, escapeHtml(fraim_config_1.fraimConfig.chatUrl || '#'));
226
- html = html.replace(/\{\{webAppUrl\}\}/g, escapeHtml(fraim_config_1.fraimConfig.webAppUrl || '#'));
227
- html = html.replace(/\{\{openingMessage\}\}/g, escapeHtml(content.openingMessage));
228
- html = html.replace(/\{\{ctaLink\}\}/g, content.ctaLink || (fraim_config_1.fraimConfig.webAppUrl ? `${fraim_config_1.fraimConfig.webAppUrl}/wellness/${fraim_config_1.fraimConfig.personaName.toLowerCase()}` : '#'));
229
- html = html.replace(/\{\{ctaText\}\}/g, escapeHtml(content.ctaText || `Get ${fraim_config_1.fraimConfig.personaName} Now`));
230
- html = html.replace(/\{\{unsubscribeLink\}\}/g, '#');
231
- // Custom header text - check if this is the first newsletter
232
- if (content.weekDate && (content.weekDate.toLowerCase().includes('month of november') || content.weekDate.toLowerCase().includes('month of october'))) {
233
- html = html.replace(/🚀 Weekly Update/g, `🎉 ${fraim_config_1.fraimConfig.personaName}'s First Update`);
234
- html = html.replace(/📅 Week of/g, "📅");
235
- html = html.replace(/Coming Next Week/g, "Coming Next Month");
236
- }
237
- // Stats
238
- html = html.replace(/\{\{statsFeatures\}\}/g, (newsletter.metadata.featuresCount || 0).toString());
239
- html = html.replace(/\{\{statsImprovements\}\}/g, (newsletter.metadata.improvementsCount || 0).toString());
240
- html = html.replace(/\{\{statsBugFixes\}\}/g, (newsletter.metadata.bugFixesCount || 0).toString());
241
- // Hero feature(s) - support both single heroFeature and array of heroFeatures
242
- const heroFeatures = content.heroFeatures || (content.heroFeature ? [content.heroFeature] : []);
243
- if (heroFeatures.length > 0) {
244
- // Generate HTML for all hero features, wrapped in proper table structure
245
- const heroFeaturesHTML = heroFeatures.map((hero, index) => {
246
- const badge = heroFeatures.length > 1 ? `HERO FEATURE ${index + 1}` : 'HERO FEATURE';
247
- const marginBottom = index < heroFeatures.length - 1 ? '40px' : '0';
248
- return `
249
- <tr>
250
- <td style="padding: 0 40px ${marginBottom} 40px;">
251
- <div style="background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border: 2px solid #667eea; border-radius: 12px; padding: 35px; position: relative; overflow: hidden;">
252
- <div style="position: absolute; top: -10px; right: -10px; background: #ff6b6b; color: white; padding: 8px 20px; border-radius: 20px; font-size: 12px; font-weight: 700; letter-spacing: 1px; transform: rotate(12deg); box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);">
253
- ⭐ ${badge}
254
- </div>
255
- <div style="font-size: 14px; color: #667eea; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 12px;">🎯 ${heroFeatures.length > 1 ? `Feature ${index + 1} of the Week` : 'Feature of the Week'}</div>
256
- <div style="font-size: 28px; font-weight: 800; color: #212529; margin-bottom: 16px; line-height: 1.3;">${escapeHtml(hero.title)}</div>
257
- <div style="font-size: 16px; color: #495057; line-height: 1.8; margin-bottom: ${hero.impact ? '20px' : '0'};">${escapeHtml(hero.description)}</div>
258
- ${hero.impact ? `
259
- <div style="background: white; border-left: 4px solid #28a745; padding: 16px 20px; border-radius: 8px; margin-top: 20px;">
260
- <div style="font-size: 14px; font-weight: 700; color: #28a745; margin-bottom: 8px;">💚 Real Impact:</div>
261
- <div style="font-size: 15px; color: #495057; line-height: 1.7;">${escapeHtml(hero.impact)}</div>
262
- </div>
263
- ` : ''}
264
- </div>
265
- </td>
266
- </tr>
267
- `;
268
- }).join('');
269
- html = html.replace(/\{\{heroFeatureTitle\}\}/g, escapeHtml(heroFeatures[0].title));
270
- html = html.replace(/\{\{heroFeatureDescription\}\}/g, escapeHtml(heroFeatures[0].description));
271
- html = html.replace(/\{\{heroFeatureImpact\}\}/g, escapeHtml(heroFeatures[0].impact || ''));
272
- html = html.replace(/\{\{#if hasHeroFeature\}\}([\s\S]*?)\{\{\/if\}\}/, heroFeaturesHTML);
273
- html = html.replace(/\{\{#if heroFeatureImpact\}\}([\s\S]*?)\{\{\/if\}\}/, heroFeatures[0].impact ? '$1' : '');
274
- }
275
- else {
276
- html = html.replace(/\{\{#if hasHeroFeature\}\}[\s\S]*?\{\{\/if\}\}/, '');
277
- }
278
- // New features list
279
- if (content.newFeatures && content.newFeatures.length > 0) {
280
- const featuresHTML = content.newFeatures.map((feature, index) => `
281
- <div style="margin-bottom: ${index < content.newFeatures.length - 1 ? '24px' : '0'}; padding: 24px; background: white; border-radius: 10px; border: 2px solid #e9ecef; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
282
- <div style="font-size: 19px; font-weight: 700; color: #212529; margin-bottom: 10px;">✨ ${escapeHtml(feature.title)}</div>
283
- <div style="font-size: 15px; color: #495057; line-height: 1.7;">${escapeHtml(feature.description)}</div>
284
- ${feature.impact ? `<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e9ecef; font-size: 14px; color: #667eea; font-weight: 600;">💡 ${escapeHtml(feature.impact)}</div>` : ''}
285
- </div>
286
- `).join('');
287
- html = html.replace(/\{\{newFeaturesList\}\}/g, featuresHTML);
288
- html = html.replace(/\{\{#if hasNewFeatures\}\}([\s\S]*?)\{\{\/if\}\}/, '$1');
289
- }
290
- else {
291
- html = html.replace(/\{\{#if hasNewFeatures\}\}[\s\S]*?\{\{\/if\}\}/, '');
292
- }
293
- // Improvements list
294
- if (content.improvements && content.improvements.length > 0) {
295
- const improvementsHTML = content.improvements.map((improvement, index) => `
296
- <div style="margin-bottom: ${index < content.improvements.length - 1 ? '20px' : '0'}; padding: 20px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #28a745;">
297
- <div style="font-size: 17px; font-weight: 700; color: #212529; margin-bottom: 8px;">🚀 ${escapeHtml(improvement.title)}</div>
298
- <div style="font-size: 15px; color: #495057; line-height: 1.7;">${escapeHtml(improvement.description)}</div>
299
- </div>
300
- `).join('');
301
- html = html.replace(/\{\{improvementsList\}\}/g, improvementsHTML);
302
- html = html.replace(/\{\{#if hasImprovements\}\}([\s\S]*?)\{\{\/if\}\}/, '$1');
303
- }
304
- else {
305
- html = html.replace(/\{\{#if hasImprovements\}\}[\s\S]*?\{\{\/if\}\}/, '');
306
- }
307
- // Bug fixes list
308
- if (content.bugFixes && content.bugFixes.length > 0) {
309
- const bugFixesHTML = content.bugFixes.map((fix, index) => `
310
- <div style="margin-bottom: ${index < content.bugFixes.length - 1 ? '16px' : '0'}; padding: 16px; background: #fff8f0; border-radius: 8px; border-left: 4px solid #fd7e14;">
311
- <div style="font-size: 16px; font-weight: 600; color: #212529; margin-bottom: 6px;">🔧 ${escapeHtml(fix.title)}</div>
312
- <div style="font-size: 14px; color: #495057; line-height: 1.6;">${escapeHtml(fix.description)}</div>
313
- </div>
314
- `).join('');
315
- html = html.replace(/\{\{bugFixesList\}\}/g, bugFixesHTML);
316
- html = html.replace(/\{\{#if hasBugFixes\}\}([\s\S]*?)\{\{\/if\}\}/, '$1');
317
- }
318
- else {
319
- html = html.replace(/\{\{#if hasBugFixes\}\}[\s\S]*?\{\{\/if\}\}/, '');
320
- }
321
- // Testimonial
322
- if (content.testimonial) {
323
- html = html.replace(/\{\{testimonialText\}\}/g, escapeHtml(content.testimonial.text));
324
- html = html.replace(/\{\{testimonialAuthor\}\}/g, escapeHtml(content.testimonial.author));
325
- html = html.replace(/\{\{testimonialRole\}\}/g, escapeHtml(content.testimonial.role));
326
- html = html.replace(/\{\{#if hasTestimonial\}\}([\s\S]*?)\{\{\/if\}\}/, '$1');
327
- }
328
- else {
329
- html = html.replace(/\{\{#if hasTestimonial\}\}[\s\S]*?\{\{\/if\}\}/, '');
330
- }
331
- // Coming next
332
- if (content.comingNext) {
333
- html = html.replace(/\{\{comingNextText\}\}/g, escapeHtml(content.comingNext));
334
- html = html.replace(/\{\{#if hasComingNext\}\}([\s\S]*?)\{\{\/if\}\}/, '$1');
335
- }
336
- else {
337
- html = html.replace(/\{\{#if hasComingNext\}\}[\s\S]*?\{\{\/if\}\}/, '');
338
- }
339
- // Conditional sections
340
- html = html.replace(/\{\{#if showStats\}\}([\s\S]*?)\{\{\/if\}\}/, '$1');
341
- html = html.replace(/\{\{#if showFOMO\}\}([\s\S]*?)\{\{\/if\}\}/, '$1');
342
- return html;
343
- }
344
- /**
345
- * Escape HTML special characters
346
- */
347
- function escapeHtml(text) {
348
- if (!text)
349
- return '';
350
- return text
351
- .replace(/&/g, '&amp;')
352
- .replace(/</g, '&lt;')
353
- .replace(/>/g, '&gt;')
354
- .replace(/"/g, '&quot;')
355
- .replace(/'/g, '&#039;');
356
- }
357
- /**
358
- * Save newsletter JSON and HTML
359
- * DETERMINISTIC: Just writes files
360
- */
361
- function saveNewsletter(newsletter, outputDir) {
362
- const dir = outputDir || (0, path_1.join)(process.cwd(), 'docs', 'customer-development', 'newsletters');
363
- if (!(0, fs_1.existsSync)(dir)) {
364
- (0, fs_1.mkdirSync)(dir, { recursive: true });
365
- }
366
- const dateStr = newsletter.metadata.weekEnd;
367
- const jsonPath = (0, path_1.join)(dir, `newsletter-${dateStr}.json`);
368
- const htmlPath = (0, path_1.join)(dir, `newsletter-${dateStr}.html`);
369
- // Generate HTML
370
- const html = generateNewsletterHTML(newsletter);
371
- // Save JSON
372
- (0, fs_1.writeFileSync)(jsonPath, JSON.stringify(newsletter, null, 2));
373
- console.log(`✅ Saved newsletter JSON to: ${jsonPath}`);
374
- // Save HTML
375
- (0, fs_1.writeFileSync)(htmlPath, html);
376
- console.log(`✅ Saved newsletter HTML to: ${htmlPath}`);
377
- return jsonPath;
378
- }
379
317
  /**
380
318
  * Send newsletter to all executives and potential customers
381
319
  * DETERMINISTIC: Just sends emails, no decisions
@@ -433,9 +371,9 @@ async function sendNewsletterToExecutives(newsletterPath, filterEmails, filterEx
433
371
  console.log(`\n👥 ACTIVE EXECUTIVES (${executives.length}):\n`);
434
372
  for (const exec of executives) {
435
373
  const fromEmail = exec.id
436
- ? await (0, generate_engagement_emails_1.getPersonaEmailForExecutive)(exec.id)
437
- : fraim_config_1.fraimConfig.defaultEmail;
438
- const fromDisplayName = `${fraim_config_1.fraimConfig.personaName} - ${exec.name}'s AI Executive Assistant`;
374
+ ? await getPersonaEmailForExecutive(exec.id)
375
+ : fraimConfig.defaultEmail;
376
+ const fromDisplayName = `${fraimConfig.personaName} - ${exec.name}'s AI Executive Assistant`;
439
377
  console.log(` ${exec.name || 'Unknown Name'}`);
440
378
  console.log(` To: ${exec.email || 'No email'}`);
441
379
  console.log(` From Email: ${fromEmail}`);
@@ -445,8 +383,8 @@ async function sendNewsletterToExecutives(newsletterPath, filterEmails, filterEx
445
383
  }
446
384
  }
447
385
  if (potentialCustomers.length > 0) {
448
- const defaultEmail = fraim_config_1.fraimConfig.prodDefaultEmail || 'agent@example.com';
449
- const defaultDisplayName = `${fraim_config_1.fraimConfig.personaName} - Your AI Executive Assistant`;
386
+ const defaultEmail = fraimConfig.prodDefaultEmail || 'agent@example.com';
387
+ const defaultDisplayName = `${fraimConfig.personaName} - Your AI Executive Assistant`;
450
388
  console.log(`\n🎯 POTENTIAL CUSTOMERS (${potentialCustomers.length}):\n`);
451
389
  potentialCustomers.forEach((customer, index) => {
452
390
  console.log(` ${customer.name || 'Unknown Name'}`);
@@ -468,15 +406,15 @@ async function sendNewsletterToExecutives(newsletterPath, filterEmails, filterEx
468
406
  try {
469
407
  console.log(`📧 Sending to executive ${executive.name} (${executive.email})...`);
470
408
  const fromEmail = executive.id
471
- ? await (0, generate_engagement_emails_1.getPersonaEmailForExecutive)(executive.id)
472
- : fraim_config_1.fraimConfig.defaultEmail;
473
- const fromDisplayName = `${fraim_config_1.fraimConfig.personaName} - ${executive.name}'s AI Executive Assistant`;
409
+ ? await getPersonaEmailForExecutive(executive.id)
410
+ : fraimConfig.defaultEmail;
411
+ const fromDisplayName = `${fraimConfig.personaName} - ${executive.name}'s AI Executive Assistant`;
474
412
  // Remove emojis from subject line for ASCII compatibility
475
413
  const subject = newsletter.content.weekTitle.replace(/[^\x00-\x7F]/g, '').trim();
476
414
  const plainText = generatePlainText(newsletter);
477
415
  // Get tokens and send (reuses existing infrastructure)
478
416
  const personaTokens = await getPersonaTokens(executive.id);
479
- const executiveData = await (0, generate_engagement_emails_1.findExecutiveByEmail)(executive.email);
417
+ const executiveData = await findExecutiveByEmail(executive.email);
480
418
  await sendEmailViaGmail({
481
419
  to: executive.email,
482
420
  subject,
@@ -493,10 +431,10 @@ async function sendNewsletterToExecutives(newsletterPath, filterEmails, filterEx
493
431
  }
494
432
  }
495
433
  // Send to potential customers (from PROD_FRAIM_DEFAULT_EMAIL)
496
- const defaultEmail = fraim_config_1.fraimConfig.prodDefaultEmail || 'agent@example.com';
497
- const defaultDisplayName = `${fraim_config_1.fraimConfig.personaName} - Your AI Executive Assistant`;
498
- const defaultAccessToken = fraim_config_1.fraimConfig.prodAccessToken || '';
499
- const defaultRefreshToken = fraim_config_1.fraimConfig.prodRefreshToken || '';
434
+ const defaultEmail = fraimConfig.prodDefaultEmail || 'agent@example.com';
435
+ const defaultDisplayName = `${fraimConfig.personaName} - Your AI Executive Assistant`;
436
+ const defaultAccessToken = fraimConfig.prodAccessToken || '';
437
+ const defaultRefreshToken = fraimConfig.prodRefreshToken || '';
500
438
  if (!defaultAccessToken || !defaultRefreshToken) {
501
439
  console.warn('⚠️ PROD_FRAIM_DEFAULT_ACCESS_TOKEN or PROD_FRAIM_DEFAULT_REFRESH_TOKEN not set. Skipping potential customers.');
502
440
  }
@@ -546,7 +484,7 @@ async function getPersonaTokens(executiveId) {
546
484
  await client.connect();
547
485
  const dbName = getDatabaseName();
548
486
  const db = client.db(dbName);
549
- const collectionName = getCollectionName(fraim_config_1.fraimConfig.identityCollection);
487
+ const collectionName = getCollectionName(fraimConfig.identityCollection);
550
488
  // Check for identity by executive ID
551
489
  const identity = await db.collection(collectionName).findOne({
552
490
  executive_id: executiveId,
@@ -620,8 +558,8 @@ function generatePlainText(newsletter) {
620
558
  if (content.comingNext) {
621
559
  text += `COMING NEXT:\n${content.comingNext}\n\n`;
622
560
  }
623
- text += `\nWith gratitude,\n${fraim_config_1.fraimConfig.personaName}\nYour AI Executive Assistant\n\n`;
624
- text += `Visit: ${fraim_config_1.fraimConfig.webAppUrl ? `${fraim_config_1.fraimConfig.webAppUrl}/wellness/${fraim_config_1.fraimConfig.personaName.toLowerCase()}` : '#'}\n`;
561
+ text += `\nWith gratitude,\n${fraimConfig.personaName}\nYour AI Executive Assistant\n\n`;
562
+ text += `Visit: ${fraimConfig.webAppUrl ? `${fraimConfig.webAppUrl}/wellness/${fraimConfig.personaName.toLowerCase()}` : '#'}\n`;
625
563
  return text;
626
564
  }
627
565
  /**
@@ -636,7 +574,7 @@ async function sendEmailViaGmail(params, executive, personaTokens) {
636
574
  accessToken = personaTokens.access_token;
637
575
  refreshToken = personaTokens.refresh_token;
638
576
  // Check if these are PROD tokens by comparing with PROD_FRAIM_DEFAULT_REFRESH_TOKEN
639
- const prodRefreshToken = fraim_config_1.fraimConfig.prodRefreshToken || '';
577
+ const prodRefreshToken = fraimConfig.prodRefreshToken || '';
640
578
  if (prodRefreshToken && refreshToken === prodRefreshToken) {
641
579
  isProdToken = true;
642
580
  }
@@ -646,10 +584,10 @@ async function sendEmailViaGmail(params, executive, personaTokens) {
646
584
  refreshToken = executive.personaRefreshToken;
647
585
  }
648
586
  else {
649
- accessToken = fraim_config_1.fraimConfig.defaultAccessToken || '';
650
- refreshToken = fraim_config_1.fraimConfig.defaultRefreshToken || '';
587
+ accessToken = fraimConfig.defaultAccessToken || '';
588
+ refreshToken = fraimConfig.defaultRefreshToken || '';
651
589
  if (!accessToken || !refreshToken) {
652
- throw new Error(`${fraim_config_1.fraimConfig.personaName} Gmail tokens not found`);
590
+ throw new Error(`${fraimConfig.personaName} Gmail tokens not found`);
653
591
  }
654
592
  }
655
593
  const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -663,6 +601,8 @@ async function sendEmailViaGmail(params, executive, personaTokens) {
663
601
  `--${boundary}`,
664
602
  `Content-Type: text/plain; charset=utf-8`,
665
603
  ``,
604
+ plainTextBody,
605
+ ``,
666
606
  `--${boundary}`,
667
607
  `Content-Type: text/html; charset=utf-8`,
668
608
  ``,
@@ -690,11 +630,11 @@ async function sendEmailViaGmail(params, executive, personaTokens) {
690
630
  if (response.status === 401) {
691
631
  // Token refresh logic - use PROD OAuth credentials if using PROD tokens
692
632
  const clientId = isProdToken
693
- ? fraim_config_1.fraimConfig.prodOAuthClientId
694
- : fraim_config_1.fraimConfig.defaultOAuthClientId;
633
+ ? fraimConfig.prodOAuthClientId
634
+ : fraimConfig.defaultOAuthClientId;
695
635
  const clientSecret = isProdToken
696
- ? fraim_config_1.fraimConfig.prodOAuthClientSecret
697
- : fraim_config_1.fraimConfig.defaultOAuthClientSecret;
636
+ ? fraimConfig.prodOAuthClientSecret
637
+ : fraimConfig.defaultOAuthClientSecret;
698
638
  if (!clientId || !clientSecret) {
699
639
  throw new Error(`OAuth credentials not found for token refresh${isProdToken ? ' (PROD)' : ''}`);
700
640
  }