life-pulse 2.3.11 → 2.4.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/dist/cli.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { collectAll } from './index.js';
3
2
  import { runAgent } from './agent.js';
4
3
  import { saveState, saveDecisions } from './state.js';
5
4
  // ProgressRenderer unused — removed
@@ -16,18 +15,15 @@ import { buildPersonalSummary, getUserName } from './profile.js';
16
15
  import { startSession, endSession, loadProgress, recordDecision, recordSurfaced, getTimeSinceLastSession, getNewDiscoveries } from './session-progress.js';
17
16
  import { runDiscovery, formatDiscoveryReport } from './icloud-discovery.js';
18
17
  import { getSourcesNeedingScan, getChangeSummary } from './incremental.js';
19
- import { startHealthServer, stopHealthServer, isDaemonRunning, getHealthStatus, touchActivity } from './health.js';
20
- // New: long-running agent harness modules
21
- import { runInitCheck } from './init-check.js';
18
+ import { startHealthServer, stopHealthServer, isDaemonRunning, touchActivity } from './health.js';
22
19
  import { installCrashHandlers, recordSuccess, recordCrash, checkpoint, getCrashStats } from './watchdog.js';
23
20
  import { startMessageLoop, sendMessage } from './message-loop.js';
24
21
  import { scanForSkills } from './skill-loader.js';
25
22
  import { TransportManager, IMessageTransport, TelegramTransport } from './transport.js';
26
23
  import { runInstaller } from './installer.js';
27
24
  import { converse } from './conversation.js';
28
- import { startGateway, isGatewayUp } from './sms-gateway.js';
25
+ import { startGateway } from './sms-gateway.js';
29
26
  import { updateAutoKnowledge } from './knowledge.js';
30
- import { startRouter, initClientRegistry } from './router.js';
31
27
  import { findHandlesForName } from './contacts.js';
32
28
  import { ensurePromptLayerFiles } from './prompt-layers.js';
33
29
  import { hasTailscale, startFunnel, getHostname as getTailscaleHostname } from './tunnel.js';
