life-pulse 2.3.10 → 2.4.0
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 +19 -0
- package/dist/agent.js +14 -2
- package/dist/analyze.d.ts +1 -19
- package/dist/analyze.js +2 -128
- package/dist/cli.js +314 -323
- package/dist/collectors/calendar.js +1 -4
- package/dist/contacts.d.ts +2 -0
- package/dist/contacts.js +54 -0
- package/dist/conversation.js +3 -0
- package/dist/crm.d.ts +1 -0
- package/dist/crm.js +176 -86
- package/dist/ghostty-frames.json +1 -0
- package/dist/installer.d.ts +2 -2
- package/dist/installer.js +112 -25
- package/dist/profile.d.ts +6 -0
- package/dist/profile.js +230 -1
- package/dist/progress.d.ts +1 -0
- package/dist/progress.js +67 -31
- package/dist/prompt-layers.d.ts +17 -0
- package/dist/prompt-layers.js +113 -0
- package/dist/router.d.ts +3 -2
- package/dist/router.js +3 -2
- package/dist/session-progress.d.ts +2 -2
- package/dist/session-progress.js +2 -11
- package/dist/skill-loader.d.ts +1 -1
- package/dist/skill-loader.js +1 -1
- package/dist/sms-gateway.d.ts +6 -11
- package/dist/sms-gateway.js +14 -11
- package/dist/state.d.ts +1 -1
- package/dist/state.js +13 -17
- package/dist/tools.js +1 -3
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/tui.d.ts +4 -3
- package/dist/tui.js +126 -66
- package/dist/tunnel.d.ts +1 -2
- package/dist/tunnel.js +1 -2
- package/dist/ui/app.d.ts +2 -1
- package/dist/ui/app.js +42 -25
- package/dist/ui/progress.d.ts +1 -0
- package/dist/ui/progress.js +75 -35
- package/dist/ui/theme.js +16 -17
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { collectAll } from './index.js';
|
|
3
2
|
import { runAgent } from './agent.js';
|
|
4
|
-
import { analyzeWithLLM } from './analyze.js';
|
|
5
3
|
import { saveState, saveDecisions } from './state.js';
|
|
6
4
|
// ProgressRenderer unused — removed
|
|
7
5
|
import { InkProgress } from './ui/progress.js';
|
|
8
6
|
import { addTodo, resolveTodos, pruneOld } from './todo.js';
|
|
9
|
-
import { renderIntro, revealContacts, revealInsights, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, RED, AMB, MID, DIM } from './tui.js';
|
|
7
|
+
import { renderIntro, revealContacts, revealInsights, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, HD, RED, AMB, MID, DIM } from './tui.js';
|
|
10
8
|
import { needsDiscovery, discoverPlatforms, savePlatforms } from './platforms.js';
|
|
11
9
|
import { generateArchetype } from './archetype.js';
|
|
12
|
-
import { runPermissionFlow, getMissingPermissions } from './permissions.js';
|
|
10
|
+
import { runPermissionFlow, hasRequiredPermissions, getMissingPermissions } from './permissions.js';
|
|
13
11
|
import { buildCRM, streamEnrichedCRM, generateInsights } from './crm.js';
|
|
14
12
|
import { saveContactSummaries } from './intelligence.js';
|
|
15
|
-
import { getUserName } from './profile.js';
|
|
13
|
+
import { buildPersonalSummary, getUserName } from './profile.js';
|
|
16
14
|
// Session progress tracking (Anthropic long-running agent pattern)
|
|
17
15
|
import { startSession, endSession, loadProgress, recordDecision, recordSurfaced, getTimeSinceLastSession, getNewDiscoveries } from './session-progress.js';
|
|
18
16
|
import { runDiscovery, formatDiscoveryReport } from './icloud-discovery.js';
|
|
19
17
|
import { getSourcesNeedingScan, getChangeSummary } from './incremental.js';
|
|
20
|
-
import { startHealthServer, stopHealthServer, isDaemonRunning,
|
|
21
|
-
// New: long-running agent harness modules
|
|
22
|
-
import { runInitCheck } from './init-check.js';
|
|
18
|
+
import { startHealthServer, stopHealthServer, isDaemonRunning, touchActivity } from './health.js';
|
|
23
19
|
import { installCrashHandlers, recordSuccess, recordCrash, checkpoint, getCrashStats } from './watchdog.js';
|
|
24
|
-
import { startMessageLoop } from './message-loop.js';
|
|
20
|
+
import { startMessageLoop, sendMessage } from './message-loop.js';
|
|
25
21
|
import { scanForSkills } from './skill-loader.js';
|
|
26
22
|
import { TransportManager, IMessageTransport, TelegramTransport } from './transport.js';
|
|
27
23
|
import { runInstaller } from './installer.js';
|
|
28
24
|
import { converse } from './conversation.js';
|
|
29
|
-
import { startGateway
|
|
25
|
+
import { startGateway } from './sms-gateway.js';
|
|
30
26
|
import { updateAutoKnowledge } from './knowledge.js';
|
|
31
|
-
import {
|
|
27
|
+
import { findHandlesForName } from './contacts.js';
|
|
28
|
+
import { ensurePromptLayerFiles } from './prompt-layers.js';
|
|
29
|
+
import { hasTailscale, startFunnel, getHostname as getTailscaleHostname } from './tunnel.js';
|
|
32
30
|
import chalk from 'chalk';
|
|
33
31
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
34
32
|
import { join, dirname } from 'path';
|
|
35
33
|
import { fileURLToPath } from 'url';
|
|
36
34
|
import { homedir } from 'os';
|
|
37
35
|
import { execSync, execFileSync } from 'child_process';
|
|
36
|
+
import { createInterface } from 'readline';
|
|
38
37
|
import dayjs from 'dayjs';
|
|
39
38
|
const collectedDecisions = [];
|
|
40
39
|
const DEFAULT_CONFIG = {
|
|
@@ -42,6 +41,7 @@ const DEFAULT_CONFIG = {
|
|
|
42
41
|
monitorIntervalSec: 30,
|
|
43
42
|
notifyTiers: ['T1', 'T2'],
|
|
44
43
|
quietHours: { start: '22:00', end: '07:00' },
|
|
44
|
+
briefSmsEnabled: true,
|
|
45
45
|
};
|
|
46
46
|
function loadConfig() {
|
|
47
47
|
const p = join(homedir(), 'Library/Application Support/life-pulse/config.json');
|
|
@@ -64,12 +64,9 @@ function loadEnvFile(path) {
|
|
|
64
64
|
// Project-local .env first (dev), then ~/.config/life-pulse/.env (npm users)
|
|
65
65
|
loadEnvFile(join(dirname(fileURLToPath(import.meta.url)), '..', '.env'));
|
|
66
66
|
loadEnvFile(join(homedir(), '.config', 'life-pulse', '.env'));
|
|
67
|
-
const API_KEY = process.env.OPENROUTER_API_KEY || process.env.LLM_API_KEY || '';
|
|
68
67
|
const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
console.log(` ${bullet} ${item}`);
|
|
72
|
-
}
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
73
70
|
}
|
|
74
71
|
async function fetchCalendarContext() {
|
|
75
72
|
try {
|
|
@@ -82,120 +79,236 @@ async function fetchCalendarContext() {
|
|
|
82
79
|
catch { }
|
|
83
80
|
return '';
|
|
84
81
|
}
|
|
82
|
+
function normalizePhoneCandidate(raw) {
|
|
83
|
+
const t = raw.trim();
|
|
84
|
+
if (!t)
|
|
85
|
+
return '';
|
|
86
|
+
if (t.startsWith('+'))
|
|
87
|
+
return '+' + t.slice(1).replace(/\D/g, '');
|
|
88
|
+
return t.replace(/\D/g, '');
|
|
89
|
+
}
|
|
90
|
+
async function promptLine(question, fallback = '') {
|
|
91
|
+
if (!process.stdin.isTTY)
|
|
92
|
+
return fallback;
|
|
93
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
94
|
+
const suffix = fallback ? ` [${fallback}]` : '';
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
rl.question(` ${question}${suffix}: `, answer => {
|
|
97
|
+
rl.close();
|
|
98
|
+
const value = answer.trim();
|
|
99
|
+
resolve(value || fallback);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function detectOpenClawToken() {
|
|
104
|
+
const keys = [
|
|
105
|
+
'gateway.http.auth.token',
|
|
106
|
+
'gateway.auth.token',
|
|
107
|
+
'gateway.http.token',
|
|
108
|
+
];
|
|
109
|
+
for (const key of keys) {
|
|
110
|
+
try {
|
|
111
|
+
const out = execSync(`openclaw config get ${key}`, { stdio: 'pipe', timeout: 5000, encoding: 'utf-8' }).trim();
|
|
112
|
+
if (!out)
|
|
113
|
+
continue;
|
|
114
|
+
const low = out.toLowerCase();
|
|
115
|
+
if (low.includes('not set') || low.includes('unknown') || low.includes('error'))
|
|
116
|
+
continue;
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
120
|
+
}
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
function writeNoxRouteJson(payload) {
|
|
124
|
+
const desktop = join(homedir(), 'Desktop');
|
|
125
|
+
mkdirSync(desktop, { recursive: true });
|
|
126
|
+
const path = join(desktop, 'nox-route.json');
|
|
127
|
+
writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
128
|
+
try {
|
|
129
|
+
execSync(`cat ${JSON.stringify(path)} | pbcopy`, { stdio: 'pipe', timeout: 5000 });
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
try {
|
|
133
|
+
execSync(`open -R ${JSON.stringify(path)}`, { stdio: 'pipe', timeout: 5000 });
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
return path;
|
|
137
|
+
}
|
|
138
|
+
function loadPhoneFromClientsRegistry() {
|
|
139
|
+
const p = join(homedir(), '.life-pulse', 'clients.json');
|
|
140
|
+
if (!existsSync(p))
|
|
141
|
+
return null;
|
|
142
|
+
try {
|
|
143
|
+
const json = JSON.parse(readFileSync(p, 'utf-8'));
|
|
144
|
+
const enabled = (json.clients || []).filter(c => c.enabled !== false && c.phone);
|
|
145
|
+
if (!enabled.length)
|
|
146
|
+
return null;
|
|
147
|
+
const first = normalizePhoneCandidate(enabled[0].phone || '');
|
|
148
|
+
return first || null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function resolveBriefSmsTarget(config) {
|
|
155
|
+
const explicit = [
|
|
156
|
+
process.env.LIFE_PULSE_BRIEF_SMS_PHONE,
|
|
157
|
+
process.env.LIFE_PULSE_SELF_PHONE,
|
|
158
|
+
process.env.NOX_OWNER_PHONE,
|
|
159
|
+
config.briefSmsPhone,
|
|
160
|
+
].map(v => normalizePhoneCandidate(v || '')).find(Boolean);
|
|
161
|
+
if (explicit)
|
|
162
|
+
return explicit;
|
|
163
|
+
const fromName = findHandlesForName(getUserName())
|
|
164
|
+
.map(h => normalizePhoneCandidate(h))
|
|
165
|
+
.find(h => /^\+?\d{10,15}$/.test(h));
|
|
166
|
+
if (fromName)
|
|
167
|
+
return fromName;
|
|
168
|
+
return loadPhoneFromClientsRegistry();
|
|
169
|
+
}
|
|
170
|
+
function buildBriefingText(name, analysis) {
|
|
171
|
+
const first = name.split(' ')[0] || name;
|
|
172
|
+
const lines = [`hey ${first}, quick life-pulse brief:`];
|
|
173
|
+
const addItems = (label, items, max) => {
|
|
174
|
+
if (!items?.length)
|
|
175
|
+
return;
|
|
176
|
+
let added = 0;
|
|
177
|
+
for (const item of items) {
|
|
178
|
+
if (added >= max)
|
|
179
|
+
break;
|
|
180
|
+
const title = String(item?.title || '').trim();
|
|
181
|
+
if (!title)
|
|
182
|
+
continue;
|
|
183
|
+
const rec = item?.options?.[0]?.label ? ` -> ${item.options[0].label}` : '';
|
|
184
|
+
lines.push(`${label} ${title}${rec}`);
|
|
185
|
+
added++;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
addItems('PROMISE:', analysis.promises, 2);
|
|
189
|
+
addItems('BLOCKER:', analysis.blockers, 2);
|
|
190
|
+
addItems('BUMP:', analysis.bumps, 1);
|
|
191
|
+
if (analysis.alpha?.length) {
|
|
192
|
+
const alpha = String(analysis.alpha[0] || '').trim();
|
|
193
|
+
if (alpha)
|
|
194
|
+
lines.push(`ALPHA: ${alpha}`);
|
|
195
|
+
}
|
|
196
|
+
if (lines.length === 1)
|
|
197
|
+
lines.push('all clear. nothing urgent right now.');
|
|
198
|
+
return lines.join('\n').slice(0, 1400);
|
|
199
|
+
}
|
|
200
|
+
async function sendViaImwebserver(phone, message) {
|
|
201
|
+
try {
|
|
202
|
+
const resp = await fetch('http://127.0.0.1:8888/sendMessage', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({ phone, message }),
|
|
206
|
+
signal: AbortSignal.timeout(5_000),
|
|
207
|
+
});
|
|
208
|
+
return resp.ok;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function sendBriefingText(phone, message) {
|
|
215
|
+
if (await sendViaImwebserver(phone, message))
|
|
216
|
+
return true;
|
|
217
|
+
return sendMessage(phone, message, false);
|
|
218
|
+
}
|
|
85
219
|
async function showCRM(apiKey, opts) {
|
|
86
|
-
const crm = await buildCRM();
|
|
220
|
+
const crm = opts?.crmPromise ? await opts.crmPromise : await buildCRM();
|
|
87
221
|
if (!crm.threads.length || !apiKey) {
|
|
88
222
|
opts?.spinner?.stop();
|
|
89
223
|
return false;
|
|
90
224
|
}
|
|
91
225
|
opts?.spinner?.stop();
|
|
92
226
|
console.log();
|
|
227
|
+
console.log(` ${HD('your inner circle')}`);
|
|
228
|
+
console.log();
|
|
93
229
|
// Fire insights in background — they'll resolve while user steps through contacts
|
|
94
230
|
const insightsGen = generateInsights(crm);
|
|
95
231
|
const calendarContext = opts?.calendarContext ?? '';
|
|
96
232
|
const hour = new Date().getHours();
|
|
97
233
|
const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
|
|
98
234
|
let brief = '';
|
|
235
|
+
let resolveBrief = null;
|
|
236
|
+
const briefReady = new Promise(resolve => { resolveBrief = resolve; });
|
|
99
237
|
const enriched = await revealContacts(streamEnrichedCRM(crm, apiKey, {
|
|
100
238
|
calendarContext,
|
|
101
239
|
timeOfDay,
|
|
102
|
-
onBrief: (b) => {
|
|
240
|
+
onBrief: (b) => {
|
|
241
|
+
brief = b;
|
|
242
|
+
resolveBrief?.();
|
|
243
|
+
},
|
|
244
|
+
briefAsync: true,
|
|
103
245
|
}));
|
|
246
|
+
// Optimistic brief: wait a beat, then move on.
|
|
247
|
+
await Promise.race([briefReady, sleep(300)]);
|
|
104
248
|
if (brief) {
|
|
105
249
|
renderDivider();
|
|
106
250
|
await renderBrief(brief);
|
|
107
251
|
}
|
|
108
|
-
//
|
|
109
|
-
await revealInsights(insightsGen);
|
|
252
|
+
// Optimistic insights: show if they land quickly, don't block the flow.
|
|
253
|
+
await revealInsights(insightsGen, { maxWaitMs: 900 });
|
|
110
254
|
renderDivider();
|
|
111
255
|
saveContactSummaries(enriched);
|
|
112
256
|
return true;
|
|
113
257
|
}
|
|
114
258
|
async function main() {
|
|
259
|
+
const createdPromptFiles = ensurePromptLayerFiles();
|
|
115
260
|
const jsonMode = process.argv.includes('--json');
|
|
116
|
-
const rawMode = process.argv.includes('--raw');
|
|
117
|
-
const legacyMode = process.argv.includes('--legacy');
|
|
118
261
|
const statusMode = process.argv.includes('--status');
|
|
119
262
|
const daemonMode = process.argv.includes('--daemon');
|
|
120
|
-
const
|
|
121
|
-
const initCheckMode = process.argv.includes('--check');
|
|
122
|
-
const phoneSetupMode = process.argv.includes('--phone-setup');
|
|
123
|
-
const testSmsMode = process.argv.includes('--test-sms');
|
|
124
|
-
const routerMode = process.argv.includes('--router');
|
|
125
|
-
const initClientsMode = process.argv.includes('--init-clients');
|
|
126
|
-
const key = process.argv.find(a => a.startsWith('--key='))?.split('=')[1] || API_KEY;
|
|
263
|
+
const pairMode = process.argv.includes('--pair');
|
|
127
264
|
// Install crash handlers early (Anthropic pattern: recover from failures)
|
|
128
265
|
installCrashHandlers((err) => {
|
|
129
266
|
console.error(chalk.red(` crash: ${err.message}`));
|
|
130
267
|
});
|
|
131
|
-
// --
|
|
132
|
-
if (
|
|
133
|
-
const
|
|
268
|
+
// --pair: generate Desktop/nox-route.json for NOX routing
|
|
269
|
+
if (pairMode) {
|
|
270
|
+
const defaultName = getUserName() || '';
|
|
271
|
+
const detectedHost = getTailscaleHostname() || '';
|
|
272
|
+
const detectedToken = detectOpenClawToken();
|
|
273
|
+
const envPhone = normalizePhoneCandidate(process.env.LIFE_PULSE_SELF_PHONE
|
|
274
|
+
|| process.env.NOX_OWNER_PHONE
|
|
275
|
+
|| process.env.LIFE_PULSE_BRIEF_SMS_PHONE
|
|
276
|
+
|| '');
|
|
134
277
|
console.log();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
console.log(` ${icon} ${c.check}`);
|
|
138
|
-
}
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(chalk.dim(` ${status.recommendation}`));
|
|
141
|
-
console.log();
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
// --router: run message router on YOUR Mac (routes to client Mac Minis)
|
|
145
|
-
if (routerMode) {
|
|
146
|
-
console.log(chalk.bold.hex('#c0caf5')(' life-pulse'));
|
|
147
|
-
console.log(chalk.dim(' routing messages'));
|
|
278
|
+
console.log(chalk.bold.hex('#c0caf5')(' pair with nox'));
|
|
279
|
+
console.log(chalk.dim(' we will create Desktop/nox-route.json'));
|
|
148
280
|
console.log();
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
// --init-clients: create template clients.json
|
|
156
|
-
if (initClientsMode) {
|
|
157
|
-
initClientRegistry();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
// --phone-setup: run phone number setup from installer
|
|
161
|
-
if (phoneSetupMode) {
|
|
162
|
-
await runInstaller(ANTHROPIC_KEY);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
// --test-sms: test the gateway by posting a fake inbound message
|
|
166
|
-
if (testSmsMode) {
|
|
167
|
-
const up = await isGatewayUp();
|
|
168
|
-
if (!up) {
|
|
169
|
-
console.log(chalk.red(' not running — start life-pulse first'));
|
|
281
|
+
const name = await promptLine('name', defaultName);
|
|
282
|
+
const phoneRaw = await promptLine('iphone number (+1...)', envPhone);
|
|
283
|
+
const phone = normalizePhoneCandidate(phoneRaw);
|
|
284
|
+
if (!phone) {
|
|
285
|
+
console.log(chalk.red(' missing phone number'));
|
|
170
286
|
return;
|
|
171
287
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
headers: { 'Content-Type': 'application/json' },
|
|
177
|
-
body: JSON.stringify({ phone, message: 'test message from life-pulse' }),
|
|
178
|
-
});
|
|
179
|
-
const data = await resp.json();
|
|
180
|
-
if (data.response)
|
|
181
|
-
console.log(chalk.green(` replied: ${data.response}`));
|
|
182
|
-
else
|
|
183
|
-
console.log(chalk.red(` failed: ${data.error}`));
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
console.log(chalk.red(` couldn't connect — is life-pulse running?`));
|
|
187
|
-
}
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
// --health: check if daemon is running and get status
|
|
191
|
-
if (healthMode) {
|
|
192
|
-
if (isDaemonRunning()) {
|
|
193
|
-
const status = getHealthStatus();
|
|
194
|
-
console.log(JSON.stringify(status, null, 2));
|
|
288
|
+
const host = await promptLine('tailscale host/ip', detectedHost);
|
|
289
|
+
if (!host) {
|
|
290
|
+
console.log(chalk.red(' missing tailscale host/ip'));
|
|
291
|
+
return;
|
|
195
292
|
}
|
|
196
|
-
|
|
197
|
-
|
|
293
|
+
const token = await promptLine('openclaw token', detectedToken);
|
|
294
|
+
if (!token) {
|
|
295
|
+
console.log(chalk.red(' missing openclaw token'));
|
|
296
|
+
return;
|
|
198
297
|
}
|
|
298
|
+
const payload = {
|
|
299
|
+
name: name || 'User',
|
|
300
|
+
phone,
|
|
301
|
+
openclaw_url: `http://${host}:18789`,
|
|
302
|
+
openclaw_token: token,
|
|
303
|
+
openclaw_agent_id: 'main',
|
|
304
|
+
openclaw_model: 'openclaw:main',
|
|
305
|
+
enabled: true,
|
|
306
|
+
};
|
|
307
|
+
const out = writeNoxRouteJson(payload);
|
|
308
|
+
console.log();
|
|
309
|
+
console.log(chalk.green(` saved ${out}`));
|
|
310
|
+
console.log(chalk.dim(' send this file to your operator'));
|
|
311
|
+
console.log();
|
|
199
312
|
return;
|
|
200
313
|
}
|
|
201
314
|
// --daemon: check if already running
|
|
@@ -232,11 +345,6 @@ async function main() {
|
|
|
232
345
|
console.log();
|
|
233
346
|
return;
|
|
234
347
|
}
|
|
235
|
-
if (rawMode) {
|
|
236
|
-
const collected = await collectAll();
|
|
237
|
-
console.log(JSON.stringify(collected, null, 2));
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
348
|
// ── Start session tracking ──
|
|
241
349
|
const sessionProgress = startSession();
|
|
242
350
|
const timeSinceLastSession = getTimeSinceLastSession();
|
|
@@ -250,7 +358,8 @@ async function main() {
|
|
|
250
358
|
activeContacts,
|
|
251
359
|
pendingFollowUps,
|
|
252
360
|
});
|
|
253
|
-
console.log(chalk.
|
|
361
|
+
console.log(chalk.green('\n demo wrapped cleanly'));
|
|
362
|
+
console.log(chalk.dim(' progress saved'));
|
|
254
363
|
process.exit(0);
|
|
255
364
|
};
|
|
256
365
|
// Auto-detect installer mode: first run with no state → white-glove installer
|
|
@@ -258,102 +367,20 @@ async function main() {
|
|
|
258
367
|
const installStateExists = existsSync(join(stateDir, 'install-state.json'));
|
|
259
368
|
const noSessionHistory = sessionProgress.totalSessions <= 1;
|
|
260
369
|
const isSetupFlag = process.argv.includes('--setup');
|
|
261
|
-
if ((noSessionHistory && !installStateExists && !
|
|
370
|
+
if ((noSessionHistory && !installStateExists && !jsonMode && !daemonMode)
|
|
262
371
|
|| isSetupFlag) {
|
|
263
372
|
// First run: launch full installer
|
|
264
373
|
await runInstaller(ANTHROPIC_KEY);
|
|
265
374
|
return;
|
|
266
375
|
}
|
|
267
|
-
if (
|
|
268
|
-
const config = loadConfig();
|
|
269
|
-
const [hour, min] = config.briefTime.split(':').map(Number);
|
|
270
|
-
const logDir = join(homedir(), 'Library/Logs/life-pulse');
|
|
271
|
-
const agentsDir = join(homedir(), 'Library/LaunchAgents');
|
|
272
|
-
const envDir = join(homedir(), '.config/life-pulse');
|
|
273
|
-
try {
|
|
274
|
-
mkdirSync(logDir, { recursive: true });
|
|
275
|
-
}
|
|
276
|
-
catch { }
|
|
277
|
-
try {
|
|
278
|
-
mkdirSync(agentsDir, { recursive: true });
|
|
279
|
-
}
|
|
280
|
-
catch { }
|
|
281
|
-
try {
|
|
282
|
-
mkdirSync(envDir, { recursive: true });
|
|
283
|
-
}
|
|
284
|
-
catch { }
|
|
285
|
-
// Detect npx path for the installed package
|
|
286
|
-
let npxPath;
|
|
287
|
-
try {
|
|
288
|
-
npxPath = execSync('which npx', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
npxPath = '/opt/homebrew/bin/npx';
|
|
292
|
-
}
|
|
293
|
-
// Env file hint
|
|
294
|
-
const envPath = join(envDir, '.env');
|
|
295
|
-
if (!existsSync(envPath)) {
|
|
296
|
-
writeFileSync(envPath, '# life-pulse environment\n# ANTHROPIC_API_KEY=sk-ant-...\n# TELEGRAM_BOT_TOKEN=...\n');
|
|
297
|
-
console.log(chalk.dim(` Created ${envPath} — add your API key`));
|
|
298
|
-
}
|
|
299
|
-
// 1. Morning brief plist (scheduled, one-shot)
|
|
300
|
-
const morningPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
301
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
302
|
-
<plist version="1.0">
|
|
303
|
-
<dict>
|
|
304
|
-
<key>Label</key><string>com.life-pulse.morning</string>
|
|
305
|
-
<key>ProgramArguments</key>
|
|
306
|
-
<array>
|
|
307
|
-
<string>${npxPath}</string><string>life-pulse</string>
|
|
308
|
-
</array>
|
|
309
|
-
<key>StartCalendarInterval</key>
|
|
310
|
-
<dict><key>Hour</key><integer>${hour}</integer><key>Minute</key><integer>${min}</integer></dict>
|
|
311
|
-
<key>StandardOutPath</key><string>${join(logDir, 'morning.log')}</string>
|
|
312
|
-
<key>StandardErrorPath</key><string>${join(logDir, 'morning-err.log')}</string>
|
|
313
|
-
<key>EnvironmentVariables</key>
|
|
314
|
-
<dict>
|
|
315
|
-
<key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
316
|
-
<key>HOME</key><string>${homedir()}</string>
|
|
317
|
-
</dict>
|
|
318
|
-
</dict>
|
|
319
|
-
</plist>`;
|
|
320
|
-
// 2. Daemon plist (KeepAlive — auto-restart on crash)
|
|
321
|
-
const daemonPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
322
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
323
|
-
<plist version="1.0">
|
|
324
|
-
<dict>
|
|
325
|
-
<key>Label</key><string>com.life-pulse.daemon</string>
|
|
326
|
-
<key>ProgramArguments</key>
|
|
327
|
-
<array>
|
|
328
|
-
<string>${npxPath}</string><string>life-pulse</string><string>--daemon</string>
|
|
329
|
-
</array>
|
|
330
|
-
<key>RunAtLoad</key><true/>
|
|
331
|
-
<key>KeepAlive</key>
|
|
332
|
-
<dict>
|
|
333
|
-
<key>SuccessfulExit</key><false/>
|
|
334
|
-
</dict>
|
|
335
|
-
<key>ThrottleInterval</key><integer>30</integer>
|
|
336
|
-
<key>StandardOutPath</key><string>${join(logDir, 'daemon.log')}</string>
|
|
337
|
-
<key>StandardErrorPath</key><string>${join(logDir, 'daemon-err.log')}</string>
|
|
338
|
-
<key>EnvironmentVariables</key>
|
|
339
|
-
<dict>
|
|
340
|
-
<key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
341
|
-
<key>HOME</key><string>${homedir()}</string>
|
|
342
|
-
</dict>
|
|
343
|
-
</dict>
|
|
344
|
-
</plist>`;
|
|
345
|
-
writeFileSync(join(agentsDir, 'com.life-pulse.morning.plist'), morningPlist);
|
|
346
|
-
writeFileSync(join(agentsDir, 'com.life-pulse.daemon.plist'), daemonPlist);
|
|
347
|
-
console.log(chalk.dim(' running in the background'));
|
|
348
|
-
console.log(chalk.dim(` morning brief at ${config.briefTime}`));
|
|
349
|
-
console.log();
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (!key && !ANTHROPIC_KEY) {
|
|
376
|
+
if (!ANTHROPIC_KEY) {
|
|
353
377
|
console.log(chalk.red('\n\n missing API key\n'));
|
|
354
378
|
process.exit(1);
|
|
355
379
|
}
|
|
356
|
-
const interactive = process.stdin.isTTY && !jsonMode
|
|
380
|
+
const interactive = process.stdin.isTTY && !jsonMode;
|
|
381
|
+
if (interactive && createdPromptFiles.length) {
|
|
382
|
+
console.log(chalk.dim(' initialized identity files'));
|
|
383
|
+
}
|
|
357
384
|
// ── Fire calendar fetch early ──
|
|
358
385
|
const calendarP = interactive ? fetchCalendarContext() : Promise.resolve('');
|
|
359
386
|
// ── Instant greeting + context line ──
|
|
@@ -366,11 +393,19 @@ async function main() {
|
|
|
366
393
|
await renderIntro(userName);
|
|
367
394
|
}
|
|
368
395
|
const spinner = interactive ? new Spinner() : undefined;
|
|
396
|
+
let crmWarmupP = null;
|
|
397
|
+
// Optimistic prewarm: kick off heavy profile context in the background.
|
|
398
|
+
if (interactive && hasRequiredPermissions()) {
|
|
399
|
+
crmWarmupP = buildCRM();
|
|
400
|
+
}
|
|
401
|
+
if (interactive && ANTHROPIC_KEY) {
|
|
402
|
+
void buildPersonalSummary();
|
|
403
|
+
}
|
|
369
404
|
// ── First-run: permissions → discovery → archetype ──
|
|
370
405
|
let crmShown = false;
|
|
371
406
|
const setupMode = process.argv.includes('--setup');
|
|
372
407
|
const isFirstRun = sessionProgress.totalSessions === 1;
|
|
373
|
-
if ((needsDiscovery() || setupMode || isFirstRun) && !
|
|
408
|
+
if ((needsDiscovery() || setupMode || isFirstRun) && !jsonMode) {
|
|
374
409
|
// Welcome message for first run
|
|
375
410
|
if (isFirstRun && interactive) {
|
|
376
411
|
console.log();
|
|
@@ -382,6 +417,9 @@ async function main() {
|
|
|
382
417
|
}
|
|
383
418
|
if (process.stdin.isTTY)
|
|
384
419
|
await runPermissionFlow();
|
|
420
|
+
if (interactive && !crmWarmupP && hasRequiredPermissions()) {
|
|
421
|
+
crmWarmupP = buildCRM();
|
|
422
|
+
}
|
|
385
423
|
spinner?.start('learning your world');
|
|
386
424
|
const platformProfile = discoverPlatforms();
|
|
387
425
|
// iCloud + local app discovery
|
|
@@ -415,7 +453,13 @@ async function main() {
|
|
|
415
453
|
if (interactive) {
|
|
416
454
|
spinner?.update('getting to know you');
|
|
417
455
|
const calCtx = await calendarP;
|
|
418
|
-
|
|
456
|
+
if (!crmWarmupP)
|
|
457
|
+
crmWarmupP = buildCRM();
|
|
458
|
+
crmShown = await showCRM(ANTHROPIC_KEY, {
|
|
459
|
+
calendarContext: calCtx,
|
|
460
|
+
spinner,
|
|
461
|
+
crmPromise: crmWarmupP,
|
|
462
|
+
});
|
|
419
463
|
}
|
|
420
464
|
if (ANTHROPIC_KEY) {
|
|
421
465
|
spinner?.start('figuring out who you are');
|
|
@@ -440,50 +484,27 @@ async function main() {
|
|
|
440
484
|
console.log();
|
|
441
485
|
}
|
|
442
486
|
}
|
|
443
|
-
//
|
|
444
|
-
if (legacyMode) {
|
|
445
|
-
spinner?.stop();
|
|
446
|
-
if (!jsonMode)
|
|
447
|
-
process.stdout.write(chalk.dim('\n pulling threads...'));
|
|
448
|
-
const collected = await collectAll();
|
|
449
|
-
if (!jsonMode)
|
|
450
|
-
process.stdout.write(chalk.dim(` ${collected.sources.length} sources... thinking`));
|
|
451
|
-
const analysis = await analyzeWithLLM(collected.data, key);
|
|
452
|
-
if (jsonMode) {
|
|
453
|
-
console.log(JSON.stringify({ collected, analysis }, null, 2));
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
renderAnalysis(analysis, collected.sources.length, collected.generated);
|
|
457
|
-
saveState(analysis);
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
// Agent mode (default): multi-turn investigation with Anthropic
|
|
487
|
+
// Agent mode: multi-turn investigation with Anthropic
|
|
461
488
|
if (!ANTHROPIC_KEY) {
|
|
462
489
|
spinner?.stop();
|
|
463
490
|
console.log(chalk.red('\n\n missing API key\n'));
|
|
464
491
|
process.exit(1);
|
|
465
492
|
}
|
|
466
|
-
// ── CRM: show relationship map before agent runs ──
|
|
467
|
-
if (interactive && !crmShown) {
|
|
468
|
-
spinner?.start('getting to know you');
|
|
469
|
-
const calCtx = await calendarP;
|
|
470
|
-
await showCRM(ANTHROPIC_KEY, { calendarContext: calCtx, spinner });
|
|
471
|
-
}
|
|
472
|
-
const renderer = interactive ? new InkProgress() : undefined;
|
|
473
|
-
renderer?.start();
|
|
474
493
|
// Track streamed cards so we don't double-show from final output
|
|
475
494
|
const streamedTitles = new Set();
|
|
476
495
|
let cardCount = 0;
|
|
477
|
-
|
|
496
|
+
// ── Start agent in background immediately — cards generate while CRM shows ──
|
|
497
|
+
const cardBuffer = [];
|
|
498
|
+
let cardsLive = false;
|
|
499
|
+
const renderer = interactive ? new InkProgress() : undefined;
|
|
500
|
+
const processCard = async (card) => {
|
|
478
501
|
cardCount++;
|
|
479
502
|
streamedTitles.add(card.title);
|
|
480
503
|
const pick = await pickCard(card, cardCount);
|
|
481
504
|
addTodo(card.title, pick, card.urgency || 'today', card.fyi || pick === 'noted');
|
|
482
505
|
collectedDecisions.push({ title: card.title, picked: pick });
|
|
483
|
-
// Record in session progress (Anthropic pattern: track decisions)
|
|
484
506
|
recordDecision(card.title, pick);
|
|
485
507
|
recordSurfaced(card.title, card.category || 'bump');
|
|
486
|
-
// Track active contacts for follow-ups
|
|
487
508
|
if (card.contact && !activeContacts.includes(card.contact)) {
|
|
488
509
|
activeContacts.push(card.contact);
|
|
489
510
|
}
|
|
@@ -491,8 +512,35 @@ async function main() {
|
|
|
491
512
|
pendingFollowUps.push(card.title);
|
|
492
513
|
}
|
|
493
514
|
return pick;
|
|
515
|
+
};
|
|
516
|
+
const onCard = interactive ? async (card) => {
|
|
517
|
+
if (cardsLive)
|
|
518
|
+
return processCard(card);
|
|
519
|
+
return new Promise((resolve) => {
|
|
520
|
+
cardBuffer.push({ card, resolve });
|
|
521
|
+
});
|
|
494
522
|
} : undefined;
|
|
495
|
-
const
|
|
523
|
+
const agentPromise = runAgent(ANTHROPIC_KEY, renderer, onCard);
|
|
524
|
+
// ── CRM: show relationship map while agent scans in background ──
|
|
525
|
+
if (interactive && !crmShown) {
|
|
526
|
+
spinner?.start('getting to know you');
|
|
527
|
+
const calCtx = await calendarP;
|
|
528
|
+
if (!crmWarmupP)
|
|
529
|
+
crmWarmupP = buildCRM();
|
|
530
|
+
await showCRM(ANTHROPIC_KEY, {
|
|
531
|
+
calendarContext: calCtx,
|
|
532
|
+
spinner,
|
|
533
|
+
crmPromise: crmWarmupP,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
renderer?.start();
|
|
537
|
+
cardsLive = true;
|
|
538
|
+
// Drain any cards that arrived during CRM display
|
|
539
|
+
for (const { card, resolve } of cardBuffer) {
|
|
540
|
+
resolve(await processCard(card));
|
|
541
|
+
}
|
|
542
|
+
cardBuffer.length = 0;
|
|
543
|
+
const analysis = await agentPromise;
|
|
496
544
|
renderer?.stop();
|
|
497
545
|
renderer?.clear();
|
|
498
546
|
if (jsonMode) {
|
|
@@ -523,12 +571,6 @@ async function main() {
|
|
|
523
571
|
for (const a of analysis.alpha)
|
|
524
572
|
console.log(` ★ ${a}`);
|
|
525
573
|
}
|
|
526
|
-
// Fallback: old format
|
|
527
|
-
for (const d of (analysis.decisions || [])) {
|
|
528
|
-
const rec = d.options?.[0];
|
|
529
|
-
const tag = d.fyi ? '[fyi]' : `[${d.urgency}]`;
|
|
530
|
-
console.log(d.fyi ? ` ${tag} ${d.title}` : ` ${tag} ${d.title} → ${rec?.label || 'no rec'}`);
|
|
531
|
-
}
|
|
532
574
|
saveState(analysis);
|
|
533
575
|
return;
|
|
534
576
|
}
|
|
@@ -543,45 +585,42 @@ async function main() {
|
|
|
543
585
|
// Handled (compact)
|
|
544
586
|
if (analysis.handled?.length)
|
|
545
587
|
renderHandled(analysis.handled, 5);
|
|
588
|
+
const remainingPromises = (analysis.promises || []).filter((d) => !streamedTitles.has(d.title));
|
|
589
|
+
const remainingBlockers = (analysis.blockers || []).filter((d) => !streamedTitles.has(d.title));
|
|
590
|
+
const remainingBumps = (analysis.bumps || []).filter((d) => !streamedTitles.has(d.title));
|
|
591
|
+
const totalCardsForWalk = cardCount
|
|
592
|
+
+ remainingPromises.length
|
|
593
|
+
+ remainingBlockers.length
|
|
594
|
+
+ remainingBumps.length;
|
|
546
595
|
// Helper: walk cards in a section with arrow-key TUI
|
|
547
596
|
const walkCards = async (items, category) => {
|
|
548
|
-
|
|
549
|
-
if (!remaining.length)
|
|
597
|
+
if (!items.length)
|
|
550
598
|
return;
|
|
551
|
-
for (let i = 0; i <
|
|
552
|
-
const d =
|
|
599
|
+
for (let i = 0; i < items.length; i++) {
|
|
600
|
+
const d = items[i];
|
|
553
601
|
const card = { ...d, category };
|
|
554
602
|
cardCount++;
|
|
555
|
-
const pick = await pickCard(card, cardCount);
|
|
603
|
+
const pick = await pickCard(card, cardCount, totalCardsForWalk);
|
|
556
604
|
addTodo(d.title, pick, d.urgency || 'today', false);
|
|
557
605
|
collectedDecisions.push({ title: d.title, picked: pick });
|
|
558
606
|
}
|
|
559
607
|
};
|
|
560
608
|
// Walk the four sections
|
|
561
|
-
if (
|
|
609
|
+
if (remainingPromises.length) {
|
|
562
610
|
renderSectionHeader('PROMISES', RED);
|
|
563
|
-
await walkCards(
|
|
611
|
+
await walkCards(remainingPromises, 'promise');
|
|
564
612
|
}
|
|
565
|
-
if (
|
|
613
|
+
if (remainingBlockers.length) {
|
|
566
614
|
renderSectionHeader('BLOCKED', AMB);
|
|
567
|
-
await walkCards(
|
|
615
|
+
await walkCards(remainingBlockers, 'blocker');
|
|
568
616
|
}
|
|
569
|
-
if (
|
|
617
|
+
if (remainingBumps.length) {
|
|
570
618
|
renderSectionHeader('BUMPS', CYN);
|
|
571
|
-
await walkCards(
|
|
619
|
+
await walkCards(remainingBumps, 'bump');
|
|
572
620
|
}
|
|
573
621
|
if (analysis.alpha?.length) {
|
|
574
622
|
renderSection('ALPHA', analysis.alpha, '★', MAG.bold, MAG);
|
|
575
623
|
}
|
|
576
|
-
// Fallback: old decisions format (for legacy/transition)
|
|
577
|
-
const oldRemaining = (analysis.decisions || []).filter(d => !streamedTitles.has(d.title));
|
|
578
|
-
for (let i = 0; i < oldRemaining.length; i++) {
|
|
579
|
-
const d = oldRemaining[i];
|
|
580
|
-
cardCount++;
|
|
581
|
-
const pick = await pickCard(d, cardCount);
|
|
582
|
-
addTodo(d.title, pick, d.urgency || 'today', d.fyi || pick === 'noted');
|
|
583
|
-
collectedDecisions.push({ title: d.title, picked: pick });
|
|
584
|
-
}
|
|
585
624
|
// Persist handled for delta context
|
|
586
625
|
for (const h of (analysis.handled || [])) {
|
|
587
626
|
addTodo(h, 'handled', 'today', true);
|
|
@@ -590,7 +629,6 @@ async function main() {
|
|
|
590
629
|
...(analysis.promises || []),
|
|
591
630
|
...(analysis.blockers || []),
|
|
592
631
|
...(analysis.bumps || []),
|
|
593
|
-
...(analysis.decisions || []),
|
|
594
632
|
].some((d) => d.options?.length);
|
|
595
633
|
if (!hasActionable && !(analysis.alpha?.length)) {
|
|
596
634
|
console.log(DIM(' All clear — nothing needs you right now.'));
|
|
@@ -601,6 +639,17 @@ async function main() {
|
|
|
601
639
|
saveState(analysis);
|
|
602
640
|
// ── Stay resident: message loop + transport layer ──
|
|
603
641
|
const config = loadConfig();
|
|
642
|
+
// End-of-brief text so the plan follows you out of terminal.
|
|
643
|
+
if (config.briefSmsEnabled !== false) {
|
|
644
|
+
const targetPhone = resolveBriefSmsTarget(config);
|
|
645
|
+
if (targetPhone) {
|
|
646
|
+
const text = buildBriefingText(getUserName(), analysis);
|
|
647
|
+
const sent = await sendBriefingText(targetPhone, text);
|
|
648
|
+
if (!daemonMode && sent) {
|
|
649
|
+
console.log(chalk.dim(` texted your brief to ${targetPhone}`));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
604
653
|
// Checkpoint state before going resident (Anthropic pattern: save before risky ops)
|
|
605
654
|
checkpoint('pre-resident');
|
|
606
655
|
recordSuccess(); // We made it through the briefing — reset crash counter
|
|
@@ -658,15 +707,30 @@ async function main() {
|
|
|
658
707
|
recordCrash(err.message, 'message-loop');
|
|
659
708
|
},
|
|
660
709
|
});
|
|
661
|
-
|
|
662
|
-
console.log(chalk.dim(' listening'));
|
|
663
|
-
console.log();
|
|
664
|
-
}
|
|
665
|
-
// Start health server for daemon mode
|
|
710
|
+
// Start NOX gateway in resident mode (daemon + interactive)
|
|
666
711
|
let gw = null;
|
|
712
|
+
if (ANTHROPIC_KEY) {
|
|
713
|
+
gw = startGateway(ANTHROPIC_KEY);
|
|
714
|
+
}
|
|
715
|
+
let noxEndpoint = null;
|
|
716
|
+
if (hasTailscale()) {
|
|
717
|
+
noxEndpoint = startFunnel(19877);
|
|
718
|
+
if (!noxEndpoint) {
|
|
719
|
+
const host = getTailscaleHostname();
|
|
720
|
+
if (host)
|
|
721
|
+
noxEndpoint = `https://${host}`;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
667
724
|
if (daemonMode) {
|
|
668
725
|
startHealthServer();
|
|
669
|
-
|
|
726
|
+
}
|
|
727
|
+
if (!daemonMode) {
|
|
728
|
+
console.log(chalk.dim(' listening'));
|
|
729
|
+
console.log(chalk.dim(' nox number is live'));
|
|
730
|
+
if (noxEndpoint)
|
|
731
|
+
console.log(chalk.dim(` endpoint ${noxEndpoint}`));
|
|
732
|
+
console.log(chalk.green(' press ctrl+c to end demo'));
|
|
733
|
+
console.log();
|
|
670
734
|
}
|
|
671
735
|
// Generate knowledge bases from CRM on startup
|
|
672
736
|
try {
|
|
@@ -701,79 +765,6 @@ async function main() {
|
|
|
701
765
|
}
|
|
702
766
|
await new Promise(() => { });
|
|
703
767
|
}
|
|
704
|
-
function renderAnalysis(analysis, sourceCount, generated) {
|
|
705
|
-
console.log();
|
|
706
|
-
console.log(chalk.bold(' LIFE PULSE'));
|
|
707
|
-
console.log();
|
|
708
|
-
console.log(` ${analysis.greeting}`);
|
|
709
|
-
console.log();
|
|
710
|
-
if (analysis.decisions?.length) {
|
|
711
|
-
console.log(chalk.bold.red(' DECISIONS'));
|
|
712
|
-
console.log();
|
|
713
|
-
for (const d of analysis.decisions) {
|
|
714
|
-
const tag = d.fyi ? chalk.dim('[fyi]') : `[${d.urgency}]`;
|
|
715
|
-
console.log(` ${d.title} ${tag}`);
|
|
716
|
-
if (d.options?.length) {
|
|
717
|
-
for (const o of d.options)
|
|
718
|
-
console.log(` · ${o.label} ${chalk.dim('— ' + (o.description || ''))}`);
|
|
719
|
-
}
|
|
720
|
-
console.log();
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
if (analysis.upcoming?.length) {
|
|
724
|
-
console.log(chalk.bold.cyan(' UPCOMING'));
|
|
725
|
-
console.log();
|
|
726
|
-
renderList(analysis.upcoming, chalk.cyan('▸'));
|
|
727
|
-
console.log();
|
|
728
|
-
}
|
|
729
|
-
if (analysis.handled?.length) {
|
|
730
|
-
console.log(chalk.bold.green(' HANDLED'));
|
|
731
|
-
console.log();
|
|
732
|
-
renderList(analysis.handled, chalk.green('✓'));
|
|
733
|
-
console.log();
|
|
734
|
-
}
|
|
735
|
-
if (analysis.intel?.length) {
|
|
736
|
-
console.log(chalk.bold.magenta(' INTEL'));
|
|
737
|
-
console.log();
|
|
738
|
-
renderList(analysis.intel, chalk.magenta('~'));
|
|
739
|
-
console.log();
|
|
740
|
-
}
|
|
741
|
-
// Legacy format fallback
|
|
742
|
-
if (analysis.right_now?.length) {
|
|
743
|
-
console.log(chalk.bold.red(' RIGHT NOW'));
|
|
744
|
-
console.log();
|
|
745
|
-
renderList(analysis.right_now, chalk.red('→'));
|
|
746
|
-
console.log();
|
|
747
|
-
}
|
|
748
|
-
if (analysis.today?.length) {
|
|
749
|
-
console.log(chalk.bold.white(' TODAY'));
|
|
750
|
-
console.log();
|
|
751
|
-
renderList(analysis.today, chalk.dim('·'));
|
|
752
|
-
console.log();
|
|
753
|
-
}
|
|
754
|
-
if (analysis.this_week?.length) {
|
|
755
|
-
console.log(chalk.bold(' THIS WEEK'));
|
|
756
|
-
console.log();
|
|
757
|
-
renderList(analysis.this_week, chalk.dim('·'));
|
|
758
|
-
console.log();
|
|
759
|
-
}
|
|
760
|
-
if (analysis.heads_up?.length) {
|
|
761
|
-
console.log(chalk.bold.cyan(' HEADS UP'));
|
|
762
|
-
console.log();
|
|
763
|
-
renderList(analysis.heads_up, chalk.cyan('!'));
|
|
764
|
-
console.log();
|
|
765
|
-
}
|
|
766
|
-
if (analysis.noticed?.length) {
|
|
767
|
-
console.log(chalk.bold.magenta(' NOTICED'));
|
|
768
|
-
console.log();
|
|
769
|
-
renderList(analysis.noticed, chalk.magenta('~'));
|
|
770
|
-
console.log();
|
|
771
|
-
}
|
|
772
|
-
if (sourceCount) {
|
|
773
|
-
console.log(chalk.dim(` ${sourceCount} sources · ${generated}`));
|
|
774
|
-
console.log();
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
768
|
main().catch(e => {
|
|
778
769
|
console.error(chalk.red('\n Error:'), e.message);
|
|
779
770
|
process.exit(1);
|