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.
- package/package.json +2 -1
- package/src/cli.js +1012 -0
- package/src/conversation.js +914 -0
- package/src/indexer.js +944 -0
- package/src/templates/namnam.md +693 -0
- package/src/templates/platforms/AGENTS.md +47 -0
- package/src/watcher.js +356 -0
- package/src/templates/core/code-review.md +0 -70
- package/src/templates/core/git-commit.md +0 -57
- package/src/templates/core/git-push.md +0 -53
- package/src/templates/core/git-status.md +0 -48
- package/src/templates/core/namnam.md +0 -324
- package/src/templates/core/validate-and-fix.md +0 -69
|
@@ -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, '<')
|
|
63
|
+
.replace(/>/g, '>')
|
|
64
|
+
.replace(/"/g, '"')
|
|
65
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|