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