pubblue 0.7.0 → 0.7.2

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.
@@ -33,6 +33,59 @@ function toCliFailure(error) {
33
33
  };
34
34
  }
35
35
 
36
+ // ../shared/protocol-runtime-core.ts
37
+ function readRecord(input) {
38
+ return input && typeof input === "object" && !Array.isArray(input) ? input : null;
39
+ }
40
+ function readString(input) {
41
+ return typeof input === "string" ? input : void 0;
42
+ }
43
+ function readNonEmptyString(input) {
44
+ return typeof input === "string" && input.trim().length > 0 ? input : void 0;
45
+ }
46
+ function readFiniteNumber(input) {
47
+ return typeof input === "number" && Number.isFinite(input) ? input : void 0;
48
+ }
49
+ function readBoolean(input) {
50
+ return typeof input === "boolean" ? input : void 0;
51
+ }
52
+ function readStringArray(input) {
53
+ if (!Array.isArray(input)) return void 0;
54
+ const values = input.filter((entry) => typeof entry === "string");
55
+ return values.length === input.length ? values : void 0;
56
+ }
57
+ function readStringRecord(input) {
58
+ const record = readRecord(input);
59
+ if (!record) return void 0;
60
+ const entries = Object.entries(record);
61
+ const values = entries.filter((entry) => typeof entry[1] === "string");
62
+ if (values.length !== entries.length) return void 0;
63
+ return Object.fromEntries(values);
64
+ }
65
+
66
+ // ../shared/live-api-core.ts
67
+ function parseLiveInfo(input) {
68
+ if (input === null || input === void 0) return null;
69
+ const record = readRecord(input);
70
+ if (!record) return null;
71
+ const slug = readNonEmptyString(record.slug);
72
+ const browserCandidates = readStringArray(record.browserCandidates);
73
+ const agentCandidates = readStringArray(record.agentCandidates);
74
+ const createdAt = readFiniteNumber(record.createdAt);
75
+ if (!slug || !browserCandidates || !agentCandidates || createdAt === void 0) {
76
+ return null;
77
+ }
78
+ return {
79
+ slug,
80
+ status: readString(record.status),
81
+ browserOffer: readString(record.browserOffer),
82
+ agentAnswer: readString(record.agentAnswer),
83
+ browserCandidates,
84
+ agentCandidates,
85
+ createdAt
86
+ };
87
+ }
88
+
36
89
  // src/lib/api.ts
37
90
  var PubApiError = class extends Error {
38
91
  constructor(message, status, retryAfterSeconds) {
@@ -163,7 +216,7 @@ var PubApiClient = class {
163
216
  });
164
217
  }
165
218
  // -- Agent live management ------------------------------------------------
166
- async getPendingLive(daemonSessionId) {
219
+ async getLive(daemonSessionId) {
167
220
  const params = new URLSearchParams();
168
221
  if (daemonSessionId) {
169
222
  params.set("daemonSessionId", daemonSessionId);
@@ -171,7 +224,11 @@ var PubApiClient = class {
171
224
  const query = params.toString();
172
225
  const path5 = query ? `/api/v1/agent/live?${query}` : "/api/v1/agent/live";
173
226
  const data = await this.request(path5);
174
- return data.live;
227
+ const live = parseLiveInfo(data.live);
228
+ if (data.live !== null && data.live !== void 0 && live === null) {
229
+ throw new PubApiError("Invalid live snapshot response from server.", 502);
230
+ }
231
+ return live;
175
232
  }
176
233
  async signalAnswer(opts) {
177
234
  await this.request("/api/v1/agent/live/signal", {
@@ -198,20 +255,21 @@ var PubApiClient = class {
198
255
  async deleteBotToken() {
199
256
  await this.request("/api/v1/agent/telegram-bot", { method: "DELETE" });
200
257
  }
201
- // -- Per-slug live info ---------------------------------------------------
202
- async getLive(slug) {
203
- const data = await this.request(
204
- `/api/v1/pubs/${encodeURIComponent(slug)}/live`
205
- );
206
- return data.live;
207
- }
208
258
  };
209
259
 
210
260
  // ../shared/bridge-protocol-core.ts
261
+ var BRIDGE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
262
+ "text",
263
+ "html",
264
+ "binary",
265
+ "stream-start",
266
+ "stream-data",
267
+ "stream-end",
268
+ "event"
269
+ ]);
211
270
  var CONTROL_CHANNEL = "_control";
212
271
  var CHANNELS = {
213
272
  CHAT: "chat",
214
- CANVAS: "canvas",
215
273
  RENDER_ERROR: "render-error",
216
274
  AUDIO: "audio",
217
275
  MEDIA: "media",
@@ -228,13 +286,51 @@ function generateMessageId() {
228
286
  function encodeMessage(msg) {
229
287
  return JSON.stringify(msg);
230
288
  }
289
+ function parseBridgeMessageMeta(input) {
290
+ if (input === void 0) return void 0;
291
+ const record = readRecord(input);
292
+ if (!record) return null;
293
+ const meta = { ...record };
294
+ const knownStringKeys = ["mime", "filename", "title"];
295
+ for (const key of knownStringKeys) {
296
+ if (record[key] === void 0) continue;
297
+ const value = readString(record[key]);
298
+ if (value === void 0) return null;
299
+ meta[key] = value;
300
+ }
301
+ const knownNumberKeys = ["sampleRate", "width", "height", "size"];
302
+ for (const key of knownNumberKeys) {
303
+ if (record[key] === void 0) continue;
304
+ const value = readFiniteNumber(record[key]);
305
+ if (value === void 0) return null;
306
+ meta[key] = value;
307
+ }
308
+ return meta;
309
+ }
310
+ function parseBridgeMessage(input) {
311
+ const record = readRecord(input);
312
+ if (!record) return null;
313
+ const id = readNonEmptyString(record.id);
314
+ const type = readString(record.type);
315
+ if (!id || !type || !BRIDGE_MESSAGE_TYPES.has(type)) {
316
+ return null;
317
+ }
318
+ const data = record.data === void 0 ? void 0 : readString(record.data);
319
+ if (record.data !== void 0 && data === void 0) {
320
+ return null;
321
+ }
322
+ const meta = parseBridgeMessageMeta(record.meta);
323
+ if (meta === null) return null;
324
+ return {
325
+ id,
326
+ type,
327
+ data,
328
+ meta
329
+ };
330
+ }
231
331
  function decodeMessage(raw) {
232
332
  try {
233
- const parsed = JSON.parse(raw);
234
- if (parsed && typeof parsed.id === "string" && typeof parsed.type === "string") {
235
- return parsed;
236
- }
237
- return null;
333
+ return parseBridgeMessage(JSON.parse(raw));
238
334
  } catch (_error) {
239
335
  return null;
240
336
  }
@@ -327,7 +423,6 @@ function buildSessionBriefing(slug, ctx, instructions) {
327
423
  "## Pub Context"
328
424
  ];
329
425
  if (ctx.title) lines.push(`- Title: ${ctx.title}`);
330
- if (ctx.contentType) lines.push(`- Content type: ${ctx.contentType}`);
331
426
  if (ctx.isPublic !== void 0)
332
427
  lines.push(`- Visibility: ${ctx.isPublic ? "public" : "private"}`);
333
428
  if (ctx.canvasContentFilePath) {
@@ -861,7 +956,10 @@ async function runClaudeSdkBridgeStartupProbe(env = process.env) {
861
956
  try {
862
957
  fs2.appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
863
958
  `);
864
- } catch {
959
+ } catch (error) {
960
+ if (process.env.PUBBLUE_DEBUG === "1") {
961
+ console.warn(`Warning: failed to append SDK probe log: ${errorMessage(error)}`);
962
+ }
865
963
  }
866
964
  };
867
965
  appendLog(`probe start socket=${socketPath}`);
@@ -903,6 +1001,7 @@ async function createClaudeSdkBridgeRunner(config, abortSignal) {
903
1001
  if (!sdk) {
904
1002
  throw new Error("Claude Agent SDK is not importable.");
905
1003
  }
1004
+ const loadedSdk = sdk;
906
1005
  const { model, claudePath, allowedTools, sdkEnv } = buildSdkSessionOptions(env);
907
1006
  const appendSystemPrompt = buildAppendSystemPrompt(config.instructions.systemPrompt, env);
908
1007
  let sessionId;
@@ -923,7 +1022,7 @@ async function createClaudeSdkBridgeRunner(config, abortSignal) {
923
1022
  }
924
1023
  const canvasReminderEvery = resolveCanvasReminderEvery();
925
1024
  function createSession() {
926
- const session2 = sdk.unstable_v2_createSession({
1025
+ const session2 = loadedSdk.unstable_v2_createSession({
927
1026
  model,
928
1027
  pathToClaudeCodeExecutable: claudePath,
929
1028
  env: {
@@ -940,7 +1039,7 @@ async function createClaudeSdkBridgeRunner(config, abortSignal) {
940
1039
  for await (const msg of session2.stream()) {
941
1040
  if (stopped) break;
942
1041
  if (msg.type === "assistant") {
943
- debugLog(`sdk assistant message received`);
1042
+ debugLog("sdk assistant message received");
944
1043
  } else if (msg.type === "result") {
945
1044
  if ("session_id" in msg && typeof msg.session_id === "string") {
946
1045
  sessionId = msg.session_id;
@@ -971,7 +1070,8 @@ async function createClaudeSdkBridgeRunner(config, abortSignal) {
971
1070
  sessionRecreations += 1;
972
1071
  try {
973
1072
  activeSession?.close();
974
- } catch {
1073
+ } catch (error2) {
1074
+ debugLog(`failed to close previous SDK session: ${errorMessage(error2)}`, error2);
975
1075
  }
976
1076
  const newSession = createSession();
977
1077
  await sendAndStream(newSession, sessionBriefing);
@@ -984,12 +1084,12 @@ async function createClaudeSdkBridgeRunner(config, abortSignal) {
984
1084
  debugLog("session briefing delivered via SDK");
985
1085
  const queue = createBridgeEntryQueue({
986
1086
  onEntry: async (entry) => {
1087
+ const includeCanvasReminder = shouldIncludeCanvasPolicyReminder(
1088
+ forwardedMessageCount + 1,
1089
+ canvasReminderEvery
1090
+ );
987
1091
  const chat = readTextChatMessage(entry);
988
1092
  if (chat) {
989
- const includeCanvasReminder = shouldIncludeCanvasPolicyReminder(
990
- forwardedMessageCount + 1,
991
- canvasReminderEvery
992
- );
993
1093
  const prompt = buildInboundPrompt(slug, chat, includeCanvasReminder, config.instructions);
994
1094
  await deliverWithRecovery(prompt);
995
1095
  forwardedMessageCount += 1;
@@ -1010,35 +1110,15 @@ async function createClaudeSdkBridgeRunner(config, abortSignal) {
1010
1110
  messageId: entry.msg.id,
1011
1111
  stage: "confirmed"
1012
1112
  });
1013
- return;
1014
- }
1015
- if (entry.msg.type === "binary" || entry.msg.type === "stream-start" || entry.msg.type === "stream-end") {
1016
- const streamId = typeof entry.msg.meta?.streamId === "string" ? entry.msg.meta.streamId : void 0;
1017
- if (entry.msg.type === "binary" && streamId) return;
1018
- const deliveryMessageId = entry.msg.type === "stream-end" && streamId ? streamId : entry.msg.id;
1019
- config.onDeliveryUpdate?.({
1020
- channel: entry.channel,
1021
- messageId: deliveryMessageId,
1022
- stage: "failed",
1023
- error: "Attachments are not supported in Claude SDK bridge mode."
1024
- });
1025
- if (entry.msg.type !== "stream-end") {
1026
- void sendMessage(CHANNELS.CHAT, {
1027
- id: generateMessageId(),
1028
- type: "text",
1029
- data: "Attachments are not supported in Claude SDK bridge mode."
1030
- });
1031
- }
1032
1113
  }
1033
1114
  },
1034
1115
  onError: (error, entry) => {
1035
1116
  const message = errorMessage(error);
1036
1117
  lastError = message;
1037
1118
  debugLog(`bridge entry processing failed: ${message}`, error);
1038
- const deliveryMessageId = entry.msg.type === "stream-end" && typeof entry.msg.meta?.streamId === "string" ? entry.msg.meta.streamId : entry.msg.id;
1039
1119
  config.onDeliveryUpdate?.({
1040
1120
  channel: entry.channel,
1041
- messageId: deliveryMessageId,
1121
+ messageId: entry.msg.id,
1042
1122
  stage: "failed",
1043
1123
  error: message
1044
1124
  });
@@ -1049,18 +1129,13 @@ async function createClaudeSdkBridgeRunner(config, abortSignal) {
1049
1129
  });
1050
1130
  }
1051
1131
  });
1052
- debugLog(`claude-sdk bridge runner started (path=${claudePath})`);
1053
1132
  return {
1054
1133
  enqueue: (entries) => queue.enqueue(entries),
1055
1134
  async stop() {
1056
1135
  if (stopped) return;
1057
1136
  stopped = true;
1058
- try {
1059
- activeSession?.close();
1060
- } catch {
1061
- }
1062
- activeSession = null;
1063
1137
  await queue.stop();
1138
+ activeSession?.close();
1064
1139
  },
1065
1140
  status() {
1066
1141
  return {
@@ -1824,19 +1899,12 @@ function sanitizeSlugForFilename(slug) {
1824
1899
  const sanitized = slug.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
1825
1900
  return sanitized.length > 0 ? sanitized : "live";
1826
1901
  }
1827
- function resolveSessionContentExtension(contentType) {
1828
- if (contentType === "html") return "html";
1829
- if (contentType === "markdown") return "md";
1830
- if (contentType === "text") return "txt";
1831
- return "txt";
1832
- }
1833
- function liveSessionContentPath(slug, contentType, rootDir) {
1902
+ function liveSessionContentPath(slug, rootDir) {
1834
1903
  const safeSlug = sanitizeSlugForFilename(slug);
1835
- const ext = resolveSessionContentExtension(contentType);
1836
- return path4.join(rootDir ?? liveInfoDir(), `${safeSlug}.session-content.${ext}`);
1904
+ return path4.join(rootDir ?? liveInfoDir(), `${safeSlug}.session-content.html`);
1837
1905
  }
1838
1906
  function writeLiveSessionContentFile(params) {
1839
- const filePath = liveSessionContentPath(params.slug, params.contentType, params.rootDir);
1907
+ const filePath = liveSessionContentPath(params.slug, params.rootDir);
1840
1908
  fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
1841
1909
  fs4.writeFileSync(filePath, params.content, "utf-8");
1842
1910
  return filePath;
@@ -1878,10 +1946,152 @@ function readLogTail(logPath, maxChars = 4e3) {
1878
1946
  }
1879
1947
  }
1880
1948
 
1949
+ // src/lib/live-ipc-protocol.ts
1950
+ function parseBufferedBridgeMessage(input) {
1951
+ const record = readRecord(input);
1952
+ if (!record) return null;
1953
+ const channel = readString(record.channel);
1954
+ const msg = parseBridgeMessage(record.msg);
1955
+ if (!channel || !msg) return null;
1956
+ return {
1957
+ channel,
1958
+ msg,
1959
+ timestamp: readFiniteNumber(record.timestamp)
1960
+ };
1961
+ }
1962
+ function parseBridgeStatus(input) {
1963
+ if (input === null) return null;
1964
+ const record = readRecord(input);
1965
+ if (!record) return null;
1966
+ const running = readBoolean(record.running);
1967
+ const forwardedMessages = readFiniteNumber(record.forwardedMessages);
1968
+ if (running === void 0 || forwardedMessages === void 0) return null;
1969
+ const sessionSourceRaw = record.sessionSource === void 0 ? void 0 : readString(record.sessionSource);
1970
+ const sessionSource = sessionSourceRaw === void 0 || sessionSourceRaw === "env" || sessionSourceRaw === "thread-canonical" || sessionSourceRaw === "thread-legacy" || sessionSourceRaw === "main-fallback" ? sessionSourceRaw : null;
1971
+ if (sessionSource === null) return null;
1972
+ return {
1973
+ running,
1974
+ sessionId: readString(record.sessionId),
1975
+ sessionKey: readString(record.sessionKey),
1976
+ sessionSource,
1977
+ lastError: readString(record.lastError),
1978
+ forwardedMessages
1979
+ };
1980
+ }
1981
+ function parseIpcRequest(input) {
1982
+ const record = readRecord(input);
1983
+ if (!record) return null;
1984
+ const method = readString(record.method);
1985
+ const params = readRecord(record.params);
1986
+ if (!method || !params) return null;
1987
+ if (method === "write") {
1988
+ const msg = parseBridgeMessage(params.msg);
1989
+ if (!msg) return null;
1990
+ const channel = params.channel === void 0 ? void 0 : readString(params.channel);
1991
+ const binaryBase64 = params.binaryBase64 === void 0 ? void 0 : readString(params.binaryBase64);
1992
+ if (params.channel !== void 0 && channel === void 0) return null;
1993
+ if (params.binaryBase64 !== void 0 && binaryBase64 === void 0) return null;
1994
+ return {
1995
+ method,
1996
+ params: {
1997
+ channel,
1998
+ msg,
1999
+ binaryBase64
2000
+ }
2001
+ };
2002
+ }
2003
+ if (method === "read") {
2004
+ const channel = params.channel === void 0 ? void 0 : readString(params.channel);
2005
+ if (params.channel !== void 0 && channel === void 0) return null;
2006
+ return { method, params: { channel } };
2007
+ }
2008
+ if (method === "channels" || method === "status" || method === "active-slug" || method === "close") {
2009
+ return { method, params: {} };
2010
+ }
2011
+ return null;
2012
+ }
2013
+ function parseIpcResponse(method, input) {
2014
+ const record = readRecord(input);
2015
+ if (!record) return null;
2016
+ const ok = readBoolean(record.ok);
2017
+ if (ok === void 0) return null;
2018
+ const error = record.error === void 0 ? void 0 : readString(record.error);
2019
+ if (record.error !== void 0 && error === void 0) return null;
2020
+ if (!ok) return { ok, error };
2021
+ if (method === "write") {
2022
+ const delivered = record.delivered === void 0 ? void 0 : readBoolean(record.delivered);
2023
+ if (record.delivered !== void 0 && delivered === void 0) return null;
2024
+ return { ok, delivered, error };
2025
+ }
2026
+ if (method === "read") {
2027
+ if (record.messages === void 0) return { ok, error };
2028
+ if (!Array.isArray(record.messages)) return null;
2029
+ const messages = record.messages.map((entry) => parseBufferedBridgeMessage(entry)).filter((entry) => entry !== null);
2030
+ if (messages.length !== record.messages.length) return null;
2031
+ return { ok, messages, error };
2032
+ }
2033
+ if (method === "channels") {
2034
+ if (record.channels === void 0) return { ok, error };
2035
+ if (!Array.isArray(record.channels)) return null;
2036
+ const channels = record.channels.map((entry) => {
2037
+ const channelRecord = readRecord(entry);
2038
+ if (!channelRecord) return null;
2039
+ const name = readString(channelRecord.name);
2040
+ const direction = readString(channelRecord.direction);
2041
+ if (!name || !direction) return null;
2042
+ return { name, direction };
2043
+ }).filter((entry) => entry !== null);
2044
+ if (channels.length !== record.channels.length) return null;
2045
+ return { ok, channels, error };
2046
+ }
2047
+ if (method === "status") {
2048
+ const connected = readBoolean(record.connected);
2049
+ const signalingConnected = record.signalingConnected === null ? null : record.signalingConnected === void 0 ? void 0 : readBoolean(record.signalingConnected);
2050
+ const activeSlug = record.activeSlug === null ? null : record.activeSlug === void 0 ? void 0 : readString(record.activeSlug);
2051
+ const uptime = readFiniteNumber(record.uptime);
2052
+ const bufferedMessages = readFiniteNumber(record.bufferedMessages);
2053
+ const lastError = record.lastError === null ? null : record.lastError === void 0 ? void 0 : readString(record.lastError);
2054
+ const bridgeMode = record.bridgeMode === null ? null : record.bridgeMode === void 0 ? void 0 : readString(record.bridgeMode);
2055
+ if (connected === void 0 || signalingConnected === void 0 || activeSlug === void 0 || uptime === void 0 || !Array.isArray(record.channels) || bufferedMessages === void 0 || lastError === void 0 || bridgeMode === void 0) {
2056
+ return null;
2057
+ }
2058
+ const channels = record.channels.filter((entry) => typeof entry === "string");
2059
+ if (channels.length !== record.channels.length) return null;
2060
+ const bridge = record.bridge === void 0 ? null : parseBridgeStatus(record.bridge);
2061
+ if (record.bridge !== void 0 && bridge === null && record.bridge !== null) return null;
2062
+ return {
2063
+ ok,
2064
+ connected,
2065
+ signalingConnected,
2066
+ activeSlug,
2067
+ uptime,
2068
+ channels,
2069
+ bufferedMessages,
2070
+ lastError,
2071
+ bridgeMode,
2072
+ bridge,
2073
+ error
2074
+ };
2075
+ }
2076
+ if (method === "active-slug") {
2077
+ const slug = record.slug === null ? null : record.slug === void 0 ? void 0 : readString(record.slug);
2078
+ if (record.slug !== void 0 && slug === void 0) return null;
2079
+ return { ok, slug, error };
2080
+ }
2081
+ return { ok, error };
2082
+ }
2083
+
1881
2084
  export {
1882
2085
  failCli,
1883
2086
  errorMessage,
1884
2087
  toCliFailure,
2088
+ readRecord,
2089
+ readString,
2090
+ readNonEmptyString,
2091
+ readFiniteNumber,
2092
+ readStringArray,
2093
+ readStringRecord,
2094
+ parseLiveInfo,
1885
2095
  PubApiError,
1886
2096
  PubApiClient,
1887
2097
  resolveOpenClawHome,
@@ -1902,6 +2112,8 @@ export {
1902
2112
  makeDeliveryReceiptMessage,
1903
2113
  parseAckMessage,
1904
2114
  shouldAcknowledgeMessage,
2115
+ parseIpcRequest,
2116
+ parseIpcResponse,
1905
2117
  buildSessionBriefing,
1906
2118
  isClaudeCodeAvailableInEnv,
1907
2119
  resolveClaudeCodePath,
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  liveInfoDir,
19
19
  liveInfoPath,
20
20
  liveLogPath,
21
+ parseIpcResponse,
21
22
  readConfig,
22
23
  readLogTail,
23
24
  resolveOpenClawHome,
@@ -28,7 +29,7 @@ import {
28
29
  saveConfig,
29
30
  toCliFailure,
30
31
  writeLatestCliVersion
31
- } from "./chunk-5ODXW2EM.js";
32
+ } from "./chunk-5LI2HLKX.js";
32
33
 
33
34
  // src/program.ts
34
35
  import { Command } from "commander";
@@ -63,10 +64,7 @@ function readFile(filePath) {
63
64
  if (!fs.existsSync(resolved)) {
64
65
  failCli(`File not found: ${resolved}`);
65
66
  }
66
- return {
67
- content: fs.readFileSync(resolved, "utf-8"),
68
- basename: path.basename(resolved)
69
- };
67
+ return fs.readFileSync(resolved, "utf-8");
70
68
  }
71
69
 
72
70
  // src/commands/configure/io.ts
@@ -430,7 +428,12 @@ async function ipcCall(socketPath, request) {
430
428
  const line = data.slice(0, newlineIdx);
431
429
  client.end();
432
430
  try {
433
- finish(() => resolve3(JSON.parse(line)));
431
+ const parsed = parseIpcResponse(request.method, JSON.parse(line));
432
+ if (!parsed) {
433
+ finish(() => reject(new Error("Invalid response from daemon")));
434
+ return;
435
+ }
436
+ finish(() => resolve3(parsed));
434
437
  } catch {
435
438
  finish(() => reject(new Error("Invalid response from daemon")));
436
439
  }
@@ -1054,7 +1057,7 @@ async function runStartPreflight(opts) {
1054
1057
  if (runtimeConfig) {
1055
1058
  const client = new PubApiClient(runtimeConfig.baseUrl, runtimeConfig.apiKey);
1056
1059
  try {
1057
- await client.getPendingLive();
1060
+ await client.getLive();
1058
1061
  passed.push({
1059
1062
  label: "api",
1060
1063
  detail: `authenticated and reachable at ${runtimeConfig.baseUrl}`
@@ -1097,7 +1100,7 @@ async function runStartPreflight(opts) {
1097
1100
  // package.json
1098
1101
  var package_default = {
1099
1102
  name: "pubblue",
1100
- version: "0.7.0",
1103
+ version: "0.7.2",
1101
1104
  description: "CLI for publishing and visualizing AI-agent output via pub.blue",
1102
1105
  type: "module",
1103
1106
  bin: {
@@ -1449,7 +1452,6 @@ function registerDoctorCommand(program2) {
1449
1452
  for (const required of [
1450
1453
  CONTROL_CHANNEL,
1451
1454
  CHANNELS.CHAT,
1452
- CHANNELS.CANVAS,
1453
1455
  CHANNELS.RENDER_ERROR,
1454
1456
  CHANNELS.COMMAND
1455
1457
  ]) {
@@ -1458,16 +1460,20 @@ function registerDoctorCommand(program2) {
1458
1460
  }
1459
1461
  }
1460
1462
  console.log("Daemon/channel check: OK");
1461
- const live = await apiClient.getLive(slug).catch(
1463
+ const live = await apiClient.getLive().catch(
1462
1464
  (error) => fail(`failed to fetch live info from API: ${formatApiError(error)}`)
1463
1465
  );
1464
- if (live.status !== "active") {
1465
- fail(`API reports live is not active (status: ${live.status})`);
1466
+ const activeLive = live ?? fail("API reports no active live session.");
1467
+ if (activeLive.slug !== slug) {
1468
+ fail(`API reports active live for "${activeLive.slug}" instead of "${slug}".`);
1469
+ }
1470
+ if (activeLive.status !== "active") {
1471
+ fail(`API reports live is not active (status: ${activeLive.status})`);
1466
1472
  }
1467
- if (typeof live.browserOffer !== "string" || live.browserOffer.length === 0) {
1473
+ if (typeof activeLive.browserOffer !== "string" || activeLive.browserOffer.length === 0) {
1468
1474
  fail("browser offer was not published.");
1469
1475
  }
1470
- if (typeof live.agentAnswer !== "string" || live.agentAnswer.length === 0) {
1476
+ if (typeof activeLive.agentAnswer !== "string" || activeLive.agentAnswer.length === 0) {
1471
1477
  fail("agent answer was not published.");
1472
1478
  }
1473
1479
  console.log("API/signaling check: OK");
@@ -1523,7 +1529,7 @@ function registerDoctorCommand(program2) {
1523
1529
  };
1524
1530
  const canvasResponse = await ipcCall(socketPath, {
1525
1531
  method: "write",
1526
- params: { channel: CHANNELS.CANVAS, msg: canvasMsg }
1532
+ params: { channel: "canvas", msg: canvasMsg }
1527
1533
  });
1528
1534
  if (!canvasResponse.ok) {
1529
1535
  fail(`canvas ping failed: ${String(canvasResponse.error || "unknown write error")}`);
@@ -1537,29 +1543,19 @@ function registerDoctorCommand(program2) {
1537
1543
 
1538
1544
  // src/commands/pubs.ts
1539
1545
  function registerPubCommands(program2) {
1540
- 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)").action(
1546
+ 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").action(
1541
1547
  async (fileArg, opts) => {
1542
1548
  const client = createClient();
1543
1549
  let content;
1544
- let filename;
1545
1550
  if (fileArg) {
1546
- const file = readFile(fileArg);
1547
- content = file.content;
1548
- filename = file.basename;
1551
+ content = readFile(fileArg);
1549
1552
  } else {
1550
1553
  content = await readFromStdin();
1551
1554
  }
1552
- const resolvedVisibility = resolveVisibilityFlags({
1553
- public: opts.public,
1554
- private: opts.private,
1555
- commandName: "create"
1556
- });
1557
1555
  const result = await client.create({
1558
1556
  content,
1559
- filename,
1560
1557
  title: opts.title,
1561
- slug: opts.slug,
1562
- isPublic: resolvedVisibility ?? false
1558
+ slug: opts.slug
1563
1559
  });
1564
1560
  console.log(`Created: ${result.url}`);
1565
1561
  const tmaUrl = getTelegramMiniAppUrl(result.slug);
@@ -1574,7 +1570,6 @@ function registerPubCommands(program2) {
1574
1570
  return;
1575
1571
  }
1576
1572
  console.log(` Slug: ${pub.slug}`);
1577
- if (pub.contentType) console.log(` Type: ${pub.contentType}`);
1578
1573
  if (pub.title) console.log(` Title: ${pub.title}`);
1579
1574
  console.log(` Status: ${formatVisibility(pub.isPublic)}`);
1580
1575
  console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
@@ -1589,11 +1584,8 @@ function registerPubCommands(program2) {
1589
1584
  async (slug, opts) => {
1590
1585
  const client = createClient();
1591
1586
  let content;
1592
- let filename;
1593
1587
  if (opts.file) {
1594
- const file = readFile(opts.file);
1595
- content = file.content;
1596
- filename = file.basename;
1588
+ content = readFile(opts.file);
1597
1589
  }
1598
1590
  const isPublic = resolveVisibilityFlags({
1599
1591
  public: opts.public,
@@ -1603,7 +1595,6 @@ function registerPubCommands(program2) {
1603
1595
  const result = await client.update({
1604
1596
  slug,
1605
1597
  content,
1606
- filename,
1607
1598
  title: opts.title,
1608
1599
  isPublic,
1609
1600
  newSlug: opts.slug
@@ -1622,10 +1613,9 @@ function registerPubCommands(program2) {
1622
1613
  }
1623
1614
  for (const pub of pubs) {
1624
1615
  const date = new Date(pub.createdAt).toLocaleDateString();
1625
- const contentLabel = pub.contentType ? `[${pub.contentType}]` : "[no content]";
1626
1616
  const sessionLabel = pub.live?.status === "active" ? " [live]" : "";
1627
1617
  console.log(
1628
- ` ${pub.slug} ${contentLabel} ${formatVisibility(pub.isPublic)} ${date}${sessionLabel}`
1618
+ ` ${pub.slug} ${formatVisibility(pub.isPublic)} ${date}${sessionLabel}`
1629
1619
  );
1630
1620
  }
1631
1621
  });
@@ -15,12 +15,20 @@ import {
15
15
  makeDeliveryReceiptMessage,
16
16
  makeEventMessage,
17
17
  parseAckMessage,
18
+ parseIpcRequest,
19
+ parseLiveInfo,
20
+ readFiniteNumber,
18
21
  readLatestCliVersion,
22
+ readNonEmptyString,
23
+ readRecord,
24
+ readString,
25
+ readStringArray,
26
+ readStringRecord,
19
27
  resolveClaudeCodePath,
20
28
  resolveOpenClawRuntime,
21
29
  shouldAcknowledgeMessage,
22
30
  writeLiveSessionContentFile
23
- } from "./chunk-5ODXW2EM.js";
31
+ } from "./chunk-5LI2HLKX.js";
24
32
 
25
33
  // src/lib/live-daemon.ts
26
34
  import { randomUUID } from "crypto";
@@ -34,48 +42,37 @@ function resolveAckChannel(input) {
34
42
  return null;
35
43
  }
36
44
 
45
+ // ../shared/webrtc-transport-core.ts
46
+ var WEBRTC_STUN_URLS = [
47
+ "stun:stun.l.google.com:19302",
48
+ "stun:stun1.l.google.com:19302"
49
+ ];
50
+ var WEBRTC_ICE_SERVER_CONFIG = [
51
+ { urls: WEBRTC_STUN_URLS[0] },
52
+ { urls: WEBRTC_STUN_URLS[1] }
53
+ ];
54
+ var ORDERED_DATA_CHANNEL_OPTIONS = {
55
+ ordered: true
56
+ };
57
+
37
58
  // src/lib/live-command-handler.ts
38
59
  import { spawn } from "child_process";
39
60
 
40
61
  // ../shared/command-protocol-core.ts
41
62
  var COMMAND_PROTOCOL_VERSION = 1;
42
63
  var COMMAND_MANIFEST_MAX_FUNCTIONS = 64;
64
+ var COMMAND_MANIFEST_MIME = "application/pubblue-command-manifest+json";
43
65
  function makeCommandResultMessage(payload) {
44
66
  return makeEventMessage("command.result", payload);
45
67
  }
46
- function readRecord(input) {
47
- return input && typeof input === "object" && !Array.isArray(input) ? input : null;
48
- }
49
- function readString(input) {
50
- return typeof input === "string" && input.trim().length > 0 ? input : void 0;
51
- }
52
68
  function readReturnType(input) {
53
69
  if (input === "void" || input === "text" || input === "json") return input;
54
70
  return void 0;
55
71
  }
56
- function readFiniteNumber(input) {
57
- if (typeof input !== "number" || !Number.isFinite(input)) return void 0;
58
- return input;
59
- }
60
- function readStringArray(input) {
61
- if (!Array.isArray(input)) return void 0;
62
- const values = input.filter((entry) => typeof entry === "string");
63
- return values.length === input.length ? values : void 0;
64
- }
65
- function readStringRecord(input) {
66
- const record = readRecord(input);
67
- if (!record) return void 0;
68
- const values = Object.entries(record).filter((entry) => {
69
- const [_key, value] = entry;
70
- return typeof value === "string";
71
- });
72
- if (values.length !== Object.keys(record).length) return void 0;
73
- return Object.fromEntries(values);
74
- }
75
72
  function parseExecutor(input) {
76
73
  const record = readRecord(input);
77
74
  if (!record) return void 0;
78
- const kind = readString(record.kind);
75
+ const kind = readNonEmptyString(record.kind);
79
76
  if (!kind) return void 0;
80
77
  if (kind === "exec") {
81
78
  const command = readString(record.command);
@@ -120,13 +117,13 @@ function parseExecutor(input) {
120
117
  function parseFunctionSpec(input, fallbackName) {
121
118
  const record = readRecord(input);
122
119
  if (!record) return null;
123
- const name = readString(record.name) ?? fallbackName;
120
+ const name = readNonEmptyString(record.name) ?? fallbackName;
124
121
  if (!name) return null;
125
122
  return {
126
123
  name,
127
124
  returns: readReturnType(record.returns),
128
125
  timeoutMs: readFiniteNumber(record.timeoutMs),
129
- description: readString(record.description),
126
+ description: readNonEmptyString(record.description),
130
127
  executor: parseExecutor(record.executor)
131
128
  };
132
129
  }
@@ -141,12 +138,11 @@ function parseFunctionList(input) {
141
138
  function parseMetaRecord(msg) {
142
139
  return msg.type === "event" && msg.meta ? readRecord(msg.meta) : null;
143
140
  }
144
- function parseCommandInvokeMessage(msg) {
145
- if (msg.type !== "event" || msg.data !== "command.invoke") return null;
146
- const meta = parseMetaRecord(msg);
141
+ function parseCommandInvokePayload(input) {
142
+ const meta = readRecord(input);
147
143
  if (!meta) return null;
148
- const callId = readString(meta.callId);
149
- const name = readString(meta.name);
144
+ const callId = readNonEmptyString(meta.callId);
145
+ const name = readNonEmptyString(meta.name);
150
146
  if (!callId || !name) return null;
151
147
  return {
152
148
  v: readFiniteNumber(meta.v) ?? COMMAND_PROTOCOL_VERSION,
@@ -156,19 +152,29 @@ function parseCommandInvokeMessage(msg) {
156
152
  timeoutMs: readFiniteNumber(meta.timeoutMs)
157
153
  };
158
154
  }
159
- function parseCommandCancelMessage(msg) {
160
- if (msg.type !== "event" || msg.data !== "command.cancel") return null;
161
- const meta = parseMetaRecord(msg);
155
+ function parseCommandInvokeMessage(msg) {
156
+ if (msg.type !== "event" || msg.data !== "command.invoke") return null;
157
+ return parseCommandInvokePayload(parseMetaRecord(msg));
158
+ }
159
+ function parseCommandCancelPayload(input) {
160
+ const meta = readRecord(input);
162
161
  if (!meta) return null;
163
- const callId = readString(meta.callId);
162
+ const callId = readNonEmptyString(meta.callId);
164
163
  if (!callId) return null;
165
164
  return {
166
165
  v: readFiniteNumber(meta.v) ?? COMMAND_PROTOCOL_VERSION,
167
166
  callId,
168
- reason: readString(meta.reason)
167
+ reason: readNonEmptyString(meta.reason)
169
168
  };
170
169
  }
171
- var MANIFEST_SCRIPT_RE = /<script\s[^>]*type\s*=\s*["']application\/pubblue-command-manifest\+json["'][^>]*>([\s\S]*?)<\/script>/i;
170
+ function parseCommandCancelMessage(msg) {
171
+ if (msg.type !== "event" || msg.data !== "command.cancel") return null;
172
+ return parseCommandCancelPayload(parseMetaRecord(msg));
173
+ }
174
+ var MANIFEST_SCRIPT_RE = new RegExp(
175
+ `<script\\s[^>]*type\\s*=\\s*["']${COMMAND_MANIFEST_MIME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'][^>]*>([\\s\\S]*?)<\\/script>`,
176
+ "i"
177
+ );
172
178
  function extractManifestFromHtml(html) {
173
179
  const match = MANIFEST_SCRIPT_RE.exec(html);
174
180
  if (!match?.[1]) return null;
@@ -422,6 +428,10 @@ function createLiveCommandHandler(params) {
422
428
  const boundFunctions = /* @__PURE__ */ new Map();
423
429
  const running = /* @__PURE__ */ new Map();
424
430
  const recentResults = /* @__PURE__ */ new Map();
431
+ function clearBindings() {
432
+ boundFunctions.clear();
433
+ params.debugLog("commands cleared bindings");
434
+ }
425
435
  function buildCancelledResult(callId, startedAt) {
426
436
  return {
427
437
  v: COMMAND_PROTOCOL_VERSION,
@@ -504,7 +514,7 @@ function createLiveCommandHandler(params) {
504
514
  });
505
515
  }
506
516
  function bindFunctions(functions) {
507
- boundFunctions.clear();
517
+ clearBindings();
508
518
  for (const entry of functions) {
509
519
  const normalized = normalizeFunctionSpec(entry);
510
520
  if (!normalized.executor) {
@@ -518,8 +528,8 @@ function createLiveCommandHandler(params) {
518
528
  function bindFromHtml(html) {
519
529
  const manifest = extractManifestFromHtml(html);
520
530
  if (!manifest) {
521
- boundFunctions.clear();
522
- params.debugLog("commands no manifest found in HTML, cleared bindings");
531
+ clearBindings();
532
+ params.debugLog("commands no manifest found in HTML");
523
533
  return;
524
534
  }
525
535
  params.debugLog(`commands manifestId=${manifest.manifestId}`);
@@ -637,11 +647,15 @@ function createLiveCommandHandler(params) {
637
647
  bindFromHtml(html) {
638
648
  bindFromHtml(html);
639
649
  },
650
+ clearBindings() {
651
+ clearBindings();
652
+ },
640
653
  stop() {
641
654
  for (const [callId, active] of running) {
642
655
  active.abort.abort();
643
656
  running.delete(callId);
644
657
  }
658
+ clearBindings();
645
659
  },
646
660
  async onMessage(message) {
647
661
  await handleBridgeMessage(message).catch((error) => {
@@ -743,20 +757,22 @@ function createAnswer(peer, browserOffer, timeoutMs) {
743
757
  }
744
758
 
745
759
  // src/lib/live-daemon-ipc-handler.ts
760
+ function unreachableIpcRequest(request) {
761
+ throw new Error(`Unsupported IPC request: ${JSON.stringify(request)}`);
762
+ }
746
763
  function createDaemonIpcHandler(params) {
747
764
  return async function handleIpcRequest(req) {
748
765
  switch (req.method) {
749
766
  case "write": {
750
767
  const channel = req.params.channel || "chat";
751
768
  const msg = req.params.msg;
752
- if (channel === CHANNELS.CANVAS && msg.type === "html" && typeof msg.data === "string") {
769
+ if (channel === "canvas" && msg.type === "html" && typeof msg.data === "string") {
753
770
  const slug = params.getActiveSlug();
754
771
  if (!slug) return { ok: false, error: "No active live session." };
755
772
  try {
756
773
  await params.apiClient.update({
757
774
  slug,
758
- content: msg.data,
759
- filename: "live-canvas.html"
775
+ content: msg.data
760
776
  });
761
777
  params.bindCanvasCommands(msg.data);
762
778
  return { ok: true, delivered: true };
@@ -862,8 +878,9 @@ function createDaemonIpcHandler(params) {
862
878
  params.shutdown();
863
879
  return { ok: true };
864
880
  }
865
- default:
866
- return { ok: false, error: `Unknown method: ${req.method}` };
881
+ default: {
882
+ return unreachableIpcRequest(req);
883
+ }
867
884
  }
868
885
  };
869
886
  }
@@ -879,17 +896,22 @@ function createDaemonIpcServer(handler) {
879
896
  if (newlineIdx === -1) return;
880
897
  const line = data.slice(0, newlineIdx);
881
898
  data = data.slice(newlineIdx + 1);
882
- let request;
883
899
  try {
884
- request = JSON.parse(line);
900
+ const request = parseIpcRequest(JSON.parse(line));
901
+ if (!request) {
902
+ conn.write(`${JSON.stringify({ ok: false, error: "Invalid request" })}
903
+ `);
904
+ return;
905
+ }
906
+ handler(request).then((response) => conn.write(`${JSON.stringify(response)}
907
+ `)).catch(
908
+ (err) => conn.write(`${JSON.stringify({ ok: false, error: errorMessage(err) })}
909
+ `)
910
+ );
885
911
  } catch {
886
912
  conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
887
913
  `);
888
- return;
889
914
  }
890
- handler(request).then((response) => conn.write(`${JSON.stringify(response)}
891
- `)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: errorMessage(err) })}
892
- `));
893
915
  });
894
916
  });
895
917
  }
@@ -990,6 +1012,9 @@ import { makeFunctionReference } from "convex/server";
990
1012
  function decideSignalingUpdate(params) {
991
1013
  const { live, activeSlug, lastAppliedBrowserOffer, lastBrowserCandidateCount } = params;
992
1014
  if (!live) {
1015
+ if (activeSlug !== null || lastAppliedBrowserOffer !== null || lastBrowserCandidateCount > 0) {
1016
+ return { type: "clear-live", nextBrowserCandidateCount: 0 };
1017
+ }
993
1018
  return { type: "noop", nextBrowserCandidateCount: lastBrowserCandidateCount };
994
1019
  }
995
1020
  if (live.browserOffer && !live.agentAnswer) {
@@ -1020,32 +1045,13 @@ function decideSignalingUpdate(params) {
1020
1045
  }
1021
1046
 
1022
1047
  // src/lib/live-daemon-signaling.ts
1023
- var LIVE_SIGNAL_QUERY = makeFunctionReference("pubs:getLiveForAgentByApiKey");
1048
+ var LIVE_SIGNAL_QUERY = makeFunctionReference("pubs:getLive");
1024
1049
  function parseLiveSnapshot(result) {
1025
- if (result === null || result === void 0) return null;
1026
- if (typeof result !== "object") {
1050
+ const live = parseLiveInfo(result);
1051
+ if (result !== null && result !== void 0 && live === null) {
1027
1052
  throw new Error("Invalid signaling snapshot: expected object or null");
1028
1053
  }
1029
- const live = result;
1030
- if (typeof live.slug !== "string") throw new Error("Invalid signaling snapshot: missing slug");
1031
- if (!Array.isArray(live.browserCandidates)) {
1032
- throw new Error("Invalid signaling snapshot: missing browserCandidates");
1033
- }
1034
- if (!Array.isArray(live.agentCandidates)) {
1035
- throw new Error("Invalid signaling snapshot: missing agentCandidates");
1036
- }
1037
- if (typeof live.createdAt !== "number") {
1038
- throw new Error("Invalid signaling snapshot: missing timestamps");
1039
- }
1040
- return {
1041
- slug: live.slug,
1042
- status: live.status,
1043
- browserOffer: live.browserOffer,
1044
- agentAnswer: live.agentAnswer,
1045
- browserCandidates: live.browserCandidates,
1046
- agentCandidates: live.agentCandidates,
1047
- createdAt: live.createdAt
1048
- };
1054
+ return live;
1049
1055
  }
1050
1056
  function createSignalingController(params) {
1051
1057
  const {
@@ -1059,7 +1065,8 @@ function createSignalingController(params) {
1059
1065
  getLastBrowserCandidateCount,
1060
1066
  setLastBrowserCandidateCount,
1061
1067
  onRecover,
1062
- onApplyBrowserCandidates
1068
+ onApplyBrowserCandidates,
1069
+ onClearLive
1063
1070
  } = params;
1064
1071
  let signalingClient = null;
1065
1072
  let signalingUnsubscribe = null;
@@ -1102,6 +1109,10 @@ function createSignalingController(params) {
1102
1109
  await onRecover(decision.slug, decision.browserOffer);
1103
1110
  return;
1104
1111
  }
1112
+ if (decision.type === "clear-live") {
1113
+ await onClearLive();
1114
+ return;
1115
+ }
1105
1116
  if (decision.type === "apply-browser-candidates") {
1106
1117
  await onApplyBrowserCandidates(decision.candidatePayloads);
1107
1118
  }
@@ -1202,7 +1213,6 @@ async function startDaemon(config) {
1202
1213
  let pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
1203
1214
  let inboundStreams = /* @__PURE__ */ new Map();
1204
1215
  let seenInboundMessageKeys = /* @__PURE__ */ new Set();
1205
- let commandProcessingQueue = Promise.resolve();
1206
1216
  let heartbeatTimer = null;
1207
1217
  let localCandidateInterval = null;
1208
1218
  let localCandidateStopTimer = null;
@@ -1311,7 +1321,7 @@ async function startDaemon(config) {
1311
1321
  runHealthCheck();
1312
1322
  }
1313
1323
  function appendBufferedMessage(entry) {
1314
- if (entry.channel === CHANNELS.CANVAS || entry.channel === CHANNELS.COMMAND) return;
1324
+ if (entry.channel === CHANNELS.COMMAND) return;
1315
1325
  buffer.messages.push(entry);
1316
1326
  if (buffer.messages.length > MAX_BUFFERED_MESSAGES) {
1317
1327
  buffer.messages.splice(0, buffer.messages.length - MAX_BUFFERED_MESSAGES);
@@ -1319,18 +1329,15 @@ async function startDaemon(config) {
1319
1329
  }
1320
1330
  function handleConnectionClosed(reason) {
1321
1331
  debugLog(`connection closed: ${reason}`);
1322
- const hadConnection = browserConnected || bridgePrimed;
1323
- browserConnected = false;
1324
- bridgePrimed = false;
1325
- bridgePriming = null;
1326
- if (bridgeAbort) {
1327
- bridgeAbort.abort();
1328
- bridgeAbort = null;
1329
- }
1330
- if (!hadConnection) return;
1331
- buffer.messages = [];
1332
- failPendingAcks();
1333
- stopPingPong();
1332
+ const hadSession = browserConnected || bridgePrimed || activeSlug !== null;
1333
+ if (!hadSession) return;
1334
+ activeSlug = null;
1335
+ commandHandler.stop();
1336
+ resetNegotiationState();
1337
+ closeCurrentPeer();
1338
+ void stopBridge().catch((error) => {
1339
+ markError("failed to stop bridge after connection closed", error);
1340
+ });
1334
1341
  }
1335
1342
  function emitDeliveryStatus(params) {
1336
1343
  if (!params.messageId || params.channel === CONTROL_CHANNEL) return;
@@ -1421,9 +1428,7 @@ async function startDaemon(config) {
1421
1428
  queueAck(msg.id, name);
1422
1429
  }
1423
1430
  if (name === CHANNELS.COMMAND) {
1424
- commandProcessingQueue = commandProcessingQueue.then(() => commandHandler.onMessage(msg)).catch((error) => {
1425
- markError("command message processing failed", error);
1426
- });
1431
+ void commandHandler.onMessage(msg);
1427
1432
  return;
1428
1433
  }
1429
1434
  appendBufferedMessage({ channel: name, msg, timestamp: Date.now() });
@@ -1556,7 +1561,7 @@ async function startDaemon(config) {
1556
1561
  if (!peer) throw new Error("PeerConnection not initialized");
1557
1562
  const existing = channels.get(name);
1558
1563
  if (existing) return existing;
1559
- const dc = peer.createDataChannel(name, { ordered: true });
1564
+ const dc = peer.createDataChannel(name, ORDERED_DATA_CHANNEL_OPTIONS);
1560
1565
  setupChannel(name, dc);
1561
1566
  return dc;
1562
1567
  }
@@ -1649,7 +1654,7 @@ async function startDaemon(config) {
1649
1654
  }
1650
1655
  function createPeer() {
1651
1656
  const nextPeer = new ndc.PeerConnection("agent", {
1652
- iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
1657
+ iceServers: [...WEBRTC_STUN_URLS]
1653
1658
  });
1654
1659
  peer = nextPeer;
1655
1660
  channels = /* @__PURE__ */ new Map();
@@ -1695,6 +1700,15 @@ async function startDaemon(config) {
1695
1700
  inboundStreams.clear();
1696
1701
  seenInboundMessageKeys.clear();
1697
1702
  }
1703
+ async function clearActiveLiveSession(reason) {
1704
+ const slug = activeSlug;
1705
+ debugLog(`clearing active live session: ${reason}${slug ? ` (${slug})` : ""}`);
1706
+ activeSlug = null;
1707
+ await stopBridge();
1708
+ commandHandler.stop();
1709
+ closeCurrentPeer();
1710
+ resetNegotiationState();
1711
+ }
1698
1712
  function startLocalCandidateFlush(slug) {
1699
1713
  clearLocalCandidateTimers();
1700
1714
  localCandidateInterval = setInterval(async () => {
@@ -1713,10 +1727,8 @@ async function startDaemon(config) {
1713
1727
  if (recovering) return;
1714
1728
  recovering = true;
1715
1729
  try {
1716
- await stopBridge();
1717
- closeCurrentPeer();
1730
+ await clearActiveLiveSession("incoming-live-recovery");
1718
1731
  createPeer();
1719
- resetNegotiationState();
1720
1732
  if (!peer) throw new Error("PeerConnection not initialized");
1721
1733
  const answer = await createAnswer(peer, browserOffer, OFFER_TIMEOUT_MS);
1722
1734
  lastAppliedBrowserOffer = browserOffer;
@@ -1755,7 +1767,10 @@ async function startDaemon(config) {
1755
1767
  lastBrowserCandidateCount = count;
1756
1768
  },
1757
1769
  onRecover: handleIncomingLive,
1758
- onApplyBrowserCandidates: applyBrowserCandidates
1770
+ onApplyBrowserCandidates: applyBrowserCandidates,
1771
+ onClearLive: async () => {
1772
+ await clearActiveLiveSession("signaling-cleared");
1773
+ }
1759
1774
  });
1760
1775
  if (fs.existsSync(socketPath2)) {
1761
1776
  let stale = true;
@@ -1836,19 +1851,16 @@ async function startDaemon(config) {
1836
1851
  async function buildInitialSessionBriefing(params) {
1837
1852
  const pub = await apiClient2.get(params.slug);
1838
1853
  const content = typeof pub.content === "string" ? pub.content : "";
1839
- if (content.length > 0) {
1840
- commandHandler.bindFromHtml(content);
1841
- }
1854
+ if (content.length > 0) commandHandler.bindFromHtml(content);
1855
+ else commandHandler.clearBindings();
1842
1856
  const canvasContentFilePath = content.length > 0 ? writeLiveSessionContentFile({
1843
1857
  slug: params.slug,
1844
- contentType: pub.contentType,
1845
1858
  content
1846
1859
  }) : void 0;
1847
1860
  return buildSessionBriefing(
1848
1861
  params.slug,
1849
1862
  {
1850
1863
  title: pub.title,
1851
- contentType: pub.contentType,
1852
1864
  isPublic: pub.isPublic,
1853
1865
  canvasContentFilePath
1854
1866
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubblue",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "CLI for publishing and visualizing AI-agent output via pub.blue",
5
5
  "type": "module",
6
6
  "bin": {