claude-relay 2.4.2 → 2.5.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/bin/cli.js +1 -2350
- package/package.json +7 -42
- package/LICENSE +0 -21
- package/README.md +0 -281
- package/lib/cli-sessions.js +0 -270
- package/lib/config.js +0 -222
- package/lib/daemon.js +0 -423
- package/lib/ipc.js +0 -112
- package/lib/pages.js +0 -714
- package/lib/project.js +0 -1224
- package/lib/public/app.js +0 -2157
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +0 -145
- package/lib/public/css/diff.css +0 -128
- package/lib/public/css/filebrowser.css +0 -1076
- package/lib/public/css/highlight.css +0 -144
- package/lib/public/css/input.css +0 -512
- package/lib/public/css/menus.css +0 -683
- package/lib/public/css/messages.css +0 -1159
- package/lib/public/css/overlays.css +0 -731
- package/lib/public/css/rewind.css +0 -529
- package/lib/public/css/sidebar.css +0 -794
- package/lib/public/favicon.svg +0 -26
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +0 -19
- package/lib/public/index.html +0 -460
- package/lib/public/manifest.json +0 -27
- package/lib/public/modules/diff.js +0 -398
- package/lib/public/modules/events.js +0 -21
- package/lib/public/modules/filebrowser.js +0 -1375
- package/lib/public/modules/fileicons.js +0 -172
- package/lib/public/modules/icons.js +0 -54
- package/lib/public/modules/input.js +0 -578
- package/lib/public/modules/markdown.js +0 -149
- package/lib/public/modules/notifications.js +0 -643
- package/lib/public/modules/qrcode.js +0 -70
- package/lib/public/modules/rewind.js +0 -334
- package/lib/public/modules/sidebar.js +0 -628
- package/lib/public/modules/state.js +0 -3
- package/lib/public/modules/terminal.js +0 -658
- package/lib/public/modules/theme.js +0 -622
- package/lib/public/modules/tools.js +0 -1410
- package/lib/public/modules/utils.js +0 -56
- package/lib/public/style.css +0 -10
- package/lib/public/sw.js +0 -75
- package/lib/push.js +0 -125
- package/lib/sdk-bridge.js +0 -771
- package/lib/server.js +0 -577
- package/lib/sessions.js +0 -402
- package/lib/terminal-manager.js +0 -187
- package/lib/terminal.js +0 -24
- package/lib/themes/ayu-light.json +0 -9
- package/lib/themes/catppuccin-latte.json +0 -9
- package/lib/themes/catppuccin-mocha.json +0 -9
- package/lib/themes/claude-light.json +0 -9
- package/lib/themes/claude.json +0 -9
- package/lib/themes/dracula.json +0 -9
- package/lib/themes/everforest-light.json +0 -9
- package/lib/themes/everforest.json +0 -9
- package/lib/themes/github-light.json +0 -9
- package/lib/themes/gruvbox-dark.json +0 -9
- package/lib/themes/gruvbox-light.json +0 -9
- package/lib/themes/monokai.json +0 -9
- package/lib/themes/nord-light.json +0 -9
- package/lib/themes/nord.json +0 -9
- package/lib/themes/one-dark.json +0 -9
- package/lib/themes/one-light.json +0 -9
- package/lib/themes/rose-pine-dawn.json +0 -9
- package/lib/themes/rose-pine.json +0 -9
- package/lib/themes/solarized-dark.json +0 -9
- package/lib/themes/solarized-light.json +0 -9
- package/lib/themes/tokyo-night-light.json +0 -9
- package/lib/themes/tokyo-night.json +0 -9
- package/lib/updater.js +0 -96
package/lib/config.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
var fs = require("fs");
|
|
2
|
-
var path = require("path");
|
|
3
|
-
var os = require("os");
|
|
4
|
-
var net = require("net");
|
|
5
|
-
|
|
6
|
-
var CONFIG_DIR = process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay");
|
|
7
|
-
var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
|
|
8
|
-
var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
|
|
9
|
-
|
|
10
|
-
function configPath() {
|
|
11
|
-
return path.join(CONFIG_DIR, "daemon.json");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function socketPath() {
|
|
15
|
-
if (process.platform === "win32") {
|
|
16
|
-
var pipeName = process.env.CLAUDE_RELAY_HOME ? "claude-relay-dev-daemon" : "claude-relay-daemon";
|
|
17
|
-
return "\\\\.\\pipe\\" + pipeName;
|
|
18
|
-
}
|
|
19
|
-
return path.join(CONFIG_DIR, "daemon.sock");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function logPath() {
|
|
23
|
-
return path.join(CONFIG_DIR, "daemon.log");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function ensureConfigDir() {
|
|
27
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function loadConfig() {
|
|
31
|
-
try {
|
|
32
|
-
var data = fs.readFileSync(configPath(), "utf8");
|
|
33
|
-
return JSON.parse(data);
|
|
34
|
-
} catch (e) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function saveConfig(config) {
|
|
40
|
-
ensureConfigDir();
|
|
41
|
-
var tmpPath = configPath() + ".tmp";
|
|
42
|
-
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
|
43
|
-
fs.renameSync(tmpPath, configPath());
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isPidAlive(pid) {
|
|
47
|
-
try {
|
|
48
|
-
process.kill(pid, 0);
|
|
49
|
-
return true;
|
|
50
|
-
} catch (e) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isDaemonAlive(config) {
|
|
56
|
-
if (!config || !config.pid) return false;
|
|
57
|
-
if (!isPidAlive(config.pid)) return false;
|
|
58
|
-
// Named pipes on Windows can't be stat'd, just check PID
|
|
59
|
-
if (process.platform === "win32") return true;
|
|
60
|
-
try {
|
|
61
|
-
fs.statSync(socketPath());
|
|
62
|
-
return true;
|
|
63
|
-
} catch (e) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isDaemonAliveAsync(config) {
|
|
69
|
-
return new Promise(function (resolve) {
|
|
70
|
-
if (!config || !config.pid) return resolve(false);
|
|
71
|
-
if (!isPidAlive(config.pid)) return resolve(false);
|
|
72
|
-
|
|
73
|
-
var sock = socketPath();
|
|
74
|
-
var client = net.connect(sock);
|
|
75
|
-
var timer = setTimeout(function () {
|
|
76
|
-
client.destroy();
|
|
77
|
-
resolve(false);
|
|
78
|
-
}, 1000);
|
|
79
|
-
|
|
80
|
-
client.on("connect", function () {
|
|
81
|
-
clearTimeout(timer);
|
|
82
|
-
client.destroy();
|
|
83
|
-
resolve(true);
|
|
84
|
-
});
|
|
85
|
-
client.on("error", function () {
|
|
86
|
-
clearTimeout(timer);
|
|
87
|
-
resolve(false);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function generateSlug(projectPath, existingSlugs) {
|
|
93
|
-
var base = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
94
|
-
if (!base) base = "project";
|
|
95
|
-
if (!existingSlugs || existingSlugs.indexOf(base) === -1) return base;
|
|
96
|
-
for (var i = 2; i < 100; i++) {
|
|
97
|
-
var candidate = base + "-" + i;
|
|
98
|
-
if (existingSlugs.indexOf(candidate) === -1) return candidate;
|
|
99
|
-
}
|
|
100
|
-
return base + "-" + Date.now();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function clearStaleConfig() {
|
|
104
|
-
try { fs.unlinkSync(configPath()); } catch (e) {}
|
|
105
|
-
if (process.platform !== "win32") {
|
|
106
|
-
try { fs.unlinkSync(socketPath()); } catch (e) {}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// --- Crash info ---
|
|
111
|
-
|
|
112
|
-
function crashInfoPath() {
|
|
113
|
-
return CRASH_INFO_PATH;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function writeCrashInfo(info) {
|
|
117
|
-
try {
|
|
118
|
-
ensureConfigDir();
|
|
119
|
-
fs.writeFileSync(CRASH_INFO_PATH, JSON.stringify(info));
|
|
120
|
-
} catch (e) {}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function readCrashInfo() {
|
|
124
|
-
try {
|
|
125
|
-
var data = fs.readFileSync(CRASH_INFO_PATH, "utf8");
|
|
126
|
-
return JSON.parse(data);
|
|
127
|
-
} catch (e) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function clearCrashInfo() {
|
|
133
|
-
try { fs.unlinkSync(CRASH_INFO_PATH); } catch (e) {}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// --- ~/.clayrc (recent projects persistence) ---
|
|
137
|
-
|
|
138
|
-
function clayrcPath() {
|
|
139
|
-
return CLAYRC_PATH;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function loadClayrc() {
|
|
143
|
-
try {
|
|
144
|
-
var data = fs.readFileSync(CLAYRC_PATH, "utf8");
|
|
145
|
-
return JSON.parse(data);
|
|
146
|
-
} catch (e) {
|
|
147
|
-
return { recentProjects: [] };
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function saveClayrc(rc) {
|
|
152
|
-
var tmpPath = CLAYRC_PATH + ".tmp";
|
|
153
|
-
fs.writeFileSync(tmpPath, JSON.stringify(rc, null, 2) + "\n");
|
|
154
|
-
fs.renameSync(tmpPath, CLAYRC_PATH);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Update ~/.clayrc with the current project list from daemon config.
|
|
159
|
-
* Merges with existing entries (preserves addedAt, updates lastUsed).
|
|
160
|
-
*/
|
|
161
|
-
function syncClayrc(projects) {
|
|
162
|
-
var rc = loadClayrc();
|
|
163
|
-
var existing = rc.recentProjects || [];
|
|
164
|
-
|
|
165
|
-
// Build a map by path for quick lookup
|
|
166
|
-
var byPath = {};
|
|
167
|
-
for (var i = 0; i < existing.length; i++) {
|
|
168
|
-
byPath[existing[i].path] = existing[i];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Update/add current projects
|
|
172
|
-
for (var j = 0; j < projects.length; j++) {
|
|
173
|
-
var p = projects[j];
|
|
174
|
-
if (byPath[p.path]) {
|
|
175
|
-
// Update existing entry
|
|
176
|
-
byPath[p.path].slug = p.slug;
|
|
177
|
-
byPath[p.path].lastUsed = Date.now();
|
|
178
|
-
if (p.title) byPath[p.path].title = p.title;
|
|
179
|
-
else delete byPath[p.path].title;
|
|
180
|
-
} else {
|
|
181
|
-
// New entry
|
|
182
|
-
byPath[p.path] = {
|
|
183
|
-
path: p.path,
|
|
184
|
-
slug: p.slug,
|
|
185
|
-
title: p.title || undefined,
|
|
186
|
-
addedAt: p.addedAt || Date.now(),
|
|
187
|
-
lastUsed: Date.now(),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Rebuild array, sorted by lastUsed descending
|
|
193
|
-
var all = Object.keys(byPath).map(function (k) { return byPath[k]; });
|
|
194
|
-
all.sort(function (a, b) { return (b.lastUsed || 0) - (a.lastUsed || 0); });
|
|
195
|
-
|
|
196
|
-
// Keep at most 20 recent projects
|
|
197
|
-
rc.recentProjects = all.slice(0, 20);
|
|
198
|
-
saveClayrc(rc);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
module.exports = {
|
|
202
|
-
CONFIG_DIR: CONFIG_DIR,
|
|
203
|
-
configPath: configPath,
|
|
204
|
-
socketPath: socketPath,
|
|
205
|
-
logPath: logPath,
|
|
206
|
-
ensureConfigDir: ensureConfigDir,
|
|
207
|
-
loadConfig: loadConfig,
|
|
208
|
-
saveConfig: saveConfig,
|
|
209
|
-
isPidAlive: isPidAlive,
|
|
210
|
-
isDaemonAlive: isDaemonAlive,
|
|
211
|
-
isDaemonAliveAsync: isDaemonAliveAsync,
|
|
212
|
-
generateSlug: generateSlug,
|
|
213
|
-
clearStaleConfig: clearStaleConfig,
|
|
214
|
-
crashInfoPath: crashInfoPath,
|
|
215
|
-
writeCrashInfo: writeCrashInfo,
|
|
216
|
-
readCrashInfo: readCrashInfo,
|
|
217
|
-
clearCrashInfo: clearCrashInfo,
|
|
218
|
-
clayrcPath: clayrcPath,
|
|
219
|
-
loadClayrc: loadClayrc,
|
|
220
|
-
saveClayrc: saveClayrc,
|
|
221
|
-
syncClayrc: syncClayrc,
|
|
222
|
-
};
|
package/lib/daemon.js
DELETED
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
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
|
-
|
|
7
|
-
var fs = require("fs");
|
|
8
|
-
var path = require("path");
|
|
9
|
-
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
|
|
10
|
-
var { createIPCServer } = require("./ipc");
|
|
11
|
-
var { createServer } = require("./server");
|
|
12
|
-
|
|
13
|
-
var configFile = process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
|
|
14
|
-
var config;
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
config = JSON.parse(fs.readFileSync(configFile, "utf8"));
|
|
18
|
-
} catch (e) {
|
|
19
|
-
console.error("[daemon] Failed to read config:", e.message);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// --- TLS ---
|
|
24
|
-
var tlsOptions = null;
|
|
25
|
-
if (config.tls) {
|
|
26
|
-
var os = require("os");
|
|
27
|
-
var certDir = path.join(process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay"), "certs");
|
|
28
|
-
var keyPath = path.join(certDir, "key.pem");
|
|
29
|
-
var certPath = path.join(certDir, "cert.pem");
|
|
30
|
-
try {
|
|
31
|
-
tlsOptions = {
|
|
32
|
-
key: fs.readFileSync(keyPath),
|
|
33
|
-
cert: fs.readFileSync(certPath),
|
|
34
|
-
};
|
|
35
|
-
} catch (e) {
|
|
36
|
-
console.error("[daemon] TLS cert not found, falling back to HTTP");
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
var caRoot = null;
|
|
41
|
-
try {
|
|
42
|
-
var { execSync } = require("child_process");
|
|
43
|
-
caRoot = path.join(
|
|
44
|
-
execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
|
|
45
|
-
"rootCA.pem"
|
|
46
|
-
);
|
|
47
|
-
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
48
|
-
} catch (e) {}
|
|
49
|
-
|
|
50
|
-
// --- Resolve LAN IP for share URL ---
|
|
51
|
-
var os2 = require("os");
|
|
52
|
-
var lanIp = (function () {
|
|
53
|
-
var ifaces = os2.networkInterfaces();
|
|
54
|
-
for (var addrs of Object.values(ifaces)) {
|
|
55
|
-
for (var i = 0; i < addrs.length; i++) {
|
|
56
|
-
if (addrs[i].family === "IPv4" && !addrs[i].internal && addrs[i].address.startsWith("100.")) return addrs[i].address;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
for (var addrs of Object.values(ifaces)) {
|
|
60
|
-
for (var i = 0; i < addrs.length; i++) {
|
|
61
|
-
if (addrs[i].family === "IPv4" && !addrs[i].internal) return addrs[i].address;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
})();
|
|
66
|
-
|
|
67
|
-
// --- Create multi-project server ---
|
|
68
|
-
var relay = createServer({
|
|
69
|
-
tlsOptions: tlsOptions,
|
|
70
|
-
caPath: caRoot,
|
|
71
|
-
pinHash: config.pinHash || null,
|
|
72
|
-
port: config.port,
|
|
73
|
-
debug: config.debug || false,
|
|
74
|
-
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
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
|
-
},
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// --- Register projects ---
|
|
119
|
-
var projects = config.projects || [];
|
|
120
|
-
for (var i = 0; i < projects.length; i++) {
|
|
121
|
-
var p = projects[i];
|
|
122
|
-
if (fs.existsSync(p.path)) {
|
|
123
|
-
console.log("[daemon] Adding project:", p.slug, "→", p.path);
|
|
124
|
-
relay.addProject(p.path, p.slug, p.title);
|
|
125
|
-
} else {
|
|
126
|
-
console.log("[daemon] Skipping missing project:", p.path);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Sync ~/.clayrc on startup
|
|
131
|
-
try { syncClayrc(config.projects); } catch (e) {}
|
|
132
|
-
|
|
133
|
-
// --- IPC server ---
|
|
134
|
-
var ipc = createIPCServer(socketPath(), function (msg) {
|
|
135
|
-
switch (msg.cmd) {
|
|
136
|
-
case "add_project": {
|
|
137
|
-
if (!msg.path) return { ok: false, error: "missing path" };
|
|
138
|
-
var absPath = path.resolve(msg.path);
|
|
139
|
-
// Check if already registered
|
|
140
|
-
for (var j = 0; j < config.projects.length; j++) {
|
|
141
|
-
if (config.projects[j].path === absPath) {
|
|
142
|
-
return { ok: true, slug: config.projects[j].slug, existing: true };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
146
|
-
var slug = generateSlug(absPath, slugs);
|
|
147
|
-
relay.addProject(absPath, slug);
|
|
148
|
-
config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
|
|
149
|
-
saveConfig(config);
|
|
150
|
-
try { syncClayrc(config.projects); } catch (e) {}
|
|
151
|
-
console.log("[daemon] Added project:", slug, "→", absPath);
|
|
152
|
-
return { ok: true, slug: slug };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
case "remove_project": {
|
|
156
|
-
if (!msg.path && !msg.slug) return { ok: false, error: "missing path or slug" };
|
|
157
|
-
var target = msg.slug;
|
|
158
|
-
if (!target) {
|
|
159
|
-
var abs = path.resolve(msg.path);
|
|
160
|
-
for (var k = 0; k < config.projects.length; k++) {
|
|
161
|
-
if (config.projects[k].path === abs) {
|
|
162
|
-
target = config.projects[k].slug;
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (!target) return { ok: false, error: "project not found" };
|
|
168
|
-
relay.removeProject(target);
|
|
169
|
-
config.projects = config.projects.filter(function (p) { return p.slug !== target; });
|
|
170
|
-
saveConfig(config);
|
|
171
|
-
try { syncClayrc(config.projects); } catch (e) {}
|
|
172
|
-
console.log("[daemon] Removed project:", target);
|
|
173
|
-
return { ok: true };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
case "get_status":
|
|
177
|
-
return {
|
|
178
|
-
ok: true,
|
|
179
|
-
pid: process.pid,
|
|
180
|
-
port: config.port,
|
|
181
|
-
tls: !!tlsOptions,
|
|
182
|
-
keepAwake: !!config.keepAwake,
|
|
183
|
-
projects: relay.getProjects(),
|
|
184
|
-
uptime: process.uptime(),
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
case "set_pin": {
|
|
188
|
-
config.pinHash = msg.pinHash || null;
|
|
189
|
-
relay.setAuthToken(config.pinHash);
|
|
190
|
-
saveConfig(config);
|
|
191
|
-
return { ok: true };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
case "set_project_title": {
|
|
195
|
-
if (!msg.slug) return { ok: false, error: "missing slug" };
|
|
196
|
-
var newTitle = msg.title || null;
|
|
197
|
-
relay.setProjectTitle(msg.slug, newTitle);
|
|
198
|
-
for (var ti = 0; ti < config.projects.length; ti++) {
|
|
199
|
-
if (config.projects[ti].slug === msg.slug) {
|
|
200
|
-
if (newTitle) {
|
|
201
|
-
config.projects[ti].title = newTitle;
|
|
202
|
-
} else {
|
|
203
|
-
delete config.projects[ti].title;
|
|
204
|
-
}
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
saveConfig(config);
|
|
209
|
-
try { syncClayrc(config.projects); } catch (e) {}
|
|
210
|
-
console.log("[daemon] Project title:", msg.slug, "→", newTitle || "(default)");
|
|
211
|
-
return { ok: true };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
case "set_keep_awake": {
|
|
215
|
-
var want = !!msg.value;
|
|
216
|
-
config.keepAwake = want;
|
|
217
|
-
saveConfig(config);
|
|
218
|
-
if (want && !caffeinateProc && process.platform === "darwin") {
|
|
219
|
-
try {
|
|
220
|
-
var { spawn: spawnCaff } = require("child_process");
|
|
221
|
-
caffeinateProc = spawnCaff("caffeinate", ["-di"], { stdio: "ignore", detached: false });
|
|
222
|
-
caffeinateProc.on("error", function () { caffeinateProc = null; });
|
|
223
|
-
} catch (e) {}
|
|
224
|
-
} else if (!want && caffeinateProc) {
|
|
225
|
-
try { caffeinateProc.kill(); } catch (e) {}
|
|
226
|
-
caffeinateProc = null;
|
|
227
|
-
}
|
|
228
|
-
console.log("[daemon] Keep awake:", want);
|
|
229
|
-
return { ok: true };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
case "shutdown":
|
|
233
|
-
console.log("[daemon] Shutdown requested via IPC");
|
|
234
|
-
gracefulShutdown();
|
|
235
|
-
return { ok: true };
|
|
236
|
-
|
|
237
|
-
case "update": {
|
|
238
|
-
console.log("[daemon] Update & restart requested via IPC");
|
|
239
|
-
|
|
240
|
-
// Dev mode (config.debug): just exit with code 120, cli.js dev watcher respawns daemon
|
|
241
|
-
if (config.debug) {
|
|
242
|
-
console.log("[daemon] Dev mode — restarting via dev watcher");
|
|
243
|
-
updateHandoff = true;
|
|
244
|
-
setTimeout(function () { gracefulShutdown(); }, 100);
|
|
245
|
-
return { ok: true };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Production: fetch latest via npx, then spawn updated daemon
|
|
249
|
-
var { execSync: execSyncUpd, spawn: spawnUpd } = require("child_process");
|
|
250
|
-
var updDaemonScript;
|
|
251
|
-
try {
|
|
252
|
-
// npx downloads the package and puts a bin symlink; `which` prints its path
|
|
253
|
-
var binPath = execSyncUpd(
|
|
254
|
-
"npx --yes --package=claude-relay@latest -- which claude-relay",
|
|
255
|
-
{ stdio: ["ignore", "pipe", "pipe"], timeout: 120000, encoding: "utf8" }
|
|
256
|
-
).trim();
|
|
257
|
-
// Resolve symlink to get the actual package directory
|
|
258
|
-
var realBin = fs.realpathSync(binPath);
|
|
259
|
-
updDaemonScript = path.join(path.dirname(realBin), "..", "lib", "daemon.js");
|
|
260
|
-
updDaemonScript = path.resolve(updDaemonScript);
|
|
261
|
-
console.log("[daemon] Resolved updated daemon:", updDaemonScript);
|
|
262
|
-
} catch (updErr) {
|
|
263
|
-
console.log("[daemon] npx resolve failed:", updErr.message);
|
|
264
|
-
// Fallback: restart with current code
|
|
265
|
-
updDaemonScript = path.join(__dirname, "daemon.js");
|
|
266
|
-
}
|
|
267
|
-
// Spawn new daemon process — it will retry if port is still in use
|
|
268
|
-
var { logPath: updLogPath, configPath: updConfigPath } = require("./config");
|
|
269
|
-
var updLogFd = fs.openSync(updLogPath(), "a");
|
|
270
|
-
var updChild = spawnUpd(process.execPath, [updDaemonScript], {
|
|
271
|
-
detached: true,
|
|
272
|
-
windowsHide: true,
|
|
273
|
-
stdio: ["ignore", updLogFd, updLogFd],
|
|
274
|
-
env: Object.assign({}, process.env, {
|
|
275
|
-
CLAUDE_RELAY_CONFIG: updConfigPath(),
|
|
276
|
-
}),
|
|
277
|
-
});
|
|
278
|
-
updChild.unref();
|
|
279
|
-
fs.closeSync(updLogFd);
|
|
280
|
-
config.pid = updChild.pid;
|
|
281
|
-
saveConfig(config);
|
|
282
|
-
console.log("[daemon] Spawned new daemon (PID " + updChild.pid + "), shutting down...");
|
|
283
|
-
updateHandoff = true;
|
|
284
|
-
setTimeout(function () { gracefulShutdown(); }, 100);
|
|
285
|
-
return { ok: true };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
default:
|
|
289
|
-
return { ok: false, error: "unknown command: " + msg.cmd };
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// --- Start listening (with retry for port-in-use during update handoff) ---
|
|
294
|
-
var listenRetries = 0;
|
|
295
|
-
var MAX_LISTEN_RETRIES = 15;
|
|
296
|
-
|
|
297
|
-
function startListening() {
|
|
298
|
-
relay.server.listen(config.port, function () {
|
|
299
|
-
var protocol = tlsOptions ? "https" : "http";
|
|
300
|
-
console.log("[daemon] Listening on " + protocol + "://0.0.0.0:" + config.port);
|
|
301
|
-
console.log("[daemon] PID:", process.pid);
|
|
302
|
-
console.log("[daemon] Projects:", config.projects.length);
|
|
303
|
-
|
|
304
|
-
// Update PID in config
|
|
305
|
-
config.pid = process.pid;
|
|
306
|
-
saveConfig(config);
|
|
307
|
-
|
|
308
|
-
// Check for crash info from a previous crash and notify clients
|
|
309
|
-
var crashInfo = readCrashInfo();
|
|
310
|
-
if (crashInfo) {
|
|
311
|
-
console.log("[daemon] Recovered from crash at", new Date(crashInfo.time).toISOString());
|
|
312
|
-
console.log("[daemon] Crash reason:", crashInfo.reason);
|
|
313
|
-
// Delay notification so clients have time to reconnect
|
|
314
|
-
setTimeout(function () {
|
|
315
|
-
relay.broadcastAll({
|
|
316
|
-
type: "toast",
|
|
317
|
-
level: "warn",
|
|
318
|
-
message: "Server recovered from a crash and was automatically restarted.",
|
|
319
|
-
detail: crashInfo.reason || null,
|
|
320
|
-
});
|
|
321
|
-
}, 3000);
|
|
322
|
-
clearCrashInfo();
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
relay.server.on("error", function (err) {
|
|
328
|
-
if (err.code === "EADDRINUSE" && listenRetries < MAX_LISTEN_RETRIES) {
|
|
329
|
-
listenRetries++;
|
|
330
|
-
console.log("[daemon] Port " + config.port + " in use, retrying (" + listenRetries + "/" + MAX_LISTEN_RETRIES + ")...");
|
|
331
|
-
setTimeout(startListening, 1000);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
console.error("[daemon] Server error:", err.message);
|
|
335
|
-
writeCrashInfo({
|
|
336
|
-
reason: "Server error: " + err.message,
|
|
337
|
-
pid: process.pid,
|
|
338
|
-
time: Date.now(),
|
|
339
|
-
});
|
|
340
|
-
process.exit(1);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
startListening();
|
|
344
|
-
|
|
345
|
-
// --- HTTP onboarding server (only when TLS is active) ---
|
|
346
|
-
if (relay.onboardingServer) {
|
|
347
|
-
var onboardingPort = config.port + 1;
|
|
348
|
-
relay.onboardingServer.on("error", function (err) {
|
|
349
|
-
console.error("[daemon] Onboarding HTTP server error:", err.message);
|
|
350
|
-
});
|
|
351
|
-
relay.onboardingServer.listen(onboardingPort, function () {
|
|
352
|
-
console.log("[daemon] Onboarding HTTP on http://0.0.0.0:" + onboardingPort);
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// --- Caffeinate (macOS) ---
|
|
357
|
-
var caffeinateProc = null;
|
|
358
|
-
if (config.keepAwake && process.platform === "darwin") {
|
|
359
|
-
try {
|
|
360
|
-
var { spawn } = require("child_process");
|
|
361
|
-
caffeinateProc = spawn("caffeinate", ["-di"], { stdio: "ignore", detached: false });
|
|
362
|
-
caffeinateProc.on("error", function () { caffeinateProc = null; });
|
|
363
|
-
} catch (e) {}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// --- Graceful shutdown ---
|
|
367
|
-
var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
|
|
368
|
-
|
|
369
|
-
function gracefulShutdown() {
|
|
370
|
-
console.log("[daemon] Shutting down...");
|
|
371
|
-
var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
|
|
372
|
-
|
|
373
|
-
if (caffeinateProc) {
|
|
374
|
-
try { caffeinateProc.kill(); } catch (e) {}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
ipc.close();
|
|
378
|
-
|
|
379
|
-
// Remove PID from config (skip if update handoff — new daemon PID is already saved)
|
|
380
|
-
if (!updateHandoff) {
|
|
381
|
-
try {
|
|
382
|
-
var c = loadConfig();
|
|
383
|
-
if (c && c.pid === process.pid) {
|
|
384
|
-
delete c.pid;
|
|
385
|
-
saveConfig(c);
|
|
386
|
-
}
|
|
387
|
-
} catch (e) {}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
relay.destroyAll();
|
|
391
|
-
|
|
392
|
-
if (relay.onboardingServer) {
|
|
393
|
-
relay.onboardingServer.close();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
relay.server.close(function () {
|
|
397
|
-
console.log("[daemon] Server closed");
|
|
398
|
-
process.exit(exitCode);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// Force exit after 5 seconds
|
|
402
|
-
setTimeout(function () {
|
|
403
|
-
console.error("[daemon] Forced exit after timeout");
|
|
404
|
-
process.exit(1);
|
|
405
|
-
}, 5000);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
process.on("SIGTERM", gracefulShutdown);
|
|
409
|
-
process.on("SIGINT", gracefulShutdown);
|
|
410
|
-
// Windows emits SIGHUP when console window closes
|
|
411
|
-
if (process.platform === "win32") {
|
|
412
|
-
process.on("SIGHUP", gracefulShutdown);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
process.on("uncaughtException", function (err) {
|
|
416
|
-
console.error("[daemon] Uncaught exception:", err);
|
|
417
|
-
writeCrashInfo({
|
|
418
|
-
reason: err ? (err.stack || err.message || String(err)) : "unknown",
|
|
419
|
-
pid: process.pid,
|
|
420
|
-
time: Date.now(),
|
|
421
|
-
});
|
|
422
|
-
gracefulShutdown();
|
|
423
|
-
});
|