clay-server 2.26.0-beta.1 → 2.26.0-beta.10
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 +5 -9
- package/lib/browser-mcp-server.js +496 -0
- package/lib/daemon.js +1 -1
- package/lib/os-users.js +23 -0
- package/lib/project-debate.js +206 -88
- package/lib/project-mate-interaction.js +766 -0
- package/lib/project-memory.js +677 -0
- package/lib/project.js +546 -1361
- package/lib/public/app.js +817 -175
- package/lib/public/css/debate.css +224 -2
- package/lib/public/css/icon-strip.css +10 -10
- package/lib/public/css/input.css +263 -83
- package/lib/public/css/mates.css +56 -57
- package/lib/public/css/mention.css +7 -4
- package/lib/public/css/menus.css +7 -0
- package/lib/public/css/messages.css +17 -0
- package/lib/public/css/mobile-nav.css +3 -1
- package/lib/public/css/overlays.css +181 -0
- package/lib/public/css/rewind.css +79 -0
- package/lib/public/css/server-settings.css +1 -0
- package/lib/public/css/sidebar.css +10 -0
- package/lib/public/css/title-bar.css +189 -3
- package/lib/public/index.html +53 -16
- package/lib/public/modules/context-sources.js +313 -0
- package/lib/public/modules/debate.js +184 -97
- package/lib/public/modules/input.js +18 -1
- package/lib/public/modules/mate-knowledge.js +11 -11
- package/lib/public/modules/mate-memory.js +5 -5
- package/lib/public/modules/mate-sidebar.js +13 -9
- package/lib/public/modules/mention.js +40 -2
- package/lib/public/modules/notifications.js +109 -1
- package/lib/public/modules/rewind.js +36 -0
- package/lib/public/modules/sidebar.js +107 -28
- package/lib/public/modules/terminal.js +8 -0
- package/lib/public/modules/theme.js +2 -1
- package/lib/public/modules/tools.js +69 -24
- package/lib/sdk-bridge.js +81 -7
- package/lib/sdk-worker.js +13 -1
- package/lib/server.js +42 -0
- package/lib/sessions.js +39 -7
- package/lib/terminal-manager.js +36 -6
- package/package.json +2 -2
package/lib/sdk-bridge.js
CHANGED
|
@@ -134,6 +134,7 @@ function createSDKBridge(opts) {
|
|
|
134
134
|
var mateDisplayName = opts.mateDisplayName || "";
|
|
135
135
|
var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
|
|
136
136
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
137
|
+
var mcpServers = opts.mcpServers || null;
|
|
137
138
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
138
139
|
var onTurnDone = opts.onTurnDone || null;
|
|
139
140
|
|
|
@@ -188,6 +189,10 @@ function createSDKBridge(opts) {
|
|
|
188
189
|
sm.sendAndRecord(session, obj);
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
function sendToSession(session, obj) {
|
|
193
|
+
sm.sendToSession(session, obj);
|
|
194
|
+
}
|
|
195
|
+
|
|
191
196
|
function processSDKMessage(session, parsed) {
|
|
192
197
|
// Timing: log key SDK milestones relative to query start
|
|
193
198
|
if (session._queryStartTs) {
|
|
@@ -435,6 +440,15 @@ function createSDKBridge(opts) {
|
|
|
435
440
|
session.isProcessing = false;
|
|
436
441
|
session.rateLimitResetsAt = null; // clear on success
|
|
437
442
|
onProcessingChanged();
|
|
443
|
+
// Fetch rich context usage breakdown (fire-and-forget, non-blocking)
|
|
444
|
+
if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
|
|
445
|
+
session.queryInstance.getContextUsage().then(function(ctxUsage) {
|
|
446
|
+
session.lastContextUsage = ctxUsage;
|
|
447
|
+
sendToSession(session, { type: "context_usage", data: ctxUsage });
|
|
448
|
+
}).catch(function(e) {
|
|
449
|
+
console.error("[sdk-bridge] getContextUsage failed (non-fatal):", e.message || e);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
438
452
|
var lastStreamInput = session.lastStreamInputTokens || null;
|
|
439
453
|
session.lastStreamInputTokens = null;
|
|
440
454
|
sendAndRecord(session, {
|
|
@@ -581,8 +595,26 @@ function createSDKBridge(opts) {
|
|
|
581
595
|
isUsingOverage: info.isUsingOverage || false,
|
|
582
596
|
});
|
|
583
597
|
// Track rejection for auto-continue / scheduled message support
|
|
584
|
-
if (
|
|
598
|
+
// Skip if using overage (extra usage) — user can continue immediately
|
|
599
|
+
if (info.status === "rejected" && info.resetsAt && !info.isUsingOverage) {
|
|
585
600
|
session.rateLimitResetsAt = info.resetsAt * 1000;
|
|
601
|
+
|
|
602
|
+
// Defensive: if query already completed before this event arrived,
|
|
603
|
+
// schedule auto-continue now (handles race condition where
|
|
604
|
+
// rate_limit_event arrives after the result/done event).
|
|
605
|
+
if (!session.isProcessing && !session.scheduledMessage && !session.destroying) {
|
|
606
|
+
var lateACEnabled = session.onQueryComplete ||
|
|
607
|
+
(typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
|
|
608
|
+
if (lateACEnabled) {
|
|
609
|
+
var lateResetsAt = session.rateLimitResetsAt;
|
|
610
|
+
session.rateLimitResetsAt = null;
|
|
611
|
+
session.rateLimitAutoContinuePending = true;
|
|
612
|
+
console.log("[sdk-bridge] Rate limit event arrived late, scheduling auto-continue for session " + session.localId);
|
|
613
|
+
if (typeof opts.scheduleMessage === "function") {
|
|
614
|
+
opts.scheduleMessage(session, "continue", lateResetsAt);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
586
618
|
}
|
|
587
619
|
}
|
|
588
620
|
|
|
@@ -1115,6 +1147,7 @@ function createSDKBridge(opts) {
|
|
|
1115
1147
|
agentProgressSummaries: true,
|
|
1116
1148
|
};
|
|
1117
1149
|
|
|
1150
|
+
if (mcpServers) queryOptions.mcpServers = mcpServers;
|
|
1118
1151
|
if (sm.currentModel) queryOptions.model = sm.currentModel;
|
|
1119
1152
|
if (sm.currentEffort) queryOptions.effort = sm.currentEffort;
|
|
1120
1153
|
if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
|
|
@@ -1138,6 +1171,8 @@ function createSDKBridge(opts) {
|
|
|
1138
1171
|
if (session.lastRewindUuid) {
|
|
1139
1172
|
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
1140
1173
|
delete session.lastRewindUuid;
|
|
1174
|
+
// Persist the deletion so server restarts don't re-use a stale UUID
|
|
1175
|
+
sm.saveSessionFile(session);
|
|
1141
1176
|
}
|
|
1142
1177
|
}
|
|
1143
1178
|
|
|
@@ -1194,6 +1229,11 @@ function createSDKBridge(opts) {
|
|
|
1194
1229
|
});
|
|
1195
1230
|
break;
|
|
1196
1231
|
|
|
1232
|
+
case "context_usage":
|
|
1233
|
+
session.lastContextUsage = msg.data;
|
|
1234
|
+
sendToSession(session, { type: "context_usage", data: msg.data });
|
|
1235
|
+
break;
|
|
1236
|
+
|
|
1197
1237
|
case "query_done":
|
|
1198
1238
|
console.log("[sdk-bridge] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
|
|
1199
1239
|
// Mark that we received a proper IPC completion, so the exit
|
|
@@ -1299,6 +1339,8 @@ function createSDKBridge(opts) {
|
|
|
1299
1339
|
sm.broadcastSessionList();
|
|
1300
1340
|
}
|
|
1301
1341
|
cleanupSessionWorker(session, worker);
|
|
1342
|
+
// Mark session as done so late rate_limit_event can detect race condition
|
|
1343
|
+
session.isProcessing = false;
|
|
1302
1344
|
// Auto-continue on rate limit (scheduler sessions, or user setting)
|
|
1303
1345
|
var workerDidScheduleAC = false;
|
|
1304
1346
|
var workerACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
|
|
@@ -1493,6 +1535,18 @@ function createSDKBridge(opts) {
|
|
|
1493
1535
|
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1494
1536
|
}
|
|
1495
1537
|
|
|
1538
|
+
// Auto-approve safe browser MCP tools.
|
|
1539
|
+
// Only watch/unwatch: user explicitly chose which tab to share.
|
|
1540
|
+
// Everything else (screenshot, read_page, list_tabs, etc.) can expose
|
|
1541
|
+
// content from tabs the user didn't intend to share, so require approval.
|
|
1542
|
+
var safeBrowserTools = { browser_watch_tab: true, browser_unwatch_tab: true };
|
|
1543
|
+
if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
|
|
1544
|
+
var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
|
|
1545
|
+
if (safeBrowserTools[mcpToolName]) {
|
|
1546
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1496
1550
|
// Auto-approve safe Bash commands (read-only, non-destructive)
|
|
1497
1551
|
// Applies to ALL sessions (mates and regular projects alike).
|
|
1498
1552
|
// These are purely read-only commands that cannot modify files, install
|
|
@@ -1687,10 +1741,16 @@ function createSDKBridge(opts) {
|
|
|
1687
1741
|
}
|
|
1688
1742
|
|
|
1689
1743
|
async function processQueryStream(session) {
|
|
1744
|
+
// Capture references at start so we only clean up OUR resources in finally,
|
|
1745
|
+
// not resources from a newer query that may have been created after an abort.
|
|
1746
|
+
var myQueryInstance = session.queryInstance;
|
|
1747
|
+
var myMessageQueue = session.messageQueue;
|
|
1748
|
+
var myAbortController = session.abortController;
|
|
1690
1749
|
try {
|
|
1691
|
-
for await (var msg of
|
|
1750
|
+
for await (var msg of myQueryInstance) {
|
|
1692
1751
|
processSDKMessage(session, msg);
|
|
1693
1752
|
}
|
|
1753
|
+
// (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
|
|
1694
1754
|
// Stream ended normally after a task stop — no "result" message was sent,
|
|
1695
1755
|
// so the session is still marked as processing. Send interrupted feedback.
|
|
1696
1756
|
if (session.isProcessing && session.taskStopRequested) {
|
|
@@ -1704,7 +1764,7 @@ function createSDKBridge(opts) {
|
|
|
1704
1764
|
if (session.isProcessing) {
|
|
1705
1765
|
session.isProcessing = false;
|
|
1706
1766
|
onProcessingChanged();
|
|
1707
|
-
if (err.name === "AbortError" || (
|
|
1767
|
+
if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
|
|
1708
1768
|
if (!session.destroying) {
|
|
1709
1769
|
sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
|
|
1710
1770
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
@@ -1777,22 +1837,29 @@ function createSDKBridge(opts) {
|
|
|
1777
1837
|
} finally {
|
|
1778
1838
|
// Close the SDK query to terminate the underlying claude child process.
|
|
1779
1839
|
// Without this, the process stays alive indefinitely (single-user mode).
|
|
1780
|
-
if
|
|
1840
|
+
// Only clean up if the session still references OUR resources.
|
|
1841
|
+
// A rewind + new startQuery may have already replaced these with
|
|
1842
|
+
// a newer query — clobbering them would kill the new query.
|
|
1843
|
+
if (session.queryInstance === myQueryInstance) {
|
|
1781
1844
|
try {
|
|
1782
1845
|
if (typeof session.queryInstance.close === "function") {
|
|
1783
1846
|
session.queryInstance.close();
|
|
1784
1847
|
}
|
|
1785
1848
|
} catch (e) {}
|
|
1849
|
+
session.queryInstance = null;
|
|
1786
1850
|
}
|
|
1787
|
-
session.
|
|
1788
|
-
session.
|
|
1789
|
-
session.abortController = null;
|
|
1851
|
+
if (session.messageQueue === myMessageQueue) session.messageQueue = null;
|
|
1852
|
+
if (session.abortController === myAbortController) session.abortController = null;
|
|
1790
1853
|
session.taskStopRequested = false;
|
|
1791
1854
|
session.pendingPermissions = {};
|
|
1792
1855
|
session.pendingAskUser = {};
|
|
1793
1856
|
session.pendingElicitations = {};
|
|
1794
1857
|
|
|
1795
1858
|
// Auto-continue on rate limit (scheduler sessions, or user setting)
|
|
1859
|
+
// Mark session as done processing so the late rate_limit_event handler
|
|
1860
|
+
// can detect the race condition and schedule auto-continue itself.
|
|
1861
|
+
session.isProcessing = false;
|
|
1862
|
+
|
|
1796
1863
|
var didScheduleAutoContinue = false;
|
|
1797
1864
|
var acEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
|
|
1798
1865
|
if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
|
|
@@ -1805,6 +1872,10 @@ function createSDKBridge(opts) {
|
|
|
1805
1872
|
if (typeof opts.scheduleMessage === "function") {
|
|
1806
1873
|
opts.scheduleMessage(session, "continue", acResetsAt);
|
|
1807
1874
|
}
|
|
1875
|
+
} else if (acEnabled && !session.destroying) {
|
|
1876
|
+
// Log why auto-continue was not scheduled (for debugging)
|
|
1877
|
+
console.log("[sdk-bridge] Query done, auto-continue enabled but not scheduled: rateLimitResetsAt=" +
|
|
1878
|
+
session.rateLimitResetsAt + " (will rely on late rate_limit_event handler)");
|
|
1808
1879
|
}
|
|
1809
1880
|
|
|
1810
1881
|
// Ralph Loop: notify completion so loop orchestrator can proceed
|
|
@@ -1911,6 +1982,7 @@ function createSDKBridge(opts) {
|
|
|
1911
1982
|
abortController: session.abortController,
|
|
1912
1983
|
promptSuggestions: true,
|
|
1913
1984
|
agentProgressSummaries: true,
|
|
1985
|
+
mcpServers: mcpServers || undefined,
|
|
1914
1986
|
canUseTool: function(toolName, input, toolOpts) {
|
|
1915
1987
|
return handleCanUseTool(session, toolName, input, toolOpts);
|
|
1916
1988
|
},
|
|
@@ -1952,6 +2024,8 @@ function createSDKBridge(opts) {
|
|
|
1952
2024
|
if (session.lastRewindUuid) {
|
|
1953
2025
|
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
1954
2026
|
delete session.lastRewindUuid;
|
|
2027
|
+
// Persist the deletion so server restarts don't re-use a stale UUID
|
|
2028
|
+
sm.saveSessionFile(session);
|
|
1955
2029
|
}
|
|
1956
2030
|
}
|
|
1957
2031
|
|
package/lib/sdk-worker.js
CHANGED
|
@@ -351,7 +351,19 @@ async function handleQueryStart(msg) {
|
|
|
351
351
|
}
|
|
352
352
|
sendToDaemon({ type: "sdk_event", event: event });
|
|
353
353
|
}
|
|
354
|
-
perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "),
|
|
354
|
+
perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), fetching context usage");
|
|
355
|
+
// Fetch context usage breakdown before queryInstance is cleared
|
|
356
|
+
try {
|
|
357
|
+
if (queryInstance && typeof queryInstance.getContextUsage === "function") {
|
|
358
|
+
var ctxUsage = await queryInstance.getContextUsage();
|
|
359
|
+
sendToDaemon({ type: "context_usage", data: ctxUsage });
|
|
360
|
+
perf("context usage sent");
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Non-fatal: SDK may have already shut down
|
|
364
|
+
console.error("[sdk-worker] getContextUsage failed (non-fatal):", e.message);
|
|
365
|
+
}
|
|
366
|
+
perf("sending query_done");
|
|
355
367
|
sendToDaemon({ type: "query_done" });
|
|
356
368
|
} catch (err) {
|
|
357
369
|
var errMsg = err.message || String(err);
|
package/lib/server.js
CHANGED
|
@@ -38,6 +38,24 @@ function httpGet(url) {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function httpGetBinary(url) {
|
|
42
|
+
return new Promise(function (resolve, reject) {
|
|
43
|
+
var mod = url.startsWith("https") ? https : http;
|
|
44
|
+
mod.get(url, { headers: { "User-Agent": "Clay/1.0" } }, function (resp) {
|
|
45
|
+
if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) {
|
|
46
|
+
return httpGetBinary(resp.headers.location).then(resolve, reject);
|
|
47
|
+
}
|
|
48
|
+
if (resp.statusCode !== 200) {
|
|
49
|
+
return reject(new Error("HTTP " + resp.statusCode));
|
|
50
|
+
}
|
|
51
|
+
var chunks = [];
|
|
52
|
+
resp.on("data", function (c) { chunks.push(c); });
|
|
53
|
+
resp.on("end", function () { resolve(Buffer.concat(chunks)); });
|
|
54
|
+
resp.on("error", reject);
|
|
55
|
+
}).on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
function fetchSkillsPage(url) {
|
|
42
60
|
return httpGet(url).then(function (html) {
|
|
43
61
|
// Data is inside self.__next_f.push() with escaped quotes: \"initialSkills\":[{\"source\":...}]
|
|
@@ -1018,6 +1036,28 @@ function createServer(opts) {
|
|
|
1018
1036
|
return;
|
|
1019
1037
|
}
|
|
1020
1038
|
|
|
1039
|
+
// Chrome extension download (proxy from GitHub)
|
|
1040
|
+
if (fullUrl === "/api/extension/download" && req.method === "GET") {
|
|
1041
|
+
if (!isRequestAuthed(req)) {
|
|
1042
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1043
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
var archiveUrl = "https://github.com/chadbyte/clay-chrome/archive/refs/heads/main.zip";
|
|
1047
|
+
httpGetBinary(archiveUrl).then(function (buf) {
|
|
1048
|
+
res.writeHead(200, {
|
|
1049
|
+
"Content-Type": "application/zip",
|
|
1050
|
+
"Content-Disposition": 'attachment; filename="clay-chrome-extension.zip"',
|
|
1051
|
+
"Content-Length": buf.length,
|
|
1052
|
+
});
|
|
1053
|
+
res.end(buf);
|
|
1054
|
+
}).catch(function (err) {
|
|
1055
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
1056
|
+
res.end(JSON.stringify({ error: "Failed to download extension: " + (err.message || "unknown error") }));
|
|
1057
|
+
});
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1021
1061
|
// CORS preflight for cross-origin requests (HTTP onboarding → HTTPS)
|
|
1022
1062
|
if (req.method === "OPTIONS") {
|
|
1023
1063
|
res.writeHead(204, {
|
|
@@ -2889,6 +2929,8 @@ function createServer(opts) {
|
|
|
2889
2929
|
osUsers: osUsers,
|
|
2890
2930
|
currentVersion: currentVersion,
|
|
2891
2931
|
lanHost: lanHost,
|
|
2932
|
+
port: portNum,
|
|
2933
|
+
tls: !!tlsOptions,
|
|
2892
2934
|
getProjectCount: function () { return projects.size; },
|
|
2893
2935
|
getProjectList: function (userId) {
|
|
2894
2936
|
var list = [];
|
package/lib/sessions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
3
4
|
var config = require("./config");
|
|
4
5
|
var utils = require("./utils");
|
|
5
6
|
var users = require("./users");
|
|
@@ -75,7 +76,9 @@ function createSessionManager(opts) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function saveSessionFile(session) {
|
|
78
|
-
if (!session.cliSessionId)
|
|
79
|
+
if (!session.cliSessionId) {
|
|
80
|
+
session.cliSessionId = crypto.randomUUID();
|
|
81
|
+
}
|
|
79
82
|
try {
|
|
80
83
|
var metaObj = {
|
|
81
84
|
type: "meta",
|
|
@@ -96,17 +99,24 @@ function createSessionManager(opts) {
|
|
|
96
99
|
lines.push(JSON.stringify(session.history[i]));
|
|
97
100
|
}
|
|
98
101
|
var sfPath = sessionFilePath(session.cliSessionId);
|
|
99
|
-
|
|
102
|
+
// Atomic write: write to temp file then rename, so a crash mid-write
|
|
103
|
+
// cannot leave a truncated/corrupted session file.
|
|
104
|
+
var tmpPath = sfPath + ".tmp." + process.pid;
|
|
105
|
+
fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
|
|
100
106
|
if (process.platform !== "win32") {
|
|
101
|
-
try { fs.chmodSync(
|
|
107
|
+
try { fs.chmodSync(tmpPath, 0o600); } catch (chmodErr) {}
|
|
102
108
|
}
|
|
109
|
+
fs.renameSync(tmpPath, sfPath);
|
|
103
110
|
} catch(e) {
|
|
104
111
|
console.error("[session] Failed to save session file:", e.message);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
function appendToSessionFile(session, obj) {
|
|
109
|
-
if (!session.cliSessionId)
|
|
116
|
+
if (!session.cliSessionId) {
|
|
117
|
+
session.cliSessionId = crypto.randomUUID();
|
|
118
|
+
saveSessionFile(session);
|
|
119
|
+
}
|
|
110
120
|
session.lastActivity = Date.now();
|
|
111
121
|
try {
|
|
112
122
|
var afPath = sessionFilePath(session.cliSessionId);
|
|
@@ -123,6 +133,13 @@ function createSessionManager(opts) {
|
|
|
123
133
|
var files;
|
|
124
134
|
try { files = fs.readdirSync(sessionsDir); } catch { return; }
|
|
125
135
|
|
|
136
|
+
// Clean up stale temp files from interrupted atomic writes
|
|
137
|
+
for (var ti = 0; ti < files.length; ti++) {
|
|
138
|
+
if (files[ti].indexOf(".tmp.") !== -1) {
|
|
139
|
+
try { fs.unlinkSync(path.join(sessionsDir, files[ti])); } catch (e) {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
var loaded = [];
|
|
127
144
|
for (var i = 0; i < files.length; i++) {
|
|
128
145
|
if (!files[i].endsWith(".jsonl")) continue;
|
|
@@ -226,10 +243,8 @@ function createSessionManager(opts) {
|
|
|
226
243
|
return [...sessions.values()].filter(function (s) {
|
|
227
244
|
if (s.hidden) return false;
|
|
228
245
|
if (!multiUser) {
|
|
229
|
-
// Single-user mode: only show sessions without ownerId
|
|
230
246
|
return !s.ownerId;
|
|
231
247
|
}
|
|
232
|
-
// Multi-user mode: include all sessions (per-user filtering done by canAccessSession)
|
|
233
248
|
return true;
|
|
234
249
|
});
|
|
235
250
|
}
|
|
@@ -355,7 +370,7 @@ function createSessionManager(opts) {
|
|
|
355
370
|
}
|
|
356
371
|
}
|
|
357
372
|
|
|
358
|
-
_send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens });
|
|
373
|
+
_send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, contextUsage: session.lastContextUsage || null });
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
function switchSession(localId, targetWs, transform) {
|
|
@@ -481,7 +496,23 @@ function createSessionManager(opts) {
|
|
|
481
496
|
sessions.delete(localId);
|
|
482
497
|
}
|
|
483
498
|
|
|
499
|
+
function doSendToSession(session, obj) {
|
|
500
|
+
// Send to active clients without recording to history/disk (ephemeral data)
|
|
501
|
+
if (sendEach) {
|
|
502
|
+
var data = JSON.stringify(obj);
|
|
503
|
+
sendEach(function (ws) {
|
|
504
|
+
if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
|
|
505
|
+
ws.send(data);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
} else if (session.localId === activeSessionId) {
|
|
509
|
+
send(obj);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
484
513
|
function doSendAndRecord(session, obj) {
|
|
514
|
+
// Stamp every recorded message so history replay preserves original times
|
|
515
|
+
if (!obj._ts) obj._ts = Date.now();
|
|
485
516
|
session.history.push(obj);
|
|
486
517
|
appendToSessionFile(session, obj);
|
|
487
518
|
if (sendEach) {
|
|
@@ -726,6 +757,7 @@ function createSessionManager(opts) {
|
|
|
726
757
|
saveSessionFile: saveSessionFile,
|
|
727
758
|
appendToSessionFile: appendToSessionFile,
|
|
728
759
|
sendAndRecord: doSendAndRecord,
|
|
760
|
+
sendToSession: doSendToSession,
|
|
729
761
|
findTurnBoundary: findTurnBoundary,
|
|
730
762
|
replayHistory: replayHistory,
|
|
731
763
|
searchSessions: searchSessions,
|
package/lib/terminal-manager.js
CHANGED
|
@@ -16,7 +16,7 @@ function createTerminalManager(opts) {
|
|
|
16
16
|
var nextId = 1;
|
|
17
17
|
var terminals = new Map(); // id -> terminal session
|
|
18
18
|
|
|
19
|
-
function create(cols, rows, osUserInfo) {
|
|
19
|
+
function create(cols, rows, osUserInfo, ownerWs) {
|
|
20
20
|
if (terminals.size >= MAX_TERMINALS) return null;
|
|
21
21
|
|
|
22
22
|
var pty = createTerminal(cwd, cols, rows, osUserInfo);
|
|
@@ -28,20 +28,24 @@ function createTerminalManager(opts) {
|
|
|
28
28
|
pty: pty,
|
|
29
29
|
scrollback: [],
|
|
30
30
|
scrollbackSize: 0,
|
|
31
|
+
totalBytesWritten: 0,
|
|
31
32
|
cols: cols || 80,
|
|
32
33
|
rows: rows || 24,
|
|
33
34
|
title: "Terminal " + id,
|
|
34
35
|
exited: false,
|
|
35
36
|
exitCode: null,
|
|
36
37
|
subscribers: new Set(),
|
|
38
|
+
ownerWs: ownerWs || null,
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
pty.onData(function (data) {
|
|
40
|
-
// Buffer scrollback
|
|
41
|
-
|
|
42
|
+
// Buffer scrollback with timestamps
|
|
43
|
+
var ts = Date.now();
|
|
44
|
+
session.scrollback.push({ ts: ts, data: data });
|
|
42
45
|
session.scrollbackSize += data.length;
|
|
46
|
+
session.totalBytesWritten += data.length;
|
|
43
47
|
while (session.scrollbackSize > SCROLLBACK_MAX && session.scrollback.length > 1) {
|
|
44
|
-
session.scrollbackSize -= session.scrollback[0].length;
|
|
48
|
+
session.scrollbackSize -= session.scrollback[0].data.length;
|
|
45
49
|
session.scrollback.shift();
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -80,10 +84,15 @@ function createTerminalManager(opts) {
|
|
|
80
84
|
|
|
81
85
|
// Replay scrollback only for newly attached clients
|
|
82
86
|
if (!alreadySubscribed && session.scrollback.length > 0) {
|
|
83
|
-
var replay = session.scrollback.join("");
|
|
87
|
+
var replay = session.scrollback.map(function(c) { return c.data; }).join("");
|
|
84
88
|
sendTo(ws, { type: "term_output", id: id, data: replay });
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
// Send current terminal dimensions so the client renders at the correct size
|
|
92
|
+
if (!alreadySubscribed && session.cols && session.rows) {
|
|
93
|
+
sendTo(ws, { type: "term_resized", id: id, cols: session.cols, rows: session.rows });
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
// If already exited, notify
|
|
88
97
|
if (session.exited) {
|
|
89
98
|
sendTo(ws, { type: "term_exited", id: id });
|
|
@@ -111,14 +120,22 @@ function createTerminalManager(opts) {
|
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
function resize(id, cols, rows) {
|
|
123
|
+
function resize(id, cols, rows, sourceWs) {
|
|
115
124
|
var session = terminals.get(id);
|
|
116
125
|
if (!session || !session.pty) return;
|
|
126
|
+
// Only the terminal owner can resize the PTY.
|
|
127
|
+
// Observers resizing would cause SIGWINCH and flood the owner with escape sequences.
|
|
128
|
+
if (session.ownerWs && sourceWs && sourceWs !== session.ownerWs) return;
|
|
117
129
|
if (cols > 0 && rows > 0) {
|
|
118
130
|
try {
|
|
119
131
|
session.pty.resize(cols, rows);
|
|
120
132
|
session.cols = cols;
|
|
121
133
|
session.rows = rows;
|
|
134
|
+
// Notify other subscribers about the resize so their xterm stays in sync
|
|
135
|
+
var msg = JSON.stringify({ type: "term_resized", id: id, cols: cols, rows: rows });
|
|
136
|
+
for (var ws of session.subscribers) {
|
|
137
|
+
if (ws.readyState === 1 && ws !== sourceWs) ws.send(msg);
|
|
138
|
+
}
|
|
122
139
|
} catch (e) {}
|
|
123
140
|
}
|
|
124
141
|
}
|
|
@@ -162,6 +179,18 @@ function createTerminalManager(opts) {
|
|
|
162
179
|
return result;
|
|
163
180
|
}
|
|
164
181
|
|
|
182
|
+
function getScrollback(id) {
|
|
183
|
+
var session = terminals.get(id);
|
|
184
|
+
if (!session) return null;
|
|
185
|
+
var content = session.scrollback.map(function(c) { return c.data; }).join("");
|
|
186
|
+
return {
|
|
187
|
+
content: content,
|
|
188
|
+
chunks: session.scrollback,
|
|
189
|
+
totalBytesWritten: session.totalBytesWritten,
|
|
190
|
+
bufferStart: session.totalBytesWritten - content.length
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
165
194
|
function destroyAll() {
|
|
166
195
|
for (var session of terminals.values()) {
|
|
167
196
|
if (session.pty) {
|
|
@@ -182,6 +211,7 @@ function createTerminalManager(opts) {
|
|
|
182
211
|
close: close,
|
|
183
212
|
rename: rename,
|
|
184
213
|
list: list,
|
|
214
|
+
getScrollback: getScrollback,
|
|
185
215
|
destroyAll: destroyAll,
|
|
186
216
|
};
|
|
187
217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "2.26.0-beta.
|
|
3
|
+
"version": "2.26.0-beta.10",
|
|
4
4
|
"description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clay-server": "./bin/cli.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"homepage": "https://github.com/chadbyte/claude-relay#readme",
|
|
37
37
|
"author": "Chad",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
39
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
40
40
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
41
41
|
"nodemailer": "^6.10.1",
|
|
42
42
|
"qrcode-terminal": "^0.12.0",
|