← back to Marketing Command Center

scripts/import-linkedin-connections.mjs

70 lines

// Normalize a LinkedIn "Connections.csv" (from the data export) into a clean local
// dataset the command center can search: who Steve knows, at which firm, in what role.
// INTERNAL / PII — never expose via a public surface; the #vendors panel is Basic-Auth.
// Also emits a firms-by-connection-count rollup (where the network is concentrated).
//
//   node scripts/import-linkedin-connections.mjs "<path-to>/Connections.csv"
import fs from 'node:fs';
import path from 'node:path';

const IN = process.argv[2];
if (!IN || !fs.existsSync(IN)) { console.error('Usage: import-linkedin-connections.mjs <Connections.csv>'); process.exit(1); }
const DATA_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'data');
const OUT = path.join(DATA_DIR, 'linkedin-connections.json');
const OUT_FIRMS = path.join(DATA_DIR, 'linkedin-network-firms.json');

function parseCsv(text) {
  const rows = []; let row = [], field = '', q = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    if (q) { if (c === '"' && text[i + 1] === '"') { field += '"'; i++; } else if (c === '"') q = false; else field += c; }
    else if (c === '"') q = true;
    else if (c === ',') { row.push(field); field = ''; }
    else if (c === '\n' || c === '\r') { if (c === '\r' && text[i + 1] === '\n') i++; row.push(field); field = ''; if (row.some(x => x !== '')) rows.push(row); row = []; }
    else field += c;
  }
  if (field !== '' || row.length) { row.push(field); if (row.some(x => x !== '')) rows.push(row); }
  return rows;
}

const rows = parseCsv(fs.readFileSync(IN, 'utf8'));
const hi = rows.findIndex(r => r.some(c => /first name/i.test(c)) && r.some(c => /company/i.test(c)));
if (hi < 0) { console.error('No header row (First Name + Company) found.'); process.exit(1); }
const H = rows[hi].map(h => h.trim().toLowerCase());
const col = (name) => H.findIndex(h => h === name);
const iFirst = col('first name'), iLast = col('last name'), iUrl = col('url'),
      iEmail = col('email address'), iCompany = col('company'), iPos = col('position'), iOn = col('connected on');

const people = [];
for (const r of rows.slice(hi + 1)) {
  const company = (r[iCompany] || '').trim();
  const first = (r[iFirst] || '').trim(), last = (r[iLast] || '').trim();
  if (!first && !last && !company) continue;
  people.push({
    name: `${first} ${last}`.trim(),
    company,
    title: (r[iPos] || '').trim(),
    connectedOn: (r[iOn] || '').trim() || null,
    profileUrl: (r[iUrl] || '').trim() || null,
    hasEmail: !!(r[iEmail] || '').trim(),   // don't persist the address itself into the dataset
  });
}

// firms-by-connection-count rollup
const byFirm = new Map();
for (const p of people) {
  if (!p.company) continue;
  const k = p.company;
  if (!byFirm.has(k)) byFirm.set(k, { company: k, count: 0, people: [] });
  const e = byFirm.get(k); e.count++; if (e.people.length < 8) e.people.push({ name: p.name, title: p.title });
}
const firms = [...byFirm.values()].sort((a, b) => b.count - a.count);

fs.writeFileSync(OUT, JSON.stringify({ note: 'Steve\'s LinkedIn 1st-degree connections (data export). INTERNAL/PII — never expose publicly.', importedAt_placeholder: true, count: people.length, people }, null, 2));
fs.writeFileSync(OUT_FIRMS, JSON.stringify({ note: 'Firms where Steve has connections, by count (from Connections.csv).', totalFirms: firms.length, firms }, null, 2));

console.log(`Connections: ${people.length} people → ${path.basename(OUT)}`);
console.log(`Firms:       ${firms.length} distinct → ${path.basename(OUT_FIRMS)}`);
console.log('\nTop 25 firms by # of your connections:');
firms.slice(0, 25).forEach((f, i) => console.log(`  ${String(i + 1).padStart(2)}. ${f.company}  (${f.count})`));