clay-server 2.5.1 → 2.6.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/claude-relay.js +6 -0
- package/bin/cli.js +26 -8
- package/lib/cli-sessions.js +2 -7
- package/lib/config.js +78 -5
- package/lib/daemon.js +233 -2
- package/lib/notes.js +3 -2
- package/lib/project.js +465 -28
- package/lib/public/app.js +187 -24
- package/lib/public/css/diff.css +3 -4
- package/lib/public/css/filebrowser.css +362 -2
- package/lib/public/css/icon-strip.css +317 -1
- package/lib/public/css/input.css +127 -50
- package/lib/public/css/messages.css +1 -1
- package/lib/public/css/mobile-nav.css +6 -2
- package/lib/public/css/server-settings.css +67 -20
- package/lib/public/css/sidebar.css +9 -4
- package/lib/public/css/skills.css +730 -0
- package/lib/public/css/title-bar.css +74 -4
- package/lib/public/index.html +261 -54
- package/lib/public/modules/input.js +119 -56
- package/lib/public/modules/project-settings.js +906 -0
- package/lib/public/modules/server-settings.js +409 -53
- package/lib/public/modules/sidebar.js +720 -1
- package/lib/public/modules/skills.js +710 -0
- package/lib/public/modules/terminal.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +18 -7
- package/lib/server.js +305 -1
- package/lib/sessions.js +9 -4
- package/lib/utils.js +18 -0
- package/package.json +3 -2
|
@@ -553,6 +553,13 @@ export function resetTerminals() {
|
|
|
553
553
|
renderTabBar();
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
+
export function sendTerminalCommand(command) {
|
|
557
|
+
if (!activeTabId || !tabs.has(activeTabId)) return;
|
|
558
|
+
if (ctx.ws && ctx.connected) {
|
|
559
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: activeTabId, data: command }));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
556
563
|
export function setTerminalTheme(xtermTheme) {
|
|
557
564
|
for (var tab of tabs.values()) {
|
|
558
565
|
if (tab.xterm) {
|
package/lib/public/style.css
CHANGED
package/lib/sdk-bridge.js
CHANGED
|
@@ -53,6 +53,7 @@ function createSDKBridge(opts) {
|
|
|
53
53
|
var pushModule = opts.pushModule;
|
|
54
54
|
var getSDK = opts.getSDK;
|
|
55
55
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
56
|
+
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
56
57
|
|
|
57
58
|
// --- Skill discovery helpers ---
|
|
58
59
|
|
|
@@ -147,8 +148,8 @@ function createSDKBridge(opts) {
|
|
|
147
148
|
send({ type: "slash_commands", commands: sm.slashCommands });
|
|
148
149
|
}
|
|
149
150
|
if (parsed.model) {
|
|
150
|
-
sm.currentModel = parsed.model;
|
|
151
|
-
send({ type: "model_info", model:
|
|
151
|
+
sm.currentModel = sm._savedDefaultModel || parsed.model;
|
|
152
|
+
send({ type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
|
|
152
153
|
}
|
|
153
154
|
if (parsed.fast_mode_state) {
|
|
154
155
|
sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
|
|
@@ -307,6 +308,7 @@ function createSDKBridge(opts) {
|
|
|
307
308
|
session.activeTaskToolIds = {};
|
|
308
309
|
session.taskIdMap = {};
|
|
309
310
|
session.isProcessing = false;
|
|
311
|
+
onProcessingChanged();
|
|
310
312
|
sendAndRecord(session, {
|
|
311
313
|
type: "result",
|
|
312
314
|
cost: parsed.total_cost_usd,
|
|
@@ -609,6 +611,7 @@ function createSDKBridge(opts) {
|
|
|
609
611
|
} catch (err) {
|
|
610
612
|
if (session.isProcessing) {
|
|
611
613
|
session.isProcessing = false;
|
|
614
|
+
onProcessingChanged();
|
|
612
615
|
if (err.name === "AbortError" || (session.abortController && session.abortController.signal.aborted)) {
|
|
613
616
|
sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
|
|
614
617
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
@@ -705,6 +708,7 @@ function createSDKBridge(opts) {
|
|
|
705
708
|
sdk = await getSDK();
|
|
706
709
|
} catch (e) {
|
|
707
710
|
session.isProcessing = false;
|
|
711
|
+
onProcessingChanged();
|
|
708
712
|
send({ type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
|
|
709
713
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
710
714
|
sm.broadcastSessionList();
|
|
@@ -794,6 +798,7 @@ function createSDKBridge(opts) {
|
|
|
794
798
|
console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!queryOptions.resume);
|
|
795
799
|
console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
|
|
796
800
|
session.isProcessing = false;
|
|
801
|
+
onProcessingChanged();
|
|
797
802
|
session.queryInstance = null;
|
|
798
803
|
session.messageQueue = null;
|
|
799
804
|
session.abortController = null;
|
|
@@ -803,7 +808,7 @@ function createSDKBridge(opts) {
|
|
|
803
808
|
return;
|
|
804
809
|
}
|
|
805
810
|
|
|
806
|
-
processQueryStream(session).catch(function(err) {
|
|
811
|
+
session.streamPromise = processQueryStream(session).catch(function(err) {
|
|
807
812
|
});
|
|
808
813
|
}
|
|
809
814
|
|
|
@@ -929,30 +934,36 @@ function createSDKBridge(opts) {
|
|
|
929
934
|
// No active query — just store the model for next startQuery
|
|
930
935
|
sm.currentModel = model;
|
|
931
936
|
send({ type: "model_info", model: model, models: sm.availableModels || [] });
|
|
932
|
-
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "
|
|
937
|
+
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
933
938
|
return;
|
|
934
939
|
}
|
|
935
940
|
try {
|
|
936
941
|
await session.queryInstance.setModel(model);
|
|
937
942
|
sm.currentModel = model;
|
|
938
943
|
send({ type: "model_info", model: model, models: sm.availableModels || [] });
|
|
939
|
-
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "
|
|
944
|
+
send({ type: "config_state", model: sm.currentModel, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
940
945
|
} catch (e) {
|
|
941
946
|
send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
|
|
942
947
|
}
|
|
943
948
|
}
|
|
944
949
|
|
|
945
950
|
async function setPermissionMode(session, mode) {
|
|
951
|
+
// When dangerouslySkipPermissions is active, ignore mode changes from UI
|
|
952
|
+
// to prevent accidentally downgrading from bypassPermissions
|
|
953
|
+
if (dangerouslySkipPermissions) {
|
|
954
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
946
957
|
if (!session.queryInstance) {
|
|
947
958
|
// No active query — just store the mode for next startQuery
|
|
948
959
|
sm.currentPermissionMode = mode;
|
|
949
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "
|
|
960
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
950
961
|
return;
|
|
951
962
|
}
|
|
952
963
|
try {
|
|
953
964
|
await session.queryInstance.setPermissionMode(mode);
|
|
954
965
|
sm.currentPermissionMode = mode;
|
|
955
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "
|
|
966
|
+
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
956
967
|
} catch (e) {
|
|
957
968
|
send({ type: "error", text: "Failed to set permission mode: " + (e.message || e) });
|
|
958
969
|
}
|
package/lib/server.js
CHANGED
|
@@ -8,10 +8,160 @@ var { createProjectContext } = require("./project");
|
|
|
8
8
|
|
|
9
9
|
var { CONFIG_DIR } = require("./config");
|
|
10
10
|
|
|
11
|
+
var https = require("https");
|
|
12
|
+
|
|
11
13
|
var publicDir = path.join(__dirname, "public");
|
|
12
14
|
var bundledThemesDir = path.join(__dirname, "themes");
|
|
13
15
|
var userThemesDir = path.join(CONFIG_DIR, "themes");
|
|
14
16
|
|
|
17
|
+
// --- Skills proxy cache & helpers ---
|
|
18
|
+
var skillsCache = {};
|
|
19
|
+
|
|
20
|
+
function httpGet(url) {
|
|
21
|
+
return new Promise(function (resolve, reject) {
|
|
22
|
+
var mod = url.startsWith("https") ? https : http;
|
|
23
|
+
mod.get(url, { headers: { "User-Agent": "Clay/1.0" } }, function (resp) {
|
|
24
|
+
if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) {
|
|
25
|
+
return httpGet(resp.headers.location).then(resolve, reject);
|
|
26
|
+
}
|
|
27
|
+
var chunks = [];
|
|
28
|
+
resp.on("data", function (c) { chunks.push(c); });
|
|
29
|
+
resp.on("end", function () { resolve(Buffer.concat(chunks).toString("utf8")); });
|
|
30
|
+
resp.on("error", reject);
|
|
31
|
+
}).on("error", reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fetchSkillsPage(url) {
|
|
36
|
+
return httpGet(url).then(function (html) {
|
|
37
|
+
// Data is inside self.__next_f.push() with escaped quotes: \"initialSkills\":[{\"source\":...}]
|
|
38
|
+
var marker = 'initialSkills';
|
|
39
|
+
var idx = html.indexOf(marker);
|
|
40
|
+
if (idx < 0) return { skills: [] };
|
|
41
|
+
|
|
42
|
+
// Find the start of the array: look for \\\":[
|
|
43
|
+
var arrStart = html.indexOf(':[', idx);
|
|
44
|
+
if (arrStart < 0) return { skills: [] };
|
|
45
|
+
arrStart += 1; // point to '['
|
|
46
|
+
|
|
47
|
+
// Find matching ']' — track bracket depth
|
|
48
|
+
var depth = 0;
|
|
49
|
+
var arrEnd = -1;
|
|
50
|
+
for (var i = arrStart; i < html.length; i++) {
|
|
51
|
+
var ch = html[i];
|
|
52
|
+
if (ch === '[') depth++;
|
|
53
|
+
else if (ch === ']') {
|
|
54
|
+
depth--;
|
|
55
|
+
if (depth === 0) { arrEnd = i + 1; break; }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (arrEnd < 0) return { skills: [] };
|
|
59
|
+
|
|
60
|
+
var raw = html.substring(arrStart, arrEnd);
|
|
61
|
+
// Unescape: \\\" → " and \\\\ → backslash
|
|
62
|
+
var unescaped = raw.replace(/\\\\"/g, '__BSLASH_QUOTE__').replace(/\\"/g, '"').replace(/__BSLASH_QUOTE__/g, '\\"');
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
return { skills: JSON.parse(unescaped) };
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { skills: [] };
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fetchSkillDetail(url) {
|
|
73
|
+
return httpGet(url).then(function (html) {
|
|
74
|
+
var result = {};
|
|
75
|
+
|
|
76
|
+
// Title: "skill-name by owner/repo"
|
|
77
|
+
var titleMatch = html.match(/<title>([^<]+)<\/title>/);
|
|
78
|
+
if (titleMatch) {
|
|
79
|
+
var parts = titleMatch[1].split(" by ");
|
|
80
|
+
result.name = parts[0].trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Description from meta
|
|
84
|
+
var descMatch = html.match(/meta name="description" content="([^"]+)"/);
|
|
85
|
+
if (descMatch) result.description = descMatch[1];
|
|
86
|
+
|
|
87
|
+
// Install command
|
|
88
|
+
var cmdMatch = html.match(/npx skills add [^ ]+ --skill [^ "<]+/);
|
|
89
|
+
if (cmdMatch) result.command = cmdMatch[0];
|
|
90
|
+
|
|
91
|
+
// Weekly installs: "Weekly Installs</span></div><div ...>VALUE</div>"
|
|
92
|
+
var wiMatch = html.match(/Weekly Installs<\/span><\/div><div[^>]*>([\d,.]+K?)<\/div>/);
|
|
93
|
+
if (wiMatch) result.weeklyInstalls = wiMatch[1];
|
|
94
|
+
|
|
95
|
+
// GitHub Stars: after SVG icon, inside <span>X.XK</span>
|
|
96
|
+
var gsIdx = html.indexOf("GitHub Stars");
|
|
97
|
+
if (gsIdx > 0) {
|
|
98
|
+
var gsRegion = html.substring(gsIdx, gsIdx + 1000);
|
|
99
|
+
var gsVal = gsRegion.match(/<span>(\d[\d,.]*K?)<\/span>/);
|
|
100
|
+
if (gsVal) result.githubStars = gsVal[1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// First Seen
|
|
104
|
+
var fsMatch = html.match(/First Seen<\/span><\/div><div[^>]*>([^<]+)<\/div>/);
|
|
105
|
+
if (fsMatch) result.firstSeen = fsMatch[1].trim();
|
|
106
|
+
|
|
107
|
+
// Repository: from title "by owner/repo"
|
|
108
|
+
if (titleMatch) {
|
|
109
|
+
var byParts = titleMatch[1].split(" by ");
|
|
110
|
+
if (byParts[1]) result.repository = byParts[1].trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Security audits: "text-foreground truncate">NAME</span><span ...>STATUS</span>"
|
|
114
|
+
var audits = [];
|
|
115
|
+
var auditRegex = /class="text-sm font-medium text-foreground truncate">([^<]+)<\/span><span class="[^"]*">(\w+)<\/span>/g;
|
|
116
|
+
var am;
|
|
117
|
+
while ((am = auditRegex.exec(html)) !== null) {
|
|
118
|
+
audits.push({ name: am[1], status: am[2].toLowerCase() });
|
|
119
|
+
}
|
|
120
|
+
if (audits.length) result.audits = audits;
|
|
121
|
+
|
|
122
|
+
// Installed on: "text-foreground">NAME</span><span class="text-muted-foreground font-mono">COUNT</span>
|
|
123
|
+
var ioIdx = html.indexOf("Installed On");
|
|
124
|
+
if (ioIdx > 0) {
|
|
125
|
+
var ioRegion = html.substring(ioIdx, ioIdx + 3000);
|
|
126
|
+
var platforms = [];
|
|
127
|
+
var platRegex = /text-foreground">([^<]+)<\/span><span class="text-muted-foreground font-mono">([\d,.]+K?)<\/span>/g;
|
|
128
|
+
var pm;
|
|
129
|
+
while ((pm = platRegex.exec(ioRegion)) !== null) {
|
|
130
|
+
platforms.push({ name: pm[1], installs: pm[2] });
|
|
131
|
+
}
|
|
132
|
+
if (platforms.length) result.installedOn = platforms;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// SKILL.md content: rendered HTML inside the main content area
|
|
136
|
+
var skillMdIdx = html.indexOf("SKILL.md");
|
|
137
|
+
if (skillMdIdx > 0) {
|
|
138
|
+
// Find the prose content div after SKILL.md marker
|
|
139
|
+
var proseIdx = html.indexOf("prose", skillMdIdx);
|
|
140
|
+
if (proseIdx > 0) {
|
|
141
|
+
var proseStart = html.indexOf(">", proseIdx) + 1;
|
|
142
|
+
// Find the closing of the prose div (heuristic: next major section boundary)
|
|
143
|
+
var endMarkers = ["<div class=\"bg-background", "<div class=\"sticky"];
|
|
144
|
+
var proseEnd = html.length;
|
|
145
|
+
for (var em = 0; em < endMarkers.length; em++) {
|
|
146
|
+
var endIdx = html.indexOf(endMarkers[em], proseStart);
|
|
147
|
+
if (endIdx > 0 && endIdx < proseEnd) proseEnd = endIdx;
|
|
148
|
+
}
|
|
149
|
+
var rawMd = html.substring(proseStart, proseEnd);
|
|
150
|
+
// Rebase relative URLs to absolute (skills.sh base)
|
|
151
|
+
result.skillMd = rawMd
|
|
152
|
+
.replace(/src="(?!https?:\/\/|data:)([^"]+)"/g, function (m, p) {
|
|
153
|
+
return 'src="' + new URL(p, url).href + '"';
|
|
154
|
+
})
|
|
155
|
+
.replace(/href="(?!https?:\/\/|mailto:|#)([^"]+)"/g, function (m, p) {
|
|
156
|
+
return 'href="' + new URL(p, url).href + '"';
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
15
165
|
var MIME_TYPES = {
|
|
16
166
|
".html": "text/html",
|
|
17
167
|
".css": "text/css",
|
|
@@ -130,6 +280,21 @@ function createServer(opts) {
|
|
|
130
280
|
var lanHost = opts.lanHost || null;
|
|
131
281
|
var onAddProject = opts.onAddProject || null;
|
|
132
282
|
var onRemoveProject = opts.onRemoveProject || null;
|
|
283
|
+
var onReorderProjects = opts.onReorderProjects || null;
|
|
284
|
+
var onSetProjectTitle = opts.onSetProjectTitle || null;
|
|
285
|
+
var onSetProjectIcon = opts.onSetProjectIcon || null;
|
|
286
|
+
var onGetServerDefaultEffort = opts.onGetServerDefaultEffort || null;
|
|
287
|
+
var onSetServerDefaultEffort = opts.onSetServerDefaultEffort || null;
|
|
288
|
+
var onGetProjectDefaultEffort = opts.onGetProjectDefaultEffort || null;
|
|
289
|
+
var onSetProjectDefaultEffort = opts.onSetProjectDefaultEffort || null;
|
|
290
|
+
var onGetServerDefaultModel = opts.onGetServerDefaultModel || null;
|
|
291
|
+
var onSetServerDefaultModel = opts.onSetServerDefaultModel || null;
|
|
292
|
+
var onGetProjectDefaultModel = opts.onGetProjectDefaultModel || null;
|
|
293
|
+
var onSetProjectDefaultModel = opts.onSetProjectDefaultModel || null;
|
|
294
|
+
var onGetServerDefaultMode = opts.onGetServerDefaultMode || null;
|
|
295
|
+
var onSetServerDefaultMode = opts.onSetServerDefaultMode || null;
|
|
296
|
+
var onGetProjectDefaultMode = opts.onGetProjectDefaultMode || null;
|
|
297
|
+
var onSetProjectDefaultMode = opts.onSetProjectDefaultMode || null;
|
|
133
298
|
var onGetDaemonConfig = opts.onGetDaemonConfig || null;
|
|
134
299
|
var onSetPin = opts.onSetPin || null;
|
|
135
300
|
var onSetKeepAwake = opts.onSetKeepAwake || null;
|
|
@@ -286,6 +451,89 @@ function createServer(opts) {
|
|
|
286
451
|
return;
|
|
287
452
|
}
|
|
288
453
|
|
|
454
|
+
// Skills proxy: leaderboard list
|
|
455
|
+
if (req.method === "GET" && fullUrl === "/api/skills") {
|
|
456
|
+
var qs = req.url.indexOf("?") >= 0 ? req.url.substring(req.url.indexOf("?")) : "";
|
|
457
|
+
var tabParam = new URLSearchParams(qs).get("tab") || "all";
|
|
458
|
+
var tabPath = tabParam === "trending" ? "/trending" : tabParam === "hot" ? "/hot" : "/";
|
|
459
|
+
var cacheKey = "skills_" + tabParam;
|
|
460
|
+
var cached = skillsCache[cacheKey];
|
|
461
|
+
if (cached && Date.now() - cached.ts < 300000) {
|
|
462
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
463
|
+
res.end(cached.data);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
fetchSkillsPage("https://skills.sh" + tabPath).then(function (data) {
|
|
467
|
+
var json = JSON.stringify(data);
|
|
468
|
+
skillsCache[cacheKey] = { ts: Date.now(), data: json };
|
|
469
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
470
|
+
res.end(json);
|
|
471
|
+
}).catch(function (err) {
|
|
472
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
473
|
+
res.end(JSON.stringify({ error: "Failed to fetch skills: " + (err.message || err) }));
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Skills proxy: search
|
|
479
|
+
if (req.method === "GET" && fullUrl.startsWith("/api/skills/search")) {
|
|
480
|
+
var sqsRaw = req.url.indexOf("?") >= 0 ? req.url.substring(req.url.indexOf("?")) : "";
|
|
481
|
+
var searchQ = new URLSearchParams(sqsRaw).get("q") || "";
|
|
482
|
+
if (!searchQ) {
|
|
483
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
484
|
+
res.end('{"error":"missing q param"}');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
var searchCacheKey = "search_" + searchQ.toLowerCase();
|
|
488
|
+
var searchCached = skillsCache[searchCacheKey];
|
|
489
|
+
if (searchCached && Date.now() - searchCached.ts < 300000) {
|
|
490
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
491
|
+
res.end(searchCached.data);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
fetchSkillsPage("https://skills.sh/?q=" + encodeURIComponent(searchQ)).then(function (data) {
|
|
495
|
+
var json = JSON.stringify(data);
|
|
496
|
+
skillsCache[searchCacheKey] = { ts: Date.now(), data: json };
|
|
497
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
498
|
+
res.end(json);
|
|
499
|
+
}).catch(function (err) {
|
|
500
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
501
|
+
res.end(JSON.stringify({ error: "Failed to search skills: " + (err.message || err) }));
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Skills proxy: skill detail
|
|
507
|
+
if (req.method === "GET" && fullUrl.startsWith("/api/skills/detail")) {
|
|
508
|
+
var qs2 = req.url.indexOf("?") >= 0 ? req.url.substring(req.url.indexOf("?")) : "";
|
|
509
|
+
var params2 = new URLSearchParams(qs2);
|
|
510
|
+
var detailSource = params2.get("source");
|
|
511
|
+
var detailSkill = params2.get("skill");
|
|
512
|
+
if (!detailSource || !detailSkill) {
|
|
513
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
514
|
+
res.end('{"error":"missing source or skill param"}');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
var detailCacheKey = "detail_" + detailSource + "_" + detailSkill;
|
|
518
|
+
var detailCached = skillsCache[detailCacheKey];
|
|
519
|
+
if (detailCached && Date.now() - detailCached.ts < 300000) {
|
|
520
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
521
|
+
res.end(detailCached.data);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
var detailUrl = "https://skills.sh/" + encodeURIComponent(detailSource).replace(/%2F/g, "/") + "/" + encodeURIComponent(detailSkill);
|
|
525
|
+
fetchSkillDetail(detailUrl).then(function (data) {
|
|
526
|
+
var json = JSON.stringify(data);
|
|
527
|
+
skillsCache[detailCacheKey] = { ts: Date.now(), data: json };
|
|
528
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
529
|
+
res.end(json);
|
|
530
|
+
}).catch(function (err) {
|
|
531
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
532
|
+
res.end(JSON.stringify({ error: "Failed to fetch skill detail: " + (err.message || err) }));
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
289
537
|
// Root path — redirect to first project
|
|
290
538
|
if (fullUrl === "/" && req.method === "GET") {
|
|
291
539
|
if (!isAuthed(req, authToken)) {
|
|
@@ -494,13 +742,28 @@ function createServer(opts) {
|
|
|
494
742
|
});
|
|
495
743
|
});
|
|
496
744
|
|
|
745
|
+
// --- Debounced broadcast for processing status changes ---
|
|
746
|
+
var processingUpdateTimer = null;
|
|
747
|
+
function broadcastProcessingChange() {
|
|
748
|
+
if (processingUpdateTimer) clearTimeout(processingUpdateTimer);
|
|
749
|
+
processingUpdateTimer = setTimeout(function () {
|
|
750
|
+
processingUpdateTimer = null;
|
|
751
|
+
broadcastAll({
|
|
752
|
+
type: "projects_updated",
|
|
753
|
+
projects: getProjects(),
|
|
754
|
+
projectCount: projects.size,
|
|
755
|
+
});
|
|
756
|
+
}, 200);
|
|
757
|
+
}
|
|
758
|
+
|
|
497
759
|
// --- Project management ---
|
|
498
|
-
function addProject(cwd, slug, title) {
|
|
760
|
+
function addProject(cwd, slug, title, icon) {
|
|
499
761
|
if (projects.has(slug)) return false;
|
|
500
762
|
var ctx = createProjectContext({
|
|
501
763
|
cwd: cwd,
|
|
502
764
|
slug: slug,
|
|
503
765
|
title: title || null,
|
|
766
|
+
icon: icon || null,
|
|
504
767
|
pushModule: pushModule,
|
|
505
768
|
debug: debug,
|
|
506
769
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
@@ -512,8 +775,24 @@ function createServer(opts) {
|
|
|
512
775
|
projects.forEach(function (ctx) { list.push(ctx.getStatus()); });
|
|
513
776
|
return list;
|
|
514
777
|
},
|
|
778
|
+
onProcessingChanged: broadcastProcessingChange,
|
|
515
779
|
onAddProject: onAddProject,
|
|
516
780
|
onRemoveProject: onRemoveProject,
|
|
781
|
+
onReorderProjects: onReorderProjects,
|
|
782
|
+
onSetProjectTitle: onSetProjectTitle,
|
|
783
|
+
onSetProjectIcon: onSetProjectIcon,
|
|
784
|
+
onGetServerDefaultEffort: onGetServerDefaultEffort,
|
|
785
|
+
onSetServerDefaultEffort: onSetServerDefaultEffort,
|
|
786
|
+
onGetProjectDefaultEffort: onGetProjectDefaultEffort,
|
|
787
|
+
onSetProjectDefaultEffort: onSetProjectDefaultEffort,
|
|
788
|
+
onGetServerDefaultModel: onGetServerDefaultModel,
|
|
789
|
+
onSetServerDefaultModel: onSetServerDefaultModel,
|
|
790
|
+
onGetProjectDefaultModel: onGetProjectDefaultModel,
|
|
791
|
+
onSetProjectDefaultModel: onSetProjectDefaultModel,
|
|
792
|
+
onGetServerDefaultMode: onGetServerDefaultMode,
|
|
793
|
+
onSetServerDefaultMode: onSetServerDefaultMode,
|
|
794
|
+
onGetProjectDefaultMode: onGetProjectDefaultMode,
|
|
795
|
+
onSetProjectDefaultMode: onSetProjectDefaultMode,
|
|
517
796
|
onGetDaemonConfig: onGetDaemonConfig,
|
|
518
797
|
onSetPin: onSetPin,
|
|
519
798
|
onSetKeepAwake: onSetKeepAwake,
|
|
@@ -540,6 +819,22 @@ function createServer(opts) {
|
|
|
540
819
|
return list;
|
|
541
820
|
}
|
|
542
821
|
|
|
822
|
+
function reorderProjects(slugs) {
|
|
823
|
+
var ordered = new Map();
|
|
824
|
+
for (var i = 0; i < slugs.length; i++) {
|
|
825
|
+
var ctx = projects.get(slugs[i]);
|
|
826
|
+
if (ctx) ordered.set(slugs[i], ctx);
|
|
827
|
+
}
|
|
828
|
+
// Append any remaining (safety)
|
|
829
|
+
projects.forEach(function (ctx, slug) {
|
|
830
|
+
if (!ordered.has(slug)) ordered.set(slug, ctx);
|
|
831
|
+
});
|
|
832
|
+
projects.clear();
|
|
833
|
+
ordered.forEach(function (ctx, slug) {
|
|
834
|
+
projects.set(slug, ctx);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
543
838
|
function setProjectTitle(slug, title) {
|
|
544
839
|
var ctx = projects.get(slug);
|
|
545
840
|
if (!ctx) return false;
|
|
@@ -547,6 +842,13 @@ function createServer(opts) {
|
|
|
547
842
|
return true;
|
|
548
843
|
}
|
|
549
844
|
|
|
845
|
+
function setProjectIcon(slug, icon) {
|
|
846
|
+
var ctx = projects.get(slug);
|
|
847
|
+
if (!ctx) return false;
|
|
848
|
+
ctx.setIcon(icon);
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
|
|
550
852
|
function setAuthToken(hash) {
|
|
551
853
|
authToken = hash;
|
|
552
854
|
}
|
|
@@ -572,7 +874,9 @@ function createServer(opts) {
|
|
|
572
874
|
addProject: addProject,
|
|
573
875
|
removeProject: removeProject,
|
|
574
876
|
getProjects: getProjects,
|
|
877
|
+
reorderProjects: reorderProjects,
|
|
575
878
|
setProjectTitle: setProjectTitle,
|
|
879
|
+
setProjectIcon: setProjectIcon,
|
|
576
880
|
setAuthToken: setAuthToken,
|
|
577
881
|
broadcastAll: broadcastAll,
|
|
578
882
|
destroyAll: destroyAll,
|
package/lib/sessions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var config = require("./config");
|
|
4
|
+
var utils = require("./utils");
|
|
4
5
|
|
|
5
6
|
function createSessionManager(opts) {
|
|
6
7
|
var cwd = opts.cwd;
|
|
@@ -15,7 +16,7 @@ function createSessionManager(opts) {
|
|
|
15
16
|
var skillNames = null; // Claude-only skills to filter from slash menu
|
|
16
17
|
|
|
17
18
|
// --- Session persistence (centralized in ~/.clay/sessions/{encoded-cwd}/) ---
|
|
18
|
-
var encodedCwd =
|
|
19
|
+
var encodedCwd = utils.encodeCwd(cwd);
|
|
19
20
|
var sessionsDir = path.join(config.CONFIG_DIR, "sessions", encodedCwd);
|
|
20
21
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
21
22
|
|
|
@@ -310,6 +311,10 @@ function createSessionManager(opts) {
|
|
|
310
311
|
appendToSessionFile(session, obj);
|
|
311
312
|
if (session.localId === activeSessionId) {
|
|
312
313
|
send(obj);
|
|
314
|
+
} else if (session.isProcessing && !session._ioThrottle) {
|
|
315
|
+
session._ioThrottle = true;
|
|
316
|
+
send({ type: "session_io", id: session.localId });
|
|
317
|
+
setTimeout(function () { session._ioThrottle = false; }, 80);
|
|
313
318
|
}
|
|
314
319
|
}
|
|
315
320
|
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Encode a cwd path into a filesystem-safe directory/file name.
|
|
7
|
+
* Replaces forward slashes and dots with hyphens so that usernames
|
|
8
|
+
* like "jon.doe" don't break session/note lookups.
|
|
9
|
+
*
|
|
10
|
+
* Example: "/Users/jon.doe/my-project" -> "-Users-jon-doe-my-project"
|
|
11
|
+
*/
|
|
12
|
+
function encodeCwd(cwd) {
|
|
13
|
+
return cwd.replace(/[\/\.]/g, "-");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
encodeCwd: encodeCwd,
|
|
18
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Web UI for Claude Code. Any device. Push notifications.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"clay-server": "./bin/cli.js"
|
|
6
|
+
"clay-server": "./bin/cli.js",
|
|
7
|
+
"claude-relay": "./bin/claude-relay.js"
|
|
7
8
|
},
|
|
8
9
|
"scripts": {
|
|
9
10
|
"dev": "node bin/cli.js --dev",
|