@@ -262,80 +258,14 @@ async function showCRM(apiKey, opts) {
262
258
  async function main() {
263
259
  const createdPromptFiles = ensurePromptLayerFiles();
264
260
  const jsonMode = process.argv.includes('--json');
265
- const rawMode = process.argv.includes('--raw');
266
261
  const statusMode = process.argv.includes('--status');
267
262
  const daemonMode = process.argv.includes('--daemon');
268
- const healthMode = process.argv.includes('--health');
269
- const initCheckMode = process.argv.includes('--check');
270
- const phoneSetupMode = process.argv.includes('--phone-setup');
271
- const testSmsMode = process.argv.includes('--test-sms');
272
- const pairMode = process.argv.includes('--pair') || process.argv.includes('--export-route');
273
- const routerMode = process.argv.includes('--router');
274
- const initClientsMode = process.argv.includes('--init-clients');
263
+ const pairMode = process.argv.includes('--pair');
275
264
  // Install crash handlers early (Anthropic pattern: recover from failures)
276
265
  installCrashHandlers((err) => {
277
266
  console.error(chalk.red(` crash: ${err.message}`));
278
267
  });
279
- // --check: pre-flight verification (Anthropic pattern: verify state before work)
280
- if (initCheckMode) {
281
- const status = runInitCheck();
282
- console.log();
283
- for (const c of status.checks) {
284
- const icon = c.passed ? chalk.green('✓') : chalk.red('✗');
285
- console.log(` ${icon} ${c.check}`);
286
- }
287
- console.log();
288
- console.log(chalk.dim(` ${status.recommendation}`));
289
- console.log();
290
- return;
291
- }
292
- // --router: legacy bridge mode (multi-machine relay setups)
293
- if (routerMode) {
294
- console.log(chalk.bold.hex('#c0caf5')(' life-pulse'));
295
- console.log(chalk.dim(' routing messages'));
296
- console.log();
297
- const router = startRouter();
298
- process.on('SIGINT', () => { router.stop(); process.exit(0); });
299
- process.on('SIGTERM', () => { router.stop(); process.exit(0); });
300
- await new Promise(() => { });
301
- return;
302
- }
303
- // --init-clients: create template clients.json
304
- if (initClientsMode) {
305
- initClientRegistry();
306
- return;
307
- }
308
- // --phone-setup: run phone number setup from installer
309
- if (phoneSetupMode) {
310
- await runInstaller(ANTHROPIC_KEY);
311
- return;
312
- }
313
- // --test-sms: test the gateway by posting a fake inbound message
314
- if (testSmsMode) {
315
- const up = await isGatewayUp();
316
- if (!up) {
317
- console.log(chalk.red(' not running — start life-pulse first'));
318
- return;
319
- }
320
- const phone = process.argv[process.argv.indexOf('--test-sms') + 1] || '+1234567890';
321
- try {
322
- const resp = await fetch('http://127.0.0.1:19877/inbound', {
323
- method: 'POST',
324
- headers: { 'Content-Type': 'application/json' },
325
- body: JSON.stringify({ phone, message: 'test message from life-pulse' }),
326
- });
327
- const data = await resp.json();
328
- if (data.response)
329
- console.log(chalk.green(` replied: ${data.response}`));
330
- else
331
- console.log(chalk.red(` failed: ${data.error}`));
332
- }
333
- catch (err) {
334
- console.log(chalk.red(` couldn't connect — is life-pulse running?`));
335
- }
336
- return;
337
- }
338
- // --pair / --export-route: generate Desktop/nox-route.json for NOX routing
268
+ // --pair: generate Desktop/nox-route.json for NOX routing
339
269
  if (pairMode) {
340
270
  const defaultName = getUserName() || '';
341
271
  const detectedHost = getTailscaleHostname() || '';
@@ -381,17 +311,6 @@ async function main() {
381
311
  console.log();
382
312
  return;
383
313
  }
384
- // --health: check if daemon is running and get status
385
- if (healthMode) {
386
- if (isDaemonRunning()) {
387
- const status = getHealthStatus();
388
- console.log(JSON.stringify(status, null, 2));
389
- }
390
- else {
391
- console.log(JSON.stringify({ status: 'not_running' }));
392
- }
393
- return;
394
- }
395
314
  // --daemon: check if already running
396
315
  if (daemonMode && isDaemonRunning()) {
397
316
  console.log(chalk.yellow(' already running'));
@@ -426,11 +345,6 @@ async function main() {
426
345
  console.log();
427
346
  return;
428
347
  }
429
- if (rawMode) {
430
- const collected = await collectAll();
431
- console.log(JSON.stringify(collected, null, 2));
432
- return;
433
- }
434
348
  // ── Start session tracking ──
435
349
  const sessionProgress = startSession();
436
350
  const timeSinceLastSession = getTimeSinceLastSession();
@@ -453,113 +367,12 @@ async function main() {
453
367
  const installStateExists = existsSync(join(stateDir, 'install-state.json'));
454
368
  const noSessionHistory = sessionProgress.totalSessions <= 1;
455
369
  const isSetupFlag = process.argv.includes('--setup');
456
- if ((noSessionHistory && !installStateExists && !rawMode && !jsonMode && !daemonMode && !process.argv.includes('--install'))
370
+ if ((noSessionHistory && !installStateExists && !jsonMode && !daemonMode)
457
371
  || isSetupFlag) {
458
372
  // First run: launch full installer
459
373
  await runInstaller(ANTHROPIC_KEY);
460
374
  return;
461
375
  }
462
- if (process.argv.includes('--install')) {
463
- const config = loadConfig();
464
- const [hour, min] = config.briefTime.split(':').map(Number);
465
- const logDir = join(homedir(), 'Library/Logs/life-pulse');
466
- const agentsDir = join(homedir(), 'Library/LaunchAgents');
467
- const envDir = join(homedir(), '.config/life-pulse');
468
- try {
469
- mkdirSync(logDir, { recursive: true });
470
- }
471
- catch { }
472
- try {
473
- mkdirSync(agentsDir, { recursive: true });
474
- }
475
- catch { }
476
- try {
477
- mkdirSync(envDir, { recursive: true });
478
- }
479
- catch { }
480
- // Detect npx path for the installed package
481
- let npxPath;
482
- try {
483
- npxPath = execSync('which npx', { encoding: 'utf-8', timeout: 5000 }).trim();
484
- }
485
- catch {
486
- npxPath = '/opt/homebrew/bin/npx';
487
- }
488
- // Env file hint
489
- const envPath = join(envDir, '.env');
490
- if (!existsSync(envPath)) {
491
- writeFileSync(envPath, '# life-pulse environment\n# ANTHROPIC_API_KEY=sk-ant-...\n# TELEGRAM_BOT_TOKEN=...\n');
492
- console.log(chalk.dim(` Created ${envPath} — add your API key`));
493
- }
494
- // 1. Morning brief plist (scheduled, one-shot)
495
- const morningPlist = `<?xml version="1.0" encoding="UTF-8"?>
496
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
497
- <plist version="1.0">
498
- <dict>
499
- <key>Label</key><string>com.life-pulse.morning</string>
500
- <key>ProgramArguments</key>
501
- <array>
502
- <string>${npxPath}</string><string>life-pulse</string>
503
- </array>
504
- <key>StartCalendarInterval</key>
505
- <dict><key>Hour</key><integer>${hour}</integer><key>Minute</key><integer>${min}</integer></dict>
506
- <key>StandardOutPath</key><string>${join(logDir, 'morning.log')}</string>
507
- <key>StandardErrorPath</key><string>${join(logDir, 'morning-err.log')}</string>
508
- <key>EnvironmentVariables</key>
509
- <dict>
510
- <key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
511
- <key>HOME</key><string>${homedir()}</string>
512
- </dict>
513
- </dict>
514
- </plist>`;
515
- // 2. Daemon plist (KeepAlive — auto-restart on crash)
516
- const daemonPlist = `<?xml version="1.0" encoding="UTF-8"?>
517
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
518
- <plist version="1.0">
519
- <dict>
520
- <key>Label</key><string>com.life-pulse.daemon</string>
521
- <key>ProgramArguments</key>
522
- <array>
523
- <string>${npxPath}</string><string>life-pulse</string><string>--daemon</string>
524
- </array>
525
- <key>RunAtLoad</key><true/>
526
- <key>KeepAlive</key>
527
- <dict>
528
- <key>SuccessfulExit</key><false/>
529
- </dict>
530
- <key>ThrottleInterval</key><integer>30</integer>
531
- <key>StandardOutPath</key><string>${join(logDir, 'daemon.log')}</string>
532
- <key>StandardErrorPath</key><string>${join(logDir, 'daemon-err.log')}</string>
533
- <key>EnvironmentVariables</key>
534
- <dict>
535
- <key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
536
- <key>HOME</key><string>${homedir()}</string>
537
- </dict>
538
- </dict>
539
- </plist>`;
540
- writeFileSync(join(agentsDir, 'com.life-pulse.morning.plist'), morningPlist);
541
- writeFileSync(join(agentsDir, 'com.life-pulse.daemon.plist'), daemonPlist);
542
- // Prefer direct NOX -> this Mac flow: ensure tailscale endpoint is configured.
543
- let noxEndpoint = null;
544
- if (hasTailscale()) {
545
- noxEndpoint = startFunnel(19877);
546
- if (!noxEndpoint) {
547
- const host = getTailscaleHostname();
548
- if (host)
549
- noxEndpoint = `https://${host}`;
550
- }
551
- }
552
- console.log(chalk.dim(' running in the background'));
553
- console.log(chalk.dim(` morning brief at ${config.briefTime}`));
554
- if (noxEndpoint) {
555
- console.log(chalk.dim(` nox endpoint ${noxEndpoint}`));
556
- }
557
- else {
558
- console.log(chalk.dim(' nox endpoint unavailable — tailscale needed'));
559
- }
560
- console.log();
561
- return;
562
- }
563
376
  if (!ANTHROPIC_KEY) {
564
377
  console.log(chalk.red('\n\n missing API key\n'));
565
378
  process.exit(1);
@@ -592,7 +405,7 @@ async function main() {
592
405
  let crmShown = false;
593
406
  const setupMode = process.argv.includes('--setup');
594
407
  const isFirstRun = sessionProgress.totalSessions === 1;
595
- if ((needsDiscovery() || setupMode || isFirstRun) && !rawMode && !jsonMode) {
408
+ if ((needsDiscovery() || setupMode || isFirstRun) && !jsonMode) {
596
409
  // Welcome message for first run
597
410
  if (isFirstRun && interactive) {
598
411
  console.log();
package/dist/installer.js CHANGED
@@ -88,6 +88,52 @@ const PLATFORM_HINTS = {
88
88
  function platformHint(platform) {
89
89
  return PLATFORM_HINTS[platform] || '';
90
90
  }
91
+ // ─── Tailscale Auto-Install ─────────────────────────────────────
92
+ function hasBrew() {
93
+ try {
94
+ execSync('which brew', { stdio: 'pipe', timeout: 5000 });
95
+ return true;
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ async function installTailscale(spinner) {
102
+ if (hasBrew()) {
103
+ spinner?.start('installing tailscale via homebrew');
104
+ try {
105
+ execSync('brew install --cask tailscale', { stdio: 'pipe', timeout: 120_000 });
106
+ spinner?.stop();
107
+ // Launch the app so the CLI helper is available
108
+ try {
109
+ execSync('open -a Tailscale', { stdio: 'pipe', timeout: 10_000 });
110
+ }
111
+ catch { }
112
+ // Give it a moment to register the CLI
113
+ execSync('sleep 2');
114
+ if (hasTailscale()) {
115
+ console.log(DIM(' tailscale installed via homebrew'));
116
+ return true;
117
+ }
118
+ }
119
+ catch {
120
+ spinner?.stop();
121
+ }
122
+ }
123
+ // Fallback: open Mac App Store page for Tailscale
124
+ spinner?.start('opening tailscale download');
125
+ try {
126
+ execSync('open "https://apps.apple.com/app/tailscale/id1475387142"', { stdio: 'pipe', timeout: 5000 });
127
+ spinner?.stop();
128
+ console.log(DIM(' opened Tailscale in App Store'));
129
+ console.log(DIM(' install it, then run setup again'));
130
+ return false;
131
+ }
132
+ catch {
133
+ spinner?.stop();
134
+ return false;
135
+ }
136
+ }
91
137
  // ─── Main Installer Flow ────────────────────────────────────────
92
138
  export async function runInstaller(apiKey) {
93
139
  const state = loadState();
@@ -276,6 +322,15 @@ export async function runInstaller(apiKey) {
276
322
  const gw = startGateway(apiKey);
277
323
  setTimeout(() => gw.stop(), 60_000);
278
324
  }
325
+ // Auto-install Tailscale if missing
326
+ if (!hasTailscale()) {
327
+ console.log(DIM(' tailscale not found — installing'));
328
+ const installed = await installTailscale(spinner);
329
+ if (!installed) {
330
+ console.log(HD(' couldn\'t install tailscale automatically'));
331
+ console.log(DIM(' install manually: tailscale.com/download'));
332
+ }
333
+ }
279
334
  if (hasTailscale()) {
280
335
  const hostname = getTailscaleHostname();
281
336
  if (hostname) {
@@ -290,7 +345,8 @@ export async function runInstaller(apiKey) {
290
345
  console.log(DIM(` ${state.funnelUrl}`));
291
346
  }
292
347
  else {
293
- console.log(HD(' network connection incomplete run setup again'));
348
+ console.log(HD(' tailscale installed but not logged in'));
349
+ console.log(DIM(' run: tailscale login'));
294
350
  }
295
351
  }
296
352
  else {
package/dist/ui/theme.js CHANGED
@@ -7,24 +7,23 @@
7
7
  import chalk from 'chalk';
8
8
  // ─── Palette ──────────────────────────────────────
9
9
  export const C = {
10
- // Brand — the only color
11
- accent: chalk.hex('#7aa2f7'),
12
- brand: chalk.bold.hex('#7aa2f7'),
13
- // Semantic — all map to the grayscale hierarchy
14
- // "ok" is bright (not green), "err" is bright bold (not red)
15
- ok: chalk.hex('#c0caf5'),
16
- warn: chalk.hex('#a9b1d6'),
17
- err: chalk.bold.hex('#c0caf5'),
18
- info: chalk.hex('#a9b1d6'),
10
+ // Brand — deep purple accent
11
+ accent: chalk.hex('#9d7cd8'),
12
+ brand: chalk.bold.hex('#9d7cd8'),
13
+ // Semantic — purple-tinted grayscale
14
+ ok: chalk.hex('#b4a4d6'),
15
+ warn: chalk.hex('#8a7aab'),
16
+ err: chalk.bold.hex('#b4a4d6'),
17
+ info: chalk.hex('#8a7aab'),
19
18
  // Text hierarchy (bright → invisible)
20
- hd: chalk.bold.hex('#c0caf5'),
21
- heading: chalk.bold.hex('#c0caf5'),
22
- bright: chalk.hex('#c0caf5'),
23
- text: chalk.hex('#a9b1d6'),
24
- mid: chalk.hex('#737aa2'),
25
- dim: chalk.hex('#565f89'),
26
- faint: chalk.hex('#3b4261'),
27
- rule: chalk.hex('#292e42'),
19
+ hd: chalk.bold.hex('#b4a4d6'),
20
+ heading: chalk.bold.hex('#b4a4d6'),
21
+ bright: chalk.hex('#b4a4d6'),
22
+ text: chalk.hex('#8a7aab'),
23
+ mid: chalk.hex('#635d80'),
24
+ dim: chalk.hex('#4a4466'),
25
+ faint: chalk.hex('#332e4a'),
26
+ rule: chalk.hex('#231f36'),
28
27
  };
29
28
  // Lowercase alias (backward compat)
30
29
  export const c = C;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "life-pulse",
3
- "version": "2.3.11",
3
+ "version": "2.4.0",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {