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,49 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { openDb, safeQuery } from '../db.js';
5
+ import dayjs from 'dayjs';
6
+ const CHROME_EPOCH = 11644473600;
7
+ function findChromeDb() {
8
+ const base = join(homedir(), 'Library/Application Support/Google/Chrome');
9
+ for (const profile of ['Default', 'Profile 1']) {
10
+ const p = join(base, profile, 'History');
11
+ if (existsSync(p))
12
+ return p;
13
+ }
14
+ return null;
15
+ }
16
+ export async function collect() {
17
+ const dbPath = findChromeDb();
18
+ if (!dbPath)
19
+ return { source: 'Chrome', available: false, data: {}, insights: [], todos: [] };
20
+ const db = openDb(dbPath);
21
+ if (!db)
22
+ return { source: 'Chrome', available: false, data: {}, insights: [], todos: [] };
23
+ try {
24
+ const monthAgoChrome = (dayjs().subtract(30, 'day').unix() + CHROME_EPOCH) * 1_000_000;
25
+ const history = safeQuery(db, `SELECT title, url FROM urls
26
+ WHERE last_visit_time > ? ORDER BY last_visit_time DESC LIMIT 1000`, [monthAgoChrome]);
27
+ const domainCounts = new Map();
28
+ for (const row of history) {
29
+ try {
30
+ domainCounts.set(new URL(row.url).hostname.replace('www.', ''), (domainCounts.get(new URL(row.url).hostname.replace('www.', '')) || 0) + 1);
31
+ }
32
+ catch { }
33
+ }
34
+ db.close();
35
+ return {
36
+ source: 'Chrome',
37
+ available: true,
38
+ data: {
39
+ totalVisits: history.length,
40
+ topDomains: [...domainCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15).map(([d, c]) => ({ domain: d, visits: c })),
41
+ },
42
+ insights: [], todos: []
43
+ };
44
+ }
45
+ catch (e) {
46
+ db.close();
47
+ return { source: 'Chrome', available: false, data: { error: String(e) }, insights: [], todos: [] };
48
+ }
49
+ }
@@ -0,0 +1,2 @@
1
+ import { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,67 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { execSync } from 'child_process';
5
+ const DEVICES_PATH = join(homedir(), 'Library/Caches/com.apple.findmy.fmipcore/Devices.data');
6
+ const ITEMS_PATH = join(homedir(), 'Library/Caches/com.apple.findmy.fmipcore/Items.data');
7
+ export async function collect() {
8
+ const insights = [];
9
+ if (!existsSync(DEVICES_PATH) && !existsSync(ITEMS_PATH)) {
10
+ return { source: 'Find My', available: false, data: {}, insights: [], todos: [] };
11
+ }
12
+ try {
13
+ const devices = [];
14
+ const items = [];
15
+ if (existsSync(DEVICES_PATH)) {
16
+ try {
17
+ const out = execSync(`plutil -convert json -o - "${DEVICES_PATH}"`, { encoding: 'utf-8', timeout: 5000 });
18
+ const parsed = JSON.parse(out);
19
+ if (Array.isArray(parsed))
20
+ devices.push(...parsed);
21
+ }
22
+ catch { }
23
+ }
24
+ if (existsSync(ITEMS_PATH)) {
25
+ try {
26
+ const out = execSync(`plutil -convert json -o - "${ITEMS_PATH}"`, { encoding: 'utf-8', timeout: 5000 });
27
+ const parsed = JSON.parse(out);
28
+ if (Array.isArray(parsed))
29
+ items.push(...parsed);
30
+ }
31
+ catch { }
32
+ }
33
+ const deviceNames = devices
34
+ .filter((d) => d.name)
35
+ .map((d) => ({
36
+ name: d.name,
37
+ batteryLevel: d.batteryLevel != null ? Math.round(d.batteryLevel * 100) : null,
38
+ batteryStatus: d.batteryStatus,
39
+ }));
40
+ // Low battery warnings
41
+ const lowBattery = deviceNames.filter(d => d.batteryLevel != null && d.batteryLevel < 20);
42
+ if (lowBattery.length > 0) {
43
+ insights.push({
44
+ category: 'system',
45
+ severity: 'warning',
46
+ title: `Low battery: ${lowBattery.map(d => `${d.name} (${d.batteryLevel}%)`).join(', ')}`,
47
+ detail: 'Charge soon to avoid losing tracking'
48
+ });
49
+ }
50
+ insights.push({
51
+ category: 'system',
52
+ severity: 'info',
53
+ title: `${deviceNames.length} devices, ${items.length} items tracked`,
54
+ detail: deviceNames.map(d => d.name).join(', ')
55
+ });
56
+ return {
57
+ source: 'Find My',
58
+ available: true,
59
+ data: { devices: deviceNames, items: items.length },
60
+ insights,
61
+ todos: []
62
+ };
63
+ }
64
+ catch (e) {
65
+ return { source: 'Find My', available: false, data: { error: String(e) }, insights: [], todos: [] };
66
+ }
67
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,125 @@
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/Messages/chat.db');
7
+ const APPLE_EPOCH = 978307200;
8
+ export async function collect() {
9
+ const db = openDb(DB_PATH);
10
+ if (!db)
11
+ return { source: 'iMessage', available: false, data: {}, insights: [], todos: [] };
12
+ try {
13
+ const now = dayjs();
14
+ const monthAgoNano = BigInt(now.subtract(30, 'day').unix() - APPLE_EPOCH) * BigInt(1_000_000_000);
15
+ // Get handles
16
+ const handles = safeQuery(db, 'SELECT ROWID, id FROM handle');
17
+ const handleMap = new Map(handles.map(h => [h.ROWID, h.id]));
18
+ // Try to get display names from chat_handle_join + chat table
19
+ const chatNames = safeQuery(db, `SELECT chj.handle_id, c.display_name
20
+ FROM chat_handle_join chj
21
+ JOIN chat c ON c.ROWID = chj.chat_id
22
+ WHERE c.display_name IS NOT NULL AND c.display_name != ''`);
23
+ const nameMap = new Map();
24
+ for (const cn of chatNames) {
25
+ if (cn.display_name)
26
+ nameMap.set(cn.handle_id, cn.display_name);
27
+ }
28
+ // Messages last 7 days with text
29
+ const msgs = safeQuery(db, `SELECT text, date, is_from_me, handle_id FROM message
30
+ WHERE date > ? ORDER BY date DESC`, [monthAgoNano.toString()]);
31
+ // Per-contact aggregation
32
+ const contacts = new Map();
33
+ for (const m of msgs) {
34
+ const rawHandle = handleMap.get(m.handle_id) || `unknown-${m.handle_id}`;
35
+ const displayName = nameMap.get(m.handle_id) || resolveName(rawHandle);
36
+ const c = contacts.get(rawHandle) || {
37
+ name: displayName, sent: 0, received: 0,
38
+ lastInbound: null, lastOutbound: null, recentTexts: []
39
+ };
40
+ const dateSeconds = Number(BigInt(m.date) / BigInt(1_000_000_000)) + APPLE_EPOCH;
41
+ const when = dayjs.unix(dateSeconds).format('ddd MMM D h:mm A');
42
+ const raw = (m.text || '');
43
+ // Skip tapback reactions
44
+ if (/^(Loved|Liked|Laughed at|Disliked|Emphasized|Questioned) (".*"|".*"|an? .+)$/.test(raw.trim()))
45
+ continue;
46
+ const text = raw.slice(0, 120);
47
+ if (m.is_from_me) {
48
+ c.sent++;
49
+ if (!c.lastOutbound && text)
50
+ c.lastOutbound = { text, when };
51
+ }
52
+ else {
53
+ c.received++;
54
+ if (!c.lastInbound && text)
55
+ c.lastInbound = { text, when };
56
+ }
57
+ if (text && c.recentTexts.length < 5)
58
+ c.recentTexts.push(text.slice(0, 80));
59
+ contacts.set(rawHandle, c);
60
+ }
61
+ // Unanswered: they sent last, >24h ago, with actual content
62
+ const unanswered = [...contacts.entries()]
63
+ .filter(([, c]) => {
64
+ if (!c.lastInbound)
65
+ return false;
66
+ // Their last message is more recent than our last reply
67
+ if (c.lastOutbound) {
68
+ const inDate = msgs.find(m => !m.is_from_me && handleMap.get(m.handle_id) === [...contacts.entries()].find(([, v]) => v === c)?.[0])?.date;
69
+ const outDate = msgs.find(m => m.is_from_me && handleMap.get(m.handle_id) === [...contacts.entries()].find(([, v]) => v === c)?.[0])?.date;
70
+ if (inDate && outDate && inDate <= outDate)
71
+ return false;
72
+ }
73
+ return c.lastInbound.text.length > 0;
74
+ })
75
+ .map(([handle, c]) => ({
76
+ name: c.name,
77
+ lastMsg: c.lastInbound.text,
78
+ when: c.lastInbound.when,
79
+ conversationVolume: c.sent + c.received
80
+ }))
81
+ .sort((a, b) => b.conversationVolume - a.conversationVolume)
82
+ .slice(0, 15);
83
+ // Top contacts by volume
84
+ const topContacts = [...contacts.values()]
85
+ .sort((a, b) => (b.sent + b.received) - (a.sent + a.received))
86
+ .slice(0, 25)
87
+ .map(c => ({
88
+ name: c.name,
89
+ sent: c.sent,
90
+ received: c.received,
91
+ lastInbound: c.lastInbound,
92
+ lastOutbound: c.lastOutbound,
93
+ }));
94
+ // Hourly distribution
95
+ const hourCounts = new Array(24).fill(0);
96
+ for (const m of msgs) {
97
+ const dateSeconds = Number(BigInt(m.date) / BigInt(1_000_000_000)) + APPLE_EPOCH;
98
+ const hour = dayjs.unix(dateSeconds).hour();
99
+ hourCounts[hour]++;
100
+ }
101
+ const peakHour = hourCounts.indexOf(Math.max(...hourCounts));
102
+ const totalSent = msgs.filter(m => m.is_from_me).length;
103
+ const totalReceived = msgs.filter(m => !m.is_from_me).length;
104
+ db.close();
105
+ return {
106
+ source: 'iMessage',
107
+ available: true,
108
+ data: {
109
+ totalMessages: msgs.length,
110
+ sent: totalSent,
111
+ received: totalReceived,
112
+ uniqueContacts: contacts.size,
113
+ peakHour: `${peakHour}:00`,
114
+ topContacts,
115
+ unanswered,
116
+ lateNight: hourCounts.slice(23).concat(hourCounts.slice(0, 5)).reduce((a, b) => a + b, 0),
117
+ },
118
+ insights: [], todos: []
119
+ };
120
+ }
121
+ catch (e) {
122
+ db.close();
123
+ return { source: 'iMessage', available: false, data: { error: String(e) }, insights: [], todos: [] };
124
+ }
125
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,49 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { openDb, safeQuery } from '../db.js';
5
+ import dayjs from 'dayjs';
6
+ function findMailDb() {
7
+ for (const v of ['V10', 'V9', 'V8']) {
8
+ const p = join(homedir(), `Library/Mail/${v}/MailData/Envelope Index`);
9
+ if (existsSync(p))
10
+ return p;
11
+ }
12
+ return null;
13
+ }
14
+ export async function collect() {
15
+ const dbPath = findMailDb();
16
+ if (!dbPath)
17
+ return { source: 'Mail', available: false, data: {}, insights: [], todos: [] };
18
+ const db = openDb(dbPath);
19
+ if (!db)
20
+ return { source: 'Mail', available: false, data: {}, insights: [], todos: [] };
21
+ try {
22
+ const monthAgo = dayjs().subtract(30, 'day').unix();
23
+ const unread = safeQuery(db, 'SELECT COUNT(*) as count FROM messages WHERE read = 0');
24
+ const flagged = safeQuery(db, 'SELECT COUNT(*) as count FROM messages WHERE flagged = 1');
25
+ const recent = safeQuery(db, 'SELECT COUNT(*) as count FROM messages WHERE date_received > ?', [monthAgo]);
26
+ // Top senders (unread) — join addresses table for actual email
27
+ const topSenders = safeQuery(db, `SELECT a.address as sender, COUNT(*) as cnt
28
+ FROM messages m
29
+ JOIN addresses a ON m.sender = a.ROWID
30
+ WHERE m.read = 0
31
+ GROUP BY a.address ORDER BY cnt DESC LIMIT 15`);
32
+ db.close();
33
+ return {
34
+ source: 'Mail',
35
+ available: true,
36
+ data: {
37
+ recentCount: recent[0]?.count || 0,
38
+ unreadCount: unread[0]?.count || 0,
39
+ flaggedCount: flagged[0]?.count || 0,
40
+ topUnreadSenders: topSenders.map(s => ({ sender: s.sender, count: s.cnt })),
41
+ },
42
+ insights: [], todos: []
43
+ };
44
+ }
45
+ catch (e) {
46
+ db.close();
47
+ return { source: 'Mail', available: false, data: { error: String(e) }, insights: [], todos: [] };
48
+ }
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,42 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { openDb, safeQuery } from '../db.js';
4
+ const DB_PATH = join(homedir(), 'Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata');
5
+ const APPLE_EPOCH = 978307200;
6
+ export async function collect() {
7
+ const db = openDb(DB_PATH);
8
+ if (!db)
9
+ return { source: 'Notes', available: false, data: {}, insights: [], todos: [] };
10
+ try {
11
+ const notes = safeQuery(db, `SELECT ZTITLE2 as title,
12
+ datetime(ZMODIFICATIONDATE1 + ${APPLE_EPOCH}, 'unixepoch', 'localtime') as modified,
13
+ datetime(ZCREATIONDATE3 + ${APPLE_EPOCH}, 'unixepoch', 'localtime') as created
14
+ FROM ZICNOTEDATA WHERE ZTITLE2 IS NOT NULL
15
+ ORDER BY ZMODIFICATIONDATE1 DESC LIMIT 30`);
16
+ // Fallback column names
17
+ if (notes.length === 0) {
18
+ notes.push(...safeQuery(db, `SELECT ZTITLE as title,
19
+ datetime(ZMODIFICATIONDATE1 + ${APPLE_EPOCH}, 'unixepoch', 'localtime') as modified,
20
+ datetime(ZCREATIONDATE + ${APPLE_EPOCH}, 'unixepoch', 'localtime') as created
21
+ FROM ZICNOTEDATA WHERE ZTITLE IS NOT NULL
22
+ ORDER BY ZMODIFICATIONDATE1 DESC LIMIT 30`));
23
+ }
24
+ db.close();
25
+ return {
26
+ source: 'Notes',
27
+ available: true,
28
+ data: {
29
+ recentNotes: notes.map(n => ({
30
+ title: n.title,
31
+ modified: n.modified,
32
+ created: n.created,
33
+ }))
34
+ },
35
+ insights: [], todos: []
36
+ };
37
+ }
38
+ catch (e) {
39
+ db.close();
40
+ return { source: 'Notes', available: false, data: { error: String(e) }, insights: [], todos: [] };
41
+ }
42
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,37 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { openDb, safeQuery } from '../db.js';
4
+ import dayjs from 'dayjs';
5
+ const DB_PATH = join(homedir(), 'Library/Group Containers/group.com.apple.usernoted/db2/db');
6
+ export async function collect() {
7
+ const db = openDb(DB_PATH);
8
+ if (!db)
9
+ return { source: 'Notifications', available: false, data: {}, insights: [], todos: [] };
10
+ try {
11
+ const monthAgo = dayjs().subtract(30, 'day').unix();
12
+ const notifs = safeQuery(db, `SELECT app_id, title, body,
13
+ datetime(delivered_date, 'unixepoch', 'localtime') as delivered
14
+ FROM record WHERE delivered_date > ?
15
+ ORDER BY delivered_date DESC LIMIT 5000`, [monthAgo]);
16
+ const appCounts = new Map();
17
+ for (const n of notifs) {
18
+ const app = n.app_id || 'unknown';
19
+ appCounts.set(app, (appCounts.get(app) || 0) + 1);
20
+ }
21
+ const topApps = [...appCounts.entries()]
22
+ .sort((a, b) => b[1] - a[1])
23
+ .slice(0, 15)
24
+ .map(([bundle, count]) => ({ bundle, count }));
25
+ db.close();
26
+ return {
27
+ source: 'Notifications',
28
+ available: true,
29
+ data: { total: notifs.length, dailyAvg: Math.round(notifs.length / 30), topApps },
30
+ insights: [], todos: []
31
+ };
32
+ }
33
+ catch (e) {
34
+ db.close();
35
+ return { source: 'Notifications', available: false, data: { error: String(e) }, insights: [], todos: [] };
36
+ }
37
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,46 @@
1
+ import { execSync } from 'child_process';
2
+ import { homedir } from 'os';
3
+ import { statSync } from 'fs';
4
+ import { join, extname } from 'path';
5
+ import dayjs from 'dayjs';
6
+ export async function collect() {
7
+ try {
8
+ const downloads = execSync(`ls -t "${join(homedir(), 'Downloads')}" | head -30`, { encoding: 'utf-8', timeout: 5000 }).trim().split('\n').filter(Boolean);
9
+ const desktop = execSync(`ls -t "${join(homedir(), 'Desktop')}" | head -30`, { encoding: 'utf-8', timeout: 5000 }).trim().split('\n').filter(Boolean);
10
+ // Age of downloads
11
+ let oldCount = 0;
12
+ const downloadDetails = [];
13
+ for (const f of downloads.slice(0, 15)) {
14
+ try {
15
+ const stat = statSync(join(homedir(), 'Downloads', f));
16
+ const age = dayjs().diff(dayjs(stat.mtime), 'day');
17
+ if (age > 7)
18
+ oldCount++;
19
+ downloadDetails.push({ name: f, age: `${age}d` });
20
+ }
21
+ catch { }
22
+ }
23
+ // Extensions
24
+ const extCounts = new Map();
25
+ for (const f of downloads) {
26
+ const ext = extname(f).toLowerCase() || '(none)';
27
+ extCounts.set(ext, (extCounts.get(ext) || 0) + 1);
28
+ }
29
+ return {
30
+ source: 'Files',
31
+ available: true,
32
+ data: {
33
+ downloadsCount: downloads.length,
34
+ desktopCount: desktop.length,
35
+ oldDownloads: oldCount,
36
+ recentDownloads: downloadDetails,
37
+ desktopItems: desktop.slice(0, 15),
38
+ fileTypes: [...extCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8),
39
+ },
40
+ insights: [], todos: []
41
+ };
42
+ }
43
+ catch (e) {
44
+ return { source: 'Files', available: false, data: { error: String(e) }, insights: [], todos: [] };
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,85 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { openDb, safeQuery } from '../db.js';
4
+ import dayjs from 'dayjs';
5
+ const DB_PATH = join(homedir(), 'Library/Safari/History.db');
6
+ const APPLE_EPOCH = 978307200;
7
+ export async function collect() {
8
+ const db = openDb(DB_PATH);
9
+ if (!db)
10
+ return { source: 'Safari', available: false, data: {}, insights: [], todos: [] };
11
+ try {
12
+ const monthAgoApple = dayjs().subtract(30, 'day').unix() - APPLE_EPOCH;
13
+ const history = safeQuery(db, `SELECT hi.url, hv.title,
14
+ datetime(hv.visit_time + ${APPLE_EPOCH}, 'unixepoch', 'localtime') as visited
15
+ FROM history_visits hv
16
+ JOIN history_items hi ON hv.history_item = hi.id
17
+ WHERE hv.visit_time > ?
18
+ LIMIT 5000
19
+ ORDER BY hv.visit_time DESC`, [monthAgoApple]);
20
+ // Domain frequency
21
+ const domainCounts = new Map();
22
+ for (const row of history) {
23
+ try {
24
+ const domain = new URL(row.url).hostname.replace('www.', '');
25
+ domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
26
+ }
27
+ catch { }
28
+ }
29
+ const topDomains = [...domainCounts.entries()]
30
+ .sort((a, b) => b[1] - a[1])
31
+ .slice(0, 25)
32
+ .map(([domain, count]) => ({ domain, visits: count }));
33
+ // Search queries
34
+ const searches = [];
35
+ for (const row of history) {
36
+ try {
37
+ const u = new URL(row.url);
38
+ const q = u.searchParams.get('q') || u.searchParams.get('query') || u.searchParams.get('search_query');
39
+ if (q && (u.hostname.includes('google') || u.hostname.includes('bing') || u.hostname.includes('duckduckgo') || u.hostname.includes('youtube'))) {
40
+ searches.push({ query: q, when: row.visited });
41
+ }
42
+ }
43
+ catch { }
44
+ }
45
+ // Dedupe searches but keep chronology
46
+ const seen = new Set();
47
+ const uniqueSearches = searches.filter(s => {
48
+ const key = s.query.toLowerCase();
49
+ if (seen.has(key))
50
+ return false;
51
+ seen.add(key);
52
+ return true;
53
+ }).slice(0, 40);
54
+ // Recent page titles (interesting ones, not just google/blank)
55
+ const interestingPages = history
56
+ .filter(h => h.title && h.title.length > 10 && !h.title.startsWith('Google'))
57
+ .slice(0, 30)
58
+ .map(h => ({ title: h.title, url: new URL(h.url).hostname, when: h.visited }));
59
+ // Hourly pattern
60
+ const hourCounts = new Array(24).fill(0);
61
+ for (const h of history) {
62
+ if (h.visited) {
63
+ const hour = parseInt(h.visited.split(' ')[1]?.split(':')[0] || '0');
64
+ hourCounts[hour]++;
65
+ }
66
+ }
67
+ db.close();
68
+ return {
69
+ source: 'Safari',
70
+ available: true,
71
+ data: {
72
+ totalVisits: history.length,
73
+ topDomains,
74
+ searches: uniqueSearches,
75
+ recentPages: interestingPages,
76
+ peakBrowsingHour: `${hourCounts.indexOf(Math.max(...hourCounts))}:00`,
77
+ },
78
+ insights: [], todos: []
79
+ };
80
+ }
81
+ catch (e) {
82
+ db.close();
83
+ return { source: 'Safari', available: false, data: { error: String(e) }, insights: [], todos: [] };
84
+ }
85
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,72 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { openDb, safeQuery } from '../db.js';
4
+ import dayjs from 'dayjs';
5
+ const DB_PATH = join(homedir(), 'Library/Application Support/Knowledge/knowledgeC.db');
6
+ const APPLE_EPOCH = 978307200;
7
+ export async function collect() {
8
+ const db = openDb(DB_PATH);
9
+ if (!db)
10
+ return { source: 'Screen Time', available: false, data: {}, insights: [], todos: [] };
11
+ try {
12
+ const monthAgoApple = dayjs().subtract(30, 'day').unix() - APPLE_EPOCH;
13
+ const usage = safeQuery(db, `SELECT ZOBJECT.ZVALUESTRING as app,
14
+ (ZOBJECT.ZENDDATE - ZOBJECT.ZSTARTDATE) as duration,
15
+ datetime(ZOBJECT.ZSTARTDATE + ${APPLE_EPOCH}, 'unixepoch', 'localtime') as start_time
16
+ FROM ZOBJECT
17
+ WHERE ZSTREAMNAME = '/app/usage'
18
+ AND ZOBJECT.ZSTARTDATE > ?
19
+ AND ZOBJECT.ZVALUESTRING IS NOT NULL
20
+ ORDER BY ZOBJECT.ZSTARTDATE DESC`, [monthAgoApple]);
21
+ // Per-app totals
22
+ const appTotals = new Map();
23
+ const hourlyTotal = new Array(24).fill(0);
24
+ const dailyTotal = new Map(); // day -> seconds
25
+ for (const row of usage) {
26
+ if (!row.app || row.duration <= 0 || row.duration > 86400)
27
+ continue;
28
+ appTotals.set(row.app, (appTotals.get(row.app) || 0) + row.duration);
29
+ if (row.start_time) {
30
+ const parts = row.start_time.split(' ');
31
+ const hour = parseInt(parts[1]?.split(':')[0] || '0');
32
+ hourlyTotal[hour] += row.duration;
33
+ const day = parts[0];
34
+ dailyTotal.set(day, (dailyTotal.get(day) || 0) + row.duration);
35
+ }
36
+ }
37
+ const ranked = [...appTotals.entries()]
38
+ .sort((a, b) => b[1] - a[1])
39
+ .map(([bundle, secs]) => ({
40
+ app: bundle.replace(/^com\./, '').replace(/\./g, ' '),
41
+ bundle,
42
+ hours: +(secs / 3600).toFixed(1),
43
+ }));
44
+ const totalHours = ranked.reduce((s, r) => s + r.hours, 0);
45
+ // Late night: 11pm-5am
46
+ const lateNightHours = +([23, 0, 1, 2, 3, 4].reduce((s, h) => s + hourlyTotal[h], 0) / 3600).toFixed(1);
47
+ // Peak usage hour
48
+ const peakHour = hourlyTotal.indexOf(Math.max(...hourlyTotal));
49
+ // Daily breakdown
50
+ const dailyBreakdown = [...dailyTotal.entries()]
51
+ .sort()
52
+ .map(([day, secs]) => ({ day, hours: +(secs / 3600).toFixed(1) }));
53
+ db.close();
54
+ return {
55
+ source: 'Screen Time',
56
+ available: true,
57
+ data: {
58
+ totalHours: +totalHours.toFixed(1),
59
+ dailyAvgHours: +(totalHours / 30).toFixed(1),
60
+ lateNightHours,
61
+ peakHour: `${peakHour}:00`,
62
+ topApps: ranked.slice(0, 20),
63
+ dailyBreakdown,
64
+ },
65
+ insights: [], todos: []
66
+ };
67
+ }
68
+ catch (e) {
69
+ db.close();
70
+ return { source: 'Screen Time', available: false, data: { error: String(e) }, insights: [], todos: [] };
71
+ }
72
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectorResult } from '../types.js';
2
+ export declare function collect(): Promise<CollectorResult>;
@@ -0,0 +1,44 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ export async function collect() {
5
+ const zshPath = join(homedir(), '.zsh_history');
6
+ const bashPath = join(homedir(), '.bash_history');
7
+ const histPath = existsSync(zshPath) ? zshPath : existsSync(bashPath) ? bashPath : null;
8
+ if (!histPath)
9
+ return { source: 'Shell', available: false, data: {}, insights: [], todos: [] };
10
+ try {
11
+ const raw = readFileSync(histPath, 'utf-8');
12
+ const lines = raw.split('\n').filter(Boolean);
13
+ const commands = [];
14
+ for (const line of lines.slice(-2000)) {
15
+ const match = line.match(/^: (\d+):\d+;(.*)/);
16
+ if (match)
17
+ commands.push({ ts: parseInt(match[1]), cmd: match[2] });
18
+ else if (!line.startsWith(':'))
19
+ commands.push({ cmd: line });
20
+ }
21
+ // Frequency
22
+ const cmdCounts = new Map();
23
+ for (const c of commands) {
24
+ const base = c.cmd.split(/\s+/)[0];
25
+ if (base)
26
+ cmdCounts.set(base, (cmdCounts.get(base) || 0) + 1);
27
+ }
28
+ // Recent unique commands (last 50)
29
+ const recentUnique = [...new Set(commands.slice(-100).map(c => c.cmd))].slice(-30);
30
+ return {
31
+ source: 'Shell',
32
+ available: true,
33
+ data: {
34
+ total: commands.length,
35
+ topCommands: [...cmdCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15).map(([cmd, n]) => ({ cmd, count: n })),
36
+ recentCommands: recentUnique,
37
+ },
38
+ insights: [], todos: []
39
+ };
40
+ }
41
+ catch (e) {
42
+ return { source: 'Shell', available: false, data: { error: String(e) }, insights: [], todos: [] };
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Contact name resolution from macOS AddressBook.
3
+ * Builds a phone/email → name lookup by reading SQLite databases.
4
+ */
5
+ export declare function buildContactMap(): Map<string, string>;
6
+ /** Resolve a phone number or email to a contact name */
7
+ export declare function resolveName(handle: string | null | undefined): string;