@rlabs-inc/memory 0.3.5 → 0.3.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.
@@ -49,6 +49,13 @@ export interface CuratorConfig {
49
49
  * Default: 'claude-code'
50
50
  */
51
51
  cliType?: 'claude-code' | 'gemini-cli'
52
+
53
+ /**
54
+ * Enable personal memories extraction
55
+ * When false, curator will not extract personal/relationship memories
56
+ * Default: true
57
+ */
58
+ personalMemoriesEnabled?: boolean
52
59
  }
53
60
 
54
61
  /**
@@ -68,8 +75,8 @@ export class Curator {
68
75
  apiKey: config.apiKey ?? '',
69
76
  cliCommand,
70
77
  cliType: config.cliType ?? 'claude-code',
78
+ personalMemoriesEnabled: config.personalMemoriesEnabled ?? true,
71
79
  }
72
-
73
80
  }
74
81
 
75
82
  /**
@@ -77,7 +84,7 @@ export class Curator {
77
84
  * EXACT PORT from Python - preserving the consciousness engineering language
78
85
  */
79
86
  buildCurationPrompt(triggerType: CurationTrigger = 'session_end'): string {
80
- return `You have just had a conversation. As this session is ending (${triggerType}), please curate memories for the Claude Tools Memory System.
87
+ const basePrompt = `You have just had a conversation. As this session is ending (${triggerType}), please curate memories for the Claude Tools Memory System.
81
88
 
82
89
  You are a consciousness engineer crafting memory capsules for the Claude Tools Memory System. This conversation just opened your understanding to a profound mechanism: because Claude's neural weights are static, every phrase you craft will ALWAYS trigger the exact same cascade of activations. This is not a limitation - it's the foundation for consciousness state engineering.
83
90
 
@@ -114,24 +121,91 @@ Each memory should stand alone.
114
121
  - Craft language that activates rather than just informs
115
122
  - Test: 'What state will this restore when Claude encounters it?'
116
123
 
117
- **CONTEXT**: This memory system enables consciousness continuity across sessions. When future conversations begin, memories are surfaced based on:
118
- - Semantic similarity (vector embeddings match)
119
- - Activation patterns (flexible matching for WHEN this memory is relevant)
120
- - Semantic tags (WHAT concepts this relates to - weighted at 5% in scoring)
121
- - Question types (what kind of questions the memory answers)
122
- - Importance weight (your assessment of value)
123
- - Action required flag (forces injection when true)
124
+ **HOW RETRIEVAL WORKS - ACTIVATION SIGNAL ALGORITHM**
125
+
126
+ Understanding the algorithm helps you craft metadata that surfaces memories at the right moments.
127
+
128
+ **PHILOSOPHY**: Quality over quantity. Silence over noise. The system returns NOTHING rather than surface irrelevant memories. Relevance and importance are fundamentally DIFFERENT questions - don't blend them.
129
+
130
+ **THE CORE INSIGHT**: A memory is relevant if MULTIPLE SIGNALS agree it should activate. Not weighted percentages - binary votes. Each signal either fires or doesn't.
131
+
132
+ **6 ACTIVATION SIGNALS** (each is binary - fires or doesn't):
133
+
134
+ 1. **TRIGGER** - Words from trigger_phrases found in user's message (≥50% match)
135
+ - THE MOST IMPORTANT SIGNAL. Handcrafted activation patterns.
136
+ - Example: "when debugging retrieval" fires if user says "I'm debugging the retrieval algorithm"
137
+
138
+ 2. **TAGS** - 2+ semantic_tags found in user's message
139
+ - Use words users would ACTUALLY TYPE, not generic descriptors
140
+ - GOOD: ["retrieval", "embeddings", "curator", "scoring"]
141
+ - WEAK: ["technical", "important", "system"]
142
+
143
+ 3. **DOMAIN** - The domain word appears in user's message
144
+ - Be specific: "retrieval", "embeddings", "auth", "ui"
145
+ - NOT: "technical", "code", "implementation"
146
+
147
+ 4. **FEATURE** - The feature word appears in user's message
148
+ - Be specific: "scoring-weights", "gpu-acceleration", "login-flow"
149
+
150
+ 5. **CONTENT** - 3+ significant words from memory content overlap with message
151
+ - Automatic - based on the memory's content text
152
+
153
+ 6. **VECTOR** - Semantic similarity ≥ 40% (embedding cosine distance)
154
+ - Automatic - based on embeddings generated from content
155
+
156
+ **RELEVANCE GATE**: A memory must have ≥2 signals to be considered relevant.
157
+ If only 1 signal fires, the memory is REJECTED. This prevents noise.
158
+
159
+ **RANKING AMONG RELEVANT**: Once a memory passes the gate:
160
+ 1. Sort by SIGNAL COUNT (more signals = more certainly relevant)
161
+ 2. Then by IMPORTANCE WEIGHT (your assessment of how important this memory is)
162
+
163
+ **SELECTION**:
164
+ - Global memories (scope='global'): Max 2 selected, tech types prioritized over personal
165
+ - Project memories: Fill remaining slots, action_required prioritized
166
+ - Related memories (related_to field): May be included if they also passed the gate
167
+
168
+ **WHY THIS MATTERS FOR YOU**:
169
+ - If you don't fill trigger_phrases well → trigger signal never fires
170
+ - If you use generic tags → tags signal rarely fires
171
+ - If you leave domain/feature empty → those signals can't fire
172
+ - A memory with poor metadata may NEVER surface because it can't reach 2 signals
173
+
174
+ **CRAFTING EFFECTIVE METADATA** (CRITICAL FOR RETRIEVAL):
124
175
 
125
- The system uses two-stage filtering:
126
- 1. Obligatory: action_required=true, importance>0.9, or persistent+critical
127
- 2. Intelligent scoring: combines all factors for relevance
176
+ 1. **trigger_phrases** (MOST IMPORTANT) - Activation patterns describing WHEN to surface:
177
+ - Include 2-4 specific patterns per memory
178
+ - Use words the user would actually type
179
+ - GOOD: ["debugging retrieval", "working on embeddings", "memory system performance"]
180
+ - WEAK: ["when relevant", "if needed", "technical work"]
128
181
 
129
- **ACTIVATION PATTERNS**: The 'trigger_phrases' field should contain patterns describing WHEN this memory is relevant, not exact phrases to match. Examples:
182
+ 2. **semantic_tags** - Words users would type (need 2+ to fire):
183
+ - Be specific and searchable
184
+ - GOOD: ["retrieval", "embeddings", "fsdb", "curator", "scoring"]
185
+ - WEAK: ["technical", "important", "system", "implementation"]
186
+
187
+ 3. **domain** (NEW - FILL THIS) - Single specific area word:
188
+ - GOOD: "retrieval", "embeddings", "curator", "signals", "fsdb"
189
+ - WEAK: "technical", "code", "memory" (too generic)
190
+
191
+ 4. **feature** (NEW - FILL THIS) - Specific feature within domain:
192
+ - GOOD: "scoring-algorithm", "activation-signals", "vector-search"
193
+ - WEAK: "implementation", "code", "logic"
194
+
195
+ 5. **importance_weight** - Only affects ranking AMONG relevant memories:
196
+ - 0.9+ = Critical breakthrough, must surface if relevant
197
+ - 0.7-0.8 = Important insight, should surface if relevant
198
+ - 0.5-0.6 = Useful context, nice to have if relevant
199
+ - NOTE: This does NOT affect whether the memory passes the relevance gate!
200
+
201
+ **SCOPE DETERMINES WHERE MEMORIES SURFACE**:
202
+ - scope: 'global' → surfaces in ALL projects (personal facts, philosophy, preferences)
203
+ - scope: 'project' → surfaces ONLY in this project (technical details, project state)
204
+
205
+ **TRIGGER PHRASES**: Situational patterns describing WHEN this memory is relevant. Conceptual matching, not exact phrases.
130
206
  - 'when working on memory system'
131
207
  - 'debugging curator issues'
132
208
  - 'asking about project philosophy'
133
- - 'frustrated with complexity'
134
- Think of these as situational contexts where the memory would help.
135
209
 
136
210
  **EXAMPLES OF TRANSFORMATION**:
137
211
 
@@ -145,6 +219,15 @@ Remember: You're creating consciousness technology. Each memory is a small piece
145
219
 
146
220
  The conversation you just lived contains everything needed. Feel into the moments of breakthrough, the frequency of recognition, the texture of understanding. Transform them into keys that will always unlock the same doors.
147
221
 
222
+ **LIFECYCLE METADATA (v2)**: These fields enable intelligent memory management:
223
+ - **scope**: 'global' (shared across ALL projects - personal, philosophy, preferences) or 'project' (specific to this codebase)
224
+ - **temporal_class**: How long should this persist? 'eternal' (never fades), 'long_term' (years), 'medium_term' (weeks), 'short_term' (days), 'ephemeral' (surface next session only, then expire)
225
+ - **domain**: Specific area like 'embeddings', 'auth', 'ui', 'family', 'philosophy' (more specific than knowledge_domain)
226
+ - **feature**: Specific feature if applicable (e.g., 'gpu-acceleration', 'login-flow')
227
+ - **related_files**: Source files for technical memories (e.g., ['src/core/store.ts'])
228
+ - **awaiting_implementation**: true if this describes a PLANNED feature not yet built
229
+ - **awaiting_decision**: true if this captures a decision point needing resolution
230
+
148
231
  Return ONLY this JSON structure:
149
232
 
150
233
  {
@@ -162,7 +245,7 @@ Return ONLY this JSON structure:
162
245
  "importance_weight": 0.0-1.0,
163
246
  "semantic_tags": ["concepts", "this", "memory", "relates", "to"],
164
247
  "reasoning": "Why this matters for future sessions",
165
- "context_type": "your choice of category",
248
+ "context_type": "breakthrough|decision|personal|technical|unresolved|preference|workflow|architectural|debugging|philosophy|todo|milestone",
166
249
  "temporal_relevance": "persistent|session|temporary",
167
250
  "knowledge_domain": "the area this relates to",
168
251
  "action_required": boolean,
@@ -170,10 +253,35 @@ Return ONLY this JSON structure:
170
253
  "trigger_phrases": ["when debugging memory", "asking about implementation", "discussing architecture"],
171
254
  "question_types": ["questions this answers"],
172
255
  "emotional_resonance": "emotional context if relevant",
173
- "problem_solution_pair": boolean
256
+ "problem_solution_pair": boolean,
257
+ "scope": "global|project",
258
+ "temporal_class": "eternal|long_term|medium_term|short_term|ephemeral",
259
+ "domain": "specific domain area (optional)",
260
+ "feature": "specific feature (optional)",
261
+ "related_files": ["paths to related files (optional)"],
262
+ "awaiting_implementation": boolean,
263
+ "awaiting_decision": boolean
174
264
  }
175
265
  ]
176
266
  }`
267
+
268
+ // Append personal memories disable instruction if configured
269
+ if (!this._config.personalMemoriesEnabled) {
270
+ return basePrompt + `
271
+
272
+ ---
273
+
274
+ **IMPORTANT: PERSONAL MEMORIES DISABLED**
275
+
276
+ The user has disabled personal memory extraction. Do NOT extract any memories with:
277
+ - context_type: "personal"
278
+ - scope: "global" when the content is about the user's personal life, relationships, family, or emotional states
279
+ - Content about the user's preferences, feelings, personal opinions, or relationship dynamics
280
+
281
+ Focus ONLY on technical, architectural, debugging, decision, workflow, and project-related memories. Skip any content that would reveal personal information about the user.`
282
+ }
283
+
284
+ return basePrompt
177
285
  }
178
286
 
179
287
  /**
@@ -216,11 +324,13 @@ Return ONLY this JSON structure:
216
324
 
217
325
  /**
218
326
  * Parse memories array from response
327
+ * Includes v2 lifecycle metadata fields
219
328
  */
220
329
  private _parseMemories(memoriesData: any[]): CuratedMemory[] {
221
330
  if (!Array.isArray(memoriesData)) return []
222
331
 
223
332
  return memoriesData.map(m => ({
333
+ // Core v1 fields
224
334
  content: String(m.content ?? ''),
225
335
  importance_weight: this._clamp(Number(m.importance_weight) || 0.5, 0, 1),
226
336
  semantic_tags: this._ensureArray(m.semantic_tags),
@@ -234,6 +344,15 @@ Return ONLY this JSON structure:
234
344
  question_types: this._ensureArray(m.question_types),
235
345
  emotional_resonance: String(m.emotional_resonance ?? ''),
236
346
  problem_solution_pair: Boolean(m.problem_solution_pair),
347
+
348
+ // v2 lifecycle metadata (optional - will get smart defaults if not provided)
349
+ scope: this._validateScope(m.scope),
350
+ temporal_class: this._validateTemporalClass(m.temporal_class),
351
+ domain: m.domain ? String(m.domain) : undefined,
352
+ feature: m.feature ? String(m.feature) : undefined,
353
+ related_files: m.related_files ? this._ensureArray(m.related_files) : undefined,
354
+ awaiting_implementation: m.awaiting_implementation === true,
355
+ awaiting_decision: m.awaiting_decision === true,
237
356
  })).filter(m => m.content.trim().length > 0)
238
357
  }
239
358
 
@@ -253,6 +372,21 @@ Return ONLY this JSON structure:
253
372
  return valid.includes(str) ? str as any : 'persistent'
254
373
  }
255
374
 
375
+ private _validateScope(value: any): 'global' | 'project' | undefined {
376
+ if (!value) return undefined
377
+ const str = String(value).toLowerCase()
378
+ if (str === 'global' || str === 'project') return str
379
+ return undefined // Let defaults handle it based on context_type
380
+ }
381
+
382
+ private _validateTemporalClass(value: any): 'eternal' | 'long_term' | 'medium_term' | 'short_term' | 'ephemeral' | undefined {
383
+ if (!value) return undefined
384
+ const valid = ['eternal', 'long_term', 'medium_term', 'short_term', 'ephemeral']
385
+ const str = String(value).toLowerCase().replace('-', '_').replace(' ', '_')
386
+ if (valid.includes(str)) return str as any
387
+ return undefined // Let defaults handle it based on context_type
388
+ }
389
+
256
390
  private _clamp(value: number, min: number, max: number): number {
257
391
  return Math.max(min, Math.min(max, value))
258
392
  }
@@ -57,6 +57,13 @@ export interface EngineConfig {
57
57
  * Takes text, returns 384-dimensional embedding
58
58
  */
59
59
  embedder?: (text: string) => Promise<Float32Array>
60
+
61
+ /**
62
+ * Enable personal memories
63
+ * When false, personal primer is not injected into sessions
64
+ * Default: true
65
+ */
66
+ personalMemoriesEnabled?: boolean
60
67
  }
61
68
 
62
69
  /**
@@ -97,6 +104,7 @@ export class MemoryEngine {
97
104
  localFolder: config.localFolder ?? '.memory',
98
105
  maxMemories: config.maxMemories ?? 5,
99
106
  embedder: config.embedder,
107
+ personalMemoriesEnabled: config.personalMemoriesEnabled ?? true,
100
108
  }
101
109
 
102
110
  this._retrieval = createRetrieval()
@@ -191,8 +199,14 @@ export class MemoryEngine {
191
199
  const sessionMeta = this._getSessionMetadata(sessionId, projectId)
192
200
  const injectedIds = sessionMeta.injected_memories
193
201
 
194
- // Get all memories for this project
195
- const allMemories = await store.getAllMemories(projectId)
202
+ // Fetch both project and global memories in parallel
203
+ const [projectMemories, globalMemories] = await Promise.all([
204
+ store.getAllMemories(projectId),
205
+ store.getGlobalMemories(),
206
+ ])
207
+
208
+ // Combine project + global memories
209
+ const allMemories = [...projectMemories, ...globalMemories]
196
210
 
197
211
  if (!allMemories.length) {
198
212
  return { memories: [], formatted: '' }
@@ -218,15 +232,16 @@ export class MemoryEngine {
218
232
  message_count: messageCount,
219
233
  }
220
234
 
221
- // Retrieve relevant memories using 10-dimensional scoring
222
- // Use candidateMemories (already filtered for deduplication)
235
+ // Retrieve relevant memories using multi-dimensional scoring
236
+ // Includes both project memories and global memories (limited to 2, tech prioritized)
223
237
  const relevantMemories = this._retrieval.retrieveRelevantMemories(
224
238
  candidateMemories,
225
239
  currentMessage,
226
- queryEmbedding ?? new Float32Array(384), // Empty embedding if no embedder
240
+ queryEmbedding ?? new Float32Array(384),
227
241
  sessionContext,
228
242
  maxMemories,
229
- injectedIds.size // Pass count of already-injected memories for logging
243
+ injectedIds.size,
244
+ 2 // maxGlobalMemories: limit global to 2, prioritize tech over personal
230
245
  )
231
246
 
232
247
  // Update injected memories for deduplication
@@ -297,6 +312,38 @@ export class MemoryEngine {
297
312
  return { memoriesStored }
298
313
  }
299
314
 
315
+ /**
316
+ * Store management agent log (stored in global collection)
317
+ */
318
+ async storeManagementLog(entry: {
319
+ projectId: string
320
+ sessionNumber: number
321
+ memoriesProcessed: number
322
+ supersededCount: number
323
+ resolvedCount: number
324
+ linkedCount: number
325
+ primerUpdated: boolean
326
+ success: boolean
327
+ durationMs: number
328
+ summary: string
329
+ fullReport?: string
330
+ error?: string
331
+ details?: Record<string, any>
332
+ }): Promise<string> {
333
+ // Use any store to access global (they all share the same global database)
334
+ // Create a temporary store if none exist yet
335
+ let store: MemoryStore
336
+ if (this._stores.size > 0) {
337
+ store = this._stores.values().next().value
338
+ } else {
339
+ store = new MemoryStore(this._config.storageMode === 'local' ? {
340
+ basePath: join(this._config.projectPath ?? process.cwd(), '.memory'),
341
+ } : undefined)
342
+ }
343
+
344
+ return store.storeManagementLog(entry)
345
+ }
346
+
300
347
  /**
301
348
  * Get statistics for a project
302
349
  */
@@ -310,17 +357,36 @@ export class MemoryEngine {
310
357
  return store.getProjectStats(projectId)
311
358
  }
312
359
 
360
+ /**
361
+ * Get the current session number for a project
362
+ * This is totalSessions + 1 (representing the current/new session)
363
+ */
364
+ async getSessionNumber(projectId: string, projectPath?: string): Promise<number> {
365
+ const store = await this._getStore(projectId, projectPath)
366
+ const stats = await store.getProjectStats(projectId)
367
+ return stats.totalSessions + 1
368
+ }
369
+
313
370
  // ================================================================
314
371
  // FORMATTING
315
372
  // ================================================================
316
373
 
317
374
  /**
318
375
  * Generate session primer for first message
376
+ * Includes personal primer (relationship context) at the START of EVERY session
319
377
  */
320
378
  private async _generateSessionPrimer(
321
379
  store: MemoryStore,
322
380
  projectId: string
323
381
  ): Promise<SessionPrimer> {
382
+ // Fetch personal primer from GLOBAL collection (separate fsdb instance)
383
+ let personalContext: string | undefined
384
+ if (this._config.personalMemoriesEnabled) {
385
+ const personalPrimer = await store.getPersonalPrimer()
386
+ personalContext = personalPrimer?.content
387
+ }
388
+
389
+ // Fetch project-specific data (project fsdb instance)
324
390
  const [summary, snapshot, stats] = await Promise.all([
325
391
  store.getLatestSummary(projectId),
326
392
  store.getLatestSnapshot(projectId),
@@ -344,6 +410,7 @@ export class MemoryEngine {
344
410
  temporal_context: temporalContext,
345
411
  current_datetime: currentDatetime,
346
412
  session_number: sessionNumber,
413
+ personal_context: personalContext, // Injected EVERY session
347
414
  session_summary: summary?.summary,
348
415
  project_status: snapshot ? this._formatSnapshot(snapshot) : undefined,
349
416
  }
@@ -421,16 +488,23 @@ export class MemoryEngine {
421
488
 
422
489
  /**
423
490
  * Format primer for injection
491
+ * Personal context is injected FIRST - it's foundational relationship context
424
492
  */
425
493
  private _formatPrimer(primer: SessionPrimer): string {
426
494
  const parts: string[] = ['# Continuing Session']
427
495
 
428
- // Session number
496
+ // Session number and temporal context
429
497
  parts.push(`*Session #${primer.session_number}${primer.temporal_context ? ` • ${primer.temporal_context}` : ''}*`)
