pubblue 0.4.11 → 0.4.12

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,3 +1,6 @@
1
+ import {
2
+ TunnelApiError
3
+ } from "./chunk-7NFHPJ76.js";
1
4
  import {
2
5
  CHANNELS,
3
6
  CONTROL_CHANNEL,
@@ -55,8 +58,9 @@ function generateOffer(peer, timeoutMs) {
55
58
 
56
59
  // src/lib/tunnel-daemon-shared.ts
57
60
  var OFFER_TIMEOUT_MS = 1e4;
58
- var SIGNAL_POLL_WAITING_MS = 500;
59
- var SIGNAL_POLL_CONNECTED_MS = 2e3;
61
+ var SIGNAL_POLL_WAITING_MS = 5e3;
62
+ var SIGNAL_POLL_CONNECTED_MS = 15e3;
63
+ var LOCAL_CANDIDATE_FLUSH_MS = 2e3;
60
64
  var RECOVERY_DELAY_MS = 1e3;
61
65
  var WRITE_ACK_TIMEOUT_MS = 5e3;
62
66
  var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the tunnel URL first, then retry.";
@@ -69,6 +73,14 @@ function shouldRecoverForBrowserAnswerChange(params) {
69
73
  if (!incomingBrowserAnswer) return false;
70
74
  return incomingBrowserAnswer !== lastAppliedBrowserAnswer;
71
75
  }
76
+ function getSignalPollDelayMs(params) {
77
+ const baseDelay = params.remoteDescriptionApplied ? SIGNAL_POLL_CONNECTED_MS : SIGNAL_POLL_WAITING_MS;
78
+ if (params.retryAfterSeconds === void 0) return baseDelay;
79
+ if (!Number.isFinite(params.retryAfterSeconds) || params.retryAfterSeconds <= 0) {
80
+ return baseDelay;
81
+ }
82
+ return Math.max(baseDelay, Math.ceil(params.retryAfterSeconds * 1e3));
83
+ }
72
84
 
73
85
  // src/lib/tunnel-daemon.ts
74
86
  async function startDaemon(config) {
@@ -321,7 +333,7 @@ async function startDaemon(config) {
321
333
  await apiClient.signal(tunnelId, { candidates: newOnes }).catch((error) => {
322
334
  debugLog("failed to publish local ICE candidates", error);
323
335
  });
324
- }, 500);
336
+ }, LOCAL_CANDIDATE_FLUSH_MS);
325
337
  localCandidateStopTimer = setTimeout(() => {
326
338
  clearLocalCandidateTimers();
327
339
  }, 3e4);
@@ -441,12 +453,16 @@ async function startDaemon(config) {
441
453
  }
442
454
  async function runPollingLoop() {
443
455
  if (stopped) return;
456
+ let retryAfterSeconds;
444
457
  try {
445
458
  await pollSignalingOnce();
446
459
  } catch (error) {
460
+ if (error instanceof TunnelApiError && error.status === 429) {
461
+ retryAfterSeconds = error.retryAfterSeconds;
462
+ }
447
463
  markError("signaling poll failed", error);
448
464
  }
449
- scheduleNextPoll(remoteDescriptionApplied ? SIGNAL_POLL_CONNECTED_MS : SIGNAL_POLL_WAITING_MS);
465
+ scheduleNextPoll(getSignalPollDelayMs({ remoteDescriptionApplied, retryAfterSeconds }));
450
466
  }
451
467
  async function runNegotiationCycle() {
452
468
  if (!peer) throw new Error("PeerConnection not initialized");
@@ -656,5 +672,6 @@ async function startDaemon(config) {
656
672
  export {
657
673
  getTunnelWriteReadinessError,
658
674
  shouldRecoverForBrowserAnswerChange,
675
+ getSignalPollDelayMs,
659
676
  startDaemon
660
677
  };
package/dist/index.js CHANGED
@@ -111,6 +111,11 @@ function getConfig(homeDir) {
111
111
  bridge: saved.bridge
112
112
  };
113
113
  }
114
+ function getTelegramMiniAppUrl(type, id) {
115
+ const saved = loadConfig();
116
+ if (!saved?.telegram?.botUsername) return null;
117
+ return `https://t.me/${saved.telegram.botUsername}?startapp=${type === "pub" ? "p" : "t"}_${id}`;
118
+ }
114
119
 
115
120
  // src/commands/shared.ts
116
121
  import * as fs2 from "fs";
@@ -278,7 +283,21 @@ function parsePositiveInteger(raw, key) {
278
283
  }
279
284
  return parsed;
280
285
  }
281
- function applyBridgeSet(bridge, key, value) {
286
+ var SUPPORTED_KEYS = [
287
+ "bridge.mode",
288
+ "openclaw.path",
289
+ "openclaw.sessionId",
290
+ "openclaw.threadId",
291
+ "openclaw.canvasReminderEvery",
292
+ "openclaw.deliver",
293
+ "openclaw.deliverChannel",
294
+ "openclaw.replyTo",
295
+ "openclaw.deliverTimeoutMs",
296
+ "openclaw.attachmentDir",
297
+ "openclaw.attachmentMaxBytes",
298
+ "telegram.botToken"
299
+ ];
300
+ function applyConfigSet(bridge, telegram, key, value) {
282
301
  switch (key) {
283
302
  case "bridge.mode":
284
303
  bridge.mode = parseBridgeModeValue(value);
@@ -292,6 +311,9 @@ function applyBridgeSet(bridge, key, value) {
292
311
  case "openclaw.threadId":
293
312
  bridge.threadId = value;
294
313
  return;
314
+ case "openclaw.canvasReminderEvery":
315
+ bridge.canvasReminderEvery = parsePositiveInteger(value, key);
316
+ return;
295
317
  case "openclaw.deliver":
296
318
  bridge.deliver = parseBooleanValue(value, key);
297
319
  return;
@@ -310,26 +332,20 @@ function applyBridgeSet(bridge, key, value) {
310
332
  case "openclaw.attachmentMaxBytes":
311
333
  bridge.attachmentMaxBytes = parsePositiveInteger(value, key);
312
334
  return;
335
+ case "telegram.botToken":
336
+ telegram.botToken = value;
337
+ return;
313
338
  default:
314
339
  throw new Error(
315
340
  [
316
341
  `Unknown config key: ${key}`,
317
342
  "Supported keys:",
318
- " bridge.mode",
319
- " openclaw.path",
320
- " openclaw.sessionId",
321
- " openclaw.threadId",
322
- " openclaw.deliver",
323
- " openclaw.deliverChannel",
324
- " openclaw.replyTo",
325
- " openclaw.deliverTimeoutMs",
326
- " openclaw.attachmentDir",
327
- " openclaw.attachmentMaxBytes"
343
+ ...SUPPORTED_KEYS.map((k) => ` ${k}`)
328
344
  ].join("\n")
329
345
  );
330
346
  }
331
347
  }
332
- function applyBridgeUnset(bridge, key) {
348
+ function applyConfigUnset(bridge, telegram, key) {
333
349
  switch (key) {
334
350
  case "bridge.mode":
335
351
  delete bridge.mode;
@@ -343,6 +359,9 @@ function applyBridgeUnset(bridge, key) {
343
359
  case "openclaw.threadId":
344
360
  delete bridge.threadId;
345
361
  return;
362
+ case "openclaw.canvasReminderEvery":
363
+ delete bridge.canvasReminderEvery;
364
+ return;
346
365
  case "openclaw.deliver":
347
366
  delete bridge.deliver;
348
367
  return;
@@ -361,16 +380,45 @@ function applyBridgeUnset(bridge, key) {
361
380
  case "openclaw.attachmentMaxBytes":
362
381
  delete bridge.attachmentMaxBytes;
363
382
  return;
383
+ case "telegram.botToken":
384
+ delete telegram.botToken;
385
+ delete telegram.botUsername;
386
+ delete telegram.hasMainWebApp;
387
+ return;
364
388
  default:
365
389
  throw new Error(`Unknown config key for --unset: ${key}`);
366
390
  }
367
391
  }
368
- function hasBridgeValues(bridge) {
369
- return Object.values(bridge).some((value) => value !== void 0);
392
+ function hasValues(obj) {
393
+ return Object.values(obj).some((value) => value !== void 0);
394
+ }
395
+ function maskSecret(value) {
396
+ if (value.length <= 8) return "********";
397
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
398
+ }
399
+ async function telegramGetMe(token) {
400
+ const resp = await fetch(`https://api.telegram.org/bot${token}/getMe`);
401
+ const data = await resp.json();
402
+ if (!data.ok || !data.result?.username) {
403
+ throw new Error(data.description ?? "Invalid bot token");
404
+ }
405
+ return {
406
+ username: data.result.username,
407
+ hasMainWebApp: data.result.has_main_web_app === true
408
+ };
370
409
  }
371
- function maskApiKey(apiKey) {
372
- if (apiKey.length <= 8) return "********";
373
- return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
410
+ async function telegramSetMenuButton(token, url) {
411
+ const resp = await fetch(`https://api.telegram.org/bot${token}/setChatMenuButton`, {
412
+ method: "POST",
413
+ headers: { "Content-Type": "application/json" },
414
+ body: JSON.stringify({
415
+ menu_button: { type: "web_app", text: "Open", web_app: { url } }
416
+ })
417
+ });
418
+ const data = await resp.json();
419
+ if (!data.ok) {
420
+ throw new Error(data.description ?? "setChatMenuButton failed");
421
+ }
374
422
  }
375
423
  function printConfigSummary(saved) {
376
424
  if (!saved) {
@@ -378,34 +426,50 @@ function printConfigSummary(saved) {
378
426
  return;
379
427
  }
380
428
  console.log("Saved config:");
381
- console.log(` apiKey: ${maskApiKey(saved.apiKey)}`);
382
- if (!saved.bridge || !hasBridgeValues(saved.bridge)) {
429
+ console.log(` apiKey: ${maskSecret(saved.apiKey)}`);
430
+ if (saved.bridge && hasValues(saved.bridge)) {
431
+ console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
432
+ if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
433
+ if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
434
+ if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
435
+ if (saved.bridge.canvasReminderEvery !== void 0)
436
+ console.log(` openclaw.canvasReminderEvery: ${saved.bridge.canvasReminderEvery}`);
437
+ if (saved.bridge.deliver !== void 0)
438
+ console.log(` openclaw.deliver: ${saved.bridge.deliver ? "true" : "false"}`);
439
+ if (saved.bridge.deliverChannel)
440
+ console.log(` openclaw.deliverChannel: ${saved.bridge.deliverChannel}`);
441
+ if (saved.bridge.replyTo) console.log(` openclaw.replyTo: ${saved.bridge.replyTo}`);
442
+ if (saved.bridge.deliverTimeoutMs !== void 0)
443
+ console.log(` openclaw.deliverTimeoutMs: ${saved.bridge.deliverTimeoutMs}`);
444
+ if (saved.bridge.attachmentDir)
445
+ console.log(` openclaw.attachmentDir: ${saved.bridge.attachmentDir}`);
446
+ if (saved.bridge.attachmentMaxBytes !== void 0)
447
+ console.log(` openclaw.attachmentMaxBytes: ${saved.bridge.attachmentMaxBytes}`);
448
+ } else {
383
449
  console.log(" bridge: none");
384
- return;
385
450
  }
386
- console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
387
- if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
388
- if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
389
- if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
390
- if (saved.bridge.deliver !== void 0)
391
- console.log(` openclaw.deliver: ${saved.bridge.deliver ? "true" : "false"}`);
392
- if (saved.bridge.deliverChannel)
393
- console.log(` openclaw.deliverChannel: ${saved.bridge.deliverChannel}`);
394
- if (saved.bridge.replyTo) console.log(` openclaw.replyTo: ${saved.bridge.replyTo}`);
395
- if (saved.bridge.deliverTimeoutMs !== void 0)
396
- console.log(` openclaw.deliverTimeoutMs: ${saved.bridge.deliverTimeoutMs}`);
397
- if (saved.bridge.attachmentDir)
398
- console.log(` openclaw.attachmentDir: ${saved.bridge.attachmentDir}`);
399
- if (saved.bridge.attachmentMaxBytes !== void 0)
400
- console.log(` openclaw.attachmentMaxBytes: ${saved.bridge.attachmentMaxBytes}`);
451
+ if (saved.telegram?.botToken && saved.telegram.botUsername) {
452
+ console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
453
+ console.log(` telegram.botUsername: @${saved.telegram.botUsername}`);
454
+ if (!saved.telegram.hasMainWebApp) {
455
+ console.log(" INFO: Register Mini App in @BotFather for deep links to open in Telegram");
456
+ }
457
+ } else if (saved.telegram?.botToken) {
458
+ console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
459
+ console.log(" telegram.botUsername: (not resolved)");
460
+ } else {
461
+ console.log(" telegram: not configured");
462
+ console.log(" INFO: Set telegram.botToken to enable Telegram Mini App links");
463
+ console.log(" Example: pubblue configure --set telegram.botToken=<BOT_TOKEN>");
464
+ }
401
465
  }
402
466
  function registerConfigureCommand(program2) {
403
467
  program2.command("configure").description("Configure the CLI with your API key").option("--api-key <key>", "Your API key (less secure: appears in shell history)").option("--api-key-stdin", "Read API key from stdin").option(
404
468
  "--set <key=value>",
405
- "Set advanced config (repeatable). Example: --set openclaw.sessionId=<id>",
469
+ "Set config key (repeatable). Example: --set telegram.botToken=<token>",
406
470
  collectValues,
407
471
  []
408
- ).option("--unset <key>", "Unset advanced config key (repeatable)", collectValues, []).option("--show", "Show saved configuration").action(
472
+ ).option("--unset <key>", "Unset config key (repeatable)", collectValues, []).option("--show", "Show saved configuration").action(
409
473
  async (opts) => {
410
474
  const saved = loadConfig();
411
475
  const hasApiUpdate = Boolean(opts.apiKey || opts.apiKeyStdin);
@@ -431,16 +495,35 @@ function registerConfigureCommand(program2) {
431
495
  }
432
496
  }
433
497
  const nextBridge = { ...saved?.bridge ?? {} };
498
+ const nextTelegram = { ...saved?.telegram ?? {} };
499
+ let telegramTokenChanged = false;
434
500
  for (const entry of opts.set) {
435
501
  const { key, value } = parseSetInput(entry);
436
- applyBridgeSet(nextBridge, key, value);
502
+ applyConfigSet(nextBridge, nextTelegram, key, value);
503
+ if (key === "telegram.botToken") telegramTokenChanged = true;
437
504
  }
438
505
  for (const key of opts.unset) {
439
- applyBridgeUnset(nextBridge, key.trim());
506
+ applyConfigUnset(nextBridge, nextTelegram, key.trim());
507
+ }
508
+ if (telegramTokenChanged && nextTelegram.botToken) {
509
+ console.log("Verifying Telegram bot token...");
510
+ const bot = await telegramGetMe(nextTelegram.botToken);
511
+ nextTelegram.botUsername = bot.username;
512
+ nextTelegram.hasMainWebApp = bot.hasMainWebApp;
513
+ console.log(` Bot: @${bot.username}`);
514
+ await telegramSetMenuButton(nextTelegram.botToken, "https://pub.blue");
515
+ console.log(" Menu button set to https://pub.blue");
516
+ if (!bot.hasMainWebApp) {
517
+ console.log("");
518
+ console.log(" INFO: For deep links to open inside Telegram, register the Mini App:");
519
+ console.log(" @BotFather \u2192 /mybots \u2192 your bot \u2192 Bot Settings \u2192 Configure Mini App");
520
+ console.log(" Set Web App URL to: https://pub.blue");
521
+ }
440
522
  }
441
523
  const nextConfig = {
442
524
  apiKey,
443
- bridge: hasBridgeValues(nextBridge) ? nextBridge : void 0
525
+ bridge: hasValues(nextBridge) ? nextBridge : void 0,
526
+ telegram: hasValues(nextTelegram) ? nextTelegram : void 0
444
527
  };
445
528
  saveConfig(nextConfig);
446
529
  console.log("Configuration saved.");
@@ -479,6 +562,8 @@ function registerPublicationCommands(program2) {
479
562
  expiresIn: opts.expires
480
563
  });
481
564
  console.log(`Created: ${result.url}`);
565
+ const tmaUrl = getTelegramMiniAppUrl("pub", result.slug);
566
+ if (tmaUrl) console.log(`Telegram: ${tmaUrl}`);
482
567
  if (result.expiresAt) {
483
568
  console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
484
569
  }
@@ -643,6 +728,9 @@ function buildBridgeProcessEnv(bridgeConfig) {
643
728
  setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
644
729
  setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
645
730
  setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
731
+ if (bridgeConfig.canvasReminderEvery !== void 0) {
732
+ setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
733
+ }
646
734
  if (bridgeConfig.deliver !== void 0) {
647
735
  setIfMissing("OPENCLAW_DELIVER", bridgeConfig.deliver ? "1" : "0");
648
736
  }
@@ -1292,7 +1380,7 @@ import * as path5 from "path";
1292
1380
  // package.json
1293
1381
  var package_default = {
1294
1382
  name: "pubblue",
1295
- version: "0.4.11",
1383
+ version: "0.4.12",
1296
1384
  description: "CLI tool for publishing static content via pub.blue",
1297
1385
  type: "module",
1298
1386
  bin: {
@@ -1434,8 +1522,10 @@ function registerTunnelStartCommand(tunnel) {
1434
1522
  "Foreground mode disables managed bridge process. Use background mode for --bridge openclaw."
1435
1523
  );
1436
1524
  }
1437
- const { startDaemon } = await import("./tunnel-daemon-7B2QUHK5.js");
1525
+ const { startDaemon } = await import("./tunnel-daemon-RKWEA5BV.js");
1438
1526
  console.log(`Tunnel started: ${target.url}`);
1527
+ const fgTma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
1528
+ if (fgTma) console.log(`Telegram: ${fgTma}`);
1439
1529
  console.log(`Tunnel ID: ${target.tunnelId}`);
1440
1530
  console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1441
1531
  if (target.mode === "existing") console.log("Mode: attached existing tunnel");
@@ -1530,6 +1620,8 @@ function registerTunnelStartCommand(tunnel) {
1530
1620
  }
1531
1621
  }
1532
1622
  console.log(`Tunnel started: ${target.url}`);
1623
+ const runTma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
1624
+ if (runTma) console.log(`Telegram: ${runTma}`);
1533
1625
  console.log(`Tunnel ID: ${target.tunnelId}`);
1534
1626
  console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1535
1627
  console.log("Daemon already running for this tunnel.");
@@ -1630,6 +1722,8 @@ function registerTunnelStartCommand(tunnel) {
1630
1722
  }
1631
1723
  }
1632
1724
  console.log(`Tunnel started: ${target.url}`);
1725
+ const tma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
1726
+ if (tma) console.log(`Telegram: ${tma}`);
1633
1727
  console.log(`Tunnel ID: ${target.tunnelId}`);
1634
1728
  console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1635
1729
  if (target.mode === "existing") console.log("Mode: attached existing tunnel");
@@ -34,6 +34,7 @@ var MONITORED_ATTACHMENT_CHANNELS = /* @__PURE__ */ new Set([
34
34
  CHANNELS.MEDIA
35
35
  ]);
36
36
  var DEFAULT_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;
37
+ var DEFAULT_CANVAS_REMINDER_EVERY = 10;
37
38
  var MAX_SEEN_IDS = 1e4;
38
39
  function sleep(ms) {
39
40
  return new Promise((resolve) => {
@@ -58,6 +59,11 @@ function resolveAttachmentMaxBytes() {
58
59
  if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_ATTACHMENT_MAX_BYTES;
59
60
  return raw;
60
61
  }
62
+ function resolveCanvasReminderEvery() {
63
+ const raw = Number.parseInt(process.env.OPENCLAW_CANVAS_REMINDER_EVERY || "", 10);
64
+ if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_CANVAS_REMINDER_EVERY;
65
+ return raw;
66
+ }
61
67
  function inferExtensionFromMime(mime) {
62
68
  const normalized = mime.split(";")[0]?.trim().toLowerCase();
63
69
  if (!normalized) return ".bin";
@@ -123,8 +129,24 @@ function stageAttachment(params) {
123
129
  streamStatus: params.streamStatus
124
130
  };
125
131
  }
126
- function buildInboundPrompt(tunnelId2, userText) {
132
+ function buildCanvasPolicyReminderBlock() {
133
+ return [
134
+ "[Canvas policy reminder: do not reply to this reminder block]",
135
+ "- Prefer canvas-first responses for substantive output.",
136
+ "- Use chat only for short clarifications, confirmations, or blockers.",
137
+ "- Keep chat replies concise.",
138
+ ""
139
+ ].join("\n");
140
+ }
141
+ function shouldIncludeCanvasPolicyReminder(forwardedMessageCount, reminderEvery) {
142
+ if (!Number.isFinite(reminderEvery) || reminderEvery <= 0) return false;
143
+ if (forwardedMessageCount <= 0) return false;
144
+ return forwardedMessageCount % reminderEvery === 0;
145
+ }
146
+ function buildInboundPrompt(tunnelId2, userText, includeCanvasReminder) {
147
+ const policyReminder = includeCanvasReminder ? buildCanvasPolicyReminderBlock() : "";
127
148
  return [
149
+ policyReminder,
128
150
  `[Pubblue Tunnel ${tunnelId2}] Incoming user message:`,
129
151
  "",
130
152
  userText,
@@ -132,10 +154,12 @@ function buildInboundPrompt(tunnelId2, userText) {
132
154
  "---",
133
155
  `Reply with: pubblue tunnel write --tunnel ${tunnelId2} "<your reply>"`,
134
156
  `Canvas update: pubblue tunnel write --tunnel ${tunnelId2} -c canvas -f /path/to/file.html`
135
- ].join("\n");
157
+ ].filter(Boolean).join("\n");
136
158
  }
137
- function buildAttachmentPrompt(tunnelId2, staged) {
159
+ function buildAttachmentPrompt(tunnelId2, staged, includeCanvasReminder) {
160
+ const policyReminder = includeCanvasReminder ? buildCanvasPolicyReminderBlock() : "";
138
161
  return [
162
+ policyReminder,
139
163
  `[Pubblue Tunnel ${tunnelId2}] Incoming user attachment:`,
140
164
  `- channel: ${staged.channel}`,
141
165
  `- type: attachment`,
@@ -336,7 +360,11 @@ async function handleAttachmentEntry(params) {
336
360
  const { entry, activeStreams } = params;
337
361
  const { channel, msg } = entry;
338
362
  const stageAndDeliver = async (staged2) => {
339
- const attachmentPrompt = buildAttachmentPrompt(params.tunnelId, staged2);
363
+ const attachmentPrompt = buildAttachmentPrompt(
364
+ params.tunnelId,
365
+ staged2,
366
+ params.includeCanvasReminder
367
+ );
340
368
  await deliverMessageToOpenClaw({
341
369
  openclawPath: params.openclawPath,
342
370
  sessionId: params.sessionId,
@@ -345,6 +373,7 @@ async function handleAttachmentEntry(params) {
345
373
  };
346
374
  if (msg.type === "stream-start") {
347
375
  const existing = activeStreams.get(channel);
376
+ let deliveredInterrupted = false;
348
377
  if (existing && existing.bytes > 0) {
349
378
  const interruptedBytes = Buffer.concat(existing.chunks);
350
379
  const stagedInterrupted = stageAttachment({
@@ -359,6 +388,7 @@ async function handleAttachmentEntry(params) {
359
388
  bytes: interruptedBytes
360
389
  });
361
390
  await stageAndDeliver(stagedInterrupted);
391
+ deliveredInterrupted = true;
362
392
  }
363
393
  activeStreams.set(channel, {
364
394
  bytes: 0,
@@ -367,15 +397,15 @@ async function handleAttachmentEntry(params) {
367
397
  mime: typeof msg.meta?.mime === "string" ? msg.meta.mime : void 0,
368
398
  streamId: msg.id
369
399
  });
370
- return;
400
+ return deliveredInterrupted;
371
401
  }
372
402
  if (msg.type === "stream-end") {
373
403
  const stream2 = activeStreams.get(channel);
374
- if (!stream2) return;
404
+ if (!stream2) return false;
375
405
  const requestedStreamId = typeof msg.meta?.streamId === "string" ? msg.meta.streamId : void 0;
376
- if (requestedStreamId && requestedStreamId !== stream2.streamId) return;
406
+ if (requestedStreamId && requestedStreamId !== stream2.streamId) return false;
377
407
  activeStreams.delete(channel);
378
- if (stream2.bytes === 0) return;
408
+ if (stream2.bytes === 0) return false;
379
409
  const bytes = Buffer.concat(stream2.chunks);
380
410
  const staged2 = stageAttachment({
381
411
  attachmentRoot: params.attachmentRoot,
@@ -389,14 +419,14 @@ async function handleAttachmentEntry(params) {
389
419
  bytes
390
420
  });
391
421
  await stageAndDeliver(staged2);
392
- return;
422
+ return true;
393
423
  }
394
424
  if (msg.type === "stream-data") {
395
- if (typeof msg.data !== "string" || msg.data.length === 0) return;
425
+ if (typeof msg.data !== "string" || msg.data.length === 0) return false;
396
426
  const stream2 = activeStreams.get(channel);
397
- if (!stream2) return;
427
+ if (!stream2) return false;
398
428
  const requestedStreamId = readStreamIdFromMeta(msg.meta);
399
- if (requestedStreamId && requestedStreamId !== stream2.streamId) return;
429
+ if (requestedStreamId && requestedStreamId !== stream2.streamId) return false;
400
430
  const chunk = decodeBinaryPayload(msg.data, `${channel}/${msg.id}`);
401
431
  const nextBytes = stream2.bytes + chunk.length;
402
432
  if (nextBytes > params.attachmentMaxBytes) {
@@ -407,16 +437,16 @@ async function handleAttachmentEntry(params) {
407
437
  }
408
438
  stream2.bytes = nextBytes;
409
439
  stream2.chunks.push(chunk);
410
- return;
440
+ return false;
411
441
  }
412
442
  if (msg.type !== "binary" || typeof msg.data !== "string") {
413
- return;
443
+ return false;
414
444
  }
415
445
  const payload = decodeBinaryPayload(msg.data, `${channel}/${msg.id}`);
416
446
  const stream = activeStreams.get(channel);
417
447
  if (stream) {
418
448
  const requestedStreamId = readStreamIdFromMeta(msg.meta);
419
- if (requestedStreamId && requestedStreamId !== stream.streamId) return;
449
+ if (requestedStreamId && requestedStreamId !== stream.streamId) return false;
420
450
  const nextBytes = stream.bytes + payload.length;
421
451
  if (nextBytes > params.attachmentMaxBytes) {
422
452
  activeStreams.delete(channel);
@@ -426,7 +456,7 @@ async function handleAttachmentEntry(params) {
426
456
  }
427
457
  stream.bytes = nextBytes;
428
458
  stream.chunks.push(payload);
429
- return;
459
+ return false;
430
460
  }
431
461
  if (payload.length > params.attachmentMaxBytes) {
432
462
  throw new Error(
@@ -444,6 +474,7 @@ async function handleAttachmentEntry(params) {
444
474
  bytes: payload
445
475
  });
446
476
  await stageAndDeliver(staged);
477
+ return true;
447
478
  }
448
479
  async function startOpenClawBridge(params) {
449
480
  const startedAt = Date.now();
@@ -514,6 +545,8 @@ async function startOpenClawBridge(params) {
514
545
  });
515
546
  const seenIds = /* @__PURE__ */ new Set();
516
547
  const activeStreams = /* @__PURE__ */ new Map();
548
+ const canvasReminderEvery = resolveCanvasReminderEvery();
549
+ let forwardedMessageCount = 0;
517
550
  let consecutiveReadFailures = 0;
518
551
  while (!shuttingDown) {
519
552
  let messages = [];
@@ -542,7 +575,7 @@ async function startOpenClawBridge(params) {
542
575
  continue;
543
576
  }
544
577
  if (messages.length === 0) {
545
- await sleep(400);
578
+ await sleep(500);
546
579
  continue;
547
580
  }
548
581
  for (const rawEntry of messages) {
@@ -555,25 +588,34 @@ async function startOpenClawBridge(params) {
555
588
  seenIds.clear();
556
589
  }
557
590
  try {
591
+ const includeCanvasReminder = shouldIncludeCanvasPolicyReminder(
592
+ forwardedMessageCount + 1,
593
+ canvasReminderEvery
594
+ );
558
595
  const chat = readTextChatMessage(entry);
559
596
  if (chat) {
560
597
  await deliverMessageToOpenClaw({
561
598
  openclawPath,
562
599
  sessionId,
563
- text: buildInboundPrompt(params.tunnelId, chat)
600
+ text: buildInboundPrompt(params.tunnelId, chat, includeCanvasReminder)
564
601
  });
602
+ forwardedMessageCount += 1;
565
603
  continue;
566
604
  }
567
605
  if (!MONITORED_ATTACHMENT_CHANNELS.has(entry.channel)) continue;
568
- await handleAttachmentEntry({
606
+ const deliveredAttachment = await handleAttachmentEntry({
569
607
  activeStreams,
570
608
  attachmentMaxBytes,
571
609
  attachmentRoot,
572
610
  entry,
611
+ includeCanvasReminder,
573
612
  openclawPath,
574
613
  sessionId,
575
614
  tunnelId: params.tunnelId
576
615
  });
616
+ if (deliveredAttachment) {
617
+ forwardedMessageCount += 1;
618
+ }
577
619
  } catch (error) {
578
620
  const message = error instanceof Error ? error.message : String(error);
579
621
  console.error(`[pubblue bridge ${params.tunnelId}] ${message}`);
@@ -1,10 +1,13 @@
1
1
  import {
2
+ getSignalPollDelayMs,
2
3
  getTunnelWriteReadinessError,
3
4
  shouldRecoverForBrowserAnswerChange,
4
5
  startDaemon
5
- } from "./chunk-HAIOMGND.js";
6
+ } from "./chunk-UW7JILRJ.js";
7
+ import "./chunk-7NFHPJ76.js";
6
8
  import "./chunk-4YTJ2WKF.js";
7
9
  export {
10
+ getSignalPollDelayMs,
8
11
  getTunnelWriteReadinessError,
9
12
  shouldRecoverForBrowserAnswerChange,
10
13
  startDaemon
@@ -1,9 +1,9 @@
1
+ import {
2
+ startDaemon
3
+ } from "./chunk-UW7JILRJ.js";
1
4
  import {
2
5
  TunnelApiClient
3
6
  } from "./chunk-7NFHPJ76.js";
4
- import {
5
- startDaemon
6
- } from "./chunk-HAIOMGND.js";
7
7
  import "./chunk-4YTJ2WKF.js";
8
8
 
9
9
  // src/tunnel-daemon-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubblue",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "CLI tool for publishing static content via pub.blue",
5
5
  "type": "module",
6
6
  "bin": {