fraim-framework 2.0.56 → 2.0.58

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 (224) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/fraim-mcp.js +14 -0
  3. package/bin/fraim.js +23 -0
  4. package/dist/src/cli/commands/init-project.js +10 -4
  5. package/dist/src/cli/commands/mcp.js +65 -0
  6. package/dist/src/cli/setup/mcp-config-generator.js +19 -16
  7. package/dist/src/fraim/issue-tracking/ado-provider.js +304 -0
  8. package/dist/src/fraim/issue-tracking/factory.js +63 -0
  9. package/dist/src/fraim/issue-tracking/github-provider.js +200 -0
  10. package/dist/src/fraim/issue-tracking/types.js +7 -0
  11. package/dist/src/fraim/issue-tracking-config.js +83 -0
  12. package/dist/src/local-mcp-server/stdio-server.js +207 -0
  13. package/dist/src/utils/workflow-parser.js +81 -0
  14. package/package.json +17 -12
  15. package/registry/scripts/pdf-styles.css +172 -0
  16. package/registry/scripts/prep-issue.sh +46 -4
  17. package/registry/scripts/profile-server.ts +131 -130
  18. package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +1 -1
  19. package/registry/stubs/workflows/customer-development/users-to-target.md +1 -1
  20. package/registry/stubs/workflows/product-building/design.md +1 -1
  21. package/registry/stubs/workflows/product-building/implement.md +1 -1
  22. package/Claude.md +0 -1
  23. package/dist/registry/ai-manager-rules/customer-development-phases/phase1-customer-profiling.md +0 -101
  24. package/dist/registry/ai-manager-rules/customer-development-phases/phase2-platform-discovery.md +0 -235
  25. package/dist/registry/ai-manager-rules/customer-development-phases/phase3-prospect-qualification.md +0 -243
  26. package/dist/registry/ai-manager-rules/customer-development-phases/phase4-inventory-compilation.md +0 -206
  27. package/dist/registry/ai-manager-rules/design-phases/design-completeness-review.md +0 -73
  28. package/dist/registry/ai-manager-rules/design-phases/design-design.md +0 -145
  29. package/dist/registry/ai-manager-rules/implement-phases/implement-code.md +0 -283
  30. package/dist/registry/ai-manager-rules/implement-phases/implement-completeness-review.md +0 -120
  31. package/dist/registry/ai-manager-rules/implement-phases/implement-regression.md +0 -173
  32. package/dist/registry/ai-manager-rules/implement-phases/implement-repro.md +0 -104
  33. package/dist/registry/ai-manager-rules/implement-phases/implement-scoping.md +0 -100
  34. package/dist/registry/ai-manager-rules/implement-phases/implement-smoke.md +0 -237
  35. package/dist/registry/ai-manager-rules/implement-phases/implement-spike.md +0 -121
  36. package/dist/registry/ai-manager-rules/implement-phases/implement-validate.md +0 -375
  37. package/dist/registry/ai-manager-rules/retrospective.md +0 -116
  38. package/dist/registry/ai-manager-rules/shared-phases/address-pr-feedback.md +0 -188
  39. package/dist/registry/ai-manager-rules/shared-phases/submit-pr.md +0 -202
  40. package/dist/registry/ai-manager-rules/shared-phases/wait-for-pr-review.md +0 -170
  41. package/dist/registry/ai-manager-rules/spec-phases/spec-competitor-analysis.md +0 -105
  42. package/dist/registry/ai-manager-rules/spec-phases/spec-completeness-review.md +0 -66
  43. package/dist/registry/ai-manager-rules/spec-phases/spec-spec.md +0 -139
  44. package/dist/registry/ai-manager-rules/user-survey-phases/phase1-survey-scoping.md +0 -60
  45. package/dist/registry/ai-manager-rules/user-survey-phases/phase2-survey-build-linkedin.md +0 -23
  46. package/dist/registry/ai-manager-rules/user-survey-phases/phase3-survey-build-reddit.md +0 -22
  47. package/dist/registry/ai-manager-rules/user-survey-phases/phase4-survey-build-x.md +0 -21
  48. package/dist/registry/ai-manager-rules/user-survey-phases/phase5-survey-build-facebook.md +0 -19
  49. package/dist/registry/ai-manager-rules/user-survey-phases/phase6-survey-build-custom.md +0 -15
  50. package/dist/registry/ai-manager-rules/user-survey-phases/phase7-survey-dispatch.md +0 -45
  51. package/dist/registry/providers/ado.json +0 -19
  52. package/dist/registry/providers/github.json +0 -19
  53. package/dist/registry/scripts/cleanup-branch.js +0 -287
  54. package/dist/registry/scripts/evaluate-code-quality.js +0 -66
  55. package/dist/registry/scripts/exec-with-timeout.js +0 -142
  56. package/dist/registry/scripts/generate-engagement-emails.js +0 -705
  57. package/dist/registry/scripts/newsletter-helpers.js +0 -671
  58. package/dist/registry/scripts/profile-server.js +0 -388
  59. package/dist/registry/scripts/run-thank-you-workflow.js +0 -92
  60. package/dist/registry/scripts/send-newsletter-simple.js +0 -85
  61. package/dist/registry/scripts/send-thank-you-emails.js +0 -54
  62. package/dist/registry/scripts/validate-openapi-limits.js +0 -311
  63. package/dist/registry/scripts/validate-test-coverage.js +0 -262
  64. package/dist/registry/scripts/verify-test-coverage.js +0 -66
  65. package/dist/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +0 -53
  66. package/dist/registry/templates/bootstrap/CODE-QUALITY-REPORT-TEMPLATE.md +0 -37
  67. package/dist/registry/templates/bootstrap/TEST-COVERAGE-REPORT-TEMPLATE.md +0 -35
  68. package/dist/registry/templates/business-development/IDEATION-REPORT-TEMPLATE.md +0 -29
  69. package/dist/registry/templates/business-development/PRICING-STRATEGY-TEMPLATE.md +0 -126
  70. package/dist/registry/templates/customer-development/customer-interview-template.md +0 -99
  71. package/dist/registry/templates/customer-development/customer-persona-template.md +0 -69
  72. package/dist/registry/templates/customer-development/follow-up-email-templates.md +0 -132
  73. package/dist/registry/templates/customer-development/insight-analysis-template.md +0 -74
  74. package/dist/registry/templates/customer-development/prospect-inventory-template.csv +0 -3
  75. package/dist/registry/templates/customer-development/search-strategy-template.md +0 -123
  76. package/dist/registry/templates/customer-development/strategic-recommendations-template.md +0 -53
  77. package/dist/registry/templates/customer-development/thank-you-email-template.html +0 -124
  78. package/dist/registry/templates/customer-development/thank-you-note-template.md +0 -16
  79. package/dist/registry/templates/customer-development/triage-log-template.md +0 -278
  80. package/dist/registry/templates/customer-development/weekly-newsletter-template.html +0 -204
  81. package/dist/registry/templates/evidence/Design-Evidence.md +0 -30
  82. package/dist/registry/templates/evidence/Implementation-BugEvidence.md +0 -94
  83. package/dist/registry/templates/evidence/Implementation-FeatureEvidence.md +0 -129
  84. package/dist/registry/templates/evidence/Spec-Evidence.md +0 -19
  85. package/dist/registry/templates/help/HelpNeeded.md +0 -14
  86. package/dist/registry/templates/legal/NDA-TEMPLATE.md +0 -170
  87. package/dist/registry/templates/legal/PATENT-TEMPLATE.md +0 -372
  88. package/dist/registry/templates/legal/TRADEMARK-TEMPLATE.md +0 -339
  89. package/dist/registry/templates/legal/contract-review-checklist.md +0 -193
  90. package/dist/registry/templates/legal/review-report-template.md +0 -198
  91. package/dist/registry/templates/legal/saas-terms-template.md +0 -174
  92. package/dist/registry/templates/legal/sow-template.md +0 -117
  93. package/dist/registry/templates/legal/template-variables.md +0 -131
  94. package/dist/registry/templates/marketing/DOMAIN-REGISTRATION-TEMPLATE.md +0 -194
  95. package/dist/registry/templates/marketing/HBR-ARTICLE-TEMPLATE.md +0 -66
  96. package/dist/registry/templates/marketing/STORYTELLING-TEMPLATE.md +0 -130
  97. package/dist/registry/templates/marketing/WEBSITE-TEMPLATE.md +0 -262
  98. package/dist/registry/templates/marketing/github-pages-workflow.yml +0 -64
  99. package/dist/registry/templates/replicate/implementation-checklist.md +0 -39
  100. package/dist/registry/templates/replicate/use-cases-template.md +0 -88
  101. package/dist/registry/templates/retrospective/RETROSPECTIVE-TEMPLATE.md +0 -55
  102. package/dist/registry/templates/specs/BUGSPEC-TEMPLATE.md +0 -37
  103. package/dist/registry/templates/specs/FEATURESPEC-TEMPLATE.md +0 -66
  104. package/dist/registry/templates/specs/TECHSPEC-TEMPLATE.md +0 -39
  105. package/dist/registry/workflows/bootstrap/create-architecture.md +0 -38
  106. package/dist/registry/workflows/bootstrap/detect-broken-windows.md +0 -300
  107. package/dist/registry/workflows/bootstrap/evaluate-code-quality.md +0 -35
  108. package/dist/registry/workflows/bootstrap/verify-test-coverage.md +0 -36
  109. package/dist/registry/workflows/brainstorming/blue-sky-brainstorming.md +0 -211
  110. package/dist/registry/workflows/brainstorming/codebase-brainstorming.md +0 -165
  111. package/dist/registry/workflows/business-development/create-business-plan.md +0 -737
  112. package/dist/registry/workflows/business-development/ideate-business-opportunity.md +0 -55
  113. package/dist/registry/workflows/business-development/price-product.md +0 -325
  114. package/dist/registry/workflows/compliance/detect-compliance-requirements.md +0 -78
  115. package/dist/registry/workflows/compliance/generate-audit-evidence.md +0 -75
  116. package/dist/registry/workflows/compliance/soc2-evidence-generator.md +0 -332
  117. package/dist/registry/workflows/customer-development/insight-analysis.md +0 -156
  118. package/dist/registry/workflows/customer-development/insight-triage.md +0 -938
  119. package/dist/registry/workflows/customer-development/interview-preparation.md +0 -452
  120. package/dist/registry/workflows/customer-development/linkedin-outreach.md +0 -593
  121. package/dist/registry/workflows/customer-development/strategic-brainstorming.md +0 -146
  122. package/dist/registry/workflows/customer-development/thank-customers.md +0 -203
  123. package/dist/registry/workflows/customer-development/user-survey-dispatch.md +0 -60
  124. package/dist/registry/workflows/customer-development/users-to-target.md +0 -112
  125. package/dist/registry/workflows/customer-development/weekly-newsletter.md +0 -366
  126. package/dist/registry/workflows/deploy/cloud-deployment.md +0 -310
  127. package/dist/registry/workflows/improve-fraim/contribute.md +0 -32
  128. package/dist/registry/workflows/improve-fraim/file-issue.md +0 -32
  129. package/dist/registry/workflows/learning/build-skillset.md +0 -212
  130. package/dist/registry/workflows/learning/synthesize-learnings.md +0 -284
  131. package/dist/registry/workflows/legal/contract-review-analysis.md +0 -382
  132. package/dist/registry/workflows/legal/nda.md +0 -69
  133. package/dist/registry/workflows/legal/patent-filing.md +0 -76
  134. package/dist/registry/workflows/legal/saas-contract-development.md +0 -213
  135. package/dist/registry/workflows/legal/trademark-filing.md +0 -77
  136. package/dist/registry/workflows/marketing/content-creation.md +0 -37
  137. package/dist/registry/workflows/marketing/convert-to-pdf.md +0 -235
  138. package/dist/registry/workflows/marketing/create-modern-website.md +0 -456
  139. package/dist/registry/workflows/marketing/domain-registration.md +0 -323
  140. package/dist/registry/workflows/marketing/hbr-article.md +0 -73
  141. package/dist/registry/workflows/marketing/launch-checklist.md +0 -37
  142. package/dist/registry/workflows/marketing/marketing-strategy.md +0 -45
  143. package/dist/registry/workflows/marketing/storytelling.md +0 -65
  144. package/dist/registry/workflows/performance/analyze-performance.md +0 -65
  145. package/dist/registry/workflows/product-building/design.md +0 -103
  146. package/dist/registry/workflows/product-building/implement.md +0 -74
  147. package/dist/registry/workflows/product-building/iterate-on-pr-comments.md +0 -70
  148. package/dist/registry/workflows/product-building/prep-issue.md +0 -41
  149. package/dist/registry/workflows/product-building/prototype.md +0 -65
  150. package/dist/registry/workflows/product-building/resolve.md +0 -168
  151. package/dist/registry/workflows/product-building/retrospect.md +0 -86
  152. package/dist/registry/workflows/product-building/spec.md +0 -181
  153. package/dist/registry/workflows/product-building/test.md +0 -125
  154. package/dist/registry/workflows/productivity-report/productivity-report.md +0 -263
  155. package/dist/registry/workflows/quality-assurance/browser-validation.md +0 -221
  156. package/dist/registry/workflows/quality-assurance/iterative-improvement-cycle.md +0 -562
  157. package/dist/registry/workflows/replicate/replicate-discovery.md +0 -336
  158. package/dist/registry/workflows/replicate/replicate-to-issues.md +0 -324
  159. package/dist/registry/workflows/reviewer/review-implementation-vs-design-spec.md +0 -638
  160. package/dist/registry/workflows/reviewer/review-implementation-vs-feature-spec.md +0 -675
  161. package/dist/registry/workflows/startup-credits/aws-activate-application.md +0 -535
  162. package/dist/registry/workflows/startup-credits/google-cloud-application.md +0 -647
  163. package/dist/registry/workflows/startup-credits/microsoft-azure-application.md +0 -538
  164. package/dist/scripts/build-stub-registry.js +0 -108
  165. package/dist/src/ai-manager/ai-manager.js +0 -480
  166. package/dist/src/ai-manager/phase-flow.js +0 -357
  167. package/dist/src/ai-manager/types.js +0 -5
  168. package/dist/src/fraim-mcp-server.js +0 -1885
  169. package/dist/tests/debug-tools.js +0 -80
  170. package/dist/tests/shared-server-utils.js +0 -57
  171. package/dist/tests/test-add-ide.js +0 -283
  172. package/dist/tests/test-ai-coach-edge-cases.js +0 -420
  173. package/dist/tests/test-ai-coach-mcp-integration.js +0 -450
  174. package/dist/tests/test-ai-coach-performance.js +0 -328
  175. package/dist/tests/test-ai-coach-phase-content.js +0 -264
  176. package/dist/tests/test-ai-coach-workflows.js +0 -514
  177. package/dist/tests/test-cli.js +0 -228
  178. package/dist/tests/test-client-scripts-validation.js +0 -167
  179. package/dist/tests/test-complete-setup-flow.js +0 -110
  180. package/dist/tests/test-config-system.js +0 -279
  181. package/dist/tests/test-debug-session.js +0 -134
  182. package/dist/tests/test-end-to-end-hybrid-validation.js +0 -328
  183. package/dist/tests/test-enhanced-session-init.js +0 -188
  184. package/dist/tests/test-first-run-journey.js +0 -368
  185. package/dist/tests/test-fraim-issues.js +0 -59
  186. package/dist/tests/test-genericization.js +0 -44
  187. package/dist/tests/test-hybrid-script-execution.js +0 -340
  188. package/dist/tests/test-ide-detector.js +0 -46
  189. package/dist/tests/test-improved-setup.js +0 -121
  190. package/dist/tests/test-mcp-config-generator.js +0 -99
  191. package/dist/tests/test-mcp-connection.js +0 -107
  192. package/dist/tests/test-mcp-issue-integration.js +0 -156
  193. package/dist/tests/test-mcp-lifecycle-methods.js +0 -240
  194. package/dist/tests/test-mcp-shared-server.js +0 -308
  195. package/dist/tests/test-mcp-template-processing.js +0 -160
  196. package/dist/tests/test-modular-issue-tracking.js +0 -165
  197. package/dist/tests/test-node-compatibility.js +0 -95
  198. package/dist/tests/test-npm-install.js +0 -68
  199. package/dist/tests/test-package-size.js +0 -108
  200. package/dist/tests/test-pr-review-workflow.js +0 -307
  201. package/dist/tests/test-prep-issue.js +0 -129
  202. package/dist/tests/test-productivity-integration.js +0 -157
  203. package/dist/tests/test-script-location-independence.js +0 -198
  204. package/dist/tests/test-script-sync.js +0 -557
  205. package/dist/tests/test-server-utils.js +0 -32
  206. package/dist/tests/test-session-rehydration.js +0 -148
  207. package/dist/tests/test-setup-integration.js +0 -98
  208. package/dist/tests/test-setup-scenarios.js +0 -322
  209. package/dist/tests/test-standalone.js +0 -143
  210. package/dist/tests/test-stub-registry.js +0 -136
  211. package/dist/tests/test-sync-stubs.js +0 -143
  212. package/dist/tests/test-sync-version-update.js +0 -93
  213. package/dist/tests/test-telemetry.js +0 -193
  214. package/dist/tests/test-token-validator.js +0 -30
  215. package/dist/tests/test-user-journey.js +0 -236
  216. package/dist/tests/test-users-to-target-workflow.js +0 -253
  217. package/dist/tests/test-utils.js +0 -109
  218. package/dist/tests/test-wizard.js +0 -71
  219. package/dist/tests/test-workflow-discovery.js +0 -242
  220. package/labels.json +0 -52
  221. package/registry/agent-guardrails.md +0 -63
  222. package/registry/fraim.md +0 -48
  223. package/setup.js +0 -171
  224. package/tsconfig.json +0 -23
@@ -1,705 +0,0 @@
1
- #!/usr/bin/env tsx
2
- "use strict";
3
- /**
4
- * Engagement Email Functions for AI Agents
5
- *
6
- * Helper functions for AI agents to use in customer engagement workflows.
7
- * Provides deterministic functions for issue retrieval, database lookup, and email sending.
8
- */
9
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- var desc = Object.getOwnPropertyDescriptor(m, k);
12
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
- desc = { enumerable: true, get: function() { return m[k]; } };
14
- }
15
- Object.defineProperty(o, k2, desc);
16
- }) : (function(o, m, k, k2) {
17
- if (k2 === undefined) k2 = k;
18
- o[k2] = m[k];
19
- }));
20
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
- Object.defineProperty(o, "default", { enumerable: true, value: v });
22
- }) : function(o, v) {
23
- o["default"] = v;
24
- });
25
- var __importStar = (this && this.__importStar) || (function () {
26
- var ownKeys = function(o) {
27
- ownKeys = Object.getOwnPropertyNames || function (o) {
28
- var ar = [];
29
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
- return ar;
31
- };
32
- return ownKeys(o);
33
- };
34
- return function (mod) {
35
- if (mod && mod.__esModule) return mod;
36
- var result = {};
37
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
- __setModuleDefault(result, mod);
39
- return result;
40
- };
41
- })();
42
- Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.getLatestThankYouDate = getLatestThankYouDate;
44
- exports.getResolvedIssues = getResolvedIssues;
45
- exports.findExecutiveByEmail = findExecutiveByEmail;
46
- exports.getPersonaEmailForExecutive = getPersonaEmailForExecutive;
47
- exports.sendCustomerMail = sendCustomerMail;
48
- const child_process_1 = require("child_process");
49
- const fs_1 = require("fs");
50
- const mongodb_1 = require("mongodb");
51
- const path_1 = require("path");
52
- // Self-contained utility functions (no FRAIM internal imports)
53
- function determineDatabaseName() {
54
- const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
55
- // If explicitly set to ppe/staging, use that; otherwise default to prod
56
- if (env === 'ppe' || env === 'staging') {
57
- return 'fraim_ppe';
58
- }
59
- // Default to production
60
- return process.env.MONGO_DB_NAME || 'fraim_prod';
61
- }
62
- function determineSchema(branchName) {
63
- if (branchName.includes('ppe') || branchName.includes('staging'))
64
- return 'ppe';
65
- return 'prod';
66
- }
67
- function getCurrentGitBranch() {
68
- try {
69
- return (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
70
- }
71
- catch (error) {
72
- return 'master';
73
- }
74
- }
75
- function loadClientConfig() {
76
- const configPath = (0, path_1.join)(process.cwd(), '.fraim', 'config.json');
77
- if (!(0, fs_1.existsSync)(configPath)) {
78
- throw new Error('.fraim/config.json not found. Run fraim init first.');
79
- }
80
- return JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
81
- }
82
- function getEnvOr(keys, fallback) {
83
- for (const key of keys) {
84
- const value = process.env[key];
85
- if (value && value.length)
86
- return value;
87
- }
88
- return fallback;
89
- }
90
- // Load configuration
91
- const config = loadClientConfig();
92
- const personaName = config.persona.name;
93
- const personaDisplayNameDefault = config.persona.displayNamePattern.replace('{executiveName}', 'Your');
94
- const fraimConfig = {
95
- repoOwner: config.git.repoOwner || 'mathursrus',
96
- repoName: config.git.repoName || 'fraim-repo',
97
- projectName: 'FRAIM',
98
- personaName,
99
- personaPronouns: getEnvOr(['FRAIM_PERSONA_PRONOUNS'], 'they/them'),
100
- personaDisplayName: getEnvOr(['FRAIM_PERSONA_DISPLAY_NAME'], personaDisplayNameDefault),
101
- personaDisplayNamePattern: config.persona.displayNamePattern,
102
- personaThankYouSignature: config.persona.emailSignature,
103
- defaultEmail: getEnvOr(['FRAIM_DEFAULT_EMAIL'], 'agent@example.com'),
104
- prodDefaultEmail: getEnvOr(['PROD_FRAIM_DEFAULT_EMAIL'], 'agent@example.com'),
105
- defaultAccessToken: getEnvOr(['FRAIM_DEFAULT_ACCESS_TOKEN'], ''),
106
- defaultRefreshToken: getEnvOr(['FRAIM_DEFAULT_REFRESH_TOKEN'], ''),
107
- prodAccessToken: getEnvOr(['PROD_FRAIM_DEFAULT_ACCESS_TOKEN'], ''),
108
- prodRefreshToken: getEnvOr(['PROD_FRAIM_DEFAULT_REFRESH_TOKEN'], ''),
109
- defaultOAuthClientId: getEnvOr(['FRAIM_DEFAULT_OAUTH_CLIENT_ID'], ''),
110
- defaultOAuthClientSecret: getEnvOr(['FRAIM_DEFAULT_OAUTH_CLIENT_SECRET'], ''),
111
- prodOAuthClientId: getEnvOr(['PROD_FRAIM_DEFAULT_OAUTH_CLIENT_ID'], ''),
112
- prodOAuthClientSecret: getEnvOr(['PROD_FRAIM_DEFAULT_OAUTH_CLIENT_SECRET'], ''),
113
- identityCollection: config.database?.identityCollection || 'Identity',
114
- executiveCollection: config.database?.executiveCollection || 'Executive',
115
- newsletterTitle: 'Weekly Update',
116
- newsletterCtaText: 'Learn More',
117
- newsletterUrl: getEnvOr(['FRAIM_NEWSLETTER_URL'], ''),
118
- issueRepoUrl: `https://github.com/${config.git.repoOwner}/${config.git.repoName}.git`,
119
- identityTokensCollection: 'Tokens',
120
- webAppUrl: config.marketing?.websiteUrl || getEnvOr(['FRAIM_WEB_APP_URL'], 'http://localhost:3000'),
121
- chatUrl: config.marketing?.chatUrl || getEnvOr(['FRAIM_CHAT_URL'], ''),
122
- };
123
- function formatExecutiveDisplayName(executiveName) {
124
- return config.persona.displayNamePattern.replace('{executiveName}', executiveName);
125
- }
126
- function formatPersonaSignature() {
127
- return `With gratitude,\n${fraimConfig.personaThankYouSignature}\n${personaName} - Your AI Executive Assistant\n\n`;
128
- }
129
- // Get template path (relative to script location)
130
- function getTemplatePath() {
131
- // Script is at <this-path>
132
- // Template is at templates/customer-development/thank-you-email-template.html (Retrieve via get_fraim_file)
133
- // Or generic path? Let's keep existing path logic but relative to process.cwd() as before
134
- return (0, path_1.join)(process.cwd(), 'registry', 'templates', 'customer-development', 'thank-you-email-template.html');
135
- }
136
- /**
137
- * Get the latest date from existing thank-you candidates files
138
- * Returns the most recent date when thank-you emails were sent, or null if no files exist
139
- */
140
- function getLatestThankYouDate() {
141
- const notesDir = 'docs/customer-development/thank-you-notes';
142
- if (!(0, fs_1.existsSync)(notesDir)) {
143
- return null;
144
- }
145
- const files = (0, fs_1.readdirSync)(notesDir).filter(f => f.startsWith('thank-you-candidates-') && f.endsWith('.json'));
146
- if (files.length === 0) {
147
- return null;
148
- }
149
- // Extract dates from filenames (e.g., thank-you-candidates-2025-10-29.json)
150
- const dates = [];
151
- for (const file of files) {
152
- const dateMatch = file.match(/thank-you-candidates-(\d{4}-\d{2}-\d{2})\.json/);
153
- if (dateMatch) {
154
- const filePath = (0, path_1.join)(notesDir, file);
155
- try {
156
- const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
157
- const json = JSON.parse(content);
158
- // Check if any candidates have status "sent"
159
- const hasSentEmails = json.candidates?.some((c) => c.status === 'sent');
160
- if (hasSentEmails) {
161
- dates.push(dateMatch[1]);
162
- }
163
- }
164
- catch (error) {
165
- // Skip invalid JSON files
166
- console.warn(`⚠️ Could not parse ${file}:`, error);
167
- }
168
- }
169
- }
170
- if (dates.length === 0) {
171
- return null;
172
- }
173
- // Return the most recent date
174
- dates.sort();
175
- const latestDate = dates[dates.length - 1];
176
- console.log(`📅 Latest thank-you date found: ${latestDate}`);
177
- return latestDate;
178
- }
179
- /**
180
- * Get resolved issues since a specified date (or latest thank-you date if not provided)
181
- * AI agents call this to retrieve issues that were resolved
182
- * @param date Optional date string (YYYY-MM-DD). If not provided, uses latest thank-you date. If no thank-you files exist, uses last 14 days.
183
- */
184
- async function getResolvedIssues(date) {
185
- if (!date) {
186
- // Try to get the latest thank-you date
187
- const latestThankYouDate = getLatestThankYouDate();
188
- if (latestThankYouDate) {
189
- date = latestThankYouDate;
190
- console.log(`📋 Using latest thank-you date as starting point: ${date}`);
191
- }
192
- else {
193
- // Fallback to last 14 days if no thank-you files exist
194
- const fallbackDate = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
195
- date = fallbackDate;
196
- console.log(`📋 No previous thank-you files found, using last 14 days: ${date}`);
197
- }
198
- }
199
- // Use ">YYYY-MM-DD" format to get issues updated AFTER the date (not on the date)
200
- // GitHub CLI requires quotes around the comparison operator
201
- const command = `gh search issues --repo=${fraimConfig.repoOwner}/${fraimConfig.repoName} --state=closed --updated=">${date}" --label=user-reported --json number,title,body,labels,closedAt,author`;
202
- const output = (0, child_process_1.execSync)(command, { encoding: 'utf-8' });
203
- return JSON.parse(output);
204
- }
205
- function getProductionDatabase() {
206
- const mongoUrl = process.env.PROD_MONGO_DATABASE_URL || process.env.MONGO_DATABASE_URL;
207
- if (!mongoUrl) {
208
- throw new Error('PROD_MONGO_DATABASE_URL or MONGO_DATABASE_URL environment variable is required');
209
- }
210
- return new mongodb_1.MongoClient(mongoUrl);
211
- }
212
- /**
213
- * Get database name, defaulting to production
214
- */
215
- function getDatabaseName() {
216
- const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
217
- // If explicitly set to ppe/staging, use that; otherwise default to prod
218
- if (env === 'ppe' || env === 'staging') {
219
- return determineDatabaseName();
220
- }
221
- // Default to production
222
- return process.env.MONGO_DB_NAME || 'fraim_prod';
223
- }
224
- /**
225
- * Get collection name with schema prefix (defaults to prod schema)
226
- */
227
- function getCollectionName(baseName) {
228
- const env = process.env.NODE_ENV || process.env.ENVIRONMENT;
229
- // If explicitly set to ppe/staging, use that schema; otherwise default to prod
230
- if (env === 'ppe' || env === 'staging') {
231
- const schema = determineSchema(getCurrentGitBranch());
232
- return `${schema}_${baseName}`;
233
- }
234
- // Default to prod schema
235
- return `prod_${baseName}`;
236
- }
237
- /**
238
- * Find executive by email in database (defaults to production)
239
- * AI agents call this to get executive info for database lookups
240
- */
241
- async function findExecutiveByEmail(email) {
242
- const client = getProductionDatabase();
243
- try {
244
- await client.connect();
245
- const dbName = getDatabaseName();
246
- const db = client.db(dbName);
247
- const executive = await db.collection(getCollectionName('Executive')).findOne({ email: email.toLowerCase() });
248
- return executive;
249
- }
250
- finally {
251
- await client.close();
252
- }
253
- }
254
- /**
255
- * Get the persona email for an executive from the identity collection
256
- * AI agents call this to get tokens or contact info for the executive's assistant
257
- */
258
- async function getPersonaEmailForExecutive(executiveId) {
259
- const { MongoClient } = await Promise.resolve().then(() => __importStar(require('mongodb')));
260
- const mongoUrl = process.env.PROD_MONGO_DATABASE_URL || process.env.MONGO_DATABASE_URL;
261
- if (!mongoUrl) {
262
- console.warn(`⚠️ PROD_MONGO_DATABASE_URL not set, using default ${fraimConfig.personaName} email`);
263
- return fraimConfig.defaultEmail;
264
- }
265
- const client = new MongoClient(mongoUrl);
266
- try {
267
- await client.connect();
268
- const dbName = getDatabaseName();
269
- const db = client.db(dbName);
270
- const collectionName = getCollectionName(fraimConfig.identityCollection);
271
- // Query the identity collection (defaults to prod)
272
- let identity = await db.collection(collectionName).findOne({
273
- executive_id: executiveId,
274
- status: 'active'
275
- });
276
- if (!identity) {
277
- identity = await db.collection(collectionName).findOne({
278
- executive_id: executiveId
279
- });
280
- }
281
- if (identity && identity.email) {
282
- return identity.email;
283
- }
284
- return fraimConfig.defaultEmail;
285
- }
286
- catch (error) {
287
- console.warn(`⚠️ Could not get ${fraimConfig.personaName} email for executive ${executiveId}:`, error);
288
- return fraimConfig.defaultEmail;
289
- }
290
- finally {
291
- await client.close();
292
- }
293
- }
294
- /**
295
- * Send emails from candidates file (JSON format)
296
- * AI agents call this after user review to send all emails
297
- * @param candidatesFilePath Path to the JSON candidates file
298
- * @param executiveId Optional executive ID to filter emails to specific executive only
299
- */
300
- async function sendCustomerMail(candidatesFilePath, executiveId) {
301
- if (!(0, fs_1.existsSync)(candidatesFilePath)) {
302
- throw new Error(`Candidates file not found: ${candidatesFilePath}`);
303
- }
304
- console.log(`📧 Sending emails from candidates file: ${candidatesFilePath}`);
305
- if (executiveId) {
306
- console.log(`🎯 Filtering to executive ID: ${executiveId}`);
307
- }
308
- const content = (0, fs_1.readFileSync)(candidatesFilePath, 'utf-8');
309
- const candidatesFile = JSON.parse(content);
310
- console.log(`📊 Found ${candidatesFile.candidates.length} candidates (${candidatesFile.metadata.totalCustomers} customers)`);
311
- // Filter candidates if executiveId is provided
312
- let candidatesToSend = candidatesFile.candidates;
313
- if (executiveId) {
314
- candidatesToSend = candidatesFile.candidates.filter(candidate => candidate.executive.id === executiveId);
315
- console.log(`🔍 Filtered to ${candidatesToSend.length} candidate(s) for executive ${executiveId}`);
316
- if (candidatesToSend.length === 0) {
317
- console.log(`⚠️ No candidates found for executive ID: ${executiveId}`);
318
- return;
319
- }
320
- }
321
- // Update file with results as we send
322
- const updatedCandidates = [...candidatesFile.candidates];
323
- for (let i = 0; i < updatedCandidates.length; i++) {
324
- const candidate = updatedCandidates[i];
325
- // Skip if filtering by executive ID and this candidate doesn't match
326
- if (executiveId && candidate.executive.id !== executiveId) {
327
- continue;
328
- }
329
- if (candidate.status !== 'pending') {
330
- console.log(`⏭️ Skipping ${candidate.customer.email} (status: ${candidate.status})`);
331
- continue;
332
- }
333
- try {
334
- console.log(`📧 Sending email to ${candidate.customer.email}...`);
335
- // Get executive from database if we have their email
336
- const executive = await findExecutiveByEmail(candidate.customer.email);
337
- // Get persona tokens from identity collection if the executive is known
338
- let personaTokens = null;
339
- if (candidate.executive.id) {
340
- try {
341
- const client = getProductionDatabase();
342
- try {
343
- await client.connect();
344
- const dbName = getDatabaseName();
345
- const db = client.db(dbName);
346
- const collectionName = getCollectionName(fraimConfig.identityCollection);
347
- const identity = await db.collection(collectionName).findOne({
348
- executive_id: candidate.executive.id,
349
- status: 'active'
350
- }) || await db.collection(collectionName).findOne({
351
- executive_id: candidate.executive.id
352
- });
353
- if (identity && identity.access_token && identity.refresh_token) {
354
- personaTokens = {
355
- access_token: identity.access_token,
356
- refresh_token: identity.refresh_token
357
- };
358
- console.log(`Found ${fraimConfig.personaName} tokens in ${dbName} database for ${candidate.from.email}`);
359
- }
360
- else {
361
- console.warn(`No ${fraimConfig.personaName} tokens found in ${dbName} database for executive ${candidate.executive.id}`);
362
- }
363
- }
364
- finally {
365
- await client.close();
366
- }
367
- }
368
- catch (error) {
369
- console.warn(`Could not get ${fraimConfig.personaName} tokens from database for executive ${candidate.executive.id}:`, error);
370
- }
371
- }
372
- // Generate plain text body for fallback (from structured format if available)
373
- const plainTextBody = generatePlainTextBody(candidate);
374
- await sendSingleEmail({
375
- to: candidate.to,
376
- subject: candidate.subject,
377
- body: plainTextBody,
378
- fromEmail: candidate.from.email,
379
- fromDisplayName: candidate.from.displayName,
380
- candidate // Pass full candidate for HTML generation
381
- }, executive, personaTokens);
382
- console.log(`✅ Email sent to ${candidate.customer.email}`);
383
- updatedCandidates[i].status = 'sent';
384
- // Update file after each successful send
385
- const updatedFile = {
386
- ...candidatesFile,
387
- candidates: updatedCandidates
388
- };
389
- (0, fs_1.writeFileSync)(candidatesFilePath, JSON.stringify(updatedFile, null, 2));
390
- }
391
- catch (error) {
392
- console.error(`❌ Failed to send email to ${candidate.customer.email}:`, error);
393
- updatedCandidates[i].status = 'failed';
394
- // Update file with failure status
395
- const updatedFile = {
396
- ...candidatesFile,
397
- candidates: updatedCandidates
398
- };
399
- (0, fs_1.writeFileSync)(candidatesFilePath, JSON.stringify(updatedFile, null, 2));
400
- }
401
- }
402
- console.log(`\n✅ Email sending complete. Status updated in ${candidatesFilePath}`);
403
- // Exit cleanly to prevent hanging
404
- process.exit(0);
405
- }
406
- async function sendSingleEmail(params, executive, personaTokens) {
407
- const { to, subject, body, fromEmail, fromDisplayName } = params;
408
- // Get Persona's tokens - prioritize identity tokens, then executive tokens, then default
409
- let accessToken;
410
- let refreshToken;
411
- if (personaTokens?.access_token && personaTokens?.refresh_token) {
412
- // Use persona identity tokens (preferred - ensures correct From address)
413
- console.log(`🔑 Using ${fraimConfig.personaName} identity tokens for ${fromEmail}`);
414
- accessToken = personaTokens.access_token;
415
- refreshToken = personaTokens.refresh_token;
416
- }
417
- else if (executive?.personaAccessToken && executive?.personaRefreshToken) {
418
- // Fallback to executive-specific tokens
419
- console.log(`🔑 Using executive persona tokens as fallback`);
420
- accessToken = executive.personaAccessToken;
421
- refreshToken = executive.personaRefreshToken;
422
- }
423
- else {
424
- // Use default persona tokens as last resort
425
- console.log(`🔑 Using default ${fraimConfig.personaName} tokens as fallback`);
426
- accessToken = fraimConfig.defaultAccessToken;
427
- refreshToken = fraimConfig.defaultRefreshToken;
428
- if (!accessToken || !refreshToken) {
429
- throw new Error(`${fraimConfig.personaName} Gmail tokens not found. Need either identity tokens, executive tokens, or default tokens in environment variables.`);
430
- }
431
- }
432
- // Verify the From email matches the authenticated account (Gmail requirement)
433
- // For plus addressing, the base email should match
434
- const fromBaseEmail = fromEmail.includes('+')
435
- ? fromEmail.split('+')[0] + '@' + fromEmail.split('@')[1]
436
- : fromEmail;
437
- console.log(`📧 Sending from: "${fromDisplayName}" <${fromEmail}>`);
438
- // Generate HTML email from template (use structured format if available)
439
- const greeting = params.candidate?.greeting || 'Hi there,';
440
- const htmlBody = generateHtmlEmail({
441
- displayName: extractFirstName(greeting) || extractFirstName(body) || extractFirstName(fromDisplayName) || 'there',
442
- greeting: greeting,
443
- opening: params.candidate?.opening
444
- ? convertTextToHtml(params.candidate.opening)
445
- : convertBodyToHtml(body), // Fallback to legacy body
446
- improvements: params.candidate?.improvements || [],
447
- closing: params.candidate?.closing || '',
448
- executiveName: extractExecutiveName(fromDisplayName),
449
- fromEmail
450
- });
451
- // Create multipart email message (plain text + HTML)
452
- const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
453
- const emailMessage = [
454
- `To: ${to}`,
455
- `From: ${fromDisplayName} <${fromEmail}>`,
456
- `Subject: ${subject}`,
457
- `Content-Type: multipart/alternative; boundary="${boundary}"`,
458
- `MIME-Version: 1.0`,
459
- ``,
460
- `--${boundary}`,
461
- `Content-Type: text/plain; charset=utf-8`,
462
- `Content-Transfer-Encoding: 7bit`,
463
- ``,
464
- body,
465
- ``,
466
- `--${boundary}`,
467
- `Content-Type: text/html; charset=utf-8`,
468
- `Content-Transfer-Encoding: 7bit`,
469
- ``,
470
- htmlBody,
471
- ``,
472
- `--${boundary}--`
473
- ].join('\r\n');
474
- // Send email using Gmail API
475
- const url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send';
476
- const requestBody = {
477
- raw: Buffer.from(emailMessage).toString('base64')
478
- .replace(/\+/g, '-')
479
- .replace(/\//g, '_')
480
- .replace(/=+$/, '')
481
- };
482
- const response = await fetch(url, {
483
- method: 'POST',
484
- headers: {
485
- 'Authorization': `Bearer ${accessToken}`,
486
- 'Content-Type': 'application/json',
487
- },
488
- body: JSON.stringify(requestBody)
489
- });
490
- if (!response.ok) {
491
- if (response.status === 401) {
492
- console.log('🔄 Access token expired, refreshing...');
493
- // Refresh the access token
494
- const clientId = fraimConfig.defaultOAuthClientId || process.env.FRAIM_DEFAULT_OAUTH_CLIENT_ID || process.env.PERSONA_DEFAULT_OAUTH_CLIENT_ID;
495
- const clientSecret = fraimConfig.defaultOAuthClientSecret || process.env.FRAIM_DEFAULT_OAUTH_CLIENT_SECRET || process.env.PERSONA_DEFAULT_OAUTH_CLIENT_SECRET;
496
- if (!clientId || !clientSecret) {
497
- throw new Error('FRAIM_DEFAULT_OAUTH_CLIENT_ID and FRAIM_DEFAULT_OAUTH_CLIENT_SECRET environment variables are required for token refresh');
498
- }
499
- console.log('🔄 Refreshing token...');
500
- const refreshResponse = await fetch('https://oauth2.googleapis.com/token', {
501
- method: 'POST',
502
- headers: {
503
- 'Content-Type': 'application/x-www-form-urlencoded',
504
- },
505
- body: new URLSearchParams({
506
- client_id: clientId,
507
- client_secret: clientSecret,
508
- refresh_token: refreshToken,
509
- grant_type: 'refresh_token'
510
- })
511
- });
512
- if (!refreshResponse.ok) {
513
- const errorText = await refreshResponse.text();
514
- console.error('❌ Token refresh failed:', refreshResponse.status, errorText);
515
- throw new Error(`Failed to refresh access token: ${refreshResponse.status} - ${errorText}`);
516
- }
517
- const tokenData = await refreshResponse.json();
518
- const newAccessToken = tokenData.access_token;
519
- console.log('✅ Token refreshed, retrying email send...');
520
- // Retry with new token
521
- const retryResponse = await fetch(url, {
522
- method: 'POST',
523
- headers: {
524
- 'Authorization': `Bearer ${newAccessToken}`,
525
- 'Content-Type': 'application/json',
526
- },
527
- body: JSON.stringify(requestBody)
528
- });
529
- if (!retryResponse.ok) {
530
- const errorText = await retryResponse.text();
531
- throw new Error(`Gmail API error after refresh: ${retryResponse.status} - ${errorText}`);
532
- }
533
- const result = await retryResponse.json();
534
- console.log(`✅ Email sent successfully to ${to}`);
535
- console.log(`📧 Email ID: ${result.id}`);
536
- return;
537
- }
538
- const errorText = await response.text();
539
- throw new Error(`Gmail API error: ${response.status} - ${errorText}`);
540
- }
541
- const result = await response.json();
542
- console.log(`✅ Email sent successfully to ${to}`);
543
- console.log(`📧 Email ID: ${result.id}`);
544
- }
545
- /**
546
- * Generate HTML email from template
547
- */
548
- function generateHtmlEmail(params) {
549
- const templatePath = getTemplatePath();
550
- let template = (0, fs_1.readFileSync)(templatePath, 'utf-8');
551
- // Replace simple template variables
552
- template = template.replace(/\{\{displayName\}\}/g, escapeHtml(params.displayName));
553
- template = template.replace(/\{\{executiveName\}\}/g, escapeHtml(params.executiveName));
554
- template = template.replace(/\{\{personaName\}\}/g, escapeHtml(fraimConfig.personaName));
555
- template = template.replace(/\{\{chatUrl\}\}/g, escapeHtml(fraimConfig.chatUrl || '#'));
556
- template = template.replace(/\{\{webAppUrl\}\}/g, escapeHtml(fraimConfig.webAppUrl || '#'));
557
- template = template.replace(/\{\{fromEmail\}\}/g, escapeHtml(params.fromEmail));
558
- template = template.replace(/\{\{greeting\}\}/g, escapeHtml(params.greeting));
559
- template = template.replace(/\{\{opening\}\}/g, params.opening);
560
- // Generate improvements list HTML
561
- const hasImprovements = params.improvements && params.improvements.length > 0;
562
- if (hasImprovements) {
563
- const improvementsHtml = params.improvements.map((improvement, index) => {
564
- let html = `<div style="margin-bottom: ${index < params.improvements.length - 1 ? '20px' : '0'}; padding-bottom: ${index < params.improvements.length - 1 ? '20px' : '0'}; border-bottom: ${index < params.improvements.length - 1 ? '1px solid #e9ecef' : 'none'};">`;
565
- html += `<div style="font-size: 16px; font-weight: 600; color: #333333; margin-bottom: 8px;">${escapeHtml(improvement.title)}</div>`;
566
- html += `<div style="font-size: 15px; color: #555555; line-height: 1.7; margin-bottom: ${improvement.verification ? '10px' : '0'};">${convertTextToHtml(improvement.description)}</div>`;
567
- if (improvement.verification) {
568
- html += `<div style="font-size: 14px; color: #667eea; font-style: italic; padding-top: 8px; border-top: 1px solid #e9ecef; margin-top: 8px;">💡 <strong>How to verify:</strong> ${convertTextToHtml(improvement.verification)}</div>`;
569
- }
570
- html += `</div>`;
571
- return html;
572
- }).join('');
573
- const improvementsSection = `
574
- <tr>
575
- <td style="padding: 0 30px 20px 30px;">
576
- <div style="background-color: #f8f9fa; border-left: 4px solid #667eea; border-radius: 6px; padding: 20px; margin: 20px 0;">
577
- <div style="font-size: 18px; font-weight: 600; color: #333333; margin-bottom: 16px;">
578
- ✨ What's Fixed
579
- </div>
580
- ${improvementsHtml}
581
- </div>
582
- </td>
583
- </tr>`;
584
- template = template.replace(/\{\{#if hasImprovements\}\}[\s\S]*?\{\{\/if\}\}/, improvementsSection);
585
- }
586
- else {
587
- // Remove conditional block if no improvements
588
- template = template.replace(/\{\{#if hasImprovements\}\}[\s\S]*?\{\{\/if\}\}/, '');
589
- }
590
- // Replace closing paragraph
591
- if (params.closing) {
592
- template = template.replace(/\{\{#if closing\}\}[\s\S]*?\{\{\/if\}\}/, `
593
- <tr>
594
- <td style="padding: 0 30px 20px 30px;">
595
- <div style="font-size: 16px; color: #555555; line-height: 1.8;">
596
- ${convertTextToHtml(params.closing)}
597
- </div>
598
- </td>
599
- </tr>`);
600
- }
601
- else {
602
- template = template.replace(/\{\{#if closing\}\}[\s\S]*?\{\{\/if\}\}/, '');
603
- }
604
- return template;
605
- }
606
- /**
607
- * Generate plain text body from candidate (supports both structured and legacy formats)
608
- */
609
- function generatePlainTextBody(candidate) {
610
- // If structured format exists, use it
611
- if (candidate.greeting && candidate.opening) {
612
- let body = `${candidate.greeting}\n\n${candidate.opening}\n\n`;
613
- if (candidate.improvements && candidate.improvements.length > 0) {
614
- body += `**What was improved:**\n\n`;
615
- candidate.improvements.forEach((improvement, index) => {
616
- body += `${index + 1}. ${improvement.title}: ${improvement.description}`;
617
- if (improvement.verification) {
618
- body += `\n **How to verify:** ${improvement.verification}`;
619
- }
620
- body += `\n\n`;
621
- });
622
- }
623
- if (candidate.closing) {
624
- body += `${candidate.closing}\n\n`;
625
- }
626
- body += formatPersonaSignature();
627
- return body;
628
- }
629
- // Fallback to legacy body format
630
- return candidate.body || '';
631
- }
632
- /**
633
- * Extract first name from text
634
- */
635
- function extractFirstName(text) {
636
- // Try to find "Hi [Name]" pattern
637
- const hiMatch = text.match(/\b[Hh]i\s+([A-Z][a-z]+)/);
638
- if (hiMatch) {
639
- return hiMatch[1];
640
- }
641
- // Try to find comma after greeting
642
- const commaMatch = text.match(/^[Hh]i\s+([^,]+),/);
643
- if (commaMatch) {
644
- return commaMatch[1].trim().split(' ')[0];
645
- }
646
- return null;
647
- }
648
- /**
649
- * Extract executive name from display name
650
- */
651
- function extractExecutiveName(displayName) {
652
- // "{Persona} - [Name]'s AI Executive Assistant"
653
- // We need to match based on the configured pattern
654
- const pattern = fraimConfig.personaDisplayNamePattern.replace('{executiveName}', '(.+?)');
655
- try {
656
- // If pattern contains regex characters, they need expanding or escaping?
657
- // This is simple pattern matching.
658
- // Example: "Persona - {executiveName}'s AI Executive Assistant"
659
- // Regex: /Persona - (.+?)'s AI Executive Assistant/
660
- // We escape special chars except the group we added
661
- const escapedPattern = pattern
662
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex chars
663
- .replace('\\(\\.+\\?\\)', '(.+?)'); // Restore capture group
664
- const match = displayName.match(new RegExp(escapedPattern));
665
- return match ? match[1] : 'Your';
666
- }
667
- catch (e) {
668
- return 'Your';
669
- }
670
- }
671
- /**
672
- * Convert plain text to HTML (for legacy body format)
673
- */
674
- function convertBodyToHtml(body) {
675
- return convertTextToHtml(body);
676
- }
677
- /**
678
- * Convert text to HTML (preserves markdown-style formatting)
679
- */
680
- function convertTextToHtml(text) {
681
- if (!text)
682
- return '';
683
- let html = text;
684
- // Convert markdown-style bold **text** to <strong>text</strong>
685
- html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
686
- // Convert line breaks to paragraphs
687
- html = html.replace(/\n\n+/g, '</p><p style="margin: 12px 0;">');
688
- html = html.replace(/\n/g, '<br>');
689
- // Wrap in paragraph tags
690
- html = `<p style="margin: 0 0 12px 0;">${html}</p>`;
691
- // Clean up empty paragraphs
692
- html = html.replace(/<p[^>]*><\/p>/g, '');
693
- return html;
694
- }
695
- /**
696
- * Escape HTML special characters
697
- */
698
- function escapeHtml(text) {
699
- return text
700
- .replace(/&/g, '&amp;')
701
- .replace(/</g, '&lt;')
702
- .replace(/>/g, '&gt;')
703
- .replace(/"/g, '&quot;')
704
- .replace(/'/g, '&#039;');
705
- }