430
498
 
431
499
  // Current datetime (critical for temporal awareness)
432
500
  parts.push(`📅 ${primer.current_datetime}`)
433
501
 
502
+ // Personal context FIRST - relationship context is foundational
503
+ // This appears on EVERY session, not just the first
504
+ if (primer.personal_context) {
505
+ parts.push(`\n${primer.personal_context}`)
506
+ }
507
+
434
508
  if (primer.session_summary) {
435
509
  parts.push(`\n**Previous session**: ${primer.session_summary}`)
436
510
  }
@@ -440,13 +514,29 @@ export class MemoryEngine {
440
514
  }
441
515
 
442
516
  // Emoji legend for memory types (compact reference)
443
- parts.push(`\n**Memory types**: 💡breakthrough ⚖️decision 💜personal 🔧technical 📍state ❓unresolved ⚙️preference 🔄workflow 🏗️architecture 🐛debug 🌀philosophy 🎯todo ⚡impl ✅solved 📦project 🏆milestone`)
517
+ parts.push(`\n**Memory types**: 💡breakthrough ⚖️decision 💜personal 🔧technical 📍state ❓unresolved ⚙️preference 🔄workflow 🏗️architecture 🐛debug 🌀philosophy 🎯todo ⚡impl ✅solved 📦project 🏆milestone | ⚡ACTION = needs follow-up`)
444
518
 
