framer-dalton 0.0.1

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.
@@ -0,0 +1,718 @@
1
+ import fs2 from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import 'child_process';
5
+ import { fileURLToPath } from 'url';
6
+ import http from 'http';
7
+ import crypto from 'crypto';
8
+ import { createRequire } from 'module';
9
+ import * as vm from 'vm';
10
+ import { connect } from 'framer-api';
11
+
12
+ /* @framer/ai relay server v0.0.1 */
13
+ var __defProp = Object.defineProperty;
14
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
15
+ function getLogPath() {
16
+ if (process.env.XDG_STATE_HOME) {
17
+ return path.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
18
+ }
19
+ if (process.platform === "win32") {
20
+ return path.join(
21
+ process.env.APPDATA || os.homedir(),
22
+ "framer",
23
+ "relay.log"
24
+ );
25
+ }
26
+ return path.join(os.homedir(), ".local", "state", "framer", "relay.log");
27
+ }
28
+ __name(getLogPath, "getLogPath");
29
+ var logPath = getLogPath();
30
+ var initialized = false;
31
+ function ensureLogDir() {
32
+ if (initialized) return;
33
+ const dir = path.dirname(logPath);
34
+ if (!fs2.existsSync(dir)) {
35
+ fs2.mkdirSync(dir, { recursive: true });
36
+ }
37
+ initialized = true;
38
+ }
39
+ __name(ensureLogDir, "ensureLogDir");
40
+ function log(message) {
41
+ ensureLogDir();
42
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
43
+ fs2.appendFileSync(logPath, `${timestamp} ${message}
44
+ `);
45
+ }
46
+ __name(log, "log");
47
+ var __filename$1 = fileURLToPath(import.meta.url);
48
+ path.dirname(__filename$1);
49
+ var VERSION = "0.0.1" ;
50
+ var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19987;
51
+
52
+ // src/connection-errors.ts
53
+ var CONNECTION_ERROR_PATTERNS = [
54
+ "Connection closed",
55
+ // FramerAPIError PROJECT_CLOSED
56
+ "Connection to the server was closed",
57
+ // FramerAPIError PROJECT_CLOSED
58
+ "Connection timeout after",
59
+ // FramerAPIError TIMEOUT
60
+ "No connection to the server",
61
+ // FramerAPIError INTERNAL
62
+ "WebSocket upgrade failed",
63
+ // WebSocket handshake failure
64
+ "Session is not connected",
65
+ // Our own check in executor.ts
66
+ "Execution timed out after",
67
+ // Likely dead connection
68
+ "Session expired"
69
+ // Server-side session expiry
70
+ ];
71
+ function isConnectionError(errorMessage) {
72
+ return CONNECTION_ERROR_PATTERNS.some(
73
+ (pattern) => errorMessage.includes(pattern)
74
+ );
75
+ }
76
+ __name(isConnectionError, "isConnectionError");
77
+ var ScopedFS = class {
78
+ static {
79
+ __name(this, "ScopedFS");
80
+ }
81
+ allowedDirs;
82
+ constructor(allowedDirs) {
83
+ const defaultDirs = [process.cwd(), "/tmp", os.tmpdir()];
84
+ const dirs = allowedDirs ?? defaultDirs;
85
+ this.allowedDirs = [...new Set(dirs.map((d) => path.resolve(d)))];
86
+ }
87
+ isPathAllowed(resolved) {
88
+ return this.allowedDirs.some((dir) => {
89
+ return resolved === dir || resolved.startsWith(dir + path.sep);
90
+ });
91
+ }
92
+ resolvePath(filePath) {
93
+ const resolved = path.resolve(filePath);
94
+ if (!this.isPathAllowed(resolved)) {
95
+ const error2 = new Error(
96
+ `EPERM: operation not permitted, access outside allowed directories: ${filePath}`
97
+ );
98
+ error2.code = "EPERM";
99
+ error2.errno = -1;
100
+ error2.syscall = "access";
101
+ error2.path = filePath;
102
+ throw error2;
103
+ }
104
+ return resolved;
105
+ }
106
+ // Sync methods
107
+ readFileSync = /* @__PURE__ */ __name((filePath, options) => {
108
+ const resolved = this.resolvePath(filePath.toString());
109
+ return fs2.readFileSync(resolved, options);
110
+ }, "readFileSync");
111
+ writeFileSync = /* @__PURE__ */ __name((filePath, data, options) => {
112
+ const resolved = this.resolvePath(filePath.toString());
113
+ fs2.writeFileSync(
114
+ resolved,
115
+ data,
116
+ options
117
+ );
118
+ }, "writeFileSync");
119
+ appendFileSync = /* @__PURE__ */ __name((filePath, data, options) => {
120
+ const resolved = this.resolvePath(filePath.toString());
121
+ fs2.appendFileSync(
122
+ resolved,
123
+ data,
124
+ options
125
+ );
126
+ }, "appendFileSync");
127
+ readdirSync = /* @__PURE__ */ __name((dirPath, options) => {
128
+ const resolved = this.resolvePath(dirPath.toString());
129
+ return fs2.readdirSync(resolved, options);
130
+ }, "readdirSync");
131
+ mkdirSync = /* @__PURE__ */ __name((dirPath, options) => {
132
+ const resolved = this.resolvePath(dirPath.toString());
133
+ return fs2.mkdirSync(resolved, options);
134
+ }, "mkdirSync");
135
+ rmdirSync = /* @__PURE__ */ __name((dirPath, options) => {
136
+ const resolved = this.resolvePath(dirPath.toString());
137
+ fs2.rmdirSync(resolved, options);
138
+ }, "rmdirSync");
139
+ unlinkSync = /* @__PURE__ */ __name((filePath) => {
140
+ const resolved = this.resolvePath(filePath.toString());
141
+ fs2.unlinkSync(resolved);
142
+ }, "unlinkSync");
143
+ statSync = /* @__PURE__ */ __name((filePath, options) => {
144
+ const resolved = this.resolvePath(filePath.toString());
145
+ return fs2.statSync(resolved, options);
146
+ }, "statSync");
147
+ lstatSync = /* @__PURE__ */ __name((filePath, options) => {
148
+ const resolved = this.resolvePath(filePath.toString());
149
+ return fs2.lstatSync(resolved, options);
150
+ }, "lstatSync");
151
+ existsSync = /* @__PURE__ */ __name((filePath) => {
152
+ try {
153
+ const resolved = this.resolvePath(filePath.toString());
154
+ return fs2.existsSync(resolved);
155
+ } catch {
156
+ return false;
157
+ }
158
+ }, "existsSync");
159
+ accessSync = /* @__PURE__ */ __name((filePath, mode) => {
160
+ const resolved = this.resolvePath(filePath.toString());
161
+ fs2.accessSync(resolved, mode);
162
+ }, "accessSync");
163
+ copyFileSync = /* @__PURE__ */ __name((src, dest, mode) => {
164
+ const resolvedSrc = this.resolvePath(src.toString());
165
+ const resolvedDest = this.resolvePath(dest.toString());
166
+ fs2.copyFileSync(resolvedSrc, resolvedDest, mode);
167
+ }, "copyFileSync");
168
+ renameSync = /* @__PURE__ */ __name((oldPath, newPath) => {
169
+ const resolvedOld = this.resolvePath(oldPath.toString());
170
+ const resolvedNew = this.resolvePath(newPath.toString());
171
+ fs2.renameSync(resolvedOld, resolvedNew);
172
+ }, "renameSync");
173
+ rmSync = /* @__PURE__ */ __name((filePath, options) => {
174
+ const resolved = this.resolvePath(filePath.toString());
175
+ fs2.rmSync(resolved, options);
176
+ }, "rmSync");
177
+ // Stream methods
178
+ createReadStream = /* @__PURE__ */ __name((filePath, options) => {
179
+ const resolved = this.resolvePath(filePath.toString());
180
+ return fs2.createReadStream(resolved, options);
181
+ }, "createReadStream");
182
+ createWriteStream = /* @__PURE__ */ __name((filePath, options) => {
183
+ const resolved = this.resolvePath(filePath.toString());
184
+ return fs2.createWriteStream(resolved, options);
185
+ }, "createWriteStream");
186
+ // Promise-based API (fs.promises equivalent)
187
+ get promises() {
188
+ return {
189
+ readFile: /* @__PURE__ */ __name(async (filePath, options) => {
190
+ const resolved = this.resolvePath(filePath.toString());
191
+ return fs2.promises.readFile(
192
+ resolved,
193
+ options
194
+ );
195
+ }, "readFile"),
196
+ writeFile: /* @__PURE__ */ __name(async (filePath, data, options) => {
197
+ const resolved = this.resolvePath(filePath.toString());
198
+ return fs2.promises.writeFile(
199
+ resolved,
200
+ data,
201
+ options
202
+ );
203
+ }, "writeFile"),
204
+ appendFile: /* @__PURE__ */ __name(async (filePath, data, options) => {
205
+ const resolved = this.resolvePath(filePath.toString());
206
+ return fs2.promises.appendFile(
207
+ resolved,
208
+ data,
209
+ options
210
+ );
211
+ }, "appendFile"),
212
+ readdir: /* @__PURE__ */ __name(async (dirPath, options) => {
213
+ const resolved = this.resolvePath(dirPath.toString());
214
+ return fs2.promises.readdir(
215
+ resolved,
216
+ options
217
+ );
218
+ }, "readdir"),
219
+ mkdir: /* @__PURE__ */ __name(async (dirPath, options) => {
220
+ const resolved = this.resolvePath(dirPath.toString());
221
+ return fs2.promises.mkdir(resolved, options);
222
+ }, "mkdir"),
223
+ rmdir: /* @__PURE__ */ __name(async (dirPath, options) => {
224
+ const resolved = this.resolvePath(dirPath.toString());
225
+ return fs2.promises.rmdir(resolved, options);
226
+ }, "rmdir"),
227
+ unlink: /* @__PURE__ */ __name(async (filePath) => {
228
+ const resolved = this.resolvePath(filePath.toString());
229
+ return fs2.promises.unlink(resolved);
230
+ }, "unlink"),
231
+ stat: /* @__PURE__ */ __name(async (filePath, options) => {
232
+ const resolved = this.resolvePath(filePath.toString());
233
+ return fs2.promises.stat(resolved, options);
234
+ }, "stat"),
235
+ access: /* @__PURE__ */ __name(async (filePath, mode) => {
236
+ const resolved = this.resolvePath(filePath.toString());
237
+ return fs2.promises.access(resolved, mode);
238
+ }, "access"),
239
+ copyFile: /* @__PURE__ */ __name(async (src, dest, mode) => {
240
+ const resolvedSrc = this.resolvePath(src.toString());
241
+ const resolvedDest = this.resolvePath(dest.toString());
242
+ return fs2.promises.copyFile(resolvedSrc, resolvedDest, mode);
243
+ }, "copyFile"),
244
+ rename: /* @__PURE__ */ __name(async (oldPath, newPath) => {
245
+ const resolvedOld = this.resolvePath(oldPath.toString());
246
+ const resolvedNew = this.resolvePath(newPath.toString());
247
+ return fs2.promises.rename(resolvedOld, resolvedNew);
248
+ }, "rename"),
249
+ rm: /* @__PURE__ */ __name(async (filePath, options) => {
250
+ const resolved = this.resolvePath(filePath.toString());
251
+ return fs2.promises.rm(resolved, options);
252
+ }, "rm")
253
+ };
254
+ }
255
+ constants = fs2.constants;
256
+ };
257
+
258
+ // src/executor.ts
259
+ var DEFAULT_TIMEOUT = 3e4;
260
+ var baseRequire = createRequire(import.meta.url);
261
+ var ALLOWED_MODULES = /* @__PURE__ */ new Set([
262
+ "path",
263
+ "node:path",
264
+ "url",
265
+ "node:url",
266
+ "fs",
267
+ "node:fs",
268
+ "fs/promises",
269
+ "node:fs/promises",
270
+ "crypto",
271
+ "node:crypto",
272
+ "buffer",
273
+ "node:buffer",
274
+ "util",
275
+ "node:util",
276
+ "os",
277
+ "node:os"
278
+ ]);
279
+ function createSandboxedRequire(scopedFs) {
280
+ const sandboxedRequire = /* @__PURE__ */ __name(((id) => {
281
+ if (!ALLOWED_MODULES.has(id)) {
282
+ const error2 = new Error(
283
+ `Module "${id}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
284
+ );
285
+ error2.name = "ModuleNotAllowedError";
286
+ throw error2;
287
+ }
288
+ if (id === "fs" || id === "node:fs") {
289
+ return scopedFs;
290
+ }
291
+ if (id === "fs/promises" || id === "node:fs/promises") {
292
+ return scopedFs.promises;
293
+ }
294
+ return baseRequire(id);
295
+ }), "sandboxedRequire");
296
+ sandboxedRequire.resolve = baseRequire.resolve;
297
+ sandboxedRequire.cache = baseRequire.cache;
298
+ sandboxedRequire.extensions = baseRequire.extensions;
299
+ sandboxedRequire.main = baseRequire.main;
300
+ return sandboxedRequire;
301
+ }
302
+ __name(createSandboxedRequire, "createSandboxedRequire");
303
+ async function sandboxedImport(scopedFs, specifier) {
304
+ if (!ALLOWED_MODULES.has(specifier)) {
305
+ const error2 = new Error(
306
+ `Module "${specifier}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
307
+ );
308
+ error2.name = "ModuleNotAllowedError";
309
+ throw error2;
310
+ }
311
+ if (specifier === "fs" || specifier === "node:fs") {
312
+ return scopedFs;
313
+ }
314
+ if (specifier === "fs/promises" || specifier === "node:fs/promises") {
315
+ return scopedFs.promises;
316
+ }
317
+ return import(specifier);
318
+ }
319
+ __name(sandboxedImport, "sandboxedImport");
320
+ async function execute(session, connection, code, options = {}) {
321
+ const { timeout = DEFAULT_TIMEOUT, cwd } = options;
322
+ const output = [];
323
+ const customConsole = {
324
+ log: /* @__PURE__ */ __name((...args) => {
325
+ output.push(args.map((arg) => formatValue(arg)).join(" "));
326
+ }, "log"),
327
+ error: /* @__PURE__ */ __name((...args) => {
328
+ output.push(`[ERROR] ${args.map((arg) => formatValue(arg)).join(" ")}`);
329
+ }, "error"),
330
+ warn: /* @__PURE__ */ __name((...args) => {
331
+ output.push(`[WARN] ${args.map((arg) => formatValue(arg)).join(" ")}`);
332
+ }, "warn"),
333
+ info: /* @__PURE__ */ __name((...args) => {
334
+ output.push(args.map((arg) => formatValue(arg)).join(" "));
335
+ }, "info")
336
+ };
337
+ const scopedFs = cwd ? new ScopedFS([cwd, "/tmp"]) : new ScopedFS();
338
+ const sandboxedRequire = createSandboxedRequire(scopedFs);
339
+ const vmContextObj = {
340
+ // Framer API
341
+ framer: connection,
342
+ state: session.state,
343
+ // Console
344
+ console: customConsole,
345
+ // Module system (sandboxed)
346
+ require: sandboxedRequire,
347
+ import: /* @__PURE__ */ __name((specifier) => sandboxedImport(scopedFs, specifier), "import"),
348
+ // Timers
349
+ setTimeout,
350
+ clearTimeout,
351
+ setInterval,
352
+ clearInterval,
353
+ // Fetch & network
354
+ fetch,
355
+ // Common globals
356
+ Buffer,
357
+ URL,
358
+ URLSearchParams,
359
+ TextEncoder,
360
+ TextDecoder,
361
+ crypto,
362
+ AbortController,
363
+ AbortSignal,
364
+ structuredClone
365
+ };
366
+ const vmContext = vm.createContext(vmContextObj);
367
+ const wrappedCode = `(async () => { ${code} })()`;
368
+ try {
369
+ const script = new vm.Script(wrappedCode, {
370
+ filename: "framer-exec.js"
371
+ });
372
+ const resultPromise = script.runInContext(vmContext, {
373
+ timeout: 5e3
374
+ // Short timeout for sync part
375
+ });
376
+ const result = await Promise.race([
377
+ resultPromise,
378
+ new Promise(
379
+ (_, reject) => setTimeout(
380
+ () => reject(new Error(`Execution timed out after ${timeout}ms`)),
381
+ timeout
382
+ )
383
+ )
384
+ ]);
385
+ if (result !== void 0) {
386
+ output.push(formatValue(result));
387
+ }
388
+ return { output };
389
+ } catch (err) {
390
+ const errorMessage = err instanceof Error ? err.message : String(err);
391
+ return {
392
+ output,
393
+ error: errorMessage
394
+ };
395
+ }
396
+ }
397
+ __name(execute, "execute");
398
+ function formatValue(value) {
399
+ if (value === null) return "null";
400
+ if (value === void 0) return "undefined";
401
+ if (typeof value === "string") return value;
402
+ if (typeof value === "number" || typeof value === "boolean")
403
+ return String(value);
404
+ if (typeof value === "function")
405
+ return `[Function: ${value.name || "anonymous"}]`;
406
+ if (value instanceof Error) return value.message;
407
+ if (value instanceof Date) return value.toISOString();
408
+ if (value instanceof Map) return `Map(${value.size})`;
409
+ if (value instanceof Set) return `Set(${value.size})`;
410
+ if (Buffer.isBuffer(value)) return `Buffer(${value.length})`;
411
+ try {
412
+ return JSON.stringify(value, null, 2);
413
+ } catch {
414
+ return String(value);
415
+ }
416
+ }
417
+ __name(formatValue, "formatValue");
418
+ var ConnectionPool = class {
419
+ static {
420
+ __name(this, "ConnectionPool");
421
+ }
422
+ pool = /* @__PURE__ */ new Map();
423
+ /**
424
+ * Acquire a connection for a session.
425
+ * If a connection already exists for the project, the session is added to it.
426
+ * Otherwise, a new connection is created.
427
+ */
428
+ async acquire(projectId, apiKey, session) {
429
+ const entry = this.pool.get(projectId);
430
+ if (entry) {
431
+ entry.sessions.add(session);
432
+ return entry.connection;
433
+ }
434
+ const connection = await connect(projectId, apiKey);
435
+ this.pool.set(projectId, {
436
+ connection,
437
+ sessions: /* @__PURE__ */ new Set([session])
438
+ });
439
+ return connection;
440
+ }
441
+ /**
442
+ * Get the connection for a project.
443
+ */
444
+ getConnection(projectId) {
445
+ const entry = this.pool.get(projectId);
446
+ return entry?.connection ?? null;
447
+ }
448
+ /**
449
+ * Reconnect a project's connection (call after catching a connection error).
450
+ * Uses the same connection object but swaps the underlying WebSocket.
451
+ */
452
+ async reconnect(projectId) {
453
+ const entry = this.pool.get(projectId);
454
+ if (!entry) {
455
+ return null;
456
+ }
457
+ await entry.connection.reconnect();
458
+ return entry.connection;
459
+ }
460
+ /**
461
+ * Release a session from a connection.
462
+ * If no sessions remain, the connection is disconnected and removed.
463
+ */
464
+ async release(projectId, session) {
465
+ const entry = this.pool.get(projectId);
466
+ if (!entry) {
467
+ return;
468
+ }
469
+ entry.sessions.delete(session);
470
+ if (entry.sessions.size === 0) {
471
+ await entry.connection.disconnect();
472
+ this.pool.delete(projectId);
473
+ }
474
+ }
475
+ /**
476
+ * Release all connections (for cleanup).
477
+ */
478
+ async releaseAll() {
479
+ for (const [projectId, entry] of this.pool) {
480
+ await entry.connection.disconnect();
481
+ this.pool.delete(projectId);
482
+ }
483
+ }
484
+ };
485
+ var connectionPool = new ConnectionPool();
486
+
487
+ // src/session-manager.ts
488
+ var SessionManager = class {
489
+ static {
490
+ __name(this, "SessionManager");
491
+ }
492
+ sessions = /* @__PURE__ */ new Map();
493
+ async create(projectId, apiKey) {
494
+ let id = 1;
495
+ while (this.sessions.has(String(id))) {
496
+ id++;
497
+ }
498
+ const session = {
499
+ id: String(id),
500
+ projectId,
501
+ apiKey,
502
+ state: {}
503
+ };
504
+ await connectionPool.acquire(projectId, apiKey, session);
505
+ this.sessions.set(String(id), session);
506
+ return String(id);
507
+ }
508
+ list() {
509
+ return Array.from(this.sessions.values()).map((session) => ({
510
+ id: session.id,
511
+ projectId: session.projectId,
512
+ stateKeys: Object.keys(session.state)
513
+ }));
514
+ }
515
+ get(id) {
516
+ return this.sessions.get(id);
517
+ }
518
+ getConnection(session) {
519
+ return connectionPool.getConnection(session.projectId);
520
+ }
521
+ async reconnect(session) {
522
+ return connectionPool.reconnect(session.projectId);
523
+ }
524
+ async destroy(id) {
525
+ const session = this.sessions.get(id);
526
+ if (!session) {
527
+ return;
528
+ }
529
+ await connectionPool.release(session.projectId, session);
530
+ this.sessions.delete(id);
531
+ }
532
+ async destroyAll() {
533
+ for (const id of this.sessions.keys()) {
534
+ await this.destroy(id);
535
+ }
536
+ }
537
+ };
538
+ var sessionManager = new SessionManager();
539
+
540
+ // src/relay-server.ts
541
+ function json(res, data, status = 200) {
542
+ res.writeHead(status, { "Content-Type": "application/json" });
543
+ res.end(JSON.stringify(data));
544
+ }
545
+ __name(json, "json");
546
+ function error(res, message, status = 400) {
547
+ json(res, { error: message }, status);
548
+ }
549
+ __name(error, "error");
550
+ async function readBody(req) {
551
+ return new Promise((resolve, reject) => {
552
+ let body = "";
553
+ req.on("data", (chunk) => {
554
+ body += chunk;
555
+ });
556
+ req.on("end", () => {
557
+ try {
558
+ resolve(body ? JSON.parse(body) : {});
559
+ } catch {
560
+ reject(new Error("Invalid JSON body"));
561
+ }
562
+ });
563
+ req.on("error", reject);
564
+ });
565
+ }
566
+ __name(readBody, "readBody");
567
+ async function startRelayServer(port = RELAY_PORT) {
568
+ const server = http.createServer(async (req, res) => {
569
+ const url = req.url || "/";
570
+ try {
571
+ if (req.method === "GET" && url === "/version") {
572
+ return json(res, { version: VERSION });
573
+ }
574
+ if (req.method === "GET" && url === "/cli/sessions") {
575
+ const sessions = sessionManager.list();
576
+ return json(res, { sessions });
577
+ }
578
+ if (req.method === "POST" && url === "/cli/session/new") {
579
+ const body = await readBody(req);
580
+ const projectId = body.projectId;
581
+ const apiKey = body.apiKey;
582
+ if (!projectId || !apiKey) {
583
+ return error(res, "projectId and apiKey are required");
584
+ }
585
+ const id = await sessionManager.create(projectId, apiKey);
586
+ log(`session.new id=${id} project=${projectId}`);
587
+ return json(res, { id });
588
+ }
589
+ if (req.method === "POST" && url === "/cli/session/destroy") {
590
+ const body = await readBody(req);
591
+ const sessionId = body.sessionId ? String(body.sessionId) : "";
592
+ if (!sessionId) {
593
+ return error(res, "sessionId is required");
594
+ }
595
+ await sessionManager.destroy(sessionId);
596
+ log(`session.destroy id=${sessionId}`);
597
+ return json(res, { success: true });
598
+ }
599
+ if (req.method === "POST" && url === "/cli/exec") {
600
+ const body = await readBody(req);
601
+ const sessionId = String(body.sessionId);
602
+ const code = body.code;
603
+ const timeout = body.timeout || 3e4;
604
+ const cwd = body.cwd;
605
+ if (!sessionId) {
606
+ return error(res, "sessionId is required");
607
+ }
608
+ if (!code) {
609
+ return error(res, "code is required");
610
+ }
611
+ const session = sessionManager.get(sessionId);
612
+ if (!session) {
613
+ return error(res, `Session ${sessionId} not found`, 404);
614
+ }
615
+ log(
616
+ `exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
617
+ );
618
+ const connection = sessionManager.getConnection(session);
619
+ if (!connection) {
620
+ return json(res, {
621
+ output: [],
622
+ error: "Failed to get connection for session"
623
+ });
624
+ }
625
+ let result;
626
+ try {
627
+ result = await execute(session, connection, code, { timeout, cwd });
628
+ } catch (execErr) {
629
+ const errMsg = execErr instanceof Error ? execErr.message : String(execErr);
630
+ result = { output: [], error: errMsg };
631
+ }
632
+ if (result.error && isConnectionError(result.error)) {
633
+ log(
634
+ `reconnect session=${sessionId} project=${session.projectId} reason="${result.error}"`
635
+ );
636
+ const newConnection = await sessionManager.reconnect(session);
637
+ if (newConnection) {
638
+ try {
639
+ result = await execute(session, newConnection, code, {
640
+ timeout,
641
+ cwd
642
+ });
643
+ log(`reconnect.success session=${sessionId}`);
644
+ } catch (retryErr) {
645
+ const errMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
646
+ log(`reconnect.failed session=${sessionId} error="${errMsg}"`);
647
+ result = { output: [], error: errMsg };
648
+ }
649
+ } else {
650
+ log(
651
+ `reconnect.failed session=${sessionId} error="no connection returned"`
652
+ );
653
+ }
654
+ }
655
+ if (result.error) {
656
+ log(`exec.error session=${sessionId} error="${result.error}"`);
657
+ }
658
+ return json(res, result);
659
+ }
660
+ if (req.method === "POST" && url === "/shutdown") {
661
+ log("shutdown requested");
662
+ json(res, { success: true });
663
+ setTimeout(() => {
664
+ process.exit(0);
665
+ }, 100);
666
+ return;
667
+ }
668
+ error(res, "Not found", 404);
669
+ } catch (err) {
670
+ const message = err instanceof Error ? err.message : String(err);
671
+ log(`error: ${message}`);
672
+ error(res, message, 500);
673
+ }
674
+ });
675
+ return new Promise((resolve, reject) => {
676
+ server.on("error", reject);
677
+ server.listen(port, "127.0.0.1", () => {
678
+ resolve(server);
679
+ });
680
+ });
681
+ }
682
+ __name(startRelayServer, "startRelayServer");
683
+
684
+ // src/start-relay-server.ts
685
+ process.title = "framer-relay-server";
686
+ process.on("uncaughtException", (err) => {
687
+ log(`uncaught exception: ${err.message}`);
688
+ console.error("Uncaught Exception:", err);
689
+ process.exit(1);
690
+ });
691
+ process.on("unhandledRejection", (reason) => {
692
+ log(`unhandled rejection: ${reason}`);
693
+ console.error("Unhandled Rejection:", reason);
694
+ process.exit(1);
695
+ });
696
+ async function main() {
697
+ const server = await startRelayServer(RELAY_PORT);
698
+ log(`started v${VERSION} on port ${RELAY_PORT}`);
699
+ console.log(`Framer relay server v${VERSION} running on port ${RELAY_PORT}`);
700
+ process.on("SIGINT", () => {
701
+ log("shutdown SIGINT");
702
+ console.log("\nShutting down...");
703
+ server.close();
704
+ process.exit(0);
705
+ });
706
+ process.on("SIGTERM", () => {
707
+ log("shutdown SIGTERM");
708
+ console.log("\nShutting down...");
709
+ server.close();
710
+ process.exit(0);
711
+ });
712
+ }
713
+ __name(main, "main");
714
+ main().catch((err) => {
715
+ log(`startup failed: ${err.message}`);
716
+ console.error("Failed to start relay server:", err);
717
+ process.exit(1);
718
+ });