@daobrew/wellness-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +98 -0
  2. package/SKILL.md +190 -0
  3. package/audio/earth_breathing.m4a +0 -0
  4. package/audio/earth_meditation.m4a +0 -0
  5. package/audio/earth_zhanZhuang.m4a +0 -0
  6. package/audio/fire_breathing.m4a +0 -0
  7. package/audio/fire_meditation.m4a +0 -0
  8. package/audio/fire_zhanZhuang.m4a +0 -0
  9. package/audio/metal_breathing.m4a +0 -0
  10. package/audio/metal_meditation.m4a +0 -0
  11. package/audio/metal_zhanZhuang.m4a +0 -0
  12. package/audio/water_breathing.m4a +0 -0
  13. package/audio/water_meditation.m4a +0 -0
  14. package/audio/water_zhanZhuang.m4a +0 -0
  15. package/audio/wood_breathing.m4a +0 -0
  16. package/audio/wood_meditation.m4a +0 -0
  17. package/audio/wood_zhanZhuang.m4a +0 -0
  18. package/dist/src/audio.d.ts +13 -0
  19. package/dist/src/audio.js +88 -0
  20. package/dist/src/cache.d.ts +7 -0
  21. package/dist/src/cache.js +31 -0
  22. package/dist/src/client.d.ts +22 -0
  23. package/dist/src/client.js +65 -0
  24. package/dist/src/cooldown.d.ts +5 -0
  25. package/dist/src/cooldown.js +35 -0
  26. package/dist/src/headphones.d.ts +6 -0
  27. package/dist/src/headphones.js +50 -0
  28. package/dist/src/health/google-fit.d.ts +13 -0
  29. package/dist/src/health/google-fit.js +108 -0
  30. package/dist/src/health/index.d.ts +6 -0
  31. package/dist/src/health/index.js +42 -0
  32. package/dist/src/health/oauth.d.ts +6 -0
  33. package/dist/src/health/oauth.js +69 -0
  34. package/dist/src/health/oura.d.ts +14 -0
  35. package/dist/src/health/oura.js +130 -0
  36. package/dist/src/health/sync.d.ts +7 -0
  37. package/dist/src/health/sync.js +194 -0
  38. package/dist/src/index.d.ts +2 -0
  39. package/dist/src/index.js +107 -0
  40. package/dist/src/mock.d.ts +8 -0
  41. package/dist/src/mock.js +176 -0
  42. package/dist/src/preferences.d.ts +13 -0
  43. package/dist/src/preferences.js +47 -0
  44. package/dist/src/session.d.ts +15 -0
  45. package/dist/src/session.js +40 -0
  46. package/dist/src/setup-cli.js +2 -0
  47. package/dist/src/setup.d.ts +17 -0
  48. package/dist/src/setup.js +323 -0
  49. package/dist/src/tools.d.ts +4 -0
  50. package/dist/src/tools.js +420 -0
  51. package/dist/src/types.d.ts +86 -0
  52. package/dist/src/types.js +52 -0
  53. package/dist/tests/audio.test.d.ts +1 -0
  54. package/dist/tests/audio.test.js +67 -0
  55. package/dist/tests/cache.test.d.ts +1 -0
  56. package/dist/tests/cache.test.js +61 -0
  57. package/dist/tests/client.test.d.ts +1 -0
  58. package/dist/tests/client.test.js +95 -0
  59. package/dist/tests/cooldown.test.d.ts +1 -0
  60. package/dist/tests/cooldown.test.js +66 -0
  61. package/dist/tests/e2e.test.d.ts +1 -0
  62. package/dist/tests/e2e.test.js +144 -0
  63. package/dist/tests/guards.test.d.ts +1 -0
  64. package/dist/tests/guards.test.js +169 -0
  65. package/dist/tests/headphones.test.d.ts +1 -0
  66. package/dist/tests/headphones.test.js +46 -0
  67. package/dist/tests/mock.test.d.ts +1 -0
  68. package/dist/tests/mock.test.js +194 -0
  69. package/dist/tests/preferences.test.d.ts +1 -0
  70. package/dist/tests/preferences.test.js +71 -0
  71. package/dist/tests/session.test.d.ts +1 -0
  72. package/dist/tests/session.test.js +85 -0
  73. package/dist/tests/sync.test.d.ts +1 -0
  74. package/dist/tests/sync.test.js +54 -0
  75. package/package.json +29 -0
  76. package/src/setup-cli.js +2 -0
