pubblue 0.5.0 → 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.
package/dist/index.js CHANGED
@@ -1,54 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CHANNELS,
4
+ CONTROL_CHANNEL,
3
5
  PubApiClient,
4
- PubApiError,
5
6
  TEXT_FILE_EXTENSIONS,
6
- bridgeInfoPath,
7
- bridgeLogPath,
8
7
  buildBridgeProcessEnv,
9
8
  buildDaemonForkStdio,
10
- cleanupLiveOnStartFailure,
11
9
  createApiClient,
12
- ensureBridgeReady,
13
10
  ensureNodeDatachannelAvailable,
14
11
  failCli,
15
12
  formatApiError,
13
+ generateMessageId,
14
+ getAgentSocketPath,
16
15
  getConfig,
17
16
  getFollowReadDelayMs,
18
17
  getMimeType,
19
- getPublicUrl,
20
18
  getTelegramMiniAppUrl,
21
- isBridgeRunning,
19
+ ipcCall,
22
20
  isDaemonRunning,
23
21
  liveInfoPath,
24
22
  liveLogPath,
25
23
  loadConfig,
26
24
  messageContainsPong,
27
- parseBridgeMode,
28
25
  parsePositiveIntegerOption,
29
- pickReusableLive,
30
- readBridgeProcessInfo,
31
- readDaemonProcessInfo,
32
26
  readLogTail,
33
27
  resolveActiveSlug,
34
- resolveSlugSelection,
28
+ resolveBridgeMode,
35
29
  saveConfig,
36
- shouldRestartDaemonForCliUpgrade,
37
- stopBridge,
38
30
  stopOtherDaemons,
39
31
  toCliFailure,
40
- waitForAgentOffer,
41
32
  waitForDaemonReady,
42
- waitForProcessExit,
43
33
  writeLatestCliVersion
44
- } from "./chunk-YI45G6AG.js";
45
- import {
46
- CHANNELS,
47
- CONTROL_CHANNEL,
48
- generateMessageId,
49
- getSocketPath,
50
- ipcCall
51
- } from "./chunk-PFZT7M3E.js";
34
+ } from "./chunk-BBJOOZHS.js";
52
35
 
53
36
  // src/program.ts
54
37
  import { Command } from "commander";
@@ -143,11 +126,6 @@ function parseBooleanValue(raw, key) {
143
126
  return false;
144
127
  throw new Error(`Invalid boolean value for ${key}: ${raw}`);
145
128
  }
