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.
- package/README.md +21 -5
- package/bin/cli.js +214 -9
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +3 -2
- package/lib/daemon.js +45 -1
- package/lib/pages.js +8 -1
- package/lib/project.js +121 -12
- package/lib/public/app.js +411 -87
- package/lib/public/css/base.css +41 -7
- package/lib/public/css/diff.css +6 -6
- package/lib/public/css/filebrowser.css +62 -52
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/input.css +11 -9
- package/lib/public/css/menus.css +82 -23
- package/lib/public/css/messages.css +183 -35
- package/lib/public/css/overlays.css +166 -50
- package/lib/public/css/rewind.css +17 -17
- package/lib/public/css/sidebar.css +210 -137
- package/lib/public/index.html +75 -42
- package/lib/public/modules/filebrowser.js +2 -1
- package/lib/public/modules/markdown.js +10 -10
- package/lib/public/modules/notifications.js +38 -1
- package/lib/public/modules/sidebar.js +109 -31
- package/lib/public/modules/terminal.js +84 -23
- package/lib/public/modules/theme.js +622 -0
- package/lib/public/modules/tools.js +247 -4
- package/lib/public/modules/utils.js +21 -5
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +95 -0
- package/lib/server.js +45 -3
- package/lib/sessions.js +16 -3
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/claude-light.json +9 -0
- package/lib/themes/claude.json +9 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- 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
|
-
|
|
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"));}
|
|
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
|
-
|
|
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
|
}
|