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,259 @@
1
+ // Vocabulary and validation for Project Logs.
2
+ //
3
+ // Extracted from project-logs-store.js so the store stays focused on records
4
+ // and stays inside the module size limit.
5
+ //
6
+ // Two independent axes, deliberately:
7
+ // category - what kind of record this is, a project-local vocabulary
8
+ // priority - how much it matters, a stable global enum
9
+ // Conflating them is what made "an urgent decision" unrepresentable before.
10
+ //
11
+ // Category is NOT a global enum. Every project evolves its own vocabulary: an
12
+ // agent reuses an established category when one fits and coins a new one when
13
+ // the project needs a durable distinction it does not yet have. What is fixed
14
+ // is only the shape of a category label, not the set of labels. The seeds
15
+ // below are guidance for a project with no history yet, never a whitelist.
16
+
17
+ // Suggested starting points, surfaced in tool guidance only.
18
+ var SEED_CATEGORIES = [
19
+ "decision",
20
+ "security",
21
+ "idea",
22
+ "operations",
23
+ "incident",
24
+ "investigation",
25
+ "progress",
26
+ "reference",
27
+ ];
28
+
29
+ var MAX_CATEGORY_CHARS = 32;
30
+ // Letters and digits in any script, so a project can keep its vocabulary in the
31
+ // language it actually works in. Normalization runs server-side only, so this
32
+ // depends on Node's Unicode property escapes and never on a browser baseline.
33
+ var CATEGORY_PATTERN = /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u;
34
+ var CATEGORY_STRIP = /[^\p{L}\p{N}-]/gu;
35
+
36
+ // Length is counted in code points, not UTF-16 units, so a CJK or Hangul label
37
+ // is bounded the same way a Latin one is.
38
+ function codePointLength(value) {
39
+ return Array.from(value).length;
40
+ }
41
+
42
+ // Deterministic normalization to a lowercase, hyphen-separated label. Anything
43
+ // that cannot be reduced to a safe label is rejected rather than silently
44
+ // coerced, so a caller is told when its input was unusable instead of getting a
45
+ // surprising slug.
46
+ //
47
+ // Path and identity shapes are refused outright: a category is dry metadata and
48
+ // must never be able to carry a slug, a path segment, or a user id.
49
+ function normalizeCategory(value) {
50
+ if (typeof value !== "string") throw new Error("A log category must be a string.");
51
+ var raw = value.normalize("NFC").trim();
52
+ if (!raw) throw new Error("A log category is required.");
53
+ if (codePointLength(raw) > MAX_CATEGORY_CHARS * 2) {
54
+ throw new Error("A log category must be " + MAX_CATEGORY_CHARS + " characters or fewer.");
55
+ }
56
+ if (/[\/\\]/.test(raw) || raw.indexOf("..") !== -1) {
57
+ throw new Error("A log category cannot contain path characters.");
58
+ }
59
+ var slug = raw
60
+ .toLowerCase()
61
+ .replace(/[_\s]+/g, "-")
62
+ .replace(CATEGORY_STRIP, "")
63
+ .replace(/-+/g, "-")
64
+ .replace(/^-|-$/g, "");
65
+ if (!slug) throw new Error("A log category must contain letters or digits: " + value);
66
+ if (codePointLength(slug) > MAX_CATEGORY_CHARS) {
67
+ throw new Error("A log category must be " + MAX_CATEGORY_CHARS + " characters or fewer.");
68
+ }
69
+ if (!CATEGORY_PATTERN.test(slug)) throw new Error("Invalid log category: " + value);
70
+ return slug;
71
+ }
72
+
73
+ // Non-throwing form for reading stored records, where a corrupt on-disk value
74
+ // should make that record unusable rather than break the whole projection.
75
+ // Filters deliberately do NOT use this: a caller that supplies a malformed
76
+ // category is told so.
77
+ function safeCategory(value) {
78
+ try {
79
+ return normalizeCategory(value);
80
+ } catch (e) {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ var PRIORITIES = ["normal", "important", "urgent"];
86
+ var DEFAULT_PRIORITY = "normal";
87
+
88
+ var MAX_SUMMARY_CHARS = 400;
89
+
90
+ function isPriority(value) {
91
+ return PRIORITIES.indexOf(value) !== -1;
92
+ }
93
+
94
+ function normalizePriority(value) {
95
+ return isPriority(value) ? value : DEFAULT_PRIORITY;
96
+ }
97
+
98
+ // A summary for an entry written before summaries existed. Deterministic and
99
+ // derived from stored content only, so the same record always yields the same
100
+ // text and nothing is invented.
101
+ function fallbackSummary(entry) {
102
+ if (!entry) return "";
103
+ if (typeof entry.summary === "string" && entry.summary.trim()) {
104
+ return entry.summary.trim().substring(0, MAX_SUMMARY_CHARS);
105
+ }
106
+ // Prefer the first line of prose. A leading Markdown heading usually just
107
+ // restates the title, so it is only used when there is nothing else.
108
+ var body = typeof entry.body === "string" ? entry.body : "";
109
+ var lines = body.split("\n");
110
+ var heading = "";
111
+ var firstLine = "";
112
+ for (var i = 0; i < lines.length; i++) {
113
+ var raw = lines[i].trim();
114
+ if (!raw) continue;
115
+ var isHeading = raw.charAt(0) === "#";
116
+ var line = raw.replace(/^#+\s*/, "").replace(/^[-*>]\s*/, "").trim();
117
+ if (!line) continue;
118
+ if (isHeading) {
119
+ if (!heading) heading = line;
120
+ continue;
121
+ }
122
+ firstLine = line;
123
+ break;
124
+ }
125
+ if (!firstLine) firstLine = heading;
126
+ if (!firstLine) return "";
127
+ return firstLine.length > MAX_SUMMARY_CHARS
128
+ ? firstLine.substring(0, MAX_SUMMARY_CHARS)
129
+ : firstLine;
130
+ }
131
+
132
+ // Validation for a new agent-written record. Category, title, and summary are
133
+ // all required: a log without a summary cannot be understood from a list, and
134
+ // that is the whole point of the surface.
135
+ function validateNewRecord(input) {
136
+ var data = input || {};
137
+ var category = normalizeCategory(data.category || data.kind);
138
+ if (typeof data.title !== "string" || !data.title.trim()) throw new Error("A log title is required.");
139
+ if (typeof data.summary !== "string" || !data.summary.trim()) {
140
+ throw new Error("A one or two sentence summary is required so the entry is understandable from a list.");
141
+ }
142
+ // A mistyped priority is refused rather than silently downgraded to normal:
143
+ // an agent that meant "urgent" must not get "normal" without being told.
144
+ if (data.priority !== undefined && data.priority !== null && !isPriority(data.priority)) {
145
+ throw new Error("Unknown log priority: " + data.priority);
146
+ }
147
+ return {
148
+ category: category,
149
+ priority: normalizePriority(data.priority),
150
+ };
151
+ }
152
+
153
+ // Validation for a revision. The category may move to another writable value
154
+ // or stay on its legacy one; nothing is required, but nothing may be emptied.
155
+ function validateRevision(input) {
156
+ var data = input || {};
157
+ var out = {};
158
+ if (data.category !== undefined || data.kind !== undefined) {
159
+ out.category = normalizeCategory(data.category || data.kind);
160
+ }
161
+ if (data.priority !== undefined) {
162
+ if (!isPriority(data.priority)) throw new Error("Unknown log priority: " + data.priority);
163
+ out.priority = data.priority;
164
+ }
165
+ if (data.title !== undefined) {
166
+ if (typeof data.title !== "string" || !data.title.trim()) throw new Error("A log title cannot be emptied.");
167
+ out.title = data.title;
168
+ }
169
+ if (data.summary !== undefined) {
170
+ if (typeof data.summary !== "string" || !data.summary.trim()) throw new Error("A log summary cannot be emptied.");
171
+ out.summary = data.summary;
172
+ }
173
+ return out;
174
+ }
175
+
176
+
177
+ // --- Normalization -------------------------------------------------------
178
+ //
179
+ // Moved here from project-logs-store.js so the store stays inside the module
180
+ // size limit. These are pure text and shape helpers with no record semantics.
181
+
182
+ var MAX_TAGS = 12;
183
+ var MAX_TAG_CHARS = 40;
184
+ var MAX_LINKS = 20;
185
+ var MAX_LINK_CHARS = 200;
186
+
187
+ function cleanText(value, max) {
188
+ if (typeof value !== "string") return "";
189
+ var text = value.replace(/\u0000/g, "").trim();
190
+ return text.length > max ? text.substring(0, max) : text;
191
+ }
192
+
193
+ function cleanLine(value, max) {
194
+ return cleanText(value, max).replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
195
+ }
196
+
197
+ function normalizeAuthor(author) {
198
+ var source = author || {};
199
+ var type = source.type === "user" ? "user" : "session";
200
+ return {
201
+ type: type,
202
+ userId: source.userId ? cleanLine(String(source.userId), 120) : null,
203
+ displayName: cleanLine(source.displayName || "", 120) || null,
204
+ sessionKey: source.sessionKey ? cleanLine(String(source.sessionKey), 200) : null,
205
+ vendor: cleanLine(source.vendor || "", 40) || null,
206
+ };
207
+ }
208
+
209
+ function normalizeTags(tags) {
210
+ if (!Array.isArray(tags)) return [];
211
+ var out = [];
212
+ for (var i = 0; i < tags.length && out.length < MAX_TAGS; i++) {
213
+ var tag = cleanLine(String(tags[i] == null ? "" : tags[i]), MAX_TAG_CHARS).toLowerCase();
214
+ if (tag && out.indexOf(tag) === -1) out.push(tag);
215
+ }
216
+ return out;
217
+ }
218
+
219
+ function normalizeLinks(links) {
220
+ if (!Array.isArray(links)) return [];
221
+ var out = [];
222
+ for (var i = 0; i < links.length && out.length < MAX_LINKS; i++) {
223
+ var raw = links[i];
224
+ var value = typeof raw === "string" ? { ref: raw } : (raw || {});
225
+ var ref = cleanLine(String(value.ref || value.sessionRef || ""), MAX_LINK_CHARS);
226
+ if (!ref) continue;
227
+ var label = cleanLine(value.label || "", MAX_LINK_CHARS) || null;
228
+ var duplicate = false;
229
+ for (var j = 0; j < out.length; j++) {
230
+ if (out[j].ref === ref) { duplicate = true; break; }
231
+ }
232
+ if (!duplicate) out.push({ ref: ref, label: label });
233
+ }
234
+ return out;
235
+ }
236
+
237
+ module.exports = {
238
+ SEED_CATEGORIES: SEED_CATEGORIES,
239
+ MAX_CATEGORY_CHARS: MAX_CATEGORY_CHARS,
240
+ CATEGORY_PATTERN: CATEGORY_PATTERN,
241
+ codePointLength: codePointLength,
242
+ normalizeCategory: normalizeCategory,
243
+ safeCategory: safeCategory,
244
+ PRIORITIES: PRIORITIES,
245
+ DEFAULT_PRIORITY: DEFAULT_PRIORITY,
246
+ MAX_SUMMARY_CHARS: MAX_SUMMARY_CHARS,
247
+ isPriority: isPriority,
248
+ normalizePriority: normalizePriority,
249
+ fallbackSummary: fallbackSummary,
250
+ MAX_TAGS: MAX_TAGS,
251
+ MAX_LINKS: MAX_LINKS,
252
+ cleanText: cleanText,
253
+ cleanLine: cleanLine,
254
+ normalizeAuthor: normalizeAuthor,
255
+ normalizeTags: normalizeTags,
256
+ normalizeLinks: normalizeLinks,
257
+ validateNewRecord: validateNewRecord,
258
+ validateRevision: validateRevision,
259
+ };
@@ -0,0 +1,357 @@
1
+ // Authorization and binding layer for Project Logs.
2
+ //
3
+ // Modeled on workspace-query-service.js: a caller never states who it is.
4
+ // Every binder re-derives the principal from authoritative server state and
5
+ // returns null when anything fails to line up, so an unbound or stale caller
6
+ // gets no capability rather than a degraded one.
7
+ //
8
+ // Product rules enforced here:
9
+ // - Project Logs are project records. Mate projects have none.
10
+ // - Ordinary Mates cannot reach project Logs at all.
11
+ // - Authoritative builtin Clay may read Logs, and only for projects the
12
+ // owning user is authorized to see. It may never write.
13
+ // - A project session is bound to exactly its own project's Logs.
14
+ // - Canonical authorship belongs to Project Driver sessions alone. A human,
15
+ // however privileged, may read and comment but may never create or revise
16
+ // a canonical entry: the log is the agent's record of the work.
17
+ // - Members of a shared project read each other's entries and comments with
18
+ // attribution.
19
+ // - A worktree resolves to its parent project for both authorization and
20
+ // storage, so worktree work never forks project knowledge.
21
+
22
+ var builtinMates = require("./builtin-mates");
23
+ var logsStore = require("./project-logs-store");
24
+
25
+ function readOnly() {
26
+ throw new Error("Project Logs are read-only for builtin Clay.");
27
+ }
28
+
29
+ // Canonical entries are written by the project's own agent sessions. A human
30
+ // participates by commenting, which is a separate, append-only surface.
31
+ function agentOnly() {
32
+ throw new Error("Project Logs are written by this project's agent sessions. Add a comment instead.");
33
+ }
34
+
35
+ // Reviewing feedback and moving the record between revisions are the Driver's
36
+ // judgement calls. A human cannot review their own comment into the record,
37
+ // and Clay cannot review another project's.
38
+ function driverOnly() {
39
+ throw new Error("Only this project's agent sessions can review feedback or change revisions.");
40
+ }
41
+
42
+ // Bounded discovery of comments still waiting on the Driver. The store owns the
43
+ // one-pass scan over every live entry, so nothing here imposes a ceiling that
44
+ // could hide a pending comment on an older record.
45
+ function attachProjectLogsService(ctx) {
46
+ var context = ctx || {};
47
+ var getProjects = context.getProjects;
48
+ var isMultiUser = context.isMultiUser;
49
+ var resolveMate = context.resolveMate;
50
+ var canAccessProject = context.canAccessProject;
51
+ var findUserById = context.findUserById || function () { return null; };
52
+ var storeCache = new Map();
53
+
54
+ var openStore = context.openStore || function (cwd) {
55
+ if (!storeCache.has(cwd)) {
56
+ storeCache.set(cwd, logsStore.createProjectLogsStore({ cwd: cwd, baseDir: context.baseDir }));
57
+ }
58
+ return storeCache.get(cwd);
59
+ };
60
+
61
+ function projectFor(slug) {
62
+ if (!slug || typeof slug !== "string") return null;
63
+ var projects = getProjects();
64
+ return (projects && projects.get(slug)) || null;
65
+ }
66
+
67
+ // A worktree inherits its parent's visibility, so authorization is decided
68
+ // against the parent whenever the parent is registered.
69
+ function effectiveStatus(status) {
70
+ if (!status || status.isWorktree !== true || !status.parentSlug) return status;
71
+ var parent = projectFor(status.parentSlug);
72
+ if (!parent) return status;
73
+ var parentStatus = parent.getStatus();
74
+ return parentStatus || status;
75
+ }
76
+
77
+ function ownsProject(principal, status) {
78
+ if (!status) return false;
79
+ if (isMultiUser()) return !!principal.userId && status.projectOwnerId === principal.userId;
80
+ return !status.projectOwnerId || (!!principal.singleUserOwnerId && status.projectOwnerId === principal.singleUserOwnerId);
81
+ }
82
+
83
+ function authorizedProject(principal, status) {
84
+ if (!status) return false;
85
+ if (status.isMate === true) return false;
86
+ var governing = effectiveStatus(status);
87
+ if (ownsProject(principal, governing)) return true;
88
+ if (isMultiUser() && principal.userId && typeof canAccessProject === "function") {
89
+ return canAccessProject(principal.userId, governing) === true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ // Open the store for the governing project. When a worktree's parent is
95
+ // registered, its path is authoritative, so worktree Logs are shared without
96
+ // depending on a Git subprocess. The store's own root resolution remains the
97
+ // fallback for unregistered parents and for subdirectories of a checkout.
98
+ function storeForStatus(status) {
99
+ var governing = effectiveStatus(status);
100
+ if (!governing || !governing.path) throw new Error("Project Logs are unavailable for this project.");
101
+ return openStore(governing.path);
102
+ }
103
+
104
+ function statusFor(principal, slug) {
105
+ var project = projectFor(slug);
106
+ if (!project) throw new Error("Project not found.");
107
+ var status = project.getStatus();
108
+ if (!authorizedProject(principal, status)) throw new Error("Project Logs are not available for this project.");
109
+ return status;
110
+ }
111
+
112
+ function displayNameFor(userId) {
113
+ if (!userId) return null;
114
+ var user = findUserById(userId);
115
+ if (!user) return null;
116
+ return user.displayName || user.username || null;
117
+ }
118
+
119
+ function sessionAuthor(session) {
120
+ return {
121
+ type: "session",
122
+ userId: session.ownerId || null,
123
+ displayName: displayNameFor(session.ownerId),
124
+ sessionKey: session.cliSessionId || "local:" + session.localId,
125
+ vendor: session.vendor || null,
126
+ };
127
+ }
128
+
129
+ function userAuthor(user) {
130
+ return {
131
+ type: "user",
132
+ userId: (user && user.id) || null,
133
+ displayName: (user && (user.displayName || user.username)) || null,
134
+ sessionKey: null,
135
+ vendor: null,
136
+ };
137
+ }
138
+
139
+ // --- Binders ---------------------------------------------------------
140
+
141
+ // Exact session object identity, not a caller-supplied session id. A session
142
+ // handed in from a stale or forged reference is rejected even when its
143
+ // localId matches a live session.
144
+ function resolveProjectSession(source) {
145
+ if (!source || !source.projectSlug || !source.session) return null;
146
+ var project = projectFor(source.projectSlug);
147
+ if (!project) return null;
148
+ var status = project.getStatus();
149
+ if (!status || status.isMate === true) return null;
150
+ var manager = project.getSessionManager ? project.getSessionManager() : project.sm;
151
+ if (!manager || !manager.sessions || manager.sessions.get(source.session.localId) !== source.session) return null;
152
+
153
+ var principal;
154
+ if (isMultiUser()) {
155
+ // Fail closed: an unattributed session in multi-user mode gets nothing.
156
+ if (!source.session.ownerId) return null;
157
+ principal = { userId: source.session.ownerId, singleUserOwnerId: null };
158
+ } else {
159
+ if (source.session.ownerId && status.projectOwnerId && source.session.ownerId !== status.projectOwnerId) return null;
160
+ principal = { userId: null, singleUserOwnerId: source.session.ownerId || status.projectOwnerId || null };
161
+ }
162
+ if (!authorizedProject(principal, status)) return null;
163
+
164
+ principal.kind = "session";
165
+ principal.projectSlug = source.projectSlug;
166
+ principal.isClay = false;
167
+ return { principal: principal, status: status, author: sessionAuthor(source.session) };
168
+ }
169
+
170
+ // A tool handler captured earlier stays valid only while its exact source
171
+ // still resolves. Session identity and project authorization are both
172
+ // re-derived on every call, so a session dropped from the manager, or
173
+ // replaced by a different object with the same id, loses the capability
174
+ // immediately rather than continuing to work through a captured handler.
175
+ function bindProjectSession(source) {
176
+ var initial = resolveProjectSession(source);
177
+ if (!initial) return null;
178
+ function revalidate() {
179
+ var current = resolveProjectSession(source);
180
+ if (!current || current.principal.projectSlug !== initial.principal.projectSlug) {
181
+ throw new Error("This Project Logs binding is no longer valid for the current session.");
182
+ }
183
+ return current;
184
+ }
185
+ return createProjectBound(initial, revalidate);
186
+ }
187
+
188
+ function bindUser(source) {
189
+ if (!source || !source.projectSlug) return null;
190
+ var project = projectFor(source.projectSlug);
191
+ if (!project) return null;
192
+ var status = project.getStatus();
193
+ if (!status || status.isMate === true) return null;
194
+ var user = source.user || null;
195
+
196
+ var principal;
197
+ if (isMultiUser()) {
198
+ if (!user || !user.id) return null;
199
+ principal = { userId: user.id, singleUserOwnerId: null };
200
+ } else {
201
+ principal = { userId: null, singleUserOwnerId: (user && user.id) || status.projectOwnerId || null };
202
+ }
203
+ if (!authorizedProject(principal, status)) return null;
204
+
205
+ principal.kind = "user";
206
+ principal.projectSlug = source.projectSlug;
207
+ principal.isClay = false;
208
+ var envelope = { principal: principal, status: status, author: userAuthor(user) };
209
+ return createProjectBound(envelope, function () { return envelope; });
210
+ }
211
+
212
+ // Ordinary Mates get null. Only the authoritative builtin Clay host agent,
213
+ // confirmed against the server Mate registry rather than a project flag,
214
+ // receives a read-only cross-project binding.
215
+ function resolveMateSource(source) {
216
+ if (!source || source.isMate !== true || !source.mateId || typeof resolveMate !== "function") return null;
217
+ var sourceProject = projectFor(source.projectSlug);
218
+ if (!sourceProject) return null;
219
+ var sourceStatus = sourceProject.getStatus();
220
+ if (!sourceStatus || sourceStatus.isMate !== true) return null;
221
+ if (sourceStatus.mateId !== source.mateId || sourceStatus.projectOwnerId !== source.projectOwnerId) return null;
222
+ if (source.session) {
223
+ var manager = sourceProject.getSessionManager ? sourceProject.getSessionManager() : sourceProject.sm;
224
+ if (!manager || !manager.sessions || manager.sessions.get(source.session.localId) !== source.session) return null;
225
+ }
226
+ var mate = resolveMate(source.projectOwnerId || null, source.mateId);
227
+ if (!mate || mate.id !== source.mateId) return null;
228
+ if (isMultiUser() && (!source.projectOwnerId || mate.createdBy !== source.projectOwnerId)) return null;
229
+
230
+ var def = mate.builtinKey ? builtinMates.getBuiltinByKey(mate.builtinKey) : null;
231
+ var isClay = !!(mate.builtinKey === "clay" && def && def.hostAgent === true);
232
+ if (!isClay) return null;
233
+ if (isMultiUser() && !source.projectOwnerId) return null;
234
+
235
+ return {
236
+ kind: "clay",
237
+ userId: isMultiUser() ? source.projectOwnerId : null,
238
+ singleUserOwnerId: isMultiUser() ? null : (source.projectOwnerId || null),
239
+ projectSlug: null,
240
+ mateId: mate.id,
241
+ isClay: true,
242
+ };
243
+ }
244
+
245
+ // Clay keeps its authoritative registry and user authorization checks on
246
+ // every call, so a Mate that is deleted, demoted out of builtin Clay, or
247
+ // reassigned to another user loses the read view immediately.
248
+ function bindMate(source) {
249
+ var initial = resolveMateSource(source);
250
+ if (!initial) return null;
251
+ function revalidate() {
252
+ var current = resolveMateSource(source);
253
+ if (!current || current.mateId !== initial.mateId || current.userId !== initial.userId) {
254
+ throw new Error("This Project Logs binding is no longer valid for the current session.");
255
+ }
256
+ return current;
257
+ }
258
+ return createClayBound(revalidate);
259
+ }
260
+
261
+ // --- Bound capability surfaces ---------------------------------------
262
+
263
+ function createProjectBound(initial, revalidate) {
264
+ var store = storeForStatus(initial.status);
265
+
266
+ // Identity, authorization, and the target store are all re-derived per
267
+ // call. revalidate() re-checks exact session identity where there is one;
268
+ // the project authorization check then catches a visibility change.
269
+ function live() {
270
+ var current = revalidate();
271
+ var project = projectFor(current.principal.projectSlug);
272
+ if (!project) throw new Error("Project not found.");
273
+ var currentStatus = project.getStatus();
274
+ if (!authorizedProject(current.principal, currentStatus)) throw new Error("Project Logs are not available for this project.");
275
+ return storeForStatus(currentStatus);
276
+ }
277
+
278
+ function author() {
279
+ return revalidate().author;
280
+ }
281
+
282
+ // A session may write canonically; a user may only comment. The capability
283
+ // is decided by the binder that produced this envelope, never by a flag on
284
+ // the request.
285
+ var canonical = initial.principal.kind === "session";
286
+ return {
287
+ isClay: false,
288
+ canWrite: canonical,
289
+ canComment: true,
290
+ projectSlug: initial.principal.projectSlug,
291
+ scopeId: store.scopeId,
292
+ root: store.root,
293
+ author: initial.author,
294
+ listLogs: function (args) { return live().list(args || {}); },
295
+ searchLogs: function (args) { return live().search(args || {}); },
296
+ readLog: function (args) {
297
+ var entry = live().read(args && args.ref, false);
298
+ if (!entry) throw new Error("Log entry not found.");
299
+ return entry;
300
+ },
301
+ logHistory: function (args) { return live().history(args && args.ref, args || {}); },
302
+ commentLog: function (args) { return live().comment(args && args.ref, args || {}, author()); },
303
+ readLogRevision: function (args) { return live().readRevision(args && args.ref, args && args.revision); },
304
+ listLogFeedback: canonical ? function (args) { return live().feedback(args || {}); } : driverOnly,
305
+ reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author()); } : driverOnly,
306
+ revertLog: canonical
307
+ ? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author()); }
308
+ : driverOnly,
309
+ createLog: canonical ? function (args) { return live().create(args || {}, author()); } : agentOnly,
310
+ updateLog: canonical ? function (args) { return live().update(args && args.ref, args || {}, author()); } : agentOnly,
311
+ linkLog: canonical ? function (args) { return live().link(args && args.ref, args && args.links, author()); } : agentOnly,
312
+ removeLog: canonical ? function (args) { return live().remove(args && args.ref, author()); } : agentOnly,
313
+ };
314
+ }
315
+
316
+ function createClayBound(revalidate) {
317
+ function target(args) {
318
+ var principal = revalidate();
319
+ var slug = args && args.projectSlug;
320
+ if (!slug) throw new Error("An exact project slug is required.");
321
+ return storeForStatus(statusFor(principal, slug));
322
+ }
323
+
324
+ return {
325
+ isClay: true,
326
+ canWrite: false,
327
+ canComment: false,
328
+ projectSlug: null,
329
+ author: null,
330
+ listLogs: function (args) { return target(args).list(args || {}); },
331
+ searchLogs: function (args) { return target(args).search(args || {}); },
332
+ readLog: function (args) {
333
+ var entry = target(args).read(args && args.ref, false);
334
+ if (!entry) throw new Error("Log entry not found.");
335
+ return entry;
336
+ },
337
+ logHistory: function (args) { return target(args).history(args && args.ref, args || {}); },
338
+ commentLog: readOnly,
339
+ readLogRevision: function (args) { return target(args).readRevision(args && args.ref, args && args.revision); },
340
+ listLogFeedback: readOnly,
341
+ reviewLogComment: readOnly,
342
+ revertLog: readOnly,
343
+ createLog: readOnly,
344
+ updateLog: readOnly,
345
+ linkLog: readOnly,
346
+ removeLog: readOnly,
347
+ };
348
+ }
349
+
350
+ return {
351
+ bindProjectSession: bindProjectSession,
352
+ bindUser: bindUser,
353
+ bindMate: bindMate,
354
+ };
355
+ }
356
+
357
+ module.exports = { attachProjectLogsService: attachProjectLogsService };