meldivo 1.0.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/THIRD_PARTY_NOTICES.md +59 -0
  4. package/bin/meldivo.mjs +514 -0
  5. package/dist/server/harnesses/claude.js +407 -0
  6. package/dist/server/harnesses/claude.js.map +1 -0
  7. package/dist/server/harnesses/index.js +27 -0
  8. package/dist/server/harnesses/index.js.map +1 -0
  9. package/dist/server/harnesses/opencode.js +391 -0
  10. package/dist/server/harnesses/opencode.js.map +1 -0
  11. package/dist/server/harnesses/pi.js +364 -0
  12. package/dist/server/harnesses/pi.js.map +1 -0
  13. package/dist/server/harnesses/types.js +2 -0
  14. package/dist/server/harnesses/types.js.map +1 -0
  15. package/dist/server/hub.js +126 -0
  16. package/dist/server/hub.js.map +1 -0
  17. package/dist/server/index.js +515 -0
  18. package/dist/server/index.js.map +1 -0
  19. package/dist/server/logger.js +98 -0
  20. package/dist/server/logger.js.map +1 -0
  21. package/dist/server/qr.js +16 -0
  22. package/dist/server/qr.js.map +1 -0
  23. package/dist/server/remote.js +166 -0
  24. package/dist/server/remote.js.map +1 -0
  25. package/dist/server/speech.js +323 -0
  26. package/dist/server/speech.js.map +1 -0
  27. package/dist/server/state.js +91 -0
  28. package/dist/server/state.js.map +1 -0
  29. package/dist/web/apple-touch-icon.png +0 -0
  30. package/dist/web/assets/_commonjsHelpers-CqkleIqs.js +1 -0
  31. package/dist/web/assets/browser-CujOq0-h.js +8 -0
  32. package/dist/web/assets/index-B1UGHGdh.css +1 -0
  33. package/dist/web/assets/index-CNq_igvY.js +43 -0
  34. package/dist/web/assets/index-CeWj4ejR.js +2948 -0
  35. package/dist/web/favicon-32.png +0 -0
  36. package/dist/web/index.html +15 -0
  37. package/dist/web/logo-192.png +0 -0
  38. package/dist/web/logo-512.png +0 -0
  39. package/dist/web/voice-assets/ort-wasm-simd-threaded.mjs +59 -0
  40. package/dist/web/voice-assets/ort-wasm-simd-threaded.wasm +0 -0
  41. package/dist/web/voice-assets/silero_vad_v5.onnx +0 -0
  42. package/dist/web/voice-assets/vad.worklet.bundle.min.js +1 -0
  43. package/package.json +78 -0
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env node
2
+ // Meldivo CLI: install/run the hub as a user service, and talk to it once it
3
+ // is up. Plain Node ESM, no build step — this file ships as-is.
4
+
5
+ import { randomBytes } from "node:crypto";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import {
8
+ chmodSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ openSync,
12
+ readFileSync,
13
+ rmSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+
20
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21
+ const pkgJson = JSON.parse(readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
22
+ const serverEntry = path.join(pkgRoot, "dist", "server", "index.js");
23
+
24
+ const DEFAULT_PORT = 4100;
25
+ const DEFAULT_HTTPS_PORT = 4443;
26
+
27
+ function configDir() {
28
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
29
+ return path.join(base, "meldivo");
30
+ }
31
+
32
+ function stateDir() {
33
+ const base = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
34
+ return path.join(base, "meldivo");
35
+ }
36
+
37
+ function secretPath() {
38
+ return path.join(configDir(), "secret");
39
+ }
40
+
41
+ function runtimePath() {
42
+ return path.join(configDir(), "runtime.json");
43
+ }
44
+
45
+ function pidPath() {
46
+ return path.join(stateDir(), "meldivo.pid");
47
+ }
48
+
49
+ function logPath() {
50
+ return path.join(stateDir(), "meldivo.log");
51
+ }
52
+
53
+ function systemdUnitPath() {
54
+ return path.join(os.homedir(), ".config", "systemd", "user", "meldivo.service");
55
+ }
56
+
57
+ function launchAgentPath() {
58
+ return path.join(os.homedir(), "Library", "LaunchAgents", "dev.meldivo.plist");
59
+ }
60
+
61
+ function ensureDir(dir, mode) {
62
+ mkdirSync(dir, { recursive: true, mode });
63
+ }
64
+
65
+ function ensureSecret() {
66
+ ensureDir(configDir(), 0o700);
67
+ const file = secretPath();
68
+ if (existsSync(file)) {
69
+ const existing = readFileSync(file, "utf8").trim();
70
+ if (existing) return existing;
71
+ }
72
+ const secret = randomBytes(32).toString("hex");
73
+ writeFileSync(file, secret, { mode: 0o600 });
74
+ chmodSync(file, 0o600);
75
+ return secret;
76
+ }
77
+
78
+ function readRuntime() {
79
+ try {
80
+ return JSON.parse(readFileSync(runtimePath(), "utf8"));
81
+ } catch {
82
+ return {};
83
+ }
84
+ }
85
+
86
+ function writeRuntime(data) {
87
+ ensureDir(configDir(), 0o700);
88
+ writeFileSync(runtimePath(), JSON.stringify(data, null, 2), { mode: 0o600 });
89
+ }
90
+
91
+ function resolvePort() {
92
+ return Number(process.env.MELDIVO_PORT) || readRuntime().port || DEFAULT_PORT;
93
+ }
94
+
95
+ function resolveHttpsPort() {
96
+ return Number(process.env.MELDIVO_HTTPS_PORT) || readRuntime().httpsPort || DEFAULT_HTTPS_PORT;
97
+ }
98
+
99
+ function baseUrl() {
100
+ return `http://127.0.0.1:${resolvePort()}`;
101
+ }
102
+
103
+ async function qrText(url) {
104
+ const { qrText: render } = await import(pathToFileURL(path.join(pkgRoot, "dist", "server", "qr.js")).href);
105
+ return render(url);
106
+ }
107
+
108
+ function hubUrl(secret) {
109
+ return `${baseUrl()}/#token=${encodeURIComponent(secret)}`;
110
+ }
111
+
112
+ async function printUrl(secret) {
113
+ const url = hubUrl(secret);
114
+ console.log(url);
115
+ try {
116
+ console.log(await qrText(url));
117
+ } catch (error) {
118
+ console.warn(`(could not render QR code: ${error instanceof Error ? error.message : String(error)})`);
119
+ }
120
+ }
121
+
122
+ async function waitForHealth(port, timeoutMs = 20_000) {
123
+ const deadline = Date.now() + timeoutMs;
124
+ while (Date.now() < deadline) {
125
+ try {
126
+ const response = await fetch(`http://127.0.0.1:${port}/api/health`);
127
+ if (response.ok) return await response.json();
128
+ } catch {
129
+ // not up yet
130
+ }
131
+ await sleep(300);
132
+ }
133
+ throw new Error(`Meldivo did not become healthy within ${timeoutMs}ms on port ${port}`);
134
+ }
135
+
136
+ function sleep(ms) {
137
+ return new Promise((resolve) => setTimeout(resolve, ms));
138
+ }
139
+
140
+ function hasSystemdUser() {
141
+ const result = spawnSync("systemctl", ["--user", "status"], { stdio: "ignore" });
142
+ return result.error === undefined && result.status !== 127;
143
+ }
144
+
145
+ function serviceEnv(port, httpsPort) {
146
+ const env = {
147
+ MELDIVO_SECRET: ensureSecret(),
148
+ MELDIVO_PORT: String(port),
149
+ MELDIVO_HTTPS_PORT: String(httpsPort),
150
+ };
151
+ if (process.env.MELDIVO_PUBLIC_URL) env.MELDIVO_PUBLIC_URL = process.env.MELDIVO_PUBLIC_URL;
152
+ if (process.env.MELDIVO_MODELS_DIR) env.MELDIVO_MODELS_DIR = process.env.MELDIVO_MODELS_DIR;
153
+ env.PATH = servicePath();
154
+ return env;
155
+ }
156
+
157
+ // Services start with a minimal PATH, so capture the user's PATH plus the usual
158
+ // install locations; the hub must find `pi`, `opencode`, `claude`, and `node`.
159
+ function servicePath() {
160
+ const home = os.homedir();
161
+ const dirs = [
162
+ path.dirname(process.execPath),
163
+ ...(process.env.PATH ?? "").split(path.delimiter),
164
+ path.join(home, ".local", "bin"),
165
+ path.join(home, ".npm-global", "bin"),
166
+ path.join(home, ".bun", "bin"),
167
+ path.join(home, ".opencode", "bin"),
168
+ path.join(home, ".claude", "local"),
169
+ "/opt/homebrew/bin",
170
+ "/usr/local/bin",
171
+ "/usr/bin",
172
+ "/bin",
173
+ ];
174
+ return [...new Set(dirs.filter((dir) => dir && existsSync(dir)))].join(path.delimiter);
175
+ }
176
+
177
+ function installSystemdUnit(env) {
178
+ const unitDir = path.dirname(systemdUnitPath());
179
+ ensureDir(unitDir, 0o700);
180
+ const envLines = Object.entries(env)
181
+ .map(([key, value]) => `Environment="${key}=${value}"`)
182
+ .join("\n");
183
+ const unit = `[Unit]
184
+ Description=Meldivo voice hub
185
+ After=network.target
186
+
187
+ [Service]
188
+ Type=simple
189
+ ExecStart=${process.execPath} ${serverEntry}
190
+ ${envLines}
191
+ Restart=always
192
+ RestartSec=2
193
+
194
+ [Install]
195
+ WantedBy=default.target
196
+ `;
197
+ writeFileSync(systemdUnitPath(), unit, { mode: 0o600 });
198
+ run("systemctl", ["--user", "daemon-reload"]);
199
+ run("systemctl", ["--user", "enable", "--now", "meldivo.service"]);
200
+ run("systemctl", ["--user", "restart", "meldivo.service"]); // pick up a new unit or package version
201
+ }
202
+
203
+ function installLaunchAgent(env) {
204
+ ensureDir(path.dirname(launchAgentPath()), 0o700);
205
+ const envEntries = Object.entries(env)
206
+ .map(([key, value]) => ` <key>${key}</key>\n <string>${value}</string>`)
207
+ .join("\n");
208
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
209
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
210
+ <plist version="1.0">
211
+ <dict>
212
+ <key>Label</key>
213
+ <string>dev.meldivo</string>
214
+ <key>ProgramArguments</key>
215
+ <array>
216
+ <string>${process.execPath}</string>
217
+ <string>${serverEntry}</string>
218
+ </array>
219
+ <key>EnvironmentVariables</key>
220
+ <dict>
221
+ ${envEntries}
222
+ </dict>
223
+ <key>KeepAlive</key>
224
+ <true/>
225
+ <key>RunAtLoad</key>
226
+ <true/>
227
+ <key>StandardOutPath</key>
228
+ <string>${logPath()}</string>
229
+ <key>StandardErrorPath</key>
230
+ <string>${logPath()}</string>
231
+ </dict>
232
+ </plist>
233
+ `;
234
+ ensureDir(stateDir(), 0o700);
235
+ writeFileSync(launchAgentPath(), plist, { mode: 0o600 });
236
+ const uid = process.getuid?.() ?? 0;
237
+ run("launchctl", ["bootout", `gui/${uid}/dev.meldivo`]); // ignore failure if not loaded
238
+ run("launchctl", ["bootstrap", `gui/${uid}`, launchAgentPath()]);
239
+ run("launchctl", ["kickstart", "-k", `gui/${uid}/dev.meldivo`]);
240
+ }
241
+
242
+ function startDetached(env) {
243
+ ensureDir(stateDir(), 0o700);
244
+ const out = openLogFd();
245
+ const child = spawn(process.execPath, [serverEntry], {
246
+ env: { ...process.env, ...env },
247
+ detached: true,
248
+ stdio: ["ignore", out, out],
249
+ });
250
+ child.unref();
251
+ writeFileSync(pidPath(), String(child.pid), { mode: 0o600 });
252
+ }
253
+
254
+ function openLogFd() {
255
+ ensureDir(stateDir(), 0o700);
256
+ return openSync(logPath(), "a");
257
+ }
258
+
259
+ function run(cmd, args) {
260
+ const result = spawnSync(cmd, args, { stdio: "inherit" });
261
+ if (result.error) throw result.error;
262
+ return result.status === 0;
263
+ }
264
+
265
+ function stopSystemd() {
266
+ run("systemctl", ["--user", "disable", "--now", "meldivo.service"]);
267
+ }
268
+
269
+ function stopLaunchAgent() {
270
+ const uid = process.getuid?.() ?? 0;
271
+ run("launchctl", ["bootout", `gui/${uid}/dev.meldivo`]);
272
+ }
273
+
274
+ function stopDetached() {
275
+ if (!existsSync(pidPath())) return;
276
+ const pid = Number(readFileSync(pidPath(), "utf8").trim());
277
+ if (Number.isFinite(pid)) {
278
+ try {
279
+ process.kill(pid, "SIGTERM");
280
+ } catch {
281
+ // already gone
282
+ }
283
+ }
284
+ rmSync(pidPath(), { force: true });
285
+ }
286
+
287
+ function serviceMode() {
288
+ if (process.platform === "linux" && hasSystemdUser()) return "systemd";
289
+ if (process.platform === "darwin") return "launchd";
290
+ return "detached";
291
+ }
292
+
293
+ async function cmdStart(args) {
294
+ const foreground = args.includes("--foreground");
295
+ const port = resolvePort();
296
+ const httpsPort = resolveHttpsPort();
297
+ const secret = ensureSecret();
298
+ writeRuntime({ port, httpsPort });
299
+
300
+ if (foreground) {
301
+ process.env.MELDIVO_SECRET = secret;
302
+ process.env.MELDIVO_PORT = String(port);
303
+ process.env.MELDIVO_HTTPS_PORT = String(httpsPort);
304
+ const { startServer } = await import(pathToFileURL(serverEntry).href);
305
+ const server = await startServer({ port, host: "127.0.0.1", secret, httpsPort });
306
+ console.log(`meldivo running in the foreground on port ${server.port}`);
307
+ await printUrl(secret);
308
+ const shutdown = () => server.close().then(() => process.exit(0));
309
+ process.on("SIGINT", shutdown);
310
+ process.on("SIGTERM", shutdown);
311
+ return;
312
+ }
313
+
314
+ const env = serviceEnv(port, httpsPort);
315
+ const mode = serviceMode();
316
+ if (mode === "systemd") {
317
+ installSystemdUnit(env);
318
+ console.log("Installed and started the meldivo systemd user service.");
319
+ console.log("To keep it running after logout: loginctl enable-linger $USER");
320
+ } else if (mode === "launchd") {
321
+ installLaunchAgent(env);
322
+ console.log("Installed and started the meldivo LaunchAgent.");
323
+ } else {
324
+ startDetached(env);
325
+ console.log(`Started meldivo as a background process (no user service manager found). Logs: ${logPath()}`);
326
+ }
327
+
328
+ await waitForHealth(port);
329
+ console.log("meldivo is up:");
330
+ await printUrl(secret);
331
+ }
332
+
333
+ async function cmdStop() {
334
+ const mode = serviceMode();
335
+ if (mode === "systemd" && existsSync(systemdUnitPath())) stopSystemd();
336
+ else if (mode === "launchd" && existsSync(launchAgentPath())) stopLaunchAgent();
337
+ else stopDetached();
338
+ console.log("meldivo stopped.");
339
+ }
340
+
341
+ async function cmdStatus() {
342
+ const port = resolvePort();
343
+ console.log(`Config: ${configDir()}`);
344
+ console.log(`Port: ${port}`);
345
+ const mode = serviceMode();
346
+ console.log(`Service manager: ${mode}`);
347
+ if (mode === "systemd") {
348
+ spawnSync("systemctl", ["--user", "is-active", "meldivo.service"], { stdio: "inherit" });
349
+ } else if (mode === "launchd") {
350
+ const uid = process.getuid?.() ?? 0;
351
+ spawnSync("launchctl", ["print", `gui/${uid}/dev.meldivo`], { stdio: "inherit" });
352
+ } else if (existsSync(pidPath())) {
353
+ console.log(`pid file: ${pidPath()} (pid ${readFileSync(pidPath(), "utf8").trim()})`);
354
+ } else {
355
+ console.log("No detached pid file found.");
356
+ }
357
+ try {
358
+ const health = await waitForHealth(port, 2_000);
359
+ console.log(`Health: ok, speech=${JSON.stringify(health.speech)}`);
360
+ } catch {
361
+ console.log("Health: unreachable");
362
+ }
363
+ }
364
+
365
+ async function cmdOpen(args) {
366
+ const secret = ensureSecret();
367
+ const url = hubUrl(secret);
368
+ console.log(url);
369
+ await printUrl(secret).then(() => {}).catch(() => {});
370
+ if (args.includes("--browser")) openInBrowser(url);
371
+ }
372
+
373
+ function openInBrowser(url) {
374
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
375
+ try {
376
+ spawnSync(opener, [url], { stdio: "ignore" });
377
+ } catch {
378
+ // best effort
379
+ }
380
+ }
381
+
382
+ async function apiCall(path_, method, secret, body) {
383
+ const response = await fetch(`${baseUrl()}${path_}`, {
384
+ method,
385
+ headers: { "content-type": "application/json", authorization: `Bearer ${secret}` },
386
+ body: body === undefined ? undefined : JSON.stringify(body),
387
+ });
388
+ const text = await response.text();
389
+ const payload = text ? JSON.parse(text) : null;
390
+ if (!response.ok) throw new Error(payload?.error ?? `Meldivo server returned ${response.status}`);
391
+ return payload;
392
+ }
393
+
394
+ async function cmdRemote(args) {
395
+ const secret = ensureSecret();
396
+ const sub = args[0];
397
+ if (!sub) {
398
+ const info = await apiCall("/api/remote", "GET", secret);
399
+ console.log("Remote access options:");
400
+ for (const option of info.options) console.log(` ${option.id}: ${option.ready ? "ready" : "not set up"} - ${option.label}`);
401
+ console.log(info.active ? `Active: ${info.active.id} -> ${info.active.url}` : "Active: none");
402
+ if (!info.options.some((option) => option.ready)) console.log(`Setup guide: ${info.guide}`);
403
+ return;
404
+ }
405
+ if (sub === "stop") {
406
+ await apiCall("/api/remote", "DELETE", secret);
407
+ console.log("Remote access turned off.");
408
+ return;
409
+ }
410
+ if (sub !== "tailscale" && sub !== "cloudflare" && sub !== "certificate") {
411
+ console.error(`Unknown remote option "${sub}". Use tailscale, cloudflare, certificate, or stop.`);
412
+ process.exitCode = 1;
413
+ return;
414
+ }
415
+ const result = await apiCall("/api/remote", "POST", secret, { id: sub });
416
+ console.log(result.url);
417
+ console.log(await qrText(result.url));
418
+ for (const alt of result.alternatives ?? []) console.log(`Also reachable at: ${alt}`);
419
+ }
420
+
421
+ async function cmdUninstall(args) {
422
+ await cmdStop();
423
+ const mode = serviceMode();
424
+ if (mode === "systemd" && existsSync(systemdUnitPath())) rmSync(systemdUnitPath(), { force: true });
425
+ if (mode === "launchd" && existsSync(launchAgentPath())) rmSync(launchAgentPath(), { force: true });
426
+ if (args.includes("--purge")) {
427
+ rmSync(configDir(), { recursive: true, force: true });
428
+ rmSync(stateDir(), { recursive: true, force: true });
429
+ const cacheDir = path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "meldivo");
430
+ rmSync(cacheDir, { recursive: true, force: true });
431
+ console.log("Removed meldivo config, state, and cache.");
432
+ }
433
+ console.log("meldivo uninstalled.");
434
+ }
435
+
436
+ function cmdLogs() {
437
+ const mode = serviceMode();
438
+ if (mode === "systemd") {
439
+ spawnSync("journalctl", ["--user", "-u", "meldivo", "-f"], { stdio: "inherit" });
440
+ return;
441
+ }
442
+ const file = logPath();
443
+ if (!existsSync(file)) {
444
+ console.log(`No log file yet at ${file}`);
445
+ return;
446
+ }
447
+ spawnSync("tail", ["-n", "200", "-f", file], { stdio: "inherit" });
448
+ }
449
+
450
+ function printHelp() {
451
+ console.log(`meldivo ${pkgJson.version}
452
+
453
+ Usage: meldivo <command> [options]
454
+
455
+ Commands:
456
+ start [--foreground] Start the meldivo hub as a user service
457
+ stop Stop and disable the service
458
+ status Show health, speech, and service state
459
+ open [--browser] Print the hub URL and QR (optionally open it)
460
+ remote [tailscale|cloudflare|certificate|stop]
461
+ Manage remote access
462
+ uninstall [--purge] Stop and remove the service (optionally wipe config/cache/state)
463
+ logs Tail service logs
464
+ --version Print the version
465
+ --help Show this help
466
+ `);
467
+ }
468
+
469
+ async function main() {
470
+ const [, , command, ...rest] = process.argv;
471
+ try {
472
+ switch (command) {
473
+ case "start":
474
+ await cmdStart(rest);
475
+ break;
476
+ case "stop":
477
+ await cmdStop();
478
+ break;
479
+ case "status":
480
+ await cmdStatus();
481
+ break;
482
+ case "open":
483
+ await cmdOpen(rest);
484
+ break;
485
+ case "remote":
486
+ await cmdRemote(rest);
487
+ break;
488
+ case "uninstall":
489
+ await cmdUninstall(rest);
490
+ break;
491
+ case "logs":
492
+ cmdLogs();
493
+ break;
494
+ case "--version":
495
+ case "-v":
496
+ console.log(pkgJson.version);
497
+ break;
498
+ case "--help":
499
+ case "-h":
500
+ case undefined:
501
+ printHelp();
502
+ break;
503
+ default:
504
+ console.error(`Unknown command "${command}"`);
505
+ printHelp();
506
+ process.exitCode = 1;
507
+ }
508
+ } catch (error) {
509
+ console.error(error instanceof Error ? error.message : String(error));
510
+ process.exitCode = 1;
511
+ }
512
+ }
513
+
514
+ await main();