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/installer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* White-Glove Installer — orchestrated setup for new Mac
|
|
2
|
+
* White-Glove Installer — orchestrated setup for new user Mac installations.
|
|
3
3
|
*
|
|
4
4
|
* 8 steps, ~15 minutes sitting beside the client:
|
|
5
5
|
* 1. Pre-flight (dirs, API key)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. Discovery (iCloud, Screen Time, apps, brew)
|
|
8
8
|
* 4. CRM Build (iMessage relationship map)
|
|
9
9
|
* 5. Archetype (Sonnet psychographic profile)
|
|
10
|
-
* 6.
|
|
10
|
+
* 6. NOX Link (Tailscale endpoint for inbound NOX calls)
|
|
11
11
|
* 7. Daemon Install (launchd plists)
|
|
12
12
|
* 8. First Briefing (live demo)
|
|
13
13
|
*
|
package/dist/installer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* White-Glove Installer — orchestrated setup for new Mac
|
|
2
|
+
* White-Glove Installer — orchestrated setup for new user Mac installations.
|
|
3
3
|
*
|
|
4
4
|
* 8 steps, ~15 minutes sitting beside the client:
|
|
5
5
|
* 1. Pre-flight (dirs, API key)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. Discovery (iCloud, Screen Time, apps, brew)
|
|
8
8
|
* 4. CRM Build (iMessage relationship map)
|
|
9
9
|
* 5. Archetype (Sonnet psychographic profile)
|
|
10
|
-
* 6.
|
|
10
|
+
* 6. NOX Link (Tailscale endpoint for inbound NOX calls)
|
|
11
11
|
* 7. Daemon Install (launchd plists)
|
|
12
12
|
* 8. First Briefing (live demo)
|
|
13
13
|
*
|
|
@@ -28,9 +28,11 @@ import { updateAutoKnowledge } from './knowledge.js';
|
|
|
28
28
|
import { scanForSkills } from './skill-loader.js';
|
|
29
29
|
import { hasTailscale, getHostname as getTailscaleHostname, startFunnel } from './tunnel.js';
|
|
30
30
|
import { startGateway } from './sms-gateway.js';
|
|
31
|
-
import { renderCRMList, renderBrief, Spinner, DIM, HD, MID } from './tui.js';
|
|
31
|
+
import { renderCRMList, renderBrief, pickCard, renderSectionHeader, renderSection, renderHandled, Spinner, DIM, HD, MID } from './tui.js';
|
|
32
32
|
import { saveContactSummaries } from './intelligence.js';
|
|
33
33
|
import { startSession } from './session-progress.js';
|
|
34
|
+
import { runAgent } from './agent.js';
|
|
35
|
+
import { ProgressRenderer } from './progress.js';
|
|
34
36
|
import * as rply from './rply-client.js';
|
|
35
37
|
const BASE = join(homedir(), '.life-pulse');
|
|
36
38
|
const STATE_PATH = join(BASE, 'install-state.json');
|
|
@@ -86,6 +88,52 @@ const PLATFORM_HINTS = {
|
|
|
86
88
|
function platformHint(platform) {
|
|
87
89
|
return PLATFORM_HINTS[platform] || '';
|
|
88
90
|
}
|
|
91
|
+
// ─── Tailscale Auto-Install ─────────────────────────────────────
|
|
92
|
+
function hasBrew() {
|
|
93
|
+
try {
|
|
94
|
+
execSync('which brew', { stdio: 'pipe', timeout: 5000 });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function installTailscale(spinner) {
|
|
102
|
+
if (hasBrew()) {
|
|
103
|
+
spinner?.start('installing tailscale via homebrew');
|
|
104
|
+
try {
|
|
105
|
+
execSync('brew install --cask tailscale', { stdio: 'pipe', timeout: 120_000 });
|
|
106
|
+
spinner?.stop();
|
|
107
|
+
// Launch the app so the CLI helper is available
|
|
108
|
+
try {
|
|
109
|
+
execSync('open -a Tailscale', { stdio: 'pipe', timeout: 10_000 });
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
// Give it a moment to register the CLI
|
|
113
|
+
execSync('sleep 2');
|
|
114
|
+
if (hasTailscale()) {
|
|
115
|
+
console.log(DIM(' tailscale installed via homebrew'));
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
spinner?.stop();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Fallback: open Mac App Store page for Tailscale
|
|
124
|
+
spinner?.start('opening tailscale download');
|
|
125
|
+
try {
|
|
126
|
+
execSync('open "https://apps.apple.com/app/tailscale/id1475387142"', { stdio: 'pipe', timeout: 5000 });
|
|
127
|
+
spinner?.stop();
|
|
128
|
+
console.log(DIM(' opened Tailscale in App Store'));
|
|
129
|
+
console.log(DIM(' install it, then run setup again'));
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
spinner?.stop();
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
89
137
|
// ─── Main Installer Flow ────────────────────────────────────────
|
|
90
138
|
export async function runInstaller(apiKey) {
|
|
91
139
|
const state = loadState();
|
|
@@ -228,6 +276,25 @@ export async function runInstaller(apiKey) {
|
|
|
228
276
|
}
|
|
229
277
|
markDone(state, 'crm');
|
|
230
278
|
}
|
|
279
|
+
// ── Background: kick off agent scan while steps 5-7 run ──
|
|
280
|
+
let agentPromise = null;
|
|
281
|
+
let agentRenderer = null;
|
|
282
|
+
const cardBuffer = [];
|
|
283
|
+
let cardsLive = false;
|
|
284
|
+
let bgCardCount = 0;
|
|
285
|
+
if (apiKey && process.stdin.isTTY && !isDone(state, 'briefing')) {
|
|
286
|
+
startSession();
|
|
287
|
+
agentRenderer = new ProgressRenderer();
|
|
288
|
+
agentPromise = runAgent(apiKey, agentRenderer, async (card) => {
|
|
289
|
+
if (cardsLive) {
|
|
290
|
+
bgCardCount++;
|
|
291
|
+
return await pickCard(card, bgCardCount);
|
|
292
|
+
}
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
cardBuffer.push({ card, resolve });
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
231
298
|
// ── Step 5: Archetype ──
|
|
232
299
|
stepHeader(5, 'Archetype', isDone(state, 'archetype'));
|
|
233
300
|
if (!isDone(state, 'archetype') && apiKey) {
|
|
@@ -248,26 +315,38 @@ export async function runInstaller(apiKey) {
|
|
|
248
315
|
}
|
|
249
316
|
markDone(state, 'archetype');
|
|
250
317
|
}
|
|
251
|
-
// ── Step 6:
|
|
252
|
-
stepHeader(6, '
|
|
318
|
+
// ── Step 6: NOX Link ──
|
|
319
|
+
stepHeader(6, 'NOX Link', isDone(state, 'phone'));
|
|
253
320
|
if (!isDone(state, 'phone')) {
|
|
254
321
|
if (apiKey) {
|
|
255
322
|
const gw = startGateway(apiKey);
|
|
256
323
|
setTimeout(() => gw.stop(), 60_000);
|
|
257
324
|
}
|
|
325
|
+
// Auto-install Tailscale if missing
|
|
326
|
+
if (!hasTailscale()) {
|
|
327
|
+
console.log(DIM(' tailscale not found — installing'));
|
|
328
|
+
const installed = await installTailscale(spinner);
|
|
329
|
+
if (!installed) {
|
|
330
|
+
console.log(HD(' couldn\'t install tailscale automatically'));
|
|
331
|
+
console.log(DIM(' install manually: tailscale.com/download'));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
258
334
|
if (hasTailscale()) {
|
|
259
335
|
const hostname = getTailscaleHostname();
|
|
260
336
|
if (hostname) {
|
|
261
337
|
state.funnelUrl = `http://${hostname}:19877`;
|
|
262
|
-
spinner?.start('
|
|
338
|
+
spinner?.start('connecting nox');
|
|
263
339
|
const funnelUrl = startFunnel(19877);
|
|
264
340
|
spinner?.stop();
|
|
265
341
|
if (funnelUrl)
|
|
266
342
|
state.funnelUrl = funnelUrl;
|
|
267
|
-
console.log(DIM(' connected'));
|
|
343
|
+
console.log(DIM(' nox link connected'));
|
|
344
|
+
if (state.funnelUrl)
|
|
345
|
+
console.log(DIM(` ${state.funnelUrl}`));
|
|
268
346
|
}
|
|
269
347
|
else {
|
|
270
|
-
console.log(HD('
|
|
348
|
+
console.log(HD(' tailscale installed but not logged in'));
|
|
349
|
+
console.log(DIM(' run: tailscale login'));
|
|
271
350
|
}
|
|
272
351
|
}
|
|
273
352
|
else {
|
|
@@ -363,21 +442,26 @@ export async function runInstaller(apiKey) {
|
|
|
363
442
|
console.log();
|
|
364
443
|
console.log(DIM(' let\'s see what\'s going on...'));
|
|
365
444
|
console.log();
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
|
|
445
|
+
if (!agentPromise) {
|
|
446
|
+
startSession();
|
|
447
|
+
agentRenderer = new ProgressRenderer();
|
|
448
|
+
bgCardCount = 0;
|
|
449
|
+
agentPromise = runAgent(apiKey, agentRenderer, async (card) => {
|
|
450
|
+
bgCardCount++;
|
|
451
|
+
return await pickCard(card, bgCardCount);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
agentRenderer.start();
|
|
455
|
+
cardsLive = true;
|
|
456
|
+
// Drain any cards that arrived during steps 5-7
|
|
457
|
+
for (const { card, resolve } of cardBuffer) {
|
|
458
|
+
bgCardCount++;
|
|
459
|
+
resolve(await pickCard(card, bgCardCount));
|
|
460
|
+
}
|
|
461
|
+
cardBuffer.length = 0;
|
|
462
|
+
const analysis = await agentPromise;
|
|
463
|
+
agentRenderer.stop();
|
|
464
|
+
agentRenderer.clear();
|
|
381
465
|
// Render results
|
|
382
466
|
if (analysis.greeting) {
|
|
383
467
|
console.log(` ${MID(analysis.greeting)}`);
|
|
@@ -386,10 +470,10 @@ export async function runInstaller(apiKey) {
|
|
|
386
470
|
if (analysis.handled?.length)
|
|
387
471
|
renderHandled(analysis.handled, 5);
|
|
388
472
|
if (analysis.promises?.length) {
|
|
389
|
-
renderSectionHeader('promises',
|
|
473
|
+
renderSectionHeader('promises', HD);
|
|
390
474
|
}
|
|
391
475
|
if (analysis.alpha?.length) {
|
|
392
|
-
renderSection('alpha', analysis.alpha, '·',
|
|
476
|
+
renderSection('alpha', analysis.alpha, '·', HD, MID);
|
|
393
477
|
}
|
|
394
478
|
markDone(state, 'briefing');
|
|
395
479
|
}
|
|
@@ -405,6 +489,9 @@ export async function runInstaller(apiKey) {
|
|
|
405
489
|
console.log(DIM(` ${active.map(p => p.display_name).join(', ')}`));
|
|
406
490
|
}
|
|
407
491
|
}
|
|
492
|
+
if (state.funnelUrl) {
|
|
493
|
+
console.log(DIM(` nox endpoint: ${state.funnelUrl}`));
|
|
494
|
+
}
|
|
408
495
|
console.log(DIM(' running in the background'));
|
|
409
496
|
console.log(DIM(' you\'ll hear from me at 7:30 AM'));
|
|
410
497
|
console.log();
|
package/dist/profile.d.ts
CHANGED
|
@@ -12,8 +12,14 @@ export interface UserProfile {
|
|
|
12
12
|
topContacts: ContactTier[];
|
|
13
13
|
projects: string[];
|
|
14
14
|
platformSummary: string;
|
|
15
|
+
personalSummary?: string;
|
|
15
16
|
}
|
|
16
17
|
/** Get the real name of the current macOS user */
|
|
17
18
|
export declare function getUserName(): string;
|
|
18
19
|
/** Build full user profile from system data. Cached for session. */
|
|
19
20
|
export declare function buildProfile(): UserProfile;
|
|
21
|
+
/**
|
|
22
|
+
* Rich personal profile summary from outbound messages.
|
|
23
|
+
* Mirrors the "highlight reel" idea: personal traits, interests, and recurring patterns.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildPersonalSummary(): Promise<string>;
|
package/dist/profile.js
CHANGED
|
@@ -9,10 +9,30 @@ import { existsSync, readdirSync } from 'fs';
|
|
|
9
9
|
import { openDb, safeQuery } from './db.js';
|
|
10
10
|
import { resolveName } from './contacts.js';
|
|
11
11
|
import { loadPlatforms, buildPlatformSummary } from './platforms.js';
|
|
12
|
+
import * as rply from './rply-client.js';
|
|
12
13
|
import dayjs from 'dayjs';
|
|
13
14
|
import { APPLE_EPOCH } from './types.js';
|
|
14
15
|
let _name = null;
|
|
15
16
|
let _profile = null;
|
|
17
|
+
let _personalSummaryPromise = null;
|
|
18
|
+
const SELF_SUMMARY_MODEL = 'claude-sonnet-4-5-20250929';
|
|
19
|
+
const SELF_SUMMARY_DAYS = 7;
|
|
20
|
+
const SELF_SUMMARY_LIMIT = 1000;
|
|
21
|
+
const SELF_SUMMARY_MAX_CHARS = 120000;
|
|
22
|
+
const ATTRIBUTED_BODY_NOISE = new Set([
|
|
23
|
+
'streamtyped',
|
|
24
|
+
'NSAttributedString',
|
|
25
|
+
'NSObject',
|
|
26
|
+
'NSString',
|
|
27
|
+
'NSDictionary',
|
|
28
|
+
'NSNumber',
|
|
29
|
+
'NSArray',
|
|
30
|
+
'NSColor',
|
|
31
|
+
'NSFont',
|
|
32
|
+
'NSParagraphStyle',
|
|
33
|
+
'NSOriginalFont',
|
|
34
|
+
]);
|
|
35
|
+
const TAPBACK_RE = /^(Loved|Liked|Laughed at|Disliked|Emphasized|Questioned) (".*"|an? .+)$/i;
|
|
16
36
|
/** Get the real name of the current macOS user */
|
|
17
37
|
export function getUserName() {
|
|
18
38
|
if (_name)
|
|
@@ -24,7 +44,7 @@ export function getUserName() {
|
|
|
24
44
|
// Output format: "RealName:\n First Last"
|
|
25
45
|
const lines = raw.trim().split('\n');
|
|
26
46
|
let name = lines.length > 1 ? lines[1].trim() : lines[0].replace('RealName:', '').trim();
|
|
27
|
-
// Split CamelCase names (e.g. "
|
|
47
|
+
// Split CamelCase names (e.g. "AlexDoe" -> "Alex Doe")
|
|
28
48
|
if (name && !/\s/.test(name) && /[a-z][A-Z]/.test(name)) {
|
|
29
49
|
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
30
50
|
}
|
|
@@ -49,6 +69,58 @@ export function buildProfile() {
|
|
|
49
69
|
_profile = { name, topContacts, projects, platformSummary };
|
|
50
70
|
return _profile;
|
|
51
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Rich personal profile summary from outbound messages.
|
|
74
|
+
* Mirrors the "highlight reel" idea: personal traits, interests, and recurring patterns.
|
|
75
|
+
*/
|
|
76
|
+
export async function buildPersonalSummary() {
|
|
77
|
+
if (_profile?.personalSummary)
|
|
78
|
+
return _profile.personalSummary;
|
|
79
|
+
if (_personalSummaryPromise)
|
|
80
|
+
return _personalSummaryPromise;
|
|
81
|
+
_personalSummaryPromise = (async () => {
|
|
82
|
+
const key = process.env.ANTHROPIC_API_KEY || '';
|
|
83
|
+
if (!key)
|
|
84
|
+
return '';
|
|
85
|
+
const messages = await fetchRecentSentMessageBodies(SELF_SUMMARY_LIMIT, SELF_SUMMARY_DAYS);
|
|
86
|
+
if (messages.length < 25)
|
|
87
|
+
return '';
|
|
88
|
+
const transcript = messages.join('\n').slice(0, SELF_SUMMARY_MAX_CHARS);
|
|
89
|
+
const user = getUserName();
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'x-api-key': key,
|
|
95
|
+
'anthropic-version': '2023-06-01',
|
|
96
|
+
'content-type': 'application/json',
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
model: SELF_SUMMARY_MODEL,
|
|
100
|
+
temperature: 0.2,
|
|
101
|
+
max_tokens: 900,
|
|
102
|
+
messages: [{
|
|
103
|
+
role: 'user',
|
|
104
|
+
content: buildSelfSummaryPrompt(transcript, user),
|
|
105
|
+
}],
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
return '';
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
const summary = data.content?.[0]?.text?.trim() || '';
|
|
112
|
+
if (!summary)
|
|
113
|
+
return '';
|
|
114
|
+
if (_profile)
|
|
115
|
+
_profile.personalSummary = summary;
|
|
116
|
+
return summary;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
return _personalSummaryPromise;
|
|
123
|
+
}
|
|
52
124
|
function discoverContacts() {
|
|
53
125
|
const home = homedir();
|
|
54
126
|
const db = openDb(join(home, 'Library/Messages/chat.db'));
|
|
@@ -93,3 +165,160 @@ function discoverProjects() {
|
|
|
93
165
|
return [];
|
|
94
166
|
}
|
|
95
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Pull outbound messages (sent by the user) with iMessage's attributedBody fallback.
|
|
170
|
+
* This mirrors the SQL pattern used in the native client.
|
|
171
|
+
*/
|
|
172
|
+
async function fetchRecentSentMessageBodies(limit, inPastDays) {
|
|
173
|
+
// ── RPLY path (preferred) ──
|
|
174
|
+
if (await rply.isAvailable()) {
|
|
175
|
+
const cutoff = dayjs().subtract(inPastDays, 'day');
|
|
176
|
+
const convos = await rply.listConversations({ limit: 80 });
|
|
177
|
+
if (convos?.data?.length) {
|
|
178
|
+
const out = [];
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const c of convos.data) {
|
|
181
|
+
const msgs = await rply.listMessages(c.id, { limit: 50 });
|
|
182
|
+
if (!msgs?.data?.length)
|
|
183
|
+
continue;
|
|
184
|
+
for (const m of msgs.data) {
|
|
185
|
+
if (m.direction !== 'sent')
|
|
186
|
+
continue;
|
|
187
|
+
if (!m.content?.text || m.content.type !== 'text')
|
|
188
|
+
continue;
|
|
189
|
+
if (dayjs(m.date).isBefore(cutoff))
|
|
190
|
+
continue;
|
|
191
|
+
const normalized = normalizeSentText(m.content.text);
|
|
192
|
+
if (!normalized)
|
|
193
|
+
continue;
|
|
194
|
+
const clipped = normalized.slice(0, 500);
|
|
195
|
+
if (seen.has(clipped))
|
|
196
|
+
continue;
|
|
197
|
+
seen.add(clipped);
|
|
198
|
+
out.push(clipped);
|
|
199
|
+
if (out.length >= limit)
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (out.length > 0)
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ── SQLite fallback ──
|
|
208
|
+
const db = openDb(join(homedir(), 'Library/Messages/chat.db'));
|
|
209
|
+
if (!db)
|
|
210
|
+
return [];
|
|
211
|
+
const cutoff = (BigInt(dayjs().subtract(inPastDays, 'day').unix() - APPLE_EPOCH) * BigInt(1e9)).toString();
|
|
212
|
+
const rows = safeQuery(db, `
|
|
213
|
+
SELECT
|
|
214
|
+
m.text AS message_text,
|
|
215
|
+
m.attributedBody AS message_attributed_body
|
|
216
|
+
FROM message AS m
|
|
217
|
+
JOIN chat_message_join AS cmj ON m.ROWID = cmj.message_id
|
|
218
|
+
JOIN chat AS c ON cmj.chat_id = c.ROWID
|
|
219
|
+
WHERE m.date >= ?
|
|
220
|
+
AND m.is_from_me = 1
|
|
221
|
+
AND m.associated_message_type = 0
|
|
222
|
+
AND c.chat_identifier NOT LIKE 'urn:biz:%'
|
|
223
|
+
ORDER BY m.date DESC
|
|
224
|
+
LIMIT ?
|
|
225
|
+
`, [cutoff, limit]);
|
|
226
|
+
db.close();
|
|
227
|
+
const out = [];
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
for (const row of rows) {
|
|
230
|
+
const raw = row.message_text || decodeAttributedText(row.message_attributed_body);
|
|
231
|
+
const normalized = normalizeSentText(raw);
|
|
232
|
+
if (!normalized)
|
|
233
|
+
continue;
|
|
234
|
+
const clipped = normalized.slice(0, 500);
|
|
235
|
+
if (seen.has(clipped))
|
|
236
|
+
continue;
|
|
237
|
+
seen.add(clipped);
|
|
238
|
+
out.push(clipped);
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
function decodeAttributedText(blob) {
|
|
243
|
+
if (!blob?.length)
|
|
244
|
+
return '';
|
|
245
|
+
// iMessage attributedBody is NSKeyedArchiver data; pull printable runs as fallback text.
|
|
246
|
+
const utf = blob.toString('utf8');
|
|
247
|
+
const runs = utf.match(/[\x20-\x7E]{3,}/g) || [];
|
|
248
|
+
if (!runs.length)
|
|
249
|
+
return '';
|
|
250
|
+
const candidates = runs
|
|
251
|
+
.map(s => s.trim())
|
|
252
|
+
.filter(s => s.length >= 2 && s.length <= 800)
|
|
253
|
+
.filter(s => !ATTRIBUTED_BODY_NOISE.has(s))
|
|
254
|
+
.filter(s => !/^NS[A-Za-z]+$/.test(s))
|
|
255
|
+
.filter(s => !/^[\[\]{}()<>]+$/.test(s));
|
|
256
|
+
if (!candidates.length)
|
|
257
|
+
return '';
|
|
258
|
+
candidates.sort((a, b) => scoreCandidate(b) - scoreCandidate(a));
|
|
259
|
+
return candidates[0];
|
|
260
|
+
}
|
|
261
|
+
function scoreCandidate(text) {
|
|
262
|
+
let score = text.length;
|
|
263
|
+
if (text.includes(' '))
|
|
264
|
+
score += 20;
|
|
265
|
+
if (/[A-Za-z]/.test(text))
|
|
266
|
+
score += 10;
|
|
267
|
+
if (/streamtyped|NSDictionary|NSAttributed|NSFont/i.test(text))
|
|
268
|
+
score -= 200;
|
|
269
|
+
if (/^[a-f0-9]{8,}$/i.test(text))
|
|
270
|
+
score -= 80;
|
|
271
|
+
if (/^\[cc:[a-f0-9]{8}\]/i.test(text))
|
|
272
|
+
score -= 40;
|
|
273
|
+
return score;
|
|
274
|
+
}
|
|
275
|
+
function normalizeSentText(raw) {
|
|
276
|
+
let text = raw || '';
|
|
277
|
+
if (!text)
|
|
278
|
+
return '';
|
|
279
|
+
text = text
|
|
280
|
+
.replace(/\uFFFC/g, ' ')
|
|
281
|
+
.replace(/\[cc:[a-f0-9]{8}\]\s*/ig, '')
|
|
282
|
+
.replace(/^[+>"'#*]+\s*/, '')
|
|
283
|
+
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
284
|
+
.replace(/\s+/g, ' ')
|
|
285
|
+
.trim();
|
|
286
|
+
if (!text)
|
|
287
|
+
return '';
|
|
288
|
+
if (TAPBACK_RE.test(text))
|
|
289
|
+
return '';
|
|
290
|
+
return text;
|
|
291
|
+
}
|
|
292
|
+
function buildSelfSummaryPrompt(transcript, user) {
|
|
293
|
+
return `You are an expert at detecting personal patterns and characteristics from chat transcripts.
|
|
294
|
+
|
|
295
|
+
TASK — Write a rich "highlight reel" for **${user}**, focusing on their personal characteristics,
|
|
296
|
+
interests, and behaviors as revealed through their own outbound messages.
|
|
297
|
+
|
|
298
|
+
This transcript contains messages sent BY the user. Infer patterns from what they choose to say.
|
|
299
|
+
|
|
300
|
+
Identify: Places · Projects · Hobbies · Values · Recurring topics · Personality traits · Decision style
|
|
301
|
+
|
|
302
|
+
FORMAT (markdown only):
|
|
303
|
+
1) First block: a high-signal user snapshot in 4-6 sentences (dense, specific, proper nouns where available).
|
|
304
|
+
2) Then 8-12 bullets in this format:
|
|
305
|
+
<Characteristic> → <Pattern> (<helpful context grounded in message evidence>)
|
|
306
|
+
|
|
307
|
+
RULES:
|
|
308
|
+
- Speak directly to the reader as "you".
|
|
309
|
+
- Focus on PERSONAL characteristics, not relationship analysis.
|
|
310
|
+
- No corporate tone, no fluff, no invented facts.
|
|
311
|
+
- If evidence is weak, skip it.
|
|
312
|
+
- High entity density, concise writing.
|
|
313
|
+
- Never use refusal/apology boilerplate.
|
|
314
|
+
|
|
315
|
+
Voice: write like a sharp friend who knows them well and helps them see their own patterns.
|
|
316
|
+
Aim for "Huh, that's exactly me."
|
|
317
|
+
|
|
318
|
+
Conversation history:
|
|
319
|
+
<transcript>
|
|
320
|
+
${transcript}
|
|
321
|
+
</transcript>
|
|
322
|
+
|
|
323
|
+
Output only the markdown summary.`;
|
|
324
|
+
}
|
package/dist/progress.d.ts
CHANGED
package/dist/progress.js
CHANGED
|
@@ -11,30 +11,51 @@ 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: '
|
|
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: '
|
|
29
|
-
get_claude_history: '
|
|
30
|
-
get_chatgpt_history: '
|
|
31
|
-
get_interests_for_plans: '
|
|
32
|
-
get_calendar: '
|
|
33
|
-
get_reminders: '
|
|
34
|
-
get_notifications: '
|
|
35
|
-
discover_platforms: '
|
|
36
|
-
generate_archetype: '
|
|
37
|
-
search_emails: '
|
|
14
|
+
search_all_messages: 'messages: keyword matches',
|
|
15
|
+
get_conversation: 'messages: thread context',
|
|
16
|
+
profile_contact: 'contact profile',
|
|
17
|
+
get_messages: 'messages: recent threads',
|
|
18
|
+
get_unanswered_messages: 'messages: waiting on reply',
|
|
19
|
+
get_screen_time: 'screen time',
|
|
20
|
+
get_browsing: 'browser history',
|
|
21
|
+
get_calls: 'call history',
|
|
22
|
+
scan_sources: 'source availability',
|
|
23
|
+
lookup_contact: 'contact lookup',
|
|
24
|
+
get_email_summary: 'mail summary',
|
|
25
|
+
get_git_activity: 'git activity',
|
|
26
|
+
get_recent_files: 'recent files',
|
|
27
|
+
get_shell_history: 'shell history',
|
|
28
|
+
get_notes: 'notes',
|
|
29
|
+
get_claude_history: 'claude chats',
|
|
30
|
+
get_chatgpt_history: 'chatgpt chats',
|
|
31
|
+
get_interests_for_plans: 'planning signals',
|
|
32
|
+
get_calendar: 'calendar (next 7d)',
|
|
33
|
+
get_reminders: 'reminders',
|
|
34
|
+
get_notifications: 'notifications',
|
|
35
|
+
discover_platforms: 'platform detection',
|
|
36
|
+
generate_archetype: 'profile synthesis',
|
|
37
|
+
search_emails: 'mail search',
|
|
38
|
+
};
|
|
39
|
+
const TOOL_SOURCE = {
|
|
40
|
+
scan_sources: 'source index',
|
|
41
|
+
get_messages: 'messages',
|
|
42
|
+
get_unanswered_messages: 'messages',
|
|
43
|
+
get_conversation: 'messages',
|
|
44
|
+
search_all_messages: 'messages',
|
|
45
|
+
get_calendar: 'calendar',
|
|
46
|
+
get_calls: 'calls',
|
|
47
|
+
get_email_summary: 'mail',
|
|
48
|
+
search_emails: 'mail',
|
|
49
|
+
get_claude_history: 'claude',
|
|
50
|
+
get_chatgpt_history: 'chatgpt',
|
|
51
|
+
get_notes: 'notes',
|
|
52
|
+
get_recent_files: 'files',
|
|
53
|
+
get_shell_history: 'shell',
|
|
54
|
+
get_git_activity: 'git',
|
|
55
|
+
get_screen_time: 'screen time',
|
|
56
|
+
get_browsing: 'browser',
|
|
57
|
+
get_notifications: 'notifications',
|
|
58
|
+
get_reminders: 'reminders',
|
|
38
59
|
};
|
|
39
60
|
const MAX_VISIBLE = 4;
|
|
40
61
|
export class ProgressRenderer {
|
|
@@ -114,6 +135,15 @@ export class ProgressRenderer {
|
|
|
114
135
|
}
|
|
115
136
|
s(name) { return LABEL[name] || name; }
|
|
116
137
|
ms(t) { return (t / 1000).toFixed(1) + 's'; }
|
|
138
|
+
phaseForTools(active) {
|
|
139
|
+
const sources = [...new Set(active.map(t => TOOL_SOURCE[t.name] || 'other'))];
|
|
140
|
+
if (!sources.length)
|
|
141
|
+
return 'scanning sources';
|
|
142
|
+
const lead = sources.slice(0, 2).join(' + ');
|
|
143
|
+
return sources.length > 2
|
|
144
|
+
? `scanning ${lead} +${sources.length - 2}`
|
|
145
|
+
: `scanning ${lead}`;
|
|
146
|
+
}
|
|
117
147
|
spin(offset = 0) {
|
|
118
148
|
return ACCENT(FRAMES[(this.frame + offset) % FRAMES.length]);
|
|
119
149
|
}
|
|
@@ -128,11 +158,12 @@ export class ProgressRenderer {
|
|
|
128
158
|
const done = this.tools.filter(t => t.done).length;
|
|
129
159
|
const total = this.tools.length;
|
|
130
160
|
if (done === total) {
|
|
131
|
-
lines.push(` ${OK('✓')} ${chalk.dim('
|
|
161
|
+
lines.push(` ${OK('✓')} ${chalk.dim('scan complete')}`);
|
|
132
162
|
}
|
|
133
163
|
else {
|
|
134
|
-
lines.push(` ${chalk.dim('scanning')} ${this.bar(done, total)} ${chalk.white(String(done))}${chalk.dim('/')}${chalk.white(String(total))}`);
|
|
135
164
|
const active = this.tools.filter(t => !t.done);
|
|
165
|
+
const phase = this.phaseForTools(active);
|
|
166
|
+
lines.push(` ${chalk.dim(phase)} ${this.bar(done, total)} ${chalk.white(String(done))}${chalk.dim('/')}${chalk.white(String(total))}`);
|
|
136
167
|
const show = active.slice(0, MAX_VISIBLE);
|
|
137
168
|
for (let i = 0; i < show.length; i++) {
|
|
138
169
|
lines.push(` ${this.spin(i)} ${chalk.white(this.s(show[i].name))}`);
|
|
@@ -149,7 +180,8 @@ export class ProgressRenderer {
|
|
|
149
180
|
const total = this.workers.length;
|
|
150
181
|
const active = this.workers.filter(w => !w.done);
|
|
151
182
|
if (active.length > 0) {
|
|
152
|
-
|
|
183
|
+
const phase = `investigating ${active.length} item${active.length === 1 ? '' : 's'}`;
|
|
184
|
+
lines.push(` ${chalk.dim(phase)} ${this.bar(done, total)} ${chalk.white(String(done))}${chalk.dim('/')}${chalk.white(String(total))}`);
|
|
153
185
|
const show = active.slice(0, MAX_VISIBLE);
|
|
154
186
|
for (let i = 0; i < show.length; i++) {
|
|
155
187
|
const w = show[i];
|
|
@@ -161,16 +193,20 @@ export class ProgressRenderer {
|
|
|
161
193
|
}
|
|
162
194
|
}
|
|
163
195
|
else {
|
|
164
|
-
lines.push(` ${
|
|
196
|
+
lines.push(` ${this.spin()} ${chalk.white('writing briefing')}`);
|
|
197
|
+
const doneWorkers = this.workers.filter(w => w.done && !w.failed).slice(-MAX_VISIBLE);
|
|
198
|
+
for (const w of doneWorkers) {
|
|
199
|
+
lines.push(` ${chalk.dim('·')} ${chalk.dim(w.label)}`);
|
|
200
|
+
}
|
|
165
201
|
}
|
|
166
202
|
lines.push('');
|
|
167
203
|
}
|
|
168
204
|
if (this.auqWaiting) {
|
|
169
|
-
lines.push(` ${this.spin()} ${chalk.white('
|
|
205
|
+
lines.push(` ${this.spin()} ${chalk.white('awaiting your pick')}`);
|
|
170
206
|
lines.push('');
|
|
171
207
|
}
|
|
172
|
-
if (this.isThinking) {
|
|
173
|
-
lines.push(` ${this.spin()} ${chalk.white('thinking
|
|
208
|
+
if (this.isThinking && !this.workers.length) {
|
|
209
|
+
lines.push(` ${this.spin()} ${chalk.white('thinking')}`);
|
|
174
210
|
lines.push('');
|
|
175
211
|
}
|
|
176
212
|
// Clear previous render
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local identity + memory injection (OpenClaw-style).
|
|
3
|
+
*
|
|
4
|
+
* Priority order:
|
|
5
|
+
* 1) Explicit env path
|
|
6
|
+
* 2) Current working directory
|
|
7
|
+
* 3) ~/.life-pulse
|
|
8
|
+
* 4) ~/.config/life-pulse
|
|
9
|
+
*/
|
|
10
|
+
export interface PromptLayers {
|
|
11
|
+
soul: string;
|
|
12
|
+
memory: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadPromptLayers(): PromptLayers;
|
|
15
|
+
export declare function buildPromptLayersSection(): string;
|
|
16
|
+
/** First-run bootstrap: create default SOUL.md + MEMORY.md if missing. */
|
|
17
|
+
export declare function ensurePromptLayerFiles(): string[];
|