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.
@@ -1,7 +1,3 @@
1
- import {
2
- ipcCall
3
- } from "./chunk-PFZT7M3E.js";
4
-
5
1
  // src/lib/cli-error.ts
6
2
  import { CommanderError } from "commander";
7
3
  var CliError = class extends Error {
@@ -124,35 +120,90 @@ var PubApiClient = class {
124
120
  method: "DELETE"
125
121
  });
126
122
  }
127
- // -- Live management -----------------------------------------------------
128
- async openLive(slug, opts = {}) {
129
- return this.request(`/api/v1/pubs/${encodeURIComponent(slug)}/live`, {
130
- method: "POST",
123
+ // -- Agent presence -------------------------------------------------------
124
+ async goOnline() {
125
+ await this.request("/api/v1/agent/online", { method: "POST" });
126
+ }
127
+ async heartbeat() {
128
+ await this.request("/api/v1/agent/heartbeat", { method: "POST" });
129
+ }
130
+ async goOffline() {
131
+ await this.request("/api/v1/agent/offline", { method: "POST" });
132
+ }
133
+ // -- Agent live management ------------------------------------------------
134
+ async getPendingLive() {
135
+ const data = await this.request("/api/v1/agent/live");
136
+ return data.live;
137
+ }
138
+ async signalAnswer(opts) {
139
+ await this.request("/api/v1/agent/live/signal", {
140
+ method: "PATCH",
131
141
  body: JSON.stringify(opts)
132
142
  });
133
143
  }
144
+ async closeActiveLive() {
145
+ await this.request("/api/v1/agent/live", { method: "DELETE" });
146
+ }
147
+ // -- Per-slug live info ---------------------------------------------------
134
148
  async getLive(slug) {
135
149
  const data = await this.request(
136
150
  `/api/v1/pubs/${encodeURIComponent(slug)}/live`
137
151
  );
138
152
  return data.live;
139
153
  }
140
- async signal(slug, opts) {
141
- await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}/live/signal`, {
142
- method: "PATCH",
143
- body: JSON.stringify(opts)
144
- });
145
- }
146
- async closeLive(slug) {
147
- await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}/live`, {
148
- method: "DELETE"
149
- });
150
- }
151
154
  };
152
155
 
156
+ // ../shared/bridge-protocol-core.ts
157
+ var CONTROL_CHANNEL = "_control";
158
+ var CHANNELS = {
159
+ CHAT: "chat",
160
+ CANVAS: "canvas",
161
+ AUDIO: "audio",
162
+ MEDIA: "media",
163
+ FILE: "file"
164
+ };
165
+ var idCounter = 0;
166
+ function generateMessageId() {
167
+ const ts = Date.now().toString(36);
168
+ const seq = (idCounter++).toString(36);
169
+ const rand = Math.random().toString(36).slice(2, 6);
170
+ return `${ts}-${seq}-${rand}`;
171
+ }
172
+ function encodeMessage(msg) {
173
+ return JSON.stringify(msg);
174
+ }
175
+ function decodeMessage(raw) {
176
+ try {
177
+ const parsed = JSON.parse(raw);
178
+ if (parsed && typeof parsed.id === "string" && typeof parsed.type === "string") {
179
+ return parsed;
180
+ }
181
+ return null;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+ function makeEventMessage(event, meta) {
187
+ return { id: generateMessageId(), type: "event", data: event, meta };
188
+ }
189
+ function makeAckMessage(messageId, channel) {
190
+ return makeEventMessage("ack", { messageId, channel, receivedAt: Date.now() });
191
+ }
192
+ function parseAckMessage(msg) {
193
+ if (msg.type !== "event" || msg.data !== "ack" || !msg.meta) return null;
194
+ const messageId = typeof msg.meta.messageId === "string" ? msg.meta.messageId : null;
195
+ const channel = typeof msg.meta.channel === "string" ? msg.meta.channel : null;
196
+ if (!messageId || !channel) return null;
197
+ const receivedAt = typeof msg.meta.receivedAt === "number" ? msg.meta.receivedAt : void 0;
198
+ return { messageId, channel, receivedAt };
199
+ }
200
+ function shouldAcknowledgeMessage(channel, msg) {
201
+ return channel !== CONTROL_CHANNEL && parseAckMessage(msg) === null;
202
+ }
203
+
153
204
  // src/commands/tunnel-helpers.ts
154
- import { fork } from "child_process";
155
205
  import * as fs2 from "fs";
206
+ import { homedir as homedir2 } from "os";
156
207
  import * as path2 from "path";
157
208
 
158
209
  // src/lib/config.ts
@@ -215,6 +266,58 @@ function getTelegramMiniAppUrl(slug) {
215
266
  return `https://t.me/${saved.telegram.botUsername}?startapp=${slug}`;
216
267
  }
217
268
 
269
+ // src/lib/tunnel-ipc.ts
270
+ import * as net from "net";
271
+ function getAgentSocketPath() {
272
+ return "/tmp/pubblue-agent.sock";
273
+ }
274
+ async function ipcCall(socketPath, request) {
275
+ return new Promise((resolve, reject) => {
276
+ let settled = false;
277
+ let timeoutId = null;
278
+ const finish = (fn) => {
279
+ if (settled) return;
280
+ settled = true;
281
+ if (timeoutId) clearTimeout(timeoutId);
282
+ fn();
283
+ };
284
+ const client = net.createConnection(socketPath, () => {
285
+ client.write(`${JSON.stringify(request)}
286
+ `);
287
+ });
288
+ let data = "";
289
+ client.on("data", (chunk) => {
290
+ data += chunk.toString();
291
+ const newlineIdx = data.indexOf("\n");
292
+ if (newlineIdx !== -1) {
293
+ const line = data.slice(0, newlineIdx);
294
+ client.end();
295
+ try {
296
+ finish(() => resolve(JSON.parse(line)));
297
+ } catch {
298
+ finish(() => reject(new Error("Invalid response from daemon")));
299
+ }
300
+ }
301
+ });
302
+ client.on("error", (err) => {
303
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
304
+ finish(() => reject(new Error("Daemon not running.")));
305
+ } else {
306
+ finish(() => reject(err));
307
+ }
308
+ });
309
+ client.on("end", () => {
310
+ if (!data.includes("\n")) {
311
+ finish(() => reject(new Error("Daemon closed connection unexpectedly")));
312
+ }
313
+ });
314
+ timeoutId = setTimeout(() => {
315
+ client.destroy();
316
+ finish(() => reject(new Error("Daemon request timed out")));
317
+ }, 1e4);
318
+ });
319
+ }
320
+
218
321
  // src/commands/tunnel-helpers.ts
219
322
  var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
220
323
  ".txt",
@@ -280,12 +383,6 @@ function liveInfoPath(slug) {
280
383
  function liveLogPath(slug) {
281
384
  return path2.join(liveInfoDir(), `${slug}.log`);
282
385
  }
283
- function bridgeInfoPath(slug) {
284
- return path2.join(liveInfoDir(), `${slug}.bridge.json`);
285
- }
286
- function bridgeLogPath(slug) {
287
- return path2.join(liveInfoDir(), `${slug}.bridge.log`);
288
- }
289
386
  function createApiClient(configOverride) {
290
387
  const config = configOverride || getConfig();
291
388
  return new PubApiClient(config.baseUrl, config.apiKey);
@@ -293,31 +390,27 @@ function createApiClient(configOverride) {
293
390
  function buildBridgeProcessEnv(bridgeConfig) {
294
391
  const env = { ...process.env };
295
392
  const setIfMissing = (key, value) => {
296
- if (value === void 0 || value === null) return;
393
+ if (value === void 0) return;
297
394
  const current = env[key];
298
395
  if (typeof current === "string" && current.length > 0) return;
299
396
  env[key] = String(value);
300
397
  };
301
398
  setIfMissing("PUBBLUE_PROJECT_ROOT", process.cwd());
399
+ setIfMissing("OPENCLAW_HOME", homedir2());
302
400
  if (!bridgeConfig) return env;
303
401
  setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
304
402
  setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
305
403
  setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
306
- if (bridgeConfig.canvasReminderEvery !== void 0) {
307
- setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
308
- }
309
- if (bridgeConfig.deliver !== void 0) {
310
- setIfMissing("OPENCLAW_DELIVER", bridgeConfig.deliver ? "1" : "0");
311
- }
404
+ setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
405
+ setIfMissing(
406
+ "OPENCLAW_DELIVER",
407
+ bridgeConfig.deliver === void 0 ? void 0 : bridgeConfig.deliver ? "1" : "0"
408
+ );
312
409
  setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
313
410
  setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
314
- if (bridgeConfig.deliverTimeoutMs !== void 0) {
315
- setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
316
- }
411
+ setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
317
412
  setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
318
- if (bridgeConfig.attachmentMaxBytes !== void 0) {
319
- setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
320
- }
413
+ setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
321
414
  return env;
322
415
  }
323
416
  async function ensureNodeDatachannelAvailable() {
@@ -343,7 +436,7 @@ function readDaemonProcessInfo(slug) {
343
436
  try {
344
437
  const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
345
438
  if (!Number.isFinite(info.pid)) throw new Error("invalid daemon pid");
346
- process.kill(info.pid, 0);
439
+ if (!isProcessAlive(info.pid)) throw new Error("process not alive");
347
440
  return info;
348
441
  } catch {
349
442
  try {
@@ -353,30 +446,6 @@ function readDaemonProcessInfo(slug) {
353
446
  return null;
354
447
  }
355
448
  }
356
- function readBridgeProcessInfo(slug) {
357
- const infoPath = bridgeInfoPath(slug);
358
- if (!fs2.existsSync(infoPath)) return null;
359
- try {
360
- return JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
361
- } catch {
362
- return null;
363
- }
364
- }
365
- function isBridgeRunning(slug) {
366
- const infoPath = bridgeInfoPath(slug);
367
- if (!fs2.existsSync(infoPath)) return false;
368
- try {
369
- const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
370
- process.kill(info.pid, 0);
371
- return true;
372
- } catch {
373
- try {
374
- fs2.unlinkSync(infoPath);
375
- } catch {
376
- }
377
- }
378
- return false;
379
- }
380
449
  function latestCliVersionPath() {
381
450
  return path2.join(liveInfoDir(), "cli-version.txt");
382
451
  }
@@ -413,19 +482,6 @@ async function waitForProcessExit(pid, timeoutMs) {
413
482
  }
414
483
  return !isProcessAlive(pid);
415
484
  }
416
- async function stopBridge(slug) {
417
- const bridge = readBridgeProcessInfo(slug);
418
- if (!bridge || !Number.isFinite(bridge.pid)) return null;
419
- if (!isProcessAlive(bridge.pid)) return null;
420
- try {
421
- process.kill(bridge.pid, "SIGTERM");
422
- } catch (error) {
423
- return `bridge ${bridge.pid}: failed to send SIGTERM (${error instanceof Error ? error.message : String(error)})`;
424
- }
425
- const stopped = await waitForProcessExit(bridge.pid, 6e3);
426
- if (!stopped) return `bridge ${bridge.pid}: did not exit after SIGTERM`;
427
- return null;
428
- }
429
485
  async function stopDaemonForLive(info) {
430
486
  const pid = info.pid;
431
487
  if (!Number.isFinite(pid) || !isProcessAlive(pid)) return null;
@@ -451,15 +507,12 @@ async function stopDaemonForLive(info) {
451
507
  if (!stopped) return `daemon ${pid}: did not exit after stop request`;
452
508
  return null;
453
509
  }
454
- async function stopOtherDaemons(exceptSlug) {
510
+ async function stopOtherDaemons() {
455
511
  const dir = liveInfoDir();
456
- const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".json") && !name.endsWith(".bridge.json"));
512
+ const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".json"));
457
513
  const failures = [];
458
514
  for (const entry of entries) {
459
515
  const slug = entry.replace(/\.json$/, "");
460
- if (exceptSlug && slug === exceptSlug) continue;
461
- const bridgeError = await stopBridge(slug);
462
- if (bridgeError) failures.push(`[${slug}] ${bridgeError}`);
463
516
  const info = readDaemonProcessInfo(slug);
464
517
  if (!info) continue;
465
518
  const daemonError = await stopDaemonForLive(info);
@@ -468,23 +521,17 @@ async function stopOtherDaemons(exceptSlug) {
468
521
  if (failures.length > 0) {
469
522
  throw new Error(
470
523
  [
471
- "Critical: failed to stop previous live daemon/bridge processes.",
524
+ "Critical: failed to stop previous live daemon processes.",
472
525
  "Starting a new daemon now would leak resources and increase bandwidth usage.",
473
526
  ...failures
474
527
  ].join("\n")
475
528
  );
476
529
  }
477
530
  }
478
- function buildBridgeForkStdio(logFd) {
479
- return ["ignore", logFd, logFd, "ipc"];
480
- }
481
531
  function getFollowReadDelayMs(disconnected, consecutiveFailures) {
482
532
  if (!disconnected) return 1e3;
483
533
  return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
484
534
  }
485
- function resolveSlugSelection(slugArg, slugOpt) {
486
- return slugOpt || slugArg;
487
- }
488
535
  function buildDaemonForkStdio(logFd) {
489
536
  return ["ignore", logFd, logFd, "ipc"];
490
537
  }
@@ -502,9 +549,8 @@ function parseBridgeMode(raw) {
502
549
  }
503
550
  throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
504
551
  }
505
- function shouldRestartDaemonForCliUpgrade(daemonCliVersion, currentCliVersion) {
506
- if (!daemonCliVersion || daemonCliVersion.trim().length === 0) return true;
507
- return daemonCliVersion.trim() !== currentCliVersion;
552
+ function resolveBridgeMode(opts) {
553
+ return parseBridgeMode(opts.bridge || (opts.foreground ? "none" : "openclaw"));
508
554
  }
509
555
  function messageContainsPong(payload) {
510
556
  if (!payload || typeof payload !== "object") return false;
@@ -514,14 +560,6 @@ function messageContainsPong(payload) {
514
560
  const data = message.data;
515
561
  return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
516
562
  }
517
- function getPublicUrl(slug) {
518
- const base = process.env.PUBBLUE_PUBLIC_URL || "https://pub.blue";
519
- return `${base.replace(/\/$/, "")}/p/${slug}`;
520
- }
521
- function pickReusableLive(pubs, nowMs = Date.now()) {
522
- const active = pubs.filter((p) => p.live?.status === "active" && p.live.expiresAt > nowMs).sort((a, b) => b.createdAt - a.createdAt);
523
- return active[0] ?? null;
524
- }
525
563
  function readLogTail(logPath, maxChars = 4e3) {
526
564
  if (!fs2.existsSync(logPath)) return null;
527
565
  try {
@@ -541,27 +579,18 @@ function formatApiError(error) {
541
579
  }
542
580
  return error instanceof Error ? error.message : String(error);
543
581
  }
544
- async function cleanupLiveOnStartFailure(apiClient, target) {
545
- if (!target.createdNew) return;
546
- try {
547
- await apiClient.closeLive(target.slug);
548
- } catch (closeError) {
549
- console.error(`Failed to clean up live for ${target.slug}: ${formatApiError(closeError)}`);
550
- }
551
- }
552
582
  async function resolveActiveSlug() {
553
- const dir = liveInfoDir();
554
- const files = fs2.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.endsWith(".bridge.json"));
555
- const active = [];
556
- for (const f of files) {
557
- const slug = f.replace(".json", "");
558
- if (isDaemonRunning(slug)) active.push(slug);
583
+ const socketPath = getAgentSocketPath();
584
+ let response;
585
+ try {
586
+ response = await ipcCall(socketPath, { method: "active-slug", params: {} });
587
+ } catch {
588
+ failCli("No active daemon. Run `pubblue start` first.");
559
589
  }
560
- if (active.length === 0) {
561
- failCli("No active lives. Run `pubblue open <slug>` first.");
590
+ if (response.ok && typeof response.slug === "string" && response.slug.length > 0) {
591
+ return response.slug;
562
592
  }
563
- if (active.length === 1) return active[0];
564
- failCli(`Multiple active lives: ${active.join(", ")}. Specify one with --slug.`);
593
+ failCli("Daemon is running but no live is active. Wait for browser to initiate live.");
565
594
  }
566
595
  function waitForDaemonReady({
567
596
  child,
@@ -603,113 +632,6 @@ function waitForDaemonReady({
603
632
  }, timeoutMs);
604
633
  });
605
634
  }
606
- async function waitForAgentOffer(params) {
607
- const startedAt = Date.now();
608
- let lastError = null;
609
- while (Date.now() - startedAt < params.timeoutMs) {
610
- try {
611
- const session = await params.apiClient.getLive(params.slug);
612
- if (typeof session.agentOffer === "string" && session.agentOffer.length > 0) {
613
- return { ok: true };
614
- }
615
- } catch (error) {
616
- lastError = formatApiError(error);
617
- }
618
- await new Promise((resolve) => setTimeout(resolve, 150));
619
- }
620
- return {
621
- ok: false,
622
- reason: lastError ? `agent offer was not published in time (last API error: ${lastError})` : "agent offer was not published in time"
623
- };
624
- }
625
- async function ensureBridgeReady(params) {
626
- if (params.bridgeMode === "none") {
627
- return { ok: true };
628
- }
629
- const infoPath = bridgeInfoPath(params.slug);
630
- if (isBridgeRunning(params.slug)) {
631
- return waitForBridgeReady({
632
- infoPath,
633
- slug: params.slug,
634
- timeoutMs: params.timeoutMs
635
- });
636
- }
637
- const bridgeScript = path2.join(import.meta.dirname, "tunnel-bridge-entry.js");
638
- const logPath = bridgeLogPath(params.slug);
639
- const logFd = fs2.openSync(logPath, "a");
640
- const child = fork(bridgeScript, [], {
641
- detached: true,
642
- stdio: buildBridgeForkStdio(logFd),
643
- env: {
644
- ...params.bridgeProcessEnv,
645
- PUBBLUE_BRIDGE_MODE: params.bridgeMode,
646
- PUBBLUE_BRIDGE_SLUG: params.slug,
647
- PUBBLUE_BRIDGE_SOCKET: params.socketPath,
648
- PUBBLUE_BRIDGE_INFO: infoPath
649
- }
650
- });
651
- fs2.closeSync(logFd);
652
- if (child.connected) {
653
- child.disconnect();
654
- }
655
- child.unref();
656
- return waitForBridgeReady({
657
- child,
658
- infoPath,
659
- slug: params.slug,
660
- timeoutMs: params.timeoutMs
661
- });
662
- }
663
- function waitForBridgeReady({
664
- child,
665
- infoPath,
666
- slug,
667
- timeoutMs
668
- }) {
669
- return new Promise((resolve) => {
670
- let settled = false;
671
- let lastState;
672
- let lastError;
673
- const done = (result) => {
674
- if (settled) return;
675
- settled = true;
676
- clearInterval(poll);
677
- clearTimeout(timeout);
678
- if (child) {
679
- child.off("exit", onExit);
680
- }
681
- resolve(result);
682
- };
683
- const onExit = (code, signal) => {
684
- const suffix = signal ? ` (signal ${signal})` : "";
685
- done({ ok: false, reason: `bridge exited with code ${code ?? 0}${suffix}` });
686
- };
687
- if (child) {
688
- child.on("exit", onExit);
689
- }
690
- const poll = setInterval(() => {
691
- if (!fs2.existsSync(infoPath)) return;
692
- const info = readBridgeProcessInfo(slug);
693
- if (!info) return;
694
- lastState = info.status;
695
- lastError = info.lastError;
696
- if (info.status === "ready" && isBridgeRunning(slug)) {
697
- done({ ok: true });
698
- return;
699
- }
700
- if (info.status === "error") {
701
- done({
702
- ok: false,
703
- reason: info.lastError ? `bridge reported startup error: ${info.lastError}` : "bridge reported startup error"
704
- });
705
- }
706
- }, 120);
707
- const timeout = setTimeout(() => {
708
- const reason = lastError && lastError.length > 0 ? `timed out after ${timeoutMs}ms waiting for bridge readiness (last error: ${lastError})` : `timed out after ${timeoutMs}ms waiting for bridge readiness (state: ${lastState || "unknown"})`;
709
- done({ ok: false, reason });
710
- }, timeoutMs);
711
- });
712
- }
713
635
 
714
636
  export {
715
637
  failCli,
@@ -720,40 +642,35 @@ export {
720
642
  getTelegramMiniAppUrl,
721
643
  PubApiError,
722
644
  PubApiClient,
645
+ CONTROL_CHANNEL,
646
+ CHANNELS,
647
+ generateMessageId,
648
+ encodeMessage,
649
+ decodeMessage,
650
+ makeAckMessage,
651
+ parseAckMessage,
652
+ shouldAcknowledgeMessage,
653
+ getAgentSocketPath,
654
+ ipcCall,
723
655
  TEXT_FILE_EXTENSIONS,
724
656
  getMimeType,
725
657
  liveInfoPath,
726
658
  liveLogPath,
727
- bridgeInfoPath,
728
- bridgeLogPath,
729
659
  createApiClient,
730
660
  buildBridgeProcessEnv,
731
661
  ensureNodeDatachannelAvailable,
732
662
  isDaemonRunning,
733
- readDaemonProcessInfo,
734
- readBridgeProcessInfo,
735
- isBridgeRunning,
736
663
  latestCliVersionPath,
737
664
  readLatestCliVersion,
738
665
  writeLatestCliVersion,
739
- waitForProcessExit,
740
- stopBridge,
741
666
  stopOtherDaemons,
742
- buildBridgeForkStdio,
743
667
  getFollowReadDelayMs,
744
- resolveSlugSelection,
745
668
  buildDaemonForkStdio,
746
669
  parsePositiveIntegerOption,
747
- parseBridgeMode,
748
- shouldRestartDaemonForCliUpgrade,
670
+ resolveBridgeMode,
749
671
  messageContainsPong,
750
- getPublicUrl,
751
- pickReusableLive,
752
672
  readLogTail,
753
673
  formatApiError,
754
- cleanupLiveOnStartFailure,
755
674
  resolveActiveSlug,
756
- waitForDaemonReady,
757
- waitForAgentOffer,
758
- ensureBridgeReady
675
+ waitForDaemonReady
759
676
  };