vipcare 0.3.24 → 0.3.26

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/bin/vip.js CHANGED
@@ -861,18 +861,31 @@ program.command('config')
861
861
  .option('--json', 'Output as JSON')
862
862
  .action((opts) => {
863
863
  const cfg = loadConfig();
864
+
865
+ const toolStatus = {
866
+ bird: checkTool('bird'),
867
+ ddgs: (() => { try { execFileSync('python3', ['-c', 'import ddgs'], { stdio: 'ignore', timeout: 5000 }); return true; } catch { return checkTool('ddgs'); } })(),
868
+ claude: checkTool('claude'),
869
+ 'yt-dlp': checkTool('yt-dlp'),
870
+ whisper: (() => { try { execFileSync('python3', ['-c', 'import whisper'], { stdio: 'ignore', timeout: 5000 }); return true; } catch { return checkTool('whisper'); } })(),
871
+ ai_backend: getBackendName(),
872
+ };
873
+
864
874
  if (opts.json) {
865
- console.log(JSON.stringify({
866
- ...cfg,
867
- tools: { bird: checkTool('bird'), ai_backend: getBackendName() }
868
- }, null, 2));
875
+ console.log(JSON.stringify({ ...cfg, tools: toolStatus }, null, 2));
869
876
  return;
870
877
  }
871
878
  console.log(c.bold(c.cyan('Current config:')));
872
879
  console.log(` Profiles dir: ${cfg.profiles_dir}`);
873
880
  console.log(` Monitor interval: ${cfg.monitor_interval_hours}h`);
874
- console.log(` Bird CLI: ${checkTool('bird') ? c.green('available') : c.red('not found')}`);
875
- console.log(` AI backend: ${(() => { const b = getBackendName(); return b !== 'none' ? c.green(b) : c.red('not found'); })()}`);
881
+ console.log();
882
+ const ok = (v) => v ? c.green('available') : c.red('not found');
883
+ console.log(` Bird CLI: ${ok(toolStatus.bird)}`);
884
+ console.log(` DDGS: ${ok(toolStatus.ddgs)}`);
885
+ console.log(` Claude CLI: ${ok(toolStatus.claude)}`);
886
+ console.log(` yt-dlp: ${ok(toolStatus['yt-dlp'])}`);
887
+ console.log(` Whisper: ${ok(toolStatus.whisper)}`);
888
+ console.log(` AI backend: ${toolStatus.ai_backend !== 'none' ? c.green(toolStatus.ai_backend) : c.red('not found')}`);
876
889
  });
877
890
 
878
891
  // --- init ---
