@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 +1 -1
- package/src/core/dream-engine.ts +552 -0
- package/src/core/emotion-detector.ts +257 -0
- package/src/core/engine.ts +45 -0
- package/src/ui/app.tsx +22 -1
- package/src/ui/commands.ts +338 -31
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/core/engine.ts
CHANGED
|
@@ -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(
|
|
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) {
|
package/src/ui/commands.ts
CHANGED
|
@@ -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
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
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('
|
|
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',
|