@@ -0,0 +1,420 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.toolDefinitions = void 0;
37
+ exports.handleToolCall = handleToolCall;
38
+ const mock_js_1 = require("./mock.js");
39
+ const audio_js_1 = require("./audio.js");
40
+ const headphones_js_1 = require("./headphones.js");
41
+ const preferences_js_1 = require("./preferences.js");
42
+ const session_js_1 = require("./session.js");
43
+ const cooldown = __importStar(require("./cooldown.js"));
44
+ const cache = __importStar(require("./cache.js"));
45
+ const fs_1 = require("fs");
46
+ const path_1 = require("path");
47
+ const os_1 = require("os");
48
+ exports.toolDefinitions = [
49
+ {
50
+ name: "daobrew_get_wellness_state",
51
+ description: "Get the user's current biometric wellness state including Yin/Yang balance and Five-Element stress pattern scores. Call proactively during long work sessions.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ force_refresh: { type: "boolean", description: "Force fresh data fetch, bypassing 30-min cache. Use true for /stress or explicit user requests." },
56
+ },
57
+ required: [],
58
+ },
59
+ },
60
+ {
61
+ name: "daobrew_get_element_detail",
62
+ description: "Get detailed evidence and intervention recommendation for a specific stress pattern.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ element: { type: "string", enum: ["wood", "fire", "earth", "metal", "water"], description: "The stress pattern to examine" },
67
+ },
68
+ required: ["element"],
69
+ },
70
+ },
71
+ {
72
+ name: "daobrew_start_breathing_session",
73
+ description: "Start a guided resonance breathing session matched to a stress pattern. Returns session ID and plays therapeutic audio.",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ element: { type: "string", enum: ["wood", "fire", "earth", "metal", "water"], description: "Stress pattern to address" },
78
+ tier: { type: "string", enum: ["text", "audio", "full"], description: "text = terminal prompts, audio = system audio playback, full = browser PWA (coming soon)" },
79
+ mode: { type: "string", enum: ["ambient", "ondemand"], description: "ambient = proactive (agent-initiated), ondemand = user-requested. Default: ondemand" },
80
+ force: { type: "boolean", description: "Bypass cooldown timer for acute stress spikes. Default: false" },
81
+ },
82
+ required: ["element"],
83
+ },
84
+ },
85
+ {
86
+ name: "daobrew_get_session_result",
87
+ description: "Get the outcome of a completed breathing session including HRV changes.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: { session_id: { type: "string", description: "Session ID from start_breathing_session" } },
91
+ required: ["session_id"],
92
+ },
93
+ },
94
+ {
95
+ name: "daobrew_get_session_history",
96
+ description: "Get recent wellness session history and trends.",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: { days: { type: "integer", description: "Lookback window in days" } },
100
+ required: [],
101
+ },
102
+ },
103
+ {
104
+ name: "daobrew_stop_session",
105
+ description: "Stop the current DaoBrew breathing session. Use when the user wants to end early.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ session_id: { type: "string", description: "Optional session ID; stops current if omitted" },
110
+ },
111
+ required: [],
112
+ },
113
+ },
114
+ {
115
+ name: "daobrew_status",
116
+ description: "Get DaoBrew server status: mode, connected data sources, headphone status, preferences, active session. Use for first-run onboarding.",
117
+ inputSchema: { type: "object", properties: {}, required: [] },
118
+ },
119
+ {
120
+ name: "daobrew_set_monitoring",
121
+ description: "Enable/disable ambient monitoring, adjust volume, cooldown, or voiceover. Persists to ~/.daobrew/prefs.json.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ ambient_optin: { type: "boolean", description: "Enable/disable ambient (proactive) mode" },
126
+ disabled: { type: "boolean", description: "Disable all proactive wellness checks" },
127
+ preferred_volume: { type: "number", description: "Audio volume 0.0-1.0" },
128
+ cooldown_minutes: { type: "integer", description: "Minutes between sessions" },
129
+ headphones_trusted: { type: "boolean", description: "Skip headphone detection" },
130
+ voiceover: { type: "boolean", description: "Enable/disable voiceover in on-demand tracks" },
131
+ },
132
+ required: [],
133
+ },
134
+ },
135
+ {
136
+ name: "daobrew_connect_source",
137
+ description: "Connect a wearable data source (Oura, Google Fit, or Apple Watch) for real biometric data.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ source: { type: "string", enum: ["oura", "google_fit", "apple_watch"], description: "Data source to connect" },
142
+ },
143
+ required: ["source"],
144
+ },
145
+ },
146
+ ];
147
+ let callLog = [];
148
+ async function handleToolCall(name, args, isMock, _apiKey, isDemo = false, client) {
149
+ // Input validation
150
+ const VALID_ELEMENTS = ["wood", "fire", "earth", "metal", "water"];
151
+ if (args.element && !VALID_ELEMENTS.includes(args.element)) {
152
+ throw new Error(`Invalid element "${args.element}". Must be one of: ${VALID_ELEMENTS.join(", ")}`);
153
+ }
154
+ function requireClient() {
155
+ if (!client)
156
+ throw new Error("DaoBrewClient not initialized. Set DAOBREW_API_KEY or use --mock.");
157
+ return client;
158
+ }
159
+ // Client-side rate limiting (free tier: 100/day during dev, 10/day for prod)
160
+ const now = Date.now();
161
+ if (!isMock) {
162
+ callLog.push(now);
163
+ callLog = callLog.filter(t => t > now - 86400000);
164
+ if (callLog.length > 100) {
165
+ throw new Error("Free tier limit reached (100/day). Upgrade at daobrew.com/pro for unlimited wellness checks.");
166
+ }
167
+ }
168
+ switch (name) {
169
+ case "daobrew_get_wellness_state": {
170
+ const forceRefresh = args.force_refresh === true;
171
+ const CACHE_KEY = "wellness_state";
172
+ if (!forceRefresh) {
173
+ const cached = cache.get(CACHE_KEY);
174
+ if (cached) {
175
+ const session = (0, session_js_1.getActiveSession)();
176
+ return {
177
+ ...cached.data,
178
+ cache_age_seconds: cached.age_seconds,
179
+ ...(session ? {
180
+ active_session: {
181
+ session_id: session.sessionId, element: session.element,
182
+ mode: session.mode, duration_played: Math.round((Date.now() - session.startTime) / 1000),
183
+ },
184
+ } : {}),
185
+ };
186
+ }
187
+ }
188
+ // Sync wearable data before fetching fresh state
189
+ if (forceRefresh && !isMock && client) {
190
+ try {
191
+ const { syncAllConnectedSources } = await import("./health/sync.js");
192
+ await syncAllConnectedSources(client);
193
+ }
194
+ catch { /* sync failure should not block state fetch */ }
195
+ }
196
+ // Check for mock state override file (for testing sparse data scenarios)
197
+ const mockStateFile = (0, path_1.join)((0, os_1.homedir)(), ".daobrew", "mock-state.json");
198
+ let state;
199
+ if ((0, fs_1.existsSync)(mockStateFile)) {
200
+ state = JSON.parse(require("fs").readFileSync(mockStateFile, "utf-8"));
201
+ }
202
+ else {
203
+ state = isMock ? (0, mock_js_1.mockWellnessState)(isDemo) : await requireClient().getWellnessState();
204
+ }
205
+ cache.set(CACHE_KEY, state);
206
+ const session = (0, session_js_1.getActiveSession)();
207
+ return {
208
+ ...state,
209
+ cache_age_seconds: 0,
210
+ ...(session ? {
211
+ active_session: {
212
+ session_id: session.sessionId, element: session.element,
213
+ mode: session.mode, duration_played: Math.round((Date.now() - session.startTime) / 1000),
214
+ },
215
+ } : {}),
216
+ };
217
+ }
218
+ case "daobrew_get_element_detail":
219
+ return isMock ? (0, mock_js_1.mockElementDetail)(args.element) : await requireClient().getElementDetail(args.element);
220
+ case "daobrew_start_breathing_session": {
221
+ const prefs = (0, preferences_js_1.load)();
222
+ const element = args.element;
223
+ const tier = args.tier || "audio";
224
+ const mode = args.mode || "ondemand";
225
+ const force = args.force === true;
226
+ // Guard 1: globally disabled
227
+ if (prefs.disabled) {
228
+ return { status: "disabled", message: "Wellness monitoring is disabled. Re-enable with daobrew_set_monitoring({ disabled: false })." };
229
+ }
230
+ // Guard 2: ambient requires opt-in
231
+ if (mode === "ambient" && !prefs.ambient_optin) {
232
+ return { status: "requires_optin", message: "Ambient mode requires user authorization. Call daobrew_set_monitoring({ ambient_optin: true }) first." };
233
+ }
234
+ // Guard 3: already playing
235
+ if ((0, session_js_1.isSessionRunning)()) {
236
+ return { status: "session_active", message: "A session is already running. Call daobrew_stop_session first." };
237
+ }
238
+ // Guard 4: cooldown (unless force=true)
239
+ if (!force && cooldown.isActive(mode)) {
240
+ const remaining = cooldown.remainingMinutes(mode);
241
+ return { status: "cooldown", remaining_minutes: remaining, message: `${mode} cooldown active. ${remaining} min remaining. Use force: true to override.` };
242
+ }
243
+ // Guard 5: headphone check
244
+ // Ambient mode ALWAYS requires real headphone detection (binaural needs headphones)
245
+ // On-demand can skip if headphones_trusted (user's choice)
246
+ if (tier !== "text" && !(mode === "ondemand" && prefs.headphones_trusted)) {
247
+ const headphones = await (0, headphones_js_1.detectHeadphones)();
248
+ if (!headphones.connected) {
249
+ if (mode === "ambient") {
250
+ // Silently skip — don't interrupt the user
251
+ return { status: "skipped_no_headphones", message: "Ambient session skipped — no headphones detected." };
252
+ }
253
+ return {
254
+ status: "no_headphones",
255
+ device: headphones.device,
256
+ message: "No headphones detected (built-in speakers only). Binaural beats require headphones. Connect headphones or use daobrew_set_monitoring({ headphones_trusted: true }) to skip this check.",
257
+ };
258
+ }
259
+ }
260
+ // All guards passed — proceed to session start
261
+ const session = isMock ? (0, mock_js_1.mockStartSession)(element, tier) : await requireClient().startSession(element, tier);
262
+ if (tier === "text") {
263
+ const script = (0, audio_js_1.generateTextBreathingScript)(element, session.duration_seconds, session.breathing_rate_bpm);
264
+ cooldown.activate(mode, (prefs.cooldown_minutes ?? 30) * 60 * 1000);
265
+ (0, preferences_js_1.incrementSessionCount)();
266
+ return { ...session, breathing_script: script };
267
+ }
268
+ if (tier === "audio" || tier === "full") {
269
+ const volume = prefs.preferred_volume ?? 0.4;
270
+ // Map the recommended task to audio file task type
271
+ const taskType = session.recommended_task === "meditation" ? "meditation"
272
+ : session.recommended_task === "zhan_zhuang" ? "zhanZhuang"
273
+ : "breathing";
274
+ const audioResult = await (0, audio_js_1.playAudio)(element, taskType, volume);
275
+ if (audioResult.status === "playing") {
276
+ (0, session_js_1.startSession)({
277
+ sessionId: session.session_id,
278
+ pid: audioResult.pid,
279
+ element,
280
+ genre: session.genre,
281
+ duration: session.duration_seconds,
282
+ startTime: Date.now(),
283
+ mode,
284
+ });
285
+ cooldown.activate(mode, (prefs.cooldown_minutes ?? 30) * 60 * 1000);
286
+ (0, preferences_js_1.incrementSessionCount)();
287
+ return { ...session, audio_status: "playing", pid: audioResult.pid, audio_file: audioResult.file };
288
+ }
289
+ // Audio failed — auto-fallback to text tier with explanation
290
+ const script = (0, audio_js_1.generateTextBreathingScript)(element, session.duration_seconds, session.breathing_rate_bpm);
291
+ cooldown.activate(mode, (prefs.cooldown_minutes ?? 30) * 60 * 1000);
292
+ (0, preferences_js_1.incrementSessionCount)();
293
+ return {
294
+ ...session,
295
+ tier: "text",
296
+ audio_status: audioResult.status,
297
+ audio_error: audioResult.error,
298
+ breathing_script: script,
299
+ fallback_reason: `Audio playback failed (${audioResult.status}). Showing text breathing guide instead.`,
300
+ };
301
+ }
302
+ return session;
303
+ }
304
+ case "daobrew_get_session_result": {
305
+ const result = isMock ? (0, mock_js_1.mockSessionResult)(args.session_id) : await requireClient().getSessionResult(args.session_id);
306
+ // Invalidate wellness cache so next check gets fresh post-session state
307
+ cache.invalidate("wellness_state");
308
+ // Fetch fresh wellness state to include as post-session snapshot
309
+ try {
310
+ const postState = isMock ? (0, mock_js_1.mockWellnessState)() : await requireClient().getWellnessState();
311
+ cache.set("wellness_state", postState);
312
+ return { ...result, post_session_state: postState };
313
+ }
314
+ catch {
315
+ return result;
316
+ }
317
+ }
318
+ case "daobrew_get_session_history":
319
+ return isMock ? (0, mock_js_1.mockSessionHistory)(args.days ?? 7) : await requireClient().getSessionHistory(args.days ?? 7);
320
+ case "daobrew_stop_session": {
321
+ const stopped = (0, audio_js_1.stopPlayback)();
322
+ const stoppedSession = (0, session_js_1.clearSession)();
323
+ if (!stopped && !stoppedSession) {
324
+ return { status: "no_active_session", duration_played: null, message: "No active session to stop." };
325
+ }
326
+ const played = stoppedSession ? Math.round((Date.now() - stoppedSession.startTime) / 1000) : 0;
327
+ return {
328
+ status: "stopped", session_id: stoppedSession?.sessionId ?? null, duration_played: played,
329
+ message: `Session stopped after ${Math.round(played / 60)} minutes.`,
330
+ };
331
+ }
332
+ case "daobrew_status": {
333
+ const headphones = await (0, headphones_js_1.detectHeadphones)();
334
+ const prefs = (0, preferences_js_1.load)();
335
+ const activeSession = (0, session_js_1.getActiveSession)();
336
+ const ouraConnected = (0, fs_1.existsSync)((0, path_1.join)((0, os_1.homedir)(), ".daobrew", "oura-token.json"));
337
+ const gfitConnected = (0, fs_1.existsSync)((0, path_1.join)((0, os_1.homedir)(), ".daobrew", "google-fit-token.json"));
338
+ return {
339
+ mode: isMock ? "mock" : "real",
340
+ data_sources: {
341
+ oura: ouraConnected ? "connected" : "not_connected",
342
+ google_fit: gfitConnected ? "connected" : "not_connected",
343
+ apple_watch: "not_connected",
344
+ },
345
+ headphones,
346
+ preferences: prefs,
347
+ active_session: activeSession ? {
348
+ session_id: activeSession.sessionId, element: activeSession.element,
349
+ mode: activeSession.mode, duration_played: Math.round((Date.now() - activeSession.startTime) / 1000),
350
+ } : null,
351
+ };
352
+ }
353
+ case "daobrew_set_monitoring": {
354
+ const updates = {};
355
+ const allowed = ["ambient_optin", "disabled", "preferred_volume", "cooldown_minutes", "headphones_trusted", "voiceover"];
356
+ for (const key of allowed) {
357
+ if (args[key] !== undefined)
358
+ updates[key] = args[key];
359
+ }
360
+ // Validate ranges
361
+ if (updates.preferred_volume !== undefined) {
362
+ updates.preferred_volume = Math.max(0, Math.min(1, updates.preferred_volume));
363
+ }
364
+ if (updates.cooldown_minutes !== undefined) {
365
+ updates.cooldown_minutes = Math.max(1, Math.min(1440, updates.cooldown_minutes));
366
+ }
367
+ if (args.ambient_optin === true) {
368
+ updates.ambient_optin_date = new Date().toISOString();
369
+ }
370
+ (0, preferences_js_1.save)(updates);
371
+ return { status: "updated", preferences: (0, preferences_js_1.load)() };
372
+ }
373
+ case "daobrew_connect_source": {
374
+ const source = args.source;
375
+ if (source === "apple_watch") {
376
+ return {
377
+ status: "install_required",
378
+ source: "apple_watch",
379
+ install_url: "https://daobrew.com/health-sync",
380
+ instructions: "Install DaoBrew Health Sync on your iPhone via TestFlight. It reads your Apple Watch data and syncs it to DaoBrew.",
381
+ };
382
+ }
383
+ if (source === "oura") {
384
+ const clientId = process.env.DAOBREW_OURA_CLIENT_ID;
385
+ if (!clientId)
386
+ return { status: "error", source, error: "DAOBREW_OURA_CLIENT_ID env var not set" };
387
+ const { startOAuthFlow } = await import("./health/oauth.js");
388
+ const oauthResult = await startOAuthFlow(source, clientId, "https://cloud.ouraring.com/oauth/authorize", "https://api.ouraring.com/oauth/token", ["daily", "heartrate", "session", "sleep"]);
389
+ if (oauthResult.status === "connected" && !isMock && client) {
390
+ try {
391
+ const { syncAllConnectedSources } = await import("./health/sync.js");
392
+ const syncResults = await syncAllConnectedSources(client);
393
+ return { ...oauthResult, initial_sync: syncResults };
394
+ }
395
+ catch { /* return without sync data */ }
396
+ }
397
+ return oauthResult;
398
+ }
399
+ if (source === "google_fit") {
400
+ const clientId = process.env.DAOBREW_GOOGLE_CLIENT_ID;
401
+ if (!clientId)
402
+ return { status: "error", source, error: "DAOBREW_GOOGLE_CLIENT_ID env var not set" };
403
+ const { startOAuthFlow } = await import("./health/oauth.js");
404
+ const oauthResult = await startOAuthFlow("google_fit", clientId, "https://accounts.google.com/o/oauth2/v2/auth", "https://oauth2.googleapis.com/token", ["https://www.googleapis.com/auth/fitness.heart_rate.read", "https://www.googleapis.com/auth/fitness.activity.read", "https://www.googleapis.com/auth/fitness.sleep.read"]);
405
+ if (oauthResult.status === "connected" && !isMock && client) {
406
+ try {
407
+ const { syncAllConnectedSources } = await import("./health/sync.js");
408
+ const syncResults = await syncAllConnectedSources(client);
409
+ return { ...oauthResult, initial_sync: syncResults };
410
+ }
411
+ catch { /* return without sync data */ }
412
+ }
413
+ return oauthResult;
414
+ }
415
+ return { status: "error", source, error: `Unknown source: ${source}` };
416
+ }
417
+ default:
418
+ throw new Error(`Unknown tool: ${name}`);
419
+ }
420
+ }
@@ -0,0 +1,86 @@
1
+ export type Quadrant = "peak" | "pushing_it" | "recharging" | "burnout";
2
+ export type Element = "wood" | "fire" | "earth" | "metal" | "water";
3
+ export type DataTier = "tier_1" | "tier_2" | "tier_3";
4
+ export type InterventionTier = "text" | "audio" | "full";
5
+ export interface WellnessState {
6
+ yin: number;
7
+ yang: number;
8
+ quadrant: Quadrant;
9
+ quadrant_label: string;
10
+ quadrant_description: string;
11
+ yin_label: string;
12
+ yang_label: string;
13
+ active_elements: Element[];
14
+ element_scores: Record<Element, number>;
15
+ top_signal: string;
16
+ recommendation: string;
17
+ data_tier: DataTier;
18
+ last_updated: string;
19
+ }
20
+ export interface SignalEvidence {
21
+ signal: string;
22
+ value: string;
23
+ baseline: string;
24
+ direction: "above" | "below" | "normal";
25
+ weight: number;
26
+ }
27
+ export interface Intervention {
28
+ type: "breathing";
29
+ genre: string;
30
+ duration_seconds: number;
31
+ breathing_rate_bpm: number;
32
+ }
33
+ export interface ElementDetail {
34
+ element: Element;
35
+ label: string;
36
+ score: number;
37
+ activated: boolean;
38
+ headline: string;
39
+ evidence: SignalEvidence[];
40
+ why_this_practice: string;
41
+ intervention: Intervention;
42
+ }
43
+ export interface SessionStart {
44
+ session_id: string;
45
+ status: "started";
46
+ tier: InterventionTier;
47
+ element: Element;
48
+ genre: string;
49
+ duration_seconds: number;
50
+ breathing_rate_bpm: number;
51
+ instruction: string;
52
+ }
53
+ export interface SessionResult {
54
+ session_id: string;
55
+ completed: boolean;
56
+ duration_seconds: number;
57
+ hrv_before: number | null;
58
+ hrv_after: number | null;
59
+ hrv_change_pct: number | null;
60
+ hr_before: number | null;
61
+ hr_after: number | null;
62
+ summary: string;
63
+ }
64
+ export interface SessionHistoryEntry {
65
+ session_id: string;
66
+ timestamp: string;
67
+ element: Element;
68
+ tier: InterventionTier;
69
+ duration_seconds: number;
70
+ hrv_change_pct: number | null;
71
+ }
72
+ export declare const ELEMENT_GENRES: Record<Element, string>;
73
+ export declare const ELEMENT_LABELS: Record<Element, string>;
74
+ export declare const ELEMENT_ORGANS: Record<Element, string>;
75
+ export declare const ELEMENT_DESCRIPTIONS: Record<Element, string>;
76
+ export declare const ELEMENT_SHORT_SUMMARIES: Record<Element, string>;
77
+ export declare const ELEMENT_ACTIVATION_REASONS: Record<Element, string>;
78
+ export declare const ELEMENT_TASK_REASONS: Record<Element, string>;
79
+ export interface HealthSampleDTO {
80
+ metric_type: string;
81
+ value: number;
82
+ unit: string;
83
+ start_time: string;
84
+ end_time: string;
85
+ source: string;
86
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ELEMENT_TASK_REASONS = exports.ELEMENT_ACTIVATION_REASONS = exports.ELEMENT_SHORT_SUMMARIES = exports.ELEMENT_DESCRIPTIONS = exports.ELEMENT_ORGANS = exports.ELEMENT_LABELS = exports.ELEMENT_GENRES = void 0;
4
+ exports.ELEMENT_GENRES = {
5
+ wood: "ambient_downtempo",
6
+ fire: "lofi_chill_jazz",
7
+ earth: "acoustic_folk_calm",
8
+ metal: "nature_soundscape_pads",
9
+ water: "drone_deep_ambient",
10
+ };
11
+ exports.ELEMENT_LABELS = {
12
+ wood: "Wood",
13
+ fire: "Fire",
14
+ earth: "Earth",
15
+ metal: "Metal",
16
+ water: "Water",
17
+ };
18
+ exports.ELEMENT_ORGANS = {
19
+ wood: "Liver",
20
+ fire: "Heart",
21
+ earth: "Spleen",
22
+ metal: "Lung",
23
+ water: "Kidney",
24
+ };
25
+ exports.ELEMENT_DESCRIPTIONS = {
26
+ wood: "The Detox · Cleansing & Qi flow",
27
+ fire: "The Vessel · Circulation & clarity",
28
+ earth: "The Core · Digestion & absorption",
29
+ metal: "The Shield · Immunity & defense",
30
+ water: "The Reservoir · Energy & vitality",
31
+ };
32
+ exports.ELEMENT_SHORT_SUMMARIES = {
33
+ wood: "stagnation detected",
34
+ fire: "restlessness detected",
35
+ earth: "imbalance detected",
36
+ metal: "tension detected",
37
+ water: "depletion detected",
38
+ };
39
+ exports.ELEMENT_ACTIVATION_REASONS = {
40
+ wood: "Low HRV detected — Liver Qi stagnation.",
41
+ fire: "Elevated resting heart rate — Heart Fire rising.",
42
+ earth: "Low activity — Spleen Qi grows sluggish.",
43
+ metal: "Unsteady breathing — Lung Qi faltering.",
44
+ water: "Weak recovery at rest — Kidney Essence low.",
45
+ };
46
+ exports.ELEMENT_TASK_REASONS = {
47
+ wood: "Deep breathing steadies and restores Liver Qi.",
48
+ fire: "Slow breathing settles Shen and draws Fire down.",
49
+ earth: "Breathing from the center awakens Spleen Qi.",
50
+ metal: "Deep breathing steadies and restores Lung Qi.",
51
+ water: "Breathing roots Qi in Dan Tian, nourishing Kidneys.",
52
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const node_test_1 = require("node:test");
37
+ const assert = __importStar(require("node:assert/strict"));
38
+ const audio_js_1 = require("../src/audio.js");
39
+ (0, node_test_1.describe)("generateTextBreathingScript", () => {
40
+ (0, node_test_1.it)("generates correct cycle count and format", () => {
41
+ const script = (0, audio_js_1.generateTextBreathingScript)("wood", 300, 6);
42
+ assert.ok(script.includes("Wood"));
43
+ assert.ok(script.includes("Liver"));
44
+ assert.ok(script.includes("Cycle 1/"));
45
+ assert.ok(script.includes("Inhale..."));
46
+ assert.ok(script.includes("Exhale slowly..."));
47
+ assert.ok(script.includes("daobrew_get_session_result"));
48
+ });
49
+ (0, node_test_1.it)("shows max 6 cycles with ellipsis", () => {
50
+ const script = (0, audio_js_1.generateTextBreathingScript)("fire", 600, 6);
51
+ assert.ok(script.includes("Cycle 6/"));
52
+ assert.ok(script.includes("more cycles"));
53
+ assert.ok(!script.includes("Cycle 7/"));
54
+ });
55
+ (0, node_test_1.it)("handles each element", () => {
56
+ const elements = ["wood", "fire", "earth", "metal", "water"];
57
+ for (const el of elements) {
58
+ const script = (0, audio_js_1.generateTextBreathingScript)(el, 300, 6);
59
+ assert.ok(script.length > 50, `${el} script too short`);
60
+ }
61
+ });
62
+ });
63
+ (0, node_test_1.describe)("stopPlayback", () => {
64
+ (0, node_test_1.it)("returns false when nothing is playing", () => {
65
+ assert.strictEqual((0, audio_js_1.stopPlayback)(), false);
66
+ });
67
+ });
@@ -0,0 +1 @@
1
+ export {};