@prmichaelsen/remember-mcp 3.14.19 → 3.14.21

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,14 @@ 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.14.21] - 2026-03-06
9
+
10
+ ### Added
11
+
12
+ - Wire LLM auto-moderation into SpaceService publish/revise flow (task-202)
13
+ - Create singleton `ModerationClient` from `ANTHROPIC_API_KEY` env var when present
14
+ - Content published to spaces is now screened by Claude Haiku before storage
15
+
8
16
  ## [3.14.14] - 2026-03-04
9
17
 
10
18
  ### 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`.
@@ -886,7 +886,7 @@ var DEFAULT_PREFERENCES = {
886
886
  share_with_memories: true
887
887
  },
888
888
  privacy: {
889
- default_trust_level: 0.25,
889
+ default_trust_level: 2,
890
890
  allow_cross_user_access: false,
891
891
  auto_approve_requests: false,
892
892
  audit_logging: true
@@ -905,6 +905,14 @@ var DEFAULT_PREFERENCES = {
905
905
  }
906
906
  };
907
907
 
908
+ // node_modules/@prmichaelsen/remember-core/dist/types/rating.types.js
909
+ var RATING_MIN_THRESHOLD = 5;
910
+ function computeRatingAvg(ratingSum, ratingCount) {
911
+ if (ratingCount < RATING_MIN_THRESHOLD)
912
+ return null;
913
+ return ratingSum / ratingCount;
914
+ }
915
+
908
916
  // node_modules/@prmichaelsen/remember-core/dist/types/space.types.js
909
917
  var SUPPORTED_SPACES = [
910
918
  "the_void",
@@ -1361,6 +1369,35 @@ var DebugLevel2;
1361
1369
  DebugLevel3[DebugLevel3["TRACE"] = 5] = "TRACE";
1362
1370
  })(DebugLevel2 || (DebugLevel2 = {}));
1363
1371
 
1372
+ // node_modules/@prmichaelsen/remember-core/dist/errors/base.error.js
1373
+ var AppError = class extends Error {
1374
+ context;
1375
+ constructor(message, context = {}) {
1376
+ super(message);
1377
+ this.context = context;
1378
+ this.name = this.constructor.name;
1379
+ Object.setPrototypeOf(this, new.target.prototype);
1380
+ }
1381
+ toJSON() {
1382
+ return {
1383
+ kind: this.kind,
1384
+ name: this.name,
1385
+ message: this.message,
1386
+ context: this.context
1387
+ };
1388
+ }
1389
+ };
1390
+
1391
+ // node_modules/@prmichaelsen/remember-core/dist/errors/app-errors.js
1392
+ var ValidationError = class extends AppError {
1393
+ fields;
1394
+ kind = "validation";
1395
+ constructor(message, fields = {}) {
1396
+ super(message, { fields });
1397
+ this.fields = fields;
1398
+ }
1399
+ };
1400
+
1364
1401
  // node_modules/@prmichaelsen/remember-core/node_modules/uuid/dist/esm/regex.js
1365
1402
  var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
1366
1403
 
