life-pulse 2.3.2 → 2.3.4

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/agent.js CHANGED
@@ -348,18 +348,17 @@ progress, onCard) {
348
348
  // User prompt
349
349
  const prompt = `Run the executor protocol. One shot. Tell me what actually matters.
350
350
 
351
- 1. SCAN (in order):
352
- - FIRST: get_claude_history(days=3), get_chatgpt_history
353
- - THEN: scan_sources, get_calendar(days_ahead=7), get_unanswered_messages, get_messages(days=7, limit=100), get_calls(missed_only=true)
354
- - For unknown contacts: search_emails(sender=NAME) before labeling unknown
351
+ 1. SCAN — call these in parallel:
352
+ get_claude_history(days=3), get_chatgpt_history, scan_sources, get_calendar(days_ahead=7), get_unanswered_messages, get_messages(days=7, limit=100), get_calls(missed_only=true)
353
+ For unknown contacts: search_emails(sender=NAME) before labeling unknown.
355
354
 
356
- 2. CHASE CONTEXT: Search for every plan, event, date, money reference across all conversations. Be relentless.
355
+ 2. STREAM as SOON as you identify an item, dispatch a worker AND present it:
356
+ - Dispatch a Task worker to investigate (draft reply, gather context, etc.)
357
+ - Immediately call ask_user with your best recommendation. Don't wait for other workers.
358
+ - ${name} picks inline while other workers run in parallel.
359
+ - CRITICAL: present the FIRST card within 30 seconds. Don't wait until all scanning is done.
357
360
 
358
- 3. DISPATCH WORKERS via Task tool for each item — one per item, parallelize. They have FULL capabilities.
359
-
360
- 4. For each decision, present via ask_user. ${name} picks inline.
361
-
362
- 5. SYNTHESIZE into final briefing.
361
+ 3. SYNTHESIZE after all cards are presented, output final JSON.
363
362
 
364
363
  Output JSON (no markdown, no code fences):
365
364
  {
@@ -374,7 +373,7 @@ Output JSON (no markdown, no code fences):
374
373
 
375
374
  RULES:
376
375
  - Promises/blockers: 2-3 options. First = recommendation. Description = draft text.
377
- - Present each decision via ask_user as you go. Don't batch.
376
+ - Present each decision via ask_user THE MOMENT you have enough context. Never batch.
378
377
  - Bumps: 1-2 options. Alpha: plain strings.
379
378
  - HARD LIMIT: 4 promises, 4 blockers, 3 bumps, 3 alpha, 3 handled.
380
379
  - Empty sections = fine. Short briefing = gift.`;
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { saveState, saveDecisions } from './state.js';
6
6
  // ProgressRenderer unused — removed
7
7
  import { InkProgress } from './ui/progress.js';
8
8
  import { addTodo, resolveTodos, pruneOld } from './todo.js';
9
- import { renderIntro, renderReveal, renderCRMList, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, RED, AMB, MID, DIM } from './tui.js';
9
+ import { renderIntro, renderReveal, renderCRMList, renderCRMContact, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, RED, AMB, MID, DIM } from './tui.js';
10
10
  import { needsDiscovery, discoverPlatforms, savePlatforms } from './platforms.js';
11
11
  import { generateArchetype } from './archetype.js';
12
12
  import { runPermissionFlow, getMissingPermissions } from './permissions.js';
@@ -97,22 +97,26 @@ async function showCRM(apiKey, opts) {
97
97
  innerCircle,
98
98
  aboutToBreak,
99
99
  });
100
- renderCRMList(crm.threads);
101
- // Enrich in background (for agent context + brief)
102
- opts?.spinner?.start('understanding the room');
103
100
  const calendarContext = opts?.calendarContext ?? '';
104
101
  const hour = new Date().getHours();
105
102
  const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
103
+ // Stream contacts as they enrich — each name + relationship appears live
106
104
  let brief = '';
107
105
  const enriched = [];
