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.
Files changed (53) hide show
  1. package/dist/agent.d.ts +11 -0
  2. package/dist/agent.js +435 -0
  3. package/dist/analyze.d.ts +28 -0
  4. package/dist/analyze.js +130 -0
  5. package/dist/auq.d.ts +15 -0
  6. package/dist/auq.js +61 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +333 -0
  9. package/dist/collectors/apps.d.ts +5 -0
  10. package/dist/collectors/apps.js +59 -0
  11. package/dist/collectors/calendar.d.ts +2 -0
  12. package/dist/collectors/calendar.js +115 -0
  13. package/dist/collectors/calls.d.ts +2 -0
  14. package/dist/collectors/calls.js +52 -0
  15. package/dist/collectors/chrome.d.ts +2 -0
  16. package/dist/collectors/chrome.js +49 -0
  17. package/dist/collectors/findmy.d.ts +2 -0
  18. package/dist/collectors/findmy.js +67 -0
  19. package/dist/collectors/imessage.d.ts +2 -0
  20. package/dist/collectors/imessage.js +125 -0
  21. package/dist/collectors/mail.d.ts +2 -0
  22. package/dist/collectors/mail.js +49 -0
  23. package/dist/collectors/notes.d.ts +2 -0
  24. package/dist/collectors/notes.js +42 -0
  25. package/dist/collectors/notifications.d.ts +2 -0
  26. package/dist/collectors/notifications.js +37 -0
  27. package/dist/collectors/recent-files.d.ts +2 -0
  28. package/dist/collectors/recent-files.js +46 -0
  29. package/dist/collectors/safari.d.ts +2 -0
  30. package/dist/collectors/safari.js +85 -0
  31. package/dist/collectors/screen-time.d.ts +2 -0
  32. package/dist/collectors/screen-time.js +72 -0
  33. package/dist/collectors/shell-history.d.ts +2 -0
  34. package/dist/collectors/shell-history.js +44 -0
  35. package/dist/contacts.d.ts +7 -0
  36. package/dist/contacts.js +88 -0
  37. package/dist/db.d.ts +9 -0
  38. package/dist/db.js +50 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.js +42 -0
  41. package/dist/profile.d.ts +18 -0
  42. package/dist/profile.js +88 -0
  43. package/dist/progress.d.ts +40 -0
  44. package/dist/progress.js +204 -0
  45. package/dist/state.d.ts +18 -0
  46. package/dist/state.js +101 -0
  47. package/dist/todo.d.ts +21 -0
  48. package/dist/todo.js +133 -0
  49. package/dist/tools.d.ts +22 -0
  50. package/dist/tools.js +1037 -0
  51. package/dist/types.d.ts +30 -0
  52. package/dist/types.js +2 -0
  53. package/package.json +38 -0
@@ -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
+ }
@@ -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;
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }