dave-code 1.0.4 → 1.2.0

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,264 @@
1
+ const ANSI_RE = /\x1b\[[0-9;?]*[a-zA-Z]/g;
2
+ const CONTROL_RE = /\x1b(?:\][\s\S]*?(?:\x07|\x1b\\)|[PX^_][\s\S]*?\x1b\\|(?:\[[0-?]*[ -/]*[@-~])|.)/g;
3
+
4
+ const STYLE = {
5
+ reset: '\x1b[0m',
6
+ bold: '\x1b[1m',
7
+ dim: '\x1b[2m',
8
+ italic: '\x1b[3m',
9
+ underline: '\x1b[4m',
10
+ strike: '\x1b[9m',
11
+ brand: '\x1b[1;38;2;250;100;30m',
12
+ cyan: '\x1b[36m',
13
+ muted: '\x1b[90m',
14
+ code: '\x1b[38;5;223m',
15
+ codeBackground: '\x1b[48;5;236m'
16
+ };
17
+
18
+ function clean(text) {
19
+ return String(text ?? '')
20
+ .replace(CONTROL_RE, '')
21
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '');
22
+ }
23
+
24
+ function width(text) {
25
+ let result = 0;
26
+ for (const char of String(text).replace(ANSI_RE, '')) {
27
+ const code = char.codePointAt(0);
28
+ result += (
29
+ (code >= 0x1100 && code <= 0x115f) ||
30
+ (code >= 0x2e80 && code <= 0xa4cf) ||
31
+ (code >= 0xac00 && code <= 0xd7a3) ||
32
+ (code >= 0xf900 && code <= 0xfaff) ||
33
+ (code >= 0xfe10 && code <= 0xfe6f) ||
34
+ (code >= 0xff00 && code <= 0xff60) ||
35
+ (code >= 0x1f300 && code <= 0x1faff)
36
+ ) ? 2 : 1;
37
+ }
38
+ return result;
39
+ }
40
+
41
+ function takeWidth(text, maxWidth) {
42
+ let value = '';
43
+ for (const char of text) {
44
+ if (width(value + char) > maxWidth) break;
45
+ value += char;
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function wrapPlain(text, maxWidth) {
51
+ if (!text) return [''];
52
+ const lines = [];
53
+ let remaining = text;
54
+ while (width(remaining) > maxWidth) {
55
+ let part = takeWidth(remaining, maxWidth);
56
+ const lastSpace = Math.max(part.lastIndexOf(' '), part.lastIndexOf('\t'));
57
+ if (lastSpace > Math.floor(part.length * 0.45)) part = part.slice(0, lastSpace);
58
+ if (!part) part = [...remaining][0];
59
+ lines.push(part.trimEnd());
60
+ remaining = remaining.slice(part.length).trimStart();
61
+ }
62
+ lines.push(remaining);
63
+ return lines;
64
+ }
65
+
66
+ const SUPERSCRIPT = { '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹', '+': '⁺', '-': '⁻', '=': '⁼', '(': '⁽', ')': '⁾', n: 'ⁿ', i: 'ⁱ' };
67
+ const SUBSCRIPT = { '0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉', '+': '₊', '-': '₋', '=': '₌', '(': '₍', ')': '₎', a: 'ₐ', e: 'ₑ', h: 'ₕ', i: 'ᵢ', j: 'ⱼ', k: 'ₖ', l: 'ₗ', m: 'ₘ', n: 'ₙ', o: 'ₒ', p: 'ₚ', r: 'ᵣ', s: 'ₛ', t: 'ₜ', u: 'ᵤ', v: 'ᵥ', x: 'ₓ' };
68
+
69
+ function script(value, map, fallbackPrefix) {
70
+ const converted = [...value].map(char => map[char] || '').join('');
71
+ return converted.length === [...value].length ? converted : `${fallbackPrefix}(${value})`;
72
+ }
73
+
74
+ export function latexToUnicode(source) {
75
+ let value = clean(source).trim();
76
+ for (let pass = 0; pass < 4; pass++) {
77
+ value = value.replace(/\\frac\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g, '($1)⁄($2)');
78
+ value = value.replace(/\\sqrt\s*\{([^{}]+)\}/g, '√($1)');
79
+ }
80
+ const replacements = {
81
+ '\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ', '\\epsilon': 'ε', '\\theta': 'θ', '\\lambda': 'λ', '\\mu': 'μ', '\\pi': 'π', '\\rho': 'ρ', '\\sigma': 'σ', '\\phi': 'φ', '\\omega': 'ω',
82
+ '\\Delta': 'Δ', '\\Theta': 'Θ', '\\Lambda': 'Λ', '\\Pi': 'Π', '\\Sigma': 'Σ', '\\Phi': 'Φ', '\\Omega': 'Ω',
83
+ '\\times': '×', '\\cdot': '·', '\\div': '÷', '\\pm': '±', '\\leq': '≤', '\\le': '≤', '\\geq': '≥', '\\ge': '≥', '\\neq': '≠', '\\ne': '≠', '\\approx': '≈', '\\infty': '∞', '\\sum': '∑', '\\prod': '∏', '\\int': '∫', '\\partial': '∂', '\\nabla': '∇', '\\rightarrow': '→', '\\leftarrow': '←', '\\Rightarrow': '⇒', '\\in': '∈', '\\notin': '∉', '\\cup': '∪', '\\cap': '∩', '\\quad': ' ', '\\qquad': ' ', '\\;': ' ', '\\!': ''
84
+ };
85
+ for (const [from, to] of Object.entries(replacements).sort((a, b) => b[0].length - a[0].length)) {
86
+ value = value.split(from).join(to);
87
+ }
88
+ value = value
89
+ .replace(/\^\{([^{}]+)\}/g, (_, item) => script(item, SUPERSCRIPT, '^'))
90
+ .replace(/_\{([^{}]+)\}/g, (_, item) => script(item, SUBSCRIPT, '_'))
91
+ .replace(/\^([0-9n i+\-=])/g, (_, item) => script(item, SUPERSCRIPT, '^'))
92
+ .replace(/_([0-9aehijklmnoprstuvx+\-=])/g, (_, item) => script(item, SUBSCRIPT, '_'))
93
+ .replace(/\\(?:left|right|mathrm|mathbf|text)\b/g, '')
94
+ .replace(/[{}]/g, '')
95
+ .replace(/\\,/g, ' ')
96
+ .replace(/\\ /g, ' ');
97
+ return value;
98
+ }
99
+
100
+ function renderInline(source, useColor) {
101
+ const protectedValues = [];
102
+ const protect = value => {
103
+ const id = `\u0001${protectedValues.length}\u0002`;
104
+ protectedValues.push(value);
105
+ return id;
106
+ };
107
+ let value = source
108
+ .replace(/`([^`]+)`/g, (_, code) => protect(useColor ? `${STYLE.codeBackground}${STYLE.code} ${code} ${STYLE.reset}` : code))
109
+ .replace(/\\\(([^\n]+?)\\\)/g, (_, math) => protect(useColor ? `${STYLE.cyan}${latexToUnicode(math)}${STYLE.reset}` : latexToUnicode(math)))
110
+ .replace(/\$([^$\n]+)\$/g, (_, math) => protect(useColor ? `${STYLE.cyan}${latexToUnicode(math)}${STYLE.reset}` : latexToUnicode(math)));
111
+ value = value
112
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => useColor ? `${STYLE.underline}${label}${STYLE.reset} ${STYLE.muted}(${url})${STYLE.reset}` : `${label} (${url})`)
113
+ .replace(/\*\*([^*]+)\*\*/g, (_, text) => useColor ? `${STYLE.bold}${text}${STYLE.reset}` : text)
114
+ .replace(/__([^_]+)__/g, (_, text) => useColor ? `${STYLE.bold}${text}${STYLE.reset}` : text)
115
+ .replace(/~~([^~]+)~~/g, (_, text) => useColor ? `${STYLE.strike}${text}${STYLE.reset}` : text)
116
+ .replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, (_, text) => useColor ? `${STYLE.italic}${text}${STYLE.reset}` : text);
117
+ return value.replace(/\u0001(\d+)\u0002/g, (_, index) => protectedValues[Number(index)] || '');
118
+ }
119
+
120
+ function tableCells(line) {
121
+ return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim());
122
+ }
123
+
124
+ function isTableDivider(line) {
125
+ const cells = tableCells(line);
126
+ return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell));
127
+ }
128
+
129
+ function renderTable(rows, maxWidth, useColor) {
130
+ const cells = rows.map(tableCells);
131
+ const columns = Math.max(...cells.map(row => row.length));
132
+ const rendered = cells.map(row => Array.from({ length: columns }, (_, index) => renderInline(row[index] || '', useColor)));
133
+ const widths = Array.from({ length: columns }, (_, column) => Math.max(1, ...rendered.map(row => width(row[column]))));
134
+ const totalWidth = widths.reduce((sum, item) => sum + item + 3, 1);
135
+ if (totalWidth > maxWidth) {
136
+ const output = [];
137
+ for (const row of rendered.slice(1)) {
138
+ row.forEach((value, index) => {
139
+ const heading = useColor ? `${STYLE.bold}${cells[0][index] || `Column ${index + 1}`}${STYLE.reset}` : (cells[0][index] || `Column ${index + 1}`);
140
+ output.push(` ${heading}: ${value}`);
141
+ });
142
+ output.push('');
143
+ }
144
+ return output.slice(0, -1);
145
+ }
146
+ const border = (left, middle, right, fill = '─') => `${left}${widths.map(item => fill.repeat(item + 2)).join(middle)}${right}`;
147
+ const rowLine = (row, header = false) => `│${row.map((cell, index) => {
148
+ const padding = ' '.repeat(widths[index] - width(cell));
149
+ const value = header && useColor ? `${STYLE.bold}${cell}${STYLE.reset}` : cell;
150
+ return ` ${value}${padding} `;
151
+ }).join('│')}│`;
152
+ return [border('┌', '┬', '┐'), rowLine(rendered[0], true), border('├', '┼', '┤'), ...rendered.slice(1).map(row => rowLine(row)), border('└', '┴', '┘')];
153
+ }
154
+
155
+ export function renderTerminalMarkdown(markdown, { color = true, width: requestedWidth = 80 } = {}) {
156
+ const useColor = Boolean(color);
157
+ const maxWidth = Math.max(24, Math.min(Number(requestedWidth) || 80, 110));
158
+ const lines = clean(markdown).replace(/\r\n?/g, '\n').split('\n');
159
+ const output = [];
160
+ let inCode = false;
161
+ let codeLanguage = '';
162
+ let codeLines = [];
163
+ let inMath = false;
164
+ let mathLines = [];
165
+
166
+ const flushCode = () => {
167
+ const label = codeLanguage ? ` ${codeLanguage} ` : ' code ';
168
+ output.push(useColor ? `${STYLE.muted}┌─${label}${'─'.repeat(Math.max(0, maxWidth - width(label) - 2))}${STYLE.reset}` : `┌─${label}${'─'.repeat(Math.max(0, maxWidth - width(label) - 2))}`);
169
+ for (const codeLine of codeLines) {
170
+ const wrapped = wrapPlain(codeLine, maxWidth - 3);
171
+ for (const part of wrapped) {
172
+ const padding = ' '.repeat(Math.max(0, maxWidth - 2 - width(part)));
173
+ output.push(useColor ? `${STYLE.codeBackground}${STYLE.code}│ ${part}${padding}${STYLE.reset}` : `│ ${part}`);
174
+ }
175
+ }
176
+ output.push(useColor ? `${STYLE.muted}└${'─'.repeat(maxWidth - 1)}${STYLE.reset}` : `└${'─'.repeat(maxWidth - 1)}`);
177
+ codeLines = [];
178
+ codeLanguage = '';
179
+ };
180
+
181
+ const flushMath = () => {
182
+ const formula = latexToUnicode(mathLines.join(' '));
183
+ const left = Math.max(2, Math.floor((maxWidth - width(formula)) / 2));
184
+ output.push(`${' '.repeat(left)}${useColor ? `${STYLE.cyan}${STYLE.bold}${formula}${STYLE.reset}` : formula}`);
185
+ mathLines = [];
186
+ };
187
+
188
+ for (let index = 0; index < lines.length; index++) {
189
+ const line = lines[index];
190
+ const fence = line.match(/^\s*```\s*([^`]*)$/);
191
+ if (fence) {
192
+ if (inCode) flushCode();
193
+ else codeLanguage = fence[1].trim();
194
+ inCode = !inCode;
195
+ continue;
196
+ }
197
+ if (inCode) {
198
+ codeLines.push(line);
199
+ continue;
200
+ }
201
+ const singleLineMath = line.match(/^\s*\$\$\s*(.+?)\s*\$\$\s*$/);
202
+ if (singleLineMath) {
203
+ mathLines = [singleLineMath[1]];
204
+ flushMath();
205
+ continue;
206
+ }
207
+ if (/^\s*(?:\$\$|\\\[|\\\])\s*$/.test(line)) {
208
+ if (inMath) flushMath();
209
+ inMath = !inMath;
210
+ continue;
211
+ }
212
+ if (inMath) {
213
+ mathLines.push(line);
214
+ continue;
215
+ }
216
+ if (line.includes('|') && index + 1 < lines.length && isTableDivider(lines[index + 1])) {
217
+ const table = [line];
218
+ index += 2;
219
+ while (index < lines.length && lines[index].includes('|') && lines[index].trim()) {
220
+ table.push(lines[index]);
221
+ index++;
222
+ }
223
+ index--;
224
+ output.push(...renderTable(table, maxWidth, useColor));
225
+ continue;
226
+ }
227
+ const heading = line.match(/^\s*(#{1,6})\s+(.+)$/);
228
+ if (heading) {
229
+ const text = renderInline(heading[2], useColor);
230
+ if (output.length && output.at(-1) !== '') output.push('');
231
+ output.push(useColor ? `${heading[1].length <= 2 ? STYLE.brand : STYLE.bold}${text}${STYLE.reset}` : text);
232
+ if (heading[1].length === 1) output.push(useColor ? `${STYLE.muted}${'─'.repeat(Math.min(width(heading[2]), maxWidth))}${STYLE.reset}` : '─'.repeat(Math.min(width(heading[2]), maxWidth)));
233
+ continue;
234
+ }
235
+ if (/^\s*(?:---+|___+|\*\*\*+)\s*$/.test(line)) {
236
+ output.push(useColor ? `${STYLE.muted}${'─'.repeat(maxWidth)}${STYLE.reset}` : '─'.repeat(maxWidth));
237
+ continue;
238
+ }
239
+ const quote = line.match(/^\s*>\s?(.*)$/);
240
+ if (quote) {
241
+ for (const part of wrapPlain(quote[1], maxWidth - 3)) output.push(`${useColor ? `${STYLE.muted}│${STYLE.reset}` : '│'} ${renderInline(part, useColor)}`);
242
+ continue;
243
+ }
244
+ const list = line.match(/^(\s*)([-+*]|\d+[.)])\s+(.+)$/);
245
+ if (list) {
246
+ const prefix = /^\d/.test(list[2]) ? `${list[2]} ` : '• ';
247
+ const indent = list[1].replace(/\t/g, ' ');
248
+ const available = Math.max(12, maxWidth - width(indent) - width(prefix));
249
+ wrapPlain(list[3], available).forEach((part, partIndex) => {
250
+ output.push(`${indent}${partIndex === 0 ? (useColor ? `${STYLE.brand}${prefix}${STYLE.reset}` : prefix) : ' '.repeat(width(prefix))}${renderInline(part, useColor)}`);
251
+ });
252
+ continue;
253
+ }
254
+ if (!line.trim()) {
255
+ if (output.at(-1) !== '') output.push('');
256
+ continue;
257
+ }
258
+ for (const part of wrapPlain(line, maxWidth)) output.push(renderInline(part, useColor));
259
+ }
260
+ if (inCode) flushCode();
261
+ if (inMath) flushMath();
262
+ while (output.at(-1) === '') output.pop();
263
+ return output.join('\n');
264
+ }
@@ -0,0 +1,182 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+
6
+ const VERSION = 1;
7
+ const CATEGORIES = new Set(['preference', 'convention', 'architecture', 'command', 'decision', 'debugging']);
8
+ let memoryBaseDir = path.join(os.homedir(), '.dave-code-memory');
9
+
10
+ export function setMemoryBaseDirForTesting(directory) {
11
+ memoryBaseDir = directory;
12
+ }
13
+
14
+ function canonicalRoot(workspaceRoot) {
15
+ const resolved = path.resolve(workspaceRoot || process.cwd());
16
+ try {
17
+ return fs.realpathSync.native(resolved).replace(/\\/g, '/').toLowerCase();
18
+ } catch {
19
+ return resolved.replace(/\\/g, '/').toLowerCase();
20
+ }
21
+ }
22
+
23
+ export function getMemoryFile(workspaceRoot) {
24
+ const hash = crypto.createHash('sha256').update(canonicalRoot(workspaceRoot)).digest('hex');
25
+ return path.join(memoryBaseDir, `${hash}.json`);
26
+ }
27
+
28
+ function atomicWrite(filePath, data) {
29
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
30
+ const temp = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
31
+ fs.writeFileSync(temp, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
32
+ try { fs.chmodSync(temp, 0o600); } catch {}
33
+ try {
34
+ fs.renameSync(temp, filePath);
35
+ } catch (error) {
36
+ if (process.platform !== 'win32' || !fs.existsSync(filePath)) throw error;
37
+ fs.unlinkSync(filePath);
38
+ fs.renameSync(temp, filePath);
39
+ } finally {
40
+ if (fs.existsSync(temp)) fs.unlinkSync(temp);
41
+ }
42
+ }
43
+
44
+ function normalizeEntry(entry) {
45
+ if (!entry || typeof entry !== 'object') return null;
46
+ const text = String(entry.text || '').trim().replace(/\s+/g, ' ');
47
+ if (!text || text.length > 600) return null;
48
+ return {
49
+ id: String(entry.id || crypto.randomUUID()),
50
+ category: CATEGORIES.has(entry.category) ? entry.category : 'decision',
51
+ text,
52
+ createdAt: Number(entry.createdAt) || Date.now(),
53
+ updatedAt: Number(entry.updatedAt) || Date.now(),
54
+ lastUsedAt: Number(entry.lastUsedAt) || 0,
55
+ sourceSessionId: String(entry.sourceSessionId || '')
56
+ };
57
+ }
58
+
59
+ export function loadMemoryStore(workspaceRoot) {
60
+ const filePath = getMemoryFile(workspaceRoot);
61
+ try {
62
+ if (!fs.existsSync(filePath)) return { version: VERSION, workspaceRoot: canonicalRoot(workspaceRoot), enabled: true, memories: [] };
63
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
64
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.memories)) throw new Error('Invalid memory schema.');
65
+ return {
66
+ version: VERSION,
67
+ workspaceRoot: canonicalRoot(workspaceRoot),
68
+ enabled: parsed.enabled !== false,
69
+ memories: parsed.memories.map(normalizeEntry).filter(Boolean).slice(-120)
70
+ };
71
+ } catch {
72
+ return { version: VERSION, workspaceRoot: canonicalRoot(workspaceRoot), enabled: true, memories: [] };
73
+ }
74
+ }
75
+
76
+ function saveMemoryStore(workspaceRoot, store) {
77
+ const normalized = {
78
+ version: VERSION,
79
+ workspaceRoot: canonicalRoot(workspaceRoot),
80
+ enabled: store.enabled !== false,
81
+ memories: (store.memories || []).map(normalizeEntry).filter(Boolean).slice(-120)
82
+ };
83
+ atomicWrite(getMemoryFile(workspaceRoot), normalized);
84
+ return normalized;
85
+ }
86
+
87
+ function tokens(text) {
88
+ const value = String(text || '').toLowerCase();
89
+ const result = new Set(value.match(/[a-z0-9_./-]{2,}/g) || []);
90
+ const cjk = [...value].filter(char => /[\u3400-\u9fff]/.test(char));
91
+ for (let index = 0; index < cjk.length - 1; index++) result.add(cjk[index] + cjk[index + 1]);
92
+ return result;
93
+ }
94
+
95
+ function memoryScore(memory, queryTokens) {
96
+ const memoryTokens = tokens(memory.text);
97
+ let overlap = 0;
98
+ for (const token of queryTokens) if (memoryTokens.has(token)) overlap++;
99
+ const categoryBoost = ['preference', 'convention', 'architecture'].includes(memory.category) ? 0.8 : 0;
100
+ const recency = Math.max(memory.updatedAt, memory.lastUsedAt || 0) / 1e13;
101
+ return overlap * 4 + categoryBoost + recency;
102
+ }
103
+
104
+ export function retrieveMemories(workspaceRoot, query, limit = 8) {
105
+ const store = loadMemoryStore(workspaceRoot);
106
+ if (!store.enabled || store.memories.length === 0) return [];
107
+ const queryTokens = tokens(query);
108
+ const ranked = store.memories
109
+ .map(memory => ({ memory, score: memoryScore(memory, queryTokens) }))
110
+ .filter(item => item.score >= 0.8)
111
+ .sort((a, b) => b.score - a.score || b.memory.updatedAt - a.memory.updatedAt)
112
+ .slice(0, Math.max(1, limit));
113
+ if (ranked.length > 0) {
114
+ const used = new Set(ranked.map(item => item.memory.id));
115
+ const now = Date.now();
116
+ for (const memory of store.memories) if (used.has(memory.id)) memory.lastUsedAt = now;
117
+ saveMemoryStore(workspaceRoot, store);
118
+ }
119
+ return ranked.map(item => item.memory);
120
+ }
121
+
122
+ function isSensitiveMemory(text) {
123
+ return /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|password|passwd|secret)\s*[:=]\s*\S+|\bsk-[a-z0-9_-]{12,}|\.env\b.*=)/i.test(text);
124
+ }
125
+
126
+ export function mergeMemoryCandidates(workspaceRoot, candidates, sourceSessionId = '') {
127
+ const store = loadMemoryStore(workspaceRoot);
128
+ if (!store.enabled) return { added: 0, updated: 0, total: store.memories.length };
129
+ let added = 0;
130
+ let updated = 0;
131
+ for (const candidate of Array.isArray(candidates) ? candidates : []) {
132
+ const text = String(candidate?.text || '').trim().replace(/\s+/g, ' ');
133
+ if (text.length < 12 || text.length > 500 || isSensitiveMemory(text)) continue;
134
+ const category = CATEGORIES.has(candidate.category) ? candidate.category : 'decision';
135
+ const key = text.toLocaleLowerCase();
136
+ const existing = store.memories.find(memory => memory.text.toLocaleLowerCase() === key);
137
+ if (existing) {
138
+ existing.updatedAt = Date.now();
139
+ existing.category = category;
140
+ updated++;
141
+ } else {
142
+ store.memories.push(normalizeEntry({ text, category, sourceSessionId }));
143
+ added++;
144
+ }
145
+ }
146
+ store.memories = store.memories
147
+ .sort((a, b) => Math.max(a.updatedAt, a.lastUsedAt) - Math.max(b.updatedAt, b.lastUsedAt))
148
+ .slice(-120);
149
+ saveMemoryStore(workspaceRoot, store);
150
+ return { added, updated, total: store.memories.length };
151
+ }
152
+
153
+ export function deleteMemory(workspaceRoot, memoryId) {
154
+ const store = loadMemoryStore(workspaceRoot);
155
+ const next = store.memories.filter(memory => memory.id !== memoryId);
156
+ if (next.length === store.memories.length) return false;
157
+ store.memories = next;
158
+ saveMemoryStore(workspaceRoot, store);
159
+ return true;
160
+ }
161
+
162
+ export function clearMemories(workspaceRoot) {
163
+ const store = loadMemoryStore(workspaceRoot);
164
+ store.memories = [];
165
+ saveMemoryStore(workspaceRoot, store);
166
+ }
167
+
168
+ export function setMemoryEnabled(workspaceRoot, enabled) {
169
+ const store = loadMemoryStore(workspaceRoot);
170
+ store.enabled = Boolean(enabled);
171
+ return saveMemoryStore(workspaceRoot, store);
172
+ }
173
+
174
+ export function memorySummary(workspaceRoot) {
175
+ const store = loadMemoryStore(workspaceRoot);
176
+ return { enabled: store.enabled, total: store.memories.length, memories: [...store.memories].sort((a, b) => b.updatedAt - a.updatedAt) };
177
+ }
178
+
179
+ export function formatMemoriesForPrompt(memories) {
180
+ if (!memories?.length) return '';
181
+ return memories.map(memory => `- [${memory.category}] ${memory.text}`).join('\n');
182
+ }