@prmichaelsen/remember-mcp 3.15.3 → 3.15.5

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.
Files changed (132) hide show
  1. package/AGENT.md +363 -5
  2. package/CHANGELOG.md +7 -0
  3. package/agent/commands/acp.clarification-capture.md +386 -0
  4. package/agent/commands/acp.clarification-create.md +50 -0
  5. package/agent/commands/acp.command-create.md +60 -0
  6. package/agent/commands/acp.design-create.md +62 -0
  7. package/agent/commands/acp.design-reference.md +355 -0
  8. package/agent/commands/acp.index.md +423 -0
  9. package/agent/commands/acp.init.md +48 -0
  10. package/agent/commands/acp.package-create.md +1 -0
  11. package/agent/commands/acp.package-info.md +1 -0
  12. package/agent/commands/acp.package-install.md +19 -0
  13. package/agent/commands/acp.package-list.md +1 -0
  14. package/agent/commands/acp.package-publish.md +1 -0
  15. package/agent/commands/acp.package-remove.md +1 -0
  16. package/agent/commands/acp.package-search.md +1 -0
  17. package/agent/commands/acp.package-update.md +1 -0
  18. package/agent/commands/acp.package-validate.md +1 -0
  19. package/agent/commands/acp.pattern-create.md +60 -0
  20. package/agent/commands/acp.plan.md +25 -0
  21. package/agent/commands/acp.proceed.md +621 -75
  22. package/agent/commands/acp.project-create.md +3 -0
  23. package/agent/commands/acp.project-info.md +3 -0
  24. package/agent/commands/acp.project-list.md +3 -1
  25. package/agent/commands/acp.project-set.md +1 -0
  26. package/agent/commands/acp.project-update.md +14 -3
  27. package/agent/commands/acp.projects-restore.md +228 -0
  28. package/agent/commands/acp.projects-sync.md +347 -0
  29. package/agent/commands/acp.report.md +13 -0
  30. package/agent/commands/acp.resume.md +3 -1
  31. package/agent/commands/acp.sessions.md +301 -0
  32. package/agent/commands/acp.status.md +13 -0
  33. package/agent/commands/acp.sync.md +1 -0
  34. package/agent/commands/acp.task-create.md +105 -3
  35. package/agent/commands/acp.update.md +1 -0
  36. package/agent/commands/acp.validate.md +32 -2
  37. package/agent/commands/acp.version-check-for-updates.md +1 -0
  38. package/agent/commands/acp.version-check.md +1 -0
  39. package/agent/commands/acp.version-update.md +1 -0
  40. package/agent/commands/command.template.md +23 -0
  41. package/agent/commands/git.commit.md +1 -0
  42. package/agent/commands/git.init.md +1 -0
  43. package/agent/design/complete-tool-set.md +157 -233
  44. package/agent/design/design.template.md +18 -0
  45. package/agent/design/user-preferences.md +11 -7
  46. package/agent/milestones/milestone-19-new-search-ghost-tools.md +46 -0
  47. package/agent/package.template.yaml +50 -0
  48. package/agent/patterns/pattern.template.md +18 -0
  49. package/agent/progress.yaml +162 -6
  50. package/agent/scripts/acp.common.sh +258 -15
  51. package/agent/scripts/acp.install.sh +91 -4
  52. package/agent/scripts/acp.package-create.sh +0 -1
  53. package/agent/scripts/acp.package-info.sh +19 -1
  54. package/agent/scripts/acp.package-install-optimized.sh +1 -1
  55. package/agent/scripts/acp.package-install.sh +388 -38
  56. package/agent/scripts/acp.package-list.sh +52 -4
  57. package/agent/scripts/acp.package-remove.sh +77 -1
  58. package/agent/scripts/acp.package-search.sh +2 -2
  59. package/agent/scripts/acp.package-update.sh +91 -12
  60. package/agent/scripts/acp.package-validate.sh +136 -1
  61. package/agent/scripts/acp.project-info.sh +34 -11
  62. package/agent/scripts/acp.project-list.sh +4 -0
  63. package/agent/scripts/acp.project-update.sh +66 -19
  64. package/agent/scripts/acp.projects-restore.sh +170 -0
  65. package/agent/scripts/acp.projects-sync.sh +155 -0
  66. package/agent/scripts/acp.sessions.sh +725 -0
  67. package/agent/scripts/acp.version-update.sh +21 -3
  68. package/agent/scripts/acp.yaml-parser.sh +20 -6
  69. package/agent/tasks/milestone-19-new-search-ghost-tools/task-203-create-search-by-tool.md +143 -0
  70. package/agent/tasks/milestone-19-new-search-ghost-tools/task-204-add-new-filters-existing-tools.md +77 -0
  71. package/agent/tasks/milestone-19-new-search-ghost-tools/task-205-add-feel-fields-create-update.md +137 -0
  72. package/agent/tasks/milestone-19-new-search-ghost-tools/task-206-add-byproperty-bysignificance-modes.md +135 -0
  73. package/agent/tasks/milestone-19-new-search-ghost-tools/task-207-add-emotional-composites-search-results.md +88 -0
  74. package/agent/tasks/milestone-19-new-search-ghost-tools/task-208-add-bybroad-byrandom-modes.md +115 -0
  75. package/agent/tasks/milestone-19-new-search-ghost-tools/task-209-create-ghost-memory-tools.md +192 -0
  76. package/agent/tasks/milestone-19-new-search-ghost-tools/task-210-create-get-core-tool.md +203 -0
  77. package/agent/tasks/milestone-19-new-search-ghost-tools/task-211-create-search-space-by-tool.md +182 -0
  78. package/agent/tasks/task-1-{title}.template.md +19 -0
  79. package/agent/tasks/unassigned/bug-report-remember-core-e2e-findings.md +99 -0
  80. package/dist/e2e-helpers.d.ts +26 -0
  81. package/dist/ghost-persona.e2e.d.ts +8 -0
  82. package/dist/memory-crud.e2e.d.ts +8 -0
  83. package/dist/preferences.e2e.d.ts +8 -0
  84. package/dist/relationships.e2e.d.ts +8 -0
  85. package/dist/search-modes.e2e.d.ts +8 -0
  86. package/dist/server-factory.js +2158 -45
  87. package/dist/server.js +1403 -44
  88. package/dist/shared-spaces.e2e.d.ts +8 -0
  89. package/dist/tools/create-ghost-memory.d.ts +70 -0
  90. package/dist/tools/create-memory.d.ts +175 -0
  91. package/dist/tools/get-core.d.ts +28 -0
  92. package/dist/tools/get-core.spec.d.ts +2 -0
  93. package/dist/tools/ghost-tools.spec.d.ts +2 -0
  94. package/dist/tools/query-ghost-memory.d.ts +34 -0
  95. package/dist/tools/query-memory.d.ts +4 -0
  96. package/dist/tools/search-by.d.ts +147 -0
  97. package/dist/tools/search-by.spec.d.ts +2 -0
  98. package/dist/tools/search-ghost-memory-by.d.ts +54 -0
  99. package/dist/tools/search-ghost-memory.d.ts +53 -0
  100. package/dist/tools/search-memory.d.ts +19 -0
  101. package/dist/tools/search-space-by.d.ts +78 -0
  102. package/dist/tools/search-space-by.spec.d.ts +2 -0
  103. package/dist/tools/search-space.d.ts +2 -0
  104. package/dist/tools/update-ghost-memory.d.ts +51 -0
  105. package/dist/tools/update-memory.d.ts +175 -0
  106. package/jest.e2e.config.js +11 -0
  107. package/package.json +2 -2
  108. package/src/e2e-helpers.ts +86 -0
  109. package/src/ghost-persona.e2e.ts +215 -0
  110. package/src/memory-crud.e2e.ts +203 -0
  111. package/src/preferences.e2e.ts +88 -0
  112. package/src/relationships.e2e.ts +156 -0
  113. package/src/search-modes.e2e.ts +184 -0
  114. package/src/server-factory.ts +56 -0
  115. package/src/shared-spaces.e2e.ts +204 -0
  116. package/src/tools/create-ghost-memory.ts +103 -0
  117. package/src/tools/create-memory.ts +45 -1
  118. package/src/tools/get-core.spec.ts +223 -0
  119. package/src/tools/get-core.ts +109 -0
  120. package/src/tools/ghost-tools.spec.ts +361 -0
  121. package/src/tools/query-ghost-memory.ts +63 -0
  122. package/src/tools/query-memory.ts +4 -0
  123. package/src/tools/search-by.spec.ts +325 -0
  124. package/src/tools/search-by.ts +298 -0
  125. package/src/tools/search-ghost-memory-by.ts +80 -0
  126. package/src/tools/search-ghost-memory.ts +73 -0
  127. package/src/tools/search-memory.ts +23 -0
  128. package/src/tools/search-space-by.spec.ts +289 -0
  129. package/src/tools/search-space-by.ts +173 -0
  130. package/src/tools/search-space.ts +20 -1
  131. package/src/tools/update-ghost-memory.ts +86 -0
  132. package/src/tools/update-memory.ts +45 -1
