claude-relay 1.0.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/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/cli.js +121 -0
- package/lib/public/app.js +1217 -0
- package/lib/public/index.html +56 -0
- package/lib/public/style.css +1211 -0
- package/lib/server.js +557 -0
- package/package.json +35 -0
package/lib/server.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawn } = require("child_process");
|
|
5
|
+
const { WebSocketServer } = require("ws");
|
|
6
|
+
|
|
7
|
+
const publicDir = path.join(__dirname, "public");
|
|
8
|
+
|
|
9
|
+
const MIME_TYPES = {
|
|
10
|
+
".html": "text/html",
|
|
11
|
+
".css": "text/css",
|
|
12
|
+
".js": "application/javascript",
|
|
13
|
+
".json": "application/json",
|
|
14
|
+
".png": "image/png",
|
|
15
|
+
".svg": "image/svg+xml",
|
|
16
|
+
".ico": "image/x-icon",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function serveStatic(req, res) {
|
|
20
|
+
var urlPath = req.url.split("?")[0];
|
|
21
|
+
if (urlPath === "/") urlPath = "/index.html";
|
|
22
|
+
|
|
23
|
+
var filePath = path.join(publicDir, urlPath);
|
|
24
|
+
|
|
25
|
+
// Prevent path traversal
|
|
26
|
+
if (!filePath.startsWith(publicDir)) {
|
|
27
|
+
res.writeHead(403);
|
|
28
|
+
res.end("Forbidden");
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
var content = fs.readFileSync(filePath);
|
|
34
|
+
var ext = path.extname(filePath);
|
|
35
|
+
var mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
36
|
+
res.writeHead(200, { "Content-Type": mime + "; charset=utf-8" });
|
|
37
|
+
res.end(content);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createServer(cwd) {
|
|
45
|
+
const project = path.basename(cwd);
|
|
46
|
+
|
|
47
|
+
// --- Multi-session state ---
|
|
48
|
+
let nextLocalId = 1;
|
|
49
|
+
let sessions = new Map(); // localId -> session object
|
|
50
|
+
let activeSessionId = null; // currently active local ID
|
|
51
|
+
let slashCommands = null; // shared across sessions
|
|
52
|
+
let activeWs = null;
|
|
53
|
+
|
|
54
|
+
// --- Session persistence ---
|
|
55
|
+
var sessionsDir = path.join(cwd, ".claude-relay", "sessions");
|
|
56
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
function sessionFilePath(cliSessionId) {
|
|
59
|
+
return path.join(sessionsDir, cliSessionId + ".jsonl");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function saveSessionFile(session) {
|
|
63
|
+
if (!session.cliSessionId) return;
|
|
64
|
+
var meta = JSON.stringify({
|
|
65
|
+
type: "meta",
|
|
66
|
+
localId: session.localId,
|
|
67
|
+
cliSessionId: session.cliSessionId,
|
|
68
|
+
title: session.title,
|
|
69
|
+
createdAt: session.createdAt,
|
|
70
|
+
});
|
|
71
|
+
var lines = [meta];
|
|
72
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
73
|
+
lines.push(JSON.stringify(session.history[i]));
|
|
74
|
+
}
|
|
75
|
+
fs.writeFileSync(sessionFilePath(session.cliSessionId), lines.join("\n") + "\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function appendToSessionFile(session, obj) {
|
|
79
|
+
if (!session.cliSessionId) return;
|
|
80
|
+
fs.appendFileSync(sessionFilePath(session.cliSessionId), JSON.stringify(obj) + "\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadSessions() {
|
|
84
|
+
var files;
|
|
85
|
+
try { files = fs.readdirSync(sessionsDir); } catch { return; }
|
|
86
|
+
|
|
87
|
+
var loaded = [];
|
|
88
|
+
for (var i = 0; i < files.length; i++) {
|
|
89
|
+
if (!files[i].endsWith(".jsonl")) continue;
|
|
90
|
+
var content;
|
|
91
|
+
try { content = fs.readFileSync(path.join(sessionsDir, files[i]), "utf8"); } catch { continue; }
|
|
92
|
+
var lines = content.trim().split("\n");
|
|
93
|
+
if (lines.length === 0) continue;
|
|
94
|
+
|
|
95
|
+
var meta;
|
|
96
|
+
try { meta = JSON.parse(lines[0]); } catch { continue; }
|
|
97
|
+
if (meta.type !== "meta" || !meta.cliSessionId) continue;
|
|
98
|
+
|
|
99
|
+
var history = [];
|
|
100
|
+
for (var j = 1; j < lines.length; j++) {
|
|
101
|
+
try { history.push(JSON.parse(lines[j])); } catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
loaded.push({ meta: meta, history: history });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
loaded.sort(function(a, b) { return a.meta.createdAt - b.meta.createdAt; });
|
|
108
|
+
|
|
109
|
+
for (var i = 0; i < loaded.length; i++) {
|
|
110
|
+
var m = loaded[i].meta;
|
|
111
|
+
var localId = nextLocalId++;
|
|
112
|
+
var session = {
|
|
113
|
+
localId: localId,
|
|
114
|
+
proc: null,
|
|
115
|
+
cliSessionId: m.cliSessionId,
|
|
116
|
+
buffer: "",
|
|
117
|
+
blocks: {},
|
|
118
|
+
sentToolResults: {},
|
|
119
|
+
isProcessing: false,
|
|
120
|
+
title: m.title || "",
|
|
121
|
+
createdAt: m.createdAt || Date.now(),
|
|
122
|
+
history: loaded[i].history,
|
|
123
|
+
};
|
|
124
|
+
sessions.set(localId, session);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load persisted sessions from disk
|
|
129
|
+
loadSessions();
|
|
130
|
+
|
|
131
|
+
function send(obj) {
|
|
132
|
+
if (activeWs && activeWs.readyState === 1) {
|
|
133
|
+
activeWs.send(JSON.stringify(obj));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Send a message and record it in session history for replay on reconnect
|
|
138
|
+
function sendAndRecord(session, obj) {
|
|
139
|
+
session.history.push(obj);
|
|
140
|
+
appendToSessionFile(session, obj);
|
|
141
|
+
if (session.localId === activeSessionId) {
|
|
142
|
+
send(obj);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getActiveSession() {
|
|
147
|
+
return sessions.get(activeSessionId) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function broadcastSessionList() {
|
|
151
|
+
send({
|
|
152
|
+
type: "session_list",
|
|
153
|
+
sessions: [...sessions.values()].map(function(s) {
|
|
154
|
+
return {
|
|
155
|
+
id: s.localId,
|
|
156
|
+
title: s.title || "New Session",
|
|
157
|
+
active: s.localId === activeSessionId,
|
|
158
|
+
isProcessing: s.isProcessing,
|
|
159
|
+
};
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createSession() {
|
|
165
|
+
var localId = nextLocalId++;
|
|
166
|
+
var session = {
|
|
167
|
+
localId: localId,
|
|
168
|
+
proc: null,
|
|
169
|
+
cliSessionId: null,
|
|
170
|
+
buffer: "",
|
|
171
|
+
blocks: {},
|
|
172
|
+
sentToolResults: {},
|
|
173
|
+
isProcessing: false,
|
|
174
|
+
title: "",
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
history: [],
|
|
177
|
+
};
|
|
178
|
+
sessions.set(localId, session);
|
|
179
|
+
spawnProcess(session);
|
|
180
|
+
switchSession(localId);
|
|
181
|
+
return session;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function replayHistory(session) {
|
|
185
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
186
|
+
send(session.history[i]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function switchSession(localId) {
|
|
191
|
+
var session = sessions.get(localId);
|
|
192
|
+
if (!session) return;
|
|
193
|
+
|
|
194
|
+
activeSessionId = localId;
|
|
195
|
+
send({ type: "session_switched", id: localId });
|
|
196
|
+
broadcastSessionList();
|
|
197
|
+
replayHistory(session);
|
|
198
|
+
|
|
199
|
+
if (session.isProcessing) {
|
|
200
|
+
send({ type: "status", status: "processing" });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function processLine(session, line) {
|
|
205
|
+
if (!line.trim()) return;
|
|
206
|
+
|
|
207
|
+
var parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(line);
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (parsed.session_id && !session.cliSessionId) {
|
|
215
|
+
session.cliSessionId = parsed.session_id;
|
|
216
|
+
saveSessionFile(session);
|
|
217
|
+
} else if (parsed.session_id) {
|
|
218
|
+
session.cliSessionId = parsed.session_id;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Cache slash_commands from CLI init message
|
|
222
|
+
if (parsed.type === "system" && parsed.subtype === "init" && parsed.slash_commands) {
|
|
223
|
+
slashCommands = parsed.slash_commands;
|
|
224
|
+
send({ type: "slash_commands", commands: slashCommands });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (parsed.type === "stream_event" && parsed.event) {
|
|
228
|
+
var evt = parsed.event;
|
|
229
|
+
|
|
230
|
+
if (evt.type === "content_block_start") {
|
|
231
|
+
var block = evt.content_block;
|
|
232
|
+
var idx = evt.index;
|
|
233
|
+
|
|
234
|
+
if (block.type === "tool_use") {
|
|
235
|
+
session.blocks[idx] = { type: "tool_use", id: block.id, name: block.name, inputJson: "" };
|
|
236
|
+
sendAndRecord(session, { type: "tool_start", id: block.id, name: block.name });
|
|
237
|
+
} else if (block.type === "thinking") {
|
|
238
|
+
session.blocks[idx] = { type: "thinking", thinkingText: "" };
|
|
239
|
+
sendAndRecord(session, { type: "thinking_start" });
|
|
240
|
+
} else if (block.type === "text") {
|
|
241
|
+
session.blocks[idx] = { type: "text" };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (evt.type === "content_block_delta" && evt.delta) {
|
|
246
|
+
var idx = evt.index;
|
|
247
|
+
|
|
248
|
+
if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
|
|
249
|
+
sendAndRecord(session, { type: "delta", text: evt.delta.text });
|
|
250
|
+
} else if (evt.delta.type === "input_json_delta" && session.blocks[idx]) {
|
|
251
|
+
session.blocks[idx].inputJson += evt.delta.partial_json;
|
|
252
|
+
} else if (evt.delta.type === "thinking_delta" && session.blocks[idx]) {
|
|
253
|
+
session.blocks[idx].thinkingText += evt.delta.thinking;
|
|
254
|
+
sendAndRecord(session, { type: "thinking_delta", text: evt.delta.thinking });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (evt.type === "content_block_stop") {
|
|
259
|
+
var idx = evt.index;
|
|
260
|
+
var block = session.blocks[idx];
|
|
261
|
+
|
|
262
|
+
if (block && block.type === "tool_use") {
|
|
263
|
+
var input = {};
|
|
264
|
+
try { input = JSON.parse(block.inputJson); } catch {}
|
|
265
|
+
sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
|
|
266
|
+
} else if (block && block.type === "thinking") {
|
|
267
|
+
sendAndRecord(session, { type: "thinking_stop" });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
delete session.blocks[idx];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
} else if ((parsed.type === "assistant" || parsed.type === "user") && parsed.message && parsed.message.content) {
|
|
274
|
+
var content = parsed.message.content;
|
|
275
|
+
for (var i = 0; i < content.length; i++) {
|
|
276
|
+
var block = content[i];
|
|
277
|
+
if (block.type === "tool_result" && !session.sentToolResults[block.tool_use_id]) {
|
|
278
|
+
var resultText = "";
|
|
279
|
+
if (typeof block.content === "string") {
|
|
280
|
+
resultText = block.content;
|
|
281
|
+
} else if (Array.isArray(block.content)) {
|
|
282
|
+
resultText = block.content
|
|
283
|
+
.filter(function(c) { return c.type === "text"; })
|
|
284
|
+
.map(function(c) { return c.text; })
|
|
285
|
+
.join("\n");
|
|
286
|
+
}
|
|
287
|
+
session.sentToolResults[block.tool_use_id] = true;
|
|
288
|
+
sendAndRecord(session, {
|
|
289
|
+
type: "tool_result",
|
|
290
|
+
id: block.tool_use_id,
|
|
291
|
+
content: resultText,
|
|
292
|
+
is_error: block.is_error || false,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
} else if (parsed.type === "result") {
|
|
298
|
+
session.blocks = {};
|
|
299
|
+
session.sentToolResults = {};
|
|
300
|
+
session.isProcessing = false;
|
|
301
|
+
sendAndRecord(session, {
|
|
302
|
+
type: "result",
|
|
303
|
+
cost: parsed.total_cost_usd,
|
|
304
|
+
duration: parsed.duration_ms,
|
|
305
|
+
sessionId: parsed.session_id,
|
|
306
|
+
});
|
|
307
|
+
sendAndRecord(session, { type: "done", code: 0 });
|
|
308
|
+
broadcastSessionList();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function spawnProcess(session) {
|
|
313
|
+
var args = [
|
|
314
|
+
"-p",
|
|
315
|
+
"--verbose",
|
|
316
|
+
"--output-format", "stream-json",
|
|
317
|
+
"--input-format", "stream-json",
|
|
318
|
+
"--include-partial-messages",
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
if (session.cliSessionId) {
|
|
322
|
+
args.push("--resume", session.cliSessionId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
session.buffer = "";
|
|
326
|
+
session.blocks = {};
|
|
327
|
+
session.sentToolResults = {};
|
|
328
|
+
|
|
329
|
+
session.proc = spawn("claude", args, {
|
|
330
|
+
cwd: cwd,
|
|
331
|
+
env: Object.assign({}, process.env),
|
|
332
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
session.proc.stdout.on("data", function(chunk) {
|
|
336
|
+
session.buffer += chunk.toString();
|
|
337
|
+
var lines = session.buffer.split("\n");
|
|
338
|
+
session.buffer = lines.pop();
|
|
339
|
+
|
|
340
|
+
for (var i = 0; i < lines.length; i++) {
|
|
341
|
+
processLine(session, lines[i]);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
session.proc.stderr.on("data", function(chunk) {
|
|
346
|
+
var errText = chunk.toString();
|
|
347
|
+
if (errText.includes("Error") || errText.includes("error")) {
|
|
348
|
+
if (session.localId === activeSessionId) {
|
|
349
|
+
send({ type: "stderr", text: errText });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
session.proc.on("close", function(code) {
|
|
355
|
+
if (session.buffer.trim()) {
|
|
356
|
+
processLine(session, session.buffer);
|
|
357
|
+
session.buffer = "";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
session.proc = null;
|
|
361
|
+
session.blocks = {};
|
|
362
|
+
|
|
363
|
+
if (session.isProcessing) {
|
|
364
|
+
session.isProcessing = false;
|
|
365
|
+
sendAndRecord(session, { type: "error", text: "Claude process exited unexpectedly (code " + code + ")" });
|
|
366
|
+
sendAndRecord(session, { type: "done", code: code || 1 });
|
|
367
|
+
broadcastSessionList();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
session.proc.on("error", function(err) {
|
|
372
|
+
session.proc = null;
|
|
373
|
+
session.isProcessing = false;
|
|
374
|
+
sendAndRecord(session, { type: "error", text: "Failed to spawn claude: " + err.message });
|
|
375
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
376
|
+
broadcastSessionList();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function writeMessage(session, text, images) {
|
|
381
|
+
var content = [];
|
|
382
|
+
|
|
383
|
+
if (images && images.length > 0) {
|
|
384
|
+
for (var i = 0; i < images.length; i++) {
|
|
385
|
+
content.push({
|
|
386
|
+
type: "image",
|
|
387
|
+
source: {
|
|
388
|
+
type: "base64",
|
|
389
|
+
media_type: images[i].mediaType,
|
|
390
|
+
data: images[i].data,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (text) {
|
|
397
|
+
content.push({ type: "text", text: text });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
var msg = {
|
|
401
|
+
type: "user",
|
|
402
|
+
session_id: "",
|
|
403
|
+
parent_tool_use_id: null,
|
|
404
|
+
message: {
|
|
405
|
+
role: "user",
|
|
406
|
+
content: content,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
session.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function writeToolResult(session, toolUseId, resultText) {
|
|
413
|
+
var msg = {
|
|
414
|
+
type: "user",
|
|
415
|
+
session_id: "",
|
|
416
|
+
parent_tool_use_id: null,
|
|
417
|
+
message: {
|
|
418
|
+
role: "user",
|
|
419
|
+
content: [{
|
|
420
|
+
type: "tool_result",
|
|
421
|
+
tool_use_id: toolUseId,
|
|
422
|
+
content: resultText,
|
|
423
|
+
}],
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
session.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --- Spawn initial session only if no persisted sessions ---
|
|
430
|
+
if (sessions.size === 0) {
|
|
431
|
+
createSession();
|
|
432
|
+
} else {
|
|
433
|
+
// Activate the most recent session
|
|
434
|
+
var lastSession = [...sessions.values()].pop();
|
|
435
|
+
activeSessionId = lastSession.localId;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// --- HTTP server ---
|
|
439
|
+
var server = http.createServer(function(req, res) {
|
|
440
|
+
if (req.method === "GET" && req.url === "/info") {
|
|
441
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
442
|
+
res.end(JSON.stringify({ cwd: cwd, project: project }));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (req.method === "GET") {
|
|
447
|
+
if (serveStatic(req, res)) return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
res.writeHead(404);
|
|
451
|
+
res.end("Not found");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// --- WebSocket ---
|
|
455
|
+
var wss = new WebSocketServer({ server: server });
|
|
456
|
+
|
|
457
|
+
wss.on("connection", function(ws) {
|
|
458
|
+
activeWs = ws;
|
|
459
|
+
|
|
460
|
+
// Send cached state
|
|
461
|
+
send({ type: "info", cwd: cwd, project: project });
|
|
462
|
+
if (slashCommands) {
|
|
463
|
+
send({ type: "slash_commands", commands: slashCommands });
|
|
464
|
+
}
|
|
465
|
+
broadcastSessionList();
|
|
466
|
+
|
|
467
|
+
// Restore active session
|
|
468
|
+
var active = getActiveSession();
|
|
469
|
+
if (active) {
|
|
470
|
+
send({ type: "session_switched", id: active.localId });
|
|
471
|
+
replayHistory(active);
|
|
472
|
+
if (active.isProcessing) {
|
|
473
|
+
send({ type: "status", status: "processing" });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
ws.on("message", function(raw) {
|
|
478
|
+
var msg;
|
|
479
|
+
try {
|
|
480
|
+
msg = JSON.parse(raw.toString());
|
|
481
|
+
} catch {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (msg.type === "new_session") {
|
|
486
|
+
createSession();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (msg.type === "switch_session") {
|
|
491
|
+
if (msg.id && sessions.has(msg.id)) {
|
|
492
|
+
switchSession(msg.id);
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (msg.type === "ask_user_response") {
|
|
498
|
+
var session = getActiveSession();
|
|
499
|
+
if (!session || !session.proc) return;
|
|
500
|
+
|
|
501
|
+
var toolId = msg.toolId;
|
|
502
|
+
var answers = msg.answers || {};
|
|
503
|
+
var resultText = JSON.stringify({ answers: answers });
|
|
504
|
+
|
|
505
|
+
writeToolResult(session, toolId, resultText);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (msg.type !== "message") return;
|
|
510
|
+
if (!msg.text && (!msg.images || msg.images.length === 0)) return;
|
|
511
|
+
|
|
512
|
+
var session = getActiveSession();
|
|
513
|
+
if (!session) return;
|
|
514
|
+
|
|
515
|
+
if (session.isProcessing) {
|
|
516
|
+
send({ type: "error", text: "Still processing previous message. Please wait." });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
session.isProcessing = true;
|
|
521
|
+
session.sentToolResults = {};
|
|
522
|
+
|
|
523
|
+
// Record user message in history for replay (without base64 data to save space)
|
|
524
|
+
var userMsg = { type: "user_message", text: msg.text || "" };
|
|
525
|
+
if (msg.images && msg.images.length > 0) {
|
|
526
|
+
userMsg.imageCount = msg.images.length;
|
|
527
|
+
}
|
|
528
|
+
session.history.push(userMsg);
|
|
529
|
+
appendToSessionFile(session, userMsg);
|
|
530
|
+
send({ type: "status", status: "processing" });
|
|
531
|
+
|
|
532
|
+
// Set title from first user message
|
|
533
|
+
if (!session.title) {
|
|
534
|
+
session.title = (msg.text || "Image").substring(0, 50);
|
|
535
|
+
saveSessionFile(session);
|
|
536
|
+
broadcastSessionList();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Respawn if process died
|
|
540
|
+
if (!session.proc) {
|
|
541
|
+
spawnProcess(session);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
writeMessage(session, msg.text || "", msg.images);
|
|
545
|
+
broadcastSessionList();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
ws.on("close", function() {
|
|
549
|
+
if (activeWs === ws) activeWs = null;
|
|
550
|
+
// Don't kill procs — they persist across reconnects
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
return server;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
module.exports = { createServer };
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-relay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Access Claude Code on your machine, from anywhere. One command, no config.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-relay": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"lib/"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"cli",
|
|
16
|
+
"mobile",
|
|
17
|
+
"remote",
|
|
18
|
+
"relay",
|
|
19
|
+
"web-ui",
|
|
20
|
+
"tailscale"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/chadbyte/claude-relay.git"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/chadbyte/claude-relay#readme",
|
|
27
|
+
"author": "Chad",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"ws": "^8.18.0"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|