life-pulse 2.3.9 → 2.3.11
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 +341 -167
- 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 +55 -24
- 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/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { collectAll } from './index.js';
|
|
3
3
|
import { runAgent } from './agent.js';
|
|
4
|
-
import { analyzeWithLLM } from './analyze.js';
|
|
5
4
|
import { saveState, saveDecisions } from './state.js';
|
|
6
5
|
// ProgressRenderer unused — removed
|
|
7
6
|
import { InkProgress } from './ui/progress.js';
|
|
8
7
|
import { addTodo, resolveTodos, pruneOld } from './todo.js';
|
|
9
|
-
import { renderIntro,
|
|
8
|
+
import { renderIntro, revealContacts, revealInsights, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, HD, RED, AMB, MID, DIM } from './tui.js';
|
|
10
9
|
import { needsDiscovery, discoverPlatforms, savePlatforms } from './platforms.js';
|
|
11
10
|
import { generateArchetype } from './archetype.js';
|
|
12
|
-
import { runPermissionFlow, getMissingPermissions } from './permissions.js';
|
|
13
|
-
import { buildCRM, streamEnrichedCRM, generateInsights
|
|
11
|
+
import { runPermissionFlow, hasRequiredPermissions, getMissingPermissions } from './permissions.js';
|
|
12
|
+
import { buildCRM, streamEnrichedCRM, generateInsights } from './crm.js';
|
|
14
13
|
import { saveContactSummaries } from './intelligence.js';
|
|
15
|
-
import { getUserName } from './profile.js';
|
|
14
|
+
import { buildPersonalSummary, getUserName } from './profile.js';
|
|
16
15
|
// Session progress tracking (Anthropic long-running agent pattern)
|
|
17
16
|
import { startSession, endSession, loadProgress, recordDecision, recordSurfaced, getTimeSinceLastSession, getNewDiscoveries } from './session-progress.js';
|
|
18
17
|
import { runDiscovery, formatDiscoveryReport } from './icloud-discovery.js';
|
|
@@ -21,7 +20,7 @@ import { startHealthServer, stopHealthServer, isDaemonRunning, getHealthStatus,
|
|
|
21
20
|
// New: long-running agent harness modules
|
|
22
21
|
import { runInitCheck } from './init-check.js';
|
|
23
22
|
import { installCrashHandlers, recordSuccess, recordCrash, checkpoint, getCrashStats } from './watchdog.js';
|
|
24
|
-
import { startMessageLoop } from './message-loop.js';
|
|
23
|
+
import { startMessageLoop, sendMessage } from './message-loop.js';
|
|
25
24
|
import { scanForSkills } from './skill-loader.js';
|
|
26
25
|
import { TransportManager, IMessageTransport, TelegramTransport } from './transport.js';
|
|
27
26
|
import { runInstaller } from './installer.js';
|
|
@@ -29,12 +28,16 @@ import { converse } from './conversation.js';
|
|
|
29
28
|
import { startGateway, isGatewayUp } from './sms-gateway.js';
|
|
30
29
|
import { updateAutoKnowledge } from './knowledge.js';
|
|
31
30
|
import { startRouter, initClientRegistry } from './router.js';
|
|
31
|
+
import { findHandlesForName } from './contacts.js';
|
|
32
|
+
import { ensurePromptLayerFiles } from './prompt-layers.js';
|
|
33
|
+
import { hasTailscale, startFunnel, getHostname as getTailscaleHostname } from './tunnel.js';
|
|
32
34
|
import chalk from 'chalk';
|
|
33
35
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
34
36
|
import { join, dirname } from 'path';
|
|
35
37
|
import { fileURLToPath } from 'url';
|
|
36
38
|
import { homedir } from 'os';
|
|
37
39
|
import { execSync, execFileSync } from 'child_process';
|
|
40
|
+
import { createInterface } from 'readline';
|
|
38
41
|
import dayjs from 'dayjs';
|
|
39
42
|
const collectedDecisions = [];
|
|
40
43
|
const DEFAULT_CONFIG = {
|
|
@@ -42,6 +45,7 @@ const DEFAULT_CONFIG = {
|
|
|
42
45
|
monitorIntervalSec: 30,
|
|
43
46
|
notifyTiers: ['T1', 'T2'],
|
|
44
47
|
quietHours: { start: '22:00', end: '07:00' },
|
|
48
|
+
briefSmsEnabled: true,
|
|
45
49
|
};
|
|
46
50
|
function loadConfig() {
|
|
47
51
|
const p = join(homedir(), 'Library/Application Support/life-pulse/config.json');
|
|
@@ -64,12 +68,9 @@ function loadEnvFile(path) {
|
|
|
64
68
|
// Project-local .env first (dev), then ~/.config/life-pulse/.env (npm users)
|
|
65
69
|
loadEnvFile(join(dirname(fileURLToPath(import.meta.url)), '..', '.env'));
|
|
66
70
|
loadEnvFile(join(homedir(), '.config', 'life-pulse', '.env'));
|
|
67
|
-
const API_KEY = process.env.OPENROUTER_API_KEY || process.env.LLM_API_KEY || '';
|
|
68
71
|
const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
console.log(` ${bullet} ${item}`);
|
|
72
|
-
}
|
|
72
|
+
function sleep(ms) {
|
|
73
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
73
74
|
}
|
|
74
75
|
async function fetchCalendarContext() {
|
|
75
76
|
try {
|
|
@@ -82,52 +83,195 @@ async function fetchCalendarContext() {
|
|
|
82
83
|
catch { }
|
|
83
84
|
return '';
|
|
84
85
|
}
|
|
86
|
+
function normalizePhoneCandidate(raw) {
|
|
87
|
+
const t = raw.trim();
|
|
88
|
+
if (!t)
|
|
89
|
+
return '';
|
|
90
|
+
if (t.startsWith('+'))
|
|
91
|
+
return '+' + t.slice(1).replace(/\D/g, '');
|
|
92
|
+
return t.replace(/\D/g, '');
|
|
93
|
+
}
|
|
94
|
+
async function promptLine(question, fallback = '') {
|
|
95
|
+
if (!process.stdin.isTTY)
|
|
96
|
+
return fallback;
|
|
97
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
98
|
+
const suffix = fallback ? ` [${fallback}]` : '';
|
|
99
|
+
return new Promise(resolve => {
|
|
100
|
+
rl.question(` ${question}${suffix}: `, answer => {
|
|
101
|
+
rl.close();
|
|
102
|
+
const value = answer.trim();
|
|
103
|
+
resolve(value || fallback);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function detectOpenClawToken() {
|
|
108
|
+
const keys = [
|
|
109
|
+
'gateway.http.auth.token',
|
|
110
|
+
'gateway.auth.token',
|
|
111
|
+
'gateway.http.token',
|
|
112
|
+
];
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
try {
|
|
115
|
+
const out = execSync(`openclaw config get ${key}`, { stdio: 'pipe', timeout: 5000, encoding: 'utf-8' }).trim();
|
|
116
|
+
if (!out)
|
|
117
|
+
continue;
|
|
118
|
+
const low = out.toLowerCase();
|
|
119
|
+
if (low.includes('not set') || low.includes('unknown') || low.includes('error'))
|
|
120
|
+
continue;
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
catch { }
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
function writeNoxRouteJson(payload) {
|
|
128
|
+
const desktop = join(homedir(), 'Desktop');
|
|
129
|
+
mkdirSync(desktop, { recursive: true });
|
|
130
|
+
const path = join(desktop, 'nox-route.json');
|
|
131
|
+
writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
132
|
+
try {
|
|
133
|
+
execSync(`cat ${JSON.stringify(path)} | pbcopy`, { stdio: 'pipe', timeout: 5000 });
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
try {
|
|
137
|
+
execSync(`open -R ${JSON.stringify(path)}`, { stdio: 'pipe', timeout: 5000 });
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
return path;
|
|
141
|
+
}
|
|
142
|
+
function loadPhoneFromClientsRegistry() {
|
|
143
|
+
const p = join(homedir(), '.life-pulse', 'clients.json');
|
|
144
|
+
if (!existsSync(p))
|
|
145
|
+
return null;
|
|
146
|
+
try {
|
|
147
|
+
const json = JSON.parse(readFileSync(p, 'utf-8'));
|
|
148
|
+
const enabled = (json.clients || []).filter(c => c.enabled !== false && c.phone);
|
|
149
|
+
if (!enabled.length)
|
|
150
|
+
return null;
|
|
151
|
+
const first = normalizePhoneCandidate(enabled[0].phone || '');
|
|
152
|
+
return first || null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function resolveBriefSmsTarget(config) {
|
|
159
|
+
const explicit = [
|
|
160
|
+
process.env.LIFE_PULSE_BRIEF_SMS_PHONE,
|
|
161
|
+
process.env.LIFE_PULSE_SELF_PHONE,
|
|
162
|
+
process.env.NOX_OWNER_PHONE,
|
|
163
|
+
config.briefSmsPhone,
|
|
164
|
+
].map(v => normalizePhoneCandidate(v || '')).find(Boolean);
|
|
165
|
+
if (explicit)
|
|
166
|
+
return explicit;
|
|
167
|
+
const fromName = findHandlesForName(getUserName())
|
|
168
|
+
.map(h => normalizePhoneCandidate(h))
|
|
169
|
+
.find(h => /^\+?\d{10,15}$/.test(h));
|
|
170
|
+
if (fromName)
|
|
171
|
+
return fromName;
|
|
172
|
+
return loadPhoneFromClientsRegistry();
|
|
173
|
+
}
|
|
174
|
+
function buildBriefingText(name, analysis) {
|
|
175
|
+
const first = name.split(' ')[0] || name;
|
|
176
|
+
const lines = [`hey ${first}, quick life-pulse brief:`];
|
|
177
|
+
const addItems = (label, items, max) => {
|
|
178
|
+
if (!items?.length)
|
|
179
|
+
return;
|
|
180
|
+
let added = 0;
|
|
181
|
+
for (const item of items) {
|
|
182
|
+
if (added >= max)
|
|
183
|
+
break;
|
|
184
|
+
const title = String(item?.title || '').trim();
|
|
185
|
+
if (!title)
|
|
186
|
+
continue;
|
|
187
|
+
const rec = item?.options?.[0]?.label ? ` -> ${item.options[0].label}` : '';
|
|
188
|
+
lines.push(`${label} ${title}${rec}`);
|
|
189
|
+
added++;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
addItems('PROMISE:', analysis.promises, 2);
|
|
193
|
+
addItems('BLOCKER:', analysis.blockers, 2);
|
|
194
|
+
addItems('BUMP:', analysis.bumps, 1);
|
|
195
|
+
if (analysis.alpha?.length) {
|
|
196
|
+
const alpha = String(analysis.alpha[0] || '').trim();
|
|
197
|
+
if (alpha)
|
|
198
|
+
lines.push(`ALPHA: ${alpha}`);
|
|
199
|
+
}
|
|
200
|
+
if (lines.length === 1)
|
|
201
|
+
lines.push('all clear. nothing urgent right now.');
|
|
202
|
+
return lines.join('\n').slice(0, 1400);
|
|
203
|
+
}
|
|
204
|
+
async function sendViaImwebserver(phone, message) {
|
|
205
|
+
try {
|
|
206
|
+
const resp = await fetch('http://127.0.0.1:8888/sendMessage', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: { 'Content-Type': 'application/json' },
|
|
209
|
+
body: JSON.stringify({ phone, message }),
|
|
210
|
+
signal: AbortSignal.timeout(5_000),
|
|
211
|
+
});
|
|
212
|
+
return resp.ok;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function sendBriefingText(phone, message) {
|
|
219
|
+
if (await sendViaImwebserver(phone, message))
|
|
220
|
+
return true;
|
|
221
|
+
return sendMessage(phone, message, false);
|
|
222
|
+
}
|
|
85
223
|
async function showCRM(apiKey, opts) {
|
|
86
|
-
const crm = await buildCRM();
|
|
224
|
+
const crm = opts?.crmPromise ? await opts.crmPromise : await buildCRM();
|
|
87
225
|
if (!crm.threads.length || !apiKey) {
|
|
88
226
|
opts?.spinner?.stop();
|
|
89
227
|
return false;
|
|
90
228
|
}
|
|
91
229
|
opts?.spinner?.stop();
|
|
92
230
|
console.log();
|
|
231
|
+
console.log(` ${HD('your inner circle')}`);
|
|
232
|
+
console.log();
|
|
93
233
|
// Fire insights in background — they'll resolve while user steps through contacts
|
|
94
234
|
const insightsGen = generateInsights(crm);
|
|
95
235
|
const calendarContext = opts?.calendarContext ?? '';
|
|
96
236
|
const hour = new Date().getHours();
|
|
97
237
|
const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
|
|
98
238
|
let brief = '';
|
|
239
|
+
let resolveBrief = null;
|
|
240
|
+
const briefReady = new Promise(resolve => { resolveBrief = resolve; });
|
|
99
241
|
const enriched = await revealContacts(streamEnrichedCRM(crm, apiKey, {
|
|
100
242
|
calendarContext,
|
|
101
243
|
timeOfDay,
|
|
102
|
-
onBrief: (b) => {
|
|
244
|
+
onBrief: (b) => {
|
|
245
|
+
brief = b;
|
|
246
|
+
resolveBrief?.();
|
|
247
|
+
},
|
|
248
|
+
briefAsync: true,
|
|
103
249
|
}));
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.slice(0, 5);
|
|
109
|
-
if (remaining.length)
|
|
110
|
-
renderCRMList(remaining);
|
|
111
|
-
if (brief)
|
|
250
|
+
// Optimistic brief: wait a beat, then move on.
|
|
251
|
+
await Promise.race([briefReady, sleep(300)]);
|
|
252
|
+
if (brief) {
|
|
253
|
+
renderDivider();
|
|
112
254
|
await renderBrief(brief);
|
|
113
|
-
|
|
114
|
-
|
|
255
|
+
}
|
|
256
|
+
// Optimistic insights: show if they land quickly, don't block the flow.
|
|
257
|
+
await revealInsights(insightsGen, { maxWaitMs: 900 });
|
|
258
|
+
renderDivider();
|
|
115
259
|
saveContactSummaries(enriched);
|
|
116
260
|
return true;
|
|
117
261
|
}
|
|
118
262
|
async function main() {
|
|
263
|
+
const createdPromptFiles = ensurePromptLayerFiles();
|
|
119
264
|
const jsonMode = process.argv.includes('--json');
|
|
120
265
|
const rawMode = process.argv.includes('--raw');
|
|
121
|
-
const legacyMode = process.argv.includes('--legacy');
|
|
122
266
|
const statusMode = process.argv.includes('--status');
|
|
123
267
|
const daemonMode = process.argv.includes('--daemon');
|
|
124
268
|
const healthMode = process.argv.includes('--health');
|
|
125
269
|
const initCheckMode = process.argv.includes('--check');
|
|
126
270
|
const phoneSetupMode = process.argv.includes('--phone-setup');
|
|
127
271
|
const testSmsMode = process.argv.includes('--test-sms');
|
|
272
|
+
const pairMode = process.argv.includes('--pair') || process.argv.includes('--export-route');
|
|
128
273
|
const routerMode = process.argv.includes('--router');
|
|
129
274
|
const initClientsMode = process.argv.includes('--init-clients');
|
|
130
|
-
const key = process.argv.find(a => a.startsWith('--key='))?.split('=')[1] || API_KEY;
|
|
131
275
|
// Install crash handlers early (Anthropic pattern: recover from failures)
|
|
132
276
|
installCrashHandlers((err) => {
|
|
133
277
|
console.error(chalk.red(` crash: ${err.message}`));
|
|
@@ -145,7 +289,7 @@ async function main() {
|
|
|
145
289
|
console.log();
|
|
146
290
|
return;
|
|
147
291
|
}
|
|
148
|
-
// --router:
|
|
292
|
+
// --router: legacy bridge mode (multi-machine relay setups)
|
|
149
293
|
if (routerMode) {
|
|
150
294
|
console.log(chalk.bold.hex('#c0caf5')(' life-pulse'));
|
|
151
295
|
console.log(chalk.dim(' routing messages'));
|
|
@@ -191,6 +335,52 @@ async function main() {
|
|
|
191
335
|
}
|
|
192
336
|
return;
|
|
193
337
|
}
|
|
338
|
+
// --pair / --export-route: generate Desktop/nox-route.json for NOX routing
|
|
339
|
+
if (pairMode) {
|
|
340
|
+
const defaultName = getUserName() || '';
|
|
341
|
+
const detectedHost = getTailscaleHostname() || '';
|
|
342
|
+
const detectedToken = detectOpenClawToken();
|
|
343
|
+
const envPhone = normalizePhoneCandidate(process.env.LIFE_PULSE_SELF_PHONE
|
|
344
|
+
|| process.env.NOX_OWNER_PHONE
|
|
345
|
+
|| process.env.LIFE_PULSE_BRIEF_SMS_PHONE
|
|
346
|
+
|| '');
|
|
347
|
+
console.log();
|
|
348
|
+
console.log(chalk.bold.hex('#c0caf5')(' pair with nox'));
|
|
349
|
+
console.log(chalk.dim(' we will create Desktop/nox-route.json'));
|
|
350
|
+
console.log();
|
|
351
|
+
const name = await promptLine('name', defaultName);
|
|
352
|
+
const phoneRaw = await promptLine('iphone number (+1...)', envPhone);
|
|
353
|
+
const phone = normalizePhoneCandidate(phoneRaw);
|
|
354
|
+
if (!phone) {
|
|
355
|
+
console.log(chalk.red(' missing phone number'));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const host = await promptLine('tailscale host/ip', detectedHost);
|
|
359
|
+
if (!host) {
|
|
360
|
+
console.log(chalk.red(' missing tailscale host/ip'));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const token = await promptLine('openclaw token', detectedToken);
|
|
364
|
+
if (!token) {
|
|
365
|
+
console.log(chalk.red(' missing openclaw token'));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const payload = {
|
|
369
|
+
name: name || 'User',
|
|
370
|
+
phone,
|
|
371
|
+
openclaw_url: `http://${host}:18789`,
|
|
372
|
+
openclaw_token: token,
|
|
373
|
+
openclaw_agent_id: 'main',
|
|
374
|
+
openclaw_model: 'openclaw:main',
|
|
375
|
+
enabled: true,
|
|
376
|
+
};
|
|
377
|
+
const out = writeNoxRouteJson(payload);
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(chalk.green(` saved ${out}`));
|
|
380
|
+
console.log(chalk.dim(' send this file to your operator'));
|
|
381
|
+
console.log();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
194
384
|
// --health: check if daemon is running and get status
|
|
195
385
|
if (healthMode) {
|
|
196
386
|
if (isDaemonRunning()) {
|
|
@@ -254,7 +444,8 @@ async function main() {
|
|
|
254
444
|
activeContacts,
|
|
255
445
|
pendingFollowUps,
|
|
256
446
|
});
|
|
257
|
-
console.log(chalk.
|
|
447
|
+
console.log(chalk.green('\n demo wrapped cleanly'));
|
|
448
|
+
console.log(chalk.dim(' progress saved'));
|
|
258
449
|
process.exit(0);
|
|
259
450
|
};
|
|
260
451
|
// Auto-detect installer mode: first run with no state → white-glove installer
|
|
@@ -262,7 +453,7 @@ async function main() {
|
|
|
262
453
|
const installStateExists = existsSync(join(stateDir, 'install-state.json'));
|
|
263
454
|
const noSessionHistory = sessionProgress.totalSessions <= 1;
|
|
264
455
|
const isSetupFlag = process.argv.includes('--setup');
|
|
265
|
-
if ((noSessionHistory && !installStateExists && !rawMode && !jsonMode && !
|
|
456
|
+
if ((noSessionHistory && !installStateExists && !rawMode && !jsonMode && !daemonMode && !process.argv.includes('--install'))
|
|
266
457
|
|| isSetupFlag) {
|
|
267
458
|
// First run: launch full installer
|
|
268
459
|
await runInstaller(ANTHROPIC_KEY);
|
|
@@ -348,16 +539,35 @@ async function main() {
|
|
|
348
539
|
</plist>`;
|
|
349
540
|
writeFileSync(join(agentsDir, 'com.life-pulse.morning.plist'), morningPlist);
|
|
350
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
|
+
}
|
|
351
552
|
console.log(chalk.dim(' running in the background'));
|
|
352
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
|
+
}
|
|
353
560
|
console.log();
|
|
354
561
|
return;
|
|
355
562
|
}
|
|
356
|
-
if (!
|
|
563
|
+
if (!ANTHROPIC_KEY) {
|
|
357
564
|
console.log(chalk.red('\n\n missing API key\n'));
|
|
358
565
|
process.exit(1);
|
|
359
566
|
}
|
|
360
|
-
const interactive = process.stdin.isTTY && !jsonMode
|
|
567
|
+
const interactive = process.stdin.isTTY && !jsonMode;
|
|
568
|
+
if (interactive && createdPromptFiles.length) {
|
|
569
|
+
console.log(chalk.dim(' initialized identity files'));
|
|
570
|
+
}
|
|
361
571
|
// ── Fire calendar fetch early ──
|
|
362
572
|
const calendarP = interactive ? fetchCalendarContext() : Promise.resolve('');
|
|
363
573
|
// ── Instant greeting + context line ──
|
|
@@ -370,6 +580,14 @@ async function main() {
|
|
|
370
580
|
await renderIntro(userName);
|
|
371
581
|
}
|
|
372
582
|
const spinner = interactive ? new Spinner() : undefined;
|
|
583
|
+
let crmWarmupP = null;
|
|
584
|
+
// Optimistic prewarm: kick off heavy profile context in the background.
|
|
585
|
+
if (interactive && hasRequiredPermissions()) {
|
|
586
|
+
crmWarmupP = buildCRM();
|
|
587
|
+
}
|
|
588
|
+
if (interactive && ANTHROPIC_KEY) {
|
|
589
|
+
void buildPersonalSummary();
|
|
590
|
+
}
|
|
373
591
|
// ── First-run: permissions → discovery → archetype ──
|
|
374
592
|
let crmShown = false;
|
|
375
593
|
const setupMode = process.argv.includes('--setup');
|
|
@@ -386,6 +604,9 @@ async function main() {
|
|
|
386
604
|
}
|
|
387
605
|
if (process.stdin.isTTY)
|
|
388
606
|
await runPermissionFlow();
|
|
607
|
+
if (interactive && !crmWarmupP && hasRequiredPermissions()) {
|
|
608
|
+
crmWarmupP = buildCRM();
|
|
609
|
+
}
|
|
389
610
|
spinner?.start('learning your world');
|
|
390
611
|
const platformProfile = discoverPlatforms();
|
|
391
612
|
// iCloud + local app discovery
|
|
@@ -419,7 +640,13 @@ async function main() {
|
|
|
419
640
|
if (interactive) {
|
|
420
641
|
spinner?.update('getting to know you');
|
|
421
642
|
const calCtx = await calendarP;
|
|
422
|
-
|
|
643
|
+
if (!crmWarmupP)
|
|
644
|
+
crmWarmupP = buildCRM();
|
|
645
|
+
crmShown = await showCRM(ANTHROPIC_KEY, {
|
|
646
|
+
calendarContext: calCtx,
|
|
647
|
+
spinner,
|
|
648
|
+
crmPromise: crmWarmupP,
|
|
649
|
+
});
|
|
423
650
|
}
|
|
424
651
|
if (ANTHROPIC_KEY) {
|
|
425
652
|
spinner?.start('figuring out who you are');
|
|
@@ -444,50 +671,27 @@ async function main() {
|
|
|
444
671
|
console.log();
|
|
445
672
|
}
|
|
446
673
|
}
|
|
447
|
-
//
|
|
448
|
-
if (legacyMode) {
|
|
449
|
-
spinner?.stop();
|
|
450
|
-
if (!jsonMode)
|
|
451
|
-
process.stdout.write(chalk.dim('\n pulling threads...'));
|
|
452
|
-
const collected = await collectAll();
|
|
453
|
-
if (!jsonMode)
|
|
454
|
-
process.stdout.write(chalk.dim(` ${collected.sources.length} sources... thinking`));
|
|
455
|
-
const analysis = await analyzeWithLLM(collected.data, key);
|
|
456
|
-
if (jsonMode) {
|
|
457
|
-
console.log(JSON.stringify({ collected, analysis }, null, 2));
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
renderAnalysis(analysis, collected.sources.length, collected.generated);
|
|
461
|
-
saveState(analysis);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
// Agent mode (default): multi-turn investigation with Anthropic
|
|
674
|
+
// Agent mode: multi-turn investigation with Anthropic
|
|
465
675
|
if (!ANTHROPIC_KEY) {
|
|
466
676
|
spinner?.stop();
|
|
467
677
|
console.log(chalk.red('\n\n missing API key\n'));
|
|
468
678
|
process.exit(1);
|
|
469
679
|
}
|
|
470
|
-
// ── CRM: show relationship map before agent runs ──
|
|
471
|
-
if (interactive && !crmShown) {
|
|
472
|
-
spinner?.start('getting to know you');
|
|
473
|
-
const calCtx = await calendarP;
|
|
474
|
-
await showCRM(ANTHROPIC_KEY, { calendarContext: calCtx, spinner });
|
|
475
|
-
}
|
|
476
|
-
const renderer = interactive ? new InkProgress() : undefined;
|
|
477
|
-
renderer?.start();
|
|
478
680
|
// Track streamed cards so we don't double-show from final output
|
|
479
681
|
const streamedTitles = new Set();
|
|
480
682
|
let cardCount = 0;
|
|
481
|
-
|
|
683
|
+
// ── Start agent in background immediately — cards generate while CRM shows ──
|
|
684
|
+
const cardBuffer = [];
|
|
685
|
+
let cardsLive = false;
|
|
686
|
+
const renderer = interactive ? new InkProgress() : undefined;
|
|
687
|
+
const processCard = async (card) => {
|
|
482
688
|
cardCount++;
|
|
483
689
|
streamedTitles.add(card.title);
|
|
484
690
|
const pick = await pickCard(card, cardCount);
|
|
485
691
|
addTodo(card.title, pick, card.urgency || 'today', card.fyi || pick === 'noted');
|
|
486
692
|
collectedDecisions.push({ title: card.title, picked: pick });
|
|
487
|
-
// Record in session progress (Anthropic pattern: track decisions)
|
|
488
693
|
recordDecision(card.title, pick);
|
|
489
694
|
recordSurfaced(card.title, card.category || 'bump');
|
|
490
|
-
// Track active contacts for follow-ups
|
|
491
695
|
if (card.contact && !activeContacts.includes(card.contact)) {
|
|
492
696
|
activeContacts.push(card.contact);
|
|
493
697
|
}
|
|
@@ -495,8 +699,35 @@ async function main() {
|
|
|
495
699
|
pendingFollowUps.push(card.title);
|
|
496
700
|
}
|
|
497
701
|
return pick;
|
|
702
|
+
};
|
|
703
|
+
const onCard = interactive ? async (card) => {
|
|
704
|
+
if (cardsLive)
|
|
705
|
+
return processCard(card);
|
|
706
|
+
return new Promise((resolve) => {
|
|
707
|
+
cardBuffer.push({ card, resolve });
|
|
708
|
+
});
|
|
498
709
|
} : undefined;
|
|
499
|
-
const
|
|
710
|
+
const agentPromise = runAgent(ANTHROPIC_KEY, renderer, onCard);
|
|
711
|
+
// ── CRM: show relationship map while agent scans in background ──
|
|
712
|
+
if (interactive && !crmShown) {
|
|
713
|
+
spinner?.start('getting to know you');
|
|
714
|
+
const calCtx = await calendarP;
|
|
715
|
+
if (!crmWarmupP)
|
|
716
|
+
crmWarmupP = buildCRM();
|
|
717
|
+
await showCRM(ANTHROPIC_KEY, {
|
|
718
|
+
calendarContext: calCtx,
|
|
719
|
+
spinner,
|
|
720
|
+
crmPromise: crmWarmupP,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
renderer?.start();
|
|
724
|
+
cardsLive = true;
|
|
725
|
+
// Drain any cards that arrived during CRM display
|
|
726
|
+
for (const { card, resolve } of cardBuffer) {
|
|
727
|
+
resolve(await processCard(card));
|
|
728
|
+
}
|
|
729
|
+
cardBuffer.length = 0;
|
|
730
|
+
const analysis = await agentPromise;
|
|
500
731
|
renderer?.stop();
|
|
501
732
|
renderer?.clear();
|
|
502
733
|
if (jsonMode) {
|
|
@@ -527,12 +758,6 @@ async function main() {
|
|
|
527
758
|
for (const a of analysis.alpha)
|
|
528
759
|
console.log(` ★ ${a}`);
|
|
529
760
|
}
|
|
530
|
-
// Fallback: old format
|
|
531
|
-
for (const d of (analysis.decisions || [])) {
|
|
532
|
-
const rec = d.options?.[0];
|
|
533
|
-
const tag = d.fyi ? '[fyi]' : `[${d.urgency}]`;
|
|
534
|
-
console.log(d.fyi ? ` ${tag} ${d.title}` : ` ${tag} ${d.title} → ${rec?.label || 'no rec'}`);
|
|
535
|
-
}
|
|
536
761
|
saveState(analysis);
|
|
537
762
|
return;
|
|
538
763
|
}
|
|
@@ -547,45 +772,42 @@ async function main() {
|
|
|
547
772
|
// Handled (compact)
|
|
548
773
|
if (analysis.handled?.length)
|
|
549
774
|
renderHandled(analysis.handled, 5);
|
|
775
|
+
const remainingPromises = (analysis.promises || []).filter((d) => !streamedTitles.has(d.title));
|
|
776
|
+
const remainingBlockers = (analysis.blockers || []).filter((d) => !streamedTitles.has(d.title));
|
|
777
|
+
const remainingBumps = (analysis.bumps || []).filter((d) => !streamedTitles.has(d.title));
|
|
778
|
+
const totalCardsForWalk = cardCount
|
|
779
|
+
+ remainingPromises.length
|
|
780
|
+
+ remainingBlockers.length
|
|
781
|
+
+ remainingBumps.length;
|
|
550
782
|
// Helper: walk cards in a section with arrow-key TUI
|
|
551
783
|
const walkCards = async (items, category) => {
|
|
552
|
-
|
|
553
|
-
if (!remaining.length)
|
|
784
|
+
if (!items.length)
|
|
554
785
|
return;
|
|
555
|
-
for (let i = 0; i <
|
|
556
|
-
const d =
|
|
786
|
+
for (let i = 0; i < items.length; i++) {
|
|
787
|
+
const d = items[i];
|
|
557
788
|
const card = { ...d, category };
|
|
558
789
|
cardCount++;
|
|
559
|
-
const pick = await pickCard(card, cardCount);
|
|
790
|
+
const pick = await pickCard(card, cardCount, totalCardsForWalk);
|
|
560
791
|
addTodo(d.title, pick, d.urgency || 'today', false);
|
|
561
792
|
collectedDecisions.push({ title: d.title, picked: pick });
|
|
562
793
|
}
|
|
563
794
|
};
|
|
564
795
|
// Walk the four sections
|
|
565
|
-
if (
|
|
796
|
+
if (remainingPromises.length) {
|
|
566
797
|
renderSectionHeader('PROMISES', RED);
|
|
567
|
-
await walkCards(
|
|
798
|
+
await walkCards(remainingPromises, 'promise');
|
|
568
799
|
}
|
|
569
|
-
if (
|
|
800
|
+
if (remainingBlockers.length) {
|
|
570
801
|
renderSectionHeader('BLOCKED', AMB);
|
|
571
|
-
await walkCards(
|
|
802
|
+
await walkCards(remainingBlockers, 'blocker');
|
|
572
803
|
}
|
|
573
|
-
if (
|
|
804
|
+
if (remainingBumps.length) {
|
|
574
805
|
renderSectionHeader('BUMPS', CYN);
|
|
575
|
-
await walkCards(
|
|
806
|
+
await walkCards(remainingBumps, 'bump');
|
|
576
807
|
}
|
|
577
808
|
if (analysis.alpha?.length) {
|
|
578
809
|
renderSection('ALPHA', analysis.alpha, '★', MAG.bold, MAG);
|
|
579
810
|
}
|
|
580
|
-
// Fallback: old decisions format (for legacy/transition)
|
|
581
|
-
const oldRemaining = (analysis.decisions || []).filter(d => !streamedTitles.has(d.title));
|
|
582
|
-
for (let i = 0; i < oldRemaining.length; i++) {
|
|
583
|
-
const d = oldRemaining[i];
|
|
584
|
-
cardCount++;
|
|
585
|
-
const pick = await pickCard(d, cardCount);
|
|
586
|
-
addTodo(d.title, pick, d.urgency || 'today', d.fyi || pick === 'noted');
|
|
587
|
-
collectedDecisions.push({ title: d.title, picked: pick });
|
|
588
|
-
}
|
|
589
811
|
// Persist handled for delta context
|
|
590
812
|
for (const h of (analysis.handled || [])) {
|
|
591
813
|
addTodo(h, 'handled', 'today', true);
|
|
@@ -594,7 +816,6 @@ async function main() {
|
|
|
594
816
|
...(analysis.promises || []),
|
|
595
817
|
...(analysis.blockers || []),
|
|
596
818
|
...(analysis.bumps || []),
|
|
597
|
-
...(analysis.decisions || []),
|
|
598
819
|
].some((d) => d.options?.length);
|
|
599
820
|
if (!hasActionable && !(analysis.alpha?.length)) {
|
|
600
821
|
console.log(DIM(' All clear — nothing needs you right now.'));
|
|
@@ -605,6 +826,17 @@ async function main() {
|
|
|
605
826
|
saveState(analysis);
|
|
606
827
|
// ── Stay resident: message loop + transport layer ──
|
|
607
828
|
const config = loadConfig();
|
|
829
|
+
// End-of-brief text so the plan follows you out of terminal.
|
|
830
|
+
if (config.briefSmsEnabled !== false) {
|
|
831
|
+
const targetPhone = resolveBriefSmsTarget(config);
|
|
832
|
+
if (targetPhone) {
|
|
833
|
+
const text = buildBriefingText(getUserName(), analysis);
|
|
834
|
+
const sent = await sendBriefingText(targetPhone, text);
|
|
835
|
+
if (!daemonMode && sent) {
|
|
836
|
+
console.log(chalk.dim(` texted your brief to ${targetPhone}`));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
608
840
|
// Checkpoint state before going resident (Anthropic pattern: save before risky ops)
|
|
609
841
|
checkpoint('pre-resident');
|
|
610
842
|
recordSuccess(); // We made it through the briefing — reset crash counter
|
|
@@ -662,15 +894,30 @@ async function main() {
|
|
|
662
894
|
recordCrash(err.message, 'message-loop');
|
|
663
895
|
},
|
|
664
896
|
});
|
|
665
|
-
|
|
666
|
-
console.log(chalk.dim(' listening'));
|
|
667
|
-
console.log();
|
|
668
|
-
}
|
|
669
|
-
// Start health server for daemon mode
|
|
897
|
+
// Start NOX gateway in resident mode (daemon + interactive)
|
|
670
898
|
let gw = null;
|
|
899
|
+
if (ANTHROPIC_KEY) {
|
|
900
|
+
gw = startGateway(ANTHROPIC_KEY);
|
|
901
|
+
}
|
|
902
|
+
let noxEndpoint = null;
|
|
903
|
+
if (hasTailscale()) {
|
|
904
|
+
noxEndpoint = startFunnel(19877);
|
|
905
|
+
if (!noxEndpoint) {
|
|
906
|
+
const host = getTailscaleHostname();
|
|
907
|
+
if (host)
|
|
908
|
+
noxEndpoint = `https://${host}`;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
671
911
|
if (daemonMode) {
|
|
672
912
|
startHealthServer();
|
|
673
|
-
|
|
913
|
+
}
|
|
914
|
+
if (!daemonMode) {
|
|
915
|
+
console.log(chalk.dim(' listening'));
|
|
916
|
+
console.log(chalk.dim(' nox number is live'));
|
|
917
|
+
if (noxEndpoint)
|
|
918
|
+
console.log(chalk.dim(` endpoint ${noxEndpoint}`));
|
|
919
|
+
console.log(chalk.green(' press ctrl+c to end demo'));
|
|
920
|
+
console.log();
|
|
674
921
|
}
|
|
675
922
|
// Generate knowledge bases from CRM on startup
|
|
676
923
|
try {
|
|
@@ -705,79 +952,6 @@ async function main() {
|
|
|
705
952
|
}
|
|
706
953
|
await new Promise(() => { });
|
|
707
954
|
}
|
|
708
|
-
function renderAnalysis(analysis, sourceCount, generated) {
|
|
709
|
-
console.log();
|
|
710
|
-
console.log(chalk.bold(' LIFE PULSE'));
|
|
711
|
-
console.log();
|
|
712
|
-
console.log(` ${analysis.greeting}`);
|
|
713
|
-
console.log();
|
|
714
|
-
if (analysis.decisions?.length) {
|
|
715
|
-
console.log(chalk.bold.red(' DECISIONS'));
|
|
716
|
-
console.log();
|
|
717
|
-
for (const d of analysis.decisions) {
|
|
718
|
-
const tag = d.fyi ? chalk.dim('[fyi]') : `[${d.urgency}]`;
|
|
719
|
-
console.log(` ${d.title} ${tag}`);
|
|
720
|
-
if (d.options?.length) {
|
|
721
|
-
for (const o of d.options)
|
|
722
|
-
console.log(` · ${o.label} ${chalk.dim('— ' + (o.description || ''))}`);
|
|
723
|
-
}
|
|
724
|
-
console.log();
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
if (analysis.upcoming?.length) {
|
|
728
|
-
console.log(chalk.bold.cyan(' UPCOMING'));
|
|
729
|
-
console.log();
|
|
730
|
-
renderList(analysis.upcoming, chalk.cyan('▸'));
|
|
731
|
-
console.log();
|
|
732
|
-
}
|
|
733
|
-
if (analysis.handled?.length) {
|
|
734
|
-
console.log(chalk.bold.green(' HANDLED'));
|
|
735
|
-
console.log();
|
|
736
|
-
renderList(analysis.handled, chalk.green('✓'));
|
|
737
|
-
console.log();
|
|
738
|
-
}
|
|
739
|
-
if (analysis.intel?.length) {
|
|
740
|
-
console.log(chalk.bold.magenta(' INTEL'));
|
|
741
|
-
console.log();
|
|
742
|
-
renderList(analysis.intel, chalk.magenta('~'));
|
|
743
|
-
console.log();
|
|
744
|
-
}
|
|
745
|
-
// Legacy format fallback
|
|
746
|
-
if (analysis.right_now?.length) {
|
|
747
|
-
console.log(chalk.bold.red(' RIGHT NOW'));
|
|
748
|
-
console.log();
|
|
749
|
-
renderList(analysis.right_now, chalk.red('→'));
|
|
750
|
-
console.log();
|
|
751
|
-
}
|
|
752
|
-
if (analysis.today?.length) {
|
|
753
|
-
console.log(chalk.bold.white(' TODAY'));
|
|
754
|
-
console.log();
|
|
755
|
-
renderList(analysis.today, chalk.dim('·'));
|
|
756
|
-
console.log();
|
|
757
|
-
}
|
|
758
|
-
if (analysis.this_week?.length) {
|
|
759
|
-
console.log(chalk.bold(' THIS WEEK'));
|
|
760
|
-
console.log();
|
|
761
|
-
renderList(analysis.this_week, chalk.dim('·'));
|
|
762
|
-
console.log();
|
|
763
|
-
}
|
|
764
|
-
if (analysis.heads_up?.length) {
|
|
765
|
-
console.log(chalk.bold.cyan(' HEADS UP'));
|
|
766
|
-
console.log();
|
|
767
|
-
renderList(analysis.heads_up, chalk.cyan('!'));
|
|
768
|
-
console.log();
|
|
769
|
-
}
|
|
770
|
-
if (analysis.noticed?.length) {
|
|
771
|
-
console.log(chalk.bold.magenta(' NOTICED'));
|
|
772
|
-
console.log();
|
|
773
|
-
renderList(analysis.noticed, chalk.magenta('~'));
|
|
774
|
-
console.log();
|
|
775
|
-
}
|
|
776
|
-
if (sourceCount) {
|
|
777
|
-
console.log(chalk.dim(` ${sourceCount} sources · ${generated}`));
|
|
778
|
-
console.log();
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
955
|
main().catch(e => {
|
|
782
956
|
console.error(chalk.red('\n Error:'), e.message);
|
|
783
957
|
process.exit(1);
|