codex-relay 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -689,6 +689,7 @@ const apiPaths = {
689
689
  workspaceTerminalSession: (sessionId) => `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}`,
690
690
  workspaceTerminalInput: (sessionId) => `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/input`,
691
691
  workspaceTerminalOutput: (sessionId) => `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/output`,
692
+ workspaceTerminalOutputStream: (sessionId) => `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/output/stream`,
692
693
  workspaceTerminalResize: (sessionId) => `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/resize`,
693
694
  imageAttachments: "/v1/attachments/images",
694
695
  imageAttachment: (attachmentId) => `/v1/attachments/images/${encodeURIComponent(attachmentId)}`,
package/dist/src.js CHANGED
@@ -1183,14 +1183,60 @@ function createApp(options = {}) {
1183
1183
  const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
1184
1184
  if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
1185
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
1186
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, workspaceTerminalOutputResponse(session, since));
1187
+ });
1188
+ app.get("/v1/workspace/terminal/sessions/:sessionId/output/stream", async (c) => {
1189
+ const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
1190
+ if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
1191
+ const since = Number(c.req.query("since") ?? "0");
1192
+ const encoder = new TextEncoder();
1193
+ const secureSession = getSecureSessionForRequest(c, options.pairing, secureSessionsByTokenHash);
1194
+ let streamController;
1195
+ let closed = false;
1196
+ let stopHeartbeat;
1197
+ let unsubscribe = () => {};
1198
+ const closeStream = () => {
1199
+ if (closed) return;
1200
+ closed = true;
1201
+ if (stopHeartbeat) {
1202
+ clearInterval(stopHeartbeat);
1203
+ stopHeartbeat = void 0;
1204
+ }
1205
+ unsubscribe();
1206
+ if (streamController) {
1207
+ activeStreamControllers.delete(streamController);
1208
+ closeSseController(streamController);
1209
+ }
1210
+ };
1211
+ const send = (response) => {
1212
+ if (closed || !streamController) return;
1213
+ if (!sendTerminalOutputSse(streamController, encoder, secureSession, response)) {
1214
+ closeStream();
1215
+ return;
1216
+ }
1217
+ if (response.exitedAt) closeStream();
1218
+ };
1219
+ const stream = new ReadableStream({
1220
+ start(controller) {
1221
+ streamController = controller;
1222
+ activeStreamControllers.add(controller);
1223
+ send(workspaceTerminalOutputResponse(session, since));
1224
+ if (session.exitedAt) return;
1225
+ unsubscribe = subscribeWorkspaceTerminalOutput(session, send);
1226
+ stopHeartbeat = setInterval(() => {
1227
+ if (!closed && streamController) enqueueSseChunk(streamController, encoder.encode(": keep-alive\n\n"));
1228
+ }, 3e4);
1229
+ },
1230
+ cancel() {
1231
+ closeStream();
1232
+ }
1192
1233
  });
1193
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
1234
+ return new Response(stream, { headers: {
1235
+ "cache-control": "no-cache, no-transform",
1236
+ connection: "keep-alive",
1237
+ "content-type": "text/event-stream",
1238
+ "x-accel-buffering": "no"
1239
+ } });
1194
1240
  });
