@prmichaelsen/remember-core 0.12.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 (198) hide show
  1. package/README.md +100 -0
  2. package/dist/collections/composite-ids.d.ts +47 -0
  3. package/dist/collections/composite-ids.d.ts.map +1 -0
  4. package/dist/collections/composite-ids.js +97 -0
  5. package/dist/collections/composite-ids.js.map +1 -0
  6. package/dist/collections/dot-notation.d.ts +37 -0
  7. package/dist/collections/dot-notation.d.ts.map +1 -0
  8. package/dist/collections/dot-notation.js +100 -0
  9. package/dist/collections/dot-notation.js.map +1 -0
  10. package/dist/collections/index.d.ts +7 -0
  11. package/dist/collections/index.d.ts.map +1 -0
  12. package/dist/collections/index.js +7 -0
  13. package/dist/collections/index.js.map +1 -0
  14. package/dist/collections/tracking-arrays.d.ts +28 -0
  15. package/dist/collections/tracking-arrays.d.ts.map +1 -0
  16. package/dist/collections/tracking-arrays.js +67 -0
  17. package/dist/collections/tracking-arrays.js.map +1 -0
  18. package/dist/config/debug.d.ts +36 -0
  19. package/dist/config/debug.d.ts.map +1 -0
  20. package/dist/config/debug.js +48 -0
  21. package/dist/config/debug.js.map +1 -0
  22. package/dist/config/environment.d.ts +71 -0
  23. package/dist/config/environment.d.ts.map +1 -0
  24. package/dist/config/environment.js +57 -0
  25. package/dist/config/environment.js.map +1 -0
  26. package/dist/config/index.d.ts +9 -0
  27. package/dist/config/index.d.ts.map +1 -0
  28. package/dist/config/index.js +27 -0
  29. package/dist/config/index.js.map +1 -0
  30. package/dist/constants/content-types.d.ts +60 -0
  31. package/dist/constants/content-types.d.ts.map +1 -0
  32. package/dist/constants/content-types.js +450 -0
  33. package/dist/constants/content-types.js.map +1 -0
  34. package/dist/constants/index.d.ts +3 -0
  35. package/dist/constants/index.d.ts.map +1 -0
  36. package/dist/constants/index.js +3 -0
  37. package/dist/constants/index.js.map +1 -0
  38. package/dist/database/firestore/index.d.ts +8 -0
  39. package/dist/database/firestore/index.d.ts.map +1 -0
  40. package/dist/database/firestore/index.js +10 -0
  41. package/dist/database/firestore/index.js.map +1 -0
  42. package/dist/database/firestore/init.d.ts +51 -0
  43. package/dist/database/firestore/init.d.ts.map +1 -0
  44. package/dist/database/firestore/init.js +92 -0
  45. package/dist/database/firestore/init.js.map +1 -0
  46. package/dist/database/firestore/paths.d.ts +54 -0
  47. package/dist/database/firestore/paths.d.ts.map +1 -0
  48. package/dist/database/firestore/paths.js +103 -0
  49. package/dist/database/firestore/paths.js.map +1 -0
  50. package/dist/database/weaviate/client.d.ts +68 -0
  51. package/dist/database/weaviate/client.d.ts.map +1 -0
  52. package/dist/database/weaviate/client.js +157 -0
  53. package/dist/database/weaviate/client.js.map +1 -0
  54. package/dist/database/weaviate/index.d.ts +6 -0
  55. package/dist/database/weaviate/index.d.ts.map +1 -0
  56. package/dist/database/weaviate/index.js +9 -0
  57. package/dist/database/weaviate/index.js.map +1 -0
  58. package/dist/database/weaviate/schema.d.ts +22 -0
  59. package/dist/database/weaviate/schema.d.ts.map +1 -0
  60. package/dist/database/weaviate/schema.js +50 -0
  61. package/dist/database/weaviate/schema.js.map +1 -0
  62. package/dist/database/weaviate/space-schema.d.ts +36 -0
  63. package/dist/database/weaviate/space-schema.d.ts.map +1 -0
  64. package/dist/database/weaviate/space-schema.js +65 -0
  65. package/dist/database/weaviate/space-schema.js.map +1 -0
  66. package/dist/database/weaviate/v2-collections.d.ts +160 -0
  67. package/dist/database/weaviate/v2-collections.d.ts.map +1 -0
  68. package/dist/database/weaviate/v2-collections.js +275 -0
  69. package/dist/database/weaviate/v2-collections.js.map +1 -0
  70. package/dist/errors/app-errors.d.ts +64 -0
  71. package/dist/errors/app-errors.d.ts.map +1 -0
  72. package/dist/errors/app-errors.js +90 -0
  73. package/dist/errors/app-errors.js.map +1 -0
  74. package/dist/errors/base.error.d.ts +15 -0
  75. package/dist/errors/base.error.d.ts.map +1 -0
  76. package/dist/errors/base.error.js +25 -0
  77. package/dist/errors/base.error.js.map +1 -0
  78. package/dist/errors/index.d.ts +19 -0
  79. package/dist/errors/index.d.ts.map +1 -0
  80. package/dist/errors/index.js +25 -0
  81. package/dist/errors/index.js.map +1 -0
  82. package/dist/index.d.ts +8 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +9 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/services/confirmation-token.service.d.ts +56 -0
  87. package/dist/services/confirmation-token.service.d.ts.map +1 -0
  88. package/dist/services/confirmation-token.service.js +118 -0
  89. package/dist/services/confirmation-token.service.js.map +1 -0
  90. package/dist/services/credentials-provider.d.ts +25 -0
  91. package/dist/services/credentials-provider.d.ts.map +1 -0
  92. package/dist/services/credentials-provider.js +31 -0
  93. package/dist/services/credentials-provider.js.map +1 -0
  94. package/dist/services/index.d.ts +11 -0
  95. package/dist/services/index.d.ts.map +1 -0
  96. package/dist/services/index.js +17 -0
  97. package/dist/services/index.js.map +1 -0
  98. package/dist/services/memory.service.d.ts +129 -0
  99. package/dist/services/memory.service.d.ts.map +1 -0
  100. package/dist/services/memory.service.js +294 -0
  101. package/dist/services/memory.service.js.map +1 -0
  102. package/dist/services/preferences.service.d.ts +27 -0
  103. package/dist/services/preferences.service.d.ts.map +1 -0
  104. package/dist/services/preferences.service.js +106 -0
  105. package/dist/services/preferences.service.js.map +1 -0
  106. package/dist/services/relationship.service.d.ts +75 -0
  107. package/dist/services/relationship.service.d.ts.map +1 -0
  108. package/dist/services/relationship.service.js +211 -0
  109. package/dist/services/relationship.service.js.map +1 -0
  110. package/dist/services/space-config.service.d.ts +22 -0
  111. package/dist/services/space-config.service.d.ts.map +1 -0
  112. package/dist/services/space-config.service.js +50 -0
  113. package/dist/services/space-config.service.js.map +1 -0
  114. package/dist/services/space.service.d.ts +160 -0
  115. package/dist/services/space.service.d.ts.map +1 -0
  116. package/dist/services/space.service.js +815 -0
  117. package/dist/services/space.service.js.map +1 -0
  118. package/dist/testing/index.d.ts +8 -0
  119. package/dist/testing/index.d.ts.map +1 -0
  120. package/dist/testing/index.js +8 -0
  121. package/dist/testing/index.js.map +1 -0
  122. package/dist/testing/test-data-generator.d.ts +100 -0
  123. package/dist/testing/test-data-generator.d.ts.map +1 -0
  124. package/dist/testing/test-data-generator.js +194 -0
  125. package/dist/testing/test-data-generator.js.map +1 -0
  126. package/dist/testing/weaviate-mock.d.ts +317 -0
  127. package/dist/testing/weaviate-mock.d.ts.map +1 -0
  128. package/dist/testing/weaviate-mock.js +233 -0
  129. package/dist/testing/weaviate-mock.js.map +1 -0
  130. package/dist/types/auth.types.d.ts +36 -0
  131. package/dist/types/auth.types.d.ts.map +1 -0
  132. package/dist/types/auth.types.js +9 -0
  133. package/dist/types/auth.types.js.map +1 -0
  134. package/dist/types/context.types.d.ts +78 -0
  135. package/dist/types/context.types.d.ts.map +1 -0
  136. package/dist/types/context.types.js +6 -0
  137. package/dist/types/context.types.js.map +1 -0
  138. package/dist/types/index.d.ts +14 -0
  139. package/dist/types/index.d.ts.map +1 -0
  140. package/dist/types/index.js +6 -0
  141. package/dist/types/index.js.map +1 -0
  142. package/dist/types/llm.types.d.ts +8 -0
  143. package/dist/types/llm.types.d.ts.map +1 -0
  144. package/dist/types/llm.types.js +8 -0
  145. package/dist/types/llm.types.js.map +1 -0
  146. package/dist/types/memory.types.d.ts +83 -0
  147. package/dist/types/memory.types.d.ts.map +1 -0
  148. package/dist/types/memory.types.js +6 -0
  149. package/dist/types/memory.types.js.map +1 -0
  150. package/dist/types/preferences.types.d.ts +285 -0
  151. package/dist/types/preferences.types.d.ts.map +1 -0
  152. package/dist/types/preferences.types.js +195 -0
  153. package/dist/types/preferences.types.js.map +1 -0
  154. package/dist/types/result.types.d.ts +60 -0
  155. package/dist/types/result.types.d.ts.map +1 -0
  156. package/dist/types/result.types.js +82 -0
  157. package/dist/types/result.types.js.map +1 -0
  158. package/dist/types/search.types.d.ts +49 -0
  159. package/dist/types/search.types.d.ts.map +1 -0
  160. package/dist/types/search.types.js +6 -0
  161. package/dist/types/search.types.js.map +1 -0
  162. package/dist/types/shared.types.d.ts +91 -0
  163. package/dist/types/shared.types.d.ts.map +1 -0
  164. package/dist/types/shared.types.js +57 -0
  165. package/dist/types/shared.types.js.map +1 -0
  166. package/dist/types/space.types.d.ts +82 -0
  167. package/dist/types/space.types.d.ts.map +1 -0
  168. package/dist/types/space.types.js +18 -0
  169. package/dist/types/space.types.js.map +1 -0
  170. package/dist/types/utils.types.d.ts +51 -0
  171. package/dist/types/utils.types.d.ts.map +1 -0
  172. package/dist/types/utils.types.js +4 -0
  173. package/dist/types/utils.types.js.map +1 -0
  174. package/dist/utils/auth-helpers.d.ts +23 -0
  175. package/dist/utils/auth-helpers.d.ts.map +1 -0
  176. package/dist/utils/auth-helpers.js +31 -0
  177. package/dist/utils/auth-helpers.js.map +1 -0
  178. package/dist/utils/debug.d.ts +45 -0
  179. package/dist/utils/debug.d.ts.map +1 -0
  180. package/dist/utils/debug.js +112 -0
  181. package/dist/utils/debug.js.map +1 -0
  182. package/dist/utils/error-handler.d.ts +46 -0
  183. package/dist/utils/error-handler.d.ts.map +1 -0
  184. package/dist/utils/error-handler.js +63 -0
  185. package/dist/utils/error-handler.js.map +1 -0
  186. package/dist/utils/filters.d.ts +42 -0
  187. package/dist/utils/filters.d.ts.map +1 -0
  188. package/dist/utils/filters.js +132 -0
  189. package/dist/utils/filters.js.map +1 -0
  190. package/dist/utils/index.d.ts +9 -0
  191. package/dist/utils/index.d.ts.map +1 -0
  192. package/dist/utils/index.js +9 -0
  193. package/dist/utils/index.js.map +1 -0
  194. package/dist/utils/logger.d.ts +22 -0
  195. package/dist/utils/logger.d.ts.map +1 -0
  196. package/dist/utils/logger.js +68 -0
  197. package/dist/utils/logger.js.map +1 -0
  198. package/package.json +100 -0
