dave-code 1.1.0 → 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
+ }
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
5
 
6
- const STORE_VERSION = 1;
6
+ const STORE_VERSION = 2;
7
7
  const VALID_STATUSES = new Set(['ready', 'running', 'completed', 'blocked']);
8
8
  let plansBaseDir = path.join(os.homedir(), '.dave-code-plans');
9
9
 
@@ -79,7 +79,13 @@ function validateStoredPlan(plan) {
79
79
  content: String(plan.content || ''),
80
80
  createdAt: Number(plan.createdAt) || Date.now(),
81
81
  updatedAt: Number(plan.updatedAt) || Date.now(),
82
- lastRun: plan.lastRun && typeof plan.lastRun === 'object' ? plan.lastRun : null
82
+ lastRun: plan.lastRun && typeof plan.lastRun === 'object' ? plan.lastRun : null,
83
+ workflowMode: plan.workflowMode === 'thunder' ? 'thunder' : 'highway',
84
+ teamId: plan.teamId ? String(plan.teamId) : null,
85
+ resourceProposal: plan.resourceProposal && typeof plan.resourceProposal === 'object' ? plan.resourceProposal : null,
86
+ teamSnapshot: plan.teamSnapshot && typeof plan.teamSnapshot === 'object' ? plan.teamSnapshot : null,
87
+ taskGraph: Array.isArray(plan.taskGraph) ? plan.taskGraph : [],
88
+ decisions: Array.isArray(plan.decisions) ? plan.decisions.map(String) : []
83
89
  };
84
90
  } catch {
85
91
  return null;
@@ -98,7 +104,7 @@ export function loadPlanStore(workspaceRoot) {
98
104
  const filePath = getPlansFile(workspaceRoot);
99
105
  try {
100
106
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
101
- if (!parsed || parsed.version !== STORE_VERSION || !Array.isArray(parsed.plans)) {
107
+ if (!parsed || ![1, STORE_VERSION].includes(parsed.version) || !Array.isArray(parsed.plans)) {
102
108
  return emptyStore(workspaceRoot);
103
109
  }
104
110
  return {
@@ -153,7 +159,7 @@ export function findPlan(workspaceRoot, name) {
153
159
  return listPlans(workspaceRoot).find(plan => normalizeName(plan.name) === key) || null;
154
160
  }
155
161
 
156
- export function upsertPlan(workspaceRoot, { name, request, content }, { replace = false } = {}) {
162
+ export function upsertPlan(workspaceRoot, { name, request, content, workflowMode = 'highway', teamId = null, resourceProposal = null, teamSnapshot = null, taskGraph = [], decisions = [] }, { replace = false } = {}) {
157
163
  const validName = validatePlanName(name);
158
164
  const validContent = String(content || '').trim();
159
165
  const missingSections = validatePlanContent(validContent);
@@ -172,7 +178,13 @@ export function upsertPlan(workspaceRoot, { name, request, content }, { replace
172
178
  content: validContent,
173
179
  createdAt: now,
174
180
  updatedAt: now,
175
- lastRun: null
181
+ lastRun: null,
182
+ workflowMode: workflowMode === 'thunder' ? 'thunder' : 'highway',
183
+ teamId,
184
+ resourceProposal,
185
+ teamSnapshot,
186
+ taskGraph,
187
+ decisions
176
188
  }
177
189
  : {
178
190
  ...store.plans[index],
@@ -181,7 +193,13 @@ export function upsertPlan(workspaceRoot, { name, request, content }, { replace
181
193
  request: String(request || '').trim(),
182
194
  content: validContent,
183
195
  updatedAt: now,
184
- lastRun: null
196
+ lastRun: null,
197
+ workflowMode: workflowMode === 'thunder' ? 'thunder' : 'highway',
198
+ teamId,
199
+ resourceProposal,
200
+ teamSnapshot,
201
+ taskGraph,
202
+ decisions
185
203
  };
186
204
 
187
205
  if (index === -1) store.plans.push(next);
@@ -231,9 +249,43 @@ export function deletePlan(workspaceRoot, planId) {
231
249
  }
232
250
 
233
251
  export function planStatusLines(workspaceRoot, limit = 4) {
234
- const plans = listPlans(workspaceRoot);
252
+ const panel = planStatusPanel(workspaceRoot, limit);
235
253
  const symbols = { ready: '○', running: '▶', completed: '✓', blocked: '!' };
236
- const lines = plans.slice(0, limit).map(plan => `${symbols[plan.status] || '○'} ${plan.name}`);
237
- if (plans.length > limit) lines.push(`+${plans.length - limit} more`);
254
+ const lines = panel.items.map(plan => `${symbols[plan.status] || '○'} ${plan.name}`);
255
+ if (panel.hidden > 0) lines.push(`+${panel.hidden} more`);
238
256
  return lines;
239
257
  }
258
+
259
+ /**
260
+ * Select plans for the persistent header panel. When active and completed plans
261
+ * coexist, reserve one slot for the most recently completed plan so completed
262
+ * work never disappears behind a long ready queue.
263
+ */
264
+ export function planStatusPanel(workspaceRoot, limit = 4) {
265
+ const plans = listPlans(workspaceRoot);
266
+ const safeLimit = Math.max(1, Math.floor(Number(limit) || 4));
267
+ const active = plans.filter(plan => plan.status !== 'completed');
268
+ const completed = plans
269
+ .filter(plan => plan.status === 'completed')
270
+ .sort((a, b) => b.updatedAt - a.updatedAt);
271
+
272
+ let items;
273
+ if (active.length > 0 && completed.length > 0 && safeLimit > 1) {
274
+ items = [...active.slice(0, safeLimit - 1), completed[0]];
275
+ } else {
276
+ items = (active.length > 0 ? active : completed).slice(0, safeLimit);
277
+ }
278
+
279
+ return {
280
+ items,
281
+ total: plans.length,
282
+ hidden: Math.max(0, plans.length - items.length),
283
+ counts: {
284
+ ready: plans.filter(plan => plan.status === 'ready').length,
285
+ running: plans.filter(plan => plan.status === 'running').length,
286
+ blocked: plans.filter(plan => plan.status === 'blocked').length,
287
+ completed: completed.length,
288
+ active: active.length
289
+ }
290
+ };
291
+ }