@prmichaelsen/remember-mcp 3.14.20 → 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`.
@@ -3831,6 +3831,111 @@ var REM_STATE_COLLECTION = `${BASE}.rem_state`;
3831
3831
  // node_modules/@prmichaelsen/remember-core/dist/services/rem.clustering.js
3832
3832
  import { Filters as Filters5 } from "weaviate-client";
3833
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
+
3834
3939
  // src/weaviate/schema.ts
3835
3940
  init_logger();
3836
3941
 
@@ -3947,6 +4052,7 @@ function getMemoryCollection(userId) {
3947
4052
  var coreLogger = createLogger("info");
3948
4053
  var tokenService = new ConfirmationTokenService(coreLogger);
3949
4054
  var preferencesService = new PreferencesDatabaseService(coreLogger);
4055
+ var moderationClient = process.env.ANTHROPIC_API_KEY ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY }) : void 0;
3950
4056
  var coreServicesCache = /* @__PURE__ */ new Map();
3951
4057
  function createCoreServices(userId) {
3952
4058
  const cached = coreServicesCache.get(userId);
@@ -3957,7 +4063,7 @@ function createCoreServices(userId) {
3957
4063
  const services = {
3958
4064
  memory: new MemoryService(collection, userId, coreLogger),
3959
4065
  relationship: new RelationshipService(collection, userId, coreLogger),
3960
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
4066
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, { moderationClient }),
3961
4067
  preferences: preferencesService,
3962
4068
  token: tokenService
3963
4069
  };
package/dist/server.js CHANGED
@@ -3835,6 +3835,111 @@ var REM_STATE_COLLECTION = `${BASE}.rem_state`;
3835
3835
  // node_modules/@prmichaelsen/remember-core/dist/services/rem.clustering.js
3836
3836
  import { Filters as Filters5 } from "weaviate-client";
3837
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
+
3838
3943
  // src/weaviate/schema.ts
3839
3944
  init_logger();
3840
3945
 
@@ -3951,6 +4056,7 @@ function getMemoryCollection(userId) {
3951
4056
  var coreLogger = createLogger("info");
3952
4057
  var tokenService = new ConfirmationTokenService(coreLogger);
3953
4058
  var preferencesService = new PreferencesDatabaseService(coreLogger);
4059
+ var moderationClient = process.env.ANTHROPIC_API_KEY ? createModerationClient({ apiKey: process.env.ANTHROPIC_API_KEY }) : void 0;
3954
4060
  var coreServicesCache = /* @__PURE__ */ new Map();
3955
4061
  function createCoreServices(userId) {
3956
4062
  const cached = coreServicesCache.get(userId);
@@ -3961,7 +4067,7 @@ function createCoreServices(userId) {
3961
4067
  const services = {
3962
4068
  memory: new MemoryService(collection, userId, coreLogger),
3963
4069
  relationship: new RelationshipService(collection, userId, coreLogger),
3964
- space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
4070
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger, { moderationClient }),
3965
4071
  preferences: preferencesService,
3966
4072
  token: tokenService
3967
4073
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/remember-mcp",
3
- "version": "3.14.20",
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",
@@ -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
  };