1195
1241
  app.post("/v1/workspace/terminal/sessions/:sessionId/input", async (c) => {
1196
1242
  const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
@@ -1199,7 +1245,7 @@ function createApp(options = {}) {
1199
1245
  if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
1200
1246
  if (session.exitedAt) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_closed", "Terminal session is closed."), 409);
1201
1247
  session.child.write(parsed.data.data);
1202
- return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
1248
+ return new Response(null, { status: 204 });
1203
1249
  });
1204
1250
  app.post("/v1/workspace/terminal/sessions/:sessionId/resize", async (c) => {
1205
1251
  const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
@@ -1209,7 +1255,7 @@ function createApp(options = {}) {
1209
1255
  session.cols = parsed.data.cols;
1210
1256
  session.rows = parsed.data.rows;
1211
1257
  if (!session.exitedAt) session.child.resize(parsed.data.cols, parsed.data.rows);
1212
- return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
1258
+ return new Response(null, { status: 204 });
1213
1259
  });
1214
1260
  app.delete("/v1/workspace/terminal/sessions/:sessionId", async (c) => {
1215
1261
  const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
@@ -1217,7 +1263,7 @@ function createApp(options = {}) {
1217
1263
  closeWorkspaceTerminalSession(session);
1218
1264
  workspaceTerminalSessions.delete(session.sessionId);
1219
1265
  }
1220
- return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
1266
+ return new Response(null, { status: 204 });
1221
1267
  });
1222
1268
  app.get(apiPaths.models, async (c) => {
1223
1269
  try {
@@ -3585,6 +3631,13 @@ function sendSse(controller, encoder, secureSession, event) {
3585
3631
  threadId
3586
3632
  });
3587
3633
  }
3634
+ function sendTerminalOutputSse(controller, encoder, secureSession, response) {
3635
+ const parsed = WorkspaceTerminalOutputResponseSchema.parse(response);
3636
+ const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(parsed))) : parsed;
3637
+ if (secureSession) secureSession.persist().catch(() => void 0);
3638
+ if (!enqueueSseChunk(controller, encoder.encode("event: output\n"))) return false;
3639
+ return enqueueSseChunk(controller, encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
3640
+ }
3588
3641
  function threadIdFromStreamEvent(event) {
3589
3642
  if ("threadId" in event && typeof event.threadId === "string") return event.threadId;
3590
3643
  if ("thread" in event && event.thread) return event.thread.id;
@@ -5303,20 +5356,27 @@ function createWorkspaceTerminalSession(input) {
5303
5356
  seq: 0,
5304
5357
  sessionId,
5305
5358
  startedAt,
5359
+ subscribers: /* @__PURE__ */ new Set(),
5306
5360
  workspacePath: input.cwd
5307
5361
  };
5308
5362
  const appendOutput = (data) => {
5309
- session.output.push({
5363
+ const chunk = {
5310
5364
  data,
5311
5365
  seq: session.seq
5312
- });
5366
+ };
5367
+ session.output.push(chunk);
5313
5368
  session.seq += 1;
5314
5369
  if (session.output.length > maxWorkspaceTerminalOutputChunks) session.output.splice(0, session.output.length - maxWorkspaceTerminalOutputChunks);
5370
+ notifyWorkspaceTerminalOutput(session, {
5371
+ chunks: [chunk],
5372
+ nextSeq: session.seq
5373
+ });
5315
5374
  };
5316
5375
  child.onData(appendOutput);
5317
5376
  child.onExit(({ exitCode }) => {
5318
5377
  session.exitCode = exitCode;
5319
5378
  session.exitedAt = (/* @__PURE__ */ new Date()).toISOString();
5379
+ notifyWorkspaceTerminalOutput(session, workspaceTerminalOutputResponse(session, session.seq));
5320
5380
  });
5321
5381
  return session;
5322
5382
  }
@@ -5324,6 +5384,23 @@ function closeWorkspaceTerminalSession(session) {
5324
5384
  if (session.exitedAt) return;
5325
5385
  session.child.kill();
5326
5386
  }
5387
+ function workspaceTerminalOutputResponse(session, since) {
5388
+ return WorkspaceTerminalOutputResponseSchema.parse({
5389
+ chunks: session.output.filter((chunk) => chunk.seq >= since),
5390
+ exitCode: session.exitCode,
5391
+ exitedAt: session.exitedAt,
5392
+ nextSeq: session.seq
5393
+ });
5394
+ }
5395
+ function subscribeWorkspaceTerminalOutput(session, subscriber) {
5396
+ session.subscribers.add(subscriber);
5397
+ return () => {
5398
+ session.subscribers.delete(subscriber);
5399
+ };
5400
+ }
5401
+ function notifyWorkspaceTerminalOutput(session, response) {
5402
+ for (const subscriber of session.subscribers) subscriber(response);
5403
+ }
5327
5404
  async function listWorkspaceFiles(workspacePath, query, directory) {
5328
5405
  const normalizedQuery = query.toLowerCase();
5329
5406
  const isIgnored = await workspaceIgnoreMatcher(workspacePath);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/codex-relay/codex-relay.git",
8
+ "url": "git+https://github.com/gronxb/codex-relay.git",
9
9
  "directory": "packages/codex-relay"
10
10
  },
11
11
  "bin": {
package/src/api-schema.ts CHANGED
@@ -976,6 +976,8 @@ export const apiPaths = {
976
976
  `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/input`,
977
977
  workspaceTerminalOutput: (sessionId: string) =>
978
978
  `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/output`,
979
+ workspaceTerminalOutputStream: (sessionId: string) =>
980
+ `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/output/stream`,
979
981
  workspaceTerminalResize: (sessionId: string) =>
980
982
  `/v1/workspace/terminal/sessions/${encodeURIComponent(sessionId)}/resize`,
981
983
  imageAttachments: "/v1/attachments/images",