hearth-dash 1.0.0 → 1.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/LICENSE +32 -21
- package/README.md +142 -0
- package/cli/commands/config.js +26 -26
- package/cli/commands/deploy.js +229 -183
- package/cli/commands/mcp.js +67 -63
- package/cli/index.js +63 -63
- package/cli/lib/platform.js +12 -12
- package/cli/lib/prompts.js +44 -44
- package/cli/lib/ui.js +41 -41
- package/cli/lib/wrangler.js +89 -62
- package/oauth-entry.js +41 -0
- package/package.json +46 -40
- package/schema.sql +88 -76
- package/worker.js +1468 -955
- package/wrangler.toml +25 -20
package/cli/commands/deploy.js
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "../lib/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
|
|
17
16
|
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 {}
|
|
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 {}
|
|
22
21
|
}
|
|
23
22
|
const merged = { ...existing, ...data };
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
|
-
async function
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
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}`);
|
|
45
52
|
return null;
|
|
46
53
|
}
|
|
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 <
|
|
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
|
-
|
|
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
|
+
|
|
79
86
|
const partner1 = await ask(" Partner 1 name", "Partner 1");
|
|
80
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); }
|
|
81
89
|
const dashPassword = await password(" Dashboard password");
|
|
82
|
-
if (!dashPassword) { fail("Password
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
info("
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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");
|
|
93
102
|
weatherLat = await ask(" Latitude (e.g. 51.5074)", "51.5074");
|
|
94
103
|
weatherLon = await ask(" Longitude (e.g. -0.1278)", "-0.1278");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
126
|
s1.stop("Source files copied");
|
|
113
127
|
|
|
114
|
-
const
|
|
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");
|
|
115
132
|
const dbId = await createD1OrReuse("hearth-dash-db", deployDir);
|
|
116
|
-
if (!dbId) { s2.fail("D1 creation failed"); process.exit(1); }
|
|
133
|
+
if (!dbId) { s2.fail("D1 creation failed"); process.exit(1); }
|
|
117
134
|
s2.stop(`D1 database ready: ${dbId.substring(0, 8)}...`);
|
|
118
135
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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");
|
|
139
159
|
toml = toml.replace("YOUR_D1_DATABASE_ID", dbId);
|
|
140
|
-
toml = toml.replace(
|
|
141
|
-
toml = toml.replace('
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
toml = toml.replace('#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
|
|
150
171
|
step(5, TOTAL_STEPS, "Deploying worker");
|
|
151
172
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (deployResult.code !== 0) {
|
|
161
|
-
s5.fail("Deploy failed");
|
|
162
|
-
console.error(deployResult.stderr || deployResult.stdout);
|
|
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);
|
|
163
181
|
process.exit(1);
|
|
164
182
|
}
|
|
165
|
-
const workerUrl = parseDeployOutput(
|
|
166
|
-
|
|
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}`);
|
|
167
190
|
|
|
168
|
-
// Init schema
|
|
169
191
|
const s6 = spinner("Initializing database schema");
|
|
170
192
|
const schemaResult = await executeSchema("hearth-dash-db", join(deployDir, "schema.sql"), deployDir);
|
|
171
193
|
if (!schemaResult.ok) {
|
|
172
194
|
s6.fail("Schema init failed: " + (schemaResult.error || "unknown error"));
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
s6.stop("Database schema initialized");
|
|
195
|
+
console.error("Deployment is incomplete; no success configuration was saved.");
|
|
196
|
+
process.exit(1);
|
|
176
197
|
}
|
|
198
|
+
s6.stop("Database schema initialized");
|
|
177
199
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
|
|
182
229
|
const config = saveConfig({
|
|
183
230
|
workerUrl,
|
|
184
|
-
mcpSecret,
|
|
185
231
|
partner1,
|
|
186
|
-
partner2,
|
|
187
|
-
dbId,
|
|
188
|
-
deployedAt: new Date().toISOString(),
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const mcpUrl = workerUrl + "/mcp
|
|
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:")}
|
|
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:")}
|
|
200
246
|
${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")} (
|
|
212
|
-
${cyan(mcpUrl)}
|
|
213
|
-
|
|
214
|
-
${dim("Config saved to: " + CONFIG_PATH)}
|
|
215
|
-
`);
|
|
216
|
-
}
|
|
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
|
+
}
|
package/cli/commands/mcp.js
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
console.log(bold(" MCP Configuration\n"));
|
|
20
|
-
|
|
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
21
|
console.log(` ${bold("Endpoint:")} ${cyan(mcpUrl)}\n`);
|
|
22
|
-
|
|
23
|
-
console.log(
|
|
24
|
-
|
|
25
|
-
console.log(`
|
|
26
|
-
console.log(`
|
|
27
|
-
console.log(`
|
|
28
|
-
console.log(`
|
|
29
|
-
console.log(`
|
|
30
|
-
console.log(`
|
|
31
|
-
|
|
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
|
+
|
|
32
34
|
console.log(` ${bold("For Claude Desktop")} (Settings > Developer > MCP):\n`);
|
|
33
|
-
console.log(` Name: hearth-dash`);
|
|
35
|
+
console.log(` Name: hearth-dash`);
|
|
34
36
|
console.log(` URL: ${mcpUrl}\n`);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.log(` ${
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
console.log();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 = {};
|
|
65
68
|
claudeConfig.mcpServers["hearth-dash"] = { url: mcpUrl };
|
|
66
69
|
writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2) + "\n", "utf-8");
|
|
70
|
+
try { chmodSync(claudeConfigPath, 0o600); } catch {}
|
|
67
71
|
info("MCP config written to " + claudeConfigPath);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
72
|
+
}
|
|
73
|
+
}
|