claude-relay 2.2.3 → 2.3.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.
package/lib/sdk-bridge.js CHANGED
@@ -43,10 +43,12 @@ function createMessageQueue() {
43
43
 
44
44
  function createSDKBridge(opts) {
45
45
  var cwd = opts.cwd;
46
+ var slug = opts.slug || "";
46
47
  var sm = opts.sessionManager; // session manager instance
47
48
  var send = opts.send; // broadcast to all clients
48
49
  var pushModule = opts.pushModule;
49
50
  var getSDK = opts.getSDK;
51
+ var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
50
52
 
51
53
  function sendAndRecord(session, obj) {
52
54
  sm.sendAndRecord(session, obj);
@@ -139,6 +141,7 @@ function createSDKBridge(opts) {
139
141
  var q = input.questions[0];
140
142
  pushModule.sendPush({
141
143
  type: "ask_user",
144
+ slug: slug,
142
145
  title: "Claude has a question",
143
146
  body: q ? q.question : "Waiting for your response",
144
147
  tag: "claude-ask",
@@ -219,6 +222,7 @@ function createSDKBridge(opts) {
219
222
  cost: parsed.total_cost_usd,
220
223
  duration: parsed.duration_ms,
221
224
  usage: parsed.usage || null,
225
+ modelUsage: parsed.modelUsage || null,
222
226
  sessionId: parsed.session_id,
223
227
  });
224
228
  sendAndRecord(session, { type: "done", code: 0 });
@@ -227,6 +231,7 @@ function createSDKBridge(opts) {
227
231
  if (preview.length > 140) preview = preview.substring(0, 140) + "...";
228
232
  pushModule.sendPush({
229
233
  type: "done",
234
+ slug: slug,
230
235
  title: session.title || "Claude",
231
236
  body: preview || "Response ready",
232
237
  tag: "claude-done",
@@ -298,6 +303,7 @@ function createSDKBridge(opts) {
298
303
  if (pushModule) {
299
304
  pushModule.sendPush({
300
305
  type: "permission_request",
306
+ slug: slug,
301
307
  requestId: requestId,
302
308
  title: permissionPushTitle(toolName, input),
303
309
  body: permissionPushBody(toolName, input),
@@ -331,6 +337,7 @@ function createSDKBridge(opts) {
331
337
  if (pushModule) {
332
338
  pushModule.sendPush({
333
339
  type: "error",
340
+ slug: slug,
334
341
  title: "Connection Lost",
335
342
  body: "Claude process disconnected: " + (err.message || "unknown error"),
336
343
  tag: "claude-error",
@@ -343,6 +350,8 @@ function createSDKBridge(opts) {
343
350
  session.queryInstance = null;
344
351
  session.messageQueue = null;
345
352
  session.abortController = null;
353
+ session.pendingPermissions = {};
354
+ session.pendingAskUser = {};
346
355
  }
347
356
  }
348
357
 
@@ -385,7 +394,10 @@ function createSDKBridge(opts) {
385
394
  try {
386
395
  sdk = await getSDK();
387
396
  } catch (e) {
397
+ session.isProcessing = false;
388
398
  send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
399
+ sendAndRecord(session, { type: "done", code: 1 });
400
+ sm.broadcastSessionList();
389
401
  return;
390
402
  }
391
403
 
@@ -428,6 +440,11 @@ function createSDKBridge(opts) {
428
440
  },
429
441
  };
430
442
 
443
+ if (dangerouslySkipPermissions) {
444
+ queryOptions.permissionMode = "bypassPermissions";
445
+ queryOptions.allowDangerouslySkipPermissions = true;
446
+ }
447
+
431
448
  if (session.cliSessionId) {
432
449
  queryOptions.resume = session.cliSessionId;
433
450
  if (session.lastRewindUuid) {
@@ -436,10 +453,21 @@ function createSDKBridge(opts) {
436
453
  }
437
454
  }
438
455
 
439
- session.queryInstance = sdk.query({
440
- prompt: session.messageQueue,
441
- options: queryOptions,
442
- });
456
+ try {
457
+ session.queryInstance = sdk.query({
458
+ prompt: session.messageQueue,
459
+ options: queryOptions,
460
+ });
461
+ } catch (e) {
462
+ session.isProcessing = false;
463
+ session.queryInstance = null;
464
+ session.messageQueue = null;
465
+ session.abortController = null;
466
+ send({ type: "error", text: "Failed to start query: " + (e.message || e) });
467
+ sendAndRecord(session, { type: "done", code: 1 });
468
+ sm.broadcastSessionList();
469
+ return;
470
+ }
443
471
 
444
472
  processQueryStream(session).catch(function(err) {
445
473
  });
@@ -466,7 +494,7 @@ function createSDKBridge(opts) {
466
494
 
467
495
  function permissionPushTitle(toolName, input) {
468
496
  if (!input) return "Claude wants to use " + toolName;
469
- var file = input.file_path ? input.file_path.split("/").pop() : "";
497
+ var file = input.file_path ? input.file_path.split(/[/\\]/).pop() : "";
470
498
  switch (toolName) {
471
499
  case "Bash": return "Claude wants to run a command";
472
500
  case "Edit": return "Claude wants to edit " + (file || "a file");
@@ -487,7 +515,7 @@ function createSDKBridge(opts) {
487
515
  if (toolName === "Bash" && input.command) {
488
516
  text = input.command;
489
517
  } else if (toolName === "Edit" && input.file_path) {
490
- text = input.file_path.split("/").pop() + ": " + (input.old_string || "").substring(0, 40) + " \u2192 " + (input.new_string || "").substring(0, 40);
518
+ text = input.file_path.split(/[/\\]/).pop() + ": " + (input.old_string || "").substring(0, 40) + " \u2192 " + (input.new_string || "").substring(0, 40);
491
519
  } else if (toolName === "Write" && input.file_path) {
492
520
  text = input.file_path;
493
521
  } else if (input.file_path) {
@@ -515,9 +543,14 @@ function createSDKBridge(opts) {
515
543
  var mq = createMessageQueue();
516
544
  mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
517
545
  mq.end();
546
+ var warmupOptions = { cwd: cwd, settingSources: ["user", "project", "local"], abortController: ac };
547
+ if (dangerouslySkipPermissions) {
548
+ warmupOptions.permissionMode = "bypassPermissions";
549
+ warmupOptions.allowDangerouslySkipPermissions = true;
550
+ }
518
551
  var stream = sdk.query({
519
552
  prompt: mq,
520
- options: { cwd: cwd, settingSources: ["user", "project", "local"], abortController: ac },
553
+ options: warmupOptions,
521
554
  });
522
555
  for await (var msg of stream) {
523
556
  if (msg.type === "system" && msg.subtype === "init") {
package/lib/server.js CHANGED
@@ -114,7 +114,7 @@ function stripPrefix(urlPath, slug) {
114
114
 
115
115
  /**
116
116
  * Create a multi-project server.
117
- * opts: { tlsOptions, caPath, pinHash, port, debug }
117
+ * opts: { tlsOptions, caPath, pinHash, port, debug, dangerouslySkipPermissions }
118
118
  */
119
119
  function createServer(opts) {
120
120
  var tlsOptions = opts.tlsOptions || null;
@@ -122,6 +122,8 @@ function createServer(opts) {
122
122
  var pinHash = opts.pinHash || null;
123
123
  var portNum = opts.port || 2633;
124
124
  var debug = opts.debug || false;
125
+ var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
126
+ var lanHost = opts.lanHost || null;
125
127
 
126
128
  var authToken = pinHash || null;
127
129
  var realVersion = require("../package.json").version;
@@ -228,8 +230,9 @@ function createServer(opts) {
228
230
  req.on("data", function (chunk) { body += chunk; });
229
231
  req.on("end", function () {
230
232
  try {
231
- var sub = JSON.parse(body);
232
- pushModule.addSubscription(sub);
233
+ var parsed = JSON.parse(body);
234
+ var sub = parsed.subscription || parsed;
235
+ pushModule.addSubscription(sub, parsed.replaceEndpoint);
233
236
  res.writeHead(200, { "Content-Type": "application/json" });
234
237
  res.end('{"ok":true}');
235
238
  } catch (e) {
@@ -402,7 +405,19 @@ function createServer(opts) {
402
405
  if (origin) {
403
406
  try {
404
407
  var originUrl = new URL(origin);
405
- if (String(originUrl.port || (originUrl.protocol === "https:" ? "443" : "80")) !== String(portNum)) {
408
+ var originPort = String(originUrl.port || (originUrl.protocol === "https:" ? "443" : "80"));
409
+ // Extract port from Host header for reverse proxy support.
410
+ // Use URL parser to correctly handle IPv6 addresses (e.g. [::1])
411
+ // and infer default port from origin protocol (not backend tlsOptions)
412
+ // so TLS-terminating proxies on :443 with HTTP backends work.
413
+ var hostPort;
414
+ try {
415
+ var hostUrl = new URL(originUrl.protocol + "//" + (req.headers.host || ""));
416
+ hostPort = String(hostUrl.port || (originUrl.protocol === "https:" ? "443" : "80"));
417
+ } catch (e2) {
418
+ hostPort = String(portNum);
419
+ }
420
+ if (originPort !== String(portNum) && originPort !== hostPort) {
406
421
  socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
407
422
  socket.destroy();
408
423
  return;
@@ -447,7 +462,9 @@ function createServer(opts) {
447
462
  title: title || null,
448
463
  pushModule: pushModule,
449
464
  debug: debug,
465
+ dangerouslySkipPermissions: dangerouslySkipPermissions,
450
466
  currentVersion: currentVersion,
467
+ lanHost: lanHost,
451
468
  getProjectCount: function () { return projects.size; },
452
469
  getProjectList: function () {
453
470
  var list = [];
@@ -487,6 +504,20 @@ function createServer(opts) {
487
504
  authToken = hash;
488
505
  }
489
506
 
507
+ function broadcastAll(msg) {
508
+ projects.forEach(function (ctx) {
509
+ ctx.send(msg);
510
+ });
511
+ }
512
+
513
+ function destroyAll() {
514
+ projects.forEach(function (ctx, slug) {
515
+ console.log("[server] Destroying project:", slug);
516
+ ctx.destroy();
517
+ });
518
+ projects.clear();
519
+ }
520
+
490
521
  return {
491
522
  server: server,
492
523
  onboardingServer: onboardingServer,
@@ -496,6 +527,8 @@ function createServer(opts) {
496
527
  getProjects: getProjects,
497
528
  setProjectTitle: setProjectTitle,
498
529
  setAuthToken: setAuthToken,
530
+ broadcastAll: broadcastAll,
531
+ destroyAll: destroyAll,
499
532
  };
500
533
  }
501
534
 
package/lib/sessions.js CHANGED
@@ -25,13 +25,15 @@ function createSessionManager(opts) {
25
25
  if (!session.cliSessionId) return;
26
26
  session.lastActivity = Date.now();
27
27
  try {
28
- var meta = JSON.stringify({
28
+ var metaObj = {
29
29
  type: "meta",
30
30
  localId: session.localId,
31
31
  cliSessionId: session.cliSessionId,
32
32
  title: session.title,
33
33
  createdAt: session.createdAt,
34
- });
34
+ };
35
+ if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
36
+ var meta = JSON.stringify(metaObj);
35
37
  var lines = [meta];
36
38
  for (var i = 0; i < session.history.length; i++) {
37
39
  lines.push(JSON.stringify(session.history[i]));
@@ -105,6 +107,7 @@ function createSessionManager(opts) {
105
107
  lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
106
108
  history: loaded[i].history,
107
109
  messageUUIDs: messageUUIDs,
110
+ lastRewindUuid: m.lastRewindUuid || null,
108
111
  };
109
112
  sessions.set(localId, session);
110
113
  }
@@ -278,9 +281,15 @@ function createSessionManager(opts) {
278
281
  if (sessions.size === 0) {
279
282
  createSession();
280
283
  } else {
281
- // Activate the most recent session
282
- var lastSession = [...sessions.values()].pop();
283
- activeSessionId = lastSession.localId;
284
+ // Activate the most recently used session
285
+ var allSessions = [...sessions.values()];
286
+ var mostRecent = allSessions[0];
287
+ for (var i = 1; i < allSessions.length; i++) {
288
+ if ((allSessions[i].lastActivity || 0) > (mostRecent.lastActivity || 0)) {
289
+ mostRecent = allSessions[i];
290
+ }
291
+ }
292
+ activeSessionId = mostRecent.localId;
284
293
  }
285
294
 
286
295
  function searchSessions(query) {
package/lib/terminal.js CHANGED
@@ -8,7 +8,8 @@ try {
8
8
  function createTerminal(cwd, cols, rows) {
9
9
  if (!pty) return null;
10
10
 
11
- var shell = process.env.SHELL || "/bin/bash";
11
+ var shell = process.env.SHELL
12
+ || (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/bash");
12
13
  var term = pty.spawn(shell, [], {
13
14
  name: "xterm-256color",
14
15
  cols: cols || 80,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-relay",
3
- "version": "2.2.3",
3
+ "version": "2.3.0",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "claude-relay": "./bin/cli.js"