hearth-dash 1.0.1 → 1.1.1

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.
@@ -1,216 +1,262 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- import { randomBytes } from "node:crypto";
5
- import { getPackageRoot, getNodeMajor } from "../lib/platform.js";
6
- import { ask, confirm, password } from "../lib/prompts.js";
7
- import {
8
- execWrangler, parseD1CreateOutput, parseDeployOutput,
9
- checkWranglerAuth, wranglerLogin, setSecret, executeSchema, listD1Databases,
10
- } from "../lib/wrangler.js";
11
- import { banner, step, bold, dim, cyan, green, yellow, red, success, fail, warn, info, spinner } from "../lib/ui.js";
12
-
13
- const TOTAL_STEPS = 6;
14
- const CONFIG_DIR = join(homedir(), ".hearth-dash");
15
- const CONFIG_PATH = join(CONFIG_DIR, "config.json");
16
-
17
- function saveConfig(data) {
18
- mkdirSync(CONFIG_DIR, { recursive: true });
19
- let existing = {};
20
- if (existsSync(CONFIG_PATH)) {
21
- try { existing = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")); } catch {}
22
- }
23
- const merged = { ...existing, ...data };
24
- writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
25
- return merged;
26
- }
27
-
28
- function genSecret() {
29
- return randomBytes(16).toString("base64url");
30
- }
31
-
32
- async function createD1OrReuse(dbName, cwd) {
33
- const result = await execWrangler(["d1", "create", dbName], cwd);
34
- if (result.code === 0) return parseD1CreateOutput(result);
35
- const combined = result.stdout + result.stderr;
36
- if (combined.includes("already exists") || combined.includes("already a database")) {
37
- warn(`Database "${dbName}" already exists. Looking up ID...`);
38
- const databases = await listD1Databases(cwd);
39
- const db = databases.find((d) => d.name === dbName);
40
- if (db) { info(`Found: ${db.uuid}`); return db.uuid; }
41
- fail("Could not find existing database ID.");
42
- return null;
43
- }
44
- fail(`Failed to create database: ${combined}`);
45
- return null;
46
- }
47
-
48
- export default async function deployCommand(args) {
49
- banner();
50
- console.log(bold(" Full Cloudflare Deployment\n"));
51
-
52
- // ── Step 1: Preflight ────────────────────────────────────
53
-
54
- step(1, TOTAL_STEPS, "Preflight checks");
55
-
56
- const nodeMajor = getNodeMajor();
57
- if (nodeMajor < 18) { fail(`Node.js ${nodeMajor} detected. Need >= 18.`); process.exit(1); }
58
- success(`Node.js ${process.version}`);
59
-
60
- const wranglerCheck = await execWrangler(["--version"], ".");
61
- if (wranglerCheck.code !== 0) {
62
- fail("Wrangler not found. Install: npm install -g wrangler");
63
- process.exit(1);
64
- }
65
- success(`Wrangler installed`);
66
-
67
- const authed = await checkWranglerAuth(".");
68
- if (!authed) {
69
- warn("Not logged into Cloudflare. Opening browser...");
70
- const loginOk = await wranglerLogin(".");
71
- if (!loginOk) { fail("Cloudflare login failed."); process.exit(1); }
72
- }
73
- success("Cloudflare authenticated");
74
-
75
- // ── Step 2: Configuration ────────────────────────────────
76
-
77
- step(2, TOTAL_STEPS, "Configuration");
78
-
79
- const partner1 = await ask(" Partner 1 name", "Partner 1");
80
- const partner2 = await ask(" Partner 2 name (or AI name)", "AI");
81
- const dashPassword = await password(" Dashboard password");
82
- if (!dashPassword) { fail("Password is required."); process.exit(1); }
83
- const mcpSecret = genSecret();
84
- success("MCP secret generated");
85
-
86
- console.log();
87
- info("Weather requires a free OpenWeatherMap API key.");
88
- info("Get one at: https://openweathermap.org/api");
89
- const weatherKey = await ask(" OpenWeatherMap API key (or skip)");
90
- let weatherLat = "", weatherLon = "";
91
- if (weatherKey) {
92
- info("Find your coordinates at: https://www.latlong.net");
93
- weatherLat = await ask(" Latitude (e.g. 51.5074)", "51.5074");
94
- weatherLon = await ask(" Longitude (e.g. -0.1278)", "-0.1278");
95
- }
96
-
97
- // ── Step 3: Create D1 database ───────────────────────────
98
-
99
- step(3, TOTAL_STEPS, "Creating D1 database");
100
-
101
- const pkgRoot = getPackageRoot();
102
- const deployDir = join(CONFIG_DIR, "deploy");
103
- mkdirSync(deployDir, { recursive: true });
104
-
105
- // Copy worker files to deploy dir
106
- const s1 = spinner("Copying source files");
107
- for (const file of ["worker.js", "schema.sql", "wrangler.toml"]) {
108
- const src = join(pkgRoot, file);
109
- if (!existsSync(src)) { s1.fail(`Missing: ${file}`); process.exit(1); }
110
- writeFileSync(join(deployDir, file), readFileSync(src, "utf-8"), "utf-8");
111
- }
112
- s1.stop("Source files copied");
113
-
114
- const s2 = spinner("Creating D1 database");
115
- const dbId = await createD1OrReuse("hearth-dash-db", deployDir);
116
- if (!dbId) { s2.fail("D1 creation failed"); process.exit(1); }
117
- s2.stop(`D1 database ready: ${dbId.substring(0, 8)}...`);
118
-
119
- // ── Step 4: Create R2 bucket ─────────────────────────────
120
-
121
- step(4, TOTAL_STEPS, "Creating R2 bucket");
122
-
123
- const s3 = spinner("Creating R2 bucket");
124
- const r2Result = await execWrangler(["r2", "bucket", "create", "hearth-dash-photos"], deployDir);
125
- if (r2Result.code !== 0) {
126
- const combined = r2Result.stdout + r2Result.stderr;
127
- if (combined.includes("already exists")) {
128
- s3.stop("R2 bucket already exists");
129
- } else {
130
- s3.fail(`R2 creation failed: ${combined}`);
131
- process.exit(1);
132
- }
133
- } else {
134
- s3.stop("R2 bucket created");
135
- }
136
-
137
- // Patch wrangler.toml with real values
138
- let toml = readFileSync(join(deployDir, "wrangler.toml"), "utf-8");
139
- toml = toml.replace("YOUR_D1_DATABASE_ID", dbId);
140
- toml = toml.replace('PARTNER_1 = "Partner 1"', `PARTNER_1 = "${partner1}"`);
141
- toml = toml.replace('PARTNER_2 = "Partner 2"', `PARTNER_2 = "${partner2}"`);
142
- if (weatherLat) {
143
- toml = toml.replace('# WEATHER_LAT = "52.5726"', `WEATHER_LAT = "${weatherLat}"`);
144
- toml = toml.replace('# WEATHER_LON = "-0.2405"', `WEATHER_LON = "${weatherLon}"`);
145
- }
146
- writeFileSync(join(deployDir, "wrangler.toml"), toml, "utf-8");
147
-
148
- // ── Step 5: Deploy worker + set secrets ──────────────────
149
-
150
- step(5, TOTAL_STEPS, "Deploying worker");
151
-
152
- const s4 = spinner("Setting secrets");
153
- await setSecret("DASHBOARD_PASSWORD", dashPassword, deployDir);
154
- await setSecret("MCP_SECRET", mcpSecret, deployDir);
155
- if (weatherKey) await setSecret("WEATHER_API_KEY", weatherKey, deployDir);
156
- s4.stop("Secrets configured");
157
-
158
- const s5 = spinner("Deploying to Cloudflare Workers");
159
- const deployResult = await execWrangler(["deploy"], deployDir);
160
- if (deployResult.code !== 0) {
161
- s5.fail("Deploy failed");
162
- console.error(deployResult.stderr || deployResult.stdout);
163
- process.exit(1);
164
- }
165
- const workerUrl = parseDeployOutput(deployResult);
166
- s5.stop(`Deployed: ${workerUrl}`);
167
-
168
- // Init schema
169
- const s6 = spinner("Initializing database schema");
170
- const schemaResult = await executeSchema("hearth-dash-db", join(deployDir, "schema.sql"), deployDir);
171
- if (!schemaResult.ok) {
172
- s6.fail("Schema init failed: " + (schemaResult.error || "unknown error"));
173
- warn("You can retry manually: npx wrangler d1 execute hearth-dash-db --remote --file schema.sql");
174
- } else {
175
- s6.stop("Database schema initialized");
176
- }
177
-
178
- // ── Step 6: Save config + print results ──────────────────
179
-
180
- step(6, TOTAL_STEPS, "Finishing up");
181
-
182
- const config = saveConfig({
183
- workerUrl,
184
- mcpSecret,
185
- partner1,
186
- partner2,
187
- dbId,
188
- deployedAt: new Date().toISOString(),
189
- });
190
-
191
- const mcpUrl = workerUrl + "/mcp/" + mcpSecret;
192
-
193
- console.log(`
194
- ${green(bold(" Done!"))} Your dashboard is live.
195
-
196
- ${bold("Dashboard:")} ${cyan(workerUrl)}
197
- ${bold("Password:")} ${dim("(the one you just set)")}
198
-
199
- ${bold("MCP Endpoint:")}
200
- ${dim(mcpUrl)}
201
-
202
- ${bold("MCP Config")} (add to Claude Code ${dim("~/.claude.json")} or Claude Desktop):
203
- ${dim(" {")}
204
- ${dim(' "mcpServers": {')}
205
- ${dim(' "hearth-dash": {')}
206
- ${dim(' "url": "' + mcpUrl + '"')}
207
- ${dim(" }")}
208
- ${dim(" }")}
209
- ${dim(" }")}
210
-
211
- ${bold("Connector URL")} (for Claude.ai mobile):
212
- ${cyan(mcpUrl)}
213
-
214
- ${dim("Config saved to: " + CONFIG_PATH)}
215
- `);
216
- }
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { getPackageRoot, getNodeMajor } from "../lib/platform.js";
5
+ import { ask, confirm, password } from "../lib/prompts.js";
6
+ import {
7
+ execWrangler, execCommand, parseD1CreateOutput, parseDeployOutput, parseKvCreateOutput,
8
+ checkWranglerAuth, wranglerLogin, setSecret, executeSchema, listD1Databases, listKvNamespaces,
9
+ } from "../lib/wrangler.js";
10
+ import { banner, step, bold, dim, cyan, green, yellow, red, success, fail, warn, info, spinner } from "../lib/ui.js";
11
+
12
+ const TOTAL_STEPS = 6;
13
+ const CONFIG_DIR = join(homedir(), ".hearth-dash");
14
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
15
+
16
+ function saveConfig(data) {
17
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
18
+ let existing = {};
19
+ if (existsSync(CONFIG_PATH)) {
20
+ try { existing = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")); } catch {}
21
+ }
22
+ const merged = { ...existing, ...data };
23
+ delete merged.mcpSecret;
24
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
25
+ try { chmodSync(CONFIG_PATH, 0o600); } catch {}
26
+ return merged;
27
+ }
28
+
29
+ async function createD1OrReuse(dbName, cwd) {
30
+ const result = await execWrangler(["d1", "create", dbName], cwd);
31
+ if (result.code === 0) return parseD1CreateOutput(result);
32
+ const combined = result.stdout + result.stderr;
33
+ if (combined.includes("already exists") || combined.includes("already a database")) {
34
+ warn(`Database "${dbName}" already exists. Looking up ID...`);
35
+ const databases = await listD1Databases(cwd);
36
+ const db = databases.find((d) => d.name === dbName);
37
+ if (db) { info(`Found: ${db.uuid}`); return db.uuid; }
38
+ fail("Could not find existing database ID.");
39
+ return null;
40
+ }
41
+ fail(`Failed to create database: ${combined}`);
42
+ return null;
43
+ }
44
+
45
+ async function createKvOrReuse(title, cwd) {
46
+ const existing = await listKvNamespaces(cwd);
47
+ const match = existing.find(namespace => namespace.title === title);
48
+ if (match) { info(`Found OAuth namespace: ${match.id}`); return match.id; }
49
+ const result = await execWrangler(["kv", "namespace", "create", title], cwd);
50
+ if (result.code === 0) return parseKvCreateOutput(result);
51
+ fail(`Failed to create OAuth KV namespace: ${result.stderr || result.stdout}`);
52
+ return null;
53
+ }
54
+
55
+ export default async function deployCommand(args) {
56
+ banner();
57
+ console.log(bold(" Full Cloudflare Deployment\n"));
58
+
59
+ // ── Step 1: Preflight ────────────────────────────────────
60
+
61
+ step(1, TOTAL_STEPS, "Preflight checks");
62
+
63
+ const nodeMajor = getNodeMajor();
64
+ if (nodeMajor < 22) { fail(`Node.js ${nodeMajor} detected. Need >= 22.`); process.exit(1); }
65
+ success(`Node.js ${process.version}`);
66
+
67
+ const wranglerCheck = await execWrangler(["--version"], ".");
68
+ if (wranglerCheck.code !== 0) {
69
+ fail("Wrangler not found. Install: npm install -g wrangler");
70
+ process.exit(1);
71
+ }
72
+ success(`Wrangler installed`);
73
+
74
+ const authed = await checkWranglerAuth(".");
75
+ if (!authed) {
76
+ warn("Not logged into Cloudflare. Opening browser...");
77
+ const loginOk = await wranglerLogin(".");
78
+ if (!loginOk) { fail("Cloudflare login failed."); process.exit(1); }
79
+ }
80
+ success("Cloudflare authenticated");
81
+
82
+ // ── Step 2: Configuration ────────────────────────────────
83
+
84
+ step(2, TOTAL_STEPS, "Configuration");
85
+
86
+ const partner1 = await ask(" Partner 1 name", "Partner 1");
87
+ const partner2 = await ask(" Partner 2 name (or AI name)", "AI");
88
+ if (partner1.length > 80 || partner2.length > 80) { fail("Partner names must be 80 characters or fewer."); process.exit(1); }
89
+ const dashPassword = await password(" Dashboard password");
90
+ if (!dashPassword || dashPassword.length < 12) { fail("Password must be at least 12 characters."); process.exit(1); }
91
+ const { randomBytes } = await import('node:crypto');
92
+ const sessionSecret = randomBytes(32).toString('base64url');
93
+ success("Secure session key generated");
94
+
95
+ console.log();
96
+ info("Weather requires a free OpenWeatherMap API key.");
97
+ info("Get one at: https://openweathermap.org/api");
98
+ const weatherKey = await ask(" OpenWeatherMap API key (or skip)");
99
+ let weatherLat = "", weatherLon = "";
100
+ if (weatherKey) {
101
+ info("Find your coordinates at: https://www.latlong.net");
102
+ weatherLat = await ask(" Latitude (e.g. 51.5074)", "51.5074");
103
+ weatherLon = await ask(" Longitude (e.g. -0.1278)", "-0.1278");
104
+ const lat = Number(weatherLat), lon = Number(weatherLon);
105
+ if (!Number.isFinite(lat) || lat < -90 || lat > 90 || !Number.isFinite(lon) || lon < -180 || lon > 180) {
106
+ fail("Latitude must be -90 to 90 and longitude must be -180 to 180.");
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ // ── Step 3: Create D1 database ───────────────────────────
112
+
113
+ step(3, TOTAL_STEPS, "Creating D1 database");
114
+
115
+ const pkgRoot = getPackageRoot();
116
+ const deployDir = join(CONFIG_DIR, "deploy");
117
+ mkdirSync(deployDir, { recursive: true });
118
+
119
+ // Copy worker files to deploy dir
120
+ const s1 = spinner("Copying source files");
121
+ for (const file of ["worker.js", "oauth-entry.js", "schema.sql", "wrangler.toml", "package.json"]) {
122
+ const src = join(pkgRoot, file);
123
+ if (!existsSync(src)) { s1.fail(`Missing: ${file}`); process.exit(1); }
124
+ writeFileSync(join(deployDir, file), readFileSync(src, "utf-8"), "utf-8");
125
+ }
126
+ s1.stop("Source files copied");
127
+
128
+ const install = await execCommand('npm', ['install', '--omit=dev', '--ignore-scripts'], deployDir);
129
+ if (install.code !== 0) { fail(`Dependency install failed: ${install.stderr || install.stdout}`); process.exit(1); }
130
+
131
+ const s2 = spinner("Creating D1 database");
132
+ const dbId = await createD1OrReuse("hearth-dash-db", deployDir);
133
+ if (!dbId) { s2.fail("D1 creation failed"); process.exit(1); }
134
+ s2.stop(`D1 database ready: ${dbId.substring(0, 8)}...`);
135
+
136
+ const oauthKvId = await createKvOrReuse('hearth-dash-oauth', deployDir);
137
+ if (!oauthKvId) { fail('OAuth storage creation failed'); process.exit(1); }
138
+
139
+ // ── Step 4: Create R2 bucket ─────────────────────────────
140
+
141
+ step(4, TOTAL_STEPS, "Creating R2 bucket");
142
+
143
+ const s3 = spinner("Creating R2 bucket");
144
+ const r2Result = await execWrangler(["r2", "bucket", "create", "hearth-dash-photos"], deployDir);
145
+ if (r2Result.code !== 0) {
146
+ const combined = r2Result.stdout + r2Result.stderr;
147
+ if (combined.includes("already exists")) {
148
+ s3.stop("R2 bucket already exists");
149
+ } else {
150
+ s3.fail(`R2 creation failed: ${combined}`);
151
+ process.exit(1);
152
+ }
153
+ } else {
154
+ s3.stop("R2 bucket created");
155
+ }
156
+
157
+ // Patch wrangler.toml with real values
158
+ let toml = readFileSync(join(deployDir, "wrangler.toml"), "utf-8");
159
+ toml = toml.replace("YOUR_D1_DATABASE_ID", dbId);
160
+ toml = toml.replace("YOUR_OAUTH_KV_ID", oauthKvId);
161
+ toml = toml.replace('PARTNER_1 = "Partner 1"', `PARTNER_1 = ${JSON.stringify(partner1)}`);
162
+ toml = toml.replace('PARTNER_2 = "Partner 2"', `PARTNER_2 = ${JSON.stringify(partner2)}`);
163
+ if (weatherLat) {
164
+ toml = toml.replace('# WEATHER_LAT = "52.5726"', `WEATHER_LAT = "${weatherLat}"`);
165
+ toml = toml.replace('# WEATHER_LON = "-0.2405"', `WEATHER_LON = "${weatherLon}"`);
166
+ }
167
+ writeFileSync(join(deployDir, "wrangler.toml"), toml, "utf-8");
168
+
169
+ // ── Step 5: Deploy worker + set secrets ──────────────────
170
+
171
+ step(5, TOTAL_STEPS, "Deploying worker");
172
+
173
+ // A Worker must exist before Wrangler can set secrets non-interactively.
174
+ // This first deployment is inert: application and consent routes return 503
175
+ // until the required secrets are present.
176
+ const provision = spinner("Provisioning Cloudflare Worker");
177
+ const provisionResult = await execWrangler(["deploy"], deployDir);
178
+ if (provisionResult.code !== 0) {
179
+ provision.fail("Worker provisioning failed");
180
+ console.error(provisionResult.stderr || provisionResult.stdout);
181
+ process.exit(1);
182
+ }
183
+ const workerUrl = parseDeployOutput(provisionResult);
184
+ if (!workerUrl) {
185
+ provision.fail("Provisioned Worker, but could not determine its workers.dev URL.");
186
+ console.error(provisionResult.stdout || provisionResult.stderr);
187
+ process.exit(1);
188
+ }
189
+ provision.stop(`Worker provisioned: ${workerUrl}`);
190
+
191
+ const s6 = spinner("Initializing database schema");
192
+ const schemaResult = await executeSchema("hearth-dash-db", join(deployDir, "schema.sql"), deployDir);
193
+ if (!schemaResult.ok) {
194
+ s6.fail("Schema init failed: " + (schemaResult.error || "unknown error"));
195
+ console.error("Deployment is incomplete; no success configuration was saved.");
196
+ process.exit(1);
197
+ }
198
+ s6.stop("Database schema initialized");
199
+
200
+ const s4 = spinner("Setting secrets");
201
+ const secrets = [
202
+ ["DASHBOARD_PASSWORD", dashPassword],
203
+ ["SESSION_SECRET", sessionSecret],
204
+ ];
205
+ if (weatherKey) secrets.push(["WEATHER_API_KEY", weatherKey]);
206
+ for (const [name, value] of secrets) {
207
+ const result = await setSecret(name, value, deployDir);
208
+ if (result.code !== 0) {
209
+ s4.fail(`Could not set ${name}`);
210
+ console.error(result.stderr || result.stdout);
211
+ process.exit(1);
212
+ }
213
+ }
214
+ s4.stop("Secrets configured");
215
+
216
+ const s5 = spinner("Activating configured Worker");
217
+ const deployResult = await execWrangler(["deploy"], deployDir);
218
+ if (deployResult.code !== 0) {
219
+ s5.fail("Deploy failed");
220
+ console.error(deployResult.stderr || deployResult.stdout);
221
+ process.exit(1);
222
+ }
223
+ s5.stop(`Deployed: ${workerUrl}`);
224
+
225
+ // ── Step 6: Save config + print results ──────────────────
226
+
227
+ step(6, TOTAL_STEPS, "Finishing up");
228
+
229
+ const config = saveConfig({
230
+ workerUrl,
231
+ partner1,
232
+ partner2,
233
+ dbId,
234
+ deployedAt: new Date().toISOString(),
235
+ });
236
+
237
+ const mcpUrl = workerUrl + "/mcp";
238
+
239
+ console.log(`
240
+ ${green(bold(" Done!"))} Your dashboard is live.
241
+
242
+ ${bold("Dashboard:")} ${cyan(workerUrl)}
243
+ ${bold("Password:")} ${dim("(the one you just set)")}
244
+
245
+ ${bold("OAuth MCP Endpoint:")}
246
+ ${dim(mcpUrl)}
247
+
248
+ ${bold("MCP Config")} (add to Claude Code ${dim("~/.claude.json")} or Claude Desktop):
249
+ ${dim(" {")}
250
+ ${dim(' "mcpServers": {')}
251
+ ${dim(' "hearth-dash": {')}
252
+ ${dim(' "url": "' + mcpUrl + '"')}
253
+ ${dim(" }")}
254
+ ${dim(" }")}
255
+ ${dim(" }")}
256
+
257
+ ${bold("Connector URL")} (Claude will open a dashboard-password consent screen):
258
+ ${cyan(mcpUrl)}
259
+
260
+ ${dim("Config saved to: " + CONFIG_PATH)}
261
+ `);
262
+ }
@@ -1,69 +1,73 @@
1
- import { readFileSync, existsSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- import { banner, bold, dim, cyan, warn, info } from "../lib/ui.js";
5
-
6
- const CONFIG_PATH = join(homedir(), ".hearth-dash", "config.json");
7
-
8
- export default async function mcpCommand(args) {
9
- banner();
10
-
11
- if (!existsSync(CONFIG_PATH)) {
12
- warn("No config found. Run 'hearth-dash deploy' first.");
13
- process.exit(1);
14
- }
15
-
16
- const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
17
- const mcpUrl = config.workerUrl + "/mcp/" + config.mcpSecret;
18
-
19
- console.log(bold(" MCP Configuration\n"));
20
-
21
- console.log(` ${bold("Endpoint:")} ${cyan(mcpUrl)}\n`);
22
-
23
- console.log(` ${bold("For Claude Code")} (add to ${dim("~/.claude.json")}):\n`);
24
- console.log(` {`);
25
- console.log(` "mcpServers": {`);
26
- console.log(` "hearth-dash": {`);
27
- console.log(` "url": "${mcpUrl}"`);
28
- console.log(` }`);
29
- console.log(` }`);
30
- console.log(` }\n`);
31
-
32
- console.log(` ${bold("For Claude Desktop")} (Settings > Developer > MCP):\n`);
33
- console.log(` Name: hearth-dash`);
34
- console.log(` URL: ${mcpUrl}\n`);
35
-
36
- console.log(` ${bold("Connector URL")} (for Claude.ai mobile):`);
37
- console.log(` ${cyan(mcpUrl)}\n`);
38
-
39
- console.log(` ${bold("Available MCP tools:")}`);
40
- const tools = [
41
- "hearth_status — Dashboard overview",
42
- "hearth_mood — Get/set moods",
43
- "hearth_note — Get/leave fridge notes",
44
- "hearth_moment — List/add moments",
45
- "hearth_date — Upcoming/add dates",
46
- "hearth_shopping_list — Get shopping list",
47
- "hearth_shopping_add — Add shopping item",
48
- "hearth_pressure — Barometric pressure data",
49
- "hearth_food_diary_today — Today's meals + water",
50
- "hearth_food_diary_history — Historical food data",
51
- "hearth_food_review — Post food review",
52
- "hearth_water_status — Water intake status",
53
- ];
54
- tools.forEach(t => console.log(` ${dim(t)}`));
55
- console.log();
56
-
57
- if (args.includes("--install")) {
58
- const { writeFileSync } = await import("node:fs");
59
- const claudeConfigPath = join(homedir(), ".claude.json");
60
- let claudeConfig = {};
61
- if (existsSync(claudeConfigPath)) {
62
- try { claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8")); } catch {}
63
- }
64
- if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
65
- claudeConfig.mcpServers["hearth-dash"] = { url: mcpUrl };
66
- writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2) + "\n", "utf-8");
67
- info("MCP config written to " + claudeConfigPath);
68
- }
69
- }
1
+ import { readFileSync, existsSync, chmodSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { banner, bold, dim, cyan, warn, info } from "../lib/ui.js";
5
+
6
+ const CONFIG_PATH = join(homedir(), ".hearth-dash", "config.json");
7
+
8
+ export default async function mcpCommand(args) {
9
+ banner();
10
+
11
+ if (!existsSync(CONFIG_PATH)) {
12
+ warn("No config found. Run 'hearth-dash deploy' first.");
13
+ process.exit(1);
14
+ }
15
+
16
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
17
+ const mcpUrl = config.workerUrl + "/mcp";
18
+
19
+ console.log(bold(" MCP Configuration\n"));
20
+
21
+ console.log(` ${bold("Endpoint:")} ${cyan(mcpUrl)}\n`);
22
+ info("Hearth uses OAuth 2.1. Your dashboard password stays out of the connector URL and Claude stores only revocable tokens.");
23
+ console.log();
24
+
25
+ console.log(` ${bold("For Claude Code")} (add to ${dim("~/.claude.json")}):\n`);
26
+ console.log(` {`);
27
+ console.log(` "mcpServers": {`);
28
+ console.log(` "hearth-dash": {`);
29
+ console.log(` "url": "${mcpUrl}"`);
30
+ console.log(` }`);
31
+ console.log(` }`);
32
+ console.log(` }\n`);
33
+
34
+ console.log(` ${bold("For Claude Desktop")} (Settings > Developer > MCP):\n`);
35
+ console.log(` Name: hearth-dash`);
36
+ console.log(` URL: ${mcpUrl}\n`);
37
+ console.log(` ${dim("When prompted, sign in with the Hearth dashboard password and approve access.")}\n`);
38
+
39
+ console.log(` ${bold("Connector URL")} (for Claude.ai mobile):`);
40
+ console.log(` ${cyan(mcpUrl)}\n`);
41
+
42
+ console.log(` ${bold("Available MCP tools:")}`);
43
+ const tools = [
44
+ "hearth_status — Dashboard overview",
45
+ "hearth_mood — Get/set moods",
46
+ "hearth_note — Get/leave fridge notes",
47
+ "hearth_moment — List/add moments",
48
+ "hearth_date — Upcoming/add dates",
49
+ "hearth_shopping_list — Get shopping list",
50
+ "hearth_shopping_add — Add shopping item",
51
+ "hearth_pressure — Barometric pressure data",
52
+ "hearth_food_diary_today — Today's meals + water",
53
+ "hearth_food_diary_history — Historical food data",
54
+ "hearth_food_review — Post food review",
55
+ "hearth_water_status — Water intake status",
56
+ ];
57
+ tools.forEach(t => console.log(` ${dim(t)}`));
58
+ console.log();
59
+
60
+ if (args.includes("--install")) {
61
+ const { writeFileSync } = await import("node:fs");
62
+ const claudeConfigPath = join(homedir(), ".claude.json");
63
+ let claudeConfig = {};
64
+ if (existsSync(claudeConfigPath)) {
65
+ try { claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8")); } catch {}
66
+ }
67
+ if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
68
+ claudeConfig.mcpServers["hearth-dash"] = { url: mcpUrl };
69
+ writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2) + "\n", "utf-8");
70
+ try { chmodSync(claudeConfigPath, 0o600); } catch {}
71
+ info("MCP config written to " + claudeConfigPath);
72
+ }
73
+ }