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
package/dist/auq.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Lightweight AUQ session protocol client.
3
+ * Speaks the same file protocol as auq-mcp-server so the `auq` TUI picks up questions.
4
+ */
5
+ import { join } from 'path';
6
+ import { homedir, platform } from 'os';
7
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
8
+ import { randomUUID } from 'crypto';
9
+ const SESSION_DIR = process.env.AUQ_SESSION_DIR
10
+ || (platform() === 'darwin'
11
+ ? join(homedir(), 'Library', 'Application Support', 'auq', 'sessions')
12
+ : join(homedir(), '.local', 'share', 'auq', 'sessions'));
13
+ export function createSession(questions) {
14
+ const sessionId = randomUUID();
15
+ const dir = join(SESSION_DIR, sessionId);
16
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
17
+ const now = new Date().toISOString();
18
+ writeFileSync(join(dir, 'request.json'), JSON.stringify({
19
+ questions, sessionId, status: 'pending', timestamp: now,
20
+ }, null, 2), { mode: 0o600 });
21
+ writeFileSync(join(dir, 'status.json'), JSON.stringify({
22
+ sessionId, status: 'pending', totalQuestions: questions.length,
23
+ createdAt: now, lastModified: now,
24
+ }, null, 2), { mode: 0o600 });
25
+ return sessionId;
26
+ }
27
+ async function waitForAnswers(sessionId, timeoutMs) {
28
+ const answersPath = join(SESSION_DIR, sessionId, 'answers.json');
29
+ const statusPath = join(SESSION_DIR, sessionId, 'status.json');
30
+ const deadline = Date.now() + timeoutMs;
31
+ while (Date.now() < deadline) {
32
+ if (existsSync(statusPath)) {
33
+ try {
34
+ const st = JSON.parse(readFileSync(statusPath, 'utf-8'));
35
+ if (st.status === 'rejected' || st.status === 'abandoned' || st.status === 'timed_out')
36
+ return null;
37
+ }
38
+ catch { }
39
+ }
40
+ if (existsSync(answersPath)) {
41
+ try {
42
+ return JSON.parse(readFileSync(answersPath, 'utf-8'));
43
+ }
44
+ catch { }
45
+ }
46
+ await new Promise(r => setTimeout(r, 500));
47
+ }
48
+ return null;
49
+ }
50
+ export async function ask(questions, timeoutMs = 300_000) {
51
+ const sessionId = createSession(questions);
52
+ const answers = await waitForAnswers(sessionId, timeoutMs);
53
+ if (!answers)
54
+ return '(user did not answer — session timed out or was rejected)';
55
+ return answers.answers.map(a => {
56
+ const q = questions[a.questionIndex];
57
+ const label = q?.title || `Q${a.questionIndex + 1}`;
58
+ const value = a.selectedOptions?.join(', ') || a.selectedOption || '(no answer)';
59
+ return `${label}: ${value}`;
60
+ }).join('\n');
61
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ import { collectAll } from './index.js';
3
+ import { runAgent } from './agent.js';
4
+ import { analyzeWithLLM } from './analyze.js';
5
+ import { saveState } from './state.js';
6
+ import { ProgressRenderer } from './progress.js';
7
+ import { addTodo, resolveTodos, pruneOld, renderTodoList } from './todo.js';
8
+ import chalk from 'chalk';
9
+ import { createInterface } from 'readline';
10
+ import { readFileSync, existsSync } from 'fs';
11
+ import { join, dirname } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import { homedir } from 'os';
14
+ function loadEnvFile(path) {
15
+ if (!existsSync(path))
16
+ return;
17
+ for (const line of readFileSync(path, 'utf-8').split('\n')) {
18
+ const m = line.match(/^(\w+)=(.*)$/);
19
+ if (m && !process.env[m[1]])
20
+ process.env[m[1]] = m[2];
21
+ }
22
+ }
23
+ // Project-local .env first (dev), then ~/.config/life-pulse/.env (npm users)
24
+ loadEnvFile(join(dirname(fileURLToPath(import.meta.url)), '..', '.env'));
25
+ loadEnvFile(join(homedir(), '.config', 'life-pulse', '.env'));
26
+ const API_KEY = process.env.OPENROUTER_API_KEY || process.env.LLM_API_KEY || '';
27
+ const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || '';
28
+ function renderList(items, bullet) {
29
+ for (const item of items) {
30
+ console.log(` ${bullet} ${item}`);
31
+ }
32
+ }
33
+ async function main() {
34
+ const jsonMode = process.argv.includes('--json');
35
+ const rawMode = process.argv.includes('--raw');
36
+ const legacyMode = process.argv.includes('--legacy');
37
+ const key = process.argv.find(a => a.startsWith('--key='))?.split('=')[1] || API_KEY;
38
+ if (rawMode) {
39
+ const collected = await collectAll();
40
+ console.log(JSON.stringify(collected, null, 2));
41
+ return;
42
+ }
43
+ if (!key && !ANTHROPIC_KEY) {
44
+ console.log(chalk.red('\n\n No API key. Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY.\n'));
45
+ process.exit(1);
46
+ }
47
+ // Legacy mode: single-shot LLM call (old behavior)
48
+ if (legacyMode) {
49
+ if (!jsonMode)
50
+ process.stdout.write(chalk.dim('\n scanning...'));
51
+ const collected = await collectAll();
52
+ if (!jsonMode)
53
+ process.stdout.write(chalk.dim(` ${collected.sources.length} sources → thinking...`));
54
+ const analysis = await analyzeWithLLM(collected.data, key);
55
+ if (jsonMode) {
56
+ console.log(JSON.stringify({ collected, analysis }, null, 2));
57
+ return;
58
+ }
59
+ renderAnalysis(analysis, collected.sources.length, collected.generated);
60
+ saveState(analysis);
61
+ return;
62
+ }
63
+ // Agent mode (default): multi-turn investigation with Anthropic
64
+ if (!ANTHROPIC_KEY) {
65
+ console.log(chalk.red('\n\n Agent mode requires ANTHROPIC_API_KEY. Use --legacy for OpenRouter mode.\n'));
66
+ process.exit(1);
67
+ }
68
+ const renderer = jsonMode ? undefined : new ProgressRenderer();
69
+ renderer?.start();
70
+ // Track streamed cards so we don't double-show from final output
71
+ const streamedTitles = new Set();
72
+ let cardCount = 0;
73
+ let headerShown = false;
74
+ const onCard = jsonMode ? undefined : async (card) => {
75
+ if (!headerShown) {
76
+ console.log();
77
+ console.log(chalk.bold(' LIFE PULSE'));
78
+ console.log();
79
+ headerShown = true;
80
+ }
81
+ cardCount++;
82
+ streamedTitles.add(card.title);
83
+ return presentCard(card, cardCount);
84
+ };
85
+ const analysis = await runAgent(ANTHROPIC_KEY, renderer, onCard);
86
+ renderer?.stop();
87
+ renderer?.clear();
88
+ if (jsonMode) {
89
+ console.log(JSON.stringify(analysis, null, 2));
90
+ return;
91
+ }
92
+ // Resolve previous todos the agent confirmed done
93
+ if (analysis.resolved_todos?.length)
94
+ resolveTodos(analysis.resolved_todos);
95
+ pruneOld();
96
+ // Show header if no cards were streamed
97
+ if (!headerShown) {
98
+ console.log();
99
+ console.log(chalk.bold(' LIFE PULSE'));
100
+ console.log();
101
+ }
102
+ // Greeting from final synthesis
103
+ if (analysis.greeting) {
104
+ console.log(` ${analysis.greeting}`);
105
+ console.log();
106
+ }
107
+ // Walk any remaining decisions not already streamed
108
+ const remaining = (analysis.decisions || []).filter(d => !streamedTitles.has(d.title));
109
+ if (remaining.length) {
110
+ await walkDecisions(remaining);
111
+ }
112
+ // Add handled items as done todos
113
+ for (const h of (analysis.handled || [])) {
114
+ addTodo(h, 'handled', 'today', true);
115
+ }
116
+ // Upcoming
117
+ if (analysis.upcoming?.length) {
118
+ console.log(chalk.bold.cyan(' UPCOMING'));
119
+ console.log();
120
+ renderList(analysis.upcoming, chalk.cyan('▸'));
121
+ console.log();
122
+ }
123
+ // Intel
124
+ if (analysis.intel?.length) {
125
+ console.log(chalk.bold.magenta(' INTEL'));
126
+ console.log();
127
+ renderList(analysis.intel, chalk.magenta('~'));
128
+ console.log();
129
+ }
130
+ // Full todo list
131
+ renderTodoList();
132
+ saveState(analysis);
133
+ }
134
+ function renderCard(d) {
135
+ const urgencyColor = d.urgency === 'now' ? chalk.red : d.urgency === 'today' ? chalk.yellow : chalk.dim;
136
+ const tag = d.fyi ? chalk.dim('[fyi]') : urgencyColor(`[${d.urgency}]`);
137
+ console.log(` ${d.title || d.ask} ${tag}`);
138
+ if (d.fyi) {
139
+ if (d.context)
140
+ console.log(` ${chalk.dim('── ' + d.context)}`);
141
+ }
142
+ else if (d.options?.length) {
143
+ for (let j = 0; j < d.options.length; j++) {
144
+ const letter = String.fromCharCode(97 + j);
145
+ const opt = d.options[j];
146
+ const marker = j === 0 ? chalk.green('▸') : ' ';
147
+ console.log(` ${marker}${chalk.bold(letter)}) ${opt.label} ${chalk.dim('— ' + opt.description)}`);
148
+ }
149
+ }
150
+ else {
151
+ // Legacy fallback
152
+ if (d.context)
153
+ console.log(` ${chalk.dim(d.context)}`);
154
+ if (d.draft)
155
+ console.log(` ${chalk.cyan('draft:')} ${d.draft}`);
156
+ }
157
+ console.log();
158
+ }
159
+ async function askOne(hint) {
160
+ if (!process.stdin.isTTY)
161
+ return '';
162
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
163
+ const suffix = hint ? chalk.dim(' (a/b/c, Enter=rec, s=skip)') : '';
164
+ return new Promise(resolve => {
165
+ rl.question(chalk.dim(' → ') + suffix, answer => {
166
+ rl.close();
167
+ resolve(answer.trim().toLowerCase());
168
+ });
169
+ });
170
+ }
171
+ async function presentCard(card, num) {
172
+ console.log(chalk.dim(` ─── ${num} ${'─'.repeat(37)}`));
173
+ console.log();
174
+ renderCard(card);
175
+ if (card.fyi || !card.options?.length) {
176
+ addTodo(card.title, 'noted', card.urgency || 'today', true);
177
+ return 'noted';
178
+ }
179
+ if (!process.stdin.isTTY) {
180
+ addTodo(card.title, card.options[0].label, card.urgency || 'today');
181
+ return card.options[0].label;
182
+ }
183
+ const answer = await askOne(num === 1);
184
+ let optIdx;
185
+ if (!answer) {
186
+ optIdx = 0;
187
+ }
188
+ else if (answer === 's' || answer === '-') {
189
+ console.log(chalk.dim(' skipped'));
190
+ console.log();
191
+ return 'skipped';
192
+ }
193
+ else {
194
+ const match = answer.match(/^([a-d])$/);
195
+ if (!match) {
196
+ console.log(chalk.dim(' skipped'));
197
+ console.log();
198
+ return 'skipped';
199
+ }
200
+ optIdx = match[1].charCodeAt(0) - 97;
201
+ }
202
+ const opt = card.options[optIdx];
203
+ if (!opt) {
204
+ console.log(chalk.dim(' skipped'));
205
+ console.log();
206
+ return 'skipped';
207
+ }
208
+ console.log(` ${chalk.green('✓')} ${opt.label}`);
209
+ console.log();
210
+ addTodo(card.title, opt.label, card.urgency || 'today');
211
+ return opt.label;
212
+ }
213
+ async function walkDecisions(decisions) {
214
+ const total = decisions.length;
215
+ let firstPrompt = true;
216
+ for (let i = 0; i < total; i++) {
217
+ const d = decisions[i];
218
+ console.log(chalk.dim(` ─── ${i + 1}/${total} ${'─'.repeat(34)}`));
219
+ console.log();
220
+ renderCard(d);
221
+ // FYI items: auto-add as done, no prompt
222
+ if (d.fyi) {
223
+ addTodo(d.title, 'noted', d.urgency || 'today', true);
224
+ continue;
225
+ }
226
+ // No options or not interactive
227
+ if (!d.options?.length || !process.stdin.isTTY)
228
+ continue;
229
+ const answer = await askOne(firstPrompt);
230
+ firstPrompt = false;
231
+ // Determine selection
232
+ let optIdx;
233
+ if (!answer) {
234
+ // Enter = pick recommendation
235
+ optIdx = 0;
236
+ }
237
+ else if (answer === 's' || answer === '-') {
238
+ console.log(chalk.dim(' skipped'));
239
+ console.log();
240
+ continue;
241
+ }
242
+ else {
243
+ const match = answer.match(/^([a-d])$/);
244
+ if (!match) {
245
+ console.log(chalk.dim(' skipped'));
246
+ console.log();
247
+ continue;
248
+ }
249
+ optIdx = match[1].charCodeAt(0) - 97;
250
+ }
251
+ const opt = d.options[optIdx];
252
+ if (!opt) {
253
+ console.log(chalk.dim(' skipped'));
254
+ console.log();
255
+ continue;
256
+ }
257
+ console.log(` ${chalk.green('✓')} ${opt.label}`);
258
+ console.log();
259
+ addTodo(d.title, opt.label, d.urgency || 'today');
260
+ }
261
+ }
262
+ function renderAnalysis(analysis, sourceCount, generated) {
263
+ console.log();
264
+ console.log(chalk.bold(' LIFE PULSE'));
265
+ console.log();
266
+ console.log(` ${analysis.greeting}`);
267
+ console.log();
268
+ // New executor format
269
+ if (analysis.decisions?.length) {
270
+ console.log(chalk.bold.red(' DECISIONS'));
271
+ console.log();
272
+ for (let i = 0; i < analysis.decisions.length; i++) {
273
+ renderCard(analysis.decisions[i]);
274
+ }
275
+ }
276
+ if (analysis.upcoming?.length) {
277
+ console.log(chalk.bold.cyan(' UPCOMING'));
278
+ console.log();
279
+ renderList(analysis.upcoming, chalk.cyan('▸'));
280
+ console.log();
281
+ }
282
+ if (analysis.handled?.length) {
283
+ console.log(chalk.bold.green(' HANDLED'));
284
+ console.log();
285
+ renderList(analysis.handled, chalk.green('✓'));
286
+ console.log();
287
+ }
288
+ if (analysis.intel?.length) {
289
+ console.log(chalk.bold.magenta(' INTEL'));
290
+ console.log();
291
+ renderList(analysis.intel, chalk.magenta('~'));
292
+ console.log();
293
+ }
294
+ // Legacy format fallback
295
+ if (analysis.right_now?.length) {
296
+ console.log(chalk.bold.red(' RIGHT NOW'));
297
+ console.log();
298
+ renderList(analysis.right_now, chalk.red('→'));
299
+ console.log();
300
+ }
301
+ if (analysis.today?.length) {
302
+ console.log(chalk.bold.yellow(' TODAY'));
303
+ console.log();
304
+ renderList(analysis.today, chalk.yellow('·'));
305
+ console.log();
306
+ }
307
+ if (analysis.this_week?.length) {
308
+ console.log(chalk.bold(' THIS WEEK'));
309
+ console.log();
310
+ renderList(analysis.this_week, chalk.dim('·'));
311
+ console.log();
312
+ }
313
+ if (analysis.heads_up?.length) {
314
+ console.log(chalk.bold.cyan(' HEADS UP'));
315
+ console.log();
316
+ renderList(analysis.heads_up, chalk.cyan('!'));
317
+ console.log();
318
+ }
319
+ if (analysis.noticed?.length) {
320
+ console.log(chalk.bold.magenta(' NOTICED'));
321
+ console.log();
322
+ renderList(analysis.noticed, chalk.magenta('~'));
323
+ console.log();
324
+ }
325
+ if (sourceCount) {
326
+ console.log(chalk.dim(` ${sourceCount} sources · ${generated}`));
327
+ console.log();
328
+ }
329
+ }
330
+ main().catch(e => {
331
+ console.error(chalk.red('\n Error:'), e.message);
332
+ process.exit(1);
333
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Collector: Installed applications via ~/Library/Application Support/
3
+ */
4
+ import type { CollectorResult } from '../types.js';
5
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Collector: Installed applications via ~/Library/Application Support/
3
+ */
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ import { readdirSync } from 'fs';
7
+ const CATEGORIES = {
8
+ 'dev tools': ['Xcode', 'JetBrains', 'Visual Studio Code', 'Windsurf', 'Zed', 'Sublime Text', 'Docker', 'TablePlus', 'Tower', 'Ghostty', 'iTerm2', 'Warp', 'Linear', 'Cursor', 'Codex'],
9
+ browsers: ['Google Chrome', 'Firefox', 'Arc', 'Brave Browser', 'Safari'],
10
+ productivity: ['Notion', 'Obsidian', 'Raycast', 'Alfred', 'Superhuman', 'Granola', 'Fantastical', '1Password', 'Bitwarden'],
11
+ messaging: ['Slack', 'Discord', 'Telegram', 'WhatsApp', 'Signal', 'Zoom', 'Microsoft Teams'],
12
+ creative: ['Figma', 'Screen Studio', 'Adobe', 'Sketch', 'Pixelmator', 'DaVinci Resolve', 'Final Cut Pro'],
13
+ finance: ['Copilot', 'YNAB', 'Quicken', 'Mint'],
14
+ ai: ['ChatGPT', 'LM Studio', 'Ollama', 'Claude'],
15
+ };
16
+ // Flatten for quick lookup: lowercase name → category
17
+ const APP_MAP = new Map();
18
+ for (const [cat, apps] of Object.entries(CATEGORIES)) {
19
+ for (const app of apps)
20
+ APP_MAP.set(app.toLowerCase(), cat);
21
+ }
22
+ export async function collect() {
23
+ const appSupport = join(homedir(), 'Library/Application Support');
24
+ let dirs;
25
+ try {
26
+ dirs = readdirSync(appSupport);
27
+ }
28
+ catch {
29
+ return { source: 'apps', available: false, data: {}, insights: [], todos: [] };
30
+ }
31
+ const found = {};
32
+ const unknown = [];
33
+ for (const dir of dirs) {
34
+ const key = dir.toLowerCase();
35
+ // Check exact match or prefix match
36
+ let matched = APP_MAP.get(key);
37
+ if (!matched) {
38
+ for (const [name, cat] of APP_MAP) {
39
+ if (key.includes(name) || name.includes(key)) {
40
+ matched = cat;
41
+ break;
42
+ }
43
+ }
44
+ }
45
+ if (matched) {
46
+ (found[matched] ||= []).push(dir);
47
+ }
48
+ }
49
+ return {
50
+ source: 'apps',
51
+ available: true,
52
+ data: {
53
+ categorized: found,
54
+ totalScanned: dirs.length,
55
+ },
56
+ insights: [],
57
+ todos: [],
58
+ };
59
+ }
@@ -0,0 +1,2 @@
1
+ import { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,115 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { openDb, safeQuery } from '../db.js';
4
+ import { APPLE_EPOCH } from '../types.js';
5
+ import dayjs from 'dayjs';
6
+ const DB_PATH = join(homedir(), 'Library/Calendars/Calendar.sqlitedb');
7
+ export async function collect() {
8
+ const db = openDb(DB_PATH);
9
+ if (!db) {
10
+ return { source: 'Calendar', available: false, data: {}, insights: [], todos: [] };
11
+ }
12
+ const insights = [];
13
+ const todos = [];
14
+ try {
15
+ const now = dayjs();
16
+ const nowApple = now.unix() - APPLE_EPOCH;
17
+ const weekAgoApple = now.subtract(7, 'day').unix() - APPLE_EPOCH;
18
+ const weekAheadApple = now.add(7, 'day').unix() - APPLE_EPOCH;
19
+ // Past week events
20
+ const pastEvents = safeQuery(db, `SELECT ci.summary, ci.start_date, ci.end_date, ci.location,
21
+ c.title as calendar_title
22
+ FROM CalendarItem ci
23
+ LEFT JOIN Calendar c ON ci.calendar_id = c.ROWID
24
+ WHERE ci.start_date BETWEEN ? AND ?
25
+ ORDER BY ci.start_date`, [weekAgoApple, nowApple]);
26
+ // Upcoming events
27
+ const upcoming = safeQuery(db, `SELECT ci.summary, ci.start_date, ci.end_date, ci.location,
28
+ c.title as calendar_title
29
+ FROM CalendarItem ci
30
+ LEFT JOIN Calendar c ON ci.calendar_id = c.ROWID
31
+ WHERE ci.start_date BETWEEN ? AND ?
32
+ ORDER BY ci.start_date`, [nowApple, weekAheadApple]);
33
+ // Meeting load analysis
34
+ let totalMeetingMinutes = 0;
35
+ for (const evt of pastEvents) {
36
+ if (evt.start_date && evt.end_date) {
37
+ const duration = (evt.end_date - evt.start_date) / 60;
38
+ if (duration > 0 && duration < 480)
39
+ totalMeetingMinutes += duration;
40
+ }
41
+ }
42
+ const meetingHoursWeek = +(totalMeetingMinutes / 60).toFixed(1);
43
+ const meetingHoursDay = +(meetingHoursWeek / 5).toFixed(1);
44
+ // Today's events
45
+ const todayStart = now.startOf('day').unix() - APPLE_EPOCH;
46
+ const todayEnd = now.endOf('day').unix() - APPLE_EPOCH;
47
+ const todayEvents = upcoming.filter(e => e.start_date >= todayStart && e.start_date <= todayEnd);
48
+ // Tomorrow's events
49
+ const tomorrowStart = now.add(1, 'day').startOf('day').unix() - APPLE_EPOCH;
50
+ const tomorrowEnd = now.add(1, 'day').endOf('day').unix() - APPLE_EPOCH;
51
+ const tomorrowEvents = upcoming.filter(e => e.start_date >= tomorrowStart && e.start_date <= tomorrowEnd);
52
+ insights.push({
53
+ category: 'work',
54
+ severity: 'info',
55
+ title: `${meetingHoursWeek}h in meetings last week`,
56
+ detail: `${meetingHoursDay}h/day avg. ${pastEvents.length} events total.`
57
+ });
58
+ if (meetingHoursDay > 4) {
59
+ insights.push({
60
+ category: 'work',
61
+ severity: 'warning',
62
+ title: 'Heavy meeting load',
63
+ detail: `${meetingHoursDay}h/day in meetings leaves limited deep work time.`
64
+ });
65
+ todos.push({
66
+ priority: 'high',
67
+ title: 'Block focus time on calendar',
68
+ reason: `${meetingHoursDay}h/day meetings — you need protected deep work blocks`,
69
+ source: 'Calendar'
70
+ });
71
+ }
72
+ if (todayEvents.length > 0) {
73
+ const nextEvent = todayEvents[0];
74
+ const startTime = dayjs.unix(nextEvent.start_date + APPLE_EPOCH);
75
+ insights.push({
76
+ category: 'work',
77
+ severity: 'info',
78
+ title: `${todayEvents.length} events today`,
79
+ detail: `Next: ${nextEvent.summary || 'Untitled'} at ${startTime.format('h:mm A')}`
80
+ });
81
+ }
82
+ if (tomorrowEvents.length > 0) {
83
+ insights.push({
84
+ category: 'work',
85
+ severity: 'info',
86
+ title: `${tomorrowEvents.length} events tomorrow`,
87
+ detail: tomorrowEvents.map(e => e.summary || 'Untitled').join(', ')
88
+ });
89
+ }
90
+ db.close();
91
+ return {
92
+ source: 'Calendar',
93
+ available: true,
94
+ data: {
95
+ meetingHoursWeek,
96
+ meetingHoursDay,
97
+ pastEventCount: pastEvents.length,
98
+ todayCount: todayEvents.length,
99
+ tomorrowCount: tomorrowEvents.length,
100
+ upcomingEvents: upcoming.slice(0, 10).map(e => ({
101
+ summary: e.summary,
102
+ start: dayjs.unix(e.start_date + APPLE_EPOCH).format('ddd h:mm A'),
103
+ location: e.location,
104
+ calendar: e.calendar_title
105
+ }))
106
+ },
107
+ insights,
108
+ todos
109
+ };
110
+ }
111
+ catch (e) {
112
+ db.close();
113
+ return { source: 'Calendar', available: false, data: { error: String(e) }, insights: [], todos: [] };
114
+ }
115
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,52 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { openDb, safeQuery } from '../db.js';
4
+ import { resolveName } from '../contacts.js';
5
+ import dayjs from 'dayjs';
6
+ const DB_PATH = join(homedir(), 'Library/Application Support/CallHistoryDB/CallHistory.storedata');
7
+ const APPLE_EPOCH = 978307200;
8
+ export async function collect() {
9
+ const db = openDb(DB_PATH);
10
+ if (!db)
11
+ return { source: 'Calls', available: false, data: {}, insights: [], todos: [] };
12
+ try {
13
+ const monthAgoApple = dayjs().subtract(30, 'day').unix() - APPLE_EPOCH;
14
+ const calls = safeQuery(db, `SELECT ZADDRESS as address, ZDURATION as duration,
15
+ ZDATE as date, ZCALLTYPE as call_type
16
+ FROM ZCALLRECORD WHERE ZDATE > ? ORDER BY ZDATE DESC`, [monthAgoApple]);
17
+ const missed = calls.filter(c => c.call_type === 3);
18
+ const incoming = calls.filter(c => c.call_type === 1);
19
+ const outgoing = calls.filter(c => c.call_type === 2);
20
+ const totalMinutes = Math.round(calls.reduce((s, c) => s + (c.duration || 0), 0) / 60);
21
+ // Missed calls not returned
22
+ const missedNotReturned = missed
23
+ .filter(m => !outgoing.some(o => o.address === m.address))
24
+ .map(m => ({
25
+ name: resolveName(m.address),
26
+ when: dayjs.unix(m.date + APPLE_EPOCH).format('ddd MMM D h:mm A')
27
+ }));
28
+ // Frequent callers
29
+ const callerCounts = new Map();
30
+ for (const c of calls)
31
+ callerCounts.set(c.address, (callerCounts.get(c.address) || 0) + 1);
32
+ const topCallers = [...callerCounts.entries()]
33
+ .sort((a, b) => b[1] - a[1])
34
+ .slice(0, 10)
35
+ .map(([addr, count]) => ({ name: resolveName(addr), calls: count }));
36
+ db.close();
37
+ return {
38
+ source: 'Calls',
39
+ available: true,
40
+ data: {
41
+ total: calls.length, incoming: incoming.length,
42
+ outgoing: outgoing.length, missed: missed.length,
43
+ totalMinutes, missedNotReturned, topCallers,
44
+ },
45
+ insights: [], todos: []
46
+ };
47
+ }
48
+ catch (e) {
49
+ db.close();
50
+ return { source: 'Calls', available: false, data: { error: String(e) }, insights: [], todos: [] };
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;