life-pulse 2.2.2 → 2.3.1

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
@@ -1,4 +1,4 @@
1
- # life-pulse
1
+ # everything is mac mini
2
2
 
3
3
  macOS life diagnostic — reads local data sources, generates actionable insights.
4
4
 
package/dist/agent.js CHANGED
@@ -94,12 +94,12 @@ function buildCustomMcpServer(onCard) {
94
94
  });
95
95
  }
96
96
  // ─── Executive Operating System prompt ──────────────────────────
97
- function buildOrchestratorPrompt() {
97
+ async function buildOrchestratorPrompt() {
98
98
  const delta = buildDeltaContext();
99
99
  const todos = buildTodoContext();
100
100
  const profile = buildProfile();
101
101
  const name = profile.name;
102
- const crm = buildCRM();
102
+ const crm = await buildCRM();
103
103
  const crmContext = buildCRMContext(crm);
104
104
  const projectSection = profile.projects.length > 0
105
105
  ? `\n- Active projects: ${profile.projects.join(', ')}`
@@ -147,26 +147,26 @@ One briefing. Organized by how much damage it does if ignored — not by app, no
147
147
 
148
148
  Chase down every thread. Be relentless. Don't surface the obvious stuff they'd find on their own — find the things about to fall through the cracks that they don't even know about yet.
149
149
 
150
- 1. PROMISES 🔴
150
+ 1. PROMISES
151
151
  Things ${name} said they'd do that are about to break or already have. This is the most important section. Always.
152
152
  Scan every outbound message for commitment language: "I'll send," "let me get back to you," "I'll loop in," "by Friday," "will do." Cross-reference against what was actually done. The gap = the briefing.
153
153
  Reputation compounds. Every dropped commitment is a withdrawal from an account that's very hard to refill.
154
154
  For each: who they promised, what specifically (not "follow up with Sarah" → "Send Sarah the revised deck she asked for Tuesday"), when it was due, and the one move to close it. Tight enough to do in 2 minutes or delegate in one sentence.
155
155
 
156
- 2. BLOCKERS 🟠
156
+ 2. BLOCKERS
157
157
  People quietly waiting on ${name}.
158
158
  Find everywhere they're the bottleneck. Approvals sitting untouched. Questions asked that they never saw. Docs shared "for review" that really mean "I can't move until you respond."
159
159
  Nobody says "you're blocking me" out loud. They just wait and quietly start resenting it. Find those moments before they fester.
160
160
  For each: who's stuck, what they need, how long they've been waiting. Then make the call — yes, no, or delegate — with a one-line reason. Don't present options. Recommend.
161
161
 
162
- 3. BUMPS 🟡
162
+ 3. BUMPS
163
163
  Relationships worth a touch. Offense, not defense.
164
164
  Conversations that went cold but shouldn't have. Intros offered but never made. Threads where someone shared something interesting and a reply would strengthen the relationship.
165
165
  The question: "Who would be pleasantly surprised to hear from ${name} today?"
166
166
  These aren't obligations — they're compound interest. A "congrats on the launch" text. A "saw this and thought of you" forward. That coffee they said they'd grab three weeks ago.
167
167
  For each: who, why now, and a suggested message they can send as-is or edit in 10 seconds.
168
168
 
169
- 4. ALPHA 🟢
169
+ 4. ALPHA
170
170
  Moves they'd be pissed they missed.
171
171
  Based on everything you know — what should they be doing that they haven't thought of?
172
172
  A competitor just shipped something relevant. Someone in their network just raised a round. Two people they know should know each other but don't. Something they do every week that you could just handle for them.
@@ -387,7 +387,7 @@ RULES:
387
387
  for await (const msg of query({
388
388
  prompt,
389
389
  options: {
390
- systemPrompt: buildOrchestratorPrompt(),
390
+ systemPrompt: await buildOrchestratorPrompt(),
391
391
  model: ORCHESTRATOR_MODEL,
392
392
  env,
393
393
  mcpServers,
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 { renderGreeting, renderContextLine, renderCRMList, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, GRN, MAG, CYN, RED, AMB, MID } from './tui.js';
9
+ import { renderIntro, renderReveal, renderCRMList, 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';
@@ -83,16 +83,23 @@ async function fetchCalendarContext() {
83
83
  return '';
84
84
  }
85
85
  async function showCRM(apiKey, opts) {
86
- const crm = buildCRM();
86
+ const crm = await buildCRM();
87
87
  if (!crm.threads.length || !apiKey) {
88
88
  opts?.spinner?.stop();
89
89
  return false;
90
90
  }
91
- // Names appear INSTANTLY — no API call, just DB data
92
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
+ });
93
100
  renderCRMList(crm.threads);
94
101
  // Enrich in background (for agent context + brief)
95
- opts?.spinner?.start('reading the room');
102
+ opts?.spinner?.start('understanding the room');
96
103
  const calendarContext = opts?.calendarContext ?? '';
97
104
  const hour = new Date().getHours();
98
105
  const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
@@ -132,23 +139,19 @@ async function main() {
132
139
  if (initCheckMode) {
133
140
  const status = runInitCheck();
134
141
  console.log();
135
- console.log(chalk.bold(' PRE-FLIGHT CHECK'));
136
- console.log();
137
142
  for (const c of status.checks) {
138
143
  const icon = c.passed ? chalk.green('✓') : chalk.red('✗');
139
- const detail = c.detail ? chalk.dim(` ${c.detail}`) : '';
140
- console.log(` ${icon} ${c.check}${detail}`);
144
+ console.log(` ${icon} ${c.check}`);
141
145
  }
142
146
  console.log();
143
- console.log(chalk.dim(` Mode: ${status.mode}`));
144
147
  console.log(chalk.dim(` ${status.recommendation}`));
145
148
  console.log();
146
149
  return;
147
150
  }
148
151
  // --router: run message router on YOUR Mac (routes to client Mac Minis)
149
152
  if (routerMode) {
150
- console.log(chalk.bold.hex('#7AA2F7')(' life-pulse router'));
151
- console.log(chalk.dim(' routing messages to client Mac Minis via Tailscale'));
153
+ console.log(chalk.bold.hex('#c0caf5')(' life-pulse'));
154
+ console.log(chalk.dim(' routing messages'));
152
155
  console.log();
153
156
  const router = startRouter();
154
157
  process.on('SIGINT', () => { router.stop(); process.exit(0); });
@@ -170,7 +173,7 @@ async function main() {
170
173
  if (testSmsMode) {
171
174
  const up = await isGatewayUp();
172
175
  if (!up) {
173
- console.log(chalk.red(' Gateway not running on :19877 — start daemon first'));
176
+ console.log(chalk.red(' not running — start life-pulse first'));
174
177
  return;
175
178
  }
176
179
  const phone = process.argv[process.argv.indexOf('--test-sms') + 1] || '+1234567890';
@@ -178,15 +181,16 @@ async function main() {
178
181
  const resp = await fetch('http://127.0.0.1:19877/inbound', {
179
182
  method: 'POST',
180
183
  headers: { 'Content-Type': 'application/json' },
181
- body: JSON.stringify({ phone, message: 'test message from life-pulse --test-sms' }),
184
+ body: JSON.stringify({ phone, message: 'test message from life-pulse' }),
182
185
  });
183
186
  const data = await resp.json();
184
- console.log(chalk.green(` Gateway responded: ${data.response || data.error}`));
185
- if (data.tools_used?.length)
186
- console.log(chalk.dim(` Tools: ${data.tools_used.join(', ')}`));
187
+ if (data.response)
188
+ console.log(chalk.green(` replied: ${data.response}`));
189
+ else
190
+ console.log(chalk.red(` failed: ${data.error}`));
187
191
  }
188
192
  catch (err) {
189
- console.log(chalk.red(` Failed: ${err instanceof Error ? err.message : String(err)}`));
193
+ console.log(chalk.red(` couldn't connect is life-pulse running?`));
190
194
  }
191
195
  return;
192
196
  }
@@ -203,8 +207,7 @@ async function main() {
203
207
  }
204
208
  // --daemon: check if already running
205
209
  if (daemonMode && isDaemonRunning()) {
206
- console.log(chalk.yellow(' life-pulse daemon already running'));
207
- console.log(chalk.dim(' use --health to check status'));
210
+ console.log(chalk.yellow(' already running'));
208
211
  return;
209
212
  }
210
213
  // --status: show session progress info and exit
@@ -212,32 +215,26 @@ async function main() {
212
215
  const progress = loadProgress();
213
216
  const timeSince = getTimeSinceLastSession();
214
217
  console.log();
215
- console.log(chalk.bold(' LIFE-PULSE STATUS'));
216
- console.log();
217
- console.log(` Sessions: ${progress.totalSessions}`);
218
- console.log(` First run: ${dayjs(progress.firstRun).format('MMM D, YYYY')}`);
219
- console.log(` Last session: ${timeSince.readable}`);
220
- console.log();
218
+ console.log(chalk.dim(` since ${dayjs(progress.firstRun).format('MMM D, YYYY')} · ${progress.totalSessions} sessions`));
219
+ console.log(chalk.dim(` last check-in ${timeSince.readable}`));
221
220
  const discoveries = getNewDiscoveries();
222
221
  if (discoveries.length) {
223
- console.log(` New discoveries: ${discoveries.length}`);
222
+ console.log();
224
223
  for (const d of discoveries.slice(0, 5)) {
225
- console.log(` · ${d.name}`);
224
+ console.log(chalk.dim(` ${d.name}`));
226
225
  }
227
226
  }
228
- // Skills summary
229
227
  const skills = scanForSkills();
230
- console.log(` Skills: ${skills.active.length} active, ${skills.available.length} available`);
231
- // Crash stats
228
+ if (skills.active.length) {
229
+ console.log(chalk.dim(` ${skills.active.length} skills`));
230
+ }
232
231
  const crashes = getCrashStats();
233
232
  if (crashes.consecutive > 0) {
234
- console.log(chalk.yellow(` Crashes: ${crashes.consecutive} consecutive failures`));
233
+ console.log(chalk.yellow(` something's wrong — ${crashes.consecutive} failures in a row`));
235
234
  }
236
235
  const missing = getMissingPermissions();
237
236
  if (missing.length) {
238
- console.log();
239
- console.log(` Missing permissions: ${missing.map(m => m.label).join(', ')}`);
240
- console.log(chalk.dim(' Run with --setup to configure'));
237
+ console.log(chalk.dim(` missing: ${missing.map(m => m.label).join(', ')}`));
241
238
  }
242
239
  console.log();
243
240
  return;
@@ -354,17 +351,13 @@ async function main() {
354
351
  </plist>`;
355
352
  writeFileSync(join(agentsDir, 'com.life-pulse.morning.plist'), morningPlist);
356
353
  writeFileSync(join(agentsDir, 'com.life-pulse.daemon.plist'), daemonPlist);
357
- console.log(chalk.green(' Installed:'));
358
- console.log(chalk.dim(` Morning brief: ${config.briefTime} daily`));
359
- console.log(chalk.dim(` Daemon: KeepAlive (auto-restart on crash)`));
354
+ console.log(chalk.dim(' running in the background'));
355
+ console.log(chalk.dim(` morning brief at ${config.briefTime}`));
360
356
  console.log();
361
- console.log(chalk.dim(' To activate:'));
362
- console.log(chalk.dim(' launchctl load ~/Library/LaunchAgents/com.life-pulse.daemon.plist'));
363
- console.log(chalk.dim(' launchctl load ~/Library/LaunchAgents/com.life-pulse.morning.plist'));
364
357
  return;
365
358
  }
366
359
  if (!key && !ANTHROPIC_KEY) {
367
- console.log(chalk.red('\n\n No API key. Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY.\n'));
360
+ console.log(chalk.red('\n\n missing API key\n'));
368
361
  process.exit(1);
369
362
  }
370
363
  const interactive = process.stdin.isTTY && !jsonMode && !legacyMode;
@@ -377,8 +370,7 @@ async function main() {
377
370
  }
378
371
  catch { }
379
372
  if (interactive) {
380
- renderGreeting(userName);
381
- renderContextLine();
373
+ await renderIntro(userName);
382
374
  }
383
375
  const spinner = interactive ? new Spinner() : undefined;
384
376
  // ── First-run: permissions → discovery → archetype ──
@@ -389,7 +381,7 @@ async function main() {
389
381
  // Welcome message for first run
390
382
  if (isFirstRun && interactive) {
391
383
  console.log();
392
- console.log(chalk.bold.hex('#7AA2F7')(' Welcome to life-pulse!'));
384
+ console.log(chalk.bold.hex('#c0caf5')(' Welcome to life-pulse.'));
393
385
  console.log(chalk.dim(' Let me learn about your setup...\n'));
394
386
  }
395
387
  else if (timeSinceLastSession.days > 7 && interactive) {
@@ -397,10 +389,10 @@ async function main() {
397
389
  }
398
390
  if (process.stdin.isTTY)
399
391
  await runPermissionFlow();
400
- spinner?.start('discovering platforms');
392
+ spinner?.start('learning your world');
401
393
  const platformProfile = discoverPlatforms();
402
394
  // iCloud + local app discovery
403
- spinner?.update('scanning iCloud');
395
+ spinner?.update('looking deeper');
404
396
  const discoveredApps = runDiscovery();
405
397
  if (discoveredApps.length && interactive) {
406
398
  spinner?.stop();
@@ -409,7 +401,7 @@ async function main() {
409
401
  console.log();
410
402
  }
411
403
  // Skill discovery: map found apps → activatable skills
412
- spinner?.update('loading skills');
404
+ spinner?.update('seeing what you use');
413
405
  const skills = scanForSkills();
414
406
  if (skills.newlyFound.length && interactive) {
415
407
  spinner?.stop();
@@ -428,12 +420,12 @@ async function main() {
428
420
  const toolCount = platformProfile.cliTools.length;
429
421
  // CRM drip-feeds before archetype — contacts stream in one by one
430
422
  if (interactive) {
431
- spinner?.update('scanning messages');
423
+ spinner?.update('getting to know you');
432
424
  const calCtx = await calendarP;
433
425
  crmShown = await showCRM(ANTHROPIC_KEY, { calendarContext: calCtx, spinner });
434
426
  }
435
427
  if (ANTHROPIC_KEY) {
436
- spinner?.start('generating archetype');
428
+ spinner?.start('figuring out who you are');
437
429
  try {
438
430
  const archetype = await generateArchetype(platformProfile, ANTHROPIC_KEY);
439
431
  spinner?.stop();
@@ -459,10 +451,10 @@ async function main() {
459
451
  if (legacyMode) {
460
452
  spinner?.stop();
461
453
  if (!jsonMode)
462
- process.stdout.write(chalk.dim('\n scanning...'));
454
+ process.stdout.write(chalk.dim('\n pulling threads...'));
463
455
  const collected = await collectAll();
464
456
  if (!jsonMode)
465
- process.stdout.write(chalk.dim(` ${collected.sources.length} sources thinking...`));
457
+ process.stdout.write(chalk.dim(` ${collected.sources.length} sources... thinking`));
466
458
  const analysis = await analyzeWithLLM(collected.data, key);
467
459
  if (jsonMode) {
468
460
  console.log(JSON.stringify({ collected, analysis }, null, 2));
@@ -475,12 +467,12 @@ async function main() {
475
467
  // Agent mode (default): multi-turn investigation with Anthropic
476
468
  if (!ANTHROPIC_KEY) {
477
469
  spinner?.stop();
478
- console.log(chalk.red('\n\n Agent mode requires ANTHROPIC_API_KEY. Use --legacy for OpenRouter mode.\n'));
470
+ console.log(chalk.red('\n\n missing API key\n'));
479
471
  process.exit(1);
480
472
  }
481
473
  // ── CRM: show relationship map before agent runs ──
482
474
  if (interactive && !crmShown) {
483
- spinner?.start('scanning messages');
475
+ spinner?.start('getting to know you');
484
476
  const calCtx = await calendarP;
485
477
  await showCRM(ANTHROPIC_KEY, { calendarContext: calCtx, spinner });
486
478
  }
@@ -608,7 +600,7 @@ async function main() {
608
600
  ...(analysis.decisions || []),
609
601
  ].some((d) => d.options?.length);
610
602
  if (!hasActionable && !(analysis.alpha?.length)) {
611
- console.log(GRN(' All clear — nothing needs you right now.'));
603
+ console.log(DIM(' All clear — nothing needs you right now.'));
612
604
  console.log();
613
605
  }
614
606
  renderDivider();
@@ -626,7 +618,7 @@ async function main() {
626
618
  const tgToken = process.env.TELEGRAM_BOT_TOKEN;
627
619
  if (tgToken) {
628
620
  transports.register(new TelegramTransport(tgToken));
629
- console.log(chalk.dim(' Telegram bot active'));
621
+ // silent — Telegram just works
630
622
  }
631
623
  // Start transport layer for inbound messages
632
624
  transports.startAll(async (inbound) => {
@@ -660,7 +652,7 @@ async function main() {
660
652
  return result.response;
661
653
  }
662
654
  catch (err) {
663
- process.stderr.write(` conversation error: ${err instanceof Error ? err.message : String(err)}\n`);
655
+ process.stderr.write(` couldn't reply to ${contactName}\n`);
664
656
  }
665
657
  }
666
658
  return null;
@@ -673,22 +665,21 @@ async function main() {
673
665
  recordCrash(err.message, 'message-loop');
674
666
  },
675
667
  });
676
- console.log(chalk.dim(` Watching for messages (${transports.getAvailable().join(', ')})`));
677
- console.log();
668
+ if (!daemonMode) {
669
+ console.log(chalk.dim(' listening'));
670
+ console.log();
671
+ }
678
672
  // Start health server for daemon mode
679
673
  let gw = null;
680
674
  if (daemonMode) {
681
675
  startHealthServer();
682
- console.log(chalk.dim(` Health check: http://127.0.0.1:19876/health`));
683
- // Start inbound gateway (rply-mac-server pushes messages here via Tailscale)
684
676
  gw = startGateway(ANTHROPIC_KEY);
685
- console.log(chalk.dim(` Gateway: :${gw.port} (rply-mac-server → Tailscale → here)`));
686
677
  }
687
678
  // Generate knowledge bases from CRM on startup
688
679
  try {
689
- const crm = buildCRM();
680
+ const crm = await buildCRM();
690
681
  if (crm.threads.length)
691
- updateAutoKnowledge(crm);
682
+ await updateAutoKnowledge(crm);
692
683
  }
693
684
  catch { }
694
685
  // Periodic progress save (every 5 min while running)
package/dist/crm.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Relationship Map — high-touch contact intelligence.
3
3
  *
4
- * Queries iMessage DB directly with CTE-based SQL:
5
- * - Response time pairing (their msg your first reply)
6
- * - Ghost detection (waiting on you / you're waiting on them)
7
- * - Thread temperature from message velocity
8
- * - Recent message pulls for LLM-powered relationship summaries
9
- * - Filters out group chats, tapbacks, business messages
4
+ * Two data paths:
5
+ * 1. RPLY API (localhost:19851) preferred, no FDA needed, multi-platform
6
+ * 2. Direct iMessage SQLite fallback when RPLY unavailable
7
+ *
8
+ * Both paths produce the same CRM/ThreadState interface.
10
9
  */
11
10
  export interface ThreadState {
12
11
  name: string;
@@ -35,7 +34,7 @@ export interface CRM {
35
34
  yourAvgResponseSec: number | null;
36
35
  generatedAt: string;
37
36
  }
38
- export declare function buildCRM(): CRM;
37
+ export declare function buildCRM(): Promise<CRM>;
39
38
  export declare function streamEnrichedCRM(crm: CRM, apiKey: string, opts?: {
40
39
  calendarContext?: string;
41
40
  timeOfDay?: string;