codex-relay 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src.js CHANGED
@@ -1,22 +1,23 @@
1
- import { A as PairResponseSchema, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, Dt as apiPaths, E as ListWorkspaceDirectoriesResponseSchema, G as ResolveApprovalRequestSchema, K as ResolveApprovalResponseSchema, Lt as stripPromptSkillMentions, Mt as promptMarkdownWithSkills, Ot as chatMessageDetailsFromPromptContext, Q as RuntimePreferencesSchema, S as ListModelsResponseSchema, St as WorkspaceGitActionResponseSchema, T as ListThreadsResponseSchema, U as RateLimitsResponseSchema, X as RuntimePreferencesByWorkspacePathSchema, Z as RuntimePreferencesResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as ThreadContextWindowResponseSchema, b as InterruptThreadRunResponseSchema, bt as WorkspaceFileContentResponseSchema, ct as ThreadMessageDetailResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadSummarySchema, et as StatusResponseSchema, ft as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, jt as normalizePromptContext, k as PairRequestSchema, kt as createOpenApiDocument, l as ChatMessageSchema, m as ContextWindowUsageSchema, mt as VersionResponseSchema, nt as StreamThreadRunRequestSchema, ot as ThreadDetailResponseSchema, p as CommitPushWorkspaceRequestSchema, pt as UpdateWorkspaceFileContentRequestSchema, q as RunThreadRequestSchema, rt as SubmitThreadInputResponseSchema, st as ThreadMessageDetailFieldSchema, tt as StreamThreadRunEventSchema, vt as WorkspaceChangesResponseSchema, w as ListSkillsResponseSchema, y as ImageAttachmentUploadResponseSchema, z as QueuedThreadInputActionResponseSchema } from "./api-schema2.js";
2
- import { r as legacyCodexRelayDataPath, t as codexRelayDataPath } from "./paths.js";
1
+ import { A as PairResponseSchema, At as WorkspaceTerminalResizeRequestSchema, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, Dt as WorkspaceTerminalInputRequestSchema, E as ListWorkspaceDirectoriesResponseSchema, Ft as createOpenApiDocument, G as ResolveApprovalRequestSchema, K as ResolveApprovalResponseSchema, Lt as normalizePromptContext, Mt as WorkspaceTerminalStartRequestSchema, Nt as apiPaths, Pt as chatMessageDetailsFromPromptContext, Q as RuntimePreferencesSchema, Rt as promptMarkdownWithSkills, S as ListModelsResponseSchema, St as WorkspaceGitActionResponseSchema, T as ListThreadsResponseSchema, U as RateLimitsResponseSchema, Ut as stripPromptSkillMentions, X as RuntimePreferencesByWorkspacePathSchema, Z as RuntimePreferencesResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as ThreadContextWindowResponseSchema, b as InterruptThreadRunResponseSchema, bt as WorkspaceFileContentResponseSchema, ct as ThreadMessageDetailResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadSummarySchema, et as StatusResponseSchema, ft as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, jt as WorkspaceTerminalSessionResponseSchema, k as PairRequestSchema, kt as WorkspaceTerminalOutputResponseSchema, l as ChatMessageSchema, m as ContextWindowUsageSchema, mt as VersionResponseSchema, nt as StreamThreadRunRequestSchema, ot as ThreadDetailResponseSchema, p as CommitPushWorkspaceRequestSchema, pt as UpdateWorkspaceFileContentRequestSchema, q as RunThreadRequestSchema, rt as SubmitThreadInputResponseSchema, st as ThreadMessageDetailFieldSchema, tt as StreamThreadRunEventSchema, vt as WorkspaceChangesResponseSchema, w as ListSkillsResponseSchema, y as ImageAttachmentUploadResponseSchema, z as QueuedThreadInputActionResponseSchema } from "./api-schema2.js";
2
+ import { a as getConnectUrlCandidates, i as createPairingQrPayload, o as getConnectUrlGuidance, r as legacyCodexRelayDataPath, s as createTursoPairingSessionStore, t as codexRelayDataPath } from "./paths.js";
3
3
  import { createRequire } from "node:module";
4
4
  import qrcode from "qrcode-terminal";
