@miphamai/cli 0.33.2 → 0.34.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": "@miphamai/cli",
3
- "version": "0.33.2",
3
+ "version": "0.34.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Auto-Dream: Background Memory Consolidation Engine
3
+ *
4
+ * Inspired by Claude Code's Auto-Dream mechanism: a periodic background process
5
+ * that reviews accumulated memory, resolves contradictions, merges related
6
+ * knowledge, and solidifies vague expressions into definite facts.
7
+ *
8
+ * Unlike Claude Code's LLM-driven approach, DreamEngine uses pure text analysis
9
+ * for the most common operations (dedup, contradiction detection via keyword
10
+ * overlap, stale entry cleanup). An optional summarizer callback enables
11
+ * LLM-powered deep consolidation when available.
12
+ *
13
+ * Designed to be called:
14
+ * - Periodically (every 24h or every 5 sessions) via daemon ScheduleManager
15
+ * - Manually via `/dream` CLI command
16
+ * - On session end as a lightweight pass
17
+ *
18
+ * Flow:
19
+ * Load all memory entries
20
+ * → Phase 1: Deduplicate near-identical entries
21
+ * → Phase 2: Detect contradictions (opposing claims)
22
+ * → Phase 3: Merge related entries (overlapping topics)
23
+ * → Phase 4: Solidify vague entries (flag "maybe"/"possibly" for review)
24
+ * → Phase 5: Prune stale entries (30+ days, low relevance)
25
+ * → Write consolidated memory + generate DreamReport
26
+ */
27
+
28
+ import {
29
+ readdirSync,
30
+ readFileSync,
31
+ writeFileSync,
32
+ unlinkSync,
33
+ existsSync,
34
+ mkdirSync,
35
+ statSync,
36
+ } from 'node:fs'
37
+ import { join } from 'node:path'
38
+ import { homedir } from 'node:os'
39
+
40
+ // ── Types ──
41
+
42
+ export interface DreamReport {
43
+ /** When the dream cycle ran */
44
+ timestamp: string
45
+ /** Total memories before consolidation */
46
+ beforeCount: number
47
+ /** Total memories after consolidation */
48
+ afterCount: number
49
+ /** Per-phase statistics */
50
+ phases: {
51
+ deduplicated: number
52
+ contradictionsFound: number
53
+ merged: number
54
+ solidified: number
55
+ pruned: number
56
+ }
57
+ /** Specific actions taken */
58
+ actions: DreamAction[]
59
+ /** Summary for display */
60
+ summary: string
61
+ }
62
+
63
+ export interface DreamAction {
64
+ type: 'dedup' | 'contradiction' | 'merge' | 'solidify' | 'prune'
65
+ description: string
66
+ filesAffected: string[]
67
+ autoApplied: boolean
68
+ }
69
+
70
+ interface MemoryFile {
71
+ path: string
72
+ name: string
73
+ frontmatter: Record<string, unknown>
74
+ content: string
75
+ /** Days since last modified */
76
+ age: number
77
+ }
78
+
79
+ // ── Constants ──
80
+
81
+ const DEFAULT_MEMORY_DIR = join(homedir(), '.mipham', 'memory')
82
+ const STALE_DAYS = 30
83
+ const SIMILARITY_THRESHOLD = 0.65
84
+
85
+ /** Vague qualifiers that suggest an entry needs solidification */
86
+ const VAGUE_PATTERNS = [
87
+ /\bmaybe\b/i,
88
+ /\bperhaps\b/i,
89
+ /\bpossibly\b/i,
90
+ /\bmight\b/i,
91
+ /\bcould be\b/i,
92
+ /\bunsure\b/i,
93
+ /\bunclear\b/i,
94
+ /\bseems?\b/i,
95
+ /\bprobably\b/i,
96
+ /\btypically\b/i,
97
+ /\busually\b/i,
98
+ /\boften\b/i,
99
+ ]
100
+
101
+ // ── Engine ──
102
+
103
+ export class DreamEngine {
104
+ private memoryDir: string
105
+ private dreamLog: string
106
+
107
+ constructor(memoryDir: string = DEFAULT_MEMORY_DIR) {
108
+ this.memoryDir = memoryDir
109
+ this.dreamLog = join(memoryDir, 'dream-log.json')
110
+ }
111
+
112
+ /**
113
+ * Run a full dream consolidation cycle.
114
+ *
115
+ * @param options.aggressive — if true, auto-apply more actions (default: safe-only)
116
+ * @returns DreamReport with statistics and actions taken
117
+ */
118
+ dream(options: { aggressive?: boolean } = {}): DreamReport {
119
+ const { aggressive = false } = options
120
+ const actions: DreamAction[] = []
121
+ const timestamp = new Date().toISOString()
122
+
123
+ // Load all memory files
124
+ const memories = this.loadMemories()
125
+ const beforeCount = memories.length
126
+
127
+ if (memories.length === 0) {
128
+ return {
129
+ timestamp,
130
+ beforeCount: 0,
131
+ afterCount: 0,
132
+ phases: { deduplicated: 0, contradictionsFound: 0, merged: 0, solidified: 0, pruned: 0 },
133
+ actions: [],
134
+ summary: 'No memories to consolidate. Dream skipped.',
135
+ }
136
+ }
137
+
138
+ // ── Phase 1: Deduplicate near-identical entries ──
139
+ const dedupResult = this.deduplicate(memories, aggressive)
140
+ actions.push(...dedupResult.actions)
141
+ let working = dedupResult.memories
142
+
143
+ // ── Phase 2: Detect contradictions ──
144
+ const contradictions = this.detectContradictions(working)
145
+ actions.push(...contradictions)
146
+ // Contradictions are flagged, not auto-resolved
147
+
148
+ // ── Phase 3: Merge related entries ──
149
+ const mergeResult = this.mergeRelated(working, aggressive)
150
+ actions.push(...mergeResult.actions)
151
+ working = mergeResult.memories
152
+
153
+ // ── Phase 4: Solidify vague entries ──
154
+ const solidifyActions = this.solidifyVague(working)
155
+ actions.push(...solidifyActions)
156
+ // Solidification just flags — human or LLM should resolve
157
+
158
+ // ── Phase 5: Prune stale entries ──
159
+ const pruneResult = this.pruneStale(working, aggressive)
160
+ actions.push(...pruneResult.actions)
161
+ working = pruneResult.memories
162
+
163
+ // ── Persist dream log ──
164
+ this.saveDreamLog(timestamp, actions)
165
+
166
+ const afterCount = working.length
167
+ const autoApplied = actions.filter((a) => a.autoApplied).length
168
+ const flagged = actions.filter((a) => !a.autoApplied).length
169
+
170
+ return {
171
+ timestamp,
172
+ beforeCount,
173
+ afterCount,
174
+ phases: {
175
+ deduplicated: dedupResult.actions.length,
176
+ contradictionsFound: contradictions.length,
177
+ merged: mergeResult.actions.length,
178
+ solidified: solidifyActions.length,
179
+ pruned: pruneResult.actions.length,
180
+ },
181
+ actions,
182
+ summary: [
183
+ `Dream complete: ${beforeCount} → ${afterCount} memories`,
184
+ `${autoApplied} actions auto-applied, ${flagged} flagged for review`,
185
+ `Phases: ${dedupResult.actions.length} deduped, ${contradictions.length} contradictions, ${mergeResult.actions.length} merged, ${solidifyActions.length} solidified, ${pruneResult.actions.length} pruned`,
186
+ ].join('\n'),
187
+ }
188
+ }
189
+
190
+ // ── Phase 1: Deduplication ──
191
+
192
+ private deduplicate(
193
+ memories: MemoryFile[],
194
+ aggressive: boolean,
195
+ ): { memories: MemoryFile[]; actions: DreamAction[] } {
196
+ const actions: DreamAction[] = []
197
+ const kept: MemoryFile[] = []
198
+ const removed = new Set<string>()
199
+
200
+ for (let i = 0; i < memories.length; i++) {
201
+ if (removed.has(memories[i]!.path)) continue
202
+
203
+ for (let j = i + 1; j < memories.length; j++) {
204
+ if (removed.has(memories[j]!.path)) continue
205
+
206
+ const similarity = this.jaccardSimilarity(memories[i]!.content, memories[j]!.content)
207
+
208
+ if (similarity >= SIMILARITY_THRESHOLD) {
209
+ // Keep the older, more established entry
210
+ const keep = memories[i]!.age >= memories[j]!.age ? memories[i]! : memories[j]!
211
+ const remove = keep === memories[i]! ? memories[j]! : memories[i]!
212
+
213
+ if (aggressive) {
214
+ try {
215
+ unlinkSync(remove.path)
216
+ removed.add(remove.path)
217
+ actions.push({
218
+ type: 'dedup',
219
+ description: `Merged near-duplicate: "${remove.name}" → "${keep.name}" (${Math.round(similarity * 100)}% similar)`,
220
+ filesAffected: [remove.path],
221
+ autoApplied: true,
222
+ })
223
+ } catch {
224
+ // Best-effort
225
+ }
226
+ } else {
227
+ actions.push({
228
+ type: 'dedup',
229
+ description: `Near-duplicate detected: "${remove.name}" ≈ "${keep.name}" (${Math.round(similarity * 100)}% similar). Run /dream --aggressive to auto-merge.`,
230
+ filesAffected: [keep.path, remove.path],
231
+ autoApplied: false,
232
+ })
233
+ }
234
+ }
235
+ }
236
+
237
+ if (!removed.has(memories[i]!.path)) {
238
+ kept.push(memories[i]!)
239
+ }
240
+ }
241
+
242
+ return { memories: kept, actions }
243
+ }
244
+
245
+ // ── Phase 2: Contradiction Detection ──
246
+
247
+ private detectContradictions(memories: MemoryFile[]): DreamAction[] {
248
+ const actions: DreamAction[] = []
249
+
250
+ // Simple heuristic: two entries about the same topic (similar name/description)
251
+ // but with opposing sentiment markers
252
+ const negationWords = /\b(not?|never|no|don't|doesn't|shouldn't|can't|cannot)\b/i
253
+
254
+ for (let i = 0; i < memories.length; i++) {
255
+ for (let j = i + 1; j < memories.length; j++) {
256
+ const a = memories[i]!
257
+ const b = memories[j]!
258
+
259
+ // Must be about similar topics
260
+ const nameSim = this.jaccardSimilarity(a.name, b.name)
261
+ if (nameSim < 0.4) continue
262
+
263
+ // One has negation, the other doesn't → potential contradiction
264
+ const aNegates = negationWords.test(a.content)
265
+ const bNegates = negationWords.test(b.content)
266
+
267
+ if (aNegates !== bNegates) {
268
+ actions.push({
269
+ type: 'contradiction',
270
+ description: `Potential contradiction: "${a.name}" vs "${b.name}" — one asserts, the other negates. Review manually.`,
271
+ filesAffected: [a.path, b.path],
272
+ autoApplied: false,
273
+ })
274
+ }
275
+ }
276
+ }
277
+
278
+ return actions
279
+ }
280
+
281
+ // ── Phase 3: Merge Related ──
282
+
283
+ private mergeRelated(
284
+ memories: MemoryFile[],
285
+ aggressive: boolean,
286
+ ): { memories: MemoryFile[]; actions: DreamAction[] } {
287
+ const actions: DreamAction[] = []
288
+ const merged = new Set<string>()
289
+ const result: MemoryFile[] = []
290
+
291
+ for (let i = 0; i < memories.length; i++) {
292
+ if (merged.has(memories[i]!.path)) continue
293
+
294
+ const related: MemoryFile[] = [memories[i]!]
295
+
296
+ for (let j = i + 1; j < memories.length; j++) {
297
+ if (merged.has(memories[j]!.path)) continue
298
+
299
+ // Related if: same type + high name similarity + moderate content overlap
300
+ const metaI = memories[i]!.frontmatter['metadata'] as Record<string, unknown> | undefined
301
+ const metaJ = memories[j]!.frontmatter['metadata'] as Record<string, unknown> | undefined
302
+ const sameType = metaI?.['type'] === metaJ?.['type']
303
+ const nameSim = this.jaccardSimilarity(memories[i]!.name, memories[j]!.name)
304
+ const contentSim = this.jaccardSimilarity(
305
+ memories[i]!.content.slice(0, 500),
306
+ memories[j]!.content.slice(0, 500),
307
+ )
308
+
309
+ if (sameType && nameSim >= 0.5 && contentSim >= 0.3) {
310
+ related.push(memories[j]!)
311
+ merged.add(memories[j]!.path)
312
+ }
313
+ }
314
+
315
+ if (related.length >= 2 && aggressive) {
316
+ // Merge: keep the most recent, append "See also" links to others
317
+ const primary = related.sort((a, b) => b.age - a.age)[0]!
318
+ const secondary = related.filter((r) => r !== primary)
319
+
320
+ try {
321
+ const links = secondary.map((s) => `- See also: [[${s.name}]]`).join('\n')
322
+ const updated = primary.content + `\n\n## Related\n${links}`
323
+ writeFileSync(primary.path, updated, 'utf-8')
324
+
325
+ for (const s of secondary) {
326
+ try {
327
+ unlinkSync(s.path)
328
+ } catch {
329
+ /* best-effort */
330
+ }
331
+ }
332
+
333
+ actions.push({
334
+ type: 'merge',
335
+ description: `Merged ${secondary.length} related entries into "${primary.name}"`,
336
+ filesAffected: [primary.path, ...secondary.map((s) => s.path)],
337
+ autoApplied: true,
338
+ })
339
+
340
+ result.push(primary)
341
+ } catch {
342
+ result.push(...related)
343
+ }
344
+ } else if (related.length >= 2) {
345
+ actions.push({
346
+ type: 'merge',
347
+ description: `${related.length} related entries found around "${memories[i]!.name}". Run /dream --aggressive to auto-merge.`,
348
+ filesAffected: related.map((r) => r.path),
349
+ autoApplied: false,
350
+ })
351
+ result.push(...related)
352
+ } else {
353
+ result.push(memories[i]!)
354
+ }
355
+ }
356
+
357
+ return { memories: result, actions }
358
+ }
359
+
360
+ // ── Phase 4: Solidify Vague ──
361
+
362
+ private solidifyVague(memories: MemoryFile[]): DreamAction[] {
363
+ const actions: DreamAction[] = []
364
+
365
+ for (const mem of memories) {
366
+ const vagueMatches = VAGUE_PATTERNS.filter((p) => p.test(mem.content))
367
+ if (vagueMatches.length > 0) {
368
+ const matched = vagueMatches
369
+ .map((p) => {
370
+ const m = mem.content.match(p)
371
+ return m ? `"${m[0]}"` : ''
372
+ })
373
+ .filter(Boolean)
374
+ .join(', ')
375
+ actions.push({
376
+ type: 'solidify',
377
+ description: `Vague entry "${mem.name}" uses qualifiers: ${matched}. Consider replacing with definite statements.`,
378
+ filesAffected: [mem.path],
379
+ autoApplied: false,
380
+ })
381
+ }
382
+ }
383
+
384
+ return actions
385
+ }
386
+
387
+ // ── Phase 5: Prune Stale ──
388
+
389
+ private pruneStale(
390
+ memories: MemoryFile[],
391
+ aggressive: boolean,
392
+ ): { memories: MemoryFile[]; actions: DreamAction[] } {
393
+ const actions: DreamAction[] = []
394
+ const kept: MemoryFile[] = []
395
+
396
+ for (const mem of memories) {
397
+ if (mem.age > STALE_DAYS) {
398
+ if (aggressive) {
399
+ try {
400
+ unlinkSync(mem.path)
401
+ actions.push({
402
+ type: 'prune',
403
+ description: `Pruned stale entry: "${mem.name}" (${mem.age}d old)`,
404
+ filesAffected: [mem.path],
405
+ autoApplied: true,
406
+ })
407
+ } catch {
408
+ kept.push(mem)
409
+ }
410
+ } else {
411
+ actions.push({
412
+ type: 'prune',
413
+ description: `Stale entry: "${mem.name}" (${mem.age}d old). Run /dream --aggressive to prune.`,
414
+ filesAffected: [mem.path],
415
+ autoApplied: false,
416
+ })
417
+ kept.push(mem)
418
+ }
419
+ } else {
420
+ kept.push(mem)
421
+ }
422
+ }
423
+
424
+ return { memories: kept, actions }
425
+ }
426
+
427
+ // ── Helpers ──
428
+
429
+ private loadMemories(): MemoryFile[] {
430
+ if (!existsSync(this.memoryDir)) return []
431
+
432
+ try {
433
+ const files = readdirSync(this.memoryDir).filter(
434
+ (f) => f.endsWith('.md') && f !== 'MEMORY.md',
435
+ )
436
+
437
+ return files
438
+ .map((f) => {
439
+ const path = join(this.memoryDir, f)
440
+ try {
441
+ const raw = readFileSync(path, 'utf-8')
442
+ const { frontmatter, content } = this.parseFrontmatter(raw)
443
+ const stat = statSync(path)
444
+ const age = Math.floor((Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24))
445
+ return {
446
+ path,
447
+ name: f.replace('.md', ''),
448
+ frontmatter: frontmatter as Record<string, unknown>,
449
+ content,
450
+ age,
451
+ }
452
+ } catch {
453
+ return null
454
+ }
455
+ })
456
+ .filter((m): m is MemoryFile => m !== null)
457
+ } catch {
458
+ return []
459
+ }
460
+ }
461
+
462
+ private parseFrontmatter(raw: string): {
463
+ frontmatter: Record<string, unknown>
464
+ content: string
465
+ } {
466
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
467
+ if (!match) return { frontmatter: {}, content: raw }
468
+ try {
469
+ const lines = match[1]!.split('\n')
470
+ const fm: Record<string, unknown> = {}
471
+ let currentNested: Record<string, unknown> | null = null
472
+ let nestedKey = ''
473
+ for (const line of lines) {
474
+ // Handle nested YAML (e.g. "metadata:\n type: feedback")
475
+ const nested = line.match(/^ (\w[\w\s]*?):\s*(.+)$/)
476
+ if (nested && currentNested !== null) {
477
+ currentNested[nested[1]!.trim()] = nested[2]!.trim()
478
+ continue
479
+ }
480
+ // Top-level key
481
+ const kv = line.match(/^(\w[\w\s]*?):\s*(.*)$/)
482
+ if (kv) {
483
+ const key = kv[1]!.trim()
484
+ const val = kv[2]!.trim()
485
+ if (val === '') {
486
+ // Empty value → start nested block
487
+ currentNested = {}
488
+ nestedKey = key
489
+ fm[key] = currentNested
490
+ } else {
491
+ currentNested = null
492
+ fm[key] = val
493
+ }
494
+ }
495
+ }
496
+ return { frontmatter: fm, content: match[2] || '' }
497
+ } catch {
498
+ return { frontmatter: {}, content: raw }
499
+ }
500
+ }
501
+
502
+ private jaccardSimilarity(a: string, b: string): number {
503
+ // Strip punctuation before tokenizing for robust text comparison
504
+ const clean = (s: string) => s.toLowerCase().replace(/[.,!?;:'"(){}[\]\\/#*&%$@]+/g, ' ')
505
+ const tokensA = new Set(
506
+ clean(a)
507
+ .split(/\s+/)
508
+ .filter((t) => t.length > 1),
509
+ )
510
+ const tokensB = new Set(
511
+ clean(b)
512
+ .split(/\s+/)
513
+ .filter((t) => t.length > 1),
514
+ )
515
+ if (tokensA.size === 0 || tokensB.size === 0) return 0
516
+
517
+ let intersection = 0
518
+ for (const t of tokensA) {
519
+ if (tokensB.has(t)) intersection++
520
+ }
521
+ const union = tokensA.size + tokensB.size - intersection
522
+ return union > 0 ? intersection / union : 0
523
+ }
524
+
525
+ private saveDreamLog(timestamp: string, actions: DreamAction[]): void {
526
+ try {
527
+ const dir = join(this.memoryDir, '..')
528
+ mkdirSync(dir, { recursive: true })
529
+
530
+ let log: Array<{ timestamp: string; actions: DreamAction[] }> = []
531
+ if (existsSync(this.dreamLog)) {
532
+ log = JSON.parse(readFileSync(this.dreamLog, 'utf-8'))
533
+ }
534
+ log.unshift({ timestamp, actions })
535
+ // Keep last 10 dream cycles
536
+ if (log.length > 10) log = log.slice(0, 10)
537
+ writeFileSync(this.dreamLog, JSON.stringify(log, null, 2), 'utf-8')
538
+ } catch {
539
+ // Best-effort persistence
540
+ }
541
+ }
542
+
543
+ /** Get dream history for display. */
544
+ getDreamHistory(): Array<{ timestamp: string; actions: DreamAction[] }> {
545
+ try {
546
+ if (!existsSync(this.dreamLog)) return []
547
+ return JSON.parse(readFileSync(this.dreamLog, 'utf-8'))
548
+ } catch {
549
+ return []
550
+ }
551
+ }
552
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Emotion Detector — User Frustration Detection
3
+ *
4
+ * Inspired by Claude Code's emotion detection: parses user input for signals
5
+ * of frustration, confusion, or satisfaction. Adjusts agent behavior accordingly:
6
+ * - Frustration → be more concise, skip explanations, get to the fix faster
7
+ * - Confusion → explain more, offer context, slow down
8
+ * - Satisfaction → maintain current mode
9
+ *
10
+ * Uses regex heuristics (no LLM call) for zero-latency detection.
11
+ * Designed as a lightweight pre-processor in the input pipeline.
12
+ */
13
+
14
+ // ── Types ──
15
+
16
+ export type Emotion =
17
+ | 'frustrated' // User is angry/annoyed — move fast, skip explanations
18
+ | 'confused' // User doesn't understand — explain more
19
+ | 'impatient' // User wants speed — be terse
20
+ | 'satisfied' // User is happy — continue normally
21
+ | 'neutral' // No strong signal
22
+
23
+ export interface EmotionResult {
24
+ emotion: Emotion
25
+ /** 0.0-1.0 confidence in the detected emotion */
26
+ confidence: number
27
+ /** Specific phrases that triggered the detection */
28
+ triggers: string[]
29
+ /** Suggested behavior adjustment for the agent */
30
+ suggestion: string
31
+ }
32
+
33
+ // ── Constants ──
34
+
35
+ /** Patterns that indicate frustration/anger. Ordered by severity. */
36
+ const FRUSTRATION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
37
+ { pattern: /\b(wtf|wtaf)\b/i, weight: 0.9, label: 'profanity' },
38
+ {
39
+ pattern: /\b(useless|worthless|garbage|rubbish|broken)\b/i,
40
+ weight: 0.8,
41
+ label: 'strong-negative',
42
+ },
43
+ { pattern: /\b(damn|shit|crap)\b/i, weight: 0.7, label: 'mild-profanity' },
44
+ {
45
+ pattern: /\b(stop|don't|do not)\s+(explain|talk|lecture|ramble)\b/i,
46
+ weight: 0.85,
47
+ label: 'stop-explaining',
48
+ },
49
+ {
50
+ pattern: /\b(just\s+fix\s+it|just\s+do\s+it|get\s+on\s+with\s+it)\b/i,
51
+ weight: 0.8,
52
+ label: 'just-fix-it',
53
+ },
54
+ {
55
+ pattern: /\b(no|wrong|incorrect|bad|terrible|awful)\s*[!!]{1,3}/i,
56
+ weight: 0.75,
57
+ label: 'emphatic-no',
58
+ },
59
+ { pattern: /\b(again\?|still|not\s+again)\b/i, weight: 0.7, label: 'repeated-failure' },
60
+ { pattern: /\b(i\s+said|i\s+told\s+you|i\s+already)\b/i, weight: 0.75, label: 'i-already-said' },
61
+ { pattern: /\b(no\s+no\s+no|nope\s+nope)\b/i, weight: 0.7, label: 'repeated-no' },
62
+ { pattern: /[!!]{2,}/, weight: 0.6, label: 'multiple-exclamation' },
63
+ {
64
+ pattern: /\b(why|why\s+would\s+you|what\s+are\s+you\s+doing)\b/i,
65
+ weight: 0.65,
66
+ label: 'why-question',
67
+ },
68
+ { pattern: /[??]{2,}/, weight: 0.4, label: 'multiple-question-marks' },
69
+ ]
70
+
71
+ /** Patterns that indicate confusion / need for more explanation */
72
+ const CONFUSION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
73
+ {
74
+ pattern: /\b(i\s+don'?t\s+understand|i\s+am\s+confused|i'?m\s+confused)\b/i,
75
+ weight: 0.9,
76
+ label: 'explicit-confusion',
77
+ },
78
+ {
79
+ pattern: /\b(what\s+does|what\s+is|what\s+are|how\s+does)\b/i,
80
+ weight: 0.6,
81
+ label: 'what-is-question',
82
+ },
83
+ {
84
+ pattern: /\b(can\s+you\s+explain|explain\s+this|elaborate)\b/i,
85
+ weight: 0.7,
86
+ label: 'ask-for-explanation',
87
+ },
88
+ { pattern: /\b(wait|huh|eh\?+|hmm+)\b/i, weight: 0.5, label: 'hesitation' },
89
+ {
90
+ pattern: /\b(i\s+thought|i\s+expected|shouldn'?t\s+it)\b/i,
91
+ weight: 0.6,
92
+ label: 'mismatch-expectation',
93
+ },
94
+ ]
95
+
96
+ /** Patterns that indicate impatience */
97
+ const IMPATIENCE_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
98
+ { pattern: /\b(hurry|quick|fast|faster|asap|urgent)\b/i, weight: 0.8, label: 'speed-words' },
99
+ {
100
+ pattern: /\b(just\s+the\s+(code|answer|result|fix|summary))\b/i,
101
+ weight: 0.75,
102
+ label: 'just-the-result',
103
+ },
104
+ {
105
+ pattern: /\b(skip\s+(the\s+)?(explanation|details|context|background))\b/i,
106
+ weight: 0.8,
107
+ label: 'skip-explanation',
108
+ },
109
+ { pattern: /\b(tldr|tl;dr|summary|summarize)\b/i, weight: 0.7, label: 'tldr' },
110
+ {
111
+ pattern: /\b(get\s+to\s+the\s+point|cut\s+to\s+the\s+chase)\b/i,
112
+ weight: 0.75,
113
+ label: 'get-to-point',
114
+ },
115
+ ]
116
+
117
+ /** Patterns that indicate satisfaction */
118
+ const SATISFACTION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
119
+ { pattern: /\b(thanks|thank\s+you|thx|ty|tyvm)\b/i, weight: 0.5, label: 'thanks' },
120
+ {
121
+ pattern: /\b(great|awesome|perfect|excellent|amazing|brilliant|beautiful)\b/i,
122
+ weight: 0.6,
123
+ label: 'positive-words',
124
+ },
125
+ { pattern: /\b(nice|good\s+job|well\s+done|love\s+it)\b/i, weight: 0.7, label: 'praise' },
126
+ {
127
+ pattern: /\b(exactly|finally|yes!|that'?s\s+it)\b/i,
128
+ weight: 0.6,
129
+ label: 'confirmation-positive',
130
+ },
131
+ { pattern: /[👍🙌🎉✨💯🔥✅]/u, weight: 0.5, label: 'positive-emoji' },
132
+ ]
133
+
134
+ // ── Detector ──
135
+
136
+ export class EmotionDetector {
137
+ /**
138
+ * Analyze user input for emotional signals.
139
+ *
140
+ * @param input — raw user message text
141
+ * @param recentContext — optional recent conversation turns for pattern detection
142
+ * @returns EmotionResult with detected emotion and behavioral suggestion
143
+ */
144
+ detect(input: string, recentContext?: string[]): EmotionResult {
145
+ // Check frustration first (most actionable)
146
+ const frustration = this.scorePatterns(input, FRUSTRATION_PATTERNS, recentContext)
147
+ if (frustration.score >= 0.5) {
148
+ return {
149
+ emotion: 'frustrated',
150
+ confidence: frustration.score,
151
+ triggers: frustration.triggers,
152
+ suggestion:
153
+ frustration.score >= 0.8
154
+ ? 'User is very frustrated. Be extremely concise. Skip ALL explanations. Output only the fix/result. No preamble, no summary, no "here\'s what I did". One sentence max before code.'
155
+ : 'User is frustrated. Be concise. Skip explanations. Get to the fix immediately.',
156
+ }
157
+ }
158
+
159
+ // Check impatience
160
+ const impatience = this.scorePatterns(input, IMPATIENCE_PATTERNS)
161
+ if (impatience.score >= 0.5) {
162
+ return {
163
+ emotion: 'impatient',
164
+ confidence: impatience.score,
165
+ triggers: impatience.triggers,
166
+ suggestion:
167
+ 'User wants speed. Skip explanations. Output the result immediately. No "let me explain why" — just the answer.',
168
+ }
169
+ }
170
+
171
+ // Check confusion
172
+ const confusion = this.scorePatterns(input, CONFUSION_PATTERNS)
173
+ if (confusion.score >= 0.5) {
174
+ return {
175
+ emotion: 'confused',
176
+ confidence: confusion.score,
177
+ triggers: confusion.triggers,
178
+ suggestion:
179
+ 'User is confused. Explain more. Offer context. Break down complex steps. Ask clarifying questions rather than assuming.',
180
+ }
181
+ }
182
+
183
+ // Check satisfaction
184
+ const satisfaction = this.scorePatterns(input, SATISFACTION_PATTERNS)
185
+ if (satisfaction.score >= 0.5) {
186
+ return {
187
+ emotion: 'satisfied',
188
+ confidence: satisfaction.score,
189
+ triggers: satisfaction.triggers,
190
+ suggestion:
191
+ 'User is satisfied. Maintain current tone and pace. Good time for optional suggestions.',
192
+ }
193
+ }
194
+
195
+ return {
196
+ emotion: 'neutral',
197
+ confidence: 0,
198
+ triggers: [],
199
+ suggestion: 'No strong emotional signal. Proceed normally.',
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Quick check — does this input contain ANY frustration signal?
205
+ * Use as a pre-filter before the full detect() call.
206
+ */
207
+ isFrustrated(input: string): boolean {
208
+ return FRUSTRATION_PATTERNS.some((p) => p.pattern.test(input))
209
+ }
210
+
211
+ /**
212
+ * Detect repeated frustration across multiple turns (escalation detection).
213
+ */
214
+ detectEscalation(recentInputs: string[]): boolean {
215
+ if (recentInputs.length < 2) return false
216
+ const recent = recentInputs.slice(-3)
217
+ const frustrationCount = recent.filter((i) => this.isFrustrated(i)).length
218
+ return frustrationCount >= 2
219
+ }
220
+
221
+ // ── Internal ──
222
+
223
+ private scorePatterns(
224
+ input: string,
225
+ patterns: Array<{ pattern: RegExp; weight: number; label: string }>,
226
+ recentContext?: string[],
227
+ ): { score: number; triggers: string[] } {
228
+ const triggers: string[] = []
229
+ let totalWeight = 0
230
+ let matchCount = 0
231
+
232
+ for (const p of patterns) {
233
+ if (p.pattern.test(input)) {
234
+ triggers.push(p.label)
235
+ totalWeight += p.weight
236
+ matchCount++
237
+ }
238
+ }
239
+
240
+ // Check recent context for frustration escalation
241
+ if (recentContext && patterns === FRUSTRATION_PATTERNS) {
242
+ const recentFrustration = recentContext
243
+ .slice(-2)
244
+ .filter((ctx) => patterns.some((p) => p.pattern.test(ctx))).length
245
+ if (recentFrustration >= 1) {
246
+ totalWeight += 0.15 // escalation bonus
247
+ }
248
+ }
249
+
250
+ // Normalize: first match passes at weight ≥ 0.5; additional matches increase confidence
251
+ // with diminishing returns (matchCount-1 so single match = no dampening)
252
+ const score =
253
+ matchCount === 0 ? 0 : Math.min(1.0, totalWeight / (1 + Math.max(0, matchCount - 1) * 0.5))
254
+
255
+ return { score, triggers }
256
+ }
257
+ }
@@ -15,6 +15,7 @@ import type { AgentRegistry } from '../agent/agent-registry'
15
15
  import { analyzeForMemory } from './memory/memory-writer'
16
16
  import { getMemoryManager } from './memory/memory-loader'
17
17
  import { AutoMemoryEngine } from './auto-memory.js'
18
+ import type { ToolCallRecord } from './auto-memory.js'
18
19
  import type { AgentViewManager } from '../agent-view/agent-view-manager'
19
20
  import type { SkillsLoader } from '../skills/loader'
20
21
  import { getBackgroundAgentRegistry } from '../agent/background-registry'
@@ -26,6 +27,7 @@ import { ErrorSignatureDB } from './error-signature-db.js'
26
27
  import { PreFlightChecker } from './preflight-checker.js'
27
28
  import { AutoCorrector } from './auto-corrector.js'
28
29
  import { MetaRuleEngine } from './meta-rule-engine.js'
30
+ import { DreamEngine } from './dream-engine.js'
29
31
  import { UsageTracker } from './usage-tracker'
30
32
  import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
31
33
  import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
@@ -55,6 +57,7 @@ export class QueryEngine {
55
57
  private _preflightChecker?: PreFlightChecker
56
58
  private _autoCorrector?: AutoCorrector
57
59
  private _metaRuleEngine?: MetaRuleEngine
60
+ private _dreamEngine?: DreamEngine
58
61
  /** Files read this session — tracks what the Read tool has loaded */
59
62
  private readFiles = new Set<string>()
60
63
  private goal?: string
@@ -428,6 +431,7 @@ export class QueryEngine {
428
431
  let thinkingContent = ''
429
432
  let turnApiInputTokens = 0
430
433
  let turnApiOutputTokens = 0
434
+ const turnStart = Date.now()
431
435
  const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
432
436
 
433
437
  // Stream model response
@@ -534,7 +538,9 @@ export class QueryEngine {
534
538
  }
535
539
 
536
540
  // Execute any tools that were requested
541
+ const toolCallRecords: ToolCallRecord[] = []
537
542
  for (const toolUse of toolUses) {
543
+ const toolStart = Date.now()
538
544
  const result = await this.executeTool(toolUse.name, toolUse.input)
539
545
  yield {
540
546
  type: 'tool_result',
@@ -542,6 +548,15 @@ export class QueryEngine {
542
548
  content: result.success ? result.content : result.error || result.content,
543
549
  }
544
550
 
551
+ // Collect tool call record for CRSI auto-reflection
552
+ toolCallRecords.push({
553
+ name: toolUse.name,
554
+ input: toolUse.input,
555
+ success: result.success,
556
+ error: result.success ? undefined : result.error || 'Unknown error',
557
+ durationMs: Date.now() - toolStart,
558
+ })
559
+
545
560
  // Add tool use + result to context
546
561
  // DeepSeek V4 thinking mode requires reasoning_content on every assistant message
547
562
  this.context.addMessage({
@@ -561,6 +576,28 @@ export class QueryEngine {
561
576
  })
562
577
  }
563
578
 
579
+ // ── CRSI Reflection: Wire AutoMemoryEngine.analyzeTurn() into main loop ──
580
+ // This is the primary CRSI learning mechanism — every turn's tool calls,
581
+ // successes, and failures are analyzed for patterns and fed into the
582
+ // CRSI pipeline (PatternAnalyzer → ExperienceRuleEngine → ErrorSignatureDB).
583
+ if (toolCallRecords.length > 0) {
584
+ try {
585
+ const autoMemory = this.getAutoMemory()
586
+ const reflection = autoMemory.analyzeTurn({
587
+ sessionId: this.sessionId,
588
+ userMessage: userInput,
589
+ assistantContent,
590
+ toolCalls: toolCallRecords,
591
+ modelProvider: this.registry.getActive().config.id,
592
+ modelId: this.registry.getActiveModel(),
593
+ turnDurationMs: turnStart ? Date.now() - turnStart : 0,
594
+ })
595
+ autoMemory.persist(reflection)
596
+ } catch {
597
+ // Reflection is non-critical — failures must not break the main loop
598
+ }
599
+ }
600
+
564
601
  // Record API token usage for this turn, attributed to executed tools
565
602
  if (turnApiInputTokens > 0 || turnApiOutputTokens > 0) {
566
603
  if (toolUses.length > 0) {
@@ -1141,6 +1178,14 @@ export class QueryEngine {
1141
1178
  return this._metaRuleEngine
1142
1179
  }
1143
1180
 
1181
+ /** Auto-Dream: Lazily-initialized background memory consolidation engine. */
1182
+ getDreamEngine(): DreamEngine {
1183
+ if (!this._dreamEngine) {
1184
+ this._dreamEngine = new DreamEngine()
1185
+ }
1186
+ return this._dreamEngine
1187
+ }
1188
+
1144
1189
  /** Register a tool dynamically (used by MCP auto-registration). */
1145
1190
  registerTool(tool: ToolDefinition): void {
1146
1191
  if (this.tools.has(tool.name)) {
package/src/ui/app.tsx CHANGED
@@ -449,6 +449,24 @@ export function App({
449
449
  }
450
450
  }
451
451
 
452
+ // ── Emotion detection: adjust behavior based on user's emotional state ──
453
+ // Uses regex heuristics (zero-latency) to detect frustration/impatience/confusion.
454
+ // When frustrated, prepends a terseness instruction to the user input so the
455
+ // AI model skips explanations and gets straight to the fix.
456
+ let emotionPrefix = ''
457
+ try {
458
+ const { EmotionDetector } = await import('../core/emotion-detector.js')
459
+ const detector = new EmotionDetector()
460
+ const result = detector.detect(input)
461
+ if (result.emotion === 'frustrated' || result.emotion === 'impatient') {
462
+ emotionPrefix = `[SYSTEM NOTE: The user is ${result.emotion}. Be extremely concise. Skip all explanations, preambles, and summaries. Output only the fix/result. No "here's what I did" or "let me explain". One sentence maximum before code.]\n\n`
463
+ } else if (result.emotion === 'confused') {
464
+ emotionPrefix = `[SYSTEM NOTE: The user seems confused. Explain more thoroughly, break down complex steps, and offer clarifying questions rather than assuming understanding.]\n\n`
465
+ }
466
+ } catch {
467
+ // Emotion detection is non-critical — fail silently
468
+ }
469
+
452
470
  // ── Normal message processing (AI chat) ──
453
471
  setMessages((prev) => [...prev, { role: 'user', content: input }])
454
472
  setIsLoading(true)
@@ -493,7 +511,10 @@ export function App({
493
511
  }
494
512
 
495
513
  try {
496
- for await (const chunk of engine.process(input, controller.signal)) {
514
+ for await (const chunk of engine.process(
515
+ emotionPrefix ? emotionPrefix + input : input,
516
+ controller.signal,
517
+ )) {
497
518
  // Reasoning content (DeepSeek V4 thinking mode) — silently consumed,
498
519
  // not shown to user to avoid noise.
499
520
  if (chunk.reasoning_content) {
@@ -929,6 +929,234 @@ const sisCleanupCmd: CommandHandler = async (ctx) => {
929
929
  return { content: lines.join('\n') }
930
930
  }
931
931
 
932
+ // ═══════════════════════════════════════════════════════════════
933
+ // Auto-Dream: Background Memory Consolidation
934
+ // ═══════════════════════════════════════════════════════════════
935
+
936
+ const dreamCmd: CommandHandler = async (ctx, args) => {
937
+ const t = resolveT(ctx)
938
+ const aggressive = args.includes('--aggressive') || args.includes('-a')
939
+ const status = args.includes('--status') || args.includes('-s')
940
+
941
+ const engine = ctx.engine.getDreamEngine?.()
942
+ if (!engine) {
943
+ return { content: 'DreamEngine 未初始化。请确认 Mipham Code 版本 >= v0.34.0。' }
944
+ }
945
+
946
+ if (status) {
947
+ const history = engine.getDreamHistory()
948
+ if (history.length === 0) {
949
+ return { content: '🌙 尚未运行过 Auto-Dream。使用 `/dream` 手动触发首次梦境整合。' }
950
+ }
951
+ const lines: string[] = ['## 🌙 Auto-Dream 历史', '']
952
+ for (const entry of history.slice(0, 5)) {
953
+ const ts = new Date(entry.timestamp).toLocaleString('zh-CN')
954
+ const total = entry.actions.length
955
+ const applied = entry.actions.filter((a) => a.autoApplied).length
956
+ const flagged = entry.actions.filter((a) => !a.autoApplied).length
957
+ lines.push(`- **${ts}**: ${total} 行动 (${applied} 自动应用, ${flagged} 标记审查)`)
958
+ }
959
+ return { content: lines.join('\n') }
960
+ }
961
+
962
+ const report = engine.dream({ aggressive })
963
+ const lines: string[] = [
964
+ '## 🌙 Auto-Dream 完成',
965
+ '',
966
+ `记忆整合: ${report.beforeCount} → ${report.afterCount}`,
967
+ ...(report.phases.deduplicated > 0 ? [`- 去重: ${report.phases.deduplicated} 条`] : []),
968
+ ...(report.phases.contradictionsFound > 0
969
+ ? [`- 矛盾冲突: ${report.phases.contradictionsFound} 处 (需人工裁决)`]
970
+ : []),
971
+ ...(report.phases.merged > 0 ? [`- 合并: ${report.phases.merged} 组`] : []),
972
+ ...(report.phases.solidified > 0
973
+ ? [`- 模糊标记: ${report.phases.solidified} 条 (建议固化)`]
974
+ : []),
975
+ ...(report.phases.pruned > 0 ? [`- 清理过期: ${report.phases.pruned} 条`] : []),
976
+ '',
977
+ ]
978
+
979
+ const flagged = report.actions.filter((a) => !a.autoApplied)
980
+ if (flagged.length > 0 && !aggressive) {
981
+ lines.push(`⚠️ ${flagged.length} 项待审查。使用 \`/dream --aggressive\` 自动应用所有操作。`)
982
+ lines.push('')
983
+ for (const action of flagged) {
984
+ lines.push(`- [ ] **${action.type}**: ${action.description}`)
985
+ }
986
+ }
987
+
988
+ if (report.actions.length === 0) {
989
+ lines.push('✅ 记忆干净,无需整理。')
990
+ }
991
+
992
+ return { content: lines.join('\n') }
993
+ }
994
+
995
+ // ═══════════════════════════════════════════════════════════════
996
+ // Bug Report
997
+ // ═══════════════════════════════════════════════════════════════
998
+
999
+ const bugReportCmd: CommandHandler = async (ctx) => {
1000
+ const t = resolveT(ctx)
1001
+ const engine = ctx.engine
1002
+ const lines: string[] = [
1003
+ '## 🐛 Bug Report',
1004
+ '',
1005
+ '> Copy the report below and paste it into your GitHub Issue.',
1006
+ '',
1007
+ '---',
1008
+ '',
1009
+ ]
1010
+
1011
+ // ── Version & Runtime ──
1012
+ lines.push('### Environment', '')
1013
+ lines.push(`- **Mipham Code**: v${ctx.version}`)
1014
+ lines.push(`- **Session**: ${ctx.sessionId}`)
1015
+ lines.push(`- **Node**: ${process.version}`)
1016
+ lines.push(`- **Platform**: ${process.platform} ${process.arch}`)
1017
+ lines.push(
1018
+ `- **OS**: ${process.platform === 'darwin' ? 'macOS' : process.platform === 'linux' ? 'Linux' : 'Windows'}`,
1019
+ )
1020
+ lines.push(`- **Provider**: ${ctx.providerId}`)
1021
+ lines.push(`- **Model**: ${ctx.modelId}`)
1022
+ lines.push('')
1023
+
1024
+ // ── SIS errors ──
1025
+ try {
1026
+ const db = engine.getErrorSignatureDB?.()
1027
+ if (db) {
1028
+ const active = db.getActive()
1029
+ if (active.length > 0) {
1030
+ lines.push('### Active Error Signatures', '')
1031
+ for (const sig of active.slice(0, 10)) {
1032
+ lines.push(
1033
+ `- \`${sig.id}\`: ${sig.pattern} (${sig.category}, ${sig.occurrences}x, ${(sig.successRate * 100).toFixed(0)}% success)`,
1034
+ )
1035
+ }
1036
+ lines.push('')
1037
+ }
1038
+ }
1039
+ } catch {
1040
+ // SIS unavailable
1041
+ }
1042
+
1043
+ // ── Hook health ──
1044
+ try {
1045
+ const hookEngine = engine.getHookEngine?.()
1046
+ if (hookEngine) {
1047
+ const health = hookEngine.getHookHealth()
1048
+ const disabled = health.filter((h) => h.health.disabled)
1049
+ if (disabled.length > 0) {
1050
+ lines.push('### Disabled Hooks', '')
1051
+ for (const entry of disabled) {
1052
+ lines.push(
1053
+ `- \`${entry.key}\`: ${entry.health.failures} failures, disabled at ${new Date(entry.health.disabledAt).toISOString()}`,
1054
+ )
1055
+ }
1056
+ lines.push('')
1057
+ }
1058
+ }
1059
+ } catch {
1060
+ // Hook health unavailable
1061
+ }
1062
+
1063
+ // ── Recent dream log ──
1064
+ try {
1065
+ const dreamEngine = engine.getDreamEngine?.()
1066
+ if (dreamEngine) {
1067
+ const history = dreamEngine.getDreamHistory()
1068
+ if (history.length > 0) {
1069
+ const last = history[0]!
1070
+ lines.push('### Last Auto-Dream', '')
1071
+ lines.push(`- **Ran at**: ${last.timestamp}`)
1072
+ lines.push(
1073
+ `- **Actions**: ${last.actions.length} (${last.actions.filter((a: { autoApplied: boolean }) => a.autoApplied).length} applied)`,
1074
+ )
1075
+ lines.push('')
1076
+ }
1077
+ }
1078
+ } catch {
1079
+ // Dream engine unavailable
1080
+ }
1081
+
1082
+ // ── Steps to reproduce (template) ──
1083
+ lines.push('### Steps to Reproduce', '')
1084
+ lines.push('1. ')
1085
+ lines.push('2. ')
1086
+ lines.push('3. ')
1087
+ lines.push('')
1088
+ lines.push('### Expected Behavior', '')
1089
+ lines.push('')
1090
+ lines.push('### Actual Behavior', '')
1091
+ lines.push('')
1092
+ lines.push('---')
1093
+ lines.push(`🤖 Generated with Mipham Code v${ctx.version}`)
1094
+
1095
+ return { content: lines.join('\n'), copyContent: lines.join('\n') }
1096
+ }
1097
+
1098
+ // ═══════════════════════════════════════════════════════════════
1099
+ // Feedback
1100
+ // ═══════════════════════════════════════════════════════════════
1101
+ // Changelog
1102
+ // ═══════════════════════════════════════════════════════════════
1103
+
1104
+ const changelogCmd: CommandHandler = async (ctx) => {
1105
+ const t = resolveT(ctx)
1106
+ const lines: string[] = ['## 📋 Changelog', '']
1107
+
1108
+ try {
1109
+ const { execSync } = await import('node:child_process')
1110
+ const tags = execSync('git tag --sort=-creatordate', { encoding: 'utf-8', timeout: 5000 })
1111
+ .trim()
1112
+ .split('\n')
1113
+ .filter(Boolean)
1114
+ .slice(0, 15)
1115
+
1116
+ if (tags.length === 0) {
1117
+ lines.push('No version tags found.')
1118
+ return { content: lines.join('\n') }
1119
+ }
1120
+
1121
+ lines.push(`Current: **v${ctx.version}**`, '')
1122
+
1123
+ for (let i = 0; i < tags.length; i++) {
1124
+ const tag = tags[i]!
1125
+ const nextTag = tags[i + 1]
1126
+ try {
1127
+ const range = nextTag ? `${nextTag}..${tag}` : tag
1128
+ const log = execSync(
1129
+ `git log --oneline --no-merges ${range} --format="%s" 2>/dev/null | head -8`,
1130
+ { encoding: 'utf-8', timeout: 5000 },
1131
+ )
1132
+ .trim()
1133
+ .split('\n')
1134
+ .filter(Boolean)
1135
+
1136
+ const isCurrent = tag === `v${ctx.version}`
1137
+ lines.push(`### ${tag}${isCurrent ? ' ← current' : ''}`, '')
1138
+ if (log.length > 0) {
1139
+ for (const entry of log) {
1140
+ lines.push(`- ${entry}`)
1141
+ }
1142
+ } else {
1143
+ lines.push(' _(release tag only)_')
1144
+ }
1145
+ lines.push('')
1146
+ } catch {
1147
+ lines.push(`### ${tag}`, '', ' _(details unavailable)_', '')
1148
+ }
1149
+ }
1150
+ } catch {
1151
+ lines.push('Unable to read git history. Make sure you are in the Mipham Code repository.')
1152
+ }
1153
+
1154
+ lines.push('---')
1155
+ lines.push(`Full history: \`git log --oneline\``)
1156
+
1157
+ return { content: lines.join('\n') }
1158
+ }
1159
+
932
1160
  // ═══════════════════════════════════════════════════════════════
933
1161
  // Workflow
934
1162
  // ═══════════════════════════════════════════════════════════════
@@ -2147,25 +2375,69 @@ const statsCmd: CommandHandler = (ctx) => {
2147
2375
  const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
2148
2376
  const systemMsgs = msgs.filter((m) => m.role === 'system').length
2149
2377
 
2150
- return {
2151
- content: [
2152
- t('commands.stats.title'),
2153
- '',
2154
- t('commands.stats.messages', {
2155
- total: String(msgs.length),
2156
- user: String(userMsgs),
2157
- assistant: String(assistantMsgs),
2158
- system: String(systemMsgs),
2159
- }),
2160
- t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
2161
- t('commands.stats.tools', { count: String(tools.size) }),
2162
- t('commands.stats.provider', { provider: ctx.providerId }),
2163
- t('commands.stats.model', { model: ctx.modelId }),
2164
- t('commands.stats.permission', { permission: ctx.config.permission }),
2165
- '',
2166
- t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
2167
- ].join('\n'),
2378
+ const base = [
2379
+ t('commands.stats.title'),
2380
+ '',
2381
+ t('commands.stats.messages', {
2382
+ total: String(msgs.length),
2383
+ user: String(userMsgs),
2384
+ assistant: String(assistantMsgs),
2385
+ system: String(systemMsgs),
2386
+ }),
2387
+ t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
2388
+ t('commands.stats.tools', { count: String(tools.size) }),
2389
+ t('commands.stats.provider', { provider: ctx.providerId }),
2390
+ t('commands.stats.model', { model: ctx.modelId }),
2391
+ t('commands.stats.permission', { permission: ctx.config.permission }),
2392
+ '',
2393
+ t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
2394
+ ]
2395
+
2396
+ // ── CRSI & SIS extensions ──
2397
+ const engine = ctx.engine
2398
+ const extras: string[] = []
2399
+
2400
+ try {
2401
+ const db = engine.getErrorSignatureDB?.()
2402
+ if (db) {
2403
+ const s = db.getStats()
2404
+ if (s.total > 0) {
2405
+ extras.push(
2406
+ '',
2407
+ '── SIS Self-Immune ──',
2408
+ `Signatures: ${s.total} total, ${s.active} active | Success rate: ${(s.avgSuccessRate * 100).toFixed(0)}%`,
2409
+ )
2410
+ }
2411
+ }
2412
+ } catch {
2413
+ /* SIS not initialized */
2168
2414
  }
2415
+
2416
+ try {
2417
+ const autoMemory = engine.getAutoMemory?.()
2418
+ if (autoMemory) {
2419
+ const count = autoMemory.sessionReflectionCount ?? 0
2420
+ if (count > 0) extras.push(`CRSI Reflections: ${count} turn(s) analyzed`)
2421
+ }
2422
+ } catch {
2423
+ /* CRSI not initialized */
2424
+ }
2425
+
2426
+ try {
2427
+ const meta = engine.getMetaRuleEngine?.()
2428
+ if (meta) {
2429
+ const analysis = meta.analyze()
2430
+ if (analysis.systemHealth) {
2431
+ extras.push(
2432
+ `System Health: ${analysis.systemHealth.score}/100 — ${analysis.systemHealth.assessment}`,
2433
+ )
2434
+ }
2435
+ }
2436
+ } catch {
2437
+ /* Meta not initialized */
2438
+ }
2439
+
2440
+ return { content: [...base, ...extras].join('\n') }
2169
2441
  }
2170
2442
 
2171
2443
  const summaryCmd: CommandHandler = (ctx) => {
@@ -3157,31 +3429,57 @@ To switch providers without clearing keys, use /switch <provider> <model>.`,
3157
3429
  // Phase 4 — Feedback
3158
3430
  // ═══════════════════════════════════════════════════════════════
3159
3431
 
3160
- const feedbackCmd: CommandHandler = (ctx, args) => {
3432
+ const feedbackCmd: CommandHandler = async (ctx, args) => {
3161
3433
  const message = args.join(' ').trim()
3162
3434
 
3163
3435
  const lines: string[] = ['── Feedback ──', '']
3164
3436
 
3437
+ // If a message was provided, save it to ~/.mipham/feedback/
3165
3438
  if (message) {
3166
- lines.push('Your feedback:')
3167
- lines.push('')
3168
- lines.push(' """')
3169
- for (const line of message.split('\n')) {
3170
- lines.push(' ' + line)
3439
+ try {
3440
+ const { writeFileSync, mkdirSync, existsSync } = await import('node:fs')
3441
+ const { join } = await import('node:path')
3442
+ const { homedir } = await import('node:os')
3443
+
3444
+ const dir = join(homedir(), '.mipham', 'feedback')
3445
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
3446
+
3447
+ const ts = new Date().toISOString().replace(/[:.]/g, '-')
3448
+ const file = join(dir, `feedback-${ts}.md`)
3449
+ const content = [
3450
+ '---',
3451
+ `timestamp: ${new Date().toISOString()}`,
3452
+ `version: ${ctx.version}`,
3453
+ `session: ${ctx.sessionId}`,
3454
+ `provider: ${ctx.providerId}`,
3455
+ `model: ${ctx.modelId}`,
3456
+ '---',
3457
+ '',
3458
+ message,
3459
+ ].join('\n')
3460
+ writeFileSync(file, content, 'utf-8')
3461
+
3462
+ lines.push('Your feedback:')
3463
+ lines.push('')
3464
+ lines.push(' """')
3465
+ for (const line of message.split('\n')) {
3466
+ lines.push(' ' + line)
3467
+ }
3468
+ lines.push(' """')
3469
+ lines.push('')
3470
+ lines.push(`✅ Saved to \`${file}\``)
3471
+ lines.push('')
3472
+ } catch (err) {
3473
+ lines.push(`⚠️ Save failed: ${String(err)}`)
3474
+ lines.push('')
3171
3475
  }
3172
- lines.push(' """')
3173
- lines.push('')
3174
- lines.push('── Preview Complete ──')
3175
- lines.push('')
3176
- lines.push('Copy the above and submit via one of the channels below.')
3177
- lines.push('')
3178
3476
  }
3179
3477
 
3180
3478
  lines.push('── Feedback Channels ──')
3181
3479
  lines.push('')
3182
3480
  lines.push(' 🐛 Bug Reports')
3183
3481
  lines.push(' GitHub Issues: https://github.com/One-Mipham/mipham-code/issues')
3184
- lines.push(' Template: Bug Report (include version + reproduction steps)')
3482
+ lines.push(' Or run `/bug-report` to auto-generate a diagnostic report')
3185
3483
  lines.push('')
3186
3484
  lines.push(' 💡 Feature Requests')
3187
3485
  lines.push(' GitHub Issues: https://github.com/One-Mipham/mipham-code/issues')
@@ -3562,6 +3860,9 @@ const commandsListCmd: CommandHandler = () => {
3562
3860
  '/recap': 'Session & Identity',
3563
3861
  '/export': 'Session & Identity',
3564
3862
  '/doctor': 'Session & Identity',
3863
+ '/dream': 'Session & Identity',
3864
+ '/bug-report': 'Session & Identity',
3865
+ '/changelog': 'Session & Identity',
3565
3866
  '/resume': 'Session & Identity',
3566
3867
  '/resume last': 'Session & Identity',
3567
3868
  '/resume delete': 'Session & Identity',
@@ -3757,6 +4058,9 @@ registry.set('/sis errors', sisErrorsCmd)
3757
4058
  registry.set('/sis stats', sisStatsCmd)
3758
4059
  registry.set('/sis clear', sisClearCmd)
3759
4060
  registry.set('/sis cleanup', sisCleanupCmd)
4061
+ registry.set('/dream', dreamCmd)
4062
+ registry.set('/bug-report', bugReportCmd)
4063
+ registry.set('/changelog', changelogCmd)
3760
4064
 
3761
4065
  // Workflow
3762
4066
  registry.set('/plan', planCmd)
@@ -3877,6 +4181,9 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
3877
4181
  '/recap': 'Summarize session so far',
3878
4182
  '/export': 'Export conversation to file',
3879
4183
  '/doctor': 'System diagnostics',
4184
+ '/dream': 'Background memory consolidation',
4185
+ '/bug-report': 'Generate diagnostic report for GitHub Issues',
4186
+ '/changelog': 'Recent version history',
3880
4187
  '/resume': 'List saved sessions',
3881
4188
  '/resume last': 'Restore the most recent saved session',
3882
4189
  '/resume delete': 'Delete a saved session',