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.
@@ -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 };
@@ -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
- ctx.sendToSession(debateSession.localId, {
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
- ctx.sendToSession(debateSession.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: mateId, mateName: profile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
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 };