← back to Marketing Command Center
vendors: LinkedIn Connections.csv importer + network-firms rollup (INTERNAL/PII gitignored)
8f46fdc1143513eb3af72357962d7dfc75d73a4b · 2026-08-31 13:19:37 -0700 · Steve Abrams
Files touched
M .gitignoreA scripts/import-linkedin-connections.mjs
Diff
commit 8f46fdc1143513eb3af72357962d7dfc75d73a4b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 13:19:37 2026 -0700
vendors: LinkedIn Connections.csv importer + network-firms rollup (INTERNAL/PII gitignored)
---
.gitignore | 3 ++
scripts/import-linkedin-connections.mjs | 69 +++++++++++++++++++++++++++++++++
2 files changed, 72 insertions(+)
diff --git a/.gitignore b/.gitignore
index 8e73fdb..905bc74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,3 +81,6 @@ public/amp-assets/*
data/vendor-amplify-assets/
data/vendor-amplify-assets.json
demo/out/
+data/linkedin-connections.json
+data/linkedin-network-firms.json
+data/vendor-linkedin-follows.json
diff --git a/scripts/import-linkedin-connections.mjs b/scripts/import-linkedin-connections.mjs
new file mode 100644
index 0000000..55803f0
--- /dev/null
+++ b/scripts/import-linkedin-connections.mjs
@@ -0,0 +1,69 @@
+// 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})`));
← 0cc700b auto-data-snapshot: 2026-08-31T13:13:14 (1 data files) — pub
·
back to Marketing Command Center
·
vendors: strict LinkedIn-connection x vendor_registry contac 2e93a4c →