prjct-cli 0.9.2 → 0.10.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/CHANGELOG.md +142 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +299 -17
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/context-filter.js +83 -83
- package/core/agentic/ground-truth.js +591 -0
- package/core/agentic/loop-detector.js +406 -0
- package/core/agentic/memory-system.js +850 -0
- package/core/agentic/parallel-tools.js +366 -0
- package/core/agentic/plan-mode.js +572 -0
- package/core/agentic/prompt-builder.js +127 -2
- package/core/agentic/response-templates.js +290 -0
- package/core/agentic/semantic-compression.js +517 -0
- package/core/agentic/think-blocks.js +657 -0
- package/core/agentic/tool-registry.js +32 -0
- package/core/agentic/validation-rules.js +380 -0
- package/core/command-registry.js +48 -0
- package/core/commands.js +128 -60
- package/core/context-sync.js +183 -0
- package/core/domain/agent-generator.js +77 -46
- package/core/domain/agent-loader.js +183 -0
- package/core/domain/agent-matcher.js +217 -0
- package/core/domain/agent-validator.js +217 -0
- package/core/domain/context-estimator.js +175 -0
- package/core/domain/product-standards.js +92 -0
- package/core/domain/smart-cache.js +157 -0
- package/core/domain/task-analyzer.js +353 -0
- package/core/domain/tech-detector.js +365 -0
- package/package.json +8 -16
- package/templates/commands/done.md +7 -0
- package/templates/commands/feature.md +8 -0
- package/templates/commands/ship.md +8 -0
- package/templates/commands/spec.md +128 -0
- package/templates/global/CLAUDE.md +17 -0
- package/core/__tests__/agentic/agent-router.test.js +0 -398
- package/core/__tests__/agentic/command-executor.test.js +0 -223
- package/core/__tests__/agentic/context-builder.test.js +0 -160
- package/core/__tests__/agentic/context-filter.test.js +0 -494
- package/core/__tests__/agentic/prompt-builder.test.js +0 -212
- package/core/__tests__/agentic/template-loader.test.js +0 -164
- package/core/__tests__/agentic/tool-registry.test.js +0 -243
- package/core/__tests__/domain/agent-generator.test.js +0 -296
- package/core/__tests__/domain/analyzer.test.js +0 -324
- package/core/__tests__/infrastructure/author-detector.test.js +0 -103
- package/core/__tests__/infrastructure/config-manager.test.js +0 -454
- package/core/__tests__/infrastructure/path-manager.test.js +0 -412
- package/core/__tests__/setup.test.js +0 -15
- package/core/__tests__/utils/date-helper.test.js +0 -169
- package/core/__tests__/utils/file-helper.test.js +0 -258
- package/core/__tests__/utils/jsonl-helper.test.js +0 -387
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered Memory System
|
|
3
|
+
* Three-tier memory for learning user patterns and preferences
|
|
4
|
+
*
|
|
5
|
+
* OPTIMIZATION (P1.1): Pattern-Based Decision Making
|
|
6
|
+
* - Tier 1: Session memory (current command context)
|
|
7
|
+
* - Tier 2: Patterns (recurring decisions/preferences)
|
|
8
|
+
* - Tier 3: History (append-only JSONL for audit)
|
|
9
|
+
*
|
|
10
|
+
* P3.3: Enhanced with semantic tags and CRUD operations
|
|
11
|
+
* - Semantic tags for categorization
|
|
12
|
+
* - Auto-memory from user decisions
|
|
13
|
+
* - Relevance-based retrieval
|
|
14
|
+
* - CRUD operations (create/update/delete)
|
|
15
|
+
*
|
|
16
|
+
* Source: Windsurf create_memory, Augment remember patterns
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs').promises
|
|
20
|
+
const path = require('path')
|
|
21
|
+
const pathManager = require('../infrastructure/path-manager')
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* P3.3: Semantic tags for memory categorization
|
|
25
|
+
*/
|
|
26
|
+
const MEMORY_TAGS = {
|
|
27
|
+
// Code preferences
|
|
28
|
+
CODE_STYLE: 'code_style',
|
|
29
|
+
NAMING_CONVENTION: 'naming_convention',
|
|
30
|
+
FILE_STRUCTURE: 'file_structure',
|
|
31
|
+
|
|
32
|
+
// Workflow preferences
|
|
33
|
+
COMMIT_STYLE: 'commit_style',
|
|
34
|
+
BRANCH_NAMING: 'branch_naming',
|
|
35
|
+
TEST_BEHAVIOR: 'test_behavior',
|
|
36
|
+
SHIP_WORKFLOW: 'ship_workflow',
|
|
37
|
+
|
|
38
|
+
// Project context
|
|
39
|
+
TECH_STACK: 'tech_stack',
|
|
40
|
+
ARCHITECTURE: 'architecture',
|
|
41
|
+
DEPENDENCIES: 'dependencies',
|
|
42
|
+
|
|
43
|
+
// User preferences
|
|
44
|
+
OUTPUT_VERBOSITY: 'output_verbosity',
|
|
45
|
+
CONFIRMATION_LEVEL: 'confirmation_level',
|
|
46
|
+
AGENT_PREFERENCE: 'agent_preference'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class MemorySystem {
|
|
50
|
+
constructor() {
|
|
51
|
+
// Session memory (in-process, cleared on restart)
|
|
52
|
+
this._sessionMemory = new Map()
|
|
53
|
+
|
|
54
|
+
// Pattern cache (loaded from disk)
|
|
55
|
+
this._patterns = null
|
|
56
|
+
this._patternsLoaded = false
|
|
57
|
+
|
|
58
|
+
// P3.3: Memories database (semantic tagged)
|
|
59
|
+
this._memories = null
|
|
60
|
+
this._memoriesLoaded = false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ═══════════════════════════════════════════════════════════
|
|
64
|
+
// P3.3: SEMANTIC MEMORIES (tagged, searchable, CRUD)
|
|
65
|
+
// ═══════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get path to memories database
|
|
69
|
+
* @param {string} projectId
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
_getMemoriesPath(projectId) {
|
|
73
|
+
return path.join(
|
|
74
|
+
pathManager.getGlobalProjectPath(projectId),
|
|
75
|
+
'memory',
|
|
76
|
+
'memories.json'
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load memories database
|
|
82
|
+
* @param {string} projectId
|
|
83
|
+
* @returns {Promise<Object>}
|
|
84
|
+
*/
|
|
85
|
+
async loadMemories(projectId) {
|
|
86
|
+
if (this._memoriesLoaded && this._memories) {
|
|
87
|
+
return this._memories
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const memoriesPath = this._getMemoriesPath(projectId)
|
|
92
|
+
const content = await fs.readFile(memoriesPath, 'utf-8')
|
|
93
|
+
this._memories = JSON.parse(content)
|
|
94
|
+
this._memoriesLoaded = true
|
|
95
|
+
return this._memories
|
|
96
|
+
} catch {
|
|
97
|
+
this._memories = {
|
|
98
|
+
version: 1,
|
|
99
|
+
memories: [], // Array of memory entries
|
|
100
|
+
index: {} // Tag -> memory IDs index
|
|
101
|
+
}
|
|
102
|
+
this._memoriesLoaded = true
|
|
103
|
+
return this._memories
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Save memories database
|
|
109
|
+
* @param {string} projectId
|
|
110
|
+
*/
|
|
111
|
+
async saveMemories(projectId) {
|
|
112
|
+
if (!this._memories) return
|
|
113
|
+
|
|
114
|
+
const memoriesPath = this._getMemoriesPath(projectId)
|
|
115
|
+
await fs.mkdir(path.dirname(memoriesPath), { recursive: true })
|
|
116
|
+
await fs.writeFile(
|
|
117
|
+
memoriesPath,
|
|
118
|
+
JSON.stringify(this._memories, null, 2),
|
|
119
|
+
'utf-8'
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a new memory (Windsurf pattern)
|
|
125
|
+
*
|
|
126
|
+
* @param {string} projectId
|
|
127
|
+
* @param {Object} memory
|
|
128
|
+
* @param {string} memory.title - Short title
|
|
129
|
+
* @param {string} memory.content - Memory content
|
|
130
|
+
* @param {string[]} memory.tags - Semantic tags
|
|
131
|
+
* @param {boolean} memory.userTriggered - If user explicitly asked
|
|
132
|
+
* @returns {Promise<string>} Memory ID
|
|
133
|
+
*/
|
|
134
|
+
async createMemory(projectId, { title, content, tags = [], userTriggered = false }) {
|
|
135
|
+
const db = await this.loadMemories(projectId)
|
|
136
|
+
|
|
137
|
+
const memory = {
|
|
138
|
+
id: `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
139
|
+
title,
|
|
140
|
+
content,
|
|
141
|
+
tags,
|
|
142
|
+
userTriggered,
|
|
143
|
+
createdAt: new Date().toISOString(),
|
|
144
|
+
updatedAt: new Date().toISOString()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
db.memories.push(memory)
|
|
148
|
+
|
|
149
|
+
// Update tag index
|
|
150
|
+
for (const tag of tags) {
|
|
151
|
+
if (!db.index[tag]) db.index[tag] = []
|
|
152
|
+
db.index[tag].push(memory.id)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await this.saveMemories(projectId)
|
|
156
|
+
|
|
157
|
+
// Log to history
|
|
158
|
+
await this.appendHistory(projectId, {
|
|
159
|
+
type: 'memory_create',
|
|
160
|
+
memoryId: memory.id,
|
|
161
|
+
title,
|
|
162
|
+
tags,
|
|
163
|
+
userTriggered
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return memory.id
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update an existing memory
|
|
171
|
+
*
|
|
172
|
+
* @param {string} projectId
|
|
173
|
+
* @param {string} memoryId
|
|
174
|
+
* @param {Object} updates
|
|
175
|
+
* @returns {Promise<boolean>}
|
|
176
|
+
*/
|
|
177
|
+
async updateMemory(projectId, memoryId, updates) {
|
|
178
|
+
const db = await this.loadMemories(projectId)
|
|
179
|
+
|
|
180
|
+
const index = db.memories.findIndex(m => m.id === memoryId)
|
|
181
|
+
if (index === -1) return false
|
|
182
|
+
|
|
183
|
+
const memory = db.memories[index]
|
|
184
|
+
const oldTags = memory.tags || []
|
|
185
|
+
|
|
186
|
+
// Apply updates
|
|
187
|
+
if (updates.title) memory.title = updates.title
|
|
188
|
+
if (updates.content) memory.content = updates.content
|
|
189
|
+
if (updates.tags) {
|
|
190
|
+
// Update tag index
|
|
191
|
+
for (const tag of oldTags) {
|
|
192
|
+
if (db.index[tag]) {
|
|
193
|
+
db.index[tag] = db.index[tag].filter(id => id !== memoryId)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const tag of updates.tags) {
|
|
197
|
+
if (!db.index[tag]) db.index[tag] = []
|
|
198
|
+
db.index[tag].push(memoryId)
|
|
199
|
+
}
|
|
200
|
+
memory.tags = updates.tags
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
memory.updatedAt = new Date().toISOString()
|
|
204
|
+
|
|
205
|
+
await this.saveMemories(projectId)
|
|
206
|
+
|
|
207
|
+
await this.appendHistory(projectId, {
|
|
208
|
+
type: 'memory_update',
|
|
209
|
+
memoryId,
|
|
210
|
+
updates: Object.keys(updates)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Delete a memory
|
|
218
|
+
*
|
|
219
|
+
* @param {string} projectId
|
|
220
|
+
* @param {string} memoryId
|
|
221
|
+
* @returns {Promise<boolean>}
|
|
222
|
+
*/
|
|
223
|
+
async deleteMemory(projectId, memoryId) {
|
|
224
|
+
const db = await this.loadMemories(projectId)
|
|
225
|
+
|
|
226
|
+
const index = db.memories.findIndex(m => m.id === memoryId)
|
|
227
|
+
if (index === -1) return false
|
|
228
|
+
|
|
229
|
+
const memory = db.memories[index]
|
|
230
|
+
|
|
231
|
+
// Remove from tag index
|
|
232
|
+
for (const tag of memory.tags || []) {
|
|
233
|
+
if (db.index[tag]) {
|
|
234
|
+
db.index[tag] = db.index[tag].filter(id => id !== memoryId)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Remove memory
|
|
239
|
+
db.memories.splice(index, 1)
|
|
240
|
+
|
|
241
|
+
await this.saveMemories(projectId)
|
|
242
|
+
|
|
243
|
+
await this.appendHistory(projectId, {
|
|
244
|
+
type: 'memory_delete',
|
|
245
|
+
memoryId,
|
|
246
|
+
title: memory.title
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Find memories by tags
|
|
254
|
+
*
|
|
255
|
+
* @param {string} projectId
|
|
256
|
+
* @param {string[]} tags - Tags to search for
|
|
257
|
+
* @param {boolean} matchAll - If true, memory must have ALL tags
|
|
258
|
+
* @returns {Promise<Object[]>}
|
|
259
|
+
*/
|
|
260
|
+
async findByTags(projectId, tags, matchAll = false) {
|
|
261
|
+
const db = await this.loadMemories(projectId)
|
|
262
|
+
|
|
263
|
+
if (matchAll) {
|
|
264
|
+
// Memory must have ALL tags
|
|
265
|
+
return db.memories.filter(m =>
|
|
266
|
+
tags.every(tag => (m.tags || []).includes(tag))
|
|
267
|
+
)
|
|
268
|
+
} else {
|
|
269
|
+
// Memory must have ANY tag
|
|
270
|
+
const matchingIds = new Set()
|
|
271
|
+
for (const tag of tags) {
|
|
272
|
+
const ids = db.index[tag] || []
|
|
273
|
+
ids.forEach(id => matchingIds.add(id))
|
|
274
|
+
}
|
|
275
|
+
return db.memories.filter(m => matchingIds.has(m.id))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Search memories by content (simple text match)
|
|
281
|
+
*
|
|
282
|
+
* @param {string} projectId
|
|
283
|
+
* @param {string} query
|
|
284
|
+
* @returns {Promise<Object[]>}
|
|
285
|
+
*/
|
|
286
|
+
async searchMemories(projectId, query) {
|
|
287
|
+
const db = await this.loadMemories(projectId)
|
|
288
|
+
const queryLower = query.toLowerCase()
|
|
289
|
+
|
|
290
|
+
return db.memories.filter(m =>
|
|
291
|
+
m.title.toLowerCase().includes(queryLower) ||
|
|
292
|
+
m.content.toLowerCase().includes(queryLower)
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get relevant memories for current context
|
|
298
|
+
* Scores memories by relevance to context
|
|
299
|
+
*
|
|
300
|
+
* @param {string} projectId
|
|
301
|
+
* @param {Object} context - Current execution context
|
|
302
|
+
* @param {number} limit - Max memories to return
|
|
303
|
+
* @returns {Promise<Object[]>}
|
|
304
|
+
*/
|
|
305
|
+
async getRelevantMemories(projectId, context, limit = 5) {
|
|
306
|
+
const db = await this.loadMemories(projectId)
|
|
307
|
+
|
|
308
|
+
// Score each memory by relevance
|
|
309
|
+
const scored = db.memories.map(memory => {
|
|
310
|
+
let score = 0
|
|
311
|
+
|
|
312
|
+
// Tag relevance
|
|
313
|
+
const contextTags = this._extractContextTags(context)
|
|
314
|
+
for (const tag of memory.tags || []) {
|
|
315
|
+
if (contextTags.includes(tag)) score += 10
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Recency boost (more recent = higher score)
|
|
319
|
+
const age = Date.now() - new Date(memory.updatedAt).getTime()
|
|
320
|
+
const daysSinceUpdate = age / (1000 * 60 * 60 * 24)
|
|
321
|
+
score += Math.max(0, 5 - daysSinceUpdate) // Up to 5 points for recent
|
|
322
|
+
|
|
323
|
+
// User triggered memories are more important
|
|
324
|
+
if (memory.userTriggered) score += 5
|
|
325
|
+
|
|
326
|
+
// Content keyword match
|
|
327
|
+
const keywords = this._extractKeywords(context)
|
|
328
|
+
for (const keyword of keywords) {
|
|
329
|
+
if (memory.content.toLowerCase().includes(keyword)) score += 2
|
|
330
|
+
if (memory.title.toLowerCase().includes(keyword)) score += 3
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { ...memory, _score: score }
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// Sort by score and return top N
|
|
337
|
+
return scored
|
|
338
|
+
.filter(m => m._score > 0)
|
|
339
|
+
.sort((a, b) => b._score - a._score)
|
|
340
|
+
.slice(0, limit)
|
|
341
|
+
.map(({ _score, ...memory }) => memory)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Extract relevant tags from context
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
_extractContextTags(context) {
|
|
349
|
+
const tags = []
|
|
350
|
+
|
|
351
|
+
// Command-based tags
|
|
352
|
+
const commandTags = {
|
|
353
|
+
ship: [MEMORY_TAGS.COMMIT_STYLE, MEMORY_TAGS.SHIP_WORKFLOW, MEMORY_TAGS.TEST_BEHAVIOR],
|
|
354
|
+
feature: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.CODE_STYLE],
|
|
355
|
+
done: [MEMORY_TAGS.SHIP_WORKFLOW],
|
|
356
|
+
analyze: [MEMORY_TAGS.TECH_STACK, MEMORY_TAGS.ARCHITECTURE],
|
|
357
|
+
spec: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.CODE_STYLE]
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (context.commandName && commandTags[context.commandName]) {
|
|
361
|
+
tags.push(...commandTags[context.commandName])
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return tags
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Extract keywords from context for matching
|
|
369
|
+
* @private
|
|
370
|
+
*/
|
|
371
|
+
_extractKeywords(context) {
|
|
372
|
+
const keywords = []
|
|
373
|
+
|
|
374
|
+
// From params
|
|
375
|
+
if (context.params?.description) {
|
|
376
|
+
keywords.push(...context.params.description.toLowerCase().split(/\s+/))
|
|
377
|
+
}
|
|
378
|
+
if (context.params?.feature) {
|
|
379
|
+
keywords.push(...context.params.feature.toLowerCase().split(/\s+/))
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Filter common words
|
|
383
|
+
const stopWords = ['the', 'a', 'an', 'is', 'are', 'to', 'for', 'and', 'or', 'in']
|
|
384
|
+
return keywords.filter(k => k.length > 2 && !stopWords.includes(k))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Auto-create memory from user decision
|
|
389
|
+
* Called when user explicitly chooses something
|
|
390
|
+
*
|
|
391
|
+
* @param {string} projectId
|
|
392
|
+
* @param {string} decisionType - Type of decision
|
|
393
|
+
* @param {string} value - Chosen value
|
|
394
|
+
* @param {string} context - Context of decision
|
|
395
|
+
*/
|
|
396
|
+
async autoRemember(projectId, decisionType, value, context = '') {
|
|
397
|
+
// Map decision types to tags
|
|
398
|
+
const tagMap = {
|
|
399
|
+
commit_footer: [MEMORY_TAGS.COMMIT_STYLE],
|
|
400
|
+
branch_naming: [MEMORY_TAGS.BRANCH_NAMING],
|
|
401
|
+
test_before_ship: [MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.SHIP_WORKFLOW],
|
|
402
|
+
preferred_agent: [MEMORY_TAGS.AGENT_PREFERENCE],
|
|
403
|
+
code_style: [MEMORY_TAGS.CODE_STYLE],
|
|
404
|
+
verbosity: [MEMORY_TAGS.OUTPUT_VERBOSITY]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const tags = tagMap[decisionType] || []
|
|
408
|
+
|
|
409
|
+
// Check if similar memory exists
|
|
410
|
+
const existing = await this.searchMemories(projectId, decisionType)
|
|
411
|
+
if (existing.length > 0) {
|
|
412
|
+
// Update existing
|
|
413
|
+
await this.updateMemory(projectId, existing[0].id, {
|
|
414
|
+
content: `${decisionType}: ${value}`,
|
|
415
|
+
tags
|
|
416
|
+
})
|
|
417
|
+
} else {
|
|
418
|
+
// Create new
|
|
419
|
+
await this.createMemory(projectId, {
|
|
420
|
+
title: `Preference: ${decisionType}`,
|
|
421
|
+
content: `${decisionType}: ${value}${context ? `\nContext: ${context}` : ''}`,
|
|
422
|
+
tags,
|
|
423
|
+
userTriggered: true
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get all memories (for debugging/display)
|
|
430
|
+
* @param {string} projectId
|
|
431
|
+
* @returns {Promise<Object[]>}
|
|
432
|
+
*/
|
|
433
|
+
async getAllMemories(projectId) {
|
|
434
|
+
const db = await this.loadMemories(projectId)
|
|
435
|
+
return db.memories
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get memory stats
|
|
440
|
+
* @param {string} projectId
|
|
441
|
+
* @returns {Promise<Object>}
|
|
442
|
+
*/
|
|
443
|
+
async getMemoryStats(projectId) {
|
|
444
|
+
const db = await this.loadMemories(projectId)
|
|
445
|
+
|
|
446
|
+
const tagCounts = {}
|
|
447
|
+
for (const [tag, ids] of Object.entries(db.index)) {
|
|
448
|
+
tagCounts[tag] = ids.length
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
totalMemories: db.memories.length,
|
|
453
|
+
userTriggered: db.memories.filter(m => m.userTriggered).length,
|
|
454
|
+
tagCounts,
|
|
455
|
+
oldestMemory: db.memories[0]?.createdAt,
|
|
456
|
+
newestMemory: db.memories[db.memories.length - 1]?.createdAt
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ═══════════════════════════════════════════════════════════
|
|
461
|
+
// TIER 1: Session Memory (ephemeral, single command context)
|
|
462
|
+
// ═══════════════════════════════════════════════════════════
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Store value in session memory
|
|
466
|
+
* @param {string} key - Memory key
|
|
467
|
+
* @param {any} value - Value to store
|
|
468
|
+
*/
|
|
469
|
+
setSession(key, value) {
|
|
470
|
+
this._sessionMemory.set(key, {
|
|
471
|
+
value,
|
|
472
|
+
timestamp: Date.now()
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get value from session memory
|
|
478
|
+
* @param {string} key - Memory key
|
|
479
|
+
* @returns {any} Stored value or undefined
|
|
480
|
+
*/
|
|
481
|
+
getSession(key) {
|
|
482
|
+
const entry = this._sessionMemory.get(key)
|
|
483
|
+
return entry?.value
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Clear session memory
|
|
488
|
+
*/
|
|
489
|
+
clearSession() {
|
|
490
|
+
this._sessionMemory.clear()
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ═══════════════════════════════════════════════════════════
|
|
494
|
+
// TIER 2: Patterns (persistent, learned preferences)
|
|
495
|
+
// ═══════════════════════════════════════════════════════════
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get path to patterns file
|
|
499
|
+
* @param {string} projectId - Project ID
|
|
500
|
+
* @returns {string} Path to patterns.json
|
|
501
|
+
*/
|
|
502
|
+
_getPatternsPath(projectId) {
|
|
503
|
+
return path.join(
|
|
504
|
+
pathManager.getGlobalProjectPath(projectId),
|
|
505
|
+
'memory',
|
|
506
|
+
'patterns.json'
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Load patterns from disk
|
|
512
|
+
* @param {string} projectId - Project ID
|
|
513
|
+
* @returns {Promise<Object>} Patterns object
|
|
514
|
+
*/
|
|
515
|
+
async loadPatterns(projectId) {
|
|
516
|
+
if (this._patternsLoaded && this._patterns) {
|
|
517
|
+
return this._patterns
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const patternsPath = this._getPatternsPath(projectId)
|
|
522
|
+
const content = await fs.readFile(patternsPath, 'utf-8')
|
|
523
|
+
this._patterns = JSON.parse(content)
|
|
524
|
+
this._patternsLoaded = true
|
|
525
|
+
return this._patterns
|
|
526
|
+
} catch {
|
|
527
|
+
// Initialize empty patterns
|
|
528
|
+
this._patterns = {
|
|
529
|
+
version: 1,
|
|
530
|
+
decisions: {}, // Key decisions (e.g., commit_footer, branch_naming)
|
|
531
|
+
preferences: {}, // User preferences (e.g., output_verbosity)
|
|
532
|
+
workflows: {}, // Workflow patterns (e.g., quick_ship for small changes)
|
|
533
|
+
counters: {} // Usage counters for learning
|
|
534
|
+
}
|
|
535
|
+
this._patternsLoaded = true
|
|
536
|
+
return this._patterns
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Save patterns to disk
|
|
542
|
+
* @param {string} projectId - Project ID
|
|
543
|
+
*/
|
|
544
|
+
async savePatterns(projectId) {
|
|
545
|
+
if (!this._patterns) return
|
|
546
|
+
|
|
547
|
+
const patternsPath = this._getPatternsPath(projectId)
|
|
548
|
+
|
|
549
|
+
// Ensure directory exists
|
|
550
|
+
await fs.mkdir(path.dirname(patternsPath), { recursive: true })
|
|
551
|
+
|
|
552
|
+
await fs.writeFile(
|
|
553
|
+
patternsPath,
|
|
554
|
+
JSON.stringify(this._patterns, null, 2),
|
|
555
|
+
'utf-8'
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Record a decision pattern
|
|
561
|
+
* After 3 consistent uses, pattern becomes "learned"
|
|
562
|
+
*
|
|
563
|
+
* @param {string} projectId - Project ID
|
|
564
|
+
* @param {string} key - Decision key (e.g., "commit_footer")
|
|
565
|
+
* @param {string} value - Decision value
|
|
566
|
+
* @param {string} context - Context where decision was made
|
|
567
|
+
*/
|
|
568
|
+
async recordDecision(projectId, key, value, context = '') {
|
|
569
|
+
const patterns = await this.loadPatterns(projectId)
|
|
570
|
+
|
|
571
|
+
// Initialize or update decision
|
|
572
|
+
if (!patterns.decisions[key]) {
|
|
573
|
+
patterns.decisions[key] = {
|
|
574
|
+
value,
|
|
575
|
+
count: 1,
|
|
576
|
+
firstSeen: new Date().toISOString(),
|
|
577
|
+
lastSeen: new Date().toISOString(),
|
|
578
|
+
confidence: 'low',
|
|
579
|
+
contexts: [context].filter(Boolean)
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
const decision = patterns.decisions[key]
|
|
583
|
+
|
|
584
|
+
if (decision.value === value) {
|
|
585
|
+
// Same value - increase confidence
|
|
586
|
+
decision.count++
|
|
587
|
+
decision.lastSeen = new Date().toISOString()
|
|
588
|
+
if (context && !decision.contexts.includes(context)) {
|
|
589
|
+
decision.contexts.push(context)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Update confidence based on count
|
|
593
|
+
if (decision.count >= 5) {
|
|
594
|
+
decision.confidence = 'high'
|
|
595
|
+
} else if (decision.count >= 3) {
|
|
596
|
+
decision.confidence = 'medium'
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
// Different value - reset if new value is used more
|
|
600
|
+
decision.value = value
|
|
601
|
+
decision.count = 1
|
|
602
|
+
decision.lastSeen = new Date().toISOString()
|
|
603
|
+
decision.confidence = 'low'
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
await this.savePatterns(projectId)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Get a learned decision
|
|
612
|
+
* Returns null if not learned (confidence < medium)
|
|
613
|
+
*
|
|
614
|
+
* @param {string} projectId - Project ID
|
|
615
|
+
* @param {string} key - Decision key
|
|
616
|
+
* @returns {Promise<{value: string, confidence: string}|null>}
|
|
617
|
+
*/
|
|
618
|
+
async getDecision(projectId, key) {
|
|
619
|
+
const patterns = await this.loadPatterns(projectId)
|
|
620
|
+
const decision = patterns.decisions[key]
|
|
621
|
+
|
|
622
|
+
if (!decision) return null
|
|
623
|
+
|
|
624
|
+
// Only return if confidence is at least medium
|
|
625
|
+
if (decision.confidence === 'low') return null
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
value: decision.value,
|
|
629
|
+
confidence: decision.confidence
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Check if a pattern exists (for quick checks)
|
|
635
|
+
* @param {string} projectId - Project ID
|
|
636
|
+
* @param {string} key - Pattern key
|
|
637
|
+
* @returns {Promise<boolean>}
|
|
638
|
+
*/
|
|
639
|
+
async hasPattern(projectId, key) {
|
|
640
|
+
const decision = await this.getDecision(projectId, key)
|
|
641
|
+
return decision !== null
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Record a workflow pattern
|
|
646
|
+
* E.g., "user ships docs changes without running tests"
|
|
647
|
+
*
|
|
648
|
+
* @param {string} projectId - Project ID
|
|
649
|
+
* @param {string} workflowName - Workflow identifier
|
|
650
|
+
* @param {Object} pattern - Workflow pattern details
|
|
651
|
+
*/
|
|
652
|
+
async recordWorkflow(projectId, workflowName, pattern) {
|
|
653
|
+
const patterns = await this.loadPatterns(projectId)
|
|
654
|
+
|
|
655
|
+
if (!patterns.workflows[workflowName]) {
|
|
656
|
+
patterns.workflows[workflowName] = {
|
|
657
|
+
...pattern,
|
|
658
|
+
count: 1,
|
|
659
|
+
firstSeen: new Date().toISOString(),
|
|
660
|
+
lastSeen: new Date().toISOString()
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
patterns.workflows[workflowName].count++
|
|
664
|
+
patterns.workflows[workflowName].lastSeen = new Date().toISOString()
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
await this.savePatterns(projectId)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Get workflow pattern if learned
|
|
672
|
+
* @param {string} projectId - Project ID
|
|
673
|
+
* @param {string} workflowName - Workflow identifier
|
|
674
|
+
* @returns {Promise<Object|null>}
|
|
675
|
+
*/
|
|
676
|
+
async getWorkflow(projectId, workflowName) {
|
|
677
|
+
const patterns = await this.loadPatterns(projectId)
|
|
678
|
+
const workflow = patterns.workflows[workflowName]
|
|
679
|
+
|
|
680
|
+
if (!workflow || workflow.count < 3) return null
|
|
681
|
+
|
|
682
|
+
return workflow
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Set user preference
|
|
687
|
+
* @param {string} projectId - Project ID
|
|
688
|
+
* @param {string} key - Preference key
|
|
689
|
+
* @param {any} value - Preference value
|
|
690
|
+
*/
|
|
691
|
+
async setPreference(projectId, key, value) {
|
|
692
|
+
const patterns = await this.loadPatterns(projectId)
|
|
693
|
+
patterns.preferences[key] = {
|
|
694
|
+
value,
|
|
695
|
+
updatedAt: new Date().toISOString()
|
|
696
|
+
}
|
|
697
|
+
await this.savePatterns(projectId)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Get user preference
|
|
702
|
+
* @param {string} projectId - Project ID
|
|
703
|
+
* @param {string} key - Preference key
|
|
704
|
+
* @param {any} defaultValue - Default if not set
|
|
705
|
+
* @returns {Promise<any>}
|
|
706
|
+
*/
|
|
707
|
+
async getPreference(projectId, key, defaultValue = null) {
|
|
708
|
+
const patterns = await this.loadPatterns(projectId)
|
|
709
|
+
return patterns.preferences[key]?.value ?? defaultValue
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ═══════════════════════════════════════════════════════════
|
|
713
|
+
// TIER 3: History (append-only JSONL audit log)
|
|
714
|
+
// ═══════════════════════════════════════════════════════════
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Get path to today's session file
|
|
718
|
+
* @param {string} projectId - Project ID
|
|
719
|
+
* @returns {string} Path to session JSONL
|
|
720
|
+
*/
|
|
721
|
+
_getSessionPath(projectId) {
|
|
722
|
+
const now = new Date()
|
|
723
|
+
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
724
|
+
const day = now.toISOString().split('T')[0]
|
|
725
|
+
|
|
726
|
+
return path.join(
|
|
727
|
+
pathManager.getGlobalProjectPath(projectId),
|
|
728
|
+
'memory',
|
|
729
|
+
'sessions',
|
|
730
|
+
yearMonth,
|
|
731
|
+
`${day}.jsonl`
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Append entry to history (JSONL)
|
|
737
|
+
* @param {string} projectId - Project ID
|
|
738
|
+
* @param {Object} entry - Entry to log
|
|
739
|
+
*/
|
|
740
|
+
async appendHistory(projectId, entry) {
|
|
741
|
+
const sessionPath = this._getSessionPath(projectId)
|
|
742
|
+
|
|
743
|
+
// Ensure directory exists
|
|
744
|
+
await fs.mkdir(path.dirname(sessionPath), { recursive: true })
|
|
745
|
+
|
|
746
|
+
const logEntry = {
|
|
747
|
+
ts: new Date().toISOString(),
|
|
748
|
+
...entry
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
await fs.appendFile(
|
|
752
|
+
sessionPath,
|
|
753
|
+
JSON.stringify(logEntry) + '\n',
|
|
754
|
+
'utf-8'
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Read recent history entries
|
|
760
|
+
* @param {string} projectId - Project ID
|
|
761
|
+
* @param {number} limit - Max entries to return
|
|
762
|
+
* @returns {Promise<Object[]>}
|
|
763
|
+
*/
|
|
764
|
+
async getRecentHistory(projectId, limit = 20) {
|
|
765
|
+
try {
|
|
766
|
+
const sessionPath = this._getSessionPath(projectId)
|
|
767
|
+
const content = await fs.readFile(sessionPath, 'utf-8')
|
|
768
|
+
const lines = content.trim().split('\n').filter(Boolean)
|
|
769
|
+
|
|
770
|
+
return lines
|
|
771
|
+
.slice(-limit)
|
|
772
|
+
.map(line => {
|
|
773
|
+
try {
|
|
774
|
+
return JSON.parse(line)
|
|
775
|
+
} catch {
|
|
776
|
+
return null
|
|
777
|
+
}
|
|
778
|
+
})
|
|
779
|
+
.filter(Boolean)
|
|
780
|
+
} catch {
|
|
781
|
+
return []
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ═══════════════════════════════════════════════════════════
|
|
786
|
+
// CONVENIENCE: Combined operations
|
|
787
|
+
// ═══════════════════════════════════════════════════════════
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Smart decision: Check pattern first, ask user only if unknown
|
|
791
|
+
* Returns existing pattern or null (caller should ask user)
|
|
792
|
+
*
|
|
793
|
+
* @param {string} projectId - Project ID
|
|
794
|
+
* @param {string} key - Decision key
|
|
795
|
+
* @returns {Promise<string|null>} Known value or null
|
|
796
|
+
*/
|
|
797
|
+
async getSmartDecision(projectId, key) {
|
|
798
|
+
// Check session first (most recent)
|
|
799
|
+
const sessionValue = this.getSession(`decision:${key}`)
|
|
800
|
+
if (sessionValue !== undefined) return sessionValue
|
|
801
|
+
|
|
802
|
+
// Check learned patterns
|
|
803
|
+
const pattern = await this.getDecision(projectId, key)
|
|
804
|
+
if (pattern) return pattern.value
|
|
805
|
+
|
|
806
|
+
return null
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Record decision and store in session
|
|
811
|
+
* @param {string} projectId - Project ID
|
|
812
|
+
* @param {string} key - Decision key
|
|
813
|
+
* @param {string} value - Decision value
|
|
814
|
+
* @param {string} context - Context
|
|
815
|
+
*/
|
|
816
|
+
async learnDecision(projectId, key, value, context = '') {
|
|
817
|
+
// Store in session for immediate reuse
|
|
818
|
+
this.setSession(`decision:${key}`, value)
|
|
819
|
+
|
|
820
|
+
// Record in patterns for future sessions
|
|
821
|
+
await this.recordDecision(projectId, key, value, context)
|
|
822
|
+
|
|
823
|
+
// Log to history
|
|
824
|
+
await this.appendHistory(projectId, {
|
|
825
|
+
type: 'decision',
|
|
826
|
+
key,
|
|
827
|
+
value,
|
|
828
|
+
context
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Get all patterns summary (for debugging/display)
|
|
834
|
+
* @param {string} projectId - Project ID
|
|
835
|
+
* @returns {Promise<Object>}
|
|
836
|
+
*/
|
|
837
|
+
async getPatternsSummary(projectId) {
|
|
838
|
+
const patterns = await this.loadPatterns(projectId)
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
decisions: Object.keys(patterns.decisions).length,
|
|
842
|
+
learnedDecisions: Object.values(patterns.decisions)
|
|
843
|
+
.filter(d => d.confidence !== 'low').length,
|
|
844
|
+
workflows: Object.keys(patterns.workflows).length,
|
|
845
|
+
preferences: Object.keys(patterns.preferences).length
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
module.exports = new MemorySystem()
|