@prmichaelsen/remember-mcp 2.2.1 → 2.3.1
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/AGENT.md +98 -5
- package/CHANGELOG.md +45 -0
- package/README.md +43 -3
- package/agent/commands/acp.init.md +376 -0
- package/agent/commands/acp.package-install.md +347 -0
- package/agent/commands/acp.proceed.md +311 -0
- package/agent/commands/acp.report.md +392 -0
- package/agent/commands/acp.status.md +280 -0
- package/agent/commands/acp.sync.md +323 -0
- package/agent/commands/acp.update.md +301 -0
- package/agent/commands/acp.validate.md +385 -0
- package/agent/commands/acp.version-check-for-updates.md +275 -0
- package/agent/commands/acp.version-check.md +190 -0
- package/agent/commands/acp.version-update.md +288 -0
- package/agent/commands/command.template.md +273 -0
- package/agent/design/core-memory-user-profile.md +1253 -0
- package/agent/design/ghost-profiles-pseudonymous-identity.md +194 -0
- package/agent/design/publish-tools-confirmation-flow.md +922 -0
- package/agent/milestones/milestone-10-shared-spaces.md +169 -0
- package/agent/progress.yaml +90 -4
- package/agent/scripts/install.sh +118 -0
- package/agent/scripts/update.sh +22 -10
- package/agent/scripts/version.sh +35 -0
- package/agent/tasks/task-27-implement-llm-provider-interface.md +51 -0
- package/agent/tasks/task-28-implement-llm-provider-factory.md +64 -0
- package/agent/tasks/task-29-update-config-for-llm.md +71 -0
- package/agent/tasks/task-30-implement-bedrock-provider.md +147 -0
- package/agent/tasks/task-31-implement-background-job-service.md +120 -0
- package/agent/tasks/task-32-test-llm-provider-integration.md +152 -0
- package/agent/tasks/task-34-create-confirmation-token-service.md +191 -0
- package/agent/tasks/task-35-create-space-memory-types-schema.md +183 -0
- package/agent/tasks/task-36-implement-remember-publish.md +227 -0
- package/agent/tasks/task-37-implement-remember-confirm.md +225 -0
- package/agent/tasks/task-38-implement-remember-deny.md +161 -0
- package/agent/tasks/task-39-implement-remember-search-space.md +188 -0
- package/agent/tasks/task-40-implement-remember-query-space.md +193 -0
- package/agent/tasks/task-41-configure-firestore-ttl.md +188 -0
- package/agent/tasks/task-42-create-tests-shared-spaces.md +216 -0
- package/agent/tasks/task-43-update-documentation.md +255 -0
- package/agent/tasks/task-44-implement-remember-retract.md +263 -0
- package/agent/tasks/task-45-fix-publish-false-success-bug.md +230 -0
- package/dist/llm/types.d.ts +1 -0
- package/dist/server-factory.js +1000 -1
- package/dist/server.js +1002 -3
- package/dist/services/confirmation-token.service.d.ts +99 -0
- package/dist/services/confirmation-token.service.spec.d.ts +5 -0
- package/dist/tools/confirm.d.ts +20 -0
- package/dist/tools/deny.d.ts +19 -0
- package/dist/tools/publish.d.ts +22 -0
- package/dist/tools/query-space.d.ts +28 -0
- package/dist/tools/search-space.d.ts +29 -0
- package/dist/types/space-memory.d.ts +80 -0
- package/dist/weaviate/space-schema.d.ts +59 -0
- package/dist/weaviate/space-schema.spec.d.ts +5 -0
- package/package.json +1 -1
- package/src/llm/types.ts +0 -0
- package/src/server-factory.ts +33 -0
- package/src/server.ts +33 -0
- package/src/services/confirmation-token.service.spec.ts +254 -0
- package/src/services/confirmation-token.service.ts +265 -0
- package/src/tools/confirm.ts +219 -0
- package/src/tools/create-memory.ts +7 -0
- package/src/tools/deny.ts +70 -0
- package/src/tools/publish.ts +190 -0
- package/src/tools/query-space.ts +197 -0
- package/src/tools/search-space.ts +189 -0
- package/src/types/space-memory.ts +94 -0
- package/src/weaviate/space-schema.spec.ts +131 -0
- package/src/weaviate/space-schema.ts +275 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remember_search_space tool
|
|
3
|
+
*
|
|
4
|
+
* Search shared spaces to discover memories from other users.
|
|
5
|
+
* Similar to remember_search_memory but searches space collections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { Filters } from 'weaviate-client';
|
|
10
|
+
import { getWeaviateClient } from '../weaviate/client.js';
|
|
11
|
+
import { ensureSpaceCollection, isValidSpaceId } from '../weaviate/space-schema.js';
|
|
12
|
+
import { SUPPORTED_SPACES } from '../types/space-memory.js';
|
|
13
|
+
import { handleToolError } from '../utils/error-handler.js';
|
|
14
|
+
import type { SearchFilters } from '../types/memory.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tool definition for remember_search_space
|
|
18
|
+
*/
|
|
19
|
+
export const searchSpaceTool: Tool = {
|
|
20
|
+
name: 'remember_search_space',
|
|
21
|
+
description: 'Search shared spaces to discover thoughts, ideas, and memories. Works like remember_search_memory but searches shared spaces instead of personal memories.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
query: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Search query (semantic + keyword hybrid)',
|
|
28
|
+
},
|
|
29
|
+
space: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Which space to search',
|
|
32
|
+
enum: SUPPORTED_SPACES,
|
|
33
|
+
default: 'the_void',
|
|
34
|
+
},
|
|
35
|
+
content_type: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Filter by content type',
|
|
38
|
+
},
|
|
39
|
+
tags: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: { type: 'string' },
|
|
42
|
+
description: 'Filter by tags (must have all specified tags)',
|
|
43
|
+
},
|
|
44
|
+
min_weight: {
|
|
45
|
+
type: 'number',
|
|
46
|
+
minimum: 0,
|
|
47
|
+
maximum: 1,
|
|
48
|
+
description: 'Minimum weight/significance (0-1)',
|
|
49
|
+
},
|
|
50
|
+
max_weight: {
|
|
51
|
+
type: 'number',
|
|
52
|
+
minimum: 0,
|
|
53
|
+
maximum: 1,
|
|
54
|
+
description: 'Maximum weight/significance (0-1)',
|
|
55
|
+
},
|
|
56
|
+
date_from: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Filter memories created after this date (ISO 8601)',
|
|
59
|
+
},
|
|
60
|
+
date_to: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Filter memories created before this date (ISO 8601)',
|
|
63
|
+
},
|
|
64
|
+
limit: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
default: 10,
|
|
67
|
+
description: 'Maximum number of results',
|
|
68
|
+
},
|
|
69
|
+
offset: {
|
|
70
|
+
type: 'number',
|
|
71
|
+
default: 0,
|
|
72
|
+
description: 'Offset for pagination',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ['query', 'space'],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface SearchSpaceArgs {
|
|
80
|
+
query: string;
|
|
81
|
+
space: string;
|
|
82
|
+
content_type?: string;
|
|
83
|
+
tags?: string[];
|
|
84
|
+
min_weight?: number;
|
|
85
|
+
max_weight?: number;
|
|
86
|
+
date_from?: string;
|
|
87
|
+
date_to?: string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
offset?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handle remember_search_space tool execution
|
|
94
|
+
*/
|
|
95
|
+
export async function handleSearchSpace(
|
|
96
|
+
args: SearchSpaceArgs,
|
|
97
|
+
userId: string // May be used for private spaces in future
|
|
98
|
+
): Promise<string> {
|
|
99
|
+
try {
|
|
100
|
+
// Validate space ID
|
|
101
|
+
if (!isValidSpaceId(args.space)) {
|
|
102
|
+
return JSON.stringify(
|
|
103
|
+
{
|
|
104
|
+
success: false,
|
|
105
|
+
error: 'Invalid space ID',
|
|
106
|
+
message: `Space "${args.space}" is not supported. Supported spaces: ${SUPPORTED_SPACES.join(', ')}`,
|
|
107
|
+
},
|
|
108
|
+
null,
|
|
109
|
+
2
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const weaviateClient = getWeaviateClient();
|
|
114
|
+
const spaceCollection = await ensureSpaceCollection(weaviateClient, args.space);
|
|
115
|
+
|
|
116
|
+
// Build filters for space search
|
|
117
|
+
const filterList: any[] = [];
|
|
118
|
+
|
|
119
|
+
// Filter by space_id
|
|
120
|
+
filterList.push(spaceCollection.filter.byProperty('space_id').equal(args.space));
|
|
121
|
+
|
|
122
|
+
// Filter by doc_type (space_memory)
|
|
123
|
+
filterList.push(spaceCollection.filter.byProperty('doc_type').equal('space_memory'));
|
|
124
|
+
|
|
125
|
+
// Apply content type filter
|
|
126
|
+
if (args.content_type) {
|
|
127
|
+
filterList.push(spaceCollection.filter.byProperty('type').equal(args.content_type));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Apply tags filter
|
|
131
|
+
if (args.tags && args.tags.length > 0) {
|
|
132
|
+
args.tags.forEach(tag => {
|
|
133
|
+
filterList.push(spaceCollection.filter.byProperty('tags').containsAny([tag]));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Apply weight filters
|
|
138
|
+
if (args.min_weight !== undefined) {
|
|
139
|
+
filterList.push(spaceCollection.filter.byProperty('weight').greaterOrEqual(args.min_weight));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (args.max_weight !== undefined) {
|
|
143
|
+
filterList.push(spaceCollection.filter.byProperty('weight').lessOrEqual(args.max_weight));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Apply date filters (convert ISO strings to Date objects)
|
|
147
|
+
if (args.date_from) {
|
|
148
|
+
filterList.push(spaceCollection.filter.byProperty('created_at').greaterOrEqual(new Date(args.date_from)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (args.date_to) {
|
|
152
|
+
filterList.push(spaceCollection.filter.byProperty('created_at').lessOrEqual(new Date(args.date_to)));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const whereFilter = filterList.length > 0 ? Filters.and(...filterList) : undefined;
|
|
156
|
+
|
|
157
|
+
// Execute hybrid search
|
|
158
|
+
const searchResults = await spaceCollection.query.hybrid(args.query, {
|
|
159
|
+
limit: args.limit || 10,
|
|
160
|
+
offset: args.offset || 0,
|
|
161
|
+
...(whereFilter && { where: whereFilter }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Format results
|
|
165
|
+
const memories = searchResults.objects.map((obj) => ({
|
|
166
|
+
id: obj.uuid,
|
|
167
|
+
...obj.properties,
|
|
168
|
+
_score: obj.metadata?.score,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
const result = {
|
|
172
|
+
space: args.space,
|
|
173
|
+
query: args.query,
|
|
174
|
+
memories,
|
|
175
|
+
total: memories.length,
|
|
176
|
+
offset: args.offset || 0,
|
|
177
|
+
limit: args.limit || 10,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return JSON.stringify(result, null, 2);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
handleToolError(error, {
|
|
183
|
+
toolName: 'remember_search_space',
|
|
184
|
+
operation: 'search space',
|
|
185
|
+
space: args.space,
|
|
186
|
+
query: args.query,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Space Memory type definitions for remember-mcp
|
|
3
|
+
*
|
|
4
|
+
* Space memories are memories published to shared collections
|
|
5
|
+
* where they can be discovered by other users.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Memory, SearchOptions, SearchResult } from './memory.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Space memory - a memory published to a shared space
|
|
12
|
+
*
|
|
13
|
+
* Extends Memory with additional fields for attribution and discovery
|
|
14
|
+
*/
|
|
15
|
+
export interface SpaceMemory extends Omit<Memory, 'user_id' | 'doc_type'> {
|
|
16
|
+
/**
|
|
17
|
+
* Space identifier (snake_case)
|
|
18
|
+
* Examples: 'the_void', 'public_space'
|
|
19
|
+
*/
|
|
20
|
+
space_id: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Original author's user_id (for permissions)
|
|
24
|
+
* This is private and not shown publicly
|
|
25
|
+
*/
|
|
26
|
+
author_id: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optional ghost profile ID for pseudonymous publishing
|
|
30
|
+
* If present, memory is attributed to ghost instead of user
|
|
31
|
+
*/
|
|
32
|
+
ghost_id?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* When the memory was published to the space
|
|
36
|
+
*/
|
|
37
|
+
published_at: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How many times this memory has been discovered/viewed
|
|
41
|
+
*/
|
|
42
|
+
discovery_count: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attribution type
|
|
46
|
+
* - 'user': Published as the user (shows author_id)
|
|
47
|
+
* - 'ghost': Published as a ghost (shows ghost_id)
|
|
48
|
+
*/
|
|
49
|
+
attribution: 'user' | 'ghost';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Document type discriminator
|
|
53
|
+
* Always 'space_memory' for space memories
|
|
54
|
+
*/
|
|
55
|
+
doc_type: 'space_memory';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Search options for space memories
|
|
60
|
+
* Same as SearchOptions but for space collections
|
|
61
|
+
*/
|
|
62
|
+
export interface SpaceSearchOptions extends Omit<SearchOptions, 'include_relationships'> {
|
|
63
|
+
/**
|
|
64
|
+
* Space to search
|
|
65
|
+
*/
|
|
66
|
+
space: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Search result for space memories
|
|
71
|
+
*/
|
|
72
|
+
export interface SpaceSearchResult extends Omit<SearchResult, 'memories' | 'relationships'> {
|
|
73
|
+
/**
|
|
74
|
+
* Found space memories
|
|
75
|
+
*/
|
|
76
|
+
space_memories: SpaceMemory[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Supported space IDs
|
|
81
|
+
*/
|
|
82
|
+
export type SpaceId = 'the_void';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Space display names mapped to IDs
|
|
86
|
+
*/
|
|
87
|
+
export const SPACE_DISPLAY_NAMES: Record<SpaceId, string> = {
|
|
88
|
+
the_void: 'The Void',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Supported spaces constant
|
|
93
|
+
*/
|
|
94
|
+
export const SUPPORTED_SPACES: SpaceId[] = ['the_void'];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Space Schema utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getSpaceCollectionName,
|
|
7
|
+
sanitizeSpaceId,
|
|
8
|
+
getSpaceDisplayName,
|
|
9
|
+
isValidSpaceId,
|
|
10
|
+
ensureSpaceCollection,
|
|
11
|
+
} from './space-schema';
|
|
12
|
+
import type { WeaviateClient } from 'weaviate-client';
|
|
13
|
+
|
|
14
|
+
// Mock Weaviate client
|
|
15
|
+
const mockWeaviateClient = {
|
|
16
|
+
collections: {
|
|
17
|
+
exists: jest.fn(),
|
|
18
|
+
create: jest.fn(),
|
|
19
|
+
get: jest.fn(),
|
|
20
|
+
},
|
|
21
|
+
} as unknown as WeaviateClient;
|
|
22
|
+
|
|
23
|
+
describe('Space Schema Utilities', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getSpaceCollectionName', () => {
|
|
29
|
+
it('should return collection name with Memory_ prefix', () => {
|
|
30
|
+
expect(getSpaceCollectionName('the_void')).toBe('Memory_the_void');
|
|
31
|
+
expect(getSpaceCollectionName('public_space')).toBe('Memory_public_space');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('sanitizeSpaceId', () => {
|
|
36
|
+
it('should convert display name to snake_case', () => {
|
|
37
|
+
expect(sanitizeSpaceId('The Void')).toBe('the_void');
|
|
38
|
+
expect(sanitizeSpaceId('Public Space')).toBe('public_space');
|
|
39
|
+
expect(sanitizeSpaceId('MY SPACE')).toBe('my_space');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle multiple spaces', () => {
|
|
43
|
+
expect(sanitizeSpaceId('The Void Space')).toBe('the_void_space');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should handle already lowercase', () => {
|
|
47
|
+
expect(sanitizeSpaceId('the_void')).toBe('the_void');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('getSpaceDisplayName', () => {
|
|
52
|
+
it('should return display name for known space IDs', () => {
|
|
53
|
+
expect(getSpaceDisplayName('the_void')).toBe('The Void');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return space ID if not found in map', () => {
|
|
57
|
+
expect(getSpaceDisplayName('unknown_space')).toBe('unknown_space');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('isValidSpaceId', () => {
|
|
62
|
+
it('should return true for supported spaces', () => {
|
|
63
|
+
expect(isValidSpaceId('the_void')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return false for unsupported spaces', () => {
|
|
67
|
+
expect(isValidSpaceId('unknown_space')).toBe(false);
|
|
68
|
+
expect(isValidSpaceId('public_space')).toBe(false); // Not yet supported
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('ensureSpaceCollection', () => {
|
|
73
|
+
it('should return existing collection if it exists', async () => {
|
|
74
|
+
const mockCollection = { name: 'Memory_the_void' };
|
|
75
|
+
(mockWeaviateClient.collections.exists as jest.Mock).mockResolvedValue(true);
|
|
76
|
+
(mockWeaviateClient.collections.get as jest.Mock).mockReturnValue(mockCollection);
|
|
77
|
+
|
|
78
|
+
const result = await ensureSpaceCollection(mockWeaviateClient, 'the_void');
|
|
79
|
+
|
|
80
|
+
expect(result).toBe(mockCollection);
|
|
81
|
+
expect(mockWeaviateClient.collections.exists).toHaveBeenCalledWith('Memory_the_void');
|
|
82
|
+
expect(mockWeaviateClient.collections.get).toHaveBeenCalledWith('Memory_the_void');
|
|
83
|
+
expect(mockWeaviateClient.collections.create).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should create collection if it does not exist', async () => {
|
|
87
|
+
const mockCollection = { name: 'Memory_the_void' };
|
|
88
|
+
(mockWeaviateClient.collections.exists as jest.Mock).mockResolvedValue(false);
|
|
89
|
+
(mockWeaviateClient.collections.create as jest.Mock).mockResolvedValue(undefined);
|
|
90
|
+
(mockWeaviateClient.collections.get as jest.Mock).mockReturnValue(mockCollection);
|
|
91
|
+
|
|
92
|
+
const result = await ensureSpaceCollection(mockWeaviateClient, 'the_void');
|
|
93
|
+
|
|
94
|
+
expect(result).toBe(mockCollection);
|
|
95
|
+
expect(mockWeaviateClient.collections.exists).toHaveBeenCalledWith('Memory_the_void');
|
|
96
|
+
expect(mockWeaviateClient.collections.create).toHaveBeenCalled();
|
|
97
|
+
expect(mockWeaviateClient.collections.get).toHaveBeenCalledWith('Memory_the_void');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should throw error for invalid space ID', async () => {
|
|
101
|
+
await expect(
|
|
102
|
+
ensureSpaceCollection(mockWeaviateClient, 'invalid_space')
|
|
103
|
+
).rejects.toThrow('Invalid space ID: invalid_space');
|
|
104
|
+
|
|
105
|
+
expect(mockWeaviateClient.collections.exists).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should create collection with correct schema', async () => {
|
|
109
|
+
(mockWeaviateClient.collections.exists as jest.Mock).mockResolvedValue(false);
|
|
110
|
+
(mockWeaviateClient.collections.create as jest.Mock).mockResolvedValue(undefined);
|
|
111
|
+
(mockWeaviateClient.collections.get as jest.Mock).mockReturnValue({});
|
|
112
|
+
|
|
113
|
+
await ensureSpaceCollection(mockWeaviateClient, 'the_void');
|
|
114
|
+
|
|
115
|
+
const createCall = (mockWeaviateClient.collections.create as jest.Mock).mock.calls[0][0];
|
|
116
|
+
expect(createCall.name).toBe('Memory_the_void');
|
|
117
|
+
expect(createCall.vectorizers).toBeDefined();
|
|
118
|
+
expect(createCall.properties).toBeDefined();
|
|
119
|
+
expect(createCall.properties.length).toBeGreaterThan(20); // Should have many properties
|
|
120
|
+
|
|
121
|
+
// Check for space-specific properties
|
|
122
|
+
const propertyNames = createCall.properties.map((p: any) => p.name);
|
|
123
|
+
expect(propertyNames).toContain('space_id');
|
|
124
|
+
expect(propertyNames).toContain('author_id');
|
|
125
|
+
expect(propertyNames).toContain('ghost_id');
|
|
126
|
+
expect(propertyNames).toContain('published_at');
|
|
127
|
+
expect(propertyNames).toContain('discovery_count');
|
|
128
|
+
expect(propertyNames).toContain('attribution');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weaviate space collection schema and utilities
|
|
3
|
+
*
|
|
4
|
+
* Manages shared space collections where users can publish memories
|
|
5
|
+
* for discovery by other users.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import weaviate, { type WeaviateClient, type Collection } from 'weaviate-client';
|
|
9
|
+
import { config } from '../config.js';
|
|
10
|
+
import { SUPPORTED_SPACES, type SpaceId } from '../types/space-memory.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get collection name for a space
|
|
14
|
+
*
|
|
15
|
+
* @param spaceId - Space identifier (snake_case)
|
|
16
|
+
* @returns Collection name in format Memory_{space_id}
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* getSpaceCollectionName('the_void') // Returns 'Memory_the_void'
|
|
20
|
+
*/
|
|
21
|
+
export function getSpaceCollectionName(spaceId: string): string {
|
|
22
|
+
return `Memory_${spaceId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize display name to space ID
|
|
27
|
+
*
|
|
28
|
+
* Converts display names like "The Void" to snake_case IDs like "the_void"
|
|
29
|
+
*
|
|
30
|
+
* @param displayName - Display name with spaces and mixed case
|
|
31
|
+
* @returns snake_case space ID
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* sanitizeSpaceId('The Void') // Returns 'the_void'
|
|
35
|
+
* sanitizeSpaceId('Public Space') // Returns 'public_space'
|
|
36
|
+
*/
|
|
37
|
+
export function sanitizeSpaceId(displayName: string): string {
|
|
38
|
+
return displayName.toLowerCase().replace(/\s+/g, '_');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get display name for a space ID
|
|
43
|
+
*
|
|
44
|
+
* @param spaceId - Space identifier
|
|
45
|
+
* @returns Display name or the space ID if not found
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* getSpaceDisplayName('the_void') // Returns 'The Void'
|
|
49
|
+
*/
|
|
50
|
+
export function getSpaceDisplayName(spaceId: string): string {
|
|
51
|
+
const { SPACE_DISPLAY_NAMES } = require('../types/space-memory.js');
|
|
52
|
+
return SPACE_DISPLAY_NAMES[spaceId as SpaceId] || spaceId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate space ID
|
|
57
|
+
*
|
|
58
|
+
* @param spaceId - Space identifier to validate
|
|
59
|
+
* @returns True if valid, false otherwise
|
|
60
|
+
*/
|
|
61
|
+
export function isValidSpaceId(spaceId: string): boolean {
|
|
62
|
+
return SUPPORTED_SPACES.includes(spaceId as SpaceId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a space collection with schema
|
|
67
|
+
*
|
|
68
|
+
* @param client - Weaviate client
|
|
69
|
+
* @param spaceId - Space identifier
|
|
70
|
+
*/
|
|
71
|
+
async function createSpaceCollection(
|
|
72
|
+
client: WeaviateClient,
|
|
73
|
+
spaceId: string
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const collectionName = getSpaceCollectionName(spaceId);
|
|
76
|
+
|
|
77
|
+
console.log(`[Weaviate] Creating space collection ${collectionName}...`);
|
|
78
|
+
|
|
79
|
+
// Create collection with schema (same as Memory schema but for spaces)
|
|
80
|
+
await client.collections.create({
|
|
81
|
+
name: collectionName,
|
|
82
|
+
|
|
83
|
+
// Vectorizer configuration
|
|
84
|
+
vectorizers: weaviate.configure.vectorizer.text2VecOpenAI({
|
|
85
|
+
model: 'text-embedding-3-small',
|
|
86
|
+
// Vectorize content for semantic search
|
|
87
|
+
sourceProperties: ['content', 'observation'],
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
properties: [
|
|
91
|
+
// Discriminator
|
|
92
|
+
{
|
|
93
|
+
name: 'doc_type',
|
|
94
|
+
dataType: 'text' as any,
|
|
95
|
+
description: 'Document type: "space_memory"',
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Space identity
|
|
99
|
+
{
|
|
100
|
+
name: 'space_id',
|
|
101
|
+
dataType: 'text' as any,
|
|
102
|
+
description: 'Space identifier (e.g., "the_void")',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'author_id',
|
|
106
|
+
dataType: 'text' as any,
|
|
107
|
+
description: 'Original author user_id (for permissions)',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'ghost_id',
|
|
111
|
+
dataType: 'text' as any,
|
|
112
|
+
description: 'Optional ghost profile ID for pseudonymous publishing',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'attribution',
|
|
116
|
+
dataType: 'text' as any,
|
|
117
|
+
description: 'Attribution type: "user" or "ghost"',
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Discovery metadata
|
|
121
|
+
{
|
|
122
|
+
name: 'published_at',
|
|
123
|
+
dataType: 'text' as any,
|
|
124
|
+
description: 'When published to space (ISO 8601)',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'discovery_count',
|
|
128
|
+
dataType: 'number' as any,
|
|
129
|
+
description: 'How many times discovered',
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Memory fields (same as personal memories)
|
|
133
|
+
{
|
|
134
|
+
name: 'content',
|
|
135
|
+
dataType: 'text' as any,
|
|
136
|
+
description: 'Main memory content (vectorized)',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'title',
|
|
140
|
+
dataType: 'text' as any,
|
|
141
|
+
description: 'Optional short title',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'summary',
|
|
145
|
+
dataType: 'text' as any,
|
|
146
|
+
description: 'Optional brief summary',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'type',
|
|
150
|
+
dataType: 'text' as any,
|
|
151
|
+
description: 'Content type (note, event, person, etc.)',
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Scoring fields
|
|
155
|
+
{
|
|
156
|
+
name: 'weight',
|
|
157
|
+
dataType: 'number' as any,
|
|
158
|
+
description: 'Significance/priority (0-1)',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'trust',
|
|
162
|
+
dataType: 'number' as any,
|
|
163
|
+
description: 'Access control level (0-1)',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'confidence',
|
|
167
|
+
dataType: 'number' as any,
|
|
168
|
+
description: 'System confidence in accuracy (0-1)',
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Location fields (flattened)
|
|
172
|
+
{
|
|
173
|
+
name: 'location_gps_latitude',
|
|
174
|
+
dataType: 'number' as any,
|
|
175
|
+
description: 'GPS latitude',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'location_gps_longitude',
|
|
179
|
+
dataType: 'number' as any,
|
|
180
|
+
description: 'GPS longitude',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'location_address_formatted',
|
|
184
|
+
dataType: 'text' as any,
|
|
185
|
+
description: 'Formatted address',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'location_address_city',
|
|
189
|
+
dataType: 'text' as any,
|
|
190
|
+
description: 'City',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'location_address_country',
|
|
194
|
+
dataType: 'text' as any,
|
|
195
|
+
description: 'Country',
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// Context fields (flattened)
|
|
199
|
+
{
|
|
200
|
+
name: 'context_conversation_id',
|
|
201
|
+
dataType: 'text' as any,
|
|
202
|
+
description: 'Conversation ID',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'context_platform',
|
|
206
|
+
dataType: 'text' as any,
|
|
207
|
+
description: 'Platform where created',
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// Tags and relationships
|
|
211
|
+
{
|
|
212
|
+
name: 'tags',
|
|
213
|
+
dataType: 'text[]' as any,
|
|
214
|
+
description: 'Tags for categorization',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'related_memory_ids',
|
|
218
|
+
dataType: 'text[]' as any,
|
|
219
|
+
description: 'IDs of related memories',
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// Timestamps
|
|
223
|
+
{
|
|
224
|
+
name: 'created_at',
|
|
225
|
+
dataType: 'text' as any,
|
|
226
|
+
description: 'Original creation timestamp (ISO 8601)',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'updated_at',
|
|
230
|
+
dataType: 'text' as any,
|
|
231
|
+
description: 'Last update timestamp (ISO 8601)',
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// Versioning
|
|
235
|
+
{
|
|
236
|
+
name: 'version',
|
|
237
|
+
dataType: 'number' as any,
|
|
238
|
+
description: 'Version number (increments on update)',
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
console.log(`[Weaviate] Space collection ${collectionName} created successfully`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Ensure a space collection exists, creating it if needed
|
|
248
|
+
*
|
|
249
|
+
* @param client - Weaviate client
|
|
250
|
+
* @param spaceId - Space identifier
|
|
251
|
+
* @returns Collection reference
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* const collection = await ensureSpaceCollection(client, 'the_void');
|
|
255
|
+
*/
|
|
256
|
+
export async function ensureSpaceCollection(
|
|
257
|
+
client: WeaviateClient,
|
|
258
|
+
spaceId: string
|
|
259
|
+
): Promise<Collection<any>> {
|
|
260
|
+
// Validate space ID
|
|
261
|
+
if (!isValidSpaceId(spaceId)) {
|
|
262
|
+
throw new Error(`Invalid space ID: ${spaceId}. Supported spaces: ${SUPPORTED_SPACES.join(', ')}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const collectionName = getSpaceCollectionName(spaceId);
|
|
266
|
+
|
|
267
|
+
// Check if collection exists
|
|
268
|
+
const exists = await client.collections.exists(collectionName);
|
|
269
|
+
|
|
270
|
+
if (!exists) {
|
|
271
|
+
await createSpaceCollection(client, spaceId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return client.collections.get(collectionName);
|
|
275
|
+
}
|