life-pulse 2.3.4 → 2.3.7

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
@@ -362,7 +362,7 @@ progress, onCard) {
362
362
 
363
363
  Output JSON (no markdown, no code fences):
364
364
  {
365
- "greeting": "[Time] [N] items need you.",
365
+ "greeting": "Short, warm one-liner. No counts or statistics.",
366
366
  "promises": [{ "title": "...", "urgency": "now|today|this_week", "who": "...", "when_due": "...", "options": [{"label":"...","description":"..."}] }],
367
367
  "blockers": [{ "title": "...", "urgency": "now|today|this_week", "who": "...", "waiting_since": "...", "options": [{"label":"...","description":"..."}] }],
368
368
  "bumps": [{ "title": "...", "who": "...", "context": "...", "options": [{"label":"...","description":"..."}] }],
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, renderCRMContact, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, RED, AMB, MID, DIM } from './tui.js';
9
+ import { renderIntro, 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';
@@ -89,14 +89,6 @@ async function showCRM(apiKey, opts) {
89
89
  return false;
90
90
  }
91
91
  opts?.spinner?.stop();
92
- // The reveal — silence, then the numbers drop
93
- const innerCircle = crm.threads.filter(t => t.threadTemp === 'hot' || t.threadTemp === 'warm' || t.msgs30d > 20).length;
94
- const aboutToBreak = crm.threads.filter(t => t.waitingOnYou).length;
95
- await renderReveal({
96
- conversations: crm.threads.length,
97
- innerCircle,
98
- aboutToBreak,
99
- });
100
92
  const calendarContext = opts?.calendarContext ?? '';
101
93
  const hour = new Date().getHours();
102
94
  const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
package/dist/crm.d.ts CHANGED
@@ -35,7 +35,7 @@ export interface CRM {
35
35
  generatedAt: string;
36
36
  }
37
37
  export declare function buildCRM(): Promise<CRM>;
38
- export declare function streamEnrichedCRM(crm: CRM, apiKey: string, opts?: {
38
+ export declare function streamEnrichedCRM(crm: CRM, _apiKey?: string, opts?: {
39
39
  calendarContext?: string;
40
40
  timeOfDay?: string;
41
41
  onBrief?: (brief: string) => void;
package/dist/crm.js CHANGED
@@ -12,7 +12,8 @@ import { join } from 'path';
12
12
  import { openDb, safeQuery } from './db.js';
13
13
  import { resolveName } from './contacts.js';
14
14
  import * as rply from './rply-client.js';
15
- import Anthropic from '@anthropic-ai/sdk';
15
+ // OpenRouter key for fast CRM enrichment (GLM-4.7)
16
+ const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY || '';
16
17
  import dayjs from 'dayjs';
17
18
  import relativeTime from 'dayjs/plugin/relativeTime.js';
18
19
  dayjs.extend(relativeTime);
@@ -324,9 +325,9 @@ function buildCRMFromSQLite() {
324
325
  return { threads: threads.slice(0, 25), yourAvgResponseSec: yourAvg, generatedAt: dayjs().toISOString() };
325
326
  }
326
327
  // ─── Stream-enrich CRM ──────────────────────────────────────────
327
- export async function* streamEnrichedCRM(crm, apiKey, opts) {
328
+ export async function* streamEnrichedCRM(crm, _apiKey, opts) {
328
329
  const top = crm.threads.slice(0, 10);
329
- if (!top.length || !apiKey) {
330
+ if (!top.length || !OPENROUTER_KEY) {
330
331
  for (const t of top)
331
332
  yield t;
332
333
  return;
@@ -367,13 +368,20 @@ export async function* streamEnrichedCRM(crm, apiKey, opts) {
367
368
  curPriority = null;
368
369
  };
369
370
  try {
370
- const client = new Anthropic({ apiKey });
371
- const stream = client.messages.stream({
372
- model: 'claude-sonnet-4-5-20250929',
373
- max_tokens: 3000,
374
- messages: [{
375
- role: 'user',
376
- content: `You are a personal chief of staff analyzing someone's message history this ${timeOfDay}.
371
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
372
+ method: 'POST',
373
+ headers: {
374
+ 'Authorization': `Bearer ${OPENROUTER_KEY}`,
375
+ 'Content-Type': 'application/json',
376
+ 'HTTP-Referer': 'https://life-pulse.dev',
377
+ },
378
+ body: JSON.stringify({
379
+ model: 'z-ai/glm-4.7',
380
+ max_tokens: 3000,
381
+ stream: true,
382
+ messages: [{
383
+ role: 'user',
384
+ content: `You are a personal chief of staff analyzing someone's message history this ${timeOfDay}.
377
385
 
378
386
  Recent iMessage threads below. → = they sent, ← = contact sent.
379
387
  ${calendar}
@@ -404,12 +412,16 @@ STYLE GUIDE:
404
412
  - If someone has a calendar meeting today, weave that in naturally.
405
413
 
406
414
  ${contactBlocks}`,
407
- }],
415
+ }],
416
+ }),
408
417
  });
409
- // Stream text deltas — yield contacts the moment each block completes
418
+ if (!res.ok || !res.body)
419
+ throw new Error(`OpenRouter ${res.status}`);
420
+ // Stream SSE deltas — yield contacts the moment each block completes
410
421
  let section = 'preamble';
411
422
  const briefLines = [];
412
423
  let lineBuf = '';
424
+ let sseBuf = '';
413
425
  const processLine = (rawLine) => {
414
426
  const line = rawLine.trim();
415
427
  if (/^CONTACTS:?$/i.test(line)) {
@@ -442,15 +454,32 @@ ${contactBlocks}`,
442
454
  briefLines.push(line);
443
455
  }
444
456
  };
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();
457
+ const decoder = new TextDecoder();
458
+ const reader = res.body.getReader();
459
+ while (true) {
460
+ const { done, value } = await reader.read();
461
+ if (done)
462
+ break;
463
+ sseBuf += decoder.decode(value, { stream: true });
464
+ const sseLines = sseBuf.split('\n');
465
+ sseBuf = sseLines.pop() || '';
466
+ for (const sseLine of sseLines) {
467
+ if (!sseLine.startsWith('data: ') || sseLine === 'data: [DONE]')
468
+ continue;
469
+ try {
470
+ const json = JSON.parse(sseLine.slice(6));
471
+ const delta = json.choices?.[0]?.delta?.content;
472
+ if (!delta)
473
+ continue;
474
+ lineBuf += delta;
475
+ const parts = lineBuf.split('\n');
476
+ lineBuf = parts.pop() || '';
477
+ for (const line of parts)
478
+ processLine(line);
479
+ while (ready.length)
480
+ yield ready.shift();
481
+ }
482
+ catch { }
454
483
  }
455
484
  }
456
485
  // Flush remaining buffer
package/dist/tui.d.ts CHANGED
@@ -17,11 +17,6 @@ export declare const HD: import("chalk").ChalkInstance;
17
17
  export declare function renderIntro(name?: string): Promise<void>;
18
18
  export declare function renderGreeting(name?: string): void;
19
19
  export declare function renderContextLine(): void;
20
- export declare function renderReveal(stats: {
21
- conversations: number;
22
- innerCircle: number;
23
- aboutToBreak: number;
24
- }): Promise<void>;
25
20
  export declare function renderCRMList(contacts: {
26
21
  name: string;
27
22
  lastMsg: {
package/dist/tui.js CHANGED
@@ -155,11 +155,20 @@ export async function renderIntro(name) {
155
155
  }
156
156
  // "Hope you're having an incredible morning."
157
157
  const h = new Date().getHours();
158
+ const greeting = timeGreeting(h);
158
159
  process.stdout.write(' ');
159
- await typewrite(timeGreeting(h), G3, 18);
160
+ await typewrite(greeting, G3, 18);
160
161
  process.stdout.write('\n\n');
162
+ await _sleep(1200);
163
+ // Build greeting lines for Ink to preserve
164
+ const greetLines = [''];
165
+ greetLines.push(` ${G1(hey)}`);
166
+ if (loc)
167
+ greetLines.push(` ${G2(`I see you're in ${loc.city}. ${weatherRemark(loc.weather)}`)}`);
168
+ greetLines.push(` ${G3(greeting)}`);
169
+ greetLines.push('');
161
170
  if (USE_INK)
162
- ink.initInk();
171
+ ink.initInk(greetLines);
163
172
  }
164
173
  // ─── Greeting (legacy / non-interactive fallback) ───
165
174
  export function renderGreeting(name) {
@@ -184,21 +193,6 @@ export function renderContextLine() {
184
193
  out(` ${C.dim(`${days[d.getDay()]} · ${h12}:${min} ${ampm}`)}`);
185
194
  out('');
186
195
  }
187
- // ─── Reveal (the moment after discovery) ───
188
- export async function renderReveal(stats) {
189
- await _sleep(600);
190
- process.stdout.write(' ');
191
- await typewrite(`${stats.conversations} conversations.`, G2, 22);
192
- await _sleep(400);
193
- process.stdout.write(' ');
194
- await typewrite(`${stats.innerCircle} people who matter.`, G1, 25);
195
- await _sleep(500);
196
- if (stats.aboutToBreak > 0) {
197
- process.stdout.write(' ');
198
- await typewrite(`${stats.aboutToBreak} thread${stats.aboutToBreak === 1 ? '' : 's'} about to break.`, chalk.bold.hex('#c0caf5'), 28);
199
- }
200
- process.stdout.write('\n\n');
201
- }
202
196
  // ─── CRM List ───
203
197
  export function renderCRMList(contacts) {
204
198
  for (const t of contacts.slice(0, 12)) {
@@ -296,14 +290,12 @@ export async function renderCRMStream(source, spinner) {
296
290
  out('');
297
291
  first = false;
298
292
  }
299
- const name = t.name.slice(0, 24);
300
- const ago = shortAgo(t.lastMsg?.ago || '');
301
- const gap = Math.max(2, w - 4 - name.length - ago.length);
302
- out(` ${C.hd(name)}${' '.repeat(gap)}${C.dim(ago)}`);
293
+ out(` ${C.hd(t.name.slice(0, 40))}`);
303
294
  if (t.relationship) {
304
295
  out(` ${C.mid(t.relationship.slice(0, w - 8))}`);
305
296
  }
306
297
  rendered.push(t);
298
+ await _sleep(180);
307
299
  }
308
300
  if (first)
309
301
  spinner?.stop();
@@ -312,12 +304,10 @@ export async function renderCRMStream(source, spinner) {
312
304
  }
313
305
  export async function renderCRMContact(t) {
314
306
  const w = W();
315
- const name = t.name.slice(0, 24);
316
- const ago = shortAgo(t.lastMsg?.ago || '');
317
- const gap = Math.max(2, w - 4 - name.length - ago.length);
318
- out(` ${C.hd(name)}${' '.repeat(gap)}${C.dim(ago)}`);
307
+ out(` ${C.hd(t.name.slice(0, 40))}`);
319
308
  if (t.relationship)
320
309
  out(` ${C.mid(t.relationship.slice(0, w - 8))}`);
310
+ await _sleep(180);
321
311
  }
322
312
  // ─── Destroy (call at exit) ───
323
313
  export function destroyUI() {
package/dist/ui/app.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface CardData {
17
17
  who?: string;
18
18
  urgency?: string;
19
19
  }
20
- export declare function initInk(): void;
20
+ export declare function initInk(seedLines?: string[]): void;
21
21
  export declare function destroy(): void;
22
22
  export declare function log(text: string): void;
23
23
  export declare function gap(): void;
package/dist/ui/app.js CHANGED
@@ -89,6 +89,7 @@ const App = () => {
89
89
  let _inst = null;
90
90
  let _started = false;
91
91
  const _origLog = console.log.bind(console);
92
+ let _seedLines = [];
92
93
  function ensureStarted() {
93
94
  if (_started)
94
95
  return;
@@ -96,6 +97,15 @@ function ensureStarted() {
96
97
  process.stdout.write('\x1Bc'); // clear screen
97
98
  process.stdout.write('\x1B[?25l'); // hide cursor
98
99
  _inst = inkRender(React.createElement(App));
100
+ // Seed initial lines (e.g. greeting from intro)
101
+ if (_seedLines.length) {
102
+ const lines = _seedLines;
103
+ _seedLines = [];
104
+ // Small delay to let React mount before pushing state
105
+ setTimeout(() => {
106
+ _update?.(p => ({ ...p, lines: [...lines, ...p.lines] }));
107
+ }, 50);
108
+ }
99
109
  // Redirect console.log through Ink
100
110
  console.log = (...args) => {
101
111
  const text = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
@@ -105,7 +115,11 @@ function ensureStarted() {
105
115
  };
106
116
  }
107
117
  // ─── Exports ──────────────────────────────────────
108
- export function initInk() { ensureStarted(); }
118
+ export function initInk(seedLines) {
119
+ if (seedLines)
120
+ _seedLines = seedLines;
121
+ ensureStarted();
122
+ }
109
123
  export function destroy() {
110
124
  console.log = _origLog;
111
125
  _inst?.unmount();
@@ -71,7 +71,8 @@ export class InkProgress {
71
71
  const t = this.tools.find(t => t.name === name && !t.done);
72
72
  if (t)
73
73
  t.done = true;
74
- this.paint();
74
+ // Delay repaint so the label lingers on screen
75
+ setTimeout(() => this.paint(), 400);
75
76
  }
76
77
  workerStart(id, label) {
77
78
  this._thinking = false;
@@ -113,6 +114,9 @@ export class InkProgress {
113
114
  }
114
115
  pause() { this._paused = true; hideProgress(); }
115
116
  resume() { this._paused = false; this.paint(); }
116
- stop() { this.paint(); hideProgress(); }
117
+ stop() {
118
+ // Let the final state linger so it doesn't vanish instantly
119
+ setTimeout(() => hideProgress(), 800);
120
+ }
117
121
  clear() { hideProgress(); }
118
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "life-pulse",
3
- "version": "2.3.4",
3
+ "version": "2.3.7",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {