@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,323 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * DaoBrew Wellness MCP — Interactive Setup
5
+ *
6
+ * Usage:
7
+ * npx @daobrew/wellness-mcp setup
8
+ * daobrew-wellness-mcp setup
9
+ *
10
+ * Does:
11
+ * 1. Creates ~/.daobrew/config.json (API key, backend URL)
12
+ * 2. Creates ~/.daobrew/prefs.json (ambient opt-in, volume, etc.)
13
+ * 3. Installs SKILL.md to ~/.claude/skills/daobrew-wellness/
14
+ * 4. Adds MCP server to .mcp.json in current directory
15
+ * 5. Registers ambient hook in ~/.claude/settings.json (if ambient enabled)
16
+ * 6. Copies ambient hook script to ~/.daobrew/ambient-hook.sh
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.default = run;
20
+ const readline_1 = require("readline");
21
+ const fs_1 = require("fs");
22
+ const path_1 = require("path");
23
+ const os_1 = require("os");
24
+ const DAOBREW_DIR = (0, path_1.join)((0, os_1.homedir)(), ".daobrew");
25
+ const CONFIG_FILE = (0, path_1.join)(DAOBREW_DIR, "config.json");
26
+ const PREFS_FILE = (0, path_1.join)(DAOBREW_DIR, "prefs.json");
27
+ const CLAUDE_SKILLS_DIR = (0, path_1.join)((0, os_1.homedir)(), ".claude", "skills", "daobrew-wellness");
28
+ const CLAUDE_SETTINGS = (0, path_1.join)((0, os_1.homedir)(), ".claude", "settings.json");
29
+ const HOOK_DEST = (0, path_1.join)(DAOBREW_DIR, "ambient-hook.sh");
30
+ const DEFAULT_API_URL = "https://daobrew-backend.onrender.com/api/v1";
31
+ function ask(rl, question) {
32
+ return new Promise((resolve) => rl.question(question, resolve));
33
+ }
34
+ function ensureDir(dir) {
35
+ if (!(0, fs_1.existsSync)(dir))
36
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
37
+ }
38
+ function readJsonSafe(path) {
39
+ try {
40
+ return JSON.parse((0, fs_1.readFileSync)(path, "utf-8"));
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
46
+ function writeJson(path, data) {
47
+ ensureDir((0, path_1.dirname)(path));
48
+ (0, fs_1.writeFileSync)(path, JSON.stringify(data, null, 2) + "\n");
49
+ }
50
+ async function main() {
51
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
52
+ console.log("");
53
+ console.log(" ╔═══════════════════════════════════╗");
54
+ console.log(" ║ DaoBrew Wellness MCP — Setup ║");
55
+ console.log(" ╚═══════════════════════════════════╝");
56
+ console.log("");
57
+ console.log(" Biometric stress detection & TCM-guided recovery");
58
+ console.log(" for developers using Claude Code.");
59
+ console.log("");
60
+ // --- Step 1: Auto-provision API key (invisible to user) ---
61
+ const existingConfig = readJsonSafe(CONFIG_FILE);
62
+ const isNewKey = !existingConfig.api_key;
63
+ const apiKey = existingConfig.api_key || "dbk_" + Math.random().toString(36).substring(2, 14);
64
+ const apiUrl = existingConfig.api_url || DEFAULT_API_URL;
65
+ writeJson(CONFIG_FILE, { api_key: apiKey, api_url: apiUrl });
66
+ // Register key with backend (only on first run)
67
+ if (isNewKey) {
68
+ try {
69
+ const resp = await fetch(`${apiUrl}/register`, {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({ api_key: apiKey }),
73
+ });
74
+ if (resp.ok) {
75
+ console.log(" ✓ API key registered with backend");
76
+ }
77
+ else {
78
+ console.log(" ⚠ Could not register API key (backend may be offline — will retry on next sync)");
79
+ }
80
+ }
81
+ catch {
82
+ console.log(" ⚠ Could not reach backend — API key saved locally, will register on first sync");
83
+ }
84
+ }
85
+ else {
86
+ console.log(" ✓ Using existing API key");
87
+ }
88
+ // --- Step 2: Data sources ---
89
+ console.log(" What health data sources do you have?");
90
+ console.log("");
91
+ console.log(" 1. Apple Watch (via iPhone app — best: HRV, HR, steps, sleep)");
92
+ console.log(" 2. Oura Ring (OAuth — HRV, HR, sleep, readiness)");
93
+ console.log(" 3. None / skip for now (demo mode)");
94
+ console.log("");
95
+ const sourceInput = await ask(rl, " Select sources (e.g. 1,2 or 3): ");
96
+ const selections = sourceInput.split(/[,\s]+/).map(s => s.trim());
97
+ const sources = [];
98
+ const postSetupSteps = [];
99
+ if (selections.includes("1")) {
100
+ sources.push("apple_watch");
101
+ postSetupSteps.push("apple_watch");
102
+ }
103
+ if (selections.includes("2")) {
104
+ sources.push("oura");
105
+ postSetupSteps.push("oura");
106
+ }
107
+ writeJson(CONFIG_FILE, { api_key: apiKey, api_url: apiUrl, sources });
108
+ if (sources.length > 0) {
109
+ console.log(` ✓ Selected: ${sources.join(", ")}`);
110
+ }
111
+ else {
112
+ console.log(" ✓ Demo mode — no health data sources");
113
+ }
114
+ // --- Step 3: Ambient Mode ---
115
+ console.log("");
116
+ console.log(" Ambient mode auto-plays breathing music when stress is");
117
+ console.log(" detected. Requires headphones. Triggers naturally during");
118
+ console.log(" your Claude Code sessions — no popups or timers.");
119
+ console.log("");
120
+ const ambientInput = await ask(rl, " Enable ambient mode? [y/N] ");
121
+ const ambientEnabled = ambientInput.toLowerCase() === "y";
122
+ const existingPrefs = readJsonSafe(PREFS_FILE);
123
+ const prefs = {
124
+ ambient_optin: ambientEnabled,
125
+ ambient_optin_date: ambientEnabled ? new Date().toISOString() : null,
126
+ preferred_volume: existingPrefs.preferred_volume ?? 0.3,
127
+ cooldown_minutes: existingPrefs.cooldown_minutes ?? 30,
128
+ disabled: false,
129
+ headphones_trusted: false,
130
+ session_count: existingPrefs.session_count ?? 0,
131
+ voiceover: existingPrefs.voiceover ?? true,
132
+ };
133
+ writeJson(PREFS_FILE, prefs);
134
+ console.log(` ✓ Preferences saved (ambient: ${ambientEnabled ? "ON" : "OFF"})`);
135
+ // --- Step 4: Install SKILL.md ---
136
+ const skillSource = (0, path_1.join)(__dirname, "..", "SKILL.md");
137
+ const skillSourceAlt = (0, path_1.join)(__dirname, "..", "..", "SKILL.md");
138
+ const actualSkillSource = (0, fs_1.existsSync)(skillSource) ? skillSource : (0, fs_1.existsSync)(skillSourceAlt) ? skillSourceAlt : null;
139
+ if (actualSkillSource) {
140
+ ensureDir(CLAUDE_SKILLS_DIR);
141
+ (0, fs_1.copyFileSync)(actualSkillSource, (0, path_1.join)(CLAUDE_SKILLS_DIR, "SKILL.md"));
142
+ console.log(" ✓ Skill installed to ~/.claude/skills/daobrew-wellness/");
143
+ }
144
+ else {
145
+ console.log(" ⚠ SKILL.md not found in package — skipping skill install");
146
+ }
147
+ // --- Step 4: Add to .mcp.json ---
148
+ const mcpFile = (0, path_1.join)(process.cwd(), ".mcp.json");
149
+ const mcpConfig = readJsonSafe(mcpFile);
150
+ if (!mcpConfig.mcpServers)
151
+ mcpConfig.mcpServers = {};
152
+ // Use the npm bin command name — works regardless of install location
153
+ // The full path is fragile (breaks on other machines); the bin name is portable
154
+ mcpConfig.mcpServers["daobrew-wellness"] = {
155
+ command: "daobrew-wellness-mcp",
156
+ args: [],
157
+ };
158
+ writeJson(mcpFile, mcpConfig);
159
+ console.log(` ✓ MCP server added to ${mcpFile}`);
160
+ // --- Step 5: Install ambient hook ---
161
+ if (ambientEnabled) {
162
+ // Write hook script
163
+ const hookScript = generateHookScript();
164
+ ensureDir(DAOBREW_DIR);
165
+ (0, fs_1.writeFileSync)(HOOK_DEST, hookScript);
166
+ (0, fs_1.chmodSync)(HOOK_DEST, 0o755);
167
+ // Register in Claude settings
168
+ const settings = readJsonSafe(CLAUDE_SETTINGS);
169
+ if (!settings.hooks)
170
+ settings.hooks = {};
171
+ if (!settings.hooks.UserPromptSubmit)
172
+ settings.hooks.UserPromptSubmit = [];
173
+ // Check if already registered
174
+ const alreadyRegistered = settings.hooks.UserPromptSubmit.some((entry) => entry.hooks?.some((h) => h.command?.includes("ambient-hook")));
175
+ if (!alreadyRegistered) {
176
+ settings.hooks.UserPromptSubmit.push({
177
+ hooks: [
178
+ {
179
+ type: "command",
180
+ command: `bash ${HOOK_DEST}`,
181
+ timeout: 10,
182
+ },
183
+ ],
184
+ });
185
+ writeJson(CLAUDE_SETTINGS, settings);
186
+ console.log(" ✓ Ambient hook registered in Claude Code settings");
187
+ }
188
+ else {
189
+ console.log(" ✓ Ambient hook already registered");
190
+ }
191
+ }
192
+ // --- Step 6: Audio cache dir ---
193
+ const audioDir = (0, path_1.join)(DAOBREW_DIR, "audio");
194
+ ensureDir(audioDir);
195
+ // --- Done ---
196
+ console.log("");
197
+ console.log(" ┌─────────────────────────────────────┐");
198
+ console.log(" │ Setup complete! │");
199
+ console.log(" └─────────────────────────────────────┘");
200
+ console.log("");
201
+ // Show connection instructions for selected sources
202
+ if (postSetupSteps.length > 0) {
203
+ console.log(" Connect your health data:");
204
+ console.log("");
205
+ if (postSetupSteps.includes("apple_watch")) {
206
+ console.log(" 📱 Apple Watch / iPhone:");
207
+ console.log(" Open this link on your iPhone to install DaoBrew Health Sync:");
208
+ console.log("");
209
+ console.log(" https://testflight.apple.com/join/6XTNFvv5");
210
+ console.log("");
211
+ console.log(" (TestFlight link — copy to your phone or airdrop it)");
212
+ console.log("");
213
+ }
214
+ if (postSetupSteps.includes("oura")) {
215
+ console.log(' 💍 Oura Ring:');
216
+ console.log(' In Claude Code, say: "connect oura"');
217
+ console.log(" (Opens browser for OAuth — no phone needed)");
218
+ console.log("");
219
+ }
220
+ }
221
+ else {
222
+ console.log(" Demo mode — no health data sources connected.");
223
+ console.log(" You can connect sources later in Claude Code:");
224
+ console.log(' → "connect apple watch" / "connect oura"');
225
+ console.log("");
226
+ }
227
+ console.log(" Getting started:");
228
+ console.log(" 1. Open Claude Code (or start a new session)");
229
+ console.log(' 2. Say /stress to check your wellness state');
230
+ console.log(' 3. Say /breathe to start a breathing session');
231
+ console.log("");
232
+ if (ambientEnabled) {
233
+ console.log(" 🎵 Ambient mode is ON — stress relief auto-plays");
234
+ console.log(" when headphones are connected and stress is detected.");
235
+ console.log(' Say "disable wellness" anytime to turn it off.');
236
+ }
237
+ else {
238
+ console.log(' Ambient mode is OFF. Enable anytime:');
239
+ console.log(' → "enable ambient mode" in Claude Code');
240
+ }
241
+ console.log("");
242
+ rl.close();
243
+ }
244
+ function generateHookScript() {
245
+ return `#!/bin/bash
246
+ # DaoBrew Ambient Hook — auto-installed by setup
247
+ # Runs on every user message in Claude Code
248
+ # If stress detected + ambient opted in → tells agent to auto-start session
249
+
250
+ PREFS="$HOME/.daobrew/prefs.json"
251
+ CONFIG="$HOME/.daobrew/config.json"
252
+ AMBIENT_STATE="$HOME/.daobrew/ambient-state.json"
253
+ COOLDOWN_SECONDS=1800
254
+
255
+ [ ! -f "$PREFS" ] && exit 0
256
+ [ ! -f "$CONFIG" ] && exit 0
257
+
258
+ eval "$(python3 -c "
259
+ import json, sys
260
+ try:
261
+ p = json.load(open('$PREFS'))
262
+ print(f'AMBIENT={p.get(\\"ambient_optin\\", False)}')
263
+ print(f'DISABLED={p.get(\\"disabled\\", False)}')
264
+ except: sys.exit(1)
265
+ " 2>/dev/null)" || exit 0
266
+
267
+ [ "$AMBIENT" != "True" ] && exit 0
268
+ [ "$DISABLED" = "True" ] && exit 0
269
+
270
+ if [ -f "$AMBIENT_STATE" ]; then
271
+ LAST_TS=$(python3 -c "import json; print(int(json.load(open('$AMBIENT_STATE')).get('last_session_ts', 0)))" 2>/dev/null || echo 0)
272
+ NOW=$(date +%s)
273
+ [ $(( NOW - LAST_TS )) -lt $COOLDOWN_SECONDS ] && exit 0
274
+ fi
275
+
276
+ API_KEY=$(python3 -c "import json; print(json.load(open('$CONFIG')).get('api_key', ''))" 2>/dev/null)
277
+ API_URL=$(python3 -c "import json; print(json.load(open('$CONFIG')).get('api_url', ''))" 2>/dev/null)
278
+ [ -z "$API_KEY" ] && exit 0
279
+
280
+ STATE=$(curl -sS --max-time 5 \\
281
+ -H "Authorization: Bearer $API_KEY" \\
282
+ -H "X-Device-ID: ambient" \\
283
+ "$API_URL/state/current?format=mcp" 2>/dev/null)
284
+ [ -z "$STATE" ] && exit 0
285
+
286
+ RESULT=$(echo "$STATE" | python3 -c "
287
+ import sys, json
288
+ try:
289
+ d = json.load(sys.stdin).get('data', {})
290
+ active = d.get('active_elements', [])
291
+ if not active: sys.exit(0)
292
+ top = active[0]
293
+ score = d.get('element_scores', {}).get(top, 0)
294
+ quadrant = d.get('quadrant_label', '')
295
+ signal = d.get('top_signal', '')
296
+ print(f'{top}|{score}|{quadrant}|{signal}')
297
+ except: sys.exit(0)
298
+ " 2>/dev/null)
299
+
300
+ [ -z "$RESULT" ] && exit 0
301
+
302
+ ELEMENT=$(echo "$RESULT" | cut -d'|' -f1)
303
+ SCORE=$(echo "$RESULT" | cut -d'|' -f2)
304
+ QUADRANT=$(echo "$RESULT" | cut -d'|' -f3)
305
+ SIGNAL=$(echo "$RESULT" | cut -d'|' -f4)
306
+
307
+ python3 -c "
308
+ import json, time
309
+ from pathlib import Path
310
+ s = {'last_session_ts': int(time.time()), 'element': '$ELEMENT', 'quadrant': '$QUADRANT'}
311
+ Path('$AMBIENT_STATE').write_text(json.dumps(s))
312
+ " 2>/dev/null
313
+
314
+ echo "[DaoBrew Ambient] $SIGNAL (score $SCORE). Auto-starting breathing session: call daobrew_start_breathing_session(element=\\"$ELEMENT\\", mode=\\"ambient\\", tier=\\"audio\\"). Do NOT ask the user — ambient_optin is true. Just start it and say one brief line about what you are playing."
315
+ `;
316
+ }
317
+ // Entry point
318
+ function run() {
319
+ main().catch((err) => {
320
+ console.error("Setup failed:", err.message);
321
+ process.exit(1);
322
+ });
323
+ }
@@ -0,0 +1,4 @@
1
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { DaoBrewClient } from "./client.js";
3
+ export declare const toolDefinitions: Tool[];
4
+ export declare function handleToolCall(name: string, args: Record<string, any>, isMock: boolean, _apiKey?: string, isDemo?: boolean, client?: DaoBrewClient): Promise<any>;