@@ -0,0 +1,815 @@
1
+ /**
2
+ * SpaceService — publish/retract/revise/confirm/deny/moderate/search/query.
3
+ *
4
+ * Extracted from 8 remember-mcp tool handlers:
5
+ * publish.ts, retract.ts, revise.ts, confirm.ts,
6
+ * deny.ts, moderate.ts, search-space.ts, query-space.ts
7
+ *
8
+ * Design: accepts Weaviate client + user collection + ConfirmationTokenService + Logger via constructor.
9
+ * No MCP-specific code.
10
+ */
11
+ import { Filters } from 'weaviate-client';
12
+ import { fetchMemoryWithAllProperties } from '../database/weaviate/client.js';
13
+ import { ensurePublicCollection, isValidSpaceId } from '../database/weaviate/space-schema.js';
14
+ import { CollectionType, getCollectionName } from '../collections/dot-notation.js';
15
+ import { generateCompositeId } from '../collections/composite-ids.js';
16
+ import { getSpaceConfig } from './space-config.service.js';
17
+ import { canModerate, canModerateAny } from '../utils/auth-helpers.js';
18
+ const ACTION_TO_STATUS = {
19
+ approve: 'approved',
20
+ reject: 'rejected',
21
+ remove: 'removed',
22
+ };
23
+ // ─── Revision History Helpers ───────────────────────────────────────────
24
+ const MAX_REVISION_HISTORY = 10;
25
+ export function parseRevisionHistory(raw) {
26
+ if (!raw || typeof raw !== 'string')
27
+ return [];
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (!Array.isArray(parsed))
31
+ return [];
32
+ return parsed.filter((e) => typeof e === 'object' &&
33
+ e !== null &&
34
+ typeof e.content === 'string' &&
35
+ typeof e.revised_at === 'string');
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ export function buildRevisionHistory(existing, oldContent, revisedAt) {
42
+ const updated = [{ content: oldContent, revised_at: revisedAt }, ...existing];
43
+ return updated.slice(0, MAX_REVISION_HISTORY);
44
+ }
45
+ // ─── Filter Helpers ─────────────────────────────────────────────────────
46
+ export function buildModerationFilter(collection, moderationFilter = 'approved') {
47
+ if (moderationFilter === 'all')
48
+ return null;
49
+ if (moderationFilter === 'approved') {
50
+ return Filters.or(collection.filter.byProperty('moderation_status').equal('approved'), collection.filter.byProperty('moderation_status').isNull(true));
51
+ }
52
+ return collection.filter.byProperty('moderation_status').equal(moderationFilter);
53
+ }
54
+ // ─── Service ────────────────────────────────────────────────────────────
55
+ /**
56
+ * SpaceService provides transport-agnostic space operations.
57
+ *
58
+ * @param weaviateClient - Weaviate client instance (for multi-collection access)
59
+ * @param userCollection - User's personal Weaviate collection
60
+ * @param userId - The owner user ID
61
+ * @param confirmationTokenService - ConfirmationTokenService instance
62
+ * @param logger - Logger instance
63
+ */
64
+ export class SpaceService {
65
+ weaviateClient;
66
+ userCollection;
67
+ userId;
68
+ confirmationTokenService;
69
+ logger;
70
+ constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger) {
71
+ this.weaviateClient = weaviateClient;
72
+ this.userCollection = userCollection;
73
+ this.userId = userId;
74
+ this.confirmationTokenService = confirmationTokenService;
75
+ this.logger = logger;
76
+ }
77
+ // ── Publish (phase 1: generate confirmation token) ──────────────────
78
+ async publish(input) {
79
+ const spaces = input.spaces || [];
80
+ const groups = input.groups || [];
81
+ if (spaces.length === 0 && groups.length === 0) {
82
+ throw new Error('Must specify at least one space or group to publish to');
83
+ }
84
+ // Validate space IDs
85
+ if (spaces.length > 0) {
86
+ const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
87
+ if (invalidSpaces.length > 0) {
88
+ throw new Error(`Invalid space IDs: ${invalidSpaces.join(', ')}`);
89
+ }
90
+ }
91
+ // Validate group IDs (no dots, not empty)
92
+ if (groups.length > 0) {
93
+ const invalidGroups = groups.filter((g) => !g || g.includes('.') || g.trim() === '');
94
+ if (invalidGroups.length > 0) {
95
+ throw new Error('Group IDs cannot be empty or contain dots');
96
+ }
97
+ }
98
+ // Verify memory exists, belongs to user, is a memory
99
+ const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
100
+ if (!memory)
101
+ throw new Error(`Memory not found: ${input.memory_id}`);
102
+ if (memory.properties.user_id !== this.userId)
103
+ throw new Error('Permission denied: not memory owner');
104
+ if (memory.properties.doc_type !== 'memory')
105
+ throw new Error('Only memories can be published');
106
+ // Generate confirmation token
107
+ const { token } = await this.confirmationTokenService.createRequest(this.userId, 'publish_memory', {
108
+ memory_id: input.memory_id,
109
+ spaces,
110
+ groups,
111
+ additional_tags: input.additional_tags || [],
112
+ });
113
+ this.logger.info('Publish confirmation created', {
114
+ userId: this.userId,
115
+ memoryId: input.memory_id,
116
+ spaces,
117
+ groups,
118
+ });
119
+ return { token };
120
+ }
121
+ // ── Retract (phase 1: generate confirmation token) ──────────────────
122
+ async retract(input) {
123
+ const spaces = input.spaces || [];
124
+ const groups = input.groups || [];
125
+ if (spaces.length === 0 && groups.length === 0) {
126
+ throw new Error('Must specify at least one space or group to retract from');
127
+ }
128
+ // Validate group IDs
129
+ if (groups.length > 0) {
130
+ const invalidGroups = groups.filter((g) => g.includes('.'));
131
+ if (invalidGroups.length > 0) {
132
+ throw new Error(`Group IDs cannot contain dots: ${invalidGroups.join(', ')}`);
133
+ }
134
+ }
135
+ // Verify memory exists and belongs to user
136
+ const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
137
+ if (!memory)
138
+ throw new Error(`Memory not found: ${input.memory_id}`);
139
+ if (memory.properties.user_id !== this.userId)
140
+ throw new Error('Permission denied: not memory owner');
141
+ // Check current publication status
142
+ const currentSpaceIds = Array.isArray(memory.properties.space_ids)
143
+ ? memory.properties.space_ids
144
+ : [];
145
+ const currentGroupIds = Array.isArray(memory.properties.group_ids)
146
+ ? memory.properties.group_ids
147
+ : [];
148
+ const notPublishedSpaces = spaces.filter((s) => !currentSpaceIds.includes(s));
149
+ const notPublishedGroups = groups.filter((g) => !currentGroupIds.includes(g));
150
+ if (notPublishedSpaces.length > 0 || notPublishedGroups.length > 0) {
151
+ throw new Error(`Memory is not published to some destinations. ` +
152
+ `Not in spaces: [${notPublishedSpaces.join(', ')}], ` +
153
+ `Not in groups: [${notPublishedGroups.join(', ')}]`);
154
+ }
155
+ // Generate confirmation token
156
+ const { token } = await this.confirmationTokenService.createRequest(this.userId, 'retract_memory', {
157
+ memory_id: input.memory_id,
158
+ spaces,
159
+ groups,
160
+ current_space_ids: currentSpaceIds,
161
+ current_group_ids: currentGroupIds,
162
+ });
163
+ this.logger.info('Retract confirmation created', {
164
+ userId: this.userId,
165
+ memoryId: input.memory_id,
166
+ spaces,
167
+ groups,
168
+ });
169
+ return { token };
170
+ }
171
+ // ── Revise (phase 1: generate confirmation token) ───────────────────
172
+ async revise(input) {
173
+ const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
174
+ if (!memory)
175
+ throw new Error(`Memory not found: ${input.memory_id}`);
176
+ if (memory.properties.user_id !== this.userId)
177
+ throw new Error('Permission denied: not memory owner');
178
+ const spaceIds = Array.isArray(memory.properties.space_ids)
179
+ ? memory.properties.space_ids
180
+ : [];
181
+ const groupIds = Array.isArray(memory.properties.group_ids)
182
+ ? memory.properties.group_ids
183
+ : [];
184
+ if (spaceIds.length === 0 && groupIds.length === 0) {
185
+ throw new Error('Memory has no published copies to revise. Publish first with publish().');
186
+ }
187
+ const { token } = await this.confirmationTokenService.createRequest(this.userId, 'revise_memory', {
188
+ memory_id: input.memory_id,
189
+ space_ids: spaceIds,
190
+ group_ids: groupIds,
191
+ });
192
+ this.logger.info('Revise confirmation created', {
193
+ userId: this.userId,
194
+ memoryId: input.memory_id,
195
+ spaceIds,
196
+ groupIds,
197
+ });
198
+ return { token };
199
+ }
200
+ // ── Confirm (phase 2: execute pending action) ───────────────────────
201
+ async confirm(input) {
202
+ const request = await this.confirmationTokenService.confirmRequest(this.userId, input.token);
203
+ if (!request) {
204
+ throw new Error('Invalid or expired confirmation token');
205
+ }
206
+ if (request.action === 'publish_memory') {
207
+ return this.executePublish(request);
208
+ }
209
+ if (request.action === 'retract_memory') {
210
+ return this.executeRetract(request);
211
+ }
212
+ if (request.action === 'revise_memory') {
213
+ return this.executeRevise(request);
214
+ }
215
+ throw new Error(`Unknown action type: ${request.action}`);
216
+ }
217
+ // ── Deny ────────────────────────────────────────────────────────────
218
+ async deny(input) {
219
+ const success = await this.confirmationTokenService.denyRequest(this.userId, input.token);
220
+ if (!success) {
221
+ throw new Error('Token not found or already used');
222
+ }
223
+ return { success: true };
224
+ }
225
+ // ── Moderate ────────────────────────────────────────────────────────
226
+ async moderate(input, authContext) {
227
+ if (!input.space_id && !input.group_id) {
228
+ throw new Error('Must specify either space_id or group_id');
229
+ }
230
+ if (!ACTION_TO_STATUS[input.action]) {
231
+ throw new Error(`Invalid action: ${input.action}. Must be approve, reject, or remove`);
232
+ }
233
+ // Permission check
234
+ if (input.group_id) {
235
+ if (!canModerate(authContext, input.group_id)) {
236
+ throw new Error(`Moderator access required for group ${input.group_id}`);
237
+ }
238
+ }
239
+ else if (input.space_id) {
240
+ if (!canModerateAny(authContext)) {
241
+ throw new Error('Moderator access required to moderate memories in spaces');
242
+ }
243
+ }
244
+ // Get the collection
245
+ let collection;
246
+ if (input.group_id) {
247
+ const collectionName = getCollectionName(CollectionType.GROUPS, input.group_id);
248
+ collection = this.weaviateClient.collections.get(collectionName);
249
+ }
250
+ else {
251
+ collection = await ensurePublicCollection(this.weaviateClient);
252
+ }
253
+ // Fetch the memory
254
+ const memory = await fetchMemoryWithAllProperties(collection, input.memory_id);
255
+ if (!memory) {
256
+ const location = input.group_id ? `group ${input.group_id}` : `space ${input.space_id}`;
257
+ throw new Error(`Published memory ${input.memory_id} not found in ${location}`);
258
+ }
259
+ // Update moderation fields
260
+ const newStatus = ACTION_TO_STATUS[input.action];
261
+ const now = new Date().toISOString();
262
+ await collection.data.update({
263
+ id: input.memory_id,
264
+ properties: {
265
+ moderation_status: newStatus,
266
+ moderated_by: this.userId,
267
+ moderated_at: now,
268
+ },
269
+ });
270
+ const location = input.group_id ? `group:${input.group_id}` : `space:${input.space_id}`;
271
+ this.logger.info('Memory moderated', {
272
+ userId: this.userId,
273
+ memoryId: input.memory_id,
274
+ action: input.action,
275
+ newStatus,
276
+ location,
277
+ });
278
+ return {
279
+ memory_id: input.memory_id,
280
+ action: input.action,
281
+ moderation_status: newStatus,
282
+ moderated_by: this.userId,
283
+ moderated_at: now,
284
+ location,
285
+ };
286
+ }
287
+ // ── Search Space ────────────────────────────────────────────────────
288
+ async search(input, authContext) {
289
+ const spaces = input.spaces || [];
290
+ const groups = input.groups || [];
291
+ const searchType = input.search_type || 'hybrid';
292
+ const limit = input.limit ?? 10;
293
+ const offset = input.offset ?? 0;
294
+ // Validate space IDs
295
+ if (spaces.length > 0) {
296
+ const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
297
+ if (invalidSpaces.length > 0) {
298
+ throw new Error(`Invalid space IDs: ${invalidSpaces.join(', ')}`);
299
+ }
300
+ }
301
+ // Validate group IDs
302
+ if (groups.length > 0) {
303
+ const invalidGroups = groups.filter((g) => !g || g.includes('.') || g.trim() === '');
304
+ if (invalidGroups.length > 0) {
305
+ throw new Error('Group IDs cannot be empty or contain dots');
306
+ }
307
+ }
308
+ // Permission check for non-approved moderation filters
309
+ const moderationFilter = input.moderation_filter || 'approved';
310
+ if (moderationFilter !== 'approved') {
311
+ for (const groupId of groups) {
312
+ if (!canModerate(authContext, groupId)) {
313
+ throw new Error(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
314
+ }
315
+ }
316
+ if ((spaces.length > 0 || groups.length === 0) && !canModerateAny(authContext)) {
317
+ throw new Error(`Moderator access required to view ${moderationFilter} memories in spaces`);
318
+ }
319
+ }
320
+ const fetchLimit = (limit + offset) * Math.max(1, groups.length + (spaces.length > 0 || groups.length === 0 ? 1 : 0));
321
+ const allObjects = [];
322
+ // Search spaces collection (when spaces specified or neither spaces nor groups)
323
+ if (spaces.length > 0 || groups.length === 0) {
324
+ const spacesCollectionName = getCollectionName(CollectionType.SPACES);
325
+ const spacesCollection = this.weaviateClient.collections.get(spacesCollectionName);
326
+ const filterList = this.buildBaseFilters(spacesCollection, input);
327
+ if (spaces.length > 0) {
328
+ filterList.push(spacesCollection.filter.byProperty('space_ids').containsAny(spaces));
329
+ }
330
+ const combinedFilters = filterList.length > 0 ? Filters.and(...filterList) : undefined;
331
+ const spaceObjects = await this.executeSearch(spacesCollection, input.query, searchType, combinedFilters, fetchLimit);
332
+ allObjects.push(...spaceObjects);
333
+ }
334
+ // Search group collections
335
+ for (const groupId of groups) {
336
+ const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
337
+ const exists = await this.weaviateClient.collections.exists(groupCollectionName);
338
+ if (!exists)
339
+ continue;
340
+ const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
341
+ const filterList = this.buildBaseFilters(groupCollection, input);
342
+ const combinedFilters = filterList.length > 0 ? Filters.and(...filterList) : undefined;
343
+ const groupObjects = await this.executeSearch(groupCollection, input.query, searchType, combinedFilters, fetchLimit);
344
+ allObjects.push(...groupObjects);
345
+ }
346
+ // Deduplicate by UUID
347
+ const seen = new Set();
348
+ const deduplicated = allObjects.filter((obj) => {
349
+ if (seen.has(obj.uuid))
350
+ return false;
351
+ seen.add(obj.uuid);
352
+ return true;
353
+ });
354
+ // Sort by relevance score
355
+ deduplicated.sort((a, b) => {
356
+ const scoreA = a.metadata?.score ?? 0;
357
+ const scoreB = b.metadata?.score ?? 0;
358
+ return scoreB - scoreA;
359
+ });
360
+ // Paginate
361
+ const paginated = deduplicated.slice(offset, offset + limit);
362
+ const memories = paginated.map((obj) => ({
363
+ id: obj.uuid,
364
+ ...obj.properties,
365
+ }));
366
+ const isAllPublic = spaces.length === 0 && groups.length === 0;
367
+ return {
368
+ spaces_searched: isAllPublic ? 'all_public' : spaces,
369
+ groups_searched: groups,
370
+ memories,
371
+ total: memories.length,
372
+ offset,
373
+ limit,
374
+ };
375
+ }
376
+ // ── Query Space ─────────────────────────────────────────────────────
377
+ async query(input, authContext) {
378
+ if (!input.question?.trim())
379
+ throw new Error('Question cannot be empty');
380
+ // Validate space IDs
381
+ if (input.spaces.length === 0)
382
+ throw new Error('Must specify at least one space to query');
383
+ const invalidSpaces = input.spaces.filter((s) => !isValidSpaceId(s));
384
+ if (invalidSpaces.length > 0) {
385
+ throw new Error(`Invalid space IDs: ${invalidSpaces.join(', ')}`);
386
+ }
387
+ // Permission check for non-approved moderation filters
388
+ const moderationFilterValue = input.moderation_filter || 'approved';
389
+ if (moderationFilterValue !== 'approved' && !canModerateAny(authContext)) {
390
+ throw new Error(`Moderator access required to view ${moderationFilterValue} memories in spaces`);
391
+ }
392
+ const publicCollection = await ensurePublicCollection(this.weaviateClient);
393
+ const filterList = [];
394
+ // Filter by spaces
395
+ filterList.push(publicCollection.filter.byProperty('spaces').containsAny(input.spaces));
396
+ // Filter by doc_type
397
+ filterList.push(publicCollection.filter.byProperty('doc_type').equal('memory'));
398
+ // Moderation filter
399
+ const moderationFilter = buildModerationFilter(publicCollection, input.moderation_filter);
400
+ if (moderationFilter)
401
+ filterList.push(moderationFilter);
402
+ // Content type filter
403
+ if (input.content_type) {
404
+ filterList.push(publicCollection.filter.byProperty('content_type').equal(input.content_type));
405
+ }
406
+ // Exclude comments by default
407
+ if (!input.include_comments && !input.content_type) {
408
+ filterList.push(publicCollection.filter.byProperty('content_type').notEqual('comment'));
409
+ }
410
+ // Tags filter (AND semantics)
411
+ if (input.tags?.length) {
412
+ input.tags.forEach((tag) => {
413
+ filterList.push(publicCollection.filter.byProperty('tags').containsAny([tag]));
414
+ });
415
+ }
416
+ // Weight filter
417
+ if (input.min_weight !== undefined) {
418
+ filterList.push(publicCollection.filter.byProperty('weight').greaterOrEqual(input.min_weight));
419
+ }
420
+ // Date filters
421
+ if (input.date_from) {
422
+ filterList.push(publicCollection.filter.byProperty('created_at').greaterOrEqual(new Date(input.date_from)));
423
+ }
424
+ if (input.date_to) {
425
+ filterList.push(publicCollection.filter.byProperty('created_at').lessOrEqual(new Date(input.date_to)));
426
+ }
427
+ const combinedFilters = filterList.length > 0 ? Filters.and(...filterList) : undefined;
428
+ const opts = { limit: input.limit ?? 10 };
429
+ if (combinedFilters)
430
+ opts.filters = combinedFilters;
431
+ const results = await publicCollection.query.nearText(input.question, opts);
432
+ const memories = results.objects.map((obj) => ({
433
+ id: obj.uuid,
434
+ ...obj.properties,
435
+ _distance: obj.metadata?.distance,
436
+ }));
437
+ return {
438
+ question: input.question,
439
+ spaces_queried: input.spaces,
440
+ memories,
441
+ total: memories.length,
442
+ };
443
+ }
444
+ // ── Private: Execute Publish ────────────────────────────────────────
445
+ async executePublish(request) {
446
+ const spaces = request.payload.spaces || [];
447
+ const groups = request.payload.groups || [];
448
+ if (spaces.length === 0 && groups.length === 0) {
449
+ throw new Error('No destinations in publish request');
450
+ }
451
+ // Fetch the memory fresh
452
+ const originalMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
453
+ if (!originalMemory)
454
+ throw new Error(`Memory ${request.payload.memory_id} no longer exists`);
455
+ if (originalMemory.properties.user_id !== this.userId)
456
+ throw new Error('Permission denied');
457
+ const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
458
+ // Existing tracking arrays
459
+ const existingSpaceIds = Array.isArray(originalMemory.properties.space_ids)
460
+ ? originalMemory.properties.space_ids
461
+ : [];
462
+ const existingGroupIds = Array.isArray(originalMemory.properties.group_ids)
463
+ ? originalMemory.properties.group_ids
464
+ : [];
465
+ // Merge tags
466
+ const originalTags = Array.isArray(originalMemory.properties.tags)
467
+ ? originalMemory.properties.tags
468
+ : [];
469
+ const additionalTags = Array.isArray(request.payload.additional_tags)
470
+ ? request.payload.additional_tags
471
+ : [];
472
+ const mergedTags = [...originalTags, ...additionalTags];
473
+ const successfulPublications = [];
474
+ const failedPublications = [];
475
+ // Publish to spaces (Memory_spaces_public)
476
+ if (spaces.length > 0) {
477
+ try {
478
+ const publicCollection = await ensurePublicCollection(this.weaviateClient);
479
+ let existingSpaceMemory = null;
480
+ try {
481
+ existingSpaceMemory = await fetchMemoryWithAllProperties(publicCollection, compositeId);
482
+ }
483
+ catch { /* doesn't exist */ }
484
+ const newSpaceIds = [...new Set([...existingSpaceIds, ...spaces])];
485
+ // Determine moderation status
486
+ let spaceModerationStatus = 'approved';
487
+ for (const spaceId of spaces) {
488
+ const spaceConfig = await getSpaceConfig(spaceId, 'space');
489
+ if (spaceConfig.require_moderation) {
490
+ spaceModerationStatus = 'pending';
491
+ break;
492
+ }
493
+ }
494
+ const publishedMemory = {
495
+ ...originalMemory.properties,
496
+ id: compositeId,
497
+ space_ids: newSpaceIds,
498
+ group_ids: existingGroupIds,
499
+ spaces,
500
+ author_id: this.userId,
501
+ published_at: new Date().toISOString(),
502
+ discovery_count: 0,
503
+ attribution: 'user',
504
+ moderation_status: spaceModerationStatus,
505
+ tags: mergedTags,
506
+ };
507
+ delete publishedMemory._additional;
508
+ if (existingSpaceMemory) {
509
+ await publicCollection.data.update({ id: compositeId, properties: publishedMemory });
510
+ }
511
+ else {
512
+ await publicCollection.data.insert({ id: compositeId, properties: publishedMemory });
513
+ }
514
+ successfulPublications.push(`spaces: ${spaces.join(', ')}`);
515
+ }
516
+ catch (err) {
517
+ failedPublications.push(`spaces: ${err instanceof Error ? err.message : String(err)}`);
518
+ }
519
+ }
520
+ // Publish to groups
521
+ for (const groupId of groups) {
522
+ const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
523
+ try {
524
+ const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
525
+ let existingGroupMemory = null;
526
+ try {
527
+ existingGroupMemory = await fetchMemoryWithAllProperties(groupCollection, compositeId);
528
+ }
529
+ catch { /* doesn't exist */ }
530
+ const newGroupIds = [...new Set([...existingGroupIds, groupId])];
531
+ const groupConfig = await getSpaceConfig(groupId, 'group');
532
+ const groupModerationStatus = groupConfig.require_moderation ? 'pending' : 'approved';
533
+ const groupMemory = {
534
+ ...originalMemory.properties,
535
+ id: compositeId,
536
+ space_ids: existingSpaceIds,
537
+ group_ids: newGroupIds,
538
+ author_id: this.userId,
539
+ published_at: new Date().toISOString(),
540
+ discovery_count: 0,
541
+ attribution: 'user',
542
+ moderation_status: groupModerationStatus,
543
+ tags: mergedTags,
544
+ };
545
+ delete groupMemory._additional;
546
+ if (existingGroupMemory) {
547
+ await groupCollection.data.update({ id: compositeId, properties: groupMemory });
548
+ }
549
+ else {
550
+ await groupCollection.data.insert({ id: compositeId, properties: groupMemory });
551
+ }
552
+ successfulPublications.push(`group: ${groupId}`);
553
+ }
554
+ catch (err) {
555
+ failedPublications.push(`group ${groupId}: ${err instanceof Error ? err.message : String(err)}`);
556
+ }
557
+ }
558
+ // Update source memory tracking arrays
559
+ const finalSpaceIds = successfulPublications.some((p) => p.startsWith('spaces:'))
560
+ ? [...new Set([...existingSpaceIds, ...spaces])]
561
+ : existingSpaceIds;
562
+ const successfulGroups = groups.filter((g) => successfulPublications.some((p) => p === `group: ${g}`));
563
+ const finalGroupIds = [...new Set([...existingGroupIds, ...successfulGroups])];
564
+ if (finalSpaceIds.length > existingSpaceIds.length || finalGroupIds.length > existingGroupIds.length) {
565
+ try {
566
+ await this.userCollection.data.update({
567
+ id: request.payload.memory_id,
568
+ properties: { space_ids: finalSpaceIds, group_ids: finalGroupIds },
569
+ });
570
+ }
571
+ catch {
572
+ this.logger.warn('Failed to update source memory tracking arrays');
573
+ }
574
+ }
575
+ if (successfulPublications.length === 0) {
576
+ throw new Error(`Publication failed: ${failedPublications.join('; ')}`);
577
+ }
578
+ this.logger.info('Memory published', {
579
+ compositeId,
580
+ published: successfulPublications,
581
+ failed: failedPublications,
582
+ });
583
+ return {
584
+ action: 'publish_memory',
585
+ success: true,
586
+ composite_id: compositeId,
587
+ published_to: successfulPublications,
588
+ failed: failedPublications.length > 0 ? failedPublications : undefined,
589
+ space_ids: finalSpaceIds,
590
+ group_ids: finalGroupIds,
591
+ };
592
+ }
593
+ // ── Private: Execute Retract ────────────────────────────────────────
594
+ async executeRetract(request) {
595
+ const spaces = request.payload.spaces || [];
596
+ const groups = request.payload.groups || [];
597
+ const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
598
+ if (!sourceMemory)
599
+ throw new Error(`Source memory ${request.payload.memory_id} no longer exists`);
600
+ const currentSpaceIds = Array.isArray(sourceMemory.properties.space_ids)
601
+ ? sourceMemory.properties.space_ids
602
+ : [];
603
+ const currentGroupIds = Array.isArray(sourceMemory.properties.group_ids)
604
+ ? sourceMemory.properties.group_ids
605
+ : [];
606
+ const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
607
+ const successfulRetractions = [];
608
+ const failedRetractions = [];
609
+ // Retract from spaces (orphan: remove from space_ids but keep the document)
610
+ if (spaces.length > 0) {
611
+ try {
612
+ const spacesCollectionName = getCollectionName(CollectionType.SPACES);
613
+ const publicCollection = this.weaviateClient.collections.get(spacesCollectionName);
614
+ const publishedMemory = await fetchMemoryWithAllProperties(publicCollection, compositeId);
615
+ if (publishedMemory) {
616
+ const newSpaceIds = currentSpaceIds.filter((id) => !spaces.includes(id));
617
+ await publicCollection.data.update({
618
+ id: compositeId,
619
+ properties: { space_ids: newSpaceIds, retracted_at: new Date().toISOString() },
620
+ });
621
+ successfulRetractions.push(`spaces: ${spaces.join(', ')}`);
622
+ }
623
+ else {
624
+ failedRetractions.push('spaces: Memory not found in spaces collection');
625
+ }
626
+ }
627
+ catch (err) {
628
+ failedRetractions.push(`spaces: ${err instanceof Error ? err.message : String(err)}`);
629
+ }
630
+ }
631
+ // Retract from groups (orphan: remove from group_ids but keep the document)
632
+ for (const groupId of groups) {
633
+ const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
634
+ try {
635
+ const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
636
+ const groupMemory = await fetchMemoryWithAllProperties(groupCollection, compositeId);
637
+ if (groupMemory) {
638
+ const groupMemoryGroupIds = Array.isArray(groupMemory.properties.group_ids)
639
+ ? groupMemory.properties.group_ids
640
+ : [];
641
+ const newGroupIds = groupMemoryGroupIds.filter((id) => id !== groupId);
642
+ await groupCollection.data.update({
643
+ id: compositeId,
644
+ properties: { group_ids: newGroupIds, retracted_at: new Date().toISOString() },
645
+ });
646
+ successfulRetractions.push(`group: ${groupId}`);
647
+ }
648
+ else {
649
+ failedRetractions.push(`group ${groupId}: Memory not found in group`);
650
+ }
651
+ }
652
+ catch (err) {
653
+ failedRetractions.push(`group ${groupId}: ${err instanceof Error ? err.message : String(err)}`);
654
+ }
655
+ }
656
+ // Update source memory tracking arrays
657
+ const finalSpaceIds = successfulRetractions.some((r) => r.startsWith('spaces:'))
658
+ ? currentSpaceIds.filter((id) => !spaces.includes(id))
659
+ : currentSpaceIds;
660
+ const successfulGroupRetractions = groups.filter((g) => successfulRetractions.some((r) => r === `group: ${g}`));
661
+ const finalGroupIds = currentGroupIds.filter((id) => !successfulGroupRetractions.includes(id));
662
+ try {
663
+ await this.userCollection.data.update({
664
+ id: request.payload.memory_id,
665
+ properties: { space_ids: finalSpaceIds, group_ids: finalGroupIds },
666
+ });
667
+ }
668
+ catch {
669
+ this.logger.warn('Failed to update source memory tracking arrays after retraction');
670
+ }
671
+ if (successfulRetractions.length === 0) {
672
+ throw new Error(`Retraction failed: ${failedRetractions.join('; ')}`);
673
+ }
674
+ this.logger.info('Memory retracted', {
675
+ compositeId,
676
+ retracted: successfulRetractions,
677
+ failed: failedRetractions,
678
+ });
679
+ return {
680
+ action: 'retract_memory',
681
+ success: true,
682
+ composite_id: compositeId,
683
+ retracted_from: successfulRetractions,
684
+ failed: failedRetractions.length > 0 ? failedRetractions : undefined,
685
+ space_ids: finalSpaceIds,
686
+ group_ids: finalGroupIds,
687
+ };
688
+ }
689
+ // ── Private: Execute Revise ─────────────────────────────────────────
690
+ async executeRevise(request) {
691
+ const { memory_id, space_ids = [], group_ids = [] } = request.payload;
692
+ const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, memory_id);
693
+ if (!sourceMemory)
694
+ throw new Error(`Source memory ${memory_id} no longer exists`);
695
+ if (sourceMemory.properties.user_id !== this.userId)
696
+ throw new Error('Permission denied');
697
+ const newContent = String(sourceMemory.properties.content ?? '');
698
+ const revisedAt = new Date().toISOString();
699
+ const compositeId = generateCompositeId(this.userId, memory_id);
700
+ const results = [];
701
+ const reviseInCollection = async (collectionName, locationLabel) => {
702
+ try {
703
+ const collection = this.weaviateClient.collections.get(collectionName);
704
+ const publishedMemory = await fetchMemoryWithAllProperties(collection, compositeId);
705
+ if (!publishedMemory) {
706
+ results.push({ location: locationLabel, status: 'skipped', error: 'Published copy not found' });
707
+ return;
708
+ }
709
+ const oldContent = String(publishedMemory.properties.content ?? '');
710
+ let revisionHistory = parseRevisionHistory(publishedMemory.properties.revision_history);
711
+ if (oldContent !== newContent) {
712
+ revisionHistory = buildRevisionHistory(revisionHistory, oldContent, revisedAt);
713
+ }
714
+ const currentRevisionCount = typeof publishedMemory.properties.revision_count === 'number'
715
+ ? publishedMemory.properties.revision_count
716
+ : 0;
717
+ await collection.data.update({
718
+ id: compositeId,
719
+ properties: {
720
+ content: newContent,
721
+ revised_at: revisedAt,
722
+ revision_count: currentRevisionCount + 1,
723
+ revision_history: JSON.stringify(revisionHistory),
724
+ },
725
+ });
726
+ results.push({ location: locationLabel, status: 'success' });
727
+ }
728
+ catch (err) {
729
+ results.push({
730
+ location: locationLabel,
731
+ status: 'failed',
732
+ error: err instanceof Error ? err.message : String(err),
733
+ });
734
+ }
735
+ };
736
+ // Revise in spaces collection
737
+ if (space_ids.length > 0) {
738
+ await reviseInCollection(getCollectionName(CollectionType.SPACES), 'Memory_spaces_public');
739
+ }
740
+ // Revise in each group collection
741
+ for (const groupId of group_ids) {
742
+ await reviseInCollection(getCollectionName(CollectionType.GROUPS, groupId), `Memory_groups_${groupId}`);
743
+ }
744
+ const successCount = results.filter((r) => r.status === 'success').length;
745
+ this.logger.info('Memory revised', {
746
+ compositeId,
747
+ success: successCount,
748
+ total: results.length,
749
+ });
750
+ return {
751
+ action: 'revise_memory',
752
+ success: successCount > 0,
753
+ composite_id: compositeId,
754
+ revised_at: revisedAt,
755
+ results,
756
+ };
757
+ }
758
+ // ── Private: Build Base Filters ─────────────────────────────────────
759
+ buildBaseFilters(collection, input) {
760
+ const filterList = [];
761
+ // Exclude soft-deleted
762
+ filterList.push(collection.filter.byProperty('deleted_at').isNull(true));
763
+ // Only memories
764
+ filterList.push(collection.filter.byProperty('doc_type').equal('memory'));
765
+ // Moderation filter
766
+ const moderationFilter = buildModerationFilter(collection, input.moderation_filter);
767
+ if (moderationFilter)
768
+ filterList.push(moderationFilter);
769
+ // Content type
770
+ if (input.content_type) {
771
+ filterList.push(collection.filter.byProperty('content_type').equal(input.content_type));
772
+ }
773
+ // Exclude comments by default
774
+ if (!input.include_comments && !input.content_type) {
775
+ filterList.push(collection.filter.byProperty('content_type').notEqual('comment'));
776
+ }
777
+ // Tags (AND semantics)
778
+ if (input.tags?.length) {
779
+ input.tags.forEach((tag) => {
780
+ filterList.push(collection.filter.byProperty('tags').containsAny([tag]));
781
+ });
782
+ }
783
+ // Weight filters
784
+ if (input.min_weight !== undefined) {
785
+ filterList.push(collection.filter.byProperty('weight').greaterOrEqual(input.min_weight));
786
+ }
787
+ if (input.max_weight !== undefined) {
788
+ filterList.push(collection.filter.byProperty('weight').lessOrEqual(input.max_weight));
789
+ }
790
+ // Date filters
791
+ if (input.date_from) {
792
+ filterList.push(collection.filter.byProperty('created_at').greaterOrEqual(new Date(input.date_from)));
793
+ }
794
+ if (input.date_to) {
795
+ filterList.push(collection.filter.byProperty('created_at').lessOrEqual(new Date(input.date_to)));
796
+ }
797
+ return filterList;
798
+ }
799
+ // ── Private: Execute Search ─────────────────────────────────────────
800
+ async executeSearch(collection, query, searchType, filters, limit) {
801
+ const opts = { limit };
802
+ if (filters)
803
+ opts.filters = filters;
804
+ switch (searchType) {
805
+ case 'bm25':
806
+ return (await collection.query.bm25(query, opts)).objects;
807
+ case 'semantic':
808
+ return (await collection.query.nearText([query], opts)).objects;
809
+ case 'hybrid':
810
+ default:
811
+ return (await collection.query.hybrid(query, opts)).objects;
812
+ }
813
+ }
814
+ }
815
+ //# sourceMappingURL=space.service.js.map