clay-server 4.0.0-beta.9 → 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 -7
  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
@@ -2,14 +2,29 @@ var toolsRegistry = require("./tools-registry");
2
2
  var toolStorage = require("./tool-storage");
3
3
  var crypto = require("crypto");
4
4
  var toolLlm = require("./tool-llm");
5
+ var capsuleFloor = require("./capsule-display-floor");
6
+ var capsuleServerRuntimes = require("./capsule-server-runtimes");
7
+ var capsuleFrameServer = require("./capsule-frame-server");
8
+ var capsuleMateTurn = require("./capsule-mate-turn");
5
9
 
6
10
  function attachTools(deps) {
7
11
  var users = deps.users;
8
12
  var projects = deps.projects;
9
- var serverLogic = Object.create(null);
13
+ var serverRuntimes = deps.serverRuntimes || capsuleServerRuntimes;
14
+ var frameServer = deps.frameServer || capsuleFrameServer.createCapsuleFrameServer({ tlsOptions: deps.tlsOptions || null });
15
+ var mateTurn = deps.mateTurn || capsuleMateTurn.attachCapsuleMateTurn({
16
+ users: users,
17
+ findMateProject: deps.findMateProject || null,
18
+ broadcastToUser: function (targetUserId, payload) { broadcastToUser(targetUserId, payload); },
19
+ });
10
20
  var controlTimeoutMs = deps.controlTimeoutMs || 15000;
11
21
  var llmControlTimeoutMs = deps.llmControlTimeoutMs || 90000;
12
22
  var pendingControl = Object.create(null);
23
+ var isCapsulesEnabled = deps.isCapsulesEnabled || function () { return true; };
24
+
25
+ function requireCapsulesEnabled(userId) {
26
+ if (!isCapsulesEnabled(userId)) throw new Error("Capsules are disabled in User Settings.");
27
+ }
13
28
 
14
29
  function contextFor(userId) {
15
30
  var multiUser = users.isMultiUser();
@@ -68,6 +83,7 @@ function attachTools(deps) {
68
83
  }
69
84
 
70
85
  function installedManifests(userId) {
86
+ if (!isCapsulesEnabled(userId)) return [];
71
87
  var ctx = contextFor(userId);
72
88
  return toolsRegistry.listTools(ctx).filter(function (manifest) { return !manifest.error; });
73
89
  }
@@ -137,20 +153,52 @@ function attachTools(deps) {
137
153
  });
138
154
  }
139
155
 
156
+ // One act pipeline for server-runtime Capsules. The human's button click and
157
+ // the Mate's clay_tool_act call both arrive here, so the same Logic enforces
158
+ // the same rules for both and neither gets a private path. The seat is set
159
+ // from the server-resolved actor, never from caller-supplied text.
160
+ //
161
+ // The caller gets state and nothing else: a Mate never sees Display. Every
162
+ // successful act additionally pushes its causal {seq, actor, action,
163
+ // previous, next} event to the user's own connected clients, so a watching
164
+ // Display re-renders on any Logic change no matter who caused it.
165
+ async function controlServerCapsule(userId, installed, actor, callerId, kind, payload) {
166
+ var runtime = serverRuntimes.createRuntime(installed.manifest.id, contextFor(userId));
167
+ if (!runtime) throw new Error("No trusted server runtime is registered for this capsule.");
168
+ var context = { userId: userId, actor: actor, callerId: callerId };
169
+ if (kind === "snapshot") return runtime.snapshot(context);
170
+ if (kind === "act") {
171
+ var result = await runtime.act(context, payload.actionId, payload.args || {});
172
+ if (result && result.event) {
173
+ broadcastToUser(userId, { type: "tool_server_event", toolId: installed.manifest.id, event: result.event });
174
+ // The game round-trips: a Mate act names the opponent seat, and a
175
+ // human act that hands the turn over wakes that Mate to play it.
176
+ if (actor === "mate" && callerId) mateTurn.rememberOpponent(userId, installed.manifest.id, callerId);
177
+ mateTurn.maybeNudge(userId, installed.manifest, actor, result.event);
178
+ return result.state;
179
+ }
180
+ return result;
181
+ }
182
+ if (kind === "set" && runtime.set) return runtime.set(context, payload.controlId, payload.value);
183
+ throw new Error("The server-runtime capsule does not support '" + kind + "'.");
184
+ }
185
+
140
186
  function controlForMate(userId, mateId, toolId, kind, payload) {
187
+ requireCapsulesEnabled(userId);
141
188
  payload = payload || {};
142
189
  var ctx = contextFor(userId);
143
190
  toolsRegistry.listTools(ctx);
144
191
  var installed = toolsRegistry.getTool(ctx, toolId);
145
192
  if (!installed) return Promise.reject(new Error("Tool is not installed."));
193
+ // Skills are gated by Display: the Mate loses a Capsule at the same moment
194
+ // the human loses the declarative floor it is operated through.
195
+ if (!capsuleFloor.hasUsableFloor(installed.manifest, installed.uiTree)) {
196
+ return Promise.reject(new Error("This Capsule has no usable human Display, so it is unavailable to Mates."));
197
+ }
146
198
  if (installed.manifest.runtime === "server") {
147
- var adapter = serverLogic[toolId];
148
- if (!adapter) return Promise.reject(new Error("No trusted server runtime is registered for this capsule."));
149
- var context = { userId: userId, callerId: mateId };
150
- if (kind === "snapshot") return Promise.resolve(adapter.snapshot(context));
151
- if (kind === "act") return Promise.resolve(adapter.act(context, payload.actionId, payload.args || {}));
152
- if (kind === "set" && adapter.set) return Promise.resolve(adapter.set(context, payload.controlId, payload.value));
153
- return Promise.reject(new Error("The server-runtime capsule does not support '" + kind + "'."));
199
+ return Promise.resolve().then(function () {
200
+ return controlServerCapsule(userId, installed, "mate", mateId, kind, payload);
201
+ });
154
202
  }
155
203
  var permissions = installed.manifest.permissions || [];
156
204
  var timeoutMs = kind === "act" && permissions.indexOf("llm") !== -1 ? llmControlTimeoutMs : controlTimeoutMs;
@@ -211,6 +259,7 @@ function attachTools(deps) {
211
259
  }
212
260
 
213
261
  function installForMate(userId, input) {
262
+ requireCapsulesEnabled(userId);
214
263
  var installed = toolsRegistry.installTool(contextFor(userId), input);
215
264
  refreshMateCatalogs(userId);
216
265
  broadcastToUser(userId, { type: "tool_installed", tool: installed });
@@ -218,6 +267,7 @@ function attachTools(deps) {
218
267
  }
219
268
 
220
269
  function sourceForMate(userId, toolId) {
270
+ requireCapsulesEnabled(userId);
221
271
  var ctx = contextFor(userId);
222
272
  var installed = toolsRegistry.getTool(ctx, toolId);
223
273
  if (!installed) throw new Error("Tool not found.");
@@ -229,6 +279,7 @@ function attachTools(deps) {
229
279
  }
230
280
 
231
281
  function updateForMate(userId, toolId, input) {
282
+ requireCapsulesEnabled(userId);
232
283
  var ctx = contextFor(userId);
233
284
  var installed = toolsRegistry.getTool(ctx, toolId);
234
285
  if (!installed) throw new Error("Tool not found.");
@@ -243,6 +294,7 @@ function attachTools(deps) {
243
294
  }
244
295
 
245
296
  function setMateAccess(userId, toolId, allowed, requestId) {
297
+ requireCapsulesEnabled(userId);
246
298
  var ctx = contextFor(userId);
247
299
  var tool = toolsRegistry.getTool(ctx, toolId);
248
300
  if (!tool) throw new Error("Tool not found.");
@@ -255,6 +307,7 @@ function attachTools(deps) {
255
307
  }
256
308
 
257
309
  function removeForMate(userId, toolId) {
310
+ requireCapsulesEnabled(userId);
258
311
  var removed = toolsRegistry.removeTool(contextFor(userId), toolId);
259
312
  if (!removed) throw new Error("Tool not found.");
260
313
  refreshMateCatalogs(userId);
@@ -262,8 +315,19 @@ function attachTools(deps) {
262
315
  return { removed: true, toolId: toolId };
263
316
  }
264
317
 
318
+ // The human half of the shared server-Capsule pipeline. Same Logic, same
319
+ // rules, no AI in the path.
320
+ function controlForUser(userId, toolId, kind, payload) {
321
+ requireCapsulesEnabled(userId);
322
+ var ctx = contextFor(userId);
323
+ var installed = toolsRegistry.getTool(ctx, toolId);
324
+ if (!installed) throw new Error("Tool is not installed.");
325
+ if (installed.manifest.runtime !== "server") throw new Error("This Capsule does not run on the server.");
326
+ return controlServerCapsule(userId, installed, "human", "user", kind, payload || {});
327
+ }
328
+
265
329
  function handleMessage(ws, msg) {
266
- var messageTypes = ["tools_list", "tool_get", "tool_install", "tool_remove", "tool_storage_op", "tool_llm_op", "tool_llm_config_get", "tool_control_response", "tool_source_get", "tool_mate_access_set"];
330
+ var messageTypes = ["tools_list", "tool_get", "tool_install", "tool_remove", "tool_storage_op", "tool_llm_op", "tool_llm_config_get", "tool_control_response", "tool_source_get", "tool_mate_access_set", "tool_server_control", "tool_frame_url"];
267
331
  if (!msg || messageTypes.indexOf(msg.type) === -1) return false;
268
332
  var userId;
269
333
  if (users.isMultiUser()) {
@@ -279,6 +343,12 @@ function attachTools(deps) {
279
343
  return true;
280
344
  }
281
345
 
346
+ if (!isCapsulesEnabled(userId)) {
347
+ if (msg.type === "tools_list") send(ws, { type: "tools_state", tools: [] });
348
+ else sendError(ws, msg, new Error("Capsules are disabled in User Settings."));
349
+ return true;
350
+ }
351
+
282
352
  if (msg.type === "tools_list") {
283
353
  run(ws, msg, function () {
284
354
  send(ws, { type: "tools_state", tools: fullToolList(ctx) });
@@ -312,6 +382,52 @@ function attachTools(deps) {
312
382
  });
313
383
  return true;
314
384
  }
385
+ if (msg.type === "tool_frame_url") {
386
+ // The rich element renders on a separate anonymous origin, so the frame
387
+ // URL is minted here, on the authenticated socket, as a short-lived
388
+ // one-time token bound to this user's tools root. The rich element is
389
+ // additive only: it exists solely when the floor already validated at
390
+ // registration, and losing it never costs the human the floor.
391
+ Promise.resolve().then(function () {
392
+ var ctx = contextFor(userId);
393
+ var installed = toolsRegistry.getTool(ctx, msg.toolId);
394
+ if (!installed) throw new Error("Tool is not installed.");
395
+ if (!installed.hasRichDisplay) throw new Error("This Capsule ships no rich Display element.");
396
+ return frameServer.issueFrameUrl(ctx, msg.toolId);
397
+ }).then(function (frame) {
398
+ send(ws, { type: "tool_frame_url_state", toolId: msg.toolId, requestId: msg.requestId || null, ok: true, frame: frame });
399
+ }).catch(function (error) {
400
+ send(ws, {
401
+ type: "tool_frame_url_state",
402
+ toolId: msg.toolId,
403
+ requestId: msg.requestId || null,
404
+ ok: false,
405
+ error: error && error.message ? error.message : "The rich Display is unavailable.",
406
+ });
407
+ });
408
+ return true;
409
+ }
410
+ if (msg.type === "tool_server_control") {
411
+ Promise.resolve().then(function () {
412
+ return controlForUser(userId, msg.toolId, msg.kind, {
413
+ actionId: msg.actionId,
414
+ args: msg.args || {},
415
+ controlId: msg.controlId,
416
+ value: msg.value,
417
+ });
418
+ }).then(function (state) {
419
+ send(ws, { type: "tool_server_state", toolId: msg.toolId, requestId: msg.requestId || null, ok: true, state: state });
420
+ }).catch(function (error) {
421
+ send(ws, {
422
+ type: "tool_server_state",
423
+ toolId: msg.toolId,
424
+ requestId: msg.requestId || null,
425
+ ok: false,
426
+ error: error && error.message ? error.message : "The Capsule action failed.",
427
+ });
428
+ });
429
+ return true;
430
+ }
315
431
  if (msg.type === "tool_install") {
316
432
  run(ws, msg, function () {
317
433
  installForMate(userId, {
@@ -366,11 +482,13 @@ function attachTools(deps) {
366
482
  handleMessage: handleMessage,
367
483
  installedManifests: installedManifests,
368
484
  controlForMate: controlForMate,
485
+ controlForUser: controlForUser,
369
486
  installForMate: installForMate,
370
487
  sourceForMate: sourceForMate,
371
488
  updateForMate: updateForMate,
372
489
  setMateAccess: setMateAccess,
373
490
  removeForMate: removeForMate,
491
+ refreshMateCatalogs: refreshMateCatalogs,
374
492
  };
375
493
  }
376
494
 
package/lib/server.js CHANGED
@@ -23,6 +23,8 @@ var serverPalette = require("./server-palette");
23
23
  var workspaceQueryModule = require("./workspace-query-service");
24
24
  var { attachWorkspaceQueryService } = workspaceQueryModule;
25
25
  var { attachWorkspaceAssignmentService } = require("./workspace-assignment-service");
26
+ var { attachProjectLogsService } = require("./project-logs-service");
27
+ var { attachMateKnowledgeService } = require("./mate-knowledge-service");
26
28
  var serverEmail = require("./server-email");
27
29
  var serverGlobalWs = require("./server-global-ws");
28
30
  var { parseWsRequestUrl } = require("./ws-request");
@@ -122,6 +124,14 @@ function stripPrefix(urlPath, slug) {
122
124
  return rest || "/";
123
125
  }
124
126
 
127
+ function injectRootProjectSlug(html, slug) {
128
+ var bodyPattern = /<body\b([^>]*)>/i;
129
+ if (!bodyPattern.test(html)) throw new Error("App shell body is missing");
130
+ return html.replace(bodyPattern, function (_match, attributes) {
131
+ return '<body' + attributes + ' data-home-project-slug="' + slug + '">';
132
+ });
133
+ }
134
+
125
135
  /**
126
136
  * Create a multi-project server.
127
137
  * opts: { tlsOptions, caPath, pinHash, port, debug, dangerouslySkipPermissions }
@@ -306,18 +316,50 @@ function createServer(opts) {
306
316
  var workspaceQueryService = attachWorkspaceQueryService({
307
317
  getProjects: function () { return projects; },
308
318
  isMultiUser: function () { return users.isMultiUser(); },
309
- canAccessProject: function (userId, status) {
310
- return users.canAccessProject(userId, {
311
- ownerId: status && status.projectOwnerId || null,
312
- visibility: status && status.visibility || "public",
313
- allowedUsers: status && status.allowedUsers || [],
314
- });
319
+ // Project access is resolved from the authoritative project record, not
320
+ // from getStatus(), which carries no visibility or allowedUsers fields and
321
+ // would otherwise default every project to public.
322
+ onGetProjectAccess: onGetProjectAccess,
323
+ canAccessProject: function (userId, access) {
324
+ return users.canAccessProject(userId, access) === true;
315
325
  },
316
326
  resolveMate: function (userId, mateId) {
317
327
  return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? userId : null), mateId);
318
328
  },
319
329
  assignmentService: assignmentService,
320
330
  });
331
+ // Project access is resolved from the authoritative project record, not from
332
+ // getStatus(), which carries no visibility or allowedUsers fields.
333
+ var projectLogsService = attachProjectLogsService({
334
+ getProjects: function () { return projects; },
335
+ isMultiUser: function () { return users.isMultiUser(); },
336
+ canAccessProject: function (userId, status) {
337
+ if (!status || !status.slug || !onGetProjectAccess) return false;
338
+ var access = onGetProjectAccess(status.slug);
339
+ if (!access || access.error) return false;
340
+ return users.canAccessProject(userId, access) === true;
341
+ },
342
+ resolveMate: function (userId, mateId) {
343
+ return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? userId : null), mateId);
344
+ },
345
+ findUserById: function (userId) { return users.findUserById(userId); },
346
+ });
347
+ // Mate Knowledge reads the synchronized Knowledge records. The Mate list is
348
+ // derived from the owning user alone, so a binding can never span users.
349
+ var mateKnowledgeService = attachMateKnowledgeService({
350
+ getProjects: function () { return projects; },
351
+ isMultiUser: function () { return users.isMultiUser(); },
352
+ resolveMate: function (userId, mateId) {
353
+ return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? userId : null), mateId);
354
+ },
355
+ listMates: function (userId) {
356
+ var mateCtx = mates.buildMateCtx(users.isMultiUser() ? userId : null);
357
+ var all = mates.getAllMates(mateCtx) || [];
358
+ return all.map(function (mate) {
359
+ return { id: mate.id, name: mate.name || null, createdBy: mate.createdBy || null, dir: mates.getMateDir(mateCtx, mate.id) };
360
+ });
361
+ },
362
+ });
321
363
 
322
364
  // --- Admin module ---
323
365
  var admin = serverAdmin.attachAdmin({
@@ -351,6 +393,9 @@ function createServer(opts) {
351
393
  projects: projects,
352
394
  opts: opts,
353
395
  CONFIG_DIR: CONFIG_DIR,
396
+ onCapsulesPreferenceChanged: function (userId) {
397
+ if (toolsHandler) toolsHandler.refreshMateCatalogs(userId);
398
+ },
354
399
  });
355
400
 
356
401
  var palette = serverPalette.attachPalette({
@@ -388,16 +433,38 @@ function createServer(opts) {
388
433
  securityHeaders["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains";
389
434
  }
390
435
 
391
- function setSecurityHeaders(res) {
436
+ // Capsule rich-Display frames are served by a second listener on the same
437
+ // hostname but a different port, so frame-src cannot stay at the default
438
+ // 'self'. The allowance is computed from the request's own hostname with
439
+ // any port, never a wildcard host, and only for the scheme the app itself
440
+ // speaks. If the host header is unusable the CSP stays at 'self' and the
441
+ // frame silently falls back to the floor Display.
442
+ function frameSrcFor(req) {
443
+ var host = req && req.headers && typeof req.headers.host === "string" ? req.headers.host : "";
444
+ if (!host) return "'self'";
445
+ var scheme = tlsOptions ? "https" : "http";
446
+ var hostname;
447
+ try {
448
+ hostname = new URL(scheme + "://" + host).hostname;
449
+ } catch (error) {
450
+ return "'self'";
451
+ }
452
+ if (!hostname) return "'self'";
453
+ if (hostname.indexOf(":") !== -1) hostname = "[" + hostname + "]";
454
+ return "'self' " + scheme + "://" + hostname + ":*";
455
+ }
456
+
457
+ function setSecurityHeaders(res, req) {
392
458
  var keys = Object.keys(securityHeaders);
393
459
  for (var i = 0; i < keys.length; i++) {
394
460
  res.setHeader(keys[i], securityHeaders[keys[i]]);
395
461
  }
462
+ res.setHeader("Content-Security-Policy", securityHeaders["Content-Security-Policy"] + " frame-src " + frameSrcFor(req) + ";");
396
463
  }
397
464
 
398
465
  // --- HTTP handler ---
399
466
  var appHandler = function (req, res) {
400
- setSecurityHeaders(res);
467
+ setSecurityHeaders(res, req);
401
468
  var fullUrl = req.url.split("?")[0];
402
469
 
403
470
  // --- Auth routes (delegated to server-auth) ---
@@ -682,7 +749,8 @@ function createServer(opts) {
682
749
  // --- Skills routes (delegated to server-skills) ---
683
750
  if (skills.handleRequest(req, res, fullUrl)) return;
684
751
 
685
- // Root path — render home while keeping the last accessible project connected.
752
+ // Root path — render the default project workspace while keeping Home
753
+ // available as an explicit client-side surface.
686
754
  if (fullUrl === "/" && req.method === "GET") {
687
755
  if (!isRequestAuthed(req)) {
688
756
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
@@ -692,11 +760,16 @@ function createServer(opts) {
692
760
  if (projects.size > 0) {
693
761
  var targetSlug = null;
694
762
  var reqUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
763
+ function isOrdinaryProject(slug) {
764
+ var candidate = slug ? projects.get(slug) : null;
765
+ var status = candidate && candidate.getStatus ? candidate.getStatus() : null;
766
+ return !!candidate && (!status || status.isMate !== true);
767
+ }
695
768
  var homePreference = typeof users.getHomeSurfacePreference === "function"
696
769
  ? users.getHomeSurfacePreference(reqUser ? reqUser.id : "default")
697
770
  : null;
698
771
  var preferredContextSlug = homePreference && homePreference.projectSlug;
699
- if (preferredContextSlug && projects.has(preferredContextSlug)) {
772
+ if (isOrdinaryProject(preferredContextSlug)) {
700
773
  if (reqUser && onGetProjectAccess) {
701
774
  var preferredAccess = onGetProjectAccess(preferredContextSlug);
702
775
  if (preferredAccess && !preferredAccess.error && users.canAccessProject(reqUser.id, preferredAccess)) {
@@ -708,7 +781,7 @@ function createServer(opts) {
708
781
  }
709
782
  // Check for last-visited project cookie
710
783
  var lastProject = parseCookies(req)["clay_last_project"];
711
- if (!targetSlug && lastProject && projects.has(lastProject)) {
784
+ if (!targetSlug && isOrdinaryProject(lastProject)) {
712
785
  if (reqUser && onGetProjectAccess) {
713
786
  var lpAccess = onGetProjectAccess(lastProject);
714
787
  if (lpAccess && !lpAccess.error && users.canAccessProject(reqUser.id, lpAccess)) {
@@ -722,6 +795,7 @@ function createServer(opts) {
722
795
  if (!targetSlug) {
723
796
  projects.forEach(function (ctx, s) {
724
797
  if (targetSlug) return;
798
+ if (!isOrdinaryProject(s)) return;
725
799
  if (reqUser && onGetProjectAccess) {
726
800
  var access = onGetProjectAccess(s);
727
801
  if (access && !access.error && users.canAccessProject(reqUser.id, access)) {
@@ -735,7 +809,7 @@ function createServer(opts) {
735
809
  if (targetSlug) {
736
810
  try {
737
811
  var homeHtml = fs.readFileSync(path.join(publicDir, "index.html"), "utf8");
738
- homeHtml = homeHtml.replace("<body>", '<body data-home-project-slug="' + targetSlug + '">');
812
+ homeHtml = injectRootProjectSlug(homeHtml, targetSlug);
739
813
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
740
814
  res.end(homeHtml);
741
815
  } catch (e) {
@@ -937,17 +1011,24 @@ function createServer(opts) {
937
1011
 
938
1012
  // --- WebSocket ---
939
1013
  var wss = new WebSocketServer({ noServer: true });
1014
+ var homePreferencesHandler = null;
940
1015
 
941
1016
  // Slug-less /ws handler: lets a user with no projects yet load the regular
942
1017
  // app shell and create/add/clone their first one. Only knows about the
943
- // small set of bootstrap messages (browse_dir, add_project, create_project,
944
- // clone_project, ping).
1018
+ // small set of bootstrap messages, including the Home preferences required
1019
+ // to dismiss the workspace restoration screen.
945
1020
  var globalWs = serverGlobalWs.attachGlobalWs({
946
1021
  osUsers: osUsers,
947
1022
  usersModule: users,
948
1023
  onAddProject: onAddProject,
949
1024
  onCreateProject: onCreateProject,
950
1025
  onCloneProject: onCloneProject,
1026
+ onHomePreferenceMessage: function (ws, msg) {
1027
+ return homePreferencesHandler ? homePreferencesHandler.handleMessage(ws, msg) : false;
1028
+ },
1029
+ onAppMessage: function (ws, msg) {
1030
+ return handleDmMessage(ws, msg);
1031
+ },
951
1032
  });
952
1033
 
953
1034
  server.on("upgrade", function (req, socket, head) {
@@ -1182,6 +1263,7 @@ function createServer(opts) {
1182
1263
  title: title || null,
1183
1264
  icon: icon || null,
1184
1265
  projectOwnerId: projectOwnerId || null,
1266
+ projectKnowledgeId: extra.projectKnowledgeId || (worktreeMeta && worktreeMeta.projectKnowledgeId) || null,
1185
1267
  worktreeMeta: worktreeMeta || null,
1186
1268
  isMate: extra.isMate || false,
1187
1269
  mateId: extra.mateId || (extra.isMate ? path.basename(cwd) : null),
@@ -1192,6 +1274,21 @@ function createServer(opts) {
1192
1274
  return mateHandler.createReadyMateFromInterview(ws, userId, definition);
1193
1275
  },
1194
1276
  workspaceQueryService: workspaceQueryService,
1277
+ projectLogsService: projectLogsService,
1278
+ mateKnowledgeService: mateKnowledgeService,
1279
+ canAccessProjectSlug: function (userId, targetSlug) {
1280
+ var target = projects.get(targetSlug);
1281
+ if (!target) return false;
1282
+ var status = target.getStatus();
1283
+ var access;
1284
+ if (status.isMate) {
1285
+ access = { visibility: "private", ownerId: status.projectOwnerId, allowedUsers: [] };
1286
+ } else {
1287
+ if (!onGetProjectAccess) return false;
1288
+ access = onGetProjectAccess(status.parentSlug || targetSlug);
1289
+ }
1290
+ return !!(access && !access.error && users.canAccessProject(userId, access));
1291
+ },
1195
1292
  getProjectAccess: function () {
1196
1293
  return onGetProjectAccess ? onGetProjectAccess(slug) : { visibility: "public", ownerId: projectOwnerId || null };
1197
1294
  },
@@ -1361,6 +1458,10 @@ function createServer(opts) {
1361
1458
  return true;
1362
1459
  }
1363
1460
 
1461
+ function setProjectLogContextState(slug, status) {
1462
+ return projectLogsService.setContextState(slug, status);
1463
+ }
1464
+
1364
1465
  // --- DM message handler (delegated to server-dm + server-mates inline) ---
1365
1466
  var dmHandler = serverDm.attachDm({
1366
1467
  users: users,
@@ -1381,9 +1482,10 @@ function createServer(opts) {
1381
1482
  projects: projects,
1382
1483
  addProject: addProject,
1383
1484
  workspaceQueryService: workspaceQueryService,
1485
+ projectLogsService: projectLogsService,
1384
1486
  });
1385
1487
 
1386
- var homePreferencesHandler = serverHomePreferences.attachHomePreferences({
1488
+ homePreferencesHandler = serverHomePreferences.attachHomePreferences({
1387
1489
  users: users,
1388
1490
  projects: projects,
1389
1491
  });
@@ -1392,6 +1494,17 @@ function createServer(opts) {
1392
1494
  var toolsHandler = serverTools.attachTools({
1393
1495
  users: users,
1394
1496
  projects: projects,
1497
+ // The rich-Display frame listener must speak the same scheme as the app,
1498
+ // or the sandboxed frame would be blocked as mixed content under TLS.
1499
+ tlsOptions: tlsOptions,
1500
+ // Lets a Capsule turn handed to the mate seat wake the opponent Mate's
1501
+ // project, registering it first if it has never been opened.
1502
+ findMateProject: homeChatHandler.findMateProject,
1503
+ isCapsulesEnabled: function (userId) {
1504
+ if (users.isMultiUser()) return users.getCapsulesEnabled(userId);
1505
+ var daemonConfig = onGetDaemonConfig ? onGetDaemonConfig() : {};
1506
+ return daemonConfig.capsulesEnabled === true;
1507
+ },
1395
1508
  });
1396
1509
 
1397
1510
  // --- Mate handler ---
@@ -1402,13 +1515,14 @@ function createServer(opts) {
1402
1515
  }
1403
1516
 
1404
1517
  function handleDmMessage(ws, msg, routedProjectSlug) {
1405
- if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return;
1406
- if (dmHandler.handleMessage(ws, msg)) return;
1407
- if (mateHandler && mateHandler.handleMessage(ws, msg)) return;
1408
- if (emailHandler.handleMessage(ws, msg)) return;
1409
- if (homeChatHandler.handleMessage(ws, msg)) return;
1410
- if (homePreferencesHandler.handleMessage(ws, msg)) return;
1411
- if (toolsHandler.handleMessage(ws, msg)) return;
1518
+ if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
1519
+ if (dmHandler.handleMessage(ws, msg)) return true;
1520
+ if (mateHandler && mateHandler.handleMessage(ws, msg)) return true;
1521
+ if (emailHandler.handleMessage(ws, msg)) return true;
1522
+ if (homeChatHandler.handleMessage(ws, msg)) return true;
1523
+ if (homePreferencesHandler.handleMessage(ws, msg)) return true;
1524
+ if (toolsHandler.handleMessage(ws, msg)) return true;
1525
+ return false;
1412
1526
  }
1413
1527
 
1414
1528
  function removeProject(slug) {
@@ -1702,6 +1816,7 @@ function createServer(opts) {
1702
1816
  forEachProject: forEachProject,
1703
1817
  destroyProject: removeProject,
1704
1818
  destroyAll: destroyAll,
1819
+ setProjectLogContextState: setProjectLogContextState,
1705
1820
  };
1706
1821
  }
1707
1822
 
@@ -0,0 +1,38 @@
1
+ // Driver capability for a visible Driver/Split Worker pair.
2
+ //
3
+ // Model and vendor choice belongs to the user. A session does not need to
4
+ // clear a model-tier threshold to coordinate a Split Worker; runtime catalog
5
+ // validation belongs to session-pair-factory.js and applies equally to every
6
+ // requested role. The only session-level exclusion here is structural: an
7
+ // embedded terminal has no project chat surface on which to host the Driver
8
+ // controls or the paired pane.
9
+
10
+ var sessionProvenance = require("./session-provenance");
11
+
12
+ function evaluateDriverSession(session) {
13
+ if (!session) {
14
+ return { ok: false, error: "No session was bound to this request." };
15
+ }
16
+ if (session.mode === "tui") {
17
+ return {
18
+ ok: false,
19
+ error: "An embedded terminal session cannot take the Driver role.",
20
+ };
21
+ }
22
+ if (sessionProvenance.isWorker(session)) {
23
+ return {
24
+ ok: false,
25
+ error: "A Split Worker session cannot take the Driver role.",
26
+ };
27
+ }
28
+ return { ok: true, error: null };
29
+ }
30
+
31
+ function isEligibleDriverSession(session) {
32
+ return evaluateDriverSession(session).ok;
33
+ }
34
+
35
+ module.exports = {
36
+ evaluateDriverSession: evaluateDriverSession,
37
+ isEligibleDriverSession: isEligibleDriverSession,
38
+ };
@@ -0,0 +1,31 @@
1
+ // Model-tier policy for proactive Split Worker orchestration.
2
+ //
3
+ // Structural Driver eligibility remains model-agnostic. This policy only
4
+ // decides whether a model should be prompted to judge when delegation helps.
5
+
6
+ var HIGH_TIER_PATTERNS = {
7
+ claude: [/(?:^|-)fable(?:-|$)/i, /(?:^|-)opus(?:-|$)/i],
8
+ codex: [/^gpt-6(?:-|$)/i, /^gpt-[0-9]+(?:\.[0-9]+)?-sol(?:-|$)/i],
9
+ };
10
+
11
+ function resolvedModel(session, sm) {
12
+ if (!session) return "";
13
+ var vendor = session.vendor || "claude";
14
+ var defaults = sm && sm.defaultModelByVendor || {};
15
+ return session.model || defaults[vendor] || "";
16
+ }
17
+
18
+ function isHighTierDriverSession(session, sm) {
19
+ if (!session) return false;
20
+ var patterns = HIGH_TIER_PATTERNS[session.vendor || "claude"] || [];
21
+ var model = resolvedModel(session, sm);
22
+ for (var i = 0; i < patterns.length; i++) {
23
+ if (patterns[i].test(model)) return true;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ module.exports = {
29
+ isHighTierDriverSession: isHighTierDriverSession,
30
+ resolvedModel: resolvedModel,
31
+ };
@@ -2,6 +2,8 @@
2
2
  // "New session" request and which abandoned blanks are safe to sweep.
3
3
  // Pure decision logic only -- deletion/switching stays in sessions.js.
4
4
 
5
+ var sessionProvenance = require("./session-provenance");
6
+
5
7
  var BLANK_SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
6
8
  var CLAUDE_INTERRUPTED_TEXT = "[Request interrupted by user]";
7
9
 
@@ -19,6 +21,7 @@ function isBlankSession(s) {
19
21
  && !s.bookmarked
20
22
  && !s.spawn
21
23
  && !s.loop
24
+ && !sessionProvenance.isWorker(s)
22
25
  && (s.mode || "gui") !== "tui"
23
26
  && s.terminalId == null;
24
27
  }