namnam-skills 1.0.0 → 1.0.2

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.
@@ -0,0 +1,914 @@
1
+ /**
2
+ * Conversation Management Utilities
3
+ *
4
+ * Handles saving, loading, and managing conversation context
5
+ * for the @conversation feature
6
+ */
7
+
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+ import crypto from 'crypto';
11
+
12
+ // Default conversation directory
13
+ const CONV_DIR_NAME = 'conversations';
14
+ const INDEX_FILE = 'index.json';
15
+ const META_FILE = 'meta.json';
16
+ const CONTEXT_FILE = 'context.md';
17
+ const FULL_LOG_FILE = 'full.md';
18
+
19
+ // Auto-memory files
20
+ const AUTO_MEMORY_DIR = 'auto-memories';
21
+ const AUTO_MEMORY_INDEX = 'auto-index.json';
22
+ const AUTO_MEMORY_CURRENT = 'current-session.json';
23
+
24
+ /**
25
+ * Validate conversation options
26
+ */
27
+ function validateConversationOptions(options) {
28
+ const errors = [];
29
+
30
+ if (options.title !== undefined && typeof options.title !== 'string') {
31
+ errors.push('title must be a string');
32
+ }
33
+
34
+ if (options.summary !== undefined && typeof options.summary !== 'string') {
35
+ errors.push('summary must be a string');
36
+ }
37
+
38
+ if (options.context !== undefined && typeof options.context !== 'string') {
39
+ errors.push('context must be a string');
40
+ }
41
+
42
+ if (options.fullLog !== undefined && typeof options.fullLog !== 'string') {
43
+ errors.push('fullLog must be a string');
44
+ }
45
+
46
+ if (options.tags !== undefined && !Array.isArray(options.tags)) {
47
+ errors.push('tags must be an array');
48
+ }
49
+
50
+ if (errors.length > 0) {
51
+ throw new Error(`Invalid conversation options: ${errors.join(', ')}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Escape XML special characters
57
+ */
58
+ function escapeXml(str) {
59
+ if (!str) return '';
60
+ return str
61
+ .replace(/&/g, '&')
62
+ .replace(/</g, '&lt;')
63
+ .replace(/>/g, '&gt;')
64
+ .replace(/"/g, '&quot;')
65
+ .replace(/'/g, '&apos;');
66
+ }
67
+
68
+ /**
69
+ * Get the conversations directory path
70
+ */
71
+ export function getConversationsDir(cwd = process.cwd()) {
72
+ return path.join(cwd, '.claude', CONV_DIR_NAME);
73
+ }
74
+
75
+ /**
76
+ * Get the index file path
77
+ */
78
+ export function getIndexPath(cwd = process.cwd()) {
79
+ return path.join(getConversationsDir(cwd), INDEX_FILE);
80
+ }
81
+
82
+ /**
83
+ * Generate a unique conversation ID
84
+ */
85
+ export function generateConvId() {
86
+ const timestamp = Date.now().toString(36);
87
+ const random = crypto.randomBytes(3).toString('hex');
88
+ return `conv_${timestamp}_${random}`;
89
+ }
90
+
91
+ /**
92
+ * Generate a short ID for easy reference
93
+ */
94
+ export function generateShortId() {
95
+ return crypto.randomBytes(4).toString('hex');
96
+ }
97
+
98
+ /**
99
+ * Initialize conversations directory
100
+ */
101
+ export async function initConversations(cwd = process.cwd()) {
102
+ const convDir = getConversationsDir(cwd);
103
+ const indexPath = getIndexPath(cwd);
104
+
105
+ await fs.ensureDir(convDir);
106
+
107
+ if (!(await fs.pathExists(indexPath))) {
108
+ await fs.writeJson(indexPath, {
109
+ version: '1.0.0',
110
+ conversations: [],
111
+ lastUpdated: new Date().toISOString()
112
+ }, { spaces: 2 });
113
+ }
114
+
115
+ return convDir;
116
+ }
117
+
118
+ /**
119
+ * Load the conversation index
120
+ */
121
+ export async function loadIndex(cwd = process.cwd()) {
122
+ const indexPath = getIndexPath(cwd);
123
+
124
+ if (!(await fs.pathExists(indexPath))) {
125
+ return { version: '1.0.0', conversations: [], lastUpdated: null };
126
+ }
127
+
128
+ return await fs.readJson(indexPath);
129
+ }
130
+
131
+ /**
132
+ * Save the conversation index
133
+ */
134
+ export async function saveIndex(index, cwd = process.cwd()) {
135
+ const indexPath = getIndexPath(cwd);
136
+ index.lastUpdated = new Date().toISOString();
137
+ await fs.writeJson(indexPath, index, { spaces: 2 });
138
+ }
139
+
140
+ /**
141
+ * Save a new conversation
142
+ */
143
+ export async function saveConversation(options, cwd = process.cwd()) {
144
+ validateConversationOptions(options);
145
+
146
+ const {
147
+ title,
148
+ summary,
149
+ context,
150
+ fullLog = null,
151
+ tags = [],
152
+ metadata = {}
153
+ } = options;
154
+
155
+ await initConversations(cwd);
156
+
157
+ const id = generateConvId();
158
+ const shortId = generateShortId();
159
+ const convDir = path.join(getConversationsDir(cwd), id);
160
+
161
+ await fs.ensureDir(convDir);
162
+
163
+ // Create metadata file
164
+ const meta = {
165
+ id,
166
+ shortId,
167
+ title: title || 'Untitled Conversation',
168
+ summary: summary || '',
169
+ tags,
170
+ createdAt: new Date().toISOString(),
171
+ updatedAt: new Date().toISOString(),
172
+ ...metadata
173
+ };
174
+
175
+ await fs.writeJson(path.join(convDir, META_FILE), meta, { spaces: 2 });
176
+
177
+ // Create context file (loadable summary)
178
+ if (context) {
179
+ await fs.writeFile(path.join(convDir, CONTEXT_FILE), context);
180
+ }
181
+
182
+ // Create full log if provided
183
+ if (fullLog) {
184
+ await fs.writeFile(path.join(convDir, FULL_LOG_FILE), fullLog);
185
+ }
186
+
187
+ // Update index
188
+ const index = await loadIndex(cwd);
189
+ index.conversations.push({
190
+ id,
191
+ shortId,
192
+ title: meta.title,
193
+ summary: meta.summary,
194
+ tags,
195
+ createdAt: meta.createdAt
196
+ });
197
+ await saveIndex(index, cwd);
198
+
199
+ return { id, shortId, path: convDir };
200
+ }
201
+
202
+ /**
203
+ * Get a conversation by ID (full or short)
204
+ */
205
+ export async function getConversation(convId, cwd = process.cwd()) {
206
+ const index = await loadIndex(cwd);
207
+
208
+ // Find by full ID or short ID
209
+ const entry = index.conversations.find(
210
+ c => c.id === convId || c.shortId === convId || c.id.includes(convId)
211
+ );
212
+
213
+ if (!entry) {
214
+ return null;
215
+ }
216
+
217
+ const convDir = path.join(getConversationsDir(cwd), entry.id);
218
+
219
+ if (!(await fs.pathExists(convDir))) {
220
+ return null;
221
+ }
222
+
223
+ const meta = await fs.readJson(path.join(convDir, META_FILE));
224
+
225
+ let context = null;
226
+ let fullLog = null;
227
+
228
+ const contextPath = path.join(convDir, CONTEXT_FILE);
229
+ const fullPath = path.join(convDir, FULL_LOG_FILE);
230
+
231
+ if (await fs.pathExists(contextPath)) {
232
+ context = await fs.readFile(contextPath, 'utf-8');
233
+ }
234
+
235
+ if (await fs.pathExists(fullPath)) {
236
+ fullLog = await fs.readFile(fullPath, 'utf-8');
237
+ }
238
+
239
+ return { ...meta, context, fullLog };
240
+ }
241
+
242
+ /**
243
+ * List all conversations
244
+ */
245
+ export async function listConversations(options = {}, cwd = process.cwd()) {
246
+ const { limit = 20, tag = null, search = null } = options;
247
+ const index = await loadIndex(cwd);
248
+
249
+ let conversations = index.conversations || [];
250
+
251
+ // Filter by tag
252
+ if (tag) {
253
+ conversations = conversations.filter(c =>
254
+ c.tags && c.tags.includes(tag)
255
+ );
256
+ }
257
+
258
+ // Filter by search term
259
+ if (search) {
260
+ const term = search.toLowerCase();
261
+ conversations = conversations.filter(c =>
262
+ c.title.toLowerCase().includes(term) ||
263
+ c.summary.toLowerCase().includes(term) ||
264
+ c.id.includes(term) ||
265
+ c.shortId.includes(term)
266
+ );
267
+ }
268
+
269
+ // Sort by date (newest first)
270
+ conversations.sort((a, b) =>
271
+ new Date(b.createdAt) - new Date(a.createdAt)
272
+ );
273
+
274
+ // Limit results
275
+ if (limit > 0) {
276
+ conversations = conversations.slice(0, limit);
277
+ }
278
+
279
+ return conversations;
280
+ }
281
+
282
+ /**
283
+ * Update a conversation
284
+ */
285
+ export async function updateConversation(convId, updates, cwd = process.cwd()) {
286
+ validateConversationOptions(updates);
287
+
288
+ const conversation = await getConversation(convId, cwd);
289
+
290
+ if (!conversation) {
291
+ return null;
292
+ }
293
+
294
+ const convDir = path.join(getConversationsDir(cwd), conversation.id);
295
+
296
+ // Store context and fullLog before processing
297
+ const newContext = updates.context;
298
+ const newFullLog = updates.fullLog;
299
+
300
+ // Prepare metadata updates (exclude file content fields)
301
+ const metaUpdates = { ...updates };
302
+ delete metaUpdates.id;
303
+ delete metaUpdates.context;
304
+ delete metaUpdates.fullLog;
305
+
306
+ // Update metadata
307
+ const meta = await fs.readJson(path.join(convDir, META_FILE));
308
+ const updatedMeta = {
309
+ ...meta,
310
+ ...metaUpdates,
311
+ updatedAt: new Date().toISOString()
312
+ };
313
+
314
+ await fs.writeJson(path.join(convDir, META_FILE), updatedMeta, { spaces: 2 });
315
+
316
+ // Update context if provided
317
+ if (newContext !== undefined) {
318
+ await fs.writeFile(path.join(convDir, CONTEXT_FILE), newContext);
319
+ }
320
+
321
+ // Update full log if provided
322
+ if (newFullLog !== undefined) {
323
+ await fs.writeFile(path.join(convDir, FULL_LOG_FILE), newFullLog);
324
+ }
325
+
326
+ // Update index
327
+ const index = await loadIndex(cwd);
328
+ const idx = index.conversations.findIndex(c => c.id === conversation.id);
329
+ if (idx !== -1) {
330
+ index.conversations[idx] = {
331
+ ...index.conversations[idx],
332
+ title: updatedMeta.title,
333
+ summary: updatedMeta.summary,
334
+ tags: updatedMeta.tags
335
+ };
336
+ await saveIndex(index, cwd);
337
+ }
338
+
339
+ return updatedMeta;
340
+ }
341
+
342
+ /**
343
+ * Delete a conversation
344
+ */
345
+ export async function deleteConversation(convId, cwd = process.cwd()) {
346
+ const conversation = await getConversation(convId, cwd);
347
+
348
+ if (!conversation) {
349
+ return false;
350
+ }
351
+
352
+ const convDir = path.join(getConversationsDir(cwd), conversation.id);
353
+
354
+ // Remove directory
355
+ await fs.remove(convDir);
356
+
357
+ // Update index
358
+ const index = await loadIndex(cwd);
359
+ index.conversations = index.conversations.filter(c => c.id !== conversation.id);
360
+ await saveIndex(index, cwd);
361
+
362
+ return true;
363
+ }
364
+
365
+ /**
366
+ * Export conversation to a single markdown file
367
+ */
368
+ export async function exportConversation(convId, outputPath, cwd = process.cwd()) {
369
+ const conversation = await getConversation(convId, cwd);
370
+
371
+ if (!conversation) {
372
+ return null;
373
+ }
374
+
375
+ const content = `# ${conversation.title}
376
+
377
+ **ID:** ${conversation.shortId}
378
+ **Created:** ${conversation.createdAt}
379
+ **Tags:** ${conversation.tags?.join(', ') || 'none'}
380
+
381
+ ## Summary
382
+
383
+ ${conversation.summary || 'No summary available.'}
384
+
385
+ ## Context
386
+
387
+ ${conversation.context || 'No context available.'}
388
+
389
+ ${conversation.fullLog ? `## Full Log\n\n${conversation.fullLog}` : ''}
390
+ `;
391
+
392
+ await fs.writeFile(outputPath, content);
393
+ return outputPath;
394
+ }
395
+
396
+ /**
397
+ * Get conversation context for AI consumption
398
+ * This is what gets loaded when @conversation:id is used
399
+ */
400
+ export async function getConversationContext(convId, cwd = process.cwd()) {
401
+ const conversation = await getConversation(convId, cwd);
402
+
403
+ if (!conversation) {
404
+ return null;
405
+ }
406
+
407
+ const id = escapeXml(conversation.shortId);
408
+ const title = escapeXml(conversation.title);
409
+ const content = conversation.context || conversation.summary || 'No context available.';
410
+
411
+ return `
412
+ <conversation-context id="${id}" title="${title}">
413
+ ${content}
414
+ </conversation-context>
415
+ `.trim();
416
+ }
417
+
418
+ /**
419
+ * Parse @conversation references from text
420
+ */
421
+ export function parseConversationRefs(text) {
422
+ const pattern = /@conversation[:\s]+([a-zA-Z0-9_-]+)/gi;
423
+ const matches = [];
424
+ let match;
425
+
426
+ while ((match = pattern.exec(text)) !== null) {
427
+ matches.push(match[1]);
428
+ }
429
+
430
+ return [...new Set(matches)]; // Remove duplicates
431
+ }
432
+
433
+ /**
434
+ * Resolve all @conversation references in text
435
+ */
436
+ export async function resolveConversationRefs(text, cwd = process.cwd()) {
437
+ const refs = parseConversationRefs(text);
438
+ const contexts = [];
439
+
440
+ for (const ref of refs) {
441
+ const context = await getConversationContext(ref, cwd);
442
+ if (context) {
443
+ contexts.push(context);
444
+ }
445
+ }
446
+
447
+ return {
448
+ refs,
449
+ contexts,
450
+ combined: contexts.join('\n\n')
451
+ };
452
+ }
453
+
454
+ // ========================================
455
+ // AUTO-MEMORY SYSTEM
456
+ // ========================================
457
+
458
+ /**
459
+ * Get auto-memory directory path
460
+ */
461
+ export function getAutoMemoryDir(cwd = process.cwd()) {
462
+ return path.join(cwd, '.claude', AUTO_MEMORY_DIR);
463
+ }
464
+
465
+ /**
466
+ * Get auto-memory index path
467
+ */
468
+ export function getAutoMemoryIndexPath(cwd = process.cwd()) {
469
+ return path.join(getAutoMemoryDir(cwd), AUTO_MEMORY_INDEX);
470
+ }
471
+
472
+ /**
473
+ * Get current session path
474
+ */
475
+ export function getCurrentSessionPath(cwd = process.cwd()) {
476
+ return path.join(getAutoMemoryDir(cwd), AUTO_MEMORY_CURRENT);
477
+ }
478
+
479
+ /**
480
+ * Initialize auto-memory system
481
+ */
482
+ export async function initAutoMemory(cwd = process.cwd()) {
483
+ const memoryDir = getAutoMemoryDir(cwd);
484
+ const indexPath = getAutoMemoryIndexPath(cwd);
485
+
486
+ await fs.ensureDir(memoryDir);
487
+
488
+ if (!(await fs.pathExists(indexPath))) {
489
+ await fs.writeJson(indexPath, {
490
+ version: '1.0.0',
491
+ memories: [],
492
+ patterns: [],
493
+ decisions: [],
494
+ lastUpdated: new Date().toISOString()
495
+ }, { spaces: 2 });
496
+ }
497
+
498
+ return memoryDir;
499
+ }
500
+
501
+ /**
502
+ * Load auto-memory index
503
+ */
504
+ export async function loadAutoMemoryIndex(cwd = process.cwd()) {
505
+ const indexPath = getAutoMemoryIndexPath(cwd);
506
+
507
+ if (!(await fs.pathExists(indexPath))) {
508
+ return { version: '1.0.0', memories: [], patterns: [], decisions: [], lastUpdated: null };
509
+ }
510
+
511
+ return await fs.readJson(indexPath);
512
+ }
513
+
514
+ /**
515
+ * Save auto-memory index
516
+ */
517
+ export async function saveAutoMemoryIndex(index, cwd = process.cwd()) {
518
+ const indexPath = getAutoMemoryIndexPath(cwd);
519
+ index.lastUpdated = new Date().toISOString();
520
+ await fs.writeJson(indexPath, index, { spaces: 2 });
521
+ }
522
+
523
+ // Validation constants for auto-memory
524
+ const VALID_MEMORY_TYPES = ['decision', 'pattern', 'context', 'learning'];
525
+ const VALID_IMPORTANCE_LEVELS = ['low', 'normal', 'high', 'critical'];
526
+ const VALID_SOURCES = ['auto', 'user', 'agent', 'session'];
527
+ const MAX_CONTENT_SIZE = 10000; // 10KB max per memory
528
+
529
+ /**
530
+ * Validate auto-memory input
531
+ */
532
+ function validateMemoryInput(memory) {
533
+ const errors = [];
534
+
535
+ // Content is required
536
+ if (!memory.content || typeof memory.content !== 'string') {
537
+ errors.push('content is required and must be a string');
538
+ } else if (memory.content.length > MAX_CONTENT_SIZE) {
539
+ errors.push(`content exceeds maximum size of ${MAX_CONTENT_SIZE} characters`);
540
+ }
541
+
542
+ // Validate type
543
+ if (memory.type && !VALID_MEMORY_TYPES.includes(memory.type)) {
544
+ errors.push(`type must be one of: ${VALID_MEMORY_TYPES.join(', ')}`);
545
+ }
546
+
547
+ // Validate importance
548
+ if (memory.importance && !VALID_IMPORTANCE_LEVELS.includes(memory.importance)) {
549
+ errors.push(`importance must be one of: ${VALID_IMPORTANCE_LEVELS.join(', ')}`);
550
+ }
551
+
552
+ // Validate source
553
+ if (memory.source && !VALID_SOURCES.includes(memory.source)) {
554
+ errors.push(`source must be one of: ${VALID_SOURCES.join(', ')}`);
555
+ }
556
+
557
+ // Validate tags
558
+ if (memory.tags && !Array.isArray(memory.tags)) {
559
+ errors.push('tags must be an array');
560
+ }
561
+
562
+ // Validate relatedFiles
563
+ if (memory.relatedFiles && !Array.isArray(memory.relatedFiles)) {
564
+ errors.push('relatedFiles must be an array');
565
+ }
566
+
567
+ if (errors.length > 0) {
568
+ throw new Error(`Invalid memory input: ${errors.join('; ')}`);
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Auto-save a memory (decision, pattern, or important context)
574
+ * This is called automatically during AI interactions
575
+ */
576
+ export async function autoSaveMemory(memory, cwd = process.cwd()) {
577
+ // Validate input
578
+ validateMemoryInput(memory);
579
+
580
+ await initAutoMemory(cwd);
581
+
582
+ const index = await loadAutoMemoryIndex(cwd);
583
+ const id = `mem_${Date.now().toString(36)}_${crypto.randomBytes(2).toString('hex')}`;
584
+
585
+ const memoryEntry = {
586
+ id,
587
+ type: memory.type || 'context', // 'decision', 'pattern', 'context', 'learning'
588
+ content: memory.content,
589
+ summary: memory.summary || null,
590
+ source: memory.source || 'auto', // 'auto', 'user', 'agent'
591
+ tags: memory.tags || [],
592
+ importance: memory.importance || 'normal', // 'low', 'normal', 'high', 'critical'
593
+ relatedFiles: memory.relatedFiles || [],
594
+ createdAt: new Date().toISOString(),
595
+ expiresAt: memory.expiresAt || null // null = never expires
596
+ };
597
+
598
+ // Add to appropriate array based on type
599
+ if (memory.type === 'decision') {
600
+ index.decisions.push(memoryEntry);
601
+ } else if (memory.type === 'pattern') {
602
+ index.patterns.push(memoryEntry);
603
+ } else {
604
+ index.memories.push(memoryEntry);
605
+ }
606
+
607
+ // Keep only last 100 memories per type (cleanup old ones)
608
+ if (index.memories.length > 100) {
609
+ index.memories = index.memories.slice(-100);
610
+ }
611
+ if (index.decisions.length > 50) {
612
+ index.decisions = index.decisions.slice(-50);
613
+ }
614
+ if (index.patterns.length > 50) {
615
+ index.patterns = index.patterns.slice(-50);
616
+ }
617
+
618
+ await saveAutoMemoryIndex(index, cwd);
619
+
620
+ return memoryEntry;
621
+ }
622
+
623
+ /**
624
+ * Get relevant memories for a query/context
625
+ * Uses keyword matching and recency
626
+ */
627
+ export async function getRelevantMemories(options = {}, cwd = process.cwd()) {
628
+ const {
629
+ query = null,
630
+ files = [],
631
+ types = ['decision', 'pattern', 'context', 'learning'],
632
+ limit = 10,
633
+ minImportance = 'low'
634
+ } = options;
635
+
636
+ const index = await loadAutoMemoryIndex(cwd);
637
+
638
+ // Combine all memory types
639
+ let allMemories = [
640
+ ...index.memories.map(m => ({ ...m, _type: 'memory' })),
641
+ ...index.decisions.map(m => ({ ...m, _type: 'decision' })),
642
+ ...index.patterns.map(m => ({ ...m, _type: 'pattern' }))
643
+ ];
644
+
645
+ // Filter by type
646
+ allMemories = allMemories.filter(m => types.includes(m.type));
647
+
648
+ // Filter by importance
649
+ const importanceOrder = ['low', 'normal', 'high', 'critical'];
650
+ const minIdx = importanceOrder.indexOf(minImportance);
651
+ allMemories = allMemories.filter(m => {
652
+ const memIdx = importanceOrder.indexOf(m.importance);
653
+ return memIdx >= minIdx;
654
+ });
655
+
656
+ // Filter expired
657
+ const now = new Date();
658
+ allMemories = allMemories.filter(m => {
659
+ if (!m.expiresAt) return true;
660
+ return new Date(m.expiresAt) > now;
661
+ });
662
+
663
+ // Score by relevance
664
+ allMemories = allMemories.map(m => {
665
+ let score = 0;
666
+
667
+ // Recency score (newer = higher)
668
+ const age = now - new Date(m.createdAt);
669
+ const hoursSinceCreated = age / (1000 * 60 * 60);
670
+ score += Math.max(0, 10 - hoursSinceCreated / 24); // Decays over 10 days
671
+
672
+ // Importance score
673
+ score += importanceOrder.indexOf(m.importance) * 2;
674
+
675
+ // Query match score
676
+ if (query) {
677
+ const queryLower = query.toLowerCase();
678
+ const content = (m.content + ' ' + (m.summary || '')).toLowerCase();
679
+ if (content.includes(queryLower)) {
680
+ score += 5;
681
+ }
682
+ // Check for word matches
683
+ const queryWords = queryLower.split(/\s+/);
684
+ for (const word of queryWords) {
685
+ if (word.length > 2 && content.includes(word)) {
686
+ score += 1;
687
+ }
688
+ }
689
+ }
690
+
691
+ // File match score
692
+ if (files.length > 0 && m.relatedFiles.length > 0) {
693
+ for (const file of files) {
694
+ if (m.relatedFiles.some(rf => rf.includes(file) || file.includes(rf))) {
695
+ score += 3;
696
+ }
697
+ }
698
+ }
699
+
700
+ // Tag match score
701
+ if (query && m.tags.length > 0) {
702
+ for (const tag of m.tags) {
703
+ if (query.toLowerCase().includes(tag.toLowerCase())) {
704
+ score += 2;
705
+ }
706
+ }
707
+ }
708
+
709
+ return { ...m, _score: score };
710
+ });
711
+
712
+ // Sort by score and limit
713
+ allMemories.sort((a, b) => b._score - a._score);
714
+ allMemories = allMemories.slice(0, limit);
715
+
716
+ return allMemories;
717
+ }
718
+
719
+ /**
720
+ * Generate auto-memory context for AI consumption
721
+ */
722
+ export async function generateAutoMemoryContext(options = {}, cwd = process.cwd()) {
723
+ const memories = await getRelevantMemories(options, cwd);
724
+
725
+ if (memories.length === 0) {
726
+ return null;
727
+ }
728
+
729
+ let context = '<auto-memories>\n';
730
+
731
+ // Group by type
732
+ const decisions = memories.filter(m => m.type === 'decision');
733
+ const patterns = memories.filter(m => m.type === 'pattern');
734
+ const others = memories.filter(m => !['decision', 'pattern'].includes(m.type));
735
+
736
+ if (decisions.length > 0) {
737
+ context += '## Prior Decisions\n';
738
+ for (const d of decisions) {
739
+ context += `- **${d.summary || 'Decision'}**: ${d.content}\n`;
740
+ }
741
+ context += '\n';
742
+ }
743
+
744
+ if (patterns.length > 0) {
745
+ context += '## Learned Patterns\n';
746
+ for (const p of patterns) {
747
+ context += `- ${p.content}\n`;
748
+ }
749
+ context += '\n';
750
+ }
751
+
752
+ if (others.length > 0) {
753
+ context += '## Relevant Context\n';
754
+ for (const m of others) {
755
+ context += `- ${m.content}\n`;
756
+ }
757
+ context += '\n';
758
+ }
759
+
760
+ context += '</auto-memories>';
761
+
762
+ return context;
763
+ }
764
+
765
+ /**
766
+ * Start a new session (for tracking current work)
767
+ */
768
+ export async function startSession(options = {}, cwd = process.cwd()) {
769
+ await initAutoMemory(cwd);
770
+
771
+ const session = {
772
+ id: `session_${Date.now().toString(36)}`,
773
+ startedAt: new Date().toISOString(),
774
+ task: options.task || null,
775
+ filesModified: [],
776
+ decisions: [],
777
+ memories: [],
778
+ status: 'active'
779
+ };
780
+
781
+ const sessionPath = getCurrentSessionPath(cwd);
782
+ await fs.writeJson(sessionPath, session, { spaces: 2 });
783
+
784
+ return session;
785
+ }
786
+
787
+ /**
788
+ * Get current session
789
+ */
790
+ export async function getCurrentSession(cwd = process.cwd()) {
791
+ const sessionPath = getCurrentSessionPath(cwd);
792
+
793
+ if (!(await fs.pathExists(sessionPath))) {
794
+ return null;
795
+ }
796
+
797
+ return await fs.readJson(sessionPath);
798
+ }
799
+
800
+ /**
801
+ * Update current session
802
+ */
803
+ export async function updateSession(updates, cwd = process.cwd()) {
804
+ let session = await getCurrentSession(cwd);
805
+
806
+ if (!session) {
807
+ session = await startSession({}, cwd);
808
+ }
809
+
810
+ // Merge updates
811
+ if (updates.task) session.task = updates.task;
812
+ if (updates.fileModified) {
813
+ if (!session.filesModified.includes(updates.fileModified)) {
814
+ session.filesModified.push(updates.fileModified);
815
+ }
816
+ }
817
+ if (updates.decision) {
818
+ session.decisions.push({
819
+ content: updates.decision,
820
+ timestamp: new Date().toISOString()
821
+ });
822
+ }
823
+ if (updates.memory) {
824
+ session.memories.push({
825
+ content: updates.memory,
826
+ timestamp: new Date().toISOString()
827
+ });
828
+ }
829
+
830
+ session.lastUpdatedAt = new Date().toISOString();
831
+
832
+ const sessionPath = getCurrentSessionPath(cwd);
833
+ await fs.writeJson(sessionPath, session, { spaces: 2 });
834
+
835
+ return session;
836
+ }
837
+
838
+ /**
839
+ * End session and save memories
840
+ */
841
+ export async function endSession(options = {}, cwd = process.cwd()) {
842
+ const session = await getCurrentSession(cwd);
843
+
844
+ if (!session) {
845
+ return null;
846
+ }
847
+
848
+ // Save important decisions as permanent memories
849
+ for (const decision of session.decisions) {
850
+ await autoSaveMemory({
851
+ type: 'decision',
852
+ content: decision.content,
853
+ relatedFiles: session.filesModified,
854
+ importance: 'high',
855
+ source: 'session'
856
+ }, cwd);
857
+ }
858
+
859
+ // Save session memories
860
+ for (const memory of session.memories) {
861
+ await autoSaveMemory({
862
+ type: 'context',
863
+ content: memory.content,
864
+ relatedFiles: session.filesModified,
865
+ importance: 'normal',
866
+ source: 'session'
867
+ }, cwd);
868
+ }
869
+
870
+ // Mark session as complete
871
+ session.status = 'completed';
872
+ session.endedAt = new Date().toISOString();
873
+ session.summary = options.summary || null;
874
+
875
+ // Archive session
876
+ const archivePath = path.join(getAutoMemoryDir(cwd), `sessions/${session.id}.json`);
877
+ await fs.ensureDir(path.dirname(archivePath));
878
+ await fs.writeJson(archivePath, session, { spaces: 2 });
879
+
880
+ // Clear current session
881
+ const sessionPath = getCurrentSessionPath(cwd);
882
+ await fs.remove(sessionPath);
883
+
884
+ return session;
885
+ }
886
+
887
+ /**
888
+ * Quick memory save (for inline use)
889
+ * Usage: await remember("User prefers tabs over spaces", { type: 'pattern' })
890
+ */
891
+ export async function remember(content, options = {}, cwd = process.cwd()) {
892
+ return await autoSaveMemory({
893
+ content,
894
+ type: options.type || 'context',
895
+ summary: options.summary,
896
+ importance: options.importance || 'normal',
897
+ tags: options.tags || [],
898
+ relatedFiles: options.files || [],
899
+ source: options.source || 'user'
900
+ }, cwd);
901
+ }
902
+
903
+ /**
904
+ * Recall memories (for inline use)
905
+ * Usage: const memories = await recall("authentication")
906
+ */
907
+ export async function recall(query, options = {}, cwd = process.cwd()) {
908
+ return await getRelevantMemories({
909
+ query,
910
+ limit: options.limit || 5,
911
+ types: options.types,
912
+ minImportance: options.minImportance
913
+ }, cwd);
914
+ }