vipcare 0.3.0 → 0.3.1
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 +8 -1
- package/bin/vip.js +48 -42
- package/lib/fetchers/twitter.js +0 -13
- package/lib/profile.js +8 -0
- package/lib/synthesizer.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,9 @@ vip update sam-altman
|
|
|
46
46
|
|
|
47
47
|
| Command | Description |
|
|
48
48
|
|---------|-------------|
|
|
49
|
+
| `vip init` | Interactive setup |
|
|
49
50
|
| `vip add <name-or-url>` | Add a new profile (`-c` company, `-f` force, `--no-ai`, `--dry-run`, `-y` YouTube URLs) |
|
|
50
|
-
| `vip list` | List all profiles |
|
|
51
|
+
| `vip list` | List all profiles (`--tag` filter by tag) |
|
|
51
52
|
| `vip show <name>` | Display a profile |
|
|
52
53
|
| `vip search <keyword>` | Search across all profiles |
|
|
53
54
|
| `vip edit <name>` | Edit profile fields (`--title`, `--company`, `--twitter`, `--linkedin`, `--note`) |
|
|
@@ -56,6 +57,12 @@ vip update sam-altman
|
|
|
56
57
|
| `vip open <name>` | Open a profile in your editor |
|
|
57
58
|
| `vip youtube <name> <url>` | Add YouTube video transcript to a profile |
|
|
58
59
|
| `vip youtube-search <name>` | Search YouTube for a person's talks (`-n` max results) |
|
|
60
|
+
| `vip compare <n1> <n2>` | Side-by-side comparison (`--json`) |
|
|
61
|
+
| `vip tag <name> <tag>` | Add tag to profile |
|
|
62
|
+
| `vip untag <name> <tag>` | Remove tag from profile |
|
|
63
|
+
| `vip tags [name]` | List tags (`--json`) |
|
|
64
|
+
| `vip stats` | Show dashboard overview (`--json`) |
|
|
65
|
+
| `vip regenerate` | Re-synthesize all profiles (`--dry-run`, `--no-ai`) |
|
|
59
66
|
| `vip card` | Generate H5 baseball card page (`-o` output path) |
|
|
60
67
|
| `vip export` | Export all profiles for backup |
|
|
61
68
|
| `vip import` | Restore profiles from backup |
|
package/bin/vip.js
CHANGED
|
@@ -6,7 +6,7 @@ import os from 'os';
|
|
|
6
6
|
import readline from 'readline/promises';
|
|
7
7
|
import { execFileSync } from 'child_process';
|
|
8
8
|
import { checkTool, getProfilesDir, loadConfig, saveConfig } from '../lib/config.js';
|
|
9
|
-
import { deleteProfile, getProfilePath, listProfiles, loadProfile, profileExists, saveProfile, searchProfiles, slugify } from '../lib/profile.js';
|
|
9
|
+
import { deleteProfile, getProfilePath, listProfiles, loadProfile, parseTags, profileExists, saveProfile, searchProfiles, slugify } from '../lib/profile.js';
|
|
10
10
|
import { isUrl, resolveFromName, resolveFromUrl } from '../lib/resolver.js';
|
|
11
11
|
import * as twitter from '../lib/fetchers/twitter.js';
|
|
12
12
|
import { searchPerson } from '../lib/fetchers/search.js';
|
|
@@ -76,7 +76,7 @@ try {
|
|
|
76
76
|
} catch {}
|
|
77
77
|
|
|
78
78
|
const program = new Command();
|
|
79
|
-
program.name('vip').description('VIP Profile Builder - Auto-build VIP person profiles from public data').version('0.3.
|
|
79
|
+
program.name('vip').description('VIP Profile Builder - Auto-build VIP person profiles from public data').version('0.3.1');
|
|
80
80
|
|
|
81
81
|
// --- add ---
|
|
82
82
|
program.command('add')
|
|
@@ -156,6 +156,10 @@ program.command('add')
|
|
|
156
156
|
const stop = spinner('Synthesizing profile with AI...');
|
|
157
157
|
try {
|
|
158
158
|
profile = await synthesizeProfile(rawData, sources);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error(c.red(`AI synthesis failed: ${e.message}`));
|
|
161
|
+
console.error(c.dim('Use --no-ai to save raw data without synthesis.'));
|
|
162
|
+
process.exit(1);
|
|
159
163
|
} finally { stop(); }
|
|
160
164
|
}
|
|
161
165
|
|
|
@@ -187,9 +191,7 @@ program.command('list')
|
|
|
187
191
|
profiles = profiles.filter(p => {
|
|
188
192
|
const content = loadProfile(p.slug);
|
|
189
193
|
if (!content) return false;
|
|
190
|
-
const
|
|
191
|
-
if (!tagsMatch) return false;
|
|
192
|
-
const tags = tagsMatch[1].split('\n').filter(l => l.match(/^- /)).map(l => l.replace(/^- /, '').trim());
|
|
194
|
+
const tags = parseTags(content);
|
|
193
195
|
return tags.includes(opts.tag);
|
|
194
196
|
});
|
|
195
197
|
}
|
|
@@ -306,7 +308,13 @@ program.command('update')
|
|
|
306
308
|
profile = `# ${personName}\n\n## Raw Data\n\n${rawData}`;
|
|
307
309
|
} else {
|
|
308
310
|
const stop2 = spinner('Re-synthesizing profile...');
|
|
309
|
-
try {
|
|
311
|
+
try {
|
|
312
|
+
profile = await synthesizeProfile(rawData, sources);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error(c.red(`AI synthesis failed: ${e.message}`));
|
|
315
|
+
console.error(c.dim('Use --no-ai to save raw data without synthesis.'));
|
|
316
|
+
process.exit(1);
|
|
317
|
+
} finally { stop2(); }
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
const filepath = saveProfile(personName, profile);
|
|
@@ -388,13 +396,7 @@ program.command('edit')
|
|
|
388
396
|
modified = true;
|
|
389
397
|
}
|
|
390
398
|
if (opts.note) {
|
|
391
|
-
|
|
392
|
-
content = content.replace('## Notes\n', `## Notes\n- ${opts.note}\n`);
|
|
393
|
-
} else if (content.includes('\n---\n')) {
|
|
394
|
-
content = content.replace('\n---\n', `\n## Notes\n- ${opts.note}\n\n---\n`);
|
|
395
|
-
} else {
|
|
396
|
-
content = content.trimEnd() + `\n\n## Notes\n- ${opts.note}\n`;
|
|
397
|
-
}
|
|
399
|
+
content = appendNote(content, opts.note);
|
|
398
400
|
modified = true;
|
|
399
401
|
}
|
|
400
402
|
|
|
@@ -407,7 +409,8 @@ program.command('youtube')
|
|
|
407
409
|
.description('Add YouTube video transcript to existing profile')
|
|
408
410
|
.argument('<name>', 'Profile name')
|
|
409
411
|
.argument('<url>', 'YouTube video URL')
|
|
410
|
-
.
|
|
412
|
+
.option('--no-ai', 'Skip AI synthesis')
|
|
413
|
+
.action(async (name, url, opts) => {
|
|
411
414
|
const content = loadProfile(name);
|
|
412
415
|
if (!content) { console.error(c.red(`Profile not found: ${name}`)); process.exit(1); }
|
|
413
416
|
|
|
@@ -428,9 +431,19 @@ program.command('youtube')
|
|
|
428
431
|
const rawData = content + `\n\n=== YouTube Video: ${yt.title} (${yt.url}) ===\n${yt.transcript}`;
|
|
429
432
|
const sources = [yt.url];
|
|
430
433
|
|
|
431
|
-
const stop2 = spinner('Re-synthesizing profile...');
|
|
432
434
|
let profile;
|
|
433
|
-
|
|
435
|
+
if (opts.ai === false) {
|
|
436
|
+
profile = `# ${meta.name || name}\n\n## Raw Data\n\n${rawData}`;
|
|
437
|
+
} else {
|
|
438
|
+
const stop2 = spinner('Re-synthesizing profile...');
|
|
439
|
+
try {
|
|
440
|
+
profile = await synthesizeProfile(rawData, sources);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error(c.red(`AI synthesis failed: ${e.message}`));
|
|
443
|
+
console.error(c.dim('Use --no-ai to save raw data without synthesis.'));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
} finally { stop2(); }
|
|
446
|
+
}
|
|
434
447
|
|
|
435
448
|
const filepath = saveProfile(meta.name || name, profile);
|
|
436
449
|
console.log(c.green(`Profile updated: ${filepath}`));
|
|
@@ -474,8 +487,13 @@ program.command('card')
|
|
|
474
487
|
// --- digest ---
|
|
475
488
|
program.command('digest')
|
|
476
489
|
.description('Show recent changes')
|
|
477
|
-
.
|
|
490
|
+
.option('--json', 'Output as JSON')
|
|
491
|
+
.action((opts) => {
|
|
478
492
|
const entries = readChangelog(30);
|
|
493
|
+
if (opts.json) {
|
|
494
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
479
497
|
if (!entries.length) { console.log(c.dim('No recent changes.')); return; }
|
|
480
498
|
|
|
481
499
|
console.log(c.bold(c.cyan('Changes in the last 30 days:\n')));
|
|
@@ -538,15 +556,7 @@ program.command('compare')
|
|
|
538
556
|
const mbtiMatch = content.match(/\*\*MBTI:\*\*\s*(.+)/);
|
|
539
557
|
const industryMatch = content.match(/\*\*Industry:\*\*\s*(.+)/);
|
|
540
558
|
|
|
541
|
-
|
|
542
|
-
const tags = [];
|
|
543
|
-
const tagsMatch = content.match(/## Tags\n([\s\S]*?)(?=\n##|\n---|$)/);
|
|
544
|
-
if (tagsMatch) {
|
|
545
|
-
for (const line of tagsMatch[1].split('\n')) {
|
|
546
|
-
const m = line.match(/^- (.+)/);
|
|
547
|
-
if (m) tags.push(m[1].trim());
|
|
548
|
-
}
|
|
549
|
-
}
|
|
559
|
+
const tags = parseTags(content);
|
|
550
560
|
if (industryMatch && !tags.length) tags.push(industryMatch[1].trim());
|
|
551
561
|
|
|
552
562
|
return {
|
|
@@ -627,7 +637,7 @@ program.command('tag')
|
|
|
627
637
|
const tagsMatch = content.match(/## Tags\n([\s\S]*?)(?=\n##|\n---|$)/);
|
|
628
638
|
|
|
629
639
|
if (tagsMatch) {
|
|
630
|
-
const existingTags =
|
|
640
|
+
const existingTags = parseTags(content);
|
|
631
641
|
if (existingTags.includes(tag)) {
|
|
632
642
|
console.log(c.yellow(`Tag '${tag}' already exists on ${name}.`));
|
|
633
643
|
return;
|
|
@@ -683,18 +693,6 @@ program.command('tags')
|
|
|
683
693
|
.argument('[name]', 'Profile name (optional)')
|
|
684
694
|
.option('--json', 'Output as JSON')
|
|
685
695
|
.action((name, opts) => {
|
|
686
|
-
function parseTags(content) {
|
|
687
|
-
const tags = [];
|
|
688
|
-
const tagsMatch = content.match(/## Tags\n([\s\S]*?)(?=\n##|\n---|$)/);
|
|
689
|
-
if (tagsMatch) {
|
|
690
|
-
for (const line of tagsMatch[1].split('\n')) {
|
|
691
|
-
const m = line.match(/^- (.+)/);
|
|
692
|
-
if (m) tags.push(m[1].trim());
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
return tags;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
696
|
if (name) {
|
|
699
697
|
const content = loadProfile(name);
|
|
700
698
|
if (!content) { console.error(c.red(`Profile not found: ${name}`)); process.exit(1); }
|
|
@@ -737,8 +735,16 @@ program.command('tags')
|
|
|
737
735
|
// --- config ---
|
|
738
736
|
program.command('config')
|
|
739
737
|
.description('View/edit settings')
|
|
740
|
-
.
|
|
738
|
+
.option('--json', 'Output as JSON')
|
|
739
|
+
.action((opts) => {
|
|
741
740
|
const cfg = loadConfig();
|
|
741
|
+
if (opts.json) {
|
|
742
|
+
console.log(JSON.stringify({
|
|
743
|
+
...cfg,
|
|
744
|
+
tools: { bird: checkTool('bird'), ai_backend: getBackendName() }
|
|
745
|
+
}, null, 2));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
742
748
|
console.log(c.bold(c.cyan('Current config:')));
|
|
743
749
|
console.log(` Profiles dir: ${cfg.profiles_dir}`);
|
|
744
750
|
console.log(` Monitor interval: ${cfg.monitor_interval_hours}h`);
|
|
@@ -767,7 +773,7 @@ program.command('init')
|
|
|
767
773
|
const backendAnswer = await rl.question(' > ');
|
|
768
774
|
const backendChoice = backendAnswer.trim() || '1';
|
|
769
775
|
|
|
770
|
-
const backendMap = { '1': 'auto', '2': 'claude-cli', '3': 'anthropic
|
|
776
|
+
const backendMap = { '1': 'auto', '2': 'claude-cli', '3': 'anthropic', '4': 'copilot-cli' };
|
|
771
777
|
const aiBackend = backendMap[backendChoice] || 'auto';
|
|
772
778
|
|
|
773
779
|
const config = {
|
|
@@ -775,7 +781,7 @@ program.command('init')
|
|
|
775
781
|
ai_backend: aiBackend,
|
|
776
782
|
};
|
|
777
783
|
|
|
778
|
-
if (aiBackend === 'anthropic
|
|
784
|
+
if (aiBackend === 'anthropic') {
|
|
779
785
|
const apiKey = await rl.question('\nAnthropic API key: ');
|
|
780
786
|
if (apiKey.trim()) {
|
|
781
787
|
config.anthropic_api_key = apiKey.trim();
|
package/lib/fetchers/twitter.js
CHANGED
|
@@ -59,16 +59,3 @@ export function fetchProfile(handle) {
|
|
|
59
59
|
return data;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export function fetchTweetsByUrl(url) {
|
|
63
|
-
if (!isAvailable()) return null;
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
return execFileSync('bird', ['read', url], {
|
|
67
|
-
encoding: 'utf-8',
|
|
68
|
-
timeout: 30000,
|
|
69
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
70
|
-
});
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
package/lib/profile.js
CHANGED
|
@@ -114,3 +114,11 @@ export function deleteProfile(name, profilesDir) {
|
|
|
114
114
|
}
|
|
115
115
|
return false;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
export function parseTags(content) {
|
|
119
|
+
const match = content.match(/## Tags\n([\s\S]*?)(?=\n##|\n---|$)/);
|
|
120
|
+
if (!match) return [];
|
|
121
|
+
return match[1].split('\n')
|
|
122
|
+
.map(line => line.replace(/^-\s*/, '').trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
}
|
package/lib/synthesizer.js
CHANGED
|
@@ -9,7 +9,7 @@ function getBackend() {
|
|
|
9
9
|
const config = loadConfig();
|
|
10
10
|
if (config.ai_backend) return config.ai_backend.toLowerCase();
|
|
11
11
|
|
|
12
|
-
if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
|
|
12
|
+
if (process.env.ANTHROPIC_API_KEY || config.anthropic_api_key) return 'anthropic';
|
|
13
13
|
if (checkTool('claude')) return 'claude-cli';
|
|
14
14
|
if (checkTool('gh') && copilotAvailable()) return 'copilot-cli';
|
|
15
15
|
|
|
@@ -48,8 +48,8 @@ async function callAnthropicApi(prompt) {
|
|
|
48
48
|
throw new Error('Anthropic SDK not installed. Run: npm install @anthropic-ai/sdk');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
52
|
-
if (!apiKey) throw new Error('ANTHROPIC_API_KEY
|
|
51
|
+
const apiKey = process.env.ANTHROPIC_API_KEY || loadConfig().anthropic_api_key;
|
|
52
|
+
if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set. Run "vip init" or set the environment variable.');
|
|
53
53
|
|
|
54
54
|
const config = loadConfig();
|
|
55
55
|
const model = config.anthropic_model || 'claude-sonnet-4-20250514';
|