life-pulse 2.4.0 → 2.4.2

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
@@ -28,7 +28,7 @@ import { findHandlesForName } from './contacts.js';
28
28
  import { ensurePromptLayerFiles } from './prompt-layers.js';
29
29
  import { hasTailscale, startFunnel, getHostname as getTailscaleHostname } from './tunnel.js';
30
30
  import chalk from 'chalk';
31
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
31
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, cpSync, rmSync } from 'fs';
32
32
  import { join, dirname } from 'path';
33
33
  import { fileURLToPath } from 'url';
34
34
  import { homedir } from 'os';
@@ -87,6 +87,47 @@ function normalizePhoneCandidate(raw) {
87
87
  return '+' + t.slice(1).replace(/\D/g, '');
88
88
  return t.replace(/\D/g, '');
89
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 resolveTailscaleBin() {
100
+ try {
101
+ const out = execSync('command -v tailscale', {
102
+ stdio: 'pipe',
103
+ timeout: 3000,
104
+ encoding: 'utf-8',
105
+ shell: '/bin/zsh',
106
+ }).trim();
107
+ if (out)
108
+ return out;
109
+ }
110
+ catch { }
111
+ const candidates = [
112
+ '/Applications/Tailscale.app/Contents/MacOS/Tailscale',
113
+ join(homedir(), 'Applications', 'Tailscale.app', 'Contents', 'MacOS', 'Tailscale'),
114
+ ];
115
+ for (const c of candidates) {
116
+ if (existsSync(c))
117
+ return c;
118
+ }
119
+ return '';
120
+ }
121
+ function hasUsableTailscale() {
122
+ return !!resolveTailscaleBin();
123
+ }
124
+ function extractExecOutput(error) {
125
+ const e = error;
126
+ const stdout = typeof e?.stdout === 'string' ? e.stdout : Buffer.isBuffer(e?.stdout) ? e.stdout.toString('utf-8') : '';
127
+ const stderr = typeof e?.stderr === 'string' ? e.stderr : Buffer.isBuffer(e?.stderr) ? e.stderr.toString('utf-8') : '';
128
+ const message = typeof e?.message === 'string' ? e.message : '';
129
+ return [stdout, stderr, message].filter(Boolean).join('\n');
130
+ }
90
131
  async function promptLine(question, fallback = '') {
91
132
  if (!process.stdin.isTTY)
92
133
  return fallback;
@@ -120,6 +161,163 @@ function detectOpenClawToken() {
120
161
  }
121
162
  return '';
122
163
  }
164
+ function detectTailscaleHostOrIp() {
165
+ const bin = resolveTailscaleBin();
166
+ if (!bin)
167
+ return '';
168
+ try {
169
+ const out = execFileSync(bin, ['status', '--json'], {
170
+ stdio: 'pipe',
171
+ timeout: 5000,
172
+ encoding: 'utf-8',
173
+ });
174
+ const status = JSON.parse(out);
175
+ const dns = normalizeHostOrIp(String(status?.Self?.DNSName || '').replace(/\.$/, ''));
176
+ if (dns)
177
+ return dns;
178
+ const ips = Array.isArray(status?.Self?.TailscaleIPs) ? status.Self.TailscaleIPs : [];
179
+ const ip = ips.find((x) => typeof x === 'string' && /^100\.\d+\.\d+\.\d+$/.test(x));
180
+ if (ip)
181
+ return String(ip);
182
+ }
183
+ catch { }
184
+ try {
185
+ const out = execFileSync(bin, ['ip', '-4'], {
186
+ stdio: 'pipe',
187
+ timeout: 5000,
188
+ encoding: 'utf-8',
189
+ });
190
+ const ip = out
191
+ .split('\n')
192
+ .map(s => s.trim())
193
+ .find(s => /^100\.\d+\.\d+\.\d+$/.test(s));
194
+ if (ip)
195
+ return ip;
196
+ }
197
+ catch { }
198
+ return '';
199
+ }
200
+ function hasHomebrew() {
201
+ try {
202
+ execSync('command -v brew', { stdio: 'pipe', timeout: 3000, encoding: 'utf-8' });
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ function installTailscaleIfMissing() {
210
+ if (hasUsableTailscale())
211
+ return true;
212
+ if (hasHomebrew()) {
213
+ try {
214
+ execSync('brew list --cask tailscale >/dev/null 2>&1 || brew install --cask tailscale', {
215
+ stdio: 'pipe',
216
+ timeout: 8 * 60 * 1000,
217
+ shell: '/bin/zsh',
218
+ });
219
+ }
220
+ catch { }
221
+ if (hasUsableTailscale())
222
+ return true;
223
+ }
224
+ // Homebrew-less fallback: install official app bundle from Tailscale zip.
225
+ try {
226
+ const tmpDir = join('/tmp', `life-pulse-ts-${Date.now()}`);
227
+ mkdirSync(tmpDir, { recursive: true });
228
+ const zipPath = join(tmpDir, 'tailscale.zip');
229
+ execSync(`curl -L --fail -o ${JSON.stringify(zipPath)} https://pkgs.tailscale.com/stable/Tailscale-latest-macos.zip`, {
230
+ stdio: 'pipe',
231
+ timeout: 3 * 60 * 1000,
232
+ shell: '/bin/zsh',
233
+ });
234
+ execSync(`unzip -oq ${JSON.stringify(zipPath)} -d ${JSON.stringify(tmpDir)}`, {
235
+ stdio: 'pipe',
236
+ timeout: 60_000,
237
+ shell: '/bin/zsh',
238
+ });
239
+ const appSrc = join(tmpDir, 'Tailscale.app');
240
+ if (existsSync(appSrc)) {
241
+ const appDestSystem = '/Applications/Tailscale.app';
242
+ const appDestUser = join(homedir(), 'Applications', 'Tailscale.app');
243
+ try {
244
+ cpSync(appSrc, appDestSystem, { recursive: true, force: true });
245
+ }
246
+ catch {
247
+ mkdirSync(join(homedir(), 'Applications'), { recursive: true });
248
+ cpSync(appSrc, appDestUser, { recursive: true, force: true });
249
+ }
250
+ }
251
+ rmSync(tmpDir, { recursive: true, force: true });
252
+ }
253
+ catch { }
254
+ return hasUsableTailscale();
255
+ }
256
+ function extractTailscaleAuthUrl(output) {
257
+ const m = output.match(/https:\/\/login\.tailscale\.com\/[^\s"']+/i);
258
+ return m ? m[0] : '';
259
+ }
260
+ function kickOffTailscaleAuth() {
261
+ const bin = resolveTailscaleBin();
262
+ if (!bin)
263
+ return;
264
+ try {
265
+ execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
266
+ }
267
+ catch { }
268
+ let output = '';
269
+ try {
270
+ output = execFileSync(bin, ['up', '--accept-dns=false'], {
271
+ stdio: 'pipe',
272
+ timeout: 15_000,
273
+ encoding: 'utf-8',
274
+ });
275
+ }
276
+ catch (err) {
277
+ output = extractExecOutput(err);
278
+ }
279
+ const authUrl = extractTailscaleAuthUrl(output);
280
+ if (authUrl) {
281
+ try {
282
+ execSync(`open ${JSON.stringify(authUrl)}`, { stdio: 'pipe', timeout: 5000 });
283
+ }
284
+ catch { }
285
+ }
286
+ }
287
+ async function waitForTailscaleHost(maxMs = 60_000) {
288
+ const start = Date.now();
289
+ while (Date.now() - start < maxMs) {
290
+ const host = detectTailscaleHostOrIp();
291
+ if (host)
292
+ return host;
293
+ await sleep(1500);
294
+ }
295
+ return '';
296
+ }
297
+ async function ensureTailscaleHostForPair(spinner) {
298
+ let host = detectTailscaleHostOrIp();
299
+ if (host)
300
+ return host;
301
+ spinner?.start('setting up secure link');
302
+ const installed = installTailscaleIfMissing();
303
+ if (!installed) {
304
+ spinner?.stop();
305
+ return '';
306
+ }
307
+ try {
308
+ execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
309
+ }
310
+ catch { }
311
+ host = await waitForTailscaleHost(12_000);
312
+ if (host) {
313
+ spinner?.stop();
314
+ return host;
315
+ }
316
+ kickOffTailscaleAuth();
317
+ host = await waitForTailscaleHost(60_000);
318
+ spinner?.stop();
319
+ return host;
320
+ }
123
321
  function writeNoxRouteJson(payload) {
124
322
  const desktop = join(homedir(), 'Desktop');
125
323
  mkdirSync(desktop, { recursive: true });
@@ -268,12 +466,13 @@ async function main() {
268
466
  // --pair: generate Desktop/nox-route.json for NOX routing
269
467
  if (pairMode) {
270
468
  const defaultName = getUserName() || '';
271
- const detectedHost = getTailscaleHostname() || '';
272
469
  const detectedToken = detectOpenClawToken();
273
470
  const envPhone = normalizePhoneCandidate(process.env.LIFE_PULSE_SELF_PHONE
274
471
  || process.env.NOX_OWNER_PHONE
275
472
  || process.env.LIFE_PULSE_BRIEF_SMS_PHONE
276
473
  || '');
474
+ const pairSpinner = process.stdin.isTTY ? new Spinner() : undefined;
475
+ const detectedHost = normalizeHostOrIp(await ensureTailscaleHostForPair(pairSpinner));
277
476
  console.log();
278
477
  console.log(chalk.bold.hex('#c0caf5')(' pair with nox'));
279
478
  console.log(chalk.dim(' we will create Desktop/nox-route.json'));
@@ -285,9 +484,10 @@ async function main() {
285
484
  console.log(chalk.red(' missing phone number'));
286
485
  return;
287
486
  }
288
- const host = await promptLine('tailscale host/ip', detectedHost);
487
+ const host = detectedHost;
289
488
  if (!host) {
290
- console.log(chalk.red(' missing tailscale host/ip'));
489
+ console.log(chalk.red(' secure link not ready yet'));
490
+ console.log(chalk.dim(' keep tailscale signed in, then rerun: npx life-pulse --pair'));
291
491
  return;
292
492
  }
293
493
  const token = await promptLine('openclaw token', detectedToken);
package/dist/ui/theme.js CHANGED
@@ -7,23 +7,23 @@
7
7
  import chalk from 'chalk';
8
8
  // ─── Palette ──────────────────────────────────────
9
9
  export const C = {
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'),
18
- // Text hierarchy (brightinvisible)
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'),
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 (darkestlightest)
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'),
27
27
  };
28
28
  // Lowercase alias (backward compat)
29
29
  export const c = C;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "life-pulse",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {