clay-server 2.26.0 → 2.27.0-beta.1

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.
@@ -0,0 +1,259 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var usersModule = require("./users");
4
+ var userPresence = require("./user-presence");
5
+
6
+ /**
7
+ * Attach connection/disconnection handlers to a project context.
8
+ *
9
+ * ctx fields:
10
+ * cwd, slug, isMate, osUsers, debug, dangerouslySkipPermissions,
11
+ * currentVersion, lanHost, sm, tm, nm, clients, send, sendTo,
12
+ * opts, loopState, loopRegistry, _loop, pushModule,
13
+ * hydrateImageRefs, broadcastClientCount, broadcastPresence,
14
+ * getProjectList, getHubSchedules, loadContextSources,
15
+ * restoreDebateState, handleMessage, handleDisconnection,
16
+ * stopFileWatch, stopAllDirWatches,
17
+ * getProjectOwnerId, setProjectOwnerId, getLatestVersion,
18
+ * getTitle, getProject
19
+ */
20
+ function attachConnection(ctx) {
21
+ var cwd = ctx.cwd;
22
+ var slug = ctx.slug;
23
+ var isMate = ctx.isMate;
24
+ var osUsers = ctx.osUsers;
25
+ var debug = ctx.debug;
26
+ var dangerouslySkipPermissions = ctx.dangerouslySkipPermissions;
27
+ var currentVersion = ctx.currentVersion;
28
+ var lanHost = ctx.lanHost;
29
+ var sm = ctx.sm;
30
+ var tm = ctx.tm;
31
+ var nm = ctx.nm;
32
+ var clients = ctx.clients;
33
+ var send = ctx.send;
34
+ var sendTo = ctx.sendTo;
35
+ var opts = ctx.opts;
36
+ var _loop = ctx._loop;
37
+ var hydrateImageRefs = ctx.hydrateImageRefs;
38
+ var broadcastClientCount = ctx.broadcastClientCount;
39
+ var broadcastPresence = ctx.broadcastPresence;
40
+ var getProjectList = ctx.getProjectList;
41
+ var getHubSchedules = ctx.getHubSchedules;
42
+ var loadContextSources = ctx.loadContextSources;
43
+ var restoreDebateState = ctx.restoreDebateState;
44
+ var stopFileWatch = ctx.stopFileWatch;
45
+ var stopAllDirWatches = ctx.stopAllDirWatches;
46
+ var getProjectOwnerId = ctx.getProjectOwnerId;
47
+ var setProjectOwnerId = ctx.setProjectOwnerId;
48
+ var getLatestVersion = ctx.getLatestVersion;
49
+ var getTitle = ctx.getTitle;
50
+ var getProject = ctx.getProject;
51
+
52
+ function handleConnection(ws, wsUser, handleMessage, handleDisconnection) {
53
+ ws._clayUser = wsUser || null;
54
+ clients.add(ws);
55
+ broadcastClientCount();
56
+
57
+ var loopState = _loop.loopState;
58
+ var loopRegistry = _loop.loopRegistry;
59
+
60
+ // Resume loop if server restarted mid-execution (deferred so client gets initial state first)
61
+ if (loopState._needsResume) {
62
+ delete loopState._needsResume;
63
+ setTimeout(function() { _loop.resumeLoop(); }, 500);
64
+ }
65
+
66
+ // Auto-assign owner if project has none and a user connects (e.g. IPC-added projects)
67
+ var projectOwnerId = getProjectOwnerId();
68
+ if (!projectOwnerId && ws._clayUser && ws._clayUser.id && !isMate) {
69
+ setProjectOwnerId(ws._clayUser.id);
70
+ projectOwnerId = ws._clayUser.id;
71
+ if (opts.onProjectOwnerChanged) {
72
+ opts.onProjectOwnerChanged(slug, projectOwnerId);
73
+ }
74
+ console.log("[project] Auto-assigned owner for " + slug + ": " + projectOwnerId);
75
+ }
76
+
77
+ // Send cached state
78
+ var _userId = ws._clayUser ? ws._clayUser.id : null;
79
+ var _filteredProjects = getProjectList(_userId);
80
+ var title = getTitle();
81
+ var project = getProject();
82
+ sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId });
83
+ var latestVersion = getLatestVersion();
84
+ if (latestVersion && ws._clayUser && ws._clayUser.role === "admin") {
85
+ sendTo(ws, { type: "update_available", version: latestVersion });
86
+ }
87
+ if (sm.slashCommands) {
88
+ sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
89
+ }
90
+ if (sm.currentModel) {
91
+ sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
92
+ }
93
+ sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
94
+ sendTo(ws, { type: "term_list", terminals: tm.list() });
95
+ var restoredSources = loadContextSources(slug);
96
+ sendTo(ws, { type: "context_sources_state", active: restoredSources });
97
+ sendTo(ws, { type: "notes_list", notes: nm.list() });
98
+ sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
99
+ _loop.sendConnectionState(ws);
100
+
101
+ // Session list (filtered for access control)
102
+ var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
103
+ if (usersModule.isMultiUser() && wsUser) {
104
+ allSessions = allSessions.filter(function (s) {
105
+ return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
106
+ });
107
+ } else if (!usersModule.isMultiUser()) {
108
+ allSessions = allSessions.filter(function (s) { return !s.ownerId; });
109
+ }
110
+ sendTo(ws, {
111
+ type: "session_list",
112
+ sessions: allSessions.map(function (s) {
113
+ var loop = s.loop ? Object.assign({}, s.loop) : null;
114
+ if (loop && loop.loopId && loopRegistry) {
115
+ var rec = loopRegistry.getById(loop.loopId);
116
+ if (rec) {
117
+ if (rec.name) loop.name = rec.name;
118
+ if (rec.source) loop.source = rec.source;
119
+ }
120
+ }
121
+ return {
122
+ id: s.localId,
123
+ cliSessionId: s.cliSessionId || null,
124
+ title: s.title || "New Session",
125
+ active: s.localId === sm.activeSessionId,
126
+ isProcessing: s.isProcessing,
127
+ lastActivity: s.lastActivity || s.createdAt || 0,
128
+ loop: loop,
129
+ ownerId: s.ownerId || null,
130
+ sessionVisibility: s.sessionVisibility || "shared",
131
+ };
132
+ }),
133
+ });
134
+
135
+ // Restore active session for this client from server-side presence
136
+ var active = null;
137
+ var presenceKey = wsUser ? wsUser.id : "_default";
138
+ var storedPresence = userPresence.getPresence(slug, presenceKey);
139
+ if (storedPresence && storedPresence.sessionId) {
140
+ if (sm.sessions.has(storedPresence.sessionId)) {
141
+ active = sm.sessions.get(storedPresence.sessionId);
142
+ } else {
143
+ sm.sessions.forEach(function (s) {
144
+ if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
145
+ });
146
+ }
147
+ if (active && usersModule.isMultiUser() && wsUser) {
148
+ if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
149
+ } else if (active && !usersModule.isMultiUser() && active.ownerId) {
150
+ active = null;
151
+ }
152
+ }
153
+ if (!active && allSessions.length > 0) {
154
+ active = allSessions[0];
155
+ for (var fi = 1; fi < allSessions.length; fi++) {
156
+ if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
157
+ active = allSessions[fi];
158
+ }
159
+ }
160
+ }
161
+ var autoCreated = false;
162
+ if (!active) {
163
+ var autoOpts = {};
164
+ if (wsUser && usersModule.isMultiUser()) autoOpts.ownerId = wsUser.id;
165
+ active = sm.createSession(autoOpts, ws);
166
+ autoCreated = true;
167
+ }
168
+ if (active && !autoCreated) {
169
+ if (!active.ownerId && wsUser && usersModule.isMultiUser()) {
170
+ active.ownerId = wsUser.id;
171
+ sm.saveSessionFile(active);
172
+ }
173
+ ws._clayActiveSession = active.localId;
174
+ sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null });
175
+
176
+ var total = active.history.length;
177
+ var fromIndex = 0;
178
+ if (total > sm.HISTORY_PAGE_SIZE) {
179
+ fromIndex = sm.findTurnBoundary(active.history, Math.max(0, total - sm.HISTORY_PAGE_SIZE));
180
+ }
181
+ sendTo(ws, { type: "history_meta", total: total, from: fromIndex });
182
+ for (var i = fromIndex; i < total; i++) {
183
+ sendTo(ws, hydrateImageRefs(active.history[i]));
184
+ }
185
+ var _lastUsage = null, _lastModelUsage = null, _lastCost = null, _lastStreamInputTokens = null;
186
+ for (var _ri = total - 1; _ri >= 0; _ri--) {
187
+ if (active.history[_ri].type === "result") {
188
+ var _r = active.history[_ri];
189
+ _lastUsage = _r.usage || null;
190
+ _lastModelUsage = _r.modelUsage || null;
191
+ _lastCost = _r.cost != null ? _r.cost : null;
192
+ _lastStreamInputTokens = _r.lastStreamInputTokens || null;
193
+ break;
194
+ }
195
+ }
196
+ sendTo(ws, { type: "history_done", lastUsage: _lastUsage, lastModelUsage: _lastModelUsage, lastCost: _lastCost, lastStreamInputTokens: _lastStreamInputTokens, contextUsage: active.lastContextUsage || null });
197
+
198
+ if (active.isProcessing) {
199
+ sendTo(ws, { type: "status", status: "processing" });
200
+ }
201
+ var pendingIds = Object.keys(active.pendingPermissions);
202
+ for (var pi = 0; pi < pendingIds.length; pi++) {
203
+ var p = active.pendingPermissions[pendingIds[pi]];
204
+ sendTo(ws, {
205
+ type: "permission_request_pending",
206
+ requestId: p.requestId,
207
+ toolName: p.toolName,
208
+ toolInput: p.toolInput,
209
+ toolUseId: p.toolUseId,
210
+ decisionReason: p.decisionReason,
211
+ mateId: p.mateId || undefined,
212
+ });
213
+ }
214
+ }
215
+
216
+ if (active) {
217
+ userPresence.setPresence(slug, presenceKey, active.localId, storedPresence ? storedPresence.mateDm : null);
218
+ }
219
+ if (storedPresence && storedPresence.mateDm && !isMate) {
220
+ sendTo(ws, { type: "restore_mate_dm", mateId: storedPresence.mateDm });
221
+ }
222
+
223
+ broadcastPresence();
224
+ restoreDebateState(ws);
225
+
226
+ ws.on("message", function (raw) {
227
+ var msg;
228
+ try { msg = JSON.parse(raw.toString()); } catch (e) { return; }
229
+ handleMessage(ws, msg);
230
+ });
231
+
232
+ ws.on("close", function () {
233
+ handleDisconnection(ws);
234
+ });
235
+ }
236
+
237
+ function handleDisconnection(ws) {
238
+ if (ws._clayActiveSession) {
239
+ var dcPresKey = ws._clayUser ? ws._clayUser.id : "_default";
240
+ var dcExisting = userPresence.getPresence(slug, dcPresKey);
241
+ userPresence.setPresence(slug, dcPresKey, ws._clayActiveSession, dcExisting ? dcExisting.mateDm : null);
242
+ }
243
+ tm.detachAll(ws);
244
+ clients.delete(ws);
245
+ if (clients.size === 0) {
246
+ stopFileWatch();
247
+ stopAllDirWatches();
248
+ }
249
+ broadcastClientCount();
250
+ broadcastPresence();
251
+ }
252
+
253
+ return {
254
+ handleConnection: handleConnection,
255
+ handleDisconnection: handleDisconnection,
256
+ };
257
+ }
258
+
259
+ module.exports = { attachConnection: attachConnection };
@@ -0,0 +1,120 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+
4
+ /**
5
+ * Attach file/directory watcher engine to a project context.
6
+ *
7
+ * ctx fields:
8
+ * cwd, send, safePath, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS
9
+ */
10
+ function attachFileWatch(ctx) {
11
+ var cwd = ctx.cwd;
12
+ var send = ctx.send;
13
+ var safePath = ctx.safePath;
14
+ var BINARY_EXTS = ctx.BINARY_EXTS;
15
+ var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
16
+ var IGNORED_DIRS = ctx.IGNORED_DIRS;
17
+
18
+ // --- File watcher ---
19
+ var fileWatcher = null;
20
+ var watchedPath = null;
21
+ var watchDebounce = null;
22
+
23
+ function startFileWatch(relPath) {
24
+ var absPath = safePath(cwd, relPath);
25
+ if (!absPath) return;
26
+ if (watchedPath === relPath) return;
27
+ stopFileWatch();
28
+ watchedPath = relPath;
29
+ try {
30
+ fileWatcher = fs.watch(absPath, function () {
31
+ clearTimeout(watchDebounce);
32
+ watchDebounce = setTimeout(function () {
33
+ try {
34
+ var stat = fs.statSync(absPath);
35
+ var ext = path.extname(absPath).toLowerCase();
36
+ if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
37
+ var content = fs.readFileSync(absPath, "utf8");
38
+ send({ type: "fs_file_changed", path: relPath, content: content, size: stat.size });
39
+ } catch (e) {
40
+ stopFileWatch();
41
+ }
42
+ }, 200);
43
+ });
44
+ fileWatcher.on("error", function () { stopFileWatch(); });
45
+ } catch (e) {
46
+ watchedPath = null;
47
+ }
48
+ }
49
+
50
+ function stopFileWatch() {
51
+ if (fileWatcher) {
52
+ try { fileWatcher.close(); } catch (e) {}
53
+ fileWatcher = null;
54
+ }
55
+ clearTimeout(watchDebounce);
56
+ watchDebounce = null;
57
+ watchedPath = null;
58
+ }
59
+
60
+ // --- Directory watcher ---
61
+ var dirWatchers = {}; // relPath -> { watcher, debounce }
62
+
63
+ function startDirWatch(relPath) {
64
+ if (dirWatchers[relPath]) return;
65
+ var absPath = safePath(cwd, relPath);
66
+ if (!absPath) return;
67
+ try {
68
+ var debounce = null;
69
+ var watcher = fs.watch(absPath, function () {
70
+ clearTimeout(debounce);
71
+ debounce = setTimeout(function () {
72
+ // Re-read directory and broadcast to all clients
73
+ try {
74
+ var items = fs.readdirSync(absPath, { withFileTypes: true });
75
+ var entries = [];
76
+ for (var i = 0; i < items.length; i++) {
77
+ if (items[i].isDirectory() && IGNORED_DIRS.has(items[i].name)) continue;
78
+ entries.push({
79
+ name: items[i].name,
80
+ type: items[i].isDirectory() ? "dir" : "file",
81
+ path: path.relative(cwd, path.join(absPath, items[i].name)).split(path.sep).join("/"),
82
+ });
83
+ }
84
+ send({ type: "fs_dir_changed", path: relPath, entries: entries });
85
+ } catch (e) {
86
+ stopDirWatch(relPath);
87
+ }
88
+ }, 300);
89
+ });
90
+ watcher.on("error", function () { stopDirWatch(relPath); });
91
+ dirWatchers[relPath] = { watcher: watcher, debounce: debounce };
92
+ } catch (e) {}
93
+ }
94
+
95
+ function stopDirWatch(relPath) {
96
+ var entry = dirWatchers[relPath];
97
+ if (entry) {
98
+ clearTimeout(entry.debounce);
99
+ try { entry.watcher.close(); } catch (e) {}
100
+ delete dirWatchers[relPath];
101
+ }
102
+ }
103
+
104
+ function stopAllDirWatches() {
105
+ var paths = Object.keys(dirWatchers);
106
+ for (var i = 0; i < paths.length; i++) {
107
+ stopDirWatch(paths[i]);
108
+ }
109
+ }
110
+
111
+ return {
112
+ startFileWatch: startFileWatch,
113
+ stopFileWatch: stopFileWatch,
114
+ startDirWatch: startDirWatch,
115
+ stopDirWatch: stopDirWatch,
116
+ stopAllDirWatches: stopAllDirWatches,
117
+ };
118
+ }
119
+
120
+ module.exports = { attachFileWatch: attachFileWatch };