5
- import { execFile, execFileSync, spawn } from "node:child_process";
5
+ import { execFile, spawn } from "node:child_process";
6
6
  import fs, { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
7
7
  import { access, appendFile, copyFile, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
8
8
  import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { z } from "zod";
11
- import os, { homedir, hostname, networkInterfaces } from "node:os";
12
- import { serve } from "@hono/node-server";
13
11
  import { fromByteArray, toByteArray } from "base64-js";
12
+ import os, { homedir, hostname } from "node:os";
13
+ import { serve } from "@hono/node-server";
14
14
  import { createHash, randomBytes, randomUUID } from "node:crypto";
15
15
  import pc from "picocolors";
16
16
  import { openRepository } from "es-git";
17
17
  import { Hono } from "hono";
18
18
  import { cors } from "hono/cors";
19
19
  import { promisify } from "node:util";
20
+ import * as pty from "@lydell/node-pty";
20
21
  import { createInterface } from "node:readline";
21
22
  import { Codex } from "@openai/codex-sdk";
22
23
  import { gcm } from "@noble/ciphers/aes.js";
@@ -24,7 +25,6 @@ import { randomBytes as randomBytes$1, utf8ToBytes } from "@noble/ciphers/utils.
24
25
  import { ed25519, x25519 } from "@noble/curves/ed25519.js";
25
26
  import { hkdf } from "@noble/hashes/hkdf.js";
26
27
  import { sha256 } from "@noble/hashes/sha2.js";
27
- import { connect } from "@tursodatabase/database";
28
28
  //#region src/app-server.ts
29
29
  var CodexAppServerClient = class {
30
30
  child;
@@ -823,6 +823,7 @@ const defaultWebPreviewPorts = [
823
823
  ];
824
824
  const PairApproveRequestSchema = z.object({ approvalCode: z.string().trim().min(1) });
825
825
  const maxResolvedApprovals = 100;
826
+ const maxWorkspaceTerminalOutputChunks = 2e3;
826
827
  function createApp(options = {}) {
827
828
  const app = new Hono();
828
829
  const appServer = options.appServer === void 0 ? process.env.VITEST ? null : new CodexAppServerClient() : options.appServer;
@@ -835,10 +836,12 @@ function createApp(options = {}) {
835
836
  const pendingApprovals = /* @__PURE__ */ new Map();
836
837
  const resolvedApprovals = /* @__PURE__ */ new Map();
837
838
  const queuedInputsByThreadId = /* @__PURE__ */ new Map();
839
+ const workspaceTerminalSessions = /* @__PURE__ */ new Map();
838
840
  const activeAppServerTurnIdsByThreadId = /* @__PURE__ */ new Map();
839
841
  const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
840
842
  const steeringThreads = /* @__PURE__ */ new Set();
841
843
  const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
844
+ const activeStreamControllers = /* @__PURE__ */ new Set();
842
845
  const threadOptions = { workingDirectory: workspacePath };
843
846
  const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
844
847
  if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
@@ -870,7 +873,7 @@ function createApp(options = {}) {
870
873
  };
871
874
  app.use("*", cors());
872
875
  app.use("*", async (c, next) => {
873
- if (!options.pairing || c.req.method === "OPTIONS" || c.req.path === apiPaths.version || c.req.path.startsWith(`${apiPaths.imageAttachments}/`) || c.req.path.startsWith(apiPaths.pair)) {
876
+ if (!options.pairing || c.req.method === "OPTIONS" || c.req.path === apiPaths.version || c.req.path.startsWith(`${apiPaths.imageAttachments}/`) || c.req.path === apiPaths.sessionsClear || c.req.path.startsWith(apiPaths.pair)) {
874
877
  await next();
875
878
  return;
876
879
  }
@@ -978,6 +981,18 @@ function createApp(options = {}) {
978
981
  });
979
982
  return c.json({ ok: true });
980
983
  });
984
+ app.post(apiPaths.sessionsClear, async (c) => {
985
+ if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
986
+ if (options.pairing.approvalSecret && c.req.header("x-codex-relay-approve-secret") !== options.pairing.approvalSecret) return c.json(apiError("unauthorized", "Pairing clear must come from this machine."), 401);
987
+ const result = await options.pairing.sessions.clearAll();
988
+ secureSessionsByTokenHash.clear();
989
+ closeActiveStreamControllers(activeStreamControllers);
990
+ options.pairing.onPairingsCleared?.(result);
991
+ return c.json({
992
+ ok: true,
993
+ ...result
994
+ });
995
+ });
981
996
  app.post(apiPaths.sessionRefresh, async (c) => {
982
997
  if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
983
998
  const oldToken = parseBearerToken(c.req.header("authorization"));
@@ -1140,6 +1155,70 @@ function createApp(options = {}) {
1140
1155
  return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_commit_push_failed", errorMessage(error)), 400);
1141
1156
  }
1142
1157
  });
1158
+ app.post(apiPaths.workspaceTerminalSessions, async (c) => {
1159
+ const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, WorkspaceTerminalStartRequestSchema);
1160
+ if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
1161
+ const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
1162
+ if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
1163
+ try {
1164
+ const session = createWorkspaceTerminalSession({
1165
+ cols: parsed.data.cols,
1166
+ cwd: selectedWorkspacePath.path,
1167
+ rows: parsed.data.rows
1168
+ });
1169
+ workspaceTerminalSessions.set(session.sessionId, session);
1170
+ const response = WorkspaceTerminalSessionResponseSchema.parse({
1171
+ cols: session.cols,
1172
+ rows: session.rows,
1173
+ sessionId: session.sessionId,
1174
+ startedAt: session.startedAt,
1175
+ workspacePath: session.workspacePath
1176
+ });
1177
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
1178
+ } catch (error) {
1179
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_start_failed", errorMessage(error)), 400);
1180
+ }
1181
+ });
1182
+ app.get("/v1/workspace/terminal/sessions/:sessionId/output", async (c) => {
1183
+ const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
1184
+ if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
1185
+ const since = Number(c.req.query("since") ?? "0");
1186
+ const chunks = session.output.filter((chunk) => chunk.seq >= since);
1187
+ const response = WorkspaceTerminalOutputResponseSchema.parse({
1188
+ chunks,
1189
+ exitCode: session.exitCode,
1190
+ exitedAt: session.exitedAt,
1191
+ nextSeq: session.seq
1192
+ });
1193
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
1194
+ });
1195
+ app.post("/v1/workspace/terminal/sessions/:sessionId/input", async (c) => {
1196
+ const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
1197
+ if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
1198
+ const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, WorkspaceTerminalInputRequestSchema);
1199
+ if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
1200
+ if (session.exitedAt) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_closed", "Terminal session is closed."), 409);
1201
+ session.child.write(parsed.data.data);
1202
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
1203
+ });
1204
+ app.post("/v1/workspace/terminal/sessions/:sessionId/resize", async (c) => {
1205
+ const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
1206
+ if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
1207
+ const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, WorkspaceTerminalResizeRequestSchema);
1208
+ if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
1209
+ session.cols = parsed.data.cols;
1210
+ session.rows = parsed.data.rows;
1211
+ if (!session.exitedAt) session.child.resize(parsed.data.cols, parsed.data.rows);
1212
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
1213
+ });
1214
+ app.delete("/v1/workspace/terminal/sessions/:sessionId", async (c) => {
1215
+ const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
1216
+ if (session) {
1217
+ closeWorkspaceTerminalSession(session);
1218
+ workspaceTerminalSessions.delete(session.sessionId);
1219
+ }
1220
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
1221
+ });
1143
1222
  app.get(apiPaths.models, async (c) => {
1144
1223
  try {
1145
1224
  const models = appServer ? await appServer.listModels() : fallbackModels();
@@ -1748,9 +1827,12 @@ function createApp(options = {}) {
1748
1827
  });
1749
1828
  const encoder = new TextEncoder();
1750
1829
  const secureSession = getSecureSessionForRequest(c, options.pairing, secureSessionsByTokenHash);
1830
+ let streamController;
1751
1831
  let streamSettled = false;
1752
1832
  const stream = new ReadableStream({
1753
1833
  start(controller) {
1834
+ streamController = controller;
1835
+ activeStreamControllers.add(controller);
1754
1836
  relayDebugLog("thread.stream.started", {
1755
1837
  mode: !runOptions.prompt && appServer ? "attach" : "run",
1756
1838
  threadId
@@ -1781,6 +1863,7 @@ function createApp(options = {}) {
1781
1863
  mode: "attach",
1782
1864
  threadId
1783
1865
  });
1866
+ activeStreamControllers.delete(controller);
1784
1867
  stopPreviewMonitor();
1785
1868
  });
1786
1869
  return;
@@ -1798,6 +1881,7 @@ function createApp(options = {}) {
1798
1881
  mode: "unsupported",
1799
1882
  threadId
1800
1883
  });
1884
+ activeStreamControllers.delete(controller);
1801
1885
  stopPreviewMonitor();
1802
1886
  return;
1803
1887
  }
@@ -1830,10 +1914,12 @@ function createApp(options = {}) {
1830
1914
  mode: "run",
1831
1915
  threadId
1832
1916
  });
1917
+ activeStreamControllers.delete(controller);
1833
1918
  stopPreviewMonitor();
1834
1919
  });
1835
1920
  },
