@prmichaelsen/remember-mcp 3.15.3 → 3.15.4

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.
@@ -2331,6 +2331,38 @@ async function getSpaceConfig(id, type, logger2) {
2331
2331
  // node_modules/@prmichaelsen/remember-core/dist/services/memory.service.js
2332
2332
  import { Filters as Filters2 } from "weaviate-client";
2333
2333
 
2334
+ // node_modules/@prmichaelsen/remember-core/dist/services/discovery.js
2335
+ var DISCOVERY_RATIO = 4;
2336
+ var DISCOVERY_THRESHOLD = 5;
2337
+ function interleaveDiscovery(options) {
2338
+ const { rated, discovery, ratio = DISCOVERY_RATIO, offset = 0, limit } = options;
2339
+ const totalAvailable = rated.length + discovery.length;
2340
+ if (totalAvailable === 0)
2341
+ return [];
2342
+ const merged = [];
2343
+ let ratedIdx = 0;
2344
+ let discoveryIdx = 0;
2345
+ for (let pos = 0; pos < totalAvailable; pos++) {
2346
+ const isDiscoverySlot = (pos + 1) % (ratio + 1) === 0;
2347
+ if (isDiscoverySlot) {
2348
+ if (discoveryIdx < discovery.length) {
2349
+ merged.push({ item: discovery[discoveryIdx++], is_discovery: true });
2350
+ } else if (ratedIdx < rated.length) {
2351
+ merged.push({ item: rated[ratedIdx++], is_discovery: false });
2352
+ }
2353
+ } else {
2354
+ if (ratedIdx < rated.length) {
2355
+ merged.push({ item: rated[ratedIdx++], is_discovery: false });
2356
+ } else if (discoveryIdx < discovery.length) {
2357
+ merged.push({ item: discovery[discoveryIdx++], is_discovery: true });
2358
+ }
2359
+ }
2360
+ }
2361
+ const start = Math.min(offset, merged.length);
2362
+ const end = limit !== void 0 ? Math.min(start + limit, merged.length) : merged.length;
2363
+ return merged.slice(start, end);
2364
+ }
2365
+
2334
2366
  // node_modules/@prmichaelsen/remember-core/dist/database/weaviate/client.js
2335
2367
  import weaviate2 from "weaviate-client";
2336
2368
  var ALL_MEMORY_PROPERTIES = [
@@ -2711,6 +2743,72 @@ var MemoryService = class {
2711
2743
  limit
2712
2744
  };
2713
2745
  }
2746
+ // ── By Discovery (interleaved rated + unrated) ────────────────────
2747
+ async byDiscovery(input) {
2748
+ const limit = input.limit ?? 50;
2749
+ const offset = input.offset ?? 0;
2750
+ const memoryFilters = buildMemoryOnlyFilters(this.collection, input.filters);
2751
+ const ghostFilters = [];
2752
+ if (input.ghost_context) {
2753
+ ghostFilters.push(buildTrustFilter(this.collection, input.ghost_context.accessor_trust_level));
2754
+ }
2755
+ if (!input.ghost_context?.include_ghost_content) {
2756
+ ghostFilters.push(this.collection.filter.byProperty("content_type").notEqual("ghost"));
2757
+ }
2758
+ const buildBaseFilters = (useDeletedFilter) => {
2759
+ const deletedFilter = useDeletedFilter ? buildDeletedFilter(this.collection, input.deleted_filter || "exclude") : null;
2760
+ return [deletedFilter, memoryFilters, ...ghostFilters].filter((f) => f !== null);
2761
+ };
2762
+ const fetchLimit = (limit + offset) * 2;
2763
+ const executeRated = async (useDeletedFilter) => {
2764
+ const base = buildBaseFilters(useDeletedFilter);
2765
+ base.push(this.collection.filter.byProperty("rating_count").greaterOrEqual(RATING_MIN_THRESHOLD));
2766
+ const combinedFilters = combineFiltersWithAnd(base);
2767
+ const queryOptions = {
2768
+ limit: fetchLimit,
2769
+ sort: this.collection.sort.byProperty("rating_bayesian", false)
2770
+ };
2771
+ if (combinedFilters)
2772
+ queryOptions.filters = combinedFilters;
2773
+ return this.collection.query.fetchObjects(queryOptions);
2774
+ };
2775
+ const executeDiscovery = async (useDeletedFilter) => {
2776
+ const base = buildBaseFilters(useDeletedFilter);
2777
+ base.push(this.collection.filter.byProperty("rating_count").lessThan(RATING_MIN_THRESHOLD));
2778
+ const combinedFilters = combineFiltersWithAnd(base);
2779
+ const queryOptions = {
2780
+ limit: fetchLimit,
2781
+ sort: this.collection.sort.byProperty("created_at", false)
2782
+ };
2783
+ if (combinedFilters)
2784
+ queryOptions.filters = combinedFilters;
2785
+ return this.collection.query.fetchObjects(queryOptions);
2786
+ };
2787
+ const [ratedResults, discoveryResults] = await Promise.all([
2788
+ this.retryWithoutDeletedFilter(executeRated),
2789
+ this.retryWithoutDeletedFilter(executeDiscovery)
2790
+ ]);
2791
+ const toDoc = (obj) => normalizeDoc({ id: obj.uuid, ...obj.properties });
2792
+ const ratedDocs = ratedResults.objects.map(toDoc).filter((d) => d.doc_type === "memory");
2793
+ const discoveryDocs = discoveryResults.objects.map(toDoc).filter((d) => d.doc_type === "memory");
2794
+ const interleaved = interleaveDiscovery({
2795
+ rated: ratedDocs,
2796
+ discovery: discoveryDocs,
2797
+ ratio: DISCOVERY_RATIO,
2798
+ offset,
2799
+ limit
2800
+ });
2801
+ const memories = interleaved.map((item) => {
2802
+ const doc = item.item;
2803
+ return Object.assign({}, doc, { is_discovery: item.is_discovery });
2804
+ });
2805
+ return {
2806
+ memories,
2807
+ total: memories.length,
2808
+ offset,
2809
+ limit
2810
+ };
2811
+ }
2714
2812
  // ── Find Similar (vector) ──────────────────────────────────────────
2715
2813
  async findSimilar(input) {
2716
2814
  if (!input.memory_id && !input.text)
@@ -3908,6 +4006,135 @@ var SpaceService = class {
3908
4006
  results
3909
4007
  };
3910
4008
  }
4009
+ // ── By Discovery (interleaved rated + unrated for spaces/groups) ────
4010
+ async byDiscovery(input, authContext) {
4011
+ const spaces = input.spaces || [];
4012
+ const groups = input.groups || [];
4013
+ const limit = input.limit ?? 10;
4014
+ const offset = input.offset ?? 0;
4015
+ if (spaces.length > 0) {
4016
+ const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
4017
+ if (invalidSpaces.length > 0) {
4018
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
4019
+ }
4020
+ }
4021
+ if (groups.length > 0) {
4022
+ const invalidGroups = groups.filter((g) => !g || g.includes(".") || g.trim() === "");
4023
+ if (invalidGroups.length > 0) {
4024
+ throw new ValidationError("Group IDs cannot be empty or contain dots");
4025
+ }
4026
+ }
4027
+ const moderationFilter = input.moderation_filter || "approved";
4028
+ if (moderationFilter !== "approved") {
4029
+ for (const groupId of groups) {
4030
+ if (!canModerate(authContext, groupId)) {
4031
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
4032
+ }
4033
+ }
4034
+ if ((spaces.length > 0 || groups.length === 0) && !canModerateAny(authContext)) {
4035
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in spaces`);
4036
+ }
4037
+ }
4038
+ const fetchLimit = (limit + offset) * 2;
4039
+ const fetchPool = async (collection, baseFilters, ratingFilter, sortProp) => {
4040
+ const allFilters = [...baseFilters, ratingFilter];
4041
+ const combined = allFilters.length > 0 ? Filters4.and(...allFilters) : void 0;
4042
+ const queryOptions = {
4043
+ limit: fetchLimit,
4044
+ sort: collection.sort.byProperty(sortProp, false)
4045
+ };
4046
+ if (combined)
4047
+ queryOptions.filters = combined;
4048
+ return (await collection.query.fetchObjects(queryOptions)).objects;
4049
+ };
4050
+ const searchInput = {
4051
+ query: "",
4052
+ // not used for filter building
4053
+ spaces: input.spaces,
4054
+ groups: input.groups,
4055
+ content_type: input.content_type,
4056
+ tags: input.tags,
4057
+ min_weight: input.min_weight,
4058
+ max_weight: input.max_weight,
4059
+ date_from: input.date_from,
4060
+ date_to: input.date_to,
4061
+ moderation_filter: input.moderation_filter,
4062
+ include_comments: input.include_comments
4063
+ };
4064
+ const allRated = [];
4065
+ const allDiscovery = [];
4066
+ if (spaces.length > 0 || groups.length === 0) {
4067
+ await ensurePublicCollection(this.weaviateClient);
4068
+ const spacesCollectionName = getCollectionName(CollectionType.SPACES);
4069
+ const spacesCollection = this.weaviateClient.collections.get(spacesCollectionName);
4070
+ const baseFilters = this.buildBaseFilters(spacesCollection, searchInput);
4071
+ if (spaces.length > 0) {
4072
+ baseFilters.push(spacesCollection.filter.byProperty("space_ids").containsAny(spaces));
4073
+ }
4074
+ const ratedFilter = spacesCollection.filter.byProperty("rating_count").greaterOrEqual(DISCOVERY_THRESHOLD);
4075
+ const discoveryFilter = spacesCollection.filter.byProperty("rating_count").lessThan(DISCOVERY_THRESHOLD);
4076
+ const [rated, discovery] = await Promise.all([
4077
+ fetchPool(spacesCollection, baseFilters, ratedFilter, "rating_bayesian"),
4078
+ fetchPool(spacesCollection, baseFilters, discoveryFilter, "created_at")
4079
+ ]);
4080
+ allRated.push(...tagWithSource(rated, spacesCollectionName));
4081
+ allDiscovery.push(...tagWithSource(discovery, spacesCollectionName));
4082
+ }
4083
+ for (const groupId of groups) {
4084
+ const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
4085
+ const exists = await this.weaviateClient.collections.exists(groupCollectionName);
4086
+ if (!exists)
4087
+ continue;
4088
+ const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
4089
+ const baseFilters = this.buildBaseFilters(groupCollection, searchInput);
4090
+ const ratedFilter = groupCollection.filter.byProperty("rating_count").greaterOrEqual(DISCOVERY_THRESHOLD);
4091
+ const discoveryFilter = groupCollection.filter.byProperty("rating_count").lessThan(DISCOVERY_THRESHOLD);
4092
+ const [rated, discovery] = await Promise.all([
4093
+ fetchPool(groupCollection, baseFilters, ratedFilter, "rating_bayesian"),
4094
+ fetchPool(groupCollection, baseFilters, discoveryFilter, "created_at")
4095
+ ]);
4096
+ allRated.push(...tagWithSource(rated, groupCollectionName));
4097
+ allDiscovery.push(...tagWithSource(discovery, groupCollectionName));
4098
+ }
4099
+ const dedupePool = (pool) => {
4100
+ const seen = /* @__PURE__ */ new Set();
4101
+ return pool.filter((obj) => {
4102
+ if (seen.has(obj.uuid))
4103
+ return false;
4104
+ seen.add(obj.uuid);
4105
+ return true;
4106
+ });
4107
+ };
4108
+ const ratedDeduped = dedupeBySourceId(dedupePool(allRated), input.dedupe);
4109
+ const discoveryDeduped = dedupeBySourceId(dedupePool(allDiscovery), input.dedupe);
4110
+ const toDoc = (obj) => ({
4111
+ id: obj.uuid,
4112
+ ...obj.properties,
4113
+ ...obj._also_in?.length ? { also_in: obj._also_in } : {}
4114
+ });
4115
+ const ratedDocs = ratedDeduped.map(toDoc);
4116
+ const discoveryDocs = discoveryDeduped.map(toDoc);
4117
+ const interleaved = interleaveDiscovery({
4118
+ rated: ratedDocs,
4119
+ discovery: discoveryDocs,
4120
+ ratio: DISCOVERY_RATIO,
4121
+ offset,
4122
+ limit
4123
+ });
4124
+ const memories = interleaved.map((item) => {
4125
+ const doc = item.item;
4126
+ return Object.assign({}, doc, { is_discovery: item.is_discovery });
4127
+ });
4128
+ const isAllPublic = spaces.length === 0 && groups.length === 0;
4129
+ return {
4130
+ spaces_searched: isAllPublic ? "all_public" : spaces,
4131
+ groups_searched: groups,
4132
+ memories,
4133
+ total: memories.length,
4134
+ offset,
4135
+ limit
4136
+ };
4137
+ }
3911
4138
  // ── Private: Build Base Filters ─────────────────────────────────────
3912
4139
  buildBaseFilters(collection, input) {
3913
4140
  const filterList = [];
@@ -3945,6 +4172,10 @@ var SpaceService = class {
3945
4172
  const opts = { limit };
3946
4173
  if (filters)
3947
4174
  opts.filters = filters;
4175
+ const isWildcard = !query.trim() || query === "*";
4176
+ if (isWildcard) {
4177
+ return (await collection.query.bm25("*", opts)).objects;
4178
+ }
3948
4179
  switch (searchType) {
3949
4180
  case "bm25":
3950
4181
  return (await collection.query.bm25(query, opts)).objects;
@@ -4047,10 +4278,13 @@ Content to evaluate:
4047
4278
  ${content}
4048
4279
  ---
4049
4280
 
4050
- Respond with ONLY valid JSON:
4051
- {"pass":true}
4052
- OR
4053
- {"pass":false,"reason":"<specific, human-friendly explanation of why this was blocked>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}`;
4281
+ \u{1F6A8} CRITICAL \u{1F6A8}:
4282
+ Respond with ONLY a single JSON object on one line \u2014 no markdown, no explanation, no code fences.
4283
+
4284
+ If the content passes: {"pass":true,"reason":"<brief reason>"}
4285
+ If the content fails: {"pass":false,"reason":"<specific explanation>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}
4286
+
4287
+ Your entire response must be parseable by JSON.parse(). Do not include any other text.`;
4054
4288
  }
4055
4289
  var DEFAULT_CACHE_MAX = 1e3;
4056
4290
  function hashContent(content) {
@@ -4077,18 +4311,21 @@ function createModerationClient(options) {
4077
4311
  body: JSON.stringify({
4078
4312
  model,
4079
4313
  max_tokens: 256,
4080
- messages: [{ role: "user", content: buildModerationPrompt(content) }]
4314
+ temperature: 0,
4315
+ messages: [
4316
+ { role: "user", content: buildModerationPrompt(content) },
4317
+ { role: "assistant", content: "{" }
4318
+ ]
4081
4319
  })
4082
4320
  });
4083
4321
  if (!response.ok) {
4084
4322
  const errorBody = await response.text().catch(() => "");
4085
- const msg = `[moderation] Anthropic API error: ${response.status} ${response.statusText} ${errorBody}`;
4086
- console.error(msg);
4087
- return { pass: false, reason: msg };
4323
+ console.error(`[moderation] Anthropic API error: ${response.status} ${response.statusText}`, errorBody);
4324
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4088
4325
  }
4089
4326
  const data = await response.json();
4090
4327
  const rawText = data.content?.[0]?.text ?? "";
4091
- const text = rawText.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
4328
+ const text = ("{" + rawText).replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
4092
4329
  const parsed = JSON.parse(text);
4093
4330
  const result = {
4094
4331
  pass: parsed.pass === true,
@@ -4102,9 +4339,8 @@ function createModerationClient(options) {
4102
4339
  cache.set(hash, result);
4103
4340
  return result;
4104
4341
  } catch (err2) {
4105
- const msg = `[moderation] Unexpected error: ${err2 instanceof Error ? err2.message : String(err2)}`;
4106
- console.error(msg, err2);
4107
- return { pass: false, reason: msg };
4342
+ console.error("[moderation] Unexpected error:", err2);
4343
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4108
4344
  }
4109
4345
  }
4110
4346
  };
package/dist/server.js CHANGED
@@ -2335,6 +2335,38 @@ async function getSpaceConfig(id, type, logger2) {
2335
2335
  // node_modules/@prmichaelsen/remember-core/dist/services/memory.service.js
2336
2336
  import { Filters as Filters2 } from "weaviate-client";
2337
2337
 
2338
+ // node_modules/@prmichaelsen/remember-core/dist/services/discovery.js
2339
+ var DISCOVERY_RATIO = 4;
2340
+ var DISCOVERY_THRESHOLD = 5;
2341
+ function interleaveDiscovery(options) {
2342
+ const { rated, discovery, ratio = DISCOVERY_RATIO, offset = 0, limit } = options;
2343
+ const totalAvailable = rated.length + discovery.length;
2344
+ if (totalAvailable === 0)
2345
+ return [];
2346
+ const merged = [];
2347
+ let ratedIdx = 0;
2348
+ let discoveryIdx = 0;
2349
+ for (let pos = 0; pos < totalAvailable; pos++) {
2350
+ const isDiscoverySlot = (pos + 1) % (ratio + 1) === 0;
2351
+ if (isDiscoverySlot) {
2352
+ if (discoveryIdx < discovery.length) {
2353
+ merged.push({ item: discovery[discoveryIdx++], is_discovery: true });
2354
+ } else if (ratedIdx < rated.length) {
2355
+ merged.push({ item: rated[ratedIdx++], is_discovery: false });
2356
+ }
2357
+ } else {
2358
+ if (ratedIdx < rated.length) {
2359
+ merged.push({ item: rated[ratedIdx++], is_discovery: false });
2360
+ } else if (discoveryIdx < discovery.length) {
2361
+ merged.push({ item: discovery[discoveryIdx++], is_discovery: true });
2362
+ }
2363
+ }
2364
+ }
2365
+ const start = Math.min(offset, merged.length);
2366
+ const end = limit !== void 0 ? Math.min(start + limit, merged.length) : merged.length;
2367
+ return merged.slice(start, end);
2368
+ }
2369
+
2338
2370
  // node_modules/@prmichaelsen/remember-core/dist/database/weaviate/client.js
2339
2371
  import weaviate2 from "weaviate-client";
2340
2372
  var ALL_MEMORY_PROPERTIES = [
@@ -2715,6 +2747,72 @@ var MemoryService = class {
2715
2747
  limit
2716
2748
  };
2717
2749
  }
2750
+ // ── By Discovery (interleaved rated + unrated) ────────────────────
2751
+ async byDiscovery(input) {
2752
+ const limit = input.limit ?? 50;
2753
+ const offset = input.offset ?? 0;
2754
+ const memoryFilters = buildMemoryOnlyFilters(this.collection, input.filters);
2755
+ const ghostFilters = [];
2756
+ if (input.ghost_context) {
2757
+ ghostFilters.push(buildTrustFilter(this.collection, input.ghost_context.accessor_trust_level));
2758
+ }
2759
+ if (!input.ghost_context?.include_ghost_content) {
2760
+ ghostFilters.push(this.collection.filter.byProperty("content_type").notEqual("ghost"));
2761
+ }
2762
+ const buildBaseFilters = (useDeletedFilter) => {
2763
+ const deletedFilter = useDeletedFilter ? buildDeletedFilter(this.collection, input.deleted_filter || "exclude") : null;
2764
+ return [deletedFilter, memoryFilters, ...ghostFilters].filter((f) => f !== null);
2765
+ };
2766
+ const fetchLimit = (limit + offset) * 2;
2767
+ const executeRated = async (useDeletedFilter) => {
2768
+ const base = buildBaseFilters(useDeletedFilter);
2769
+ base.push(this.collection.filter.byProperty("rating_count").greaterOrEqual(RATING_MIN_THRESHOLD));
2770
+ const combinedFilters = combineFiltersWithAnd(base);
2771
+ const queryOptions = {
2772
+ limit: fetchLimit,
2773
+ sort: this.collection.sort.byProperty("rating_bayesian", false)
2774
+ };
2775
+ if (combinedFilters)
2776
+ queryOptions.filters = combinedFilters;
2777
+ return this.collection.query.fetchObjects(queryOptions);
2778
+ };
2779
+ const executeDiscovery = async (useDeletedFilter) => {
2780
+ const base = buildBaseFilters(useDeletedFilter);
2781
+ base.push(this.collection.filter.byProperty("rating_count").lessThan(RATING_MIN_THRESHOLD));
2782
+ const combinedFilters = combineFiltersWithAnd(base);
2783
+ const queryOptions = {
2784
+ limit: fetchLimit,
2785
+ sort: this.collection.sort.byProperty("created_at", false)
2786
+ };
2787
+ if (combinedFilters)
2788
+ queryOptions.filters = combinedFilters;
2789
+ return this.collection.query.fetchObjects(queryOptions);
2790
+ };
2791
+ const [ratedResults, discoveryResults] = await Promise.all([
2792
+ this.retryWithoutDeletedFilter(executeRated),
2793
+ this.retryWithoutDeletedFilter(executeDiscovery)
2794
+ ]);
2795
+ const toDoc = (obj) => normalizeDoc({ id: obj.uuid, ...obj.properties });
2796
+ const ratedDocs = ratedResults.objects.map(toDoc).filter((d) => d.doc_type === "memory");
2797
+ const discoveryDocs = discoveryResults.objects.map(toDoc).filter((d) => d.doc_type === "memory");
2798
+ const interleaved = interleaveDiscovery({
2799
+ rated: ratedDocs,
2800
+ discovery: discoveryDocs,
2801
+ ratio: DISCOVERY_RATIO,
2802
+ offset,
2803
+ limit
2804
+ });
2805
+ const memories = interleaved.map((item) => {
2806
+ const doc = item.item;
2807
+ return Object.assign({}, doc, { is_discovery: item.is_discovery });
2808
+ });
2809
+ return {
2810
+ memories,
2811
+ total: memories.length,
2812
+ offset,
2813
+ limit
2814
+ };
2815
+ }
2718
2816
  // ── Find Similar (vector) ──────────────────────────────────────────
2719
2817
  async findSimilar(input) {
2720
2818
  if (!input.memory_id && !input.text)
@@ -3912,6 +4010,135 @@ var SpaceService = class {
3912
4010
  results
3913
4011
  };
3914
4012
  }
4013
+ // ── By Discovery (interleaved rated + unrated for spaces/groups) ────
4014
+ async byDiscovery(input, authContext) {
4015
+ const spaces = input.spaces || [];
4016
+ const groups = input.groups || [];
4017
+ const limit = input.limit ?? 10;
4018
+ const offset = input.offset ?? 0;
4019
+ if (spaces.length > 0) {
4020
+ const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
4021
+ if (invalidSpaces.length > 0) {
4022
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
4023
+ }
4024
+ }
4025
+ if (groups.length > 0) {
4026
+ const invalidGroups = groups.filter((g) => !g || g.includes(".") || g.trim() === "");
4027
+ if (invalidGroups.length > 0) {
4028
+ throw new ValidationError("Group IDs cannot be empty or contain dots");
4029
+ }
4030
+ }
4031
+ const moderationFilter = input.moderation_filter || "approved";
4032
+ if (moderationFilter !== "approved") {
4033
+ for (const groupId of groups) {
4034
+ if (!canModerate(authContext, groupId)) {
4035
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
4036
+ }
4037
+ }
4038
+ if ((spaces.length > 0 || groups.length === 0) && !canModerateAny(authContext)) {
4039
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in spaces`);
4040
+ }
4041
+ }
4042
+ const fetchLimit = (limit + offset) * 2;
4043
+ const fetchPool = async (collection, baseFilters, ratingFilter, sortProp) => {
4044
+ const allFilters = [...baseFilters, ratingFilter];
4045
+ const combined = allFilters.length > 0 ? Filters4.and(...allFilters) : void 0;
4046
+ const queryOptions = {
4047
+ limit: fetchLimit,
4048
+ sort: collection.sort.byProperty(sortProp, false)
4049
+ };
4050
+ if (combined)
4051
+ queryOptions.filters = combined;
4052
+ return (await collection.query.fetchObjects(queryOptions)).objects;
4053
+ };
4054
+ const searchInput = {
4055
+ query: "",
4056
+ // not used for filter building
4057
+ spaces: input.spaces,
4058
+ groups: input.groups,
4059
+ content_type: input.content_type,
4060
+ tags: input.tags,
4061
+ min_weight: input.min_weight,
4062
+ max_weight: input.max_weight,
4063
+ date_from: input.date_from,
4064
+ date_to: input.date_to,
4065
+ moderation_filter: input.moderation_filter,
4066
+ include_comments: input.include_comments
4067
+ };
4068
+ const allRated = [];
4069
+ const allDiscovery = [];
4070
+ if (spaces.length > 0 || groups.length === 0) {
4071
+ await ensurePublicCollection(this.weaviateClient);
4072
+ const spacesCollectionName = getCollectionName(CollectionType.SPACES);
4073
+ const spacesCollection = this.weaviateClient.collections.get(spacesCollectionName);
4074
+ const baseFilters = this.buildBaseFilters(spacesCollection, searchInput);
4075
+ if (spaces.length > 0) {
4076
+ baseFilters.push(spacesCollection.filter.byProperty("space_ids").containsAny(spaces));
4077
+ }
4078
+ const ratedFilter = spacesCollection.filter.byProperty("rating_count").greaterOrEqual(DISCOVERY_THRESHOLD);
4079
+ const discoveryFilter = spacesCollection.filter.byProperty("rating_count").lessThan(DISCOVERY_THRESHOLD);
4080
+ const [rated, discovery] = await Promise.all([
4081
+ fetchPool(spacesCollection, baseFilters, ratedFilter, "rating_bayesian"),
4082
+ fetchPool(spacesCollection, baseFilters, discoveryFilter, "created_at")
4083
+ ]);
4084
+ allRated.push(...tagWithSource(rated, spacesCollectionName));
4085
+ allDiscovery.push(...tagWithSource(discovery, spacesCollectionName));
4086
+ }
4087
+ for (const groupId of groups) {
4088
+ const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
4089
+ const exists = await this.weaviateClient.collections.exists(groupCollectionName);
4090
+ if (!exists)
4091
+ continue;
4092
+ const groupCollection = this.weaviateClient.collections.get(groupCollectionName);
4093
+ const baseFilters = this.buildBaseFilters(groupCollection, searchInput);
4094
+ const ratedFilter = groupCollection.filter.byProperty("rating_count").greaterOrEqual(DISCOVERY_THRESHOLD);
4095
+ const discoveryFilter = groupCollection.filter.byProperty("rating_count").lessThan(DISCOVERY_THRESHOLD);
4096
+ const [rated, discovery] = await Promise.all([
4097
+ fetchPool(groupCollection, baseFilters, ratedFilter, "rating_bayesian"),
4098
+ fetchPool(groupCollection, baseFilters, discoveryFilter, "created_at")
4099
+ ]);
4100
+ allRated.push(...tagWithSource(rated, groupCollectionName));
4101
+ allDiscovery.push(...tagWithSource(discovery, groupCollectionName));
4102
+ }
4103
+ const dedupePool = (pool) => {
4104
+ const seen = /* @__PURE__ */ new Set();
4105
+ return pool.filter((obj) => {
4106
+ if (seen.has(obj.uuid))
4107
+ return false;
4108
+ seen.add(obj.uuid);
4109
+ return true;
4110
+ });
4111
+ };
4112
+ const ratedDeduped = dedupeBySourceId(dedupePool(allRated), input.dedupe);
4113
+ const discoveryDeduped = dedupeBySourceId(dedupePool(allDiscovery), input.dedupe);
4114
+ const toDoc = (obj) => ({
4115
+ id: obj.uuid,
4116
+ ...obj.properties,
4117
+ ...obj._also_in?.length ? { also_in: obj._also_in } : {}
4118
+ });
4119
+ const ratedDocs = ratedDeduped.map(toDoc);
4120
+ const discoveryDocs = discoveryDeduped.map(toDoc);
4121
+ const interleaved = interleaveDiscovery({
4122
+ rated: ratedDocs,
4123
+ discovery: discoveryDocs,
4124
+ ratio: DISCOVERY_RATIO,
4125
+ offset,
4126
+ limit
4127
+ });
4128
+ const memories = interleaved.map((item) => {
4129
+ const doc = item.item;
4130
+ return Object.assign({}, doc, { is_discovery: item.is_discovery });
4131
+ });
4132
+ const isAllPublic = spaces.length === 0 && groups.length === 0;
4133
+ return {
4134
+ spaces_searched: isAllPublic ? "all_public" : spaces,
4135
+ groups_searched: groups,
4136
+ memories,
4137
+ total: memories.length,
4138
+ offset,
4139
+ limit
4140
+ };
4141
+ }
3915
4142
  // ── Private: Build Base Filters ─────────────────────────────────────
3916
4143
  buildBaseFilters(collection, input) {
3917
4144
  const filterList = [];
@@ -3949,6 +4176,10 @@ var SpaceService = class {
3949
4176
  const opts = { limit };
3950
4177
  if (filters)
3951
4178
  opts.filters = filters;
4179
+ const isWildcard = !query.trim() || query === "*";
4180
+ if (isWildcard) {
4181
+ return (await collection.query.bm25("*", opts)).objects;
4182
+ }
3952
4183
  switch (searchType) {
3953
4184
  case "bm25":
3954
4185
  return (await collection.query.bm25(query, opts)).objects;
@@ -4051,10 +4282,13 @@ Content to evaluate:
4051
4282
  ${content}
4052
4283
  ---
4053
4284
 
4054
- Respond with ONLY valid JSON:
4055
- {"pass":true}
4056
- OR
4057
- {"pass":false,"reason":"<specific, human-friendly explanation of why this was blocked>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}`;
4285
+ \u{1F6A8} CRITICAL \u{1F6A8}:
4286
+ Respond with ONLY a single JSON object on one line \u2014 no markdown, no explanation, no code fences.
4287
+
4288
+ If the content passes: {"pass":true,"reason":"<brief reason>"}
4289
+ If the content fails: {"pass":false,"reason":"<specific explanation>","category":"<hate_speech|extremism|violence_incitement|csam|self_harm_encouragement>"}
4290
+
4291
+ Your entire response must be parseable by JSON.parse(). Do not include any other text.`;
4058
4292
  }
4059
4293
  var DEFAULT_CACHE_MAX = 1e3;
4060
4294
  function hashContent(content) {
@@ -4081,18 +4315,21 @@ function createModerationClient(options) {
4081
4315
  body: JSON.stringify({
4082
4316
  model,
4083
4317
  max_tokens: 256,
4084
- messages: [{ role: "user", content: buildModerationPrompt(content) }]
4318
+ temperature: 0,
4319
+ messages: [
4320
+ { role: "user", content: buildModerationPrompt(content) },
4321
+ { role: "assistant", content: "{" }
4322
+ ]
4085
4323
  })
4086
4324
  });
4087
4325
  if (!response.ok) {
4088
4326
  const errorBody = await response.text().catch(() => "");
4089
- const msg = `[moderation] Anthropic API error: ${response.status} ${response.statusText} ${errorBody}`;
4090
- console.error(msg);
4091
- return { pass: false, reason: msg };
4327
+ console.error(`[moderation] Anthropic API error: ${response.status} ${response.statusText}`, errorBody);
4328
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4092
4329
  }
4093
4330
  const data = await response.json();
4094
4331
  const rawText = data.content?.[0]?.text ?? "";
4095
- const text = rawText.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
4332
+ const text = ("{" + rawText).replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
4096
4333
  const parsed = JSON.parse(text);
4097
4334
  const result = {
4098
4335
  pass: parsed.pass === true,
@@ -4106,9 +4343,8 @@ function createModerationClient(options) {
4106
4343
  cache.set(hash, result);
4107
4344
  return result;
4108
4345
  } catch (err2) {
4109
- const msg = `[moderation] Unexpected error: ${err2 instanceof Error ? err2.message : String(err2)}`;
4110
- console.error(msg, err2);
4111
- return { pass: false, reason: msg };
4346
+ console.error("[moderation] Unexpected error:", err2);
4347
+ return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4112
4348
  }
4113
4349
  }
4114
4350
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/remember-mcp",
3
- "version": "3.15.3",
3
+ "version": "3.15.4",
4
4
  "description": "Multi-tenant memory system MCP server with vector search and relationships",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",
@@ -52,7 +52,7 @@
52
52
  "@modelcontextprotocol/sdk": "^1.0.4",
53
53
  "@prmichaelsen/firebase-admin-sdk-v8": "^2.2.0",
54
54
  "@prmichaelsen/mcp-auth": "^7.0.4",
55
- "@prmichaelsen/remember-core": "^0.34.14",
55
+ "@prmichaelsen/remember-core": "^0.35.2",
56
56
  "dotenv": "^16.4.5",
57
57
  "uuid": "^13.0.0",
58
58
  "weaviate-client": "^3.2.0"