106
+ const enrichedNames = new Set();
108
107
  for await (const t of streamEnrichedCRM(crm, apiKey, {
109
108
  calendarContext,
110
109
  timeOfDay,
111
110
  onBrief: (b) => { brief = b; },
112
111
  })) {
113
112
  enriched.push(t);
113
+ enrichedNames.add(t.name);
114
+ await renderCRMContact(t);
114
115
  }
115
- opts?.spinner?.stop();
116
+ // Show remaining contacts (not enriched) — just names
117
+ const remaining = crm.threads.filter(t => !enrichedNames.has(t.name)).slice(0, 5);
118
+ if (remaining.length)
119
+ renderCRMList(remaining);
116
120
  if (brief)
117
121
  await renderBrief(brief);
118
122
  saveContactSummaries(enriched);
package/dist/crm.js CHANGED
@@ -406,33 +406,30 @@ STYLE GUIDE:
406
406
  ${contactBlocks}`,
407
407
  }],
408
408
  });
409
- const response = await stream.finalMessage();
410
- const fullText = response.content
411
- .filter(b => b.type === 'text')
412
- .map(b => b.text)
413
- .join('');
409
+ // Stream text deltas — yield contacts the moment each block completes
414
410
  let section = 'preamble';
415
411
  const briefLines = [];
416
- for (const rawLine of fullText.split('\n')) {
412
+ let lineBuf = '';
413
+ const processLine = (rawLine) => {
417
414
  const line = rawLine.trim();
418
415
  if (/^CONTACTS:?$/i.test(line)) {
419
416
  section = 'contacts';
420
- continue;
417
+ return;
421
418
  }
422
419
  if (/^BRIEF:?$/i.test(line)) {
423
420
  flushContact();
424
421
  section = 'brief';
425
- continue;
422
+ return;
426
423
  }
427
424
  const bracketMatch = line.match(/^\[([^\]]+)\]/);
428
425
  if (bracketMatch && (section === 'contacts' || section === 'preamble')) {
429
426
  flushContact();
430
427
  curName = bracketMatch[1].trim();
431
428
  section = 'contacts';
432
- continue;
429
+ return;
433
430
  }
434
431
  if (!line)
435
- continue;
432
+ return;
436
433
  if (section === 'contacts' || section === 'preamble') {
437
434
  if (line.startsWith('Priority:')) {
438
435
  curPriority = line.replace('Priority:', '').trim().toLowerCase();
@@ -444,7 +441,21 @@ ${contactBlocks}`,
444
441
  else if (section === 'brief') {
445
442
  briefLines.push(line);
446
443
  }
444
+ };
445
+ for await (const event of stream) {
446
+ if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
447
+ lineBuf += event.delta.text;
448
+ const parts = lineBuf.split('\n');
449
+ lineBuf = parts.pop() || '';
450
+ for (const line of parts)
451
+ processLine(line);
452
+ while (ready.length)
453
+ yield ready.shift();
454
+ }
447
455
  }
456
+ // Flush remaining buffer
457
+ if (lineBuf.trim())
458
+ processLine(lineBuf);
448
459
  flushContact();
449
460
  while (ready.length)
450
461
  yield ready.shift();
@@ -452,7 +463,7 @@ ${contactBlocks}`,
452
463
  opts.onBrief(briefLines.join(' '));
453
464
  }
454
465
  catch (err) {
455
- process.stderr.write(`\x1B[2K\r enrichment error: ${err instanceof Error ? err.stack || err.message : String(err)}\n`);
466
+ // silent contacts still show without enrichment
456
467
  }
457
468
  for (const t of top) {
458
469
  if (!yielded.has(t.name) && t.recentSnippets?.length)
package/dist/tui.js CHANGED
@@ -39,19 +39,21 @@ async function typewrite(text, style, delay = 30) {
39
39
  await _sleep(delay);
40
40
  }
41
41
  }
42
- async function fetchWeather(city) {
42
+ async function fetchLocationWeather() {
43
43
  try {
44
44
  const ac = new AbortController();
45
- const t = setTimeout(() => ac.abort(), 2000);
46
- const r = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=%t|%C`, { signal: ac.signal, headers: { 'User-Agent': 'life-pulse' } });
45
+ const t = setTimeout(() => ac.abort(), 3000);
46
+ const r = await fetch(`https://wttr.in/?format=%l|%t|%C`, { signal: ac.signal, headers: { 'User-Agent': 'life-pulse' } });
47
47
  clearTimeout(t);
