claude-relay 1.1.1 → 1.2.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 +5 -5
- package/bin/cli.js +71 -31
- package/lib/public/app.js +168 -6
- package/lib/public/favicon.svg +26 -0
- package/lib/public/index.html +2 -1
- package/lib/public/style.css +43 -3
- package/lib/server.js +49 -0
- package/lib/updater.js +96 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,11 +71,7 @@ Browser (any device) <--> claude-relay (your machine) <--> Claude Agent SDK
|
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
# Run directly (no install needed)
|
|
74
|
-
npx claude-relay
|
|
75
|
-
|
|
76
|
-
# Or install globally
|
|
77
|
-
npm install -g claude-relay
|
|
78
|
-
claude-relay
|
|
74
|
+
npx claude-relay@latest
|
|
79
75
|
```
|
|
80
76
|
|
|
81
77
|
## Usage
|
|
@@ -120,6 +116,10 @@ We strongly recommend using a private network layer such as [Tailscale](https://
|
|
|
120
116
|
|
|
121
117
|
If you choose to expose it beyond your private network, that's your call. **Entirely at your own risk.** The authors assume no responsibility for any damage, data loss, or security incidents.
|
|
122
118
|
|
|
119
|
+
## Disclaimer
|
|
120
|
+
|
|
121
|
+
claude-relay is an independent, unofficial project. It is not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" is a trademark of Anthropic.
|
|
122
|
+
|
|
123
123
|
## License
|
|
124
124
|
|
|
125
125
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const { createServer } = require("../lib/server");
|
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
11
|
let port = 2633;
|
|
12
12
|
let useHttps = true;
|
|
13
|
+
let skipUpdate = false;
|
|
13
14
|
|
|
14
15
|
for (let i = 0; i < args.length; i++) {
|
|
15
16
|
if (args[i] === "-p" || args[i] === "--port") {
|
|
@@ -21,12 +22,15 @@ for (let i = 0; i < args.length; i++) {
|
|
|
21
22
|
i++;
|
|
22
23
|
} else if (args[i] === "--no-https") {
|
|
23
24
|
useHttps = false;
|
|
25
|
+
} else if (args[i] === "--no-update" || args[i] === "--skip-update") {
|
|
26
|
+
skipUpdate = true;
|
|
24
27
|
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
25
|
-
console.log("Usage: claude-relay [-p|--port <port>] [--no-https]");
|
|
28
|
+
console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update]");
|
|
26
29
|
console.log("");
|
|
27
30
|
console.log("Options:");
|
|
28
31
|
console.log(" -p, --port <port> Port to listen on (default: 2633)");
|
|
29
32
|
console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
|
|
33
|
+
console.log(" --no-update Skip auto-update check on startup");
|
|
30
34
|
process.exit(0);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -102,8 +106,8 @@ var caffeinateProc = null;
|
|
|
102
106
|
function startCaffeinate() {
|
|
103
107
|
var { spawn } = require("child_process");
|
|
104
108
|
caffeinateProc = spawn("caffeinate", ["-di"], { stdio: "ignore", detached: false });
|
|
105
|
-
caffeinateProc.on("error", function() { caffeinateProc = null; });
|
|
106
|
-
process.on("exit", function() { if (caffeinateProc) caffeinateProc.kill(); });
|
|
109
|
+
caffeinateProc.on("error", function () { caffeinateProc = null; });
|
|
110
|
+
process.on("exit", function () { if (caffeinateProc) caffeinateProc.kill(); });
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
// --- Certs ---
|
|
@@ -119,7 +123,7 @@ function ensureCerts(ip) {
|
|
|
119
123
|
"rootCA.pem"
|
|
120
124
|
);
|
|
121
125
|
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
122
|
-
} catch (e) {}
|
|
126
|
+
} catch (e) { }
|
|
123
127
|
|
|
124
128
|
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
125
129
|
return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
@@ -142,36 +146,61 @@ function ensureCerts(ip) {
|
|
|
142
146
|
return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
// --- Logo ---
|
|
150
|
+
function printLogo() {
|
|
151
|
+
var c = "\x1b[38;2;218;119;86m";
|
|
152
|
+
var r = a.reset;
|
|
153
|
+
var lines = [
|
|
154
|
+
" ██████╗ ██╗ █████╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗ ███████╗ ██╗ █████╗ ██╗ ██╗",
|
|
155
|
+
" ██╔════╝ ██║ ██╔══██╗ ██║ ██║ ██╔══██╗ ██╔════╝ ██╔══██╗ ██╔════╝ ██║ ██╔══██╗ ╚██╗ ██╔╝",
|
|
156
|
+
" ██║ ██║ ███████║ ██║ ██║ ██║ ██║ █████╗ ██████╔╝ █████╗ ██║ ███████║ ╚████╔╝ ",
|
|
157
|
+
" ██║ ██║ ██╔══██║ ██║ ██║ ██║ ██║ ██╔══╝ ██╔══██╗ ██╔══╝ ██║ ██╔══██║ ╚██╔╝ ",
|
|
158
|
+
" ╚██████╗ ███████╗ ██║ ██║ ╚██████╔╝ ██████╔╝ ███████╗ ██║ ██║ ███████╗ ███████╗ ██║ ██║ ██║ ",
|
|
159
|
+
" ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ",
|
|
160
|
+
];
|
|
161
|
+
console.log("");
|
|
162
|
+
for (var i = 0; i < lines.length; i++) {
|
|
163
|
+
console.log(c + lines[i] + r);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
// --- Interactive setup (clack-style) ---
|
|
146
168
|
function setup(callback) {
|
|
169
|
+
console.clear();
|
|
170
|
+
printLogo();
|
|
147
171
|
log("");
|
|
148
|
-
log(sym.pointer + " " + a.bold + "Claude Relay" + a.reset);
|
|
149
|
-
log(sym.bar);
|
|
150
|
-
log(sym.warn + " " + a.yellow + a.bold + "READ BEFORE CONTINUING" + a.reset);
|
|
172
|
+
log(sym.pointer + " " + a.bold + "Claude Relay" + a.reset + a.dim + " · Unofficial, open-source project" + a.reset);
|
|
151
173
|
log(sym.bar);
|
|
152
|
-
log(sym.bar + " Anyone with
|
|
153
|
-
log(sym.bar + "
|
|
154
|
-
log(sym.bar + "
|
|
155
|
-
log(sym.bar);
|
|
156
|
-
log(sym.bar + " We strongly recommend using a private network layer such as");
|
|
157
|
-
log(sym.bar + " " + a.bold + "Tailscale" + a.reset + ", " + a.bold + "WireGuard" + a.reset + ", or a " + a.bold + "VPN" + a.reset + ".");
|
|
174
|
+
log(sym.bar + " " + a.dim + "Anyone with the URL gets full Claude Code access to this machine." + a.reset);
|
|
175
|
+
log(sym.bar + " " + a.dim + "Use a private network (Tailscale, VPN)." + a.reset);
|
|
176
|
+
log(sym.bar + " " + a.dim + "The authors assume no responsibility for any damage or data loss." + a.reset);
|
|
158
177
|
log(sym.bar);
|
|
159
178
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
log(sym.
|
|
163
|
-
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
179
|
+
promptToggle("Accept and continue", null, true, function (accepted) {
|
|
180
|
+
if (!accepted) {
|
|
181
|
+
log(sym.end + " " + a.dim + "Aborted." + a.reset);
|
|
164
182
|
log("");
|
|
183
|
+
process.exit(0);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
log(sym.bar);
|
|
165
187
|
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
promptPin(function (pin) {
|
|
189
|
+
promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
|
|
190
|
+
log(sym.bar);
|
|
191
|
+
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
192
|
+
log("");
|
|
193
|
+
|
|
194
|
+
if (keepAwake) startCaffeinate();
|
|
195
|
+
callback(pin);
|
|
196
|
+
});
|
|
168
197
|
});
|
|
169
198
|
});
|
|
170
199
|
}
|
|
171
200
|
|
|
172
201
|
function promptPin(callback) {
|
|
173
202
|
log(sym.pointer + " " + a.bold + "PIN protection" + a.reset);
|
|
174
|
-
log(sym.bar + " " + a.dim + "6-digit PIN
|
|
203
|
+
log(sym.bar + " " + a.dim + "Require a 6-digit PIN to access the web UI. Enter to skip." + a.reset);
|
|
175
204
|
process.stdout.write(" " + sym.bar + " ");
|
|
176
205
|
|
|
177
206
|
var pin = "";
|
|
@@ -221,8 +250,8 @@ function promptPin(callback) {
|
|
|
221
250
|
});
|
|
222
251
|
}
|
|
223
252
|
|
|
224
|
-
function promptToggle(title, desc, callback) {
|
|
225
|
-
var value = false;
|
|
253
|
+
function promptToggle(title, desc, defaultValue, callback) {
|
|
254
|
+
var value = defaultValue || false;
|
|
226
255
|
|
|
227
256
|
function renderToggle() {
|
|
228
257
|
var yes = value
|
|
@@ -234,12 +263,17 @@ function promptToggle(title, desc, callback) {
|
|
|
234
263
|
return yes + a.dim + " / " + a.reset + no;
|
|
235
264
|
}
|
|
236
265
|
|
|
266
|
+
var lines = 2;
|
|
237
267
|
log(sym.pointer + " " + a.bold + title + a.reset);
|
|
238
|
-
|
|
268
|
+
if (desc) {
|
|
269
|
+
log(sym.bar + " " + a.dim + desc + a.reset);
|
|
270
|
+
lines = 3;
|
|
271
|
+
}
|
|
239
272
|
process.stdout.write(" " + sym.bar + " " + renderToggle());
|
|
240
273
|
|
|
241
274
|
process.stdin.setRawMode(true);
|
|
242
275
|
process.stdin.resume();
|
|
276
|
+
process.stdin.setEncoding("utf8");
|
|
243
277
|
|
|
244
278
|
process.stdin.on("data", function onToggle(ch) {
|
|
245
279
|
if (ch === "\x1b[D" || ch === "\x1b[C" || ch === "\t") {
|
|
@@ -257,14 +291,14 @@ function promptToggle(title, desc, callback) {
|
|
|
257
291
|
process.stdin.removeListener("data", onToggle);
|
|
258
292
|
process.stdout.write("\n");
|
|
259
293
|
|
|
260
|
-
clearUp(
|
|
294
|
+
clearUp(lines);
|
|
261
295
|
var result = value ? a.green + "Yes" + a.reset : a.dim + "No" + a.reset;
|
|
262
296
|
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + result);
|
|
263
297
|
|
|
264
298
|
callback(value);
|
|
265
299
|
} else if (ch === "\x03") {
|
|
266
300
|
process.stdout.write("\n");
|
|
267
|
-
clearUp(
|
|
301
|
+
clearUp(lines);
|
|
268
302
|
log(sym.end + " " + a.dim + "Cancelled" + a.reset);
|
|
269
303
|
process.exit(0);
|
|
270
304
|
}
|
|
@@ -296,7 +330,7 @@ function start(pin) {
|
|
|
296
330
|
var entryServer = result.entryServer;
|
|
297
331
|
var httpsServer = result.httpsServer;
|
|
298
332
|
|
|
299
|
-
entryServer.on("error", function(err) {
|
|
333
|
+
entryServer.on("error", function (err) {
|
|
300
334
|
if (err.code === "EADDRINUSE") {
|
|
301
335
|
log(a.red + "Port " + port + " is already in use." + a.reset);
|
|
302
336
|
log(a.dim + "Run: claude-relay -p <port>" + a.reset);
|
|
@@ -308,7 +342,7 @@ function start(pin) {
|
|
|
308
342
|
|
|
309
343
|
var httpsPort = port + 1;
|
|
310
344
|
if (httpsServer) {
|
|
311
|
-
httpsServer.on("error", function(err) {
|
|
345
|
+
httpsServer.on("error", function (err) {
|
|
312
346
|
if (err.code === "EADDRINUSE") {
|
|
313
347
|
log(a.red + "HTTPS port " + httpsPort + " is already in use." + a.reset);
|
|
314
348
|
} else {
|
|
@@ -319,13 +353,13 @@ function start(pin) {
|
|
|
319
353
|
httpsServer.listen(httpsPort);
|
|
320
354
|
}
|
|
321
355
|
|
|
322
|
-
entryServer.listen(port, function() {
|
|
356
|
+
entryServer.listen(port, function () {
|
|
323
357
|
var project = path.basename(cwd);
|
|
324
358
|
var url = "http://" + ip + ":" + port;
|
|
325
359
|
|
|
326
360
|
if (ip !== "localhost") {
|
|
327
|
-
qrcode.generate(url, { small: true }, function(code) {
|
|
328
|
-
var lines = code.split("\n").map(function(l) { return " " + l; }).join("\n");
|
|
361
|
+
qrcode.generate(url, { small: true }, function (code) {
|
|
362
|
+
var lines = code.split("\n").map(function (l) { return " " + l; }).join("\n");
|
|
329
363
|
console.log(lines);
|
|
330
364
|
console.log("");
|
|
331
365
|
log(a.bold + "Claude Relay" + a.reset + " running at " + a.bold + url + a.reset);
|
|
@@ -340,4 +374,10 @@ function start(pin) {
|
|
|
340
374
|
});
|
|
341
375
|
}
|
|
342
376
|
|
|
343
|
-
|
|
377
|
+
const { checkAndUpdate } = require("../lib/updater");
|
|
378
|
+
const currentVersion = require("../package.json").version;
|
|
379
|
+
|
|
380
|
+
(async () => {
|
|
381
|
+
const updated = await checkAndUpdate(currentVersion, skipUpdate);
|
|
382
|
+
if (!updated) setup(start);
|
|
383
|
+
})();
|
package/lib/public/app.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
var pendingImages = []; // [{data: base64, mediaType: "image/png"}]
|
|
42
42
|
var pendingPermissions = {}; // requestId -> container element
|
|
43
43
|
var cliSessionId = null;
|
|
44
|
+
var projectName = "";
|
|
44
45
|
|
|
45
46
|
var builtinCommands = [
|
|
46
47
|
{ name: "clear", desc: "Clear conversation" },
|
|
@@ -129,12 +130,29 @@
|
|
|
129
130
|
el.className = "session-item" + (s.active ? " active" : "");
|
|
130
131
|
el.dataset.sessionId = s.id;
|
|
131
132
|
|
|
132
|
-
var
|
|
133
|
+
var textSpan = document.createElement("span");
|
|
134
|
+
textSpan.className = "session-item-text";
|
|
135
|
+
var textHtml = "";
|
|
133
136
|
if (s.isProcessing) {
|
|
134
|
-
|
|
137
|
+
textHtml += '<span class="session-processing"></span>';
|
|
135
138
|
}
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
textHtml += escapeHtml(s.title || "New Session");
|
|
140
|
+
textSpan.innerHTML = textHtml;
|
|
141
|
+
el.appendChild(textSpan);
|
|
142
|
+
|
|
143
|
+
var deleteBtn = document.createElement("button");
|
|
144
|
+
deleteBtn.className = "session-delete-btn";
|
|
145
|
+
deleteBtn.innerHTML = iconHtml("trash-2");
|
|
146
|
+
deleteBtn.title = "Delete session";
|
|
147
|
+
deleteBtn.addEventListener("click", (function(id) {
|
|
148
|
+
return function(e) {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
if (ws && connected) {
|
|
151
|
+
ws.send(JSON.stringify({ type: "delete_session", id: id }));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
})(s.id));
|
|
155
|
+
el.appendChild(deleteBtn);
|
|
138
156
|
|
|
139
157
|
el.addEventListener("click", (function (id) {
|
|
140
158
|
return function () {
|
|
@@ -147,6 +165,21 @@
|
|
|
147
165
|
|
|
148
166
|
sessionListEl.appendChild(el);
|
|
149
167
|
}
|
|
168
|
+
refreshIcons();
|
|
169
|
+
updatePageTitle();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function updatePageTitle() {
|
|
173
|
+
var sessionTitle = "";
|
|
174
|
+
var activeItem = sessionListEl.querySelector(".session-item.active .session-item-text");
|
|
175
|
+
if (activeItem) sessionTitle = activeItem.textContent;
|
|
176
|
+
if (projectName && sessionTitle) {
|
|
177
|
+
document.title = projectName + " - " + sessionTitle;
|
|
178
|
+
} else if (projectName) {
|
|
179
|
+
document.title = projectName;
|
|
180
|
+
} else {
|
|
181
|
+
document.title = "Claude Relay";
|
|
182
|
+
}
|
|
150
183
|
}
|
|
151
184
|
|
|
152
185
|
function openSidebar() {
|
|
@@ -200,6 +233,7 @@
|
|
|
200
233
|
// --- Pixel character animation ---
|
|
201
234
|
var pixelAnimTimer = null;
|
|
202
235
|
var pixelBlocks = [];
|
|
236
|
+
var antennaBlocks = [];
|
|
203
237
|
|
|
204
238
|
(function initPixelAnim() {
|
|
205
239
|
var canvas = document.getElementById("pixel-canvas");
|
|
@@ -207,7 +241,10 @@
|
|
|
207
241
|
|
|
208
242
|
// Character grid: 1 = body, 2 = eye, 0 = empty
|
|
209
243
|
// 12 cols x 9 rows
|
|
244
|
+
// 0=empty, 1=body, 2=eye, 3=antenna
|
|
210
245
|
var grid = [
|
|
246
|
+
[0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0],
|
|
247
|
+
[0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0],
|
|
211
248
|
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
|
|
212
249
|
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
|
|
213
250
|
[0, 0, 1, 2, 1, 1, 1, 1, 2, 1, 0, 0],
|
|
@@ -223,15 +260,18 @@
|
|
|
223
260
|
var CELL = 12;
|
|
224
261
|
var accent = "#DA7756";
|
|
225
262
|
var eye = "#2F2E2B";
|
|
263
|
+
var antenna = "#E8E5DE";
|
|
226
264
|
|
|
227
265
|
for (var r = 0; r < grid.length; r++) {
|
|
228
266
|
for (var c = 0; c < grid[r].length; c++) {
|
|
229
267
|
if (grid[r][c] === 0) continue;
|
|
230
268
|
var el = document.createElement("div");
|
|
231
269
|
el.className = "px";
|
|
232
|
-
|
|
270
|
+
var v = grid[r][c];
|
|
271
|
+
el.style.background = v === 2 ? eye : v === 3 ? antenna : accent;
|
|
233
272
|
el.style.left = c * CELL + "px";
|
|
234
273
|
el.style.top = r * CELL + "px";
|
|
274
|
+
if (v === 3) antennaBlocks.push(el);
|
|
235
275
|
canvas.appendChild(el);
|
|
236
276
|
pixelBlocks.push(el);
|
|
237
277
|
}
|
|
@@ -239,6 +279,7 @@
|
|
|
239
279
|
})();
|
|
240
280
|
|
|
241
281
|
function pixelScatter() {
|
|
282
|
+
stopSpark();
|
|
242
283
|
for (var i = 0; i < pixelBlocks.length; i++) {
|
|
243
284
|
var el = pixelBlocks[i];
|
|
244
285
|
var angle = Math.random() * Math.PI * 2;
|
|
@@ -252,6 +293,8 @@
|
|
|
252
293
|
}
|
|
253
294
|
}
|
|
254
295
|
|
|
296
|
+
var sparkTimer = null;
|
|
297
|
+
|
|
255
298
|
function pixelAssemble() {
|
|
256
299
|
for (var i = 0; i < pixelBlocks.length; i++) {
|
|
257
300
|
(function (el, delay) {
|
|
@@ -262,6 +305,36 @@
|
|
|
262
305
|
}, delay);
|
|
263
306
|
})(pixelBlocks[i], Math.random() * 300);
|
|
264
307
|
}
|
|
308
|
+
startSpark();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function startSpark() {
|
|
312
|
+
stopSpark();
|
|
313
|
+
var count = 0;
|
|
314
|
+
sparkTimer = setInterval(function () {
|
|
315
|
+
for (var i = 0; i < antennaBlocks.length; i++) {
|
|
316
|
+
if (Math.random() < 0.4) {
|
|
317
|
+
antennaBlocks[i].style.background = "#FFF";
|
|
318
|
+
antennaBlocks[i].style.boxShadow = "0 0 6px 2px rgba(255,255,255,0.6)";
|
|
319
|
+
} else {
|
|
320
|
+
antennaBlocks[i].style.background = "#E8E5DE";
|
|
321
|
+
antennaBlocks[i].style.boxShadow = "none";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
count++;
|
|
325
|
+
if (count > 20) stopSpark();
|
|
326
|
+
}, 80);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function stopSpark() {
|
|
330
|
+
if (sparkTimer) {
|
|
331
|
+
clearInterval(sparkTimer);
|
|
332
|
+
sparkTimer = null;
|
|
333
|
+
}
|
|
334
|
+
for (var i = 0; i < antennaBlocks.length; i++) {
|
|
335
|
+
antennaBlocks[i].style.background = "#E8E5DE";
|
|
336
|
+
antennaBlocks[i].style.boxShadow = "none";
|
|
337
|
+
}
|
|
265
338
|
}
|
|
266
339
|
|
|
267
340
|
function startPixelAnim() {
|
|
@@ -291,6 +364,23 @@
|
|
|
291
364
|
}
|
|
292
365
|
}
|
|
293
366
|
|
|
367
|
+
// --- Dynamic favicon ---
|
|
368
|
+
var faviconSvg = null;
|
|
369
|
+
var faviconLink = document.querySelector('link[rel="icon"]');
|
|
370
|
+
|
|
371
|
+
function updateFavicon(bgColor) {
|
|
372
|
+
if (!faviconLink) return;
|
|
373
|
+
if (!faviconSvg) {
|
|
374
|
+
var xhr = new XMLHttpRequest();
|
|
375
|
+
xhr.open("GET", "/favicon.svg", false);
|
|
376
|
+
xhr.send();
|
|
377
|
+
if (xhr.status === 200) faviconSvg = xhr.responseText;
|
|
378
|
+
else return;
|
|
379
|
+
}
|
|
380
|
+
var svg = faviconSvg.replace(/fill="#57AB5A"/, 'fill="' + bgColor + '"');
|
|
381
|
+
faviconLink.href = "data:image/svg+xml," + encodeURIComponent(svg);
|
|
382
|
+
}
|
|
383
|
+
|
|
294
384
|
// --- Status & Activity ---
|
|
295
385
|
function setSendBtnMode(mode) {
|
|
296
386
|
if (mode === "stop") {
|
|
@@ -315,11 +405,13 @@
|
|
|
315
405
|
setSendBtnMode("send");
|
|
316
406
|
connectOverlay.classList.add("hidden");
|
|
317
407
|
stopVerbCycle();
|
|
408
|
+
updateFavicon("#57AB5A");
|
|
318
409
|
} else if (status === "processing") {
|
|
319
410
|
statusDot.classList.add("processing");
|
|
320
411
|
statusTextEl.textContent = "";
|
|
321
412
|
processing = true;
|
|
322
413
|
setSendBtnMode("stop");
|
|
414
|
+
updateFavicon("#E0943A");
|
|
323
415
|
} else {
|
|
324
416
|
statusTextEl.textContent = "Disconnected";
|
|
325
417
|
connected = false;
|
|
@@ -328,6 +420,7 @@
|
|
|
328
420
|
connectStatusEl.textContent = "Reconnecting...";
|
|
329
421
|
startVerbCycle();
|
|
330
422
|
startPixelAnim();
|
|
423
|
+
updateFavicon("#E5534B");
|
|
331
424
|
}
|
|
332
425
|
}
|
|
333
426
|
|
|
@@ -1220,7 +1313,9 @@
|
|
|
1220
1313
|
|
|
1221
1314
|
switch (msg.type) {
|
|
1222
1315
|
case "info":
|
|
1223
|
-
|
|
1316
|
+
projectName = msg.project || msg.cwd;
|
|
1317
|
+
projectNameEl.textContent = projectName;
|
|
1318
|
+
updatePageTitle();
|
|
1224
1319
|
break;
|
|
1225
1320
|
|
|
1226
1321
|
case "slash_commands":
|
|
@@ -1396,6 +1491,10 @@
|
|
|
1396
1491
|
processing = false;
|
|
1397
1492
|
setStatus("connected");
|
|
1398
1493
|
tools = {};
|
|
1494
|
+
if (document.hidden && notifPermission === "granted") {
|
|
1495
|
+
showDoneNotification();
|
|
1496
|
+
playDoneSound();
|
|
1497
|
+
}
|
|
1399
1498
|
break;
|
|
1400
1499
|
|
|
1401
1500
|
case "stderr":
|
|
@@ -1409,6 +1508,7 @@
|
|
|
1409
1508
|
case "error":
|
|
1410
1509
|
setActivity(null);
|
|
1411
1510
|
addSystemMessage(msg.text, true);
|
|
1511
|
+
updateFavicon("#E5534B");
|
|
1412
1512
|
break;
|
|
1413
1513
|
}
|
|
1414
1514
|
};
|
|
@@ -1769,6 +1869,68 @@
|
|
|
1769
1869
|
}
|
|
1770
1870
|
});
|
|
1771
1871
|
|
|
1872
|
+
// --- Browser notifications ---
|
|
1873
|
+
var notifPermission = ("Notification" in window) ? Notification.permission : "denied";
|
|
1874
|
+
|
|
1875
|
+
function requestNotifPermission() {
|
|
1876
|
+
if (!("Notification" in window)) return;
|
|
1877
|
+
if (Notification.permission === "granted") {
|
|
1878
|
+
notifPermission = "granted";
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (Notification.permission !== "denied") {
|
|
1882
|
+
Notification.requestPermission().then(function(p) {
|
|
1883
|
+
notifPermission = p;
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
document.addEventListener("click", function requestOnce() {
|
|
1889
|
+
requestNotifPermission();
|
|
1890
|
+
document.removeEventListener("click", requestOnce);
|
|
1891
|
+
}, { once: true });
|
|
1892
|
+
|
|
1893
|
+
function playDoneSound() {
|
|
1894
|
+
try {
|
|
1895
|
+
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
1896
|
+
var osc = ctx.createOscillator();
|
|
1897
|
+
var gain = ctx.createGain();
|
|
1898
|
+
osc.type = "sine";
|
|
1899
|
+
osc.frequency.value = 880;
|
|
1900
|
+
gain.gain.value = 0.1;
|
|
1901
|
+
osc.connect(gain);
|
|
1902
|
+
gain.connect(ctx.destination);
|
|
1903
|
+
osc.start();
|
|
1904
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
|
1905
|
+
osc.stop(ctx.currentTime + 0.3);
|
|
1906
|
+
} catch(e) {}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function showDoneNotification() {
|
|
1910
|
+
var lastAssistant = messagesEl.querySelector(".msg-assistant:last-of-type .md-content");
|
|
1911
|
+
var preview = lastAssistant ? lastAssistant.textContent.substring(0, 100) : "Response ready";
|
|
1912
|
+
|
|
1913
|
+
var sessionTitle = "Claude";
|
|
1914
|
+
var activeItem = sessionListEl.querySelector(".session-item.active");
|
|
1915
|
+
if (activeItem) {
|
|
1916
|
+
var textEl = activeItem.querySelector(".session-item-text");
|
|
1917
|
+
if (textEl) sessionTitle = textEl.textContent || "Claude";
|
|
1918
|
+
else sessionTitle = activeItem.textContent || "Claude";
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
var n = new Notification(sessionTitle, {
|
|
1922
|
+
body: preview,
|
|
1923
|
+
tag: "claude-done",
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
n.onclick = function() {
|
|
1927
|
+
window.focus();
|
|
1928
|
+
n.close();
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
setTimeout(function() { n.close(); }, 5000);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1772
1934
|
// --- Init ---
|
|
1773
1935
|
lucide.createIcons();
|
|
1774
1936
|
startVerbCycle();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="128" height="128">
|
|
2
|
+
<!-- background -->
|
|
3
|
+
<rect x="0" y="0" width="12" height="12" rx="2" fill="#57AB5A"/>
|
|
4
|
+
<!-- antenna -->
|
|
5
|
+
<rect x="5" y="0" width="1" height="1" fill="#E8E5DE"/>
|
|
6
|
+
<rect x="6" y="0" width="1" height="1" fill="#E8E5DE"/>
|
|
7
|
+
<rect x="5" y="1" width="1" height="1" fill="#E8E5DE"/>
|
|
8
|
+
<rect x="6" y="1" width="1" height="1" fill="#E8E5DE"/>
|
|
9
|
+
<!-- head -->
|
|
10
|
+
<rect x="2" y="2" width="8" height="2" fill="#DA7756"/>
|
|
11
|
+
<!-- eyes row -->
|
|
12
|
+
<rect x="2" y="4" width="1" height="2" fill="#DA7756"/>
|
|
13
|
+
<rect x="3" y="4" width="1" height="2" fill="#2F2E2B"/>
|
|
14
|
+
<rect x="4" y="4" width="4" height="2" fill="#DA7756"/>
|
|
15
|
+
<rect x="8" y="4" width="1" height="2" fill="#2F2E2B"/>
|
|
16
|
+
<rect x="9" y="4" width="1" height="2" fill="#DA7756"/>
|
|
17
|
+
<!-- arms -->
|
|
18
|
+
<rect x="0" y="6" width="12" height="2" fill="#DA7756"/>
|
|
19
|
+
<!-- body -->
|
|
20
|
+
<rect x="2" y="8" width="8" height="2" fill="#DA7756"/>
|
|
21
|
+
<!-- feet -->
|
|
22
|
+
<rect x="2" y="10" width="1" height="2" fill="#DA7756"/>
|
|
23
|
+
<rect x="4" y="10" width="1" height="2" fill="#DA7756"/>
|
|
24
|
+
<rect x="7" y="10" width="1" height="2" fill="#DA7756"/>
|
|
25
|
+
<rect x="9" y="10" width="1" height="2" fill="#DA7756"/>
|
|
26
|
+
</svg>
|
package/lib/public/index.html
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
8
|
<link rel="manifest" href="/manifest.json">
|
|
9
9
|
<meta name="theme-color" content="#2F2E2B">
|
|
10
|
-
<
|
|
10
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
11
|
+
<title>Claude Relay</title>
|
|
11
12
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13
14
|
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600&family=Styrene+A+Web:wght@400;500&display=swap" rel="stylesheet">
|
package/lib/public/style.css
CHANGED
|
@@ -130,16 +130,24 @@ html, body {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
.session-item {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 4px;
|
|
133
136
|
padding: 10px 12px;
|
|
134
137
|
border-radius: 10px;
|
|
135
138
|
cursor: pointer;
|
|
136
139
|
font-size: 14px;
|
|
137
140
|
color: var(--text-secondary);
|
|
141
|
+
margin-bottom: 2px;
|
|
142
|
+
transition: background 0.15s, color 0.15s;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.session-item-text {
|
|
146
|
+
flex: 1;
|
|
138
147
|
overflow: hidden;
|
|
139
148
|
text-overflow: ellipsis;
|
|
140
149
|
white-space: nowrap;
|
|
141
|
-
|
|
142
|
-
transition: background 0.15s, color 0.15s;
|
|
150
|
+
min-width: 0;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
.session-item:hover {
|
|
@@ -163,6 +171,38 @@ html, body {
|
|
|
163
171
|
vertical-align: middle;
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
.session-delete-btn {
|
|
175
|
+
display: none;
|
|
176
|
+
width: 24px;
|
|
177
|
+
height: 24px;
|
|
178
|
+
border-radius: 6px;
|
|
179
|
+
border: none;
|
|
180
|
+
background: transparent;
|
|
181
|
+
color: var(--text-muted);
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
186
|
+
padding: 0;
|
|
187
|
+
transition: color 0.15s, background 0.15s;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.session-item:hover .session-delete-btn { display: flex; }
|
|
191
|
+
|
|
192
|
+
.session-delete-btn:hover {
|
|
193
|
+
color: var(--error);
|
|
194
|
+
background: rgba(238, 85, 85, 0.1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.session-delete-btn .lucide {
|
|
198
|
+
width: 14px;
|
|
199
|
+
height: 14px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@media (hover: none) {
|
|
203
|
+
.session-delete-btn { display: flex; }
|
|
204
|
+
}
|
|
205
|
+
|
|
166
206
|
#sidebar-footer {
|
|
167
207
|
padding: 12px 16px;
|
|
168
208
|
border-top: 1px solid var(--border-subtle);
|
|
@@ -296,7 +336,7 @@ html, body {
|
|
|
296
336
|
#pixel-canvas {
|
|
297
337
|
position: relative;
|
|
298
338
|
width: 144px;
|
|
299
|
-
height:
|
|
339
|
+
height: 144px;
|
|
300
340
|
}
|
|
301
341
|
|
|
302
342
|
.px {
|
package/lib/server.js
CHANGED
|
@@ -426,6 +426,35 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
function deleteSession(localId) {
|
|
430
|
+
var session = sessions.get(localId);
|
|
431
|
+
if (!session) return;
|
|
432
|
+
|
|
433
|
+
if (session.abortController) {
|
|
434
|
+
try { session.abortController.abort(); } catch(e) {}
|
|
435
|
+
}
|
|
436
|
+
if (session.messageQueue) {
|
|
437
|
+
try { session.messageQueue.end(); } catch(e) {}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (session.cliSessionId) {
|
|
441
|
+
try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
sessions.delete(localId);
|
|
445
|
+
|
|
446
|
+
if (activeSessionId === localId) {
|
|
447
|
+
var remaining = [...sessions.keys()];
|
|
448
|
+
if (remaining.length > 0) {
|
|
449
|
+
switchSession(remaining[remaining.length - 1]);
|
|
450
|
+
} else {
|
|
451
|
+
createSession();
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
broadcastSessionList();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
429
458
|
// --- SDK message processing ---
|
|
430
459
|
|
|
431
460
|
function processSDKMessage(session, parsed) {
|
|
@@ -475,6 +504,7 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
|
|
|
475
504
|
var idx = evt.index;
|
|
476
505
|
|
|
477
506
|
if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
|
|
507
|
+
session.streamedText = true;
|
|
478
508
|
sendAndRecord(session, { type: "delta", text: evt.delta.text });
|
|
479
509
|
} else if (evt.delta.type === "input_json_delta" && session.blocks[idx]) {
|
|
480
510
|
session.blocks[idx].inputJson += evt.delta.partial_json;
|
|
@@ -502,6 +532,17 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
|
|
|
502
532
|
} else if ((parsed.type === "assistant" || parsed.type === "user") && parsed.message && parsed.message.content) {
|
|
503
533
|
var content = parsed.message.content;
|
|
504
534
|
|
|
535
|
+
// Fallback: if assistant text wasn't streamed via deltas, send it now
|
|
536
|
+
if (parsed.type === "assistant" && !session.streamedText && Array.isArray(content)) {
|
|
537
|
+
var assistantText = content
|
|
538
|
+
.filter(function(c) { return c.type === "text"; })
|
|
539
|
+
.map(function(c) { return c.text; })
|
|
540
|
+
.join("");
|
|
541
|
+
if (assistantText) {
|
|
542
|
+
sendAndRecord(session, { type: "delta", text: assistantText });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
505
546
|
// Check for local slash command output in user messages
|
|
506
547
|
if (parsed.type === "user") {
|
|
507
548
|
var fullText = "";
|
|
@@ -648,6 +689,7 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
|
|
|
648
689
|
session.messageQueue = createMessageQueue();
|
|
649
690
|
session.blocks = {};
|
|
650
691
|
session.sentToolResults = {};
|
|
692
|
+
session.streamedText = false;
|
|
651
693
|
|
|
652
694
|
// Build initial user message
|
|
653
695
|
var content = [];
|
|
@@ -914,6 +956,13 @@ function createServer(cwd, tlsOptions, caPath, pin, mainPort) {
|
|
|
914
956
|
return;
|
|
915
957
|
}
|
|
916
958
|
|
|
959
|
+
if (msg.type === "delete_session") {
|
|
960
|
+
if (msg.id && sessions.has(msg.id)) {
|
|
961
|
+
deleteSession(msg.id);
|
|
962
|
+
}
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
917
966
|
if (msg.type === "stop") {
|
|
918
967
|
var session = getActiveSession();
|
|
919
968
|
if (session && session.abortController && session.isProcessing) {
|
package/lib/updater.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const https = require("https");
|
|
2
|
+
const { execSync, spawn } = require("child_process");
|
|
3
|
+
|
|
4
|
+
// ANSI helpers (mirrors cli.js)
|
|
5
|
+
var a = {
|
|
6
|
+
reset: "\x1b[0m",
|
|
7
|
+
bold: "\x1b[1m",
|
|
8
|
+
dim: "\x1b[2m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
yellow: "\x1b[33m",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
var sym = {
|
|
15
|
+
pointer: a.cyan + "\u25C6" + a.reset,
|
|
16
|
+
done: a.green + "\u25C7" + a.reset,
|
|
17
|
+
bar: a.dim + "\u2502" + a.reset,
|
|
18
|
+
warn: a.yellow + "\u25B2" + a.reset,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function log(s) { console.log(" " + s); }
|
|
22
|
+
|
|
23
|
+
function fetchLatestVersion() {
|
|
24
|
+
return new Promise(function (resolve) {
|
|
25
|
+
var req = https.get("https://registry.npmjs.org/claude-relay/latest", function (res) {
|
|
26
|
+
var data = "";
|
|
27
|
+
res.on("data", function (chunk) { data += chunk; });
|
|
28
|
+
res.on("end", function () {
|
|
29
|
+
try {
|
|
30
|
+
resolve(JSON.parse(data).version || null);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
resolve(null);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
req.on("error", function () { resolve(null); });
|
|
37
|
+
req.setTimeout(3000, function () {
|
|
38
|
+
req.destroy();
|
|
39
|
+
resolve(null);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isNewer(latest, current) {
|
|
45
|
+
if (!latest || !current) return false;
|
|
46
|
+
var lp = latest.split(".").map(Number);
|
|
47
|
+
var cp = current.split(".").map(Number);
|
|
48
|
+
for (var i = 0; i < 3; i++) {
|
|
49
|
+
var l = lp[i] || 0;
|
|
50
|
+
var c = cp[i] || 0;
|
|
51
|
+
if (l > c) return true;
|
|
52
|
+
if (l < c) return false;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function performUpdate() {
|
|
58
|
+
try {
|
|
59
|
+
execSync("npm install -g claude-relay@latest", { stdio: "pipe" });
|
|
60
|
+
return true;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function reExec() {
|
|
67
|
+
var args = process.argv.slice(1).concat("--no-update");
|
|
68
|
+
var child = spawn(process.execPath, args, { stdio: "inherit" });
|
|
69
|
+
child.on("exit", function (code) {
|
|
70
|
+
process.exit(code);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function checkAndUpdate(currentVersion, skipUpdate) {
|
|
75
|
+
if (skipUpdate) return false;
|
|
76
|
+
|
|
77
|
+
var latest = await fetchLatestVersion();
|
|
78
|
+
if (!latest || !isNewer(latest, currentVersion)) return false;
|
|
79
|
+
|
|
80
|
+
log(sym.pointer + " " + a.bold + "Update available" + a.reset + " " + a.dim + currentVersion + " -> " + latest + a.reset);
|
|
81
|
+
log(sym.bar + " Installing...");
|
|
82
|
+
|
|
83
|
+
if (performUpdate()) {
|
|
84
|
+
log(sym.done + " Updated to " + a.green + latest + a.reset);
|
|
85
|
+
log("");
|
|
86
|
+
reExec();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log(sym.warn + " " + a.yellow + "Update failed" + a.reset + a.dim + " (permission denied?)" + a.reset);
|
|
91
|
+
log(sym.bar + " " + a.dim + "Run manually: npm install -g claude-relay@latest" + a.reset);
|
|
92
|
+
log("");
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { checkAndUpdate };
|