vipcare 0.3.12 → 0.3.14

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/README.md CHANGED
@@ -73,7 +73,7 @@ vip update sam-altman
73
73
  ## Features
74
74
 
75
75
  - **Auto profile building** — Give a name or URL, get a structured profile
76
- - **Multi-source data** — Twitter (via [bird CLI](https://github.com/nickytonline/bird)), LinkedIn, web search
76
+ - **Multi-source data** — Twitter (via [bird CLI](https://github.com/steipete/bird)), LinkedIn, web search
77
77
  - **AI synthesis** — Claude CLI, Anthropic API, or GitHub Copilot CLI
78
78
  - **Auto monitoring** — Scheduled profile refresh with change detection (macOS launchd)
79
79
  - **Markdown output** — One `.md` file per person
package/bin/vip.js CHANGED
@@ -474,16 +474,44 @@ program.command('youtube-search')
474
474
 
475
475
  // --- card ---
476
476
  program.command('card')
477
- .description('Generate H5 baseball card page from all profiles')
477
+ .description('Generate and serve H5 baseball card page')
478
478
  .option('-o, --output <path>', 'Output HTML file', 'web/index.html')
479
- .action((opts) => {
479
+ .option('-p, --port <port>', 'Server port', '3000')
480
+ .option('--no-serve', 'Only generate, do not start server')
481
+ .action(async (opts) => {
480
482
  console.log(c.cyan('Generating baseball cards...'));
481
483
  const profiles = listProfiles();
482
484
  if (!profiles.length) { console.log(c.dim('No profiles. Use "vip add" first.')); return; }
483
485
 
484
486
  const outputPath = generateCards(profiles, opts.output);
485
487
  console.log(c.green(`Cards generated: ${outputPath}`));
486
- console.log(c.dim(`Open in browser: open ${outputPath}`));
488
+
489
+ if (opts.serve === false) {
490
+ console.log(c.dim(`Open in browser: open ${outputPath}`));
491
+ return;
492
+ }
493
+
494
+ const http = await import('http');
495
+ const port = parseInt(opts.port);
496
+ const dir = path.dirname(outputPath);
497
+ const file = path.basename(outputPath);
498
+
499
+ const server = http.createServer((req, res) => {
500
+ const filePath = req.url === '/' ? path.join(dir, file) : path.join(dir, req.url);
501
+ if (!fs.existsSync(filePath)) { res.writeHead(404); res.end('Not found'); return; }
502
+ const ext = path.extname(filePath);
503
+ const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.json': 'application/json', '.png': 'image/png' };
504
+ res.writeHead(200, { 'Content-Type': types[ext] || 'text/plain' });
505
+ res.end(fs.readFileSync(filePath));
506
+ });
507
+
508
+ server.listen(port, () => {
509
+ const url = `http://localhost:${port}`;
510
+ console.log(c.green(`\nServer running at ${c.bold(url)}`));
511
+ console.log(c.dim('Press Ctrl+C to stop\n'));
512
+ // Auto-open in browser
513
+ try { execFileSync('open', [url], { stdio: 'ignore' }); } catch {}
514
+ });
487
515
  });
488
516
 
489
517
  // --- digest ---
@@ -834,7 +862,7 @@ program.command('init')
834
862
  console.log(c.bold(c.cyan('\nChecking dependencies...\n')));
835
863
 
836
864
  const deps = [
837
- { name: 'bird', label: 'Bird CLI (Twitter data)', install: 'npm install -g @nickytonline/bird', check: () => checkTool('bird') },
865
+ { name: 'bird', label: 'Bird CLI (Twitter data)', install: 'npm install -g @steipete/bird', check: () => checkTool('bird') },
838
866
  { name: 'ddgs', label: 'DDGS (web search)', install: 'pip install ddgs', check: () => checkTool('ddgs') || fs.existsSync(path.join(os.homedir(), 'Library', 'Python', '3.9', 'bin', 'ddgs')) },
839
867
  { name: 'claude', label: 'Claude Code CLI (AI synthesis)', install: 'npm install -g @anthropic-ai/claude-code', check: () => checkTool('claude') },
840
868
  { name: 'yt-dlp', label: 'yt-dlp (YouTube download)', install: 'pip install yt-dlp', check: () => checkTool('yt-dlp') },
package/lib/card.js CHANGED
@@ -31,6 +31,7 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
31
31
  const industryMatch = content.match(/\*\*Industry:\*\*\s*(.+)/);
32
32
 
33
33
  cards.push({
34
+ slug: p.slug,
34
35
  name: nameMatch ? nameMatch[1] : p.name,
35
36
  title: titleMatch ? titleMatch[1].trim() : '',
36
37
  company: companyMatch ? companyMatch[1].trim() : '',
@@ -50,9 +51,20 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
50
51
  continue;
51
52
  }
52
53
 
54
+ data.slug = p.slug;
53
55
  cards.push(data);
54
56
  }
55
57
 
58
+ // Generate individual profile pages
59
+ for (const p of profiles) {
60
+ const content = loadProfile(p.slug);
61
+ if (!content) continue;
62
+ const profileHtml = buildProfilePage(p.name, content);
63
+ const dir = path.dirname(outputPath);
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ fs.writeFileSync(path.join(dir, `${p.slug}.html`), profileHtml, 'utf-8');
66
+ }
67
+
56
68
  const html = buildHtml(cards);
57
69
 
58
70
  const dir = path.dirname(outputPath);
@@ -62,6 +74,45 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
62
74
  return path.resolve(outputPath);
63
75
  }
64
76
 
77
+ function buildProfilePage(name, markdown) {
78
+ // Convert markdown to simple HTML
79
+ const htmlContent = markdown
80
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
81
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
82
+ .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
83
+ .replace(/^- \*\*(.+?):\*\* (.+)$/gm, '<p><strong>$1:</strong> $2</p>')
84
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
85
+ .replace(/\n\n/g, '<br><br>');
86
+
87
+ return `<!DOCTYPE html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="UTF-8">
91
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
92
+ <title>${escapeHtml(name)} - VIPCare</title>
93
+ <style>
94
+ * { margin: 0; padding: 0; box-sizing: border-box; }
95
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; max-width: 800px; margin: 0 auto; line-height: 1.7; }
96
+ a { color: #38bdf8; text-decoration: none; }
97
+ a:hover { text-decoration: underline; }
98
+ .back { display: inline-block; margin-bottom: 20px; font-size: 0.9em; }
99
+ h1 { color: #38bdf8; font-size: 2em; margin: 16px 0 8px; }
100
+ h2 { color: #818cf8; font-size: 1.3em; margin: 24px 0 8px; border-bottom: 1px solid #334155; padding-bottom: 6px; }
101
+ blockquote { color: #fbbf24; font-style: italic; padding: 8px 16px; border-left: 3px solid #475569; margin: 8px 0; }
102
+ p, li { color: #cbd5e1; margin: 4px 0; }
103
+ strong { color: #f1f5f9; }
104
+ li { margin-left: 20px; }
105
+ hr { border: none; border-top: 1px solid #334155; margin: 24px 0; }
106
+ .meta { color: #64748b; font-size: 0.85em; }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <a href="index.html" class="back">&larr; Back to all cards</a>
111
+ ${htmlContent}
112
+ </body>
113
+ </html>`;
114
+ }
115
+
65
116
  function escapeHtml(str) {
66
117
  return String(str)
67
118
  .replace(/&/g, '&amp;')
@@ -286,6 +337,8 @@ function openModal(index) {
286
337
  \${card.gifts?.length ? \`<p><strong>🎁 Gifts:</strong> \${card.gifts.join(', ')}</p>\` : ''}
287
338
 
288
339
  \${card.tags?.length ? \`<h2>Tags</h2><div class="tags">\${card.tags.map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
340
+
341
+ \${card.slug ? \`<p style="margin-top:20px;text-align:center"><a href="\${card.slug}.html" style="color:#38bdf8;text-decoration:none;padding:8px 20px;border:1px solid #38bdf8;border-radius:8px;display:inline-block">View Full Profile →</a></p>\` : ''}
289
342
  \`;
290
343
 
291
344
  document.getElementById('modal').classList.add('active');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {