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.
- package/CHANGELOG.md +10 -0
- package/dist/src/cli/commands/init-project.js +10 -4
- package/dist/src/cli/setup/mcp-config-generator.js +23 -15
- package/dist/src/local-mcp-server/stdio-server.js +207 -0
- package/dist/src/utils/validate-workflows.js +101 -0
- package/dist/src/utils/workflow-parser.js +81 -0
- package/package.json +16 -11
- package/registry/scripts/pdf-styles.css +172 -0
- package/registry/scripts/prep-issue.sh +46 -4
- package/registry/scripts/profile-server.ts +131 -130
- package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +1 -1
- package/registry/stubs/workflows/customer-development/users-to-target.md +1 -1
- package/registry/stubs/workflows/product-building/design.md +1 -1
- package/registry/stubs/workflows/product-building/implement.md +1 -1
- package/Claude.md +0 -1
- package/dist/registry/ai-manager-rules/design-phases/design-completeness-review.md +0 -73
- package/dist/registry/ai-manager-rules/design-phases/design-design.md +0 -145
- package/dist/registry/ai-manager-rules/implement-phases/implement-code.md +0 -283
- package/dist/registry/ai-manager-rules/implement-phases/implement-completeness-review.md +0 -120
- package/dist/registry/ai-manager-rules/implement-phases/implement-regression.md +0 -173
- package/dist/registry/ai-manager-rules/implement-phases/implement-repro.md +0 -104
- package/dist/registry/ai-manager-rules/implement-phases/implement-scoping.md +0 -100
- package/dist/registry/ai-manager-rules/implement-phases/implement-smoke.md +0 -237
- package/dist/registry/ai-manager-rules/implement-phases/implement-spike.md +0 -121
- package/dist/registry/ai-manager-rules/implement-phases/implement-validate.md +0 -375
- package/dist/registry/ai-manager-rules/retrospective.md +0 -116
- package/dist/registry/ai-manager-rules/shared-phases/address-pr-feedback.md +0 -188
- package/dist/registry/ai-manager-rules/shared-phases/submit-pr.md +0 -202
- package/dist/registry/ai-manager-rules/shared-phases/wait-for-pr-review.md +0 -170
- package/dist/registry/ai-manager-rules/spec-phases/spec-competitor-analysis.md +0 -105
- package/dist/registry/ai-manager-rules/spec-phases/spec-completeness-review.md +0 -66
- package/dist/registry/ai-manager-rules/spec-phases/spec-spec.md +0 -139
- package/dist/registry/providers/ado.json +0 -19
- package/dist/registry/providers/github.json +0 -19
- package/dist/registry/scripts/cleanup-branch.js +0 -287
- package/dist/registry/scripts/evaluate-code-quality.js +0 -66
- package/dist/registry/scripts/exec-with-timeout.js +0 -142
- package/dist/registry/scripts/generate-engagement-emails.js +0 -705
- package/dist/registry/scripts/newsletter-helpers.js +0 -671
- package/dist/registry/scripts/profile-server.js +0 -388
- package/dist/registry/scripts/run-thank-you-workflow.js +0 -92
- package/dist/registry/scripts/send-newsletter-simple.js +0 -85
- package/dist/registry/scripts/send-thank-you-emails.js +0 -54
- package/dist/registry/scripts/validate-openapi-limits.js +0 -311
- package/dist/registry/scripts/validate-test-coverage.js +0 -262
- package/dist/registry/scripts/verify-test-coverage.js +0 -66
- package/dist/scripts/build-stub-registry.js +0 -108
- package/dist/src/ai-manager/ai-manager.js +0 -482
- package/dist/src/ai-manager/phase-flow.js +0 -357
- package/dist/src/ai-manager/types.js +0 -5
- package/dist/src/fraim-mcp-server.js +0 -1885
- package/dist/tests/debug-tools.js +0 -80
- package/dist/tests/shared-server-utils.js +0 -57
- package/dist/tests/test-add-ide.js +0 -283
- package/dist/tests/test-ai-coach-edge-cases.js +0 -420
- package/dist/tests/test-ai-coach-mcp-integration.js +0 -450
- package/dist/tests/test-ai-coach-performance.js +0 -328
- package/dist/tests/test-ai-coach-phase-content.js +0 -264
- package/dist/tests/test-ai-coach-workflows.js +0 -514
- package/dist/tests/test-cli.js +0 -228
- package/dist/tests/test-client-scripts-validation.js +0 -167
- package/dist/tests/test-complete-setup-flow.js +0 -110
- package/dist/tests/test-config-system.js +0 -279
- package/dist/tests/test-debug-session.js +0 -134
- package/dist/tests/test-end-to-end-hybrid-validation.js +0 -328
- package/dist/tests/test-enhanced-session-init.js +0 -188
- package/dist/tests/test-first-run-journey.js +0 -368
- package/dist/tests/test-fraim-issues.js +0 -59
- package/dist/tests/test-genericization.js +0 -44
- package/dist/tests/test-hybrid-script-execution.js +0 -340
- package/dist/tests/test-ide-detector.js +0 -46
- package/dist/tests/test-improved-setup.js +0 -121
- package/dist/tests/test-mcp-config-generator.js +0 -99
- package/dist/tests/test-mcp-connection.js +0 -107
- package/dist/tests/test-mcp-issue-integration.js +0 -156
- package/dist/tests/test-mcp-lifecycle-methods.js +0 -240
- package/dist/tests/test-mcp-shared-server.js +0 -308
- package/dist/tests/test-mcp-template-processing.js +0 -160
- package/dist/tests/test-modular-issue-tracking.js +0 -165
- package/dist/tests/test-node-compatibility.js +0 -95
- package/dist/tests/test-npm-install.js +0 -68
- package/dist/tests/test-package-size.js +0 -108
- package/dist/tests/test-pr-review-workflow.js +0 -307
- package/dist/tests/test-prep-issue.js +0 -129
- package/dist/tests/test-productivity-integration.js +0 -157
- package/dist/tests/test-script-location-independence.js +0 -198
- package/dist/tests/test-script-sync.js +0 -557
- package/dist/tests/test-server-utils.js +0 -32
- package/dist/tests/test-session-rehydration.js +0 -148
- package/dist/tests/test-setup-integration.js +0 -98
- package/dist/tests/test-setup-scenarios.js +0 -322
- package/dist/tests/test-standalone.js +0 -143
- package/dist/tests/test-stub-registry.js +0 -136
- package/dist/tests/test-sync-stubs.js +0 -143
- package/dist/tests/test-sync-version-update.js +0 -93
- package/dist/tests/test-telemetry.js +0 -193
- package/dist/tests/test-token-validator.js +0 -30
- package/dist/tests/test-user-journey.js +0 -236
- package/dist/tests/test-users-to-target-workflow.js +0 -253
- package/dist/tests/test-utils.js +0 -109
- package/dist/tests/test-wizard.js +0 -71
- package/dist/tests/test-workflow-discovery.js +0 -242
- package/labels.json +0 -52
- package/registry/agent-guardrails.md +0 -63
- package/registry/fraim.md +0 -48
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-customer-profiling.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-survey-scoping.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-platform-discovery.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-survey-build-linkedin.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-prospect-qualification.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-survey-build-reddit.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-inventory-compilation.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-survey-build-x.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase5-survey-build-facebook.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase6-survey-build-custom.md +0 -11
- package/registry/stubs/workflows/customer-development/ai-coach-phases/phase7-survey-dispatch.md +0 -11
- package/registry/stubs/workflows/customer-development/templates/customer-persona-template.md +0 -11
- package/registry/stubs/workflows/customer-development/templates/search-strategy-template.md +0 -11
- package/setup.js +0 -171
- 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
|
-
}
|