life-pulse 2.3.8 → 2.3.9
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/dist/cli.js +5 -3
- package/dist/crm.d.ts +1 -0
- package/dist/crm.js +22 -21
- package/dist/tui.js +12 -15
- package/dist/ui/app.js +6 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { renderIntro, renderCRMList, revealContacts, revealInsights, pickCard, r
|
|
|
10
10
|
import { needsDiscovery, discoverPlatforms, savePlatforms } from './platforms.js';
|
|
11
11
|
import { generateArchetype } from './archetype.js';
|
|
12
12
|
import { runPermissionFlow, getMissingPermissions } from './permissions.js';
|
|
13
|
-
import { buildCRM, streamEnrichedCRM, generateInsights } from './crm.js';
|
|
13
|
+
import { buildCRM, streamEnrichedCRM, generateInsights, isHumanContact } from './crm.js';
|
|
14
14
|
import { saveContactSummaries } from './intelligence.js';
|
|
15
15
|
import { getUserName } from './profile.js';
|
|
16
16
|
// Session progress tracking (Anthropic long-running agent pattern)
|
|
@@ -102,8 +102,10 @@ async function showCRM(apiKey, opts) {
|
|
|
102
102
|
onBrief: (b) => { brief = b; },
|
|
103
103
|
}));
|
|
104
104
|
const enrichedNames = new Set(enriched.map(t => t.name));
|
|
105
|
-
// Show remaining contacts (not enriched) — just names
|
|
106
|
-
const remaining = crm.threads
|
|
105
|
+
// Show remaining human contacts (not enriched) — just names
|
|
106
|
+
const remaining = crm.threads
|
|
107
|
+
.filter(t => !enrichedNames.has(t.name) && isHumanContact(t.name))
|
|
108
|
+
.slice(0, 5);
|
|
107
109
|
if (remaining.length)
|
|
108
110
|
renderCRMList(remaining);
|
|
109
111
|
if (brief)
|
package/dist/crm.d.ts
CHANGED
package/dist/crm.js
CHANGED
|
@@ -26,7 +26,7 @@ const BUSINESS_PATTERNS = [
|
|
|
26
26
|
/\binc\.?\b/i, /\bllc\b/i, /\bcorp\b/i, /^doordash/i, /^uber/i,
|
|
27
27
|
/^lyft/i, /^venmo/i, /^cashapp/i, /^paypal/i, /^amazon/i,
|
|
28
28
|
];
|
|
29
|
-
function isHumanContact(name) {
|
|
29
|
+
export function isHumanContact(name) {
|
|
30
30
|
if (!name || name.length < 3)
|
|
31
31
|
return false;
|
|
32
32
|
// Raw phone numbers
|
|
@@ -368,7 +368,16 @@ async function enrichContact(t) {
|
|
|
368
368
|
max_tokens: 100,
|
|
369
369
|
messages: [{
|
|
370
370
|
role: 'user',
|
|
371
|
-
content:
|
|
371
|
+
content: `You are briefing the person who sent the → messages. "${t.name}" sent the ← messages.${tag}
|
|
372
|
+
|
|
373
|
+
One sentence only. What's the ONE thing happening right now between them. Be terse — 15 words max. Use the specific detail from the messages (names, places, times). Don't explain the relationship. Don't say "you" or "they" — just state the situation like a ticker.
|
|
374
|
+
|
|
375
|
+
Good: "Sunday for Harrison in Woodside — 9pm fell through."
|
|
376
|
+
Good: "8:30 tonight, Lunar New Year party with NOX crew."
|
|
377
|
+
Good: "Waiting on FDM1 — he's shipping the post Monday then scaling up."
|
|
378
|
+
Bad: "You have a casual friendship where you coordinate plans." (too abstract)
|
|
379
|
+
Bad: "Molly wants to visit you in Woodside." (wrong voice — never address ${t.name})
|
|
380
|
+
|
|
372
381
|
Priority: "work", "personal", or "service" (businesses/bots/automated).
|
|
373
382
|
|
|
374
383
|
${snippets}
|
|
@@ -380,7 +389,9 @@ JSON only: {"summary": "...", "priority": "work|personal|service"}`,
|
|
|
380
389
|
if (!res.ok)
|
|
381
390
|
return null;
|
|
382
391
|
const data = await res.json();
|
|
383
|
-
|
|
392
|
+
// Strip markdown fences (```json ... ```) before parsing
|
|
393
|
+
const raw = data.content?.[0]?.text?.trim() || '';
|
|
394
|
+
const content = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '');
|
|
384
395
|
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
385
396
|
if (!jsonMatch)
|
|
386
397
|
return { relationship: content.slice(0, 300), priority: 'personal' };
|
|
@@ -518,28 +529,18 @@ export async function* generateInsights(crm) {
|
|
|
518
529
|
if (!ENRICH_KEY || crm.threads.length < 3)
|
|
519
530
|
return;
|
|
520
531
|
const s = extractSignals(crm);
|
|
532
|
+
// Determine chronotype from hour buckets without LLM
|
|
533
|
+
const peakHour = s.hourBuckets.indexOf(Math.max(...s.hourBuckets));
|
|
534
|
+
const chronotype = peakHour < 10 ? 'morning person' : peakHour < 16 ? 'afternoon person' : 'night owl';
|
|
521
535
|
const prompts = [
|
|
522
536
|
{
|
|
523
537
|
label: 'chronotype',
|
|
524
|
-
prompt: `
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
{
|
|
529
|
-
label: 'response personality',
|
|
530
|
-
prompt: `This person has ${s.withResponse.length} contacts they regularly text.
|
|
531
|
-
Fast replies (<10 min avg): ${s.fastResponders.map(t => t.name).join(', ') || 'none'}
|
|
532
|
-
Slow replies (>1 day avg): ${s.slowResponders.map(t => t.name).join(', ') || 'none'}
|
|
533
|
-
Overall avg response: ${s.yourAvgSec ? fmtTime(s.yourAvgSec) : 'unknown'}
|
|
534
|
-
|
|
535
|
-
Write ONE sentence about their response patterns. Name names. Be direct. Address them as "you".`,
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
label: 'inner circle',
|
|
539
|
-
prompt: `This person's relationship map: ${s.hot} hot (daily), ${s.warm} warm (every few days), ${s.cooling} cooling (weekly), ${s.cold} cold (dormant).
|
|
540
|
-
Top 3 by volume: ${s.threads.slice(0, 3).map(t => `${t.name} (${t.msgs30d} msgs/mo)`).join(', ')}
|
|
538
|
+
prompt: `This person is a ${chronotype} (peak messaging around ${peakHour > 12 ? peakHour - 12 : peakHour}${peakHour >= 12 ? 'pm' : 'am'}).
|
|
539
|
+
Top 3 people: ${s.threads.slice(0, 3).map(t => t.name).join(', ')}.
|
|
540
|
+
${s.fastResponders.length ? `Quick with: ${s.fastResponders.slice(0, 3).map(t => t.name).join(', ')}.` : ''}
|
|
541
|
+
${s.slowResponders.length ? `Slow with: ${s.slowResponders.slice(0, 2).map(t => t.name).join(', ')}.` : ''}
|
|
541
542
|
|
|
542
|
-
|
|
543
|
+
One sentence. Something an aide who knows them well would notice. No numbers, no statistics, no counts. Address as "you".`,
|
|
543
544
|
},
|
|
544
545
|
];
|
|
545
546
|
const indexed = prompts.map((p, i) => callInsight(p.prompt)
|
package/dist/tui.js
CHANGED
|
@@ -50,23 +50,20 @@ const WMO = {
|
|
|
50
50
|
};
|
|
51
51
|
async function fetchLocationWeather() {
|
|
52
52
|
try {
|
|
53
|
-
// 1.
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
53
|
+
// 1. Actual city from IP geolocation (not timezone)
|
|
54
|
+
const ipR = await fetch('https://ipinfo.io/json', { signal: AbortSignal.timeout(3000) });
|
|
55
|
+
const ip = await ipR.json();
|
|
56
|
+
const city = ip.city;
|
|
57
|
+
const [lat, lon] = (ip.loc || '').split(',');
|
|
58
|
+
if (!city || !lat || !lon)
|
|
57
59
|
return null;
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const place = geo.results?.[0];
|
|
61
|
-
if (!place)
|
|
62
|
-
return null;
|
|
63
|
-
// 2. Current weather from coordinates — no API key
|
|
64
|
-
const wxR = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}&longitude=${place.longitude}¤t=temperature_2m,weather_code&temperature_unit=fahrenheit`, { signal: AbortSignal.timeout(3000) });
|
|
60
|
+
// 2. Current weather from real coordinates
|
|
61
|
+
const wxR = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code&temperature_unit=fahrenheit`, { signal: AbortSignal.timeout(3000) });
|
|
65
62
|
const wx = await wxR.json();
|
|
66
63
|
const temp = Math.round(wx.current?.temperature_2m ?? 0);
|
|
67
64
|
const code = wx.current?.weather_code ?? -1;
|
|
68
65
|
const sky = WMO[code] || 'clear';
|
|
69
|
-
return { city
|
|
66
|
+
return { city, weather: `${temp}° and ${sky}` };
|
|
70
67
|
}
|
|
71
68
|
catch {
|
|
72
69
|
return null;
|
|
@@ -141,7 +138,7 @@ export async function renderIntro(name) {
|
|
|
141
138
|
const rows = process.stdout.rows || 24;
|
|
142
139
|
// Push old content out of scrollback
|
|
143
140
|
process.stdout.write('\n'.repeat(rows * 2));
|
|
144
|
-
process.stdout.write('\
|
|
141
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
145
142
|
process.stdout.write('\x1B[?25l');
|
|
146
143
|
// === Phase 1: Geometric scan field ===
|
|
147
144
|
const field = renderScanField(cols, rows);
|
|
@@ -151,7 +148,7 @@ export async function renderIntro(name) {
|
|
|
151
148
|
}
|
|
152
149
|
await _sleep(350);
|
|
153
150
|
// === Phase 2: Greeting ===
|
|
154
|
-
process.stdout.write('\
|
|
151
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
155
152
|
process.stdout.write('\n');
|
|
156
153
|
// "Hey Molly."
|
|
157
154
|
const hey = firstName ? `Hey ${firstName}.` : 'Hey.';
|
|
@@ -194,7 +191,7 @@ export function renderGreeting(name) {
|
|
|
194
191
|
ink.log(` ${C.hd(name ? `Hey ${name}` : 'Hey')}`);
|
|
195
192
|
}
|
|
196
193
|
else {
|
|
197
|
-
process.stdout.write('\
|
|
194
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
198
195
|
console.log();
|
|
199
196
|
console.log(` ${HD(name ? `Hey ${name}` : 'Hey')}`);
|
|
200
197
|
}
|
package/dist/ui/app.js
CHANGED
|
@@ -52,11 +52,14 @@ const App = () => {
|
|
|
52
52
|
_update = (fn) => setState(fn);
|
|
53
53
|
return () => { _update = null; };
|
|
54
54
|
}, []);
|
|
55
|
-
//
|
|
55
|
+
// Only tick when something is animating (spinner, progress, card)
|
|
56
|
+
const needsAnim = !!(state.spinner || state.progress || state.card);
|
|
56
57
|
useEffect(() => {
|
|
58
|
+
if (!needsAnim)
|
|
59
|
+
return;
|
|
57
60
|
const t = setInterval(() => setFrame(f => f + 1), 80);
|
|
58
61
|
return () => clearInterval(t);
|
|
59
|
-
}, []);
|
|
62
|
+
}, [needsAnim]);
|
|
60
63
|
// Input
|
|
61
64
|
useInput((input, key) => {
|
|
62
65
|
const s = stateRef.current;
|
|
@@ -101,7 +104,7 @@ function ensureStarted() {
|
|
|
101
104
|
if (_started)
|
|
102
105
|
return;
|
|
103
106
|
_started = true;
|
|
104
|
-
process.stdout.write('\
|
|
107
|
+
process.stdout.write('\x1B[2J\x1B[H'); // clear screen
|
|
105
108
|
process.stdout.write('\x1B[?25l'); // hide cursor
|
|
106
109
|
_inst = inkRender(React.createElement(App));
|
|
107
110
|
// Seed initial lines (e.g. greeting from intro)
|