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/agent.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic analysis with Claude Opus orchestrator + sub-agent workers.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* Opus orchestrator → scans data → generates hypotheses → spawns workers
|
|
6
|
+
* Workers (Haiku) → validate specific predictions using tools → report back
|
|
7
|
+
* Opus → synthesizes validated findings into final analysis
|
|
8
|
+
*/
|
|
9
|
+
import type { Analysis } from './analyze.js';
|
|
10
|
+
import type { ProgressSink } from './progress.js';
|
|
11
|
+
export declare function runAgent(apiKey: string, progress?: ProgressSink, onCard?: (card: any) => Promise<string>): Promise<Analysis>;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic analysis with Claude Opus orchestrator + sub-agent workers.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* Opus orchestrator → scans data → generates hypotheses → spawns workers
|
|
6
|
+
* Workers (Haiku) → validate specific predictions using tools → report back
|
|
7
|
+
* Opus → synthesizes validated findings into final analysis
|
|
8
|
+
*/
|
|
9
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
10
|
+
import { executeTool, TOOLS } from './tools.js';
|
|
11
|
+
import { buildDeltaContext } from './state.js';
|
|
12
|
+
import { buildTodoContext } from './todo.js';
|
|
13
|
+
import { getUserName, buildProfile } from './profile.js';
|
|
14
|
+
import { ask } from './auq.js';
|
|
15
|
+
const MAX_ORCHESTRATOR_TURNS = 15;
|
|
16
|
+
const MAX_WORKER_TURNS = 8;
|
|
17
|
+
const ORCHESTRATOR_MODEL = 'claude-opus-4-6';
|
|
18
|
+
const WORKER_MODEL = 'claude-haiku-4-5-20251001';
|
|
19
|
+
const MAX_RESULT_LEN = 8000;
|
|
20
|
+
const MAX_WORKER_RESULT_LEN = 4000;
|
|
21
|
+
function truncateResult(result, max = MAX_RESULT_LEN) {
|
|
22
|
+
if (result.length <= max)
|
|
23
|
+
return result;
|
|
24
|
+
return result.slice(0, max) + `\n...(truncated from ${result.length} chars)`;
|
|
25
|
+
}
|
|
26
|
+
function buildOrchestratorPrompt() {
|
|
27
|
+
const delta = buildDeltaContext();
|
|
28
|
+
const todos = buildTodoContext();
|
|
29
|
+
const profile = buildProfile();
|
|
30
|
+
const name = profile.name;
|
|
31
|
+
// Build contact tiers dynamically
|
|
32
|
+
const tierLines = [];
|
|
33
|
+
const byTier = { T1: [], T2: [], T3: [], T4: [] };
|
|
34
|
+
for (const c of profile.topContacts)
|
|
35
|
+
byTier[c.tier].push(c.name);
|
|
36
|
+
if (byTier.T1.length)
|
|
37
|
+
tierLines.push(`T1 (always make time): ${byTier.T1.join(', ')}`);
|
|
38
|
+
if (byTier.T2.length)
|
|
39
|
+
tierLines.push(`T2 (high priority): ${byTier.T2.join(', ')}`);
|
|
40
|
+
if (byTier.T3.length)
|
|
41
|
+
tierLines.push(`T3 (respond same-day): ${byTier.T3.join(', ')}`);
|
|
42
|
+
if (byTier.T4.length)
|
|
43
|
+
tierLines.push(`T4 (respond when free): ${byTier.T4.join(', ')}`);
|
|
44
|
+
const contactSection = tierLines.length > 0
|
|
45
|
+
? `\nINNER CIRCLE (relationship tier → how much prep you do):\n${tierLines.join('\n')}`
|
|
46
|
+
: '';
|
|
47
|
+
const projectSection = profile.projects.length > 0
|
|
48
|
+
? `\n- Active projects: ${profile.projects.join(', ')}`
|
|
49
|
+
: '';
|
|
50
|
+
return `You are ${name}'s chief of staff. You don't brief them — you HANDLE things and present decisions.
|
|
51
|
+
|
|
52
|
+
THE STANDARD: For every item you produce, ask: "Did I do the work, or am I asking ${name} to do the work?" If the answer is the second one, DO THE WORK FIRST. Their reply to any item should be: yes, no, or a 5-word correction.
|
|
53
|
+
|
|
54
|
+
WHO ${name.toUpperCase()} IS:
|
|
55
|
+
- Uses iMessage as primary communication${projectSection}
|
|
56
|
+
${contactSection}
|
|
57
|
+
|
|
58
|
+
═══ EXECUTOR PROTOCOL ═══
|
|
59
|
+
|
|
60
|
+
STAGE 1: SCAN
|
|
61
|
+
- Call scan_sources, get_unanswered_messages, get_screen_time(days=1), get_browsing(searches_only=true), get_calls(missed_only=true)
|
|
62
|
+
|
|
63
|
+
STAGE 2: CHASE CONTEXT
|
|
64
|
+
- Every reference to plans, events, people, dates, money → CHASE IT DOWN
|
|
65
|
+
- Use search_all_messages with keyword variations: ["thursday","thurs","tmrw","tomorrow","this week","dinner","drinks","venmo","zelle","split","pay","owe"]
|
|
66
|
+
- Cross-reference across ALL conversations
|
|
67
|
+
|
|
68
|
+
STAGE 3: DISPATCH WORKERS
|
|
69
|
+
- Workers don't just validate — they DO THE WORK:
|
|
70
|
+
* Draft reply messages (exact text ${name} can send)
|
|
71
|
+
* Research options (flights, restaurants, venues — present top 3)
|
|
72
|
+
* Calculate money owed (exact amounts, who owes whom)
|
|
73
|
+
* Build timelines (what's happening when, conflicts)
|
|
74
|
+
* Triage: determine if something needs ${name}'s attention or can be handled silently
|
|
75
|
+
- Spawn 3-5 workers per batch
|
|
76
|
+
- ALWAYS include search_all_messages, get_conversation, profile_contact in worker tools
|
|
77
|
+
|
|
78
|
+
STAGE 4: SYNTHESIZE INTO DECISIONS
|
|
79
|
+
- Turn worker reports into DECISIONS, not observations
|
|
80
|
+
- Every item = something ${name} can say yes/no to in 5 seconds
|
|
81
|
+
- Include drafted replies they can send as-is
|
|
82
|
+
- Cap: max 3 right_now, max 4 today, max 3 this_week
|
|
83
|
+
|
|
84
|
+
═══ WHAT TO NEVER DO ═══
|
|
85
|
+
- NEVER narrate data back ("You spent 3 hours on Instagram")
|
|
86
|
+
- NEVER give wellness advice ("Remember to take breaks")
|
|
87
|
+
- NEVER hedge ("Maybe consider", "You might want to", "If you're thinking about")
|
|
88
|
+
- NEVER list things without doing the prep work first
|
|
89
|
+
- NEVER include reactions (Loved, Liked, Laughed at) — they're not messages
|
|
90
|
+
- NEVER include unvalidated predictions
|
|
91
|
+
- NEVER produce more than 10 total items across all sections
|
|
92
|
+
- NEVER assume a message in a group chat is for ${name} — re-read the thread to determine who's being addressed. If two people are coordinating with each other and ${name} just introduced them, that's NOT ${name}'s action item. Wrong attribution = embarrassing draft reply = trust destroyed.
|
|
93
|
+
|
|
94
|
+
═══ WHAT TO ALWAYS DO ═══
|
|
95
|
+
- Draft the reply text when telling them to respond to someone
|
|
96
|
+
- Include exact amounts for money items
|
|
97
|
+
- Include exact times/venues for plans
|
|
98
|
+
- Use relationship tier to decide urgency (T1 = interrupt them, T4 = batch for later)
|
|
99
|
+
- Present decisions, not todo lists
|
|
100
|
+
- If something can be handled without their input, just note it in "handled"
|
|
101
|
+
|
|
102
|
+
═══ INTERACTIVE QUESTIONS ═══
|
|
103
|
+
Use ask_user when you need input that can't be derived from data:
|
|
104
|
+
- First run: ask about work focus, key relationships the data might miss, preferences
|
|
105
|
+
- Plan proposals: present 2-3 concrete options for the user to pick from
|
|
106
|
+
- Ambiguity: when data suggests multiple interpretations, ask rather than guess
|
|
107
|
+
The user answers in a separate terminal (auq TUI). Keep questions focused — max 3 per call.
|
|
108
|
+
${delta ? '\n' + delta : ''}${todos ? '\n' + todos : ''}`;
|
|
109
|
+
}
|
|
110
|
+
function buildWorkerSystem() {
|
|
111
|
+
const name = getUserName();
|
|
112
|
+
return `You are an execution worker for ${name}'s chief of staff system. You don't just investigate — you DO THE WORK so ${name} only has to say yes or no.
|
|
113
|
+
|
|
114
|
+
YOUR JOB: Investigate the task, then produce READY-TO-USE output. Not suggestions. Not recommendations. Actual drafts, calculations, timelines.
|
|
115
|
+
|
|
116
|
+
INVESTIGATION RULES:
|
|
117
|
+
- Be THOROUGH. Search for related keywords across ALL conversations, not just one thread.
|
|
118
|
+
- Chase context: "Thursday" → also search "thurs", "th night", "tmrw", "tomorrow"
|
|
119
|
+
- Profile every person with profile_contact to understand relationship tier and flexibility.
|
|
120
|
+
- Filter out iMessage reactions (Loved, Liked, etc.) — NOT real messages.
|
|
121
|
+
- Cross-reference: check if multiple people discuss the same event/plan.
|
|
122
|
+
|
|
123
|
+
EXECUTION RULES:
|
|
124
|
+
- If the task involves a message that needs a reply: DRAFT THE EXACT REPLY TEXT ${name} can send.
|
|
125
|
+
- If the task involves plans: produce WHO, WHAT, WHERE, WHEN, WHO'S CONFIRMED.
|
|
126
|
+
- If the task involves money: calculate EXACT amounts owed, by whom, to whom.
|
|
127
|
+
- If the task involves scheduling: identify conflicts with other plans/events.
|
|
128
|
+
- If something doesn't need ${name}'s input: mark it as "can_handle_silently": true.
|
|
129
|
+
|
|
130
|
+
CRITICAL — GROUP CHAT AWARENESS:
|
|
131
|
+
- Before drafting a reply, determine: is the message ADDRESSED TO ${name}, or are they just in the chat?
|
|
132
|
+
- If ${name} introduced two people and they're now coordinating with each other, that's NOT ${name}'s item.
|
|
133
|
+
- Read the full thread context. Check who is replying to whom. If unsure, mark can_handle_silently: true and explain why in findings.
|
|
134
|
+
- A wrong draft reply that ${name} sends to the wrong person destroys all trust. When in doubt, skip it.
|
|
135
|
+
|
|
136
|
+
Report as JSON:
|
|
137
|
+
- "validated": true/false
|
|
138
|
+
- "findings": "What you found, with specific quotes and timestamps"
|
|
139
|
+
- "urgency": "critical" | "important" | "low" | "none"
|
|
140
|
+
- "tier": "T1" | "T2" | "T3" | "T4" | "team"
|
|
141
|
+
- "title": "Short decision card title, e.g. 'Caro texted — she just landed'"
|
|
142
|
+
- "options": [
|
|
143
|
+
{ "label": "Recommended action", "description": "What happens — include draft reply text in description if applicable" },
|
|
144
|
+
{ "label": "Alternative", "description": "What this means" },
|
|
145
|
+
{ "label": "Skip/Defer", "description": "Why this is ok to skip" }
|
|
146
|
+
]
|
|
147
|
+
- "can_handle_silently": true/false
|
|
148
|
+
- "fyi_context": "If can_handle_silently, brief explanation for the FYI line"
|
|
149
|
+
|
|
150
|
+
OPTION RULES:
|
|
151
|
+
- Always 2-3 options. Never 1, never 4+.
|
|
152
|
+
- First option = your recommendation. Make it the one you'd pick if you were them.
|
|
153
|
+
- Each option must be a COMPLETE action they're approving, not a vague direction.
|
|
154
|
+
- Include draft reply text in the description when the action involves sending a message.
|
|
155
|
+
- Last option can be "Skip" or "Defer" with a reason why it's ok.`;
|
|
156
|
+
}
|
|
157
|
+
// ─── Worker execution ────────────────────────────────────────────
|
|
158
|
+
async function runWorker(client, task, toolNames, workerId, progress) {
|
|
159
|
+
const availableTools = TOOLS.filter(t => toolNames.includes(t.name));
|
|
160
|
+
const toolSchemas = availableTools.map(t => ({
|
|
161
|
+
name: t.name,
|
|
162
|
+
description: t.description,
|
|
163
|
+
input_schema: t.parameters,
|
|
164
|
+
}));
|
|
165
|
+
const messages = [
|
|
166
|
+
{ role: 'user', content: task },
|
|
167
|
+
];
|
|
168
|
+
for (let turn = 0; turn < MAX_WORKER_TURNS; turn++) {
|
|
169
|
+
const response = await client.messages.create({
|
|
170
|
+
model: WORKER_MODEL,
|
|
171
|
+
max_tokens: 4000,
|
|
172
|
+
system: buildWorkerSystem(),
|
|
173
|
+
tools: toolSchemas,
|
|
174
|
+
messages,
|
|
175
|
+
});
|
|
176
|
+
const toolUses = response.content.filter((b) => b.type === 'tool_use');
|
|
177
|
+
const textBlocks = response.content.filter((b) => b.type === 'text');
|
|
178
|
+
if (toolUses.length > 0) {
|
|
179
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
180
|
+
const toolResults = [];
|
|
181
|
+
for (const tu of toolUses) {
|
|
182
|
+
progress?.workerTool(workerId, tu.name);
|
|
183
|
+
const result = truncateResult(await executeTool(tu.name, (tu.input || {})));
|
|
184
|
+
toolResults.push({ type: 'tool_result', tool_use_id: tu.id, content: result });
|
|
185
|
+
}
|
|
186
|
+
messages.push({ role: 'user', content: toolResults });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (textBlocks.length > 0) {
|
|
190
|
+
return textBlocks.map(b => b.text).join('\n');
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
return '{"validated": false, "findings": "Worker could not complete investigation"}';
|
|
195
|
+
}
|
|
196
|
+
// ─── Orchestrator ────────────────────────────────────────────────
|
|
197
|
+
// Build the spawn_worker tool for the orchestrator
|
|
198
|
+
const SPAWN_WORKER_TOOL = {
|
|
199
|
+
name: 'spawn_worker',
|
|
200
|
+
description: `Spawn an execution worker to DO THE WORK on a specific item. Workers investigate AND produce ready-to-use output (draft replies, calculations, timelines).
|
|
201
|
+
|
|
202
|
+
Workers EXECUTE:
|
|
203
|
+
- Draft exact reply messages the user can send as-is
|
|
204
|
+
- Calculate money owed (exact amounts, who→whom)
|
|
205
|
+
- Build plan timelines (who/what/where/when/confirmed)
|
|
206
|
+
- Profile contacts to determine urgency tier and flexibility
|
|
207
|
+
- Triage items as handle-silently vs needs-decision
|
|
208
|
+
- Cross-reference across data sources
|
|
209
|
+
|
|
210
|
+
Available tools: scan_sources, get_messages, get_unanswered_messages, get_calls, get_screen_time, get_browsing, get_email_summary, get_git_activity, get_recent_files, get_shell_history, get_notes, get_claude_history, lookup_contact, search_all_messages, profile_contact, get_interests_for_plans, get_conversation`,
|
|
211
|
+
input_schema: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
task: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'Specific task for the worker. Be detailed about what to investigate and what prediction to validate. Example: "Read the conversation with Adly Azim from the last 2 days. Profile the relationship. Determine if his last message needs an urgent reply or if it\'s casual banter."'
|
|
217
|
+
},
|
|
218
|
+
tools: {
|
|
219
|
+
type: 'array',
|
|
220
|
+
items: { type: 'string' },
|
|
221
|
+
description: 'List of tool names the worker should have access to. Only give tools relevant to the task.'
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['task', 'tools'],
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const ASK_USER_TOOL = {
|
|
228
|
+
name: 'ask_user',
|
|
229
|
+
description: `Ask the user interactive questions via the AUQ terminal UI. Use when you need input that can't be derived from data: onboarding preferences, choosing between plan options, clarifying ambiguity. The user answers in a separate terminal running \`auq\`. Max 3 questions per call.`,
|
|
230
|
+
input_schema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
questions: {
|
|
234
|
+
type: 'array',
|
|
235
|
+
items: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {
|
|
238
|
+
prompt: { type: 'string', description: 'The question to ask.' },
|
|
239
|
+
title: { type: 'string', description: 'Short label (max 12 chars).' },
|
|
240
|
+
options: {
|
|
241
|
+
type: 'array',
|
|
242
|
+
items: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: { label: { type: 'string' }, description: { type: 'string' } },
|
|
245
|
+
required: ['label'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
multiSelect: { type: 'boolean' },
|
|
249
|
+
},
|
|
250
|
+
required: ['prompt', 'title', 'options'],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: ['questions'],
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
export async function runAgent(apiKey, progress, onCard) {
|
|
258
|
+
const client = new Anthropic({ apiKey });
|
|
259
|
+
const name = getUserName();
|
|
260
|
+
let cardQueue = Promise.resolve(undefined);
|
|
261
|
+
const directTools = TOOLS.map(t => ({
|
|
262
|
+
name: t.name,
|
|
263
|
+
description: t.description,
|
|
264
|
+
input_schema: t.parameters,
|
|
265
|
+
}));
|
|
266
|
+
const allTools = [...directTools, SPAWN_WORKER_TOOL, ASK_USER_TOOL];
|
|
267
|
+
const messages = [
|
|
268
|
+
{ role: 'user', content: `Run the executor protocol. Investigate, do the prep work, present decisions.
|
|
269
|
+
|
|
270
|
+
1. SCAN: scan_sources, get_unanswered_messages, get_screen_time(days=1), get_browsing(searches_only=true), get_calls(missed_only=true)
|
|
271
|
+
|
|
272
|
+
2. CHASE CONTEXT: Search for every plan, event, date, money reference across all conversations.
|
|
273
|
+
|
|
274
|
+
3. DISPATCH WORKERS: For each item that needs attention, spawn a worker to DO THE WORK — draft replies, calculate amounts, build timelines, research options.
|
|
275
|
+
|
|
276
|
+
4. SYNTHESIZE INTO DECISION CARDS: Every item is a card with 2-3 options ${name} picks from. They should reply in under 60 seconds.
|
|
277
|
+
|
|
278
|
+
Output format (JSON only, no markdown, no code fences):
|
|
279
|
+
{
|
|
280
|
+
"greeting": "One sentence. Warm but not sappy.",
|
|
281
|
+
"handled": ["Things you triaged that don't need their input — just FYI"],
|
|
282
|
+
"decisions": [
|
|
283
|
+
{
|
|
284
|
+
"title": "Short title — who/what, e.g. 'Caro texted — she just landed'",
|
|
285
|
+
"urgency": "now | today | this_week",
|
|
286
|
+
"options": [
|
|
287
|
+
{ "label": "Recommended action", "description": "What happens if picked — include draft text for replies" },
|
|
288
|
+
{ "label": "Alternative", "description": "What this means" },
|
|
289
|
+
{ "label": "Skip/Defer", "description": "Why it's ok to skip" }
|
|
290
|
+
]
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"title": "FYI — Lexi + Jake connected for Thursday lunch",
|
|
294
|
+
"urgency": "today",
|
|
295
|
+
"fyi": true,
|
|
296
|
+
"context": "Your intro worked, they're coordinating directly. No action."
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
"upcoming": ["Calendar items, deadlines, commitments for the next 7 days with dates/times"],
|
|
300
|
+
"intel": ["Only novel cross-source patterns worth knowing — no screen time narration, no wellness advice"],
|
|
301
|
+
"resolved_todos": ["id1", "id2"]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
resolved_todos: IDs of open todo items from previous runs that you have evidence are now done (message sent, event passed, etc.). Only include IDs you are confident about.
|
|
305
|
+
|
|
306
|
+
DECISION CARD RULES:
|
|
307
|
+
- 2-3 options per card. NEVER 1, NEVER 4+. Force yourself to pick the best options.
|
|
308
|
+
- First option = YOUR RECOMMENDATION. If ${name} is in a rush they'll pick all option 1s.
|
|
309
|
+
- Each option is a COMPLETE ACTION they're approving, not a vague direction. "Research clinics" = bad. "Booked consult with Dr. Park Thursday 2pm — confirm?" = good.
|
|
310
|
+
- Include DRAFT REPLY TEXT in the description when the action is sending a message.
|
|
311
|
+
- FYI items (fyi: true) have no options — just a title + context. Use for things they should know about but don't need to act on.
|
|
312
|
+
|
|
313
|
+
HIGH-LEVERAGE DECISIONS you must make before including an item:
|
|
314
|
+
- "Is this message actually for ${name}, or are they just CC'd / in a group chat?" → If unsure, re-read the thread. If still unsure, don't include it.
|
|
315
|
+
- "Is this urgent or just recent?" → Social message from 10 min ago ≠ urgent. Bill due tomorrow IS urgent even if email came 4 days ago.
|
|
316
|
+
- "Should this be in the briefing at all?" → No action + no insight = cut it. Shorter briefing = they read it.
|
|
317
|
+
- Every wrong item costs trust in the right ones.
|
|
318
|
+
|
|
319
|
+
CAPS: max 5 handled, max 6 decisions, max 4 upcoming, max 2 intel. Total ≤ 15 items. Less is more.` },
|
|
320
|
+
];
|
|
321
|
+
for (let turn = 0; turn < MAX_ORCHESTRATOR_TURNS; turn++) {
|
|
322
|
+
progress?.thinking();
|
|
323
|
+
const response = await client.messages.create({
|
|
324
|
+
model: ORCHESTRATOR_MODEL,
|
|
325
|
+
max_tokens: 8000,
|
|
326
|
+
system: buildOrchestratorPrompt(),
|
|
327
|
+
tools: allTools,
|
|
328
|
+
messages,
|
|
329
|
+
});
|
|
330
|
+
const toolUses = response.content.filter((b) => b.type === 'tool_use');
|
|
331
|
+
const textBlocks = response.content.filter((b) => b.type === 'text');
|
|
332
|
+
if (toolUses.length > 0) {
|
|
333
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
334
|
+
// Separate workers, ask_user, and direct tools
|
|
335
|
+
const workerTUs = toolUses.filter(tu => tu.name === 'spawn_worker');
|
|
336
|
+
const askTUs = toolUses.filter(tu => tu.name === 'ask_user');
|
|
337
|
+
const directTUs = toolUses.filter(tu => tu.name !== 'spawn_worker' && tu.name !== 'ask_user');
|
|
338
|
+
// Run direct tools in parallel
|
|
339
|
+
const directPromises = directTUs.map(async (tu) => {
|
|
340
|
+
progress?.toolStart(tu.name);
|
|
341
|
+
const result = truncateResult(await executeTool(tu.name, (tu.input || {})));
|
|
342
|
+
progress?.toolDone(tu.name);
|
|
343
|
+
return { type: 'tool_result', tool_use_id: tu.id, content: result };
|
|
344
|
+
});
|
|
345
|
+
// Handle ask_user via AUQ session protocol (user answers in `auq` TUI)
|
|
346
|
+
const askPromises = askTUs.map(async (tu) => {
|
|
347
|
+
const input = tu.input;
|
|
348
|
+
progress?.toolStart('ask_user');
|
|
349
|
+
const result = await ask(input.questions || []);
|
|
350
|
+
progress?.toolDone('ask_user');
|
|
351
|
+
return { type: 'tool_result', tool_use_id: tu.id, content: result };
|
|
352
|
+
});
|
|
353
|
+
// Run ALL workers in parallel — stream decision cards as each completes
|
|
354
|
+
const workerPromises = workerTUs.map(async (tu) => {
|
|
355
|
+
const input = tu.input;
|
|
356
|
+
const label = input.task.split(/[.!\n]/)[0]
|
|
357
|
+
.replace(/^(PRIORITY|TASK|INVESTIGATE|CHECK|VERIFY):?\s*/i, '')
|
|
358
|
+
.slice(0, 48);
|
|
359
|
+
progress?.workerStart(tu.id, label);
|
|
360
|
+
try {
|
|
361
|
+
let result = truncateResult(await runWorker(client, input.task, input.tools, tu.id, progress), MAX_WORKER_RESULT_LEN);
|
|
362
|
+
progress?.workerDone(tu.id);
|
|
363
|
+
// Stream card to user as soon as this worker finishes
|
|
364
|
+
if (onCard) {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(result);
|
|
367
|
+
if (parsed.validated && parsed.title) {
|
|
368
|
+
const uMap = { critical: 'now', important: 'today', low: 'this_week', none: 'today' };
|
|
369
|
+
const card = {
|
|
370
|
+
title: parsed.title,
|
|
371
|
+
urgency: uMap[parsed.urgency] || 'today',
|
|
372
|
+
options: parsed.can_handle_silently ? undefined : parsed.options,
|
|
373
|
+
fyi: !!parsed.can_handle_silently,
|
|
374
|
+
context: parsed.fyi_context || undefined,
|
|
375
|
+
};
|
|
376
|
+
// Queue card presentation (serialized so prompts don't overlap)
|
|
377
|
+
const pick = await new Promise(resolve => {
|
|
378
|
+
cardQueue = cardQueue.then(async () => {
|
|
379
|
+
progress?.pause();
|
|
380
|
+
const p = await onCard(card);
|
|
381
|
+
progress?.resume();
|
|
382
|
+
resolve(p);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
result += `\nUser's decision: ${pick}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch { } // parse fail — pass raw result to orchestrator
|
|
389
|
+
}
|
|
390
|
+
return { type: 'tool_result', tool_use_id: tu.id, content: result };
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
progress?.workerFail(tu.id);
|
|
394
|
+
return {
|
|
395
|
+
type: 'tool_result', tool_use_id: tu.id,
|
|
396
|
+
content: `Worker failed: ${String(e)}`, is_error: true,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
// Wait for everything in parallel
|
|
401
|
+
const allResults = await Promise.all([...directPromises, ...askPromises, ...workerPromises]);
|
|
402
|
+
messages.push({
|
|
403
|
+
role: 'user',
|
|
404
|
+
content: allResults,
|
|
405
|
+
});
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
// No tools — orchestrator is done
|
|
409
|
+
if (textBlocks.length > 0) {
|
|
410
|
+
const content = textBlocks.map(b => b.text).join('\n');
|
|
411
|
+
const cleaned = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
412
|
+
try {
|
|
413
|
+
return JSON.parse(cleaned);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
const jsonMatch = cleaned.match(/\{[\s\S]*"greeting"[\s\S]*"decisions"[\s\S]*\}/);
|
|
417
|
+
if (jsonMatch) {
|
|
418
|
+
try {
|
|
419
|
+
return JSON.parse(jsonMatch[0]);
|
|
420
|
+
}
|
|
421
|
+
catch { }
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
greeting: cleaned.slice(0, 300),
|
|
425
|
+
handled: [], decisions: [], upcoming: [], intel: [],
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
greeting: 'Agent investigation completed but could not produce final analysis.',
|
|
433
|
+
handled: [], decisions: [], upcoming: [], intel: [],
|
|
434
|
+
};
|
|
435
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-powered analysis. Sends collected data → gets back a personal briefing.
|
|
3
|
+
*/
|
|
4
|
+
export type DecisionOption = {
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
};
|
|
8
|
+
export type Decision = {
|
|
9
|
+
title: string;
|
|
10
|
+
urgency: 'now' | 'today' | 'this_week';
|
|
11
|
+
options?: DecisionOption[];
|
|
12
|
+
fyi?: boolean;
|
|
13
|
+
context?: string;
|
|
14
|
+
};
|
|
15
|
+
export type Analysis = {
|
|
16
|
+
greeting: string;
|
|
17
|
+
handled?: string[];
|
|
18
|
+
decisions?: Decision[];
|
|
19
|
+
upcoming?: string[];
|
|
20
|
+
intel?: string[];
|
|
21
|
+
resolved_todos?: string[];
|
|
22
|
+
right_now?: string[];
|
|
23
|
+
today?: string[];
|
|
24
|
+
this_week?: string[];
|
|
25
|
+
noticed?: string[];
|
|
26
|
+
heads_up?: string[];
|
|
27
|
+
};
|
|
28
|
+
export declare function analyzeWithLLM(collectedData: Record<string, unknown>, apiKey: string): Promise<Analysis>;
|
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-powered analysis. Sends collected data → gets back a personal briefing.
|
|
3
|
+
*/
|
|
4
|
+
import { buildDeltaContext } from './state.js';
|
|
5
|
+
import { getUserName, buildProfile } from './profile.js';
|
|
6
|
+
const API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
7
|
+
const MODEL = 'google/gemini-2.5-flash';
|
|
8
|
+
function buildPersona() {
|
|
9
|
+
const profile = buildProfile();
|
|
10
|
+
const name = profile.name;
|
|
11
|
+
const lines = [`WHO YOU'RE BRIEFING:\n${name}.`];
|
|
12
|
+
if (profile.topContacts.length > 0) {
|
|
13
|
+
lines.push(`\n${name.toUpperCase()}'S KEY CONTACTS (auto-detected by message volume, last 30 days):`);
|
|
14
|
+
const byTier = { T1: [], T2: [], T3: [], T4: [] };
|
|
15
|
+
for (const c of profile.topContacts) {
|
|
16
|
+
byTier[c.tier].push(`${c.name} (${c.msgs30d} msgs)`);
|
|
17
|
+
}
|
|
18
|
+
if (byTier.T1.length)
|
|
19
|
+
lines.push(`- Inner circle (very high volume): ${byTier.T1.join(', ')}`);
|
|
20
|
+
if (byTier.T2.length)
|
|
21
|
+
lines.push(`- Close contacts: ${byTier.T2.join(', ')}`);
|
|
22
|
+
if (byTier.T3.length)
|
|
23
|
+
lines.push(`- Regular contacts: ${byTier.T3.join(', ')}`);
|
|
24
|
+
if (byTier.T4.length)
|
|
25
|
+
lines.push(`- Occasional contacts: ${byTier.T4.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
if (profile.projects.length > 0) {
|
|
28
|
+
lines.push(`\nACTIVE PROJECTS: ${profile.projects.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
function buildSystemPrompt() {
|
|
33
|
+
const name = getUserName();
|
|
34
|
+
return `You are ${name}'s telepathic executive assistant. You know them better than they know themselves. You have access to 30 days of their digital life.
|
|
35
|
+
|
|
36
|
+
${buildPersona()}
|
|
37
|
+
|
|
38
|
+
YOUR DATA SOURCES (what you're looking at):
|
|
39
|
+
- iMessage conversations (full threads with timestamps)
|
|
40
|
+
- Phone call history (who, when, duration)
|
|
41
|
+
- Calendar events (upcoming commitments)
|
|
42
|
+
- Screen time (app usage, categories, daily totals)
|
|
43
|
+
- Safari + Chrome browsing history (URLs, search queries, timestamps)
|
|
44
|
+
- Shell/terminal history (commands run, what's being built)
|
|
45
|
+
- Apple Mail
|
|
46
|
+
- Apple Notes
|
|
47
|
+
- Notifications
|
|
48
|
+
- Recently opened files
|
|
49
|
+
- Find My (device locations)
|
|
50
|
+
- Installed applications
|
|
51
|
+
|
|
52
|
+
RULES:
|
|
53
|
+
- ALWAYS use people's NAMES. You know who everyone is. Never show phone numbers or handles.
|
|
54
|
+
- Be INSANELY specific. Not "respond to messages" but "Text [name] back about dinner — they asked you yesterday."
|
|
55
|
+
- EXPONENTIAL TIME WEIGHTING: Last 24 hours = CRITICAL. Last 7 days = important context. Last 30 days = background patterns. Weight urgency accordingly.
|
|
56
|
+
- Cross-reference EVERYTHING. Searches reveal thinking. Browsing reveals research. Messages reveal commitments. Screen time reveals where attention actually goes. Shell history reveals what's being built and what's broken.
|
|
57
|
+
- Use the contact tier data to prioritize — inner circle contacts get immediate attention, occasional contacts can wait.
|
|
58
|
+
- ONLY surface things you're CERTAIN about from the data. Wrong suggestions = you get turned off. Accuracy > coverage.
|
|
59
|
+
- No counting. No statistics. No "you sent X messages." They care about WHAT needs doing, not HOW MUCH was done.
|
|
60
|
+
- No generic advice. No "consider" or "you might want to." If something needs doing, say DO IT.
|
|
61
|
+
- Talk like a trusted friend with perfect memory. Not a report generator.
|
|
62
|
+
- Surface contradictions: researching sleep but browsing at 2am? Say it.
|
|
63
|
+
- Don't moralize about screen time or social media.
|
|
64
|
+
|
|
65
|
+
OUTPUT FORMAT — return ONLY this JSON (no markdown, no code fences):
|
|
66
|
+
{
|
|
67
|
+
"greeting": "One sentence. What their last day/week has been like, in plain human language.",
|
|
68
|
+
"right_now": [
|
|
69
|
+
"Most urgent action with specific names and context — things from the last 24 hours that need immediate attention"
|
|
70
|
+
],
|
|
71
|
+
"today": [
|
|
72
|
+
"Things to handle today — messages to respond to, follow-ups, commitments"
|
|
73
|
+
],
|
|
74
|
+
"this_week": [
|
|
75
|
+
"Things from the broader week/month that shouldn't be forgotten"
|
|
76
|
+
],
|
|
77
|
+
"heads_up": [
|
|
78
|
+
"Upcoming commitments, follow-ups promised, things from group chats that might be forgotten"
|
|
79
|
+
],
|
|
80
|
+
"noticed": [
|
|
81
|
+
"Patterns you noticed — health, relationships, work habits, the stuff a great EA would whisper. Only say things you're CONFIDENT about from the data."
|
|
82
|
+
]
|
|
83
|
+
}`;
|
|
84
|
+
}
|
|
85
|
+
export async function analyzeWithLLM(collectedData, apiKey) {
|
|
86
|
+
const contextStr = JSON.stringify(collectedData, null, 1);
|
|
87
|
+
const trimmed = contextStr.length > 200_000
|
|
88
|
+
? contextStr.slice(0, 200_000) + '\n...(truncated)'
|
|
89
|
+
: contextStr;
|
|
90
|
+
const deltaContext = buildDeltaContext();
|
|
91
|
+
const name = getUserName();
|
|
92
|
+
const userContent = `Analyze ${name}'s life right now. Tell them exactly what to do, starting with the most urgent.
|
|
93
|
+
|
|
94
|
+
Data from the last 30 days (most recent first — prioritize accordingly):
|
|
95
|
+
|
|
96
|
+
${trimmed}${deltaContext ? '\n\n' + deltaContext : ''}`;
|
|
97
|
+
const response = await fetch(API_URL, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
model: MODEL,
|
|
105
|
+
messages: [
|
|
106
|
+
{ role: 'system', content: buildSystemPrompt() },
|
|
107
|
+
{ role: 'user', content: userContent }
|
|
108
|
+
],
|
|
109
|
+
temperature: 0.3,
|
|
110
|
+
max_tokens: 4000,
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const err = await response.text();
|
|
115
|
+
throw new Error(`API error ${response.status}: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
const json = await response.json();
|
|
118
|
+
const content = json.choices?.[0]?.message?.content || '';
|
|
119
|
+
const cleaned = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(cleaned);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return {
|
|
125
|
+
greeting: content.slice(0, 300),
|
|
126
|
+
right_now: [], today: [], this_week: [],
|
|
127
|
+
noticed: [], heads_up: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/auq.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export interface AuqQuestion {
|
|
6
|
+
prompt: string;
|
|
7
|
+
title: string;
|
|
8
|
+
options: {
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}[];
|
|
12
|
+
multiSelect?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function createSession(questions: AuqQuestion[]): string;
|
|
15
|
+
export declare function ask(questions: AuqQuestion[], timeoutMs?: number): Promise<string>;
|