@@ -0,0 +1,73 @@
1
+ /**
2
+ * remember_search_ghost_memory tool
3
+ * Wraps search_memory with hardcoded ghost type filter
4
+ */
5
+
6
+ import { handleToolError } from '../utils/error-handler.js';
7
+ import { createDebugLogger } from '../utils/debug.js';
8
+ import type { AuthContext } from '../types/auth.js';
9
+ import { handleSearchMemory } from './search-memory.js';
10
+
11
+ export const searchGhostMemoryTool = {
12
+ name: 'remember_search_ghost_memory',
13
+ description: `Search ghost memories using hybrid semantic + keyword search.
14
+ Automatically filters to content_type: ghost. Use this to find specific
15
+ ghost interaction records.`,
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ query: { type: 'string', description: 'Search query' },
20
+ alpha: { type: 'number', minimum: 0, maximum: 1, description: 'Semantic vs keyword balance. Default: 0.7' },
21
+ tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' },
22
+ limit: { type: 'number', description: 'Max results. Default: 10' },
23
+ offset: { type: 'number' },
24
+ deleted_filter: { type: 'string', enum: ['exclude', 'include', 'only'] },
25
+ },
26
+ required: ['query'],
27
+ },
28
+ };
29
+
30
+ export interface SearchGhostMemoryArgs {
31
+ query: string;
32
+ alpha?: number;
33
+ tags?: string[];
34
+ limit?: number;
35
+ offset?: number;
36
+ deleted_filter?: 'exclude' | 'include' | 'only';
37
+ }
38
+
39
+ export async function handleSearchGhostMemory(
40
+ args: SearchGhostMemoryArgs,
41
+ userId: string,
42
+ authContext?: AuthContext
43
+ ): Promise<string> {
44
+ const debug = createDebugLogger({ tool: 'remember_search_ghost_memory', userId, operation: 'search ghost memories' });
45
+ try {
46
+ debug.info('Tool invoked');
47
+ debug.trace('Arguments', { args });
48
+
49
+ // Delegate to search_memory with ghost type filter hardcoded
50
+ return await handleSearchMemory(
51
+ {
52
+ query: args.query,
53
+ alpha: args.alpha,
54
+ limit: args.limit,
55
+ offset: args.offset,
56
+ filters: {
57
+ types: ['ghost'] as any,
58
+ tags: args.tags,
59
+ },
60
+ deleted_filter: args.deleted_filter,
61
+ },
62
+ userId,
63
+ authContext
64
+ );
65
+ } catch (error) {
66
+ debug.error('Tool failed', { error: error instanceof Error ? error.message : String(error) });
67
+ handleToolError(error, {
68
+ toolName: 'remember_search_ghost_memory',
69
+ operation: 'search ghost memories',
70
+ userId,
71
+ });
72
+ }
73
+ }
@@ -35,6 +35,12 @@ export const searchMemoryTool = {
35
35
  - "Search for recipes I saved" → returns recipe memories + related relationships
36
36
  - "Show me notes from last week" → returns notes + any relationships created that week
37
37
 
38
+ **GRAPH TRAVERSAL**: Use include_relationships: true (the default) to discover graph nodes
39
+ that link memories together. Relationships contain observations about how memories connect.
40
+ When exploring a memory in more detail, search for it and examine the returned relationships
41
+ to find related memories you can drill into — this lets you traverse the memory graph and
42
+ explore a memory's "near field" of connected knowledge.
43
+
38
44
  **AGENT GUIDANCE**:
39
45
  - ⚠️ **CRITICAL - CONTENT TYPE FILTERING**: Do NOT add filters.types unless the user explicitly requests filtering by content type.
40
46
  * ✅ CORRECT: User says "search for hiking" → { query: "hiking" }
@@ -106,6 +112,23 @@ export const searchMemoryTool = {
106
112
  type: 'string',
107
113
  description: 'End date (ISO 8601)',
108
114
  },
115
+ exclude_types: {
116
+ type: 'array',
117
+ items: { type: 'string' },
118
+ description: 'Exclude specific content types (takes precedence over types if both provided)',
119
+ },
120
+ rating_min: {
121
+ type: 'number',
122
+ description: 'Minimum Bayesian rating average',
123
+ },
124
+ relationship_count_min: {
125
+ type: 'number',
126
+ description: 'Minimum relationship count',
127
+ },
128
+ relationship_count_max: {
129
+ type: 'number',
130
+ description: 'Maximum relationship count',
131
+ },
109
132
  },
