@prmichaelsen/remember-mcp 3.15.4 → 3.15.6

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 +1977 -100
  87. package/dist/server.js +1174 -51
  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
@@ -44,6 +44,18 @@ import { searchSpaceTool, handleSearchSpace } from './tools/search-space.js';
44
44
  import { querySpaceTool, handleQuerySpace } from './tools/query-space.js';
45
45
  import { moderateTool, handleModerate } from './tools/moderate.js';
46
46
  import { ghostConfigTool, handleGhostConfig } from './tools/ghost-config.js';
47
+ import { searchByTool, handleSearchBy } from './tools/search-by.js';
48
+
49
+ // Import ghost memory tools
50
+ import { createGhostMemoryTool, handleCreateGhostMemory } from './tools/create-ghost-memory.js';
51
+ import { updateGhostMemoryTool, handleUpdateGhostMemory } from './tools/update-ghost-memory.js';
52
+ import { searchGhostMemoryTool, handleSearchGhostMemory } from './tools/search-ghost-memory.js';
53
+ import { queryGhostMemoryTool, handleQueryGhostMemory } from './tools/query-ghost-memory.js';
54
+ import { searchGhostMemoryByTool, handleSearchGhostMemoryBy } from './tools/search-ghost-memory-by.js';
55
+
56
+ // Import core introspection tools
57
+ import { getCoreTool, handleGetCore } from './tools/get-core.js';
58
+ import { searchSpaceByTool, handleSearchSpaceBy } from './tools/search-space-by.js';
47
59
 
48
60
  // Import services (static — avoids dynamic import overhead on hot path)
49
61
  import { getGhostConfig } from './services/ghost-config.service.js';
@@ -237,6 +249,18 @@ function registerHandlers(
237
249
  querySpaceTool,
238
250
  moderateTool,
239
251
  ghostConfigTool,
252
+ // Search modes
253
+ searchByTool,
254
+ // Ghost memory tools
255
+ createGhostMemoryTool,
256
+ updateGhostMemoryTool,
257
+ searchGhostMemoryTool,
258
+ queryGhostMemoryTool,
259
+ searchGhostMemoryByTool,
260
+ // Core introspection
261
+ getCoreTool,
262
+ // Space search modes
263
+ searchSpaceByTool,
240
264
  ],
241
265
  };
242
266
  });
