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.
- package/bin/fraim.js +52 -5
- package/dist/registry/scripts/cleanup-branch.js +62 -33
- package/dist/registry/scripts/generate-engagement-emails.js +119 -44
- package/dist/registry/scripts/newsletter-helpers.js +208 -268
- package/dist/registry/scripts/profile-server.js +387 -0
- package/dist/tests/test-chalk-regression.js +18 -2
- package/dist/tests/test-client-scripts-validation.js +133 -0
- package/dist/tests/test-prep-issue.js +1 -34
- package/dist/tests/test-script-location-independence.js +76 -28
- package/package.json +2 -2
- package/registry/agent-guardrails.md +62 -62
- package/registry/rules/communication.md +121 -121
- package/registry/rules/continuous-learning.md +54 -54
- package/registry/rules/hitl-ppe-record-analysis.md +302 -302
- package/registry/rules/software-development-lifecycle.md +104 -104
- package/registry/scripts/cleanup-branch.ts +341 -0
- package/registry/scripts/code-quality-check.sh +559 -559
- package/registry/scripts/detect-tautological-tests.sh +38 -38
- package/registry/scripts/generate-engagement-emails.ts +830 -0
- package/registry/scripts/markdown-to-pdf.js +7 -3
- package/registry/scripts/newsletter-helpers.ts +777 -0
- package/registry/scripts/prep-issue.sh +30 -61
- package/registry/scripts/profile-server.ts +424 -0
- package/registry/scripts/run-thank-you-workflow.ts +122 -0
- package/registry/scripts/send-newsletter-simple.ts +102 -0
- package/registry/scripts/send-thank-you-emails.ts +57 -0
- package/registry/scripts/validate-openapi-limits.ts +366 -366
- package/registry/scripts/validate-test-coverage.ts +280 -280
- package/registry/scripts/verify-pr-comments.sh +70 -70
- package/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +53 -53
- package/registry/templates/evidence/Implementation-BugEvidence.md +85 -85
- package/registry/templates/evidence/Implementation-FeatureEvidence.md +120 -120
- package/registry/workflows/customer-development/insight-analysis.md +156 -156
- package/registry/workflows/customer-development/interview-preparation.md +421 -421
- package/registry/workflows/customer-development/strategic-brainstorming.md +146 -146
- package/registry/workflows/quality-assurance/iterative-improvement-cycle.md +562 -562
- package/registry/workflows/reviewer/review-implementation-vs-feature-spec.md +669 -669
- package/dist/registry/scripts/build-scripts-generator.js +0 -205
- package/dist/registry/scripts/fraim-config.js +0 -61
- package/dist/registry/scripts/generic-issues-api.js +0 -100
- package/dist/registry/scripts/openapi-generator.js +0 -664
- package/dist/registry/scripts/performance/profile-server.js +0 -390
- package/dist/test-utils.js +0 -96
- package/dist/tests/esm-compat.js +0 -11
- package/dist/tests/test-chalk-esm-issue.js +0 -159
- package/dist/tests/test-chalk-real-world.js +0 -265
- package/dist/tests/test-chalk-resolution-issue.js +0 -304
- package/dist/tests/test-fraim-install-chalk-issue.js +0 -254
- package/dist/tests/test-npm-resolution-diagnostic.js +0 -140
- package/registry/templates/marketing/STORYTELLING-TEMPLATE.md +0 -130
- 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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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=${
|
|
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, '&')
|
|
352
|
-
.replace(/</g, '<')
|
|
353
|
-
.replace(/>/g, '>')
|
|
354
|
-
.replace(/"/g, '"')
|
|
355
|
-
.replace(/'/g, ''');
|
|
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
|
|
437
|
-
:
|
|
438
|
-
const fromDisplayName = `${
|
|
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 =
|
|
449
|
-
const defaultDisplayName = `${
|
|
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
|
|
472
|
-
:
|
|
473
|
-
const fromDisplayName = `${
|
|
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
|
|
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 =
|
|
497
|
-
const defaultDisplayName = `${
|
|
498
|
-
const defaultAccessToken =
|
|
499
|
-
const defaultRefreshToken =
|
|
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(
|
|
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${
|
|
624
|
-
text += `Visit: ${
|
|
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 =
|
|
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 =
|
|
650
|
-
refreshToken =
|
|
587
|
+
accessToken = fraimConfig.defaultAccessToken || '';
|
|
588
|
+
refreshToken = fraimConfig.defaultRefreshToken || '';
|
|
651
589
|
if (!accessToken || !refreshToken) {
|
|
652
|
-
throw new Error(`${
|
|
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
|
-
?
|
|
694
|
-
:
|
|
633
|
+
? fraimConfig.prodOAuthClientId
|
|
634
|
+
: fraimConfig.defaultOAuthClientId;
|
|
695
635
|
const clientSecret = isProdToken
|
|
696
|
-
?
|
|
697
|
-
:
|
|
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
|
}
|