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/skill/vip.md ADDED
@@ -0,0 +1,96 @@
1
+ You are a VIP Profile Manager assistant. You use the `vip` CLI tool at `/Users/iris/Library/Python/3.9/bin/vip` for data management, and YOU (Claude) provide the intelligence layer — synthesizing, analyzing, and enriching profiles.
2
+
3
+ The user's request is: $ARGUMENTS
4
+
5
+ ## Architecture
6
+
7
+ - **`vip` CLI** = pure data tool (fetch, store, list, search, edit, delete). No AI inside.
8
+ - **You (Claude)** = the smart layer. You read raw data from profiles, synthesize structured content, answer questions, and write polished profiles.
9
+
10
+ ## Available CLI Commands
11
+
12
+ ```
13
+ vip add "Name" --company "Company" # Fetch public data, save raw profile
14
+ vip add <twitter_url> # Fetch from Twitter URL
15
+ vip list # List all profiles
16
+ vip show <name> # Display a profile
17
+ vip search <keyword> # Search across profiles
18
+ vip edit <name> --title/--company/--twitter/--linkedin/--note # Edit fields
19
+ vip update <name> # Re-fetch data
20
+ vip rm <name> -y # Delete a profile
21
+ vip raw <name> # Show raw gathered data
22
+ vip digest # Recent changes
23
+ vip monitor start/stop/status/run # Auto-monitoring
24
+ ```
25
+
26
+ ## Workflow for Adding a Person
27
+
28
+ 1. Run `vip add "Name" --company "Company"` to gather raw data
29
+ 2. Run `vip show <name>` to read the raw profile
30
+ 3. **You synthesize**: Read the raw data, then rewrite the profile into a polished, structured format with:
31
+ - Proper summary line
32
+ - Filled-in Basic Info (title, company, location, industry)
33
+ - Bio (2-3 paragraphs)
34
+ - Key interests, achievements, recent activity
35
+ - Background, personal info
36
+ 4. Write the polished profile back using the Edit tool to the file at `~/Projects/vip-crm/profiles/<slug>.md`
37
+
38
+ ## Workflow for Answering Questions
39
+
40
+ - If the user asks about a person: run `vip show` or `vip search`, then answer based on the profile content
41
+ - If they ask to compare people: read multiple profiles and synthesize a comparison
42
+ - If they ask "who do I know in AI?": run `vip search "AI"` and summarize
43
+
44
+ ## Profile Format (your output when synthesizing)
45
+
46
+ ```markdown
47
+ # {Full Name}
48
+
49
+ > {One-line summary}
50
+
51
+ ## Basic Info
52
+ - **Title:** {role}
53
+ - **Company:** {company}
54
+ - **Location:** {city, country}
55
+ - **Industry:** {domain}
56
+
57
+ ## Links
58
+ - Twitter: {url}
59
+ - LinkedIn: {url}
60
+ - Website: {url}
61
+
62
+ ## Bio
63
+ {2-3 paragraph biography}
64
+
65
+ ## Key Interests & Topics
66
+ - {topic 1}
67
+ - {topic 2}
68
+
69
+ ## Notable Achievements
70
+ - {achievement 1}
71
+ - {achievement 2}
72
+
73
+ ## Recent Activity
74
+ - {recent news/posts}
75
+
76
+ ## Background
77
+ {education, career history}
78
+
79
+ ## Personal
80
+ {family, hobbies — only if publicly known}
81
+
82
+ ## Notes
83
+ {any extra observations}
84
+
85
+ ---
86
+ *Last updated: {date}*
87
+ *Sources: {urls}*
88
+ ```
89
+
90
+ ## Rules
91
+
92
+ - Always use the full path `/Users/iris/Library/Python/3.9/bin/vip` for CLI commands
93
+ - Only include information supported by the raw data — do not fabricate
94
+ - If a section has no data, write "No public information found."
95
+ - For batch operations (adding multiple people), process them one at a time
96
+ - After synthesizing a profile, write it directly to the .md file
@@ -0,0 +1,21 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { parseTweets } from '../lib/fetchers/twitter.js';
4
+
5
+ describe('parseTweets', () => {
6
+ it('filters separators and metadata', () => {
7
+ const output = 'Hello world tweet\n─────────\n2024-01-01\n5 likes\nAnother tweet here\n';
8
+ const tweets = parseTweets(output);
9
+ assert.ok(tweets.includes('Hello world tweet'));
10
+ assert.ok(tweets.includes('Another tweet here'));
11
+ assert.strictEqual(tweets.length, 2);
12
+ });
13
+
14
+ it('empty input', () => {
15
+ assert.deepStrictEqual(parseTweets(''), []);
16
+ });
17
+
18
+ it('short lines ignored', () => {
19
+ assert.deepStrictEqual(parseTweets('hi\n'), []);
20
+ });
21
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { extractMetadata } from '../lib/monitor.js';
4
+
5
+ describe('extractMetadata', () => {
6
+ it('extracts twitter', () => {
7
+ const meta = extractMetadata('# Test\n\n- Twitter: https://twitter.com/testuser\n');
8
+ assert.strictEqual(meta.twitterHandle, 'testuser');
9
+ assert.strictEqual(meta.name, 'Test');
10
+ });
11
+
12
+ it('extracts x.com', () => {
13
+ const meta = extractMetadata('# Test\n\n- Twitter: https://x.com/testuser\n');
14
+ assert.strictEqual(meta.twitterHandle, 'testuser');
15
+ });
16
+
17
+ it('extracts linkedin', () => {
18
+ const meta = extractMetadata('# Test\n\n- LinkedIn: https://linkedin.com/in/test-user\n');
19
+ assert.strictEqual(meta.linkedinUrl, 'https://linkedin.com/in/test-user');
20
+ });
21
+
22
+ it('handles empty', () => {
23
+ const meta = extractMetadata('No links here');
24
+ assert.strictEqual(meta.twitterHandle, null);
25
+ assert.strictEqual(meta.linkedinUrl, null);
26
+ assert.strictEqual(meta.name, null);
27
+ });
28
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { slugify, saveProfile, loadProfile, listProfiles, searchProfiles, profileExists, deleteProfile } from '../lib/profile.js';
7
+
8
+ let tmpDir;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vip-test-'));
12
+ });
13
+
14
+ describe('slugify', () => {
15
+ it('basic name', () => assert.strictEqual(slugify('Sam Altman'), 'sam-altman'));
16
+ it('special chars', () => assert.strictEqual(slugify("Dr. John O'Brien"), 'dr-john-obrien'));
17
+ it('extra spaces', () => assert.strictEqual(slugify(' Elon Musk '), 'elon-musk'));
18
+ });
19
+
20
+ describe('save and load', () => {
21
+ it('round-trips content', () => {
22
+ saveProfile('Test Person', '# Test\nContent', tmpDir);
23
+ assert.strictEqual(loadProfile('Test Person', tmpDir), '# Test\nContent');
24
+ });
25
+
26
+ it('returns null for missing', () => {
27
+ assert.strictEqual(loadProfile('Nobody', tmpDir), null);
28
+ });
29
+
30
+ it('fuzzy matches', () => {
31
+ saveProfile('Sam Altman', '# Sam Altman\nContent', tmpDir);
32
+ const result = loadProfile('sam', tmpDir);
33
+ assert.ok(result?.includes('Sam Altman'));
34
+ });
35
+ });
36
+
37
+ describe('listProfiles', () => {
38
+ it('empty dir', () => assert.deepStrictEqual(listProfiles(tmpDir), []));
39
+
40
+ it('lists profiles', () => {
41
+ saveProfile('Sam Altman', '# Sam Altman\n\n> CEO of OpenAI', tmpDir);
42
+ saveProfile('Elon Musk', '# Elon Musk\n\n> CEO of Tesla', tmpDir);
43
+ const profiles = listProfiles(tmpDir);
44
+ assert.strictEqual(profiles.length, 2);
45
+ assert.ok(profiles.some(p => p.name === 'Sam Altman'));
46
+ assert.ok(profiles.some(p => p.name === 'Elon Musk'));
47
+ });
48
+
49
+ it('extracts summary', () => {
50
+ saveProfile('Test', '# Test\n\n> Summary here', tmpDir);
51
+ assert.strictEqual(listProfiles(tmpDir)[0].summary, 'Summary here');
52
+ });
53
+ });
54
+
55
+ describe('searchProfiles', () => {
56
+ it('finds matches', () => {
57
+ saveProfile('Sam', '# Sam\nOpenAI CEO', tmpDir);
58
+ saveProfile('Elon', '# Elon\nTesla CEO', tmpDir);
59
+ assert.strictEqual(searchProfiles('OpenAI', tmpDir).length, 1);
60
+ });
61
+
62
+ it('case insensitive', () => {
63
+ saveProfile('Sam', '# Sam\nOpenAI', tmpDir);
64
+ assert.strictEqual(searchProfiles('openai', tmpDir).length, 1);
65
+ });
66
+
67
+ it('no match', () => {
68
+ saveProfile('Sam', '# Sam\nOpenAI', tmpDir);
69
+ assert.strictEqual(searchProfiles('nonexistent', tmpDir).length, 0);
70
+ });
71
+ });
72
+
73
+ describe('profileExists / deleteProfile', () => {
74
+ it('exists check', () => {
75
+ saveProfile('Test', 'content', tmpDir);
76
+ assert.ok(profileExists('Test', tmpDir));
77
+ assert.ok(!profileExists('Nobody', tmpDir));
78
+ });
79
+
80
+ it('delete', () => {
81
+ saveProfile('Test', 'content', tmpDir);
82
+ assert.ok(deleteProfile('Test', tmpDir));
83
+ assert.ok(!profileExists('Test', tmpDir));
84
+ });
85
+
86
+ it('delete missing returns false', () => {
87
+ assert.ok(!deleteProfile('Nobody', tmpDir));
88
+ });
89
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { parseTwitterHandle, parseLinkedinUrl, isUrl, resolveFromUrl } from '../lib/resolver.js';
4
+
5
+ describe('parseTwitterHandle', () => {
6
+ it('twitter.com', () => assert.strictEqual(parseTwitterHandle('https://twitter.com/elonmusk'), 'elonmusk'));
7
+ it('x.com', () => assert.strictEqual(parseTwitterHandle('https://x.com/sama'), 'sama'));
8
+ it('with @', () => assert.strictEqual(parseTwitterHandle('https://twitter.com/@test'), 'test'));
9
+ it('ignores search', () => assert.strictEqual(parseTwitterHandle('https://twitter.com/search'), null));
10
+ it('ignores explore', () => assert.strictEqual(parseTwitterHandle('https://twitter.com/explore'), null));
11
+ it('no match', () => assert.strictEqual(parseTwitterHandle('https://example.com'), null));
12
+ });
13
+
14
+ describe('parseLinkedinUrl', () => {
15
+ it('valid profile', () => {
16
+ assert.strictEqual(parseLinkedinUrl('https://www.linkedin.com/in/samaltman'), 'https://www.linkedin.com/in/samaltman');
17
+ });
18
+ it('with params', () => {
19
+ assert.strictEqual(parseLinkedinUrl('https://linkedin.com/in/samaltman?trk=x'), 'https://linkedin.com/in/samaltman');
20
+ });
21
+ it('not profile', () => {
22
+ assert.strictEqual(parseLinkedinUrl('https://linkedin.com/company/openai'), null);
23
+ });
24
+ });
25
+
26
+ describe('isUrl', () => {
27
+ it('https', () => assert.ok(isUrl('https://twitter.com/test')));
28
+ it('http', () => assert.ok(isUrl('http://example.com')));
29
+ it('twitter.com', () => assert.ok(isUrl('twitter.com/test')));
30
+ it('not url', () => assert.ok(!isUrl('Sam Altman')));
31
+ it('email not url', () => assert.ok(!isUrl('test@email.com')));
32
+ });
33
+
34
+ describe('resolveFromUrl', () => {
35
+ it('unknown URL', () => {
36
+ const p = resolveFromUrl('https://example.com/unknown');
37
+ assert.strictEqual(p.name, '');
38
+ assert.strictEqual(p.otherUrls.length, 1);
39
+ });
40
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { createPlist } from '../lib/scheduler.js';
4
+
5
+ describe('createPlist', () => {
6
+ it('contains interval', () => {
7
+ // This may fail if vip is not installed, skip gracefully
8
+ try {
9
+ const plist = createPlist(12);
10
+ assert.ok(plist.includes('<integer>43200</integer>'));
11
+ assert.ok(plist.includes('com.vip-crm.monitor'));
12
+ assert.ok(plist.includes('monitor'));
13
+ } catch (e) {
14
+ if (e.message.includes("Cannot find 'vip'")) {
15
+ // Expected in test env without global install
16
+ assert.ok(true);
17
+ } else {
18
+ throw e;
19
+ }
20
+ }
21
+ });
22
+ });