local-operator-ui 0.20.1 → 0.21.0
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/out/main/index.js +426 -343
- package/out/preload/index.js +10 -0
- package/out/renderer/assets/{_basePickBy-C7yOlBwG.js → _basePickBy-Du3cfBqe.js} +1 -1
- package/out/renderer/assets/{_baseUniq-BxWDXUxR.js → _baseUniq-Dq4vzytk.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DGusXpiv.js → agent-details-page-BBT_i4U4.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-BEy-xjst.js → agent-hub-page-D8gesCRj.js} +1 -1
- package/out/renderer/assets/{agents-page-VfOUQ-yP.js → agents-page-B2NnQfhM.js} +2 -2
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-358ZDLa0.js → architectureDiagram-IEHRJDOE-CS_3oayq.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-tzEHVS4j.js → blockDiagram-JOT3LUYC-BNDbdGzn.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-D3tP-CCo.js → c4Diagram-VJAJSXHY-5HDa_Dur.js} +1 -1
- package/out/renderer/assets/channel-nFiBFvkY.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-BKUSTR_S.js → chunk-4BMEZGHF-me2GzChw.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-DvXPJSTp.js → chunk-A2AXSNBT-Q-KKGGGm.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-iJyV8fDW.js → chunk-AEK57VVT-BAp_jb_D.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-B3DDJ3MU.js → chunk-D6G4REZN-DR8iAD8Y.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-Dbn_07ZJ.js → chunk-RZ5BOZE2-42LQGahH.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-ChdMxZnm.js → chunk-XZIHB7SX-CRdJ4v_1.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-BmZ3ZDSH.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-BmZ3ZDSH.js +1 -0
- package/out/renderer/assets/clone-BsKeRQ9y.js +1 -0
- package/out/renderer/assets/{compact-pagination-duvMYksr.js → compact-pagination-Ke-mkrto.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-BuSM1V0w.js → dagre-OKDRZEBW-Dj-_-lbb.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-39xt9Mcn.js → diagram-SSKATNLV-KAsvPn06.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-Bgyt_DWD.js → diagram-VNBRO52H-BLSKSGv7.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-DNfsSD46.js → erDiagram-Q7BY3M3F-B1uo8Ttt.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-DbS8UHwN.js → flowDiagram-4HSFHLVR-D89Q6QeY.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-Dxdp2V-6.js → ganttDiagram-APWFNJXF-DYEIQner.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-BLrXJPe3.js → gitGraphDiagram-7IBYFJ6S-DNFcQAXC.js} +1 -1
- package/out/renderer/assets/{graph-Cgkq5xQ7.js → graph-DPB5A4Er.js} +1 -1
- package/out/renderer/assets/icon-GWZ2Pe4t.css +1 -0
- package/out/renderer/assets/{index-C4RcIL0i.js → index-D7h0cbc3.js} +1 -1
- package/out/renderer/assets/{index-CLwRuOtJ.js → index-Dsvvzi5i.js} +308 -306
- package/out/renderer/assets/{index-lEoaSpWQ.js → index-OkokcYbT.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-DR6So_J8.js → infoDiagram-PH2N3AL5-CjDiLpkp.js} +1 -1
- package/out/renderer/assets/{installer-Ck14_4Oh.js → installer-BR2Mx27S.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-DubpieGX.js → journeyDiagram-U35MCT3I-2AwP5v9s.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BtwsU2Rg.js → kanban-definition-NDS4AKOZ-CHOQal41.js} +1 -1
- package/out/renderer/assets/{layout-BDeRPl4h.js → layout-PzJFXrpY.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-ZyBMetf1.js → legacy-agents-page-DS9cJ5Ch.js} +1 -1
- package/out/renderer/assets/{mermaid.core-d-O-_a6F.js → mermaid.core-ObZGxTfv.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-BCZdiKEB.js → mindmap-definition-ALO5MXBD-CJVu4Cq9.js} +1 -1
- package/out/renderer/assets/{page-header-D-uc8qqA.js → page-header-MHBoBUg-.js} +1 -1
- package/out/renderer/assets/{parseISO-PdZfrqAM.js → parseISO-BCz-yMl-.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-Bd2e7gHe.js → pieDiagram-IB7DONF6-FNr0Rin7.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-oypZsih9.js → quadrantDiagram-7GDLP6J5-ZBae0crI.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-c4YagPOX.js → radar-MK3ICKWK-QkpqLslW.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-BUlY7XDi.js → radient-auth-buttons-cVPSsmip.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-7UoAwEus.js → requirementDiagram-KVF5MWMF-BTkK1lFT.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-JStPMuRd.js → sankeyDiagram-QLVOVGJD-Bavn2Nby.js} +1 -1
- package/out/renderer/assets/{schedules-page-IjBtVuIo.js → schedules-page-DnPP_ud3.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-Lene9gQ-.js → sequenceDiagram-X6HHIX6F-CKHtEkHH.js} +1 -1
- package/out/renderer/assets/{settings-page-BqJRfztd.js → settings-page-B3RdcR4e.js} +1 -1
- package/out/renderer/assets/{square-pen-CQEKS3JT.js → square-pen-DJCejD2T.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-umC2tGUK.js → stateDiagram-DGXRK772-DC4AGn0I.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Cn7HubUo.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-h2KfXqek.js → timeline-definition-BDJGKUSR-Crx7z7bt.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-BanZ02f7.js → use-agent-like-mutation-DwCdN3lD.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-BOH5ZQcH.js → xychartDiagram-VJFVF3MP-CY5IW4O0.js} +1 -1
- package/out/renderer/index.html +3 -3
- package/out/renderer/installer.html +3 -3
- package/package.json +2 -2
- package/out/renderer/assets/channel-D37fF6my.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CcnNtrPC.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CcnNtrPC.js +0 -1
- package/out/renderer/assets/clone-DPqLvbSr.js +0 -1
- package/out/renderer/assets/icon-HSrrv8O-.css +0 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Cp1tcztr.js +0 -1
- /package/out/renderer/assets/{icon-RIkwUxL5.js → icon-Wjb6dt9o.js} +0 -0
package/out/main/index.js
CHANGED
|
@@ -6,11 +6,11 @@ const utils = require("@electron-toolkit/utils");
|
|
|
6
6
|
const electron = require("electron");
|
|
7
7
|
const posthogNode = require("posthog-node");
|
|
8
8
|
const path = require("path");
|
|
9
|
+
const zod = require("zod");
|
|
9
10
|
const node_child_process = require("node:child_process");
|
|
10
11
|
const node_crypto = require("node:crypto");
|
|
11
12
|
const os = require("node:os");
|
|
12
13
|
const node_util = require("node:util");
|
|
13
|
-
const zod = require("zod");
|
|
14
14
|
const dotenv = require("dotenv");
|
|
15
15
|
const electronLog = require("electron-log");
|
|
16
16
|
const https = require("node:https");
|
|
@@ -35,291 +35,6 @@ const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path$1);
|
|
|
35
35
|
const https__namespace = /* @__PURE__ */ _interopNamespaceDefault(https);
|
|
36
36
|
const icon = path.join(__dirname, "../../resources/icon.png");
|
|
37
37
|
const id$1 = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
38
|
-
const MAX_UPLOAD_BYTES = 16 * 1024 * 1024;
|
|
39
|
-
const MAX_DOWNLOAD_BYTES = 32 * 1024 * 1024;
|
|
40
|
-
const mediaRequestSchema = zod.z.discriminatedUnion("op", [
|
|
41
|
-
zod.z.object({
|
|
42
|
-
op: zod.z.literal("speech.create"),
|
|
43
|
-
request: zod.z.record(zod.z.unknown())
|
|
44
|
-
}).strict(),
|
|
45
|
-
zod.z.object({
|
|
46
|
-
op: zod.z.literal("speech.agent"),
|
|
47
|
-
agentId: id$1,
|
|
48
|
-
request: zod.z.record(zod.z.unknown())
|
|
49
|
-
}).strict(),
|
|
50
|
-
zod.z.object({
|
|
51
|
-
op: zod.z.literal("transcription.create"),
|
|
52
|
-
fileName: zod.z.string().min(1).max(255),
|
|
53
|
-
mimeType: zod.z.string().min(1).max(128),
|
|
54
|
-
fields: zod.z.record(zod.z.string().max(4096))
|
|
55
|
-
}).strict(),
|
|
56
|
-
zod.z.object({
|
|
57
|
-
op: zod.z.literal("agent.import"),
|
|
58
|
-
fileName: zod.z.string().min(1).max(255)
|
|
59
|
-
}).strict(),
|
|
60
|
-
// Export is a gated GET that answers with a ZIP, so it belongs on this relay
|
|
61
|
-
// rather than the JSON transport: `desktopEndpoint`'s envelope has nowhere to
|
|
62
|
-
// put binary. It is the only read here, which is why `endpoint()` below
|
|
63
|
-
// returns a method instead of assuming POST.
|
|
64
|
-
zod.z.object({ op: zod.z.literal("agent.export"), agentId: id$1 }).strict(),
|
|
65
|
-
// A durable transcript row references an image by content digest with the
|
|
66
|
-
// payload stripped, so rendering a screenshot after a reload means fetching
|
|
67
|
-
// bytes. That cannot go through the JSON transport (its envelope has nowhere
|
|
68
|
-
// to put them), which is what puts it on this relay.
|
|
69
|
-
//
|
|
70
|
-
// Both identifiers are shape-constrained here as well as by the backend
|
|
71
|
-
// route: this file's whole premise is that renderer code cannot pick a URL,
|
|
72
|
-
// and a digest that reaches `endpoint()` unvalidated is renderer-controlled
|
|
73
|
-
// path text.
|
|
74
|
-
zod.z.object({
|
|
75
|
-
op: zod.z.literal("sessions.attachment"),
|
|
76
|
-
sessionId: zod.z.string().regex(/^[a-f0-9]{12}$/),
|
|
77
|
-
digest: zod.z.string().regex(/^[a-f0-9]{32}$/)
|
|
78
|
-
}).strict()
|
|
79
|
-
]);
|
|
80
|
-
function endpoint(request) {
|
|
81
|
-
switch (request.op) {
|
|
82
|
-
case "speech.create":
|
|
83
|
-
return { path: "/v1/tools/speech", method: "POST" };
|
|
84
|
-
case "speech.agent":
|
|
85
|
-
return { path: `/v1/agents/${request.agentId}/speech`, method: "POST" };
|
|
86
|
-
case "transcription.create":
|
|
87
|
-
return { path: "/v1/transcriptions", method: "POST" };
|
|
88
|
-
case "agent.import":
|
|
89
|
-
return { path: "/v1/agents/import", method: "POST" };
|
|
90
|
-
case "agent.export":
|
|
91
|
-
return { path: `/v1/agents/${request.agentId}/export`, method: "GET" };
|
|
92
|
-
case "sessions.attachment":
|
|
93
|
-
return {
|
|
94
|
-
path: `/v1/desktop/sessions/${request.sessionId}/attachments/${request.digest}`,
|
|
95
|
-
method: "GET"
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
async function requestDesktopMedia(input, bytes, backendUrl, token) {
|
|
100
|
-
const parsed = mediaRequestSchema.safeParse(input);
|
|
101
|
-
if (!parsed.success) {
|
|
102
|
-
return { status: 422, kind: "error", detail: "Invalid media operation." };
|
|
103
|
-
}
|
|
104
|
-
if (!token) {
|
|
105
|
-
return {
|
|
106
|
-
status: 503,
|
|
107
|
-
kind: "error",
|
|
108
|
-
detail: "Restart with a desktop-managed backend to use these controls."
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const request = parsed.data;
|
|
112
|
-
if (bytes && bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
113
|
-
return { status: 413, kind: "error", detail: "This file is too large." };
|
|
114
|
-
}
|
|
115
|
-
const target2 = endpoint(request);
|
|
116
|
-
let body;
|
|
117
|
-
let contentType;
|
|
118
|
-
if (request.op === "agent.export" || request.op === "sessions.attachment") {
|
|
119
|
-
body = void 0;
|
|
120
|
-
} else if (request.op === "speech.create" || request.op === "speech.agent") {
|
|
121
|
-
body = JSON.stringify(request.request);
|
|
122
|
-
contentType = "application/json";
|
|
123
|
-
} else {
|
|
124
|
-
if (!bytes) {
|
|
125
|
-
return { status: 422, kind: "error", detail: "A file is required." };
|
|
126
|
-
}
|
|
127
|
-
const form = new FormData();
|
|
128
|
-
const blob = new Blob([bytes], {
|
|
129
|
-
type: request.op === "transcription.create" ? request.mimeType : "application/zip"
|
|
130
|
-
});
|
|
131
|
-
form.append("file", blob, request.fileName);
|
|
132
|
-
if (request.op === "transcription.create") {
|
|
133
|
-
for (const [key, value] of Object.entries(request.fields)) {
|
|
134
|
-
form.append(key, value);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
body = form;
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
const response = await fetch(new URL(target2.path, backendUrl), {
|
|
141
|
-
method: target2.method,
|
|
142
|
-
headers: {
|
|
143
|
-
Accept: "application/json, audio/*, image/*, application/octet-stream",
|
|
144
|
-
...contentType ? { "Content-Type": contentType } : {},
|
|
145
|
-
Authorization: `Bearer ${token}`
|
|
146
|
-
},
|
|
147
|
-
body,
|
|
148
|
-
redirect: "error",
|
|
149
|
-
signal: AbortSignal.timeout(12e4)
|
|
150
|
-
});
|
|
151
|
-
const responseType = response.headers.get("content-type") ?? "";
|
|
152
|
-
if (!response.ok) {
|
|
153
|
-
let detail = "The media request failed.";
|
|
154
|
-
if (responseType.includes("application/json")) {
|
|
155
|
-
try {
|
|
156
|
-
const parsedError = await response.json();
|
|
157
|
-
const text = parsedError.detail ?? parsedError.message;
|
|
158
|
-
if (typeof text === "string") detail = text.slice(0, 512);
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return { status: response.status, kind: "error", detail };
|
|
163
|
-
}
|
|
164
|
-
if (responseType.includes("application/json")) {
|
|
165
|
-
return {
|
|
166
|
-
status: response.status,
|
|
167
|
-
kind: "json",
|
|
168
|
-
body: await response.json()
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
172
|
-
if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {
|
|
173
|
-
return {
|
|
174
|
-
status: 502,
|
|
175
|
-
kind: "error",
|
|
176
|
-
detail: "The backend returned too much data."
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
status: response.status,
|
|
181
|
-
kind: "bytes",
|
|
182
|
-
mimeType: responseType || "application/octet-stream",
|
|
183
|
-
data: buffer
|
|
184
|
-
};
|
|
185
|
-
} catch {
|
|
186
|
-
return {
|
|
187
|
-
status: 503,
|
|
188
|
-
kind: "error",
|
|
189
|
-
detail: "The backend could not complete this request. Check its connection and try again."
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const SESSION_ID = /^[a-f0-9]{12}$/;
|
|
194
|
-
const SAFE_ID = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
195
|
-
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
196
|
-
class DesktopStreamRelay {
|
|
197
|
-
constructor(backendUrl, token) {
|
|
198
|
-
this.backendUrl = backendUrl;
|
|
199
|
-
this.token = token;
|
|
200
|
-
}
|
|
201
|
-
streams = /* @__PURE__ */ new Map();
|
|
202
|
-
/** Frame observer for main-owned side effects (notifications). It sees
|
|
203
|
-
* every parsed frame before the renderer does, on the same subscription. */
|
|
204
|
-
observer = null;
|
|
205
|
-
observe(observer) {
|
|
206
|
-
this.observer = observer;
|
|
207
|
-
}
|
|
208
|
-
/** True when this relay can authenticate; a false answer is why the UI
|
|
209
|
-
* shows the pairing/setup state rather than a dead transcript. */
|
|
210
|
-
get available() {
|
|
211
|
-
return Boolean(this.token);
|
|
212
|
-
}
|
|
213
|
-
subscribe(args, emit) {
|
|
214
|
-
if (!SESSION_ID.test(args.sessionId)) {
|
|
215
|
-
throw new Error("Invalid session for streaming.");
|
|
216
|
-
}
|
|
217
|
-
if (args.epoch !== void 0 && !SAFE_ID.test(args.epoch)) {
|
|
218
|
-
throw new Error("Invalid stream epoch.");
|
|
219
|
-
}
|
|
220
|
-
if (args.afterSeq !== void 0 && (!Number.isInteger(args.afterSeq) || args.afterSeq < 0)) {
|
|
221
|
-
throw new Error("Invalid stream cursor.");
|
|
222
|
-
}
|
|
223
|
-
if (!this.token) {
|
|
224
|
-
throw new Error("This backend was not started with desktop controls.");
|
|
225
|
-
}
|
|
226
|
-
const streamId = node_crypto.randomBytes(16).toString("hex");
|
|
227
|
-
const controller = new AbortController();
|
|
228
|
-
this.streams.set(streamId, controller);
|
|
229
|
-
const query = new URLSearchParams();
|
|
230
|
-
if (args.epoch) query.set("epoch", args.epoch);
|
|
231
|
-
if (args.afterSeq !== void 0)
|
|
232
|
-
query.set("after_seq", String(args.afterSeq));
|
|
233
|
-
const suffix = query.size > 0 ? `?${query}` : "";
|
|
234
|
-
const url = new URL(
|
|
235
|
-
`/v1/desktop/sessions/${args.sessionId}/events${suffix}`,
|
|
236
|
-
this.backendUrl
|
|
237
|
-
);
|
|
238
|
-
void this.pump(
|
|
239
|
-
streamId,
|
|
240
|
-
args.sessionId,
|
|
241
|
-
url,
|
|
242
|
-
controller.signal,
|
|
243
|
-
emit
|
|
244
|
-
).finally(() => {
|
|
245
|
-
this.streams.delete(streamId);
|
|
246
|
-
});
|
|
247
|
-
return { streamId };
|
|
248
|
-
}
|
|
249
|
-
async pump(streamId, sessionId2, url, signal, emit) {
|
|
250
|
-
try {
|
|
251
|
-
const response = await fetch(url, {
|
|
252
|
-
headers: {
|
|
253
|
-
Accept: "text/event-stream",
|
|
254
|
-
Authorization: `Bearer ${this.token}`
|
|
255
|
-
},
|
|
256
|
-
redirect: "error",
|
|
257
|
-
signal
|
|
258
|
-
});
|
|
259
|
-
if (!response.ok || !response.body) {
|
|
260
|
-
emit({
|
|
261
|
-
streamId,
|
|
262
|
-
kind: "error",
|
|
263
|
-
detail: `The event stream was refused (${response.status}).`
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const reader = response.body.getReader();
|
|
268
|
-
const decoder = new TextDecoder();
|
|
269
|
-
let buffer = "";
|
|
270
|
-
for (; ; ) {
|
|
271
|
-
const { done, value } = await reader.read();
|
|
272
|
-
if (done) break;
|
|
273
|
-
buffer += decoder.decode(value, { stream: true });
|
|
274
|
-
for (; ; ) {
|
|
275
|
-
const boundary = buffer.indexOf("\n\n");
|
|
276
|
-
if (boundary < 0) {
|
|
277
|
-
if (buffer.length > MAX_FRAME_BYTES) {
|
|
278
|
-
emit({
|
|
279
|
-
streamId,
|
|
280
|
-
kind: "error",
|
|
281
|
-
detail: "The event stream exceeded its frame budget."
|
|
282
|
-
});
|
|
283
|
-
reader.cancel().catch(() => void 0);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
const record = buffer.slice(0, boundary);
|
|
289
|
-
buffer = buffer.slice(boundary + 2);
|
|
290
|
-
for (const line of record.split("\n")) {
|
|
291
|
-
if (line.startsWith("data:")) {
|
|
292
|
-
const data = line.slice(5).replace(/^ /, "");
|
|
293
|
-
this.observer?.(sessionId2, data);
|
|
294
|
-
emit({ streamId, kind: "data", data });
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
emit({ streamId, kind: "end" });
|
|
300
|
-
} catch (error) {
|
|
301
|
-
if (signal.aborted) {
|
|
302
|
-
emit({ streamId, kind: "end" });
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
emit({
|
|
306
|
-
streamId,
|
|
307
|
-
kind: "error",
|
|
308
|
-
detail: error instanceof Error && error.name === "AbortError" ? "The event stream ended." : "The event stream connection failed."
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
unsubscribe(streamId) {
|
|
313
|
-
if (!/^[a-f0-9]{32}$/.test(streamId)) return;
|
|
314
|
-
this.streams.get(streamId)?.abort();
|
|
315
|
-
this.streams.delete(streamId);
|
|
316
|
-
}
|
|
317
|
-
dispose() {
|
|
318
|
-
for (const controller of this.streams.values()) controller.abort();
|
|
319
|
-
this.streams.clear();
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const id = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
323
38
|
const settingKey = zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/);
|
|
324
39
|
const secret = zod.z.string().min(1).max(32768);
|
|
325
40
|
const sessionId = zod.z.string().regex(/^[a-f0-9]{12}$/);
|
|
@@ -422,7 +137,7 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
422
137
|
zod.z.object({
|
|
423
138
|
op: zod.z.literal("sessions.history"),
|
|
424
139
|
sessionId,
|
|
425
|
-
beforeId: id.optional(),
|
|
140
|
+
beforeId: id$1.optional(),
|
|
426
141
|
limit: zod.z.number().int().min(1).max(500).optional()
|
|
427
142
|
}).strict(),
|
|
428
143
|
zod.z.object({
|
|
@@ -444,8 +159,8 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
444
159
|
zod.z.object({
|
|
445
160
|
op: zod.z.literal("sessions.answer"),
|
|
446
161
|
sessionId,
|
|
447
|
-
epoch: id,
|
|
448
|
-
requestId: id,
|
|
162
|
+
epoch: id$1,
|
|
163
|
+
requestId: id$1,
|
|
449
164
|
value: zod.z.string().max(32768).optional(),
|
|
450
165
|
approved: zod.z.boolean().optional(),
|
|
451
166
|
questionIndex: zod.z.number().int().min(0).optional()
|
|
@@ -494,8 +209,8 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
494
209
|
zod.z.object({ op: zod.z.literal("legacy.models.providers") }).strict(),
|
|
495
210
|
// Reachability is checked only when the user asks for it: a probe is a real
|
|
496
211
|
// network round trip, and nothing behind first paint may make one.
|
|
497
|
-
zod.z.object({ op: zod.z.literal("auth.probe"), provider: id }).strict(),
|
|
498
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.upload"), agentId: id }).strict(),
|
|
212
|
+
zod.z.object({ op: zod.z.literal("auth.probe"), provider: id$1 }).strict(),
|
|
213
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.upload"), agentId: id$1 }).strict(),
|
|
499
214
|
zod.z.object({
|
|
500
215
|
op: zod.z.literal("legacy.agents.list"),
|
|
501
216
|
page: zod.z.number().int().min(1).optional(),
|
|
@@ -504,19 +219,19 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
504
219
|
sort: zod.z.string().max(64).optional(),
|
|
505
220
|
direction: zod.z.enum(["asc", "desc"]).optional()
|
|
506
221
|
}).strict(),
|
|
507
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.get"), agentId: id }).strict(),
|
|
222
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.get"), agentId: id$1 }).strict(),
|
|
508
223
|
zod.z.object({
|
|
509
224
|
op: zod.z.literal("legacy.agent.history"),
|
|
510
|
-
agentId: id,
|
|
225
|
+
agentId: id$1,
|
|
511
226
|
page: zod.z.number().int().min(1).optional(),
|
|
512
227
|
perPage: zod.z.number().int().min(1).max(500).optional()
|
|
513
228
|
}).strict(),
|
|
514
229
|
zod.z.object({
|
|
515
230
|
op: zod.z.literal("legacy.jobs.list"),
|
|
516
|
-
agentId: id.optional(),
|
|
231
|
+
agentId: id$1.optional(),
|
|
517
232
|
status: zod.z.string().max(64).optional()
|
|
518
233
|
}).strict(),
|
|
519
|
-
zod.z.object({ op: zod.z.literal("legacy.job.get"), jobId: id }).strict(),
|
|
234
|
+
zod.z.object({ op: zod.z.literal("legacy.job.get"), jobId: id$1 }).strict(),
|
|
520
235
|
// The schedules surface. Gated in managed mode not because a schedule is
|
|
521
236
|
// sensitive to READ but because both writes hand their `prompt` to the
|
|
522
237
|
// scheduler, which later runs it as the user's own agent -- so a bare
|
|
@@ -528,13 +243,13 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
528
243
|
}).strict(),
|
|
529
244
|
zod.z.object({
|
|
530
245
|
op: zod.z.literal("legacy.agent.schedules.list"),
|
|
531
|
-
agentId: id,
|
|
246
|
+
agentId: id$1,
|
|
532
247
|
page: zod.z.number().int().min(1).optional(),
|
|
533
248
|
perPage: zod.z.number().int().min(1).max(100).optional()
|
|
534
249
|
}).strict(),
|
|
535
250
|
zod.z.object({
|
|
536
251
|
op: zod.z.literal("legacy.agent.schedule.create"),
|
|
537
|
-
agentId: id,
|
|
252
|
+
agentId: id$1,
|
|
538
253
|
// Passed through as the request body. Declared field-by-field rather
|
|
539
254
|
// than as a passthrough object so the renderer cannot smuggle keys the
|
|
540
255
|
// backend model would silently accept.
|
|
@@ -544,10 +259,10 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
544
259
|
unit: scheduleUnit
|
|
545
260
|
})
|
|
546
261
|
}).strict(),
|
|
547
|
-
zod.z.object({ op: zod.z.literal("legacy.schedule.get"), scheduleId: id }).strict(),
|
|
262
|
+
zod.z.object({ op: zod.z.literal("legacy.schedule.get"), scheduleId: id$1 }).strict(),
|
|
548
263
|
zod.z.object({
|
|
549
264
|
op: zod.z.literal("legacy.schedule.edit"),
|
|
550
|
-
scheduleId: id,
|
|
265
|
+
scheduleId: id$1,
|
|
551
266
|
// Every field optional: the backend applies `exclude_unset`, so sending
|
|
552
267
|
// a key the user did not touch would overwrite it with a default.
|
|
553
268
|
schedule: scheduleWrite.extend({
|
|
@@ -556,7 +271,7 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
556
271
|
unit: scheduleUnit.nullish()
|
|
557
272
|
})
|
|
558
273
|
}).strict(),
|
|
559
|
-
zod.z.object({ op: zod.z.literal("legacy.schedule.remove"), scheduleId: id }).strict(),
|
|
274
|
+
zod.z.object({ op: zod.z.literal("legacy.schedule.remove"), scheduleId: id$1 }).strict(),
|
|
560
275
|
// The remaining gated legacy writes and reads the renderer still issued as
|
|
561
276
|
// bare same-origin `fetch`. `apiConfig.baseUrl` points AT THE BACKEND, not
|
|
562
277
|
// through the main-process relay, so in managed mode these went out with no
|
|
@@ -572,52 +287,52 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
572
287
|
}).strict(),
|
|
573
288
|
zod.z.object({
|
|
574
289
|
op: zod.z.literal("legacy.agent.update"),
|
|
575
|
-
agentId: id,
|
|
290
|
+
agentId: id$1,
|
|
576
291
|
update: zod.z.record(zod.z.unknown())
|
|
577
292
|
}).strict(),
|
|
578
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.delete"), agentId: id }).strict(),
|
|
579
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.conversation.clear"), agentId: id }).strict(),
|
|
580
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.systemPrompt.get"), agentId: id }).strict(),
|
|
293
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.delete"), agentId: id$1 }).strict(),
|
|
294
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.conversation.clear"), agentId: id$1 }).strict(),
|
|
295
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.systemPrompt.get"), agentId: id$1 }).strict(),
|
|
581
296
|
zod.z.object({
|
|
582
297
|
op: zod.z.literal("legacy.agent.systemPrompt.update"),
|
|
583
|
-
agentId: id,
|
|
298
|
+
agentId: id$1,
|
|
584
299
|
systemPrompt: zod.z.string().max(DESKTOP_SYSTEM_PROMPT_MAX_CHARS)
|
|
585
300
|
}).strict(),
|
|
586
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.download"), agentId: id }).strict(),
|
|
587
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.variables.list"), agentId: id }).strict(),
|
|
301
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.download"), agentId: id$1 }).strict(),
|
|
302
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.variables.list"), agentId: id$1 }).strict(),
|
|
588
303
|
zod.z.object({
|
|
589
304
|
op: zod.z.literal("legacy.agent.variables.create"),
|
|
590
|
-
agentId: id,
|
|
305
|
+
agentId: id$1,
|
|
591
306
|
variable: zod.z.record(zod.z.unknown())
|
|
592
307
|
}).strict(),
|
|
593
308
|
zod.z.object({
|
|
594
309
|
op: zod.z.literal("legacy.agent.variables.get"),
|
|
595
|
-
agentId: id,
|
|
310
|
+
agentId: id$1,
|
|
596
311
|
key: variableKey
|
|
597
312
|
}).strict(),
|
|
598
313
|
zod.z.object({
|
|
599
314
|
op: zod.z.literal("legacy.agent.variables.update"),
|
|
600
|
-
agentId: id,
|
|
315
|
+
agentId: id$1,
|
|
601
316
|
key: variableKey,
|
|
602
317
|
variable: zod.z.record(zod.z.unknown())
|
|
603
318
|
}).strict(),
|
|
604
319
|
zod.z.object({
|
|
605
320
|
op: zod.z.literal("legacy.agent.variables.delete"),
|
|
606
|
-
agentId: id,
|
|
321
|
+
agentId: id$1,
|
|
607
322
|
key: variableKey
|
|
608
323
|
}).strict(),
|
|
609
|
-
zod.z.object({ op: zod.z.literal("legacy.job.cancel"), jobId: id }).strict(),
|
|
324
|
+
zod.z.object({ op: zod.z.literal("legacy.job.cancel"), jobId: id$1 }).strict(),
|
|
610
325
|
zod.z.object({ op: zod.z.literal("commands.list") }).strict(),
|
|
611
326
|
zod.z.object({
|
|
612
327
|
op: zod.z.literal("commands.entities"),
|
|
613
328
|
sessionId,
|
|
614
|
-
command: id,
|
|
329
|
+
command: id$1,
|
|
615
330
|
name: zod.z.string().max(128).optional()
|
|
616
331
|
}).strict(),
|
|
617
332
|
zod.z.object({ op: zod.z.literal("models.catalogue"), live: zod.z.boolean().optional() }).strict(),
|
|
618
333
|
zod.z.object({
|
|
619
334
|
op: zod.z.literal("usage.get"),
|
|
620
|
-
provider: id.optional(),
|
|
335
|
+
provider: id$1.optional(),
|
|
621
336
|
live: zod.z.boolean().optional(),
|
|
622
337
|
refresh: zod.z.boolean().optional()
|
|
623
338
|
}).strict(),
|
|
@@ -628,7 +343,7 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
628
343
|
untilMs: zod.z.number().int().nonnegative().optional(),
|
|
629
344
|
days: zod.z.number().int().min(1).max(366).optional()
|
|
630
345
|
}).strict(),
|
|
631
|
-
zod.z.object({ op: zod.z.literal("skills.list"), sessionId, name: id.optional() }).strict(),
|
|
346
|
+
zod.z.object({ op: zod.z.literal("skills.list"), sessionId, name: id$1.optional() }).strict(),
|
|
632
347
|
zod.z.object({ op: zod.z.literal("sessions.failovers"), sessionId }).strict(),
|
|
633
348
|
zod.z.object({
|
|
634
349
|
op: zod.z.literal("sessions.credential"),
|
|
@@ -737,10 +452,10 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
737
452
|
"account.agents"
|
|
738
453
|
]),
|
|
739
454
|
request_id: requestId.optional(),
|
|
740
|
-
tenant_id: id.optional(),
|
|
741
|
-
account_id: id.optional(),
|
|
742
|
-
agent_id: id.optional(),
|
|
743
|
-
comment_id: id.optional(),
|
|
455
|
+
tenant_id: id$1.optional(),
|
|
456
|
+
account_id: id$1.optional(),
|
|
457
|
+
agent_id: id$1.optional(),
|
|
458
|
+
comment_id: id$1.optional(),
|
|
744
459
|
query: zod.z.record(zod.z.union([zod.z.string().max(1024), zod.z.number().int()])).optional(),
|
|
745
460
|
payload: zod.z.record(zod.z.unknown()).optional(),
|
|
746
461
|
confirmed: zod.z.boolean().optional()
|
|
@@ -753,12 +468,12 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
753
468
|
accountId: zod.z.number().int().positive(),
|
|
754
469
|
confirmed: zod.z.literal(true)
|
|
755
470
|
}).strict(),
|
|
756
|
-
zod.z.object({ op: zod.z.literal("auth.start"), provider: id }).strict(),
|
|
757
|
-
zod.z.object({ op: zod.z.literal("auth.status"), id }).strict(),
|
|
758
|
-
zod.z.object({ op: zod.z.literal("auth.input"), id, promptId: id, value: secret }).strict(),
|
|
759
|
-
zod.z.object({ op: zod.z.literal("auth.cancel"), id }).strict(),
|
|
760
|
-
zod.z.object({ op: zod.z.literal("auth.key"), provider: id, value: secret }).strict(),
|
|
761
|
-
zod.z.object({ op: zod.z.literal("auth.logout"), provider: id }).strict(),
|
|
471
|
+
zod.z.object({ op: zod.z.literal("auth.start"), provider: id$1 }).strict(),
|
|
472
|
+
zod.z.object({ op: zod.z.literal("auth.status"), id: id$1 }).strict(),
|
|
473
|
+
zod.z.object({ op: zod.z.literal("auth.input"), id: id$1, promptId: id$1, value: secret }).strict(),
|
|
474
|
+
zod.z.object({ op: zod.z.literal("auth.cancel"), id: id$1 }).strict(),
|
|
475
|
+
zod.z.object({ op: zod.z.literal("auth.key"), provider: id$1, value: secret }).strict(),
|
|
476
|
+
zod.z.object({ op: zod.z.literal("auth.logout"), provider: id$1 }).strict(),
|
|
762
477
|
zod.z.object({ op: zod.z.literal("settings.list") }).strict(),
|
|
763
478
|
zod.z.object({
|
|
764
479
|
op: zod.z.literal("settings.edit"),
|
|
@@ -1262,18 +977,305 @@ function desktopEndpoint(request) {
|
|
|
1262
977
|
return { path: "/v1/config/system-prompt", method: "GET" };
|
|
1263
978
|
case "instructions.update":
|
|
1264
979
|
return {
|
|
1265
|
-
path: "/v1/config/system-prompt",
|
|
1266
|
-
method: "PATCH",
|
|
1267
|
-
body: { content: request.content }
|
|
980
|
+
path: "/v1/config/system-prompt",
|
|
981
|
+
method: "PATCH",
|
|
982
|
+
body: { content: request.content }
|
|
983
|
+
};
|
|
984
|
+
case "credentials.list":
|
|
985
|
+
return { path: "/v1/credentials", method: "GET" };
|
|
986
|
+
case "credentials.update":
|
|
987
|
+
return {
|
|
988
|
+
path: "/v1/credentials",
|
|
989
|
+
method: "PATCH",
|
|
990
|
+
body: { key: request.key, value: request.value }
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const MAX_PROBE_PATHS = 64;
|
|
995
|
+
const MAX_FILE_READ_BYTES = 64 * 1024 * 1024;
|
|
996
|
+
const id = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
997
|
+
const MAX_UPLOAD_BYTES = 16 * 1024 * 1024;
|
|
998
|
+
const MAX_DOWNLOAD_BYTES = 32 * 1024 * 1024;
|
|
999
|
+
const mediaRequestSchema = zod.z.discriminatedUnion("op", [
|
|
1000
|
+
zod.z.object({
|
|
1001
|
+
op: zod.z.literal("speech.create"),
|
|
1002
|
+
request: zod.z.record(zod.z.unknown())
|
|
1003
|
+
}).strict(),
|
|
1004
|
+
zod.z.object({
|
|
1005
|
+
op: zod.z.literal("speech.agent"),
|
|
1006
|
+
agentId: id,
|
|
1007
|
+
request: zod.z.record(zod.z.unknown())
|
|
1008
|
+
}).strict(),
|
|
1009
|
+
zod.z.object({
|
|
1010
|
+
op: zod.z.literal("transcription.create"),
|
|
1011
|
+
fileName: zod.z.string().min(1).max(255),
|
|
1012
|
+
mimeType: zod.z.string().min(1).max(128),
|
|
1013
|
+
fields: zod.z.record(zod.z.string().max(4096))
|
|
1014
|
+
}).strict(),
|
|
1015
|
+
zod.z.object({
|
|
1016
|
+
op: zod.z.literal("agent.import"),
|
|
1017
|
+
fileName: zod.z.string().min(1).max(255)
|
|
1018
|
+
}).strict(),
|
|
1019
|
+
// Export is a gated GET that answers with a ZIP, so it belongs on this relay
|
|
1020
|
+
// rather than the JSON transport: `desktopEndpoint`'s envelope has nowhere to
|
|
1021
|
+
// put binary. It is the only read here, which is why `endpoint()` below
|
|
1022
|
+
// returns a method instead of assuming POST.
|
|
1023
|
+
zod.z.object({ op: zod.z.literal("agent.export"), agentId: id }).strict(),
|
|
1024
|
+
// A durable transcript row references an image by content digest with the
|
|
1025
|
+
// payload stripped, so rendering a screenshot after a reload means fetching
|
|
1026
|
+
// bytes. That cannot go through the JSON transport (its envelope has nowhere
|
|
1027
|
+
// to put them), which is what puts it on this relay.
|
|
1028
|
+
//
|
|
1029
|
+
// Both identifiers are shape-constrained here as well as by the backend
|
|
1030
|
+
// route: this file's whole premise is that renderer code cannot pick a URL,
|
|
1031
|
+
// and a digest that reaches `endpoint()` unvalidated is renderer-controlled
|
|
1032
|
+
// path text.
|
|
1033
|
+
zod.z.object({
|
|
1034
|
+
op: zod.z.literal("sessions.attachment"),
|
|
1035
|
+
sessionId: zod.z.string().regex(/^[a-f0-9]{12}$/),
|
|
1036
|
+
digest: zod.z.string().regex(/^[a-f0-9]{32}$/)
|
|
1037
|
+
}).strict()
|
|
1038
|
+
]);
|
|
1039
|
+
function endpoint(request) {
|
|
1040
|
+
switch (request.op) {
|
|
1041
|
+
case "speech.create":
|
|
1042
|
+
return { path: "/v1/tools/speech", method: "POST" };
|
|
1043
|
+
case "speech.agent":
|
|
1044
|
+
return { path: `/v1/agents/${request.agentId}/speech`, method: "POST" };
|
|
1045
|
+
case "transcription.create":
|
|
1046
|
+
return { path: "/v1/transcriptions", method: "POST" };
|
|
1047
|
+
case "agent.import":
|
|
1048
|
+
return { path: "/v1/agents/import", method: "POST" };
|
|
1049
|
+
case "agent.export":
|
|
1050
|
+
return { path: `/v1/agents/${request.agentId}/export`, method: "GET" };
|
|
1051
|
+
case "sessions.attachment":
|
|
1052
|
+
return {
|
|
1053
|
+
path: `/v1/desktop/sessions/${request.sessionId}/attachments/${request.digest}`,
|
|
1054
|
+
method: "GET"
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function requestDesktopMedia(input, bytes, backendUrl, token) {
|
|
1059
|
+
const parsed = mediaRequestSchema.safeParse(input);
|
|
1060
|
+
if (!parsed.success) {
|
|
1061
|
+
return { status: 422, kind: "error", detail: "Invalid media operation." };
|
|
1062
|
+
}
|
|
1063
|
+
if (!token) {
|
|
1064
|
+
return {
|
|
1065
|
+
status: 503,
|
|
1066
|
+
kind: "error",
|
|
1067
|
+
detail: "Restart with a desktop-managed backend to use these controls."
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
const request = parsed.data;
|
|
1071
|
+
if (bytes && bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
1072
|
+
return { status: 413, kind: "error", detail: "This file is too large." };
|
|
1073
|
+
}
|
|
1074
|
+
const target2 = endpoint(request);
|
|
1075
|
+
let body;
|
|
1076
|
+
let contentType;
|
|
1077
|
+
if (request.op === "agent.export" || request.op === "sessions.attachment") {
|
|
1078
|
+
body = void 0;
|
|
1079
|
+
} else if (request.op === "speech.create" || request.op === "speech.agent") {
|
|
1080
|
+
body = JSON.stringify(request.request);
|
|
1081
|
+
contentType = "application/json";
|
|
1082
|
+
} else {
|
|
1083
|
+
if (!bytes) {
|
|
1084
|
+
return { status: 422, kind: "error", detail: "A file is required." };
|
|
1085
|
+
}
|
|
1086
|
+
const form = new FormData();
|
|
1087
|
+
const blob = new Blob([bytes], {
|
|
1088
|
+
type: request.op === "transcription.create" ? request.mimeType : "application/zip"
|
|
1089
|
+
});
|
|
1090
|
+
form.append("file", blob, request.fileName);
|
|
1091
|
+
if (request.op === "transcription.create") {
|
|
1092
|
+
for (const [key, value] of Object.entries(request.fields)) {
|
|
1093
|
+
form.append(key, value);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
body = form;
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
const response = await fetch(new URL(target2.path, backendUrl), {
|
|
1100
|
+
method: target2.method,
|
|
1101
|
+
headers: {
|
|
1102
|
+
Accept: "application/json, audio/*, image/*, application/octet-stream",
|
|
1103
|
+
...contentType ? { "Content-Type": contentType } : {},
|
|
1104
|
+
Authorization: `Bearer ${token}`
|
|
1105
|
+
},
|
|
1106
|
+
body,
|
|
1107
|
+
redirect: "error",
|
|
1108
|
+
signal: AbortSignal.timeout(12e4)
|
|
1109
|
+
});
|
|
1110
|
+
const responseType = response.headers.get("content-type") ?? "";
|
|
1111
|
+
if (!response.ok) {
|
|
1112
|
+
let detail = "The media request failed.";
|
|
1113
|
+
if (responseType.includes("application/json")) {
|
|
1114
|
+
try {
|
|
1115
|
+
const parsedError = await response.json();
|
|
1116
|
+
const text = parsedError.detail ?? parsedError.message;
|
|
1117
|
+
if (typeof text === "string") detail = text.slice(0, 512);
|
|
1118
|
+
} catch {
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return { status: response.status, kind: "error", detail };
|
|
1122
|
+
}
|
|
1123
|
+
if (responseType.includes("application/json")) {
|
|
1124
|
+
return {
|
|
1125
|
+
status: response.status,
|
|
1126
|
+
kind: "json",
|
|
1127
|
+
body: await response.json()
|
|
1268
1128
|
};
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1129
|
+
}
|
|
1130
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
1131
|
+
if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {
|
|
1272
1132
|
return {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1133
|
+
status: 502,
|
|
1134
|
+
kind: "error",
|
|
1135
|
+
detail: "The backend returned too much data."
|
|
1276
1136
|
};
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
status: response.status,
|
|
1140
|
+
kind: "bytes",
|
|
1141
|
+
mimeType: responseType || "application/octet-stream",
|
|
1142
|
+
data: buffer
|
|
1143
|
+
};
|
|
1144
|
+
} catch {
|
|
1145
|
+
return {
|
|
1146
|
+
status: 503,
|
|
1147
|
+
kind: "error",
|
|
1148
|
+
detail: "The backend could not complete this request. Check its connection and try again."
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const SESSION_ID = /^[a-f0-9]{12}$/;
|
|
1153
|
+
const SAFE_ID = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
1154
|
+
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
1155
|
+
class DesktopStreamRelay {
|
|
1156
|
+
constructor(backendUrl, token) {
|
|
1157
|
+
this.backendUrl = backendUrl;
|
|
1158
|
+
this.token = token;
|
|
1159
|
+
}
|
|
1160
|
+
streams = /* @__PURE__ */ new Map();
|
|
1161
|
+
/** Frame observer for main-owned side effects (notifications). It sees
|
|
1162
|
+
* every parsed frame before the renderer does, on the same subscription. */
|
|
1163
|
+
observer = null;
|
|
1164
|
+
observe(observer) {
|
|
1165
|
+
this.observer = observer;
|
|
1166
|
+
}
|
|
1167
|
+
/** True when this relay can authenticate; a false answer is why the UI
|
|
1168
|
+
* shows the pairing/setup state rather than a dead transcript. */
|
|
1169
|
+
get available() {
|
|
1170
|
+
return Boolean(this.token);
|
|
1171
|
+
}
|
|
1172
|
+
subscribe(args, emit) {
|
|
1173
|
+
if (!SESSION_ID.test(args.sessionId)) {
|
|
1174
|
+
throw new Error("Invalid session for streaming.");
|
|
1175
|
+
}
|
|
1176
|
+
if (args.epoch !== void 0 && !SAFE_ID.test(args.epoch)) {
|
|
1177
|
+
throw new Error("Invalid stream epoch.");
|
|
1178
|
+
}
|
|
1179
|
+
if (args.afterSeq !== void 0 && (!Number.isInteger(args.afterSeq) || args.afterSeq < 0)) {
|
|
1180
|
+
throw new Error("Invalid stream cursor.");
|
|
1181
|
+
}
|
|
1182
|
+
if (!this.token) {
|
|
1183
|
+
throw new Error("This backend was not started with desktop controls.");
|
|
1184
|
+
}
|
|
1185
|
+
const streamId = node_crypto.randomBytes(16).toString("hex");
|
|
1186
|
+
const controller = new AbortController();
|
|
1187
|
+
this.streams.set(streamId, controller);
|
|
1188
|
+
const query = new URLSearchParams();
|
|
1189
|
+
if (args.epoch) query.set("epoch", args.epoch);
|
|
1190
|
+
if (args.afterSeq !== void 0)
|
|
1191
|
+
query.set("after_seq", String(args.afterSeq));
|
|
1192
|
+
const suffix = query.size > 0 ? `?${query}` : "";
|
|
1193
|
+
const url = new URL(
|
|
1194
|
+
`/v1/desktop/sessions/${args.sessionId}/events${suffix}`,
|
|
1195
|
+
this.backendUrl
|
|
1196
|
+
);
|
|
1197
|
+
void this.pump(
|
|
1198
|
+
streamId,
|
|
1199
|
+
args.sessionId,
|
|
1200
|
+
url,
|
|
1201
|
+
controller.signal,
|
|
1202
|
+
emit
|
|
1203
|
+
).finally(() => {
|
|
1204
|
+
this.streams.delete(streamId);
|
|
1205
|
+
});
|
|
1206
|
+
return { streamId };
|
|
1207
|
+
}
|
|
1208
|
+
async pump(streamId, sessionId2, url, signal, emit) {
|
|
1209
|
+
try {
|
|
1210
|
+
const response = await fetch(url, {
|
|
1211
|
+
headers: {
|
|
1212
|
+
Accept: "text/event-stream",
|
|
1213
|
+
Authorization: `Bearer ${this.token}`
|
|
1214
|
+
},
|
|
1215
|
+
redirect: "error",
|
|
1216
|
+
signal
|
|
1217
|
+
});
|
|
1218
|
+
if (!response.ok || !response.body) {
|
|
1219
|
+
emit({
|
|
1220
|
+
streamId,
|
|
1221
|
+
kind: "error",
|
|
1222
|
+
detail: `The event stream was refused (${response.status}).`
|
|
1223
|
+
});
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const reader = response.body.getReader();
|
|
1227
|
+
const decoder = new TextDecoder();
|
|
1228
|
+
let buffer = "";
|
|
1229
|
+
for (; ; ) {
|
|
1230
|
+
const { done, value } = await reader.read();
|
|
1231
|
+
if (done) break;
|
|
1232
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1233
|
+
for (; ; ) {
|
|
1234
|
+
const boundary = buffer.indexOf("\n\n");
|
|
1235
|
+
if (boundary < 0) {
|
|
1236
|
+
if (buffer.length > MAX_FRAME_BYTES) {
|
|
1237
|
+
emit({
|
|
1238
|
+
streamId,
|
|
1239
|
+
kind: "error",
|
|
1240
|
+
detail: "The event stream exceeded its frame budget."
|
|
1241
|
+
});
|
|
1242
|
+
reader.cancel().catch(() => void 0);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
const record = buffer.slice(0, boundary);
|
|
1248
|
+
buffer = buffer.slice(boundary + 2);
|
|
1249
|
+
for (const line of record.split("\n")) {
|
|
1250
|
+
if (line.startsWith("data:")) {
|
|
1251
|
+
const data = line.slice(5).replace(/^ /, "");
|
|
1252
|
+
this.observer?.(sessionId2, data);
|
|
1253
|
+
emit({ streamId, kind: "data", data });
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
emit({ streamId, kind: "end" });
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (signal.aborted) {
|
|
1261
|
+
emit({ streamId, kind: "end" });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
emit({
|
|
1265
|
+
streamId,
|
|
1266
|
+
kind: "error",
|
|
1267
|
+
detail: error instanceof Error && error.name === "AbortError" ? "The event stream ended." : "The event stream connection failed."
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
unsubscribe(streamId) {
|
|
1272
|
+
if (!/^[a-f0-9]{32}$/.test(streamId)) return;
|
|
1273
|
+
this.streams.get(streamId)?.abort();
|
|
1274
|
+
this.streams.delete(streamId);
|
|
1275
|
+
}
|
|
1276
|
+
dispose() {
|
|
1277
|
+
for (const controller of this.streams.values()) controller.abort();
|
|
1278
|
+
this.streams.clear();
|
|
1277
1279
|
}
|
|
1278
1280
|
}
|
|
1279
1281
|
async function requestDesktop(input, backendUrl, token) {
|
|
@@ -7492,6 +7494,14 @@ function describeWindowLaunch(plan) {
|
|
|
7492
7494
|
return `window mode ${plan.mode}: ${plan.width}x${plan.height}, ${behaviour}, page throttling ${plan.backgroundThrottling ? "on" : "off"}`;
|
|
7493
7495
|
}
|
|
7494
7496
|
const BASE64_FILE_EXTENSIONS = ["csv", "tsv", "xls", "xlsx", "ods"];
|
|
7497
|
+
const resolveUserPath = (filePath, cwd) => {
|
|
7498
|
+
if (filePath === "~") return electron.app.getPath("home");
|
|
7499
|
+
if (filePath.startsWith("~/"))
|
|
7500
|
+
return path$1.join(electron.app.getPath("home"), filePath.slice(2));
|
|
7501
|
+
if (cwd && !filePath.startsWith("/"))
|
|
7502
|
+
return path$1.join(cwd.startsWith("~/") ? resolveUserPath(cwd) : cwd, filePath);
|
|
7503
|
+
return filePath;
|
|
7504
|
+
};
|
|
7495
7505
|
electron.app.setName("Local Operator");
|
|
7496
7506
|
const image = electron.nativeImage.createFromPath(icon);
|
|
7497
7507
|
if (process.platform === "darwin" && electron.app.dock) {
|
|
@@ -7856,8 +7866,7 @@ electron.app.whenReady().then(async () => {
|
|
|
7856
7866
|
"read-file",
|
|
7857
7867
|
async (_, filePath, encoding = "utf-8") => {
|
|
7858
7868
|
try {
|
|
7859
|
-
const
|
|
7860
|
-
const data = fs.readFileSync(normalizedPath, encoding);
|
|
7869
|
+
const data = fs.readFileSync(resolveUserPath(filePath), encoding);
|
|
7861
7870
|
return { success: true, data };
|
|
7862
7871
|
} catch (error) {
|
|
7863
7872
|
logger.error("Error reading file:", LogFileType.BACKEND, error);
|
|
@@ -7865,6 +7874,82 @@ electron.app.whenReady().then(async () => {
|
|
|
7865
7874
|
}
|
|
7866
7875
|
}
|
|
7867
7876
|
);
|
|
7877
|
+
electron.ipcMain.handle(
|
|
7878
|
+
"probe-files",
|
|
7879
|
+
async (_, paths, cwd) => {
|
|
7880
|
+
const asked = Array.isArray(paths) ? paths.filter((path2) => typeof path2 === "string") : [];
|
|
7881
|
+
return asked.slice(0, MAX_PROBE_PATHS).map((input) => {
|
|
7882
|
+
let resolved = input;
|
|
7883
|
+
try {
|
|
7884
|
+
resolved = resolveUserPath(input, cwd);
|
|
7885
|
+
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
7886
|
+
return {
|
|
7887
|
+
input,
|
|
7888
|
+
resolved,
|
|
7889
|
+
exists: stat !== void 0,
|
|
7890
|
+
isFile: stat?.isFile() ?? false,
|
|
7891
|
+
sizeBytes: stat?.isFile() ? stat.size : null,
|
|
7892
|
+
mtimeMs: stat?.isFile() ? stat.mtimeMs : null
|
|
7893
|
+
};
|
|
7894
|
+
} catch (error) {
|
|
7895
|
+
return {
|
|
7896
|
+
input,
|
|
7897
|
+
resolved,
|
|
7898
|
+
exists: false,
|
|
7899
|
+
isFile: false,
|
|
7900
|
+
sizeBytes: null,
|
|
7901
|
+
mtimeMs: null,
|
|
7902
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7903
|
+
};
|
|
7904
|
+
}
|
|
7905
|
+
});
|
|
7906
|
+
}
|
|
7907
|
+
);
|
|
7908
|
+
electron.ipcMain.handle(
|
|
7909
|
+
"read-file-bytes",
|
|
7910
|
+
async (_, filePath, maxBytes = MAX_FILE_READ_BYTES) => {
|
|
7911
|
+
const cap = Number.isFinite(maxBytes) && maxBytes > 0 ? Math.min(maxBytes, MAX_FILE_READ_BYTES) : MAX_FILE_READ_BYTES;
|
|
7912
|
+
try {
|
|
7913
|
+
const resolved = resolveUserPath(filePath);
|
|
7914
|
+
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
7915
|
+
if (!stat) {
|
|
7916
|
+
return {
|
|
7917
|
+
success: false,
|
|
7918
|
+
code: "not-found",
|
|
7919
|
+
error: `No file at ${resolved}`
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
if (!stat.isFile()) {
|
|
7923
|
+
return {
|
|
7924
|
+
success: false,
|
|
7925
|
+
code: "not-a-file",
|
|
7926
|
+
error: `${resolved} is not a regular file`
|
|
7927
|
+
};
|
|
7928
|
+
}
|
|
7929
|
+
if (stat.size > cap) {
|
|
7930
|
+
return {
|
|
7931
|
+
success: false,
|
|
7932
|
+
code: "too-large",
|
|
7933
|
+
error: `${resolved} is ${stat.size} bytes, over the ${cap}-byte preview cap`,
|
|
7934
|
+
sizeBytes: stat.size
|
|
7935
|
+
};
|
|
7936
|
+
}
|
|
7937
|
+
const buffer = fs.readFileSync(resolved);
|
|
7938
|
+
return {
|
|
7939
|
+
success: true,
|
|
7940
|
+
data: new Uint8Array(buffer),
|
|
7941
|
+
sizeBytes: buffer.byteLength
|
|
7942
|
+
};
|
|
7943
|
+
} catch (error) {
|
|
7944
|
+
logger.error("Error reading file bytes:", LogFileType.BACKEND, error);
|
|
7945
|
+
return {
|
|
7946
|
+
success: false,
|
|
7947
|
+
code: "unreadable",
|
|
7948
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7949
|
+
};
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7952
|
+
);
|
|
7868
7953
|
electron.ipcMain.handle("open-external", async (_, url) => {
|
|
7869
7954
|
try {
|
|
7870
7955
|
await electron.shell.openExternal(url);
|
|
@@ -7883,8 +7968,7 @@ electron.app.whenReady().then(async () => {
|
|
|
7883
7968
|
"save-file",
|
|
7884
7969
|
async (_, filePath, content, encoding = "utf-8") => {
|
|
7885
7970
|
try {
|
|
7886
|
-
|
|
7887
|
-
fs.writeFileSync(normalizedPath, content, encoding);
|
|
7971
|
+
fs.writeFileSync(resolveUserPath(filePath), content, encoding);
|
|
7888
7972
|
} catch (error) {
|
|
7889
7973
|
logger.error("Error saving file:", LogFileType.BACKEND, error);
|
|
7890
7974
|
throw error;
|
|
@@ -7892,14 +7976,13 @@ electron.app.whenReady().then(async () => {
|
|
|
7892
7976
|
}
|
|
7893
7977
|
);
|
|
7894
7978
|
electron.ipcMain.handle("file-exists", async (_, filePath) => {
|
|
7895
|
-
|
|
7896
|
-
return fs.existsSync(normalizedPath);
|
|
7979
|
+
return fs.existsSync(resolveUserPath(filePath));
|
|
7897
7980
|
});
|
|
7898
7981
|
electron.ipcMain.handle("directory-exists", async (_, dirPath) => {
|
|
7899
|
-
const home = electron.app.getPath("home");
|
|
7900
|
-
const normalizedPath = dirPath === "~" ? home : dirPath.startsWith("~/") ? path$1.join(home, dirPath.slice(2)) : dirPath;
|
|
7901
7982
|
try {
|
|
7902
|
-
return fs.statSync(
|
|
7983
|
+
return fs.statSync(resolveUserPath(dirPath), {
|
|
7984
|
+
throwIfNoEntry: false
|
|
7985
|
+
})?.isDirectory() ?? false;
|
|
7903
7986
|
} catch {
|
|
7904
7987
|
return false;
|
|
7905
7988
|
}
|