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.
- package/dist/cli.js +188 -16
- 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 =
|
|
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
|
|
141
|
-
if (
|
|
142
|
-
return
|
|
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 =
|
|
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 (
|
|
305
|
+
if (hasUsableTailscale())
|
|
170
306
|
return true;
|
|
171
|
-
if (
|
|
172
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
588
|
+
const token = detectedToken;
|
|
418
589
|
if (!token) {
|
|
419
|
-
console.log(chalk.red('
|
|
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 = {
|