life-pulse 2.4.1 → 2.4.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +188 -16
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -28,12 +28,13 @@ 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';
35
35
  import { execSync, execFileSync } from 'child_process';
36
36
  import { createInterface } from 'readline';
37
+ import { randomBytes } from 'crypto';
37
38
  import dayjs from 'dayjs';
38
39
  const collectedDecisions = [];
39
40
  const DEFAULT_CONFIG = {
@@ -96,6 +97,31 @@ function normalizeHostOrIp(raw) {
96
97
  t = t.replace(/:\d+$/, '');
97
98
  return t.trim();
98
99
  }
100
+ function resolveTailscaleBin() {
101
+ try {
102
+ const out = execSync('command -v tailscale', {
103
+ stdio: 'pipe',
104
+ timeout: 3000,
105
+ encoding: 'utf-8',
106
+ shell: '/bin/zsh',
107
+ }).trim();
108
+ if (out)
109
+ return out;
110
+ }
111
+ catch { }
112
+ const candidates = [
113
+ '/Applications/Tailscale.app/Contents/MacOS/Tailscale',
114
+ join(homedir(), 'Applications', 'Tailscale.app', 'Contents', 'MacOS', 'Tailscale'),
115
+ ];
116
+ for (const c of candidates) {
117
+ if (existsSync(c))
118
+ return c;
119
+ }
120
+ return '';
121
+ }
122
+ function hasUsableTailscale() {
123
+ return !!resolveTailscaleBin();
124
+ }
99
125
  function extractExecOutput(error) {
100
126
  const e = error;
101
127
  const stdout = typeof e?.stdout === 'string' ? e.stdout : Buffer.isBuffer(e?.stdout) ? e.stdout.toString('utf-8') : '';
@@ -117,14 +143,22 @@ async function promptLine(question, fallback = '') {
117
143
  });
118
144
  }
