@framer/agent 0.0.0 → 0.0.29

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,1669 @@
1
+ import fs4 from 'fs';
2
+ import os5 from 'os';
3
+ import path5 from 'path';
4
+ import { execFileSync } from 'child_process';
5
+ import 'net';
6
+ import { fileURLToPath } from 'url';
7
+ import { createTRPCClient, httpLink } from '@trpc/client';
8
+ import crypto, { randomUUID, timingSafeEqual } from 'crypto';
9
+ import http from 'http';
10
+ import { createHTTPHandler } from '@trpc/server/adapters/standalone';
11
+ import { initTRPC, TRPCError } from '@trpc/server';
12
+ import { connect, FramerAPIError } from 'framer-api';
13
+ import { z } from 'zod';
14
+ import { createRequire } from 'module';
15
+ import * as vm from 'vm';
16
+
17
+ /* @framer/ai relay server v0.0.29 */
18
+ var __defProp = Object.defineProperty;
19
+ var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name2);
20
+ var __typeError = (msg) => {
21
+ throw TypeError(msg);
22
+ };
23
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
24
+ var __using = (stack, value, async) => {
25
+ if (value != null) {
26
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
27
+ var dispose, inner;
28
+ if (dispose === void 0) {
29
+ dispose = value[__knownSymbol("dispose")];
30
+ }
31
+ if (typeof dispose !== "function") __typeError("Object not disposable");
32
+ if (inner) dispose = function() {
33
+ try {
34
+ inner.call(this);
35
+ } catch (e) {
36
+ return Promise.reject(e);
37
+ }
38
+ };
39
+ stack.push([async, dispose, value]);
40
+ }
41
+ return value;
42
+ };
43
+ var __callDispose = (stack, error, hasError) => {
44
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
45
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
46
+ };
47
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
48
+ var next = (it) => {
49
+ while (it = stack.pop()) {
50
+ try {
51
+ var result = it[1] && it[1].call(it[2]);
52
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
53
+ } catch (e) {
54
+ fail(e);
55
+ }
56
+ }
57
+ if (hasError) throw error;
58
+ };
59
+ return next();
60
+ };
61
+
62
+ // package.json
63
+ var name = "@framer/agent";
64
+
65
+ // src/check-node-version.ts
66
+ var MINIMUM_NODE_VERSION = 22;
67
+ var currentMajor = Number(process.versions.node.split(".")[0]);
68
+ if (currentMajor < MINIMUM_NODE_VERSION) {
69
+ console.error(
70
+ `${name} requires Node.js >= ${MINIMUM_NODE_VERSION}. You are running Node.js ${process.versions.node}.
71
+ Please upgrade: https://nodejs.org/`
72
+ );
73
+ process.exit(1);
74
+ }
75
+ var DEFAULT_MAX_LOG_FILE_BYTES = 1024 * 1024;
76
+ var DEFAULT_RETAINED_LOG_FILE_BYTES = 512 * 1024;
77
+ var LOG_TRIM_CHECK_INTERVAL = 10;
78
+ var maxLogFileBytes = DEFAULT_MAX_LOG_FILE_BYTES;
79
+ var retainedLogFileBytes = DEFAULT_RETAINED_LOG_FILE_BYTES;
80
+ function getLogPath() {
81
+ if (process.env.XDG_STATE_HOME) {
82
+ return path5.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
83
+ }
84
+ if (process.platform === "win32") {
85
+ return path5.join(
86
+ process.env.APPDATA || os5.homedir(),
87
+ "framer",
88
+ "relay.log"
89
+ );
90
+ }
91
+ return path5.join(os5.homedir(), ".local", "state", "framer", "relay.log");
92
+ }
93
+ __name(getLogPath, "getLogPath");
94
+ var logPath = getLogPath();
95
+ var initialized = false;
96
+ var logEntriesSinceTrimCheck = LOG_TRIM_CHECK_INTERVAL - 1;
97
+ function ensureLogDir() {
98
+ if (initialized) return;
99
+ const dir = path5.dirname(logPath);
100
+ fs4.mkdirSync(dir, { recursive: true });
101
+ initialized = true;
102
+ }
103
+ __name(ensureLogDir, "ensureLogDir");
104
+ function trimLogFileIfNeeded() {
105
+ logEntriesSinceTrimCheck += 1;
106
+ if (logEntriesSinceTrimCheck < LOG_TRIM_CHECK_INTERVAL) return;
107
+ logEntriesSinceTrimCheck = 0;
108
+ let size;
109
+ try {
110
+ size = fs4.statSync(logPath).size;
111
+ } catch (err) {
112
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
113
+ return;
114
+ }
115
+ throw err;
116
+ }
117
+ if (size <= maxLogFileBytes) return;
118
+ const retainedBytes = Math.min(retainedLogFileBytes, size);
119
+ const buffer = Buffer.alloc(retainedBytes);
120
+ const file = fs4.openSync(logPath, "r");
121
+ let bytesRead = 0;
122
+ try {
123
+ bytesRead = fs4.readSync(
124
+ file,
125
+ buffer,
126
+ 0,
127
+ retainedBytes,
128
+ size - retainedBytes
129
+ );
130
+ } finally {
131
+ fs4.closeSync(file);
132
+ }
133
+ fs4.writeFileSync(logPath, buffer.subarray(0, bytesRead));
134
+ }
135
+ __name(trimLogFileIfNeeded, "trimLogFileIfNeeded");
136
+ function log(message) {
137
+ ensureLogDir();
138
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
139
+ fs4.appendFileSync(logPath, `${timestamp} ${message}
140
+ `);
141
+ trimLogFileIfNeeded();
142
+ }
143
+ __name(log, "log");
144
+ function getConfigDir() {
145
+ if (process.env.XDG_CONFIG_HOME) {
146
+ return path5.join(process.env.XDG_CONFIG_HOME, "framer");
147
+ }
148
+ if (process.platform === "win32") {
149
+ return path5.join(process.env.APPDATA || os5.homedir(), "framer");
150
+ }
151
+ return path5.join(os5.homedir(), ".config", "framer");
152
+ }
153
+ __name(getConfigDir, "getConfigDir");
154
+ function ensureConfigDir() {
155
+ const configDir = getConfigDir();
156
+ fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
157
+ }
158
+ __name(ensureConfigDir, "ensureConfigDir");
159
+
160
+ // src/config/relay-token.ts
161
+ function getRelayTokenPath() {
162
+ return path5.join(getConfigDir(), "relay-token");
163
+ }
164
+ __name(getRelayTokenPath, "getRelayTokenPath");
165
+ function generateRelayToken() {
166
+ return crypto.randomBytes(32).toString("base64url");
167
+ }
168
+ __name(generateRelayToken, "generateRelayToken");
169
+ function readRelayToken() {
170
+ try {
171
+ const token = fs4.readFileSync(getRelayTokenPath(), "utf-8").trim();
172
+ return token.length > 0 ? token : null;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ __name(readRelayToken, "readRelayToken");
178
+ function writeRelayToken(token) {
179
+ ensureConfigDir();
180
+ fs4.writeFileSync(getRelayTokenPath(), token, { mode: 384 });
181
+ }
182
+ __name(writeRelayToken, "writeRelayToken");
183
+ function clearRelayToken(expectedToken) {
184
+ const filePath = getRelayTokenPath();
185
+ if (expectedToken) {
186
+ const currentToken = readRelayToken();
187
+ if (currentToken !== expectedToken) {
188
+ return;
189
+ }
190
+ }
191
+ fs4.rmSync(filePath, { force: true });
192
+ }
193
+ __name(clearRelayToken, "clearRelayToken");
194
+ function debug(tag, message) {
195
+ return;
196
+ }
197
+ __name(debug, "debug");
198
+
199
+ // src/version.ts
200
+ var VERSION = (
201
+ // typeof is used to ensure this can be used just via tsx or node etc. without build
202
+ "0.0.29"
203
+ );
204
+
205
+ // src/relay-client.ts
206
+ var __filename$1 = fileURLToPath(import.meta.url);
207
+ path5.dirname(__filename$1);
208
+ var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
209
+ createTRPCClient({
210
+ links: [
211
+ httpLink({
212
+ url: `http://127.0.0.1:${RELAY_PORT}`,
213
+ headers() {
214
+ const token = readRelayToken();
215
+ if (!token) {
216
+ return {};
217
+ }
218
+ return { Authorization: `Bearer ${token}` };
219
+ }
220
+ })
221
+ ]
222
+ });
223
+
224
+ // src/agent-thread-context.ts
225
+ var MAX_AGENT_THREAD_ID_LENGTH = 256;
226
+ var MAX_AGENT_PROVIDER_LENGTH = 64;
227
+ var SAFE_VALUE_PATTERN = /^[A-Za-z0-9._:-]+$/;
228
+ function normalizeValue(value, maxLength) {
229
+ const trimmed = value?.trim();
230
+ if (!trimmed) return void 0;
231
+ if (trimmed.length > maxLength) return void 0;
232
+ if (!SAFE_VALUE_PATTERN.test(trimmed)) return void 0;
233
+ return trimmed;
234
+ }
235
+ __name(normalizeValue, "normalizeValue");
236
+ function normalizeAgentThreadContext(input) {
237
+ const agentThreadId = normalizeValue(
238
+ input.agentThreadId,
239
+ MAX_AGENT_THREAD_ID_LENGTH
240
+ );
241
+ if (!agentThreadId) return {};
242
+ const agentProvider = normalizeValue(
243
+ input.agentProvider,
244
+ MAX_AGENT_PROVIDER_LENGTH
245
+ );
246
+ return {
247
+ agentThreadId,
248
+ ...agentProvider ? { agentProvider } : {}
249
+ };
250
+ }
251
+ __name(normalizeAgentThreadContext, "normalizeAgentThreadContext");
252
+
253
+ // src/connection-errors.ts
254
+ var CONNECTION_ERROR_PATTERNS = [
255
+ "Connection closed",
256
+ // FramerAPIError PROJECT_CLOSED
257
+ "Connection to the server was closed",
258
+ // FramerAPIError PROJECT_CLOSED
259
+ "Connection timeout after",
260
+ // FramerAPIError TIMEOUT
261
+ "No connection to the server",
262
+ // FramerAPIError INTERNAL
263
+ "WebSocket upgrade failed",
264
+ // WebSocket handshake failure
265
+ "Session is not connected",
266
+ // Our own check in executor.ts
267
+ "Execution timed out after",
268
+ // Likely dead connection
269
+ "Session expired",
270
+ // Server-side session expiry
271
+ // Typed page-death (PROJECT_CLOSED) for the string fallback; isRetryableError is the primary path.
272
+ "Project session crashed",
273
+ // PageCrashedError (renderer crash / OOM)
274
+ "Project was closed",
275
+ // StaleSessionError (page closed/reaped)
276
+ "Session was recycled"
277
+ // SessionRecycledError (destroy->respawn reconnect race)
278
+ ];
279
+ function isConnectionError(errorMessage) {
280
+ return CONNECTION_ERROR_PATTERNS.some(
281
+ (pattern) => errorMessage.includes(pattern)
282
+ );
283
+ }
284
+ __name(isConnectionError, "isConnectionError");
285
+ function isRetryableError(err) {
286
+ if (err && typeof err === "object") {
287
+ const e = err;
288
+ if (e.retryable === true) return true;
289
+ if (e.code === "PROJECT_CLOSED") return true;
290
+ if (typeof e.message === "string" && isConnectionError(e.message))
291
+ return true;
292
+ return false;
293
+ }
294
+ return typeof err === "string" && isConnectionError(err);
295
+ }
296
+ __name(isRetryableError, "isRetryableError");
297
+ var AUTH_ERROR_PATTERNS = ["does not have access", "UNAUTHORIZED"];
298
+ function isAuthError(errorMessage) {
299
+ return AUTH_ERROR_PATTERNS.some((p) => errorMessage.includes(p));
300
+ }
301
+ __name(isAuthError, "isAuthError");
302
+ var ScopedFS = class {
303
+ static {
304
+ __name(this, "ScopedFS");
305
+ }
306
+ allowedRoots;
307
+ constructor(allowedDirs) {
308
+ const defaultDirs = [process.cwd(), "/tmp", os5.tmpdir()];
309
+ const dirs = allowedDirs ?? defaultDirs;
310
+ this.allowedRoots = [
311
+ ...new Set(dirs.map((dir) => this.canonicalizePath(dir)))
312
+ ];
313
+ }
314
+ isPathAllowed(candidatePath) {
315
+ return this.allowedRoots.some((allowedRoot) => {
316
+ return candidatePath === allowedRoot || candidatePath.startsWith(allowedRoot + path5.sep);
317
+ });
318
+ }
319
+ resolvePath(filePath) {
320
+ const resolvedPath = path5.resolve(filePath);
321
+ const canonicalPath = this.canonicalizePath(resolvedPath);
322
+ if (!this.isPathAllowed(canonicalPath)) {
323
+ this.throwOutsideAllowedRoots(filePath);
324
+ }
325
+ return resolvedPath;
326
+ }
327
+ canonicalizePath(inputPath) {
328
+ const resolvedPath = path5.resolve(inputPath);
329
+ const missingSegments = [];
330
+ let ancestorPath = resolvedPath;
331
+ while (true) {
332
+ const realPath = this.resolveRealPath(ancestorPath);
333
+ if (realPath !== null) {
334
+ return path5.resolve(realPath, ...missingSegments);
335
+ }
336
+ if (this.isSymbolicLink(ancestorPath)) {
337
+ this.throwOutsideAllowedRoots(inputPath);
338
+ }
339
+ const parentPath = path5.dirname(ancestorPath);
340
+ if (parentPath === ancestorPath) {
341
+ return resolvedPath;
342
+ }
343
+ missingSegments.unshift(path5.basename(ancestorPath));
344
+ ancestorPath = parentPath;
345
+ }
346
+ }
347
+ resolveRealPath(targetPath) {
348
+ try {
349
+ return path5.resolve(fs4.realpathSync.native(targetPath));
350
+ } catch (error) {
351
+ const errnoError = error;
352
+ if (errnoError.code === "ENOENT") {
353
+ return null;
354
+ }
355
+ throw error;
356
+ }
357
+ }
358
+ isSymbolicLink(targetPath) {
359
+ try {
360
+ return fs4.lstatSync(targetPath).isSymbolicLink();
361
+ } catch {
362
+ return false;
363
+ }
364
+ }
365
+ throwOutsideAllowedRoots(filePath) {
366
+ const error = new Error(
367
+ `EPERM: operation not permitted, access outside allowed directories: ${filePath}`
368
+ );
369
+ error.code = "EPERM";
370
+ error.errno = -1;
371
+ error.syscall = "access";
372
+ error.path = filePath;
373
+ throw error;
374
+ }
375
+ // Sync methods
376
+ readFileSync = /* @__PURE__ */ __name((filePath, options) => {
377
+ const resolved = this.resolvePath(filePath.toString());
378
+ return fs4.readFileSync(resolved, options);
379
+ }, "readFileSync");
380
+ writeFileSync = /* @__PURE__ */ __name((filePath, data, options) => {
381
+ const resolved = this.resolvePath(filePath.toString());
382
+ fs4.writeFileSync(
383
+ resolved,
384
+ data,
385
+ options
386
+ );
387
+ }, "writeFileSync");
388
+ appendFileSync = /* @__PURE__ */ __name((filePath, data, options) => {
389
+ const resolved = this.resolvePath(filePath.toString());
390
+ fs4.appendFileSync(
391
+ resolved,
392
+ data,
393
+ options
394
+ );
395
+ }, "appendFileSync");
396
+ readdirSync = /* @__PURE__ */ __name((dirPath, options) => {
397
+ const resolved = this.resolvePath(dirPath.toString());
398
+ return fs4.readdirSync(resolved, options);
399
+ }, "readdirSync");
400
+ mkdirSync = /* @__PURE__ */ __name((dirPath, options) => {
401
+ const resolved = this.resolvePath(dirPath.toString());
402
+ return fs4.mkdirSync(resolved, options);
403
+ }, "mkdirSync");
404
+ rmdirSync = /* @__PURE__ */ __name((dirPath, options) => {
405
+ const resolved = this.resolvePath(dirPath.toString());
406
+ fs4.rmdirSync(resolved, options);
407
+ }, "rmdirSync");
408
+ unlinkSync = /* @__PURE__ */ __name((filePath) => {
409
+ const resolved = this.resolvePath(filePath.toString());
410
+ fs4.unlinkSync(resolved);
411
+ }, "unlinkSync");
412
+ statSync = /* @__PURE__ */ __name((filePath, options) => {
413
+ const resolved = this.resolvePath(filePath.toString());
414
+ return fs4.statSync(resolved, options);
415
+ }, "statSync");
416
+ lstatSync = /* @__PURE__ */ __name((filePath, options) => {
417
+ const resolved = this.resolvePath(filePath.toString());
418
+ return fs4.lstatSync(resolved, options);
419
+ }, "lstatSync");
420
+ existsSync = /* @__PURE__ */ __name((filePath) => {
421
+ try {
422
+ const resolved = this.resolvePath(filePath.toString());
423
+ return fs4.existsSync(resolved);
424
+ } catch {
425
+ return false;
426
+ }
427
+ }, "existsSync");
428
+ accessSync = /* @__PURE__ */ __name((filePath, mode) => {
429
+ const resolved = this.resolvePath(filePath.toString());
430
+ fs4.accessSync(resolved, mode);
431
+ }, "accessSync");
432
+ copyFileSync = /* @__PURE__ */ __name((src, dest, mode) => {
433
+ const resolvedSrc = this.resolvePath(src.toString());
434
+ const resolvedDest = this.resolvePath(dest.toString());
435
+ fs4.copyFileSync(resolvedSrc, resolvedDest, mode);
436
+ }, "copyFileSync");
437
+ renameSync = /* @__PURE__ */ __name((oldPath, newPath) => {
438
+ const resolvedOld = this.resolvePath(oldPath.toString());
439
+ const resolvedNew = this.resolvePath(newPath.toString());
440
+ fs4.renameSync(resolvedOld, resolvedNew);
441
+ }, "renameSync");
442
+ rmSync = /* @__PURE__ */ __name((filePath, options) => {
443
+ const resolved = this.resolvePath(filePath.toString());
444
+ fs4.rmSync(resolved, options);
445
+ }, "rmSync");
446
+ // Stream methods
447
+ createReadStream = /* @__PURE__ */ __name((filePath, options) => {
448
+ const resolved = this.resolvePath(filePath.toString());
449
+ return fs4.createReadStream(resolved, options);
450
+ }, "createReadStream");
451
+ createWriteStream = /* @__PURE__ */ __name((filePath, options) => {
452
+ const resolved = this.resolvePath(filePath.toString());
453
+ return fs4.createWriteStream(resolved, options);
454
+ }, "createWriteStream");
455
+ // Promise-based API (fs.promises equivalent)
456
+ get promises() {
457
+ return {
458
+ readFile: /* @__PURE__ */ __name(async (filePath, options) => {
459
+ const resolved = this.resolvePath(filePath.toString());
460
+ return fs4.promises.readFile(
461
+ resolved,
462
+ options
463
+ );
464
+ }, "readFile"),
465
+ writeFile: /* @__PURE__ */ __name(async (filePath, data, options) => {
466
+ const resolved = this.resolvePath(filePath.toString());
467
+ return fs4.promises.writeFile(
468
+ resolved,
469
+ data,
470
+ options
471
+ );
472
+ }, "writeFile"),
473
+ appendFile: /* @__PURE__ */ __name(async (filePath, data, options) => {
474
+ const resolved = this.resolvePath(filePath.toString());
475
+ return fs4.promises.appendFile(
476
+ resolved,
477
+ data,
478
+ options
479
+ );
480
+ }, "appendFile"),
481
+ readdir: /* @__PURE__ */ __name(async (dirPath, options) => {
482
+ const resolved = this.resolvePath(dirPath.toString());
483
+ return fs4.promises.readdir(
484
+ resolved,
485
+ options
486
+ );
487
+ }, "readdir"),
488
+ mkdir: /* @__PURE__ */ __name(async (dirPath, options) => {
489
+ const resolved = this.resolvePath(dirPath.toString());
490
+ return fs4.promises.mkdir(resolved, options);
491
+ }, "mkdir"),
492
+ rmdir: /* @__PURE__ */ __name(async (dirPath, options) => {
493
+ const resolved = this.resolvePath(dirPath.toString());
494
+ return fs4.promises.rmdir(resolved, options);
495
+ }, "rmdir"),
496
+ unlink: /* @__PURE__ */ __name(async (filePath) => {
497
+ const resolved = this.resolvePath(filePath.toString());
498
+ return fs4.promises.unlink(resolved);
499
+ }, "unlink"),
500
+ stat: /* @__PURE__ */ __name(async (filePath, options) => {
501
+ const resolved = this.resolvePath(filePath.toString());
502
+ return fs4.promises.stat(resolved, options);
503
+ }, "stat"),
504
+ access: /* @__PURE__ */ __name(async (filePath, mode) => {
505
+ const resolved = this.resolvePath(filePath.toString());
506
+ return fs4.promises.access(resolved, mode);
507
+ }, "access"),
508
+ copyFile: /* @__PURE__ */ __name(async (src, dest, mode) => {
509
+ const resolvedSrc = this.resolvePath(src.toString());
510
+ const resolvedDest = this.resolvePath(dest.toString());
511
+ return fs4.promises.copyFile(resolvedSrc, resolvedDest, mode);
512
+ }, "copyFile"),
513
+ rename: /* @__PURE__ */ __name(async (oldPath, newPath) => {
514
+ const resolvedOld = this.resolvePath(oldPath.toString());
515
+ const resolvedNew = this.resolvePath(newPath.toString());
516
+ return fs4.promises.rename(resolvedOld, resolvedNew);
517
+ }, "rename"),
518
+ rm: /* @__PURE__ */ __name(async (filePath, options) => {
519
+ const resolved = this.resolvePath(filePath.toString());
520
+ return fs4.promises.rm(resolved, options);
521
+ }, "rm")
522
+ };
523
+ }
524
+ constants = fs4.constants;
525
+ };
526
+ function getErrorRef(error) {
527
+ if (!(error instanceof Error)) return void 0;
528
+ const ref = error.ref;
529
+ return typeof ref === "string" && ref.length > 0 ? ref : void 0;
530
+ }
531
+ __name(getErrorRef, "getErrorRef");
532
+ function formatError(error) {
533
+ if (error instanceof Error) {
534
+ return error.message;
535
+ }
536
+ return String(error);
537
+ }
538
+ __name(formatError, "formatError");
539
+
540
+ // src/execute.ts
541
+ var EXECUTION_TIMEOUT = 10 * 60 * 1e3;
542
+ var baseRequire = createRequire(import.meta.url);
543
+ var ALLOWED_MODULES = /* @__PURE__ */ new Set([
544
+ "path",
545
+ "node:path",
546
+ "url",
547
+ "node:url",
548
+ "fs",
549
+ "node:fs",
550
+ "fs/promises",
551
+ "node:fs/promises",
552
+ "crypto",
553
+ "node:crypto",
554
+ "buffer",
555
+ "node:buffer",
556
+ "util",
557
+ "node:util",
558
+ "os",
559
+ "node:os"
560
+ ]);
561
+ function createSandboxedRequire(scopedFs) {
562
+ const sandboxedRequire = /* @__PURE__ */ __name(((id) => {
563
+ if (!ALLOWED_MODULES.has(id)) {
564
+ const error = new Error(
565
+ `Module "${id}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
566
+ );
567
+ error.name = "ModuleNotAllowedError";
568
+ throw error;
569
+ }
570
+ if (id === "fs" || id === "node:fs") {
571
+ return scopedFs;
572
+ }
573
+ if (id === "fs/promises" || id === "node:fs/promises") {
574
+ return scopedFs.promises;
575
+ }
576
+ return baseRequire(id);
577
+ }), "sandboxedRequire");
578
+ sandboxedRequire.resolve = baseRequire.resolve;
579
+ sandboxedRequire.cache = baseRequire.cache;
580
+ sandboxedRequire.extensions = baseRequire.extensions;
581
+ sandboxedRequire.main = baseRequire.main;
582
+ return sandboxedRequire;
583
+ }
584
+ __name(createSandboxedRequire, "createSandboxedRequire");
585
+ async function sandboxedImport(scopedFs, specifier) {
586
+ if (!ALLOWED_MODULES.has(specifier)) {
587
+ const error = new Error(
588
+ `Module "${specifier}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
589
+ );
590
+ error.name = "ModuleNotAllowedError";
591
+ throw error;
592
+ }
593
+ if (specifier === "fs" || specifier === "node:fs") {
594
+ return scopedFs;
595
+ }
596
+ if (specifier === "fs/promises" || specifier === "node:fs/promises") {
597
+ return scopedFs.promises;
598
+ }
599
+ return import(specifier);
600
+ }
601
+ __name(sandboxedImport, "sandboxedImport");
602
+ async function execute(session, code, options = {}) {
603
+ const { cwd } = options;
604
+ const output = [];
605
+ const customConsole = {
606
+ log: /* @__PURE__ */ __name((...args) => {
607
+ output.push(args.map((arg) => formatValue(arg)).join(" "));
608
+ }, "log"),
609
+ error: /* @__PURE__ */ __name((...args) => {
610
+ output.push(`[ERROR] ${args.map((arg) => formatValue(arg)).join(" ")}`);
611
+ }, "error"),
612
+ warn: /* @__PURE__ */ __name((...args) => {
613
+ output.push(`[WARN] ${args.map((arg) => formatValue(arg)).join(" ")}`);
614
+ }, "warn"),
615
+ info: /* @__PURE__ */ __name((...args) => {
616
+ output.push(args.map((arg) => formatValue(arg)).join(" "));
617
+ }, "info")
618
+ };
619
+ const scopedFs = cwd ? new ScopedFS([cwd, "/tmp", os5.tmpdir()]) : new ScopedFS();
620
+ const sandboxedRequire = createSandboxedRequire(scopedFs);
621
+ const vmContextObj = {
622
+ // Framer API
623
+ framer: session.connection.framer,
624
+ state: session.state,
625
+ // Console
626
+ console: customConsole,
627
+ // Module system (sandboxed)
628
+ require: sandboxedRequire,
629
+ import: /* @__PURE__ */ __name((specifier) => sandboxedImport(scopedFs, specifier), "import"),
630
+ // Timers
631
+ setTimeout,
632
+ clearTimeout,
633
+ setInterval,
634
+ clearInterval,
635
+ // Fetch & network
636
+ fetch,
637
+ // Common globals
638
+ Buffer,
639
+ URL,
640
+ URLSearchParams,
641
+ TextEncoder,
642
+ TextDecoder,
643
+ crypto: crypto,
644
+ AbortController,
645
+ AbortSignal,
646
+ structuredClone
647
+ };
648
+ const vmContext = vm.createContext(vmContextObj);
649
+ const wrappedCode = `(async () => { ${code} })()`;
650
+ let timeoutId;
651
+ try {
652
+ const script = new vm.Script(wrappedCode, {
653
+ filename: "framer-exec.js"
654
+ });
655
+ const resultPromise = script.runInContext(vmContext, {
656
+ timeout: 5e3
657
+ });
658
+ const result = await Promise.race([
659
+ resultPromise,
660
+ new Promise((_, reject) => {
661
+ timeoutId = setTimeout(
662
+ () => reject(
663
+ new Error(`Execution timed out after ${EXECUTION_TIMEOUT}ms`)
664
+ ),
665
+ EXECUTION_TIMEOUT
666
+ );
667
+ })
668
+ ]);
669
+ if (result !== void 0) {
670
+ output.push(formatValue(result));
671
+ }
672
+ return { output };
673
+ } catch (err) {
674
+ const errorMessage = err instanceof Error ? err.message : String(err);
675
+ const retryable = isRetryableError(err);
676
+ if (retryable || isConnectionError(errorMessage)) {
677
+ session.connection.markDisconnected();
678
+ }
679
+ const errorRef = getErrorRef(err);
680
+ return {
681
+ output,
682
+ error: errorMessage,
683
+ ...errorRef ? { errorRef } : {},
684
+ ...retryable ? { retryable: true } : {}
685
+ };
686
+ } finally {
687
+ if (timeoutId) clearTimeout(timeoutId);
688
+ }
689
+ }
690
+ __name(execute, "execute");
691
+ function isReconnectable(err) {
692
+ return isRetryableError(err) || isConnectionError(err instanceof Error ? err.message : String(err));
693
+ }
694
+ __name(isReconnectable, "isReconnectable");
695
+ function connectionFailureResult(prefix, err) {
696
+ const inner = err instanceof Error ? err.message : String(err);
697
+ const errorRef = getErrorRef(err);
698
+ return {
699
+ output: [],
700
+ error: `${prefix}: ${inner}`,
701
+ ...errorRef ? { errorRef } : {},
702
+ ...isRetryableError(err) ? { retryable: true } : {}
703
+ };
704
+ }
705
+ __name(connectionFailureResult, "connectionFailureResult");
706
+ async function executeWithReconnect(session, code, options, execId) {
707
+ if (!session.connection.isConnected()) {
708
+ log(
709
+ `exec.reconnect exec=${execId} session=${session.id} reason="idle disconnected"`
710
+ );
711
+ try {
712
+ await session.connection.reconnect();
713
+ } catch (err) {
714
+ if (!isReconnectable(err)) {
715
+ return connectionFailureResult(
716
+ "Failed to get connection for session",
717
+ err
718
+ );
719
+ }
720
+ log(
721
+ `exec.reconnect.retry exec=${execId} session=${session.id} reason="${err instanceof Error ? err.message : String(err)}"`
722
+ );
723
+ try {
724
+ await session.connection.reconnect();
725
+ } catch (err2) {
726
+ return connectionFailureResult(
727
+ "Failed to get connection for session",
728
+ err2
729
+ );
730
+ }
731
+ }
732
+ }
733
+ const result = await execute(session, code, options);
734
+ if (!result.error || !(result.retryable || isConnectionError(result.error))) {
735
+ return result;
736
+ }
737
+ log(
738
+ `reconnect exec=${execId} session=${session.id} req=${session.connection.framer.requestId} reason="${result.error}"`
739
+ );
740
+ try {
741
+ await session.connection.reconnect();
742
+ } catch (err) {
743
+ log(
744
+ `reconnect.failed exec=${execId} session=${session.id} req=${session.connection.framer.requestId} error="${err instanceof Error ? err.message : String(err)}"`
745
+ );
746
+ return connectionFailureResult(
747
+ "Connection lost and failed to reconnect",
748
+ err
749
+ );
750
+ }
751
+ log(
752
+ `reconnect.success exec=${execId} session=${session.id} req=${session.connection.framer.requestId}`
753
+ );
754
+ return execute(session, code, options);
755
+ }
756
+ __name(executeWithReconnect, "executeWithReconnect");
757
+ function formatValue(value) {
758
+ if (value === null) return "null";
759
+ if (value === void 0) return "undefined";
760
+ if (typeof value === "string") return value;
761
+ if (typeof value === "number" || typeof value === "boolean")
762
+ return String(value);
763
+ if (typeof value === "function")
764
+ return `[Function: ${value.name || "anonymous"}]`;
765
+ if (value instanceof Error) return value.message;
766
+ if (value instanceof Date) return value.toISOString();
767
+ if (value instanceof Map) return `Map(${value.size})`;
768
+ if (value instanceof Set) return `Set(${value.size})`;
769
+ if (Buffer.isBuffer(value)) return `Buffer(${value.length})`;
770
+ try {
771
+ return JSON.stringify(value, null, 2);
772
+ } catch {
773
+ return String(value);
774
+ }
775
+ }
776
+ __name(formatValue, "formatValue");
777
+ var DEFAULT_FS_POLL_INTERVAL_MS = 1e3;
778
+ var fsPollIntervalMs = DEFAULT_FS_POLL_INTERVAL_MS;
779
+ var SettingsWatcher = class {
780
+ constructor(settingsPath) {
781
+ this.settingsPath = settingsPath;
782
+ }
783
+ settingsPath;
784
+ static {
785
+ __name(this, "SettingsWatcher");
786
+ }
787
+ start(callback) {
788
+ fs4.watchFile(
789
+ this.settingsPath,
790
+ { persistent: false, interval: fsPollIntervalMs },
791
+ (curr, prev) => {
792
+ if (
793
+ // File was modified
794
+ curr.mtimeMs !== prev.mtimeMs || // File was renamed and replaced by a new file with the same name
795
+ curr.ino !== prev.ino || // File changed without any of the above changing for some reason?
796
+ curr.size !== prev.size
797
+ ) {
798
+ callback();
799
+ }
800
+ }
801
+ );
802
+ return this;
803
+ }
804
+ stop() {
805
+ fs4.unwatchFile(this.settingsPath);
806
+ return this;
807
+ }
808
+ };
809
+
810
+ // src/config/settings.ts
811
+ var DEFAULT_MACHINE_ID = "unknown-machine-id-this-should-never-be-persisted";
812
+ var DEFAULT_SETTINGS = {
813
+ machineId: DEFAULT_MACHINE_ID,
814
+ telemetryEnabled: true,
815
+ telemetryNoticeShown: false
816
+ };
817
+ var SettingsFileSchema = z.object({
818
+ machineId: z.string().optional(),
819
+ telemetryEnabled: z.boolean().optional(),
820
+ telemetryNoticeShown: z.boolean().optional()
821
+ });
822
+ function getSettingsPath() {
823
+ return path5.join(getConfigDir(), "settings.json");
824
+ }
825
+ __name(getSettingsPath, "getSettingsPath");
826
+ var settings;
827
+ var settingsWatcher;
828
+ function getSettings() {
829
+ if (settings) return settings;
830
+ settings = readSettingsFile();
831
+ if (!settingsWatcher) {
832
+ settingsWatcher = new SettingsWatcher(getSettingsPath()).start(() => {
833
+ settings = void 0;
834
+ });
835
+ }
836
+ return settings;
837
+ }
838
+ __name(getSettings, "getSettings");
839
+ function readSettingsFile() {
840
+ const settingsPath = getSettingsPath();
841
+ try {
842
+ const raw = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
843
+ const result = SettingsFileSchema.parse(raw);
844
+ return {
845
+ machineId: result.machineId ?? DEFAULT_SETTINGS.machineId,
846
+ telemetryEnabled: result.telemetryEnabled ?? DEFAULT_SETTINGS.telemetryEnabled,
847
+ telemetryNoticeShown: result.telemetryNoticeShown ?? DEFAULT_SETTINGS.telemetryNoticeShown
848
+ };
849
+ } catch {
850
+ return { ...DEFAULT_SETTINGS };
851
+ }
852
+ }
853
+ __name(readSettingsFile, "readSettingsFile");
854
+ function setSettings(newSettings) {
855
+ settings = newSettings;
856
+ writeSettingsFile(newSettings);
857
+ }
858
+ __name(setSettings, "setSettings");
859
+ function writeSettingsFile(settings2) {
860
+ ensureConfigDir();
861
+ fs4.writeFileSync(getSettingsPath(), JSON.stringify(settings2, null, " "), {
862
+ mode: 384
863
+ });
864
+ }
865
+ __name(writeSettingsFile, "writeSettingsFile");
866
+ function getMachineId() {
867
+ const settings2 = getSettings();
868
+ if (settings2.machineId === DEFAULT_MACHINE_ID) {
869
+ const machineId = crypto.randomUUID();
870
+ setSettings({ ...settings2, machineId });
871
+ }
872
+ return getSettings().machineId;
873
+ }
874
+ __name(getMachineId, "getMachineId");
875
+ function isTelemetryEnabled() {
876
+ return getSettings().telemetryEnabled;
877
+ }
878
+ __name(isTelemetryEnabled, "isTelemetryEnabled");
879
+ var trackingEndpoint = "https://events.framer.com/track";
880
+ var inProgressTrackings = /* @__PURE__ */ new Set();
881
+ var cachedSharedFields;
882
+ function getOsName(platform) {
883
+ switch (platform) {
884
+ case "darwin":
885
+ return "macos";
886
+ case "win32":
887
+ return "windows";
888
+ case "linux":
889
+ return "linux";
890
+ default:
891
+ return "unknown";
892
+ }
893
+ }
894
+ __name(getOsName, "getOsName");
895
+ function getMacOsVersion() {
896
+ try {
897
+ const version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], {
898
+ encoding: "utf8",
899
+ timeout: 1e3
900
+ }).trim();
901
+ return version || "unknown";
902
+ } catch {
903
+ return "unknown";
904
+ }
905
+ }
906
+ __name(getMacOsVersion, "getMacOsVersion");
907
+ function getOsVersion(platform) {
908
+ if (platform === "darwin") return getMacOsVersion();
909
+ const version = os5.release().trim();
910
+ return version || "unknown";
911
+ }
912
+ __name(getOsVersion, "getOsVersion");
913
+ function getOsMetadata() {
914
+ const platform = os5.platform();
915
+ const arch = os5.arch();
916
+ return {
917
+ arch: arch || "unknown",
918
+ osName: getOsName(platform),
919
+ osVersion: getOsVersion(platform)
920
+ };
921
+ }
922
+ __name(getOsMetadata, "getOsMetadata");
923
+ function sharedFields() {
924
+ cachedSharedFields ??= {
925
+ machineId: getMachineId(),
926
+ cliVersion: VERSION,
927
+ ...getOsMetadata()
928
+ };
929
+ return cachedSharedFields;
930
+ }
931
+ __name(sharedFields, "sharedFields");
932
+ function wrapEvent(event) {
933
+ return {
934
+ source: "framer-dalton",
935
+ timestamp: Date.now(),
936
+ type: "track",
937
+ uuid: randomUUID(),
938
+ data: event
939
+ };
940
+ }
941
+ __name(wrapEvent, "wrapEvent");
942
+ function postEvent(createEvent) {
943
+ if (!isTelemetryEnabled()) return false;
944
+ if (process.env.NODE_ENV === "test" && true) return false;
945
+ const event = createEvent();
946
+ const body = JSON.stringify([wrapEvent(event)]);
947
+ debug("tracking", `sending ${event.event} to ${trackingEndpoint}: ${body}`);
948
+ const promise = fetch(trackingEndpoint, {
949
+ method: "POST",
950
+ headers: {
951
+ "Content-Type": "application/json",
952
+ "User-Agent": `framer-dalton/${VERSION}`
953
+ },
954
+ body,
955
+ signal: AbortSignal.timeout(
956
+ 5e3
957
+ /* 5 seconds */
958
+ )
959
+ }).then((response) => {
960
+ if (!response.ok) {
961
+ debug(
962
+ "tracking",
963
+ `failed to send ${event.event}: HTTP ${response.status}`
964
+ );
965
+ }
966
+ }).catch((error) => {
967
+ debug(
968
+ "tracking",
969
+ `failed to send ${event.event}: ${error instanceof Error ? error.message : String(error)}`
970
+ );
971
+ }).finally(() => {
972
+ inProgressTrackings.delete(promise);
973
+ });
974
+ inProgressTrackings.add(promise);
975
+ return true;
976
+ }
977
+ __name(postEvent, "postEvent");
978
+ function waitForTrackingToFinish() {
979
+ return Promise.allSettled(Array.from(inProgressTrackings));
980
+ }
981
+ __name(waitForTrackingToFinish, "waitForTrackingToFinish");
982
+ function trackRelayStart() {
983
+ postEvent(
984
+ () => ({
985
+ event: "local_agents_relay_start",
986
+ ...sharedFields()
987
+ })
988
+ );
989
+ }
990
+ __name(trackRelayStart, "trackRelayStart");
991
+ var relayShutdownTracked = false;
992
+ function trackRelayShutdown() {
993
+ if (relayShutdownTracked) return;
994
+ relayShutdownTracked = postEvent(
995
+ () => ({
996
+ event: "local_agents_relay_shutdown",
997
+ ...sharedFields()
998
+ })
999
+ );
1000
+ }
1001
+ __name(trackRelayShutdown, "trackRelayShutdown");
1002
+ function trackSessionCreate(payload) {
1003
+ postEvent(
1004
+ () => ({
1005
+ event: "local_agents_session_create",
1006
+ ...sharedFields(),
1007
+ ...payload
1008
+ })
1009
+ );
1010
+ }
1011
+ __name(trackSessionCreate, "trackSessionCreate");
1012
+ function trackSessionDestroy(payload) {
1013
+ postEvent(
1014
+ () => ({
1015
+ event: "local_agents_session_destroy",
1016
+ ...sharedFields(),
1017
+ ...payload
1018
+ })
1019
+ );
1020
+ }
1021
+ __name(trackSessionDestroy, "trackSessionDestroy");
1022
+ function trackExec(payload) {
1023
+ postEvent(
1024
+ () => ({
1025
+ event: "local_agents_exec",
1026
+ ...sharedFields(),
1027
+ ...payload
1028
+ })
1029
+ );
1030
+ }
1031
+ __name(trackExec, "trackExec");
1032
+ function trackConnectionAcquire(payload) {
1033
+ postEvent(
1034
+ () => ({
1035
+ event: "local_agents_connection_acquire",
1036
+ ...sharedFields(),
1037
+ ...payload
1038
+ })
1039
+ );
1040
+ }
1041
+ __name(trackConnectionAcquire, "trackConnectionAcquire");
1042
+ function trackConnectionReconnect(payload) {
1043
+ postEvent(
1044
+ () => ({
1045
+ event: "local_agents_connection_reconnect",
1046
+ ...sharedFields(),
1047
+ ...payload
1048
+ })
1049
+ );
1050
+ }
1051
+ __name(trackConnectionReconnect, "trackConnectionReconnect");
1052
+ function trackConnectionIdleDisconnect(payload) {
1053
+ postEvent(
1054
+ () => ({
1055
+ event: "local_agents_connection_idle_disconnect",
1056
+ ...sharedFields(),
1057
+ ...payload
1058
+ })
1059
+ );
1060
+ }
1061
+ __name(trackConnectionIdleDisconnect, "trackConnectionIdleDisconnect");
1062
+ function trackError(payload) {
1063
+ postEvent(
1064
+ () => ({
1065
+ event: "local_agents_error",
1066
+ ...sharedFields(),
1067
+ ...payload
1068
+ })
1069
+ );
1070
+ }
1071
+ __name(trackError, "trackError");
1072
+
1073
+ // src/session-manager.ts
1074
+ var SESSION_IDLE_TIMEOUT_MS = 60 * 1e3;
1075
+ var SESSION_IDLE_CHECK_INTERVAL_MS = 30 * 1e3;
1076
+ var MISSING_SERVER_SESSION_ID = "missing-session-id-should-not-happen";
1077
+ var Connection = class _Connection {
1078
+ constructor(projectId, userId, apiKey, headlessServerUrl, framer) {
1079
+ this.projectId = projectId;
1080
+ this.userId = userId;
1081
+ this.apiKey = apiKey;
1082
+ this.headlessServerUrl = headlessServerUrl;
1083
+ this.framer = framer;
1084
+ }
1085
+ projectId;
1086
+ userId;
1087
+ apiKey;
1088
+ headlessServerUrl;
1089
+ framer;
1090
+ static {
1091
+ __name(this, "Connection");
1092
+ }
1093
+ sessions = [];
1094
+ status = "connected";
1095
+ pendingReconnect;
1096
+ static async open(projectId, userId, apiKey, headlessServerUrl) {
1097
+ const framer = await connect(projectId, apiKey, {
1098
+ clientId: `dalton/${VERSION}`,
1099
+ serverUrl: headlessServerUrl
1100
+ });
1101
+ return new _Connection(projectId, userId, apiKey, headlessServerUrl, framer);
1102
+ }
1103
+ isConnected() {
1104
+ return this.status === "connected";
1105
+ }
1106
+ markDisconnected() {
1107
+ this.status = "disconnected";
1108
+ }
1109
+ getServerSessionId() {
1110
+ return this.framer.sessionId ?? MISSING_SERVER_SESSION_ID;
1111
+ }
1112
+ listSessionInfo() {
1113
+ return this.sessions.map((session) => ({
1114
+ id: session.id,
1115
+ projectId: this.projectId,
1116
+ stateKeys: Object.keys(session.state),
1117
+ headlessServerUrl: this.headlessServerUrl
1118
+ }));
1119
+ }
1120
+ createSession(id, agentThreadContext) {
1121
+ const session = {
1122
+ id,
1123
+ state: {},
1124
+ agentThreadContext,
1125
+ lastActivityAt: Date.now(),
1126
+ inflight: 0,
1127
+ connection: this
1128
+ };
1129
+ this.sessions.push(session);
1130
+ return session;
1131
+ }
1132
+ findSession(id) {
1133
+ return this.sessions.find((session) => session.id === id);
1134
+ }
1135
+ removeSession(id) {
1136
+ const index = this.sessions.findIndex((session) => session.id === id);
1137
+ if (index >= 0) {
1138
+ this.sessions.splice(index, 1);
1139
+ }
1140
+ }
1141
+ hasSessions() {
1142
+ return this.sessions.length > 0;
1143
+ }
1144
+ clearSessions() {
1145
+ this.sessions.length = 0;
1146
+ }
1147
+ isIdle(now, timeoutMs) {
1148
+ return this.sessions.every(
1149
+ (session) => session.inflight === 0 && now - session.lastActivityAt >= timeoutMs
1150
+ );
1151
+ }
1152
+ async reconnect() {
1153
+ if (this.status === "connected") {
1154
+ return;
1155
+ }
1156
+ if (this.pendingReconnect) {
1157
+ return this.pendingReconnect;
1158
+ }
1159
+ this.status = "reconnecting";
1160
+ this.pendingReconnect = this.framer.reconnect().then(() => {
1161
+ this.status = "connected";
1162
+ trackConnectionReconnect({
1163
+ projectId: this.projectId,
1164
+ userId: this.userId,
1165
+ sessionId: this.getServerSessionId()
1166
+ });
1167
+ }).catch((error) => {
1168
+ this.status = "disconnected";
1169
+ throw error;
1170
+ }).finally(() => {
1171
+ this.pendingReconnect = void 0;
1172
+ });
1173
+ return this.pendingReconnect;
1174
+ }
1175
+ async disconnect() {
1176
+ if (this.pendingReconnect) {
1177
+ try {
1178
+ await this.pendingReconnect;
1179
+ } catch {
1180
+ }
1181
+ }
1182
+ if (this.status === "connected") {
1183
+ await this.framer.disconnect();
1184
+ }
1185
+ this.status = "disconnected";
1186
+ this.pendingReconnect = void 0;
1187
+ }
1188
+ };
1189
+ var SessionManager = class {
1190
+ static {
1191
+ __name(this, "SessionManager");
1192
+ }
1193
+ connections = [];
1194
+ idleCheck = null;
1195
+ async create(projectId, userId, apiKey, headlessServerUrl, agentThreadContext = {}) {
1196
+ const connection = await this.findOrCreateConnection(
1197
+ projectId,
1198
+ userId,
1199
+ apiKey,
1200
+ headlessServerUrl
1201
+ );
1202
+ const session = connection.createSession(
1203
+ this.getNextSessionId(),
1204
+ agentThreadContext
1205
+ );
1206
+ this.startIdleCheck();
1207
+ return session;
1208
+ }
1209
+ list() {
1210
+ return this.connections.flatMap(
1211
+ (connection) => connection.listSessionInfo()
1212
+ );
1213
+ }
1214
+ get(id) {
1215
+ return this.findSession(id);
1216
+ }
1217
+ async execute(id, code, options, execId) {
1218
+ var _stack = [];
1219
+ try {
1220
+ const session = this.findSession(id);
1221
+ if (!session) {
1222
+ return void 0;
1223
+ }
1224
+ log(
1225
+ `exec exec=${execId} session=${id} req=${session.connection.framer.requestId} code=${JSON.stringify(code).slice(0, 100)}`
1226
+ );
1227
+ const start = performance.now();
1228
+ const _guard = __using(_stack, this.startExecution(session));
1229
+ const result = await executeWithReconnect(session, code, options, execId);
1230
+ const durationMs = Math.round(performance.now() - start);
1231
+ trackExec({
1232
+ projectId: session.connection.projectId,
1233
+ userId: session.connection.userId,
1234
+ sessionId: session.connection.getServerSessionId(),
1235
+ ...session.agentThreadContext,
1236
+ durationMs,
1237
+ hasError: result.error !== void 0,
1238
+ ...result.error ? { errorMessage: result.error } : {}
1239
+ });
1240
+ if (result.error) {
1241
+ log(
1242
+ `exec.error exec=${execId} session=${id} req=${session.connection.framer.requestId} ${durationMs}ms error="${result.error}"`
1243
+ );
1244
+ } else {
1245
+ log(
1246
+ `exec.done exec=${execId} session=${id} req=${session.connection.framer.requestId} ${durationMs}ms`
1247
+ );
1248
+ }
1249
+ return result;
1250
+ } catch (_) {
1251
+ var _error = _, _hasError = true;
1252
+ } finally {
1253
+ __callDispose(_stack, _error, _hasError);
1254
+ }
1255
+ }
1256
+ exec(session) {
1257
+ return this.startExecution(session);
1258
+ }
1259
+ startExecution(session) {
1260
+ session.inflight++;
1261
+ return {
1262
+ [Symbol.dispose]: () => {
1263
+ session.inflight--;
1264
+ session.lastActivityAt = Date.now();
1265
+ }
1266
+ };
1267
+ }
1268
+ async destroy(id) {
1269
+ const session = this.findSession(id);
1270
+ if (!session) {
1271
+ return;
1272
+ }
1273
+ trackSessionDestroy({
1274
+ projectId: session.connection.projectId,
1275
+ userId: session.connection.userId,
1276
+ sessionId: session.connection.getServerSessionId(),
1277
+ ...session.agentThreadContext
1278
+ });
1279
+ session.connection.removeSession(id);
1280
+ if (!session.connection.hasSessions()) {
1281
+ await session.connection.disconnect();
1282
+ const index = this.connections.indexOf(session.connection);
1283
+ if (index >= 0) {
1284
+ this.connections.splice(index, 1);
1285
+ }
1286
+ }
1287
+ if (this.connections.length === 0) {
1288
+ this.stopIdleCheck();
1289
+ }
1290
+ }
1291
+ async destroyAll() {
1292
+ const connections = [...this.connections];
1293
+ this.connections = [];
1294
+ this.stopIdleCheck();
1295
+ for (const connection of connections) {
1296
+ connection.clearSessions();
1297
+ await connection.disconnect();
1298
+ }
1299
+ }
1300
+ startIdleCheck() {
1301
+ if (this.idleCheck) return;
1302
+ this.idleCheck = setInterval(() => {
1303
+ this.reapIdleConnections().catch((err) => {
1304
+ log(`reap error: ${err instanceof Error ? err.message : err}`);
1305
+ });
1306
+ }, SESSION_IDLE_CHECK_INTERVAL_MS);
1307
+ this.idleCheck.unref();
1308
+ }
1309
+ stopIdleCheck() {
1310
+ if (this.idleCheck) {
1311
+ clearInterval(this.idleCheck);
1312
+ this.idleCheck = null;
1313
+ }
1314
+ }
1315
+ async reapIdleConnections() {
1316
+ const now = Date.now();
1317
+ const disconnects = [];
1318
+ for (const connection of this.connections) {
1319
+ if (!connection.isConnected()) continue;
1320
+ if (!connection.isIdle(now, SESSION_IDLE_TIMEOUT_MS)) continue;
1321
+ log(
1322
+ `idle disconnect project=${connection.projectId} req=${connection.framer.requestId}`
1323
+ );
1324
+ disconnects.push({
1325
+ projectId: connection.projectId,
1326
+ userId: connection.userId,
1327
+ sessionId: connection.getServerSessionId(),
1328
+ disconnectPromise: connection.disconnect()
1329
+ });
1330
+ }
1331
+ const results = await Promise.allSettled(
1332
+ disconnects.map((d) => d.disconnectPromise)
1333
+ );
1334
+ for (const [index, result] of results.entries()) {
1335
+ const { projectId, sessionId, userId } = disconnects[index];
1336
+ if (result.status === "fulfilled") {
1337
+ trackConnectionIdleDisconnect({ projectId, sessionId, userId });
1338
+ continue;
1339
+ }
1340
+ if (result.status === "rejected") {
1341
+ const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
1342
+ log(`disconnect error: ${errorMessage}`);
1343
+ trackError({
1344
+ errorType: "connection_idle_disconnect_error",
1345
+ projectId,
1346
+ sessionId,
1347
+ userId,
1348
+ errorMessage
1349
+ });
1350
+ }
1351
+ }
1352
+ }
1353
+ findConnection(projectId, apiKey, headlessServerUrl) {
1354
+ return this.connections.find(
1355
+ (connection) => connection.projectId === projectId && connection.apiKey === apiKey && connection.headlessServerUrl === headlessServerUrl
1356
+ );
1357
+ }
1358
+ async findOrCreateConnection(projectId, userId, apiKey, headlessServerUrl) {
1359
+ const existingConnection = this.findConnection(
1360
+ projectId,
1361
+ apiKey,
1362
+ headlessServerUrl
1363
+ );
1364
+ if (existingConnection) {
1365
+ await existingConnection.reconnect();
1366
+ trackConnectionAcquire({
1367
+ projectId,
1368
+ userId: existingConnection.userId,
1369
+ sessionId: existingConnection.getServerSessionId(),
1370
+ isReuse: true
1371
+ });
1372
+ return existingConnection;
1373
+ }
1374
+ const connection = await Connection.open(
1375
+ projectId,
1376
+ userId,
1377
+ apiKey,
1378
+ headlessServerUrl
1379
+ );
1380
+ trackConnectionAcquire({
1381
+ projectId,
1382
+ userId: connection.userId,
1383
+ sessionId: connection.getServerSessionId(),
1384
+ isReuse: false
1385
+ });
1386
+ this.connections.push(connection);
1387
+ return connection;
1388
+ }
1389
+ findSession(id) {
1390
+ for (const connection of this.connections) {
1391
+ const session = connection.findSession(id);
1392
+ if (session) return session;
1393
+ }
1394
+ return void 0;
1395
+ }
1396
+ getNextSessionId() {
1397
+ let id = 1;
1398
+ while (this.findSession(String(id))) {
1399
+ id++;
1400
+ }
1401
+ return String(id);
1402
+ }
1403
+ };
1404
+ var sessionManager = new SessionManager();
1405
+
1406
+ // src/router.ts
1407
+ var t = initTRPC.create({
1408
+ errorFormatter({ shape, error }) {
1409
+ const cause = error.cause;
1410
+ if (cause instanceof FramerAPIError) {
1411
+ const framerApi = { code: cause.code };
1412
+ if (cause.ref !== void 0) framerApi.ref = cause.ref;
1413
+ return { ...shape, data: { ...shape.data, framerApi } };
1414
+ }
1415
+ return shape;
1416
+ }
1417
+ });
1418
+ var nextExecId = 0;
1419
+ var appRouter = t.router({
1420
+ version: t.procedure.query(() => {
1421
+ return { version: VERSION };
1422
+ }),
1423
+ listSessions: t.procedure.query(() => {
1424
+ return sessionManager.list();
1425
+ }),
1426
+ createSession: t.procedure.input(
1427
+ z.object({
1428
+ projectId: z.string(),
1429
+ apiKey: z.string(),
1430
+ userId: z.string().optional(),
1431
+ headlessServerUrl: z.string(),
1432
+ agentThreadId: z.string().optional(),
1433
+ agentProvider: z.string().optional()
1434
+ })
1435
+ ).mutation(async ({ input }) => {
1436
+ const start = performance.now();
1437
+ const agentThreadContext = normalizeAgentThreadContext(input);
1438
+ try {
1439
+ const session = await sessionManager.create(
1440
+ input.projectId,
1441
+ input.userId,
1442
+ input.apiKey,
1443
+ input.headlessServerUrl,
1444
+ agentThreadContext
1445
+ );
1446
+ log(
1447
+ `session.new id=${session.id} project=${input.projectId} headlessServerUrl=${input.headlessServerUrl}`
1448
+ );
1449
+ trackSessionCreate({
1450
+ projectId: input.projectId,
1451
+ userId: session.connection.userId,
1452
+ sessionId: session.connection.getServerSessionId(),
1453
+ ...session.agentThreadContext,
1454
+ durationMs: Math.round(performance.now() - start)
1455
+ });
1456
+ return { id: session.id };
1457
+ } catch (err) {
1458
+ const message = formatError(err);
1459
+ trackError({
1460
+ errorType: "session_create_error",
1461
+ projectId: input.projectId,
1462
+ userId: input.userId,
1463
+ ...agentThreadContext,
1464
+ errorMessage: message
1465
+ });
1466
+ throw new TRPCError({
1467
+ code: isAuthError(message) ? "UNAUTHORIZED" : "INTERNAL_SERVER_ERROR",
1468
+ message,
1469
+ cause: err
1470
+ });
1471
+ }
1472
+ }),
1473
+ destroySession: t.procedure.input(z.object({ sessionId: z.string() })).mutation(async ({ input }) => {
1474
+ await sessionManager.destroy(input.sessionId);
1475
+ log(`session.destroy id=${input.sessionId}`);
1476
+ }),
1477
+ exec: t.procedure.input(
1478
+ z.object({
1479
+ sessionId: z.string(),
1480
+ code: z.string(),
1481
+ cwd: z.string().optional()
1482
+ })
1483
+ ).mutation(async ({ input }) => {
1484
+ const { sessionId, code, cwd } = input;
1485
+ const execId = nextExecId++;
1486
+ const result = await sessionManager.execute(
1487
+ sessionId,
1488
+ code,
1489
+ { cwd },
1490
+ execId
1491
+ );
1492
+ if (!result) {
1493
+ throw new TRPCError({
1494
+ code: "BAD_REQUEST",
1495
+ message: `Session ${sessionId} is invalid or has expired`
1496
+ });
1497
+ }
1498
+ return result;
1499
+ }),
1500
+ shutdown: t.procedure.mutation(() => {
1501
+ log("shutdown requested");
1502
+ trackRelayShutdown();
1503
+ setTimeout(() => {
1504
+ waitForTrackingToFinish().finally(() => {
1505
+ process.exit(0);
1506
+ });
1507
+ }, 100);
1508
+ })
1509
+ });
1510
+
1511
+ // src/relay-server.ts
1512
+ var IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
1513
+ var IDLE_CHECK_INTERVAL_MS = 60 * 1e3;
1514
+ var trpcHandler = createHTTPHandler({ router: appRouter });
1515
+ function getHostHeader(req) {
1516
+ const host = req.headers.host;
1517
+ if (typeof host === "string") {
1518
+ return host;
1519
+ }
1520
+ return null;
1521
+ }
1522
+ __name(getHostHeader, "getHostHeader");
1523
+ function isAllowedHost(hostHeader, listeningPort) {
1524
+ if (!hostHeader || listeningPort === null) {
1525
+ return false;
1526
+ }
1527
+ const normalizedHost = hostHeader.trim().toLowerCase();
1528
+ return normalizedHost === `127.0.0.1:${listeningPort}`;
1529
+ }
1530
+ __name(isAllowedHost, "isAllowedHost");
1531
+ function getAuthorizationHeader(req) {
1532
+ const header = req.headers.authorization;
1533
+ if (typeof header === "string") {
1534
+ return header;
1535
+ }
1536
+ return null;
1537
+ }
1538
+ __name(getAuthorizationHeader, "getAuthorizationHeader");
1539
+ function isAuthorized(authorizationHeader, expectedToken) {
1540
+ if (!authorizationHeader) {
1541
+ return false;
1542
+ }
1543
+ const match = /^Bearer\s+(.+)$/i.exec(authorizationHeader.trim());
1544
+ if (!match) {
1545
+ return false;
1546
+ }
1547
+ const providedToken = match[1] ?? "";
1548
+ const expectedBuffer = Buffer.from(expectedToken);
1549
+ const providedBuffer = Buffer.from(providedToken);
1550
+ if (expectedBuffer.length !== providedBuffer.length) {
1551
+ return false;
1552
+ }
1553
+ return timingSafeEqual(expectedBuffer, providedBuffer);
1554
+ }
1555
+ __name(isAuthorized, "isAuthorized");
1556
+ function getListeningPort(server) {
1557
+ const address = server.address();
1558
+ if (!address || typeof address === "string") {
1559
+ return null;
1560
+ }
1561
+ return address.port;
1562
+ }
1563
+ __name(getListeningPort, "getListeningPort");
1564
+ async function startRelayServer(port = RELAY_PORT) {
1565
+ let lastActivityAt = Date.now();
1566
+ const relayToken = generateRelayToken();
1567
+ const server = http.createServer((req, res) => {
1568
+ const listeningPort = getListeningPort(server);
1569
+ if (!isAllowedHost(getHostHeader(req), listeningPort)) {
1570
+ res.writeHead(403).end("Forbidden");
1571
+ return;
1572
+ }
1573
+ if (!isAuthorized(getAuthorizationHeader(req), relayToken)) {
1574
+ res.writeHead(401).end("Unauthorized");
1575
+ return;
1576
+ }
1577
+ lastActivityAt = Date.now();
1578
+ trpcHandler(req, res);
1579
+ });
1580
+ server.on("close", () => {
1581
+ clearRelayToken(relayToken);
1582
+ });
1583
+ const idleCheck = setInterval(() => {
1584
+ const idleMs = Date.now() - lastActivityAt;
1585
+ if (idleMs >= IDLE_TIMEOUT_MS) {
1586
+ log(`idle for ${Math.round(idleMs / 1e3)}s, shutting down`);
1587
+ clearInterval(idleCheck);
1588
+ server.close();
1589
+ trackRelayShutdown();
1590
+ waitForTrackingToFinish().finally(() => {
1591
+ process.exit(0);
1592
+ });
1593
+ }
1594
+ }, IDLE_CHECK_INTERVAL_MS);
1595
+ idleCheck.unref();
1596
+ return new Promise((resolve, reject) => {
1597
+ server.on("error", (error) => {
1598
+ clearRelayToken(relayToken);
1599
+ reject(error);
1600
+ });
1601
+ server.listen(port, "127.0.0.1", () => {
1602
+ writeRelayToken(relayToken);
1603
+ const listeningPort = getListeningPort(server) ?? port;
1604
+ log(`started v${VERSION} on port ${listeningPort}`);
1605
+ resolve(server);
1606
+ });
1607
+ });
1608
+ }
1609
+ __name(startRelayServer, "startRelayServer");
1610
+
1611
+ // src/start-relay-server.ts
1612
+ process.title = "framer-relay-server";
1613
+ process.on("uncaughtException", (err) => {
1614
+ log(`uncaught exception: ${err.message}`);
1615
+ console.error("Uncaught Exception:", err);
1616
+ trackError({
1617
+ errorType: "relay_uncaught_exception",
1618
+ errorMessage: err.message
1619
+ });
1620
+ waitForTrackingToFinish().finally(() => {
1621
+ process.exit(1);
1622
+ });
1623
+ });
1624
+ process.on("unhandledRejection", (reason) => {
1625
+ log(`unhandled rejection: ${reason}`);
1626
+ console.error("Unhandled Rejection:", reason);
1627
+ trackError({
1628
+ errorType: "relay_unhandled_rejection",
1629
+ errorMessage: reason instanceof Error ? reason.message : String(reason)
1630
+ });
1631
+ waitForTrackingToFinish().finally(() => {
1632
+ process.exit(1);
1633
+ });
1634
+ });
1635
+ async function main() {
1636
+ const server = await startRelayServer(RELAY_PORT);
1637
+ console.log(`Framer relay server v${VERSION} running on port ${RELAY_PORT}`);
1638
+ trackRelayStart();
1639
+ process.on("SIGINT", () => {
1640
+ log("shutdown SIGINT");
1641
+ console.log("\nShutting down...");
1642
+ server.close();
1643
+ trackRelayShutdown();
1644
+ waitForTrackingToFinish().finally(() => {
1645
+ process.exit(0);
1646
+ });
1647
+ });
1648
+ process.on("SIGTERM", () => {
1649
+ log("shutdown SIGTERM");
1650
+ console.log("\nShutting down...");
1651
+ server.close();
1652
+ trackRelayShutdown();
1653
+ waitForTrackingToFinish().finally(() => {
1654
+ process.exit(0);
1655
+ });
1656
+ });
1657
+ }
1658
+ __name(main, "main");
1659
+ main().catch((err) => {
1660
+ log(`startup failed: ${err.message}`);
1661
+ console.error("Failed to start relay server:", err);
1662
+ trackError({
1663
+ errorType: "relay_startup_error",
1664
+ errorMessage: err.message
1665
+ });
1666
+ waitForTrackingToFinish().finally(() => {
1667
+ process.exit(1);
1668
+ });
1669
+ });