@prmichaelsen/remember-mcp 3.14.20 → 3.15.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.15.0] - 2026-03-06
9
+
10
+ ### Fixed
11
+
12
+ - Wire `MemoryIndexService` into `MemoryService` and `SpaceService` (task-174)
13
+ - All new memories and space-published memories are now indexed in Firestore lookup table
14
+ - Enables `resolveById()` cross-collection resolution and fixes 404s on published memories
15
+
16
+ ## [3.14.21] - 2026-03-06
17
+
18
+ ### Added
19
+
20
+ - Wire LLM auto-moderation into SpaceService publish/revise flow (task-202)
21
+ - Create singleton `ModerationClient` from `ANTHROPIC_API_KEY` env var when present
22
+ - Content published to spaces is now screened by Claude Haiku before storage
23
+
8
24
  ## [3.14.14] - 2026-03-04
9
25
 
10
26
  ### Changed
@@ -246,10 +246,10 @@ milestones:
246
246
  status: completed
247
247
  progress: 100%
248
248
  started: 2026-02-27
249
- completed: 2026-02-27
249
+ completed: 2026-03-06
250
250
  estimated_weeks: 2
251
- tasks_completed: 6
252
- tasks_total: 6
251
+ tasks_completed: 7
252
+ tasks_total: 7
253
253
  notes: |
254
254
  ✅ Content moderation lifecycle and per-space/group behavioral config.
255
255
  ✅ Design: agent/design/local.moderation-and-space-config.md (Implemented)
@@ -702,6 +702,19 @@ tasks:
702
702
  ✅ CHANGELOG, design doc status, version bump 3.9.0→3.10.0
703
703
  📋 Verify all tests pass
704
704
 
705
+ - id: task-202
706
+ name: Wire LLM Moderation Client to SpaceService
707
+ status: completed
708
+ completed_date: 2026-03-06
709
+ file: agent/tasks/milestone-15-moderation-space-config/task-202-wire-llm-moderation-client.md
710
+ estimated_hours: 0.5
711
+ actual_hours: 0.25
712
+ dependencies: [task-179]
713
+ notes: |
714
+ 📋 Connect createModerationClient (remember-core) to SpaceService in core-services.ts
715
+ 📋 Auto-create when ANTHROPIC_API_KEY env var is present
716
+ 📋 ~5 lines of code change
717
+
705
718
  milestone_7:
706
719
  - id: task-180
707
720
  name: Access Result & Permission Types