119
145
  function detectOpenClawToken() {
146
+ const bin = resolveOpenClawBin();
147
+ if (!bin)
148
+ return '';
120
149
  const keys = [
121
150
  'gateway.http.auth.token',
122
151
  'gateway.auth.token',
123
152
  'gateway.http.token',
153
+ 'gateway.token',
124
154
  ];
125
155
  for (const key of keys) {
126
156
  try {
127
- const out = execSync(`openclaw config get ${key}`, { stdio: 'pipe', timeout: 5000, encoding: 'utf-8' }).trim();
157
+ const out = execFileSync(bin, ['config', 'get', key], {
158
+ stdio: 'pipe',
159
+ timeout: 5000,
160
+ encoding: 'utf-8',
161
+ }).trim();
128
162
  if (!out)
129
163
  continue;
130
164
  const low = out.toLowerCase();
@@ -134,14 +168,116 @@ function detectOpenClawToken() {
134
168
  }
135
169
  catch { }
136
170
  }
171
+ // Fallback: parse any config listing output for token-like values.
172
+ const listCommands = [
173
+ ['config', 'list'],
174
+ ['config', 'show'],
175
+ ['config', '--json'],
176
+ ];
177
+ for (const cmd of listCommands) {
178
+ try {
179
+ const out = execFileSync(bin, cmd, {
180
+ stdio: 'pipe',
181
+ timeout: 5000,
182
+ encoding: 'utf-8',
183
+ });
184
+ const tokenMatch = out.match(/token[^:\n=]*[:=]\s*([A-Za-z0-9_\-]{16,})/i);
185
+ if (tokenMatch?.[1])
186
+ return tokenMatch[1];
187
+ }
188
+ catch { }
189
+ }
137
190
  return '';
138
191
  }
192
+ function resolveOpenClawBin() {
193
+ try {
194
+ const out = execSync('command -v openclaw', {
195
+ stdio: 'pipe',
196
+ timeout: 3000,
197
+ encoding: 'utf-8',
198
+ shell: '/bin/zsh',
199
+ }).trim();
200
+ if (out)
201
+ return out;
202
+ }
203
+ catch { }
204
+ const candidates = [
205
+ join(homedir(), 'Library', 'pnpm', 'openclaw'),
206
+ '/opt/homebrew/bin/openclaw',
207
+ '/usr/local/bin/openclaw',
208
+ ];
209
+ for (const c of candidates) {
210
+ if (existsSync(c))
211
+ return c;
212
+ }
213
+ return '';
214
+ }
215
+ function ensureOpenClawChatEndpoint(bin) {
216
+ const keys = [
217
+ 'gateway.http.endpoints.chatCompletions.enabled',
218
+ 'gateway.http.endpoints.chat.enabled',
219
+ ];
220
+ for (const key of keys) {
221
+ try {
222
+ execFileSync(bin, ['config', 'set', key, 'true'], {
223
+ stdio: 'pipe',
224
+ timeout: 5000,
225
+ encoding: 'utf-8',
226
+ });
227
+ }
228
+ catch { }
229
+ }
230
+ }
231
+ function ensureOpenClawToken() {
232
+ const bin = resolveOpenClawBin();
233
+ if (!bin)
234
+ return '';
235
+ ensureOpenClawChatEndpoint(bin);
236
+ const existing = detectOpenClawToken();
237
+ if (existing)
238
+ return existing;
239
+ const generated = randomBytes(20).toString('hex');
240
+ const keys = [
241
+ 'gateway.http.auth.token',
242
+ 'gateway.auth.token',
243
+ 'gateway.http.token',
244
+ 'gateway.token',
245
+ ];
246
+ for (const key of keys) {
247
+ try {
248
+ execFileSync(bin, ['config', 'set', key, generated], {
249
+ stdio: 'pipe',
250
+ timeout: 5000,
251
+ encoding: 'utf-8',
252
+ });
253
+ }
254
+ catch { }
255
+ }
256
+ // Best effort: return detected token if persisted, else generated.
257
+ return detectOpenClawToken() || generated;
258
+ }
139
259
  function detectTailscaleHostOrIp() {
140
- const dns = getTailscaleHostname();
141
- if (dns)
142
- return normalizeHostOrIp(dns);
260
+ const bin = resolveTailscaleBin();
261
+ if (!bin)
262
+ return '';
263
+ try {
264
+ const out = execFileSync(bin, ['status', '--json'], {
265
+ stdio: 'pipe',
266
+ timeout: 5000,
267
+ encoding: 'utf-8',
268
+ });
269
+ const status = JSON.parse(out);
270
+ const dns = normalizeHostOrIp(String(status?.Self?.DNSName || '').replace(/\.$/, ''));
271
+ if (dns)
272
+ return dns;
273
+ const ips = Array.isArray(status?.Self?.TailscaleIPs) ? status.Self.TailscaleIPs : [];
274
+ const ip = ips.find((x) => typeof x === 'string' && /^100\.\d+\.\d+\.\d+$/.test(x));
275
+ if (ip)
276
+ return String(ip);
277
+ }
278
+ catch { }
143
279
  try {
144
- const out = execSync('tailscale ip -4', {
280
+ const out = execFileSync(bin, ['ip', '-4'], {
145
281
  stdio: 'pipe',
146
282
  timeout: 5000,
147
283
  encoding: 'utf-8',
@@ -166,32 +302,67 @@ function hasHomebrew() {
166
302
  }
167
303
  }
168
304
  function installTailscaleIfMissing() {
169
- if (hasTailscale())
305
+ if (hasUsableTailscale())
170
306
  return true;
171
- if (!hasHomebrew())
172
- return false;
307
+ if (hasHomebrew()) {
308
+ try {
309
+ execSync('brew list --cask tailscale >/dev/null 2>&1 || brew install --cask tailscale', {
310
+ stdio: 'pipe',
311
+ timeout: 8 * 60 * 1000,
312
+ shell: '/bin/zsh',
313
+ });
314
+ }
315
+ catch { }
316
+ if (hasUsableTailscale())
317
+ return true;
318
+ }
319
+ // Homebrew-less fallback: install official app bundle from Tailscale zip.
173
320
  try {
174
- execSync('brew list --cask tailscale >/dev/null 2>&1 || brew install --cask tailscale', {
321
+ const tmpDir = join('/tmp', `life-pulse-ts-${Date.now()}`);
322
+ mkdirSync(tmpDir, { recursive: true });
323
+ const zipPath = join(tmpDir, 'tailscale.zip');
324
+ execSync(`curl -L --fail -o ${JSON.stringify(zipPath)} https://pkgs.tailscale.com/stable/Tailscale-latest-macos.zip`, {
175
325
  stdio: 'pipe',
176
- timeout: 8 * 60 * 1000,
326
+ timeout: 3 * 60 * 1000,
177
327
  shell: '/bin/zsh',
178
328
  });
329
+ execSync(`unzip -oq ${JSON.stringify(zipPath)} -d ${JSON.stringify(tmpDir)}`, {
330
+ stdio: 'pipe',
331
+ timeout: 60_000,
332
+ shell: '/bin/zsh',
333
+ });
334
+ const appSrc = join(tmpDir, 'Tailscale.app');
335
+ if (existsSync(appSrc)) {
336
+ const appDestSystem = '/Applications/Tailscale.app';
337
+ const appDestUser = join(homedir(), 'Applications', 'Tailscale.app');
338
+ try {
339
+ cpSync(appSrc, appDestSystem, { recursive: true, force: true });
340
+ }
341
+ catch {
342
+ mkdirSync(join(homedir(), 'Applications'), { recursive: true });
343
+ cpSync(appSrc, appDestUser, { recursive: true, force: true });
344
+ }
345
+ }
346
+ rmSync(tmpDir, { recursive: true, force: true });
179
347
  }
180
348
  catch { }
181
- return hasTailscale();
349
+ return hasUsableTailscale();
182
350
  }
183
351
  function extractTailscaleAuthUrl(output) {
184
352
  const m = output.match(/https:\/\/login\.tailscale\.com\/[^\s"']+/i);
185
353
  return m ? m[0] : '';
186
354
  }
187
355
  function kickOffTailscaleAuth() {
356
+ const bin = resolveTailscaleBin();
357
+ if (!bin)
358
+ return;
188
359
  try {
189
360
  execSync('open -g -a Tailscale', { stdio: 'pipe', timeout: 5000 });
190
361
  }
191
362
  catch { }
192
363
  let output = '';
193
364
  try {
194
- output = execSync('tailscale up --accept-dns=false', {
365
+ output = execFileSync(bin, ['up', '--accept-dns=false'], {
195
366
  stdio: 'pipe',
196
367
  timeout: 15_000,
197
368
  encoding: 'utf-8',
@@ -390,13 +561,13 @@ async function main() {
390
561
  // --pair: generate Desktop/nox-route.json for NOX routing
391
562
  if (pairMode) {
392
563
  const defaultName = getUserName() || '';
393
- const detectedToken = detectOpenClawToken();
394
564
  const envPhone = normalizePhoneCandidate(process.env.LIFE_PULSE_SELF_PHONE
395
565
  || process.env.NOX_OWNER_PHONE
396
566
  || process.env.LIFE_PULSE_BRIEF_SMS_PHONE
397
567
  || '');
398
568
  const pairSpinner = process.stdin.isTTY ? new Spinner() : undefined;
399
569
  const detectedHost = normalizeHostOrIp(await ensureTailscaleHostForPair(pairSpinner));
570
+ const detectedToken = ensureOpenClawToken();
400
571
  console.log();
401
572
  console.log(chalk.bold.hex('#c0caf5')(' pair with nox'));
402
573
  console.log(chalk.dim(' we will create Desktop/nox-route.json'));
@@ -414,9 +585,10 @@ async function main() {
414
585
  console.log(chalk.dim(' keep tailscale signed in, then rerun: npx life-pulse --pair'));
415
586
  return;
416
587
  }
417
- const token = await promptLine('openclaw token', detectedToken);
588
+ const token = detectedToken;
418
589
  if (!token) {
419
- console.log(chalk.red(' missing openclaw token'));
590
+ console.log(chalk.red(' openclaw token unavailable'));
591
+ console.log(chalk.dim(' make sure openclaw is installed and running, then rerun: npx life-pulse --pair'));
420
592
  return;
421
593
  }
422
594
  const payload = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "life-pulse",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "macOS life diagnostic — reads local data sources, generates actionable insights",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {