vipcare 0.1.0

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/lib/monitor.js ADDED
@@ -0,0 +1,109 @@
1
+ import fs from 'fs';
2
+ import { CHANGELOG_FILE, getProfilesDir } from './config.js';
3
+ import { searchPerson } from './fetchers/search.js';
4
+ import * as twitter from './fetchers/twitter.js';
5
+ import { listProfiles, loadProfile, saveProfile } from './profile.js';
6
+ import { synthesizeProfile, detectChanges } from './synthesizer.js';
7
+
8
+ export function extractMetadata(content) {
9
+ const meta = { twitterHandle: null, linkedinUrl: null, name: null };
10
+
11
+ const nameMatch = content.match(/^# (.+)$/m);
12
+ if (nameMatch) meta.name = nameMatch[1];
13
+
14
+ let twMatch = content.match(/twitter\.com\/(\w+)/i);
15
+ if (!twMatch) twMatch = content.match(/x\.com\/(\w+)/i);
16
+ if (twMatch) meta.twitterHandle = twMatch[1];
17
+
18
+ const liMatch = content.match(/(https?:\/\/[^/]*linkedin\.com\/in\/[^\s)]+)/);
19
+ if (liMatch) meta.linkedinUrl = liMatch[1];
20
+
21
+ return meta;
22
+ }
23
+
24
+ function gatherFreshData(meta) {
25
+ const rawParts = [];
26
+ const sources = [];
27
+
28
+ if (meta.twitterHandle) {
29
+ const data = twitter.fetchProfile(meta.twitterHandle);
30
+ if (data?.rawOutput) {
31
+ rawParts.push(`=== Twitter (@${meta.twitterHandle}) ===\n${data.rawOutput}`);
32
+ sources.push(`https://twitter.com/${meta.twitterHandle}`);
33
+ }
34
+ }
35
+
36
+ if (meta.name) {
37
+ const results = searchPerson(meta.name);
38
+ for (const r of results) {
39
+ rawParts.push(`${r.title}\n${r.body}`);
40
+ sources.push(r.url);
41
+ }
42
+ }
43
+
44
+ return [rawParts.join('\n\n'), sources];
45
+ }
46
+
47
+ export function appendChangelog(entry) {
48
+ const dir = CHANGELOG_FILE.substring(0, CHANGELOG_FILE.lastIndexOf('/'));
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ fs.appendFileSync(CHANGELOG_FILE, JSON.stringify(entry) + '\n');
51
+ }
52
+
53
+ export function readChangelog(days = 30) {
54
+ if (!fs.existsSync(CHANGELOG_FILE)) return [];
55
+
56
+ const cutoff = Date.now() - days * 86400000;
57
+ const lines = fs.readFileSync(CHANGELOG_FILE, 'utf-8').split('\n').filter(Boolean);
58
+
59
+ return lines
60
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
61
+ .filter(e => e && new Date(e.timestamp).getTime() >= cutoff);
62
+ }
63
+
64
+ export function unreadCount() {
65
+ return readChangelog(7).length;
66
+ }
67
+
68
+ export async function runMonitor(profilesDir, verbose = false) {
69
+ const dir = profilesDir || getProfilesDir();
70
+ const changes = [];
71
+ const profiles = listProfiles(dir);
72
+
73
+ for (const p of profiles) {
74
+ if (verbose) console.log(` Checking ${p.name}...`);
75
+
76
+ const oldContent = loadProfile(p.slug, dir);
77
+ if (!oldContent) continue;
78
+
79
+ const meta = extractMetadata(oldContent);
80
+ const [newData, sources] = gatherFreshData(meta);
81
+
82
+ if (!newData.trim()) {
83
+ if (verbose) console.log(' No new data found, skipping.');
84
+ continue;
85
+ }
86
+
87
+ const changeSummary = await detectChanges(oldContent, newData);
88
+
89
+ if (changeSummary) {
90
+ const newProfile = await synthesizeProfile(newData, sources);
91
+ saveProfile(p.name, newProfile, dir);
92
+
93
+ const entry = {
94
+ timestamp: new Date().toISOString(),
95
+ name: p.name,
96
+ slug: p.slug,
97
+ summary: changeSummary,
98
+ };
99
+ appendChangelog(entry);
100
+ changes.push(entry);
101
+
102
+ if (verbose) console.log(` Changes detected: ${changeSummary}`);
103
+ } else if (verbose) {
104
+ console.log(' No significant changes.');
105
+ }
106
+ }
107
+
108
+ return changes;
109
+ }
package/lib/profile.js ADDED
@@ -0,0 +1,106 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getProfilesDir } from './config.js';
4
+
5
+ export function slugify(name) {
6
+ return name
7
+ .toLowerCase()
8
+ .trim()
9
+ .replace(/[^\w\s-]/g, '')
10
+ .replace(/[\s_]+/g, '-')
11
+ .replace(/-+/g, '-')
12
+ .replace(/^-|-$/g, '');
13
+ }
14
+
15
+ export function saveProfile(name, content, profilesDir) {
16
+ const dir = profilesDir || getProfilesDir();
17
+ const slug = slugify(name);
18
+ const filepath = path.join(dir, `${slug}.md`);
19
+ fs.writeFileSync(filepath, content, 'utf-8');
20
+ return filepath;
21
+ }
22
+
23
+ export function loadProfile(nameOrSlug, profilesDir) {
24
+ const dir = profilesDir || getProfilesDir();
25
+ const slug = slugify(nameOrSlug);
26
+ const filepath = path.join(dir, `${slug}.md`);
27
+
28
+ if (fs.existsSync(filepath)) {
29
+ return fs.readFileSync(filepath, 'utf-8');
30
+ }
31
+
32
+ // Fuzzy match
33
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
34
+ const matches = files.filter(f => f.replace('.md', '').includes(slug));
35
+ if (matches.length === 1) {
36
+ return fs.readFileSync(path.join(dir, matches[0]), 'utf-8');
37
+ }
38
+
39
+ return null;
40
+ }
41
+
42
+ export function listProfiles(profilesDir) {
43
+ const dir = profilesDir || getProfilesDir();
44
+ if (!fs.existsSync(dir)) return [];
45
+
46
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort();
47
+ return files.map(f => {
48
+ const filepath = path.join(dir, f);
49
+ const content = fs.readFileSync(filepath, 'utf-8');
50
+
51
+ const nameMatch = content.match(/^# (.+)$/m);
52
+ const name = nameMatch ? nameMatch[1] : f.replace('.md', '').replace(/-/g, ' ');
53
+
54
+ const summaryMatch = content.match(/^> (.+)$/m);
55
+ const summary = summaryMatch ? summaryMatch[1] : '';
56
+
57
+ const stat = fs.statSync(filepath);
58
+ const updated = stat.mtime.toISOString().slice(0, 10);
59
+
60
+ return { slug: f.replace('.md', ''), name, summary, updated, path: filepath };
61
+ });
62
+ }
63
+
64
+ export function searchProfiles(keyword, profilesDir) {
65
+ const dir = profilesDir || getProfilesDir();
66
+ const kw = keyword.toLowerCase();
67
+ const results = [];
68
+
69
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort();
70
+ for (const f of files) {
71
+ const content = fs.readFileSync(path.join(dir, f), 'utf-8');
72
+ if (content.toLowerCase().includes(kw)) {
73
+ const nameMatch = content.match(/^# (.+)$/m);
74
+ const name = nameMatch ? nameMatch[1] : f.replace('.md', '');
75
+
76
+ const matches = content.split('\n')
77
+ .filter(line => line.toLowerCase().includes(kw))
78
+ .slice(0, 3)
79
+ .map(line => line.trim());
80
+
81
+ results.push({ slug: f.replace('.md', ''), name, matches, path: path.join(dir, f) });
82
+ }
83
+ }
84
+
85
+ return results;
86
+ }
87
+
88
+ export function profileExists(name, profilesDir) {
89
+ const dir = profilesDir || getProfilesDir();
90
+ return fs.existsSync(path.join(dir, `${slugify(name)}.md`));
91
+ }
92
+
93
+ export function getProfilePath(name, profilesDir) {
94
+ const dir = profilesDir || getProfilesDir();
95
+ return path.join(dir, `${slugify(name)}.md`);
96
+ }
97
+
98
+ export function deleteProfile(name, profilesDir) {
99
+ const dir = profilesDir || getProfilesDir();
100
+ const filepath = path.join(dir, `${slugify(name)}.md`);
101
+ if (fs.existsSync(filepath)) {
102
+ fs.unlinkSync(filepath);
103
+ return true;
104
+ }
105
+ return false;
106
+ }
@@ -0,0 +1,95 @@
1
+ import { search, searchPerson } from './fetchers/search.js';
2
+
3
+ const TWITTER_IGNORE = new Set(['search', 'explore', 'home', 'hashtag', 'i', 'settings', 'login']);
4
+
5
+ export function parseTwitterHandle(url) {
6
+ const match = url.match(/(?:twitter\.com|x\.com)\/(@?\w+)/);
7
+ if (match) {
8
+ const handle = match[1].replace(/^@/, '');
9
+ if (!TWITTER_IGNORE.has(handle.toLowerCase())) return handle;
10
+ }
11
+ return null;
12
+ }
13
+
14
+ export function parseLinkedinUrl(url) {
15
+ if (!url.includes('linkedin.com/in/')) return null;
16
+ const match = url.match(/(https?:\/\/[^/]*linkedin\.com\/in\/[^/?#]+)/);
17
+ return match ? match[1] : null;
18
+ }
19
+
20
+ function linkedinMatchesPerson(snippetTitle, snippetBody, personName) {
21
+ const parts = personName.toLowerCase().split(/\s+/);
22
+ if (parts.length < 2) return true;
23
+ const text = (snippetTitle + ' ' + snippetBody).toLowerCase();
24
+ return text.includes(parts[0]) && text.includes(parts[parts.length - 1]);
25
+ }
26
+
27
+ function extractRealName(snippets, handle) {
28
+ for (const snippet of snippets) {
29
+ const re1 = new RegExp(`([A-Z][a-z]+ [A-Z][a-z]+(?:\\s[A-Z][a-z]+)?)\\s*(?:\\(|[-–—/|,])\\s*@?${handle}`, '');
30
+ const m1 = snippet.match(re1);
31
+ if (m1) return m1[1];
32
+
33
+ const re2 = new RegExp(`@?${handle}\\s*(?:\\)|[-–—/|,])\\s*([A-Z][a-z]+ [A-Z][a-z]+)`, '');
34
+ const m2 = snippet.match(re2);
35
+ if (m2) return m2[1];
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export function resolveFromUrl(url) {
41
+ const person = { name: '', twitterHandle: null, linkedinUrl: null, otherUrls: [], rawSnippets: [] };
42
+
43
+ const handle = parseTwitterHandle(url);
44
+ if (handle) {
45
+ person.twitterHandle = handle;
46
+
47
+ const results = search(`@${handle} twitter`, 5);
48
+ const snippets = results.map(r => `${r.title}\n${r.body}`);
49
+
50
+ person.name = extractRealName(snippets, handle) || handle;
51
+ person.rawSnippets = snippets;
52
+ return person;
53
+ }
54
+
55
+ const linkedin = parseLinkedinUrl(url);
56
+ if (linkedin) {
57
+ person.linkedinUrl = linkedin;
58
+ const match = linkedin.match(/\/in\/([^/?#]+)/);
59
+ if (match) person.name = match[1].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
60
+ return person;
61
+ }
62
+
63
+ person.otherUrls.push(url);
64
+ return person;
65
+ }
66
+
67
+ export function resolveFromName(name, company) {
68
+ const results = searchPerson(name, company);
69
+ const person = { name, twitterHandle: null, linkedinUrl: null, otherUrls: [], rawSnippets: [] };
70
+
71
+ for (const r of results) {
72
+ if (!person.twitterHandle) {
73
+ const handle = parseTwitterHandle(r.url);
74
+ if (handle) person.twitterHandle = handle;
75
+ }
76
+
77
+ if (!person.linkedinUrl) {
78
+ const linkedin = parseLinkedinUrl(r.url);
79
+ if (linkedin && linkedinMatchesPerson(r.title, r.body, name)) {
80
+ person.linkedinUrl = linkedin;
81
+ }
82
+ }
83
+
84
+ if (!person.otherUrls.includes(r.url)) person.otherUrls.push(r.url);
85
+
86
+ const snippet = `${r.title}\n${r.body}`;
87
+ if (!person.rawSnippets.includes(snippet)) person.rawSnippets.push(snippet);
88
+ }
89
+
90
+ return person;
91
+ }
92
+
93
+ export function isUrl(text) {
94
+ return /^(https?:\/\/|twitter\.com|x\.com|linkedin\.com)/.test(text);
95
+ }
@@ -0,0 +1,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { execSync } from 'child_process';
5
+ import { loadConfig } from './config.js';
6
+
7
+ const PLIST_NAME = 'com.vip-crm.monitor';
8
+ const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_NAME}.plist`);
9
+
10
+ function getVipPath() {
11
+ try {
12
+ return execSync('which vip', { encoding: 'utf-8' }).trim();
13
+ } catch {
14
+ const candidates = [
15
+ path.join(os.homedir(), '.npm-global', 'bin', 'vip'),
16
+ '/opt/homebrew/bin/vip',
17
+ '/usr/local/bin/vip',
18
+ ];
19
+ for (const p of candidates) {
20
+ if (fs.existsSync(p)) return p;
21
+ }
22
+ throw new Error("Cannot find 'vip' executable. Is it installed?");
23
+ }
24
+ }
25
+
26
+ export function createPlist(intervalHours) {
27
+ if (!intervalHours) {
28
+ const config = loadConfig();
29
+ intervalHours = config.monitor_interval_hours || 24;
30
+ }
31
+
32
+ const vipPath = getVipPath();
33
+ const intervalSeconds = intervalHours * 3600;
34
+ const logDir = path.join(os.homedir(), '.vip-crm');
35
+
36
+ return `<?xml version="1.0" encoding="UTF-8"?>
37
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
38
+ <plist version="1.0">
39
+ <dict>
40
+ <key>Label</key>
41
+ <string>${PLIST_NAME}</string>
42
+ <key>ProgramArguments</key>
43
+ <array>
44
+ <string>${vipPath}</string>
45
+ <string>monitor</string>
46
+ <string>run</string>
47
+ </array>
48
+ <key>StartInterval</key>
49
+ <integer>${intervalSeconds}</integer>
50
+ <key>RunAtLoad</key>
51
+ <false/>
52
+ <key>StandardOutPath</key>
53
+ <string>${logDir}/monitor.log</string>
54
+ <key>StandardErrorPath</key>
55
+ <string>${logDir}/monitor-error.log</string>
56
+ </dict>
57
+ </plist>
58
+ `;
59
+ }
60
+
61
+ export function install() {
62
+ const plist = createPlist();
63
+ fs.mkdirSync(path.dirname(PLIST_PATH), { recursive: true });
64
+ fs.writeFileSync(PLIST_PATH, plist);
65
+ execSync(`launchctl load "${PLIST_PATH}"`);
66
+ }
67
+
68
+ export function uninstall() {
69
+ if (fs.existsSync(PLIST_PATH)) {
70
+ try { execSync(`launchctl unload "${PLIST_PATH}"`); } catch { /* ignore */ }
71
+ fs.unlinkSync(PLIST_PATH);
72
+ }
73
+ }
74
+
75
+ export function isRunning() {
76
+ try {
77
+ const output = execSync('launchctl list', { encoding: 'utf-8' });
78
+ return output.includes(PLIST_NAME);
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ export function status() {
85
+ const config = loadConfig();
86
+ return {
87
+ installed: fs.existsSync(PLIST_PATH),
88
+ running: isRunning(),
89
+ intervalHours: config.monitor_interval_hours || 24,
90
+ plistPath: PLIST_PATH,
91
+ };
92
+ }
@@ -0,0 +1,124 @@
1
+ import { execSync } from 'child_process';
2
+ import { checkTool, loadConfig } from './config.js';
3
+ import { PROFILE_SYSTEM_PROMPT, CHANGE_DETECTION_PROMPT } from './templates.js';
4
+
5
+ function getBackend() {
6
+ const envBackend = process.env.VIP_AI_BACKEND?.toLowerCase();
7
+ if (envBackend) return envBackend;
8
+
9
+ const config = loadConfig();
10
+ if (config.ai_backend) return config.ai_backend.toLowerCase();
11
+
12
+ if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
13
+ if (checkTool('claude')) return 'claude-cli';
14
+ if (checkTool('gh') && copilotAvailable()) return 'copilot-cli';
15
+
16
+ throw new Error(
17
+ 'No AI backend available. Options:\n' +
18
+ ' 1. Install Claude Code CLI\n' +
19
+ ' 2. Set ANTHROPIC_API_KEY env var\n' +
20
+ ' 3. Install GitHub Copilot CLI (gh copilot)'
21
+ );
22
+ }
23
+
24
+ function copilotAvailable() {
25
+ try {
26
+ execSync('gh copilot --help', { stdio: 'ignore', timeout: 5000 });
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function callClaudeCli(prompt, timeout = 120000) {
34
+ const result = execSync(`claude --print -p ${JSON.stringify(prompt)}`, {
35
+ encoding: 'utf-8',
36
+ timeout,
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ maxBuffer: 1024 * 1024 * 10,
39
+ });
40
+ return result.trim();
41
+ }
42
+
43
+ async function callAnthropicApi(prompt) {
44
+ let anthropic;
45
+ try {
46
+ anthropic = await import('@anthropic-ai/sdk');
47
+ } catch {
48
+ throw new Error('Anthropic SDK not installed. Run: npm install @anthropic-ai/sdk');
49
+ }
50
+
51
+ const apiKey = process.env.ANTHROPIC_API_KEY;
52
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY environment variable not set.');
53
+
54
+ const config = loadConfig();
55
+ const model = config.anthropic_model || 'claude-sonnet-4-20250514';
56
+
57
+ const client = new anthropic.default({ apiKey });
58
+ const message = await client.messages.create({
59
+ model,
60
+ max_tokens: 4096,
61
+ messages: [{ role: 'user', content: prompt }],
62
+ });
63
+
64
+ return message.content[0].text.trim();
65
+ }
66
+
67
+ function callCopilotCli(prompt, timeout = 120000) {
68
+ const result = execSync(`gh copilot suggest -t shell ${JSON.stringify(prompt)}`, {
69
+ encoding: 'utf-8',
70
+ timeout,
71
+ stdio: ['pipe', 'pipe', 'pipe'],
72
+ });
73
+ return result.trim();
74
+ }
75
+
76
+ async function callBackend(prompt, backend) {
77
+ if (backend === 'claude-cli') return callClaudeCli(prompt);
78
+ if (backend === 'anthropic') return await callAnthropicApi(prompt);
79
+ if (backend === 'copilot-cli') return callCopilotCli(prompt);
80
+ throw new Error(`Unknown AI backend: ${backend}`);
81
+ }
82
+
83
+ export function getBackendName() {
84
+ try {
85
+ return getBackend();
86
+ } catch {
87
+ return 'none';
88
+ }
89
+ }
90
+
91
+ export async function synthesizeProfile(rawData, sources) {
92
+ const backend = getBackend();
93
+ const prompt = `${PROFILE_SYSTEM_PROMPT}\n\nRaw data:\n${rawData}`;
94
+
95
+ const profile = await callBackend(prompt, backend);
96
+
97
+ const today = new Date().toISOString().slice(0, 10);
98
+ let footer = `\n\n---\n*Last updated: ${today}*`;
99
+ if (sources?.length) {
100
+ footer += `\n*Sources: ${sources.slice(0, 10).join(', ')}*`;
101
+ }
102
+
103
+ return profile + footer;
104
+ }
105
+
106
+ export async function detectChanges(oldProfile, newData) {
107
+ let backend;
108
+ try {
109
+ backend = getBackend();
110
+ } catch {
111
+ return null;
112
+ }
113
+
114
+ const prompt = CHANGE_DETECTION_PROMPT
115
+ .replace('{old_profile}', oldProfile)
116
+ .replace('{new_data}', newData);
117
+
118
+ try {
119
+ const output = await callBackend(prompt, backend);
120
+ return output.includes('NO_SIGNIFICANT_CHANGES') ? null : output;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
@@ -0,0 +1,66 @@
1
+ export const PROFILE_SYSTEM_PROMPT = `You are a research assistant building a VIP contact profile.
2
+ Given the raw data below from public sources (tweets, LinkedIn snippets, web search results), \
3
+ synthesize a clean profile in the exact Markdown format provided.
4
+
5
+ Rules:
6
+ - Only include information you can directly support from the provided data
7
+ - If a section has no data, write "No public information found."
8
+ - Do not fabricate or hallucinate details
9
+ - Write in a neutral, professional tone
10
+ - For the summary line, write a concise one-line description of who this person is
11
+ - Output ONLY the Markdown profile, no extra commentary
12
+
13
+ Format:
14
+ # {Full Name}
15
+
16
+ > {One-line summary / tagline}
17
+
18
+ ## Basic Info
19
+ - **Title:** {Current role}
20
+ - **Company:** {Current company}
21
+ - **Location:** {City, Country}
22
+ - **Industry:** {Industry/domain}
23
+
24
+ ## Links
25
+ - Twitter: {url}
26
+ - LinkedIn: {url}
27
+ - Website: {url if found}
28
+
29
+ ## Bio
30
+ {2-3 paragraph biography synthesized from public sources}
31
+
32
+ ## Key Interests & Topics
33
+ - {Topic 1}
34
+ - {Topic 2}
35
+ - {Topic 3}
36
+
37
+ ## Notable Achievements
38
+ - {Achievement 1}
39
+ - {Achievement 2}
40
+
41
+ ## Recent Activity
42
+ - {Summary of recent public posts/tweets/news}
43
+
44
+ ## Background
45
+ {Education, career history, other public background}
46
+
47
+ ## Personal
48
+ {Family info, hobbies, or personal details only if publicly shared}
49
+
50
+ ## Notes
51
+ {Any other relevant public information}`;
52
+
53
+ export const CHANGE_DETECTION_PROMPT = `Compare the OLD profile and NEW data below. Identify any significant changes such as:
54
+ - Job title or company change
55
+ - New achievements or milestones
56
+ - Notable new public statements or positions
57
+ - Changes in focus areas or interests
58
+
59
+ If there are significant changes, output a brief summary (2-3 sentences) of what changed.
60
+ If there are no significant changes, output exactly: NO_SIGNIFICANT_CHANGES
61
+
62
+ OLD PROFILE:
63
+ {old_profile}
64
+
65
+ NEW DATA:
66
+ {new_data}`;
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "vipcare",
3
+ "version": "0.1.0",
4
+ "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
+ "type": "module",
6
+ "bin": {
7
+ "vip": "bin/vip.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test tests/*.test.js"
11
+ },
12
+ "keywords": [
13
+ "crm",
14
+ "profile",
15
+ "twitter",
16
+ "linkedin",
17
+ "cli"
18
+ ],
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "commander": "^13.0.0"
22
+ },
23
+ "devDependencies": {}
24
+ }
File without changes
@@ -0,0 +1,49 @@
1
+ # Sam Altman
2
+
3
+ > CEO of OpenAI, leading the development and deployment of artificial general intelligence
4
+
5
+ ## Basic Info
6
+ - **Title:** CEO
7
+ - **Company:** OpenAI
8
+ - **Location:** San Francisco, USA
9
+ - **Industry:** Artificial Intelligence
10
+
11
+ ## Links
12
+ - Twitter: https://x.com/sama
13
+ - LinkedIn: No public information found.
14
+ - Website: No public information found.
15
+
16
+ ## Bio
17
+ Sam Altman is the CEO of OpenAI, the artificial intelligence research and deployment company behind ChatGPT and the GPT series of large language models. He is a prominent figure in the technology industry, known for his leadership in advancing AI capabilities while advocating for AI safety.
18
+
19
+ Before leading OpenAI, Altman served as President of Y Combinator (YC), one of Silicon Valley's most influential startup accelerators. In that role he funded and mentored numerous startups, building a reputation as a key connector and investor in the tech ecosystem.
20
+
21
+ Under his leadership, OpenAI has launched products including GPT-4, GPT-5, and ChatGPT, which have become some of the most widely used AI systems in the world. Altman has also navigated significant partnerships, including agreements with the U.S. Department of War for classified AI deployments.
22
+
23
+ ## Key Interests & Topics
24
+ - Artificial intelligence safety and alignment
25
+ - Broad distribution of AI benefits
26
+ - AI product development and deployment (GPT series, ChatGPT)
27
+
28
+ ## Notable Achievements
29
+ - Led OpenAI as CEO through the launch of ChatGPT and the GPT model series
30
+ - Served as President of Y Combinator, mentoring and funding hundreds of startups
31
+
32
+ ## Recent Activity
33
+ - **Aug 2025:** Announced GPT-5 rollout updates, including doubled rate limits for ChatGPT Plus users and continued access to legacy models like GPT-4o
34
+ - **Feb 2026:** Announced an agreement with the Department of War to deploy OpenAI models on their classified network, emphasizing AI safety principles
35
+ - **Mar 2026:** Published an internal post detailing amendments to the DoW agreement, including language reinforcing constitutional and legal safeguards (Fourth Amendment, National Security Act, FISA)
36
+
37
+ ## Background
38
+ Sam Altman was President of Y Combinator before becoming CEO of OpenAI. He has been active in the startup and technology community since at least the early 2010s, funding companies and serving as a mentor to entrepreneurs.
39
+
40
+ ## Personal
41
+ No public information found.
42
+
43
+ ## Notes
44
+ - Twitter handle @sama has been active since July 2006
45
+ - Note: multiple unrelated individuals share the "Sama" name on LinkedIn; none of those profiles correspond to Sam Altman
46
+
47
+ ---
48
+ *Last updated: 2026-04-07*
49
+ *Sources: https://www.linkedin.com/in/andrew-a-sama-md-4bb65242*