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
|
@@ -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,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,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,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,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,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,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,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,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,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;
|