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
@@ -7,6 +7,7 @@
7
7
  var fs = require("fs");
8
8
  var { scanWorktrees, isWorktree } = require("./worktree");
9
9
  var { daemonSync } = require("./daemon-sync");
10
+ var logContext = require("./project-log-context");
10
11
 
11
12
  // --- Worktree tracking state ---
12
13
  var worktreeRegistry = {}; // parentSlug -> [wtSlug, ...]
@@ -28,9 +29,9 @@ function isWorktreeSlug(slug) {
28
29
  * @param {string} parentIcon - icon of parent project
29
30
  * @param {string} parentOwnerId - owner user ID
30
31
  */
31
- function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId) {
32
- if (isWorktree(parentPath)) return;
33
- var worktrees = scanWorktrees(parentPath);
32
+ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, projectKnowledgeId, osUserInfo) {
33
+ if (isWorktree(parentPath, osUserInfo)) return;
34
+ var worktrees = scanWorktrees(parentPath, osUserInfo);
34
35
  if (worktrees.length === 0) return;
35
36
  if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
36
37
  for (var i = 0; i < worktrees.length; i++) {
@@ -41,13 +42,14 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
41
42
  if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
42
43
  }
43
44
  if (alreadyRegistered) continue;
44
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
45
+ var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName }, osUserInfo);
46
+ var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context);
45
47
  relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
46
48
  worktreeRegistry[parentSlug].push(wtSlug);
47
49
  console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)");
48
50
  }
49
51
  daemonSync.register(getWorktreeSyncKey(parentSlug), function () {
50
- rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId);
52
+ rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, null, projectKnowledgeId, osUserInfo);
51
53
  });
52
54
  }
53
55
 
@@ -60,11 +62,11 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
60
62
  * @param {string} parentOwnerId - owner user ID
61
63
  * @param {object} [config] - daemon config (optional, for broadcasting project count)
62
64
  */
63
- function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config) {
65
+ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId, config, projectKnowledgeId, osUserInfo) {
64
66
  if (worktreeScanning[parentSlug]) return;
65
67
  worktreeScanning[parentSlug] = true;
66
68
  try {
67
- var discovered = scanWorktrees(parentPath);
69
+ var discovered = scanWorktrees(parentPath, osUserInfo);
68
70
  var changed = false;
69
71
  var existingSlugs = worktreeRegistry[parentSlug] || [];
70
72
  var discoveredNames = {};
@@ -79,7 +81,8 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
79
81
  if (existingSlugs[ei] === wtSlug) { found = true; break; }
80
82
  }
81
83
  if (!found) {
82
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
84
+ var context = logContext.resolveWorktreeContext(wt.path, projectKnowledgeId, { branch: wt.branch || wt.dirName }, osUserInfo);
85
+ var wtMeta = Object.assign({ parentSlug: parentSlug, external: wt.external, projectKnowledgeId: projectKnowledgeId }, context);
83
86
  relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
84
87
  if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
85
88
  worktreeRegistry[parentSlug].push(wtSlug);
@@ -91,6 +94,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
91
94
  var sSlug = existingSlugs[si];
92
95
  var dirName = sSlug.split("--").slice(1).join("--");
93
96
  if (!discoveredNames[dirName]) {
97
+ if (relay.setProjectLogContextState) relay.setProjectLogContextState(sSlug, "archived");
94
98
  relay.removeProject(sSlug);
95
99
  existingSlugs.splice(si, 1);
96
100
  console.log("[daemon] Rescan: removed stale worktree:", sSlug);
@@ -112,6 +116,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
112
116
  function cleanupWorktreesForParent(relay, parentSlug) {
113
117
  var wtSlugs = worktreeRegistry[parentSlug] || [];
114
118
  for (var i = 0; i < wtSlugs.length; i++) {
119
+ if (relay.setProjectLogContextState) relay.setProjectLogContextState(wtSlugs[i], "archived");
115
120
  relay.removeProject(wtSlugs[i]);
116
121
  console.log("[daemon] Cascade removed worktree:", wtSlugs[i]);
117
122
  }
package/lib/daemon.js CHANGED
@@ -34,7 +34,9 @@ var { checkAclSupport, grantProjectAccess, revokeProjectAccess, provisionAllUser
34
34
  var usersModule = require("./users");
35
35
  var { createWorktree, removeWorktree, isWorktree } = require("./worktree");
36
36
  var mates = require("./mates");
37
+ var mateKnowledgeMigration = require("./mate-knowledge-migration");
37
38
  var { isWorktreeSlug, scanAndRegisterWorktrees, rescanWorktrees, cleanupWorktreesForParent, getFilteredRemovedProjects, registerWorktreeSlug, unregisterWorktreeSlug } = require("./daemon-projects");
39
+ var projectLogContext = require("./project-log-context");
38
40
 
39
41
  var daemonVersion = require("../package.json").version;
40
42
  var configFile = process.env.CLAY_CONFIG || process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
@@ -64,6 +66,11 @@ try {
64
66
  console.error("[daemon] ensureMultiUser threw (continuing):", e && e.message);
65
67
  }
66
68
 
69
+ if (projectLogContext.initializeProjectKnowledge(config)) {
70
+ saveConfig(config);
71
+ console.log("[daemon] Assigned stable knowledge identities to existing projects");
72
+ }
73
+
67
74
  // --- OS users mode: check required system dependencies ---
68
75
  // Supplementary-group inheritance (issue #367): default on so sessions behave
69
76
  // like a normal login; operators can disable via config / the settings toggle.
@@ -152,6 +159,39 @@ var lanIp = (function () {
152
159
  // --- Create multi-project server ---
153
160
  var listenHost = config.host || "0.0.0.0";
154
161
 
162
+ function rememberedProjectKnowledgeId(projectPath) {
163
+ var removed = config.removedProjects || [];
164
+ for (var i = removed.length - 1; i >= 0; i--) {
165
+ if (removed[i].path === projectPath && projectLogContext.validId(removed[i].projectKnowledgeId)) {
166
+ return removed[i].projectKnowledgeId;
167
+ }
168
+ }
169
+ return projectLogContext.createProjectKnowledgeId();
170
+ }
171
+
172
+ function projectRuntimeOptions(project) {
173
+ return { projectKnowledgeId: project.projectKnowledgeId };
174
+ }
175
+
176
+ function worktreeOsUserInfo(project, requestUser) {
177
+ if (!config.osUsers) return null;
178
+ var owner = project && project.ownerId ? usersModule.findUserById(project.ownerId) : null;
179
+ var executionUser = owner && owner.linuxUser ? owner : requestUser;
180
+ if (!executionUser || !executionUser.linuxUser) {
181
+ throw new Error("The project owner does not have a provisioned Linux account");
182
+ }
183
+ return osUsersMod.resolveOsUserInfo(executionUser.linuxUser);
184
+ }
185
+
186
+ function registerProjectWorktrees(project, requestUser) {
187
+ try {
188
+ var osUserInfo = worktreeOsUserInfo(project, requestUser);
189
+ scanAndRegisterWorktrees(relay, project.path, project.slug, project.icon, project.ownerId, project.projectKnowledgeId, osUserInfo);
190
+ } catch (e) {
191
+ console.error("[daemon] Failed to scan worktrees for " + project.slug + ": " + e.message);
192
+ }
193
+ }
194
+
155
195
  var relay = createServer({
156
196
  tlsOptions: tlsOptions,
157
197
  caPath: caRoot,
@@ -172,13 +212,14 @@ var relay = createServer({
172
212
  }
173
213
  var slugs = config.projects.map(function (p) { return p.slug; });
174
214
  var slug = generateSlug(absPath, slugs);
175
- relay.addProject(absPath, slug, null, null, wsUser ? wsUser.id : null);
176
215
  var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
216
+ projectEntry.projectKnowledgeId = rememberedProjectKnowledgeId(absPath);
177
217
  // The user who adds a project always becomes the owner
178
218
  if (wsUser && wsUser.id) {
179
219
  projectEntry.ownerId = wsUser.id;
180
220
  }
181
221
  config.projects.push(projectEntry);
222
+ relay.addProject(absPath, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
182
223
  // Remove from removedProjects if present
183
224
  if (config.removedProjects) {
184
225
  config.removedProjects = config.removedProjects.filter(function (rp) { return rp.path !== absPath; });
@@ -197,7 +238,7 @@ var relay = createServer({
197
238
  }
198
239
  }
199
240
  // Discover and register worktrees for the new project
200
- scanAndRegisterWorktrees(relay, absPath, slug, null, wsUser && wsUser.id && wsUser.role !== "admin" ? wsUser.id : null);
241
+ registerProjectWorktrees(projectEntry, wsUser);
201
242
  // Broadcast updated project list to all clients
202
243
  relay.broadcastAll({
203
244
  type: "projects_updated",
@@ -254,10 +295,11 @@ var relay = createServer({
254
295
  }
255
296
  // Register project - creator always becomes owner, default private
256
297
  var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
298
+ projectEntry.projectKnowledgeId = projectLogContext.createProjectKnowledgeId();
257
299
  if (wsUser && wsUser.id) {
258
300
  projectEntry.ownerId = wsUser.id;
259
301
  }
260
- relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null);
302
+ relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
261
303
  config.projects.push(projectEntry);
262
304
  saveConfig(config);
263
305
  try { syncClayrc(config.projects); } catch (e) {}
@@ -344,10 +386,11 @@ var relay = createServer({
344
386
  // Register project - creator always becomes owner
345
387
  // Creator always becomes owner, default private
346
388
  var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
389
+ projectEntry.projectKnowledgeId = projectLogContext.createProjectKnowledgeId();
347
390
  if (wsUser && wsUser.id) {
348
391
  projectEntry.ownerId = wsUser.id;
349
392
  }
350
- relay.addProject(targetDir, slug);
393
+ relay.addProject(targetDir, slug, null, null, wsUser ? wsUser.id : null, null, projectRuntimeOptions(projectEntry));
351
394
  config.projects.push(projectEntry);
352
395
  saveConfig(config);
353
396
  try { syncClayrc(config.projects); } catch (e) {}
@@ -379,11 +422,21 @@ var relay = createServer({
379
422
  if (config.projects[pi].slug === wtParent) { parentProject = config.projects[pi]; break; }
380
423
  }
381
424
  if (parentProject) {
382
- var rmResult = removeWorktree(parentProject.path, wtDirName);
425
+ var wtPath = path.join(parentProject.path, wtDirName);
426
+ var removeUser = userId ? usersModule.findUserById(userId) : null;
427
+ var removeOsUserInfo;
428
+ try {
429
+ removeOsUserInfo = worktreeOsUserInfo(parentProject, removeUser);
430
+ } catch (e) {
431
+ return { ok: false, error: e.message };
432
+ }
433
+ var contextStatus = projectLogContext.isMerged(parentProject.path, wtPath, removeOsUserInfo) ? "merged" : "archived";
434
+ var rmResult = removeWorktree(parentProject.path, wtDirName, removeOsUserInfo);
383
435
  if (!rmResult.ok) {
384
436
  console.log("[daemon] Failed to remove worktree:", slug, rmResult.error);
385
437
  return { ok: false, error: rmResult.error };
386
438
  }
439
+ relay.setProjectLogContextState(slug, contextStatus);
387
440
  }
388
441
  relay.removeProject(slug);
389
442
  unregisterWorktreeSlug(wtParent, slug);
@@ -409,6 +462,7 @@ var relay = createServer({
409
462
  title: found.title || null,
410
463
  icon: found.icon || null,
411
464
  userId: userId || null,
465
+ projectKnowledgeId: found.projectKnowledgeId || null,
412
466
  removedAt: Date.now(),
413
467
  });
414
468
  // Cap at 20 entries (oldest first)
@@ -739,6 +793,7 @@ var relay = createServer({
739
793
  inheritGroups: config.inheritGroups !== false,
740
794
  chatLayout: config.chatLayout || "channel",
741
795
  matesEnabled: config.matesEnabled === true,
796
+ capsulesEnabled: config.capsulesEnabled === true,
742
797
  pinEnabled: !!config.pinHash,
743
798
  platform: process.platform,
744
799
  hostname: os2.hostname(),
@@ -817,6 +872,13 @@ var relay = createServer({
817
872
  console.log("[daemon] Project Mate DMs:", want ? "shown" : "hidden", "(web)");
818
873
  return { ok: true, matesEnabled: want };
819
874
  },
875
+ onSetCapsulesEnabled: function (value) {
876
+ var want = value === true;
877
+ config.capsulesEnabled = want;
878
+ saveConfig(config);
879
+ console.log("[daemon] Experimental Capsules:", want ? "enabled" : "disabled", "(web)");
880
+ return { ok: true, capsulesEnabled: want };
881
+ },
820
882
  onGetToolPalettes: function () {
821
883
  return config.toolPalettes || {};
822
884
  },
@@ -967,19 +1029,30 @@ var relay = createServer({
967
1029
  if (!config.osUsers || !linuxUser) return;
968
1030
  deactivateLinuxUser(linuxUser);
969
1031
  },
970
- onCreateWorktree: function (parentSlug, branchName, dirName, baseBranch) {
1032
+ onCreateWorktree: function (parentSlug, branchName, dirName, baseBranch, requestUser) {
971
1033
  // Find the parent project
972
1034
  var parent = null;
973
1035
  for (var j = 0; j < config.projects.length; j++) {
974
1036
  if (config.projects[j].slug === parentSlug) { parent = config.projects[j]; break; }
975
1037
  }
976
1038
  if (!parent) return { ok: false, error: "Parent project not found" };
977
- if (isWorktree(parent.path)) return { ok: false, error: "Cannot create worktrees from a worktree project" };
978
- var result = createWorktree(parent.path, branchName, dirName, baseBranch);
1039
+ var osUserInfo;
1040
+ try {
1041
+ osUserInfo = worktreeOsUserInfo(parent, requestUser);
1042
+ } catch (e) {
1043
+ return { ok: false, error: e.message };
1044
+ }
1045
+ if (isWorktree(parent.path, osUserInfo)) return { ok: false, error: "Cannot create worktrees from a worktree project" };
1046
+ var baseCommit = projectLogContext.commit(parent.path, baseBranch || "HEAD", osUserInfo);
1047
+ var result = createWorktree(parent.path, branchName, dirName, baseBranch, osUserInfo);
979
1048
  if (!result.ok) return result;
980
1049
  // Register the new worktree as ephemeral project
981
1050
  var wtSlug = parentSlug + "--" + dirName;
982
- var wtMeta = { parentSlug: parentSlug, branch: branchName, external: false };
1051
+ var context = projectLogContext.resolveWorktreeContext(result.path, parent.projectKnowledgeId, {
1052
+ branch: branchName,
1053
+ baseCommit: baseCommit,
1054
+ }, osUserInfo);
1055
+ var wtMeta = Object.assign({ parentSlug: parentSlug, external: false, projectKnowledgeId: parent.projectKnowledgeId }, context);
983
1056
  relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
984
1057
  registerWorktreeSlug(parentSlug, wtSlug);
985
1058
  console.log("[daemon] Created worktree:", wtSlug, "->", result.path);
@@ -1065,13 +1138,15 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1065
1138
  }
1066
1139
  var slugs = config.projects.map(function (p) { return p.slug; });
1067
1140
  var slug = generateSlug(absPath, slugs);
1068
- relay.addProject(absPath, slug);
1069
- config.projects.push({ path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" });
1141
+ var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
1142
+ projectEntry.projectKnowledgeId = rememberedProjectKnowledgeId(absPath);
1143
+ relay.addProject(absPath, slug, null, null, null, null, projectRuntimeOptions(projectEntry));
1144
+ config.projects.push(projectEntry);
1070
1145
  saveConfig(config);
1071
1146
  try { syncClayrc(config.projects); } catch (e) {}
1072
1147
  console.log("[daemon] Added project:", slug, "→", absPath);
1073
1148
  // Discover and register worktrees for the new project
1074
- scanAndRegisterWorktrees(relay, absPath, slug, null, null);
1149
+ registerProjectWorktrees(projectEntry, null);
1075
1150
  relay.broadcastAll({
1076
1151
  type: "projects_updated",
1077
1152
  projects: relay.getProjects(),
@@ -1093,6 +1168,23 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1093
1168
  }
1094
1169
  }
1095
1170
  if (!target) return { ok: false, error: "project not found" };
1171
+ var removedProject = null;
1172
+ for (var ri = 0; ri < config.projects.length; ri++) {
1173
+ if (config.projects[ri].slug === target) { removedProject = config.projects[ri]; break; }
1174
+ }
1175
+ if (removedProject) {
1176
+ if (!config.removedProjects) config.removedProjects = [];
1177
+ config.removedProjects.push({
1178
+ path: removedProject.path,
1179
+ title: removedProject.title || null,
1180
+ icon: removedProject.icon || null,
1181
+ projectKnowledgeId: removedProject.projectKnowledgeId || null,
1182
+ removedAt: Date.now(),
1183
+ });
1184
+ if (config.removedProjects.length > 20) {
1185
+ config.removedProjects = config.removedProjects.slice(config.removedProjects.length - 20);
1186
+ }
1187
+ }
1096
1188
  relay.removeProject(target);
1097
1189
  config.projects = config.projects.filter(function (p) { return p.slug !== target; });
1098
1190
  saveConfig(config);
@@ -1283,9 +1375,9 @@ for (var i = 0; i < projects.length; i++) {
1283
1375
  var p = projects[i];
1284
1376
  if (fs.existsSync(p.path)) {
1285
1377
  console.log("[daemon] Adding project:", p.slug, "→", p.path);
1286
- relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
1378
+ relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId, null, projectRuntimeOptions(p));
1287
1379
  // Discover and register worktrees for this project
1288
- scanAndRegisterWorktrees(relay, p.path, p.slug, p.icon, p.ownerId);
1380
+ registerProjectWorktrees(p, null);
1289
1381
  } else {
1290
1382
  console.log("[daemon] Skipping missing project:", p.path);
1291
1383
  }
@@ -1294,6 +1386,27 @@ for (var i = 0; i < projects.length; i++) {
1294
1386
  // Migrate legacy mates storage before registration
1295
1387
  mates.migrateLegacyMates();
1296
1388
 
1389
+ // Import durable Mate knowledge into the Clay-wide Knowledge backend. Runs on
1390
+ // every start, imports nothing when there is nothing new, and never touches
1391
+ // the legacy files that remain the live source for existing read paths.
1392
+ mateKnowledgeMigration.runAtStartup({
1393
+ extraRoots: (function () {
1394
+ var roots = [];
1395
+ try {
1396
+ if (!usersModule.isMultiUser()) return roots;
1397
+ var all = usersModule.getAllUsers() || [];
1398
+ for (var mi = 0; mi < all.length; mi++) {
1399
+ if (!all[mi] || !all[mi].linuxUser) continue;
1400
+ roots.push({
1401
+ userId: all[mi].id,
1402
+ matesRoot: mates.resolveMatesRoot({ userId: all[mi].id, multiUser: true, linuxUser: all[mi].linuxUser }),
1403
+ });
1404
+ }
1405
+ } catch (e) {}
1406
+ return roots;
1407
+ })(),
1408
+ });
1409
+
1297
1410
  // Register existing mates as projects
1298
1411
  if (usersModule.isMultiUser()) {
1299
1412
  // Multi-user mode: iterate over all users and load each user's mates
package/lib/git-cli.js CHANGED
@@ -6,6 +6,7 @@ var { execFile, execFileSync } = require("child_process");
6
6
  var { wrapSpawnAsUser } = require("./os-users");
7
7
 
8
8
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
9
+ var MAX_REMOTE_LABEL = 64;
9
10
 
10
11
  function gitEnvironment(osUserInfo) {
11
12
  var overrides = { GIT_TERMINAL_PROMPT: "0" };
@@ -157,6 +158,24 @@ function normalizeGitPath(cwd, value) {
157
158
  return path.normalize(path.isAbsolute(value) ? value : path.resolve(cwd, value));
158
159
  }
159
160
 
161
+ // Whether this checkout is a linked worktree. The two directories have to be
162
+ // compared through realpath: git answers --absolute-git-dir already resolved
163
+ // but --git-common-dir relative to cwd, so on any platform where the working
164
+ // directory sits behind a symlink (macOS /var -> /private/var, for one) a
165
+ // plain string compare reports every ordinary repository as a worktree.
166
+ function isLinkedWorktree(gitDir, commonDir) {
167
+ if (!gitDir || !commonDir) return false;
168
+ return realGitPath(gitDir) !== realGitPath(commonDir);
169
+ }
170
+
171
+ function realGitPath(value) {
172
+ try {
173
+ return fs.realpathSync(value);
174
+ } catch (e) {
175
+ return value;
176
+ }
177
+ }
178
+
160
179
  function parseWorktrees(raw) {
161
180
  var blocks = String(raw || "").trim().split(/\n\n+/);
162
181
  var result = [];
@@ -219,7 +238,7 @@ function getStatus(cwd, osUserInfo) {
219
238
  origin: origin,
220
239
  gitDir: gitDir,
221
240
  commonDir: commonDir,
222
- isWorktree: !!(gitDir && commonDir && gitDir !== commonDir),
241
+ isWorktree: isLinkedWorktree(gitDir, commonDir),
223
242
  mainWorktree: worktrees.length > 0 ? worktrees[0].path : root,
224
243
  worktrees: worktrees,
225
244
  files: parsed.files,
@@ -227,6 +246,78 @@ function getStatus(cwd, osUserInfo) {
227
246
  };
228
247
  }
229
248
 
249
+ // A bounded, credential-free remote label. The raw remote URL can carry an
250
+ // embedded token, so the userinfo segment is dropped before the label is
251
+ // built rather than after.
252
+ function shortRemoteLabel(origin) {
253
+ if (!origin) return null;
254
+ var label = String(origin).trim()
255
+ .replace(/^git@([^:]+):/, "$1/")
256
+ .replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
257
+ .replace(/^[^@/]*@/, "")
258
+ .replace(/\.git$/, "")
259
+ .replace(/\/+$/, "");
260
+ if (label.length > MAX_REMOTE_LABEL) label = label.slice(0, MAX_REMOTE_LABEL - 1) + "…";
261
+ return label || null;
262
+ }
263
+
264
+ function countChangedFiles(files) {
265
+ var counts = { changed: files.length, staged: 0, unstaged: 0, untracked: 0, conflicted: 0 };
266
+ for (var i = 0; i < files.length; i++) {
267
+ var file = files[i];
268
+ if (file.conflicted) counts.conflicted++;
269
+ if (file.untracked) counts.untracked++;
270
+ if (file.staged) counts.staged++;
271
+ if (file.unstaged && !file.untracked) counts.unstaged++;
272
+ }
273
+ return counts;
274
+ }
275
+
276
+ // Cheap always-visible repository placard data. Deliberately narrower than
277
+ // getStatus: three git invocations instead of six, no per-file payload, no
278
+ // session attribution, and no filesystem path in the result. Identity is
279
+ // derived here so the client never resolves or renders a repository path.
280
+ function getSummary(cwd, osUserInfo) {
281
+ var probe;
282
+ try {
283
+ probe = runGitSync(cwd, [
284
+ "rev-parse", "--is-inside-work-tree", "--show-toplevel",
285
+ "--absolute-git-dir", "--git-common-dir",
286
+ ], null, osUserInfo);
287
+ } catch (e) {
288
+ return { isRepository: false };
289
+ }
290
+ var lines = String(probe || "").trim().split("\n");
291
+ if (String(lines[0] || "").trim() !== "true" || !lines[1]) return { isRepository: false };
292
+
293
+ var root = String(lines[1]).trim();
294
+ var gitDir = lines[2] ? normalizeGitPath(cwd, String(lines[2]).trim()) : null;
295
+ var commonDir = lines[3] ? normalizeGitPath(cwd, String(lines[3]).trim()) : null;
296
+ var parsed = parsePorcelainV2(runGitSync(root, ["status", "--porcelain=v2", "--branch", "-z"], null, osUserInfo));
297
+ var origin = null;
298
+ try { origin = runGitSync(cwd, ["remote", "get-url", "origin"], null, osUserInfo).trim() || null; } catch (e) {}
299
+
300
+ var detached = parsed.branch === "(detached)";
301
+ var counts = countChangedFiles(parsed.files);
302
+ return {
303
+ isRepository: true,
304
+ name: path.basename(root),
305
+ branch: detached ? null : parsed.branch,
306
+ detached: detached,
307
+ shortOid: parsed.oid && parsed.oid !== "(initial)" ? String(parsed.oid).slice(0, 8) : null,
308
+ isWorktree: isLinkedWorktree(gitDir, commonDir),
309
+ hasUpstream: !!parsed.upstream,
310
+ ahead: parsed.ahead,
311
+ behind: parsed.behind,
312
+ changed: counts.changed,
313
+ staged: counts.staged,
314
+ unstaged: counts.unstaged,
315
+ untracked: counts.untracked,
316
+ conflicted: counts.conflicted,
317
+ remote: shortRemoteLabel(origin),
318
+ };
319
+ }
320
+
230
321
  function resolveChangedPaths(status, requestedPaths) {
231
322
  if (!Array.isArray(requestedPaths) || requestedPaths.length === 0 || requestedPaths.length > 200) {
232
323
  throw new Error("Select at least one changed file");
@@ -363,8 +454,11 @@ module.exports = {
363
454
  getFileAtCommit: getFileAtCommit,
364
455
  getFileDiff: getFileDiff,
365
456
  getStatus: getStatus,
457
+ getSummary: getSummary,
366
458
  parsePorcelainV2: parsePorcelainV2,
367
459
  parseWorktrees: parseWorktrees,
368
460
  readWorkingTreeFile: readWorkingTreeFile,
461
+ runGitSync: runGitSync,
369
462
  runAction: runAction,
463
+ shortRemoteLabel: shortRemoteLabel,
370
464
  };