1836
1921
  cancel(reason) {
1922
+ if (streamController) activeStreamControllers.delete(streamController);
1837
1923
  relayDebugLog("thread.stream.cancelled_by_client", {
1838
1924
  reason: debugReason(reason),
1839
1925
  settled: streamSettled,
@@ -3476,10 +3562,32 @@ function updateThread(threads, messagesByThreadId, threadId, update) {
3476
3562
  }
3477
3563
  function sendSse(controller, encoder, secureSession, event) {
3478
3564
  const parsed = StreamThreadRunEventSchema.parse(event);
3565
+ const threadId = threadIdFromStreamEvent(parsed);
3566
+ relayDebugLog("thread.stream.sse", {
3567
+ direction: "server_to_mobile",
3568
+ eventType: parsed.type,
3569
+ threadId,
3570
+ payload: parsed
3571
+ });
3479
3572
  const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(parsed))) : parsed;
3480
3573
  if (secureSession) secureSession.persist().catch(() => void 0);
3481
- if (!enqueueSseChunk(controller, encoder.encode(`event: ${parsed.type}\n`))) return;
3482
- enqueueSseChunk(controller, encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
3574
+ if (!enqueueSseChunk(controller, encoder.encode(`event: ${parsed.type}\n`))) {
3575
+ relayDebugLog("thread.stream.sse.enqueue_failed", {
3576
+ eventType: parsed.type,
3577
+ stage: "event",
3578
+ threadId
3579
+ });
3580
+ return;
3581
+ }
3582
+ if (!enqueueSseChunk(controller, encoder.encode(`data: ${JSON.stringify(data)}\n\n`))) relayDebugLog("thread.stream.sse.enqueue_failed", {
3583
+ eventType: parsed.type,
3584
+ stage: "data",
3585
+ threadId
3586
+ });
3587
+ }
3588
+ function threadIdFromStreamEvent(event) {
3589
+ if ("threadId" in event && typeof event.threadId === "string") return event.threadId;
3590
+ if ("thread" in event && event.thread) return event.thread.id;
3483
3591
  }
3484
3592
  function enqueueSseChunk(controller, chunk) {
3485
3593
  try {
@@ -3499,6 +3607,10 @@ function closeSseController(controller) {
3499
3607
  throw error;
3500
3608
  }
3501
3609
  }
3610
+ function closeActiveStreamControllers(controllers) {
3611
+ for (const controller of controllers) closeSseController(controller);
3612
+ controllers.clear();
3613
+ }
3502
3614
  function isClosedStreamControllerError(error) {
3503
3615
  return error instanceof TypeError && error.code === "ERR_INVALID_STATE" && error.message.includes("Controller is already closed");
3504
3616
  }
@@ -5168,10 +5280,57 @@ async function git(cwd, args) {
5168
5280
  });
5169
5281
  return stdout.trimEnd();
5170
5282
  }
5283
+ function createWorkspaceTerminalSession(input) {
5284
+ const sessionId = randomUUID();
5285
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5286
+ const shell = process.env.SHELL || "/bin/sh";
5287
+ const child = pty.spawn(shell, ["-l"], {
5288
+ cols: input.cols,
5289
+ cwd: input.cwd,
5290
+ env: {
5291
+ ...process.env,
5292
+ COLORTERM: process.env.COLORTERM ?? "truecolor",
5293
+ TERM: process.env.TERM && process.env.TERM !== "dumb" ? process.env.TERM : "xterm-256color"
5294
+ },
5295
+ name: "xterm-256color",
5296
+ rows: input.rows
5297
+ });
5298
+ const session = {
5299
+ child,
5300
+ cols: input.cols,
5301
+ output: [],
5302
+ rows: input.rows,
5303
+ seq: 0,
5304
+ sessionId,
5305
+ startedAt,
5306
+ workspacePath: input.cwd
5307
+ };
5308
+ const appendOutput = (data) => {
5309
+ session.output.push({
5310
+ data,
5311
+ seq: session.seq
5312
+ });
5313
+ session.seq += 1;
5314
+ if (session.output.length > maxWorkspaceTerminalOutputChunks) session.output.splice(0, session.output.length - maxWorkspaceTerminalOutputChunks);
5315
+ };
5316
+ child.onData(appendOutput);
5317
+ child.onExit(({ exitCode }) => {
5318
+ session.exitCode = exitCode;
5319
+ session.exitedAt = (/* @__PURE__ */ new Date()).toISOString();
5320
+ });
5321
+ return session;
5322
+ }
5323
+ function closeWorkspaceTerminalSession(session) {
5324
+ if (session.exitedAt) return;
5325
+ session.child.kill();
5326
+ }
5171
5327
  async function listWorkspaceFiles(workspacePath, query, directory) {
5172
5328
  const normalizedQuery = query.toLowerCase();
5173
- const filePaths = await workspaceFilePaths(workspacePath);
5329
+ const isIgnored = await workspaceIgnoreMatcher(workspacePath);
5330
+ if (directory && isWorkspacePathIgnored(directory, isIgnored)) return [];
5331
+ const filePaths = await workspaceFilePaths(workspacePath, isIgnored);
5174
5332
  const entriesByPath = /* @__PURE__ */ new Map();
5333
+ for (const entry of await workspaceDirectoryEntries(workspacePath, directory, isIgnored)) entriesByPath.set(entry.path, entry);
5175
5334
  for (const path of filePaths) {
5176
5335
  if (!path || path.startsWith("../") || path.includes("/.git/")) continue;
5177
5336
  if (!directory && normalizedQuery) {
@@ -5184,6 +5343,7 @@ async function listWorkspaceFiles(workspacePath, query, directory) {
5184
5343
  const parts = path.split("/");
5185
5344
  for (let index = 1; index < parts.length; index += 1) {
5186
5345
  const directoryPath = parts.slice(0, index).join("/");
5346
+ if (isWorkspacePathIgnored(directoryPath, isIgnored)) continue;
5187
5347
  entriesByPath.set(directoryPath, {
5188
5348
  directory: dirname(directoryPath) === "." ? "" : dirname(directoryPath),
5189
5349
  kind: "directory",
@@ -5205,6 +5365,7 @@ async function listWorkspaceFiles(workspacePath, query, directory) {
5205
5365
  });
5206
5366
  else {
5207
5367
  const directoryPath = [...directory ? directory.split("/") : [], childParts[0]].join("/");
5368
+ if (isWorkspacePathIgnored(directoryPath, isIgnored)) continue;
5208
5369
  entriesByPath.set(directoryPath, {
5209
5370
  directory,
5210
5371
  kind: "directory",
@@ -5383,19 +5544,32 @@ function languageFromWorkspaceFile(path) {
5383
5544
  yml: "yaml"
5384
5545
  }[extension] ?? extension;
5385
5546
  }
5386
- async function workspaceFilePaths(workspacePath) {
5387
- const isIgnored = await workspaceIgnoreMatcher(workspacePath);
5547
+ async function workspaceFilePaths(workspacePath, isIgnored) {
5388
5548
  try {
5389
5549
  return (await git(workspacePath, [
5390
5550
  "ls-files",
5391
5551
  "--cached",
5392
5552
  "--others",
5393
5553
  "--exclude-standard"
5394
- ])).split("\n").filter(Boolean).filter((path) => !isIgnored(path));
5554
+ ])).split("\n").filter(Boolean).filter((path) => !isWorkspacePathIgnored(path, isIgnored));
5395
5555
  } catch {
5396
5556
  return recursiveWorkspaceFilePaths(workspacePath, isIgnored);
5397
5557
  }
5398
5558
  }
5559
+ async function workspaceDirectoryEntries(workspacePath, directory, isIgnored) {
5560
+ const rootPath = resolve(workspacePath);
5561
+ const absoluteDirectory = resolve(rootPath, directory);
5562
+ if (absoluteDirectory !== rootPath && !isPathInside(rootPath, absoluteDirectory)) return [];
5563
+ return (await readdir(absoluteDirectory, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== ".git").map((entry) => {
5564
+ const path = [...directory ? directory.split("/") : [], entry.name].join("/");
5565
+ return {
5566
+ directory,
5567
+ kind: "directory",
5568
+ name: entry.name,
5569
+ path
5570
+ };
5571
+ }).filter((entry) => !isWorkspacePathIgnored(entry.path, isIgnored));
5572
+ }
5399
5573
  async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
5400
5574
  const ignoredDirectories = new Set([
5401
5575
  ".git",
@@ -5411,7 +5585,7 @@ async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
5411
5585
  if (entry.name.startsWith(".") && entry.name !== ".github") continue;
5412
5586
  const absolutePath = resolve(directory, entry.name);
5413
5587
  const relativePath = relative(rootPath, absolutePath).split("\\").join("/");
5414
- if (isIgnored(relativePath)) continue;
5588
+ if (isWorkspacePathIgnored(relativePath, isIgnored)) continue;
5415
5589
  if (entry.isDirectory()) {
5416
5590
  if (ignoredDirectories.has(entry.name)) continue;
5417
5591
  await visit(absolutePath);
@@ -5427,6 +5601,17 @@ async function workspaceIgnoreMatcher(workspacePath) {
5427
5601
  const matchers = (await readFile(join(workspacePath, ".gitignore"), "utf8").catch(() => "")).split(/\r?\n/).map((line) => gitignorePatternMatcher(line)).filter((matcher) => Boolean(matcher));
5428
5602
  return (path) => matchers.some((matcher) => matcher(path.split("\\").join("/")));
5429
5603
  }
5604
+ function isWorkspacePathIgnored(path, isIgnored) {
5605
+ const normalizedPath = path.replace(/^\/+/, "").replaceAll("\\", "/").replace(/\/+$/, "");
5606
+ if (!normalizedPath) return false;
5607
+ if (isIgnored(normalizedPath) || isIgnored(`${normalizedPath}/`)) return true;
5608
+ const parts = normalizedPath.split("/");
5609
+ for (let index = 1; index < parts.length; index += 1) {
5610
+ const ancestorPath = parts.slice(0, index).join("/");
5611
+ if (isIgnored(ancestorPath) || isIgnored(`${ancestorPath}/`)) return true;
5612
+ }
5613
+ return false;
5614
+ }
5430
5615
  function gitignorePatternMatcher(line) {
5431
5616
  const trimmed = line.trim();
5432
5617
  if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return;
@@ -5439,7 +5624,7 @@ function gitignorePatternMatcher(line) {
5439
5624
  return (path) => {
5440
5625
  const normalizedPath = path.replace(/^\/+/, "");
5441
5626
  return (anchored || hasSlash ? [normalizedPath] : normalizedPath.split("/")).some((candidate) => {
5442
- if (directoryOnly) return matcher(candidate) || normalizedPath.startsWith(`${candidate}/`);
5627
+ if (directoryOnly) return matcher(candidate);
5443
5628
  return matcher(candidate);
5444
5629
  });
5445
5630
  };
@@ -5473,238 +5658,6 @@ function errorMessage(error) {
5473
5658
  return error instanceof Error ? error.message : "Codex run failed.";
5474
5659
  }
5475
5660
  //#endregion
5476
- //#region src/pairing-store.ts
5477
- async function createTursoPairingSessionStore(path) {
5478
- if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
5479
- const db = await connect(path);
5480
- await db.exec(`
5481
- CREATE TABLE IF NOT EXISTS pairing_sessions (
5482
- token_hash TEXT PRIMARY KEY,
5483
- client_session_id TEXT,
5484
- client_name TEXT,
5485
- expires_at INTEGER NOT NULL,
5486
- key_epoch INTEGER,
5487
- mobile_to_server_key TEXT,
5488
- server_to_mobile_key TEXT,
5489
- last_mobile_counter INTEGER,
5490
- next_server_counter INTEGER,
5491
- created_at INTEGER NOT NULL,
5492
- updated_at INTEGER NOT NULL
5493
- );
5494
-
5495
- CREATE TABLE IF NOT EXISTS pending_pairings (
5496
- approval_code TEXT PRIMARY KEY,
5497
- client_session_id TEXT,
5498
- client_name TEXT,
5499
- client_ephemeral_public_key TEXT NOT NULL,
5500
- client_nonce TEXT NOT NULL,
5501
- server_url TEXT NOT NULL,
5502
- approved INTEGER NOT NULL DEFAULT 0,
5503
- expires_at INTEGER NOT NULL,
5504
- created_at INTEGER NOT NULL,
5505
- updated_at INTEGER NOT NULL
5506
- );
5507
- `);
5508
- await ensurePairingSessionColumns();
5509
- async function countActive(now) {
5510
- const row = await db.prepare("SELECT COUNT(DISTINCT COALESCE(client_session_id, token_hash)) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
5511
- return Number(row?.count ?? 0);
5512
- }
5513
- async function deleteSession(tokenHash) {
5514
- await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(tokenHash);
5515
- }
5516
- async function deletePendingPairing(approvalCode) {
5517
- await db.prepare("DELETE FROM pending_pairings WHERE approval_code = ?").run(approvalCode);
5518
- }
5519
- async function getPendingPairing(approvalCode, now) {
5520
- const row = await db.prepare(`SELECT approval_code AS approvalCode,
5521
- client_session_id AS clientSessionId,
5522
- client_name AS clientName,
5523
- client_ephemeral_public_key AS clientEphemeralPublicKey,
5524
- client_nonce AS clientNonce,
5525
- server_url AS serverUrl,
5526
- approved,
5527
- expires_at AS expiresAt
5528
- FROM pending_pairings
5529
- WHERE approval_code = ?`).get(approvalCode);
5530
- if (!row) return;
5531
- const expiresAt = Number(row.expiresAt);
5532
- if (now > expiresAt) {
5533
- await deletePendingPairing(approvalCode);
5534
- return;
5535
- }
5536
- return {
5537
- approvalCode: String(row.approvalCode),
5538
- approved: Number(row.approved) === 1,
5539
- clientEphemeralPublicKey: String(row.clientEphemeralPublicKey),
5540
- clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
5541
- clientName: typeof row.clientName === "string" ? row.clientName : void 0,
5542
- clientNonce: String(row.clientNonce),
5543
- expiresAt,
5544
- serverUrl: String(row.serverUrl)
5545
- };
5546
- }
5547
- return {
5548
- async approvePendingPairing(approvalCode, now) {
5549
- const pending = await getPendingPairing(approvalCode, now);
5550
- if (!pending) return;
5551
- await db.prepare("UPDATE pending_pairings SET approved = 1, updated_at = ? WHERE approval_code = ?").run(now, approvalCode);
5552
- return {
5553
- ...pending,
5554
- approved: true
5555
- };
5556
- },
5557
- countActive,
5558
- async createPendingPairing(pairing) {
5559
- const now = Date.now();
5560
- await db.prepare(`INSERT INTO pending_pairings (
5561
- approval_code,
5562
- client_session_id,
5563
- client_name,
5564
- client_ephemeral_public_key,
5565
- client_nonce,
5566
- server_url,
5567
- approved,
5568
- expires_at,
5569
- created_at,
5570
- updated_at
5571
- )
5572
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientSessionId ?? null, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
5573
- },
5574
- async createSession(tokenHash, session) {
5575
- const now = Date.now();
5576
- const secure = encodeSecureSession(session.secureSession);
5577
- if (session.clientSessionId) {
5578
- await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
5579
- if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
5580
- }
5581
- await db.prepare(`INSERT INTO pairing_sessions (
5582
- token_hash,
5583
- client_session_id,
5584
- client_name,
5585
- expires_at,
5586
- key_epoch,
5587
- mobile_to_server_key,
5588
- server_to_mobile_key,
5589
- last_mobile_counter,
5590
- next_server_counter,
5591
- created_at,
5592
- updated_at
5593
- )
5594
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(tokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
5595
- return countActive(now);
5596
- },
5597
- deleteSession,
5598
- deletePendingPairing,
5599
- getPendingPairing,
5600
- async getValidSession(tokenHash, now) {
5601
- const row = await db.prepare(`SELECT client_name AS clientName,
5602
- client_session_id AS clientSessionId,
5603
- expires_at AS expiresAt,
5604
- key_epoch AS keyEpoch,
5605
- mobile_to_server_key AS mobileToServerKey,
5606
- server_to_mobile_key AS serverToMobileKey,
5607
- last_mobile_counter AS lastMobileCounter,
5608
- next_server_counter AS nextServerCounter
5609
- FROM pairing_sessions
5610
- WHERE token_hash = ?`).get(tokenHash);
5611
- if (!row) return;
5612
- const expiresAt = Number(row.expiresAt);
5613
- if (now > expiresAt) {
5614
- await deleteSession(tokenHash);
5615
- return;
5616
- }
5617
- return {
5618
- clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
5619
- clientName: typeof row.clientName === "string" ? row.clientName : void 0,
5620
- expiresAt,
5621
- secureSession: decodeSecureSession(row)
5622
- };
5623
- },
5624
- async pruneExpired(now) {
5625
- await db.prepare("DELETE FROM pairing_sessions WHERE expires_at <= ?").run(now);
5626
- await db.prepare("DELETE FROM pending_pairings WHERE expires_at <= ?").run(now);
5627
- },
5628
- async rotateSession(oldTokenHash, newTokenHash, session) {
5629
- const now = Date.now();
5630
- const secure = encodeSecureSession(session.secureSession);
5631
- await db.transaction(async () => {
5632
- await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(oldTokenHash);
5633
- if (session.clientSessionId) {
5634
- await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
5635
- if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
5636
- }
5637
- await db.prepare(`INSERT INTO pairing_sessions (
5638
- token_hash,
5639
- client_session_id,
5640
- client_name,
5641
- expires_at,
5642
- key_epoch,
5643
- mobile_to_server_key,
5644
- server_to_mobile_key,
5645
- last_mobile_counter,
5646
- next_server_counter,
5647
- created_at,
5648
- updated_at
5649
- )
5650
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newTokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
5651
- })();
5652
- return countActive(now);
5653
- },
5654
- async updateSecureSession(tokenHash, secureSession) {
5655
- const secure = encodeSecureSession(secureSession);
5656
- const now = Date.now();
5657
- await db.prepare(`UPDATE pairing_sessions
5658
- SET key_epoch = ?,
5659
- mobile_to_server_key = ?,
5660
- server_to_mobile_key = ?,
5661
- last_mobile_counter = ?,
5662
- next_server_counter = ?,
5663
- updated_at = ?
5664
- WHERE token_hash = ?`).run(secure.keyEpoch, secure.mobileToServerKey, secure.serverToMobileKey, secure.lastMobileCounter, secure.nextServerCounter, now, tokenHash);
5665
- }
5666
- };
5667
- async function ensurePairingSessionColumns() {
5668
- const rows = await db.prepare("PRAGMA table_info(pairing_sessions)").all();
5669
- const columns = new Set(resultRows(rows).map((row) => String(row.name)));
5670
- for (const [column, sql] of [
5671
- ["client_session_id", "ALTER TABLE pairing_sessions ADD COLUMN client_session_id TEXT"],
5672
- ["key_epoch", "ALTER TABLE pairing_sessions ADD COLUMN key_epoch INTEGER"],
5673
- ["mobile_to_server_key", "ALTER TABLE pairing_sessions ADD COLUMN mobile_to_server_key TEXT"],
5674
- ["server_to_mobile_key", "ALTER TABLE pairing_sessions ADD COLUMN server_to_mobile_key TEXT"],
5675
- ["last_mobile_counter", "ALTER TABLE pairing_sessions ADD COLUMN last_mobile_counter INTEGER"],
5676
- ["next_server_counter", "ALTER TABLE pairing_sessions ADD COLUMN next_server_counter INTEGER"]
5677
- ]) if (!columns.has(column)) await db.exec(sql);
5678
- const pendingRows = await db.prepare("PRAGMA table_info(pending_pairings)").all();
5679
- if (!new Set(resultRows(pendingRows).map((row) => String(row.name))).has("client_session_id")) await db.exec("ALTER TABLE pending_pairings ADD COLUMN client_session_id TEXT");
5680
- }
5681
- }
5682
- function encodeSecureSession(session) {
5683
- if (!session) return;
5684
- return {
5685
- keyEpoch: session.keyEpoch,
5686
- lastMobileCounter: session.lastMobileCounter,
5687
- mobileToServerKey: fromByteArray(session.mobileToServerKey),
5688
- nextServerCounter: session.nextServerCounter,
5689
- serverToMobileKey: fromByteArray(session.serverToMobileKey)
5690
- };
5691
- }
5692
- function decodeSecureSession(row) {
5693
- if (typeof row.mobileToServerKey !== "string" || typeof row.serverToMobileKey !== "string" || row.keyEpoch === null || row.lastMobileCounter === null || row.nextServerCounter === null) return;
5694
- return {
5695
- keyEpoch: Number(row.keyEpoch),
5696
- lastMobileCounter: Number(row.lastMobileCounter),
5697
- mobileToServerKey: toByteArray(row.mobileToServerKey),
5698
- nextServerCounter: Number(row.nextServerCounter),
5699
- serverToMobileKey: toByteArray(row.serverToMobileKey)
5700
- };
5701
- }
5702
- function resultRows(result) {
5703
- if (Array.isArray(result)) return result;
5704
- if (result && typeof result === "object" && Array.isArray(result.rows)) return result.rows;
5705
- return [];
5706
- }
5707
- //#endregion
5708
5661
  //#region src/index.ts
5709
5662
  const port = Number(process.env.PORT ?? 8787);
5710
5663
  const hostname$1 = process.env.HOST ?? "0.0.0.0";
@@ -5755,6 +5708,9 @@ serve({
5755
5708
  onPairApproved: ({ clientName }) => {
5756
5709
  logRuntimeEvent("Approved", `Pairing request approved${clientName ? ` for ${clientName}` : ""}. Waiting for secure session pickup.`);
5757
5710
  },
5711
+ onPairingsCleared: ({ pendingPairingsCleared, sessionsCleared }) => {
5712
+ logRuntimeEvent("Cleared", `Signed out ${sessionsCleared} mobile session${sessionsCleared === 1 ? "" : "s"} and removed ${pendingPairingsCleared} pending pairing request${pendingPairingsCleared === 1 ? "" : "s"}.`);
5713
+ },
5758
5714
  onTokenRefreshed: ({ clientName, tokenCount }) => {
5759
5715
  logRuntimeEvent("Refreshed", `Mobile session rotated${clientName ? ` for ${clientName}` : ""}; ${formatClientCount(tokenCount)} active.`);
5760
5716
  }
@@ -5765,10 +5721,19 @@ serve({
5765
5721
  port
5766
5722
  }, (info) => {
5767
5723
  const listenUrl = `http://${info.address}:${info.port}`;
5768
- const connectUrl = getConfiguredConnectUrl() ?? getTailscaleConnectUrl(info.port) ?? getLocalNetworkConnectUrl(info.port) ?? listenUrl;
5769
- const pairingPayload = createPairingQrPayload(connectUrl);
5724
+ const connectUrlCandidates = getConnectUrlCandidates({
5725
+ listenUrl,
5726
+ port: info.port
5727
+ });
5728
+ const connectUrl = connectUrlCandidates[0]?.url ?? listenUrl;
5729
+ const connectUrls = connectUrlCandidates.map((candidate) => candidate.url);
5730
+ const pairingPayload = createPairingQrPayload({
5731
+ serverPublicKey: serverIdentity.publicKey,
5732
+ serverUrls: connectUrls.length > 0 ? connectUrls : [connectUrl]
5733
+ });
5770
5734
  writeServerState({
5771
5735
  connectUrl,
5736
+ connectUrlCandidates,
5772
5737
  host: hostname$1,
5773
5738
  listenUrl,
5774
5739
  pairingPayload,
@@ -5779,6 +5744,7 @@ serve({
5779
5744
  logRuntimeEvent("Debug", `Writing diagnostics to ${debugLogPath}`);
5780
5745
  relayDebugLog("relay.started", {
5781
5746
  connectUrl,
5747
+ connectUrlCandidates,
5782
5748
  listenUrl,
5783
5749
  port: info.port,
5784
5750
  workspacePath: process.env.CODEX_RELAY_WORKSPACE_PATH ?? process.cwd()
@@ -5788,6 +5754,7 @@ serve({
5788
5754
  qrcode.generate(pairingPayload, { small: true });
5789
5755
  console.log(formatStartupInstructions({
5790
5756
  connectUrl,
5757
+ connectUrlCandidates,
5791
5758
  dangerouslyAutoApprove,
5792
5759
  listenUrl,
5793
5760
  pairingPayload,
@@ -5801,6 +5768,8 @@ function formatStartupInstructions(details) {
5801
5768
  `${color.prompt("›")} Scan the QR code above to pair ${color.brand("Codex Relay mobile")}.`,
5802
5769
  "",
5803
5770
  `${color.prompt("›")} Mobile: ${color.url(details.connectUrl)}`,
5771
+ ...formatConnectUrlGuidance(details.connectUrl),
5772
+ ...formatConnectUrlCandidates(details.connectUrlCandidates),
5804
5773
  `${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
5805
5774
  "",
5806
5775
  `${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
@@ -5817,34 +5786,23 @@ function formatStartupInstructions(details) {
5817
5786
  ""
5818
5787
  ].join("\n");
5819
5788
  }
5789
+ function formatConnectUrlGuidance(connectUrl) {
5790
+ const guidance = getConnectUrlGuidance(connectUrl);
5791
+ return guidance ? [`${color.prompt("›")} Network: ${guidance}`] : [];
5792
+ }
5793
+ function formatConnectUrlCandidates(candidates) {
5794
+ if (candidates.length <= 1) return [];
5795
+ return [`${color.prompt("›")} QR includes ${candidates.length} candidate addresses; the app will use the first reachable one.`, ...candidates.slice(1).map((candidate) => ` ${color.muted(candidate.label)} ${color.url(candidate.url)}`)];
5796
+ }
5820
5797
  function logRuntimeEvent(label, message) {
5821
5798
  console.log(`${color.prompt("›")} ${color.event(label.padEnd(8))} ${message}`);
5822
5799
  }
5823
5800
  function formatClientCount(tokenCount) {
5824
5801
  return `${tokenCount} client${tokenCount === 1 ? "" : "s"}`;
5825
5802
  }
5826
- function createPairingQrPayload(serverUrl) {
5827
- const url = new URL("codex-relay://pair");
5828
- url.searchParams.set("serverUrl", serverUrl);
5829
- url.searchParams.set("serverPublicKey", serverIdentity.publicKey);
5830
- return url.toString();
5831
- }
5832
5803
  function hashClientToken(token) {
5833
5804
  return createHash("sha256").update(token).digest("base64url");
5834
5805
  }
5835
- function getConfiguredConnectUrl() {
5836
- const configuredUrl = normalizeUrl(process.env.CODEX_RELAY_PUBLIC_URL);
5837
- if (configuredUrl) return configuredUrl;
5838
- }
5839
- function getTailscaleConnectUrl(port) {
5840
- const status = getTailscaleStatus();
5841
- const tailscaleIp = status?.Self?.TailscaleIPs?.find((ip) => ip.startsWith("100.") && ip.includes("."));
5842
- if (tailscaleIp) return `http://${tailscaleIp}:${port}`;
5843
- const dnsName = status?.Self?.DNSName?.replace(/\.$/, "");
5844
- if (dnsName) return getTailscaleServeHttpsUrl(dnsName, port) ?? `http://${dnsName}:${port}`;
5845
- const tailscaleHost = status?.Self?.TailscaleIPs?.find((ip) => ip.includes("."));
5846
- return tailscaleHost ? `http://${tailscaleHost}:${port}` : void 0;
5847
- }
5848
5806
  async function getApprovalSecret() {
5849
5807
  if (process.env.CODEX_RELAY_APPROVAL_SECRET) return process.env.CODEX_RELAY_APPROVAL_SECRET;
5850
5808
  const path = await prepareCodexRelayDataPath("approval-secret");
@@ -5897,58 +5855,5 @@ async function writeBackgroundPid() {
5897
5855
  function formatApprovalCommand(approvalCode, activePort) {
5898
5856
  return activePort === 8787 ? `${npxCommand} approve ${approvalCode}` : `PORT=${activePort} ${npxCommand} approve ${approvalCode}`;
5899
5857
  }
5900
- function getLocalNetworkConnectUrl(port) {
5901
- for (const addresses of Object.values(networkInterfaces())) for (const address of addresses ?? []) if (address.family === "IPv4" && !address.internal) return `http://${address.address}:${port}`;
5902
- }
5903
- function normalizeUrl(value) {
5904
- if (!value) return;
5905
- const trimmed = value.trim().replace(/\/$/, "");
5906
- if (!trimmed) return;
5907
- try {
5908
- const url = new URL(trimmed);
5909
- return url.protocol === "http:" || url.protocol === "https:" ? url.toString().replace(/\/$/, "") : void 0;
5910
- } catch {
5911
- return;
5912
- }
5913
- }
5914
- function getTailscaleStatus() {
5915
- try {
5916
- const output = execFileSync("tailscale", ["status", "--json"], {
5917
- encoding: "utf8",
5918
- stdio: [
5919
- "ignore",
5920
- "pipe",
5921
- "ignore"
5922
- ],
5923
- timeout: 1500
5924
- });
5925
- return JSON.parse(output);
5926
- } catch {
5927
- return;
5928
- }
5929
- }
5930
- function getTailscaleServeHttpsUrl(dnsName, port) {
5931
- try {
5932
- const output = execFileSync("tailscale", [
5933
- "serve",
5934
- "status",
5935
- "--json"
5936
- ], {
5937
- encoding: "utf8",
5938
- stdio: [
5939
- "ignore",
5940
- "pipe",
5941
- "ignore"
5942
- ],
5943
- timeout: 1500
5944
- });
5945
- const serveStatus = JSON.parse(output);
5946
- const portKey = String(port);
5947
- const hostPort = `${dnsName}:${portKey}`;
5948
- return serveStatus.TCP?.[portKey]?.HTTPS && serveStatus.Web?.[hostPort] ? `https://${hostPort}` : void 0;
5949
- } catch {
5950
- return;
5951
- }
5952
- }
5953
5858
  //#endregion
5954
5859
  export {};