local-operator-ui 0.20.0 → 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.
Files changed (68) hide show
  1. package/out/main/index.js +426 -343
  2. package/out/preload/index.js +10 -0
  3. package/out/renderer/assets/{_basePickBy-FfClaKQg.js → _basePickBy-Du3cfBqe.js} +1 -1
  4. package/out/renderer/assets/{_baseUniq-C9SvGZui.js → _baseUniq-Dq4vzytk.js} +1 -1
  5. package/out/renderer/assets/{agent-details-page-AoSG_fVH.js → agent-details-page-BBT_i4U4.js} +1 -1
  6. package/out/renderer/assets/{agent-hub-page-CaZjaTZe.js → agent-hub-page-D8gesCRj.js} +1 -1
  7. package/out/renderer/assets/{agents-page-RS6PE_fF.js → agents-page-B2NnQfhM.js} +2 -2
  8. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-Be7LQKHg.js → architectureDiagram-IEHRJDOE-CS_3oayq.js} +1 -1
  9. package/out/renderer/assets/{blockDiagram-JOT3LUYC-iWnvArS2.js → blockDiagram-JOT3LUYC-BNDbdGzn.js} +1 -1
  10. package/out/renderer/assets/{c4Diagram-VJAJSXHY-BBuir5yQ.js → c4Diagram-VJAJSXHY-5HDa_Dur.js} +1 -1
  11. package/out/renderer/assets/channel-nFiBFvkY.js +1 -0
  12. package/out/renderer/assets/{chunk-4BMEZGHF-DzRvFZnz.js → chunk-4BMEZGHF-me2GzChw.js} +1 -1
  13. package/out/renderer/assets/{chunk-A2AXSNBT-4uP9DbW7.js → chunk-A2AXSNBT-Q-KKGGGm.js} +1 -1
  14. package/out/renderer/assets/{chunk-AEK57VVT-BL3rbHq4.js → chunk-AEK57VVT-BAp_jb_D.js} +1 -1
  15. package/out/renderer/assets/{chunk-D6G4REZN-D6wbdfUw.js → chunk-D6G4REZN-DR8iAD8Y.js} +1 -1
  16. package/out/renderer/assets/{chunk-RZ5BOZE2-D2Fl_NaB.js → chunk-RZ5BOZE2-42LQGahH.js} +1 -1
  17. package/out/renderer/assets/{chunk-XZIHB7SX-DN5jo0Xq.js → chunk-XZIHB7SX-CRdJ4v_1.js} +1 -1
  18. package/out/renderer/assets/classDiagram-GIVACNV2-BmZ3ZDSH.js +1 -0
  19. package/out/renderer/assets/classDiagram-v2-COTLJTTW-BmZ3ZDSH.js +1 -0
  20. package/out/renderer/assets/clone-BsKeRQ9y.js +1 -0
  21. package/out/renderer/assets/{compact-pagination-20O8NjDK.js → compact-pagination-Ke-mkrto.js} +1 -1
  22. package/out/renderer/assets/{dagre-OKDRZEBW-BYIesx33.js → dagre-OKDRZEBW-Dj-_-lbb.js} +1 -1
  23. package/out/renderer/assets/{diagram-SSKATNLV-DP6lL-Te.js → diagram-SSKATNLV-KAsvPn06.js} +1 -1
  24. package/out/renderer/assets/{diagram-VNBRO52H-bmsCnWFr.js → diagram-VNBRO52H-BLSKSGv7.js} +1 -1
  25. package/out/renderer/assets/{erDiagram-Q7BY3M3F-EsgbzeoI.js → erDiagram-Q7BY3M3F-B1uo8Ttt.js} +1 -1
  26. package/out/renderer/assets/{flowDiagram-4HSFHLVR-DOiNzXZN.js → flowDiagram-4HSFHLVR-D89Q6QeY.js} +1 -1
  27. package/out/renderer/assets/{ganttDiagram-APWFNJXF-DJmMxmWH.js → ganttDiagram-APWFNJXF-DYEIQner.js} +1 -1
  28. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-CsW8qQBE.js → gitGraphDiagram-7IBYFJ6S-DNFcQAXC.js} +1 -1
  29. package/out/renderer/assets/{graph-DPdUvm6W.js → graph-DPB5A4Er.js} +1 -1
  30. package/out/renderer/assets/icon-GWZ2Pe4t.css +1 -0
  31. package/out/renderer/assets/{index-C2JWjezT.js → index-D7h0cbc3.js} +1 -1
  32. package/out/renderer/assets/{index-DTxYU-h1.js → index-Dsvvzi5i.js} +308 -306
  33. package/out/renderer/assets/{index-BHWYYomf.js → index-OkokcYbT.js} +1 -1
  34. package/out/renderer/assets/{infoDiagram-PH2N3AL5-DlGp0OAs.js → infoDiagram-PH2N3AL5-CjDiLpkp.js} +1 -1
  35. package/out/renderer/assets/{installer-Ck14_4Oh.js → installer-BR2Mx27S.js} +1 -1
  36. package/out/renderer/assets/{journeyDiagram-U35MCT3I-BMKeuZdQ.js → journeyDiagram-U35MCT3I-2AwP5v9s.js} +1 -1
  37. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-CBMkFWiV.js → kanban-definition-NDS4AKOZ-CHOQal41.js} +1 -1
  38. package/out/renderer/assets/{layout-CQLCCDdm.js → layout-PzJFXrpY.js} +1 -1
  39. package/out/renderer/assets/{legacy-agents-page-DaL-s8eO.js → legacy-agents-page-DS9cJ5Ch.js} +1 -1
  40. package/out/renderer/assets/{mermaid.core-zhMaqo9l.js → mermaid.core-ObZGxTfv.js} +5 -5
  41. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-ChxcwGV6.js → mindmap-definition-ALO5MXBD-CJVu4Cq9.js} +1 -1
  42. package/out/renderer/assets/{page-header-D-uc8qqA.js → page-header-MHBoBUg-.js} +1 -1
  43. package/out/renderer/assets/{parseISO-Bgv71POy.js → parseISO-BCz-yMl-.js} +1 -1
  44. package/out/renderer/assets/{pieDiagram-IB7DONF6-BMEW9BJi.js → pieDiagram-IB7DONF6-FNr0Rin7.js} +1 -1
  45. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-D-ErPCKs.js → quadrantDiagram-7GDLP6J5-ZBae0crI.js} +1 -1
  46. package/out/renderer/assets/{radar-MK3ICKWK-2tp4r77h.js → radar-MK3ICKWK-QkpqLslW.js} +1 -1
  47. package/out/renderer/assets/{radient-auth-buttons-klNSjDud.js → radient-auth-buttons-cVPSsmip.js} +1 -1
  48. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-nNjxV4M0.js → requirementDiagram-KVF5MWMF-BTkK1lFT.js} +1 -1
  49. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-HK65A8n7.js → sankeyDiagram-QLVOVGJD-Bavn2Nby.js} +1 -1
  50. package/out/renderer/assets/{schedules-page-CLrcFhCL.js → schedules-page-DnPP_ud3.js} +1 -1
  51. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-D3VDaA9I.js → sequenceDiagram-X6HHIX6F-CKHtEkHH.js} +1 -1
  52. package/out/renderer/assets/{settings-page-CKVWBbsu.js → settings-page-B3RdcR4e.js} +1 -1
  53. package/out/renderer/assets/{square-pen-CQEKS3JT.js → square-pen-DJCejD2T.js} +1 -1
  54. package/out/renderer/assets/{stateDiagram-DGXRK772-BJMfya--.js → stateDiagram-DGXRK772-DC4AGn0I.js} +1 -1
  55. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Cn7HubUo.js +1 -0
  56. package/out/renderer/assets/{timeline-definition-BDJGKUSR-wvXoFLOl.js → timeline-definition-BDJGKUSR-Crx7z7bt.js} +1 -1
  57. package/out/renderer/assets/{use-agent-like-mutation-DAmS19om.js → use-agent-like-mutation-DwCdN3lD.js} +1 -1
  58. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-DnyQ060j.js → xychartDiagram-VJFVF3MP-CY5IW4O0.js} +1 -1
  59. package/out/renderer/index.html +3 -3
  60. package/out/renderer/installer.html +3 -3
  61. package/package.json +2 -2
  62. package/out/renderer/assets/channel-D4JK8GjQ.js +0 -1
  63. package/out/renderer/assets/classDiagram-GIVACNV2-yxaMQW5a.js +0 -1
  64. package/out/renderer/assets/classDiagram-v2-COTLJTTW-yxaMQW5a.js +0 -1
  65. package/out/renderer/assets/clone-B7rlppnW.js +0 -1
  66. package/out/renderer/assets/icon-HSrrv8O-.css +0 -1
  67. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CmCw0Yzl.js +0 -1
  68. /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
- case "credentials.list":
1270
- return { path: "/v1/credentials", method: "GET" };
1271
- case "credentials.update":
1129
+ }
1130
+ const buffer = new Uint8Array(await response.arrayBuffer());
1131
+ if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {
1272
1132
  return {
1273
- path: "/v1/credentials",
1274
- method: "PATCH",
1275
- body: { key: request.key, value: request.value }
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 normalizedPath = filePath.startsWith("~/") ? path$1.join(electron.app.getPath("home"), filePath.slice(2)) : filePath;
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
- const normalizedPath = filePath.startsWith("~/") ? path$1.join(electron.app.getPath("home"), filePath.slice(2)) : filePath;
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
- const normalizedPath = filePath.startsWith("~/") ? path$1.join(electron.app.getPath("home"), filePath.slice(2)) : filePath;
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(normalizedPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
7983
+ return fs.statSync(resolveUserPath(dirPath), {
7984
+ throwIfNoEntry: false
7985
+ })?.isDirectory() ?? false;
7903
7986
  } catch {
7904
7987
  return false;
7905
7988
  }