@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.
- package/README.md +98 -0
- package/SKILL.md +190 -0
- package/audio/earth_breathing.m4a +0 -0
- package/audio/earth_meditation.m4a +0 -0
- package/audio/earth_zhanZhuang.m4a +0 -0
- package/audio/fire_breathing.m4a +0 -0
- package/audio/fire_meditation.m4a +0 -0
- package/audio/fire_zhanZhuang.m4a +0 -0
- package/audio/metal_breathing.m4a +0 -0
- package/audio/metal_meditation.m4a +0 -0
- package/audio/metal_zhanZhuang.m4a +0 -0
- package/audio/water_breathing.m4a +0 -0
- package/audio/water_meditation.m4a +0 -0
- package/audio/water_zhanZhuang.m4a +0 -0
- package/audio/wood_breathing.m4a +0 -0
- package/audio/wood_meditation.m4a +0 -0
- package/audio/wood_zhanZhuang.m4a +0 -0
- package/dist/src/audio.d.ts +13 -0
- package/dist/src/audio.js +88 -0
- package/dist/src/cache.d.ts +7 -0
- package/dist/src/cache.js +31 -0
- package/dist/src/client.d.ts +22 -0
- package/dist/src/client.js +65 -0
- package/dist/src/cooldown.d.ts +5 -0
- package/dist/src/cooldown.js +35 -0
- package/dist/src/headphones.d.ts +6 -0
- package/dist/src/headphones.js +50 -0
- package/dist/src/health/google-fit.d.ts +13 -0
- package/dist/src/health/google-fit.js +108 -0
- package/dist/src/health/index.d.ts +6 -0
- package/dist/src/health/index.js +42 -0
- package/dist/src/health/oauth.d.ts +6 -0
- package/dist/src/health/oauth.js +69 -0
- package/dist/src/health/oura.d.ts +14 -0
- package/dist/src/health/oura.js +130 -0
- package/dist/src/health/sync.d.ts +7 -0
- package/dist/src/health/sync.js +194 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +107 -0
- package/dist/src/mock.d.ts +8 -0
- package/dist/src/mock.js +176 -0
- package/dist/src/preferences.d.ts +13 -0
- package/dist/src/preferences.js +47 -0
- package/dist/src/session.d.ts +15 -0
- package/dist/src/session.js +40 -0
- package/dist/src/setup-cli.js +2 -0
- package/dist/src/setup.d.ts +17 -0
- package/dist/src/setup.js +323 -0
- package/dist/src/tools.d.ts +4 -0
- package/dist/src/tools.js +420 -0
- package/dist/src/types.d.ts +86 -0
- package/dist/src/types.js +52 -0
- package/dist/tests/audio.test.d.ts +1 -0
- package/dist/tests/audio.test.js +67 -0
- package/dist/tests/cache.test.d.ts +1 -0
- package/dist/tests/cache.test.js +61 -0
- package/dist/tests/client.test.d.ts +1 -0
- package/dist/tests/client.test.js +95 -0
- package/dist/tests/cooldown.test.d.ts +1 -0
- package/dist/tests/cooldown.test.js +66 -0
- package/dist/tests/e2e.test.d.ts +1 -0
- package/dist/tests/e2e.test.js +144 -0
- package/dist/tests/guards.test.d.ts +1 -0
- package/dist/tests/guards.test.js +169 -0
- package/dist/tests/headphones.test.d.ts +1 -0
- package/dist/tests/headphones.test.js +46 -0
- package/dist/tests/mock.test.d.ts +1 -0
- package/dist/tests/mock.test.js +194 -0
- package/dist/tests/preferences.test.d.ts +1 -0
- package/dist/tests/preferences.test.js +71 -0
- package/dist/tests/session.test.d.ts +1 -0
- package/dist/tests/session.test.js +85 -0
- package/dist/tests/sync.test.d.ts +1 -0
- package/dist/tests/sync.test.js +54 -0
- package/package.json +29 -0
- package/src/setup-cli.js +2 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const tools_js_1 = require("./tools.js");
|
|
8
|
+
const audio_js_1 = require("./audio.js");
|
|
9
|
+
const client_js_1 = require("./client.js");
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const path_1 = require("path");
|
|
12
|
+
const os_1 = require("os");
|
|
13
|
+
// Load API config from file (env vars are unreliable — Claude Code may not pass them)
|
|
14
|
+
const CONFIG_FILE = (0, path_1.join)((0, os_1.homedir)(), ".daobrew", "config.json");
|
|
15
|
+
let fileConfig = {};
|
|
16
|
+
if ((0, fs_1.existsSync)(CONFIG_FILE)) {
|
|
17
|
+
try {
|
|
18
|
+
fileConfig = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
}
|
|
22
|
+
const apiKey = process.env.DAOBREW_API_KEY || fileConfig.api_key;
|
|
23
|
+
const apiBaseUrl = process.env.DAOBREW_API_URL || fileConfig.api_url;
|
|
24
|
+
// Mock only when explicitly requested via env AND no API key available
|
|
25
|
+
const isMock = process.env.DAOBREW_MOCK === "true" || !apiKey;
|
|
26
|
+
const isDemo = process.argv.includes("--demo");
|
|
27
|
+
if (!isMock && !apiKey) {
|
|
28
|
+
// This shouldn't happen since isMock=true when !apiKey, but just in case
|
|
29
|
+
console.error("No API key found. Add api_key to ~/.daobrew/config.json or set DAOBREW_API_KEY.");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const client = !isMock && apiKey
|
|
33
|
+
? new client_js_1.DaoBrewClient({ apiKey, baseUrl: apiBaseUrl })
|
|
34
|
+
: undefined;
|
|
35
|
+
const server = new index_js_1.Server({ name: "daobrew-wellness", version: "0.1.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
36
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
37
|
+
tools: tools_js_1.toolDefinitions,
|
|
38
|
+
}));
|
|
39
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
40
|
+
const { name, arguments: args } = request.params;
|
|
41
|
+
try {
|
|
42
|
+
const result = await (0, tools_js_1.handleToolCall)(name, args ?? {}, isMock, apiKey, isDemo, client);
|
|
43
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
|
|
48
|
+
isError: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const PROMPTS = [
|
|
53
|
+
{ name: "breathe", description: "Start a guided breathing session matched to your current stress pattern", arguments: [] },
|
|
54
|
+
{ name: "stress", description: "Check your current biometric wellness state", arguments: [] },
|
|
55
|
+
{ name: "stop", description: "Stop the currently playing breathing session", arguments: [] },
|
|
56
|
+
];
|
|
57
|
+
server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
|
|
58
|
+
prompts: PROMPTS,
|
|
59
|
+
}));
|
|
60
|
+
server.setRequestHandler(types_js_1.GetPromptRequestSchema, async (request) => {
|
|
61
|
+
const { name } = request.params;
|
|
62
|
+
switch (name) {
|
|
63
|
+
case "breathe":
|
|
64
|
+
return {
|
|
65
|
+
description: "Start a guided breathing session",
|
|
66
|
+
messages: [
|
|
67
|
+
{
|
|
68
|
+
role: "user",
|
|
69
|
+
content: { type: "text", text: "I need a breathing break. Check my wellness state and start a session for my top stress pattern." },
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
case "stress":
|
|
74
|
+
return {
|
|
75
|
+
description: "Check wellness state",
|
|
76
|
+
messages: [
|
|
77
|
+
{
|
|
78
|
+
role: "user",
|
|
79
|
+
content: { type: "text", text: "Check my current stress levels and biometric wellness state." },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
case "stop":
|
|
84
|
+
return {
|
|
85
|
+
description: "Stop current session",
|
|
86
|
+
messages: [
|
|
87
|
+
{
|
|
88
|
+
role: "user",
|
|
89
|
+
content: { type: "text", text: "Stop the current breathing session." },
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
default:
|
|
94
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
async function main() {
|
|
98
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
99
|
+
await server.connect(transport);
|
|
100
|
+
}
|
|
101
|
+
function shutdown() {
|
|
102
|
+
(0, audio_js_1.stopPlayback)();
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
process.on("SIGTERM", shutdown);
|
|
106
|
+
process.on("SIGINT", shutdown);
|
|
107
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { WellnessState, ElementDetail, SessionStart, SessionResult, SessionHistoryEntry, Element } from "./types.js";
|
|
2
|
+
export declare function mockWellnessState(isDemo?: boolean): WellnessState;
|
|
3
|
+
export declare function mockElementDetail(element: Element): ElementDetail;
|
|
4
|
+
export declare function mockStartSession(element: Element, tier?: string): SessionStart;
|
|
5
|
+
export declare function mockSessionResult(sessionId: string): SessionResult;
|
|
6
|
+
export declare function mockSessionHistory(days?: number): SessionHistoryEntry[];
|
|
7
|
+
/** Reset wellnessCallCount — for tests only. */
|
|
8
|
+
export declare function _resetWellnessCallCount(): void;
|
package/dist/src/mock.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mockWellnessState = mockWellnessState;
|
|
4
|
+
exports.mockElementDetail = mockElementDetail;
|
|
5
|
+
exports.mockStartSession = mockStartSession;
|
|
6
|
+
exports.mockSessionResult = mockSessionResult;
|
|
7
|
+
exports.mockSessionHistory = mockSessionHistory;
|
|
8
|
+
exports._resetWellnessCallCount = _resetWellnessCallCount;
|
|
9
|
+
const types_js_1 = require("./types.js");
|
|
10
|
+
let wellnessCallCount = 0;
|
|
11
|
+
function getQuadrant(yin, yang) {
|
|
12
|
+
if (yin >= 50 && yang >= 50)
|
|
13
|
+
return "peak";
|
|
14
|
+
if (yin < 50 && yang >= 50)
|
|
15
|
+
return "pushing_it";
|
|
16
|
+
if (yin >= 50 && yang < 50)
|
|
17
|
+
return "recharging";
|
|
18
|
+
return "burnout";
|
|
19
|
+
}
|
|
20
|
+
const QUADRANT_LABELS = {
|
|
21
|
+
peak: "In Flow",
|
|
22
|
+
pushing_it: "Pushing It",
|
|
23
|
+
recharging: "Recharging",
|
|
24
|
+
burnout: "Running on Empty",
|
|
25
|
+
};
|
|
26
|
+
const QUADRANT_DESCRIPTIONS = {
|
|
27
|
+
peak: "Your energy is in harmony. Maintain this state.",
|
|
28
|
+
pushing_it: "Active but under-recovered. Consider rest soon.",
|
|
29
|
+
recharging: "Well-recovered but low activity. Your body is conserving energy.",
|
|
30
|
+
burnout: "Under-recovered and inactive. Gentle movement and rest recommended.",
|
|
31
|
+
};
|
|
32
|
+
function yinStateLabel(score) {
|
|
33
|
+
if (score >= 80)
|
|
34
|
+
return "Fully Charged";
|
|
35
|
+
if (score >= 60)
|
|
36
|
+
return "Steady";
|
|
37
|
+
if (score >= 40)
|
|
38
|
+
return "Thinning";
|
|
39
|
+
if (score >= 20)
|
|
40
|
+
return "Running Low";
|
|
41
|
+
return "Empty";
|
|
42
|
+
}
|
|
43
|
+
function yangStateLabel(score) {
|
|
44
|
+
if (score >= 80)
|
|
45
|
+
return "Fired Up";
|
|
46
|
+
if (score >= 60)
|
|
47
|
+
return "Moving";
|
|
48
|
+
if (score >= 40)
|
|
49
|
+
return "Coasting";
|
|
50
|
+
if (score >= 20)
|
|
51
|
+
return "Sluggish";
|
|
52
|
+
return "Stalled";
|
|
53
|
+
}
|
|
54
|
+
function rand(min, max) {
|
|
55
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
56
|
+
}
|
|
57
|
+
function timeAwareBias() {
|
|
58
|
+
const hour = new Date().getHours();
|
|
59
|
+
if (hour >= 22 || hour < 6)
|
|
60
|
+
return { yinBias: -15, yangBias: -20, fireBias: 20, earthBias: 0 };
|
|
61
|
+
if (hour >= 13 && hour < 15)
|
|
62
|
+
return { yinBias: -10, yangBias: -10, fireBias: 0, earthBias: 15 };
|
|
63
|
+
if (hour >= 9 && hour < 12)
|
|
64
|
+
return { yinBias: 10, yangBias: 15, fireBias: 0, earthBias: -10 };
|
|
65
|
+
return { yinBias: 0, yangBias: 0, fireBias: 0, earthBias: 0 };
|
|
66
|
+
}
|
|
67
|
+
function mockWellnessState(isDemo = false) {
|
|
68
|
+
wellnessCallCount++;
|
|
69
|
+
const bias = timeAwareBias();
|
|
70
|
+
// First call always stressed (good first impression). Demo mode = always stressed.
|
|
71
|
+
const stressed = isDemo || wellnessCallCount === 1 || wellnessCallCount % 3 === 0;
|
|
72
|
+
const yin = Math.max(0, Math.min(100, rand(stressed ? 15 : 40, stressed ? 45 : 85) + bias.yinBias));
|
|
73
|
+
const yang = Math.max(0, Math.min(100, rand(stressed ? 50 : 35, stressed ? 80 : 90) + bias.yangBias));
|
|
74
|
+
const quadrant = getQuadrant(yin, yang);
|
|
75
|
+
const scores = {
|
|
76
|
+
wood: Math.max(0, Math.min(100, rand(stressed ? 40 : 10, stressed ? 80 : 45))),
|
|
77
|
+
fire: Math.max(0, Math.min(100, rand(stressed ? 30 : 5, stressed ? 70 : 40) + bias.fireBias)),
|
|
78
|
+
earth: Math.max(0, Math.min(100, rand(stressed ? 25 : 10, stressed ? 60 : 40) + bias.earthBias)),
|
|
79
|
+
metal: Math.max(0, Math.min(100, rand(5, stressed ? 55 : 35))),
|
|
80
|
+
water: Math.max(0, Math.min(100, rand(5, stressed ? 45 : 25))),
|
|
81
|
+
};
|
|
82
|
+
// On first call or demo, ensure at least one element is active (score >= 50)
|
|
83
|
+
if (stressed) {
|
|
84
|
+
const topKey = Object.entries(scores)
|
|
85
|
+
.sort((a, b) => b[1] - a[1])[0][0];
|
|
86
|
+
if (scores[topKey] < 50)
|
|
87
|
+
scores[topKey] = rand(55, 75);
|
|
88
|
+
}
|
|
89
|
+
const active = Object.entries(scores)
|
|
90
|
+
.filter(([, s]) => s >= 50)
|
|
91
|
+
.map(([e]) => e);
|
|
92
|
+
const topElement = Object.entries(scores)
|
|
93
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
94
|
+
return {
|
|
95
|
+
yin, yang, quadrant,
|
|
96
|
+
quadrant_label: QUADRANT_LABELS[quadrant],
|
|
97
|
+
quadrant_description: QUADRANT_DESCRIPTIONS[quadrant],
|
|
98
|
+
yin_label: yinStateLabel(yin),
|
|
99
|
+
yang_label: yangStateLabel(yang),
|
|
100
|
+
active_elements: active,
|
|
101
|
+
element_scores: scores,
|
|
102
|
+
top_signal: active.length > 0
|
|
103
|
+
? `${types_js_1.ELEMENT_LABELS[topElement[0]]} Qi · ${types_js_1.ELEMENT_SHORT_SUMMARIES[topElement[0]]} — ${types_js_1.ELEMENT_ACTIVATION_REASONS[topElement[0]]}`
|
|
104
|
+
: `All biometrics within normal range`,
|
|
105
|
+
recommendation: active.length > 0
|
|
106
|
+
? `${types_js_1.ELEMENT_LABELS[topElement[0]]} Qi · ${types_js_1.ELEMENT_SHORT_SUMMARIES[topElement[0]]}, try deep breathing.`
|
|
107
|
+
: `Your energy is in harmony. Maintain this state.`,
|
|
108
|
+
data_tier: "tier_2",
|
|
109
|
+
last_updated: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function mockElementDetail(element) {
|
|
113
|
+
const score = rand(40, 85);
|
|
114
|
+
const mockHrv = rand(18, 40);
|
|
115
|
+
const mockBaseline = rand(45, 65);
|
|
116
|
+
return {
|
|
117
|
+
element,
|
|
118
|
+
label: `${types_js_1.ELEMENT_LABELS[element]} · ${types_js_1.ELEMENT_ORGANS[element]}`,
|
|
119
|
+
score,
|
|
120
|
+
activated: score >= 50,
|
|
121
|
+
headline: types_js_1.ELEMENT_ACTIVATION_REASONS[element],
|
|
122
|
+
evidence: [
|
|
123
|
+
{ signal: "HRV", value: `${mockHrv}ms`, baseline: `${mockBaseline}ms`, direction: "below", weight: 0.35 },
|
|
124
|
+
{ signal: "Heart Rate", value: `${rand(72, 92)}bpm`, baseline: `${rand(62, 72)}bpm`, direction: "above", weight: 0.25 },
|
|
125
|
+
],
|
|
126
|
+
why_this_practice: types_js_1.ELEMENT_TASK_REASONS[element],
|
|
127
|
+
intervention: {
|
|
128
|
+
type: "breathing", genre: types_js_1.ELEMENT_GENRES[element],
|
|
129
|
+
duration_seconds: 300, breathing_rate_bpm: 6.0,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const activeSessions = new Map();
|
|
134
|
+
function mockStartSession(element, tier = "audio") {
|
|
135
|
+
const sessionId = `sess_${Date.now().toString(36)}`;
|
|
136
|
+
activeSessions.set(sessionId, { element, tier, startTime: Date.now() });
|
|
137
|
+
return {
|
|
138
|
+
session_id: sessionId, status: "started",
|
|
139
|
+
tier: tier, element,
|
|
140
|
+
genre: types_js_1.ELEMENT_GENRES[element], duration_seconds: 300, breathing_rate_bpm: 6.0,
|
|
141
|
+
instruction: `${types_js_1.ELEMENT_LABELS[element]} Qi · ${types_js_1.ELEMENT_SHORT_SUMMARIES[element]}, try deep breathing. Inhale as music rises, exhale as it fades. Session runs 5 minutes.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function mockSessionResult(sessionId) {
|
|
145
|
+
const hrvBefore = rand(20, 40);
|
|
146
|
+
const improvement = rand(20, 60);
|
|
147
|
+
const hrvAfter = Math.round(hrvBefore * (1 + improvement / 100));
|
|
148
|
+
const hrBefore = rand(72, 92);
|
|
149
|
+
const hrAfter = hrBefore - rand(5, 15);
|
|
150
|
+
return {
|
|
151
|
+
session_id: sessionId, completed: true, duration_seconds: 300,
|
|
152
|
+
hrv_before: hrvBefore, hrv_after: hrvAfter, hrv_change_pct: improvement,
|
|
153
|
+
hr_before: hrBefore, hr_after: hrAfter,
|
|
154
|
+
summary: `[Mock] Simulated — connect a wearable for real data. HRV improved ${improvement}%, resting HR dropped ${hrBefore - hrAfter}bpm. ${types_js_1.ELEMENT_LABELS[activeSessions.get(sessionId)?.element ?? "wood"]} Qi rebalancing — score ${rand(60, 75)} → ${rand(30, 48)}.`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function mockSessionHistory(days = 7) {
|
|
158
|
+
const entries = [];
|
|
159
|
+
const elements = ["wood", "fire", "earth", "metal", "water"];
|
|
160
|
+
const count = rand(3, Math.min(days * 2, 14));
|
|
161
|
+
for (let i = 0; i < count; i++) {
|
|
162
|
+
const daysAgo = rand(0, days - 1);
|
|
163
|
+
const d = new Date();
|
|
164
|
+
d.setDate(d.getDate() - daysAgo);
|
|
165
|
+
entries.push({
|
|
166
|
+
session_id: `sess_hist_${i}`, timestamp: d.toISOString(),
|
|
167
|
+
element: elements[rand(0, 4)], tier: "audio",
|
|
168
|
+
duration_seconds: 300, hrv_change_pct: rand(10, 55),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
172
|
+
}
|
|
173
|
+
/** Reset wellnessCallCount — for tests only. */
|
|
174
|
+
function _resetWellnessCallCount() {
|
|
175
|
+
wellnessCallCount = 0;
|
|
176
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Preferences {
|
|
2
|
+
ambient_optin: boolean;
|
|
3
|
+
ambient_optin_date: string | null;
|
|
4
|
+
preferred_volume: number;
|
|
5
|
+
cooldown_minutes: number;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
headphones_trusted: boolean;
|
|
8
|
+
session_count: number;
|
|
9
|
+
voiceover: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function load(): Preferences;
|
|
12
|
+
export declare function save(prefs: Partial<Preferences>): void;
|
|
13
|
+
export declare function incrementSessionCount(): number;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.load = load;
|
|
4
|
+
exports.save = save;
|
|
5
|
+
exports.incrementSessionCount = incrementSessionCount;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const PREFS_DIR = (0, path_1.join)((0, os_1.homedir)(), ".daobrew");
|
|
10
|
+
const PREFS_FILE = (0, path_1.join)(PREFS_DIR, "prefs.json");
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
ambient_optin: false,
|
|
13
|
+
ambient_optin_date: null,
|
|
14
|
+
preferred_volume: 0.3,
|
|
15
|
+
cooldown_minutes: 30,
|
|
16
|
+
disabled: false,
|
|
17
|
+
headphones_trusted: false,
|
|
18
|
+
session_count: 0,
|
|
19
|
+
voiceover: true,
|
|
20
|
+
};
|
|
21
|
+
function load() {
|
|
22
|
+
if (!(0, fs_1.existsSync)(PREFS_FILE))
|
|
23
|
+
return { ...DEFAULTS };
|
|
24
|
+
try {
|
|
25
|
+
return { ...DEFAULTS, ...JSON.parse((0, fs_1.readFileSync)(PREFS_FILE, "utf-8")) };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { ...DEFAULTS };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function save(prefs) {
|
|
32
|
+
if (!(0, fs_1.existsSync)(PREFS_DIR))
|
|
33
|
+
(0, fs_1.mkdirSync)(PREFS_DIR, { recursive: true });
|
|
34
|
+
const current = load();
|
|
35
|
+
const updated = { ...current, ...prefs };
|
|
36
|
+
(0, fs_1.writeFileSync)(PREFS_FILE, JSON.stringify(updated, null, 2));
|
|
37
|
+
}
|
|
38
|
+
function incrementSessionCount() {
|
|
39
|
+
const prefs = load();
|
|
40
|
+
const count = prefs.session_count + 1;
|
|
41
|
+
const updates = { session_count: count };
|
|
42
|
+
if (count >= 3 && prefs.voiceover) {
|
|
43
|
+
updates.voiceover = false;
|
|
44
|
+
}
|
|
45
|
+
save(updates);
|
|
46
|
+
return count;
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Element } from "./types.js";
|
|
2
|
+
export interface ActiveSession {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
pid: number;
|
|
5
|
+
element: Element;
|
|
6
|
+
genre: string;
|
|
7
|
+
duration: number;
|
|
8
|
+
startTime: number;
|
|
9
|
+
mode: "ambient" | "ondemand";
|
|
10
|
+
}
|
|
11
|
+
export declare function startSession(session: ActiveSession): void;
|
|
12
|
+
export declare function getActiveSession(): ActiveSession | null;
|
|
13
|
+
export declare function clearSession(): ActiveSession | null;
|
|
14
|
+
export declare function isSessionRunning(): boolean;
|
|
15
|
+
export declare function durationPlayed(): number | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startSession = startSession;
|
|
4
|
+
exports.getActiveSession = getActiveSession;
|
|
5
|
+
exports.clearSession = clearSession;
|
|
6
|
+
exports.isSessionRunning = isSessionRunning;
|
|
7
|
+
exports.durationPlayed = durationPlayed;
|
|
8
|
+
let activeSession = null;
|
|
9
|
+
function startSession(session) {
|
|
10
|
+
activeSession = session;
|
|
11
|
+
}
|
|
12
|
+
function getActiveSession() {
|
|
13
|
+
return activeSession;
|
|
14
|
+
}
|
|
15
|
+
function clearSession() {
|
|
16
|
+
const prev = activeSession;
|
|
17
|
+
activeSession = null;
|
|
18
|
+
return prev;
|
|
19
|
+
}
|
|
20
|
+
function isSessionRunning() {
|
|
21
|
+
if (!activeSession)
|
|
22
|
+
return false;
|
|
23
|
+
// Verify the audio process is still alive (skip for pid <= 0, e.g. text tier)
|
|
24
|
+
if (activeSession.pid > 0) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(activeSession.pid, 0);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Process is dead — clear stale session
|
|
30
|
+
activeSession = null;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
function durationPlayed() {
|
|
37
|
+
if (!activeSession)
|
|
38
|
+
return null;
|
|
39
|
+
return Math.round((Date.now() - activeSession.startTime) / 1000);
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* DaoBrew Wellness MCP — Interactive Setup
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @daobrew/wellness-mcp setup
|
|
7
|
+
* daobrew-wellness-mcp setup
|
|
8
|
+
*
|
|
9
|
+
* Does:
|
|
10
|
+
* 1. Creates ~/.daobrew/config.json (API key, backend URL)
|
|
11
|
+
* 2. Creates ~/.daobrew/prefs.json (ambient opt-in, volume, etc.)
|
|
12
|
+
* 3. Installs SKILL.md to ~/.claude/skills/daobrew-wellness/
|
|
13
|
+
* 4. Adds MCP server to .mcp.json in current directory
|
|
14
|
+
* 5. Registers ambient hook in ~/.claude/settings.json (if ambient enabled)
|
|
15
|
+
* 6. Copies ambient hook script to ~/.daobrew/ambient-hook.sh
|
|
16
|
+
*/
|
|
17
|
+
export default function run(): void;
|