Function bodies 406 total
audit function · javascript · L29-L313 (285 LOC)audit-lemlist-campaign.js
async function audit() {
console.log('\n╔════════════════════════════════════════════════════════════════════╗');
console.log('║ LEMLIST CAMPAIGN AUDIT ║');
console.log('╚════════════════════════════════════════════════════════════════════╝\n');
console.log(`Campaign: ${CAMPAIGN_ID}`);
console.log(`Mode: ${VERIFY_MODE ? 'AUDIT + VERIFY + REMOVE INVALID' : 'AUDIT ONLY (read-only)'}\n`);
// Step 1: Pull all leads
console.log('Fetching leads from Lemlist...\n');
let leads;
try {
leads = await getLeadsFromCampaign(CAMPAIGN_ID);
} catch (err) {
console.error(`Failed to fetch leads: ${err.message}`);
process.exit(1);
}
if (!leads || leads.length === 0) {
console.log('No leads found in campaign.\n');
process.exit(0);
}
console.log(`Found ${leads.length} leads\n`);
// Step 2: Audit each lead
const issues = []; // { lead, problems: string[] }
const byCategory = {};
const byNameQuality = { pct function · javascript · L315-L318 (4 LOC)audit-lemlist-campaign.js
function pct(n, total) {
if (total === 0) return '0%';
return `${Math.round(n / total * 100)}%`;
}makeRequest function · javascript · L11-L45 (35 LOC)check-lemlist-status.js
function makeRequest(path) {
return new Promise((resolve, reject) => {
const apiKey = getCredential('lemlist', 'apiKey');
const auth = Buffer.from(`:${apiKey}`).toString('base64');
const options = {
hostname: 'api.lemlist.com',
path: path,
method: 'GET',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data));
} catch (e) {
resolve(data);
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', reject);
req.end();
});
}checkLemlistStatus function · javascript · L47-L187 (141 LOC)check-lemlist-status.js
async function checkLemlistStatus() {
console.log('\n╔════════════════════════════════════════════════════════════════════╗');
console.log('║ LEMLIST CAMPAIGN STATUS CHECK ║');
console.log('╚════════════════════════════════════════════════════════════════════╝\n');
console.log(`Campaign ID: ${CAMPAIGN_ID}\n`);
try {
// Get all leads
console.log('Fetching leads from Lemlist...\n');
const leads = await makeRequest(`/api/campaigns/${CAMPAIGN_ID}/leads`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`TOTAL LEADS: ${leads.length}`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
// Analyze leads
let verified = 0;
let notVerified = 0;
let contacted = 0;
let notContacted = 0;
let replied = 0;
let bounced = 0;
const emailDomains = {};
const companyNames = [];
const missingData = {
noPhonisBadName function · javascript · L59-L85 (27 LOC)cleanup-bad-leads.js
function isBadName(firstName, lastName) {
const first = (firstName || '').toLowerCase().trim();
const last = (lastName || '').toLowerCase().trim();
// Already fallback or empty - skip
if (first === 'there' || first === '') return false;
// Check bad first names
if (BAD_FIRST_NAMES.has(first)) return true;
// Check bad last names
if (BAD_LAST_NAMES.has(last)) return true;
// "Su" as last name with any first name
if (last === 'su') return true;
// Last name is 2 chars or less and not a known short surname
if (last.length > 0 && last.length <= 2 && !isCommonShortSurname(last)) return true;
// First name looks like a company/business word
if (isBusinessWord(first)) return true;
// Last name looks like a business descriptor
if (isBusinessWord(last)) return true;
return false;
}isCommonShortSurname function · javascript · L87-L93 (7 LOC)cleanup-bad-leads.js
function isCommonShortSurname(name) {
const realShortSurnames = new Set([
'li', 'wu', 'xu', 'ye', 'ma', 'he', 'hu', 'lu', 'ng',
'ho', 'lo', 'ko', 'do', 'le', 'ly', 'qi', 'yu', 'ai'
]);
return realShortSurnames.has(name.toLowerCase());
}isBusinessWord function · javascript · L95-L108 (14 LOC)cleanup-bad-leads.js
function isBusinessWord(word) {
const businessWords = new Set([
'accountancy', 'accounting', 'insurance', 'structural', 'consulting',
'commercial', 'community', 'engineering', 'recruitment', 'chartered',
'management', 'certified', 'protection', 'employment', 'elimination',
'professional', 'construction', 'architectural', 'financial',
'digital', 'response', 'approaches', 'solutions', 'services',
'associates', 'partnership', 'enterprises', 'holdings', 'group',
'limited', 'ltd', 'plc', 'inc', 'corp',
'diet', 'files', 'operators', 'businesses', 'survey', 'socials',
'recurring', 'mixed', 'cutter', 'attach',
]);
return businessWords.has(word.toLowerCase());
}Want this analysis on your repo? https://repobility.com/scan/
isBadEmail function · javascript · L110-L124 (15 LOC)cleanup-bad-leads.js
function isBadEmail(email) {
if (!email) return true;
// Check bad domains
for (const domain of BAD_EMAIL_DOMAINS) {
if (email.endsWith('@' + domain)) return true;
}
// Check bad patterns (hex IDs)
for (const pattern of BAD_EMAIL_PATTERNS) {
if (pattern.test(email)) return true;
}
return false;
}makeRequest function · javascript · L130-L171 (42 LOC)cleanup-bad-leads.js
function makeRequest(path, method, body) {
return new Promise((resolve, reject) => {
const apiKey = getCredential('lemlist', 'apiKey');
const auth = Buffer.from(`:${apiKey}`).toString('base64');
const options = {
hostname: 'api.lemlist.com',
path: path,
method: method || 'GET',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
}
};
if (body) {
const postData = JSON.stringify(body);
options.headers['Content-Length'] = Buffer.byteLength(postData);
}
const req = https.request(options, (res) => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
const data = Buffer.concat(chunks).toString('utf8');
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(data ? JSON.parse(data) : { success: true });
} catch (e) {
resolve(data || { success: true });
updateLeadInLemlist function · javascript · L173-L179 (7 LOC)cleanup-bad-leads.js
async function updateLeadInLemlist(email, updates) {
return makeRequest(
`/api/campaigns/${CAMPAIGN_ID}/leads/${encodeURIComponent(email)}`,
'PATCH',
updates
);
}deleteLeadFromLemlist function · javascript · L181-L186 (6 LOC)cleanup-bad-leads.js
async function deleteLeadFromLemlist(email) {
return makeRequest(
`/api/campaigns/${CAMPAIGN_ID}/leads/${encodeURIComponent(email)}`,
'DELETE'
);
}cleanup function · javascript · L192-L373 (182 LOC)cleanup-bad-leads.js
async function cleanup() {
console.log('\n╔════════════════════════════════════════════════════════════════════╗');
console.log('║ LEAD CLEANUP - Fix Bad Names & Emails ║');
console.log('╚════════════════════════════════════════════════════════════════════╝\n');
// Step 1: Get all leads from DATABASE (Lemlist GET leads API returns empty)
console.log('Reading leads from database...\n');
const Database = require('better-sqlite3');
const dbPath = './ksd/local-outreach/orchestrator/data/businesses.db';
const db = new Database(dbPath);
const leads = db.prepare(`
SELECT id, name, owner_first_name, owner_last_name, owner_email,
category, postcode, email_verified
FROM businesses
WHERE owner_email IS NOT NULL AND length(owner_email) > 0
ORDER BY owner_first_name
`).all();
console.log(`Found ${leads.length} leads with emails in database\n`);
// Step 2: Categorize issues
const badNames = [];
const badEmails = [compareScraper function · javascript · L9-L126 (118 LOC)compare-scrapers.js
async function compareScraper() {
console.log('\n' + '='.repeat(80));
console.log('SCRAPER COMPARISON: HasData vs Outscraper');
console.log('='.repeat(80) + '\n');
// Test params
const location = 'Bramhall';
const postcode = 'SK7';
const businessTypes = ['dentists']; // Small test set
const limit = 5; // Test with just 5 businesses
console.log('Test Parameters:');
console.log(` Location: ${location} (${postcode})`);
console.log(` Business Types: ${businessTypes.join(', ')}`);
console.log(` Limit: ${limit} businesses`);
console.log('\n' + '-'.repeat(80) + '\n');
// Test 1: HasData
console.log('🔵 TEST 1: HasData Scraper\n');
let hasdataResults = [];
let hasdataError = null;
let hasdataTime = 0;
try {
const startTime = Date.now();
hasdataResults = await hasdataScraper.scrapeGoogleMaps(location, postcode, businessTypes, true);
hasdataTime = Date.now() - startTime;
// Limit to 5 for comparison
hasdataResults = hasdataResults.analyzeResults function · javascript · L128-L153 (26 LOC)compare-scrapers.js
function analyzeResults(results, scraperName) {
const total = results.length;
const withPhone = results.filter(b => b.phone).length;
const withWebsite = results.filter(b => b.website).length;
const withEmail = results.filter(b => b.email).length;
const withEmailsFromWebsite = results.filter(b => b.emailsFromWebsite && b.emailsFromWebsite.length > 0).length;
const withRating = results.filter(b => b.rating).length;
const avgRating = results.filter(b => b.rating).reduce((sum, b) => sum + b.rating, 0) / withRating || 0;
const avgReviews = results.reduce((sum, b) => sum + (b.reviewCount || 0), 0) / total || 0;
return {
scraper: scraperName,
total,
withPhone,
withWebsite,
withEmail,
withEmailsFromWebsite,
withRating,
avgRating,
avgReviews,
phoneRate: Math.round((withPhone / total) * 100),
websiteRate: Math.round((withWebsite / total) * 100),
emailRate: Math.round((withEmail / total) * 100),
emailsFromWebsiteRate: Math.rouprintStats function · javascript · L155-L191 (37 LOC)compare-scrapers.js
function printStats(hasdata, outscraper) {
const fields = [
{ label: 'Businesses Found', hasdataVal: hasdata.total, outscraperVal: outscraper.total },
{ label: 'With Phone', hasdataVal: `${hasdata.withPhone} (${hasdata.phoneRate}%)`, outscraperVal: `${outscraper.withPhone} (${outscraper.phoneRate}%)` },
{ label: 'With Website', hasdataVal: `${hasdata.withWebsite} (${hasdata.websiteRate}%)`, outscraperVal: `${outscraper.withWebsite} (${outscraper.websiteRate}%)` },
{ label: 'With Email (GMB)', hasdataVal: `${hasdata.withEmail} (${hasdata.emailRate}%)`, outscraperVal: `${outscraper.withEmail} (${outscraper.emailRate}%)` },
{ label: 'With Website Emails', hasdataVal: `${hasdata.withEmailsFromWebsite} (${hasdata.emailsFromWebsiteRate}%)`, outscraperVal: `${outscraper.withEmailsFromWebsite} (${outscraper.emailsFromWebsiteRate}%)` },
{ label: 'Avg Rating', hasdataVal: hasdata.avgRating.toFixed(1), outscraperVal: outscraper.avgRating.toFixed(1) },
{ label: 'Avg RRepobility — same analyzer, your code, free for public repos · /scan/
deleteLead function · javascript · L12-L52 (41 LOC)delete-empty-leads.js
async function deleteLead(campaignId, email) {
const apiKey = getCredential("lemlist", "apiKey");
const authString = Buffer.from(":" + apiKey).toString("base64");
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.lemlist.com',
path: `/api/campaigns/${campaignId}/leads/${encodeURIComponent(email)}`,
method: "DELETE",
headers: {
"Authorization": `Basic ${authString}`,
"Accept": "application/json"
}
};
const req = https.request(options, (res) => {
const chunks = [];
let totalLength = 0;
res.on("data", (chunk) => {
chunks.push(chunk);
totalLength += chunk.length;
});
res.on("end", () => {
const data = Buffer.concat(chunks, totalLength).toString('utf8');
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ success: true, email });
} else {
reject(new Error(`Failed to delete ${email}: ${data}`));
cleanupEmptyLeads function · javascript · L54-L127 (74 LOC)delete-empty-leads.js
async function cleanupEmptyLeads() {
console.log('\n╔════════════════════════════════════════════════════════════════════╗');
console.log('║ CLEANUP: Delete Empty Leads from Lemlist ║');
console.log('╚════════════════════════════════════════════════════════════════════╝\n');
try {
console.log('📥 Fetching all leads from campaign...\n');
const leads = await getLeadsFromCampaign(CAMPAIGN_ID);
console.log(`Found ${leads.length} total leads\n`);
// Find empty leads (no firstName or no email)
const emptyLeads = leads.filter(lead =>
!lead.firstName ||
!lead.email ||
lead.firstName === 'undefined' ||
lead.email.includes('undefined')
);
console.log(`🗑️ Found ${emptyLeads.length} empty leads to delete:\n`);
if (emptyLeads.length === 0) {
console.log('✅ No empty leads found! Campaign is clean.\n');
return;
}
for (const lead of emptyLeads) {
console.log(` - ${lead.email || 'demo function · javascript · L8-L101 (94 LOC)demo-email-extraction.js
async function demo() {
console.log('\n╔════════════════════════════════════════════════════════════════════╗');
console.log('║ DEMO: EMAIL EXTRACTION FROM WEBSITE ║');
console.log('╚════════════════════════════════════════════════════════════════════╝\n');
const testWebsite = 'https://www.arundeldentalpractice.co.uk';
console.log(`🔍 Scraping: ${testWebsite}\n`);
console.log('Looking for:');
console.log(' - Email addresses on the website');
console.log(' - Owner/staff names');
console.log(' - Matching emails to people\n');
try {
const websiteData = await scrapeWebsite(testWebsite);
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📧 RAW EMAILS FOUND ON WEBSITE');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
if (websiteData.emails && websiteData.emails.length > 0) {
console.log(`✅ Found ${websiteData.emails.length} emawithTimeout function · javascript · L27-L34 (8 LOC)enrich-campaign.js
function withTimeout(promise, ms, label) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`TIMEOUT after ${ms / 1000}s: ${label}`)), ms)
),
]);
}main function · javascript · L54-L292 (239 LOC)enrich-campaign.js
async function main() {
console.log(`\n=== Enrichment: ${CAMPAIGN} ===\n`);
initDatabase();
// Load businesses for this campaign
const allBusinesses = loadBusinesses({ campaign: CAMPAIGN });
console.log(`Total in campaign: ${allBusinesses.length}`);
// Filter to those with websites and not yet enriched
let toEnrich = allBusinesses.filter(b => {
const biz = b.business || {};
const hasWebsite = biz.website &&
!biz.website.includes('facebook.com') &&
!biz.website.includes('instagram.com') &&
!biz.website.includes('twitter.com');
// Skip already enriched (has email or has been through LLM)
const alreadyEnriched = biz.ownerFirstName || biz.ownerEmail || biz.email;
return hasWebsite && !alreadyEnriched;
});
if (LIMIT) toEnrich = toEnrich.slice(0, LIMIT);
console.log(`With website (not yet enriched): ${toEnrich.length}`);
if (LIMIT) console.log(`Limited to: ${LIMIT}`);
if (DRY_RUN) {
console.log('\nDRY RUN — would enrich:searchOutscraper function · javascript · L133-L184 (52 LOC)explore-football-clubs.js
async function searchOutscraper(query, location) {
const fullQuery = `${query} ${location.name.toLowerCase()}, ${location.postcode.toLowerCase()}`;
const url = `https://api.outscraper.com/maps/search-v3?query=${encodeURIComponent(fullQuery)}&limit=500`;
console.log(` Submitting: "${fullQuery}"`);
const submitRes = await fetch(url, {
headers: { 'X-API-KEY': API_KEY }
});
if (!submitRes.ok) {
const text = await submitRes.text();
throw new Error(`Submit failed (${submitRes.status}): ${text}`);
}
const submitData = await submitRes.json();
const jobId = submitData.id;
console.log(` Job ID: ${jobId} — polling...`);
// Poll for results
let delay = 2000;
const maxAttempts = 30;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await new Promise(r => setTimeout(r, delay));
const pollRes = await fetch(`https://api.outscraper.cloud/requests/${jobId}`, {
headers: { 'X-API-KEY': API_KEY }
});
if (!pollRes.ok) {
comain function · javascript · L186-L382 (197 LOC)explore-football-clubs.js
async function main() {
console.log(`\n=== Football Clubs/Academies — ${CAMPAIGN} (${AREA_ARG}) ===\n`);
console.log(`Area: ${AREA_ARG} — ${LOCATIONS.length} locations`);
const totalQueries = SEARCH_TERMS.length * LOCATIONS.length;
if (DRY_RUN) {
console.log('DRY RUN — no API calls will be made\n');
for (const loc of LOCATIONS) {
console.log(`Location: ${loc.name} (${loc.postcode})`);
for (const term of SEARCH_TERMS) {
console.log(` - "${term} ${loc.name.toLowerCase()}, ${loc.postcode.toLowerCase()}"`);
}
console.log('');
}
console.log(`Total queries: ${totalQueries}`);
console.log(`Estimated cost: ~$${(totalQueries * 30 * 0.002).toFixed(2)} (assuming ~30 results each)`);
console.log(`Campaign tag: ${CAMPAIGN}`);
console.log('\nRun without --dry-run to execute.\n');
return;
}
// Initialize DB
initDatabase();
const allResults = new Map(); // placeId -> business (dedup across locations + terms)
const pisJunkEmail function · javascript · L91-L94 (4 LOC)export-campaign.js
function isJunkEmail(email) {
if (!email) return false;
return JUNK_EMAIL_PATTERNS.some(p => p.test(email));
}Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
isJunkContactName function · javascript · L96-L100 (5 LOC)export-campaign.js
function isJunkContactName(firstName, lastName) {
const fullName = [firstName, lastName].filter(Boolean).join(' ').trim();
if (!fullName) return false;
return JUNK_NAME_PATTERNS.some(p => p.test(fullName));
}isNonFootballBusiness function · javascript · L102-L113 (12 LOC)export-campaign.js
function isNonFootballBusiness(name, category) {
const lowerName = (name || '').toLowerCase();
const lowerCat = (category || '').toLowerCase();
// Check name keywords
if (NON_FOOTBALL_NAME_KEYWORDS.some(kw => lowerName.includes(kw))) return true;
// Check category
if (NON_FOOTBALL_CATEGORIES.some(kw => lowerCat.includes(kw))) return true;
return false;
}cleanBusiness function · javascript · L115-L134 (20 LOC)export-campaign.js
function cleanBusiness(b) {
const biz = b.business || {};
const email = biz.ownerEmail || biz.email || '';
const name = biz.name || biz.businessName || '';
const category = biz.category || '';
// Check for junk email
if (isJunkEmail(email)) return null;
// Check for non-football business
if (isNonFootballBusiness(name, category)) return null;
// Clean garbage contact names (blank them out rather than reject the business)
if (isJunkContactName(biz.ownerFirstName, biz.ownerLastName)) {
biz.ownerFirstName = '';
biz.ownerLastName = '';
}
return b;
}main function · javascript · L153-L251 (99 LOC)export-campaign.js
async function main() {
initDatabase();
// List mode — show all campaigns with counts
if (LIST_MODE) {
const campaigns = listCampaigns();
console.log('\n=== Campaigns in Database ===\n');
if (campaigns.length === 0) {
console.log(' No campaigns found.');
} else {
for (const campaign of campaigns) {
const stats = getBusinessStats({ campaign });
console.log(` ${campaign}`);
console.log(` Total: ${stats.total} | Email: ${stats.withEmail} | Exported: ${stats.exported}`);
}
}
console.log('');
closeDatabase();
return;
}
if (!CAMPAIGN) {
console.error('ERROR: --campaign=<name> is required. Use --list to see available campaigns.');
process.exit(1);
}
// Load businesses for this campaign
const filters = { campaign: CAMPAIGN };
if (HAS_EMAIL) filters.hasEmail = true;
const allBusinesses = loadBusinesses(filters);
// Apply additional filters
let businesses = allBusinesses;
if (HAS_exportCSV function · javascript · L253-L301 (49 LOC)export-campaign.js
function exportCSV(businesses) {
const headers = [
'name', 'category', 'address', 'postcode', 'phone', 'website',
'email', 'email_source', 'email_verified',
'contact_first_name', 'contact_last_name',
'rating', 'reviews',
'revenue_band', 'tier',
'campaigns'
];
const rows = businesses.map(b => {
const biz = b.business || {};
return [
escapeCsv(biz.name || biz.businessName || ''),
escapeCsv(biz.category || ''),
escapeCsv(biz.address || b.postcode || ''),
escapeCsv(biz.postcode || b.postcode || ''),
escapeCsv(biz.phone || ''),
escapeCsv(biz.website || ''),
escapeCsv(biz.ownerEmail || biz.email || ''),
escapeCsv(biz.emailSource || ''),
biz.emailVerified ? 'yes' : 'no',
escapeCsv(biz.ownerFirstName || ''),
escapeCsv(biz.ownerLastName || ''),
biz.rating || '',
biz.reviewCount || '',
escapeCsv(biz.revenueBand || ''),
biz.assignedOfferTier || '',
escapeCsv(ArrayexportMailead function · javascript · L303-L338 (36 LOC)export-campaign.js
function exportMailead(businesses) {
// Mailead uses simple CSV: email, first_name, last_name, company_name, custom fields
const headers = [
'email', 'first_name', 'last_name', 'company_name', 'phone', 'website', 'category', 'postcode'
];
const rows = businesses.map(b => {
const biz = b.business || {};
return [
escapeCsv(biz.ownerEmail || biz.email || ''),
escapeCsv(biz.ownerFirstName || ''),
escapeCsv(biz.ownerLastName || ''),
escapeCsv(biz.name || biz.businessName || ''),
escapeCsv(biz.phone || ''),
escapeCsv(biz.website || ''),
escapeCsv(biz.category || ''),
escapeCsv(biz.postcode || b.postcode || ''),
];
});
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const timestamp = new Date().toISOString().slice(0, 10);
const filename = `${CAMPAIGN}-mailead-${timestamp}.csv`;
const outputPath = path.join(__dirname, 'exports', filename);
fs.mkdirSync(path.dirname(outputPath), { rescapeCsv function · javascript · L340-L347 (8 LOC)export-campaign.js
function escapeCsv(val) {
if (val === null || val === undefined) return '';
const str = String(val);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
}exportLemlist function · javascript · L349-L389 (41 LOC)export-campaign.js
async function exportLemlist(businesses) {
// Dynamic import of lemlist exporter
const { addLeadToCampaign } = require('./shared/outreach-core/export-managers/lemlist-exporter');
let exported = 0;
let skipped = 0;
let errors = 0;
for (const b of businesses) {
const biz = b.business || {};
const email = biz.ownerEmail || biz.email;
if (!email) { skipped++; continue; }
try {
const lead = {
email,
firstName: biz.ownerFirstName || '',
lastName: biz.ownerLastName || '',
companyName: biz.name || biz.businessName || '',
phone: biz.phone || '',
};
// Add merge variables if they exist
if (biz.mergeVariables) {
Object.assign(lead, biz.mergeVariables);
}
await addLeadToCampaign(LEMLIST_CAMPAIGN_ID, lead);
exported++;
} catch (err) {
if (err.message?.includes('DUPLICATE')) {
skipped++;
} else {
errors++;
console.error(` Error exporting Repobility · severity-and-effort ranking · https://repobility.com
parseCSV function · javascript · L63-L107 (45 LOC)import-pressranger.js
function parseCSV(text) {
const lines = text.split('\n').filter(l => l.trim());
if (lines.length < 2) return { headers: [], rows: [] };
const parseLine = (line) => {
const fields = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
current += '"';
i++; // skip escaped quote
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
fields.push(current.trim());
current = '';
} else {
current += char;
}
}
fields.push(current.trim());
return fields;
};
const headers = parseLine(lines[0]).map(h => h.replace(/^"(.*)"$/, '$1').trim());
const rows = [];
for (let i = 1; i < lines.length; i++) {
const values = parseLine(lines[i]);
if (values.length === 0 || (values.length === 1 && !values[0])) continue;
cmapColumns function · javascript · L145-L160 (16 LOC)import-pressranger.js
function mapColumns(headers) {
const mapping = {};
const lowerHeaders = headers.map(h => h.toLowerCase().trim());
for (const [field, variants] of Object.entries(COLUMN_MAP)) {
for (const variant of variants) {
const idx = lowerHeaders.indexOf(variant);
if (idx !== -1) {
mapping[field] = headers[idx]; // Use original header case
break;
}
}
}
return mapping;
}isValidEmail function · javascript · L164-L173 (10 LOC)import-pressranger.js
function isValidEmail(email) {
if (!email || typeof email !== 'string') return false;
const cleaned = email.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(cleaned)) return false;
// Reject obvious junk
if (cleaned.includes('example.com') || cleaned.includes('test.com')) return false;
if (cleaned.includes('sentry') || cleaned.includes('wixpress')) return false;
if (/^[a-f0-9]{20,}@/.test(cleaned)) return false; // Tracking hashes
return true;
}main function · javascript · L177-L462 (286 LOC)import-pressranger.js
async function main() {
console.log(`\n=== PressRanger Import: ${CAMPAIGN} ===\n`);
console.log(`File: ${FILE}`);
console.log(`Type: ${CONTACT_TYPE}`);
console.log(`Verify emails: ${VERIFY ? 'Yes (Reoon)' : 'No'}`);
console.log(`Mode: ${DRY_RUN ? 'DRY RUN' : 'LIVE'}`);
// Read CSV
const filePath = path.resolve(FILE);
if (!fs.existsSync(filePath)) {
console.error(`\nERROR: File not found: ${filePath}`);
process.exit(1);
}
const csvText = fs.readFileSync(filePath, 'utf8');
const { headers, rows } = parseCSV(csvText);
console.log(`\nCSV loaded: ${rows.length} rows, ${headers.length} columns`);
console.log(`Headers: ${headers.join(', ')}`);
// Auto-detect column mapping
const colMap = mapColumns(headers);
console.log('\nColumn mapping detected:');
for (const [field, header] of Object.entries(colMap)) {
console.log(` ${field.padEnd(20)} → "${header}"`);
}
// Check for unmapped headers (show what we're ignoring)
const mappedHeaders = getDomain function · javascript · L62-L68 (7 LOC)improve-emails.js
function getDomain(url) {
try {
return new URL(url.startsWith('http') ? url : `https://${url}`).hostname.replace(/^www\./, '');
} catch {
return null;
}
}isGenericEmail function · javascript · L73-L80 (8 LOC)improve-emails.js
function isGenericEmail(email) {
if (!email) return true;
const prefix = email.split('@')[0].toLowerCase();
const genericPrefixes = ['info', 'hello', 'contact', 'enquiries', 'enquiry', 'admin',
'office', 'sales', 'mail', 'support', 'team', 'help', 'service', 'bookings',
'booking', 'appointments', 'reception', 'general'];
return genericPrefixes.some(g => prefix === g || prefix.startsWith(g + '.'));
}runLlmExtraction function · javascript · L85-L197 (113 LOC)improve-emails.js
async function runLlmExtraction(db, businesses) {
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('STAGE 1: LLM EMAIL EXTRACTION (from website text)');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
// Target: businesses with no email OR only generic email, that have a website
const targets = businesses.filter(b =>
b.website && (!b.owner_email || isGenericEmail(b.owner_email)) &&
!b.website.includes('facebook.com') && !b.website.includes('instagram.com')
);
console.log(` Targets: ${targets.length} businesses (no email or generic email)\n`);
if (DRY_RUN) return;
for (const biz of targets) {
const label = biz.name.substring(0, 40).padEnd(40);
try {
const text = await fetchWebsiteText(biz.website);
if (!text) {
console.log(` NOFETCH ${label}`);
continue;
}
const result = await llmExtractOwners(biz.name, text);
STATS.llm.processed++;
runPatternGuessing function · javascript · L202-L301 (100 LOC)improve-emails.js
async function runPatternGuessing(db, businesses) {
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('STAGE 2: PATTERN GUESSING + REOON VERIFICATION');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
// Re-read businesses to get updated state after LLM stage
const fresh = db.prepare(`
SELECT * FROM businesses
WHERE owner_first_name IS NOT NULL AND length(owner_first_name) > 0
AND owner_first_name != 'there'
AND website IS NOT NULL AND length(website) > 0
AND (owner_email IS NULL OR length(owner_email) = 0)
ORDER BY name
`).all();
const reoonRemaining = getReoonQuota();
console.log(` Targets: ${fresh.length} businesses (have name + website, no email)`);
console.log(` Reoon credits remaining: ${reoonRemaining}`);
// Each business needs up to 7 pattern checks
const maxBusinesses = Math.floor(reoonRemaining / 3); // Budget ~3 patterns per business (stop on first valWant this analysis on your repo? https://repobility.com/scan/
runIcypeasFinder function · javascript · L306-L379 (74 LOC)improve-emails.js
async function runIcypeasFinder(db, businesses) {
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('STAGE 3: ICYPEAS EMAIL FINDER');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
// Re-read: businesses with name + website but still no email after stages 1-2
const fresh = db.prepare(`
SELECT * FROM businesses
WHERE owner_first_name IS NOT NULL AND length(owner_first_name) > 0
AND owner_first_name != 'there'
AND owner_last_name IS NOT NULL AND length(owner_last_name) > 0
AND website IS NOT NULL AND length(website) > 0
AND (owner_email IS NULL OR length(owner_email) = 0)
ORDER BY name
`).all();
console.log(` Targets: ${fresh.length} businesses (have full name + website, still no email)`);
console.log(` Icypeas limit: 100/day\n`);
const toProcess = fresh.slice(0, 100); // Respect daily limit
if (DRY_RUN) {
for (const biz of toProcess.slice(0, 10)) {
main function · javascript · L381-L437 (57 LOC)improve-emails.js
async function main() {
console.log('\n╔════════════════════════════════════════════════════════════════════╗');
console.log('║ EMAIL IMPROVEMENT PIPELINE ║');
console.log('╚════════════════════════════════════════════════════════════════════╝');
const db = new Database(DB_PATH);
// Get initial state
let query = `SELECT * FROM businesses WHERE website IS NOT NULL AND length(website) > 0 ORDER BY name`;
if (LIMIT) query = query.replace('ORDER BY', `ORDER BY`) + ` LIMIT ${LIMIT}`;
const businesses = db.prepare(query).all();
const withEmail = businesses.filter(b => b.owner_email && b.owner_email.length > 0);
const generic = withEmail.filter(b => isGenericEmail(b.owner_email));
const personal = withEmail.filter(b => !isGenericEmail(b.owner_email));
const noEmail = businesses.filter(b => !b.owner_email || b.owner_email.length === 0);
console.log(`\n BEFORE: ${businesses.length} businesses with websites`);
console.log(` processBusinesses function · javascript · L473-L577 (105 LOC)ksd/local-outreach/orchestrator/main.js
async function processBusinesses(location, postcode, businessTypes = [], extractEmails = true) {
const scrapedAt = new Date().toISOString();
// Step 1: Scrape Google Maps (with postcode for accuracy)
// Try Outscraper first, fallback to HasData if it fails OR returns 0 results
logger.info('main', `Scraping businesses in ${location}${postcode ? ` (${postcode})` : ""}...`);
let businesses = [];
let scraperUsed = null;
try {
logger.info('main', 'Trying Outscraper API...');
businesses = await scrapeGoogleMapsOutscraper(location, postcode, businessTypes, extractEmails);
scraperUsed = 'outscraper';
logger.info('main', `Found ${businesses.length} businesses via Outscraper`);
// If Outscraper returned 0 results, try HasData as backup
if (businesses.length === 0) {
logger.info('main', 'Outscraper returned 0 results, trying HasData as backup...');
try {
const hasdataResults = await scrapeGoogleMaps(location, postcode, businessTypes,generateAndExport function · javascript · L582-L640 (59 LOC)ksd/local-outreach/orchestrator/main.js
async function generateAndExport(enrichedBusinesses, config = {}) {
const approvedTemplates = loadApprovedTemplates();
const exported = [];
for (const business of enrichedBusinesses) {
try {
// Generate content
const content = await generateOutreachContent(business, {
provider: process.env.CONTENT_PROVIDER || 'claude', // Use Claude by default
generateEmail: true,
generateLinkedIn: !!business.linkedInUrl,
emailSequence: true
});
// Check if approval needed
if (needsApproval(business, approvedTemplates)) {
addToApprovalQueue(business, content.email || content.emailSequence[0]);
logger.info('main', `Added ${business.category} email to approval queue`);
continue; // Skip export until approved
}
const exportedTo = [];
// Export to Lemlist
if (business.ownerEmail && config.lemlistCampaignId) {
await exportToLemlist(business, config.lemlistCaisValidLocation function · javascript · L647-L652 (6 LOC)ksd/local-outreach/orchestrator/main.js
function isValidLocation(location) {
if (!location || typeof location !== "string") return false;
// Allow alphanumeric, spaces, hyphens, apostrophes (for places like "Bishop's Stortford")
const locationPattern = /^[a-zA-Z0-9\s\-']+$/;
return locationPattern.test(location) && location.length >= 2 && location.length <= 100;
}isValidPostcode function · javascript · L659-L664 (6 LOC)ksd/local-outreach/orchestrator/main.js
function isValidPostcode(postcode) {
if (!postcode || typeof postcode !== "string") return false;
// UK postcode prefix pattern (e.g., SK7, M1, SW1A, EC1A)
const postcodePattern = /^[A-Z]{1,2}[0-9][0-9A-Z]?$/i;
return postcodePattern.test(postcode.trim());
}sanitizeBusinessTypes function · javascript · L671-L677 (7 LOC)ksd/local-outreach/orchestrator/main.js
function sanitizeBusinessTypes(input) {
if (!input || typeof input !== "string") return [];
return input
.split(",")
.map(type => type.trim().toLowerCase())
.filter(type => type.length > 0 && type.length <= 50 && /^[a-zA-Z0-9\s\-]+$/.test(type));
}printUsage function · javascript · L682-L701 (20 LOC)ksd/local-outreach/orchestrator/main.js
function printUsage() {
console.log(`
Usage: node main.js [location] [postcode] [businessTypes] [options]
Arguments:
location Location name (e.g., "Bramhall", "Manchester")
postcode UK postcode prefix (e.g., "SK7", "M1")
businessTypes Comma-separated list (e.g., "restaurants,cafes")
Options:
--no-emails Skip email extraction from websites
--load Load existing businesses instead of scraping
--help Show this help message
Examples:
node main.js Bramhall SK7
node main.js "Manchester" M1 "restaurants,bars" --no-emails
node main.js --load Bramhall SK7
`);
}Repobility — same analyzer, your code, free for public repos · /scan/
loadAgreements function · javascript · L15-L26 (12 LOC)ksd/local-outreach/orchestrator/modules/barter-agreements.js
function loadAgreements() {
try {
if (fs.existsSync(AGREEMENTS_FILE)) {
const data = fs.readFileSync(AGREEMENTS_FILE, "utf8");
return JSON.parse(data);
}
return {};
} catch (error) {
logger.warn('barter-agreements', 'Failed to load barter agreements', { error: error.message });
return {};
}
}saveAgreements function · javascript · L31-L37 (7 LOC)ksd/local-outreach/orchestrator/modules/barter-agreements.js
function saveAgreements(agreements) {
const dir = path.dirname(AGREEMENTS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(AGREEMENTS_FILE, JSON.stringify(agreements, null, 2));
}hasAgreement function · javascript · L42-L46 (5 LOC)ksd/local-outreach/orchestrator/modules/barter-agreements.js
function hasAgreement(category) {
const agreements = loadAgreements();
const categoryKey = (category || "").toLowerCase();
return agreements[categoryKey] && agreements[categoryKey].length > 0;
}page 1 / 9next ›