clay-server 2.26.1-beta.1 → 2.27.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/lib/project-connection.js +259 -0
- package/lib/project-debate.js +25 -12
- package/lib/project-file-watch.js +120 -0
- package/lib/project-filesystem.js +482 -0
- package/lib/project-http.js +685 -0
- package/lib/project-image.js +94 -0
- package/lib/project-knowledge.js +161 -0
- package/lib/project-loop.js +1160 -0
- package/lib/project-sessions.js +1152 -0
- package/lib/project-user-message.js +631 -0
- package/lib/project.js +358 -4438
- package/lib/public/app.js +12 -5
- package/lib/public/modules/debate.js +21 -4
- package/lib/sdk-bridge.js +4 -3
- package/lib/server.js +34 -0
- package/package.json +1 -1
|
@@ -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 };
|
package/lib/project-debate.js
CHANGED
|
@@ -299,6 +299,10 @@ function attachDebate(ctx) {
|
|
|
299
299
|
}),
|
|
300
300
|
};
|
|
301
301
|
ctx.sendToSession(session.localId, briefReadyMsg);
|
|
302
|
+
// Also send to setup session if client switched there (quickStart flow)
|
|
303
|
+
if (debate.setupSessionId && debate.setupSessionId !== session.localId) {
|
|
304
|
+
ctx.sendToSession(debate.setupSessionId, briefReadyMsg);
|
|
305
|
+
}
|
|
302
306
|
} else {
|
|
303
307
|
console.log("[debate] Brief picked up, transitioning to live. Topic:", debate.topic);
|
|
304
308
|
// Transition to live (standard flow via modal/skill)
|
|
@@ -530,6 +534,8 @@ function attachDebate(ctx) {
|
|
|
530
534
|
ctx.sm.switchSession(setupSession.localId, null, ctx.hydrateImageRefs);
|
|
531
535
|
debate.setupSessionId = setupSession.localId;
|
|
532
536
|
debate.setupStartedAt = setupSession.loop.startedAt;
|
|
537
|
+
// Share debate state with setup session so confirm_brief works from either
|
|
538
|
+
setupSession._debate = debate;
|
|
533
539
|
|
|
534
540
|
// Build DM conversation context for the moderator
|
|
535
541
|
var dmContext = msg.dmContext || "";
|
|
@@ -715,6 +721,13 @@ function attachDebate(ctx) {
|
|
|
715
721
|
ctx.send({ type: "mention_processing", mateId: mateId, active: active });
|
|
716
722
|
}
|
|
717
723
|
|
|
724
|
+
// Persist a debate message to session history and send to clients
|
|
725
|
+
function debateSendAndRecord(session, msg) {
|
|
726
|
+
session.history.push(msg);
|
|
727
|
+
ctx.sm.appendToSessionFile(session, msg);
|
|
728
|
+
ctx.sendToSession(session.localId, msg);
|
|
729
|
+
}
|
|
730
|
+
|
|
718
731
|
// --- Live debate ---
|
|
719
732
|
|
|
720
733
|
function startDebateLive(session) {
|
|
@@ -765,7 +778,7 @@ function attachDebate(ctx) {
|
|
|
765
778
|
|
|
766
779
|
// Signal moderator's first turn
|
|
767
780
|
debateMateProcessing(debate.moderatorId, true);
|
|
768
|
-
|
|
781
|
+
debateSendAndRecord(debateSession, {
|
|
769
782
|
type: "debate_turn",
|
|
770
783
|
mateId: debate.moderatorId,
|
|
771
784
|
mateName: moderatorProfile.name,
|
|
@@ -792,7 +805,7 @@ function attachDebate(ctx) {
|
|
|
792
805
|
},
|
|
793
806
|
onDelta: function (delta) {
|
|
794
807
|
if (debateSession._debate && debateSession._debate.phase !== "ended") {
|
|
795
|
-
|
|
808
|
+
debateSendAndRecord(debateSession, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
796
809
|
}
|
|
797
810
|
},
|
|
798
811
|
onDone: function (fullText) {
|
|
@@ -900,7 +913,7 @@ function attachDebate(ctx) {
|
|
|
900
913
|
|
|
901
914
|
// Notify clients of new turn
|
|
902
915
|
debateMateProcessing(mateId, true);
|
|
903
|
-
|
|
916
|
+
debateSendAndRecord(session, {
|
|
904
917
|
type: "debate_turn",
|
|
905
918
|
mateId: mateId,
|
|
906
919
|
mateName: profile.name,
|
|
@@ -920,7 +933,7 @@ function attachDebate(ctx) {
|
|
|
920
933
|
onDelta: function (delta) {
|
|
921
934
|
if (session._debate && session._debate.phase !== "ended") {
|
|
922
935
|
debate._currentTurnText += delta;
|
|
923
|
-
|
|
936
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: mateId, mateName: profile.name, delta: delta });
|
|
924
937
|
}
|
|
925
938
|
},
|
|
926
939
|
onDone: function (fullText) {
|
|
@@ -1065,7 +1078,7 @@ function attachDebate(ctx) {
|
|
|
1065
1078
|
|
|
1066
1079
|
// Notify clients of moderator turn
|
|
1067
1080
|
debateMateProcessing(debate.moderatorId, true);
|
|
1068
|
-
|
|
1081
|
+
debateSendAndRecord(session, {
|
|
1069
1082
|
type: "debate_turn",
|
|
1070
1083
|
mateId: debate.moderatorId,
|
|
1071
1084
|
mateName: moderatorProfile.name,
|
|
@@ -1095,7 +1108,7 @@ function attachDebate(ctx) {
|
|
|
1095
1108
|
},
|
|
1096
1109
|
onDelta: function (delta) {
|
|
1097
1110
|
if (session._debate && session._debate.phase !== "ended") {
|
|
1098
|
-
|
|
1111
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1099
1112
|
}
|
|
1100
1113
|
},
|
|
1101
1114
|
onDone: function (fullText) {
|
|
@@ -1162,7 +1175,7 @@ function attachDebate(ctx) {
|
|
|
1162
1175
|
debate.turnInProgress = true;
|
|
1163
1176
|
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1164
1177
|
|
|
1165
|
-
|
|
1178
|
+
debateSendAndRecord(session, {
|
|
1166
1179
|
type: "debate_turn",
|
|
1167
1180
|
mateId: debate.moderatorId,
|
|
1168
1181
|
mateName: moderatorProfile.name,
|
|
@@ -1201,7 +1214,7 @@ function attachDebate(ctx) {
|
|
|
1201
1214
|
debate.turnInProgress = true;
|
|
1202
1215
|
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1203
1216
|
|
|
1204
|
-
|
|
1217
|
+
debateSendAndRecord(session, {
|
|
1205
1218
|
type: "debate_turn",
|
|
1206
1219
|
mateId: debate.moderatorId,
|
|
1207
1220
|
mateName: moderatorProfile.name,
|
|
@@ -1232,7 +1245,7 @@ function attachDebate(ctx) {
|
|
|
1232
1245
|
},
|
|
1233
1246
|
onDelta: function (delta) {
|
|
1234
1247
|
if (session._debate && session._debate.phase !== "ended") {
|
|
1235
|
-
|
|
1248
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1236
1249
|
}
|
|
1237
1250
|
},
|
|
1238
1251
|
onDone: function (fullText) {
|
|
@@ -1281,7 +1294,7 @@ function attachDebate(ctx) {
|
|
|
1281
1294
|
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1282
1295
|
|
|
1283
1296
|
debateMateProcessing(debate.moderatorId, true);
|
|
1284
|
-
|
|
1297
|
+
debateSendAndRecord(session, {
|
|
1285
1298
|
type: "debate_turn",
|
|
1286
1299
|
mateId: debate.moderatorId,
|
|
1287
1300
|
mateName: moderatorProfile.name,
|
|
@@ -1471,7 +1484,7 @@ function attachDebate(ctx) {
|
|
|
1471
1484
|
|
|
1472
1485
|
debate.turnInProgress = true;
|
|
1473
1486
|
debateMateProcessing(debate.moderatorId, true);
|
|
1474
|
-
|
|
1487
|
+
debateSendAndRecord(session, {
|
|
1475
1488
|
type: "debate_turn",
|
|
1476
1489
|
mateId: debate.moderatorId,
|
|
1477
1490
|
mateName: moderatorProfile.name,
|
|
@@ -1519,7 +1532,7 @@ function attachDebate(ctx) {
|
|
|
1519
1532
|
},
|
|
1520
1533
|
onDelta: function (delta) {
|
|
1521
1534
|
if (session._debate && session._debate.phase !== "ended") {
|
|
1522
|
-
|
|
1535
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1523
1536
|
}
|
|
1524
1537
|
},
|
|
1525
1538
|
onDone: function (fullText) {
|
|
@@ -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 };
|