life-pulse 2.3.0 → 2.3.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/README.md +1 -1
- package/dist/agent.js +4 -4
- package/dist/cli.js +51 -60
- package/dist/health.js +1 -1
- package/dist/icloud-discovery.js +7 -7
- package/dist/init-check.js +13 -17
- package/dist/installer.js +41 -63
- package/dist/progress.js +24 -24
- package/dist/router.js +1 -1
- package/dist/sms-gateway.js +2 -2
- package/dist/tui.d.ts +6 -0
- package/dist/tui.js +158 -6
- package/dist/tunnel.js +4 -4
- package/dist/ui/app.js +10 -13
- package/dist/ui/progress.js +24 -24
- package/dist/ui/theme.d.ts +2 -2
- package/dist/ui/theme.js +42 -41
- package/package.json +1 -1
package/dist/installer.js
CHANGED
|
@@ -19,7 +19,6 @@ import { join } from 'path';
|
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
20
20
|
import { execSync } from 'child_process';
|
|
21
21
|
import { createInterface } from 'readline';
|
|
22
|
-
import chalk from 'chalk';
|
|
23
22
|
import { runPermissionFlow, hasRequiredPermissions } from './permissions.js';
|
|
24
23
|
import { discoverPlatforms, savePlatforms } from './platforms.js';
|
|
25
24
|
import { runDiscovery, formatDiscoveryReport } from './icloud-discovery.js';
|
|
@@ -29,7 +28,7 @@ import { updateAutoKnowledge } from './knowledge.js';
|
|
|
29
28
|
import { scanForSkills } from './skill-loader.js';
|
|
30
29
|
import { hasTailscale, getHostname as getTailscaleHostname, startFunnel } from './tunnel.js';
|
|
31
30
|
import { startGateway } from './sms-gateway.js';
|
|
32
|
-
import { renderCRMList, renderBrief, Spinner,
|
|
31
|
+
import { renderCRMList, renderBrief, Spinner, DIM, HD, MID } from './tui.js';
|
|
33
32
|
import { saveContactSummaries } from './intelligence.js';
|
|
34
33
|
import { startSession } from './session-progress.js';
|
|
35
34
|
import * as rply from './rply-client.js';
|
|
@@ -62,9 +61,8 @@ function isDone(state, step) {
|
|
|
62
61
|
}
|
|
63
62
|
// ─── TUI Helpers ────────────────────────────────────────────────
|
|
64
63
|
function stepHeader(num, title, done) {
|
|
65
|
-
const icon = done ? GRN('✓') : chalk.bold.hex('#7AA2F7')(`${num}`);
|
|
66
64
|
const label = done ? DIM(title) : HD(title);
|
|
67
|
-
console.log(`\n ${
|
|
65
|
+
console.log(`\n ${label}`);
|
|
68
66
|
}
|
|
69
67
|
async function prompt(question) {
|
|
70
68
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -93,9 +91,6 @@ export async function runInstaller(apiKey) {
|
|
|
93
91
|
const state = loadState();
|
|
94
92
|
const spinner = process.stdin.isTTY ? new Spinner() : undefined;
|
|
95
93
|
console.log();
|
|
96
|
-
console.log(chalk.bold.hex('#7AA2F7')(' life-pulse'));
|
|
97
|
-
console.log(DIM(' personal AI operating system'));
|
|
98
|
-
console.log();
|
|
99
94
|
// ── Step 1: Pre-flight ──
|
|
100
95
|
stepHeader(1, 'Pre-flight', isDone(state, 'preflight'));
|
|
101
96
|
if (!isDone(state, 'preflight')) {
|
|
@@ -121,15 +116,15 @@ export async function runInstaller(apiKey) {
|
|
|
121
116
|
process.env.ANTHROPIC_API_KEY = key;
|
|
122
117
|
apiKey = key;
|
|
123
118
|
state.apiKeySet = true;
|
|
124
|
-
console.log(
|
|
119
|
+
console.log(DIM(' API key saved'));
|
|
125
120
|
}
|
|
126
121
|
else {
|
|
127
|
-
console.log(
|
|
122
|
+
console.log(DIM(' no API key — some features will be limited'));
|
|
128
123
|
}
|
|
129
124
|
}
|
|
130
125
|
else {
|
|
131
126
|
state.apiKeySet = true;
|
|
132
|
-
console.log(
|
|
127
|
+
console.log(DIM(' API key configured'));
|
|
133
128
|
}
|
|
134
129
|
markDone(state, 'preflight');
|
|
135
130
|
}
|
|
@@ -139,13 +134,13 @@ export async function runInstaller(apiKey) {
|
|
|
139
134
|
if (process.stdin.isTTY) {
|
|
140
135
|
const allGranted = await runPermissionFlow();
|
|
141
136
|
if (allGranted) {
|
|
142
|
-
console.log(
|
|
137
|
+
console.log(DIM(' all permissions granted'));
|
|
143
138
|
}
|
|
144
139
|
else if (hasRequiredPermissions()) {
|
|
145
140
|
console.log(DIM(' required permissions granted (some optional skipped)'));
|
|
146
141
|
}
|
|
147
142
|
else {
|
|
148
|
-
console.log(
|
|
143
|
+
console.log(HD(' some required permissions missing — re-run to retry'));
|
|
149
144
|
}
|
|
150
145
|
}
|
|
151
146
|
markDone(state, 'permissions');
|
|
@@ -153,9 +148,9 @@ export async function runInstaller(apiKey) {
|
|
|
153
148
|
// ── Step 3: Discovery ──
|
|
154
149
|
stepHeader(3, 'Discovery', isDone(state, 'discovery'));
|
|
155
150
|
if (!isDone(state, 'discovery')) {
|
|
156
|
-
spinner?.start('
|
|
151
|
+
spinner?.start('learning your world');
|
|
157
152
|
const platformProfile = discoverPlatforms();
|
|
158
|
-
spinner?.update('
|
|
153
|
+
spinner?.update('looking deeper');
|
|
159
154
|
const discoveredApps = runDiscovery();
|
|
160
155
|
spinner?.stop();
|
|
161
156
|
if (discoveredApps.length) {
|
|
@@ -183,7 +178,7 @@ export async function runInstaller(apiKey) {
|
|
|
183
178
|
const inactive = rplyStatus.platforms.filter(p => !p.is_available || !p.is_enabled);
|
|
184
179
|
console.log();
|
|
185
180
|
for (const p of active) {
|
|
186
|
-
console.log(` ${
|
|
181
|
+
console.log(` ${HD(p.display_name)}`);
|
|
187
182
|
}
|
|
188
183
|
for (const p of inactive) {
|
|
189
184
|
const hint = platformHint(p.platform);
|
|
@@ -191,12 +186,12 @@ export async function runInstaller(apiKey) {
|
|
|
191
186
|
}
|
|
192
187
|
if (inactive.length) {
|
|
193
188
|
console.log();
|
|
194
|
-
console.log(DIM(' sign into these
|
|
195
|
-
console.log(DIM('
|
|
189
|
+
console.log(DIM(' sign into these to connect them'));
|
|
190
|
+
console.log(DIM(' run setup again after'));
|
|
196
191
|
}
|
|
197
192
|
}
|
|
198
193
|
else {
|
|
199
|
-
console.log(DIM('
|
|
194
|
+
console.log(DIM(' iMessage only — more platforms available'));
|
|
200
195
|
}
|
|
201
196
|
savePlatforms(platformProfile);
|
|
202
197
|
markDone(state, 'discovery');
|
|
@@ -204,7 +199,7 @@ export async function runInstaller(apiKey) {
|
|
|
204
199
|
// ── Step 4: CRM Build ──
|
|
205
200
|
stepHeader(4, 'Relationship Map', isDone(state, 'crm'));
|
|
206
201
|
if (!isDone(state, 'crm')) {
|
|
207
|
-
spinner?.start('
|
|
202
|
+
spinner?.start('reading the room');
|
|
208
203
|
const crm = await buildCRM();
|
|
209
204
|
spinner?.stop();
|
|
210
205
|
if (crm.threads.length) {
|
|
@@ -214,7 +209,7 @@ export async function runInstaller(apiKey) {
|
|
|
214
209
|
console.log(DIM(` ${knowledgeCount} contact knowledge bases created`));
|
|
215
210
|
// Enrichment
|
|
216
211
|
if (apiKey) {
|
|
217
|
-
spinner?.start('
|
|
212
|
+
spinner?.start('understanding the room');
|
|
218
213
|
let brief = '';
|
|
219
214
|
const enriched = [];
|
|
220
215
|
for await (const t of streamEnrichedCRM(crm, apiKey, {
|
|
@@ -236,7 +231,7 @@ export async function runInstaller(apiKey) {
|
|
|
236
231
|
// ── Step 5: Archetype ──
|
|
237
232
|
stepHeader(5, 'Archetype', isDone(state, 'archetype'));
|
|
238
233
|
if (!isDone(state, 'archetype') && apiKey) {
|
|
239
|
-
spinner?.start('
|
|
234
|
+
spinner?.start('figuring out who you are');
|
|
240
235
|
try {
|
|
241
236
|
const platforms = discoverPlatforms(); // re-read (may have been saved)
|
|
242
237
|
const archetype = await generateArchetype(platforms, apiKey);
|
|
@@ -249,48 +244,40 @@ export async function runInstaller(apiKey) {
|
|
|
249
244
|
}
|
|
250
245
|
catch (err) {
|
|
251
246
|
spinner?.stop();
|
|
252
|
-
console.log(
|
|
247
|
+
console.log(DIM(` archetype generation failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
253
248
|
}
|
|
254
249
|
markDone(state, 'archetype');
|
|
255
250
|
}
|
|
256
|
-
// ── Step 6: Messaging
|
|
257
|
-
stepHeader(6, 'Messaging
|
|
251
|
+
// ── Step 6: Messaging ──
|
|
252
|
+
stepHeader(6, 'Messaging', isDone(state, 'phone'));
|
|
258
253
|
if (!isDone(state, 'phone')) {
|
|
259
|
-
// Start the inbound gateway (rply-mac-server pushes messages here)
|
|
260
254
|
if (apiKey) {
|
|
261
255
|
const gw = startGateway(apiKey);
|
|
262
|
-
console.log(GRN(` gateway listening on :${gw.port}`));
|
|
263
|
-
// Stop after installer finishes (daemon will restart it)
|
|
264
256
|
setTimeout(() => gw.stop(), 60_000);
|
|
265
257
|
}
|
|
266
|
-
// Tailscale — required for rply-mac-server to reach this Mac
|
|
267
258
|
if (hasTailscale()) {
|
|
268
259
|
const hostname = getTailscaleHostname();
|
|
269
260
|
if (hostname) {
|
|
270
261
|
state.funnelUrl = `http://${hostname}:19877`;
|
|
271
|
-
|
|
272
|
-
console.log(DIM(` rply-mac-server → ${state.funnelUrl}/inbound`));
|
|
273
|
-
// Optionally expose via Funnel for HTTPS
|
|
274
|
-
spinner?.start('setting up tunnel');
|
|
262
|
+
spinner?.start('opening the connection');
|
|
275
263
|
const funnelUrl = startFunnel(19877);
|
|
276
264
|
spinner?.stop();
|
|
277
|
-
if (funnelUrl)
|
|
265
|
+
if (funnelUrl)
|
|
278
266
|
state.funnelUrl = funnelUrl;
|
|
279
|
-
|
|
280
|
-
}
|
|
267
|
+
console.log(DIM(' connected'));
|
|
281
268
|
}
|
|
282
269
|
else {
|
|
283
|
-
console.log(
|
|
270
|
+
console.log(HD(' network connection incomplete — run setup again'));
|
|
284
271
|
}
|
|
285
272
|
}
|
|
286
273
|
else {
|
|
287
|
-
console.log(
|
|
288
|
-
console.log(DIM('
|
|
274
|
+
console.log(HD(' missing network layer — messages won\'t reach this machine'));
|
|
275
|
+
console.log(DIM(' tailscale.com/download'));
|
|
289
276
|
}
|
|
290
277
|
markDone(state, 'phone');
|
|
291
278
|
}
|
|
292
279
|
// ── Step 7: Daemon Install ──
|
|
293
|
-
stepHeader(7, '
|
|
280
|
+
stepHeader(7, 'Background', isDone(state, 'daemon'));
|
|
294
281
|
if (!isDone(state, 'daemon')) {
|
|
295
282
|
const logDir = join(homedir(), 'Library/Logs/life-pulse');
|
|
296
283
|
const agentsDir = join(homedir(), 'Library/LaunchAgents');
|
|
@@ -356,35 +343,31 @@ export async function runInstaller(apiKey) {
|
|
|
356
343
|
execSync('launchctl load ~/Library/LaunchAgents/com.life-pulse.daemon.plist', {
|
|
357
344
|
stdio: 'pipe', timeout: 5000,
|
|
358
345
|
});
|
|
359
|
-
console.log(GRN(' daemon installed and started'));
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
console.log(DIM(' plists written — load manually:'));
|
|
363
|
-
console.log(DIM(' launchctl load ~/Library/LaunchAgents/com.life-pulse.daemon.plist'));
|
|
364
346
|
}
|
|
347
|
+
catch { }
|
|
365
348
|
// Load morning
|
|
366
349
|
try {
|
|
367
350
|
execSync('launchctl load ~/Library/LaunchAgents/com.life-pulse.morning.plist', {
|
|
368
351
|
stdio: 'pipe', timeout: 5000,
|
|
369
352
|
});
|
|
370
|
-
console.log(GRN(' morning brief: 7:30 AM daily'));
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
console.log(DIM(' launchctl load ~/Library/LaunchAgents/com.life-pulse.morning.plist'));
|
|
374
353
|
}
|
|
354
|
+
catch { }
|
|
355
|
+
console.log(DIM(' always on'));
|
|
356
|
+
console.log(DIM(' morning brief at 7:30 AM'));
|
|
375
357
|
markDone(state, 'daemon');
|
|
376
358
|
}
|
|
377
359
|
// ── Step 8: First Briefing ──
|
|
378
360
|
stepHeader(8, 'First Briefing', isDone(state, 'briefing'));
|
|
379
361
|
if (!isDone(state, 'briefing') && apiKey && process.stdin.isTTY) {
|
|
380
362
|
console.log();
|
|
381
|
-
console.log(
|
|
363
|
+
console.log();
|
|
364
|
+
console.log(DIM(' let\'s see what\'s going on...'));
|
|
382
365
|
console.log();
|
|
383
366
|
// Start a session for the briefing
|
|
384
367
|
startSession();
|
|
385
368
|
// Import and run the agent directly
|
|
386
369
|
const { runAgent } = await import('./agent.js');
|
|
387
|
-
const { pickCard, renderSectionHeader, renderSection, renderHandled,
|
|
370
|
+
const { pickCard, renderSectionHeader, renderSection, renderHandled, HD: tHD, DIM: tDIM, MID: tMID } = await import('./tui.js');
|
|
388
371
|
const { ProgressRenderer } = await import('./progress.js');
|
|
389
372
|
const renderer = new ProgressRenderer();
|
|
390
373
|
renderer.start();
|
|
@@ -403,31 +386,26 @@ export async function runInstaller(apiKey) {
|
|
|
403
386
|
if (analysis.handled?.length)
|
|
404
387
|
renderHandled(analysis.handled, 5);
|
|
405
388
|
if (analysis.promises?.length) {
|
|
406
|
-
renderSectionHeader('
|
|
389
|
+
renderSectionHeader('promises', tHD);
|
|
407
390
|
}
|
|
408
391
|
if (analysis.alpha?.length) {
|
|
409
|
-
renderSection('
|
|
392
|
+
renderSection('alpha', analysis.alpha, '·', tHD, tMID);
|
|
410
393
|
}
|
|
411
394
|
markDone(state, 'briefing');
|
|
412
395
|
}
|
|
413
396
|
// ── Summary ──
|
|
414
397
|
console.log();
|
|
415
|
-
console.log(
|
|
398
|
+
console.log(HD(' You\'re all set.'));
|
|
416
399
|
console.log();
|
|
417
400
|
// Show active platform count
|
|
418
401
|
const finalStatus = await rply.status();
|
|
419
402
|
if (finalStatus) {
|
|
420
403
|
const active = finalStatus.platforms.filter(p => p.is_available && p.is_enabled);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
if (state.funnelUrl) {
|
|
426
|
-
console.log(HD(` Gateway: ${state.funnelUrl}/inbound`));
|
|
427
|
-
console.log(DIM(' Configure rply-mac-server to POST here'));
|
|
404
|
+
if (active.length) {
|
|
405
|
+
console.log(DIM(` ${active.map(p => p.display_name).join(', ')}`));
|
|
406
|
+
}
|
|
428
407
|
}
|
|
429
|
-
console.log(DIM('
|
|
430
|
-
console.log(DIM('
|
|
431
|
-
console.log(DIM(' Run --check to verify, --status for stats'));
|
|
408
|
+
console.log(DIM(' running in the background'));
|
|
409
|
+
console.log(DIM(' you\'ll hear from me at 7:30 AM'));
|
|
432
410
|
console.log();
|
|
433
411
|
}
|
package/dist/progress.js
CHANGED
|
@@ -11,30 +11,30 @@ const STRUCTURE = chalk.hex('#3B3B3B');
|
|
|
11
11
|
const OK = chalk.hex('#98C379');
|
|
12
12
|
const FAIL = chalk.hex('#E06C75');
|
|
13
13
|
const LABEL = {
|
|
14
|
-
search_all_messages: '
|
|
15
|
-
get_conversation: '
|
|
16
|
-
profile_contact: '
|
|
17
|
-
get_messages: '
|
|
18
|
-
get_unanswered_messages: '
|
|
19
|
-
get_screen_time: '
|
|
20
|
-
get_browsing: '
|
|
21
|
-
get_calls: 'checking
|
|
22
|
-
scan_sources: '
|
|
23
|
-
lookup_contact: '
|
|
24
|
-
get_email_summary: '
|
|
25
|
-
get_git_activity: '
|
|
26
|
-
get_recent_files: '
|
|
27
|
-
get_shell_history: '
|
|
28
|
-
get_notes: 'reading
|
|
29
|
-
get_claude_history: '
|
|
30
|
-
get_chatgpt_history: '
|
|
31
|
-
get_interests_for_plans: '
|
|
32
|
-
get_calendar: '
|
|
33
|
-
get_reminders: 'checking
|
|
34
|
-
get_notifications: '
|
|
35
|
-
discover_platforms: '
|
|
36
|
-
generate_archetype: '
|
|
37
|
-
search_emails: '
|
|
14
|
+
search_all_messages: 'reading the room',
|
|
15
|
+
get_conversation: 'listening in',
|
|
16
|
+
profile_contact: 'learning who this is',
|
|
17
|
+
get_messages: 'reading the room',
|
|
18
|
+
get_unanswered_messages: 'finding what you missed',
|
|
19
|
+
get_screen_time: 'tracking your attention',
|
|
20
|
+
get_browsing: 'retracing your steps',
|
|
21
|
+
get_calls: 'checking who called',
|
|
22
|
+
scan_sources: 'pulling threads',
|
|
23
|
+
lookup_contact: 'learning who this is',
|
|
24
|
+
get_email_summary: 'going through your inbox',
|
|
25
|
+
get_git_activity: 'seeing what you shipped',
|
|
26
|
+
get_recent_files: 'noticing what you touched',
|
|
27
|
+
get_shell_history: 'retracing your steps',
|
|
28
|
+
get_notes: 'reading your thoughts',
|
|
29
|
+
get_claude_history: 'seeing what you asked',
|
|
30
|
+
get_chatgpt_history: 'seeing what you asked',
|
|
31
|
+
get_interests_for_plans: 'understanding your world',
|
|
32
|
+
get_calendar: 'looking at your day',
|
|
33
|
+
get_reminders: 'checking what you owe yourself',
|
|
34
|
+
get_notifications: 'catching up',
|
|
35
|
+
discover_platforms: 'figuring out your world',
|
|
36
|
+
generate_archetype: 'figuring out who you are',
|
|
37
|
+
search_emails: 'digging through mail',
|
|
38
38
|
};
|
|
39
39
|
const MAX_VISIBLE = 4;
|
|
40
40
|
export class ProgressRenderer {
|
package/dist/router.js
CHANGED
|
@@ -146,7 +146,7 @@ const stats = {
|
|
|
146
146
|
lastForwarded: null,
|
|
147
147
|
};
|
|
148
148
|
function log(msg) {
|
|
149
|
-
process.stderr.write(`
|
|
149
|
+
process.stderr.write(` ${msg}\n`);
|
|
150
150
|
}
|
|
151
151
|
// ─── Poll Cycle ───────────────────────────────────────────────
|
|
152
152
|
async function poll() {
|
package/dist/sms-gateway.js
CHANGED
|
@@ -87,7 +87,7 @@ async function handleInbound(req, res, apiKey) {
|
|
|
87
87
|
catch (err) {
|
|
88
88
|
stats.errors++;
|
|
89
89
|
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
-
process.stderr.write(`
|
|
90
|
+
process.stderr.write(` couldn't reply to ${contactName}\n`);
|
|
91
91
|
respond(res, 500, { error: 'conversation failed' });
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -122,7 +122,7 @@ export function startGateway(apiKey) {
|
|
|
122
122
|
respond(res, 404, { error: 'not found' });
|
|
123
123
|
});
|
|
124
124
|
server.listen(GATEWAY_PORT, '0.0.0.0', () => {
|
|
125
|
-
|
|
125
|
+
// silent startup — no infrastructure noise
|
|
126
126
|
});
|
|
127
127
|
return {
|
|
128
128
|
port: GATEWAY_PORT,
|
package/dist/tui.d.ts
CHANGED
|
@@ -14,8 +14,14 @@ export declare const GRN: import("chalk").ChalkInstance;
|
|
|
14
14
|
export declare const MAG: import("chalk").ChalkInstance;
|
|
15
15
|
export declare const CYN: import("chalk").ChalkInstance;
|
|
16
16
|
export declare const HD: import("chalk").ChalkInstance;
|
|
17
|
+
export declare function renderIntro(name?: string): Promise<void>;
|
|
17
18
|
export declare function renderGreeting(name?: string): void;
|
|
18
19
|
export declare function renderContextLine(): void;
|
|
20
|
+
export declare function renderReveal(stats: {
|
|
21
|
+
conversations: number;
|
|
22
|
+
innerCircle: number;
|
|
23
|
+
aboutToBreak: number;
|
|
24
|
+
}): Promise<void>;
|
|
19
25
|
export declare function renderCRMList(contacts: {
|
|
20
26
|
name: string;
|
|
21
27
|
lastMsg: {
|
package/dist/tui.js
CHANGED
|
@@ -26,7 +26,144 @@ function out(text) {
|
|
|
26
26
|
else
|
|
27
27
|
console.log(text);
|
|
28
28
|
}
|
|
29
|
-
// ───
|
|
29
|
+
// ─── Intro ───
|
|
30
|
+
import chalk from 'chalk';
|
|
31
|
+
const _sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
32
|
+
// Warm amber palette — fades like candlelight
|
|
33
|
+
const G1 = chalk.hex('#d4a574'); // name
|
|
34
|
+
const G2 = chalk.hex('#a8885c'); // location + weather
|
|
35
|
+
const G3 = chalk.hex('#7a6544'); // closing
|
|
36
|
+
async function typewrite(text, style, delay = 30) {
|
|
37
|
+
for (const ch of text) {
|
|
38
|
+
process.stdout.write(style(ch));
|
|
39
|
+
await _sleep(delay);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function fetchWeather(city) {
|
|
43
|
+
try {
|
|
44
|
+
const ac = new AbortController();
|
|
45
|
+
const t = setTimeout(() => ac.abort(), 2000);
|
|
46
|
+
const r = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=%t|%C`, { signal: ac.signal, headers: { 'User-Agent': 'life-pulse' } });
|
|
47
|
+
clearTimeout(t);
|
|
48
|
+
const raw = (await r.text()).trim();
|
|
49
|
+
const [temp, cond] = raw.split('|');
|
|
50
|
+
if (!temp || !cond)
|
|
51
|
+
return null;
|
|
52
|
+
const deg = temp.replace(/[+\s]/g, '').replace(/°[CF]/, '°');
|
|
53
|
+
const sky = cond.trim().toLowerCase();
|
|
54
|
+
return `${deg} and ${sky}`;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function timeGreeting(h) {
|
|
61
|
+
if (h < 5)
|
|
62
|
+
return "Don't stay up too late.";
|
|
63
|
+
if (h < 12)
|
|
64
|
+
return 'Hope you have an incredible morning.';
|
|
65
|
+
if (h < 17)
|
|
66
|
+
return "Hope you're having a wonderful afternoon.";
|
|
67
|
+
if (h < 21)
|
|
68
|
+
return "Hope you're having a beautiful evening.";
|
|
69
|
+
return "Don't stay up too late.";
|
|
70
|
+
}
|
|
71
|
+
function weatherRemark(weather) {
|
|
72
|
+
const w = weather.toLowerCase();
|
|
73
|
+
if (w.includes('sunny') || w.includes('clear'))
|
|
74
|
+
return `${weather} out there.`;
|
|
75
|
+
if (w.includes('rain') || w.includes('drizzle'))
|
|
76
|
+
return `${weather} — good day to stay sharp.`;
|
|
77
|
+
if (w.includes('cloud') || w.includes('overcast'))
|
|
78
|
+
return `${weather}.`;
|
|
79
|
+
if (w.includes('snow'))
|
|
80
|
+
return `${weather}. Stay warm.`;
|
|
81
|
+
return `${weather}.`;
|
|
82
|
+
}
|
|
83
|
+
// ─── Geometric animation ───
|
|
84
|
+
function renderScanField(cols, rows) {
|
|
85
|
+
const cy = Math.floor(rows / 2);
|
|
86
|
+
const shades = [
|
|
87
|
+
chalk.hex('#1a1b26'),
|
|
88
|
+
chalk.hex('#1a1b26'),
|
|
89
|
+
chalk.hex('#292e42'),
|
|
90
|
+
chalk.hex('#292e42'),
|
|
91
|
+
chalk.hex('#3b4261'),
|
|
92
|
+
];
|
|
93
|
+
const lines = [];
|
|
94
|
+
for (let y = 0; y < rows; y++) {
|
|
95
|
+
const d = Math.abs(y - cy) / Math.max(cy, 1);
|
|
96
|
+
const brightness = 1 - d;
|
|
97
|
+
const segW = Math.max(0, Math.floor(cols * brightness * 0.85));
|
|
98
|
+
const pad = Math.floor((cols - segW) / 2);
|
|
99
|
+
if (segW < 4) {
|
|
100
|
+
lines.push(' '.repeat(cols));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const si = Math.min(Math.floor(brightness * shades.length), shades.length - 1);
|
|
104
|
+
const shade = shades[si];
|
|
105
|
+
// Thin lines with periodic breaks — creates a lens/field shape
|
|
106
|
+
let seg = '';
|
|
107
|
+
for (let x = 0; x < segW; x++) {
|
|
108
|
+
if (x === 0 || x === segW - 1)
|
|
109
|
+
seg += shade('·');
|
|
110
|
+
else if (x % 12 === 0)
|
|
111
|
+
seg += shade('·');
|
|
112
|
+
else
|
|
113
|
+
seg += shade('─');
|
|
114
|
+
}
|
|
115
|
+
lines.push(' '.repeat(pad) + seg + ' '.repeat(Math.max(0, cols - pad - segW)));
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
export async function renderIntro(name) {
|
|
120
|
+
const firstName = name?.split(' ')[0] || name;
|
|
121
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
122
|
+
const city = tz.split('/').pop()?.replace(/_/g, ' ') || '';
|
|
123
|
+
// Fire weather fetch while animation runs
|
|
124
|
+
const weatherP = city ? fetchWeather(city) : Promise.resolve(null);
|
|
125
|
+
const cols = process.stdout.columns || 80;
|
|
126
|
+
const rows = process.stdout.rows || 24;
|
|
127
|
+
// Push old content out of scrollback
|
|
128
|
+
process.stdout.write('\n'.repeat(rows * 2));
|
|
129
|
+
process.stdout.write('\x1Bc');
|
|
130
|
+
process.stdout.write('\x1B[?25l');
|
|
131
|
+
// === Phase 1: Geometric scan field ===
|
|
132
|
+
const field = renderScanField(cols, rows);
|
|
133
|
+
for (let y = 0; y < field.length; y++) {
|
|
134
|
+
process.stdout.write(`\x1B[${y + 1};1H${field[y]}`);
|
|
135
|
+
await _sleep(12);
|
|
136
|
+
}
|
|
137
|
+
await _sleep(350);
|
|
138
|
+
// === Phase 2: Greeting ===
|
|
139
|
+
process.stdout.write('\x1Bc');
|
|
140
|
+
process.stdout.write('\n');
|
|
141
|
+
// "Hey Molly."
|
|
142
|
+
const hey = firstName ? `Hey ${firstName}.` : 'Hey.';
|
|
143
|
+
process.stdout.write(' ');
|
|
144
|
+
await typewrite(hey, G1, 35);
|
|
145
|
+
process.stdout.write('\n');
|
|
146
|
+
await _sleep(250);
|
|
147
|
+
// "I see you're in Los Angeles. 72° and sunny out there."
|
|
148
|
+
const weather = await weatherP;
|
|
149
|
+
const locLine = weather
|
|
150
|
+
? `I see you're in ${city}. ${weatherRemark(weather)}`
|
|
151
|
+
: city ? `I see you're in ${city}.` : '';
|
|
152
|
+
if (locLine) {
|
|
153
|
+
process.stdout.write(' ');
|
|
154
|
+
await typewrite(locLine, G2, 20);
|
|
155
|
+
process.stdout.write('\n');
|
|
156
|
+
await _sleep(200);
|
|
157
|
+
}
|
|
158
|
+
// "Hope you're having an incredible morning."
|
|
159
|
+
const h = new Date().getHours();
|
|
160
|
+
process.stdout.write(' ');
|
|
161
|
+
await typewrite(timeGreeting(h), G3, 18);
|
|
162
|
+
process.stdout.write('\n\n');
|
|
163
|
+
if (USE_INK)
|
|
164
|
+
ink.initInk();
|
|
165
|
+
}
|
|
166
|
+
// ─── Greeting (legacy / non-interactive fallback) ───
|
|
30
167
|
export function renderGreeting(name) {
|
|
31
168
|
if (USE_INK) {
|
|
32
169
|
ink.initInk();
|
|
@@ -49,6 +186,21 @@ export function renderContextLine() {
|
|
|
49
186
|
out(` ${C.dim(`${days[d.getDay()]} · ${h12}:${min} ${ampm}`)}`);
|
|
50
187
|
out('');
|
|
51
188
|
}
|
|
189
|
+
// ─── Reveal (the moment after discovery) ───
|
|
190
|
+
export async function renderReveal(stats) {
|
|
191
|
+
await _sleep(600);
|
|
192
|
+
process.stdout.write(' ');
|
|
193
|
+
await typewrite(`${stats.conversations} conversations.`, G2, 22);
|
|
194
|
+
await _sleep(400);
|
|
195
|
+
process.stdout.write(' ');
|
|
196
|
+
await typewrite(`${stats.innerCircle} people who matter.`, G1, 25);
|
|
197
|
+
await _sleep(500);
|
|
198
|
+
if (stats.aboutToBreak > 0) {
|
|
199
|
+
process.stdout.write(' ');
|
|
200
|
+
await typewrite(`${stats.aboutToBreak} thread${stats.aboutToBreak === 1 ? '' : 's'} about to break.`, chalk.bold.hex('#c0caf5'), 28);
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write('\n\n');
|
|
203
|
+
}
|
|
52
204
|
// ─── CRM List ───
|
|
53
205
|
export function renderCRMList(contacts) {
|
|
54
206
|
for (const t of contacts.slice(0, 12)) {
|
|
@@ -86,11 +238,11 @@ export function renderSection(title, items, bullet, colorBold, colorNorm) {
|
|
|
86
238
|
export function renderHandled(items, max = 5) {
|
|
87
239
|
if (!items.length)
|
|
88
240
|
return;
|
|
89
|
-
out(` ${C.
|
|
241
|
+
out(` ${C.faint('handled')}`);
|
|
90
242
|
for (const item of items.slice(0, max))
|
|
91
|
-
out(` ${C.
|
|
243
|
+
out(` ${C.faint('·')} ${C.dim(item)}`);
|
|
92
244
|
if (items.length > max)
|
|
93
|
-
out(` ${C.
|
|
245
|
+
out(` ${C.faint(`+${items.length - max} more`)}`);
|
|
94
246
|
out('');
|
|
95
247
|
}
|
|
96
248
|
// ─── Brief ───
|
|
@@ -151,7 +303,7 @@ export async function renderCRMStream(source, spinner) {
|
|
|
151
303
|
const gap = Math.max(2, w - 4 - name.length - ago.length);
|
|
152
304
|
out(` ${C.hd(name)}${' '.repeat(gap)}${C.dim(ago)}`);
|
|
153
305
|
if (t.relationship) {
|
|
154
|
-
out(` ${C.
|
|
306
|
+
out(` ${C.mid(t.relationship.slice(0, w - 8))}`);
|
|
155
307
|
}
|
|
156
308
|
rendered.push(t);
|
|
157
309
|
}
|
|
@@ -167,7 +319,7 @@ export async function renderCRMContact(t) {
|
|
|
167
319
|
const gap = Math.max(2, w - 4 - name.length - ago.length);
|
|
168
320
|
out(` ${C.hd(name)}${' '.repeat(gap)}${C.dim(ago)}`);
|
|
169
321
|
if (t.relationship)
|
|
170
|
-
out(` ${C.
|
|
322
|
+
out(` ${C.mid(t.relationship.slice(0, w - 8))}`);
|
|
171
323
|
}
|
|
172
324
|
// ─── Destroy (call at exit) ───
|
|
173
325
|
export function destroyUI() {
|
package/dist/tunnel.js
CHANGED
|
@@ -49,12 +49,12 @@ export function isFunnelActive() {
|
|
|
49
49
|
export function startFunnel(port = DEFAULT_PORT) {
|
|
50
50
|
state.port = port;
|
|
51
51
|
if (!hasTailscale()) {
|
|
52
|
-
|
|
52
|
+
// silent — caller handles missing network
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
const hostname = getHostname();
|
|
56
56
|
if (!hostname) {
|
|
57
|
-
|
|
57
|
+
// silent — caller handles missing hostname
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
// Check if already active
|
|
@@ -78,7 +78,7 @@ export function startFunnel(port = DEFAULT_PORT) {
|
|
|
78
78
|
child.on('exit', (code) => {
|
|
79
79
|
state.healthy = false;
|
|
80
80
|
if (code !== 0) {
|
|
81
|
-
process.stderr.write(`
|
|
81
|
+
process.stderr.write(` connection dropped\n`);
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
84
|
// Give it a moment to establish
|
|
@@ -86,7 +86,7 @@ export function startFunnel(port = DEFAULT_PORT) {
|
|
|
86
86
|
return state.url;
|
|
87
87
|
}
|
|
88
88
|
catch (err) {
|
|
89
|
-
process.stderr.write(`
|
|
89
|
+
process.stderr.write(` connection failed\n`);
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
92
|
}
|