48
48
  const raw = (await r.text()).trim();
49
- const [temp, cond] = raw.split('|');
50
- if (!temp || !cond)
49
+ const [loc, temp, cond] = raw.split('|');
50
+ if (!loc || !temp || !cond)
51
51
  return null;
52
+ // loc comes back as "City, Region, Country" — take just the city
53
+ const city = loc.split(',')[0].trim();
52
54
  const deg = temp.replace(/[+\s]/g, '').replace(/°[CF]/, '°');
53
55
  const sky = cond.trim().toLowerCase();
54
- return `${deg} and ${sky}`;
56
+ return { city, weather: `${deg} and ${sky}` };
55
57
  }
56
58
  catch {
57
59
  return null;
@@ -118,10 +120,8 @@ function renderScanField(cols, rows) {
118
120
  }
119
121
  export async function renderIntro(name) {
120
122
  const firstName = name?.split(' ')[0] || name;
121
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
122
- const city = tz.split('/').pop()?.replace(/_/g, ' ') || '';
123
- // Fire weather fetch while animation runs
124
- const weatherP = city ? fetchWeather(city) : Promise.resolve(null);
123
+ // Fire IP-based location + weather fetch while animation runs
124
+ const locP = fetchLocationWeather();
125
125
  const cols = process.stdout.columns || 80;
126
126
  const rows = process.stdout.rows || 24;
127
127
  // Push old content out of scrollback
@@ -144,12 +144,10 @@ export async function renderIntro(name) {
144
144
  await typewrite(hey, G1, 35);
145
145
  process.stdout.write('\n');
146
146
  await _sleep(250);
147
- // "I see you're in Los Angeles. 72° and sunny out there."
148
- const weather = await weatherP;
149
- const locLine = weather
150
- ? `I see you're in ${city}. ${weatherRemark(weather)}`
151
- : city ? `I see you're in ${city}.` : '';
152
- if (locLine) {
147
+ // "I see you're in Short Hills. 42° and cloudy."
148
+ const loc = await locP;
149
+ if (loc) {
150
+ const locLine = `I see you're in ${loc.city}. ${weatherRemark(loc.weather)}`;
153
151
  process.stdout.write(' ');
154
152
  await typewrite(locLine, G2, 20);
155
153
  process.stdout.write('\n');
package/dist/ui/app.js CHANGED
@@ -25,13 +25,13 @@ const Bar = ({ done, total }) => {
25
25
  return (_jsxs(Text, { children: [_jsx(Text, { color: "#565f89", children: '─'.repeat(f) }), _jsx(Text, { color: "#292e42", children: '─'.repeat(BAR_W - f) }), ' ', _jsx(Text, { color: "#565f89", children: done }), _jsx(Text, { color: "#3b4261", children: "/" }), _jsx(Text, { color: "#565f89", children: total })] }));
26
26
  };
27
27
  const ProgressView = ({ progress, frame }) => {
28
- const spin = (off = 0) => BAR_CHARS[(frame + off) % BAR_CHARS.length];
28
+ const spin = (off = 0) => BAR_CHARS[(Math.floor((frame + off) / 5)) % BAR_CHARS.length];
29
29
  if (progress.thinking && !progress.items.length && !progress.total) {
30
30
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: ' ' }), _jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin() }), ' ', _jsx(Text, { color: "#565f89", children: "thinking" })] })] }));
31
31
  }
32
32
  const { phase, done, total, items } = progress;
