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,188 @@
1
+ // Clay-wide append-only knowledge record backend.
2
+ //
3
+ // One physical store per scope. Every Knowledge surface is a projection over
4
+ // this backend rather than its own store: Project Logs today, Mate Knowledge
5
+ // and Shared Knowledge later. Records are never rewritten in place, so
6
+ // revision chains, tombstones, and author/blame fall out of the storage format
7
+ // instead of being layered on top of it.
8
+ //
9
+ // Storage lives under config.CONFIG_DIR, never inside a user repository:
10
+ // {CONFIG_DIR}/knowledge/{scope}/{name}/records.jsonl
11
+ //
12
+ // Loading is incremental and torn-write tolerant. Bytes are consumed only up to
13
+ // the last newline, so a partially written trailing line is held back until it
14
+ // completes, and a complete-but-unparseable line is skipped and counted rather
15
+ // than failing the load.
16
+
17
+ var fs = require("fs");
18
+ var path = require("path");
19
+ var crypto = require("crypto");
20
+ var config = require("./config");
21
+
22
+ var RECORD_VERSION = 1;
23
+ var MAX_RECORD_BYTES = 64 * 1024;
24
+ var MAX_STORE_BYTES = 64 * 1024 * 1024;
25
+ var SEGMENT_PATTERN = /^[A-Za-z0-9_-]{1,120}$/;
26
+ var NEWLINE = 0x0a;
27
+
28
+ function knowledgeRoot() {
29
+ return path.join(config.CONFIG_DIR, "knowledge");
30
+ }
31
+
32
+ function newRecordId() {
33
+ return Date.now().toString(36) + "-" + crypto.randomBytes(6).toString("hex");
34
+ }
35
+
36
+ // A scope id is a slash-separated set of strict segments. Anything else is
37
+ // rejected outright so a caller-supplied value can never escape the root.
38
+ function scopeSegments(scopeId) {
39
+ if (typeof scopeId !== "string" || !scopeId) throw new Error("A knowledge scope id is required.");
40
+ var segments = scopeId.split("/");
41
+ if (segments.length < 2 || segments.length > 4) throw new Error("Invalid knowledge scope id: " + scopeId);
42
+ for (var i = 0; i < segments.length; i++) {
43
+ if (!SEGMENT_PATTERN.test(segments[i])) throw new Error("Invalid knowledge scope id: " + scopeId);
44
+ }
45
+ return segments;
46
+ }
47
+
48
+ function createRecordStore(opts) {
49
+ var options = opts || {};
50
+ var scopeId = options.scopeId;
51
+ var segments = scopeSegments(scopeId);
52
+ var baseDir = options.baseDir || knowledgeRoot();
53
+ var storeDir = path.join.apply(path, [baseDir].concat(segments));
54
+ var filePath = path.join(storeDir, "records.jsonl");
55
+
56
+ var records = [];
57
+ var consumed = 0;
58
+ var partial = Buffer.alloc(0);
59
+ var skipped = 0;
60
+ var loaded = false;
61
+
62
+ function reset() {
63
+ records = [];
64
+ consumed = 0;
65
+ partial = Buffer.alloc(0);
66
+ skipped = 0;
67
+ }
68
+
69
+ function readRange(size) {
70
+ var length = size - consumed;
71
+ if (length <= 0) return Buffer.alloc(0);
72
+ var buffer = Buffer.alloc(length);
73
+ var fd = fs.openSync(filePath, "r");
74
+ try {
75
+ var read = fs.readSync(fd, buffer, 0, length, consumed);
76
+ return read === length ? buffer : buffer.slice(0, read);
77
+ } finally {
78
+ fs.closeSync(fd);
79
+ }
80
+ }
81
+
82
+ function ingest(chunk) {
83
+ var pending = partial.length > 0 ? Buffer.concat([partial, chunk]) : chunk;
84
+ var lastNewline = pending.lastIndexOf(NEWLINE);
85
+ if (lastNewline === -1) {
86
+ partial = pending;
87
+ return;
88
+ }
89
+ partial = pending.slice(lastNewline + 1);
90
+ var lines = pending.slice(0, lastNewline).toString("utf8").split("\n");
91
+ for (var i = 0; i < lines.length; i++) {
92
+ var line = lines[i].trim();
93
+ if (!line) continue;
94
+ var parsed = null;
95
+ try {
96
+ parsed = JSON.parse(line);
97
+ } catch (e) {
98
+ skipped++;
99
+ continue;
100
+ }
101
+ if (!parsed || typeof parsed !== "object" || parsed.v !== RECORD_VERSION || !parsed.id) {
102
+ skipped++;
103
+ continue;
104
+ }
105
+ records.push(parsed);
106
+ }
107
+ }
108
+
109
+ // Pick up appends made by this process and by any other process holding the
110
+ // same file open. A shrunk file means it was truncated or replaced, so the
111
+ // projection is rebuilt from the start.
112
+ function refresh() {
113
+ var stat = null;
114
+ try {
115
+ stat = fs.statSync(filePath);
116
+ } catch (e) {
117
+ if (loaded && consumed === 0 && records.length === 0) return;
118
+ reset();
119
+ loaded = true;
120
+ return;
121
+ }
122
+ if (stat.size > MAX_STORE_BYTES) throw new Error("Knowledge store is too large to load: " + scopeId);
123
+ if (stat.size < consumed) reset();
124
+ if (loaded && stat.size === consumed) return;
125
+ var chunk = readRange(stat.size);
126
+ consumed += chunk.length;
127
+ loaded = true;
128
+ if (chunk.length > 0) ingest(chunk);
129
+ }
130
+
131
+ // O_APPEND plus a single write syscall keeps a record from interleaving with
132
+ // a concurrent writer's record. The refresh on either side keeps this
133
+ // process's projection consistent with what actually landed on disk.
134
+ function append(record) {
135
+ if (!record || typeof record !== "object") throw new Error("A knowledge record object is required.");
136
+ refresh();
137
+ var stored = Object.assign({}, record, { v: RECORD_VERSION });
138
+ if (!stored.id) stored.id = newRecordId();
139
+ var buffer = Buffer.from(JSON.stringify(stored) + "\n", "utf8");
140
+ if (buffer.indexOf(NEWLINE) !== buffer.length - 1) throw new Error("A knowledge record must serialize to a single line.");
141
+ if (buffer.length > MAX_RECORD_BYTES) throw new Error("Knowledge record exceeds " + MAX_RECORD_BYTES + " bytes.");
142
+ fs.mkdirSync(storeDir, { recursive: true });
143
+ var fd = fs.openSync(filePath, "a");
144
+ try {
145
+ fs.writeSync(fd, buffer, 0, buffer.length);
146
+ } finally {
147
+ fs.closeSync(fd);
148
+ }
149
+ config.chmodSafe(filePath, 0o600);
150
+ refresh();
151
+ return stored;
152
+ }
153
+
154
+ // Returned for reading only. Callers project over it and must not mutate it.
155
+ function all() {
156
+ refresh();
157
+ return records;
158
+ }
159
+
160
+ function stats() {
161
+ refresh();
162
+ return {
163
+ scopeId: scopeId,
164
+ filePath: filePath,
165
+ records: records.length,
166
+ skipped: skipped,
167
+ pendingBytes: partial.length,
168
+ };
169
+ }
170
+
171
+ return {
172
+ scopeId: scopeId,
173
+ filePath: filePath,
174
+ append: append,
175
+ all: all,
176
+ stats: stats,
177
+ refresh: refresh,
178
+ };
179
+ }
180
+
181
+ module.exports = {
182
+ RECORD_VERSION: RECORD_VERSION,
183
+ MAX_RECORD_BYTES: MAX_RECORD_BYTES,
184
+ knowledgeRoot: knowledgeRoot,
185
+ newRecordId: newRecordId,
186
+ scopeSegments: scopeSegments,
187
+ createRecordStore: createRecordStore,
188
+ };
@@ -0,0 +1,224 @@
1
+ // Shared BM25 adapter for Clay Knowledge surfaces.
2
+ //
3
+ // There is exactly one ranking implementation in Clay: the generic
4
+ // buildIndex/searchIndex engine in session-search.js, including its tokenizer
5
+ // with CJK/Japanese/Thai bigram segmentation. This module is a thin adapter
6
+ // over it, not a second ranker.
7
+ //
8
+ // Field weighting is expressed by controlled repetition of a field's text
9
+ // before indexing, which is how session-search.js already weights digests.
10
+ // A title repeated three times contributes three times the term frequency of
11
+ // the body, so BM25 saturation and length normalisation still apply normally.
12
+ //
13
+ // Long records are segmented rather than truncated. A record whose body runs to
14
+ // tens of thousands of characters is indexed as several overlapping documents,
15
+ // so a term that appears only near the end is still found. Short fields (title,
16
+ // name, tags) are anchors and are repeated into every segment, so weighting is
17
+ // identical in each one. Segments are collapsed back to one result per original
18
+ // item by taking its best-scoring segment; the caller still applies its own
19
+ // score, recency, and reference tie-breaks on top.
20
+
21
+ var sessionSearch = require("./session-search");
22
+
23
+ var MAX_SNIPPET_CHARS = 320;
24
+ var SNIPPET_LEAD_CHARS = 80;
25
+ // A field at or under this length is an anchor, replicated into every segment.
26
+ // Anything longer is windowed.
27
+ var SEGMENT_CHARS = 8000;
28
+ // Overlap so a term straddling a window boundary is still indexed intact.
29
+ var SEGMENT_OVERLAP = 800;
30
+ // Memory bound. A record needing more windows than this is indexed up to the
31
+ // cap; the cap is high enough that no realistic Knowledge record reaches it.
32
+ var MAX_SEGMENTS_PER_ITEM = 64;
33
+
34
+ function repeat(text, weight) {
35
+ var out = [];
36
+ for (var i = 0; i < weight; i++) out.push(text);
37
+ return out.join(" ");
38
+ }
39
+
40
+ function normalizeWeight(weight) {
41
+ return Number.isFinite(weight) ? Math.max(1, Math.floor(weight)) : 1;
42
+ }
43
+
44
+ function usableFields(fields) {
45
+ var out = [];
46
+ for (var i = 0; i < (fields || []).length; i++) {
47
+ var field = fields[i];
48
+ if (!field || typeof field.text !== "string" || !field.text) continue;
49
+ out.push({ text: field.text, weight: normalizeWeight(field.weight) });
50
+ }
51
+ return out;
52
+ }
53
+
54
+ // Kept for callers and tests that want the single-document form. Anchors and
55
+ // short bodies produce exactly this text.
56
+ function weightedText(fields) {
57
+ var parts = [];
58
+ var usable = usableFields(fields);
59
+ for (var i = 0; i < usable.length; i++) parts.push(repeat(usable[i].text, usable[i].weight));
60
+ return parts.join(" ");
61
+ }
62
+
63
+ var SEGMENT_STRIDE = SEGMENT_CHARS - SEGMENT_OVERLAP;
64
+ // The maximum searchable characters of one long field that can be indexed
65
+ // before the segment cap stops coverage. Reported to callers so an incomplete
66
+ // index is stated rather than implied.
67
+ var MAX_INDEXED_CHARS = SEGMENT_CHARS + (MAX_SEGMENTS_PER_ITEM - 1) * SEGMENT_STRIDE;
68
+
69
+ // One shared plan drives both the indexed documents and the coverage report,
70
+ // so what a caller is told was indexed is exactly what was indexed.
71
+ function planSegments(fields) {
72
+ var usable = usableFields(fields);
73
+ var anchors = [];
74
+ var longs = [];
75
+ for (var i = 0; i < usable.length; i++) {
76
+ if (usable[i].text.length > SEGMENT_CHARS) longs.push(usable[i]);
77
+ else anchors.push(usable[i]);
78
+ }
79
+
80
+ var windows = [];
81
+ var totalChars = 0;
82
+ var indexedChars = 0;
83
+ for (var l = 0; l < longs.length; l++) {
84
+ var text = longs[l].text;
85
+ totalChars += text.length;
86
+ var covered = 0;
87
+ for (var offset = 0; offset < text.length && windows.length < MAX_SEGMENTS_PER_ITEM; offset += SEGMENT_STRIDE) {
88
+ var end = Math.min(text.length, offset + SEGMENT_CHARS);
89
+ windows.push({ field: longs[l], start: offset, end: end });
90
+ covered = end;
91
+ if (end >= text.length) break;
92
+ }
93
+ indexedChars += covered;
94
+ }
95
+ for (var a = 0; a < anchors.length; a++) {
96
+ totalChars += anchors[a].text.length;
97
+ indexedChars += anchors[a].text.length;
98
+ }
99
+
100
+ return {
101
+ anchors: anchors,
102
+ longs: longs,
103
+ windows: windows,
104
+ totalChars: totalChars,
105
+ indexedChars: indexedChars,
106
+ complete: indexedChars >= totalChars,
107
+ };
108
+ }
109
+
110
+ // How much of an item's searchable text this adapter can index. `complete` is
111
+ // false when the segment cap stopped short, which means a term appearing only
112
+ // in the uncovered tail cannot be ranked.
113
+ function coverage(fields) {
114
+ var plan = planSegments(fields);
115
+ return { totalChars: plan.totalChars, indexedChars: plan.indexedChars, complete: plan.complete };
116
+ }
117
+
118
+ // One item becomes one or more BM25 documents. Short fields anchor every
119
+ // segment so title and tag weighting is consistent wherever the body matched.
120
+ function segmentTexts(fields) {
121
+ var plan = planSegments(fields);
122
+ var anchorText = weightedText(plan.anchors);
123
+ if (plan.windows.length === 0) return [anchorText];
124
+
125
+ var texts = [];
126
+ for (var w = 0; w < plan.windows.length; w++) {
127
+ var window = plan.windows[w];
128
+ var body = repeat(window.field.text.substring(window.start, window.end), window.field.weight);
129
+ texts.push(anchorText ? anchorText + " " + body : body);
130
+ }
131
+ return texts;
132
+ }
133
+
134
+ // Build the generic {id, text, meta} documents the shared engine expects.
135
+ // project(item, position) must return { id, fields, meta }. One item may yield
136
+ // several documents; each carries the item's position so results collapse back.
137
+ function buildDocs(items, project) {
138
+ var docs = [];
139
+ for (var i = 0; i < (items || []).length; i++) {
140
+ var projected = project(items[i], i);
141
+ if (!projected) continue;
142
+ var texts = segmentTexts(projected.fields);
143
+ for (var s = 0; s < texts.length; s++) {
144
+ docs.push({
145
+ id: projected.id,
146
+ text: texts[s],
147
+ meta: { position: i, segment: s, id: projected.id, meta: projected.meta },
148
+ });
149
+ }
150
+ }
151
+ return docs;
152
+ }
153
+
154
+ // Rank with the shared engine. Returns [{ id, score, meta }] highest first, one
155
+ // entry per original item. A query that tokenizes to nothing scores nothing,
156
+ // rather than silently falling back to a different matching strategy.
157
+ //
158
+ // Aggregation across an item's segments is the maximum segment score: a record
159
+ // is as relevant as its best-matching part, so a long record is neither
160
+ // penalised for its unmatched bulk nor rewarded for repeating a term across
161
+ // windows that overlap.
162
+ function rank(items, query, project, maxResults) {
163
+ var docs = buildDocs(items, project);
164
+ if (docs.length === 0) return [];
165
+ var index = sessionSearch.buildIndex(docs);
166
+ var scored = sessionSearch.searchIndex(index, query, docs.length);
167
+
168
+ var bestByPosition = new Map();
169
+ for (var i = 0; i < scored.length; i++) {
170
+ var envelope = scored[i].meta;
171
+ if (!envelope) continue;
172
+ var existing = bestByPosition.get(envelope.position);
173
+ if (existing && existing.score >= scored[i].score) continue;
174
+ bestByPosition.set(envelope.position, { id: envelope.id, score: scored[i].score, meta: envelope.meta });
175
+ }
176
+
177
+ var collapsed = [];
178
+ bestByPosition.forEach(function (entry) { collapsed.push(entry); });
179
+ // Stable, deterministic ordering before the caller applies its own
180
+ // recency and reference tie-breaks.
181
+ collapsed.sort(function (a, b) {
182
+ return b.score - a.score || String(a.id).localeCompare(String(b.id));
183
+ });
184
+ var limit = maxResults || collapsed.length;
185
+ return collapsed.slice(0, limit);
186
+ }
187
+
188
+ // A snippet centred on the first query term that appears in the text, so the
189
+ // excerpt shows why the document matched rather than just its opening. Works
190
+ // across the whole record, not only its first segment.
191
+ function snippet(text, query, maxChars) {
192
+ if (typeof text !== "string" || !text) return "";
193
+ var limit = maxChars || MAX_SNIPPET_CHARS;
194
+ var haystack = text.toLowerCase();
195
+ var tokens = sessionSearch.tokenize(query || "");
196
+ var best = -1;
197
+ for (var i = 0; i < tokens.length; i++) {
198
+ var at = haystack.indexOf(tokens[i]);
199
+ if (at !== -1 && (best === -1 || at < best)) best = at;
200
+ }
201
+ if (best === -1) {
202
+ var raw = (query || "").trim().toLowerCase();
203
+ if (raw) best = haystack.indexOf(raw);
204
+ }
205
+ var start = best === -1 ? 0 : Math.max(0, best - SNIPPET_LEAD_CHARS);
206
+ return text.substring(start, start + limit);
207
+ }
208
+
209
+ module.exports = {
210
+ MAX_SNIPPET_CHARS: MAX_SNIPPET_CHARS,
211
+ SEGMENT_CHARS: SEGMENT_CHARS,
212
+ SEGMENT_OVERLAP: SEGMENT_OVERLAP,
213
+ SEGMENT_STRIDE: SEGMENT_STRIDE,
214
+ MAX_SEGMENTS_PER_ITEM: MAX_SEGMENTS_PER_ITEM,
215
+ MAX_INDEXED_CHARS: MAX_INDEXED_CHARS,
216
+ tokenize: sessionSearch.tokenize,
217
+ weightedText: weightedText,
218
+ planSegments: planSegments,
219
+ coverage: coverage,
220
+ segmentTexts: segmentTexts,
221
+ buildDocs: buildDocs,
222
+ rank: rank,
223
+ snippet: snippet,
224
+ };
@@ -0,0 +1,55 @@
1
+ // Query-bound proposal tool for Clay-led Mate creation interviews.
2
+
3
+ var z;
4
+ try { z = require("zod"); } catch (e) { z = null; }
5
+
6
+ function schema() {
7
+ if (!z) return {};
8
+ return {
9
+ name: z.string().describe("The proposed Mate name"),
10
+ bio: z.string().describe("A concise public description of how this Mate helps"),
11
+ relationship: z.string().describe("The working relationship this Mate should have with the user"),
12
+ activities: z.string().describe("JSON array of the Mate's main activities or contexts"),
13
+ communicationStyle: z.string().describe("JSON array of communication preferences"),
14
+ autonomy: z.string().describe("A concise description of autonomy, boundaries, and escalation behavior"),
15
+ identityMarkdown: z.string().describe("The complete first-person Mate identity in Markdown"),
16
+ };
17
+ }
18
+
19
+ function parseArray(value, field) {
20
+ var parsed;
21
+ try { parsed = JSON.parse(value || "[]"); } catch (e) { throw new Error(field + " must be a JSON array."); }
22
+ if (!Array.isArray(parsed)) throw new Error(field + " must be a JSON array.");
23
+ return parsed;
24
+ }
25
+
26
+ function getToolDefs(onPropose) {
27
+ return [{
28
+ name: "propose_mate",
29
+ description: "Present the completed Mate identity for explicit user approval. This does not create the Mate until the user approves the inline proposal.",
30
+ inputSchema: schema(),
31
+ handler: function (args) {
32
+ var proposal;
33
+ try {
34
+ proposal = {
35
+ name: args.name,
36
+ bio: args.bio,
37
+ relationship: args.relationship,
38
+ activities: parseArray(args.activities, "activities"),
39
+ communicationStyle: parseArray(args.communicationStyle, "communicationStyle"),
40
+ autonomy: args.autonomy,
41
+ identityMarkdown: args.identityMarkdown,
42
+ };
43
+ } catch (error) {
44
+ return Promise.resolve({ content: [{ type: "text", text: "Error: " + error.message }], isError: true });
45
+ }
46
+ return onPropose(proposal).then(function (result) {
47
+ if (result && result.action === "create") return { content: [{ type: "text", text: "Mate created: " + proposal.name }] };
48
+ if (result && result.action === "error") return { content: [{ type: "text", text: "Error: " + (result.error || "The Mate could not be created.") }], isError: true };
49
+ return { content: [{ type: "text", text: "Mate proposal was cancelled by the user." }] };
50
+ });
51
+ },
52
+ }];
53
+ }
54
+
55
+ module.exports = { getToolDefs: getToolDefs };
@@ -0,0 +1,151 @@
1
+ // SDK-free `clay-knowledge` MCP tool definitions for Mate Knowledge.
2
+ //
3
+ // Two disjoint tool sets. An ordinary Mate gets tools with no mateId, owner, or
4
+ // scope argument at all, because the binding decides the scope and a tool
5
+ // argument must never be able to widen it or reveal that other Mates exist.
6
+ // Authoritative builtin Clay gets same-user cross-Mate read tools that may name
7
+ // a mateId for list and search, while read resolves an opaque reference inside
8
+ // the authorized user's Mate scopes only.
9
+
10
+ var buildShape = require("./session-spawn-mcp-server").buildShape;
11
+ var service = require("./mate-knowledge-service");
12
+
13
+ var MATE_CONTRACT =
14
+ "Your Knowledge is the durable personal and expertise context you own: what you have learned, the material you keep, and your accumulated observations. " +
15
+ "It is yours alone. There are no other Knowledge collections available to you here, and project Logs are a separate, unrelated surface. " +
16
+ "Search it when recalling something specific would genuinely change your answer, and read only the records you actually need. " +
17
+ "Do not enumerate or restate your Knowledge to the user unasked, and never reproduce whole records as filler.";
18
+
19
+ var CLAY_CONTRACT =
20
+ "You may read the Knowledge of Mates belonging to the current user in order to coordinate between them, and only for that user. " +
21
+ "Use it to find who holds relevant expertise or context, and to answer with that context attributed to the Mate that owns it. " +
22
+ "Search first, read narrowly, and cite the owning Mate. Never reproduce a Mate's Knowledge wholesale, and never present it as your own.";
23
+
24
+ var REF_DESCRIPTION = "Opaque knowledge reference returned by a list or search tool.";
25
+ var PAGE_DESCRIPTION = "Page size, from 1 to " + service.MAX_PAGE + ".";
26
+ var CURSOR_DESCRIPTION = "Opaque pagination cursor from a previous response.";
27
+ var OFFSET_DESCRIPTION = "Character offset to start from. Omit for the beginning; pass the previous response's nextOffset to continue.";
28
+ var MAX_CHARS_DESCRIPTION = "Characters to return, from 1 to " + service.MAX_READ_CHARS + ". Defaults to " + service.DEFAULT_READ_CHARS + ".";
29
+ var READ_RESULT = " The whole record is reassembled and verified before any slice is returned. The response carries offset, totalChars, nextOffset, and complete; when nextOffset is not null, call again with it to continue.";
30
+
31
+ function textResult(value) {
32
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
33
+ }
34
+
35
+ function errorResult(error) {
36
+ return Promise.resolve({
37
+ content: [{ type: "text", text: "Error: " + (error && error.message ? error.message : String(error)) }],
38
+ isError: true,
39
+ });
40
+ }
41
+
42
+ // An unbound descriptor exists only so a tool list can be advertised before a
43
+ // session is known. Every call against it fails closed.
44
+ function handler(bound, method) {
45
+ return function (args) {
46
+ if (!bound || typeof bound[method] !== "function") {
47
+ return errorResult(new Error("Knowledge tools require an exact session-bound Mate."));
48
+ }
49
+ try {
50
+ return textResult(bound[method](args || {}));
51
+ } catch (e) {
52
+ return errorResult(e);
53
+ }
54
+ };
55
+ }
56
+
57
+ function mateTools(bound) {
58
+ return [
59
+ {
60
+ name: "list_knowledge",
61
+ description: MATE_CONTRACT + " List your own Knowledge records, most recently updated first. Returns summaries with a short preview, not full records.",
62
+ inputSchema: buildShape({
63
+ kind: { type: "string", description: "Optional record kind filter, such as knowledge-file, memory-summary, session-digest, or user-observation." },
64
+ cursor: { type: "string", description: CURSOR_DESCRIPTION },
65
+ limit: { type: "number", description: PAGE_DESCRIPTION },
66
+ }),
67
+ handler: handler(bound, "listKnowledge"),
68
+ },
69
+ {
70
+ name: "search_knowledge",
71
+ description: MATE_CONTRACT + " Search your own Knowledge by relevance. Returns ranked summaries with a matching snippet.",
72
+ inputSchema: buildShape({
73
+ query: { type: "string", description: "Search query." },
74
+ kind: { type: "string", description: "Optional record kind filter." },
75
+ cursor: { type: "string", description: CURSOR_DESCRIPTION },
76
+ limit: { type: "number", description: PAGE_DESCRIPTION },
77
+ }, ["query"]),
78
+ handler: handler(bound, "searchKnowledge"),
79
+ },
80
+ {
81
+ name: "read_knowledge",
82
+ description: MATE_CONTRACT + " Read one of your own Knowledge records. Read only what you need." + READ_RESULT,
83
+ inputSchema: buildShape({
84
+ ref: { type: "string", description: REF_DESCRIPTION },
85
+ offset: { type: "number", description: OFFSET_DESCRIPTION },
86
+ maxChars: { type: "number", description: MAX_CHARS_DESCRIPTION },
87
+ }, ["ref"]),
88
+ handler: handler(bound, "readKnowledge"),
89
+ },
90
+ ];
91
+ }
92
+
93
+ function clayTools(bound) {
94
+ return [
95
+ {
96
+ name: "list_mate_knowledge",
97
+ description: CLAY_CONTRACT + " List Knowledge records across the current user's Mates, or one named Mate. Available only to authoritative builtin Clay.",
98
+ inputSchema: buildShape({
99
+ mateId: { type: "string", description: "Optional exact Mate id belonging to the current user. Omit to span all of them." },
100
+ kind: { type: "string", description: "Optional record kind filter." },
101
+ cursor: { type: "string", description: CURSOR_DESCRIPTION },
102
+ limit: { type: "number", description: PAGE_DESCRIPTION },
103
+ }),
104
+ handler: handler(bound, "listMateKnowledge"),
105
+ },
106
+ {
107
+ name: "search_mate_knowledge",
108
+ description: CLAY_CONTRACT + " Search Knowledge across the current user's Mates, or one named Mate. Results identify the owning Mate. Available only to authoritative builtin Clay.",
109
+ inputSchema: buildShape({
110
+ query: { type: "string", description: "Search query." },
111
+ mateId: { type: "string", description: "Optional exact Mate id belonging to the current user." },
112
+ kind: { type: "string", description: "Optional record kind filter." },
113
+ cursor: { type: "string", description: CURSOR_DESCRIPTION },
114
+ limit: { type: "number", description: PAGE_DESCRIPTION },
115
+ }, ["query"]),
116
+ handler: handler(bound, "searchMateKnowledge"),
117
+ },
118
+ {
119
+ name: "read_mate_knowledge",
120
+ description: CLAY_CONTRACT + " Read one Knowledge record using the opaque reference returned by a list or search tool. The reference resolves only inside the current user's Mate Knowledge. Available only to authoritative builtin Clay." + READ_RESULT,
121
+ inputSchema: buildShape({
122
+ ref: { type: "string", description: REF_DESCRIPTION },
123
+ offset: { type: "number", description: OFFSET_DESCRIPTION },
124
+ maxChars: { type: "number", description: MAX_CHARS_DESCRIPTION },
125
+ }, ["ref"]),
126
+ handler: handler(bound, "readMateKnowledge"),
127
+ },
128
+ ];
129
+ }
130
+
131
+ // A binding is either one Mate's own scope or Clay's cross-Mate read view. The
132
+ // two sets are never advertised together, so no tool name is duplicated.
133
+ function getToolDefs(bound, includeClay) {
134
+ return includeClay === true ? clayTools(bound) : mateTools(bound);
135
+ }
136
+
137
+ function createMcpServer(adapter, bound, includeClay) {
138
+ if (!adapter || typeof adapter.createToolServer !== "function") return null;
139
+ return adapter.createToolServer({
140
+ name: "clay-knowledge",
141
+ version: "1.0.0",
142
+ tools: getToolDefs(bound, includeClay),
143
+ });
144
+ }
145
+
146
+ module.exports = {
147
+ MATE_CONTRACT: MATE_CONTRACT,
148
+ CLAY_CONTRACT: CLAY_CONTRACT,
149
+ getToolDefs: getToolDefs,
150
+ createMcpServer: createMcpServer,
151
+ };