package/lib/card.js CHANGED
@@ -24,9 +24,12 @@ export function generateCards(profiles, outputPath) {
24
24
  const content = loadProfile(p.slug);
25
25
  if (!content) continue;
26
26
 
27
+ // Extract twitter handle from content
28
+ const twMatch = content.match(/twitter\.com\/(\w+)/i) || content.match(/x\.com\/(\w+)/i);
29
+ const twitterHandle = twMatch ? twMatch[1] : null;
30
+
27
31
  const data = extractVipData(content);
28
32
  if (!data) {
29
- // Fallback: parse what we can from markdown headers
30
33
  const nameMatch = content.match(/^# (.+)$/m);
31
34
  const summaryMatch = content.match(/^> (.+)$/m);
32
35
  const titleMatch = content.match(/\*\*Title:\*\*\s*(.+)/);
@@ -36,11 +39,11 @@ export function generateCards(profiles, outputPath) {
36
39
 
37
40
  cards.push({
38
41
  slug: p.slug,
42
+ twitter_handle: twitterHandle,
39
43
  name: nameMatch ? nameMatch[1] : p.name,
40
44
  title: titleMatch ? titleMatch[1].trim() : '',
41
45
  company: companyMatch ? companyMatch[1].trim() : '',
42
46
  location: locationMatch ? locationMatch[1].trim() : '',
43
- disc: '?',
44
47
  mbti: '?',
45
48
  scores: {},
46
49
  tags: industryMatch ? [industryMatch[1].trim()] : [],
@@ -56,6 +59,7 @@ export function generateCards(profiles, outputPath) {
56
59
  }
57
60
 
58
61
  data.slug = p.slug;
62
+ data.twitter_handle = data.twitter_handle || twitterHandle;
59
63
  cards.push(data);
60
64
  }
61
65
 
@@ -96,18 +100,18 @@ function buildProfilePage(name, markdown) {
96
100
  <title>${escapeHtml(name)} - VIPCare</title>
97
101
  <style>
98
102
  * { margin: 0; padding: 0; box-sizing: border-box; }
99
- 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; }
100
- a { color: #38bdf8; text-decoration: none; }
103
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f8fafc; color: #1e293b; padding: 24px; max-width: 800px; margin: 0 auto; line-height: 1.7; }
104
+ a { color: #2563eb; text-decoration: none; }
101
105
  a:hover { text-decoration: underline; }
102
106
  .back { display: inline-block; margin-bottom: 20px; font-size: 0.9em; }
103
- h1 { color: #38bdf8; font-size: 2em; margin: 16px 0 8px; }
104
- h2 { color: #818cf8; font-size: 1.3em; margin: 24px 0 8px; border-bottom: 1px solid #334155; padding-bottom: 6px; }
105
- blockquote { color: #fbbf24; font-style: italic; padding: 8px 16px; border-left: 3px solid #475569; margin: 8px 0; }
106
- p, li { color: #cbd5e1; margin: 4px 0; }
107
- strong { color: #f1f5f9; }
107
+ h1 { color: #2563eb; font-size: 2em; margin: 16px 0 8px; }
108
+ h2 { color: #7c3aed; font-size: 1.3em; margin: 24px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 6px; }
109
+ blockquote { color: #d97706; font-style: italic; padding: 8px 16px; border-left: 3px solid #e2e8f0; margin: 8px 0; }
110
+ p, li { color: #475569; margin: 4px 0; }
111
+ strong { color: #0f172a; }
108
112
  li { margin-left: 20px; }
109
- hr { border: none; border-top: 1px solid #334155; margin: 24px 0; }
110
- .meta { color: #64748b; font-size: 0.85em; }
113
+ hr { border: none; border-top: 1px solid #e2e8f0; margin: 24px 0; }
114
+ .meta { color: #94a3b8; font-size: 0.85em; }
111
115
  </style>
112
116
  </head>
113
117
  <body>
@@ -138,15 +142,15 @@ function buildHtml(cards) {
138
142
  <style>
139
143
  * { margin: 0; padding: 0; box-sizing: border-box; }
140
144
  html { scroll-behavior: smooth; }
141
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; padding: 20px; padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); }
142
- h1 { text-align: center; font-size: 1.8em; margin: 20px 0 30px; color: #38bdf8; }
145
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f8fafc; color: #1e293b; min-height: 100vh; padding: 20px; padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); }
146
+ h1 { text-align: center; font-size: 1.8em; margin: 20px 0 30px; color: #2563eb; }
143
147
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; max-width: 1200px; margin: 0 auto; }
144
148
 
145
149
  .card {
146
- background: linear-gradient(145deg, #1e293b, #334155);
150
+ background: #ffffff;
147
151
  border-radius: 16px;
148
152
  padding: 24px;
149
- border: 1px solid #475569;
153
+ border: 1px solid #e2e8f0;
150
154
  cursor: pointer;
151
155
  transition: transform 0.2s, box-shadow 0.2s;
152
156
  position: relative;
@@ -154,53 +158,53 @@ h1 { text-align: center; font-size: 1.8em; margin: 20px 0 30px; color: #38bdf8;
154
158
  min-height: 44px;
155
159
  -webkit-tap-highlight-color: transparent;
156
160
  }
157
- .card:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(56,189,248,0.15); }
158
- .card:active { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(56,189,248,0.1); }
161
+ .card:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(37,99,235,0.12); }
162
+ .card:active { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(37,99,235,0.08); }
159
163
  .card::before {
160
164
  content: '';
161
165
  position: absolute;
162
166
  top: 0; left: 0; right: 0;
163
167
  height: 4px;
164
- background: linear-gradient(90deg, #38bdf8, #818cf8, #c084fc);
168
+ background: linear-gradient(90deg, #2563eb, #7c3aed, #db2777);
165
169
  }
166
170
 
167
171
  .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
168
- .card-name { font-size: 1.4em; font-weight: 700; color: #f1f5f9; }
169
- .card-role { font-size: 0.85em; color: #94a3b8; margin-top: 2px; }
172
+ .card-name { font-size: 1.4em; font-weight: 700; color: #0f172a; }
173
+ .card-role { font-size: 0.85em; color: #64748b; margin-top: 2px; }
170
174
  .card-badges { display: flex; gap: 6px; }
171
175
  .badge { padding: 4px 10px; border-radius: 6px; font-size: 0.75em; font-weight: 700; min-height: 28px; display: inline-flex; align-items: center; }
172
- .badge-disc { background: #38bdf8; color: #0f172a; }
173
- .badge-mbti { background: #818cf8; color: #0f172a; }
176
+ .badge-disc { background: #2563eb; color: #ffffff; }
177
+ .badge-mbti { background: #7c3aed; color: #ffffff; }
174
178
 
175
- .card-quote { font-style: italic; color: #94a3b8; font-size: 0.8em; margin: 10px 0; padding: 8px 12px; border-left: 3px solid #475569; }
179
+ .card-quote { font-style: italic; color: #64748b; font-size: 0.8em; margin: 10px 0; padding: 8px 12px; border-left: 3px solid #e2e8f0; }
176
180
 
177
181
  .radar-container { display: flex; justify-content: center; margin: 16px 0; }
178
182
  .radar { width: 200px; height: 200px; max-width: 100%; }
179
183
 
180
184
  .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
181
- .tag { background: #1e3a5f; color: #38bdf8; padding: 4px 12px; border-radius: 12px; font-size: 0.75em; min-height: 28px; display: inline-flex; align-items: center; }
185
+ .tag { background: #eff6ff; color: #2563eb; padding: 4px 12px; border-radius: 12px; font-size: 0.75em; min-height: 28px; display: inline-flex; align-items: center; }
182
186
 
183
187
  .expertise { margin: 10px 0; }
184
- .expertise-title { font-size: 0.75em; color: #64748b; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
185
- .expertise-item { font-size: 0.8em; color: #cbd5e1; padding: 2px 0; }
186
- .superpower { color: #fbbf24; font-weight: 600; font-size: 0.85em; margin: 6px 0; }
188
+ .expertise-title { font-size: 0.75em; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
189
+ .expertise-item { font-size: 0.8em; color: #475569; padding: 2px 0; }
190
+ .superpower { color: #d97706; font-weight: 600; font-size: 0.85em; margin: 6px 0; }
187
191
 
188
- .tips { margin-top: 12px; border-top: 1px solid #475569; padding-top: 12px; }
189
- .tip-row { display: flex; gap: 4px; font-size: 0.8em; margin: 4px 0; color: #cbd5e1; min-height: 44px; align-items: center; }
192
+ .tips { margin-top: 12px; border-top: 1px solid #e2e8f0; padding-top: 12px; }
193
+ .tip-row { display: flex; gap: 4px; font-size: 0.8em; margin: 4px 0; color: #475569; min-height: 44px; align-items: center; }
190
194
  .tip-icon { width: 20px; text-align: center; }
191
- .tip-label { color: #64748b; min-width: 55px; }
195
+ .tip-label { color: #94a3b8; min-width: 55px; }
192
196
 
193
197
  /* Modal */
194
- .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 100; justify-content: center; align-items: center; padding: 20px; }
198
+ .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); z-index: 100; justify-content: center; align-items: center; padding: 20px; }
195
199
  .modal-overlay.active { display: flex; }
196
200
  .modal {
197
- background: #1e293b; border-radius: 16px; max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto; padding: 32px;
198
- border: 1px solid #475569;
201
+ background: #ffffff; border-radius: 16px; max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto; padding: 32px;
202
+ border: 1px solid #e2e8f0; box-shadow: 0 25px 50px rgba(0,0,0,0.15);
199
203
  -webkit-overflow-scrolling: touch;
200
204
  }
201
205
  .modal-close { float: right; background: none; border: none; color: #94a3b8; font-size: 1.5em; cursor: pointer; min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
202
- .modal h2 { color: #38bdf8; margin: 16px 0 8px; font-size: 1.1em; }
203
- .modal p, .modal li { color: #cbd5e1; font-size: 0.9em; line-height: 1.6; }
206
+ .modal h2 { color: #2563eb; margin: 16px 0 8px; font-size: 1.1em; }
207
+ .modal p, .modal li { color: #475569; font-size: 0.9em; line-height: 1.6; }
204
208
  .modal ul { padding-left: 20px; }
205
209
 
206
210
  /* Mobile: screens < 480px */
@@ -262,7 +266,7 @@ function radarSvg(scores, size = 200) {
262
266
  const angle = (Math.PI * 2 * i / n) - Math.PI / 2;
263
267
  pts.push(\`\${cx + lr * Math.cos(angle)},\${cy + lr * Math.sin(angle)}\`);
264
268
  }
265
- gridLines += \`<polygon points="\${pts.join(' ')}" fill="none" stroke="#334155" stroke-width="0.5"/>\`;
269
+ gridLines += \`<polygon points="\${pts.join(' ')}" fill="none" stroke="#e2e8f0" stroke-width="0.5"/>\`;
266
270
  }
267
271
 
268
272
  let axes = '', labels = '', dataPoints = [];
@@ -270,7 +274,7 @@ function radarSvg(scores, size = 200) {
270
274
  const angle = (Math.PI * 2 * i / n) - Math.PI / 2;
271
275
  const x = cx + r * Math.cos(angle);
272
276
  const y = cy + r * Math.sin(angle);
273
- axes += \`<line x1="\${cx}" y1="\${cy}" x2="\${x}" y2="\${y}" stroke="#334155" stroke-width="0.5"/>\`;
277
+ axes += \`<line x1="\${cx}" y1="\${cy}" x2="\${x}" y2="\${y}" stroke="#e2e8f0" stroke-width="0.5"/>\`;
274
278
 
275
279
  const lx = cx + (r + 22) * Math.cos(angle);
276
280
  const ly = cy + (r + 22) * Math.sin(angle);
@@ -283,7 +287,7 @@ function radarSvg(scores, size = 200) {
283
287
  dataPoints.push(\`\${dx},\${dy}\`);
284
288
  }
285
289
 
286
- const dataPolygon = \`<polygon points="\${dataPoints.join(' ')}" fill="rgba(56,189,248,0.2)" stroke="#38bdf8" stroke-width="1.5"/>\`;
290
+ const dataPolygon = \`<polygon points="\${dataPoints.join(' ')}" fill="rgba(37,99,235,0.15)" stroke="#2563eb" stroke-width="1.5"/>\`;
287
291
 
288
292
  return \`<svg viewBox="0 0 \${size} \${size}" class="radar">\${gridLines}\${axes}\${dataPolygon}\${labels}</svg>\`;
289
293
  }
@@ -292,17 +296,20 @@ function renderCard(card, index) {
292
296
  const scores = card.scores || {};
293
297
  const radar = radarSvg(scores);
294
298
 
299
+ const twitterUrl = card.twitter || (card.slug ? \`https://unavatar.io/twitter/\${card.slug}\` : '');
300
+ const avatarUrl = \`https://unavatar.io/twitter/\${card.twitter_handle || card.slug || 'unknown'}\`;
301
+
295
302
  return \`
296
303
  <div class="card" onclick="openModal(\${index})">
297
304
  <div class="card-header">
298
- <div>
299
- <div class="card-name">\${card.name || 'Unknown'}</div>
300
- <div class="card-role">\${card.title || ''}\${card.company ? ' @ ' + card.company : ''}</div>
301
- </div>
302
- <div class="card-badges">
303
- <span class="badge badge-disc">\${card.disc || '?'}</span>
304
- <span class="badge badge-mbti">\${card.mbti || '?'}</span>
305
+ <div style="display:flex;gap:12px;align-items:center">
306
+ <img src="\${avatarUrl}" alt="" style="width:48px;height:48px;border-radius:50%;object-fit:cover;border:2px solid #e2e8f0" onerror="this.style.display='none'">
307
+ <div>
308
+ <div class="card-name">\${card.name || 'Unknown'}</div>
309
+ <div class="card-role">\${card.title || ''}\${card.company ? ' @ ' + card.company : ''}</div>
310
+ </div>
305
311
  </div>
312
+ <span class="badge badge-mbti">\${card.mbti || '?'}</span>
306
313
  </div>
307
314
  \${card.quote ? \`<div class="card-quote">"\${card.quote.slice(0, 120)}\${card.quote.length > 120 ? '...' : ''}"</div>\` : ''}
308
315
  <div class="radar-container">\${radar}</div>
@@ -321,14 +328,22 @@ function openModal(index) {
321
328
  const s = card.scores || {};
322
329
  const modal = document.getElementById('modal-content');
323
330
 
331
+ const avatarUrl = \`https://unavatar.io/twitter/\${card.twitter_handle || card.slug || 'unknown'}\`;
332
+
324
333
  modal.innerHTML = \`
325
334
  <button class="modal-close" onclick="closeModal()">&times;</button>
326
- <h1 style="color:#38bdf8;margin-bottom:4px">\${card.name}</h1>
327
- <p style="color:#94a3b8">\${card.title || ''}\${card.company ? ' @ ' + card.company : ''}\${card.location ? ' · ' + card.location : ''}</p>
335
+ <div style="display:flex;gap:16px;align-items:center;margin-bottom:12px">
336
+ <img src="\${avatarUrl}" alt="" style="width:64px;height:64px;border-radius:50%;object-fit:cover;border:2px solid #e2e8f0" onerror="this.style.display='none'">
337
+ <div>
338
+ <h1 style="color:#2563eb;margin-bottom:4px;font-size:1.5em">\${card.name}</h1>
339
+ <p style="color:#64748b">\${card.title || ''}\${card.company ? ' @ ' + card.company : ''}\${card.location ? ' · ' + card.location : ''}</p>
340
+ \${card.twitter_handle ? \`<a href="https://twitter.com/\${card.twitter_handle}" target="_blank" style="color:#2563eb;font-size:0.85em;text-decoration:none">@\${card.twitter_handle}</a>\` : ''}
341
+ </div>
342
+ </div>
328
343
  \${card.quote ? \`<div class="card-quote" style="margin:16px 0">"\${card.quote}"</div>\` : ''}
329
344
 
330
345
  <h2>Personality</h2>
331
- <p><strong>DISC:</strong> \${card.disc || '?'} &nbsp; <strong>MBTI:</strong> \${card.mbti || '?'}</p>
346
+ <p><strong>MBTI:</strong> \${card.mbti || '?'}</p>
332
347
  <div style="display:flex;justify-content:center;margin:16px 0">\${radarSvg(s, 260)}</div>
333
348
 
334
349
  \${card.expertise?.length ? \`<h2>Expertise</h2><ul>\${card.expertise.map(e => \`<li>\${e}</li>\`).join('')}</ul>\` : ''}
@@ -342,7 +357,7 @@ function openModal(index) {
342
357
 
343
358
  \${card.tags?.length ? \`<h2>Tags</h2><div class="tags">\${card.tags.map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
344
359
 
345
- \${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>\` : ''}
360
+ \${card.slug ? \`<p style="margin-top:20px;text-align:center"><a href="\${card.slug}.html" style="color:#2563eb;text-decoration:none;padding:8px 20px;border:1px solid #2563eb;border-radius:8px;display:inline-block">View Full Profile →</a></p>\` : ''}
346
361
  \`;
347
362
 
348
363
  document.getElementById('modal').classList.add('active');
@@ -33,7 +33,7 @@ function copilotAvailable() {
33
33
  }
34
34
  }
35
35
 
36
- function callClaudeCli(prompt, timeout = 120000) {
36
+ function callClaudeCli(prompt, timeout = 300000) {
37
37
  // Write prompt to temp file, pass via -p flag reading from file
38
38
  const tmpFile = path.join(os.tmpdir(), `vip-prompt-${Date.now()}.txt`);
39
39
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {