life-pulse 1.0.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/dist/agent.d.ts +11 -0
- package/dist/agent.js +435 -0
- package/dist/analyze.d.ts +28 -0
- package/dist/analyze.js +130 -0
- package/dist/auq.d.ts +15 -0
- package/dist/auq.js +61 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/collectors/apps.d.ts +5 -0
- package/dist/collectors/apps.js +59 -0
- package/dist/collectors/calendar.d.ts +2 -0
- package/dist/collectors/calendar.js +115 -0
- package/dist/collectors/calls.d.ts +2 -0
- package/dist/collectors/calls.js +52 -0
- package/dist/collectors/chrome.d.ts +2 -0
- package/dist/collectors/chrome.js +49 -0
- package/dist/collectors/findmy.d.ts +2 -0
- package/dist/collectors/findmy.js +67 -0
- package/dist/collectors/imessage.d.ts +2 -0
- package/dist/collectors/imessage.js +125 -0
- package/dist/collectors/mail.d.ts +2 -0
- package/dist/collectors/mail.js +49 -0
- package/dist/collectors/notes.d.ts +2 -0
- package/dist/collectors/notes.js +42 -0
- package/dist/collectors/notifications.d.ts +2 -0
- package/dist/collectors/notifications.js +37 -0
- package/dist/collectors/recent-files.d.ts +2 -0
- package/dist/collectors/recent-files.js +46 -0
- package/dist/collectors/safari.d.ts +2 -0
- package/dist/collectors/safari.js +85 -0
- package/dist/collectors/screen-time.d.ts +2 -0
- package/dist/collectors/screen-time.js +72 -0
- package/dist/collectors/shell-history.d.ts +2 -0
- package/dist/collectors/shell-history.js +44 -0
- package/dist/contacts.d.ts +7 -0
- package/dist/contacts.js +88 -0
- package/dist/db.d.ts +9 -0
- package/dist/db.js +50 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +42 -0
- package/dist/profile.d.ts +18 -0
- package/dist/profile.js +88 -0
- package/dist/progress.d.ts +40 -0
- package/dist/progress.js +204 -0
- package/dist/state.d.ts +18 -0
- package/dist/state.js +101 -0
- package/dist/todo.d.ts +21 -0
- package/dist/todo.js +133 -0
- package/dist/tools.d.ts +22 -0
- package/dist/tools.js +1037 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +2 -0
- package/package.json +38 -0
package/dist/contacts.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact name resolution from macOS AddressBook.
|
|
3
|
+
* Builds a phone/email → name lookup by reading SQLite databases.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { existsSync, readdirSync } from 'fs';
|
|
8
|
+
import { openDb, safeQuery } from './db.js';
|
|
9
|
+
let _cache = null;
|
|
10
|
+
function normalizePhone(raw) {
|
|
11
|
+
const digits = raw.replace(/\D/g, '');
|
|
12
|
+
return digits.length >= 10 ? digits.slice(-10) : digits;
|
|
13
|
+
}
|
|
14
|
+
export function buildContactMap() {
|
|
15
|
+
if (_cache)
|
|
16
|
+
return _cache;
|
|
17
|
+
const map = new Map();
|
|
18
|
+
const abDir = join(homedir(), 'Library/Application Support/AddressBook/Sources');
|
|
19
|
+
if (!existsSync(abDir)) {
|
|
20
|
+
_cache = map;
|
|
21
|
+
return map;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const sources = readdirSync(abDir);
|
|
25
|
+
for (const source of sources) {
|
|
26
|
+
const dbPath = join(abDir, source, 'AddressBook-v22.abcddb');
|
|
27
|
+
const db = openDb(dbPath);
|
|
28
|
+
if (!db)
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
// Phone numbers
|
|
32
|
+
const phones = safeQuery(db, `SELECT r.ZFIRSTNAME as first, r.ZLASTNAME as last, p.ZFULLNUMBER as phone
|
|
33
|
+
FROM ZABCDRECORD r
|
|
34
|
+
JOIN ZABCDPHONENUMBER p ON p.ZOWNER = r.Z_PK
|
|
35
|
+
WHERE p.ZFULLNUMBER IS NOT NULL`);
|
|
36
|
+
for (const row of phones) {
|
|
37
|
+
const name = [row.first, row.last].filter(Boolean).join(' ').trim();
|
|
38
|
+
if (!name)
|
|
39
|
+
continue;
|
|
40
|
+
const normalized = normalizePhone(row.phone);
|
|
41
|
+
if (normalized.length >= 7)
|
|
42
|
+
map.set(normalized, name);
|
|
43
|
+
map.set(row.phone.trim(), name);
|
|
44
|
+
}
|
|
45
|
+
// Email addresses
|
|
46
|
+
const emails = safeQuery(db, `SELECT r.ZFIRSTNAME as first, r.ZLASTNAME as last, e.ZADDRESS as email
|
|
47
|
+
FROM ZABCDRECORD r
|
|
48
|
+
JOIN ZABCDEMAILADDRESS e ON e.ZOWNER = r.Z_PK
|
|
49
|
+
WHERE e.ZADDRESS IS NOT NULL`);
|
|
50
|
+
for (const row of emails) {
|
|
51
|
+
const name = [row.first, row.last].filter(Boolean).join(' ').trim();
|
|
52
|
+
if (name)
|
|
53
|
+
map.set(row.email.toLowerCase().trim(), name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
db.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
_cache = map;
|
|
63
|
+
return map;
|
|
64
|
+
}
|
|
65
|
+
/** Resolve a phone number or email to a contact name */
|
|
66
|
+
export function resolveName(handle) {
|
|
67
|
+
if (!handle)
|
|
68
|
+
return 'Unknown';
|
|
69
|
+
const map = buildContactMap();
|
|
70
|
+
// Direct lookup
|
|
71
|
+
if (map.has(handle))
|
|
72
|
+
return map.get(handle);
|
|
73
|
+
// Normalize phone and try
|
|
74
|
+
const normalized = normalizePhone(handle);
|
|
75
|
+
if (normalized.length >= 7 && map.has(normalized))
|
|
76
|
+
return map.get(normalized);
|
|
77
|
+
// Try without country code prefix
|
|
78
|
+
if (handle.startsWith('+1')) {
|
|
79
|
+
const without = normalizePhone(handle.slice(2));
|
|
80
|
+
if (map.has(without))
|
|
81
|
+
return map.get(without);
|
|
82
|
+
}
|
|
83
|
+
// Email lowercased
|
|
84
|
+
if (handle.includes('@') && map.has(handle.toLowerCase())) {
|
|
85
|
+
return map.get(handle.toLowerCase());
|
|
86
|
+
}
|
|
87
|
+
return handle;
|
|
88
|
+
}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Open a SQLite database safely by copying to /tmp first.
|
|
4
|
+
* Many macOS databases are locked by running processes — copying
|
|
5
|
+
* the file (+ WAL if present) avoids SQLITE_BUSY errors.
|
|
6
|
+
*/
|
|
7
|
+
export declare function openDb(dbPath: string, readonly?: boolean): Database.Database | null;
|
|
8
|
+
/** Run a query safely, returning empty array on error */
|
|
9
|
+
export declare function safeQuery<T = Record<string, unknown>>(db: Database.Database, sql: string, params?: unknown[]): T[];
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { existsSync, copyFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Open a SQLite database safely by copying to /tmp first.
|
|
8
|
+
* Many macOS databases are locked by running processes — copying
|
|
9
|
+
* the file (+ WAL if present) avoids SQLITE_BUSY errors.
|
|
10
|
+
*/
|
|
11
|
+
export function openDb(dbPath, readonly = true) {
|
|
12
|
+
if (!existsSync(dbPath))
|
|
13
|
+
return null;
|
|
14
|
+
const tmpPath = join(tmpdir(), `lp-${randomBytes(4).toString('hex')}.db`);
|
|
15
|
+
try {
|
|
16
|
+
copyFileSync(dbPath, tmpPath);
|
|
17
|
+
// Copy WAL/SHM if they exist
|
|
18
|
+
if (existsSync(dbPath + '-wal')) {
|
|
19
|
+
copyFileSync(dbPath + '-wal', tmpPath + '-wal');
|
|
20
|
+
}
|
|
21
|
+
if (existsSync(dbPath + '-shm')) {
|
|
22
|
+
copyFileSync(dbPath + '-shm', tmpPath + '-shm');
|
|
23
|
+
}
|
|
24
|
+
const db = new Database(tmpPath, { readonly });
|
|
25
|
+
// Ensure WAL checkpoint so we see latest data
|
|
26
|
+
try {
|
|
27
|
+
db.pragma('wal_checkpoint(PASSIVE)');
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
// If copy fails (permissions), try opening directly
|
|
34
|
+
try {
|
|
35
|
+
return new Database(dbPath, { readonly: true });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Run a query safely, returning empty array on error */
|
|
43
|
+
export function safeQuery(db, sql, params = []) {
|
|
44
|
+
try {
|
|
45
|
+
return db.prepare(sql).all(...params);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CollectionResult {
|
|
2
|
+
generated: string;
|
|
3
|
+
hostname: string;
|
|
4
|
+
sources: string[];
|
|
5
|
+
unavailable: string[];
|
|
6
|
+
data: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
/** Collect raw data from all macOS sources */
|
|
9
|
+
export declare function collectAll(): Promise<CollectionResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { hostname } from 'os';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
// Collectors — pure data extraction, no heuristics
|
|
4
|
+
import * as imessage from './collectors/imessage.js';
|
|
5
|
+
import * as screenTime from './collectors/screen-time.js';
|
|
6
|
+
import * as safari from './collectors/safari.js';
|
|
7
|
+
import * as chrome from './collectors/chrome.js';
|
|
8
|
+
import * as calendar from './collectors/calendar.js';
|
|
9
|
+
import * as calls from './collectors/calls.js';
|
|
10
|
+
import * as notes from './collectors/notes.js';
|
|
11
|
+
import * as notifications from './collectors/notifications.js';
|
|
12
|
+
import * as shellHistory from './collectors/shell-history.js';
|
|
13
|
+
import * as mail from './collectors/mail.js';
|
|
14
|
+
import * as findmy from './collectors/findmy.js';
|
|
15
|
+
import * as recentFiles from './collectors/recent-files.js';
|
|
16
|
+
import * as apps from './collectors/apps.js';
|
|
17
|
+
const ALL_COLLECTORS = [
|
|
18
|
+
imessage, screenTime, safari, chrome, calendar,
|
|
19
|
+
calls, notes, notifications, shellHistory, mail,
|
|
20
|
+
findmy, recentFiles, apps,
|
|
21
|
+
];
|
|
22
|
+
/** Collect raw data from all macOS sources */
|
|
23
|
+
export async function collectAll() {
|
|
24
|
+
const settled = await Promise.allSettled(ALL_COLLECTORS.map(c => c.collect()));
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const r of settled) {
|
|
27
|
+
if (r.status === 'fulfilled')
|
|
28
|
+
results.push(r.value);
|
|
29
|
+
}
|
|
30
|
+
const available = results.filter(r => r.available);
|
|
31
|
+
const unavailable = results.filter(r => !r.available);
|
|
32
|
+
const data = {};
|
|
33
|
+
for (const r of available)
|
|
34
|
+
data[r.source] = r.data;
|
|
35
|
+
return {
|
|
36
|
+
generated: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
|
37
|
+
hostname: hostname(),
|
|
38
|
+
sources: available.map(r => r.source),
|
|
39
|
+
unavailable: unavailable.map(r => r.source),
|
|
40
|
+
data,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discover user profile from system data.
|
|
3
|
+
* No config needed — derives everything from macOS databases + filesystem.
|
|
4
|
+
*/
|
|
5
|
+
export interface ContactTier {
|
|
6
|
+
name: string;
|
|
7
|
+
tier: 'T1' | 'T2' | 'T3' | 'T4';
|
|
8
|
+
msgs30d: number;
|
|
9
|
+
}
|
|
10
|
+
export interface UserProfile {
|
|
11
|
+
name: string;
|
|
12
|
+
topContacts: ContactTier[];
|
|
13
|
+
projects: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Get the real name of the current macOS user */
|
|
16
|
+
export declare function getUserName(): string;
|
|
17
|
+
/** Build full user profile from system data. Cached for session. */
|
|
18
|
+
export declare function buildProfile(): UserProfile;
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discover user profile from system data.
|
|
3
|
+
* No config needed — derives everything from macOS databases + filesystem.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { existsSync, readdirSync } from 'fs';
|
|
9
|
+
import { openDb, safeQuery } from './db.js';
|
|
10
|
+
import { resolveName } from './contacts.js';
|
|
11
|
+
import dayjs from 'dayjs';
|
|
12
|
+
const APPLE_EPOCH = 978307200;
|
|
13
|
+
let _name = null;
|
|
14
|
+
let _profile = null;
|
|
15
|
+
/** Get the real name of the current macOS user */
|
|
16
|
+
export function getUserName() {
|
|
17
|
+
if (_name)
|
|
18
|
+
return _name;
|
|
19
|
+
try {
|
|
20
|
+
const raw = execSync('dscl . -read /Users/$(whoami) RealName', {
|
|
21
|
+
encoding: 'utf-8', timeout: 3000,
|
|
22
|
+
});
|
|
23
|
+
// Output format: "RealName:\n First Last"
|
|
24
|
+
const lines = raw.trim().split('\n');
|
|
25
|
+
const name = lines.length > 1 ? lines[1].trim() : lines[0].replace('RealName:', '').trim();
|
|
26
|
+
if (name && name !== 'RealName:') {
|
|
27
|
+
_name = name;
|
|
28
|
+
return _name;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
_name = process.env.USER || 'User';
|
|
33
|
+
return _name;
|
|
34
|
+
}
|
|
35
|
+
/** Build full user profile from system data. Cached for session. */
|
|
36
|
+
export function buildProfile() {
|
|
37
|
+
if (_profile)
|
|
38
|
+
return _profile;
|
|
39
|
+
const name = getUserName();
|
|
40
|
+
const topContacts = discoverContacts();
|
|
41
|
+
const projects = discoverProjects();
|
|
42
|
+
_profile = { name, topContacts, projects };
|
|
43
|
+
return _profile;
|
|
44
|
+
}
|
|
45
|
+
function discoverContacts() {
|
|
46
|
+
const home = homedir();
|
|
47
|
+
const db = openDb(join(home, 'Library/Messages/chat.db'));
|
|
48
|
+
if (!db)
|
|
49
|
+
return [];
|
|
50
|
+
const ago30Nano = (BigInt(dayjs().subtract(30, 'day').unix() - APPLE_EPOCH) * BigInt(1e9)).toString();
|
|
51
|
+
const handles = safeQuery(db, 'SELECT ROWID, id FROM handle');
|
|
52
|
+
const counts = new Map();
|
|
53
|
+
for (const h of handles) {
|
|
54
|
+
const msgs = safeQuery(db, `SELECT COUNT(*) as c FROM message WHERE handle_id = ? AND date > ?`, [h.ROWID, ago30Nano]);
|
|
55
|
+
const count = msgs[0]?.c || 0;
|
|
56
|
+
if (count < 3)
|
|
57
|
+
continue;
|
|
58
|
+
const name = resolveName(h.id);
|
|
59
|
+
if (name === 'Unknown' || name === h.id)
|
|
60
|
+
continue;
|
|
61
|
+
counts.set(name, (counts.get(name) || 0) + count);
|
|
62
|
+
}
|
|
63
|
+
db.close();
|
|
64
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
65
|
+
return sorted.slice(0, 30).map(([name, msgs30d]) => ({
|
|
66
|
+
name,
|
|
67
|
+
tier: msgs30d > 100 ? 'T1' : msgs30d > 30 ? 'T2' : msgs30d > 10 ? 'T3' : 'T4',
|
|
68
|
+
msgs30d,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function discoverProjects() {
|
|
72
|
+
const projectsDir = join(homedir(), 'Projects');
|
|
73
|
+
if (!existsSync(projectsDir))
|
|
74
|
+
return [];
|
|
75
|
+
try {
|
|
76
|
+
return readdirSync(projectsDir).filter(f => {
|
|
77
|
+
try {
|
|
78
|
+
return existsSync(join(projectsDir, f, '.git'));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live terminal progress renderer.
|
|
3
|
+
* Shows parallel workers with individual spinners, tool calls, and elapsed times.
|
|
4
|
+
*/
|
|
5
|
+
export interface ProgressSink {
|
|
6
|
+
thinking(): void;
|
|
7
|
+
toolStart(name: string): void;
|
|
8
|
+
toolDone(name: string): void;
|
|
9
|
+
workerStart(id: string, label: string): void;
|
|
10
|
+
workerTool(id: string, tool: string): void;
|
|
11
|
+
workerDone(id: string): void;
|
|
12
|
+
workerFail(id: string): void;
|
|
13
|
+
pause(): void;
|
|
14
|
+
resume(): void;
|
|
15
|
+
}
|
|
16
|
+
export declare class ProgressRenderer implements ProgressSink {
|
|
17
|
+
private tools;
|
|
18
|
+
private workers;
|
|
19
|
+
private frame;
|
|
20
|
+
private timer;
|
|
21
|
+
private prevLines;
|
|
22
|
+
private isThinking;
|
|
23
|
+
private auqWaiting;
|
|
24
|
+
start(): void;
|
|
25
|
+
thinking(): void;
|
|
26
|
+
toolStart(name: string): void;
|
|
27
|
+
toolDone(name: string): void;
|
|
28
|
+
workerStart(id: string, label: string): void;
|
|
29
|
+
workerTool(id: string, tool: string): void;
|
|
30
|
+
workerDone(id: string): void;
|
|
31
|
+
workerFail(id: string): void;
|
|
32
|
+
private s;
|
|
33
|
+
private ms;
|
|
34
|
+
private spin;
|
|
35
|
+
private paint;
|
|
36
|
+
pause(): void;
|
|
37
|
+
resume(): void;
|
|
38
|
+
stop(): void;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live terminal progress renderer.
|
|
3
|
+
* Shows parallel workers with individual spinners, tool calls, and elapsed times.
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
|
+
const TICK_MS = 80;
|
|
8
|
+
const SHORT = {
|
|
9
|
+
search_all_messages: 'search',
|
|
10
|
+
get_conversation: 'conversation',
|
|
11
|
+
profile_contact: 'profile',
|
|
12
|
+
get_messages: 'messages',
|
|
13
|
+
get_unanswered_messages: 'unanswered',
|
|
14
|
+
get_screen_time: 'screen_time',
|
|
15
|
+
get_browsing: 'browsing',
|
|
16
|
+
get_calls: 'calls',
|
|
17
|
+
scan_sources: 'scan',
|
|
18
|
+
lookup_contact: 'lookup',
|
|
19
|
+
get_email_summary: 'email',
|
|
20
|
+
get_git_activity: 'git',
|
|
21
|
+
get_recent_files: 'files',
|
|
22
|
+
get_shell_history: 'shell',
|
|
23
|
+
get_notes: 'notes',
|
|
24
|
+
get_claude_history: 'claude',
|
|
25
|
+
get_interests_for_plans: 'interests',
|
|
26
|
+
};
|
|
27
|
+
export class ProgressRenderer {
|
|
28
|
+
tools = [];
|
|
29
|
+
workers = [];
|
|
30
|
+
frame = 0;
|
|
31
|
+
timer = null;
|
|
32
|
+
prevLines = 0;
|
|
33
|
+
isThinking = false;
|
|
34
|
+
auqWaiting = false;
|
|
35
|
+
start() {
|
|
36
|
+
process.stderr.write('\x1B[?25l');
|
|
37
|
+
this.timer = setInterval(() => {
|
|
38
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
39
|
+
this.paint();
|
|
40
|
+
}, TICK_MS);
|
|
41
|
+
}
|
|
42
|
+
thinking() { this.isThinking = true; }
|
|
43
|
+
toolStart(name) {
|
|
44
|
+
this.isThinking = false;
|
|
45
|
+
if (name === 'ask_user') {
|
|
46
|
+
this.auqWaiting = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.tools.push({ name, done: false, startTime: Date.now() });
|
|
50
|
+
}
|
|
51
|
+
toolDone(name) {
|
|
52
|
+
if (name === 'ask_user') {
|
|
53
|
+
this.auqWaiting = false;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const t = this.tools.find(t => t.name === name && !t.done);
|
|
57
|
+
if (t) {
|
|
58
|
+
t.done = true;
|
|
59
|
+
t.endTime = Date.now();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
workerStart(id, label) {
|
|
63
|
+
this.isThinking = false;
|
|
64
|
+
this.workers.push({
|
|
65
|
+
id, label: label.slice(0, 48), tool: null,
|
|
66
|
+
done: false, failed: false,
|
|
67
|
+
startTime: Date.now(), toolCount: 0,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
workerTool(id, tool) {
|
|
71
|
+
const w = this.workers.find(w => w.id === id);
|
|
72
|
+
if (w) {
|
|
73
|
+
w.tool = tool;
|
|
74
|
+
w.toolCount++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
workerDone(id) {
|
|
78
|
+
const w = this.workers.find(w => w.id === id);
|
|
79
|
+
if (w) {
|
|
80
|
+
w.done = true;
|
|
81
|
+
w.endTime = Date.now();
|
|
82
|
+
w.tool = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
workerFail(id) {
|
|
86
|
+
const w = this.workers.find(w => w.id === id);
|
|
87
|
+
if (w) {
|
|
88
|
+
w.done = true;
|
|
89
|
+
w.failed = true;
|
|
90
|
+
w.endTime = Date.now();
|
|
91
|
+
w.tool = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
s(name) { return SHORT[name] || name; }
|
|
95
|
+
ms(t) { return (t / 1000).toFixed(1) + 's'; }
|
|
96
|
+
spin(offset = 0) { return chalk.cyan(FRAMES[(this.frame + offset) % FRAMES.length]); }
|
|
97
|
+
paint() {
|
|
98
|
+
const lines = [''];
|
|
99
|
+
lines.push(chalk.bold(' life pulse ') + chalk.dim('─'.repeat(34)));
|
|
100
|
+
lines.push('');
|
|
101
|
+
// Direct tools
|
|
102
|
+
const allDone = this.tools.length > 0 && this.tools.every(t => t.done);
|
|
103
|
+
if (this.tools.length > 0) {
|
|
104
|
+
if (allDone) {
|
|
105
|
+
const names = this.tools.map(t => this.s(t.name));
|
|
106
|
+
const maxMs = Math.max(...this.tools.map(t => (t.endTime - t.startTime)));
|
|
107
|
+
lines.push(` ${chalk.green('✓')} ${chalk.dim(names.join(' · '))} ${chalk.dim(this.ms(maxMs))}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
for (let i = 0; i < this.tools.length; i++) {
|
|
111
|
+
const t = this.tools[i];
|
|
112
|
+
if (t.done) {
|
|
113
|
+
lines.push(` ${chalk.green('✓')} ${chalk.dim(this.s(t.name))}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push(` ${this.spin(i)} ${this.s(t.name)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
// Workers
|
|
123
|
+
if (this.workers.length > 0) {
|
|
124
|
+
const done = this.workers.filter(w => w.done).length;
|
|
125
|
+
const total = this.workers.length;
|
|
126
|
+
const active = total - done;
|
|
127
|
+
const hdr = active > 0
|
|
128
|
+
? ` ${chalk.dim('workers')} ${chalk.cyan(String(done))}${chalk.dim('/')}${chalk.white(String(total))}`
|
|
129
|
+
: ` ${chalk.dim('workers')} ${chalk.green(total + '/' + total)}`;
|
|
130
|
+
lines.push(hdr);
|
|
131
|
+
for (let i = 0; i < this.workers.length; i++) {
|
|
132
|
+
const w = this.workers[i];
|
|
133
|
+
if (w.done) {
|
|
134
|
+
const el = this.ms((w.endTime - w.startTime));
|
|
135
|
+
const icon = w.failed ? chalk.red('✗') : chalk.green('✓');
|
|
136
|
+
lines.push(` ${icon} ${chalk.dim(w.label)} ${chalk.dim(w.toolCount + ' calls')} ${chalk.dim(el)}`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const tool = w.tool ? chalk.dim(` → ${this.s(w.tool)}`) : '';
|
|
140
|
+
lines.push(` ${this.spin(i * 3)} ${chalk.white(w.label)}${tool}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
// AUQ waiting for user answers
|
|
146
|
+
if (this.auqWaiting) {
|
|
147
|
+
lines.push(` ${this.spin()} ${chalk.yellow('waiting for answers')} ${chalk.dim("— run 'auq' in another terminal")}`);
|
|
148
|
+
lines.push('');
|
|
149
|
+
}
|
|
150
|
+
// Thinking / synthesizing
|
|
151
|
+
if (this.isThinking) {
|
|
152
|
+
const hasWorkers = this.workers.length > 0 && this.workers.every(w => w.done);
|
|
153
|
+
const label = hasWorkers ? 'synthesizing decisions...' : 'thinking...';
|
|
154
|
+
lines.push(` ${this.spin()} ${chalk.dim(label)}`);
|
|
155
|
+
lines.push('');
|
|
156
|
+
}
|
|
157
|
+
// Clear previous render
|
|
158
|
+
if (this.prevLines > 0) {
|
|
159
|
+
process.stderr.write(`\x1B[${this.prevLines}A`);
|
|
160
|
+
for (let i = 0; i < this.prevLines; i++) {
|
|
161
|
+
process.stderr.write('\x1B[2K\n');
|
|
162
|
+
}
|
|
163
|
+
process.stderr.write(`\x1B[${this.prevLines}A`);
|
|
164
|
+
}
|
|
165
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
166
|
+
this.prevLines = lines.length;
|
|
167
|
+
}
|
|
168
|
+
pause() {
|
|
169
|
+
if (this.timer) {
|
|
170
|
+
clearInterval(this.timer);
|
|
171
|
+
this.timer = null;
|
|
172
|
+
}
|
|
173
|
+
this.clear();
|
|
174
|
+
process.stderr.write('\x1B[?25h');
|
|
175
|
+
}
|
|
176
|
+
resume() {
|
|
177
|
+
if (!this.timer) {
|
|
178
|
+
process.stderr.write('\x1B[?25l');
|
|
179
|
+
this.prevLines = 0;
|
|
180
|
+
this.timer = setInterval(() => {
|
|
181
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
182
|
+
this.paint();
|
|
183
|
+
}, TICK_MS);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
stop() {
|
|
187
|
+
if (this.timer) {
|
|
188
|
+
clearInterval(this.timer);
|
|
189
|
+
this.timer = null;
|
|
190
|
+
}
|
|
191
|
+
this.paint();
|
|
192
|
+
process.stderr.write('\x1B[?25h');
|
|
193
|
+
}
|
|
194
|
+
clear() {
|
|
195
|
+
if (this.prevLines > 0) {
|
|
196
|
+
process.stderr.write(`\x1B[${this.prevLines}A`);
|
|
197
|
+
for (let i = 0; i < this.prevLines; i++) {
|
|
198
|
+
process.stderr.write('\x1B[2K\n');
|
|
199
|
+
}
|
|
200
|
+
process.stderr.write(`\x1B[${this.prevLines}A`);
|
|
201
|
+
this.prevLines = 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent state for cross-run tracking.
|
|
3
|
+
* Stores previous analysis so we can surface deltas and avoid repeating ourselves.
|
|
4
|
+
*/
|
|
5
|
+
export interface RunState {
|
|
6
|
+
lastRun: string;
|
|
7
|
+
lastAnalysis: Record<string, unknown>;
|
|
8
|
+
/** Rolling history of key observations for trend detection */
|
|
9
|
+
history: {
|
|
10
|
+
date: string;
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export declare function loadState(): RunState;
|
|
16
|
+
export declare function saveState(analysis: Record<string, unknown>): void;
|
|
17
|
+
/** Build context string for LLM about what's changed since last run */
|
|
18
|
+
export declare function buildDeltaContext(): string;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent state for cross-run tracking.
|
|
3
|
+
* Stores previous analysis so we can surface deltas and avoid repeating ourselves.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import dayjs from 'dayjs';
|
|
9
|
+
const STATE_DIR = join(homedir(), 'Library/Application Support/life-pulse');
|
|
10
|
+
const STATE_FILE = join(STATE_DIR, 'state.json');
|
|
11
|
+
function defaultState() {
|
|
12
|
+
return {
|
|
13
|
+
lastRun: '',
|
|
14
|
+
lastAnalysis: {},
|
|
15
|
+
history: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function loadState() {
|
|
19
|
+
try {
|
|
20
|
+
if (existsSync(STATE_FILE)) {
|
|
21
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
return defaultState();
|
|
26
|
+
}
|
|
27
|
+
export function saveState(analysis) {
|
|
28
|
+
try {
|
|
29
|
+
if (!existsSync(STATE_DIR))
|
|
30
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
31
|
+
const prev = loadState();
|
|
32
|
+
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
|
33
|
+
// Extract key observations for history (keep last 50)
|
|
34
|
+
const newEntries = [];
|
|
35
|
+
const decisions = analysis.decisions;
|
|
36
|
+
if (decisions) {
|
|
37
|
+
for (const d of decisions) {
|
|
38
|
+
if (d.title)
|
|
39
|
+
newEntries.push({ date: now, key: 'decision', value: d.title });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const intel = analysis.intel;
|
|
43
|
+
if (intel) {
|
|
44
|
+
for (const v of intel)
|
|
45
|
+
newEntries.push({ date: now, key: 'intel', value: v });
|
|
46
|
+
}
|
|
47
|
+
// Legacy format support
|
|
48
|
+
const right_now = analysis.right_now;
|
|
49
|
+
if (right_now) {
|
|
50
|
+
for (const v of right_now)
|
|
51
|
+
newEntries.push({ date: now, key: 'right_now', value: v });
|
|
52
|
+
}
|
|
53
|
+
const history = [...prev.history, ...newEntries].slice(-50);
|
|
54
|
+
const state = { lastRun: now, lastAnalysis: analysis, history };
|
|
55
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
}
|
|
59
|
+
/** Build context string for LLM about what's changed since last run */
|
|
60
|
+
export function buildDeltaContext() {
|
|
61
|
+
const state = loadState();
|
|
62
|
+
if (!state.lastRun)
|
|
63
|
+
return '';
|
|
64
|
+
const lines = [];
|
|
65
|
+
lines.push(`\n--- PREVIOUS RUN (${state.lastRun}) ---`);
|
|
66
|
+
lines.push('Last time you presented:');
|
|
67
|
+
const a = state.lastAnalysis;
|
|
68
|
+
const decisions = a.decisions;
|
|
69
|
+
if (decisions?.length) {
|
|
70
|
+
lines.push('DECISIONS: ' + decisions.map(d => d.title || '').filter(Boolean).join(' | '));
|
|
71
|
+
}
|
|
72
|
+
const handled = a.handled;
|
|
73
|
+
if (handled?.length) {
|
|
74
|
+
lines.push('HANDLED: ' + handled.join(' | '));
|
|
75
|
+
}
|
|
76
|
+
const intel = a.intel;
|
|
77
|
+
if (intel?.length) {
|
|
78
|
+
lines.push('INTEL: ' + intel.join(' | '));
|
|
79
|
+
}
|
|
80
|
+
// Legacy fallback
|
|
81
|
+
const right_now = a.right_now;
|
|
82
|
+
if (right_now?.length) {
|
|
83
|
+
lines.push('RIGHT NOW: ' + right_now.join(' | '));
|
|
84
|
+
}
|
|
85
|
+
if (state.history.length > 5) {
|
|
86
|
+
lines.push('\nRecurring themes from past runs:');
|
|
87
|
+
// Find repeated observations
|
|
88
|
+
const counts = new Map();
|
|
89
|
+
for (const h of state.history) {
|
|
90
|
+
const key = h.value.slice(0, 60).toLowerCase();
|
|
91
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
const recurring = [...counts.entries()].filter(([, c]) => c > 1).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
94
|
+
for (const [obs, count] of recurring) {
|
|
95
|
+
lines.push(` (${count}x) ${obs}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
lines.push('--- END PREVIOUS RUN ---');
|
|
99
|
+
lines.push('IMPORTANT: Don\'t repeat the same observations. Surface what\'s NEW and what\'s CHANGED. If something was flagged before and still isn\'t resolved, escalate it.');
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|