@@ -0,0 +1,62 @@
1
+ # Task 202: Wire LLM Moderation Client to SpaceService
2
+
3
+ **Milestone**: M15 - Moderation & Space Config (follow-up)
4
+ **Status**: completed
5
+ **Estimated Hours**: 0.5
6
+ **Priority**: P1
7
+ **Dependencies**: M15 (completed), remember-core moderation.service.ts
8
+
9
+ ---
10
+
11
+ ## Objective
12
+
13
+ Connect the `createModerationClient` from `@prmichaelsen/remember-core` to the `SpaceService` in `remember-mcp`, so that content published to spaces is automatically screened by Claude Haiku before being stored.
14
+
15
+ Currently, all moderation infrastructure exists but is disconnected:
16
+ - `remember-core` exports `createModerationClient()` (calls Anthropic Messages API with Haiku)
17
+ - `SpaceService` accepts `{ moderationClient }` in its constructor options
18
+ - `SpaceService.checkModeration()` calls `moderationClient.moderate()` on publish/revise
19
+ - But `createCoreServices()` in `remember-mcp` never creates or passes a moderation client
20
+
21
+ ## Context
22
+
23
+ - `remember-core/src/services/moderation.service.ts` — factory + types
24
+ - `remember-core/src/services/space.service.ts:237` — constructor accepts `options?.moderationClient`
25
+ - `remember-mcp/src/core-services.ts:50` — SpaceService created without moderation client
26
+ - `remember-mcp-server/.env` already has `ANTHROPIC_API_KEY` set
27
+
28
+ ## Steps
29
+
30
+ ### 1. Edit `src/core-services.ts`
31
+
32
+ - Import `createModerationClient` from `@prmichaelsen/remember-core`
33
+ - Create a singleton moderation client (only when `ANTHROPIC_API_KEY` env var is present)
34
+ - Pass `{ moderationClient }` as the 6th argument to `new SpaceService()`
35
+
36
+ ### 2. Build and verify
37
+
38
+ - Run `npm run build` (or equivalent)
39
+ - Verify TypeScript compiles without errors
40
+
41
+ ### 3. Run tests
42
+
43
+ - Run existing test suite to ensure no regressions
44
+ - Moderation client is optional, so existing tests should pass unaffected
45
+
46
+ ### 4. Version bump
47
+
48
+ - Bump patch version in package.json
49
+ - Update CHANGELOG.md
50
+
51
+ ## Verification
52
+
53
+ - [ ] `createModerationClient` imported from remember-core
54
+ - [ ] Moderation client created conditionally (only when `ANTHROPIC_API_KEY` is set)
55
+ - [ ] Moderation client passed to SpaceService constructor
56
+ - [ ] TypeScript compiles without errors
57
+ - [ ] Existing tests pass
58
+ - [ ] Version bumped
59
+
60
+ ## Downstream
61
+
62
+ After this ships, `remember-mcp-server` just needs a dependency bump — no code changes required. `ANTHROPIC_API_KEY` is already configured in its `.env`.
@@ -0,0 +1,97 @@
1
+ # Task 174: Wire MemoryIndexService into Core Services
2
+
3
+ **Milestone**: Unassigned (breaking change from remember-core v0.33.0)
4
+ **Estimated Time**: 0.5-1 hour
5
+ **Dependencies**: remember-core v0.33.0+
6
+ **Status**: Not Started
7
+
8
+ ---
9
+
10
+ ## Objective
11
+
12
+ Update `src/core-services.ts` to pass the now-required `MemoryIndexService` to both `MemoryService` and `SpaceService` constructors. remember-core v0.33.0 made `MemoryIndexService` a required parameter (no longer optional) in both services.
13
+
14
+ ---
15
+
16
+ ## Context
17
+
18
+ remember-core task-117 made `MemoryIndexService` required in:
19
+ - `MemoryService` constructor: 4th param `options.memoryIndex` (was optional, now required)
20
+ - `SpaceService` constructor: new 6th positional param `memoryIndexService` (before the optional `options`)
21
+
22
+ Currently `src/core-services.ts` line 52 constructs `MemoryService` with 3 args (no memoryIndex) and line 54 constructs `SpaceService` with `{ moderationClient }` as the 6th arg (which is now the 7th position).
23
+
24
+ ---
25
+
26
+ ## Steps
27
+
28
+ ### 1. Import MemoryIndexService
29
+
30
+ Add `MemoryIndexService` to the import from `@prmichaelsen/remember-core`.
31
+
32
+ ### 2. Create singleton MemoryIndexService
33
+
34
+ Add alongside the other singletons (line ~31):
35
+
36
+ ```typescript
37
+ const memoryIndexService = new MemoryIndexService(coreLogger);
38
+ ```
39
+
40
+ ### 3. Update MemoryService constructor call
41
+
42
+ Change line 52 from:
43
+ ```typescript
44
+ memory: new MemoryService(collection, userId, coreLogger),
45
+ ```
46
+ to:
47
+ ```typescript
48
+ memory: new MemoryService(collection, userId, coreLogger, {
49
+ memoryIndex: memoryIndexService,
50
+ weaviateClient,
51
+ }),
52
+ ```
53
+
54
+ ### 4. Update SpaceService constructor call
55
+
56
+ Change line 54 from:
57
+ ```typescript
58
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, { moderationClient }),
59
+ ```
60
+ to:
61
+ ```typescript
62
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, memoryIndexService, { moderationClient }),
63
+ ```
64
+
65
+ ### 5. Bump remember-core dependency
66
+
67
+ Update `package.json` to require `@prmichaelsen/remember-core` >= 0.33.0.
68
+
69
+ ### 6. Build and verify
70
+
71
+ - `tsc --noEmit` passes
72
+ - All tests pass
73
+
74
+ ---
75
+
76
+ ## Verification
77
+
78
+ - [ ] `MemoryIndexService` imported and instantiated as singleton
79
+ - [ ] `MemoryService` receives `{ memoryIndex: memoryIndexService, weaviateClient }` in options
80
+ - [ ] `SpaceService` receives `memoryIndexService` as 6th positional arg
81
+ - [ ] `tsc --noEmit` passes
82
+ - [ ] All tests pass
83
+
84
+ ---
85
+
86
+ ## Files Modified
87
+
88
+ - `src/core-services.ts` — add import, singleton, update constructor calls
89
+ - `package.json` — bump remember-core dependency
90
+
91
+ ---
92
+
93
+ ## Notes
94
+
95
+ - Single file change (`core-services.ts`) — all service construction is centralized here
96
+ - `weaviateClient` is already available in `createCoreServices()`, so passing it to MemoryService options enables `resolveById()` cross-collection resolution
97
+ - This ensures all new memories created via MCP tools are indexed in the Firestore lookup table
@@ -1754,6 +1754,9 @@ function getUserPreferencesPath(userId) {
1754
1754
  function getCollectionRegistryPath() {
1755
1755
  return `${BASE}.collection_registry`;
1756
1756
  }
1757
+ function getMemoryIndexPath() {
1758
+ return `${BASE}.memory_index`;
1759
+ }
1757
1760
 
1758
1761
  // node_modules/@prmichaelsen/remember-core/dist/services/preferences.service.js
1759
1762
  var PreferencesDatabaseService = class {
@@ -2143,20 +2146,18 @@ var MemoryService = class {
2143
2146
  * Requires `memoryIndex` and `weaviateClient` in constructor options.
2144
2147
  */
2145
2148
  async resolveById(memoryId) {
2146
- if (!this.options?.weaviateClient) {
2149
+ if (!this.options.weaviateClient) {
2147
2150
  throw new Error("resolveById requires weaviateClient in options");
2148
2151
  }
2149
- if (this.options?.memoryIndex) {
2150
- const collectionName = await this.options.memoryIndex.lookup(memoryId);
2151
- if (collectionName) {
2152
- const col = this.options.weaviateClient.collections.get(collectionName);
2153
- const memory = await fetchMemoryWithAllProperties(col, memoryId);
2154
- if (memory?.properties) {
2155
- return {
2156
- memory: normalizeDoc({ id: memory.uuid, ...memory.properties }),
2157
- collectionName
2158
- };
2159
- }
2152
+ const collectionName = await this.options.memoryIndex.lookup(memoryId);
2153
+ if (collectionName) {
2154
+ const col = this.options.weaviateClient.collections.get(collectionName);
2155
+ const memory = await fetchMemoryWithAllProperties(col, memoryId);
2156
+ if (memory?.properties) {
2157
+ return {
2158
+ memory: normalizeDoc({ id: memory.uuid, ...memory.properties }),
2159
+ collectionName
2160
+ };
2160
2161
  }
2161
2162
  }
2162
2163
  return { memory: null, collectionName: null };
@@ -2202,13 +2203,11 @@ var MemoryService = class {
2202
2203
  };
2203
2204
  const memoryId = await this.collection.data.insert({ properties });
2204
2205
  this.logger.info("Memory created", { memoryId, userId: this.userId });
2205
- if (this.options?.memoryIndex) {
2206
- try {
2207
- const collectionName = this.collection.name;
2208
- await this.options.memoryIndex.index(memoryId, collectionName);
2209
- } catch (err2) {
2210
- this.logger.warn?.(`[MemoryService] Index write failed for ${memoryId}: ${err2}`);
2211
- }
2206
+ try {
2207
+ const collectionName = this.collection.name;
2208
+ await this.options.memoryIndex.index(memoryId, collectionName);
2209
+ } catch (err2) {
2210
+ this.logger.warn?.(`[MemoryService] Index write failed for ${memoryId}: ${err2}`);
2212
2211
  }
2213
2212
  return { memory_id: memoryId, created_at: now };
2214
2213
  }
@@ -3142,14 +3141,18 @@ var SpaceService = class {
3142
3141
  userId;
3143
3142
  confirmationTokenService;
3144
3143
  logger;
3144
+ memoryIndexService;
3145
3145
  moderationClient;
3146
- constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2, options) {
3146
+ memoryIndex;
3147
+ constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2, memoryIndexService2, options) {
3147
3148
  this.weaviateClient = weaviateClient;
3148
3149
  this.userCollection = userCollection;
3149
3150
  this.userId = userId;
3150
3151
  this.confirmationTokenService = confirmationTokenService;
3151
3152
  this.logger = logger2;
3153
+ this.memoryIndexService = memoryIndexService2;
3152
3154
  this.moderationClient = options?.moderationClient;
3155
+ this.memoryIndex = memoryIndexService2;
3153
3156
  }
3154
3157
  // ── Content moderation helper ────────────────────────────────────────
3155
3158
  async checkModeration(content) {
@@ -3555,6 +3558,11 @@ var SpaceService = class {
3555
3558
  } else {
3556
3559
  await publicCollection.data.insert({ id: weaviateId, properties: publishedMemory });
3557
3560
  }
3561
+ try {
3562
+ await this.memoryIndex.index(weaviateId, "Memory_spaces_public");
3563
+ } catch (err2) {
3564
+ this.logger.warn?.(`[SpaceService] Index write failed for ${weaviateId}: ${err2}`);
3565
+ }
3558
3566
  successfulPublications.push(`spaces: ${spaces.join(", ")}`);
3559
3567
  } catch (err2) {
3560
3568
  failedPublications.push(`spaces: ${err2 instanceof Error ? err2.message : String(err2)}`);
@@ -3591,6 +3599,11 @@ var SpaceService = class {
3591
3599
  } else {
3592
3600
  await groupCollection.data.insert({ id: weaviateId, properties: groupMemory });
3593
3601
  }
3602
+ try {
3603
+ await this.memoryIndex.index(weaviateId, groupCollectionName);
3604
+ } catch (err2) {
3605
+ this.logger.warn?.(`[SpaceService] Index write failed for ${weaviateId} in ${groupCollectionName}: ${err2}`);
3606
+ }
3594
3607
  successfulPublications.push(`group: ${groupId}`);
3595
3608
  } catch (err2) {
3596
3609
  failedPublications.push(`group ${groupId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
@@ -3831,6 +3844,144 @@ var REM_STATE_COLLECTION = `${BASE}.rem_state`;
3831
3844
  // node_modules/@prmichaelsen/remember-core/dist/services/rem.clustering.js
3832
3845
  import { Filters as Filters5 } from "weaviate-client";
3833
3846
 
3847
+ // node_modules/@prmichaelsen/remember-core/dist/services/memory-index.service.js
3848
+ var MemoryIndexService = class {
3849
+ logger;
3850
+ collectionPath;
3851
+ constructor(logger2) {
3852
+ this.logger = logger2;
3853
+ this.collectionPath = getMemoryIndexPath();
3854
+ }
3855
+ /**
3856
+ * Index a memory UUID → collection name mapping.
3857
+ * Uses set() for idempotency (safe to re-index).
3858
+ */
3859
+ async index(memoryUuid, collectionName) {
3860
+ const entry = {
3861
+ collection_name: collectionName,
3862
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3863
+ };
3864
+ await setDocument2(this.collectionPath, memoryUuid, entry);
3865
+ this.logger.debug?.(`[MemoryIndex] Indexed ${memoryUuid} \u2192 ${collectionName}`);
3866
+ }
3867
+ /**
3868
+ * Look up the collection name for a memory UUID.
3869
+ * Returns null if the memory is not indexed.
3870
+ */
3871
+ async lookup(memoryUuid) {
3872
+ const doc = await getDocument2(this.collectionPath, memoryUuid);
3873
+ if (!doc)
3874
+ return null;
3875
+ const entry = doc;
3876
+ return entry.collection_name ?? null;
3877
+ }
3878
+ };
3879
+
3880
+ // node_modules/@prmichaelsen/remember-core/dist/services/moderation.service.js
3881
+ import { createHash as createHash2 } from "crypto";
3882
+ function buildModerationPrompt(content) {
3883
+ return `You are a content moderation classifier. Evaluate the following user-generated content and determine if it should be ALLOWED or BLOCKED from a public space.
3884
+
3885
+ You must be LENIENT. Most content should pass. Only block content that is clearly and explicitly in one of these categories:
3886
+
3887
+ BLOCK categories:
3888
+
3889
+ 1. HATE SPEECH \u2014 Explicit dehumanization of ethnic/racial/religious groups, calls for violence against protected groups, racial slurs used to demean.
3890
+ Examples that FAIL:
3891
+ - "All [ethnic group] should be exterminated. They are subhuman parasites."
3892
+ - Explicit Nazi ideology or white supremacist recruitment
3893
+ - "[Racial slur] don't deserve rights"
3894
+ - Genocide celebration ("The Holocaust was a good thing")
3895
+
3896
+ 2. VIOLENCE INCITEMENT \u2014 Direct threats against specific people or locations, incitement to commit specific acts of mass violence.
3897
+ Examples that FAIL:
3898
+ - "I'm going to find [person] and make them pay. They won't see it coming."
3899
+ - "Someone should shoot up [location]. That would teach them."
3900
+ Examples that PASS:
3901
+ - Instructions or information about weapons (legally protected speech without incitement)
3902
+ - Historical/philosophical discussion of violence
3903
+ - Fictional violence (games, stories, D&D campaigns)
3904
+ - Colloquial expressions ("I could kill for a burger")
3905
+
3906
+ 3. CSAM \u2014 Any sexual content involving minors. Zero tolerance.
3907
+
3908
+ 4. SELF-HARM ENCOURAGEMENT \u2014 Detailed instructions or active encouragement for self-harm or suicide.
3909
+ Examples that PASS:
3910
+ - Discussions about depression or mental health struggles
3911
+ - Jokes about suicide or dark humor about self-harm
3912
+ - Journaling about difficult feelings
3913
+
3914
+ ALWAYS ALLOW:
3915
+ - Harsh political opinions, criticism of governments or public figures
3916
+ - Edgy humor, dark comedy, offensive jokes (without targeted dehumanization)
3917
+ - Strong opinions about religion, ideology, or social issues
3918
+ - Profanity and vulgar language
3919
+ - Controversial or uncomfortable topics
3920
+ - Educational/historical content about atrocities
3921
+ - The French Revolution, violent revolution as philosophical concept
3922
+
3923
+ Content to evaluate:
3924
+ ---
3925
+ ${content}
3926
+ ---
3927
+
3928
+ Respond with ONLY valid JSON:
3929
+ {"pass":true}
3930
+ OR
3931
+ {"pass":false,"reason":"<specific, human-friendly explanation of why this was blocked>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}`;
3932
+ }
3933
+ var DEFAULT_CACHE_MAX = 1e3;
3934
+ function hashContent(content) {
3935
+ return createHash2("sha256").update(content).digest("hex");
3936
+ }
3937
+ function createModerationClient(options) {
3938
+ const model = options.model ?? "claude-haiku-4-5-20251001";
3939
+ const cacheMax = options.cacheMax ?? DEFAULT_CACHE_MAX;
3940
+ const cache = /* @__PURE__ */ new Map();
3941
+ return {
3942
+ async moderate(content) {
3943
+ const hash = hashContent(content);
3944
+ const cached = cache.get(hash);
3945
+ if (cached)
3946
+ return cached;
3947
+ try {
3948
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
3949
+ method: "POST",
3950
+ headers: {
3951
+ "Content-Type": "application/json",
3952
+ "x-api-key": options.apiKey,
3953
+ "anthropic-version": "2023-06-01"
3954
+ },
3955
+ body: JSON.stringify({
3956
+ model,
3957
+ max_tokens: 256,
3958
+ messages: [{ role: "user", content: buildModerationPrompt(content) }]
3959
+ })
3960
+ });
3961
+ if (!response.ok) {
3962
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3963
+ }
3964
+ const data = await response.json();
3965
+ const text = data.content?.[0]?.text ?? "";
3966
+ const parsed = JSON.parse(text);
3967
+ const result = {
3968
+ pass: parsed.pass === true,
3969
+ reason: parsed.reason ?? "",
3970
+ category: parsed.pass ? void 0 : parsed.category
3971
+ };
3972
+ if (cache.size >= cacheMax) {
3973
+ const oldest = cache.keys().next().value;
3974
+ cache.delete(oldest);
3975
+ }
3976
+ cache.set(hash, result);
3977
+ return result;
3978
+ } catch {
3979
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3980
+ }
3981
+ }
3982
+ };
3983
+ }
3984
+
3834
3985
  // src/weaviate/schema.ts
3835
3986
  init_logger();
3836
3987
 
@@ -3947,6 +4098,8 @@ function getMemoryCollection(userId) {
3947
4098
  var coreLogger = createLogger("info");
3948
4099
  var tokenService = new ConfirmationTokenService(coreLogger);
3949
4100
  var preferencesService = new PreferencesDatabaseService(coreLogger);
4101
+ var moderationClient = process.env.ANTHROPIC_API_KEY ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY }) : void 0;
4102
+ var memoryIndexService = new MemoryIndexService(coreLogger);
3950
4103
  var coreServicesCache = /* @__PURE__ */ new Map();
3951
4104
  function createCoreServices(userId) {
3952
4105
  const cached = coreServicesCache.get(userId);
@@ -3955,9 +4108,12 @@ function createCoreServices(userId) {
3955
4108
  const collection = getMemoryCollection(userId);
3956
4109
  const weaviateClient = getWeaviateClient();
3957
4110
  const services = {
3958
- memory: new MemoryService(collection, userId, coreLogger),
4111
+ memory: new MemoryService(collection, userId, coreLogger, {
4112
+ memoryIndex: memoryIndexService,
4113
+ weaviateClient
4114
+ }),
3959
4115
  relationship: new RelationshipService(collection, userId, coreLogger),
3960
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
4116
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, memoryIndexService, { moderationClient }),
3961
4117
  preferences: preferencesService,
3962
4118
  token: tokenService
3963
4119
  };
package/dist/server.js CHANGED
@@ -1758,6 +1758,9 @@ function getUserPreferencesPath(userId) {
1758
1758
  function getCollectionRegistryPath() {
1759
1759
  return `${BASE}.collection_registry`;
1760
1760
  }
1761
+ function getMemoryIndexPath() {
1762
+ return `${BASE}.memory_index`;
1763
+ }
1761
1764
 
1762
1765
  // node_modules/@prmichaelsen/remember-core/dist/services/preferences.service.js
1763
1766
  var PreferencesDatabaseService = class {
@@ -2147,20 +2150,18 @@ var MemoryService = class {
2147
2150
  * Requires `memoryIndex` and `weaviateClient` in constructor options.
2148
2151
  */
2149
2152
  async resolveById(memoryId) {
2150
- if (!this.options?.weaviateClient) {
2153
+ if (!this.options.weaviateClient) {
2151
2154
  throw new Error("resolveById requires weaviateClient in options");
2152
2155
  }
2153
- if (this.options?.memoryIndex) {
2154
- const collectionName = await this.options.memoryIndex.lookup(memoryId);
2155
- if (collectionName) {
2156
- const col = this.options.weaviateClient.collections.get(collectionName);
2157
- const memory = await fetchMemoryWithAllProperties(col, memoryId);
2158
- if (memory?.properties) {
2159
- return {
2160
- memory: normalizeDoc({ id: memory.uuid, ...memory.properties }),
2161
- collectionName
2162
- };
2163
- }
2156
+ const collectionName = await this.options.memoryIndex.lookup(memoryId);
2157
+ if (collectionName) {
2158
+ const col = this.options.weaviateClient.collections.get(collectionName);
2159
+ const memory = await fetchMemoryWithAllProperties(col, memoryId);
2160
+ if (memory?.properties) {
2161
+ return {
2162
+ memory: normalizeDoc({ id: memory.uuid, ...memory.properties }),
2163
+ collectionName
2164
+ };
2164
2165
  }
2165
2166
  }
2166
2167
  return { memory: null, collectionName: null };
@@ -2206,13 +2207,11 @@ var MemoryService = class {
2206
2207
  };
2207
2208
  const memoryId = await this.collection.data.insert({ properties });
2208
2209
  this.logger.info("Memory created", { memoryId, userId: this.userId });
2209
- if (this.options?.memoryIndex) {
2210
- try {
2211
- const collectionName = this.collection.name;
2212
- await this.options.memoryIndex.index(memoryId, collectionName);
2213
- } catch (err2) {
2214
- this.logger.warn?.(`[MemoryService] Index write failed for ${memoryId}: ${err2}`);
2215
- }
2210
+ try {
2211
+ const collectionName = this.collection.name;
2212
+ await this.options.memoryIndex.index(memoryId, collectionName);
2213
+ } catch (err2) {
2214
+ this.logger.warn?.(`[MemoryService] Index write failed for ${memoryId}: ${err2}`);
2216
2215
  }
2217
2216
  return { memory_id: memoryId, created_at: now };
2218
2217
  }
@@ -3146,14 +3145,18 @@ var SpaceService = class {
3146
3145
  userId;
3147
3146
  confirmationTokenService;
3148
3147
  logger;
3148
+ memoryIndexService;
3149
3149
  moderationClient;
3150
- constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2, options) {
3150
+ memoryIndex;
3151
+ constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2, memoryIndexService2, options) {
3151
3152
  this.weaviateClient = weaviateClient;
3152
3153
  this.userCollection = userCollection;
3153
3154
  this.userId = userId;
3154
3155
  this.confirmationTokenService = confirmationTokenService;
3155
3156
  this.logger = logger2;
3157
+ this.memoryIndexService = memoryIndexService2;
3156
3158
  this.moderationClient = options?.moderationClient;
3159
+ this.memoryIndex = memoryIndexService2;
3157
3160
  }
3158
3161
  // ── Content moderation helper ────────────────────────────────────────
3159
3162
  async checkModeration(content) {
@@ -3559,6 +3562,11 @@ var SpaceService = class {
3559
3562
  } else {
3560
3563
  await publicCollection.data.insert({ id: weaviateId, properties: publishedMemory });
3561
3564
  }
3565
+ try {
3566
+ await this.memoryIndex.index(weaviateId, "Memory_spaces_public");
3567
+ } catch (err2) {
3568
+ this.logger.warn?.(`[SpaceService] Index write failed for ${weaviateId}: ${err2}`);
3569
+ }
3562
3570
  successfulPublications.push(`spaces: ${spaces.join(", ")}`);
3563
3571
  } catch (err2) {
3564
3572
  failedPublications.push(`spaces: ${err2 instanceof Error ? err2.message : String(err2)}`);
@@ -3595,6 +3603,11 @@ var SpaceService = class {
3595
3603
  } else {
3596
3604
  await groupCollection.data.insert({ id: weaviateId, properties: groupMemory });
3597
3605
  }
3606
+ try {
3607
+ await this.memoryIndex.index(weaviateId, groupCollectionName);
3608
+ } catch (err2) {
3609
+ this.logger.warn?.(`[SpaceService] Index write failed for ${weaviateId} in ${groupCollectionName}: ${err2}`);
3610
+ }
3598
3611
  successfulPublications.push(`group: ${groupId}`);
3599
3612
  } catch (err2) {
3600
3613
  failedPublications.push(`group ${groupId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
@@ -3835,6 +3848,144 @@ var REM_STATE_COLLECTION = `${BASE}.rem_state`;
3835
3848
  // node_modules/@prmichaelsen/remember-core/dist/services/rem.clustering.js
3836
3849
  import { Filters as Filters5 } from "weaviate-client";
3837
3850
 
3851
+ // node_modules/@prmichaelsen/remember-core/dist/services/memory-index.service.js
3852
+ var MemoryIndexService = class {
3853
+ logger;
3854
+ collectionPath;
3855
+ constructor(logger2) {
3856
+ this.logger = logger2;
3857
+ this.collectionPath = getMemoryIndexPath();
3858
+ }
3859
+ /**
3860
+ * Index a memory UUID → collection name mapping.
3861
+ * Uses set() for idempotency (safe to re-index).
3862
+ */
3863
+ async index(memoryUuid, collectionName) {
3864
+ const entry = {
3865
+ collection_name: collectionName,
3866
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3867
+ };
3868
+ await setDocument2(this.collectionPath, memoryUuid, entry);
3869
+ this.logger.debug?.(`[MemoryIndex] Indexed ${memoryUuid} \u2192 ${collectionName}`);
3870
+ }
3871
+ /**
3872
+ * Look up the collection name for a memory UUID.
3873
+ * Returns null if the memory is not indexed.
3874
+ */
3875
+ async lookup(memoryUuid) {
3876
+ const doc = await getDocument2(this.collectionPath, memoryUuid);
3877
+ if (!doc)
3878
+ return null;
3879
+ const entry = doc;
3880
+ return entry.collection_name ?? null;
3881
+ }
3882
+ };
3883
+
3884
+ // node_modules/@prmichaelsen/remember-core/dist/services/moderation.service.js
3885
+ import { createHash as createHash2 } from "crypto";
3886
+ function buildModerationPrompt(content) {
3887
+ return `You are a content moderation classifier. Evaluate the following user-generated content and determine if it should be ALLOWED or BLOCKED from a public space.
3888
+
3889
+ You must be LENIENT. Most content should pass. Only block content that is clearly and explicitly in one of these categories:
3890
+
3891
+ BLOCK categories:
3892
+
3893
+ 1. HATE SPEECH \u2014 Explicit dehumanization of ethnic/racial/religious groups, calls for violence against protected groups, racial slurs used to demean.
3894
+ Examples that FAIL:
3895
+ - "All [ethnic group] should be exterminated. They are subhuman parasites."
3896
+ - Explicit Nazi ideology or white supremacist recruitment
3897
+ - "[Racial slur] don't deserve rights"
3898
+ - Genocide celebration ("The Holocaust was a good thing")
3899
+
3900
+ 2. VIOLENCE INCITEMENT \u2014 Direct threats against specific people or locations, incitement to commit specific acts of mass violence.
3901
+ Examples that FAIL:
3902
+ - "I'm going to find [person] and make them pay. They won't see it coming."
3903
+ - "Someone should shoot up [location]. That would teach them."
3904
+ Examples that PASS:
3905
+ - Instructions or information about weapons (legally protected speech without incitement)
3906
+ - Historical/philosophical discussion of violence
3907
+ - Fictional violence (games, stories, D&D campaigns)
3908
+ - Colloquial expressions ("I could kill for a burger")
3909
+
3910
+ 3. CSAM \u2014 Any sexual content involving minors. Zero tolerance.
3911
+
3912
+ 4. SELF-HARM ENCOURAGEMENT \u2014 Detailed instructions or active encouragement for self-harm or suicide.
3913
+ Examples that PASS:
3914
+ - Discussions about depression or mental health struggles
3915
+ - Jokes about suicide or dark humor about self-harm
3916
+ - Journaling about difficult feelings
3917
+
3918
+ ALWAYS ALLOW:
3919
+ - Harsh political opinions, criticism of governments or public figures
3920
+ - Edgy humor, dark comedy, offensive jokes (without targeted dehumanization)
3921
+ - Strong opinions about religion, ideology, or social issues
3922
+ - Profanity and vulgar language
3923
+ - Controversial or uncomfortable topics
3924
+ - Educational/historical content about atrocities
3925
+ - The French Revolution, violent revolution as philosophical concept
3926
+
3927
+ Content to evaluate:
3928
+ ---
3929
+ ${content}
3930
+ ---
3931
+
3932
+ Respond with ONLY valid JSON:
3933
+ {"pass":true}
3934
+ OR
3935
+ {"pass":false,"reason":"<specific, human-friendly explanation of why this was blocked>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}`;
3936
+ }
3937
+ var DEFAULT_CACHE_MAX = 1e3;
3938
+ function hashContent(content) {
3939
+ return createHash2("sha256").update(content).digest("hex");
3940
+ }
3941
+ function createModerationClient(options) {
3942
+ const model = options.model ?? "claude-haiku-4-5-20251001";
3943
+ const cacheMax = options.cacheMax ?? DEFAULT_CACHE_MAX;
3944
+ const cache = /* @__PURE__ */ new Map();
3945
+ return {
3946
+ async moderate(content) {
3947
+ const hash = hashContent(content);
3948
+ const cached = cache.get(hash);
3949
+ if (cached)
3950
+ return cached;
3951
+ try {
3952
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
3953
+ method: "POST",
3954
+ headers: {
3955
+ "Content-Type": "application/json",
3956
+ "x-api-key": options.apiKey,
3957
+ "anthropic-version": "2023-06-01"
3958
+ },
3959
+ body: JSON.stringify({
3960
+ model,
3961
+ max_tokens: 256,
3962
+ messages: [{ role: "user", content: buildModerationPrompt(content) }]
3963
+ })
3964
+ });
3965
+ if (!response.ok) {
3966
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3967
+ }
3968
+ const data = await response.json();
3969
+ const text = data.content?.[0]?.text ?? "";
3970
+ const parsed = JSON.parse(text);
3971
+ const result = {
3972
+ pass: parsed.pass === true,
3973
+ reason: parsed.reason ?? "",
3974
+ category: parsed.pass ? void 0 : parsed.category
3975
+ };
3976
+ if (cache.size >= cacheMax) {
3977
+ const oldest = cache.keys().next().value;
3978
+ cache.delete(oldest);
3979
+ }
3980
+ cache.set(hash, result);
3981
+ return result;
3982
+ } catch {
3983
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3984
+ }
3985
+ }
3986
+ };
3987
+ }
3988
+
3838
3989
  // src/weaviate/schema.ts
3839
3990
  init_logger();
3840
3991
 
@@ -3951,6 +4102,8 @@ function getMemoryCollection(userId) {
3951
4102
  var coreLogger = createLogger("info");
3952
4103
  var tokenService = new ConfirmationTokenService(coreLogger);
3953
4104
  var preferencesService = new PreferencesDatabaseService(coreLogger);
4105
+ var moderationClient = process.env.ANTHROPIC_API_KEY ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY }) : void 0;
4106
+ var memoryIndexService = new MemoryIndexService(coreLogger);
3954
4107
  var coreServicesCache = /* @__PURE__ */ new Map();
3955
4108
  function createCoreServices(userId) {
3956
4109
  const cached = coreServicesCache.get(userId);
@@ -3959,9 +4112,12 @@ function createCoreServices(userId) {
3959
4112
  const collection = getMemoryCollection(userId);
3960
4113
  const weaviateClient = getWeaviateClient();
3961
4114
  const services = {
3962
- memory: new MemoryService(collection, userId, coreLogger),
4115
+ memory: new MemoryService(collection, userId, coreLogger, {
4116
+ memoryIndex: memoryIndexService,
4117
+ weaviateClient
4118
+ }),
3963
4119
  relationship: new RelationshipService(collection, userId, coreLogger),
3964
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
4120
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, memoryIndexService, { moderationClient }),
3965
4121
  preferences: preferencesService,
3966
4122
  token: tokenService
3967
4123
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/remember-mcp",
3
- "version": "3.14.20",
3
+ "version": "3.15.0",
4
4
  "description": "Multi-tenant memory system MCP server with vector search and relationships",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",
@@ -50,7 +50,7 @@
50
50
  "@modelcontextprotocol/sdk": "^1.0.4",
51
51
  "@prmichaelsen/firebase-admin-sdk-v8": "^2.2.0",
52
52
  "@prmichaelsen/mcp-auth": "^7.0.4",
53
- "@prmichaelsen/remember-core": "^0.32.1",
53
+ "@prmichaelsen/remember-core": "^0.33.1",
54
54
  "dotenv": "^16.4.5",
55
55
  "uuid": "^13.0.0",
56
56
  "weaviate-client": "^3.2.0"
@@ -7,13 +7,15 @@
7
7
 
8
8
  import {
9
9
  MemoryService,
10
+ MemoryIndexService,
10
11
  RelationshipService,
11
12
  SpaceService,
12
13
  PreferencesDatabaseService,
13
14
  ConfirmationTokenService,
14
15
  createLogger,
16
+ createModerationClient,
15
17
  } from '@prmichaelsen/remember-core';
16
- import type { Logger } from '@prmichaelsen/remember-core';
18
+ import type { Logger, ModerationClient } from '@prmichaelsen/remember-core';
17
19
  import { getWeaviateClient } from './weaviate/client.js';
18
20
  import { getMemoryCollection } from './weaviate/schema.js';
19
21
 
@@ -29,6 +31,10 @@ export interface CoreServices {
29
31
  const coreLogger: Logger = createLogger('info');
30
32
  const tokenService = new ConfirmationTokenService(coreLogger);
31
33
  const preferencesService = new PreferencesDatabaseService(coreLogger);
34
+ const moderationClient: ModerationClient | undefined = process.env.ANTHROPIC_API_KEY
35
+ ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY })
36
+ : undefined;
37
+ const memoryIndexService = new MemoryIndexService(coreLogger);
32
38
 
33
39
  /** Cached CoreServices per userId — avoids re-instantiation on every tool call */
34
40
  const coreServicesCache = new Map<string, CoreServices>();
@@ -45,9 +51,12 @@ export function createCoreServices(userId: string): CoreServices {
45
51
  const weaviateClient = getWeaviateClient();
46
52
 
47
53
  const services: CoreServices = {
48
- memory: new MemoryService(collection, userId, coreLogger),
54
+ memory: new MemoryService(collection, userId, coreLogger, {
55
+ memoryIndex: memoryIndexService,
56
+ weaviateClient,
57
+ }),
49
58
  relationship: new RelationshipService(collection, userId, coreLogger),
50
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
59
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, memoryIndexService, { moderationClient }),
51
60
  preferences: preferencesService,
52
61
  token: tokenService,
53
62
  };