vipcare 0.3.0 → 0.3.2

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
@@ -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.0');
79
+ program.name('vip').description('VIP Profile Builder - Auto-build VIP person profiles from public data').version('0.3.2');
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 tagsMatch = content.match(/## Tags\n([\s\S]*?)(?=\n##|\n---|$)/);
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 { profile = await synthesizeProfile(rawData, sources); } finally { stop2(); }
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
- if (content.includes('## Notes')) {
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
- .action(async (name, url) => {
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
- try { profile = await synthesizeProfile(rawData, sources); } finally { stop2(); }
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
- .action(() => {
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
- // Parse tags from ## Tags section
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 = tagsMatch[1].split('\n').filter(l => l.match(/^- /)).map(l => l.replace(/^- /, '').trim());
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
- .action(() => {
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-api', '4': 'github-copilot' };
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-api') {
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();
@@ -792,7 +798,23 @@ program.command('init')
792
798
 
793
799
  const { CONFIG_FILE: cfgPath } = await import('../lib/config.js');
794
800
  console.log(c.green(`\nConfig saved to ${cfgPath}`));
795
- console.log(`You're ready! Try: ${c.cyan('vip add "Sam Altman" --company "OpenAI"')}\n`);
801
+
802
+ // Install Claude Code skill
803
+ const skillSrc = new URL('../skill/vip.md', import.meta.url);
804
+ const skillDest = path.join(os.homedir(), '.claude', 'commands', 'vip.md');
805
+
806
+ if (fs.existsSync(new URL(skillSrc).pathname)) {
807
+ console.log(`\n${c.cyan('Claude Code /vip skill:')}`);
808
+ const installSkill = await rl.question(` Install /vip slash command for Claude Code? (Y/n) > `);
809
+ if (!installSkill.trim() || installSkill.trim().toLowerCase().startsWith('y')) {
810
+ fs.mkdirSync(path.dirname(skillDest), { recursive: true });
811
+ fs.copyFileSync(new URL(skillSrc).pathname, skillDest);
812
+ console.log(c.green(` Installed: ${skillDest}`));
813
+ console.log(c.dim(' Use /vip in Claude Code to manage profiles with natural language'));
814
+ }
815
+ }
816
+
817
+ console.log(`\nYou're ready! Try: ${c.cyan('vip add "Sam Altman" --company "OpenAI"')}\n`);
796
818
  } finally {
797
819
  rl.close();
798
820
  }
@@ -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
+ }
@@ -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 environment variable not set.');
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {