clay-server 3.8.1 → 4.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/background-task-timing.js +105 -0
  3. package/lib/builtin-mates.js +6 -6
  4. package/lib/claude-hook-installer.js +15 -0
  5. package/lib/clay-history-mcp-server.js +62 -297
  6. package/lib/daemon.js +23 -1
  7. package/lib/debate-model-selection.js +119 -0
  8. package/lib/home-debate-tool-policy.js +164 -0
  9. package/lib/home-response-notifications.js +26 -0
  10. package/lib/knowledge-import.js +436 -0
  11. package/lib/knowledge-record-store.js +188 -0
  12. package/lib/knowledge-search.js +224 -0
  13. package/lib/mate-creation-mcp-server.js +55 -0
  14. package/lib/mate-knowledge-mcp-server.js +151 -0
  15. package/lib/mate-knowledge-migration.js +283 -0
  16. package/lib/mate-knowledge-service.js +488 -0
  17. package/lib/mate-knowledge-sync.js +482 -0
  18. package/lib/mate-ready-creation.js +95 -0
  19. package/lib/mates.js +2 -0
  20. package/lib/notes.js +2 -1
  21. package/lib/project-ask-user.js +64 -0
  22. package/lib/project-capsule-catalog.js +101 -0
  23. package/lib/project-connection.js +17 -0
  24. package/lib/project-debate-proposal.js +133 -17
  25. package/lib/project-debate.js +214 -110
  26. package/lib/project-delegated-follow-up.js +84 -0
  27. package/lib/project-delegated-session.js +53 -0
  28. package/lib/project-filesystem.js +7 -9
  29. package/lib/project-knowledge.js +23 -0
  30. package/lib/project-logs-comments.js +137 -0
  31. package/lib/project-logs-mcp-server.js +290 -0
  32. package/lib/project-logs-query.js +167 -0
  33. package/lib/project-logs-root.js +67 -0
  34. package/lib/project-logs-schema.js +259 -0
  35. package/lib/project-logs-service.js +357 -0
  36. package/lib/project-logs-snapshot.js +202 -0
  37. package/lib/project-logs-store.js +498 -0
  38. package/lib/project-logs-versioning.js +128 -0
  39. package/lib/project-logs.js +347 -0
  40. package/lib/project-mate-creation-proposal.js +120 -0
  41. package/lib/project-mate-interaction.js +14 -0
  42. package/lib/project-mate-knowledge.js +110 -0
  43. package/lib/project-memory.js +36 -7
  44. package/lib/project-models.js +157 -32
  45. package/lib/project-session-notes.js +2 -2
  46. package/lib/project-session-pair.js +8 -8
  47. package/lib/project-sessions.js +66 -51
  48. package/lib/project-vendor-login.js +414 -0
  49. package/lib/project-worker-proposal.js +25 -25
  50. package/lib/project-workspace-query.js +144 -0
  51. package/lib/project.js +433 -102
  52. package/lib/public/app.js +109 -48
  53. package/lib/public/css/avatar-imprints.css +0 -1
  54. package/lib/public/css/base.css +4 -0
  55. package/lib/public/css/capsule-ui.css +389 -0
  56. package/lib/public/css/command-palette.css +306 -6
  57. package/lib/public/css/home-capsule-library.css +243 -0
  58. package/lib/public/css/home-capsule-source.css +192 -0
  59. package/lib/public/css/home-debate-live.css +146 -0
  60. package/lib/public/css/home-debate-planning.css +125 -0
  61. package/lib/public/css/home-debates-archive.css +281 -0
  62. package/lib/public/css/home-hub.css +896 -405
  63. package/lib/public/css/home-mate-creation.css +34 -0
  64. package/lib/public/css/home-mate-model-picker.css +131 -0
  65. package/lib/public/css/home-mate-settings.css +344 -0
  66. package/lib/public/css/home-session-actions.css +265 -0
  67. package/lib/public/css/home-sidebar.css +667 -0
  68. package/lib/public/css/icon-strip.css +1 -2
  69. package/lib/public/css/input.css +79 -25
  70. package/lib/public/css/mates.css +7 -34
  71. package/lib/public/css/pane.css +1 -1
  72. package/lib/public/css/playbook.css +0 -80
  73. package/lib/public/css/project-logs.css +234 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/sticky-notes.css +53 -117
  76. package/lib/public/css/tui-attention.css +0 -44
  77. package/lib/public/css/workspace-assignment.css +45 -0
  78. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  79. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  80. package/lib/public/index.html +96 -74
  81. package/lib/public/modules/app-connection.js +100 -5
  82. package/lib/public/modules/app-dm.js +43 -160
  83. package/lib/public/modules/app-home-hub.js +328 -585
  84. package/lib/public/modules/app-message-cards.js +242 -0
  85. package/lib/public/modules/app-message-router.js +84 -0
  86. package/lib/public/modules/app-messages.js +124 -38
  87. package/lib/public/modules/app-notifications.js +8 -119
  88. package/lib/public/modules/app-panels.js +43 -2
  89. package/lib/public/modules/app-projects.js +9 -1
  90. package/lib/public/modules/app-rendering.js +21 -602
  91. package/lib/public/modules/avatar-imprint.js +11 -8
  92. package/lib/public/modules/background-tasks-ui.js +139 -16
  93. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  94. package/lib/public/modules/chat-render-runtime.js +198 -0
  95. package/lib/public/modules/command-palette.js +225 -436
  96. package/lib/public/modules/dm-render.js +82 -0
  97. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  98. package/lib/public/modules/home-capsule-library.js +146 -0
  99. package/lib/public/modules/home-capsule-source.js +383 -0
  100. package/lib/public/modules/home-chat-identity.js +35 -0
  101. package/lib/public/modules/home-chat-scroll.js +77 -0
  102. package/lib/public/modules/home-chat-stream-state.js +34 -0
  103. package/lib/public/modules/home-composer-focus.js +25 -0
  104. package/lib/public/modules/home-conversations-sheet.js +179 -0
  105. package/lib/public/modules/home-debate-activity.js +48 -0
  106. package/lib/public/modules/home-debate-controls.js +295 -0
  107. package/lib/public/modules/home-debate-launch.js +41 -0
  108. package/lib/public/modules/home-debate-live.js +284 -0
  109. package/lib/public/modules/home-debate-models.js +73 -0
  110. package/lib/public/modules/home-debate-planning.js +335 -0
  111. package/lib/public/modules/home-debates-archive.js +279 -0
  112. package/lib/public/modules/home-dock-resize.js +74 -0
  113. package/lib/public/modules/home-dock.js +446 -0
  114. package/lib/public/modules/home-mate-chat.js +496 -0
  115. package/lib/public/modules/home-mate-creation.js +109 -0
  116. package/lib/public/modules/home-mate-model-picker.js +281 -0
  117. package/lib/public/modules/home-mate-selection.js +38 -0
  118. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  119. package/lib/public/modules/home-mate-settings.js +383 -0
  120. package/lib/public/modules/home-session-actions.js +373 -0
  121. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  122. package/lib/public/modules/home-shell.js +9 -0
  123. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  124. package/lib/public/modules/home-sidebar.js +165 -0
  125. package/lib/public/modules/home-sub-surface.js +19 -0
  126. package/lib/public/modules/home-surface-boot.js +68 -0
  127. package/lib/public/modules/home-surface.js +141 -0
  128. package/lib/public/modules/home-tools.js +271 -0
  129. package/lib/public/modules/markdown.js +50 -2
  130. package/lib/public/modules/mate-management.js +86 -0
  131. package/lib/public/modules/mate-sidebar.js +0 -8
  132. package/lib/public/modules/mate-wizard.js +0 -7
  133. package/lib/public/modules/mention.js +17 -19
  134. package/lib/public/modules/pane-bridge.js +9 -0
  135. package/lib/public/modules/profile.js +4 -5
  136. package/lib/public/modules/project-logs-ambient.js +238 -0
  137. package/lib/public/modules/project-logs-render.js +359 -0
  138. package/lib/public/modules/project-logs.js +328 -0
  139. package/lib/public/modules/project-settings.js +34 -28
  140. package/lib/public/modules/project-switcher.js +11 -5
  141. package/lib/public/modules/project-workspace-assignment.js +23 -0
  142. package/lib/public/modules/scheduler.js +17 -3
  143. package/lib/public/modules/search-clay-chat.js +402 -0
  144. package/lib/public/modules/server-settings.js +14 -12
  145. package/lib/public/modules/session-actions.js +1 -1
  146. package/lib/public/modules/sidebar-mates.js +56 -552
  147. package/lib/public/modules/sidebar-mobile.js +12 -73
  148. package/lib/public/modules/sidebar-presence.js +37 -0
  149. package/lib/public/modules/sidebar-sessions.js +25 -1
  150. package/lib/public/modules/split-pair-ui.js +13 -13
  151. package/lib/public/modules/split-view.js +7 -0
  152. package/lib/public/modules/sticky-notes.js +106 -53
  153. package/lib/public/modules/tool-input-composition.js +48 -0
  154. package/lib/public/modules/tool-llm-status.js +167 -0
  155. package/lib/public/modules/tool-palette.js +76 -4
  156. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  157. package/lib/public/modules/tool-renderer-chart.js +158 -0
  158. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  159. package/lib/public/modules/tool-renderer.js +464 -0
  160. package/lib/public/modules/tool-runtime.js +309 -0
  161. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  162. package/lib/public/modules/tool-ui-tree.js +21 -0
  163. package/lib/public/modules/tui-attention.js +10 -0
  164. package/lib/public/modules/user-settings.js +16 -23
  165. package/lib/public/modules/vendor-login.js +287 -0
  166. package/lib/public/modules/worker-proposal.js +2 -2
  167. package/lib/public/modules/workspace-assignment-card.js +149 -0
  168. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  169. package/lib/public/style.css +14 -2
  170. package/lib/runtime-env.js +65 -0
  171. package/lib/sdk-bridge.js +120 -32
  172. package/lib/sdk-message-processor.js +34 -13
  173. package/lib/server-home-capsule-creation.js +36 -0
  174. package/lib/server-home-chat-events.js +287 -0
  175. package/lib/server-home-chat.js +518 -0
  176. package/lib/server-home-clay-entry.js +65 -0
  177. package/lib/server-home-clay-session-links.js +26 -0
  178. package/lib/server-home-debate-planning.js +214 -0
  179. package/lib/server-home-debates.js +137 -0
  180. package/lib/server-home-mate-creation.js +261 -0
  181. package/lib/server-home-models.js +241 -0
  182. package/lib/server-home-preferences.js +61 -0
  183. package/lib/server-mates.js +37 -0
  184. package/lib/server-tools.js +377 -0
  185. package/lib/server.js +166 -59
  186. package/lib/session-pair-mcp-server.js +6 -6
  187. package/lib/sessions.js +22 -4
  188. package/lib/tool-capsule-source.js +142 -0
  189. package/lib/tool-control-mcp-server.js +126 -0
  190. package/lib/tool-llm.js +48 -0
  191. package/lib/tool-storage.js +77 -0
  192. package/lib/tool-ui-spec-advanced.js +82 -0
  193. package/lib/tool-ui-spec.js +261 -0
  194. package/lib/tools-registry.js +286 -0
  195. package/lib/users-home-dock-preferences.js +77 -0
  196. package/lib/users-home-surface-preferences.js +99 -0
  197. package/lib/users-preferences.js +4 -7
  198. package/lib/users.js +12 -0
  199. package/lib/whats-new-content.js +3 -3
  200. package/lib/workspace-assignment-service.js +420 -0
  201. package/lib/workspace-query-mcp-server.js +154 -0
  202. package/lib/workspace-query-service.js +492 -0
  203. package/lib/ws-schema.js +107 -12
  204. package/lib/yoke/adapters/acp.js +2 -1
  205. package/lib/yoke/adapters/antigravity.js +2 -1
  206. package/lib/yoke/adapters/claude-worker.js +11 -4
  207. package/lib/yoke/adapters/claude.js +141 -46
  208. package/lib/yoke/adapters/codex.js +79 -11
  209. package/lib/yoke/adapters/kiro.js +2 -1
  210. package/lib/yoke/claude-user-input.js +38 -0
  211. package/lib/yoke/codex-app-server.js +20 -2
  212. package/lib/yoke/codex-background-tasks.js +8 -2
  213. package/lib/yoke/codex-user-input.js +64 -0
  214. package/lib/yoke/complete-once.js +164 -0
  215. package/lib/yoke/index.js +5 -1
  216. package/lib/yoke/interface.js +19 -0
  217. package/lib/yoke/mcp-bridge-server.js +2 -6
  218. package/lib/yoke/user-input.js +254 -0
  219. package/package.json +4 -3
  220. package/lib/public/css/home-chat.css +0 -380
  221. package/lib/public/modules/home-chat.js +0 -438
  222. package/lib/server-clay-home.js +0 -245
@@ -0,0 +1,420 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var crypto = require("crypto");
4
+
5
+ var ACTIVE = { proposed: true, starting: true, running: true };
6
+ var MAX_RUNNING = 3;
7
+
8
+ function clean(value, max) {
9
+ if (typeof value !== "string") return "";
10
+ return value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
11
+ }
12
+
13
+ function userStorageKey(userId) {
14
+ return crypto.createHash("sha256").update(String(userId || "_single_user")).digest("hex").slice(0, 24) + ".json";
15
+ }
16
+
17
+ function attachWorkspaceAssignmentService(ctx) {
18
+ var storageDir = ctx.storageDir || null;
19
+ var records = [];
20
+ var unsubscribers = {};
21
+ var pendingRestartReplay = {};
22
+
23
+ function recordFile(userId) {
24
+ return storageDir ? path.join(storageDir, userStorageKey(userId)) : null;
25
+ }
26
+
27
+ function saveUser(userId) {
28
+ var file = recordFile(userId);
29
+ if (!file) return;
30
+ fs.mkdirSync(path.dirname(file), { recursive: true });
31
+ var owned = records.filter(function (record) { return record.userId === (userId || null); });
32
+ var temp = file + ".tmp." + process.pid;
33
+ fs.writeFileSync(temp, JSON.stringify(owned, null, 2), { mode: 0o600 });
34
+ fs.renameSync(temp, file);
35
+ }
36
+
37
+ function load() {
38
+ if (!storageDir) return;
39
+ var files = [];
40
+ try { files = fs.readdirSync(storageDir); } catch (e) { return; }
41
+ for (var i = 0; i < files.length; i++) {
42
+ if (!files[i].endsWith(".json") || files[i].indexOf(".tmp.") !== -1) continue;
43
+ try {
44
+ var loaded = JSON.parse(fs.readFileSync(path.join(storageDir, files[i]), "utf8"));
45
+ if (Array.isArray(loaded)) records = records.concat(loaded);
46
+ } catch (e) {}
47
+ }
48
+ var changedUsers = {};
49
+ for (var j = 0; j < records.length; j++) {
50
+ if (records[j].status !== "starting" && records[j].status !== "running") continue;
51
+ records[j].status = "interrupted";
52
+ records[j].error = "Clay restarted before this assignment completed.";
53
+ records[j].updatedAt = Date.now();
54
+ pendingRestartReplay[records[j].assignmentId] = true;
55
+ changedUsers[String(records[j].userId || "_single_user")] = records[j].userId || null;
56
+ }
57
+ var keys = Object.keys(changedUsers);
58
+ for (var k = 0; k < keys.length; k++) saveUser(changedUsers[keys[k]]);
59
+ }
60
+
61
+ function sourceKey(principal) {
62
+ return String(principal.userId || "_single_user") + ":" + principal.sourceProjectSlug + ":" + principal.sourceSessionRef;
63
+ }
64
+
65
+ function normalizePrincipal(principal) {
66
+ if (!principal || !principal.sourceProjectSlug) return null;
67
+ var project = ctx.getProjects().get(principal.sourceProjectSlug);
68
+ var manager = project && project.getSessionManager ? project.getSessionManager() : null;
69
+ var session = manager && manager.sessions ? manager.sessions.get(principal.sourceSessionId) : null;
70
+ if (!session && manager && manager.sessions && principal.sourceSessionRef) {
71
+ manager.sessions.forEach(function (candidate) {
72
+ if (!session && ctx.sessionRef(principal.sourceProjectSlug, candidate) === principal.sourceSessionRef) session = candidate;
73
+ });
74
+ }
75
+ if (!session) return null;
76
+ return Object.assign({}, principal, {
77
+ sourceSessionId: session.localId,
78
+ sourceSessionRef: ctx.sessionRef(principal.sourceProjectSlug, session),
79
+ });
80
+ }
81
+
82
+ function publicRecord(record) {
83
+ return {
84
+ assignmentId: record.assignmentId,
85
+ delivery: record.delivery || "new_session",
86
+ projectSlug: record.targetProjectSlug,
87
+ projectTitle: record.targetProjectTitle,
88
+ title: record.title,
89
+ task: record.task,
90
+ status: record.status,
91
+ sourceProjectSlug: record.sourceProjectSlug,
92
+ sourceSessionRef: record.sourceSessionRef,
93
+ targetSessionRef: record.targetSessionRef || null,
94
+ targetSessionTitle: record.targetSessionTitle || null,
95
+ resultSummary: record.resultSummary || "",
96
+ error: record.error || "",
97
+ createdAt: record.createdAt,
98
+ updatedAt: record.updatedAt,
99
+ };
100
+ }
101
+
102
+ function ownsProject(record, status) {
103
+ if (!status || status.isWorktree || status.isMate) return false;
104
+ if (ctx.isMultiUser()) return !!record.userId && status.projectOwnerId === record.userId;
105
+ return !status.projectOwnerId || (!!record.singleUserOwnerId && status.projectOwnerId === record.singleUserOwnerId);
106
+ }
107
+
108
+ function exactTarget(record) {
109
+ var project = ctx.getProjects().get(record.targetProjectSlug);
110
+ if (!project || !ownsProject(record, project.getStatus())) return null;
111
+ return project;
112
+ }
113
+
114
+ function exactSource(record) {
115
+ var project = ctx.getProjects().get(record.sourceProjectSlug);
116
+ if (!project) return null;
117
+ var status = project.getStatus();
118
+ if (!status || status.isMate !== true || status.mateId !== record.sourceMateId) return null;
119
+ if (ctx.isMultiUser() && status.projectOwnerId !== record.userId) return null;
120
+ var manager = project.getSessionManager();
121
+ var session = null;
122
+ if (manager && manager.sessions) {
123
+ manager.sessions.forEach(function (candidate) {
124
+ if (!session && ctx.sessionRef(record.sourceProjectSlug, candidate) === record.sourceSessionRef) session = candidate;
125
+ });
126
+ }
127
+ if (!session) return null;
128
+ if (ctx.isMultiUser() && session.ownerId !== record.userId) return null;
129
+ if (!ctx.isMultiUser() && session.ownerId && session.ownerId !== record.singleUserOwnerId) return null;
130
+ return { project: project, manager: manager, session: session };
131
+ }
132
+
133
+ function emit(record, eventType) {
134
+ var source = exactSource(record);
135
+ if (!source) return;
136
+ source.manager.sendAndRecord(source.session, {
137
+ type: eventType || "project_assignment_status",
138
+ assignment: publicRecord(record),
139
+ requestId: record.sourceRequestId || null,
140
+ });
141
+ }
142
+
143
+ function reconcileProject(projectSlug) {
144
+ for (var i = 0; i < records.length; i++) {
145
+ if (!pendingRestartReplay[records[i].assignmentId] || records[i].sourceProjectSlug !== projectSlug) continue;
146
+ if (!exactSource(records[i])) continue;
147
+ emit(records[i]);
148
+ delete pendingRestartReplay[records[i].assignmentId];
149
+ }
150
+ }
151
+
152
+ function propose(principal, args) {
153
+ principal = normalizePrincipal(principal);
154
+ if (!principal || !principal.sourceSessionRef) throw new Error("Project assignments require an exact session-bound Mate query.");
155
+ var projectSlug = clean(args.projectSlug, 120);
156
+ var title = clean(args.title, 160);
157
+ var task = clean(args.task, 12000);
158
+ if (!projectSlug || !title || !task) throw new Error("projectSlug, title, and task are required.");
159
+ var candidate = {
160
+ userId: principal.userId || null,
161
+ singleUserOwnerId: principal.singleUserOwnerId || null,
162
+ targetProjectSlug: projectSlug,
163
+ };
164
+ var target = exactTarget(candidate);
165
+ if (!target) throw new Error("Target project not found in the owned workspace.");
166
+ var key = sourceKey(principal);
167
+ var idempotency = crypto.createHash("sha256").update(key + "\u0000" + projectSlug + "\u0000" + title + "\u0000" + task).digest("hex");
168
+ for (var i = 0; i < records.length; i++) {
169
+ if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
170
+ if (records[i].sourceKey === key && ACTIVE[records[i].status]) throw new Error("This conversation already has an active assignment.");
171
+ }
172
+ var now = Date.now();
173
+ var targetStatus = target.getStatus();
174
+ var record = {
175
+ assignmentId: "assignment_" + crypto.randomUUID(),
176
+ idempotencyKey: idempotency,
177
+ sourceKey: key,
178
+ userId: principal.userId || null,
179
+ singleUserOwnerId: principal.singleUserOwnerId || null,
180
+ sourceMateId: principal.mateId,
181
+ sourceProjectSlug: principal.sourceProjectSlug,
182
+ sourceSessionRef: principal.sourceSessionRef,
183
+ sourceRequestId: principal.sourceRequestId || null,
184
+ targetProjectSlug: projectSlug,
185
+ targetProjectTitle: clean(targetStatus.title || targetStatus.project || projectSlug, 160),
186
+ title: title,
187
+ task: task,
188
+ delivery: "new_session",
189
+ status: "proposed",
190
+ createdAt: now,
191
+ updatedAt: now,
192
+ };
193
+ if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
194
+ records.push(record);
195
+ saveUser(record.userId);
196
+ emit(record, "project_assignment_proposal");
197
+ return publicRecord(record);
198
+ }
199
+
200
+ function proposeFollowUp(principal, args) {
201
+ principal = normalizePrincipal(principal);
202
+ if (!principal || !principal.sourceSessionRef) throw new Error("Project follow-ups require an exact session-bound Mate query.");
203
+ var projectSlug = clean(args.projectSlug, 120);
204
+ var targetSessionRef = clean(args.targetSessionRef, 100);
205
+ var title = clean(args.title, 160);
206
+ var task = clean(args.task, 12000);
207
+ if (!projectSlug || !targetSessionRef || !title || !task) throw new Error("projectSlug, targetSessionRef, title, and task are required.");
208
+ var key = sourceKey(principal);
209
+ var idempotency = crypto.createHash("sha256").update(key + "\u0000follow_up\u0000" + projectSlug + "\u0000" + targetSessionRef + "\u0000" + title + "\u0000" + task).digest("hex");
210
+ for (var i = 0; i < records.length; i++) {
211
+ if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
212
+ if (records[i].sourceKey === key && ACTIVE[records[i].status]) throw new Error("This conversation already has an active assignment.");
213
+ }
214
+ var candidate = { userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null, targetProjectSlug: projectSlug };
215
+ var target = exactTarget(candidate);
216
+ if (!target || typeof target.inspectDelegatedFollowUp !== "function") throw new Error("Target project is unavailable for follow-ups.");
217
+ var inspected = target.inspectDelegatedFollowUp({ userId: principal.userId }, targetSessionRef);
218
+ var now = Date.now();
219
+ var targetStatus = target.getStatus();
220
+ var record = {
221
+ assignmentId: "assignment_" + crypto.randomUUID(), idempotencyKey: idempotency, sourceKey: key,
222
+ userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null,
223
+ sourceMateId: principal.mateId, sourceProjectSlug: principal.sourceProjectSlug,
224
+ sourceSessionRef: principal.sourceSessionRef, sourceRequestId: principal.sourceRequestId || null,
225
+ targetProjectSlug: projectSlug, targetProjectTitle: clean(targetStatus.title || targetStatus.project || projectSlug, 160),
226
+ targetSessionRef: inspected.sessionRef, targetSessionTitle: clean(inspected.title || "Existing session", 160),
227
+ title: title, task: task, delivery: "follow_up", status: "proposed", createdAt: now, updatedAt: now,
228
+ };
229
+ if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
230
+ records.push(record);
231
+ saveUser(record.userId);
232
+ emit(record, "project_assignment_proposal");
233
+ return publicRecord(record);
234
+ }
235
+
236
+ function findOwned(userId, assignmentId) {
237
+ for (var i = 0; i < records.length; i++) {
238
+ if (records[i].assignmentId === assignmentId && records[i].userId === (userId || null)) return records[i];
239
+ }
240
+ return null;
241
+ }
242
+
243
+ function responseUserId(ws) {
244
+ return ctx.isMultiUser() && ws && ws._clayUser ? ws._clayUser.id : null;
245
+ }
246
+
247
+ function summarize(session) {
248
+ var text = "";
249
+ for (var i = (session && session.history || []).length - 1; i >= 0; i--) {
250
+ if (session.history[i].type === "delta") text = session.history[i].text + text;
251
+ else if (text && (session.history[i].type === "user_message" || session.history[i].type === "delegated_work" || session.history[i].type === "delegated_follow_up")) break;
252
+ }
253
+ return clean(text, 1200);
254
+ }
255
+
256
+ function settle(record, status, session, error) {
257
+ if (!ACTIVE[record.status]) return publicRecord(record);
258
+ record.status = status;
259
+ record.resultSummary = status === "completed" ? summarize(session) : "";
260
+ record.error = clean(error || "", 800);
261
+ record.updatedAt = Date.now();
262
+ if (unsubscribers[record.assignmentId]) unsubscribers[record.assignmentId]();
263
+ delete unsubscribers[record.assignmentId];
264
+ saveUser(record.userId);
265
+ emit(record);
266
+ return publicRecord(record);
267
+ }
268
+
269
+ function watch(record, manager, session) {
270
+ var unsubscribe = manager.subscribeSession(session.localId, function (event) {
271
+ if (event.type === "session_id") {
272
+ record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
273
+ record.updatedAt = Date.now();
274
+ saveUser(record.userId);
275
+ emit(record);
276
+ } else if (event.type === "error") {
277
+ settle(record, "failed", session, event.text || "Assignment failed.");
278
+ } else if (event.type === "done" || event.type === "result") {
279
+ settle(record, session._lastTurnInterrupted ? "interrupted" : "completed", session, "");
280
+ }
281
+ });
282
+ if (unsubscribe) unsubscribers[record.assignmentId] = unsubscribe;
283
+ }
284
+
285
+ async function approve(record) {
286
+ if (record.status !== "proposed") return publicRecord(record);
287
+ var running = records.filter(function (item) {
288
+ return item.userId === record.userId && (item.status === "starting" || item.status === "running");
289
+ }).length;
290
+ if (running >= MAX_RUNNING) throw new Error("Too many assignments are already running.");
291
+ if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
292
+ var target = exactTarget(record);
293
+ var followUp = record.delivery === "follow_up";
294
+ if (!target || (followUp ? typeof target.dispatchDelegatedFollowUp !== "function" : typeof target.createDelegatedSession !== "function")) throw new Error("Target project is no longer available.");
295
+ record.decision = "approve";
296
+ record.status = "starting";
297
+ record.updatedAt = Date.now();
298
+ saveUser(record.userId);
299
+ emit(record);
300
+ try {
301
+ var metadata = {
302
+ assignmentId: record.assignmentId,
303
+ title: record.title,
304
+ sourceMateId: record.sourceMateId,
305
+ sourceProjectSlug: record.sourceProjectSlug,
306
+ sourceSessionRef: record.sourceSessionRef,
307
+ };
308
+ var session;
309
+ if (followUp) {
310
+ session = await target.dispatchDelegatedFollowUp({ userId: record.userId }, record.targetSessionRef, record.task, metadata, function (existing) { watch(record, target.getSessionManager(), existing); });
311
+ } else {
312
+ session = await target.createDelegatedSession({ userId: record.userId }, record.task, metadata, function (created) { watch(record, target.getSessionManager(), created); });
313
+ }
314
+ if (record.status !== "starting") return publicRecord(record);
315
+ record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
316
+ record.targetSessionId = session.localId;
317
+ record.status = "running";
318
+ record.updatedAt = Date.now();
319
+ saveUser(record.userId);
320
+ emit(record);
321
+ } catch (e) {
322
+ settle(record, "failed", null, e.message || String(e));
323
+ }
324
+ return publicRecord(record);
325
+ }
326
+
327
+ function cancel(record) {
328
+ if (record.status !== "proposed") return publicRecord(record);
329
+ record.decision = "cancel";
330
+ record.status = "cancelled";
331
+ record.updatedAt = Date.now();
332
+ saveUser(record.userId);
333
+ emit(record);
334
+ return publicRecord(record);
335
+ }
336
+
337
+ function getStatus(principal, assignmentId) {
338
+ principal = normalizePrincipal(principal);
339
+ if (!principal) throw new Error("Assignment not found for this conversation.");
340
+ var record = findOwned(principal.userId, assignmentId);
341
+ if (!record || record.sourceKey !== sourceKey(principal)) throw new Error("Assignment not found for this conversation.");
342
+ return publicRecord(record);
343
+ }
344
+
345
+ function validateResponseSource(ws, msg, record, routedProjectSlug) {
346
+ var source = exactSource(record);
347
+ if (!source) throw new Error("Source conversation is no longer available.");
348
+ var userId = responseUserId(ws);
349
+ if (record.userId !== (userId || null)) throw new Error("Assignment access denied.");
350
+ if (msg.sourceProjectSlug !== record.sourceProjectSlug || msg.sourceSessionRef !== record.sourceSessionRef) {
351
+ throw new Error("Assignment response does not match its source conversation.");
352
+ }
353
+ if (msg.surface === "home") {
354
+ var tap = ws && ws._homeChatTap;
355
+ if (!tap || tap.mateSlug !== record.sourceProjectSlug || tap.sessionId !== source.session.localId) {
356
+ throw new Error("Open the exact Home conversation before responding.");
357
+ }
358
+ if (msg.mateId !== tap.mateId || msg.sessionId !== tap.sessionReference || msg.requestId !== tap.requestId) {
359
+ throw new Error("Assignment response is stale for this Home conversation.");
360
+ }
361
+ } else if (routedProjectSlug !== record.sourceProjectSlug || !ws || ws._clayActiveSession !== source.session.localId) {
362
+ throw new Error("Open the exact project conversation before responding.");
363
+ }
364
+ return source;
365
+ }
366
+
367
+ async function respond(ws, msg, routedProjectSlug) {
368
+ var userId = responseUserId(ws);
369
+ var record = findOwned(userId, clean(msg.assignmentId, 100));
370
+ if (!record) throw new Error("Assignment proposal is stale or unavailable.");
371
+ validateResponseSource(ws, msg, record, routedProjectSlug);
372
+ var action = msg.action === "approve" ? "approve" : (msg.action === "cancel" ? "cancel" : "");
373
+ if (!action) throw new Error("Choose approve or cancel.");
374
+ if (record.status !== "proposed") {
375
+ if (record.decision === action) return publicRecord(record);
376
+ throw new Error("Assignment proposal has already been resolved.");
377
+ }
378
+ return action === "approve" ? approve(record) : cancel(record);
379
+ }
380
+
381
+ function handleMessage(ws, msg, routedProjectSlug) {
382
+ if (!msg || msg.type !== "project_assignment_response") return false;
383
+ respond(ws, msg, routedProjectSlug).then(function (assignment) {
384
+ if (ws.readyState !== 1) return;
385
+ if (msg.surface === "home") {
386
+ ws.send(JSON.stringify({ type: "home_project_assignment_status", assignment: assignment, mateId: msg.mateId, sessionId: msg.sessionId, requestId: msg.requestId }));
387
+ } else {
388
+ ws.send(JSON.stringify({ type: "project_assignment_status", assignment: assignment }));
389
+ }
390
+ }).catch(function (error) {
391
+ if (ws.readyState !== 1) return;
392
+ if (msg.surface === "home") {
393
+ var userId = responseUserId(ws);
394
+ var record = findOwned(userId, msg.assignmentId);
395
+ var assignment = record ? publicRecord(record) : { assignmentId: msg.assignmentId || null, status: "proposed" };
396
+ assignment.error = error.message || String(error);
397
+ ws.send(JSON.stringify({ type: "home_project_assignment_status", assignment: assignment, mateId: msg.mateId, sessionId: msg.sessionId, requestId: msg.requestId }));
398
+ } else {
399
+ ws.send(JSON.stringify({ type: "project_assignment_error", assignmentId: msg.assignmentId || null, error: error.message || String(error) }));
400
+ }
401
+ });
402
+ return true;
403
+ }
404
+
405
+ load();
406
+ return {
407
+ propose: propose,
408
+ proposeFollowUp: proposeFollowUp,
409
+ getStatus: getStatus,
410
+ respond: respond,
411
+ handleMessage: handleMessage,
412
+ reconcileProject: reconcileProject,
413
+ records: records,
414
+ };
415
+ }
416
+
417
+ module.exports = {
418
+ attachWorkspaceAssignmentService: attachWorkspaceAssignmentService,
419
+ userStorageKey: userStorageKey,
420
+ };
@@ -0,0 +1,154 @@
1
+ var z;
2
+ try { z = require("zod"); } catch (e) { z = null; }
3
+
4
+ function field(type, description, optional) {
5
+ if (!z) return {};
6
+ var value = type === "number" ? z.number() : z.string();
7
+ if (description) value = value.describe(description);
8
+ return optional ? value.optional() : value;
9
+ }
10
+
11
+ function shape(fields) {
12
+ if (!z) return {};
13
+ return fields;
14
+ }
15
+
16
+ function textResult(value) {
17
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
18
+ }
19
+
20
+ function errorResult(error) {
21
+ return Promise.resolve({
22
+ content: [{ type: "text", text: "Error: " + (error && error.message ? error.message : String(error)) }],
23
+ isError: true,
24
+ });
25
+ }
26
+
27
+ function handler(bound, method) {
28
+ return function (args) {
29
+ if (!bound || typeof bound[method] !== "function") return errorResult(new Error("Workspace tools require an exact session-bound Mate query."));
30
+ try { return textResult(bound[method](args || {})); }
31
+ catch (e) { return errorResult(e); }
32
+ };
33
+ }
34
+
35
+ var SESSION_CITATION = " When citing a returned session in user-visible text, write [clayos/<sessionRef> — short label] so Clay Studio renders an owner-validated session control.";
36
+
37
+ function commonTools(bound) {
38
+ return [
39
+ {
40
+ name: "list_projects",
41
+ description: "List projects owned by the current user. Returns sanitized summaries only; use a project-specific tool to inspect sessions or history.",
42
+ inputSchema: shape({
43
+ query: field("string", "Optional project title or slug filter.", true),
44
+ cursor: field("string", "Opaque pagination cursor from a previous response.", true),
45
+ limit: field("number", "Page size, from 1 to 50.", true),
46
+ }),
47
+ handler: handler(bound, "listProjects"),
48
+ },
49
+ {
50
+ name: "list_project_sessions",
51
+ description: "List sanitized session summaries for one owned project, newest activity first." + SESSION_CITATION,
52
+ inputSchema: shape({
53
+ projectSlug: field("string", "Exact project slug."),
54
+ cursor: field("string", "Opaque pagination cursor from a previous response.", true),
55
+ limit: field("number", "Page size, from 1 to 50.", true),
56
+ }),
57
+ handler: handler(bound, "listProjectSessions"),
58
+ },
59
+ {
60
+ name: "search_project_history",
61
+ description: "Search canonical user and assistant text in one owned project. Tool inputs, tool results, hidden prompts, and internal paths are excluded." + SESSION_CITATION,
62
+ inputSchema: shape({
63
+ projectSlug: field("string", "Exact project slug."),
64
+ query: field("string", "Search query."),
65
+ cursor: field("string", "Opaque pagination cursor from a previous response.", true),
66
+ limit: field("number", "Page size, from 1 to 50.", true),
67
+ }),
68
+ handler: handler(bound, "searchProjectHistory"),
69
+ },
70
+ {
71
+ name: "read_project_session",
72
+ description: "Read a bounded page of canonical user and assistant turns from one owned session using the opaque session reference returned by list or search." + SESSION_CITATION,
73
+ inputSchema: shape({
74
+ projectSlug: field("string", "Exact project slug."),
75
+ sessionRef: field("string", "Opaque session reference returned by a workspace list or search tool."),
76
+ cursor: field("string", "Opaque pagination cursor from a previous response.", true),
77
+ limit: field("number", "Page size, from 1 to 50.", true),
78
+ }),
79
+ handler: handler(bound, "readProjectSession"),
80
+ },
81
+ {
82
+ name: "propose_project_assignment",
83
+ description: "Propose a new delegated session in an owned project. This posts a user-visible approval card and performs no target work until explicit approval.",
84
+ inputSchema: shape({
85
+ projectSlug: field("string", "Exact owned target project slug."),
86
+ title: field("string", "Short delegated session title."),
87
+ task: field("string", "Standalone bounded task without hidden source transcript."),
88
+ }),
89
+ handler: handler(bound, "proposeProjectAssignment"),
90
+ },
91
+ {
92
+ name: "propose_project_follow_up",
93
+ description: "Propose a bounded follow-up in an existing durable private session. This posts a user-visible approval card and performs no target work until explicit approval.",
94
+ inputSchema: shape({
95
+ projectSlug: field("string", "Exact owned target project slug."),
96
+ targetSessionRef: field("string", "Durable opaque session reference returned by list_project_sessions."),
97
+ title: field("string", "Short follow-up title."),
98
+ task: field("string", "Standalone bounded follow-up without hidden source transcript."),
99
+ }),
100
+ handler: handler(bound, "proposeProjectFollowUp"),
101
+ },
102
+ {
103
+ name: "get_assignment_status",
104
+ description: "Read a new-session or follow-up assignment status for this exact Mate conversation.",
105
+ inputSchema: shape({ assignmentId: field("string", "Assignment ID returned by propose_project_assignment.") }),
106
+ handler: handler(bound, "getAssignmentStatus"),
107
+ },
108
+ ];
109
+ }
110
+
111
+ function globalTools(bound) {
112
+ return [
113
+ {
114
+ name: "search_workspace_history",
115
+ description: "Search canonical user and assistant text across every project owned by the current user. Available only to authoritative builtin Clay." + SESSION_CITATION,
116
+ inputSchema: shape({
117
+ query: field("string", "Search query."),
118
+ cursor: field("string", "Opaque pagination cursor from a previous response.", true),
119
+ limit: field("number", "Page size, from 1 to 50.", true),
120
+ }),
121
+ handler: handler(bound, "searchWorkspaceHistory"),
122
+ },
123
+ {
124
+ name: "list_workspace_activity",
125
+ description: "List recent session activity across every project owned by the current user. Available only to authoritative builtin Clay." + SESSION_CITATION,
126
+ inputSchema: shape({
127
+ status: field("string", "Optional status: idle, processing, or waiting_for_approval.", true),
128
+ cursor: field("string", "Opaque pagination cursor from a previous response.", true),
129
+ limit: field("number", "Page size, from 1 to 50.", true),
130
+ }),
131
+ handler: handler(bound, "listWorkspaceActivity"),
132
+ },
133
+ ];
134
+ }
135
+
136
+ function getToolDefs(bound, includeGlobal) {
137
+ var tools = commonTools(bound);
138
+ if (includeGlobal === true) tools = tools.concat(globalTools(bound));
139
+ return tools;
140
+ }
141
+
142
+ function createMcpServer(adapter, bound, includeGlobal) {
143
+ if (!adapter || typeof adapter.createToolServer !== "function") return null;
144
+ return adapter.createToolServer({
145
+ name: "clay-workspace",
146
+ version: "1.0.0",
147
+ tools: getToolDefs(bound, includeGlobal),
148
+ });
149
+ }
150
+
151
+ module.exports = {
152
+ createMcpServer: createMcpServer,
153
+ getToolDefs: getToolDefs,
154
+ };