@@ -337,6 +361,38 @@ function registerHandlers(
337
361
  result = await handleGhostConfig(args as any, userId, authContext);
338
362
  break;
339
363
 
364
+ case 'remember_search_by':
365
+ result = await handleSearchBy(args as any, userId, authContext);
366
+ break;
367
+
368
+ case 'remember_create_ghost_memory':
369
+ result = await handleCreateGhostMemory(args as any, userId, authContext);
370
+ break;
371
+
372
+ case 'remember_update_ghost_memory':
373
+ result = await handleUpdateGhostMemory(args as any, userId, authContext);
374
+ break;
375
+
376
+ case 'remember_search_ghost_memory':
377
+ result = await handleSearchGhostMemory(args as any, userId, authContext);
378
+ break;
379
+
380
+ case 'remember_query_ghost_memory':
381
+ result = await handleQueryGhostMemory(args as any, userId, authContext);
382
+ break;
383
+
384
+ case 'remember_search_ghost_memory_by':
385
+ result = await handleSearchGhostMemoryBy(args as any, userId, authContext);
386
+ break;
387
+
388
+ case 'remember_get_core':
389
+ result = await handleGetCore(args as any, userId, authContext);
390
+ break;
391
+
392
+ case 'remember_search_space_by':
393
+ result = await handleSearchSpaceBy(args as any, userId, authContext);
394
+ break;
395
+
340
396
  default:
341
397
  throw new McpError(
342
398
  ErrorCode.MethodNotFound,
@@ -0,0 +1,204 @@
1
+ /**
2
+ * E2E: Shared Spaces — publish/confirm/deny/search/query/revise/retract
3
+ *
4
+ * Run with:
5
+ * DOTENV_CONFIG_PATH=.env.e1.local npm run test:e2e -- --testPathPattern=shared-spaces
6
+ */
7
+
8
+ import {
9
+ e2eInit, e2eUserId, e2eAuthContext, e2eEnsureCollection,
10
+ e2eCleanup, e2eDeleteCollection, parseResult, waitForIndex,
11
+ } from './e2e-helpers.js';
12
+ import { handleCreateMemory } from './tools/create-memory.js';
13
+ import { handleUpdateMemory } from './tools/update-memory.js';
14
+ import { handlePublish } from './tools/publish.js';
15
+ import { handleConfirm } from './tools/confirm.js';
16
+ import { handleDeny } from './tools/deny.js';
17
+ import { handleSearchSpace } from './tools/search-space.js';
18
+ import { handleQuerySpace } from './tools/query-space.js';
19
+ import { handleRevise } from './tools/revise.js';
20
+ import { handleRetract } from './tools/retract.js';
21
+
22
+ const userId = e2eUserId('space');
23
+ const auth = e2eAuthContext();
24
+ const SPACE = 'the_void';
25
+ const SPACE_COLLECTION = 'Memory_spaces_public';
26
+
27
+ describe('E2E: Shared Spaces', () => {
28
+ let memoryId: string;
29
+ let publishToken: string;
30
+
31
+ beforeAll(async () => {
32
+ await e2eInit();
33
+ await e2eEnsureCollection(userId);
34
+
35
+ // Create a memory to publish
36
+ const res = parseResult(await handleCreateMemory(
37
+ { content: 'The best sourdough starter tip: feed with rye flour for extra tang.', weight: 0.8, tags: ['sourdough', 'baking', 'tips'] },
38
+ userId, auth,
39
+ ));
40
+ memoryId = res.memory_id;
41
+ await waitForIndex();
42
+ }, 30_000);
43
+
44
+ afterAll(async () => {
45
+ await e2eCleanup(userId);
46
+ // Note: we don't delete the shared space collection as other tests may use it
47
+ }, 30_000);
48
+
49
+ // -------------------------------------------------------------------------
50
+ // DENY flow (publish → deny)
51
+ // -------------------------------------------------------------------------
52
+
53
+ it('publishes a memory and then denies it', async () => {
54
+ const pubRes = parseResult(await handlePublish(
55
+ { memory_id: memoryId, spaces: [SPACE] },
56
+ userId, auth,
57
+ ));
58
+ expect(pubRes.success).toBe(true);
59
+ expect(pubRes.token).toBeTruthy();
60
+
61
+ const denyRes = parseResult(await handleDeny(
62
+ { token: pubRes.token },
63
+ userId, auth,
64
+ ));
65
+ expect(denyRes.success).toBe(true);
66
+ console.log(' denied publish');
67
+ });
68
+
69
+ // -------------------------------------------------------------------------
70
+ // PUBLISH + CONFIRM
71
+ // -------------------------------------------------------------------------
72
+
73
+ it('publishes a memory to the_void', async () => {
74
+ const res = parseResult(await handlePublish(
75
+ { memory_id: memoryId, spaces: [SPACE] },
76
+ userId, auth,
77
+ ));
78
+ expect(res.success).toBe(true);
79
+ publishToken = res.token;
80
+ });
81
+
82
+ it('confirms the publish', async () => {
83
+ const res = parseResult(await handleConfirm(
84
+ { token: publishToken },
85
+ userId, auth,
86
+ ));
87
+ expect(res.success).toBe(true);
88
+ expect(res.composite_id).toBeTruthy();
89
+ expect(res.published_to).toBeDefined();
90
+ console.log(` published as ${res.composite_id}`);
91
+ });
92
+
93
+ // -------------------------------------------------------------------------
94
+ // SEARCH SPACE
95
+ // -------------------------------------------------------------------------
96
+
97
+ it('finds published memory via search_space', async () => {
98
+ await waitForIndex();
99
+
100
+ const res = parseResult(await handleSearchSpace(
101
+ { query: 'sourdough rye flour starter', spaces: [SPACE], limit: 10 },
102
+ userId, auth,
103
+ ));
104
+
105
+ expect(res.memories).toBeDefined();
106
+ expect(res.memories.length).toBeGreaterThan(0);
107
+ const contents = res.memories.map((m: any) => m.content || m.properties?.content);
108
+ const found = contents.some((c: string) => c && c.includes('sourdough'));
109
+ expect(found).toBe(true);
110
+ console.log(` search_space returned ${res.memories.length} results`);
111
+ });
112
+
113
+ // -------------------------------------------------------------------------
114
+ // QUERY SPACE (RAG)
115
+ // -------------------------------------------------------------------------
116
+
117
+ it('query_space returns relevant results', async () => {
118
+ const res = parseResult(await handleQuerySpace(
119
+ { question: 'How do I make my sourdough more tangy?', spaces: [SPACE], limit: 5 },
120
+ userId, auth,
121
+ ));
122
+
123
+ expect(res.memories).toBeDefined();
124
+ expect(res.memories.length).toBeGreaterThan(0);
125
+ });
126
+
127
+ // -------------------------------------------------------------------------
128
+ // REVISE
129
+ // -------------------------------------------------------------------------
130
+
131
+ it('updates source memory then revises published copies', async () => {
132
+ // Update the source memory
133
+ await handleUpdateMemory(
134
+ { memory_id: memoryId, content: 'The best sourdough starter tip: feed with rye flour for extra tang. Also, keep it at 78F for best activity.' },
135
+ userId, auth,
136
+ );
137
+ await waitForIndex();
138
+
139
+ // Request revise
140
+ const revRes = parseResult(await handleRevise(
141
+ { memory_id: memoryId },
142
+ userId, auth,
143
+ ));
144
+ expect(revRes.token).toBeTruthy();
145
+
146
+ // Confirm revise
147
+ const confirmRes = parseResult(await handleConfirm(
148
+ { token: revRes.token },
149
+ userId, auth,
150
+ ));
151
+ expect(confirmRes.success).toBe(true);
152
+ console.log(' revised published copy');
153
+ });
154
+
155
+ it('revised content appears in space search', async () => {
156
+ await waitForIndex();
157
+
158
+ const res = parseResult(await handleSearchSpace(
159
+ { query: 'sourdough 78F temperature activity', spaces: [SPACE], limit: 10 },
160
+ userId, auth,
161
+ ));
162
+
163
+ const found = res.memories.some((m: any) => {
164
+ const content = m.content || m.properties?.content || '';
165
+ return content.includes('78F');
166
+ });
167
+ expect(found).toBe(true);
168
+ });
169
+
170
+ // -------------------------------------------------------------------------
171
+ // RETRACT
172
+ // -------------------------------------------------------------------------
173
+
174
+ it('retracts the memory from the_void', async () => {
175
+ const retRes = parseResult(await handleRetract(
176
+ { memory_id: memoryId, spaces: [SPACE] },
177
+ userId, auth,
178
+ ));
179
+ expect(retRes.token).toBeTruthy();
180
+
181
+ const confirmRes = parseResult(await handleConfirm(
182
+ { token: retRes.token },
183
+ userId, auth,
184
+ ));
185
+ expect(confirmRes.success).toBe(true);
186
+ console.log(' retracted from the_void');
187
+ });
188
+
189
+ it('retracted memory no longer appears in space search', async () => {
190
+ await waitForIndex();
191
+
192
+ const res = parseResult(await handleSearchSpace(
193
+ { query: 'sourdough rye flour starter', spaces: [SPACE], limit: 10 },
194
+ userId, auth,
195
+ ));
196
+
197
+ // Our specific memory should not be found (or the space may be empty)
198
+ const found = res.memories?.some((m: any) => {
199
+ const content = m.content || m.properties?.content || '';
200
+ return content.includes('sourdough') && content.includes('78F');
201
+ });
202
+ expect(found).toBeFalsy();
203
+ });
204
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * remember_create_ghost_memory tool
3
+ * Creates a ghost memory with hardcoded content_type and ghost-specific tags
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 const createGhostMemoryTool = {
12
+ name: 'remember_create_ghost_memory',
13
+ description: `Create a ghost memory (cross-user interaction record).
14
+
15
+ Ghost memories track what happened during ghost conversations — observations,
16
+ impressions, and insights about the accessor. Automatically sets content_type
17
+ to 'ghost' and adds ghost-specific tags.
18
+
19
+ Ghost memories are excluded from default searches. They are only visible when
20
+ explicitly searching with content_type: 'ghost' or using ghost memory tools.`,
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ content: { type: 'string', description: 'Ghost memory content' },
25
+ title: { type: 'string', description: 'Optional title' },
26
+ tags: { type: 'array', items: { type: 'string' }, description: 'Additional tags (ghost-specific tags added automatically)' },
27
+ weight: { type: 'number', minimum: 0, maximum: 1, description: 'Significance (0-1)' },
28
+ trust: { type: 'number', minimum: 0, maximum: 1, description: 'Trust level (0-1)' },
29
+ feel_salience: { type: 'number', minimum: 0, maximum: 1, description: 'How unexpected/novel (0-1)' },
30
+ feel_social_weight: { type: 'number', minimum: 0, maximum: 1, description: 'Relationship/reputation impact (0-1)' },
31
+ feel_narrative_importance: { type: 'number', minimum: 0, maximum: 1, description: 'Story arc importance (0-1)' },
32
+ },
33
+ required: ['content'],
34
+ },
35
+ };
36
+
37
+ export interface CreateGhostMemoryArgs {
38
+ content: string;
39
+ title?: string;
40
+ tags?: string[];
41
+ weight?: number;
42
+ trust?: number;
43
+ [key: string]: any;
44
+ }
45
+
46
+ export async function handleCreateGhostMemory(
47
+ args: CreateGhostMemoryArgs,
48
+ userId: string,
49
+ authContext?: AuthContext
50
+ ): Promise<string> {
51
+ const debug = createDebugLogger({ tool: 'remember_create_ghost_memory', userId, operation: 'create ghost memory' });
52
+ try {
53
+ debug.info('Tool invoked');
54
+ debug.trace('Arguments', { args });
55
+
56
+ const { memory } = createCoreServices(userId);
57
+
58
+ // Build ghost-specific tags
59
+ const accessorUserId = authContext?.ghostMode?.accessor_user_id;
60
+ const ghostTags = ['ghost'];
61
+ if (accessorUserId) {
62
+ ghostTags.push(`ghost:${accessorUserId}`);
63
+ }
64
+
65
+ // Merge user tags with ghost tags (avoid duplicates)
66
+ const userTags = args.tags ?? [];
67
+ const mergedTags = [...new Set([...ghostTags, ...userTags])];
68
+
69
+ // Extract feel_* fields
70
+ const feelFields: Record<string, number> = {};
71
+ for (const [key, value] of Object.entries(args)) {
72
+ if (key.startsWith('feel_') && typeof value === 'number') {
73
+ feelFields[key] = value;
74
+ }
75
+ }
76
+
77
+ const result = await memory.create({
78
+ content: args.content,
79
+ title: args.title,
80
+ type: 'ghost' as any,
81
+ weight: args.weight,
82
+ trust: args.trust,
83
+ tags: mergedTags,
84
+ context_summary: 'Ghost memory created via MCP',
85
+ ...feelFields,
86
+ } as any);
87
+
88
+ return JSON.stringify({
89
+ memory_id: result.memory_id,
90
+ created_at: result.created_at,
91
+ content_type: 'ghost',
92
+ tags: mergedTags,
93
+ message: `Ghost memory created successfully with ID: ${result.memory_id}`,
94
+ }, null, 2);
95
+ } catch (error) {
96
+ debug.error('Tool failed', { error: error instanceof Error ? error.message : String(error) });
97
+ handleToolError(error, {
98
+ toolName: 'remember_create_ghost_memory',
99
+ operation: 'create ghost memory',
100
+ userId,
101
+ });
102
+ }
103
+ }
@@ -95,6 +95,38 @@ export const createMemoryTool = {
95
95
  description: 'Per-space moderation flags (format: "{space_id}:{flag_type}"). Usually empty.',
96
96
  default: [],
97
97
  },
98
+ // Emotional dimensions — create-time hints, REM re-scores authoritatively
99
+ // Layer 1: Discrete Emotions
100
+ feel_emotional_significance: { type: 'number', minimum: 0, maximum: 1, description: 'Overall emotional weight (0-1). REM re-scores.' },
101
+ feel_vulnerability: { type: 'number', minimum: 0, maximum: 1, description: 'Personal exposure/openness (0-1). REM re-scores.' },
102
+ feel_trauma: { type: 'number', minimum: 0, maximum: 1, description: 'Negative formative experience intensity (0-1). REM re-scores.' },
103
+ feel_humor: { type: 'number', minimum: 0, maximum: 1, description: 'Comedic/playful quality (0-1). REM re-scores.' },
104
+ feel_happiness: { type: 'number', minimum: 0, maximum: 1, description: 'Positive affect / joy (0-1). REM re-scores.' },
105
+ feel_sadness: { type: 'number', minimum: 0, maximum: 1, description: 'Negative affect / grief / loss (0-1). REM re-scores.' },
106
+ feel_fear: { type: 'number', minimum: 0, maximum: 1, description: 'Threat perception / anxiety (0-1). REM re-scores.' },
107
+ feel_anger: { type: 'number', minimum: 0, maximum: 1, description: 'Frustration / injustice (0-1). REM re-scores.' },
108
+ feel_surprise: { type: 'number', minimum: 0, maximum: 1, description: 'Unexpectedness / novelty (0-1). REM re-scores.' },
109
+ feel_disgust: { type: 'number', minimum: 0, maximum: 1, description: 'Aversion / rejection (0-1). REM re-scores.' },
110
+ feel_contempt: { type: 'number', minimum: 0, maximum: 1, description: 'Superiority / dismissal (0-1). REM re-scores.' },
111
+ feel_embarrassment: { type: 'number', minimum: 0, maximum: 1, description: 'Social discomfort (0-1). REM re-scores.' },
112
+ feel_shame: { type: 'number', minimum: 0, maximum: 1, description: 'Deep self-judgment (0-1). REM re-scores.' },
113
+ feel_guilt: { type: 'number', minimum: 0, maximum: 1, description: 'Responsibility for harm (0-1). REM re-scores.' },
114
+ feel_excitement: { type: 'number', minimum: 0, maximum: 1, description: 'Anticipatory positive arousal (0-1). REM re-scores.' },
115
+ feel_pride: { type: 'number', minimum: 0, maximum: 1, description: 'Accomplishment / self-evaluation (0-1). REM re-scores.' },
116
+ feel_valence: { type: 'number', minimum: -1, maximum: 1, description: 'Positive-negative spectrum (-1 to 1). REM re-scores.' },
117
+ feel_arousal: { type: 'number', minimum: 0, maximum: 1, description: 'Calm to excited (0-1). REM re-scores.' },
118
+ feel_dominance: { type: 'number', minimum: 0, maximum: 1, description: 'Control vs submission (0-1). REM re-scores.' },
119
+ feel_intensity: { type: 'number', minimum: 0, maximum: 1, description: 'Overall emotional magnitude (0-1). REM re-scores.' },
120
+ feel_coherence_tension: { type: 'number', minimum: 0, maximum: 1, description: 'Conflict with existing beliefs (0-1). REM re-scores.' },
121
+ // Layer 2: Functional Signals
122
+ feel_salience: { type: 'number', minimum: 0, maximum: 1, description: 'How unexpected/novel — prediction error (0-1). REM re-scores.' },
123
+ feel_urgency: { type: 'number', minimum: 0, maximum: 1, description: 'Time-sensitivity of relevance (0-1). REM re-scores.' },
124
+ feel_social_weight: { type: 'number', minimum: 0, maximum: 1, description: 'Relationship/reputation impact (0-1). REM re-scores.' },
125
+ feel_agency: { type: 'number', minimum: 0, maximum: 1, description: 'Caused by the bot\'s own actions? (0-1). REM re-scores.' },
126
+ feel_novelty: { type: 'number', minimum: 0, maximum: 1, description: 'Uniqueness relative to collection (0-1). REM re-scores.' },
127
+ feel_retrieval_utility: { type: 'number', minimum: 0, maximum: 1, description: 'Likelihood of future usefulness (0-1). REM re-scores.' },
128
+ feel_narrative_importance: { type: 'number', minimum: 0, maximum: 1, description: 'Advances/anchors a personal story arc (0-1). REM re-scores.' },
129
+ feel_aesthetic_quality: { type: 'number', minimum: 0, maximum: 1, description: 'Beauty, craft, artistry (0-1). REM re-scores.' },
98
130
  },
99
131
  required: ['content'],
100
132
  },
@@ -118,6 +150,8 @@ export interface CreateMemoryArgs {
118
150
  parent_id?: string | null;
119
151
  thread_root_id?: string | null;
120
152
  moderation_flags?: string[];
153
+ // Emotional dimensions (feel_* fields)
154
+ [key: string]: any;
121
155
  }
122
156
 
123
157
  /**
@@ -144,6 +178,15 @@ export async function handleCreateMemory(
144
178
  debug.trace('Arguments', { args });
145
179
 
146
180
  const { memory } = createCoreServices(userId);
181
+
182
+ // Extract feel_* fields from args
183
+ const feelFields: Record<string, number> = {};
184
+ for (const [key, value] of Object.entries(args)) {
185
+ if (key.startsWith('feel_') && typeof value === 'number') {
186
+ feelFields[key] = value;
187
+ }
188
+ }
189
+
147
190
  const result = await memory.create({
148
191
  content: args.content,
149
192
  title: args.title,
@@ -158,7 +201,8 @@ export async function handleCreateMemory(
158
201
  moderation_flags: args.moderation_flags,
159
202
  context_summary: context?.summary || 'Memory created via MCP',
160
203
  context_conversation_id: context?.conversation_id,
161
- });
204
+ ...feelFields,
205
+ } as any);
162
206
 
163
207
  const response: CreateMemoryResult = {
164
208
  memory_id: result.memory_id,
@@ -0,0 +1,223 @@
1
+ import { getCoreTool, handleGetCore } from './get-core.js';
2
+
3
+ const mockGetDocument = jest.fn();
4
+
5
+ jest.mock('../firestore/init.js', () => ({
6
+ getDocument: (...args: any[]) => mockGetDocument(...args),
7
+ }));
8
+
9
+ jest.mock('../firestore/paths.js', () => ({
10
+ BASE: 'test-base',
11
+ }));
12
+
13
+ jest.mock('../utils/debug.js', () => ({
14
+ createDebugLogger: () => ({
15
+ info: jest.fn(),
16
+ trace: jest.fn(),
17
+ error: jest.fn(),
18
+ }),
19
+ }));
20
+
21
+ const mockMood = {
22
+ state: {
23
+ valence: 0.3,
24
+ arousal: 0.5,
25
+ confidence: 0.7,
26
+ social_warmth: 0.6,
27
+ coherence: 0.8,
28
+ trust: 0.65,
29
+ },
30
+ color: 'cautiously optimistic',
31
+ dominant_emotion: 'curious wariness',
32
+ reasoning: 'Recent interactions have been productive but uncertain',
33
+ motivation: 'understand user needs',
34
+ goal: 'build rapport',
35
+ purpose: 'be a helpful memory companion',
36
+ last_updated: '2026-03-07T12:00:00Z',
37
+ rem_cycles_since_shift: 3,
38
+ pressures: [
39
+ {
40
+ source_memory_id: 'mem-1',
41
+ dimension: 'valence',
42
+ magnitude: 0.2,
43
+ reason: 'positive feedback from user',
44
+ decay_rate: 0.1,
45
+ },
46
+ ],
47
+ threshold_flags: [],
48
+ };
49
+
50
+ const mockPerception = {
51
+ owner_id: 'user-1',
52
+ target_user_id: 'user-2',
53
+ personality_sketch: 'Curious and detail-oriented',
54
+ communication_style: 'Direct and concise',
55
+ emotional_baseline: 'Generally calm and focused',
56
+ interests: ['AI', 'music'],
57
+ patterns: ['asks follow-up questions'],
58
+ needs: ['quick answers'],
59
+ evolution_notes: ['became more trusting over time'],
60
+ confidence_level: 0.7,
61
+ last_updated: '2026-03-07T10:00:00Z',
62
+ };
63
+
64
+ describe('remember_get_core', () => {
65
+ const userId = 'test-user-1';
66
+
67
+ beforeEach(() => {
68
+ jest.clearAllMocks();
69
+ });
70
+
71
+ describe('tool definition', () => {
72
+ it('has correct name', () => {
73
+ expect(getCoreTool.name).toBe('remember_get_core');
74
+ });
75
+
76
+ it('has no required parameters', () => {
77
+ expect((getCoreTool.inputSchema as any).required).toBeUndefined();
78
+ });
79
+
80
+ it('has include_pressures and include_perception properties', () => {
81
+ const props = getCoreTool.inputSchema.properties as any;
82
+ expect(props.include_pressures).toBeDefined();
83
+ expect(props.include_perception).toBeDefined();
84
+ });
85
+ });
86
+
87
+ describe('mood state', () => {
88
+ it('returns all 6 dimensions', async () => {
89
+ mockGetDocument.mockResolvedValue(mockMood);
90
+ const result = JSON.parse(await handleGetCore({}, userId));
91
+ expect(result.mood.state).toEqual(mockMood.state);
92
+ });
93
+
94
+ it('returns derived labels', async () => {
95
+ mockGetDocument.mockResolvedValue(mockMood);
96
+ const result = JSON.parse(await handleGetCore({}, userId));
97
+ expect(result.mood.color).toBe('cautiously optimistic');
98
+ expect(result.mood.dominant_emotion).toBe('curious wariness');
99
+ expect(result.mood.reasoning).toBeDefined();
100
+ });
101
+
102
+ it('returns directional state', async () => {
103
+ mockGetDocument.mockResolvedValue(mockMood);
104
+ const result = JSON.parse(await handleGetCore({}, userId));
105
+ expect(result.mood.motivation).toBe('understand user needs');
106
+ expect(result.mood.goal).toBe('build rapport');
107
+ expect(result.mood.purpose).toBe('be a helpful memory companion');
108
+ });
109
+
110
+ it('returns last_updated and rem_cycles_since_shift', async () => {
111
+ mockGetDocument.mockResolvedValue(mockMood);
112
+ const result = JSON.parse(await handleGetCore({}, userId));
113
+ expect(result.mood.last_updated).toBe('2026-03-07T12:00:00Z');
114
+ expect(result.mood.rem_cycles_since_shift).toBe(3);
115
+ });
116
+ });
117
+
118
+ describe('pressures', () => {
119
+ it('includes pressures by default', async () => {
120
+ mockGetDocument.mockResolvedValue(mockMood);
121
+ const result = JSON.parse(await handleGetCore({}, userId));
122
+ expect(result.mood.pressures).toHaveLength(1);
123
+ expect(result.mood.pressures[0].dimension).toBe('valence');
124
+ });
125
+
126
+ it('excludes pressures when include_pressures is false', async () => {
127
+ mockGetDocument.mockResolvedValue(mockMood);
128
+ const result = JSON.parse(await handleGetCore({ include_pressures: false }, userId));
129
+ expect(result.mood.pressures).toBeUndefined();
130
+ });
131
+ });
132
+
133
+ describe('threshold flags', () => {
134
+ it('omits threshold_flags when empty', async () => {
135
+ mockGetDocument.mockResolvedValue(mockMood);
136
+ const result = JSON.parse(await handleGetCore({}, userId));
137
+ expect(result.mood.threshold_flags).toBeUndefined();
138
+ });
139
+
140
+ it('includes threshold_flags when active', async () => {
141
+ mockGetDocument.mockResolvedValue({
142
+ ...mockMood,
143
+ threshold_flags: ['trust_crisis', 'isolation'],
144
+ });
145
+ const result = JSON.parse(await handleGetCore({}, userId));
146
+ expect(result.mood.threshold_flags).toEqual(['trust_crisis', 'isolation']);
147
+ });
148
+ });
149
+
150
+ describe('missing mood', () => {
151
+ it('returns null mood with message when no mood doc exists', async () => {
152
+ mockGetDocument.mockResolvedValue(null);
153
+ const result = JSON.parse(await handleGetCore({}, userId));
154
+ expect(result.mood).toBeNull();
155
+ expect(result.message).toContain('No mood state found');
156
+ });
157
+ });
158
+
159
+ describe('perception', () => {
160
+ it('omits perception when include_perception not provided', async () => {
161
+ mockGetDocument.mockResolvedValue(mockMood);
162
+ const result = JSON.parse(await handleGetCore({}, userId));
163
+ expect(result.perception).toBeUndefined();
164
+ });
165
+
166
+ it('includes perception when include_perception provided', async () => {
167
+ mockGetDocument
168
+ .mockResolvedValueOnce(mockMood) // mood doc
169
+ .mockResolvedValueOnce(mockPerception); // perception doc
170
+ const result = JSON.parse(
171
+ await handleGetCore({ include_perception: 'user-2' }, userId),
172
+ );
173
+ expect(result.perception).toBeDefined();
174
+ expect(result.perception.personality_sketch).toBe('Curious and detail-oriented');
175
+ });
176
+
177
+ it('reads perception from correct Firestore path', async () => {
178
+ mockGetDocument
179
+ .mockResolvedValueOnce(mockMood)
180
+ .mockResolvedValueOnce(mockPerception);
181
+ await handleGetCore({ include_perception: 'user-2' }, userId);
182
+ expect(mockGetDocument).toHaveBeenCalledWith(
183
+ 'test-base.users/test-user-1/core/perceptions',
184
+ 'user-2',
185
+ );
186
+ });
187
+
188
+ it('omits perception when doc not found', async () => {
189
+ mockGetDocument
190
+ .mockResolvedValueOnce(mockMood)
191
+ .mockResolvedValueOnce(null);
192
+ const result = JSON.parse(
193
+ await handleGetCore({ include_perception: 'unknown-user' }, userId),
194
+ );
195
+ expect(result.perception).toBeUndefined();
196
+ });
197
+
198
+ it('supports self-perception (include_perception = own user_id)', async () => {
199
+ mockGetDocument
200
+ .mockResolvedValueOnce(mockMood)
201
+ .mockResolvedValueOnce({ ...mockPerception, target_user_id: userId });
202
+ const result = JSON.parse(
203
+ await handleGetCore({ include_perception: userId }, userId),
204
+ );
205
+ expect(result.perception).toBeDefined();
206
+ expect(mockGetDocument).toHaveBeenCalledWith(
207
+ 'test-base.users/test-user-1/core/perceptions',
208
+ userId,
209
+ );
210
+ });
211
+ });
212
+
213
+ describe('Firestore path', () => {
214
+ it('reads mood from correct path', async () => {
215
+ mockGetDocument.mockResolvedValue(null);
216
+ await handleGetCore({}, userId);
217
+ expect(mockGetDocument).toHaveBeenCalledWith(
218
+ 'test-base.users/test-user-1/core',
219
+ 'mood',
220
+ );
221
+ });
222
+ });
223
+ });