clay-server 4.0.0-beta.8 → 4.0.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.
Files changed (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -5,7 +5,7 @@ var path = require("path");
5
5
  * Attach file/directory watcher engine to a project context.
6
6
  *
7
7
  * ctx fields:
8
- * cwd, send, sendTo, safePath, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS
8
+ * cwd, send, sendTo, safePath, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS, requestAccess, fsAsUser
9
9
  */
10
10
  function attachFileWatch(ctx) {
11
11
  var cwd = ctx.cwd;
@@ -15,6 +15,19 @@ function attachFileWatch(ctx) {
15
15
  var BINARY_EXTS = ctx.BINARY_EXTS;
16
16
  var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
17
17
  var IGNORED_DIRS = ctx.IGNORED_DIRS;
18
+ var access = ctx.requestAccess;
19
+
20
+ function identityFor(client) {
21
+ if (!access) return null;
22
+ if (!access.canUseFiles(client)) throw new Error("File browser access is not permitted");
23
+ return access.osIdentity(client);
24
+ }
25
+
26
+ function checkedPath(relPath) {
27
+ var resolved = safePath(cwd, relPath);
28
+ if (!resolved) throw new Error("Access denied");
29
+ return resolved;
30
+ }
18
31
 
19
32
  // --- File watcher ---
20
33
  // One open file per websocket client. A project-wide singleton watcher made
@@ -47,18 +60,21 @@ function attachFileWatch(ctx) {
47
60
  }
48
61
  }
49
62
 
50
- function readFileSnapshot(absPath) {
51
- var stat = fs.statSync(absPath);
63
+ function readFileSnapshot(client, relPath) {
64
+ var identity = identityFor(client);
65
+ var absPath = checkedPath(relPath);
66
+ var stat = identity ? ctx.fsAsUser("stat", { file: absPath }, identity) : fs.statSync(absPath);
52
67
  var ext = path.extname(absPath).toLowerCase();
53
68
  if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return null;
69
+ if (identity) return ctx.fsAsUser("read", { file: absPath, readContent: true }, identity);
54
70
  return { content: fs.readFileSync(absPath, "utf8"), size: stat.size };
55
71
  }
56
72
 
57
- function publishFileChanged(key, client, relPath, absPath) {
73
+ function publishFileChanged(key, client, relPath) {
58
74
  var latest = fileWatchers.get(key);
59
75
  if (!latest || latest.relPath !== relPath) return;
60
76
  try {
61
- var snapshot = readFileSnapshot(absPath);
77
+ var snapshot = readFileSnapshot(client, relPath);
62
78
  if (!snapshot) return;
63
79
  if (latest.hasSnapshot && latest.content === snapshot.content && latest.size === snapshot.size) return;
64
80
  latest.hasSnapshot = true;
@@ -97,7 +113,9 @@ function attachFileWatch(ctx) {
97
113
  var parentPath = path.dirname(absPath);
98
114
  var baseName = path.basename(absPath);
99
115
  var initialSnapshot = null;
100
- try { initialSnapshot = readFileSnapshot(absPath); } catch (e) {}
116
+ try { initialSnapshot = readFileSnapshot(client, relPath); } catch (e) {
117
+ return Promise.resolve(false);
118
+ }
101
119
  try {
102
120
  var watcher = fs.watch(parentPath, function (eventType, filename) {
103
121
  if (filename && String(filename) !== baseName) return;
@@ -105,7 +123,7 @@ function attachFileWatch(ctx) {
105
123
  if (!active || active.relPath !== relPath) return;
106
124
  clearTimeout(active.debounce);
107
125
  active.debounce = setTimeout(function () {
108
- publishFileChanged(key, client, relPath, absPath);
126
+ publishFileChanged(key, client, relPath);
109
127
  }, 200);
110
128
  });
111
129
  var resolveReady = null;
@@ -127,7 +145,7 @@ function attachFileWatch(ctx) {
127
145
  // drop them under load. Periodic content reconciliation is the source of
128
146
  // truth and also survives atomic replacements with identical metadata.
129
147
  entry.pollTimer = setInterval(function () {
130
- publishFileChanged(key, client, relPath, absPath);
148
+ publishFileChanged(key, client, relPath);
131
149
  }, 1000);
132
150
  // fs.watch has no readiness event. Reconcile once on the next event-loop
133
151
  // turn so a change between the initial read and native watcher activation
@@ -135,7 +153,7 @@ function attachFileWatch(ctx) {
135
153
  entry.reconcile = setImmediate(function () {
136
154
  var active = fileWatchers.get(key);
137
155
  if (active) active.reconcile = null;
138
- publishFileChanged(key, client, relPath, absPath);
156
+ publishFileChanged(key, client, relPath);
139
157
  if (fileWatchers.get(key) === entry) settleFileWatchReady(entry, true);
140
158
  });
141
159
  watcher.on("error", function () { closeFileWatch(key); });
@@ -155,55 +173,65 @@ function attachFileWatch(ctx) {
155
173
  for (var i = 0; i < keys.length; i++) closeFileWatch(keys[i]);
156
174
  }
157
175
 
158
- // --- Directory watcher ---
159
- var dirWatchers = {}; // relPath -> { watcher, debounce }
176
+ // Directory subscriptions are private to the requesting socket.
177
+ var dirWatchers = new Map();
160
178
 
161
- function startDirWatch(relPath) {
162
- if (dirWatchers[relPath]) return;
163
- var absPath = safePath(cwd, relPath);
164
- if (!absPath) return;
179
+ function readDirectory(client, relPath) {
180
+ var identity = identityFor(client);
181
+ var absPath = checkedPath(relPath);
182
+ var items = identity ? ctx.fsAsUser("list", { dir: absPath }, identity) :
183
+ fs.readdirSync(absPath, { withFileTypes: true }).map(function (item) {
184
+ return { name: item.name, isDir: item.isDirectory() };
185
+ });
186
+ return items.filter(function (item) {
187
+ return !item.isDir || !IGNORED_DIRS.has(item.name);
188
+ }).map(function (item) {
189
+ return { name: item.name, type: item.isDir ? "dir" : "file",
190
+ path: path.relative(cwd, path.join(absPath, item.name)).split(path.sep).join("/") };
191
+ });
192
+ }
193
+
194
+ function startDirWatch(client, relPath) {
195
+ var subscriptions = dirWatchers.get(client);
196
+ if (!subscriptions) { subscriptions = new Map(); dirWatchers.set(client, subscriptions); }
197
+ if (subscriptions.has(relPath)) return;
165
198
  try {
166
- var debounce = null;
167
- var watcher = fs.watch(absPath, function () {
168
- clearTimeout(debounce);
169
- debounce = setTimeout(function () {
170
- // Re-read directory and broadcast to all clients
199
+ readDirectory(client, relPath);
200
+ var entry = { watcher: null, debounce: null };
201
+ entry.watcher = fs.watch(checkedPath(relPath), function () {
202
+ clearTimeout(entry.debounce);
203
+ entry.debounce = setTimeout(function () {
171
204
  try {
172
- var items = fs.readdirSync(absPath, { withFileTypes: true });
173
- var entries = [];
174
- for (var i = 0; i < items.length; i++) {
175
- if (items[i].isDirectory() && IGNORED_DIRS.has(items[i].name)) continue;
176
- entries.push({
177
- name: items[i].name,
178
- type: items[i].isDirectory() ? "dir" : "file",
179
- path: path.relative(cwd, path.join(absPath, items[i].name)).split(path.sep).join("/"),
180
- });
181
- }
182
- send({ type: "fs_dir_changed", path: relPath, entries: entries });
183
- } catch (e) {
184
- stopDirWatch(relPath);
185
- }
205
+ var entries = readDirectory(client, relPath);
206
+ sendTo(client, { type: "fs_dir_changed", path: relPath, entries: entries });
207
+ } catch (e) { stopDirWatch(client, relPath); }
186
208
  }, 300);
187
209
  });
188
- watcher.on("error", function () { stopDirWatch(relPath); });
189
- dirWatchers[relPath] = { watcher: watcher, debounce: debounce };
190
- } catch (e) {}
210
+ entry.watcher.on("error", function () { stopDirWatch(client, relPath); });
211
+ subscriptions.set(relPath, entry);
212
+ } catch (e) {
213
+ if (!subscriptions.size) dirWatchers.delete(client);
214
+ }
191
215
  }
192
216
 
193
- function stopDirWatch(relPath) {
194
- var entry = dirWatchers[relPath];
195
- if (entry) {
196
- clearTimeout(entry.debounce);
197
- try { entry.watcher.close(); } catch (e) {}
198
- delete dirWatchers[relPath];
199
- }
217
+ function stopDirWatch(client, relPath) {
218
+ var subscriptions = dirWatchers.get(client);
219
+ var entry = subscriptions && subscriptions.get(relPath);
220
+ if (!entry) return;
221
+ clearTimeout(entry.debounce);
222
+ try { entry.watcher.close(); } catch (e) {}
223
+ subscriptions.delete(relPath);
224
+ if (!subscriptions.size) dirWatchers.delete(client);
200
225
  }
201
226
 
202
- function stopAllDirWatches() {
203
- var paths = Object.keys(dirWatchers);
204
- for (var i = 0; i < paths.length; i++) {
205
- stopDirWatch(paths[i]);
206
- }
227
+ function stopAllDirWatches(client) {
228
+ var clients = arguments.length ? [client] : Array.from(dirWatchers.keys());
229
+ clients.forEach(function (key) {
230
+ var subscriptions = dirWatchers.get(key);
231
+ if (subscriptions) Array.from(subscriptions.keys()).forEach(function (relPath) {
232
+ stopDirWatch(key, relPath);
233
+ });
234
+ });
207
235
  }
208
236
 
209
237
  return {
@@ -1,6 +1,7 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
- var execFileSync = require("child_process").execFileSync;
3
+ var attachRequestAccess = require("./project-request-access").attachRequestAccess;
4
+ var attachFileHistory = require("./project-file-history").attachFileHistory;
4
5
 
5
6
  /**
6
7
  * Attach filesystem-related message handlers to a project context.
@@ -20,17 +21,12 @@ var execFileSync = require("child_process").execFileSync;
20
21
  function attachFilesystem(ctx) {
21
22
  var cwd = ctx.cwd;
22
23
  var slug = ctx.slug;
23
- var osUsers = ctx.osUsers;
24
- var sm = ctx.sm;
25
- var send = ctx.send;
26
24
  var sendTo = ctx.sendTo;
27
25
  var safePath = ctx.safePath;
28
26
  var safeAbsPath = ctx.safeAbsPath;
29
- var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
30
27
  var startFileWatch = ctx.startFileWatch;
31
28
  var stopFileWatch = ctx.stopFileWatch;
32
29
  var startDirWatch = ctx.startDirWatch;
33
- var usersModule = ctx.usersModule;
34
30
  var fsAsUser = ctx.fsAsUser;
35
31
  var validateEnvString = ctx.validateEnvString;
36
32
  var onEnvironmentChanged = ctx.onEnvironmentChanged || function () {};
@@ -40,15 +36,19 @@ function attachFilesystem(ctx) {
40
36
  var IMAGE_EXTS = ctx.IMAGE_EXTS;
41
37
  var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
42
38
 
39
+ var access = ctx.requestAccess || attachRequestAccess(ctx);
40
+ var fileHistory = attachFileHistory(ctx, access);
41
+
43
42
  function handleFilesystemMessage(ws, msg) {
44
- // --- File browser permission gate ---
45
- if (msg.type === "fs_list" || msg.type === "fs_read" || msg.type === "fs_write" || msg.type === "fs_delete" || msg.type === "fs_rename" || msg.type === "fs_mkdir" || msg.type === "fs_upload" || msg.type === "fs_search") {
46
- if (ws._clayUser) {
47
- var fbPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
48
- if (!fbPerms.fileBrowser) {
49
- sendTo(ws, { type: msg.type + "_result", error: "File browser access is not permitted" });
50
- return true;
51
- }
43
+ // Unsubscription must remain available after permission revocation.
44
+ if (/^fs_/.test(msg.type) && msg.type !== "fs_unwatch") {
45
+ if (!access.canUseFiles(ws)) {
46
+ sendTo(ws, { type: msg.type + "_result", error: "File browser access is not permitted" });
47
+ return true;
48
+ }
49
+ try { access.osIdentity(ws); } catch (e) {
50
+ sendTo(ws, { type: msg.type + "_result", error: e.message });
51
+ return true;
52
52
  }
53
53
  }
54
54
 
@@ -56,7 +56,7 @@ function attachFilesystem(ctx) {
56
56
  if (msg.type === "fs_list") {
57
57
  var fsDir = safePath(cwd, msg.path || ".");
58
58
  // In OS user mode, fall back to absolute path resolution (ACL enforces access)
59
- if (!fsDir && getOsUserInfoForWs(ws)) {
59
+ if (!fsDir && access.osIdentity(ws)) {
60
60
  fsDir = safeAbsPath(msg.path);
61
61
  }
62
62
  if (!fsDir) {
@@ -64,7 +64,7 @@ function attachFilesystem(ctx) {
64
64
  return true;
65
65
  }
66
66
  try {
67
- var fsListUserInfo = getOsUserInfoForWs(ws);
67
+ var fsListUserInfo = access.osIdentity(ws);
68
68
  var entries = [];
69
69
  if (fsListUserInfo) {
70
70
  // Run as target OS user to respect Linux file permissions
@@ -92,7 +92,7 @@ function attachFilesystem(ctx) {
92
92
  }
93
93
  sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
94
94
  // Auto-watch the directory for changes
95
- startDirWatch(msg.path || ".");
95
+ startDirWatch(ws, msg.path || ".");
96
96
  } catch (e) {
97
97
  sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: e.message });
98
98
  }
@@ -109,7 +109,7 @@ function attachFilesystem(ctx) {
109
109
  try {
110
110
  var searchResults = [];
111
111
  var MAX_RESULTS = 50;
112
- var searchUserInfo = getOsUserInfoForWs(ws);
112
+ var searchUserInfo = access.osIdentity(ws);
113
113
 
114
114
  function walkDir(dir, relPrefix) {
115
115
  if (searchResults.length >= MAX_RESULTS) return;
@@ -148,7 +148,7 @@ function attachFilesystem(ctx) {
148
148
  // --- fs_read ---
149
149
  if (msg.type === "fs_read") {
150
150
  var fsFile = safePath(cwd, msg.path);
151
- if (!fsFile && getOsUserInfoForWs(ws)) {
151
+ if (!fsFile && access.osIdentity(ws)) {
152
152
  fsFile = safeAbsPath(msg.path);
153
153
  }
154
154
  if (!fsFile) {
@@ -156,7 +156,7 @@ function attachFilesystem(ctx) {
156
156
  return true;
157
157
  }
158
158
  try {
159
- var fsReadUserInfo = getOsUserInfoForWs(ws);
159
+ var fsReadUserInfo = access.osIdentity(ws);
160
160
  var ext = path.extname(fsFile).toLowerCase();
161
161
  if (fsReadUserInfo) {
162
162
  // Run stat and read as target OS user
@@ -197,7 +197,7 @@ function attachFilesystem(ctx) {
197
197
  // --- fs_write ---
198
198
  if (msg.type === "fs_write") {
199
199
  var fsWriteFile = safePath(cwd, msg.path);
200
- if (!fsWriteFile && getOsUserInfoForWs(ws)) {
200
+ if (!fsWriteFile && access.osIdentity(ws)) {
201
201
  fsWriteFile = safeAbsPath(msg.path);
202
202
  }
203
203
  if (!fsWriteFile) {
@@ -205,7 +205,7 @@ function attachFilesystem(ctx) {
205
205
  return true;
206
206
  }
207
207
  try {
208
- var fsWriteUserInfo = getOsUserInfoForWs(ws);
208
+ var fsWriteUserInfo = access.osIdentity(ws);
209
209
  if (fsWriteUserInfo) {
210
210
  fsAsUser("write", { file: fsWriteFile, content: msg.content || "" }, fsWriteUserInfo);
211
211
  } else {
@@ -223,12 +223,12 @@ function attachFilesystem(ctx) {
223
223
  msg.type === "read_global_claude_md" || msg.type === "write_global_claude_md" ||
224
224
  msg.type === "get_shared_env" || msg.type === "set_shared_env" ||
225
225
  msg.type === "transfer_project_owner") {
226
- if (ws._clayUser) {
227
- var psPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
228
- if (!psPerms.projectSettings) {
229
- sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
230
- return true;
231
- }
226
+ var globalSetting = /^(read_global_claude_md|write_global_claude_md|get_shared_env|set_shared_env)$/.test(msg.type);
227
+ var targetSlug = msg.slug || slug;
228
+ if (!access.hasPermission(ws, "projectSettings") ||
229
+ (globalSetting ? !access.isAdmin(ws) : !access.canAccessProject(ws, targetSlug))) {
230
+ sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
231
+ return true;
232
232
  }
233
233
  }
234
234
 
@@ -236,7 +236,7 @@ function attachFilesystem(ctx) {
236
236
  if (msg.type === "get_project_env") {
237
237
  var envrc = "";
238
238
  if (typeof opts.onGetProjectEnv === "function") {
239
- var envResult = opts.onGetProjectEnv(msg.slug);
239
+ var envResult = opts.onGetProjectEnv(targetSlug);
240
240
  envrc = envResult.envrc || "";
241
241
  }
242
242
  sendTo(ws, { type: "project_env_result", slug: msg.slug, envrc: envrc });
@@ -250,7 +250,7 @@ function attachFilesystem(ctx) {
250
250
  sendTo(ws, { type: "set_project_env_result", ok: false, slug: msg.slug, error: envError });
251
251
  return true;
252
252
  }
253
- var setResult = opts.onSetProjectEnv(msg.slug, msg.envrc || "");
253
+ var setResult = opts.onSetProjectEnv(targetSlug, msg.envrc || "");
254
254
  if (setResult.ok) onEnvironmentChanged();
255
255
  sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
256
256
  } else {
@@ -324,196 +324,7 @@ function attachFilesystem(ctx) {
324
324
  return true;
325
325
  }
326
326
 
327
- // --- File edit history ---
328
- if (msg.type === "fs_file_history") {
329
- var histPath = msg.path;
330
- if (!histPath) {
331
- sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: [] });
332
- return true;
333
- }
334
- var absHistPath = path.resolve(cwd, histPath);
335
- var entries = [];
336
-
337
- // Collect session edits
338
- sm.sessions.forEach(function (session) {
339
- var sessionLocalId = session.localId;
340
- var sessionTitle = session.title || "Untitled";
341
- var histLen = session.history.length || 1;
342
-
343
- for (var hi = 0; hi < session.history.length; hi++) {
344
- var entry = session.history[hi];
345
- if (entry.type !== "tool_executing") continue;
346
- if (entry.name !== "Edit" && entry.name !== "Write") continue;
347
- if (!entry.input || !entry.input.file_path) continue;
348
- if (entry.input.file_path !== absHistPath) continue;
349
-
350
- // Find parent assistant UUID + message snippet by scanning backwards
351
- var assistantUuid = null;
352
- var uuidIndex = -1;
353
- for (var hj = hi - 1; hj >= 0; hj--) {
354
- if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
355
- assistantUuid = session.history[hj].uuid;
356
- uuidIndex = hj;
357
- break;
358
- }
359
- }
360
-
361
- // Find user prompt by scanning backwards from the assistant uuid
362
- var messageSnippet = "";
363
- var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
364
- for (var hk = searchFrom - 1; hk >= 0; hk--) {
365
- if (session.history[hk].type === "user_message" && session.history[hk].text) {
366
- messageSnippet = session.history[hk].text.trim().substring(0, 100);
367
- break;
368
- }
369
- }
370
-
371
- // Collect Claude's explanation: scan backwards from tool_executing
372
- // to find the nearest delta text block (skipping tool_start).
373
- // If no delta found immediately before this tool, scan past
374
- // intervening tool blocks to find the last delta text within
375
- // the same assistant turn.
376
- var assistantSnippet = "";
377
- var deltaChunks = [];
378
- for (var hd = hi - 1; hd >= 0; hd--) {
379
- var hEntry = session.history[hd];
380
- if (hEntry.type === "tool_start") continue;
381
- if (hEntry.type === "delta" && hEntry.text) {
382
- deltaChunks.unshift(hEntry.text);
383
- } else {
384
- break;
385
- }
386
- }
387
- if (deltaChunks.length === 0) {
388
- // No delta immediately before; scan past tool blocks
389
- // to find the nearest preceding delta in the same turn
390
- for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
391
- var hEntry2 = session.history[hd2];
392
- if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
393
- if (hEntry2.type === "delta" && hEntry2.text) {
394
- // Found a delta before an earlier tool in the same turn.
395
- // Collect this contiguous block of deltas.
396
- for (var hd3 = hd2; hd3 >= 0; hd3--) {
397
- var hEntry3 = session.history[hd3];
398
- if (hEntry3.type === "tool_start") continue;
399
- if (hEntry3.type === "delta" && hEntry3.text) {
400
- deltaChunks.unshift(hEntry3.text);
401
- } else {
402
- break;
403
- }
404
- }
405
- break;
406
- } else {
407
- // Hit message_uuid, user_message, etc. Stop.
408
- break;
409
- }
410
- }
411
- }
412
- assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
413
-
414
- // Approximate timestamp: interpolate between session creation and last activity
415
- var tStart = session.createdAt || 0;
416
- var tEnd = session.lastActivity || tStart;
417
- var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
418
-
419
- var editRecord = {
420
- source: "session",
421
- timestamp: ts,
422
- sessionLocalId: sessionLocalId,
423
- sessionTitle: sessionTitle,
424
- assistantUuid: assistantUuid,
425
- toolId: entry.id,
426
- messageSnippet: messageSnippet,
427
- assistantSnippet: assistantSnippet,
428
- toolName: entry.name,
429
- };
430
-
431
- if (entry.name === "Edit") {
432
- editRecord.old_string = entry.input.old_string || "";
433
- editRecord.new_string = entry.input.new_string || "";
434
- } else {
435
- editRecord.isFullWrite = true;
436
- }
437
-
438
- entries.push(editRecord);
439
- }
440
- });
441
-
442
- // Collect git commits
443
- try {
444
- var gitLog = execFileSync(
445
- "git", ["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
446
- { cwd: cwd, encoding: "utf8", timeout: 5000 }
447
- );
448
- var gitLines = gitLog.trim().split("\n");
449
- for (var gi = 0; gi < gitLines.length; gi++) {
450
- if (!gitLines[gi]) continue;
451
- var parts = gitLines[gi].split("|");
452
- if (parts.length < 4) continue;
453
- entries.push({
454
- source: "git",
455
- hash: parts[0],
456
- timestamp: parseInt(parts[1], 10) * 1000,
457
- author: parts[2],
458
- message: parts.slice(3).join("|"),
459
- });
460
- }
461
- } catch (e) {
462
- // Not a git repo or file not tracked, that is fine
463
- }
464
-
465
- // Sort by timestamp descending (newest first)
466
- entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
467
-
468
- sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: entries });
469
- return true;
470
- }
471
-
472
- // --- Git diff for file history ---
473
- if (msg.type === "fs_git_diff") {
474
- var diffPath = msg.path;
475
- var hash = msg.hash;
476
- var hash2 = msg.hash2 || null;
477
- if (!diffPath || !hash) {
478
- sendTo(ws, { type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
479
- return true;
480
- }
481
- try {
482
- var diff;
483
- if (hash2) {
484
- diff = execFileSync("git", ["diff", hash, hash2, "--", diffPath],
485
- { cwd: cwd, encoding: "utf8", timeout: 5000 });
486
- } else {
487
- diff = execFileSync("git", ["show", hash, "--format=", "--", diffPath],
488
- { cwd: cwd, encoding: "utf8", timeout: 5000 });
489
- }
490
- sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
491
- } catch (e) {
492
- sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
493
- }
494
- return true;
495
- }
496
-
497
- // --- File content at a git commit ---
498
- if (msg.type === "fs_file_at") {
499
- var atPath = msg.path;
500
- var atHash = msg.hash;
501
- if (!atPath || !atHash) {
502
- sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
503
- return true;
504
- }
505
- try {
506
- // Convert to repo-relative path (git show requires hash:relative/path)
507
- var atAbsPath = path.resolve(cwd, atPath);
508
- var atRelPath = path.relative(cwd, atAbsPath);
509
- var content = execFileSync("git", ["show", atHash + ":" + atRelPath],
510
- { cwd: cwd, encoding: "utf8", timeout: 5000 });
511
- sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
512
- } catch (e) {
513
- sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
514
- }
515
- return true;
516
- }
327
+ if (fileHistory.handleFileHistory(ws, msg)) return true;
517
328
 
518
329
  return false;
519
330
  }
@@ -730,7 +730,7 @@ function attachHTTP(ctx) {
730
730
  }
731
731
 
732
732
  // Git panel status and actions
733
- var isGitPanelRequest = urlPath === "/api/git/status" ||
733
+ var isGitPanelRequest = urlPath === "/api/git/status" || urlPath === "/api/git/summary" ||
734
734
  urlPath === "/api/git/action" || urlPath.indexOf("/api/git/file-diff?") === 0 ||
735
735
  urlPath.indexOf("/api/git/session-diff?") === 0;
736
736
  if (isGitPanelRequest && usersModule.isMultiUser()) {
@@ -768,6 +768,20 @@ function attachHTTP(ctx) {
768
768
  return true;
769
769
  }
770
770
 
771
+ // Compact repository placard data for the always-visible sidebar block.
772
+ // Same permission gate as the panel, but no file payload and no path.
773
+ if (req.method === "GET" && urlPath === "/api/git/summary") {
774
+ try {
775
+ var gitSummary = gitCli.getSummary(cwd, getOsUserInfoForReq(req));
776
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
777
+ res.end(JSON.stringify(gitSummary));
778
+ } catch (e) {
779
+ res.writeHead(500, { "Content-Type": "application/json" });
780
+ res.end(JSON.stringify({ error: e.message || "Unable to read Git summary" }));
781
+ }
782
+ return true;
783
+ }
784
+
771
785
  if (req.method === "GET" && urlPath.indexOf("/api/git/file-diff?") === 0) {
772
786
  var gitDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
773
787
  var gitDiffPath = gitDiffParams.get("path");
@@ -1,5 +1,6 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
+ var mateSync = require("./mate-knowledge-sync");
3
4
 
4
5
  /**
5
6
  * Attach knowledge file management to a project context.
@@ -75,6 +76,23 @@ function attachKnowledge(ctx) {
75
76
  return files;
76
77
  }
77
78
 
79
+ // Mirror a Mate-owned knowledge file into the Clay-wide Knowledge backend
80
+ // after the legacy write has already succeeded. The legacy file stays
81
+ // authoritative; this only keeps the new scope from going stale.
82
+ function mirrorMateSource(ws, fileName) {
83
+ if (!isMate) return;
84
+ var user = (ws && ws._clayUser) || null;
85
+ mateSync.syncQuietly({
86
+ mateDir: cwd,
87
+ fileName: fileName,
88
+ actor: {
89
+ type: user ? "user" : "system",
90
+ userId: user ? user.id : null,
91
+ displayName: user ? (user.displayName || user.username) : null,
92
+ },
93
+ });
94
+ }
95
+
78
96
  function handleKnowledgeMessage(ws, msg) {
79
97
  if (msg.type === "knowledge_list") {
80
98
  sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
@@ -111,6 +129,7 @@ function attachKnowledge(ctx) {
111
129
  var knowledgeDir = path.join(cwd, "knowledge");
112
130
  fs.mkdirSync(knowledgeDir, { recursive: true });
113
131
  fs.writeFileSync(path.join(knowledgeDir, safeName), msg.content);
132
+ mirrorMateSource(ws, safeName);
114
133
  sendTo(ws, { type: "knowledge_saved", name: safeName });
115
134
  sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
116
135
  return true;
@@ -121,6 +140,9 @@ function attachKnowledge(ctx) {
121
140
  var safeName = path.basename(msg.name);
122
141
  var filePath = path.join(cwd, "knowledge", safeName);
123
142
  try { fs.unlinkSync(filePath); } catch (e) {}
143
+ // Reconciling after the unlink appends a tombstone, so a deleted file
144
+ // does not stay live in the new backend.
145
+ mirrorMateSource(ws, safeName);
124
146
  sendTo(ws, { type: "knowledge_deleted", name: safeName });
125
147
  sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
126
148
  return true;