deeper-cli 1.2.4 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deeper-cli",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "description": "DeeperCode - 一句话生成完整项目的 AI Agentic CLI 工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -952,7 +952,7 @@ After writing or editing code files, ALWAYS verify the changes:
952
952
  }
953
953
  r.push(e);
954
954
  }
955
- return r;
955
+ return r.map(m => { if (m.role === 'tool' && !m.name) m.name = 'tool'; return m; });
956
956
  }
957
957
 
958
958
  function trimHistory(h: Message[], max: number) { while (h.length > max) h.shift(); }
@@ -7,14 +7,14 @@ export interface MemoryEntry {
7
7
  type: 'working' | 'episodic' | 'semantic' | 'procedural';
8
8
  content: string;
9
9
  tags: string[];
10
- importance: number; // 0-10
11
- accuracy: number; // 0-10, 自评置信度
10
+ importance: number;
11
+ accuracy: number;
12
12
  createdAt: number;
13
13
  accessedAt: number;
14
14
  accessCount: number;
15
15
  sessionId: string;
16
- source: string; // 'user' | 'agent' | 'tool' | 'system'
17
- references: string[]; // 关联条目 ID
16
+ source: string;
17
+ references: string[];
18
18
  }
19
19
 
20
20
  interface XMemStats {
@@ -22,13 +22,17 @@ interface XMemStats {
22
22
  byType: Record<string, number>;
23
23
  totalTokens: number;
24
24
  lastCleanup: number;
25
+ lastConsolidate: number;
25
26
  }
26
27
 
27
28
  const MEM_DIR = join(DEEPER_HOME, 'xmemory');
28
29
  const STATS_FILE = join(MEM_DIR, 'stats.json');
29
- const MAX_WORKING_MEM = 50;
30
- const MAX_TOTAL_MEM = 2000;
31
- const CLEANUP_THRESHOLD = 1500;
30
+ const MAX_WORKING_MEM = 100;
31
+ const MAX_TOTAL_MEM = 3000;
32
+ const CLEANUP_THRESHOLD = 2000;
33
+ const DEDUP_SIMILARITY_THRESHOLD = 0.85;
34
+ const CONSOLIDATE_THRESHOLD = 5;
35
+ const SAVE_DEBOUNCE_MS = 3000;
32
36
 
33
37
  let currentSessionId = '';
34
38
 
@@ -41,30 +45,87 @@ function uid(): string {
41
45
  function ensureDir(): void {
42
46
  if (!existsSync(MEM_DIR)) mkdirSync(MEM_DIR, { recursive: true });
43
47
  if (!existsSync(STATS_FILE)) {
44
- writeFileSync(STATS_FILE, JSON.stringify({ totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: Date.now() }), 'utf-8');
48
+ writeFileSync(STATS_FILE, JSON.stringify({ totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: Date.now(), lastConsolidate: Date.now() }, null, 2), 'utf-8');
45
49
  }
46
50
  }
47
51
 
48
52
  function loadStats(): XMemStats {
49
53
  try {
50
54
  return JSON.parse(readFileSync(STATS_FILE, 'utf-8')) as XMemStats;
51
- } catch { return { totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: 0 }; }
55
+ } catch { return { totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: 0, lastConsolidate: 0 }; }
52
56
  }
53
57
 
54
58
  function saveStats(s: XMemStats): void {
55
59
  writeFileSync(STATS_FILE, JSON.stringify(s, null, 2), 'utf-8');
56
60
  }
57
61
 
62
+ function tokenize(text: string): string[] {
63
+ const lower = text.toLowerCase();
64
+ const words = lower.split(/[\s\p{P}\p{S}]+/).filter(w => w.length >= 2);
65
+ const bigrams: string[] = [];
66
+ for (let i = 0; i < words.length - 1; i++) bigrams.push(words[i] + '_' + words[i + 1]);
67
+ return [...new Set([...words, ...bigrams])];
68
+ }
69
+
70
+ function similarity(a: string, b: string): number {
71
+ if (a === b) return 1;
72
+ if (!a || !b) return 0;
73
+ const ta = tokenize(a);
74
+ const tb = tokenize(b);
75
+ if (ta.length === 0 || tb.length === 0) return 0;
76
+ const sa = new Set(ta);
77
+ const sb = new Set(tb);
78
+ let intersection = 0;
79
+ for (const t of sa) if (sb.has(t)) intersection++;
80
+ const union = Math.max(sa.size, sb.size);
81
+ return union > 0 ? intersection / union : 0;
82
+ }
83
+
84
+ class TFIDFIndexer {
85
+ private docFreq: Map<string, number> = new Map();
86
+ private numDocs = 0;
87
+
88
+ build(entries: Iterable<MemoryEntry>): void {
89
+ this.docFreq.clear();
90
+ this.numDocs = 0;
91
+ for (const entry of entries) {
92
+ this.numDocs++;
93
+ const tokens = new Set(tokenize(entry.content));
94
+ for (const t of tokens) this.docFreq.set(t, (this.docFreq.get(t) || 0) + 1);
95
+ }
96
+ }
97
+
98
+ idf(term: string): number {
99
+ const df = this.docFreq.get(term) || 0;
100
+ if (df === 0 || df >= this.numDocs) return 0;
101
+ return Math.log((this.numDocs + 0.5) / (df + 0.5)) + 1;
102
+ }
103
+
104
+ tfidfScore(content: string, queryTerms: string[]): number {
105
+ const tokens = tokenize(content);
106
+ const tfMap = new Map<string, number>();
107
+ for (const t of tokens) tfMap.set(t, (tfMap.get(t) || 0) + 1);
108
+ const maxTf = Math.max(...tfMap.values(), 1);
109
+ let score = 0;
110
+ for (const qt of queryTerms) {
111
+ const tf = ((tfMap.get(qt) || 0) / maxTf) * (1 + Math.log(tokens.length));
112
+ score += tf * this.idf(qt);
113
+ }
114
+ return score;
115
+ }
116
+ }
117
+
58
118
  export class XMemory {
59
119
  private working: MemoryEntry[] = [];
60
120
  private index: Map<string, MemoryEntry> = new Map();
61
121
  private dirty = false;
122
+ private saveTimer: ReturnType<typeof setTimeout> | null = null;
123
+ private tfidf: TFIDFIndexer = new TFIDFIndexer();
62
124
 
63
125
  constructor() {
64
126
  ensureDir();
65
127
  }
66
128
 
67
- // ============ 写入 ============
68
129
  store(
69
130
  type: MemoryEntry['type'],
70
131
  content: string,
@@ -74,9 +135,12 @@ export class XMemory {
74
135
  source: MemoryEntry['source'] = 'agent',
75
136
  references: string[] = [],
76
137
  ): string {
138
+ const deduped = this.deduplicate(type, content, importance);
139
+ if (deduped !== null) return deduped;
140
+
77
141
  const id = uid();
78
142
  const entry: MemoryEntry = {
79
- id, type, content: content.slice(0, 2000), tags,
143
+ id, type, content: content.slice(0, 4000), tags,
80
144
  importance: Math.min(10, Math.max(0, importance)),
81
145
  accuracy: Math.min(10, Math.max(0, accuracy)),
82
146
  createdAt: Date.now(), accessedAt: Date.now(),
@@ -86,13 +150,12 @@ export class XMemory {
86
150
 
87
151
  if (type === 'working') {
88
152
  this.working.push(entry);
89
- if (this.working.length > MAX_WORKING_MEM) {
90
- this.working.shift();
91
- }
153
+ this.evictWorking();
92
154
  }
93
155
 
94
156
  this.index.set(id, entry);
95
157
  this.dirty = true;
158
+ this.scheduleSave();
96
159
 
97
160
  if (this.index.size > CLEANUP_THRESHOLD) {
98
161
  this.autoCleanup();
@@ -117,38 +180,54 @@ export class XMemory {
117
180
  return this.store('working', content, tags, 3, 5, 'agent');
118
181
  }
119
182
 
120
- // ============ 检索 ============
121
183
  recall(query: string, limit = 5, minImportance = 0): MemoryEntry[] {
122
- const keywords = query.toLowerCase().split(/[\s,,。]+/).filter(w => w.length > 1);
184
+ const keywords = tokenize(query).filter(w => w.length >= 2);
123
185
  if (keywords.length === 0) return [];
124
186
 
125
- const scored: Array<{ entry: MemoryEntry; score: number }> = [];
187
+ this.tfidf.build(this.index.values());
126
188
  const now = Date.now();
189
+ const scored: Array<{ entry: MemoryEntry; score: number }> = [];
127
190
 
128
191
  for (const entry of this.index.values()) {
129
192
  if (entry.importance < minImportance) continue;
130
- let score = 0;
131
- const c = (entry.content || '').toLowerCase();
193
+
194
+ const c = entry.content.toLowerCase();
132
195
  const t = entry.tags.join(' ').toLowerCase();
133
196
 
197
+ let keywordScore = 0;
198
+ let exactMatchBonus = 0;
134
199
  for (const kw of keywords) {
135
- if (c.includes(kw)) score += 3;
136
- if (t.includes(kw)) score += 2;
137
- if (entry.type === 'procedural' && c.includes(kw)) score += 1;
200
+ if (c.includes(kw)) { keywordScore += 3; exactMatchBonus += 1; }
201
+ if (t.includes(kw)) keywordScore += 2;
202
+ if (entry.type === 'procedural' && c.includes(kw)) keywordScore += 1;
203
+ if (entry.type === 'semantic' && c.includes(kw)) keywordScore += 1.5;
138
204
  }
139
205
 
140
- // 衰减: 重要度高 + 最近访问 权重高
141
- const age = (now - entry.accessedAt) / (1000 * 60 * 60);
142
- score += entry.importance * 0.5;
143
- score -= Math.min(age / 24, 5); // 每 24 小时衰减 max 5
206
+ const tfidfScore = this.tfidf.tfidfScore(entry.content, keywords);
207
+
208
+ const recencyBoost = Math.max(0, 3 - (now - entry.accessedAt) / (1000 * 60 * 30));
209
+ const frequencyBoost = Math.min(entry.accessCount * 0.3, 4);
210
+ const importanceWeight = entry.importance * 0.6;
211
+ const ageHours = (now - entry.createdAt) / (1000 * 60 * 60);
212
+ const ageDecay = Math.min(ageHours / 48, 6);
144
213
 
145
- if (score > 0) scored.push({ entry, score });
214
+ const totalScore =
215
+ keywordScore * 1.0 +
216
+ tfidfScore * 2.0 +
217
+ importanceWeight +
218
+ recencyBoost +
219
+ frequencyBoost -
220
+ ageDecay +
221
+ (exactMatchBonus >= 3 ? 3 : 0);
222
+
223
+ if (totalScore > 0) scored.push({ entry, score: totalScore });
146
224
  }
147
225
 
148
226
  scored.sort((a, b) => b.score - a.score);
149
227
  return scored.slice(0, limit).map(s => {
150
228
  s.entry.accessedAt = now;
151
229
  s.entry.accessCount++;
230
+ s.entry.importance = Math.min(10, s.entry.importance + 0.1);
152
231
  return s.entry;
153
232
  });
154
233
  }
@@ -158,20 +237,23 @@ export class XMemory {
158
237
  for (const entry of this.index.values()) {
159
238
  if (entry.type === type) res.push(entry);
160
239
  }
161
- res.sort((a, b) => b.createdAt - a.createdAt);
240
+ res.sort((a, b) => (b.importance * 2 + b.accessCount) - (a.importance * 2 + a.accessCount));
162
241
  return res.slice(0, limit);
163
242
  }
164
243
 
165
244
  getWorking(): MemoryEntry[] {
166
- return this.working;
245
+ return this.working.sort((a, b) =>
246
+ (b.importance * 2 + b.accessCount) - (a.importance * 2 + a.accessCount)
247
+ );
167
248
  }
168
249
 
169
- getWorkingContext(maxTokens = 2000): string {
250
+ getWorkingContext(maxTokens = 4000): string {
170
251
  if (this.working.length === 0) return '';
171
- const lines = this.working.map(e => `[工作记忆] ${e.content.slice(0, 300)}`);
252
+ const sorted = this.getWorking();
253
+ const lines = sorted.map(e => `[${e.source}] ${e.content.slice(0, 500)}`);
172
254
  let result = '';
173
255
  for (const line of lines) {
174
- if ((result + line).length > maxTokens * 4) break;
256
+ if ((result + line).length > maxTokens * 3) break;
175
257
  result += line + '\n';
176
258
  }
177
259
  return result;
@@ -181,28 +263,51 @@ export class XMemory {
181
263
  const recalled = this.recall(task, limit, 3);
182
264
  const proc = recalled.filter(r => r.type === 'procedural' || r.type === 'semantic');
183
265
  if (proc.length === 0) return '';
184
- return '[记忆提示]\n' + proc.map(p => `- ${p.content.slice(0, 250)}`).join('\n');
266
+ return '[记忆提示]\n' + proc.map(p => `- [${p.type === 'procedural' ? '技能' : '知识'}] ${p.content.slice(0, 350)}`).join('\n');
185
267
  }
186
268
 
187
269
  getSessionSummary(): string {
188
270
  const entries = [...this.index.values()].filter(e => e.sessionId === currentSessionId);
189
271
  if (entries.length === 0) return '';
190
- const key = entries
191
- .filter(e => e.importance >= 5)
272
+
273
+ const byType: Record<string, MemoryEntry[]> = { semantic: [], procedural: [], episodic: [], working: [] };
274
+ for (const e of entries) {
275
+ if (byType[e.type]) byType[e.type].push(e);
276
+ }
277
+
278
+ const parts: string[] = [];
279
+
280
+ const keySemantic = byType.semantic
281
+ .sort((a, b) => b.importance - a.importance)
282
+ .slice(0, 5);
283
+ if (keySemantic.length > 0) {
284
+ parts.push(`[知识记忆·${keySemantic.length}条]\n` +
285
+ keySemantic.map(e => `• ${e.content.slice(0, 250)}`).join('\n'));
286
+ }
287
+
288
+ const keyProcedural = byType.procedural
289
+ .sort((a, b) => b.importance - a.importance)
290
+ .slice(0, 5);
291
+ if (keyProcedural.length > 0) {
292
+ parts.push(`[技能记忆·${keyProcedural.length}条]\n` +
293
+ keyProcedural.map(e => `• ${e.content.slice(0, 250)}`).join('\n'));
294
+ }
295
+
296
+ const keyEpisodic = byType.episodic
192
297
  .sort((a, b) => b.importance - a.importance)
193
- .slice(0, 8);
194
- return `[XMemory·本会话]\n` + key.map(e => {
195
- const t = e.type === 'semantic' ? '知识' : e.type === 'procedural' ? '技能' : e.type === 'episodic' ? '经历' : '工作';
196
- return `[${t}] ${e.content.slice(0, 200)}`;
197
- }).join('\n');
298
+ .slice(0, 3);
299
+ if (keyEpisodic.length > 0) {
300
+ parts.push(`[经历记忆·${keyEpisodic.length}条]\n` +
301
+ keyEpisodic.map(e => `→ ${e.content.slice(0, 150)}`).join('\n'));
302
+ }
303
+
304
+ return `[XMemory·本会话 ${entries.length}条]\n${parts.join('\n')}`;
198
305
  }
199
306
 
200
- // ============ 持久化 ============
201
307
  async save(): Promise<void> {
202
308
  if (!this.dirty) return;
203
309
  ensureDir();
204
310
 
205
- // 分批写入:每个 type 一个文件
206
311
  const byType: Record<string, MemoryEntry[]> = {};
207
312
  for (const entry of this.index.values()) {
208
313
  if (!byType[entry.type]) byType[entry.type] = [];
@@ -211,7 +316,7 @@ export class XMemory {
211
316
 
212
317
  for (const [type, entries] of Object.entries(byType)) {
213
318
  const file = join(MEM_DIR, `${type}.json`);
214
- writeFileSync(file, JSON.stringify(entries.slice(-500)), 'utf-8'); // 每个类型最多 500 条
319
+ writeFileSync(file, JSON.stringify(entries.slice(-800)), 'utf-8');
215
320
  }
216
321
 
217
322
  const stats = loadStats();
@@ -241,24 +346,125 @@ export class XMemory {
241
346
  if (entry.type === 'working') this.working.push(entry);
242
347
  total++;
243
348
  }
244
- } catch { /* skip corrupt files */ }
349
+ } catch {}
245
350
  }
246
351
 
247
352
  if (this.working.length > MAX_WORKING_MEM) {
248
353
  this.working = this.working.slice(-MAX_WORKING_MEM);
249
354
  }
355
+
356
+ this.consolidateIfNeeded();
357
+ }
358
+
359
+ private scheduleSave(): void {
360
+ if (this.saveTimer) return;
361
+ this.saveTimer = setTimeout(() => {
362
+ this.saveTimer = null;
363
+ this.save().catch(() => {});
364
+ }, SAVE_DEBOUNCE_MS);
365
+ }
366
+
367
+ private deduplicate(type: MemoryEntry['type'], content: string, importance: number): string | null {
368
+ const threshold = type === 'working' ? 0.95 : type === 'episodic' ? 0.85 : DEDUP_SIMILARITY_THRESHOLD;
369
+ const shortContent = content.slice(0, 500);
370
+
371
+ for (const entry of this.index.values()) {
372
+ if (entry.type !== type) continue;
373
+ const sim = similarity(shortContent, entry.content.slice(0, 500));
374
+ if (sim >= threshold) {
375
+ entry.accessedAt = Date.now();
376
+ entry.accessCount++;
377
+ entry.importance = Math.min(10, Math.max(entry.importance, importance));
378
+ if (content.length > entry.content.length && content.length <= 4000) {
379
+ entry.content = content;
380
+ this.dirty = true;
381
+ }
382
+ return entry.id;
383
+ }
384
+ }
385
+ return null;
386
+ }
387
+
388
+ private evictWorking(): void {
389
+ while (this.working.length > MAX_WORKING_MEM) {
390
+ let worstIdx = 0;
391
+ let worstScore = Infinity;
392
+ for (let i = 0; i < this.working.length; i++) {
393
+ const e = this.working[i];
394
+ const score = e.importance + e.accessCount * 0.3 - (Date.now() - e.accessedAt) / (1000 * 60 * 15);
395
+ if (score < worstScore) { worstScore = score; worstIdx = i; }
396
+ }
397
+ this.working.splice(worstIdx, 1);
398
+ }
399
+ }
400
+
401
+ private consolidateIfNeeded(): void {
402
+ const stats = loadStats();
403
+ const now = Date.now();
404
+ if (now - stats.lastConsolidate < 24 * 60 * 60 * 1000) return;
405
+
406
+ const candidates: Map<string, MemoryEntry[]> = new Map();
407
+ for (const entry of this.index.values()) {
408
+ if (entry.type === 'episodic' && entry.accessCount >= CONSOLIDATE_THRESHOLD) {
409
+ const key = entry.tags.slice(0, 3).sort().join('|') || '_default_';
410
+ if (!candidates.has(key)) candidates.set(key, []);
411
+ candidates.get(key)!.push(entry);
412
+ }
413
+ }
414
+
415
+ let consolidated = 0;
416
+ for (const [, group] of candidates) {
417
+ if (group.length < 2) continue;
418
+ group.sort((a, b) => b.accessCount - a.accessCount);
419
+ const best = group[0];
420
+
421
+ const allContent = group.map(g => g.content).join('\n');
422
+ const summary = `[ consolidated from ${group.length} memories ]\n${allContent.slice(0, 2000)}`;
423
+
424
+ const newType = best.accessCount >= 10 ? 'procedural' : 'semantic';
425
+ const existing = this.findSimilar(newType, best.content);
426
+ if (existing) {
427
+ existing.content = summary.slice(0, 4000);
428
+ existing.accessCount += Math.floor(group.reduce((s, g) => s + g.accessCount, 0) / group.length);
429
+ existing.importance = Math.min(10, existing.importance + 1);
430
+ } else {
431
+ this.store(newType, summary, best.tags, Math.min(10, best.importance + 2), 9, 'system');
432
+ consolidated++;
433
+ }
434
+
435
+ for (const old of group) {
436
+ if (old.id !== best.id) this.index.delete(old.id);
437
+ }
438
+ }
439
+
440
+ if (consolidated > 0 || candidates.size > 0) {
441
+ stats.lastConsolidate = now;
442
+ saveStats(stats);
443
+ this.dirty = true;
444
+ }
445
+ }
446
+
447
+ private findSimilar(type: MemoryEntry['type'], content: string): MemoryEntry | null {
448
+ const threshold = 0.75;
449
+ let best: MemoryEntry | null = null;
450
+ let bestSim = 0;
451
+ for (const entry of this.index.values()) {
452
+ if (entry.type !== type) continue;
453
+ const sim = similarity(content.slice(0, 300), entry.content.slice(0, 300));
454
+ if (sim > bestSim) { bestSim = sim; best = entry; }
455
+ }
456
+ return bestSim >= threshold ? best : null;
250
457
  }
251
458
 
252
- // ============ 维护 ============
253
459
  private autoCleanup(): void {
254
460
  const entries = [...this.index.values()];
255
461
  entries.sort((a, b) => {
256
- const scoreA = a.importance * 2 + a.accessCount - (Date.now() - a.accessedAt) / (1000 * 60 * 60 * 24);
257
- const scoreB = b.importance * 2 + b.accessCount - (Date.now() - b.accessedAt) / (1000 * 60 * 60 * 24);
462
+ const scoreA = a.importance * 2 + a.accessCount + (a.type === 'semantic' ? 3 : a.type === 'procedural' ? 2 : 0) - (Date.now() - a.accessedAt) / (1000 * 60 * 60 * 24);
463
+ const scoreB = b.importance * 2 + b.accessCount + (b.type === 'semantic' ? 3 : b.type === 'procedural' ? 2 : 0) - (Date.now() - b.accessedAt) / (1000 * 60 * 60 * 24);
258
464
  return scoreA - scoreB;
259
465
  });
260
466
 
261
- const toKeep = entries.slice(-500);
467
+ const toKeep = entries.slice(-800);
262
468
  this.index.clear();
263
469
  this.working = [];
264
470
  for (const entry of toKeep) {
@@ -210,6 +210,11 @@ export class DeepSeekClient {
210
210
  const rc = m.reasoning_content || m.thinking;
211
211
  if (rc) msg.reasoning_content = rc;
212
212
  return msg;
213
+ }).map((msg) => {
214
+ if (msg.role === 'tool' && !msg.name) {
215
+ (msg as Record<string, unknown>).name = 'tool';
216
+ }
217
+ return msg;
213
218
  }),
214
219
  temperature: config.temperature,
215
220
  max_tokens: config.maxTokens,