110
133
  },
111
134
  include_relationships: {
@@ -0,0 +1,289 @@
1
+ import { searchSpaceByTool, handleSearchSpaceBy } from './search-space-by.js';
2
+
3
+ const mockByDiscovery = jest.fn();
4
+ const mockByTime = jest.fn();
5
+ const mockByRating = jest.fn();
6
+ const mockByProperty = jest.fn();
7
+ const mockByBroad = jest.fn();
8
+ const mockByRandom = jest.fn();
9
+
10
+ jest.mock('../core-services.js', () => ({
11
+ createCoreServices: jest.fn(() => ({
12
+ space: {
13
+ byDiscovery: mockByDiscovery,
14
+ byTime: mockByTime,
15
+ byRating: mockByRating,
16
+ byProperty: mockByProperty,
17
+ byBroad: mockByBroad,
18
+ byRandom: mockByRandom,
19
+ },
20
+ })),
21
+ }));
22
+
23
+ jest.mock('../utils/debug.js', () => ({
24
+ createDebugLogger: () => ({
25
+ info: jest.fn(),
26
+ trace: jest.fn(),
27
+ error: jest.fn(),
28
+ }),
29
+ }));
30
+
31
+ const mockResult = {
32
+ memories: [{ id: 'pub-1', content: 'published memory' }],
33
+ total: 1,
34
+ offset: 0,
35
+ limit: 10,
36
+ };
37
+
38
+ describe('remember_search_space_by', () => {
39
+ const userId = 'test-user-1';
40
+
41
+ beforeEach(() => {
42
+ jest.clearAllMocks();
43
+ mockByDiscovery.mockResolvedValue(mockResult);
44
+ mockByTime.mockResolvedValue(mockResult);
45
+ mockByRating.mockResolvedValue(mockResult);
46
+ mockByProperty.mockResolvedValue(mockResult);
47
+ mockByBroad.mockResolvedValue(mockResult);
48
+ mockByRandom.mockResolvedValue(mockResult);
49
+ });
50
+
51
+ describe('tool definition', () => {
52
+ it('has correct name', () => {
53
+ expect(searchSpaceByTool.name).toBe('remember_search_space_by');
54
+ });
55
+
56
+ it('requires mode', () => {
57
+ expect(searchSpaceByTool.inputSchema.required).toContain('mode');
58
+ });
59
+
60
+ it('has all mode options (no byDensity)', () => {
61
+ const modeProp = (searchSpaceByTool.inputSchema.properties as any).mode;
62
+ expect(modeProp.enum).toEqual(['byTime', 'byRating', 'byDiscovery', 'byProperty', 'byBroad', 'byRandom']);
63
+ expect(modeProp.enum).not.toContain('byDensity');
64
+ });
65
+
66
+ it('has spaces and groups parameters', () => {
67
+ const props = searchSpaceByTool.inputSchema.properties as any;
68
+ expect(props.spaces).toBeDefined();
69
+ expect(props.groups).toBeDefined();
70
+ });
71
+
72
+ it('has moderation_filter and include_comments', () => {
73
+ const props = searchSpaceByTool.inputSchema.properties as any;
74
+ expect(props.moderation_filter).toBeDefined();
75
+ expect(props.include_comments).toBeDefined();
76
+ });
77
+ });
78
+
79
+ describe('validation', () => {
80
+ it('returns error when neither spaces nor groups provided', async () => {
81
+ const result = await handleSearchSpaceBy({ mode: 'byDiscovery' }, userId);
82
+ const parsed = JSON.parse(result);
83
+ expect(parsed.error).toContain('spaces');
84
+ expect(parsed.error).toContain('groups');
85
+ });
86
+
87
+ it('returns error with empty spaces array and no groups', async () => {
88
+ const result = await handleSearchSpaceBy({ mode: 'byDiscovery', spaces: [] }, userId);
89
+ const parsed = JSON.parse(result);
90
+ expect(parsed.error).toContain('spaces');
91
+ });
92
+ });
93
+
94
+ describe('byDiscovery mode', () => {
95
+ it('dispatches to space.byDiscovery', async () => {
96
+ const result = await handleSearchSpaceBy(
97
+ { mode: 'byDiscovery', spaces: ['public'] },
98
+ userId,
99
+ );
100
+ expect(mockByDiscovery).toHaveBeenCalledTimes(1);
101
+ expect(JSON.parse(result)).toEqual(mockResult);
102
+ });
103
+
104
+ it('passes spaces parameter', async () => {
105
+ await handleSearchSpaceBy({ mode: 'byDiscovery', spaces: ['space-1', 'space-2'] }, userId);
106
+ expect(mockByDiscovery).toHaveBeenCalledWith(
107
+ expect.objectContaining({ spaces: ['space-1', 'space-2'] }),
108
+ undefined,
109
+ );
110
+ });
111
+
112
+ it('passes groups parameter', async () => {
113
+ await handleSearchSpaceBy({ mode: 'byDiscovery', groups: ['group-1'] }, userId);
114
+ expect(mockByDiscovery).toHaveBeenCalledWith(
115
+ expect.objectContaining({ groups: ['group-1'] }),
116
+ undefined,
117
+ );
118
+ });
119
+
120
+ it('passes both spaces and groups', async () => {
121
+ await handleSearchSpaceBy(
122
+ { mode: 'byDiscovery', spaces: ['pub'], groups: ['grp-1'] },
123
+ userId,
124
+ );
125
+ expect(mockByDiscovery).toHaveBeenCalledWith(
126
+ expect.objectContaining({ spaces: ['pub'], groups: ['grp-1'] }),
127
+ undefined,
128
+ );
129
+ });
130
+
131
+ it('passes query through', async () => {
132
+ await handleSearchSpaceBy(
133
+ { mode: 'byDiscovery', spaces: ['pub'], query: 'explore' },
134
+ userId,
135
+ );
136
+ expect(mockByDiscovery).toHaveBeenCalledWith(
137
+ expect.objectContaining({ query: 'explore' }),
138
+ undefined,
139
+ );
140
+ });
141
+
142
+ it('passes limit and offset', async () => {
143
+ await handleSearchSpaceBy(
144
+ { mode: 'byDiscovery', spaces: ['pub'], limit: 20, offset: 5 },
145
+ userId,
146
+ );
147
+ expect(mockByDiscovery).toHaveBeenCalledWith(
148
+ expect.objectContaining({ limit: 20, offset: 5 }),
149
+ undefined,
150
+ );
151
+ });
152
+
153
+ it('defaults limit to 10 and offset to 0', async () => {
154
+ await handleSearchSpaceBy({ mode: 'byDiscovery', spaces: ['pub'] }, userId);
155
+ expect(mockByDiscovery).toHaveBeenCalledWith(
156
+ expect.objectContaining({ limit: 10, offset: 0 }),
157
+ undefined,
158
+ );
159
+ });
160
+
161
+ it('passes moderation_filter', async () => {
162
+ await handleSearchSpaceBy(
163
+ { mode: 'byDiscovery', spaces: ['pub'], moderation_filter: 'approved' },
164
+ userId,
165
+ );
166
+ expect(mockByDiscovery).toHaveBeenCalledWith(
167
+ expect.objectContaining({ moderation_filter: 'approved' }),
168
+ undefined,
169
+ );
170
+ });
171
+
172
+ it('passes include_comments', async () => {
173
+ await handleSearchSpaceBy(
174
+ { mode: 'byDiscovery', spaces: ['pub'], include_comments: true },
175
+ userId,
176
+ );
177
+ expect(mockByDiscovery).toHaveBeenCalledWith(
178
+ expect.objectContaining({ include_comments: true }),
179
+ undefined,
180
+ );
181
+ });
182
+ });
183
+
184
+ describe('byTime mode', () => {
185
+ it('dispatches to space.byTime', async () => {
186
+ const result = await handleSearchSpaceBy({ mode: 'byTime', spaces: ['pub'] }, userId);
187
+ expect(mockByTime).toHaveBeenCalledTimes(1);
188
+ expect(JSON.parse(result)).toEqual(mockResult);
189
+ });
190
+
191
+ it('passes sort_order as direction', async () => {
192
+ await handleSearchSpaceBy({ mode: 'byTime', spaces: ['pub'], sort_order: 'asc' }, userId);
193
+ expect(mockByTime).toHaveBeenCalledWith(
194
+ expect.objectContaining({ direction: 'asc' }),
195
+ undefined,
196
+ );
197
+ });
198
+
199
+ it('defaults direction to desc', async () => {
200
+ await handleSearchSpaceBy({ mode: 'byTime', spaces: ['pub'] }, userId);
201
+ expect(mockByTime).toHaveBeenCalledWith(
202
+ expect.objectContaining({ direction: 'desc' }),
203
+ undefined,
204
+ );
205
+ });
206
+ });
207
+
208
+ describe('byRating mode', () => {
209
+ it('dispatches to space.byRating', async () => {
210
+ const result = await handleSearchSpaceBy({ mode: 'byRating', spaces: ['pub'] }, userId);
211
+ expect(mockByRating).toHaveBeenCalledTimes(1);
212
+ expect(JSON.parse(result)).toEqual(mockResult);
213
+ });
214
+
215
+ it('passes sort_order as direction', async () => {
216
+ await handleSearchSpaceBy({ mode: 'byRating', spaces: ['pub'], sort_order: 'asc' }, userId);
217
+ expect(mockByRating).toHaveBeenCalledWith(
218
+ expect.objectContaining({ direction: 'asc' }),
219
+ undefined,
220
+ );
221
+ });
222
+ });
223
+
224
+ describe('byProperty mode', () => {
225
+ it('returns error when sort_field missing', async () => {
226
+ const result = await handleSearchSpaceBy({ mode: 'byProperty', spaces: ['pub'] }, userId);
227
+ const parsed = JSON.parse(result);
228
+ expect(parsed.error).toContain('sort_field is required');
229
+ });
230
+
231
+ it('dispatches to space.byProperty with sort_field', async () => {
232
+ const result = await handleSearchSpaceBy(
233
+ { mode: 'byProperty', spaces: ['pub'], sort_field: 'feel_trauma' },
234
+ userId,
235
+ );
236
+ expect(mockByProperty).toHaveBeenCalledWith(
237
+ expect.objectContaining({ sort_field: 'feel_trauma', sort_direction: 'desc' }),
238
+ undefined,
239
+ );
240
+ expect(JSON.parse(result)).toEqual(mockResult);
241
+ });
242
+
243
+ it('passes sort_order as sort_direction', async () => {
244
+ await handleSearchSpaceBy(
245
+ { mode: 'byProperty', spaces: ['pub'], sort_field: 'weight', sort_order: 'asc' },
246
+ userId,
247
+ );
248
+ expect(mockByProperty).toHaveBeenCalledWith(
249
+ expect.objectContaining({ sort_field: 'weight', sort_direction: 'asc' }),
250
+ undefined,
251
+ );
252
+ });
253
+ });
254
+
255
+ describe('byBroad mode', () => {
256
+ it('dispatches to space.byBroad', async () => {
257
+ const result = await handleSearchSpaceBy({ mode: 'byBroad', spaces: ['pub'] }, userId);
258
+ expect(mockByBroad).toHaveBeenCalledTimes(1);
259
+ expect(JSON.parse(result)).toEqual(mockResult);
260
+ });
261
+
262
+ it('passes query through', async () => {
263
+ await handleSearchSpaceBy({ mode: 'byBroad', spaces: ['pub'], query: 'explore' }, userId);
264
+ expect(mockByBroad).toHaveBeenCalledWith(
265
+ expect.objectContaining({ query: 'explore' }),
266
+ undefined,
267
+ );
268
+ });
269
+ });
270
+
271
+ describe('byRandom mode', () => {
272
+ it('dispatches to space.byRandom', async () => {
273
+ const result = await handleSearchSpaceBy({ mode: 'byRandom', spaces: ['pub'] }, userId);
274
+ expect(mockByRandom).toHaveBeenCalledTimes(1);
275
+ expect(JSON.parse(result)).toEqual(mockResult);
276
+ });
277
+ });
278
+
279
+ describe('invalid mode', () => {
280
+ it('returns error for unknown mode', async () => {
281
+ const result = await handleSearchSpaceBy(
282
+ { mode: 'byInvalid' as any, spaces: ['pub'] },
283
+ userId,
284
+ );
285
+ const parsed = JSON.parse(result);
286
+ expect(parsed.error).toContain('Unknown mode');
287
+ });
288
+ });
289
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * remember_search_space_by tool
3
+ * Unified search modes for space/group published memories
4
+ */
5
+
6
+ import { handleToolError } from '../utils/error-handler.js';
7
+ import { createDebugLogger } from '../utils/debug.js';
8
+ import type { AuthContext } from '../types/auth.js';
9
+ import { createCoreServices } from '../core-services.js';
10
+
11
+ export type SearchSpaceByMode = 'byTime' | 'byRating' | 'byDiscovery' | 'byProperty' | 'byBroad' | 'byRandom';
12
+
13
+ export const searchSpaceByTool = {
14
+ name: 'remember_search_space_by',
15
+ description: `Search shared spaces using specialized modes. Similar to remember_search_by
16
+ but operates on published memories in spaces and groups.
17
+
18
+ Modes:
19
+ - byTime: Chronological sort of published memories
20
+ - byRating: Sort by Bayesian rating average (social ratings)
21
+ - byDiscovery: Interleaved rated + unrated for exploration
22
+ - byProperty: Sort by any property (e.g., feel_trauma, total_significance)
23
+ - byBroad: Massive results with truncated content
24
+ - byRandom: Random sampling from space
25
+
26
+ At least one of 'spaces' or 'groups' must be provided.`,
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ mode: {
31
+ type: 'string',
32
+ enum: ['byTime', 'byRating', 'byDiscovery', 'byProperty', 'byBroad', 'byRandom'],
33
+ description: 'Search mode',
34
+ },
35
+ spaces: {
36
+ type: 'array',
37
+ items: { type: 'string' },
38
+ description: 'Space names to search',
39
+ },
40
+ groups: {
41
+ type: 'array',
42
+ items: { type: 'string' },
43
+ description: 'Group IDs to search',
44
+ },
45
+ query: { type: 'string', description: 'Optional search query' },
46
+ sort_order: {
47
+ type: 'string',
48
+ enum: ['asc', 'desc'],
49
+ description: 'Sort order. Default: desc',
50
+ },
51
+ sort_field: {
52
+ type: 'string',
53
+ description: 'Property to sort by (byProperty mode only)',
54
+ },
55
+ limit: { type: 'number', description: 'Max results. Default: 10 (byBroad: 50)' },
56
+ offset: { type: 'number', description: 'Pagination offset' },
57
+ moderation_filter: {
58
+ type: 'string',
59
+ description: 'Moderation filter level',
60
+ },
61
+ include_comments: {
62
+ type: 'boolean',
63
+ description: 'Include comments on published memories. Default: false',
64
+ },
65
+ },
66
+ required: ['mode'],
67
+ },
68
+ };
69
+
70
+ export interface SearchSpaceByArgs {
71
+ mode: SearchSpaceByMode;
72
+ spaces?: string[];
73
+ groups?: string[];
74
+ query?: string;
75
+ sort_order?: 'asc' | 'desc';
76
+ sort_field?: string;
77
+ limit?: number;
78
+ offset?: number;
79
+ moderation_filter?: string;
80
+ include_comments?: boolean;
81
+ }
82
+
83
+ export async function handleSearchSpaceBy(
84
+ args: SearchSpaceByArgs,
85
+ userId: string,
86
+ authContext?: AuthContext
87
+ ): Promise<string> {
88
+ const debug = createDebugLogger({ tool: 'remember_search_space_by', userId, operation: `space search by ${args.mode}` });
89
+ try {
90
+ debug.info('Tool invoked');
91
+ debug.trace('Arguments', { args });
92
+
93
+ // Validate at least one of spaces or groups is provided
94
+ if (!args.spaces?.length && !args.groups?.length) {
95
+ return JSON.stringify({
96
+ error: 'At least one of "spaces" or "groups" must be provided.',
97
+ });
98
+ }
99
+
100
+ const { space } = createCoreServices(userId);
101
+
102
+ const baseParams = {
103
+ spaces: args.spaces,
104
+ groups: args.groups,
105
+ query: args.query,
106
+ limit: args.limit ?? 10,
107
+ offset: args.offset ?? 0,
108
+ moderation_filter: args.moderation_filter,
109
+ include_comments: args.include_comments,
110
+ };
111
+
112
+ let result;
113
+ switch (args.mode) {
114
+ case 'byDiscovery':
115
+ result = await space.byDiscovery(baseParams as any, authContext as any);
116
+ break;
117
+
118
+ case 'byTime':
119
+ result = await space.byTime({
120
+ ...baseParams,
121
+ direction: args.sort_order ?? 'desc',
122
+ } as any, authContext as any);
123
+ break;
124
+
125
+ case 'byRating':
126
+ result = await space.byRating({
127
+ ...baseParams,
128
+ direction: args.sort_order ?? 'desc',
129
+ } as any, authContext as any);
130
+ break;
131
+
132
+ case 'byProperty': {
133
+ if (!args.sort_field) {
134
+ return JSON.stringify({
135
+ error: 'sort_field is required for byProperty mode. Provide a property name (e.g., "feel_trauma", "total_significance").',
136
+ });
137
+ }
138
+ result = await space.byProperty({
139
+ ...baseParams,
140
+ sort_field: args.sort_field,
141
+ sort_direction: args.sort_order ?? 'desc',
142
+ } as any, authContext as any);
143
+ break;
144
+ }
145
+
146
+ case 'byBroad':
147
+ result = await space.byBroad({
148
+ ...baseParams,
149
+ query: args.query,
150
+ sort_order: args.sort_order,
151
+ } as any, authContext as any);
152
+ break;
153
+
154
+ case 'byRandom':
155
+ result = await space.byRandom(baseParams as any, authContext as any);
156
+ break;
157
+
158
+ default:
159
+ return JSON.stringify({
160
+ error: `Unknown mode: ${(args as any).mode}. Valid modes: byTime, byRating, byDiscovery, byProperty, byBroad, byRandom`,
161
+ });
162
+ }
163
+
164
+ return JSON.stringify(result, null, 2);
165
+ } catch (error) {
166
+ debug.error('Tool failed', { error: error instanceof Error ? error.message : String(error) });
167
+ handleToolError(error, {
168
+ toolName: 'remember_search_space_by',
169
+ operation: `space search by ${args.mode}`,
170
+ userId,
171
+ });
172
+ }
173
+ }
@@ -28,6 +28,11 @@ Destinations:
28
28
 
29
29
  Results from multiple sources are merged and deduplicated by composite ID, sorted by relevance.
30
30
 
31
+ **GRAPH TRAVERSAL**: To explore a space memory's connections, take the memory's source ID and
32
+ use remember_search_memory with include_relationships: true to find graph nodes linking to it.
33
+ This lets you traverse the original author's memory graph to discover related context, connected
34
+ topics, and the memory's "near field" of knowledge.
35
+
31
36
  ⚠️ **CRITICAL - CONTENT TYPE FILTERING**: Do NOT add content_type filter unless the user explicitly requests filtering by type.
32
37
  - ✅ CORRECT: User says "search The Void for hiking" → { spaces: ["the_void"], query: "hiking" }
33
38
  - ❌ WRONG: User says "search The Void for hiking" → { spaces: ["the_void"], query: "hiking", content_type: "note" }
@@ -91,6 +96,15 @@ Let the search algorithm find ALL relevant memories regardless of type unless ex
91
96
  type: 'string',
92
97
  description: 'Filter memories created before this date (ISO 8601)',
93
98
  },
