life-pulse 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +11 -0
- package/dist/agent.js +435 -0
- package/dist/analyze.d.ts +28 -0
- package/dist/analyze.js +130 -0
- package/dist/auq.d.ts +15 -0
- package/dist/auq.js +61 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/collectors/apps.d.ts +5 -0
- package/dist/collectors/apps.js +59 -0
- package/dist/collectors/calendar.d.ts +2 -0
- package/dist/collectors/calendar.js +115 -0
- package/dist/collectors/calls.d.ts +2 -0
- package/dist/collectors/calls.js +52 -0
- package/dist/collectors/chrome.d.ts +2 -0
- package/dist/collectors/chrome.js +49 -0
- package/dist/collectors/findmy.d.ts +2 -0
- package/dist/collectors/findmy.js +67 -0
- package/dist/collectors/imessage.d.ts +2 -0
- package/dist/collectors/imessage.js +125 -0
- package/dist/collectors/mail.d.ts +2 -0
- package/dist/collectors/mail.js +49 -0
- package/dist/collectors/notes.d.ts +2 -0
- package/dist/collectors/notes.js +42 -0
- package/dist/collectors/notifications.d.ts +2 -0
- package/dist/collectors/notifications.js +37 -0
- package/dist/collectors/recent-files.d.ts +2 -0
- package/dist/collectors/recent-files.js +46 -0
- package/dist/collectors/safari.d.ts +2 -0
- package/dist/collectors/safari.js +85 -0
- package/dist/collectors/screen-time.d.ts +2 -0
- package/dist/collectors/screen-time.js +72 -0
- package/dist/collectors/shell-history.d.ts +2 -0
- package/dist/collectors/shell-history.js +44 -0
- package/dist/contacts.d.ts +7 -0
- package/dist/contacts.js +88 -0
- package/dist/db.d.ts +9 -0
- package/dist/db.js +50 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +42 -0
- package/dist/profile.d.ts +18 -0
- package/dist/profile.js +88 -0
- package/dist/progress.d.ts +40 -0
- package/dist/progress.js +204 -0
- package/dist/state.d.ts +18 -0
- package/dist/state.js +101 -0
- package/dist/todo.d.ts +21 -0
- package/dist/todo.js +133 -0
- package/dist/tools.d.ts +22 -0
- package/dist/tools.js +1037 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +2 -0
- package/package.json +38 -0
package/dist/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
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,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,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,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
|
+
}
|