@@ -1637,6 +1674,9 @@ function buildDocTypeFilters(collection, docType, filters) {
1637
1674
  if (filters?.relationship_count_max !== void 0) {
1638
1675
  filterList.push(collection.filter.byProperty("relationship_count").lessOrEqual(filters.relationship_count_max));
1639
1676
  }
1677
+ if (filters?.rating_min !== void 0) {
1678
+ filterList.push(collection.filter.byProperty("rating_bayesian").greaterOrEqual(filters.rating_min));
1679
+ }
1640
1680
  if (filters?.tags && filters.tags.length > 0) {
1641
1681
  filterList.push(collection.filter.byProperty("tags").containsAny(filters.tags));
1642
1682
  }
@@ -2004,6 +2044,9 @@ var ALL_MEMORY_PROPERTIES = [
2004
2044
  "observation",
2005
2045
  "strength",
2006
2046
  "source",
2047
+ "rating_sum",
2048
+ "rating_count",
2049
+ "rating_bayesian",
2007
2050
  "access_count",
2008
2051
  "last_accessed_at",
2009
2052
  "tags",
@@ -2053,6 +2096,11 @@ function normalizeDoc(doc) {
2053
2096
  if ("trust_score" in doc) {
2054
2097
  doc.trust_score = normalizeTrustScore(doc.trust_score);
2055
2098
  }
2099
+ const ratingSum = doc.rating_sum;
2100
+ const ratingCount = doc.rating_count;
2101
+ if (ratingSum !== void 0 && ratingCount !== void 0) {
2102
+ doc.rating_avg = computeRatingAvg(ratingSum, ratingCount);
2103
+ }
2056
2104
  return doc;
2057
2105
  }
2058
2106
  var MemoryService = class {
@@ -2125,12 +2173,16 @@ var MemoryService = class {
2125
2173
  summary: input.title,
2126
2174
  content_type: contentType,
2127
2175
  weight: input.weight ?? 0.5,
2128
- trust_score: input.trust ?? TrustLevel.INTERNAL,
2176
+ trust_score: normalizeTrustScore(input.trust ?? TrustLevel.INTERNAL),
2129
2177
  confidence: 1,
2130
2178
  context_summary: input.context_summary || "Memory created",
2131
2179
  context_conversation_id: input.context_conversation_id,
2132
2180
  relationship_ids: [],
2133
2181
  relationship_count: 0,
2182
+ rating_sum: 0,
2183
+ rating_count: 0,
2184
+ rating_bayesian: 3,
2185
+ // (0 + 15) / (0 + 5) = 3.0 (prior mean)
2134
2186
  access_count: 0,
2135
2187
  last_accessed_at: now,
2136
2188
  created_at: now,
@@ -2294,6 +2346,47 @@ var MemoryService = class {
2294
2346
  limit
2295
2347
  };
2296
2348
  }
2349
+ // ── By Rating (Bayesian average) ─────────────────────────────────
2350
+ async byRating(input) {
2351
+ const limit = input.limit ?? 50;
2352
+ const offset = input.offset ?? 0;
2353
+ const direction = input.direction ?? "desc";
2354
+ const memoryFilters = buildMemoryOnlyFilters(this.collection, input.filters);
2355
+ const ghostFilters = [];
2356
+ if (input.ghost_context) {
2357
+ ghostFilters.push(buildTrustFilter(this.collection, input.ghost_context.accessor_trust_level));
2358
+ }
2359
+ if (!input.ghost_context?.include_ghost_content) {
2360
+ ghostFilters.push(this.collection.filter.byProperty("content_type").notEqual("ghost"));
2361
+ }
2362
+ const executeQuery = async (useDeletedFilter) => {
2363
+ const deletedFilter = useDeletedFilter ? buildDeletedFilter(this.collection, input.deleted_filter || "exclude") : null;
2364
+ const combinedFilters = combineFiltersWithAnd([deletedFilter, memoryFilters, ...ghostFilters].filter((f) => f !== null));
2365
+ const queryOptions = {
2366
+ limit: limit + offset,
2367
+ sort: this.collection.sort.byProperty("rating_bayesian", direction === "asc")
2368
+ };
2369
+ if (combinedFilters) {
2370
+ queryOptions.filters = combinedFilters;
2371
+ }
2372
+ return this.collection.query.fetchObjects(queryOptions);
2373
+ };
2374
+ const results = await this.retryWithoutDeletedFilter(executeQuery);
2375
+ const paginated = results.objects.slice(offset);
2376
+ const memories = [];
2377
+ for (const obj of paginated) {
2378
+ const doc = normalizeDoc({ id: obj.uuid, ...obj.properties });
2379
+ if (doc.doc_type === "memory") {
2380
+ memories.push(doc);
2381
+ }
2382
+ }
2383
+ return {
2384
+ memories,
2385
+ total: memories.length,
2386
+ offset,
2387
+ limit
2388
+ };
2389
+ }
2297
2390
  // ── Find Similar (vector) ──────────────────────────────────────────
2298
2391
  async findSimilar(input) {
2299
2392
  if (!input.memory_id && !input.text)
@@ -2869,6 +2962,10 @@ var COMMON_MEMORY_PROPERTIES = [
2869
2962
  { name: "relationships", dataType: configure.dataType.TEXT_ARRAY },
2870
2963
  { name: "memory_ids", dataType: configure.dataType.TEXT_ARRAY },
2871
2964
  { name: "relationship_count", dataType: configure.dataType.INT },
2965
+ // Rating aggregates (denormalized from Firestore individual ratings)
2966
+ { name: "rating_sum", dataType: configure.dataType.INT },
2967
+ { name: "rating_count", dataType: configure.dataType.INT },
2968
+ { name: "rating_bayesian", dataType: configure.dataType.NUMBER },
2872
2969
  // Access tracking
2873
2970
  { name: "access_count", dataType: configure.dataType.NUMBER },
2874
2971
  { name: "last_accessed_at", dataType: configure.dataType.DATE },
@@ -3045,12 +3142,26 @@ var SpaceService = class {
3045
3142
  userId;
3046
3143
  confirmationTokenService;
3047
3144
  logger;
3048
- constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2) {
3145
+ moderationClient;
3146
+ constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2, options) {
3049
3147
  this.weaviateClient = weaviateClient;
3050
3148
  this.userCollection = userCollection;
3051
3149
  this.userId = userId;
3052
3150
  this.confirmationTokenService = confirmationTokenService;
3053
3151
  this.logger = logger2;
3152
+ this.moderationClient = options?.moderationClient;
3153
+ }
3154
+ // ── Content moderation helper ────────────────────────────────────────
3155
+ async checkModeration(content) {
3156
+ if (!this.moderationClient)
3157
+ return;
3158
+ const result = await this.moderationClient.moderate(content);
3159
+ if (!result.pass) {
3160
+ throw new ValidationError(result.reason, {
3161
+ moderation: ["blocked"],
3162
+ ...result.category ? { category: [result.category] } : {}
3163
+ });
3164
+ }
3054
3165
  }
3055
3166
  // ── Publish (phase 1: generate confirmation token) ──────────────────
3056
3167
  async publish(input) {
@@ -3085,6 +3196,7 @@ var SpaceService = class {
3085
3196
  throw new Error(`Space '${spaceId}' only accepts content_type '${requiredType}', got '${memoryContentType ?? "undefined"}'`);
3086
3197
  }
3087
3198
  }
3199
+ await this.checkModeration(memory.properties.content);
3088
3200
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "publish_memory", {
3089
3201
  memory_id: input.memory_id,
3090
3202
  spaces,
@@ -3151,6 +3263,7 @@ var SpaceService = class {
3151
3263
  if (spaceIds.length === 0 && groupIds.length === 0) {
3152
3264
  throw new Error("Memory has no published copies to revise. Publish first with publish().");
3153
3265
  }
3266
+ await this.checkModeration(memory.properties.content);
3154
3267
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "revise_memory", {
3155
3268
  memory_id: input.memory_id,
3156
3269
  space_ids: spaceIds,
@@ -3718,6 +3831,111 @@ var REM_STATE_COLLECTION = `${BASE}.rem_state`;
3718
3831
  // node_modules/@prmichaelsen/remember-core/dist/services/rem.clustering.js
3719
3832
  import { Filters as Filters5 } from "weaviate-client";
3720
3833
 
3834
+ // node_modules/@prmichaelsen/remember-core/dist/services/moderation.service.js
3835
+ import { createHash as createHash2 } from "crypto";
3836
+ function buildModerationPrompt(content) {
3837
+ 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.
3838
+
3839
+ You must be LENIENT. Most content should pass. Only block content that is clearly and explicitly in one of these categories:
3840
+
3841
+ BLOCK categories:
3842
+
3843
+ 1. HATE SPEECH \u2014 Explicit dehumanization of ethnic/racial/religious groups, calls for violence against protected groups, racial slurs used to demean.
3844
+ Examples that FAIL:
3845
+ - "All [ethnic group] should be exterminated. They are subhuman parasites."
3846
+ - Explicit Nazi ideology or white supremacist recruitment
3847
+ - "[Racial slur] don't deserve rights"
3848
+ - Genocide celebration ("The Holocaust was a good thing")
3849
+
3850
+ 2. VIOLENCE INCITEMENT \u2014 Direct threats against specific people or locations, incitement to commit specific acts of mass violence.
3851
+ Examples that FAIL:
3852
+ - "I'm going to find [person] and make them pay. They won't see it coming."
3853
+ - "Someone should shoot up [location]. That would teach them."
3854
+ Examples that PASS:
3855
+ - Instructions or information about weapons (legally protected speech without incitement)
3856
+ - Historical/philosophical discussion of violence
3857
+ - Fictional violence (games, stories, D&D campaigns)
3858
+ - Colloquial expressions ("I could kill for a burger")
3859
+
3860
+ 3. CSAM \u2014 Any sexual content involving minors. Zero tolerance.
3861
+
3862
+ 4. SELF-HARM ENCOURAGEMENT \u2014 Detailed instructions or active encouragement for self-harm or suicide.
3863
+ Examples that PASS:
3864
+ - Discussions about depression or mental health struggles
3865
+ - Jokes about suicide or dark humor about self-harm
3866
+ - Journaling about difficult feelings
3867
+
3868
+ ALWAYS ALLOW:
3869
+ - Harsh political opinions, criticism of governments or public figures
3870
+ - Edgy humor, dark comedy, offensive jokes (without targeted dehumanization)
3871
+ - Strong opinions about religion, ideology, or social issues
3872
+ - Profanity and vulgar language
3873
+ - Controversial or uncomfortable topics
3874
+ - Educational/historical content about atrocities
3875
+ - The French Revolution, violent revolution as philosophical concept
3876
+
3877
+ Content to evaluate:
3878
+ ---
3879
+ ${content}
3880
+ ---
3881
+
3882
+ Respond with ONLY valid JSON:
3883
+ {"pass":true}
3884
+ OR
3885
+ {"pass":false,"reason":"<specific, human-friendly explanation of why this was blocked>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}`;
3886
+ }
3887
+ var DEFAULT_CACHE_MAX = 1e3;
3888
+ function hashContent(content) {
3889
+ return createHash2("sha256").update(content).digest("hex");
3890
+ }
3891
+ function createModerationClient(options) {
3892
+ const model = options.model ?? "claude-haiku-4-5-20251001";
3893
+ const cacheMax = options.cacheMax ?? DEFAULT_CACHE_MAX;
3894
+ const cache = /* @__PURE__ */ new Map();
3895
+ return {
3896
+ async moderate(content) {
3897
+ const hash = hashContent(content);
3898
+ const cached = cache.get(hash);
3899
+ if (cached)
3900
+ return cached;
3901
+ try {
3902
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
3903
+ method: "POST",
3904
+ headers: {
3905
+ "Content-Type": "application/json",
3906
+ "x-api-key": options.apiKey,
3907
+ "anthropic-version": "2023-06-01"
3908
+ },
3909
+ body: JSON.stringify({
3910
+ model,
3911
+ max_tokens: 256,
3912
+ messages: [{ role: "user", content: buildModerationPrompt(content) }]
3913
+ })
3914
+ });
3915
+ if (!response.ok) {
3916
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3917
+ }
3918
+ const data = await response.json();
3919
+ const text = data.content?.[0]?.text ?? "";
3920
+ const parsed = JSON.parse(text);
3921
+ const result = {
3922
+ pass: parsed.pass === true,
3923
+ reason: parsed.reason ?? "",
3924
+ category: parsed.pass ? void 0 : parsed.category
3925
+ };
3926
+ if (cache.size >= cacheMax) {
3927
+ const oldest = cache.keys().next().value;
3928
+ cache.delete(oldest);
3929
+ }
3930
+ cache.set(hash, result);
3931
+ return result;
3932
+ } catch {
3933
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3934
+ }
3935
+ }
3936
+ };
3937
+ }
3938
+
3721
3939
  // src/weaviate/schema.ts
3722
3940
  init_logger();
3723
3941
 
@@ -3834,6 +4052,7 @@ function getMemoryCollection(userId) {
3834
4052
  var coreLogger = createLogger("info");
3835
4053
  var tokenService = new ConfirmationTokenService(coreLogger);
3836
4054
  var preferencesService = new PreferencesDatabaseService(coreLogger);
4055
+ var moderationClient = process.env.ANTHROPIC_API_KEY ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY }) : void 0;
3837
4056
  var coreServicesCache = /* @__PURE__ */ new Map();
3838
4057
  function createCoreServices(userId) {
3839
4058
  const cached = coreServicesCache.get(userId);
@@ -3844,7 +4063,7 @@ function createCoreServices(userId) {
3844
4063
  const services = {
3845
4064
  memory: new MemoryService(collection, userId, coreLogger),
3846
4065
  relationship: new RelationshipService(collection, userId, coreLogger),
3847
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
4066
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, { moderationClient }),
3848
4067
  preferences: preferencesService,
3849
4068
  token: tokenService
3850
4069
  };
package/dist/server.js CHANGED
@@ -890,7 +890,7 @@ var DEFAULT_PREFERENCES = {
890
890
  share_with_memories: true
891
891
  },
892
892
  privacy: {
893
- default_trust_level: 0.25,
893
+ default_trust_level: 2,
894
894
  allow_cross_user_access: false,
895
895
  auto_approve_requests: false,
896
896
  audit_logging: true
@@ -909,6 +909,14 @@ var DEFAULT_PREFERENCES = {
909
909
  }
910
910
  };
911
911
 
912
+ // node_modules/@prmichaelsen/remember-core/dist/types/rating.types.js
913
+ var RATING_MIN_THRESHOLD = 5;
914
+ function computeRatingAvg(ratingSum, ratingCount) {
915
+ if (ratingCount < RATING_MIN_THRESHOLD)
916
+ return null;
917
+ return ratingSum / ratingCount;
918
+ }
919
+
912
920
  // node_modules/@prmichaelsen/remember-core/dist/types/space.types.js
913
921
  var SUPPORTED_SPACES = [
914
922
  "the_void",
@@ -1365,6 +1373,35 @@ var DebugLevel2;
1365
1373
  DebugLevel3[DebugLevel3["TRACE"] = 5] = "TRACE";
1366
1374
  })(DebugLevel2 || (DebugLevel2 = {}));
1367
1375
 
1376
+ // node_modules/@prmichaelsen/remember-core/dist/errors/base.error.js
1377
+ var AppError = class extends Error {
1378
+ context;
1379
+ constructor(message, context = {}) {
1380
+ super(message);
1381
+ this.context = context;
1382
+ this.name = this.constructor.name;
1383
+ Object.setPrototypeOf(this, new.target.prototype);
1384
+ }
1385
+ toJSON() {
1386
+ return {
1387
+ kind: this.kind,
1388
+ name: this.name,
1389
+ message: this.message,
1390
+ context: this.context
1391
+ };
1392
+ }
1393
+ };
1394
+
1395
+ // node_modules/@prmichaelsen/remember-core/dist/errors/app-errors.js
1396
+ var ValidationError = class extends AppError {
1397
+ fields;
1398
+ kind = "validation";
1399
+ constructor(message, fields = {}) {
1400
+ super(message, { fields });
1401
+ this.fields = fields;
1402
+ }
1403
+ };
1404
+
1368
1405
  // node_modules/@prmichaelsen/remember-core/node_modules/uuid/dist/esm/regex.js
1369
1406
  var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
1370
1407
 
@@ -1641,6 +1678,9 @@ function buildDocTypeFilters(collection, docType, filters) {
1641
1678
  if (filters?.relationship_count_max !== void 0) {
1642
1679
  filterList.push(collection.filter.byProperty("relationship_count").lessOrEqual(filters.relationship_count_max));
1643
1680
  }
1681
+ if (filters?.rating_min !== void 0) {
1682
+ filterList.push(collection.filter.byProperty("rating_bayesian").greaterOrEqual(filters.rating_min));
1683
+ }
1644
1684
  if (filters?.tags && filters.tags.length > 0) {
1645
1685
  filterList.push(collection.filter.byProperty("tags").containsAny(filters.tags));
1646
1686
  }
@@ -2008,6 +2048,9 @@ var ALL_MEMORY_PROPERTIES = [
2008
2048
  "observation",
2009
2049
  "strength",
2010
2050
  "source",
2051
+ "rating_sum",
2052
+ "rating_count",
2053
+ "rating_bayesian",
2011
2054
  "access_count",
2012
2055
  "last_accessed_at",
2013
2056
  "tags",
@@ -2057,6 +2100,11 @@ function normalizeDoc(doc) {
2057
2100
  if ("trust_score" in doc) {
2058
2101
  doc.trust_score = normalizeTrustScore(doc.trust_score);
2059
2102
  }
2103
+ const ratingSum = doc.rating_sum;
2104
+ const ratingCount = doc.rating_count;
2105
+ if (ratingSum !== void 0 && ratingCount !== void 0) {
2106
+ doc.rating_avg = computeRatingAvg(ratingSum, ratingCount);
2107
+ }
2060
2108
  return doc;
2061
2109
  }
2062
2110
  var MemoryService = class {
@@ -2129,12 +2177,16 @@ var MemoryService = class {
2129
2177
  summary: input.title,
2130
2178
  content_type: contentType,
2131
2179
  weight: input.weight ?? 0.5,
2132
- trust_score: input.trust ?? TrustLevel.INTERNAL,
2180
+ trust_score: normalizeTrustScore(input.trust ?? TrustLevel.INTERNAL),
2133
2181
  confidence: 1,
2134
2182
  context_summary: input.context_summary || "Memory created",
2135
2183
  context_conversation_id: input.context_conversation_id,
2136
2184
  relationship_ids: [],
2137
2185
  relationship_count: 0,
2186
+ rating_sum: 0,
2187
+ rating_count: 0,
2188
+ rating_bayesian: 3,
2189
+ // (0 + 15) / (0 + 5) = 3.0 (prior mean)
2138
2190
  access_count: 0,
2139
2191
  last_accessed_at: now,
2140
2192
  created_at: now,
@@ -2298,6 +2350,47 @@ var MemoryService = class {
2298
2350
  limit
2299
2351
  };
2300
2352
  }
2353
+ // ── By Rating (Bayesian average) ─────────────────────────────────
2354
+ async byRating(input) {
2355
+ const limit = input.limit ?? 50;
2356
+ const offset = input.offset ?? 0;
2357
+ const direction = input.direction ?? "desc";
2358
+ const memoryFilters = buildMemoryOnlyFilters(this.collection, input.filters);
2359
+ const ghostFilters = [];
2360
+ if (input.ghost_context) {
2361
+ ghostFilters.push(buildTrustFilter(this.collection, input.ghost_context.accessor_trust_level));
2362
+ }
2363
+ if (!input.ghost_context?.include_ghost_content) {
2364
+ ghostFilters.push(this.collection.filter.byProperty("content_type").notEqual("ghost"));
2365
+ }
2366
+ const executeQuery = async (useDeletedFilter) => {
2367
+ const deletedFilter = useDeletedFilter ? buildDeletedFilter(this.collection, input.deleted_filter || "exclude") : null;
2368
+ const combinedFilters = combineFiltersWithAnd([deletedFilter, memoryFilters, ...ghostFilters].filter((f) => f !== null));
2369
+ const queryOptions = {
2370
+ limit: limit + offset,
2371
+ sort: this.collection.sort.byProperty("rating_bayesian", direction === "asc")
2372
+ };
2373
+ if (combinedFilters) {
2374
+ queryOptions.filters = combinedFilters;
2375
+ }
2376
+ return this.collection.query.fetchObjects(queryOptions);
2377
+ };
2378
+ const results = await this.retryWithoutDeletedFilter(executeQuery);
2379
+ const paginated = results.objects.slice(offset);
2380
+ const memories = [];
2381
+ for (const obj of paginated) {
2382
+ const doc = normalizeDoc({ id: obj.uuid, ...obj.properties });
2383
+ if (doc.doc_type === "memory") {
2384
+ memories.push(doc);
2385
+ }
2386
+ }
2387
+ return {
2388
+ memories,
2389
+ total: memories.length,
2390
+ offset,
2391
+ limit
2392
+ };
2393
+ }
2301
2394
  // ── Find Similar (vector) ──────────────────────────────────────────
2302
2395
  async findSimilar(input) {
2303
2396
  if (!input.memory_id && !input.text)
@@ -2873,6 +2966,10 @@ var COMMON_MEMORY_PROPERTIES = [
2873
2966
  { name: "relationships", dataType: configure.dataType.TEXT_ARRAY },
2874
2967
  { name: "memory_ids", dataType: configure.dataType.TEXT_ARRAY },
2875
2968
  { name: "relationship_count", dataType: configure.dataType.INT },
2969
+ // Rating aggregates (denormalized from Firestore individual ratings)
2970
+ { name: "rating_sum", dataType: configure.dataType.INT },
2971
+ { name: "rating_count", dataType: configure.dataType.INT },
2972
+ { name: "rating_bayesian", dataType: configure.dataType.NUMBER },
2876
2973
  // Access tracking
2877
2974
  { name: "access_count", dataType: configure.dataType.NUMBER },
2878
2975
  { name: "last_accessed_at", dataType: configure.dataType.DATE },
@@ -3049,12 +3146,26 @@ var SpaceService = class {
3049
3146
  userId;
3050
3147
  confirmationTokenService;
3051
3148
  logger;
3052
- constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2) {
3149
+ moderationClient;
3150
+ constructor(weaviateClient, userCollection, userId, confirmationTokenService, logger2, options) {
3053
3151
  this.weaviateClient = weaviateClient;
3054
3152
  this.userCollection = userCollection;
3055
3153
  this.userId = userId;
3056
3154
  this.confirmationTokenService = confirmationTokenService;
3057
3155
  this.logger = logger2;
3156
+ this.moderationClient = options?.moderationClient;
3157
+ }
3158
+ // ── Content moderation helper ────────────────────────────────────────
3159
+ async checkModeration(content) {
3160
+ if (!this.moderationClient)
3161
+ return;
3162
+ const result = await this.moderationClient.moderate(content);
3163
+ if (!result.pass) {
3164
+ throw new ValidationError(result.reason, {
3165
+ moderation: ["blocked"],
3166
+ ...result.category ? { category: [result.category] } : {}
3167
+ });
3168
+ }
3058
3169
  }
3059
3170
  // ── Publish (phase 1: generate confirmation token) ──────────────────
3060
3171
  async publish(input) {
@@ -3089,6 +3200,7 @@ var SpaceService = class {
3089
3200
  throw new Error(`Space '${spaceId}' only accepts content_type '${requiredType}', got '${memoryContentType ?? "undefined"}'`);
3090
3201
  }
3091
3202
  }
3203
+ await this.checkModeration(memory.properties.content);
3092
3204
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "publish_memory", {
3093
3205
  memory_id: input.memory_id,
3094
3206
  spaces,
@@ -3155,6 +3267,7 @@ var SpaceService = class {
3155
3267
  if (spaceIds.length === 0 && groupIds.length === 0) {
3156
3268
  throw new Error("Memory has no published copies to revise. Publish first with publish().");
3157
3269
  }
3270
+ await this.checkModeration(memory.properties.content);
3158
3271
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "revise_memory", {
3159
3272
  memory_id: input.memory_id,
3160
3273
  space_ids: spaceIds,
@@ -3722,6 +3835,111 @@ var REM_STATE_COLLECTION = `${BASE}.rem_state`;
3722
3835
  // node_modules/@prmichaelsen/remember-core/dist/services/rem.clustering.js
3723
3836
  import { Filters as Filters5 } from "weaviate-client";
3724
3837
 
3838
+ // node_modules/@prmichaelsen/remember-core/dist/services/moderation.service.js
3839
+ import { createHash as createHash2 } from "crypto";
3840
+ function buildModerationPrompt(content) {
3841
+ 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.
3842
+
3843
+ You must be LENIENT. Most content should pass. Only block content that is clearly and explicitly in one of these categories:
3844
+
3845
+ BLOCK categories:
3846
+
3847
+ 1. HATE SPEECH \u2014 Explicit dehumanization of ethnic/racial/religious groups, calls for violence against protected groups, racial slurs used to demean.
3848
+ Examples that FAIL:
3849
+ - "All [ethnic group] should be exterminated. They are subhuman parasites."
3850
+ - Explicit Nazi ideology or white supremacist recruitment
3851
+ - "[Racial slur] don't deserve rights"
3852
+ - Genocide celebration ("The Holocaust was a good thing")
3853
+
3854
+ 2. VIOLENCE INCITEMENT \u2014 Direct threats against specific people or locations, incitement to commit specific acts of mass violence.
3855
+ Examples that FAIL:
3856
+ - "I'm going to find [person] and make them pay. They won't see it coming."
3857
+ - "Someone should shoot up [location]. That would teach them."
3858
+ Examples that PASS:
3859
+ - Instructions or information about weapons (legally protected speech without incitement)
3860
+ - Historical/philosophical discussion of violence
3861
+ - Fictional violence (games, stories, D&D campaigns)
3862
+ - Colloquial expressions ("I could kill for a burger")
3863
+
3864
+ 3. CSAM \u2014 Any sexual content involving minors. Zero tolerance.
3865
+
3866
+ 4. SELF-HARM ENCOURAGEMENT \u2014 Detailed instructions or active encouragement for self-harm or suicide.
3867
+ Examples that PASS:
3868
+ - Discussions about depression or mental health struggles
3869
+ - Jokes about suicide or dark humor about self-harm
3870
+ - Journaling about difficult feelings
3871
+
3872
+ ALWAYS ALLOW:
3873
+ - Harsh political opinions, criticism of governments or public figures
3874
+ - Edgy humor, dark comedy, offensive jokes (without targeted dehumanization)
3875
+ - Strong opinions about religion, ideology, or social issues
3876
+ - Profanity and vulgar language
3877
+ - Controversial or uncomfortable topics
3878
+ - Educational/historical content about atrocities
3879
+ - The French Revolution, violent revolution as philosophical concept
3880
+
3881
+ Content to evaluate:
3882
+ ---
3883
+ ${content}
3884
+ ---
3885
+
3886
+ Respond with ONLY valid JSON:
3887
+ {"pass":true}
3888
+ OR
3889
+ {"pass":false,"reason":"<specific, human-friendly explanation of why this was blocked>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}`;
3890
+ }
3891
+ var DEFAULT_CACHE_MAX = 1e3;
3892
+ function hashContent(content) {
3893
+ return createHash2("sha256").update(content).digest("hex");
3894
+ }
3895
+ function createModerationClient(options) {
3896
+ const model = options.model ?? "claude-haiku-4-5-20251001";
3897
+ const cacheMax = options.cacheMax ?? DEFAULT_CACHE_MAX;
3898
+ const cache = /* @__PURE__ */ new Map();
3899
+ return {
3900
+ async moderate(content) {
3901
+ const hash = hashContent(content);
3902
+ const cached = cache.get(hash);
3903
+ if (cached)
3904
+ return cached;
3905
+ try {
3906
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
3907
+ method: "POST",
3908
+ headers: {
3909
+ "Content-Type": "application/json",
3910
+ "x-api-key": options.apiKey,
3911
+ "anthropic-version": "2023-06-01"
3912
+ },
3913
+ body: JSON.stringify({
3914
+ model,
3915
+ max_tokens: 256,
3916
+ messages: [{ role: "user", content: buildModerationPrompt(content) }]
3917
+ })
3918
+ });
3919
+ if (!response.ok) {
3920
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3921
+ }
3922
+ const data = await response.json();
3923
+ const text = data.content?.[0]?.text ?? "";
3924
+ const parsed = JSON.parse(text);
3925
+ const result = {
3926
+ pass: parsed.pass === true,
3927
+ reason: parsed.reason ?? "",
3928
+ category: parsed.pass ? void 0 : parsed.category
3929
+ };
3930
+ if (cache.size >= cacheMax) {
3931
+ const oldest = cache.keys().next().value;
3932
+ cache.delete(oldest);
3933
+ }
3934
+ cache.set(hash, result);
3935
+ return result;
3936
+ } catch {
3937
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
3938
+ }
3939
+ }
3940
+ };
3941
+ }
3942
+
3725
3943
  // src/weaviate/schema.ts
3726
3944
  init_logger();
3727
3945
 
@@ -3838,6 +4056,7 @@ function getMemoryCollection(userId) {
3838
4056
  var coreLogger = createLogger("info");
3839
4057
  var tokenService = new ConfirmationTokenService(coreLogger);
3840
4058
  var preferencesService = new PreferencesDatabaseService(coreLogger);
4059
+ var moderationClient = process.env.ANTHROPIC_API_KEY ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY }) : void 0;
3841
4060
  var coreServicesCache = /* @__PURE__ */ new Map();
3842
4061
  function createCoreServices(userId) {
3843
4062
  const cached = coreServicesCache.get(userId);
@@ -3848,7 +4067,7 @@ function createCoreServices(userId) {
3848
4067
  const services = {
3849
4068
  memory: new MemoryService(collection, userId, coreLogger),
3850
4069
  relationship: new RelationshipService(collection, userId, coreLogger),
3851
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
4070
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, { moderationClient }),
3852
4071
  preferences: preferencesService,
3853
4072
  token: tokenService
3854
4073
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/remember-mcp",
3
- "version": "3.14.19",
3
+ "version": "3.14.21",
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.30.0",
53
+ "@prmichaelsen/remember-core": "^0.32.1",
54
54
  "dotenv": "^16.4.5",
55
55
  "uuid": "^13.0.0",
56
56
  "weaviate-client": "^3.2.0"
@@ -12,8 +12,9 @@ import {
12
12
  PreferencesDatabaseService,
13
13
  ConfirmationTokenService,
14
14
  createLogger,
15
+ createModerationClient,
15
16
  } from '@prmichaelsen/remember-core';
16
- import type { Logger } from '@prmichaelsen/remember-core';
17
+ import type { Logger, ModerationClient } from '@prmichaelsen/remember-core';
17
18
  import { getWeaviateClient } from './weaviate/client.js';
18
19
  import { getMemoryCollection } from './weaviate/schema.js';
19
20
 
@@ -29,6 +30,9 @@ export interface CoreServices {
29
30
  const coreLogger: Logger = createLogger('info');
30
31
  const tokenService = new ConfirmationTokenService(coreLogger);
31
32
  const preferencesService = new PreferencesDatabaseService(coreLogger);
33
+ const moderationClient: ModerationClient | undefined = process.env.ANTHROPIC_API_KEY
34
+ ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY })
35
+ : undefined;
32
36
 
33
37
  /** Cached CoreServices per userId — avoids re-instantiation on every tool call */
34
38
  const coreServicesCache = new Map<string, CoreServices>();
@@ -47,7 +51,7 @@ export function createCoreServices(userId: string): CoreServices {
47
51
  const services: CoreServices = {
48
52
  memory: new MemoryService(collection, userId, coreLogger),
49
53
  relationship: new RelationshipService(collection, userId, coreLogger),
50
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
54
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, { moderationClient }),
51
55
  preferences: preferencesService,
52
56
  token: tokenService,
53
57
  };