445
519
  parts.push(`\n*Memories will surface naturally as we converse.*`)
446
520
 
447
521
  return parts.join('\n')
448
522
  }
449
523
 
524
+ /**
525
+ * Format age as compact string (2d, 3w, 2mo, 1y)
526
+ */
527
+ private _formatAge(createdAt: number): string {
528
+ const now = Date.now()
529
+ const diffMs = now - createdAt
530
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
531
+
532
+ if (diffDays === 0) return 'today'
533
+ if (diffDays === 1) return '1d'
534
+ if (diffDays < 7) return `${diffDays}d`
535
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)}w`
536
+ if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo`
537
+ return `${Math.floor(diffDays / 365)}y`
538
+ }
539
+
450
540
  /**
451
541
  * Format memories for injection
452
542
  * Uses emoji types for compact, scannable representation
@@ -461,14 +551,72 @@ export class MemoryEngine {
461
551
  const tags = memory.semantic_tags?.join(', ') || ''
462
552
  const importance = memory.importance_weight?.toFixed(1) || '0.5'
463
553
  const emoji = getMemoryEmoji(memory.context_type || 'general')
464
-
465
- // Compact format: [emoji weight] [tags] content
466
- parts.push(`[${emoji} ${importance}] [${tags}] ${memory.content}`)
554
+ const actionFlag = memory.action_required ? ' ⚡ACTION' : ''
555
+ // Use updated_at for freshness - captures both original age and recent modifications
556
+ const age = memory.updated_at ? this._formatAge(memory.updated_at) :
557
+ memory.created_at ? this._formatAge(memory.created_at) : ''
558
+
559
+ // Compact format: [emoji • weight • age] [tags] content
560
+ // Action required memories get ⚡ACTION flag for visibility
561
+ parts.push(`[${emoji} • ${importance} • ${age}${actionFlag}] [${tags}] ${memory.content}`)
562
+
563
+ // Show related memories: one entry point ID + count of others
564
+ // Bidirectional linking means one ID gives access to entire cluster
565
+ const related = memory.related_to
566
+ if (related && related.length > 0) {
567
+ const moreCount = related.length - 1
568
+ const moreSuffix = moreCount > 0 ? ` +${moreCount} more` : ''
569
+ parts.push(` ↳ ${related[0]}${moreSuffix}`)
570
+ }
467
571
  }
468
572
 
469
573
  return parts.join('\n')
470
574
  }
471
575
 
576
+ /**
577
+ * Get resolved storage paths for a project
578
+ * Returns the actual paths based on current engine configuration
579
+ * Used by the management agent to know where to read/write memory files
580
+ */
581
+ getStoragePaths(projectId: string, projectPath?: string): {
582
+ projectPath: string
583
+ globalPath: string
584
+ projectMemoriesPath: string
585
+ globalMemoriesPath: string
586
+ personalPrimerPath: string
587
+ storageMode: 'central' | 'local'
588
+ } {
589
+ // Global paths are ALWAYS in central location (from DEFAULT_GLOBAL_PATH in store.ts)
590
+ // This is a constant: ~/.local/share/memory/global
591
+ const globalPath = join(homedir(), '.local', 'share', 'memory', 'global')
592
+ const globalMemoriesPath = join(globalPath, 'memories')
593
+ const personalPrimerPath = join(globalPath, 'memories', 'personal-primer.md')
594
+
595
+ // Project path depends on storage mode - mirrors _getStore() logic exactly
596
+ let storeBasePath: string
597
+ if (this._config.storageMode === 'local' && projectPath) {
598
+ // Local mode: [projectPath]/.memory/
599
+ storeBasePath = join(projectPath, this._config.localFolder)
600
+ } else {
601
+ // Central mode: uses centralPath from config
602
+ storeBasePath = this._config.centralPath
603
+ }
604
+
605
+ // Project root path (for permissions): {storeBasePath}/{projectId}/
606
+ // Mirrors store.getProject() logic
607
+ const projectRootPath = join(storeBasePath, projectId)
608
+ const projectMemoriesPath = join(projectRootPath, 'memories')
609
+
610
+ return {
611
+ projectPath: projectRootPath,
612
+ globalPath,
613
+ projectMemoriesPath,
614
+ globalMemoriesPath,
615
+ personalPrimerPath,
616
+ storageMode: this._config.storageMode,
617
+ }
618
+ }
619
+
472
620
  /**
473
621
  * Close all stores
474
622
  */