fraim-framework 2.0.63 → 2.0.65

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 (106) hide show
  1. package/bin/fraim-mcp.js +52 -19
  2. package/bin/fraim.js +23 -0
  3. package/dist/src/cli/commands/add-ide.js +53 -14
  4. package/dist/src/cli/commands/doctor.js +12 -24
  5. package/dist/src/cli/commands/init-project.js +0 -3
  6. package/dist/src/cli/commands/init.js +0 -2
  7. package/dist/src/cli/commands/mcp.js +65 -0
  8. package/dist/src/cli/commands/setup.js +17 -1
  9. package/dist/src/cli/commands/sync.js +173 -104
  10. package/dist/src/cli/setup/auto-mcp-setup.js +6 -4
  11. package/dist/src/cli/setup/mcp-config-generator.js +65 -41
  12. package/dist/src/fraim/issue-tracking/ado-provider.js +304 -0
  13. package/dist/src/fraim/issue-tracking/factory.js +63 -0
  14. package/dist/src/fraim/issue-tracking/github-provider.js +200 -0
  15. package/dist/src/fraim/issue-tracking/types.js +7 -0
  16. package/dist/src/fraim/issue-tracking-config.js +83 -0
  17. package/dist/src/local-mcp-server/stdio-server.js +91 -15
  18. package/dist/src/utils/remote-sync.js +130 -0
  19. package/package.json +3 -4
  20. package/dist/src/utils/enforcement-utils.js +0 -239
  21. package/dist/src/utils/validate-workflows.js +0 -101
  22. package/registry/scripts/cleanup-branch.ts +0 -341
  23. package/registry/scripts/code-quality-check.sh +0 -566
  24. package/registry/scripts/comprehensive-explorer.py +0 -297
  25. package/registry/scripts/create-git-labels.sh +0 -49
  26. package/registry/scripts/create-website-structure.js +0 -562
  27. package/registry/scripts/detect-tautological-tests.sh +0 -38
  28. package/registry/scripts/evaluate-code-quality.ts +0 -36
  29. package/registry/scripts/exec-with-timeout.ts +0 -122
  30. package/registry/scripts/generate-engagement-emails.ts +0 -830
  31. package/registry/scripts/interactive-explorer.py +0 -270
  32. package/registry/scripts/markdown-to-pdf.js +0 -395
  33. package/registry/scripts/newsletter-helpers.ts +0 -777
  34. package/registry/scripts/pdf-styles.css +0 -172
  35. package/registry/scripts/prep-issue.sh +0 -548
  36. package/registry/scripts/productivity/build-productivity-csv.mjs +0 -242
  37. package/registry/scripts/productivity/fetch-pr-details.mjs +0 -144
  38. package/registry/scripts/productivity/productivity-report.sh +0 -147
  39. package/registry/scripts/profile-server.ts +0 -426
  40. package/registry/scripts/run-thank-you-workflow.ts +0 -122
  41. package/registry/scripts/scrape-site.py +0 -302
  42. package/registry/scripts/send-newsletter-simple.ts +0 -102
  43. package/registry/scripts/send-thank-you-emails.ts +0 -57
  44. package/registry/scripts/validate-openapi-limits.ts +0 -366
  45. package/registry/scripts/validate-test-coverage.ts +0 -280
  46. package/registry/scripts/verify-pr-comments.sh +0 -74
  47. package/registry/scripts/verify-test-coverage.ts +0 -36
  48. package/registry/stubs/workflows/bootstrap/create-architecture.md +0 -11
  49. package/registry/stubs/workflows/bootstrap/detect-broken-windows.md +0 -11
  50. package/registry/stubs/workflows/bootstrap/evaluate-code-quality.md +0 -11
  51. package/registry/stubs/workflows/bootstrap/verify-test-coverage.md +0 -11
  52. package/registry/stubs/workflows/brainstorming/blue-sky-brainstorming.md +0 -11
  53. package/registry/stubs/workflows/brainstorming/codebase-brainstorming.md +0 -11
  54. package/registry/stubs/workflows/business-development/create-business-plan.md +0 -11
  55. package/registry/stubs/workflows/business-development/ideate-business-opportunity.md +0 -11
  56. package/registry/stubs/workflows/business-development/price-product.md +0 -18
  57. package/registry/stubs/workflows/compliance/detect-compliance-requirements.md +0 -11
  58. package/registry/stubs/workflows/compliance/generate-audit-evidence.md +0 -11
  59. package/registry/stubs/workflows/compliance/soc2-evidence-generator.md +0 -11
  60. package/registry/stubs/workflows/customer-development/insight-analysis.md +0 -11
  61. package/registry/stubs/workflows/customer-development/insight-triage.md +0 -11
  62. package/registry/stubs/workflows/customer-development/interview-preparation.md +0 -11
  63. package/registry/stubs/workflows/customer-development/linkedin-outreach.md +0 -11
  64. package/registry/stubs/workflows/customer-development/strategic-brainstorming.md +0 -11
  65. package/registry/stubs/workflows/customer-development/thank-customers.md +0 -11
  66. package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +0 -11
  67. package/registry/stubs/workflows/customer-development/users-to-target.md +0 -11
  68. package/registry/stubs/workflows/customer-development/weekly-newsletter.md +0 -11
  69. package/registry/stubs/workflows/deploy/cloud-deployment.md +0 -11
  70. package/registry/stubs/workflows/improve-fraim/contribute.md +0 -11
  71. package/registry/stubs/workflows/improve-fraim/file-issue.md +0 -11
  72. package/registry/stubs/workflows/learning/build-skillset.md +0 -11
  73. package/registry/stubs/workflows/learning/synthesize-learnings.md +0 -11
  74. package/registry/stubs/workflows/legal/contract-review-analysis.md +0 -11
  75. package/registry/stubs/workflows/legal/nda.md +0 -11
  76. package/registry/stubs/workflows/legal/patent-filing.md +0 -11
  77. package/registry/stubs/workflows/legal/saas-contract-development.md +0 -11
  78. package/registry/stubs/workflows/legal/trademark-filing.md +0 -11
  79. package/registry/stubs/workflows/marketing/content-creation.md +0 -11
  80. package/registry/stubs/workflows/marketing/convert-to-pdf.md +0 -11
  81. package/registry/stubs/workflows/marketing/create-modern-website.md +0 -11
  82. package/registry/stubs/workflows/marketing/domain-registration.md +0 -11
  83. package/registry/stubs/workflows/marketing/hbr-article.md +0 -11
  84. package/registry/stubs/workflows/marketing/launch-checklist.md +0 -11
  85. package/registry/stubs/workflows/marketing/marketing-strategy.md +0 -11
  86. package/registry/stubs/workflows/marketing/storytelling.md +0 -11
  87. package/registry/stubs/workflows/performance/analyze-performance.md +0 -11
  88. package/registry/stubs/workflows/product-building/design.md +0 -11
  89. package/registry/stubs/workflows/product-building/implement.md +0 -11
  90. package/registry/stubs/workflows/product-building/iterate-on-pr-comments.md +0 -11
  91. package/registry/stubs/workflows/product-building/prep-issue.md +0 -11
  92. package/registry/stubs/workflows/product-building/prototype.md +0 -11
  93. package/registry/stubs/workflows/product-building/resolve.md +0 -11
  94. package/registry/stubs/workflows/product-building/retrospect.md +0 -11
  95. package/registry/stubs/workflows/product-building/spec.md +0 -11
  96. package/registry/stubs/workflows/product-building/test.md +0 -11
  97. package/registry/stubs/workflows/productivity-report/productivity-report.md +0 -11
  98. package/registry/stubs/workflows/quality-assurance/browser-validation.md +0 -11
  99. package/registry/stubs/workflows/quality-assurance/iterative-improvement-cycle.md +0 -11
  100. package/registry/stubs/workflows/replicate/replicate-discovery.md +0 -11
  101. package/registry/stubs/workflows/replicate/replicate-to-issues.md +0 -11
  102. package/registry/stubs/workflows/reviewer/review-implementation-vs-design-spec.md +0 -11
  103. package/registry/stubs/workflows/reviewer/review-implementation-vs-feature-spec.md +0 -11
  104. package/registry/stubs/workflows/startup-credits/aws-activate-application.md +0 -11
  105. package/registry/stubs/workflows/startup-credits/google-cloud-application.md +0 -11
  106. package/registry/stubs/workflows/startup-credits/microsoft-azure-application.md +0 -11
@@ -1,302 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Reusable web scraper for any website
4
- Scrapes the entire site and collects HTML, CSS, JS, and structure information
5
-
6
- Usage:
7
- python scrape_site.py <base_url> [--max-pages N] [--output-dir DIR]
8
- """
9
-
10
- import requests
11
- from bs4 import BeautifulSoup
12
- from urllib.parse import urljoin, urlparse
13
- import json
14
- import time
15
- import sys
16
- import argparse
17
- from collections import defaultdict
18
- from pathlib import Path
19
- from datetime import datetime
20
-
21
- class SiteScraper:
22
- def __init__(self, base_url, output_dir="."):
23
- self.base_url = base_url.rstrip('/')
24
- self.output_dir = Path(output_dir)
25
- self.output_dir.mkdir(parents=True, exist_ok=True)
26
-
27
- self.visited_urls = set()
28
- self.pages_data = []
29
- self.css_files = set()
30
- self.js_files = set()
31
- self.images = set()
32
- self.external_links = set()
33
- self.forms = []
34
- self.meta_tags = {}
35
- self.session = requests.Session()
36
- self.session.headers.update({
37
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
38
- })
39
-
40
- def is_valid_url(self, url):
41
- """Check if URL belongs to the same domain"""
42
- parsed = urlparse(url)
43
- base_parsed = urlparse(self.base_url)
44
- return parsed.netloc == base_parsed.netloc or parsed.netloc == ''
45
-
46
- def normalize_url(self, url):
47
- """Normalize URL to avoid duplicates"""
48
- url = urljoin(self.base_url, url)
49
- parsed = urlparse(url)
50
- # Remove fragment
51
- normalized = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
52
- if parsed.query:
53
- normalized += f"?{parsed.query}"
54
- return normalized.rstrip('/')
55
-
56
- def extract_resources(self, soup, page_url):
57
- """Extract CSS, JS, images, and other resources"""
58
- # CSS files
59
- for link in soup.find_all('link', rel='stylesheet'):
60
- href = link.get('href')
61
- if href:
62
- full_url = urljoin(page_url, href)
63
- self.css_files.add(full_url)
64
-
65
- # JavaScript files
66
- for script in soup.find_all('script', src=True):
67
- src = script.get('src')
68
- if src:
69
- full_url = urljoin(page_url, src)
70
- self.js_files.add(full_url)
71
-
72
- # Images
73
- for img in soup.find_all('img', src=True):
74
- src = img.get('src')
75
- if src:
76
- full_url = urljoin(page_url, src)
77
- self.images.add(full_url)
78
-
79
- # External links
80
- for a in soup.find_all('a', href=True):
81
- href = a.get('href')
82
- if href:
83
- parsed = urlparse(href)
84
- if parsed.netloc and parsed.netloc != urlparse(self.base_url).netloc:
85
- self.external_links.add(href)
86
-
87
- def extract_forms(self, soup, page_url):
88
- """Extract form information"""
89
- for form in soup.find_all('form'):
90
- form_data = {
91
- 'action': form.get('action', ''),
92
- 'method': form.get('method', 'GET').upper(),
93
- 'inputs': []
94
- }
95
-
96
- for input_tag in form.find_all(['input', 'textarea', 'select']):
97
- input_data = {
98
- 'type': input_tag.get('type', input_tag.name),
99
- 'name': input_tag.get('name', ''),
100
- 'id': input_tag.get('id', ''),
101
- 'placeholder': input_tag.get('placeholder', ''),
102
- 'required': input_tag.has_attr('required'),
103
- 'label': ''
104
- }
105
-
106
- # Try to find associated label
107
- if input_data['id']:
108
- label = soup.find('label', {'for': input_data['id']})
109
- if label:
110
- input_data['label'] = label.get_text(strip=True)
111
-
112
- form_data['inputs'].append(input_data)
113
-
114
- if form_data['inputs']:
115
- self.forms.append({
116
- 'page': page_url,
117
- 'form': form_data
118
- })
119
-
120
- def extract_meta_info(self, soup, page_url):
121
- """Extract meta tags and page information"""
122
- meta_info = {
123
- 'title': soup.title.string if soup.title else '',
124
- 'description': '',
125
- 'keywords': '',
126
- 'viewport': '',
127
- 'og_tags': {},
128
- 'other_meta': {}
129
- }
130
-
131
- for meta in soup.find_all('meta'):
132
- name = meta.get('name', '').lower()
133
- property_attr = meta.get('property', '').lower()
134
- content = meta.get('content', '')
135
-
136
- if name == 'description':
137
- meta_info['description'] = content
138
- elif name == 'keywords':
139
- meta_info['keywords'] = content
140
- elif name == 'viewport':
141
- meta_info['viewport'] = content
142
- elif property_attr.startswith('og:'):
143
- meta_info['og_tags'][property_attr] = content
144
- elif name:
145
- meta_info['other_meta'][name] = content
146
-
147
- return meta_info
148
-
149
- def scrape_page(self, url):
150
- """Scrape a single page"""
151
- if url in self.visited_urls:
152
- return None
153
-
154
- self.visited_urls.add(url)
155
- print(f" Scraping: {url}")
156
-
157
- try:
158
- response = self.session.get(url, timeout=10)
159
- response.raise_for_status()
160
-
161
- # Check if it's HTML
162
- content_type = response.headers.get('Content-Type', '')
163
- if 'text/html' not in content_type:
164
- return None
165
-
166
- soup = BeautifulSoup(response.text, 'html.parser')
167
-
168
- # Extract page data
169
- page_data = {
170
- 'url': url,
171
- 'status_code': response.status_code,
172
- 'title': soup.title.string if soup.title else '',
173
- 'meta': self.extract_meta_info(soup, url),
174
- 'headings': {
175
- 'h1': [h.get_text(strip=True) for h in soup.find_all('h1')],
176
- 'h2': [h.get_text(strip=True) for h in soup.find_all('h2')],
177
- 'h3': [h.get_text(strip=True) for h in soup.find_all('h3')],
178
- },
179
- 'links': [],
180
- 'text_content': soup.get_text(separator=' ', strip=True)[:5000], # First 5000 chars
181
- 'html_structure': self.analyze_structure(soup),
182
- }
183
-
184
- # Extract resources
185
- self.extract_resources(soup, url)
186
- self.extract_forms(soup, url)
187
-
188
- # Find internal links for further scraping
189
- for link in soup.find_all('a', href=True):
190
- href = link.get('href')
191
- if href:
192
- normalized = self.normalize_url(href)
193
- if self.is_valid_url(normalized) and normalized not in self.visited_urls:
194
- page_data['links'].append(normalized)
195
-
196
- self.pages_data.append(page_data)
197
- return page_data
198
-
199
- except Exception as e:
200
- print(f" āš ļø Error scraping {url}: {e}")
201
- return None
202
-
203
- def analyze_structure(self, soup):
204
- """Analyze HTML structure"""
205
- structure = {
206
- 'main_tags': defaultdict(int),
207
- 'ids': [],
208
- 'classes': [],
209
- 'data_attributes': []
210
- }
211
-
212
- for tag in soup.find_all():
213
- structure['main_tags'][tag.name] += 1
214
-
215
- if tag.get('id'):
216
- structure['ids'].append(tag.get('id'))
217
-
218
- if tag.get('class'):
219
- structure['classes'].extend(tag.get('class'))
220
-
221
- # Data attributes
222
- for attr in tag.attrs:
223
- if attr.startswith('data-'):
224
- structure['data_attributes'].append(attr)
225
-
226
- return {
227
- 'main_tags': dict(structure['main_tags']),
228
- 'unique_ids_count': len(set(structure['ids'])),
229
- 'unique_classes_count': len(set(structure['classes'])),
230
- 'data_attributes': list(set(structure['data_attributes']))
231
- }
232
-
233
- def scrape_site(self, max_pages=50):
234
- """Scrape the entire site starting from base URL"""
235
- queue = [self.base_url]
236
-
237
- print(f"\nšŸš€ Starting to scrape {self.base_url}...")
238
- print(f" Max pages: {max_pages}\n")
239
-
240
- while queue and len(self.visited_urls) < max_pages:
241
- current_url = queue.pop(0)
242
- page_data = self.scrape_page(current_url)
243
-
244
- if page_data:
245
- # Add new links to queue
246
- for link in page_data.get('links', []):
247
- if link not in self.visited_urls and link not in queue:
248
- queue.append(link)
249
-
250
- time.sleep(0.5) # Be polite
251
-
252
- def get_summary(self):
253
- """Generate summary of scraped data"""
254
- return {
255
- 'base_url': self.base_url,
256
- 'scrape_date': datetime.now().isoformat(),
257
- 'total_pages': len(self.pages_data),
258
- 'total_css_files': len(self.css_files),
259
- 'total_js_files': len(self.js_files),
260
- 'total_images': len(self.images),
261
- 'total_forms': len(self.forms),
262
- 'total_external_links': len(self.external_links),
263
- 'pages': self.pages_data,
264
- 'css_files': list(self.css_files),
265
- 'js_files': list(self.js_files),
266
- 'images': list(self.images)[:20], # First 20
267
- 'external_links': list(self.external_links),
268
- 'forms': self.forms
269
- }
270
-
271
- def save_results(self, filename='site_analysis.json'):
272
- """Save results to JSON file"""
273
- summary = self.get_summary()
274
- output_path = self.output_dir / filename
275
-
276
- with open(output_path, 'w', encoding='utf-8') as f:
277
- json.dump(summary, f, indent=2, ensure_ascii=False)
278
-
279
- print(f"\nāœ… Scraping complete!")
280
- print(f" šŸ“„ Pages scraped: {summary['total_pages']}")
281
- print(f" šŸŽØ CSS files: {summary['total_css_files']}")
282
- print(f" šŸ“œ JS files: {summary['total_js_files']}")
283
- print(f" šŸ–¼ļø Images: {summary['total_images']}")
284
- print(f" šŸ“ Forms: {summary['total_forms']}")
285
- print(f" šŸ”— External links: {summary['total_external_links']}")
286
- print(f"\n šŸ’¾ Data saved to: {output_path}")
287
-
288
- def main():
289
- parser = argparse.ArgumentParser(description='Scrape a website and analyze its structure')
290
- parser.add_argument('url', help='Base URL of the website to scrape')
291
- parser.add_argument('--max-pages', type=int, default=50, help='Maximum number of pages to scrape (default: 50)')
292
- parser.add_argument('--output-dir', default='.', help='Output directory for results (default: current directory)')
293
- parser.add_argument('--output-file', default='site_analysis.json', help='Output filename (default: site_analysis.json)')
294
-
295
- args = parser.parse_args()
296
-
297
- scraper = SiteScraper(args.url, args.output_dir)
298
- scraper.scrape_site(max_pages=args.max_pages)
299
- scraper.save_results(args.output_file)
300
-
301
- if __name__ == "__main__":
302
- main()
@@ -1,102 +0,0 @@
1
- #!/usr/bin/env tsx
2
-
3
- /**
4
- * Send Newsletter - Deterministic Script
5
- *
6
- * Simple script that users run after approving newsletter JSON.
7
- * No decisions, just sends the newsletter.
8
- *
9
- * Usage:
10
- * npm run send-newsletter # Send to all executives and potential customers
11
- * npm run send-newsletter -- --showonly # List recipients without sending
12
- * npm run send-newsletter -- --no-potential-customers # Send only to active executives
13
- * npm run send-newsletter -- --only-potential-customers # Send only to potential customers
14
- * npm run send-newsletter -- --email=user@example.com # Send to specific email
15
- * npm run send-newsletter -- exec-XXX exec-YYY # Send to specific executive IDs
16
- */
17
-
18
- import { sendNewsletterToExecutives } from './newsletter-helpers';
19
- import { readdirSync, existsSync } from 'fs';
20
- import { join } from 'path';
21
-
22
- async function main() {
23
- console.log('šŸ“§ Weekly Newsletter Sender\n');
24
-
25
- // Parse command line arguments
26
- const args = process.argv.slice(2);
27
- const showOnly = args.includes('--showonly');
28
- const excludePotentialCustomers = args.includes('--no-potential-customers');
29
- const onlyPotentialCustomers = args.includes('--only-potential-customers');
30
- const filterEmails: string[] = [];
31
- const filterExecIds: string[] = [];
32
-
33
- // Parse email filters (--email=address)
34
- args.forEach(arg => {
35
- if (arg.startsWith('--email=')) {
36
- filterEmails.push(arg.substring(8));
37
- } else if (arg.startsWith('exec-')) {
38
- filterExecIds.push(arg);
39
- }
40
- });
41
-
42
- // Find the most recent newsletter
43
- const newslettersDir = join(process.cwd(), 'docs', 'customer-development', 'newsletters');
44
-
45
- if (!existsSync(newslettersDir)) {
46
- console.error('āŒ Error: No newsletters directory found');
47
- console.error(' Expected: docs/customer-development/newsletters/');
48
- console.error(' Generate a newsletter first using the AI agent workflow');
49
- process.exit(1);
50
- }
51
-
52
- const files = readdirSync(newslettersDir)
53
- .filter(f => f.startsWith('newsletter-') && f.endsWith('.json'))
54
- .sort()
55
- .reverse(); // Most recent first
56
-
57
- if (files.length === 0) {
58
- console.error('āŒ Error: No newsletter files found');
59
- console.error(' Generate a newsletter first using the AI agent workflow');
60
- process.exit(1);
61
- }
62
-
63
- const latestNewsletter = files[0];
64
- const newsletterPath = join(newslettersDir, latestNewsletter);
65
-
66
- console.log(`šŸ“„ Found newsletter: ${latestNewsletter}`);
67
-
68
- if (showOnly) {
69
- console.log(`šŸ“‹ Listing recipients (--showonly mode - no emails will be sent)...\n`);
70
- } else if (filterEmails.length > 0 || filterExecIds.length > 0) {
71
- console.log(`šŸ“§ Sending to filtered recipients...\n`);
72
- console.log(`āš ļø This will send real emails. Make sure the newsletter is approved!`);
73
- } else {
74
- if (onlyPotentialCustomers) {
75
- console.log(`šŸ“§ Sending to potential customers only...\n`);
76
- } else {
77
- console.log(`šŸ“§ Sending to all active executives${excludePotentialCustomers ? '' : ' and potential customers'}...\n`);
78
- }
79
- console.log(`āš ļø This will send real emails. Make sure the newsletter is approved!`);
80
- }
81
-
82
- console.log(` Preview: ${newsletterPath.replace('.json', '.html')}\n`);
83
-
84
- // Determine what to include based on flags
85
- const includePotentialCustomers = onlyPotentialCustomers || !excludePotentialCustomers;
86
- const includeExecutives = !onlyPotentialCustomers;
87
-
88
- // Send newsletter (or show only)
89
- await sendNewsletterToExecutives(
90
- newsletterPath,
91
- filterEmails.length > 0 ? filterEmails : undefined,
92
- filterExecIds.length > 0 ? filterExecIds : undefined,
93
- includePotentialCustomers,
94
- showOnly,
95
- includeExecutives
96
- );
97
- }
98
-
99
- main().catch(error => {
100
- console.error('āŒ Error:', error.message);
101
- process.exit(1);
102
- });
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env tsx
2
-
3
- /**
4
- * Send Thank-You Emails from Candidates JSON File
5
- *
6
- * Usage:
7
- * npm run send-thank-you -- <filename> [exec-id-1] [exec-id-2] ...
8
- *
9
- * Examples:
10
- * npm run send-thank-you -- docs/customer-development/thank-you-notes/thank-you-candidates-2025-10-29.json
11
- * npm run send-thank-you -- docs/customer-development/thank-you-notes/thank-you-candidates-2025-10-29.json exec-123 exec-456
12
- */
13
-
14
- import { sendCustomerMail } from './generate-engagement-emails';
15
-
16
- async function main() {
17
- const args = process.argv.slice(2);
18
-
19
- if (args.length === 0) {
20
- console.error('āŒ Error: Missing required argument: filename');
21
- console.error('');
22
- console.error('Usage: npm run send-thank-you -- <filename> [exec-id-1] [exec-id-2] ...');
23
- console.error('');
24
- console.error('Examples:');
25
- console.error(' npm run send-thank-you -- docs/customer-development/thank-you-notes/thank-you-candidates-2025-10-29.json');
26
- console.error(' npm run send-thank-you -- docs/customer-development/thank-you-notes/thank-you-candidates-2025-10-29.json exec-1760627251980-31qmjy5y4');
27
- process.exit(1);
28
- }
29
-
30
- const filename = args[0];
31
- const execIds = args.slice(1).filter(arg => arg.startsWith('exec-'));
32
-
33
- if (execIds.length === 0) {
34
- // Send to all pending candidates
35
- console.log(`šŸ“§ Sending emails from: ${filename}`);
36
- console.log(`šŸŽÆ Sending to ALL pending candidates`);
37
- await sendCustomerMail(filename);
38
- } else if (execIds.length === 1) {
39
- // Send to specific executive
40
- console.log(`šŸ“§ Sending emails from: ${filename}`);
41
- console.log(`šŸŽÆ Sending to executive: ${execIds[0]}`);
42
- await sendCustomerMail(filename, execIds[0]);
43
- } else {
44
- // Multiple executives - send to each one
45
- console.log(`šŸ“§ Sending emails from: ${filename}`);
46
- console.log(`šŸŽÆ Sending to ${execIds.length} executives: ${execIds.join(', ')}`);
47
- for (const execId of execIds) {
48
- console.log(`\nšŸ“§ Processing executive: ${execId}`);
49
- await sendCustomerMail(filename, execId);
50
- }
51
- }
52
- }
53
-
54
- main().catch(error => {
55
- console.error('āŒ Error:', error);
56
- process.exit(1);
57
- });