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.
- package/dist/{chunk-5ODXW2EM.js → chunk-5LI2HLKX.js} +273 -61
- package/dist/index.js +26 -36
- package/dist/live-daemon-entry.js +118 -106
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
1463
|
+
const live = await apiClient.getLive().catch(
|
|
1462
1464
|
(error) => fail(`failed to fetch live info from API: ${formatApiError(error)}`)
|
|
1463
1465
|
);
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
|
1473
|
+
if (typeof activeLive.browserOffer !== "string" || activeLive.browserOffer.length === 0) {
|
|
1468
1474
|
fail("browser offer was not published.");
|
|
1469
1475
|
}
|
|
1470
|
-
if (typeof
|
|
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:
|
|
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").
|
|
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
|
-
|
|
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
|
-
|
|
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} ${
|
|
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-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
145
|
-
|
|
146
|
-
const meta = parseMetaRecord(msg);
|
|
141
|
+
function parseCommandInvokePayload(input) {
|
|
142
|
+
const meta = readRecord(input);
|
|
147
143
|
if (!meta) return null;
|
|
148
|
-
const callId =
|
|
149
|
-
const 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
|
|
160
|
-
if (msg.type !== "event" || msg.data !== "command.
|
|
161
|
-
|
|
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 =
|
|
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:
|
|
167
|
+
reason: readNonEmptyString(meta.reason)
|
|
169
168
|
};
|
|
170
169
|
}
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
params.debugLog("commands no manifest found in HTML
|
|
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 ===
|
|
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
|
|
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:
|
|
1048
|
+
var LIVE_SIGNAL_QUERY = makeFunctionReference("pubs:getLive");
|
|
1024
1049
|
function parseLiveSnapshot(result) {
|
|
1025
|
-
|
|
1026
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
-
|
|
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,
|
|
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: [
|
|
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
|
|
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
|
-
|
|
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
|
},
|