@prmichaelsen/remember-mcp 3.15.2 → 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.
@@ -1397,6 +1397,24 @@ var ValidationError = class extends AppError {
1397
1397
  this.fields = fields;
1398
1398
  }
1399
1399
  };
1400
+ var NotFoundError = class extends AppError {
1401
+ resource;
1402
+ id;
1403
+ kind = "not_found";
1404
+ constructor(resource, id) {
1405
+ super(`${resource} not found: ${id}`, { resource, id });
1406
+ this.resource = resource;
1407
+ this.id = id;
1408
+ }
1409
+ };
1410
+ var ForbiddenError = class extends AppError {
1411
+ requiredRole;
1412
+ kind = "forbidden";
1413
+ constructor(message = "Access denied", requiredRole) {
1414
+ super(message, { requiredRole });
1415
+ this.requiredRole = requiredRole;
1416
+ }
1417
+ };
1400
1418
 
1401
1419
  // node_modules/@prmichaelsen/remember-core/node_modules/uuid/dist/esm/regex.js
1402
1420
  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;
@@ -2313,6 +2331,38 @@ async function getSpaceConfig(id, type, logger2) {
2313
2331
  // node_modules/@prmichaelsen/remember-core/dist/services/memory.service.js
2314
2332
  import { Filters as Filters2 } from "weaviate-client";
2315
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
+
2316
2366
  // node_modules/@prmichaelsen/remember-core/dist/database/weaviate/client.js
2317
2367
  import weaviate2 from "weaviate-client";
2318
2368
  var ALL_MEMORY_PROPERTIES = [
@@ -2693,6 +2743,72 @@ var MemoryService = class {
2693
2743
  limit
2694
2744
  };
2695
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
+ }
2696
2812
  // ── Find Similar (vector) ──────────────────────────────────────────
2697
2813
  async findSimilar(input) {
2698
2814
  if (!input.memory_id && !input.text)
@@ -3272,32 +3388,32 @@ var SpaceService = class {
3272
3388
  const spaces = input.spaces || [];
3273
3389
  const groups = input.groups || [];
3274
3390
  if (spaces.length === 0 && groups.length === 0) {
3275
- throw new Error("Must specify at least one space or group to publish to");
3391
+ throw new ValidationError("Must specify at least one space or group to publish to");
3276
3392
  }
3277
3393
  if (spaces.length > 0) {
3278
3394
  const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
3279
3395
  if (invalidSpaces.length > 0) {
3280
- throw new Error(`Invalid space IDs: ${invalidSpaces.join(", ")}`);
3396
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
3281
3397
  }
3282
3398
  }
3283
3399
  if (groups.length > 0) {
3284
3400
  const invalidGroups = groups.filter((g) => !g || g.includes(".") || g.trim() === "");
3285
3401
  if (invalidGroups.length > 0) {
3286
- throw new Error("Group IDs cannot be empty or contain dots");
3402
+ throw new ValidationError("Group IDs cannot be empty or contain dots");
3287
3403
  }
3288
3404
  }
3289
3405
  const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
3290
3406
  if (!memory)
3291
- throw new Error(`Memory not found: ${input.memory_id}`);
3407
+ throw new NotFoundError("Memory", input.memory_id);
3292
3408
  if (memory.properties.user_id !== this.userId)
3293
- throw new Error("Permission denied: not memory owner");
3409
+ throw new ForbiddenError("Permission denied: not memory owner");
3294
3410
  if (memory.properties.doc_type !== "memory")
3295
- throw new Error("Only memories can be published");
3411
+ throw new ValidationError("Only memories can be published");
3296
3412
  const memoryContentType = memory.properties.content_type;
3297
3413
  for (const spaceId of spaces) {
3298
3414
  const requiredType = SPACE_CONTENT_TYPE_RESTRICTIONS[spaceId];
3299
3415
  if (requiredType && memoryContentType !== requiredType) {
3300
- throw new Error(`Space '${spaceId}' only accepts content_type '${requiredType}', got '${memoryContentType ?? "undefined"}'`);
3416
+ throw new ValidationError(`Space '${spaceId}' only accepts content_type '${requiredType}', got '${memoryContentType ?? "undefined"}'`);
3301
3417
  }
3302
3418
  }
3303
3419
  await this.checkModeration(memory.properties.content);
@@ -3320,25 +3436,25 @@ var SpaceService = class {
3320
3436
  const spaces = input.spaces || [];
3321
3437
  const groups = input.groups || [];
3322
3438
  if (spaces.length === 0 && groups.length === 0) {
3323
- throw new Error("Must specify at least one space or group to retract from");
3439
+ throw new ValidationError("Must specify at least one space or group to retract from");
3324
3440
  }
3325
3441
  if (groups.length > 0) {
3326
3442
  const invalidGroups = groups.filter((g) => g.includes("."));
3327
3443
  if (invalidGroups.length > 0) {
3328
- throw new Error(`Group IDs cannot contain dots: ${invalidGroups.join(", ")}`);
3444
+ throw new ValidationError(`Group IDs cannot contain dots: ${invalidGroups.join(", ")}`);
3329
3445
  }
3330
3446
  }
3331
3447
  const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
3332
3448
  if (!memory)
3333
- throw new Error(`Memory not found: ${input.memory_id}`);
3449
+ throw new NotFoundError("Memory", input.memory_id);
3334
3450
  if (memory.properties.user_id !== this.userId)
3335
- throw new Error("Permission denied: not memory owner");
3451
+ throw new ForbiddenError("Permission denied: not memory owner");
3336
3452
  const currentSpaceIds = Array.isArray(memory.properties.space_ids) ? memory.properties.space_ids : [];
3337
3453
  const currentGroupIds = Array.isArray(memory.properties.group_ids) ? memory.properties.group_ids : [];
3338
3454
  const notPublishedSpaces = spaces.filter((s) => !currentSpaceIds.includes(s));
3339
3455
  const notPublishedGroups = groups.filter((g) => !currentGroupIds.includes(g));
3340
3456
  if (notPublishedSpaces.length > 0 || notPublishedGroups.length > 0) {
3341
- throw new Error(`Memory is not published to some destinations. Not in spaces: [${notPublishedSpaces.join(", ")}], Not in groups: [${notPublishedGroups.join(", ")}]`);
3457
+ throw new ValidationError(`Memory is not published to some destinations. Not in spaces: [${notPublishedSpaces.join(", ")}], Not in groups: [${notPublishedGroups.join(", ")}]`);
3342
3458
  }
3343
3459
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "retract_memory", {
3344
3460
  memory_id: input.memory_id,
@@ -3359,13 +3475,13 @@ var SpaceService = class {
3359
3475
  async revise(input) {
3360
3476
  const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
3361
3477
  if (!memory)
3362
- throw new Error(`Memory not found: ${input.memory_id}`);
3478
+ throw new NotFoundError("Memory", input.memory_id);
3363
3479
  if (memory.properties.user_id !== this.userId)
3364
- throw new Error("Permission denied: not memory owner");
3480
+ throw new ForbiddenError("Permission denied: not memory owner");
3365
3481
  const spaceIds = Array.isArray(memory.properties.space_ids) ? memory.properties.space_ids : [];
3366
3482
  const groupIds = Array.isArray(memory.properties.group_ids) ? memory.properties.group_ids : [];
3367
3483
  if (spaceIds.length === 0 && groupIds.length === 0) {
3368
- throw new Error("Memory has no published copies to revise. Publish first with publish().");
3484
+ throw new ValidationError("Memory has no published copies to revise. Publish first with publish().");
3369
3485
  }
3370
3486
  await this.checkModeration(memory.properties.content);
3371
3487
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "revise_memory", {
@@ -3385,7 +3501,7 @@ var SpaceService = class {
3385
3501
  async confirm(input) {
3386
3502
  const request = await this.confirmationTokenService.confirmRequest(this.userId, input.token);
3387
3503
  if (!request) {
3388
- throw new Error("Invalid or expired confirmation token");
3504
+ throw new ValidationError("Invalid or expired confirmation token");
3389
3505
  }
3390
3506
  if (request.action === "publish_memory") {
3391
3507
  return this.executePublish(request);
@@ -3396,31 +3512,31 @@ var SpaceService = class {
3396
3512
  if (request.action === "revise_memory") {
3397
3513
  return this.executeRevise(request);
3398
3514
  }
3399
- throw new Error(`Unknown action type: ${request.action}`);
3515
+ throw new ValidationError(`Unknown action type: ${request.action}`);
3400
3516
  }
3401
3517
  // ── Deny ────────────────────────────────────────────────────────────
3402
3518
  async deny(input) {
3403
3519
  const success = await this.confirmationTokenService.denyRequest(this.userId, input.token);
3404
3520
  if (!success) {
3405
- throw new Error("Token not found or already used");
3521
+ throw new NotFoundError("Token", input.token);
3406
3522
  }
3407
3523
  return { success: true };
3408
3524
  }
3409
3525
  // ── Moderate ────────────────────────────────────────────────────────
3410
3526
  async moderate(input, authContext) {
3411
3527
  if (!input.space_id && !input.group_id) {
3412
- throw new Error("Must specify either space_id or group_id");
3528
+ throw new ValidationError("Must specify either space_id or group_id");
3413
3529
  }
3414
3530
  if (!ACTION_TO_STATUS[input.action]) {
3415
- throw new Error(`Invalid action: ${input.action}. Must be approve, reject, or remove`);
3531
+ throw new ValidationError(`Invalid action: ${input.action}. Must be approve, reject, or remove`);
3416
3532
  }
3417
3533
  if (input.group_id) {
3418
3534
  if (!canModerate(authContext, input.group_id)) {
3419
- throw new Error(`Moderator access required for group ${input.group_id}`);
3535
+ throw new ForbiddenError(`Moderator access required for group ${input.group_id}`);
3420
3536
  }
3421
3537
  } else if (input.space_id) {
3422
3538
  if (!canModerateAny(authContext)) {
3423
- throw new Error("Moderator access required to moderate memories in spaces");
3539
+ throw new ForbiddenError("Moderator access required to moderate memories in spaces");
3424
3540
  }
3425
3541
  }
3426
3542
  let collection;
@@ -3432,8 +3548,7 @@ var SpaceService = class {
3432
3548
  }
3433
3549
  const memory = await fetchMemoryWithAllProperties(collection, input.memory_id);
3434
3550
  if (!memory) {
3435
- const location2 = input.group_id ? `group ${input.group_id}` : `space ${input.space_id}`;
3436
- throw new Error(`Published memory ${input.memory_id} not found in ${location2}`);
3551
+ throw new NotFoundError("Published memory", input.memory_id);
3437
3552
  }
3438
3553
  const newStatus = ACTION_TO_STATUS[input.action];
3439
3554
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -3472,24 +3587,24 @@ var SpaceService = class {
3472
3587
  if (spaces.length > 0) {
3473
3588
  const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
3474
3589
  if (invalidSpaces.length > 0) {
3475
- throw new Error(`Invalid space IDs: ${invalidSpaces.join(", ")}`);
3590
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
3476
3591
  }
3477
3592
  }
3478
3593
  if (groups.length > 0) {
3479
3594
  const invalidGroups = groups.filter((g) => !g || g.includes(".") || g.trim() === "");
3480
3595
  if (invalidGroups.length > 0) {
3481
- throw new Error("Group IDs cannot be empty or contain dots");
3596
+ throw new ValidationError("Group IDs cannot be empty or contain dots");
3482
3597
  }
3483
3598
  }
3484
3599
  const moderationFilter = input.moderation_filter || "approved";
3485
3600
  if (moderationFilter !== "approved") {
3486
3601
  for (const groupId of groups) {
3487
3602
  if (!canModerate(authContext, groupId)) {
3488
- throw new Error(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
3603
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
3489
3604
  }
3490
3605
  }
3491
3606
  if ((spaces.length > 0 || groups.length === 0) && !canModerateAny(authContext)) {
3492
- throw new Error(`Moderator access required to view ${moderationFilter} memories in spaces`);
3607
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in spaces`);
3493
3608
  }
3494
3609
  }
3495
3610
  const fetchLimit = (limit + offset) * Math.max(1, groups.length + (spaces.length > 0 || groups.length === 0 ? 1 : 0));
@@ -3549,16 +3664,16 @@ var SpaceService = class {
3549
3664
  // ── Query Space ─────────────────────────────────────────────────────
3550
3665
  async query(input, authContext) {
3551
3666
  if (!input.question?.trim())
3552
- throw new Error("Question cannot be empty");
3667
+ throw new ValidationError("Question cannot be empty");
3553
3668
  if (input.spaces.length === 0)
3554
- throw new Error("Must specify at least one space to query");
3669
+ throw new ValidationError("Must specify at least one space to query");
3555
3670
  const invalidSpaces = input.spaces.filter((s) => !isValidSpaceId(s));
3556
3671
  if (invalidSpaces.length > 0) {
3557
- throw new Error(`Invalid space IDs: ${invalidSpaces.join(", ")}`);
3672
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
3558
3673
  }
3559
3674
  const moderationFilterValue = input.moderation_filter || "approved";
3560
3675
  if (moderationFilterValue !== "approved" && !canModerateAny(authContext)) {
3561
- throw new Error(`Moderator access required to view ${moderationFilterValue} memories in spaces`);
3676
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilterValue} memories in spaces`);
3562
3677
  }
3563
3678
  const publicCollection = await ensurePublicCollection(this.weaviateClient);
3564
3679
  const filterList = [];
@@ -3609,13 +3724,13 @@ var SpaceService = class {
3609
3724
  const spaces = request.payload.spaces || [];
3610
3725
  const groups = request.payload.groups || [];
3611
3726
  if (spaces.length === 0 && groups.length === 0) {
3612
- throw new Error("No destinations in publish request");
3727
+ throw new ValidationError("No destinations in publish request");
3613
3728
  }
3614
3729
  const originalMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
3615
3730
  if (!originalMemory)
3616
- throw new Error(`Memory ${request.payload.memory_id} no longer exists`);
3731
+ throw new NotFoundError("Memory", request.payload.memory_id);
3617
3732
  if (originalMemory.properties.user_id !== this.userId)
3618
- throw new Error("Permission denied");
3733
+ throw new ForbiddenError("Permission denied");
3619
3734
  const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
3620
3735
  const weaviateId = compositeIdToUuid(compositeId);
3621
3736
  const existingSpaceIds = Array.isArray(originalMemory.properties.space_ids) ? originalMemory.properties.space_ids : [];
@@ -3751,7 +3866,7 @@ var SpaceService = class {
3751
3866
  const groups = request.payload.groups || [];
3752
3867
  const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
3753
3868
  if (!sourceMemory)
3754
- throw new Error(`Source memory ${request.payload.memory_id} no longer exists`);
3869
+ throw new NotFoundError("Memory", request.payload.memory_id);
3755
3870
  const currentSpaceIds = Array.isArray(sourceMemory.properties.space_ids) ? sourceMemory.properties.space_ids : [];
3756
3871
  const currentGroupIds = Array.isArray(sourceMemory.properties.group_ids) ? sourceMemory.properties.group_ids : [];
3757
3872
  const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
@@ -3831,9 +3946,9 @@ var SpaceService = class {
3831
3946
  const { memory_id, space_ids = [], group_ids = [] } = request.payload;
3832
3947
  const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, memory_id);
3833
3948
  if (!sourceMemory)
3834
- throw new Error(`Source memory ${memory_id} no longer exists`);
3949
+ throw new NotFoundError("Memory", memory_id);
3835
3950
  if (sourceMemory.properties.user_id !== this.userId)
3836
- throw new Error("Permission denied");
3951
+ throw new ForbiddenError("Permission denied");
3837
3952
  const newContent = String(sourceMemory.properties.content ?? "");
3838
3953
  const revisedAt = (/* @__PURE__ */ new Date()).toISOString();
3839
3954
  const compositeId = generateCompositeId(this.userId, memory_id);
@@ -3891,6 +4006,135 @@ var SpaceService = class {
3891
4006
  results
3892
4007
  };
3893
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
+ }
3894
4138
  // ── Private: Build Base Filters ─────────────────────────────────────
3895
4139
  buildBaseFilters(collection, input) {
3896
4140
  const filterList = [];
@@ -3928,6 +4172,10 @@ var SpaceService = class {
3928
4172
  const opts = { limit };
3929
4173
  if (filters)
3930
4174
  opts.filters = filters;
4175
+ const isWildcard = !query.trim() || query === "*";
4176
+ if (isWildcard) {
4177
+ return (await collection.query.bm25("*", opts)).objects;
4178
+ }
3931
4179
  switch (searchType) {
3932
4180
  case "bm25":
3933
4181
  return (await collection.query.bm25(query, opts)).objects;
@@ -4030,10 +4278,13 @@ Content to evaluate:
4030
4278
  ${content}
4031
4279
  ---
4032
4280
 
4033
- Respond with ONLY valid JSON:
4034
- {"pass":true}
4035
- OR
4036
- {"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.`;
4037
4288
  }
4038
4289
  var DEFAULT_CACHE_MAX = 1e3;
4039
4290
  function hashContent(content) {
@@ -4060,14 +4311,21 @@ function createModerationClient(options) {
4060
4311
  body: JSON.stringify({
4061
4312
  model,
4062
4313
  max_tokens: 256,
4063
- messages: [{ role: "user", content: buildModerationPrompt(content) }]
4314
+ temperature: 0,
4315
+ messages: [
4316
+ { role: "user", content: buildModerationPrompt(content) },
4317
+ { role: "assistant", content: "{" }
4318
+ ]
4064
4319
  })
4065
4320
  });
4066
4321
  if (!response.ok) {
4322
+ const errorBody = await response.text().catch(() => "");
4323
+ console.error(`[moderation] Anthropic API error: ${response.status} ${response.statusText}`, errorBody);
4067
4324
  return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4068
4325
  }
4069
4326
  const data = await response.json();
4070
- const text = data.content?.[0]?.text ?? "";
4327
+ const rawText = data.content?.[0]?.text ?? "";
4328
+ const text = ("{" + rawText).replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
4071
4329
  const parsed = JSON.parse(text);
4072
4330
  const result = {
4073
4331
  pass: parsed.pass === true,
@@ -4080,7 +4338,8 @@ function createModerationClient(options) {
4080
4338
  }
4081
4339
  cache.set(hash, result);
4082
4340
  return result;
4083
- } catch {
4341
+ } catch (err2) {
4342
+ console.error("[moderation] Unexpected error:", err2);
4084
4343
  return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4085
4344
  }
4086
4345
  }
package/dist/server.js CHANGED
@@ -1401,6 +1401,24 @@ var ValidationError = class extends AppError {
1401
1401
  this.fields = fields;
1402
1402
  }
1403
1403
  };
1404
+ var NotFoundError = class extends AppError {
1405
+ resource;
1406
+ id;
1407
+ kind = "not_found";
1408
+ constructor(resource, id) {
1409
+ super(`${resource} not found: ${id}`, { resource, id });
1410
+ this.resource = resource;
1411
+ this.id = id;
1412
+ }
1413
+ };
1414
+ var ForbiddenError = class extends AppError {
1415
+ requiredRole;
1416
+ kind = "forbidden";
1417
+ constructor(message = "Access denied", requiredRole) {
1418
+ super(message, { requiredRole });
1419
+ this.requiredRole = requiredRole;
1420
+ }
1421
+ };
1404
1422
 
1405
1423
  // node_modules/@prmichaelsen/remember-core/node_modules/uuid/dist/esm/regex.js
1406
1424
  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;
@@ -2317,6 +2335,38 @@ async function getSpaceConfig(id, type, logger2) {
2317
2335
  // node_modules/@prmichaelsen/remember-core/dist/services/memory.service.js
2318
2336
  import { Filters as Filters2 } from "weaviate-client";
2319
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
+
2320
2370
  // node_modules/@prmichaelsen/remember-core/dist/database/weaviate/client.js
2321
2371
  import weaviate2 from "weaviate-client";
2322
2372
  var ALL_MEMORY_PROPERTIES = [
@@ -2697,6 +2747,72 @@ var MemoryService = class {
2697
2747
  limit
2698
2748
  };
2699
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
+ }
2700
2816
  // ── Find Similar (vector) ──────────────────────────────────────────
2701
2817
  async findSimilar(input) {
2702
2818
  if (!input.memory_id && !input.text)
@@ -3276,32 +3392,32 @@ var SpaceService = class {
3276
3392
  const spaces = input.spaces || [];
3277
3393
  const groups = input.groups || [];
3278
3394
  if (spaces.length === 0 && groups.length === 0) {
3279
- throw new Error("Must specify at least one space or group to publish to");
3395
+ throw new ValidationError("Must specify at least one space or group to publish to");
3280
3396
  }
3281
3397
  if (spaces.length > 0) {
3282
3398
  const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
3283
3399
  if (invalidSpaces.length > 0) {
3284
- throw new Error(`Invalid space IDs: ${invalidSpaces.join(", ")}`);
3400
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
3285
3401
  }
3286
3402
  }
3287
3403
  if (groups.length > 0) {
3288
3404
  const invalidGroups = groups.filter((g) => !g || g.includes(".") || g.trim() === "");
3289
3405
  if (invalidGroups.length > 0) {
3290
- throw new Error("Group IDs cannot be empty or contain dots");
3406
+ throw new ValidationError("Group IDs cannot be empty or contain dots");
3291
3407
  }
3292
3408
  }
3293
3409
  const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
3294
3410
  if (!memory)
3295
- throw new Error(`Memory not found: ${input.memory_id}`);
3411
+ throw new NotFoundError("Memory", input.memory_id);
3296
3412
  if (memory.properties.user_id !== this.userId)
3297
- throw new Error("Permission denied: not memory owner");
3413
+ throw new ForbiddenError("Permission denied: not memory owner");
3298
3414
  if (memory.properties.doc_type !== "memory")
3299
- throw new Error("Only memories can be published");
3415
+ throw new ValidationError("Only memories can be published");
3300
3416
  const memoryContentType = memory.properties.content_type;
3301
3417
  for (const spaceId of spaces) {
3302
3418
  const requiredType = SPACE_CONTENT_TYPE_RESTRICTIONS[spaceId];
3303
3419
  if (requiredType && memoryContentType !== requiredType) {
3304
- throw new Error(`Space '${spaceId}' only accepts content_type '${requiredType}', got '${memoryContentType ?? "undefined"}'`);
3420
+ throw new ValidationError(`Space '${spaceId}' only accepts content_type '${requiredType}', got '${memoryContentType ?? "undefined"}'`);
3305
3421
  }
3306
3422
  }
3307
3423
  await this.checkModeration(memory.properties.content);
@@ -3324,25 +3440,25 @@ var SpaceService = class {
3324
3440
  const spaces = input.spaces || [];
3325
3441
  const groups = input.groups || [];
3326
3442
  if (spaces.length === 0 && groups.length === 0) {
3327
- throw new Error("Must specify at least one space or group to retract from");
3443
+ throw new ValidationError("Must specify at least one space or group to retract from");
3328
3444
  }
3329
3445
  if (groups.length > 0) {
3330
3446
  const invalidGroups = groups.filter((g) => g.includes("."));
3331
3447
  if (invalidGroups.length > 0) {
3332
- throw new Error(`Group IDs cannot contain dots: ${invalidGroups.join(", ")}`);
3448
+ throw new ValidationError(`Group IDs cannot contain dots: ${invalidGroups.join(", ")}`);
3333
3449
  }
3334
3450
  }
3335
3451
  const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
3336
3452
  if (!memory)
3337
- throw new Error(`Memory not found: ${input.memory_id}`);
3453
+ throw new NotFoundError("Memory", input.memory_id);
3338
3454
  if (memory.properties.user_id !== this.userId)
3339
- throw new Error("Permission denied: not memory owner");
3455
+ throw new ForbiddenError("Permission denied: not memory owner");
3340
3456
  const currentSpaceIds = Array.isArray(memory.properties.space_ids) ? memory.properties.space_ids : [];
3341
3457
  const currentGroupIds = Array.isArray(memory.properties.group_ids) ? memory.properties.group_ids : [];
3342
3458
  const notPublishedSpaces = spaces.filter((s) => !currentSpaceIds.includes(s));
3343
3459
  const notPublishedGroups = groups.filter((g) => !currentGroupIds.includes(g));
3344
3460
  if (notPublishedSpaces.length > 0 || notPublishedGroups.length > 0) {
3345
- throw new Error(`Memory is not published to some destinations. Not in spaces: [${notPublishedSpaces.join(", ")}], Not in groups: [${notPublishedGroups.join(", ")}]`);
3461
+ throw new ValidationError(`Memory is not published to some destinations. Not in spaces: [${notPublishedSpaces.join(", ")}], Not in groups: [${notPublishedGroups.join(", ")}]`);
3346
3462
  }
3347
3463
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "retract_memory", {
3348
3464
  memory_id: input.memory_id,
@@ -3363,13 +3479,13 @@ var SpaceService = class {
3363
3479
  async revise(input) {
3364
3480
  const memory = await fetchMemoryWithAllProperties(this.userCollection, input.memory_id);
3365
3481
  if (!memory)
3366
- throw new Error(`Memory not found: ${input.memory_id}`);
3482
+ throw new NotFoundError("Memory", input.memory_id);
3367
3483
  if (memory.properties.user_id !== this.userId)
3368
- throw new Error("Permission denied: not memory owner");
3484
+ throw new ForbiddenError("Permission denied: not memory owner");
3369
3485
  const spaceIds = Array.isArray(memory.properties.space_ids) ? memory.properties.space_ids : [];
3370
3486
  const groupIds = Array.isArray(memory.properties.group_ids) ? memory.properties.group_ids : [];
3371
3487
  if (spaceIds.length === 0 && groupIds.length === 0) {
3372
- throw new Error("Memory has no published copies to revise. Publish first with publish().");
3488
+ throw new ValidationError("Memory has no published copies to revise. Publish first with publish().");
3373
3489
  }
3374
3490
  await this.checkModeration(memory.properties.content);
3375
3491
  const { token } = await this.confirmationTokenService.createRequest(this.userId, "revise_memory", {
@@ -3389,7 +3505,7 @@ var SpaceService = class {
3389
3505
  async confirm(input) {
3390
3506
  const request = await this.confirmationTokenService.confirmRequest(this.userId, input.token);
3391
3507
  if (!request) {
3392
- throw new Error("Invalid or expired confirmation token");
3508
+ throw new ValidationError("Invalid or expired confirmation token");
3393
3509
  }
3394
3510
  if (request.action === "publish_memory") {
3395
3511
  return this.executePublish(request);
@@ -3400,31 +3516,31 @@ var SpaceService = class {
3400
3516
  if (request.action === "revise_memory") {
3401
3517
  return this.executeRevise(request);
3402
3518
  }
3403
- throw new Error(`Unknown action type: ${request.action}`);
3519
+ throw new ValidationError(`Unknown action type: ${request.action}`);
3404
3520
  }
3405
3521
  // ── Deny ────────────────────────────────────────────────────────────
3406
3522
  async deny(input) {
3407
3523
  const success = await this.confirmationTokenService.denyRequest(this.userId, input.token);
3408
3524
  if (!success) {
3409
- throw new Error("Token not found or already used");
3525
+ throw new NotFoundError("Token", input.token);
3410
3526
  }
3411
3527
  return { success: true };
3412
3528
  }
3413
3529
  // ── Moderate ────────────────────────────────────────────────────────
3414
3530
  async moderate(input, authContext) {
3415
3531
  if (!input.space_id && !input.group_id) {
3416
- throw new Error("Must specify either space_id or group_id");
3532
+ throw new ValidationError("Must specify either space_id or group_id");
3417
3533
  }
3418
3534
  if (!ACTION_TO_STATUS[input.action]) {
3419
- throw new Error(`Invalid action: ${input.action}. Must be approve, reject, or remove`);
3535
+ throw new ValidationError(`Invalid action: ${input.action}. Must be approve, reject, or remove`);
3420
3536
  }
3421
3537
  if (input.group_id) {
3422
3538
  if (!canModerate(authContext, input.group_id)) {
3423
- throw new Error(`Moderator access required for group ${input.group_id}`);
3539
+ throw new ForbiddenError(`Moderator access required for group ${input.group_id}`);
3424
3540
  }
3425
3541
  } else if (input.space_id) {
3426
3542
  if (!canModerateAny(authContext)) {
3427
- throw new Error("Moderator access required to moderate memories in spaces");
3543
+ throw new ForbiddenError("Moderator access required to moderate memories in spaces");
3428
3544
  }
3429
3545
  }
3430
3546
  let collection;
@@ -3436,8 +3552,7 @@ var SpaceService = class {
3436
3552
  }
3437
3553
  const memory = await fetchMemoryWithAllProperties(collection, input.memory_id);
3438
3554
  if (!memory) {
3439
- const location2 = input.group_id ? `group ${input.group_id}` : `space ${input.space_id}`;
3440
- throw new Error(`Published memory ${input.memory_id} not found in ${location2}`);
3555
+ throw new NotFoundError("Published memory", input.memory_id);
3441
3556
  }
3442
3557
  const newStatus = ACTION_TO_STATUS[input.action];
3443
3558
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -3476,24 +3591,24 @@ var SpaceService = class {
3476
3591
  if (spaces.length > 0) {
3477
3592
  const invalidSpaces = spaces.filter((s) => !isValidSpaceId(s));
3478
3593
  if (invalidSpaces.length > 0) {
3479
- throw new Error(`Invalid space IDs: ${invalidSpaces.join(", ")}`);
3594
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
3480
3595
  }
3481
3596
  }
3482
3597
  if (groups.length > 0) {
3483
3598
  const invalidGroups = groups.filter((g) => !g || g.includes(".") || g.trim() === "");
3484
3599
  if (invalidGroups.length > 0) {
3485
- throw new Error("Group IDs cannot be empty or contain dots");
3600
+ throw new ValidationError("Group IDs cannot be empty or contain dots");
3486
3601
  }
3487
3602
  }
3488
3603
  const moderationFilter = input.moderation_filter || "approved";
3489
3604
  if (moderationFilter !== "approved") {
3490
3605
  for (const groupId of groups) {
3491
3606
  if (!canModerate(authContext, groupId)) {
3492
- throw new Error(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
3607
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in group ${groupId}`);
3493
3608
  }
3494
3609
  }
3495
3610
  if ((spaces.length > 0 || groups.length === 0) && !canModerateAny(authContext)) {
3496
- throw new Error(`Moderator access required to view ${moderationFilter} memories in spaces`);
3611
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilter} memories in spaces`);
3497
3612
  }
3498
3613
  }
3499
3614
  const fetchLimit = (limit + offset) * Math.max(1, groups.length + (spaces.length > 0 || groups.length === 0 ? 1 : 0));
@@ -3553,16 +3668,16 @@ var SpaceService = class {
3553
3668
  // ── Query Space ─────────────────────────────────────────────────────
3554
3669
  async query(input, authContext) {
3555
3670
  if (!input.question?.trim())
3556
- throw new Error("Question cannot be empty");
3671
+ throw new ValidationError("Question cannot be empty");
3557
3672
  if (input.spaces.length === 0)
3558
- throw new Error("Must specify at least one space to query");
3673
+ throw new ValidationError("Must specify at least one space to query");
3559
3674
  const invalidSpaces = input.spaces.filter((s) => !isValidSpaceId(s));
3560
3675
  if (invalidSpaces.length > 0) {
3561
- throw new Error(`Invalid space IDs: ${invalidSpaces.join(", ")}`);
3676
+ throw new ValidationError(`Invalid space IDs: ${invalidSpaces.join(", ")}`, { spaces: invalidSpaces });
3562
3677
  }
3563
3678
  const moderationFilterValue = input.moderation_filter || "approved";
3564
3679
  if (moderationFilterValue !== "approved" && !canModerateAny(authContext)) {
3565
- throw new Error(`Moderator access required to view ${moderationFilterValue} memories in spaces`);
3680
+ throw new ForbiddenError(`Moderator access required to view ${moderationFilterValue} memories in spaces`);
3566
3681
  }
3567
3682
  const publicCollection = await ensurePublicCollection(this.weaviateClient);
3568
3683
  const filterList = [];
@@ -3613,13 +3728,13 @@ var SpaceService = class {
3613
3728
  const spaces = request.payload.spaces || [];
3614
3729
  const groups = request.payload.groups || [];
3615
3730
  if (spaces.length === 0 && groups.length === 0) {
3616
- throw new Error("No destinations in publish request");
3731
+ throw new ValidationError("No destinations in publish request");
3617
3732
  }
3618
3733
  const originalMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
3619
3734
  if (!originalMemory)
3620
- throw new Error(`Memory ${request.payload.memory_id} no longer exists`);
3735
+ throw new NotFoundError("Memory", request.payload.memory_id);
3621
3736
  if (originalMemory.properties.user_id !== this.userId)
3622
- throw new Error("Permission denied");
3737
+ throw new ForbiddenError("Permission denied");
3623
3738
  const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
3624
3739
  const weaviateId = compositeIdToUuid(compositeId);
3625
3740
  const existingSpaceIds = Array.isArray(originalMemory.properties.space_ids) ? originalMemory.properties.space_ids : [];
@@ -3755,7 +3870,7 @@ var SpaceService = class {
3755
3870
  const groups = request.payload.groups || [];
3756
3871
  const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, request.payload.memory_id);
3757
3872
  if (!sourceMemory)
3758
- throw new Error(`Source memory ${request.payload.memory_id} no longer exists`);
3873
+ throw new NotFoundError("Memory", request.payload.memory_id);
3759
3874
  const currentSpaceIds = Array.isArray(sourceMemory.properties.space_ids) ? sourceMemory.properties.space_ids : [];
3760
3875
  const currentGroupIds = Array.isArray(sourceMemory.properties.group_ids) ? sourceMemory.properties.group_ids : [];
3761
3876
  const compositeId = generateCompositeId(this.userId, request.payload.memory_id);
@@ -3835,9 +3950,9 @@ var SpaceService = class {
3835
3950
  const { memory_id, space_ids = [], group_ids = [] } = request.payload;
3836
3951
  const sourceMemory = await fetchMemoryWithAllProperties(this.userCollection, memory_id);
3837
3952
  if (!sourceMemory)
3838
- throw new Error(`Source memory ${memory_id} no longer exists`);
3953
+ throw new NotFoundError("Memory", memory_id);
3839
3954
  if (sourceMemory.properties.user_id !== this.userId)
3840
- throw new Error("Permission denied");
3955
+ throw new ForbiddenError("Permission denied");
3841
3956
  const newContent = String(sourceMemory.properties.content ?? "");
3842
3957
  const revisedAt = (/* @__PURE__ */ new Date()).toISOString();
3843
3958
  const compositeId = generateCompositeId(this.userId, memory_id);
@@ -3895,6 +4010,135 @@ var SpaceService = class {
3895
4010
  results
3896
4011
  };
3897
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
+ }
3898
4142
  // ── Private: Build Base Filters ─────────────────────────────────────
3899
4143
  buildBaseFilters(collection, input) {
3900
4144
  const filterList = [];
@@ -3932,6 +4176,10 @@ var SpaceService = class {
3932
4176
  const opts = { limit };
3933
4177
  if (filters)
3934
4178
  opts.filters = filters;
4179
+ const isWildcard = !query.trim() || query === "*";
4180
+ if (isWildcard) {
4181
+ return (await collection.query.bm25("*", opts)).objects;
4182
+ }
3935
4183
  switch (searchType) {
3936
4184
  case "bm25":
3937
4185
  return (await collection.query.bm25(query, opts)).objects;
@@ -4034,10 +4282,13 @@ Content to evaluate:
4034
4282
  ${content}
4035
4283
  ---
4036
4284
 
4037
- Respond with ONLY valid JSON:
4038
- {"pass":true}
4039
- OR
4040
- {"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.`;
4041
4292
  }
4042
4293
  var DEFAULT_CACHE_MAX = 1e3;
4043
4294
  function hashContent(content) {
@@ -4064,14 +4315,21 @@ function createModerationClient(options) {
4064
4315
  body: JSON.stringify({
4065
4316
  model,
4066
4317
  max_tokens: 256,
4067
- messages: [{ role: "user", content: buildModerationPrompt(content) }]
4318
+ temperature: 0,
4319
+ messages: [
4320
+ { role: "user", content: buildModerationPrompt(content) },
4321
+ { role: "assistant", content: "{" }
4322
+ ]
4068
4323
  })
4069
4324
  });
4070
4325
  if (!response.ok) {
4326
+ const errorBody = await response.text().catch(() => "");
4327
+ console.error(`[moderation] Anthropic API error: ${response.status} ${response.statusText}`, errorBody);
4071
4328
  return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4072
4329
  }
4073
4330
  const data = await response.json();
4074
- const text = data.content?.[0]?.text ?? "";
4331
+ const rawText = data.content?.[0]?.text ?? "";
4332
+ const text = ("{" + rawText).replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
4075
4333
  const parsed = JSON.parse(text);
4076
4334
  const result = {
4077
4335
  pass: parsed.pass === true,
@@ -4084,7 +4342,8 @@ function createModerationClient(options) {
4084
4342
  }
4085
4343
  cache.set(hash, result);
4086
4344
  return result;
4087
- } catch {
4345
+ } catch (err2) {
4346
+ console.error("[moderation] Unexpected error:", err2);
4088
4347
  return { pass: false, reason: "Content moderation unavailable. Please try again later." };
4089
4348
  }
4090
4349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/remember-mcp",
3
- "version": "3.15.2",
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.12",
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"