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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * White-Glove Installer — orchestrated setup for new Mac Mini installations.
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. Phone Number Setup (Twilio + Tailscale Funnel)
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 Mini installations.
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. Phone Number Setup (Twilio + Tailscale Funnel)
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');
@@ -228,6 +230,25 @@ export async function runInstaller(apiKey) {
228
230
  }
229
231
  markDone(state, 'crm');
230
232
  }
233
+ // ── Background: kick off agent scan while steps 5-7 run ──
234
+ let agentPromise = null;
235
+ let agentRenderer = null;
236
+ const cardBuffer = [];
237
+ let cardsLive = false;
238
+ let bgCardCount = 0;
239
+ if (apiKey && process.stdin.isTTY && !isDone(state, 'briefing')) {
240
+ startSession();
241
+ agentRenderer = new ProgressRenderer();
242
+ agentPromise = runAgent(apiKey, agentRenderer, async (card) => {
243
+ if (cardsLive) {
244
+ bgCardCount++;
245
+ return await pickCard(card, bgCardCount);
246
+ }
247
+ return new Promise((resolve) => {
248
+ cardBuffer.push({ card, resolve });
249
+ });
250
+ });
251
+ }
231
252
  // ── Step 5: Archetype ──
232
253
  stepHeader(5, 'Archetype', isDone(state, 'archetype'));
233
254
  if (!isDone(state, 'archetype') && apiKey) {
@@ -248,8 +269,8 @@ export async function runInstaller(apiKey) {
248
269
  }
249
270
  markDone(state, 'archetype');
250
271
  }
251
- // ── Step 6: Messaging ──
252
- stepHeader(6, 'Messaging', isDone(state, 'phone'));
272
+ // ── Step 6: NOX Link ──
273
+ stepHeader(6, 'NOX Link', isDone(state, 'phone'));
253
274
  if (!isDone(state, 'phone')) {
254
275
  if (apiKey) {
255
276
  const gw = startGateway(apiKey);
@@ -259,12 +280,14 @@ export async function runInstaller(apiKey) {
259
280
  const hostname = getTailscaleHostname();
260
281
  if (hostname) {
261
282
  state.funnelUrl = `http://${hostname}:19877`;
262
- spinner?.start('opening the connection');
283
+ spinner?.start('connecting nox');
263
284
  const funnelUrl = startFunnel(19877);
264
285
  spinner?.stop();
265
286
  if (funnelUrl)
266
287
  state.funnelUrl = funnelUrl;
267
- console.log(DIM(' connected'));
288
+ console.log(DIM(' nox link connected'));
289
+ if (state.funnelUrl)
290
+ console.log(DIM(` ${state.funnelUrl}`));
268
291
  }
269
292
  else {
270
293
  console.log(HD(' network connection incomplete — run setup again'));
@@ -363,21 +386,26 @@ export async function runInstaller(apiKey) {
363
386
  console.log();
364
387
  console.log(DIM(' let\'s see what\'s going on...'));
365
388
  console.log();
366
- // Start a session for the briefing
367
- startSession();
368
- // Import and run the agent directly
369
- const { runAgent } = await import('./agent.js');
370
- const { pickCard, renderSectionHeader, renderSection, renderHandled, HD: tHD, DIM: tDIM, MID: tMID } = await import('./tui.js');
371
- const { ProgressRenderer } = await import('./progress.js');
372
- const renderer = new ProgressRenderer();
373
- renderer.start();
374
- let cardCount = 0;
375
- const analysis = await runAgent(apiKey, renderer, async (card) => {
376
- cardCount++;
377
- return await pickCard(card, cardCount);
378
- });
379
- renderer.stop();
380
- renderer.clear();
389
+ if (!agentPromise) {
390
+ startSession();
391
+ agentRenderer = new ProgressRenderer();
392
+ bgCardCount = 0;
393
+ agentPromise = runAgent(apiKey, agentRenderer, async (card) => {
394
+ bgCardCount++;
395
+ return await pickCard(card, bgCardCount);
396
+ });
397
+ }
398
+ agentRenderer.start();
399
+ cardsLive = true;
400
+ // Drain any cards that arrived during steps 5-7
401
+ for (const { card, resolve } of cardBuffer) {
402
+ bgCardCount++;
403
+ resolve(await pickCard(card, bgCardCount));
404
+ }
405
+ cardBuffer.length = 0;
406
+ const analysis = await agentPromise;
407
+ agentRenderer.stop();
408
+ agentRenderer.clear();
381
409
  // Render results
382
410
  if (analysis.greeting) {
383
411
  console.log(` ${MID(analysis.greeting)}`);
@@ -386,10 +414,10 @@ export async function runInstaller(apiKey) {
386
414
  if (analysis.handled?.length)
387
415
  renderHandled(analysis.handled, 5);
388
416
  if (analysis.promises?.length) {
389
- renderSectionHeader('promises', tHD);
417
+ renderSectionHeader('promises', HD);
390
418
  }
391
419
  if (analysis.alpha?.length) {
392
- renderSection('alpha', analysis.alpha, '·', tHD, tMID);
420
+ renderSection('alpha', analysis.alpha, '·', HD, MID);
393
421
  }
394
422
  markDone(state, 'briefing');
395
423
  }
@@ -405,6 +433,9 @@ export async function runInstaller(apiKey) {
405
433
  console.log(DIM(` ${active.map(p => p.display_name).join(', ')}`));
406
434
  }
407
435
  }
436
+ if (state.funnelUrl) {
437
+ console.log(DIM(` nox endpoint: ${state.funnelUrl}`));
438
+ }
408
439
  console.log(DIM(' running in the background'));
409
440
  console.log(DIM(' you\'ll hear from me at 7:30 AM'));
410
441
  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. "MollyCantillon" "Molly Cantillon")
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
+ }
@@ -31,6 +31,7 @@ export declare class ProgressRenderer implements ProgressSink {
31
31
  workerFail(id: string): void;
32
32
  private s;
33
33
  private ms;
34
+ private phaseForTools;
34
35
  private spin;
35
36
  private bar;
36
37
  private paint;
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: 'reading the room',
15
- get_conversation: 'listening in',
16
- profile_contact: 'learning who this is',
17
- get_messages: 'reading the room',
18
- get_unanswered_messages: 'finding what you missed',
19
- get_screen_time: 'tracking your attention',
20
- get_browsing: 'retracing your steps',
21
- get_calls: 'checking who called',
22
- scan_sources: 'pulling threads',
23
- lookup_contact: 'learning who this is',
24
- get_email_summary: 'going through your inbox',
25
- get_git_activity: 'seeing what you shipped',
26
- get_recent_files: 'noticing what you touched',
27
- get_shell_history: 'retracing your steps',
28
- get_notes: 'reading your thoughts',
29
- get_claude_history: 'seeing what you asked',
30
- get_chatgpt_history: 'seeing what you asked',
31
- get_interests_for_plans: 'understanding your world',
32
- get_calendar: 'looking at your day',
33
- get_reminders: 'checking what you owe yourself',
34
- get_notifications: 'catching up',
35
- discover_platforms: 'figuring out your world',
36
- generate_archetype: 'figuring out who you are',
37
- search_emails: 'digging through mail',
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('scanned')}`);
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
- lines.push(` ${chalk.dim('working')} ${this.bar(done, total)} ${chalk.white(String(done))}${chalk.dim('/')}${chalk.white(String(total))}`);
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(` ${OK('✓')} ${chalk.dim('done')}`);
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('waiting for your answer')}`);
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[];