@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.
- package/AGENT.md +363 -5
- package/CHANGELOG.md +7 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +50 -0
- package/agent/commands/acp.command-create.md +60 -0
- package/agent/commands/acp.design-create.md +62 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +48 -0
- package/agent/commands/acp.package-create.md +1 -0
- package/agent/commands/acp.package-info.md +1 -0
- package/agent/commands/acp.package-install.md +19 -0
- package/agent/commands/acp.package-list.md +1 -0
- package/agent/commands/acp.package-publish.md +1 -0
- package/agent/commands/acp.package-remove.md +1 -0
- package/agent/commands/acp.package-search.md +1 -0
- package/agent/commands/acp.package-update.md +1 -0
- package/agent/commands/acp.package-validate.md +1 -0
- package/agent/commands/acp.pattern-create.md +60 -0
- package/agent/commands/acp.plan.md +25 -0
- package/agent/commands/acp.proceed.md +621 -75
- package/agent/commands/acp.project-create.md +3 -0
- package/agent/commands/acp.project-info.md +3 -0
- package/agent/commands/acp.project-list.md +3 -1
- package/agent/commands/acp.project-set.md +1 -0
- package/agent/commands/acp.project-update.md +14 -3
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +13 -0
- package/agent/commands/acp.resume.md +3 -1
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +13 -0
- package/agent/commands/acp.sync.md +1 -0
- package/agent/commands/acp.task-create.md +105 -3
- package/agent/commands/acp.update.md +1 -0
- package/agent/commands/acp.validate.md +32 -2
- package/agent/commands/acp.version-check-for-updates.md +1 -0
- package/agent/commands/acp.version-check.md +1 -0
- package/agent/commands/acp.version-update.md +1 -0
- package/agent/commands/command.template.md +23 -0
- package/agent/commands/git.commit.md +1 -0
- package/agent/commands/git.init.md +1 -0
- package/agent/design/complete-tool-set.md +157 -233
- package/agent/design/design.template.md +18 -0
- package/agent/design/user-preferences.md +11 -7
- package/agent/milestones/milestone-19-new-search-ghost-tools.md +46 -0
- package/agent/package.template.yaml +50 -0
- package/agent/patterns/pattern.template.md +18 -0
- package/agent/progress.yaml +162 -6
- package/agent/scripts/acp.common.sh +258 -15
- package/agent/scripts/acp.install.sh +91 -4
- package/agent/scripts/acp.package-create.sh +0 -1
- package/agent/scripts/acp.package-info.sh +19 -1
- package/agent/scripts/acp.package-install-optimized.sh +1 -1
- package/agent/scripts/acp.package-install.sh +388 -38
- package/agent/scripts/acp.package-list.sh +52 -4
- package/agent/scripts/acp.package-remove.sh +77 -1
- package/agent/scripts/acp.package-search.sh +2 -2
- package/agent/scripts/acp.package-update.sh +91 -12
- package/agent/scripts/acp.package-validate.sh +136 -1
- package/agent/scripts/acp.project-info.sh +34 -11
- package/agent/scripts/acp.project-list.sh +4 -0
- package/agent/scripts/acp.project-update.sh +66 -19
- package/agent/scripts/acp.projects-restore.sh +170 -0
- package/agent/scripts/acp.projects-sync.sh +155 -0
- package/agent/scripts/acp.sessions.sh +725 -0
- package/agent/scripts/acp.version-update.sh +21 -3
- package/agent/scripts/acp.yaml-parser.sh +20 -6
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-203-create-search-by-tool.md +143 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-204-add-new-filters-existing-tools.md +77 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-205-add-feel-fields-create-update.md +137 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-206-add-byproperty-bysignificance-modes.md +135 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-207-add-emotional-composites-search-results.md +88 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-208-add-bybroad-byrandom-modes.md +115 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-209-create-ghost-memory-tools.md +192 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-210-create-get-core-tool.md +203 -0
- package/agent/tasks/milestone-19-new-search-ghost-tools/task-211-create-search-space-by-tool.md +182 -0
- package/agent/tasks/task-1-{title}.template.md +19 -0
- package/agent/tasks/unassigned/bug-report-remember-core-e2e-findings.md +99 -0
- package/dist/e2e-helpers.d.ts +26 -0
- package/dist/ghost-persona.e2e.d.ts +8 -0
- package/dist/memory-crud.e2e.d.ts +8 -0
- package/dist/preferences.e2e.d.ts +8 -0
- package/dist/relationships.e2e.d.ts +8 -0
- package/dist/search-modes.e2e.d.ts +8 -0
- package/dist/server-factory.js +2158 -45
- package/dist/server.js +1403 -44
- package/dist/shared-spaces.e2e.d.ts +8 -0
- package/dist/tools/create-ghost-memory.d.ts +70 -0
- package/dist/tools/create-memory.d.ts +175 -0
- package/dist/tools/get-core.d.ts +28 -0
- package/dist/tools/get-core.spec.d.ts +2 -0
- package/dist/tools/ghost-tools.spec.d.ts +2 -0
- package/dist/tools/query-ghost-memory.d.ts +34 -0
- package/dist/tools/query-memory.d.ts +4 -0
- package/dist/tools/search-by.d.ts +147 -0
- package/dist/tools/search-by.spec.d.ts +2 -0
- package/dist/tools/search-ghost-memory-by.d.ts +54 -0
- package/dist/tools/search-ghost-memory.d.ts +53 -0
- package/dist/tools/search-memory.d.ts +19 -0
- package/dist/tools/search-space-by.d.ts +78 -0
- package/dist/tools/search-space-by.spec.d.ts +2 -0
- package/dist/tools/search-space.d.ts +2 -0
- package/dist/tools/update-ghost-memory.d.ts +51 -0
- package/dist/tools/update-memory.d.ts +175 -0
- package/jest.e2e.config.js +11 -0
- package/package.json +2 -2
- package/src/e2e-helpers.ts +86 -0
- package/src/ghost-persona.e2e.ts +215 -0
- package/src/memory-crud.e2e.ts +203 -0
- package/src/preferences.e2e.ts +88 -0
- package/src/relationships.e2e.ts +156 -0
- package/src/search-modes.e2e.ts +184 -0
- package/src/server-factory.ts +56 -0
- package/src/shared-spaces.e2e.ts +204 -0
- package/src/tools/create-ghost-memory.ts +103 -0
- package/src/tools/create-memory.ts +45 -1
- package/src/tools/get-core.spec.ts +223 -0
- package/src/tools/get-core.ts +109 -0
- package/src/tools/ghost-tools.spec.ts +361 -0
- package/src/tools/query-ghost-memory.ts +63 -0
- package/src/tools/query-memory.ts +4 -0
- package/src/tools/search-by.spec.ts +325 -0
- package/src/tools/search-by.ts +298 -0
- package/src/tools/search-ghost-memory-by.ts +80 -0
- package/src/tools/search-ghost-memory.ts +73 -0
- package/src/tools/search-memory.ts +23 -0
- package/src/tools/search-space-by.spec.ts +289 -0
- package/src/tools/search-space-by.ts +173 -0
- package/src/tools/search-space.ts +20 -1
- package/src/tools/update-ghost-memory.ts +86 -0
- 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
|
|