life-pulse 2.3.11 → 2.4.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/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';
@@ -91,6 +87,22 @@ function normalizePhoneCandidate(raw) {
91
87
  return '+' + t.slice(1).replace(/\D/g, '');
92
88
  return t.replace(/\D/g, '');
93
89
  }
90
+ function normalizeHostOrIp(raw) {
91
+ let t = raw.trim();
92
+ if (!t)
93
+ return '';
94
+ t = t.replace(/^https?:\/\//i, '');
95
+ t = t.split('/')[0] || '';
96
+ t = t.replace(/:\d+$/, '');
97
+ return t.trim();
98
+ }
99
+ function extractExecOutput(error) {
100
+ const e = error;
101
+ const stdout = typeof e?.stdout === 'string' ? e.stdout : Buffer.isBuffer(e?.stdout) ? e.stdout.toString('utf-8') : '';
102
+ const stderr = typeof e?.stderr === 'string' ? e.stderr : Buffer.isBuffer(e?.stderr) ? e.stderr.toString('utf-8') : '';
103
+ const message = typeof e?.message === 'string' ? e.message : '';
104
+ return [stdout, stderr, message].filter(Boolean).join('\n');
105
+ }
94
106
  async function promptLine(question, fallback = '') {
95
107
  if (!process.stdin.isTTY)
96
108
  return fallback;
@@ -124,6 +136,112 @@ function detectOpenClawToken() {
124
136
  }
125
137
  return '';
126
138
  }
139
+ function detectTailscaleHostOrIp() {
140
+ const dns = getTailscaleHostname();
141
+ if (dns)
142
+ return normalizeHostOrIp(dns);
143
+ try {
144
+ const out = execSync('tailscale ip -4', {
145
+ stdio: 'pipe',
146
+ timeout: 5000,
147
+ encoding: 'utf-8',
148
+ });
149
+ const ip = out
150
+ .split('\n')
151
+ .map(s => s.trim())
152
+ .find(s => /^100\.\d+\.\d+\.\d+$/.test(s));
153
+ if (ip)
154
+ return ip;
155
+ }
156
+ catch { }
157
+ return '';
158
+ }
159
+ function hasHomebrew() {
160
+ try {
161
+ execSync('command -v brew', { stdio: 'pipe', timeout: 3000, encoding: 'utf-8' });
162
+ return true;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ function installTailscaleIfMissing() {
169
+ if (hasTailscale())
170
+ return true;
171
+ if (!hasHomebrew())
172
+ return false;
173
+ try {
174
+ execSync('brew list --cask tailscale >/dev/null 2>&1 || brew install --cask tailscale', {
175
+ stdio: 'pipe',
176
+ timeout: 8 * 60 * 1000,
177
+ shell: '/bin/zsh',
178
+ });
179
+ }
180
+ catch { }
181
+ return hasTailscale();
182
+ }
183
+ function extractTailscaleAuthUrl(output) {
184
+ const m = output.match(/https:\/\/login\.tailscale\.com\/[^\s"']+/i);
185
+ return m ? m[0] : '';
186
+ }
187
+ function kickOffTailscaleAuth() {
188
+ try {
189
+ execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
190
+ }
191
+ catch { }
192
+ let output = '';
193
+ try {
194
+ output = execSync('tailscale up --accept-dns=false', {
195
+ stdio: 'pipe',
196
+ timeout: 15_000,
197
+ encoding: 'utf-8',
198
+ });
199
+ }
200
+ catch (err) {
201
+ output = extractExecOutput(err);
202
+ }
203
+ const authUrl = extractTailscaleAuthUrl(output);
204
+ if (authUrl) {
205
+ try {
206
+ execSync(`open ${JSON.stringify(authUrl)}`, { stdio: 'pipe', timeout: 5000 });
207
+ }
208
+ catch { }
209
+ }
210
+ }
211
+ async function waitForTailscaleHost(maxMs = 60_000) {
212
+ const start = Date.now();
213
+ while (Date.now() - start < maxMs) {
214
+ const host = detectTailscaleHostOrIp();
215
+ if (host)
216
+ return host;
217
+ await sleep(1500);
218
+ }
219
+ return '';
220
+ }
221
+ async function ensureTailscaleHostForPair(spinner) {
222
+ let host = detectTailscaleHostOrIp();
223
+ if (host)
224
+ return host;
225
+ spinner?.start('setting up secure link');
226
+ const installed = installTailscaleIfMissing();
227
+ if (!installed) {
228
+ spinner?.stop();
229
+ return '';
230
+ }
231
+ try {
232
+ execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
233
+ }
234
+ catch { }
235
+ host = await waitForTailscaleHost(12_000);
236
+ if (host) {
237
+ spinner?.stop();
238
+ return host;
239
+ }
240
+ kickOffTailscaleAuth();
241
+ host = await waitForTailscaleHost(60_000);
242
+ spinner?.stop();
243
+ return host;
244
+ }
127
245
  function writeNoxRouteJson(payload) {
128
246
  const desktop = join(homedir(), 'Desktop');
129
247
  mkdirSync(desktop, { recursive: true });
@@ -262,88 +380,23 @@ async function showCRM(apiKey, opts) {
262
380
  async function main() {
263
381
  const createdPromptFiles = ensurePromptLayerFiles();
264
382
  const jsonMode = process.argv.includes('--json');
265
- const rawMode = process.argv.includes('--raw');
266
383
  const statusMode = process.argv.includes('--status');
267
384
  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');
385
+ const pairMode = process.argv.includes('--pair');
275
386
  // Install crash handlers early (Anthropic pattern: recover from failures)
276
387
  installCrashHandlers((err) => {
277
388
  console.error(chalk.red(` crash: ${err.message}`));
278
389
  });
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
390
+ // --pair: generate Desktop/nox-route.json for NOX routing
339
391
  if (pairMode) {
340
392
  const defaultName = getUserName() || '';
341
- const detectedHost = getTailscaleHostname() || '';
342
393
  const detectedToken = detectOpenClawToken();
343
394
  const envPhone = normalizePhoneCandidate(process.env.LIFE_PULSE_SELF_PHONE
344
395
  || process.env.NOX_OWNER_PHONE
345
396
  || process.env.LIFE_PULSE_BRIEF_SMS_PHONE
346
397
  || '');
398
+ const pairSpinner = process.stdin.isTTY ? new Spinner() : undefined;
399
+ const detectedHost = normalizeHostOrIp(await ensureTailscaleHostForPair(pairSpinner));
347
400
  console.log();
348
401
  console.log(chalk.bold.hex('#c0caf5')(' pair with nox'));
349
402
  console.log(chalk.dim(' we will create Desktop/nox-route.json'));
@@ -355,9 +408,10 @@ async function main() {
355
408
  console.log(chalk.red(' missing phone number'));
356
409
  return;
357
410
  }
358
- const host = await promptLine('tailscale host/ip', detectedHost);
411
+ const host = detectedHost;
359
412
  if (!host) {
360
- console.log(chalk.red(' missing tailscale host/ip'));
413
+ console.log(chalk.red(' secure link not ready yet'));
414
+ console.log(chalk.dim(' keep tailscale signed in, then rerun: npx life-pulse --pair'));
361
415
  return;
362
416
  }
363
417
  const token = await promptLine('openclaw token', detectedToken);
@@ -381,17 +435,6 @@ async function main() {
381
435
  console.log();
382
436
  return;
383
437
  }
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
438
  // --daemon: check if already running
396
439
  if (daemonMode && isDaemonRunning()) {
397
440
  console.log(chalk.yellow(' already running'));
@@ -426,11 +469,6 @@ async function main() {
426
469
  console.log();
427
470
  return;
428
471
  }
429
- if (rawMode) {
430
- const collected = await collectAll();
431
- console.log(JSON.stringify(collected, null, 2));
432
- return;
433
- }
434
472
  // ── Start session tracking ──
435
473
  const sessionProgress = startSession();
436
474
  const timeSinceLastSession = getTimeSinceLastSession();
@@ -453,113 +491,12 @@ async function main() {
453
491
  const installStateExists = existsSync(join(stateDir, 'install-state.json'));
454
492
  const noSessionHistory = sessionProgress.totalSessions <= 1;
455
493
  const isSetupFlag = process.argv.includes('--setup');
456
- if ((noSessionHistory && !installStateExists && !rawMode && !jsonMode && !daemonMode && !process.argv.includes('--install'))
494
+ if ((noSessionHistory && !installStateExists && !jsonMode && !daemonMode)
457
495
  || isSetupFlag) {
458
496
  // First run: launch full installer
459
497
  await runInstaller(ANTHROPIC_KEY);
460
498
  return;
461
499
  }
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
500
  if (!ANTHROPIC_KEY) {
564
501
  console.log(chalk.red('\n\n missing API key\n'));
565
502
  process.exit(1);
@@ -592,7 +529,7 @@ async function main() {
592
529
  let crmShown = false;
593
530
  const setupMode = process.argv.includes('--setup');
594
531
  const isFirstRun = sessionProgress.totalSessions === 1;
595
- if ((needsDiscovery() || setupMode || isFirstRun) && !rawMode && !jsonMode) {
532
+ if ((needsDiscovery() || setupMode || isFirstRun) && !jsonMode) {
596
533
  // Welcome message for first run
597
534
  if (isFirstRun && interactive) {
598
535
  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'),
19
- // 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'),
10
+ // Brand — dark purple accent
11
+ accent: chalk.hex('#6b21a8'),
12
+ brand: chalk.bold.hex('#6b21a8'),
13
+ // Semantic — dark purple tones
14
+ ok: chalk.hex('#4a1d6e'),
15
+ warn: chalk.hex('#6b3fa0'),
16
+ err: chalk.bold.hex('#4a1d6e'),
17
+ info: chalk.hex('#6b3fa0'),
18
+ // Text hierarchy (darkest → lightest)
19
+ hd: chalk.bold.hex('#3b0764'),
20
+ heading: chalk.bold.hex('#3b0764'),
21
+ bright: chalk.hex('#4a1d6e'),
22
+ text: chalk.hex('#581c87'),
23
+ mid: chalk.hex('#7e22ce'),
24
+ dim: chalk.hex('#9333ea'),
25
+ faint: chalk.hex('#a855f7'),
26
+ rule: chalk.hex('#c084fc'),
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.1",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {