fraim-framework 2.0.55 → 2.0.57

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 (120) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/src/cli/commands/init-project.js +10 -4
  3. package/dist/src/cli/setup/mcp-config-generator.js +23 -15
  4. package/dist/src/local-mcp-server/stdio-server.js +207 -0
  5. package/dist/src/utils/validate-workflows.js +101 -0
  6. package/dist/src/utils/workflow-parser.js +81 -0
  7. package/package.json +16 -11
  8. package/registry/scripts/pdf-styles.css +172 -0
  9. package/registry/scripts/prep-issue.sh +46 -4
  10. package/registry/scripts/profile-server.ts +131 -130
  11. package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +1 -1
  12. package/registry/stubs/workflows/customer-development/users-to-target.md +1 -1
  13. package/registry/stubs/workflows/product-building/design.md +1 -1
  14. package/registry/stubs/workflows/product-building/implement.md +1 -1
  15. package/Claude.md +0 -1
  16. package/dist/registry/ai-manager-rules/design-phases/design-completeness-review.md +0 -73
  17. package/dist/registry/ai-manager-rules/design-phases/design-design.md +0 -145
  18. package/dist/registry/ai-manager-rules/implement-phases/implement-code.md +0 -283
  19. package/dist/registry/ai-manager-rules/implement-phases/implement-completeness-review.md +0 -120
  20. package/dist/registry/ai-manager-rules/implement-phases/implement-regression.md +0 -173
  21. package/dist/registry/ai-manager-rules/implement-phases/implement-repro.md +0 -104
  22. package/dist/registry/ai-manager-rules/implement-phases/implement-scoping.md +0 -100
  23. package/dist/registry/ai-manager-rules/implement-phases/implement-smoke.md +0 -237
  24. package/dist/registry/ai-manager-rules/implement-phases/implement-spike.md +0 -121
  25. package/dist/registry/ai-manager-rules/implement-phases/implement-validate.md +0 -375
  26. package/dist/registry/ai-manager-rules/retrospective.md +0 -116
  27. package/dist/registry/ai-manager-rules/shared-phases/address-pr-feedback.md +0 -188
  28. package/dist/registry/ai-manager-rules/shared-phases/submit-pr.md +0 -202
  29. package/dist/registry/ai-manager-rules/shared-phases/wait-for-pr-review.md +0 -170
  30. package/dist/registry/ai-manager-rules/spec-phases/spec-competitor-analysis.md +0 -105
  31. package/dist/registry/ai-manager-rules/spec-phases/spec-completeness-review.md +0 -66
  32. package/dist/registry/ai-manager-rules/spec-phases/spec-spec.md +0 -139
  33. package/dist/registry/providers/ado.json +0 -19
  34. package/dist/registry/providers/github.json +0 -19
  35. package/dist/registry/scripts/cleanup-branch.js +0 -287
  36. package/dist/registry/scripts/evaluate-code-quality.js +0 -66
  37. package/dist/registry/scripts/exec-with-timeout.js +0 -142
  38. package/dist/registry/scripts/generate-engagement-emails.js +0 -705
  39. package/dist/registry/scripts/newsletter-helpers.js +0 -671
  40. package/dist/registry/scripts/profile-server.js +0 -388
  41. package/dist/registry/scripts/run-thank-you-workflow.js +0 -92
  42. package/dist/registry/scripts/send-newsletter-simple.js +0 -85
  43. package/dist/registry/scripts/send-thank-you-emails.js +0 -54
  44. package/dist/registry/scripts/validate-openapi-limits.js +0 -311
  45. package/dist/registry/scripts/validate-test-coverage.js +0 -262
  46. package/dist/registry/scripts/verify-test-coverage.js +0 -66
  47. package/dist/scripts/build-stub-registry.js +0 -108
  48. package/dist/src/ai-manager/ai-manager.js +0 -482
  49. package/dist/src/ai-manager/phase-flow.js +0 -357
  50. package/dist/src/ai-manager/types.js +0 -5
  51. package/dist/src/fraim-mcp-server.js +0 -1885
  52. package/dist/tests/debug-tools.js +0 -80
  53. package/dist/tests/shared-server-utils.js +0 -57
  54. package/dist/tests/test-add-ide.js +0 -283
  55. package/dist/tests/test-ai-coach-edge-cases.js +0 -420
  56. package/dist/tests/test-ai-coach-mcp-integration.js +0 -450
  57. package/dist/tests/test-ai-coach-performance.js +0 -328
  58. package/dist/tests/test-ai-coach-phase-content.js +0 -264
  59. package/dist/tests/test-ai-coach-workflows.js +0 -514
  60. package/dist/tests/test-cli.js +0 -228
  61. package/dist/tests/test-client-scripts-validation.js +0 -167
  62. package/dist/tests/test-complete-setup-flow.js +0 -110
  63. package/dist/tests/test-config-system.js +0 -279
  64. package/dist/tests/test-debug-session.js +0 -134
  65. package/dist/tests/test-end-to-end-hybrid-validation.js +0 -328
  66. package/dist/tests/test-enhanced-session-init.js +0 -188
  67. package/dist/tests/test-first-run-journey.js +0 -368
  68. package/dist/tests/test-fraim-issues.js +0 -59
  69. package/dist/tests/test-genericization.js +0 -44
  70. package/dist/tests/test-hybrid-script-execution.js +0 -340
  71. package/dist/tests/test-ide-detector.js +0 -46
  72. package/dist/tests/test-improved-setup.js +0 -121
  73. package/dist/tests/test-mcp-config-generator.js +0 -99
  74. package/dist/tests/test-mcp-connection.js +0 -107
  75. package/dist/tests/test-mcp-issue-integration.js +0 -156
  76. package/dist/tests/test-mcp-lifecycle-methods.js +0 -240
  77. package/dist/tests/test-mcp-shared-server.js +0 -308
  78. package/dist/tests/test-mcp-template-processing.js +0 -160
  79. package/dist/tests/test-modular-issue-tracking.js +0 -165
  80. package/dist/tests/test-node-compatibility.js +0 -95
  81. package/dist/tests/test-npm-install.js +0 -68
  82. package/dist/tests/test-package-size.js +0 -108
  83. package/dist/tests/test-pr-review-workflow.js +0 -307
  84. package/dist/tests/test-prep-issue.js +0 -129
  85. package/dist/tests/test-productivity-integration.js +0 -157
  86. package/dist/tests/test-script-location-independence.js +0 -198
  87. package/dist/tests/test-script-sync.js +0 -557
  88. package/dist/tests/test-server-utils.js +0 -32
  89. package/dist/tests/test-session-rehydration.js +0 -148
  90. package/dist/tests/test-setup-integration.js +0 -98
  91. package/dist/tests/test-setup-scenarios.js +0 -322
  92. package/dist/tests/test-standalone.js +0 -143
  93. package/dist/tests/test-stub-registry.js +0 -136
  94. package/dist/tests/test-sync-stubs.js +0 -143
  95. package/dist/tests/test-sync-version-update.js +0 -93
  96. package/dist/tests/test-telemetry.js +0 -193
  97. package/dist/tests/test-token-validator.js +0 -30
  98. package/dist/tests/test-user-journey.js +0 -236
  99. package/dist/tests/test-users-to-target-workflow.js +0 -253
  100. package/dist/tests/test-utils.js +0 -109
  101. package/dist/tests/test-wizard.js +0 -71
  102. package/dist/tests/test-workflow-discovery.js +0 -242
  103. package/labels.json +0 -52
  104. package/registry/agent-guardrails.md +0 -63
  105. package/registry/fraim.md +0 -48
  106. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-customer-profiling.md +0 -11
  107. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-survey-scoping.md +0 -11
  108. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-platform-discovery.md +0 -11
  109. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-survey-build-linkedin.md +0 -11
  110. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-prospect-qualification.md +0 -11
  111. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-survey-build-reddit.md +0 -11
  112. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-inventory-compilation.md +0 -11
  113. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-survey-build-x.md +0 -11
  114. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase5-survey-build-facebook.md +0 -11
  115. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase6-survey-build-custom.md +0 -11
  116. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase7-survey-dispatch.md +0 -11
  117. package/registry/stubs/workflows/customer-development/templates/customer-persona-template.md +0 -11
  118. package/registry/stubs/workflows/customer-development/templates/search-strategy-template.md +0 -11
  119. package/setup.js +0 -171
  120. package/tsconfig.json +0 -23
@@ -1,671 +0,0 @@
1
- #!/usr/bin/env tsx
2
- "use strict";
3
- /**
4
- * Newsletter Helper Functions
5
- *
6
- * DETERMINISTIC functions for AI agents to use when creating newsletters.
7
- * AI agents do the CREATIVE work (categorization, content writing).
8
- * These functions provide TOOLS (data fetching, email sending).
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
- })();
43
- Object.defineProperty(exports, "__esModule", { value: true });
44
- exports.getLatestNewsletterDate = getLatestNewsletterDate;
45
- exports.getResolvedIssuesForNewsletter = getResolvedIssuesForNewsletter;
46
- exports.getAllActiveExecutives = getAllActiveExecutives;
47
- exports.getPotentialCustomers = getPotentialCustomers;
48
- exports.sendNewsletterToExecutives = sendNewsletterToExecutives;
49
- const child_process_1 = require("child_process");
50
- const fs_1 = require("fs");
51
- const mongodb_1 = require("mongodb");
52
- const path_1 = require("path");
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
- }
201
- /**
202
- * Get the latest date from existing newsletter files
203
- * Returns the most recent date when newsletter was sent, or null if no files exist
204
- */
205
- function getLatestNewsletterDate() {
206
- const newslettersDir = 'docs/customer-development/newsletters';
207
- if (!(0, fs_1.existsSync)(newslettersDir)) {
208
- return null;
209
- }
210
- const files = (0, fs_1.readdirSync)(newslettersDir).filter(f => f.startsWith('newsletter-') && f.endsWith('.json'));
211
- if (files.length === 0) {
212
- return null;
213
- }
214
- // Extract dates from filenames (e.g., newsletter-2025-11-01.json)
215
- const dates = [];
216
- for (const file of files) {
217
- const dateMatch = file.match(/newsletter-(\d{4}-\d{2}-\d{2})\.json/);
218
- if (dateMatch) {
219
- const filePath = (0, path_1.join)(newslettersDir, file);
220
- // Verify content is valid if needed, but for date extraction filename is enough
221
- dates.push(dateMatch[1]);
222
- }
223
- }
224
- if (dates.length === 0) {
225
- return null;
226
- }
227
- // Return the most recent date
228
- dates.sort();
229
- const latestDate = dates[dates.length - 1];
230
- console.log(`📅 Latest newsletter date found: ${latestDate}`);
231
- return latestDate;
232
- }
233
- /**
234
- * Get resolved issues for newsletter
235
- * DETERMINISTIC: Just fetches issues, doesn't categorize or filter
236
- * AI agent decides what to include and how to present it
237
- *
238
- * @param date Optional date string (YYYY-MM-DD). If not provided, uses latest newsletter date or last 7 days
239
- */
240
- async function getResolvedIssuesForNewsletter(date) {
241
- if (!date) {
242
- // Try to get the latest newsletter date
243
- const latestNewsletterDate = getLatestNewsletterDate();
244
- if (latestNewsletterDate) {
245
- date = latestNewsletterDate;
246
- console.log(`📋 Using latest newsletter date as starting point: ${date}`);
247
- }
248
- else {
249
- // Fallback to last 7 days if no previous newsletters exist
250
- const fallbackDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
251
- date = fallbackDate;
252
- console.log(`📋 No previous newsletters found, using last 7 days: ${date}`);
253
- }
254
- }
255
- console.log(`📋 Fetching issues resolved since ${date}...`);
256
- // Get all closed issues from the specified date (not just user-reported)
257
- const command = `gh search issues --repo=${fraimConfig.repoOwner}/${fraimConfig.repoName} --state=closed --closed=">${date}" --json number,title,body,labels,closedAt,author --limit 100`;
258
- const output = (0, child_process_1.execSync)(command, { encoding: 'utf-8' });
259
- const issues = JSON.parse(output);
260
- console.log(`✅ Found ${issues.length} resolved issues`);
261
- return issues;
262
- }
263
- /**
264
- * Get all active executives (defaults to production database)
265
- * DETERMINISTIC: Just fetches executive list
266
- */
267
- async function getAllActiveExecutives() {
268
- const mongoUrl = process.env.PROD_MONGO_DATABASE_URL || process.env.MONGO_DATABASE_URL;
269
- if (!mongoUrl) {
270
- throw new Error('PROD_MONGO_DATABASE_URL or MONGO_DATABASE_URL environment variable is required');
271
- }
272
- const client = new mongodb_1.MongoClient(mongoUrl);
273
- try {
274
- await client.connect();
275
- const dbName = getDatabaseName();
276
- const collectionName = getCollectionName('Executive');
277
- console.log(`[getAllActiveExecutives] Querying database: ${dbName}, collection: ${collectionName}`);
278
- const db = client.db(dbName);
279
- const executives = await db.collection(collectionName)
280
- .find({})
281
- .toArray();
282
- console.log(`[getAllActiveExecutives] Found ${executives.length} executives in ${dbName}.${collectionName}`);
283
- // If no executives found in prod, check PPE as fallback
284
- if (executives.length === 0 && dbName === 'fraim_prod') {
285
- console.log(`[getAllActiveExecutives] No executives in prod, checking PPE as fallback...`);
286
- const ppeDb = client.db('fraim_ppe');
287
- const ppeExecutives = await ppeDb.collection('ppe_Executive')
288
- .find({})
289
- .toArray();
290
- console.log(`[getAllActiveExecutives] Found ${ppeExecutives.length} executives in fraim_ppe.ppe_Executive`);
291
- return ppeExecutives;
292
- }
293
- return executives;
294
- }
295
- finally {
296
- await client.close();
297
- }
298
- }
299
- /**
300
- * Get all potential customers (leads who have expressed interest)
301
- * DETERMINISTIC: Just fetches potential customer list from JSON file
302
- */
303
- function getPotentialCustomers() {
304
- const potentialCustomersPath = (0, path_1.join)(process.cwd(), 'docs', 'customer-development', 'potential-customers.json');
305
- if (!(0, fs_1.existsSync)(potentialCustomersPath)) {
306
- return [];
307
- }
308
- try {
309
- const content = (0, fs_1.readFileSync)(potentialCustomersPath, 'utf-8');
310
- return JSON.parse(content);
311
- }
312
- catch (error) {
313
- console.error('❌ Error reading potential customers file:', error);
314
- return [];
315
- }
316
- }
317
- /**
318
- * Send newsletter to all executives and potential customers
319
- * DETERMINISTIC: Just sends emails, no decisions
320
- *
321
- * AI agents call this AFTER user approves the newsletter JSON
322
- * @param newsletterPath Path to newsletter JSON file
323
- * @param filterEmails Optional array of email addresses to send to (for testing)
324
- * @param filterExecIds Optional array of executive IDs to send to (for testing)
325
- * @param includePotentialCustomers Whether to include potential customers in the send (default: true)
326
- * @param showOnly If true, only show recipient list without sending
327
- */
328
- async function sendNewsletterToExecutives(newsletterPath, filterEmails, filterExecIds, includePotentialCustomers = true, showOnly = false, includeExecutives = true) {
329
- console.log(`📧 Sending newsletter from: ${newsletterPath}`);
330
- if (!(0, fs_1.existsSync)(newsletterPath)) {
331
- throw new Error(`Newsletter file not found: ${newsletterPath}`);
332
- }
333
- const newsletter = JSON.parse((0, fs_1.readFileSync)(newsletterPath, 'utf-8'));
334
- const htmlPath = newsletterPath.replace('.json', '.html');
335
- if (!(0, fs_1.existsSync)(htmlPath)) {
336
- throw new Error(`Newsletter HTML not found: ${htmlPath}. Generate it first.`);
337
- }
338
- const html = (0, fs_1.readFileSync)(htmlPath, 'utf-8');
339
- let executives = [];
340
- // Get executives only if includeExecutives is true
341
- if (includeExecutives) {
342
- executives = await getAllActiveExecutives();
343
- // Filter executives if filters are provided
344
- if (filterEmails && filterEmails.length > 0) {
345
- console.log(`🔍 Filtering to ${filterEmails.length} email(s): ${filterEmails.join(', ')}`);
346
- executives = executives.filter(exec => filterEmails.includes(exec.email));
347
- }
348
- if (filterExecIds && filterExecIds.length > 0) {
349
- console.log(`🔍 Filtering to ${filterExecIds.length} executive ID(s): ${filterExecIds.join(', ')}`);
350
- executives = executives.filter(exec => exec.id && filterExecIds.includes(exec.id));
351
- }
352
- }
353
- // Get potential customers if requested
354
- let potentialCustomers = [];
355
- if (includePotentialCustomers) {
356
- potentialCustomers = getPotentialCustomers();
357
- // Filter potential customers if filterEmails is provided
358
- if (filterEmails && filterEmails.length > 0) {
359
- potentialCustomers = potentialCustomers.filter(customer => filterEmails.includes(customer.email));
360
- }
361
- }
362
- if (includeExecutives) {
363
- console.log(`📊 Found ${executives.length} executive(s) to send to`);
364
- }
365
- if (includePotentialCustomers) {
366
- console.log(`📊 Found ${potentialCustomers.length} potential customer(s) to send to`);
367
- }
368
- if (showOnly) {
369
- console.log(`\n📋 Recipient List (--showonly mode - no emails will be sent):\n`);
370
- if (includeExecutives && executives.length > 0) {
371
- console.log(`\n👥 ACTIVE EXECUTIVES (${executives.length}):\n`);
372
- for (const exec of executives) {
373
- const fromEmail = exec.id
374
- ? await getPersonaEmailForExecutive(exec.id)
375
- : fraimConfig.defaultEmail;
376
- const fromDisplayName = `${fraimConfig.personaName} - ${exec.name}'s AI Executive Assistant`;
377
- console.log(` ${exec.name || 'Unknown Name'}`);
378
- console.log(` To: ${exec.email || 'No email'}`);
379
- console.log(` From Email: ${fromEmail}`);
380
- console.log(` From Display Name: ${fromDisplayName}`);
381
- console.log(` Executive ID: ${exec.id || 'No ID'}`);
382
- console.log('');
383
- }
384
- }
385
- if (potentialCustomers.length > 0) {
386
- const defaultEmail = fraimConfig.prodDefaultEmail || 'agent@example.com';
387
- const defaultDisplayName = `${fraimConfig.personaName} - Your AI Executive Assistant`;
388
- console.log(`\n🎯 POTENTIAL CUSTOMERS (${potentialCustomers.length}):\n`);
389
- potentialCustomers.forEach((customer, index) => {
390
- console.log(` ${customer.name || 'Unknown Name'}`);
391
- console.log(` To: ${customer.email || 'No email'}`);
392
- console.log(` From Email: ${defaultEmail}`);
393
- console.log(` From Display Name: ${defaultDisplayName}`);
394
- console.log(` Source: ${customer.source || 'Unknown'}`);
395
- console.log('');
396
- });
397
- }
398
- const totalRecipients = (includeExecutives ? executives.length : 0) + (includePotentialCustomers ? potentialCustomers.length : 0);
399
- console.log(`✅ Total: ${totalRecipients} recipient(s) would receive the newsletter`);
400
- return;
401
- }
402
- console.log(`\n📧 Sending newsletter...\n`);
403
- // Send to active executives (from their personal Persona email) - only if includeExecutives is true
404
- if (includeExecutives) {
405
- for (const executive of executives) {
406
- try {
407
- console.log(`📧 Sending to executive ${executive.name} (${executive.email})...`);
408
- const fromEmail = executive.id
409
- ? await getPersonaEmailForExecutive(executive.id)
410
- : fraimConfig.defaultEmail;
411
- const fromDisplayName = `${fraimConfig.personaName} - ${executive.name}'s AI Executive Assistant`;
412
- // Remove emojis from subject line for ASCII compatibility
413
- const subject = newsletter.content.weekTitle.replace(/[^\x00-\x7F]/g, '').trim();
414
- const plainText = generatePlainText(newsletter);
415
- // Get tokens and send (reuses existing infrastructure)
416
- const personaTokens = await getPersonaTokens(executive.id);
417
- const executiveData = await findExecutiveByEmail(executive.email);
418
- await sendEmailViaGmail({
419
- to: executive.email,
420
- subject,
421
- plainTextBody: plainText,
422
- htmlBody: html,
423
- fromEmail,
424
- fromDisplayName
425
- }, executiveData, personaTokens);
426
- console.log(`✅ Sent to ${executive.email}`);
427
- }
428
- catch (error) {
429
- console.error(`❌ Failed to send to ${executive.email}:`, error);
430
- }
431
- }
432
- }
433
- // Send to potential customers (from PROD_FRAIM_DEFAULT_EMAIL)
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 || '';
438
- if (!defaultAccessToken || !defaultRefreshToken) {
439
- console.warn('⚠️ PROD_FRAIM_DEFAULT_ACCESS_TOKEN or PROD_FRAIM_DEFAULT_REFRESH_TOKEN not set. Skipping potential customers.');
440
- }
441
- else {
442
- for (const customer of potentialCustomers) {
443
- try {
444
- console.log(`📧 Sending to potential customer ${customer.name} (${customer.email})...`);
445
- // Remove emojis from subject line for ASCII compatibility
446
- const subject = newsletter.content.weekTitle.replace(/[^\x00-\x7F]/g, '').trim();
447
- const plainText = generatePlainText(newsletter);
448
- // Send from default Persona email with default tokens
449
- await sendEmailViaGmail({
450
- to: customer.email,
451
- subject,
452
- plainTextBody: plainText,
453
- htmlBody: html,
454
- fromEmail: defaultEmail,
455
- fromDisplayName: defaultDisplayName
456
- }, undefined, { access_token: defaultAccessToken, refresh_token: defaultRefreshToken });
457
- console.log(`✅ Sent to ${customer.email}`);
458
- }
459
- catch (error) {
460
- console.error(`❌ Failed to send to ${customer.email}:`, error);
461
- }
462
- }
463
- }
464
- console.log(`\n✅ Newsletter sending complete!`);
465
- if (includeExecutives) {
466
- console.log(` Sent to ${executives.length} executive(s)`);
467
- }
468
- if (includePotentialCustomers) {
469
- console.log(` Sent to ${potentialCustomers.length} potential customer(s)`);
470
- }
471
- process.exit(0);
472
- }
473
- /**
474
- * Get Persona tokens for executive
475
- */
476
- async function getPersonaTokens(executiveId) {
477
- if (!executiveId)
478
- return null;
479
- const mongoUrl = process.env.PROD_MONGO_DATABASE_URL || process.env.MONGO_DATABASE_URL;
480
- if (!mongoUrl)
481
- return null;
482
- const client = new mongodb_1.MongoClient(mongoUrl);
483
- try {
484
- await client.connect();
485
- const dbName = getDatabaseName();
486
- const db = client.db(dbName);
487
- const collectionName = getCollectionName(fraimConfig.identityCollection);
488
- // Check for identity by executive ID
489
- const identity = await db.collection(collectionName).findOne({
490
- executive_id: executiveId,
491
- status: 'active'
492
- }) || await db.collection(collectionName).findOne({
493
- executive_id: executiveId
494
- });
495
- if (identity && identity.access_token && identity.refresh_token) {
496
- return {
497
- access_token: identity.access_token,
498
- refresh_token: identity.refresh_token
499
- };
500
- }
501
- return null;
502
- }
503
- finally {
504
- await client.close();
505
- }
506
- }
507
- /**
508
- * Generate plain text version
509
- */
510
- function generatePlainText(newsletter) {
511
- const { content } = newsletter;
512
- let text = `${content.weekTitle}\n`;
513
- text += `${content.weekSubtitle}\n`;
514
- text += `Week of ${content.weekDate}\n\n`;
515
- text += `${content.openingMessage}\n\n`;
516
- // Hero feature(s) - support both single heroFeature and array of heroFeatures
517
- const heroFeatures = content.heroFeatures || (content.heroFeature ? [content.heroFeature] : []);
518
- if (heroFeatures.length > 0) {
519
- const heroLabel = heroFeatures.length > 1 ? '✨ HERO FEATURES OF THE WEEK' : '✨ FEATURE OF THE WEEK';
520
- text += `${heroLabel}\n\n`;
521
- heroFeatures.forEach((hero, index) => {
522
- if (heroFeatures.length > 1) {
523
- text += `HERO FEATURE ${index + 1}:\n`;
524
- }
525
- text += `${hero.title}\n`;
526
- text += `${hero.description}\n`;
527
- if (hero.impact) {
528
- text += `Impact: ${hero.impact}\n`;
529
- }
530
- text += `\n`;
531
- });
532
- }
533
- if (content.newFeatures && content.newFeatures.length > 0) {
534
- text += `NEW FEATURES:\n`;
535
- content.newFeatures.forEach((f) => {
536
- text += `• ${f.title}: ${f.description}\n`;
537
- });
538
- text += `\n`;
539
- }
540
- if (content.improvements && content.improvements.length > 0) {
541
- text += `IMPROVEMENTS:\n`;
542
- content.improvements.forEach((i) => {
543
- text += `• ${i.title}: ${i.description}\n`;
544
- });
545
- text += `\n`;
546
- }
547
- if (content.bugFixes && content.bugFixes.length > 0) {
548
- text += `BUG FIXES:\n`;
549
- content.bugFixes.forEach((b) => {
550
- text += `• ${b.title}: ${b.description}\n`;
551
- });
552
- text += `\n`;
553
- }
554
- if (content.testimonial) {
555
- text += `\n"${content.testimonial.text}"\n`;
556
- text += `— ${content.testimonial.author}, ${content.testimonial.role}\n\n`;
557
- }
558
- if (content.comingNext) {
559
- text += `COMING NEXT:\n${content.comingNext}\n\n`;
560
- }
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`;
563
- return text;
564
- }
565
- /**
566
- * Send email via Gmail API (reuses existing pattern)
567
- */
568
- async function sendEmailViaGmail(params, executive, personaTokens) {
569
- const { to, subject, plainTextBody, htmlBody, fromEmail, fromDisplayName } = params;
570
- let accessToken;
571
- let refreshToken;
572
- let isProdToken = false; // Track if we're using PROD tokens
573
- if (personaTokens?.access_token && personaTokens?.refresh_token) {
574
- accessToken = personaTokens.access_token;
575
- refreshToken = personaTokens.refresh_token;
576
- // Check if these are PROD tokens by comparing with PROD_FRAIM_DEFAULT_REFRESH_TOKEN
577
- const prodRefreshToken = fraimConfig.prodRefreshToken || '';
578
- if (prodRefreshToken && refreshToken === prodRefreshToken) {
579
- isProdToken = true;
580
- }
581
- }
582
- else if (executive?.personaAccessToken && executive?.personaRefreshToken) {
583
- accessToken = executive.personaAccessToken;
584
- refreshToken = executive.personaRefreshToken;
585
- }
586
- else {
587
- accessToken = fraimConfig.defaultAccessToken || '';
588
- refreshToken = fraimConfig.defaultRefreshToken || '';
589
- if (!accessToken || !refreshToken) {
590
- throw new Error(`${fraimConfig.personaName} Gmail tokens not found`);
591
- }
592
- }
593
- const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
594
- const emailMessage = [
595
- `To: ${to}`,
596
- `From: ${fromDisplayName} <${fromEmail}>`,
597
- `Subject: ${subject}`,
598
- `Content-Type: multipart/alternative; boundary="${boundary}"`,
599
- `MIME-Version: 1.0`,
600
- ``,
601
- `--${boundary}`,
602
- `Content-Type: text/plain; charset=utf-8`,
603
- ``,
604
- plainTextBody,
605
- ``,
606
- `--${boundary}`,
607
- `Content-Type: text/html; charset=utf-8`,
608
- ``,
609
- htmlBody,
610
- ``,
611
- `--${boundary}--`
612
- ].join('\r\n');
613
- // Note: base64 encoding without newlines is important
614
- const url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send';
615
- const requestBody = {
616
- raw: Buffer.from(emailMessage).toString('base64')
617
- .replace(/\+/g, '-')
618
- .replace(/\//g, '_')
619
- .replace(/=+$/, '')
620
- };
621
- const response = await fetch(url, {
622
- method: 'POST',
623
- headers: {
624
- 'Authorization': `Bearer ${accessToken}`,
625
- 'Content-Type': 'application/json',
626
- },
627
- body: JSON.stringify(requestBody)
628
- });
629
- if (!response.ok) {
630
- if (response.status === 401) {
631
- // Token refresh logic - use PROD OAuth credentials if using PROD tokens
632
- const clientId = isProdToken
633
- ? fraimConfig.prodOAuthClientId
634
- : fraimConfig.defaultOAuthClientId;
635
- const clientSecret = isProdToken
636
- ? fraimConfig.prodOAuthClientSecret
637
- : fraimConfig.defaultOAuthClientSecret;
638
- if (!clientId || !clientSecret) {
639
- throw new Error(`OAuth credentials not found for token refresh${isProdToken ? ' (PROD)' : ''}`);
640
- }
641
- const refreshResponse = await fetch('https://oauth2.googleapis.com/token', {
642
- method: 'POST',
643
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
644
- body: new URLSearchParams({
645
- client_id: clientId,
646
- client_secret: clientSecret,
647
- refresh_token: refreshToken,
648
- grant_type: 'refresh_token'
649
- })
650
- });
651
- if (!refreshResponse.ok) {
652
- throw new Error('Failed to refresh access token');
653
- }
654
- const tokenData = await refreshResponse.json();
655
- // Retry with new token
656
- const retryResponse = await fetch(url, {
657
- method: 'POST',
658
- headers: {
659
- 'Authorization': `Bearer ${tokenData.access_token}`,
660
- 'Content-Type': 'application/json',
661
- },
662
- body: JSON.stringify(requestBody)
663
- });
664
- if (!retryResponse.ok) {
665
- throw new Error(`Gmail API error after refresh: ${retryResponse.status}`);
666
- }
667
- return;
668
- }
669
- throw new Error(`Gmail API error: ${response.status}`);
670
- }
671
- }