feat(integrations): add Google Health (Fit) integration with full permissions

OAuth2 flow with all 11 Google Fitness scopes (activity, body, sleep,
heart rate, nutrition, location, blood glucose/pressure/temperature,
oxygen saturation, reproductive health). Stores access + refresh tokens;
auto-refreshes on expiry.

GoogleHealthSignalSource fetches steps, sleep sessions, active minutes,
calories, and heart rate from the Fit aggregate + sessions APIs. Signals
flow into both the tip orchestrator and the health-vitals pre-compute
agent, which generates prompt snippets about step progress, sleep
deficit, sedentary time, and elevated heart rate.

Signal.kind extended with 'health'; IntegrationProvider extended with
'google-health'. Agent compute signal mapping enriched to include source,
kind, and all features so health-vitals can filter its own signals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 11:12:11 +00:00
parent 161e654027
commit d1f28666b0
9 changed files with 576 additions and 7 deletions

View File

@@ -6,12 +6,13 @@ import { eq, and, gt, lt } from 'drizzle-orm';
import { config } from '../config.js';
import { getProfile, type Profile } from '../profile/builder.js';
import { todoistSource } from '../signals/todoist.js';
import { googleHealthSource } from '../signals/google-health.js';
import { SignalAggregator } from '../signals/aggregator.js';
const router: IRouter = Router();
// Separate aggregator instance — avoids circular dep with recommender.ts.
const _agentAggregator = new SignalAggregator().register(todoistSource);
const _agentAggregator = new SignalAggregator().register(todoistSource).register(googleHealthSource);
// ── Internal auth helper ──────────────────────────────────────────────────────
@@ -132,11 +133,16 @@ export async function computeAndStore(userId: string, agentId: string): Promise<
const signals = await _agentAggregator.fetchAll(userId);
tasks = signals.map((s) => ({
id: s.id,
source: s.source,
kind: s.kind,
content: s.content,
// Task-specific fields (default to harmless values for non-task signals)
priority: (s.features.priority as number) ?? 1,
is_overdue: Boolean(s.features.is_overdue),
task_age_days: (s.features.task_age_days as number) ?? 0,
project_id: (s.metadata as Record<string, unknown>).project_id ?? null,
// All features spread so source-specific agents (e.g. health-vitals) can read them
...s.features,
}));
} catch {
// No integration or fetch error — agents that need tasks will report "no tasks"