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 +133 -196
- package/dist/installer.js +57 -1
- package/dist/ui/theme.js +17 -18
- package/package.json +1 -1
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,
|
|
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
|
|
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
|
|
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
|
-
// --
|
|
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 =
|
|
411
|
+
const host = detectedHost;
|
|
359
412
|
if (!host) {
|
|
360
|
-
console.log(chalk.red('
|
|
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 && !
|
|
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) && !
|
|
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('
|
|
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 —
|
|
11
|
-
accent: chalk.hex('#
|
|
12
|
-
brand: chalk.bold.hex('#
|
|
13
|
-
// Semantic —
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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;
|