pubblue 0.6.9 → 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-JSX5KHV3.js → chunk-5LI2HLKX.js} +552 -72
- package/dist/index.js +70 -49
- package/dist/live-daemon-entry.js +181 -217
- package/dist/sdk-IV5ZYS3G.js +12299 -0
- package/package.json +4 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
buildClaudeArgs,
|
|
6
6
|
buildSessionBriefing,
|
|
7
7
|
createClaudeCodeBridgeRunner,
|
|
8
|
+
createClaudeSdkBridgeRunner,
|
|
8
9
|
createOpenClawBridgeRunner,
|
|
9
10
|
decodeMessage,
|
|
10
11
|
encodeMessage,
|
|
@@ -14,12 +15,20 @@ import {
|
|
|
14
15
|
makeDeliveryReceiptMessage,
|
|
15
16
|
makeEventMessage,
|
|
16
17
|
parseAckMessage,
|
|
18
|
+
parseIpcRequest,
|
|
19
|
+
parseLiveInfo,
|
|
20
|
+
readFiniteNumber,
|
|
17
21
|
readLatestCliVersion,
|
|
22
|
+
readNonEmptyString,
|
|
23
|
+
readRecord,
|
|
24
|
+
readString,
|
|
25
|
+
readStringArray,
|
|
26
|
+
readStringRecord,
|
|
18
27
|
resolveClaudeCodePath,
|
|
19
28
|
resolveOpenClawRuntime,
|
|
20
29
|
shouldAcknowledgeMessage,
|
|
21
30
|
writeLiveSessionContentFile
|
|
22
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-5LI2HLKX.js";
|
|
23
32
|
|
|
24
33
|
// src/lib/live-daemon.ts
|
|
25
34
|
import { randomUUID } from "crypto";
|
|
@@ -33,51 +42,37 @@ function resolveAckChannel(input) {
|
|
|
33
42
|
return null;
|
|
34
43
|
}
|
|
35
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
|
+
|
|
36
58
|
// src/lib/live-command-handler.ts
|
|
37
59
|
import { spawn } from "child_process";
|
|
38
60
|
|
|
39
61
|
// ../shared/command-protocol-core.ts
|
|
40
62
|
var COMMAND_PROTOCOL_VERSION = 1;
|
|
41
63
|
var COMMAND_MANIFEST_MAX_FUNCTIONS = 64;
|
|
42
|
-
|
|
43
|
-
return makeEventMessage("command.bind.result", payload);
|
|
44
|
-
}
|
|
64
|
+
var COMMAND_MANIFEST_MIME = "application/pubblue-command-manifest+json";
|
|
45
65
|
function makeCommandResultMessage(payload) {
|
|
46
66
|
return makeEventMessage("command.result", payload);
|
|
47
67
|
}
|
|
48
|
-
function readRecord(input) {
|
|
49
|
-
return input && typeof input === "object" && !Array.isArray(input) ? input : null;
|
|
50
|
-
}
|
|
51
|
-
function readString(input) {
|
|
52
|
-
return typeof input === "string" && input.trim().length > 0 ? input : void 0;
|
|
53
|
-
}
|
|
54
68
|
function readReturnType(input) {
|
|
55
69
|
if (input === "void" || input === "text" || input === "json") return input;
|
|
56
70
|
return void 0;
|
|
57
71
|
}
|
|
58
|
-
function readFiniteNumber(input) {
|
|
59
|
-
if (typeof input !== "number" || !Number.isFinite(input)) return void 0;
|
|
60
|
-
return input;
|
|
61
|
-
}
|
|
62
|
-
function readStringArray(input) {
|
|
63
|
-
if (!Array.isArray(input)) return void 0;
|
|
64
|
-
const values = input.filter((entry) => typeof entry === "string");
|
|
65
|
-
return values.length === input.length ? values : void 0;
|
|
66
|
-
}
|
|
67
|
-
function readStringRecord(input) {
|
|
68
|
-
const record = readRecord(input);
|
|
69
|
-
if (!record) return void 0;
|
|
70
|
-
const values = Object.entries(record).filter((entry) => {
|
|
71
|
-
const [_key, value] = entry;
|
|
72
|
-
return typeof value === "string";
|
|
73
|
-
});
|
|
74
|
-
if (values.length !== Object.keys(record).length) return void 0;
|
|
75
|
-
return Object.fromEntries(values);
|
|
76
|
-
}
|
|
77
72
|
function parseExecutor(input) {
|
|
78
73
|
const record = readRecord(input);
|
|
79
74
|
if (!record) return void 0;
|
|
80
|
-
const kind =
|
|
75
|
+
const kind = readNonEmptyString(record.kind);
|
|
81
76
|
if (!kind) return void 0;
|
|
82
77
|
if (kind === "exec") {
|
|
83
78
|
const command = readString(record.command);
|
|
@@ -122,13 +117,13 @@ function parseExecutor(input) {
|
|
|
122
117
|
function parseFunctionSpec(input, fallbackName) {
|
|
123
118
|
const record = readRecord(input);
|
|
124
119
|
if (!record) return null;
|
|
125
|
-
const name =
|
|
120
|
+
const name = readNonEmptyString(record.name) ?? fallbackName;
|
|
126
121
|
if (!name) return null;
|
|
127
122
|
return {
|
|
128
123
|
name,
|
|
129
124
|
returns: readReturnType(record.returns),
|
|
130
125
|
timeoutMs: readFiniteNumber(record.timeoutMs),
|
|
131
|
-
description:
|
|
126
|
+
description: readNonEmptyString(record.description),
|
|
132
127
|
executor: parseExecutor(record.executor)
|
|
133
128
|
};
|
|
134
129
|
}
|
|
@@ -143,43 +138,62 @@ function parseFunctionList(input) {
|
|
|
143
138
|
function parseMetaRecord(msg) {
|
|
144
139
|
return msg.type === "event" && msg.meta ? readRecord(msg.meta) : null;
|
|
145
140
|
}
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
const meta = parseMetaRecord(msg);
|
|
141
|
+
function parseCommandInvokePayload(input) {
|
|
142
|
+
const meta = readRecord(input);
|
|
149
143
|
if (!meta) return null;
|
|
150
|
-
const
|
|
151
|
-
|
|
144
|
+
const callId = readNonEmptyString(meta.callId);
|
|
145
|
+
const name = readNonEmptyString(meta.name);
|
|
146
|
+
if (!callId || !name) return null;
|
|
152
147
|
return {
|
|
153
148
|
v: readFiniteNumber(meta.v) ?? COMMAND_PROTOCOL_VERSION,
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
callId,
|
|
150
|
+
name,
|
|
151
|
+
args: readRecord(meta.args) ?? void 0,
|
|
152
|
+
timeoutMs: readFiniteNumber(meta.timeoutMs)
|
|
156
153
|
};
|
|
157
154
|
}
|
|
158
155
|
function parseCommandInvokeMessage(msg) {
|
|
159
156
|
if (msg.type !== "event" || msg.data !== "command.invoke") return null;
|
|
160
|
-
|
|
157
|
+
return parseCommandInvokePayload(parseMetaRecord(msg));
|
|
158
|
+
}
|
|
159
|
+
function parseCommandCancelPayload(input) {
|
|
160
|
+
const meta = readRecord(input);
|
|
161
161
|
if (!meta) return null;
|
|
162
|
-
const callId =
|
|
163
|
-
|
|
164
|
-
if (!callId || !name) return null;
|
|
162
|
+
const callId = readNonEmptyString(meta.callId);
|
|
163
|
+
if (!callId) return null;
|
|
165
164
|
return {
|
|
166
165
|
v: readFiniteNumber(meta.v) ?? COMMAND_PROTOCOL_VERSION,
|
|
167
166
|
callId,
|
|
168
|
-
|
|
169
|
-
args: readRecord(meta.args) ?? void 0,
|
|
170
|
-
timeoutMs: readFiniteNumber(meta.timeoutMs)
|
|
167
|
+
reason: readNonEmptyString(meta.reason)
|
|
171
168
|
};
|
|
172
169
|
}
|
|
173
170
|
function parseCommandCancelMessage(msg) {
|
|
174
171
|
if (msg.type !== "event" || msg.data !== "command.cancel") return null;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
);
|
|
178
|
+
function extractManifestFromHtml(html) {
|
|
179
|
+
const match = MANIFEST_SCRIPT_RE.exec(html);
|
|
180
|
+
if (!match?.[1]) return null;
|
|
181
|
+
const raw = match[1].trim();
|
|
182
|
+
if (raw.length === 0) return null;
|
|
183
|
+
let parsed;
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(raw);
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
190
|
+
const record = parsed;
|
|
191
|
+
const manifestId = typeof record.manifestId === "string" && record.manifestId.length > 0 ? record.manifestId : `manifest-${Date.now().toString(36)}`;
|
|
192
|
+
const functions = parseFunctionList(record.functions);
|
|
179
193
|
return {
|
|
180
|
-
v:
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
v: typeof record.version === "number" ? record.version : 1,
|
|
195
|
+
manifestId,
|
|
196
|
+
functions
|
|
183
197
|
};
|
|
184
198
|
}
|
|
185
199
|
|
|
@@ -414,6 +428,10 @@ function createLiveCommandHandler(params) {
|
|
|
414
428
|
const boundFunctions = /* @__PURE__ */ new Map();
|
|
415
429
|
const running = /* @__PURE__ */ new Map();
|
|
416
430
|
const recentResults = /* @__PURE__ */ new Map();
|
|
431
|
+
function clearBindings() {
|
|
432
|
+
boundFunctions.clear();
|
|
433
|
+
params.debugLog("commands cleared bindings");
|
|
434
|
+
}
|
|
417
435
|
function buildCancelledResult(callId, startedAt) {
|
|
418
436
|
return {
|
|
419
437
|
v: COMMAND_PROTOCOL_VERSION,
|
|
@@ -433,9 +451,6 @@ function createLiveCommandHandler(params) {
|
|
|
433
451
|
});
|
|
434
452
|
await params.sendCommandMessage(makeCommandResultMessage(payload));
|
|
435
453
|
}
|
|
436
|
-
async function sendBindResult(payload) {
|
|
437
|
-
await params.sendCommandMessage(makeCommandBindResultMessage(payload));
|
|
438
|
-
}
|
|
439
454
|
async function executeFunction(spec, args, abortSignal) {
|
|
440
455
|
const executor = spec.executor;
|
|
441
456
|
if (!executor) {
|
|
@@ -498,39 +513,27 @@ function createLiveCommandHandler(params) {
|
|
|
498
513
|
signal: abortSignal
|
|
499
514
|
});
|
|
500
515
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
);
|
|
505
|
-
const accepted = [];
|
|
506
|
-
const rejected = [];
|
|
507
|
-
boundFunctions.clear();
|
|
508
|
-
for (const entry of message.functions) {
|
|
516
|
+
function bindFunctions(functions) {
|
|
517
|
+
clearBindings();
|
|
518
|
+
for (const entry of functions) {
|
|
509
519
|
const normalized = normalizeFunctionSpec(entry);
|
|
510
520
|
if (!normalized.executor) {
|
|
511
|
-
params.debugLog(`
|
|
512
|
-
rejected.push({
|
|
513
|
-
name: normalized.name,
|
|
514
|
-
code: "INVALID_FUNCTION",
|
|
515
|
-
message: `Function "${normalized.name}" is missing executor definition.`
|
|
516
|
-
});
|
|
521
|
+
params.debugLog(`commands skipped "${normalized.name}" \u2014 missing executor`);
|
|
517
522
|
continue;
|
|
518
523
|
}
|
|
519
524
|
boundFunctions.set(normalized.name, normalized);
|
|
520
|
-
accepted.push({
|
|
521
|
-
name: normalized.name,
|
|
522
|
-
returns: normalized.returns ?? "void"
|
|
523
|
-
});
|
|
524
525
|
}
|
|
525
|
-
params.debugLog(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
526
|
+
params.debugLog(`commands bound=[${[...boundFunctions.keys()].join(", ")}]`);
|
|
527
|
+
}
|
|
528
|
+
function bindFromHtml(html) {
|
|
529
|
+
const manifest = extractManifestFromHtml(html);
|
|
530
|
+
if (!manifest) {
|
|
531
|
+
clearBindings();
|
|
532
|
+
params.debugLog("commands no manifest found in HTML");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
params.debugLog(`commands manifestId=${manifest.manifestId}`);
|
|
536
|
+
bindFunctions(manifest.functions);
|
|
534
537
|
}
|
|
535
538
|
async function handleInvoke(message) {
|
|
536
539
|
if (!message) return;
|
|
@@ -552,7 +555,7 @@ function createLiveCommandHandler(params) {
|
|
|
552
555
|
}
|
|
553
556
|
const spec = getSpec(message.name);
|
|
554
557
|
if (!spec) {
|
|
555
|
-
params.debugLog(`
|
|
558
|
+
params.debugLog(`commands invoke COMMAND_NOT_FOUND "${message.name}"`);
|
|
556
559
|
await sendResult({
|
|
557
560
|
v: COMMAND_PROTOCOL_VERSION,
|
|
558
561
|
callId: message.callId,
|
|
@@ -566,7 +569,7 @@ function createLiveCommandHandler(params) {
|
|
|
566
569
|
return;
|
|
567
570
|
}
|
|
568
571
|
params.debugLog(
|
|
569
|
-
`
|
|
572
|
+
`commands invoke "${message.name}" callId=${message.callId} args=${JSON.stringify(message.args ?? {}).slice(0, 200)}`
|
|
570
573
|
);
|
|
571
574
|
const abort = new AbortController();
|
|
572
575
|
const startedAt = Date.now();
|
|
@@ -576,14 +579,14 @@ function createLiveCommandHandler(params) {
|
|
|
576
579
|
const active = running.get(message.callId);
|
|
577
580
|
if (abort.signal.aborted || active?.cancelled) {
|
|
578
581
|
params.debugLog(
|
|
579
|
-
`
|
|
582
|
+
`commands invoke "${message.name}" cancelled after ${Date.now() - startedAt}ms`
|
|
580
583
|
);
|
|
581
584
|
await sendResult(buildCancelledResult(message.callId, startedAt));
|
|
582
585
|
return;
|
|
583
586
|
}
|
|
584
587
|
const durationMs = Date.now() - startedAt;
|
|
585
588
|
params.debugLog(
|
|
586
|
-
`
|
|
589
|
+
`commands invoke "${message.name}" ok=${true} duration=${durationMs}ms value=${JSON.stringify(value).slice(0, 200)}`
|
|
587
590
|
);
|
|
588
591
|
await sendResult({
|
|
589
592
|
v: COMMAND_PROTOCOL_VERSION,
|
|
@@ -600,7 +603,7 @@ function createLiveCommandHandler(params) {
|
|
|
600
603
|
}
|
|
601
604
|
const durationMs = Date.now() - startedAt;
|
|
602
605
|
params.debugLog(
|
|
603
|
-
`
|
|
606
|
+
`commands invoke "${message.name}" FAILED duration=${durationMs}ms error=${detail.slice(0, 300)}`
|
|
604
607
|
);
|
|
605
608
|
await sendResult({
|
|
606
609
|
v: COMMAND_PROTOCOL_VERSION,
|
|
@@ -623,18 +626,13 @@ function createLiveCommandHandler(params) {
|
|
|
623
626
|
async function handleBridgeMessage(message) {
|
|
624
627
|
if (message.type !== "event") return;
|
|
625
628
|
params.debugLog(
|
|
626
|
-
`
|
|
629
|
+
`commands message type=${message.type} data=${typeof message.data === "string" ? message.data.slice(0, 120) : "?"}`
|
|
627
630
|
);
|
|
628
631
|
for (const [callId, result] of recentResults) {
|
|
629
632
|
if (result.expiresAt <= Date.now()) {
|
|
630
633
|
recentResults.delete(callId);
|
|
631
634
|
}
|
|
632
635
|
}
|
|
633
|
-
const bind = parseCommandBindMessage(message);
|
|
634
|
-
if (bind) {
|
|
635
|
-
await handleBind(bind);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
636
|
const invoke = parseCommandInvokeMessage(message);
|
|
639
637
|
if (invoke) {
|
|
640
638
|
await handleInvoke(invoke);
|
|
@@ -646,11 +644,18 @@ function createLiveCommandHandler(params) {
|
|
|
646
644
|
}
|
|
647
645
|
}
|
|
648
646
|
return {
|
|
647
|
+
bindFromHtml(html) {
|
|
648
|
+
bindFromHtml(html);
|
|
649
|
+
},
|
|
650
|
+
clearBindings() {
|
|
651
|
+
clearBindings();
|
|
652
|
+
},
|
|
649
653
|
stop() {
|
|
650
654
|
for (const [callId, active] of running) {
|
|
651
655
|
active.abort.abort();
|
|
652
656
|
running.delete(callId);
|
|
653
657
|
}
|
|
658
|
+
clearBindings();
|
|
654
659
|
},
|
|
655
660
|
async onMessage(message) {
|
|
656
661
|
await handleBridgeMessage(message).catch((error) => {
|
|
@@ -752,14 +757,33 @@ function createAnswer(peer, browserOffer, timeoutMs) {
|
|
|
752
757
|
}
|
|
753
758
|
|
|
754
759
|
// src/lib/live-daemon-ipc-handler.ts
|
|
760
|
+
function unreachableIpcRequest(request) {
|
|
761
|
+
throw new Error(`Unsupported IPC request: ${JSON.stringify(request)}`);
|
|
762
|
+
}
|
|
755
763
|
function createDaemonIpcHandler(params) {
|
|
756
764
|
return async function handleIpcRequest(req) {
|
|
757
765
|
switch (req.method) {
|
|
758
766
|
case "write": {
|
|
759
767
|
const channel = req.params.channel || "chat";
|
|
768
|
+
const msg = req.params.msg;
|
|
769
|
+
if (channel === "canvas" && msg.type === "html" && typeof msg.data === "string") {
|
|
770
|
+
const slug = params.getActiveSlug();
|
|
771
|
+
if (!slug) return { ok: false, error: "No active live session." };
|
|
772
|
+
try {
|
|
773
|
+
await params.apiClient.update({
|
|
774
|
+
slug,
|
|
775
|
+
content: msg.data
|
|
776
|
+
});
|
|
777
|
+
params.bindCanvasCommands(msg.data);
|
|
778
|
+
return { ok: true, delivered: true };
|
|
779
|
+
} catch (error) {
|
|
780
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
781
|
+
params.markError(`failed to persist canvas HTML for "${slug}"`, error);
|
|
782
|
+
return { ok: false, error: `Canvas update failed: ${errMsg}` };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
760
785
|
const readinessError = params.getWriteReadinessError();
|
|
761
786
|
if (readinessError) return { ok: false, error: readinessError };
|
|
762
|
-
const msg = req.params.msg;
|
|
763
787
|
const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
|
|
764
788
|
const binaryPayload = msg.type === "binary" && binaryBase64 ? Buffer.from(binaryBase64, "base64") : void 0;
|
|
765
789
|
const maxAttempts = Math.max(1, params.writeAckMaxAttempts);
|
|
@@ -809,7 +833,6 @@ function createDaemonIpcHandler(params) {
|
|
|
809
833
|
continue;
|
|
810
834
|
}
|
|
811
835
|
}
|
|
812
|
-
params.trackOutboundMessage(channel, msg);
|
|
813
836
|
return { ok: true, delivered: true };
|
|
814
837
|
}
|
|
815
838
|
return {
|
|
@@ -855,8 +878,9 @@ function createDaemonIpcHandler(params) {
|
|
|
855
878
|
params.shutdown();
|
|
856
879
|
return { ok: true };
|
|
857
880
|
}
|
|
858
|
-
default:
|
|
859
|
-
return
|
|
881
|
+
default: {
|
|
882
|
+
return unreachableIpcRequest(req);
|
|
883
|
+
}
|
|
860
884
|
}
|
|
861
885
|
};
|
|
862
886
|
}
|
|
@@ -872,17 +896,22 @@ function createDaemonIpcServer(handler) {
|
|
|
872
896
|
if (newlineIdx === -1) return;
|
|
873
897
|
const line = data.slice(0, newlineIdx);
|
|
874
898
|
data = data.slice(newlineIdx + 1);
|
|
875
|
-
let request;
|
|
876
899
|
try {
|
|
877
|
-
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
|
+
);
|
|
878
911
|
} catch {
|
|
879
912
|
conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
|
|
880
913
|
`);
|
|
881
|
-
return;
|
|
882
914
|
}
|
|
883
|
-
handler(request).then((response) => conn.write(`${JSON.stringify(response)}
|
|
884
|
-
`)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: errorMessage(err) })}
|
|
885
|
-
`));
|
|
886
915
|
});
|
|
887
916
|
});
|
|
888
917
|
}
|
|
@@ -937,7 +966,7 @@ var CANVAS_COMMAND_PROTOCOL_GUIDE_MARKDOWN = [
|
|
|
937
966
|
|
|
938
967
|
// src/lib/live-daemon-shared.ts
|
|
939
968
|
function buildBridgeInstructions(mode) {
|
|
940
|
-
if (mode === "claude-code") {
|
|
969
|
+
if (mode === "claude-code" || mode === "claude-sdk") {
|
|
941
970
|
return {
|
|
942
971
|
replyHint: 'Reply command: pubblue write "<your reply>"',
|
|
943
972
|
canvasHint: "Canvas command: pubblue write -c canvas -f /path/to/file.html",
|
|
@@ -974,13 +1003,6 @@ function shouldRecoverForBrowserOfferChange(params) {
|
|
|
974
1003
|
if (!lastAppliedBrowserOffer) return false;
|
|
975
1004
|
return incomingBrowserOffer !== lastAppliedBrowserOffer;
|
|
976
1005
|
}
|
|
977
|
-
function readCanvasHtmlFromOutbound(params) {
|
|
978
|
-
if (params.channel !== CHANNELS.CANVAS) return null;
|
|
979
|
-
if (params.msg.type !== "html") return null;
|
|
980
|
-
if (typeof params.msg.data !== "string") return null;
|
|
981
|
-
if (params.msg.data.length === 0) return null;
|
|
982
|
-
return params.msg.data;
|
|
983
|
-
}
|
|
984
1006
|
|
|
985
1007
|
// src/lib/live-daemon-signaling.ts
|
|
986
1008
|
import { ConvexClient } from "convex/browser";
|
|
@@ -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,33 +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" || typeof live.expiresAt !== "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
|
-
expiresAt: live.expiresAt
|
|
1049
|
-
};
|
|
1054
|
+
return live;
|
|
1050
1055
|
}
|
|
1051
1056
|
function createSignalingController(params) {
|
|
1052
1057
|
const {
|
|
@@ -1060,7 +1065,8 @@ function createSignalingController(params) {
|
|
|
1060
1065
|
getLastBrowserCandidateCount,
|
|
1061
1066
|
setLastBrowserCandidateCount,
|
|
1062
1067
|
onRecover,
|
|
1063
|
-
onApplyBrowserCandidates
|
|
1068
|
+
onApplyBrowserCandidates,
|
|
1069
|
+
onClearLive
|
|
1064
1070
|
} = params;
|
|
1065
1071
|
let signalingClient = null;
|
|
1066
1072
|
let signalingUnsubscribe = null;
|
|
@@ -1103,6 +1109,10 @@ function createSignalingController(params) {
|
|
|
1103
1109
|
await onRecover(decision.slug, decision.browserOffer);
|
|
1104
1110
|
return;
|
|
1105
1111
|
}
|
|
1112
|
+
if (decision.type === "clear-live") {
|
|
1113
|
+
await onClearLive();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1106
1116
|
if (decision.type === "apply-browser-candidates") {
|
|
1107
1117
|
await onApplyBrowserCandidates(decision.candidatePayloads);
|
|
1108
1118
|
}
|
|
@@ -1203,9 +1213,6 @@ async function startDaemon(config) {
|
|
|
1203
1213
|
let pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
1204
1214
|
let inboundStreams = /* @__PURE__ */ new Map();
|
|
1205
1215
|
let seenInboundMessageKeys = /* @__PURE__ */ new Set();
|
|
1206
|
-
let lastCanvasSnapshot = null;
|
|
1207
|
-
let lastPersistedCanvasSnapshot = null;
|
|
1208
|
-
let persistCanvasQueue = Promise.resolve();
|
|
1209
1216
|
let heartbeatTimer = null;
|
|
1210
1217
|
let localCandidateInterval = null;
|
|
1211
1218
|
let localCandidateStopTimer = null;
|
|
@@ -1222,14 +1229,10 @@ async function startDaemon(config) {
|
|
|
1222
1229
|
markError,
|
|
1223
1230
|
sendCommandMessage: async (msg) => {
|
|
1224
1231
|
if (!isLiveConnected()) return false;
|
|
1225
|
-
|
|
1232
|
+
return sendOutboundMessageWithAck(CHANNELS.COMMAND, msg, {
|
|
1226
1233
|
context: 'command outbound on "command"',
|
|
1227
1234
|
maxAttempts: OUTBOUND_SEND_MAX_ATTEMPTS
|
|
1228
1235
|
});
|
|
1229
|
-
if (sent) {
|
|
1230
|
-
trackOutboundMessage(CHANNELS.COMMAND, msg);
|
|
1231
|
-
}
|
|
1232
|
-
return sent;
|
|
1233
1236
|
}
|
|
1234
1237
|
});
|
|
1235
1238
|
function debugLog(message, error) {
|
|
@@ -1318,64 +1321,23 @@ async function startDaemon(config) {
|
|
|
1318
1321
|
runHealthCheck();
|
|
1319
1322
|
}
|
|
1320
1323
|
function appendBufferedMessage(entry) {
|
|
1321
|
-
if (entry.channel === CHANNELS.
|
|
1324
|
+
if (entry.channel === CHANNELS.COMMAND) return;
|
|
1322
1325
|
buffer.messages.push(entry);
|
|
1323
1326
|
if (buffer.messages.length > MAX_BUFFERED_MESSAGES) {
|
|
1324
1327
|
buffer.messages.splice(0, buffer.messages.length - MAX_BUFFERED_MESSAGES);
|
|
1325
1328
|
}
|
|
1326
1329
|
}
|
|
1327
|
-
function getActiveCanvasSnapshot() {
|
|
1328
|
-
if (!activeSlug || !lastCanvasSnapshot) return null;
|
|
1329
|
-
if (lastCanvasSnapshot.slug !== activeSlug) return null;
|
|
1330
|
-
return lastCanvasSnapshot;
|
|
1331
|
-
}
|
|
1332
|
-
function isSameCanvasSnapshot(a, b) {
|
|
1333
|
-
if (!a || !b) return false;
|
|
1334
|
-
return a.slug === b.slug && a.html === b.html;
|
|
1335
|
-
}
|
|
1336
|
-
function queuePersistCanvasSnapshot(snapshot, reason) {
|
|
1337
|
-
if (!snapshot) return Promise.resolve();
|
|
1338
|
-
if (isSameCanvasSnapshot(lastPersistedCanvasSnapshot, snapshot)) return Promise.resolve();
|
|
1339
|
-
persistCanvasQueue = persistCanvasQueue.then(async () => {
|
|
1340
|
-
if (isSameCanvasSnapshot(lastPersistedCanvasSnapshot, snapshot)) return;
|
|
1341
|
-
try {
|
|
1342
|
-
await apiClient2.update({
|
|
1343
|
-
slug: snapshot.slug,
|
|
1344
|
-
content: snapshot.html,
|
|
1345
|
-
filename: "live-canvas.html"
|
|
1346
|
-
});
|
|
1347
|
-
lastPersistedCanvasSnapshot = snapshot;
|
|
1348
|
-
debugLog(`persisted latest canvas for "${snapshot.slug}" (${reason})`);
|
|
1349
|
-
} catch (error) {
|
|
1350
|
-
markError(`failed to persist latest canvas for "${snapshot.slug}" (${reason})`, error);
|
|
1351
|
-
if (!debugEnabled) {
|
|
1352
|
-
console.error(
|
|
1353
|
-
`[pubblue-agent] failed to persist latest canvas for "${snapshot.slug}" (${reason}): ${errorMessage(error)}`
|
|
1354
|
-
);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
});
|
|
1358
|
-
return persistCanvasQueue;
|
|
1359
|
-
}
|
|
1360
|
-
function trackOutboundMessage(channel, msg) {
|
|
1361
|
-
const html = readCanvasHtmlFromOutbound({ channel, msg });
|
|
1362
|
-
if (!html || !activeSlug) return;
|
|
1363
|
-
lastCanvasSnapshot = { slug: activeSlug, html };
|
|
1364
|
-
}
|
|
1365
1330
|
function handleConnectionClosed(reason) {
|
|
1366
|
-
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
buffer.messages = [];
|
|
1377
|
-
failPendingAcks();
|
|
1378
|
-
stopPingPong();
|
|
1331
|
+
debugLog(`connection closed: ${reason}`);
|
|
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
|
+
});
|
|
1379
1341
|
}
|
|
1380
1342
|
function emitDeliveryStatus(params) {
|
|
1381
1343
|
if (!params.messageId || params.channel === CONTROL_CHANNEL) return;
|
|
@@ -1599,7 +1561,7 @@ async function startDaemon(config) {
|
|
|
1599
1561
|
if (!peer) throw new Error("PeerConnection not initialized");
|
|
1600
1562
|
const existing = channels.get(name);
|
|
1601
1563
|
if (existing) return existing;
|
|
1602
|
-
const dc = peer.createDataChannel(name,
|
|
1564
|
+
const dc = peer.createDataChannel(name, ORDERED_DATA_CHANNEL_OPTIONS);
|
|
1603
1565
|
setupChannel(name, dc);
|
|
1604
1566
|
return dc;
|
|
1605
1567
|
}
|
|
@@ -1692,7 +1654,7 @@ async function startDaemon(config) {
|
|
|
1692
1654
|
}
|
|
1693
1655
|
function createPeer() {
|
|
1694
1656
|
const nextPeer = new ndc.PeerConnection("agent", {
|
|
1695
|
-
iceServers: [
|
|
1657
|
+
iceServers: [...WEBRTC_STUN_URLS]
|
|
1696
1658
|
});
|
|
1697
1659
|
peer = nextPeer;
|
|
1698
1660
|
channels = /* @__PURE__ */ new Map();
|
|
@@ -1738,6 +1700,15 @@ async function startDaemon(config) {
|
|
|
1738
1700
|
inboundStreams.clear();
|
|
1739
1701
|
seenInboundMessageKeys.clear();
|
|
1740
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
|
+
}
|
|
1741
1712
|
function startLocalCandidateFlush(slug) {
|
|
1742
1713
|
clearLocalCandidateTimers();
|
|
1743
1714
|
localCandidateInterval = setInterval(async () => {
|
|
@@ -1756,14 +1727,8 @@ async function startDaemon(config) {
|
|
|
1756
1727
|
if (recovering) return;
|
|
1757
1728
|
recovering = true;
|
|
1758
1729
|
try {
|
|
1759
|
-
|
|
1760
|
-
if (previousCanvasSnapshot && previousCanvasSnapshot.slug !== slug) {
|
|
1761
|
-
void queuePersistCanvasSnapshot(previousCanvasSnapshot, `session-switch:${slug}`);
|
|
1762
|
-
}
|
|
1763
|
-
await stopBridge();
|
|
1764
|
-
closeCurrentPeer();
|
|
1730
|
+
await clearActiveLiveSession("incoming-live-recovery");
|
|
1765
1731
|
createPeer();
|
|
1766
|
-
resetNegotiationState();
|
|
1767
1732
|
if (!peer) throw new Error("PeerConnection not initialized");
|
|
1768
1733
|
const answer = await createAnswer(peer, browserOffer, OFFER_TIMEOUT_MS);
|
|
1769
1734
|
lastAppliedBrowserOffer = browserOffer;
|
|
@@ -1802,7 +1767,10 @@ async function startDaemon(config) {
|
|
|
1802
1767
|
lastBrowserCandidateCount = count;
|
|
1803
1768
|
},
|
|
1804
1769
|
onRecover: handleIncomingLive,
|
|
1805
|
-
onApplyBrowserCandidates: applyBrowserCandidates
|
|
1770
|
+
onApplyBrowserCandidates: applyBrowserCandidates,
|
|
1771
|
+
onClearLive: async () => {
|
|
1772
|
+
await clearActiveLiveSession("signaling-cleared");
|
|
1773
|
+
}
|
|
1806
1774
|
});
|
|
1807
1775
|
if (fs.existsSync(socketPath2)) {
|
|
1808
1776
|
let stale = true;
|
|
@@ -1834,6 +1802,8 @@ async function startDaemon(config) {
|
|
|
1834
1802
|
}
|
|
1835
1803
|
}, HEARTBEAT_INTERVAL_MS);
|
|
1836
1804
|
const handleIpcRequest = createDaemonIpcHandler({
|
|
1805
|
+
apiClient: apiClient2,
|
|
1806
|
+
bindCanvasCommands: (html) => commandHandler.bindFromHtml(html),
|
|
1837
1807
|
getConnected: () => isLiveConnected(),
|
|
1838
1808
|
getSignalingConnected: () => {
|
|
1839
1809
|
const state = signaling.status();
|
|
@@ -1854,7 +1824,6 @@ async function startDaemon(config) {
|
|
|
1854
1824
|
waitForChannelOpen,
|
|
1855
1825
|
waitForDeliveryAck,
|
|
1856
1826
|
settlePendingAck,
|
|
1857
|
-
trackOutboundMessage,
|
|
1858
1827
|
markError,
|
|
1859
1828
|
shutdown: () => {
|
|
1860
1829
|
void shutdown();
|
|
@@ -1874,28 +1843,24 @@ async function startDaemon(config) {
|
|
|
1874
1843
|
signaling.start();
|
|
1875
1844
|
async function sendOnChannel(channel, msg) {
|
|
1876
1845
|
if (stopped || !isLiveConnected()) return false;
|
|
1877
|
-
|
|
1846
|
+
return sendOutboundMessageWithAck(channel, msg, {
|
|
1878
1847
|
context: `bridge outbound on "${channel}"`,
|
|
1879
1848
|
maxAttempts: OUTBOUND_SEND_MAX_ATTEMPTS
|
|
1880
1849
|
});
|
|
1881
|
-
if (sent) {
|
|
1882
|
-
trackOutboundMessage(channel, msg);
|
|
1883
|
-
}
|
|
1884
|
-
return sent;
|
|
1885
1850
|
}
|
|
1886
1851
|
async function buildInitialSessionBriefing(params) {
|
|
1887
1852
|
const pub = await apiClient2.get(params.slug);
|
|
1888
1853
|
const content = typeof pub.content === "string" ? pub.content : "";
|
|
1854
|
+
if (content.length > 0) commandHandler.bindFromHtml(content);
|
|
1855
|
+
else commandHandler.clearBindings();
|
|
1889
1856
|
const canvasContentFilePath = content.length > 0 ? writeLiveSessionContentFile({
|
|
1890
1857
|
slug: params.slug,
|
|
1891
|
-
contentType: pub.contentType,
|
|
1892
1858
|
content
|
|
1893
1859
|
}) : void 0;
|
|
1894
1860
|
return buildSessionBriefing(
|
|
1895
1861
|
params.slug,
|
|
1896
1862
|
{
|
|
1897
1863
|
title: pub.title,
|
|
1898
|
-
contentType: pub.contentType,
|
|
1899
1864
|
isPublic: pub.isPublic,
|
|
1900
1865
|
canvasContentFilePath
|
|
1901
1866
|
},
|
|
@@ -1928,7 +1893,7 @@ async function startDaemon(config) {
|
|
|
1928
1893
|
debugLog,
|
|
1929
1894
|
instructions
|
|
1930
1895
|
};
|
|
1931
|
-
const runner = config.bridgeMode === "claude-code" ? await createClaudeCodeBridgeRunner(bridgeConfig, abort.signal) : await createOpenClawBridgeRunner(bridgeConfig);
|
|
1896
|
+
const runner = config.bridgeMode === "claude-sdk" ? await createClaudeSdkBridgeRunner(bridgeConfig, abort.signal) : config.bridgeMode === "claude-code" ? await createClaudeCodeBridgeRunner(bridgeConfig, abort.signal) : await createOpenClawBridgeRunner(bridgeConfig);
|
|
1932
1897
|
if (stopped || activeSlug !== slug || abort.signal.aborted) {
|
|
1933
1898
|
await runner.stop();
|
|
1934
1899
|
return;
|
|
@@ -1974,7 +1939,6 @@ async function startDaemon(config) {
|
|
|
1974
1939
|
clearHeartbeatTimer();
|
|
1975
1940
|
stopPingPong();
|
|
1976
1941
|
await signaling.stop();
|
|
1977
|
-
await queuePersistCanvasSnapshot(getActiveCanvasSnapshot(), "daemon-shutdown");
|
|
1978
1942
|
try {
|
|
1979
1943
|
await apiClient2.goOffline({ daemonSessionId });
|
|
1980
1944
|
} catch (error) {
|