33
33
  const allDone = done === total && total > 0;
34
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: ' ' }), allDone ? (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#565f89", children: '·' }), ' ', _jsx(Text, { color: "#565f89", children: phase === 'scanning' ? 'scanned' : 'done' })] })) : total > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [' ', _jsx(Text, { color: "#565f89", children: phase || 'working' }), ' ', _jsx(Bar, { done: done, total: total })] }), items.slice(0, MAX_VIS).map((item, i) => (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin(i) }), ' ', _jsx(Text, { color: "#565f89", children: item.label }), item.tool && _jsxs(Text, { color: "#3b4261", children: [' — ', item.tool] })] }, i))), items.length > MAX_VIS && _jsxs(Text, { color: "#3b4261", children: [' ', "+", items.length - MAX_VIS, " more"] })] })) : null, progress.thinking && total > 0 && (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin() }), ' ', _jsx(Text, { color: "#565f89", children: "thinking" })] }))] }));
34
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: ' ' }), allDone ? (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin() }), ' ', _jsx(Text, { color: "#565f89", children: phase === 'scanning' ? 'scanned' : 'putting it together' })] })) : total > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [' ', _jsx(Text, { color: "#565f89", children: phase || 'working' }), ' ', _jsx(Bar, { done: done, total: total })] }), items.slice(0, MAX_VIS).map((item, i) => (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin(i) }), ' ', _jsx(Text, { color: "#565f89", children: item.label }), item.tool && _jsxs(Text, { color: "#3b4261", children: [' — ', item.tool] })] }, i))), items.length > MAX_VIS && _jsxs(Text, { color: "#3b4261", children: [' ', "+", items.length - MAX_VIS, " more"] })] })) : null, progress.thinking && total > 0 && (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#3b4261", children: spin() }), ' ', _jsx(Text, { color: "#565f89", children: "thinking" })] }))] }));
35
35
  };
36
36
  const CardView = ({ card, num, sel }) => {
37
37
  const opts = card.options || [];
@@ -43,14 +43,14 @@ export class InkProgress {
43
43
  const workersDone = this.workers.filter(w => w.done).length;
44
44
  const activeWorkers = this.workers.filter(w => !w.done);
45
45
  if (this.workers.length > 0) {
46
- showProgress(activeWorkers.length > 0 ? 'working' : 'done', workersDone, this.workers.length, activeWorkers.map(w => ({
46
+ showProgress(activeWorkers.length > 0 ? 'working' : 'finishing up', workersDone, this.workers.length, activeWorkers.map(w => ({
47
47
  label: w.label,
48
48
  tool: w.tool ? this.s(w.tool) : undefined,
49
49
  })), this._thinking);
50
50
  }
51
51
  else if (this.tools.length > 0) {
52
52
  const active = this.tools.filter(t => !t.done);
53
- showProgress(toolsDone === this.tools.length ? 'done' : 'scanning', toolsDone, this.tools.length, active.map(t => ({ label: this.s(t.name) })), this._thinking);
53
+ showProgress(toolsDone === this.tools.length ? 'finishing up' : 'scanning', toolsDone, this.tools.length, active.map(t => ({ label: this.s(t.name) })), this._thinking);
54
54
  }
55
55
  else if (this._thinking) {
56
56
  showProgress('', 0, 0, [], true);
package/dist/ui/theme.js CHANGED
@@ -51,7 +51,7 @@ export const W = () => Math.min(process.stdout.columns || 80, 80);
51
51
  export const INNER = () => W() - 4;
52
52
  // Spinner frames
53
53
  export const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
54
- export const BAR_CHARS = ['', '', '', '⢿', '⡿', '⣟', '⣯', '⣷'];
54
+ export const BAR_CHARS = ['', '*', ''];
55
55
  // Progress label map — personality, not developer console
56
56
  export const TOOL_LABEL = {
57
57
  search_all_messages: 'reading the room',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "life-pulse",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {