claude-relay 2.3.0 → 2.4.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.
Files changed (54) hide show
  1. package/README.md +21 -5
  2. package/bin/cli.js +214 -9
  3. package/lib/cli-sessions.js +270 -0
  4. package/lib/config.js +3 -2
  5. package/lib/daemon.js +45 -1
  6. package/lib/pages.js +8 -1
  7. package/lib/project.js +121 -12
  8. package/lib/public/app.js +411 -87
  9. package/lib/public/css/base.css +41 -7
  10. package/lib/public/css/diff.css +6 -6
  11. package/lib/public/css/filebrowser.css +62 -52
  12. package/lib/public/css/highlight.css +144 -0
  13. package/lib/public/css/input.css +11 -9
  14. package/lib/public/css/menus.css +82 -23
  15. package/lib/public/css/messages.css +183 -35
  16. package/lib/public/css/overlays.css +166 -50
  17. package/lib/public/css/rewind.css +17 -17
  18. package/lib/public/css/sidebar.css +210 -137
  19. package/lib/public/index.html +75 -42
  20. package/lib/public/modules/filebrowser.js +2 -1
  21. package/lib/public/modules/markdown.js +10 -10
  22. package/lib/public/modules/notifications.js +38 -1
  23. package/lib/public/modules/sidebar.js +109 -31
  24. package/lib/public/modules/terminal.js +84 -23
  25. package/lib/public/modules/theme.js +622 -0
  26. package/lib/public/modules/tools.js +247 -4
  27. package/lib/public/modules/utils.js +21 -5
  28. package/lib/public/style.css +1 -0
  29. package/lib/sdk-bridge.js +95 -0
  30. package/lib/server.js +45 -3
  31. package/lib/sessions.js +16 -3
  32. package/lib/themes/ayu-light.json +9 -0
  33. package/lib/themes/catppuccin-latte.json +9 -0
  34. package/lib/themes/catppuccin-mocha.json +9 -0
  35. package/lib/themes/claude-light.json +9 -0
  36. package/lib/themes/claude.json +9 -0
  37. package/lib/themes/dracula.json +9 -0
  38. package/lib/themes/everforest-light.json +9 -0
  39. package/lib/themes/everforest.json +9 -0
  40. package/lib/themes/github-light.json +9 -0
  41. package/lib/themes/gruvbox-dark.json +9 -0
  42. package/lib/themes/gruvbox-light.json +9 -0
  43. package/lib/themes/monokai.json +9 -0
  44. package/lib/themes/nord-light.json +9 -0
  45. package/lib/themes/nord.json +9 -0
  46. package/lib/themes/one-dark.json +9 -0
  47. package/lib/themes/one-light.json +9 -0
  48. package/lib/themes/rose-pine-dawn.json +9 -0
  49. package/lib/themes/rose-pine.json +9 -0
  50. package/lib/themes/solarized-dark.json +9 -0
  51. package/lib/themes/solarized-light.json +9 -0
  52. package/lib/themes/tokyo-night-light.json +9 -0
  53. package/lib/themes/tokyo-night.json +9 -0
  54. package/package.json +2 -1
package/lib/config.js CHANGED
@@ -3,7 +3,7 @@ var path = require("path");
3
3
  var os = require("os");
4
4
  var net = require("net");
5
5
 
6
- var CONFIG_DIR = path.join(os.homedir(), ".claude-relay");
6
+ var CONFIG_DIR = process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay");
7
7
  var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
8
8
  var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
9
9
 
@@ -13,7 +13,8 @@ function configPath() {
13
13
 
14
14
  function socketPath() {
15
15
  if (process.platform === "win32") {
16
- return "\\\\.\\pipe\\claude-relay-daemon";
16
+ var pipeName = process.env.CLAUDE_RELAY_HOME ? "claude-relay-dev-daemon" : "claude-relay-daemon";
17
+ return "\\\\.\\pipe\\" + pipeName;
17
18
  }
18
19
  return path.join(CONFIG_DIR, "daemon.sock");
19
20
  }
package/lib/daemon.js CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Polyfill Symbol.dispose/asyncDispose for Node 18 (used by claude-agent-sdk)
4
+ if (!Symbol.dispose) Symbol.dispose = Symbol("Symbol.dispose");
5
+ if (!Symbol.asyncDispose) Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
6
+
3
7
  var fs = require("fs");
4
8
  var path = require("path");
5
9
  var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
@@ -20,7 +24,7 @@ try {
20
24
  var tlsOptions = null;
21
25
  if (config.tls) {
22
26
  var os = require("os");
23
- var certDir = path.join(os.homedir(), ".claude-relay", "certs");
27
+ var certDir = path.join(process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay"), "certs");
24
28
  var keyPath = path.join(certDir, "key.pem");
25
29
  var certPath = path.join(certDir, "cert.pem");
26
30
  try {
@@ -69,6 +73,46 @@ var relay = createServer({
69
73
  debug: config.debug || false,
70
74
  dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
71
75
  lanHost: lanIp ? lanIp + ":" + config.port : null,
76
+ onAddProject: function (absPath) {
77
+ // Check if already registered
78
+ for (var j = 0; j < config.projects.length; j++) {
79
+ if (config.projects[j].path === absPath) {
80
+ return { ok: true, slug: config.projects[j].slug, existing: true };
81
+ }
82
+ }
83
+ var slugs = config.projects.map(function (p) { return p.slug; });
84
+ var slug = generateSlug(absPath, slugs);
85
+ relay.addProject(absPath, slug);
86
+ config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
87
+ saveConfig(config);
88
+ try { syncClayrc(config.projects); } catch (e) {}
89
+ console.log("[daemon] Added project (web):", slug, "→", absPath);
90
+ // Broadcast updated project list to all clients
91
+ relay.broadcastAll({
92
+ type: "projects_updated",
93
+ projects: relay.getProjects(),
94
+ projectCount: config.projects.length,
95
+ });
96
+ return { ok: true, slug: slug };
97
+ },
98
+ onRemoveProject: function (slug) {
99
+ var found = false;
100
+ for (var j = 0; j < config.projects.length; j++) {
101
+ if (config.projects[j].slug === slug) { found = true; break; }
102
+ }
103
+ if (!found) return { ok: false, error: "Project not found" };
104
+ relay.removeProject(slug);
105
+ config.projects = config.projects.filter(function (p) { return p.slug !== slug; });
106
+ saveConfig(config);
107
+ try { syncClayrc(config.projects); } catch (e) {}
108
+ console.log("[daemon] Removed project (web):", slug);
109
+ relay.broadcastAll({
110
+ type: "projects_updated",
111
+ projects: relay.getProjects(),
112
+ projectCount: config.projects.length,
113
+ });
114
+ return { ok: true };
115
+ },
72
116
  });
73
117
 
74
118
  // --- Register projects ---
package/lib/pages.js CHANGED
@@ -695,8 +695,15 @@ function dashboardPageHtml(projects, version) {
695
695
  '<div class="subtitle">Select a project</div>' +
696
696
  '<div class="cards">' + cards + '</div>' +
697
697
  '<div class="footer">v' + escapeHtml(version || "") + '</div>' +
698
+ '<style>' +
699
+ '.toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#3A3936;border:1px solid #DA7756;color:#E8E5DE;padding:12px 24px;border-radius:8px;font-size:14px;z-index:999;opacity:0;transition:opacity .3s}' +
700
+ '.toast.show{opacity:1}' +
701
+ '</style>' +
698
702
  '<script>var s=window.matchMedia("(display-mode:standalone)").matches||navigator.standalone;' +
699
- 'if(s&&!localStorage.getItem("setup-done")){var t=/^100\\./.test(location.hostname);location.replace("/setup"+(t?"":"?mode=lan"));}</script>' +
703
+ 'if(s&&!localStorage.getItem("setup-done")){var t=/^100\\./.test(location.hostname);location.replace("/setup"+(t?"":"?mode=lan"));}' +
704
+ 'var p=new URLSearchParams(location.search);var g=p.get("gone");' +
705
+ 'if(g){history.replaceState(null,"","/");var d=document.createElement("div");d.className="toast";d.textContent="Project \\""+g+"\\" is no longer available";document.body.appendChild(d);' +
706
+ 'requestAnimationFrame(function(){d.className="toast show"});setTimeout(function(){d.className="toast";setTimeout(function(){d.remove()},300)},5000);}</script>' +
700
707
  '</body></html>';
701
708
  }
702
709
 
package/lib/project.js CHANGED
@@ -320,10 +320,52 @@ function createProjectContext(opts) {
320
320
 
321
321
  if (msg.type === "resume_session") {
322
322
  if (!msg.cliSessionId) return;
323
- sm.resumeSession(msg.cliSessionId);
323
+ var cliSess = require("./cli-sessions");
324
+ cliSess.readCliSessionHistory(cwd, msg.cliSessionId).then(function (history) {
325
+ var title = "Resumed session";
326
+ for (var i = 0; i < history.length; i++) {
327
+ if (history[i].type === "user_message" && history[i].text) {
328
+ title = history[i].text.substring(0, 50);
329
+ break;
330
+ }
331
+ }
332
+ sm.resumeSession(msg.cliSessionId, { history: history, title: title });
333
+ }).catch(function () {
334
+ sm.resumeSession(msg.cliSessionId);
335
+ });
324
336
  return;
325
337
  }
326
338
 
339
+ if (msg.type === "list_cli_sessions") {
340
+ var cliSessions = require("./cli-sessions");
341
+ var _fs = require("fs");
342
+ var _path = require("path");
343
+ // Collect session IDs already in relay (in-memory + persisted on disk)
344
+ var relayIds = {};
345
+ sm.sessions.forEach(function (s) {
346
+ if (s.cliSessionId) relayIds[s.cliSessionId] = true;
347
+ });
348
+ try {
349
+ var sessDir = _path.join(cwd, ".claude-relay", "sessions");
350
+ var diskFiles = _fs.readdirSync(sessDir);
351
+ for (var fi = 0; fi < diskFiles.length; fi++) {
352
+ if (diskFiles[fi].endsWith(".jsonl")) {
353
+ relayIds[diskFiles[fi].replace(".jsonl", "")] = true;
354
+ }
355
+ }
356
+ } catch (e) {}
357
+ cliSessions.listCliSessions(cwd).then(function (sessions) {
358
+ var filtered = sessions.filter(function (s) {
359
+ return !relayIds[s.sessionId];
360
+ });
361
+ sendTo(ws, { type: "cli_session_list", sessions: filtered });
362
+ }).catch(function () {
363
+ sendTo(ws, { type: "cli_session_list", sessions: [] });
364
+ });
365
+ return;
366
+ }
367
+
368
+
327
369
  if (msg.type === "switch_session") {
328
370
  if (msg.id && sm.sessions.has(msg.id)) {
329
371
  sm.switchSession(msg.id);
@@ -550,6 +592,84 @@ function createProjectContext(opts) {
550
592
  return;
551
593
  }
552
594
 
595
+ // --- Browse directories (for add-project autocomplete) ---
596
+ if (msg.type === "browse_dir") {
597
+ var rawPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
598
+ var absTarget = path.resolve(rawPath);
599
+ var parentDir, prefix;
600
+ try {
601
+ var stat = fs.statSync(absTarget);
602
+ if (stat.isDirectory()) {
603
+ // Input is an existing directory — list its children
604
+ parentDir = absTarget;
605
+ prefix = "";
606
+ } else {
607
+ parentDir = path.dirname(absTarget);
608
+ prefix = path.basename(absTarget).toLowerCase();
609
+ }
610
+ } catch (e) {
611
+ // Path doesn't exist — list parent and filter by typed prefix
612
+ parentDir = path.dirname(absTarget);
613
+ prefix = path.basename(absTarget).toLowerCase();
614
+ }
615
+ try {
616
+ var dirItems = fs.readdirSync(parentDir, { withFileTypes: true });
617
+ var dirEntries = [];
618
+ for (var di = 0; di < dirItems.length; di++) {
619
+ var d = dirItems[di];
620
+ if (!d.isDirectory()) continue;
621
+ if (d.name.charAt(0) === ".") continue;
622
+ if (IGNORED_DIRS.has(d.name)) continue;
623
+ if (prefix && !d.name.toLowerCase().startsWith(prefix)) continue;
624
+ dirEntries.push({ name: d.name, path: path.join(parentDir, d.name) });
625
+ }
626
+ dirEntries.sort(function (a, b) { return a.name.localeCompare(b.name); });
627
+ sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: dirEntries });
628
+ } catch (e) {
629
+ sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message });
630
+ }
631
+ return;
632
+ }
633
+
634
+ // --- Add project from web UI ---
635
+ if (msg.type === "add_project") {
636
+ var addPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
637
+ var addAbs = path.resolve(addPath);
638
+ try {
639
+ var addStat = fs.statSync(addAbs);
640
+ if (!addStat.isDirectory()) {
641
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Not a directory" });
642
+ return;
643
+ }
644
+ } catch (e) {
645
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Directory not found" });
646
+ return;
647
+ }
648
+ if (typeof opts.onAddProject === "function") {
649
+ var result = opts.onAddProject(addAbs);
650
+ sendTo(ws, { type: "add_project_result", ok: result.ok, slug: result.slug, error: result.error, existing: result.existing });
651
+ } else {
652
+ sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
653
+ }
654
+ return;
655
+ }
656
+
657
+ // --- Remove project from web UI ---
658
+ if (msg.type === "remove_project") {
659
+ var removeSlug = msg.slug;
660
+ if (!removeSlug) {
661
+ sendTo(ws, { type: "remove_project_result", ok: false, error: "Missing slug" });
662
+ return;
663
+ }
664
+ if (typeof opts.onRemoveProject === "function") {
665
+ var removeResult = opts.onRemoveProject(removeSlug);
666
+ sendTo(ws, { type: "remove_project_result", ok: removeResult.ok, slug: removeSlug, error: removeResult.error });
667
+ } else {
668
+ sendTo(ws, { type: "remove_project_result", ok: false, error: "Not supported" });
669
+ }
670
+ return;
671
+ }
672
+
553
673
  // --- File browser ---
554
674
  if (msg.type === "fs_list") {
555
675
  var fsDir = safePath(cwd, msg.path || ".");
@@ -911,17 +1031,6 @@ function createProjectContext(opts) {
911
1031
  if (clients.size === 0) {
912
1032
  stopFileWatch();
913
1033
  stopAllDirWatches();
914
- // Abort all running queries when no clients are connected
915
- var aborted = 0;
916
- sm.sessions.forEach(function (session) {
917
- if (session.isProcessing && session.abortController) {
918
- try { session.abortController.abort(); } catch (e) {}
919
- aborted++;
920
- }
921
- });
922
- if (aborted > 0) {
923
- console.log("[project:" + slug + "] No clients connected, aborted " + aborted + " active queries");
924
- }
925
1034
  }
926
1035
  broadcastClientCount();
927
1036
  }