clay-server 4.0.0-beta.9 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -7
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -0,0 +1,488 @@
1
+ // Read projection and authorization for Mate Knowledge.
2
+ //
3
+ // Reads the continuously synchronized Clay Knowledge records, never the legacy
4
+ // files. Chunk records and tombstones are excluded, chunked content is
5
+ // reassembled and hash-verified, and a record that cannot be read back exactly
6
+ // is refused rather than returned partially.
7
+ //
8
+ // Authorization mirrors project-logs-service.js: a caller never states who it
9
+ // is. An ordinary Mate is bound to its own scope and can express no other, and
10
+ // only the authoritative builtin Clay host agent receives same-user cross-Mate
11
+ // reads. No binding ever spans users.
12
+
13
+ var path = require("path");
14
+ var builtinMates = require("./builtin-mates");
15
+ var knowledgeImport = require("./knowledge-import");
16
+ var knowledgeSearch = require("./knowledge-search");
17
+ var mateSync = require("./mate-knowledge-sync");
18
+
19
+ var MAX_PAGE = 50;
20
+ var DEFAULT_PAGE = 20;
21
+ var MAX_QUERY_CHARS = 200;
22
+ var MAX_PREVIEW_CHARS = 240;
23
+ var MAX_SNIPPET_CHARS = 320;
24
+ // Reads are bounded and continuable rather than refused. A record larger than
25
+ // one response is returned as verified slices the caller can page through.
26
+ var DEFAULT_READ_CHARS = 8000;
27
+ var MAX_READ_CHARS = 40000;
28
+ var REF_PATTERN = /^know:[A-Za-z0-9_-]{24}$/;
29
+
30
+ // Name outranks body slightly; a Mate's knowledge file names are meaningful,
31
+ // while journal lines all share one file name and rank on content.
32
+ var NAME_WEIGHT = 2;
33
+ var BODY_WEIGHT = 1;
34
+
35
+ function parseLimit(value) {
36
+ var limit = Number(value);
37
+ if (!Number.isFinite(limit)) limit = DEFAULT_PAGE;
38
+ return Math.max(1, Math.min(MAX_PAGE, Math.floor(limit)));
39
+ }
40
+
41
+ function encodeCursor(offset) {
42
+ return offset > 0 ? Buffer.from(String(offset), "utf8").toString("base64url") : null;
43
+ }
44
+
45
+ function decodeCursor(value) {
46
+ if (!value) return 0;
47
+ try {
48
+ var parsed = Number(Buffer.from(String(value), "base64url").toString("utf8"));
49
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
50
+ } catch (e) {
51
+ return 0;
52
+ }
53
+ }
54
+
55
+ function page(items, args) {
56
+ var offset = decodeCursor(args && args.cursor);
57
+ var limit = parseLimit(args && args.limit);
58
+ return {
59
+ items: items.slice(offset, offset + limit),
60
+ nextCursor: offset + limit < items.length ? encodeCursor(offset + limit) : null,
61
+ total: items.length,
62
+ };
63
+ }
64
+
65
+ function parseOffset(value) {
66
+ var offset = Number(value);
67
+ if (!Number.isFinite(offset) || offset < 0) return 0;
68
+ return Math.floor(offset);
69
+ }
70
+
71
+ function parseReadSize(value) {
72
+ var size = Number(value);
73
+ if (!Number.isFinite(size)) size = DEFAULT_READ_CHARS;
74
+ return Math.max(1, Math.min(MAX_READ_CHARS, Math.floor(size)));
75
+ }
76
+
77
+ // Never end a slice inside a UTF-16 surrogate pair, so every slice is on its
78
+ // own a valid string and concatenating slices reproduces the source exactly.
79
+ function safeSliceEnd(text, end) {
80
+ if (end >= text.length) return text.length;
81
+ var code = text.charCodeAt(end - 1);
82
+ if (code >= 0xd800 && code <= 0xdbff) return end - 1;
83
+ return end;
84
+ }
85
+
86
+ function clean(value, max) {
87
+ if (typeof value !== "string") return "";
88
+ var text = value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
89
+ return text.length > max ? text.substring(0, max) : text;
90
+ }
91
+
92
+ function attachMateKnowledgeService(ctx) {
93
+ var context = ctx || {};
94
+ var getProjects = context.getProjects;
95
+ var isMultiUser = context.isMultiUser;
96
+ var resolveMate = context.resolveMate;
97
+ var listMates = context.listMates || function () { return []; };
98
+ var baseDir = context.baseDir;
99
+ var importerCache = new Map();
100
+
101
+ function importerForScope(scopeId) {
102
+ if (context.openImporter) return context.openImporter(scopeId);
103
+ if (!importerCache.has(scopeId)) {
104
+ importerCache.set(scopeId, knowledgeImport.createKnowledgeImporter({ scopeId: scopeId, baseDir: baseDir }));
105
+ }
106
+ return importerCache.get(scopeId);
107
+ }
108
+
109
+ // --- Read projection --------------------------------------------------
110
+
111
+ // Bring this exact Mate's records up to date with its legacy files before
112
+ // reading them, so a direct filesystem edit or an offline deletion is visible
113
+ // on the next tool call instead of only after a daemon restart. Only ever
114
+ // called for a scope the principal is already authorized for. Reconciliation
115
+ // is idempotent, so a no-op call appends nothing.
116
+ // A reconciliation failure means the record for that source may no longer
117
+ // match the legacy file. Presenting the previous record would be stale
118
+ // content dressed as current, so every failure is attributed to a source and
119
+ // that source is withheld. A failure that cannot be attributed withholds the
120
+ // whole scope, because there is no way to know which record is stale.
121
+ function refresh(mate) {
122
+ if (context.syncBeforeRead === false || !mate.mateDir) {
123
+ return { failed: 0, files: {}, scopeFailed: false };
124
+ }
125
+ var summary;
126
+ try {
127
+ summary = mateSync.reconcileMate({
128
+ mateDir: mate.mateDir,
129
+ mateName: mate.mateName,
130
+ baseDir: baseDir,
131
+ importer: importerForScope(mate.scopeId),
132
+ actor: { type: "system" },
133
+ });
134
+ } catch (e) {
135
+ return { failed: 1, files: {}, scopeFailed: true };
136
+ }
137
+ var failed = (summary && summary.failed) || 0;
138
+ if (failed === 0) return { failed: 0, files: {}, scopeFailed: false };
139
+
140
+ var files = {};
141
+ var attributed = 0;
142
+ var errors = (summary && summary.errors) || [];
143
+ for (var i = 0; i < errors.length; i++) {
144
+ var name = errors[i] && errors[i].file;
145
+ // Only a plain basename is trusted as an identity; anything else is
146
+ // treated as unattributable rather than matched loosely.
147
+ if (typeof name !== "string" || !name || path.basename(name) !== name) continue;
148
+ if (!files[name]) attributed++;
149
+ files[name] = true;
150
+ }
151
+ return { failed: failed, files: files, scopeFailed: attributed < failed };
152
+ }
153
+
154
+ // entries() already excludes chunk records and tombstones. Chunked sources
155
+ // are reassembled and hash-verified here; one that does not verify is
156
+ // reported as degraded and left out of ranking rather than half-returned.
157
+ function materialize(mate, refreshed) {
158
+ var state = refreshed || refresh(mate);
159
+ var degraded = state.failed;
160
+ // An unattributable failure withholds every record in this scope.
161
+ if (state.scopeFailed) return { items: [], degraded: degraded, scopeFailed: true };
162
+ var importer = importerForScope(mate.scopeId);
163
+ var live = importer.entries();
164
+ var out = [];
165
+ for (var i = 0; i < live.length; i++) {
166
+ var entry = live[i];
167
+ var sourceName = entry.source && entry.source.fileName;
168
+ // Withhold the previous record for a source that just failed to reconcile.
169
+ if (sourceName && state.files[sourceName]) continue;
170
+ var read = importer.readContent(entry);
171
+ if (!read.complete) {
172
+ degraded++;
173
+ continue;
174
+ }
175
+ out.push({
176
+ ref: entry.ref,
177
+ kind: entry.kind,
178
+ name: entry.name,
179
+ content: read.content,
180
+ updatedAt: entry.updatedAt,
181
+ revisions: entry.revisions,
182
+ relPath: entry.source ? entry.source.relPath : null,
183
+ sourceDate: entry.source ? entry.source.sourceDate || null : null,
184
+ mateId: mate.mateId,
185
+ mateName: mate.mateName || null,
186
+ });
187
+ }
188
+ return { items: out, degraded: degraded, scopeFailed: false };
189
+ }
190
+
191
+ // Public shapes never carry the full body; read_knowledge does that.
192
+ function summary(item, includeMate) {
193
+ var out = {
194
+ ref: item.ref,
195
+ kind: item.kind,
196
+ name: item.name,
197
+ updatedAt: item.updatedAt,
198
+ revisions: item.revisions,
199
+ relPath: item.relPath,
200
+ sourceDate: item.sourceDate,
201
+ preview: clean(item.content, MAX_PREVIEW_CHARS),
202
+ };
203
+ if (includeMate) {
204
+ out.mateId = item.mateId;
205
+ out.mateName = item.mateName;
206
+ }
207
+ return out;
208
+ }
209
+
210
+ function projectItem(item) {
211
+ return {
212
+ id: item.ref,
213
+ fields: [
214
+ { text: item.name || "", weight: NAME_WEIGHT },
215
+ { text: item.content || "", weight: BODY_WEIGHT },
216
+ ],
217
+ meta: item,
218
+ };
219
+ }
220
+
221
+ function collect(mates) {
222
+ var items = [];
223
+ var degraded = 0;
224
+ var degradedScopes = 0;
225
+ for (var i = 0; i < mates.length; i++) {
226
+ var result = materialize(mates[i]);
227
+ degraded += result.degraded;
228
+ if (result.scopeFailed) degradedScopes++;
229
+ for (var j = 0; j < result.items.length; j++) items.push(result.items[j]);
230
+ }
231
+ return { items: items, degraded: degraded, degradedScopes: degradedScopes };
232
+ }
233
+
234
+ // How much of the candidate set could actually be indexed. Reported so an
235
+ // incomplete search is stated rather than implied by an empty result.
236
+ function coverageOf(candidates) {
237
+ var incomplete = 0;
238
+ for (var i = 0; i < candidates.length; i++) {
239
+ if (!knowledgeSearch.coverage(projectItem(candidates[i]).fields).complete) incomplete++;
240
+ }
241
+ return incomplete;
242
+ }
243
+
244
+ function listFor(mates, args, includeMate) {
245
+ var options = args || {};
246
+ var collected = collect(mates);
247
+ var items = collected.items;
248
+ if (options.kind) {
249
+ var kind = clean(String(options.kind), 60);
250
+ items = items.filter(function (item) { return item.kind === kind; });
251
+ }
252
+ items.sort(function (a, b) {
253
+ return b.updatedAt - a.updatedAt || knowledgeSearch.compareIds(a.ref, b.ref);
254
+ });
255
+ var result = page(items, options);
256
+ return {
257
+ entries: result.items.map(function (item) { return summary(item, includeMate); }),
258
+ nextCursor: result.nextCursor,
259
+ total: result.total,
260
+ degraded: collected.degraded,
261
+ degradedScopes: collected.degradedScopes,
262
+ };
263
+ }
264
+
265
+ function searchFor(mates, args, includeMate) {
266
+ var options = args || {};
267
+ var query = clean(options.query || "", MAX_QUERY_CHARS);
268
+ if (!query) throw new Error("A search query is required.");
269
+ var collected = collect(mates);
270
+ var candidates = collected.items;
271
+ if (options.kind) {
272
+ var kind = clean(String(options.kind), 60);
273
+ candidates = candidates.filter(function (item) { return item.kind === kind; });
274
+ }
275
+ var ranked = knowledgeSearch.rank(candidates, query, projectItem, candidates.length);
276
+ var hits = [];
277
+ for (var i = 0; i < ranked.length; i++) {
278
+ var item = ranked[i].meta;
279
+ var hit = summary(item, includeMate);
280
+ hit.score = ranked[i].score;
281
+ hit.snippet = clean(knowledgeSearch.snippet(item.content, query, MAX_SNIPPET_CHARS), MAX_SNIPPET_CHARS);
282
+ hits.push(hit);
283
+ }
284
+ hits.sort(function (a, b) {
285
+ return b.score - a.score || b.updatedAt - a.updatedAt || knowledgeSearch.compareIds(a.ref, b.ref);
286
+ });
287
+ var result = page(hits, options);
288
+ return {
289
+ results: result.items,
290
+ nextCursor: result.nextCursor,
291
+ total: result.total,
292
+ degraded: collected.degraded,
293
+ degradedScopes: collected.degradedScopes,
294
+ // Records whose searchable text exceeds what the index can cover. A term
295
+ // appearing only past that bound cannot be ranked, so say so.
296
+ incompleteCoverage: coverageOf(candidates),
297
+ coveredCharsPerRecord: knowledgeSearch.MAX_INDEXED_CHARS,
298
+ };
299
+ }
300
+
301
+ // A ref is resolved only inside the scopes this principal is authorized for,
302
+ // so an opaque ref from elsewhere simply does not exist here.
303
+ //
304
+ // The whole source is always reassembled and hash-verified before any slicing,
305
+ // so a bounded read is a window onto verified content, never a shortcut past
306
+ // verification. A record larger than one response is continued through
307
+ // nextOffset rather than refused.
308
+ function readFor(mates, args, includeMate) {
309
+ var options = args || {};
310
+ var ref = options.ref;
311
+ if (typeof ref !== "string" || !REF_PATTERN.test(ref)) throw new Error("A valid knowledge reference is required.");
312
+ var offset = parseOffset(options.offset);
313
+ var maxChars = parseReadSize(options.maxChars);
314
+ for (var i = 0; i < mates.length; i++) {
315
+ var state = refresh(mates[i]);
316
+ var importer = importerForScope(mates[i].scopeId);
317
+ var live = importer.entries();
318
+ for (var j = 0; j < live.length; j++) {
319
+ if (live[j].ref !== ref) continue;
320
+ // A record whose source just failed to reconcile is refused rather than
321
+ // served from the previous, possibly stale, content.
322
+ if (state.scopeFailed) {
323
+ throw new Error("This Mate's Knowledge could not be brought up to date, so reads are withheld.");
324
+ }
325
+ var failedName = live[j].source && live[j].source.fileName;
326
+ if (failedName && state.files[failedName]) {
327
+ throw new Error("The source of this knowledge record could not be brought up to date: " + failedName);
328
+ }
329
+ var read = importer.readContent(live[j]);
330
+ if (!read.complete) {
331
+ throw new Error("This knowledge record could not be read back completely: " + (read.reason || "unknown"));
332
+ }
333
+ var total = read.content.length;
334
+ if (offset > total) throw new Error("Offset " + offset + " is beyond the end of this record (" + total + " characters).");
335
+ var end = safeSliceEnd(read.content, Math.min(total, offset + maxChars));
336
+ var slice = read.content.substring(offset, end);
337
+ var out = summary({
338
+ ref: live[j].ref,
339
+ kind: live[j].kind,
340
+ name: live[j].name,
341
+ content: read.content,
342
+ updatedAt: live[j].updatedAt,
343
+ revisions: live[j].revisions,
344
+ relPath: live[j].source ? live[j].source.relPath : null,
345
+ sourceDate: live[j].source ? live[j].source.sourceDate || null : null,
346
+ mateId: mates[i].mateId,
347
+ mateName: mates[i].mateName || null,
348
+ }, includeMate);
349
+ delete out.preview;
350
+ out.content = slice;
351
+ out.offset = offset;
352
+ out.totalChars = total;
353
+ out.nextOffset = end < total ? end : null;
354
+ // True only when this single response carried the whole record.
355
+ out.complete = offset === 0 && end >= total;
356
+ return out;
357
+ }
358
+ }
359
+ throw new Error("Knowledge record not found.");
360
+ }
361
+
362
+ // --- Binding ----------------------------------------------------------
363
+
364
+ function scopeForMate(mateDir, mateId, mateName) {
365
+ var scope = mateSync.scopeForMateDir(mateDir);
366
+ if (!scope || scope.mateId !== mateId) return null;
367
+ return { scopeId: scope.scopeId, mateId: mateId, mateName: mateName || null, mateDir: scope.mateDir };
368
+ }
369
+
370
+ // Exact live Mate project and session identity, confirmed against the server
371
+ // Mate registry. Anything that fails to line up yields no capability.
372
+ function resolve(source) {
373
+ if (!source || source.isMate !== true || !source.mateId) return null;
374
+ var project = getProjects().get(source.projectSlug);
375
+ if (!project) return null;
376
+ var status = project.getStatus();
377
+ if (!status || status.isMate !== true) return null;
378
+ if (status.mateId !== source.mateId || status.projectOwnerId !== source.projectOwnerId) return null;
379
+ if (source.session) {
380
+ var manager = project.getSessionManager ? project.getSessionManager() : project.sm;
381
+ if (!manager || !manager.sessions || manager.sessions.get(source.session.localId) !== source.session) return null;
382
+ }
383
+ if (typeof resolveMate !== "function") return null;
384
+ var mate = resolveMate(source.projectOwnerId || null, source.mateId);
385
+ if (!mate || mate.id !== source.mateId) return null;
386
+ if (isMultiUser()) {
387
+ if (!source.projectOwnerId) return null;
388
+ if (mate.createdBy !== source.projectOwnerId) return null;
389
+ }
390
+ if (!status.path) return null;
391
+ var own = scopeForMate(status.path, source.mateId, mate.name || null);
392
+ if (!own) return null;
393
+
394
+ var def = mate.builtinKey ? builtinMates.getBuiltinByKey(mate.builtinKey) : null;
395
+ var isClay = !!(mate.builtinKey === "clay" && def && def.hostAgent === true);
396
+ return { own: own, isClay: isClay, ownerId: source.projectOwnerId || null };
397
+ }
398
+
399
+ // A tool handler captured earlier stays valid only while its exact source
400
+ // still resolves. Identity is re-derived on every call, so a session that has
401
+ // since been dropped, a Mate that has been deleted, or a registry entry that
402
+ // has changed hands loses the capability immediately rather than continuing
403
+ // to work through a handler that was created while it was still live.
404
+ function bind(source) {
405
+ var initial = resolve(source);
406
+ if (!initial) return null;
407
+
408
+ function revalidate() {
409
+ var current = resolve(source);
410
+ if (!current) throw new Error("This Knowledge binding is no longer valid for the current session.");
411
+ if (current.isClay !== initial.isClay || current.own.scopeId !== initial.own.scopeId) {
412
+ throw new Error("This Knowledge binding is no longer valid for the current session.");
413
+ }
414
+ return current;
415
+ }
416
+
417
+ return initial.isClay ? createClayBound(revalidate, initial) : createMateBound(revalidate, initial);
418
+ }
419
+
420
+ // An ordinary Mate's own scope, and nothing else. There is no argument on
421
+ // any tool that could widen it.
422
+ function createMateBound(revalidate, initial) {
423
+ function ownScope() {
424
+ return [revalidate().own];
425
+ }
426
+ return {
427
+ isClay: false,
428
+ mateId: initial.own.mateId,
429
+ scopeId: initial.own.scopeId,
430
+ listKnowledge: function (args) { return listFor(ownScope(), args || {}, false); },
431
+ searchKnowledge: function (args) { return searchFor(ownScope(), args || {}, false); },
432
+ readKnowledge: function (args) { return readFor(ownScope(), args || {}, false); },
433
+ };
434
+ }
435
+
436
+ // Every Mate belonging to the same user, resolved fresh on each call so a
437
+ // newly created or deleted Mate is reflected without a rebind. Never spans
438
+ // users: the Mate list is derived from the bound owner id alone.
439
+ function createClayBound(revalidate, initial) {
440
+ function authorizedMates(mateId) {
441
+ var current = revalidate();
442
+ var ownerId = current.ownerId;
443
+ var own = current.own;
444
+ var out = [];
445
+ var listed = listMates(ownerId) || [];
446
+ for (var i = 0; i < listed.length; i++) {
447
+ var entry = listed[i];
448
+ if (!entry || !entry.id || !entry.dir) continue;
449
+ if (isMultiUser() && ownerId && entry.createdBy && entry.createdBy !== ownerId) continue;
450
+ var scope = scopeForMate(entry.dir, entry.id, entry.name || null);
451
+ if (!scope) continue;
452
+ out.push(scope);
453
+ }
454
+ if (out.length === 0) out.push(own);
455
+ if (!mateId) return out;
456
+ var wanted = String(mateId);
457
+ var filtered = out.filter(function (scope) { return scope.mateId === wanted; });
458
+ if (filtered.length === 0) throw new Error("Mate not found for the current user.");
459
+ return filtered;
460
+ }
461
+ return {
462
+ isClay: true,
463
+ mateId: initial.own.mateId,
464
+ scopeId: initial.own.scopeId,
465
+ listMateKnowledge: function (args) {
466
+ return listFor(authorizedMates(args && args.mateId), args || {}, true);
467
+ },
468
+ searchMateKnowledge: function (args) {
469
+ return searchFor(authorizedMates(args && args.mateId), args || {}, true);
470
+ },
471
+ readMateKnowledge: function (args) {
472
+ return readFor(authorizedMates(null), args || {}, true);
473
+ },
474
+ };
475
+ }
476
+
477
+ return { bind: bind };
478
+ }
479
+
480
+ module.exports = {
481
+ MAX_PAGE: MAX_PAGE,
482
+ MAX_READ_CHARS: MAX_READ_CHARS,
483
+ DEFAULT_READ_CHARS: DEFAULT_READ_CHARS,
484
+ REF_PATTERN: REF_PATTERN,
485
+ NAME_WEIGHT: NAME_WEIGHT,
486
+ BODY_WEIGHT: BODY_WEIGHT,
487
+ attachMateKnowledgeService: attachMateKnowledgeService,
488
+ };