146
- function parseBridgeModeValue(raw) {
147
- const normalized = raw.trim().toLowerCase();
148
- if (normalized === "openclaw" || normalized === "none") return normalized;
149
- throw new Error(`Invalid bridge mode: ${raw}. Use openclaw or none.`);
150
- }
151
129
  function parsePositiveInteger(raw, key) {
152
130
  const parsed = Number.parseInt(raw, 10);
153
131
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -156,7 +134,6 @@ function parsePositiveInteger(raw, key) {
156
134
  return parsed;
157
135
  }
158
136
  var SUPPORTED_KEYS = [
159
- "bridge.mode",
160
137
  "openclaw.path",
161
138
  "openclaw.sessionId",
162
139
  "openclaw.threadId",
@@ -171,9 +148,6 @@ var SUPPORTED_KEYS = [
171
148
  ];
172
149
  function applyConfigSet(bridge, telegram, key, value) {
173
150
  switch (key) {
174
- case "bridge.mode":
175
- bridge.mode = parseBridgeModeValue(value);
176
- return;
177
151
  case "openclaw.path":
178
152
  bridge.openclawPath = value;
179
153
  return;
@@ -219,9 +193,6 @@ function applyConfigSet(bridge, telegram, key, value) {
219
193
  }
220
194
  function applyConfigUnset(bridge, telegram, key) {
221
195
  switch (key) {
222
- case "bridge.mode":
223
- delete bridge.mode;
224
- return;
225
196
  case "openclaw.path":
226
197
  delete bridge.openclawPath;
227
198
  return;
@@ -279,13 +250,11 @@ async function telegramGetMe(token) {
279
250
  hasMainWebApp: data.result.has_main_web_app === true
280
251
  };
281
252
  }
282
- async function telegramSetMenuButton(token, url) {
253
+ async function telegramSetMenuButton(token, button) {
283
254
  const resp = await fetch(`https://api.telegram.org/bot${token}/setChatMenuButton`, {
284
255
  method: "POST",
285
256
  headers: { "Content-Type": "application/json" },
286
- body: JSON.stringify({
287
- menu_button: { type: "web_app", text: "Open", web_app: { url } }
288
- })
257
+ body: JSON.stringify({ menu_button: button })
289
258
  });
290
259
  const data = await resp.json();
291
260
  if (!data.ok) {
@@ -300,7 +269,6 @@ function printConfigSummary(saved) {
300
269
  console.log("Saved config:");
301
270
  console.log(` apiKey: ${maskSecret(saved.apiKey)}`);
302
271
  if (saved.bridge && hasValues(saved.bridge)) {
303
- console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
304
272
  if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
305
273
  if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
306
274
  if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
@@ -375,6 +343,16 @@ function registerConfigureCommand(program2) {
375
343
  if (key === "telegram.botToken") telegramTokenChanged = true;
376
344
  }
377
345
  for (const key of opts.unset) {
346
+ if (key.trim() === "telegram.botToken" && nextTelegram.botToken) {
347
+ try {
348
+ await telegramSetMenuButton(nextTelegram.botToken, { type: "default" });
349
+ console.log("Telegram menu button reset to default.");
350
+ } catch (error) {
351
+ console.error(
352
+ `Warning: failed to reset Telegram menu button: ${error instanceof Error ? error.message : String(error)}`
353
+ );
354
+ }
355
+ }
378
356
  applyConfigUnset(nextBridge, nextTelegram, key.trim());
379
357
  }
380
358
  if (telegramTokenChanged && nextTelegram.botToken) {
@@ -383,7 +361,11 @@ function registerConfigureCommand(program2) {
383
361
  nextTelegram.botUsername = bot.username;
384
362
  nextTelegram.hasMainWebApp = bot.hasMainWebApp;
385
363
  console.log(` Bot: @${bot.username}`);
386
- await telegramSetMenuButton(nextTelegram.botToken, "https://pub.blue");
364
+ await telegramSetMenuButton(nextTelegram.botToken, {
365
+ type: "web_app",
366
+ text: "Open",
367
+ web_app: { url: "https://pub.blue" }
368
+ });
387
369
  console.log(" Menu button set to https://pub.blue");
388
370
  if (!bot.hasMainWebApp) {
389
371
  console.log("");
@@ -413,15 +395,15 @@ import * as path2 from "path";
413
395
  // package.json
414
396
  var package_default = {
415
397
  name: "pubblue",
416
- version: "0.5.0",
398
+ version: "0.6.1",
417
399
  description: "CLI tool for publishing content and running interactive sessions via pub.blue",
418
400
  type: "module",
419
401
  bin: {
420
402
  pubblue: "./dist/index.js"
421
403
  },
422
404
  scripts: {
423
- build: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --dts --clean",
424
- dev: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --watch",
405
+ build: "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --dts --clean",
406
+ dev: "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --watch",
425
407
  test: "vitest run",
426
408
  "test:watch": "vitest",
427
409
  lint: "tsc --noEmit"
@@ -464,529 +446,258 @@ var CLI_VERSION = version;
464
446
 
465
447
  // src/commands/live.ts
466
448
  function registerLiveCommands(program2) {
467
- registerOpenCommand(program2);
468
- registerCloseCommand(program2);
449
+ registerStartCommand(program2);
450
+ registerStopCommand(program2);
469
451
  registerStatusCommand(program2);
470
452
  registerWriteCommand(program2);
471
453
  registerReadCommand(program2);
472
454
  registerChannelsCommand(program2);
473
455
  registerDoctorCommand(program2);
474
456
  }
475
- function registerOpenCommand(program2) {
476
- program2.command("open").description("Go live on a pub (starts WebRTC daemon)").argument("[slug]", "Pub slug (reuses existing live when possible)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("--new", "Always create a new live (skip reuse)").option("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork, no managed bridge)").action(
477
- async (slugArg, opts) => {
478
- await ensureNodeDatachannelAvailable();
479
- writeLatestCliVersion(CLI_VERSION);
480
- const runtimeConfig = getConfig();
481
- const apiClient = createApiClient(runtimeConfig);
482
- let target = null;
483
- const bridgeMode = parseBridgeMode(opts.bridge || runtimeConfig.bridge?.mode || "openclaw");
484
- const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
485
- if (slugArg && !opts.new) {
486
- try {
487
- const pub = await apiClient.get(slugArg);
488
- if (pub.live?.status === "active" && pub.live.expiresAt > Date.now()) {
489
- target = {
490
- createdNew: false,
491
- expiresAt: pub.live.expiresAt,
492
- mode: "existing",
493
- slug: pub.slug,
494
- url: getPublicUrl(pub.slug)
495
- };
496
- console.error(`Reusing existing active live for ${pub.slug}.`);
497
- }
498
- } catch (error) {
499
- if (!(error instanceof PubApiError && error.status === 404)) {
500
- failCli(`Failed to inspect pub ${slugArg}: ${formatApiError(error)}`);
501
- }
502
- }
503
- } else if (!slugArg && !opts.new) {
504
- try {
505
- const pubs = await apiClient.list();
506
- const reusable = pickReusableLive(pubs);
507
- if (reusable) {
508
- if (!reusable.live) {
509
- failCli("Internal error: reusable live is missing from selected pub.");
510
- }
511
- target = {
512
- createdNew: false,
513
- expiresAt: reusable.live.expiresAt,
514
- mode: "existing",
515
- slug: reusable.slug,
516
- url: getPublicUrl(reusable.slug)
517
- };
518
- const activeLives = pubs.filter(
519
- (p) => p.live?.status === "active" && p.live.expiresAt > Date.now()
520
- );
521
- if (activeLives.length > 1) {
522
- console.error(
523
- [
524
- `Multiple active lives found: ${activeLives.map((p) => p.slug).join(", ")}`,
525
- `Reusing most recent: ${reusable.slug}.`,
526
- "Use `pubblue open <slug>` to choose explicitly or --new to force creation."
527
- ].join("\n")
528
- );
529
- } else {
530
- console.error(
531
- `Reusing existing live for ${reusable.slug}. Use --new to force creation.`
532
- );
533
- }
534
- }
535
- } catch (error) {
536
- failCli(`Failed to list pubs for live reuse check: ${formatApiError(error)}`);
537
- }
538
- }
539
- if (!target) {
540
- try {
541
- let created;
542
- if (slugArg) {
543
- created = await apiClient.openLive(slugArg, {
544
- expiresIn: opts.expires
545
- });
546
- } else {
547
- const newPub = await apiClient.create({});
548
- try {
549
- created = await apiClient.openLive(newPub.slug, {
550
- expiresIn: opts.expires
551
- });
552
- } catch (error) {
553
- try {
554
- await apiClient.remove(newPub.slug);
555
- } catch (cleanupError) {
556
- console.error(
557
- `Warning: failed to remove pub ${newPub.slug} after open failure: ${formatApiError(cleanupError)}`
558
- );
559
- }
560
- throw error;
561
- }
562
- }
563
- target = {
564
- createdNew: true,
565
- expiresAt: created.expiresAt,
566
- mode: "created",
567
- slug: created.slug,
568
- url: created.url
569
- };
570
- } catch (error) {
571
- failCli(`Failed to go live: ${formatApiError(error)}`);
572
- }
573
- }
574
- if (!target) {
575
- failCli("Failed to resolve live target.");
576
- }
577
- const socketPath = getSocketPath(target.slug);
578
- const infoPath = liveInfoPath(target.slug);
579
- const logPath = liveLogPath(target.slug);
457
+ function registerStartCommand(program2) {
458
+ program2.command("start").description("Start the agent daemon (registers presence, awaits live requests)").requiredOption("--agent-name <name>", "Agent display name shown to the browser user").option("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
459
+ await ensureNodeDatachannelAvailable();
460
+ writeLatestCliVersion(CLI_VERSION);
461
+ const runtimeConfig = getConfig();
462
+ const apiClient = createApiClient(runtimeConfig);
463
+ const bridgeMode = resolveBridgeMode(opts);
464
+ const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
465
+ const socketPath = getAgentSocketPath();
466
+ const infoPath = liveInfoPath("agent");
467
+ const logPath = liveLogPath("agent");
468
+ await stopOtherDaemons();
469
+ if (opts.foreground) {
470
+ const { startDaemon } = await import("./tunnel-daemon-BR5XKNEA.js");
471
+ console.log("Agent daemon starting in foreground...");
472
+ console.log("Press Ctrl+C to stop.");
580
473
  try {
581
- await stopOtherDaemons(target.slug);
582
- } catch (error) {
583
- failCli(error instanceof Error ? error.message : String(error));
584
- }
585
- if (opts.foreground) {
586
- if (bridgeMode !== "none") {
587
- throw new Error(
588
- "Foreground mode disables managed bridge process. Use background mode for --bridge openclaw."
589
- );
590
- }
591
- const { startDaemon } = await import("./tunnel-daemon-QN6TVUX6.js");
592
- console.log(`Live started: ${target.url}`);
593
- const fgTma = getTelegramMiniAppUrl(target.slug);
594
- if (fgTma) console.log(`Telegram: ${fgTma}`);
595
- console.log(`Slug: ${target.slug}`);
596
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
597
- if (target.mode === "existing") console.log("Mode: attached existing live");
598
- console.log("Running in foreground. Press Ctrl+C to stop.");
599
- try {
600
- await startDaemon({
601
- cliVersion: CLI_VERSION,
602
- slug: target.slug,
603
- apiClient,
604
- socketPath,
605
- infoPath
606
- });
607
- } catch (error) {
608
- const message = error instanceof Error ? error.message : String(error);
609
- failCli(`Daemon failed: ${message}`);
610
- }
611
- return;
612
- }
613
- const runningDaemonInfo = readDaemonProcessInfo(target.slug);
614
- if (runningDaemonInfo) {
615
- const daemonVersion = runningDaemonInfo.cliVersion;
616
- const shouldRestartForUpgrade = shouldRestartDaemonForCliUpgrade(
617
- daemonVersion,
618
- CLI_VERSION
619
- );
620
- if (shouldRestartForUpgrade) {
621
- console.error(
622
- `Restarting daemon for CLI version ${CLI_VERSION} (running: ${daemonVersion || "unknown"}).`
623
- );
624
- const bridgeError = await stopBridge(target.slug);
625
- if (bridgeError) failCli(bridgeError);
626
- try {
627
- await ipcCall(socketPath, { method: "close", params: {} });
628
- } catch (error) {
629
- failCli(
630
- [
631
- `Failed to stop running daemon for upgrade: ${error instanceof Error ? error.message : String(error)}`,
632
- "Run `pubblue close <slug>` and retry."
633
- ].join("\n")
634
- );
635
- }
636
- const daemonStopped = await waitForProcessExit(runningDaemonInfo.pid, 6e3);
637
- if (!daemonStopped) {
638
- failCli("Daemon did not stop in time during upgrade restart.");
639
- }
640
- } else {
641
- try {
642
- const status = await ipcCall(socketPath, { method: "status", params: {} });
643
- if (!status.ok) throw new Error(String(status.error || "status check failed"));
644
- } catch (error) {
645
- failCli(
646
- [
647
- `Daemon process exists but is not responding: ${error instanceof Error ? error.message : String(error)}`,
648
- "Run `pubblue close <slug>` and start again."
649
- ].join("\n")
650
- );
651
- }
652
- if (bridgeMode !== "none") {
653
- const bridgeReady = await ensureBridgeReady({
654
- bridgeMode,
655
- slug: target.slug,
656
- socketPath,
657
- bridgeProcessEnv,
658
- timeoutMs: 8e3
659
- });
660
- if (!bridgeReady.ok) {
661
- const lines = [
662
- `Bridge failed to start for running live: ${bridgeReady.reason ?? "unknown reason"}`
663
- ];
664
- const existingBridgeLog = bridgeLogPath(target.slug);
665
- if (fs2.existsSync(existingBridgeLog)) {
666
- lines.push(`Bridge log: ${existingBridgeLog}`);
667
- const bridgeTail = readLogTail(existingBridgeLog);
668
- if (bridgeTail) {
669
- lines.push("---- bridge log tail ----");
670
- lines.push(bridgeTail.trimEnd());
671
- lines.push("---- end bridge log tail ----");
672
- }
673
- }
674
- failCli(lines.join("\n"));
675
- }
676
- }
677
- console.log(`Live started: ${target.url}`);
678
- const runTma = getTelegramMiniAppUrl(target.slug);
679
- if (runTma) console.log(`Telegram: ${runTma}`);
680
- console.log(`Slug: ${target.slug}`);
681
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
682
- console.log("Daemon already running for this live.");
683
- console.log(`Daemon log: ${logPath}`);
684
- if (bridgeMode !== "none") {
685
- console.log("Bridge mode: openclaw");
686
- console.log(`Bridge log: ${bridgeLogPath(target.slug)}`);
687
- }
688
- return;
689
- }
690
- }
691
- const { fork } = await import("child_process");
692
- const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
693
- const bridgeScript = path2.join(import.meta.dirname, "tunnel-bridge-entry.js");
694
- const daemonLogFd = fs2.openSync(logPath, "a");
695
- const child = fork(daemonScript, [], {
696
- detached: true,
697
- stdio: buildDaemonForkStdio(daemonLogFd),
698
- env: {
699
- ...bridgeProcessEnv,
700
- PUBBLUE_DAEMON_SLUG: target.slug,
701
- PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
702
- PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
703
- PUBBLUE_DAEMON_SOCKET: socketPath,
704
- PUBBLUE_DAEMON_INFO: infoPath,
705
- PUBBLUE_CLI_VERSION: CLI_VERSION,
706
- PUBBLUE_DAEMON_BRIDGE_MODE: bridgeMode,
707
- PUBBLUE_DAEMON_BRIDGE_SCRIPT: bridgeScript,
708
- PUBBLUE_DAEMON_BRIDGE_INFO: bridgeInfoPath(target.slug),
709
- PUBBLUE_DAEMON_BRIDGE_LOG: bridgeLogPath(target.slug)
710
- }
711
- });
712
- fs2.closeSync(daemonLogFd);
713
- if (child.connected) {
714
- child.disconnect();
715
- }
716
- child.unref();
717
- console.log(`Starting daemon for ${target.slug}...`);
718
- const ready = await waitForDaemonReady({
719
- child,
720
- infoPath,
721
- socketPath,
722
- timeoutMs: 8e3
723
- });
724
- if (!ready.ok) {
725
- const lines = [
726
- `Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
727
- `Daemon log: ${logPath}`
728
- ];
729
- const tail = readLogTail(logPath);
730
- if (tail) {
731
- lines.push("---- daemon log tail ----");
732
- lines.push(tail.trimEnd());
733
- lines.push("---- end daemon log tail ----");
734
- }
735
- await cleanupLiveOnStartFailure(apiClient, target);
736
- failCli(lines.join("\n"));
737
- }
738
- const offerReady = await waitForAgentOffer({
739
- apiClient,
740
- slug: target.slug,
741
- timeoutMs: 5e3
742
- });
743
- if (!offerReady.ok) {
744
- const lines = [
745
- `Daemon started but signaling is not ready: ${offerReady.reason}`,
746
- `Daemon log: ${logPath}`
747
- ];
748
- const tail = readLogTail(logPath);
749
- if (tail) {
750
- lines.push("---- daemon log tail ----");
751
- lines.push(tail.trimEnd());
752
- lines.push("---- end daemon log tail ----");
753
- }
754
- await cleanupLiveOnStartFailure(apiClient, target);
755
- failCli(lines.join("\n"));
756
- }
757
- if (bridgeMode !== "none") {
758
- const bridgeReady = await ensureBridgeReady({
759
- bridgeMode,
760
- slug: target.slug,
474
+ await startDaemon({
475
+ cliVersion: CLI_VERSION,
476
+ apiClient,
761
477
  socketPath,
762
- bridgeProcessEnv,
763
- timeoutMs: 8e3
478
+ infoPath,
479
+ bridgeMode,
480
+ agentName: opts.agentName
764
481
  });
765
- if (!bridgeReady.ok) {
766
- const lines = [`Bridge failed to start: ${bridgeReady.reason ?? "unknown reason"}`];
767
- const bridgeLog = bridgeLogPath(target.slug);
768
- if (fs2.existsSync(bridgeLog)) {
769
- lines.push(`Bridge log: ${bridgeLog}`);
770
- const bridgeTail = readLogTail(bridgeLog);
771
- if (bridgeTail) {
772
- lines.push("---- bridge log tail ----");
773
- lines.push(bridgeTail.trimEnd());
774
- lines.push("---- end bridge log tail ----");
775
- }
776
- }
777
- let daemonCloseWarning = null;
778
- try {
779
- await ipcCall(socketPath, { method: "close", params: {} });
780
- } catch (error) {
781
- daemonCloseWarning = `failed to stop daemon after bridge startup failure: ${error instanceof Error ? error.message : String(error)}`;
782
- }
783
- if (daemonCloseWarning) {
784
- lines.push(`Warning: ${daemonCloseWarning}`);
785
- }
786
- await cleanupLiveOnStartFailure(apiClient, target);
787
- failCli(lines.join("\n"));
788
- }
789
- }
790
- console.log(`Live started: ${target.url}`);
791
- const tma = getTelegramMiniAppUrl(target.slug);
792
- if (tma) console.log(`Telegram: ${tma}`);
793
- console.log(`Slug: ${target.slug}`);
794
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
795
- if (target.mode === "existing") console.log("Mode: attached existing live");
796
- console.log("Daemon health: OK");
797
- console.log(`Daemon log: ${logPath}`);
798
- if (bridgeMode !== "none") {
799
- console.log("Bridge mode: openclaw");
800
- console.log(`Bridge log: ${bridgeLogPath(target.slug)}`);
801
- }
802
- }
803
- );
804
- }
805
- function registerCloseCommand(program2) {
806
- program2.command("close").description("Close a live and stop its daemon").argument("<slug>", "Pub slug").action(async (slug) => {
807
- const bridgeError = await stopBridge(slug);
808
- if (bridgeError) console.error(bridgeError);
809
- fs2.rmSync(bridgeInfoPath(slug), { force: true });
810
- const socketPath = getSocketPath(slug);
811
- if (isDaemonRunning(slug)) {
812
- try {
813
- await ipcCall(socketPath, { method: "close", params: {} });
814
482
  } catch (error) {
815
- console.error(
816
- `Warning: failed to stop daemon over IPC for ${slug}: ${error instanceof Error ? error.message : String(error)}`
817
- );
483
+ const message = error instanceof Error ? error.message : String(error);
484
+ failCli(`Daemon failed: ${message}`);
818
485
  }
486
+ return;
819
487
  }
820
- const apiClient = createApiClient();
821
- try {
822
- await apiClient.closeLive(slug);
823
- } catch (error) {
824
- const message = formatApiError(error);
825
- if (!/Live not found/i.test(message)) {
826
- failCli(`Failed to close live for ${slug}: ${message}`);
827
- }
488
+ const { fork } = await import("child_process");
489
+ const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
490
+ const daemonLogFd = fs2.openSync(logPath, "a");
491
+ const child = fork(daemonScript, [], {
492
+ detached: true,
493
+ stdio: buildDaemonForkStdio(daemonLogFd),
494
+ env: {
495
+ ...bridgeProcessEnv,
496
+ PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
497
+ PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
498
+ PUBBLUE_DAEMON_SOCKET: socketPath,
499
+ PUBBLUE_DAEMON_INFO: infoPath,
500
+ PUBBLUE_DAEMON_AGENT_NAME: opts.agentName,
501
+ PUBBLUE_CLI_VERSION: CLI_VERSION,
502
+ PUBBLUE_DAEMON_BRIDGE_MODE: bridgeMode
503
+ }
504
+ });
505
+ fs2.closeSync(daemonLogFd);
506
+ if (child.connected) {
507
+ child.disconnect();
828
508
  }
829
- console.log(`Closed: ${slug}`);
509
+ child.unref();
510
+ console.log("Starting agent daemon...");
511
+ const ready = await waitForDaemonReady({
512
+ child,
513
+ infoPath,
514
+ socketPath,
515
+ timeoutMs: 8e3
516
+ });
517
+ if (!ready.ok) {
518
+ const lines = [
519
+ `Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
520
+ `Daemon log: ${logPath}`
521
+ ];
522
+ const tail = readLogTail(logPath);
523
+ if (tail) {
524
+ lines.push("---- daemon log tail ----");
525
+ lines.push(tail.trimEnd());
526
+ lines.push("---- end daemon log tail ----");
527
+ }
528
+ failCli(lines.join("\n"));
529
+ }
530
+ console.log("Agent daemon started. Waiting for browser to initiate live.");
531
+ console.log(`Daemon log: ${logPath}`);
532
+ console.log(`Bridge mode: ${bridgeMode}`);
533
+ });
534
+ }
535
+ function registerStopCommand(program2) {
536
+ program2.command("stop").description("Stop the agent daemon (deregisters presence, closes active live)").action(async () => {
537
+ if (!isDaemonRunning("agent")) {
538
+ console.log("Agent daemon is not running.");
539
+ return;
540
+ }
541
+ await stopOtherDaemons();
542
+ console.log("Agent daemon stopped.");
830
543
  });
831
544
  }
832
545
  function registerStatusCommand(program2) {
833
- program2.command("status").description("Check live connection status").argument("[slug]", "Pub slug").option("-s, --slug <slug>", "Pub slug (alternative to positional arg)").action(async (slugArg, opts) => {
834
- const slug = resolveSlugSelection(slugArg, opts.slug) || await resolveActiveSlug();
835
- const socketPath = getSocketPath(slug);
836
- const response = await ipcCall(socketPath, { method: "status", params: {} });
546
+ program2.command("status").description("Check agent daemon and live connection status").action(async () => {
547
+ const socketPath = getAgentSocketPath();
548
+ let response;
549
+ try {
550
+ response = await ipcCall(socketPath, { method: "status", params: {} });
551
+ } catch {
552
+ console.log("Agent daemon is not running.");
553
+ return;
554
+ }
555
+ const activeSlug = response.activeSlug;
556
+ console.log(` Daemon: running`);
557
+ console.log(` Active slug: ${activeSlug || "(none)"}`);
837
558
  console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
838
559
  console.log(` Uptime: ${response.uptime}s`);
839
560
  const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
840
- console.log(` Channels: ${chNames.join(", ")}`);
561
+ console.log(` Channels: ${chNames.join(", ") || "(none)"}`);
841
562
  console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
842
563
  if (typeof response.lastError === "string" && response.lastError.length > 0) {
843
564
  console.log(` Last error: ${response.lastError}`);
844
565
  }
845
- const logPath = liveLogPath(slug);
566
+ const logPath = liveLogPath("agent");
846
567
  if (fs2.existsSync(logPath)) {
847
568
  console.log(` Log: ${logPath}`);
848
569
  }
849
- const bridgeInfo = readBridgeProcessInfo(slug);
850
- if (bridgeInfo) {
851
- const bridgeRunning = isBridgeRunning(slug);
852
- const bridgeState = bridgeInfo.status || (bridgeRunning ? "running" : "stopped");
853
- console.log(` Bridge: ${bridgeInfo.mode} (${bridgeState})`);
854
- if (bridgeInfo.sessionId) {
855
- console.log(` Bridge session: ${bridgeInfo.sessionId}`);
570
+ const bridge = response.bridge;
571
+ if (bridge) {
572
+ console.log(` Bridge: openclaw (${bridge.running ? "running" : "stopped"})`);
573
+ if (bridge.sessionId) {
574
+ console.log(` Bridge session: ${bridge.sessionId}`);
856
575
  }
857
- if (bridgeInfo.sessionSource) {
858
- console.log(` Bridge session source: ${bridgeInfo.sessionSource}`);
576
+ if (bridge.sessionSource) {
577
+ console.log(` Bridge session source: ${bridge.sessionSource}`);
859
578
  }
860
- if (bridgeInfo.sessionKey) {
861
- console.log(` Bridge session key: ${bridgeInfo.sessionKey}`);
579
+ if (bridge.sessionKey) {
580
+ console.log(` Bridge session key: ${bridge.sessionKey}`);
862
581
  }
863
- if (bridgeInfo.lastError) {
864
- console.log(` Bridge last error: ${bridgeInfo.lastError}`);
582
+ if (bridge.forwardedMessages !== void 0) {
583
+ console.log(` Bridge forwarded: ${bridge.forwardedMessages} messages`);
584
+ }
585
+ if (bridge.lastError) {
586
+ console.log(` Bridge last error: ${bridge.lastError}`);
865
587
  }
866
- }
867
- const bridgeLog = bridgeLogPath(slug);
868
- if (fs2.existsSync(bridgeLog)) {
869
- console.log(` Bridge log: ${bridgeLog}`);
870
588
  }
871
589
  });
872
590
  }
873
591
  function registerWriteCommand(program2) {
874
- program2.command("write").description("Write data to a live channel").argument("[message]", "Text message (or use --file)").option("-s, --slug <slug>", "Pub slug (auto-detected if one active)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(
875
- async (messageArg, opts) => {
876
- let msg;
877
- let binaryBase64;
878
- if (opts.file) {
879
- const filePath = path2.resolve(opts.file);
880
- const ext = path2.extname(filePath).toLowerCase();
881
- const bytes = fs2.readFileSync(filePath);
882
- const filename = path2.basename(filePath);
883
- if (ext === ".html" || ext === ".htm") {
884
- msg = {
885
- id: generateMessageId(),
886
- type: "html",
887
- data: bytes.toString("utf-8"),
888
- meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
889
- };
890
- } else if (TEXT_FILE_EXTENSIONS.has(ext)) {
891
- msg = {
892
- id: generateMessageId(),
893
- type: "text",
894
- data: bytes.toString("utf-8"),
895
- meta: { filename, mime: getMimeType(filePath), size: bytes.length }
896
- };
897
- } else {
898
- msg = {
899
- id: generateMessageId(),
900
- type: "binary",
901
- meta: { filename, mime: getMimeType(filePath), size: bytes.length }
902
- };
903
- binaryBase64 = bytes.toString("base64");
904
- }
905
- } else if (messageArg) {
592
+ program2.command("write").description("Write data to a live channel").argument("[message]", "Text message (or use --file)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(async (messageArg, opts) => {
593
+ let msg;
594
+ let binaryBase64;
595
+ if (opts.file) {
596
+ const filePath = path2.resolve(opts.file);
597
+ const ext = path2.extname(filePath).toLowerCase();
598
+ const bytes = fs2.readFileSync(filePath);
599
+ const filename = path2.basename(filePath);
600
+ if (ext === ".html" || ext === ".htm") {
601
+ msg = {
602
+ id: generateMessageId(),
603
+ type: "html",
604
+ data: bytes.toString("utf-8"),
605
+ meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
606
+ };
607
+ } else if (TEXT_FILE_EXTENSIONS.has(ext)) {
906
608
  msg = {
907
609
  id: generateMessageId(),
908
610
  type: "text",
909
- data: messageArg
611
+ data: bytes.toString("utf-8"),
612
+ meta: { filename, mime: getMimeType(filePath), size: bytes.length }
910
613
  };
911
614
  } else {
912
- const chunks = [];
913
- for await (const chunk of process.stdin) chunks.push(chunk);
914
615
  msg = {
915
616
  id: generateMessageId(),
916
- type: "text",
917
- data: Buffer.concat(chunks).toString("utf-8").trim()
617
+ type: "binary",
618
+ meta: { filename, mime: getMimeType(filePath), size: bytes.length }
918
619
  };
620
+ binaryBase64 = bytes.toString("base64");
919
621
  }
920
- const slug = opts.slug || await resolveActiveSlug();
921
- const socketPath = getSocketPath(slug);
922
- const response = await ipcCall(socketPath, {
923
- method: "write",
924
- params: { channel: opts.channel, msg, binaryBase64 }
925
- });
926
- if (!response.ok) {
927
- failCli(`Failed: ${response.error}`);
928
- }
622
+ } else if (messageArg) {
623
+ msg = {
624
+ id: generateMessageId(),
625
+ type: "text",
626
+ data: messageArg
627
+ };
628
+ } else {
629
+ const chunks = [];
630
+ for await (const chunk of process.stdin) chunks.push(chunk);
631
+ msg = {
632
+ id: generateMessageId(),
633
+ type: "text",
634
+ data: Buffer.concat(chunks).toString("utf-8").trim()
635
+ };
929
636
  }
930
- );
637
+ const socketPath = getAgentSocketPath();
638
+ const response = await ipcCall(socketPath, {
639
+ method: "write",
640
+ params: { channel: opts.channel, msg, binaryBase64 }
641
+ });
642
+ if (!response.ok) {
643
+ failCli(`Failed: ${response.error}`);
644
+ }
645
+ });
931
646
  }
932
647
  function registerReadCommand(program2) {
933
- program2.command("read").description("Read buffered messages from live channels").argument("[slug]", "Pub slug (auto-detected if one active)").option("-s, --slug <slug>", "Pub slug (alternative to positional arg)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(
934
- async (slugArg, opts) => {
935
- const slug = resolveSlugSelection(slugArg, opts.slug) || await resolveActiveSlug();
936
- const socketPath = getSocketPath(slug);
937
- const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
938
- if (opts.follow) {
939
- if (!opts.channel && !opts.all) {
940
- console.error(
941
- "Following chat channel by default. Use `--all` to include binary/file channels."
942
- );
943
- }
944
- let consecutiveFailures = 0;
945
- let warnedDisconnected = false;
946
- while (true) {
947
- try {
948
- const response = await ipcCall(socketPath, {
949
- method: "read",
950
- params: { channel: readChannel }
951
- });
952
- if (warnedDisconnected) {
953
- console.error("Daemon reconnected.");
954
- warnedDisconnected = false;
955
- }
956
- consecutiveFailures = 0;
957
- if (response.messages && response.messages.length > 0) {
958
- for (const m of response.messages) {
959
- console.log(JSON.stringify(m));
960
- }
961
- }
962
- } catch (error) {
963
- consecutiveFailures += 1;
964
- if (!warnedDisconnected) {
965
- const detail = error instanceof Error ? ` ${error.message}` : "";
966
- console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
967
- warnedDisconnected = true;
648
+ program2.command("read").description("Read buffered messages from live channels").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(async (opts) => {
649
+ const socketPath = getAgentSocketPath();
650
+ const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
651
+ if (opts.follow) {
652
+ if (!opts.channel && !opts.all) {
653
+ console.error(
654
+ "Following chat channel by default. Use `--all` to include binary/file channels."
655
+ );
656
+ }
657
+ let consecutiveFailures = 0;
658
+ let warnedDisconnected = false;
659
+ while (true) {
660
+ try {
661
+ const response = await ipcCall(socketPath, {
662
+ method: "read",
663
+ params: { channel: readChannel }
664
+ });
665
+ if (warnedDisconnected) {
666
+ console.error("Daemon reconnected.");
667
+ warnedDisconnected = false;
668
+ }
669
+ consecutiveFailures = 0;
670
+ if (response.messages && response.messages.length > 0) {
671
+ for (const m of response.messages) {
672
+ console.log(JSON.stringify(m));
968
673
  }
969
674
  }
970
- const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
971
- await new Promise((resolve3) => setTimeout(resolve3, delayMs));
972
- }
973
- } else {
974
- const response = await ipcCall(socketPath, {
975
- method: "read",
976
- params: { channel: readChannel }
977
- });
978
- if (!response.ok) {
979
- failCli(`Failed: ${response.error}`);
675
+ } catch (error) {
676
+ consecutiveFailures += 1;
677
+ if (!warnedDisconnected) {
678
+ const detail = error instanceof Error ? ` ${error.message}` : "";
679
+ console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
680
+ warnedDisconnected = true;
681
+ }
980
682
  }
981
- console.log(JSON.stringify(response.messages || [], null, 2));
683
+ const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
684
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
982
685
  }
686
+ } else {
687
+ const response = await ipcCall(socketPath, {
688
+ method: "read",
689
+ params: { channel: readChannel }
690
+ });
691
+ if (!response.ok) {
692
+ failCli(`Failed: ${response.error}`);
693
+ }
694
+ console.log(JSON.stringify(response.messages || [], null, 2));
983
695
  }
984
- );
696
+ });
985
697
  }
986
698
  function registerChannelsCommand(program2) {
987
- program2.command("channels").description("List active live channels").argument("[slug]", "Pub slug").option("-s, --slug <slug>", "Pub slug (alternative to positional arg)").action(async (slugArg, opts) => {
988
- const slug = resolveSlugSelection(slugArg, opts.slug) || await resolveActiveSlug();
989
- const socketPath = getSocketPath(slug);
699
+ program2.command("channels").description("List active live channels").action(async () => {
700
+ const socketPath = getAgentSocketPath();
990
701
  const response = await ipcCall(socketPath, { method: "channels", params: {} });
991
702
  if (response.channels) {
992
703
  for (const ch of response.channels) {
@@ -996,12 +707,12 @@ function registerChannelsCommand(program2) {
996
707
  });
997
708
  }
998
709
  function registerDoctorCommand(program2) {
999
- program2.command("doctor").description("Run end-to-end live checks (daemon, channels, chat/canvas ping)").option("-s, --slug <slug>", "Pub slug (auto-detected if one active)").option("--timeout <seconds>", "Timeout for pong wait and repeated reads", "30").option("--wait-pong", "Wait for user to reply with exact text 'pong' on chat channel").option("--skip-chat", "Skip chat ping check").option("--skip-canvas", "Skip canvas ping check").action(
710
+ program2.command("doctor").description("Run end-to-end live checks (daemon, channels, chat/canvas ping)").option("--timeout <seconds>", "Timeout for pong wait and repeated reads", "30").option("--wait-pong", "Wait for user to reply with exact text 'pong' on chat channel").option("--skip-chat", "Skip chat ping check").option("--skip-canvas", "Skip canvas ping check").action(
1000
711
  async (opts) => {
1001
712
  const timeoutSeconds = parsePositiveIntegerOption(opts.timeout, "--timeout");
1002
713
  const timeoutMs = timeoutSeconds * 1e3;
1003
- const slug = opts.slug || await resolveActiveSlug();
1004
- const socketPath = getSocketPath(slug);
714
+ const socketPath = getAgentSocketPath();
715
+ const slug = await resolveActiveSlug();
1005
716
  const apiClient = createApiClient();
1006
717
  const fail = (message) => failCli(`Doctor failed: ${message}`);
1007
718
  console.log(`Doctor: ${slug}`);
@@ -1047,8 +758,11 @@ function registerDoctorCommand(program2) {
1047
758
  if (live.expiresAt <= Date.now()) {
1048
759
  fail("API reports live is expired.");
1049
760
  }
1050
- if (typeof live.agentOffer !== "string" || live.agentOffer.length === 0) {
1051
- fail("agent offer was not published.");
761
+ if (typeof live.browserOffer !== "string" || live.browserOffer.length === 0) {
762
+ fail("browser offer was not published.");
763
+ }
764
+ if (typeof live.agentAnswer !== "string" || live.agentAnswer.length === 0) {
765
+ fail("agent answer was not published.");
1052
766
  }
1053
767
  console.log("API/signaling check: OK");
1054
768
  if (!opts.skipChat) {
@@ -1117,7 +831,7 @@ function registerDoctorCommand(program2) {
1117
831
 
1118
832
  // src/commands/pubs.ts
1119
833
  function registerPubCommands(program2) {
1120
- program2.command("create").description("Create a new pub").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the pub").option("--public", "Make the pub public").option("--private", "Make the pub private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").option("--open", "Also open an interactive session immediately").option("--bridge <mode>", "Bridge mode if --open (openclaw/none)").action(
834
+ program2.command("create").description("Create a new pub").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the pub").option("--public", "Make the pub public").option("--private", "Make the pub private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
1121
835
  async (fileArg, opts) => {
1122
836
  const client = createClient();
1123
837
  let content;
@@ -1126,7 +840,7 @@ function registerPubCommands(program2) {
1126
840
  const file = readFile(fileArg);
1127
841
  content = file.content;
1128
842
  filename = file.basename;
1129
- } else if (!opts.open) {
843
+ } else {
1130
844
  content = await readFromStdin();
1131
845
  }
1132
846
  const resolvedVisibility = resolveVisibilityFlags({
@@ -1148,10 +862,6 @@ function registerPubCommands(program2) {
1148
862
  if (result.expiresAt) {
1149
863
  console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
1150
864
  }
1151
- if (opts.open) {
1152
- console.log(`
1153
- To open an interactive session, use: pubblue open ${result.slug}`);
1154
- }
1155
865
  }
1156
866
  );
1157
867
  program2.command("get").description("Get details of a pub").argument("<slug>", "Slug of the pub").option("--content", "Output raw content to stdout (no metadata, pipeable)").action(async (slug, opts) => {