viveworker 0.5.5 → 0.6.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.
@@ -81,6 +81,12 @@ async function main(cliOptions) {
81
81
  case "setup":
82
82
  await runSetup(cliOptions);
83
83
  return;
84
+ case "pair":
85
+ await runPair(cliOptions);
86
+ return;
87
+ case "enable":
88
+ await runEnable(cliOptions);
89
+ return;
84
90
  case "start":
85
91
  await runStart(cliOptions);
86
92
  return;
@@ -219,6 +225,14 @@ async function runSetup(cliOptions) {
219
225
 
220
226
  await fs.writeFile(envFile, `${envLines.join("\n")}\n`, "utf8");
221
227
 
228
+ progress.update("cli.setup.progress.providers");
229
+ const providerSetup = await autoConfigureProvidersDuringSetup({
230
+ cliOptions,
231
+ envFile,
232
+ sessionSecret,
233
+ codexHome: existing.CODEX_HOME || process.env.CODEX_HOME || path.join(os.homedir(), ".codex"),
234
+ });
235
+
222
236
  if (!cliOptions.noLaunchd) {
223
237
  progress.update("cli.setup.progress.launchd");
224
238
  const plist = buildLaunchAgentPlist({
@@ -258,8 +272,9 @@ async function runSetup(cliOptions) {
258
272
  let caDownloadLocalUrl = `${publicBaseUrl}${caPath}`;
259
273
  let caDownloadIpUrl = `${fallbackBaseUrl}${caPath}`;
260
274
  let temporaryCaServer = null;
275
+ const shouldPromptCaTrust = Boolean(webPushEnabled && canShowCaDownload && !cliOptions.pair && tlsAssets?.ranMkcertInstall);
261
276
 
262
- if (cliOptions.installMkcert && canShowCaDownload) {
277
+ if (shouldPromptCaTrust) {
263
278
  temporaryCaServer = await startTemporaryCaDownloadServer({
264
279
  rootCaFile: mkcertRootCaFile,
265
280
  preferredPort: port + 1,
@@ -289,77 +304,184 @@ async function runSetup(cliOptions) {
289
304
  }
290
305
  }
291
306
 
292
- console.log("");
293
- if (cliOptions.pair) {
294
- console.log(t(locale, "cli.setup.pairRefresh.title"));
295
- console.log(t(locale, "cli.setup.pairRefresh.copy"));
296
- console.log(t(locale, "cli.setup.pairRefresh.reminder"));
297
- console.log("");
298
- }
299
- console.log(t(locale, "cli.setup.primaryUrl", { url: publicBaseUrl }));
300
- console.log(t(locale, "cli.setup.fallbackUrl", { url: fallbackBaseUrl }));
301
- console.log(t(locale, "cli.setup.pairingCode", { code: pairCode }));
302
- console.log(t(locale, "cli.setup.pairingUrlLocal", { url: `${publicBaseUrl}${pairPath}` }));
303
- console.log(t(locale, "cli.setup.pairingUrlIp", { url: `${fallbackBaseUrl}${pairPath}` }));
304
- console.log(t(locale, webPushEnabled ? "cli.setup.webPushEnabled" : "cli.setup.webPushDisabled"));
305
- if (allowInsecureHttpLan) {
306
- console.log(t(locale, "cli.setup.warning.insecureHttpLan"));
307
- }
308
- if (canShowCaDownload && !cliOptions.installMkcert && !cliOptions.pair) {
309
- console.log(t(locale, "cli.setup.caDownloadLocal", { url: caDownloadLocalUrl }));
310
- console.log(t(locale, "cli.setup.caDownloadIp", { url: caDownloadIpUrl }));
307
+ printCliSection(locale, "cli.section.ready", [
308
+ t(locale, "cli.setup.primaryUrl", { url: publicBaseUrl }),
309
+ t(locale, "cli.setup.fallbackUrl", { url: fallbackBaseUrl }),
310
+ t(locale, "cli.setup.pairingCode", { code: pairCode }),
311
+ t(locale, webPushEnabled ? "cli.setup.webPushEnabled" : "cli.setup.webPushDisabled"),
312
+ ...getSetupProviderSummaryLines(locale, providerSetup),
313
+ ]);
314
+ printCliSection(locale, "cli.section.needsAttention", [
315
+ allowInsecureHttpLan ? t(locale, "cli.setup.warning.insecureHttpLan") : "",
316
+ healthy && !pairingReady ? t(locale, "cli.setup.warning.stalePairingServer", { port }) : "",
317
+ ]);
318
+ printCliSection(locale, "cli.section.next", [
319
+ cliOptions.pair ? t(locale, "cli.setup.pairRefresh.copy") : "",
320
+ cliOptions.pair ? t(locale, "cli.setup.pairRefresh.reminder") : "",
321
+ webPushEnabled
322
+ ? t(locale, shouldPromptCaTrust ? "cli.setup.instructions.afterCa" : "cli.setup.instructions.https")
323
+ : allowInsecureHttpLan
324
+ ? t(locale, "cli.setup.instructions.insecureHttpLan")
325
+ : t(locale, "cli.setup.instructions.localOnlyHttp"),
326
+ ]);
327
+ printCliSection(locale, "cli.section.pairingLinks", [
328
+ t(locale, "cli.setup.pairingUrlLocal", { url: `${publicBaseUrl}${pairPath}` }),
329
+ t(locale, "cli.setup.pairingUrlIp", { url: `${fallbackBaseUrl}${pairPath}` }),
330
+ ]);
331
+ if (canShowCaDownload && !shouldPromptCaTrust && !cliOptions.pair) {
332
+ printCliSection(locale, "cli.section.caDownload", [
333
+ t(locale, "cli.setup.caDownloadLocal", { url: caDownloadLocalUrl }),
334
+ t(locale, "cli.setup.caDownloadIp", { url: caDownloadIpUrl }),
335
+ ]);
311
336
  }
312
337
  console.log("");
313
- if (webPushEnabled) {
314
- console.log(t(locale, cliOptions.installMkcert ? "cli.setup.instructions.afterCa" : "cli.setup.instructions.https"));
315
- } else if (allowInsecureHttpLan) {
316
- console.log(t(locale, "cli.setup.instructions.insecureHttpLan"));
317
- } else {
318
- console.log(t(locale, "cli.setup.instructions.localOnlyHttp"));
319
- }
320
- console.log("");
321
- console.log(t(locale, "cli.setup.qrPairing"));
338
+ console.log(t(locale, "cli.section.qrPairing"));
322
339
  await printQrCode(`${publicBaseUrl}${pairPath}`);
323
- if (canShowCaDownload && !cliOptions.installMkcert && !cliOptions.pair) {
340
+ if (canShowCaDownload && !shouldPromptCaTrust && !cliOptions.pair) {
324
341
  console.log("");
325
- console.log(t(locale, "cli.setup.qrCaDownload"));
342
+ console.log(t(locale, "cli.section.qrCaDownload"));
326
343
  await printQrCode(caDownloadIpUrl);
327
344
  }
328
345
 
329
- const claudeSettingsPath = resolvePath(
330
- cliOptions.claudeSettingsFile || path.join(os.homedir(), ".claude", "settings.json")
331
- );
332
- const claudeHomeExists = await fileExists(path.dirname(claudeSettingsPath));
333
- if (cliOptions.claudeSettingsFile || claudeHomeExists) {
334
- await installClaudeHooks({
335
- envFile,
336
- claudeSettingsFile: cliOptions.claudeSettingsFile,
337
- sessionSecret,
338
- });
339
- } else {
346
+ await maybeRunLegacySetupFeatureAliases(cliOptions, { envFile, locale, sessionSecret, port });
347
+ }
348
+
349
+ async function runPair(cliOptions) {
350
+ const setup = await loadExistingSetup(cliOptions);
351
+ const progress = createCliProgressReporter(setup.locale);
352
+ progress.update("cli.start.progress.refreshPairing");
353
+ await refreshPairingCredentials(setup.envFile, setup.config, { force: true });
354
+ progress.clear();
355
+ await runStart(cliOptions);
356
+ const nextConfig = await ensureDefaultLocalePersisted(setup.envFile, cliOptions);
357
+ printCliTitle(setup.locale, "cli.pair.done");
358
+ printCliSection(setup.locale, "cli.section.ready", [
359
+ t(setup.locale, "cli.setup.primaryUrl", { url: String(nextConfig.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "").trim() || "(not configured)" }),
360
+ t(setup.locale, "cli.setup.pairingCode", { code: String(nextConfig.PAIRING_CODE || "").trim() || "(missing)" }),
361
+ ]);
362
+ printCliSection(setup.locale, "cli.section.next", [
363
+ t(setup.locale, "cli.setup.pairRefresh.copy"),
364
+ t(setup.locale, "cli.setup.pairRefresh.reminder"),
365
+ ]);
366
+ await printPairingInfo(setup.locale, nextConfig, { sectioned: true });
367
+ const pairToken = String(nextConfig.PAIRING_TOKEN || "").trim();
368
+ if (pairToken && nextConfig.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL) {
340
369
  console.log("");
341
- console.log(t(locale, "cli.setup.claudeHooksSkipped"));
370
+ console.log(t(setup.locale, "cli.section.qrPairing"));
371
+ await printQrCode(
372
+ `${String(nextConfig.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "").trim()}/app?pairToken=${encodeURIComponent(pairToken)}`
373
+ );
374
+ }
375
+ }
376
+
377
+ async function runEnable(cliOptions) {
378
+ const target = String(cliOptions.enableTarget || "").trim().toLowerCase();
379
+ if (!target) {
380
+ throw new Error("Usage: viveworker enable <claude|a2a|moltbook|scout> [...]");
342
381
  }
343
382
 
344
- if (cliOptions.moltbook) {
345
- try {
346
- await installMoltbookWatcher({ cliOptions, sessionSecret, port });
347
- } catch (error) {
348
- console.log("");
349
- console.log(`Moltbook watcher install failed: ${error.message}`);
383
+ switch (target) {
384
+ case "claude":
385
+ await runEnableClaude(cliOptions);
386
+ return;
387
+ case "a2a": {
388
+ const { runA2ACli } = await import("./a2a-cli.mjs");
389
+ await runA2ACli(["setup", ...(cliOptions.enableArgs || [])]);
390
+ return;
350
391
  }
392
+ case "moltbook":
393
+ await runEnableMoltbook(cliOptions);
394
+ return;
395
+ case "scout":
396
+ await runEnableScout(cliOptions);
397
+ return;
398
+ default:
399
+ throw new Error(`Unknown feature: ${target}`);
351
400
  }
401
+ }
352
402
 
403
+ async function runEnableClaude(cliOptions) {
404
+ const setup = await loadExistingSetup(cliOptions);
405
+ const settingsFile = resolvePath(
406
+ cliOptions.claudeSettingsFile || path.join(os.homedir(), ".claude", "settings.json")
407
+ );
408
+ if (!cliOptions.claudeSettingsFile && !(await fileExists(path.dirname(settingsFile)))) {
409
+ throw new Error("Claude Desktop settings were not found. Install Claude Desktop first or pass --settings-file.");
410
+ }
411
+ if (!setup.config.SESSION_SECRET) {
412
+ throw new Error("SESSION_SECRET is missing. Run `npx viveworker doctor --fix` or `npx viveworker setup` first.");
413
+ }
414
+ const settingsPath = await installClaudeHooks({
415
+ envFile: setup.envFile,
416
+ claudeSettingsFile: cliOptions.claudeSettingsFile,
417
+ sessionSecret: setup.config.SESSION_SECRET,
418
+ suppressOutput: true,
419
+ });
420
+ printCliTitle(setup.locale, "cli.enable.claude.title");
421
+ printCliSection(setup.locale, "cli.section.changed", [
422
+ t(setup.locale, "cli.enable.claude.changed", { path: settingsPath }),
423
+ ]);
424
+ printCliSection(setup.locale, "cli.section.next", [
425
+ t(setup.locale, "cli.enable.claude.next"),
426
+ ]);
427
+ }
428
+
429
+ async function runEnableMoltbook(cliOptions) {
430
+ const setup = await loadExistingSetup(cliOptions);
431
+ if (!setup.config.SESSION_SECRET) {
432
+ throw new Error("SESSION_SECRET is missing. Run `npx viveworker doctor --fix` or `npx viveworker setup` first.");
433
+ }
434
+ const watcherResult = await installMoltbookWatcher({
435
+ cliOptions,
436
+ sessionSecret: setup.config.SESSION_SECRET,
437
+ port: Number(setup.config.NATIVE_APPROVAL_SERVER_PORT) || defaultServerPort,
438
+ suppressOutput: true,
439
+ });
440
+ let scoutResult = null;
441
+ if (!cliOptions.noScout) {
442
+ scoutResult = await installMoltbookScout({ cliOptions, suppressOutput: true });
443
+ }
444
+ printCliTitle(setup.locale, "cli.enable.moltbook.title");
445
+ printCliSection(setup.locale, "cli.section.changed", [
446
+ t(setup.locale, "cli.enable.moltbook.watcher", { path: watcherResult.plistPath }),
447
+ t(setup.locale, "cli.enable.moltbook.env", { path: watcherResult.moltbookEnvFile }),
448
+ scoutResult ? t(setup.locale, "cli.enable.moltbook.scout", { interval: scoutResult.interval }) : t(setup.locale, "cli.enable.moltbook.watcherOnly"),
449
+ ]);
450
+ printCliSection(setup.locale, "cli.section.needsAttention", [
451
+ watcherResult.warning || "",
452
+ scoutResult?.warning || "",
453
+ scoutResult?.manualNote || "",
454
+ scoutResult && !scoutResult.hasPersona ? t(setup.locale, "cli.enable.moltbook.personaTip") : "",
455
+ ]);
456
+ printCliSection(setup.locale, "cli.section.next", [
457
+ t(setup.locale, "cli.enable.moltbook.next"),
458
+ ]);
459
+ }
460
+
461
+ async function runEnableScout(cliOptions) {
462
+ const setup = await loadExistingSetup(cliOptions);
353
463
  if (cliOptions.autoScoutUninstall) {
354
- await uninstallMoltbookScout();
355
- } else if (cliOptions.autoScout) {
356
- try {
357
- await installMoltbookScout({ cliOptions });
358
- } catch (error) {
359
- console.log("");
360
- console.log(`Moltbook auto-scout install failed: ${error.message}`);
361
- }
464
+ const uninstallResult = await uninstallMoltbookScout({ suppressOutput: true });
465
+ printCliTitle(setup.locale, "cli.enable.scout.removed");
466
+ printCliSection(setup.locale, "cli.section.changed", [uninstallResult.message]);
467
+ return;
362
468
  }
469
+ const scoutResult = await installMoltbookScout({ cliOptions, suppressOutput: true });
470
+ printCliTitle(setup.locale, "cli.enable.scout.title");
471
+ printCliSection(setup.locale, "cli.section.changed", [
472
+ t(setup.locale, "cli.enable.scout.installed", { interval: scoutResult.interval }),
473
+ scoutResult.harness.kind === "manual"
474
+ ? t(setup.locale, "cli.enable.scout.manualHarness")
475
+ : t(setup.locale, "cli.enable.scout.harness", { path: scoutResult.harness.bin }),
476
+ ]);
477
+ printCliSection(setup.locale, "cli.section.needsAttention", [
478
+ scoutResult.warning || "",
479
+ scoutResult.manualNote || "",
480
+ !scoutResult.hasPersona ? t(setup.locale, "cli.enable.moltbook.personaTip") : "",
481
+ ]);
482
+ printCliSection(setup.locale, "cli.section.next", [
483
+ t(setup.locale, "cli.enable.scout.next"),
484
+ ]);
363
485
  }
364
486
 
365
487
  async function detectScoutHarness(preferred) {
@@ -382,7 +504,7 @@ async function detectScoutHarness(preferred) {
382
504
  return { kind: "manual", bin: "" };
383
505
  }
384
506
 
385
- async function uninstallMoltbookScout() {
507
+ async function uninstallMoltbookScout({ suppressOutput = false } = {}) {
386
508
  const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${"com.viveworker.moltbook-scout"}.plist`);
387
509
  const { spawn } = await import("node:child_process");
388
510
  await new Promise((resolve) => {
@@ -392,15 +514,23 @@ async function uninstallMoltbookScout() {
392
514
  });
393
515
  try {
394
516
  await fs.unlink(plistPath);
395
- console.log("");
396
- console.log(`Moltbook auto-scout uninstalled (removed ${plistPath}).`);
517
+ const message = `Moltbook auto-scout removed: ${plistPath}`;
518
+ if (!suppressOutput) {
519
+ console.log("");
520
+ console.log(message);
521
+ }
522
+ return { removed: true, plistPath, message };
397
523
  } catch {
398
- console.log("");
399
- console.log(`No auto-scout plist found at ${plistPath}.`);
524
+ const message = `No auto-scout plist was installed at ${plistPath}.`;
525
+ if (!suppressOutput) {
526
+ console.log("");
527
+ console.log(message);
528
+ }
529
+ return { removed: false, plistPath, message };
400
530
  }
401
531
  }
402
532
 
403
- async function installMoltbookScout({ cliOptions }) {
533
+ async function installMoltbookScout({ cliOptions, suppressOutput = false }) {
404
534
  const moltbookEnvFile = path.join(os.homedir(), ".viveworker", "moltbook.env");
405
535
  try {
406
536
  await fs.access(moltbookEnvFile);
@@ -481,6 +611,7 @@ async function installMoltbookScout({ cliOptions }) {
481
611
  p.on("exit", () => resolve());
482
612
  p.on("error", () => resolve());
483
613
  });
614
+ let warning = "";
484
615
  try {
485
616
  await new Promise((resolve, reject) => {
486
617
  const p = spawn("launchctl", ["bootstrap", `gui/${process.getuid()}`, plistPath], { stdio: "ignore" });
@@ -488,33 +619,45 @@ async function installMoltbookScout({ cliOptions }) {
488
619
  p.on("error", reject);
489
620
  });
490
621
  } catch (error) {
491
- console.log("");
492
- console.log(`Moltbook scout plist written but launchctl bootstrap failed: ${error.message}`);
493
- console.log(`Run manually: launchctl bootstrap gui/$(id -u) ${plistPath}`);
622
+ warning = `Moltbook scout plist was written, but launchctl bootstrap failed: ${error.message}. Run manually: launchctl bootstrap gui/$(id -u) ${plistPath}`;
494
623
  }
495
-
496
- console.log("");
497
- console.log(`Moltbook auto-scout installed (${harness.kind}).`);
498
- console.log(` plist: ${plistPath}`);
499
- console.log(` interval: ${interval}s`);
500
- if (harness.kind === "manual") {
501
- console.log(
502
- ` note: Neither 'codex' nor 'claude' CLI was found. The scheduled task will only run scout and print candidates to the log. Install Codex CLI (npm i -g @openai/codex) or Claude Code CLI to enable automated drafting.`
503
- );
504
- } else {
505
- console.log(` harness: ${harness.bin}`);
506
- }
507
- console.log(` logs: /tmp/viveworker-moltbook-scout.{out,err}.log`);
508
- if (!hasPersona) {
509
- console.log(
510
- ` tip: No persona file found. Run 'npx viveworker moltbook persona init' to describe your agent — draft quality improves a lot.`
511
- );
512
- } else {
513
- console.log(` persona: ${personaPath}`);
624
+ const manualNote = harness.kind === "manual"
625
+ ? `Neither 'codex' nor 'claude' CLI was found. The scheduled task will only run scout and print candidates to the log. Install Codex CLI (npm i -g @openai/codex) or Claude Code CLI to enable automated drafting.`
626
+ : "";
627
+ const result = {
628
+ plistPath,
629
+ interval,
630
+ harness,
631
+ hasPersona,
632
+ personaPath,
633
+ warning,
634
+ manualNote,
635
+ logsPath: "/tmp/viveworker-moltbook-scout.{out,err}.log",
636
+ };
637
+ if (!suppressOutput) {
638
+ console.log("");
639
+ console.log(`Moltbook auto-scout installed (${harness.kind}).`);
640
+ console.log(` plist: ${plistPath}`);
641
+ console.log(` interval: ${interval}s`);
642
+ if (manualNote) {
643
+ console.log(` note: ${manualNote}`);
644
+ } else {
645
+ console.log(` harness: ${harness.bin}`);
646
+ }
647
+ console.log(` logs: ${result.logsPath}`);
648
+ if (!hasPersona) {
649
+ console.log(` tip: No persona file found. Run 'npx viveworker moltbook persona init' to describe your agent — draft quality improves a lot.`);
650
+ } else {
651
+ console.log(` persona: ${personaPath}`);
652
+ }
653
+ if (warning) {
654
+ console.log(` warning: ${warning}`);
655
+ }
514
656
  }
657
+ return result;
515
658
  }
516
659
 
517
- async function installMoltbookWatcher({ cliOptions, sessionSecret, port }) {
660
+ async function installMoltbookWatcher({ cliOptions, sessionSecret, port, suppressOutput = false }) {
518
661
  if (!cliOptions.moltbookApiKey || !cliOptions.moltbookAgentId) {
519
662
  throw new Error(
520
663
  "--moltbook requires --moltbook-api-key and --moltbook-agent-id"
@@ -564,6 +707,7 @@ async function installMoltbookWatcher({ cliOptions, sessionSecret, port }) {
564
707
  await fs.writeFile(plistPath, plistBody, "utf8");
565
708
 
566
709
  // Reload the agent if launchctl is available
710
+ let warning = "";
567
711
  try {
568
712
  const { spawn } = await import("node:child_process");
569
713
  await new Promise((resolve) => {
@@ -577,20 +721,28 @@ async function installMoltbookWatcher({ cliOptions, sessionSecret, port }) {
577
721
  load.on("error", reject);
578
722
  });
579
723
  } catch (error) {
724
+ warning = `Moltbook watcher plist was written, but launchctl load failed: ${error.message}. Run manually: launchctl load ${plistPath}`;
725
+ }
726
+ const result = {
727
+ moltbookEnvFile,
728
+ plistPath,
729
+ logsPath: "/tmp/viveworker-moltbook-watcher.{out,err}.log",
730
+ warning,
731
+ };
732
+ if (!suppressOutput) {
580
733
  console.log("");
581
- console.log(`Moltbook watcher plist written but launchctl load failed: ${error.message}`);
582
- console.log(`Run manually: launchctl load ${plistPath}`);
583
- return;
734
+ console.log(`Moltbook watcher installed.`);
735
+ console.log(` env: ${moltbookEnvFile}`);
736
+ console.log(` plist: ${plistPath}`);
737
+ console.log(` logs: ${result.logsPath}`);
738
+ if (warning) {
739
+ console.log(` warning: ${warning}`);
740
+ }
584
741
  }
585
-
586
- console.log("");
587
- console.log(`Moltbook watcher installed.`);
588
- console.log(` env: ${moltbookEnvFile}`);
589
- console.log(` plist: ${plistPath}`);
590
- console.log(` logs: /tmp/viveworker-moltbook-watcher.{out,err}.log`);
742
+ return result;
591
743
  }
592
744
 
593
- async function installClaudeHooks({ envFile, claudeSettingsFile, sessionSecret }) {
745
+ async function installClaudeHooks({ envFile, claudeSettingsFile, sessionSecret, suppressOutput = false }) {
594
746
  const hookScript = path.join(packageRoot, "scripts", "viveworker-claude-hook.mjs");
595
747
  const nodePath = process.execPath;
596
748
  const settingsFile = resolvePath(
@@ -638,8 +790,12 @@ async function installClaudeHooks({ envFile, claudeSettingsFile, sessionSecret }
638
790
  await fs.mkdir(path.dirname(settingsFile), { recursive: true });
639
791
  await fs.writeFile(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
640
792
 
641
- console.log("");
642
- console.log(`Claude hooks installed: ${settingsFile}`);
793
+ if (!suppressOutput) {
794
+ console.log("");
795
+ console.log(`Claude hooks installed: ${settingsFile}`);
796
+ }
797
+
798
+ return settingsFile;
643
799
  }
644
800
 
645
801
  async function runStart(cliOptions) {
@@ -675,12 +831,22 @@ async function runStart(cliOptions) {
675
831
  ? await waitForExpectedPairing(config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "", rotatedPairing.pairingToken)
676
832
  : true;
677
833
  progress.done(healthy && pairingReady ? "cli.start.launchdStarted" : "cli.start.launchdStartedPending");
678
- if (healthy && !pairingReady) {
679
- console.log("");
680
- console.log(t(locale, "cli.setup.warning.stalePairingServer", { port: config.NATIVE_APPROVAL_SERVER_PORT || defaultServerPort }));
681
- }
834
+ printCliTitle(locale, "cli.start.title");
835
+ printCliSection(locale, "cli.section.ready", [
836
+ t(locale, "cli.status.baseUrl", { value: config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "(not configured)" }),
837
+ t(locale, "cli.start.readyLaunchd"),
838
+ t(locale, "cli.status.health", { value: t(locale, healthy ? "cli.status.ok" : "cli.status.failed") }),
839
+ ]);
840
+ printCliSection(locale, "cli.section.needsAttention", [
841
+ healthy && !pairingReady
842
+ ? t(locale, "cli.setup.warning.stalePairingServer", { port: config.NATIVE_APPROVAL_SERVER_PORT || defaultServerPort })
843
+ : "",
844
+ ]);
845
+ printCliSection(locale, "cli.section.next", [
846
+ rotatedPairing.rotated ? t(locale, "cli.start.nextPairing") : "",
847
+ ]);
682
848
  if (rotatedPairing.rotated) {
683
- await printPairingInfo(locale, config);
849
+ await printPairingInfo(locale, config, { sectioned: true });
684
850
  }
685
851
  return;
686
852
  }
@@ -697,12 +863,22 @@ async function runStart(cliOptions) {
697
863
  ? await waitForExpectedPairing(config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "", rotatedPairing.pairingToken)
698
864
  : true;
699
865
  progress.done(healthy && pairingReady ? "cli.start.bridgeStarted" : "cli.start.bridgeStartedPending");
700
- if (healthy && !pairingReady) {
701
- console.log("");
702
- console.log(t(locale, "cli.setup.warning.stalePairingServer", { port: config.NATIVE_APPROVAL_SERVER_PORT || defaultServerPort }));
703
- }
866
+ printCliTitle(locale, "cli.start.title");
867
+ printCliSection(locale, "cli.section.ready", [
868
+ t(locale, "cli.status.baseUrl", { value: config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "(not configured)" }),
869
+ t(locale, "cli.start.readyBridge"),
870
+ t(locale, "cli.status.health", { value: t(locale, healthy ? "cli.status.ok" : "cli.status.failed") }),
871
+ ]);
872
+ printCliSection(locale, "cli.section.needsAttention", [
873
+ healthy && !pairingReady
874
+ ? t(locale, "cli.setup.warning.stalePairingServer", { port: config.NATIVE_APPROVAL_SERVER_PORT || defaultServerPort })
875
+ : "",
876
+ ]);
877
+ printCliSection(locale, "cli.section.next", [
878
+ rotatedPairing.rotated ? t(locale, "cli.start.nextPairing") : "",
879
+ ]);
704
880
  if (rotatedPairing.rotated) {
705
- await printPairingInfo(locale, config);
881
+ await printPairingInfo(locale, config, { sectioned: true });
706
882
  }
707
883
  }
708
884
 
@@ -710,15 +886,22 @@ async function runStop(cliOptions) {
710
886
  const configDir = resolvePath(cliOptions.configDir || defaultConfigDir);
711
887
  const launchAgentPath = resolvePath(cliOptions.launchAgentPath || defaultLaunchAgentPath);
712
888
  const pidFile = resolvePath(cliOptions.pidFile || path.join(configDir, "viveworker.pid"));
889
+ const locale = await resolveCliLocale(cliOptions);
713
890
  if (await fileExists(launchAgentPath)) {
714
891
  await execCommand(["launchctl", "bootout", `gui/${process.getuid()}`, launchAgentPath], { ignoreError: true });
715
- console.log(t(await resolveCliLocale(cliOptions), "cli.stop.launchdStopped"));
892
+ printCliTitle(locale, "cli.stop.title");
893
+ printCliSection(locale, "cli.section.changed", [
894
+ t(locale, "cli.stop.changedLaunchd"),
895
+ ]);
716
896
  return;
717
897
  }
718
898
 
719
899
  const pid = await maybeReadPid(pidFile);
720
900
  if (!pid) {
721
- console.log(t(await resolveCliLocale(cliOptions), "cli.stop.noProcess"));
901
+ printCliTitle(locale, "cli.stop.alreadyStopped");
902
+ printCliSection(locale, "cli.section.ready", [
903
+ t(locale, "cli.stop.noProcess"),
904
+ ]);
722
905
  return;
723
906
  }
724
907
 
@@ -730,7 +913,10 @@ async function runStop(cliOptions) {
730
913
  }
731
914
  }
732
915
  await fs.rm(pidFile, { force: true });
733
- console.log(t(await resolveCliLocale(cliOptions), "cli.stop.stopped", { pid }));
916
+ printCliTitle(locale, "cli.stop.title");
917
+ printCliSection(locale, "cli.section.changed", [
918
+ t(locale, "cli.stop.changedPid", { pid }),
919
+ ]);
734
920
  }
735
921
 
736
922
  async function runStatus(cliOptions) {
@@ -745,15 +931,17 @@ async function runStatus(cliOptions) {
745
931
  const httpsEnabled = isHttpsUrl(baseUrl);
746
932
  const locale = await resolveCliLocale(cliOptions, config);
747
933
 
748
- console.log(t(locale, "cli.status.envFile", { value: envFile }));
749
- console.log(t(locale, "cli.status.baseUrl", { value: baseUrl || "(not configured)" }));
750
- console.log(t(locale, "cli.status.webPush", { value: t(locale, webPushEnabled ? "cli.status.enabled" : "cli.status.disabled") }));
751
- console.log(t(locale, "cli.status.https", { value: t(locale, httpsEnabled ? "cli.status.enabled" : "cli.status.disabled") }));
934
+ const readyLines = [
935
+ t(locale, "cli.status.envFile", { value: envFile }),
936
+ t(locale, "cli.status.baseUrl", { value: baseUrl || "(not configured)" }),
937
+ t(locale, "cli.status.webPush", { value: t(locale, webPushEnabled ? "cli.status.enabled" : "cli.status.disabled") }),
938
+ t(locale, "cli.status.https", { value: t(locale, httpsEnabled ? "cli.status.enabled" : "cli.status.disabled") }),
939
+ ];
752
940
  if (webPushEnabled) {
753
- console.log(t(locale, "cli.status.tlsCert", { value: config.TLS_CERT_FILE || "(missing)" }));
754
- console.log(t(locale, "cli.status.tlsKey", { value: config.TLS_KEY_FILE || "(missing)" }));
941
+ readyLines.push(t(locale, "cli.status.tlsCert", { value: config.TLS_CERT_FILE || "(missing)" }));
942
+ readyLines.push(t(locale, "cli.status.tlsKey", { value: config.TLS_KEY_FILE || "(missing)" }));
755
943
  }
756
- console.log(
944
+ readyLines.push(
757
945
  t(locale, "cli.status.launchAgent", {
758
946
  value: (await fileExists(launchAgentPath)) ? launchAgentPath : "(not installed)",
759
947
  })
@@ -764,23 +952,25 @@ async function runStatus(cliOptions) {
764
952
  ["launchctl", "print", `gui/${process.getuid()}/${defaultLabel}`],
765
953
  { ignoreError: true }
766
954
  );
767
- console.log(
955
+ readyLines.push(
768
956
  t(locale, "cli.status.launchd", {
769
957
  value: t(locale, printed.ok ? "cli.status.installed" : "cli.status.notRunning"),
770
958
  })
771
959
  );
772
960
  } else {
773
961
  const pid = await maybeReadPid(pidFile);
774
- console.log(t(locale, "cli.status.pid", { value: pid || "(not running)" }));
962
+ readyLines.push(t(locale, "cli.status.pid", { value: pid || "(not running)" }));
775
963
  }
776
964
 
965
+ let healthOutput = "";
777
966
  if (healthUrl) {
778
967
  const health = await execCommand(buildHealthCheckArgs(healthUrl), { ignoreError: true });
779
- console.log(t(locale, "cli.status.health", { value: t(locale, health.ok ? "cli.status.ok" : "cli.status.failed") }));
780
- if (health.stdout) {
781
- console.log(health.stdout.trim());
782
- }
968
+ readyLines.push(t(locale, "cli.status.health", { value: t(locale, health.ok ? "cli.status.ok" : "cli.status.failed") }));
969
+ healthOutput = health.stdout.trim();
783
970
  }
971
+ printCliTitle(locale, "cli.status.title");
972
+ printCliSection(locale, "cli.section.ready", readyLines);
973
+ printCliSection(locale, "cli.section.details", summarizeHealthOutput(healthOutput));
784
974
  }
785
975
 
786
976
  async function runDoctor(cliOptions) {
@@ -850,14 +1040,38 @@ async function runDoctor(cliOptions) {
850
1040
  }
851
1041
 
852
1042
  if (issues.length === 0) {
853
- console.log(t(locale, "cli.doctor.ok"));
1043
+ printCliTitle(locale, "cli.doctor.ok");
1044
+ printCliSection(locale, "cli.section.ready", [
1045
+ t(locale, "cli.status.baseUrl", { value: baseUrl || "(not configured)" }),
1046
+ healthUrl ? t(locale, "cli.status.health", { value: t(locale, "cli.status.ok") }) : "",
1047
+ ]);
854
1048
  return;
855
1049
  }
856
1050
 
857
- console.log(t(locale, "cli.doctor.foundIssues"));
858
- for (const issue of issues) {
859
- console.log(`- ${issue}`);
1051
+ printCliTitle(locale, "cli.doctor.titleIssues");
1052
+ printCliSection(locale, "cli.section.needsAttention", issues);
1053
+
1054
+ if (!cliOptions.doctorFix) {
1055
+ printCliSection(locale, "cli.section.next", [
1056
+ t(locale, "cli.doctor.nextFix"),
1057
+ ]);
1058
+ return;
860
1059
  }
1060
+
1061
+ if (!(await fileExists(envFile))) {
1062
+ throw new Error("No viveworker config was found. Run `npx viveworker setup` first.");
1063
+ }
1064
+
1065
+ console.log("");
1066
+ console.log(t(locale, "cli.doctor.fixing"));
1067
+ const appliedChanges = await repairDoctorIssues(cliOptions, { envFile, config, locale, hostname, localHostname, chosenIp, webPushEnabled, allowInsecureHttpLan });
1068
+ printCliTitle(locale, "cli.doctor.titleFix");
1069
+ printCliSection(locale, "cli.section.changed", appliedChanges);
1070
+ printCliSection(locale, "cli.section.next", [
1071
+ t(locale, "cli.doctor.nextRestarting"),
1072
+ ]);
1073
+ console.log(t(locale, "cli.doctor.fixed"));
1074
+ await runStart(cliOptions);
861
1075
  }
862
1076
 
863
1077
  async function runUpdate(cliOptions) {
@@ -893,40 +1107,248 @@ async function runUpdate(cliOptions) {
893
1107
 
894
1108
  // 3. Restart bridge
895
1109
  const launchAgentPath = resolvePath(cliOptions.launchAgentPath || defaultLaunchAgentPath);
1110
+ let restartedBridge = false;
896
1111
  if (await fileExists(launchAgentPath)) {
897
1112
  progress.update("cli.update.progress.restartBridge");
898
1113
  await execCommand(["launchctl", "bootout", `gui/${process.getuid()}`, launchAgentPath], { ignoreError: true });
899
1114
  await execCommand(["launchctl", "bootstrap", `gui/${process.getuid()}`, launchAgentPath], { ignoreError: true });
900
1115
  await execCommand(["launchctl", "kickstart", "-k", `gui/${process.getuid()}/${defaultLabel}`], { ignoreError: true });
1116
+ restartedBridge = true;
901
1117
  }
902
1118
 
903
1119
  // 4. Restart moltbook-watcher if present
904
1120
  const watcherLabel = "com.viveworker.moltbook-watcher";
905
1121
  const watcherPlistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${watcherLabel}.plist`);
1122
+ let restartedWatcher = false;
906
1123
  if (await fileExists(watcherPlistPath)) {
907
1124
  progress.update("cli.update.progress.restartWatcher");
908
1125
  await execCommand(["launchctl", "bootout", `gui/${process.getuid()}`, watcherPlistPath], { ignoreError: true });
909
1126
  await execCommand(["launchctl", "bootstrap", `gui/${process.getuid()}`, watcherPlistPath], { ignoreError: true });
1127
+ restartedWatcher = true;
910
1128
  }
911
1129
 
912
1130
  // 5. Restart moltbook-scout if present
913
1131
  const scoutLabel = "com.viveworker.moltbook-scout";
914
1132
  const scoutPlistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${scoutLabel}.plist`);
1133
+ let restartedScout = false;
915
1134
  if (await fileExists(scoutPlistPath)) {
916
1135
  progress.update("cli.update.progress.restartScout");
917
1136
  await execCommand(["launchctl", "bootout", `gui/${process.getuid()}`, scoutPlistPath], { ignoreError: true });
918
1137
  await execCommand(["launchctl", "bootstrap", `gui/${process.getuid()}`, scoutPlistPath], { ignoreError: true });
1138
+ restartedScout = true;
919
1139
  }
920
1140
 
921
1141
  progress.done("cli.update.done", { version: latestVersion });
1142
+ printCliTitle(locale, "cli.update.title");
1143
+ printCliSection(locale, "cli.section.changed", [
1144
+ t(locale, "cli.update.changedVersion", { current: currentVersion, latest: latestVersion }),
1145
+ restartedBridge ? t(locale, "cli.update.changedBridge") : "",
1146
+ restartedWatcher ? t(locale, "cli.update.changedWatcher") : "",
1147
+ restartedScout ? t(locale, "cli.update.changedScout") : "",
1148
+ ]);
1149
+ printCliSection(locale, "cli.section.next", [
1150
+ t(locale, "cli.update.next"),
1151
+ ]);
1152
+ }
1153
+
1154
+ async function loadExistingSetup(cliOptions) {
1155
+ const configDir = resolvePath(cliOptions.configDir || defaultConfigDir);
1156
+ const envFile = resolvePath(cliOptions.envFile || path.join(configDir, "config.env"));
1157
+ const logFile = resolvePath(cliOptions.logFile || path.join(configDir, "logs", "viveworker.log"));
1158
+ const pidFile = resolvePath(cliOptions.pidFile || path.join(configDir, "viveworker.pid"));
1159
+ const launchAgentPath = resolvePath(cliOptions.launchAgentPath || defaultLaunchAgentPath);
1160
+ const locale = await resolveCliLocale(cliOptions);
1161
+ if (!(await fileExists(envFile))) {
1162
+ throw new Error("No viveworker config was found. Run `npx viveworker setup` first.");
1163
+ }
1164
+ const config = await ensureDefaultLocalePersisted(envFile, cliOptions);
1165
+ return { configDir, envFile, logFile, pidFile, launchAgentPath, locale, config };
1166
+ }
1167
+
1168
+ async function refreshPairingCredentials(envFile, config = {}, { force = false } = {}) {
1169
+ const now = Date.now();
1170
+ const rotated = shouldRotatePairing({
1171
+ force,
1172
+ pairingCode: config.PAIRING_CODE,
1173
+ pairingToken: config.PAIRING_TOKEN,
1174
+ pairingExpiresAtMs: config.PAIRING_EXPIRES_AT_MS,
1175
+ }, now);
1176
+
1177
+ if (!rotated) {
1178
+ return { rotated: false };
1179
+ }
1180
+
1181
+ const nextPairing = generatePairingCredentials(now);
1182
+ const currentText = (await fileExists(envFile)) ? await fs.readFile(envFile, "utf8") : "";
1183
+ const nextText = upsertEnvText(currentText, {
1184
+ PAIRING_CODE: nextPairing.pairingCode,
1185
+ PAIRING_TOKEN: nextPairing.pairingToken,
1186
+ PAIRING_EXPIRES_AT_MS: String(nextPairing.pairingExpiresAtMs),
1187
+ });
1188
+ await fs.mkdir(path.dirname(envFile), { recursive: true });
1189
+ await fs.writeFile(envFile, nextText, "utf8");
1190
+
1191
+ return {
1192
+ rotated: true,
1193
+ ...nextPairing,
1194
+ };
1195
+ }
1196
+
1197
+ async function maybeRunLegacySetupFeatureAliases(cliOptions, { envFile, locale, sessionSecret, port }) {
1198
+ const legacyActions = [];
1199
+ if (cliOptions.moltbook) {
1200
+ legacyActions.push("moltbook");
1201
+ }
1202
+ if (cliOptions.autoScout || cliOptions.autoScoutUninstall) {
1203
+ legacyActions.push("scout");
1204
+ }
1205
+
1206
+ if (legacyActions.length === 0) {
1207
+ return;
1208
+ }
1209
+
1210
+ console.log("");
1211
+ console.log(t(locale, "cli.setup.legacyFeatureFlags", {
1212
+ commands: legacyActions.map((action) => `viveworker enable ${action}`).join(", "),
1213
+ }));
1214
+
1215
+ if (cliOptions.moltbook) {
1216
+ try {
1217
+ await installMoltbookWatcher({ cliOptions, sessionSecret, port });
1218
+ } catch (error) {
1219
+ console.log("");
1220
+ console.log(`Moltbook watcher install failed: ${error.message}`);
1221
+ }
1222
+ }
1223
+
1224
+ if (cliOptions.autoScoutUninstall) {
1225
+ await uninstallMoltbookScout();
1226
+ } else if (cliOptions.autoScout) {
1227
+ try {
1228
+ await installMoltbookScout({ cliOptions });
1229
+ } catch (error) {
1230
+ console.log("");
1231
+ console.log(`Moltbook auto-scout install failed: ${error.message}`);
1232
+ }
1233
+ }
1234
+ }
1235
+
1236
+ async function repairDoctorIssues(cliOptions, { envFile, config, locale, hostname, localHostname, chosenIp, webPushEnabled, allowInsecureHttpLan }) {
1237
+ const updates = {};
1238
+ const changed = [];
1239
+
1240
+ if (!config.SESSION_SECRET) {
1241
+ updates.SESSION_SECRET = crypto.randomBytes(32).toString("hex");
1242
+ changed.push(t(locale, "cli.doctor.fixed.sessionSecret"));
1243
+ }
1244
+ if (!config.PAIRING_CODE || !config.PAIRING_TOKEN) {
1245
+ const nextPairing = generatePairingCredentials();
1246
+ updates.PAIRING_CODE = nextPairing.pairingCode;
1247
+ updates.PAIRING_TOKEN = nextPairing.pairingToken;
1248
+ updates.PAIRING_EXPIRES_AT_MS = String(nextPairing.pairingExpiresAtMs);
1249
+ changed.push(t(locale, "cli.doctor.fixed.pairing"));
1250
+ }
1251
+ if (!config.DEFAULT_LOCALE) {
1252
+ updates.DEFAULT_LOCALE = locale;
1253
+ changed.push(t(locale, "cli.doctor.fixed.locale"));
1254
+ }
1255
+ if (!config.NATIVE_APPROVAL_SERVER_HOST) {
1256
+ updates.NATIVE_APPROVAL_SERVER_HOST = webPushEnabled || allowInsecureHttpLan ? "0.0.0.0" : "127.0.0.1";
1257
+ changed.push(t(locale, "cli.doctor.fixed.host"));
1258
+ }
1259
+ if (!config.NATIVE_APPROVAL_SERVER_PORT) {
1260
+ updates.NATIVE_APPROVAL_SERVER_PORT = String(defaultServerPort);
1261
+ changed.push(t(locale, "cli.doctor.fixed.port"));
1262
+ }
1263
+ if (!config.VIVEWORKER_HOSTNAME) {
1264
+ updates.VIVEWORKER_HOSTNAME = hostname;
1265
+ changed.push(t(locale, "cli.doctor.fixed.hostname"));
1266
+ }
1267
+ if (!config.DEVICE_TRUST_TTL_MS) {
1268
+ updates.DEVICE_TRUST_TTL_MS = String(30 * 24 * 60 * 60 * 1000);
1269
+ changed.push(t(locale, "cli.doctor.fixed.deviceTrust"));
1270
+ }
1271
+ if (!config.WEB_UI_ENABLED) {
1272
+ updates.WEB_UI_ENABLED = "1";
1273
+ changed.push(t(locale, "cli.doctor.fixed.webUi"));
1274
+ }
1275
+ if (!config.AUTH_REQUIRED) {
1276
+ updates.AUTH_REQUIRED = "1";
1277
+ changed.push(t(locale, "cli.doctor.fixed.auth"));
1278
+ }
1279
+ if (!config.CHOICE_PAGE_SIZE) {
1280
+ updates.CHOICE_PAGE_SIZE = "5";
1281
+ changed.push(t(locale, "cli.doctor.fixed.choicePageSize"));
1282
+ }
1283
+ if (!config.MAX_HISTORY_ITEMS) {
1284
+ updates.MAX_HISTORY_ITEMS = "100";
1285
+ changed.push(t(locale, "cli.doctor.fixed.maxHistory"));
1286
+ }
1287
+ if (!config.NATIVE_APPROVALS) {
1288
+ updates.NATIVE_APPROVALS = "1";
1289
+ changed.push(t(locale, "cli.doctor.fixed.nativeApprovals"));
1290
+ }
1291
+
1292
+ const nextPort = Number(updates.NATIVE_APPROVAL_SERVER_PORT || config.NATIVE_APPROVAL_SERVER_PORT) || defaultServerPort;
1293
+ const nextHostname = updates.VIVEWORKER_HOSTNAME || config.VIVEWORKER_HOSTNAME || hostname;
1294
+ if (!config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL) {
1295
+ const scheme = webPushEnabled ? "https" : "http";
1296
+ const publicHost = webPushEnabled || allowInsecureHttpLan
1297
+ ? (nextHostname.endsWith(".local") ? nextHostname : `${nextHostname}.local`)
1298
+ : "127.0.0.1";
1299
+ updates.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL = `${scheme}://${publicHost}:${nextPort}`;
1300
+ changed.push(t(locale, "cli.doctor.fixed.baseUrl"));
1301
+ }
1302
+
1303
+ const nextConfig = { ...config, ...updates };
1304
+ if (webPushEnabled) {
1305
+ const tlsCertFile = resolvePath(
1306
+ nextConfig.TLS_CERT_FILE || path.join(path.dirname(envFile), "tls", "cert.pem")
1307
+ );
1308
+ const tlsKeyFile = resolvePath(
1309
+ nextConfig.TLS_KEY_FILE || path.join(path.dirname(envFile), "tls", "key.pem")
1310
+ );
1311
+ const progress = createCliProgressReporter(locale);
1312
+ const tlsAssets = await ensureWebPushAssets({
1313
+ cliOptions,
1314
+ existing: nextConfig,
1315
+ hostname: nextHostname,
1316
+ localHostname: nextHostname.endsWith(".local") ? nextHostname : `${nextHostname}.local`,
1317
+ locale,
1318
+ progress,
1319
+ chosenIp,
1320
+ tlsCertFile,
1321
+ tlsKeyFile,
1322
+ });
1323
+ progress.clear();
1324
+ updates.TLS_CERT_FILE = tlsAssets.certFile;
1325
+ updates.TLS_KEY_FILE = tlsAssets.keyFile;
1326
+ updates.WEB_PUSH_VAPID_PUBLIC_KEY = tlsAssets.vapidPublicKey;
1327
+ updates.WEB_PUSH_VAPID_PRIVATE_KEY = tlsAssets.vapidPrivateKey;
1328
+ changed.push(t(locale, "cli.doctor.fixed.webPushAssets"));
1329
+ if (!nextConfig.WEB_PUSH_SUBJECT) {
1330
+ updates.WEB_PUSH_SUBJECT = "mailto:viveworker@example.com";
1331
+ changed.push(t(locale, "cli.doctor.fixed.webPushSubject"));
1332
+ }
1333
+ }
1334
+
1335
+ const currentText = (await fileExists(envFile)) ? await fs.readFile(envFile, "utf8") : "";
1336
+ const nextText = upsertEnvText(currentText, updates);
1337
+ await fs.writeFile(envFile, nextText, "utf8");
1338
+ return changed;
922
1339
  }
923
1340
 
924
1341
  function parseArgs(argv) {
925
1342
  const parsed = {
926
1343
  command: "help",
1344
+ enableTarget: "",
1345
+ enableArgs: [],
927
1346
  enableNtfy: false,
928
1347
  enableWebPush: false,
929
1348
  disableWebPush: false,
1349
+ noAutoMkcert: false,
1350
+ noAutoClaude: false,
1351
+ noScout: false,
930
1352
  allowInsecureHttpLan: false,
931
1353
  installMkcert: false,
932
1354
  noLaunchd: false,
@@ -950,6 +1372,7 @@ function parseArgs(argv) {
950
1372
  locale: "",
951
1373
  mkcertTrustStores: "",
952
1374
  claudeSettingsFile: "",
1375
+ doctorFix: false,
953
1376
  moltbook: false,
954
1377
  moltbookApiKey: "",
955
1378
  moltbookAgentId: "",
@@ -967,6 +1390,15 @@ function parseArgs(argv) {
967
1390
  argv = argv.slice(1);
968
1391
  }
969
1392
 
1393
+ if (parsed.command === "enable" && argv[0] && !argv[0].startsWith("-")) {
1394
+ parsed.enableTarget = argv[0];
1395
+ argv = argv.slice(1);
1396
+ if (parsed.enableTarget === "a2a") {
1397
+ parsed.enableArgs = argv.slice();
1398
+ return parsed;
1399
+ }
1400
+ }
1401
+
970
1402
  for (let index = 0; index < argv.length; index += 1) {
971
1403
  const arg = argv[index];
972
1404
  const next = argv[index + 1] ?? "";
@@ -976,6 +1408,12 @@ function parseArgs(argv) {
976
1408
  parsed.enableWebPush = true;
977
1409
  } else if (arg === "--disable-web-push") {
978
1410
  parsed.disableWebPush = true;
1411
+ } else if (arg === "--no-auto-mkcert") {
1412
+ parsed.noAutoMkcert = true;
1413
+ } else if (arg === "--no-auto-claude") {
1414
+ parsed.noAutoClaude = true;
1415
+ } else if (arg === "--no-scout") {
1416
+ parsed.noScout = true;
979
1417
  } else if (arg === "--allow-insecure-http-lan") {
980
1418
  parsed.allowInsecureHttpLan = true;
981
1419
  } else if (arg === "--install-mkcert") {
@@ -1030,6 +1468,8 @@ function parseArgs(argv) {
1030
1468
  } else if (arg === "--locale") {
1031
1469
  parsed.locale = next;
1032
1470
  index += 1;
1471
+ } else if (arg === "--fix") {
1472
+ parsed.doctorFix = true;
1033
1473
  } else if (arg === "--vapid-public-key") {
1034
1474
  parsed.vapidPublicKey = next;
1035
1475
  index += 1;
@@ -1038,9 +1478,23 @@ function parseArgs(argv) {
1038
1478
  index += 1;
1039
1479
  } else if (arg === "--pair") {
1040
1480
  parsed.pair = true;
1481
+ } else if (arg === "--settings-file") {
1482
+ parsed.claudeSettingsFile = next;
1483
+ index += 1;
1041
1484
  } else if (arg === "--claude-settings-file") {
1042
1485
  parsed.claudeSettingsFile = next;
1043
1486
  index += 1;
1487
+ } else if (arg === "--api-key") {
1488
+ parsed.moltbook = true;
1489
+ parsed.moltbookApiKey = next;
1490
+ index += 1;
1491
+ } else if (arg === "--agent-id") {
1492
+ parsed.moltbook = true;
1493
+ parsed.moltbookAgentId = next;
1494
+ index += 1;
1495
+ } else if (arg === "--agent-name") {
1496
+ parsed.moltbookAgentName = next;
1497
+ index += 1;
1044
1498
  } else if (arg === "--moltbook") {
1045
1499
  parsed.moltbook = true;
1046
1500
  } else if (arg === "--moltbook-api-key") {
@@ -1056,6 +1510,20 @@ function parseArgs(argv) {
1056
1510
  index += 1;
1057
1511
  } else if (arg === "--auto-scout") {
1058
1512
  parsed.autoScout = true;
1513
+ } else if (arg === "--uninstall") {
1514
+ parsed.autoScoutUninstall = true;
1515
+ } else if (arg === "--interval") {
1516
+ parsed.autoScoutInterval = Number(next) || 120;
1517
+ index += 1;
1518
+ } else if (arg === "--harness") {
1519
+ parsed.autoScoutHarness = next;
1520
+ index += 1;
1521
+ } else if (arg === "--submolts") {
1522
+ parsed.autoScoutSubmolts = next;
1523
+ index += 1;
1524
+ } else if (arg === "--max-daily") {
1525
+ parsed.autoScoutMaxDaily = Number(next) || 0;
1526
+ index += 1;
1059
1527
  } else if (arg === "--auto-scout-uninstall") {
1060
1528
  parsed.autoScoutUninstall = true;
1061
1529
  } else if (arg === "--auto-scout-interval") {
@@ -1090,6 +1558,8 @@ function printHelp() {
1090
1558
 
1091
1559
  ${t(locale, "cli.help.commands")}
1092
1560
  ${t(locale, "cli.help.setup")}
1561
+ ${t(locale, "cli.help.pair")}
1562
+ ${t(locale, "cli.help.enable")}
1093
1563
  ${t(locale, "cli.help.start")}
1094
1564
  ${t(locale, "cli.help.stop")}
1095
1565
  ${t(locale, "cli.help.status")}
@@ -1103,6 +1573,8 @@ ${t(locale, "cli.help.commonOptions")}
1103
1573
  --config-dir <path>
1104
1574
  --disable-web-push
1105
1575
  --enable-web-push
1576
+ --no-auto-mkcert
1577
+ --no-auto-claude
1106
1578
  --allow-insecure-http-lan
1107
1579
  --install-mkcert
1108
1580
  --mkcert-trust-stores <system[,java][,nss]>
@@ -1115,6 +1587,13 @@ ${t(locale, "cli.help.commonOptions")}
1115
1587
  --enable-ntfy
1116
1588
  --no-launchd
1117
1589
  --pair
1590
+
1591
+ ${t(locale, "cli.help.featureOptions")}
1592
+ ${t(locale, "cli.help.enableClaude")}
1593
+ ${t(locale, "cli.help.enableA2a")}
1594
+ ${t(locale, "cli.help.enableMoltbook")}
1595
+ ${t(locale, "cli.help.enableScout")}
1596
+ ${t(locale, "cli.help.doctorFix")}
1118
1597
  `);
1119
1598
  }
1120
1599
 
@@ -1287,6 +1766,119 @@ async function maybeRotateStartupPairing(envFile, config = {}) {
1287
1766
  };
1288
1767
  }
1289
1768
 
1769
+ async function autoConfigureProvidersDuringSetup({ cliOptions, envFile, sessionSecret, codexHome }) {
1770
+ const codex = await detectCodexAvailability(codexHome);
1771
+ const explicitClaudeSettingsFile = cliOptions.claudeSettingsFile
1772
+ ? resolvePath(cliOptions.claudeSettingsFile)
1773
+ : "";
1774
+ const defaultClaudeSettingsFile = resolvePath(path.join(os.homedir(), ".claude", "settings.json"));
1775
+ const claudeSettingsFile = explicitClaudeSettingsFile || defaultClaudeSettingsFile;
1776
+
1777
+ if (!explicitClaudeSettingsFile && cliOptions.noAutoClaude) {
1778
+ return {
1779
+ codex,
1780
+ claude: {
1781
+ status: "skipped",
1782
+ settingsFile: "",
1783
+ message: "",
1784
+ },
1785
+ };
1786
+ }
1787
+
1788
+ const claudeDetected = explicitClaudeSettingsFile
1789
+ ? true
1790
+ : await detectClaudeAvailability(defaultClaudeSettingsFile);
1791
+
1792
+ if (!claudeDetected) {
1793
+ return {
1794
+ codex,
1795
+ claude: {
1796
+ status: "not_detected",
1797
+ settingsFile: "",
1798
+ message: "",
1799
+ },
1800
+ };
1801
+ }
1802
+
1803
+ try {
1804
+ const installedPath = await installClaudeHooks({
1805
+ envFile,
1806
+ claudeSettingsFile,
1807
+ sessionSecret,
1808
+ suppressOutput: true,
1809
+ });
1810
+ return {
1811
+ codex,
1812
+ claude: {
1813
+ status: "enabled",
1814
+ settingsFile: installedPath,
1815
+ message: "",
1816
+ },
1817
+ };
1818
+ } catch (error) {
1819
+ return {
1820
+ codex,
1821
+ claude: {
1822
+ status: "failed",
1823
+ settingsFile: claudeSettingsFile,
1824
+ message: error?.message || String(error),
1825
+ },
1826
+ };
1827
+ }
1828
+ }
1829
+
1830
+ async function detectCodexAvailability(codexHome) {
1831
+ const candidates = Array.from(new Set([
1832
+ resolvePath(codexHome || path.join(os.homedir(), ".codex")),
1833
+ "/Applications/Codex.app",
1834
+ path.join(os.homedir(), "Applications", "Codex.app"),
1835
+ ]));
1836
+ for (const candidate of candidates) {
1837
+ if (await fileExists(candidate)) {
1838
+ return {
1839
+ detected: true,
1840
+ path: candidate,
1841
+ };
1842
+ }
1843
+ }
1844
+ return {
1845
+ detected: false,
1846
+ path: "",
1847
+ };
1848
+ }
1849
+
1850
+ async function detectClaudeAvailability(settingsFile) {
1851
+ return (await fileExists(settingsFile)) || (await fileExists(path.dirname(settingsFile)));
1852
+ }
1853
+
1854
+ function getSetupProviderSummaryLines(locale, providerSetup) {
1855
+ const lines = [
1856
+ t(locale, providerSetup.codex.detected
1857
+ ? "cli.setup.providers.codexReady"
1858
+ : "cli.setup.providers.codexNotDetected"),
1859
+ ];
1860
+ switch (providerSetup.claude.status) {
1861
+ case "enabled":
1862
+ lines.push(t(locale, "cli.setup.providers.claudeEnabled", {
1863
+ path: providerSetup.claude.settingsFile,
1864
+ }));
1865
+ break;
1866
+ case "skipped":
1867
+ lines.push(t(locale, "cli.setup.providers.claudeSkipped"));
1868
+ break;
1869
+ case "failed":
1870
+ lines.push(t(locale, "cli.setup.providers.claudeFailed", {
1871
+ message: providerSetup.claude.message,
1872
+ }));
1873
+ break;
1874
+ case "not_detected":
1875
+ default:
1876
+ lines.push(t(locale, "cli.setup.providers.claudeNotDetected"));
1877
+ break;
1878
+ }
1879
+ return lines;
1880
+ }
1881
+
1290
1882
  async function findLocalIpv4Addresses() {
1291
1883
  const interfaces = os.networkInterfaces();
1292
1884
  const result = [];
@@ -1688,13 +2280,15 @@ async function ensureWebPushAssets({
1688
2280
  throw new Error("TLS_CERT_FILE and TLS_KEY_FILE must both exist.");
1689
2281
  }
1690
2282
 
2283
+ let ranMkcertInstall = false;
2284
+
1691
2285
  if (!certExists) {
1692
2286
  if (manualCertOverride) {
1693
2287
  throw new Error("The provided TLS certificate or key file does not exist.");
1694
2288
  }
1695
2289
 
1696
2290
  let mkcertPath = await findExecutable("mkcert");
1697
- if (!mkcertPath && cliOptions.installMkcert) {
2291
+ if (!mkcertPath && !cliOptions.noAutoMkcert) {
1698
2292
  mkcertPath = await installMkcertForMac(progress, locale);
1699
2293
  }
1700
2294
  if (!mkcertPath) {
@@ -1716,6 +2310,7 @@ async function ensureWebPushAssets({
1716
2310
  streamOutput: true,
1717
2311
  beforeStreamOutput: () => progress?.clear(),
1718
2312
  });
2313
+ ranMkcertInstall = true;
1719
2314
  progress?.update("cli.setup.progress.generateCert");
1720
2315
  await fs.mkdir(path.dirname(tlsCertFile), { recursive: true });
1721
2316
  await execCommand([
@@ -1740,6 +2335,7 @@ async function ensureWebPushAssets({
1740
2335
  streamOutput: true,
1741
2336
  beforeStreamOutput: () => progress?.clear(),
1742
2337
  });
2338
+ ranMkcertInstall = true;
1743
2339
  }
1744
2340
  }
1745
2341
 
@@ -1757,6 +2353,7 @@ async function ensureWebPushAssets({
1757
2353
  keyFile: tlsKeyFile,
1758
2354
  vapidPublicKey,
1759
2355
  vapidPrivateKey,
2356
+ ranMkcertInstall,
1760
2357
  };
1761
2358
  }
1762
2359
 
@@ -1767,6 +2364,7 @@ async function ensureWebPushAssets({
1767
2364
  keyFile: tlsKeyFile,
1768
2365
  vapidPublicKey: generated.publicKey,
1769
2366
  vapidPrivateKey: generated.privateKey,
2367
+ ranMkcertInstall,
1770
2368
  };
1771
2369
  }
1772
2370
 
@@ -1874,7 +2472,7 @@ function escapeXml(value) {
1874
2472
  .replace(/"/gu, "&quot;");
1875
2473
  }
1876
2474
 
1877
- async function printPairingInfo(locale, config) {
2475
+ async function printPairingInfo(locale, config, { sectioned = false } = {}) {
1878
2476
  const baseUrl = String(config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "").trim();
1879
2477
  const pairCode = String(config.PAIRING_CODE || "").trim();
1880
2478
  const pairToken = String(config.PAIRING_TOKEN || "").trim();
@@ -1886,6 +2484,14 @@ async function printPairingInfo(locale, config) {
1886
2484
  const ips = await findLocalIpv4Addresses();
1887
2485
  const fallbackBaseUrl = buildFallbackBaseUrl(baseUrl, ips[0] || "127.0.0.1");
1888
2486
 
2487
+ if (sectioned) {
2488
+ printCliSection(locale, "cli.section.pairingLinks", [
2489
+ t(locale, "cli.setup.pairingUrlLocal", { url: `${baseUrl}${pairPath}` }),
2490
+ t(locale, "cli.setup.pairingUrlIp", { url: `${fallbackBaseUrl}${pairPath}` }),
2491
+ ]);
2492
+ return;
2493
+ }
2494
+
1889
2495
  console.log("");
1890
2496
  console.log(t(locale, "cli.setup.pairingCode", { code: pairCode }));
1891
2497
  console.log(t(locale, "cli.setup.pairingUrlLocal", { url: `${baseUrl}${pairPath}` }));
@@ -1901,3 +2507,38 @@ function buildFallbackBaseUrl(baseUrl, ipAddress) {
1901
2507
  return baseUrl;
1902
2508
  }
1903
2509
  }
2510
+
2511
+ function summarizeHealthOutput(rawValue) {
2512
+ const text = String(rawValue || "").trim();
2513
+ if (!text) {
2514
+ return [];
2515
+ }
2516
+ try {
2517
+ const parsed = JSON.parse(text);
2518
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2519
+ return [text];
2520
+ }
2521
+ return Object.entries(parsed)
2522
+ .filter(([key]) => key !== "ok")
2523
+ .map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
2524
+ } catch {
2525
+ return [text];
2526
+ }
2527
+ }
2528
+
2529
+ function printCliTitle(locale, key, vars = {}) {
2530
+ console.log("");
2531
+ console.log(t(locale, key, vars));
2532
+ }
2533
+
2534
+ function printCliSection(locale, key, lines = [], vars = {}) {
2535
+ const filtered = (lines || []).filter((line) => String(line || "").trim());
2536
+ if (filtered.length === 0) {
2537
+ return;
2538
+ }
2539
+ console.log("");
2540
+ console.log(t(locale, key, vars));
2541
+ for (const line of filtered) {
2542
+ console.log(`- ${line}`);
2543
+ }
2544
+ }