99
+ exclude_types: {
100
+ type: 'array',
101
+ items: { type: 'string' },
102
+ description: 'Exclude specific content types',
103
+ },
104
+ rating_min: {
105
+ type: 'number',
106
+ description: 'Minimum Bayesian rating average',
107
+ },
94
108
  moderation_filter: {
95
109
  type: 'string',
96
110
  enum: ['approved', 'pending', 'rejected', 'removed', 'all'],
@@ -161,11 +175,13 @@ interface SearchSpaceArgs {
161
175
  groups?: string[];
162
176
  search_type?: 'hybrid' | 'bm25' | 'semantic';
163
177
  content_type?: string;
178
+ exclude_types?: string[];
164
179
  tags?: string[];
165
180
  min_weight?: number;
166
181
  max_weight?: number;
167
182
  date_from?: string;
168
183
  date_to?: string;
184
+ rating_min?: number;
169
185
  moderation_filter?: ModerationFilter;
170
186
  include_comments?: boolean;
171
187
  limit?: number;
@@ -207,7 +223,10 @@ export async function handleSearchSpace(
207
223
  include_comments: args.include_comments,
208
224
  limit: args.limit,
209
225
  offset: args.offset,
210
- },
226
+ // New filters — passed through when core supports them
227
+ ...(args.exclude_types && { exclude_types: args.exclude_types }),
228
+ ...(args.rating_min !== undefined && { rating_min: args.rating_min }),
229
+ } as any,
211
230
  authContext as any
212
231
  );
213
232