life-pulse 2.4.0 → 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
@@ -87,6 +87,22 @@ 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 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
+ }
90
106
  async function promptLine(question, fallback = '') {
91
107
  if (!process.stdin.isTTY)
92
108
  return fallback;
@@ -120,6 +136,112 @@ function detectOpenClawToken() {
120
136
  }
121
137
  return '';
122
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
+ }
123
245
  function writeNoxRouteJson(payload) {
124
246
  const desktop = join(homedir(), 'Desktop');
125
247
  mkdirSync(desktop, { recursive: true });
@@ -268,12 +390,13 @@ async function main() {
268
390
  // --pair: generate Desktop/nox-route.json for NOX routing
269
391
  if (pairMode) {
270
392
  const defaultName = getUserName() || '';
271
- const detectedHost = getTailscaleHostname() || '';
272
393
  const detectedToken = detectOpenClawToken();
273
394
  const envPhone = normalizePhoneCandidate(process.env.LIFE_PULSE_SELF_PHONE
274
395
  || process.env.NOX_OWNER_PHONE
275
396
  || process.env.LIFE_PULSE_BRIEF_SMS_PHONE
276
397
  || '');
398
+ const pairSpinner = process.stdin.isTTY ? new Spinner() : undefined;
399
+ const detectedHost = normalizeHostOrIp(await ensureTailscaleHostForPair(pairSpinner));
277
400
  console.log();
278
401
  console.log(chalk.bold.hex('#c0caf5')(' pair with nox'));
279
402
  console.log(chalk.dim(' we will create Desktop/nox-route.json'));
@@ -285,9 +408,10 @@ async function main() {
285
408
  console.log(chalk.red(' missing phone number'));
286
409
  return;
287
410
  }
288
- const host = await promptLine('tailscale host/ip', detectedHost);
411
+ const host = detectedHost;
289
412
  if (!host) {
290
- 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'));
291
415
  return;
292
416
  }
293
417
  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.1",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {