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.
- package/bin/fraim-mcp.js +52 -19
- package/bin/fraim.js +23 -0
- package/dist/src/cli/commands/add-ide.js +53 -14
- package/dist/src/cli/commands/doctor.js +12 -24
- package/dist/src/cli/commands/init-project.js +0 -3
- package/dist/src/cli/commands/init.js +0 -2
- package/dist/src/cli/commands/mcp.js +65 -0
- package/dist/src/cli/commands/setup.js +17 -1
- package/dist/src/cli/commands/sync.js +173 -104
- package/dist/src/cli/setup/auto-mcp-setup.js +6 -4
- package/dist/src/cli/setup/mcp-config-generator.js +65 -41
- package/dist/src/fraim/issue-tracking/ado-provider.js +304 -0
- package/dist/src/fraim/issue-tracking/factory.js +63 -0
- package/dist/src/fraim/issue-tracking/github-provider.js +200 -0
- package/dist/src/fraim/issue-tracking/types.js +7 -0
- package/dist/src/fraim/issue-tracking-config.js +83 -0
- package/dist/src/local-mcp-server/stdio-server.js +91 -15
- package/dist/src/utils/remote-sync.js +130 -0
- package/package.json +3 -4
- package/dist/src/utils/enforcement-utils.js +0 -239
- package/dist/src/utils/validate-workflows.js +0 -101
- package/registry/scripts/cleanup-branch.ts +0 -341
- package/registry/scripts/code-quality-check.sh +0 -566
- package/registry/scripts/comprehensive-explorer.py +0 -297
- package/registry/scripts/create-git-labels.sh +0 -49
- package/registry/scripts/create-website-structure.js +0 -562
- package/registry/scripts/detect-tautological-tests.sh +0 -38
- package/registry/scripts/evaluate-code-quality.ts +0 -36
- package/registry/scripts/exec-with-timeout.ts +0 -122
- package/registry/scripts/generate-engagement-emails.ts +0 -830
- package/registry/scripts/interactive-explorer.py +0 -270
- package/registry/scripts/markdown-to-pdf.js +0 -395
- package/registry/scripts/newsletter-helpers.ts +0 -777
- package/registry/scripts/pdf-styles.css +0 -172
- package/registry/scripts/prep-issue.sh +0 -548
- package/registry/scripts/productivity/build-productivity-csv.mjs +0 -242
- package/registry/scripts/productivity/fetch-pr-details.mjs +0 -144
- package/registry/scripts/productivity/productivity-report.sh +0 -147
- package/registry/scripts/profile-server.ts +0 -426
- package/registry/scripts/run-thank-you-workflow.ts +0 -122
- package/registry/scripts/scrape-site.py +0 -302
- package/registry/scripts/send-newsletter-simple.ts +0 -102
- package/registry/scripts/send-thank-you-emails.ts +0 -57
- package/registry/scripts/validate-openapi-limits.ts +0 -366
- package/registry/scripts/validate-test-coverage.ts +0 -280
- package/registry/scripts/verify-pr-comments.sh +0 -74
- package/registry/scripts/verify-test-coverage.ts +0 -36
- package/registry/stubs/workflows/bootstrap/create-architecture.md +0 -11
- package/registry/stubs/workflows/bootstrap/detect-broken-windows.md +0 -11
- package/registry/stubs/workflows/bootstrap/evaluate-code-quality.md +0 -11
- package/registry/stubs/workflows/bootstrap/verify-test-coverage.md +0 -11
- package/registry/stubs/workflows/brainstorming/blue-sky-brainstorming.md +0 -11
- package/registry/stubs/workflows/brainstorming/codebase-brainstorming.md +0 -11
- package/registry/stubs/workflows/business-development/create-business-plan.md +0 -11
- package/registry/stubs/workflows/business-development/ideate-business-opportunity.md +0 -11
- package/registry/stubs/workflows/business-development/price-product.md +0 -18
- package/registry/stubs/workflows/compliance/detect-compliance-requirements.md +0 -11
- package/registry/stubs/workflows/compliance/generate-audit-evidence.md +0 -11
- package/registry/stubs/workflows/compliance/soc2-evidence-generator.md +0 -11
- package/registry/stubs/workflows/customer-development/insight-analysis.md +0 -11
- package/registry/stubs/workflows/customer-development/insight-triage.md +0 -11
- package/registry/stubs/workflows/customer-development/interview-preparation.md +0 -11
- package/registry/stubs/workflows/customer-development/linkedin-outreach.md +0 -11
- package/registry/stubs/workflows/customer-development/strategic-brainstorming.md +0 -11
- package/registry/stubs/workflows/customer-development/thank-customers.md +0 -11
- package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +0 -11
- package/registry/stubs/workflows/customer-development/users-to-target.md +0 -11
- package/registry/stubs/workflows/customer-development/weekly-newsletter.md +0 -11
- package/registry/stubs/workflows/deploy/cloud-deployment.md +0 -11
- package/registry/stubs/workflows/improve-fraim/contribute.md +0 -11
- package/registry/stubs/workflows/improve-fraim/file-issue.md +0 -11
- package/registry/stubs/workflows/learning/build-skillset.md +0 -11
- package/registry/stubs/workflows/learning/synthesize-learnings.md +0 -11
- package/registry/stubs/workflows/legal/contract-review-analysis.md +0 -11
- package/registry/stubs/workflows/legal/nda.md +0 -11
- package/registry/stubs/workflows/legal/patent-filing.md +0 -11
- package/registry/stubs/workflows/legal/saas-contract-development.md +0 -11
- package/registry/stubs/workflows/legal/trademark-filing.md +0 -11
- package/registry/stubs/workflows/marketing/content-creation.md +0 -11
- package/registry/stubs/workflows/marketing/convert-to-pdf.md +0 -11
- package/registry/stubs/workflows/marketing/create-modern-website.md +0 -11
- package/registry/stubs/workflows/marketing/domain-registration.md +0 -11
- package/registry/stubs/workflows/marketing/hbr-article.md +0 -11
- package/registry/stubs/workflows/marketing/launch-checklist.md +0 -11
- package/registry/stubs/workflows/marketing/marketing-strategy.md +0 -11
- package/registry/stubs/workflows/marketing/storytelling.md +0 -11
- package/registry/stubs/workflows/performance/analyze-performance.md +0 -11
- package/registry/stubs/workflows/product-building/design.md +0 -11
- package/registry/stubs/workflows/product-building/implement.md +0 -11
- package/registry/stubs/workflows/product-building/iterate-on-pr-comments.md +0 -11
- package/registry/stubs/workflows/product-building/prep-issue.md +0 -11
- package/registry/stubs/workflows/product-building/prototype.md +0 -11
- package/registry/stubs/workflows/product-building/resolve.md +0 -11
- package/registry/stubs/workflows/product-building/retrospect.md +0 -11
- package/registry/stubs/workflows/product-building/spec.md +0 -11
- package/registry/stubs/workflows/product-building/test.md +0 -11
- package/registry/stubs/workflows/productivity-report/productivity-report.md +0 -11
- package/registry/stubs/workflows/quality-assurance/browser-validation.md +0 -11
- package/registry/stubs/workflows/quality-assurance/iterative-improvement-cycle.md +0 -11
- package/registry/stubs/workflows/replicate/replicate-discovery.md +0 -11
- package/registry/stubs/workflows/replicate/replicate-to-issues.md +0 -11
- package/registry/stubs/workflows/reviewer/review-implementation-vs-design-spec.md +0 -11
- package/registry/stubs/workflows/reviewer/review-implementation-vs-feature-spec.md +0 -11
- package/registry/stubs/workflows/startup-credits/aws-activate-application.md +0 -11
- package/registry/stubs/workflows/startup-credits/google-cloud-application.md +0 -11
- 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
|
-
});
|