life-pulse 2.3.4 → 2.3.8
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/dist/agent.js +1 -1
- package/dist/cli.js +10 -19
- package/dist/crm.d.ts +6 -1
- package/dist/crm.js +216 -133
- package/dist/icloud-discovery.js +1 -1
- package/dist/tui.d.ts +6 -7
- package/dist/tui.js +108 -55
- package/dist/ui/app.d.ts +2 -1
- package/dist/ui/app.js +29 -3
- package/dist/ui/progress.js +6 -2
- package/dist/ui/theme.js +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -362,7 +362,7 @@ progress, onCard) {
|
|
|
362
362
|
|
|
363
363
|
Output JSON (no markdown, no code fences):
|
|
364
364
|
{
|
|
365
|
-
"greeting": "
|
|
365
|
+
"greeting": "Short, warm one-liner. No counts or statistics.",
|
|
366
366
|
"promises": [{ "title": "...", "urgency": "now|today|this_week", "who": "...", "when_due": "...", "options": [{"label":"...","description":"..."}] }],
|
|
367
367
|
"blockers": [{ "title": "...", "urgency": "now|today|this_week", "who": "...", "waiting_since": "...", "options": [{"label":"...","description":"..."}] }],
|
|
368
368
|
"bumps": [{ "title": "...", "who": "...", "context": "...", "options": [{"label":"...","description":"..."}] }],
|
package/dist/cli.js
CHANGED
|
@@ -6,11 +6,11 @@ import { saveState, saveDecisions } from './state.js';
|
|
|
6
6
|
// ProgressRenderer unused — removed
|
|
7
7
|
import { InkProgress } from './ui/progress.js';
|
|
8
8
|
import { addTodo, resolveTodos, pruneOld } from './todo.js';
|
|
9
|
-
import { renderIntro,
|
|
9
|
+
import { renderIntro, renderCRMList, revealContacts, revealInsights, pickCard, renderSection, renderSectionHeader, renderHandled, renderDivider, renderBrief, renderArchetype, Spinner, destroyUI, MAG, CYN, RED, AMB, MID, DIM } from './tui.js';
|
|
10
10
|
import { needsDiscovery, discoverPlatforms, savePlatforms } from './platforms.js';
|
|
11
11
|
import { generateArchetype } from './archetype.js';
|
|
12
12
|
import { runPermissionFlow, getMissingPermissions } from './permissions.js';
|
|
13
|
-
import { buildCRM, streamEnrichedCRM } from './crm.js';
|
|
13
|
+
import { buildCRM, streamEnrichedCRM, generateInsights } from './crm.js';
|
|
14
14
|
import { saveContactSummaries } from './intelligence.js';
|
|
15
15
|
import { getUserName } from './profile.js';
|
|
16
16
|
// Session progress tracking (Anthropic long-running agent pattern)
|
|
@@ -89,36 +89,27 @@ async function showCRM(apiKey, opts) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
opts?.spinner?.stop();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
await renderReveal({
|
|
96
|
-
conversations: crm.threads.length,
|
|
97
|
-
innerCircle,
|
|
98
|
-
aboutToBreak,
|
|
99
|
-
});
|
|
92
|
+
console.log();
|
|
93
|
+
// Fire insights in background — they'll resolve while user steps through contacts
|
|
94
|
+
const insightsGen = generateInsights(crm);
|
|
100
95
|
const calendarContext = opts?.calendarContext ?? '';
|
|
101
96
|
const hour = new Date().getHours();
|
|
102
97
|
const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
|
|
103
|
-
// Stream contacts as they enrich — each name + relationship appears live
|
|
104
98
|
let brief = '';
|
|
105
|
-
const enriched =
|
|
106
|
-
const enrichedNames = new Set();
|
|
107
|
-
for await (const t of streamEnrichedCRM(crm, apiKey, {
|
|
99
|
+
const enriched = await revealContacts(streamEnrichedCRM(crm, apiKey, {
|
|
108
100
|
calendarContext,
|
|
109
101
|
timeOfDay,
|
|
110
102
|
onBrief: (b) => { brief = b; },
|
|
111
|
-
}))
|
|
112
|
-
|
|
113
|
-
enrichedNames.add(t.name);
|
|
114
|
-
await renderCRMContact(t);
|
|
115
|
-
}
|
|
103
|
+
}));
|
|
104
|
+
const enrichedNames = new Set(enriched.map(t => t.name));
|
|
116
105
|
// Show remaining contacts (not enriched) — just names
|
|
117
106
|
const remaining = crm.threads.filter(t => !enrichedNames.has(t.name)).slice(0, 5);
|
|
118
107
|
if (remaining.length)
|
|
119
108
|
renderCRMList(remaining);
|
|
120
109
|
if (brief)
|
|
121
110
|
await renderBrief(brief);
|
|
111
|
+
// Insights drip in after contacts
|
|
112
|
+
await revealInsights(insightsGen);
|
|
122
113
|
saveContactSummaries(enriched);
|
|
123
114
|
return true;
|
|
124
115
|
}
|
package/dist/crm.d.ts
CHANGED
|
@@ -35,9 +35,14 @@ export interface CRM {
|
|
|
35
35
|
generatedAt: string;
|
|
36
36
|
}
|
|
37
37
|
export declare function buildCRM(): Promise<CRM>;
|
|
38
|
-
export declare function streamEnrichedCRM(crm: CRM,
|
|
38
|
+
export declare function streamEnrichedCRM(crm: CRM, _apiKey?: string, opts?: {
|
|
39
39
|
calendarContext?: string;
|
|
40
40
|
timeOfDay?: string;
|
|
41
41
|
onBrief?: (brief: string) => void;
|
|
42
42
|
}): AsyncGenerator<ThreadState>;
|
|
43
|
+
export interface Insight {
|
|
44
|
+
label: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function generateInsights(crm: CRM): AsyncGenerator<Insight>;
|
|
43
48
|
export declare function buildCRMContext(crm: CRM): string;
|
package/dist/crm.js
CHANGED
|
@@ -12,12 +12,31 @@ import { join } from 'path';
|
|
|
12
12
|
import { openDb, safeQuery } from './db.js';
|
|
13
13
|
import { resolveName } from './contacts.js';
|
|
14
14
|
import * as rply from './rply-client.js';
|
|
15
|
-
|
|
15
|
+
const ENRICH_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
16
16
|
import dayjs from 'dayjs';
|
|
17
17
|
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
|
18
18
|
dayjs.extend(relativeTime);
|
|
19
19
|
import { APPLE_EPOCH } from './types.js';
|
|
20
20
|
const NANO = BigInt(1e9);
|
|
21
|
+
// ─── Contact quality filter ─────────────────────────────────────
|
|
22
|
+
const BUSINESS_PATTERNS = [
|
|
23
|
+
/^apple\s/i, /\bstore\b/i, /\bsupport\b/i, /\bairlines?\b/i,
|
|
24
|
+
/\bbank\b/i, /\binsurance\b/i, /\bpharmacy\b/i, /\bclinic\b/i,
|
|
25
|
+
/\bhotel\b/i, /\bairport\b/i, /\bdelivery\b/i, /\bservice\b/i,
|
|
26
|
+
/\binc\.?\b/i, /\bllc\b/i, /\bcorp\b/i, /^doordash/i, /^uber/i,
|
|
27
|
+
/^lyft/i, /^venmo/i, /^cashapp/i, /^paypal/i, /^amazon/i,
|
|
28
|
+
];
|
|
29
|
+
function isHumanContact(name) {
|
|
30
|
+
if (!name || name.length < 3)
|
|
31
|
+
return false;
|
|
32
|
+
// Raw phone numbers
|
|
33
|
+
if (/^\+?\d[\d\s()-]{6,}$/.test(name.trim()))
|
|
34
|
+
return false;
|
|
35
|
+
// Businesses
|
|
36
|
+
if (BUSINESS_PATTERNS.some(p => p.test(name)))
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
21
40
|
function nowNano() {
|
|
22
41
|
return (BigInt(dayjs().unix() - APPLE_EPOCH) * NANO).toString();
|
|
23
42
|
}
|
|
@@ -117,7 +136,14 @@ async function buildCRMFromRPLY() {
|
|
|
117
136
|
recentSnippets: recentSnippets.slice(0, 30),
|
|
118
137
|
});
|
|
119
138
|
}
|
|
120
|
-
|
|
139
|
+
// Sort by total message volume (all-time proxy: total fetched msgs, not just 30d)
|
|
140
|
+
threads.sort((a, b) => {
|
|
141
|
+
// Total messages fetched is the best all-time proxy from RPLY (200 limit per contact)
|
|
142
|
+
// Fall back to 30d if equal
|
|
143
|
+
const aTotal = (a.conversationPairs * 2) + a.msgs30d;
|
|
144
|
+
const bTotal = (b.conversationPairs * 2) + b.msgs30d;
|
|
145
|
+
return bTotal - aTotal;
|
|
146
|
+
});
|
|
121
147
|
const yourAvg = allResponseTimes.length > 0
|
|
122
148
|
? allResponseTimes.reduce((s, r) => s + r.avg * r.pairs, 0)
|
|
123
149
|
/ allResponseTimes.reduce((s, r) => s + r.pairs, 0)
|
|
@@ -189,7 +215,7 @@ function buildCRMFromSQLite() {
|
|
|
189
215
|
ORDER BY avg_response_seconds ASC LIMIT 50
|
|
190
216
|
`, [ago30, now]);
|
|
191
217
|
const responseMap = new Map(responseTimes.map(r => [r.handle_id, r]));
|
|
192
|
-
// ── 2. Top contacts by
|
|
218
|
+
// ── 2. Top contacts by ALL-TIME volume ──
|
|
193
219
|
const topContacts = safeQuery(db, `
|
|
194
220
|
WITH NonGroupChats AS (
|
|
195
221
|
SELECT chat_id FROM chat_handle_join GROUP BY chat_id HAVING COUNT(DISTINCT handle_id) = 1
|
|
@@ -198,9 +224,9 @@ function buildCRMFromSQLite() {
|
|
|
198
224
|
FROM message m JOIN handle h ON m.handle_id = h.ROWID
|
|
199
225
|
JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
|
200
226
|
JOIN NonGroupChats ngc ON ngc.chat_id = cmj.chat_id
|
|
201
|
-
WHERE m.
|
|
227
|
+
WHERE m.associated_message_type = 0 AND h.id NOT LIKE 'urn:biz:%'
|
|
202
228
|
GROUP BY h.id ORDER BY msg_count DESC LIMIT 30
|
|
203
|
-
|
|
229
|
+
`);
|
|
204
230
|
// ── 3. Last message per contact ──
|
|
205
231
|
const lastMessages = safeQuery(db, `
|
|
206
232
|
WITH NonGroupChats AS (
|
|
@@ -307,7 +333,7 @@ function buildCRMFromSQLite() {
|
|
|
307
333
|
}
|
|
308
334
|
threads.push({
|
|
309
335
|
name, handle: handleId,
|
|
310
|
-
msgs30d: contact?.msg_count || 0,
|
|
336
|
+
msgs30d: contact?.msg_count || 0, // now all-time volume from SQL
|
|
311
337
|
avgResponseSec: response?.avg_response_seconds ?? null,
|
|
312
338
|
conversationPairs: response?.conversation_pairs ?? 0,
|
|
313
339
|
lastMsg, waitingOnYou, waitingOnThem, waitingSince,
|
|
@@ -323,10 +349,55 @@ function buildCRMFromSQLite() {
|
|
|
323
349
|
: null;
|
|
324
350
|
return { threads: threads.slice(0, 25), yourAvgResponseSec: yourAvg, generatedAt: dayjs().toISOString() };
|
|
325
351
|
}
|
|
352
|
+
// ─── Per-contact enrichment ──────────────────────────────────────
|
|
353
|
+
async function enrichContact(t) {
|
|
354
|
+
const snippets = (t.recentSnippets || []).slice(0, 25).reverse().join('\n');
|
|
355
|
+
if (!snippets)
|
|
356
|
+
return null;
|
|
357
|
+
const tag = t.waitingOnYou ? ' [WAITING ON YOU]'
|
|
358
|
+
: t.waitingOnThem ? ` [WAITING ON THEM — ${t.waitingSince}]` : '';
|
|
359
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: {
|
|
362
|
+
'x-api-key': ENRICH_KEY,
|
|
363
|
+
'anthropic-version': '2023-06-01',
|
|
364
|
+
'content-type': 'application/json',
|
|
365
|
+
},
|
|
366
|
+
body: JSON.stringify({
|
|
367
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
368
|
+
max_tokens: 100,
|
|
369
|
+
messages: [{
|
|
370
|
+
role: 'user',
|
|
371
|
+
content: `${t.name}.${tag} Two sentences max. What's the vibe, what's live right now. Use names from the messages. No fluff.
|
|
372
|
+
Priority: "work", "personal", or "service" (businesses/bots/automated).
|
|
373
|
+
|
|
374
|
+
${snippets}
|
|
375
|
+
|
|
376
|
+
JSON only: {"summary": "...", "priority": "work|personal|service"}`,
|
|
377
|
+
}],
|
|
378
|
+
}),
|
|
379
|
+
});
|
|
380
|
+
if (!res.ok)
|
|
381
|
+
return null;
|
|
382
|
+
const data = await res.json();
|
|
383
|
+
const content = data.content?.[0]?.text?.trim() || '';
|
|
384
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
385
|
+
if (!jsonMatch)
|
|
386
|
+
return { relationship: content.slice(0, 300), priority: 'personal' };
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
389
|
+
return { relationship: parsed.summary || '', priority: parsed.priority || 'personal' };
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return { relationship: content.slice(0, 300), priority: 'personal' };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
326
395
|
// ─── Stream-enrich CRM ──────────────────────────────────────────
|
|
327
|
-
export async function* streamEnrichedCRM(crm,
|
|
328
|
-
|
|
329
|
-
|
|
396
|
+
export async function* streamEnrichedCRM(crm, _apiKey, opts) {
|
|
397
|
+
// Filter: only real people, not businesses or raw phone numbers
|
|
398
|
+
const humans = crm.threads.filter(t => isHumanContact(t.name));
|
|
399
|
+
const top = humans.slice(0, 10);
|
|
400
|
+
if (!top.length || !ENRICH_KEY) {
|
|
330
401
|
for (const t of top)
|
|
331
402
|
yield t;
|
|
332
403
|
return;
|
|
@@ -337,137 +408,149 @@ export async function* streamEnrichedCRM(crm, apiKey, opts) {
|
|
|
337
408
|
yield t;
|
|
338
409
|
return;
|
|
339
410
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
411
|
+
const indexed = withSnippets.map((t, i) => enrichContact(t)
|
|
412
|
+
.then(r => {
|
|
413
|
+
if (r) {
|
|
414
|
+
t.relationship = r.relationship;
|
|
415
|
+
t.priority = r.priority;
|
|
416
|
+
}
|
|
417
|
+
return { thread: t, idx: i };
|
|
418
|
+
})
|
|
419
|
+
.catch(() => ({ thread: t, idx: i })));
|
|
420
|
+
const pending = new Map(indexed.map((p, i) => [i, p]));
|
|
350
421
|
const yielded = new Set();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
422
|
+
while (pending.size > 0) {
|
|
423
|
+
const resolved = await Promise.race(pending.values());
|
|
424
|
+
pending.delete(resolved.idx);
|
|
425
|
+
// Skip contacts classified as "service" (businesses, bots)
|
|
426
|
+
if (resolved.thread.priority === 'service')
|
|
427
|
+
continue;
|
|
428
|
+
yielded.add(resolved.thread.name);
|
|
429
|
+
yield resolved.thread;
|
|
430
|
+
}
|
|
431
|
+
// Yield remaining contacts without snippets (already filtered to humans)
|
|
432
|
+
for (const t of top) {
|
|
433
|
+
if (!yielded.has(t.name) && t.priority !== 'service')
|
|
434
|
+
yield t;
|
|
435
|
+
}
|
|
436
|
+
// Brief from collected summaries
|
|
437
|
+
if (opts?.onBrief) {
|
|
438
|
+
const summaries = top.filter(t => t.relationship).map(t => `${t.name}: ${t.relationship}`).join('\n');
|
|
439
|
+
if (summaries) {
|
|
440
|
+
try {
|
|
441
|
+
const briefRes = await fetch('https://api.anthropic.com/v1/messages', {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: {
|
|
444
|
+
'x-api-key': ENRICH_KEY,
|
|
445
|
+
'anthropic-version': '2023-06-01',
|
|
446
|
+
'content-type': 'application/json',
|
|
447
|
+
},
|
|
448
|
+
body: JSON.stringify({
|
|
449
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
450
|
+
max_tokens: 150,
|
|
451
|
+
messages: [{
|
|
452
|
+
role: 'user',
|
|
453
|
+
content: `You're their trusted aide. Given these relationship summaries, write 2 short sentences about what matters most in their world right now. Address them as "you". Reference real names. No lists, no counting, no statistics.\n\n${summaries}`,
|
|
454
|
+
}],
|
|
455
|
+
}),
|
|
456
|
+
});
|
|
457
|
+
if (briefRes.ok) {
|
|
458
|
+
const data = await briefRes.json();
|
|
459
|
+
const brief = data.content?.[0]?.text?.trim() || '';
|
|
460
|
+
if (brief)
|
|
461
|
+
opts.onBrief(brief);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch { }
|
|
364
465
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function extractSignals(crm) {
|
|
469
|
+
const t = crm.threads;
|
|
470
|
+
const hot = t.filter(x => x.threadTemp === 'hot').length;
|
|
471
|
+
const warm = t.filter(x => x.threadTemp === 'warm').length;
|
|
472
|
+
const cooling = t.filter(x => x.threadTemp === 'cooling').length;
|
|
473
|
+
const cold = t.filter(x => x.threadTemp === 'cold').length;
|
|
474
|
+
const rising = t.filter(x => x.monthlyTrend === 'rising');
|
|
475
|
+
const declining = t.filter(x => x.monthlyTrend === 'declining');
|
|
476
|
+
const waiting = t.filter(x => x.waitingOnYou);
|
|
477
|
+
const totalMsgs = t.reduce((s, x) => s + x.msgs30d, 0);
|
|
478
|
+
const activeThreads = t.filter(x => x.msgs30d > 0).length;
|
|
479
|
+
// Response time buckets
|
|
480
|
+
const withResponse = t.filter(x => x.avgResponseSec != null && x.conversationPairs >= 3);
|
|
481
|
+
const fastResponders = withResponse.filter(x => x.avgResponseSec < 600); // <10 min
|
|
482
|
+
const slowResponders = withResponse.filter(x => x.avgResponseSec > 86400); // >1 day
|
|
483
|
+
// Message timing (hour buckets from lastMsg timestamps)
|
|
484
|
+
const hourBuckets = new Array(24).fill(0);
|
|
485
|
+
for (const thread of t) {
|
|
486
|
+
if (thread.lastMsg?.when) {
|
|
487
|
+
const h = new Date(thread.lastMsg.when).getHours();
|
|
488
|
+
hourBuckets[h] += thread.msgs30d;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
hot, warm, cooling, cold, rising, declining, waiting,
|
|
493
|
+
totalMsgs, activeThreads, withResponse, fastResponders, slowResponders,
|
|
494
|
+
hourBuckets, threads: t,
|
|
495
|
+
yourAvgSec: crm.yourAvgResponseSec,
|
|
368
496
|
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
497
|
+
}
|
|
498
|
+
async function callInsight(prompt) {
|
|
499
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
500
|
+
method: 'POST',
|
|
501
|
+
headers: {
|
|
502
|
+
'x-api-key': ENRICH_KEY,
|
|
503
|
+
'anthropic-version': '2023-06-01',
|
|
504
|
+
'content-type': 'application/json',
|
|
505
|
+
},
|
|
506
|
+
body: JSON.stringify({
|
|
372
507
|
model: 'claude-sonnet-4-5-20250929',
|
|
373
|
-
max_tokens:
|
|
374
|
-
messages: [{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
[
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
[Next Name]
|
|
391
|
-
...
|
|
508
|
+
max_tokens: 120,
|
|
509
|
+
messages: [{ role: 'user', content: prompt }],
|
|
510
|
+
}),
|
|
511
|
+
});
|
|
512
|
+
if (!res.ok)
|
|
513
|
+
throw new Error(`insight ${res.status}`);
|
|
514
|
+
const data = await res.json();
|
|
515
|
+
return data.content?.[0]?.text?.trim() || '';
|
|
516
|
+
}
|
|
517
|
+
export async function* generateInsights(crm) {
|
|
518
|
+
if (!ENRICH_KEY || crm.threads.length < 3)
|
|
519
|
+
return;
|
|
520
|
+
const s = extractSignals(crm);
|
|
521
|
+
const prompts = [
|
|
522
|
+
{
|
|
523
|
+
label: 'chronotype',
|
|
524
|
+
prompt: `Based on this message volume by hour (index = hour 0-23): [${s.hourBuckets.join(',')}]
|
|
392
525
|
|
|
393
|
-
|
|
394
|
-
|
|
526
|
+
Write ONE sentence about whether this person is a morning person, night owl, or peaks midday. Be specific about the hours. No hedging. Address them as "you".`,
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
label: 'response personality',
|
|
530
|
+
prompt: `This person has ${s.withResponse.length} contacts they regularly text.
|
|
531
|
+
Fast replies (<10 min avg): ${s.fastResponders.map(t => t.name).join(', ') || 'none'}
|
|
532
|
+
Slow replies (>1 day avg): ${s.slowResponders.map(t => t.name).join(', ') || 'none'}
|
|
533
|
+
Overall avg response: ${s.yourAvgSec ? fmtTime(s.yourAvgSec) : 'unknown'}
|
|
395
534
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
- Priority: "work" for professional/business, "personal" for friends/family/social, "service" for automated/transactional/bots
|
|
403
|
-
- BRIEF: synthesize across relationships. Mention names. Don't list — narrate.
|
|
404
|
-
- If someone has a calendar meeting today, weave that in naturally.
|
|
535
|
+
Write ONE sentence about their response patterns. Name names. Be direct. Address them as "you".`,
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
label: 'inner circle',
|
|
539
|
+
prompt: `This person's relationship map: ${s.hot} hot (daily), ${s.warm} warm (every few days), ${s.cooling} cooling (weekly), ${s.cold} cold (dormant).
|
|
540
|
+
Top 3 by volume: ${s.threads.slice(0, 3).map(t => `${t.name} (${t.msgs30d} msgs/mo)`).join(', ')}
|
|
405
541
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
if (/^BRIEF:?$/i.test(line)) {
|
|
420
|
-
flushContact();
|
|
421
|
-
section = 'brief';
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
const bracketMatch = line.match(/^\[([^\]]+)\]/);
|
|
425
|
-
if (bracketMatch && (section === 'contacts' || section === 'preamble')) {
|
|
426
|
-
flushContact();
|
|
427
|
-
curName = bracketMatch[1].trim();
|
|
428
|
-
section = 'contacts';
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (!line)
|
|
432
|
-
return;
|
|
433
|
-
if (section === 'contacts' || section === 'preamble') {
|
|
434
|
-
if (line.startsWith('Priority:')) {
|
|
435
|
-
curPriority = line.replace('Priority:', '').trim().toLowerCase();
|
|
436
|
-
}
|
|
437
|
-
else if (curName) {
|
|
438
|
-
curLines.push(line);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
else if (section === 'brief') {
|
|
442
|
-
briefLines.push(line);
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
for await (const event of stream) {
|
|
446
|
-
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
447
|
-
lineBuf += event.delta.text;
|
|
448
|
-
const parts = lineBuf.split('\n');
|
|
449
|
-
lineBuf = parts.pop() || '';
|
|
450
|
-
for (const line of parts)
|
|
451
|
-
processLine(line);
|
|
452
|
-
while (ready.length)
|
|
453
|
-
yield ready.shift();
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// Flush remaining buffer
|
|
457
|
-
if (lineBuf.trim())
|
|
458
|
-
processLine(lineBuf);
|
|
459
|
-
flushContact();
|
|
460
|
-
while (ready.length)
|
|
461
|
-
yield ready.shift();
|
|
462
|
-
if (briefLines.length && opts?.onBrief)
|
|
463
|
-
opts.onBrief(briefLines.join(' '));
|
|
464
|
-
}
|
|
465
|
-
catch (err) {
|
|
466
|
-
// silent — contacts still show without enrichment
|
|
467
|
-
}
|
|
468
|
-
for (const t of top) {
|
|
469
|
-
if (!yielded.has(t.name) && t.recentSnippets?.length)
|
|
470
|
-
yield t;
|
|
542
|
+
Write ONE sentence about the shape of their social world. Be specific. Address them as "you".`,
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
const indexed = prompts.map((p, i) => callInsight(p.prompt)
|
|
546
|
+
.then(text => ({ label: p.label, text, idx: i }))
|
|
547
|
+
.catch(() => ({ label: p.label, text: '', idx: i })));
|
|
548
|
+
const pending = new Map(indexed.map((p, i) => [i, p]));
|
|
549
|
+
while (pending.size > 0) {
|
|
550
|
+
const resolved = await Promise.race(pending.values());
|
|
551
|
+
pending.delete(resolved.idx);
|
|
552
|
+
if (resolved.text)
|
|
553
|
+
yield { label: resolved.label, text: resolved.text };
|
|
471
554
|
}
|
|
472
555
|
}
|
|
473
556
|
// ─── CRM context for agent prompt injection ─────────────────────
|
package/dist/icloud-discovery.js
CHANGED
|
@@ -373,7 +373,7 @@ export function formatDiscoveryReport(discoveries) {
|
|
|
373
373
|
if (!discoveries.length)
|
|
374
374
|
return '';
|
|
375
375
|
const lines = [];
|
|
376
|
-
lines.push('
|
|
376
|
+
lines.push('');
|
|
377
377
|
// Group by category
|
|
378
378
|
const noteApps = discoveries.filter(d => ['Obsidian', 'Bear', 'Logseq', 'Apple Notes', 'Craft', 'iA Writer', 'Ulysses'].includes(d.app));
|
|
379
379
|
const taskApps = discoveries.filter(d => ['Things 3', 'OmniFocus', 'Todoist'].includes(d.app));
|
package/dist/tui.d.ts
CHANGED
|
@@ -17,11 +17,6 @@ export declare const HD: import("chalk").ChalkInstance;
|
|
|
17
17
|
export declare function renderIntro(name?: string): Promise<void>;
|
|
18
18
|
export declare function renderGreeting(name?: string): void;
|
|
19
19
|
export declare function renderContextLine(): void;
|
|
20
|
-
export declare function renderReveal(stats: {
|
|
21
|
-
conversations: number;
|
|
22
|
-
innerCircle: number;
|
|
23
|
-
aboutToBreak: number;
|
|
24
|
-
}): Promise<void>;
|
|
25
20
|
export declare function renderCRMList(contacts: {
|
|
26
21
|
name: string;
|
|
27
22
|
lastMsg: {
|
|
@@ -68,7 +63,11 @@ interface CRMThread {
|
|
|
68
63
|
} | null;
|
|
69
64
|
relationship?: string;
|
|
70
65
|
}
|
|
71
|
-
export declare function
|
|
72
|
-
|
|
66
|
+
export declare function revealContacts(source: AsyncIterable<CRMThread>): Promise<CRMThread[]>;
|
|
67
|
+
interface InsightItem {
|
|
68
|
+
label: string;
|
|
69
|
+
text: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function revealInsights(source: AsyncIterable<InsightItem>): Promise<void>;
|
|
73
72
|
export declare function destroyUI(): void;
|
|
74
73
|
export {};
|
package/dist/tui.js
CHANGED
|
@@ -39,21 +39,34 @@ async function typewrite(text, style, delay = 30) {
|
|
|
39
39
|
await _sleep(delay);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
// WMO weather code → short description
|
|
43
|
+
const WMO = {
|
|
44
|
+
0: 'clear', 1: 'mostly clear', 2: 'partly cloudy', 3: 'overcast',
|
|
45
|
+
45: 'foggy', 48: 'foggy', 51: 'light drizzle', 53: 'drizzle', 55: 'drizzle',
|
|
46
|
+
61: 'light rain', 63: 'rain', 65: 'heavy rain', 66: 'freezing rain', 67: 'freezing rain',
|
|
47
|
+
71: 'light snow', 73: 'snow', 75: 'heavy snow', 77: 'snow',
|
|
48
|
+
80: 'rain showers', 81: 'rain showers', 82: 'heavy showers',
|
|
49
|
+
85: 'snow showers', 86: 'heavy snow', 95: 'thunderstorm', 96: 'thunderstorm', 99: 'thunderstorm',
|
|
50
|
+
};
|
|
42
51
|
async function fetchLocationWeather() {
|
|
43
52
|
try {
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const raw = (await r.text()).trim();
|
|
49
|
-
const [loc, temp, cond] = raw.split('|');
|
|
50
|
-
if (!loc || !temp || !cond)
|
|
53
|
+
// 1. System timezone → city name + coordinates via Open-Meteo geocoding
|
|
54
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
55
|
+
const cityGuess = tz.split('/').pop()?.replace(/_/g, ' ');
|
|
56
|
+
if (!cityGuess)
|
|
51
57
|
return null;
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const geoR = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityGuess)}&count=1`, { signal: AbortSignal.timeout(3000) });
|
|
59
|
+
const geo = await geoR.json();
|
|
60
|
+
const place = geo.results?.[0];
|
|
61
|
+
if (!place)
|
|
62
|
+
return null;
|
|
63
|
+
// 2. Current weather from coordinates — no API key
|
|
64
|
+
const wxR = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}&longitude=${place.longitude}¤t=temperature_2m,weather_code&temperature_unit=fahrenheit`, { signal: AbortSignal.timeout(3000) });
|
|
65
|
+
const wx = await wxR.json();
|
|
66
|
+
const temp = Math.round(wx.current?.temperature_2m ?? 0);
|
|
67
|
+
const code = wx.current?.weather_code ?? -1;
|
|
68
|
+
const sky = WMO[code] || 'clear';
|
|
69
|
+
return { city: place.name, weather: `${temp}° and ${sky}` };
|
|
57
70
|
}
|
|
58
71
|
catch {
|
|
59
72
|
return null;
|
|
@@ -63,7 +76,7 @@ function timeGreeting(h) {
|
|
|
63
76
|
if (h < 5)
|
|
64
77
|
return "Don't stay up too late.";
|
|
65
78
|
if (h < 12)
|
|
66
|
-
return
|
|
79
|
+
return "Hope you're having an incredible morning.";
|
|
67
80
|
if (h < 17)
|
|
68
81
|
return "Hope you're having a wonderful afternoon.";
|
|
69
82
|
if (h < 21)
|
|
@@ -72,14 +85,16 @@ function timeGreeting(h) {
|
|
|
72
85
|
}
|
|
73
86
|
function weatherRemark(weather) {
|
|
74
87
|
const w = weather.toLowerCase();
|
|
75
|
-
if (w.includes('
|
|
88
|
+
if (w.includes('clear') || w.includes('sunny'))
|
|
76
89
|
return `${weather} out there.`;
|
|
77
|
-
if (w.includes('rain') || w.includes('drizzle'))
|
|
90
|
+
if (w.includes('rain') || w.includes('drizzle') || w.includes('shower'))
|
|
78
91
|
return `${weather} — good day to stay sharp.`;
|
|
79
|
-
if (w.includes('cloud') || w.includes('overcast'))
|
|
92
|
+
if (w.includes('cloud') || w.includes('overcast') || w.includes('fog'))
|
|
80
93
|
return `${weather}.`;
|
|
81
94
|
if (w.includes('snow'))
|
|
82
95
|
return `${weather}. Stay warm.`;
|
|
96
|
+
if (w.includes('thunder'))
|
|
97
|
+
return `${weather}. Stay inside.`;
|
|
83
98
|
return `${weather}.`;
|
|
84
99
|
}
|
|
85
100
|
// ─── Geometric animation ───
|
|
@@ -155,11 +170,21 @@ export async function renderIntro(name) {
|
|
|
155
170
|
}
|
|
156
171
|
// "Hope you're having an incredible morning."
|
|
157
172
|
const h = new Date().getHours();
|
|
173
|
+
const greeting = timeGreeting(h);
|
|
158
174
|
process.stdout.write(' ');
|
|
159
|
-
await typewrite(
|
|
175
|
+
await typewrite(greeting, G3, 18);
|
|
160
176
|
process.stdout.write('\n\n');
|
|
177
|
+
await _sleep(1200);
|
|
178
|
+
// Build greeting lines for Ink to preserve
|
|
179
|
+
const greetLines = [''];
|
|
180
|
+
greetLines.push(` ${G1(hey)}`);
|
|
181
|
+
if (loc)
|
|
182
|
+
greetLines.push(` ${G2(`I see you're in ${loc.city}. ${weatherRemark(loc.weather)}`)}`);
|
|
183
|
+
greetLines.push(` ${G3(greeting)}`);
|
|
184
|
+
greetLines.push('');
|
|
185
|
+
greetLines.push('');
|
|
161
186
|
if (USE_INK)
|
|
162
|
-
ink.initInk();
|
|
187
|
+
ink.initInk(greetLines);
|
|
163
188
|
}
|
|
164
189
|
// ─── Greeting (legacy / non-interactive fallback) ───
|
|
165
190
|
export function renderGreeting(name) {
|
|
@@ -184,21 +209,6 @@ export function renderContextLine() {
|
|
|
184
209
|
out(` ${C.dim(`${days[d.getDay()]} · ${h12}:${min} ${ampm}`)}`);
|
|
185
210
|
out('');
|
|
186
211
|
}
|
|
187
|
-
// ─── Reveal (the moment after discovery) ───
|
|
188
|
-
export async function renderReveal(stats) {
|
|
189
|
-
await _sleep(600);
|
|
190
|
-
process.stdout.write(' ');
|
|
191
|
-
await typewrite(`${stats.conversations} conversations.`, G2, 22);
|
|
192
|
-
await _sleep(400);
|
|
193
|
-
process.stdout.write(' ');
|
|
194
|
-
await typewrite(`${stats.innerCircle} people who matter.`, G1, 25);
|
|
195
|
-
await _sleep(500);
|
|
196
|
-
if (stats.aboutToBreak > 0) {
|
|
197
|
-
process.stdout.write(' ');
|
|
198
|
-
await typewrite(`${stats.aboutToBreak} thread${stats.aboutToBreak === 1 ? '' : 's'} about to break.`, chalk.bold.hex('#c0caf5'), 28);
|
|
199
|
-
}
|
|
200
|
-
process.stdout.write('\n\n');
|
|
201
|
-
}
|
|
202
212
|
// ─── CRM List ───
|
|
203
213
|
export function renderCRMList(contacts) {
|
|
204
214
|
for (const t of contacts.slice(0, 12)) {
|
|
@@ -285,39 +295,82 @@ export class Spinner {
|
|
|
285
295
|
export function renderFYI(title, context) {
|
|
286
296
|
out(` ${C.dim('·')} ${title}${context ? ' ' + C.dim(context) : ''}`);
|
|
287
297
|
}
|
|
288
|
-
export async function
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
+
export async function revealContacts(source) {
|
|
299
|
+
const buffer = [];
|
|
300
|
+
let done = false;
|
|
301
|
+
const revealed = [];
|
|
302
|
+
// Background: drain async generator into buffer
|
|
303
|
+
const drain = (async () => {
|
|
304
|
+
for await (const t of source)
|
|
305
|
+
buffer.push(t);
|
|
306
|
+
done = true;
|
|
307
|
+
})();
|
|
308
|
+
if (USE_INK)
|
|
309
|
+
ink.showSpinner('reading the room');
|
|
310
|
+
while (!done || buffer.length > 0) {
|
|
311
|
+
if (buffer.length === 0) {
|
|
312
|
+
await _sleep(80);
|
|
313
|
+
continue;
|
|
298
314
|
}
|
|
315
|
+
if (USE_INK)
|
|
316
|
+
ink.hideSpinner();
|
|
317
|
+
const t = buffer.shift();
|
|
318
|
+
revealed.push(t);
|
|
319
|
+
const w = W();
|
|
299
320
|
const name = t.name.slice(0, 24);
|
|
300
321
|
const ago = shortAgo(t.lastMsg?.ago || '');
|
|
301
322
|
const gap = Math.max(2, w - 4 - name.length - ago.length);
|
|
323
|
+
out('');
|
|
302
324
|
out(` ${C.hd(name)}${' '.repeat(gap)}${C.dim(ago)}`);
|
|
303
325
|
if (t.relationship) {
|
|
304
|
-
|
|
326
|
+
// Word-wrap relationship across lines
|
|
327
|
+
const maxW = w - 8;
|
|
328
|
+
const words = t.relationship.split(' ');
|
|
329
|
+
let line = '';
|
|
330
|
+
for (const word of words) {
|
|
331
|
+
if (line.length + word.length + 1 > maxW && line) {
|
|
332
|
+
out(` ${C.mid(line)}`);
|
|
333
|
+
line = word;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
line = line ? line + ' ' + word : word;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (line)
|
|
340
|
+
out(` ${C.mid(line)}`);
|
|
341
|
+
}
|
|
342
|
+
// Wait for Enter before showing next (interactive only)
|
|
343
|
+
if (USE_INK && (!done || buffer.length > 0)) {
|
|
344
|
+
await ink.waitForEnter();
|
|
305
345
|
}
|
|
306
|
-
rendered.push(t);
|
|
307
346
|
}
|
|
308
|
-
|
|
309
|
-
|
|
347
|
+
await drain;
|
|
348
|
+
if (USE_INK)
|
|
349
|
+
ink.hideSpinner();
|
|
310
350
|
out('');
|
|
311
|
-
return
|
|
351
|
+
return revealed;
|
|
312
352
|
}
|
|
313
|
-
export async function
|
|
353
|
+
export async function revealInsights(source) {
|
|
314
354
|
const w = W();
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
355
|
+
for await (const insight of source) {
|
|
356
|
+
// Word-wrap the insight text
|
|
357
|
+
const maxW = w - 6;
|
|
358
|
+
const words = insight.text.split(' ');
|
|
359
|
+
let line = '';
|
|
360
|
+
let first = true;
|
|
361
|
+
for (const word of words) {
|
|
362
|
+
if (line.length + word.length + 1 > maxW && line) {
|
|
363
|
+
out(` ${first ? C.dim('·') : ' '} ${C.dim(line)}`);
|
|
364
|
+
first = false;
|
|
365
|
+
line = word;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
line = line ? line + ' ' + word : word;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (line)
|
|
372
|
+
out(` ${first ? C.dim('·') : ' '} ${C.dim(line)}`);
|
|
373
|
+
}
|
|
321
374
|
}
|
|
322
375
|
// ─── Destroy (call at exit) ───
|
|
323
376
|
export function destroyUI() {
|
package/dist/ui/app.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface CardData {
|
|
|
17
17
|
who?: string;
|
|
18
18
|
urgency?: string;
|
|
19
19
|
}
|
|
20
|
-
export declare function initInk(): void;
|
|
20
|
+
export declare function initInk(seedLines?: string[]): void;
|
|
21
21
|
export declare function destroy(): void;
|
|
22
22
|
export declare function log(text: string): void;
|
|
23
23
|
export declare function gap(): void;
|
|
@@ -30,3 +30,4 @@ export declare function showProgress(phase: string, done: number, total: number,
|
|
|
30
30
|
tool?: string;
|
|
31
31
|
}[], thinking: boolean): void;
|
|
32
32
|
export declare function hideProgress(): void;
|
|
33
|
+
export declare function waitForEnter(): Promise<void>;
|
package/dist/ui/app.js
CHANGED
|
@@ -7,17 +7,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
*/
|
|
8
8
|
import React, { useState, useEffect, useRef } from 'react';
|
|
9
9
|
import { render as inkRender, Box, Text, useInput, useApp } from 'ink';
|
|
10
|
-
import { C,
|
|
10
|
+
import { C, BAR_CHARS } from './theme.js';
|
|
11
11
|
// ─── Module bridge ────────────────────────────────
|
|
12
12
|
let _update = null;
|
|
13
13
|
let _cardResolve = null;
|
|
14
|
+
let _enterResolve = null;
|
|
14
15
|
const INIT = {
|
|
15
16
|
lines: [], spinner: null,
|
|
16
17
|
card: null, cardNum: 0, cardSel: 0,
|
|
17
18
|
progress: null,
|
|
18
19
|
};
|
|
19
20
|
// ─── Components ───────────────────────────────────
|
|
20
|
-
const SpinnerLine = ({ label, frame }) => (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#565f89", children:
|
|
21
|
+
const SpinnerLine = ({ label, frame }) => (_jsxs(Text, { children: [' ', _jsx(Text, { color: "#565f89", children: BAR_CHARS[Math.floor(frame / 3) % BAR_CHARS.length] }), ' ', _jsx(Text, { color: "#565f89", children: label })] }));
|
|
21
22
|
const BAR_W = 20;
|
|
22
23
|
const MAX_VIS = 4;
|
|
23
24
|
const Bar = ({ done, total }) => {
|
|
@@ -76,8 +77,14 @@ const App = () => {
|
|
|
76
77
|
_cardResolve = null;
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
if (key.return && !s.card && _enterResolve) {
|
|
81
|
+
const r = _enterResolve;
|
|
82
|
+
_enterResolve = null;
|
|
83
|
+
r();
|
|
84
|
+
}
|
|
79
85
|
if (key.ctrl && input === 'c') {
|
|
80
86
|
_cardResolve?.('skip');
|
|
87
|
+
_enterResolve?.();
|
|
81
88
|
exit();
|
|
82
89
|
process.stdout.write('\x1B[?25h');
|
|
83
90
|
process.exit(0);
|
|
@@ -89,6 +96,7 @@ const App = () => {
|
|
|
89
96
|
let _inst = null;
|
|
90
97
|
let _started = false;
|
|
91
98
|
const _origLog = console.log.bind(console);
|
|
99
|
+
let _seedLines = [];
|
|
92
100
|
function ensureStarted() {
|
|
93
101
|
if (_started)
|
|
94
102
|
return;
|
|
@@ -96,6 +104,15 @@ function ensureStarted() {
|
|
|
96
104
|
process.stdout.write('\x1Bc'); // clear screen
|
|
97
105
|
process.stdout.write('\x1B[?25l'); // hide cursor
|
|
98
106
|
_inst = inkRender(React.createElement(App));
|
|
107
|
+
// Seed initial lines (e.g. greeting from intro)
|
|
108
|
+
if (_seedLines.length) {
|
|
109
|
+
const lines = _seedLines;
|
|
110
|
+
_seedLines = [];
|
|
111
|
+
// Small delay to let React mount before pushing state
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
_update?.(p => ({ ...p, lines: [...lines, ...p.lines] }));
|
|
114
|
+
}, 50);
|
|
115
|
+
}
|
|
99
116
|
// Redirect console.log through Ink
|
|
100
117
|
console.log = (...args) => {
|
|
101
118
|
const text = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
|
|
@@ -105,7 +122,11 @@ function ensureStarted() {
|
|
|
105
122
|
};
|
|
106
123
|
}
|
|
107
124
|
// ─── Exports ──────────────────────────────────────
|
|
108
|
-
export function initInk() {
|
|
125
|
+
export function initInk(seedLines) {
|
|
126
|
+
if (seedLines)
|
|
127
|
+
_seedLines = seedLines;
|
|
128
|
+
ensureStarted();
|
|
129
|
+
}
|
|
109
130
|
export function destroy() {
|
|
110
131
|
console.log = _origLog;
|
|
111
132
|
_inst?.unmount();
|
|
@@ -162,3 +183,8 @@ export function showProgress(phase, done, total, items, thinking) {
|
|
|
162
183
|
export function hideProgress() {
|
|
163
184
|
_update?.(p => ({ ...p, progress: null }));
|
|
164
185
|
}
|
|
186
|
+
// ── Wait for Enter ──
|
|
187
|
+
export function waitForEnter() {
|
|
188
|
+
ensureStarted();
|
|
189
|
+
return new Promise(resolve => { _enterResolve = resolve; });
|
|
190
|
+
}
|
package/dist/ui/progress.js
CHANGED
|
@@ -71,7 +71,8 @@ export class InkProgress {
|
|
|
71
71
|
const t = this.tools.find(t => t.name === name && !t.done);
|
|
72
72
|
if (t)
|
|
73
73
|
t.done = true;
|
|
74
|
-
|
|
74
|
+
// Delay repaint so the label lingers on screen
|
|
75
|
+
setTimeout(() => this.paint(), 400);
|
|
75
76
|
}
|
|
76
77
|
workerStart(id, label) {
|
|
77
78
|
this._thinking = false;
|
|
@@ -113,6 +114,9 @@ export class InkProgress {
|
|
|
113
114
|
}
|
|
114
115
|
pause() { this._paused = true; hideProgress(); }
|
|
115
116
|
resume() { this._paused = false; this.paint(); }
|
|
116
|
-
stop() {
|
|
117
|
+
stop() {
|
|
118
|
+
// Let the final state linger so it doesn't vanish instantly
|
|
119
|
+
setTimeout(() => hideProgress(), 800);
|
|
120
|
+
}
|
|
117
121
|
clear() { hideProgress(); }
|
|
118
122
|
}
|
package/dist/ui/theme.js
CHANGED
|
@@ -51,7 +51,7 @@ export const W = () => Math.min(process.stdout.columns || 80, 80);
|
|
|
51
51
|
export const INNER = () => W() - 4;
|
|
52
52
|
// Spinner frames
|
|
53
53
|
export const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
54
|
-
export const BAR_CHARS = ['✶', '*', '✢'];
|
|
54
|
+
export const BAR_CHARS = ['✶', '*', '✢', '·', '✦', '⊹', '✧'];
|
|
55
55
|
// Progress label map — personality, not developer console
|
|
56
56
|
export const TOOL_LABEL = {
|
|
57
57
|
search_all_messages: 'reading the room',
|